atlas-react-kit 0.7.0 → 0.8.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/COMPONENTS.md CHANGED
@@ -5,60 +5,60 @@ underlying element or Radix props, including `className`, alongside their option
5
5
  Form helpers and `TableBodySkeleton` accept only their documented props. There is
6
6
  no root component barrel.
7
7
 
8
- | Subpath | Main exports and options |
9
- | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
10
- | `accordion` | `Accordion`, `AccordionItem`, `AccordionTrigger`, `AccordionContent`; Radix `type` `single` (with `collapsible`) or `multiple` |
11
- | `alert` | `Alert`, `AlertTitle`, `AlertDescription`, `alertVariants`; `variant` `default`, `success`, `warning`, `destructive`; tone on title and icon, body stays muted; destructive is `role="alert"` |
12
- | `avatar` | `Avatar`, `AvatarImage`, `AvatarFallback`; `size` `sm`, `default`, `lg`; round by design |
13
- | `breadcrumb` | `Breadcrumb`, `BreadcrumbList`, `BreadcrumbItem`, `BreadcrumbLink` (`asChild` for router links), `BreadcrumbPage` (`aria-current`), `BreadcrumbSeparator`, `BreadcrumbEllipsis` |
14
- | `button` | `Button`, `buttonVariants`, `quietButtonInteraction`; `variant`, `size`, `asChild`; outline/ghost hover is brand purple + selected wash, not muted grey |
15
- | `badge` | `Badge`, `badgeVariants`; `variant`, `tone`, `asChild` |
16
- | `calendar` | `Calendar`, `CalendarDayButton`; React Day Picker props and `buttonVariant` |
17
- | `card` | `Card`, `CardHeader`, `CardTitle`, `CardDescription`, `CardAction`, `CardContent`, `CardFooter`; `Card` supports `flush`; root uses `min-h-min shrink-0` so flex columns do not collapse it, and `min-w-0` so nested tables scroll; `CardContent className="px-0"` overrides padding without `!` |
18
- | `checkbox` | `Checkbox` |
19
- | `color-legend` | `ColorLegend`; `title`, `items` as `{ color, range?, label }[]`, optional `caption`, `children`, `aria-label`, `className` (overlay chrome). No attribution. |
20
- | `combobox` | `Combobox`; `options` as `{ value, label }[]`, `value`, `onValueChange`; optional placeholder, search, empty text; trigger matches Input/SelectTrigger chrome; keyboard highlight scrolls into view |
21
- | `date-picker` | `DatePickerButton`, `toDate`, `formatLocalDate`, `DateRange`; `mode` `"single"` (default) or `"range"`; single `value` is `Date` or `yyyy-MM-dd`; range `value` is `{ from, to }`; trigger matches Input chrome; optional `numberOfMonths` (range default 1 below `md`, 2 at `md+`) and `side` |
22
- | `date-time-picker` | `DateTimePickerButton`, `resolveTimeStep`, `DEFAULT_TIME_STEP`; controlled `value` is `Date` or `null`; `onChange` emits `Date \ |
23
- | `dialog` | `Dialog`, trigger, portal, overlay, content, close, header, footer, title, description; `DialogContent` supports `showCloseButton` |
24
- | `dropdown-menu` | `DropdownMenu` family (trigger, content, item, checkbox/radio items, label, separator, shortcut, sub menus); item `variant="destructive"` and `inset`; for commands, not values |
25
- | `empty-panel` | `EmptyPanel`; required `children`; optional `size`, `muted`, `className` |
26
- | `error-boundary` | `ErrorBoundary`, `ErrorFallback`; optional `message`; retry resets, reload refreshes the page |
27
- | `field` | Field, label, legend, description, group, set, title, content, separator, error; `Field` supports `orientation` |
28
- | `info-tip` | `InfoTip`, `InlineTip`; required `label` (aria-label); `title`, `description`, `children`, `layout` `box` \| `inline`, `side`, `align`, `badge`, `icon`, `className`. Popover, 20px glyph, 44px hit. Hover opens only on a fine pointer. Not a Tooltip. |
29
- | `input`, `textarea`, `label` | `Input`, `Textarea`, `Label` |
30
- | `markdown` | `Markdown`, `MarkdownContent`; `content`, optional `className`. GFM via `react-markdown` + `remark-gfm` in this subpath only. Sanitized (no raw HTML; http/https/mailto/#/`/` URLs). Token styles, no typography plugin. |
31
- | `meter` | `Meter`; `value`, `max`, optional `min` (default 0), `fillClassName`, `markers` as `{ position, className? }[]`, required `aria-label`, `className`. `role="meter"`. Not `Progress`. |
32
- | `pagination` | `Pagination`, `PaginationContent`, `PaginationItem`, `PaginationLink` (`isActive` sets `aria-current`, the selected wash, and a selected border), `PaginationPrevious`, `PaginationNext`, `PaginationEllipsis` |
33
- | `popover` | `Popover`, trigger, anchor, content, header, title, description |
34
- | `progress` | `Progress`; Radix props, `value` 0–100; label it with `aria-label` or `aria-labelledby`. For a value against a scale with ticks, use `Meter`. |
35
- | `radio-group` | `RadioGroup`, `RadioGroupItem`; short visible lists; arrow keys move the selection; round dots |
36
- | `search-picker` | `SearchPicker<T>`; always-visible combobox + listbox; `items`, `getValue`, `getLabel`, optional `filter` (default casefold + accent-fold includes), `value`, `onSelect`, `label`, `placeholder`, `emptyText`, `listLabel`, `disabled`, `className`. Arrows, Home/End, PageUp/PageDown, Enter, Escape. |
37
- | `select` | Select, trigger, value, content, group, label, item, separator, scroll buttons; trigger `size`; trigger text is `text-base md:text-sm` like Input |
38
- | `separator`, `skeleton` | `Separator`, `Skeleton` |
39
- | `slider` | `Slider`; `value` number, `onValueChange`, optional `onValueCommit`, `min`, `max`, `step`, `disabled`, `id`, `aria-labelledby`, `className`. Radix Slider; 44px thumb hit; square track; primary fill. Name it with `aria-labelledby`. |
40
- | `sortable-table-head` | `SortableTableHead`; `sorted`, `direction` (`asc` \ |
41
- | `sonner` | `Toaster`, `toast`; kit-styled toasts (status icons, kit buttons, close button); import `toast` from here, not `sonner` |
42
- | `spinner` | `Spinner`; `role="status"` with `aria-label` (default `Loading`) |
43
- | `stat-tile` | `StatTile`, `statTileGrid`, `sectionLabel`; `label`, `value`, optional `sub`, `children`, `className`. |
44
- | `switch` | `Switch`; settings that apply immediately; square track and thumb |
45
- | `table` | Table, header, body, footer, row, head, cell, caption; root scroller is `min-w-0 overflow-x-auto` with a fade cue; cells are `px-2` below `sm` |
46
- | `table-body-skeleton` | `TableBodySkeleton`; `columns`, optional `rows` (default 5) |
47
- | `tabs` | `Tabs`, `TabsList`, `TabsTrigger`, `TabsContent`, `TabsNav`, `tabsListVariants`, `tabsTriggerVariants`; list `variant` is `default` or `line`; style `TabsNav` links with `tabsTriggerVariants()` |
48
- | `toggle` | `Toggle`, `toggleVariants`, `selectedToggleChrome`; `variant`, `size`. Pressed state is the selected wash, text, and border via `data-state=on`, `aria-pressed`, and `aria-checked` |
49
- | `toggle-group` | `ToggleGroup`, `ToggleGroupItem`; `variant`, `size`. Items share `toggleVariants`, so single (radio) and multiple selections match a pressed `Toggle` |
50
- | `theme-provider` | `ThemeProvider`, `useTheme`; optional `storageKey` (default `atlas-theme`), `enableShortcut` for the `d` toggle |
51
- | `tooltip` | `Tooltip`, `TooltipTrigger`, `TooltipContent`, `TooltipProvider`; opens on hover and focus; short labels only |
52
- | `form/markdown-field` | `FormMarkdownField`; `field` is `TextFieldApi`; `label`, optional `id`, `placeholder`, `rows`, `showError`. Write / preview; preview uses `Markdown`. |
53
- | `form/text-field` | `FormTextField`, type `TextFieldApi`; controlled field, label, multiline, description, input constraints |
54
- | `form/select-field` | `FormSelectField`; `value`, `onValueChange`, `label`, optional `meta` and placeholder; select item children |
55
- | `form/date-picker-field` | `FormDatePickerField`; `value`, `onValueChange`, `label`, optional `meta`, placeholder, and `className` (forwarded to the trigger) |
56
- | `form/date-time-picker-field` | `FormDateTimePickerField`; `value` is `Date` or `null`; `onValueChange` emits `Date \ |
57
- | `form/field-error` | `FieldError`, `getFieldErrorMessage`; types `FieldErrorMessage`, `FieldMetaState` |
58
- | `form/feedback-field` | `FormFeedbackField`; nullable `message` |
59
- | `next/tabs-nav-link` | `TabsNavLink`; Next Link props and required `active` boolean; requires Next.js 16 |
60
- | `utils` | `cn`, `onInputChange` |
61
- | `theme.css` | Tailwind 4 theme; installation in [README](./README.md) |
8
+ | Subpath | Main exports and options |
9
+ | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
10
+ | `accordion` | `Accordion`, `AccordionItem`, `AccordionTrigger`, `AccordionContent`; Radix `type` `single` (with `collapsible`) or `multiple` |
11
+ | `alert` | `Alert`, `AlertTitle`, `AlertDescription`, `alertVariants`; `variant` `default`, `success`, `warning`, `destructive`; tone on title and icon, body stays muted; destructive is `role="alert"` |
12
+ | `avatar` | `Avatar`, `AvatarImage`, `AvatarFallback`; `size` `sm`, `default`, `lg`; round by design |
13
+ | `breadcrumb` | `Breadcrumb`, `BreadcrumbList`, `BreadcrumbItem`, `BreadcrumbLink` (`asChild` for router links), `BreadcrumbPage` (`aria-current`), `BreadcrumbSeparator`, `BreadcrumbEllipsis` |
14
+ | `button` | `Button`, `buttonVariants`, `quietButtonInteraction`; `variant`, `size`, `asChild`; outline/ghost hover is brand purple + selected wash, not muted grey |
15
+ | `badge` | `Badge`, `badgeVariants`; `variant`, `tone`, `asChild` |
16
+ | `calendar` | `Calendar`, `CalendarDayButton`; React Day Picker props and `buttonVariant` |
17
+ | `card` | `Card`, `CardHeader`, `CardTitle`, `CardDescription`, `CardAction`, `CardContent`, `CardFooter`; `Card` supports `flush`; root uses `min-h-min shrink-0` so flex columns do not collapse it, and `min-w-0` so nested tables scroll; `CardContent className="px-0"` overrides padding without `!` |
18
+ | `checkbox` | `Checkbox` |
19
+ | `color-legend` | `ColorLegend`; `title`, `items` as `{ color, range?, label }[]`, optional `caption`, `children`, `aria-label`, `className` (overlay chrome). No attribution. |
20
+ | `combobox` | `Combobox`; `options` as `{ value, label }[]`, `value`, `onValueChange`; optional placeholder, search, empty text; trigger matches Input/SelectTrigger chrome; keyboard highlight scrolls into view |
21
+ | `date-picker` | `DatePickerButton`, `toDate`, `formatLocalDate`, `DateRange`; `mode` `"single"` (default) or `"range"`; single `value` is `Date` or `yyyy-MM-dd`; range `value` is `{ from, to }`; trigger matches Input chrome; optional `numberOfMonths` (range default 1 below `md`, 2 at `md+`) and `side` |
22
+ | `date-time-picker` | `DateTimePickerButton`, `resolveTimeStep`, `DEFAULT_TIME_STEP`; controlled `value` is `Date` or `null`; `onChange` emits `Date \ |
23
+ | `dialog` | `Dialog`, trigger, portal, overlay, content, close, header, footer, title, description; `DialogContent` supports `showCloseButton` |
24
+ | `dropdown-menu` | `DropdownMenu` family (trigger, content, item, checkbox/radio items, label, separator, shortcut, sub menus); item `variant="destructive"` and `inset`; for commands, not values |
25
+ | `empty-panel` | `EmptyPanel`; required `children`; optional `size`, `muted`, `className` |
26
+ | `error-boundary` | `ErrorBoundary`, `ErrorFallback`; optional `message`; retry resets, reload refreshes the page |
27
+ | `field` | Field, label, legend, description, group, set, title, content, separator, error; `Field` supports `orientation` |
28
+ | `info-tip` | `InfoTip`, `InlineTip`; required `label` (aria-label); `title`, `description`, `children`, `layout` `box` \| `inline`, `side`, `align`, `badge`, `icon`, `className`. Popover, 20px glyph, 44px hit. Hover opens only on a fine pointer. Not a Tooltip. |
29
+ | `input`, `textarea`, `label` | `Input`, `Textarea`, `Label` |
30
+ | `markdown` | `Markdown`, `MarkdownContent`; `content`, optional `className`. GFM via `react-markdown` + `remark-gfm` in this subpath only. Sanitized (no raw HTML; http/https/mailto/#/`/` URLs). Token styles, no typography plugin. |
31
+ | `meter` | `Meter`; `value`, `max`, optional `min` (default 0), `fillClassName`, `markers` as `{ position, className? }[]`, required `aria-label`, `className`. `role="meter"`. Not `Progress`. |
32
+ | `pagination` | `Pagination`, `PaginationContent`, `PaginationItem`, `PaginationLink` (`isActive` sets `aria-current`, the selected wash, and a selected border), `PaginationPrevious`, `PaginationNext`, `PaginationEllipsis` |
33
+ | `popover` | `Popover`, trigger, anchor, content, header, title, description |
34
+ | `progress` | `Progress`; Radix props, `value` 0–100; label it with `aria-label` or `aria-labelledby`. For a value against a scale with ticks, use `Meter`. |
35
+ | `radio-group` | `RadioGroup`, `RadioGroupItem`; short visible lists; arrow keys move the selection; round dots |
36
+ | `search-picker` | `SearchPicker<T>`; always-visible combobox + listbox; `items`, `getValue`, `getLabel`, optional `filter` (default casefold + accent-fold includes), `value`, `onSelect`, `label`, `placeholder`, `emptyText`, `listLabel`, `disabled`, `className`. Arrows, Home/End, PageUp/PageDown, Enter, Escape. |
37
+ | `select` | Select, trigger, value, content, group, label, item, separator, scroll buttons; trigger `size`; trigger text is `text-base md:text-sm` like Input |
38
+ | `separator`, `skeleton` | `Separator`, `Skeleton` |
39
+ | `slider` | `Slider`; `value` number, `onValueChange`, optional `onValueCommit`, `min`, `max`, `step`, `disabled`, `id`, `aria-labelledby`, `className`. Radix Slider; 44px thumb hit; square track; primary fill. Name it with `aria-labelledby`. |
40
+ | `sortable-table-head` | `SortableTableHead`; `sorted`, `direction` (`asc` \ |
41
+ | `spinner` | `Spinner`; `role="status"` with `aria-label` (default `Loading`) |
42
+ | `stat-tile` | `StatTile`, `statTileGrid`, `sectionLabel`; `label`, `value`, optional `sub`, `children`, `className`. |
43
+ | `switch` | `Switch`; settings that apply immediately; square track and thumb |
44
+ | `table` | Table, header, body, footer, row, head, cell, caption; root scroller is `min-w-0 overflow-x-auto` with a fade cue; cells are `px-2` below `sm` |
45
+ | `table-body-skeleton` | `TableBodySkeleton`; `columns`, optional `rows` (default 5) |
46
+ | `tabs` | `Tabs`, `TabsList`, `TabsTrigger`, `TabsContent`, `TabsNav`, `tabsListVariants`, `tabsTriggerVariants`; list `variant` is `default` or `line`; style `TabsNav` links with `tabsTriggerVariants()`; `TabsNav` / `TabsList` scroll horizontally only (`overflow-y` hidden, edge fade, active item into view) — do not wrap them in `overflow-x-auto` |
47
+ | `toast` | `Toaster`, `toast`; mount one `Toaster` (`position`, `closeButton` (default on), `expand`, `visibleToasts`, `duration`), then `toast(message, options)`, `.success`, `.info`, `.warning`, `.error`, `.loading`, `.dismiss(id?)`. Options: `id` (replaces that toast), `description`, `duration`, `action` / `cancel` as `{ label, onClick }`. Kit-owned API: no toast library is exposed |
48
+ | `toggle` | `Toggle`, `toggleVariants`, `selectedToggleChrome`; `variant`, `size`. Pressed state is the selected wash, text, and border via `data-state=on`, `aria-pressed`, and `aria-checked` |
49
+ | `toggle-group` | `ToggleGroup`, `ToggleGroupItem`; `variant`, `size`. Items share `toggleVariants`, so single (radio) and multiple selections match a pressed `Toggle` |
50
+ | `theme-provider` | `ThemeProvider`, `useTheme`; optional `storageKey` (default `atlas-theme`), `enableShortcut` for the `d` toggle |
51
+ | `tooltip` | `Tooltip`, `TooltipTrigger`, `TooltipContent`, `TooltipProvider`; opens on hover and focus; short labels only |
52
+ | `form/markdown-field` | `FormMarkdownField`; `field` is `TextFieldApi`; `label`, optional `id`, `placeholder`, `rows`, `showError`. Write / preview; preview uses `Markdown`. |
53
+ | `form/text-field` | `FormTextField`, type `TextFieldApi`; controlled field, label, multiline, description, input constraints |
54
+ | `form/select-field` | `FormSelectField`; `value`, `onValueChange`, `label`, optional `meta` and placeholder; select item children |
55
+ | `form/date-picker-field` | `FormDatePickerField`; `value`, `onValueChange`, `label`, optional `meta`, placeholder, and `className` (forwarded to the trigger) |
56
+ | `form/date-time-picker-field` | `FormDateTimePickerField`; `value` is `Date` or `null`; `onValueChange` emits `Date \ |
57
+ | `form/field-error` | `FieldError`, `getFieldErrorMessage`; types `FieldErrorMessage`, `FieldMetaState` |
58
+ | `form/feedback-field` | `FormFeedbackField`; nullable `message` |
59
+ | `next/tabs-nav-link` | `TabsNavLink`; Next Link props and required `active` boolean; requires Next.js 16 |
60
+ | `utils` | `cn`, `onInputChange` |
61
+ | `theme.css` | Tailwind 4 theme; installation in [README](./README.md) |
62
62
 
