Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/gentle-moons-repeat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@tailor-platform/app-shell": patch
---

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 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.

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` 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`.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
14 changes: 8 additions & 6 deletions catalogue/src/fundamental/design-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,21 +35,23 @@ Tokens exist in two layers, and knowing which one you are touching matters:
1. **Raw CSS variables** (`--background`, `--primary`, `--radius`) — defined on `:root` in `themes/*.css`. These are what you **override**.
2. **The Tailwind bridge** (`@theme inline` in `theme.bridge.css`) — maps each raw variable into Tailwind's namespace (`--background` → `--color-background`), which is what makes `bg-background` a real utility. You do **not** edit this layer.

Override raw variables in `:root` (global) or a scoped selector, after the `styles` import:
Override raw variables after the `styles` import, using `:root` for light and `:root.dark` for dark. Set every override 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 {
--primary: #3b82f6;
--background: #ffffff;
}

.dark {
--background: #0a0a0a;
--foreground: #fafafa;
:root.dark {
--primary: #60a5fa;
}
```

Override at the highest scope where the change applies. Do not duplicate token values across files — change them at the source.
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.

**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.

**Dark mode is a `.dark` class on `<html>`**, 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.

Expand Down
30 changes: 29 additions & 1 deletion docs/concepts/styling-theming.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. 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`:

```css
Expand Down Expand Up @@ -90,13 +92,39 @@ 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

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";

:root {
--primary: #2563eb;
}

:root.dark {
--primary: #60a5fa;
}
```

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).

`: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 `<html class="dark">`; if you apply `.dark` to a subtree to darken one region, scope your overrides to that subtree rather than to `:root.dark`.

## 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.
Expand Down
63 changes: 63 additions & 0 deletions docs/migrations.md
Original file line number Diff line number Diff line change
@@ -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.
Loading