@supertype.ai/foundations 0.1.36 → 0.1.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -74,7 +74,7 @@ untagged git dependency resolves to a different commit on a fresh install.
74
74
 
75
75
  ```jsonc
76
76
  // package.json
77
- "@supertype.ai/foundations": "https://github.com/supertypeai/foundations.git#v0.1.36"
77
+ "@supertype.ai/foundations": "https://github.com/supertypeai/foundations.git#v0.1.37"
78
78
  ```
79
79
 
80
80
  </details>
@@ -0,0 +1,73 @@
1
+ /**
2
+ * How far a centred mark misses the letters, per rung of a type ramp.
3
+ *
4
+ * `items-center` centres boxes, and the box is the line box: leading, ascent and
5
+ * descent, only some of which the letters use. The mark beside a label therefore
6
+ * centres on the font's box rather than on the band of ink a reader sees, and
7
+ * whether those two agree is a property of the font, not of the design.
8
+ *
9
+ * The whole tool is one line of arithmetic, and the rounding is the reason it is
10
+ * worth shipping. Ratios alone give one constant tilt for a face, 0.0235em for
11
+ * Ubuntu Sans, which reads as every rung being equally out. Browsers quantise
12
+ * ascent, descent and cap height to whole pixels before they lay a line out, and
13
+ * rounded, that same face is half a pixel out at 11px, flat at 13px and half a
14
+ * pixel out again at 22px. The rendered pages agree, so the rounding is what
15
+ * separates a rung that needs the trim from one where it buys nothing.
16
+ *
17
+ * Build-time only, and only as good as the metrics handed to it. Verify cap
18
+ * height against the browser rather than a table: `next/font` ships 693 for
19
+ * Ubuntu Sans where canvas measures 727, and the wrong one flips the answer at
20
+ * every rung.
21
+ */
22
+ /** A font's vertical metrics, in font units. The four numbers every metrics
23
+ * table carries, `next/font`'s and capsize's alike. */
24
+ export interface FontMetrics {
25
+ unitsPerEm: number;
26
+ ascent: number;
27
+ descent: number;
28
+ capHeight: number;
29
+ }
30
+ /** One step of a ramp: the name an app knows it by, and its size in px. */
31
+ export interface TypeRung {
32
+ name: string;
33
+ fontSize: number;
34
+ }
35
+ export interface OpticalOffset extends TypeRung {
36
+ /** How far below the cap band's centre a centred mark sits, in px. Positive is
37
+ * low, which is the direction rounding takes it. */
38
+ offset: number;
39
+ /** The same miss against the height of the letters it misses. Half a pixel is
40
+ * a twelfth of an 11px cap band and a thirty-second of a 36px one, so this is
41
+ * the number that says whether a reader sees it. */
42
+ share: number;
43
+ /** The rounded metrics the offset came out of, for a message worth reading. */
44
+ used: {
45
+ ascent: number;
46
+ descent: number;
47
+ capHeight: number;
48
+ };
49
+ }
50
+ /**
51
+ * The gap between the line box's centre and the cap band's, at one size.
52
+ *
53
+ * Half-leading cancels, so line height does not appear: a rung that is out stays
54
+ * out however loosely it is set, and no retune of the ramp's leading fixes it.
55
+ * Paint rounds the baseline a second time, in the same direction, so treat this
56
+ * as the floor of the error rather than the whole of it.
57
+ */
58
+ export declare function capBandOffset(metrics: FontMetrics, fontSize: number): number;
59
+ /**
60
+ * Every rung whose centred mark misses the letters by enough to see, worst first.
61
+ *
62
+ * A rung that comes back is one where an icon, a badge or a swatch set beside the
63
+ * text with `items-center` wants `CAP_TRIM` on the text to land on it. Most rungs
64
+ * of a ramp are half a pixel out, so the pixel is not the question: `tolerance` is
65
+ * a share of the cap band, and 0.05 is where a miss stops reading as a rounding
66
+ * artefact and starts reading as two things that do not line up. Raise it for a
67
+ * surface that only sets headlines, lower it to see the whole ramp.
68
+ */
69
+ export declare function checkOptical(metrics: FontMetrics, rungs: readonly TypeRung[], { tolerance }?: {
70
+ tolerance?: number;
71
+ }): OpticalOffset[];
72
+ /** The failures as lines, on the model of `formatFailures` in contrast.ts. */
73
+ export declare function formatOffsets(offsets: readonly OpticalOffset[]): string;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * How far a centred mark misses the letters, per rung of a type ramp.
3
+ *
4
+ * `items-center` centres boxes, and the box is the line box: leading, ascent and
5
+ * descent, only some of which the letters use. The mark beside a label therefore
6
+ * centres on the font's box rather than on the band of ink a reader sees, and
7
+ * whether those two agree is a property of the font, not of the design.
8
+ *
9
+ * The whole tool is one line of arithmetic, and the rounding is the reason it is
10
+ * worth shipping. Ratios alone give one constant tilt for a face, 0.0235em for
11
+ * Ubuntu Sans, which reads as every rung being equally out. Browsers quantise
12
+ * ascent, descent and cap height to whole pixels before they lay a line out, and
13
+ * rounded, that same face is half a pixel out at 11px, flat at 13px and half a
14
+ * pixel out again at 22px. The rendered pages agree, so the rounding is what
15
+ * separates a rung that needs the trim from one where it buys nothing.
16
+ *
17
+ * Build-time only, and only as good as the metrics handed to it. Verify cap
18
+ * height against the browser rather than a table: `next/font` ships 693 for
19
+ * Ubuntu Sans where canvas measures 727, and the wrong one flips the answer at
20
+ * every rung.
21
+ */
22
+ const px = (value, fontSize, unitsPerEm) => Math.round((value * fontSize) / unitsPerEm);
23
+ /**
24
+ * The gap between the line box's centre and the cap band's, at one size.
25
+ *
26
+ * Half-leading cancels, so line height does not appear: a rung that is out stays
27
+ * out however loosely it is set, and no retune of the ramp's leading fixes it.
28
+ * Paint rounds the baseline a second time, in the same direction, so treat this
29
+ * as the floor of the error rather than the whole of it.
30
+ */
31
+ export function capBandOffset(metrics, fontSize) {
32
+ const ascent = px(metrics.ascent, fontSize, metrics.unitsPerEm);
33
+ const descent = px(metrics.descent, fontSize, metrics.unitsPerEm);
34
+ const capHeight = px(metrics.capHeight, fontSize, metrics.unitsPerEm);
35
+ return capHeight / 2 - (ascent - descent) / 2;
36
+ }
37
+ /**
38
+ * Every rung whose centred mark misses the letters by enough to see, worst first.
39
+ *
40
+ * A rung that comes back is one where an icon, a badge or a swatch set beside the
41
+ * text with `items-center` wants `CAP_TRIM` on the text to land on it. Most rungs
42
+ * of a ramp are half a pixel out, so the pixel is not the question: `tolerance` is
43
+ * a share of the cap band, and 0.05 is where a miss stops reading as a rounding
44
+ * artefact and starts reading as two things that do not line up. Raise it for a
45
+ * surface that only sets headlines, lower it to see the whole ramp.
46
+ */
47
+ export function checkOptical(metrics, rungs, { tolerance = 0.05 } = {}) {
48
+ return rungs
49
+ .map((rung) => {
50
+ const used = {
51
+ ascent: px(metrics.ascent, rung.fontSize, metrics.unitsPerEm),
52
+ descent: px(metrics.descent, rung.fontSize, metrics.unitsPerEm),
53
+ capHeight: px(metrics.capHeight, rung.fontSize, metrics.unitsPerEm),
54
+ };
55
+ const offset = used.capHeight / 2 - (used.ascent - used.descent) / 2;
56
+ return { ...rung, offset, share: Math.abs(offset) / used.capHeight, used };
57
+ })
58
+ .filter((rung) => rung.share > tolerance)
59
+ .sort((a, b) => b.share - a.share);
60
+ }
61
+ /** The failures as lines, on the model of `formatFailures` in contrast.ts. */
62
+ export function formatOffsets(offsets) {
63
+ return offsets
64
+ .map(({ name, fontSize, offset, used }) => ` ${name} (${fontSize}px) mark sits ${Math.abs(offset)}px ${offset > 0 ? "below" : "above"} the cap band` +
65
+ `, ${Math.round((Math.abs(offset) / used.capHeight) * 100)}% of it` +
66
+ ` [ascent ${used.ascent}, descent ${used.descent}, cap ${used.capHeight}]`)
67
+ .join("\n");
68
+ }
@@ -2,3 +2,4 @@ export type { TypographyTag } from "./as.js";
2
2
  export * from "./header.js";
