@supertype.ai/foundations 0.1.28 → 0.1.30
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/README.md +27 -23
- package/bin/foundations.mjs +5 -2
- package/dist/blocks/accordion.js +2 -1
- package/dist/blocks/badge.d.ts +27 -0
- package/dist/blocks/badge.js +86 -0
- package/dist/blocks/button.d.ts +32 -0
- package/dist/blocks/button.js +127 -0
- package/dist/blocks/callout.d.ts +2 -39
- package/dist/blocks/callout.js +13 -38
- package/dist/blocks/card.d.ts +5 -6
- package/dist/blocks/card.js +28 -12
- package/dist/blocks/focus.d.ts +9 -0
- package/dist/blocks/focus.js +9 -0
- package/dist/blocks/index.d.ts +3 -1
- package/dist/blocks/index.js +3 -1
- package/dist/blocks/interactive-accordion.js +2 -1
- package/dist/blocks/render-as.d.ts +11 -0
- package/dist/blocks/render-as.js +22 -0
- package/dist/blocks/segment.d.ts +22 -14
- package/dist/blocks/segment.js +23 -14
- package/dist/blocks/tabs.d.ts +52 -17
- package/dist/blocks/tabs.js +86 -37
- package/dist/cjs/eslint.js +49 -14
- package/dist/contrast.d.ts +56 -0
- package/dist/contrast.js +144 -12
- package/dist/eslint.d.ts +42 -8
- package/dist/eslint.js +47 -14
- package/dist/essay/essay.js +14 -6
- package/dist/essay/index.d.ts +1 -1
- package/dist/essay/index.js +1 -1
- package/dist/essay/layout.d.ts +37 -1
- package/dist/essay/layout.js +33 -1
- package/dist/essay/rail.d.ts +11 -1
- package/dist/essay/reading.d.ts +1 -1
- package/dist/essay/scroll.js +1 -1
- package/dist/href.d.ts +42 -0
- package/dist/href.js +63 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +9 -0
- package/dist/mdx.d.ts +26 -4
- package/dist/mdx.js +30 -4
- package/dist/tone.d.ts +130 -0
- package/dist/tone.js +140 -0
- package/dist/typography/header.d.ts +32 -2
- package/dist/typography/header.js +10 -5
- package/dist/typography/highlight.d.ts +7 -6
- package/dist/typography/highlight.js +15 -9
- package/dist/typography/paragraph.d.ts +12 -25
- package/dist/typography/paragraph.js +40 -29
- package/llms.txt +99 -22
- package/package.json +2 -1
- package/src/theme.css +143 -46
- package/src/tokens.css +5 -4
- package/src/type.css +1 -1
package/dist/blocks/card.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { cn } from "../cn.js";
|
|
3
|
-
import {
|
|
3
|
+
import { FOCUS_RING } from "./focus.js";
|
|
4
|
+
import { inkOnSurface, toneClass } from "../tone.js";
|
|
5
|
+
import { resolveLink } from "../href.js";
|
|
4
6
|
/** Two columns from `sm` up: a pair reads as a set rather than two panels. */
|
|
5
7
|
export function Cards({ className, children, ...props }) {
|
|
6
8
|
return (_jsx("div", { className: cn("my-6 grid gap-4 sm:grid-cols-2", className), ...props, children: children }));
|
|
@@ -10,9 +12,20 @@ export function Cards({ className, children, ...props }) {
|
|
|
10
12
|
* grid and `overflow-hidden` clips a bleed image cleanly. Padding is vertical
|
|
11
13
|
* only — the horizontal inset belongs to the slots, so bands can run edge to edge.
|
|
12
14
|
*/
|
|
13
|
-
const CARD_CLASS =
|
|
15
|
+
const CARD_CLASS = `flex flex-col gap-4 overflow-hidden rounded-xl bg-card py-4 text-sm text-card-foreground ring-1 ring-border ${inkOnSurface("--card-foreground")} ` +
|
|
14
16
|
"has-[>img:first-child]:pt-0 " +
|
|
15
17
|
"*:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl";
|
|
18
|
+
/**
|
|
19
|
+
* What a card does when it is a link, which is the only time it does anything: two pixels
|
|
20
|
+
* up, a shadow under it, the ring firming from `--border` to a cut of the page's own ink.
|
|
21
|
+
* Following a link is not a colour, so nothing here is one — `--elevation-raised` is the
|
|
22
|
+
* token for a layer leaving the page plane, and that is the whole gesture. The lift is
|
|
23
|
+
* `motion-safe:` and the shadow is not, so reduced motion keeps the affordance.
|
|
24
|
+
*
|
|
25
|
+
* `toneClass` is declared here so the icon below can take `--tone-hue` rather than naming
|
|
26
|
+
* a token, the way every other tinted role in the package reads it.
|
|
27
|
+
*/
|
|
28
|
+
const CARD_LINK_CLASS = cn(toneClass("primary"), FOCUS_RING, "group/card no-underline transition duration-200 ease-out", "hover:shadow-raised hover:ring-foreground/15 motion-safe:hover:-translate-y-0.5");
|
|
16
29
|
export function CardHeader({ className, ...props }) {
|
|
17
30
|
return (_jsx("div", { "data-slot": "card-header", className: cn("grid auto-rows-min items-start gap-1 px-4 [.border-b]:pb-4", className), ...props }));
|
|
18
31
|
}
|
|
@@ -36,21 +49,24 @@ export function CardContent({ className, ...props }) {
|
|
|
36
49
|
/**
|
|
37
50
|
* Takes either shape: `title`/`href` fills the header, or compose the slots
|
|
38
51
|
* directly. Unrecognised props pass through — MDX authors reach for the whole
|
|
39
|
-
* HTML surface.
|
|
40
|
-
*
|
|
52
|
+
* HTML surface. Where the href goes is ../href.ts's call, the same one Button
|
|
53
|
+
* and TypographyLink make.
|
|
41
54
|
*/
|
|
42
|
-
export function Card({ href, className, external, title, description, icon, children, ...rest }) {
|
|
43
|
-
const header = title || description || icon ? (_jsxs(CardHeader, { children: [icon
|
|
55
|
+
export function Card({ href, className, external, newTab, title, description, icon, children, ...rest }) {
|
|
56
|
+
const header = title || description || icon ? (_jsxs(CardHeader, { children: [icon || title ? (
|
|
57
|
+
// The icon sits on the title's line and is its mark; stacked, it read as a
|
|
58
|
+
// decoration the title happened to follow. `gap-2` is a gap between two
|
|
59
|
+
// objects, not the header's `gap-1` between two lines.
|
|
60
|
+
_jsxs("div", { className: "flex items-center gap-2", children: [icon ? (
|
|
61
|
+
// Sized here, not at the call site, so two cards cannot disagree about how
|
|
62
|
+
// big an icon is. On a link card it takes the tone as the card lifts.
|
|
63
|
+
_jsx("span", { className: "shrink-0 text-muted-foreground transition-colors group-hover/card:text-(color:--tone-hue) [&_svg]:size-4 [&_svg]:shrink-0", children: icon })) : null, title ? _jsx(CardTitle, { children: title }) : null] })) : null, description ? _jsx(CardDescription, { children: description }) : null] })) : null;
|
|
44
64
|
// Bare children compose; children under a shorthand header are body copy.
|
|
45
65
|
const body = header ? (_jsxs(_Fragment, { children: [header, children ? _jsx(CardContent, { children: children }) : null] })) : (children);
|
|
46
66
|
const shared = { "data-slot": "card" };
|
|
47
67
|
if (!href) {
|
|
48
68
|
return (_jsx("div", { className: cn(CARD_CLASS, className), ...shared, ...rest, children: body }));
|
|
49
69
|
}
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
if (leavesApp) {
|
|
53
|
-
return (_jsx("a", { href: href, className: classes, target: "_blank", rel: "noopener noreferrer", ...shared, ...rest, children: body }));
|
|
54
|
-
}
|
|
55
|
-
return (_jsx(Link, { href: href, className: classes, ...shared, ...rest, children: body }));
|
|
70
|
+
const { Component, props: link } = resolveLink(href, { external, newTab });
|
|
71
|
+
return (_jsx(Component, { className: cn(CARD_CLASS, CARD_LINK_CLASS, className), ...link, ...shared, ...rest, children: body }));
|
|
56
72
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The focus ring, stated once.
|
|
3
|
+
*
|
|
4
|
+
* It was five copies — Button, Badge, Accordion, `SEGMENT.item`, and Card, which
|
|
5
|
+
* added the fifth while its own comment observed that the other four existed.
|
|
6
|
+
* Retuning the width or the ink was a find-and-replace across five files with no
|
|
7
|
+
* compiler help if one was missed.
|
|
8
|
+
*/
|
|
9
|
+
export declare const FOCUS_RING = "outline-none focus-visible:ring-3 focus-visible:ring-ring/50";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The focus ring, stated once.
|
|
3
|
+
*
|
|
4
|
+
* It was five copies — Button, Badge, Accordion, `SEGMENT.item`, and Card, which
|
|
5
|
+
* added the fifth while its own comment observed that the other four existed.
|
|
6
|
+
* Retuning the width or the ink was a find-and-replace across five files with no
|
|
7
|
+
* compiler help if one was missed.
|
|
8
|
+
*/
|
|
9
|
+
export const FOCUS_RING = "outline-none focus-visible:ring-3 focus-visible:ring-ring/50";
|
package/dist/blocks/index.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ export { Cards, Card, CardHeader, CardTitle, CardDescription, CardContent, } fro
|
|
|
2
2
|
export { Disclosure, DisclosureGroup } from "./accordion.js";
|
|
3
3
|
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent, } from "./interactive-accordion.js";
|
|
4
4
|
export { Callout } from "./callout.js";
|
|
5
|
-
export {
|
|
5
|
+
export { Button, buttonVariants, type ButtonLook } from "./button.js";
|
|
6
|
+
export { Badge, badgeVariants, type BadgeLook } from "./badge.js";
|
|
7
|
+
export { Tabs, TabsList, TabsTrigger, TabsContent, TabGroup, type TabItem, } from "./tabs.js";
|
|
6
8
|
export { SEGMENT } from "./segment.js";
|
|
7
9
|
export { Steps, Step } from "./steps.js";
|
package/dist/blocks/index.js
CHANGED
|
@@ -2,6 +2,8 @@ export { Cards, Card, CardHeader, CardTitle, CardDescription, CardContent, } fro
|
|
|
2
2
|
export { Disclosure, DisclosureGroup } from "./accordion.js";
|
|
3
3
|
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent, } from "./interactive-accordion.js";
|
|
4
4
|
export { Callout } from "./callout.js";
|
|
5
|
-
export {
|
|
5
|
+
export { Button, buttonVariants } from "./button.js";
|
|
6
|
+
export { Badge, badgeVariants } from "./badge.js";
|
|
7
|
+
export { Tabs, TabsList, TabsTrigger, TabsContent, TabGroup, } from "./tabs.js";
|
|
6
8
|
export { SEGMENT } from "./segment.js";
|
|
7
9
|
export { Steps, Step } from "./steps.js";
|
|
@@ -8,6 +8,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
8
8
|
*/
|
|
9
9
|
import { Accordion as AccordionPrimitive } from "@base-ui/react/accordion";
|
|
10
10
|
import { cn } from "../cn.js";
|
|
11
|
+
import { FOCUS_RING } from "./focus.js";
|
|
11
12
|
function Accordion({ className, ...props }) {
|
|
12
13
|
return (_jsx(AccordionPrimitive.Root, { "data-slot": "accordion", className: cn("flex w-full flex-col", className), ...props }));
|
|
13
14
|
}
|
|
@@ -15,7 +16,7 @@ function AccordionItem({ className, ...props }) {
|
|
|
15
16
|
return (_jsx(AccordionPrimitive.Item, { "data-slot": "accordion-item", className: cn("not-last:border-b", className), ...props }));
|
|
16
17
|
}
|
|
17
18
|
function AccordionTrigger({ className, children, ...props }) {
|
|
18
|
-
return (_jsx(AccordionPrimitive.Header, { className: "flex", children: _jsxs(AccordionPrimitive.Trigger, { "data-slot": "accordion-trigger", className: cn("group/accordion-trigger relative flex flex-1 items-start justify-between rounded-lg border border-transparent py-2.5 text-left text-sm font-medium transition
|
|
19
|
+
return (_jsx(AccordionPrimitive.Header, { className: "flex", children: _jsxs(AccordionPrimitive.Trigger, { "data-slot": "accordion-trigger", className: cn(FOCUS_RING, "group/accordion-trigger relative flex flex-1 items-start justify-between rounded-lg border border-transparent py-2.5 text-left text-sm font-medium transition hover:underline focus-visible:border-ring focus-visible:after:border-ring aria-disabled:pointer-events-none aria-disabled:opacity-50 **:data-[slot=accordion-trigger-icon]:ml-auto **:data-[slot=accordion-trigger-icon]:size-4 **:data-[slot=accordion-trigger-icon]:text-muted-foreground", className), ...props, children: [children, _jsx(Chevron, { className: "group-aria-expanded/accordion-trigger:hidden" }), _jsx(Chevron, { up: true, className: "hidden group-aria-expanded/accordion-trigger:inline" })] }) }));
|
|
19
20
|
}
|
|
20
21
|
function AccordionContent({ className, children, ...props }) {
|
|
21
22
|
return (_jsx(AccordionPrimitive.Panel, { "data-slot": "accordion-content", className: "overflow-hidden text-sm data-open:animate-accordion-down data-closed:animate-accordion-up", ...props, children: _jsx("div", { className: cn("h-(--accordion-panel-height) pt-0 pb-2.5 data-ending-style:h-0 data-starting-style:h-0 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground [&_p:not(:last-child)]:mb-4", className), children: children }) }));
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type ReactElement } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* `render={<a href="…" />}` — the component's classes and data marks put onto an element
|
|
4
|
+
* the caller supplies, so a badge or a button can BE a link rather than wrap one. A
|
|
5
|
+
* screen reader announces the element, and the element is the anchor.
|
|
6
|
+
*
|
|
7
|
+
* Shared because Button and Badge both do it, identically, for the same reason; both
|
|
8
|
+
* files' comments used to say so while carrying their own copy. Returns `null` when
|
|
9
|
+
* `render` is not an element, which is the caller's signal to render its own tag.
|
|
10
|
+
*/
|
|
11
|
+
export declare function renderAs(render: unknown, classes: string, props: Record<string, unknown>): ReactElement | null;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { cloneElement, isValidElement } from "react";
|
|
2
|
+
import { cn } from "../cn.js";
|
|
3
|
+
/**
|
|
4
|
+
* `render={<a href="…" />}` — the component's classes and data marks put onto an element
|
|
5
|
+
* the caller supplies, so a badge or a button can BE a link rather than wrap one. A
|
|
6
|
+
* screen reader announces the element, and the element is the anchor.
|
|
7
|
+
*
|
|
8
|
+
* Shared because Button and Badge both do it, identically, for the same reason; both
|
|
9
|
+
* files' comments used to say so while carrying their own copy. Returns `null` when
|
|
10
|
+
* `render` is not an element, which is the caller's signal to render its own tag.
|
|
11
|
+
*/
|
|
12
|
+
export function renderAs(render, classes, props) {
|
|
13
|
+
// `unknown`, because Base UI's `render` is an element OR a render function; only the
|
|
14
|
+
// element half is ours to clone, and `isValidElement` is the narrowing.
|
|
15
|
+
if (!isValidElement(render))
|
|
16
|
+
return null;
|
|
17
|
+
const { className } = render.props;
|
|
18
|
+
return cloneElement(render, {
|
|
19
|
+
...props,
|
|
20
|
+
className: cn(classes, className),
|
|
21
|
+
});
|
|
22
|
+
}
|
package/dist/blocks/segment.d.ts
CHANGED
|
@@ -9,29 +9,37 @@
|
|
|
9
9
|
* class strings, which is exactly the kind of duplication that drifts the first time
|
|
10
10
|
* someone retunes the palette.
|
|
11
11
|
*
|
|
12
|
-
* Some entries appear twice, once bare and once `data-active:`-prefixed. That is not
|
|
13
|
-
* laziness: Tailwind generates a utility only if it appears literally in scanned source, so
|
|
14
|
-
* prefixing at runtime would compile to classes that never got built. Writing both forms
|
|
15
|
-
* side by side is the honest version, and it keeps the pair impossible to change by half.
|
|
16
|
-
*
|
|
17
12
|
* The active segment is deliberately flat — a card surface and a hairline, no shadow.
|
|
18
13
|
* Elevation in this system means "this layer left the page plane" (see --elevation-* in
|
|
19
14
|
* theme.css), and a segment sitting inside its own track has not.
|
|
15
|
+
*
|
|
16
|
+
* The other half of that: the segment stays on the page plane because the RAIL drops below
|
|
17
|
+
* it. The well is the shadow and the hairline, not a fill — the track is `bg-background`
|
|
18
|
+
* pressed in. A `--muted` rail cannot work, because `--muted` sits below `--card` in light
|
|
19
|
+
* and above it in dark, so a muted fill reads correct in one theme and inverted in the
|
|
20
|
+
* other, and `dark:` is what the package's own ESLint rule exists to stop.
|
|
20
21
|
*/
|
|
21
22
|
export declare const SEGMENT: {
|
|
22
|
-
/**
|
|
23
|
-
|
|
23
|
+
/**
|
|
24
|
+
* The rail a set of segments sits in. Surface only; each component owns its layout.
|
|
25
|
+
*
|
|
26
|
+
* `rounded-md` over `activeSurface`'s `rounded-sm` is the 2px of `p-0.5`: concentric
|
|
27
|
+
* radii, so the segment's corner runs parallel to the rail's rather than across it.
|
|
28
|
+
*/
|
|
29
|
+
readonly track: "rounded-md border border-border bg-background p-0.5 shadow-recessed";
|
|
24
30
|
/** Affordances every segment shares, whatever its shape or engine. */
|
|
25
|
-
readonly item: "relative inline-flex items-center gap-1.5 font-medium outline-none
|
|
31
|
+
readonly item: "relative inline-flex items-center gap-1.5 font-medium transition-colors outline-none focus-visible:ring-3 focus-visible:ring-ring/50";
|
|
26
32
|
/** Selected: the ink lifts to full strength. */
|
|
27
33
|
readonly active: "text-foreground";
|
|
28
34
|
/**
|
|
29
35
|
* Unselected: quiet, but lighting its own surface on hover so the whole strip reads as
|
|
30
|
-
* reachable rather than only the segment already chosen.
|
|
36
|
+
* reachable rather than only the segment already chosen. It moves toward `activeSurface`
|
|
37
|
+
* and not toward the rail, so a hover previews being picked.
|
|
38
|
+
*/
|
|
39
|
+
readonly idle: "text-muted-foreground hover:bg-card/60 hover:text-foreground";
|
|
40
|
+
/**
|
|
41
|
+
* The flat surface marking the selection. One string, worn by both engines: the marketing
|
|
42
|
+
* picker slides it with `motion`, `Tabs` hands it to the element Base UI positions.
|
|
31
43
|
*/
|
|
32
|
-
readonly
|
|
33
|
-
/** The flat surface marking the selection. */
|
|
34
|
-
readonly activeSurface: "bg-card ring-1 ring-border";
|
|
35
|
-
/** `activeSurface`, for an engine that marks its own trigger with `data-active`. */
|
|
36
|
-
readonly dataActiveSurface: "data-active:bg-card data-active:text-foreground data-active:ring-1 data-active:ring-border";
|
|
44
|
+
readonly activeSurface: "rounded-sm bg-card ring-1 ring-border";
|
|
37
45
|
};
|
package/dist/blocks/segment.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { FOCUS_RING } from "./focus.js";
|
|
1
2
|
/**
|
|
2
3
|
* The segmented picker, as one set of surfaces.
|
|
3
4
|
*
|
|
@@ -9,29 +10,37 @@
|
|
|
9
10
|
* class strings, which is exactly the kind of duplication that drifts the first time
|
|
10
11
|
* someone retunes the palette.
|
|
11
12
|
*
|
|
12
|
-
* Some entries appear twice, once bare and once `data-active:`-prefixed. That is not
|
|
13
|
-
* laziness: Tailwind generates a utility only if it appears literally in scanned source, so
|
|
14
|
-
* prefixing at runtime would compile to classes that never got built. Writing both forms
|
|
15
|
-
* side by side is the honest version, and it keeps the pair impossible to change by half.
|
|
16
|
-
*
|
|
17
13
|
* The active segment is deliberately flat — a card surface and a hairline, no shadow.
|
|
18
14
|
* Elevation in this system means "this layer left the page plane" (see --elevation-* in
|
|
19
15
|
* theme.css), and a segment sitting inside its own track has not.
|
|
16
|
+
*
|
|
17
|
+
* The other half of that: the segment stays on the page plane because the RAIL drops below
|
|
18
|
+
* it. The well is the shadow and the hairline, not a fill — the track is `bg-background`
|
|
19
|
+
* pressed in. A `--muted` rail cannot work, because `--muted` sits below `--card` in light
|
|
20
|
+
* and above it in dark, so a muted fill reads correct in one theme and inverted in the
|
|
21
|
+
* other, and `dark:` is what the package's own ESLint rule exists to stop.
|
|
20
22
|
*/
|
|
21
23
|
export const SEGMENT = {
|
|
22
|
-
/**
|
|
23
|
-
|
|
24
|
+
/**
|
|
25
|
+
* The rail a set of segments sits in. Surface only; each component owns its layout.
|
|
26
|
+
*
|
|
27
|
+
* `rounded-md` over `activeSurface`'s `rounded-sm` is the 2px of `p-0.5`: concentric
|
|
28
|
+
* radii, so the segment's corner runs parallel to the rail's rather than across it.
|
|
29
|
+
*/
|
|
30
|
+
track: "rounded-md border border-border bg-background p-0.5 shadow-recessed",
|
|
24
31
|
/** Affordances every segment shares, whatever its shape or engine. */
|
|
25
|
-
item:
|
|
32
|
+
item: `relative inline-flex items-center gap-1.5 font-medium transition-colors ${FOCUS_RING}`,
|
|
26
33
|
/** Selected: the ink lifts to full strength. */
|
|
27
34
|
active: "text-foreground",
|
|
28
35
|
/**
|
|
29
36
|
* Unselected: quiet, but lighting its own surface on hover so the whole strip reads as
|
|
30
|
-
* reachable rather than only the segment already chosen.
|
|
37
|
+
* reachable rather than only the segment already chosen. It moves toward `activeSurface`
|
|
38
|
+
* and not toward the rail, so a hover previews being picked.
|
|
39
|
+
*/
|
|
40
|
+
idle: "text-muted-foreground hover:bg-card/60 hover:text-foreground",
|
|
41
|
+
/**
|
|
42
|
+
* The flat surface marking the selection. One string, worn by both engines: the marketing
|
|
43
|
+
* picker slides it with `motion`, `Tabs` hands it to the element Base UI positions.
|
|
31
44
|
*/
|
|
32
|
-
|
|
33
|
-
/** The flat surface marking the selection. */
|
|
34
|
-
activeSurface: "bg-card ring-1 ring-border",
|
|
35
|
-
/** `activeSurface`, for an engine that marks its own trigger with `data-active`. */
|
|
36
|
-
dataActiveSurface: "data-active:bg-card data-active:text-foreground data-active:ring-1 data-active:ring-border",
|
|
45
|
+
activeSurface: "rounded-sm bg-card ring-1 ring-border",
|
|
37
46
|
};
|
package/dist/blocks/tabs.d.ts
CHANGED
|
@@ -1,32 +1,67 @@
|
|
|
1
1
|
import { Tabs as TabsPrimitive } from "@base-ui/react/tabs";
|
|
2
2
|
import type { ReactNode } from "react";
|
|
3
3
|
import { type VariantProps } from "class-variance-authority";
|
|
4
|
+
import { type Tone } from "../tone.js";
|
|
4
5
|
declare function Tabs({ className, orientation, ...props }: TabsPrimitive.Root.Props): import("react").JSX.Element;
|
|
6
|
+
/**
|
|
7
|
+
* Surfaces come from SEGMENT, so this and marketing/segmented-control cannot drift: a
|
|
8
|
+
* reader who meets the picker on a docs page and again on the usage dashboard should not
|
|
9
|
+
* have to learn it twice. Layout stays local, since only this one has orientation to serve.
|
|
10
|
+
*
|
|
11
|
+
* Each variant states its own box, rather than sharing a base tuned for the boxed track
|
|
12
|
+
* that `line` then had to undo at the call site.
|
|
13
|
+
*/
|
|
5
14
|
declare const tabsListVariants: (props?: ({
|
|
6
15
|
variant?: "line" | "default" | null | undefined;
|
|
7
16
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
8
|
-
|
|
17
|
+
/**
|
|
18
|
+
* `tone` inks the marker, and only the marker. On `line` that is the underline and the
|
|
19
|
+
* active tab's icon; the boxed track's marker is a card surface and a hairline, which
|
|
20
|
+
* SEGMENT keeps deliberately flat, so a tone there would be a colour with nothing to
|
|
21
|
+
* paint. The label stays `--foreground` in both: it is read, not signalled.
|
|
22
|
+
*/
|
|
23
|
+
declare function TabsList({ className, variant, tone, children, ...props }: TabsPrimitive.List.Props & VariantProps<typeof tabsListVariants> & {
|
|
24
|
+
tone?: Tone;
|
|
25
|
+
}): import("react").JSX.Element;
|
|
9
26
|
declare function TabsTrigger({ className, ...props }: TabsPrimitive.Tab.Props): import("react").JSX.Element;
|
|
10
27
|
declare function TabsContent({ className, ...props }: TabsPrimitive.Panel.Props): import("react").JSX.Element;
|
|
28
|
+
/** One tab, whole: what it is called, what marks it, and what it shows. */
|
|
29
|
+
export type TabItem = {
|
|
30
|
+
/** Stable across a relabel — it is what `defaultValue` and `onValueChange` speak. */
|
|
31
|
+
value: string;
|
|
32
|
+
label: ReactNode;
|
|
33
|
+
/**
|
|
34
|
+
* An element — `<Icons.Mic />`, `<PriceChip />` — sized and inked by the trigger.
|
|
35
|
+
*
|
|
36
|
+
* An element and not a component, which this briefly also took. `TabGroup` is a client
|
|
37
|
+
* component, so a component reference handed to it from a server page is a function
|
|
38
|
+
* crossing the RSC boundary, which React refuses at render; an element is already
|
|
39
|
+
* rendered and crosses fine. One accepted shape also spares the slot a branch, and
|
|
40
|
+
* matches what `Card`'s `icon` has always taken.
|
|
41
|
+
*/
|
|
42
|
+
icon?: ReactNode;
|
|
43
|
+
content: ReactNode;
|
|
44
|
+
};
|
|
11
45
|
/**
|
|
12
|
-
* The declarative shorthand:
|
|
13
|
-
* `
|
|
14
|
-
* the
|
|
46
|
+
* The declarative shorthand: the tabs as data. `TabGroup` is to `Tabs` what
|
|
47
|
+
* `DisclosureGroup` is to `Accordion`: the shape to reach for. An app that rebuilds it
|
|
48
|
+
* over the primitives ends up re-adding the icon, the change handler and the stable value
|
|
49
|
+
* by hand.
|
|
15
50
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* below the adapter is the same component the product surfaces use, so a tab
|
|
20
|
-
* strip in the docs and one on a dashboard behave identically.
|
|
51
|
+
* Everything below the adapter is the same component the product surfaces use, so a tab
|
|
52
|
+
* strip in the docs and one on a dashboard behave identically. The positional
|
|
53
|
+
* `items`-plus-children shape lives in the MDX map, the only thing that speaks it.
|
|
21
54
|
*/
|
|
22
|
-
export declare function TabGroup({
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
/** `value` names the panel at the call site; it is not used for matching. */
|
|
28
|
-
export declare function Tab({ children }: {
|
|
55
|
+
export declare function TabGroup({ tabs, defaultValue, value, onValueChange, variant, tone, iconPosition, className, }: {
|
|
56
|
+
tabs: readonly TabItem[];
|
|
57
|
+
/** Defaults to the first tab, since a picker with nothing picked is not a state. */
|
|
58
|
+
defaultValue?: string;
|
|
59
|
+
/** Pass with `onValueChange` to drive it from outside. */
|
|
29
60
|
value?: string;
|
|
30
|
-
|
|
61
|
+
onValueChange?: (value: string) => void;
|
|
62
|
+
variant?: VariantProps<typeof tabsListVariants>["variant"];
|
|
63
|
+
tone?: Tone;
|
|
64
|
+
iconPosition?: "inline-start" | "inline-end";
|
|
65
|
+
className?: string;
|
|
31
66
|
}): import("react").JSX.Element;
|
|
32
67
|
export { Tabs, TabsList, TabsTrigger, TabsContent };
|
package/dist/blocks/tabs.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { Tabs as TabsPrimitive } from "@base-ui/react/tabs";
|
|
4
4
|
import { cva } from "class-variance-authority";
|
|
5
5
|
import { cn } from "../cn.js";
|
|
6
|
+
import { toneClass } from "../tone.js";
|
|
6
7
|
import { SEGMENT } from "./segment.js";
|
|
7
8
|
function Tabs({ className, orientation = "horizontal", ...props }) {
|
|
8
9
|
return (_jsx(TabsPrimitive.Root, { "data-slot": "tabs", "data-orientation": orientation, className: cn(
|
|
@@ -12,58 +13,106 @@ function Tabs({ className, orientation = "horizontal", ...props }) {
|
|
|
12
13
|
// flex container and laid its panels out beside the tab strip.
|
|
13
14
|
"group/tabs flex gap-2 data-[orientation=horizontal]:flex-col", className), ...props }));
|
|
14
15
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Surfaces come from SEGMENT, so this and marketing/segmented-control cannot drift: a
|
|
18
|
+
* reader who meets the picker on a docs page and again on the usage dashboard should not
|
|
19
|
+
* have to learn it twice. Layout stays local, since only this one has orientation to serve.
|
|
20
|
+
*
|
|
21
|
+
* Each variant states its own box, rather than sharing a base tuned for the boxed track
|
|
22
|
+
* that `line` then had to undo at the call site.
|
|
23
|
+
*/
|
|
24
|
+
const tabsListVariants = cva("group/tabs-list inline-flex w-fit items-center text-muted-foreground", {
|
|
19
25
|
variants: {
|
|
20
26
|
variant: {
|
|
21
|
-
|
|
22
|
-
|
|
27
|
+
/** The boxed track: one fixed-height rail, segments splitting it evenly. */
|
|
28
|
+
default: cn(SEGMENT.track, "h-8 justify-center"),
|
|
29
|
+
/**
|
|
30
|
+
* A strip of labels over a rule. Free to wrap, so no fixed height — and `pb-2` is
|
|
31
|
+
* the marker's own room (6px offset plus its 2px), so the list's box contains
|
|
32
|
+
* everything the list draws and the gaps below measure from the right edge. The
|
|
33
|
+
* row gap clears the same 8px, or a wrapped row wears the rule above it.
|
|
34
|
+
*/
|
|
35
|
+
line: "flex-wrap justify-start gap-x-1 gap-y-3 rounded-none border-0 bg-transparent pb-2",
|
|
23
36
|
},
|
|
24
37
|
},
|
|
25
38
|
defaultVariants: {
|
|
26
39
|
variant: "default",
|
|
27
40
|
},
|
|
28
41
|
});
|
|
29
|
-
|
|
30
|
-
|
|
42
|
+
/**
|
|
43
|
+
* The marker: one element, positioned by Base UI from `--active-tab-left/top/width/height`
|
|
44
|
+
* on the list, dressed by the variant it is in. It outlives the selection, so it carries
|
|
45
|
+
* the answer from the old tab to the new instead of being destroyed and rebuilt.
|
|
46
|
+
*
|
|
47
|
+
* `data-activation-direction` is `none` before anything is picked, and that is the one case
|
|
48
|
+
* that must not animate — without the guard every strip on the page slides in from its left
|
|
49
|
+
* edge on hydration.
|
|
50
|
+
*/
|
|
51
|
+
const tabsIndicatorVariants = cva("pointer-events-none absolute left-0 top-0 w-(--active-tab-width) transition-[translate,width] duration-200 ease-out data-[activation-direction=none]:transition-none motion-reduce:transition-none", {
|
|
52
|
+
variants: {
|
|
53
|
+
variant: {
|
|
54
|
+
/** The pill, on the page plane. The rail below it is what carries the depth. */
|
|
55
|
+
default: cn(SEGMENT.activeSurface, "h-(--active-tab-height) translate-x-(--active-tab-left) translate-y-(--active-tab-top)"),
|
|
56
|
+
/**
|
|
57
|
+
* A rule under the label, clear of the descenders, in the list's tone. The 6px
|
|
58
|
+
* offset plus its own 2px is the `pb-2` the line list reserves — the two are one
|
|
59
|
+
* measurement, and changing either alone puts the rule back outside its box.
|
|
60
|
+
*/
|
|
61
|
+
line: "h-0.5 translate-x-(--active-tab-left) translate-y-[calc(var(--active-tab-top)+var(--active-tab-height)+6px)] rounded-full bg-(--tone-hue)",
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
defaultVariants: { variant: "default" },
|
|
65
|
+
});
|
|
66
|
+
/**
|
|
67
|
+
* `tone` inks the marker, and only the marker. On `line` that is the underline and the
|
|
68
|
+
* active tab's icon; the boxed track's marker is a card surface and a hairline, which
|
|
69
|
+
* SEGMENT keeps deliberately flat, so a tone there would be a colour with nothing to
|
|
70
|
+
* paint. The label stays `--foreground` in both: it is read, not signalled.
|
|
71
|
+
*/
|
|
72
|
+
function TabsList({ className, variant = "default", tone = "primary", children, ...props }) {
|
|
73
|
+
return (_jsxs(TabsPrimitive.List, { "data-slot": "tabs-list", "data-variant": variant, className: cn("relative", toneClass(tone), tabsListVariants({ variant }), className), ...props, children: [_jsx(TabsPrimitive.Indicator, { renderBeforeHydration: true, className: tabsIndicatorVariants({ variant }) }), children] }));
|
|
31
74
|
}
|
|
32
75
|
function TabsTrigger({ className, ...props }) {
|
|
33
|
-
return (_jsx(TabsPrimitive.Tab, { "data-slot": "tabs-trigger", className: cn(SEGMENT.item,
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
"
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
// The
|
|
42
|
-
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
"
|
|
76
|
+
return (_jsx(TabsPrimitive.Tab, { "data-slot": "tabs-trigger", className: cn(SEGMENT.item,
|
|
77
|
+
// Ink only — the surface and the underline belong to the indicator. The radius is
|
|
78
|
+
// for the hover wash, and matches the marker that wash previews.
|
|
79
|
+
"rounded-sm text-muted-foreground hover:text-foreground data-active:text-foreground", "px-1.5 py-0.5 text-sm whitespace-nowrap disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
80
|
+
// An icon sits inside the label's gap, so the padding on that side comes off.
|
|
81
|
+
// `TabGroup` writes the `data-icon` these two read.
|
|
82
|
+
"has-data-[icon=inline-start]:pl-1 has-data-[icon=inline-end]:pr-1",
|
|
83
|
+
// Filling the track is the boxed variant's business; a line tab is as wide as its
|
|
84
|
+
// label. The variant is read off the list's `data-variant` rather than taken as a
|
|
85
|
+
// prop, so a caller states it once on `TabsList` and not on every trigger.
|
|
86
|
+
// Selectors are spelled out, never interpolated: Tailwind's scanner cannot see an
|
|
87
|
+
// interpolated class and would compile nothing.
|
|
88
|
+
"group-data-[variant=default]/tabs-list:h-full group-data-[variant=default]/tabs-list:flex-1 group-data-[variant=default]/tabs-list:justify-center",
|
|
89
|
+
// Hover moves toward the marker's surface, so it previews the selection.
|
|
90
|
+
"group-data-[variant=default]/tabs-list:not-data-active:hover:bg-card/60",
|
|
91
|
+
// The active icon takes the ink the marker is drawn in.
|
|
92
|
+
"group-data-[variant=line]/tabs-list:data-active:[&_[data-icon]]:text-(color:--tone-hue)", className), ...props }));
|
|
46
93
|
}
|
|
47
94
|
function TabsContent({ className, ...props }) {
|
|
48
95
|
return (_jsx(TabsPrimitive.Panel, { "data-slot": "tabs-content", className: cn("flex-1 text-sm outline-none", className), ...props }));
|
|
49
96
|
}
|
|
97
|
+
/** `data-icon` is the hook the trigger's padding and tone selectors read. */
|
|
98
|
+
function TabIconSlot({ icon, position, }) {
|
|
99
|
+
return (_jsx("span", { "data-icon": position, className: "flex items-center transition-colors [&_svg]:size-4 [&_svg]:shrink-0", children: icon }));
|
|
100
|
+
}
|
|
50
101
|
/**
|
|
51
|
-
* The declarative shorthand:
|
|
52
|
-
* `
|
|
53
|
-
* the
|
|
102
|
+
* The declarative shorthand: the tabs as data. `TabGroup` is to `Tabs` what
|
|
103
|
+
* `DisclosureGroup` is to `Accordion`: the shape to reach for. An app that rebuilds it
|
|
104
|
+
* over the primitives ends up re-adding the icon, the change handler and the stable value
|
|
105
|
+
* by hand.
|
|
54
106
|
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
* below the adapter is the same component the product surfaces use, so a tab
|
|
59
|
-
* strip in the docs and one on a dashboard behave identically.
|
|
107
|
+
* Everything below the adapter is the same component the product surfaces use, so a tab
|
|
108
|
+
* strip in the docs and one on a dashboard behave identically. The positional
|
|
109
|
+
* `items`-plus-children shape lives in the MDX map, the only thing that speaks it.
|
|
60
110
|
*/
|
|
61
|
-
export function TabGroup({
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
return _jsx(_Fragment, { children: children });
|
|
111
|
+
export function TabGroup({ tabs, defaultValue, value, onValueChange, variant, tone, iconPosition = "inline-start", className, }) {
|
|
112
|
+
return (
|
|
113
|
+
// The handler is adapted rather than wrapped when absent: an arrow declared
|
|
114
|
+
// unconditionally is a function crossing the server boundary on every page that
|
|
115
|
+
// renders tabs without one.
|
|
116
|
+
_jsxs(Tabs, { defaultValue: defaultValue ?? tabs[0]?.value, value: value, onValueChange: onValueChange && ((next) => onValueChange(String(next))), className: cn("my-6", className), children: [_jsx(TabsList, { variant: variant, tone: tone, children: tabs.map(({ value: tabValue, label, icon }) => (_jsxs(TabsTrigger, { value: tabValue, children: [icon && iconPosition === "inline-start" && (_jsx(TabIconSlot, { icon: icon, position: "inline-start" })), label, icon && iconPosition === "inline-end" && (_jsx(TabIconSlot, { icon: icon, position: "inline-end" }))] }, tabValue))) }), tabs.map(({ value: tabValue, content }) => (_jsx(TabsContent, { value: tabValue, className: "pt-2 text-muted-foreground", children: content }, tabValue)))] }));
|
|
68
117
|
}
|
|
69
118
|
export { Tabs, TabsList, TabsTrigger, TabsContent };
|