Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
9 changes: 9 additions & 0 deletions .changeset/gentle-moons-repeat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@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.

`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"]`.
32 changes: 16 additions & 16 deletions catalogue/src/fundamental/design-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -15,38 +15,38 @@ 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 (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`; 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. `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.

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 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. 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 {
--color-primary: #3b82f6;
--color-background: #ffffff;
--primary: #3b82f6;
}

[data-theme="dark"] {
--color-background: #0a0a0a;
--color-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.
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 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()` / `<AppearanceSwitcher />`). 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

Expand Down Expand Up @@ -74,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

Expand Down
77 changes: 76 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 [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`:

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

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

Comment thread
interacsean marked this conversation as resolved.
Outdated
## 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
Loading