3
3
  export * from "./paragraph.js";
4
4
  export * from "./highlight.js";
5
+ export * from "./trim.js";
@@ -1,3 +1,4 @@
1
1
  export * from "./header.js";
2
2
  export * from "./paragraph.js";
3
3
  export * from "./highlight.js";
4
+ export * from "./trim.js";
@@ -0,0 +1,28 @@
1
+ /**
2
+ * The optical box: cap top to baseline, with the leading either side removed.
3
+ *
4
+ * A line box reserves room for the ascenders and descenders a string may not
5
+ * use, so `items-center` beside an icon centres that reservation rather than
6
+ * the letters. On an 11px uppercase label the mark next to it renders about a
7
+ * pixel low, which is a whole device pixel against an eight pixel cap band.
8
+ * Trimming makes the element as tall as its own ink, so the row centres what
9
+ * the reader actually sees.
10
+ *
11
+ * Two things follow. It goes on the text element, since `text-box` is not
12
+ * inherited and a row cannot hand it down. And it shortens that element, so a
13
+ * row of trimmed text needs a height floor of its own: without one, the card
14
+ * whose label carries no mark sits shorter than the three beside it and its
15
+ * figure rides high. Chrome and Safari trim, and a browser that does not know
16
+ * `text-box` keeps the untrimmed box, which is the behaviour of every consumer
17
+ * today.
18
+ *
19
+ * Never on a string that also clips. The bottom edge is the baseline, so
20
+ * descenders sit outside the box, and `truncate` or any other overflow hidden
21
+ * cuts the tails off every g and p in it.
22
+ *
23
+ * Uppercase is where it pays. A cap band fills half of an 11px line box and the
24
+ * mark beside it lands a whole device pixel low, where 13px mixed case measures
25
+ * the same trimmed or not: ascenders reach the top of the line box on their own,
26
+ * so there is little leading left to take.
27
+ */
28
+ export declare const CAP_TRIM = "[text-box:trim-both_cap_alphabetic]";
@@ -0,0 +1,28 @@
1
+ /**
2
+ * The optical box: cap top to baseline, with the leading either side removed.
3
+ *
4
+ * A line box reserves room for the ascenders and descenders a string may not
5
+ * use, so `items-center` beside an icon centres that reservation rather than
6
+ * the letters. On an 11px uppercase label the mark next to it renders about a
7
+ * pixel low, which is a whole device pixel against an eight pixel cap band.
8
+ * Trimming makes the element as tall as its own ink, so the row centres what
9
+ * the reader actually sees.
10
+ *
11
+ * Two things follow. It goes on the text element, since `text-box` is not
12
+ * inherited and a row cannot hand it down. And it shortens that element, so a
13
+ * row of trimmed text needs a height floor of its own: without one, the card
14
+ * whose label carries no mark sits shorter than the three beside it and its
15
+ * figure rides high. Chrome and Safari trim, and a browser that does not know
16
+ * `text-box` keeps the untrimmed box, which is the behaviour of every consumer
17
+ * today.
18
+ *
19
+ * Never on a string that also clips. The bottom edge is the baseline, so
20
+ * descenders sit outside the box, and `truncate` or any other overflow hidden
21
+ * cuts the tails off every g and p in it.
22
+ *
23
+ * Uppercase is where it pays. A cap band fills half of an 11px line box and the
24
+ * mark beside it lands a whole device pixel low, where 13px mixed case measures
25
+ * the same trimmed or not: ascenders reach the top of the line box on their own,
26
+ * so there is little leading left to take.
27
+ */
28
+ export const CAP_TRIM = "[text-box:trim-both_cap_alphabetic]";
package/llms.txt CHANGED
@@ -67,6 +67,7 @@ silently. Full reference: https://github.com/supertypeai/foundations
67
67
  | an article whose body is prose or MDX | `EssayHeader` + `ReadingLayout` | `/essay` |
