atlas-react-kit 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/COMPONENTS.md +41 -37
  2. package/DESIGN_RULES.md +23 -32
  3. package/README.md +5 -47
  4. package/dist/date-picker.d.ts +13 -2
  5. package/dist/date-picker.js +35 -22
  6. package/dist/date-time-picker.d.ts +1 -1
  7. package/dist/date-time-picker.js +24 -38
  8. package/dist/form/date-picker-field.d.ts +6 -1
  9. package/dist/form/date-picker-field.js +3 -2
  10. package/dist/form/date-time-picker-field.d.ts +1 -1
  11. package/dist/form/date-time-picker-field.js +3 -2
  12. package/dist/form/field-error.d.ts +4 -3
  13. package/dist/form/field-shell.d.ts +6 -5
  14. package/dist/form/field-shell.js +1 -1
  15. package/dist/form/focus-first-invalid.d.ts +28 -0
  16. package/dist/form/focus-first-invalid.js +29 -0
  17. package/dist/form/select-field.d.ts +7 -1
  18. package/dist/form/select-field.js +3 -2
  19. package/dist/internal/listbox-navigation.d.ts +0 -11
  20. package/dist/internal/listbox-navigation.js +2 -2
  21. package/dist/internal/listbox-option.d.ts +0 -15
  22. package/dist/internal/listbox-option.js +1 -1
  23. package/dist/internal/use-drag-scroll.d.ts +17 -0
  24. package/dist/internal/use-drag-scroll.js +98 -0
  25. package/dist/nav-link.d.ts +12 -4
  26. package/dist/nav-link.js +14 -4
  27. package/dist/next/nav-link.d.ts +6 -3
  28. package/dist/next/nav-link.js +8 -6
  29. package/dist/next/tabs-nav-link.d.ts +6 -0
  30. package/dist/next/tabs-nav-link.js +8 -3
  31. package/dist/picker-shell.d.ts +24 -21
  32. package/dist/picker-shell.js +12 -10
  33. package/dist/tabs.d.ts +18 -7
  34. package/dist/tabs.js +22 -9
  35. package/package.json +7 -3
  36. package/skills/atlas-react-kit/SKILL.md +46 -40
  37. package/theme.css +2 -0
@@ -1,10 +1,11 @@
1
1
  import * as React from "react";
2
2
  import type { Locale } from "date-fns";
3
3
  import { type DateLike, type DateMatcher } from "./internal/date-like.js";
4
+ import { LazyCalendar } from "./lazy-calendar.js";
4
5
  import { PopoverContent } from "./popover.js";
5
6
  /**
6
- * Not a public entry. The trigger, popover, and Clear footer that
7
- * `DatePickerButton` and `DateTimePickerButton` share.
7
+ * Not a public entry. The trigger, popover, calendar wiring, and Clear row
8
+ * that `DatePickerButton` and `DateTimePickerButton` share.
8
9
  */
9
10
  /**
10
11
  * Options for the picker's calendar. The picker owns the selection, focus,
@@ -80,13 +81,7 @@ export type PickerTriggerProps = {
80
81
  /** Day Picker options passed through to the calendar. */
81
82
  calendarProps?: PickerCalendarProps;
82
83
  };
