Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
17 changes: 13 additions & 4 deletions .agents/skills/create-changeset/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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())],
});
```
````
13 changes: 13 additions & 0 deletions .changeset/gentle-moons-repeat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"@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.

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`.
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
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
1 change: 1 addition & 0 deletions catalogue/expected-skills-files.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 7 additions & 1 deletion catalogue/scripts/SKILL.template.md
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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}}
Expand Down
37 changes: 37 additions & 0 deletions catalogue/scripts/generate-skill.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {{<templateKey>}} placeholder to SKILL.template.md.
Expand Down Expand Up @@ -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 = [];
Expand All @@ -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);
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, 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.** `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 `<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. `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`:

```css
Expand Down Expand Up @@ -132,13 +134,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, 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.

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