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
68 changes: 56 additions & 12 deletions .claude/docs/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,17 +258,51 @@ bg-[var(--bg-surface)] text-[var(--text-default)] text-[var(--text-muted)] ring-
| Role | Examples |
| ------------- | --------------------------------------------------------------------------------------------------------- |
| Brand actions | `--primary`, `--primary-contrast`, `--secondary`, `--secondary-contrast` |
| Surfaces | `--bg-canvas`, `--bg-surface`, `--bg-surface-raised`, `--bg-hover`, `--bg-active`, `--bg-selected`, `--bg-disabled`, `--bg-mask` |
| Surfaces | `--bg-canvas`, `--bg-surface`, `--bg-surface-raised`, `--bg-surface-overlay`, `--bg-hover`, `--bg-active`, `--bg-selected`, `--bg-disabled`, `--bg-mask` |
| Placeholders | `--bg-placeholder`, `--bg-placeholder-highlight` (see § Loading placeholders) |
| Text | `--text-default`, `--text-muted`, `--text-disabled` |
| Borders | `--border-default`, `--border-muted`, `--border-strong`, `--border-selected` |
| Feedback | `--success`, `--success-border`, `--success-contrast`, `--warning`, `--warning-border`, `--warning-contrast`, `--danger`, `--danger-border`, `--danger-contrast`, `--info`, `--info-border`, `--info-contrast` |
| Code syntax | `--code-sintax-identifier`, `--code-sintax-line-number`, `--code-sintax-keyword`, `--code-sintax-string`, `--code-sintax-function`, `--code-sintax-type`, `--code-sintax-punctuation` |

### Loading placeholders

A loading placeholder (skeleton, image stand-in, lazy block) does **not** use a surface token. Surface tokens
are opaque and tuned for one background, so a placeholder built from one is only legible on the surface it was
picked against — `--bg-surface-overlay` (`#FAFAFA`) on a `--bg-surface` card (`#FFFFFF`) is a contrast ratio of
**1.02**, effectively invisible. Use the placeholder pair instead:

| Token | light | dark | Role |
| ---------------------------- | ---------------------- | ---------------------- | --------------------------------------------- |
| `--bg-placeholder` | `#00000014` (8% black) | `#FFFFFF1A` (10% white) | The fill that reserves the content's space. |
| `--bg-placeholder-highlight` | `#FFFFFF99` (60% white) | `#FFFFFF1A` (10% white) | The sweep that passes over the fill while loading. |

Both are **translucent by design** — that is what makes one value per mode correct everywhere. The fill
composites over whatever sits behind it, so the same token holds its contrast on `--bg-canvas`, `--bg-surface`,
`--bg-surface-raised` and `--bg-surface-overlay` alike (ratio `1.19`–`1.32` across all eight combinations)
instead of being tuned for a single card color.

The highlight is a white alpha in **both** modes: it *lightens* the fill rather than replacing it. Keep the fill
as `background-color` and put the sweep in a `background-image` gradient whose other stops are `transparent`, so
the animated and static states share the same base tone:

```html
class="bg-[var(--bg-placeholder)]
data-[animated]:motion-safe:bg-[linear-gradient(90deg,transparent_0%,transparent_35%,var(--bg-placeholder-highlight)_50%,transparent_65%,transparent_100%)]
data-[animated]:motion-safe:bg-[length:200%_100%]
data-[animated]:motion-safe:animate-[var(--animate-shimmer)]
motion-reduce:animate-none"
```

Reference: `skeleton.vue`. The sweep pairs with `--animate-shimmer` (see § Motion primitives) and is gated behind
`motion-safe:` with a `motion-reduce:animate-none` fallback.

### Do not

- Hardcode hex/rgb/hsl in component class strings.
- Use Tailwind palette names (`bg-gray-500`, `text-violet-600`) in `components/webkit/`.
- Use external/legacy color utilities (`text-color`, `surface-*`) in new webkit components — map to `var(--*)` tokens instead.
- Build a loading placeholder from an opaque surface token (`--bg-surface-overlay`, `--bg-surface-raised`) — it only reads on the one surface it was picked against. Use `--bg-placeholder` / `--bg-placeholder-highlight`.

