atlas-react-kit 1.0.0 → 1.2.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 (61) hide show
  1. package/COMPONENTS.md +43 -39
  2. package/DESIGN_RULES.md +23 -32
  3. package/README.md +5 -47
  4. package/dist/accordion.js +2 -2
  5. package/dist/badge.js +5 -2
  6. package/dist/breadcrumb.js +2 -2
  7. package/dist/button.js +7 -4
  8. package/dist/combobox.js +4 -5
  9. package/dist/date-picker.d.ts +13 -2
  10. package/dist/date-picker.js +35 -22
  11. package/dist/date-time-picker.d.ts +1 -1
  12. package/dist/date-time-picker.js +24 -38
  13. package/dist/dropdown-menu.js +3 -3
  14. package/dist/form/date-picker-field.d.ts +6 -1
  15. package/dist/form/date-picker-field.js +3 -2
  16. package/dist/form/date-time-picker-field.d.ts +1 -1
  17. package/dist/form/date-time-picker-field.js +3 -2
  18. package/dist/form/field-error.d.ts +4 -3
  19. package/dist/form/field-shell.d.ts +6 -5
  20. package/dist/form/field-shell.js +1 -1
  21. package/dist/form/focus-first-invalid.d.ts +28 -0
  22. package/dist/form/focus-first-invalid.js +29 -0
  23. package/dist/form/select-field.d.ts +7 -1
  24. package/dist/form/select-field.js +3 -2
  25. package/dist/info-tip.js +7 -11
  26. package/dist/internal/listbox-navigation.d.ts +0 -16
  27. package/dist/internal/listbox-navigation.js +2 -9
  28. package/dist/internal/listbox-option.d.ts +0 -15
  29. package/dist/internal/listbox-option.js +1 -1
  30. package/dist/internal/overlay-styles.d.ts +0 -2
  31. package/dist/internal/overlay-styles.js +0 -2
  32. package/dist/internal/search.d.ts +5 -1
  33. package/dist/internal/search.js +8 -5
  34. package/dist/internal/styles.d.ts +27 -0
  35. package/dist/internal/styles.js +27 -0
  36. package/dist/internal/tabs-styles.d.ts +8 -7
  37. package/dist/internal/tabs-styles.js +15 -13
  38. package/dist/internal/use-drag-scroll.d.ts +17 -0
  39. package/dist/internal/use-drag-scroll.js +98 -0
  40. package/dist/internal/use-media-query.d.ts +5 -0
  41. package/dist/internal/use-media-query.js +5 -0
  42. package/dist/kanban.d.ts +1 -1
  43. package/dist/kanban.js +4 -3
  44. package/dist/nav-link.d.ts +12 -4
  45. package/dist/nav-link.js +14 -4
  46. package/dist/next/nav-link.d.ts +6 -3
  47. package/dist/next/nav-link.js +8 -6
  48. package/dist/next/tabs-nav-link.d.ts +6 -0
  49. package/dist/next/tabs-nav-link.js +8 -3
  50. package/dist/picker-shell.d.ts +24 -21
  51. package/dist/picker-shell.js +12 -10
  52. package/dist/search-picker.d.ts +1 -1
  53. package/dist/search-picker.js +5 -6
  54. package/dist/select.js +5 -5
  55. package/dist/tabs.d.ts +21 -10
  56. package/dist/tabs.js +26 -13
  57. package/dist/toggle.d.ts +2 -0
  58. package/dist/toggle.js +8 -3
  59. package/package.json +7 -3
  60. package/skills/atlas-react-kit/SKILL.md +46 -40
  61. package/theme.css +2 -0
@@ -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
  }
@@ -1,5 +1,5 @@
1
1
  import * as React from "react";
2
- import { foldSearchText } from "./internal/listbox-navigation.js";
2
+ import { foldSearchText } from "./internal/search.js";
3
3
  export { foldSearchText };
4
4
  /**
5
5
  * Default SearchPicker filter: the folded label includes the folded query.
@@ -3,9 +3,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { Input } from "./input.js";
5
5
  import { Label } from "./label.js";
6
- import { foldSearchText, handleListboxKey, listboxOptionId, } from "./internal/listbox-navigation.js";
6
+ import { handleListboxKey, listboxOptionId, } from "./internal/listbox-navigation.js";
7
7
  import { ListboxEmptyStatus, PickerOption } from "./internal/listbox-option.js";
8
- import { createSearchIndex, filterSearchIndex, matchesSearchLabel, normalizeSearchQuery, } from "./internal/search.js";
8
+ import { createSearchIndex, filterSearchIndex, foldSearchText, normalizeSearchQuery, } from "./internal/search.js";
9
+ import { listboxScroll } from "./internal/styles.js";
9
10
  import { cn } from "./utils.js";
10
11
  export { foldSearchText };
11
12
  /**
@@ -15,7 +16,7 @@ export function defaultSearchFilter(item, query, getLabel) {
15
16
  const needle = normalizeSearchQuery(query);
16
17
  if (!needle)
17
18
  return true;
18
- return matchesSearchLabel(foldSearchText(getLabel(item)), needle);
19
+ return foldSearchText(getLabel(item)).includes(needle);
19
20
  }
20
21
  /** Frame shared by the listbox and the empty state that replaces it. */
