@teamblind-chorus/ui 1.1.0 β†’ 1.2.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.
Files changed (46) hide show
  1. package/agents/catalog.md +6 -4
  2. package/agents/components/avatar-rail/avatar-rail.spec.json +19 -0
  3. package/agents/components/banner/banner.family.json +3 -1
  4. package/agents/components/banner/banner.md +54 -1
  5. package/agents/components/banner/banner.spec.json +24 -1
  6. package/agents/components/button/check.spec.json +19 -0
  7. package/agents/components/button/fab.spec.json +19 -0
  8. package/agents/components/button/icon.spec.json +19 -0
  9. package/agents/components/button/standard.spec.json +19 -0
  10. package/agents/components/button/text.spec.json +19 -0
  11. package/agents/components/button/toggle.spec.json +19 -0
  12. package/agents/components/chip/filter.spec.json +19 -0
  13. package/agents/components/chip/tag.spec.json +19 -0
  14. package/agents/components/empty-state/empty-state.family.json +28 -0
  15. package/agents/components/empty-state/empty-state.md +69 -0
  16. package/agents/components/empty-state/empty-state.spec.json +87 -0
  17. package/agents/components/form-field/input.spec.json +8 -1
  18. package/agents/components/form-field/search.spec.json +8 -1
  19. package/agents/components/form-field/select.spec.json +9 -1
  20. package/agents/components/form-field/textarea.spec.json +8 -1
  21. package/agents/components/list/accordion.spec.json +9 -0
  22. package/agents/components/list/entry.spec.json +19 -0
  23. package/agents/components/list/radio.spec.json +19 -0
  24. package/agents/components/list/standard.md +46 -0
  25. package/agents/components/list/standard.spec.json +37 -2
  26. package/agents/components/nav-card/nav-card.spec.json +9 -0
  27. package/agents/components/page-shell/page-shell.family.json +1 -1
  28. package/agents/components/page-shell/page-shell.md +33 -0
  29. package/agents/components/page-shell/page-shell.spec.json +85 -0
  30. package/agents/components/spinner/spinner.family.json +27 -0
  31. package/agents/components/spinner/spinner.md +98 -0
  32. package/agents/components/spinner/spinner.spec.json +82 -0
  33. package/agents/components/switch/switch.spec.json +9 -0
  34. package/agents/components/tab-bar/tab-bar.spec.json +16 -0
  35. package/agents/components/tabs/rounded.spec.json +19 -0
  36. package/agents/components/tabs/underline.spec.json +19 -0
  37. package/agents/manifest.json +8 -6
  38. package/agents/usage.json +12 -0
  39. package/dist/index.cjs +340 -60
  40. package/dist/index.cjs.map +1 -1
  41. package/dist/index.d.cts +46 -2
  42. package/dist/index.d.ts +46 -2
  43. package/dist/index.js +339 -61
  44. package/dist/index.js.map +1 -1
  45. package/dist/styles.css +182 -0
  46. package/package.json +1 -1
