@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
package/dist/index.cjs CHANGED
@@ -108,13 +108,13 @@ function formatCount(value) {
108
108
  return String(Math.floor(value));
109
109
  }
110
110
  function sizingStyle(spec, size) {
111
- const s = spec.sizes[size] ?? spec.sizes.medium;
111
+ const s2 = spec.sizes[size] ?? spec.sizes.medium;
112
112
  return {
113
- "--badge-min-height": tokenToCss(s.minHeight),
114
- "--badge-min-width": tokenToCss(s.minWidth),
115
- "--badge-padding-block": tokenToCss(s.paddingBlock),
116
- "--badge-padding-inline": tokenToCss(s.paddingInline),
117
- ...s.labelTypo ? typoStyles(s.labelTypo) : null
113
+ "--badge-min-height": tokenToCss(s2.minHeight),
114
+ "--badge-min-width": tokenToCss(s2.minWidth),
115
+ "--badge-padding-block": tokenToCss(s2.paddingBlock),
116
+ "--badge-padding-inline": tokenToCss(s2.paddingInline),
117
+ ...s2.labelTypo ? typoStyles(s2.labelTypo) : null
118
118
  };
119
119
  }
120
120
  function appearanceStyle(spec) {
@@ -238,10 +238,12 @@ function useFullBleedGuard(ref, name) {
238
238
  function Banner({
239
239
  appearance = "default",
240
240
  outlined = false,
241
+ neutralBody = false,
241
242
  title,
242
243
  icon,
243
244
  thumbnail,
244
245
  action,
246
+ trailingAction,
245
247
  trailingIcon,
246
248
  children,
247
249
  className,
@@ -257,6 +259,7 @@ function Banner({
257
259
  "chorus-banner",
258
260
  `chorus-banner--${appearance}`,
259
261
  outlined && "chorus-banner--outlined",
262
+ neutralBody && "chorus-banner--neutral-body",
260
263
  className
261
264
  ),
262
265
  role: "note",
@@ -277,7 +280,7 @@ function Banner({
277
280
  }
278
281
  ) : null
279
282
  ] }),
280
- trailingIcon ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "chorus-banner__trailing-icon", "aria-hidden": "true", children: trailingIcon }) : null
283
+ trailingAction ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "chorus-banner__trailing-action", children: trailingAction }) : trailingIcon ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "chorus-banner__trailing-icon", "aria-hidden": "true", children: trailingIcon }) : null
281
284
  ]
282
285
  }
283
286
  );
@@ -374,16 +377,16 @@ var standard_spec_default = {
374
377
  insetColor: "sys.color.focusInset"
375
378
  }}};
376
379
  function sizeStyle(size) {
377
- const s = standard_spec_default.sizes[size] ?? standard_spec_default.sizes[standard_spec_default.props.size.default];
380
+ const s2 = standard_spec_default.sizes[size] ?? standard_spec_default.sizes[standard_spec_default.props.size.default];
378
381
  return {
379
- "--button-standard-padding-block": tokenToCss(s.paddingBlock),
380
- "--button-standard-padding-inline": tokenToCss(s.paddingInline),
381
- "--button-standard-gap": tokenToCss(s.gap),
382
- "--button-standard-min-height": tokenToCss(s.minHeight),
383
- "--button-standard-min-width": tokenToCss(s.minWidth),
384
- "--button-standard-radius": tokenToCss(s.radius),
385
- "--button-standard-icon-size": tokenToCss(s.iconSize),
386
- ...typoStyles(s.labelTypo)
382
+ "--button-standard-padding-block": tokenToCss(s2.paddingBlock),
383
+ "--button-standard-padding-inline": tokenToCss(s2.paddingInline),
384
+ "--button-standard-gap": tokenToCss(s2.gap),
385
+ "--button-standard-min-height": tokenToCss(s2.minHeight),
386
+ "--button-standard-min-width": tokenToCss(s2.minWidth),
387
+ "--button-standard-radius": tokenToCss(s2.radius),
388
+ "--button-standard-icon-size": tokenToCss(s2.iconSize),
389
+ ...typoStyles(s2.labelTypo)
387
390
  };
388
391
  }
