@once-ui-system/core 1.7.9 → 1.7.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ai/eval-tasks.md +71 -0
- package/ai/recipes.md +149 -0
- package/ai/rules.md +217 -0
- package/ai/spec.json +2702 -0
- package/dist/components/Column.d.ts +1 -1
- package/dist/components/Column.d.ts.map +1 -1
- package/dist/components/Column.js +1 -2
- package/dist/components/Column.js.map +1 -1
- package/dist/components/Grid.d.ts.map +1 -1
- package/dist/components/Grid.js +0 -1
- package/dist/components/Grid.js.map +1 -1
- package/dist/components/Row.d.ts +1 -1
- package/dist/components/Row.d.ts.map +1 -1
- package/dist/components/Row.js +1 -2
- package/dist/components/Row.js.map +1 -1
- package/dist/data/emoji-data.json +0 -18
- package/dist/modules/navigation/HeadingNav.js +1 -1
- package/dist/modules/navigation/HeadingNav.js.map +1 -1
- package/dist/package.json +1 -1
- package/package.json +3 -2
package/ai/eval-tasks.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Once UI — AI generation eval suite
|
|
2
|
+
|
|
3
|
+
Fixed prompt set for measuring harness effectiveness. Run each task with and
|
|
4
|
+
without the harness (rules.md + spec.json), ideally across two models, and
|
|
5
|
+
score with the checklist below. Add a regression task whenever a new pitfall
|
|
6
|
+
is found in the wild.
|
|
7
|
+
|
|
8
|
+
## Tasks
|
|
9
|
+
|
|
10
|
+
1. **Analytics dashboard** — KPI stat cards (trend indicators), a line chart, a bar chart, recent-activity list. Tests: data components, `CountFx`, grid responsiveness.
|
|
11
|
+
2. **Settings page** — sidebar sections, profile form (avatar upload, inputs, selects), danger zone. Tests: form controls, `MediaUpload`, surface recipes.
|
|
12
|
+
3. **Pricing page** — three tiers, highlighted middle tier, feature comparison table, FAQ accordion. Tests: `Card`, `Badge`, `Table`, `Accordion`, proportional emphasis.
|
|
13
|
+
4. **Chat UI** — message list with avatars, streaming indicator, input bar with emoji picker, command palette hint. Tests: `EmojiPickerDropdown`, `Kbd`, `Spinner`, scroll containers.
|
|
14
|
+
5. **Marketing hero** — headline with text effect, CTA pair, social proof logos, background effect. Tests: effects usage (`LetterFx`/`TypeFx`/`RevealFx`, `Background`/`MatrixFx`/`BlobFx`), restraint.
|
|
15
|
+
6. **Auth flow** — login with password + OTP step in a stacked dialog. Tests: `Dialog` `stack`, `OTPInput`, `PasswordInput`, validation states.
|
|
16
|
+
7. **E-commerce product card grid** — image, price, tag, hover actions, skeleton loading state. Tests: `Card`, `Media`, `Skeleton`, `Tag`, hover patterns.
|
|
17
|
+
8. **Onboarding checklist** — progress bar, step list with status indicators, celebratory completion. Tests: `ProgressBar`, `StatusIndicator`, `CelebrationFx`.
|
|
18
|
+
9. **Data table page** — sortable table, filters row (segmented control, date range, search), pagination. Tests: `Table`, `SegmentedControl`, `DateRangePicker`, toolbar composition.
|
|
19
|
+
10. **User profile popover** — avatar trigger, `UserMenu`/`HoverCard` with stats and actions. Tests: overlay components, `AvatarGroup`.
|
|
20
|
+
11. **Changelog/timeline page** — versioned entries on a `Timeline`, tags per release, copyable code snippets. Tests: `Timeline`, `CodeBlock`, `InlineCode`.
|
|
21
|
+
12. **Feedback widget** — floating trigger, dialog with rating chips, textarea, toast on submit. Tests: `Chip` selection, `useToast`/`Feedback`, positioning.
|
|
22
|
+
|
|
23
|
+
## Scoring checklist (per task)
|
|
24
|
+
|
|
25
|
+
Mechanical (count occurrences):
|
|
26
|
+
|
|
27
|
+
- [ ] Compiles / no invented props or components
|
|
28
|
+
- [ ] Redundant-default props (`position="relative"`, `variant="primary"` on Button, ...) — target: 0
|
|
29
|
+
- [ ] Shorthand violations (`fillWidth fillHeight` instead of `fill`, `horizontal+vertical center` instead of `center`, `Flex direction=` instead of `Row`/`Column`) — target: 0
|
|
30
|
+
- [ ] `style={{}}` uses where a token prop exists — target: 0
|
|
31
|
+
- [ ] Raw HTML elements where a component exists (`img`, `a`, `button`) — target: 0
|
|
32
|
+
- [ ] Hex/rgb colors — target: 0
|
|
33
|
+
- [ ] Distinct Once UI components used — higher is better (task-appropriate)
|
|
34
|
+
|
|
35
|
+
Judgment (1–5):
|
|
36
|
+
|
|
37
|
+
- [ ] Proportionality: spacing rhythm, type scale, surface hierarchy look like real Once UI
|
|
38
|
+
- [ ] Component appropriateness: purpose-built components chosen over generic primitives
|
|
39
|
+
- [ ] Responsiveness: breakpoint props used where layout should adapt
|
|
40
|
+
- [ ] Restraint: effects enhance rather than dominate
|
|
41
|
+
|
|
42
|
+
## Pitfall log
|
|
43
|
+
|
|
44
|
+
Record every recurring failure here; each entry should become a rule in
|
|
45
|
+
`rules.md` or a gold example, plus stay in this list as a regression check.
|
|
46
|
+
|
|
47
|
+
| Date | Pitfall observed | Fix applied |
|
|
48
|
+
|------|------------------|-------------|
|
|
49
|
+
| 2026-06-12 | `position="relative"` written despite being default | rules.md #3 |
|
|
50
|
+
| 2026-06-12 | `fillWidth fillHeight` instead of `fill` | rules.md #2 |
|
|
51
|
+
| 2026-06-12 | `horizontal="center" vertical="center"` instead of `center` | rules.md #2 |
|
|
52
|
+
| 2026-06-12 | Only ~10 common components used, defaults only | rules.md #12–13 |
|
|
53
|
+
| 2026-06-12 | Section gaps too small (48), page feels cramped | rules.md #17 |
|
|
54
|
+
| 2026-06-12 | Section headings too small (`heading-strong-m`), hero undersized | rules.md #18 |
|
|
55
|
+
| 2026-06-12 | Testimonial quote (BlockQuote default) larger than section title | rules.md #19 |
|
|
56
|
+
| 2026-06-12 | Identical centered heading+subtitle on every section (dated); Tag+sparkle eyebrow | rules.md #20 |
|
|
57
|
+
| 2026-06-12 | Absolute Background missing `top="0" left="0"` → offset by parent padding | rules.md #21, recipes.md |
|
|
58
|
+
| 2026-06-12 | Gradient `width: 500` (125% of container); units misunderstood | rules.md #22 |
|
|
59
|
+
| 2026-06-12 | `brand-background-weak` glow at `opacity: 30` — invisible vs page | rules.md #23, recipes.md |
|
|
60
|
+
| 2026-06-12 | `RevealFx delay={index * 80}` (seconds!) and `translateY={16}` (16rem!) | rules.md #24, recipes.md #7 |
|
|
61
|
+
| 2026-06-12 | `LogoCloud` without `columns` → single-column stack | rules.md #25 |
|
|
62
|
+
| 2026-06-12 | No decorative components used beyond one gradient; flat result | recipes.md + rules.md #26 |
|
|
63
|
+
| 2026-06-12 | RevealFx on every section (dated, eye-grabbing); mount-timed animations finish off-screen | rules.md #27–29, recipes.md #7 |
|
|
64
|
+
| 2026-06-12 | Hero gradient clipped: ambient layer inside narrow `maxWidth` container with `overflow="hidden"` | rules.md #21b, recipes.md #1 |
|
|
65
|
+
| 2026-06-12 | Reveal on hero text fragments + lone highlighted card (incoherent grouping) | rules.md #28b |
|
|
66
|
+
| 2026-06-12 | `Card` used for static panels (tiers, testimonials, stats) | rules.md #12 |
|
|
67
|
+
| 2026-06-12 | Arbitrary `maxWidth` numbers; no container-width convention | rules.md #15 |
|
|
68
|
+
| 2026-06-12 | Design intent (style/layout/color/decoration/imagery) guessed instead of asked | rules.md "Before you build" |
|
|
69
|
+
| 2026-06-12 | Invented icon names (`more`, `bell`, `arrowRight`) | rules.md #14b, IconName list in spec.json |
|
|
70
|
+
| 2026-06-12 | `Badge title` + children rendered same text twice ("2 unread2") | rules.md #14c |
|
|
71
|
+
| 2026-06-12 | `Fade` wrapped the whole message list → washed-out content | rules.md #14d |
|
package/ai/recipes.md
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# Once UI — decoration recipes
|
|
2
|
+
|
|
3
|
+
Curated decorative patterns that keep pages premium without going over the top.
|
|
4
|
+
Use them verbatim, then adjust values. All units follow rules.md #21–24.
|
|
5
|
+
|
|
6
|
+
## Budget (hard limits)
|
|
7
|
+
|
|
8
|
+
- **One ambient background layer per section**, and only in sections that anchor the page (hero, highlighted card, final CTA). Plain sections stay plain — contrast is what makes the decorated ones read as premium.
|
|
9
|
+
- **Continuous motion** (MatrixFx flicker, BlobFx drift) in at most **two places per page**, never adjacent.
|
|
10
|
+
- **One accent family per page.** Decorations use `brand-*` (or `neutral-*`); never mix brand + accent + schemes in decoration.
|
|
11
|
+
- Every decorative layer: `position="absolute" top="0" left="0" fill pointerEvents="none"`, content sibling `zIndex={1}`, parent `overflow="hidden"`.
|
|
12
|
+
|
|
13
|
+
## 1. Hero glow + dots
|
|
14
|
+
|
|
15
|
+
Soft brand glow from the top edge with a subtle dot texture. The workhorse ambient layer.
|
|
16
|
+
|
|
17
|
+
Placement matters: the wrapper is **full-bleed** (`fillWidth`, outside any `maxWidth` column) and has **no** `overflow="hidden"` — the gradient fades out via `colorEnd: "static-transparent"`. Clipping it inside a narrow container produces a hard-edged band.
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
<Column fillWidth horizontal="center" paddingY="80">
|
|
21
|
+
<Background
|
|
22
|
+
position="absolute" top="0" left="0" fill pointerEvents="none"
|
|
23
|
+
gradient={{
|
|
24
|
+
display: true,
|
|
25
|
+
colorStart: "brand-alpha-medium",
|
|
26
|
+
colorEnd: "static-transparent",
|
|
27
|
+
x: 50, y: 0,
|
|
28
|
+
width: 150, height: 80,
|
|
29
|
+
opacity: 60,
|
|
30
|
+
}}
|
|
31
|
+
dots={{ display: true, color: "neutral-alpha-weak", size: "2", opacity: 40 }}
|
|
32
|
+
/>
|
|
33
|
+
<Column zIndex={1} horizontal="center" gap="24" maxWidth={48}>
|
|
34
|
+
{/* hero content */}
|
|
35
|
+
</Column>
|
|
36
|
+
</Column>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## 2. MatrixFx hero strip (with fade-out)
|
|
40
|
+
|
|
41
|
+
Animated matrix field behind a hero, faded into the page with a `page-background` gradient so it doesn't compete with content. Pair exactly like this:
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
<Column fillWidth minHeight="s" center overflow="hidden">
|
|
45
|
+
<MatrixFx
|
|
46
|
+
position="absolute" top="0" left="0" fill
|
|
47
|
+
flicker size={1.5} spacing={8}
|
|
48
|
+
colors={["brand-solid-strong"]}
|
|
49
|
+
bulge={{ type: "wave", duration: 3, intensity: 20, repeat: true }}
|
|
50
|
+
/>
|
|
51
|
+
<Background
|
|
52
|
+
position="absolute" top="0" left="0" fill pointerEvents="none"
|
|
53
|
+
gradient={{ display: true, colorStart: "page-background", x: 0, y: 50, width: 150, height: 300 }}
|
|
54
|
+
/>
|
|
55
|
+
<Column zIndex={1} center gap="16" padding="48">
|
|
56
|
+
{/* hero content */}
|
|
57
|
+
</Column>
|
|
58
|
+
</Column>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## 3. Highlighted card glow
|
|
62
|
+
|
|
63
|
+
For the emphasized item in a set (e.g. "most popular" pricing tier). Border + top glow, nothing else:
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
<Card fillWidth padding="24" radius="l" background="surface" border="brand-alpha-medium" overflow="hidden">
|
|
67
|
+
<Background
|
|
68
|
+
position="absolute" top="0" left="0" fill pointerEvents="none"
|
|
69
|
+
gradient={{
|
|
70
|
+
display: true,
|
|
71
|
+
colorStart: "brand-alpha-medium",
|
|
72
|
+
colorEnd: "static-transparent",
|
|
73
|
+
x: 50, y: 0,
|
|
74
|
+
width: 200, height: 60,
|
|
75
|
+
opacity: 50,
|
|
76
|
+
}}
|
|
77
|
+
/>
|
|
78
|
+
<Column zIndex={1} gap="20" fillWidth>
|
|
79
|
+
{/* card content */}
|
|
80
|
+
</Column>
|
|
81
|
+
</Card>
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## 4. BlobFx ambient panel
|
|
85
|
+
|
|
86
|
+
Self-contained drifting brand/accent blobs (already blurred and pointer-transparent). Use behind a final CTA or feature visual — counts as continuous motion:
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
<Card fillWidth radius="l" border="neutral-alpha-weak" overflow="hidden" padding="48">
|
|
90
|
+
<BlobFx position="absolute" top="0" left="0" fill opacity={60} seed={2} />
|
|
91
|
+
<Column zIndex={1} center gap="24">
|
|
92
|
+
{/* CTA content */}
|
|
93
|
+
</Column>
|
|
94
|
+
</Card>
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## 5. Technical texture (lines / grid)
|
|
98
|
+
|
|
99
|
+
Diagonal lines or grid texture for footers of cards, table headers, "engineering" flavored strips. No motion:
|
|
100
|
+
|
|
101
|
+
```tsx
|
|
102
|
+
<Background
|
|
103
|
+
position="absolute" top="0" left="0" fill pointerEvents="none"
|
|
104
|
+
lines={{ display: true, color: "neutral-alpha-weak", angle: -45, size: "4" }}
|
|
105
|
+
/>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## 6. Section divider
|
|
109
|
+
|
|
110
|
+
Between major sections when the gap alone isn't enough — a short line, optionally with an eyebrow:
|
|
111
|
+
|
|
112
|
+
```tsx
|
|
113
|
+
<Column fillWidth horizontal="center" gap="16">
|
|
114
|
+
<Line width="40" />
|
|
115
|
+
</Column>
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## 7. Reveal stagger (entry, not ambient)
|
|
119
|
+
|
|
120
|
+
Entrance animation for **one** key set per page (rules.md #27–28), viewport-triggered and latched so it plays once, when actually seen (rules.md #29):
|
|
121
|
+
|
|
122
|
+
```tsx
|
|
123
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
124
|
+
const inViewport = useInViewport(ref);
|
|
125
|
+
const [seen, setSeen] = useState(false);
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
if (inViewport) setSeen(true);
|
|
128
|
+
}, [inViewport]);
|
|
129
|
+
|
|
130
|
+
<Grid ref={ref} columns="3" gap="16" s={{ columns: 1 }}>
|
|
131
|
+
{items.map((item, index) => (
|
|
132
|
+
<RevealFx key={item.id} trigger={seen} translateY="8" delay={index * 0.1}>
|
|
133
|
+
<Card ... />
|
|
134
|
+
</RevealFx>
|
|
135
|
+
))}
|
|
136
|
+
</Grid>
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
For stats, drive `CountFx` value from the same latch: `<CountFx value={seen ? 2400 : 0} separator="," />`.
|
|
140
|
+
|
|
141
|
+
## Anti-patterns
|
|
142
|
+
|
|
143
|
+
- Glow with `colorStart: "brand-background-weak"` — invisible against the page; use alpha tokens.
|
|
144
|
+
- `width: 500` gradients — that's 125% of the container blown past its edges; stay ≤ 200 for glows.
|
|
145
|
+
- Background layers without `top="0" left="0"` inside padded parents — misaligned.
|
|
146
|
+
- Decorating every section — if everything glows, nothing does.
|
|
147
|
+
- Two different Fx components fighting in one viewport (MatrixFx + BlobFx side by side).
|
|
148
|
+
- `overflow="hidden"` on a full-bleed section (no radius) — clips the glow into a hard-edged band.
|
|
149
|
+
- Animating one item out of a sibling set (only the highlighted card) — stagger all or animate none.
|
package/ai/rules.md
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# Once UI — code generation rules
|
|
2
|
+
|
|
3
|
+
Rules for writing Once UI code. Follow every rule; each shows the correct form first.
|
|
4
|
+
|
|
5
|
+
## Before you build
|
|
6
|
+
|
|
7
|
+
When the request doesn't specify design intent, ask these five questions first (one short message, all at once), then build:
|
|
8
|
+
|
|
9
|
+
1. **Content style** — bold / restrained / conservative?
|
|
10
|
+
2. **Layout** — wide or narrow, centered or left-aligned?
|
|
11
|
+
3. **Color** — single accent, bold colors, visually distinct sections?
|
|
12
|
+
4. **Decoration** — gradients, MatrixFx, animated blobs, none?
|
|
13
|
+
5. **Imagery** — placeholder images, or avatars/logos/icons only?
|
|
14
|
+
|
|
15
|
+
If you can't ask (or the user says "you decide"), default to: restrained, centered with `maxWidth="m"` content, single brand accent, one subtle gradient (recipes.md #1), no placeholder images (use `Avatar`, `Logo`, `Icon`).
|
|
16
|
+
|
|
17
|
+
## Layout primitives
|
|
18
|
+
|
|
19
|
+
1. Use `Row` and `Column` for layout. `Flex` with `direction` is only for dynamic direction.
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
// Good
|
|
23
|
+
<Column gap="16">...</Column>
|
|
24
|
+
// Bad
|
|
25
|
+
<Flex direction="column" gap="16">...</Flex>
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
2. Use shorthands — they replace multi-prop combinations:
|
|
29
|
+
|
|
30
|
+
| Write | Instead of |
|
|
31
|
+
|-------|------------|
|
|
32
|
+
| `fill` | `fillWidth fillHeight` |
|
|
33
|
+
| `fit` | `fitWidth fitHeight` |
|
|
34
|
+
| `center` | `horizontal="center" vertical="center"` |
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
// Good
|
|
38
|
+
<Row fill center>...</Row>
|
|
39
|
+
// Bad
|
|
40
|
+
<Row fillWidth fillHeight horizontal="center" vertical="center">...</Row>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
3. Never write a prop that equals its default. The spec marks defaults with `= value`. Common offenders:
|
|
44
|
+
- `position="relative"` (default on all layout components)
|
|
45
|
+
- `direction="row"` on `Row`, `direction="column"` on `Column`
|
|
46
|
+
- `variant="primary"`, `size="m"` on `Button`
|
|
47
|
+
|
|
48
|
+
## Spacing and sizing
|
|
49
|
+
|
|
50
|
+
4. Use spacing tokens, never raw CSS: `gap`, `padding`, `margin`, `paddingX/Y`, `marginX/Y` accept tokens (`"2" "4" "8" "12" "16" "20" "24" "32" "40" "48" "64"` or `"xs" "s" "m" "l" "xl"`).
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
// Good
|
|
54
|
+
<Column gap="8" padding="24">
|
|
55
|
+
// Bad
|
|
56
|
+
<div style={{ gap: 8, padding: 24 }}>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
5. `width`/`height`/`maxWidth`/`minHeight` accept numbers (rem), tokens, or CSS units. Prefer numbers/tokens.
|
|
60
|
+
|
|
61
|
+
6. Only use `style={{}}` for things no token prop covers (e.g. `isolation`, custom filters). If a prop exists, the prop wins.
|
|
62
|
+
|
|
63
|
+
## Color and surfaces
|
|
64
|
+
|
|
65
|
+
7. Colors are semantic tokens, never hex/rgb: `{scheme}-{weight}` or `{scheme}-alpha-{weight}` where scheme = `neutral | brand | accent | info | danger | warning | success` and weight = `weak | medium | strong`.
|
|
66
|
+
- `background` also accepts `surface | overlay | page | transparent`
|
|
67
|
+
- `onBackground` colors text/icons on a background
|
|
68
|
+
|
|
69
|
+
8. Standard surface recipe: `background="surface"` (or `"overlay"` over imagery) + `border="neutral-alpha-weak"` (or `-medium`) + `radius="l"`.
|
|
70
|
+
|
|
71
|
+
```tsx
|
|
72
|
+
<Column background="surface" border="neutral-alpha-weak" radius="l" padding="24" gap="16">
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Text
|
|
76
|
+
|
|
77
|
+
9. Use `Text` and `Heading` with `variant="{type}-{weight}-{size}"`: type = `body | heading | display | label | code`, weight = `default | strong`, size = `xs | s | m | l | xl`. Examples: `body-default-m`, `heading-strong-s`, `label-default-s`, `display-strong-m`.
|
|
78
|
+
|
|
79
|
+
10. If a layout component only wraps text, put `textVariant` (and `onBackground`) directly on it instead of nesting a `Text`:
|
|
80
|
+
|
|
81
|
+
```tsx
|
|
82
|
+
// Good
|
|
83
|
+
<Row textVariant="label-default-s" onBackground="neutral-weak">Updated today</Row>
|
|
84
|
+
// Bad
|
|
85
|
+
<Row><Text variant="label-default-s" onBackground="neutral-weak">Updated today</Text></Row>
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Responsive
|
|
89
|
+
|
|
90
|
+
11. Use breakpoint props (`xl l m s xs`) with override objects — never media queries or conditional rendering for layout:
|
|
91
|
+
|
|
92
|
+
```tsx
|
|
93
|
+
<Row gap="24" s={{ direction: "column" }}>
|
|
94
|
+
<Grid columns="3" s={{ columns: 1 }}>
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Components over primitives
|
|
98
|
+
|
|
99
|
+
12. Reach for the purpose-built component before composing primitives:
|
|
100
|
+
- `Card` is for **interactive** surfaces only (`href` or `onClick`). Static panels (pricing tiers, testimonials, stat boxes) = `Column`/`Row` with the surface recipe (#8), not `Card`
|
|
101
|
+
- Image/video → `Media`, not `img`
|
|
102
|
+
- Links → `SmartLink` / `Button href`, not `a`
|
|
103
|
+
- Status dot → `StatusIndicator`; progress → `ProgressBar`; loading → `Spinner` / `Skeleton`
|
|
104
|
+
- Labels/metadata → `Tag`, `Chip`, `Badge`, `Kbd`, `InlineCode`
|
|
105
|
+
|
|
106
|
+
13. Use the distinctive features when the design calls for them — they exist and are cheap:
|
|
107
|
+
- `Dialog` supports `stack` (stacked dialogs) and `base` props
|
|
108
|
+
- Effects: `RevealFx`, `LetterFx`, `TypeFx`, `CountFx`, `GlitchFx`, `HoloFx`, `TiltFx`, `FlipFx`, `MatrixFx`, `BlobFx`, `Particle`, `ShineFx`, `CelebrationFx`
|
|
109
|
+
- `CursorCard`, `HoverCard`, `ContextMenu`, `EmojiPicker`, `CompareImage`, `OgCard`, `Timeline`, `MasonryGrid`, `InfiniteScroll`, `SegmentedControl`, `Kbar` (command palette)
|
|
110
|
+
- Forms: `Input`, `PasswordInput`, `OTPInput`, `NumberInput`, `TagInput`, `Select`, `DatePicker`, `DateRangePicker`, `ColorInput`, `Slider`, `Switch`, `Checkbox`, `Chip`
|
|
111
|
+
|
|
112
|
+
14. Buttons: `variant` = `primary | secondary | tertiary | danger`; pair with `prefixIcon`/`suffixIcon` (icon names, not elements). `IconButton` for icon-only actions with `tooltip`.
|
|
113
|
+
|
|
114
|
+
14b. Icon names must come from the `IconName` list in spec.json — never invent names (`more`, `bell`, and `arrowRight` don't exist; `close`, `chevronRight`, and `smiley` do). If no icon fits, omit the icon.
|
|
115
|
+
|
|
116
|
+
14c. Components with a text prop render it — never pass the same text as both prop and children, it renders twice:
|
|
117
|
+
|
|
118
|
+
```tsx
|
|
119
|
+
// Good
|
|
120
|
+
<Badge>{unread}</Badge> or <Tag size="s">Save 20%</Tag>
|
|
121
|
+
// Bad: renders "2 unread2" / "Save 20%Save 20%"
|
|
122
|
+
<Badge title={`${unread} unread`}>{unread}</Badge>
|
|
123
|
+
<Tag label="Save 20%">Save 20%</Tag>
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
14d. `Fade` is an **edge overlay strip** (scroll-fade at the boundary of a scroll container), not a content wrapper. Wrapping a message list or section in `Fade` washes the whole content out:
|
|
127
|
+
|
|
128
|
+
```tsx
|
|
129
|
+
// Good: thin absolute strip over the scroll edge
|
|
130
|
+
<Column flex={1} overflowY="auto">...</Column>
|
|
131
|
+
<Fade position="absolute" top="0" left="0" fillWidth height="40" to="bottom" base="surface" pointerEvents="none" />
|
|
132
|
+
// Bad
|
|
133
|
+
<Fade to="top" fill><Column overflowY="auto">...</Column></Fade>
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Composition rhythm
|
|
137
|
+
|
|
138
|
+
15. Page skeleton: `Column as="main" fillWidth horizontal="center"` → content `Column maxWidth={...}` → sections. Gaps between siblings, padding inside surfaces; avoid margins for layout when a parent `gap` can do it.
|
|
139
|
+
|
|
140
|
+
`maxWidth` conventions: primary page containers `"xl"`–`"m"` (marketing/landing toward `"xl"`/`"l"`, app and docs content `"m"`). `"s"` is only for narrow reading content (about, legal); `"xs"` for special surfaces (drawers, popovers).
|
|
141
|
+
|
|
142
|
+
16. Data attributes for one-off theme overrides: `data-border="rounded"`, `data-brand="emerald"`, `data-solid="contrast"` — not custom CSS.
|
|
143
|
+
|
|
144
|
+
## Scale and hierarchy (marketing/landing pages)
|
|
145
|
+
|
|
146
|
+
17. Vertical rhythm is generous. Gap between page sections: `"80"`–`"160"` (typical `"104"`). Gap inside a section: `"24"`–`"40"`. Spacing tokens go up to `"160"` — use the top of the scale between sections, not `"48"`.
|
|
147
|
+
|
|
148
|
+
18. Heading scale: hero = `display-strong-l` (or `xl`), section headings = `display-strong-xs` or `display-strong-s` — never `heading-*` variants for section titles. Hero supporting text = `body-default-l`; section supporting text = `body-default-m`.
|
|
149
|
+
|
|
150
|
+
19. Type hierarchy must be monotonic: quotes, testimonials, and body content never render larger than the section heading above them. Testimonial quotes = `body-default-m` max, not `BlockQuote` default size.
|
|
151
|
+
|
|
152
|
+
20. Don't repeat the same centered heading + subtitle block for every section — it reads dated. Vary: left-aligned headers, heading-only sections, or an eyebrow label above the heading. Eyebrow = plain text, not a `Tag` with an icon:
|
|
153
|
+
|
|
154
|
+
```tsx
|
|
155
|
+
// Good eyebrow
|
|
156
|
+
<Text variant="label-default-s" onBackground="brand-medium">Pricing</Text>
|
|
157
|
+
// Bad eyebrow
|
|
158
|
+
<Tag variant="neutral" prefixIcon="sparkle">Pricing</Tag>
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Decorative layers
|
|
162
|
+
|
|
163
|
+
21. Every absolute decorative layer uses the full recipe — `top="0" left="0"` are required or the layer gets offset by parent padding:
|
|
164
|
+
|
|
165
|
+
```tsx
|
|
166
|
+
<Background position="absolute" top="0" left="0" fill pointerEvents="none" ... />
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Siblings holding content get `zIndex={1}`. `overflow="hidden"` goes on the parent **only when it has a radius** (cards, panels) — clipping a full-bleed section turns the glow into a hard-edged band.
|
|
170
|
+
|
|
171
|
+
21b. Ambient section layers (hero glows) live on the **full-width section wrapper**, outside the `maxWidth` content column, with generous room (`paddingY="80"`+). Inside a narrow container the gradient gets clipped at the container edge and reads as a misplaced card:
|
|
172
|
+
|
|
173
|
+
```tsx
|
|
174
|
+
// Good: full-bleed section, narrow content inside
|
|
175
|
+
<Column fillWidth horizontal="center" paddingY="80">
|
|
176
|
+
<Background position="absolute" top="0" left="0" fill pointerEvents="none" gradient={{...}} />
|
|
177
|
+
<Column zIndex={1} maxWidth={48}>...</Column>
|
|
178
|
+
</Column>
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
22. `Background` gradient units: `x`/`y` are percent positions of the container (50/50 = center). `width`/`height` are quarter-percent units: `400` = 100% of the container, `100` = 25%. A soft corner glow is `width: 100–200`; `width: 500` overshoots the container.
|
|
182
|
+
|
|
183
|
+
23. Color visibility: `*-background-weak` is nearly the page color — it is invisible as an accent. For tints and glows use alpha tokens (`brand-alpha-medium`, `brand-alpha-strong`) or solid tokens (`brand-solid-strong`) and do not stack a low `opacity` on top of an already-weak color.
|
|
184
|
+
|
|
185
|
+
## Effect units
|
|
186
|
+
|
|
187
|
+
24. `RevealFx`: `delay` is in **seconds** — stagger with `delay={index * 0.1}`, never `index * 80`. `translateY` as a number is in **rem** — use `translateY={1}` or a token like `"16"` (1rem); `16` means 16rem.
|
|
188
|
+
|
|
189
|
+
25. `LogoCloud` extends `Grid` — always set `columns` (and responsive overrides), or logos stack in one column: `columns="4" m={{ columns: 3 }} s={{ columns: 2 }}`.
|
|
190
|
+
|
|
191
|
+
26. Use decoration recipes from `recipes.md` verbatim, then adjust. Budget: at most one ambient background layer per section, ambient motion in at most the hero and final CTA, one accent color family per page.
|
|
192
|
+
|
|
193
|
+
## Motion
|
|
194
|
+
|
|
195
|
+
27. Animations are **eye-leading, not eye-grabbing**: they direct attention to the one thing that should be read next, they don't announce themselves. Small distances (`translateY="8"`–`"16"`, i.e. 0.5–1rem), short staggers (`delay={index * 0.1}` max), and only on elements worth leading the eye to — the hero heading, stat numbers, the highlighted card in a set.
|
|
196
|
+
|
|
197
|
+
28. Do **not** wrap every section in `RevealFx` — uniform entrance animation on everything reads dated and makes scrolling feel slow. Entry-animation budget per page: the hero, plus at most one or two key moments (stats, a card set). Everything else just renders.
|
|
198
|
+
|
|
199
|
+
28b. Animate **coherent blocks, all-or-none**. Within a sibling set (cards, list items), either every sibling animates with a stagger or none do — never a single item out of the set (e.g. only the highlighted pricing card). For a hero, reveal it as 1–3 stacked chunks with increasing delay (`0 / 0.1 / 0.2`), not individual text fragments. One entrance moment per viewport: if the hero animates, the cards below it don't.
|
|
200
|
+
|
|
201
|
+
29. Trigger below-the-fold animations with `useInViewport`, latched so they play once. `RevealFx` takes `trigger`; `CountFx` has no trigger prop — drive its `value` instead:
|
|
202
|
+
|
|
203
|
+
```tsx
|
|
204
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
205
|
+
const inViewport = useInViewport(ref);
|
|
206
|
+
const [seen, setSeen] = useState(false);
|
|
207
|
+
useEffect(() => {
|
|
208
|
+
if (inViewport) setSeen(true); // latch: useInViewport flips back to false on scroll-out
|
|
209
|
+
}, [inViewport]);
|
|
210
|
+
|
|
211
|
+
<Column ref={ref} gap="16">
|
|
212
|
+
<RevealFx trigger={seen} translateY="8">...</RevealFx>
|
|
213
|
+
<CountFx value={seen ? 2400 : 0} speed={1500} separator="," variant="display-strong-m" />
|
|
214
|
+
</Column>
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Without this, mount-timed animations play while still off-screen and the user scrolls into an already-finished state.
|