@@ -0,0 +1,98 @@
1
+ # Spinner
2
+
3
+ > πŸ‡°πŸ‡· ν•œκ΅­μ–΄: [`i18n/ko/schema/components/spinner/spinner.md`](../../../i18n/ko/schema/components/spinner/spinner.md)
4
+
5
+ An indeterminate loading indicator β€” a rotating arc in `sys.color.primary` over a faint `scrimSubtle` ring that signals a short, progress-unknown wait on a neutral host surface. Two rungs ride the `icon.*` ladder β€” `medium` (24px, default) and `small` (16px). Pass a `label` for a single line of loading copy beside the arc.
6
+
7
+ **Reach for this when** a wait is brief and indeterminate (under ~1 second) β€” a button submit, an inline action, or a first-paint loader before a screen's data resolves β€” and a content-shaped placeholder would be heavier than the wait itself. **Skip when** the wait mirrors a known shape (use [Skeleton](../skeleton/skeleton.md)), the task has a measurable ratio (use [Progress](../progress/progress.md)), or the surface already shows another Spinner β€” reserve one per view.
8
+
9
+ **Layout inset.** `inline` β€” Spinner ships no padding or container chrome of its own. It sits as a leaf at the size of its rung; the host owns centering and surrounding rhythm. An optional label sits beside the arc with `sys.layout.inline.sm` between them.
10
+
11
+ ## Default
12
+
13
+ A 24px rotating arc. `role='status'` announces the loading state; `aria-label` defaults to `'Loading'`.
14
+
15
+ ```preview
16
+ spinner/default
17
+ ---
18
+ import { Spinner } from '@teamblind-chorus/ui';
19
+
20
+ <Spinner aria-label="Loading" />
21
+ ```
22
+
23
+ ## Use cases
24
+
25
+ ### With label
26
+
27
+ A single line of loading copy beside the arc. The label doubles as the accessible name, so `aria-label` is not needed.
28
+
29
+ ```preview
30
+ spinner/with-label
31
+ ---
32
+ import { Spinner } from '@teamblind-chorus/ui';
33
+
34
+ <Spinner label="Loading…" />
35
+ ```
36
+
37
+ ### Small
38
+
39
+ The 16px rung β€” for tight inline affordances (a button label, a form-field affix) where the 24px default would crowd.
40
+
41
+ ```preview
42
+ spinner/small
43
+ ---
44
+ import { Spinner } from '@teamblind-chorus/ui';
45
+
46
+ <Spinner size="small" aria-label="Loading" />
47
+ ```
48
+
49
+ ### Centered
50
+
51
+ A first-paint loader centered inside the surface that will hold the data. One Spinner per view.
52
+
53
+ ```preview
54
+ spinner/centered
55
+ ---
56
+ import { Spinner } from '@teamblind-chorus/ui';
57
+
58
+ <div style={{ display: 'flex', justifyContent: 'center', padding: 'var(--sys-layout-container-xl)' }}>
59
+ <Spinner label="Loading…" />
60
+ </div>
61
+ ```
62
+
63
+ ## Slots
64
+
65
+ - **container** β€” inline-flex wrapper. Carries `role='status'` and the accessible name; holds the arc and the optional label, separated by `sys.layout.inline.sm`.
66
+ - **arc** β€” the rotating ring. `sys.color.primary` foreground arc over a `sys.color.scrimSubtle` track ring, fully rounded, no stroke. Carries the spin animation. Decorative (`aria-hidden`).
67
+ - **label** β€” optional single line of loading copy beside the arc. `sys.typo.body.sm`, `sys.color.onSurfaceVariant`.
68
+
69
+ ## Anatomy
70
+
71
+ | Slot | Token bindings |
72
+ |------------|----------------|
73
+ | container | inline-flex, `sys.layout.inline.sm` gap between arc and label |
74
+ | arc | `sys.color.primary` foreground arc, `sys.color.scrimSubtle` track ring, `sys.radius.full`, no stroke |
75
+ | medium | `sys.icon.lg` (24px) diameter β€” the default rung |
76
+ | small | `sys.icon.md` (16px) diameter |
77
+ | label | `sys.typo.body.sm`, `sys.color.onSurfaceVariant` |
78
+
79
+ ## Sizes
80
+
81
+ | Size | Diameter | When to reach |
82
+ |----------|---------------------|---------------|
83
+ | `medium` | `sys.icon.lg` (24px) | **Default**. Standalone or centered first-paint loaders. |
84
+ | `small` | `sys.icon.md` (16px) | Tight inline affordances β€” button labels, form-field affixes. |
85
+
86
+ ## States
87
+
88
+ | State | Animation | Notes |
89
+ |----------------|---------------------------------|-------|
90
+ | `default` | `rotate 0.8s linear infinite` | The arc spins continuously. Indeterminate β€” the rotation never reflects a value. |
91
+ | reduced-motion | suppressed | Under `prefers-reduced-motion: reduce` the spin halts and the full ring shows statically. |
92
+
93
+ ## Behavior
94
+
95
+ - **Indeterminate only.** The rotation never maps to a ratio β€” for a known percentage use [Progress](../progress/progress.md).
96
+ - **One per view.** Reserve a single Spinner per screen; concurrent regional waits use [Skeleton](../skeleton/skeleton.md) or lift to one screen-level Spinner.
97
+ - **Accessible name.** `role='status'` announces loading without interrupting focus. A visible `label` doubles as the name; otherwise `aria-label` (default `'Loading'`) carries it. The arc is decorative.
98
+ - **Reduced motion.** Under `prefers-reduced-motion: reduce` the spin is suppressed; the full ring shows statically as a quiet loading mark.
@@ -0,0 +1,82 @@
1
+ {
2
+ "$schema": "../../spec.schema.json",
3
+ "name": "Spinner",
4
+ "family": "spinner",
5
+ "description": "A rotating arc that signals an indeterminate, sub-second wait on a neutral host surface. The arc paints in `sys.color.primary` as the foreground motion and spins continuously over a faint `sys.color.scrimSubtle` ring so the rotation reads on any surface tier. Two rungs ride the `icon.*` ladder β€” `medium` (`sys.icon.lg` / 24px, default) and `small` (`sys.icon.md` / 16px). An optional `label` slot places a single line of loading copy beside the arc. `role='status'` + an accessible name (`aria-label`, default `'Loading'`) announce the loading state. Indeterminate only β€” for a known ratio use Progress; for content-shaped waits use Skeleton.",
6
+ "element": "span",
7
+ "props": {
8
+ "size": {
9
+ "type": "enum",
10
+ "values": ["medium", "small"],
11
+ "default": "medium",
12
+ "description": "Selects the arc diameter off the `icon.*` ladder. `medium` paints at `sys.icon.lg` (24px); `small` at `sys.icon.md` (16px)."
13
+ },
14
+ "label": {
15
+ "type": "node",
16
+ "optional": true,
17
+ "description": "Optional loading copy rendered beside the arc in `sys.typo.body.sm` / `sys.color.onSurfaceVariant`. When present it also supplies the accessible name, so `aria-label` is not required."
18
+ },
19
+ "aria-label": {
20
+ "type": "string",
21
+ "optional": true,
22
+ "description": "Accessible label announced by screen readers. Defaults to `'Loading'`. Supply a more specific name (e.g. `'Signing in'`) when the wait scope is meaningful. Redundant when a visible `label` is passed."
23
+ }
24
+ },
25
+ "slots": {
26
+ "container": {
27
+ "required": true,
28
+ "description": "Inline-flex wrapper carrying `role='status'` and the accessible name. Holds the arc and the optional label, separated by `sys.layout.inline.sm`.",
29
+ "intrinsic": true
30
+ },
31
+ "arc": {
32
+ "required": true,
33
+ "description": "The rotating ring. `sys.color.primary` foreground arc over a `sys.color.scrimSubtle` track ring, fully rounded, no stroke border. Carries the continuous spin animation. Decorative β€” `aria-hidden`.",
34
+ "intrinsic": true
35
+ },
36
+ "label": {
37
+ "required": false,
38
+ "description": "Optional single line of loading copy beside the arc. `sys.typo.body.sm`, `sys.color.onSurfaceVariant`.",
39
+ "omittedBehavior": "collapse"
40
+ }
41
+ },
42
+ "sizes": {
43
+ "medium": {
44
+ "diameter": "sys.icon.lg",
45
+ "labelTypo": "sys.typo.body.sm",
46
+ "gap": "sys.layout.inline.sm"
47
+ },
48
+ "small": {
49
+ "diameter": "sys.icon.md",
50
+ "labelTypo": "sys.typo.body.sm",
51
+ "gap": "sys.layout.inline.sm"
52
+ }
53
+ },
54
+ "appearances": {
55
+ "default": {
56
+ "arc": "sys.color.primary",
57
+ "track": "sys.color.scrimSubtle",
58
+ "label": "sys.color.onSurfaceVariant",
59
+ "note": "The only appearance. The foreground arc paints `sys.color.primary` for the motion accent; the track ring paints `sys.color.scrimSubtle` (a faint inverse-tone scrim β€” ~8% black light / ~8% white dark) so the rotation reads on any host surface tier. Spinner has no emphasis axis β€” for a higher-attention loading mark the emphasis belongs in adjacent copy, not in a chromatic arc swap."
60
+ }
61
+ },
62
+ "states": {
63
+ "default": {
64
+ "animation": "chorus-spinner-rotate 0.8s linear infinite",
65
+ "note": "The arc rotates continuously at 0.8s per turn (well below the WCAG flash threshold β€” rotation modulates position, not luminance). Indeterminate: the rotation never reflects a value. The animation respects `prefers-reduced-motion: reduce` β€” the spin halts and the full ring is shown statically as a quiet loading mark."
66
+ }
67
+ },
68
+ "behavior": {
69
+ "ariaStatus": "Container carries `role='status'` and the accessible name (visible `label` or `aria-label`, default `'Loading'`) so screen readers announce the loading state without yanking focus. The arc is decorative (`aria-hidden`).",
70
+ "reducedMotion": "Under `@media (prefers-reduced-motion: reduce)` the spin is suppressed; the full ring shows statically as a quiet loading mark.",
71
+ "singleInstance": "Reserve one Spinner per view. Concurrent regional waits stack visual noise β€” lift to a single screen-level Spinner or switch the inner waits to Skeleton.",
72
+ "labelComposition": "Pass `label` for visible loading copy beside the arc; it doubles as the accessible name. Without a label, `aria-label` (default `'Loading'`) carries the announcement."
73
+ },
74
+ "forbidden": [
75
+ "Spinner bound to a known progress ratio / percentage β€” indeterminate only; a determinate value belongs to `progress`",
76
+ "more than one Spinner visible in a single view β€” reserve to one per view; concurrent regional waits use `skeleton` or lift to a single screen-level Spinner",
77
+ "Spinner used as a content-shaped placeholder (list row, feed card, avatar) β€” that role is `skeleton`, which mirrors the footprint of the data being fetched",
78
+ "arc painted with a non-token hex / raw px diameter β€” the arc tone is `sys.color.primary` and the diameter rides the `icon.*` ladder (`sys.icon.md` / `sys.icon.lg`)",
79
+ "shimmer / gradient sweep on the arc β€” Spinner modulates rotation only (one motion axis keeps it readable under the reduced-motion fallback)",
80
+ "arc track or ring drawn with a `border:` β€” the ring draws as a `box-shadow` / conic fill, not a layout stroke"
81
+ ]
82
+ }
@@ -88,6 +88,15 @@
88
88
  "pressed": {
89
89
  "overlay": { "color": "label", "opacity": "sys.state.pressed" }
90
90
  },