389
392
  function appearanceStyle2(appearance) {
@@ -490,8 +493,7 @@ var fab_spec_default = {
490
493
  overlay: {
491
494
  opacity: "sys.state.pressed"
492
495
  }
493
- }
494
- },
496
+ }},
495
497
  focusIndicator: {
496
498
  overlay: {
497
499
  opacity: "sys.state.focus"
@@ -1591,6 +1593,25 @@ var filter_spec_default = {
1591
1593
  opacity: "sys.state.pressed"
1592
1594
  }
1593
1595
  },
1596
+ focused: {
1597
+ overlay: {
1598
+ color: "label",
1599
+ opacity: "sys.state.focus"
1600
+ },
1601
+ focusRing: {
1602
+ composition: "outward",
1603
+ layer: "::after overlay \u2014 position:absolute, inset:0, no reflow (DESIGN.md Focus ring composition)",
1604
+ innerCounterRing: {
1605
+ width: "sys.borderWidth.hairline",
1606
+ color: "sys.color.focusInset"
1607
+ },
1608
+ outerRing: {
1609
+ width: "sys.borderWidth.thin",
1610
+ color: "sys.color.focus"
1611
+ }
1612
+ },
1613
+ 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 chip is in; never via plain mouse click."
1614
+ },
1594
1615
  disabled: {
1595
1616
  overlay: null,
1596
1617
  containerOpacity: "sys.state.disabled",
@@ -1698,6 +1719,25 @@ var tag_spec_default = {
1698
1719
  opacity: "sys.state.pressed"
1699
1720
  }
1700
1721
  },
1722
+ focused: {
1723
+ overlay: {
1724
+ color: "label",
1725
+ opacity: "sys.state.focus"
1726
+ },
1727
+ focusRing: {
1728
+ composition: "outward",
1729
+ layer: "::after overlay \u2014 position:absolute, inset:0, no reflow (DESIGN.md Focus ring composition)",
1730
+ innerCounterRing: {
1731
+ width: "sys.borderWidth.hairline",
1732
+ color: "sys.color.focusInset"
1733
+ },
1734
+ outerRing: {
1735
+ width: "sys.borderWidth.thin",
1736
+ color: "sys.color.focus"
1737
+ }
1738
+ },
1739
+ 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 chip is in; never via plain mouse click."
1740
+ },
1701
1741
  disabled: {
1702
1742
  overlay: null,
1703
1743
  containerOpacity: "sys.state.disabled",
@@ -1823,6 +1863,25 @@ var toggle_spec_default = {
1823
1863
  opacity: "sys.state.pressed"
1824
1864
  }
1825
1865
  },
1866
+ focused: {
1867
+ overlay: {
1868
+ color: "label",
1869
+ opacity: "sys.state.focus"
1870
+ },
1871
+ focusRing: {
1872
+ composition: "outward",
1873
+ layer: "::after overlay \u2014 position:absolute, inset:0, no reflow (DESIGN.md Focus ring composition)",
1874
+ innerCounterRing: {
1875
+ width: "sys.borderWidth.hairline",
1876
+ color: "sys.color.focusInset"
1877
+ },
1878
+ outerRing: {
1879
+ width: "sys.borderWidth.thin",
1880
+ color: "sys.color.focus"
1881
+ }
1882
+ },
1883
+ 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 button is in; never via plain mouse click."
1884
+ },
1826
1885
  disabled: {
1827
1886
  overlay: null,
1828
1887
  containerOpacity: "sys.state.disabled",
@@ -2009,6 +2068,46 @@ var Button = react.forwardRef(function Button2({ variant, ...rest }, ref) {
2009
2068
  const Impl = variant && VARIANTS[variant] || ButtonStandard;
2010
2069
  return /* @__PURE__ */ jsxRuntime.jsx(Impl, { ref, ...rest });
2011
2070
  });
2071
+ var FOCUSABLE_SELECTOR = [
2072
+ "a[href]",
2073
+ "button",
2074
+ "input",
2075
+ "select",
2076
+ "textarea",
2077
+ '[tabindex]:not([tabindex="-1"])'
2078
+ ].join(",");
2079
+ function useFocusTrap(ref, active) {
2080
+ react.useEffect(() => {
2081
+ if (!active) return void 0;
2082
+ const onKey = (e) => {
2083
+ if (e.key !== "Tab") return;
2084
+ const container = ref.current;
2085
+ if (!container) return;
2086
+ const focusable = Array.from(
2087
+ container.querySelectorAll(FOCUSABLE_SELECTOR)
2088
+ ).filter((el) => !el.disabled && el.offsetParent !== null);
2089
+ if (focusable.length === 0) {
2090
+ e.preventDefault();
2091
+ container.focus({ preventScroll: true });
2092
+ return;
2093
+ }
2094
+ const first = focusable[0];
2095
+ const last = focusable[focusable.length - 1];
2096
+ const activeEl = document.activeElement;
2097
+ if (e.shiftKey) {
2098
+ if (activeEl === first || !container.contains(activeEl)) {
2099
+ e.preventDefault();
2100
+ last.focus({ preventScroll: true });
2101
+ }
2102
+ } else if (activeEl === last || !container.contains(activeEl)) {
2103
+ e.preventDefault();
2104
+ first.focus({ preventScroll: true });
2105
+ }
2106
+ };
2107
+ document.addEventListener("keydown", onKey);
2108
+ return () => document.removeEventListener("keydown", onKey);
2109
+ }, [ref, active]);
2110
+ }
2012
2111
  function useBodyScrollLock(locked) {
2013
2112
  react.useEffect(() => {
2014
2113
  if (!locked) return void 0;
@@ -2048,6 +2147,7 @@ function BottomSheet({
2048
2147
  const lastFocusedRef = react.useRef(null);
2049
2148
  const [overflowing, setOverflowing] = react.useState(false);
2050
2149
  useBodyScrollLock(open && !inline);
2150
+ useFocusTrap(cardRef, open && !inline);
2051
2151
  react.useEffect(() => {
2052
2152
  if (!open || inline) return void 0;
2053
2153
  const vv = typeof window !== "undefined" ? window.visualViewport : null;
@@ -2111,6 +2211,7 @@ function BottomSheet({
2111
2211
  className: "chorus-bottom-sheet__card",
2112
2212
  role: "dialog",
2113
2213
  "aria-modal": "true",
2214
+ tabIndex: -1,
2114
2215
  "aria-label": ariaLabel ?? title,
2115
2216
  onClick: (e) => e.stopPropagation(),
2116
2217
  ...rest,
@@ -2429,6 +2530,26 @@ function List({
2429
2530
  if (isRadio) onChange == null ? void 0 : onChange(item.value);
2430
2531
  (_a = item.onClick) == null ? void 0 : _a.call(item);
2431
2532
  };
2533
+ const rowMain = isEntry ? null : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2534
+ isRadio ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "chorus-list__leading", children: /* @__PURE__ */ jsxRuntime.jsx(RadioIndicator, { selected }) }) : item.thumbnail ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "chorus-list__leading chorus-list__leading--image", children: /* @__PURE__ */ jsxRuntime.jsx(Thumbnail, { size: 40, ...item.thumbnail }) }) : item.icon ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "chorus-list__leading chorus-list__leading--icon", "aria-hidden": "true", children: item.icon }) : null,
2535
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "chorus-list__label-col", children: [
2536
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "chorus-list__primary-row", children: [
2537
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "chorus-list__label", children: item.label }),
2538
+ item.count != null ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "chorus-list__count", children: item.count }) : null
2539
+ ] }),
2540
+ item.supportingText ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "chorus-list__supporting", children: item.supportingText }) : null
2541
+ ] }),
2542
+ item.nav && !item.trailingIcon ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "chorus-list__trailing chorus-list__nav-chevron", "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsx(ChevronDownIcon, { size: 16 }) }) : item.trailingIcon ? /* @__PURE__ */ jsxRuntime.jsx(
2543
+ "span",
2544
+ {
2545
+ className: "chorus-list__trailing",
2546
+ "data-nested-action": "",
2547
+ onClick: (e) => e.stopPropagation(),
2548
+ onKeyDown: (e) => e.stopPropagation(),
2549
+ children: item.trailingIcon
2550
+ }
2551
+ ) : null
2552
+ ] });
2432
2553
  return /* @__PURE__ */ jsxRuntime.jsx(
2433
2554
  "div",
2434
2555
  {
@@ -2463,26 +2584,26 @@ function List({
2463
2584
  description: item.description,
2464
2585
  trailing: item.trailingIcon
2465
2586
  }
2466
- ) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2467
- isRadio ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "chorus-list__leading", children: /* @__PURE__ */ jsxRuntime.jsx(RadioIndicator, { selected }) }) : item.thumbnail ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "chorus-list__leading chorus-list__leading--image", children: /* @__PURE__ */ jsxRuntime.jsx(Thumbnail, { size: 40, ...item.thumbnail }) }) : item.icon ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "chorus-list__leading chorus-list__leading--icon", "aria-hidden": "true", children: item.icon }) : null,
2468
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "chorus-list__label-col", children: [
2469
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "chorus-list__primary-row", children: [
2470
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "chorus-list__label", children: item.label }),
2471
- item.count != null ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "chorus-list__count", children: item.count }) : null
2472
- ] }),
2473
- item.supportingText ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "chorus-list__supporting", children: item.supportingText }) : null
2474
- ] }),
2475
- item.nav && !item.trailingIcon ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "chorus-list__trailing chorus-list__nav-chevron", "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsx(ChevronDownIcon, { size: 16 }) }) : item.trailingIcon ? /* @__PURE__ */ jsxRuntime.jsx(
2476
- "span",
2477
- {
2478
- className: "chorus-list__trailing",
2479
- "data-nested-action": "",
2480
- onClick: (e) => e.stopPropagation(),
2481
- onKeyDown: (e) => e.stopPropagation(),
2482
- children: item.trailingIcon
2483
- }
2484
- ) : null
2485
- ] })
2587
+ ) : item.banner ? (
2588
+ // Embedded-Banner row the text group stacks over a Banner
2589
+ // that spans the row's full content width, 8px (stack.xs)
2590
+ // below. The Banner is a nested action region: its own
2591
+ // controls never commit the row, and the row's hover/press
2592
+ // overlay is suppressed over it (see styles.css).
2593
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "chorus-list__stack", children: [
2594
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "chorus-list__row-main", children: rowMain }),
2595
+ /* @__PURE__ */ jsxRuntime.jsx(
2596
+ "span",
2597
+ {
2598
+ className: "chorus-list__banner",
2599
+ "data-nested-action": "",
2600
+ onClick: (e) => e.stopPropagation(),
2601
+ onKeyDown: (e) => e.stopPropagation(),
2602
+ children: item.banner
2603
+ }
2604
+ )
2605
+ ] })
2606
+ ) : rowMain
2486
2607
  },
