@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/README.md
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
[](https://www.npmjs.com/package/@supertype.ai/foundations)
|
|
5
5
|
[](LICENSE)
|
|
6
6
|
|
|
7
|
-
[The foundations philosophy](https://supertypeai.github.io/foundations/philosophy/)
|
|
8
|
-
|
|
7
|
+
[The foundations philosophy](https://supertypeai.github.io/foundations/philosophy/) explains the reasoning in more detail, but the short version is simple: this is a reusable design system for typography primitives, content blocks, the long-form essay shell, token and theme CSS, and the build-time tooling that keeps the baseline consistent. That includes SEO, OG cards, lint rules, and contrast checks.
|
|
8
|
+
|
|
9
|
+
It is used in Supertype projects like [Viably work operating system](https://viably.app) and [supertype.ai](https://supertype.ai), and it is MIT-licensed for any Next.js 15+ app built on Tailwind and Shadcn.
|
|
9
10
|
|
|
10
11
|
```sh
|
|
11
12
|
yarn add @supertype.ai/foundations
|
|
@@ -31,11 +32,11 @@ yarn example:install # once, to install Next, the peers and the package
|
|
|
31
32
|
yarn example # then open http://localhost:3000
|
|
32
33
|
```
|
|
33
34
|
|
|
34
|
-
[`examples/site`](examples/site) renders every component
|
|
35
|
+
[`examples/site`](examples/site) renders every component with the code next to it, and includes whole-page [recipes](examples/site/app/_recipes) you can copy into your project. It also includes the `dark` and `.editorial` switches.
|
|
35
36
|
|
|
36
37
|
## Initialization and Diagnostics
|
|
37
38
|
|
|
38
|
-
This package
|
|
39
|
+
This package includes a CLI that writes the CSS for you and checks the rest:
|
|
39
40
|
|
|
40
41
|
```sh
|
|
41
42
|
npx @supertype.ai/foundations init # edits your CSS entry, prints the rest
|
|
@@ -43,7 +44,7 @@ npx @supertype.ai/foundations doctor # checks this app against everything bel
|
|
|
43
44
|
```
|
|
44
45
|
|
|
45
46
|
`init` edits one file: the CSS entry that imports Tailwind. It adds the imports
|
|
46
|
-
you are missing and reorders anything that is out of place. Run it with `--dry-run` first to
|
|
47
|
+
you are missing and reorders anything that is out of place. Run it with `--dry-run` first to preview the patch. Everything else it prints for you to paste is the font binding and the `llms.txt` lines for a coding agent.
|
|
47
48
|
|
|
48
49
|
The steps performed by `init` are written out below anyway. See [the CLI](docs/cli.md) for the full list of checks and details.
|
|
49
50
|
|
|
@@ -59,13 +60,13 @@ Peers are React 19+, Next 15+, `next-view-transitions` 0.3+ and `@base-ui/react`
|
|
|
59
60
|
<details>
|
|
60
61
|
<summary>Installing from a git tag instead</summary>
|
|
61
62
|
|
|
62
|
-
Every release is tagged
|
|
63
|
-
|
|
64
|
-
untagged git dependency
|
|
63
|
+
Every release is tagged and published, so a commit can be installed directly
|
|
64
|
+
when you want to try an unreleased fix. Pin a tag rather than `#main`: an
|
|
65
|
+
untagged git dependency resolves to a different commit on a fresh install.
|
|
65
66
|
|
|
66
67
|
```jsonc
|
|
67
68
|
// package.json
|
|
68
|
-
"@supertype.ai/foundations": "https://github.com/supertypeai/foundations.git#v0.1.
|
|
69
|
+
"@supertype.ai/foundations": "https://github.com/supertypeai/foundations.git#v0.1.30"
|
|
69
70
|
```
|
|
70
71
|
|
|
71
72
|
</details>
|
|
@@ -85,15 +86,16 @@ untagged git dependency re-resolves to a different commit on any fresh install.
|
|
|
85
86
|
```
|
|
86
87
|
|
|
87
88
|
**The `@source` line is required.** Tailwind does not scan `node_modules` by
|
|
88
|
-
default, so without it
|
|
89
|
-
render
|
|
89
|
+
default, so without it the package’s classes get purged and the
|
|
90
|
+
components render without styles.
|
|
90
91
|
|
|
91
|
-
**`theme.css` is required.** `tokens.css` names the colour roles
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
92
|
+
**`theme.css` is required.** `tokens.css` names the colour roles, and
|
|
93
|
+
`theme.css` gives them values. Without it, the colour utilities cannot be
|
|
94
|
+
resolved, so the page renders unpainted without an obvious error. It also
|
|
95
|
+
carries `--secondary-ink`, `--subtle-foreground`, the four earth tones used
|
|
96
|
+
for marker highlights, and the `accordion-down` and `accordion-up` keyframes.
|
|
97
|
+
Skip it only if you declare every role yourself; `foundations doctor` fails if
|
|
98
|
+
neither path is true.
|
|
97
99
|
|
|
98
100
|
### 3. Bind the fonts
|
|
99
101
|
|
|
@@ -112,8 +114,10 @@ const serif = Average({ variable: "--font-average", weight: "400", subsets: ["la
|
|
|
112
114
|
<html className={`${sans.variable} ${mono.variable} ${serif.variable} font-sans`}>
|
|
113
115
|
```
|
|
114
116
|
|
|
115
|
-
**Bind with `.variable`, never `.className`.** A className sets `font-family`
|
|
116
|
-
the element itself and leaves the roles unresolved,
|
|
117
|
+
**Bind with `.variable`, never `.className`.** A className sets `font-family`
|
|
118
|
+
on the element itself and leaves the roles unresolved, which causes a mismatch
|
|
119
|
+
where the page renders one typeface while the `font-sans` and `font-heading`
|
|
120
|
+
utilities render another.
|
|
117
121
|
|
|
118
122
|
### 4. Check the wiring
|
|
119
123
|
|
|
@@ -213,9 +217,9 @@ only from this package, so you can paste it into your app and it compiles.
|
|
|
213
217
|
|
|
214
218
|
## For coding agents
|
|
215
219
|
|
|
216
|
-
The package
|
|
217
|
-
that do not produce an error. Point your agent at it once and it stops
|
|
218
|
-
|
|
220
|
+
The package includes an `llms.txt` with the public API, the rules, and the
|
|
221
|
+
mistakes that do not produce an error. Point your agent at it once and it stops
|
|
222
|
+
writing `text-sm text-muted-foreground` where a primitive already exists:
|
|
219
223
|
|
|
220
224
|
```md
|
|
221
225
|
<!-- CLAUDE.md, AGENTS.md, or your agent's equivalent -->
|
|
@@ -233,7 +237,7 @@ package.
|
|
|
233
237
|
| import | contains | docs |
|
|
234
238
|
| --------------------------------------------------------------------- | ------------------------------------------------------------------------ | -------------------------------------------- |
|
|
235
239
|
| `@supertype.ai/foundations` | all typography primitives, `cn` | [Typography](docs/typography.md) |
|
|
236
|
-
| `@supertype.ai/foundations/blocks` | `Card`, `Callout`, `Steps`, `
|
|
240
|
+
| `@supertype.ai/foundations/blocks` | `Button`, `Badge`, `Card`, `Callout`, `Steps`, `TabGroup`, `Accordion`, `SEGMENT` | [Blocks](docs/blocks.md) |
|
|
237
241
|
| `@supertype.ai/foundations/mdx` | `proseMdxComponents` — the MDX element map | [In MDX](docs/blocks.md#in-mdx) |
|
|
238
242
|
| `@supertype.ai/foundations/essay` | the long-form shell, TOC, reading rail, post meta | [Essay](docs/essay.md) |
|
|
239
243
|
| `@supertype.ai/foundations/seo` | `createSeo(...)` — metadata + JSON-LD | [Tooling](docs/tooling.md#seo-and-og-images) |
|
package/bin/foundations.mjs
CHANGED
|
@@ -518,9 +518,9 @@ const checkContrast = async (appRoot) => {
|
|
|
518
518
|
const cssFile = findCssEntry(appRoot);
|
|
519
519
|
if (!cssFile) return out;
|
|
520
520
|
|
|
521
|
-
let checkLegibility, checkSignals, formatFailures;
|
|
521
|
+
let checkLegibility, checkSignals, checkHairlines, formatFailures;
|
|
522
522
|
try {
|
|
523
|
-
({ checkLegibility, checkSignals, formatFailures } = await import(
|
|
523
|
+
({ checkLegibility, checkSignals, checkHairlines, formatFailures } = await import(
|
|
524
524
|
join(pkgRoot, "dist/contrast.js")
|
|
525
525
|
));
|
|
526
526
|
} catch {
|
|
@@ -533,6 +533,9 @@ const checkContrast = async (appRoot) => {
|
|
|
533
533
|
const groups = [
|
|
534
534
|
["error", "structural ink below 4.5:1", checkLegibility(css)],
|
|
535
535
|
["warn", "mark or tinted ink below its bar", checkSignals(css)],
|
|
536
|
+
// A rule owes no WCAG bar, so this can never be an error. It is here because
|
|
537
|
+
// an app that retints --border almost always retints one theme.
|
|
538
|
+
["warn", "hairline too faint to read as a rule", checkHairlines(css)],
|
|
536
539
|
];
|
|
537
540
|
|
|
538
541
|
for (const [level, title, failures] of groups) {
|
package/dist/blocks/accordion.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Children, cloneElement, isValidElement } from "react";
|
|
3
3
|
import { cn } from "../cn.js";
|
|
4
|
+
import { inkOnSurface } from "../tone.js";
|
|
4
5
|
/**
|
|
5
6
|
* A disclosure group: `<details>`/`<summary>`, no JS, correct before hydration.
|
|
6
7
|
*
|
|
@@ -55,5 +56,5 @@ function deriveGroupName(children) {
|
|
|
55
56
|
return `accordion-${(hash >>> 0).toString(36)}`;
|
|
56
57
|
}
|
|
57
58
|
export function Disclosure({ title, children, className, ...props }) {
|
|
58
|
-
return (_jsxs("details", { className: cn("group bg-card", className), ...props, children: [_jsxs("summary", { className: "flex cursor-pointer list-none items-center justify-between gap-4 px-4 py-3 text-sm font-medium text-foreground marker:hidden hover:bg-accent [&::-webkit-details-marker]:hidden", children: [title, _jsx("svg", { "aria-hidden": "true", className: "h-4 w-4 shrink-0 text-muted-foreground transition-transform group-open:rotate-180", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("path", { d: "m6 9 6 6 6-6" }) })] }), _jsx("div", { className: "px-4 pb-4 text-sm text-muted-foreground", children: children })] }));
|
|
59
|
+
return (_jsxs("details", { className: cn("group bg-card", inkOnSurface("--card-foreground"), className), ...props, children: [_jsxs("summary", { className: "flex cursor-pointer list-none items-center justify-between gap-4 px-4 py-3 text-sm font-medium text-foreground marker:hidden hover:bg-accent [&::-webkit-details-marker]:hidden", children: [title, _jsx("svg", { "aria-hidden": "true", className: "h-4 w-4 shrink-0 text-muted-foreground transition-transform group-open:rotate-180", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("path", { d: "m6 9 6 6 6-6" }) })] }), _jsx("div", { className: "px-4 pb-4 text-sm text-muted-foreground", children: children })] }));
|
|
59
60
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ComponentProps, ReactElement } from "react";
|
|
2
|
+
import { type VariantProps } from "class-variance-authority";
|
|
3
|
+
import { type LinkBehavior } from "../href.js";
|
|
4
|
+
declare const badge: (props?: ({
|
|
5
|
+
tone?: "muted" | "primary" | "secondary" | "brand" | "success" | "warn" | "destructive" | null | undefined;
|
|
6
|
+
size?: "sm" | "xs" | null | undefined;
|
|
7
|
+
pill?: boolean | null | undefined;
|
|
8
|
+
variant?: "solid" | "soft" | "outline" | "ghost" | null | undefined;
|
|
9
|
+
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
10
|
+
export type BadgeLook = VariantProps<typeof badge>;
|
|
11
|
+
export declare function badgeVariants(props?: Parameters<typeof badge>[0]): string;
|
|
12
|
+
/**
|
|
13
|
+
* A `span`, an anchor when given an `href` — a tag pill leading to its listing —
|
|
14
|
+
* and whatever `render` says otherwise — cloned rather than run through a
|
|
15
|
+
* `useRender` hook, which is what viably's badge did. A hook would make every
|
|
16
|
+
* badge in the tree a client component to serve the one call site that renders
|
|
17
|
+
* an anchor, and a badge is a label: it should cost nothing on the server. This
|
|
18
|
+
* is the same mechanism `Button` uses for the same reason.
|
|
19
|
+
*
|
|
20
|
+
* The `[a]:hover` rules above light up on their own when an anchor is the parent
|
|
21
|
+
* or the rendered element.
|
|
22
|
+
*/
|
|
23
|
+
export declare function Badge({ className, variant, tone, size, pill, render, href, external, newTab, ...props }: ComponentProps<"span"> & BadgeLook & LinkBehavior & {
|
|
24
|
+
render?: ReactElement;
|
|
25
|
+
href?: string;
|
|
26
|
+
}): import("react").JSX.Element;
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cva } from "class-variance-authority";
|
|
3
|
+
import { cn } from "../cn.js";
|
|
4
|
+
import { renderAs } from "./render-as.js";
|
|
5
|
+
import { resolveLink } from "../href.js";
|
|
6
|
+
import { FOCUS_RING } from "./focus.js";
|
|
7
|
+
import { INK_ON_FILL, TONE, TONE_SURFACE, impliedTone } from "../tone.js";
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// A label that is not a control. Same two axes as Button, and for the same
|
|
10
|
+
// reason: the two apps had each grown their own list, and the lists disagreed
|
|
11
|
+
// with each other and with the rest of the package.
|
|
12
|
+
//
|
|
13
|
+
// ssite's read `default | secondary | destructive | outline | success | warning
|
|
14
|
+
// | supertype | tint`. Two of those are the package's tones under invented
|
|
15
|
+
// spellings — `warning` for `warn`, `supertype` for `brand` — which is precisely
|
|
16
|
+
// the second vocabulary a design system exists to prevent. `secondary` was
|
|
17
|
+
// `bg-muted/80 text-foreground` against a `default` of `bg-muted
|
|
18
|
+
// text-muted-foreground`: a distinction no reader could name, let alone use.
|
|
19
|
+
//
|
|
20
|
+
// viably's read `default | secondary | destructive | outline | ghost | link`,
|
|
21
|
+
// which is a copy of the button list it was cargo-culted from — including the
|
|
22
|
+
// `link` variant, which no badge has ever used, because a badge is not a link.
|
|
23
|
+
//
|
|
24
|
+
// So: `variant` for how much ink, `tone` for what it means, both spelled exactly
|
|
25
|
+
// as Button spells them. `link` is absent because it was never real; everything
|
|
26
|
+
// else about the vocabulary is the same list, so knowing one component's axes is
|
|
27
|
+
// knowing this one's.
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
const badge = cva(cn("inline-flex w-fit shrink-0 items-center justify-center gap-1", "overflow-hidden border border-transparent font-medium whitespace-nowrap", cn("transition focus-visible:border-ring", FOCUS_RING), "aria-invalid:border-destructive aria-invalid:ring-destructive/20", "[&>svg]:pointer-events-none [&>svg]:size-3!", TONE_SURFACE), {
|
|
30
|
+
// Same cascade order as Button: `pill` beats `size` on radius.
|
|
31
|
+
variants: {
|
|
32
|
+
tone: TONE,
|
|
33
|
+
/**
|
|
34
|
+
* Two rungs, because a badge has two jobs. `sm` is the label riding beside
|
|
35
|
+
* a title; `xs` is the figure riding beside a toolbar control — a filter
|
|
36
|
+
* tally, an unread count.
|
|
37
|
+
*
|
|
38
|
+
* `leading-none` restates the default on purpose: `text-3xs` carries a
|
|
39
|
+
* line-height of its own, which lands after the base class and wins.
|
|
40
|
+
* Stating it per size is what keeps `!leading-none` out of call sites.
|
|
41
|
+
*/
|
|
42
|
+
size: {
|
|
43
|
+
xs: "h-4 min-w-4 rounded px-1 text-3xs leading-none",
|
|
44
|
+
sm: "h-5 rounded-md px-2 py-0.5 text-xs leading-none",
|
|
45
|
+
},
|
|
46
|
+
pill: { true: "rounded-full", false: "" },
|
|
47
|
+
variant: {
|
|
48
|
+
solid: `bg-(--tone-fill) text-(color:--tone-ink) [a]:hover:bg-(--tone-fill-hover) ${INK_ON_FILL}`,
|
|
49
|
+
soft: "bg-(--tone-wash) text-(color:--tone-hue) [a]:hover:bg-(--tone-wash-hover)",
|
|
50
|
+
outline: "border-(color:--tone-line) text-(color:--tone-hue) [a]:hover:bg-(--tone-wash)",
|
|
51
|
+
ghost: "text-(color:--tone-hue) hover:bg-(--tone-wash)",
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
defaultVariants: { variant: "solid", size: "sm", pill: false },
|
|
55
|
+
});
|
|
56
|
+
export function badgeVariants(props = {}) {
|
|
57
|
+
return badge({ tone: props?.tone ?? impliedTone(props?.variant), ...props });
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* A `span`, an anchor when given an `href` — a tag pill leading to its listing —
|
|
61
|
+
* and whatever `render` says otherwise — cloned rather than run through a
|
|
62
|
+
* `useRender` hook, which is what viably's badge did. A hook would make every
|
|
63
|
+
* badge in the tree a client component to serve the one call site that renders
|
|
64
|
+
* an anchor, and a badge is a label: it should cost nothing on the server. This
|
|
65
|
+
* is the same mechanism `Button` uses for the same reason.
|
|
66
|
+
*
|
|
67
|
+
* The `[a]:hover` rules above light up on their own when an anchor is the parent
|
|
68
|
+
* or the rendered element.
|
|
69
|
+
*/
|
|
70
|
+
export function Badge({ className, variant, tone, size, pill, render, href, external, newTab, ...props }) {
|
|
71
|
+
const resolved = tone ?? impliedTone(variant);
|
|
72
|
+
const classes = cn(badge({ variant, tone: resolved, size, pill, className }));
|
|
73
|
+
const marks = {
|
|
74
|
+
"data-slot": "badge",
|
|
75
|
+
"data-variant": variant ?? "solid",
|
|
76
|
+
"data-tone": resolved,
|
|
77
|
+
};
|
|
78
|
+
if (href !== undefined) {
|
|
79
|
+
const { Component, props: link } = resolveLink(href, { external, newTab });
|
|
80
|
+
return (_jsx(Component, { ...marks, ...link, className: classes, ...props }));
|
|
81
|
+
}
|
|
82
|
+
const as = renderAs(render, classes, { ...marks, ...props });
|
|
83
|
+
if (as)
|
|
84
|
+
return as;
|
|
85
|
+
return _jsx("span", { ...marks, className: classes, ...props });
|
|
86
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Button as ButtonPrimitive } from "@base-ui/react/button";
|
|
2
|
+
import { type VariantProps } from "class-variance-authority";
|
|
3
|
+
import { type LinkBehavior } from "../href.js";
|
|
4
|
+
declare const button: (props?: ({
|
|
5
|
+
tone?: "muted" | "primary" | "secondary" | "brand" | "success" | "warn" | "destructive" | null | undefined;
|
|
6
|
+
size?: "sm" | "xs" | "md" | "lg" | "xl" | null | undefined;
|
|
7
|
+
icon?: boolean | null | undefined;
|
|
8
|
+
pill?: boolean | null | undefined;
|
|
9
|
+
variant?: "link" | "solid" | "soft" | "outline" | "ghost" | null | undefined;
|
|
10
|
+
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
11
|
+
export type ButtonLook = VariantProps<typeof button>;
|
|
12
|
+
/**
|
|
13
|
+
* The class list, for the handful of places that style someone else's element
|
|
14
|
+
* and cannot render a `Button` — a router `Link` inside a `not-found`, a
|
|
15
|
+
* calendar day cell. Takes the same props, including the implied tone.
|
|
16
|
+
*/
|
|
17
|
+
export declare function buttonVariants(props?: Parameters<typeof button>[0]): string;
|
|
18
|
+
/**
|
|
19
|
+
* `href` makes the button a link — the anchor is the button, and where the href
|
|
20
|
+
* goes is ../href.ts's decision, not the call site's. `render={<a href="…" />}`
|
|
21
|
+
* did this before, and got a bare anchor: no router, so a CTA reloaded the page
|
|
22
|
+
* and lost the view transition, and an off-site href never grew a `rel`.
|
|
23
|
+
*
|
|
24
|
+
* Either way a non-`<button>` element bypasses the primitive on purpose: Base UI
|
|
25
|
+
* always stamps `type="button"` or `role="button"`, and the latter drops an
|
|
26
|
+
* anchor out of screen-reader link navigation. `render` remains for an element
|
|
27
|
+
* that is genuinely neither — a `<label>`, a menu item.
|
|
28
|
+
*/
|
|
29
|
+
export declare function Button({ className, variant, tone, size, icon, pill, render, nativeButton, href, external, newTab, ...props }: ButtonPrimitive.Props & ButtonLook & LinkBehavior & {
|
|
30
|
+
href?: string;
|
|
31
|
+
}): import("react").JSX.Element;
|
|
32
|
+
export {};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { isValidElement } from "react";
|
|
3
|
+
import { Button as ButtonPrimitive } from "@base-ui/react/button";
|
|
4
|
+
import { cva } from "class-variance-authority";
|
|
5
|
+
import { cn } from "../cn.js";
|
|
6
|
+
import { renderAs } from "./render-as.js";
|
|
7
|
+
import { resolveLink } from "../href.js";
|
|
8
|
+
import { FOCUS_RING } from "./focus.js";
|
|
9
|
+
import { INK_ON_FILL, TONE, TONE_SURFACE, impliedTone } from "../tone.js";
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// The control both apps were re-declaring. viably and ssite each carried their
|
|
12
|
+
// own `cva` with its own variant list — `default | secondary | accent |
|
|
13
|
+
// destructive | ghost | outline | link` in one, the same names minus two plus a
|
|
14
|
+
// `rose` in the other — and the two had already drifted on radius, on height,
|
|
15
|
+
// and on what `destructive` even means (a solid red fill in ssite, a tinted wash
|
|
16
|
+
// in viably).
|
|
17
|
+
//
|
|
18
|
+
// The fix is not a longer shared list. Those names answer two questions at once:
|
|
19
|
+
//
|
|
20
|
+
// variant — how much ink the button spends. Filled, washed, hairline, bare.
|
|
21
|
+
// tone — what the ink means. See ../tone.ts; Callout and TypographyLink
|
|
22
|
+
// take the same seven, because a component does not get to invent a
|
|
23
|
+
// name for a colour the package has already named. Its default is
|
|
24
|
+
// the one the variant implies — see `impliedTone`.
|
|
25
|
+
//
|
|
26
|
+
// `destructive` is a tone. `ghost` is a variant. A list holding both can only
|
|
27
|
+
// express the pairs someone thought to add, which is why neither app could write
|
|
28
|
+
// a quiet destructive button without a className.
|
|
29
|
+
//
|
|
30
|
+
// Everything else is a modifier, and both are boolean because both have exactly
|
|
31
|
+
// two states: `icon` squares the box, `pill` rounds it off. They compose — a
|
|
32
|
+
// round icon button is `icon pill` — which is why they are not one `shape` enum.
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
const button = cva(cn("inline-flex shrink-0 cursor-pointer items-center justify-center", "border border-transparent bg-clip-padding font-medium whitespace-nowrap", "transition select-none",
|
|
35
|
+
// The border joins the ring where a control has one; the ring itself is shared.
|
|
36
|
+
cn(FOCUS_RING, "focus-visible:border-ring"), "active:not-aria-[haspopup]:translate-y-px", "disabled:pointer-events-none disabled:opacity-50", "aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20", "[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", TONE_SURFACE), {
|
|
37
|
+
// Key order is cascade order: `cva` emits these as declared and `cn` resolves
|
|
38
|
+
// a conflict in favour of the last one written. `pill` therefore beats
|
|
39
|
+
// `size` on radius, and `variant` beats `size` on the box — which is what
|
|
40
|
+
// lets `link` shed the height and padding of whatever size it was given.
|
|
41
|
+
// Reordering this object is a visual change.
|
|
42
|
+
variants: {
|
|
43
|
+
tone: TONE,
|
|
44
|
+
/**
|
|
45
|
+
* One ladder, 24px to 40px on a 4px step. `md` is the product default —
|
|
46
|
+
* dense rows of controls beside a table — and `lg`/`xl` the marketing
|
|
47
|
+
* rungs. `--radius-lg` is 10px and theme.css already labels it "buttons,
|
|
48
|
+
* default control"; the two rungs below borrow `md`, because a 10px radius
|
|
49
|
+
* on a 24px box reads as a lozenge.
|
|
50
|
+
*/
|
|
51
|
+
size: {
|
|
52
|
+
xs: "h-6 gap-1 rounded-md px-2 text-xs [&_svg:not([class*='size-'])]:size-3",
|
|
53
|
+
sm: "h-7 gap-1 rounded-md px-2.5 text-[0.8rem] [&_svg:not([class*='size-'])]:size-3.5",
|
|
54
|
+
md: "h-8 gap-1.5 rounded-lg px-3 text-sm",
|
|
55
|
+
lg: "h-9 gap-2 rounded-lg px-4 text-sm",
|
|
56
|
+
xl: "h-10 gap-2 rounded-lg px-6 text-sm",
|
|
57
|
+
},
|
|
58
|
+
/** A square box for a lone glyph, on whichever rung you are already on. No second ladder of `icon-sm` names to keep aligned with the first. */
|
|
59
|
+
icon: { true: "px-0", false: "" },
|
|
60
|
+
/** Full-round corners. Marketing surfaces; also every filter chip. */
|
|
61
|
+
pill: { true: "rounded-full", false: "" },
|
|
62
|
+
variant: {
|
|
63
|
+
solid: `bg-(--tone-fill) text-(color:--tone-ink) hover:bg-(--tone-fill-hover) ${INK_ON_FILL}`,
|
|
64
|
+
soft: "bg-(--tone-wash) text-(color:--tone-hue) hover:bg-(--tone-wash-hover)",
|
|
65
|
+
outline: "border-(color:--tone-line) bg-background text-(color:--tone-hue) hover:bg-(--tone-wash)",
|
|
66
|
+
ghost: "text-(color:--tone-hue) hover:bg-(--tone-wash)",
|
|
67
|
+
// No box of its own: a button that reads as a link has to sit on the
|
|
68
|
+
// text baseline, not on a 32px control's centre line.
|
|
69
|
+
link: "h-auto gap-1 rounded-none px-0 py-0 text-(color:--tone-hue) underline-offset-4 hover:underline",
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
compoundVariants: [
|
|
73
|
+
{ icon: true, size: "xs", class: "size-6" },
|
|
74
|
+
{ icon: true, size: "sm", class: "size-7" },
|
|
75
|
+
{ icon: true, size: "md", class: "size-8" },
|
|
76
|
+
{ icon: true, size: "lg", class: "size-9" },
|
|
77
|
+
{ icon: true, size: "xl", class: "size-10" },
|
|
78
|
+
],
|
|
79
|
+
defaultVariants: {
|
|
80
|
+
variant: "solid",
|
|
81
|
+
size: "md",
|
|
82
|
+
icon: false,
|
|
83
|
+
pill: false,
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
/**
|
|
87
|
+
* The class list, for the handful of places that style someone else's element
|
|
88
|
+
* and cannot render a `Button` — a router `Link` inside a `not-found`, a
|
|
89
|
+
* calendar day cell. Takes the same props, including the implied tone.
|
|
90
|
+
*/
|
|
91
|
+
export function buttonVariants(props = {}) {
|
|
92
|
+
return button({ tone: props?.tone ?? impliedTone(props?.variant), ...props });
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* `href` makes the button a link — the anchor is the button, and where the href
|
|
96
|
+
* goes is ../href.ts's decision, not the call site's. `render={<a href="…" />}`
|
|
97
|
+
* did this before, and got a bare anchor: no router, so a CTA reloaded the page
|
|
98
|
+
* and lost the view transition, and an off-site href never grew a `rel`.
|
|
99
|
+
*
|
|
100
|
+
* Either way a non-`<button>` element bypasses the primitive on purpose: Base UI
|
|
101
|
+
* always stamps `type="button"` or `role="button"`, and the latter drops an
|
|
102
|
+
* anchor out of screen-reader link navigation. `render` remains for an element
|
|
103
|
+
* that is genuinely neither — a `<label>`, a menu item.
|
|
104
|
+
*/
|
|
105
|
+
export function Button({ className, variant, tone, size, icon, pill, render, nativeButton, href, external, newTab, ...props }) {
|
|
106
|
+
const resolved = tone ?? impliedTone(variant);
|
|
107
|
+
const classes = cn(button({ variant, tone: resolved, size, icon, pill, className }));
|
|
108
|
+
// The resolved axes, stamped: a child can style off its parent's tone, and a
|
|
109
|
+
// test can assert the ramp without asserting a class string.
|
|
110
|
+
const marks = {
|
|
111
|
+
"data-slot": "button",
|
|
112
|
+
"data-variant": variant ?? "solid",
|
|
113
|
+
"data-tone": resolved,
|
|
114
|
+
};
|
|
115
|
+
if (href !== undefined) {
|
|
116
|
+
const { Component, props: link } = resolveLink(href, { external, newTab });
|
|
117
|
+
return (_jsx(Component, { ...marks, ...link, className: classes, ...props }));
|
|
118
|
+
}
|
|
119
|
+
// `render.type !== "button"`: a plain <button/> still goes through the primitive, which
|
|
120
|
+
// is what supplies the native semantics.
|
|
121
|
+
const as = isValidElement(render) && render.type === "button"
|
|
122
|
+
? null
|
|
123
|
+
: renderAs(render, classes, { ...marks, ...props });
|
|
124
|
+
if (as)
|
|
125
|
+
return as;
|
|
126
|
+
return (_jsx(ButtonPrimitive, { ...marks, className: classes, render: render, nativeButton: nativeButton ?? true, ...props }));
|
|
127
|
+
}
|
package/dist/blocks/callout.d.ts
CHANGED
|
@@ -1,41 +1,5 @@
|
|
|
1
1
|
import type { ComponentType, ReactNode } from "react";
|
|
2
|
-
|
|
3
|
-
/** The default: an explanation, not a problem. */
|
|
4
|
-
readonly muted: {
|
|
5
|
-
readonly box: "border-border bg-muted/40";
|
|
6
|
-
readonly title: "text-foreground";
|
|
7
|
-
readonly icon: "text-muted-foreground";
|
|
8
|
-
readonly rail: "bg-border";
|
|
9
|
-
};
|
|
10
|
-
/** Something failed and the reader needs to see that it did. */
|
|
11
|
-
readonly destructive: {
|
|
12
|
-
readonly box: "border-destructive/40 bg-destructive/5";
|
|
13
|
-
readonly title: "text-destructive";
|
|
14
|
-
readonly icon: "text-destructive";
|
|
15
|
-
readonly rail: "bg-destructive/60";
|
|
16
|
-
};
|
|
17
|
-
/** A prerequisite or a footgun: the reader can still proceed, but not blindly. */
|
|
18
|
-
readonly warn: {
|
|
19
|
-
readonly box: "border-warn/25 bg-warn/5";
|
|
20
|
-
readonly title: "text-warn-ink";
|
|
21
|
-
readonly icon: "text-warn";
|
|
22
|
-
readonly rail: "bg-warn/60";
|
|
23
|
-
};
|
|
24
|
-
/** A detail that rewards the reader rather than warning them. */
|
|
25
|
-
readonly accent: {
|
|
26
|
-
readonly box: "border-primary/25 bg-primary/5";
|
|
27
|
-
readonly title: "text-foreground";
|
|
28
|
-
readonly icon: "text-primary";
|
|
29
|
-
readonly rail: "bg-primary/60";
|
|
30
|
-
};
|
|
31
|
-
};
|
|
32
|
-
/**
|
|
33
|
-
* Two densities, because this notice serves two ramps. `compact` is the product
|
|
34
|
-
* default (12px title over 12px body, no rail) that the relay sheet and the contact
|
|
35
|
-
* record already render. `editorial` is the docs form: body copy at reading size,
|
|
36
|
-
* and a 3px accent rail carrying the tone so the surface itself can stay quiet.
|
|
37
|
-
* Splitting on a prop rather than forking the component is the point of the file.
|
|
38
|
-
*/
|
|
2
|
+
import { type Tone } from "../tone.js";
|
|
39
3
|
export declare function Callout({ icon: Icon, title, tone, density, bodyClassName, action, children, className, }: {
|
|
40
4
|
/** Injected, so the package needs no icon set. Optional: a notice whose title already reads as a label
|
|
41
5
|
* ("Replied into Norman's thread") gains nothing from a glyph beside it. */
|
|
@@ -43,7 +7,7 @@ export declare function Callout({ icon: Icon, title, tone, density, bodyClassNam
|
|
|
43
7
|
className?: string;
|
|
44
8
|
}>;
|
|
45
9
|
title?: ReactNode;
|
|
46
|
-
tone?:
|
|
10
|
+
tone?: Tone;
|
|
47
11
|
density?: "compact" | "editorial";
|
|
48
12
|
/** For the one body that is not prose — a raw delivery error, which needs mono and its own
|
|
49
13
|
* line breaks preserved. */
|
|
@@ -54,4 +18,3 @@ export declare function Callout({ icon: Icon, title, tone, density, bodyClassNam
|
|
|
54
18
|
children: ReactNode;
|
|
55
19
|
className?: string;
|
|
56
20
|
}): import("react").JSX.Element;
|
|
57
|
-
export {};
|
package/dist/blocks/callout.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { cn } from "../cn.js";
|
|
3
|
+
import { toneClass } from "../tone.js";
|
|
3
4
|
import { TypographyCaption, TypographyLabel, TypographyMuted, TypographySmall, } from "../typography/paragraph.js";
|
|
4
5
|
// ---------------------------------------------------------------------------
|
|
5
6
|
// An inline notice: a titled paragraph that explains something the surface it sits in cannot say
|
|
@@ -15,47 +16,21 @@ import { TypographyCaption, TypographyLabel, TypographyMuted, TypographySmall, }
|
|
|
15
16
|
// that just happened; these are quiet, permanent explanations sitting inside a panel, and they
|
|
16
17
|
// must not announce themselves to a screen reader every time a sheet opens.
|
|
17
18
|
// ---------------------------------------------------------------------------
|
|
18
|
-
const TONE = {
|
|
19
|
-
/** The default: an explanation, not a problem. */
|
|
20
|
-
muted: {
|
|
21
|
-
box: "border-border bg-muted/40",
|
|
22
|
-
title: "text-foreground",
|
|
23
|
-
icon: "text-muted-foreground",
|
|
24
|
-
rail: "bg-border",
|
|
25
|
-
},
|
|
26
|
-
/** Something failed and the reader needs to see that it did. */
|
|
27
|
-
destructive: {
|
|
28
|
-
box: "border-destructive/40 bg-destructive/5",
|
|
29
|
-
title: "text-destructive",
|
|
30
|
-
icon: "text-destructive",
|
|
31
|
-
rail: "bg-destructive/60",
|
|
32
|
-
},
|
|
33
|
-
/** A prerequisite or a footgun: the reader can still proceed, but not blindly. */
|
|
34
|
-
warn: {
|
|
35
|
-
box: "border-warn/25 bg-warn/5",
|
|
36
|
-
title: "text-warn-ink",
|
|
37
|
-
icon: "text-warn",
|
|
38
|
-
rail: "bg-warn/60",
|
|
39
|
-
},
|
|
40
|
-
/** A detail that rewards the reader rather than warning them. */
|
|
41
|
-
accent: {
|
|
42
|
-
box: "border-primary/25 bg-primary/5",
|
|
43
|
-
title: "text-foreground",
|
|
44
|
-
icon: "text-primary",
|
|
45
|
-
rail: "bg-primary/60",
|
|
46
|
-
},
|
|
47
|
-
};
|
|
48
19
|
/**
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
20
|
+
* No tone table of its own: the same seven `Button` and `TypographyLink` take.
|
|
21
|
+
* The four hand-tuned rows this file used to carry are gone, `accent` among them
|
|
22
|
+
* — it was `--primary` under another name, which is why a "tip" callout and a
|
|
23
|
+
* "primary" one were indistinguishable. `muted` keeps its name and its values.
|
|
24
|
+
* See ../tone.ts.
|
|
25
|
+
*
|
|
26
|
+
* A callout is a panel, so it tints with `--tone-veil` (5%) where a control uses
|
|
27
|
+
* `--tone-wash` (10%). That is the only thing this file knows about colour.
|
|
54
28
|
*/
|
|
29
|
+
const BOX = "border-(color:--tone-line) bg-(--tone-veil)";
|
|
55
30
|
export function Callout({ icon: Icon, title, tone = "muted", density = "compact", bodyClassName, action, children, className, }) {
|
|
56
|
-
const
|
|
31
|
+
const toned = toneClass(tone);
|
|
57
32
|
if (density === "editorial") {
|
|
58
|
-
return (
|
|
33
|
+
return (_jsxs("div", { className: cn("relative overflow-hidden rounded-lg border py-3.5 pl-5 pr-4", toned, BOX, className), children: [_jsx("span", { "aria-hidden": true, className: "absolute inset-y-0 left-0 w-[3px] bg-(--tone-line)" }), _jsxs("div", { className: "flex items-start gap-2.5", children: [Icon && (_jsx(Icon, { className: "mt-0.5 size-4 shrink-0 text-(color:--tone-hue)" })), _jsxs("div", { className: "flex min-w-0 flex-col gap-1", children: [title && (_jsx(TypographyLabel, { className: "text-(color:--tone-hue)", children: title })), _jsx(TypographyMuted, { className: cn("leading-relaxed", bodyClassName), children: children }), action && (_jsx("div", { className: "mt-1 flex items-center gap-1", children: action }))] })] })] }));
|
|
59
34
|
}
|
|
60
|
-
return (_jsxs("div", { className: cn("rounded-md border p-3",
|
|
35
|
+
return (_jsxs("div", { className: cn("rounded-md border p-3", toned, BOX, className), children: [title && (_jsxs(TypographySmall, { className: "flex items-center gap-1.5 font-medium text-(color:--tone-hue)", children: [Icon && _jsx(Icon, { className: "size-3.5 shrink-0" }), title] })), _jsx(TypographyCaption, { className: cn("mt-1 block leading-relaxed", bodyClassName), children: children }), action && _jsx("div", { className: "mt-2 flex items-center gap-1", children: action })] }));
|
|
61
36
|
}
|
package/dist/blocks/card.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ComponentProps, ReactNode } from "react";
|
|
2
|
+
import { type LinkBehavior } from "../href.js";
|
|
2
3
|
/** Two columns from `sm` up: a pair reads as a set rather than two panels. */
|
|
3
4
|
export declare function Cards({ className, children, ...props }: ComponentProps<"div">): import("react").JSX.Element;
|
|
4
5
|
export declare function CardHeader({ className, ...props }: ComponentProps<"div">): import("react").JSX.Element;
|
|
@@ -18,17 +19,15 @@ type CardShorthand = {
|
|
|
18
19
|
title?: ReactNode;
|
|
19
20
|
description?: ReactNode;
|
|
20
21
|
icon?: ReactNode;
|
|
21
|
-
/** Override the scheme sniff: an absolute URL home, or a relative one away. */
|
|
22
|
-
external?: boolean;
|
|
23
22
|
};
|
|
24
23
|
/**
|
|
25
24
|
* Takes either shape: `title`/`href` fills the header, or compose the slots
|
|
26
25
|
* directly. Unrecognised props pass through — MDX authors reach for the whole
|
|
27
|
-
* HTML surface.
|
|
28
|
-
*
|
|
26
|
+
* HTML surface. Where the href goes is ../href.ts's call, the same one Button
|
|
27
|
+
* and TypographyLink make.
|
|
29
28
|
*/
|
|
30
|
-
export declare function Card({ href, className, external, title, description, icon, children, ...rest }: CardShorthand & {
|
|
29
|
+
export declare function Card({ href, className, external, newTab, title, description, icon, children, ...rest }: CardShorthand & LinkBehavior & {
|
|
31
30
|
href?: string;
|
|
32
31
|
children?: ReactNode;
|
|
33
|
-
} & Omit<ComponentProps<"a">, keyof CardShorthand | "href" | "children">): import("react").JSX.Element;
|
|
32
|
+
} & Omit<ComponentProps<"a">, keyof CardShorthand | keyof LinkBehavior | "href" | "children">): import("react").JSX.Element;
|
|
34
33
|
export {};
|