@teamblind-chorus/ui 1.0.1 → 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 (131) hide show
  1. package/agents/AGENTS.md +4 -6
  2. package/agents/DESIGN.md +2 -0
  3. package/agents/LOVABLE.md +167 -373
  4. package/agents/anti-patterns.md +2 -2
  5. package/agents/catalog.md +12 -6
  6. package/agents/components/avatar-rail/avatar-rail.md +2 -0
  7. package/agents/components/avatar-rail/avatar-rail.spec.json +19 -0
  8. package/agents/components/badge/badge.md +2 -0
  9. package/agents/components/badge/role.md +2 -0
  10. package/agents/components/badge/update.md +2 -0
  11. package/agents/components/banner/banner.family.json +3 -1
  12. package/agents/components/banner/banner.md +125 -9
  13. package/agents/components/banner/banner.spec.json +64 -3
  14. package/agents/components/bottom-sheet/bottom-sheet.md +2 -0
  15. package/agents/components/bubble/bubble.md +2 -0
  16. package/agents/components/button/button.family.json +8 -2
  17. package/agents/components/button/button.md +2 -0
  18. package/agents/components/button/check.md +2 -0
  19. package/agents/components/button/check.spec.json +19 -0
  20. package/agents/components/button/fab.md +2 -0
  21. package/agents/components/button/fab.spec.json +19 -0
  22. package/agents/components/button/group.spec.json +65 -0
  23. package/agents/components/button/icon.md +2 -0
  24. package/agents/components/button/icon.spec.json +19 -0
  25. package/agents/components/button/standard.md +45 -19
  26. package/agents/components/button/standard.spec.json +19 -0
  27. package/agents/components/button/text.md +2 -0
  28. package/agents/components/button/text.spec.json +19 -0
  29. package/agents/components/button/toggle.md +2 -0
  30. package/agents/components/button/toggle.spec.json +19 -0
  31. package/agents/components/button/toolbar.md +2 -0
  32. package/agents/components/carousel/carousel.md +2 -0
  33. package/agents/components/carousel/post.md +5 -3
  34. package/agents/components/carousel/post.spec.json +4 -6
  35. package/agents/components/carousel/profile.md +4 -2
  36. package/agents/components/carousel/profile.spec.json +4 -6
  37. package/agents/components/chip/chip.md +2 -0
  38. package/agents/components/chip/filter.md +2 -0
  39. package/agents/components/chip/filter.spec.json +19 -0
  40. package/agents/components/chip/tag.md +2 -0
  41. package/agents/components/chip/tag.spec.json +19 -0
  42. package/agents/components/dialog/dialog.md +2 -0
  43. package/agents/components/directory-list/directory-list.md +2 -0
  44. package/agents/components/divider/divider.md +2 -0
  45. package/agents/components/empty-state/empty-state.family.json +28 -0
  46. package/agents/components/empty-state/empty-state.md +69 -0
  47. package/agents/components/empty-state/empty-state.spec.json +87 -0
  48. package/agents/components/feed/ad.md +2 -0
  49. package/agents/components/feed/feed.md +2 -0
  50. package/agents/components/feed/post.md +2 -0
  51. package/agents/components/form-field/form-field.md +3 -1
  52. package/agents/components/form-field/input.md +2 -0
  53. package/agents/components/form-field/input.spec.json +10 -2
  54. package/agents/components/form-field/search.md +2 -0
  55. package/agents/components/form-field/search.spec.json +10 -2
  56. package/agents/components/form-field/select.md +2 -0
  57. package/agents/components/form-field/select.spec.json +9 -1
  58. package/agents/components/form-field/textarea.md +2 -0
  59. package/agents/components/form-field/textarea.spec.json +10 -2
  60. package/agents/components/header/header.md +2 -0
  61. package/agents/components/header/main.md +2 -0
  62. package/agents/components/header/sub.md +2 -0
  63. package/agents/components/list/accordion.md +2 -0
  64. package/agents/components/list/accordion.spec.json +9 -0
  65. package/agents/components/list/entry.md +2 -0
  66. package/agents/components/list/entry.spec.json +21 -1
  67. package/agents/components/list/list.md +3 -1
  68. package/agents/components/list/radio.md +2 -0
  69. package/agents/components/list/radio.spec.json +19 -0
  70. package/agents/components/list/standard.md +48 -0
  71. package/agents/components/list/standard.spec.json +39 -3
  72. package/agents/components/metadata/compact.md +13 -7
  73. package/agents/components/metadata/compact.spec.json +19 -6
  74. package/agents/components/metadata/metadata.family.json +3 -3
  75. package/agents/components/metadata/metadata.md +4 -2
  76. package/agents/components/metadata/standard.md +24 -0
  77. package/agents/components/nav-card/nav-card.md +2 -0
  78. package/agents/components/nav-card/nav-card.spec.json +9 -0
  79. package/agents/components/nav-list/nav-list.md +2 -0
  80. package/agents/components/navigation-bar/main.md +2 -0
  81. package/agents/components/navigation-bar/navigation-bar.md +2 -0
  82. package/agents/components/navigation-bar/search.md +2 -0
  83. package/agents/components/navigation-bar/sub.md +2 -0
  84. package/agents/components/page-shell/page-shell.family.json +1 -1
  85. package/agents/components/page-shell/page-shell.md +35 -0
  86. package/agents/components/page-shell/page-shell.spec.json +85 -0
  87. package/agents/components/pagination/pagination.family.json +26 -0
  88. package/agents/components/pagination/pagination.md +40 -0
  89. package/agents/components/pagination/pagination.spec.json +54 -0
  90. package/agents/components/profile-header/profile-header.md +2 -0
  91. package/agents/components/progress/progress.md +2 -0
  92. package/agents/components/side-sheet/side-sheet.md +2 -0
  93. package/agents/components/skeleton/skeleton.md +2 -0
  94. package/agents/components/spinner/spinner.family.json +27 -0
  95. package/agents/components/spinner/spinner.md +98 -0
  96. package/agents/components/spinner/spinner.spec.json +82 -0
  97. package/agents/components/status-tag/status-tag.md +2 -0
  98. package/agents/components/suggestion-list/suggestion-list.md +2 -0
  99. package/agents/components/switch/switch.md +2 -0
  100. package/agents/components/switch/switch.spec.json +9 -0
  101. package/agents/components/tab-bar/tab-bar.md +2 -0
  102. package/agents/components/tab-bar/tab-bar.spec.json +16 -0
  103. package/agents/components/tabs/rounded.md +2 -0
  104. package/agents/components/tabs/rounded.spec.json +19 -0
  105. package/agents/components/tabs/segmented.md +2 -0
  106. package/agents/components/tabs/tabs.md +2 -0
  107. package/agents/components/tabs/underline.md +2 -0
  108. package/agents/components/tabs/underline.spec.json +19 -0
  109. package/agents/components/thumbnail/thumbnail.md +2 -0
  110. package/agents/components/toast/toast.md +2 -0
  111. package/agents/components/tooltip/tooltip.md +2 -0
  112. package/agents/compose.md +3 -3
  113. package/agents/manifest.json +9 -6
  114. package/agents/patterns/README.md +2 -0
  115. package/agents/patterns/actions.md +2 -0
  116. package/agents/patterns/browsing.md +2 -0
  117. package/agents/patterns/communications.md +2 -0
  118. package/agents/patterns/layout.md +2 -0
  119. package/agents/patterns/modals.md +2 -0
  120. package/agents/patterns/visual.md +2 -0
  121. package/agents/usage.json +27 -3
  122. package/dist/index.cjs +433 -97
  123. package/dist/index.cjs.map +1 -1
  124. package/dist/index.d.cts +74 -3
  125. package/dist/index.d.ts +74 -3
  126. package/dist/index.js +430 -98
  127. package/dist/index.js.map +1 -1
  128. package/dist/styles.css +365 -41
  129. package/package.json +1 -2
  130. package/agents/reconstruct.md +0 -55
  131. package/agents/scoped-adoption.md +0 -111
