@teamblind-chorus/ui 1.0.0
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/LICENSE +21 -0
- package/README.md +112 -0
- package/agents/AGENTS.md +143 -0
- package/agents/DESIGN.md +1311 -0
- package/agents/LOVABLE.md +472 -0
- package/agents/anti-patterns.md +533 -0
- package/agents/catalog.md +232 -0
- package/agents/components/avatar-rail/avatar-rail.family.json +46 -0
- package/agents/components/avatar-rail/avatar-rail.md +103 -0
- package/agents/components/avatar-rail/avatar-rail.spec.json +160 -0
- package/agents/components/badge/badge.family.json +45 -0
- package/agents/components/badge/badge.md +10 -0
- package/agents/components/badge/role.md +100 -0
- package/agents/components/badge/role.spec.json +75 -0
- package/agents/components/badge/update.md +132 -0
- package/agents/components/badge/update.spec.json +114 -0
- package/agents/components/banner/banner.family.json +28 -0
- package/agents/components/banner/banner.md +136 -0
- package/agents/components/banner/banner.spec.json +136 -0
- package/agents/components/bottom-sheet/bottom-sheet.family.json +29 -0
- package/agents/components/bottom-sheet/bottom-sheet.md +176 -0
- package/agents/components/bottom-sheet/bottom-sheet.spec.json +168 -0
- package/agents/components/bubble/bubble.family.json +29 -0
- package/agents/components/bubble/bubble.md +134 -0
- package/agents/components/bubble/bubble.spec.json +91 -0
- package/agents/components/button/button.family.json +76 -0
- package/agents/components/button/button.md +31 -0
- package/agents/components/button/check.md +138 -0
- package/agents/components/button/check.spec.json +161 -0
- package/agents/components/button/fab.md +161 -0
- package/agents/components/button/fab.spec.json +106 -0
- package/agents/components/button/icon.md +141 -0
- package/agents/components/button/icon.spec.json +164 -0
- package/agents/components/button/standard.md +219 -0
- package/agents/components/button/standard.spec.json +205 -0
- package/agents/components/button/text.md +186 -0
- package/agents/components/button/text.spec.json +215 -0
- package/agents/components/button/toggle.md +108 -0
- package/agents/components/button/toggle.spec.json +124 -0
- package/agents/components/button/toolbar.md +189 -0
- package/agents/components/button/toolbar.spec.json +109 -0
- package/agents/components/carousel/carousel.family.json +41 -0
- package/agents/components/carousel/carousel.md +40 -0
- package/agents/components/carousel/post.md +148 -0
- package/agents/components/carousel/post.spec.json +229 -0
- package/agents/components/carousel/profile.md +184 -0
- package/agents/components/carousel/profile.spec.json +219 -0
- package/agents/components/chip/chip.family.json +37 -0
- package/agents/components/chip/chip.md +10 -0
- package/agents/components/chip/filter.md +212 -0
- package/agents/components/chip/filter.spec.json +124 -0
- package/agents/components/chip/tag.md +137 -0
- package/agents/components/chip/tag.spec.json +104 -0
- package/agents/components/dialog/dialog.family.json +29 -0
- package/agents/components/dialog/dialog.md +113 -0
- package/agents/components/dialog/dialog.spec.json +156 -0
- package/agents/components/directory-list/directory-list.family.json +46 -0
- package/agents/components/directory-list/directory-list.md +87 -0
- package/agents/components/directory-list/directory-list.spec.json +104 -0
- package/agents/components/divider/divider.family.json +28 -0
- package/agents/components/divider/divider.md +78 -0
- package/agents/components/divider/divider.spec.json +51 -0
- package/agents/components/feed/ad.md +108 -0
- package/agents/components/feed/ad.spec.json +187 -0
- package/agents/components/feed/feed.family.json +48 -0
- package/agents/components/feed/feed.md +30 -0
- package/agents/components/feed/post.md +240 -0
- package/agents/components/feed/post.spec.json +361 -0
- package/agents/components/form-field/form-field.family.json +50 -0
- package/agents/components/form-field/form-field.md +11 -0
- package/agents/components/form-field/input.md +198 -0
- package/agents/components/form-field/input.spec.json +202 -0
- package/agents/components/form-field/search.md +81 -0
- package/agents/components/form-field/search.spec.json +135 -0
- package/agents/components/form-field/select.md +101 -0
- package/agents/components/form-field/select.spec.json +194 -0
- package/agents/components/form-field/textarea.md +89 -0
- package/agents/components/form-field/textarea.spec.json +176 -0
- package/agents/components/header/header.family.json +43 -0
- package/agents/components/header/header.md +18 -0
- package/agents/components/header/main.md +101 -0
- package/agents/components/header/main.spec.json +117 -0
- package/agents/components/header/sub.md +129 -0
- package/agents/components/header/sub.spec.json +81 -0
- package/agents/components/list/accordion.md +183 -0
- package/agents/components/list/accordion.spec.json +201 -0
- package/agents/components/list/entry.md +280 -0
- package/agents/components/list/entry.spec.json +237 -0
- package/agents/components/list/list.family.json +75 -0
- package/agents/components/list/list.md +24 -0
- package/agents/components/list/radio.md +144 -0
- package/agents/components/list/radio.spec.json +186 -0
- package/agents/components/list/standard.md +262 -0
- package/agents/components/list/standard.spec.json +221 -0
- package/agents/components/metadata/compact.md +69 -0
- package/agents/components/metadata/compact.spec.json +69 -0
- package/agents/components/metadata/metadata.family.json +42 -0
- package/agents/components/metadata/metadata.md +26 -0
- package/agents/components/metadata/standard.md +104 -0
- package/agents/components/metadata/standard.spec.json +152 -0
- package/agents/components/nav-card/nav-card.family.json +29 -0
- package/agents/components/nav-card/nav-card.md +179 -0
- package/agents/components/nav-card/nav-card.spec.json +161 -0
- package/agents/components/nav-list/nav-list.family.json +46 -0
- package/agents/components/nav-list/nav-list.md +91 -0
- package/agents/components/nav-list/nav-list.spec.json +107 -0
- package/agents/components/navigation-bar/main.md +201 -0
- package/agents/components/navigation-bar/main.spec.json +109 -0
- package/agents/components/navigation-bar/navigation-bar.family.json +44 -0
- package/agents/components/navigation-bar/navigation-bar.md +21 -0
- package/agents/components/navigation-bar/search.md +96 -0
- package/agents/components/navigation-bar/search.spec.json +142 -0
- package/agents/components/navigation-bar/sub.md +174 -0
- package/agents/components/navigation-bar/sub.spec.json +123 -0
- package/agents/components/page-shell/page-shell.family.json +22 -0
- package/agents/components/page-shell/page-shell.md +51 -0
- package/agents/components/profile-header/profile-header.family.json +29 -0
- package/agents/components/profile-header/profile-header.md +149 -0
- package/agents/components/profile-header/profile-header.spec.json +200 -0
- package/agents/components/progress/progress.family.json +27 -0
- package/agents/components/progress/progress.md +38 -0
- package/agents/components/progress/progress.spec.json +67 -0
- package/agents/components/side-sheet/side-sheet.family.json +30 -0
- package/agents/components/side-sheet/side-sheet.md +154 -0
- package/agents/components/side-sheet/side-sheet.spec.json +109 -0
- package/agents/components/skeleton/skeleton.family.json +28 -0
- package/agents/components/skeleton/skeleton.md +123 -0
- package/agents/components/skeleton/skeleton.spec.json +73 -0
- package/agents/components/status-tag/status-tag.family.json +26 -0
- package/agents/components/status-tag/status-tag.md +114 -0
- package/agents/components/status-tag/status-tag.spec.json +69 -0
- package/agents/components/suggestion-list/suggestion-list.family.json +46 -0
- package/agents/components/suggestion-list/suggestion-list.md +91 -0
- package/agents/components/suggestion-list/suggestion-list.spec.json +178 -0
- package/agents/components/switch/switch.family.json +27 -0
- package/agents/components/switch/switch.md +114 -0
- package/agents/components/switch/switch.spec.json +123 -0
- package/agents/components/tab-bar/tab-bar.family.json +27 -0
- package/agents/components/tab-bar/tab-bar.md +178 -0
- package/agents/components/tab-bar/tab-bar.spec.json +184 -0
- package/agents/components/tabs/rounded.md +150 -0
- package/agents/components/tabs/rounded.spec.json +140 -0
- package/agents/components/tabs/segmented.md +114 -0
- package/agents/components/tabs/segmented.spec.json +100 -0
- package/agents/components/tabs/tabs.family.json +59 -0
- package/agents/components/tabs/tabs.md +18 -0
- package/agents/components/tabs/underline.md +147 -0
- package/agents/components/tabs/underline.spec.json +139 -0
- package/agents/components/thumbnail/thumbnail.family.json +28 -0
- package/agents/components/thumbnail/thumbnail.md +152 -0
- package/agents/components/thumbnail/thumbnail.spec.json +172 -0
- package/agents/components/toast/toast.family.json +28 -0
- package/agents/components/toast/toast.md +133 -0
- package/agents/components/toast/toast.spec.json +89 -0
- package/agents/components/tooltip/tooltip.family.json +29 -0
- package/agents/components/tooltip/tooltip.md +139 -0
- package/agents/components/tooltip/tooltip.spec.json +110 -0
- package/agents/compose.md +240 -0
- package/agents/icons.json +831 -0
- package/agents/images.md +66 -0
- package/agents/manifest.json +87 -0
- package/agents/patterns/README.md +59 -0
- package/agents/patterns/actions.md +50 -0
- package/agents/patterns/browsing.md +52 -0
- package/agents/patterns/communications.md +56 -0
- package/agents/patterns/layout.md +72 -0
- package/agents/patterns/modals.md +50 -0
- package/agents/patterns/visual.md +55 -0
- package/agents/reconstruct.md +55 -0
- package/agents/scoped-adoption.md +111 -0
- package/agents/tokens.usage.json +1657 -0
- package/agents/usage.json +422 -0
- package/dist/icons/index.cjs +1332 -0
- package/dist/icons/index.cjs.map +1 -0
- package/dist/icons/index.d.cts +228 -0
- package/dist/icons/index.d.ts +228 -0
- package/dist/icons/index.js +1114 -0
- package/dist/icons/index.js.map +1 -0
- package/dist/index.cjs +5905 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +896 -0
- package/dist/index.d.ts +896 -0
- package/dist/index.js +5847 -0
- package/dist/index.js.map +1 -0
- package/dist/styles.css +5765 -0
- package/eslint/README.md +79 -0
- package/eslint/index.js +78 -0
- package/eslint/rules.js +472 -0
- package/eslint/test.mjs +135 -0
- package/package.json +96 -0
- package/placeholder.png +0 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Teamblind, Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# @teamblind-chorus/ui
|
|
2
|
+
|
|
3
|
+
React component library for the Chorus design system. Ships prebuilt ESM + CJS bundles (`dist/`) and a single `styles.css`; import once and use.
|
|
4
|
+
|
|
5
|
+
The component contract (anatomy, slots, token bindings) lives in `schema/components/<family>/<sub>.spec.json` in the [Chorus monorepo](https://github.com/teamblind/chorus). This package is the React reference implementation of that contract.
|
|
6
|
+
|
|
7
|
+
> **License:** MIT. See [`LICENSE`](../../LICENSE).
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @teamblind-chorus/ui @teamblind-chorus/tokens
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Public on npmjs.org — no auth, no `.npmrc` setup required. Peer dependency: `react >= 18`.
|
|
16
|
+
|
|
17
|
+
## Setup
|
|
18
|
+
|
|
19
|
+
Two stylesheets and one font, loaded once at your app entry.
|
|
20
|
+
|
|
21
|
+
```js
|
|
22
|
+
// app entry (e.g. Next.js app/layout.tsx, Vite main.tsx)
|
|
23
|
+
import '@teamblind-chorus/tokens/tokens.css';
|
|
24
|
+
import '@teamblind-chorus/ui/styles.css';
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Then load Pretendard — the only typeface Chorus speaks. The CDN drop-in works in most setups:
|
|
28
|
+
|
|
29
|
+
```html
|
|
30
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css" />
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Dark mode
|
|
34
|
+
|
|
35
|
+
Tokens flip on the `data-theme` attribute. Set it on `<html>` (or any subtree):
|
|
36
|
+
|
|
37
|
+
```html
|
|
38
|
+
<html data-theme="dark">
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
No-script default is light; set the attribute server-side or in an inline `<script>` before paint to avoid a flash.
|
|
42
|
+
|
|
43
|
+
## Usage
|
|
44
|
+
|
|
45
|
+
```jsx
|
|
46
|
+
import { Button, Chip, Banner } from '@teamblind-chorus/ui';
|
|
47
|
+
|
|
48
|
+
export default function Example() {
|
|
49
|
+
return (
|
|
50
|
+
<>
|
|
51
|
+
<Button variant="standard" appearance="filled">Save</Button>
|
|
52
|
+
<Chip appearance="assist">Filter</Chip>
|
|
53
|
+
<Banner appearance="accent">Heads up.</Banner>
|
|
54
|
+
</>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Component-level props (`variant`, `appearance`, `size`, slots) follow the per-component spec in `schema/components/`. The docs site renders these specs as the authoritative reference.
|
|
60
|
+
|
|
61
|
+
## Use tokens directly in your own CSS
|
|
62
|
+
|
|
63
|
+
Every system token is emitted as a CSS custom property by `@teamblind-chorus/tokens/tokens.css`:
|
|
64
|
+
|
|
65
|
+
```css
|
|
66
|
+
.card {
|
|
67
|
+
background: var(--sys-color-surface);
|
|
68
|
+
color: var(--sys-color-on-surface);
|
|
69
|
+
border: 1px solid var(--sys-color-outline-variant);
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Or read the resolved JSON in build tooling:
|
|
74
|
+
|
|
75
|
+
```js
|
|
76
|
+
import lightTokens from '@teamblind-chorus/tokens/resolved.light.json' with { type: 'json' };
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Agent-friendly docs (Lovable, Cursor, Claude Code, …)
|
|
80
|
+
|
|
81
|
+
The package self-contains the docs an LLM agent needs. After `npm install`, they live under `node_modules/@teamblind-chorus/ui/agents/`:
|
|
82
|
+
|
|
83
|
+
| File | Purpose |
|
|
84
|
+
| :--- | :--- |
|
|
85
|
+
| `agents/LOVABLE.md` | Lovable system-prompt source — paste §1 once per session, §2 per task. |
|
|
86
|
+
| `agents/AGENTS.md` | Hard contract every Chorus-aware agent obeys. |
|
|
87
|
+
| `agents/catalog.md` | Intent → component map. |
|
|
88
|
+
| `agents/manifest.json` | Component inventory. |
|
|
89
|
+
| `agents/DESIGN.md` | Token model, four guiding principles, authorized literal exceptions. |
|
|
90
|
+
|
|
91
|
+
Also shipped: `@teamblind-chorus/ui/placeholder.png` — copy once into your app's `public/` and reference as `src="/placeholder.png"`.
|
|
92
|
+
|
|
93
|
+
## Native sibling packages (pilot)
|
|
94
|
+
|
|
95
|
+
The same tokens are generated into Swift and Kotlin sources. UI implementations are early — Button (full spec) and Chip (filter + tag) ported so far:
|
|
96
|
+
|
|
97
|
+
- **iOS (SwiftUI):** [`@teamblind-chorus/tokens-ios`](https://github.com/teamblind/chorus/tree/main/packages/tokens-ios) (`ChorusTokens`) + [`@teamblind-chorus/ui-ios`](https://github.com/teamblind/chorus/tree/main/packages/ui-ios) (`ChorusUI`).
|
|
98
|
+
- **Android (Compose):** [`@teamblind-chorus/tokens-android`](https://github.com/teamblind/chorus/tree/main/packages/tokens-android) (`chorus-tokens`) + [`@teamblind-chorus/ui-android`](https://github.com/teamblind/chorus/tree/main/packages/ui-android) (`chorus-ui`).
|
|
99
|
+
|
|
100
|
+
CI runs `node packages/tokens-{ios,android}/scripts/check.mjs` and fails if `schema/tokens/*.json` changed without the matching generated Swift/Kotlin sources being regenerated — keeping web, iOS, and Android in lockstep.
|
|
101
|
+
|
|
102
|
+
## Upgrading
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
npm update @teamblind-chorus/ui @teamblind-chorus/tokens
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Changelogs are published with each release on the [GitHub Releases](https://github.com/teamblind/chorus/releases) page of `teamblind/chorus` and inside each package's `CHANGELOG.md`.
|
|
109
|
+
|
|
110
|
+
## Versioning
|
|
111
|
+
|
|
112
|
+
Semver. Breaking changes to component props, slot names, or required CSS variables bump the major version. Token name changes are coordinated with `@teamblind-chorus/tokens`.
|
package/agents/AGENTS.md
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
Entrypoint for AI agents, external renderers, and design tools (Lovable, v0, Cursor, Figma plugins, Claude Design) consuming Chorus to compose prototypes. Human-facing readme: [`README.md`](README.md). This file is the machine contract.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Design principles
|
|
8
|
+
|
|
9
|
+
Five directives. Apply in order; later directives never override earlier ones.
|
|
10
|
+
|
|
11
|
+
1. **Design-system-first (Source of Truth).** Chorus is the source of truth for every surface. Start from Chorus tokens, components, and patterns — not generic libraries, screenshot inference, or invented values. Begin every task by reading [`schema/manifest.json`](schema/manifest.json) + [`schema/catalog.md`](schema/catalog.md).
|
|
12
|
+
2. **Component flexibility — extrapolate, don't fork.** Read the intent and respect each component's anatomy invariants (slot grammar, sizing tokens, state contract), but flex composition (slot fill, layout placement, modifier props) to fit context. The contract is the token bindings and spec slot rules, not the example screenshot. The family's `visualReuse` flag in `<family>.family.json` says how far flexibility extends — `"open"` families (section, banner, feed, list, button, chip, badge, navigation-bar, tab-bar, tabs, suggestion-list, avatar-rail, thumbnail) may be picked **on visual-fit grounds even when the brief's intent does not match `useCases` verbatim**; `"locked"` families (dialog, bottom-sheet, toast, tooltip, form-field — interaction-contract primitives) MUST only be used in their canonical role. Never wrap a Chorus component to restyle — re-compose with the slots it already gives you.
|
|
13
|
+
3. **New surfaces stay token-true.** When Chorus has no component for what the surface needs, design a new screen or primitive. Every color, spacing, typography, radius, and border-width MUST resolve through Chorus tokens and the foundations in [`schema/DESIGN.md`](schema/DESIGN.md). No raw hex, no off-scale px, no third-party type ramp — regardless of novelty.
|
|
14
|
+
4. **Lego-block composition.** Combine and extend existing components Lego-style — nest, group, sequence, re-purpose. Tokens stay non-negotiable; components are the flexible part. A novel screen should still read as one harmony with the system.
|
|
15
|
+
5. **UX-pattern consistency.** Pick components by expected interaction when interaction is the point — Dialog for modal commits, BottomSheet for committed-sheet flows, Toast for non-blocking feedback, Tooltip for trigger-anchored hints, FormField for real text entry. These five (`visualReuse: "locked"`) MUST NOT be borrowed for shape alone — focus trap, auto-dismiss, ARIA live region, hover/focus trigger, `<input>` semantics are the contract. The other thirteen (`"open"`) carry interaction defaults too — List for menus/pickers, Feed for authored content, Chip vs Button for facet vs commit — but defaults are *suggestions*, not rules; pick by visual fit when the design calls for it. Across a flow, keep behavior/motion/affordance predictable regardless of tier.
|
|
16
|
+
|
|
17
|
+
The "Hard rules" below are the machine-checkable carve-outs — principles tell you *how to think*; hard rules tell you *what not to ship*.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## MCP server (preferred access)
|
|
22
|
+
|
|
23
|
+
MCP server at [`apps/mcp-server`](apps/mcp-server) exposes the schema to any MCP client (Claude Desktop, Cursor, Claude Code, Lovable). Tools enumerate families/specs and validate compositions; resources expose `AGENTS.md`, the manifest, the catalog, `DESIGN.md`, and resolved token bundles. Wiring: [`apps/mcp-server/README.md`](apps/mcp-server/README.md). Fall back to direct file reads in the order below if MCP is unavailable.
|
|
24
|
+
|
|
25
|
+
## Read order (do not skip)
|
|
26
|
+
|
|
27
|
+
1. **[`schema/manifest.json`](schema/manifest.json)** — single entry point. Every family, sub-component, resolved-token bundle, JSON Schema. Never crawl `schema/components/` to infer system shape.
|
|
28
|
+
2. **[`schema/catalog.md`](schema/catalog.md)** — intent → component map. Pick components before reading specs.
|
|
29
|
+
3. **`schema/components/<family>/<sub>.spec.json`** — per-sub-component contract: props, slots, sizes, appearances, states, behavior.
|
|
30
|
+
4. **[`schema/tokens/resolved.light.json`](schema/tokens/resolved.light.json)** / **[`resolved.dark.json`](schema/tokens/resolved.dark.json)** — flat `path → { $value, $type }` for rendering. Fall back to `reference.json` + `system.json` only for the dependency graph. `resolved.web.*.json` are sparse `≥800px` overlays.
|
|
31
|
+
5. **[`schema/DESIGN.md`](schema/DESIGN.md)** — cross-cutting rules every spec defers to (color quartets, focus rings, no-layout strokes, state overlays). Keep loaded.
|
|
32
|
+
6. **[`patterns/`](patterns)** — canonical pixel references as function-grouped image boards (`actions`, `browsing`, `communications`, `layout`, `modals`, `visual`) + notes. Consult when matching a new composition to a signed-off visual target.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Hard rules
|
|
37
|
+
|
|
38
|
+
Not expressible in JSON Schemas. Agents MUST encode as guardrails.
|
|
39
|
+
|
|
40
|
+
1. **Token-only color, type, space, radius.** Every visible value resolves to a `sys.*` (or `ref.*` if no semantic alias) from `resolved.*.json`. No literal hex, no `color-mix()`, no magic numbers. Exception: exact pixel values already in a spec's `sizing` block (e.g. `"160px"` minWidth on `standard`).
|
|
41
|
+
2. **Color pairs travel together.** `sys.color.<role>Container` foreground MUST be `sys.color.on<Role>Container`. Retune the token; never split the pair. See [memory: token pairs].
|
|
42
|
+
3. **No-layout strokes & focus rings.** Edge strokes are inset `box-shadow`, never `border:`. Focus rings are `::after` overlays. State/focus MUST NOT reflow the control. Always `box-sizing: border-box`. **Cards hosting a full-bleed child with its own opaque fill (cover band, hero thumbnail, header band under `overflow: hidden`) promote the outline stroke to the same `::after` overlay layer the focus ring uses** — an inset shadow on the card paints below the child and gets masked at edges the child touches. See [`schema/DESIGN.md`](schema/DESIGN.md).
|
|
43
|
+
4. **Slot contract is closed.** `slots` enumerates every region. Do not introduce undeclared slots. Respect `slotCompatibility` when present.
|
|
44
|
+
5. **States compose via overlay, not replacement.** Hover/pressed/focused/disabled overlay the resolved `appearance`. Use `DESIGN.md`'s recipe; do not invent per-component recipes.
|
|
45
|
+
6. **One geometry across a navigation flow.** Bar/row/chrome heights are stable across screens (e.g. all three `navigation-bar` subs share `56px` min-height, `8/8` padding).
|
|
46
|
+
7. **Preview/demo strings are English.** Even when source screenshots are Korean.
|
|
47
|
+
8. **Link-affordance Text Buttons use `accent`.** Section-header `See all`, card-header `Follow`, inline `View details` — any Text Button reading as a link picks `appearance="accent"` so navigational intent carries chromatic emphasis. `default` is reserved for inline commits that should recede into body copy. Full rule: [text.spec.json:appearances.accent.linkAffordanceRecommendation](schema/components/button/text.spec.json).
|
|
48
|
+
9. **Image areas are image-typed and context-swappable — one universal placeholder.** Any spec field marked `"assetType": "image"` (`Thumbnail.src`/`Thumbnail.image`, `FeedAd.media.src`/`FeedAd.media`, `ProfileCarousel.items[].cover.src`, every future image-typed slot) is a typed asset slot. Fill with `/placeholder.png` — the **single universal Chorus image-area placeholder** — when scaffolding; the same asset wires as the **runtime CSS fallback** (`background-image` on the image-area layer) so a failed `<img>` still resolves to the placeholder rather than an empty surface tone. **The path `/placeholder.png` itself is the canonical contract**; consuming hosts MUST serve the bundled asset (`node_modules/@teamblind-chorus/ui/placeholder.png` — copied to the host's public root, renamed verbatim, never to `placeholder.png` truncated of context, never to `placeholder_thumbnail.png`, never to a renamed variant). The dataURL inlined as `--chorus-placeholder-image` in [`packages/ui/src/styles.css`](packages/ui/src/styles.css) is a runtime CSS-only safety net for `<img>` load failures — external renderers that don't load `styles.css` (Lovable previews, Figma plugins, headless screen-shotters) still resolve the slot via the served path, so omitting the public asset breaks the contract even when the dataURL is present. The placeholder paints the lowercase Blind wordmark on a calm neutral surface, `object-fit: cover` -ed to the slot's intrinsic footprint — aspect ratio preserved, cropped to fill — never reshaping the slot. When the composition gives a clear subject (named channel, known author, brand, topic), swap the placeholder for a context-appropriate image — only the `src` URL changes, never the slot's footprint or chrome. **When no context-appropriate image can be inferred, fall back to `/placeholder.png`** rather than a glyph-in-tinted-circle, inline SVG wordmark, empty `src`, or invented stock URL. Glyph-bearing slots (e.g. `Banner.icon`) are a different contract.
|
|
49
|
+
10. **Icons render as SVG glyph components, never text characters.** Any icon affordance — leading / trailing on a Button, the icon slot on Banner / Status tag / Navigation bar / Header / List row, an inline glyph beside a label — MUST be a Chorus icon component imported from `@teamblind-chorus/ui/icons` (`PlusIcon`, `XIcon`, `CheckIcon`, `ChevronRightIcon`, the entity-family icons, …). Do NOT type ASCII / Unicode characters in label strings or `aria-label`s to stand in for an icon (`'+ Create'`, `'× Close'`, `'→ Continue'`, `'★ Favorite'`, `'⌃'`, `'⌄'`, `'•'`, `'·'`). Text characters bypass the family-wide `currentColor` re-tone ([button.family.json:iconColorNote](schema/components/button/button.family.json)), the per-rung sizing (`icon.md` 16 / `icon.lg` 20 / `icon.xl` 24), the `aria-hidden` decorative contract, and the keyword-driven swap-by-intent map at [`schema/icons/icons.json`](schema/icons/icons.json) — so they read as visible glyphs to screen readers, paint in the wrong colour under hover / disabled / destructive flavors, and drift off the icon rung. Header's `headerAction` accepts `leadingIcon` / `trailingIcon` node slots for exactly this case; reach for them instead of prefixing the label with `+`. The single carve-out is prose that mentions a glyph as a *concept* (fab.md: "universally legible actions (`+`, pencil)") — the rendered FAB itself still fills its `icon` slot with `<PlusIcon>`, never a `+` character.
|
|
50
|
+
11. **Readability is non-negotiable — pair contrast, don't compose it.** Foreground colour for every text run, icon glyph, and graphic boundary MUST resolve to the host surface's pre-paired `on*` token. The pairings ship in `sys.color`: `surface*` ↔ `onSurface` / `onSurfaceVariant`; `primary` ↔ `onPrimary`; `primaryContainer` ↔ `onPrimaryContainer`; same shape for `secondary` / `brand` / `success` / `error` and their `*Container` siblings; `inverseSurface` ↔ `inverseOnSurface`; the dedicated `sys.color.icon.*` palette is tuned for **neutral `surface*` hosts only**. NEVER cross-pair (`onPrimary` on `surface`, `onSurface` on `primary`, a `sys.color.icon.*` paint on a `primary` / `error` / `brand` / `*Container` fill, dark text in an `inverseSurface` chip). When a new surface needs a tint with no pre-paired foreground, the agent MUST (a) compute the WCAG contrast ratio against the actual fill in BOTH light and dark mode; (b) refuse to ship anything below **4.5 : 1 for normal text, 3 : 1 for ≥18pt or Semibold ≥14pt text, 3 : 1 for non-text glyphs and graphic boundaries**; (c) if the chosen pair fails, fall back to the nearest `sys.color` quartet rather than searching for a darker shade. The most common failures the agent MUST refuse: (i) `onSurface` (`neutral.900` light / `neutral.50` dark) text composed onto a non-`surface*` fill — produces near-invisible "black on dark-blue" / "white on light-yellow" results; (ii) `sys.color.icon.muted` (translucent 20 % black / 24 % white) on a colour-tinted host — the alpha mixes against the tint, not the surface, and usually collapses below 3 : 1; (iii) inventing a new fill colour for a hero / banner without re-checking that the existing `onSurface` text still clears AA against the new fill in both themes. When you cannot guarantee AA, **the right answer is to pick a different host fill from the existing surface ladder**, not to hand-tune the foreground.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Composing a new screen
|
|
55
|
+
|
|
56
|
+
1. For each slot, pick a family from `catalog.md` and a sub from the family's `family.json`.
|
|
57
|
+
2. Swap families inside slots — only to families in the slot's `accepts` list.
|
|
58
|
+
3. Re-resolve tokens for active theme.
|
|
59
|
+
4. Cross-check the rules below.
|
|
60
|
+
|
|
61
|
+
### Composition rules
|
|
62
|
+
|
|
63
|
+
- **NavigationBar at top.** `home` for landing, `page` for drill-ins, `search` for search. Never stack two.
|
|
64
|
+
- **BottomSheet and Dialog are modal.** Never inline. Pair each with a triggering action.
|
|
65
|
+
- **FAB is the single canonical commit.** ≤1 per screen. Destructive primary commits → Dialog, not FAB.
|
|
66
|
+
- **List rows are the click target.** Leading controls (radio, thumbnail) are not separate hit targets.
|
|
67
|
+
- **AvatarRail = horizontal entity quick-nav (avatar + label). SuggestionList = vertical swipeable block of follow-suggestion rows.** Not interchangeable.
|
|
68
|
+
- **Feed vs List.** Feed = authored content streams (author + body + footer). List = menus/settings/pickers.
|
|
69
|
+
- **One shared horizontal gutter — never double-pay padding.** The page shell pays `layout.page.*` (default `layout.page.md`) once; full-bleed components (Carousel, ListRow, NavigationBar, Feed item, Banner, AvatarRail, ProfileHeader, Chip group, …) stretch edge-to-edge inside it and MUST NOT also wrap in another `layout.container.md` of horizontal padding. Stacking `layout.page.md` + `layout.container.md` makes each component a different inset from the page edge, so heading text, list content, and chip rows fail to align down a shared left rail. The contract is the *visual* `layout.page.md` from the screen edge (DESIGN.md → [Section horizontal padding](schema/DESIGN.md)) — pay it once, at the page shell.
|
|
70
|
+
- **The one-inset rule applies recursively in two directions.**
|
|
71
|
+
- **(a) Bounded-surface hosts a full-bleed child → negative-margin opt-out.** Bounded surfaces (Card, Dialog, BottomSheet content slot, Sheet body, padded Section interior) own their own `layout.container.*` padding for direct content (titles, body, form fields, primary action). But when a *full-bleed* component (List, Feed, AvatarRail, Chip group, Tabs rail) is placed inside one, the SAME double-pay collision happens: surface padding + child's row padding = two insets stacked, row content lands further from the card edge than the surface's other content. The full-bleed child MUST stretch to the surface's content-box edge so its OWN internal padding becomes the visual inset. Canonical idiom: `style={{ marginInline: 'calc(-1 * var(--sys-layout-container-md))', width: 'calc(100% + 2 * var(--sys-layout-container-md))', maxWidth: 'none' }}`. Visual check: row content (radio glyph, list-item primary text, chip group's first chip, feed-item author block) lands on the same vertical line as the surface's titles, body, and action labels. Precedent: `bottom-sheet/overflow` and `bottom-sheet/nested-step`.
|
|
72
|
+
- **(b) Full-bleed host with its own chrome hosts another chrome-bearing full-bleed child → embedded mode.** A full-bleed surface that pays its own `background` + `padding` (`<Carousel>`, `<Feed>`) hosting another chrome-bearing full-bleed family (`AvatarRail`, `SuggestionList`, `Tabs`, `List`) re-pays both — gutter is double-paid (rail items inset further than the host header) and background paints over background (subtle but real on raised-surface dark themes). The child MUST enter **embedded mode**: declare `embedded={true}` on the child (`<Carousel label="Shortcuts"><AvatarRail embedded items={…} /></Carousel>`); the component renders `data-embedded="true"` and zeroes `background` + `padding-inline` + `padding-block`. A `:where(.chorus-carousel, .chorus-feed) > :where(.chorus-avatar-rail, .chorus-suggestion-list, .chorus-tabs, .chorus-list)` ancestry rule in `packages/ui/src/styles.css` also catches the case where the caller omits the prop. Eligible: `AvatarRail`, `SuggestionList`, `Tabs`, `List`. NOT eligible (kept standalone always): `Banner` (its tinted block is identity, not chrome), `NavigationBar`, `TabBar`, `NavCard`, and the hosts themselves (`Carousel`, `Feed`).
|
|
73
|
+
- **Group for visual alignment, not component packaging.** Grouping `<div>`s enforce a single shared left/right rail across siblings — not to wrap each component in its own bounding box that re-applies the component's `layout.container.*`. Two siblings (section header + list, chip rail + feed) belonging to the same region share one parent that owns the horizontal inset; each child stretches to that parent's content box. Same rule vertically: pay it once via `gap: var(--sys-layout-stack-*)` on the parent, not `margin` on each child.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Renderer guidance
|
|
78
|
+
|
|
79
|
+
`@teamblind-chorus/ui` is workspace-only and source-distributed. External tools pick ONE:
|
|
80
|
+
|
|
81
|
+
- **(a) Compile JSX directly.** Import from `packages/ui/src/index.js`. Load `packages/ui/src/styles.css` once. JSX emits inline `--<component>-*` plumbing vars; static rules in `styles.css` consume them.
|
|
82
|
+
- **(b) Re-render from the spec.** Read `schema/components/<family>/<sub>.spec.json` and emit equivalent. Path for Figma plugins, Lovable-style synth, non-React runtimes. MUST encode all seven hard rules.
|
|
83
|
+
|
|
84
|
+
Prefer (a) — carries no-layout-stroke and focus-ring rules for free.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Validators
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
npm run lint # everything below
|
|
92
|
+
npm run lint:tokens # token usage + hex collisions (informational by default)
|
|
93
|
+
npm run lint:layout-inset # family layoutInset catalog audit
|
|
94
|
+
node schema/lint/validate-tokens.mjs --strict # fail on cross-family hex collisions
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Checked:
|
|
98
|
+
|
|
99
|
+
**Tokens** ([`validate-tokens.mjs`](schema/lint/validate-tokens.mjs)):
|
|
100
|
+
1. **Token usage.** Every `sys.*`/`ref.*`/`comp.*` in `schema/components/*.spec.json` and `packages/ui/src/**/*.jsx` resolves in `resolved.<theme>.json`. Hard fail.
|
|
101
|
+
2. **Hex collisions.** Same-theme `sys.color.*` keys resolving to the same hex are reported. Cross-family collisions print informationally; `--strict` fails.
|
|
102
|
+
|
|
103
|
+
**Layout inset** ([`validate-layout-inset.mjs`](schema/lint/validate-layout-inset.mjs)):
|
|
104
|
+
3. **Catalog audit.** Every family in `manifest.json` declares `layoutInset` (`full-bleed` / `bounded-surface` / `inline`). Hard fail.
|
|
105
|
+
|
|
106
|
+
Not yet automated:
|
|
107
|
+
|
|
108
|
+
4. **Visual regression.** Composition renders under both `resolved.light` and `resolved.dark`. Use `npm run dev` for spot-checks.
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Not covered (yet)
|
|
113
|
+
|
|
114
|
+
- **Layout primitives** (Stack / Inset / SafeArea) — not componentized. Use `sys.layout.*` on a `<div>`. Atomic spacing values live at `ref.space.*`; semantic spacing roles live at `sys.layout.{container,stack,inline,page}.*`. There is no `sys.space.*` tier. Do not invent. (`Divider` IS componentized — see `schema/components/divider`.)
|
|
115
|
+
- **Motion tokens** — not in system. Specs use CSS defaults (timing functions and durations) directly; there is no `sys.motion.*` tier. Do not invent.
|
|
116
|
+
- **Form validation, toasts, snackbars, tooltips, menus (non-bottom-sheet), date pickers** — not in system. Do not synthesize. Ask the user.
|
|
117
|
+
- **`comp.*` token tier is intentionally empty.** Do not write to it.
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Commit conventions
|
|
122
|
+
|
|
123
|
+
Conventional Commits. Format: `type(scope): subject`.
|
|
124
|
+
|
|
125
|
+
- **type** — `feat` (new capability), `fix` (bug), `docs` (docs only), `chore` (deps/tooling/rename), `ci` (workflows), `refactor` (no behavior change), `test`.
|
|
126
|
+
- **scope** — the touched surface. Prefer existing scopes from `git log`: `badge`, `toast`, `tokens`, `components`, `docs`, `pages`, `release`. Multi-scope: comma, no space — `feat(toast,tokens): …`.
|
|
127
|
+
- **subject** — imperative, lowercase, no trailing period, ≤72 chars. Describe *what changes*, not *why*. Example: `feat(badge): host dot on thumbnail/icon with surface outline`.
|
|
128
|
+
- **body** (optional) — one blank line after subject. Use for the *why* or non-obvious constraints. Wrap at ~72.
|
|
129
|
+
- **one logical change per commit.** Token retunes and spec edits that travel together stay together; unrelated cleanup goes in its own commit.
|
|
130
|
+
- **schema/spec/token edits** — name the family or token tier in the scope (`feat(button): …`, `chore(tokens): …`). Re-run `npm run lint` before committing — a failing validator is not a commit.
|
|
131
|
+
- **never commit** generated `apps/docs/out/`, `node_modules/`, or local `claude-memory` symlink contents.
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Glossary
|
|
136
|
+
|
|
137
|
+
- **family** — group of related sub-components sharing one anatomy (`button`, `tabs`, `navigation-bar`).
|
|
138
|
+
- **sub-component** — concrete form within a family (`standard`, `underline-tabs`, `home-navigation-bar`).
|
|
139
|
+
- **spec** — `<sub>.spec.json`: machine-readable contract for one sub-component.
|
|
140
|
+
- **family.json** — family-level index: sub-components + family-wide axes (e.g. `flavor: ["default", "destructive"]`).
|
|
141
|
+
- **resolved tokens** — `tokens/resolved.<theme>.json`: flat, fully-dereferenced bundles.
|
|
142
|
+
|
|
143
|
+
[memory: token pairs]: claude-memory/feedback_token_pairs.md
|