@graundtech/fluent2-react-kit 0.5.1

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 (98) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +69 -0
  3. package/dist/components/ui/accordion.d.ts +133 -0
  4. package/dist/components/ui/accordion.js +80 -0
  5. package/dist/components/ui/accordion.js.map +1 -0
  6. package/dist/components/ui/alert.d.ts +106 -0
  7. package/dist/components/ui/alert.js +73 -0
  8. package/dist/components/ui/alert.js.map +1 -0
  9. package/dist/components/ui/avatar.d.ts +42 -0
  10. package/dist/components/ui/avatar.js +55 -0
  11. package/dist/components/ui/avatar.js.map +1 -0
  12. package/dist/components/ui/badge.d.ts +36 -0
  13. package/dist/components/ui/badge.js +81 -0
  14. package/dist/components/ui/badge.js.map +1 -0
  15. package/dist/components/ui/breadcrumb.d.ts +53 -0
  16. package/dist/components/ui/breadcrumb.js +149 -0
  17. package/dist/components/ui/breadcrumb.js.map +1 -0
  18. package/dist/components/ui/button.d.ts +24 -0
  19. package/dist/components/ui/button.js +86 -0
  20. package/dist/components/ui/button.js.map +1 -0
  21. package/dist/components/ui/card.d.ts +51 -0
  22. package/dist/components/ui/card.js +97 -0
  23. package/dist/components/ui/card.js.map +1 -0
  24. package/dist/components/ui/checkbox.d.ts +101 -0
  25. package/dist/components/ui/checkbox.js +57 -0
  26. package/dist/components/ui/checkbox.js.map +1 -0
  27. package/dist/components/ui/combobox.d.ts +212 -0
  28. package/dist/components/ui/combobox.js +236 -0
  29. package/dist/components/ui/combobox.js.map +1 -0
  30. package/dist/components/ui/command.d.ts +187 -0
  31. package/dist/components/ui/command.js +225 -0
  32. package/dist/components/ui/command.js.map +1 -0
  33. package/dist/components/ui/dialog.d.ts +117 -0
  34. package/dist/components/ui/dialog.js +165 -0
  35. package/dist/components/ui/dialog.js.map +1 -0
  36. package/dist/components/ui/dropdown-menu.d.ts +155 -0
  37. package/dist/components/ui/dropdown-menu.js +295 -0
  38. package/dist/components/ui/dropdown-menu.js.map +1 -0
  39. package/dist/components/ui/input.d.ts +44 -0
  40. package/dist/components/ui/input.js +43 -0
  41. package/dist/components/ui/input.js.map +1 -0
  42. package/dist/components/ui/label.d.ts +39 -0
  43. package/dist/components/ui/label.js +30 -0
  44. package/dist/components/ui/label.js.map +1 -0
  45. package/dist/components/ui/link.d.ts +62 -0
  46. package/dist/components/ui/link.js +61 -0
  47. package/dist/components/ui/link.js.map +1 -0
  48. package/dist/components/ui/multi-select.d.ts +115 -0
  49. package/dist/components/ui/multi-select.js +133 -0
  50. package/dist/components/ui/multi-select.js.map +1 -0
  51. package/dist/components/ui/pagination.d.ts +46 -0
  52. package/dist/components/ui/pagination.js +164 -0
  53. package/dist/components/ui/pagination.js.map +1 -0
  54. package/dist/components/ui/popover.d.ts +117 -0
  55. package/dist/components/ui/popover.js +62 -0
  56. package/dist/components/ui/popover.js.map +1 -0
  57. package/dist/components/ui/progress.d.ts +98 -0
  58. package/dist/components/ui/progress.js +67 -0
  59. package/dist/components/ui/progress.js.map +1 -0
  60. package/dist/components/ui/radio-group.d.ts +94 -0
  61. package/dist/components/ui/radio-group.js +65 -0
  62. package/dist/components/ui/radio-group.js.map +1 -0
  63. package/dist/components/ui/select.d.ts +145 -0
  64. package/dist/components/ui/select.js +240 -0
  65. package/dist/components/ui/select.js.map +1 -0
  66. package/dist/components/ui/separator.d.ts +43 -0
  67. package/dist/components/ui/separator.js +27 -0
  68. package/dist/components/ui/separator.js.map +1 -0
  69. package/dist/components/ui/skeleton.d.ts +81 -0
  70. package/dist/components/ui/skeleton.js +24 -0
  71. package/dist/components/ui/skeleton.js.map +1 -0
  72. package/dist/components/ui/spinner.d.ts +56 -0
  73. package/dist/components/ui/spinner.js +83 -0
  74. package/dist/components/ui/spinner.js.map +1 -0
  75. package/dist/components/ui/switch.d.ts +60 -0
  76. package/dist/components/ui/switch.js +63 -0
  77. package/dist/components/ui/switch.js.map +1 -0
  78. package/dist/components/ui/tabs.d.ts +107 -0
  79. package/dist/components/ui/tabs.js +102 -0
  80. package/dist/components/ui/tabs.js.map +1 -0
  81. package/dist/components/ui/textarea.d.ts +28 -0
  82. package/dist/components/ui/textarea.js +36 -0
  83. package/dist/components/ui/textarea.js.map +1 -0
  84. package/dist/components/ui/toast.d.ts +185 -0
  85. package/dist/components/ui/toast.js +181 -0
  86. package/dist/components/ui/toast.js.map +1 -0
  87. package/dist/components/ui/tooltip.d.ts +115 -0
  88. package/dist/components/ui/tooltip.js +72 -0
  89. package/dist/components/ui/tooltip.js.map +1 -0
  90. package/dist/index.d.ts +55 -0
  91. package/dist/index.js +284 -0
  92. package/dist/index.js.map +1 -0
  93. package/dist/lib/utils.d.ts +9 -0
  94. package/dist/lib/utils.js +9 -0
  95. package/dist/lib/utils.js.map +1 -0
  96. package/package.json +77 -0
  97. package/src/styles/tokens.css +596 -0
  98. package/src/styles/tokens.test.ts +216 -0