83
- /**
84
- * `calendarProps` with `minDate` / `maxDate` applied: days outside them join
85
- * any `disabled` matchers, and they bound paging unless `startMonth` /
86
- * `endMonth` are set.
87
- */
88
- export declare function limitCalendarProps(calendarProps: PickerCalendarProps | undefined, minDate: DateLike, maxDate: DateLike): PickerCalendarProps | undefined;
89
- type PickerShellProps = Omit<PickerTriggerProps, "placeholder" | "defaultMonth" | "calendarProps" | "minDate" | "maxDate"> & {
84
+ type PickerShellProps = Omit<PickerTriggerProps, "placeholder"> & {
90
85
  /** Muted trigger text while nothing is picked. */
91
86
  empty: boolean;
92
87
  /** Trigger text: the formatted value or the placeholder. */
@@ -97,17 +92,25 @@ type PickerShellProps = Omit<PickerTriggerProps, "placeholder" | "defaultMonth"
97
92
  open: boolean;
98
93
  /** Called when the popover opens or closes. */
99
94
  onOpenChange: (open: boolean) => void;
100
- /** Called on a pointer down or focus outside the popover; can prevent the dismiss. */
101
- onInteractOutside?: React.ComponentProps<typeof PopoverContent>["onInteractOutside"];
102
- children: React.ReactNode;
95
+ /**
96
+ * The picker's own calendar props: its mode, selection and `onSelect`.
97
+ * The shell adds `calendarProps`, the date limits, `defaultMonth`, and
98
+ * focus on the selected day.
99
+ */
100
+ calendar: React.ComponentProps<typeof LazyCalendar>;
101
+ /**
102
+ * Shows a Clear row under the panel. Called when Clear is pressed; the
103
+ * popover then closes.
104
+ */
105
+ onClear?: () => void;
106
+ /** Disable Clear, e.g. while nothing is picked. */
107
+ clearDisabled?: boolean;
108
+ /** Extra panel rows between the calendar and Clear, e.g. time selects. */
109
+ children?: React.ReactNode;
103
110
  };
104
- /** Form-control trigger plus the viewport-clamped calendar popover. */
105
- export declare function PickerShell({ id, disabled, className, contentClassName, "aria-invalid": ariaInvalid, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, side, empty, label, dialogLabel, open, onOpenChange, onInteractOutside, children, }: PickerShellProps): React.JSX.Element;
106
- /** Clear button row under the calendar. */
107
- export declare function PickerClearFooter({ disabled, onClear, }: {
108
- /** Disable the Clear button, e.g. while nothing is picked. */
109
- disabled: boolean;
110
- /** Called when Clear is pressed. */
111
- onClear: () => void;
112
- }): React.JSX.Element;
111
+ /**
112
+ * Form-control trigger plus the viewport-clamped popover holding the
113
+ * calendar, any extra rows, and an optional Clear row.
114
+ */
115
+ export declare function PickerShell({ id, disabled, className, contentClassName, "aria-invalid": ariaInvalid, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, side, defaultMonth, minDate, maxDate, calendarProps, empty, label, dialogLabel, open, onOpenChange, calendar, onClear, clearDisabled, children, }: PickerShellProps): React.JSX.Element;
113
116
  export {};
@@ -5,7 +5,7 @@ import { CalendarIcon } from "lucide-react";
5
5
  import { Button } from "./button.js";
6
6
  import { toDate, } from "./internal/date-like.js";
7
7
  import { PICKER_COLLISION_PADDING, pickerContentClassName, pickerTriggerClassName, pickerValueId, } from "./internal/form-control.js";
8
- import { preloadCalendar } from "./lazy-calendar.js";
8
+ import { LazyCalendar, preloadCalendar } from "./lazy-calendar.js";
9
9
  import { Popover, PopoverContent, PopoverTrigger } from "./popover.js";
10
10
  import { cn } from "./utils.js";
11
11
  /**
@@ -13,7 +13,7 @@ import { cn } from "./utils.js";
13
13
  * any `disabled` matchers, and they bound paging unless `startMonth` /
14
14
  * `endMonth` are set.
15
15
  */
16
- export function limitCalendarProps(calendarProps, minDate, maxDate) {
16
+ function limitCalendarProps(calendarProps, minDate, maxDate) {
17
17
  const min = toDate(minDate);
18
18
  const max = toDate(maxDate);
19
19
  if (!min && !max)
@@ -35,15 +35,17 @@ export function limitCalendarProps(calendarProps, minDate, maxDate) {
35
35
  endMonth: calendarProps?.endMonth ?? max,
36
36
  };
37
37
  }
38
- /** Form-control trigger plus the viewport-clamped calendar popover. */
39
- export function PickerShell({ id, disabled, className, contentClassName, "aria-invalid": ariaInvalid, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, side, empty, label, dialogLabel, open, onOpenChange, onInteractOutside, children, }) {
40
- return (_jsxs(Popover, { open: open, onOpenChange: onOpenChange, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { id: id, type: "button", variant: "outline", disabled: disabled, "aria-invalid": ariaInvalid, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, "data-empty": empty, className: cn(pickerTriggerClassName, className), onPointerEnter: preloadCalendar, onFocus: preloadCalendar, children: [_jsx(CalendarIcon, { "data-icon": "inline-start" }), _jsx("span", { "data-slot": "date-picker-value", className: "min-w-0 text-left whitespace-normal", id: id ? pickerValueId(id) : undefined, children: label })] }) }), _jsx(PopoverContent, { align: "start", side: side, collisionPadding: PICKER_COLLISION_PADDING, "aria-label": dialogLabel,
38
+ /**
39
+ * Form-control trigger plus the viewport-clamped popover holding the
40
+ * calendar, any extra rows, and an optional Clear row.
41
+ */
42
+ export function PickerShell({ id, disabled, className, contentClassName, "aria-invalid": ariaInvalid, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, side, defaultMonth, minDate, maxDate, calendarProps, empty, label, dialogLabel, open, onOpenChange, calendar, onClear, clearDisabled = false, children, }) {
43
+ return (_jsxs(Popover, { open: open, onOpenChange: onOpenChange, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { id: id, type: "button", variant: "outline", disabled: disabled, "aria-invalid": ariaInvalid, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, "data-empty": empty, className: cn(pickerTriggerClassName, className), onPointerEnter: preloadCalendar, onFocus: preloadCalendar, children: [_jsx(CalendarIcon, { "data-icon": "inline-start" }), _jsx("span", { "data-slot": "date-picker-value", className: "min-w-0 text-left whitespace-normal", id: id ? pickerValueId(id) : undefined, children: label })] }) }), _jsxs(PopoverContent, { align: "start", side: side, collisionPadding: PICKER_COLLISION_PADDING, "aria-label": dialogLabel,
41
44
  // The calendar's `autoFocus` puts focus on the selected day (or
42
45
  // today) once it loads, as the APG date picker dialog does; Radix
43
46
  // focusing the popover first would leave it on the container.
44
- onOpenAutoFocus: (event) => event.preventDefault(), onInteractOutside: onInteractOutside, className: cn("w-auto min-w-0", pickerContentClassName, contentClassName), children: children })] }));
45
- }
46
- /** Clear button row under the calendar. */
47
- export function PickerClearFooter({ disabled, onClear, }) {
48
- return (_jsx("div", { "data-slot": "date-picker-footer", className: "flex justify-end border-t p-2", children: _jsx(Button, { type: "button", variant: "ghost", size: "sm", disabled: disabled, onClick: onClear, children: "Clear" }) }));
47
+ onOpenAutoFocus: (event) => event.preventDefault(), className: cn("w-auto min-w-0", pickerContentClassName, contentClassName), children: [_jsx(LazyCalendar, { ...limitCalendarProps(calendarProps, minDate, maxDate), ...calendar, autoFocus: true, defaultMonth: defaultMonth }), children, onClear ? (_jsx("div", { "data-slot": "date-picker-footer", className: "flex justify-end border-t p-2", children: _jsx(Button, { type: "button", variant: "ghost", size: "sm", disabled: clearDisabled, onClick: () => {
48
+ onClear();
49
+ onOpenChange(false);
50
+ }, children: "Clear" }) })) : null] })] }));
49
51
  }
