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 +1 -1
- package/DESIGN_RULES.md +7 -3
- package/README.md +2 -2
- package/dist/internal/tabs-styles.js +5 -2
- package/dist/internal/use-horizontal-overflow.d.ts +7 -0
- package/dist/internal/use-horizontal-overflow.js +23 -0
- package/dist/table.js +1 -20
- package/dist/tabs.d.ts +5 -0
- package/dist/tabs.js +35 -3
- package/package.json +1 -1
- package/skills/atlas-react-kit/SKILL.md +1 -1
- package/theme.css +49 -0
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`
|
|
58
|
-
|
|
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.
|
|
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.
|
|
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-
|
|
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
|
-
|
|
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 {
|
|
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 }),
|
|
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
|
@@ -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
|
|
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
|
}
|