package/dist/styles.css CHANGED
@@ -222,6 +222,65 @@ textarea {
222
222
  opacity: 0;
223
223
  }
224
224
 
225
+ /* ------------------------------------------------------------
226
+ ButtonGroup — Buttons composed as one block. The row carries the
227
+ family's 8px (`sys.layout.inline.md`) gap; an optional label stacks
228
+ above it. The docked form adds the footer-bar chrome (surface fill,
229
+ 16px inset, upward `sys.elevation.sheet` shadow). See ButtonGroup.jsx
230
+ and schema/components/button/standard.md.
231
+ ------------------------------------------------------------ */
232
+ .chorus-button-group {
233
+ display: flex;
234
+ flex-direction: column;
235
+ }
236
+
237
+ .chorus-button-group__row {
238
+ display: flex;
239
+ gap: var(--sys-layout-inline-md);
240
+ }
241
+
242
+ /* Vertical inline group — full-width Buttons stacked (primary over
243
+ secondary). Same 8px gap, now on the block axis. */
244
+ .chorus-button-group--vertical .chorus-button-group__row {
245
+ flex-direction: column;
246
+ align-items: stretch;
247
+ }
248
+
249
+ /* Optional caption above the row — `sys.typo.body.md` (16px) in the muted
250
+ on-surface tone, centered, 16px (`sys.layout.stack.md`) above the row. An
251
+ inline <strong> reads as the emphasized value in the full-strength tone. */
252
+ .chorus-button-group__label {
253
+ text-align: center;
254
+ margin-bottom: var(--sys-layout-stack-md);
255
+ color: var(--sys-color-onSurfaceVariant);
256
+ font-size: var(--sys-typo-body-md-size);
257
+ font-weight: var(--sys-typo-body-md-weight);
258
+ line-height: var(--sys-typo-body-md-line);
259
+ letter-spacing: var(--sys-typo-body-md-tracking);
260
+ }
261
+
262
+ .chorus-button-group__label strong {
263
+ color: var(--sys-color-onSurface);
264
+ }
265
+
266
+ /* Docked footer bar — full-bleed surface pinned to the bottom of the app.
267
+ No top stroke; the upward `sys.elevation.sheet` shadow (same one
268
+ BottomSheet / SideSheet cast) lifts it off the scrolling body so content
269
+ passing behind reads as a separate region. Renders in flow — PageShell
270
+ owns the pin, so the bar must NOT set position: sticky/fixed itself. */
271
+ .chorus-button-group--docked {
272
+ background: var(--sys-color-surface);
273
+ padding: var(--sys-layout-container-md);
274
+ box-shadow: var(--sys-elevation-sheet);
275
+ }
276
+
277
+ /* In the docked bar the two Buttons split the row equally, so the consumer
278
+ drops in two `<Button size="large">` without wiring `fullWidth`. */
279
+ .chorus-button-group--docked .chorus-button-group__row > * {
280
+ flex: 1 1 0;
281
+ min-width: 0;
282
+ }
283
+
225
284
  /* ============================================================
226
285
  FAB
227
286
  ============================================================ */