package/dist/tabs.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import * as React from "react";
2
2
  import { type VariantProps } from "class-variance-authority";
3
3
  import { Tabs as TabsPrimitive } from "radix-ui";
4
+ import type { AsChildProps } from "./internal/as-child.js";
4
5
  import { tabsListVariants, tabsTriggerVariants } from "./internal/tabs-styles.js";
5
6
  /** Props `TabsList` and `TabsNav` share for the scroller around their row. */
6
7
  type TabsScrollerClassNameProps = {
@@ -16,18 +17,28 @@ declare function Tabs({ className, orientation, ...props }: React.ComponentProps
16
17
  declare function TabsList({ className, scrollerClassName, variant, ...props }: React.ComponentProps<typeof TabsPrimitive.List> & Omit<VariantProps<typeof tabsListVariants>, "orientation"> & TabsScrollerClassNameProps): React.JSX.Element;
17
18
  declare function TabsTrigger({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Trigger>): React.JSX.Element;
18
19
  declare function TabsContent({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Content>): React.JSX.Element;
20
+ /**
21
+ * Tab-styled link for `TabsNav`. Pass `active` from the route: the active
22
+ * link gets the trigger's selected chrome (`data-state="active"`) and
23
+ * `aria-current="page"`. `asChild` wraps a router `Link` with these styles,
24
+ * like `NavLink`; Next apps can import `next/tabs-nav-link` instead.
25
+ */
26
+ declare function TabsNavLink({ asChild, active, className, ...props }: React.ComponentProps<"a"> & AsChildProps & {
27
+ /** This link is the current view: `aria-current="page"` and the active chrome. */
28
+ active: boolean;
29
+ }): React.JSX.Element;
19
30
  /**
20
31
  * Tab-styled links for a view switcher that changes the URL. Use this, not
21
32
  * `Tabs` + `TabsTrigger asChild`, when there are no `TabsContent` panels:
22
- * tabs would point assistive tech at panels that don't exist. Style each link
23
- * with `tabsTriggerVariants()` and mark the active one with
24
- * `data-state="active"` and `aria-current="page"` (the Next adapter
25
- * `TabsNavLink` does this for you).
33
+ * tabs would point assistive tech at panels that don't exist. Each link is
34
+ * a `TabsNavLink` with `active` from the app (`asChild` for a router link,
35
+ * or the Next adapter `next/tabs-nav-link`).
26
36
  *
27
37
  * Overflow is handled here: the row scrolls horizontally only, with padding
28
38
  * for active borders and focus rings counted in the scroller height (so the
29
- * list cannot overflow on y), an edge fade while it overflows, and the
30
- * active link scrolled into view on mount. Do not wrap it in
39
+ * list cannot overflow on y), an edge fade while it overflows, the
40
+ * active link scrolled into view on mount, and a mouse drag that scrolls
41
+ * the row (a plain click still follows the link). Do not wrap it in
31
42
  * `overflow-x-auto`. `scrollerClassName` styles that scroller and
32
43
  * `listClassName` the row of links inside it.
33
44
  */
@@ -37,4 +48,4 @@ declare function TabsNav({ className, scrollerClassName, listClassName, children
37
48
  /** Classes for the row of links inside the scroller. */
38
49
  listClassName?: string;
39
50
  }): React.JSX.Element;
40
- export { Tabs, TabsList, TabsTrigger, TabsContent, TabsNav, tabsListVariants, tabsTriggerVariants, };
51
+ export { Tabs, TabsList, TabsTrigger, TabsContent, TabsNav, TabsNavLink, tabsListVariants, tabsTriggerVariants, };
package/dist/tabs.js CHANGED
@@ -2,9 +2,10 @@
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import {} from "class-variance-authority";
5
- import { Tabs as TabsPrimitive } from "radix-ui";
5
+ import { Slot, Tabs as TabsPrimitive } from "radix-ui";
6
6
  import { focusRing } from "./internal/styles.js";
7
7
  import { tabsListVariants, tabsScrollerVariants, tabsTriggerVariants, } from "./internal/tabs-styles.js";
8
+ import { useDragScroll } from "./internal/use-drag-scroll.js";
8
9
  import { useHorizontalOverflow } from "./internal/use-horizontal-overflow.js";
9
10
  import { cn } from "./utils.js";
10
11
  /**
@@ -22,12 +23,14 @@ function Tabs({ className, orientation = "horizontal", ...props }) {
22
23
  * active tab's border and focus ring grow a vertical scrollbar. Pad for
23
24
  * those strokes *inside* the box (`h-13` = `h-11` list + `p-1`), clip the
24
25
  * y axis as a backstop, and fade the edges while the row overflows —
25
- * same cue as `Table`.
26
+ * same cue as `Table`. While it overflows, a mouse can also drag the row
27
+ * sideways (`useDragScroll`); touch, wheel and keyboard scroll as before.
26
28
  */
27
29
  function TabsScroller({ variant = "default", orientation = "horizontal", className, children, }) {
28
30
  const ref = React.useRef(null);
29
31
  const revealed = React.useRef(null);
30
32
  const overflowing = useHorizontalOverflow(ref);
33
+ useDragScroll(ref, overflowing);
31
34
  // Runs after every render, not just mount: TabsNav usually sits in a
32
35
  // persistent layout, so a route change updates the active link in place.
33
36
  React.useLayoutEffect(() => {
@@ -63,22 +66,32 @@ function TabsTrigger({ className, ...props }) {
63
66
  function TabsContent({ className, ...props }) {
64
67
  return (_jsx(TabsPrimitive.Content, { "data-slot": "tabs-content", className: cn("flex-1 text-sm", focusRing, className), ...props }));
65
68
  }
69
+ /**
70
+ * Tab-styled link for `TabsNav`. Pass `active` from the route: the active
71
+ * link gets the trigger's selected chrome (`data-state="active"`) and
72
+ * `aria-current="page"`. `asChild` wraps a router `Link` with these styles,
73
+ * like `NavLink`; Next apps can import `next/tabs-nav-link` instead.
74
+ */
75
+ function TabsNavLink({ asChild, active, className, ...props }) {
76
+ const Comp = asChild ? Slot.Root : "a";
77
+ return (_jsx(Comp, { "data-slot": "tabs-nav-link", "data-state": active ? "active" : "inactive", "aria-current": active ? "page" : undefined, className: cn(tabsTriggerVariants(), className), ...props }));
78
+ }
66
79
  /**
67
80
  * Tab-styled links for a view switcher that changes the URL. Use this, not
68
81
  * `Tabs` + `TabsTrigger asChild`, when there are no `TabsContent` panels:
69
- * tabs would point assistive tech at panels that don't exist. Style each link
70
- * with `tabsTriggerVariants()` and mark the active one with
71
- * `data-state="active"` and `aria-current="page"` (the Next adapter
72
- * `TabsNavLink` does this for you).
82
+ * tabs would point assistive tech at panels that don't exist. Each link is
83
+ * a `TabsNavLink` with `active` from the app (`asChild` for a router link,
84
+ * or the Next adapter `next/tabs-nav-link`).
73
85
  *
74
86
  * Overflow is handled here: the row scrolls horizontally only, with padding
75
87
  * for active borders and focus rings counted in the scroller height (so the
76
- * list cannot overflow on y), an edge fade while it overflows, and the
77
- * active link scrolled into view on mount. Do not wrap it in
88
+ * list cannot overflow on y), an edge fade while it overflows, the
89
+ * active link scrolled into view on mount, and a mouse drag that scrolls
90
+ * the row (a plain click still follows the link). Do not wrap it in
78
91
  * `overflow-x-auto`. `scrollerClassName` styles that scroller and
79
92
  * `listClassName` the row of links inside it.
80
93
  */
81
94
  function TabsNav({ className, scrollerClassName, listClassName, children, ...props }) {
82
95
  return (_jsx("nav", { "data-slot": "tabs-nav", "data-orientation": "horizontal", className: cn("group/tabs min-w-0 max-w-full", className), ...props, children: _jsx(TabsScroller, { className: scrollerClassName, children: _jsx("div", { "data-slot": "tabs-list", "data-variant": "default", className: cn(tabsListVariants({ orientation: "horizontal" }), "bg-transparent", listClassName), children: children }) }) }));
83
96
  }
84
- export { Tabs, TabsList, TabsTrigger, TabsContent, TabsNav, tabsListVariants, tabsTriggerVariants, };
97
+ export { Tabs, TabsList, TabsTrigger, TabsContent, TabsNav, TabsNavLink, tabsListVariants, tabsTriggerVariants, };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atlas-react-kit",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Shared React components and Tailwind theme for Atlas",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -252,6 +252,10 @@
252
252
  "types": "./dist/form/field-error.d.ts",
253
253
  "default": "./dist/form/field-error.js"
254
254
  },
255
+ "./form/focus-first-invalid": {
256
+ "types": "./dist/form/focus-first-invalid.d.ts",
257
+ "default": "./dist/form/focus-first-invalid.js"
258
+ },
255
259
  "./form/markdown-field": {
256
260
  "types": "./dist/form/markdown-field.d.ts",
257
261
  "default": "./dist/form/markdown-field.js"
@@ -295,11 +299,11 @@
295
299
  "docs:check": "node scripts/check-doc-links.mjs",
296
300
  "lint": "oxlint --type-aware --deny-warnings",
297
301
  "knip": "knip",
298
- "sast": "node scripts/sast.mjs",
302
+ "sast": "semgrep scan --config .semgrep.yml --metrics=off --disable-version-check --error --quiet src",
299
303
  "secrets": "secretlint \"**/*\"",
300
304
  "size": "size-limit",
301
305
  "gate:static": "npm run lint && npm run knip && npm run sast && npm run secrets",
302
- "gate": "npm run check && npm run gate:static && npm run size && vitest run",
306
+ "gate": "npm run check && npm run gate:static && npm run size && vitest run --coverage",
303
307
  "test": "npm run build && vitest run",
304
308
  "test:coverage": "npm run build && vitest run --coverage",
305
309
  "prepack": "npm run build",
@@ -42,10 +42,9 @@ root (or a `dark` class on an ancestor). Toasts need one `<Toaster />` from
42
42
  `atlas-react-kit/toast`; show them with `toast()` from the same module, using
43
43
  only the options it declares. Never import a toast library such as `sonner`
44
44
  directly: the kit owns the toast API, and a second copy never reaches its
45
- toaster. Markdown, dates, toasts, and `ThemeProvider` are optional peers —
46
- install the extras listed in `COMPONENTS.md` when you import those entry
47
- points. Fix missing setup rather than working
48
- around unstyled components.
45
+ toaster. Some entry points need optional peers; install the extras from the
46
+ "Optional peer dependencies" table in `COMPONENTS.md` when you import them.
47
+ Fix missing setup rather than working around unstyled components.
49
48
 
50
49
  ## 3. Pick the kit component for the job
51
50
 
@@ -53,42 +52,49 @@ Import each component from its subpath, e.g.
53
52
  `import { Button } from "atlas-react-kit/button"`. There is no root barrel, and
54
53
  `dist/` or `internal/` paths are not public API.
55
54
 
56
- | Need | Use |
57
- | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
58
- | Short fixed list (a handful of options) | `Select` family from `select` |
59
- | Long or searchable list (timezones, currencies) | `Combobox` from `combobox` with `{ value, label }[]` options; set `creatable` to invent a value the catalog lacks |
60
- | Always-visible searchable list (places) | `SearchPicker` from `search-picker` |
61
- | One date, or a from/to range | `DatePickerButton` from `date-picker` (`mode="range"` for ranges) |
62
- | Date and time (not `datetime-local`) | `DateTimePickerButton` from `date-time-picker` |
63
- | Labelled field with error text | `FormTextField`, `FormSelectField`, `FormDatePickerField`, `FormDateTimePickerField`, `FormMarkdownField` from `form/*` |
64
- | Labelled custom control (combobox) in a form | `FormControlField` from `form/control-field` |
65
- | Form-level error (save failed) | `FormFeedbackField` from `form/feedback-field` |
66
- | Content panels on one page | `Tabs`, `TabsList`, `TabsTrigger`, `TabsContent` from `tabs` |
67
- | View switch that changes the URL | `TabsNav` with links: `TabsNavLink` from `next/tabs-nav-link` in Next, else `tabsTriggerVariants()` |
68
- | App shell nav (Calendar, Tasks, Settings) | `NavLink` from `nav-link` (`asChild` for a router link); Next: `next/nav-link` |
69
- | Status chip (paid, pending, failed) | `Badge variant="soft"` with `tone="success"`, `"warning"`, `"destructive"`, or `"neutral"` |
70
- | Data table | `Table` family from `table`; `SortableTableHead`; `TableBodySkeleton` while loading |
71
- | Nothing to show yet | `EmptyPanel` from `empty-panel` |
72
- | Grouped content | `Card` family from `card`; `<Card flush>` for an edge-to-edge table or list |
73
- | Modal | `Dialog` family from `dialog` |
74
- | Label for an icon button, or a short hint | `Tooltip` from `tooltip`; keep `aria-label` on icon buttons |
75
- | Longer help from a glyph | `InfoTip` / `InlineTip` from `info-tip` (not `Tooltip`) |
76
- | Color scale key | `ColorLegend` from `color-legend` |
77
- | GitHub-flavoured Markdown | `Markdown` from `markdown`; write/preview with `FormMarkdownField` |
78
- | Summary figure in a strip | `StatTile` from `stat-tile` (`statTileGrid`, `sectionLabel`) |
79
- | Value against a scale, optional ticks | `Meter` from `meter` (not `Progress`) |
80
- | Number on a continuous scale | `Slider` from `slider` |
81
- | Row actions or an overflow menu of commands | `DropdownMenu` family from `dropdown-menu` |
82
- | Setting that applies immediately | `Switch` from `switch`; `Checkbox` for choices submitted with a form |
83
- | One choice from a few visible options | `RadioGroup` and `RadioGroupItem` from `radio-group` |
84
- | Inline message about a page or section | `Alert` from `alert` (`success`, `warning`, `destructive` variants) |
85
- | Person or workspace image | `Avatar`, `AvatarImage`, `AvatarFallback` from `avatar` |
86
- | Sections that expand in place | `Accordion` family from `accordion` |
87
- | Pages of a long list | `Pagination` family from `pagination` |
88
- | Where the page sits in a hierarchy | `Breadcrumb` family from `breadcrumb` |
89
- | Work in progress | `Progress` from `progress` when the percent is known, else `Spinner` from `spinner` |
90
- | Crash recovery around a section | `ErrorBoundary` from `error-boundary` |
91
- | Conditional classes | `cn` from `utils` |
55
+ | Need | Use |
56
+ | ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
57
+ | Short fixed list (a handful of options) | `Select` family from `select` |
58
+ | Long or searchable list (timezones, currencies) | `Combobox` from `combobox` with `{ value, label }[]` options; set `creatable` to invent a value the catalog lacks |
59
+ | Always-visible searchable list (places) | `SearchPicker` from `search-picker` |
60
+ | One date, or a from/to range | `DatePickerButton` from `date-picker` (`mode="range"` for ranges) |
61
+ | Date and time (not `datetime-local`) | `DateTimePickerButton` from `date-time-picker` |
62
+ | Labelled field with error text | `FormTextField`, `FormSelectField`, `FormDatePickerField`, `FormDateTimePickerField`, `FormMarkdownField` from `form/*` |
63
+ | Checkbox, checkbox group, toggle group, combobox in a form | `FormCheckboxField`, `FormCheckboxGroupField`, `FormToggleGroupField`, `FormComboboxField` from `form/*` |
64
+ | Custom control the kit has no form field for | `FormControlField` from `form/control-field` |
65
+ | Form-level error (save failed) | `FormFeedbackField` from `form/feedback-field` |
66
+ | Content panels on one page | `Tabs`, `TabsList`, `TabsTrigger`, `TabsContent` from `tabs` |
67
+ | View switch that changes the URL | `TabsNav` with `TabsNavLink` from `tabs` (`asChild` for a router link); Next: `next/tabs-nav-link` |
68
+ | App shell nav (Calendar, Tasks, Settings) | `NavLink` from `nav-link` (`asChild` for a router link); Next: `next/nav-link`; `active={isNavActive(pathname, href)}` |
69
+ | Focus the first invalid field when a submit fails | `focusFirstInvalid(formId)` from `form/focus-first-invalid`; give fields `id={field.name}` |
70
+ | Skip-to-content link | `SkipLink` from `skip-link` |
71
+ | Page title row with actions | `PageHeader` from `page-header` |
72
+ | Previous / today / next period | `PeriodStepper` from `period-stepper`; `next/period-stepper` for URL periods |
73
+ | Cards in columns that drag | `KanbanBoard` from `kanban`; `KanbanSkeleton` while loading |
74
+ | SVG chart axes, grid, tooltip, legend | `chart` helpers (`chartFrame`, `ChartYAxisGrid`, `ChartTooltip`, `ChartLegend`) |
75
+ | Status chip (paid, pending, failed) | `Badge variant="soft"` with `tone="success"`, `"warning"`, `"destructive"`, or `"neutral"` |
76
+ | Data table | `Table` family from `table`; `SortableTableHead`; `TableBodySkeleton` while loading |
77
+ | Nothing to show yet | `EmptyPanel` from `empty-panel` |
78
+ | Grouped content | `Card` family from `card`; `<Card flush>` for an edge-to-edge table or list |
79
+ | Modal | `Dialog` family from `dialog` |
80
+ | Label for an icon button, or a short hint | `Tooltip` from `tooltip`; keep `aria-label` on icon buttons |
81
+ | Longer help from a glyph | `InfoTip` / `InlineTip` from `info-tip` (not `Tooltip`) |
82
+ | Color scale key | `ColorLegend` from `color-legend` |
83
+ | GitHub-flavoured Markdown | `Markdown` from `markdown`; write/preview with `FormMarkdownField` |
84
+ | Summary figure in a strip | `StatTile` from `stat-tile` (`statTileGrid`, `sectionLabel`) |
85
+ | Value against a scale, optional ticks | `Meter` from `meter` (not `Progress`) |
86
+ | Number on a continuous scale | `Slider` from `slider` |
87
+ | Row actions or an overflow menu of commands | `DropdownMenu` family from `dropdown-menu` |
88
+ | Setting that applies immediately | `Switch` from `switch`; `Checkbox` for choices submitted with a form |
89
+ | One choice from a few visible options | `RadioGroup` and `RadioGroupItem` from `radio-group` |
90
+ | Inline message about a page or section | `Alert` from `alert` (`success`, `warning`, `destructive` variants) |
91
+ | Person or workspace image | `Avatar`, `AvatarImage`, `AvatarFallback` from `avatar` |
92
+ | Sections that expand in place | `Accordion` family from `accordion` |
93
+ | Pages of a long list | `Pagination` family from `pagination` |
94
+ | Where the page sits in a hierarchy | `Breadcrumb` family from `breadcrumb` |
95
+ | Work in progress | `Progress` from `progress` when the percent is known, else `Spinner` from `spinner` |
96
+ | Crash recovery around a section | `ErrorBoundary` from `error-boundary` |
97
+ | Conditional classes | `cn` from `utils` |
92
98
 
93
99
  If nothing fits, build the piece in the app from kit primitives and semantic
94
100
  tokens. Do not copy kit source into the app; a missing shared component belongs
package/theme.css CHANGED
@@ -90,6 +90,7 @@
90
90
  }
91
91
 
92
92
  :root {
93
+ color-scheme: light;
93
94
  --background: oklch(1 0 0);
94
95
  --foreground: oklch(0.145 0 0);
95
96
  --card: oklch(1 0 0);
@@ -142,6 +143,7 @@
142
143
  }
143
144
 
144
145
  .dark {
146
+ color-scheme: dark;
145
147
  --background: oklch(0.145 0 0);
146
148
  --foreground: oklch(0.985 0 0);
147
149
  --card: oklch(0.205 0 0);