@supertype.ai/foundations 0.1.33 → 0.1.35
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 +1 -1
- package/dist/blocks/bulletin.d.ts +71 -0
- package/dist/blocks/bulletin.js +53 -0
- package/dist/blocks/button.d.ts +2 -2
- package/dist/blocks/colophon.d.ts +48 -0
- package/dist/blocks/colophon.js +82 -0
- package/dist/blocks/index.d.ts +2 -0
- package/dist/blocks/index.js +2 -0
- package/dist/cjs/eslint.js +33 -7
- package/dist/cn.js +35 -1
- package/dist/eslint.d.ts +4 -1
- package/dist/eslint.js +33 -7
- package/dist/essay/essay.js +1 -1
- package/dist/essay/layout.js +1 -1
- package/dist/essay/rail.js +1 -1
- package/dist/typography/header.d.ts +3 -3
- package/dist/typography/paragraph.d.ts +13 -7
- package/dist/typography/paragraph.js +54 -19
- package/llms.txt +8 -2
- package/package.json +1 -1
- package/src/tokens.css +15 -0
- package/src/type.css +22 -2
package/README.md
CHANGED
|
@@ -66,7 +66,7 @@ untagged git dependency resolves to a different commit on a fresh install.
|
|
|
66
66
|
|
|
67
67
|
```jsonc
|
|
68
68
|
// package.json
|
|
69
|
-
"@supertype.ai/foundations": "https://github.com/supertypeai/foundations.git#v0.1.
|
|
69
|
+
"@supertype.ai/foundations": "https://github.com/supertypeai/foundations.git#v0.1.35"
|
|
70
70
|
```
|
|
71
71
|
|
|
72
72
|
</details>
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
/** One cell of the grid. `mark` and `ink` are class names, so they can be any
|
|
3
|
+
* hue the consuming app has: `bg-fern`/`text-fern-ink` from theme.css, or a
|
|
4
|
+
* token of their own. Both optional — a point with no hue is a plain one. */
|
|
5
|
+
export interface BulletinPoint {
|
|
6
|
+
title: ReactNode;
|
|
7
|
+
body: ReactNode;
|
|
8
|
+
/** The dot before the title. A background class: `bg-terracotta`. */
|
|
9
|
+
mark?: string;
|
|
10
|
+
/** The title's ink. A text class: `text-terracotta-ink`. */
|
|
11
|
+
ink?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface BulletinProps {
|
|
14
|
+
/** `card` is the panel. `line` is the same statement at one line: the accent,
|
|
15
|
+
* the action and the sentence, with the grid dropped. */
|
|
16
|
+
variant?: "card" | "line";
|
|
17
|
+
/**
|
|
18
|
+
* Drawn along the top edge of the panel, full width, before the padding
|
|
19
|
+
* starts. `<Ribbon />` is the one that ships; a rule, a gradient or nothing
|
|
20
|
+
* are all fine.
|
|
21
|
+
*/
|
|
22
|
+
accent?: ReactNode;
|
|
23
|
+
/** The kicker over the headline. */
|
|
24
|
+
eyebrow?: ReactNode;
|
|
25
|
+
headline?: ReactNode;
|
|
26
|
+
/** The sentence under the headline on `card`, and the whole of the copy on `line`. */
|
|
27
|
+
lede?: ReactNode;
|
|
28
|
+
/** The grid. One point renders full width; two or more go two-up from `sm`. */
|
|
29
|
+
points?: readonly BulletinPoint[];
|
|
30
|
+
/** The control on the footer rule — a `Button`, a link, a form. */
|
|
31
|
+
action?: ReactNode;
|
|
32
|
+
/** The line opposite the action. A licence, a date, a version. */
|
|
33
|
+
footnote?: ReactNode;
|
|
34
|
+
/** Under the rule, for whatever the panel has left to say. */
|
|
35
|
+
children?: ReactNode;
|
|
36
|
+
className?: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The shell. Every slot is optional, and an omitted one renders nothing rather
|
|
40
|
+
* than an empty box, so the same component covers a full credits panel and a
|
|
41
|
+
* headline with one button under it.
|
|
42
|
+
*/
|
|
43
|
+
export declare function Bulletin({ variant, accent, eyebrow, headline, lede, points, action, footnote, children, className, }: BulletinProps): import("react").JSX.Element;
|
|
44
|
+
/** One band of the ribbon: the class that paints it, and the word shown on hover. */
|
|
45
|
+
export interface RibbonHue {
|
|
46
|
+
name: string;
|
|
47
|
+
fill: string;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The categorical palette in ribbon order: warm through green through cool and
|
|
51
|
+
* back to earth, so the run reads as one spectrum rather than eight swatches.
|
|
52
|
+
*
|
|
53
|
+
* Written out one full classname at a time, never assembled from the name.
|
|
54
|
+
* Tailwind generates the classes it can see as literals, and a template string
|
|
55
|
+
* would leave every one of them purged with nothing to report. A consumer
|
|
56
|
+
* passing hues of their own is subject to the same rule in their own source.
|
|
57
|
+
*/
|
|
58
|
+
export declare const EDITORIAL_INKS: readonly RibbonHue[];
|
|
59
|
+
/**
|
|
60
|
+
* A band of hues, for a `Bulletin`'s `accent`. Decorative, so it is hidden from
|
|
61
|
+
* the reader that cannot see it and carries a `title` for the one that can:
|
|
62
|
+
* hovering names the hue, and the segment widens to show the cut at full size.
|
|
63
|
+
*
|
|
64
|
+
* `h-1.5 w-full` on a panel and `h-1 min-w-20 flex-1 rounded-full` in a row —
|
|
65
|
+
* stated by the caller, since the two shapes have nothing in common but the
|
|
66
|
+
* colours.
|
|
67
|
+
*/
|
|
68
|
+
export declare function Ribbon({ hues, className, }: {
|
|
69
|
+
hues?: readonly RibbonHue[];
|
|
70
|
+
className?: string;
|
|
71
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from "../cn.js";
|
|
3
|
+
import { INK_ON_CARD } from "../tone.js";
|
|
4
|
+
import { TypographyCaption, TypographyLabel, TypographyMuted, } from "../typography/paragraph.js";
|
|
5
|
+
import { TypographyEyebrow, TypographyH3 } from "../typography/header.js";
|
|
6
|
+
/**
|
|
7
|
+
* The shell. Every slot is optional, and an omitted one renders nothing rather
|
|
8
|
+
* than an empty box, so the same component covers a full credits panel and a
|
|
9
|
+
* headline with one button under it.
|
|
10
|
+
*/
|
|
11
|
+
export function Bulletin({ variant = "card", accent, eyebrow, headline, lede, points, action, footnote, children, className, }) {
|
|
12
|
+
if (variant === "line") {
|
|
13
|
+
return (_jsxs("div", { className: cn("flex flex-wrap items-center gap-x-4 gap-y-3", "pt-4", className), children: [action, accent, lede && _jsx(TypographyCaption, { size: "2xs", children: lede })] }));
|
|
14
|
+
}
|
|
15
|
+
const rule = action || footnote;
|
|
16
|
+
return (_jsxs("section", { className: cn("overflow-hidden rounded-xl border border-border",
|
|
17
|
+
// It paints, so it hands down its ink.
|
|
18
|
+
"bg-card", INK_ON_CARD, className), children: [accent, _jsxs("div", { className: "p-6 sm:p-8", children: [eyebrow && (_jsx(TypographyEyebrow, { tone: "subtle", children: eyebrow })), headline && (_jsx(TypographyH3, { className: cn("max-w-lg", eyebrow && "mt-2"), children: headline })), lede && (_jsx(TypographyMuted, { as: "p", className: "mt-2 max-w-xl", children: lede })), points && points.length > 0 && (
|
|
19
|
+
// A lone point takes the width. Two columns holding one of them is a
|
|
20
|
+
// grid drawn around an empty cell.
|
|
21
|
+
_jsx("ul", { className: cn("mt-7 grid gap-6", points.length > 1 && "sm:grid-cols-2"), children: points.map(({ title, body, mark, ink }, i) => (_jsxs("li", { children: [_jsxs(TypographyLabel, { as: "p", size: "xs", className: cn("flex items-center gap-2", ink), children: [mark && (_jsx("span", { "aria-hidden": true, className: cn("size-2 rounded-full", mark) })), title] }), _jsx(TypographyCaption, { as: "p", size: "xs", className: "mt-1.5", children: body })] }, i))) })), rule && (_jsxs("div", { className: "mt-8 flex flex-wrap items-center justify-between gap-4 border-t border-border pt-5", children: [action, footnote && (_jsx(TypographyCaption, { size: "2xs", children: footnote }))] })), children && (_jsx(TypographyCaption, { as: "div", size: "xs", className: cn(rule && "mt-5"), children: children }))] })] }));
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The categorical palette in ribbon order: warm through green through cool and
|
|
25
|
+
* back to earth, so the run reads as one spectrum rather than eight swatches.
|
|
26
|
+
*
|
|
27
|
+
* Written out one full classname at a time, never assembled from the name.
|
|
28
|
+
* Tailwind generates the classes it can see as literals, and a template string
|
|
29
|
+
* would leave every one of them purged with nothing to report. A consumer
|
|
30
|
+
* passing hues of their own is subject to the same rule in their own source.
|
|
31
|
+
*/
|
|
32
|
+
export const EDITORIAL_INKS = [
|
|
33
|
+
{ name: "terracotta", fill: "bg-terracotta" },
|
|
34
|
+
{ name: "ochre", fill: "bg-ochre" },
|
|
35
|
+
{ name: "moss", fill: "bg-moss" },
|
|
36
|
+
{ name: "fern", fill: "bg-fern" },
|
|
37
|
+
{ name: "sage", fill: "bg-sage" },
|
|
38
|
+
{ name: "stone", fill: "bg-stone" },
|
|
39
|
+
{ name: "fig", fill: "bg-fig" },
|
|
40
|
+
{ name: "cocoa", fill: "bg-cocoa" },
|
|
41
|
+
];
|
|
42
|
+
/**
|
|
43
|
+
* A band of hues, for a `Bulletin`'s `accent`. Decorative, so it is hidden from
|
|
44
|
+
* the reader that cannot see it and carries a `title` for the one that can:
|
|
45
|
+
* hovering names the hue, and the segment widens to show the cut at full size.
|
|
46
|
+
*
|
|
47
|
+
* `h-1.5 w-full` on a panel and `h-1 min-w-20 flex-1 rounded-full` in a row —
|
|
48
|
+
* stated by the caller, since the two shapes have nothing in common but the
|
|
49
|
+
* colours.
|
|
50
|
+
*/
|
|
51
|
+
export function Ribbon({ hues = EDITORIAL_INKS, className, }) {
|
|
52
|
+
return (_jsx("div", { "aria-hidden": true, className: cn("flex overflow-hidden", className), children: hues.map(({ name, fill }) => (_jsx("span", { title: name, className: cn("flex-1 transition-[flex-grow] duration-500 ease-out hover:grow-[2.5]", fill) }, name))) }));
|
|
53
|
+
}
|
package/dist/blocks/button.d.ts
CHANGED
|
@@ -4,10 +4,10 @@ import { type VariantProps } from "class-variance-authority";
|
|
|
4
4
|
import { type LinkBehavior } from "../href.js";
|
|
5
5
|
declare const button: (props?: ({
|
|
6
6
|
tone?: "muted" | "primary" | "secondary" | "brand" | "success" | "warn" | "destructive" | null | undefined;
|
|
7
|
-
size?: "sm" | "xs" | "
|
|
7
|
+
size?: "sm" | "xs" | "lg" | "xl" | "md" | null | undefined;
|
|
8
8
|
icon?: boolean | null | undefined;
|
|
9
9
|
pill?: boolean | null | undefined;
|
|
10
|
-
variant?: "
|
|
10
|
+
variant?: "link" | "solid" | "soft" | "outline" | "ghost" | null | undefined;
|
|
11
11
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
12
12
|
export type ButtonLook = VariantProps<typeof button>;
|
|
13
13
|
/**
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
/** Where the mark points. Exported because a footer sometimes wants the bare
|
|
3
|
+
* href — a `<link rel="…">`, a sitemap entry, an analytics label. */
|
|
4
|
+
export declare const FOUNDATIONS_URL = "https://github.com/supertypeai/foundations";
|
|
5
|
+
/**
|
|
6
|
+
* The palette reduced to a small chip. It is small enough for a footer row and
|
|
7
|
+
* still recognisable from the full panel.
|
|
8
|
+
*/
|
|
9
|
+
export declare function FoundationsMark({ className }: {
|
|
10
|
+
className?: string;
|
|
11
|
+
}): import("react").JSX.Element;
|
|
12
|
+
/**
|
|
13
|
+
* The standalone link. It is meant for a footer row or another layout that
|
|
14
|
+
* already has its own structure. Use `Colophon` when the block itself is the
|
|
15
|
+
* thing being placed.
|
|
16
|
+
*/
|
|
17
|
+
export declare function BuiltWithFoundations({ label, className, }: {
|
|
18
|
+
label?: ReactNode;
|
|
19
|
+
className?: string;
|
|
20
|
+
}): import("react").JSX.Element;
|
|
21
|
+
export interface ColophonProps {
|
|
22
|
+
/**
|
|
23
|
+
* `card` is the full panel and `line` is the compact row.
|
|
24
|
+
*/
|
|
25
|
+
variant?: "card" | "line";
|
|
26
|
+
/**
|
|
27
|
+
* The link label. Use this for translations or a different wording.
|
|
28
|
+
*/
|
|
29
|
+
label?: ReactNode;
|
|
30
|
+
/**
|
|
31
|
+
* Extra content for the panel, such as a short note about the site or the
|
|
32
|
+
* people behind it.
|
|
33
|
+
*/
|
|
34
|
+
children?: ReactNode;
|
|
35
|
+
className?: string;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* A preset built on top of `Bulletin`.
|
|
39
|
+
*
|
|
40
|
+
* ```tsx
|
|
41
|
+
* <Colophon /> // the panel
|
|
42
|
+
* <Colophon variant="line" /> // the compact row
|
|
43
|
+
* ```
|
|
44
|
+
*
|
|
45
|
+
* Use `BuiltWithFoundations` for the standalone link, and `Bulletin` if you
|
|
46
|
+
* want the same layout with different copy.
|
|
47
|
+
*/
|
|
48
|
+
export declare function Colophon({ variant, label, children, className, }: ColophonProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from "../cn.js";
|
|
3
|
+
import { Button } from "./button.js";
|
|
4
|
+
import { Bulletin, EDITORIAL_INKS, Ribbon, } from "./bulletin.js";
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// A compact statement block for a footer, a section, or a page.
|
|
7
|
+
//
|
|
8
|
+
// Colophon keeps the name because it matches the role: a short note about the
|
|
9
|
+
// system behind the site. The layout is shared with Bulletin, while the copy
|
|
10
|
+
// here is the package's own preset.
|
|
11
|
+
//
|
|
12
|
+
// The panel uses the editorial inks and the row uses the same mark in a tighter
|
|
13
|
+
// layout. The content is mostly fixed, but the label and children can be
|
|
14
|
+
// replaced for a site that wants its own wording.
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
/** Where the mark points. Exported because a footer sometimes wants the bare
|
|
17
|
+
* href — a `<link rel="…">`, a sitemap entry, an analytics label. */
|
|
18
|
+
export const FOUNDATIONS_URL = "https://github.com/supertypeai/foundations";
|
|
19
|
+
/**
|
|
20
|
+
* The two claims, each paired with a hue from the ribbon above it.
|
|
21
|
+
*
|
|
22
|
+
* They are short statements about how the package is built and how the system
|
|
23
|
+
* is checked.
|
|
24
|
+
*/
|
|
25
|
+
const CLAIMS = [
|
|
26
|
+
{
|
|
27
|
+
mark: "bg-terracotta",
|
|
28
|
+
ink: "text-terracotta-ink",
|
|
29
|
+
title: "Decisions live in one place",
|
|
30
|
+
body: "Utility classes are for styling. The actual decision belongs in the design system. Every type style, tone, and divider on this page comes from one package, so a change is one diff instead of a search through the app.",
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
mark: "bg-sage",
|
|
34
|
+
ink: "text-sage-ink",
|
|
35
|
+
title: "Every colour is measured twice",
|
|
36
|
+
body: "WCAG tells us whether a colour passes the audit. APCA's Lc tells us how it feels in context, since the same contrast can read differently on different backgrounds. Both checks run against the shipped stylesheet in light and dark mode, so CI catches anything that slips.",
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
const HEADLINE = "Designed with intention and mathematical rigor.";
|
|
40
|
+
/** The panel has a paragraph's room under its headline. */
|
|
41
|
+
const LEDE = "Typography primitives, semantic tokens, the essay shell, and contrast checks in one package, enforced in CI.";
|
|
42
|
+
/** The row has a line. Sharing `LEDE` with the panel put a paragraph in a
|
|
43
|
+
* footer, where it wrapped under the ribbon and stopped being a row. */
|
|
44
|
+
const NOTE = "Colors, typography, and blocks, tested and measured.";
|
|
45
|
+
const FOOTNOTE = "Open source design system by Supertype. MIT licensed.";
|
|
46
|
+
/**
|
|
47
|
+
* The palette reduced to a small chip. It is small enough for a footer row and
|
|
48
|
+
* still recognisable from the full panel.
|
|
49
|
+
*/
|
|
50
|
+
export function FoundationsMark({ className }) {
|
|
51
|
+
return (_jsx("span", { "aria-hidden": true, className: cn("grid size-4 shrink-0 grid-cols-4 grid-rows-2 overflow-hidden rounded-[3px]", className), children: EDITORIAL_INKS.map(({ name, fill }) => (_jsx("span", { className: fill }, name))) }));
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The trailing arrow. It sits just under the text and keeps the link visually
|
|
55
|
+
* aligned with the other controls.
|
|
56
|
+
*/
|
|
57
|
+
function Arrow() {
|
|
58
|
+
return (_jsx("svg", { "aria-hidden": "true", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className: "size-3 opacity-60 transition-opacity group-hover:opacity-100", children: _jsx("path", { d: "M7 17 17 7M7 7h10v10" }) }));
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The standalone link. It is meant for a footer row or another layout that
|
|
62
|
+
* already has its own structure. Use `Colophon` when the block itself is the
|
|
63
|
+
* thing being placed.
|
|
64
|
+
*/
|
|
65
|
+
export function BuiltWithFoundations({ label = "Built with Foundations", className, }) {
|
|
66
|
+
return (_jsxs(Button, { href: FOUNDATIONS_URL, variant: "outline", size: "sm", className: cn("group gap-2", className), children: [_jsx(FoundationsMark, { className: "size-3.5" }), label, _jsx(Arrow, {})] }));
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* A preset built on top of `Bulletin`.
|
|
70
|
+
*
|
|
71
|
+
* ```tsx
|
|
72
|
+
* <Colophon /> // the panel
|
|
73
|
+
* <Colophon variant="line" /> // the compact row
|
|
74
|
+
* ```
|
|
75
|
+
*
|
|
76
|
+
* Use `BuiltWithFoundations` for the standalone link, and `Bulletin` if you
|
|
77
|
+
* want the same layout with different copy.
|
|
78
|
+
*/
|
|
79
|
+
export function Colophon({ variant = "card", label, children, className, }) {
|
|
80
|
+
const line = variant === "line";
|
|
81
|
+
return (_jsx(Bulletin, { variant: variant, accent: _jsx(Ribbon, { className: line ? "h-1 min-w-20 flex-1 rounded-full" : "h-1.5 w-full" }), eyebrow: "Colophon", headline: HEADLINE, lede: line ? NOTE : LEDE, points: CLAIMS, action: _jsx(BuiltWithFoundations, { label: label }), footnote: FOOTNOTE, className: className, children: children }));
|
|
82
|
+
}
|
package/dist/blocks/index.d.ts
CHANGED
|
@@ -8,3 +8,5 @@ export { Badge, badgeVariants, type BadgeLook } from "./badge.js";
|
|
|
8
8
|
export { Tabs, TabsList, TabsTrigger, TabsContent, TabGroup, type TabItem, } from "./tabs.js";
|
|
9
9
|
export { SEGMENT } from "./segment.js";
|
|
10
10
|
export { Steps, Step } from "./steps.js";
|
|
11
|
+
export { Bulletin, Ribbon, EDITORIAL_INKS, type BulletinProps, type BulletinPoint, type RibbonHue, } from "./bulletin.js";
|
|
12
|
+
export { Colophon, BuiltWithFoundations, FoundationsMark, FOUNDATIONS_URL, type ColophonProps, } from "./colophon.js";
|
package/dist/blocks/index.js
CHANGED
|
@@ -8,3 +8,5 @@ export { Badge, badgeVariants } from "./badge.js";
|
|
|
8
8
|
export { Tabs, TabsList, TabsTrigger, TabsContent, TabGroup, } from "./tabs.js";
|
|
9
9
|
export { SEGMENT } from "./segment.js";
|
|
10
10
|
export { Steps, Step } from "./steps.js";
|
|
11
|
+
export { Bulletin, Ribbon, EDITORIAL_INKS, } from "./bulletin.js";
|
|
12
|
+
export { Colophon, BuiltWithFoundations, FoundationsMark, FOUNDATIONS_URL, } from "./colophon.js";
|
package/dist/cjs/eslint.js
CHANGED
|
@@ -54,7 +54,20 @@ function themeOverrideRules() {
|
|
|
54
54
|
* role.
|
|
55
55
|
*/
|
|
56
56
|
function surfaceAsInkRules() {
|
|
57
|
-
return
|
|
57
|
+
return [
|
|
58
|
+
...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."),
|
|
59
|
+
// The three fills with no legitimate use as a foreground, glyph or word.
|
|
60
|
+
// Each is a background whose lightness is chosen to hold a label, so read as
|
|
61
|
+
// ink against the page it lands under the tertiary rung: in dark, secondary
|
|
62
|
+
// measures 2.77:1, primary 3.36:1 and success 3.79:1, where
|
|
63
|
+
// `--subtle-foreground` is 6.75:1. An accent quieter than the quietest ink is
|
|
64
|
+
// the bug, and it reads the same way on an icon as it does in a sentence.
|
|
65
|
+
//
|
|
66
|
+
// Unconditional, unlike the opt-in rules above, because the sweep is done in
|
|
67
|
+
// both apps and there is no correct call site left to grandfather. `warn` and
|
|
68
|
+
// `info` stay out: their fills are bright enough to read (7.68:1 and 6.10:1).
|
|
69
|
+
...rule("/(^| )(dark:|hover:|focus:|group-hover:)*text-(primary|secondary|success)($| )/", "That is a fill token used as ink. A fill's lightness is chosen to hold a label printed on it, so against the page it reads under the tertiary ink in dark (primary 3.36:1, secondary 2.77:1, success 3.79:1). Each ships an `-ink` cut checked at 4.5:1 against the page. Add `-ink`."),
|
|
70
|
+
];
|
|
58
71
|
}
|
|
59
72
|
/**
|
|
60
73
|
* `-foreground` means the label printed on a fill; `-ink` means the hue as
|
|
@@ -92,7 +105,7 @@ function linkRules() {
|
|
|
92
105
|
},
|
|
93
106
|
];
|
|
94
107
|
}
|
|
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, } = {}) {
|
|
108
|
+
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, leading = false, } = {}) {
|
|
96
109
|
return [
|
|
97
110
|
// Alpha ink composites against whatever surface it lands on, so its
|
|
98
111
|
// contrast is unmeasurable. The ink tokens are measured.
|
|
@@ -103,11 +116,10 @@ function typographyRules({ weights = false, ramp = "text-3xs 10 / text-2xs 11 /
|
|
|
103
116
|
...rule("/(^| )text-\\[\\d+px\\]/", `Arbitrary font sizes bypass the type ramp. Use a rung (${ramp}).`),
|
|
104
117
|
// A primitive that owns a size axis, reached past for a class that does the
|
|
105
118
|
// same thing. The class wins on the page, so nothing looks wrong — what is
|
|
106
|
-
// lost is everything else the axis carries: `
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
// literal gets the size and silently drops the rest.
|
|
119
|
+
// lost is everything else the axis carries: `TypographyStat` pairs its rungs
|
|
120
|
+
// with the heading ladder, so a figure and the heading beside it retune
|
|
121
|
+
// together on an editorial surface, where a literal stays put. A literal
|
|
122
|
+
// gets the size and silently drops the rest.
|
|
111
123
|
//
|
|
112
124
|
// Matching the class node INSIDE the attribute, rather than the className
|
|
113
125
|
// string on its own, lets this name the component. It reaches into `cn()`
|
|
@@ -124,6 +136,20 @@ function typographyRules({ weights = false, ramp = "text-3xs 10 / text-2xs 11 /
|
|
|
124
136
|
})),
|
|
125
137
|
]
|
|
126
138
|
: []),
|
|
139
|
+
// Leading stated at a call site, on a primitive whose rung already carries
|
|
140
|
+
// one. The ramp is what an app retunes, and a class pins past it: the pinned
|
|
141
|
+
// value follows the component onto a surface tuned for different sizes and
|
|
142
|
+
// reports nothing when it no longer fits. Where a role genuinely needs a
|
|
143
|
+
// number the rung cannot give, the reading paragraph and the value being the
|
|
144
|
+
// two, the primitive states it and every call site inherits the decision.
|
|
145
|
+
...(leading
|
|
146
|
+
? [
|
|
147
|
+
...["Literal[value", "TemplateElement[value.raw"].map((node) => ({
|
|
148
|
+
selector: `JSXOpeningElement[name.name=/^Typography[A-Z]/] JSXAttribute[name.name="className"] ${node}=/(^| )leading-/]`,
|
|
149
|
+
message: "Leading comes from the rung, which is what an app retunes. Move the rung (size=), or state it on the primitive if every call site wants it.",
|
|
150
|
+
})),
|
|
151
|
+
]
|
|
152
|
+
: []),
|
|
127
153
|
// Two valid primitives forming an invalid pair, which the value rules above
|
|
128
154
|
// cannot see: `<TypographyP>` is the 14px interface rung, and the list under
|
|
129
155
|
// it reads at the prose rung, so one passage lands two rungs apart.
|
package/dist/cn.js
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
import { clsx } from "clsx";
|
|
2
|
-
import {
|
|
2
|
+
import { extendTailwindMerge } from "tailwind-merge";
|
|
3
|
+
/**
|
|
4
|
+
* `text-h1`…`text-h4` are this package's rungs, declared in type.css as
|
|
5
|
+
* `--text-h1`…`--text-h4`. tailwind-merge has never heard of them.
|
|
6
|
+
*
|
|
7
|
+
* Faced with a `text-*` it does not know, it has to pick a class group, and
|
|
8
|
+
* `h3` matches neither a t-shirt size nor an arbitrary value — so it falls
|
|
9
|
+
* through to `text-color`, which accepts any token. tailwind-merge therefore
|
|
10
|
+
* believed `text-h3` was a colour, and deleted whichever colour stood beside
|
|
11
|
+
* it. Measured, not assumed:
|
|
12
|
+
*
|
|
13
|
+
* twMerge("text-red-500 text-h3") → "text-h3" // collided
|
|
14
|
+
* twMerge("text-sm text-h3") → "text-sm text-h3" // did not
|
|
15
|
+
*
|
|
16
|
+
* Which side lost depended on `cva` key order, so it broke in both directions
|
|
17
|
+
* at once. `HEADING_BASE` states the ink before the rung, so every heading on
|
|
18
|
+
* the default variant shipped with no colour class at all and quietly inherited
|
|
19
|
+
* whatever painted above it — which is right on a page and wrong on every
|
|
20
|
+
* surface that hands down an ink, the contract `INK_ON_CARD` and friends exist
|
|
21
|
+
* to keep. `statVariants` orders `size` before `tone`, so a
|
|
22
|
+
* `<TypographyStat size="card" tone="muted">` lost its rung instead and
|
|
23
|
+
* rendered at the inherited size.
|
|
24
|
+
*
|
|
25
|
+
* `text-2xs` and `text-3xs` are custom rungs too and were never affected: they
|
|
26
|
+
* match tailwind-merge's t-shirt-size pattern and land in `font-size` on their
|
|
27
|
+
* own. Only the four heading rungs fall through, which is why this list is
|
|
28
|
+
* exactly four names long.
|
|
29
|
+
*
|
|
30
|
+
* Registering them leaves every real conflict intact — two rungs still collide
|
|
31
|
+
* with each other and with the body ramp, two colours still collide — and stops
|
|
32
|
+
* the one that was never a conflict. test/heading-ink.test.tsx holds it.
|
|
33
|
+
*/
|
|
34
|
+
const twMerge = extendTailwindMerge({
|
|
35
|
+
extend: { classGroups: { "font-size": [{ text: ["h1", "h2", "h3", "h4"] }] } },
|
|
36
|
+
});
|
|
3
37
|
export function cn(...inputs) {
|
|
4
38
|
return twMerge(clsx(inputs));
|
|
5
39
|
}
|
package/dist/eslint.d.ts
CHANGED
|
@@ -59,8 +59,11 @@ export interface TypographyOptions {
|
|
|
59
59
|
* migrated, and that migration is the intended end state.
|
|
60
60
|
*/
|
|
61
61
|
axis?: boolean;
|
|
62
|
+
/** Flag a leading class on a typography primitive. Off by default: an app
|
|
63
|
+
* adopting it has a backlog to clear first. */
|
|
64
|
+
leading?: boolean;
|
|
62
65
|
}
|
|
63
|
-
export declare function typographyRules({ weights, ramp, pairing, axis, }?: TypographyOptions): RestrictedSyntax[];
|
|
66
|
+
export declare function typographyRules({ weights, ramp, pairing, axis, leading, }?: TypographyOptions): RestrictedSyntax[];
|
|
64
67
|
/**
|
|
65
68
|
* Every design rule, as one list.
|
|
66
69
|
*
|
package/dist/eslint.js
CHANGED
|
@@ -44,7 +44,20 @@ export function themeOverrideRules() {
|
|
|
44
44
|
* role.
|
|
45
45
|
*/
|
|
46
46
|
export function surfaceAsInkRules() {
|
|
47
|
-
return
|
|
47
|
+
return [
|
|
48
|
+
...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."),
|
|
49
|
+
// The three fills with no legitimate use as a foreground, glyph or word.
|
|
50
|
+
// Each is a background whose lightness is chosen to hold a label, so read as
|
|
51
|
+
// ink against the page it lands under the tertiary rung: in dark, secondary
|
|
52
|
+
// measures 2.77:1, primary 3.36:1 and success 3.79:1, where
|
|
53
|
+
// `--subtle-foreground` is 6.75:1. An accent quieter than the quietest ink is
|
|
54
|
+
// the bug, and it reads the same way on an icon as it does in a sentence.
|
|
55
|
+
//
|
|
56
|
+
// Unconditional, unlike the opt-in rules above, because the sweep is done in
|
|
57
|
+
// both apps and there is no correct call site left to grandfather. `warn` and
|
|
58
|
+
// `info` stay out: their fills are bright enough to read (7.68:1 and 6.10:1).
|
|
59
|
+
...rule("/(^| )(dark:|hover:|focus:|group-hover:)*text-(primary|secondary|success)($| )/", "That is a fill token used as ink. A fill's lightness is chosen to hold a label printed on it, so against the page it reads under the tertiary ink in dark (primary 3.36:1, secondary 2.77:1, success 3.79:1). Each ships an `-ink` cut checked at 4.5:1 against the page. Add `-ink`."),
|
|
60
|
+
];
|
|
48
61
|
}
|
|
49
62
|
/**
|
|
50
63
|
* `-foreground` means the label printed on a fill; `-ink` means the hue as
|
|
@@ -82,7 +95,7 @@ export function linkRules() {
|
|
|
82
95
|
},
|
|
83
96
|
];
|
|
84
97
|
}
|
|
85
|
-
export 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, } = {}) {
|
|
98
|
+
export 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, leading = false, } = {}) {
|
|
86
99
|
return [
|
|
87
100
|
// Alpha ink composites against whatever surface it lands on, so its
|
|
88
101
|
// contrast is unmeasurable. The ink tokens are measured.
|
|
@@ -93,11 +106,10 @@ export function typographyRules({ weights = false, ramp = "text-3xs 10 / text-2x
|
|
|
93
106
|
...rule("/(^| )text-\\[\\d+px\\]/", `Arbitrary font sizes bypass the type ramp. Use a rung (${ramp}).`),
|
|
94
107
|
// A primitive that owns a size axis, reached past for a class that does the
|
|
95
108
|
// same thing. The class wins on the page, so nothing looks wrong — what is
|
|
96
|
-
// lost is everything else the axis carries: `
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
// literal gets the size and silently drops the rest.
|
|
109
|
+
// lost is everything else the axis carries: `TypographyStat` pairs its rungs
|
|
110
|
+
// with the heading ladder, so a figure and the heading beside it retune
|
|
111
|
+
// together on an editorial surface, where a literal stays put. A literal
|
|
112
|
+
// gets the size and silently drops the rest.
|
|
101
113
|
//
|
|
102
114
|
// Matching the class node INSIDE the attribute, rather than the className
|
|
103
115
|
// string on its own, lets this name the component. It reaches into `cn()`
|
|
@@ -114,6 +126,20 @@ export function typographyRules({ weights = false, ramp = "text-3xs 10 / text-2x
|
|
|
114
126
|
})),
|
|
115
127
|
]
|
|
116
128
|
: []),
|
|
129
|
+
// Leading stated at a call site, on a primitive whose rung already carries
|
|
130
|
+
// one. The ramp is what an app retunes, and a class pins past it: the pinned
|
|
131
|
+
// value follows the component onto a surface tuned for different sizes and
|
|
132
|
+
// reports nothing when it no longer fits. Where a role genuinely needs a
|
|
133
|
+
// number the rung cannot give, the reading paragraph and the value being the
|
|
134
|
+
// two, the primitive states it and every call site inherits the decision.
|
|
135
|
+
...(leading
|
|
136
|
+
? [
|
|
137
|
+
...["Literal[value", "TemplateElement[value.raw"].map((node) => ({
|
|
138
|
+
selector: `JSXOpeningElement[name.name=/^Typography[A-Z]/] JSXAttribute[name.name="className"] ${node}=/(^| )leading-/]`,
|
|
139
|
+
message: "Leading comes from the rung, which is what an app retunes. Move the rung (size=), or state it on the primitive if every call site wants it.",
|
|
140
|
+
})),
|
|
141
|
+
]
|
|
142
|
+
: []),
|
|
117
143
|
// Two valid primitives forming an invalid pair, which the value rules above
|
|
118
144
|
// cannot see: `<TypographyP>` is the 14px interface rung, and the list under
|
|
119
145
|
// it reads at the prose rung, so one passage lands two rungs apart.
|
package/dist/essay/essay.js
CHANGED
|
@@ -62,7 +62,7 @@ export function createEssay({ Reveal = PlainReveal, Glow = NoGlow, } = {}) {
|
|
|
62
62
|
* An ordered list says that without an arrow — the ordinal is the ornament.
|
|
63
63
|
*/
|
|
64
64
|
function EssayMovements({ items }) {
|
|
65
|
-
return (_jsx("ol", { className: "flex flex-col", children: items.map(({ title, body }, i) => (_jsxs("li", { className: "border-t border-border/60 py-8 first:border-t-0 first:pt-0 last:pb-0 sm:grid sm:grid-cols-[3.5rem_minmax(0,1fr)] sm:gap-6", children: [_jsx(TypographyCaption, { className: "block pt-2 font-mono tabular-nums text-primary max-sm:mb-2", children: String(i + 1).padStart(2, "0") }), _jsxs("div", { className: "flex flex-col gap-3", children: [_jsx(TypographyH3, { children: title }), body] })] }, title))) }));
|
|
65
|
+
return (_jsx("ol", { className: "flex flex-col", children: items.map(({ title, body }, i) => (_jsxs("li", { className: "border-t border-border/60 py-8 first:border-t-0 first:pt-0 last:pb-0 sm:grid sm:grid-cols-[3.5rem_minmax(0,1fr)] sm:gap-6", children: [_jsx(TypographyCaption, { className: "block pt-2 font-mono tabular-nums text-primary-ink max-sm:mb-2", children: String(i + 1).padStart(2, "0") }), _jsxs("div", { className: "flex flex-col gap-3", children: [_jsx(TypographyH3, { children: title }), body] })] }, title))) }));
|
|
66
66
|
}
|
|
67
67
|
/**
|
|
68
68
|
* A reference document as data: the index derives from the sections, so a
|
package/dist/essay/layout.js
CHANGED
|
@@ -105,5 +105,5 @@ export function ReadTime({ minutes, icon: Icon, className, }) {
|
|
|
105
105
|
export function TagPills({ tags, className, }) {
|
|
106
106
|
if (tags.length === 0)
|
|
107
107
|
return null;
|
|
108
|
-
return (_jsx("span", { className: cn("flex flex-wrap gap-1.5", className), children: tags.map((tag) => (_jsx("span", { className: "rounded-full bg-primary/15 px-2.5 py-0.5 text-xs font-medium text-primary", children: tag }, tag))) }));
|
|
108
|
+
return (_jsx("span", { className: cn("flex flex-wrap gap-1.5", className), children: tags.map((tag) => (_jsx("span", { className: "rounded-full bg-primary/15 px-2.5 py-0.5 text-xs font-medium text-primary-ink", children: tag }, tag))) }));
|
|
109
109
|
}
|
package/dist/essay/rail.js
CHANGED
|
@@ -18,7 +18,7 @@ export function RailLink({ active = false, nested = false, className, children,
|
|
|
18
18
|
// inactive neighbour keeps its line and nothing shifts when the active
|
|
19
19
|
// item changes.
|
|
20
20
|
"before:absolute before:inset-y-1 before:-left-px before:w-0.5 before:rounded-full before:bg-primary before:opacity-0 before:transition-opacity", active
|
|
21
|
-
? "font-medium text-primary before:opacity-100"
|
|
21
|
+
? "font-medium text-primary-ink before:opacity-100"
|
|
22
22
|
: "text-muted-foreground hover:text-foreground", className),
|
|
23
23
|
children,
|
|
24
24
|
};
|
|
@@ -31,11 +31,11 @@ import { type WithAs } from "./as.js";
|
|
|
31
31
|
*/
|
|
32
32
|
export declare const headingFace = "font-heading font-[number:var(--heading-weight)]";
|
|
33
33
|
declare const h1Variants: (props?: ({
|
|
34
|
-
variant?: "
|
|
34
|
+
variant?: "display" | "default" | null | undefined;
|
|
35
35
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
36
36
|
export declare function TypographyH1({ className, variant, children, ...props }: React.ComponentProps<"h1"> & VariantProps<typeof h1Variants>): import("react").JSX.Element;
|
|
37
37
|
declare const h2Variants: (props?: ({
|
|
38
|
-
variant?: "
|
|
38
|
+
variant?: "display" | "default" | null | undefined;
|
|
39
39
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
40
40
|
/** The h2 ramp as a class, for a caller that must render its own element. */
|
|
41
41
|
export declare const headingClass: (variant?: VariantProps<typeof h2Variants>["variant"]) => string;
|
|
@@ -48,7 +48,7 @@ export declare function TypographyH2({ className, variant, divider, children, ..
|
|
|
48
48
|
divider?: boolean;
|
|
49
49
|
}): import("react").JSX.Element;
|
|
50
50
|
declare const h3Variants: (props?: ({
|
|
51
|
-
variant?: "
|
|
51
|
+
variant?: "display" | "default" | null | undefined;
|
|
52
52
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
53
53
|
export declare function TypographyH3({ className, variant, children, ...props }: React.ComponentProps<"h3"> & VariantProps<typeof h3Variants>): import("react").JSX.Element;
|
|
54
54
|
/** The card / panel title: 14px in the product, 20 on an editorial surface. */
|
|
@@ -55,11 +55,11 @@ export declare function TypographyProseList(props: Preset<ListProps, typeof PROS
|
|
|
55
55
|
* `sm` is the default because meta is separated from body by ink, not size;
|
|
56
56
|
* the smaller rungs are a deliberate step down, not the norm.
|
|
57
57
|
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
58
|
+
* Leading comes from the rung, as it does for every other primitive here. Two
|
|
59
|
+
* of these rungs used to pin a fixed ratio on top of the ramp, which held a
|
|
60
|
+
* caption apart from the label beside it and overrode the retune an editorial
|
|
61
|
+
* subtree had just made. An app that wants more air under a wrapped caption
|
|
62
|
+
* moves the rung, which is where the rest of the page reads its leading from.
|
|
63
63
|
*
|
|
64
64
|
* `inherit` is the parenthetical inside a heading, an eyebrow or a stat. It
|
|
65
65
|
* takes the size of whatever set it and resets the weight, because the only
|
|
@@ -108,13 +108,19 @@ export declare function TypographyLabel({ className, size, as, children, ...prop
|
|
|
108
108
|
*
|
|
109
109
|
* Tabular is right in a column and wrong in a headline, so it is an axis rather
|
|
110
110
|
* than a constant. Keep `tabular` anywhere a value updates in place.
|
|
111
|
+
*
|
|
112
|
+
* Leading is the one thing here the rung does not decide. A stat sits on the
|
|
113
|
+
* body rungs, whose leading is room for the line that follows, and a value has
|
|
114
|
+
* no line following it: the ratio lands as dead space arguing with the padding
|
|
115
|
+
* the tile around it already sets. Pinned tight, for the reason a badge is.
|
|
111
116
|
*/
|
|
112
117
|
declare const statVariants: (props?: ({
|
|
113
|
-
size?: "inherit" | "page" | "
|
|
118
|
+
size?: "display" | "base" | "section" | "inherit" | "page" | "sm" | "xs" | "2xs" | "3xs" | "lg" | "xl" | "2xl" | "3xl" | "card" | "panel" | null | undefined;
|
|
119
|
+
tone?: "muted" | "default" | null | undefined;
|
|
114
120
|
figures?: "tabular" | "proportional" | null | undefined;
|
|
115
121
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
116
122
|
export type StatVariants = VariantProps<typeof statVariants>;
|
|
117
|
-
export declare function TypographyStat({ className, size, figures, children, ...props }: ComponentProps<"span"> & StatVariants): import("react").JSX.Element;
|
|
123
|
+
export declare function TypographyStat({ className, size, figures, tone, children, ...props }: ComponentProps<"span"> & StatVariants): import("react").JSX.Element;
|
|
118
124
|
/**
|
|
119
125
|
* A run of code inside a sentence: a command, a field name, a trigger.
|
|
120
126
|
*
|
|
@@ -86,11 +86,11 @@ export function TypographyProseList(props) {
|
|
|
86
86
|
* `sm` is the default because meta is separated from body by ink, not size;
|
|
87
87
|
* the smaller rungs are a deliberate step down, not the norm.
|
|
88
88
|
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
89
|
+
* Leading comes from the rung, as it does for every other primitive here. Two
|
|
90
|
+
* of these rungs used to pin a fixed ratio on top of the ramp, which held a
|
|
91
|
+
* caption apart from the label beside it and overrode the retune an editorial
|
|
92
|
+
* subtree had just made. An app that wants more air under a wrapped caption
|
|
93
|
+
* moves the rung, which is where the rest of the page reads its leading from.
|
|
94
94
|
*
|
|
95
95
|
* `inherit` is the parenthetical inside a heading, an eyebrow or a stat. It
|
|
96
96
|
* takes the size of whatever set it and resets the weight, because the only
|
|
@@ -100,8 +100,8 @@ export function TypographyProseList(props) {
|
|
|
100
100
|
const captionVariants = cva("text-[color:var(--ink-muted,var(--muted-foreground))]", {
|
|
101
101
|
variants: {
|
|
102
102
|
size: {
|
|
103
|
-
sm: "text-sm
|
|
104
|
-
xs: "text-xs
|
|
103
|
+
sm: "text-sm",
|
|
104
|
+
xs: "text-xs",
|
|
105
105
|
"2xs": "text-2xs",
|
|
106
106
|
inherit: "font-normal",
|
|
107
107
|
},
|
|
@@ -158,27 +158,62 @@ export function TypographyLabel({ className, size, as, children, ...props }) {
|
|
|
158
158
|
*
|
|
159
159
|
* Tabular is right in a column and wrong in a headline, so it is an axis rather
|
|
160
160
|
* than a constant. Keep `tabular` anywhere a value updates in place.
|
|
161
|
+
*
|
|
162
|
+
* Leading is the one thing here the rung does not decide. A stat sits on the
|
|
163
|
+
* body rungs, whose leading is room for the line that follows, and a value has
|
|
164
|
+
* no line following it: the ratio lands as dead space arguing with the padding
|
|
165
|
+
* the tile around it already sets. Pinned tight, for the reason a badge is.
|
|
161
166
|
*/
|
|
162
|
-
const statVariants = cva("
|
|
167
|
+
const statVariants = cva("tracking-tight leading-none", {
|
|
163
168
|
variants: {
|
|
164
169
|
/**
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
+
* Two ladders, because a figure sits in one of two places and they scale
|
|
171
|
+
* apart on an editorial surface. The rung names are the body ramp: a value
|
|
172
|
+
* in a table cell, a chip or a tile, beside interface copy it should step
|
|
173
|
+
* with. The surface names ride the heading ladder, for a figure that is
|
|
174
|
+
* itself the headline, so it and the heading beside it retune together.
|
|
170
175
|
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
176
|
+
* The heading half shipped alone and covered three call sites in eighteen.
|
|
177
|
+
* The other fifteen wanted a body rung, could not say so, and spelled a
|
|
178
|
+
* class instead, which takes the size and drops everything else the axis
|
|
179
|
+
* carries.
|
|
180
|
+
*
|
|
181
|
+
* `inherit` is the default and writes nothing: a stat inside a heading or a
|
|
182
|
+
* sentence takes the size that set it.
|
|
174
183
|
*/
|
|
175
184
|
size: {
|
|
176
185
|
inherit: "",
|
|
186
|
+
"3xs": "text-3xs",
|
|
187
|
+
"2xs": "text-2xs",
|
|
188
|
+
xs: "text-xs",
|
|
189
|
+
sm: "text-sm",
|
|
190
|
+
base: "text-base",
|
|
191
|
+
lg: "text-lg",
|
|
192
|
+
xl: "text-xl",
|
|
193
|
+
"2xl": "text-2xl",
|
|
194
|
+
"3xl": "text-3xl",
|
|
177
195
|
card: "text-h4",
|
|
178
196
|
panel: "text-h3",
|
|
197
|
+
section: "text-h2",
|
|
179
198
|
page: "text-h1",
|
|
180
199
|
display: "text-6xl font-black",
|
|
181
200
|
},
|
|
201
|
+
/**
|
|
202
|
+
* How loud the value is. `muted` is the qualifier that follows a figure,
|
|
203
|
+
* "of 2,000" beside "1,284": a value, so it keeps the tight leading, and
|
|
204
|
+
* quiet, so it does not compete with what it qualifies. Unweighted for the
|
|
205
|
+
* reason the caption is, that colour and weight both saying "secondary" is
|
|
206
|
+
* one of them arguing with the other.
|
|
207
|
+
*
|
|
208
|
+
* `default` states no ink on purpose. A stat takes the ink around it, which
|
|
209
|
+
* inside a filled control is that control's label and in a muted row is the
|
|
210
|
+
* row's, and naming one here would repaint every stat that inherits a
|
|
211
|
+
* quieter ink deliberately.
|
|
212
|
+
*/
|
|
213
|
+
tone: {
|
|
214
|
+
default: "font-semibold",
|
|
215
|
+
muted: "font-normal text-[color:var(--ink-muted,var(--muted-foreground))]",
|
|
216
|
+
},
|
|
182
217
|
figures: {
|
|
183
218
|
/** Even advances stop a value jittering as it refreshes. */
|
|
184
219
|
tabular: "tabular-nums",
|
|
@@ -186,10 +221,10 @@ const statVariants = cva("font-semibold tracking-tight", {
|
|
|
186
221
|
proportional: "proportional-nums",
|
|
187
222
|
},
|
|
188
223
|
},
|
|
189
|
-
defaultVariants: { size: "inherit", figures: "tabular" },
|
|
224
|
+
defaultVariants: { size: "inherit", figures: "tabular", tone: "default" },
|
|
190
225
|
});
|
|
191
|
-
export function TypographyStat({ className, size, figures, children, ...props }) {
|
|
192
|
-
return (_jsx("span", { className: cn(statVariants({ size, figures }), className), ...props, children: children }));
|
|
226
|
+
export function TypographyStat({ className, size, figures, tone, children, ...props }) {
|
|
227
|
+
return (_jsx("span", { className: cn(statVariants({ size, figures, tone }), className), ...props, children: children }));
|
|
193
228
|
}
|
|
194
229
|
/**
|
|
195
230
|
* A run of code inside a sentence: a command, a field name, a trigger.
|
package/llms.txt
CHANGED
|
@@ -57,6 +57,9 @@ silently. Full reference: https://github.com/supertypeai/foundations
|
|
|
57
57
|
| a link card, or a grid of them | `Card`, `Cards` | `/blocks` |
|
|
58
58
|
| an aside, warning or note | `Callout` | `/blocks` |
|
|
59
59
|
| numbered instructions | `Steps`, `Step` | `/blocks` |
|
|
60
|
+
| an announcement, release note or credits panel | `Bulletin` | `/blocks` |
|
|
61
|
+
| a compact statement block for a page, section, or footer | `Colophon` | `/blocks` |
|
|
62
|
+
| the same statement as one item in a footer row | `BuiltWithFoundations` | `/blocks` |
|
|
60
63
|
| an FAQ or collapsible, no JS | `Disclosure`, `DisclosureGroup` | `/blocks` |
|
|
61
64
|
| an animated accordion | `Accordion` (client) | `/blocks` |
|
|
62
65
|
| tabbed content | `TabGroup` when the tabs are data, `Tabs` to compose by hand (client) | `/blocks` |
|
|
@@ -73,7 +76,7 @@ silently. Full reference: https://github.com/supertypeai/foundations
|
|
|
73
76
|
| import | exports |
|
|
74
77
|
|---|---|
|
|
75
78
|
| `@supertype.ai/foundations` | `cn`, `TypographyH1`, `TypographyH2`, `TypographyH3`, `TypographyH4`, `TypographyEyebrow`, `TypographyP`, `TypographyMuted`, `TypographyProse`, `TypographyList`, `TypographyProseList`, `TypographyCaption`, `TypographySmall`, `TypographyLabel`, `TypographyStat`, `TypographyInlineCode`, `TypographyLink`, `TypographyHighlight`, `headingClass`, `headingFace`, `eyebrowClass`, `toneClass`, `impliedTone`, `resolveLink`, `isExternalHref`. Types: `TypographyTag`, `ParagraphVariants`, `ListProps`, `CaptionVariants`, `LabelVariants`, `StatVariants`, `Tone`, `HighlightTone`, `LinkBehavior`, `ResolvedLink` |
|
|
76
|
-
| `@supertype.ai/foundations/blocks` | `Anchor`, `Cards`, `Card`, `CardHeader`, `CardTitle`, `CardDescription`, `CardContent`, `Callout`, `Button`, `buttonVariants`, `Badge`, `badgeVariants`, `Steps`, `Step`, `Disclosure`, `DisclosureGroup`, `Accordion`, `AccordionItem`, `AccordionTrigger`, `AccordionContent`, `Tabs`, `TabsList`, `TabsTrigger`, `TabsContent`, `TabGroup`, `SEGMENT`. Types: `ButtonLook`, `BadgeLook`, `TabItem` |
|
|
79
|
+
| `@supertype.ai/foundations/blocks` | `Anchor`, `Cards`, `Card`, `CardHeader`, `CardTitle`, `CardDescription`, `CardContent`, `Callout`, `Button`, `buttonVariants`, `Badge`, `badgeVariants`, `Steps`, `Step`, `Disclosure`, `DisclosureGroup`, `Accordion`, `AccordionItem`, `AccordionTrigger`, `AccordionContent`, `Tabs`, `TabsList`, `TabsTrigger`, `TabsContent`, `TabGroup`, `SEGMENT`, `Bulletin`, `Ribbon`, `EDITORIAL_INKS`, `Colophon`, `BuiltWithFoundations`, `FoundationsMark`, `FOUNDATIONS_URL`. Types: `ButtonLook`, `BadgeLook`, `TabItem`, `BulletinProps`, `BulletinPoint`, `RibbonHue`, `ColophonProps` |
|
|
77
80
|
| `@supertype.ai/foundations/mdx` | `proseMdxComponents` |
|
|
78
81
|
| `@supertype.ai/foundations/essay` | `createEssay`, `EssayHeader`, `EssayLayout`, `EssaySection`, `EssayPullQuote`, `EssayFigure`, `EssayMovements`, `EssayDocument`, `EssayColumns`, `EssayAside`, `EssayBody`, `ReadingLayout`, `TableOfContents`, `ReadingRail`, `ReadingProgressBar`, `Rail`, `RailLink`, `PostMetaRow`, `PostDate`, `ReadTime`, `TagPills`, `MetaDot`, `formatPostDate`, `extractHeadings`, `readingTime`, `createSlugger`, `useReadingProgress`, `useScrollSpy`. Types: `TocHeading`, `EssayDecorations`, `EssayIndexEntry`, `EssayDocSection`, `EssayMovement`, `PostDateFormat` |
|
|
79
82
|
| `@supertype.ai/foundations/seo` | `createSeo`. Types: `SeoConfig`, `ArticleAuthor`, `ArticleOptions`, `PageMetadata` |
|
|
@@ -91,7 +94,7 @@ silently. Full reference: https://github.com/supertypeai/foundations
|
|
|
91
94
|
prop each one pins is dropped from its type, so passing it fails to compile.
|
|
92
95
|
- `TypographyCaption`, `TypographyLabel`, `TypographySmall`: `size?: "sm" | "xs" | "2xs" | "inherit"`, `as?`.
|
|
93
96
|
- `TypographyEyebrow`: `tone?: "heading" | "label" | "muted" | "subtle"`, `size?: "sm" | "xs" | "2xs" | "3xs"`, `as?`. Each tone carries the rung it is usually set at and `size` overrides it, so omitting it changes nothing. Reach for `muted` for the uppercase micro-label a dense product sets over a group of controls, and `subtle` for a column head or rail marker read on the way past — that shape hand-rolled is the most common way an app ends up spelling type classes.
|
|
94
|
-
- `TypographyStat`: `size?: "inherit" | "card" | "panel" | "page" | "display"`, `figures?: "tabular" | "proportional"`. Keep tabular where a value updates in place.
|
|
97
|
+
- `TypographyStat`: `size?: "inherit" | "3xs" | "2xs" | "xs" | "sm" | "base" | "lg" | "xl" | "2xl" | "3xl" | "card" | "panel" | "section" | "page" | "display"`, `figures?: "tabular" | "proportional"`, `tone?: "default" | "muted"`. Two ladders in one axis: the rung names are the body ramp, for a figure beside interface copy it should step with, and the surface names ride the heading ladder, for a figure that is the headline. `tone="muted"` is the qualifier after a figure, "of 2,000" beside "1,284": still tight, no longer competing. Keep tabular where a value updates in place.
|
|
95
98
|
- `TypographyLink`: `href` (required), `tone?: Tone` (default `muted`), `addArrow?`, `newTab?`. The href decides internal versus external.
|
|
96
99
|
- `TypographyHighlight`: `tone?: HighlightTone`, one of `"primary" | "success" | "ochre" | "terracotta" | "sage" | "fig"`, plus `seed?: number`. A separate type from `Tone` on purpose: this axis is categorical (which one it is) where `Tone` is semantic (what it means), the same split theme.css draws between the earth swatches and the status tokens.
|
|
97
100
|
- `Card`: `href`, `title`, `description`, `icon`, `external`. An href makes the whole card a link.
|
|
@@ -104,6 +107,9 @@ silently. Full reference: https://github.com/supertypeai/foundations
|
|
|
104
107
|
- `Button`: `variant?: "solid" | "soft" | "outline" | "ghost" | "link"` (default `solid`), `tone?: Tone` (defaults to `primary` on a solid button and `muted` on every other variant — filling a button in is how a page says this is the action), `size?: "xs" | "sm" | "md" | "lg" | "xl"` (default `md`), `icon?: boolean` for a square glyph box, `pill?: boolean` for full-round corners, `href` to make it a link, `render` for an element that is neither a button nor a link. Variant is how much ink it spends and tone is what the ink means, on separate axes, so a quiet delete is `variant="ghost" tone="destructive"`.
|
|
105
108
|
- `Badge`: `variant?: "solid" | "soft" | "outline" | "ghost"` (default `solid`), `tone?: Tone`, `size?: "xs" | "sm"` (default `sm`), `pill?: boolean`, `href` for a badge that leads somewhere. Same axes and same spellings as `Button`, minus `link`, which belongs to things you click. `warning` and `supertype` were `tone="warn"` and `tone="brand"` under invented names.
|
|
106
109
|
- `Callout`: `tone?: Tone`, `density?: "compact" | "editorial"`, `title`, `icon`, `action`.
|
|
110
|
+
- `Bulletin`: the panel shape with no copy in it — `variant?: "card" | "line"` (default `card`), `accent` (drawn along the top edge, full width, before the padding; `<Ribbon />` is the one that ships), `eyebrow`, `headline`, `lede`, `points` (a `BulletinPoint[]` of `{ title, body, mark?, ink? }`, where `mark` and `ink` are class names like `bg-fern`/`text-fern-ink`; one point takes the width, two or more go two-up from `sm`), `action` (the control on the footer rule), `footnote` (the line opposite it), `children` (under the rule) and `className`. Every slot is optional and an omitted one renders nothing, so the same component covers a credits panel and a headline with one button under it. Reach for it for an announcement, a release note, a status bulletin. `Ribbon` takes `hues?: RibbonHue[]` (default `EDITORIAL_INKS`, the eight categorical hues as data) and states no size of its own — the caller passes `h-1.5 w-full` on a panel and `h-1 min-w-20 flex-1 rounded-full` in a row.
|
|
111
|
+
- `Colophon`: `Bulletin` with this package's words in it, and the words are not props. `variant?: "card" | "line"` (default `card`), plus `label` (the link's words, for translation) and `children` (the consumer's own credit, rendered under the rule on `card`). A compact statement block for a page (`card`) or a footer row (`line`). There is nothing to configure, so `<Colophon />` is the whole call. Its copy is deliberately not props: this is the package's only content component — every other block is an empty container whose words come from the call site — which makes it a mark rather than a block, and a mark every site rewrites is not a mark. An app wanting to say something in its own words wants a `Card`. It draws the eight categorical hues and inks two of them, all declared in theme.css: an app that imported tokens.css alone renders a panel with an invisible ribbon and no error.
|
|
112
|
+
- **`Colophon` or `BuiltWithFoundations`.** A colophon is a block with something to say and needs a place to say it — a credits page, an about page, the foot of a long article. `BuiltWithFoundations` is one item in a row somebody else laid out, beside a privacy link and a copyright line, and says only where the site came from. The test is what the surface is for: somewhere with its own columns already takes the link, and somewhere the block itself is being placed takes the colophon. There is no `Colophon` variant rendering the link alone, since that would be a second name for a component that already has one. `BuiltWithFoundations` is a `Button variant="outline" size="sm"` with an `href`, so it sits on the same hairline and the same radius rung as every other control on the page. `FoundationsMark` is the eight-hue chip alone and `FOUNDATIONS_URL` the bare href.
|
|
107
113
|
- `DisclosureGroup`: `type?: "multiple" | "single"`, `defaultValue` (matches the title string).
|
|
108
114
|
- `TabsList`: `variant?: "default" | "line"`, `tone?: Tone` (default `primary`). The tone
|
|
109
115
|
inks the marker alone, which on `line` means the underline and the active icon. The
|
package/package.json
CHANGED
package/src/tokens.css
CHANGED
|
@@ -38,6 +38,21 @@
|
|
|
38
38
|
--color-info-ink: var(--info-ink);
|
|
39
39
|
--color-danger: var(--danger);
|
|
40
40
|
|
|
41
|
+
/* The one role with no value in this package, and registered anyway. A v4
|
|
42
|
+
utility exists only for a name in the namespace, so without these three
|
|
43
|
+
`bg-brand` compiles to nothing at all and the element keeps whatever ink it
|
|
44
|
+
inherited, silently. Naming a role is structural; the hue stays the app's.
|
|
45
|
+
|
|
46
|
+
The fallbacks are the chains tone.ts already reads, so an app that declares
|
|
47
|
+
no identity hue paints its principal one here too, and an app that declares
|
|
48
|
+
`--brand` alone still gets a readable ink and label rather than nothing. */
|
|
49
|
+
--color-brand: var(--brand, var(--primary));
|
|
50
|
+
--color-brand-ink: var(--brand-ink, var(--primary-ink, var(--primary)));
|
|
51
|
+
--color-brand-foreground: var(
|
|
52
|
+
--brand-foreground,
|
|
53
|
+
var(--primary-foreground)
|
|
54
|
+
);
|
|
55
|
+
|
|
41
56
|
--color-border: var(--border);
|
|
42
57
|
--color-input: var(--input);
|
|
43
58
|
--color-ring: var(--ring);
|
package/src/type.css
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
/*
|
|
2
|
-
|
|
1
|
+
/* The type ramp. A product may retune the body rungs at the root, where density
|
|
2
|
+
is its own concern; `.editorial` below restates the reading sizes so a marketing
|
|
3
|
+
or docs subtree inside a dense app lands back on them without the app copying
|
|
4
|
+
Tailwind's table into its own stylesheet. */
|
|
3
5
|
|
|
4
6
|
@theme {
|
|
5
7
|
/* Below Tailwind's 12px floor. Without them these get written as `text-[11px]`. */
|
|
@@ -50,6 +52,24 @@
|
|
|
50
52
|
--font-heading: var(--font-average, ui-serif), Georgia, serif;
|
|
51
53
|
--heading-weight: 400;
|
|
52
54
|
|
|
55
|
+
/* Tailwind's own rungs, restated rather than inherited. A dense app has
|
|
56
|
+
already moved these at the root, and this is the surface that has to get
|
|
57
|
+
back to reading sizes: without them `.editorial` retunes the headings and
|
|
58
|
+
leaves the body under them at 13px, which is the ratio the ladder exists
|
|
59
|
+
to keep. Stated here, the class is the whole opt-out. */
|
|
60
|
+
--text-xs: 0.75rem;
|
|
61
|
+
--text-xs--line-height: calc(1 / 0.75);
|
|
62
|
+
--text-sm: 0.875rem;
|
|
63
|
+
--text-sm--line-height: calc(1.25 / 0.875);
|
|
64
|
+
--text-base: 1rem;
|
|
65
|
+
--text-base--line-height: calc(1.5 / 1);
|
|
66
|
+
--text-lg: 1.125rem;
|
|
67
|
+
--text-lg--line-height: calc(1.75 / 1.125);
|
|
68
|
+
--text-xl: 1.25rem;
|
|
69
|
+
--text-xl--line-height: calc(1.75 / 1.25);
|
|
70
|
+
--text-2xl: 1.5rem;
|
|
71
|
+
--text-2xl--line-height: calc(2 / 1.5);
|
|
72
|
+
|
|
53
73
|
/* The same ladder off an 18px reading rung: 1.11 / 1.33 / 1.67 / 2. Every
|
|
54
74
|
rung clears the body it sits over — the contract the product values above
|
|
55
75
|
keep too, and the one the shared rungs could not. */
|