atlas-react-kit 0.8.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
@@ -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, 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,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.8.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.8.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, };
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.1",
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
  }