91
+ "focused": {
92
+ "focusRing": {
93
+ "composition": "outward",
94
+ "layer": "::after overlay β€” position:absolute, inset:0, no reflow (DESIGN.md Focus ring composition)",
95
+ "innerCounterRing": { "width": "sys.borderWidth.hairline", "color": "sys.color.focusInset" },
96
+ "outerRing": { "width": "sys.borderWidth.thin", "color": "sys.color.focus" }
97
+ },
98
+ "note": "Keyboard-focus (:focus-visible) visual β€” a three-layer outward ring on the track's outer edge, with no state-overlay tint (the ring alone carries focus here). Mirrors the `focusIndicator` block for spec-only renderers. Composes over the lifecycle state the control is in."
99
+ },
91
100
  "disabled": {
92
101
  "containerOpacity": "sys.state.disabled",
93
102
  "pointerEvents": "none",
@@ -142,6 +142,22 @@
142
142
  "description": "Active press on the item. State layer fills the slot, painted with `onSurface` at `sys.state.pressed` (12%).",
143
143
  "stateLayerFill": "color-mix(sys.color.onSurface, sys.state.pressed)"
144
144
  },
145
+ "focused": {
146
+ "stateLayerFill": "color-mix(sys.color.onSurface, sys.state.focus)",
147
+ "focusRing": {
148
+ "composition": "inward",
149
+ "layer": "::after/::before overlay β€” position:absolute, inset:0, inset box-shadow, no reflow (DESIGN.md Focus ring composition)",
150
+ "innerCounterRing": {
151
+ "width": "sys.borderWidth.hairline",
152
+ "color": "sys.color.focusInset"
153
+ },
154
+ "outerRing": {
155
+ "width": "sys.borderWidth.thin",
156
+ "color": "sys.color.focus"
157
+ }
158
+ },
159
+ "note": "Keyboard-focus (:focus-visible) visual. State layer fills the slot with `onSurface` at `sys.state.focus` (12%); the three-layer ring is forced inward (adjacent items are flush under flex:1 1 0). Mirrors the `focusIndicator` block for spec-only renderers. Single-focus: at most one item holds the ring."
160
+ },
145
161
  "active": {
146
162
  "description": "Currently selected destination. Filled glyph (`activeIcon`) + label, both at `onSurface`. Carries `aria-current='page'`. No persistent state layer β€” only hover / pressed / focus paint."
147
163
  },
