@supertype.ai/foundations 0.1.29 → 0.1.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/tone.js CHANGED
@@ -30,8 +30,8 @@
30
30
  */
31
31
  /**
32
32
  * `muted` is the only row that names a fourth value. A hairline derived from the
33
- * ink at 45% is right for a hue and wrong for the absence of one; `--border` is
34
- * the tuned answer there, and it is not a wash of `--foreground`.
33
+ * ink at 45% is right for a hue and wrong for the absence of one. `--border` is
34
+ * the tuned answer there, in place of a wash of `--foreground`.
35
35
  *
36
36
  * Its hue is `--foreground` rather than `--muted-foreground` on purpose. `muted`
37
37
  * says the control carries no meaning, not that it carries less contrast — a
@@ -47,15 +47,15 @@ export const TONE = {
47
47
  /** No meaning: chrome, toolbars, anything that must not compete. */
48
48
  muted: "[--tone-fill:var(--muted)] [--tone-ink:var(--foreground)] [--tone-hue:var(--foreground)] [--tone-line:var(--border)]",
49
49
  /** The principal action, and the package's default wherever a tone is optional. */
50
- primary: "[--tone-fill:var(--primary)] [--tone-ink:var(--primary-foreground)] [--tone-hue:var(--primary)]",
50
+ primary: "[--tone-fill:var(--primary)] [--tone-ink:var(--primary-foreground)] [--tone-hue:var(--primary-ink,var(--primary))]",
51
51
  /** The warm accent. `--secondary-ink` is its readable cut; the fill is not. */
52
52
  secondary: "[--tone-fill:var(--secondary)] [--tone-ink:var(--secondary-foreground)] [--tone-hue:var(--secondary-ink)]",
53
53
  /** The consumer's identity hue, if it defined one. Otherwise the principal one. */
54
- brand: "[--tone-fill:var(--brand,var(--primary))] [--tone-ink:var(--tint-foreground)] [--tone-hue:var(--brand-ink,var(--primary))]",
54
+ brand: "[--tone-fill:var(--brand,var(--primary))] [--tone-ink:var(--brand-foreground,var(--primary-foreground))] [--tone-hue:var(--brand-ink,var(--primary-ink,var(--primary)))]",
55
55
  /** It worked. */
56
- success: "[--tone-fill:var(--success)] [--tone-ink:var(--tint-foreground)] [--tone-hue:var(--success-ink)]",
56
+ success: "[--tone-fill:var(--success)] [--tone-ink:var(--success-foreground)] [--tone-hue:var(--success-ink)]",
57
57
  /** A footgun: the reader can still proceed, but not blindly. */
58
- warn: "[--tone-fill:var(--warn)] [--tone-ink:var(--tint-foreground)] [--tone-hue:var(--warn-ink)]",
58
+ warn: "[--tone-fill:var(--warn)] [--tone-ink:var(--warn-foreground)] [--tone-hue:var(--warn-ink)]",
59
59
  /** It deletes something, or it already failed. */
60
60
  destructive: "[--tone-fill:var(--destructive)] [--tone-ink:var(--destructive-foreground)] [--tone-hue:var(--destructive)]",
61
61
  };