21
22
  const listFrame = "border border-input bg-background";
@@ -84,8 +85,6 @@ function SearchPicker({ items, getValue, getLabel, filter = defaultSearchFilter,
84
85
  }
85
86
  event.currentTarget.blur();
86
87
  };
87
- return (_jsxs("div", { "data-slot": "search-picker", className: cn("flex flex-col gap-1.5", className), ...props, children: [_jsx(Label, { htmlFor: inputId, children: label }), _jsx(Input, { id: inputId, role: "combobox", "aria-expanded": true, "aria-controls": listId, "aria-autocomplete": "list", "aria-activedescendant": active ? listboxOptionId(listId, highlight) : undefined, value: query, disabled: disabled, placeholder: placeholder, onChange: (event) => setQuery(event.target.value), onKeyDown: onKeyDown }), _jsx("ul", { id: listId, role: "listbox", "aria-label": listLabel ?? label, className: empty
88
- ? "sr-only"
89
- : cn("max-h-60 overflow-y-auto", listFrame, listClassName), children: rows.map(({ item, value: itemValue }, index) => (_jsx(PickerOption, { "data-slot": "search-picker-option", listId: listId, index: index, active: index === highlight, selected: itemValue === value, disabled: disabled, item: item, getLabel: getLabel, onHighlight: setHighlight, onSelect: selectItem }, itemValue))) }), _jsx(ListboxEmptyStatus, { "data-slot": "search-picker-empty", empty: empty, className: listFrame, children: emptyText })] }));
88
+ return (_jsxs("div", { "data-slot": "search-picker", className: cn("flex flex-col gap-1.5", className), ...props, children: [_jsx(Label, { htmlFor: inputId, children: label }), _jsx(Input, { id: inputId, role: "combobox", "aria-expanded": true, "aria-controls": listId, "aria-autocomplete": "list", "aria-activedescendant": active ? listboxOptionId(listId, highlight) : undefined, value: query, disabled: disabled, placeholder: placeholder, onChange: (event) => setQuery(event.target.value), onKeyDown: onKeyDown }), _jsx("ul", { id: listId, role: "listbox", "aria-label": listLabel ?? label, className: empty ? "sr-only" : cn(listboxScroll, listFrame, listClassName), children: rows.map(({ item, value: itemValue }, index) => (_jsx(PickerOption, { "data-slot": "search-picker-option", listId: listId, index: index, active: index === highlight, selected: itemValue === value, disabled: disabled, item: item, getLabel: getLabel, onHighlight: setHighlight, onSelect: selectItem }, itemValue))) }), _jsx(ListboxEmptyStatus, { "data-slot": "search-picker-empty", empty: empty, className: listFrame, children: emptyText })] }));
90
89
  }
91
90
  export { SearchPicker };
package/dist/select.js CHANGED
@@ -2,8 +2,8 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { Select as SelectPrimitive } from "radix-ui";
5
- import { dataDisabledState, disabledState, fieldSurface, focusRing, invalidState, optionState, } from "./internal/styles.js";
6
- import { menuLabel, optionIcon, optionRow, overlayFadeZoom, overlaySideSlide, overlaySurface, } from "./internal/overlay-styles.js";
5
+ import { dataDisabledState, disabledState, fieldSurface, focusRing, inlineIcon, invalidState, optionState, } from "./internal/styles.js";
6
+ import { menuLabel, optionRow, overlayFadeZoom, overlaySideSlide, overlaySurface, } from "./internal/overlay-styles.js";
7
7
  import { cn } from "./utils.js";
8
8
  import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from "lucide-react";
9
9
  /**
@@ -11,7 +11,7 @@ import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from "lucide-react";
11
11
  * replaces them (a `data-[size=*]:` variant would outrank it).
12
12
  */
13
13
  const triggerHeight = { default: "h-10", sm: "h-9" };