@@ -106,6 +106,25 @@
106
106
  "opacity": "sys.state.pressed"
107
107
  }
108
108
  },
109
+ "focused": {
110
+ "overlay": {
111
+ "color": "label",
112
+ "opacity": "sys.state.focus"
113
+ },
114
+ "focusRing": {
115
+ "composition": "inward",
116
+ "layer": "::after/::before overlay β€” position:absolute, inset:0, inset box-shadow, no reflow (DESIGN.md Focus ring composition)",
117
+ "innerCounterRing": {
118
+ "width": "sys.borderWidth.hairline",
119
+ "color": "sys.color.focusInset"
120
+ },
121
+ "outerRing": {
122
+ "width": "sys.borderWidth.thin",
123
+ "color": "sys.color.focus"
124
+ }
125
+ },
126
+ "note": "Keyboard-focus (:focus-visible) visual. Mirrors the `focusIndicator` block (the external-reader contract); kept here so spec-only renderers see focus in the states map. Composes over the lifecycle state the tab is in; never via plain mouse click."
127
+ },
109
128
  "disabled": {
110
129
  "overlay": null,
111
130
  "containerOpacity": "sys.state.disabled",
@@ -103,6 +103,25 @@
103
103
  "opacity": "sys.state.pressed"
104
104
  }
