@supertype.ai/foundations 0.1.27 → 0.1.29

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.
Files changed (47) hide show
  1. package/README.md +2 -3
  2. package/dist/blocks/badge.d.ts +24 -0
  3. package/dist/blocks/badge.js +80 -0
  4. package/dist/blocks/button.d.ts +24 -0
  5. package/dist/blocks/button.js +117 -0
  6. package/dist/blocks/callout.d.ts +2 -39
  7. package/dist/blocks/callout.js +13 -38
  8. package/dist/blocks/card.js +22 -2
  9. package/dist/blocks/focus.d.ts +9 -0
  10. package/dist/blocks/focus.js +9 -0
  11. package/dist/blocks/index.d.ts +3 -1
  12. package/dist/blocks/index.js +3 -1
  13. package/dist/blocks/interactive-accordion.js +2 -1
  14. package/dist/blocks/render-as.d.ts +11 -0
  15. package/dist/blocks/render-as.js +22 -0
  16. package/dist/blocks/segment.d.ts +22 -14
  17. package/dist/blocks/segment.js +23 -14
  18. package/dist/blocks/tabs.d.ts +52 -17
  19. package/dist/blocks/tabs.js +86 -37
  20. package/dist/cjs/eslint.js +17 -8
  21. package/dist/contrast.d.ts +29 -0
  22. package/dist/contrast.js +26 -8
  23. package/dist/eslint.d.ts +24 -6
  24. package/dist/eslint.js +16 -8
  25. package/dist/essay/essay.js +14 -6
  26. package/dist/essay/index.d.ts +1 -1
  27. package/dist/essay/index.js +1 -1
  28. package/dist/essay/layout.d.ts +36 -0
  29. package/dist/essay/layout.js +32 -0
  30. package/dist/essay/reading.d.ts +1 -1
  31. package/dist/index.d.ts +1 -0
  32. package/dist/index.js +5 -0
  33. package/dist/mdx.d.ts +26 -4
  34. package/dist/mdx.js +30 -4
  35. package/dist/seo.d.ts +4 -2
  36. package/dist/seo.js +14 -5
  37. package/dist/tone.d.ts +101 -0
  38. package/dist/tone.js +111 -0
  39. package/dist/typography/header.d.ts +30 -0
  40. package/dist/typography/header.js +7 -2
  41. package/dist/typography/highlight.d.ts +4 -4
  42. package/dist/typography/highlight.js +4 -4
  43. package/dist/typography/paragraph.d.ts +3 -13
  44. package/dist/typography/paragraph.js +13 -9
  45. package/llms.txt +34 -12
  46. package/package.json +2 -1
  47. package/src/theme.css +70 -26
@@ -1,4 +1,5 @@
1
1
  import type { ComponentProps, ComponentType, ReactNode } from "react";
2
+ import type { TocHeading } from "./toc.js";
2
3
  /**
3
4
  * Three tracks with the third empty: two would push the prose off-centre the
4
5
  * moment an aside appeared, setting body copy on a different axis per page.
@@ -24,6 +25,41 @@ import type { ComponentProps, ComponentType, ReactNode } from "react";
24
25
  export declare function EssayColumns({ aside, children, className, ...props }: ComponentProps<"div"> & {
25
26
  aside?: ReactNode;
26
27
  }): import("react").JSX.Element;
28
+ /**
29
+ * The margin track's contents, pinned as the column scrolls.
30
+ *
31
+ * The offset is stated here and nowhere else: it has to clear the same sticky
32
+ * site nav that `EssaySection`'s `scroll-mt` clears, and two literals a file
33
+ * apart is how an anchored heading ends up under the chrome that the rail
34
+ * scrolled it to.
35
+ */
36
+ export declare function EssayAside({ children, className, }: {
37
+ children: ReactNode;
38
+ className?: string;
39
+ }): import("react").JSX.Element;
40
+ /**
41
+ * The join between a header and the body under it: the one place in the package that draws
42
+ * it, so a seam cannot be ruled twice by a header and a layout that cannot see each other.
43
+ *
44
+ * The rule is for the narrow layout alone. Past `@6xl` the margin rail marks the join, and
45
+ * the header's own bottom padding is the whole of the gap.
46
+ */
47
+ export declare function EssayBody({ children, className, }: {
48
+ children: ReactNode;
49
+ className?: string;
50
+ }): import("react").JSX.Element;
51
+ /**
52
+ * The reading column with a scroll-spied rail in its margin: an article whose
53
+ * body is prose or MDX, rather than the declared sections `EssayLayout` sets.
54
+ *
55
+ * The rail is dropped when a piece has no headings, so a short post gets a
56
+ * centred measure instead of a margin holding an empty nav.
57
+ */
58
+ export declare function ReadingLayout({ headings, children, className, }: {
59
+ headings: readonly TocHeading[];
60
+ children: ReactNode;
61
+ className?: string;
62
+ }): import("react").JSX.Element;
27
63
  /** A separator between meta items. Decorative, so it is hidden from assistive tech. */
