@hackcove/ui-react 0.10.1 → 0.10.3

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.
@@ -0,0 +1,42 @@
1
+ import { splitLines } from "./highlightKit.js";
2
+ const CONSOLE_PROMPT = "$ ";
3
+ /**
4
+ * The lines of a console session, marked: a line that starts with `$ ` is a command (`prompt`), and
5
+ * so is every line after a command line that ends with `\\`, which continues it. The rest is output.
6
+ */
7
+ function consoleLines(code) {
8
+ let continued = false;
9
+ return splitLines(code).map(({ body, eol }) => {
10
+ const prompt = !continued && body.startsWith(CONSOLE_PROMPT);
11
+ const command = prompt || continued;
12
+ continued = command && body.endsWith("\\");
13
+ return { body, eol, command, prompt };
14
+ });
15
+ }
16
+ /** A shell session: `$ ` is the prompt, the command after it is bash, other lines are output. */
17
+ export function scanConsole(code, emit, bash) {
18
+ for (const { body, eol, command, prompt } of consoleLines(code)) {
19
+ if (prompt)
20
+ emit("prompt", CONSOLE_PROMPT);
21
+ const text = prompt ? body.slice(CONSOLE_PROMPT.length) : body;
22
+ if (command)
23
+ bash(text, emit);
24
+ else
25
+ emit("plain", text);
26
+ emit("plain", eol);
27
+ }
28
+ }
29
+ /**
30
+ * What a console block's copy button copies: each command without its `$ `, plus the lines that
31
+ * continue it as written (the trailing `\\` and the indentation kept, so the paste runs), joined by
32
+ * newlines. Output is left out. A block with no `$ ` line has no commands, so it is copied as given.
33
+ */
34
+ export function consoleCommands(code) {
35
+ const lines = consoleLines(code);
36
+ if (!lines.some((line) => line.prompt))
37
+ return code;
38
+ return lines
39
+ .filter((line) => line.command)
40
+ .map((line) => (line.prompt ? line.body.slice(CONSOLE_PROMPT.length) : line.body))
41
+ .join("\n");
42
+ }
@@ -0,0 +1,7 @@
1
+ import type { Emit, Scanner } from "./highlightKit.js";
2
+ /**
3
+ * HTML: comments, the doctype, tags with their attributes (a start tag may span lines), and text,
4
+ * which is plain. The content of `<script>` and `<style>` runs to the first closing tag of that
5
+ * name, wherever it falls, and is scanned with `script` (JS) or `style` (CSS).
6
+ */
7
+ export declare function scanHtml(code: string, emit: Emit, script: Scanner, style: Scanner): void;
@@ -0,0 +1,132 @@
1
+ import { sticky } from "./highlightKit.js";
2
+ const OPEN = /<(\/?)([A-Za-z][\w:-]*)/y;
3
+ const DOCTYPE = /<!doctype\b[^>]*>?/iy;
4
+ const SPACE = /\s+/y;
5
+ const ATTR = /[^\s"'<>/=]+/y;
6
+ const UNQUOTED = /[^\s>"'=<`]+/y;
7
+ /** `>` or `/>` where the tag ends. */
8
+ function tagEnd(code, at) {
9
+ if (code.startsWith("/>", at))
10
+ return "/>";
11
+ return code.charAt(at) === ">" ? ">" : undefined;
12
+ }
13
+ /** The value after an `=`: a quoted string (which may span lines) or a bare run, which stays plain. */
14
+ function takeValue(scan) {
15
+ const { code, at } = scan;
16
+ const c = code.charAt(at);
17
+ if (c === '"' || c === "'") {
18
+ const close = code.indexOf(c, at + 1);
19
+ const end = close === -1 ? code.length : close + 1;
20
+ scan.emit("string", code.slice(at, end));
21
+ scan.at = end;
22
+ return true;
23
+ }
24
+ const bare = sticky(UNQUOTED, code, at);
25
+ if (bare === undefined)
26
+ return false;
27
+ scan.emit("plain", bare);
28
+ scan.at += bare.length;
29
+ return true;
30
+ }
31
+ /** One piece of a tag's insides: space, `=`, a value, an attribute name, or a stray character. */
32
+ function tagStep(scan) {
33
+ const { code, at } = scan;
34
+ const space = sticky(SPACE, code, at);
35
+ if (space !== undefined) {
36
+ scan.emit("plain", space);
37
+ scan.at += space.length;
38
+ return;
39
+ }
40
+ if (code.charAt(at) === "=") {
41
+ scan.emit("plain", "=");
42
+ scan.afterEquals = true;
43
+ scan.at += 1;
44
+ return;
45
+ }
46
+ const wasAfterEquals = scan.afterEquals;
47
+ scan.afterEquals = false;
48
+ if (wasAfterEquals && takeValue(scan))
49
+ return;
50
+ const name = sticky(ATTR, code, at);
51
+ scan.emit(name === undefined ? "plain" : "attr", name ?? code.charAt(at));
52
+ scan.at += name?.length ?? 1;
53
+ }
54
+ /** The rest of a tag from just past its name; says where it ended and how. */
55
+ function scanTag(code, from, emit) {
56
+ const scan = { code, emit, at: from, afterEquals: false };
57
+ while (scan.at < code.length) {
58
+ const end = tagEnd(code, scan.at);
59
+ if (end !== undefined) {
60
+ emit("plain", end);
61
+ return { end: scan.at + end.length, closed: true, selfClosed: end === "/>" };
62
+ }
63
+ tagStep(scan);
64
+ }
65
+ return { end: code.length, closed: false, selfClosed: false };
66
+ }
67
+ /** A comment (which may span lines) or a doctype at `at`: where it ends and its colour. */
68
+ function special(code, at) {
69
+ if (code.startsWith("<!--", at)) {
70
+ const close = code.indexOf("-->", at + 4);
71
+ return { end: close === -1 ? code.length : close + 3, kind: "comment" };
72
+ }
73
+ const doctype = sticky(DOCTYPE, code, at);
74
+ return doctype === undefined ? undefined : { end: at + doctype.length, kind: "keyword" };
75
+ }
76
+ /** The `<script>` or `<style>` start tag whose content is another language, if this is one. */
77
+ function rawOf(name, tag, embedded) {
78
+ if (!tag.closed || tag.selfClosed)
79
+ return undefined;
80
+ if (name === "script")
81
+ return { name, scan: embedded.script };
82
+ return name === "style" ? { name, scan: embedded.style } : undefined;
83
+ }
84
+ /** The content of a `<script>` or `<style>`, up to the first closing tag of that name, wherever it falls. */
85
+ function rawContent(code, at, raw, emit) {
86
+ const closing = new RegExp(`</${raw.name}(?=[\\s/>]|$)`, "gi");
87
+ closing.lastIndex = at;
88
+ const end = closing.exec(code)?.index ?? code.length;
89
+ raw.scan(code.slice(at, end), emit);
90
+ return end;
91
+ }
92
+ /** A tag at a `<`, or a lone `<` that opens nothing. Returns the index to carry on from. */
93
+ function scanElement(code, at, emit, embedded) {
94
+ OPEN.lastIndex = at;
95
+ const open = OPEN.exec(code);
96
+ if (open === null) {
97
+ emit("plain", "<");
98
+ return at + 1;
99
+ }
100
+ const [whole, slash = "", tagName = ""] = open;
101
+ emit("plain", `<${slash}`);
102
+ emit("tag", tagName);
103
+ const tag = scanTag(code, at + whole.length, emit);
104
+ const raw = slash === "" ? rawOf(tagName.toLowerCase(), tag, embedded) : undefined;
105
+ return raw === undefined ? tag.end : rawContent(code, tag.end, raw, emit);
106
+ }
107
+ /** Markup at a `<`: a comment, a doctype or a tag. Returns the index to carry on from. */
108
+ function scanMarkup(code, at, emit, embedded) {
109
+ const found = special(code, at);
110
+ if (found === undefined)
111
+ return scanElement(code, at, emit, embedded);
112
+ emit(found.kind, code.slice(at, found.end));
113
+ return found.end;
114
+ }
115
+ /**
116
+ * HTML: comments, the doctype, tags with their attributes (a start tag may span lines), and text,
117
+ * which is plain. The content of `<script>` and `<style>` runs to the first closing tag of that
118
+ * name, wherever it falls, and is scanned with `script` (JS) or `style` (CSS).
119
+ */
120
+ export function scanHtml(code, emit, script, style) {
121
+ let at = 0;
122
+ while (at < code.length) {
123
+ if (code.charAt(at) === "<") {
124
+ at = scanMarkup(code, at, emit, { script, style });
125
+ continue;
126
+ }
127
+ const next = code.indexOf("<", at);
128
+ const end = next === -1 ? code.length : next;
129
+ emit("plain", code.slice(at, end));
130
+ at = end;
131
+ }
132
+ }
@@ -0,0 +1,16 @@
1
+ /** The pieces the scanners in `highlight*.ts` share. */
2
+ export type TokenKind = "plain" | "comment" | "keyword" | "string" | "number" | "tag" | "attr" | "function" | "prompt";
3
+ export type Token = {
4
+ text: string;
5
+ kind: TokenKind;
6
+ };
7
+ export type Emit = (kind: TokenKind, text: string) => void;
8
+ /** A scanner for a whole run of code, given to a scanner of a language that holds another. */
9
+ export type Scanner = (code: string, emit: Emit) => void;
10
+ /** The text a sticky `pattern` matches at `at`, or `undefined`. */
11
+ export declare function sticky(pattern: RegExp, code: string, at: number): string | undefined;
12
+ /** Each line of `code` without its line break, and the break (`\n`, `\r\n` or nothing at the end). */
13
+ export declare function splitLines(code: string): {
14
+ body: string;
15
+ eol: string;
16
+ }[];
@@ -0,0 +1,23 @@
1
+ /** The pieces the scanners in `highlight*.ts` share. */
2
+ /** The text a sticky `pattern` matches at `at`, or `undefined`. */
3
+ export function sticky(pattern, code, at) {
4
+ pattern.lastIndex = at;
5
+ return pattern.exec(code)?.[0];
6
+ }
7
+ /** Each line of `code` without its line break, and the break (`\n`, `\r\n` or nothing at the end). */
8
+ export function splitLines(code) {
9
+ const out = [];
10
+ let at = 0;
11
+ for (;;) {
12
+ const newline = code.indexOf("\n", at);
13
+ const end = newline === -1 ? code.length : newline;
14
+ const bodyEnd = end > at && code.charAt(end - 1) === "\r" ? end - 1 : end;
15
+ out.push({
16
+ body: code.slice(at, bodyEnd),
17
+ eol: code.slice(bodyEnd, newline === -1 ? end : end + 1),
18
+ });
19
+ if (newline === -1)
20
+ return out;
21
+ at = newline + 1;
22
+ }
23
+ }
@@ -0,0 +1,3 @@
1
+ import type { Emit } from "./highlightKit.js";
2
+ /** Line by line. The lines of a block scalar are one plain run: no comment or key is read in them. */
3
+ export declare function scanYaml(code: string, emit: Emit): void;
@@ -0,0 +1,143 @@
1
+ import { splitLines, sticky } from "./highlightKit.js";
2
+ const YAML_MARKER = /-(?: +|$)/y;
3
+ const YAML_KEYWORDS = /^(?:true|false|null|~)$/;
4
+ const YAML_NUMBER = /^-?\d[\d._]*$/;
5
+ const YAML_BLOCK_HEADER = /^[|>](?:[1-9][-+]?|[-+][1-9]?)?$/;
6
+ /**
7
+ * The index past the quoted scalar at `at` in a YAML line, or `undefined`. A single-quoted scalar
8
+ * escapes a quote by doubling it (`'it''s'`), a double-quoted one with a backslash.
9
+ */
10
+ function quotedEnd(line, at) {
11
+ const quote = line.charAt(at);
12
+ if (quote !== '"' && quote !== "'")
13
+ return undefined;
14
+ let i = at + 1;
15
+ while (i < line.length) {
16
+ const c = line.charAt(i);
17
+ if (quote === '"' && c === "\\")
18
+ i += 2;
19
+ else if (c !== quote)
20
+ i += 1;
21
+ else if (quote === "'" && line.charAt(i + 1) === "'")
22
+ i += 2;
23
+ else
24
+ return i + 1;
25
+ }
26
+ return line.length;
27
+ }
28
+ /** What follows a quoted scalar: spaces, then perhaps a comment. */
29
+ function trailing(rest, emit) {
30
+ const comment = /^(\s+)(#.*)$/.exec(rest);
31
+ if (comment === null)
32
+ emit("plain", rest);
33
+ else {
34
+ emit("plain", comment[1] ?? "");
35
+ emit("comment", comment[2] ?? "");
36
+ }
37
+ }
38
+ /** A quoted key: the closing quote must be followed by `:` and a space or the line end. */
39
+ function quotedKey(line, at) {
40
+ const end = quotedEnd(line, at);
41
+ if (end === undefined || end - at < 2 || line.charAt(end - 1) !== line.charAt(at))
42
+ return undefined;
43
+ return /^:(?:\s|$)/.test(line.slice(end)) ? { end, kind: "string" } : undefined;
44
+ }
45
+ /** An unquoted key: the text up to the first `:` that is followed by a space or the line end. */
46
+ function plainKey(line, at) {
47
+ if ("#[{|>".includes(line.charAt(at)))
48
+ return undefined;
49
+ const colon = /:(?:\s|$)/.exec(line.slice(at));
50
+ if (colon === null || colon.index === 0)
51
+ return undefined;
52
+ const text = line.slice(at, at + colon.index);
53
+ if (/(?:^|\s)#/.test(text))
54
+ return undefined;
55
+ return { end: at + text.trimEnd().length, kind: "attr" };
56
+ }
57
+ /** The mapping key at `at`: where its text ends (before the `:`) and its colour. */
58
+ function keyAt(line, at) {
59
+ const first = line.charAt(at);
60
+ if (first === "")
61
+ return undefined;
62
+ return first === '"' || first === "'" ? quotedKey(line, at) : plainKey(line, at);
63
+ }
64
+ /** The colour of an unquoted scalar read whole. */
65
+ function scalarKind(scalar) {
66
+ if (YAML_NUMBER.test(scalar))
67
+ return "number";
68
+ return YAML_KEYWORDS.test(scalar) ? "keyword" : "plain";
69
+ }
70
+ /** A value: a quoted string, or an unquoted scalar up to a comment. True when it opens a block scalar. */
71
+ function emitValue(line, from, emit) {
72
+ const value = line.slice(from);
73
+ if (value === "")
74
+ return false;
75
+ if (value.startsWith("#")) {
76
+ emit("comment", value);
77
+ return false;
78
+ }
79
+ const quoted = quotedEnd(line, from);
80
+ if (quoted !== undefined) {
81
+ emit("string", line.slice(from, quoted));
82
+ trailing(line.slice(quoted), emit);
83
+ return false;
84
+ }
85
+ const comment = /\s#/.exec(value);
86
+ const scalar = line.slice(from, comment === null ? line.length : from + comment.index).trimEnd();
87
+ emit(scalarKind(scalar), scalar);
88
+ const commentAt = comment === null ? line.length : from + comment.index + 1;
89
+ emit("plain", line.slice(from + scalar.length, commentAt));
90
+ emit("comment", line.slice(commentAt));
91
+ return YAML_BLOCK_HEADER.test(scalar);
92
+ }
93
+ /** The `- ` markers from `at`: where the content starts, and the column of the last dash. */
94
+ function emitMarkers(line, at, emit) {
95
+ let column = at;
96
+ let next = at;
97
+ for (;;) {
98
+ const marker = sticky(YAML_MARKER, line, next);
99
+ if (marker === undefined)
100
+ return { at: next, column };
101
+ column = next;
102
+ emit("plain", marker);
103
+ next += marker.length;
104
+ }
105
+ }
106
+ /**
107
+ * One YAML line. Returns the column its block scalar's lines must be indented past, when it ends
108
+ * in `|` or `>`: the key's own column, after any `- ` markers (a key-less item uses its dash's).
109
+ */
110
+ function scanLine(line, emit) {
111
+ const indent = line.length - line.trimStart().length;
112
+ emit("plain", line.slice(0, indent));
113
+ if (line.charAt(indent) === "#") {
114
+ emit("comment", line.slice(indent));
115
+ return undefined;
116
+ }
117
+ const markers = emitMarkers(line, indent, emit);
118
+ let { at, column } = markers;
119
+ const key = keyAt(line, at);
120
+ if (key !== undefined) {
121
+ column = at;
122
+ emit(key.kind, line.slice(at, key.end));
123
+ const colon = line.indexOf(":", key.end);
124
+ emit("plain", line.slice(key.end, colon + 1));
125
+ at = colon + 1;
126
+ }
127
+ const gap = line.length - at - line.slice(at).trimStart().length;
128
+ emit("plain", line.slice(at, at + gap));
129
+ return emitValue(line, at + gap, emit) ? column : undefined;
130
+ }
131
+ /** Line by line. The lines of a block scalar are one plain run: no comment or key is read in them. */
132
+ export function scanYaml(code, emit) {
133
+ let block;
134
+ for (const { body, eol } of splitLines(code)) {
135
+ const indent = body.length - body.trimStart().length;
136
+ if (block !== undefined && (body.trim() === "" || indent > block)) {
137
+ emit("plain", body + eol);
138
+ continue;
139
+ }
140
+ block = scanLine(body, emit);
141
+ emit("plain", eol);
142
+ }
143
+ }
package/dist/index.d.ts CHANGED
@@ -77,7 +77,7 @@ export { MediaTile, type MediaTileProps } from "./MediaTile.js";
77
77
  export { ScrollStrip, type ScrollStripProps } from "./ScrollStrip.js";
78
78
  export { TextLink, type TextLinkSize } from "./TextLink.js";
79
79
  export { ToolCallCard } from "./ToolCallCard.js";
80
- export { Caption, Display, Lead, Overline, PageHeading, SectionHeading, SubsectionHeading, type SubsectionHeadingSize, Text, type TextTone, type TypographyColor, type TypographyProps, } from "./typography.js";
80
+ export { Caption, Display, Lead, Overline, PageHeading, Prose, SectionHeading, SubsectionHeading, type SubsectionHeadingSize, Text, type TextTone, type TypographyColor, type TypographyProps, } from "./typography.js";
81
81
  export { DateField, type DateFieldProps } from "./DateField.js";
82
82
  export { fromISODate, toISODate } from "./isoDate.js";
83
83
  export type { CalendarDate } from "@internationalized/date";
package/dist/index.js CHANGED
@@ -73,6 +73,6 @@ export { MediaTile } from "./MediaTile.js";
73
73
  export { ScrollStrip } from "./ScrollStrip.js";
74
74
  export { TextLink } from "./TextLink.js";
75
75
  export { ToolCallCard } from "./ToolCallCard.js";
76
- export { Caption, Display, Lead, Overline, PageHeading, SectionHeading, SubsectionHeading, Text, } from "./typography.js";
76
+ export { Caption, Display, Lead, Overline, PageHeading, Prose, SectionHeading, SubsectionHeading, Text, } from "./typography.js";
77
77
  export { DateField } from "./DateField.js";
78
78
  export { fromISODate, toISODate } from "./isoDate.js";
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Marks a box that scrolls with which of its edges still has content past it, so the layout can fade
3
+ * them: `data-more-start` (scrolled away from the start), `data-more-end` (more to scroll to) and
4
+ * `data-more` for either. `axis` is the way it scrolls: `x` (a row) or `y` (a column). A box that
5
+ * fits carries none. The attributes only carry state; a class turns them into a `mask-image`, so the
6
+ * fade follows any theme. `element` is the box once it is mounted (null before, and then nothing
7
+ * happens).
8
+ *
9
+ * The effect runs after every render on purpose: the box's children may have changed, and each one
10
+ * needs observing so a change in its size is noticed.
11
+ */
12
+ export declare function useScrollEdges(element: HTMLElement | null, axis: "x" | "y"): void;
@@ -0,0 +1,40 @@
1
+ import { useEffect } from "react";
2
+ /**
3
+ * Marks a box that scrolls with which of its edges still has content past it, so the layout can fade
4
+ * them: `data-more-start` (scrolled away from the start), `data-more-end` (more to scroll to) and
5
+ * `data-more` for either. `axis` is the way it scrolls: `x` (a row) or `y` (a column). A box that
6
+ * fits carries none. The attributes only carry state; a class turns them into a `mask-image`, so the
7
+ * fade follows any theme. `element` is the box once it is mounted (null before, and then nothing
8
+ * happens).
9
+ *
10
+ * The effect runs after every render on purpose: the box's children may have changed, and each one
11
+ * needs observing so a change in its size is noticed.
12
+ */
13
+ export function useScrollEdges(element, axis) {
14
+ useEffect(() => {
15
+ if (!element)
16
+ return undefined;
17
+ const el = element;
18
+ const update = () => {
19
+ const [position, scrollSize, clientSize] = axis === "x"
20
+ ? [el.scrollLeft, el.scrollWidth, el.clientWidth]
21
+ : [el.scrollTop, el.scrollHeight, el.clientHeight];
22
+ const start = position > 1;
23
+ const end = scrollSize - clientSize - position > 1;
24
+ el.toggleAttribute("data-more-start", start);
25
+ el.toggleAttribute("data-more-end", end);
26
+ el.toggleAttribute("data-more", start || end);
27
+ };
28
+ update();
29
+ el.addEventListener("scroll", update, { passive: true });
30
+ // jsdom (a consumer's own tests) has no ResizeObserver.
31
+ const resize = typeof ResizeObserver === "undefined" ? undefined : new ResizeObserver(update);
32
+ resize?.observe(el);
33
+ for (const child of el.children)
34
+ resize?.observe(child);
35
+ return () => {
36
+ el.removeEventListener("scroll", update);
37
+ resize?.disconnect();
38
+ };
39
+ });
40
+ }
@@ -15,3 +15,13 @@ export declare const STICKY_ASIDE_MD = "md:sticky md:top-[var(--app-header,0px)]
15
15
  export declare const STICKY_SCROLL_MD = "md:-m-1 md:max-h-[calc(100dvh-var(--app-header,0px)-2rem)] md:overflow-y-auto md:p-1";
16
16
  export declare const STICKY_ASIDE_LG = "lg:sticky lg:top-[var(--app-header,0px)] lg:self-start";
17
17
  export declare const STICKY_SCROLL_LG = "lg:-m-1 lg:max-h-[calc(100dvh-var(--app-header,0px)-2rem)] lg:overflow-y-auto lg:p-1";
18
+ /**
19
+ * While a sticky column's scroller has more to scroll to on a side, that edge fades out (a mask, so
20
+ * it follows any theme), the way AppShell's header row does. `useScrollEdges` sets `data-more-start`
21
+ * / `data-more-end` (and `data-more` for either); a column that fits has none and is untouched.
22
+ * Each is prefixed with its breakpoint, below which the column neither sticks nor scrolls and gets
23
+ * no mask. While the scroller itself holds the keyboard's tab stop (`focus-visible`) the mask is
24
+ * off, since it would hide the focus ring (an outline) at the faded edges.
25
+ */
26
+ export declare const STICKY_FADE_MD = "md:data-more-start:[--fade-start:1.5rem] md:data-more-end:[--fade-end:1.5rem] md:data-more:[mask-image:linear-gradient(to_bottom,transparent,#000_var(--fade-start,0px),#000_calc(100%_-_var(--fade-end,0px)),transparent)] md:data-more:focus-visible:[mask-image:none]";
27
+ export declare const STICKY_FADE_LG = "lg:data-more-start:[--fade-start:1.5rem] lg:data-more-end:[--fade-end:1.5rem] lg:data-more:[mask-image:linear-gradient(to_bottom,transparent,#000_var(--fade-start,0px),#000_calc(100%_-_var(--fade-end,0px)),transparent)] lg:data-more:focus-visible:[mask-image:none]";
@@ -15,3 +15,13 @@ export const STICKY_ASIDE_MD = "md:sticky md:top-[var(--app-header,0px)] md:self
15
15
  export const STICKY_SCROLL_MD = "md:-m-1 md:max-h-[calc(100dvh-var(--app-header,0px)-2rem)] md:overflow-y-auto md:p-1";
16
16
  export const STICKY_ASIDE_LG = "lg:sticky lg:top-[var(--app-header,0px)] lg:self-start";
17
17
  export const STICKY_SCROLL_LG = "lg:-m-1 lg:max-h-[calc(100dvh-var(--app-header,0px)-2rem)] lg:overflow-y-auto lg:p-1";
18
+ /**
19
+ * While a sticky column's scroller has more to scroll to on a side, that edge fades out (a mask, so
20
+ * it follows any theme), the way AppShell's header row does. `useScrollEdges` sets `data-more-start`
21
+ * / `data-more-end` (and `data-more` for either); a column that fits has none and is untouched.
22
+ * Each is prefixed with its breakpoint, below which the column neither sticks nor scrolls and gets
23
+ * no mask. While the scroller itself holds the keyboard's tab stop (`focus-visible`) the mask is
24
+ * off, since it would hide the focus ring (an outline) at the faded edges.
25
+ */
26
+ export const STICKY_FADE_MD = "md:data-more-start:[--fade-start:1.5rem] md:data-more-end:[--fade-end:1.5rem] md:data-more:[mask-image:linear-gradient(to_bottom,transparent,#000_var(--fade-start,0px),#000_calc(100%_-_var(--fade-end,0px)),transparent)] md:data-more:focus-visible:[mask-image:none]";
27
+ export const STICKY_FADE_LG = "lg:data-more-start:[--fade-start:1.5rem] lg:data-more-end:[--fade-end:1.5rem] lg:data-more:[mask-image:linear-gradient(to_bottom,transparent,#000_var(--fade-start,0px),#000_calc(100%_-_var(--fade-end,0px)),transparent)] lg:data-more:focus-visible:[mask-image:none]";
@@ -1,4 +1,5 @@
1
1
  import { type ComponentPropsWithoutRef, type ElementType, type ReactElement, type ReactNode } from "react";
2
+ import type { Passthrough } from "./props.js";
2
3
  import type { Size } from "./size.js";
3
4
  export type TypographyColor = "default" | "secondary" | "tertiary" | "muted" | "brand";
4
5
  /**
@@ -77,8 +78,31 @@ export declare const PageHeading: {
77
78
  <E extends ElementType = "h1">(props: TypographyProps<E>): ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
78
79
  displayName: string;
79
80
  };
81
+ /** True inside a `Prose`, where the text roles take the long-form scale. Internal. */
82
+ export declare function useProse(): boolean;
80
83
  /**
81
- * Top-level section header. `h2`, text-base/6 semibold, primary text. `rule` sets
84
+ * Sets the text roles back to the app scale around what a component renders itself (a `Dialog`, a
85
+ * `Drawer`, an `EmptyState`), so one opened from a docs page looks as it does anywhere else.
86
+ * Renders no element. Internal: not exported from the package.
87
+ */
88
+ export declare function AppScale({ children }: {
89
+ children?: ReactNode;
90
+ }): ReactElement;
91
+ /**
92
+ * Long-form content, for a docs page or an article. A block (`div`) that sets text-base/7 on itself,
93
+ * so a bare `<li>` or text node inside reads at 16px, and puts the text roles inside it on the
94
+ * prose scale: `Text` text-base/7, `SectionHeading` text-2xl/8, `SubsectionHeading` `lg` text-2xl/8,
95
+ * `md` text-xl/7 and `sm` text-base/7 (a semibold line at body size), and the box of a `Callout`
96
+ * text-base/7. Every other role keeps its size. Nothing outside a `Prose` changes, and a `Dialog`,
97
+ * `Drawer` or `EmptyState` inside one is back on the app scale. An app screen never uses it: wrap the
98
+ * body of a docs page or an article, inside a `ProseLayout`, a `SidebarLayout` or anywhere else.
99
+ */
100
+ export declare function Prose({ children, ...rest }: {
101
+ children?: ReactNode;
102
+ } & Passthrough): ReactElement;
103
+ /**
104
+ * Top-level section header. `h2`, text-base/6 semibold, primary text (text-2xl/8 semibold tight
105
+ * inside a `Prose`). `rule` sets
82
106
  * a hairline under it (the `Divider` line colour) with the space either side of
83
107
  * the line built in. `description` is a muted line under the heading, outside the `<h2>`, so the
84
108
  * heading's name stays the title; the rule then sits under the description.
@@ -92,7 +116,9 @@ export declare function SectionHeading<E extends ElementType = "h2">({ rule, des
92
116
  export type SubsectionHeadingSize = Size;
93
117
  /**
94
118
  * Nested header. `h3`, text-base/6 semibold, primary text. `size` steps it down for a dense card
95
- * or up for a drawer or form-group title: `sm` (text-sm), `md` (default) or `lg`. `truncate` keeps it on
119
+ * or up for a drawer or form-group title: `sm` (text-sm), `md` (default) or `lg`. Inside a `Prose`
120
+ * `md` is text-xl/7 (the h3 of a doc), `sm` text-base/7 (the h4: body size, set apart by its weight)
121
+ * and `lg` text-2xl/8, the size of a `SectionHeading` there. `truncate` keeps it on
96
122
  * one line and cuts it with an ellipsis where its container ends, the full text as the `title`.
97
123
  */
98
124
  export declare function SubsectionHeading<E extends ElementType = "h3">({ size, ...props }: TypographyProps<E> & {
@@ -115,7 +141,8 @@ export declare const Overline: {
115
141
  /** A fixed width for a run of text: `amount` is a money figure, right-aligned, at a width that lines a column up. */
116
142
  export type TextMinWidth = "amount";
117
143
  /**
118
- * Running text. `span` (inline; `as="p"` for a paragraph), text-sm/5 normal, secondary. A block of it
144
+ * Running text. `span` (inline; `as="p"` for a paragraph), text-sm/5 normal, secondary (text-base/7
145
+ * inside a `Prose`). A block of it
119
146
  * stops at a readable line length (`max-w-[72ch]`), wherever it sits. `strong` emphasises it:
120
147
  * font-medium in the primary ink, unless `color` or `tone` says otherwise.
121
148
  * `preserveNewlines` keeps the line breaks in the text (a user-typed note); runs of spaces still collapse.