105
105
  },
106
+ "focused": {
107
+ "overlay": {
108
+ "color": "label",
109
+ "opacity": "sys.state.focus"
110
+ },
111
+ "focusRing": {
112
+ "composition": "inward",
113
+ "layer": "::after/::before overlay β€” position:absolute, inset:0, inset box-shadow, no reflow (DESIGN.md Focus ring composition)",
114
+ "innerCounterRing": {
115
+ "width": "sys.borderWidth.hairline",
116
+ "color": "sys.color.focusInset"
117
+ },
118
+ "outerRing": {
119
+ "width": "sys.borderWidth.thin",
120
+ "color": "sys.color.focus"
121
+ }
122
+ },
123
+ "note": "Keyboard-focus (:focus-visible) visual. Mirrors the `focusIndicator` block (the external-reader contract); kept here so spec-only renderers see focus in the states map. Composes over the lifecycle state the tab is in; never via plain mouse click."
124
+ },
106
125
  "disabled": {
107
126
  "overlay": null,
108
127
  "containerOpacity": "sys.state.disabled",
@@ -39,15 +39,15 @@
39
39
  "styles": "packages/ui/src/styles.css",
40
40
  "iconsExportSubpath": "@teamblind-chorus/ui/icons",
41
41
  "stylesExportSubpath": "@teamblind-chorus/ui/styles.css",
42
- "published": false,
43
- "distribution": "source",
44
- "note": "Workspace-only, source-distributed. External tools either (a) read JSX directly from `entry` (compile JSX themselves) or (b) consume the structural contract via `schema/components/<family>/<sub>.spec.json` and re-render with their own renderer. Either path MUST also load `styles.css` once at the app entry β€” the JSX emits inline `--<component>-*` plumbing vars but expects the static rules in `styles.css` to receive them. The package is marked `private: true` and intentionally pinned at version `0.0.0` until a publishing decision is made."
42
+ "published": true,
43
+ "distribution": "npm",
44
+ "note": "Published to npm as `@teamblind-chorus/ui` (built `dist/` β€” compiled components, type declarations, and CSS). External tools either (a) install the package and import its components, or (b) consume the structural contract via `schema/components/<family>/<sub>.spec.json` and re-render with their own renderer. Either path MUST also load `@teamblind-chorus/ui/styles.css` once at the app entry β€” components emit inline `--<component>-*` plumbing vars but expect the static rules in `styles.css` to receive them."
45
45
  },