68
68
  | a post meta row (date, read time, tags) | `PostMetaRow` and friends | `/essay` |
69
69
  | a table of contents | `TableOfContents`, `ReadingRail` | `/essay` |
70
+ | where a centred mark lands on a rung | `checkOptical` | `/optical` |
70
71
  | page metadata and JSON-LD | `createSeo` | `/seo` |
71
72
  | an OG image | `ogCard`, `OG_SIZE` | `/og` |
72
73
  | to merge classnames | `cn` | root |
@@ -75,13 +76,14 @@ silently. Full reference: https://github.com/supertypeai/foundations
75
76
 
76
77
  | import | exports |
77
78
  |---|---|
78
- | `@supertype.ai/foundations` | `cn`, `TypographyH1`, `TypographyH2`, `TypographyH3`, `TypographyH4`, `TypographyEyebrow`, `TypographyP`, `TypographyMuted`, `TypographyProse`, `TypographyList`, `TypographyProseList`, `TypographyCaption`, `TypographySmall`, `TypographyLabel`, `TypographyStat`, `TypographyInlineCode`, `TypographyLink`, `TypographyHighlight`, `headingClass`, `headingFace`, `eyebrowClass`, `toneClass`, `impliedTone`, `resolveLink`, `isExternalHref`. Types: `TypographyTag`, `ParagraphVariants`, `ListProps`, `CaptionVariants`, `LabelVariants`, `StatVariants`, `Tone`, `HighlightTone`, `LinkBehavior`, `ResolvedLink` |
79
+ | `@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`, `CAP_TRIM`. Types: `TypographyTag`, `ParagraphVariants`, `ListProps`, `CaptionVariants`, `LabelVariants`, `StatVariants`, `Tone`, `HighlightTone`, `LinkBehavior`, `ResolvedLink` |
79
80
  | `@supertype.ai/foundations/blocks` | `Anchor`, `Cards`, `Card`, `CardHeader`, `CardTitle`, `CardDescription`, `CardContent`, `Callout`, `Button`, `buttonVariants`, `Badge`, `badgeVariants`, `Steps`, `Step`, `Disclosure`, `DisclosureGroup`, `Accordion`, `AccordionItem`, `AccordionTrigger`, `AccordionContent`, `Tabs`, `TabsList`, `TabsTrigger`, `TabsContent`, `TabGroup`, `SEGMENT`, `Bulletin`, `Ribbon`, `EDITORIAL_INKS`, `Colophon`, `BuiltWithFoundations`, `FoundationsMark`, `FOUNDATIONS_URL`. Types: `ButtonLook`, `BadgeLook`, `TabItem`, `BulletinProps`, `BulletinPoint`, `RibbonHue`, `ColophonProps` |