63
63
  ## Status
64
64
 
package/DESIGN_RULES.md CHANGED
@@ -54,8 +54,8 @@ keyboard and screen-reader testing.
54
54
  - Focus is a solid `ring-ring` (brand purple), never a translucent ring such
55
55
  as `ring-ring/30`, which fades below 3:1. Keyboard-highlighted menu and list
56
56
  items show a 2px `ring` bar on their leading edge. Content that scrolls must
57
- be reachable by keyboard; `Table` adds itself to the tab order while it
58
- overflows.
57
+ be reachable by keyboard; `Table`, `TabsList`, and `TabsNav` add their
58
+ scroller to the tab order while they overflow.
59
59
 
60
60
  ## Writing
61
61
 
@@ -103,7 +103,11 @@ triggers as outline buttons in application code.
103
103
 
104
104
  Use `Tabs` with actual `TabsContent` panels. For URL view switches, use a labelled
105
105
  `TabsNav` with links and `aria-current="page"` on the active link. The optional
106
- Next adapter provides `TabsNavLink` with this behavior.
106
+ Next adapter provides `TabsNavLink` with this behavior. `TabsNav` and `TabsList`
107
+ scroll overflowing labels inside themselves (`overflow-x-auto overflow-y-hidden`,
108
+ with padding for the active border and focus ring, and an edge fade like
109
+ `Table`). Do not wrap them in another `overflow-x-auto` — that pairing forces
110
+ `overflow-y: auto` and a stray vertical scrollbar.
107
111
 