46
46
  "tokens": {
47
47
  "name": "@teamblind-chorus/tokens",
48
- "published": false,
49
- "distribution": "source",
50
- "note": "Workspace-only. External tools should prefer the `tokens.resolved.*` bundles above over the three-tier source files unless they specifically need the reference graph."
48
+ "published": true,
49
+ "distribution": "npm",
50
+ "note": "Published to npm as `@teamblind-chorus/tokens`. External tools should prefer the `tokens.resolved.*` bundles above over the three-tier source files unless they specifically need the reference graph."
51
51
  }
52
52
  },
53
53
  "components": [
@@ -62,6 +62,7 @@
62
62
  { "family": "dialog", "root": "components/dialog/dialog.family.json" },
63
63
  { "family": "divider", "root": "components/divider/divider.family.json" },
64
64
  { "family": "directory-list", "root": "components/directory-list/directory-list.family.json" },
65
+ { "family": "empty-state", "root": "components/empty-state/empty-state.family.json" },
65
66
  { "family": "feed", "root": "components/feed/feed.family.json" },
66
67
  { "family": "form-field", "root": "components/form-field/form-field.family.json" },
67
68
  { "family": "header", "root": "components/header/header.family.json" },
@@ -77,6 +78,7 @@
77
78
  { "family": "carousel", "root": "components/carousel/carousel.family.json" },
78
79
  { "family": "side-sheet", "root": "components/side-sheet/side-sheet.family.json" },
79
80
  { "family": "skeleton", "root": "components/skeleton/skeleton.family.json" },
81
+ { "family": "spinner", "root": "components/spinner/spinner.family.json" },
80
82
  { "family": "status-tag", "root": "components/status-tag/status-tag.family.json" },
81
83
  { "family": "switch", "root": "components/switch/switch.family.json" },
82
84
  { "family": "tab-bar", "root": "components/tab-bar/tab-bar.family.json" },
package/agents/usage.json CHANGED
@@ -156,6 +156,12 @@
156
156
  "note": "Rows are an `items` array of descriptors (name/followers/description/thumbnail/active/onToggle) β€” there are NO List or row children.",
157
157
  "example": "<DirectoryList label=\"…\" items={[{ value, name, followers, description, thumbnail: { src, alt }, active, onToggle }]} />"
158
158
  },
159
+ "empty-state": {
160
+ "import": "EmptyState",
161
+ "from": "@teamblind-chorus/ui",
162
+ "note": "Headline is the `headline` prop (required); `body` is the `body` prop; the CTA is the `action` object ({ label, href?/onClick? }) which renders a default-size primary Button β€” there is NO `cta` slot to fill with your own button. Pass an icon node to `illustration`.",
163
+ "example": "<EmptyState illustration={<ChatIcon />} headline=\"No posts yet\" body=\"Conversations you start or join will appear here.\" action={{ label: 'Start a post', onClick: () => {} }} />"
164
+ },
159
165
  "feed": {
160
166
  "import": "Feed",
161
167
  "from": "@teamblind-chorus/ui",
@@ -363,6 +369,12 @@
363
369
  "note": "shape defaults to text; width/height are consumer-supplied. Stack multiples in SkeletonGroup to mirror the loading content's rhythm.",
364
370
  "example": "<Skeleton shape=\"block\" height={120} />"
365
371
  },
372
+ "spinner": {
373
+ "import": "Spinner",
374
+ "from": "@teamblind-chorus/ui",
375
+ "note": "indeterminate only β€” never bind it to a progress ratio (use Progress). Reserve to one Spinner per view. aria-label defaults to 'Loading'; pass a label child for visible loading copy beside the arc.",
376
+ "example": "<Spinner aria-label=\"Loading\" />"
377
+ },
366
378
  "status-tag": {
367
379
  "import": "StatusTag",
368
380
  "from": "@teamblind-chorus/ui",