Skip to content
Merged
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
19 changes: 16 additions & 3 deletions .agents/skills/quality-check/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: "Runs type-check, lint, test, check-dts, and formatting for the App

# Quality Check

Run type-check, lint, test, `check-dts`, and formatting in sequence. Fix errors where possible and re-run until clean. Formatting runs last so that any code changes from fixing errors are also formatted.
Run type-check, lint, test, `check-dts`, browser style tests, and formatting in sequence. Fix errors where possible and re-run until clean. Formatting runs last so that any code changes from fixing errors are also formatted.

These are the same tasks CI runs in `.github/actions/ci`. Keep this list in step with that file — a task that runs there but not here passes locally and fails on the PR.

Expand Down Expand Up @@ -53,7 +53,19 @@ Step 1 cannot catch what this catches: `type-check` sets `skipLibCheck: true` an
the emitted declarations, while `check-dts` packs a tarball and type-checks it with
`skipLibCheck: false`. A declaration that only breaks for consumers fails here and nowhere else.

### Step 5: Format
### Step 5: Browser style contracts

Run the shipped CSS contracts in Chromium (the script builds the package first):

```sh
pnpm --filter @tailor-platform/app-shell test:browser
```

On first use, install Chromium with `pnpm --filter @tailor-platform/app-shell exec playwright install chromium`.
Linux CI uses `playwright install --with-deps chromium` to install system libraries too.
Keep these tests enabled in CI when replacing Node-based style checks; `pnpm test` alone does not run them.

### Step 6: Format

Run formatting:

Expand All @@ -76,4 +88,5 @@ Provide a summary of:
2. Lint errors found and fixed (if any)
3. Test failures found and fixed (if any)
4. Declaration (`check-dts`) errors found and fixed (if any)
5. Formatting changes (if any)
5. Browser style test failures found and fixed (if any)
6. Formatting changes (if any)
5 changes: 5 additions & 0 deletions .changeset/semantic-contrast-thresholds.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@tailor-platform/app-shell": patch
---

Document the contrast thresholds for the semantic colour roles: 4.5:1 for text and 3:1 for non-text. The styling guide now states that the hue roles have the same values in every shipped palette, and lists the known exceptions: hover text, status content directly on the `bloom` light shell gradient, and the `warning` solid and indicator against a light card. The contrast test now checks the `cream` and `bloom` palettes as well as `default`. No colour values change.
6 changes: 6 additions & 0 deletions .github/actions/ci/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ runs:
# sets skipLibCheck: true and never looks at the emitted declarations.
- run: pnpm exec turbo run lint test type-check check-dts
shell: bash
- name: Install Chromium for style contract tests
run: pnpm --filter @tailor-platform/app-shell exec playwright install --with-deps chromium
shell: bash
- name: Check styles in Chromium
run: pnpm --filter @tailor-platform/app-shell exec vitest run --config vitest.browser.config.ts
shell: bash
# Deterministic docs + generated-skill drift gate (no LLM). Runs uncached
# so a docs/ change always re-validates the manifest and consumer skill.
- run: pnpm docs:check
Expand Down
29 changes: 29 additions & 0 deletions decisions/semantic-color-roles.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,32 @@ The default palette's surfaces and brand tokens read scale steps. For the defaul
- New role `--backdrop`, `var(--primitive-black-a7)` (50%) in both modes, bridged as `bg-backdrop`. `Dialog`, `Sheet` and the CsvImporter drawer use it instead of `bg-black/50`; the value is the same. The name follows the CSS `::backdrop` pseudo-element and the Base UI `Backdrop` part. It is not `overlay`, because `--z-overlay` already names the modal layer, which holds both the backdrop and the popup.
- `--accent`, the shadows and the chart colours stay literal. They have no matching step. `--destructive` reads `--danger-solid` (see Update 2026-10-06).
- `cream`, `bloom` and `_template.css` keep their own literal values. The header comments now list `--backdrop` among the inherited tokens.

## Update 2026-10-24: palettes, literal values and contrast thresholds