108
112
  Use `FormTextField`, `FormSelectField`, `FormDatePickerField`,
109
113
  `FormDateTimePickerField`, `FormMarkdownField`, and `FormFeedbackField` for
package/README.md CHANGED
@@ -7,7 +7,7 @@ keeping locally (`InfoTip`, `ColorLegend`, `SearchPicker`, `Markdown`,
7
7
  React 19 and Tailwind 4 are required.
8
8
 
9
9
  ```sh
10
- npm install --save-exact atlas-react-kit@0.7.0
10
+ npm install --save-exact atlas-react-kit@0.8.1
11
11
  ```
12
12
 
13
13
  Import components by subpath:
@@ -116,7 +116,7 @@ artifact with the package owner's npm login:
116
116
  ```sh
117
117
  mkdir -p release-artifacts
118
118
  npm pack --pack-destination release-artifacts
119
- npm publish ./release-artifacts/atlas-react-kit-0.7.0.tgz --access public
119
+ npm publish ./release-artifacts/atlas-react-kit-0.8.1.tgz --access public
120
120
  ```
121
121
 
122
122
  See [CONTRIBUTING.md](./CONTRIBUTING.md) for adding components, the browser
@@ -1,6 +1,6 @@
1
1
  import { cva } from "class-variance-authority";
2
2
  import { disabledState, focusRing, selectedState } from "./styles.js";
3
- const tabsListVariants = cva('group/tabs-list inline-flex w-fit items-stretch justify-center p-0 text-muted-foreground group-data-[orientation="horizontal"]/tabs:h-11 group-data-[orientation="vertical"]/tabs:h-fit group-data-[orientation="vertical"]/tabs:flex-col', {
3
+ const tabsListVariants = cva('group/tabs-list inline-flex w-max min-w-full items-stretch justify-start p-0 text-muted-foreground group-data-[orientation="horizontal"]/tabs:h-11 group-data-[orientation="vertical"]/tabs:h-fit group-data-[orientation="vertical"]/tabs:flex-col', {
4
4
  variants: {
5
5
  variant: {
6
6
  default: "bg-muted",
@@ -14,7 +14,10 @@ const tabsListVariants = cva('group/tabs-list inline-flex w-fit items-stretch ju
14
14
  const tabsTriggerVariants = cva([
15
15
  focusRing,
16
16
  disabledState,
17
- 'relative inline-flex h-full min-w-0 flex-1 items-center justify-center gap-2 border border-transparent px-4 py-1.5 text-xs font-semibold tracking-wider whitespace-nowrap text-foreground/60 uppercase transition-all group-data-[orientation="vertical"]/tabs:w-full group-data-[orientation="vertical"]/tabs:justify-start group-data-[orientation="vertical"]/tabs:px-4 group-data-[orientation="vertical"]/tabs:py-2 hover:text-foreground has-data-[icon=inline-end]:pr-2.5 has-data-[icon=inline-start]:pl-2.5 dark:text-muted-foreground dark:hover:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-3.5',
17
+ // flex-none + px-4: every trigger and TabsNav link keeps the same
18
+ // horizontal padding. flex-1 used to grow short labels and make the
19
+ // longest (e.g. Apple Reminders) look tighter. No last-child padding.
20
+ 'relative inline-flex h-full min-w-max flex-none items-center justify-center gap-2 border border-transparent px-4 py-1.5 text-xs font-semibold tracking-wider whitespace-nowrap text-foreground/60 uppercase transition-all group-data-[orientation="vertical"]/tabs:w-full group-data-[orientation="vertical"]/tabs:justify-start group-data-[orientation="vertical"]/tabs:px-4 group-data-[orientation="vertical"]/tabs:py-2 hover:text-foreground has-data-[icon=inline-end]:pr-2.5 has-data-[icon=inline-start]:pl-2.5 dark:text-muted-foreground dark:hover:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-3.5',
18
21
  'group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state="active"]:border-transparent group-data-[variant=line]/tabs-list:data-[state="active"]:bg-transparent dark:group-data-[variant=line]/tabs-list:data-[state="active"]:bg-transparent',
19
22
  selectedState.active,
20
23
  'after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-[orientation="horizontal"]/tabs:after:inset-x-0 group-data-[orientation="horizontal"]/tabs:after:bottom-[-5px] group-data-[orientation="horizontal"]/tabs:after:h-0.5 group-data-[orientation="vertical"]/tabs:after:inset-y-0 group-data-[orientation="vertical"]/tabs:after:-right-1 group-data-[orientation="vertical"]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-[state="active"]:after:opacity-100',
@@ -0,0 +1,7 @@
1
+ import * as React from "react";
2
+ /**
3
+ * True while the element's content is wider than its box. Re-checked when
4
+ * the container or its content resizes. Used so a horizontal scroller can
5
+ * join the tab order only while it overflows (WCAG 2.1.1).
6
+ */
7
+ export declare function useHorizontalOverflow(ref: React.RefObject<HTMLElement | null>): boolean;
@@ -0,0 +1,23 @@
1
+ "use client";
2
+ import * as React from "react";
3
+ /**
4
+ * True while the element's content is wider than its box. Re-checked when
5
+ * the container or its content resizes. Used so a horizontal scroller can
6
+ * join the tab order only while it overflows (WCAG 2.1.1).
7
+ */
8
+ export function useHorizontalOverflow(ref) {
9
+ const [overflowing, setOverflowing] = React.useState(false);
10
+ React.useEffect(() => {
11
+ const element = ref.current;
12
+ if (!element || typeof ResizeObserver === "undefined")
13
+ return;
14
+ const check = () => setOverflowing(element.scrollWidth > element.clientWidth + 1);
15
+ check();
16
+ const observer = new ResizeObserver(check);
17
+ observer.observe(element);
18
+ if (element.firstElementChild)
19
+ observer.observe(element.firstElementChild);
20
+ return () => observer.disconnect();
21
+ }, [ref]);
22
+ return overflowing;
23
+ }
package/dist/table.js CHANGED
@@ -2,27 +2,8 @@
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { focusRing } from "./internal/styles.js";
5
+ import { useHorizontalOverflow } from "./internal/use-horizontal-overflow.js";
5
6
  import { cn } from "./utils.js";
6
- /**
7
- * True while the element's content is wider than its box. Re-checked when
8
- * the container or its content resizes.
9
- */
10
- function useHorizontalOverflow(ref) {
11
- const [overflowing, setOverflowing] = React.useState(false);
12
- React.useEffect(() => {
13
- const element = ref.current;
14
- if (!element || typeof ResizeObserver === "undefined")
15
- return;
16
- const check = () => setOverflowing(element.scrollWidth > element.clientWidth + 1);
17
- check();
18
- const observer = new ResizeObserver(check);
19
- observer.observe(element);
20
- if (element.firstElementChild)
21
- observer.observe(element.firstElementChild);
22
- return () => observer.disconnect();
23
- }, [ref]);
24
- return overflowing;
25
- }
26
7
  /**
27
8
  * Data table. The root is a `min-w-0 overflow-x-auto` scroller so nowrap
28
9
  * columns scroll inside the table instead of widening a card or panel.
package/dist/tabs.d.ts CHANGED
@@ -13,6 +13,11 @@ declare function TabsContent({ className, ...props }: React.ComponentProps<typeo
13
13
  * with `tabsTriggerVariants()` and mark the active one with
14
14
  * `data-state="active"` and `aria-current="page"` (the Next adapter
15
15
  * `TabsNavLink` does this for you).
16
+ *
17
+ * Overflow is handled here: the row scrolls horizontally only, with padding
18
+ * for active borders and focus rings, an edge fade while it overflows, and
19
+ * the active link scrolled into view on mount. Do not wrap it in
20
+ * `overflow-x-auto`.
16
21
  */
17
22
  declare function TabsNav({ className, children, ...props }: React.ComponentProps<"nav"> & {
18
23
  "aria-label": string;
package/dist/tabs.js CHANGED
@@ -3,13 +3,40 @@ import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import {} from "class-variance-authority";
5
5
  import { Tabs as TabsPrimitive } from "radix-ui";
6
- import { cn } from "./utils.js";
6
+ import { focusRing } from "./internal/styles.js";
7
7
  import { tabsListVariants, tabsTriggerVariants, } from "./internal/tabs-styles.js";
8
+ import { useHorizontalOverflow } from "./internal/use-horizontal-overflow.js";
9
+ import { cn } from "./utils.js";
8
10
  function Tabs({ className, orientation = "horizontal", ...props }) {
9
11
  return (_jsx(TabsPrimitive.Root, { "data-slot": "tabs", "data-orientation": orientation, orientation: orientation, className: cn('group/tabs flex gap-2 data-[orientation="horizontal"]:flex-col', className), ...props }));
10
12
  }
13
+ /**
14
+ * Horizontal scroller around a tab row. `overflow-x: auto` alone would
15
+ * compute `overflow-y` to `auto` as well (CSS overflow pairing), so the
16
+ * active tab's border and focus ring grow a vertical scrollbar. Clip the
17
+ * y axis, pad for those strokes, and fade the edges while the row
18
+ * overflows — same cue as `Table`.
19
+ */
20
+ function TabsScroller({ variant = "default", children, }) {
21
+ const ref = React.useRef(null);
22
+ const overflowing = useHorizontalOverflow(ref);
23
+ React.useLayoutEffect(() => {
24
+ const scroller = ref.current;
25
+ if (!scroller)
26
+ return;
27
+ const active = scroller.querySelector('[aria-current="page"], [data-state="active"]');
28
+ active?.scrollIntoView({
29
+ block: "nearest",
30
+ inline: "nearest",
31
+ behavior: "auto",
32
+ });
33
+ }, []);
34
+ return (_jsx("div", { ref: ref, "data-slot": "tabs-scroller", "data-variant": variant, tabIndex: overflowing ? 0 : undefined, className: cn(focusRing, "relative w-full min-w-0 max-w-full overflow-x-auto overflow-y-hidden overscroll-x-contain p-1 [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden", variant === "line" && "pb-2"), children: children }));
35
+ }
11
36
  function TabsList({ className, variant = "default", ...props }) {
12
- return (_jsx(TabsPrimitive.List, { "data-slot": "tabs-list", "data-variant": variant, className: cn(tabsListVariants({ variant }), className), ...props }));
37
+ return (_jsx(TabsScroller, { variant: variant ?? "default", children: _jsx(TabsPrimitive.List, { "data-slot": "tabs-list", "data-variant": variant, className: cn(tabsListVariants({ variant }),
38
+ // The scroller owns the muted fill so edge fades show through.
39
+ variant === "default" && "bg-transparent", className), ...props }) }));
13
40
  }
14
41
  function TabsTrigger({ className, ...props }) {
15
42
  return (_jsx(TabsPrimitive.Trigger, { "data-slot": "tabs-trigger", className: cn(tabsTriggerVariants(), className), ...props }));
@@ -24,8 +51,13 @@ function TabsContent({ className, ...props }) {
24
51
  * with `tabsTriggerVariants()` and mark the active one with
25
52
  * `data-state="active"` and `aria-current="page"` (the Next adapter
26
53
  * `TabsNavLink` does this for you).
54
+ *
55
+ * Overflow is handled here: the row scrolls horizontally only, with padding
56
+ * for active borders and focus rings, an edge fade while it overflows, and
57
+ * the active link scrolled into view on mount. Do not wrap it in
58
+ * `overflow-x-auto`.
27
59
  */
28
60
  function TabsNav({ className, children, ...props }) {
29
- return (_jsx("nav", { "data-slot": "tabs-nav", "data-orientation": "horizontal", className: cn("group/tabs", className), ...props, children: _jsx("div", { "data-slot": "tabs-list", "data-variant": "default", className: tabsListVariants(), children: children }) }));
61
+ return (_jsx("nav", { "data-slot": "tabs-nav", "data-orientation": "horizontal", className: cn("group/tabs min-w-0 w-full max-w-full", className), ...props, children: _jsx(TabsScroller, { children: _jsx("div", { "data-slot": "tabs-list", "data-variant": "default", className: cn(tabsListVariants(), "bg-transparent"), children: children }) }) }));
30
62
  }
31
63
  export { Tabs, TabsList, TabsTrigger, TabsContent, TabsNav, tabsListVariants, tabsTriggerVariants, };
@@ -0,0 +1,57 @@
1
+ import type { ReactNode } from "react";
2
+ /** Identifies a toast, to replace it in place or dismiss it. */
3
+ type ToastId = string | number;
4
+ /** A button in a toast. Clicking it runs `onClick`, then closes the toast. */
5
+ interface ToastAction {
6
+ label: string;
7
+ onClick: () => void;
8
+ }
9
+ interface ToastOptions {
10
+ /** An existing toast's id replaces that toast, e.g. loading → success. */
11
+ id?: ToastId;
12
+ /** A second, muted line under the message. */
13
+ description?: ReactNode;
14
+ /** Milliseconds before it closes; `Number.POSITIVE_INFINITY` keeps it open. */
15
+ duration?: number;
16
+ /** The primary button, such as "Undo" or "Retry". */
17
+ action?: ToastAction;
18
+ /** A secondary outline button, such as "Dismiss". */
19
+ cancel?: ToastAction;
20
+ }
21
+ /** Shows a toast and returns its id. */
22
+ type ShowToast = (message: ReactNode, options?: ToastOptions) => ToastId;
23
+ type Toast = ShowToast & {
24
+ success: ShowToast;
25
+ info: ShowToast;
26
+ warning: ShowToast;
27
+ error: ShowToast;
28
+ /** A spinner toast that stays until it is replaced (same `id`) or dismissed. */
29
+ loading: ShowToast;
30
+ /** Closes one toast by id, or every toast when called without one. */
31
+ dismiss: (id?: ToastId) => void;
32
+ };
33
+ type ToastPosition = "top-left" | "top-center" | "top-right" | "bottom-left" | "bottom-center" | "bottom-right";
34
+ interface ToasterProps {
35
+ /** Where toasts stack. Default `bottom-right`. */
36
+ position?: ToastPosition;
37
+ /** A labelled close button on every toast. Default `true`. */
38
+ closeButton?: boolean;
39
+ /** Keep the stack expanded instead of collapsed behind the newest toast. */
40
+ expand?: boolean;
41
+ /** How many toasts show at once. Default 3. */
42
+ visibleToasts?: number;
43
+ /** Milliseconds before a toast closes when it sets no `duration`. Default 4000. */
44
+ duration?: number;
45
+ }
46
+ /**
47
+ * Shows a toast through the app's one `Toaster`: `toast("Saved")`,
48
+ * `toast.success(...)`, `toast.error(...)`. Returns its id.
49
+ */
50
+ declare const toast: Toast;
51
+ /**
52
+ * Mount one near the app root, and show toasts with `toast()` from this
53
+ * module. Only the props declared here reach the library.
54
+ */
55
+ declare const Toaster: ({ position, closeButton, expand, visibleToasts, duration, }: ToasterProps) => import("react").JSX.Element;
56
+ export { Toaster, toast };
57
+ export type { ToastAction, ToastId, ToastOptions, ToastPosition, ToasterProps };
@@ -2,9 +2,38 @@
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { CircleAlertIcon, CircleCheckIcon, InfoIcon, LoaderCircleIcon, XIcon, } from "lucide-react";
4
4
  import { useTheme } from "next-themes";
5
- import { Toaster as Sonner, toast } from "sonner";
5
+ import { Toaster as Sonner, toast as sonnerToast, } from "sonner";
6
6
  import { buttonVariants } from "./button.js";
7
7
  import { cn } from "./utils.js";
8
+ /**
9
+ * Copies only the kit's options, so plain JavaScript callers cannot hand the
10
+ * library its own styling options (class names, `unstyled`, rich colors).
11
+ */
12
+ function libraryOptions(options = {}) {
13
+ const { id, description, duration, action, cancel } = options;
14
+ return {
15
+ id,
16
+ description,
17
+ duration,
18
+ action: action && { label: action.label, onClick: () => action.onClick() },
19
+ cancel: cancel && { label: cancel.label, onClick: () => cancel.onClick() },
20
+ };
21
+ }
22
+ const show = (method) => (message, options) => method(message, libraryOptions(options));
23
+ /**
24
+ * Shows a toast through the app's one `Toaster`: `toast("Saved")`,
25
+ * `toast.success(...)`, `toast.error(...)`. Returns its id.
26
+ */
27
+ const toast = Object.assign(show(sonnerToast), {
28
+ success: show(sonnerToast.success),
29
+ info: show(sonnerToast.info),
30
+ warning: show(sonnerToast.warning),
31
+ error: show(sonnerToast.error),
32
+ loading: show(sonnerToast.loading),
33
+ dismiss: (id) => {
34
+ sonnerToast.dismiss(id);
35
+ },
36
+ });
8
37
  /**
9
38
  * Toasts drawn entirely with kit classes. Sonner's own styles are unlayered
10
39
  * CSS that Tailwind utilities cannot override, so the toaster runs Sonner
@@ -36,26 +65,12 @@ const icons = {
36
65
  loading: (_jsx(LoaderCircleIcon, { "aria-hidden": true, className: "animate-spin text-muted-foreground" })),
37
66
  close: _jsx(XIcon, { "aria-hidden": true }),
38
67
  };
39
- /** Merge app class names onto the kit's, part by part. */
40
- function mergeClassNames(extra) {
41
- if (!extra)
42
- return classNames;
43
- const merged = { ...classNames };
44
- for (const [part, value] of Object.entries(extra)) {
45
- merged[part] = cn(classNames[part], value);
46
- }
47
- return merged;
48
- }
49
68
  /**
50
- * Mount one near the app root and call `toast()` from this module (not from
51
- * `sonner` directly), so the app and the toaster always share one Sonner.
69
+ * Mount one near the app root, and show toasts with `toast()` from this
70
+ * module. Only the props declared here reach the library.
52
71
  */
53
- const Toaster = ({ toastOptions, ...props }) => {
72
+ const Toaster = ({ position, closeButton = true, expand, visibleToasts, duration, }) => {
54
73
  const { theme = "system" } = useTheme();
55
- return (_jsx(Sonner, { theme: theme, className: "toaster group", closeButton: true, icons: icons, ...props, toastOptions: {
56
- ...toastOptions,
57
- unstyled: true,
58
- classNames: mergeClassNames(toastOptions?.classNames),
59
- } }));
74
+ return (_jsx(Sonner, { theme: theme, className: "toaster group", position: position, closeButton: closeButton, expand: expand, visibleToasts: visibleToasts, duration: duration, icons: icons, toastOptions: { unstyled: true, classNames } }));
60
75
  };
61
76
  export { Toaster, toast };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atlas-react-kit",
3
- "version": "0.7.0",
3
+ "version": "0.8.1",
4
4
  "description": "Shared React components and Tailwind theme for Atlas",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -148,10 +148,6 @@
148
148
  "types": "./dist/slider.d.ts",
149
149
  "default": "./dist/slider.js"
150
150
  },
151
- "./sonner": {
152
- "types": "./dist/sonner.d.ts",
153
- "default": "./dist/sonner.js"
154
- },
155
151
  "./sortable-table-head": {
156
152
  "types": "./dist/sortable-table-head.d.ts",
157
153
  "default": "./dist/sortable-table-head.js"
@@ -188,6 +184,10 @@
188
184
  "types": "./dist/theme-provider.d.ts",
189
185
  "default": "./dist/theme-provider.js"
190
186
  },
187
+ "./toast": {
188
+ "types": "./dist/toast.d.ts",
189
+ "default": "./dist/toast.js"
190
+ },
191
191
  "./toggle": {
192
192
  "types": "./dist/toggle.d.ts",
193
193
  "default": "./dist/toggle.js"
@@ -39,8 +39,10 @@ and tells Tailwind to scan the kit's compiled files:
39
39
 
40
40
  Dark mode needs `ThemeProvider` from `atlas-react-kit/theme-provider` near the
41
41
  root (or a `dark` class on an ancestor). Toasts need one `<Toaster />` from
42
- `atlas-react-kit/sonner`; import `toast` from `atlas-react-kit/sonner` too,
43
- never from `sonner` (a second copy of Sonner never reaches the kit's toaster). Fix missing setup rather than working
42
+ `atlas-react-kit/toast`; show them with `toast()` from the same module, using
43
+ only the options it declares. Never import a toast library such as `sonner`
44
+ directly: the kit owns the toast API, and a second copy never reaches its
45
+ toaster. Fix missing setup rather than working
44
46
  around unstyled components.
45
47
 
46
48
  ## 3. Pick the kit component for the job
@@ -125,7 +127,7 @@ list; the ones that come up most:
125
127
  Overriding their height, padding, or border makes one field look different
126
128
  from its neighbors.
127
129
  - **Layout overrides**: `CardContent className="px-0"` makes one section flush
128
- without `!important`. Tables already scroll horizontally inside cards, so do
130
+ without `!important`. Tables and tab rows already scroll horizontally, so do
129
131
  not wrap them in another overflow container.
130
132
  - **Headings**: `CardTitle` and `DialogTitle` already apply the uppercase,
131
133
  tracked heading style; use `font-heading` for other headings.
package/theme.css CHANGED
@@ -255,4 +255,53 @@
255
255
  [data-slot="card"] [data-slot="table-container"] {
256
256
  --table-scroll-fade: var(--card);
257
257
  }
258
+
259
+ /*
260
+ * Tab row overflow: same pinned-shadow / local-cover trick as tables.
261
+ * Covers match the muted list so a clipped label reads as overflow.
262
+ * The fifth layer is the list fill — the list itself stays transparent
263
+ * so the fades show through. Scrollbars stay hidden; the fade is the cue.
264
+ */
265
+ [data-slot="tabs-scroller"] {
266
+ --tabs-scroll-fade: var(--muted);
267
+ scrollbar-width: none;
268
+ -ms-overflow-style: none;
269
+ scroll-padding-inline: 1rem;
270
+ background:
271
+ linear-gradient(to right, var(--tabs-scroll-fade) 40%, transparent),
272
+ linear-gradient(to left, var(--tabs-scroll-fade) 40%, transparent),
273
+ linear-gradient(
274
+ to right,
275
+ color-mix(in oklch, var(--foreground) 16%, transparent),
276
+ transparent 80%
277
+ ),
278
+ linear-gradient(
279
+ to left,
280
+ color-mix(in oklch, var(--foreground) 16%, transparent),
281
+ transparent 80%
282
+ ),
283
+ var(--tabs-scroll-fade);
284
+ background-repeat: no-repeat;
285
+ background-size:
286
+ 1.5rem 100%,
287
+ 1.5rem 100%,
288
+ 0.75rem 100%,
289
+ 0.75rem 100%,
290
+ 100% 100%;
291
+ background-position:
292
+ left center,
293
+ right center,
294
+ left center,
295
+ right center,
296
+ center;
297
+ background-attachment: local, local, scroll, scroll, scroll;
298
+ }
299
+
300
+ [data-slot="tabs-scroller"]::-webkit-scrollbar {
301
+ display: none;
302
+ }
303
+
304
+ [data-slot="tabs-scroller"][data-variant="line"] {
305
+ --tabs-scroll-fade: var(--background);
306
+ }
258
307
  }
package/dist/sonner.d.ts DELETED
@@ -1,8 +0,0 @@
1
- import { toast, type ToasterProps } from "sonner";
2
- /**
3
- * Mount one near the app root and call `toast()` from this module (not from
4
- * `sonner` directly), so the app and the toaster always share one Sonner.
5
- */
6
- declare const Toaster: ({ toastOptions, ...props }: ToasterProps) => import("react").JSX.Element;
7
- export { Toaster, toast };
8
- export type { ToasterProps };