@supertype.ai/foundations 0.1.29 → 0.1.31
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 +26 -22
- package/bin/foundations.mjs +5 -2
- package/dist/blocks/accordion.js +2 -1
- package/dist/blocks/anchor.d.ts +27 -0
- package/dist/blocks/anchor.js +28 -0
- package/dist/blocks/badge.d.ts +7 -2
- package/dist/blocks/badge.js +10 -4
- package/dist/blocks/button.d.ts +25 -4
- package/dist/blocks/button.js +16 -6
- package/dist/blocks/card.d.ts +5 -6
- package/dist/blocks/card.js +8 -12
- package/dist/blocks/index.d.ts +1 -0
- package/dist/blocks/index.js +1 -0
- package/dist/blocks/tabs.d.ts +5 -5
- package/dist/blocks/tabs.js +3 -3
- package/dist/cjs/eslint.js +37 -11
- package/dist/contrast.d.ts +34 -0
- package/dist/contrast.js +208 -10
- package/dist/eslint.d.ts +20 -4
- package/dist/eslint.js +36 -11
- package/dist/essay/layout.d.ts +1 -1
- package/dist/essay/layout.js +1 -1
- package/dist/essay/rail.d.ts +11 -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 -1
- package/dist/index.js +6 -2
- package/dist/tone.d.ts +58 -9
- package/dist/tone.js +60 -12
- package/dist/typography/header.d.ts +2 -2
- package/dist/typography/header.js +4 -4
- package/dist/typography/highlight.d.ts +3 -2
- package/dist/typography/highlight.js +11 -5
- package/dist/typography/paragraph.d.ts +9 -12
- package/dist/typography/paragraph.js +29 -22
- package/llms.txt +97 -33
- package/package.json +2 -2
- package/src/theme.css +73 -20
- 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.31"
|
|
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 -->
|
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 { INK_ON_CARD } 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", INK_ON_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
60
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ComponentProps } from "react";
|
|
2
|
+
import { type LinkBehavior } from "../href.js";
|
|
3
|
+
/**
|
|
4
|
+
* An anchor whose destination is this package's decision rather than the call
|
|
5
|
+
* site's — and nothing else.
|
|
6
|
+
*
|
|
7
|
+
* `Button`, `Badge`, `Card` and `TypographyLink` all take an `href` and hand it
|
|
8
|
+
* to ../href.ts. What was left over was every anchor that is none of those: a
|
|
9
|
+
* thumbnail, a chip, a tooltip trigger, a footer row, an icon in a dialog
|
|
10
|
+
* header. One consumer had thirty-six, each writing `target="_blank"
|
|
11
|
+
* rel="noopener noreferrer"` out by hand, five of them missing the `rel`, four
|
|
12
|
+
* of them putting the pair on a router `Link` — which asks for a client
|
|
13
|
+
* navigation and a new tab in the same breath.
|
|
14
|
+
*
|
|
15
|
+
* No styling, deliberately. `TypographyLink` is the inline link and brings a
|
|
16
|
+
* weight, an ink and an underline with it, which is why it could not take these:
|
|
17
|
+
* they wrap something already drawn, and the only thing they have in common is
|
|
18
|
+
* where they go.
|
|
19
|
+
*
|
|
20
|
+
* Pass `external` for a same-origin path that is not a route — an `/api/…`
|
|
21
|
+
* redirect, a file endpoint. `Link` prefetches on viewport entry, so a signed-URL
|
|
22
|
+
* endpoint would mint one for a link nobody clicked. Call-site props land after
|
|
23
|
+
* the resolved ones, so a link needing a `rel` of its own can still say so.
|
|
24
|
+
*/
|
|
25
|
+
export declare function Anchor({ href, external, newTab, ...props }: Omit<ComponentProps<"a">, "href"> & LinkBehavior & {
|
|
26
|
+
href: string;
|
|
27
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { resolveLink } from "../href.js";
|
|
3
|
+
/**
|
|
4
|
+
* An anchor whose destination is this package's decision rather than the call
|
|
5
|
+
* site's — and nothing else.
|
|
6
|
+
*
|
|
7
|
+
* `Button`, `Badge`, `Card` and `TypographyLink` all take an `href` and hand it
|
|
8
|
+
* to ../href.ts. What was left over was every anchor that is none of those: a
|
|
9
|
+
* thumbnail, a chip, a tooltip trigger, a footer row, an icon in a dialog
|
|
10
|
+
* header. One consumer had thirty-six, each writing `target="_blank"
|
|
11
|
+
* rel="noopener noreferrer"` out by hand, five of them missing the `rel`, four
|
|
12
|
+
* of them putting the pair on a router `Link` — which asks for a client
|
|
13
|
+
* navigation and a new tab in the same breath.
|
|
14
|
+
*
|
|
15
|
+
* No styling, deliberately. `TypographyLink` is the inline link and brings a
|
|
16
|
+
* weight, an ink and an underline with it, which is why it could not take these:
|
|
17
|
+
* they wrap something already drawn, and the only thing they have in common is
|
|
18
|
+
* where they go.
|
|
19
|
+
*
|
|
20
|
+
* Pass `external` for a same-origin path that is not a route — an `/api/…`
|
|
21
|
+
* redirect, a file endpoint. `Link` prefetches on viewport entry, so a signed-URL
|
|
22
|
+
* endpoint would mint one for a link nobody clicked. Call-site props land after
|
|
23
|
+
* the resolved ones, so a link needing a `rel` of its own can still say so.
|
|
24
|
+
*/
|
|
25
|
+
export function Anchor({ href, external, newTab, ...props }) {
|
|
26
|
+
const { Component, props: link } = resolveLink(href, { external, newTab });
|
|
27
|
+
return _jsx(Component, { ...link, ...props });
|
|
28
|
+
}
|
package/dist/blocks/badge.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ComponentProps, ReactElement } from "react";
|
|
2
2
|
import { type VariantProps } from "class-variance-authority";
|
|
3
|
+
import { type LinkBehavior } from "../href.js";
|
|
3
4
|
declare const badge: (props?: ({
|
|
4
5
|
tone?: "muted" | "primary" | "secondary" | "brand" | "success" | "warn" | "destructive" | null | undefined;
|
|
5
6
|
size?: "sm" | "xs" | null | undefined;
|
|
@@ -9,7 +10,8 @@ declare const badge: (props?: ({
|
|
|
9
10
|
export type BadgeLook = VariantProps<typeof badge>;
|
|
10
11
|
export declare function badgeVariants(props?: Parameters<typeof badge>[0]): string;
|
|
11
12
|
/**
|
|
12
|
-
* A `span
|
|
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
|
|
13
15
|
* `useRender` hook, which is what viably's badge did. A hook would make every
|
|
14
16
|
* badge in the tree a client component to serve the one call site that renders
|
|
15
17
|
* an anchor, and a badge is a label: it should cost nothing on the server. This
|
|
@@ -18,7 +20,10 @@ export declare function badgeVariants(props?: Parameters<typeof badge>[0]): stri
|
|
|
18
20
|
* The `[a]:hover` rules above light up on their own when an anchor is the parent
|
|
19
21
|
* or the rendered element.
|
|
20
22
|
*/
|
|
21
|
-
export declare function Badge({ className, variant, tone, size, pill, render, ...props }: ComponentProps<"span"> & BadgeLook & {
|
|
23
|
+
export declare function Badge({ className, variant, tone, size, pill, render, href, external, newTab, ...props }: ComponentProps<"span"> & BadgeLook & LinkBehavior & {
|
|
22
24
|
render?: ReactElement;
|
|
25
|
+
href?: string;
|
|
26
|
+
/** A link that is not a navigation. See Button, which documents the pairing with `external`. */
|
|
27
|
+
download?: ComponentProps<"a">["download"];
|
|
23
28
|
}): import("react").JSX.Element;
|
|
24
29
|
export {};
|
package/dist/blocks/badge.js
CHANGED
|
@@ -2,8 +2,9 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { cva } from "class-variance-authority";
|
|
3
3
|
import { cn } from "../cn.js";
|
|
4
4
|
import { renderAs } from "./render-as.js";
|
|
5
|
+
import { resolveLink } from "../href.js";
|
|
5
6
|
import { FOCUS_RING } from "./focus.js";
|
|
6
|
-
import { TONE, TONE_SURFACE, impliedTone } from "../tone.js";
|
|
7
|
+
import { INK_ON_FILL, TONE, TONE_SURFACE, impliedTone } from "../tone.js";
|
|
7
8
|
// ---------------------------------------------------------------------------
|
|
8
9
|
// A label that is not a control. Same two axes as Button, and for the same
|
|
9
10
|
// reason: the two apps had each grown their own list, and the lists disagreed
|
|
@@ -44,7 +45,7 @@ const badge = cva(cn("inline-flex w-fit shrink-0 items-center justify-center gap
|
|
|
44
45
|
},
|
|
45
46
|
pill: { true: "rounded-full", false: "" },
|
|
46
47
|
variant: {
|
|
47
|
-
solid:
|
|
48
|
+
solid: `bg-(--tone-fill) text-(color:--tone-ink) [a]:hover:bg-(--tone-fill-hover) ${INK_ON_FILL}`,
|
|
48
49
|
soft: "bg-(--tone-wash) text-(color:--tone-hue) [a]:hover:bg-(--tone-wash-hover)",
|
|
49
50
|
outline: "border-(color:--tone-line) text-(color:--tone-hue) [a]:hover:bg-(--tone-wash)",
|
|
50
51
|
ghost: "text-(color:--tone-hue) hover:bg-(--tone-wash)",
|
|
@@ -56,7 +57,8 @@ export function badgeVariants(props = {}) {
|
|
|
56
57
|
return badge({ tone: props?.tone ?? impliedTone(props?.variant), ...props });
|
|
57
58
|
}
|
|
58
59
|
/**
|
|
59
|
-
* A `span
|
|
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
|
|
60
62
|
* `useRender` hook, which is what viably's badge did. A hook would make every
|
|
61
63
|
* badge in the tree a client component to serve the one call site that renders
|
|
62
64
|
* an anchor, and a badge is a label: it should cost nothing on the server. This
|
|
@@ -65,7 +67,7 @@ export function badgeVariants(props = {}) {
|
|
|
65
67
|
* The `[a]:hover` rules above light up on their own when an anchor is the parent
|
|
66
68
|
* or the rendered element.
|
|
67
69
|
*/
|
|
68
|
-
export function Badge({ className, variant, tone, size, pill, render, ...props }) {
|
|
70
|
+
export function Badge({ className, variant, tone, size, pill, render, href, external, newTab, ...props }) {
|
|
69
71
|
const resolved = tone ?? impliedTone(variant);
|
|
70
72
|
const classes = cn(badge({ variant, tone: resolved, size, pill, className }));
|
|
71
73
|
const marks = {
|
|
@@ -73,6 +75,10 @@ export function Badge({ className, variant, tone, size, pill, render, ...props }
|
|
|
73
75
|
"data-variant": variant ?? "solid",
|
|
74
76
|
"data-tone": resolved,
|
|
75
77
|
};
|
|
78
|
+
if (href !== undefined) {
|
|
79
|
+
const { Component, props: link } = resolveLink(href, { external, newTab });
|
|
80
|
+
return (_jsx(Component, { ...marks, ...link, className: classes, ...props }));
|
|
81
|
+
}
|
|
76
82
|
const as = renderAs(render, classes, { ...marks, ...props });
|
|
77
83
|
if (as)
|
|
78
84
|
return as;
|
package/dist/blocks/button.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { type ComponentProps } from "react";
|
|
1
2
|
import { Button as ButtonPrimitive } from "@base-ui/react/button";
|
|
2
3
|
import { type VariantProps } from "class-variance-authority";
|
|
4
|
+
import { type LinkBehavior } from "../href.js";
|
|
3
5
|
declare const button: (props?: ({
|
|
4
6
|
tone?: "muted" | "primary" | "secondary" | "brand" | "success" | "warn" | "destructive" | null | undefined;
|
|
5
7
|
size?: "sm" | "xs" | "md" | "lg" | "xl" | null | undefined;
|
|
@@ -15,10 +17,29 @@ export type ButtonLook = VariantProps<typeof button>;
|
|
|
15
17
|
*/
|
|
16
18
|
export declare function buttonVariants(props?: Parameters<typeof button>[0]): string;
|
|
17
19
|
/**
|
|
18
|
-
*
|
|
20
|
+
* `href` makes the button a link — the anchor is the button, and where the href
|
|
21
|
+
* goes is ../href.ts's decision, not the call site's. `render={<a href="…" />}`
|
|
22
|
+
* did this before, and got a bare anchor: no router, so a CTA reloaded the page
|
|
23
|
+
* and lost the view transition, and an off-site href never grew a `rel`.
|
|
24
|
+
*
|
|
25
|
+
* Either way a non-`<button>` element bypasses the primitive on purpose: Base UI
|
|
19
26
|
* always stamps `type="button"` or `role="button"`, and the latter drops an
|
|
20
|
-
* anchor out of screen-reader link navigation.
|
|
21
|
-
*
|
|
27
|
+
* anchor out of screen-reader link navigation. `render` remains for an element
|
|
28
|
+
* that is genuinely neither — a `<label>`, a menu item.
|
|
22
29
|
*/
|
|
23
|
-
export declare function Button({ className, variant, tone, size, icon, pill, render, nativeButton, ...props }: ButtonPrimitive.Props & ButtonLook
|
|
30
|
+
export declare function Button({ className, variant, tone, size, icon, pill, render, nativeButton, href, external, newTab, ...props }: ButtonPrimitive.Props & ButtonLook & LinkBehavior & {
|
|
31
|
+
href?: string;
|
|
32
|
+
/**
|
|
33
|
+
* The one anchor attribute the `href` branch has to name itself. A download
|
|
34
|
+
* is a link that is not a navigation, so it is the case `href` alone cannot
|
|
35
|
+
* express — and `render={<a download />}`, which is how every call site said
|
|
36
|
+
* it before, is exactly what `linkRules()` now flags. Card already takes it,
|
|
37
|
+
* off `ComponentProps<"a">`; Button and Badge are anchors here too.
|
|
38
|
+
*
|
|
39
|
+
* Pair it with `external` for a same-origin route. `Link` steps aside on the
|
|
40
|
+
* click, but it still prefetches the href on viewport entry, which for an
|
|
41
|
+
* export endpoint means running the export to throw the rows away.
|
|
42
|
+
*/
|
|
43
|
+
download?: ComponentProps<"a">["download"];
|
|
44
|
+
}): import("react").JSX.Element;
|
|
24
45
|
export {};
|
package/dist/blocks/button.js
CHANGED
|
@@ -4,8 +4,9 @@ import { Button as ButtonPrimitive } from "@base-ui/react/button";
|
|
|
4
4
|
import { cva } from "class-variance-authority";
|
|
5
5
|
import { cn } from "../cn.js";
|
|
6
6
|
import { renderAs } from "./render-as.js";
|
|
7
|
+
import { resolveLink } from "../href.js";
|
|
7
8
|
import { FOCUS_RING } from "./focus.js";
|
|
8
|
-
import { TONE, TONE_SURFACE, impliedTone } from "../tone.js";
|
|
9
|
+
import { INK_ON_FILL, TONE, TONE_SURFACE, impliedTone } from "../tone.js";
|
|
9
10
|
// ---------------------------------------------------------------------------
|
|
10
11
|
// The control both apps were re-declaring. viably and ssite each carried their
|
|
11
12
|
// own `cva` with its own variant list — `default | secondary | accent |
|
|
@@ -59,7 +60,7 @@ cn(FOCUS_RING, "focus-visible:border-ring"), "active:not-aria-[haspopup]:transla
|
|
|
59
60
|
/** Full-round corners. Marketing surfaces; also every filter chip. */
|
|
60
61
|
pill: { true: "rounded-full", false: "" },
|
|
61
62
|
variant: {
|
|
62
|
-
solid:
|
|
63
|
+
solid: `bg-(--tone-fill) text-(color:--tone-ink) hover:bg-(--tone-fill-hover) ${INK_ON_FILL}`,
|
|
63
64
|
soft: "bg-(--tone-wash) text-(color:--tone-hue) hover:bg-(--tone-wash-hover)",
|
|
64
65
|
outline: "border-(color:--tone-line) bg-background text-(color:--tone-hue) hover:bg-(--tone-wash)",
|
|
65
66
|
ghost: "text-(color:--tone-hue) hover:bg-(--tone-wash)",
|
|
@@ -91,12 +92,17 @@ export function buttonVariants(props = {}) {
|
|
|
91
92
|
return button({ tone: props?.tone ?? impliedTone(props?.variant), ...props });
|
|
92
93
|
}
|
|
93
94
|
/**
|
|
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
|
|
95
101
|
* always stamps `type="button"` or `role="button"`, and the latter drops an
|
|
96
|
-
* anchor out of screen-reader link navigation.
|
|
97
|
-
*
|
|
102
|
+
* anchor out of screen-reader link navigation. `render` remains for an element
|
|
103
|
+
* that is genuinely neither — a `<label>`, a menu item.
|
|
98
104
|
*/
|
|
99
|
-
export function Button({ className, variant, tone, size, icon, pill, render, nativeButton, ...props }) {
|
|
105
|
+
export function Button({ className, variant, tone, size, icon, pill, render, nativeButton, href, external, newTab, ...props }) {
|
|
100
106
|
const resolved = tone ?? impliedTone(variant);
|
|
101
107
|
const classes = cn(button({ variant, tone: resolved, size, icon, pill, className }));
|
|
102
108
|
// The resolved axes, stamped: a child can style off its parent's tone, and a
|
|
@@ -106,6 +112,10 @@ export function Button({ className, variant, tone, size, icon, pill, render, nat
|
|
|
106
112
|
"data-variant": variant ?? "solid",
|
|
107
113
|
"data-tone": resolved,
|
|
108
114
|
};
|
|
115
|
+
if (href !== undefined) {
|
|
116
|
+
const { Component, props: link } = resolveLink(href, { external, newTab });
|
|
117
|
+
return (_jsx(Component, { ...marks, ...link, className: classes, ...props }));
|
|
118
|
+
}
|
|
109
119
|
// `render.type !== "button"`: a plain <button/> still goes through the primitive, which
|
|
110
120
|
// is what supplies the native semantics.
|
|
111
121
|
const as = isValidElement(render) && render.type === "button"
|
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 {};
|
package/dist/blocks/card.js
CHANGED
|
@@ -1,8 +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
3
|
import { FOCUS_RING } from "./focus.js";
|
|
4
|
-
import { toneClass } from "../tone.js";
|
|
5
|
-
import {
|
|
4
|
+
import { INK_ON_CARD, toneClass } from "../tone.js";
|
|
5
|
+
import { resolveLink } from "../href.js";
|
|
6
6
|
/** Two columns from `sm` up: a pair reads as a set rather than two panels. */
|
|
7
7
|
export function Cards({ className, children, ...props }) {
|
|
8
8
|
return (_jsx("div", { className: cn("my-6 grid gap-4 sm:grid-cols-2", className), ...props, children: children }));
|
|
@@ -12,7 +12,7 @@ export function Cards({ className, children, ...props }) {
|
|
|
12
12
|
* grid and `overflow-hidden` clips a bleed image cleanly. Padding is vertical
|
|
13
13
|
* only — the horizontal inset belongs to the slots, so bands can run edge to edge.
|
|
14
14
|
*/
|
|
15
|
-
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 ${INK_ON_CARD} ` +
|
|
16
16
|
"has-[>img:first-child]:pt-0 " +
|
|
17
17
|
"*:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl";
|
|
18
18
|
/**
|
|
@@ -49,10 +49,10 @@ export function CardContent({ className, ...props }) {
|
|
|
49
49
|
/**
|
|
50
50
|
* Takes either shape: `title`/`href` fills the header, or compose the slots
|
|
51
51
|
* directly. Unrecognised props pass through — MDX authors reach for the whole
|
|
52
|
-
* HTML surface.
|
|
53
|
-
*
|
|
52
|
+
* HTML surface. Where the href goes is ../href.ts's call, the same one Button
|
|
53
|
+
* and TypographyLink make.
|
|
54
54
|
*/
|
|
55
|
-
export function Card({ href, className, external, title, description, icon, children, ...rest }) {
|
|
55
|
+
export function Card({ href, className, external, newTab, title, description, icon, children, ...rest }) {
|
|
56
56
|
const header = title || description || icon ? (_jsxs(CardHeader, { children: [icon || title ? (
|
|
57
57
|
// The icon sits on the title's line and is its mark; stacked, it read as a
|
|
58
58
|
// decoration the title happened to follow. `gap-2` is a gap between two
|
|
@@ -67,10 +67,6 @@ export function Card({ href, className, external, title, description, icon, chil
|
|
|
67
67
|
if (!href) {
|
|
68
68
|
return (_jsx("div", { className: cn(CARD_CLASS, className), ...shared, ...rest, children: body }));
|
|
69
69
|
}
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
if (leavesApp) {
|
|
73
|
-
return (_jsx("a", { href: href, className: classes, target: "_blank", rel: "noopener noreferrer", ...shared, ...rest, children: body }));
|
|
74
|
-
}
|
|
75
|
-
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 }));
|
|
76
72
|
}
|
package/dist/blocks/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { Cards, Card, CardHeader, CardTitle, CardDescription, CardContent, } from "./card.js";
|
|
2
2
|
export { Disclosure, DisclosureGroup } from "./accordion.js";
|
|
3
3
|
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent, } from "./interactive-accordion.js";
|
|
4
|
+
export { Anchor } from "./anchor.js";
|
|
4
5
|
export { Callout } from "./callout.js";
|
|
5
6
|
export { Button, buttonVariants, type ButtonLook } from "./button.js";
|
|
6
7
|
export { Badge, badgeVariants, type BadgeLook } from "./badge.js";
|
package/dist/blocks/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { Cards, Card, CardHeader, CardTitle, CardDescription, CardContent, } from "./card.js";
|
|
2
2
|
export { Disclosure, DisclosureGroup } from "./accordion.js";
|
|
3
3
|
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent, } from "./interactive-accordion.js";
|
|
4
|
+
export { Anchor } from "./anchor.js";
|
|
4
5
|
export { Callout } from "./callout.js";
|
|
5
6
|
export { Button, buttonVariants } from "./button.js";
|
|
6
7
|
export { Badge, badgeVariants } from "./badge.js";
|
package/dist/blocks/tabs.d.ts
CHANGED
|
@@ -36,17 +36,17 @@ export type TabItem = {
|
|
|
36
36
|
* An element and not a component, which this briefly also took. `TabGroup` is a client
|
|
37
37
|
* component, so a component reference handed to it from a server page is a function
|
|
38
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
|
-
*
|
|
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
41
|
*/
|
|
42
42
|
icon?: ReactNode;
|
|
43
43
|
content: ReactNode;
|
|
44
44
|
};
|
|
45
45
|
/**
|
|
46
46
|
* The declarative shorthand: the tabs as data. `TabGroup` is to `Tabs` what
|
|
47
|
-
* `DisclosureGroup` is to `Accordion
|
|
48
|
-
*
|
|
49
|
-
*
|
|
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.
|
|
50
50
|
*
|
|
51
51
|
* Everything below the adapter is the same component the product surfaces use, so a tab
|
|
52
52
|
* strip in the docs and one on a dashboard behave identically. The positional
|
package/dist/blocks/tabs.js
CHANGED
|
@@ -100,9 +100,9 @@ function TabIconSlot({ icon, position, }) {
|
|
|
100
100
|
}
|
|
101
101
|
/**
|
|
102
102
|
* The declarative shorthand: the tabs as data. `TabGroup` is to `Tabs` what
|
|
103
|
-
* `DisclosureGroup` is to `Accordion
|
|
104
|
-
*
|
|
105
|
-
*
|
|
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.
|
|
106
106
|
*
|
|
107
107
|
* Everything below the adapter is the same component the product surfaces use, so a tab
|
|
108
108
|
* strip in the docs and one on a dashboard behave identically. The positional
|
package/dist/cjs/eslint.js
CHANGED
|
@@ -9,6 +9,7 @@ exports.colourRules = colourRules;
|
|
|
9
9
|
exports.themeOverrideRules = themeOverrideRules;
|
|
10
10
|
exports.surfaceAsInkRules = surfaceAsInkRules;
|
|
11
11
|
exports.renamedTokenRules = renamedTokenRules;
|
|
12
|
+
exports.linkRules = linkRules;
|
|
12
13
|
exports.typographyRules = typographyRules;
|
|
13
14
|
exports.designRules = designRules;
|
|
14
15
|
exports.designConfig = designConfig;
|
|
@@ -48,22 +49,49 @@ function themeOverrideRules() {
|
|
|
48
49
|
return rule(`/(^| )dark:(${COLOUR_PREFIX})-(${TOKEN})($| )/`, "A `dark:` override on a token means the token is wrong — fix it in theme.css, where one change covers every call site, rather than here. Alpha variants (dark:bg-destructive/20) stay legal: those tune a wash's density, not the token.");
|
|
49
50
|
}
|
|
50
51
|
/**
|
|
51
|
-
* `--muted` is a fill at L92%, so `text-muted`
|
|
52
|
-
* shipped at 17 sites. `text-background`
|
|
52
|
+
* `--muted` is a fill at L92%, so `text-muted` lands at ~1.1:1. Invisible, and
|
|
53
|
+
* it shipped at 17 sites. `text-background` stays legal: inverse ink is a real
|
|
54
|
+
* role.
|
|
53
55
|
*/
|
|
54
56
|
function surfaceAsInkRules() {
|
|
55
57
|
return rule("/(^| )(dark:|hover:|focus:|group-hover:)*text-(muted|card|popover|input)($| )/", "That is a surface token, not an ink — as text it has no defined contrast (text-muted measures ~1.1:1 on a light page). Use text-muted-foreground for secondary ink, text-subtle-foreground for tertiary, or text-card-foreground on a card.");
|
|
56
58
|
}
|
|
57
59
|
/**
|
|
58
60
|
* `-foreground` means the label printed on a fill; `-ink` means the hue as
|
|
59
|
-
* words.
|
|
60
|
-
*
|
|
61
|
-
*
|
|
61
|
+
* words. The eight categorical `-foreground` tokens were always inks, under the
|
|
62
|
+
* other name. The old spellings still resolve, so nothing breaks on the day of
|
|
63
|
+
* the rename; this is what stops them surviving it.
|
|
64
|
+
*
|
|
65
|
+
* `warn` left this list when the status tones gained real on-fill labels:
|
|
66
|
+
* `--warn-foreground` now means what its name says, the ink printed on the warn
|
|
67
|
+
* fill, and `Button tone="warn" variant="solid"` is what reads it.
|
|
62
68
|
*/
|
|
63
|
-
const RENAMED_INKS = "
|
|
69
|
+
const RENAMED_INKS = "terracotta|ochre|moss|fern|sage|stone|fig|cocoa";
|
|
64
70
|
function renamedTokenRules() {
|
|
65
71
|
return rule(`/(^| )(dark:|hover:|focus:|group-hover:)*(text|bg|border|ring|fill|stroke|decoration)-(${RENAMED_INKS})-foreground($| )/`, "That is the deprecated name for the same hue's `-ink`. In this package `-foreground` is the label printed on a fill and `-ink` is the hue used as words, and none of these hues has a printed-on label — they are checked at 4.5:1 against the page, and printing one on its own fill measures about 1.2:1. Use `-ink`.");
|
|
66
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* `render={<a href="…" />}` on a component that takes an `href`. It reads as a
|
|
75
|
+
* styling choice and is a routing one: the cloned anchor skips the router, so
|
|
76
|
+
* the page fully reloads and the view transition is lost, and an off-site href
|
|
77
|
+
* never grows a `rel`. Button, Badge and Card each decide internal vs external
|
|
78
|
+
* from the href itself, so the anchor is never needed and cannot be right more
|
|
79
|
+
* often than the one shared rule is.
|
|
80
|
+
*
|
|
81
|
+
* Narrow on both axes, so it never fires on a line that is correct. Only those
|
|
82
|
+
* three components — `RailLink` deliberately takes a router element through
|
|
83
|
+
* `render`, because its module has to stay importable without Next. And only a
|
|
84
|
+
* bare `<a>`: `render={<Link/>}` is redundant beside `href` but it still routes,
|
|
85
|
+
* so it is not a bug.
|
|
86
|
+
*/
|
|
87
|
+
function linkRules() {
|
|
88
|
+
return [
|
|
89
|
+
{
|
|
90
|
+
selector: 'JSXOpeningElement[name.name=/^(Button|Badge|Card)$/] > JSXAttribute[name.name="render"] > JSXExpressionContainer > JSXElement > JSXOpeningElement[name.name="a"]',
|
|
91
|
+
message: "Pass `href` instead of rendering an anchor. A cloned <a> bypasses the router (full page load, no view transition) and gets no rel on an off-site href; `href` routes through the package's one rule. `render` is for an element that is not a link.",
|
|
92
|
+
},
|
|
93
|
+
];
|
|
94
|
+
}
|
|
67
95
|
function typographyRules({ weights = false, ramp = "text-3xs 10 / text-2xs 11 / text-xs 12 / text-sm 14 / text-base 16 and up", pairing = false, axis = false, } = {}) {
|
|
68
96
|
return [
|
|
69
97
|
// Alpha ink composites against whatever surface it lands on, so its
|
|
@@ -109,7 +137,7 @@ function typographyRules({ weights = false, ramp = "text-3xs 10 / text-2xs 11 /
|
|
|
109
137
|
? [
|
|
110
138
|
{
|
|
111
139
|
selector: 'JSXElement:has(>JSXOpeningElement[name.name="TypographyP"]) ~ JSXElement > JSXOpeningElement[name.name="TypographyProseList"]',
|
|
112
|
-
message:
|
|
140
|
+
message: 'A ui paragraph over a prose list splits one passage across two rungs. Promote the paragraph with TypographyProse, or drop the list to the paragraph\'s rung with TypographyList variant="ui".',
|
|
113
141
|
},
|
|
114
142
|
]
|
|
115
143
|
: []),
|
|
@@ -122,6 +150,7 @@ function designRules({ accents, typography = true, ...type } = {}) {
|
|
|
122
150
|
return [
|
|
123
151
|
...colourRules({ accents }),
|
|
124
152
|
...(typography ? typographyRules(type) : []),
|
|
153
|
+
...linkRules(),
|
|
125
154
|
...themeOverrideRules(),
|
|
126
155
|
...surfaceAsInkRules(),
|
|
127
156
|
...renamedTokenRules(),
|
|
@@ -145,10 +174,7 @@ function designConfig({ files = ["**/*.{ts,tsx,js,jsx}"], ...options } = {}) {
|
|
|
145
174
|
name: "@supertype.ai/foundations/design",
|
|
146
175
|
files,
|
|
147
176
|
rules: {
|
|
148
|
-
"no-restricted-syntax": [
|
|
149
|
-
"error",
|
|
150
|
-
...designRules(options),
|
|
151
|
-
],
|
|
177
|
+
"no-restricted-syntax": ["error", ...designRules(options)],
|
|
152
178
|
},
|
|
153
179
|
},
|
|
154
180
|
];
|