Reference: `button.vue` kind variants (`bg-[var(--secondary)]`, `text-[var(--secondary-contrast)]`, etc.).

Expand All @@ -293,17 +327,27 @@ Reference: `button.vue` kind variants (`bg-[var(--secondary)]`, `text-[var(--sec

## Animations

Use **only** the named utilities below; they ship with `motion-safe` / `motion-reduce` variants enabled by the theme plugin. Defined in `packages/theme/src/tokens/semantic/animations.js`.

| Utility | Behavior | Duration · Easing |
| ------------------------- | ------------------------- | ----------------------------------------------------------------------------- |
| `animate-fade-in` | opacity 0 → 1 | 220ms · `ease-in-out` |
| `animate-fade-out` | opacity 1 → 0 | 220ms · `ease-in-out` |
| `animate-slide-down` | height 0 → auto | 220ms · `ease-in-out` |
| `animate-popup-scale-in` | scale 0.95 → 1 + fade in | `moderate-01` · `productive-entrance` (origin: `var(--popup-origin, center)`) |
| `animate-popup-scale-out` | scale 1 → 0.95 + fade out | `fast-02` · `productive-exit` |
| `animate-blink` | opacity 1/0 cycle | 1s · `step-end` infinite |
| `animate-highlight-fade` | row-flash highlight | `ease-in forwards` |
Use **only** the named utilities below; they ship with `motion-safe` / `motion-reduce` variants enabled by the theme plugin. The `--animate-*` presets are defined in `packages/theme/src/tokens/primitives/animations/animate.js` (each with a one-line `useWhen` note) and their `@keyframes` in the sibling `keyframes.js`; `build-tokens.mjs` emits both into `dist/v4/globals.css`.

| Utility | Behavior | Duration · Easing |
| ------------------------------------ | -------------------------------------------- | ----------------------------------------------------------------------------- |
| `animate-fade-in` | opacity 0 → 1 | 220ms · `ease-in-out` |
| `animate-fade-out` | opacity 1 → 0 | 220ms · `ease-in-out` |
| `animate-slide-down` | height 0 → auto | 220ms · `ease-in-out` |
| `animate-popup-scale-in` | scale 0.9 → 1 + fade in | `moderate-01` · `productive-entrance` (origin: `var(--popup-origin, center)`) |
| `animate-popup-scale-out` | scale 1 → 0.9 + fade out | `fast-02` · `productive-exit` |
| `animate-slide-in-left` | translateX -100% → 0 | `moderate-02` · `productive-entrance` |
| `animate-slide-out-left` | translateX 0 → -100% | `moderate-01` · `productive-exit` |
| `animate-slide-in-right` | translateX 100% → 0 | `moderate-02` · `productive-entrance` |
| `animate-slide-out-right` | translateX 0 → 100% | `moderate-01` · `productive-exit` |
| `animate-shimmer` | `background-position` 200% → -200% sweep | 1.6s · `linear` infinite |
| `animate-blink` | opacity 1/0 cycle | 1s · `step-end` infinite |
| `animate-highlight-fade` | row-flash highlight | `ease-in forwards` |
| `animate-progress-indeterminate` | indeterminate bar, primary sweep | `slow-04` · `productive-entrance` infinite |
| `animate-progress-indeterminate-short` | indeterminate bar, secondary short sweep | `slow-04` · `expressive-entrance` (delay `slow-03`) infinite |
| `animate-spin` · `animate-ping` · `animate-pulse` · `animate-bounce` | Tailwind's stock loading/attention loops, re-registered in `animate.js` | see `animate.js` |

`animate-shimmer` drives a gradient sweep and expects a `background-image` gradient plus `bg-[length:200%_100%]` — see § Loading placeholders for the canonical class string.

### Easing primitives — `primitives/animations/ease.js`

Expand Down
15 changes: 8 additions & 7 deletions .specs/skeleton.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ spec_version: 1
figma:
url: https://www.figma.com/design/t97pXRs7xME3SJDs5iZ5RF/Webkit?node-id=479-881
node_id: 479:881
checksum: 5ebe624103f06c4dcdc3fac32789b74cc7fd48142bd277515151e8c6d44e5423
checksum: e62845a1427b073406d90b13110cc5d38a20b9025b7a8cd2d043b20db6c2cd04
created: 2026-06-23
last_updated: 2026-06-26
last_updated: 2026-07-28
---

# Skeleton — Component Spec
Expand Down Expand Up @@ -58,20 +58,21 @@ import Skeleton from '@aziontech/webkit/skeleton'

| Trigger | Animation / Transition | Token (see `.claude/docs/DESIGN.md` § Animations) | Reduced-motion fallback |
|---|---|---|---|
| while loading (`animated`) | `animate-shimmer` (linear gradient sweep over the fill) | `--animate-shimmer` (`animate.js`) + `@keyframes shimmer` (`semantic/animations.js`) | gated behind `motion-safe:` (no sweep) + `motion-reduce:animate-none` (static) |
| while loading (`animated`) | `animate-shimmer` (linear gradient sweep over the fill) | `--animate-shimmer` (`primitives/animations/animate.js`) + `@keyframes shimmer` (`primitives/animations/keyframes.js`) | gated behind `motion-safe:` (no sweep) + `motion-reduce:animate-none` (static) |

## Tokens

| Region | Token (DESIGN.md) |
|---|---|
| fill | `var(--bg-placeholder)` |
| shimmer sweep | `var(--bg-placeholder-highlight)` |
| shape radius (`shape`) | `var(--shape-elements)` |

`--bg-placeholder` and `--bg-placeholder-highlight` are **translucent** (alpha) by design: the fill composites over whatever surface the placeholder sits on (`--bg-canvas`, `--bg-surface`, `--bg-surface-raised`, `--bg-surface-overlay`), so a single value per mode keeps the contrast correct everywhere. The sweep is a white alpha in both modes — it lightens the fill rather than replacing it, so the animated and static states share the same base tone.

## Theme gaps

| Figma variable | Temporary primitive | Follow-up |
|---|---|---|
| `--bg-surface-overlay` (skeleton fill, #4D4D4D dark / #FAFAFA light) | `bg-[var(--bg-surface-overlay)]` (real token, not yet in DESIGN.md) | `TODO: document --bg-surface-overlay in DESIGN.md` |
| shimmer animation | `animate-shimmer` (`--animate-shimmer` preset in `animate.js` + `@keyframes shimmer` in `semantic/animations.js`) | `TODO: document --animate-shimmer in DESIGN.md § Animations` |
| _none_ | — | Both prior gaps are closed: `--bg-placeholder` / `--bg-placeholder-highlight` are documented in `.claude/docs/DESIGN.md` § Colors → Loading placeholders, and `--animate-shimmer` in § Animations. |

## Accessibility (WCAG 2.1 AA)

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions apps/storybook/src/foundations/utils/theme-tokens.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@ const DESCRIPTIONS = {
'--bg-surface': 'Primary element background (cards, panels).',
'--bg-surface-raised': 'Raised element background — one step above the surface.',
'--bg-surface-overlay': 'Floating surface for menus, popovers and dropdowns.',
'--bg-placeholder':
'Translucent fill for loading placeholders (skeletons, image stand-ins) — composites over any surface.',
'--bg-placeholder-highlight': 'Sweep highlight passing over a loading placeholder.',
'--bg-hover': 'Hover overlay for interactive surfaces.',
'--bg-active': 'Active / pressed overlay for interactive surfaces.',
'--bg-selected': 'Background for a selected element.',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,47 @@ export const Types = {
}
}

const SURFACES_TEMPLATE = `<div class="grid grid-cols-2 gap-[var(--spacing-md)]">
<div class="flex flex-col gap-[var(--spacing-sm)] rounded-[var(--shape-card)] bg-[var(--bg-canvas)] p-[var(--spacing-md)]">
<span class="text-label-sm text-[var(--text-muted)]">--bg-canvas</span>
<Skeleton kind="shape" width="100%" height="16px" />
<Skeleton kind="shape" width="60%" height="16px" />
</div>
<div class="flex flex-col gap-[var(--spacing-sm)] rounded-[var(--shape-card)] bg-[var(--bg-surface)] p-[var(--spacing-md)]">
<span class="text-label-sm text-[var(--text-muted)]">--bg-surface</span>
<Skeleton kind="shape" width="100%" height="16px" />
<Skeleton kind="shape" width="60%" height="16px" />
</div>
<div class="flex flex-col gap-[var(--spacing-sm)] rounded-[var(--shape-card)] bg-[var(--bg-surface-raised)] p-[var(--spacing-md)]">
<span class="text-label-sm text-[var(--text-muted)]">--bg-surface-raised</span>
<Skeleton kind="shape" width="100%" height="16px" />
<Skeleton kind="shape" width="60%" height="16px" />
</div>
<div class="flex flex-col gap-[var(--spacing-sm)] rounded-[var(--shape-card)] bg-[var(--bg-surface-overlay)] p-[var(--spacing-md)]">
<span class="text-label-sm text-[var(--text-muted)]">--bg-surface-overlay</span>
<Skeleton kind="shape" width="100%" height="16px" />
<Skeleton kind="shape" width="60%" height="16px" />
</div>
</div>`

/** @type {import('@storybook/vue3').StoryObj<typeof Skeleton>} */
export const OnSurfaces = {
render: () => ({
components: { Skeleton },
template: SURFACES_TEMPLATE
}),
parameters: {
docs: {
controls: { disable: true },
description: {
story:
'The same placeholder on all four surface layers. The fill is `var(--bg-placeholder)`, a translucent role rather than an opaque surface, so it composites over whatever sits behind it and holds its contrast on every layer — in both themes. Built from a surface token instead, a placeholder is only legible on the one surface it was picked against.'
},
source: { code: toSfc(IMPORT, SURFACES_TEMPLATE) }
}
}
}

const STATIC_MARKUP = '<Skeleton kind="shape" width="240px" height="100px" :animated="false" />'

/** @type {import('@storybook/vue3').StoryObj<typeof Skeleton>} */
Expand Down
6 changes: 6 additions & 0 deletions packages/theme/dist/v4/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -845,6 +845,8 @@
--bg-surface-raised: #FFFFFF;
--bg-mask: #00000005;
--bg-surface-overlay: #FAFAFA;
--bg-placeholder: #00000014;
--bg-placeholder-highlight: #FFFFFF99;
--bg-hover: #00000014;
--bg-active: #00000005;
--bg-backdrop: #000000CC;
Expand Down Expand Up @@ -913,6 +915,8 @@
--bg-surface-raised: #141414;
--bg-mask: #FFFFFF0D;
--bg-surface-overlay: #4D4D4D;
--bg-placeholder: #FFFFFF1A;
--bg-placeholder-highlight: #FFFFFF1A;
--bg-hover: #00000033;
--bg-active: #00000014;
--bg-backdrop: #000000CC;
Expand Down Expand Up @@ -1323,6 +1327,8 @@
@utility bg-surface-raised { background-color: var(--bg-surface-raised); }
@utility bg-mask { background-color: var(--bg-mask); }
@utility bg-surface-overlay { background-color: var(--bg-surface-overlay); }
@utility bg-placeholder { background-color: var(--bg-placeholder); }
@utility bg-placeholder-highlight { background-color: var(--bg-placeholder-highlight); }
@utility bg-hover { background-color: var(--bg-hover); }
@utility bg-active { background-color: var(--bg-active); }
@utility bg-backdrop { background-color: var(--bg-backdrop); }
Expand Down
6 changes: 6 additions & 0 deletions packages/theme/dist/v4/globals.scss
Original file line number Diff line number Diff line change
Expand Up @@ -845,6 +845,8 @@
--bg-surface-raised: #FFFFFF;
--bg-mask: #00000005;
--bg-surface-overlay: #FAFAFA;
--bg-placeholder: #00000014;
--bg-placeholder-highlight: #FFFFFF99;
--bg-hover: #00000014;
--bg-active: #00000005;
--bg-backdrop: #000000CC;
Expand Down Expand Up @@ -913,6 +915,8 @@
--bg-surface-raised: #141414;
--bg-mask: #FFFFFF0D;
--bg-surface-overlay: #4D4D4D;
--bg-placeholder: #FFFFFF1A;
--bg-placeholder-highlight: #FFFFFF1A;
--bg-hover: #00000033;
--bg-active: #00000014;
--bg-backdrop: #000000CC;
Expand Down Expand Up @@ -1323,6 +1327,8 @@
@utility bg-surface-raised { background-color: var(--bg-surface-raised); }
@utility bg-mask { background-color: var(--bg-mask); }
@utility bg-surface-overlay { background-color: var(--bg-surface-overlay); }
@utility bg-placeholder { background-color: var(--bg-placeholder); }
@utility bg-placeholder-highlight { background-color: var(--bg-placeholder-highlight); }
@utility bg-hover { background-color: var(--bg-hover); }
@utility bg-active { background-color: var(--bg-active); }
@utility bg-backdrop { background-color: var(--bg-backdrop); }
Expand Down
4 changes: 4 additions & 0 deletions packages/theme/src/tokens/theme/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ export const background = {
'bg-surface-raised': tokenRef('theme.surfaces.surface-0'),
'bg-mask': tokenRef('primitives.alpha.black.50'),
'bg-surface-overlay': tokenRef('theme.surfaces.surface-50'),
'bg-placeholder': tokenRef('primitives.alpha.black.100'),
'bg-placeholder-highlight': tokenRef('primitives.alpha.white.600'),
'bg-hover': tokenRef('primitives.alpha.black.100'),
'bg-active': tokenRef('primitives.alpha.black.50'),
'bg-backdrop': tokenRef('primitives.alpha.black.800'),
Expand All @@ -20,6 +22,8 @@ export const background = {
'bg-surface-raised': tokenRef('theme.surfaces.surface-900'),
'bg-mask': tokenRef('primitives.alpha.white.50'),
'bg-surface-overlay': tokenRef('theme.surfaces.surface-700'),
'bg-placeholder': tokenRef('primitives.alpha.white.100'),
'bg-placeholder-highlight': tokenRef('primitives.alpha.white.100'),
'bg-hover': tokenRef('primitives.alpha.black.200'),
'bg-active': tokenRef('primitives.alpha.black.100'),
'bg-backdrop': tokenRef('primitives.alpha.black.800'),
Expand Down
4 changes: 4 additions & 0 deletions packages/webkit/catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@
"--bg-disabled",
"--bg-hover",
"--bg-mask",
"--bg-placeholder",
"--bg-placeholder-highlight",
"--bg-selected",
"--bg-surface",
"--bg-surface-overlay",
Expand Down Expand Up @@ -746,6 +748,8 @@
"--bg-disabled",
"--bg-hover",
"--bg-mask",
"--bg-placeholder",
"--bg-placeholder-highlight",
"--bg-selected",
"--bg-surface",
"--bg-surface-overlay",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,25 @@ names from the theme package; never invent a `surface-3`.
</div>
```

## Loading placeholders use a translucent role, not a surface

A surface token is opaque and tuned for one background, so a skeleton built from one is only legible on
the surface it was picked against — and it inverts badly across themes. `--bg-placeholder` (the fill)
and `--bg-placeholder-highlight` (the sweep) are **translucent**: they composite over whatever layer
they sit on, so the same token holds its contrast on canvas, surface, raised and overlay alike, in both
themes. Keep the fill as `background-color` and put the sweep in a gradient whose other stops are
`transparent`, so the animated and static states share one base tone.

```vue
<!-- ❌ opaque surface as a placeholder: near-invisible on a card in light -->
<div class="bg-[var(--bg-surface-overlay)]" />
<!-- ✅ translucent placeholder role, correct on any layer in both themes -->
<div class="bg-[var(--bg-placeholder)]" />
```

Prefer the design system's `Skeleton` component over hand-rolling this — it already pairs the tokens
with the shimmer animation and its reduced-motion fallback.

## Don't fight the tokens with `dark:` overrides

If a role has a token, the token already handles both themes — adding `dark:bg-…` on top is redundant
Expand Down
Loading
Loading