80
81
  | `@supertype.ai/foundations/mdx` | `proseMdxComponents` |
81
82
  | `@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` |
82
83
  | `@supertype.ai/foundations/seo` | `createSeo`. Types: `SeoConfig`, `ArticleAuthor`, `ArticleOptions`, `PageMetadata` |
83
84
  | `@supertype.ai/foundations/og` | `ogCard`, `OG_SIZE`. Types: `OgCardOptions` |
84
85
  | `@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` |
86
+ | `@supertype.ai/foundations/optical` | `capBandOffset` (the gap between a line box's centre and its cap band's, at one size), `checkOptical` (every rung of a ramp whose centred mark misses the letters, worst first), `formatOffsets`. Types: `FontMetrics`, `TypeRung`, `OpticalOffset`. Build-time only |
85
87
  | `@supertype.ai/foundations/rehype` | `rehypeProseCode`, `proseCodeOptions`, `PROSE_LANGS`, `PROSE_THEMES`. Build-time only, must not resolve React |
86
88
  | `@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: the tone rows are read off `TONE` and each cut is resolved along its `var()` fallback chain, so an app that declares `--brand` without `--brand-foreground` is measured on the label the cascade really reaches for rather than skipped), `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 |
87
89
 
@@ -103,6 +105,16 @@ silently. Full reference: https://github.com/supertypeai/foundations
103
105
  trigger. It exists so `target`/`rel` are never written at a call site; `external`
104
106
  is for a same-origin path that is not a route, which the router would prefetch.
105
107
  - `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`.
108
+ - **`CAP_TRIM` centres a mark on the letters.** `items-center` centres line
109
+ boxes, and a line box reserves room for ascenders and descenders the string may
110
+ not use, so an (i) or a badge beside an uppercase micro-label renders about a
111
+ pixel low. Put `CAP_TRIM` on the text element, never on the row, because
112
+ `text-box` is not inherited. It shortens the element, so give the row a height
113
+ floor from whatever sits beside it: a trimmed label with no mark next to it
114
+ makes a shorter row than its neighbours and the figure under it rides high.
115
+ Never put it on a string that clips: the box ends at the baseline, so `truncate`
116
+ cuts the tail off every descender. Which rungs need it is computed rather than
117
+ eyeballed: `checkOptical` from `/optical` reads a face's metrics and names them.
106
118
  - **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 adds `INK_ON_CARD`, `INK_ON_POPOVER` or `INK_ON_SIDEBAR`, and one the package does not name spreads `inkOnSurfaceStyle(token)` into `style` rather than building a class. 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.
107
119
  - `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"`.
108
120
  - `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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supertype.ai/foundations",
3
- "version": "0.1.36",
3
+ "version": "0.1.37",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -68,6 +68,10 @@
68
68
  "require": "./dist/cjs/eslint.js",
69
69
  "default": "./dist/eslint.js"
70
70
  },
71
+ "./optical": {
72
+ "types": "./dist/optical.d.ts",
73
+ "default": "./dist/optical.js"
74
+ },
71
75
  "./rehype": {
72
76
  "types": "./dist/rehype.d.ts",
73
77
  "default": "./dist/rehype.js"