28
64
  export declare function MetaDot({ className }: {
29
65
  className?: string;
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { cn } from "../cn.js";
3
+ import { ReadingRail } from "./reading.js";
3
4
  /**
4
5
  * Three tracks with the third empty: two would push the prose off-centre the
5
6
  * moment an aside appeared, setting body copy on a different axis per page.
@@ -25,6 +26,37 @@ import { cn } from "../cn.js";
25
26
  export function EssayColumns({ aside, children, className, ...props }) {
26
27
  return (_jsx("div", { className: "@container w-full", children: _jsxs("div", { className: cn("mx-auto grid w-full max-w-6xl gap-10 px-6", "@6xl:grid-cols-[1fr_minmax(0,42rem)_1fr] @6xl:gap-0", "@7xl:max-w-7xl @7xl:grid-cols-[1fr_minmax(0,44rem)_1fr]", "@min-[84rem]:max-w-[84rem] @min-[84rem]:grid-cols-[1fr_minmax(0,46rem)_1fr]", className), ...props, children: [_jsx("div", { className: "hidden @6xl:block @6xl:pr-10", children: aside }), _jsx("div", { className: "mx-auto w-full min-w-0 max-w-2xl @6xl:max-w-none", children: children }), _jsx("div", { className: "hidden @6xl:block" })] }) }));
27
28
  }
29
+ /**
30
+ * The margin track's contents, pinned as the column scrolls.
31
+ *
32
+ * The offset is stated here and nowhere else: it has to clear the same sticky
33
+ * site nav that `EssaySection`'s `scroll-mt` clears, and two literals a file
34
+ * apart is how an anchored heading ends up under the chrome that the rail
35
+ * scrolled it to.
36
+ */
37
+ export function EssayAside({ children, className, }) {
38
+ return _jsx("div", { className: cn("sticky top-24", className), children: children });
39
+ }
40
+ /**
41
+ * The join between a header and the body under it: the one place in the package that draws
42
+ * it, so a seam cannot be ruled twice by a header and a layout that cannot see each other.
43
+ *
44
+ * The rule is for the narrow layout alone. Past `@6xl` the margin rail marks the join, and
45
+ * the header's own bottom padding is the whole of the gap.
46
+ */
47
+ export function EssayBody({ children, className, }) {
48
+ return (_jsx("div", { className: cn("border-t border-border pt-12 @6xl:border-t-0 @6xl:pt-0", className), children: children }));
49
+ }
50
+ /**
51
+ * The reading column with a scroll-spied rail in its margin: an article whose
52
+ * body is prose or MDX, rather than the declared sections `EssayLayout` sets.
53
+ *
54
+ * The rail is dropped when a piece has no headings, so a short post gets a
55
+ * centred measure instead of a margin holding an empty nav.
56
+ */
57
+ export function ReadingLayout({ headings, children, className, }) {
58
+ return (_jsx(EssayColumns, { className: cn("pb-16 sm:pb-24", className), aside: headings.length > 0 ? (_jsx(EssayAside, { children: _jsx(ReadingRail, { headings: headings }) })) : undefined, children: _jsx(EssayBody, { children: children }) }));
59
+ }
28
60
  /** A separator between meta items. Decorative, so it is hidden from assistive tech. */
29
61
  export function MetaDot({ className }) {
30
62
  return (_jsx("span", { "aria-hidden": true, className: cn("text-muted-foreground/50", className), children: "\u00B7" }));
@@ -12,6 +12,6 @@ export declare function ReadingProgressBar({ className }: {
12
12
  * stores, so mounting them together costs one scroll subscription, not two.
13
13
  */
14
14
  export declare function ReadingRail({ headings, className, }: {
15
- headings: TocHeading[];
15
+ headings: readonly TocHeading[];
16
16
  className?: string;
17
17
  }): import("react").JSX.Element | null;
package/dist/index.d.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export { cn } from "./cn.js";
2
+ export { toneClass, impliedTone, type Tone } from "./tone.js";
2
3
  export * from "./typography/index.js";
package/dist/index.js CHANGED
@@ -1,4 +1,9 @@
1
1
  export { cn } from "./cn.js";
2
+ // The semantic colour vocabulary. Exported from the root because typography
3
+ // takes it too — a link has a tone, and it is the same seven a button has.
4
+ // `toneClass` only: the raw table and its derived half used to ship separately,
5
+ // and the order they were combined in was load-bearing.
6
+ export { toneClass, impliedTone } from "./tone.js";
2
7
  export * from "./typography/index.js";
3
8
  // NOTE: blocks, the MDX map, and the Shiki plugin are all deliberately absent
4
9
  // from this barrel.
package/dist/mdx.d.ts CHANGED
@@ -1,9 +1,30 @@
1
- import type { ComponentProps } from "react";
1
+ import { type ComponentProps, type ReactNode } from "react";
2
2
  import { Disclosure, DisclosureGroup } from "./blocks/accordion.js";
3
3
  import { Callout } from "./blocks/callout.js";
4
4
  import { Card, Cards } from "./blocks/card.js";
5
5
  import { Step, Steps } from "./blocks/steps.js";
6
- import { TabGroup, Tab } from "./blocks/tabs.js";
6
+ /**
7
+ * `<Tabs items={["npm","pnpm"]}>` with a `<Tab>` per panel: the shape an MDX author
8
+ * writes, and the only place in the package that speaks it.
9
+ *
10
+ * Children pair with `items` **by position**, since a caller writing markdown has no
11
+ * value to bind. `value` on a `<Tab>` is for the author's eye and is not matched —
12
+ * matching it would silently drop a panel the moment a label was edited. The label is
13
+ * the value here, so the strip still survives a reorder.
14
+ *
15
+ * This lived in `TabGroup` and made `TabGroup` unusable everywhere else: an app has data,
16
+ * not positional children, so every one of them rebuilt the adapter by hand. Positional
17
+ * children are an authoring convenience, not a component API, so they stop here.
18
+ */
19
+ declare function MdxTabs({ items, children }: {
20
+ items: string[];
21
+ children: ReactNode;
22
+ }): import("react").JSX.Element;
23
+ /** `value` names the panel at the call site; it is not used for matching. */
24
+ declare function MdxTab({ children }: {
25
+ value?: string;
26
+ children: ReactNode;
27
+ }): import("react").JSX.Element;
7
28
  /**
8
29
  * Rendered from markdown syntax, so there is no call site and no knob — a knob
9
30
  * here is one every project turns differently. Headings carry variants and this
@@ -19,8 +40,8 @@ export declare const proseMdxComponents: {
19
40
  Accordions: typeof DisclosureGroup;
20
41
  Accordion: typeof Disclosure;
21
42
  Banner: (props: ComponentProps<typeof Callout>) => import("react").JSX.Element;
22
- Tabs: typeof TabGroup;
23
- Tab: typeof Tab;
43
+ Tabs: typeof MdxTabs;
44
+ Tab: typeof MdxTab;
24
45
  Steps: typeof Steps;
25
46
  Step: typeof Step;
26
47
  h1: (props: ComponentProps<"h1">) => import("react").JSX.Element;
@@ -45,3 +66,4 @@ export declare const proseMdxComponents: {
45
66
  th: (props: ComponentProps<"th">) => import("react").JSX.Element;
46
67
  td: (props: ComponentProps<"td">) => import("react").JSX.Element;
47
68
  };
69
+ export {};
package/dist/mdx.js CHANGED
@@ -1,4 +1,5 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Children } from "react";
2
3
  import { TypographyH1, TypographyH2, TypographyH3, TypographyH4, } from "./typography/header.js";
3
4
  import { cn } from "./cn.js";
4
5
  import Image from "next/image";
@@ -6,8 +7,33 @@ import { Disclosure, DisclosureGroup } from "./blocks/accordion.js";
6
7
  import { Callout } from "./blocks/callout.js";
7
8
  import { Card, Cards } from "./blocks/card.js";
8
9
  import { Step, Steps } from "./blocks/steps.js";
9
- import { TabGroup, Tab } from "./blocks/tabs.js";
10
+ import { TabGroup } from "./blocks/tabs.js";
10
11
  import { TypographyProse, TypographyProseList, TypographyLink, } from "./typography/paragraph.js";
12
+ /**
13
+ * `<Tabs items={["npm","pnpm"]}>` with a `<Tab>` per panel: the shape an MDX author
14
+ * writes, and the only place in the package that speaks it.
15
+ *
16
+ * Children pair with `items` **by position**, since a caller writing markdown has no
17
+ * value to bind. `value` on a `<Tab>` is for the author's eye and is not matched —
18
+ * matching it would silently drop a panel the moment a label was edited. The label is
19
+ * the value here, so the strip still survives a reorder.
20
+ *
21
+ * This lived in `TabGroup` and made `TabGroup` unusable everywhere else: an app has data,
22
+ * not positional children, so every one of them rebuilt the adapter by hand. Positional
23
+ * children are an authoring convenience, not a component API, so they stop here.
24
+ */
25
+ function MdxTabs({ items, children }) {
26
+ const panels = Children.toArray(children);
27
+ return (_jsx(TabGroup, { tabs: items.map((label, i) => ({
28
+ value: label,
29
+ label,
30
+ content: panels[i],
31
+ })) }));
32
+ }
33
+ /** `value` names the panel at the call site; it is not used for matching. */
34
+ function MdxTab({ children }) {
35
+ return _jsx(_Fragment, { children: children });
36
+ }
11
37
  /**
12
38
  * Rendered from markdown syntax, so there is no call site and no knob — a knob
13
39
  * here is one every project turns differently. Headings carry variants and this
@@ -26,8 +52,8 @@ export const proseMdxComponents = {
26
52
  Accordions: DisclosureGroup,
27
53
  Accordion: Disclosure,
28
54
  Banner: (props) => (_jsx(Callout, { density: "editorial", ...props })),
29
- Tabs: TabGroup,
30
- Tab,
55
+ Tabs: MdxTabs,
56
+ Tab: MdxTab,
31
57
  Steps,
32
58
  Step,
33
59
  h1: (props) => (_jsx(TypographyH1, { variant: "display", ...props })),
package/dist/seo.d.ts CHANGED
@@ -76,9 +76,10 @@ export declare function createSeo(config: SeoConfig): {
76
76
  "@type": string;
77
77
  url: string;
78
78
  } | undefined;
79
- "@type": string;
80
79
  name: string;
81
80
  url: string;
81
+ "@id"?: string | undefined;
82
+ "@type": string;
82
83
  };
83
84
  mainEntityOfPage: {
84
85
  "@type": string;
@@ -154,9 +155,10 @@ export declare function createSeo(config: SeoConfig): {
154
155
  "@type": string;
155
156
  url: string;
156
157
  } | undefined;
157
- "@type": string;
158
158
  name: string;
159
159
  url: string;
160
+ "@id"?: string | undefined;
161
+ "@type": string;
160
162
  };
161
163
  author?: {
162
164
  jobTitle?: string | undefined;
package/dist/seo.js CHANGED
@@ -7,11 +7,6 @@ export function createSeo(config) {
7
7
  const { baseUrl, siteName, defaultOgImage, logoUrl, articleBasePath = "notes", publisherUrl, trailingSlash = false, } = config;
8
8
  /** Resolves a possibly-relative URL against the site origin. */
9
9
  const absolute = (url) => url.startsWith("http") ? url : `${baseUrl}${url.startsWith("/") ? "" : "/"}${url}`;
10
- /**
11
- * Stable `@id` anchors for the site's core entities. Pages reference these
12
- * rather than re-declaring an Organization node, so crawlers merge them into
13
- * one entity instead of collecting near-duplicates.
14
- */
15
10
  /**
16
11
  * A page route in the shape this site actually serves. A URL already carrying
17
12
  * a query, a fragment or a file extension is left alone — only a route gets
@@ -34,10 +29,24 @@ export function createSeo(config) {
34
29
  ...(author.jobTitle ? { jobTitle: author.jobTitle } : {}),
35
30
  };
36
31
  };
32
+ /**
33
+ * Stable `@id` anchors for the site's core entities. Pages reference these
34
+ * rather than re-declaring an Organization node, so crawlers merge them into
35
+ * one entity instead of collecting near-duplicates.
36
+ */
37
37
  const ORG_ID = `${baseUrl}/#organization`;
38
38
  const WEBSITE_ID = `${baseUrl}/#website`;
39
+ /**
40
+ * The publisher every Article and WebPage node points at. It carries `@id`
41
+ * only when the publisher is this site: without one, each page declares a
42
+ * fresh Organization and a crawler has no way to merge a corpus of them into
43
+ * the canonical entity the site emits once. A `publisherUrl` naming a
44
+ * different site gets no `@id` — `ORG_ID` is derived from `baseUrl`, so
45
+ * stamping it there would claim someone else's publisher as this one.
46
+ */
39
47
  const publisher = {
40
48
  "@type": "Organization",
49
+ ...(!publisherUrl || publisherUrl === baseUrl ? { "@id": ORG_ID } : {}),
41
50
  name: siteName,
42
51
  url: publisherUrl ?? baseUrl,
43
52
  ...(logoUrl
package/dist/tone.d.ts ADDED
@@ -0,0 +1,101 @@
1
+ /**
2
+ * One semantic colour vocabulary, for every component that carries meaning in a
3
+ * hue: Button, Callout, TypographyLink. Before this there were three lists —
4
+ * Callout's `muted`, TypographyLink's `foreground` and a button's `default` were
5
+ * three spellings of "no meaning at all", and `accent` named `--accent` on a
6
+ * button and `--primary` in a callout.
7
+ *
8
+ * A component that wants a tone does not get to invent a name for one.
9
+ *
10
+ * Seven tones, seven tokens, one to one. That is the test for admitting a new
11
+ * one: `muted` is `--muted`, `warn` is `--warn`, and a proposed tone with no
12
+ * token of its own is a second name for a tone that already has one. It is what
13
+ * ruled out `neutral` (no such token, and `muted` is the word the rest of the
14
+ * package already uses — `--muted-foreground`, `TypographyMuted`,
15
+ * `TypographyP tone="muted"`), `accent` (that is `--primary`'s hover tint, so a
16
+ * washed `primary` renders the same control), and `info` (a real token, but
17
+ * `success | warn | destructive` is already the complete good/careful/bad
18
+ * triad, and nothing in either app had ever reached for a fourth).
19
+ *
20
+ * The rule the package already applies to every tinted role holds here too, and
21
+ * is why each row names three values rather than one: `--tone-fill` is a surface,
22
+ * `--tone-ink` is the label printed on it, and `--tone-hue` is the same colour
23
+ * used as words. A fill is a mark and clears 3:1; an ink is read and clears
24
+ * 4.5:1. Tuning one value to do both jobs sinks whichever job it was not tuned
25
+ * for. `checkSignals` in /contrast is the test.
26
+ *
27
+ * Custom properties rather than thirty-five class strings: a tone declares, a
28
+ * component spends. `Button` has five variants and `Callout` two densities, and
29
+ * neither has a per-tone branch anywhere in it.
30
+ */
31
+ /**
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`.
35
+ *
36
+ * Its hue is `--foreground` rather than `--muted-foreground` on purpose. `muted`
37
+ * says the control carries no meaning, not that it carries less contrast — a
38
+ * cancel button beside a save button is quiet because it is not filled in, and
39
+ * its label still has to be read.
40
+ *
41
+ * `brand` is the consumer's own identity, and the only row whose token the
42
+ * package does not define. It falls back to `--primary`, so an app that has no
43
+ * identity hue of its own gets its principal one — the tone stays meaningful
44
+ * everywhere rather than rendering nothing in the apps that skipped it.
45
+ */
46
+ export declare const TONE: {
47
+ /** No meaning: chrome, toolbars, anything that must not compete. */
48
+ readonly muted: "[--tone-fill:var(--muted)] [--tone-ink:var(--foreground)] [--tone-hue:var(--foreground)] [--tone-line:var(--border)]";
49
+ /** The principal action, and the package's default wherever a tone is optional. */
50
+ readonly primary: "[--tone-fill:var(--primary)] [--tone-ink:var(--primary-foreground)] [--tone-hue:var(--primary)]";
51
+ /** The warm accent. `--secondary-ink` is its readable cut; the fill is not. */
52
+ readonly secondary: "[--tone-fill:var(--secondary)] [--tone-ink:var(--secondary-foreground)] [--tone-hue:var(--secondary-ink)]";
53
+ /** The consumer's identity hue, if it defined one. Otherwise the principal one. */
54
+ readonly brand: "[--tone-fill:var(--brand,var(--primary))] [--tone-ink:var(--tint-foreground)] [--tone-hue:var(--brand-ink,var(--primary))]";
55
+ /** It worked. */
56
+ readonly success: "[--tone-fill:var(--success)] [--tone-ink:var(--tint-foreground)] [--tone-hue:var(--success-ink)]";
57
+ /** A footgun: the reader can still proceed, but not blindly. */
58
+ readonly warn: "[--tone-fill:var(--warn)] [--tone-ink:var(--tint-foreground)] [--tone-hue:var(--warn-ink)]";
59
+ /** It deletes something, or it already failed. */
60
+ readonly destructive: "[--tone-fill:var(--destructive)] [--tone-ink:var(--destructive-foreground)] [--tone-hue:var(--destructive)]";
61
+ };
62
+ export type Tone = keyof typeof TONE;
63
+ /**
64
+ * What every tone is worth once mixed, derived once here rather than eight times
65
+ * above. Declared before `TONE` in any class list, so `neutral` can override
66
+ * `--tone-line`.
67
+ *
68
+ * Expressed as variables rather than through Tailwind's `/10` modifier because
69
+ * the modifier's support for arbitrary custom properties is a moving target,
70
+ * while `color-mix` is what the modifier compiles to anyway — the same CSS, one
71
+ * layer less of trust.
72
+ *
73
+ * `--tone-fill-hover` mixes toward `--foreground` rather than darkening by a
74
+ * fixed amount, so a filled control deepens on the light theme and lifts on the
75
+ * dark one from a single declaration. A `dark:` override here is what the
76
+ * package's own ESLint rule exists to prevent.
77
+ */
78
+ export declare const TONE_SURFACE: string;
79
+ /**
80
+ * A tone, as one class list. This is the only thing a component should need.
81
+ *
82
+ * It was two — `cn(TONE_SURFACE, TONE[tone])` — and the order was load-bearing
83
+ * in a way nothing enforced: the derived values have to be declared first so
84
+ * `muted` can override `--tone-line` with `--border`. Getting that backwards
85
+ * gave every quiet control a hairline washed out of its own ink, and the only
86
+ * thing standing between a caller and that bug was a comment. Two arguments
87
+ * whose order matters and whose values always travel together is one argument.
88
+ */
89
+ export declare const toneClass: (tone: Tone) => string;
90
+ /**
91
+ * What an unstated tone means, given how much ink the component is spending.
92
+ * Shared, because `Button` and `Badge` both need it and two copies of a default
93
+ * is how two components come to disagree about what saying nothing means.
94
+ *
95
+ * Filling a control in is how a page says *this is the action*, so a filled one
96
+ * with nothing else stated is the principal one; anything less is chrome until a
97
+ * call site says otherwise. Measured, not assumed: 244 of the 246 buttons across
98
+ * both apps that name a variant name `outline` or `ghost`, and every one wants
99
+ * the page's own ink.
100
+ */
101
+ export declare const impliedTone: (variant: string | null | undefined) => Tone;
package/dist/tone.js ADDED
@@ -0,0 +1,111 @@
1
+ /**
2
+ * One semantic colour vocabulary, for every component that carries meaning in a
3
+ * hue: Button, Callout, TypographyLink. Before this there were three lists —
4
+ * Callout's `muted`, TypographyLink's `foreground` and a button's `default` were
5
+ * three spellings of "no meaning at all", and `accent` named `--accent` on a
6
+ * button and `--primary` in a callout.
7
+ *
8
+ * A component that wants a tone does not get to invent a name for one.
9
+ *
10
+ * Seven tones, seven tokens, one to one. That is the test for admitting a new
11
+ * one: `muted` is `--muted`, `warn` is `--warn`, and a proposed tone with no
12
+ * token of its own is a second name for a tone that already has one. It is what
13
+ * ruled out `neutral` (no such token, and `muted` is the word the rest of the
14
+ * package already uses — `--muted-foreground`, `TypographyMuted`,
15
+ * `TypographyP tone="muted"`), `accent` (that is `--primary`'s hover tint, so a
16
+ * washed `primary` renders the same control), and `info` (a real token, but
17
+ * `success | warn | destructive` is already the complete good/careful/bad
18
+ * triad, and nothing in either app had ever reached for a fourth).
19
+ *
20
+ * The rule the package already applies to every tinted role holds here too, and
21
+ * is why each row names three values rather than one: `--tone-fill` is a surface,
22
+ * `--tone-ink` is the label printed on it, and `--tone-hue` is the same colour
23
+ * used as words. A fill is a mark and clears 3:1; an ink is read and clears
24
+ * 4.5:1. Tuning one value to do both jobs sinks whichever job it was not tuned
25
+ * for. `checkSignals` in /contrast is the test.
26
+ *
27
+ * Custom properties rather than thirty-five class strings: a tone declares, a
28
+ * component spends. `Button` has five variants and `Callout` two densities, and
29
+ * neither has a per-tone branch anywhere in it.
30
+ */
31
+ /**
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`.
35
+ *
36
+ * Its hue is `--foreground` rather than `--muted-foreground` on purpose. `muted`
37
+ * says the control carries no meaning, not that it carries less contrast — a
38
+ * cancel button beside a save button is quiet because it is not filled in, and
39
+ * its label still has to be read.
40
+ *
41
+ * `brand` is the consumer's own identity, and the only row whose token the
42
+ * package does not define. It falls back to `--primary`, so an app that has no
43
+ * identity hue of its own gets its principal one — the tone stays meaningful
44
+ * everywhere rather than rendering nothing in the apps that skipped it.
45
+ */
46
+ export const TONE = {
47
+ /** No meaning: chrome, toolbars, anything that must not compete. */
48
+ muted: "[--tone-fill:var(--muted)] [--tone-ink:var(--foreground)] [--tone-hue:var(--foreground)] [--tone-line:var(--border)]",
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)]",
51
+ /** The warm accent. `--secondary-ink` is its readable cut; the fill is not. */
52
+ secondary: "[--tone-fill:var(--secondary)] [--tone-ink:var(--secondary-foreground)] [--tone-hue:var(--secondary-ink)]",
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))]",
55
+ /** It worked. */
56
+ success: "[--tone-fill:var(--success)] [--tone-ink:var(--tint-foreground)] [--tone-hue:var(--success-ink)]",
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)]",
59
+ /** It deletes something, or it already failed. */
60
+ destructive: "[--tone-fill:var(--destructive)] [--tone-ink:var(--destructive-foreground)] [--tone-hue:var(--destructive)]",
61
+ };
62
+ /**
63
+ * What every tone is worth once mixed, derived once here rather than eight times
64
+ * above. Declared before `TONE` in any class list, so `neutral` can override
65
+ * `--tone-line`.
66
+ *
67
+ * Expressed as variables rather than through Tailwind's `/10` modifier because
68
+ * the modifier's support for arbitrary custom properties is a moving target,
69
+ * while `color-mix` is what the modifier compiles to anyway — the same CSS, one
70
+ * layer less of trust.
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
75
+ * package's own ESLint rule exists to prevent.
76
+ */
77
+ export const TONE_SURFACE = [
78
+ /** A hairline, or a rule carrying the tone. */
79
+ "[--tone-line:color-mix(in_oklab,var(--tone-hue)_45%,transparent)]",
80
+ /** A panel's tint: large area, so barely there. */
81
+ "[--tone-veil:color-mix(in_oklab,var(--tone-hue)_5%,transparent)]",
82
+ /** A control's tint at rest. */
83
+ "[--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)]",
86
+ /** A filled control under the pointer. */
87
+ "[--tone-fill-hover:color-mix(in_oklab,var(--tone-fill)_88%,var(--foreground))]",
88
+ ].join(" ");
89
+ /**
90
+ * A tone, as one class list. This is the only thing a component should need.
91
+ *
92
+ * It was two — `cn(TONE_SURFACE, TONE[tone])` — and the order was load-bearing
93
+ * in a way nothing enforced: the derived values have to be declared first so
94
+ * `muted` can override `--tone-line` with `--border`. Getting that backwards
95
+ * gave every quiet control a hairline washed out of its own ink, and the only
96
+ * thing standing between a caller and that bug was a comment. Two arguments
97
+ * whose order matters and whose values always travel together is one argument.
98
+ */
99
+ export const toneClass = (tone) => `${TONE_SURFACE} ${TONE[tone]}`;
100
+ /**
101
+ * What an unstated tone means, given how much ink the component is spending.
102
+ * Shared, because `Button` and `Badge` both need it and two copies of a default
103
+ * is how two components come to disagree about what saying nothing means.
104
+ *
105
+ * Filling a control in is how a page says *this is the action*, so a filled one
106
+ * with nothing else stated is the principal one; anything less is chrome until a
107
+ * call site says otherwise. Measured, not assumed: 244 of the 246 buttons across
108
+ * both apps that name a variant name `outline` or `ghost`, and every one wants
109
+ * the page's own ink.
110
+ */
111
+ export const impliedTone = (variant) => variant == null || variant === "solid" ? "primary" : "muted";
@@ -1,5 +1,35 @@
1
1
  import { type VariantProps } from "class-variance-authority";
