@supertype.ai/foundations 0.1.24

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 (72) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +369 -0
  3. package/bin/foundations.mjs +713 -0
  4. package/dist/blocks/accordion.d.ts +23 -0
  5. package/dist/blocks/accordion.js +59 -0
  6. package/dist/blocks/callout.d.ts +57 -0
  7. package/dist/blocks/callout.js +61 -0
  8. package/dist/blocks/card.d.ts +34 -0
  9. package/dist/blocks/card.js +56 -0
  10. package/dist/blocks/index.d.ts +7 -0
  11. package/dist/blocks/index.js +7 -0
  12. package/dist/blocks/interactive-accordion.d.ts +13 -0
  13. package/dist/blocks/interactive-accordion.js +27 -0
  14. package/dist/blocks/segment.d.ts +37 -0
  15. package/dist/blocks/segment.js +37 -0
  16. package/dist/blocks/steps.d.ts +10 -0
  17. package/dist/blocks/steps.js +13 -0
  18. package/dist/blocks/tabs.d.ts +32 -0
  19. package/dist/blocks/tabs.js +69 -0
  20. package/dist/cjs/eslint.js +146 -0
  21. package/dist/cjs/package.json +3 -0
  22. package/dist/cn.d.ts +2 -0
  23. package/dist/cn.js +5 -0
  24. package/dist/contrast.d.ts +47 -0
  25. package/dist/contrast.js +255 -0
  26. package/dist/eslint.d.ts +74 -0
  27. package/dist/eslint.js +138 -0
  28. package/dist/essay/contents.d.ts +10 -0
  29. package/dist/essay/contents.js +17 -0
  30. package/dist/essay/essay.d.ts +125 -0
  31. package/dist/essay/essay.js +92 -0
  32. package/dist/essay/index.d.ts +7 -0
  33. package/dist/essay/index.js +9 -0
  34. package/dist/essay/layout.d.ts +72 -0
  35. package/dist/essay/layout.js +77 -0
  36. package/dist/essay/rail.d.ts +15 -0
  37. package/dist/essay/rail.js +26 -0
  38. package/dist/essay/reading.d.ts +17 -0
  39. package/dist/essay/reading.js +31 -0
  40. package/dist/essay/scroll.d.ts +8 -0
  41. package/dist/essay/scroll.js +78 -0
  42. package/dist/essay/toc.d.ts +23 -0
  43. package/dist/essay/toc.js +50 -0
  44. package/dist/index.d.ts +2 -0
  45. package/dist/index.js +33 -0
  46. package/dist/injection.d.ts +8 -0
  47. package/dist/injection.js +1 -0
  48. package/dist/mdx.d.ts +47 -0
  49. package/dist/mdx.js +68 -0
  50. package/dist/og.d.ts +18 -0
  51. package/dist/og.js +50 -0
  52. package/dist/rehype.d.ts +18 -0
  53. package/dist/rehype.js +41 -0
  54. package/dist/seo.d.ts +174 -0
  55. package/dist/seo.js +152 -0
  56. package/dist/typography/as.d.ts +15 -0
  57. package/dist/typography/as.js +8 -0
  58. package/dist/typography/header.d.ts +44 -0
  59. package/dist/typography/header.js +119 -0
  60. package/dist/typography/highlight.d.ts +33 -0
  61. package/dist/typography/highlight.js +98 -0
  62. package/dist/typography/index.d.ts +4 -0
  63. package/dist/typography/index.js +3 -0
  64. package/dist/typography/paragraph.d.ts +157 -0
  65. package/dist/typography/paragraph.js +229 -0
  66. package/llms.txt +125 -0
  67. package/package.json +140 -0
  68. package/src/prose.css +12 -0
  69. package/src/shiki.css +23 -0
  70. package/src/theme.css +272 -0
  71. package/src/tokens.css +43 -0
  72. package/src/type.css +73 -0