Three open points from the remaining-work issue ([platform-planning#2208](https://github.com/tailor-inc/platform-planning/issues/2208)) and the contrast issue ([platform-planning#1892](https://github.com/tailor-inc/platform-planning/issues/1892)) are decided. No colour value changes.

### Status colours are the same in every palette

`cream` and `bloom` do not set the hue roles, and they will not. The info, success, warning and danger roles have the same values in every shipped palette. A palette changes brand and surface colours; a status colour keeps one meaning across products. Material Design 3 treats error the same way: it calls error a static colour that does not change even in dynamic colour schemes. The neutral roles still follow the palette, because they read the system tokens. A palette that needs other status colours overrides the roles (`--{intent}-{role}`), as `_template.css` says.

### Literal values stay literal

`--accent`, `--chart-1` to `--chart-5` and the `--semantic-shadow-*` tokens keep literal values. `DocumentProgressCard` keeps its Tailwind palette colours.

- `--accent` (light `rgb(237, 237, 237)`, dark `rgb(48, 48, 48)`) sits between two gray steps. Moving it to a step changes the hover and selected colour, so it waits for the palette work with the Theme Generator.
- The chart colours and the `DocumentProgressCard` colours are categorical. They tell series apart and carry no status. The `DocumentProgressCard` `color` prop takes colour names (`indigo`, `pink`, `green` and so on), not intents. The semantic roles do not fit them, and the scales have no steps for most of their hues. A shared categorical palette, if one is needed, is a separate axis.
- A shadow is a composite value (offsets, blur and colour), not one colour, so it has no scale step to read.

### Contrast thresholds

AppShell's thresholds are 4.5:1 for text (WCAG 1.4.3) and 3:1 for non-text (WCAG 1.4.11). `tests/browser/semantic-contrast.test.ts` now runs the same checks for `default`, `cream` and `bloom`, in light and dark mode, on `--card` and `--background`. The pairs and the hover floor are unchanged, and all three palettes pass. The styling guide states the thresholds and the exceptions:

- `text` on `surface-hover` stays at the 4.2:1 floor. Choosing another step is a visible change for a later PR.
- In `cream` and `bloom`, the main content area is transparent over the shell gradient. At the top of the bloom light gradient (55% of `--shell-gradient-base`), `text` on `surface` is 4.49:1 for success and 4.48:1 for danger. cream light is 4.68:1 or more. The figure under "Not guaranteed" (4.16:1 over `#efe8ff`) used `--shell-gradient-base` itself, which the gradient covers.
- `warning` `solid` and `indicator` are 2.15:1 against a light card. They are used with a label.
- The neutral intent is not checked.

This supersedes "The `cream` and `bloom` palettes and their tinted shell backgrounds" under "Not guaranteed".

**How the checks run.** The contrast checks run in Chromium with Vitest Browser Mode (#652). The browser resolves the built stylesheet, including aliases, palette overrides, inheritance and alpha, so the tests check the colours that consumers get, and the tests no longer need their own CSS parser. Structural checks (bridge completeness, alias chains and the typography role declarations) stay in the Node suite, because they are fast and do not need a browser. CI installs Chromium and runs `vitest.browser.config.ts` after the unit tests. `pnpm test` does not run the browser tests; run `pnpm --filter @tailor-platform/app-shell test:browser` locally.
2 changes: 1 addition & 1 deletion decisions/typography-roles.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ Limit: this does not let a role replace a size that core sets on its own element
- **API.** `Text` takes the role as `variant`. The prop is not called `role`, because `role` is an ARIA attribute. The element is set with Base UI's `render` prop, as in `Button`, the sidebar primitives, and the breadcrumb. AppShell has no `as` or `asChild` prop.
- **Default elements.** The three heading roles render `h2`. The body roles, including the relaxed variants, render `p`. `label-md`, `label-sm`, and `code-sm` render `span`. All headings share one default so that no heading level is implied by size. `h2` is the default because a page has one `h1` (`Layout.Header`), and `Dialog.Title` from Base UI also renders `h2`. A default is hard to change later, because every call site without `render` would get a different element.
- **Lint.** The `jsx-a11y/heading-has-content` rule reports `render={<h3 />}` as an empty heading, and the function form `render={(props) => <h3 {...props} />}` too. Apps that enable the rule disable it on that line. A `level` prop would avoid this, but it would add a second way to choose the element next to `render`. The documentation shows the disable comment instead.
- **No color or modifier props.** Color and the four modifiers stay in `className`, as for the utilities. A consumer `leading-none`, `font-semibold`, or `tracking-*` overrides the role even though AppShell's prefixed sheet loads later, because the role reads the shared `--tw-leading`, `--tw-font-weight`, and `--tw-tracking` variables first. `tests/typography-compile.test.ts` checks this.
- **No color or modifier props.** Color and the four modifiers stay in `className`, as for the utilities. A consumer `leading-none`, `font-semibold`, or `tracking-*` overrides the role even though AppShell's prefixed sheet loads later, because the role reads the shared `--tw-leading`, `--tw-font-weight`, and `--tw-tracking` variables first. `tests/browser/theme.test.tsx` checks the computed styles in Chromium; `tests/typography-compile.test.ts` retains the full role-declaration checks.
- **No `textVariants` export.** tailor-inc/platform-planning#1580 asked for a variants export. It is descoped, not deferred. The `text-<role>` utilities already apply the same role mappings, so a cva export would be a third way to do the same thing, and it would put `astw:`-prefixed classes into consumer code. Unlike `Button` and `Badge`, `Text` does not export its cva function.
- **`code-sm` includes `font-mono`.** The utility cannot set the font family, but the component can.
- **cream and bloom.** The `h1` to `h6` rule is unchanged. The default `h2` takes the palette's line height and letter spacing under these palettes, the same as existing headings in AppShell components today.
Expand Down
6 changes: 3 additions & 3 deletions docs-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -1381,9 +1381,9 @@
"symbols": [],
"hashes": {
"typeSurface": null,
"outline": "052cc2c234995a16",
"outline": "9f6a42c42dd3f54f",
"snapshot": null,
"outputMd": "755e362908623d2c",
"outputMd": "7ee0130a20b88310",
"examples": "4d19f4f688e0f0ac"
}
},
Expand Down Expand Up @@ -1770,7 +1770,7 @@
"packages/core/skills/app-shell-patterns/references/concepts/modules-and-resources.md": "bf15b5460f22d165",
"packages/core/skills/app-shell-patterns/references/concepts/routing-navigation.md": "8034c7f885994b29",
"packages/core/skills/app-shell-patterns/references/concepts/sidebar-navigation.md": "5a3c8bac248061f9",
"packages/core/skills/app-shell-patterns/references/concepts/styling-theming.md": "755e362908623d2c",
"packages/core/skills/app-shell-patterns/references/concepts/styling-theming.md": "7ee0130a20b88310",
"packages/core/skills/app-shell-patterns/references/components/action-panel.md": "dc861082aa9610af",
"packages/core/skills/app-shell-patterns/references/components/activity-card.md": "4f386e49380fb0b0",
"packages/core/skills/app-shell-patterns/references/components/ai-chat.md": "824d1aa36bb7f194",
Expand Down
11 changes: 9 additions & 2 deletions docs-src/concepts/styling-theming.docs.outline.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,14 @@ Each intent has eight roles. The intents are `info`, `success`, `warning`, `dang
<span className="rounded-md bg-success-surface px-2 py-0.5 text-success-text">Paid</span>
```

In the default palette, `text` on `surface` and `contrast` on `solid` and `solid-hover` are at least 4.5:1 for the four hue intents, in light and dark mode, on `--card` and `--background`. A unit test checks this from the shipped CSS. `text` on `surface-hover` is held at 4.2:1 by the same test; it is below 4.5:1 for `danger` and `success` in light mode. The test does not cover the `cream` and `bloom` palettes or the `neutral` intent.
The hue roles have the same values in every shipped palette. `cream` and `bloom` change brand and surface colors, not status colors. The `neutral` roles follow the palette, because they read the system tokens.

**Contrast.** AppShell's thresholds are 4.5:1 for text (WCAG 1.4.3) and 3:1 for non-text (WCAG 1.4.11). In every shipped palette (`default`, `cream`, `bloom`), in light and dark mode, on `--card` and `--background`, `text` on `surface` and `contrast` on `solid` and `solid-hover` are at least 4.5:1 for the four hue intents, and `danger` `solid` (the invalid-input border) is at least 3:1. A browser test checks this in Chromium against the built stylesheet. Known exceptions:

- `text` on `surface-hover` is below 4.5:1 for `danger` and `success` in light mode. The test holds it at 4.2:1 or more.
- In `cream` and `bloom`, the main content area is transparent over the shell gradient. At the top of the `bloom` light gradient, `text` on `surface` is 4.49:1 for `success` and 4.48:1 for `danger`. Put status content on a card when it needs to pass.
- `warning` `solid` and `indicator` are 2.15:1 against a light card. Use them with a label, not as the only sign of a state.
- The `neutral` intent reads the system tokens and is not checked.

Most `surface` and `border` values are translucent tints, so an opacity modifier compounds rather than replaces — `bg-info-surface/50` halves the tint's alpha instead of setting it to 50%. Some light-mode values are opaque instead of translucent: the `warning` surface, surface-hover and border, the `info` border and the `danger` surface-hover. They do not blend with a tinted parent. All dark-mode values are translucent.

Expand All @@ -210,7 +217,7 @@ Most `surface` and `border` values are translucent tints, so an opacity modifier

`--sidebar`, `--sidebar-foreground`, `--sidebar-border`, `--sidebar-primary(-foreground)`, `--sidebar-accent(-foreground)`, `--sidebar-ring` → `bg-sidebar`, `text-sidebar-foreground`, and so on. These let a palette tint the shell independently of page content.

`--chart-1` … `--chart-5` → `bg-chart-1`, `text-chart-1`, `fill-chart-1`. Use them in order for categorical series.
`--chart-1` … `--chart-5` → `bg-chart-1`, `text-chart-1`, `fill-chart-1`. Use them in order for categorical series. They tell series apart and carry no status, so they are not semantic roles and have literal values.

#### Alerts

Expand Down
11 changes: 9 additions & 2 deletions docs/concepts/styling-theming.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"vendor:fonts": "node scripts/sync-noto-fonts.mjs",
"lint": "oxlint -c .oxlintrc.jsonc",
"test": "vitest run",
"test:browser": "pnpm build && vitest run --config vitest.browser.config.ts",
"type-check": "tsc --incremental"
},
"dependencies": {
Expand Down Expand Up @@ -84,8 +85,10 @@
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"@vitejs/plugin-react": "catalog:",
"@vitest/browser-playwright": "4.1.11",
"@vitest/coverage-v8": "4.1.11",
"happy-dom": "21.0.4",
"playwright": "1.63.0",
"postcss": "8.5.31",
"react": "catalog:",
"react-dom": "catalog:",
Expand Down
Loading
Loading