@@ -0,0 +1,43 @@
1
+ import * as react from 'react';
2
+ import { ComponentProps } from 'react';
3
+
4
+ /**
5
+ * Separator — Fluent 2-styled, shadcn-API separator.
6
+ *
7
+ * A thin 1px Fluent divider (`bg-border`) that separates content
8
+ * horizontally or vertically. Implemented natively with a plain `<div>` — no
9
+ * primitive is needed since the WAI-ARIA "separator" pattern
10
+ * (https://www.w3.org/WAI/ARIA/apg/patterns/separator/) is trivial: a role
11
+ * swap plus an orientation attribute.
12
+ *
13
+ * Stroke color — deliberate token-consistency deviation: Fluent's Divider uses
14
+ * its own stroke token (`#e0e0e0`, grey-88), one ramp step lighter than the
15
+ * generic `--border` (`#d1d1d1`, grey-82) this component reuses. The kit keeps
16
+ * `bg-border` on purpose: a divider sharing the exact border token used by
17
+ * inputs/cards is worth more than a one-step-lighter grey, and `--border` is
18
+ * intentionally NOT re-pointed globally to `#e0e0e0` (that would lighten every
19
+ * input/card border too). Net effect: the divider reads a hair more prominent
20
+ * than Fluent's spec — an accepted, single-step fidelity trade for token
21
+ * cohesion.
22
+ *
23
+ * ARIA:
24
+ * - `decorative` (default `true`) → `role="none"`, removing it from the
25
+ * accessibility tree. `aria-orientation` is withheld in this case too —
26
+ * ARIA forbids relying on global states/properties alongside `role="none"`.
27
+ * - non-decorative → `role="separator"`. `aria-orientation="vertical"` is
28
+ * emitted only for the vertical case; horizontal is the implicit default
29
+ * per the APG, so it is intentionally never set to `"horizontal"`.
30
+ *
31
+ * Sizing follows current shadcn: a single 1px stroke that fills the cross
32
+ * axis, switched by `data-orientation` selectors rather than a cva variant
33
+ * group (there is only one visual treatment, just two axes).
34
+ *
35
+ * Server-safe: no `"use client"`, no hooks — the React import is type-only,
36
+ * so the file can be dropped straight into an RSC tree.
37
+ */
38
+ declare function Separator({ className, orientation, decorative, ...props }: ComponentProps<"div"> & {
39
+ orientation?: "horizontal" | "vertical";
40
+ decorative?: boolean;
41
+ }): react.JSX.Element;
42
+
43
+ export { Separator };
@@ -0,0 +1,27 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { cn } from "../../lib/utils.js";
3
+ function Separator({
4
+ className,
5
+ orientation = "horizontal",
6
+ decorative = true,
7
+ ...props
8
+ }) {
9
+ return /* @__PURE__ */ jsx(
10
+ "div",
11
+ {
12
+ role: decorative ? "none" : "separator",
13
+ "aria-orientation": !decorative && orientation === "vertical" ? "vertical" : void 0,
14
+ "data-slot": "separator",
15
+ "data-orientation": orientation,
16
+ className: cn(
17
+ "shrink-0 bg-border data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
18
+ className
19
+ ),
20
+ ...props
21
+ }
22
+ );
23
+ }
24
+ export {
25
+ Separator
26
+ };
27
+ //# sourceMappingURL=separator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/components/ui/separator.tsx"],"sourcesContent":["import type { ComponentProps } from \"react\";\n\nimport { cn } from \"../../lib/utils\";\n\n/**\n * Separator — Fluent 2-styled, shadcn-API separator.\n *\n * A thin 1px Fluent divider (`bg-border`) that separates content\n * horizontally or vertically. Implemented natively with a plain `<div>` — no\n * primitive is needed since the WAI-ARIA \"separator\" pattern\n * (https://www.w3.org/WAI/ARIA/apg/patterns/separator/) is trivial: a role\n * swap plus an orientation attribute.\n *\n * Stroke color — deliberate token-consistency deviation: Fluent's Divider uses\n * its own stroke token (`#e0e0e0`, grey-88), one ramp step lighter than the\n * generic `--border` (`#d1d1d1`, grey-82) this component reuses. The kit keeps\n * `bg-border` on purpose: a divider sharing the exact border token used by\n * inputs/cards is worth more than a one-step-lighter grey, and `--border` is\n * intentionally NOT re-pointed globally to `#e0e0e0` (that would lighten every\n * input/card border too). Net effect: the divider reads a hair more prominent\n * than Fluent's spec — an accepted, single-step fidelity trade for token\n * cohesion.\n *\n * ARIA:\n * - `decorative` (default `true`) → `role=\"none\"`, removing it from the\n * accessibility tree. `aria-orientation` is withheld in this case too —\n * ARIA forbids relying on global states/properties alongside `role=\"none\"`.\n * - non-decorative → `role=\"separator\"`. `aria-orientation=\"vertical\"` is\n * emitted only for the vertical case; horizontal is the implicit default\n * per the APG, so it is intentionally never set to `\"horizontal\"`.\n *\n * Sizing follows current shadcn: a single 1px stroke that fills the cross\n * axis, switched by `data-orientation` selectors rather than a cva variant\n * group (there is only one visual treatment, just two axes).\n *\n * Server-safe: no `\"use client\"`, no hooks — the React import is type-only,\n * so the file can be dropped straight into an RSC tree.\n */\nfunction Separator({\n className,\n orientation = \"horizontal\",\n decorative = true,\n ...props\n}: ComponentProps<\"div\"> & {\n orientation?: \"horizontal\" | \"vertical\";\n decorative?: boolean;\n}) {\n return (\n <div\n role={decorative ? \"none\" : \"separator\"}\n aria-orientation={\n !decorative && orientation === \"vertical\" ? \"vertical\" : undefined\n }\n data-slot=\"separator\"\n data-orientation={orientation}\n className={cn(\n \"shrink-0 bg-border data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px\",\n className\n )}\n {...props}\n />\n );\n}\n\nexport { Separator };\n"],"mappings":"AAgDI;AA9CJ,SAAS,UAAU;AAoCnB,SAAS,UAAU;AAAA,EACjB;AAAA,EACA,cAAc;AAAA,EACd,aAAa;AAAA,EACb,GAAG;AACL,GAGG;AACD,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAM,aAAa,SAAS;AAAA,MAC5B,oBACE,CAAC,cAAc,gBAAgB,aAAa,aAAa;AAAA,MAE3D,aAAU;AAAA,MACV,oBAAkB;AAAA,MAClB,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;","names":[]}
@@ -0,0 +1,81 @@
1
+ import * as react from 'react';
2
+ import { ComponentProps } from 'react';
3
+
4
+ /**
5
+ * Skeleton — Fluent 2-styled, shadcn-API loading placeholder.
6
+ *
7
+ * A single `<div>` with no `cva` table (conventions §2 allows this for
8
+ * leaf/static components — mirrors `Separator`): there is one visual
9
+ * treatment, and every shape variation (line, avatar circle, button block,
10
+ * card) is just a `className` override on width/height/radius, not a prop.
11
+ *
12
+ * **Fill color — `bg-secondary`, not `bg-accent`.** Current shadcn ships
13
+ * `bg-accent animate-pulse rounded-md`
14
+ * (https://ui.shadcn.com/docs/components/skeleton); this kit deliberately
15
+ * swaps in `bg-secondary` after weighing all three themes against `tokens.css`:
16
+ * - Dark theme: `--accent` and `--secondary` are the *same* hex (`#333333`,
17
+ * grey[20]) — no visual difference at all.
18
+ * - Light theme: `--secondary` is grey[96] (`#f5f5f5`) vs. `--accent`'s
19
+ * grey[94] (`#f0f0f0`) — a hair lighter, i.e. marginally less contrast
20
+ * against the white `--background`. The delta (96 vs 94) is negligible; a
21
+ * skeleton still reads clearly as "content pending" at either value.
22
+ * - High contrast (`.high-contrast`): the deciding case. `--accent` maps to
23
+ * the system `Highlight` keyword (selection semantics), so an inert
24
+ * placeholder painted in `bg-accent` lights up in the OS *selection* color
25
+ * and reads as "this is selected" — wrong. `--secondary` maps to
26
+ * `ButtonFace`, a neutral surface, so `bg-secondary` stays a plain neutral
27
+ * block in HC. A component can't theme-scope its own class (there's no
28
+ * `high-contrast:` variant to swap the fill only in that theme), so the
29
+ * right call is the token that stays neutral in *all three* themes —
30
+ * `--secondary`, not `--accent`.
31
+ * `bg-muted` was also considered and rejected: it matches `--secondary` in
32
+ * light mode but drops to grey[12] (`#1f1f1f`) in dark mode — visibly
33
+ * dimmer than the `#242424` `--background` it sits on, i.e. *less* visible
34
+ * exactly where contrast matters most.
35
+ *
36
+ * **Animation — the Fluent wave shimmer (`animate-shimmer`).** Fluent 2's
37
+ * Skeleton uses a moving gradient "wave" sweep
38
+ * (https://storybooks.fluentui.dev/react/?path=/docs/components-skeleton--docs),
39
+ * not shadcn's opacity pulse. The keyframes live in the token layer
40
+ * (`tokens.css` `--animate-shimmer`, sweeping `background-position` across a
41
+ * 200%-wide gradient); this file paints the gradient — `--secondary` base
42
+ * with an `--input`-grey crest, both theme-aware (the secondary/accent pair
43
+ * was rejected for the crest because those two are the SAME hex in dark
44
+ * mode, which would erase the wave exactly where it's hardest to see).
45
+ *
46
+ * **Reduced motion — `motion-reduce:animate-none`, full stop (not a slowed
47
+ * animation).** This deliberately *contrasts* with `Spinner`, which slows to
48
+ * `animation-duration: 3s` under `prefers-reduced-motion` instead of
49
+ * stopping outright. The difference is what each animation communicates:
50
+ * `Spinner`'s spin is the *only* signal that a background operation is still
51
+ * in progress — freezing it would erase that information for a
52
+ * reduced-motion user (its `role="status"` region only helps assistive tech,
53
+ * not sighted users). `Skeleton` carries no such information: it is a static
54
+ * placeholder shape whether or not it pulses, and content either has loaded
55
+ * or hasn't — nothing is lost by holding it still, and a persistently
56
+ * animating region is exactly what `prefers-reduced-motion` asks apps to
57
+ * avoid. `motion-reduce:animate-none` is a plain Tailwind utility, not a
58
+ * `duration-*` token, so it isn't covered by the automatic duration collapse
59
+ * `tokens.css` already applies — it has to be declared per animated
60
+ * component (see `Spinner`'s doc comment for the same note).
61
+ *
62
+ * **Accessibility — no `aria-hidden` here, by design.** A lone `<Skeleton>`
63
+ * is inert markup (a styled `<div>`, no text, no role) that assistive tech
64
+ * already has nothing to announce for, so forcing `aria-hidden="true"` on
65
+ * every instance would be redundant — and current shadcn doesn't do it
66
+ * either. Instead, wrap the *loading region* (the parent containing one or
67
+ * more `Skeleton`s standing in for real content) in `aria-busy="true"` —
68
+ * optionally with `aria-live="polite"` if the region should announce when
69
+ * loading completes and real content replaces the skeletons. That's a
70
+ * consumer-side decision (it depends on what the region becomes once
71
+ * loaded), so it isn't baked into this component. Verified with axe: a
72
+ * skeleton "card" composition (avatar circle + text lines) inside an
73
+ * `aria-busy="true"` wrapper produces no violations — see
74
+ * `skeleton.test.tsx`.
75
+ *
76
+ * Server-safe: no `"use client"`, no hooks — the React import is type-only,
77
+ * so this drops straight into an RSC tree.
78
+ */
79
+ declare function Skeleton({ className, ...props }: ComponentProps<"div">): react.JSX.Element;
80
+
81
+ export { Skeleton };
@@ -0,0 +1,24 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { cn } from "../../lib/utils.js";
3
+ function Skeleton({ className, ...props }) {
4
+ return /* @__PURE__ */ jsx(
5
+ "div",
6
+ {
7
+ "data-slot": "skeleton",
8
+ className: cn(
9
+ // Fluent wave: a 200%-wide gradient (secondary base, input-grey crest)
10
+ // swept by the token-layer shimmer keyframes. bg-secondary stays as
11
+ // the fallback fill (and the forced-colors surface, where gradients
12
+ // are stripped by forced-color-adjust).
13
+ "animate-shimmer rounded-md bg-secondary motion-reduce:animate-none",
14
+ "bg-[linear-gradient(90deg,var(--secondary)_25%,var(--input)_50%,var(--secondary)_75%)] bg-[length:200%_100%]",
15
+ className
16
+ ),
17
+ ...props
18
+ }
19
+ );
20
+ }
21
+ export {
22
+ Skeleton
23
+ };
24
+ //# sourceMappingURL=skeleton.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/components/ui/skeleton.tsx"],"sourcesContent":["import type { ComponentProps } from \"react\";\n\nimport { cn } from \"../../lib/utils\";\n\n/**\n * Skeleton — Fluent 2-styled, shadcn-API loading placeholder.\n *\n * A single `<div>` with no `cva` table (conventions §2 allows this for\n * leaf/static components — mirrors `Separator`): there is one visual\n * treatment, and every shape variation (line, avatar circle, button block,\n * card) is just a `className` override on width/height/radius, not a prop.\n *\n * **Fill color — `bg-secondary`, not `bg-accent`.** Current shadcn ships\n * `bg-accent animate-pulse rounded-md`\n * (https://ui.shadcn.com/docs/components/skeleton); this kit deliberately\n * swaps in `bg-secondary` after weighing all three themes against `tokens.css`:\n * - Dark theme: `--accent` and `--secondary` are the *same* hex (`#333333`,\n * grey[20]) — no visual difference at all.\n * - Light theme: `--secondary` is grey[96] (`#f5f5f5`) vs. `--accent`'s\n * grey[94] (`#f0f0f0`) — a hair lighter, i.e. marginally less contrast\n * against the white `--background`. The delta (96 vs 94) is negligible; a\n * skeleton still reads clearly as \"content pending\" at either value.\n * - High contrast (`.high-contrast`): the deciding case. `--accent` maps to\n * the system `Highlight` keyword (selection semantics), so an inert\n * placeholder painted in `bg-accent` lights up in the OS *selection* color\n * and reads as \"this is selected\" — wrong. `--secondary` maps to\n * `ButtonFace`, a neutral surface, so `bg-secondary` stays a plain neutral\n * block in HC. A component can't theme-scope its own class (there's no\n * `high-contrast:` variant to swap the fill only in that theme), so the\n * right call is the token that stays neutral in *all three* themes —\n * `--secondary`, not `--accent`.\n * `bg-muted` was also considered and rejected: it matches `--secondary` in\n * light mode but drops to grey[12] (`#1f1f1f`) in dark mode — visibly\n * dimmer than the `#242424` `--background` it sits on, i.e. *less* visible\n * exactly where contrast matters most.\n *\n * **Animation — the Fluent wave shimmer (`animate-shimmer`).** Fluent 2's\n * Skeleton uses a moving gradient \"wave\" sweep\n * (https://storybooks.fluentui.dev/react/?path=/docs/components-skeleton--docs),\n * not shadcn's opacity pulse. The keyframes live in the token layer\n * (`tokens.css` `--animate-shimmer`, sweeping `background-position` across a\n * 200%-wide gradient); this file paints the gradient — `--secondary` base\n * with an `--input`-grey crest, both theme-aware (the secondary/accent pair\n * was rejected for the crest because those two are the SAME hex in dark\n * mode, which would erase the wave exactly where it's hardest to see).\n *\n * **Reduced motion — `motion-reduce:animate-none`, full stop (not a slowed\n * animation).** This deliberately *contrasts* with `Spinner`, which slows to\n * `animation-duration: 3s` under `prefers-reduced-motion` instead of\n * stopping outright. The difference is what each animation communicates:\n * `Spinner`'s spin is the *only* signal that a background operation is still\n * in progress — freezing it would erase that information for a\n * reduced-motion user (its `role=\"status\"` region only helps assistive tech,\n * not sighted users). `Skeleton` carries no such information: it is a static\n * placeholder shape whether or not it pulses, and content either has loaded\n * or hasn't — nothing is lost by holding it still, and a persistently\n * animating region is exactly what `prefers-reduced-motion` asks apps to\n * avoid. `motion-reduce:animate-none` is a plain Tailwind utility, not a\n * `duration-*` token, so it isn't covered by the automatic duration collapse\n * `tokens.css` already applies — it has to be declared per animated\n * component (see `Spinner`'s doc comment for the same note).\n *\n * **Accessibility — no `aria-hidden` here, by design.** A lone `<Skeleton>`\n * is inert markup (a styled `<div>`, no text, no role) that assistive tech\n * already has nothing to announce for, so forcing `aria-hidden=\"true\"` on\n * every instance would be redundant — and current shadcn doesn't do it\n * either. Instead, wrap the *loading region* (the parent containing one or\n * more `Skeleton`s standing in for real content) in `aria-busy=\"true\"` —\n * optionally with `aria-live=\"polite\"` if the region should announce when\n * loading completes and real content replaces the skeletons. That's a\n * consumer-side decision (it depends on what the region becomes once\n * loaded), so it isn't baked into this component. Verified with axe: a\n * skeleton \"card\" composition (avatar circle + text lines) inside an\n * `aria-busy=\"true\"` wrapper produces no violations — see\n * `skeleton.test.tsx`.\n *\n * Server-safe: no `\"use client\"`, no hooks — the React import is type-only,\n * so this drops straight into an RSC tree.\n */\nfunction Skeleton({ className, ...props }: ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"skeleton\"\n className={cn(\n // Fluent wave: a 200%-wide gradient (secondary base, input-grey crest)\n // swept by the token-layer shimmer keyframes. bg-secondary stays as\n // the fallback fill (and the forced-colors surface, where gradients\n // are stripped by forced-color-adjust).\n \"animate-shimmer rounded-md bg-secondary motion-reduce:animate-none\",\n \"bg-[linear-gradient(90deg,var(--secondary)_25%,var(--input)_50%,var(--secondary)_75%)] bg-[length:200%_100%]\",\n className\n )}\n {...props}\n />\n );\n}\n\nexport { Skeleton };\n"],"mappings":"AAiFI;AA/EJ,SAAS,UAAU;AA6EnB,SAAS,SAAS,EAAE,WAAW,GAAG,MAAM,GAA0B;AAChE,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA,QAKT;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;","names":[]}
@@ -0,0 +1,56 @@
1
+ import * as react from 'react';
2
+ import { ComponentProps } from 'react';
3
+ import * as class_variance_authority_types from 'class-variance-authority/types';
4
+ import { VariantProps } from 'class-variance-authority';
5
+
6
+ /**
7
+ * Spinner — Fluent 2-styled, shadcn-API loading indicator.
8
+ *
9
+ * Fluent 2's spinner is a rotating brand-colored ARC (not a full ring). It is
10
+ * drawn as an inline SVG: a faint full-circle "track" plus a shorter,
11
+ * round-capped arc in `text-primary` (brand). The whole SVG spins via
12
+ * `animate-spin`; because only the arc has visible stroke, rotating the
13
+ * (rotationally symmetric) track alongside it is visually inert — only the arc
14
+ * appears to move.
15
+ *
16
+ * Track color: Fluent's Primary spinner track is `BrandStroke2Contrast.Rest`
17
+ * = `#b4d6fa`, which is this kit's `--brand-140` exactly — a pale *blue* tint,
18
+ * not the neutral grey an earlier version used. So the track is
19
+ * `text-brand-140`. Dark: the Figma file had only a light frame, so the dark
20
+ * pairing is derived — `dark:text-brand-40` (a low, subtle brand stop) keeps
21
+ * the track a faint blue that reads clearly *behind* the arc without colliding
22
+ * with the arc's own dark color (`--primary` = `brand-70` in dark).
23
+ *
24
+ * Sizes map 1:1 onto Tailwind's `size-*` scale so the numbers in the spec
25
+ * line up exactly: sm=16px (`size-4`), default=24px (`size-6`), lg=32px
26
+ * (`size-8`), xl=40px (`size-10`).
27
+ *
28
+ * Labeled spinner: when `label` is set, Fluent renders visible text next to
29
+ * the glyph (default `labelPosition="after"`) — reproduced here as
30
+ * `text-sm text-foreground` to the right of the arc via `gap-2`. The label
31
+ * text also becomes the accessible name (see a11y notes on `Spinner` below).
32
+ *
33
+ * Reduced motion: `animate-spin` is a CSS animation, not a token-driven
34
+ * transition, so it does not get the automatic duration collapse tokens.css
35
+ * applies to `duration-*` utilities. Per spec we deliberately slow rather than
36
+ * stop the spin under `prefers-reduced-motion` — `motion-reduce:[animation-duration:3s]`
37
+ * — because hiding the animation entirely would remove the only visual signal
38
+ * that progress is still happening (the wrapper's `role="status"` covers the
39
+ * non-visual signal either way).
40
+ *
41
+ * Server-safe: no `"use client"`, no hooks/handlers — the React import is
42
+ * type-only and the animation is pure CSS, so this drops straight into an RSC
43
+ * tree.
44
+ */
45
+ declare const spinnerVariants: (props?: ({
46
+ size?: "default" | "sm" | "lg" | "xl" | null | undefined;
47
+ } & class_variance_authority_types.ClassProp) | undefined) => string;
48
+ declare function Spinner({ className, size, label, "aria-label": ariaLabel, ...props }: ComponentProps<"span"> & VariantProps<typeof spinnerVariants> & {
49
+ /**
50
+ * Visible text rendered beside the arc (Fluent's "labeled spinner").
51
+ * When set, this text is also the element's accessible name.
52
+ */
53
+ label?: string;
54
+ }): react.JSX.Element;
55
+
56
+ export { Spinner, spinnerVariants };
@@ -0,0 +1,83 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { cva } from "class-variance-authority";
3
+ import { cn } from "../../lib/utils.js";
4
+ const spinnerVariants = cva(
5
+ ["shrink-0", "animate-spin", "motion-reduce:[animation-duration:3s]"],
6
+ {
7
+ variants: {
8
+ size: {
9
+ sm: "size-4",
10
+ default: "size-6",
11
+ lg: "size-8",
12
+ xl: "size-10"
13
+ }
14
+ },
15
+ defaultVariants: {
16
+ size: "default"
17
+ }
18
+ }
19
+ );
20
+ function Spinner({
21
+ className,
22
+ size = "default",
23
+ label,
24
+ "aria-label": ariaLabel,
25
+ ...props
26
+ }) {
27
+ const accessibleName = label || ariaLabel || "Loading";
28
+ return /* @__PURE__ */ jsxs(
29
+ "span",
30
+ {
31
+ role: "status",
32
+ "aria-label": accessibleName,
33
+ "data-slot": "spinner",
34
+ "data-size": size,
35
+ className: cn("inline-flex items-center gap-2", className),
36
+ ...props,
37
+ children: [
38
+ /* @__PURE__ */ jsxs(
39
+ "svg",
40
+ {
41
+ viewBox: "0 0 16 16",
42
+ fill: "none",
43
+ "aria-hidden": "true",
44
+ className: spinnerVariants({ size }),
45
+ children: [
46
+ /* @__PURE__ */ jsx(
47
+ "circle",
48
+ {
49
+ cx: "8",
50
+ cy: "8",
51
+ r: "7",
52
+ stroke: "currentColor",
53
+ strokeWidth: "2",
54
+ className: "text-brand-140 dark:text-brand-40"
55
+ }
56
+ ),
57
+ /* @__PURE__ */ jsx(
58
+ "circle",
59
+ {
60
+ cx: "8",
61
+ cy: "8",
62
+ r: "7",
63
+ stroke: "currentColor",
64
+ strokeWidth: "2",
65
+ strokeLinecap: "round",
66
+ pathLength: "100",
67
+ strokeDasharray: "28 72",
68
+ className: "text-primary"
69
+ }
70
+ )
71
+ ]
72
+ }
73
+ ),
74
+ label ? /* @__PURE__ */ jsx("span", { className: "text-sm text-foreground", children: label }) : null
75
+ ]
76
+ }
77
+ );
78
+ }
79
+ export {
80
+ Spinner,
81
+ spinnerVariants
82
+ };
83
+ //# sourceMappingURL=spinner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/components/ui/spinner.tsx"],"sourcesContent":["import { cva, type VariantProps } from \"class-variance-authority\";\nimport type { ComponentProps } from \"react\";\n\nimport { cn } from \"../../lib/utils\";\n\n/**\n * Spinner — Fluent 2-styled, shadcn-API loading indicator.\n *\n * Fluent 2's spinner is a rotating brand-colored ARC (not a full ring). It is\n * drawn as an inline SVG: a faint full-circle \"track\" plus a shorter,\n * round-capped arc in `text-primary` (brand). The whole SVG spins via\n * `animate-spin`; because only the arc has visible stroke, rotating the\n * (rotationally symmetric) track alongside it is visually inert — only the arc\n * appears to move.\n *\n * Track color: Fluent's Primary spinner track is `BrandStroke2Contrast.Rest`\n * = `#b4d6fa`, which is this kit's `--brand-140` exactly — a pale *blue* tint,\n * not the neutral grey an earlier version used. So the track is\n * `text-brand-140`. Dark: the Figma file had only a light frame, so the dark\n * pairing is derived — `dark:text-brand-40` (a low, subtle brand stop) keeps\n * the track a faint blue that reads clearly *behind* the arc without colliding\n * with the arc's own dark color (`--primary` = `brand-70` in dark).\n *\n * Sizes map 1:1 onto Tailwind's `size-*` scale so the numbers in the spec\n * line up exactly: sm=16px (`size-4`), default=24px (`size-6`), lg=32px\n * (`size-8`), xl=40px (`size-10`).\n *\n * Labeled spinner: when `label` is set, Fluent renders visible text next to\n * the glyph (default `labelPosition=\"after\"`) — reproduced here as\n * `text-sm text-foreground` to the right of the arc via `gap-2`. The label\n * text also becomes the accessible name (see a11y notes on `Spinner` below).\n *\n * Reduced motion: `animate-spin` is a CSS animation, not a token-driven\n * transition, so it does not get the automatic duration collapse tokens.css\n * applies to `duration-*` utilities. Per spec we deliberately slow rather than\n * stop the spin under `prefers-reduced-motion` — `motion-reduce:[animation-duration:3s]`\n * — because hiding the animation entirely would remove the only visual signal\n * that progress is still happening (the wrapper's `role=\"status\"` covers the\n * non-visual signal either way).\n *\n * Server-safe: no `\"use client\"`, no hooks/handlers — the React import is\n * type-only and the animation is pure CSS, so this drops straight into an RSC\n * tree.\n */\nconst spinnerVariants = cva(\n [\"shrink-0\", \"animate-spin\", \"motion-reduce:[animation-duration:3s]\"],\n {\n variants: {\n size: {\n sm: \"size-4\",\n default: \"size-6\",\n lg: \"size-8\",\n xl: \"size-10\",\n },\n },\n defaultVariants: {\n size: \"default\",\n },\n }\n);\n\nfunction Spinner({\n className,\n size = \"default\",\n label,\n \"aria-label\": ariaLabel,\n ...props\n}: ComponentProps<\"span\"> &\n VariantProps<typeof spinnerVariants> & {\n /**\n * Visible text rendered beside the arc (Fluent's \"labeled spinner\").\n * When set, this text is also the element's accessible name.\n */\n label?: string;\n }) {\n // Accessible name precedence: visible label > explicit aria-label override\n // > default \"Loading\". `aria-label` is pulled out of the rest spread so it\n // can't be set twice on the root element. Use `||` (not `??`) so an *empty*\n // string falls through to the next option: an empty `aria-label` is ignored\n // by the accessible-name computation, which would leave the role=status\n // region unnamed — `label=\"\"` must still resolve to \"Loading\".\n const accessibleName = label || ariaLabel || \"Loading\";\n\n return (\n <span\n role=\"status\"\n aria-label={accessibleName}\n data-slot=\"spinner\"\n data-size={size}\n className={cn(\"inline-flex items-center gap-2\", className)}\n {...props}\n >\n <svg\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n aria-hidden=\"true\"\n className={spinnerVariants({ size })}\n >\n {/* faint full-circle track — Fluent BrandStroke2Contrast (#b4d6fa =\n brand-140); dark derived to a low brand stop (see doc comment) */}\n <circle\n cx=\"8\"\n cy=\"8\"\n r=\"7\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n className=\"text-brand-140 dark:text-brand-40\"\n />\n {/* rotating brand arc — ~100° of the circle, round-capped */}\n <circle\n cx=\"8\"\n cy=\"8\"\n r=\"7\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n pathLength=\"100\"\n strokeDasharray=\"28 72\"\n className=\"text-primary\"\n />\n </svg>\n {label ? <span className=\"text-sm text-foreground\">{label}</span> : null}\n </span>\n );\n}\n\nexport { Spinner, spinnerVariants };\n"],"mappings":"AA4FM,SAQE,KARF;AA5FN,SAAS,WAA8B;AAGvC,SAAS,UAAU;AAyCnB,MAAM,kBAAkB;AAAA,EACtB,CAAC,YAAY,gBAAgB,uCAAuC;AAAA,EACpE;AAAA,IACE,UAAU;AAAA,MACR,MAAM;AAAA,QACJ,IAAI;AAAA,QACJ,SAAS;AAAA,QACT,IAAI;AAAA,QACJ,IAAI;AAAA,MACN;AAAA,IACF;AAAA,IACA,iBAAiB;AAAA,MACf,MAAM;AAAA,IACR;AAAA,EACF;AACF;AAEA,SAAS,QAAQ;AAAA,EACf;AAAA,EACA,OAAO;AAAA,EACP;AAAA,EACA,cAAc;AAAA,EACd,GAAG;AACL,GAOK;AAOH,QAAM,iBAAiB,SAAS,aAAa;AAE7C,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,cAAY;AAAA,MACZ,aAAU;AAAA,MACV,aAAW;AAAA,MACX,WAAW,GAAG,kCAAkC,SAAS;AAAA,MACxD,GAAG;AAAA,MAEJ;AAAA;AAAA,UAAC;AAAA;AAAA,YACC,SAAQ;AAAA,YACR,MAAK;AAAA,YACL,eAAY;AAAA,YACZ,WAAW,gBAAgB,EAAE,KAAK,CAAC;AAAA,YAInC;AAAA;AAAA,gBAAC;AAAA;AAAA,kBACC,IAAG;AAAA,kBACH,IAAG;AAAA,kBACH,GAAE;AAAA,kBACF,QAAO;AAAA,kBACP,aAAY;AAAA,kBACZ,WAAU;AAAA;AAAA,cACZ;AAAA,cAEA;AAAA,gBAAC;AAAA;AAAA,kBACC,IAAG;AAAA,kBACH,IAAG;AAAA,kBACH,GAAE;AAAA,kBACF,QAAO;AAAA,kBACP,aAAY;AAAA,kBACZ,eAAc;AAAA,kBACd,YAAW;AAAA,kBACX,iBAAgB;AAAA,kBAChB,WAAU;AAAA;AAAA,cACZ;AAAA;AAAA;AAAA,QACF;AAAA,QACC,QAAQ,oBAAC,UAAK,WAAU,2BAA2B,iBAAM,IAAU;AAAA;AAAA;AAAA,EACtE;AAEJ;","names":[]}
@@ -0,0 +1,60 @@
1
+ import * as react from 'react';
2
+ import { ComponentProps } from 'react';
3
+ import { Switch as Switch$1 } from '@base-ui/react/switch';
4
+
5
+ /**
6
+ * Switch — Fluent 2-styled, shadcn-API on/off toggle.
7
+ *
8
+ * This is a Base UI case (conventions §9): checked/unchecked state, the
9
+ * hidden `<input type="checkbox">` + form bridge, and the `role="switch"` +
10
+ * `aria-checked` wiring are genuine interactive behavior plain markup can't
11
+ * express, so this wraps `@base-ui/react/switch`'s `Switch.Root`/
12
+ * `Switch.Thumb` (imported as the `Switch` namespace from the
13
+ * `@base-ui/react/switch` subpath — that module is literally
14
+ * `export * as Switch from "./index.parts.js"`, matching Avatar's precedent)
15
+ * instead of a raw `<button>`/`<span>` pair. `Switch.Root` renders a `<span
16
+ * role="switch">` (not a native `<button>`/`<input>`) plus a visually-hidden
17
+ * `<input type="checkbox">` for form submission.
18
+ *
19
+ * `"use client"` is intentionally omitted, same reasoning as `avatar.tsx`:
20
+ * `SwitchRoot`/`SwitchThumb` each carry their own `'use client'` directive at
21
+ * the source level (verified in the compiled package output under
22
+ * `@base-ui/react/switch/root/SwitchRoot.mjs` and `.../thumb/SwitchThumb.mjs`),
23
+ * so they are already client boundaries on their own. This wrapper has no
24
+ * hooks/handlers of its own — it only forwards props into those parts — so a
25
+ * Server Component tree can render it unchanged (a Server Component may
26
+ * render an already-client child without re-declaring the directive itself).
27
+ * Confirmed via `pnpm typecheck` + the test suite; add the directive here
28
+ * only if that stops holding true.
29
+ *
30
+ * Aesthetics follow Fluent 2's Toggle Switch (verified against the live
31
+ * Storybook at storybooks.fluentui.dev, DOM-inspected directly since the
32
+ * docs page itself carries no static spec text): a 40x20px (`h-5 w-10`) pill
33
+ * track with a circular thumb — Fluent's own thumb measures ~18px against an
34
+ * 18px content box (nearly flush); this kit intentionally uses the smaller
35
+ * 14px (`size-3.5`) thumb specified for this component, leaving a visible
36
+ * ~2px gap, which reads cleanly at this track size. Fluent's *unchecked*
37
+ * track is outlined + transparent (measured: 1px solid `#616161`, background
38
+ * transparent) — NOT gray-filled like shadcn's default Radix implementation
39
+ * (`data-[state=unchecked]:bg-input`). This kit now binds that outline to the
40
+ * `NeutralStrokeAccessible` ramp (`border-stroke-accessible` = `#616161` rest,
41
+ * `#575757` hover, `#4d4d4d` pressed): spec-true to the measured Fluent value,
42
+ * so this is no longer the earlier `border-input` (`#d1d1d1`) substitution —
43
+ * that documented deviation is retired. The *checked* track measured exactly
44
+ * `#0f6cbd` — this kit's `--primary`/`bg-primary` token — with a white thumb
45
+ * (`#ffffff`, this kit's `--primary-foreground` token in both themes); on
46
+ * hover/press the on-track steps through Fluent's `CompoundBrandBackground`
47
+ * (border+fill `brand-70`/`brand-60`; dark hover `brand-80`), mirroring
48
+ * Button's per-theme brand ramp (conventions §4).
49
+ *
50
+ * The prop surface (`Switch`, `checked`/`defaultChecked`/`onCheckedChange`,
51
+ * no `size` prop) follows the classic shadcn/ui switch API. Note Base UI's
52
+ * state data-attributes differ from Radix's: it emits presence-only
53
+ * `data-checked`/`data-unchecked` (and `data-disabled`/`data-readonly`/
54
+ * `data-required`), not Radix's `data-state="checked" | "unchecked"` — the
55
+ * classes below target Base UI's actual output via Tailwind's presence-based
56
+ * `data-[checked]:` variant.
57
+ */
58
+ declare function Switch({ className, ...props }: ComponentProps<typeof Switch$1.Root>): react.JSX.Element;
59
+
60
+ export { Switch };
@@ -0,0 +1,63 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { Switch as SwitchPrimitive } from "@base-ui/react/switch";
3
+ import { cn } from "../../lib/utils.js";
4
+ function Switch({
5
+ className,
6
+ ...props
7
+ }) {
8
+ return /* @__PURE__ */ jsx(
9
+ SwitchPrimitive.Root,
10
+ {
11
+ "data-slot": "switch",
12
+ className: cn(
13
+ // layout — Fluent medium toggle: 40x20 pill track, thumb centered via flex
14
+ "inline-flex h-5 w-10 shrink-0 cursor-pointer items-center rounded-full border px-0.5",
15
+ // motion — border/bg color transition on check/uncheck
16
+ "outline-none transition-colors duration-fast ease-ease",
17
+ // off (unchecked) — Fluent's outlined/transparent track, bound to the
18
+ // NeutralStrokeAccessible ramp (spec-true measured #616161 rest ->
19
+ // #575757 hover -> #4d4d4d pressed), see file header.
20
+ "border-stroke-accessible bg-transparent hover:border-stroke-accessible-hover active:border-stroke-accessible-pressed",
21
+ // on (checked) — brand fill (Fluent CompoundBrandBackground), stepping
22
+ // border+fill through Hover/Pressed; dark hover brightens to brand-80.
23
+ // The data-[checked]:hover/active selectors outrank the neutral
24
+ // hover/active border by specificity so the on-track stays brand.
25
+ "data-[checked]:border-primary data-[checked]:bg-primary data-[checked]:hover:border-brand-70 data-[checked]:hover:bg-brand-70 data-[checked]:active:border-brand-60 data-[checked]:active:bg-brand-60 dark:data-[checked]:hover:border-brand-80 dark:data-[checked]:hover:bg-brand-80",
26
+ // focus — Fluent double-stroke approximation (conventions §4)
27
+ "focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
28
+ // invalid — shadcn aria-invalid treatment (conventions §4); harmless
29
+ // unless the caller pairs this with a Field/validation integration
30
+ "aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
31
+ // disabled — opacity read (conventions §4), keyed off Base UI's
32
+ // `data-disabled` rather than the CSS `disabled:` pseudo-class: this
33
+ // control renders a <span role="switch">, not a native
34
+ // <button>/<input>, so `:disabled` never matches it.
35
+ "data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
36
+ className
37
+ ),
38
+ ...props,
39
+ children: /* @__PURE__ */ jsx(
40
+ SwitchPrimitive.Thumb,
41
+ {
42
+ "data-slot": "switch-thumb",
43
+ className: cn(
44
+ "pointer-events-none block size-3.5 rounded-full",
45
+ "transition-transform duration-fast ease-ease",
46
+ // off — neutral thumb, matches Fluent's measured `#616161`
47
+ "bg-muted-foreground",
48
+ // on — white thumb via the `primary-foreground` token (`#ffffff`
49
+ // in both themes) rather than a literal `bg-white`, per
50
+ // conventions §3.7 (never hardcode a color). Travel distance is
51
+ // the track's content-box width (40 - 2×1px border - 2×2px
52
+ // padding = 34px) minus the 14px thumb = 20px = `translate-x-5`.
53
+ "data-[checked]:translate-x-5 data-[checked]:bg-primary-foreground"
54
+ )
55
+ }
56
+ )
57
+ }
58
+ );
59
+ }
60
+ export {
61
+ Switch
62
+ };
63
+ //# sourceMappingURL=switch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/components/ui/switch.tsx"],"sourcesContent":["import { Switch as SwitchPrimitive } from \"@base-ui/react/switch\";\nimport type { ComponentProps } from \"react\";\n\nimport { cn } from \"../../lib/utils\";\n\n/**\n * Switch — Fluent 2-styled, shadcn-API on/off toggle.\n *\n * This is a Base UI case (conventions §9): checked/unchecked state, the\n * hidden `<input type=\"checkbox\">` + form bridge, and the `role=\"switch\"` +\n * `aria-checked` wiring are genuine interactive behavior plain markup can't\n * express, so this wraps `@base-ui/react/switch`'s `Switch.Root`/\n * `Switch.Thumb` (imported as the `Switch` namespace from the\n * `@base-ui/react/switch` subpath — that module is literally\n * `export * as Switch from \"./index.parts.js\"`, matching Avatar's precedent)\n * instead of a raw `<button>`/`<span>` pair. `Switch.Root` renders a `<span\n * role=\"switch\">` (not a native `<button>`/`<input>`) plus a visually-hidden\n * `<input type=\"checkbox\">` for form submission.\n *\n * `\"use client\"` is intentionally omitted, same reasoning as `avatar.tsx`:\n * `SwitchRoot`/`SwitchThumb` each carry their own `'use client'` directive at\n * the source level (verified in the compiled package output under\n * `@base-ui/react/switch/root/SwitchRoot.mjs` and `.../thumb/SwitchThumb.mjs`),\n * so they are already client boundaries on their own. This wrapper has no\n * hooks/handlers of its own — it only forwards props into those parts — so a\n * Server Component tree can render it unchanged (a Server Component may\n * render an already-client child without re-declaring the directive itself).\n * Confirmed via `pnpm typecheck` + the test suite; add the directive here\n * only if that stops holding true.\n *\n * Aesthetics follow Fluent 2's Toggle Switch (verified against the live\n * Storybook at storybooks.fluentui.dev, DOM-inspected directly since the\n * docs page itself carries no static spec text): a 40x20px (`h-5 w-10`) pill\n * track with a circular thumb — Fluent's own thumb measures ~18px against an\n * 18px content box (nearly flush); this kit intentionally uses the smaller\n * 14px (`size-3.5`) thumb specified for this component, leaving a visible\n * ~2px gap, which reads cleanly at this track size. Fluent's *unchecked*\n * track is outlined + transparent (measured: 1px solid `#616161`, background\n * transparent) — NOT gray-filled like shadcn's default Radix implementation\n * (`data-[state=unchecked]:bg-input`). This kit now binds that outline to the\n * `NeutralStrokeAccessible` ramp (`border-stroke-accessible` = `#616161` rest,\n * `#575757` hover, `#4d4d4d` pressed): spec-true to the measured Fluent value,\n * so this is no longer the earlier `border-input` (`#d1d1d1`) substitution —\n * that documented deviation is retired. The *checked* track measured exactly\n * `#0f6cbd` — this kit's `--primary`/`bg-primary` token — with a white thumb\n * (`#ffffff`, this kit's `--primary-foreground` token in both themes); on\n * hover/press the on-track steps through Fluent's `CompoundBrandBackground`\n * (border+fill `brand-70`/`brand-60`; dark hover `brand-80`), mirroring\n * Button's per-theme brand ramp (conventions §4).\n *\n * The prop surface (`Switch`, `checked`/`defaultChecked`/`onCheckedChange`,\n * no `size` prop) follows the classic shadcn/ui switch API. Note Base UI's\n * state data-attributes differ from Radix's: it emits presence-only\n * `data-checked`/`data-unchecked` (and `data-disabled`/`data-readonly`/\n * `data-required`), not Radix's `data-state=\"checked\" | \"unchecked\"` — the\n * classes below target Base UI's actual output via Tailwind's presence-based\n * `data-[checked]:` variant.\n */\nfunction Switch({\n className,\n ...props\n}: ComponentProps<typeof SwitchPrimitive.Root>) {\n return (\n <SwitchPrimitive.Root\n data-slot=\"switch\"\n className={cn(\n // layout — Fluent medium toggle: 40x20 pill track, thumb centered via flex\n \"inline-flex h-5 w-10 shrink-0 cursor-pointer items-center rounded-full border px-0.5\",\n // motion — border/bg color transition on check/uncheck\n \"outline-none transition-colors duration-fast ease-ease\",\n // off (unchecked) — Fluent's outlined/transparent track, bound to the\n // NeutralStrokeAccessible ramp (spec-true measured #616161 rest ->\n // #575757 hover -> #4d4d4d pressed), see file header.\n \"border-stroke-accessible bg-transparent hover:border-stroke-accessible-hover active:border-stroke-accessible-pressed\",\n // on (checked) — brand fill (Fluent CompoundBrandBackground), stepping\n // border+fill through Hover/Pressed; dark hover brightens to brand-80.\n // The data-[checked]:hover/active selectors outrank the neutral\n // hover/active border by specificity so the on-track stays brand.\n \"data-[checked]:border-primary data-[checked]:bg-primary data-[checked]:hover:border-brand-70 data-[checked]:hover:bg-brand-70 data-[checked]:active:border-brand-60 data-[checked]:active:bg-brand-60 dark:data-[checked]:hover:border-brand-80 dark:data-[checked]:hover:bg-brand-80\",\n // focus — Fluent double-stroke approximation (conventions §4)\n \"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n // invalid — shadcn aria-invalid treatment (conventions §4); harmless\n // unless the caller pairs this with a Field/validation integration\n \"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40\",\n // disabled — opacity read (conventions §4), keyed off Base UI's\n // `data-disabled` rather than the CSS `disabled:` pseudo-class: this\n // control renders a <span role=\"switch\">, not a native\n // <button>/<input>, so `:disabled` never matches it.\n \"data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n className\n )}\n {...props}\n >\n <SwitchPrimitive.Thumb\n data-slot=\"switch-thumb\"\n className={cn(\n \"pointer-events-none block size-3.5 rounded-full\",\n \"transition-transform duration-fast ease-ease\",\n // off — neutral thumb, matches Fluent's measured `#616161`\n \"bg-muted-foreground\",\n // on — white thumb via the `primary-foreground` token (`#ffffff`\n // in both themes) rather than a literal `bg-white`, per\n // conventions §3.7 (never hardcode a color). Travel distance is\n // the track's content-box width (40 - 2×1px border - 2×2px\n // padding = 34px) minus the 14px thumb = 20px = `translate-x-5`.\n \"data-[checked]:translate-x-5 data-[checked]:bg-primary-foreground\"\n )}\n />\n </SwitchPrimitive.Root>\n );\n}\n\nexport { Switch };\n"],"mappings":"AA6FM;AA7FN,SAAS,UAAU,uBAAuB;AAG1C,SAAS,UAAU;AAuDnB,SAAS,OAAO;AAAA,EACd;AAAA,EACA,GAAG;AACL,GAAgD;AAC9C,SACE;AAAA,IAAC,gBAAgB;AAAA,IAAhB;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA;AAAA,QAET;AAAA;AAAA,QAEA;AAAA;AAAA;AAAA;AAAA,QAIA;AAAA;AAAA;AAAA;AAAA;AAAA,QAKA;AAAA;AAAA,QAEA;AAAA;AAAA;AAAA,QAGA;AAAA;AAAA;AAAA;AAAA;AAAA,QAKA;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA,MAEJ;AAAA,QAAC,gBAAgB;AAAA,QAAhB;AAAA,UACC,aAAU;AAAA,UACV,WAAW;AAAA,YACT;AAAA,YACA;AAAA;AAAA,YAEA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,YAMA;AAAA,UACF;AAAA;AAAA,MACF;AAAA;AAAA,EACF;AAEJ;","names":[]}
@@ -0,0 +1,107 @@
1
+ import * as react from 'react';
2
+ import { ComponentProps } from 'react';
3
+ import { Tabs as Tabs$1 } from '@base-ui/react/tabs';
4
+
5
+ /**
6
+ * Tabs — Fluent 2-styled, shadcn-API tabs.
7
+ *
8
+ * ## Base UI mapping (conventions §9)
9
+ * Roving-tabindex focus, arrow-key navigation, and (per divergence 3 below)
10
+ * the sliding active-tab indicator are genuine behavior/layout-measurement
11
+ * that plain markup can't express, so the four parts wrap `@base-ui/react/tabs`
12
+ * (namespace import as `TabsPrimitive`, matching the actual
13
+ * `export * as Tabs from "./index.parts.js"` shape in node_modules — the same
14
+ * pattern `select.tsx`/`radio-group.tsx` use):
15
+ *
16
+ * | Exported (shadcn name) | Base UI primitive |
17
+ * | ----------------------- | --------------------- |
18
+ * | `Tabs` | `Tabs.Root` |
19
+ * | `TabsList` | `Tabs.List` (+ `Tabs.Indicator` appended) |
20
+ * | `TabsTrigger` | `Tabs.Tab` |
21
+ * | `TabsContent` | `Tabs.Panel` |
22
+ *
23
+ * `"use client"` is required here — unlike `radio-group.tsx`, this wrapper
24
+ * itself needs it, not just the primitive parts. `Tabs.Indicator` measures the
25
+ * active tab's DOM layout (`getBoundingClientRect`/`offsetLeft`) inside a
26
+ * `React.useEffect` to drive the sliding-underline CSS vars (divergence 3), so
27
+ * the component genuinely participates in client-side behavior beyond just
28
+ * rendering a client-boundary child — matching the reasoning that already
29
+ * governs `select.tsx`/`checkbox.tsx` for a different trigger (icons there;
30
+ * layout measurement here).
31
+ *
32
+ * ## Divergences from the shadcn/Radix Tabs API (all deliberate)
33
+ * 1. **`TabsList` is a transparent underline bar, not a muted pill.** shadcn's
34
+ * reference `TabsList` is a segmented control: `bg-muted` rounded box with
35
+ * each `TabsTrigger` becoming a filled `bg-background` pill when active.
36
+ * Fluent 2's `TabList` has no pill — it's a transparent row of labels over a
37
+ * hairline `border-b`, with a 2px brand bar that slides under the active
38
+ * label. This implementation follows Fluent, not shadcn's segmented look:
39
+ * `TabsList` carries `border-b border-border` (the resting hairline) and no
40
+ * background; `TabsTrigger` is unstyled at rest beyond text color (muted →
41
+ * foreground on hover, foreground + semibold when active via
42
+ * `data-[active]:`), never a filled pill.
43
+ * 2. **Active state uses Base UI's `data-active`, not shadcn's `data-state`
44
+ * value pair.** Radix's `Tabs.Trigger` exposes `data-state="active"|
45
+ * "inactive"`; Base UI's `Tabs.Tab` instead exposes `data-active` as a plain
46
+ * presence attribute (see `TabsTabDataAttributes` in node_modules — boolean
47
+ * presence, not a value pair, the same model `radio-group.tsx`'s
48
+ * `data-checked` uses). Styled here via the bracketed
49
+ * `data-[active]:` form per conventions §4.
50
+ * 3. **The sliding indicator uses `Tabs.Indicator`'s CSS-var position API**
51
+ * (`--active-tab-left`/`--active-tab-width`, from `TabsIndicatorCssVars` in
52
+ * node_modules), not a custom `ref`-measured span. `Tabs.Indicator` renders
53
+ * a `<span>` inside `TabsList` with those vars set as inline custom
54
+ * properties (re-measured via a `ResizeObserver`-backed listener whenever
55
+ * the active tab or list layout changes) and `hidden` until a non-zero size
56
+ * is available. This wrapper positions the span with
57
+ * `left-[var(--active-tab-left)] w-[var(--active-tab-width)]` and animates
58
+ * moves with `transition-[left,width] duration-normal ease-ease` — the
59
+ * Fluent sliding-underline motion, confirmed working. **jsdom caveat:**
60
+ * jsdom has no layout engine, so `offsetWidth`/`getBoundingClientRect` are
61
+ * always `0`; `Tabs.Indicator` treats that as "not yet measured" and stays
62
+ * `hidden`, so the indicator element exists in the test DOM
63
+ * (`data-slot="tabs-indicator"`) but the sliding motion itself is only
64
+ * observable in a real browser (see the Select/Dialog popup-motion note in
65
+ * conventions §3.5 for the same class of jsdom limitation applied to
66
+ * motion). The component test asserts the indicator renders and tracks
67
+ * `data-activation-direction`, not the pixel position.
68
+ * 4. **Default activation is manual (Enter/Space), not automatic-on-arrow.**
69
+ * Radix/shadcn Tabs activates on arrow-key focus move (`automatic`). Base
70
+ * UI's `Tabs.List` defaults `activateOnFocus` to `false` — the
71
+ * WAI-ARIA-recommended "manual activation" pattern: arrow keys move focus
72
+ * among tabs, and the focused tab is only selected on `Enter`/`Space` (or
73
+ * click). This wrapper does not override that default, so the kit ships
74
+ * Base UI's more accessible default rather than papering over it to match
75
+ * Radix bit-for-bit; callers who want automatic activation can pass
76
+ * `activateOnFocus` to `TabsList` themselves (it's forwarded straight
77
+ * through as an ordinary prop).
78
+ *
79
+ * ## data-slot note
80
+ * Every part renders a real DOM element, so every part gets a `data-slot`:
81
+ * `tabs`, `tabs-list`, `tabs-indicator`, `tabs-trigger`, `tabs-content`.
82
+ */
83
+ declare function Tabs({ className, ...props }: ComponentProps<typeof Tabs$1.Root>): react.JSX.Element;
84
+ /**
85
+ * List — transparent row of tab labels over a hairline bottom border, with
86
+ * the brand sliding indicator (divergence 3) appended as the last child so it
87
+ * paints above the hairline.
88
+ */
89
+ declare function TabsList({ className, children, ...props }: ComponentProps<typeof Tabs$1.List>): react.JSX.Element;
90
+ /**
91
+ * Trigger — muted label at rest, brand-adjacent foreground + semibold when
92
+ * active (divergence 2: `data-[active]:`, not shadcn's filled pill). Focus
93
+ * uses the generic offset-ring recipe (conventions §4) — a tab is a
94
+ * selectable control, not a typed-into field, so it doesn't get the bottom
95
+ * brand-accent treatment `Input`/`Select` use.
96
+ */
97
+ declare function TabsTrigger({ className, ...props }: ComponentProps<typeof Tabs$1.Tab>): react.JSX.Element;
98
+ /**
99
+ * Content — the panel shown for the active tab. Base UI's `Tabs.Panel`
100
+ * unmounts inactive panels from the accessibility tree (`hidden`) and only
101
+ * the active one is focusable/rendered visible, so this wrapper is just
102
+ * spacing + a focus ring for when a panel itself receives focus (e.g. no
103
+ * focusable content inside it).
104
+ */
105
+ declare function TabsContent({ className, ...props }: ComponentProps<typeof Tabs$1.Panel>): react.JSX.Element;
106
+
107
+ export { Tabs, TabsContent, TabsList, TabsTrigger };