@supertype.ai/foundations 0.1.28 → 0.1.30
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 +27 -23
- package/bin/foundations.mjs +5 -2
- package/dist/blocks/accordion.js +2 -1
- package/dist/blocks/badge.d.ts +27 -0
- package/dist/blocks/badge.js +86 -0
- package/dist/blocks/button.d.ts +32 -0
- package/dist/blocks/button.js +127 -0
- package/dist/blocks/callout.d.ts +2 -39
- package/dist/blocks/callout.js +13 -38
- package/dist/blocks/card.d.ts +5 -6
- package/dist/blocks/card.js +28 -12
- package/dist/blocks/focus.d.ts +9 -0
- package/dist/blocks/focus.js +9 -0
- package/dist/blocks/index.d.ts +3 -1
- package/dist/blocks/index.js +3 -1
- package/dist/blocks/interactive-accordion.js +2 -1
- package/dist/blocks/render-as.d.ts +11 -0
- package/dist/blocks/render-as.js +22 -0
- package/dist/blocks/segment.d.ts +22 -14
- package/dist/blocks/segment.js +23 -14
- package/dist/blocks/tabs.d.ts +52 -17
- package/dist/blocks/tabs.js +86 -37
- package/dist/cjs/eslint.js +49 -14
- package/dist/contrast.d.ts +56 -0
- package/dist/contrast.js +144 -12
- package/dist/eslint.d.ts +42 -8
- package/dist/eslint.js +47 -14
- package/dist/essay/essay.js +14 -6
- package/dist/essay/index.d.ts +1 -1
- package/dist/essay/index.js +1 -1
- package/dist/essay/layout.d.ts +37 -1
- package/dist/essay/layout.js +33 -1
- package/dist/essay/rail.d.ts +11 -1
- package/dist/essay/reading.d.ts +1 -1
- package/dist/essay/scroll.js +1 -1
- package/dist/href.d.ts +42 -0
- package/dist/href.js +63 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +9 -0
- package/dist/mdx.d.ts +26 -4
- package/dist/mdx.js +30 -4
- package/dist/tone.d.ts +130 -0
- package/dist/tone.js +140 -0
- package/dist/typography/header.d.ts +32 -2
- package/dist/typography/header.js +10 -5
- package/dist/typography/highlight.d.ts +7 -6
- package/dist/typography/highlight.js +15 -9
- package/dist/typography/paragraph.d.ts +12 -25
- package/dist/typography/paragraph.js +40 -29
- package/llms.txt +99 -22
- package/package.json +2 -1
- package/src/theme.css +143 -46
- package/src/tokens.css +5 -4
- package/src/type.css +1 -1
package/dist/essay/layout.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { ComponentProps, ComponentType, ReactNode } from "react";
|
|
2
|
+
import type { TocHeading } from "./toc.js";
|
|
2
3
|
/**
|
|
3
4
|
* Three tracks with the third empty: two would push the prose off-centre the
|
|
4
5
|
* moment an aside appeared, setting body copy on a different axis per page.
|
|
5
6
|
* The measure grows per step — a comfortable line length is a range.
|
|
6
7
|
*
|
|
7
|
-
* The margin track is what the third one buys, and
|
|
8
|
+
* The margin track is what the third one buys, and appears only where the
|
|
8
9
|
* container can pay for it:
|
|
9
10
|
*
|
|
10
11
|
* aside = (container − 3rem padding − measure) ÷ 2 − 2.5rem gutter
|
|
@@ -24,6 +25,41 @@ import type { ComponentProps, ComponentType, ReactNode } from "react";
|
|
|
24
25
|
export declare function EssayColumns({ aside, children, className, ...props }: ComponentProps<"div"> & {
|
|
25
26
|
aside?: ReactNode;
|
|
26
27
|
}): import("react").JSX.Element;
|
|
28
|
+
/**
|
|
29
|
+
* The margin track's contents, pinned as the column scrolls.
|
|
30
|
+
*
|
|
31
|
+
* The offset is stated here and nowhere else: it has to clear the same sticky
|
|
32
|
+
* site nav that `EssaySection`'s `scroll-mt` clears, and two literals a file
|
|
33
|
+
* apart is how an anchored heading ends up under the chrome that the rail
|
|
34
|
+
* scrolled it to.
|
|
35
|
+
*/
|
|
36
|
+
export declare function EssayAside({ children, className, }: {
|
|
37
|
+
children: ReactNode;
|
|
38
|
+
className?: string;
|
|
39
|
+
}): import("react").JSX.Element;
|
|
40
|
+
/**
|
|
41
|
+
* The join between a header and the body under it: the one place in the package that draws
|
|
42
|
+
* it, so a seam cannot be ruled twice by a header and a layout that cannot see each other.
|
|
43
|
+
*
|
|
44
|
+
* The rule is for the narrow layout alone. Past `@6xl` the margin rail marks the join, and
|
|
45
|
+
* the header's own bottom padding is the whole of the gap.
|
|
46
|
+
*/
|
|
47
|
+
export declare function EssayBody({ children, className, }: {
|
|
48
|
+
children: ReactNode;
|
|
49
|
+
className?: string;
|
|
50
|
+
}): import("react").JSX.Element;
|
|
51
|
+
/**
|
|
52
|
+
* The reading column with a scroll-spied rail in its margin: an article whose
|
|
53
|
+
* body is prose or MDX, rather than the declared sections `EssayLayout` sets.
|
|
54
|
+
*
|
|
55
|
+
* The rail is dropped when a piece has no headings, so a short post gets a
|
|
56
|
+
* centred measure instead of a margin holding an empty nav.
|
|
57
|
+
*/
|
|
58
|
+
export declare function ReadingLayout({ headings, children, className, }: {
|
|
59
|
+
headings: readonly TocHeading[];
|
|
60
|
+
children: ReactNode;
|
|
61
|
+
className?: string;
|
|
62
|
+
}): import("react").JSX.Element;
|
|
27
63
|
/** A separator between meta items. Decorative, so it is hidden from assistive tech. */
|
|
28
64
|
export declare function MetaDot({ className }: {
|
|
29
65
|
className?: string;
|
package/dist/essay/layout.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { cn } from "../cn.js";
|
|
3
|
+
import { ReadingRail } from "./reading.js";
|
|
3
4
|
/**
|
|
4
5
|
* Three tracks with the third empty: two would push the prose off-centre the
|
|
5
6
|
* moment an aside appeared, setting body copy on a different axis per page.
|
|
6
7
|
* The measure grows per step — a comfortable line length is a range.
|
|
7
8
|
*
|
|
8
|
-
* The margin track is what the third one buys, and
|
|
9
|
+
* The margin track is what the third one buys, and appears only where the
|
|
9
10
|
* container can pay for it:
|
|
10
11
|
*
|
|
11
12
|
* aside = (container − 3rem padding − measure) ÷ 2 − 2.5rem gutter
|
|
@@ -25,6 +26,37 @@ import { cn } from "../cn.js";
|
|
|
25
26
|
export function EssayColumns({ aside, children, className, ...props }) {
|
|
26
27
|
return (_jsx("div", { className: "@container w-full", children: _jsxs("div", { className: cn("mx-auto grid w-full max-w-6xl gap-10 px-6", "@6xl:grid-cols-[1fr_minmax(0,42rem)_1fr] @6xl:gap-0", "@7xl:max-w-7xl @7xl:grid-cols-[1fr_minmax(0,44rem)_1fr]", "@min-[84rem]:max-w-[84rem] @min-[84rem]:grid-cols-[1fr_minmax(0,46rem)_1fr]", className), ...props, children: [_jsx("div", { className: "hidden @6xl:block @6xl:pr-10", children: aside }), _jsx("div", { className: "mx-auto w-full min-w-0 max-w-2xl @6xl:max-w-none", children: children }), _jsx("div", { className: "hidden @6xl:block" })] }) }));
|
|
27
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* The margin track's contents, pinned as the column scrolls.
|
|
31
|
+
*
|
|
32
|
+
* The offset is stated here and nowhere else: it has to clear the same sticky
|
|
33
|
+
* site nav that `EssaySection`'s `scroll-mt` clears, and two literals a file
|
|
34
|
+
* apart is how an anchored heading ends up under the chrome that the rail
|
|
35
|
+
* scrolled it to.
|
|
36
|
+
*/
|
|
37
|
+
export function EssayAside({ children, className, }) {
|
|
38
|
+
return _jsx("div", { className: cn("sticky top-24", className), children: children });
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The join between a header and the body under it: the one place in the package that draws
|
|
42
|
+
* it, so a seam cannot be ruled twice by a header and a layout that cannot see each other.
|
|
43
|
+
*
|
|
44
|
+
* The rule is for the narrow layout alone. Past `@6xl` the margin rail marks the join, and
|
|
45
|
+
* the header's own bottom padding is the whole of the gap.
|
|
46
|
+
*/
|
|
47
|
+
export function EssayBody({ children, className, }) {
|
|
48
|
+
return (_jsx("div", { className: cn("border-t border-border pt-12 @6xl:border-t-0 @6xl:pt-0", className), children: children }));
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The reading column with a scroll-spied rail in its margin: an article whose
|
|
52
|
+
* body is prose or MDX, rather than the declared sections `EssayLayout` sets.
|
|
53
|
+
*
|
|
54
|
+
* The rail is dropped when a piece has no headings, so a short post gets a
|
|
55
|
+
* centred measure instead of a margin holding an empty nav.
|
|
56
|
+
*/
|
|
57
|
+
export function ReadingLayout({ headings, children, className, }) {
|
|
58
|
+
return (_jsx(EssayColumns, { className: cn("pb-16 sm:pb-24", className), aside: headings.length > 0 ? (_jsx(EssayAside, { children: _jsx(ReadingRail, { headings: headings }) })) : undefined, children: _jsx(EssayBody, { children: children }) }));
|
|
59
|
+
}
|
|
28
60
|
/** A separator between meta items. Decorative, so it is hidden from assistive tech. */
|
|
29
61
|
export function MetaDot({ className }) {
|
|
30
62
|
return (_jsx("span", { "aria-hidden": true, className: cn("text-muted-foreground/50", className), children: "\u00B7" }));
|
package/dist/essay/rail.d.ts
CHANGED
|
@@ -10,6 +10,16 @@ export declare function RailLink({ active, nested, className, children, render,
|
|
|
10
10
|
/** A sub-heading under the item above it, indented a step further in. */
|
|
11
11
|
nested?: boolean;
|
|
12
12
|
children: ReactNode;
|
|
13
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* Swap the anchor for another link element, e.g. `<Link href={…} />`.
|
|
15
|
+
*
|
|
16
|
+
* The one link in the package that does NOT take an `href` and route it
|
|
17
|
+
* itself, and deliberately: this module is reached from `contents.tsx`,
|
|
18
|
+
* `reading.tsx` and `layout.tsx`, which a consumer imports in bare Node and in
|
|
19
|
+
* a test runner with no Next installed. Importing ../href.ts here would put
|
|
20
|
+
* `next/link` on that path — test/essay-toc.test.ts is what holds the line.
|
|
21
|
+
* The rail's own links are `#hash` anchors, which want no router anyway; a
|
|
22
|
+
* rail of routes passes the router's Link through `render`.
|
|
23
|
+
*/
|
|
14
24
|
render?: ReactElement<ComponentProps<"a">>;
|
|
15
25
|
}): import("react").JSX.Element;
|
package/dist/essay/reading.d.ts
CHANGED
|
@@ -12,6 +12,6 @@ export declare function ReadingProgressBar({ className }: {
|
|
|
12
12
|
* stores, so mounting them together costs one scroll subscription, not two.
|
|
13
13
|
*/
|
|
14
14
|
export declare function ReadingRail({ headings, className, }: {
|
|
15
|
-
headings: TocHeading[];
|
|
15
|
+
headings: readonly TocHeading[];
|
|
16
16
|
className?: string;
|
|
17
17
|
}): import("react").JSX.Element | null;
|
package/dist/essay/scroll.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { useEffect, useState, useSyncExternalStore } from "react";
|
|
3
3
|
/**
|
|
4
|
-
* Reading progress 0
|
|
4
|
+
* Reading progress, 0 to 1, from one shared listener: a page mounts both the bar and
|
|
5
5
|
* the rail, and hook-local state would double every subscription. Reads coalesce
|
|
6
6
|
* to a frame, since `scrollHeight` forces layout.
|
|
7
7
|
*/
|
package/dist/href.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { ComponentProps, ReactElement } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* Where a link goes, decided once.
|
|
4
|
+
*
|
|
5
|
+
* This branch — scheme test, router `Link` or plain `<a>`, `rel` on the way out
|
|
6
|
+
* — was written three times: Card, TypographyLink, and (by omission) every call
|
|
7
|
+
* site that reached for `render={<a href="…" />}` because the component it was
|
|
8
|
+
* calling had no `href` of its own. The last of those is the expensive copy: it
|
|
9
|
+
* looks like a styling escape hatch and is actually a routing decision, made at
|
|
10
|
+
* the call site, wrongly. A hero CTA written that way full-page-reloads past the
|
|
11
|
+
* router and drops the view transition, and nothing in the type system says so.
|
|
12
|
+
*
|
|
13
|
+
* So components take `href`, not an anchor. `render` stays for what it is for:
|
|
14
|
+
* an element that is genuinely not an anchor.
|
|
15
|
+
*/
|
|
16
|
+
/** A scheme (`mailto:`, `https:`) means the href leaves the app entirely. */
|
|
17
|
+
export declare function isExternalHref(href: string): boolean;
|
|
18
|
+
export type LinkBehavior = {
|
|
19
|
+
/** Override the scheme sniff: an absolute URL that is home, or a relative one that is not. */
|
|
20
|
+
external?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Defaults on for an http(s) href, off for everything else — `mailto:` and
|
|
23
|
+
* `tel:` hand off to another app and have no tab to open.
|
|
24
|
+
*/
|
|
25
|
+
newTab?: boolean;
|
|
26
|
+
};
|
|
27
|
+
/** `Link` requires its own href; as far as a call site here goes it is an anchor. */
|
|
28
|
+
type AnchorComponent = (props: ComponentProps<"a">) => ReactElement | null;
|
|
29
|
+
export type ResolvedLink = {
|
|
30
|
+
Component: AnchorComponent | "a";
|
|
31
|
+
/** Spread onto the element: the href, plus `target`/`rel` when it opens away. */
|
|
32
|
+
props: ComponentProps<"a">;
|
|
33
|
+
/** For a caller that renders differently for an off-site link — an arrow glyph, an icon. */
|
|
34
|
+
external: boolean;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* A same-page hash is the one internal href that stays a plain anchor: routing
|
|
38
|
+
* `#section` through the router asks for a navigation and a view transition to
|
|
39
|
+
* reach a place the browser can already scroll to.
|
|
40
|
+
*/
|
|
41
|
+
export declare function resolveLink(href: string, { external, newTab }?: LinkBehavior): ResolvedLink;
|
|
42
|
+
export {};
|
package/dist/href.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Link } from "next-view-transitions";
|
|
2
|
+
/**
|
|
3
|
+
* Where a link goes, decided once.
|
|
4
|
+
*
|
|
5
|
+
* This branch — scheme test, router `Link` or plain `<a>`, `rel` on the way out
|
|
6
|
+
* — was written three times: Card, TypographyLink, and (by omission) every call
|
|
7
|
+
* site that reached for `render={<a href="…" />}` because the component it was
|
|
8
|
+
* calling had no `href` of its own. The last of those is the expensive copy: it
|
|
9
|
+
* looks like a styling escape hatch and is actually a routing decision, made at
|
|
10
|
+
* the call site, wrongly. A hero CTA written that way full-page-reloads past the
|
|
11
|
+
* router and drops the view transition, and nothing in the type system says so.
|
|
12
|
+
*
|
|
13
|
+
* So components take `href`, not an anchor. `render` stays for what it is for:
|
|
14
|
+
* an element that is genuinely not an anchor.
|
|
15
|
+
*/
|
|
16
|
+
/** A scheme (`mailto:`, `https:`) means the href leaves the app entirely. */
|
|
17
|
+
export function isExternalHref(href) {
|
|
18
|
+
return /^[a-z][a-z0-9+.-]*:/i.test(href);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* What a wrong `href` is worth saying out loud.
|
|
22
|
+
*
|
|
23
|
+
* A value exported from a `"use client"` module and imported by a server
|
|
24
|
+
* component arrives as a boundary stub — a function that throws when called —
|
|
25
|
+
* rather than the string it is in the client bundle. Passing one here read as
|
|
26
|
+
* `TypeError: href.startsWith is not a function`, which React reported with an
|
|
27
|
+
* empty stack: no component, no file, and every route in the app failing at once
|
|
28
|
+
* because the offending link sat in a layout.
|
|
29
|
+
*
|
|
30
|
+
* The value is the diagnosis, so it goes in the message. `String()` on that stub
|
|
31
|
+
* prints the "Attempted to call X() from the server" text React put there, which
|
|
32
|
+
* names the export and the boundary in one line.
|
|
33
|
+
*/
|
|
34
|
+
function assertHref(href) {
|
|
35
|
+
if (typeof href === "string")
|
|
36
|
+
return;
|
|
37
|
+
const seen = typeof href === "function"
|
|
38
|
+
? `a function: ${String(href).replace(/\s+/g, " ").slice(0, 160)}`
|
|
39
|
+
: `${typeof href}: ${JSON.stringify(href)}`;
|
|
40
|
+
throw new TypeError(`href must be a string, and this one is ${seen}. A function here is usually ` +
|
|
41
|
+
`a value exported from a "use client" module and imported by a server ` +
|
|
42
|
+
`component, which crosses the boundary as a stub rather than a string. ` +
|
|
43
|
+
`Move the constant to a plain module and import it from both sides.`);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* A same-page hash is the one internal href that stays a plain anchor: routing
|
|
47
|
+
* `#section` through the router asks for a navigation and a view transition to
|
|
48
|
+
* reach a place the browser can already scroll to.
|
|
49
|
+
*/
|
|
50
|
+
export function resolveLink(href, { external, newTab } = {}) {
|
|
51
|
+
assertHref(href);
|
|
52
|
+
const leavesApp = external ?? isExternalHref(href);
|
|
53
|
+
const away = leavesApp && (newTab ?? href.startsWith("http"));
|
|
54
|
+
const inPage = !leavesApp && href.startsWith("#");
|
|
55
|
+
return {
|
|
56
|
+
Component: leavesApp || inPage ? "a" : Link,
|
|
57
|
+
props: {
|
|
58
|
+
href,
|
|
59
|
+
...(away ? { target: "_blank", rel: "noopener noreferrer" } : {}),
|
|
60
|
+
},
|
|
61
|
+
external: leavesApp,
|
|
62
|
+
};
|
|
63
|
+
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
export { cn } from "./cn.js";
|
|
2
|
+
// The semantic colour vocabulary. Exported from the root because typography
|
|
3
|
+
// takes it too: a link has a tone, drawn from the same seven a button has.
|
|
4
|
+
// `toneClass` only: the raw table and its derived half used to ship separately,
|
|
5
|
+
// and the order they were combined in was load-bearing.
|
|
6
|
+
export { toneClass, impliedTone, INK_ON_FILL, inkOnSurface, } from "./tone.js";
|
|
7
|
+
// Where an href goes, for the rare call site that styles someone else's element
|
|
8
|
+
// and cannot render a Card/Button/TypographyLink — the same pairing with
|
|
9
|
+
// `buttonVariants`. Prefer passing `href` to a component over calling this.
|
|
10
|
+
export { resolveLink, isExternalHref, } from "./href.js";
|
|
2
11
|
export * from "./typography/index.js";
|
|
3
12
|
// NOTE: blocks, the MDX map, and the Shiki plugin are all deliberately absent
|
|
4
13
|
// from this barrel.
|
package/dist/mdx.d.ts
CHANGED
|
@@ -1,9 +1,30 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ComponentProps, type ReactNode } from "react";
|
|
2
2
|
import { Disclosure, DisclosureGroup } from "./blocks/accordion.js";
|
|
3
3
|
import { Callout } from "./blocks/callout.js";
|
|
4
4
|
import { Card, Cards } from "./blocks/card.js";
|
|
5
5
|
import { Step, Steps } from "./blocks/steps.js";
|
|
6
|
-
|
|
6
|
+
/**
|
|
7
|
+
* `<Tabs items={["npm","pnpm"]}>` with a `<Tab>` per panel: the shape an MDX author
|
|
8
|
+
* writes, and the only place in the package that speaks it.
|
|
9
|
+
*
|
|
10
|
+
* Children pair with `items` **by position**, since a caller writing markdown has no
|
|
11
|
+
* value to bind. `value` on a `<Tab>` is for the author's eye and is not matched —
|
|
12
|
+
* matching it would silently drop a panel the moment a label was edited. The label is
|
|
13
|
+
* the value here, so the strip still survives a reorder.
|
|
14
|
+
*
|
|
15
|
+
* This lived in `TabGroup` and made `TabGroup` unusable everywhere else: an app has data,
|
|
16
|
+
* not positional children, so every one of them rebuilt the adapter by hand. Positional
|
|
17
|
+
* children are an authoring convenience, not a component API, so they stop here.
|
|
18
|
+
*/
|
|
19
|
+
declare function MdxTabs({ items, children }: {
|
|
20
|
+
items: string[];
|
|
21
|
+
children: ReactNode;
|
|
22
|
+
}): import("react").JSX.Element;
|
|
23
|
+
/** `value` names the panel at the call site; it is not used for matching. */
|
|
24
|
+
declare function MdxTab({ children }: {
|
|
25
|
+
value?: string;
|
|
26
|
+
children: ReactNode;
|
|
27
|
+
}): import("react").JSX.Element;
|
|
7
28
|
/**
|
|
8
29
|
* Rendered from markdown syntax, so there is no call site and no knob — a knob
|
|
9
30
|
* here is one every project turns differently. Headings carry variants and this
|
|
@@ -19,8 +40,8 @@ export declare const proseMdxComponents: {
|
|
|
19
40
|
Accordions: typeof DisclosureGroup;
|
|
20
41
|
Accordion: typeof Disclosure;
|
|
21
42
|
Banner: (props: ComponentProps<typeof Callout>) => import("react").JSX.Element;
|
|
22
|
-
Tabs: typeof
|
|
23
|
-
Tab: typeof
|
|
43
|
+
Tabs: typeof MdxTabs;
|
|
44
|
+
Tab: typeof MdxTab;
|
|
24
45
|
Steps: typeof Steps;
|
|
25
46
|
Step: typeof Step;
|
|
26
47
|
h1: (props: ComponentProps<"h1">) => import("react").JSX.Element;
|
|
@@ -45,3 +66,4 @@ export declare const proseMdxComponents: {
|
|
|
45
66
|
th: (props: ComponentProps<"th">) => import("react").JSX.Element;
|
|
46
67
|
td: (props: ComponentProps<"td">) => import("react").JSX.Element;
|
|
47
68
|
};
|
|
69
|
+
export {};
|
package/dist/mdx.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Children } from "react";
|
|
2
3
|
import { TypographyH1, TypographyH2, TypographyH3, TypographyH4, } from "./typography/header.js";
|
|
3
4
|
import { cn } from "./cn.js";
|
|
4
5
|
import Image from "next/image";
|
|
@@ -6,8 +7,33 @@ import { Disclosure, DisclosureGroup } from "./blocks/accordion.js";
|
|
|
6
7
|
import { Callout } from "./blocks/callout.js";
|
|
7
8
|
import { Card, Cards } from "./blocks/card.js";
|
|
8
9
|
import { Step, Steps } from "./blocks/steps.js";
|
|
9
|
-
import { TabGroup
|
|
10
|
+
import { TabGroup } from "./blocks/tabs.js";
|
|
10
11
|
import { TypographyProse, TypographyProseList, TypographyLink, } from "./typography/paragraph.js";
|
|
12
|
+
/**
|
|
13
|
+
* `<Tabs items={["npm","pnpm"]}>` with a `<Tab>` per panel: the shape an MDX author
|
|
14
|
+
* writes, and the only place in the package that speaks it.
|
|
15
|
+
*
|
|
16
|
+
* Children pair with `items` **by position**, since a caller writing markdown has no
|
|
17
|
+
* value to bind. `value` on a `<Tab>` is for the author's eye and is not matched —
|
|
18
|
+
* matching it would silently drop a panel the moment a label was edited. The label is
|
|
19
|
+
* the value here, so the strip still survives a reorder.
|
|
20
|
+
*
|
|
21
|
+
* This lived in `TabGroup` and made `TabGroup` unusable everywhere else: an app has data,
|
|
22
|
+
* not positional children, so every one of them rebuilt the adapter by hand. Positional
|
|
23
|
+
* children are an authoring convenience, not a component API, so they stop here.
|
|
24
|
+
*/
|
|
25
|
+
function MdxTabs({ items, children }) {
|
|
26
|
+
const panels = Children.toArray(children);
|
|
27
|
+
return (_jsx(TabGroup, { tabs: items.map((label, i) => ({
|
|
28
|
+
value: label,
|
|
29
|
+
label,
|
|
30
|
+
content: panels[i],
|
|
31
|
+
})) }));
|
|
32
|
+
}
|
|
33
|
+
/** `value` names the panel at the call site; it is not used for matching. */
|
|
34
|
+
function MdxTab({ children }) {
|
|
35
|
+
return _jsx(_Fragment, { children: children });
|
|
36
|
+
}
|
|
11
37
|
/**
|
|
12
38
|
* Rendered from markdown syntax, so there is no call site and no knob — a knob
|
|
13
39
|
* here is one every project turns differently. Headings carry variants and this
|
|
@@ -26,8 +52,8 @@ export const proseMdxComponents = {
|
|
|
26
52
|
Accordions: DisclosureGroup,
|
|
27
53
|
Accordion: Disclosure,
|
|
28
54
|
Banner: (props) => (_jsx(Callout, { density: "editorial", ...props })),
|
|
29
|
-
Tabs:
|
|
30
|
-
Tab,
|
|
55
|
+
Tabs: MdxTabs,
|
|
56
|
+
Tab: MdxTab,
|
|
31
57
|
Steps,
|
|
32
58
|
Step,
|
|
33
59
|
h1: (props) => (_jsx(TypographyH1, { variant: "display", ...props })),
|
package/dist/tone.d.ts
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One semantic colour vocabulary, for every component that carries meaning in a
|
|
3
|
+
* hue: Button, Callout, TypographyLink. Before this there were three lists —
|
|
4
|
+
* Callout's `muted`, TypographyLink's `foreground` and a button's `default` were
|
|
5
|
+
* three spellings of "no meaning at all", and `accent` named `--accent` on a
|
|
6
|
+
* button and `--primary` in a callout.
|
|
7
|
+
*
|
|
8
|
+
* A component that wants a tone does not get to invent a name for one.
|
|
9
|
+
*
|
|
10
|
+
* Seven tones, seven tokens, one to one. That is the test for admitting a new
|
|
11
|
+
* one: `muted` is `--muted`, `warn` is `--warn`, and a proposed tone with no
|
|
12
|
+
* token of its own is a second name for a tone that already has one. It is what
|
|
13
|
+
* ruled out `neutral` (no such token, and `muted` is the word the rest of the
|
|
14
|
+
* package already uses — `--muted-foreground`, `TypographyMuted`,
|
|
15
|
+
* `TypographyP tone="muted"`), `accent` (that is `--primary`'s hover tint, so a
|
|
16
|
+
* washed `primary` renders the same control), and `info` (a real token, but
|
|
17
|
+
* `success | warn | destructive` is already the complete good/careful/bad
|
|
18
|
+
* triad, and nothing in either app had ever reached for a fourth).
|
|
19
|
+
*
|
|
20
|
+
* The rule the package already applies to every tinted role holds here too, and
|
|
21
|
+
* is why each row names three values rather than one: `--tone-fill` is a surface,
|
|
22
|
+
* `--tone-ink` is the label printed on it, and `--tone-hue` is the same colour
|
|
23
|
+
* used as words. A fill is a mark and clears 3:1; an ink is read and clears
|
|
24
|
+
* 4.5:1. Tuning one value to do both jobs sinks whichever job it was not tuned
|
|
25
|
+
* for. `checkSignals` in /contrast is the test.
|
|
26
|
+
*
|
|
27
|
+
* Custom properties rather than thirty-five class strings: a tone declares, a
|
|
28
|
+
* component spends. `Button` has five variants and `Callout` two densities, and
|
|
29
|
+
* neither has a per-tone branch anywhere in it.
|
|
30
|
+
*/
|
|
31
|
+
/**
|
|
32
|
+
* `muted` is the only row that names a fourth value. A hairline derived from the
|
|
33
|
+
* ink at 45% is right for a hue and wrong for the absence of one. `--border` is
|
|
34
|
+
* the tuned answer there, in place of a wash of `--foreground`.
|
|
35
|
+
*
|
|
36
|
+
* Its hue is `--foreground` rather than `--muted-foreground` on purpose. `muted`
|
|
37
|
+
* says the control carries no meaning, not that it carries less contrast — a
|
|
38
|
+
* cancel button beside a save button is quiet because it is not filled in, and
|
|
39
|
+
* its label still has to be read.
|
|
40
|
+
*
|
|
41
|
+
* `brand` is the consumer's own identity, and the only row whose token the
|
|
42
|
+
* package does not define. It falls back to `--primary`, so an app that has no
|
|
43
|
+
* identity hue of its own gets its principal one — the tone stays meaningful
|
|
44
|
+
* everywhere rather than rendering nothing in the apps that skipped it.
|
|
45
|
+
*/
|
|
46
|
+
export declare const TONE: {
|
|
47
|
+
/** No meaning: chrome, toolbars, anything that must not compete. */
|
|
48
|
+
readonly muted: "[--tone-fill:var(--muted)] [--tone-ink:var(--foreground)] [--tone-hue:var(--foreground)] [--tone-line:var(--border)]";
|
|
49
|
+
/** The principal action, and the package's default wherever a tone is optional. */
|
|
50
|
+
readonly primary: "[--tone-fill:var(--primary)] [--tone-ink:var(--primary-foreground)] [--tone-hue:var(--primary-ink,var(--primary))]";
|
|
51
|
+
/** The warm accent. `--secondary-ink` is its readable cut; the fill is not. */
|
|
52
|
+
readonly secondary: "[--tone-fill:var(--secondary)] [--tone-ink:var(--secondary-foreground)] [--tone-hue:var(--secondary-ink)]";
|
|
53
|
+
/** The consumer's identity hue, if it defined one. Otherwise the principal one. */
|
|
54
|
+
readonly brand: "[--tone-fill:var(--brand,var(--primary))] [--tone-ink:var(--brand-foreground,var(--primary-foreground))] [--tone-hue:var(--brand-ink,var(--primary-ink,var(--primary)))]";
|
|
55
|
+
/** It worked. */
|
|
56
|
+
readonly success: "[--tone-fill:var(--success)] [--tone-ink:var(--success-foreground)] [--tone-hue:var(--success-ink)]";
|
|
57
|
+
/** A footgun: the reader can still proceed, but not blindly. */
|
|
58
|
+
readonly warn: "[--tone-fill:var(--warn)] [--tone-ink:var(--warn-foreground)] [--tone-hue:var(--warn-ink)]";
|
|
59
|
+
/** It deletes something, or it already failed. */
|
|
60
|
+
readonly destructive: "[--tone-fill:var(--destructive)] [--tone-ink:var(--destructive-foreground)] [--tone-hue:var(--destructive)]";
|
|
61
|
+
};
|
|
62
|
+
export type Tone = keyof typeof TONE;
|
|
63
|
+
/**
|
|
64
|
+
* What every tone is worth once mixed, derived once here rather than eight times
|
|
65
|
+
* above. Declared before `TONE` in any class list, so `neutral` can override
|
|
66
|
+
* `--tone-line`.
|
|
67
|
+
*
|
|
68
|
+
* Expressed as variables rather than through Tailwind's `/10` modifier because
|
|
69
|
+
* the modifier's support for arbitrary custom properties is a moving target,
|
|
70
|
+
* while `color-mix` is what the modifier compiles to anyway — the same CSS, one
|
|
71
|
+
* layer less of trust.
|
|
72
|
+
*
|
|
73
|
+
* A hover moves the fill 18% toward `--hover-toward`, the extreme theme.css
|
|
74
|
+
* points away from the page: black on latte, white on espresso. The token
|
|
75
|
+
* carries the direction and the percentage carries the state. Mixing toward
|
|
76
|
+
* `--foreground` instead made the step as long as the gap between the fill and
|
|
77
|
+
* the ink, so a `primary` button moved 4.9 ΔL* on latte against 6.2 on espresso
|
|
78
|
+
* and read as no hover at all. Every tone now clears 6 ΔL* in both themes,
|
|
79
|
+
* measured in test/composition.test.ts. A `dark:` override here is what the
|
|
80
|
+
* package's own ESLint rule exists to prevent.
|
|
81
|
+
*/
|
|
82
|
+
export declare const TONE_SURFACE: string;
|
|
83
|
+
/**
|
|
84
|
+
* A tone, as one class list. This is the only thing a component should need.
|
|
85
|
+
*
|
|
86
|
+
* It was two — `cn(TONE_SURFACE, TONE[tone])` — and the order was load-bearing
|
|
87
|
+
* in a way nothing enforced: the derived values have to be declared first so
|
|
88
|
+
* `muted` can override `--tone-line` with `--border`. Getting that backwards
|
|
89
|
+
* gave every quiet control a hairline washed out of its own ink, and the only
|
|
90
|
+
* thing standing between a caller and that bug was a comment. Two arguments
|
|
91
|
+
* whose order matters and whose values always travel together is one argument.
|
|
92
|
+
*/
|
|
93
|
+
export declare const toneClass: (tone: Tone) => string;
|
|
94
|
+
/**
|
|
95
|
+
* The ink a nested element inherits, declared by whatever painted the surface
|
|
96
|
+
* under it. Two properties, one rule: paint a background, hand down its ink.
|
|
97
|
+
*
|
|
98
|
+
* `toneClass` alone is a palette, not a surface — a `Callout` spends the same
|
|
99
|
+
* seven values as a filled `Button` and tints at 5%, so the words inside it
|
|
100
|
+
* still sit on the page and still want the page's ink. Only a component that
|
|
101
|
+
* actually fills promotes `--tone-ink` to the inherited ink, and the type
|
|
102
|
+
* primitives read it with the page as their fallback. A tint that promotes
|
|
103
|
+
* nothing is therefore correct by default, which is the failure this replaces:
|
|
104
|
+
* `TypographyLabel` pinned `text-foreground`, won over the `text-primary-foreground`
|
|
105
|
+
* on the anchor around it, and printed 2.34:1 on a filled button.
|
|
106
|
+
*
|
|
107
|
+
* `--ink-muted` collapses to the ink itself, because a hue fill has no second
|
|
108
|
+
* rung: mixing the ink 20% toward `--primary` measures 4.22:1 on the
|
|
109
|
+
* espresso theme and 3.49:1 at 30%. Nothing on a filled control may be quieter
|
|
110
|
+
* than its label. Wanting two rungs is wanting a tinted surface.
|
|
111
|
+
*/
|
|
112
|
+
export declare const INK_ON_FILL = "[--ink:var(--tone-ink)] [--ink-muted:var(--tone-ink)]";
|
|
113
|
+
/**
|
|
114
|
+
* The same contract for a surface the tones do not name: `--card`, `--popover`,
|
|
115
|
+
* a sidebar. These are tints of the page rather than hues, so both rungs
|
|
116
|
+
* survive and the pair is stated rather than collapsed.
|
|
117
|
+
*/
|
|
118
|
+
export declare const inkOnSurface: (ink: string, muted?: string) => string;
|
|
119
|
+
/**
|
|
120
|
+
* What an unstated tone means, given how much ink the component is spending.
|
|
121
|
+
* Shared, because `Button` and `Badge` both need it and two copies of a default
|
|
122
|
+
* is how two components come to disagree about what saying nothing means.
|
|
123
|
+
*
|
|
124
|
+
* Filling a control in is how a page says *this is the action*, so a filled one
|
|
125
|
+
* with nothing else stated is the principal one; anything less is chrome until a
|
|
126
|
+
* call site says otherwise. Measured, not assumed: 244 of the 246 buttons across
|
|
127
|
+
* both apps that name a variant name `outline` or `ghost`, and every one wants
|
|
128
|
+
* the page's own ink.
|
|
129
|
+
*/
|
|
130
|
+
export declare const impliedTone: (variant: string | null | undefined) => Tone;
|
package/dist/tone.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One semantic colour vocabulary, for every component that carries meaning in a
|
|
3
|
+
* hue: Button, Callout, TypographyLink. Before this there were three lists —
|
|
4
|
+
* Callout's `muted`, TypographyLink's `foreground` and a button's `default` were
|
|
5
|
+
* three spellings of "no meaning at all", and `accent` named `--accent` on a
|
|
6
|
+
* button and `--primary` in a callout.
|
|
7
|
+
*
|
|
8
|
+
* A component that wants a tone does not get to invent a name for one.
|
|
9
|
+
*
|
|
10
|
+
* Seven tones, seven tokens, one to one. That is the test for admitting a new
|
|
11
|
+
* one: `muted` is `--muted`, `warn` is `--warn`, and a proposed tone with no
|
|
12
|
+
* token of its own is a second name for a tone that already has one. It is what
|
|
13
|
+
* ruled out `neutral` (no such token, and `muted` is the word the rest of the
|
|
14
|
+
* package already uses — `--muted-foreground`, `TypographyMuted`,
|
|
15
|
+
* `TypographyP tone="muted"`), `accent` (that is `--primary`'s hover tint, so a
|
|
16
|
+
* washed `primary` renders the same control), and `info` (a real token, but
|
|
17
|
+
* `success | warn | destructive` is already the complete good/careful/bad
|
|
18
|
+
* triad, and nothing in either app had ever reached for a fourth).
|
|
19
|
+
*
|
|
20
|
+
* The rule the package already applies to every tinted role holds here too, and
|
|
21
|
+
* is why each row names three values rather than one: `--tone-fill` is a surface,
|
|
22
|
+
* `--tone-ink` is the label printed on it, and `--tone-hue` is the same colour
|
|
23
|
+
* used as words. A fill is a mark and clears 3:1; an ink is read and clears
|
|
24
|
+
* 4.5:1. Tuning one value to do both jobs sinks whichever job it was not tuned
|
|
25
|
+
* for. `checkSignals` in /contrast is the test.
|
|
26
|
+
*
|
|
27
|
+
* Custom properties rather than thirty-five class strings: a tone declares, a
|
|
28
|
+
* component spends. `Button` has five variants and `Callout` two densities, and
|
|
29
|
+
* neither has a per-tone branch anywhere in it.
|
|
30
|
+
*/
|
|
31
|
+
/**
|
|
32
|
+
* `muted` is the only row that names a fourth value. A hairline derived from the
|
|
33
|
+
* ink at 45% is right for a hue and wrong for the absence of one. `--border` is
|
|
34
|
+
* the tuned answer there, in place of a wash of `--foreground`.
|
|
35
|
+
*
|
|
36
|
+
* Its hue is `--foreground` rather than `--muted-foreground` on purpose. `muted`
|
|
37
|
+
* says the control carries no meaning, not that it carries less contrast — a
|
|
38
|
+
* cancel button beside a save button is quiet because it is not filled in, and
|
|
39
|
+
* its label still has to be read.
|
|
40
|
+
*
|
|
41
|
+
* `brand` is the consumer's own identity, and the only row whose token the
|
|
42
|
+
* package does not define. It falls back to `--primary`, so an app that has no
|
|
43
|
+
* identity hue of its own gets its principal one — the tone stays meaningful
|
|
44
|
+
* everywhere rather than rendering nothing in the apps that skipped it.
|
|
45
|
+
*/
|
|
46
|
+
export const TONE = {
|
|
47
|
+
/** No meaning: chrome, toolbars, anything that must not compete. */
|
|
48
|
+
muted: "[--tone-fill:var(--muted)] [--tone-ink:var(--foreground)] [--tone-hue:var(--foreground)] [--tone-line:var(--border)]",
|
|
49
|
+
/** The principal action, and the package's default wherever a tone is optional. */
|
|
50
|
+
primary: "[--tone-fill:var(--primary)] [--tone-ink:var(--primary-foreground)] [--tone-hue:var(--primary-ink,var(--primary))]",
|
|
51
|
+
/** The warm accent. `--secondary-ink` is its readable cut; the fill is not. */
|
|
52
|
+
secondary: "[--tone-fill:var(--secondary)] [--tone-ink:var(--secondary-foreground)] [--tone-hue:var(--secondary-ink)]",
|
|
53
|
+
/** The consumer's identity hue, if it defined one. Otherwise the principal one. */
|
|
54
|
+
brand: "[--tone-fill:var(--brand,var(--primary))] [--tone-ink:var(--brand-foreground,var(--primary-foreground))] [--tone-hue:var(--brand-ink,var(--primary-ink,var(--primary)))]",
|
|
55
|
+
/** It worked. */
|
|
56
|
+
success: "[--tone-fill:var(--success)] [--tone-ink:var(--success-foreground)] [--tone-hue:var(--success-ink)]",
|
|
57
|
+
/** A footgun: the reader can still proceed, but not blindly. */
|
|
58
|
+
warn: "[--tone-fill:var(--warn)] [--tone-ink:var(--warn-foreground)] [--tone-hue:var(--warn-ink)]",
|
|
59
|
+
/** It deletes something, or it already failed. */
|
|
60
|
+
destructive: "[--tone-fill:var(--destructive)] [--tone-ink:var(--destructive-foreground)] [--tone-hue:var(--destructive)]",
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* What every tone is worth once mixed, derived once here rather than eight times
|
|
64
|
+
* above. Declared before `TONE` in any class list, so `neutral` can override
|
|
65
|
+
* `--tone-line`.
|
|
66
|
+
*
|
|
67
|
+
* Expressed as variables rather than through Tailwind's `/10` modifier because
|
|
68
|
+
* the modifier's support for arbitrary custom properties is a moving target,
|
|
69
|
+
* while `color-mix` is what the modifier compiles to anyway — the same CSS, one
|
|
70
|
+
* layer less of trust.
|
|
71
|
+
*
|
|
72
|
+
* A hover moves the fill 18% toward `--hover-toward`, the extreme theme.css
|
|
73
|
+
* points away from the page: black on latte, white on espresso. The token
|
|
74
|
+
* carries the direction and the percentage carries the state. Mixing toward
|
|
75
|
+
* `--foreground` instead made the step as long as the gap between the fill and
|
|
76
|
+
* the ink, so a `primary` button moved 4.9 ΔL* on latte against 6.2 on espresso
|
|
77
|
+
* and read as no hover at all. Every tone now clears 6 ΔL* in both themes,
|
|
78
|
+
* measured in test/composition.test.ts. A `dark:` override here is what the
|
|
79
|
+
* package's own ESLint rule exists to prevent.
|
|
80
|
+
*/
|
|
81
|
+
export const TONE_SURFACE = [
|
|
82
|
+
/** A hairline, or a rule carrying the tone. */
|
|
83
|
+
"[--tone-line:color-mix(in_oklab,var(--tone-hue)_45%,transparent)]",
|
|
84
|
+
/** A panel's tint: large area, so barely there. */
|
|
85
|
+
"[--tone-veil:color-mix(in_oklab,var(--tone-hue)_5%,transparent)]",
|
|
86
|
+
/** A control's tint at rest. */
|
|
87
|
+
"[--tone-wash:color-mix(in_oklab,var(--tone-hue)_10%,transparent)]",
|
|
88
|
+
/** The same control under the pointer: twice the tint, so the step reads. */
|
|
89
|
+
"[--tone-wash-hover:color-mix(in_oklab,var(--tone-hue)_20%,transparent)]",
|
|
90
|
+
/** A filled control under the pointer. */
|
|
91
|
+
"[--tone-fill-hover:color-mix(in_oklab,var(--tone-fill),var(--hover-toward)_18%)]",
|
|
92
|
+
].join(" ");
|
|
93
|
+
/**
|
|
94
|
+
* A tone, as one class list. This is the only thing a component should need.
|
|
95
|
+
*
|
|
96
|
+
* It was two — `cn(TONE_SURFACE, TONE[tone])` — and the order was load-bearing
|
|
97
|
+
* in a way nothing enforced: the derived values have to be declared first so
|
|
98
|
+
* `muted` can override `--tone-line` with `--border`. Getting that backwards
|
|
99
|
+
* gave every quiet control a hairline washed out of its own ink, and the only
|
|
100
|
+
* thing standing between a caller and that bug was a comment. Two arguments
|
|
101
|
+
* whose order matters and whose values always travel together is one argument.
|
|
102
|
+
*/
|
|
103
|
+
export const toneClass = (tone) => `${TONE_SURFACE} ${TONE[tone]}`;
|
|
104
|
+
/**
|
|
105
|
+
* The ink a nested element inherits, declared by whatever painted the surface
|
|
106
|
+
* under it. Two properties, one rule: paint a background, hand down its ink.
|
|
107
|
+
*
|
|
108
|
+
* `toneClass` alone is a palette, not a surface — a `Callout` spends the same
|
|
109
|
+
* seven values as a filled `Button` and tints at 5%, so the words inside it
|
|
110
|
+
* still sit on the page and still want the page's ink. Only a component that
|
|
111
|
+
* actually fills promotes `--tone-ink` to the inherited ink, and the type
|
|
112
|
+
* primitives read it with the page as their fallback. A tint that promotes
|
|
113
|
+
* nothing is therefore correct by default, which is the failure this replaces:
|
|
114
|
+
* `TypographyLabel` pinned `text-foreground`, won over the `text-primary-foreground`
|
|
115
|
+
* on the anchor around it, and printed 2.34:1 on a filled button.
|
|
116
|
+
*
|
|
117
|
+
* `--ink-muted` collapses to the ink itself, because a hue fill has no second
|
|
118
|
+
* rung: mixing the ink 20% toward `--primary` measures 4.22:1 on the
|
|
119
|
+
* espresso theme and 3.49:1 at 30%. Nothing on a filled control may be quieter
|
|
120
|
+
* than its label. Wanting two rungs is wanting a tinted surface.
|
|
121
|
+
*/
|
|
122
|
+
export const INK_ON_FILL = "[--ink:var(--tone-ink)] [--ink-muted:var(--tone-ink)]";
|
|
123
|
+
/**
|
|
124
|
+
* The same contract for a surface the tones do not name: `--card`, `--popover`,
|
|
125
|
+
* a sidebar. These are tints of the page rather than hues, so both rungs
|
|
126
|
+
* survive and the pair is stated rather than collapsed.
|
|
127
|
+
*/
|
|
128
|
+
export const inkOnSurface = (ink, muted = "--muted-foreground") => `[--ink:var(${ink})] [--ink-muted:var(${muted})]`;
|
|
129
|
+
/**
|
|
130
|
+
* What an unstated tone means, given how much ink the component is spending.
|
|
131
|
+
* Shared, because `Button` and `Badge` both need it and two copies of a default
|
|
132
|
+
* is how two components come to disagree about what saying nothing means.
|
|
133
|
+
*
|
|
134
|
+
* Filling a control in is how a page says *this is the action*, so a filled one
|
|
135
|
+
* with nothing else stated is the principal one; anything less is chrome until a
|
|
136
|
+
* call site says otherwise. Measured, not assumed: 244 of the 246 buttons across
|
|
137
|
+
* both apps that name a variant name `outline` or `ghost`, and every one wants
|
|
138
|
+
* the page's own ink.
|
|
139
|
+
*/
|
|
140
|
+
export const impliedTone = (variant) => variant == null || variant === "solid" ? "primary" : "muted";
|