@supertype.ai/foundations 0.1.31 → 0.1.33

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
@@ -66,7 +66,7 @@ untagged git dependency resolves to a different commit on a fresh install.
66
66
 
67
67
  ```jsonc
68
68
  // package.json
69
- "@supertype.ai/foundations": "https://github.com/supertypeai/foundations.git#v0.1.31"
69
+ "@supertype.ai/foundations": "https://github.com/supertypeai/foundations.git#v0.1.33"
70
70
  ```
71
71
 
72
72
  </details>
@@ -62,7 +62,7 @@ cn(FOCUS_RING, "focus-visible:border-ring"), "active:not-aria-[haspopup]:transla
62
62
  variant: {
63
63
  solid: `bg-(--tone-fill) text-(color:--tone-ink) hover:bg-(--tone-fill-hover) ${INK_ON_FILL}`,
64
64
  soft: "bg-(--tone-wash) text-(color:--tone-hue) hover:bg-(--tone-wash-hover)",
65
- outline: "border-(color:--tone-line) bg-background text-(color:--tone-hue) hover:bg-(--tone-wash)",
65
+ outline: "border-(color:--tone-line) text-(color:--tone-hue) hover:bg-(--tone-wash)",
66
66
  ghost: "text-(color:--tone-hue) hover:bg-(--tone-wash)",
67
67
  // No box of its own: a button that reads as a link has to sit on the
68
68
  // text baseline, not on a 32px control's centre line.
@@ -12,6 +12,10 @@ import { TypographyCaption, TypographyLabel, TypographyMuted, TypographySmall, }
12
12
  // already drifted on the radius. Hand-rolled type styles are also exactly what the project's own
13
13
  // guidance forbids, and six copies is how a rule like that gets broken without anyone deciding to.
14
14
  //
15
+ // The body is a slot, so it renders as a div in both densities. A caller passes a list, a
16
+ // pair of paragraphs or a mono block of delivery errors, none of which may sit inside a <p>:
17
+ // the parser closes it early and React reports a hydration error on a callout that looks fine.
18
+ //
15
19
  // Deliberately not a shadcn Alert. Alert is a page-level, role="alert" affordance for something
16
20
  // that just happened; these are quiet, permanent explanations sitting inside a panel, and they
17
21
  // must not announce themselves to a screen reader every time a sheet opens.
@@ -30,7 +34,7 @@ const BOX = "border-(color:--tone-line) bg-(--tone-veil)";
30
34
  export function Callout({ icon: Icon, title, tone = "muted", density = "compact", bodyClassName, action, children, className, }) {
31
35
  const toned = toneClass(tone);
32
36
  if (density === "editorial") {
33
- return (_jsxs("div", { className: cn("relative overflow-hidden rounded-lg border py-3.5 pl-5 pr-4", toned, BOX, className), children: [_jsx("span", { "aria-hidden": true, className: "absolute inset-y-0 left-0 w-[3px] bg-(--tone-line)" }), _jsxs("div", { className: "flex items-start gap-2.5", children: [Icon && (_jsx(Icon, { className: "mt-0.5 size-4 shrink-0 text-(color:--tone-hue)" })), _jsxs("div", { className: "flex min-w-0 flex-col gap-1", children: [title && (_jsx(TypographyLabel, { className: "text-(color:--tone-hue)", children: title })), _jsx(TypographyMuted, { className: cn("leading-relaxed", bodyClassName), children: children }), action && (_jsx("div", { className: "mt-1 flex items-center gap-1", children: action }))] })] })] }));
37
+ return (_jsxs("div", { className: cn("relative overflow-hidden rounded-lg border py-3.5 pl-5 pr-4", toned, BOX, className), children: [_jsx("span", { "aria-hidden": true, className: "absolute inset-y-0 left-0 w-[3px] bg-(--tone-line)" }), _jsxs("div", { className: "flex items-start gap-2.5", children: [Icon && (_jsx(Icon, { className: "mt-0.5 size-4 shrink-0 text-(color:--tone-hue)" })), _jsxs("div", { className: "flex min-w-0 flex-col gap-1", children: [title && (_jsx(TypographyLabel, { className: "text-(color:--tone-hue)", children: title })), _jsx(TypographyMuted, { as: "div", className: cn("leading-relaxed", bodyClassName), children: children }), action && (_jsx("div", { className: "mt-1 flex items-center gap-1", children: action }))] })] })] }));
34
38
  }
35
- return (_jsxs("div", { className: cn("rounded-md border p-3", toned, BOX, className), children: [title && (_jsxs(TypographySmall, { className: "flex items-center gap-1.5 font-medium text-(color:--tone-hue)", children: [Icon && _jsx(Icon, { className: "size-3.5 shrink-0" }), title] })), _jsx(TypographyCaption, { className: cn("mt-1 block leading-relaxed", bodyClassName), children: children }), action && _jsx("div", { className: "mt-2 flex items-center gap-1", children: action })] }));
39
+ return (_jsxs("div", { className: cn("rounded-md border p-3", toned, BOX, className), children: [title && (_jsxs(TypographySmall, { className: "flex items-center gap-1.5 font-medium text-(color:--tone-hue)", children: [Icon && _jsx(Icon, { className: "size-3.5 shrink-0" }), title] })), _jsx(TypographyCaption, { as: "div", className: cn("mt-1 leading-relaxed", bodyClassName), children: children }), action && _jsx("div", { className: "mt-2 flex items-center gap-1", children: action })] }));
36
40
  }
@@ -54,7 +54,8 @@ export declare function TypographyH3({ className, variant, children, ...props }:
54
54
  /** The card / panel title: 14px in the product, 20 on an editorial surface. */
55
55
  export declare function TypographyH4({ className, children, ...props }: React.ComponentProps<"h4">): import("react").JSX.Element;
56
56
  declare const eyebrowVariants: (props?: ({
57
- tone?: "label" | "heading" | null | undefined;
57
+ tone?: "muted" | "label" | "heading" | "subtle" | null | undefined;
58
+ size?: "sm" | "xs" | "2xs" | "3xs" | null | undefined;
58
59
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
59
60
  /**
60
61
  * The eyebrow's ramp as a class, for a caller that cannot render our element —
@@ -62,7 +63,7 @@ declare const eyebrowVariants: (props?: ({
62
63
  * `headingClass`. It exists so a consumer needing the class can take ours rather
63
64
  * than hand-rolling a copy that drifts from the component.
64
65
  */
65
- export declare const eyebrowClass: (tone?: VariantProps<typeof eyebrowVariants>["tone"]) => string;
66
+ export declare const eyebrowClass: (tone?: VariantProps<typeof eyebrowVariants>["tone"], size?: VariantProps<typeof eyebrowVariants>["size"]) => string;
66
67
  /**
67
68
  * An all-caps micro-label above a stat or a group of controls, and — since the
68
69
  * deck was folded into it — the standfirst that sits with a page title.
@@ -70,5 +71,5 @@ export declare const eyebrowClass: (tone?: VariantProps<typeof eyebrowVariants>[
70
71
  * `as` covers the case the span cannot: an eyebrow that is also the section's
71
72
  * heading. See `TypographyTag` in as.tsx for why the classes hold across tags.
72
73
  */
73
- export declare function TypographyEyebrow({ className, tone, as, children, ...props }: WithAs<VariantProps<typeof eyebrowVariants>>): import("react").JSX.Element;
74
+ export declare function TypographyEyebrow({ className, tone, size, as, children, ...props }: WithAs<VariantProps<typeof eyebrowVariants>>): import("react").JSX.Element;
74
75
  export {};
@@ -92,15 +92,32 @@ export function TypographyH4({ className, children, ...props }) {
92
92
  }
93
93
  const eyebrowVariants = cva("block uppercase tracking-wider", {
94
94
  variants: {
95
+ /** Ink and weight. Every tone states its ink: an eyebrow names the section
96
+ * under it, and one that takes its colour from its surroundings is not a
97
+ * heading. Weight is load-bearing too, since caps at this size lose their
98
+ * shape at 400. */
95
99
  tone: {
96
- /**
97
- * Weight is load-bearing here: uppercase at this size loses shape at 400.
98
- * Primary ink, stated not inherited — an eyebrow names the section under it,
99
- * and one that turns red from its surroundings is not a heading.
100
- */
101
100
  heading: "text-xs font-semibold text-[color:var(--ink,var(--foreground))]",
102
101
  /** Stat cards invert it: the figure is the headline, so the label yields. */
103
102
  label: "text-2xs font-medium text-accent-foreground",
103
+ /** The dense product default: a micro-label over a group of controls,
104
+ * quiet enough that the rows under it stay the thing being read. */
105
+ muted: "text-2xs font-medium text-[color:var(--ink-muted,var(--muted-foreground))]",
106
+ /** A rung quieter again, for a label the reader only needs on the way past:
107
+ * a column head in a mock, a rail marker, a key in a spec table. */
108
+ subtle: "text-2xs font-medium text-subtle-foreground",
109
+ },
110
+ /**
111
+ * The rung, when the tone's own is wrong for the surface — a dense table head
112
+ * wants the smallest one, a page standfirst the largest. Declared after the
113
+ * tone so the merge keeps this one, and omitted it leaves the tone's rung
114
+ * standing, which is what every existing call site relies on.
115
+ */
116
+ size: {
117
+ sm: "text-sm",
118
+ xs: "text-xs",
119
+ "2xs": "text-2xs",
120
+ "3xs": "text-3xs",
104
121
  },
105
122
  },
106
123
  defaultVariants: { tone: "heading" },
@@ -111,7 +128,7 @@ const eyebrowVariants = cva("block uppercase tracking-wider", {
111
128
  * `headingClass`. It exists so a consumer needing the class can take ours rather
112
129
  * than hand-rolling a copy that drifts from the component.
113
130
  */
114
- export const eyebrowClass = (tone) => eyebrowVariants({ tone });
131
+ export const eyebrowClass = (tone, size) => eyebrowVariants({ tone, size });
115
132
  /**
116
133
  * An all-caps micro-label above a stat or a group of controls, and — since the
117
134
  * deck was folded into it — the standfirst that sits with a page title.
@@ -119,6 +136,6 @@ export const eyebrowClass = (tone) => eyebrowVariants({ tone });
119
136
  * `as` covers the case the span cannot: an eyebrow that is also the section's
120
137
  * heading. See `TypographyTag` in as.tsx for why the classes hold across tags.
121
138
  */
122
- export function TypographyEyebrow({ className, tone, as, children, ...props }) {
123
- return (_jsx(TextAs, { as: as, className: cn(eyebrowVariants({ tone }), className), ...props, children: children }));
139
+ export function TypographyEyebrow({ className, tone, size, as, children, ...props }) {
140
+ return (_jsx(TextAs, { as: as, className: cn(eyebrowVariants({ tone, size }), className), ...props, children: children }));
124
141
  }
@@ -11,12 +11,19 @@ declare const pVariants: (props?: ({
11
11
  tone?: "muted" | "default" | null | undefined;
12
12
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
13
13
  export type ParagraphVariants = VariantProps<typeof pVariants>;
14
- export declare function TypographyP({ className, variant, tone, children, ...props }: ComponentProps<"p"> & ParagraphVariants): import("react").JSX.Element;
14
+ /**
15
+ * `as` is here for the reason the caption has it, one step further on: a
16
+ * component that hands its body to a caller cannot know whether what arrives is
17
+ * a sentence or a list, and a paragraph may hold neither a list nor a div. The
18
+ * HTML parser closes the `<p>` early and React reports a hydration error, so
19
+ * every wrapper of that shape (`Callout` was the one) renders `as="div"`.
20
+ */
21
+ export declare function TypographyP({ className, variant, tone, as, children, ...props }: WithAs<ParagraphVariants>): import("react").JSX.Element;
15
22
  /** A preset's props: its base's, minus the axes it has decided. `keyof Pins` reads
16
23
  * the exclusion off the pinned object the preset also spreads, so the two cannot
17
24
  * drift — `<TypographyMuted tone="default">` used to compile and un-mute it. */
18
25
  type Preset<Base, Pins> = Omit<Base, keyof Pins>;
19
- type ParagraphProps = ComponentProps<"p"> & ParagraphVariants;
26
+ type ParagraphProps = WithAs<ParagraphVariants>;
20
27
  /** The UI rung in the secondary ink. */
21
28
  declare const MUTED: {
22
29
  readonly tone: "muted";
@@ -48,10 +55,11 @@ export declare function TypographyProseList(props: Preset<ListProps, typeof PROS
48
55
  * `sm` is the default because meta is separated from body by ink, not size;
49
56
  * the smaller rungs are a deliberate step down, not the norm.
50
57
  *
51
- * Leading is pinned per size rather than left to the rung. Plenty of captions
52
- * are a wrapped sentence, and the ramp's tight setting sets those cramped
53
- * descenders nearly on the caps below. `leading-normal` writes `--tw-leading`,
54
- * the variable the `text-*` step reads, so 1.5 wins at every rung.
58
+ * The two upper rungs open their leading past the ramp: plenty of captions are
59
+ * a wrapped sentence, and the ramp sets those cramped at reading rungs. The
60
+ * micro rung keeps whatever the ramp gives it, which is already tuned tight —
61
+ * half again the glyph height at 11px reads as a stray gap under a one-line
62
+ * note, and it is the rung the labels beside it are set on.
55
63
  *
56
64
  * `inherit` is the parenthetical inside a heading, an eyebrow or a stat. It
57
65
  * takes the size of whatever set it and resets the weight, because the only
@@ -25,8 +25,15 @@ const pVariants = cva("", {
25
25
  },
26
26
  defaultVariants: { variant: "ui", tone: "default" },
27
27
  });
28
- export function TypographyP({ className, variant, tone, children, ...props }) {
29
- return (_jsx("p", { className: cn(pVariants({ variant, tone }), className), ...props, children: children }));
28
+ /**
29
+ * `as` is here for the reason the caption has it, one step further on: a
30
+ * component that hands its body to a caller cannot know whether what arrives is
31
+ * a sentence or a list, and a paragraph may hold neither a list nor a div. The
32
+ * HTML parser closes the `<p>` early and React reports a hydration error, so
33
+ * every wrapper of that shape (`Callout` was the one) renders `as="div"`.
34
+ */
35
+ export function TypographyP({ className, variant, tone, as = "p", children, ...props }) {
36
+ return (_jsx(TextAs, { as: as, className: cn(pVariants({ variant, tone }), className), ...props, children: children }));
30
37
  }
31
38
  /** The UI rung in the secondary ink. */
32
39
  const MUTED = { tone: "muted" };
@@ -79,10 +86,11 @@ export function TypographyProseList(props) {
79
86
  * `sm` is the default because meta is separated from body by ink, not size;
80
87
  * the smaller rungs are a deliberate step down, not the norm.
81
88
  *
82
- * Leading is pinned per size rather than left to the rung. Plenty of captions
83
- * are a wrapped sentence, and the ramp's tight setting sets those cramped
84
- * descenders nearly on the caps below. `leading-normal` writes `--tw-leading`,
85
- * the variable the `text-*` step reads, so 1.5 wins at every rung.
89
+ * The two upper rungs open their leading past the ramp: plenty of captions are
90
+ * a wrapped sentence, and the ramp sets those cramped at reading rungs. The
91
+ * micro rung keeps whatever the ramp gives it, which is already tuned tight —
92
+ * half again the glyph height at 11px reads as a stray gap under a one-line
93
+ * note, and it is the rung the labels beside it are set on.
86
94
  *
87
95
  * `inherit` is the parenthetical inside a heading, an eyebrow or a stat. It
88
96
  * takes the size of whatever set it and resets the weight, because the only
@@ -94,7 +102,7 @@ const captionVariants = cva("text-[color:var(--ink-muted,var(--muted-foreground)
94
102
  size: {
95
103
  sm: "text-sm leading-normal",
96
104
  xs: "text-xs leading-normal",
97
- "2xs": "text-2xs leading-normal",
105
+ "2xs": "text-2xs",
98
106
  inherit: "font-normal",
99
107
  },
100
108
  },
package/llms.txt CHANGED
@@ -90,7 +90,7 @@ silently. Full reference: https://github.com/supertypeai/foundations
90
90
  - `TypographyMuted`, `TypographyProse`, `TypographyProseList` are presets. The
91
91
  prop each one pins is dropped from its type, so passing it fails to compile.
92
92
  - `TypographyCaption`, `TypographyLabel`, `TypographySmall`: `size?: "sm" | "xs" | "2xs" | "inherit"`, `as?`.
93
- - `TypographyEyebrow`: `tone?: "heading" | "label"`, `as?`.
93
+ - `TypographyEyebrow`: `tone?: "heading" | "label" | "muted" | "subtle"`, `size?: "sm" | "xs" | "2xs" | "3xs"`, `as?`. Each tone carries the rung it is usually set at and `size` overrides it, so omitting it changes nothing. Reach for `muted` for the uppercase micro-label a dense product sets over a group of controls, and `subtle` for a column head or rail marker read on the way past — that shape hand-rolled is the most common way an app ends up spelling type classes.
94
94
  - `TypographyStat`: `size?: "inherit" | "card" | "panel" | "page" | "display"`, `figures?: "tabular" | "proportional"`. Keep tabular where a value updates in place.
95
95
  - `TypographyLink`: `href` (required), `tone?: Tone` (default `muted`), `addArrow?`, `newTab?`. The href decides internal versus external.
96
96
  - `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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supertype.ai/foundations",
3
- "version": "0.1.31",
3
+ "version": "0.1.33",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public",
package/src/type.css CHANGED
@@ -71,3 +71,33 @@
71
71
  .editorial.font-heading {
72
72
  font-weight: var(--heading-weight);
73
73
  }
74
+
75
+ /* A glyph set beside words, sized and seated so it belongs to the line.
76
+
77
+ Two numbers, both read off the font rather than chosen. Lucide draws its ink
78
+ across 22 of its 24 grid units, so a 0.8em box puts that ink at 1.02 times the
79
+ cap height of whatever rung it sits beside.
80
+
81
+ The seat corrects a bias the browser introduces. Centring aligns a glyph on the
82
+ middle of the line box, and the line box is built from the font's ascent and
83
+ descent rounded to whole pixels, which lands its middle below the middle of the
84
+ capitals: 0.31px low at the 11px rung, 0.46px at the 10px, 0.50px at the 13px.
85
+ So a centred glyph is always seated slightly low, by an amount that changes with
86
+ the rung, which is why hand nudges get written one place at a time and never
87
+ generalise. Lifting by 0.035em takes it back onto the cap band across the ramp.
88
+
89
+ A box pinned in pixels cannot hold the first number either, because the band it
90
+ has to match moves with the rung. Twelve pixels is 1.39 times the cap height at
91
+ the 11px rung and 1.53 at the 10px one, so the glyph climbs over the ascenders
92
+ and hangs well under the baseline while every letter beside it rests on it.
93
+
94
+ For a glyph inline with words. A glyph that stands alone, in a button, a nav
95
+ rail or an avatar slot, has no band to answer to and keeps a fixed size.
96
+
97
+ The 22-of-24 figure is lucide's grid. An app on a different icon set wants a
98
+ different box for the same ratio. */
99
+ @utility icon-inline {
100
+ width: 0.8em;
101
+ height: 0.8em;
102
+ translate: 0 -0.035em;
103
+ }