atlas-react-kit 0.8.0 → 0.8.2

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
@@ -43,7 +43,7 @@ no root component barrel.
43
43
  | `switch` | `Switch`; settings that apply immediately; square track and thumb |
44
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
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()` |
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, `p-1` counted in the scroller height, edge fade, active item into view) — do not wrap them in `overflow-x-auto` |
47
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
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
49
  | `toggle-group` | `ToggleGroup`, `ToggleGroupItem`; `variant`, `size`. Items share `toggleVariants`, so single (radio) and multiple selections match a pressed `Toggle` |
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,12 @@ 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 counted in the scroller
109
+ height, and an edge fade like `Table`). Do not wrap them in another
110
+ `overflow-x-auto` — that pairing forces `overflow-y: auto` and a stray
111
+ vertical scrollbar.
107
112
 
108
113
  Use `FormTextField`, `FormSelectField`, `FormDatePickerField`,
109
114
  `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.8.0
10
+ npm install --save-exact atlas-react-kit@0.8.2
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.8.0.tgz --access public
119
+ npm publish ./release-artifacts/atlas-react-kit-0.8.2.tgz --access public
120
120
  ```
121
121
 
122
122
  See [CONTRIBUTING.md](./CONTRIBUTING.md) for adding components, the browser
@@ -1,5 +1,17 @@
1
1
  declare const tabsListVariants: (props?: ({
2
2
  variant?: "default" | "line" | null | undefined;
3
3
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
4
+ /**
5
+ * Horizontal scroller around a tab row. `p-1` leaves room for the active
6
+ * border and focus ring; the height is the `h-11` list plus that padding
7
+ * (`h-13` = 44px + 8px) so the list fits the content box. Putting padding
8
+ * on an overflow box without this height makes `scrollHeight` 4px taller
9
+ * than `clientHeight` (48 vs 44) and a vertical scrollbar appears if
10
+ * `overflow-y` is not hidden. Line variant adds `pb-2` for the underline
11
+ * (`h-14` = 44px + 4px + 8px).
12
+ */
13
+ declare const tabsScrollerVariants: (props?: ({
14
+ variant?: "default" | "line" | null | undefined;
15
+ } & import("class-variance-authority/types").ClassProp) | undefined) => string;
4
16
  declare const tabsTriggerVariants: (props?: import("class-variance-authority/types").ClassProp | undefined) => string;
5
- export { tabsListVariants, tabsTriggerVariants };
17
+ export { tabsListVariants, tabsScrollerVariants, tabsTriggerVariants };
@@ -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",
@@ -11,12 +11,35 @@ const tabsListVariants = cva('group/tabs-list inline-flex w-fit items-stretch ju
11
11
  variant: "default",
12
12
  },
13
13
  });
14
+ /**
15
+ * Horizontal scroller around a tab row. `p-1` leaves room for the active
16
+ * border and focus ring; the height is the `h-11` list plus that padding
17
+ * (`h-13` = 44px + 8px) so the list fits the content box. Putting padding
18
+ * on an overflow box without this height makes `scrollHeight` 4px taller
19
+ * than `clientHeight` (48 vs 44) and a vertical scrollbar appears if
20
+ * `overflow-y` is not hidden. Line variant adds `pb-2` for the underline
21
+ * (`h-14` = 44px + 4px + 8px).
22
+ */
23
+ const tabsScrollerVariants = cva("relative box-border 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", {
24
+ variants: {
25
+ variant: {
26
+ default: 'group-data-[orientation="horizontal"]/tabs:h-13',
27
+ line: 'group-data-[orientation="horizontal"]/tabs:h-14 pb-2',
28
+ },
29
+ },
30
+ defaultVariants: {
31
+ variant: "default",
32
+ },
33
+ });
14
34
  const tabsTriggerVariants = cva([
15
35
  focusRing,
16
36
  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',
37
+ // flex-none + px-4: every trigger and TabsNav link keeps the same
38
+ // horizontal padding. flex-1 used to grow short labels and make the
39
+ // longest (e.g. Apple Reminders) look tighter. No last-child padding.
40
+ '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
41
  '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
42
  selectedState.active,
20
43
  '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',
21
44
  ]);
22
- export { tabsListVariants, tabsTriggerVariants };
45
+ export { tabsListVariants, tabsScrollerVariants, tabsTriggerVariants };
@@ -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,12 @@ 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 counted in the scroller height (so the
19
+ * list cannot overflow on y), an edge fade while it overflows, and the
20
+ * active link scrolled into view on mount. Do not wrap it in
21
+ * `overflow-x-auto`.
16
22
  */
17
23
  declare function TabsNav({ className, children, ...props }: React.ComponentProps<"nav"> & {
18
24
  "aria-label": string;
package/dist/tabs.js CHANGED
@@ -3,13 +3,41 @@ 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 { focusRing } from "./internal/styles.js";
7
+ import { tabsListVariants, tabsScrollerVariants, tabsTriggerVariants, } from "./internal/tabs-styles.js";
8
+ import { useHorizontalOverflow } from "./internal/use-horizontal-overflow.js";
6
9
  import { cn } from "./utils.js";
7
- import { tabsListVariants, tabsTriggerVariants, } from "./internal/tabs-styles.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. Pad for
17
+ * those strokes *inside* the box (`h-13` = `h-11` list + `p-1`), clip the
18
+ * y axis as a backstop, and fade the edges while the row overflows —
19
+ * same cue as `Table`.
20
+ */
21
+ function TabsScroller({ variant = "default", children, }) {
22
+ const ref = React.useRef(null);
23
+ const overflowing = useHorizontalOverflow(ref);
24
+ React.useLayoutEffect(() => {
25
+ const scroller = ref.current;
26
+ if (!scroller)
27
+ return;
28
+ const active = scroller.querySelector('[aria-current="page"], [data-state="active"]');
29
+ active?.scrollIntoView({
30
+ block: "nearest",
31
+ inline: "nearest",
32
+ behavior: "auto",
33
+ });
34
+ }, []);
35
+ return (_jsx("div", { ref: ref, "data-slot": "tabs-scroller", "data-variant": variant, tabIndex: overflowing ? 0 : undefined, className: cn(focusRing, tabsScrollerVariants({ variant })), children: children }));
36
+ }
11
37
  function TabsList({ className, variant = "default", ...props }) {
12
- return (_jsx(TabsPrimitive.List, { "data-slot": "tabs-list", "data-variant": variant, className: cn(tabsListVariants({ variant }), className), ...props }));
38
+ return (_jsx(TabsScroller, { variant: variant ?? "default", children: _jsx(TabsPrimitive.List, { "data-slot": "tabs-list", "data-variant": variant, className: cn(tabsListVariants({ variant }),
39
+ // The scroller owns the muted fill so edge fades show through.
40
+ variant === "default" && "bg-transparent", className), ...props }) }));
13
41
  }
14
42
  function TabsTrigger({ className, ...props }) {
15
43
  return (_jsx(TabsPrimitive.Trigger, { "data-slot": "tabs-trigger", className: cn(tabsTriggerVariants(), className), ...props }));
@@ -24,8 +52,14 @@ function TabsContent({ className, ...props }) {
24
52
  * with `tabsTriggerVariants()` and mark the active one with
25
53
  * `data-state="active"` and `aria-current="page"` (the Next adapter
26
54
  * `TabsNavLink` does this for you).
55
+ *
56
+ * Overflow is handled here: the row scrolls horizontally only, with padding
57
+ * for active borders and focus rings counted in the scroller height (so the
58
+ * list cannot overflow on y), an edge fade while it overflows, and the
59
+ * active link scrolled into view on mount. Do not wrap it in
60
+ * `overflow-x-auto`.
27
61
  */
28
62
  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 }) }));
63
+ 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
64
  }
31
65
  export { Tabs, TabsList, TabsTrigger, TabsContent, TabsNav, tabsListVariants, tabsTriggerVariants, };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atlas-react-kit",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "description": "Shared React components and Tailwind theme for Atlas",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -127,7 +127,7 @@ list; the ones that come up most:
127
127
  Overriding their height, padding, or border makes one field look different
128
128
  from its neighbors.
129
129
  - **Layout overrides**: `CardContent className="px-0"` makes one section flush
130
- without `!important`. Tables already scroll horizontally inside cards, so do
130
+ without `!important`. Tables and tab rows already scroll horizontally, so do
131
131
  not wrap them in another overflow container.
132
132
  - **Headings**: `CardTitle` and `DialogTitle` already apply the uppercase,
133
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
  }