2487
2608
  item.value ?? idx
2488
2609
  );
@@ -2742,6 +2863,7 @@ function Dialog({
2742
2863
  const cardRef = react.useRef(null);
2743
2864
  const lastFocusedRef = react.useRef(null);
2744
2865
  useBodyScrollLock(open && !inline);
2866
+ useFocusTrap(cardRef, open && !inline);
2745
2867
  react.useEffect(() => {
2746
2868
  var _a;
2747
2869
  if (!open) return void 0;
@@ -2787,6 +2909,7 @@ function Dialog({
2787
2909
  className: joinClasses("chorus-dialog__card", image && "chorus-dialog__card--with-image"),
2788
2910
  role: "dialog",
2789
2911
  "aria-modal": "true",
2912
+ tabIndex: -1,
2790
2913
  "aria-label": ariaLabel ?? title,
2791
2914
  onClick: (e) => e.stopPropagation(),
2792
2915
  ...rest,
@@ -2820,6 +2943,80 @@ function Divider({
2820
2943
  }
2821
2944
  );
2822
2945
  }
2946
+
2947
+ // ../../schema/components/empty-state/empty-state.spec.json
2948
+ var empty_state_spec_default = {
2949
+ sizing: {
2950
+ illustrationSize: "ref.space.600",
2951
+ illustrationColor: "sys.color.onSurfaceVariant",
2952
+ illustrationGap: "sys.layout.stack.sm",
2953
+ headlineTypo: "sys.typo.heading.sm",
2954
+ headlineColor: "sys.color.onSurface",
2955
+ bodyTypo: "sys.typo.body.sm",
2956
+ bodyColor: "sys.color.onSurfaceVariant",
2957
+ bodyGap: "sys.layout.stack.2xs",
2958
+ actionGap: "sys.layout.stack.md"
2959
+ }};
2960
+ var s = empty_state_spec_default.sizing;
2961
+ var CONTAINER_STYLE = {
2962
+ "--empty-state-illustration-gap": tokenToCss(s.illustrationGap),
2963
+ "--empty-state-body-gap": tokenToCss(s.bodyGap),
2964
+ "--empty-state-action-gap": tokenToCss(s.actionGap)
2965
+ };
2966
+ var ILLUSTRATION_STYLE = {
2967
+ "--empty-state-illustration-size": tokenToCss(s.illustrationSize),
2968
+ "--empty-state-illustration-color": tokenToCss(s.illustrationColor)
2969
+ };
2970
+ var HEADLINE_STYLE = {
2971
+ "--empty-state-headline-color": tokenToCss(s.headlineColor),
2972
+ ...typoStyles(s.headlineTypo)
2973
+ };
2974
+ var BODY_STYLE = {
2975
+ "--empty-state-body-color": tokenToCss(s.bodyColor),
2976
+ ...typoStyles(s.bodyTypo)
2977
+ };
2978
+ function EmptyState({
2979
+ illustration,
2980
+ headline,
2981
+ body,
2982
+ action,
2983
+ className,
2984
+ style,
2985
+ ...rest
2986
+ }) {
2987
+ return /* @__PURE__ */ jsxRuntime.jsxs(
2988
+ "div",
2989
+ {
2990
+ role: "status",
2991
+ className: joinClasses("chorus-empty-state", className),
2992
+ style: { ...CONTAINER_STYLE, ...style },
2993
+ ...rest,
2994
+ children: [
2995
+ illustration ? /* @__PURE__ */ jsxRuntime.jsx(
2996
+ "span",
2997
+ {
2998
+ className: "chorus-empty-state__illustration",
2999
+ "aria-hidden": "true",
3000
+ style: ILLUSTRATION_STYLE,
3001
+ children: illustration
3002
+ }
3003
+ ) : null,
3004
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "chorus-empty-state__headline", style: HEADLINE_STYLE, children: headline }),
3005
+ body ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "chorus-empty-state__body", style: BODY_STYLE, children: body }) : null,
3006
+ action ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "chorus-empty-state__action", children: /* @__PURE__ */ jsxRuntime.jsx(
3007
+ Button,
3008
+ {
3009
+ appearance: "primary",
3010
+ onClick: action.onClick ?? (action.href ? () => {
3011
+ window.location.assign(action.href);
3012
+ } : void 0),
3013
+ children: action.label
3014
+ }
3015
+ ) }) : null
3016
+ ]
3017
+ }
3018
+ );
3019
+ }
2823
3020
  var TabsContext = react.createContext({
2824
3021
  variant: "underline",
2825
3022
  value: null,
@@ -2942,19 +3139,19 @@ function TabsUnderline({ className, style, children, ...rest }) {
2942
3139
  useScrollOverflow(ref);
2943
3140
  useSlidingIndicator(ref, indicatorRef, value);
2944
3141
  useFullBleedGuard(ref, "Tabs");
2945
- const s = underline_spec_default.sizing;
3142
+ const s2 = underline_spec_default.sizing;
2946
3143
  const composedStyle = {
2947
- "--tabs-container-padding-inline": tokenToCss(s.containerPaddingInline),
2948
- "--tabs-tab-min-height": tokenToCss(s.minHeight),
2949
- "--tabs-tab-padding-block": tokenToCss(s.paddingBlock),
2950
- "--tabs-tab-padding-inline": tokenToCss(s.paddingInline),
2951
- "--tabs-inter-tab-gap": tokenToCss(s.interTabGap),
2952
- "--tabs-slot-gap": tokenToCss(s.slotGap),
2953
- "--tabs-icon-size": tokenToCss(s.iconSize),
2954
- "--tabs-indicator-height": tokenToCss(s.indicatorHeight),
2955
- "--tabs-divider-width": tokenToCss(s.dividerWidth),
2956
- "--tabs-divider-color": tokenToCss(s.dividerColor),
2957
- "--tabs-fade-width": tokenToCss(s.fadeWidth),
3144
+ "--tabs-container-padding-inline": tokenToCss(s2.containerPaddingInline),
3145
+ "--tabs-tab-min-height": tokenToCss(s2.minHeight),
3146
+ "--tabs-tab-padding-block": tokenToCss(s2.paddingBlock),
3147
+ "--tabs-tab-padding-inline": tokenToCss(s2.paddingInline),
3148
+ "--tabs-inter-tab-gap": tokenToCss(s2.interTabGap),
3149
+ "--tabs-slot-gap": tokenToCss(s2.slotGap),
3150
+ "--tabs-icon-size": tokenToCss(s2.iconSize),
3151
+ "--tabs-indicator-height": tokenToCss(s2.indicatorHeight),
3152
+ "--tabs-divider-width": tokenToCss(s2.dividerWidth),
3153
+ "--tabs-divider-color": tokenToCss(s2.dividerColor),
3154
+ "--tabs-fade-width": tokenToCss(s2.fadeWidth),
2958
3155
  "--tabs-label-unselected": tokenToCss(underline_spec_default.selectionStates.unselected.label),
2959
3156
  "--tabs-label-selected": tokenToCss(underline_spec_default.selectionStates.selected.label),
2960
3157
  "--tabs-indicator-color": tokenToCss(underline_spec_default.selectionStates.selected.indicator),
@@ -2966,7 +3163,7 @@ function TabsUnderline({ className, style, children, ...rest }) {
2966
3163
  "--tabs-focus-outer-color": tokenToCss(underline_spec_default.focusIndicator.ring.outerColor),
2967
3164
  "--tabs-focus-inset-width": tokenToCss(underline_spec_default.focusIndicator.ring.insetWidth),
2968
3165
  "--tabs-focus-inset-color": tokenToCss(underline_spec_default.focusIndicator.ring.insetColor),
2969
- ...typoStyles(s.labelTypo),
3166
+ ...typoStyles(s2.labelTypo),
2970
3167
  ...style
2971
3168
  };
2972
3169
  return /* @__PURE__ */ jsxRuntime.jsxs(
@@ -3028,12 +3225,12 @@ function TabsSegmented({ className, style, children, ...rest }) {
3028
3225
  const ref = react.useRef(null);
3029
3226
  useScrollOverflow(ref);
3030
3227
  useFullBleedGuard(ref, "Tabs");
3031
- const s = segmented_spec_default.sizing;
3228
+ const s2 = segmented_spec_default.sizing;
3032
3229
  const composedStyle = {
3033
- "--tabs-container-padding-block": tokenToCss(s.containerPaddingBlock),
3034
- "--tabs-container-padding-inline": tokenToCss(s.containerPaddingInline),
3035
- "--tabs-inter-segment-gap": tokenToCss(s.interSegmentGap),
3036
- "--tabs-fade-width": tokenToCss(s.fadeWidth),
3230
+ "--tabs-container-padding-block": tokenToCss(s2.containerPaddingBlock),
3231
+ "--tabs-container-padding-inline": tokenToCss(s2.containerPaddingInline),
3232
+ "--tabs-inter-segment-gap": tokenToCss(s2.interSegmentGap),
3233
+ "--tabs-fade-width": tokenToCss(s2.fadeWidth),
3037
3234
  ...style
3038
3235
  };
3039
3236
  return /* @__PURE__ */ jsxRuntime.jsx(
@@ -4373,12 +4570,19 @@ var input_spec_default = {
4373
4570
  nestedActionScope: "The pressed overlay (and hover stroke) is suppressed while the pointer presses / hovers the trailing clear button \u2014 that '\xD7' is an independent nested action, so the small control owns the state and the large field does not also read as pressed. The visual-state boundary matches the action boundary."
4374
4571
  },
4375
4572
  active: {
4573
+ isFocusState: true,
4376
4574
  overlay: null,
4377
4575
  border: "borderActive",
4378
4576
  strokeWeight: "activeStrokeWeight",
4379
4577
  caret: "visible",
4380
4578
  showsClearWhenValue: true,
4381
- note: "The stroke steps from its rest `hairline` (1px) to `activeStrokeWeight` (2px) and re-tones to `borderActive` \u2014 but it is an inset `box-shadow`, not a `border`, so the box model is untouched: the field's footprint, caret, and text position are pixel-stable as it goes active. Nothing reflows. The clear button is shown only in this state (and only when the value is non-empty)."
4579
+ focusRing: {
4580
+ composition: "outward",
4581
+ layer: "::after overlay \u2014 position:absolute, inset:0, no reflow (DESIGN.md Focus ring composition)",
4582
+ innerCounterRing: { width: "sys.borderWidth.hairline", color: "sys.color.focusInset" },
4583
+ outerRing: { width: "sys.borderWidth.thin", color: "sys.color.focus" }
4584
+ },
4585
+ note: "This IS the field's keyboard/input-focus state \u2014 `active` (caret visible, input engaged) and `:focus-visible` coincide for a text field, so there is no separate `focused` state; the focus ring described here (and in the parallel `focusIndicator` block) is the focus affordance. The stroke steps from its rest `hairline` (1px) to `activeStrokeWeight` (2px) and re-tones to `borderActive` \u2014 but it is an inset `box-shadow`, not a `border`, so the box model is untouched: the field's footprint, caret, and text position are pixel-stable as it goes active. Nothing reflows. The clear button is shown only in this state (and only when the value is non-empty)."
4382
4586
  },
4383
4587
  disabled: {
4384
4588
  overlay: null,
@@ -4560,11 +4764,18 @@ var textarea_spec_default = {
4560
4764
  nestedActionScope: "The pressed overlay (and hover stroke) is suppressed while the pointer presses / hovers the trailing clear button \u2014 that '\xD7' is an independent nested action, so the small control owns the state and the large field does not also read as pressed. The visual-state boundary matches the action boundary."
4561
4765
  },
4562
4766
  active: {
4767
+ isFocusState: true,
4563
4768
  overlay: null,
4564
4769
  border: "borderActive",
4565
4770
  strokeWeight: "activeStrokeWeight",
4566
4771
  caret: "visible",
4567
- note: "Stroke steps from `hairline` (1px) to `activeStrokeWeight` (2px) as an inset box-shadow \u2014 same pixel-stable contract as input."
4772
+ focusRing: {
4773
+ composition: "outward",
4774
+ layer: "::after overlay \u2014 position:absolute, inset:0, no reflow (DESIGN.md Focus ring composition)",
4775
+ innerCounterRing: { width: "sys.borderWidth.hairline", color: "sys.color.focusInset" },
4776
+ outerRing: { width: "sys.borderWidth.thin", color: "sys.color.focus" }
4777
+ },
4778
+ note: "This IS the field's keyboard/input-focus state \u2014 `active` (caret visible, input engaged) and `:focus-visible` coincide for a text field, so there is no separate `focused` state; the focus ring described here (and in the parallel `focusIndicator` block) is the focus affordance. Stroke steps from `hairline` (1px) to `activeStrokeWeight` (2px) as an inset box-shadow \u2014 same pixel-stable contract as input."
4568
4779
  },
4569
4780
  disabled: {
4570
4781
  overlay: null,
@@ -4698,12 +4909,19 @@ var search_spec_default = {
4698
4909
  nestedActionScope: "The pressed overlay (and hover stroke) is suppressed while the pointer presses / hovers the trailing clear button \u2014 that '\xD7' is an independent nested action, so the small control owns the state and the large field does not also read as pressed. The visual-state boundary matches the action boundary."
4699
4910
  },
4700
4911
  active: {
4912
+ isFocusState: true,
4701
4913
  overlay: null,
4702
4914
  border: "borderActive",
4703
4915
  strokeWeight: "activeStrokeWeight",
4704
4916
  caret: "visible",
4705
4917
  showsClearWhenValue: true,
4706
- note: "The stroke steps from its rest `hairline` (1px) to `activeStrokeWeight` (2px) and re-tones to `borderActive` \u2014 but it is an inset `box-shadow`, not a `border`, so the box model is untouched: the field's footprint, caret, and text position are pixel-stable as it goes active. Nothing reflows. The clear button is shown only in this state (and only when the value is non-empty)."
4918
+ focusRing: {
4919
+ composition: "outward",
4920
+ layer: "::after overlay \u2014 position:absolute, inset:0, no reflow (DESIGN.md Focus ring composition)",
4921
+ innerCounterRing: { width: "sys.borderWidth.hairline", color: "sys.color.focusInset" },
4922
+ outerRing: { width: "sys.borderWidth.thin", color: "sys.color.focus" }
4923
+ },
4924
+ note: "This IS the field's keyboard/input-focus state \u2014 `active` (caret visible, input engaged) and `:focus-visible` coincide for a text field, so there is no separate `focused` state; the focus ring described here (and in the parallel `focusIndicator` block) is the focus affordance. The stroke steps from its rest `hairline` (1px) to `activeStrokeWeight` (2px) and re-tones to `borderActive` \u2014 but it is an inset `box-shadow`, not a `border`, so the box model is untouched: the field's footprint, caret, and text position are pixel-stable as it goes active. Nothing reflows. The clear button is shown only in this state (and only when the value is non-empty)."
4707
4925
  },
4708
4926
  disabled: {
4709
4927
  overlay: null,
@@ -4890,9 +5108,17 @@ var select_spec_default = {
4890
5108
  }
4891
5109
  },
4892
5110
  active: {
5111
+ isFocusState: true,
4893
5112
  overlay: null,
4894
5113
  border: "borderActive",
4895
- strokeWeight: "activeStrokeWeight"
5114
+ strokeWeight: "activeStrokeWeight",
5115
+ focusRing: {
5116
+ composition: "outward",
5117
+ layer: "::after overlay \u2014 position:absolute, inset:0, no reflow (DESIGN.md Focus ring composition)",
5118
+ innerCounterRing: { width: "sys.borderWidth.hairline", color: "sys.color.focusInset" },
5119
+ outerRing: { width: "sys.borderWidth.thin", color: "sys.color.focus" }
5120
+ },
5121
+ note: "This IS the trigger's keyboard-focus / open state \u2014 `:focus-visible` and the engaged (open) state coincide for the select trigger, so there is no separate `focused` state; the focus ring described here (and in the parallel `focusIndicator` block) is the focus affordance. The stroke re-tones to `borderActive` at `activeStrokeWeight` (2px) as an inset box-shadow, pixel-stable (no reflow)."
4896
5122
  },
4897
5123
  disabled: {
4898
5124
  overlay: null,
@@ -5513,6 +5739,58 @@ function SkeletonGroup({
5513
5739
  }
5514
5740
  );
5515
5741
  }
5742
+
5743
+ // ../../schema/components/spinner/spinner.spec.json
5744
+ var spinner_spec_default = {
5745
+ sizes: {
5746
+ medium: {
5747
+ diameter: "sys.icon.lg",
5748
+ labelTypo: "sys.typo.body.sm",
5749
+ gap: "sys.layout.inline.sm"
5750
+ },
5751
+ small: {
5752
+ diameter: "sys.icon.md",
5753
+ labelTypo: "sys.typo.body.sm",
5754
+ gap: "sys.layout.inline.sm"
5755
+ }
5756
+ }};
5757
+ function sizingStyle4(spec, size) {
5758
+ const s2 = spec.sizes[size] ?? spec.sizes.medium;
5759
+ return {
5760
+ "--spinner-diameter": tokenToCss(s2.diameter),
5761
+ "--spinner-gap": tokenToCss(s2.gap)
5762
+ };
5763
+ }
5764
+ function Spinner({
5765
+ size = "medium",
5766
+ label,
5767
+ className,
5768
+ style,
5769
+ "aria-label": ariaLabel,
5770
+ ...rest
5771
+ }) {
5772
+ var _a;
5773
+ const labelTypo = ((_a = spinner_spec_default.sizes[size]) == null ? void 0 : _a.labelTypo) ?? spinner_spec_default.sizes.medium.labelTypo;
5774
+ const a11yLabel = label != null ? void 0 : ariaLabel ?? "Loading";
5775
+ return /* @__PURE__ */ jsxRuntime.jsxs(
5776
+ "span",
5777
+ {
5778
+ role: "status",
5779
+ "aria-label": a11yLabel,
5780
+ className: joinClasses(
5781
+ "chorus-spinner",
5782
+ `chorus-spinner--${size}`,
5783
+ className
5784
+ ),
5785
+ style: { ...sizingStyle4(spinner_spec_default, size), ...style },
5786
+ ...rest,
5787
+ children: [
5788
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "chorus-spinner__arc", "aria-hidden": "true" }),
5789
+ label != null ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "chorus-spinner__label", style: typoStyles(labelTypo), children: label }) : null
5790
+ ]
5791
+ }
5792
+ );
5793
+ }
5516
5794
  function StatusTag({
5517
5795
  appearance = "neutral",
5518
5796
  children,
@@ -5918,6 +6196,7 @@ exports.Dialog = Dialog;
5918
6196
  exports.DirectoryList = DirectoryList;
5919
6197
  exports.Divider = Divider;
5920
6198
  exports.Drawer = BottomSheet;
6199
+ exports.EmptyState = EmptyState;
5921
6200
  exports.Feed = Feed;
5922
6201
  exports.FeedAd = FeedAd;
5923
6202
  exports.FeedGroup = FeedGroup;
@@ -5946,6 +6225,7 @@ exports.SideSheet = SideSheet;
5946
6225
  exports.SideSheetGroup = SideSheetGroup;
5947
6226
  exports.Skeleton = Skeleton;
5948
6227
  exports.SkeletonGroup = SkeletonGroup;
6228
+ exports.Spinner = Spinner;
5949
6229
  exports.StatusTag = StatusTag;
5950
6230
  exports.SubHeader = SubHeader;
5951
6231
  exports.SuggestionList = SuggestionList;