Skip to content

chore: align service shells + shared @tale/webui utilities#1701

Closed
yannickmonney wants to merge 2 commits into
mainfrom
chore/align-service-shells
Closed

chore: align service shells + shared @tale/webui utilities#1701
yannickmonney wants to merge 2 commits into
mainfrom
chore/align-service-shells

Conversation

@yannickmonney

@yannickmonney yannickmonney commented May 11, 2026

Copy link
Copy Markdown
Contributor

Cross-service alignment + bundle of pre-existing WIP. Reduces boilerplate across services/platform, services/web, services/docs and pulls common patterns into @tale/ui and @tale/webui.

Service-shell alignment

  • One-liner services/*/app/globals.css (all three import @tale/webui/globals.css)
  • Service-local CSS moved to sibling app/locals.css imported alongside globals
  • main.tsx files share the same shape (router import, providers around RouterProvider, identical root/error string, same import order). Platform's eight-provider stack lifted out of __root.tsx into main.tsx
  • router.tsx files share the same module-level export pattern; platform's QueryClient + ConvexQueryClient construction lifted to module scope
  • tailwind.config.ts, postcss.config.mjs, tsr.config.json, tsconfig.json byte-identical across the three services
  • services/*/lib/i18n/i18n.ts collapsed via new initServiceI18n helper

