@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 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/components/ui/progress.tsx"],"sourcesContent":["import { Progress as ProgressPrimitive } from \"@base-ui/react/progress\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport type { ComponentProps } from \"react\";\n\nimport { cn } from \"../../lib/utils\";\n\n/**\n * Progress — Fluent 2-styled, shadcn-API progress bar.\n *\n * Base UI ships a Progress primitive (`@base-ui/react/progress`, imported as\n * a namespace exposing `Root` / `Track` / `Indicator` / `Label` / `Value`,\n * matching the actual export shape in node_modules — same pattern as\n * `avatar.tsx`). This is the batch's Base UI case (conventions §9): value→\n * percentage math, `role=\"progressbar\"`, `aria-valuemin`/`aria-valuemax`/\n * `aria-valuenow`/`aria-valuetext` wiring, and indeterminate detection\n * (`aria-valuenow` correctly omitted when `value` is `null`/`undefined`) are\n * exactly the kind of genuine behavior a primitive should own rather than\n * hand-rolling ARIA math. `Root`, `Track`, and `Indicator` are composed\n * internally into a single `Progress` export to match shadcn's flat API\n * (`<Progress value={33} />`) — the sub-parts are not re-exported, since the\n * spec calls for one component, not shadcn's newer multi-part variant.\n *\n * `\"use client\"` is intentionally omitted, mirroring `avatar.tsx`'s\n * reasoning: Base UI's Root/Track/Indicator modules each carry their own\n * `'use client'` directive at the source level, so they are already client\n * boundaries on their own. This wrapper file has no hooks/handlers of its\n * own — it only forwards props and branches on a plain value comparison — so\n * it stays a plain (server-renderable) module per conventions §2. Confirmed\n * via `pnpm typecheck` + the test suite.\n *\n * Aesthetics follow Fluent 2's ProgressBar, which is thin — 2px (`h-0.5`),\n * not shadcn's 8px `h-2` default. This is a deliberate, documented deviation\n * from the shadcn reference; consumers who want the shadcn-thick bar (or any\n * other height) can override via `className` (`cn` + tailwind-merge resolve\n * the conflict, last utility wins). Track is `bg-secondary` (flat neutral\n * fill) with `rounded-full overflow-hidden`; the indicator is `bg-primary`,\n * also `rounded-full`.\n *\n * Intent variants: Fluent's ProgressBar ships a `State` axis (Default/Success/\n * Error/Warning), so `Progress` exposes a matching `variant` prop\n * (`default`/`success`/`warning`/`destructive`) via `progressVariants` — a\n * `cva` on the indicator sub-part, the same precedent `spinner.tsx` sets by\n * putting its `cva` on the inner glyph. `success` (`bg-success` `#107c10`) and\n * `warning` (`bg-warning` `#da3b01`) are exact hex matches to Fluent's\n * Success/Warning `State`. `destructive` reuses `bg-destructive` (`#d13438`)\n * rather than Fluent's filled-status `#c50f1f`: this kit standardizes on one\n * destructive red across Badge/Progress/Label per the single-red policy\n * (docs/design/tokens-research.md §12.9), keeping consistency over a one-off\n * fifth red token. The indicator width is driven by Base UI's own computed\n * `style.width` percentage (`ProgressIndicator` reads `value`/`min`/`max`\n * from context and sets `width: ${percent}%` — this is \"the cleaner\" of the\n * two width strategies the spec allows: it needs no manual percent math on\n * our side, unlike shadcn's classic `translateX(-${100 - value}%)` recipe).\n * `transition-[width]` (not `transition-all`/`transform`, since Base UI\n * animates `width` directly) plus the token duration/easing pair animates\n * value changes; reduced motion is handled automatically by the token layer\n * (conventions §3.5) since `duration-normal`/`ease-ease` are token-driven.\n *\n * Indeterminate (`value` is `null`/undefined): Fluent's sliding segment. The\n * indicator renders a `w-1/3` segment driven by the token-layer\n * `--animate-progress-indeterminate` keyframes (tokens.css): it travels from\n * fully off-track left (`translate: -100%` of its own width) to fully\n * off-track right (`300%`), clipped by the root's `overflow-hidden`.\n *\n * Under `prefers-reduced-motion` the slide is fully stopped via\n * `motion-reduce:animate-none` (same call as `skeleton.tsx`, not `spinner.tsx`'s\n * slow-down): the pulse carries no information a reduced-motion user would\n * lose by holding it still — the indeterminate/busy state is conveyed by the\n * `role=\"progressbar\"` + omitted `aria-valuenow` to assistive tech and by the\n * bar's presence to sighted users — and a persistently animating region is\n * exactly what `prefers-reduced-motion` asks apps to avoid. `motion-reduce:\n * animate-none` is a plain Tailwind utility (not a `duration-*` token), so it\n * isn't covered by the automatic duration collapse `tokens.css` applies and\n * has to be declared here explicitly.\n *\n * `role=\"progressbar\"` requires an accessible name; Base UI does not default\n * one. Pass `aria-label` (forwarded straight through to the root via\n * `...props`) or compose Base UI's own `Progress.Label` alongside this\n * component — consumers must supply one or the other.\n */\nconst progressVariants = cva(\n \"h-full rounded-full transition-[width] duration-normal ease-ease\",\n {\n variants: {\n variant: {\n default: \"bg-primary\",\n success: \"bg-success\",\n warning: \"bg-warning\",\n destructive: \"bg-destructive\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n },\n }\n);\n\nfunction Progress({\n className,\n value,\n variant = \"default\",\n size = \"default\",\n ...props\n}: Omit<ComponentProps<typeof ProgressPrimitive.Root>, \"value\"> &\n VariantProps<typeof progressVariants> & {\n /**\n * 0–100 (relative to `min`/`max`, both default 0/100). Omit or pass `null`\n * for the indeterminate state — Base UI's Root then correctly omits\n * `aria-valuenow` and this wrapper renders the pulsing stand-in indicator.\n */\n value?: number | null;\n /**\n * Fluent ProgressBar `Size` axis: `default` = Medium (2px track),\n * `lg` = Large (4px) — surfaced by the pass-1 Figma validation.\n */\n size?: \"default\" | \"lg\";\n }) {\n return (\n <ProgressPrimitive.Root\n data-slot=\"progress\"\n data-variant={variant}\n data-size={size}\n value={value ?? null}\n className={cn(\n \"relative w-full overflow-hidden rounded-full bg-secondary\",\n size === \"lg\" ? \"h-1\" : \"h-0.5\",\n className\n )}\n {...props}\n >\n <ProgressPrimitive.Track\n data-slot=\"progress-track\"\n className=\"relative h-full w-full\"\n >\n <ProgressPrimitive.Indicator\n data-slot=\"progress-indicator\"\n className={cn(\n progressVariants({ variant }),\n // Fluent indeterminate: the token-layer keyframes slide the 1/3\n // segment from off-track left to off-track right (root clips).\n value == null &&\n \"w-1/3 animate-progress-indeterminate motion-reduce:animate-none\"\n )}\n />\n </ProgressPrimitive.Track>\n </ProgressPrimitive.Root>\n );\n}\n\nexport { Progress, progressVariants };\n"],"mappings":"AAsIQ;AAtIR,SAAS,YAAY,yBAAyB;AAC9C,SAAS,WAA8B;AAGvC,SAAS,UAAU;AA4EnB,MAAM,mBAAmB;AAAA,EACvB;AAAA,EACA;AAAA,IACE,UAAU;AAAA,MACR,SAAS;AAAA,QACP,SAAS;AAAA,QACT,SAAS;AAAA,QACT,SAAS;AAAA,QACT,aAAa;AAAA,MACf;AAAA,IACF;AAAA,IACA,iBAAiB;AAAA,MACf,SAAS;AAAA,IACX;AAAA,EACF;AACF;AAEA,SAAS,SAAS;AAAA,EAChB;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV,OAAO;AAAA,EACP,GAAG;AACL,GAaK;AACH,SACE;AAAA,IAAC,kBAAkB;AAAA,IAAlB;AAAA,MACC,aAAU;AAAA,MACV,gBAAc;AAAA,MACd,aAAW;AAAA,MACX,OAAO,SAAS;AAAA,MAChB,WAAW;AAAA,QACT;AAAA,QACA,SAAS,OAAO,QAAQ;AAAA,QACxB;AAAA,MACF;AAAA,MACC,GAAG;AAAA,MAEJ;AAAA,QAAC,kBAAkB;AAAA,QAAlB;AAAA,UACC,aAAU;AAAA,UACV,WAAU;AAAA,UAEV;AAAA,YAAC,kBAAkB;AAAA,YAAlB;AAAA,cACC,aAAU;AAAA,cACV,WAAW;AAAA,gBACT,iBAAiB,EAAE,QAAQ,CAAC;AAAA;AAAA;AAAA,gBAG5B,SAAS,QACP;AAAA,cACJ;AAAA;AAAA,UACF;AAAA;AAAA,MACF;AAAA;AAAA,EACF;AAEJ;","names":[]}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { Radio } from '@base-ui/react/radio';
|
|
3
|
+
import { RadioGroup as RadioGroup$1 } from '@base-ui/react/radio-group';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* RadioGroup / RadioGroupItem — Fluent 2-styled, shadcn-API radio group.
|
|
7
|
+
*
|
|
8
|
+
* This is a Base UI case (conventions §9): roving tabindex, arrow-key
|
|
9
|
+
* selection and the checked/unchecked state machine are genuine behavior a
|
|
10
|
+
* `<div>` + `<input type="radio">` can't express on their own without
|
|
11
|
+
* reimplementing Base UI's composite-focus logic, so the two parts wrap
|
|
12
|
+
* `@base-ui/react/radio-group` (`RadioGroup`, imported and re-exported as
|
|
13
|
+
* `RadioGroupPrimitive`) and `@base-ui/react/radio` (`Radio`, imported as a
|
|
14
|
+
* namespace exposing `Radio.Root` / `Radio.Indicator` — matches the actual
|
|
15
|
+
* `export * as Radio from "./index.parts.js"` shape in node_modules, same
|
|
16
|
+
* pattern `avatar.tsx` uses for `@base-ui/react/avatar`). Arrow-key
|
|
17
|
+
* roving/selection, click-to-select, and the hidden native
|
|
18
|
+
* `<input type="radio">` (native form participation) all come from Base UI —
|
|
19
|
+
* none of it is reimplemented here.
|
|
20
|
+
*
|
|
21
|
+
* `"use client"` is intentionally omitted here, same reasoning as
|
|
22
|
+
* `avatar.tsx`: `@base-ui/react/radio-group`'s `RadioGroup` and
|
|
23
|
+
* `@base-ui/react/radio`'s `Radio.Root` / `Radio.Indicator` each carry their
|
|
24
|
+
* own `'use client'` directive at the compiled-source level (verified in
|
|
25
|
+
* `node_modules/@base-ui/react/{radio-group/RadioGroup.js,radio/root/RadioRoot.js,radio/indicator/RadioIndicator.js}`),
|
|
26
|
+
* so they are already client boundaries on their own. This wrapper file has
|
|
27
|
+
* no hooks/handlers of its own — it only forwards props into those parts —
|
|
28
|
+
* so per conventions §2 it can stay a plain (server-renderable) module; a
|
|
29
|
+
* Server Component tree can render a Client Component child without the
|
|
30
|
+
* parent re-declaring the directive. Confirmed via `pnpm typecheck` + the
|
|
31
|
+
* test suite; add the directive here only if that stops holding true.
|
|
32
|
+
*
|
|
33
|
+
* Layout: shadcn's reference implementation defaults `RadioGroup` to
|
|
34
|
+
* `"grid gap-3"`. This kit ships `"grid gap-2"` instead — a deliberate,
|
|
35
|
+
* spec-directed deviation to match this component family's spacing scale (a
|
|
36
|
+
* 16px control at `gap-2` == 8px reads as a tighter, denser Fluent list than
|
|
37
|
+
* shadcn's 12px gap). Callers needing shadcn's exact spacing can override via
|
|
38
|
+
* `className="gap-3"` (later utility wins through `cn`'s `tailwind-merge`).
|
|
39
|
+
*
|
|
40
|
+
* Checked indicator: shadcn fills a `lucide-react` `CircleIcon` positioned
|
|
41
|
+
* absolutely inside the indicator. Fluent 2's RadioGroup instead shows a
|
|
42
|
+
* brand-colored **ring** (the whole circle's border turns brand) with a
|
|
43
|
+
* small brand **dot** centered inside — no checkmark glyph. This
|
|
44
|
+
* implementation matches that: the item's border switches to
|
|
45
|
+
* `border-primary` when `data-checked` is present, and the indicator renders
|
|
46
|
+
* a plain `size-2 rounded-full bg-primary` `<span>` (flex-centered, no icon
|
|
47
|
+
* import needed — a filled circle needs no glyph, so pulling in
|
|
48
|
+
* `@fluentui/react-icons` for this would add a dependency for no visual
|
|
49
|
+
* gain). `Radio.Indicator` only mounts while the radio is checked (Base UI
|
|
50
|
+
* unmounts it otherwise, per `RadioIndicator.js`'s `shouldRender` check), so
|
|
51
|
+
* the dot itself needs no extra checked-conditional styling — but it does
|
|
52
|
+
* ramp its color alongside the ring (see below).
|
|
53
|
+
*
|
|
54
|
+
* Borders + interaction ramp: the unchecked ring outlines with Fluent's
|
|
55
|
+
* `NeutralStrokeAccessible` ramp — rest `#616161` (`border-stroke-accessible`),
|
|
56
|
+
* hover `#575757`, pressed `#4d4d4d` — the higher-contrast neutral spec for
|
|
57
|
+
* interactive control outlines (extracted from the Figma radio), NOT the
|
|
58
|
+
* lighter `border-input` grey the item previously used. When checked, the ring
|
|
59
|
+
* (border) and dot step through `CompoundBrandStroke`/`CompoundBrandForeground1`
|
|
60
|
+
* Hover/Pressed (`brand-70`/`brand-60`; dark hover `brand-80`), mirroring
|
|
61
|
+
* Button's per-theme brand ramp (conventions §4). The ring uses
|
|
62
|
+
* `data-[checked]:hover:`/`:active:` (which outrank the neutral hover/active
|
|
63
|
+
* border by specificity); the dot, having no `data-checked` of its own, rides
|
|
64
|
+
* the root's `group` hover/press instead.
|
|
65
|
+
*
|
|
66
|
+
* States: `data-checked`/`data-unchecked` and `data-disabled` are Base UI's
|
|
67
|
+
* own presence attributes on `Radio.Root`'s rendered `<span>` (see
|
|
68
|
+
* `RadioRootDataAttributes` in node_modules — boolean presence, not a
|
|
69
|
+
* `data-state="checked"` value pair like Radix), matched here via Tailwind's
|
|
70
|
+
* `data-[checked]:` / `data-[disabled]:` arbitrary-attribute variants (there
|
|
71
|
+
* is no native `:disabled` pseudo-class to reach for — the rendered element
|
|
72
|
+
* is a `<span role="radio">`, not a real `<input>`/`<button>`, so the
|
|
73
|
+
* opacity/pointer-events disabled recipe from conventions §4 is re-expressed
|
|
74
|
+
* against `data-[disabled]:` instead of `disabled:`). Focus, per conventions
|
|
75
|
+
* §4, stays the generic offset-ring recipe (not the field bottom-accent
|
|
76
|
+
* variant — a radio is a selectable control, not a typed-into field).
|
|
77
|
+
* `aria-invalid` is a bare ARIA attribute Base UI passes through untouched,
|
|
78
|
+
* so the standard `aria-invalid:` recipe applies as-is.
|
|
79
|
+
*
|
|
80
|
+
* Typing: Base UI's `RadioGroup`/`Radio.Root` are generic over the radio
|
|
81
|
+
* value type (`Value = any` upstream). `ComponentProps<typeof X>` on a
|
|
82
|
+
* generic function component collapses that parameter to `unknown` (it has
|
|
83
|
+
* no call site to infer from), which would force every consumer to cast in
|
|
84
|
+
* `onValueChange`. Both parts pin `Value` to `string` instead via each
|
|
85
|
+
* primitive's own namespace type (`RadioGroupPrimitive.Props<string>`,
|
|
86
|
+
* `Radio.Root.Props<string>` — available because Base UI declaration-merges
|
|
87
|
+
* a namespace onto each component, matching shadcn/Radix's own radio API,
|
|
88
|
+
* which is string-only, not generic) so callers get a plain, un-cast
|
|
89
|
+
* `value`/`onValueChange: (value: string, …) => void` surface.
|
|
90
|
+
*/
|
|
91
|
+
declare function RadioGroup({ className, ...props }: RadioGroup$1.Props<string>): react.JSX.Element;
|
|
92
|
+
declare function RadioGroupItem({ className, ...props }: Radio.Root.Props<string>): react.JSX.Element;
|
|
93
|
+
|
|
94
|
+
export { RadioGroup, RadioGroupItem };
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Radio } from "@base-ui/react/radio";
|
|
3
|
+
import { RadioGroup as RadioGroupPrimitive } from "@base-ui/react/radio-group";
|
|
4
|
+
import { cn } from "../../lib/utils.js";
|
|
5
|
+
function RadioGroup({
|
|
6
|
+
className,
|
|
7
|
+
...props
|
|
8
|
+
}) {
|
|
9
|
+
return /* @__PURE__ */ jsx(
|
|
10
|
+
RadioGroupPrimitive,
|
|
11
|
+
{
|
|
12
|
+
"data-slot": "radio-group",
|
|
13
|
+
className: cn("grid gap-2", className),
|
|
14
|
+
...props
|
|
15
|
+
}
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
function RadioGroupItem({
|
|
19
|
+
className,
|
|
20
|
+
...props
|
|
21
|
+
}) {
|
|
22
|
+
return /* @__PURE__ */ jsx(
|
|
23
|
+
Radio.Root,
|
|
24
|
+
{
|
|
25
|
+
"data-slot": "radio-group-item",
|
|
26
|
+
className: cn(
|
|
27
|
+
// layout — Fluent 16px circle. `group` lets the checked dot (a child
|
|
28
|
+
// span) pick up the root's hover/press to ramp alongside the ring.
|
|
29
|
+
"group aspect-square size-4 shrink-0 cursor-pointer rounded-full border bg-background",
|
|
30
|
+
// motion
|
|
31
|
+
"outline-none transition-colors duration-fast ease-ease",
|
|
32
|
+
// unchecked — Fluent NeutralStrokeAccessible rest/hover/pressed ramp
|
|
33
|
+
"border-stroke-accessible hover:border-stroke-accessible-hover active:border-stroke-accessible-pressed",
|
|
34
|
+
// checked — Fluent brand ring (the dot itself lives in the indicator),
|
|
35
|
+
// stepping through CompoundBrandStroke Hover/Pressed; dark hover
|
|
36
|
+
// brightens to brand-80 (Button's per-theme ramp). These outrank the
|
|
37
|
+
// neutral hover/active border by specificity so the ring stays brand.
|
|
38
|
+
"data-[checked]:border-primary data-[checked]:hover:border-brand-70 data-[checked]:active:border-brand-60 dark:data-[checked]:hover:border-brand-80",
|
|
39
|
+
// disabled — opacity-based (conventions §4), re-expressed against
|
|
40
|
+
// Base UI's data-disabled presence attribute (this renders a
|
|
41
|
+
// <span>, not a native disableable element)
|
|
42
|
+
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
43
|
+
// focus — generic offset-ring recipe (conventions §4)
|
|
44
|
+
"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
|
|
45
|
+
// invalid — shadcn aria-invalid treatment (conventions §4)
|
|
46
|
+
"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
|
|
47
|
+
className
|
|
48
|
+
),
|
|
49
|
+
...props,
|
|
50
|
+
children: /* @__PURE__ */ jsx(
|
|
51
|
+
Radio.Indicator,
|
|
52
|
+
{
|
|
53
|
+
"data-slot": "radio-group-indicator",
|
|
54
|
+
className: "flex items-center justify-center",
|
|
55
|
+
children: /* @__PURE__ */ jsx("span", { className: "size-2 rounded-full bg-primary transition-colors duration-fast ease-ease group-hover:bg-brand-70 group-active:bg-brand-60 dark:group-hover:bg-brand-80" })
|
|
56
|
+
}
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
export {
|
|
62
|
+
RadioGroup,
|
|
63
|
+
RadioGroupItem
|
|
64
|
+
};
|
|
65
|
+
//# sourceMappingURL=radio-group.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/components/ui/radio-group.tsx"],"sourcesContent":["import { Radio } from \"@base-ui/react/radio\";\nimport { RadioGroup as RadioGroupPrimitive } from \"@base-ui/react/radio-group\";\n\nimport { cn } from \"../../lib/utils\";\n\n/**\n * RadioGroup / RadioGroupItem — Fluent 2-styled, shadcn-API radio group.\n *\n * This is a Base UI case (conventions §9): roving tabindex, arrow-key\n * selection and the checked/unchecked state machine are genuine behavior a\n * `<div>` + `<input type=\"radio\">` can't express on their own without\n * reimplementing Base UI's composite-focus logic, so the two parts wrap\n * `@base-ui/react/radio-group` (`RadioGroup`, imported and re-exported as\n * `RadioGroupPrimitive`) and `@base-ui/react/radio` (`Radio`, imported as a\n * namespace exposing `Radio.Root` / `Radio.Indicator` — matches the actual\n * `export * as Radio from \"./index.parts.js\"` shape in node_modules, same\n * pattern `avatar.tsx` uses for `@base-ui/react/avatar`). Arrow-key\n * roving/selection, click-to-select, and the hidden native\n * `<input type=\"radio\">` (native form participation) all come from Base UI —\n * none of it is reimplemented here.\n *\n * `\"use client\"` is intentionally omitted here, same reasoning as\n * `avatar.tsx`: `@base-ui/react/radio-group`'s `RadioGroup` and\n * `@base-ui/react/radio`'s `Radio.Root` / `Radio.Indicator` each carry their\n * own `'use client'` directive at the compiled-source level (verified in\n * `node_modules/@base-ui/react/{radio-group/RadioGroup.js,radio/root/RadioRoot.js,radio/indicator/RadioIndicator.js}`),\n * so they are already client boundaries on their own. This wrapper file has\n * no hooks/handlers of its own — it only forwards props into those parts —\n * so per conventions §2 it can stay a plain (server-renderable) module; a\n * Server Component tree can render a Client Component child without the\n * parent re-declaring the directive. Confirmed via `pnpm typecheck` + the\n * test suite; add the directive here only if that stops holding true.\n *\n * Layout: shadcn's reference implementation defaults `RadioGroup` to\n * `\"grid gap-3\"`. This kit ships `\"grid gap-2\"` instead — a deliberate,\n * spec-directed deviation to match this component family's spacing scale (a\n * 16px control at `gap-2` == 8px reads as a tighter, denser Fluent list than\n * shadcn's 12px gap). Callers needing shadcn's exact spacing can override via\n * `className=\"gap-3\"` (later utility wins through `cn`'s `tailwind-merge`).\n *\n * Checked indicator: shadcn fills a `lucide-react` `CircleIcon` positioned\n * absolutely inside the indicator. Fluent 2's RadioGroup instead shows a\n * brand-colored **ring** (the whole circle's border turns brand) with a\n * small brand **dot** centered inside — no checkmark glyph. This\n * implementation matches that: the item's border switches to\n * `border-primary` when `data-checked` is present, and the indicator renders\n * a plain `size-2 rounded-full bg-primary` `<span>` (flex-centered, no icon\n * import needed — a filled circle needs no glyph, so pulling in\n * `@fluentui/react-icons` for this would add a dependency for no visual\n * gain). `Radio.Indicator` only mounts while the radio is checked (Base UI\n * unmounts it otherwise, per `RadioIndicator.js`'s `shouldRender` check), so\n * the dot itself needs no extra checked-conditional styling — but it does\n * ramp its color alongside the ring (see below).\n *\n * Borders + interaction ramp: the unchecked ring outlines with Fluent's\n * `NeutralStrokeAccessible` ramp — rest `#616161` (`border-stroke-accessible`),\n * hover `#575757`, pressed `#4d4d4d` — the higher-contrast neutral spec for\n * interactive control outlines (extracted from the Figma radio), NOT the\n * lighter `border-input` grey the item previously used. When checked, the ring\n * (border) and dot step through `CompoundBrandStroke`/`CompoundBrandForeground1`\n * Hover/Pressed (`brand-70`/`brand-60`; dark hover `brand-80`), mirroring\n * Button's per-theme brand ramp (conventions §4). The ring uses\n * `data-[checked]:hover:`/`:active:` (which outrank the neutral hover/active\n * border by specificity); the dot, having no `data-checked` of its own, rides\n * the root's `group` hover/press instead.\n *\n * States: `data-checked`/`data-unchecked` and `data-disabled` are Base UI's\n * own presence attributes on `Radio.Root`'s rendered `<span>` (see\n * `RadioRootDataAttributes` in node_modules — boolean presence, not a\n * `data-state=\"checked\"` value pair like Radix), matched here via Tailwind's\n * `data-[checked]:` / `data-[disabled]:` arbitrary-attribute variants (there\n * is no native `:disabled` pseudo-class to reach for — the rendered element\n * is a `<span role=\"radio\">`, not a real `<input>`/`<button>`, so the\n * opacity/pointer-events disabled recipe from conventions §4 is re-expressed\n * against `data-[disabled]:` instead of `disabled:`). Focus, per conventions\n * §4, stays the generic offset-ring recipe (not the field bottom-accent\n * variant — a radio is a selectable control, not a typed-into field).\n * `aria-invalid` is a bare ARIA attribute Base UI passes through untouched,\n * so the standard `aria-invalid:` recipe applies as-is.\n *\n * Typing: Base UI's `RadioGroup`/`Radio.Root` are generic over the radio\n * value type (`Value = any` upstream). `ComponentProps<typeof X>` on a\n * generic function component collapses that parameter to `unknown` (it has\n * no call site to infer from), which would force every consumer to cast in\n * `onValueChange`. Both parts pin `Value` to `string` instead via each\n * primitive's own namespace type (`RadioGroupPrimitive.Props<string>`,\n * `Radio.Root.Props<string>` — available because Base UI declaration-merges\n * a namespace onto each component, matching shadcn/Radix's own radio API,\n * which is string-only, not generic) so callers get a plain, un-cast\n * `value`/`onValueChange: (value: string, …) => void` surface.\n */\nfunction RadioGroup({\n className,\n ...props\n}: RadioGroupPrimitive.Props<string>) {\n return (\n <RadioGroupPrimitive\n data-slot=\"radio-group\"\n className={cn(\"grid gap-2\", className)}\n {...props}\n />\n );\n}\n\nfunction RadioGroupItem({\n className,\n ...props\n}: Radio.Root.Props<string>) {\n return (\n <Radio.Root\n data-slot=\"radio-group-item\"\n className={cn(\n // layout — Fluent 16px circle. `group` lets the checked dot (a child\n // span) pick up the root's hover/press to ramp alongside the ring.\n \"group aspect-square size-4 shrink-0 cursor-pointer rounded-full border bg-background\",\n // motion\n \"outline-none transition-colors duration-fast ease-ease\",\n // unchecked — Fluent NeutralStrokeAccessible rest/hover/pressed ramp\n \"border-stroke-accessible hover:border-stroke-accessible-hover active:border-stroke-accessible-pressed\",\n // checked — Fluent brand ring (the dot itself lives in the indicator),\n // stepping through CompoundBrandStroke Hover/Pressed; dark hover\n // brightens to brand-80 (Button's per-theme ramp). These outrank the\n // neutral hover/active border by specificity so the ring stays brand.\n \"data-[checked]:border-primary data-[checked]:hover:border-brand-70 data-[checked]:active:border-brand-60 dark:data-[checked]:hover:border-brand-80\",\n // disabled — opacity-based (conventions §4), re-expressed against\n // Base UI's data-disabled presence attribute (this renders a\n // <span>, not a native disableable element)\n \"data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n // focus — generic offset-ring recipe (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)\n \"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40\",\n className\n )}\n {...props}\n >\n <Radio.Indicator\n data-slot=\"radio-group-indicator\"\n className=\"flex items-center justify-center\"\n >\n {/* dot — brand fill, ramping with the ring via the root's `group`\n hover/press (CompoundBrandForeground1 Hover/Pressed). */}\n <span className=\"size-2 rounded-full bg-primary transition-colors duration-fast ease-ease group-hover:bg-brand-70 group-active:bg-brand-60 dark:group-hover:bg-brand-80\" />\n </Radio.Indicator>\n </Radio.Root>\n );\n}\n\nexport { RadioGroup, RadioGroupItem };\n"],"mappings":"AAgGI;AAhGJ,SAAS,aAAa;AACtB,SAAS,cAAc,2BAA2B;AAElD,SAAS,UAAU;AAwFnB,SAAS,WAAW;AAAA,EAClB;AAAA,EACA,GAAG;AACL,GAAsC;AACpC,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,cAAc,SAAS;AAAA,MACpC,GAAG;AAAA;AAAA,EACN;AAEJ;AAEA,SAAS,eAAe;AAAA,EACtB;AAAA,EACA,GAAG;AACL,GAA6B;AAC3B,SACE;AAAA,IAAC,MAAM;AAAA,IAAN;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA;AAAA;AAAA,QAGT;AAAA;AAAA,QAEA;AAAA;AAAA,QAEA;AAAA;AAAA;AAAA;AAAA;AAAA,QAKA;AAAA;AAAA;AAAA;AAAA,QAIA;AAAA;AAAA,QAEA;AAAA;AAAA,QAEA;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA,MAEJ;AAAA,QAAC,MAAM;AAAA,QAAN;AAAA,UACC,aAAU;AAAA,UACV,WAAU;AAAA,UAIV,8BAAC,UAAK,WAAU,0JAAyJ;AAAA;AAAA,MAC3K;AAAA;AAAA,EACF;AAEJ;","names":[]}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ComponentProps } from 'react';
|
|
3
|
+
import { Select as Select$1 } from '@base-ui/react/select';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Select — Fluent 2-styled, shadcn-API select (dropdown listbox).
|
|
7
|
+
*
|
|
8
|
+
* This is the kit's first popup/overlay component, so the patterns set here
|
|
9
|
+
* (portal + positioner + popup surface, `shadow-16` flyout elevation, item
|
|
10
|
+
* highlight via Base UI `data-highlighted`) are the reference for Dialog,
|
|
11
|
+
* Tooltip, and Dropdown later.
|
|
12
|
+
*
|
|
13
|
+
* ## Base UI mapping (conventions §9)
|
|
14
|
+
* Behavior — focus management, portalling, open/close state, roving tabindex,
|
|
15
|
+
* type-ahead, collision-aware positioning — genuinely needs a primitive, so the
|
|
16
|
+
* parts wrap `@base-ui/react/select` (namespace import, matching the actual
|
|
17
|
+
* export shape in node_modules, exactly like `avatar.tsx`). shadcn part names
|
|
18
|
+
* are mapped onto Base UI's model:
|
|
19
|
+
*
|
|
20
|
+
* | Exported (shadcn name) | Base UI primitive |
|
|
21
|
+
* | ----------------------- | ---------------------------------------------------- |
|
|
22
|
+
* | `Select` | `Select.Root` |
|
|
23
|
+
* | `SelectGroup` | `Select.Group` |
|
|
24
|
+
* | `SelectValue` | `Select.Value` |
|
|
25
|
+
* | `SelectTrigger` | `Select.Trigger` (+ `Select.Icon` chevron appended) |
|
|
26
|
+
* | `SelectContent` | `Select.Portal` + `Select.Positioner` + `Select.Popup` + `Select.List` |
|
|
27
|
+
* | `SelectLabel` | `Select.GroupLabel` ← see divergence 1 |
|
|
28
|
+
* | `SelectItem` | `Select.Item` (+ `Select.ItemIndicator` + `Select.ItemText`) |
|
|
29
|
+
* | `SelectSeparator` | `Select.Separator` |
|
|
30
|
+
* | `SelectScrollUpButton` | `Select.ScrollUpArrow` |
|
|
31
|
+
* | `SelectScrollDownButton`| `Select.ScrollDownArrow` |
|
|
32
|
+
*
|
|
33
|
+
* ## Divergences from the shadcn/Radix Select API (all deliberate)
|
|
34
|
+
* 1. **`SelectLabel` maps to Base UI `Select.GroupLabel`, not `Select.Label`.**
|
|
35
|
+
* Base UI has *two* label parts: `Select.Label` labels the whole control
|
|
36
|
+
* (like a field `<label>`), while `Select.GroupLabel` is the heading inside a
|
|
37
|
+
* `Select.Group`. shadcn's `SelectLabel` is the group heading, so it maps to
|
|
38
|
+
* `GroupLabel`. Base UI's control-level `Select.Label` is intentionally not
|
|
39
|
+
* re-exported — pair the trigger with the kit's own `Label` component instead.
|
|
40
|
+
* `Label` is a separate registry item (`registry/items/label.json`) and is
|
|
41
|
+
* not a `registryDependency` of `select`, so install it explicitly:
|
|
42
|
+
* `npx shadcn@latest add <registry-url>/r/label.json`.
|
|
43
|
+
*
|
|
44
|
+
* **Labeling recipe.** Give the `Label` an `id` and the `SelectTrigger` its
|
|
45
|
+
* own `id`, then set `aria-labelledby="<label-id> <trigger-id>"` on the
|
|
46
|
+
* trigger (referencing *both*). Pointing only at the external label drops
|
|
47
|
+
* the chosen value from the accessible name; adding the trigger's own id
|
|
48
|
+
* folds the `SelectValue` text (the current selection) back in, so a screen
|
|
49
|
+
* reader announces "<label>, <selected value>". `htmlFor`/`for` can't be
|
|
50
|
+
* used here — the trigger renders a `<button>`, not a labelable form field —
|
|
51
|
+
* which is why the association is `aria-labelledby`, not a wrapping/`for`
|
|
52
|
+
* `<label>`. The Select preview page uses this pattern on every example.
|
|
53
|
+
* 2. **`SelectValue` shows the raw value unless `<Select>` gets an `items` prop.**
|
|
54
|
+
* Unlike Radix (where `SelectValue` mirrors the selected `SelectItem`'s text),
|
|
55
|
+
* Base UI's `Select.Value` renders the raw `value` by default. To display a
|
|
56
|
+
* friendly label, either pass `items` to `<Select>` (Root) — e.g.
|
|
57
|
+
* `items={[{ value: "apple", label: "Apple" }]}` — or pass a render function
|
|
58
|
+
* as `SelectValue`'s child: `<SelectValue>{(v) => labels[v]}</SelectValue>`.
|
|
59
|
+
* This is Base UI's model; the wrapper stays thin and does not paper over it.
|
|
60
|
+
* 3. **`SelectTrigger` sizes follow the kit's Button scale, not shadcn's.**
|
|
61
|
+
* shadcn ships `sm`/`default` at h-8/h-9; this kit maps Fluent's field sizes
|
|
62
|
+
* onto the Button height scale instead — `sm` 24px (`h-6`), `default` 32px
|
|
63
|
+
* (`h-8`, matches `Input`), `lg` 40px (`h-10`) — surfaced as `data-size`.
|
|
64
|
+
* 4. **`SelectContent` positions below the trigger (`alignItemWithTrigger=false`).**
|
|
65
|
+
* Base UI defaults to overlapping the trigger so the selected item's text lines
|
|
66
|
+
* up with the value (native-select behavior). A shadcn-style dropdown that opens
|
|
67
|
+
* beneath the field is the expected Fluent flyout, so this is forced off; the
|
|
68
|
+
* scroll arrows/`data-[side=none]` overlap edge cases go away with it.
|
|
69
|
+
*
|
|
70
|
+
* ## `"use client"` — required (integration-pass correction)
|
|
71
|
+
* Every Base UI Select part module does carry its own `'use client'`
|
|
72
|
+
* directive, so on that basis alone this wrapper could stay server-renderable
|
|
73
|
+
* (same reasoning as `avatar.tsx`). But `CheckmarkRegular`/`ChevronDownRegular`/
|
|
74
|
+
* `ChevronUpRegular` from `@fluentui/react-icons` break that: the package's
|
|
75
|
+
* shared icon-sizing module (`createFluentIcon.styles.js`) calls `@griffel/
|
|
76
|
+
* react`'s `__styles()` at module scope *without* its own `'use client'`
|
|
77
|
+
* directive, even though `__styles` itself is client-only. Rendering any of
|
|
78
|
+
* these icons from a Server Component pulls that module into the server's
|
|
79
|
+
* RSC graph, and `next build` (Turbopack) fails collecting page data with
|
|
80
|
+
* "Attempted to call __styles() from the server but __styles is on the
|
|
81
|
+
* client" — reproduced against `@fluentui/react-icons@2.0.333` /
|
|
82
|
+
* `@griffel/react@1.7.5`, and confirmed to affect every route that shares
|
|
83
|
+
* Turbopack's chunk for these icons, not just this one. `"use client"` here
|
|
84
|
+
* keeps the icon imports inside a client boundary so they're never evaluated
|
|
85
|
+
* on the server. `checkbox.tsx` carries the same fix for the same reason.
|
|
86
|
+
*
|
|
87
|
+
* ## data-slot note
|
|
88
|
+
* `Select` (Root) renders no DOM element of its own and its Base UI props type
|
|
89
|
+
* is strict (no `data-*` passthrough), so it carries no `data-slot`. Every part
|
|
90
|
+
* that renders an element does — `select-trigger`, `select-content`,
|
|
91
|
+
* `select-item`, etc. — which is what tests/consumers hook onto.
|
|
92
|
+
*/
|
|
93
|
+
declare function Select<Value, Multiple extends boolean | undefined = false>(props: Select$1.Root.Props<Value, Multiple>): react.JSX.Element;
|
|
94
|
+
declare function SelectGroup({ className, ...props }: ComponentProps<typeof Select$1.Group>): react.JSX.Element;
|
|
95
|
+
declare function SelectValue({ className, ...props }: ComponentProps<typeof Select$1.Value>): react.JSX.Element;
|
|
96
|
+
/**
|
|
97
|
+
* Trigger — Fluent field look, styled to match `Input` (h-8, rounded-md,
|
|
98
|
+
* border-input, px-3, text-sm, flat at rest). Focus uses the sanctioned
|
|
99
|
+
* per-field deviation from conventions §4: a 2px brand underline painted with
|
|
100
|
+
* an inset box-shadow (no reflow) plus `border-primary`, and the `aria-invalid`
|
|
101
|
+
* treatment declared *after* focus so an invalid+focused field shows the
|
|
102
|
+
* destructive accent. The chevron is appended via `Select.Icon`; the trigger's
|
|
103
|
+
* `[&_svg]` rules size it (`size-4`) and mute it (`text-muted-foreground`).
|
|
104
|
+
*/
|
|
105
|
+
declare function SelectTrigger({ className, children, size, ...props }: ComponentProps<typeof Select$1.Trigger> & {
|
|
106
|
+
/** Fluent field sizes on the Button height scale — see divergence 3. */
|
|
107
|
+
size?: "sm" | "default" | "lg";
|
|
108
|
+
}): react.JSX.Element;
|
|
109
|
+
/**
|
|
110
|
+
* Content — the floating flyout: `Portal` → `Positioner` → `Popup`, with the
|
|
111
|
+
* items in a scrollable `List` between hover-scroll arrows. Surface is
|
|
112
|
+
* `bg-popover` + `border` + `shadow-16` (Fluent flyout elevation, conventions
|
|
113
|
+
* §3.6), `rounded-md`, `min-w` matched to the trigger via Base UI's
|
|
114
|
+
* `--anchor-width`. A subtle scale+fade open/close animation rides Base UI's
|
|
115
|
+
* `data-starting-style`/`data-ending-style` hooks with token durations/easings.
|
|
116
|
+
*/
|
|
117
|
+
declare function SelectContent({ className, children, sideOffset, align, side, ...props }: ComponentProps<typeof Select$1.Popup> & Pick<ComponentProps<typeof Select$1.Positioner>, "side" | "align" | "sideOffset">): react.JSX.Element;
|
|
118
|
+
/**
|
|
119
|
+
* Label — a heading for a `SelectGroup` (Base UI `Select.GroupLabel`), *not* a
|
|
120
|
+
* control-level field label (divergence 1). Small muted caption, Fluent style.
|
|
121
|
+
*/
|
|
122
|
+
declare function SelectLabel({ className, ...props }: ComponentProps<typeof Select$1.GroupLabel>): react.JSX.Element;
|
|
123
|
+
/**
|
|
124
|
+
* Item — one option. Text via `ItemText`; the selected-state check
|
|
125
|
+
* (`CheckmarkRegular`) is absolutely positioned on the right (shadcn placement)
|
|
126
|
+
* inside `ItemIndicator`, which Base UI only mounts for the selected item.
|
|
127
|
+
* `rounded-md` (4px) matches Fluent's `Corner-radius/List/Default`. Highlight
|
|
128
|
+
* (keyboard focus or hover) uses `data-highlighted` → `bg-accent`
|
|
129
|
+
* (`#f0f0f0`) `text-accent-foreground`; Fluent's list-hover token is `#f5f5f5`
|
|
130
|
+
* (one grey step lighter), but the kit keeps `--accent` for token consistency
|
|
131
|
+
* with every other hover surface — the difference is imperceptible on a 32px
|
|
132
|
+
* row. Disabled items are muted + inert.
|
|
133
|
+
*/
|
|
134
|
+
declare function SelectItem({ className, children, ...props }: ComponentProps<typeof Select$1.Item>): react.JSX.Element;
|
|
135
|
+
declare function SelectSeparator({ className, ...props }: ComponentProps<typeof Select$1.Separator>): react.JSX.Element;
|
|
136
|
+
/**
|
|
137
|
+
* ScrollUpButton / ScrollDownButton — Base UI hover-scroll arrows that Base UI
|
|
138
|
+
* only renders when the list overflows in that direction. Rendered as siblings
|
|
139
|
+
* of the `List` inside `SelectContent`, so consumers don't place them manually
|
|
140
|
+
* (shadcn parity — the export exists for API compatibility / manual use).
|
|
141
|
+
*/
|
|
142
|
+
declare function SelectScrollUpButton({ className, ...props }: ComponentProps<typeof Select$1.ScrollUpArrow>): react.JSX.Element;
|
|
143
|
+
declare function SelectScrollDownButton({ className, ...props }: ComponentProps<typeof Select$1.ScrollDownArrow>): react.JSX.Element;
|
|
144
|
+
|
|
145
|
+
export { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue };
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Select as SelectPrimitive } from "@base-ui/react/select";
|
|
4
|
+
import {
|
|
5
|
+
CheckmarkRegular,
|
|
6
|
+
ChevronDownRegular,
|
|
7
|
+
ChevronUpRegular
|
|
8
|
+
} from "@fluentui/react-icons";
|
|
9
|
+
import { cn } from "../../lib/utils.js";
|
|
10
|
+
function Select(props) {
|
|
11
|
+
return /* @__PURE__ */ jsx(SelectPrimitive.Root, { ...props });
|
|
12
|
+
}
|
|
13
|
+
function SelectGroup({
|
|
14
|
+
className,
|
|
15
|
+
...props
|
|
16
|
+
}) {
|
|
17
|
+
return /* @__PURE__ */ jsx(
|
|
18
|
+
SelectPrimitive.Group,
|
|
19
|
+
{
|
|
20
|
+
"data-slot": "select-group",
|
|
21
|
+
className: cn(className),
|
|
22
|
+
...props
|
|
23
|
+
}
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
function SelectValue({
|
|
27
|
+
className,
|
|
28
|
+
...props
|
|
29
|
+
}) {
|
|
30
|
+
return /* @__PURE__ */ jsx(
|
|
31
|
+
SelectPrimitive.Value,
|
|
32
|
+
{
|
|
33
|
+
"data-slot": "select-value",
|
|
34
|
+
className: cn(className),
|
|
35
|
+
...props
|
|
36
|
+
}
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
function SelectTrigger({
|
|
40
|
+
className,
|
|
41
|
+
children,
|
|
42
|
+
size = "default",
|
|
43
|
+
...props
|
|
44
|
+
}) {
|
|
45
|
+
return /* @__PURE__ */ jsxs(
|
|
46
|
+
SelectPrimitive.Trigger,
|
|
47
|
+
{
|
|
48
|
+
"data-slot": "select-trigger",
|
|
49
|
+
"data-size": size,
|
|
50
|
+
className: cn(
|
|
51
|
+
// layout — Fluent field on the Button height scale (divergence 3);
|
|
52
|
+
// default is the 32px medium that matches Input. Resting bottom
|
|
53
|
+
// edge uses the darker NeutralStrokeAccessible (#616161) accent while
|
|
54
|
+
// the other sides stay border-input; focus (border-primary + inset
|
|
55
|
+
// underline) and aria-invalid (border-destructive) both override it.
|
|
56
|
+
"flex w-full items-center justify-between gap-2 rounded-md border border-input border-b-stroke-accessible bg-background py-1 whitespace-nowrap",
|
|
57
|
+
size === "sm" && "h-6 px-2 text-xs",
|
|
58
|
+
size === "default" && "h-8 px-3 text-sm",
|
|
59
|
+
size === "lg" && "h-10 px-4 text-base",
|
|
60
|
+
// placeholder text muted (Base UI sets data-placeholder when no value)
|
|
61
|
+
"data-[placeholder]:text-muted-foreground",
|
|
62
|
+
// keep a long value on one line, left-aligned
|
|
63
|
+
"*:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:text-left",
|
|
64
|
+
// icon defaults — chevron sized + muted unless overridden
|
|
65
|
+
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
|
|
66
|
+
// motion — color + box-shadow so the focus accent animates (§4)
|
|
67
|
+
"outline-none transition-[color,box-shadow] duration-fast ease-ease",
|
|
68
|
+
// disabled — opacity read; Base UI emits both `disabled` + `data-disabled`
|
|
69
|
+
"disabled:pointer-events-none disabled:opacity-50 data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
70
|
+
// focus — Fluent bottom brand accent via inset box-shadow, no reflow (§4)
|
|
71
|
+
"focus-visible:border-primary focus-visible:shadow-[inset_0_-2px_0_0_var(--brand-80)] dark:focus-visible:shadow-[inset_0_-2px_0_0_var(--brand-100)]",
|
|
72
|
+
// invalid — shadcn aria-invalid treatment; after focus so it wins the border
|
|
73
|
+
"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
|
|
74
|
+
"aria-invalid:focus-visible:shadow-[inset_0_-2px_0_0_var(--destructive)]",
|
|
75
|
+
className
|
|
76
|
+
),
|
|
77
|
+
...props,
|
|
78
|
+
children: [
|
|
79
|
+
children,
|
|
80
|
+
/* @__PURE__ */ jsx(SelectPrimitive.Icon, { children: /* @__PURE__ */ jsx(ChevronDownRegular, {}) })
|
|
81
|
+
]
|
|
82
|
+
}
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
function SelectContent({
|
|
86
|
+
className,
|
|
87
|
+
children,
|
|
88
|
+
sideOffset = 4,
|
|
89
|
+
align = "start",
|
|
90
|
+
side = "bottom",
|
|
91
|
+
...props
|
|
92
|
+
}) {
|
|
93
|
+
return /* @__PURE__ */ jsx(SelectPrimitive.Portal, { children: /* @__PURE__ */ jsx(
|
|
94
|
+
SelectPrimitive.Positioner,
|
|
95
|
+
{
|
|
96
|
+
"data-slot": "select-positioner",
|
|
97
|
+
sideOffset,
|
|
98
|
+
align,
|
|
99
|
+
side,
|
|
100
|
+
alignItemWithTrigger: false,
|
|
101
|
+
className: "z-50 outline-none",
|
|
102
|
+
children: /* @__PURE__ */ jsxs(
|
|
103
|
+
SelectPrimitive.Popup,
|
|
104
|
+
{
|
|
105
|
+
"data-slot": "select-content",
|
|
106
|
+
className: cn(
|
|
107
|
+
"relative min-w-[var(--anchor-width)] origin-[var(--transform-origin)] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-16 outline-none",
|
|
108
|
+
// motion — subtle scale + fade on open (enter) / close (exit)
|
|
109
|
+
"transition-[opacity,scale] duration-fast ease-decelerate-mid",
|
|
110
|
+
"data-starting-style:scale-95 data-starting-style:opacity-0",
|
|
111
|
+
"data-ending-style:scale-95 data-ending-style:opacity-0 data-ending-style:ease-accelerate-mid",
|
|
112
|
+
className
|
|
113
|
+
),
|
|
114
|
+
...props,
|
|
115
|
+
children: [
|
|
116
|
+
/* @__PURE__ */ jsx(SelectScrollUpButton, {}),
|
|
117
|
+
/* @__PURE__ */ jsx(
|
|
118
|
+
SelectPrimitive.List,
|
|
119
|
+
{
|
|
120
|
+
"data-slot": "select-list",
|
|
121
|
+
className: "max-h-[var(--available-height)] overflow-y-auto overscroll-contain p-1",
|
|
122
|
+
children
|
|
123
|
+
}
|
|
124
|
+
),
|
|
125
|
+
/* @__PURE__ */ jsx(SelectScrollDownButton, {})
|
|
126
|
+
]
|
|
127
|
+
}
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
) });
|
|
131
|
+
}
|
|
132
|
+
function SelectLabel({
|
|
133
|
+
className,
|
|
134
|
+
...props
|
|
135
|
+
}) {
|
|
136
|
+
return /* @__PURE__ */ jsx(
|
|
137
|
+
SelectPrimitive.GroupLabel,
|
|
138
|
+
{
|
|
139
|
+
"data-slot": "select-label",
|
|
140
|
+
className: cn(
|
|
141
|
+
"px-2 py-1.5 text-xs font-medium text-muted-foreground select-none",
|
|
142
|
+
className
|
|
143
|
+
),
|
|
144
|
+
...props
|
|
145
|
+
}
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
function SelectItem({
|
|
149
|
+
className,
|
|
150
|
+
children,
|
|
151
|
+
...props
|
|
152
|
+
}) {
|
|
153
|
+
return /* @__PURE__ */ jsxs(
|
|
154
|
+
SelectPrimitive.Item,
|
|
155
|
+
{
|
|
156
|
+
"data-slot": "select-item",
|
|
157
|
+
className: cn(
|
|
158
|
+
// layout — Fluent 32px row, 4px list radius, px-2 with room on the
|
|
159
|
+
// right for the check
|
|
160
|
+
"relative flex h-8 w-full cursor-default items-center rounded-md pr-8 pl-2 text-sm outline-none select-none",
|
|
161
|
+
// rest text is NeutralForeground2, darkening to accent-foreground on
|
|
162
|
+
// highlight (Figma validation: Fluent list rows rest at #424242)
|
|
163
|
+
"text-foreground-2 data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground",
|
|
164
|
+
// disabled item — muted + non-interactive
|
|
165
|
+
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
166
|
+
// icons that a consumer puts inside the item text
|
|
167
|
+
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
168
|
+
className
|
|
169
|
+
),
|
|
170
|
+
...props,
|
|
171
|
+
children: [
|
|
172
|
+
/* @__PURE__ */ jsx("span", { className: "absolute right-2 flex size-4 items-center justify-center", children: /* @__PURE__ */ jsx(SelectPrimitive.ItemIndicator, { children: /* @__PURE__ */ jsx(CheckmarkRegular, { className: "size-4" }) }) }),
|
|
173
|
+
/* @__PURE__ */ jsx(SelectPrimitive.ItemText, { children })
|
|
174
|
+
]
|
|
175
|
+
}
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
function SelectSeparator({
|
|
179
|
+
className,
|
|
180
|
+
...props
|
|
181
|
+
}) {
|
|
182
|
+
return /* @__PURE__ */ jsx(
|
|
183
|
+
SelectPrimitive.Separator,
|
|
184
|
+
{
|
|
185
|
+
"data-slot": "select-separator",
|
|
186
|
+
className: cn(
|
|
187
|
+
"pointer-events-none -mx-1 my-1 h-px bg-border",
|
|
188
|
+
className
|
|
189
|
+
),
|
|
190
|
+
...props
|
|
191
|
+
}
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
function SelectScrollUpButton({
|
|
195
|
+
className,
|
|
196
|
+
...props
|
|
197
|
+
}) {
|
|
198
|
+
return /* @__PURE__ */ jsx(
|
|
199
|
+
SelectPrimitive.ScrollUpArrow,
|
|
200
|
+
{
|
|
201
|
+
"data-slot": "select-scroll-up-button",
|
|
202
|
+
className: cn(
|
|
203
|
+
"flex cursor-default items-center justify-center py-1 text-muted-foreground",
|
|
204
|
+
className
|
|
205
|
+
),
|
|
206
|
+
...props,
|
|
207
|
+
children: /* @__PURE__ */ jsx(ChevronUpRegular, { className: "size-4" })
|
|
208
|
+
}
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
function SelectScrollDownButton({
|
|
212
|
+
className,
|
|
213
|
+
...props
|
|
214
|
+
}) {
|
|
215
|
+
return /* @__PURE__ */ jsx(
|
|
216
|
+
SelectPrimitive.ScrollDownArrow,
|
|
217
|
+
{
|
|
218
|
+
"data-slot": "select-scroll-down-button",
|
|
219
|
+
className: cn(
|
|
220
|
+
"flex cursor-default items-center justify-center py-1 text-muted-foreground",
|
|
221
|
+
className
|
|
222
|
+
),
|
|
223
|
+
...props,
|
|
224
|
+
children: /* @__PURE__ */ jsx(ChevronDownRegular, { className: "size-4" })
|
|
225
|
+
}
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
export {
|
|
229
|
+
Select,
|
|
230
|
+
SelectContent,
|
|
231
|
+
SelectGroup,
|
|
232
|
+
SelectItem,
|
|
233
|
+
SelectLabel,
|
|
234
|
+
SelectScrollDownButton,
|
|
235
|
+
SelectScrollUpButton,
|
|
236
|
+
SelectSeparator,
|
|
237
|
+
SelectTrigger,
|
|
238
|
+
SelectValue
|
|
239
|
+
};
|
|
240
|
+
//# sourceMappingURL=select.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/components/ui/select.tsx"],"sourcesContent":["\"use client\";\n\nimport { Select as SelectPrimitive } from \"@base-ui/react/select\";\nimport {\n CheckmarkRegular,\n ChevronDownRegular,\n ChevronUpRegular,\n} from \"@fluentui/react-icons\";\nimport type { ComponentProps } from \"react\";\n\nimport { cn } from \"../../lib/utils\";\n\n/**\n * Select — Fluent 2-styled, shadcn-API select (dropdown listbox).\n *\n * This is the kit's first popup/overlay component, so the patterns set here\n * (portal + positioner + popup surface, `shadow-16` flyout elevation, item\n * highlight via Base UI `data-highlighted`) are the reference for Dialog,\n * Tooltip, and Dropdown later.\n *\n * ## Base UI mapping (conventions §9)\n * Behavior — focus management, portalling, open/close state, roving tabindex,\n * type-ahead, collision-aware positioning — genuinely needs a primitive, so the\n * parts wrap `@base-ui/react/select` (namespace import, matching the actual\n * export shape in node_modules, exactly like `avatar.tsx`). shadcn part names\n * are mapped onto Base UI's model:\n *\n * | Exported (shadcn name) | Base UI primitive |\n * | ----------------------- | ---------------------------------------------------- |\n * | `Select` | `Select.Root` |\n * | `SelectGroup` | `Select.Group` |\n * | `SelectValue` | `Select.Value` |\n * | `SelectTrigger` | `Select.Trigger` (+ `Select.Icon` chevron appended) |\n * | `SelectContent` | `Select.Portal` + `Select.Positioner` + `Select.Popup` + `Select.List` |\n * | `SelectLabel` | `Select.GroupLabel` ← see divergence 1 |\n * | `SelectItem` | `Select.Item` (+ `Select.ItemIndicator` + `Select.ItemText`) |\n * | `SelectSeparator` | `Select.Separator` |\n * | `SelectScrollUpButton` | `Select.ScrollUpArrow` |\n * | `SelectScrollDownButton`| `Select.ScrollDownArrow` |\n *\n * ## Divergences from the shadcn/Radix Select API (all deliberate)\n * 1. **`SelectLabel` maps to Base UI `Select.GroupLabel`, not `Select.Label`.**\n * Base UI has *two* label parts: `Select.Label` labels the whole control\n * (like a field `<label>`), while `Select.GroupLabel` is the heading inside a\n * `Select.Group`. shadcn's `SelectLabel` is the group heading, so it maps to\n * `GroupLabel`. Base UI's control-level `Select.Label` is intentionally not\n * re-exported — pair the trigger with the kit's own `Label` component instead.\n * `Label` is a separate registry item (`registry/items/label.json`) and is\n * not a `registryDependency` of `select`, so install it explicitly:\n * `npx shadcn@latest add <registry-url>/r/label.json`.\n *\n * **Labeling recipe.** Give the `Label` an `id` and the `SelectTrigger` its\n * own `id`, then set `aria-labelledby=\"<label-id> <trigger-id>\"` on the\n * trigger (referencing *both*). Pointing only at the external label drops\n * the chosen value from the accessible name; adding the trigger's own id\n * folds the `SelectValue` text (the current selection) back in, so a screen\n * reader announces \"<label>, <selected value>\". `htmlFor`/`for` can't be\n * used here — the trigger renders a `<button>`, not a labelable form field —\n * which is why the association is `aria-labelledby`, not a wrapping/`for`\n * `<label>`. The Select preview page uses this pattern on every example.\n * 2. **`SelectValue` shows the raw value unless `<Select>` gets an `items` prop.**\n * Unlike Radix (where `SelectValue` mirrors the selected `SelectItem`'s text),\n * Base UI's `Select.Value` renders the raw `value` by default. To display a\n * friendly label, either pass `items` to `<Select>` (Root) — e.g.\n * `items={[{ value: \"apple\", label: \"Apple\" }]}` — or pass a render function\n * as `SelectValue`'s child: `<SelectValue>{(v) => labels[v]}</SelectValue>`.\n * This is Base UI's model; the wrapper stays thin and does not paper over it.\n * 3. **`SelectTrigger` sizes follow the kit's Button scale, not shadcn's.**\n * shadcn ships `sm`/`default` at h-8/h-9; this kit maps Fluent's field sizes\n * onto the Button height scale instead — `sm` 24px (`h-6`), `default` 32px\n * (`h-8`, matches `Input`), `lg` 40px (`h-10`) — surfaced as `data-size`.\n * 4. **`SelectContent` positions below the trigger (`alignItemWithTrigger=false`).**\n * Base UI defaults to overlapping the trigger so the selected item's text lines\n * up with the value (native-select behavior). A shadcn-style dropdown that opens\n * beneath the field is the expected Fluent flyout, so this is forced off; the\n * scroll arrows/`data-[side=none]` overlap edge cases go away with it.\n *\n * ## `\"use client\"` — required (integration-pass correction)\n * Every Base UI Select part module does carry its own `'use client'`\n * directive, so on that basis alone this wrapper could stay server-renderable\n * (same reasoning as `avatar.tsx`). But `CheckmarkRegular`/`ChevronDownRegular`/\n * `ChevronUpRegular` from `@fluentui/react-icons` break that: the package's\n * shared icon-sizing module (`createFluentIcon.styles.js`) calls `@griffel/\n * react`'s `__styles()` at module scope *without* its own `'use client'`\n * directive, even though `__styles` itself is client-only. Rendering any of\n * these icons from a Server Component pulls that module into the server's\n * RSC graph, and `next build` (Turbopack) fails collecting page data with\n * \"Attempted to call __styles() from the server but __styles is on the\n * client\" — reproduced against `@fluentui/react-icons@2.0.333` /\n * `@griffel/react@1.7.5`, and confirmed to affect every route that shares\n * Turbopack's chunk for these icons, not just this one. `\"use client\"` here\n * keeps the icon imports inside a client boundary so they're never evaluated\n * on the server. `checkbox.tsx` carries the same fix for the same reason.\n *\n * ## data-slot note\n * `Select` (Root) renders no DOM element of its own and its Base UI props type\n * is strict (no `data-*` passthrough), so it carries no `data-slot`. Every part\n * that renders an element does — `select-trigger`, `select-content`,\n * `select-item`, etc. — which is what tests/consumers hook onto.\n */\n\nfunction Select<Value, Multiple extends boolean | undefined = false>(\n props: SelectPrimitive.Root.Props<Value, Multiple>\n) {\n return <SelectPrimitive.Root {...props} />;\n}\n\nfunction SelectGroup({\n className,\n ...props\n}: ComponentProps<typeof SelectPrimitive.Group>) {\n return (\n <SelectPrimitive.Group\n data-slot=\"select-group\"\n className={cn(className)}\n {...props}\n />\n );\n}\n\nfunction SelectValue({\n className,\n ...props\n}: ComponentProps<typeof SelectPrimitive.Value>) {\n return (\n <SelectPrimitive.Value\n data-slot=\"select-value\"\n className={cn(className)}\n {...props}\n />\n );\n}\n\n/**\n * Trigger — Fluent field look, styled to match `Input` (h-8, rounded-md,\n * border-input, px-3, text-sm, flat at rest). Focus uses the sanctioned\n * per-field deviation from conventions §4: a 2px brand underline painted with\n * an inset box-shadow (no reflow) plus `border-primary`, and the `aria-invalid`\n * treatment declared *after* focus so an invalid+focused field shows the\n * destructive accent. The chevron is appended via `Select.Icon`; the trigger's\n * `[&_svg]` rules size it (`size-4`) and mute it (`text-muted-foreground`).\n */\nfunction SelectTrigger({\n className,\n children,\n size = \"default\",\n ...props\n}: ComponentProps<typeof SelectPrimitive.Trigger> & {\n /** Fluent field sizes on the Button height scale — see divergence 3. */\n size?: \"sm\" | \"default\" | \"lg\";\n}) {\n return (\n <SelectPrimitive.Trigger\n data-slot=\"select-trigger\"\n data-size={size}\n className={cn(\n // layout — Fluent field on the Button height scale (divergence 3);\n // default is the 32px medium that matches Input. Resting bottom\n // edge uses the darker NeutralStrokeAccessible (#616161) accent while\n // the other sides stay border-input; focus (border-primary + inset\n // underline) and aria-invalid (border-destructive) both override it.\n \"flex w-full items-center justify-between gap-2 rounded-md border border-input border-b-stroke-accessible bg-background py-1 whitespace-nowrap\",\n size === \"sm\" && \"h-6 px-2 text-xs\",\n size === \"default\" && \"h-8 px-3 text-sm\",\n size === \"lg\" && \"h-10 px-4 text-base\",\n // placeholder text muted (Base UI sets data-placeholder when no value)\n \"data-[placeholder]:text-muted-foreground\",\n // keep a long value on one line, left-aligned\n \"*:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:text-left\",\n // icon defaults — chevron sized + muted unless overridden\n \"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground\",\n // motion — color + box-shadow so the focus accent animates (§4)\n \"outline-none transition-[color,box-shadow] duration-fast ease-ease\",\n // disabled — opacity read; Base UI emits both `disabled` + `data-disabled`\n \"disabled:pointer-events-none disabled:opacity-50 data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n // focus — Fluent bottom brand accent via inset box-shadow, no reflow (§4)\n \"focus-visible:border-primary focus-visible:shadow-[inset_0_-2px_0_0_var(--brand-80)] dark:focus-visible:shadow-[inset_0_-2px_0_0_var(--brand-100)]\",\n // invalid — shadcn aria-invalid treatment; after focus so it wins the border\n \"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40\",\n \"aria-invalid:focus-visible:shadow-[inset_0_-2px_0_0_var(--destructive)]\",\n className\n )}\n {...props}\n >\n {children}\n <SelectPrimitive.Icon>\n <ChevronDownRegular />\n </SelectPrimitive.Icon>\n </SelectPrimitive.Trigger>\n );\n}\n\n/**\n * Content — the floating flyout: `Portal` → `Positioner` → `Popup`, with the\n * items in a scrollable `List` between hover-scroll arrows. Surface is\n * `bg-popover` + `border` + `shadow-16` (Fluent flyout elevation, conventions\n * §3.6), `rounded-md`, `min-w` matched to the trigger via Base UI's\n * `--anchor-width`. A subtle scale+fade open/close animation rides Base UI's\n * `data-starting-style`/`data-ending-style` hooks with token durations/easings.\n */\nfunction SelectContent({\n className,\n children,\n sideOffset = 4,\n align = \"start\",\n side = \"bottom\",\n ...props\n}: ComponentProps<typeof SelectPrimitive.Popup> &\n Pick<\n ComponentProps<typeof SelectPrimitive.Positioner>,\n \"side\" | \"align\" | \"sideOffset\"\n >) {\n return (\n <SelectPrimitive.Portal>\n <SelectPrimitive.Positioner\n data-slot=\"select-positioner\"\n sideOffset={sideOffset}\n align={align}\n side={side}\n alignItemWithTrigger={false}\n className=\"z-50 outline-none\"\n >\n <SelectPrimitive.Popup\n data-slot=\"select-content\"\n className={cn(\n \"relative min-w-[var(--anchor-width)] origin-[var(--transform-origin)] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-16 outline-none\",\n // motion — subtle scale + fade on open (enter) / close (exit)\n \"transition-[opacity,scale] duration-fast ease-decelerate-mid\",\n \"data-starting-style:scale-95 data-starting-style:opacity-0\",\n \"data-ending-style:scale-95 data-ending-style:opacity-0 data-ending-style:ease-accelerate-mid\",\n className\n )}\n {...props}\n >\n <SelectScrollUpButton />\n <SelectPrimitive.List\n data-slot=\"select-list\"\n className=\"max-h-[var(--available-height)] overflow-y-auto overscroll-contain p-1\"\n >\n {children}\n </SelectPrimitive.List>\n <SelectScrollDownButton />\n </SelectPrimitive.Popup>\n </SelectPrimitive.Positioner>\n </SelectPrimitive.Portal>\n );\n}\n\n/**\n * Label — a heading for a `SelectGroup` (Base UI `Select.GroupLabel`), *not* a\n * control-level field label (divergence 1). Small muted caption, Fluent style.\n */\nfunction SelectLabel({\n className,\n ...props\n}: ComponentProps<typeof SelectPrimitive.GroupLabel>) {\n return (\n <SelectPrimitive.GroupLabel\n data-slot=\"select-label\"\n className={cn(\n \"px-2 py-1.5 text-xs font-medium text-muted-foreground select-none\",\n className\n )}\n {...props}\n />\n );\n}\n\n/**\n * Item — one option. Text via `ItemText`; the selected-state check\n * (`CheckmarkRegular`) is absolutely positioned on the right (shadcn placement)\n * inside `ItemIndicator`, which Base UI only mounts for the selected item.\n * `rounded-md` (4px) matches Fluent's `Corner-radius/List/Default`. Highlight\n * (keyboard focus or hover) uses `data-highlighted` → `bg-accent`\n * (`#f0f0f0`) `text-accent-foreground`; Fluent's list-hover token is `#f5f5f5`\n * (one grey step lighter), but the kit keeps `--accent` for token consistency\n * with every other hover surface — the difference is imperceptible on a 32px\n * row. Disabled items are muted + inert.\n */\nfunction SelectItem({\n className,\n children,\n ...props\n}: ComponentProps<typeof SelectPrimitive.Item>) {\n return (\n <SelectPrimitive.Item\n data-slot=\"select-item\"\n className={cn(\n // layout — Fluent 32px row, 4px list radius, px-2 with room on the\n // right for the check\n \"relative flex h-8 w-full cursor-default items-center rounded-md pr-8 pl-2 text-sm outline-none select-none\",\n // rest text is NeutralForeground2, darkening to accent-foreground on\n // highlight (Figma validation: Fluent list rows rest at #424242)\n \"text-foreground-2 data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground\",\n // disabled item — muted + non-interactive\n \"data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n // icons that a consumer puts inside the item text\n \"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n className\n )}\n {...props}\n >\n <span className=\"absolute right-2 flex size-4 items-center justify-center\">\n <SelectPrimitive.ItemIndicator>\n <CheckmarkRegular className=\"size-4\" />\n </SelectPrimitive.ItemIndicator>\n </span>\n <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>\n </SelectPrimitive.Item>\n );\n}\n\nfunction SelectSeparator({\n className,\n ...props\n}: ComponentProps<typeof SelectPrimitive.Separator>) {\n return (\n <SelectPrimitive.Separator\n data-slot=\"select-separator\"\n className={cn(\n \"pointer-events-none -mx-1 my-1 h-px bg-border\",\n className\n )}\n {...props}\n />\n );\n}\n\n/**\n * ScrollUpButton / ScrollDownButton — Base UI hover-scroll arrows that Base UI\n * only renders when the list overflows in that direction. Rendered as siblings\n * of the `List` inside `SelectContent`, so consumers don't place them manually\n * (shadcn parity — the export exists for API compatibility / manual use).\n */\nfunction SelectScrollUpButton({\n className,\n ...props\n}: ComponentProps<typeof SelectPrimitive.ScrollUpArrow>) {\n return (\n <SelectPrimitive.ScrollUpArrow\n data-slot=\"select-scroll-up-button\"\n className={cn(\n \"flex cursor-default items-center justify-center py-1 text-muted-foreground\",\n className\n )}\n {...props}\n >\n <ChevronUpRegular className=\"size-4\" />\n </SelectPrimitive.ScrollUpArrow>\n );\n}\n\nfunction SelectScrollDownButton({\n className,\n ...props\n}: ComponentProps<typeof SelectPrimitive.ScrollDownArrow>) {\n return (\n <SelectPrimitive.ScrollDownArrow\n data-slot=\"select-scroll-down-button\"\n className={cn(\n \"flex cursor-default items-center justify-center py-1 text-muted-foreground\",\n className\n )}\n {...props}\n >\n <ChevronDownRegular className=\"size-4\" />\n </SelectPrimitive.ScrollDownArrow>\n );\n}\n\nexport {\n Select,\n SelectGroup,\n SelectValue,\n SelectTrigger,\n SelectContent,\n SelectLabel,\n SelectItem,\n SelectSeparator,\n SelectScrollUpButton,\n SelectScrollDownButton,\n};\n"],"mappings":";AAwGS,cAgDL,YAhDK;AAtGT,SAAS,UAAU,uBAAuB;AAC1C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAGP,SAAS,UAAU;AA2FnB,SAAS,OACP,OACA;AACA,SAAO,oBAAC,gBAAgB,MAAhB,EAAsB,GAAG,OAAO;AAC1C;AAEA,SAAS,YAAY;AAAA,EACnB;AAAA,EACA,GAAG;AACL,GAAiD;AAC/C,SACE;AAAA,IAAC,gBAAgB;AAAA,IAAhB;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,SAAS;AAAA,MACtB,GAAG;AAAA;AAAA,EACN;AAEJ;AAEA,SAAS,YAAY;AAAA,EACnB;AAAA,EACA,GAAG;AACL,GAAiD;AAC/C,SACE;AAAA,IAAC,gBAAgB;AAAA,IAAhB;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,SAAS;AAAA,MACtB,GAAG;AAAA;AAAA,EACN;AAEJ;AAWA,SAAS,cAAc;AAAA,EACrB;AAAA,EACA;AAAA,EACA,OAAO;AAAA,EACP,GAAG;AACL,GAGG;AACD,SACE;AAAA,IAAC,gBAAgB;AAAA,IAAhB;AAAA,MACC,aAAU;AAAA,MACV,aAAW;AAAA,MACX,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAMT;AAAA,QACA,SAAS,QAAQ;AAAA,QACjB,SAAS,aAAa;AAAA,QACtB,SAAS,QAAQ;AAAA;AAAA,QAEjB;AAAA;AAAA,QAEA;AAAA;AAAA,QAEA;AAAA;AAAA,QAEA;AAAA;AAAA,QAEA;AAAA;AAAA,QAEA;AAAA;AAAA,QAEA;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA,MAEH;AAAA;AAAA,QACD,oBAAC,gBAAgB,MAAhB,EACC,8BAAC,sBAAmB,GACtB;AAAA;AAAA;AAAA,EACF;AAEJ;AAUA,SAAS,cAAc;AAAA,EACrB;AAAA,EACA;AAAA,EACA,aAAa;AAAA,EACb,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,GAAG;AACL,GAIK;AACH,SACE,oBAAC,gBAAgB,QAAhB,EACC;AAAA,IAAC,gBAAgB;AAAA,IAAhB;AAAA,MACC,aAAU;AAAA,MACV;AAAA,MACA;AAAA,MACA;AAAA,MACA,sBAAsB;AAAA,MACtB,WAAU;AAAA,MAEV;AAAA,QAAC,gBAAgB;AAAA,QAAhB;AAAA,UACC,aAAU;AAAA,UACV,WAAW;AAAA,YACT;AAAA;AAAA,YAEA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,UACF;AAAA,UACC,GAAG;AAAA,UAEJ;AAAA,gCAAC,wBAAqB;AAAA,YACtB;AAAA,cAAC,gBAAgB;AAAA,cAAhB;AAAA,gBACC,aAAU;AAAA,gBACV,WAAU;AAAA,gBAET;AAAA;AAAA,YACH;AAAA,YACA,oBAAC,0BAAuB;AAAA;AAAA;AAAA,MAC1B;AAAA;AAAA,EACF,GACF;AAEJ;AAMA,SAAS,YAAY;AAAA,EACnB;AAAA,EACA,GAAG;AACL,GAAsD;AACpD,SACE;AAAA,IAAC,gBAAgB;AAAA,IAAhB;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;AAaA,SAAS,WAAW;AAAA,EAClB;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAgD;AAC9C,SACE;AAAA,IAAC,gBAAgB;AAAA,IAAhB;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA;AAAA;AAAA,QAGT;AAAA;AAAA;AAAA,QAGA;AAAA;AAAA,QAEA;AAAA;AAAA,QAEA;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA,MAEJ;AAAA,4BAAC,UAAK,WAAU,4DACd,8BAAC,gBAAgB,eAAhB,EACC,8BAAC,oBAAiB,WAAU,UAAS,GACvC,GACF;AAAA,QACA,oBAAC,gBAAgB,UAAhB,EAA0B,UAAS;AAAA;AAAA;AAAA,EACtC;AAEJ;AAEA,SAAS,gBAAgB;AAAA,EACvB;AAAA,EACA,GAAG;AACL,GAAqD;AACnD,SACE;AAAA,IAAC,gBAAgB;AAAA,IAAhB;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;AAQA,SAAS,qBAAqB;AAAA,EAC5B;AAAA,EACA,GAAG;AACL,GAAyD;AACvD,SACE;AAAA,IAAC,gBAAgB;AAAA,IAAhB;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA,MAEJ,8BAAC,oBAAiB,WAAU,UAAS;AAAA;AAAA,EACvC;AAEJ;AAEA,SAAS,uBAAuB;AAAA,EAC9B;AAAA,EACA,GAAG;AACL,GAA2D;AACzD,SACE;AAAA,IAAC,gBAAgB;AAAA,IAAhB;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA,MAEJ,8BAAC,sBAAmB,WAAU,UAAS;AAAA;AAAA,EACzC;AAEJ;","names":[]}
|