@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.
- package/LICENSE +21 -0
- package/README.md +69 -0
- package/dist/components/ui/accordion.d.ts +133 -0
- package/dist/components/ui/accordion.js +80 -0
- package/dist/components/ui/accordion.js.map +1 -0
- package/dist/components/ui/alert.d.ts +106 -0
- package/dist/components/ui/alert.js +73 -0
- package/dist/components/ui/alert.js.map +1 -0
- package/dist/components/ui/avatar.d.ts +42 -0
- package/dist/components/ui/avatar.js +55 -0
- package/dist/components/ui/avatar.js.map +1 -0
- package/dist/components/ui/badge.d.ts +36 -0
- package/dist/components/ui/badge.js +81 -0
- package/dist/components/ui/badge.js.map +1 -0
- package/dist/components/ui/breadcrumb.d.ts +53 -0
- package/dist/components/ui/breadcrumb.js +149 -0
- package/dist/components/ui/breadcrumb.js.map +1 -0
- package/dist/components/ui/button.d.ts +24 -0
- package/dist/components/ui/button.js +86 -0
- package/dist/components/ui/button.js.map +1 -0
- package/dist/components/ui/card.d.ts +51 -0
- package/dist/components/ui/card.js +97 -0
- package/dist/components/ui/card.js.map +1 -0
- package/dist/components/ui/checkbox.d.ts +101 -0
- package/dist/components/ui/checkbox.js +57 -0
- package/dist/components/ui/checkbox.js.map +1 -0
- package/dist/components/ui/combobox.d.ts +212 -0
- package/dist/components/ui/combobox.js +236 -0
- package/dist/components/ui/combobox.js.map +1 -0
- package/dist/components/ui/command.d.ts +187 -0
- package/dist/components/ui/command.js +225 -0
- package/dist/components/ui/command.js.map +1 -0
- package/dist/components/ui/dialog.d.ts +117 -0
- package/dist/components/ui/dialog.js +165 -0
- package/dist/components/ui/dialog.js.map +1 -0
- package/dist/components/ui/dropdown-menu.d.ts +155 -0
- package/dist/components/ui/dropdown-menu.js +295 -0
- package/dist/components/ui/dropdown-menu.js.map +1 -0
- package/dist/components/ui/input.d.ts +44 -0
- package/dist/components/ui/input.js +43 -0
- package/dist/components/ui/input.js.map +1 -0
- package/dist/components/ui/label.d.ts +39 -0
- package/dist/components/ui/label.js +30 -0
- package/dist/components/ui/label.js.map +1 -0
- package/dist/components/ui/link.d.ts +62 -0
- package/dist/components/ui/link.js +61 -0
- package/dist/components/ui/link.js.map +1 -0
- package/dist/components/ui/multi-select.d.ts +115 -0
- package/dist/components/ui/multi-select.js +133 -0
- package/dist/components/ui/multi-select.js.map +1 -0
- package/dist/components/ui/pagination.d.ts +46 -0
- package/dist/components/ui/pagination.js +164 -0
- package/dist/components/ui/pagination.js.map +1 -0
- package/dist/components/ui/popover.d.ts +117 -0
- package/dist/components/ui/popover.js +62 -0
- package/dist/components/ui/popover.js.map +1 -0
- package/dist/components/ui/progress.d.ts +98 -0
- package/dist/components/ui/progress.js +67 -0
- package/dist/components/ui/progress.js.map +1 -0
- package/dist/components/ui/radio-group.d.ts +94 -0
- package/dist/components/ui/radio-group.js +65 -0
- package/dist/components/ui/radio-group.js.map +1 -0
- package/dist/components/ui/select.d.ts +145 -0
- package/dist/components/ui/select.js +240 -0
- package/dist/components/ui/select.js.map +1 -0
- package/dist/components/ui/separator.d.ts +43 -0
- package/dist/components/ui/separator.js +27 -0
- package/dist/components/ui/separator.js.map +1 -0
- package/dist/components/ui/skeleton.d.ts +81 -0
- package/dist/components/ui/skeleton.js +24 -0
- package/dist/components/ui/skeleton.js.map +1 -0
- package/dist/components/ui/spinner.d.ts +56 -0
- package/dist/components/ui/spinner.js +83 -0
- package/dist/components/ui/spinner.js.map +1 -0
- package/dist/components/ui/switch.d.ts +60 -0
- package/dist/components/ui/switch.js +63 -0
- package/dist/components/ui/switch.js.map +1 -0
- package/dist/components/ui/tabs.d.ts +107 -0
- package/dist/components/ui/tabs.js +102 -0
- package/dist/components/ui/tabs.js.map +1 -0
- package/dist/components/ui/textarea.d.ts +28 -0
- package/dist/components/ui/textarea.js +36 -0
- package/dist/components/ui/textarea.js.map +1 -0
- package/dist/components/ui/toast.d.ts +185 -0
- package/dist/components/ui/toast.js +181 -0
- package/dist/components/ui/toast.js.map +1 -0
- package/dist/components/ui/tooltip.d.ts +115 -0
- package/dist/components/ui/tooltip.js +72 -0
- package/dist/components/ui/tooltip.js.map +1 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.js +284 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/utils.d.ts +9 -0
- package/dist/lib/utils.js +9 -0
- package/dist/lib/utils.js.map +1 -0
- package/package.json +77 -0
- package/src/styles/tokens.css +596 -0
- 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 };
|