Shared utilities pulled into packages

  • @tale/ui/globals.css is the canonical stylesheet; tokens, base layer, animations, json-diff-kit and react-flow overrides all live here
  • @fontsource/inter bundled at @tale/ui (one source of truth for the body font); removed per-service @fontsource imports
  • @tale/ui/i18n/init-service composes initI18n + collectRegionalBundles
  • @tale/webui/server exports startSimpleServer used by services/web and services/docs; covers locale negotiation, /api/health (with optional graceful-drain marker), shared security headers via defaultSimpleSecurityHeaders, and an extraRoutes hook for service-only endpoints (web's Discord form proxy, etc.). Platform's Hono-based server keeps its specifics (CSP nonce injection, file-events SSE, branding, canvas preview) but its /api/health body shape was aligned to match

Other

  • components.json moved to repo root; aliases now point at packages/ui
  • Plop templates updated to emit the aligned shape (locals.css, router.tsx with declare-module block, initServiceI18n-based i18n.ts, identical tsconfig/tsr/tailwind/postcss)
  • Pre-existing WIP bundled in: webui search test suite, rehype-preserve- code-meta plugin, theme-asset-sync component + ThemeAssets refactor, storybook cleanup, favicon rename, translation edits

Pre-PR checklist

  • Ran `bun run check` (format, lint, typecheck, all tests)
  • Updated services/platform/messages/{en,de,fr}.json — N/A (no user-facing platform copy changed)
  • Updated /docs/{en,de,fr}/ for every user-visible change — translation edits bundled
  • Ran `bun run --filter @tale/docs lint` and `bun run --filter @tale/docs test` — covered by `bun run check`
  • Updated README.md, README.de.md, README.fr.md — N/A

Summary

Pre-merge checklist

Tick each box or mark N/A with a short reason. Empty boxes are a blocker.

  • Ran bun run check (format, lint, typecheck, all tests).
  • Updated services/platform/messages/{en,de,fr}.json for any new/renamed/removed keys — or N/A.
  • Updated docs/, docs/de/, and docs/fr/ for every user-visible change — or N/A.
  • Ran bun run --filter @tale/docs lint (broken links + oxlint) — or N/A.
  • Updated README.md, README.de.md, README.fr.md if the change affects what they document — or N/A.
Does this change need docs and translations? (decision tree)

Walk top-down. First yes wins.

  • Did you add, rename, or remove a key in services/platform/messages/? → Yes.
  • Did you add, change, or remove a UI element a user can click, see, or read? → Yes.
  • Did you add, rename, remove, or change the default of an env var, CLI flag, config file key, or API field? → Yes.
  • Did you change error wording, validation rules, or rate limits a user can hit? → Yes.
  • Pure refactor, internal type, test, build script, or comment? → No. Note the scope in the commit body.

If unsure, default to yes. Reviewer time is cheaper than stale docs.

Test plan

Summary by CodeRabbit

Release Notes

  • New Features

    • Enhanced search with improved relevance ranking, breadcrumb navigation, and section grouping.
    • Search dialog now shows helpful "keep typing" hint for short queries and preloads when opened.
  • Documentation

    • Updated German and French documentation with new translations and localized content.
    • Clarified user preference interface: Account settings now located in bottom-left corner.
    • Improved file type tables with consistent formatting across all languages.
  • Improvements

    • Better dark/light mode theme synchronization across browser tabs and favicons.
    • Enhanced search snippet extraction and result display.

Review Change Stack

Cross-service alignment + bundle of pre-existing WIP. Reduces boilerplate
across services/platform, services/web, services/docs and pulls common
patterns into @tale/ui and @tale/webui.

Service-shell alignment
- One-liner services/*/app/globals.css (all three import @tale/webui/globals.css)
- Service-local CSS moved to sibling app/locals.css imported alongside globals
- main.tsx files share the same shape (router import, providers around
  RouterProvider, identical root/error string, same import order). Platform's
  eight-provider stack lifted out of __root.tsx into main.tsx
- router.tsx files share the same module-level export pattern; platform's
  QueryClient + ConvexQueryClient construction lifted to module scope
- tailwind.config.ts, postcss.config.mjs, tsr.config.json, tsconfig.json
  byte-identical across the three services
- services/*/lib/i18n/i18n.ts collapsed via new initServiceI18n helper

Shared utilities pulled into packages
- @tale/ui/globals.css is the canonical stylesheet; tokens, base layer,
  animations, json-diff-kit and react-flow overrides all live here
- @fontsource/inter bundled at @tale/ui (one source of truth for the body
  font); removed per-service @fontsource imports
- @tale/ui/i18n/init-service composes initI18n + collectRegionalBundles
- @tale/webui/server exports startSimpleServer used by services/web and
  services/docs; covers locale negotiation, /api/health (with optional
  graceful-drain marker), shared security headers via
  defaultSimpleSecurityHeaders, and an extraRoutes hook for service-only
  endpoints (web's Discord form proxy, etc.). Platform's Hono-based server
  keeps its specifics (CSP nonce injection, file-events SSE, branding,
  canvas preview) but its /api/health body shape was aligned to match

Other
- components.json moved to repo root; aliases now point at packages/ui
- Plop templates updated to emit the aligned shape (locals.css, router.tsx
  with declare-module block, initServiceI18n-based i18n.ts, identical
  tsconfig/tsr/tailwind/postcss)
- Pre-existing WIP bundled in: webui search test suite, rehype-preserve-
  code-meta plugin, theme-asset-sync component + ThemeAssets refactor,
  storybook cleanup, favicon rename, translation edits

Pre-PR checklist
- [x] Ran \`bun run check\` (format, lint, typecheck, all tests)
- [x] Updated services/platform/messages/{en,de,fr}.json — N/A (no user-facing platform copy changed)
- [x] Updated /docs/{en,de,fr}/ for every user-visible change — translation edits bundled
- [x] Ran \`bun run --filter @tale/docs lint\` and \`bun run --filter @tale/docs test\` — covered by \`bun run check\`
- [x] Updated README.md, README.de.md, README.fr.md — N/A
@coderabbitai

coderabbitai Bot commented May 11, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Unifies global styling by importing shared globals.css across services, introduces ThemeAssetSync, and switches Tailwind presets. Refactors i18n initialization to initServiceI18n. Overhauls docs search: index options, markdown stripping, reranking, UI behaviors, and extensive tests. Adds a shared Bun SPA server utility and adopts it in docs/web servers. Updates Storybook/Vitest configs (multi-project, addon-vitest), router setup in platform, and scaffolding templates. Minor docs localization/formatting tweaks.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • tale-project/tale#1692: Also restructures markdown/shiki/CodeGroup usage by extracting to @tale/markdown, closely touching areas modified here.
  • tale-project/tale#1693: Adjusts platform provider settings and models UI, related to UI theming and component changes present in this PR.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chore/align-service-shells

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
services/docs/app/components/docs/docs-toc.tsx (1)

95-95: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add explicit list semantics to TOC <ul>.

Please add role="list" to the <ul> so VoiceOver/Safari consistently exposes list semantics under Tailwind Preflight.

Suggested patch
-      <ul className="flex flex-col">
+      <ul role="list" className="flex flex-col">

Based on learnings: “do not remove the role="list" attribute from ul elements… Safari/VoiceOver requires an explicit role="list" for proper semantics.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/docs/app/components/docs/docs-toc.tsx` at line 95, The TOC unordered
list element (<ul className="flex flex-col">) in the docs-toc.tsx component is
missing explicit list semantics; update that <ul> (in the Docs TOC component) to
include role="list" so VoiceOver/Safari expose list semantics consistently under
Tailwind Preflight—add the role attribute to the existing <ul className="flex
flex-col"> element.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/ui/src/globals.css`:
- Around line 263-264: Normalize the CSS to satisfy stylelint by removing the
unnecessary quotes around the Inter family and lowercasing the text-rendering
keyword: change font-family: 'Inter', ui-sans-serif, system-ui, sans-serif; to
font-family: Inter, ui-sans-serif, system-ui, sans-serif; and change
text-rendering: optimizeLegibility; to text-rendering: optimizelegibility;
(update these in globals.css where the font-family and text-rendering
declarations appear).

In `@packages/ui/src/i18n/init-service.ts`:
- Line 4: Change the Bundle type from using unknown to a concrete allowed type
(e.g., type Bundle = Record<string, Record<string, string>>) so public typings
do not use unknown, and add an explicit return type annotation to the exported
initializer function in this module (the exported initializer API) to fully
annotate its public signature; update any internal places that rely on Bundle or
the initializer return type to satisfy the new stricter types.

In `@packages/ui/src/markdown/components/code-group.tsx`:
- Around line 145-155: The extractCode function currently returns an empty
string when it can't find code which leads HighlightedCode to render an empty
panel; change extractCode (and its usages) to return null (or undefined) instead
of '' so callers can skip rendering the panel, and update the caller that
renders HighlightedCode to check for a null/undefined result and omit the
HighlightedCode panel (or render a clear placeholder). Specifically, modify
extractCode (referenced here) to return null when no code element is found (use
findCodeElement and stringifyChildren as before), and update the component that
consumes extractCode/HighlightedCode to guard on the returned value and not
render the HighlightedCode UI when it's null.
- Around line 180-197: The extractFilename function is using the raw data-meta
string as the tab label which surfaces noisy attributes like `{1,3-5}` or
`title="server.ts" showLineNumbers`; update extractFilename (and the spot where
it reads dataMeta via findCodeElement) to first detect attribute-shaped metas
and only accept simple single-token labels: if dataMeta contains braces `{`, an
equals `=`, or whitespace-separated tokens, attempt to extract a title attribute
by matching title="..."/title='...' and return that value trimmed; if no title
is present, bail out and return undefined so only pure simple labels (e.g.,
"Python") are used as tab text.

In `@packages/webui/package.json`:
- Around line 7-10: The package exports "./server" which exposes ./src/server.ts
that imports from `@tale/i18n` (e.g., its cookie and negotiate modules), but
`@tale/i18n` is not declared—add `@tale/i18n` to the package.json dependencies (not
devDependencies) so consumers installing `@tale/webui` get the required runtime
package; ensure you pick the correct semver version used by the repo, update
package.json "dependencies" to include "@tale/i18n": "<appropriate-version>",
and reinstall/lock to validate the fix.

In `@packages/webui/src/search/client.ts`:
- Around line 146-150: The slug bonus currently uses substring matching
(lowerUrl.includes(t)) which causes false positives; update the logic around
lowerUrl, lowerTokens, slugHit and slugBonus to split the URL path into discrete
slug segments (split on '/' and '-' after stripping protocol/query) and then set
slugHit only if a token exactly matches a segment or appears as a whole slug
component (e.g., segment === token or segment.startsWith(token) depending on
desired strictness), rather than using includes; ensure you compute segments
once, iterate lowerTokens against those segments, and preserve the slugBonus =
slugHit ? 1.25 : 1 assignment.

In `@packages/webui/src/search/dialog.tsx`:
- Around line 132-139: The catch handler for the loadIndex(...) prefetch
currently swallows all errors with console.debug; change it to distinguish
aborts from real failures and log appropriately: in the catch for the promise
returned by loadIndex(locale, baseUrl) detect an AbortError (or error.name ===
'AbortError' / fetch aborted shape) and keep a debug/ignore path for those, but
use console.warn or console.error for other errors so non-abort failures (CORS,
5xx, parse errors) are visible; update the anonymous catch callback that
references loadIndex, locale, baseUrl and replace console.debug with conditional
logging based on the error shape.

In `@packages/webui/src/search/highlight.tsx`:
- Around line 22-26: The term filtering pipeline uses
terms.filter(...).slice().sort(...).map(escapeRegex) but doesn't trim individual
terms, so whitespace-surrounded terms can produce incorrect regexes; update the
pipeline that builds filtered (operating on the variable terms) to trim each
term before filtering and escaping (e.g., apply t = t.trim() in the initial map
or use terms.map(t => t.trim()).filter(...).sort(...).map(escapeRegex)) so that
the variable filtered contains trimmed, escaped terms; reference symbols:
filtered, terms, escapeRegex.

In `@packages/webui/src/search/search-results.tsx`:
- Around line 238-257: The span that renders the breadcrumb currently sets
aria-label={result.url}, which overrides the accessible name (causing screen
readers to read the raw URL); change the span to remove aria-label and instead
add title={result.url} (for hover tooltip) or data-url={result.url} (for
tests/automation) while keeping the visible breadcrumb text intact; update the
test selector in search-results.test.tsx from
[aria-label="/self-hosted/configuration/retention"] to the corresponding
[data-url="/self-hosted/configuration/retention"] (or adapt to title if you
chose title) and ensure the span with className "text-fg-subtle mt-1 flex..." is
the element receiving the data-url/title.

In `@packages/webui/src/server.ts`:
- Around line 223-238: The resolver currently falls back to dist/index.html for
every missing path; change it to return 404 for asset-like requests while
keeping the SPA fallback for extensionless routes: detect whether the incoming
pathname is an asset (e.g., has a file extension via path.extname(pathname) or a
'.' after the last '/'), and if an asset-path candidate (resolved or routeHtml)
does not exist return a 404 Response instead of serving index.html; only when
the pathname is extensionless should you continue to return the SPA shell by
returning file(join(distDir, 'index.html')). Use the existing symbols (resolve,
distDir, distPrefix, file(...), contentTypeFor(pathname), pathname, routeHtml)
to locate and implement this conditional.

In `@services/docs/index.html`:
- Around line 91-93: The catch block that currently swallows all errors during
theme bootstrapping (catch (_e) { /* localStorage unavailable; fall through to
React default */ }) should log the caught error and context before continuing
with the fallback; update that catch to capture the error variable (e or err)
and call console.error or the existing logger with a clear message like "Theme
bootstrap failed, falling back to defaults" plus the error object so regressions
are visible, then allow the function to proceed with the React default behavior.

In `@services/web/lib/i18n/i18n.ts`:
- Around line 8-13: The alias Bundle uses Record<string, Record<string,
unknown>> which uses `unknown` and weakens the glob contract; update the type
used for the regional import to a concrete recursive message-tree type (or
import the exported message-tree type from the shared i18n package) and replace
Bundle with that type where referenced (e.g., the Bundle alias, the
import.meta.glob<Bundle> call and the initServiceI18n bundles object) so the
glob contract is strongly typed without `unknown` (use a recursive shape like a
map of keys to string or nested maps or the shared package's MessageTree type).

In `@tools/plop/templates/react-package/vitest.config.ts.hbs`:
- Around line 1-7: The template currently always imports node:path even though
it's only used when the storybook flag is true (path.dirname(...) in the
storybook block); update the Handlebars template so the import "import path from
'node:path';" is wrapped inside the {{`#if` storybook}}...{{/if}} block to avoid
an unused import when storybook is false, keeping the existing imports for
react, defineConfig, and the conditional storybookTest/playwright imports
unchanged; ensure the path usage (path.dirname(...)) remains inside the same
{{`#if` storybook}} block so there are no dangling references.

---

Outside diff comments:
In `@services/docs/app/components/docs/docs-toc.tsx`:
- Line 95: The TOC unordered list element (<ul className="flex flex-col">) in
the docs-toc.tsx component is missing explicit list semantics; update that <ul>
(in the Docs TOC component) to include role="list" so VoiceOver/Safari expose
list semantics consistently under Tailwind Preflight—add the role attribute to
the existing <ul className="flex flex-col"> element.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 08fdf0b9-ea34-4e87-ac02-b3f965798a81

📥 Commits

Reviewing files that changed from the base of the PR and between 9b92396 and 41036b3.

⛔ Files ignored due to path filters (4)
  • bun.lock is excluded by !**/*.lock
  • services/docs/public/favicon-dark.png is excluded by !**/*.png
  • services/docs/public/favicon-light.png is excluded by !**/*.png
  • services/docs/public/favicon.ico is excluded by !**/*.ico
📒 Files selected for processing (109)
  • components.json
  • docs/de-CH/platform/admin/governance.md
  • docs/de-CH/platform/chat/attachments.md
  • docs/de/platform/admin/governance.md
  • docs/de/platform/chat/attachments.md
  • docs/de/platform/member/preferences.md
  • docs/en/platform/chat/attachments.md
  • docs/en/platform/member/preferences.md
  • docs/fr/platform/chat/attachments.md
  • docs/fr/platform/member/preferences.md
  • package.json
  • packages/ui/.storybook/preview.tsx
  • packages/ui/package.json
  • packages/ui/src/globals.css
  • packages/ui/src/i18n/init-service.ts
  • packages/ui/src/markdown/components/code-group.tsx
  • packages/ui/src/markdown/globals.css
  • packages/ui/src/markdown/markdown.tsx
  • packages/ui/src/markdown/plugins/rehype-preserve-code-meta.ts
  • packages/ui/src/markdown/shiki.ts
  • packages/ui/src/theme/index.ts
  • packages/ui/src/theme/theme-asset-sync.tsx
  • packages/ui/vitest.config.ts
  • packages/webui/.storybook/main.ts
  • packages/webui/package.json
  • packages/webui/src/globals.css
  • packages/webui/src/search/__fixtures__/sample-docs.ts
  • packages/webui/src/search/build-index.test.ts
  • packages/webui/src/search/build-index.ts
  • packages/webui/src/search/client.test.ts
  • packages/webui/src/search/client.ts
  • packages/webui/src/search/dialog.test.tsx
  • packages/webui/src/search/dialog.tsx
  • packages/webui/src/search/highlight.test.tsx
  • packages/webui/src/search/highlight.tsx
  • packages/webui/src/search/recent-searches.test.ts
  • packages/webui/src/search/search-empty.tsx
  • packages/webui/src/search/search-results.test.tsx
  • packages/webui/src/search/search-results.tsx
  • packages/webui/src/search/snippet.test.ts
  • packages/webui/src/search/snippet.ts
  • packages/webui/src/search/types.ts
  • packages/webui/src/search/use-search.test.ts
  • packages/webui/src/search/use-search.ts
  • packages/webui/src/server.ts
  • packages/webui/src/test/setup.ts
  • packages/webui/vitest.config.ts
  • services/docs/app/components/docs/docs-toc.tsx
  • services/docs/app/globals.css
  • services/docs/app/locals.css
  • services/docs/app/main.tsx
  • services/docs/app/routes/__root.tsx
  • services/docs/index.html
  • services/docs/lib/i18n/i18n.ts
  • services/docs/messages/de.json
  • services/docs/messages/en.json
  • services/docs/messages/fr.json
  • services/docs/package.json
  • services/docs/postcss.config.mjs
  • services/docs/scripts/build-search-index.ts
  • services/docs/server.ts
  • services/docs/tailwind.config.ts
  • services/docs/tsconfig.json
  • services/docs/tsr.config.json
  • services/platform/.storybook/manager.ts
  • services/platform/.storybook/preview.tsx
  • services/platform/.storybook/theme.ts
  • services/platform/.storybook/vitest.setup.ts
  • services/platform/app/components/theme/theme-assets.tsx
  • services/platform/app/components/theme/theme-color-meta.tsx
  • services/platform/app/globals.css
  • services/platform/app/locals.css
  • services/platform/app/main.tsx
  • services/platform/app/router.tsx
  • services/platform/app/routes/__root.tsx
  • services/platform/components.json
  • services/platform/index.html
  • services/platform/lib/i18n/i18n.ts
  • services/platform/package.json
  • services/platform/server.ts
  • services/platform/tailwind.config.ts
  • services/platform/tsconfig.json
  • services/platform/tsr.config.json
  • services/platform/vitest.config.ts
  • services/web/app/globals.css
  • services/web/app/locals.css
  • services/web/app/main.tsx
  • services/web/components.json
  • services/web/lib/i18n/i18n.ts
  • services/web/server.ts
  • services/web/tailwind.config.ts
  • services/web/tsconfig.json
  • services/web/tsr.config.json
  • tools/plop/generators/react-package.ts
  • tools/plop/generators/react-service.ts
  • tools/plop/templates/react-package/src/globals.css
  • tools/plop/templates/react-package/src/test/setup.ts
  • tools/plop/templates/react-package/vitest.config.ts.hbs
  • tools/plop/templates/react-service/.storybook/vitest.setup.ts
  • tools/plop/templates/react-service/app/globals.css
  • tools/plop/templates/react-service/app/locals.css
  • tools/plop/templates/react-service/app/main.tsx.hbs
  • tools/plop/templates/react-service/app/router.tsx.hbs
  • tools/plop/templates/react-service/components.json
  • tools/plop/templates/react-service/lib/i18n/i18n.ts.hbs
  • tools/plop/templates/react-service/tailwind.config.ts.hbs
  • tools/plop/templates/react-service/tsconfig.json.hbs
  • tools/plop/templates/react-service/tsr.config.json
  • tools/plop/templates/react-service/vitest.config.ts.hbs
💤 Files with no reviewable changes (12)
  • services/platform/.storybook/vitest.setup.ts
  • services/platform/components.json
  • tools/plop/templates/react-service/components.json
  • services/docs/app/globals.css
  • tools/plop/templates/react-service/.storybook/vitest.setup.ts
  • packages/webui/src/globals.css
  • services/platform/vitest.config.ts
  • services/web/components.json
  • services/platform/.storybook/theme.ts
  • services/web/app/globals.css
  • services/docs/package.json
  • services/platform/app/components/theme/theme-color-meta.tsx

Comment on lines +263 to +264
font-family: 'Inter', ui-sans-serif, system-ui, sans-serif;
text-rendering: optimizeLegibility;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Normalize these body styles to the current CSS lint rules.

Stylelint is already flagging the quoted Inter family and optimizeLegibility casing here, so this hunk won't lint clean as-is.

Suggested fix
-    font-family: 'Inter', ui-sans-serif, system-ui, sans-serif;
-    text-rendering: optimizeLegibility;
+    font-family: Inter, ui-sans-serif, system-ui, sans-serif;
+    text-rendering: optimizelegibility;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
font-family: 'Inter', ui-sans-serif, system-ui, sans-serif;
text-rendering: optimizeLegibility;
font-family: Inter, ui-sans-serif, system-ui, sans-serif;
text-rendering: optimizelegibility;
🧰 Tools
🪛 Stylelint (17.10.0)

[error] 263-263: Expected no quotes around "Inter" (font-family-name-quotes)

(font-family-name-quotes)


[error] 264-264: Expected "optimizeLegibility" to be "optimizelegibility" (value-keyword-case)

(value-keyword-case)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ui/src/globals.css` around lines 263 - 264, Normalize the CSS to
satisfy stylelint by removing the unnecessary quotes around the Inter family and
lowercasing the text-rendering keyword: change font-family: 'Inter',
ui-sans-serif, system-ui, sans-serif; to font-family: Inter, ui-sans-serif,
system-ui, sans-serif; and change text-rendering: optimizeLegibility; to
text-rendering: optimizelegibility; (update these in globals.css where the
font-family and text-rendering declarations appear).

import { initI18n } from './init';
import { collectRegionalBundles } from './regional-bundles';

type Bundle = Record<string, Record<string, unknown>>;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

# First, check if the file exists and read it
cat -n packages/ui/src/i18n/init-service.ts

Repository: tale-project/tale

Length of output: 1584


🏁 Script executed:

# Also check for any related files that might contain initI18n
fd "init" packages/ui/src/i18n/ -type f

Repository: tale-project/tale

Length of output: 232


🏁 Script executed:

# Check the initI18n function and its return type
cat -n packages/ui/src/i18n/init.ts | head -100

Repository: tale-project/tale

Length of output: 3159


🏁 Script executed:

# Also check if there are other type definitions in the i18n directory
ls -la packages/ui/src/i18n/

Repository: tale-project/tale

Length of output: 745


Align public typing with TS guidelines (unknown + missing return type).

Please avoid unknown in Bundle and add an explicit return type on the exported initializer API.

♻️ Proposed fix
-type Bundle = Record<string, Record<string, unknown>>;
+type MessageValue =
+  | string
+  | number
+  | boolean
+  | null
+  | MessageValue[]
+  | { [key: string]: MessageValue };
+type Bundle = Record<string, Record<string, MessageValue>>;
@@
 export function initServiceI18n({
   bundles,
   regional,
   global,
-}: InitServiceParams) {
+}: InitServiceParams): ReturnType<typeof initI18n> {
   return initI18n({
     bundles: { ...bundles, ...collectRegionalBundles(regional) },
     global,
   });
 }

As per coding guidelines: "Never use as, any, or unknown in TypeScript" and "annotate public APIs, exported functions...".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ui/src/i18n/init-service.ts` at line 4, Change the Bundle type from
using unknown to a concrete allowed type (e.g., type Bundle = Record<string,
Record<string, string>>) so public typings do not use unknown, and add an
explicit return type annotation to the exported initializer function in this
module (the exported initializer API) to fully annotate its public signature;
update any internal places that rely on Bundle or the initializer return type to
satisfy the new stricter types.

Comment on lines 145 to 155
function extractCode(child: ReactElement<CodeGroupChildProps>): string {
if (typeof child.props.code === 'string') return child.props.code;
if (typeof child.props.children === 'string') return child.props.children;
return '';
// Markdown path: child is a <pre> (rendered by react-markdown's pre map)
// whose subtree contains <code class="language-X">…</code>. Stringify the
// nested code element's children to recover the source text.
const codeEl = findCodeElement(child.props.children);
if (!codeEl) return '';
const codeProps = codeEl.props as { children?: ReactNode };
return stringifyChildren(codeProps.children);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | 💤 Low value

extractCode returning '' silently renders an empty highlighted block.

When neither a direct code prop nor a nested <code> element is found, the fallback is an empty string passed to <HighlightedCode>, which will still render the panel chrome. Returning null/undefined and skipping the panel (or rendering a small placeholder) would fail more loudly when an authoring mistake strips the code. Optional — current behavior is at least non-crashing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ui/src/markdown/components/code-group.tsx` around lines 145 - 155,
The extractCode function currently returns an empty string when it can't find
code which leads HighlightedCode to render an empty panel; change extractCode
(and its usages) to return null (or undefined) instead of '' so callers can skip
rendering the panel, and update the caller that renders HighlightedCode to check
for a null/undefined result and omit the HighlightedCode panel (or render a
clear placeholder). Specifically, modify extractCode (referenced here) to return
null when no code element is found (use findCodeElement and stringifyChildren as
before), and update the component that consumes extractCode/HighlightedCode to
guard on the returned value and not render the HighlightedCode UI when it's
null.

Comment on lines +180 to +197
function extractFilename(
child: ReactElement<CodeGroupChildProps>,
): string | undefined {
if (
typeof child.props.filename === 'string' &&
child.props.filename.length > 0
) {
return child.props.filename;
}
const codeEl = findCodeElement(child.props.children);
const dataMeta = (codeEl?.props as { 'data-meta'?: string } | undefined)?.[
'data-meta'
];
if (typeof dataMeta === 'string' && dataMeta.trim().length > 0) {
return dataMeta.trim();
}
return undefined;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Raw metastring may produce noisy tab labels.

extractFilename uses the entire data-meta string as the tab label. For Mintlify-style fences (```python Python) that's exactly right, but real-world fences often carry attribute-shaped meta — e.g. ```ts {1,3-5}, ```ts title="server.ts" showLineNumbers — and those will surface verbatim as tab text ({1,3-5} or title="server.ts" showLineNumbers). If you only want to honour the simple-label form, consider bailing out (or extracting title="…") when the meta contains {, =, or whitespace-separated tokens.

♻️ One possible refinement
   const codeEl = findCodeElement(child.props.children);
   const dataMeta = (codeEl?.props as { 'data-meta'?: string } | undefined)?.[
     'data-meta'
   ];
-  if (typeof dataMeta === 'string' && dataMeta.trim().length > 0) {
-    return dataMeta.trim();
+  if (typeof dataMeta === 'string' && dataMeta.trim().length > 0) {
+    const meta = dataMeta.trim();
+    // Prefer an explicit `title="…"` attribute when present.
+    const titleMatch = /title=(?:"([^"]+)"|'([^']+)'|(\S+))/.exec(meta);
+    if (titleMatch) return titleMatch[1] ?? titleMatch[2] ?? titleMatch[3];
+    // Otherwise only accept a single bareword label (Mintlify-style).
+    if (/^[^\s{=]+$/.test(meta)) return meta;
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ui/src/markdown/components/code-group.tsx` around lines 180 - 197,
The extractFilename function is using the raw data-meta string as the tab label
which surfaces noisy attributes like `{1,3-5}` or `title="server.ts"
showLineNumbers`; update extractFilename (and the spot where it reads dataMeta
via findCodeElement) to first detect attribute-shaped metas and only accept
simple single-token labels: if dataMeta contains braces `{`, an equals `=`, or
whitespace-separated tokens, attempt to extract a title attribute by matching
title="..."/title='...' and return that value trimmed; if no title is present,
bail out and return undefined so only pure simple labels (e.g., "Python") are
used as tab text.

Comment on lines 7 to 10
"exports": {
".": "./src/index.ts",
"./server": "./src/server.ts",
"./layout/site-container": "./src/layout/site-container.tsx",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

python - <<'PY'
import json
from pathlib import Path

pkg = json.loads(Path("packages/webui/package.json").read_text())
declared = {}
declared.update(pkg.get("dependencies", {}))
declared.update(pkg.get("peerDependencies", {}))
print("@tale/i18n declared in package.json:", declared.get("@tale/i18n"))
PY

rg -n "from '@tale/i18n/(cookie|negotiate)'" packages/webui/src/server.ts

Repository: tale-project/tale

Length of output: 199


Declare @tale/i18n before exporting ./server.

./src/server.ts imports @tale/i18n/cookie and @tale/i18n/negotiate (lines 14, 18), but @tale/webui doesn't declare @tale/i18n as a dependency. Adding ./server as a public entrypoint exposes this undeclared dependency to consumers, breaking installs and builds.

Suggested fix
  "dependencies": {
    "@radix-ui/react-dialog": "1.1.15",
+   "@tale/i18n": "workspace:*",
    "@tale/ui": "workspace:*",
    "@tanstack/react-router": "1.168.10",
    "framer-motion": "12.38.0",
    "lucide-react": "1.8.0",
    "minisearch": "7.2.0"
  },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/webui/package.json` around lines 7 - 10, The package exports
"./server" which exposes ./src/server.ts that imports from `@tale/i18n` (e.g., its
cookie and negotiate modules), but `@tale/i18n` is not declared—add `@tale/i18n` to
the package.json dependencies (not devDependencies) so consumers installing
`@tale/webui` get the required runtime package; ensure you pick the correct semver
version used by the repo, update package.json "dependencies" to include
"@tale/i18n": "<appropriate-version>", and reinstall/lock to validate the fix.

Comment on lines +238 to 257
<span
className="text-fg-subtle mt-1 flex min-w-0 items-center gap-1 truncate text-[10.5px]"
aria-label={result.url}
>
{breadcrumb.length > 0 ? (
breadcrumb.map((segment, i) => (
<span
key={`${segment}-${i}`}
className="inline-flex shrink-0 items-center gap-1"
>
{i > 0 ? (
<ChevronRight aria-hidden className="size-2.5 opacity-60" />
) : null}
<span className="truncate">{segment}</span>
</span>
))
) : (
<span className="font-mono">{result.url}</span>
)}
</span>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

aria-label={result.url} on a span replaces friendly breadcrumb text in the AT announcement.

aria-label overrides an element's accessible name. On this span the visible content is the humanised breadcrumb (e.g., "Self Hosted › Configuration"), but the accessibility tree will substitute the raw URL /self-hosted/configuration/retention — so screen-reader users hear the URL while sighted users see the breadcrumb. The option's overall accessible name (button content concatenation) is also degraded.

If the goal is a hover/tooltip with the URL, use title. If the goal is a programmatic data hook for the test, use data-url. Either lets the visible breadcrumb keep its accessible name.

🛡️ Suggested fix
           <span
             className="text-fg-subtle mt-1 flex min-w-0 items-center gap-1 truncate text-[10.5px]"
-            aria-label={result.url}
+            title={result.url}
+            data-url={result.url}
           >

The search-results.test.tsx selector at line 278 ([aria-label="/self-hosted/configuration/retention"]) needs to follow — switch it to [data-url="…"].

As per coding guidelines: "Accessibility must meet WCAG 2.1 Level AA standards — use real HTML elements, ensure keyboard reachability, maintain visible focus, label controls, and verify AA contrast".

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<span
className="text-fg-subtle mt-1 flex min-w-0 items-center gap-1 truncate text-[10.5px]"
aria-label={result.url}
>
{breadcrumb.length > 0 ? (
breadcrumb.map((segment, i) => (
<span
key={`${segment}-${i}`}
className="inline-flex shrink-0 items-center gap-1"
>
{i > 0 ? (
<ChevronRight aria-hidden className="size-2.5 opacity-60" />
) : null}
<span className="truncate">{segment}</span>
</span>
))
) : (
<span className="font-mono">{result.url}</span>
)}
</span>
<span
className="text-fg-subtle mt-1 flex min-w-0 items-center gap-1 truncate text-[10.5px]"
title={result.url}
data-url={result.url}
>
{breadcrumb.length > 0 ? (
breadcrumb.map((segment, i) => (
<span
key={`${segment}-${i}`}
className="inline-flex shrink-0 items-center gap-1"
>
{i > 0 ? (
<ChevronRight aria-hidden className="size-2.5 opacity-60" />
) : null}
<span className="truncate">{segment}</span>
</span>
))
) : (
<span className="font-mono">{result.url}</span>
)}
</span>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/webui/src/search/search-results.tsx` around lines 238 - 257, The
span that renders the breadcrumb currently sets aria-label={result.url}, which
overrides the accessible name (causing screen readers to read the raw URL);
change the span to remove aria-label and instead add title={result.url} (for
hover tooltip) or data-url={result.url} (for tests/automation) while keeping the
visible breadcrumb text intact; update the test selector in
search-results.test.tsx from [aria-label="/self-hosted/configuration/retention"]
to the corresponding [data-url="/self-hosted/configuration/retention"] (or adapt
to title if you chose title) and ensure the span with className "text-fg-subtle
mt-1 flex..." is the element receiving the data-url/title.

Comment on lines +223 to +238
const resolved = resolve(distDir, rel);
if (resolved === distDir || resolved.startsWith(distPrefix)) {
const candidate = file(resolved);
if (await candidate.exists()) {
const ct = contentTypeFor(pathname);
return new Response(candidate, {
headers: ct ? { 'content-type': ct } : undefined,
});
}
// Try the prerendered route HTML (e.g. /pricing → dist/pricing/index.html).
const routeHtml = file(join(resolved, 'index.html'));
if (await routeHtml.exists()) {
return new Response(routeHtml);
}
}
return new Response(file(join(distDir, 'index.html')));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Return 404 for missing asset paths instead of the SPA shell.

This falls back to index.html for every unresolved request, including /assets/*.js, CSS, and image URLs. After a deploy or a broken asset reference, browsers will get HTML with 200 at a JS/CSS URL, which is much harder to diagnose and can be cached incorrectly. Keep the SPA fallback for extensionless routes, but return 404 for missing asset-like paths.

Suggested fix
   const resolved = resolve(distDir, rel);
+  const isAssetLikePath = /\.[^/]+$/.test(rel);
   if (resolved === distDir || resolved.startsWith(distPrefix)) {
     const candidate = file(resolved);
     if (await candidate.exists()) {
       const ct = contentTypeFor(pathname);
       return new Response(candidate, {
         headers: ct ? { 'content-type': ct } : undefined,
       });
     }
     // Try the prerendered route HTML (e.g. /pricing → dist/pricing/index.html).
     const routeHtml = file(join(resolved, 'index.html'));
     if (await routeHtml.exists()) {
       return new Response(routeHtml);
     }
   }
+  if (isAssetLikePath) {
+    return new Response('Not found', { status: 404 });
+  }
   return new Response(file(join(distDir, 'index.html')));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const resolved = resolve(distDir, rel);
if (resolved === distDir || resolved.startsWith(distPrefix)) {
const candidate = file(resolved);
if (await candidate.exists()) {
const ct = contentTypeFor(pathname);
return new Response(candidate, {
headers: ct ? { 'content-type': ct } : undefined,
});
}
// Try the prerendered route HTML (e.g. /pricing → dist/pricing/index.html).
const routeHtml = file(join(resolved, 'index.html'));
if (await routeHtml.exists()) {
return new Response(routeHtml);
}
}
return new Response(file(join(distDir, 'index.html')));
const resolved = resolve(distDir, rel);
const isAssetLikePath = /\.[^/]+$/.test(rel);
if (resolved === distDir || resolved.startsWith(distPrefix)) {
const candidate = file(resolved);
if (await candidate.exists()) {
const ct = contentTypeFor(pathname);
return new Response(candidate, {
headers: ct ? { 'content-type': ct } : undefined,
});
}
// Try the prerendered route HTML (e.g. /pricing → dist/pricing/index.html).
const routeHtml = file(join(resolved, 'index.html'));
if (await routeHtml.exists()) {
return new Response(routeHtml);
}
}
if (isAssetLikePath) {
return new Response('Not found', { status: 404 });
}
return new Response(file(join(distDir, 'index.html')));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/webui/src/server.ts` around lines 223 - 238, The resolver currently
falls back to dist/index.html for every missing path; change it to return 404
for asset-like requests while keeping the SPA fallback for extensionless routes:
detect whether the incoming pathname is an asset (e.g., has a file extension via
path.extname(pathname) or a '.' after the last '/'), and if an asset-path
candidate (resolved or routeHtml) does not exist return a 404 Response instead
of serving index.html; only when the pathname is extensionless should you
continue to return the SPA shell by returning file(join(distDir, 'index.html')).
Use the existing symbols (resolve, distDir, distPrefix, file(...),
contentTypeFor(pathname), pathname, routeHtml) to locate and implement this
conditional.

Comment thread services/docs/index.html
Comment on lines 91 to 93
} catch (_e) {
/* localStorage unavailable; fall through to React default */
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Do not silently swallow bootstrap errors in this catch.

This catch currently hides all failures in theme bootstrapping; log and continue fallback so regressions are visible.

Proposed fix
-        } catch (_e) {
-          /* localStorage unavailable; fall through to React default */
+        } catch (error) {
+          console.warn('Theme bootstrap fallback:', error);
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch (_e) {
/* localStorage unavailable; fall through to React default */
}
} catch (error) {
console.warn('Theme bootstrap fallback:', error);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/docs/index.html` around lines 91 - 93, The catch block that
currently swallows all errors during theme bootstrapping (catch (_e) { /*
localStorage unavailable; fall through to React default */ }) should log the
caught error and context before continuing with the fallback; update that catch
to capture the error variable (e or err) and call console.error or the existing
logger with a clear message like "Theme bootstrap failed, falling back to
defaults" plus the error object so regressions are visible, then allow the
function to proceed with the React default behavior.

Comment on lines 8 to +13
type Bundle = Record<string, Record<string, unknown>>;

// Auto-discover regional variants (e.g. `de-CH.json`, future `fr-CA.json`).
// Drop a `xx-YY.json` file in `messages/` and Vite picks it up here.
// Vite requires `import.meta.glob` patterns to be literal relative or
// absolute paths — aliases (`@/...`) are rejected at build time.
const regionalBundles = collectRegionalBundles(
import.meta.glob<Bundle>('../../messages/*-*.json', {
export const i18n = initServiceI18n({
bundles: { en: enMessages, de: deMessages, fr: frMessages },
// Vite requires the glob pattern to be a literal at the call site.
regional: import.meta.glob<Bundle>('../../messages/*-*.json', {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Avoid unknown in the bundle shape.

This local alias weakens the glob contract and violates the repo's TS rule. Use a concrete recursive message-tree type here, or reuse one exported by the shared i18n package.

♻️ Proposed typing change
-type Bundle = Record<string, Record<string, unknown>>;
+type Bundle = Record<string, string | Bundle>;
As per coding guidelines: "Never use `as`, `any`, or `unknown` in TypeScript — use type guards, generics, discriminated unions, or `never` instead; framework-generated code and third-party gaps are exceptions".
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
type Bundle = Record<string, Record<string, unknown>>;
// Auto-discover regional variants (e.g. `de-CH.json`, future `fr-CA.json`).
// Drop a `xx-YY.json` file in `messages/` and Vite picks it up here.
// Vite requires `import.meta.glob` patterns to be literal relative or
// absolute paths — aliases (`@/...`) are rejected at build time.
const regionalBundles = collectRegionalBundles(
import.meta.glob<Bundle>('../../messages/*-*.json', {
export const i18n = initServiceI18n({
bundles: { en: enMessages, de: deMessages, fr: frMessages },
// Vite requires the glob pattern to be a literal at the call site.
regional: import.meta.glob<Bundle>('../../messages/*-*.json', {
type Bundle = Record<string, string | Bundle>;
export const i18n = initServiceI18n({
bundles: { en: enMessages, de: deMessages, fr: frMessages },
// Vite requires the glob pattern to be a literal at the call site.
regional: import.meta.glob<Bundle>('../../messages/*-*.json', {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/web/lib/i18n/i18n.ts` around lines 8 - 13, The alias Bundle uses
Record<string, Record<string, unknown>> which uses `unknown` and weakens the
glob contract; update the type used for the regional import to a concrete
recursive message-tree type (or import the exported message-tree type from the
shared i18n package) and replace Bundle with that type where referenced (e.g.,
the Bundle alias, the import.meta.glob<Bundle> call and the initServiceI18n
bundles object) so the glob contract is strongly typed without `unknown` (use a
recursive shape like a map of keys to string or nested maps or the shared
package's MessageTree type).

Comment on lines +1 to +7
import path from 'node:path';
import { fileURLToPath } from 'node:url';

{{#if storybook}}import { storybookTest } from '@storybook/addon-vitest/vitest-plugin';
{{/if}}import react from '@vitejs/plugin-react';
{{#if storybook}}import { playwright } from '@vitest/browser-playwright';
{{/if}}import { defineConfig } from 'vitest/config';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Conditionalize the node:path import so non-storybook scaffolds don't ship an unused import.

path is only referenced inside the {{#if storybook}} block on line 9 (path.dirname(...)). When storybook is false, the generated vitest.config.ts still imports node:path with no usage, which will trip noUnusedLocals / no-unused-vars lint in any scaffolded package that inherits the standard toolchain.

♻️ Proposed template fix
-import path from 'node:path';
-import { fileURLToPath } from 'node:url';
+{{`#if` storybook}}import path from 'node:path';
+{{/if}}import { fileURLToPath } from 'node:url';

Note: the ember-template-lint warnings flagged by static analysis (lines 4, 6, 9, 19) are false positives — this is a Handlebars/Plop template, not an Ember .hbs template, so the no-implicit-this rule does not apply here.

🧰 Tools
🪛 ember-template-lint (7.9.3)

[error] 4-4: Ambiguous path 'storybook' is not allowed. Use '@storybook' if it is a named argument or 'this.storybook' if it is a property on 'this'. If it is a helper or component that has no arguments, you must either convert it to an angle bracket invocation or manually add it to the 'no-implicit-this' rule configuration, e.g. 'no-implicit-this': { allow: ['storybook'] }.

(no-implicit-this)


[error] 6-6: Ambiguous path 'storybook' is not allowed. Use '@storybook' if it is a named argument or 'this.storybook' if it is a property on 'this'. If it is a helper or component that has no arguments, you must either convert it to an angle bracket invocation or manually add it to the 'no-implicit-this' rule configuration, e.g. 'no-implicit-this': { allow: ['storybook'] }.

(no-implicit-this)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tools/plop/templates/react-package/vitest.config.ts.hbs` around lines 1 - 7,
The template currently always imports node:path even though it's only used when
the storybook flag is true (path.dirname(...) in the storybook block); update
the Handlebars template so the import "import path from 'node:path';" is wrapped
inside the {{`#if` storybook}}...{{/if}} block to avoid an unused import when
storybook is false, keeping the existing imports for react, defineConfig, and
the conditional storybookTest/playwright imports unchanged; ensure the path
usage (path.dirname(...)) remains inside the same {{`#if` storybook}} block so
there are no dangling references.

@yannickmonney

Copy link
Copy Markdown
Contributor Author

Closing as duplicate of #1700 (same branch, same commits, better-formatted body).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant