@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,3 +1,4 @@
1
+ import { FOCUS_RING } from "./focus.js";
1
2
  /**
2
3
  * The segmented picker, as one set of surfaces.
3
4
  *
@@ -9,29 +10,37 @@
9
10
  * class strings, which is exactly the kind of duplication that drifts the first time
10
11
  * someone retunes the palette.
11
12
  *
12
- * Some entries appear twice, once bare and once `data-active:`-prefixed. That is not
13
- * laziness: Tailwind generates a utility only if it appears literally in scanned source, so
14
- * prefixing at runtime would compile to classes that never got built. Writing both forms
15
- * side by side is the honest version, and it keeps the pair impossible to change by half.
16
- *
17
13
  * The active segment is deliberately flat — a card surface and a hairline, no shadow.
18
14
  * Elevation in this system means "this layer left the page plane" (see --elevation-* in
19
15
  * theme.css), and a segment sitting inside its own track has not.
16
+ *
17
+ * The other half of that: the segment stays on the page plane because the RAIL drops below
18
+ * it. The well is the shadow and the hairline, not a fill — the track is `bg-background`
19
+ * pressed in. A `--muted` rail cannot work, because `--muted` sits below `--card` in light
20
+ * and above it in dark, so a muted fill reads correct in one theme and inverted in the
21
+ * other, and `dark:` is what the package's own ESLint rule exists to stop.
20
22
  */
21
23
  export const SEGMENT = {
22
- /** The rail a set of segments sits in. Surface only; each component owns its layout. */
23
- track: "rounded-lg border border-border bg-muted/40 p-0.5",
24
+ /**
25
+ * The rail a set of segments sits in. Surface only; each component owns its layout.
26
+ *
27
+ * `rounded-md` over `activeSurface`'s `rounded-sm` is the 2px of `p-0.5`: concentric
28
+ * radii, so the segment's corner runs parallel to the rail's rather than across it.
29
+ */
30
+ track: "rounded-md border border-border bg-background p-0.5 shadow-recessed",
24
31
  /** Affordances every segment shares, whatever its shape or engine. */
25
- item: "relative inline-flex items-center gap-1.5 font-medium outline-none transition-colors focus-visible:ring-3 focus-visible:ring-ring/50",
32
+ item: `relative inline-flex items-center gap-1.5 font-medium transition-colors ${FOCUS_RING}`,
26
33
  /** Selected: the ink lifts to full strength. */
27
34
  active: "text-foreground",
28
35
  /**
29
36
  * Unselected: quiet, but lighting its own surface on hover so the whole strip reads as
30
- * reachable rather than only the segment already chosen.
37
+ * reachable rather than only the segment already chosen. It moves toward `activeSurface`
38
+ * and not toward the rail, so a hover previews being picked.
39
+ */
40
+ idle: "text-muted-foreground hover:bg-card/60 hover:text-foreground",
41
+ /**
42
+ * The flat surface marking the selection. One string, worn by both engines: the marketing
43
+ * picker slides it with `motion`, `Tabs` hands it to the element Base UI positions.
31
44
  */
32
- idle: "text-muted-foreground hover:bg-muted/60 hover:text-foreground",
33
- /** The flat surface marking the selection. */
34
- activeSurface: "bg-card ring-1 ring-border",
35
- /** `activeSurface`, for an engine that marks its own trigger with `data-active`. */
36
- dataActiveSurface: "data-active:bg-card data-active:text-foreground data-active:ring-1 data-active:ring-border",
45
+ activeSurface: "rounded-sm bg-card ring-1 ring-border",
37
46
  };
@@ -1,32 +1,67 @@
1
1
  import { Tabs as TabsPrimitive } from "@base-ui/react/tabs";
2
2
  import type { ReactNode } from "react";
3
3
  import { type VariantProps } from "class-variance-authority";
4
+ import { type Tone } from "../tone.js";
4
5
  declare function Tabs({ className, orientation, ...props }: TabsPrimitive.Root.Props): import("react").JSX.Element;
6
+ /**
7
+ * Surfaces come from SEGMENT, so this and marketing/segmented-control cannot drift: a
8
+ * reader who meets the picker on a docs page and again on the usage dashboard should not
9
+ * have to learn it twice. Layout stays local, since only this one has orientation to serve.
10
+ *
11
+ * Each variant states its own box, rather than sharing a base tuned for the boxed track
12
+ * that `line` then had to undo at the call site.
13
+ */
5
14
  declare const tabsListVariants: (props?: ({
6
15
  variant?: "line" | "default" | null | undefined;
7
16
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
8
- declare function TabsList({ className, variant, ...props }: TabsPrimitive.List.Props & VariantProps<typeof tabsListVariants>): import("react").JSX.Element;
17
+ /**
18
+ * `tone` inks the marker, and only the marker. On `line` that is the underline and the
19
+ * active tab's icon; the boxed track's marker is a card surface and a hairline, which
20
+ * SEGMENT keeps deliberately flat, so a tone there would be a colour with nothing to
21
+ * paint. The label stays `--foreground` in both: it is read, not signalled.
22
+ */
23
+ declare function TabsList({ className, variant, tone, children, ...props }: TabsPrimitive.List.Props & VariantProps<typeof tabsListVariants> & {
24
+ tone?: Tone;
25
+ }): import("react").JSX.Element;
9
26
  declare function TabsTrigger({ className, ...props }: TabsPrimitive.Tab.Props): import("react").JSX.Element;
10
27
  declare function TabsContent({ className, ...props }: TabsPrimitive.Panel.Props): import("react").JSX.Element;
28
+ /** One tab, whole: what it is called, what marks it, and what it shows. */
29
+ export type TabItem = {
30
+ /** Stable across a relabel — it is what `defaultValue` and `onValueChange` speak. */
31
+ value: string;
32
+ label: ReactNode;
33
+ /**
34
+ * An element — `<Icons.Mic />`, `<PriceChip />` — sized and inked by the trigger.
35
+ *
36
+ * An element and not a component, which this briefly also took. `TabGroup` is a client
37
+ * component, so a component reference handed to it from a server page is a function
38
+ * crossing the RSC boundary, which React refuses at render; an element is already
39
+ * rendered and crosses fine. One accepted shape also spares the slot a branch, and it
40
+ * is the shape `Card`'s `icon` has always taken.
41
+ */
42
+ icon?: ReactNode;
43
+ content: ReactNode;
44
+ };
11
45
  /**
12
- * The declarative shorthand: `items` plus a `<Tab>` per panel. `TabGroup` is to
13
- * `Tabs` what `DisclosureGroup` is to `Accordion` the shape you reach for when
14
- * the tabs are data, and what an MDX author writes as `<Tabs>`.
46
+ * The declarative shorthand: the tabs as data. `TabGroup` is to `Tabs` what
47
+ * `DisclosureGroup` is to `Accordion`, and it is the shape to reach for — an app that
48
+ * rebuilds it over the primitives ends up re-adding the icon, the change handler and the
49
+ * stable value by hand.
15
50
  *
16
- * A caller passing data has no value to bind, so children pair with `items`
17
- * **by position** `value` on a `<Tab>` is for readability and is not matched,
18
- * since matching would silently drop a panel on an edited label. Everything
19
- * below the adapter is the same component the product surfaces use, so a tab
20
- * strip in the docs and one on a dashboard behave identically.
51
+ * Everything below the adapter is the same component the product surfaces use, so a tab
52
+ * strip in the docs and one on a dashboard behave identically. The positional
53
+ * `items`-plus-children shape lives in the MDX map, the only thing that speaks it.
21
54
  */
22
- export declare function TabGroup({ items, children, className, }: {
23
- items: string[];
24
- children: ReactNode;
25
- className?: string;
26
- }): import("react").JSX.Element;
27
- /** `value` names the panel at the call site; it is not used for matching. */
28
- export declare function Tab({ children }: {
55
+ export declare function TabGroup({ tabs, defaultValue, value, onValueChange, variant, tone, iconPosition, className, }: {
56
+ tabs: readonly TabItem[];
57
+ /** Defaults to the first tab, since a picker with nothing picked is not a state. */
58
+ defaultValue?: string;
59
+ /** Pass with `onValueChange` to drive it from outside. */
29
60
  value?: string;
30
- children: ReactNode;
61
+ onValueChange?: (value: string) => void;
62
+ variant?: VariantProps<typeof tabsListVariants>["variant"];
63
+ tone?: Tone;
64
+ iconPosition?: "inline-start" | "inline-end";
65
+ className?: string;
31
66
  }): import("react").JSX.Element;
32
67
  export { Tabs, TabsList, TabsTrigger, TabsContent };
@@ -1,8 +1,9 @@
1
1
  "use client";
2
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { Tabs as TabsPrimitive } from "@base-ui/react/tabs";
4
4
  import { cva } from "class-variance-authority";
5
5
  import { cn } from "../cn.js";
6
+ import { toneClass } from "../tone.js";
6
7
  import { SEGMENT } from "./segment.js";
7
8
  function Tabs({ className, orientation = "horizontal", ...props }) {
8
9
  return (_jsx(TabsPrimitive.Root, { "data-slot": "tabs", "data-orientation": orientation, className: cn(
@@ -12,58 +13,106 @@ function Tabs({ className, orientation = "horizontal", ...props }) {
12
13
  // flex container and laid its panels out beside the tab strip.
13
14
  "group/tabs flex gap-2 data-[orientation=horizontal]:flex-col", className), ...props }));
14
15
  }
15
- // Surfaces come from SEGMENT, so this and marketing/segmented-control cannot drift: a
16
- // reader who meets the picker on a docs page and again on the usage dashboard should not
17
- // have to learn it twice. Layout stays local, since only this one has orientation to serve.
18
- const tabsListVariants = cva("group/tabs-list inline-flex h-8 w-fit items-center justify-center text-muted-foreground data-[variant=line]:rounded-none", {
16
+ /**
17
+ * Surfaces come from SEGMENT, so this and marketing/segmented-control cannot drift: a
18
+ * reader who meets the picker on a docs page and again on the usage dashboard should not
19
+ * have to learn it twice. Layout stays local, since only this one has orientation to serve.
20
+ *
21
+ * Each variant states its own box, rather than sharing a base tuned for the boxed track
22
+ * that `line` then had to undo at the call site.
23
+ */
24
+ const tabsListVariants = cva("group/tabs-list inline-flex w-fit items-center text-muted-foreground", {
19
25
  variants: {
20
26
  variant: {
21
- default: SEGMENT.track,
22
- line: "gap-1 border-0 bg-transparent",
27
+ /** The boxed track: one fixed-height rail, segments splitting it evenly. */
28
+ default: cn(SEGMENT.track, "h-8 justify-center"),
29
+ /**
30
+ * A strip of labels over a rule. Free to wrap, so no fixed height — and `pb-2` is
31
+ * the marker's own room (6px offset plus its 2px), so the list's box contains
32
+ * everything the list draws and the gaps below measure from the right edge. The
33
+ * row gap clears the same 8px, or a wrapped row wears the rule above it.
34
+ */
35
+ line: "flex-wrap justify-start gap-x-1 gap-y-3 rounded-none border-0 bg-transparent pb-2",
23
36
  },
24
37
  },
25
38
  defaultVariants: {
26
39
  variant: "default",
27
40
  },
28
41
  });
29
- function TabsList({ className, variant = "default", ...props }) {
30
- return (_jsx(TabsPrimitive.List, { "data-slot": "tabs-list", "data-variant": variant, className: cn(tabsListVariants({ variant }), className), ...props }));
42
+ /**
43
+ * The marker: one element, positioned by Base UI from `--active-tab-left/top/width/height`
44
+ * on the list, dressed by the variant it is in. It outlives the selection, so it carries
45
+ * the answer from the old tab to the new instead of being destroyed and rebuilt.
46
+ *
47
+ * `data-activation-direction` is `none` before anything is picked, and that is the one case
48
+ * that must not animate — without the guard every strip on the page slides in from its left
49
+ * edge on hydration.
50
+ */
51
+ const tabsIndicatorVariants = cva("pointer-events-none absolute left-0 top-0 w-(--active-tab-width) transition-[translate,width] duration-200 ease-out data-[activation-direction=none]:transition-none motion-reduce:transition-none", {
52
+ variants: {
53
+ variant: {
54
+ /** The pill, on the page plane. The rail below it is what carries the depth. */
55
+ default: cn(SEGMENT.activeSurface, "h-(--active-tab-height) translate-x-(--active-tab-left) translate-y-(--active-tab-top)"),
56
+ /**
57
+ * A rule under the label, clear of the descenders, in the list's tone. The 6px
58
+ * offset plus its own 2px is the `pb-2` the line list reserves — the two are one
59
+ * measurement, and changing either alone puts the rule back outside its box.
60
+ */
61
+ line: "h-0.5 translate-x-(--active-tab-left) translate-y-[calc(var(--active-tab-top)+var(--active-tab-height)+6px)] rounded-full bg-(--tone-hue)",
62
+ },
63
+ },
64
+ defaultVariants: { variant: "default" },
65
+ });
66
+ /**
67
+ * `tone` inks the marker, and only the marker. On `line` that is the underline and the
68
+ * active tab's icon; the boxed track's marker is a card surface and a hairline, which
69
+ * SEGMENT keeps deliberately flat, so a tone there would be a colour with nothing to
70
+ * paint. The label stays `--foreground` in both: it is read, not signalled.
71
+ */
72
+ function TabsList({ className, variant = "default", tone = "primary", children, ...props }) {
73
+ return (_jsxs(TabsPrimitive.List, { "data-slot": "tabs-list", "data-variant": variant, className: cn("relative", toneClass(tone), tabsListVariants({ variant }), className), ...props, children: [_jsx(TabsPrimitive.Indicator, { renderBeforeHydration: true, className: tabsIndicatorVariants({ variant }) }), children] }));
31
74
  }
32
75
  function TabsTrigger({ className, ...props }) {
33
- return (_jsx(TabsPrimitive.Tab, { "data-slot": "tabs-trigger", className: cn(SEGMENT.item, SEGMENT.dataActiveSurface, "rounded-md text-muted-foreground hover:text-foreground",
34
- // A trigger fills its share of the track and may carry an icon, neither of which a
35
- // standalone picker button has to do.
36
- "h-[calc(100%-1px)] flex-1 justify-center px-1.5 py-0.5 text-sm whitespace-nowrap disabled:pointer-events-none disabled:opacity-50 has-data-[icon=inline-end]:pr-1 has-data-[icon=inline-start]:pl-1 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
37
- // The idle hover, scoped to the boxed variant. Spelled out rather than
38
- // interpolated: an interpolated class is invisible to Tailwind's scanner and would
39
- // compile to nothing at all.
40
- "group-data-[variant=default]/tabs-list:not-data-active:hover:bg-muted/60",
41
- // The line variant wears no surface at all; the underline below is its whole signal.
42
- "group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent group-data-[variant=line]/tabs-list:data-active:ring-0",
43
- // That underline carries the accent rather than plain ink, so it reads as the
44
- // brand's marker and not as a bold rule.
45
- "after:absolute after:inset-x-0 after:bottom-[-5px] after:h-0.5 after:bg-primary after:opacity-0 after:transition-opacity group-data-[variant=line]/tabs-list:data-active:after:opacity-100", className), ...props }));
76
+ return (_jsx(TabsPrimitive.Tab, { "data-slot": "tabs-trigger", className: cn(SEGMENT.item,
77
+ // Ink only the surface and the underline belong to the indicator. The radius is
78
+ // for the hover wash, and matches the marker that wash previews.
79
+ "rounded-sm text-muted-foreground hover:text-foreground data-active:text-foreground", "px-1.5 py-0.5 text-sm whitespace-nowrap disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
80
+ // An icon sits inside the label's gap, so the padding on that side comes off.
81
+ // `TabGroup` writes the `data-icon` these two read.
82
+ "has-data-[icon=inline-start]:pl-1 has-data-[icon=inline-end]:pr-1",
83
+ // Filling the track is the boxed variant's business; a line tab is as wide as its
84
+ // label. The variant is read off the list's `data-variant` rather than taken as a
85
+ // prop, so a caller states it once on `TabsList` and not on every trigger.
86
+ // Selectors are spelled out, never interpolated: Tailwind's scanner cannot see an
87
+ // interpolated class and would compile nothing.
88
+ "group-data-[variant=default]/tabs-list:h-full group-data-[variant=default]/tabs-list:flex-1 group-data-[variant=default]/tabs-list:justify-center",
89
+ // Hover moves toward the marker's surface, so it previews the selection.
90
+ "group-data-[variant=default]/tabs-list:not-data-active:hover:bg-card/60",
91
+ // The active icon takes the ink the marker is drawn in.
92
+ "group-data-[variant=line]/tabs-list:data-active:[&_[data-icon]]:text-(color:--tone-hue)", className), ...props }));
46
93
  }
47
94
  function TabsContent({ className, ...props }) {
48
95
  return (_jsx(TabsPrimitive.Panel, { "data-slot": "tabs-content", className: cn("flex-1 text-sm outline-none", className), ...props }));
49
96
  }
97
+ /** `data-icon` is the hook the trigger's padding and tone selectors read. */
98
+ function TabIconSlot({ icon, position, }) {
99
+ return (_jsx("span", { "data-icon": position, className: "flex items-center transition-colors [&_svg]:size-4 [&_svg]:shrink-0", children: icon }));
100
+ }
50
101
  /**
51
- * The declarative shorthand: `items` plus a `<Tab>` per panel. `TabGroup` is to
52
- * `Tabs` what `DisclosureGroup` is to `Accordion` the shape you reach for when
53
- * the tabs are data, and what an MDX author writes as `<Tabs>`.
102
+ * The declarative shorthand: the tabs as data. `TabGroup` is to `Tabs` what
103
+ * `DisclosureGroup` is to `Accordion`, and it is the shape to reach for — an app that
104
+ * rebuilds it over the primitives ends up re-adding the icon, the change handler and the
105
+ * stable value by hand.
54
106
  *
55
- * A caller passing data has no value to bind, so children pair with `items`
56
- * **by position** `value` on a `<Tab>` is for readability and is not matched,
57
- * since matching would silently drop a panel on an edited label. Everything
58
- * below the adapter is the same component the product surfaces use, so a tab
59
- * strip in the docs and one on a dashboard behave identically.
107
+ * Everything below the adapter is the same component the product surfaces use, so a tab
108
+ * strip in the docs and one on a dashboard behave identically. The positional
109
+ * `items`-plus-children shape lives in the MDX map, the only thing that speaks it.
60
110
  */
61
- export function TabGroup({ items, children, className, }) {
62
- const panels = Array.isArray(children) ? children : [children];
63
- return (_jsxs(Tabs, { defaultValue: 0, className: cn("my-6", className), children: [_jsx(TabsList, { children: items.map((label, i) => (_jsx(TabsTrigger, { value: i, children: label }, label))) }), panels.map((panel, i) => (_jsx(TabsContent, { value: i, className: "pt-2 text-muted-foreground", children: panel }, i)))] }));
64
- }
65
- /** `value` names the panel at the call site; it is not used for matching. */
66
- export function Tab({ children }) {
67
- return _jsx(_Fragment, { children: children });
111
+ export function TabGroup({ tabs, defaultValue, value, onValueChange, variant, tone, iconPosition = "inline-start", className, }) {
112
+ return (
113
+ // The handler is adapted rather than wrapped when absent: an arrow declared
114
+ // unconditionally is a function crossing the server boundary on every page that
115
+ // renders tabs without one.
116
+ _jsxs(Tabs, { defaultValue: defaultValue ?? tabs[0]?.value, value: value, onValueChange: onValueChange && ((next) => onValueChange(String(next))), className: cn("my-6", className), children: [_jsx(TabsList, { variant: variant, tone: tone, children: tabs.map(({ value: tabValue, label, icon }) => (_jsxs(TabsTrigger, { value: tabValue, children: [icon && iconPosition === "inline-start" && (_jsx(TabIconSlot, { icon: icon, position: "inline-start" })), label, icon && iconPosition === "inline-end" && (_jsx(TabIconSlot, { icon: icon, position: "inline-end" }))] }, tabValue))) }), tabs.map(({ value: tabValue, content }) => (_jsx(TabsContent, { value: tabValue, className: "pt-2 text-muted-foreground", children: content }, tabValue)))] }));
68
117
  }
69
118
  export { Tabs, TabsList, TabsTrigger, TabsContent };
@@ -10,6 +10,7 @@ exports.themeOverrideRules = themeOverrideRules;
10
10
  exports.surfaceAsInkRules = surfaceAsInkRules;
11
11
  exports.renamedTokenRules = renamedTokenRules;
12
12
  exports.typographyRules = typographyRules;
13
+ exports.designRules = designRules;
13
14
  exports.designConfig = designConfig;
14
15
  /** A className written as a plain string, or as a chunk of a template literal. */
15
16
  const classString = (pattern) => [
@@ -55,10 +56,13 @@ function surfaceAsInkRules() {
55
56
  }
56
57
  /**
57
58
  * `-foreground` means the label printed on a fill; `-ink` means the hue as
58
- * words. `warn-foreground` was always the ink, under the other name.
59
+ * words. `warn-foreground` and the eight categorical `-foreground` tokens were
60
+ * always inks, under the other name. The old spellings still resolve, so nothing
61
+ * breaks on the day of the rename; this is what stops them surviving it.
59
62
  */
63
+ const RENAMED_INKS = "warn|terracotta|ochre|moss|fern|sage|stone|fig|cocoa";
60
64
  function renamedTokenRules() {
61
- return rule("/(^| )(dark:|hover:|focus:|group-hover:)*(text|bg|border|ring|fill|stroke)-warn-foreground($| )/", "`warn-foreground` is the deprecated name for `warn-ink`. In this package `-foreground` is the label printed on a fill and `-ink` is the hue used as text; warn has no printed-on label, because orange carries white text at no lightness. Use warn-ink.");
65
+ return rule(`/(^| )(dark:|hover:|focus:|group-hover:)*(text|bg|border|ring|fill|stroke|decoration)-(${RENAMED_INKS})-foreground($| )/`, "That is the deprecated name for the same hue's `-ink`. In this package `-foreground` is the label printed on a fill and `-ink` is the hue used as words, and none of these hues has a printed-on label — they are checked at 4.5:1 against the page, and printing one on its own fill measures about 1.2:1. Use `-ink`.");
62
66
  }
63
67
  function typographyRules({ weights = false, ramp = "text-3xs 10 / text-2xs 11 / text-xs 12 / text-sm 14 / text-base 16 and up", pairing = false, axis = false, } = {}) {
64
68
  return [
@@ -114,6 +118,15 @@ function typographyRules({ weights = false, ramp = "text-3xs 10 / text-2xs 11 /
114
118
  : []),
115
119
  ];
116
120
  }
121
+ function designRules({ accents, typography = true, ...type } = {}) {
122
+ return [
123
+ ...colourRules({ accents }),
124
+ ...(typography ? typographyRules(type) : []),
125
+ ...themeOverrideRules(),
126
+ ...surfaceAsInkRules(),
127
+ ...renamedTokenRules(),
128
+ ];
129
+ }
117
130
  /**
118
131
  * Every rule in one flat-config entry, ready to spread into eslint.config.js:
119
132
  *
@@ -126,7 +139,7 @@ function typographyRules({ weights = false, ramp = "text-3xs 10 / text-2xs 11 /
126
139
  * half the set by accident. If you need a second scope, call this again with a
127
140
  * different `files` and no overlap.
128
141
  */
129
- function designConfig({ files = ["**/*.{ts,tsx,js,jsx}"], accents, weights, ramp, pairing, axis, } = {}) {
142
+ function designConfig({ files = ["**/*.{ts,tsx,js,jsx}"], ...options } = {}) {
130
143
  return [
131
144
  {
132
145
  name: "@supertype.ai/foundations/design",
@@ -134,11 +147,7 @@ function designConfig({ files = ["**/*.{ts,tsx,js,jsx}"], accents, weights, ramp
134
147
  rules: {
135
148
  "no-restricted-syntax": [
136
149
  "error",
137
- ...colourRules({ accents }),
138
- ...typographyRules({ weights, ramp, pairing, axis }),
139
- ...themeOverrideRules(),
140
- ...surfaceAsInkRules(),
141
- ...renamedTokenRules(),
150
+ ...designRules(options),
142
151
  ],
143
152
  },
144
153
  },
@@ -36,6 +36,35 @@ export declare function checkLegibility(css: string, { minimum, inks, surfaces,
36
36
  surfaces?: string[] | undefined;
37
37
  themes?: Theme[] | undefined;
38
38
  }): LegibilityFailure[];
39
+ /** What a token is: a surface or mark, a label printed on it, a hue used as words. */
40
+ export interface TokenCuts {
41
+ /** The role itself — a surface, or a mark held to 3:1 against the page. */
42
+ fill: string;
43
+ /**
44
+ * The label printed *on* that fill. The only pair measured one against the
45
+ * other rather than against the page, which is why it is the only pair a
46
+ * swatch may print one on top of the other.
47
+ */
48
+ onFill?: string;
49
+ /**
50
+ * The same hue used as words, held to 4.5:1 against the page and never printed
51
+ * on the fill — there it measures about 1.2:1. Always `-ink`; the categorical
52
+ * hues shipped under `-foreground` until that was corrected, and the old names
53
+ * survive only as aliases the ESLint rules flag.
54
+ */
55
+ asInk?: string;
56
+ }
57
+ /**
58
+ * The cuts a token ships, read off the same three sets `checkSignals` measures.
59
+ *
60
+ * Exported because the alternative is every consumer keeping its own idea of
61
+ * which tokens are pairs — the docs site did, and got the categorical hues
62
+ * wrong, rendering `--ochre` as a lone square while its ink, the colour the
63
+ * marker highlight is painted with, appeared nowhere. A palette checked against
64
+ * one taxonomy and documented from another will drift, and the drift shows up as
65
+ * a page that is quietly wrong rather than a build that fails.
66
+ */
67
+ export declare function tokenCuts(token: string): TokenCuts;
39
68
  /**
40
69
  * The three bars a palette owes, run over the same engine as `checkLegibility`.
41
70
  * Without this the numbers in a theme's comments are claims, not measurements.
package/dist/contrast.js CHANGED
@@ -214,14 +214,14 @@ const INKS_TINTED = [
214
214
  "--info-ink",
215
215
  "--danger",
216
216
  "--secondary-ink",
217
- "--terracotta-foreground",
218
- "--ochre-foreground",
219
- "--moss-foreground",
220
- "--fern-foreground",
221
- "--sage-foreground",
222
- "--stone-foreground",
223
- "--fig-foreground",
224
- "--cocoa-foreground",
217
+ "--terracotta-ink",
218
+ "--ochre-ink",
219
+ "--moss-ink",
220
+ "--fern-ink",
221
+ "--sage-ink",
222
+ "--stone-ink",
223
+ "--fig-ink",
224
+ "--cocoa-ink",
225
225
  ];
226
226
  /**
227
227
  * shadcn's shape: `-foreground` is the label printed on the fill, so the pair is
@@ -236,6 +236,24 @@ const ON_FILL = [
236
236
  ["--popover", "--popover-foreground"],
237
237
  ["--sidebar", "--sidebar-foreground"],
238
238
  ];
239
+ /**
240
+ * The cuts a token ships, read off the same three sets `checkSignals` measures.
241
+ *
242
+ * Exported because the alternative is every consumer keeping its own idea of
243
+ * which tokens are pairs — the docs site did, and got the categorical hues
244
+ * wrong, rendering `--ochre` as a lone square while its ink, the colour the
245
+ * marker highlight is painted with, appeared nowhere. A palette checked against
246
+ * one taxonomy and documented from another will drift, and the drift shows up as
247
+ * a page that is quietly wrong rather than a build that fails.
248
+ */
249
+ export function tokenCuts(token) {
250
+ const fill = token.startsWith("--") ? token : `--${token}`;
251
+ return {
252
+ fill,
253
+ onFill: ON_FILL.find(([surface]) => surface === fill)?.[1],
254
+ asInk: INKS_TINTED.find((ink) => ink === `${fill}-ink`),
255
+ };
256
+ }
239
257
  /**
240
258
  * The three bars a palette owes, run over the same engine as `checkLegibility`.
241
259
  * Without this the numbers in a theme's comments are claims, not measurements.
package/dist/eslint.d.ts CHANGED
@@ -24,10 +24,6 @@ export declare function themeOverrideRules(): RestrictedSyntax[];
24
24
  * shipped at 17 sites. `text-background` is absent: inverse ink is a real role.
25
25
  */
26
26
  export declare function surfaceAsInkRules(): RestrictedSyntax[];
27
- /**
28
- * `-foreground` means the label printed on a fill; `-ink` means the hue as
29
- * words. `warn-foreground` was always the ink, under the other name.
30
- */
31
27
  export declare function renamedTokenRules(): RestrictedSyntax[];
32
28
  export interface TypographyOptions {
33
29
  /** Three-weight ramp. Off for editorial, where 700 is a register not a shout. */
@@ -49,13 +45,35 @@ export interface TypographyOptions {
49
45
  axis?: boolean;
50
46
  }
51
47
  export declare function typographyRules({ weights, ramp, pairing, axis, }?: TypographyOptions): RestrictedSyntax[];
48
+ /**
49
+ * Every design rule, as one list.
50
+ *
51
+ * The five builders below it are still exported, and spreading them by hand is
52
+ * what both consumers were doing — one of them into a flat config, the other
53
+ * into a legacy `.eslintrc`, and *both* of them had quietly left out
54
+ * `renamedTokenRules`, so neither would have flagged a deprecated token name.
55
+ * That is not a mistake either author made; it is what a five-name API costs
56
+ * every time somebody wires it up. Spread this instead, and a rule added here
57
+ * arrives in both apps on their next bump.
58
+ */
59
+ export interface DesignRuleOptions extends ColourOptions, TypographyOptions {
60
+ /**
61
+ * Off for a surface that sets its own type ramp — a marketing page under
62
+ * `.editorial`, a mockup drawing the product at reduced scale. Everything
63
+ * about colour still applies: a deprecated token name is wrong on every
64
+ * surface, which is why this is a flag rather than an invitation to pick
65
+ * three of the five builders by hand.
66
+ */
67
+ typography?: boolean;
68
+ }
69
+ export declare function designRules({ accents, typography, ...type }?: DesignRuleOptions): RestrictedSyntax[];
52
70
  /** A flat-config entry, described structurally so the package needs no ESLint dependency. */
53
71
  export interface FlatConfigEntry {
54
72
  name: string;
55
73
  files: string[];
56
74
  rules: Record<string, unknown>;
57
75
  }
58
- export interface DesignConfigOptions extends ColourOptions, TypographyOptions {
76
+ export interface DesignConfigOptions extends DesignRuleOptions {
59
77
  /** What the rules apply to. Narrow it to exclude generated or vendored code. */
60
78
  files?: string[];
61
79
  }
@@ -71,4 +89,4 @@ export interface DesignConfigOptions extends ColourOptions, TypographyOptions {
71
89
  * half the set by accident. If you need a second scope, call this again with a
72
90
  * different `files` and no overlap.
73
91
  */
74
- export declare function designConfig({ files, accents, weights, ramp, pairing, axis, }?: DesignConfigOptions): FlatConfigEntry[];
92
+ export declare function designConfig({ files, ...options }?: DesignConfigOptions): FlatConfigEntry[];
package/dist/eslint.js CHANGED
@@ -47,10 +47,13 @@ export function surfaceAsInkRules() {
47
47
  }
48
48
  /**
49
49
  * `-foreground` means the label printed on a fill; `-ink` means the hue as
50
- * words. `warn-foreground` was always the ink, under the other name.
50
+ * words. `warn-foreground` and the eight categorical `-foreground` tokens were
51
+ * always inks, under the other name. The old spellings still resolve, so nothing
52
+ * breaks on the day of the rename; this is what stops them surviving it.
51
53
  */
54
+ const RENAMED_INKS = "warn|terracotta|ochre|moss|fern|sage|stone|fig|cocoa";
52
55
  export function renamedTokenRules() {
53
- return rule("/(^| )(dark:|hover:|focus:|group-hover:)*(text|bg|border|ring|fill|stroke)-warn-foreground($| )/", "`warn-foreground` is the deprecated name for `warn-ink`. In this package `-foreground` is the label printed on a fill and `-ink` is the hue used as text; warn has no printed-on label, because orange carries white text at no lightness. Use warn-ink.");
56
+ return rule(`/(^| )(dark:|hover:|focus:|group-hover:)*(text|bg|border|ring|fill|stroke|decoration)-(${RENAMED_INKS})-foreground($| )/`, "That is the deprecated name for the same hue's `-ink`. In this package `-foreground` is the label printed on a fill and `-ink` is the hue used as words, and none of these hues has a printed-on label — they are checked at 4.5:1 against the page, and printing one on its own fill measures about 1.2:1. Use `-ink`.");
54
57
  }
55
58
  export function typographyRules({ weights = false, ramp = "text-3xs 10 / text-2xs 11 / text-xs 12 / text-sm 14 / text-base 16 and up", pairing = false, axis = false, } = {}) {
56
59
  return [
@@ -106,6 +109,15 @@ export function typographyRules({ weights = false, ramp = "text-3xs 10 / text-2x
106
109
  : []),
107
110
  ];
108
111
  }
112
+ export function designRules({ accents, typography = true, ...type } = {}) {
113
+ return [
114
+ ...colourRules({ accents }),
115
+ ...(typography ? typographyRules(type) : []),
116
+ ...themeOverrideRules(),
117
+ ...surfaceAsInkRules(),
118
+ ...renamedTokenRules(),
119
+ ];
120
+ }
109
121
  /**
110
122
  * Every rule in one flat-config entry, ready to spread into eslint.config.js:
111
123
  *
@@ -118,7 +130,7 @@ export function typographyRules({ weights = false, ramp = "text-3xs 10 / text-2x
118
130
  * half the set by accident. If you need a second scope, call this again with a
119
131
  * different `files` and no overlap.
120
132
  */
121
- export function designConfig({ files = ["**/*.{ts,tsx,js,jsx}"], accents, weights, ramp, pairing, axis, } = {}) {
133
+ export function designConfig({ files = ["**/*.{ts,tsx,js,jsx}"], ...options } = {}) {
122
134
  return [
123
135
  {
124
136
  name: "@supertype.ai/foundations/design",
@@ -126,11 +138,7 @@ export function designConfig({ files = ["**/*.{ts,tsx,js,jsx}"], accents, weight
126
138
  rules: {
127
139
  "no-restricted-syntax": [
128
140
  "error",
129
- ...colourRules({ accents }),
130
- ...typographyRules({ weights, ramp, pairing, axis }),
131
- ...themeOverrideRules(),
132
- ...surfaceAsInkRules(),
133
- ...renamedTokenRules(),
141
+ ...designRules(options),
134
142
  ],
135
143
  },
136
144
  },
@@ -1,8 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { cn } from "../cn.js";
3
- import { headingClass, TypographyEyebrow, TypographyH1, TypographyH2, TypographyH3, } from "../typography/header.js";
3
+ import { headingFace, TypographyEyebrow, TypographyH1, TypographyH2, TypographyH3, } from "../typography/header.js";
4
4
  import { TypographyCaption, TypographyMuted, TypographyProse, } from "../typography/paragraph.js";
5
- import { EssayColumns } from "./layout.js";
5
+ import { EssayAside, EssayBody, EssayColumns, PostMetaRow } from "./layout.js";
6
6
  import { TableOfContents } from "./contents.js";
7
7
  /** Pass-through: keeps the className the shell relies on for layout. */
8
8
  const PlainReveal = ({ children, className, }) => _jsx("div", { className: className, children: children });
@@ -31,19 +31,27 @@ const anchorIds = (sections) => {
31
31
  export function createEssay({ Reveal = PlainReveal, Glow = NoGlow, } = {}) {
32
32
  /** Left aligned: the eye has to reach the first line of prose either way. */
33
33
  function EssayHeader({ eyebrow, title, lede, byline, }) {
34
- return (_jsxs("header", { className: "relative overflow-hidden pb-12 pt-16 sm:pt-24", children: [_jsx(Glow, { className: "-top-40 left-1/2 -translate-x-1/2", intensity: 0.16 }), _jsx(EssayColumns, { children: _jsxs(Reveal, { eager: true, className: "flex flex-col gap-6", children: [_jsx(TypographyEyebrow, { children: eyebrow }), _jsx(TypographyH1, { variant: "display", className: "text-balance", children: title }), lede && (_jsx(TypographyMuted, { className: "text-pretty text-xl leading-relaxed", children: lede })), byline && (_jsx(TypographyMuted, { className: "border-t border-border/60 pt-5 text-base", children: byline }))] }) })] }));
34
+ return (_jsxs("header", { className: "relative overflow-hidden pb-12 pt-16 sm:pt-24", children: [_jsx(Glow, { className: "-top-40 left-1/2 -translate-x-1/2", intensity: 0.16 }), _jsx(EssayColumns, { children: _jsxs(Reveal, { eager: true, className: "flex flex-col gap-6", children: [_jsx(TypographyEyebrow, { children: eyebrow }), _jsx(TypographyH1, { variant: "display", className: "text-balance", children: title }), lede && (_jsx(TypographyMuted, { className: "text-pretty text-xl leading-relaxed", children: lede })), byline && _jsx(PostMetaRow, { children: byline })] }) })] }));
35
35
  }
36
36
  /** The reading column, with the sticky index sitting in its left margin. */
37
37
  function EssayLayout({ index, children, }) {
38
- return (_jsx(EssayColumns, { className: "pb-16 sm:pb-24", aside: _jsx("div", { className: "sticky top-24", children: _jsx(TableOfContents, { sections: index }) }), children: _jsx("div", { className: "flex flex-col gap-16 border-t border-border pt-12 @6xl:border-t-0 @6xl:pt-0", children: children }) }));
38
+ return (_jsx(EssayColumns, { className: "pb-16 sm:pb-24", aside: _jsx(EssayAside, { children: _jsx(TableOfContents, { sections: index }) }), children: _jsx(EssayBody, { className: "flex flex-col gap-16", children: children }) }));
39
39
  }
40
40
  /** The heading carries the anchor, offset so it lands under the sticky nav. */
41
41
  function EssaySection({ id, heading, children, }) {
42
42
  return (_jsx("section", { id: id, className: "scroll-mt-24", children: _jsxs(Reveal, { className: "flex flex-col gap-5", children: [_jsx(TypographyH2, { className: "text-balance", children: heading }), children] }) }));
43
43
  }
44
- /** One per essay: a page with three of them has decided nothing. */
44
+ /**
45
+ * One per essay: a page with three of them has decided nothing.
46
+ *
47
+ * The face and a rung, not the whole `headingClass()` ramp — see `headingFace` in
48
+ * typography/header.tsx for why a non-heading takes one and not the other. `text-pretty`
49
+ * over `text-balance`: this is prose set large, so it fills the measure rather than being
50
+ * set in even lines like a headline. The rung is the section heading's, so one pull quote
51
+ * stands level with them and retunes with them under `.editorial`.
52
+ */
45
53
  function EssayPullQuote({ children }) {
46
- return (_jsx(Reveal, { children: _jsx("blockquote", { className: cn(headingClass(), "text-balance border-l-2 border-primary/40 py-1 pl-6 leading-snug"), children: children }) }));
54
+ return (_jsx(Reveal, { children: _jsx("blockquote", { className: cn(headingFace, "text-h2 leading-snug text-foreground text-pretty", "border-l-2 border-primary/40 py-1 pl-6"), children: children }) }));
47
55
  }
48
56
  /** Generic on purpose: a page hands it anything; this decides only the fit. */
49
57
  function EssayFigure({ children, caption, }) {
@@ -4,4 +4,4 @@ export { TableOfContents } from "./contents.js";
4
4
  export { createEssay, EssayHeader, EssayLayout, EssaySection, EssayPullQuote, EssayFigure, EssayMovements, EssayDocument, type EssayDecorations, type EssayIndexEntry, type EssayDocSection, type EssayMovement, } from "./essay.js";
5
5
  export { useReadingProgress, useScrollSpy } from "./scroll.js";
6
6
  export { ReadingProgressBar, ReadingRail } from "./reading.js";
7
- export { EssayColumns, MetaDot, PostMetaRow, PostDate, formatPostDate, type PostDateFormat, ReadTime, TagPills, } from "./layout.js";
7
+ export { EssayColumns, EssayAside, EssayBody, ReadingLayout, MetaDot, PostMetaRow, PostDate, formatPostDate, type PostDateFormat, ReadTime, TagPills, } from "./layout.js";
@@ -6,4 +6,4 @@ export { createEssay,
6
6
  EssayHeader, EssayLayout, EssaySection, EssayPullQuote, EssayFigure, EssayMovements, EssayDocument, } from "./essay.js";
7
7
  export { useReadingProgress, useScrollSpy } from "./scroll.js";
8
8
  export { ReadingProgressBar, ReadingRail } from "./reading.js";
9
- export { EssayColumns, MetaDot, PostMetaRow, PostDate, formatPostDate, ReadTime, TagPills, } from "./layout.js";
9
+ export { EssayColumns, EssayAside, EssayBody, ReadingLayout, MetaDot, PostMetaRow, PostDate, formatPostDate, ReadTime, TagPills, } from "./layout.js";