2
2
  import { type WithAs } from "./as.js";
3
+ /**
4
+ * The heading ladder. Four levels, one rung each.
5
+ *
6
+ * A heading does not pick its size — `--text-h1`…`--text-h4` in type.css do, and
7
+ * `.editorial` retunes all four together. That is the whole design: size is a
8
+ * property of the SURFACE, and level is the only thing a call site knows. When
9
+ * the rungs were shared with body copy the call site had to know both, which is
10
+ * how `larger` and `entry` appeared — variants whose entire job was to climb out
11
+ * of a rung that read fine in the product and landed under the paragraph on a
12
+ * marketing page. Retuning a surface now means editing two lines of CSS.
13
+ *
14
+ * `display` is a role rather than a size: the landing-page heading that has to
15
+ * outrank the same level in the docs. Size is all it changes — the slant it once
16
+ * carried turned out to be unwanted on every surface.
17
+ *
18
+ * Tailwind scans comments — never spell a class out here or it becomes a real
19
+ * utility.
20
+ */
21
+ /**
22
+ * The heading face, stated once. Anything wearing it composes this rather than
23
+ * respelling it: a second literal here is a level that forked, and a literal
24
+ * weight beside the face survives into `.editorial` and synthesises the
25
+ * single-weight serif. viably asserts there is exactly one of these strings.
26
+ *
27
+ * Exported so a non-heading can ask for the face alone. `headingClass()` below is the whole
28
+ * h2 ramp — face, anchor offset, margin reset, rung — and is right only for something that
29
+ * IS a heading wearing another tag. A pull quote is not, and took the ramp anyway because
30
+ * the face had no door, arriving with a `scroll-m-20` for an anchor it does not have.
31
+ */
32
+ export declare const headingFace = "font-heading font-[number:var(--heading-weight)]";
3
33
  declare const h1Variants: (props?: ({
4
34
  variant?: "default" | "display" | null | undefined;
5
35
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
@@ -25,9 +25,14 @@ import { TextAs } from "./as.js";
25
25
  * respelling it: a second literal here is a level that forked, and a literal
26
26
  * weight beside the face survives into `.editorial` and synthesises the
27
27
  * single-weight serif. viably asserts there is exactly one of these strings.
28
+ *
29
+ * Exported so a non-heading can ask for the face alone. `headingClass()` below is the whole
30
+ * h2 ramp — face, anchor offset, margin reset, rung — and is right only for something that
31
+ * IS a heading wearing another tag. A pull quote is not, and took the ramp anyway because
32
+ * the face had no door, arriving with a `scroll-m-20` for an anchor it does not have.
28
33
  */
29
- const HEADING_FACE = "font-heading font-[number:var(--heading-weight)]";
30
- const HEADING_BASE = `scroll-m-20 ${HEADING_FACE} text-foreground`;
34
+ export const headingFace = "font-heading font-[number:var(--heading-weight)]";
35
+ const HEADING_BASE = `scroll-m-20 ${headingFace} text-foreground`;
31
36
  const h1Variants = cva(`${HEADING_BASE} tracking-tight`, {
32
37
  variants: {
33
38
  variant: {
@@ -14,10 +14,10 @@ import type { ComponentProps } from "react";
14
14
  declare const MARKER_TONES: {
15
15
  readonly primary: "var(--primary)";
16
16
  readonly success: "var(--success)";
17
- readonly ochre: "var(--ochre-foreground)";
18
- readonly terracotta: "var(--terracotta-foreground)";
19
- readonly sage: "var(--sage-foreground)";
20
- readonly fig: "var(--fig-foreground)";
17
+ readonly ochre: "var(--ochre-ink)";
18
+ readonly terracotta: "var(--terracotta-ink)";
19
+ readonly sage: "var(--sage-ink)";
20
+ readonly fig: "var(--fig-ink)";
21
21
  };
22
22
  export type HighlightTone = keyof typeof MARKER_TONES;
23
23
  /**
@@ -15,10 +15,10 @@ import { cn } from "../cn.js";
15
15
  const MARKER_TONES = {
16
16
  primary: "var(--primary)",
17
17
  success: "var(--success)",
18
- ochre: "var(--ochre-foreground)",
19
- terracotta: "var(--terracotta-foreground)",
20
- sage: "var(--sage-foreground)",
21
- fig: "var(--fig-foreground)",
18
+ ochre: "var(--ochre-ink)",
19
+ terracotta: "var(--terracotta-ink)",
20
+ sage: "var(--sage-ink)",
21
+ fig: "var(--fig-ink)",
22
22
  };
23
23
  /** Wash density at a fraction of the theme's base alpha. */
24
24
  const ink = (weight) => `color-mix(in srgb, var(--marker) calc(var(--marker-alpha) * ${weight}), transparent)`;
@@ -1,12 +1,13 @@
1
1
  import { type VariantProps } from "class-variance-authority";
2
2
  import type { ComponentProps, ReactNode } from "react";
3
+ import { type Tone } from "../tone.js";
3
4
  import { type WithAs } from "./as.js";
4
5
  /** The body layer: two axes and no more. A paragraph picks a rung and an ink; a
5
6
  * caption is always secondary ink and picks a size. `lead` was a third rung a
6
7
  * breakpoint away from `prose`, and its standfirst role is now the eyebrow's. */
7
8
  declare const pVariants: (props?: ({
8
9
  variant?: "ui" | "prose" | null | undefined;
9
- tone?: "default" | "muted" | null | undefined;
10
+ tone?: "muted" | "default" | null | undefined;
10
11
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
11
12
  export type ParagraphVariants = VariantProps<typeof pVariants>;
12
13
  export declare function TypographyP({ className, variant, tone, children, ...props }: ComponentProps<"p"> & ParagraphVariants): import("react").JSX.Element;
@@ -113,21 +114,10 @@ export declare function TypographyStat({ className, size, figures, children, ...
113
114
  * optical correction — the mono face carries a taller x-height than the sans.
114
115
  */
115
116
  export declare function TypographyInlineCode({ className, children, ...props }: ComponentProps<"code">): import("react").JSX.Element;
116
- /**
117
- * A statement about the surface, not the link: `foreground` inside a paragraph,
118
- * `primary` when the link is the point of the line, `secondary` for a note
119
- * beneath a hero where `primary` would compete with the CTA beside it.
120
- */
121
- declare const LINK_TONES: {
122
- readonly foreground: "font-medium text-foreground";
123
- readonly primary: "font-medium text-primary";
124
- readonly secondary: "text-secondary-ink";
125
- };
126
- export type LinkTone = keyof typeof LINK_TONES;
127
117
  type TypographyLinkProps = Omit<ComponentProps<"a">, "href"> & {
128
118
  href: string;
129
119
  children: ReactNode;
130
- tone?: LinkTone;
120
+ tone?: Tone;
131
121
  /** Defaults on for an off-site link. Turn it off for one that starts a flow the reader should stay in. */
132
122
  newTab?: boolean;
133
123
  /**