package/dist/seo.d.ts ADDED
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Structured data and page metadata, as pure functions over a site config. No
3
+ * `next` import, not even a type one: `buildMetadata` returns a structurally
4
+ * compatible object a consumer assigns straight to `Metadata`.
5
+ */
6
+ export interface SeoConfig {
7
+ /** Absolute site origin, no trailing slash. e.g. "https://supertype.ai" */
8
+ baseUrl: string;
9
+ siteName: string;
10
+ /** Absolute URL of the default social card. */
11
+ defaultOgImage: string;
12
+ /** Absolute URL of the publisher logo, for Article publisher nodes. */
13
+ logoUrl?: string;
14
+ /** Path prefix articles live under, no slashes. Defaults to "notes". */
15
+ articleBasePath?: string;
16
+ /** The publisher's own site, when it is not this one. Defaults to `baseUrl`. */
17
+ publisherUrl?: string;
18
+ /**
19
+ * Routes served as directory indexes, as a static export with `trailingSlash`
20
+ * does. Structured data then names the same URL the canonical does.
21
+ */
22
+ trailingSlash?: boolean;
23
+ }
24
+ export interface ArticleAuthor {
25
+ name: string;
26
+ url?: string;
27
+ sameAs?: string[];
28
+ /** The byline's role. Carries an authorship signal a name alone does not. */
29
+ jobTitle?: string;
30
+ }
31
+ export interface ArticleOptions {
32
+ /** Topic tags, emitted as the comma-separated string schema.org expects. */
33
+ keywords?: string[];
34
+ /** Reading time in minutes, emitted as an ISO-8601 duration. */
35
+ readingMinutes?: number;
36
+ }
37
+ /** The subset of Next's Metadata this builds. Assignable to it structurally. */
38
+ export interface PageMetadata {
39
+ title: string;
40
+ description: string;
41
+ alternates: {
42
+ canonical: string;
43
+ };
44
+ openGraph: {
45
+ title: string;
46
+ description: string;
47
+ url: string;
48
+ images: {
49
+ url: string;
50
+ width: number;
51
+ height: number;
52
+ alt: string;
53
+ }[];
54
+ };
55
+ twitter: {
56
+ card: "summary_large_image";
57
+ title: string;
58
+ description: string;
59
+ images: string[];
60
+ };
61
+ }
62
+ export declare function createSeo(config: SeoConfig): {
63
+ absolute: (url: string) => string;
64
+ ORG_ID: string;
65
+ WEBSITE_ID: string;
66
+ /**
67
+ * The canonical is a path; Next resolves it against `metadataBase`. Never set
68
+ * one in the root layout — it merges into every page and declares them all
69
+ * duplicates of the homepage.
70
+ */
71
+ buildMetadata(title: string, description: string, slug: string, ogImage?: string): PageMetadata;
72
+ /** Schema.org BlogPosting for an article. */
73
+ buildArticleJsonLd(title: string, description: string, slug: string, authors: Array<string | ArticleAuthor>, datePublished?: string, image?: string, dateModified?: string, { keywords, readingMinutes }?: ArticleOptions): {
74
+ publisher: {
75
+ logo?: {
76
+ "@type": string;
77
+ url: string;
78
+ } | undefined;
79
+ "@type": string;
80
+ name: string;
81
+ url: string;
82
+ };
83
+ mainEntityOfPage: {
84
+ "@type": string;
85
+ "@id": string;
86
+ };
87
+ timeRequired?: string | undefined;
88
+ keywords?: string | undefined;
89
+ "@context": string;
90
+ "@type": string;
91
+ headline: string;
92
+ description: string;
93
+ url: string;
94
+ image: string;
95
+ datePublished: string | undefined;
96
+ dateModified: string | undefined;
97
+ author: {
98
+ jobTitle?: string | undefined;
99
+ sameAs?: string[] | undefined;
100
+ url?: string | undefined;
101
+ "@type": string;
102
+ name: string;
103
+ }[];
104
+ };
105
+ /** Schema.org BreadcrumbList. Item URLs may be relative. */
106
+ buildBreadcrumbJsonLd(items: Array<{
107
+ name: string;
108
+ url: string;
109
+ }>): {
110
+ "@context": string;
111
+ "@type": string;
112
+ itemListElement: {
113
+ "@type": string;
114
+ position: number;
115
+ name: string;
116
+ item: string;
117
+ }[];
118
+ };
119
+ /** Schema.org ItemList, for an ordered collection such as a topic page. */
120
+ buildItemListJsonLd(items: Array<{
121
+ name: string;
122
+ url: string;
123
+ }>): {
124
+ "@context": string;
125
+ "@type": string;
126
+ numberOfItems: number;
127
+ itemListElement: {
128
+ "@type": string;
129
+ position: number;
130
+ name: string;
131
+ url: string;
132
+ }[];
133
+ };
134
+ /** Schema.org FAQPage from question/answer pairs. */
135
+ buildFaqJsonLd(faq: Array<{
136
+ q: string;
137
+ a: string;
138
+ }>): {
139
+ "@context": string;
140
+ "@type": string;
141
+ mainEntity: {
142
+ "@type": string;
143
+ name: string;
144
+ acceptedAnswer: {
145
+ "@type": string;
146
+ text: string;
147
+ };
148
+ }[];
149
+ };
150
+ /** Schema.org WebPage and its narrower types. */
151
+ buildWebPageJsonLd(name: string, description: string, slug: string, type?: "WebPage" | "AboutPage" | "ContactPage" | "CollectionPage" | "WebSite", author?: string | ArticleAuthor): {
152
+ publisher: {
153
+ logo?: {
154
+ "@type": string;
155
+ url: string;
156
+ } | undefined;
157
+ "@type": string;
158
+ name: string;
159
+ url: string;
160
+ };
161
+ author?: {
162
+ jobTitle?: string | undefined;
163
+ sameAs?: string[] | undefined;
164
+ url?: string | undefined;
165
+ "@type": string;
166
+ name: string;
167
+ } | undefined;
168
+ "@context": string;
169
+ "@type": "WebPage" | "AboutPage" | "ContactPage" | "CollectionPage" | "WebSite";
170
+ name: string;
171
+ description: string;
172
+ url: string;
173
+ };
174
+ };
package/dist/seo.js ADDED
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Structured data and page metadata, as pure functions over a site config. No
3
+ * `next` import, not even a type one: `buildMetadata` returns a structurally
4
+ * compatible object a consumer assigns straight to `Metadata`.
5
+ */
6
+ export function createSeo(config) {
7
+ const { baseUrl, siteName, defaultOgImage, logoUrl, articleBasePath = "notes", publisherUrl, trailingSlash = false, } = config;
8
+ /** Resolves a possibly-relative URL against the site origin. */
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
+ /**
16
+ * A page route in the shape this site actually serves. A URL already carrying
17
+ * a query, a fragment or a file extension is left alone — only a route gets
18
+ * the slash.
19
+ */
20
+ const route = (url) => {
21
+ const abs = absolute(url);
22
+ if (!trailingSlash || abs.endsWith("/") || /[#?]|\.[a-z0-9]+$/i.test(abs))
23
+ return abs;
24
+ return `${abs}/`;
25
+ };
26
+ /** A schema.org Person from a name or a fuller author record. */
27
+ const person = (a) => {
28
+ const author = typeof a === "string" ? { name: a } : a;
29
+ return {
30
+ "@type": "Person",
31
+ name: author.name,
32
+ ...(author.url ? { url: absolute(author.url) } : {}),
33
+ ...(author.sameAs?.length ? { sameAs: author.sameAs } : {}),
34
+ ...(author.jobTitle ? { jobTitle: author.jobTitle } : {}),
35
+ };
36
+ };
37
+ const ORG_ID = `${baseUrl}/#organization`;
38
+ const WEBSITE_ID = `${baseUrl}/#website`;
39
+ const publisher = {
40
+ "@type": "Organization",
41
+ name: siteName,
42
+ url: publisherUrl ?? baseUrl,
43
+ ...(logoUrl
44
+ ? { logo: { "@type": "ImageObject", url: logoUrl } }
45
+ : {}),
46
+ };
47
+ return {
48
+ absolute,
49
+ ORG_ID,
50
+ WEBSITE_ID,
51
+ /**
52
+ * The canonical is a path; Next resolves it against `metadataBase`. Never set
53
+ * one in the root layout — it merges into every page and declares them all
54
+ * duplicates of the homepage.
55
+ */
56
+ buildMetadata(title, description, slug, ogImage) {
57
+ const image = ogImage ?? defaultOgImage;
58
+ const path = slug ? `/${slug.replace(/^\//, "")}` : "/";
59
+ return {
60
+ title,
61
+ description,
62
+ alternates: { canonical: path },
63
+ openGraph: {
64
+ title,
65
+ description,
66
+ url: path,
67
+ images: [{ url: image, width: 1200, height: 628, alt: title }],
68
+ },
69
+ twitter: {
70
+ card: "summary_large_image",
71
+ title,
72
+ description,
73
+ images: [image],
74
+ },
75
+ };
76
+ },
77
+ /** Schema.org BlogPosting for an article. */
78
+ buildArticleJsonLd(title, description, slug, authors, datePublished, image, dateModified, { keywords, readingMinutes } = {}) {
79
+ const url = route(`${articleBasePath}/${slug}`);
80
+ return {
81
+ "@context": "https://schema.org",
82
+ "@type": "BlogPosting",
83
+ headline: title,
84
+ description,
85
+ url,
86
+ image: image ?? defaultOgImage,
87
+ datePublished,
88
+ // Falls back to datePublished rather than omitting. To a crawler, an
89
+ // article with no modified date reads as never revised.
90
+ dateModified: dateModified ?? datePublished,
91
+ author: authors.map(person),
92
+ ...(keywords?.length ? { keywords: keywords.join(", ") } : {}),
93
+ ...(readingMinutes ? { timeRequired: `PT${readingMinutes}M` } : {}),
94
+ publisher,
95
+ mainEntityOfPage: { "@type": "WebPage", "@id": url },
96
+ };
97
+ },
98
+ /** Schema.org BreadcrumbList. Item URLs may be relative. */
99
+ buildBreadcrumbJsonLd(items) {
100
+ return {
101
+ "@context": "https://schema.org",
102
+ "@type": "BreadcrumbList",
103
+ itemListElement: items.map((item, index) => ({
104
+ "@type": "ListItem",
105
+ position: index + 1,
106
+ name: item.name,
107
+ item: route(item.url),
108
+ })),
109
+ };
110
+ },
111
+ /** Schema.org ItemList, for an ordered collection such as a topic page. */
112
+ buildItemListJsonLd(items) {
113
+ return {
114
+ "@context": "https://schema.org",
115
+ "@type": "ItemList",
116
+ numberOfItems: items.length,
117
+ itemListElement: items.map((item, index) => ({
118
+ "@type": "ListItem",
119
+ position: index + 1,
120
+ name: item.name,
121
+ url: route(item.url),
122
+ })),
123
+ };
124
+ },
125
+ /** Schema.org FAQPage from question/answer pairs. */
126
+ buildFaqJsonLd(faq) {
127
+ return {
128
+ "@context": "https://schema.org",
129
+ "@type": "FAQPage",
130
+ mainEntity: faq.map((item) => ({
131
+ "@type": "Question",
132
+ name: item.q,
133
+ acceptedAnswer: { "@type": "Answer", text: item.a },
134
+ })),
135
+ };
136
+ },
137
+ /** Schema.org WebPage and its narrower types. */
138
+ buildWebPageJsonLd(name, description, slug, type = "WebPage",
139
+ /** A page carrying a byline says who wrote it, same as an article does. */
140
+ author) {
141
+ return {
142
+ "@context": "https://schema.org",
143
+ "@type": type,
144
+ name,
145
+ description,
146
+ url: route(slug),
147
+ ...(author ? { author: person(author) } : {}),
148
+ publisher,
149
+ };
150
+ },
151
+ };
152
+ }
@@ -0,0 +1,15 @@
1
+ import type { ComponentProps } from "react";
2
+ /** One union, not a bespoke one per component: the narrow ones only decided, for
3
+ * the reader, that a caption could not be a heading. Classes never change with
4
+ * the tag, so the tag is a prop rather than three more components. */
5
+ export type TypographyTag = "span" | "p" | "div" | "small" | "label" | "h1" | "h2" | "h3" | "h4";
6
+ /** A primitive's own props, plus the element choice. */
7
+ export type WithAs<Own = unknown> = ComponentProps<"span"> & Own & {
8
+ as?: TypographyTag;
9
+ };
10
+ /** The cast lives here once instead of in each primitive; a per-tag generic would
11
+ * only narrow `ref`, at the price of a generic in four public signatures. `as`
12
+ * is not forwarded — on the DOM node it is an unknown attribute. */
13
+ export declare function TextAs({ as, ...props }: ComponentProps<"span"> & {
14
+ as?: TypographyTag;
15
+ }): import("react").JSX.Element;
@@ -0,0 +1,8 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /** The cast lives here once instead of in each primitive; a per-tag generic would
3
+ * only narrow `ref`, at the price of a generic in four public signatures. `as`
4
+ * is not forwarded — on the DOM node it is an unknown attribute. */
5
+ export function TextAs({ as = "span", ...props }) {
6
+ const As = as;
7
+ return _jsx(As, { ...props });
8
+ }
@@ -0,0 +1,44 @@
1
+ import { type VariantProps } from "class-variance-authority";
2
+ import { type WithAs } from "./as.js";
3
+ declare const h1Variants: (props?: ({
4
+ variant?: "default" | "display" | null | undefined;
5
+ } & import("class-variance-authority/types").ClassProp) | undefined) => string;
6
+ export declare function TypographyH1({ className, variant, children, ...props }: React.ComponentProps<"h1"> & VariantProps<typeof h1Variants>): import("react").JSX.Element;
7
+ declare const h2Variants: (props?: ({
8
+ variant?: "default" | "display" | null | undefined;
9
+ } & import("class-variance-authority/types").ClassProp) | undefined) => string;
10
+ /** The h2 ramp as a class, for a caller that must render its own element. */
11
+ export declare const headingClass: (variant?: VariantProps<typeof h2Variants>["variant"]) => string;
12
+ /**
13
+ * `divider` is a rule under the heading, not a size — it used to ride the size
14
+ * axis as `default` vs `unbordered`, which made every call site state a border
15
+ * it had no opinion about in order to reach the size it wanted.
16
+ */
17
+ export declare function TypographyH2({ className, variant, divider, children, ...props }: React.ComponentProps<"h2"> & VariantProps<typeof h2Variants> & {
18
+ divider?: boolean;
19
+ }): import("react").JSX.Element;
20
+ declare const h3Variants: (props?: ({
21
+ variant?: "default" | "display" | null | undefined;
22
+ } & import("class-variance-authority/types").ClassProp) | undefined) => string;
23
+ export declare function TypographyH3({ className, variant, children, ...props }: React.ComponentProps<"h3"> & VariantProps<typeof h3Variants>): import("react").JSX.Element;
24
+ /** The card / panel title: 14px in the product, 20 on an editorial surface. */
25
+ export declare function TypographyH4({ className, children, ...props }: React.ComponentProps<"h4">): import("react").JSX.Element;
26
+ declare const eyebrowVariants: (props?: ({
27
+ tone?: "label" | "heading" | null | undefined;
28
+ } & import("class-variance-authority/types").ClassProp) | undefined) => string;
29
+ /**
30
+ * The eyebrow's ramp as a class, for a caller that cannot render our element —
31
+ * a dialog title primitive, a motion element. Same escape hatch as
32
+ * `headingClass`, and it exists so that a consumer needing the class does not
33
+ * hand-roll a second copy of it that then drifts from the component.
34
+ */
35
+ export declare const eyebrowClass: (tone?: VariantProps<typeof eyebrowVariants>["tone"]) => string;
36
+ /**
37
+ * An all-caps micro-label above a stat or a group of controls, and — since the
38
+ * deck was folded into it — the standfirst that sits with a page title.
39
+ *
40
+ * `as` covers the case the span cannot: an eyebrow that is also the section's
41
+ * heading. See `TypographyTag` in as.tsx for why the classes hold across tags.
42
+ */
43
+ export declare function TypographyEyebrow({ className, tone, as, children, ...props }: WithAs<VariantProps<typeof eyebrowVariants>>): import("react").JSX.Element;
44
+ export {};
@@ -0,0 +1,119 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { cva } from "class-variance-authority";
3
+ import { cn } from "../cn.js";
4
+ import { TextAs } from "./as.js";
5
+ /**
6
+ * The heading ladder. Four levels, one rung each.
7
+ *
8
+ * A heading does not pick its size — `--text-h1`…`--text-h4` in type.css do, and
9
+ * `.editorial` retunes all four together. That is the whole design: size is a
10
+ * property of the SURFACE, and level is the only thing a call site knows. When
11
+ * the rungs were shared with body copy the call site had to know both, which is
12
+ * how `larger` and `entry` appeared — variants whose entire job was to climb out
13
+ * of a rung that read fine in the product and landed under the paragraph on a
14
+ * marketing page. Retuning a surface now means editing two lines of CSS.
15
+ *
16
+ * `display` is a role rather than a size: the landing-page heading that has to
17
+ * outrank the same level in the docs. Size is all it changes — the slant it once
18
+ * carried turned out to be unwanted on every surface.
19
+ *
20
+ * Tailwind scans comments — never spell a class out here or it becomes a real
21
+ * utility.
22
+ */
23
+ /**
24
+ * The heading face, stated once. Anything wearing it composes this rather than
25
+ * respelling it: a second literal here is a level that forked, and a literal
26
+ * weight beside the face survives into `.editorial` and synthesises the
27
+ * single-weight serif. viably asserts there is exactly one of these strings.
28
+ */
29
+ const HEADING_FACE = "font-heading font-[number:var(--heading-weight)]";
30
+ const HEADING_BASE = `scroll-m-20 ${HEADING_FACE} text-foreground`;
31
+ const h1Variants = cva(`${HEADING_BASE} tracking-tight`, {
32
+ variants: {
33
+ variant: {
34
+ /** The page title: 22px in the product, 36 on an editorial surface. */
35
+ default: "text-h1",
36
+ /** A landing hero, drawn to be seen from the top of a scroll. */
37
+ display: "text-4xl sm:text-5xl",
38
+ },
39
+ },
40
+ defaultVariants: { variant: "default" },
41
+ });
42
+ export function TypographyH1({ className, variant, children, ...props }) {
43
+ return (_jsx("h1", { className: cn(h1Variants({ variant }), className), ...props, children: children }));
44
+ }
45
+ const h2Variants = cva(`${HEADING_BASE} tracking-[-0.01em] first:mt-0`, {
46
+ variants: {
47
+ variant: {
48
+ /** The section heading: 18px in the product, 30 on an editorial surface. */
49
+ default: "text-h2",
50
+ /** A landing page's section heading, one step over the docs equivalent. */
51
+ display: "text-3xl sm:text-4xl",
52
+ },
53
+ },
54
+ defaultVariants: { variant: "default" },
55
+ });
56
+ /** The h2 ramp as a class, for a caller that must render its own element. */
57
+ export const headingClass = (variant) => h2Variants({ variant });
58
+ /**
59
+ * `divider` is a rule under the heading, not a size — it used to ride the size
60
+ * axis as `default` vs `unbordered`, which made every call site state a border
61
+ * it had no opinion about in order to reach the size it wanted.
62
+ */
63
+ export function TypographyH2({ className, variant, divider, children, ...props }) {
64
+ return (_jsx("h2", { className: cn(h2Variants({ variant }), divider && "w-fit border-b pb-2", className), ...props, children: children }));
65
+ }
66
+ const h3Variants = cva(HEADING_BASE, {
67
+ variants: {
68
+ variant: {
69
+ /** The subhead: 16px in the product, 24 on an editorial surface. */
70
+ default: "text-h3",
71
+ /**
72
+ * The lead card in a grid — a featured post, a pinned series. Present at
73
+ * this rung and not below it: h4 is a panel title, and a panel title that
74
+ * reaches for a display size is a section heading wearing the wrong tag.
75
+ */
76
+ display: "text-2xl sm:text-3xl",
77
+ },
78
+ },
79
+ defaultVariants: { variant: "default" },
80
+ });
81
+ export function TypographyH3({ className, variant, children, ...props }) {
82
+ return (_jsx("h3", { className: cn(h3Variants({ variant }), className), ...props, children: children }));
83
+ }
84
+ /** The card / panel title: 14px in the product, 20 on an editorial surface. */
85
+ export function TypographyH4({ className, children, ...props }) {
86
+ return (_jsx("h4", { className: cn(HEADING_BASE, "text-h4", className), ...props, children: children }));
87
+ }
88
+ const eyebrowVariants = cva("block uppercase tracking-wider", {
89
+ variants: {
90
+ tone: {
91
+ /**
92
+ * Weight is load-bearing here: uppercase at this size loses shape at 400.
93
+ * Primary ink, stated not inherited — an eyebrow names the section under it,
94
+ * and one that turns red from its surroundings is not a heading.
95
+ */
96
+ heading: "text-xs font-semibold text-foreground",
97
+ /** Stat cards invert it: the figure is the headline, so the label yields. */
98
+ label: "text-2xs font-medium text-accent-foreground",
99
+ },
100
+ },
101
+ defaultVariants: { tone: "heading" },
102
+ });
103
+ /**
104
+ * The eyebrow's ramp as a class, for a caller that cannot render our element —
105
+ * a dialog title primitive, a motion element. Same escape hatch as
106
+ * `headingClass`, and it exists so that a consumer needing the class does not
107
+ * hand-roll a second copy of it that then drifts from the component.
108
+ */
109
+ export const eyebrowClass = (tone) => eyebrowVariants({ tone });
110
+ /**
111
+ * An all-caps micro-label above a stat or a group of controls, and — since the
112
+ * deck was folded into it — the standfirst that sits with a page title.
113
+ *
114
+ * `as` covers the case the span cannot: an eyebrow that is also the section's
115
+ * heading. See `TypographyTag` in as.tsx for why the classes hold across tags.
116
+ */
117
+ export function TypographyEyebrow({ className, tone, as, children, ...props }) {
118
+ return (_jsx(TextAs, { as: as, className: cn(eyebrowVariants({ tone }), className), ...props, children: children }));
119
+ }
@@ -0,0 +1,33 @@
1
+ import type { ComponentProps } from "react";
2
+ /**
3
+ * Inks only. A fill token (`--secondary`, `--accent`) has no chroma to wash with —
4
+ * at 44% alpha it is invisible, and the `luminosity` blend then strips the text's
5
+ * hue for nothing. That is why the earth tones enter as `-foreground`: those are
6
+ * the ink-grade pair, mixed to hold at text weight in both themes.
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.
10
+ *
11
+ * The four earth tones are one palette, not a menu: each is a different hue,
12
+ * because two a reader cannot tell apart are one tone with two names.
13
+ */
14
+ declare const MARKER_TONES: {
15
+ readonly primary: "var(--primary)";
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)";
21
+ };
22
+ export type HighlightTone = keyof typeof MARKER_TONES;
23
+ /**
24
+ * Marker highlight for inline text. Painted as the run's own background, never a
25
+ * mask — a mask would shave the glyph tops. `luminosity` lets letters borrow the
26
+ * marker's hue while keeping their own lightness, so contrast holds in both themes.
27
+ */
28
+ export declare function TypographyHighlight({ tone, seed, className, style, children, ...props }: ComponentProps<"span"> & {
29
+ tone?: HighlightTone;
30
+ /** Changes the wobble and the grain of the swipe. Any integer. */
31
+ seed?: number;
32
+ }): import("react").JSX.Element;
33
+ export {};
@@ -0,0 +1,98 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { cn } from "../cn.js";
3
+ /**
4
+ * Inks only. A fill token (`--secondary`, `--accent`) has no chroma to wash with —
5
+ * at 44% alpha it is invisible, and the `luminosity` blend then strips the text's
6
+ * hue for nothing. That is why the earth tones enter as `-foreground`: those are
7
+ * the ink-grade pair, mixed to hold at text weight in both themes.
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.
11
+ *
12
+ * The four earth tones are one palette, not a menu: each is a different hue,
13
+ * because two a reader cannot tell apart are one tone with two names.
14
+ */
15
+ const MARKER_TONES = {
16
+ primary: "var(--primary)",
17
+ success: "var(--success)",
18
+ ochre: "var(--ochre-foreground)",
19
+ terracotta: "var(--terracotta-foreground)",
20
+ sage: "var(--sage-foreground)",
21
+ fig: "var(--fig-foreground)",
22
+ };
23
+ /** Wash density at a fraction of the theme's base alpha. */
24
+ const ink = (weight) => `color-mix(in srgb, var(--marker) calc(var(--marker-alpha) * ${weight}), transparent)`;
25
+ // One dab of the tip, in percentages so the swipe stretches with the phrase.
26
+ // They overlap by a third — that scalloping is what separates a swipe from a
27
+ // shape. The last three are the strand a felt tip pools into as it lifts.
28
+ const DABS = [
29
+ { x: 3, y: 64, rx: 10, ry: 32, w: 0.34 },
30
+ { x: 16, y: 61, rx: 18, ry: 37, w: 0.46 },
31
+ { x: 31, y: 63, rx: 19, ry: 34, w: 0.44 },
32
+ { x: 46, y: 60, rx: 19, ry: 37, w: 0.48 },
33
+ { x: 61, y: 62, rx: 19, ry: 34, w: 0.43 },
34
+ { x: 76, y: 59, rx: 19, ry: 36, w: 0.47 },
35
+ { x: 89, y: 61, rx: 15, ry: 33, w: 0.4 },
36
+ { x: 98, y: 63, rx: 8, ry: 30, w: 0.3 },
37
+ { x: 18, y: 95.5, rx: 14, ry: 3.4, w: 0.3 },
38
+ { x: 44, y: 97.5, rx: 18, ry: 2.8, w: 0.26 },
39
+ { x: 68, y: 96, rx: 15, ry: 3.2, w: 0.2 },
40
+ ];
41
+ // Integer-only: `Math.sin` is implementation-defined, so Node and the browser
42
+ // disagreed in the last bits and the swipe hydrated as a mismatch.
43
+ const hash = (seed, i) => {
44
+ let h = Math.imul(seed ^ 0x9e3779b9, 0x85ebca6b) ^ Math.imul(i + 0x165667b1, 0xc2b2ae35);
45
+ h ^= h >>> 15;
46
+ h = Math.imul(h, 0x2545f491);
47
+ h ^= h >>> 13;
48
+ return (h >>> 0) / 4294967296;
49
+ };
50
+ /** Rounding keeps the string identical across engines. */
51
+ const q = (n) => Math.round(n * 100) / 100;
52
+ const q4 = (n) => Math.round(n * 10000) / 10000;
53
+ /** Deterministic noise in [-1, 1], so `seed` reshapes a swipe without a random source. */
54
+ const drift = (seed, i) => hash(seed, i) * 2 - 1;
55
+ /** `drift` remapped to a range, for the values that are a size rather than an offset. */
56
+ const between = (seed, i, lo, hi) => lo + hash(seed, i) * (hi - lo);
57
+ // Grain is drag, not noise. Bounds keep streaks inside the wash — one clearing
58
+ // the feathered edge reads as dirt.
59
+ const STREAKS = 9;
60
+ const streak = (seed, k) => {
61
+ const i = 100 + k * 7; // Stride, so neighbouring streaks draw on unrelated noise.
62
+ const x = q(between(seed, i, 16, 82));
63
+ const y = q(between(seed, i + 1, 44, 86));
64
+ const rx = q(between(seed, i + 2, 10, 27));
65
+ const ry = q(between(seed, i + 3, 1.3, 3.1));
66
+ const w = q4(between(seed, i + 4, 0.07, 0.14));
67
+ return `radial-gradient(ellipse ${rx}% ${ry}% at ${x}% ${y}%, ${ink(w)} 0 58%, transparent 100%)`;
68
+ };
69
+ const fillCache = new Map();
70
+ const markerFill = (seed) => {
71
+ const cached = fillCache.get(seed);
72
+ if (cached)
73
+ return cached;
74
+ // Streaks first: later layers paint under earlier ones.
75
+ const fill = [
76
+ ...Array.from({ length: STREAKS }, (_, k) => streak(seed, k)),
77
+ ...DABS.map(({ x, y, rx, ry, w }, i) => {
78
+ // Wobble scales with the dab; a flat offset would relocate the strand.
79
+ const cy = q(y + drift(seed, i) * Math.min(4, ry * 0.4));
80
+ const r = q(rx + drift(seed, i + DABS.length) * 2);
81
+ return `radial-gradient(ellipse ${r}% ${ry}% at ${x}% ${cy}%, ${ink(w)} 0 56%, ${ink(w * 0.45)} 82%, transparent 100%)`;
82
+ }),
83
+ ].join(", ");
84
+ fillCache.set(seed, fill);
85
+ return fill;
86
+ };
87
+ /**
88
+ * Marker highlight for inline text. Painted as the run's own background, never a
89
+ * mask — a mask would shave the glyph tops. `luminosity` lets letters borrow the
90
+ * marker's hue while keeping their own lightness, so contrast holds in both themes.
91
+ */
92
+ 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: {
94
+ "--marker": MARKER_TONES[tone],
95
+ backgroundImage: markerFill(seed),
96
+ ...style,
97
+ }, ...props, children: _jsx("span", { className: "[mix-blend-mode:luminosity]", children: children }) }));
98
+ }
@@ -0,0 +1,4 @@
1
+ export type { TypographyTag } from "./as.js";
2
+ export * from "./header.js";
3
+ export * from "./paragraph.js";
4
+ export * from "./highlight.js";
@@ -0,0 +1,3 @@
1
+ export * from "./header.js";
2
+ export * from "./paragraph.js";
3
+ export * from "./highlight.js";