Ask AI

Changelog

What changed in each release of the shared UI and tasks, from CHANGELOG.md at the repository root.

Changelog

All notable changes to the shared UI package @joeblew999/remy-ui are documented here. The format follows Keep a Changelog and the package follows Semantic Versioning.

Unreleased

Changed

  • GitHub's verdict on a commit is main's push run: a release tag's run on the same commit (the release job's) no longer holds dev:promote as "still checking".

[0.15.1] - 2026-10-11

Changed

  • remy docs publish is TypeScript in the command line (the three docs/scripts/*.mjs are gone) and waits for the index's sync it started before asking the fixed questions; remy docs questions asks them alone. On 2026-10-11 the questions ran seconds after the sync started, against the old index.
  • remy.config.ts alone sets the origins and the release (DEPLOY_ORIGIN, STAGING_ORIGIN, DOCS_ORIGIN, their VITE_*, RELEASE_*): a value left in the shell by another project is removed, where it used to win. The ports still take a value given in the shell.
  • GitHub's verdict on a commit (dev:status, dev:change, dev:promote) is its push run's; a Dependabot run on the same commit no longer reads as green.
  • The check's route tree part also reruns when src/start.ts changes (the tree registers the Start instance's types).
  • @joeblew999/remy-showcase 0.1.1 declares what it imports, at the platform's exact versions (a unit check holds them equal; ui:version keeps them so): in an app whose lock put zod 3 at the top of node_modules, zod/mini resolved to it and the build failed. @joeblew999/remy-auth-contract 0.4.1 (its peer floor).

[0.15.0] - 2026-10-11

0.14.0 was prepared on main and never published; its changes ship here.

Changed

  • No mise. package.json is the entry: its scripts run every command (npm run dev:status|start|change|land|promote|release|done, npm run dev|build|preview|test|setup|packages|docs|…). The tooling is remy, the package's bin (packages/ui/src/cli, TypeScript; an app runs the JavaScript emitted from it), with remy.config.ts for an app's origins, ports and release package and .node-version for Node. The shared workflow runs on setup-node and npm run; the blank app has no mise.toml; an app moves to a release with npm run upgrade -- <version>. The deploy's wait ends only when the home page's every asset answers.

[0.14.0] - 2026-10-09

Added

  • Which version is deployed (remy-sport's build stamp, for every app). build/vite: remyBuild(), in remyApp() and remyDocs() already, works out a stamp per build from the sources (the app's package, the commit, a hash of anything uncommitted, the platform packages' versions) as virtual:remy-build. Every Worker's /healthz answers with it, its environment and Cloudflare's deployedAt, readable from any origin. versions: BuildStamp, now in the app frame's footer (the environment off production, the product's name, the commit, a reload control when the deployment has moved on under an open page), and Versions (this app, its docs Worker and other listed deployments, each answering for itself). build: the shapes and TanStack Query options. mise run cf:versions: a row per deployment, each commit placed against the checkout. Checks in every app's shared set (versions.checks, build.checks).

  • Staging: mise run cf:staging deploys the Wrangler configuration's env.staging (built with CLOUDFLARE_ENV=staging) to STAGING_ORIGIN; cf:deploy and its provisioning guard read the environment being deployed, and cf:versions asks staging too. defineRemyApp's deployments: the app's deployments, which Versions lists. mail's unreachable(address): a message to a reserved test or example domain is never sent.

  • The product's name is the app's, written once (defineRemyApp's brand): pageTitle, pageHead's title and description receive it as a second argument, and productNameChecks (every app's shared set) fails a page of an app with another name that says "Remy". The showcase's Settings page shows where the name is used and how another would read (ProductNameCard).

  • app-config: defineRemyApp (the app's brand, source, and site and app navigation, each link written with TanStack's linkOptions so it is checked against the app's own routes), NavItem, useRemyApp, usePreferredLocale. The shared frame names no route but /, so an app with other pages than remy-auth's type-checks and shows only its own.

  • app-shell: AppShell alone, without the showcase pages.

  • @joeblew999/remy-showcase/app-nav: showcaseSiteNav, showcaseAppNav, the showcase's lists for apps that show it.

  • docs/*: the docs Worker itself (Fumadocs on TanStack Start and Cloudflare): docs/vite's remyDocs(docsConfig, { contract }) builds an app's docs from its docs/content/ and docs.config.ts, generating the Worker's Cloudflare configuration into docs/.remy-docs/; docs/config's defineDocsConfig; docs/tsconfig.json for an app's one-line docs/tsconfig.json. Ask AI is opt-in (ask in docs.config.ts); an app without an API (contract) has no reference.

  • skills/remy: the platform's rules as an agent skill, generated from the developer docs; the shared skills:install installs it, agents:rules points AGENTS.md at it.

  • defineRemyApp's docs: the docs Worker's origin; the frame shows the Docs, Developers and Guide links.

  • app/vite: remyApp(), an app's whole vite.config.ts (TanStack Devtools, the parts, Cloudflare, Fontaine, Tailwind, Start, React), compiling the app's own catalog when it has project.inlang.

  • root (remyRoot, RemyRouterContext, preferredLocale) and router (remyRouter): an app's __root.tsx and router.tsx are one call each.

  • locale's followLocale(runtime): a second Paraglide catalog (an app's or a package's) in the platform's language.

  • The CSP report route /csp-report is the package's, mounted beside every app's routes; startMiddleware enforces the policy and reports there by default.

  • Zod's jitless is set once for every app (AppProviders) and the docs Worker.

  • Tasks: packages:check, packages:upgrade, packages:pack, packages:release, packages:tag, packages:publish, packages:notes for any repository that publishes packages (its non-private workspaces; RELEASE_PACKAGE, RELEASE_TITLE); project:release-checks (a hook), project:doctor, project:verify-tooling (lockfile, wrangler.jsonc's observability and preview_urls: false, skill pins), project:single-copies (in project:check), project:preconditions (in project:setup), project:refresh-tasks.

  • CI: .github/workflows/google.yml is a reusable workflow; an app calls it at its tag, and project:upgrade-ui moves that tag with the package and the tasks.

  • i18n:messages:check fails when an app's catalog lists other languages than the platform serves.

  • project:test:consumers: a hook tiers 3 and 4 and CI run last, for the other repositories a repository serves (remy-auth: template:test, the consumer fixture in fixtures/consumer/).

  • packages:publish --dry-run; packages:publish first checks that each registry accepts its token (npm whoami) and stops on any answer but "not found", before anything is published.

  • template/: the blank app a new repository starts from (identity, one home page in every language, a docs index per site); docs:init takes template/docs.

  • api/guard-core: the guard, free of any oRPC import so the same code runs on oRPC 1 and 2: who may call a procedure is its meta.policy (public or session), guardMiddleware enforces it as the router's root middleware, and a procedure that declares none never runs. guardProblems names a procedure with no policy, with no guard in front of it, a public one whose response names a person (an email address, say), and a personal one that does not say who receives it (meta.personal).

  • api/guard: guard() (the guard for the installed oRPC, an app router's root middleware), GuardContext (getSession, as oRPC's Better Auth guide shares a session), once, signedIn, noSession, and everything in api/guard-core and api/policy.

  • api/policy: policy() and personal(), the oRPC metadata a contract procedure declares for the guard, and actions(vocabulary) for policies that are actions of the app's relation vocabulary.

  • api/relations: the relation engine, lifted from remy-sport and taking an app's vocabulary as data (defineVocabulary, relationEngine with can, canAll, canFor, holds, heldAmong, objectsHeldBy, usersHolding, audienceFor; vocabularyProblems, schemaProblems). The guard enforces an action policy with it: 401 without a session, 404 for a missing object before 403.

  • environment: environments(), an app's table of what each environment permits; anything undeclared is production. mail: mailerFor, mail through Cloudflare Email Service or kept in the Worker's outbox where the environment captures it.

  • allowed: <Allowed can action>, which shows a control only when the server allowed that action for this viewer on this object; allowed.checks: offeredActions, allowedActions.

  • api/server: apiHandlers answers no-store to a request with credentials, and on every 401.

  • @joeblew999/remy-showcase/app-pages: AccountPage takes auth (who is signed in, and the app's sign-in and sign-out controls); without it, the empty state as before.

  • @joeblew999/remy-auth-contract 0.4.0: me (GET /api/me, the signed-in person's own account; 401 without a session), the notes demo (notes.*, notesVocabulary, ./notes) and errorStatuses; written for oRPC 2. ui:version sets its peer floor for @joeblew999/remy-ui to the release.

  • Tasks: project:prepare, a hook project:dev and project:build run first (nothing by default); plans:open, a parked plan taken up again.

Changed (breaking)

  • Every message that named the product takes it as {product}: home_title, home_description, home_intro, home_shared_heading, home_shared_intro, home_developers_text, formats_description, app_home_description, account_description, account_empty_description, account_sign_in_description, email_code_subject, email_code_intro. Pass { product: useRemyApp().brand }; TypeScript names each caller. showcaseChecks, formatsChecks and preloadChecks need product. Without AppProviders the frame's brand is empty, no longer "Remy". Three messages nothing used are gone: ask_description, ask_intro, search_description.
  • oRPC 2: every @orpc/* dependency is 2.0.0-beta.42 (@orpc/openapi-client is gone, merged into @orpc/openapi). An app's contract writes its routes as .meta(openapi({ method, path, ... })) and its policy as .meta(policy('public')) (api/policy; ApiMeta and oc.$meta are gone); its errors carry no status, and the API's own error codes get theirs from errorStatuses, passed to apiHandlers, generateSpec, apiChecks and coverageProblems. An error's body no longer holds status. contractClient(contract, { origin }) replaces { url }. api/coverage reads a route with routeOf(procedure). A client still on oRPC 1 reads a successful answer but not a typed error.
  • apiChecks now fails an API whose procedures are not behind the guard. An app with an API puts guard() (api/guard) at its router's root (implement(contract).$context<GuardContext<...>>().use(guard())) and gives each call a getSession() in its context; an app that signs nobody in passes noSession.
  • Tasks: cf:deploy and cf:preview refuse when a D1, KV or R2 binding names no existing resource, instead of letting Wrangler create one during the deploy.
  • One dependency set: the framework, toolchain and the tools the shared tasks run are the package's exact dependencies (no more optional peers); an app's package.json names @joeblew999/remy-ui and drops its own pins of them, or project:single-copies fails.
  • No remy-auth page list is a default: localizedWorker's entryPaths, entryRedirect, sitemapXml's paths and prerenderPages' paths are required; the sitemap lists defineRemyApp's sitePaths.
  • The shared check sets take the app's sitePaths and appPaths; the home page's words are checked only when the app passes home.
  • An app's own src/routes/csp-report.ts, src/csp.ts and src/preferred.ts go (the package's route, defaults and root replace them).
  • defineRemyApp takes service and sitePaths (required: the sitemap lists them) and registers the app (registeredApp); pageHead's titles and the home page's WebSite data use its brand, where they used 'Remy'.
  • startMiddleware's csp is optional. The check sets' ownSitePaths, ownAppPaths, formats and devicePath options are gone (sitePaths, appPaths, showcase).
  • Tests are type-checked: an app's tsconfig.json includes tests/ (the blank app's does). HomeContent's functions take a Locale.
  • remy-auth's showcase pages are a package of their own, @joeblew999/remy-showcase (its README): pages, app-pages, app-nav (was showcase/app-nav), clock-route, reservation, search-params, navigation-blocking, the showcase's paths lists and checks (showcase.checks, reservation.checks, with reservationApiChecks from api/checks). The platform's paths keeps isAppPath. The parts' own pieces moved into their parts (below); build-boundaries.checks and code-splitting.checks are at the root; rows and zod-csp are exported.
  • Parts from other packages: an app lists another package's part as <package>/<name> in src/parts.json; the package offers them in its parts/catalog.json, each part's folder with a checks.js whose default export runs its checks. The showcase's parts (time-zones, deferred-place, status-card) moved there with their pieces (@joeblew999/remy-showcase/parts/time-zones/page, parts/deferred-place/device-place, parts/status-card/card, parts/status-card/query): list them as @joeblew999/remy-showcase/<name>. The time-zones breadcrumb's middle link is the app's parent option (src/parts/time-zones.ts). problemChecks lost timeZones (the part's own checks now); partChecks lost root and file. The platform's own part is seo-routes.
  • The showcase's checks are one call an app showing the showcase makes itself, showcaseChecks() (@joeblew999/remy-showcase/showcase.checks, with demoChecks, appNavChecks and formatsChecks from checks); serverAppChecks and prerenderedAppChecks lost showcase. problem.checks is at the root.
  • @joeblew999/remy-auth-contract 0.3.0 imports the reservation from @joeblew999/remy-showcase (peer: remy-showcase 0.1.0 on); 0.2.1 needs remy-ui below 0.14.
  • packages:bumped (first in packages:release, and in packages:tag): a published package whose files changed since the last release tag must have a new version, or the release stops.
  • buildBoundaryChecks checks a page's device time only with deviceTimePath (the showcase's /formats).
  • project:verify also runs project:doctor and project:verify-tooling; project:generate compiles the app's own catalog by default. A missing src/parts.json lists no parts.
  • AppProviders takes app (a defineRemyApp config) and preferred (the root loader's language) in place of repository; every frame reads both, so pages no longer pass preferred.
  • SourceLink, SiteNavLinks and AppNavLinks are gone: the config's repository, site.links and app.links replace them.
  • ./preferred (usePreferred) is gone: the root passes preferred to AppProviders.
  • Intro's and FormatsContent's backTo is 'site' | 'app' (the app's home comes from the config).
  • The sidebar and the phone's bottom bar list the config's app.nav, no longer remy-auth's pages; an app showing them passes showcaseAppNav.
  • An app's docs/ is content/, docs.config.ts and three one-line files (docs:init writes them); its wrangler.jsonc, src/, scripts/, tests/ and playwright.config.ts go. The shared docs tasks read docs/.remy-docs/wrangler.json; docs:publish and the questions check read docs/content/questions.json.
  • mdast-util-to-markdown is pinned at 2.1.2: 2.1.3 overflows the stack on bold text in Fumadocs' build.

[0.13.0] - 2026-09-26

Added

  • providers: AppProviders, what an app's root renders its pages in (reading direction, theme, source link), so no app misses one (remy-auth-app's theme toggle did nothing without the theme provider).
  • checks: themeChecks (choosing Dark darkens the page), in both app check sets.
  • clock-route: clockRouteOptions, clockDefaults, clockZones for an app's Clock route.

Changed

  • project:check builds before it type-checks, so a new route's generated route tree is current.

[0.12.0] - 2026-09-26

With @joeblew999/remy-auth-contract 0.2.1 (unchanged code; its peer range is now >=0.12.0, so a release changes one version number, not four).

Added

  • Formats page, one shape in every language (issue #5): every section two columns, so none ends with a hole; notes in their card's footer (Group's note); each language offered its own digits too (ownValues().numberingSystems adds the numbering systems written in its script: Thai, Devanagari, Han, Arabic-Indic), so no language is Latin-only; sorting shows names in every script; NameList isolates each name in a mixed-script list. FormatsExtras changed: numbers is rows in the Numbers card, money is gone and currencyNote is the currency card's footer.
  • shell: SourceLink, a context for the site header's "GitHub" link. An app sets its own repository (<SourceLink value="https://github.com/you/app">); without it the header shows no source link instead of remy-auth's.
  • Translation tasks in two pipelines (tasks/i18n/): i18n:messages:check and i18n:docs:check (offline, read-only; git, jq, @lingual/i18n-check 0.9.5, a plural-category check), i18n:check both (a warning; strict with I18N_STRICT=1), and i18n:messages:translate, i18n:docs:translate, i18n:translate: Claude Code 2.1.282 pinned in the task, with no tools, on main under a lock shared by every worktree, committing what it translated. Replaces i18n:status, the old i18n:translate and the provenance lines. Plural messages use =* for their fallback variant.
  • Mobile navigation (.plans/done/mobile-navigation.md): on phones (below md, 768 px) a bottom bar (blocks/bottom-nav, an owned block of stock shadcn parts: shadcn has none) with the core pages and More, which opens the sidebar as its sheet; on tablets and desktops the sidebar, now collapsing to icons (collapsible="icon"). One list feeds both: app-nav.tsx (appNavItems, matchesNav).
  • App pages ClockPage (time now in chosen zones; the route keeps them), SettingsPage (language, appearance, the device's zone and languages) and AccountPage (an empty state until sign-in); appPaths gains /app/clock, /app/account, /app/settings, so an app on the package adds those three routes when it updates. New catalog keys are English only while translation is frozen.
  • checks: appNavChecks (the phone bar, More, the desktop sidebar, the clock's zones), run by both serverAppChecks and prerenderedAppChecks.
  • checks: fontChecks enforces a font byte budget: a first visit to each site page downloads at most fontBudget (300 KB, option budget) of fonts, in every language. Measured on a local production build: 28.7 KB (Latin) to 227.2 KB (/ar/formats) (.plans/fonts.md, step 1). New exports fontBudget and systemFontScripts.
  • Shared tasks i18n:status, i18n:check and i18n:translate (tasks/i18n/): per locale, docs translations missing or stale against the English version recorded in their first line (<!-- translated-from: <path> @ <blob sha> -->), and catalog keys missing, extra or with other placeholders than the base locale. project:check runs i18n:check as a warning; ui:release runs it strict first. An app with nothing to translate gets "nothing to translate". Replaces remy-auth's heading-only docs:translations.

Changed

  • fonts.css: Japanese and Traditional Chinese draw with the system's font for their language (Chrome picks it by the page's lang: Hiragino Kaku Gothic ProN and PingFang TC on macOS). Their Noto web fonts cost 351 KB (/ja) to 1,176 KB (/zh-TW/formats) per first visit; now 0 bytes beyond Geist. The dependencies @fontsource-variable/noto-sans-jp and -tc are removed, and the build's font files drop from 496 to 25.
  • fonts.css: Persian draws with Vazirmatn (@fontsource-variable/vazirmatn 5.3.0, a Persian-designed face; fontsource has no Persian Noto) instead of Noto Sans Arabic: /fa downloads 74 KB of fonts instead of 191 KB, /fa/formats 108 KB instead of 259 KB. Arabic keeps Noto Sans Arabic.
  • checks: fontChecks lets a system font draw a Han page, never tofu (LastResort), and its Han check compares the fonts that actually draw each language's heading instead of the names in the CSS, so Japanese and Traditional Chinese drawn by one font still fails. Every other script still needs its web font.

Changed

  • checks: cspChecks({ enforce }), default true: expects the nonce policy under Content-Security-Policy (and none under Content-Security-Policy-Report-Only), or the reverse with enforce: false; checks the not-found page's policy too; enforced, proves a script without the nonce is blocked and reported. serverAppChecks({ cspEnforced }) passes the app's switch. An app still sending the policy report-only passes cspEnforced: false.

[0.11.0] - 2026-09-25

Added

  • Moves that remove copying from consumers (.plans/publisher-consumer-analysis.md, D4, D8, D9, D12): ./tailwind.css (globals.css, fonts.css, text.css and the package's own @source, so an app writes one import and its own @source "../src"); ./prerender (prerenderPages({ notFoundPath }), a prerendered app's page list); seo's sitemapXml({ origin, paths, extra }), sitemapEntries, robotsTxt(origin), sitemapType, robotsType; ./app-checks (serverAppChecks, prerenderedAppChecks: the shared check set in one call per kind of app, the app's own pages passed beside the shared ones). remy-auth uses them; its CSS, sitemap, robots.txt and registered checks are unchanged. The separate imports keep working.
  • showcase/status-card: the live status card (StatusCard, LiveStatus, statusRefreshMs), moved from remy-auth so remy-auth-app shows it too. The app passes the query: its own client's queryOptions (server-rendered), or a contractClient on another app's origin with serverRendered={false} (asked by the browser, with its own note, message live_status_note_browser). An answer that breaks the contract, or none, shows as an error (data-status="error"), never as data.
  • ./invalidate: invalidateEverything(router, queryClient), moved from remy-auth (it took the router only and read the QueryClient from its context). @tanstack/react-query is a new optional peer.
  • api/server: apiHandlers takes origins, the registered apps' exact origins allowed to call the API from their pages (oRPC's CORSPlugin; none by default, a wildcard is refused).
  • api/checks: apiChecks({ origins }) checks CORS: each registered origin allowed on a simple call and a preflight, any other origin not.
  • showcase/status-card.checks: statusCardChecks({ origin, registered }) for a consumer that asks another app's contract across origins (no status in the prerendered HTML, the other Worker names the page's origin, nothing asked from an unregistered origin), and for every card on a contract endpoint a check that an answer breaking the contract is shown as an error.
  • Parts (.plans/parts.md): ./parts (readParts, catalog), ./parts/vite (remyParts(): a Vite plugin generating virtual:remy-parts from the app's src/parts.json, and the route config that mounts each listed part's routes through TanStack's virtualRouteConfig and physical()), ./parts/checks (partChecks()). First part: time-zones (the /time-zones/$ route and its checks), so an app adds or removes it with one line. New dependency @tanstack/virtual-file-routes.
  • Parts, second pass: three more parts, each one line in src/parts.json. deferred-place (Cloudflare's place streamed into the formats and location pages, its getPlace server function and checks; links a zone to time-zones only when that part is listed), seo-routes (/robots.txt and /sitemap.xml, listing the package's site pages, every listed part's and the app's own entries from its src/parts/seo-routes.ts) and status-card (the live status card on the app home, with the app's status query from its src/parts/status-card.ts). The mechanism gains entry modules (virtual:remy-parts/<part>/<entry>, undefined for an unlisted part), app options (virtual:remy-parts/<part>/app), site paths a part adds (sitePaths) and partSitePaths(). New exports ./parts/seo-routes/sitemap and ./parts/status-card/query (types for the app's options). One implementation each: the seo-routes part builds its routes with seo's sitemapXml and robotsTxt (as a prerendered app writes its files), and the status-card part is showcase/status-card's card with the app's query.
  • serverAppChecks({ parts }) is part-aware: what a listed part owns runs with partChecks() instead, never twice (with seo-routes no sitemap test in publicPageChecks; the device-place row expects Cloudflare's place only with deferred-place). parts defaults to the app's src/parts.json, or none without one; prerenderedAppChecks is unchanged. remy-auth's test file is one serverAppChecks call, one partChecks call and its own checks.
  • sitemapChecks({ paths, oneLanguage }) (checks): the sitemap test, split out of publicPageChecks, which runs it unless told sitemap: false (the seo-routes part's checks run it then). The 404 check it shared a test with is a test of its own, still in publicPageChecks.
  • ./problem (Problem, NotFound, ErrorPage, problemPages) and ./preferred (usePreferred), moved from remy-auth so parts' routes can use them. Nothing changes for existing imports.
  • ./smoke: smokeChecks({ sitePaths, appPaths, hydrate, locales }), tier 1 of the test tiers (every page answers, site pages with a heading, chosen pages hydrate cleanly) for any app on the package.
  • shell: the site frame (SiteShell, Shell, SiteNavLinks, Intro, SkipLink, ZoneBadge) in a module of its own, so a page that needs only the frame (remy-auth's docs and problem pages) no longer downloads the home and formats pages. pages re-exports all of it: nothing changes for existing imports.
  • publicPageChecks({ oneLanguage: { translations } }): one-language pages that also have their own text in other languages (remy-auth's translated docs) are expected in the sitemap once per language, self-canonical, with those languages as alternates and x-default.
  • locale-data (plain JavaScript, re-exported by locale-info): one module that derives, per locale, its region, script, currency, calendars, numbering systems and plural counts (ownValues), each control's choices over all of Paraglide's locales (allChoices, choicesFor: the page's language's own first) and the formats page's searchDefaults (from the base locale). The region's currency comes from country-to-currency (new dependency; the runtime has no currency-for-region API).
  • A numbering-system control on the formats page (?numbering=), in the Numbers section.
  • LocaleInfo has region, currency and counts; Group passes other props (data attributes) to its card.
  • fontChecks({ paths }) (checks): per language, the fonts that actually draw the heading and intro (Chrome DevTools Protocol CSS.getPlatformFontsForNode) are the ones fonts.css names for it; a system or fallback font drawing the text fails naming the page's script and the font to add, a named script font that draws nothing fails, and Japanese and Traditional Chinese must name different fonts.

Fixed

  • fonts.css: Arabic, Persian and Hebrew pages are drawn with Noto Sans Arabic and Hebrew on macOS and Windows. Each language's script font now comes before fontaine's Geist fallback (local Arial, which carries Arabic and Hebrew and so drew them itself), followed by the script font's own metric-matched fallback face; Geist's fallback stays last. No new preloads.

Changed

  • Formats page: every section opens with what it is for the page's language (data-own-area): Money shows the amount in the language's own currency (was euros), Words lists the language's plural forms. Every control offers the union over all locales, the page's own values first, in the secondary look and described by a "This language" badge. currencies, countChoices and the showcase calendar list are gone; the currency default is the base locale's (USD, was EUR) and the count default its first count above one in the general form (2, was 3).
  • formatsChecks and searchParamsChecks iterate the derived values, so a new locale needs no check edit; they also check the "for this language" card of every section and each control's order and marking.
  • The formats rows (Group, Row) live in their own module and the showcase modules whose route options run in every page's first load (search params, time zones, device place) import them and the frame from there instead of from pages. pages still exports them.

Added

  • SiteNavLinks (pages): a context through which an app adds its own links to the site header; remy-auth adds "Docs". Without it the header is unchanged.
  • publicPageChecks({ oneLanguage }): site pages written in one language only, listed once in the sitemap without alternates and checked on a narrow screen like the others.
  • shadcn's table component (through ui:components), and messages for the docs and the answer page in every language.

Changed

  • publicPageChecks: the narrow-screen, fonts and mirrored-header check runs one test per language, and one-language pages once, in their own language.

[0.10.5] - 2026-09-25

Added

  • Ten more languages: fa, he, th, ja, zh-TW, hi, am, pl, tr, de (13 in all), each with its Noto font from fontsource where Geist has no glyphs, applied by :lang().
  • text.css: hyphenation by the page's language, casing by lang (Turkish İ), phrase breaking for Japanese headings. checks: textChecks({ paths }) (every page fits 320 px, per language).
  • Formats: each language's own calendar and digits through one explicit format tag (formatTag), more calendars in the control, week rules from Intl.Locale getWeekInfo, word segmentation with Intl.Segmenter, native digits in counts through Paraglide's number function.
  • matching.js: a Paraglide custom strategy that sends Chinese browsers to zh-TW.
  • Quick test tier (project:test:quick): one language per writing system (QUICK_LOCALES, default en, ar, ja, th).

Changed

  • Per-language checks (app pages kept out of search, CSP violations while hydrating, text) run as one test per language, so the time per test does not grow with the language count.

0.10.4 - 2026-09-25

Added

  • Site header on shadcn's NavigationMenu (plain links, complete without JavaScript), a footer list of every language as real links, and a header language dropdown over the same links whose trigger falls back to that list without JavaScript.

  • theme: shadcn's TanStack Start ThemeProvider and ModeToggle (light, dark, system), on site and app pages. Without JavaScript site pages show the default theme.

  • Breadcrumb on the time zone pages (shadcn Breadcrumb, server-rendered links).

  • tanstack: pageHead adds schema.org WebSite structured data (name, site root) to the site home page through TanStack's script:ld+json head entry.

  • worker: every response sends Content-Security-Policy: frame-ancestors 'none', Cross-Origin-Opener-Policy: same-origin-allow-popups and Strict-Transport-Security: max-age=300.

  • checks: cspChecks({ paths, reportPath }) for a nonce-based Content Security Policy (nonce on every script, no violations while hydrating, report endpoint); publicPageChecks and zoneChecks cover the structured data; observabilityChecks covers the new headers.

  • ./api/server, ./api/client, ./api/coverage, ./api/checks: contract-first APIs on oRPC 1.15.4 (.plans/openapi-contracts.md). apiHandlers mounts an oRPC router in one TanStack Start server route with the generated OpenAPI 3.1 document (/api/openapi.json) and its reference page (/api/doc); contractClient calls any contract with every response validated; isomorphicClient gives an app one client for its own contract on both sides; coverageProblems and apiChecks fail on a procedure without a route, a policy or documented errors; reservationApiChecks covers the demo reservation over HTTP. New dependencies: @orpc/client, @orpc/contract, @orpc/openapi, @orpc/openapi-client, @orpc/server, @orpc/zod, all 1.15.4; @tanstack/react-start is a new optional peer.

  • ./reservation: reservationFieldErrors and reservationConfirmation, the wire shapes a contract declares for a rejected and an accepted reservation.

  • Paraglide's routeStrategies keep /api/* out of URL localisation: no redirect, and the language comes from Accept-Language, else the base locale.

Changed

  • One zone badge style (secondary) on site and app pages; the app pages no longer repeat it as a label; site pages have no back links (they navigate through the header).
  • ./reservation sets Zod's jitless option, so Zod never probes for eval under a strict CSP.
  • zoneChecks accepts a nonce attribute on the app pages' robots meta.
  • showcase/status-card.checks: statusCardChecks takes endpoint, the path the card asks for the status; without it, any server function as before.

0.10.3 - 2026-09-25

Added

  • ./reservation: the demo reservation's rules as one Zod 4 schema (reservationSchema(locale), localized messages) for the browser and the server, the server function's input shape (reservationInput) and reservationErrors for a server's field errors.

Changed

  • showcase/search-params: the formats search params are validated by a Zod 4 schema, formatsSearchSchema, which TanStack Router takes directly as validateSearch (Standard Schema, no adapter). It replaces the hand-written validateSearch function and its parsing helpers; FormatsSearch is the schema's output type. Written with Zod Mini to keep the entry chunk small. New dependency: zod 4.6.5.
  • DemoPage's form runs on TanStack Form (@tanstack/react-form 1.33.5) with shadcn's Field pattern: the shared schema validates on submit, a server's field errors show as the form's own, and onDirtyChange follows the form's isDirty. Its own validation code and input tracking are gone; Reservation and ReservationResult now come from ./reservation (still re-exported).

0.10.2 - 2026-09-25

Changed

  • Formats in five sections (this language, dates and times, numbers, money, words) with a list of links to them; each search-param control sits in the section it changes. FormatsExtras slots are now rows in groups (language, systems, dates, currency) and cards per section (time, numbers, money, words); FormatsControls and PrerenderedFormatsControls become choiceCards and prerenderedChoiceCards (one ChoiceCard per param, with to).
  • Full width in both frames, as shadcn's sidebar-16 block has it; formats cards two per row on medium screens and three on wide ones.

0.10.1 - 2026-09-25

Added

  • Formats as a site page and an app page: FormatsContent (the page's content once), FormatsPage (site frame) and AppFormatsPage (app frame); /app/formats in appPaths and the app sidebar; FormatsControls takes the page it belongs to (to).
  • LanguageMenu: the app pages' language picker, shadcn's DropdownMenu with a radio group; choosing calls Paraglide's setLocale. Site pages keep the plain links.
  • zoneChecks: on a phone in landscape, the way back to the site stays in view.

Changed

  • The app sidebar's "Back to the site" moves into SidebarFooter, so it stays in view on short screens.
  • performanceChecks judges the median of five runs (Lighthouse's computeMedianRun).
  • demoChecks switches language through the app's menu.

0.10.0 - 2026-09-25

All in on shadcn, and site pages kept apart from app pages. Breaking for consumers.

Changed

  • shadcn's monorepo layout: the app's components.json routes shadcn add into this package; the stylesheet is globals.css (was styles.css), exactly what the shadcn CLI writes (default Nova style, neutral theme, Geist), checked by mise run ui:verify before every release.
  • Components regenerated with shadcn's RTL mode; direction (DirectionProvider), skeleton, empty, sidebar, sheet, tooltip, breadcrumb, collapsible, dropdown-menu, avatar and the use-mobile hook added through the CLI.
  • fonts.css: Geist from shadcn, Noto Sans Arabic on Arabic pages per shadcn's RTL guide, generic code font; apps add fontaine's size-matched fallbacks in their Vite config.
  • paths: sitePaths (for Google: no JavaScript needed, indexed, in the sitemap), appPaths (under /app: JavaScript, noindex) and allPaths, replacing publicPaths; pageHead adds noindex to app pages.
  • pages: SiteShell (static shadcn parts), ZoneBadge, SkipLink, Intro; the demo moves to app-pages with AppShell (shadcn's sidebar-16 block, owned in blocks/sidebar-16), AppHomePage and LocationPage.
  • publicPageChecks enforces that every named font is loaded; new zoneChecks; checks find page links inside #main; codeSplittingChecks takes a start page per zone.

0.9.3 - 2026-09-25

Added

  • showcase/device-place: DevicePlace, the device's own location from the Geolocation API, asked only when the visitor presses its button, explained before asking, never sent anywhere; with Cloudflare's location it also shows the distance between the two. devicePlaceChecks.
  • cloudflare: Place carries Cloudflare's latitude and longitude when valid.
  • worker: every response sends Permissions-Policy: geolocation=(self), camera=(), microphone=(), asserted by observabilityChecks.

0.9.2 - 2026-09-25

Added

  • showcase/search-params: PrerenderedFormatsControls, the formats controls for a prerendered page (static defaults in the HTML, the address's values once hydrated, through TanStack's ClientOnly), and FormatsControls' interactive option behind it.
  • checkedLocales in @joeblew999/remy-ui/checks: every locale, or the subset in CHECK_LOCALES; every per-language check iterates it, so the shared project:test:quick tier can run the checks on a few representative languages.

Changed

  • statusCardChecks jumps the refresh interval with Playwright's clock instead of waiting it out (about 10 s to under 1 s).

0.9.1 - 2026-09-25

Fixed

  • DemoPage notices input typed before hydration (a prerendered page fires no onInput for it), so the leave warning still guards it.
  • demoChecks, navigationBlockingChecks and codeSplittingChecks act only once React has hydrated the element, instead of racing hydration on prerendered pages.

Added

  • hydrated(locator) in @joeblew999/remy-ui/checks: resolves once React has hydrated an element.

0.9.0 - 2026-09-25

The move to TanStack Start, Router and Query, with the TanStack showcase (plan). Replaces React Router: consumers move their routes to TanStack file routes (see remy-auth and remy-auth-app).

Added

  • @joeblew999/remy-ui/tanstack: localizedWorker(service, start), the Worker entry for a server-rendered Start app (withObservability around Paraglide's middleware around Start's handler, which gets the original request so its Cloudflare cf properties reach server functions; un-localized entry URLs answer 302 with Vary; HTML is no-store); entryRedirect; localeRewrite, the router rewrite (Paraglide's deLocalizeUrl/localizeUrl); pageHead({ path, title, description }) for a route's head(): title, description, self-canonical and reciprocal hreflang; suggestedLocale(request) and suggestedLocaleInBrowser(page) for the language hint.
  • DemoPage takes optional onReserve (called once the form's own validation passes; its field errors or confirmation message are shown) and onDirtyChange (unsaved input), and exports the Reservation and ReservationResult types. Without them it behaves as before.
  • Export patterns ./showcase/* (src/showcase/*.tsx) and ./showcase/*.checks (src/showcase/*.checks.js) for the TanStack showcase's components and checks.
  • worker: the inner handler receives the generated request ID as X-Request-ID (a client's own value is replaced); new exports logContext, writeLog, outcome, level and requestIdHeader, so the log contract has one definition.
  • Showcase modules: showcase/search-params (validateSearch, searchDefaults, calendarsFor, FormatsControls), showcase/navigation-blocking (useLeaveGuard) and showcase/time-zone (TimeZonePage, canonicalTimeZone, timeZoneName, timeZonePath).
  • Showcase checks: search-params, preload, navigation-blocking, server-functions, deferred-place, status-card, problem (localized 404 for an unknown sub-resource, error page without leaks and retry, read-only server routes with caching and 405), code-splitting and build-boundaries (serverOnlyMarkers, devtoolsMarkers).
  • Catalog keys for the search-param controls, the leave-page warning, the live status card, the retry button and the time zone page.

Changed

  • pages: in-app links are TanStack Links to de-localized paths with preload="intent"; the router's rewrite adds the locale. Language changes stay plain anchors (full navigations).
  • Peer dependency react-router replaced by @tanstack/react-router (optional).
  • HomePage takes children, shown inside the page under its links (the live status card).
  • LanguageHint puts its two actions under the text instead of shadcn's absolutely placed AlertAction, which overlapped the text at phone width.
  • DemoPage shows the localized "Please try again." when onReserve rejects, instead of an unhandled rejection.
  • performanceChecks no longer warms a page before Lighthouse measures it. The warm-up hid a first-layout stall of seconds that every visitor paid in a fresh Chrome renderer on macOS; the cause was font-family names that never load (Inter, ui-monospace, SFMono-Regular, ...), fixed by the new fonts.css export (generic families only: system-ui, sans-serif and monospace), which each app imports after styles.css.
  • publicPageChecks fails when any element on any page uses a non-generic font family or the page declares a web font.

Removed

  • @joeblew999/remy-ui/react-router (languageMiddleware, redirectToLocalized, pageMeta, requireLocale, suggestedLocale); routes no longer carry a :locale segment.

0.8.0 - 2026-09-24

Added

  • @joeblew999/remy-ui/worker: withObservability(service, handler) wraps any Worker's fetch: X-Request-ID on every response, one structured log line per request following the shared contract (schemaVersion, service, environment, release from the version metadata binding, event, level, requestId, route template, method, status, outcome, reasonCode on failures, never URLs or headers), and /healthz for liveness.
  • observabilityChecks({ service, paths }) in the shared checks.

0.7.0 - 2026-09-24

Added

  • @joeblew999/remy-ui/pages: Shell, HomePage, DemoPage, FormatsPage (with slots for an app's extra rows and sections), Group and Row: the pages the shared checks test, so both apps render the same markup instead of keeping copies.
  • @joeblew999/remy-ui/paths: publicPaths as plain JavaScript for route configs and specs.
  • requireLocale in @joeblew999/remy-ui/react-router.

0.6.0 - 2026-09-24

Changed

  • playwrightConfig() splits checks into two levels: ours (the app's own checks, fast) and google plus google-cwv (Lighthouse audits and Core Web Vitals, slow). The shared project:test runs level 1; project:test:google runs level 2; project:test:remote runs both against a deployment.

0.5.0 - 2026-09-24

Added

  • performanceChecks in @joeblew999/remy-ui/checks: Core Web Vitals through Google's lighthouse package (optional peer) over Playwright's Chrome, gated on Google's "good" thresholds (LCP 2.5 s, CLS 0.1, TBT 200 ms) and a Performance score of at least 0.9, with the HTML report attached. The Chrome DevTools CLI excludes that category by design. playwrightConfig() runs tests/performance.spec.ts alone after every other file, so parallel browsers cannot inflate the timings, and Lighthouse measures a page the check has already warmed, since a fresh browser's cold font scan is not a page cost.

Changed

  • Every component in src/components is generated by the pinned shadcn CLI from the official base-nova registry (button, card, input, label, badge, alert, separator, field); mise run ui:components regenerates them and the gate fails on any drift. Exported as @joeblew999/remy-ui/components/*.
  • LanguageSwitcher and LanguageHint are built from those components (Alert, Button) and Tailwind utilities; the package ships no component CSS of its own.

0.4.0 - 2026-09-24

Added

  • @joeblew999/remy-ui/playwright: playwrightConfig(), the shared Playwright configuration (local target on Cloudflare's local host at PREVIEW_PORT, remote target from TEST_BASE_URL, per-target HTML reports), so a project's config is one call.

0.3.0 - 2026-09-24

Added

  • @joeblew999/remy-ui/checks: shared Playwright checks (publicPageChecks, entryChecks, demoChecks, formatsChecks, lighthouseChecks, plus collectErrors, endonym, direction, localizedPath) so every app built on the package runs the same Google-facing checks; @playwright/test is an optional peer.
  • @joeblew999/remy-ui/samples: the fixed sample values the formats and demo pages render and the checks expect.

0.2.0 - 2026-09-24

Added

  • Paraglide strategies url, cookie, preferredLanguage, baseLocale with URL patterns that keep every locale prefixed; @joeblew999/remy-ui/locale re-exports the runtime's getLocale, setLocale, localizeHref, localizeUrl, deLocalizeHref, cookieName and text direction.
  • @joeblew999/remy-ui/language: LanguageSwitcher and LanguageHint on Paraglide's localized hrefs and setLocale, with their styles in styles.css.
  • @joeblew999/remy-ui/react-router: languageMiddleware (Paraglide's middleware as root middleware), suggestedLocale, redirectToLocalized and pageMeta; react-router is an optional peer.
  • @joeblew999/remy-ui/seo: alternates built from Paraglide's URL patterns.
  • @joeblew999/remy-ui/client: useSuggestedLocale and DeviceTime for prerendered apps.
  • @joeblew999/remy-ui/cloudflare: placeFromCloudflare.
  • @joeblew999/remy-ui/runtime: the generated runtime as plain JavaScript for build configs.

Changed

  • The compiler options live in packages/ui/paraglide.mjs, shared by the Vite plugin and mise run ui:generate. The remembered-choice cookie is Paraglide's PARAGLIDE_LOCALE.

0.1.0 - 2026-09-24

Added

  • @joeblew999/remy-ui/button: the shadcn base-nova / Base UI button with Remy's variants.
  • @joeblew999/remy-ui/styles.css: Remy's stone/orange theme tokens for light and dark mode.
  • @joeblew999/remy-ui/messages: compiled Paraglide catalogs for English, Spanish and Arabic, including number, datetime, relativetime and plural formatters. The Arabic catalog is agent-authored and not yet reviewed.
  • @joeblew999/remy-ui/locale: the locale list, isLocale, baseLocale, reading direction and endonym localeName from Intl.
  • @joeblew999/remy-ui/locale-info: a locale's calendars, numbering system, hour cycle and week conventions, plus weekdayName.

On this page