14
- const scrollButtonClassName = "z-10 flex cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-3.5";
14
+ const scrollButtonClassName = cn("z-10 flex cursor-default items-center justify-center bg-popover py-1", inlineIcon.default);
15
15
  function Select({ ...props }) {
16
16
  return _jsx(SelectPrimitive.Root, { "data-slot": "select", ...props });
17
17
  }
@@ -22,7 +22,7 @@ function SelectValue({ ...props }) {
22
22
  return _jsx(SelectPrimitive.Value, { "data-slot": "select-value", ...props });
23
23
  }
24
24
  function SelectTrigger({ className, size = "default", children, ...props }) {
25
- return (_jsxs(SelectPrimitive.Trigger, { "data-slot": "select-trigger", "data-size": size, className: cn(fieldSurface, focusRing, invalidState, disabledState, optionIcon, triggerHeight[size], "flex items-center justify-between gap-1.5 whitespace-nowrap data-placeholder:text-muted-foreground *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5", className), ...props, children: [children, _jsx(SelectPrimitive.Icon, { "data-slot": "select-icon", asChild: true, children: _jsx(ChevronDownIcon, { className: "pointer-events-none size-3.5 text-muted-foreground" }) })] }));
25
+ return (_jsxs(SelectPrimitive.Trigger, { "data-slot": "select-trigger", "data-size": size, className: cn(fieldSurface, focusRing, invalidState, disabledState, inlineIcon.default, triggerHeight[size], "flex items-center justify-between gap-1.5 whitespace-nowrap data-placeholder:text-muted-foreground *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5", className), ...props, children: [children, _jsx(SelectPrimitive.Icon, { "data-slot": "select-icon", asChild: true, children: _jsx(ChevronDownIcon, { className: "pointer-events-none size-3.5 text-muted-foreground" }) })] }));
26
26
  }
