@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/card.tsx"],"sourcesContent":["import type { ComponentProps } from \"react\";\n\nimport { cn } from \"../../lib/utils\";\n\n/**\n * Card — Fluent 2-styled, shadcn-API card family.\n *\n * Structure matches shadcn/ui's current Card exactly: seven composable parts\n * (`Card`, `CardHeader`, `CardTitle`, `CardDescription`, `CardAction`,\n * `CardContent`, `CardFooter`), each a plain `<div>` carrying its own\n * `data-slot`. `CardHeader` is a grid so an optional `CardAction` can sit in\n * its own column/row-span without extra markup (shadcn's `has-data-[slot=...]`\n * trick) — see https://ui.shadcn.com/docs/components/card.\n *\n * Fluent 2 aesthetics layered on top of that skeleton:\n * - `rounded-lg` + `border` + `shadow-4` (Fluent's resting elevation for a\n * filled card — https://storybooks.fluentui.dev/react/?path=/docs/components-card--docs)\n * instead of shadcn's default `rounded-xl` + `shadow-sm`.\n * - `bg-card text-card-foreground` (shadcn semantic tokens, re-pointed for\n * dark mode by tokens.css).\n * - `CardTitle` is `font-semibold` (Fluent card titles are weight 600).\n * - Spacing: Fluent's own medium card padding is 12px (`p-3`), noticeably\n * tighter than shadcn's 24px (`p-6`) default. We split the difference and\n * land on 16px (`gap-4` / `p-4` / `px-4`) — visibly tighter than shadcn so\n * the Fluent influence reads, while keeping enough breathing room that the\n * header/content/footer rhythm still feels like the familiar shadcn\n * composition (a straight 12px port felt cramped next to Button's 32px\n * controls). Document this if you change it.\n *\n * No `variant`/`size` props: shadcn's Card ships none, so there is no `cva`\n * table here (unlike Button) — every part is a plain function component.\n *\n * `interactive` (kit extension): Fluent cards have hover/pressed states when\n * clickable; passing `interactive` raises the resting `shadow-4` to `shadow-8`\n * on hover (Fluent's interactive-card elevation step) with a pointer cursor\n * and `data-interactive` hook. Visual-only — semantics/focus come from what\n * you compose it with (wrap in an `<a>`/`<button>` or add role/tabIndex).\n *\n * Server-safe: no `\"use client\"`, no hooks — the React import is type-only.\n */\n\nfunction Card({\n className,\n interactive = false,\n ...props\n}: ComponentProps<\"div\"> & {\n /** Fluent interactive-card elevation states (hover raises to shadow-8). */\n interactive?: boolean;\n}) {\n return (\n <div\n data-slot=\"card\"\n data-interactive={interactive || undefined}\n className={cn(\n \"flex flex-col gap-4 rounded-lg border bg-card py-4 text-card-foreground shadow-4\",\n interactive &&\n \"cursor-pointer transition-shadow duration-fast ease-ease hover:shadow-8 active:shadow-4\",\n className\n )}\n {...props}\n />\n );\n}\n\nfunction CardHeader({ className, ...props }: ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"card-header\"\n className={cn(\n \"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-4 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-4\",\n className\n )}\n {...props}\n />\n );\n}\n\nfunction CardTitle({ className, ...props }: ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"card-title\"\n className={cn(\"font-semibold leading-none\", className)}\n {...props}\n />\n );\n}\n\nfunction CardDescription({ className, ...props }: ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"card-description\"\n className={cn(\"text-sm text-muted-foreground\", className)}\n {...props}\n />\n );\n}\n\nfunction CardAction({ className, ...props }: ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"card-action\"\n className={cn(\n \"col-start-2 row-span-2 row-start-1 self-start justify-self-end\",\n className\n )}\n {...props}\n />\n );\n}\n\nfunction CardContent({ className, ...props }: ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"card-content\"\n className={cn(\"px-4\", className)}\n {...props}\n />\n );\n}\n\nfunction CardFooter({ className, ...props }: ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"card-footer\"\n className={cn(\"flex items-center px-4 [.border-t]:pt-4\", className)}\n {...props}\n />\n );\n}\n\nexport {\n Card,\n CardHeader,\n CardFooter,\n CardTitle,\n CardAction,\n CardDescription,\n CardContent,\n};\n"],"mappings":"AAkDI;AAhDJ,SAAS,UAAU;AAuCnB,SAAS,KAAK;AAAA,EACZ;AAAA,EACA,cAAc;AAAA,EACd,GAAG;AACL,GAGG;AACD,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,oBAAkB,eAAe;AAAA,MACjC,WAAW;AAAA,QACT;AAAA,QACA,eACE;AAAA,QACF;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;AAEA,SAAS,WAAW,EAAE,WAAW,GAAG,MAAM,GAA0B;AAClE,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;AAEA,SAAS,UAAU,EAAE,WAAW,GAAG,MAAM,GAA0B;AACjE,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,8BAA8B,SAAS;AAAA,MACpD,GAAG;AAAA;AAAA,EACN;AAEJ;AAEA,SAAS,gBAAgB,EAAE,WAAW,GAAG,MAAM,GAA0B;AACvE,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,iCAAiC,SAAS;AAAA,MACvD,GAAG;AAAA;AAAA,EACN;AAEJ;AAEA,SAAS,WAAW,EAAE,WAAW,GAAG,MAAM,GAA0B;AAClE,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;AAEA,SAAS,YAAY,EAAE,WAAW,GAAG,MAAM,GAA0B;AACnE,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,QAAQ,SAAS;AAAA,MAC9B,GAAG;AAAA;AAAA,EACN;AAEJ;AAEA,SAAS,WAAW,EAAE,WAAW,GAAG,MAAM,GAA0B;AAClE,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,2CAA2C,SAAS;AAAA,MACjE,GAAG;AAAA;AAAA,EACN;AAEJ;","names":[]}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ComponentProps } from 'react';
|
|
3
|
+
import { Checkbox as Checkbox$1 } from '@base-ui/react/checkbox';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Checkbox — Fluent 2-styled, shadcn-API checkbox.
|
|
7
|
+
*
|
|
8
|
+
* This is a Base UI case (conventions §9): the checked/unchecked/indeterminate
|
|
9
|
+
* state machine, the hidden native `<input type="checkbox">` for real form
|
|
10
|
+
* participation, and keyboard activation (Space/Enter) are genuine behavior a
|
|
11
|
+
* styled `<span>` can't express on its own, so this wraps
|
|
12
|
+
* `@base-ui/react/checkbox` (`Checkbox`, imported as a namespace exposing
|
|
13
|
+
* `Checkbox.Root` / `Checkbox.Indicator` — matches the actual
|
|
14
|
+
* `export * as Checkbox from "./index.parts.js"` shape in node_modules, the
|
|
15
|
+
* same pattern `avatar.tsx` and `radio-group.tsx` use for their own Base UI
|
|
16
|
+
* subpaths). `Checkbox.Root` renders a `<span role="checkbox">` plus a
|
|
17
|
+
* visually-hidden native `<input type="checkbox">` sibling for form
|
|
18
|
+
* submission; none of that is reimplemented here.
|
|
19
|
+
*
|
|
20
|
+
* `"use client"` is required here — integration-pass correction of the
|
|
21
|
+
* original reasoning below. `@base-ui/react/checkbox`'s `CheckboxRoot` and
|
|
22
|
+
* `CheckboxIndicator` do each carry their own `'use client'` directive, so
|
|
23
|
+
* *those* parts alone wouldn't force this file to be a client boundary
|
|
24
|
+
* (same reasoning as `avatar.tsx` / `radio-group.tsx`). But
|
|
25
|
+
* `CheckmarkFilled`/`SubtractRegular` from `@fluentui/react-icons` don't hold
|
|
26
|
+
* to that pattern: the package's shared icon-sizing module
|
|
27
|
+
* (`createFluentIcon.styles.js`) calls `@griffel/react`'s `__styles()` at
|
|
28
|
+
* module scope *without* its own `'use client'` directive, even though
|
|
29
|
+
* `__styles` itself is client-only. Rendering either icon from a Server
|
|
30
|
+
* Component pulls that module into the server's RSC graph, and `next build`
|
|
31
|
+
* (Turbopack) fails collecting page data with "Attempted to call __styles()
|
|
32
|
+
* from the server but __styles is on the client" — reproduced against
|
|
33
|
+
* `@fluentui/react-icons@2.0.333` / `@griffel/react@1.7.5`, and confirmed to
|
|
34
|
+
* affect every route that shares Turbopack's chunk for these icons, not just
|
|
35
|
+
* this one. `"use client"` here keeps the icon imports inside a client
|
|
36
|
+
* boundary so they're never evaluated on the server. `select.tsx` carries the
|
|
37
|
+
* same fix for the same reason.
|
|
38
|
+
*
|
|
39
|
+
* Box: Fluent 2's checkbox indicator is 16px square (`size-4`) with
|
|
40
|
+
* `borderRadius: tokens.borderRadiusSmall` (confirmed against Fluent UI v9's
|
|
41
|
+
* `useCheckboxStyles.styles.ts` — `indicatorSizeMedium = '16px'`,
|
|
42
|
+
* `borderRadius: tokens.borderRadiusSmall`, checkmark `fontSize: '12px'`).
|
|
43
|
+
* `borderRadiusSmall` is Fluent's 2px step, which is exactly this kit's
|
|
44
|
+
* `rounded-sm` (`--radius-sm: calc(var(--radius) - 4px)` = 6px − 4px = 2px in
|
|
45
|
+
* `tokens.css`) — no new radius token needed. `aspect-square` guards the box
|
|
46
|
+
* against flex-row squish when paired with a `Label` (mirrors
|
|
47
|
+
* `radio-group.tsx`'s `RadioGroupItem`).
|
|
48
|
+
*
|
|
49
|
+
* Borders + interaction ramp: the unchecked box outlines with Fluent's
|
|
50
|
+
* `NeutralStrokeAccessible` ramp — rest `#616161` (`border-stroke-accessible`),
|
|
51
|
+
* hover `#575757` (`border-stroke-accessible-hover`), pressed `#4d4d4d`
|
|
52
|
+
* (`border-stroke-accessible-pressed`) — the higher-contrast neutral Fluent
|
|
53
|
+
* specs for interactive control outlines (extracted from the Figma checkbox),
|
|
54
|
+
* NOT the lighter `--border`/`border-input` grey the box previously used. When
|
|
55
|
+
* checked/indeterminate the box fills brand, and its border + fill step
|
|
56
|
+
* through Fluent's `CompoundBrandBackground` on hover/press (`brand-70` /
|
|
57
|
+
* `brand-60`; dark hover `brand-80`), mirroring Button's documented per-theme
|
|
58
|
+
* brand ramp (conventions §4). The `data-[checked]:hover:`/`:active:`
|
|
59
|
+
* selectors outrank the neutral hover/active border by specificity, so a
|
|
60
|
+
* checked box stays brand (never the neutral grey) while hovered.
|
|
61
|
+
*
|
|
62
|
+
* Checked glyph: `CheckmarkFilled` from `@fluentui/react-icons` at `size-3`
|
|
63
|
+
* (12px, matching Fluent's checkmark `fontSize`) — a real glyph is
|
|
64
|
+
* appropriate here (unlike Radio's plain dot) because Fluent's checkbox
|
|
65
|
+
* indicator specifically shows a checkmark, not just a filled shape.
|
|
66
|
+
*
|
|
67
|
+
* Indeterminate: Base UI's `indeterminate` prop (forwarded to `Checkbox.Root`
|
|
68
|
+
* so it drives `aria-checked="mixed"` and the `data-indeterminate` attribute
|
|
69
|
+
* Base UI uses for its own state) renders a dash instead of a checkmark. The
|
|
70
|
+
* spec offered a choice between `SubtractRegular` and a hand-drawn 8×2 bar;
|
|
71
|
+
* `SubtractRegular` is used since `@fluentui/react-icons` ships that exact
|
|
72
|
+
* glyph and pulling in an icon dependency is already paid for by the checked
|
|
73
|
+
* state. Which icon to render is decided from this component's own
|
|
74
|
+
* `indeterminate` prop (a plain ternary), not a CSS toggle keyed off Base
|
|
75
|
+
* UI's data attributes — simpler and directly assertable in tests, and
|
|
76
|
+
* `Checkbox.Indicator` already only mounts while `checked || indeterminate`
|
|
77
|
+
* (see `CheckboxIndicator.mjs`'s `rendered` check), so no extra
|
|
78
|
+
* checked-conditional is needed beyond that ternary.
|
|
79
|
+
*
|
|
80
|
+
* States: `data-checked`/`data-unchecked`/`data-indeterminate` and
|
|
81
|
+
* `data-disabled` are Base UI's own presence attributes on `Checkbox.Root`'s
|
|
82
|
+
* rendered `<span>` (see `CheckboxRootDataAttributes` in node_modules —
|
|
83
|
+
* boolean presence, not a `data-state="checked"` value pair like Radix),
|
|
84
|
+
* matched here via Tailwind's `data-[checked]:` / `data-[indeterminate]:` /
|
|
85
|
+
* `data-[disabled]:` arbitrary-attribute variants — the same convention
|
|
86
|
+
* `radio-group.tsx` established. There is no native `:disabled` pseudo-class
|
|
87
|
+
* to reach for (confirmed against `useFocusableWhenDisabled.mjs`: a
|
|
88
|
+
* non-native-button Base UI root gets `aria-disabled`, not a real `disabled`
|
|
89
|
+
* DOM attribute, when disabled — and `:disabled` only ever matches elements
|
|
90
|
+
* that support the HTML `disabled` attribute natively), so the
|
|
91
|
+
* opacity/pointer-events disabled recipe from conventions §4 is re-expressed
|
|
92
|
+
* against `data-[disabled]:` instead of `disabled:`. Focus, per conventions
|
|
93
|
+
* §4, stays the generic offset-ring recipe (not the field bottom-accent
|
|
94
|
+
* variant — a checkbox is a selectable control, not a typed-into field).
|
|
95
|
+
* `aria-invalid` is a bare ARIA attribute Base UI passes through untouched
|
|
96
|
+
* (not in `CheckboxRoot`'s destructured prop list), so the standard
|
|
97
|
+
* `aria-invalid:` recipe applies as-is.
|
|
98
|
+
*/
|
|
99
|
+
declare function Checkbox({ className, indeterminate, ...props }: ComponentProps<typeof Checkbox$1.Root>): react.JSX.Element;
|
|
100
|
+
|
|
101
|
+
export { Checkbox };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox";
|
|
4
|
+
import { CheckmarkFilled, SubtractRegular } from "@fluentui/react-icons";
|
|
5
|
+
import { cn } from "../../lib/utils.js";
|
|
6
|
+
function Checkbox({
|
|
7
|
+
className,
|
|
8
|
+
indeterminate = false,
|
|
9
|
+
...props
|
|
10
|
+
}) {
|
|
11
|
+
return /* @__PURE__ */ jsx(
|
|
12
|
+
CheckboxPrimitive.Root,
|
|
13
|
+
{
|
|
14
|
+
"data-slot": "checkbox",
|
|
15
|
+
indeterminate,
|
|
16
|
+
className: cn(
|
|
17
|
+
// layout — Fluent 16px box, small radius (Fluent borderRadiusSmall)
|
|
18
|
+
"aspect-square size-4 shrink-0 cursor-pointer rounded-sm border bg-background",
|
|
19
|
+
"inline-flex items-center justify-center",
|
|
20
|
+
// motion
|
|
21
|
+
"outline-none transition-colors duration-fast ease-ease",
|
|
22
|
+
// unchecked — Fluent NeutralStrokeAccessible rest/hover/pressed ramp
|
|
23
|
+
"border-stroke-accessible hover:border-stroke-accessible-hover active:border-stroke-accessible-pressed",
|
|
24
|
+
// checked / indeterminate — brand fill (Base UI presence attributes)
|
|
25
|
+
"data-[checked]:border-primary data-[checked]:bg-primary data-[checked]:text-primary-foreground",
|
|
26
|
+
"data-[indeterminate]:border-primary data-[indeterminate]:bg-primary data-[indeterminate]:text-primary-foreground",
|
|
27
|
+
// checked interaction ramp — Fluent CompoundBrandBackground Hover/
|
|
28
|
+
// Pressed (border + fill together); dark hover brightens to brand-80
|
|
29
|
+
// per Button's per-theme ramp. Outranks the neutral hover/active
|
|
30
|
+
// border above by specificity so the checked box stays brand.
|
|
31
|
+
"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",
|
|
32
|
+
// disabled — opacity-based (conventions §4), re-expressed against
|
|
33
|
+
// Base UI's data-disabled presence attribute (this renders a
|
|
34
|
+
// <span>, not a native disableable element — see doc comment)
|
|
35
|
+
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
36
|
+
// focus — generic offset-ring recipe (conventions §4)
|
|
37
|
+
"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
|
|
38
|
+
// invalid — shadcn aria-invalid treatment (conventions §4)
|
|
39
|
+
"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
|
|
40
|
+
className
|
|
41
|
+
),
|
|
42
|
+
...props,
|
|
43
|
+
children: /* @__PURE__ */ jsx(
|
|
44
|
+
CheckboxPrimitive.Indicator,
|
|
45
|
+
{
|
|
46
|
+
"data-slot": "checkbox-indicator",
|
|
47
|
+
className: "flex items-center justify-center",
|
|
48
|
+
children: indeterminate ? /* @__PURE__ */ jsx(SubtractRegular, { "aria-hidden": "true", className: "size-3" }) : /* @__PURE__ */ jsx(CheckmarkFilled, { "aria-hidden": "true", className: "size-3" })
|
|
49
|
+
}
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
export {
|
|
55
|
+
Checkbox
|
|
56
|
+
};
|
|
57
|
+
//# sourceMappingURL=checkbox.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/components/ui/checkbox.tsx"],"sourcesContent":["\"use client\";\n\nimport { Checkbox as CheckboxPrimitive } from \"@base-ui/react/checkbox\";\nimport { CheckmarkFilled, SubtractRegular } from \"@fluentui/react-icons\";\nimport type { ComponentProps } from \"react\";\n\nimport { cn } from \"../../lib/utils\";\n\n/**\n * Checkbox — Fluent 2-styled, shadcn-API checkbox.\n *\n * This is a Base UI case (conventions §9): the checked/unchecked/indeterminate\n * state machine, the hidden native `<input type=\"checkbox\">` for real form\n * participation, and keyboard activation (Space/Enter) are genuine behavior a\n * styled `<span>` can't express on its own, so this wraps\n * `@base-ui/react/checkbox` (`Checkbox`, imported as a namespace exposing\n * `Checkbox.Root` / `Checkbox.Indicator` — matches the actual\n * `export * as Checkbox from \"./index.parts.js\"` shape in node_modules, the\n * same pattern `avatar.tsx` and `radio-group.tsx` use for their own Base UI\n * subpaths). `Checkbox.Root` renders a `<span role=\"checkbox\">` plus a\n * visually-hidden native `<input type=\"checkbox\">` sibling for form\n * submission; none of that is reimplemented here.\n *\n * `\"use client\"` is required here — integration-pass correction of the\n * original reasoning below. `@base-ui/react/checkbox`'s `CheckboxRoot` and\n * `CheckboxIndicator` do each carry their own `'use client'` directive, so\n * *those* parts alone wouldn't force this file to be a client boundary\n * (same reasoning as `avatar.tsx` / `radio-group.tsx`). But\n * `CheckmarkFilled`/`SubtractRegular` from `@fluentui/react-icons` don't hold\n * to that pattern: the package's shared icon-sizing module\n * (`createFluentIcon.styles.js`) calls `@griffel/react`'s `__styles()` at\n * module scope *without* its own `'use client'` directive, even though\n * `__styles` itself is client-only. Rendering either icon from a Server\n * Component pulls that module into the server's RSC graph, and `next build`\n * (Turbopack) fails collecting page data with \"Attempted to call __styles()\n * from the server but __styles is on the client\" — reproduced against\n * `@fluentui/react-icons@2.0.333` / `@griffel/react@1.7.5`, and confirmed to\n * affect every route that shares Turbopack's chunk for these icons, not just\n * this one. `\"use client\"` here keeps the icon imports inside a client\n * boundary so they're never evaluated on the server. `select.tsx` carries the\n * same fix for the same reason.\n *\n * Box: Fluent 2's checkbox indicator is 16px square (`size-4`) with\n * `borderRadius: tokens.borderRadiusSmall` (confirmed against Fluent UI v9's\n * `useCheckboxStyles.styles.ts` — `indicatorSizeMedium = '16px'`,\n * `borderRadius: tokens.borderRadiusSmall`, checkmark `fontSize: '12px'`).\n * `borderRadiusSmall` is Fluent's 2px step, which is exactly this kit's\n * `rounded-sm` (`--radius-sm: calc(var(--radius) - 4px)` = 6px − 4px = 2px in\n * `tokens.css`) — no new radius token needed. `aspect-square` guards the box\n * against flex-row squish when paired with a `Label` (mirrors\n * `radio-group.tsx`'s `RadioGroupItem`).\n *\n * Borders + interaction ramp: the unchecked box outlines with Fluent's\n * `NeutralStrokeAccessible` ramp — rest `#616161` (`border-stroke-accessible`),\n * hover `#575757` (`border-stroke-accessible-hover`), pressed `#4d4d4d`\n * (`border-stroke-accessible-pressed`) — the higher-contrast neutral Fluent\n * specs for interactive control outlines (extracted from the Figma checkbox),\n * NOT the lighter `--border`/`border-input` grey the box previously used. When\n * checked/indeterminate the box fills brand, and its border + fill step\n * through Fluent's `CompoundBrandBackground` on hover/press (`brand-70` /\n * `brand-60`; dark hover `brand-80`), mirroring Button's documented per-theme\n * brand ramp (conventions §4). The `data-[checked]:hover:`/`:active:`\n * selectors outrank the neutral hover/active border by specificity, so a\n * checked box stays brand (never the neutral grey) while hovered.\n *\n * Checked glyph: `CheckmarkFilled` from `@fluentui/react-icons` at `size-3`\n * (12px, matching Fluent's checkmark `fontSize`) — a real glyph is\n * appropriate here (unlike Radio's plain dot) because Fluent's checkbox\n * indicator specifically shows a checkmark, not just a filled shape.\n *\n * Indeterminate: Base UI's `indeterminate` prop (forwarded to `Checkbox.Root`\n * so it drives `aria-checked=\"mixed\"` and the `data-indeterminate` attribute\n * Base UI uses for its own state) renders a dash instead of a checkmark. The\n * spec offered a choice between `SubtractRegular` and a hand-drawn 8×2 bar;\n * `SubtractRegular` is used since `@fluentui/react-icons` ships that exact\n * glyph and pulling in an icon dependency is already paid for by the checked\n * state. Which icon to render is decided from this component's own\n * `indeterminate` prop (a plain ternary), not a CSS toggle keyed off Base\n * UI's data attributes — simpler and directly assertable in tests, and\n * `Checkbox.Indicator` already only mounts while `checked || indeterminate`\n * (see `CheckboxIndicator.mjs`'s `rendered` check), so no extra\n * checked-conditional is needed beyond that ternary.\n *\n * States: `data-checked`/`data-unchecked`/`data-indeterminate` and\n * `data-disabled` are Base UI's own presence attributes on `Checkbox.Root`'s\n * rendered `<span>` (see `CheckboxRootDataAttributes` in node_modules —\n * boolean presence, not a `data-state=\"checked\"` value pair like Radix),\n * matched here via Tailwind's `data-[checked]:` / `data-[indeterminate]:` /\n * `data-[disabled]:` arbitrary-attribute variants — the same convention\n * `radio-group.tsx` established. There is no native `:disabled` pseudo-class\n * to reach for (confirmed against `useFocusableWhenDisabled.mjs`: a\n * non-native-button Base UI root gets `aria-disabled`, not a real `disabled`\n * DOM attribute, when disabled — and `:disabled` only ever matches elements\n * that support the HTML `disabled` attribute natively), 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 checkbox is a selectable control, not a typed-into field).\n * `aria-invalid` is a bare ARIA attribute Base UI passes through untouched\n * (not in `CheckboxRoot`'s destructured prop list), so the standard\n * `aria-invalid:` recipe applies as-is.\n */\nfunction Checkbox({\n className,\n indeterminate = false,\n ...props\n}: ComponentProps<typeof CheckboxPrimitive.Root>) {\n return (\n <CheckboxPrimitive.Root\n data-slot=\"checkbox\"\n indeterminate={indeterminate}\n className={cn(\n // layout — Fluent 16px box, small radius (Fluent borderRadiusSmall)\n \"aspect-square size-4 shrink-0 cursor-pointer rounded-sm border bg-background\",\n \"inline-flex items-center justify-center\",\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 / indeterminate — brand fill (Base UI presence attributes)\n \"data-[checked]:border-primary data-[checked]:bg-primary data-[checked]:text-primary-foreground\",\n \"data-[indeterminate]:border-primary data-[indeterminate]:bg-primary data-[indeterminate]:text-primary-foreground\",\n // checked interaction ramp — Fluent CompoundBrandBackground Hover/\n // Pressed (border + fill together); dark hover brightens to brand-80\n // per Button's per-theme ramp. Outranks the neutral hover/active\n // border above by specificity so the checked box stays brand.\n \"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 // 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 — see doc comment)\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 <CheckboxPrimitive.Indicator\n data-slot=\"checkbox-indicator\"\n className=\"flex items-center justify-center\"\n >\n {indeterminate ? (\n <SubtractRegular aria-hidden=\"true\" className=\"size-3\" />\n ) : (\n <CheckmarkFilled aria-hidden=\"true\" className=\"size-3\" />\n )}\n </CheckboxPrimitive.Indicator>\n </CheckboxPrimitive.Root>\n );\n}\n\nexport { Checkbox };\n"],"mappings":";AAgJU;AA9IV,SAAS,YAAY,yBAAyB;AAC9C,SAAS,iBAAiB,uBAAuB;AAGjD,SAAS,UAAU;AAgGnB,SAAS,SAAS;AAAA,EAChB;AAAA,EACA,gBAAgB;AAAA,EAChB,GAAG;AACL,GAAkD;AAChD,SACE;AAAA,IAAC,kBAAkB;AAAA,IAAlB;AAAA,MACC,aAAU;AAAA,MACV;AAAA,MACA,WAAW;AAAA;AAAA,QAET;AAAA,QACA;AAAA;AAAA,QAEA;AAAA;AAAA,QAEA;AAAA;AAAA,QAEA;AAAA,QACA;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,kBAAkB;AAAA,QAAlB;AAAA,UACC,aAAU;AAAA,UACV,WAAU;AAAA,UAET,0BACC,oBAAC,mBAAgB,eAAY,QAAO,WAAU,UAAS,IAEvD,oBAAC,mBAAgB,eAAY,QAAO,WAAU,UAAS;AAAA;AAAA,MAE3D;AAAA;AAAA,EACF;AAEJ;","names":[]}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ComponentProps } from 'react';
|
|
3
|
+
import { Combobox as Combobox$1 } from '@base-ui/react/combobox';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Combobox — Fluent 2-styled, composable filterable select built on Base UI.
|
|
7
|
+
*
|
|
8
|
+
* This is the REFERENCE for the kit's filter-list family: Multi Select and
|
|
9
|
+
* Command Menu are built by later agents reading THIS file the way the overlay
|
|
10
|
+
* batch read `select.tsx`. Read the divergence notes and extension-point notes
|
|
11
|
+
* below before extending it.
|
|
12
|
+
*
|
|
13
|
+
* It is the typed-into sibling of `Select`: the field is the kit's Input recipe
|
|
14
|
+
* (h-8, `border-input` + `border-b-stroke-accessible`, bottom brand-accent
|
|
15
|
+
* focus) instead of a button trigger, and the flyout is `SelectContent`'s
|
|
16
|
+
* validated popup recipe verbatim (`bg-popover`, `border`, `shadow-16`,
|
|
17
|
+
* `rounded-md`, `min-w-[var(--anchor-width)]`, scale+fade motion on Base UI's
|
|
18
|
+
* `data-starting-style`/`data-ending-style` hooks). Item rows are `SelectItem`'s
|
|
19
|
+
* 32px `data-[highlighted]` rows with the right-side check.
|
|
20
|
+
*
|
|
21
|
+
* ## Why `@base-ui/react/combobox` and NOT `@base-ui/react/autocomplete`
|
|
22
|
+
* Base UI ships BOTH. They share the same AriaCombobox engine but draw the line
|
|
23
|
+
* at *selection*:
|
|
24
|
+
* - **Combobox** has a selection model — `value`/`onValueChange` (the committed
|
|
25
|
+
* selected value, single or `multiple`) that is SEPARATE from
|
|
26
|
+
* `inputValue`/`onInputValueChange` (the transient filter text). Picking an
|
|
27
|
+
* item commits a discrete value; the input filters the list. This is exactly a
|
|
28
|
+
* "filterable select".
|
|
29
|
+
* - **Autocomplete** is `selectionMode: 'none'` — it deliberately omits
|
|
30
|
+
* `selectedValue`/`onValueChange`; the input value IS the only state. It's for
|
|
31
|
+
* free-text-with-suggestions (search boxes), where there is no discrete
|
|
32
|
+
* committed choice.
|
|
33
|
+
* The whole filter-list family (this, Multi Select, Command Menu) selects
|
|
34
|
+
* discrete items, so it wraps **combobox**. A future free-text search field is
|
|
35
|
+
* the one case that would wrap autocomplete instead.
|
|
36
|
+
*
|
|
37
|
+
* ## Base UI mapping (conventions §9)
|
|
38
|
+
* Namespace import of `@base-ui/react/combobox`, matching the export shape in
|
|
39
|
+
* node_modules (like `select.tsx`/`avatar.tsx`). shadcn-style part names mapped
|
|
40
|
+
* onto Base UI's model:
|
|
41
|
+
*
|
|
42
|
+
* | Exported (kit name) | Base UI primitive |
|
|
43
|
+
* | ------------------- | ---------------------------------------------------------- |
|
|
44
|
+
* | `Combobox` | `Combobox.Root` |
|
|
45
|
+
* | `ComboboxInput` | field wrapper + `Combobox.Input` + `Combobox.Clear` + `Combobox.Trigger` (+ `Combobox.Icon`) |
|
|
46
|
+
* | `ComboboxContent` | `Combobox.Portal` + `Combobox.Positioner` + `Combobox.Popup` |
|
|
47
|
+
* | `ComboboxList` | `Combobox.List` ← see divergence 5 |
|
|
48
|
+
* | `ComboboxEmpty` | `Combobox.Empty` |
|
|
49
|
+
* | `ComboboxItem` | `Combobox.Item` (+ `Combobox.ItemIndicator`) |
|
|
50
|
+
* | `ComboboxGroup` | `Combobox.Group` |
|
|
51
|
+
* | `ComboboxLabel` | `Combobox.GroupLabel` ← see divergence 1 |
|
|
52
|
+
* | `ComboboxSeparator` | `Combobox.Separator` |
|
|
53
|
+
* | `ComboboxValue` | `Combobox.Value` ← see divergence 4 |
|
|
54
|
+
*
|
|
55
|
+
* ## Divergences vs Base UI naming (all deliberate)
|
|
56
|
+
* 1. **`ComboboxLabel` maps to `Combobox.GroupLabel`, not `Combobox.Label`.**
|
|
57
|
+
* Base UI has two label parts: `Combobox.Label` labels the whole control (a
|
|
58
|
+
* field `<label>`), `Combobox.GroupLabel` is the heading inside a
|
|
59
|
+
* `Combobox.Group`. Like Select, the kit exposes the *group* heading as
|
|
60
|
+
* `ComboboxLabel` and does NOT re-export the control-level label — pair
|
|
61
|
+
* `ComboboxInput` with the kit's own `Label` component (a separate registry
|
|
62
|
+
* item; install it explicitly). Labeling recipe: give `Label` an `id` and set
|
|
63
|
+
* `aria-labelledby` on the `ComboboxInput` — but unlike Select's button
|
|
64
|
+
* trigger, the input IS a labelable form field, so a plain `htmlFor`/`id`
|
|
65
|
+
* pairing works too (the preview uses `htmlFor` + `id`).
|
|
66
|
+
* 2. **`ComboboxInput` is a composed field, not a bare `<input>`.** It renders a
|
|
67
|
+
* field-chrome wrapper (`data-slot="combobox-input-wrapper"`) around the real
|
|
68
|
+
* `Combobox.Input` (`data-slot="combobox-input"`), plus a `Combobox.Clear`
|
|
69
|
+
* reset button (Base UI auto-unmounts it when the field is empty) and a
|
|
70
|
+
* `Combobox.Trigger` chevron that toggles the popup. This is the sibling of
|
|
71
|
+
* `SelectTrigger`, which likewise bakes its chevron in. `className`, `ref` and
|
|
72
|
+
* all other props forward to the inner `<input>`; the wrapper is styled via
|
|
73
|
+
* `wrapperClassName`. The chevron/clear are `tabIndex={-1}` (not extra tab
|
|
74
|
+
* stops — the input already owns `role="combobox"` + keyboard control).
|
|
75
|
+
* 3. **No `Combobox.Arrow`, no scroll buttons.** The popup is a flyout beneath
|
|
76
|
+
* the field (not an anchored caret-overlap like a native `<select>`), so the
|
|
77
|
+
* positioner arrow and Select's ScrollUp/DownArrow parts are omitted; the
|
|
78
|
+
* `List` scrolls with `overflow-y-auto`.
|
|
79
|
+
* 4. **`ComboboxValue` renders the raw selected value unless `items` is given.**
|
|
80
|
+
* Same model as Select's `SelectValue`: `Combobox.Value` shows the raw
|
|
81
|
+
* `value` unless `<Combobox items={[{ value, label }]}>` is supplied or a
|
|
82
|
+
* render child is used. It is exported for callers who display the selection
|
|
83
|
+
* outside the input; the field itself shows the selection as input text (Base
|
|
84
|
+
* UI fills the input on select), so most single-select UIs never need it.
|
|
85
|
+
* 5. **`ComboboxList` is a separate part (Select auto-wraps its `List`).** Base
|
|
86
|
+
* UI does the filtering, so the natural API passes `items` to `<Combobox>` and
|
|
87
|
+
* renders the list with a render-function child:
|
|
88
|
+
* `<ComboboxList>{(item) => <ComboboxItem value={item}>{item.label}</ComboboxItem>}</ComboboxList>`.
|
|
89
|
+
* `ComboboxEmpty` is a sibling of `ComboboxList` INSIDE `ComboboxContent` (not
|
|
90
|
+
* a `List` child) — it mirrors Base UI's own structure and lets the no-match
|
|
91
|
+
* caption sit at the popup level. Static `ComboboxItem` children also work,
|
|
92
|
+
* but the built-in filter only runs against the `items` prop, so filtering UIs
|
|
93
|
+
* should pass `items` + the render-function form. Unlike `SelectContent`
|
|
94
|
+
* (which owns its `List`), keeping `List` explicit is what makes correct
|
|
95
|
+
* `Empty` placement and the render-function API possible.
|
|
96
|
+
*
|
|
97
|
+
* ## Divergences vs shadcn's Combobox recipe
|
|
98
|
+
* shadcn has NO Combobox component — its docs "Combobox" is a hand-rolled recipe
|
|
99
|
+
* composing `Popover` + `Command` (cmdk) with local `useState`, `open`, and a
|
|
100
|
+
* manually-wired `CommandInput`/`CommandItem`/`onSelect`. This kit instead ships
|
|
101
|
+
* a real `Combobox*` part family:
|
|
102
|
+
* - **Filtering is built in.** Base UI's `Combobox.Root` filters `List` items
|
|
103
|
+
* against the input value automatically (`mode="list"` default); the shadcn
|
|
104
|
+
* recipe leans on cmdk's `Command` to filter. No `cmdk` dependency here.
|
|
105
|
+
* - **One selection model, not two hooks.** shadcn juggles Popover `open` +
|
|
106
|
+
* Command `value` + an external `value` `useState`. Here `value`/`inputValue`
|
|
107
|
+
* are one model on the Root — controlled or uncontrolled — exposed honestly.
|
|
108
|
+
* - **Composable parts, not a monolith.** `Combobox`, `ComboboxInput`,
|
|
109
|
+
* `ComboboxContent`, `ComboboxItem`, … read as the sibling of `Select`, not as
|
|
110
|
+
* a bespoke Popover+Command assembly the consumer re-wires each time.
|
|
111
|
+
*
|
|
112
|
+
* ## Extension points for Multi Select (do NOT style chips here)
|
|
113
|
+
* `multiple` is passed through UNBLOCKED (`Combobox`'s generic second type param,
|
|
114
|
+
* exactly like `Select`). When `multiple` is set, Base UI:
|
|
115
|
+
* - makes `value`/`onValueChange` arrays;
|
|
116
|
+
* - keeps `Combobox.Clear` clearing to `[]`;
|
|
117
|
+
* - exposes `Combobox.Chips` / `Combobox.Chip` / `Combobox.ChipRemove` for a
|
|
118
|
+
* token field, and `Combobox.Row` for grid layouts.
|
|
119
|
+
* The Multi Select agent should build its OWN field part (e.g. `MultiSelectInput`
|
|
120
|
+
* / a chips field) that wraps `Combobox.Chips` around `Combobox.Input` inside the
|
|
121
|
+
* same field-chrome wrapper this file uses — it must NOT reuse `ComboboxInput`,
|
|
122
|
+
* which is the single-line surface. This file intentionally leaves chip styling
|
|
123
|
+
* unspecified; the `combobox-input-wrapper` class string is the shared field
|
|
124
|
+
* recipe to copy. Everything else (`ComboboxContent`, `ComboboxItem`,
|
|
125
|
+
* `ComboboxGroup`, `ComboboxLabel`, `ComboboxEmpty`) is reusable as-is.
|
|
126
|
+
*
|
|
127
|
+
* ## `"use client"` — required
|
|
128
|
+
* `@fluentui/react-icons` forces it: the package's shared icon-sizing module
|
|
129
|
+
* (`createFluentIcon.styles.js`) calls `@griffel/react`'s `__styles()` at module
|
|
130
|
+
* scope without its own `'use client'`, so importing an icon into a Server
|
|
131
|
+
* Component makes `next build` (Turbopack) fail collecting page data. Same fix
|
|
132
|
+
* and root cause as `select.tsx`/`checkbox.tsx` (conventions §9). Every Base UI
|
|
133
|
+
* Combobox part module already carries its own `'use client'`.
|
|
134
|
+
*
|
|
135
|
+
* ## data-slot note
|
|
136
|
+
* `Combobox` (Root) renders no DOM element and its Base UI props type is strict
|
|
137
|
+
* (no `data-*` passthrough), so it carries no `data-slot`. Every part that
|
|
138
|
+
* renders an element does (`combobox-input`, `combobox-content`, `combobox-item`,
|
|
139
|
+
* …) — the styling/testing hook consumers rely on.
|
|
140
|
+
*/
|
|
141
|
+
declare function Combobox<Value, Multiple extends boolean | undefined = false>(props: Combobox$1.Root.Props<Value, Multiple>): react.JSX.Element;
|
|
142
|
+
/**
|
|
143
|
+
* Value — displays the selected value (Base UI `Combobox.Value`). Renders the
|
|
144
|
+
* raw value unless `items` is passed to `<Combobox>` or a render child is used
|
|
145
|
+
* (divergence 4). Exported for showing the selection outside the input; the
|
|
146
|
+
* field itself already reflects the selection as input text.
|
|
147
|
+
*/
|
|
148
|
+
declare function ComboboxValue(props: ComponentProps<typeof Combobox$1.Value>): react.JSX.Element;
|
|
149
|
+
/**
|
|
150
|
+
* Input — the Fluent single-line combobox field (divergence 2). A field-chrome
|
|
151
|
+
* wrapper holds the real `<input>` (transparent, fills the field), a `Clear`
|
|
152
|
+
* reset button (auto-unmounts when empty) and a chevron `Trigger`. The wrapper
|
|
153
|
+
* carries the kit Input recipe: h-8, `rounded-md`, `border-input` +
|
|
154
|
+
* `border-b-stroke-accessible` at rest, and the Fluent bottom brand accent on
|
|
155
|
+
* focus via an inset box-shadow (no reflow) — keyed off `has-[input:focus-visible]`
|
|
156
|
+
* because focus lands on the inner input, not the wrapper. `aria-invalid` on the
|
|
157
|
+
* input swaps the field to destructive, declared after focus so it wins.
|
|
158
|
+
* `className`/`ref`/`...props` forward to the inner `<input>`; style the wrapper
|
|
159
|
+
* with `wrapperClassName`.
|
|
160
|
+
*/
|
|
161
|
+
declare function ComboboxInput({ className, wrapperClassName, ...props }: ComponentProps<typeof Combobox$1.Input> & {
|
|
162
|
+
wrapperClassName?: string;
|
|
163
|
+
}): react.JSX.Element;
|
|
164
|
+
/**
|
|
165
|
+
* Content — the floating flyout: `Portal` → `Positioner` → `Popup`. Surface is
|
|
166
|
+
* `SelectContent`'s validated recipe verbatim: `bg-popover` + `border` +
|
|
167
|
+
* `shadow-16` (Fluent flyout elevation), `rounded-md`, min-width matched to the
|
|
168
|
+
* field via Base UI's `--anchor-width`, and the scale+fade open/close animation
|
|
169
|
+
* on `data-starting-style`/`data-ending-style` with token durations/easings.
|
|
170
|
+
* `transition-[opacity,scale]` (never transform-based — conventions §3.5).
|
|
171
|
+
* Children are placed directly in the popup: a `ComboboxEmpty` (no-match
|
|
172
|
+
* caption) and a `ComboboxList` (the scroll container). See divergence 5.
|
|
173
|
+
*/
|
|
174
|
+
declare function ComboboxContent({ className, children, sideOffset, align, side, ...props }: ComponentProps<typeof Combobox$1.Popup> & Pick<ComponentProps<typeof Combobox$1.Positioner>, "side" | "align" | "sideOffset">): react.JSX.Element;
|
|
175
|
+
/**
|
|
176
|
+
* List — the scrollable options container (Base UI `Combobox.List`, divergence
|
|
177
|
+
* 5). `children` may be a render function `(item, index) => …` (the filtering
|
|
178
|
+
* API: pass `items` to `<Combobox>` and map each) or static `ComboboxItem`
|
|
179
|
+
* children. Scrolls at `overflow-y-auto`; capped at `--available-height` or 20rem.
|
|
180
|
+
*/
|
|
181
|
+
declare function ComboboxList({ className, ...props }: ComponentProps<typeof Combobox$1.List>): react.JSX.Element;
|
|
182
|
+
/**
|
|
183
|
+
* Empty — the no-match state (Base UI `Combobox.Empty`). Base UI renders its
|
|
184
|
+
* children ONLY when the filtered list is empty, and its root element must stay
|
|
185
|
+
* mounted to announce (it carries `role="presentation"` + a live region), so
|
|
186
|
+
* always place a `<ComboboxEmpty>` in the content rather than conditionally
|
|
187
|
+
* rendering it. Muted, centered caption.
|
|
188
|
+
*/
|
|
189
|
+
declare function ComboboxEmpty({ className, ...props }: ComponentProps<typeof Combobox$1.Empty>): react.JSX.Element;
|
|
190
|
+
/**
|
|
191
|
+
* Item — one option (Base UI `Combobox.Item`, renders a `<div>` with
|
|
192
|
+
* `role="option"`). `value` selects it; the selected-state check
|
|
193
|
+
* (`CheckmarkRegular`) is absolutely positioned on the right inside
|
|
194
|
+
* `ItemIndicator`, which Base UI only mounts for the selected item. 32px row,
|
|
195
|
+
* 4px list radius, highlight (keyboard focus or hover) via `data-highlighted` →
|
|
196
|
+
* `bg-accent`, disabled items muted + inert — the `SelectItem` recipe.
|
|
197
|
+
*/
|
|
198
|
+
declare function ComboboxItem({ className, children, ...props }: ComponentProps<typeof Combobox$1.Item>): react.JSX.Element;
|
|
199
|
+
/**
|
|
200
|
+
* Group — wraps a set of items under a `ComboboxLabel` heading (Base UI
|
|
201
|
+
* `Combobox.Group`). With Base UI's built-in filtering, pass grouped `items` to
|
|
202
|
+
* `<Combobox>` so empty groups drop out automatically; see the preview.
|
|
203
|
+
*/
|
|
204
|
+
declare function ComboboxGroup({ className, ...props }: ComponentProps<typeof Combobox$1.Group>): react.JSX.Element;
|
|
205
|
+
/**
|
|
206
|
+
* Label — a heading for a `ComboboxGroup` (Base UI `Combobox.GroupLabel`), NOT a
|
|
207
|
+
* control-level field label (divergence 1). Small muted caption, Fluent style.
|
|
208
|
+
*/
|
|
209
|
+
declare function ComboboxLabel({ className, ...props }: ComponentProps<typeof Combobox$1.GroupLabel>): react.JSX.Element;
|
|
210
|
+
declare function ComboboxSeparator({ className, ...props }: ComponentProps<typeof Combobox$1.Separator>): react.JSX.Element;
|
|
211
|
+
|
|
212
|
+
export { Combobox, ComboboxContent, ComboboxEmpty, ComboboxGroup, ComboboxInput, ComboboxItem, ComboboxLabel, ComboboxList, ComboboxSeparator, ComboboxValue };
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Combobox as ComboboxPrimitive } from "@base-ui/react/combobox";
|
|
4
|
+
import {
|
|
5
|
+
CheckmarkRegular,
|
|
6
|
+
ChevronDownRegular,
|
|
7
|
+
DismissRegular
|
|
8
|
+
} from "@fluentui/react-icons";
|
|
9
|
+
import { cn } from "../../lib/utils.js";
|
|
10
|
+
function Combobox(props) {
|
|
11
|
+
return /* @__PURE__ */ jsx(ComboboxPrimitive.Root, { ...props });
|
|
12
|
+
}
|
|
13
|
+
function ComboboxValue(props) {
|
|
14
|
+
return /* @__PURE__ */ jsx(ComboboxPrimitive.Value, { "data-slot": "combobox-value", ...props });
|
|
15
|
+
}
|
|
16
|
+
function ComboboxInput({
|
|
17
|
+
className,
|
|
18
|
+
wrapperClassName,
|
|
19
|
+
...props
|
|
20
|
+
}) {
|
|
21
|
+
return /* @__PURE__ */ jsxs(
|
|
22
|
+
"div",
|
|
23
|
+
{
|
|
24
|
+
"data-slot": "combobox-input-wrapper",
|
|
25
|
+
className: cn(
|
|
26
|
+
// layout — Fluent medium field, 32px, matches Input. Resting bottom edge
|
|
27
|
+
// uses the darker NeutralStrokeAccessible (#616161) accent; the other
|
|
28
|
+
// sides stay border-input. Focus (border-primary + inset underline) and
|
|
29
|
+
// aria-invalid (border-destructive) both override it.
|
|
30
|
+
"flex h-8 w-full items-center rounded-md border border-input border-b-stroke-accessible bg-background text-sm",
|
|
31
|
+
// motion — color + box-shadow so the focus accent animates (§4)
|
|
32
|
+
"transition-[color,box-shadow] duration-fast ease-ease",
|
|
33
|
+
// focus — Fluent bottom brand accent via inset box-shadow, no reflow (§4).
|
|
34
|
+
// has-[input:focus-visible] because focus is on the inner input; text
|
|
35
|
+
// inputs match :focus-visible on pointer focus too, so this covers both.
|
|
36
|
+
"has-[input:focus-visible]:border-primary has-[input:focus-visible]:shadow-[inset_0_-2px_0_0_var(--brand-80)] dark:has-[input:focus-visible]:shadow-[inset_0_-2px_0_0_var(--brand-100)]",
|
|
37
|
+
// invalid — shadcn aria-invalid treatment; after focus so it wins the border
|
|
38
|
+
"has-[input[aria-invalid='true']]:border-destructive has-[input[aria-invalid='true']]:ring-destructive/20 dark:has-[input[aria-invalid='true']]:ring-destructive/40",
|
|
39
|
+
"has-[input[aria-invalid='true']:focus-visible]:shadow-[inset_0_-2px_0_0_var(--destructive)]",
|
|
40
|
+
// disabled — opacity read when the whole control is disabled
|
|
41
|
+
"has-[input:disabled]:pointer-events-none has-[input:disabled]:opacity-50",
|
|
42
|
+
wrapperClassName
|
|
43
|
+
),
|
|
44
|
+
children: [
|
|
45
|
+
/* @__PURE__ */ jsx(
|
|
46
|
+
ComboboxPrimitive.Input,
|
|
47
|
+
{
|
|
48
|
+
"data-slot": "combobox-input",
|
|
49
|
+
className: cn(
|
|
50
|
+
"h-full min-w-0 flex-1 bg-transparent px-3 py-1 outline-none",
|
|
51
|
+
"placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground",
|
|
52
|
+
className
|
|
53
|
+
),
|
|
54
|
+
...props
|
|
55
|
+
}
|
|
56
|
+
),
|
|
57
|
+
/* @__PURE__ */ jsx(
|
|
58
|
+
ComboboxPrimitive.Clear,
|
|
59
|
+
{
|
|
60
|
+
"data-slot": "combobox-clear",
|
|
61
|
+
"aria-label": "Clear",
|
|
62
|
+
tabIndex: -1,
|
|
63
|
+
className: cn(
|
|
64
|
+
"mr-0.5 flex size-6 shrink-0 items-center justify-center rounded-sm text-muted-foreground outline-none",
|
|
65
|
+
"transition-colors duration-fast ease-ease hover:text-foreground",
|
|
66
|
+
"focus-visible:ring-2 focus-visible:ring-ring",
|
|
67
|
+
"[&_svg]:pointer-events-none [&_svg]:size-4"
|
|
68
|
+
),
|
|
69
|
+
children: /* @__PURE__ */ jsx(DismissRegular, {})
|
|
70
|
+
}
|
|
71
|
+
),
|
|
72
|
+
/* @__PURE__ */ jsx(
|
|
73
|
+
ComboboxPrimitive.Trigger,
|
|
74
|
+
{
|
|
75
|
+
"data-slot": "combobox-trigger",
|
|
76
|
+
"aria-label": "Show options",
|
|
77
|
+
tabIndex: -1,
|
|
78
|
+
className: cn(
|
|
79
|
+
"mr-2 flex shrink-0 items-center text-muted-foreground outline-none",
|
|
80
|
+
"[&_svg]:pointer-events-none [&_svg]:size-4"
|
|
81
|
+
),
|
|
82
|
+
children: /* @__PURE__ */ jsx(ComboboxPrimitive.Icon, { "data-slot": "combobox-icon", children: /* @__PURE__ */ jsx(ChevronDownRegular, {}) })
|
|
83
|
+
}
|
|
84
|
+
)
|
|
85
|
+
]
|
|
86
|
+
}
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
function ComboboxContent({
|
|
90
|
+
className,
|
|
91
|
+
children,
|
|
92
|
+
sideOffset = 4,
|
|
93
|
+
align = "start",
|
|
94
|
+
side = "bottom",
|
|
95
|
+
...props
|
|
96
|
+
}) {
|
|
97
|
+
return /* @__PURE__ */ jsx(ComboboxPrimitive.Portal, { children: /* @__PURE__ */ jsx(
|
|
98
|
+
ComboboxPrimitive.Positioner,
|
|
99
|
+
{
|
|
100
|
+
"data-slot": "combobox-positioner",
|
|
101
|
+
sideOffset,
|
|
102
|
+
align,
|
|
103
|
+
side,
|
|
104
|
+
className: "z-50 outline-none",
|
|
105
|
+
children: /* @__PURE__ */ jsx(
|
|
106
|
+
ComboboxPrimitive.Popup,
|
|
107
|
+
{
|
|
108
|
+
"data-slot": "combobox-content",
|
|
109
|
+
className: cn(
|
|
110
|
+
"relative min-w-[var(--anchor-width)] origin-[var(--transform-origin)] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-16 outline-none",
|
|
111
|
+
// motion — subtle scale + fade on open (enter) / close (exit)
|
|
112
|
+
"transition-[opacity,scale] duration-fast ease-decelerate-mid",
|
|
113
|
+
"data-starting-style:scale-95 data-starting-style:opacity-0",
|
|
114
|
+
"data-ending-style:scale-95 data-ending-style:opacity-0 data-ending-style:ease-accelerate-mid",
|
|
115
|
+
className
|
|
116
|
+
),
|
|
117
|
+
...props,
|
|
118
|
+
children
|
|
119
|
+
}
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
) });
|
|
123
|
+
}
|
|
124
|
+
function ComboboxList({
|
|
125
|
+
className,
|
|
126
|
+
...props
|
|
127
|
+
}) {
|
|
128
|
+
return /* @__PURE__ */ jsx(
|
|
129
|
+
ComboboxPrimitive.List,
|
|
130
|
+
{
|
|
131
|
+
"data-slot": "combobox-list",
|
|
132
|
+
className: cn(
|
|
133
|
+
"max-h-[min(var(--available-height),20rem)] overflow-y-auto overscroll-contain p-1",
|
|
134
|
+
className
|
|
135
|
+
),
|
|
136
|
+
...props
|
|
137
|
+
}
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
function ComboboxEmpty({
|
|
141
|
+
className,
|
|
142
|
+
...props
|
|
143
|
+
}) {
|
|
144
|
+
return /* @__PURE__ */ jsx(
|
|
145
|
+
ComboboxPrimitive.Empty,
|
|
146
|
+
{
|
|
147
|
+
"data-slot": "combobox-empty",
|
|
148
|
+
className: cn("py-6 text-center text-sm text-muted-foreground", className),
|
|
149
|
+
...props
|
|
150
|
+
}
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
function ComboboxItem({
|
|
154
|
+
className,
|
|
155
|
+
children,
|
|
156
|
+
...props
|
|
157
|
+
}) {
|
|
158
|
+
return /* @__PURE__ */ jsxs(
|
|
159
|
+
ComboboxPrimitive.Item,
|
|
160
|
+
{
|
|
161
|
+
"data-slot": "combobox-item",
|
|
162
|
+
className: cn(
|
|
163
|
+
// layout — Fluent 32px row, 4px list radius, room on the right for check
|
|
164
|
+
"relative flex h-8 w-full cursor-default items-center rounded-md pr-8 pl-2 text-sm outline-none select-none",
|
|
165
|
+
// rest text is NeutralForeground2, darkening to accent-foreground on
|
|
166
|
+
// highlight (Figma validation: Fluent list rows rest at #424242)
|
|
167
|
+
"text-foreground-2 data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground",
|
|
168
|
+
// disabled item — muted + non-interactive
|
|
169
|
+
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
170
|
+
// icons a consumer puts inside the item text
|
|
171
|
+
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
172
|
+
className
|
|
173
|
+
),
|
|
174
|
+
...props,
|
|
175
|
+
children: [
|
|
176
|
+
/* @__PURE__ */ jsx("span", { className: "absolute right-2 flex size-4 items-center justify-center", children: /* @__PURE__ */ jsx(ComboboxPrimitive.ItemIndicator, { children: /* @__PURE__ */ jsx(CheckmarkRegular, { className: "size-4" }) }) }),
|
|
177
|
+
children
|
|
178
|
+
]
|
|
179
|
+
}
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
function ComboboxGroup({
|
|
183
|
+
className,
|
|
184
|
+
...props
|
|
185
|
+
}) {
|
|
186
|
+
return /* @__PURE__ */ jsx(
|
|
187
|
+
ComboboxPrimitive.Group,
|
|
188
|
+
{
|
|
189
|
+
"data-slot": "combobox-group",
|
|
190
|
+
className: cn(className),
|
|
191
|
+
...props
|
|
192
|
+
}
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
function ComboboxLabel({
|
|
196
|
+
className,
|
|
197
|
+
...props
|
|
198
|
+
}) {
|
|
199
|
+
return /* @__PURE__ */ jsx(
|
|
200
|
+
ComboboxPrimitive.GroupLabel,
|
|
201
|
+
{
|
|
202
|
+
"data-slot": "combobox-label",
|
|
203
|
+
className: cn(
|
|
204
|
+
"px-2 py-1.5 text-xs font-medium text-muted-foreground select-none",
|
|
205
|
+
className
|
|
206
|
+
),
|
|
207
|
+
...props
|
|
208
|
+
}
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
function ComboboxSeparator({
|
|
212
|
+
className,
|
|
213
|
+
...props
|
|
214
|
+
}) {
|
|
215
|
+
return /* @__PURE__ */ jsx(
|
|
216
|
+
ComboboxPrimitive.Separator,
|
|
217
|
+
{
|
|
218
|
+
"data-slot": "combobox-separator",
|
|
219
|
+
className: cn("pointer-events-none -mx-1 my-1 h-px bg-border", className),
|
|
220
|
+
...props
|
|
221
|
+
}
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
export {
|
|
225
|
+
Combobox,
|
|
226
|
+
ComboboxContent,
|
|
227
|
+
ComboboxEmpty,
|
|
228
|
+
ComboboxGroup,
|
|
229
|
+
ComboboxInput,
|
|
230
|
+
ComboboxItem,
|
|
231
|
+
ComboboxLabel,
|
|
232
|
+
ComboboxList,
|
|
233
|
+
ComboboxSeparator,
|
|
234
|
+
ComboboxValue
|
|
235
|
+
};
|
|
236
|
+
//# sourceMappingURL=combobox.js.map
|