@@ -1057,6 +1116,8 @@ a.chorus-metadata__name:focus-visible {
1057
1116
  timestamp. Single text line — the row never wraps; identity links
1058
1117
  truncate before the timestamp gives way. */
1059
1118
  .chorus-metadata--compact .chorus-metadata__meta {
1119
+ flex: 1 1 auto;
1120
+ min-width: 0;
1060
1121
  flex-wrap: nowrap;
1061
1122
  white-space: nowrap;
1062
1123
  }
@@ -1074,6 +1135,9 @@ a.chorus-metadata__name:focus-visible {
1074
1135
 
1075
1136
  .chorus-metadata--compact .chorus-metadata__timestamp {
1076
1137
  flex: 0 0 auto;
1138
+ /* No leading middot — pay the name↔time gap directly (matches the
1139
+ Standard head's 8px primary-line gap). */
1140
+ margin-inline-start: var(--sys-layout-inline-md);
1077
1141
  }
1078
1142
 
1079
1143
  /* ============================================================
@@ -1248,7 +1312,7 @@ a.chorus-metadata__name:focus-visible {
1248
1312
  gap: var(--sys-layout-inline-sm);
1249
1313
  }
1250
1314
  .chorus-feed__poll-glyph { color: var(--sys-color-brand); display: inline-flex; }
1251
- .chorus-feed__poll-label { color: var(--sys-color-brand); font-weight: 700; }
1315
+ .chorus-feed__poll-label { color: var(--sys-color-brand); font-weight: var(--ref-fontWeight-bold); }
1252
1316
 
1253
1317
  /* Offer-evaluation tone — same chrome as the poll banner, but the
1254
1318
  leading glyph and label paint in `sys.color.success` (resolves to
@@ -1266,7 +1330,7 @@ a.chorus-metadata__name:focus-visible {
1266
1330
  background: var(--sys-color-outlineVariant);
1267
1331
  }
1268
1332
  .chorus-feed__poll-participants { color: var(--sys-color-onSurface); }
1269
- .chorus-feed__poll-participants strong { font-weight: 700; }
1333
+ .chorus-feed__poll-participants strong { font-weight: var(--ref-fontWeight-bold); }
1270
1334
 
1271
1335
  .chorus-feed__citation {
1272
1336
  /* Citation is one bordered surface: the hero and the text column sit
@@ -1831,23 +1895,12 @@ a.chorus-metadata__name:focus-visible {
1831
1895
  color: currentColor;
1832
1896
  }
1833
1897
 
1834
- .chorus-post-carousel__pagination {
1835
- display: flex;
1836
- align-items: center;
1837
- justify-content: center;
1838
- gap: var(--sys-layout-inline-sm);
1839
- }
1840
-
1841
- .chorus-post-carousel__dot {
1842
- width: var(--ref-space-75);
1843
- height: var(--ref-space-75);
1844
- border-radius: var(--sys-radius-full);
1845
- background: var(--sys-color-outlineVariant);
1846
- display: block;
1847
- }
1848
-
1849
- .chorus-post-carousel__dot--active {
1850
- background: var(--sys-color-onSurface);
1898
+ /* Pagination dots below the pager are the shared Pagination component
1899
+ (.chorus-pagination) — no carousel-local dot rules. The component is
1900
+ an intrinsic-width inline element, so the carousel (column flex,
1901
+ align-items stretch) centers it itself. */
1902
+ .chorus-post-carousel > .chorus-pagination {
1903
+ align-self: center;
1851
1904
  }
1852
1905
 
1853
1906
  /* ============================================================
@@ -2078,23 +2131,12 @@ a.chorus-metadata__name:focus-visible {
2078
2131
  justify-content: center;
2079
2132
  }
2080
2133
 
2081
- .chorus-profile-carousel__pagination {
2082
- display: flex;
2083
- align-items: center;
2084
- justify-content: center;
2085
- gap: var(--sys-layout-inline-sm);
2086
- }
2087
-
2088
- .chorus-profile-carousel__dot {
2089
- width: var(--ref-space-75);
2090
- height: var(--ref-space-75);
2091
- border-radius: var(--sys-radius-full);
2092
- background: var(--sys-color-outlineVariant);
2093
- display: block;
2094
- }
2095
-
2096
- .chorus-profile-carousel__dot--active {
2097
- background: var(--sys-color-onSurface);
2134
+ /* Pagination dots below the pager are the shared Pagination component
2135
+ (.chorus-pagination) — no carousel-local dot rules. The component is
2136
+ an intrinsic-width inline element, so the carousel (column flex,
2137
+ align-items stretch) centers it itself. */
2138
+ .chorus-profile-carousel > .chorus-pagination {
2139
+ align-self: center;
2098
2140
  }
2099
2141
 
2100
2142
  /* ============================================================
@@ -2437,11 +2479,20 @@ a.chorus-metadata__name:focus-visible {
2437
2479
  pointer-events: none;
2438
2480
  }
2439
2481
 
2440
- .chorus-list__row:hover {
2482
+ /* Nested-action scope — a trailing slot carrying an independent action
2483
+ (a Follow / mute / favorite button, an overflow control) is marked
2484
+ `data-nested-action` and already stops click/key propagation so it never
2485
+ commits the row's primary action. The row's hover / press overlay is
2486
+ suppressed while the pointer sits on that action so the large row does
2487
+ NOT read as hovered / pressed at the same time the small control does —
2488
+ the visual state boundary matches the event boundary. The decorative
2489
+ nav chevron carries no `data-nested-action`, so hovering it still lights
2490
+ the row (it IS the row's drill-in affordance). */
2491
+ .chorus-list__row:hover:not(:has([data-nested-action]:hover)) {
2441
2492
  background: color-mix(in srgb, var(--sys-color-onSurface) calc(var(--sys-state-hover) * 100%), transparent);
2442
2493
  }
2443
2494
 
2444
- .chorus-list__row:active {
2495
+ .chorus-list__row:active:not(:has([data-nested-action]:active)) {
2445
2496
  background: color-mix(in srgb, var(--sys-color-onSurface) calc(var(--sys-state-pressed) * 100%), transparent);
2446
2497
  }
2447
2498
 
@@ -2564,6 +2615,31 @@ a.chorus-metadata__name:focus-visible {
2564
2615
  margin-left: var(--sys-layout-inline-md);
2565
2616
  }
2566
2617
 
2618
+ /* Embedded-Banner row — a Standard row whose text group stacks over a
2619
+ Banner that spans the row's full content width. The row flips from a
2620
+ single horizontal line to a vertical stack: the normal leading + label
2621
+ + trailing line (`__row-main`) sits on top, the Banner `stack.xs` (8)
2622
+ below it. The stack fills the row's content box so the Banner aligns to
2623
+ the same 16px inline inset as the text group above it. The Banner owns
2624
+ its own tinted fill / radius / padding; the slot only spaces it and
2625
+ marks it a nested-action region (its controls never commit the row). */
2626
+ .chorus-list__stack {
2627
+ display: flex;
2628
+ flex-direction: column;
2629
+ gap: var(--sys-layout-stack-xs);
2630
+ flex: 1 1 auto;
2631
+ min-width: 0;
2632
+ }
2633
+
2634
+ .chorus-list__row-main {
2635
+ display: flex;
2636
+ align-items: center;
2637
+ }
2638
+
2639
+ .chorus-list__banner {
2640
+ display: block;
2641
+ }
2642
+
2567
2643
  /* Entry variant — directory-entry row with selectable leading Thumbnail
2568
2644
  (32 / 40 / 48 / 56 via `data-size="small|medium|large|xlarge"`), an
2569
2645
  identity group (label + optional inline count Badge + optional stacked
@@ -3227,6 +3303,65 @@ a.chorus-metadata__name:focus-visible {
3227
3303
  rule already excludes `[data-divider="false"]`, so no extra CSS
3228
3304
  is needed to hide it here. */
3229
3305
 
3306
+ /* ============================================================
3307
+ EmptyState — centered no-data composition. Painted inside the
3308
+ surface that would otherwise hold the data: an optional monochrome
3309
+ illustration, a required headline, optional body copy, and an
3310
+ optional primary-Button CTA. Ships no surface fill of its own —
3311
+ the host supplies the surface tier and the bounding box; EmptyState
3312
+ only centers its column inside it. Inter-slot rhythm follows the
3313
+ stack tokens (illustration→headline 12, headline→body 4, body→CTA
3314
+ 16). Distinct from Skeleton (an in-flight loading placeholder).
3315
+ ============================================================ */
3316
+ .chorus-empty-state {
3317
+ display: flex;
3318
+ flex-direction: column;
3319
+ align-items: center;
3320
+ text-align: center;
3321
+ }
3322
+
3323
+ /* Optional illustration — a glyph / illustration sized to a 48-box
3324
+ (larger than `sys.icon.lg`, realizing the 'icon.xl or larger' intent
3325
+ since no `sys.icon.xl` rung exists), painted monochrome in
3326
+ `onSurfaceVariant` via `currentColor` so it reads as quiet chrome,
3327
+ not a brand moment. 12 below it to the headline. */
3328
+ .chorus-empty-state__illustration {
3329
+ display: inline-flex;
3330
+ align-items: center;
3331
+ justify-content: center;
3332
+ width: var(--empty-state-illustration-size);
3333
+ height: var(--empty-state-illustration-size);
3334
+ margin-bottom: var(--empty-state-illustration-gap);
3335
+ color: var(--empty-state-illustration-color);
3336
+ }
3337
+
3338
+ .chorus-empty-state__illustration img,
3339
+ .chorus-empty-state__illustration svg {
3340
+ width: var(--empty-state-illustration-size);
3341
+ height: var(--empty-state-illustration-size);
3342
+ display: block;
3343
+ color: currentColor;
3344
+ }
3345
+
3346
+ /* Required headline — heading.sm in `onSurface`. Typography vars are
3347
+ emitted inline by the React file (typoStyles). */
3348
+ .chorus-empty-state__headline {
3349
+ margin: 0;
3350
+ color: var(--empty-state-headline-color);
3351
+ }
3352
+
3353
+ /* Optional body — body.sm in `onSurfaceVariant`, 4 below the headline. */
3354
+ .chorus-empty-state__body {
3355
+ margin: 0;
3356
+ margin-top: var(--empty-state-body-gap);
3357
+ color: var(--empty-state-body-color);
3358
+ }
3359
+
3360
+ /* Optional CTA — a default-size primary Button, 16 below the body. */
3361
+ .chorus-empty-state__action {
3362
+ margin-top: var(--empty-state-action-gap);
3363
+ }
3364
+
3230
3365
  /* ============================================================
3231
3366
  Banner — in-body explanation block (info / neutral)
3232
3367
  ============================================================ */
@@ -3241,6 +3376,7 @@ a.chorus-metadata__name:focus-visible {
3241
3376
  .chorus-banner--accent {
3242
3377
  background: var(--sys-color-primaryContainer);
3243
3378
  color: var(--sys-color-onPrimaryContainer);
3379
+ --banner-outline-color: color-mix(in srgb, var(--sys-color-primary) 40%, transparent);
3244
3380
  }
3245
3381
 
3246
3382
  /* Destructive — error-tinted banner. Reach for it when the aside is a
@@ -3251,6 +3387,7 @@ a.chorus-metadata__name:focus-visible {
3251
3387
  .chorus-banner--destructive {
3252
3388
  background: var(--sys-color-errorContainer);
3253
3389
  color: var(--sys-color-onErrorContainer);
3390
+ --banner-outline-color: color-mix(in srgb, var(--sys-color-error) 40%, transparent);
3254
3391
  }
3255
3392
 
3256
3393
  /* Default uses `sys.color.scrimSubtle` (~8% inverse-tone overlay —
@@ -3264,6 +3401,21 @@ a.chorus-metadata__name:focus-visible {
3264
3401
  .chorus-banner--default {
3265
3402
  background: var(--sys-color-scrimSubtle);
3266
3403
  color: var(--sys-color-onSurface);
3404
+ --banner-outline-color: var(--sys-color-outlineVariant);
3405
+ }
3406
+
3407
+ /* Optional outline — a hairline inset stroke toned to the appearance's
3408
+ color family (each appearance block above sets
3409
+ `--banner-outline-color`), kept deliberately faint so the edge reads
3410
+ as a soft boundary of the same tint, not a frame: the subtle gray
3411
+ hairline (`sys.color.outlineVariant`) on default's gray-tinted
3412
+ scrim; `primary` / `error` at 40% over transparent (the Skeleton
3413
+ fill recipe) on accent / destructive. Painted as an inset
3414
+ box-shadow, never a `border`, so toggling it can't change the
3415
+ banner's footprint — same idiom as Chip (see DESIGN.md → Border &
3416
+ Stroke). */
3417
+ .chorus-banner--outlined {
3418
+ box-shadow: inset 0 0 0 var(--sys-borderWidth-hairline) var(--banner-outline-color);
3267
3419
  }
3268
3420
 
3269
3421
  /* Leading icon slot — a 16 × 16 glyph (`sys.icon.md`) that paints in
@@ -3306,6 +3458,23 @@ a.chorus-metadata__name:focus-visible {
3306
3458
  gap: var(--sys-layout-stack-xs);
3307
3459
  }
3308
3460
 
3461
+ /* Optional heading line above the body — label.md (14 / Semibold) in
3462
+ the container's foreground. Title↔body wants the tighter
3463
+ `sys.layout.stack.2xs` (4) so the pair reads as one passage, while
3464
+ the content column's gap stays `stack.xs` (8) for body↔action; the
3465
+ negative margin nets the column gap down to 4 for this pair only. */
3466
+ .chorus-banner__title {
3467
+ margin: 0;
3468
+ font-size: var(--sys-typo-label-md-size);
3469
+ line-height: var(--sys-typo-label-md-line);
3470
+ font-weight: var(--sys-typo-label-md-weight);
3471
+ color: inherit;
3472
+ }
3473
+
3474
+ .chorus-banner__title + .chorus-banner__body {
3475
+ margin-top: calc(var(--sys-layout-stack-2xs) - var(--sys-layout-stack-xs));
3476
+ }
3477
+
3309
3478
  .chorus-banner__body {
3310
3479
  margin: 0;
3311
3480
  font-size: var(--sys-typo-body-sm-size);
@@ -3332,6 +3501,60 @@ a.chorus-metadata__name:focus-visible {
3332
3501
  color: var(--sys-color-primary);
3333
3502
  }
3334
3503
 
3504
+ /* neutral-body — keeps the `accent` fill but lays the Default
3505
+ appearance's neutral foreground over it: title + body re-tone to
3506
+ `onSurface` (quiet, high-legibility body copy) while the action
3507
+ steps to `primary`, exactly as on `default`. The accent tint still
3508
+ pulls the eye; the text just stops shouting in the primary family.
3509
+ Only meaningful combined with `--accent` (default is already
3510
+ `onSurface`; destructive must carry the warning tone through copy). */
3511
+ .chorus-banner--accent.chorus-banner--neutral-body {
3512
+ color: var(--sys-color-onSurface);
3513
+ }
3514
+
3515
+ .chorus-banner--accent.chorus-banner--neutral-body .chorus-banner__action {
3516
+ color: var(--sys-color-primary);
3517
+ }
3518
+
3519
+ /* Trailing icon slot — a 16 × 16 glyph (`sys.icon.md`) at the trailing
3520
+ edge, vertically centered against the whole block (`align-self:
3521
+ center` overrides the container's flex-start). Paints in the
3522
+ banner's foreground (`currentColor`); typically a forward affordance
3523
+ (e.g. ForwardCircleFillIcon) signaling the aside leads somewhere. */
3524
+ .chorus-banner__trailing-icon {
3525
+ flex: 0 0 var(--sys-icon-md);
3526
+ align-self: center;
3527
+ display: inline-flex;
3528
+ align-items: center;
3529
+ justify-content: center;
3530
+ color: currentColor;
3531
+ }
3532
+
3533
+ .chorus-banner__trailing-icon img,
3534
+ .chorus-banner__trailing-icon svg {
3535
+ width: var(--sys-icon-md);
3536
+ height: var(--sys-icon-md);
3537
+ display: block;
3538
+ color: currentColor;
3539
+ }
3540
+
3541
+ /* Trailing action slot — hosts an inline Text Button (`<Button
3542
+ variant="text">`) at the trailing edge, vertically centered against
3543
+ the whole block (`align-self: center` overrides the container's
3544
+ flex-start) and footprint-preserving (`flex: 0 0 auto`). The Button
3545
+ owns its own size + appearance per the button/text spec; by default
3546
+ pick the appearance whose color family matches the banner fill
3547
+ (accent → accent, default → default, destructive → destructive
3548
+ flavor) so the commit reads as part of the tinted block. Unlike the
3549
+ trailing icon it is a real interactive control, so it is not
3550
+ aria-hidden. The Text Button's optical-alignment margin lets its
3551
+ visible label sit flush against the container's trailing padding. */
3552
+ .chorus-banner__trailing-action {
3553
+ flex: 0 0 auto;
3554
+ align-self: center;
3555
+ display: inline-flex;
3556
+ }
3557
+
3335
3558
  /* ============================================================
3336
3559
  Divider — section-break band between adjacent regions that
3337
3560
  don't share an enclosing container. Single full-bleed block
@@ -3715,14 +3938,21 @@ a.chorus-metadata__name:focus-visible {
3715
3938
  /* Hover / pressed re-tone the 1px stroke to `borderHover` (`outline`,
3716
3939
  or `error` on the error appearance). `:active` also lights the
3717
3940
  state-overlay `::before`. */
3718
- .chorus-field:hover,
3941
+ /* The trailing clear button is an independent nested action (it wipes the
3942
+ value, distinct from focusing / pressing the field) — marked
3943
+ `data-nested-action`. While the pointer hovers / presses it, the field's
3944
+ own hover stroke and pressed overlay are suppressed so the large field
3945
+ does NOT read as pressed at the same time the small "×" does. The Select
3946
+ chevron is NOT marked: it fires the same action as clicking the field
3947
+ (open), so it correctly lights the field. */
3719
3948
  .chorus-field[data-force-state="hovered"],
3720
3949
  .chorus-field[data-force-state="pressed"],
3721
- .chorus-field:not(.is-disabled):active {
3950
+ .chorus-field:hover:not(:has([data-nested-action]:hover)),
3951
+ .chorus-field:not(.is-disabled):active:not(:has([data-nested-action]:active)) {
3722
3952
  box-shadow: inset 0 0 0 var(--field-border-width) var(--field-border-hover);
3723
3953
  }
3724
3954
 
3725
- .chorus-field:not(.is-disabled):active::before,
3955
+ .chorus-field:not(.is-disabled):active:not(:has([data-nested-action]:active))::before,
3726
3956
  .chorus-field[data-force-state="pressed"]::before {
3727
3957
  opacity: var(--field-overlay-pressed);
3728
3958
  }
@@ -5225,6 +5455,34 @@ a.chorus-metadata__name:focus-visible {
5225
5455
  transform: rotate(45deg);
5226
5456
  }
5227
5457
 
5458
+ /* ============================================================
5459
+ Pagination — decorative dot-position indicator
5460
+ ============================================================ */
5461
+ /* One 6px dot per page in an inline.sm row. An inline element —
5462
+ inline-flex, intrinsic width (no stretch, no self-centering); the
5463
+ host owns horizontal placement. Active dot paints `onSurface`, the
5464
+ rest `outlineVariant`. Non-interactive (`aria-hidden` on the root) —
5465
+ the host pager owns the active index and keyboard reach.
5466
+ See schema/components/pagination/pagination.md. */
5467
+
5468
+ .chorus-pagination {
5469
+ display: inline-flex;
5470
+ align-items: center;
5471
+ gap: var(--sys-layout-inline-sm);
5472
+ }
5473
+
5474
+ .chorus-pagination__dot {
5475
+ width: var(--ref-space-75);
5476
+ height: var(--ref-space-75);
5477
+ border-radius: var(--sys-radius-full);
5478
+ background: var(--sys-color-outlineVariant);
5479
+ display: block;
5480
+ }
5481
+
5482
+ .chorus-pagination__dot--active {
5483
+ background: var(--sys-color-onSurface);
5484
+ }
5485
+
5228
5486
  /* ============================================================
5229
5487
  Progress — linear progress bar (determinate)
5230
5488
  ============================================================ */
@@ -5363,6 +5621,72 @@ a.chorus-metadata__name:focus-visible {
5363
5621
  }
5364
5622
  }
5365
5623
 
5624
+ /* ============================================================
5625
+ Spinner — indeterminate rotating-arc loading indicator
5626
+ ============================================================ */
5627
+ /* A rotating ring for short, progress-unknown waits (< ~1s) on a
5628
+ neutral host surface. The arc paints `sys.color.primary` as the
5629
+ foreground motion over a faint `sys.color.scrimSubtle` track ring
5630
+ (inverse-tone ~8% tint) so the rotation reads on any surface tier.
5631
+ The ring is drawn with a conic gradient masked to an annulus — no
5632
+ `border:` (per the no-layout-strokes rule). Diameter rides the
5633
+ icon.* ladder via the `--spinner-diameter` plumbing var (sys.icon.lg
5634
+ medium / sys.icon.md small). Rotation modulates position, not
5635
+ luminance, and is suppressed under `prefers-reduced-motion: reduce`,
5636
+ leaving the full ring static as a quiet loading mark. */
5637
+ @keyframes chorus-spinner-rotate {
5638
+ to { transform: rotate(360deg); }
5639
+ }
5640
+
5641
+ .chorus-spinner {
5642
+ display: inline-flex;
5643
+ align-items: center;
5644
+ gap: var(--spinner-gap);
5645
+ }
5646
+
5647
+ .chorus-spinner__arc {
5648
+ display: block;
5649
+ flex: none;
5650
+ width: var(--spinner-diameter);
5651
+ height: var(--spinner-diameter);
5652
+ border-radius: var(--sys-radius-full);
5653
+ /* Foreground arc sweeps from primary to transparent over the track. */
5654
+ background:
5655
+ conic-gradient(var(--sys-color-primary), color-mix(in srgb, var(--sys-color-primary) 0%, transparent) 75%, color-mix(in srgb, var(--sys-color-primary) 0%, transparent));
5656
+ /* Annulus mask: punch out the centre so it reads as a ring, 2px thick.
5657
+ The mask fill is alpha-only — the opaque keep-region of the mask is
5658
+ not a design color, so it is not a token binding. */
5659
+ /* chorus-token-exempt */
5660
+ -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 2px), #000 calc(100% - 2px));
5661
+ /* chorus-token-exempt */
5662
+ mask: radial-gradient(farthest-side, transparent calc(100% - 2px), #000 calc(100% - 2px));
5663
+ animation: chorus-spinner-rotate 0.8s linear infinite;
5664
+ }
5665
+
5666
+ /* Faint full track ring under the rotating arc, painted via box-shadow
5667
+ inset so the bare ring stays visible on any surface tier. */
5668
+ .chorus-spinner__arc::before {
5669
+ content: '';
5670
+ position: absolute;
5671
+ inset: 0;
5672
+ border-radius: inherit;
5673
+ box-shadow: inset 0 0 0 2px var(--sys-color-scrimSubtle);
5674
+ }
5675
+
5676
+ .chorus-spinner__arc {
5677
+ position: relative;
5678
+ }
5679
+
5680
+ .chorus-spinner__label {
5681
+ color: var(--sys-color-onSurfaceVariant);
5682
+ }
5683
+
5684
+ @media (prefers-reduced-motion: reduce) {
5685
+ .chorus-spinner__arc {
5686
+ animation: none;
5687
+ }
5688
+ }
5689
+
5366
5690
  /* ============================================================
5367
5691
  Switch — binary active/inactive pill with translating thumb
5368
5692
  ============================================================ */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teamblind-chorus/ui",
3
- "version": "1.0.1",
3
+ "version": "1.2.0",
4
4
  "description": "Chorus React components. Ships prebuilt ESM + CJS bundles (`dist/`) and a single `styles.css`; import `@teamblind-chorus/tokens/tokens.css` + `@teamblind-chorus/ui/styles.css` once at the app entry. The contract every component honors lives in schema/components/<family>/<sub>.spec.json; see schema/manifest.json for the inventory.",
5
5
  "license": "MIT",
6
6
  "author": "Teamblind, Inc.",
@@ -54,7 +54,6 @@
54
54
  "./agents/usage.json": "./agents/usage.json",
55
55
  "./agents/DESIGN.md": "./agents/DESIGN.md",
56
56
  "./agents/LOVABLE.md": "./agents/LOVABLE.md",
57
- "./agents/reconstruct.md": "./agents/reconstruct.md",
58
57
  "./agents/images.md": "./agents/images.md",
59
58
  "./agents/patterns/": "./agents/patterns/",
60
59
  "./agents/components/": "./agents/components/",
@@ -1,55 +0,0 @@
1
- # Chorus reconstruction prompt
2
-
3
- Paste the block below into a Chorus-aware agent (Lovable etc.) to rebuild ad-hoc,
4
- agent-invented UI as pure Chorus. Strongest with `LOVABLE.md` loaded as the system
5
- prompt — this block is a thin **driver**; the full rules live in `LOVABLE.md` and the
6
- guides it tells the agent to read, so the prompt stays short (lower token cost).
7
-
8
- ---
9
-
10
- ```text
11
- Chorus reconstruction pass — rebuild every ad-hoc component as pure Chorus.
12
-
13
- GOAL: every hand-built component (raw div/button, Tailwind, shadcn, inline styles, hex)
14
- becomes pure Chorus. No mixed renders — a screen is 100% Chorus or not done. The old UI
15
- is the source of a migration to @teamblind-chorus/ui + @teamblind-chorus/tokens; don't preserve or
16
- "match" it — the old style is the bug.
17
-
18
- FIRST read node_modules/@teamblind-chorus/ui/agents/: catalog.md, manifest.json, compose.md,
19
- anti-patterns.md. The component name is not the contract — bindings are in
20
- components/<family>/<sub>.spec.json.
21
-
22
- DO, in order:
23
- 1. INVENTORY every non-Chorus component in src/ (one line each: file → what it is).
24
- 2. MAP each intent via catalog.md to a family + sub. If none fits, climb the ladder
25
- (recompose slots → LEGO-combine → new primitive with every value a var(--sys-*)),
26
- then flag a one-line "Chorus gap". Never fall back to raw HTML/Tailwind/shadcn.
27
- 3. REBUILD with the real import from @teamblind-chorus/ui (icons from @teamblind-chorus/ui/icons),
28
- honoring the spec; then delete the old component + its CSS.
29
-
30
- NON-NEGOTIABLE (else discard + regenerate):
31
- - Tokens only: no hex, no Tailwind colors, no off-scale px; card edge = inset shadow,
32
- never border:.
33
- - Typography = className="sys-typo-<role>-<rung>" (NO `font: var(--sys-typo-*)` token).
34
- - Compound children: <Tabs> needs <Tab>; List/SuggestionList/AvatarRail take an items
35
- array — never bare text.
36
- - One gutter at the shell; full-bleed children (Tabs/Feed/List/Carousel/bars) never
37
- wrapped in a padded div. Floating action = <Button variant="fab">, not a pinned
38
- standard button. Fixed bars via .page-shell (height:100dvh + <main> overflow-y:auto).
39
-
40
- VERIFY: eslint.config.js extends @teamblind-chorus/ui/eslint — fix every chorus/* error (never
41
- suppress); run the §E pre-flight checklist + rail self-diagnostic (anti-patterns.md).
42
-
43
- SCOPE (optional): "SCOPE: <area/route globs>" limits the pass to that boundary —
44
- inventory, rebuild, and lint only inside it; outside files are reported, never edited
45
- (boundary rules: agents/scoped-adoption.md).
46
-
47
- DELIVER: a before→after table (component → Chorus family/sub, or "gap") and screens with
48
- zero mixed renders. If scope is large, reconstruct the entry screen first, list the rest.
49
- ```
50
-
51
- ---
52
-
53
- The ESLint preset (`@teamblind-chorus/ui/eslint`, wired in the VERIFY step) is the safety net:
54
- even if the model drops a rule mid-session, `chorus/*` errors fail the build and its own
55
- fix loop catches the drift.