@@ -69,9 +69,13 @@ export const TONE = {
69
69
  * while `color-mix` is what the modifier compiles to anyway — the same CSS, one
70
70
  * layer less of trust.
71
71
  *
72
- * `--tone-fill-hover` mixes toward `--foreground` rather than darkening by a
73
- * fixed amount, so a filled control deepens on the light theme and lifts on the
74
- * dark one from a single declaration. A `dark:` override here is what the
72
+ * A hover moves the fill 18% toward `--hover-toward`, the extreme theme.css
73
+ * points away from the page: black on latte, white on espresso. The token
74
+ * carries the direction and the percentage carries the state. Mixing toward
75
+ * `--foreground` instead made the step as long as the gap between the fill and
76
+ * the ink, so a `primary` button moved 4.9 ΔL* on latte against 6.2 on espresso
77
+ * and read as no hover at all. Every tone now clears 6 ΔL* in both themes,
78
+ * measured in test/composition.test.ts. A `dark:` override here is what the
75
79
  * package's own ESLint rule exists to prevent.
76
80
  */
77
81
  export const TONE_SURFACE = [
@@ -81,10 +85,10 @@ export const TONE_SURFACE = [
81
85
  "[--tone-veil:color-mix(in_oklab,var(--tone-hue)_5%,transparent)]",
82
86
  /** A control's tint at rest. */
83
87
  "[--tone-wash:color-mix(in_oklab,var(--tone-hue)_10%,transparent)]",
84
- /** The same control under the pointer. */
85
- "[--tone-wash-hover:color-mix(in_oklab,var(--tone-hue)_18%,transparent)]",
88
+ /** The same control under the pointer: twice the tint, so the step reads. */
89
+ "[--tone-wash-hover:color-mix(in_oklab,var(--tone-hue)_20%,transparent)]",
86
90
  /** A filled control under the pointer. */
87
- "[--tone-fill-hover:color-mix(in_oklab,var(--tone-fill)_88%,var(--foreground))]",
91
+ "[--tone-fill-hover:color-mix(in_oklab,var(--tone-fill),var(--hover-toward)_18%)]",
88
92
  ].join(" ");
89
93
  /**
90
94
  * A tone, as one class list. This is the only thing a component should need.
@@ -97,6 +101,31 @@ export const TONE_SURFACE = [
97
101
  * whose order matters and whose values always travel together is one argument.
98
102
  */
99
103
  export const toneClass = (tone) => `${TONE_SURFACE} ${TONE[tone]}`;
104
+ /**
105
+ * The ink a nested element inherits, declared by whatever painted the surface
106
+ * under it. Two properties, one rule: paint a background, hand down its ink.
107
+ *
108
+ * `toneClass` alone is a palette, not a surface — a `Callout` spends the same
109
+ * seven values as a filled `Button` and tints at 5%, so the words inside it
110
+ * still sit on the page and still want the page's ink. Only a component that
111
+ * actually fills promotes `--tone-ink` to the inherited ink, and the type
112
+ * primitives read it with the page as their fallback. A tint that promotes
113
+ * nothing is therefore correct by default, which is the failure this replaces:
114
+ * `TypographyLabel` pinned `text-foreground`, won over the `text-primary-foreground`
115
+ * on the anchor around it, and printed 2.34:1 on a filled button.
116
+ *
117
+ * `--ink-muted` collapses to the ink itself, because a hue fill has no second
118
+ * rung: mixing the ink 20% toward `--primary` measures 4.22:1 on the
119
+ * espresso theme and 3.49:1 at 30%. Nothing on a filled control may be quieter
120
+ * than its label. Wanting two rungs is wanting a tinted surface.
121
+ */
122
+ export const INK_ON_FILL = "[--ink:var(--tone-ink)] [--ink-muted:var(--tone-ink)]";
123
+ /**
124
+ * The same contract for a surface the tones do not name: `--card`, `--popover`,
125
+ * a sidebar. These are tints of the page rather than hues, so both rungs
126
+ * survive and the pair is stated rather than collapsed.
127
+ */
128
+ export const inkOnSurface = (ink, muted = "--muted-foreground") => `[--ink:var(${ink})] [--ink-muted:var(${muted})]`;
100
129
  /**
101
130
  * What an unstated tone means, given how much ink the component is spending.
102
131
  * Shared, because `Button` and `Badge` both need it and two copies of a default
@@ -59,8 +59,8 @@ declare const eyebrowVariants: (props?: ({
59
59
  /**
60
60
  * The eyebrow's ramp as a class, for a caller that cannot render our element —
61
61
  * a dialog title primitive, a motion element. Same escape hatch as
62
- * `headingClass`, and it exists so that a consumer needing the class does not
63
- * hand-roll a second copy of it that then drifts from the component.
62
+ * `headingClass`. It exists so a consumer needing the class can take ours rather
63
+ * than hand-rolling a copy that drifts from the component.
64
64
  */
65
65
  export declare const eyebrowClass: (tone?: VariantProps<typeof eyebrowVariants>["tone"]) => string;
66
66
  /**
@@ -32,7 +32,7 @@ import { TextAs } from "./as.js";
32
32
  * the face had no door, arriving with a `scroll-m-20` for an anchor it does not have.
33
33
  */
34
34
  export const headingFace = "font-heading font-[number:var(--heading-weight)]";
35
- const HEADING_BASE = `scroll-m-20 ${headingFace} text-foreground`;
35
+ const HEADING_BASE = `scroll-m-20 ${headingFace} text-[color:var(--ink,var(--foreground))]`;
36
36
  const h1Variants = cva(`${HEADING_BASE} tracking-tight`, {
37
37
  variants: {
38
38
  variant: {
@@ -98,7 +98,7 @@ const eyebrowVariants = cva("block uppercase tracking-wider", {
98
98
  * Primary ink, stated not inherited — an eyebrow names the section under it,
99
99
  * and one that turns red from its surroundings is not a heading.
100
100
  */
101
- heading: "text-xs font-semibold text-foreground",
101
+ heading: "text-xs font-semibold text-[color:var(--ink,var(--foreground))]",
102
102
  /** Stat cards invert it: the figure is the headline, so the label yields. */
103
103
  label: "text-2xs font-medium text-accent-foreground",
104
104
  },
@@ -108,8 +108,8 @@ const eyebrowVariants = cva("block uppercase tracking-wider", {
108
108
  /**
109
109
  * The eyebrow's ramp as a class, for a caller that cannot render our element —
110
110
  * a dialog title primitive, a motion element. Same escape hatch as
111
- * `headingClass`, and it exists so that a consumer needing the class does not
112
- * hand-roll a second copy of it that then drifts from the component.
111
+ * `headingClass`. It exists so a consumer needing the class can take ours rather
112
+ * than hand-rolling a copy that drifts from the component.
113
113
  */
114
114
  export const eyebrowClass = (tone) => eyebrowVariants({ tone });
115
115
  /**
@@ -5,8 +5,7 @@ import type { ComponentProps } from "react";
5
5
  * hue for nothing. That is why the earth tones enter as `-foreground`: those are
6
6
  * the ink-grade pair, mixed to hold at text weight in both themes.
7
7
  *
8
- * Emphasis, never status. Warn and info and destructive are absent on purpose
9
- * a swipe of red under a phrase says less than the words do.
8
+ * Emphasis, never status. Warn and info and destructive are absent on purpose.
10
9
  *
11
10
  * The four earth tones are one palette, not a menu: each is a different hue,
12
11
  * because two a reader cannot tell apart are one tone with two names.
@@ -24,6 +23,8 @@ export type HighlightTone = keyof typeof MARKER_TONES;
24
23
  * Marker highlight for inline text. Painted as the run's own background, never a
25
24
  * mask — a mask would shave the glyph tops. `luminosity` lets letters borrow the
26
25
  * marker's hue while keeping their own lightness, so contrast holds in both themes.
26
+ * That lightness comes from `--marker-ink`, a deepened `--foreground`, so the run
27
+ * still reads as the emphasised one rather than sinking into its own wash.
27
28
  */
28
29
  export declare function TypographyHighlight({ tone, seed, className, style, children, ...props }: ComponentProps<"span"> & {
29
30
  tone?: HighlightTone;
@@ -6,8 +6,7 @@ import { cn } from "../cn.js";
6
6
  * hue for nothing. That is why the earth tones enter as `-foreground`: those are
7
7
  * the ink-grade pair, mixed to hold at text weight in both themes.
8
8
  *
9
- * Emphasis, never status. Warn and info and destructive are absent on purpose
10
- * a swipe of red under a phrase says less than the words do.
9
+ * Emphasis, never status. Warn and info and destructive are absent on purpose.
11
10
  *
12
11
  * The four earth tones are one palette, not a menu: each is a different hue,
13
12
  * because two a reader cannot tell apart are one tone with two names.
@@ -41,7 +40,8 @@ const DABS = [
41
40
  // Integer-only: `Math.sin` is implementation-defined, so Node and the browser
42
41
  // disagreed in the last bits and the swipe hydrated as a mismatch.
43
42
  const hash = (seed, i) => {
44
- let h = Math.imul(seed ^ 0x9e3779b9, 0x85ebca6b) ^ Math.imul(i + 0x165667b1, 0xc2b2ae35);
43
+ let h = Math.imul(seed ^ 0x9e3779b9, 0x85ebca6b) ^
44
+ Math.imul(i + 0x165667b1, 0xc2b2ae35);
45
45
  h ^= h >>> 15;
46
46
  h = Math.imul(h, 0x2545f491);
47
47
  h ^= h >>> 13;
@@ -88,11 +88,17 @@ const markerFill = (seed) => {
88
88
  * Marker highlight for inline text. Painted as the run's own background, never a
89
89
  * mask — a mask would shave the glyph tops. `luminosity` lets letters borrow the
90
90
  * marker's hue while keeping their own lightness, so contrast holds in both themes.
91
+ * That lightness comes from `--marker-ink`, a deepened `--foreground`, so the run
92
+ * still reads as the emphasised one rather than sinking into its own wash.
91
93
  */
92
94
  export function TypographyHighlight({ tone = "primary", seed = 3, className, style, children, ...props }) {
93
- return (_jsx("span", { className: cn("isolate inline px-[0.3em] py-[0.06em]", "[-webkit-box-decoration-break:clone] [box-decoration-break:clone]", "[--marker-alpha:44%] dark:[--marker-alpha:58%]", className), style: {
95
+ return (_jsx("span", { className: cn("isolate inline px-[0.3em] py-[0.06em]", "[-webkit-box-decoration-break:clone] [box-decoration-break:clone]", "[--marker-alpha:44%] dark:[--marker-alpha:58%]",
96
+ // The blend keeps a glyph's lightness and nothing else, so a run over the
97
+ // wash reads flatter than the same words beside it. Pushing the lightness
98
+ // past --foreground buys that punch back without touching the hue borrow.
99
+ "[--marker-ink:color-mix(in_oklab,var(--foreground)_85%,black)]", "dark:[--marker-ink:color-mix(in_oklab,var(--foreground)_88%,white)]", className), style: {
94
100
  "--marker": MARKER_TONES[tone],
95
101
  backgroundImage: markerFill(seed),
96
102
  ...style,
97
- }, ...props, children: _jsx("span", { className: "[mix-blend-mode:luminosity]", children: children }) }));
103
+ }, ...props, children: _jsx("span", { className: "text-(color:--marker-ink) [mix-blend-mode:luminosity]", children: children }) }));
98
104
  }
@@ -1,3 +1,4 @@
1
+ import { type LinkBehavior } from "../href.js";
1
2
  import { type VariantProps } from "class-variance-authority";
2
3
  import type { ComponentProps, ReactNode } from "react";
3
4
  import { type Tone } from "../tone.js";
@@ -101,7 +102,7 @@ export declare function TypographyLabel({ className, size, as, children, ...prop
101
102
  * than a constant. Keep `tabular` anywhere a value updates in place.
102
103
  */
103
104
  declare const statVariants: (props?: ({
104
- size?: "inherit" | "display" | "card" | "panel" | "page" | null | undefined;
105
+ size?: "inherit" | "page" | "display" | "card" | "panel" | null | undefined;
105
106
  figures?: "tabular" | "proportional" | null | undefined;
106
107
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
107
108
  export type StatVariants = VariantProps<typeof statVariants>;
@@ -114,28 +115,24 @@ export declare function TypographyStat({ className, size, figures, children, ...
114
115
  * optical correction — the mono face carries a taller x-height than the sans.
115
116
  */
116
117
  export declare function TypographyInlineCode({ className, children, ...props }: ComponentProps<"code">): import("react").JSX.Element;
117
- type TypographyLinkProps = Omit<ComponentProps<"a">, "href"> & {
118
+ type TypographyLinkProps = Omit<ComponentProps<"a">, "href"> & LinkBehavior & {
118
119
  href: string;
119
120
  children: ReactNode;
120
121
  tone?: Tone;
121
- /** Defaults on for an off-site link. Turn it off for one that starts a flow the reader should stay in. */
122
- newTab?: boolean;
123
122
  /**
124
123
  * A trailing arrow, for a link that ends a sentence and leads somewhere. The
125
124
  * glyph follows the href: `↗` when the link leaves the site, `→` when it does
126
- * not. That is the convention, and it is not a call site's to get wrong.
125
+ * not. The href picks it, so a call site never has to.
127
126
  */
128
127
  addArrow?: boolean;
129
128
  };
130
129
  /**
131
130
  * The inline link.
132
131
  *
133
- * Internal and external are decided from the href, never at the call site: an
134
- * href with a scheme renders a plain anchor and, if it is http(s), opens away
135
- * with `rel="noopener noreferrer"`; everything else routes through the router's
136
- * Link. `newTab` is the one override, for an off-site href that starts a flow
137
- * the reader should stay in. Call-site props apply last, so a passed
138
- * `target`/`rel` still wins.
132
+ * Internal and external are decided from the href, never at the call site
133
+ * ../href.ts holds that decision, and Button, Badge and Card make the same one.
134
+ * `newTab` and `external` are the overrides. Call-site props apply last, so a
135
+ * passed `target`/`rel` still wins.
139
136
  *
140
137
  * The router is `next-view-transitions`, imported rather than injected. Every
141
138
  * project on this package is a Next app and wants the same link, and a factory
@@ -143,5 +140,5 @@ type TypographyLinkProps = Omit<ComponentProps<"a">, "href"> & {
143
140
  * not be imported by name. One call site ended up on the unbound version that
144
141
  * way and lost its decoration.
145
142
  */
146
- export declare function TypographyLink({ href, children, tone, newTab, addArrow, className, ...props }: TypographyLinkProps): import("react").JSX.Element;
143
+ export declare function TypographyLink({ href, children, tone, external: leavesApp, newTab, addArrow, className, ...props }: TypographyLinkProps): import("react").JSX.Element;
147
144
  export {};
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Link } from "next-view-transitions";
2
+ import { resolveLink } from "../href.js";
3
3
  import { cva } from "class-variance-authority";
4
4
  import { cn } from "../cn.js";
5
5
  import { toneClass } from "../tone.js";
@@ -19,8 +19,8 @@ const pVariants = cva("", {
19
19
  prose: "text-pretty text-lg leading-relaxed",
20
20
  },
21
21
  tone: {
22
- default: "text-foreground",
23
- muted: "text-muted-foreground",
22
+ default: "text-[color:var(--ink,var(--foreground))]",
23
+ muted: "text-[color:var(--ink-muted,var(--muted-foreground))]",
24
24
  },
25
25
  },
26
26
  defaultVariants: { variant: "ui", tone: "default" },
@@ -34,7 +34,10 @@ export function TypographyMuted(props) {
34
34
  return _jsx(TypographyP, { ...props, ...MUTED });
35
35
  }
36
36
  /** Reading-size body copy. `TypographyMuted` is the same ink one rung down. */
37
- const PROSE = { variant: "prose", tone: "muted" };
37
+ const PROSE = {
38
+ variant: "prose",
39
+ tone: "muted",
40
+ };
38
41
  export function TypographyProse(props) {
39
42
  return _jsx(TypographyP, { ...props, ...PROSE });
40
43
  }
@@ -86,7 +89,7 @@ export function TypographyProseList(props) {
86
89
  * reason to sit there is to be quieter than the thing you qualify — and every
87
90
  * container that sets a size for you sets a weight too.
88
91
  */
89
- const captionVariants = cva("text-muted-foreground", {
92
+ const captionVariants = cva("text-[color:var(--ink-muted,var(--muted-foreground))]", {
90
93
  variants: {
91
94
  size: {
92
95
  sm: "text-sm leading-normal",
@@ -127,7 +130,7 @@ export function TypographySmall(props) {
127
130
  * `as` is here for the same reason it is on `TypographyEyebrow`: a config panel
128
131
  * names its sections at this size, and those names are the page's outline.
129
132
  */
130
- const labelVariants = cva("font-medium text-foreground", {
133
+ const labelVariants = cva("font-medium text-[color:var(--ink,var(--foreground))]", {
131
134
  variants: {
132
135
  size: {
133
136
  sm: "text-sm",
@@ -188,11 +191,11 @@ export function TypographyStat({ className, size, figures, children, ...props })
188
191
  * optical correction — the mono face carries a taller x-height than the sans.
189
192
  */
190
193
  export function TypographyInlineCode({ className, children, ...props }) {
191
- return (_jsx("code", { className: cn("rounded-[3px] bg-foreground/[0.03] px-[0.3em] py-[0.1em] font-mono text-[0.9em] text-secondary-ink", className), ...props, children: children }));
194
+ return (_jsx("code", { className: cn("rounded-[3px] bg-current/[0.06] px-[0.3em] py-[0.1em] font-mono text-[0.9em] text-[color:var(--ink,var(--secondary-ink))]", className), ...props, children: children }));
192
195
  }
193
196
  /**
194
197
  * A statement about the surface, not the link: `muted` inside a paragraph,
195
- * `primary` when the link is the point of the line, `secondary` for a note
198
+ * `primary` when the link is the main thing on the line, `secondary` for a note
196
199
  * beneath a hero where `primary` would compete with the CTA beside it. The other
197
200
  * four come free, and a link inside a warning should be able to say so.
198
201
  *
@@ -203,17 +206,22 @@ export function TypographyInlineCode({ className, children, ...props }) {
203
206
  * `font-medium`, which read as a lighter link rather than a differently-coloured
204
207
  * one.
205
208
  */
209
+ const INHERITED_INK = "text-[color:var(--ink,var(--foreground))]";
206
210
  const LINK_DECORATION = "underline decoration-dotted decoration-1 decoration-muted-foreground decoration-skip-ink-none underline-offset-2 hover:decoration-solid hover:decoration-current/70";
207
- const linkClass = (tone = "muted", className) => cn(toneClass(tone), "font-medium text-(color:--tone-hue)", LINK_DECORATION, className);
211
+ /**
212
+ * No tone stated means "read the surface": the ink comes from whatever painted
213
+ * the ground, which on a page is `--foreground` and inside a filled control is
214
+ * that control's label ink. The old default spelled this `muted`, whose hue is
215
+ * `--foreground` — identical on a page, and 2.28:1 on a filled button.
216
+ */
217
+ const linkClass = (tone, className) => cn(tone ? cn(toneClass(tone), "text-(color:--tone-hue)") : INHERITED_INK, "font-medium", LINK_DECORATION, className);
208
218
  /**
209
219
  * The inline link.
210
220
  *
211
- * Internal and external are decided from the href, never at the call site: an
212
- * href with a scheme renders a plain anchor and, if it is http(s), opens away
213
- * with `rel="noopener noreferrer"`; everything else routes through the router's
214
- * Link. `newTab` is the one override, for an off-site href that starts a flow
215
- * the reader should stay in. Call-site props apply last, so a passed
216
- * `target`/`rel` still wins.
221
+ * Internal and external are decided from the href, never at the call site
222
+ * ../href.ts holds that decision, and Button, Badge and Card make the same one.
223
+ * `newTab` and `external` are the overrides. Call-site props apply last, so a
224
+ * passed `target`/`rel` still wins.
217
225
  *
218
226
  * The router is `next-view-transitions`, imported rather than injected. Every
219
227
  * project on this package is a Next app and wants the same link, and a factory
@@ -221,13 +229,12 @@ const linkClass = (tone = "muted", className) => cn(toneClass(tone), "font-mediu
221
229
  * not be imported by name. One call site ended up on the unbound version that
222
230
  * way and lost its decoration.
223
231
  */
224
- export function TypographyLink({ href, children, tone = "muted", newTab, addArrow, className, ...props }) {
232
+ export function TypographyLink({ href, children, tone = "muted", external: leavesApp, newTab, addArrow, className, ...props }) {
225
233
  const style = linkClass(tone, className);
226
- const external = /^[a-z][a-z0-9+.-]*:/i.test(href);
234
+ const { Component, props: link, external } = resolveLink(href, {
235
+ external: leavesApp,
236
+ newTab,
237
+ });
227
238
  const body = (_jsxs(_Fragment, { children: [children, addArrow && (_jsx("svg", { "aria-hidden": "true", className: "ml-1 inline size-3.5 align-middle", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("path", { d: external ? "M7 17 17 7M7 7h10v10" : "M5 12h14M12 5l7 7-7 7" }) }))] }));
228
- if (external) {
229
- const away = newTab ?? href.startsWith("http");
230
- return (_jsx("a", { href: href, className: style, ...(away ? { target: "_blank", rel: "noopener noreferrer" } : {}), ...props, children: body }));
231
- }
232
- return (_jsx(Link, { href: href, className: style, ...props, children: body }));
239
+ return (_jsx(Component, { className: style, ...link, ...props, children: body }));
233
240
  }
package/llms.txt CHANGED
@@ -5,8 +5,8 @@ content blocks, a long-form essay shell, token and theme CSS, and build-time
5
5
  tooling for SEO, OG cards, lint rules and contrast checks.
6
6
 
7
7
  This file is for coding agents working in an app that consumes the package. It
8
- covers what to import and the mistakes that do not produce an error. Full
9
- reference: https://github.com/supertypeai/foundations
8
+ covers what to import, how the copy should read, and the mistakes that pass
9
+ silently. Full reference: https://github.com/supertypeai/foundations
10
10
 
11
11
  ## Rules
12
12
 
@@ -21,6 +21,11 @@ reference: https://github.com/supertypeai/foundations
21
21
  4. **Use colour tokens, never literal colours.** `bg-card`, `text-foreground`,
22
22
  `border-border`. No hex values, no `bg-zinc-800`, no `dark:` overrides that
23
23
  swap one token for another.
24
+ 5. **Paint a surface, hand down its ink.** Any element you give a background
25
+ needs `INK_ON_FILL` (a tone fill) or `inkOnSurface(token)` (a tinted one), or
26
+ the type inside it keeps the page's ink and fails contrast silently.
27
+ 6. **Write the words like a person would.** See Writing copy below. It applies
28
+ to every string a reader sees and to the comments you leave behind.
24
29
 
25
30
  ## Which component
26
31
 
@@ -40,6 +45,7 @@ reference: https://github.com/supertypeai/foundations
40
45
  | a number or metric | `TypographyStat` | root |
41
46
  | inline code | `TypographyInlineCode` | root |
42
47
  | a link | `TypographyLink` | root |
48
+ | where an href goes, without a component | `resolveLink` | root |
43
49
  | a highlighted phrase | `TypographyHighlight` | root |
44
50
  | a button, or a link that looks like one | `Button` | `/blocks` |
45
51
  | a status label, count or chip | `Badge` | `/blocks` |
@@ -61,15 +67,15 @@ reference: https://github.com/supertypeai/foundations
61
67
 
62
68
  | import | exports |
63
69
  |---|---|
64
- | `@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`. Types: `TypographyTag`, `ParagraphVariants`, `ListProps`, `CaptionVariants`, `LabelVariants`, `StatVariants`, `Tone`, `HighlightTone` |
70
+ | `@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` |
65
71
  | `@supertype.ai/foundations/blocks` | `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` |
66
72
  | `@supertype.ai/foundations/mdx` | `proseMdxComponents` |
67
73
  | `@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` |
68
74
  | `@supertype.ai/foundations/seo` | `createSeo`. Types: `SeoConfig`, `ArticleAuthor`, `ArticleOptions`, `PageMetadata` |
69
75
  | `@supertype.ai/foundations/og` | `ogCard`, `OG_SIZE`. Types: `OgCardOptions` |
70
- | `@supertype.ai/foundations/eslint` | `designRules` (every rule as one array spread this), `designConfig` (the same set wrapped as a flat-config entry). The five builders `colourRules`, `typographyRules`, `themeOverrideRules`, `surfaceAsInkRules`, `renamedTokenRules` are still exported, but spreading them by hand is how a consumer ends up missing one. Types: `FlatConfigEntry`, `DesignRuleOptions`, `DesignConfigOptions`, `RestrictedSyntax`, `ColourOptions`, `TypographyOptions` |
76
+ | `@supertype.ai/foundations/eslint` | `designRules` (every rule as one array, the one to spread), `designConfig` (the same set wrapped as a flat-config entry). The builders `colourRules`, `typographyRules`, `linkRules`, `themeOverrideRules`, `surfaceAsInkRules`, `renamedTokenRules` are exported too, though spreading them by hand is how a consumer ends up missing one. Types: `FlatConfigEntry`, `DesignRuleOptions`, `DesignConfigOptions`, `RestrictedSyntax`, `ColourOptions`, `TypographyOptions` |
71
77
  | `@supertype.ai/foundations/rehype` | `rehypeProseCode`, `proseCodeOptions`, `PROSE_LANGS`, `PROSE_THEMES`. Build-time only, must not resolve React |
72
- | `@supertype.ai/foundations/contrast` | `checkLegibility` (inks at 4.5:1), `checkSignals` (fills at 3:1, tinted inks at 4.5:1, labels against their own fill), `resolveTokens`, `formatFailures`, `specificity`, `parseColor`, `luminance`, `contrast`, `tokenCuts` (which cuts a token ships fill, the label printed on it, the hue as words; the taxonomy `checkSignals` measures). Types: `Rgb`, `Theme`, `LegibilityFailure`, `TokenCuts`. Build-time only |
78
+ | `@supertype.ai/foundations/contrast` | `checkLegibility` (inks at 4.5:1), `checkSignals` (fills at 3:1 — status hues, the categorical earth hues and the six chart series alike — tinted inks at 4.5:1, `--subtle-foreground` at the 3:1 it is documented for, labels against their own fill), `checkHairlines` (`--border` and `--input` at 1.4:1 on `--background` and `--card`, `--sidebar-border` on `--sidebar`: a rule is exempt from the ink and mark bars, but it still has to read as the same weight in both themes), `resolveTokens`, `formatFailures`, `specificity`, `parseColor`, `luminance`, `contrast` (WCAG ratio), `lc` (APCA lightness contrast: polarity-aware, for checking that an ink ramp is perceptually ordered rather than merely ordered by ratio), `tokenCuts` (which cuts a token ships: fill, the label printed on it, the hue as words, the taxonomy `checkSignals` measures against). Types: `Rgb`, `Theme`, `LegibilityFailure`, `TokenCuts`. Build-time only |
73
79
 
74
80
  ## Props worth knowing
75
81
 
@@ -77,51 +83,100 @@ reference: https://github.com/supertypeai/foundations
77
83
  - `TypographyH2`: `variant?`, plus `divider?: boolean` for a rule underneath.
78
84
  - `TypographyP`: `variant?: "ui" | "prose"` (default `ui`), `tone?: "default" | "muted"`.
79
85
  - `TypographyMuted`, `TypographyProse`, `TypographyProseList` are presets. The
80
- prop each one pins is removed from its type, so passing it will not compile.
86
+ prop each one pins is dropped from its type, so passing it fails to compile.
81
87
  - `TypographyCaption`, `TypographyLabel`, `TypographySmall`: `size?: "sm" | "xs" | "2xs" | "inherit"`, `as?`.
82
88
  - `TypographyEyebrow`: `tone?: "heading" | "label"`, `as?`.
83
89
  - `TypographyStat`: `size?: "inherit" | "card" | "panel" | "page" | "display"`, `figures?: "tabular" | "proportional"`. Keep tabular where a value updates in place.
84
- - `TypographyLink`: `href` (required), `tone?: Tone` (default `muted`), `addArrow?`, `newTab?`. Internal versus external is decided from the href.
85
- - `TypographyHighlight`: `tone?: HighlightTone` `"primary" | "success" | "ochre" | "terracotta" | "sage" | "fig"`, `seed?: number`. Deliberately NOT `Tone`: this axis is categorical (which one it is), not semantic (what it means), which is the same split theme.css draws between the earth swatches and the status tokens.
90
+ - `TypographyLink`: `href` (required), `tone?: Tone` (default `muted`), `addArrow?`, `newTab?`. The href decides internal versus external.
91
+ - `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.
86
92
  - `Card`: `href`, `title`, `description`, `icon`, `external`. An href makes the whole card a link.
87
- - `Tone` the one semantic colour vocabulary, shared by `Button`, `Callout` and `TypographyLink`: `"muted" | "primary" | "secondary" | "brand" | "success" | "warn" | "destructive"`, defaulting to `muted` everywhere except a solid `Button`. Seven tones, seven tokens, one to one that is the bar for adding one. There is no `neutral` (that is `muted`, the word the rest of the package uses), no `foreground` (also `muted`), no `accent` (that is `--primary`'s hover tint, so a washed `primary` renders the same thing) and no `info` (`success | warn | destructive` is already the whole triad). `brand` falls back to `--primary` in an app that defines no `--brand`.
88
- - `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, `render` for the element to become. Variant is how much ink it spends; tone is what the ink means separate axes, so a quiet delete is `variant="ghost" tone="destructive"`, not a `destructive` variant.
89
- - `Badge`: `variant?: "solid" | "soft" | "outline" | "ghost"` (default `solid`), `tone?: Tone`, `size?: "xs" | "sm"` (default `sm`), `pill?: boolean`. Same axes and same spellings as `Button`, minus `link` a badge is not a link. There is no `warning` or `supertype` variant; those were `tone="warn"` and `tone="brand"` under invented names.
93
+ - `Tone` is the one semantic colour vocabulary, shared by `Button`, `Badge`, `Callout`, `TypographyLink` and `TabsList`: `"muted" | "primary" | "secondary" | "brand" | "success" | "warn" | "destructive"`, defaulting to `muted` everywhere except a solid `Button`. Seven tones, seven tokens, one to one, which is the bar for adding one. Four names map onto others: `neutral` and `foreground` are `muted`, the word the rest of the package uses; `accent` is `--primary`'s hover tint, so a washed `primary` renders the same thing; `info` is covered by the `success | warn | destructive` triad. `brand` falls back to `--primary` in an app that defines no `--brand`.
94
+ - **Ink is handed down by whatever paints.** `toneClass(tone)` is a palette and sets no ink. A surface that fills adds `INK_ON_FILL`; a tinted one uses `inkOnSurface("--card-foreground")`. Both declare `--ink` and `--ink-muted`, which every type primitive reads, falling back to the page. Paint a background without them and a nested `TypographyLabel` prints `--foreground` on your fill, which measures 2.34:1 on `--primary`. On a hue fill `--ink-muted` equals `--ink`: a filled control has one ink, and wanting a second rung means wanting a tinted surface.
95
+ - `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"`.
96
+ - `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.
90
97
  - `Callout`: `tone?: Tone`, `density?: "compact" | "editorial"`, `title`, `icon`, `action`.
91
98
  - `DisclosureGroup`: `type?: "multiple" | "single"`, `defaultValue` (matches the title string).
92
- - `TabsList`: `variant?: "default" | "line"`, `tone?: Tone` (default `primary`) the tone
93
- inks the marker only: on `line` the underline and the active icon. The boxed track's
94
- marker is deliberately flat, so a tone there paints nothing.
99
+ - `TabsList`: `variant?: "default" | "line"`, `tone?: Tone` (default `primary`). The tone
100
+ inks the marker alone, which on `line` means the underline and the active icon. The
101
+ boxed track keeps its marker flat, so a tone there paints nothing.
95
102
  - `TabGroup`: `tabs: readonly TabItem[]` (`{ value, label, icon?, content }`), `defaultValue?`,
96
103
  `value?`, `onValueChange?`, `variant?`, `tone?`, `iconPosition?: "inline-start" | "inline-end"`.
97
- `icon` is an element (`<Icons.Mic />`); the trigger sizes and inks it. Reach for it
98
- whenever the tabs are data do not rebuild it over `Tabs`/`TabsTrigger`.
104
+ `icon` is an element (`<Icons.Mic />`) that the trigger sizes and inks. Reach for it
105
+ whenever the tabs are data, and compose `Tabs`/`TabsTrigger` by hand only for a shape
106
+ it cannot express.
107
+
108
+ ## Writing copy
109
+
110
+ Every string a reader sees goes through this: page copy, section notes, button
111
+ labels, callout titles, error messages, and the comments you leave in the file.
112
+ Write the way a person writes when they know the subject and respect the reader's
113
+ time.
114
+
115
+ - **Say the thing.** State what is true and move on. Skip the setup sentence, the
116
+ restatement, and the closing summary of what you just said.
117
+ - **Define forwards.** "A tone sets three variables and the component paints from
118
+ them" over "the component does not branch on the tone". Reach for a negative
119
+ only when the absence is the point.
120
+ - **One em dash per page, at most.** Prefer a full stop, a comma, or a colon. The
121
+ same goes for a parenthetical aside mid-sentence.
122
+ - **No arrow glyphs in prose.** Write "3.44:1 in light, 6.75:1 in dark" rather
123
+ than joining the two with an arrow. Where a UI genuinely points at something,
124
+ render a real icon.
125
+ - **Watch the tacked-on clause.** "…, and it is the shape to reach for", "…, and
126
+ it exists so that…". The second half is usually the point: lead with it, or
127
+ make it its own sentence.
128
+ - **Break the triples.** "X, Y, and Z" as a rhetorical flourish, three clauses
129
+ joined by "and", and the "not just A, but B" shape all read as machine prose.
130
+ Two items, or a list.
131
+ - **Cut the throat-clearing.** No "it is worth noting", "it is important to",
132
+ "simply", "just", "of course", "in other words". No "this is not X, it is Y".
133
+ - **Skip the rationale.** A note explains what the reader needs to use the thing.
134
+ Design history, near-misses, and the case for a decision go in the commit
135
+ message or the docs, not in the copy.
136
+ - **Concrete over abstract.** Name the class, the prop, the file. "Five
137
+ components take the prop" beats "several components".
138
+ - **Vary the sentences.** A paragraph of same-length clauses reads as generated
139
+ even when every sentence is true.
140
+ - **Headings are labels.** "How it stays current", not "Why it cannot go stale".
141
+
142
+ Code samples and commit messages stay conventional. This is about prose.
99
143
 
100
144
  ## Common mistakes
101
145
 
102
- - `<p className="text-sm text-muted-foreground">` use `<TypographyMuted>`.
103
- - `<h2 className="text-lg font-semibold">` use `<TypographyH2>`, or
104
- `headingClass()` if you must pass classes to someone else's component.
146
+ - `<p className="text-sm text-muted-foreground">`: use `<TypographyMuted>`.
147
+ - `<h2 className="text-lg font-semibold">`: use `<TypographyH2>`, or
148
+ `headingClass()` to pass classes into someone else's component.
105
149
  - Importing `Card`, `Callout` or `Button` from `@supertype.ai/foundations`. They
106
150
  live in `@supertype.ai/foundations/blocks`.
107
- - `<Button variant="destructive">` or `variant="secondary"` those are tones, not
108
- variants. Write `variant="soft" tone="destructive"`, `variant="solid" tone="secondary"`.
109
- - `<Button size="icon-sm">` — there is one size ladder. Write `size="sm" icon`.
110
- - `<Badge variant="warning">` / `variant="supertype"` tones, and misspelled ones.
151
+ - `<Button variant="destructive">` or `variant="secondary"`: those are tones.
152
+ Write `variant="soft" tone="destructive"`, `variant="solid" tone="secondary"`.
153
+ - `<Button size="icon-sm">`: one size ladder. Write `size="sm" icon`.
154
+ - `<Badge variant="warning">` or `variant="supertype"`: tones, and misspelled ones.
111
155
  Write `variant="outline" tone="warn"` and `variant="solid" tone="brand"`.
112
- - `tone="neutral"`, `tone="foreground"` or `tone="accent"` all three were
113
- removed when the tone lists merged. Write `tone="muted"` for the first two and
114
- `tone="primary"` for the third.
115
- - An `<a>` or `<Link>` styled by hand to look like a button. Pass it as
116
- `render={<Link href="/x" />}`; the anchor keeps its link semantics.
117
- - `<TypographyMuted tone="default">` a type error. Use `TypographyP`.
156
+ - `tone="neutral"`, `tone="foreground"` or `tone="accent"`: all three went when the
157
+ tone lists merged. Write `tone="muted"` for the first two, `tone="primary"` for
158
+ the third.
159
+ - An `<a>` or `<Link>` styled by hand to look like a button, and equally
160
+ `render={<a href="/x" />}` or `render={<Link href="/x" />}` on anything that
161
+ takes an `href`. Write `<Button href="/x">`. `Button`, `Badge`, `Card` and
162
+ `TypographyLink` all take `href` and route it through one rule
163
+ (`resolveLink`): a scheme leaves the app and gets `target`/`rel`, a `#hash`
164
+ stays a plain anchor, everything else goes through the router's `Link` and
165
+ keeps the view transition. A hand-passed anchor skips all of that. `render` is
166
+ for an element that is genuinely not a link — and for `RailLink`, which takes a
167
+ router `Link` that way on purpose, since `essay/rail.tsx` has to import without
168
+ Next.
169
+ - Exporting a constant from a `"use client"` module and importing it into a server
170
+ component. It crosses the boundary as a stub, not a string, so an `href` built that
171
+ way arrives as a function. Keep shared constants in a plain module.
172
+ - `<TypographyMuted tone="default">`: a type error. Use `TypographyP`.
118
173
  - Binding fonts with `font.className` instead of `font.variable`. The className
119
174
  form sets `font-family` on the element and leaves the roles unresolved.
120
175
  - Omitting `@source '../node_modules/@supertype.ai/foundations/dist/**/*.js'` from
121
176
  the CSS entry. Tailwind then purges every class the package ships.
122
177
  - Omitting `@import "@supertype.ai/foundations/theme.css"`. `tokens.css` names the
123
178
  colour roles but holds no values, so the whole palette resolves to nothing.
124
- - Using `Accordion` for a static FAQ. `Disclosure` needs no JavaScript.
179
+ - Using `Accordion` for a static FAQ. `Disclosure` runs on the browser alone.
125
180
  - Adding a second `@custom-variant dark`. `tokens.css` already binds it.
126
181
 
127
182
  Run `npx foundations doctor` in the app to check the last four.
@@ -130,8 +185,8 @@ Run `npx foundations doctor` in the app to check the last four.
130
185
 
131
186
  Two classes change how everything renders, and both go on `<html>` or a subtree:
132
187
 
133
- - `.dark` re-points every colour token. It is bound by `tokens.css`, not by the
134
- OS setting.
188
+ - `.dark` re-points every colour token. `tokens.css` binds it to the class, so
189
+ the OS setting leaves it alone.
135
190
  - `.editorial` gives the heading role to the serif at weight 400 and retunes the
136
191
  whole heading ladder against an 18px body, instead of the product's 13px.
137
192
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supertype.ai/foundations",
3
- "version": "0.1.29",
3
+ "version": "0.1.30",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public",