From ebefa4e24f197130cbdc18d23dc126518da5e81c Mon Sep 17 00:00:00 2001 From: interacsean Date: Wed, 12 Aug 2026 10:30:01 +1000 Subject: [PATCH 1/9] docs: document removal of the 1.5.0 theme bridge workaround (#1646) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apps that adopted the 1.5.0-era workaround — pasting the @theme inline block, @custom-variant dark, and a copy of the palette into their entry CSS — keep those definitions winning over AppShell's own palette on 1.6+, because AppShell imports it inside layer(theme.defaults) while consumer CSS is unlayered. Nothing warns, and dark mode breaks silently. - docs/concepts/styling-theming.md: add "Overriding tokens and cascade layers" and "Upgrading from 1.5.x", covering detection, removal, the correct minimal entry CSS, and how to verify the fix. Fix the import order in the palette example while here. - catalogue design-system.md (source of the shipped app-shell-patterns skill): stop instructing apps to import the deprecated theme.css no-op shim, document dark mode as the .dark class rather than [data-theme="dark"], and warn against copying the palette. Co-Authored-By: Claude Opus 5 --- .changeset/gentle-moons-repeat.md | 9 ++ catalogue/src/fundamental/design-system.md | 32 +++--- docs/concepts/styling-theming.md | 122 ++++++++++++++++++++- 3 files changed, 147 insertions(+), 16 deletions(-) create mode 100644 .changeset/gentle-moons-repeat.md diff --git a/.changeset/gentle-moons-repeat.md b/.changeset/gentle-moons-repeat.md new file mode 100644 index 00000000..6003ee7a --- /dev/null +++ b/.changeset/gentle-moons-repeat.md @@ -0,0 +1,9 @@ +--- +"@tailor-platform/app-shell": patch +--- + +Document how to remove the 1.5.0 `@theme` bridge workaround, which silently breaks dark mode on 1.6+. + +Apps that pasted the `@theme inline` block, `@custom-variant dark (&:is(.dark *))`, and a copy of the palette into their entry CSS (the recommended workaround while `styles` was missing the bridge) keep those definitions winning over AppShell's own palette, because AppShell imports it inside `layer(theme.defaults)` and consumer CSS is unlayered. The build succeeds with no warning, but surfaces added since the copy render light-mode colours in dark mode. + +`docs/concepts/styling-theming.md` now covers how to detect and remove the workaround, and what token overrides should look like given the cascade layer. The bundled `app-shell-patterns` skill no longer tells apps to import the deprecated `@tailor-platform/app-shell/theme.css` shim, and documents dark mode as the `.dark` class rather than `[data-theme="dark"]`. diff --git a/catalogue/src/fundamental/design-system.md b/catalogue/src/fundamental/design-system.md index 78b4d583..dd5a00d3 100644 --- a/catalogue/src/fundamental/design-system.md +++ b/catalogue/src/fundamental/design-system.md @@ -2,7 +2,7 @@ Authority for **visual-only** decisions — tokens, theme imports, breakpoints intent, `**astw:`** rules, and custom-component conformance. For **React component APIs** (imports, props, JSX composition), pair this file with `**components.md`\*\*; that split avoids duplicating tables and lengthy examples across both docs. -`@tailor-platform/app-shell` (ERP scaffolds target **≥0.36**; bump your app’s pinned version deliberately) ships an opinionated design system via CSS variables and a `theme.css` import. Use it whether you are consuming AppShell components (most cases) or building a custom component to fill a gap. +`@tailor-platform/app-shell` (ERP scaffolds target **≥0.36**; bump your app’s pinned version deliberately) ships an opinionated design system via CSS variables, delivered by the `styles` import. Use it whether you are consuming AppShell components (most cases) or building a custom component to fill a gap. **The tokens are the rails.** Consistency across customers, apps, and AI runs comes from the token system, not from rules written in prose. A hand-typed `#fff` or `padding: 13px` is not a "small deviation" — it is the mechanism by which consistency dies. Every visual value you reach for must resolve to a token in this file. If a token is missing, add one; never inline. @@ -15,38 +15,40 @@ Authoritative app wiring also lives in `**project-setup.md`**; **scaffold `index @import "tw-animate-css"; @import "@tailor-platform/app-shell/styles"; -@import "@tailor-platform/app-shell/theme.css"; ``` -Adjust if your App Shell version documents a different barrel filename, but keep this split: +That is the whole wiring: -- `**theme.css**` — design tokens as CSS variables on `:root`. -- `**styles**` (package export) — bundled component styles AppShell ships for primitives. -- `**tailwindcss**` — utilities; token-backed classes (`bg-surface-1`, `text-fg-muted`) resolve through the theme. +- `**styles**` (package export) — design tokens as CSS variables (light **and** dark), the Tailwind v4 `@theme inline` bridge, the `dark` custom variant, and the bundled component styles AppShell ships for primitives. One import, everything. +- `**tailwindcss**` — utilities; token-backed classes (`bg-background`, `text-muted-foreground`) resolve through the bridge that `styles` provides. -Older docs referred to `app-shell.css`; prefer the `**styles**` import the template uses. +Older docs referred to `app-shell.css` or to a separate `@tailor-platform/app-shell/theme.css` import; use neither. Since 1.6.0 `theme.css` is a deprecated no-op shim kept only so pre-1.6 apps keep building. + +**Do not paste a `@theme inline` block, a `@custom-variant dark` rule, or a copy of AppShell's palette into the app's entry CSS.** A 1.5.0-era workaround did exactly that; on 1.6+ those unlayered copies beat AppShell's layered palette and silently break dark mode. See [Styling and Theming → Upgrading from 1.5.x](https://github.com/tailor-platform/app-shell/blob/main/docs/concepts/styling-theming.md#upgrading-from-15x-remove-the-theme-bridge-workaround) for the removal and detection steps. Tailwind v4 stays CSS-first; minimal `vite` / PostCSS wiring is in `**project-setup.md**`. ## 2. Theming via CSS variables -AppShell controls its theme through CSS variables. Override them in `:root` (global) or a scoped selector (per-section, per-tenant, dark mode) to customize. Any token defined in `theme.css` can be overridden after the import. +AppShell controls its theme through CSS variables. Override them in `:root` (global) or a scoped selector (per-section, per-tenant) to customize. AppShell's palette is imported inside a cascade layer, so your unlayered declarations win after the import. + +Override **only** the specific tokens you mean to change, and set each one in both modes — a `:root`-only override leaks its light value into dark mode: ```css :root { - --color-primary: #3b82f6; - --color-background: #ffffff; + --primary: #3b82f6; + --background: #ffffff; } -[data-theme="dark"] { - --color-background: #0a0a0a; - --color-foreground: #fafafa; +.dark { + --primary: #60a5fa; + --background: #0a0a0a; } ``` -Override at the highest scope where the change applies. Do not duplicate token values across files — change them at the source. +Override at the highest scope where the change applies. Do not duplicate token values across files — change them at the source. Never copy the palette wholesale; tokens you did not copy stay on AppShell's values and the two halves drift apart on every upgrade. -**Dark mode** is supported via `[data-theme="dark"]` on the root element. AppShell primitives respect it automatically. Custom components inherit dark-mode behaviour for free as long as they reference tokens (`bg-surface-1`, `text-fg-default`) and never inline literal colors. +**Dark mode** is driven by a `.dark` class on the root element, managed by AppShell (`useTheme()` / ``). AppShell primitives respect it automatically. Custom components inherit dark-mode behaviour for free as long as they reference tokens (`bg-background`, `text-foreground`) and never inline literal colors. ## 3. Component styling with data attributes diff --git a/docs/concepts/styling-theming.md b/docs/concepts/styling-theming.md index 475e2b2e..da342976 100644 --- a/docs/concepts/styling-theming.md +++ b/docs/concepts/styling-theming.md @@ -14,6 +14,8 @@ To configure your application, import AppShell styles from your global CSS or to @import "@tailor-platform/app-shell/styles"; ``` +That is the whole setup. `styles` already ships the palette (light **and** dark), the Tailwind v4 `@theme inline` bridge, and the `dark` custom variant — your entry CSS should not declare any of them itself. If yours contains a `@theme inline` block, a `@custom-variant dark` rule, or copies of AppShell's palette tokens, see [Upgrading from 1.5.x](#upgrading-from-15x-remove-the-theme-bridge-workaround): they will silently break dark mode. + If you want a branded palette, import exactly one theme file after `styles`: ```css @@ -90,13 +92,131 @@ AppShell ships three palettes, each with light and dark variants: Select a palette by importing its CSS file — no prop needed. Import it in your global CSS **after** `@tailor-platform/app-shell/styles`: ```css +@import "tailwindcss"; @import "@tailor-platform/app-shell/styles"; @import "@tailor-platform/app-shell/themes/cream"; /* overrides default palette */ -@import "tailwindcss"; ``` Only import one palette at a time. +## Overriding tokens and cascade layers + +AppShell's palette is imported inside a cascade layer: + +```css +/* inside @tailor-platform/app-shell/styles */ +@import "./themes/default.css" layer(theme.defaults); +``` + +Anything you declare in your own CSS is **unlayered**, and unlayered declarations always beat layered ones — regardless of specificity or source order. That is deliberate: it is what lets you override a token without `!important` or import-order games. + +```css +@import "tailwindcss"; +@import "@tailor-platform/app-shell/styles"; + +/* Wins over the layered default. This is the supported way to override. */ +:root { + --radius: 0.5rem; +} +``` + +The trap is that this cuts both ways. Because your declarations always win, a **partial** copy of AppShell's palette silently pins whatever it defines, and any token you did not copy stays on AppShell's value. The two halves then drift apart across upgrades. + +Two rules keep you out of trouble: + +- Override the specific tokens you mean to change, and nothing else. Never copy the palette wholesale. +- If you override a token in `:root`, override it in `.dark` too. A `:root`-only override applies in both modes, so a light-mode value will leak into dark mode. + +```css +/* Good — one token, both modes */ +:root { + --card: rgba(255, 255, 255, 1); +} + +.dark { + --card: rgba(23, 23, 23, 1); +} +``` + +## Upgrading from 1.5.x: remove the theme bridge workaround + +**Applies to:** apps upgrading from 1.5.x to 1.6 or later that adopted the documented 1.5.0 workaround. + +In 1.5.0 the `@tailor-platform/app-shell/theme.css` export stopped shipping the Tailwind v4 theme bridge, and the recommended workaround was to paste the `@theme inline` block, a `@custom-variant dark` rule, and the palette definitions into your app's entry CSS. + +From 1.6.0 onwards `@tailor-platform/app-shell/styles` ships all three again, so the workaround is no longer needed. **It is not merely redundant — leaving it in place actively breaks dark mode**, and the build succeeds with no warning: + +- Your pasted `:root` and `.dark` palette blocks are unlayered, so they beat AppShell's layered palette. The app renders your frozen 1.5.0-era colours forever. Any surface added to AppShell after you copied the palette has no dark value at all, so it renders light-mode colours in dark mode — white text on white cards, unreadable values in disabled inputs. +- The workaround's `@custom-variant dark (&:is(.dark *))` overrides AppShell's `@custom-variant dark (&:where(.dark, .dark *))`. The `:is(.dark *)` form matches only _descendants_ of `.dark`, so `dark:` utilities stop applying to the `.dark` element itself. +- Because the symptoms look like ordinary app-side CSS bugs, they are expensive to trace back to the workaround. + +### Detecting it + +Search your app's CSS for the three shapes of the workaround: + +```bash +grep -rn "@theme inline\|@custom-variant dark\|--card:\|--background:" src --include="*.css" +``` + +Hits inside your own entry CSS are the workaround (or overrides that need the treatment described in [Overriding tokens and cascade layers](#overriding-tokens-and-cascade-layers)). A clean app has none. + +### Fixing it + +Delete from your entry CSS: + +- the entire `@theme inline { … }` block, +- the `@custom-variant dark (…)` rule, +- every `:root` and `.dark` block that redefines AppShell palette tokens (`--background`, `--foreground`, `--card`, `--popover`, `--muted`, `--border`, `--input`, `--primary`, `--secondary`, `--accent`, `--destructive`, `--ring`, `--radius`, `--chart-*`, `--sidebar-*`), +- any `@import "@tailor-platform/app-shell/theme.css"` (a deprecated no-op shim since 1.6.0 — kept only so old apps keep building). + +Keep only genuine app-specific rules. The correct end state is short: + +```css +/* Before — 1.5.0 workaround, breaks dark mode on 1.6+ */ +@import "tailwindcss"; +@import "@tailor-platform/app-shell/styles"; +@import "@tailor-platform/app-shell/theme.css"; + +@custom-variant dark (&:is(.dark *)); + +@theme inline { + --color-background: var(--background); + --color-card: var(--card); + /* …dozens more… */ +} + +:root { + --background: rgba(250, 250, 250, 1); + --card: rgba(255, 255, 255, 1); + /* …the whole palette… */ +} + +.dark { + --background: rgba(10, 10, 10, 1); + /* …the whole dark palette… */ +} +``` + +```css +/* After — everything comes from `styles` */ +@import "tailwindcss"; +@import "@tailor-platform/app-shell/styles"; + +html, +body { + margin: 0; + padding: 0; +} +``` + +[`examples/vite-app/src/index.css`](https://github.com/tailor-platform/app-shell/blob/main/examples/vite-app/src/index.css) is a working reference for this shape. + +If you need to change specific colours, add a branded palette import (see [Theme Palettes](#theme-palettes)) or override individual tokens per [Overriding tokens and cascade layers](#overriding-tokens-and-cascade-layers) — do not reinstate the copied palette. + +### Verifying the fix + +Switch the app to dark mode and confirm the computed values flip. Against the default palette you should see `--card` go from `#fff` to `#171717`, `--input` from `#e5e5e5` to `rgba(255, 255, 255, 0.15)`, and `--destructive` from `#dc2626` to `#f87171`. + ## Z-Index Layering AppShell defines CSS custom properties for z-index values so you can adjust the stacking order to integrate with other libraries or overlays in your application. From 8730922c14225b546279f68a5b8f9a33dc0cdbc9 Mon Sep 17 00:00:00 2001 From: interacsean Date: Wed, 12 Aug 2026 12:46:37 +1000 Subject: [PATCH 2/9] docs: correct the version the theme bridge was restored in, and tighten MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adversarial review found the migration note named the wrong version and would have broken apps that followed it. - The bridge came back in 1.7.0 (ff30974), not 1.6.0. 1.6.0 only restored theme.css as a no-op shim; theme.bridge.css does not exist at 1.6.1. On 1.5.0-1.6.1 the workaround is load-bearing, so the section now says to upgrade to >=1.7.0 before deleting anything. - Recommend :root / :root.dark for overrides. Only the default palette is layered; cream and bloom are imported unlayered and define dark values on :root.dark, which outranks a bare .dark, so the previously documented .dark override silently lost on branded palettes. - The delete list omitted the *-foreground pairs (plus --status-*, --alert-*, --semantic-shadow-*) — the very tokens behind white-on-white text. Say "all of it" rather than enumerating a partial set. - The detection grep had no term for the theme.css import and hardcoded src/, missing app/ and styles/. Broadened, and dropped the false all-clear that followed it. - Verification quoted hex values that never appear: custom properties compute to their authored token stream, so getPropertyValue("--card") echoes the copy either way. Check a Card's rendered background-color. - design-system.md §4 still attributed tokens to theme.css, contradicting §1 two sections earlier in the same shipped skill. Also tightened both new sections: dropped the redundant re-derivation of the layer mechanism, the 24-line "before" block, and one H3 level. Co-Authored-By: Claude Opus 5 --- catalogue/src/fundamental/design-system.md | 14 ++- docs/concepts/styling-theming.md | 105 ++++++--------------- 2 files changed, 35 insertions(+), 84 deletions(-) diff --git a/catalogue/src/fundamental/design-system.md b/catalogue/src/fundamental/design-system.md index dd5a00d3..2ebf50d1 100644 --- a/catalogue/src/fundamental/design-system.md +++ b/catalogue/src/fundamental/design-system.md @@ -19,30 +19,28 @@ Authoritative app wiring also lives in `**project-setup.md`**; **scaffold `index That is the whole wiring: -- `**styles**` (package export) — design tokens as CSS variables (light **and** dark), the Tailwind v4 `@theme inline` bridge, the `dark` custom variant, and the bundled component styles AppShell ships for primitives. One import, everything. +- `**styles**` (package export) — design tokens as CSS variables (light **and** dark), the Tailwind v4 `@theme inline` bridge, the `dark` custom variant, and the bundled component styles AppShell ships for primitives. One import, everything (since 1.7.0). - `**tailwindcss**` — utilities; token-backed classes (`bg-background`, `text-muted-foreground`) resolve through the bridge that `styles` provides. -Older docs referred to `app-shell.css` or to a separate `@tailor-platform/app-shell/theme.css` import; use neither. Since 1.6.0 `theme.css` is a deprecated no-op shim kept only so pre-1.6 apps keep building. +Older docs referred to `app-shell.css` or to a separate `@tailor-platform/app-shell/theme.css` import; use neither. `theme.css` is a deprecated no-op shim kept only so pre-1.6 apps keep building. -**Do not paste a `@theme inline` block, a `@custom-variant dark` rule, or a copy of AppShell's palette into the app's entry CSS.** A 1.5.0-era workaround did exactly that; on 1.6+ those unlayered copies beat AppShell's layered palette and silently break dark mode. See [Styling and Theming → Upgrading from 1.5.x](https://github.com/tailor-platform/app-shell/blob/main/docs/concepts/styling-theming.md#upgrading-from-15x-remove-the-theme-bridge-workaround) for the removal and detection steps. +**Do not paste a `@theme inline` block, a `@custom-variant dark` rule, or a copy of AppShell's palette into the app's entry CSS.** A workaround for 1.5.0–1.6.1, where `styles` shipped without the bridge, did exactly that; from 1.7.0 those unlayered copies beat AppShell's layered palette and silently break dark mode. On 1.6.x the workaround is still load-bearing — upgrade to ≥1.7.0 before removing it. See [Styling and Theming → Upgrading from 1.5.x or 1.6.x](https://github.com/tailor-platform/app-shell/blob/main/docs/concepts/styling-theming.md#upgrading-from-15x-or-16x-remove-the-theme-bridge-workaround) for the removal steps. Tailwind v4 stays CSS-first; minimal `vite` / PostCSS wiring is in `**project-setup.md**`. ## 2. Theming via CSS variables -AppShell controls its theme through CSS variables. Override them in `:root` (global) or a scoped selector (per-section, per-tenant) to customize. AppShell's palette is imported inside a cascade layer, so your unlayered declarations win after the import. +AppShell controls its theme through CSS variables. Override them after the AppShell imports, using `:root` for light and `:root.dark` for dark — that pair wins against both the layered default palette and the unlayered branded ones (`cream`, `bloom`), which define their dark values on `:root.dark` and so outrank a bare `.dark`. Override **only** the specific tokens you mean to change, and set each one in both modes — a `:root`-only override leaks its light value into dark mode: ```css :root { --primary: #3b82f6; - --background: #ffffff; } -.dark { +:root.dark { --primary: #60a5fa; - --background: #0a0a0a; } ``` @@ -76,7 +74,7 @@ Check each component's API reference (`components.md`) for the data attributes i ## 4. Tokens -All values below are exposed by `theme.css`. **Use the token, never hand-type the value.** A hex literal or magic px in a PR is a review failure. +The values below come from the `styles` import. **Use the token, never hand-type the value.** A hex literal or magic px in a PR is a review failure. ### Color diff --git a/docs/concepts/styling-theming.md b/docs/concepts/styling-theming.md index da342976..6ecf4d04 100644 --- a/docs/concepts/styling-theming.md +++ b/docs/concepts/styling-theming.md @@ -14,7 +14,7 @@ To configure your application, import AppShell styles from your global CSS or to @import "@tailor-platform/app-shell/styles"; ``` -That is the whole setup. `styles` already ships the palette (light **and** dark), the Tailwind v4 `@theme inline` bridge, and the `dark` custom variant — your entry CSS should not declare any of them itself. If yours contains a `@theme inline` block, a `@custom-variant dark` rule, or copies of AppShell's palette tokens, see [Upgrading from 1.5.x](#upgrading-from-15x-remove-the-theme-bridge-workaround): they will silently break dark mode. +That is the whole setup. Since 1.7.0, `styles` ships the palette (light **and** dark), the Tailwind v4 `@theme inline` bridge, and the `dark` custom variant, so your entry CSS should declare none of them itself. If yours does, see [Upgrading from 1.5.x or 1.6.x](#upgrading-from-15x-or-16x-remove-the-theme-bridge-workaround) — those leftovers silently break dark mode. If you want a branded palette, import exactly one theme file after `styles`: @@ -99,106 +99,61 @@ Select a palette by importing its CSS file — no prop needed. Import it in your Only import one palette at a time. -## Overriding tokens and cascade layers +## Overriding tokens -AppShell's palette is imported inside a cascade layer: - -```css -/* inside @tailor-platform/app-shell/styles */ -@import "./themes/default.css" layer(theme.defaults); -``` - -Anything you declare in your own CSS is **unlayered**, and unlayered declarations always beat layered ones — regardless of specificity or source order. That is deliberate: it is what lets you override a token without `!important` or import-order games. +Redeclare any token after the AppShell imports. Use `:root` for light and `:root.dark` for dark — that pair wins against every palette AppShell ships: ```css @import "tailwindcss"; @import "@tailor-platform/app-shell/styles"; -/* Wins over the layered default. This is the supported way to override. */ -:root { - --radius: 0.5rem; -} -``` - -The trap is that this cuts both ways. Because your declarations always win, a **partial** copy of AppShell's palette silently pins whatever it defines, and any token you did not copy stays on AppShell's value. The two halves then drift apart across upgrades. - -Two rules keep you out of trouble: - -- Override the specific tokens you mean to change, and nothing else. Never copy the palette wholesale. -- If you override a token in `:root`, override it in `.dark` too. A `:root`-only override applies in both modes, so a light-mode value will leak into dark mode. - -```css -/* Good — one token, both modes */ :root { - --card: rgba(255, 255, 255, 1); + --primary: #2563eb; } -.dark { - --card: rgba(23, 23, 23, 1); +:root.dark { + --primary: #60a5fa; } ``` -## Upgrading from 1.5.x: remove the theme bridge workaround - -**Applies to:** apps upgrading from 1.5.x to 1.6 or later that adopted the documented 1.5.0 workaround. +Two rules: -In 1.5.0 the `@tailor-platform/app-shell/theme.css` export stopped shipping the Tailwind v4 theme bridge, and the recommended workaround was to paste the `@theme inline` block, a `@custom-variant dark` rule, and the palette definitions into your app's entry CSS. +- **Set each override in both modes.** A `:root`-only override applies in dark mode too, so a light value leaks there. +- **Override individual tokens; never copy the palette wholesale.** Copied tokens freeze at the value you copied while everything else tracks AppShell, and the two halves drift apart on upgrade. That is the failure described in [Upgrading from 1.5.x or 1.6.x](#upgrading-from-15x-or-16x-remove-the-theme-bridge-workaround). -From 1.6.0 onwards `@tailor-platform/app-shell/styles` ships all three again, so the workaround is no longer needed. **It is not merely redundant — leaving it in place actively breaks dark mode**, and the build succeeds with no warning: +`:root.dark` rather than `.dark` because the two palette families behave differently. The default palette is imported inside a cascade layer (`layer(theme.defaults)`), so any unlayered declaration of yours beats it. The branded palettes (`cream`, `bloom`) are imported by you, unlayered, and define dark values on `:root.dark` — which outranks a bare `.dark`, so a `.dark` override would silently lose. `:root.dark` is correct against both. -- Your pasted `:root` and `.dark` palette blocks are unlayered, so they beat AppShell's layered palette. The app renders your frozen 1.5.0-era colours forever. Any surface added to AppShell after you copied the palette has no dark value at all, so it renders light-mode colours in dark mode — white text on white cards, unreadable values in disabled inputs. -- The workaround's `@custom-variant dark (&:is(.dark *))` overrides AppShell's `@custom-variant dark (&:where(.dark, .dark *))`. The `:is(.dark *)` form matches only _descendants_ of `.dark`, so `dark:` utilities stop applying to the `.dark` element itself. -- Because the symptoms look like ordinary app-side CSS bugs, they are expensive to trace back to the workaround. +## Upgrading from 1.5.x or 1.6.x: remove the theme bridge workaround -### Detecting it +**Applies to:** apps that pasted the `@theme inline` block, `@custom-variant dark`, and AppShell's palette into their entry CSS — the workaround for `styles` shipping without the Tailwind bridge. -Search your app's CSS for the three shapes of the workaround: +**`styles` regained the bridge in 1.7.0.** On 1.5.0–1.6.1 the workaround is load-bearing, so upgrade to 1.7.0 or later _before_ deleting any of it. Remove it earlier and every token-backed utility — `bg-card`, `text-muted-foreground`, `rounded-lg`, all `dark:` variants — stops resolving. -```bash -grep -rn "@theme inline\|@custom-variant dark\|--card:\|--background:" src --include="*.css" -``` +From 1.7.0 the workaround is not merely redundant. It actively breaks dark mode, and the build succeeds with no warning: -Hits inside your own entry CSS are the workaround (or overrides that need the treatment described in [Overriding tokens and cascade layers](#overriding-tokens-and-cascade-layers)). A clean app has none. +- Your pasted `:root` and `.dark` blocks are unlayered, so they beat AppShell's layered default palette. Colours freeze at the values you copied, and any surface AppShell has added since has no dark value at all — so it renders light colours in dark mode: white text on white cards, unreadable disabled inputs. +- `@custom-variant dark (&:is(.dark *))` overrides AppShell's `&:where(.dark, .dark *)`. The `:is(.dark *)` form matches only _descendants_ of `.dark`, so `dark:` utilities stop applying to the `.dark` element itself. -### Fixing it +### Removing it Delete from your entry CSS: -- the entire `@theme inline { … }` block, +- the `@theme inline { … }` block, - the `@custom-variant dark (…)` rule, -- every `:root` and `.dark` block that redefines AppShell palette tokens (`--background`, `--foreground`, `--card`, `--popover`, `--muted`, `--border`, `--input`, `--primary`, `--secondary`, `--accent`, `--destructive`, `--ring`, `--radius`, `--chart-*`, `--sidebar-*`), -- any `@import "@tailor-platform/app-shell/theme.css"` (a deprecated no-op shim since 1.6.0 — kept only so old apps keep building). +- every `:root` and `.dark` block copied from AppShell's palette — **all** of it, including the `*-foreground` pairs, `--status-*`, `--alert-*`, `--sidebar-*` and `--semantic-shadow-*`. The foregrounds are what leave text white on white, so a partial deletion reproduces the bug. +- any `@import "@tailor-platform/app-shell/theme.css"` (a no-op shim since 1.6.0, kept only so older apps keep building). -Keep only genuine app-specific rules. The correct end state is short: +To find it, search every CSS file the app loads — not just the entry point, since `app/` and `styles/` are as common as `src/`: -```css -/* Before — 1.5.0 workaround, breaks dark mode on 1.6+ */ -@import "tailwindcss"; -@import "@tailor-platform/app-shell/styles"; -@import "@tailor-platform/app-shell/theme.css"; - -@custom-variant dark (&:is(.dark *)); - -@theme inline { - --color-background: var(--background); - --color-card: var(--card); - /* …dozens more… */ -} +```bash +grep -rnE "@theme inline|@custom-variant|app-shell/theme\.css|^ *--" --include="*.css" . +``` -:root { - --background: rgba(250, 250, 250, 1); - --card: rgba(255, 255, 255, 1); - /* …the whole palette… */ -} +Hits are either the workaround, which goes, or deliberate overrides, which should take the `:root` / `:root.dark` form above. -.dark { - --background: rgba(10, 10, 10, 1); - /* …the whole dark palette… */ -} -``` +What remains is short — [`examples/vite-app/src/index.css`](https://github.com/tailor-platform/app-shell/blob/main/examples/vite-app/src/index.css) is a working reference for the shape (it also imports a branded palette, which is optional): ```css -/* After — everything comes from `styles` */ @import "tailwindcss"; @import "@tailor-platform/app-shell/styles"; @@ -209,13 +164,11 @@ body { } ``` -[`examples/vite-app/src/index.css`](https://github.com/tailor-platform/app-shell/blob/main/examples/vite-app/src/index.css) is a working reference for this shape. - -If you need to change specific colours, add a branded palette import (see [Theme Palettes](#theme-palettes)) or override individual tokens per [Overriding tokens and cascade layers](#overriding-tokens-and-cascade-layers) — do not reinstate the copied palette. +### Verifying -### Verifying the fix +Toggle dark mode and confirm a real surface changes: inspect a `Card` and watch its computed `background-color` go from `rgb(255, 255, 255)` to `rgb(23, 23, 23)` on the default palette. -Switch the app to dark mode and confirm the computed values flip. Against the default palette you should see `--card` go from `#fff` to `#171717`, `--input` from `#e5e5e5` to `rgba(255, 255, 255, 0.15)`, and `--destructive` from `#dc2626` to `#f87171`. +Check the rendered colour, not the variable. `getPropertyValue("--card")` echoes whatever value is authored, so it reads the same whether the token came from AppShell or from a stale copy. ## Z-Index Layering From 43edad25a55fb08476eac034a6ee1b53d546320a Mon Sep 17 00:00:00 2001 From: interacsean Date: Wed, 12 Aug 2026 12:47:03 +1000 Subject: [PATCH 3/9] docs: correct version references in the changeset Co-Authored-By: Claude Opus 5 --- .changeset/gentle-moons-repeat.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.changeset/gentle-moons-repeat.md b/.changeset/gentle-moons-repeat.md index 6003ee7a..f7675a74 100644 --- a/.changeset/gentle-moons-repeat.md +++ b/.changeset/gentle-moons-repeat.md @@ -2,8 +2,8 @@ "@tailor-platform/app-shell": patch --- -Document how to remove the 1.5.0 `@theme` bridge workaround, which silently breaks dark mode on 1.6+. +Document how to remove the `@theme` bridge workaround, which silently breaks dark mode on 1.7+. -Apps that pasted the `@theme inline` block, `@custom-variant dark (&:is(.dark *))`, and a copy of the palette into their entry CSS (the recommended workaround while `styles` was missing the bridge) keep those definitions winning over AppShell's own palette, because AppShell imports it inside `layer(theme.defaults)` and consumer CSS is unlayered. The build succeeds with no warning, but surfaces added since the copy render light-mode colours in dark mode. +Apps that pasted the `@theme inline` block, `@custom-variant dark (&:is(.dark *))`, and a copy of the palette into their entry CSS (the workaround while `styles` shipped without the bridge, 1.5.0–1.6.1) keep those definitions winning over AppShell's own palette, because the default palette is imported inside `layer(theme.defaults)` and consumer CSS is unlayered. The build succeeds with no warning, but surfaces added since the copy render light-mode colours in dark mode. -`docs/concepts/styling-theming.md` now covers how to detect and remove the workaround, and what token overrides should look like given the cascade layer. The bundled `app-shell-patterns` skill no longer tells apps to import the deprecated `@tailor-platform/app-shell/theme.css` shim, and documents dark mode as the `.dark` class rather than `[data-theme="dark"]`. +`docs/concepts/styling-theming.md` now covers how to detect and remove the workaround — including the requirement to be on 1.7.0 or later first, since the workaround is load-bearing before that — and documents `:root` / `:root.dark` as the override form that holds against both the layered default palette and the unlayered branded ones. The bundled `app-shell-patterns` skill no longer tells apps to import the deprecated `@tailor-platform/app-shell/theme.css` shim, and documents dark mode as the `.dark` class rather than `[data-theme="dark"]`. From ff342b05619176b4d922298715f444500495e2cd Mon Sep 17 00:00:00 2001 From: interacsean Date: Wed, 12 Aug 2026 15:06:38 +1000 Subject: [PATCH 4/9] docs: fix issues the second adversarial pass found, two self-inflicted MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - The detection grep's `^ *--` term matched every custom property in the tree and `grep -r .` walked node_modules: on a fixture with just two AppShell palette files installed, 163 of 168 hits were library CSS the reader must not touch — under a sentence saying every hit is actionable. Now matches AppShell-specific token names with --exclude-dir, which also catches tab-indented properties the old anchor missed (6/6 real hits on the same fixture). - "getPropertyValue echoes whatever is authored" was wrong. The computed value is the winning declaration, which is exactly how the reporter diagnosed #1646. The real caveat is notation: AppShell authors rgba(23, 23, 23, 1), so a hex comparison fails even when correct. - ":root-only leaks into dark mode" holds only for the layered default palette. Against cream/bloom's :root.dark (0,2,0) a consumer's :root (0,1,0) loses, so the override stops applying in dark instead. Both symptoms now stated. - The :root.dark recommendation had dropped scoped overrides while design-system.md kept telling readers to use them. Restored, with the selector shape they need, plus a note that :root.dark does not match a subtree .dark region. - Tightened the 1.6.x warning: rounded-lg resolves to Tailwind's default rather than failing, and dark: falls back to prefers-color-scheme. Co-Authored-By: Claude Opus 5 --- catalogue/src/fundamental/design-system.md | 4 ++-- docs/concepts/styling-theming.md | 12 +++++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/catalogue/src/fundamental/design-system.md b/catalogue/src/fundamental/design-system.md index 2ebf50d1..5e251b9c 100644 --- a/catalogue/src/fundamental/design-system.md +++ b/catalogue/src/fundamental/design-system.md @@ -32,7 +32,7 @@ Tailwind v4 stays CSS-first; minimal `vite` / PostCSS wiring is in `**project-se AppShell controls its theme through CSS variables. Override them after the AppShell imports, using `:root` for light and `:root.dark` for dark — that pair wins against both the layered default palette and the unlayered branded ones (`cream`, `bloom`), which define their dark values on `:root.dark` and so outrank a bare `.dark`. -Override **only** the specific tokens you mean to change, and set each one in both modes — a `:root`-only override leaks its light value into dark mode: +Override **only** the specific tokens you mean to change, and set each one in both modes. Overriding just `:root` misbehaves either way: on the default palette the light value carries into dark mode, and on a branded palette the override stops applying in dark mode altogether. ```css :root { @@ -44,7 +44,7 @@ Override **only** the specific tokens you mean to change, and set each one in bo } ``` -Override at the highest scope where the change applies. Do not duplicate token values across files — change them at the source. Never copy the palette wholesale; tokens you did not copy stay on AppShell's values and the two halves drift apart on every upgrade. +Override at the highest scope where the change applies. A narrower scope (per-section, per-tenant) needs the same both-modes treatment, and its dark rule must still outrank a branded palette's `:root.dark` — pair `.tenant-a` with `:root.dark .tenant-a`. Do not duplicate token values across files — change them at the source. Never copy the palette wholesale; tokens you did not copy stay on AppShell's values and the two halves drift apart on every upgrade. **Dark mode** is driven by a `.dark` class on the root element, managed by AppShell (`useTheme()` / ``). AppShell primitives respect it automatically. Custom components inherit dark-mode behaviour for free as long as they reference tokens (`bg-background`, `text-foreground`) and never inline literal colors. diff --git a/docs/concepts/styling-theming.md b/docs/concepts/styling-theming.md index 6ecf4d04..fb25151e 100644 --- a/docs/concepts/styling-theming.md +++ b/docs/concepts/styling-theming.md @@ -118,16 +118,18 @@ Redeclare any token after the AppShell imports. Use `:root` for light and `:root Two rules: -- **Set each override in both modes.** A `:root`-only override applies in dark mode too, so a light value leaks there. +- **Set each override in both modes.** Overriding only `:root` misbehaves either way: on the default palette the light value carries into dark mode, and on a branded palette the override stops applying in dark mode altogether. - **Override individual tokens; never copy the palette wholesale.** Copied tokens freeze at the value you copied while everything else tracks AppShell, and the two halves drift apart on upgrade. That is the failure described in [Upgrading from 1.5.x or 1.6.x](#upgrading-from-15x-or-16x-remove-the-theme-bridge-workaround). `:root.dark` rather than `.dark` because the two palette families behave differently. The default palette is imported inside a cascade layer (`layer(theme.defaults)`), so any unlayered declaration of yours beats it. The branded palettes (`cream`, `bloom`) are imported by you, unlayered, and define dark values on `:root.dark` — which outranks a bare `.dark`, so a `.dark` override would silently lose. `:root.dark` is correct against both. +Overriding under a narrower scope — per-section or per-tenant — needs the same care: pair `.tenant-a` with `:root.dark .tenant-a` so the dark rule still outranks a branded palette's `:root.dark`. Note also that `:root.dark` matches only ``; if you apply `.dark` to a subtree to darken one region, scope your overrides to that subtree rather than to `:root.dark`. + ## Upgrading from 1.5.x or 1.6.x: remove the theme bridge workaround **Applies to:** apps that pasted the `@theme inline` block, `@custom-variant dark`, and AppShell's palette into their entry CSS — the workaround for `styles` shipping without the Tailwind bridge. -**`styles` regained the bridge in 1.7.0.** On 1.5.0–1.6.1 the workaround is load-bearing, so upgrade to 1.7.0 or later _before_ deleting any of it. Remove it earlier and every token-backed utility — `bg-card`, `text-muted-foreground`, `rounded-lg`, all `dark:` variants — stops resolving. +**`styles` regained the bridge in 1.7.0.** On 1.5.0–1.6.1 the workaround is load-bearing, so upgrade to 1.7.0 or later _before_ deleting any of it. Remove it earlier and every AppShell-token utility — `bg-card`, `bg-background`, `text-muted-foreground`, `border-border` — stops resolving, while `dark:` variants fall back to Tailwind's `prefers-color-scheme` default and stop tracking the `.dark` class. From 1.7.0 the workaround is not merely redundant. It actively breaks dark mode, and the build succeeds with no warning: @@ -146,10 +148,10 @@ Delete from your entry CSS: To find it, search every CSS file the app loads — not just the entry point, since `app/` and `styles/` are as common as `src/`: ```bash -grep -rnE "@theme inline|@custom-variant|app-shell/theme\.css|^ *--" --include="*.css" . +grep -rnE "@theme inline|@custom-variant|app-shell/theme\.css|--(card|popover|muted|sidebar|destructive|accent)(-foreground)?:" --include="*.css" --exclude-dir=node_modules --exclude-dir=dist --exclude-dir=.next . ``` -Hits are either the workaround, which goes, or deliberate overrides, which should take the `:root` / `:root.dark` form above. +Excluding `node_modules` matters: AppShell's own palette files declare these tokens too, and they must not be touched. In your own CSS, hits are either the workaround, which goes, or deliberate overrides, which should take the `:root` / `:root.dark` form above. What remains is short — [`examples/vite-app/src/index.css`](https://github.com/tailor-platform/app-shell/blob/main/examples/vite-app/src/index.css) is a working reference for the shape (it also imports a branded palette, which is optional): @@ -168,7 +170,7 @@ body { Toggle dark mode and confirm a real surface changes: inspect a `Card` and watch its computed `background-color` go from `rgb(255, 255, 255)` to `rgb(23, 23, 23)` on the default palette. -Check the rendered colour, not the variable. `getPropertyValue("--card")` echoes whatever value is authored, so it reads the same whether the token came from AppShell or from a stale copy. +Reading the token directly also works — `getComputedStyle(document.documentElement).getPropertyValue("--card")` returns the winning declaration, so a stale copy shows up as its own value. Just compare against the authored notation: AppShell writes `rgba(23, 23, 23, 1)`, not `#171717`, and the computed value preserves that form. ## Z-Index Layering From 958733ea1b2949e23ab1c490af8f144fe5e72078 Mon Sep 17 00:00:00 2001 From: interacsean Date: Thu, 13 Aug 2026 12:09:58 +1000 Subject: [PATCH 5/9] docs: curate breaking changes in docs/migrations.md (review feedback) Per review on #432: version-specific migration steps are easier to find, and easier for AI agents to consume, in one dedicated place than spread across the concept doc they happen to touch. CHANGELOG.md nominally covers this but mixes breaking changes in with features and fixes. - Add docs/migrations.md: an index table plus one section per change, newest first, scoped to changes that require the reader to edit their app. Explicitly not a changelog, and entries are never pruned, since apps upgrade across arbitrary version gaps. - Move the theme bridge section there from concepts/styling-theming.md, which keeps only the evergreen "Overriding tokens" guidance and links out. - Repoint the two in-doc references and the shipped skill's link. - List the page in README.md's docs section. Co-Authored-By: Claude Opus 5 --- .changeset/gentle-moons-repeat.md | 4 +- README.md | 1 + catalogue/src/fundamental/design-system.md | 2 +- docs/concepts/styling-theming.md | 51 +----------------- docs/migrations.md | 63 ++++++++++++++++++++++ 5 files changed, 70 insertions(+), 51 deletions(-) create mode 100644 docs/migrations.md diff --git a/.changeset/gentle-moons-repeat.md b/.changeset/gentle-moons-repeat.md index f7675a74..7e0fb713 100644 --- a/.changeset/gentle-moons-repeat.md +++ b/.changeset/gentle-moons-repeat.md @@ -6,4 +6,6 @@ Document how to remove the `@theme` bridge workaround, which silently breaks dar Apps that pasted the `@theme inline` block, `@custom-variant dark (&:is(.dark *))`, and a copy of the palette into their entry CSS (the workaround while `styles` shipped without the bridge, 1.5.0–1.6.1) keep those definitions winning over AppShell's own palette, because the default palette is imported inside `layer(theme.defaults)` and consumer CSS is unlayered. The build succeeds with no warning, but surfaces added since the copy render light-mode colours in dark mode. -`docs/concepts/styling-theming.md` now covers how to detect and remove the workaround — including the requirement to be on 1.7.0 or later first, since the workaround is load-bearing before that — and documents `:root` / `:root.dark` as the override form that holds against both the layered default palette and the unlayered branded ones. The bundled `app-shell-patterns` skill no longer tells apps to import the deprecated `@tailor-platform/app-shell/theme.css` shim, and documents dark mode as the `.dark` class rather than `[data-theme="dark"]`. +Adds `docs/migrations.md`, a curated list of breaking changes and the steps each one requires, newest first — separate from the changelog so upgraders and AI agents have one narrow place to look. The first entry covers detecting and removing this workaround, including the requirement to be on 1.7.0 or later first, since the workaround is load-bearing before that. + +`docs/concepts/styling-theming.md` documents `:root` / `:root.dark` as the override form that holds against both the layered default palette and the unlayered branded ones. The bundled `app-shell-patterns` skill no longer tells apps to import the deprecated `@tailor-platform/app-shell/theme.css` shim, and documents dark mode as the `.dark` class rather than `[data-theme="dark"]`. diff --git a/README.md b/README.md index 4e656ffd..4f2bed2d 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ For users building applications with AppShell, see the detailed guides in `docs/ - [Introduction](./docs/introduction.md) — What is AppShell and why use it - [Quick Start](./docs/quickstart.md) — Installation, setup, and first steps +- [Migrations](./docs/migrations.md) — Breaking changes and the steps each one requires, newest first Hosted references, no checkout required: diff --git a/catalogue/src/fundamental/design-system.md b/catalogue/src/fundamental/design-system.md index 5e251b9c..a495e6e7 100644 --- a/catalogue/src/fundamental/design-system.md +++ b/catalogue/src/fundamental/design-system.md @@ -24,7 +24,7 @@ That is the whole wiring: Older docs referred to `app-shell.css` or to a separate `@tailor-platform/app-shell/theme.css` import; use neither. `theme.css` is a deprecated no-op shim kept only so pre-1.6 apps keep building. -**Do not paste a `@theme inline` block, a `@custom-variant dark` rule, or a copy of AppShell's palette into the app's entry CSS.** A workaround for 1.5.0–1.6.1, where `styles` shipped without the bridge, did exactly that; from 1.7.0 those unlayered copies beat AppShell's layered palette and silently break dark mode. On 1.6.x the workaround is still load-bearing — upgrade to ≥1.7.0 before removing it. See [Styling and Theming → Upgrading from 1.5.x or 1.6.x](https://github.com/tailor-platform/app-shell/blob/main/docs/concepts/styling-theming.md#upgrading-from-15x-or-16x-remove-the-theme-bridge-workaround) for the removal steps. +**Do not paste a `@theme inline` block, a `@custom-variant dark` rule, or a copy of AppShell's palette into the app's entry CSS.** A workaround for 1.5.0–1.6.1, where `styles` shipped without the bridge, did exactly that; from 1.7.0 those unlayered copies beat AppShell's layered palette and silently break dark mode. On 1.6.x the workaround is still load-bearing — upgrade to ≥1.7.0 before removing it. See [Migrations → remove the theme bridge workaround](https://github.com/tailor-platform/app-shell/blob/main/docs/migrations.md#150--170-remove-the-theme-bridge-workaround) for the removal steps. Tailwind v4 stays CSS-first; minimal `vite` / PostCSS wiring is in `**project-setup.md**`. diff --git a/docs/concepts/styling-theming.md b/docs/concepts/styling-theming.md index fb25151e..c2cee2ea 100644 --- a/docs/concepts/styling-theming.md +++ b/docs/concepts/styling-theming.md @@ -14,7 +14,7 @@ To configure your application, import AppShell styles from your global CSS or to @import "@tailor-platform/app-shell/styles"; ``` -That is the whole setup. Since 1.7.0, `styles` ships the palette (light **and** dark), the Tailwind v4 `@theme inline` bridge, and the `dark` custom variant, so your entry CSS should declare none of them itself. If yours does, see [Upgrading from 1.5.x or 1.6.x](#upgrading-from-15x-or-16x-remove-the-theme-bridge-workaround) — those leftovers silently break dark mode. +That is the whole setup. Since 1.7.0, `styles` ships the palette (light **and** dark), the Tailwind v4 `@theme inline` bridge, and the `dark` custom variant, so your entry CSS should declare none of them itself. If yours does, see [Migrations → remove the theme bridge workaround](../migrations.md#150--170-remove-the-theme-bridge-workaround) — those leftovers silently break dark mode. If you want a branded palette, import exactly one theme file after `styles`: @@ -119,59 +119,12 @@ Redeclare any token after the AppShell imports. Use `:root` for light and `:root Two rules: - **Set each override in both modes.** Overriding only `:root` misbehaves either way: on the default palette the light value carries into dark mode, and on a branded palette the override stops applying in dark mode altogether. -- **Override individual tokens; never copy the palette wholesale.** Copied tokens freeze at the value you copied while everything else tracks AppShell, and the two halves drift apart on upgrade. That is the failure described in [Upgrading from 1.5.x or 1.6.x](#upgrading-from-15x-or-16x-remove-the-theme-bridge-workaround). +- **Override individual tokens; never copy the palette wholesale.** Copied tokens freeze at the value you copied while everything else tracks AppShell, and the two halves drift apart on upgrade. That is the failure described in [Migrations → remove the theme bridge workaround](../migrations.md#150--170-remove-the-theme-bridge-workaround). `:root.dark` rather than `.dark` because the two palette families behave differently. The default palette is imported inside a cascade layer (`layer(theme.defaults)`), so any unlayered declaration of yours beats it. The branded palettes (`cream`, `bloom`) are imported by you, unlayered, and define dark values on `:root.dark` — which outranks a bare `.dark`, so a `.dark` override would silently lose. `:root.dark` is correct against both. Overriding under a narrower scope — per-section or per-tenant — needs the same care: pair `.tenant-a` with `:root.dark .tenant-a` so the dark rule still outranks a branded palette's `:root.dark`. Note also that `:root.dark` matches only ``; if you apply `.dark` to a subtree to darken one region, scope your overrides to that subtree rather than to `:root.dark`. -## Upgrading from 1.5.x or 1.6.x: remove the theme bridge workaround - -**Applies to:** apps that pasted the `@theme inline` block, `@custom-variant dark`, and AppShell's palette into their entry CSS — the workaround for `styles` shipping without the Tailwind bridge. - -**`styles` regained the bridge in 1.7.0.** On 1.5.0–1.6.1 the workaround is load-bearing, so upgrade to 1.7.0 or later _before_ deleting any of it. Remove it earlier and every AppShell-token utility — `bg-card`, `bg-background`, `text-muted-foreground`, `border-border` — stops resolving, while `dark:` variants fall back to Tailwind's `prefers-color-scheme` default and stop tracking the `.dark` class. - -From 1.7.0 the workaround is not merely redundant. It actively breaks dark mode, and the build succeeds with no warning: - -- Your pasted `:root` and `.dark` blocks are unlayered, so they beat AppShell's layered default palette. Colours freeze at the values you copied, and any surface AppShell has added since has no dark value at all — so it renders light colours in dark mode: white text on white cards, unreadable disabled inputs. -- `@custom-variant dark (&:is(.dark *))` overrides AppShell's `&:where(.dark, .dark *)`. The `:is(.dark *)` form matches only _descendants_ of `.dark`, so `dark:` utilities stop applying to the `.dark` element itself. - -### Removing it - -Delete from your entry CSS: - -- the `@theme inline { … }` block, -- the `@custom-variant dark (…)` rule, -- every `:root` and `.dark` block copied from AppShell's palette — **all** of it, including the `*-foreground` pairs, `--status-*`, `--alert-*`, `--sidebar-*` and `--semantic-shadow-*`. The foregrounds are what leave text white on white, so a partial deletion reproduces the bug. -- any `@import "@tailor-platform/app-shell/theme.css"` (a no-op shim since 1.6.0, kept only so older apps keep building). - -To find it, search every CSS file the app loads — not just the entry point, since `app/` and `styles/` are as common as `src/`: - -```bash -grep -rnE "@theme inline|@custom-variant|app-shell/theme\.css|--(card|popover|muted|sidebar|destructive|accent)(-foreground)?:" --include="*.css" --exclude-dir=node_modules --exclude-dir=dist --exclude-dir=.next . -``` - -Excluding `node_modules` matters: AppShell's own palette files declare these tokens too, and they must not be touched. In your own CSS, hits are either the workaround, which goes, or deliberate overrides, which should take the `:root` / `:root.dark` form above. - -What remains is short — [`examples/vite-app/src/index.css`](https://github.com/tailor-platform/app-shell/blob/main/examples/vite-app/src/index.css) is a working reference for the shape (it also imports a branded palette, which is optional): - -```css -@import "tailwindcss"; -@import "@tailor-platform/app-shell/styles"; - -html, -body { - margin: 0; - padding: 0; -} -``` - -### Verifying - -Toggle dark mode and confirm a real surface changes: inspect a `Card` and watch its computed `background-color` go from `rgb(255, 255, 255)` to `rgb(23, 23, 23)` on the default palette. - -Reading the token directly also works — `getComputedStyle(document.documentElement).getPropertyValue("--card")` returns the winning declaration, so a stale copy shows up as its own value. Just compare against the authored notation: AppShell writes `rgba(23, 23, 23, 1)`, not `#171717`, and the computed value preserves that form. - ## Z-Index Layering AppShell defines CSS custom properties for z-index values so you can adjust the stacking order to integrate with other libraries or overlays in your application. diff --git a/docs/migrations.md b/docs/migrations.md new file mode 100644 index 00000000..6203948a --- /dev/null +++ b/docs/migrations.md @@ -0,0 +1,63 @@ +--- +title: Migrations +description: Breaking changes and required migration steps for AppShell upgrades, newest first +--- + +# Migrations + +Every change that requires you to edit your application before or after upgrading, newest first. + +This page is deliberately narrow. It is **not** a changelog — see [`packages/core/CHANGELOG.md`](../packages/core/CHANGELOG.md) for the full release history including features and fixes. A change belongs here only if an app that does nothing will break, misbehave, or silently drift. + +Each entry states which versions are affected, what breaks, how to detect it, and what to change. Entries stay here permanently; they are not pruned when they get old, because apps upgrade across arbitrary version gaps. + +| Versions | Change | +| ------------- | ---------------------------------------------------------------------------------- | +| 1.5.0 → 1.7.0 | [Remove the theme bridge workaround](#150--170-remove-the-theme-bridge-workaround) | + +## 1.5.0 → 1.7.0: remove the theme bridge workaround + +**Applies to:** apps that pasted the `@theme inline` block, `@custom-variant dark`, and AppShell's palette into their entry CSS — the workaround for `styles` shipping without the Tailwind bridge. + +**`styles` regained the bridge in 1.7.0.** On 1.5.0–1.6.1 the workaround is load-bearing, so upgrade to 1.7.0 or later _before_ deleting any of it. Remove it earlier and every AppShell-token utility — `bg-card`, `bg-background`, `text-muted-foreground`, `border-border` — stops resolving, while `dark:` variants fall back to Tailwind's `prefers-color-scheme` default and stop tracking the `.dark` class. + +From 1.7.0 the workaround is not merely redundant. It actively breaks dark mode, and the build succeeds with no warning: + +- Your pasted `:root` and `.dark` blocks are unlayered, so they beat AppShell's layered default palette. Colours freeze at the values you copied, and any surface AppShell has added since has no dark value at all — so it renders light colours in dark mode: white text on white cards, unreadable disabled inputs. +- `@custom-variant dark (&:is(.dark *))` overrides AppShell's `&:where(.dark, .dark *)`. The `:is(.dark *)` form matches only _descendants_ of `.dark`, so `dark:` utilities stop applying to the `.dark` element itself. + +### Removing it + +Delete from your entry CSS: + +- the `@theme inline { … }` block, +- the `@custom-variant dark (…)` rule, +- every `:root` and `.dark` block copied from AppShell's palette — **all** of it, including the `*-foreground` pairs, `--status-*`, `--alert-*`, `--sidebar-*` and `--semantic-shadow-*`. The foregrounds are what leave text white on white, so a partial deletion reproduces the bug. +- any `@import "@tailor-platform/app-shell/theme.css"` (a no-op shim since 1.6.0, kept only so older apps keep building). + +To find it, search every CSS file the app loads — not just the entry point, since `app/` and `styles/` are as common as `src/`: + +```bash +grep -rnE "@theme inline|@custom-variant|app-shell/theme\.css|--(card|popover|muted|sidebar|destructive|accent)(-foreground)?:" --include="*.css" --exclude-dir=node_modules --exclude-dir=dist --exclude-dir=.next . +``` + +Excluding `node_modules` matters: AppShell's own palette files declare these tokens too, and they must not be touched. In your own CSS, hits are either the workaround, which goes, or deliberate overrides, which should take the `:root` / `:root.dark` form described in [Overriding tokens](./concepts/styling-theming.md#overriding-tokens). + +What remains is short — [`examples/vite-app/src/index.css`](https://github.com/tailor-platform/app-shell/blob/main/examples/vite-app/src/index.css) is a working reference for the shape (it also imports a branded palette, which is optional): + +```css +@import "tailwindcss"; +@import "@tailor-platform/app-shell/styles"; + +html, +body { + margin: 0; + padding: 0; +} +``` + +### Verifying + +Toggle dark mode and confirm a real surface changes: inspect a `Card` and watch its computed `background-color` go from `rgb(255, 255, 255)` to `rgb(23, 23, 23)` on the default palette. + +Reading the token directly also works — `getComputedStyle(document.documentElement).getPropertyValue("--card")` returns the winning declaration, so a stale copy shows up as its own value. Just compare against the authored notation: AppShell writes `rgba(23, 23, 23, 1)`, not `#171717`, and the computed value preserves that form. From ef8fd70700f1109b3d31df889ca10b6abf9b6b8c Mon Sep 17 00:00:00 2001 From: interacsean Date: Thu, 13 Aug 2026 14:23:01 +1000 Subject: [PATCH 6/9] docs: ship docs/migrations.md with the package (#1646) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The published tarball is dist/** and skills/** plus README.md and package.json — no docs/, and no CHANGELOG.md. A consumer app therefore had no migration record available locally at all, which is the visibility gap this ticket is about: the breakage in #1646 is silent, so someone hitting it has nothing local to consult. - generate-skill.mjs copies docs/migrations.md to the skill as references/migrations.md. The source is authored for GitHub, so its relative links (which resolve against docs/) are rewritten to absolute repo URLs, and the frontmatter is dropped to match the other copied references. Single authored source; the drift test now covers it. - SKILL.md gains a Migrations section, and the skill description names upgrades and post-version-bump styling breakage as triggers so agents find it at the point they need it. - packages/core/README.md links the guide — the README is the only human-readable file npm publishes. Verified with npm pack --dry-run: both README.md and skills/app-shell-patterns/references/migrations.md are in the tarball. Co-Authored-By: Claude Opus 5 --- .changeset/gentle-moons-repeat.md | 2 ++ catalogue/expected-skills-files.txt | 1 + catalogue/scripts/SKILL.template.md | 8 +++++- catalogue/scripts/generate-skill.mjs | 37 ++++++++++++++++++++++++++++ packages/core/README.md | 1 + 5 files changed, 48 insertions(+), 1 deletion(-) diff --git a/.changeset/gentle-moons-repeat.md b/.changeset/gentle-moons-repeat.md index 4ab2832a..8b369608 100644 --- a/.changeset/gentle-moons-repeat.md +++ b/.changeset/gentle-moons-repeat.md @@ -8,4 +8,6 @@ Apps that pasted the `@theme inline` block, `@custom-variant dark (&:is(.dark *) Adds `docs/migrations.md`, a curated list of breaking changes and the steps each one requires, newest first — separate from the changelog so upgraders and AI agents have one narrow place to look. The first entry covers detecting and removing this workaround, including the requirement to be on 1.7.0 or later first, since the workaround is load-bearing before that. +That page now ships with the package. The published tarball contains only `dist/**` and `skills/**` — no `docs/`, no `CHANGELOG.md` — so a consumer app previously had no migration record available locally at all. `docs/migrations.md` is generated into the bundled `app-shell-patterns` skill as `references/migrations.md` (with relative links rewritten to repo URLs), and the skill now names upgrades and post-version-bump styling breakage among its triggers, so coding agents reach it from `node_modules`. `packages/core/README.md` links it for the human path, since the README is the only human-readable file npm publishes. + `docs/concepts/styling-theming.md` and the bundled `app-shell-patterns` skill now document `:root` / `:root.dark` as the override form to use. A bare `.dark` override silently loses against the branded palettes (`cream`, `bloom`), which are imported unlayered and define their dark values on `:root.dark`. diff --git a/catalogue/expected-skills-files.txt b/catalogue/expected-skills-files.txt index 775aa3c3..2ece7188 100644 --- a/catalogue/expected-skills-files.txt +++ b/catalogue/expected-skills-files.txt @@ -2,6 +2,7 @@ skills/app-shell-patterns/SKILL.md skills/app-shell-patterns/references/fundamental/components.md skills/app-shell-patterns/references/fundamental/design-system.md skills/app-shell-patterns/references/fundamental/graphql.md +skills/app-shell-patterns/references/migrations.md skills/app-shell-patterns/references/patterns/detail-hero-with-actions.md skills/app-shell-patterns/references/patterns/form-modal.md skills/app-shell-patterns/references/patterns/form-sectioned.md diff --git a/catalogue/scripts/SKILL.template.md b/catalogue/scripts/SKILL.template.md index a11d0dd7..aa5e0b9e 100644 --- a/catalogue/scripts/SKILL.template.md +++ b/catalogue/scripts/SKILL.template.md @@ -1,6 +1,6 @@ --- name: app-shell-patterns -description: "Best-practice UI patterns and correct component usage for building pages in apps that use @tailor-platform/app-shell. Use when: building or editing any screen, page, list, table, detail view, form, modal, dialog, wizard, or bulk/confirm/toast interaction in an app with @tailor-platform/app-shell installed — or when choosing the right AppShell component, layout, or design token for a UI." +description: "Best-practice UI patterns and correct component usage for building pages in apps that use @tailor-platform/app-shell. Use when: building or editing any screen, page, list, table, detail view, form, modal, dialog, wizard, or bulk/confirm/toast interaction in an app with @tailor-platform/app-shell installed — when choosing the right AppShell component, layout, or design token for a UI — or when upgrading @tailor-platform/app-shell, or diagnosing styling, theming, or dark-mode breakage that appeared after a version bump." --- # App-Shell Patterns @@ -15,6 +15,12 @@ These are the foundational rules that underpin all patterns. All patterns build {{FUNDAMENTAL_TABLE}} +## Migrations + +[`migrations.md`](references/migrations.md) lists every change that requires editing the consuming app, newest first — what breaks, how to detect it, and what to change. + +Read it when upgrading `@tailor-platform/app-shell`, and whenever styling, theming, or dark mode looks wrong after a version bump. These breakages are silent: the build succeeds and nothing warns, so the cause is not discoverable from the error output. The package ships no CHANGELOG, so this file is the only migration record available locally. + ## Available Patterns {{PATTERNS_TABLE}} diff --git a/catalogue/scripts/generate-skill.mjs b/catalogue/scripts/generate-skill.mjs index 876860e9..03a120ab 100644 --- a/catalogue/scripts/generate-skill.mjs +++ b/catalogue/scripts/generate-skill.mjs @@ -19,6 +19,17 @@ const repoRoot = join(catalogueRoot, ".."); const skillsDir = join(repoRoot, "packages", "core", "skills", "app-shell-patterns"); const referencesDir = join(skillsDir, "references"); +/** + * docs/migrations.md is authored for GitHub but has to reach consumers too: + * the published package contains only dist/** and skills/**, so a consumer + * app has no docs/ tree and no CHANGELOG.md to read. Copying it into the + * skill is the only channel that puts migration steps in node_modules, where + * coding agents working in a consumer app can actually find them. + */ +const migrationsSource = join(repoRoot, "docs", "migrations.md"); +const migrationsOutput = join(referencesDir, "migrations.md"); +const repoBlobUrl = "https://github.com/tailor-platform/app-shell/blob/main"; + /** * Category definitions. To add a new category, append an entry here * and add a corresponding {{}} placeholder to SKILL.template.md. @@ -171,6 +182,30 @@ async function processEntryCategory(category, categoryDir, outputDir) { return { category, entries }; } +/** + * Copy docs/migrations.md to references/migrations.md. + * + * Two transforms are needed because the source is written for GitHub: + * relative links resolve against docs/, which does not exist in the package, + * so they become absolute repo URLs; and the frontmatter is dropped to match + * the other copied reference files. + */ +async function processMigrations() { + const raw = await readFile(migrationsSource, "utf-8"); + const { content } = matter(raw); + + const body = content + .trim() + .replace( + /\]\((\.{1,2}\/[^)\s]+)\)/g, + (_match, target) => `](${repoBlobUrl}/${join("docs", target)})`, + ); + + await mkdir(referencesDir, { recursive: true }); + await writeFile(migrationsOutput, `${body}\n`); + console.log(" Generated references/migrations.md"); +} + async function main() { // Process all categories const results = []; @@ -179,6 +214,8 @@ async function main() { results.push(result); } + await processMigrations(); + // Generate SKILL.md index const skillMd = await generateSkillIndex(results); await writeFile(join(skillsDir, "SKILL.md"), skillMd); diff --git a/packages/core/README.md b/packages/core/README.md index 64efa69c..c94a3d20 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -24,3 +24,4 @@ We've made sensible default choices so you can focus on what matters most — bu - [Introduction](https://github.com/tailor-platform/app-shell/blob/main/docs/introduction.md) — What is AppShell and why use it - [Quick Start](https://github.com/tailor-platform/app-shell/blob/main/docs/quickstart.md) — Installation, setup, and first steps +- [Migrations](https://github.com/tailor-platform/app-shell/blob/main/docs/migrations.md) — Breaking changes and the steps each one requires, newest first. Check this when upgrading, or when something looks wrong after a version bump. From 0d07788713c6df3860cb16bfe2be6259e7b09ed5 Mon Sep 17 00:00:00 2001 From: interacsean Date: Thu, 13 Aug 2026 14:42:05 +1000 Subject: [PATCH 7/9] docs: keep concept docs to the current version, migrations to migrations.md Per review: docs describing how AppShell works should describe the version you are on, with version history confined to migrations.md. - styling-theming.md drops the "Since 1.7.0" qualifier and both links to the migration entry; the rules it states (entry CSS declares none of the palette/bridge/dark-variant, override individual tokens rather than copying) are true of the current version on their own terms. - The same version history had accumulated in the shipped skill's design-system.md, so it gets the same treatment. SKILL.md's Migrations section is the entry point there, as README and the docs index are for docs/, so no inline pointers are needed. Verified 248 relative links and their anchors across docs/ still resolve. Co-Authored-By: Claude Opus 5 --- catalogue/src/fundamental/design-system.md | 4 ++-- docs/concepts/styling-theming.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/catalogue/src/fundamental/design-system.md b/catalogue/src/fundamental/design-system.md index 9b07ff99..4cf0eb75 100644 --- a/catalogue/src/fundamental/design-system.md +++ b/catalogue/src/fundamental/design-system.md @@ -49,9 +49,9 @@ Override raw variables after the `styles` import, using `:root` for light and `: Use `:root.dark`, not a bare `.dark`. The default palette is imported inside a cascade layer (`layer(theme.defaults)`), so any unlayered declaration of yours beats it — but the branded palettes (`cream`, `bloom`) are imported unlayered and define their dark values on `:root.dark`, which outranks `.dark`. A `.dark` override would silently lose against those. -Override at the highest scope where the change applies. A narrower scope (per-section, per-tenant) needs the same both-modes treatment, and its dark rule must still outrank a branded palette's `:root.dark` — pair `.tenant-a` with `:root.dark .tenant-a`. Do not duplicate token values across files — change them at the source. Never copy the palette wholesale; tokens you did not copy stay on AppShell's values and the two halves drift apart on every upgrade. +Override at the highest scope where the change applies. A narrower scope (per-section, per-tenant) needs the same both-modes treatment, and its dark rule must still outrank a branded palette's `:root.dark` — pair `.tenant-a` with `:root.dark .tenant-a`. Do not duplicate token values across files — change them at the source. Never copy the palette wholesale; tokens you did not copy stay on AppShell's values, so the two halves drift apart and any surface AppShell adds later has no value in the copy at all. -**Do not paste a `@theme inline` block, a `@custom-variant dark` rule, or a copy of AppShell's palette into the app's entry CSS.** A workaround for 1.5.0–1.6.1, where `styles` shipped without the bridge, did exactly that; from 1.7.0 those unlayered copies beat AppShell's layered palette and silently break dark mode. On 1.6.x the workaround is still load-bearing — upgrade to ≥1.7.0 before removing it. See [Migrations → remove the theme bridge workaround](https://github.com/tailor-platform/app-shell/blob/main/docs/migrations.md#150--170-remove-the-theme-bridge-workaround) for the removal steps. +**Do not paste a `@theme inline` block, a `@custom-variant dark` rule, or a copy of AppShell's palette into the app's entry CSS.** `styles` provides all three. A copy of any of them in the app's own CSS is unlayered, so it beats AppShell's layered palette and silently breaks dark mode — the build succeeds and nothing warns. **Dark mode is a `.dark` class on ``**, not a data attribute. AppShell's theme provider toggles `document.documentElement.classList` between `light` and `dark` and persists the choice under the `appshell-ui-theme` localStorage key; the bundled `AppearanceSwitcher` component drives it. The bridge registers `@custom-variant dark (&:where(.dark, .dark *))`, so the `dark:` variant works in your own markup. diff --git a/docs/concepts/styling-theming.md b/docs/concepts/styling-theming.md index c2cee2ea..121126cd 100644 --- a/docs/concepts/styling-theming.md +++ b/docs/concepts/styling-theming.md @@ -14,7 +14,7 @@ To configure your application, import AppShell styles from your global CSS or to @import "@tailor-platform/app-shell/styles"; ``` -That is the whole setup. Since 1.7.0, `styles` ships the palette (light **and** dark), the Tailwind v4 `@theme inline` bridge, and the `dark` custom variant, so your entry CSS should declare none of them itself. If yours does, see [Migrations → remove the theme bridge workaround](../migrations.md#150--170-remove-the-theme-bridge-workaround) — those leftovers silently break dark mode. +That is the whole setup. `styles` ships the palette (light **and** dark), the Tailwind v4 `@theme inline` bridge, and the `dark` custom variant, so your entry CSS should declare none of them itself. A copy of any of them in your own CSS overrides AppShell's and silently breaks dark mode. If you want a branded palette, import exactly one theme file after `styles`: @@ -119,7 +119,7 @@ Redeclare any token after the AppShell imports. Use `:root` for light and `:root Two rules: - **Set each override in both modes.** Overriding only `:root` misbehaves either way: on the default palette the light value carries into dark mode, and on a branded palette the override stops applying in dark mode altogether. -- **Override individual tokens; never copy the palette wholesale.** Copied tokens freeze at the value you copied while everything else tracks AppShell, and the two halves drift apart on upgrade. That is the failure described in [Migrations → remove the theme bridge workaround](../migrations.md#150--170-remove-the-theme-bridge-workaround). +- **Override individual tokens; never copy the palette wholesale.** Copied tokens freeze at the value you copied while everything else tracks AppShell, so the two halves drift apart — and any surface AppShell adds later has no value in your copy at all. `:root.dark` rather than `.dark` because the two palette families behave differently. The default palette is imported inside a cascade layer (`layer(theme.defaults)`), so any unlayered declaration of yours beats it. The branded palettes (`cream`, `bloom`) are imported by you, unlayered, and define dark values on `:root.dark` — which outranks a bare `.dark`, so a `.dark` override would silently lose. `:root.dark` is correct against both. From ce288d51f456874f9705a93748fcc3b954c0886b Mon Sep 17 00:00:00 2001 From: interacsean Date: Fri, 14 Aug 2026 12:37:46 +1000 Subject: [PATCH 8/9] docs: backfill prior breaking changes into migrations.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Audited all 73 releases in packages/core/CHANGELOG.md so the page lands in an accurate state rather than describing only the change that prompted it. 1.x entries, each with what breaks and what to do: - 1.11.0 react/react-dom floor raised to 19.2.7 and React Router v8 (1.10.1 had deliberately stayed on v7, so this hop crosses a major) - 1.11.0 non-modal Sheet drops its backdrop - 1.8.0 stream removed from useAIChat() - 1.5.0 loader removed from file-based page definitions - 1.3.0 inferColumns() no longer sets a default render, and badge variants default to outline-neutral - 1.0.2 Toaster no longer accepts richColors Pre-1.0 changes are condensed into one table (0.4.0 through 0.33.0 — mostly auth and routing), pointing at the CHANGELOG for the code. They supersede each other, so the ordering is called out: 0.13.0 replaced defaultResourceRedirectPath with redirectToResource, which 0.24.0 then removed in favour of guards + redirectTo(). Excluded pure additions, internal dependency bumps, and visual polish that needs no consumer edit. Co-Authored-By: Claude Opus 5 --- docs/migrations.md | 77 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 74 insertions(+), 3 deletions(-) diff --git a/docs/migrations.md b/docs/migrations.md index 6203948a..dc7166e6 100644 --- a/docs/migrations.md +++ b/docs/migrations.md @@ -11,9 +11,40 @@ This page is deliberately narrow. It is **not** a changelog — see [`packages/c Each entry states which versions are affected, what breaks, how to detect it, and what to change. Entries stay here permanently; they are not pruned when they get old, because apps upgrade across arbitrary version gaps. -| Versions | Change | -| ------------- | ---------------------------------------------------------------------------------- | -| 1.5.0 → 1.7.0 | [Remove the theme bridge workaround](#150--170-remove-the-theme-bridge-workaround) | +| Version | Change | +| ------------- | -------------------------------------------------------------------------------------------------- | +| 1.11.0 | [React 19.2.7 and React Router v8 required](#1110-react-1927-and-react-router-v8-are-now-required) | +| 1.11.0 | [Non-modal `Sheet` renders no backdrop](#1110-non-modal-sheet-no-longer-renders-a-backdrop) | +| 1.8.0 | [`stream` removed from `useAIChat()`](#180-stream-removed-from-useaichat) | +| 1.5.0 → 1.7.0 | [Remove the theme bridge workaround](#150--170-remove-the-theme-bridge-workaround) | +| 1.5.0 | [`loader` removed from file-based pages](#150-loader-removed-from-file-based-page-definitions) | +| 1.3.0 | [Column inference and badge defaults changed](#130-column-inference-and-badge-defaults-changed) | +| 1.0.2 | [`Toaster` no longer accepts `richColors`](#102-toaster-no-longer-accepts-richcolors) | +| before 1.0 | [Pre-1.0 breaking changes](#before-10) | + +## 1.11.0: React 19.2.7 and React Router v8 are now required + +**Applies to:** every app upgrading to 1.11.0. + +The minimum supported `react` and `react-dom` is raised to `19.2.7`, and AppShell moves to React Router v8. React 18 is no longer supported. + +Upgrade `react` and `react-dom` to `>=19.2.7` in the same change. If your app imports from `react-router` directly, review the React Router v8 release notes for its own breaking changes — AppShell re-exports a subset (`useNavigate`, `useParams`, `useLocation`, and friends), and those are unaffected. + +Note that 1.10.1 deliberately stayed on React Router v7 to pick up its security fixes while avoiding v8. Going 1.10.1 → 1.11.0 therefore crosses a router major. + +## 1.11.0: non-modal `Sheet` no longer renders a backdrop + +**Applies to:** apps using ``. + +A non-modal sheet previously still rendered a backdrop, dimming and blocking the page behind it. It now omits the backdrop, which is what `modal={false}` implies. Nothing errors — the page behind simply stays undimmed and interactive. + +If you relied on the dimming, drop `modal={false}` and use a modal sheet. + +## 1.8.0: `stream` removed from `useAIChat()` + +**Applies to:** apps passing `stream` to `useAIChat()`, or constructing an `AIGatewayChatRequest` by hand. + +AppShell now selects streaming or JSON transport automatically from the model, so the option is gone. TypeScript errors on the removed property; delete it. There is no replacement. ## 1.5.0 → 1.7.0: remove the theme bridge workaround @@ -61,3 +92,43 @@ body { Toggle dark mode and confirm a real surface changes: inspect a `Card` and watch its computed `background-color` go from `rgb(255, 255, 255)` to `rgb(23, 23, 23)` on the default palette. Reading the token directly also works — `getComputedStyle(document.documentElement).getPropertyValue("--card")` returns the winning declaration, so a stale copy shows up as its own value. Just compare against the authored notation: AppShell writes `rgba(23, 23, 23, 1)`, not `#171717`, and the computed value preserves that form. + +## 1.5.0: `loader` removed from file-based page definitions + +**Applies to:** file-based routing apps that set `loader` in `Page.appShellPageProps`. + +`loader` was an incomplete API exposed by accident, and `guards` is now the single source of page-level route behaviour. Move any access checks into a guard, and any data loading into the page component or your data layer. + +## 1.3.0: column inference and badge defaults changed + +**Applies to:** apps using `inferColumns()`, or `DataTable`'s status badges. + +Two changes that alter rendering without any error: + +- `inferColumns()` no longer sets a default `render`. A column with neither an explicit `type` nor `render` now displays `—` for null and empty values, matching typed-column behaviour. +- Badge variant resolution moved into a shared helper whose default is `outline-neutral`. `DataTable` previously defaulted to `neutral`, so unstyled status badges change appearance. + +Set `type` or `render` explicitly on any column whose previous rendering you want back, and pass an explicit variant where the old badge styling mattered. `BadgeVariantType` is deprecated in favour of `BadgeVariant`. + +## 1.0.2: `Toaster` no longer accepts `richColors` + +**Applies to:** apps passing `richColors` to ``. + +The prop is removed, and toasts no longer colour-code the success, error, warning, and info variants. TypeScript errors on the prop; delete it. + +## Before 1.0 + +Pre-1.0 releases changed the public API often, mostly around authentication and routing. If you are upgrading from a 0.x version, work through these in order — several supersede each other, so applying them out of sequence will not land you in the right place. + +Each is summarised here; [`packages/core/CHANGELOG.md`](../packages/core/CHANGELOG.md) carries the full before/after code for every one. + +| Version | Change | +| ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 0.33.0 | `AsyncFetcherFn` receives `string \| null` instead of `string`. It is called with `null` when the user has typed nothing — return initial items, or an empty array to show nothing until they type. | +| 0.28.0 | `EnhancedAuthClient.getAuthHeadersForQuery()` removed. Pass `fetch: authClient.fetch` to your GraphQL client instead; it handles DPoP proofs and token refresh transparently. | +| 0.27.0 | Module-level `guards` and `loaders` no longer cascade to child resources — declare them on each resource or page. A module without a `component` no longer auto-redirects to its first visible resource and must declare `guards`, or it throws at runtime. | +| 0.26.0 | Authentication moved to `@tailor-platform/auth-public-client` with DPoP. `AuthProvider` requires a `client` from `createAuthClient`, `apiEndpoint` is gone, `useAuth` returns its fields directly rather than under `authState`, and built-in user fetching (`meQuery`, `AuthState.user`, `DefaultUser`, `AuthRegister`) is removed — fetch the user with your own GraphQL client. | +| 0.24.0 | `accessControl` replaced by the `guards` array on `defineModule`/`defineResource`. `RedirectConfig` and `redirectToResource` removed — use `guards` with `redirectTo()`. | +| 0.19.0 | `BuiltinIdPAuthProvider` → `AuthProvider`, `useBuiltinIdpAuth` → `useAuth`. The `buildAuthorizationUrl`, `exchangeCodeForToken`, `prepareLogin`, and `handleOAuthCallback` utilities are no longer exported. | +| 0.13.0 | `defaultResourceRedirectPath` removed from `defineModule` in favour of a `redirectToResource` helper — which 0.24.0 then removed in turn. Coming from 0.13.0 or earlier, go straight to the 0.24.0 form: `guards` with `redirectTo()`. | +| 0.4.0 | `meta.title` no longer renders the page title automatically. The title is passed to the resource component via props (`ResourceComponentProps`); render it yourself. | From e25c6824d08c6d6a52168a577f6b47612d46bff0 Mon Sep 17 00:00:00 2001 From: interacsean Date: Fri, 14 Aug 2026 14:09:25 +1000 Subject: [PATCH 9/9] docs: correct stale API guidance in CLAUDE.md and the changeset skill MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Surfaced by the migrations.md audit: repo-level agent guidance was pointing at APIs that no longer exist, which is the same failure mode as #1647 — docs describing a package that isn't the one being shipped. - CLAUDE.md said redirects use `redirectToResource()` "instead of deprecated `defaultResourceRedirectPath`". Both were removed (0.13.0 and 0.24.0); neither appears anywhere in packages/core/src. The current form is a guard returning `redirectTo("/path")`. - CLAUDE.md said routing uses react-router v7. It has been v8 since 1.11.0 (packages/core/package.json pins ^8.3.0). - The create-changeset skill's "major changeset" example demonstrated the same dead pair. Swapped for accessControl → guards, a real 0.24.0 breaking change whose after-state is still the current API; verified `pass`/`hidden`/`redirectTo` are exported and match the signatures used. Co-Authored-By: Claude Opus 5 --- .agents/skills/create-changeset/SKILL.md | 17 +++++++++++++---- CLAUDE.md | 4 ++-- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/.agents/skills/create-changeset/SKILL.md b/.agents/skills/create-changeset/SKILL.md index b1499598..2f622842 100644 --- a/.agents/skills/create-changeset/SKILL.md +++ b/.agents/skills/create-changeset/SKILL.md @@ -108,18 +108,27 @@ Fix sidebar not collapsing properly on mobile viewports when navigating between "@tailor-platform/app-shell": major --- -Replace `defaultResourceRedirectPath` with `redirectToResource()` helper for module-level redirects. +Replace `accessControl` with a `guards` array on `defineModule` and `defineResource`. Guards run in order, and the first non-`pass` result stops the chain. Before: ```tsx -defineModule({ defaultResourceRedirectPath: "/dashboard" }); +defineResource({ + path: "admin", + component: AdminPage, + accessControl: async () => ({ state: (await isAdmin()) ? "visible" : "hidden" }), +}); ``` After: ```tsx -import { redirectToResource } from "@tailor-platform/app-shell"; -defineModule({ redirect: redirectToResource("dashboard") }); +import { defineResource, hidden, pass } from "@tailor-platform/app-shell"; + +defineResource({ + path: "admin", + component: AdminPage, + guards: [async () => ((await isAdmin()) ? pass() : hidden())], +}); ``` ```` diff --git a/CLAUDE.md b/CLAUDE.md index 631fb2cf..7763e073 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -28,8 +28,8 @@ Tailor Platform AppShell - A React-based framework for building ERP applications ### Essential Concepts for Code Navigation - **Module System**: `defineModule()` creates top-level nav items, `defineResource()` creates pages/sub-pages -- **Routing**: Uses react-router v7 (not Next.js file-based routing) -- **Redirects**: Use `redirectToResource()` helper instead of deprecated `defaultResourceRedirectPath` +- **Routing**: Uses react-router v8 (not Next.js file-based routing) +- **Redirects**: Use a guard returning `redirectTo("/path")`. The older `defaultResourceRedirectPath` prop and `redirectToResource()` helper were both removed (in 0.13.0 and 0.24.0 respectively) and no longer exist - **Core Components**: `AppShell` (root provider), `SidebarLayout` (default layout) - **Context**: Access via `useAppShell()` hook