27
27
  function SelectContent({ className, children, position = "popper", align = "center", sideOffset = 4, collisionPadding = 8, ...props }) {
28
28
  return (_jsx(SelectPrimitive.Portal, { children: _jsxs(SelectPrimitive.Content, { "data-slot": "select-content", "data-align-trigger": position === "item-aligned", className: cn(overlaySurface, overlaySideSlide, overlayFadeZoom, "relative z-50 max-h-(--radix-select-content-available-height) min-w-(--radix-select-trigger-width) origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto data-[align-trigger=true]:animate-none", position === "popper" &&
@@ -32,7 +32,7 @@ function SelectLabel({ className, ...props }) {
32
32
  return (_jsx(SelectPrimitive.Label, { "data-slot": "select-label", className: cn(menuLabel, className), ...props }));
33
33
  }
34
34
  function SelectItem({ className, children, ...props }) {
35
- return (_jsxs(SelectPrimitive.Item, { "data-slot": "select-item", className: cn(optionRow, optionState.focus, optionState.checked, dataDisabledState, optionIcon, "pr-8 not-data-[variant=destructive]:focus:**:text-accent-foreground *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2", className), ...props, children: [_jsx("span", { className: "pointer-events-none absolute right-2 flex size-4 items-center justify-center", children: _jsx(SelectPrimitive.ItemIndicator, { "data-slot": "select-item-indicator", children: _jsx(CheckIcon, { className: "pointer-events-none" }) }) }), _jsx(SelectPrimitive.ItemText, { children: children })] }));
35
+ return (_jsxs(SelectPrimitive.Item, { "data-slot": "select-item", className: cn(optionRow, optionState.focus, optionState.checked, dataDisabledState, inlineIcon.default, "pr-8 not-data-[variant=destructive]:focus:**:text-accent-foreground *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2", className), ...props, children: [_jsx("span", { className: "pointer-events-none absolute right-2 flex size-4 items-center justify-center", children: _jsx(SelectPrimitive.ItemIndicator, { "data-slot": "select-item-indicator", children: _jsx(CheckIcon, { className: "pointer-events-none" }) }) }), _jsx(SelectPrimitive.ItemText, { children: children })] }));
36
36
  }
37
37
  function SelectSeparator({ className, ...props }) {
38
38
  return (_jsx(SelectPrimitive.Separator, { "data-slot": "select-separator", className: cn("pointer-events-none -mx-1.5 my-1.5 h-px bg-border", className), ...props }));
package/dist/tabs.d.ts CHANGED
@@ -1,33 +1,44 @@
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 = {
7
- /** Classes for the scroller around the tab row (`h-13` with `p-1` by default). */
8
+ /** Classes for the scroller around the tab row (`h-9` with `p-1` by default). */
8
9
  scrollerClassName?: string;
9
10
  };
10
11
  declare function Tabs({ className, orientation, ...props }: React.ComponentProps<typeof TabsPrimitive.Root>): React.JSX.Element;
11
12
  /**
12
- * The tab row inside its scroller. `className` styles the list (its `h-11`
13
+ * The tab row inside its scroller. `className` styles the list (its `h-7`
13
14
  * height, fill, gap); `scrollerClassName` styles the scroller around it
14
- * (`h-13` with `p-1` for the active border and focus ring).
15
+ * (`h-9` with `p-1` for the active border and focus ring).
15
16
  */
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,13 +2,14 @@
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
  /**
11
- * The root's orientation, so the list can pick plain size classes (`h-11`)
12
+ * The root's orientation, so the list can pick plain size classes (`h-7`)
12
13
  * that a consumer `className` replaces, instead of `group-data-*` variants
13
14
  * that would beat it.
14
15
  */
@@ -20,14 +21,16 @@ function Tabs({ className, orientation = "horizontal", ...props }) {
20
21
  * Horizontal scroller around a tab row. `overflow-x: auto` alone would
21
22
  * compute `overflow-y` to `auto` as well (CSS overflow pairing), so the
22
23
  * active tab's border and focus ring grow a vertical scrollbar. Pad for
23
- * those strokes *inside* the box (`h-13` = `h-11` list + `p-1`), clip the
24
+ * those strokes *inside* the box (`h-9` = `h-7` 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(() => {
@@ -47,9 +50,9 @@ function TabsScroller({ variant = "default", orientation = "horizontal", classNa
47
50
  return (_jsx("div", { ref: ref, "data-slot": "tabs-scroller", "data-variant": variant, tabIndex: overflowing ? 0 : undefined, className: cn(focusRing, tabsScrollerVariants({ variant, orientation }), className), children: children }));
48
51
  }
49
52
  /**
50
- * The tab row inside its scroller. `className` styles the list (its `h-11`
53
+ * The tab row inside its scroller. `className` styles the list (its `h-7`
51
54
  * height, fill, gap); `scrollerClassName` styles the scroller around it
52
- * (`h-13` with `p-1` for the active border and focus ring).
55
+ * (`h-9` with `p-1` for the active border and focus ring).
53
56
  */
54
57
  function TabsList({ className, scrollerClassName, variant = "default", ...props }) {
55
58
  const orientation = React.useContext(TabsOrientationContext);
@@ -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/dist/toggle.d.ts CHANGED
@@ -5,6 +5,8 @@ import { Toggle as TogglePrimitive } from "radix-ui";
5
5
  * Selected chrome shared by Toggle and ToggleGroupItem (see
6
6
  * `selectedState.toggle`): the wash, selected text, and a selected border
7
7
  * for `data-state=on`, `aria-pressed`, and `aria-checked`.
8
+ *
9
+ * @deprecated `toggleVariants()` already includes this chrome. Removed in 2.0.
8
10
  */
9
11
  declare const selectedToggleChrome: string;
10
12
  declare const toggleVariants: (props?: ({
package/dist/toggle.js CHANGED
@@ -3,21 +3,26 @@ import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { cva } from "class-variance-authority";
5
5
  import { Toggle as TogglePrimitive } from "radix-ui";
6
- import { controlSize, disabledState, focusRing, invalidState, selectedState, } from "./internal/styles.js";
6
+ import { controlSize, controlText, disabledState, focusRing, inlineIcon, invalidState, selectedState, } from "./internal/styles.js";
7
7
  import { cn } from "./utils.js";
8
8
  /**
9
9
  * Selected chrome shared by Toggle and ToggleGroupItem (see
10
10
  * `selectedState.toggle`): the wash, selected text, and a selected border
11
11
  * for `data-state=on`, `aria-pressed`, and `aria-checked`.
12
+ *
13
+ * @deprecated `toggleVariants()` already includes this chrome. Removed in 2.0.
12
14
  */
13
15
  const selectedToggleChrome = selectedState.toggle;
14
16
  const toggleVariants = cva([
15
17
  focusRing,
16
18
  selectedState.hover,
17
- selectedToggleChrome,
19
+ selectedState.toggle,
18
20
  invalidState,
19
21
  disabledState,
20
- "group/toggle inline-flex items-center justify-center gap-1.5 rounded-none text-xs font-semibold tracking-widest whitespace-nowrap uppercase transition-colors focus-visible:text-selected-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
22
+ "group/toggle inline-flex items-center justify-center gap-1.5 rounded-none",
23
+ controlText,
24
+ "transition-colors focus-visible:text-selected-foreground",
25
+ inlineIcon.default,
21
26
  ], {
22
27
  variants: {
23
28
  variant: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atlas-react-kit",
3
- "version": "1.0.0",
3
+ "version": "1.2.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);