@mitcsutt/kiln-ui 0.4.0 → 0.6.0
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/dist/components/display/CodeBlock/CodeBlock.d.ts +44 -3
- package/dist/components/display/CodeBlock/CodeBlock.js +54 -49
- package/dist/components/display/CodeBlock/CodeBlock.js.map +1 -1
- package/dist/components/display/CodeBlock/CodeBlock.module.js +4 -3
- package/dist/components/display/CodeBlock/CodeBlock.module.js.map +1 -1
- package/dist/components/display/CodeBlock/index.d.ts +1 -1
- package/dist/components/display/List/List.d.ts +8 -0
- package/dist/components/display/List/List.js +5 -4
- package/dist/components/display/List/List.js.map +1 -1
- package/dist/components/display/List/List.module.js.map +1 -1
- package/dist/components/display/List/index.d.ts +1 -1
- package/dist/components/display/Media/Media.d.ts +2 -0
- package/dist/components/display/Media/Media.js +41 -25
- package/dist/components/display/Media/Media.js.map +1 -1
- package/dist/components/display/Table/Table.d.ts +17 -0
- package/dist/components/display/Table/Table.js +13 -10
- package/dist/components/display/Table/Table.js.map +1 -1
- package/dist/components/display/Table/Table.module.js.map +1 -1
- package/dist/components/display/Table/index.d.ts +1 -1
- package/dist/components/layout/Box/Box.d.ts +12 -3
- package/dist/components/layout/Box/Box.js +11 -10
- package/dist/components/layout/Box/Box.js.map +1 -1
- package/dist/components/layout/Box/Box.module.js.map +1 -1
- package/dist/components/layout/ScrollArea/ScrollArea.d.ts +36 -0
- package/dist/components/layout/ScrollArea/ScrollArea.js +25 -0
- package/dist/components/layout/ScrollArea/ScrollArea.js.map +1 -0
- package/dist/components/layout/ScrollArea/ScrollArea.module.js +6 -0
- package/dist/components/layout/ScrollArea/ScrollArea.module.js.map +1 -0
- package/dist/components/layout/ScrollArea/index.d.ts +2 -0
- package/dist/components/layout/Section/Section.d.ts +11 -2
- package/dist/components/layout/Section/Section.js +10 -9
- package/dist/components/layout/Section/Section.js.map +1 -1
- package/dist/components/layout/Section/Section.module.js.map +1 -1
- package/dist/highlight/index.d.ts +18 -0
- package/dist/highlight/index.js +103 -0
- package/dist/highlight/index.js.map +1 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.js +102 -101
- package/dist/styles.css +1 -1
- package/dist/utils/newlines.d.ts +2 -0
- package/dist/utils/newlines.js +10 -0
- package/dist/utils/newlines.js.map +1 -0
- package/package.json +13 -2
- package/skills/design-rules/SKILL.md +1 -0
- package/skills/layout-composition/SKILL.md +2 -0
- package/skills/layout-composition/references/box.md +21 -1
- package/skills/layout-composition/references/scroll-area.md +68 -0
- package/skills/layout-composition/references/section.md +2 -1
- package/skills/setup-and-theming/SKILL.md +1 -0
|
@@ -1,9 +1,25 @@
|
|
|
1
1
|
import { type HTMLAttributes, type ReactNode } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* What a piece of highlighted source is. Each kind is coloured from the theme's own tokens, so
|
|
4
|
+
* highlighted code follows the theme and the colour mode.
|
|
5
|
+
*/
|
|
6
|
+
export type CodeTokenType = 'keyword' | 'string' | 'comment' | 'constant' | 'function' | 'type' | 'tag' | 'attribute' | 'punctuation';
|
|
7
|
+
/** A run of source text and its kind. A token with no `type` is plain text. */
|
|
8
|
+
export interface CodeToken {
|
|
9
|
+
content: string;
|
|
10
|
+
type?: CodeTokenType;
|
|
11
|
+
}
|
|
2
12
|
export interface CodeBlockProps extends Omit<HTMLAttributes<HTMLElement>, 'title'> {
|
|
3
13
|
/** The source, verbatim. Trailing newlines are trimmed. */
|
|
4
14
|
code: string;
|
|
5
|
-
/** Shown as a label ("TypeScript", "bash").
|
|
15
|
+
/** Shown as a label ("TypeScript", "bash"). It doesn't turn on highlighting: `tokens` does. */
|
|
6
16
|
language?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Syntax highlighting for `code`: one array of tokens per line, as `highlight` from
|
|
19
|
+
* `@mitcsutt/kiln-ui/highlight` returns them. Without it, or when the tokens don't spell out
|
|
20
|
+
* `code` line for line (say, tokens from a previous `code`), the code is plain.
|
|
21
|
+
*/
|
|
22
|
+
tokens?: CodeToken[][];
|
|
7
23
|
/** Filename or caption shown in the header: `src/queries/keys.ts`. */
|
|
8
24
|
title?: ReactNode;
|
|
9
25
|
/** Number the lines in a gutter (not copied with a selection). */
|
|
@@ -23,8 +39,33 @@ export interface CodeBlockProps extends Omit<HTMLAttributes<HTMLElement>, 'title
|
|
|
23
39
|
*
|
|
24
40
|
* @remarks
|
|
25
41
|
* `CodeBlock` shows code: a snippet in an article, a command, a config example. It's a plain
|
|
26
|
-
* surface with an optional title bar, never fake window chrome
|
|
27
|
-
*
|
|
42
|
+
* surface with an optional title bar, never fake window chrome. It's plain unless you pass
|
|
43
|
+
* `tokens`: `highlight` from `@mitcsutt/kiln-ui/highlight` turns JavaScript, JSX, TypeScript and
|
|
44
|
+
* TSX into tokens, coloured from the theme. Every code sample in these docs is a `CodeBlock`.
|
|
45
|
+
*
|
|
46
|
+
* ## Highlighting
|
|
47
|
+
*
|
|
48
|
+
* `highlight(code, language)` resolves to the tokens for `code`, or to `undefined` for a language
|
|
49
|
+
* other than `js`, `jsx`, `ts` or `tsx` (or `javascript` and `typescript`), which `CodeBlock`
|
|
50
|
+
* shows as plain code. It runs [Shiki](https://shiki.style), an optional peer dependency, so
|
|
51
|
+
* install `shiki` to use it. The main `@mitcsutt/kiln-ui` entry never loads it.
|
|
52
|
+
*
|
|
53
|
+
* Keywords and tags take the accent, strings, constants, functions and types take the theme's
|
|
54
|
+
* tones, and comments and punctuation are muted. A custom theme needs no code palette of its own.
|
|
55
|
+
*
|
|
56
|
+
* The tokens are plain data, so highlight wherever the code is ready. In a server component or
|
|
57
|
+
* at build time, `await` it and pass the tokens to the client component that renders the
|
|
58
|
+
* `CodeBlock`, and the browser downloads no highlighter. In the browser, call it in an effect, as
|
|
59
|
+
* in the example above: Shiki and the grammar load on the first call.
|
|
60
|
+
*
|
|
61
|
+
* ```tsx
|
|
62
|
+
* import { highlight } from '@mitcsutt/kiln-ui/highlight'
|
|
63
|
+
*
|
|
64
|
+
* export async function Snippet({ source }: { source: string }) {
|
|
65
|
+
* const tokens = await highlight(source, 'tsx')
|
|
66
|
+
* return <SnippetBlock code={source} tokens={tokens} />
|
|
67
|
+
* }
|
|
68
|
+
* ```
|
|
28
69
|
*
|
|
29
70
|
* @privateRemarks
|
|
30
71
|
* A block of source: a snippet in a blog post, a command in a README, a config example.
|
|
@@ -1,85 +1,90 @@
|
|
|
1
1
|
import { cx as e } from "../../../utils/cx.js";
|
|
2
2
|
import { CheckIcon as t, CopyIcon as n } from "../../../icons/icons.js";
|
|
3
3
|
import { Button as r } from "../../actions/Button/Button.js";
|
|
4
|
-
import i from "
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
4
|
+
import { trimTrailingNewlines as i } from "../../../utils/newlines.js";
|
|
5
|
+
import a from "./CodeBlock.module.js";
|
|
6
|
+
import { forwardRef as o, useEffect as s, useId as c, useRef as l, useState as u } from "react";
|
|
7
|
+
import { jsx as d, jsxs as f } from "react/jsx-runtime";
|
|
7
8
|
//#region src/components/display/CodeBlock/CodeBlock.tsx
|
|
8
|
-
var
|
|
9
|
-
let [
|
|
10
|
-
|
|
11
|
-
clearTimeout(
|
|
9
|
+
var p = o(function({ code: o, language: p, tokens: m, title: h, showLineNumbers: g = !1, highlightLines: _, copyable: v = !0, wrap: y = !1, copyLabel: b = "Copy code", className: x, ...S }, C) {
|
|
10
|
+
let [w, T] = u("idle"), E = l(void 0), D = c(), O = i(o), k = O.split("\n"), A = m?.length === k.length && m.every((e, t) => e.map((e) => e.content).join("") === k[t]) ? m : void 0, j = new Set(_), M = !!h || !!p || v;
|
|
11
|
+
s(() => () => {
|
|
12
|
+
clearTimeout(E.current);
|
|
12
13
|
}, []);
|
|
13
|
-
let
|
|
14
|
-
clearTimeout(
|
|
14
|
+
let N = async () => {
|
|
15
|
+
clearTimeout(E.current);
|
|
15
16
|
try {
|
|
16
|
-
await navigator.clipboard.writeText(
|
|
17
|
+
await navigator.clipboard.writeText(O), T("copied");
|
|
17
18
|
} catch {
|
|
18
|
-
|
|
19
|
+
T("failed");
|
|
19
20
|
}
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
E.current = setTimeout(() => {
|
|
22
|
+
T("idle");
|
|
22
23
|
}, 2e3);
|
|
23
24
|
};
|
|
24
|
-
return /* @__PURE__ */
|
|
25
|
-
ref:
|
|
26
|
-
className: e(
|
|
25
|
+
return /* @__PURE__ */ f("figure", {
|
|
26
|
+
ref: C,
|
|
27
|
+
className: e(a.codeBlock, x),
|
|
27
28
|
"data-kiln-component": "",
|
|
28
|
-
"data-wrap":
|
|
29
|
-
"data-line-numbers":
|
|
30
|
-
"aria-labelledby":
|
|
31
|
-
...
|
|
29
|
+
"data-wrap": y || void 0,
|
|
30
|
+
"data-line-numbers": g || void 0,
|
|
31
|
+
"aria-labelledby": h ? D : void 0,
|
|
32
|
+
...S,
|
|
32
33
|
children: [
|
|
33
|
-
|
|
34
|
-
className:
|
|
34
|
+
M ? /* @__PURE__ */ f("figcaption", {
|
|
35
|
+
className: a.header,
|
|
35
36
|
children: [
|
|
36
|
-
|
|
37
|
-
id:
|
|
38
|
-
className:
|
|
39
|
-
children:
|
|
37
|
+
h ? /* @__PURE__ */ d("span", {
|
|
38
|
+
id: D,
|
|
39
|
+
className: a.title,
|
|
40
|
+
children: h
|
|
40
41
|
}) : null,
|
|
41
|
-
|
|
42
|
-
className:
|
|
43
|
-
children:
|
|
42
|
+
p ? /* @__PURE__ */ d("span", {
|
|
43
|
+
className: a.language,
|
|
44
|
+
children: p
|
|
44
45
|
}) : null,
|
|
45
|
-
|
|
46
|
+
v ? /* @__PURE__ */ d(r, {
|
|
46
47
|
variant: "ghost",
|
|
47
48
|
tone: "neutral",
|
|
48
49
|
size: "sm",
|
|
49
|
-
className:
|
|
50
|
-
leadingIcon:
|
|
51
|
-
"aria-label":
|
|
50
|
+
className: a.copy,
|
|
51
|
+
leadingIcon: d(w === "copied" ? t : n, {}),
|
|
52
|
+
"aria-label": b,
|
|
52
53
|
onClick: () => {
|
|
53
|
-
|
|
54
|
+
N();
|
|
54
55
|
},
|
|
55
|
-
"data-state":
|
|
56
|
-
children:
|
|
56
|
+
"data-state": w,
|
|
57
|
+
children: w === "copied" ? "Copied" : w === "failed" ? "Copy failed" : "Copy"
|
|
57
58
|
}) : null
|
|
58
59
|
]
|
|
59
60
|
}) : null,
|
|
60
|
-
/* @__PURE__ */
|
|
61
|
-
className:
|
|
61
|
+
/* @__PURE__ */ d("pre", {
|
|
62
|
+
className: a.pre,
|
|
62
63
|
tabIndex: 0,
|
|
63
|
-
children: /* @__PURE__ */
|
|
64
|
-
className:
|
|
65
|
-
children:
|
|
66
|
-
className:
|
|
67
|
-
"data-line":
|
|
68
|
-
"data-highlighted":
|
|
69
|
-
children: [e, t
|
|
64
|
+
children: /* @__PURE__ */ d("code", {
|
|
65
|
+
className: a.code,
|
|
66
|
+
children: k.map((e, t) => /* @__PURE__ */ f("span", {
|
|
67
|
+
className: a.line,
|
|
68
|
+
"data-line": g ? t + 1 : void 0,
|
|
69
|
+
"data-highlighted": j.has(t + 1) || void 0,
|
|
70
|
+
children: [A?.[t] ? A[t].map((e, t) => e.type ? /* @__PURE__ */ d("span", {
|
|
71
|
+
className: a.token,
|
|
72
|
+
"data-token": e.type,
|
|
73
|
+
children: e.content
|
|
74
|
+
}, t) : e.content) : e, t < k.length - 1 ? "\n" : null]
|
|
70
75
|
}, t))
|
|
71
76
|
})
|
|
72
77
|
}),
|
|
73
|
-
|
|
74
|
-
className:
|
|
78
|
+
v ? /* @__PURE__ */ d("span", {
|
|
79
|
+
className: a.visuallyHidden,
|
|
75
80
|
role: "status",
|
|
76
81
|
"aria-live": "polite",
|
|
77
|
-
children:
|
|
82
|
+
children: w === "copied" ? "Copied to clipboard" : w === "failed" ? "Copy failed" : ""
|
|
78
83
|
}) : null
|
|
79
84
|
]
|
|
80
85
|
});
|
|
81
86
|
});
|
|
82
87
|
//#endregion
|
|
83
|
-
export {
|
|
88
|
+
export { p as CodeBlock };
|
|
84
89
|
|
|
85
90
|
//# sourceMappingURL=CodeBlock.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CodeBlock.js","names":["styles","CodeBlock_module_default","CodeBlock","forwardRef","rest","copy","setCopy","useState","timer","useRef","captionId","useId","source","code","lines","highlighted","highlightLines","hasHeader","title","language","copyable","onCopy","cx","className","wrap","showLineNumbers","Button","jsx","CheckIcon","CopyIcon","copyLabel","i","line"],"sources":["../../../../src/components/display/CodeBlock/CodeBlock.tsx"],"sourcesContent":["import {\n forwardRef,\n useEffect,\n useId,\n useRef,\n useState,\n type HTMLAttributes,\n type ReactNode,\n} from 'react'\nimport { CheckIcon, CopyIcon } from '#icons'\nimport { Button } from '#components/actions/Button'\nimport { cx } from '#utils/cx'\nimport styles from './CodeBlock.module.css'\n\nexport interface CodeBlockProps extends Omit<HTMLAttributes<HTMLElement>, 'title'> {\n /** The source, verbatim. Trailing newlines are trimmed. */\n code: string\n /** Shown as a label (\"TypeScript\", \"bash\"). No syntax highlighting — the code is the design. */\n language?: string\n /** Filename or caption shown in the header: `src/queries/keys.ts`. */\n title?: ReactNode\n /** Number the lines in a gutter (not copied with a selection). */\n showLineNumbers?: boolean\n /** 1-based line numbers to mark with the theme's highlight colour. */\n highlightLines?: number[]\n /** Show a copy button. Default `true`. */\n copyable?: boolean\n /** Wrap long lines instead of scrolling sideways. */\n wrap?: boolean\n /** Accessible name of the copy button. Default \"Copy code\". */\n copyLabel?: string\n}\n\ntype CopyState = 'idle' | 'copied' | 'failed'\n\n/**\n * A block of source with an optional filename bar, line numbers, highlighted lines and a copy\n * button.\n *\n * @remarks\n * `CodeBlock` shows code: a snippet in an article, a command, a config example. It's a plain\n * surface with an optional title bar, never fake window chrome, and it has no syntax highlighting:\n * the code is the design. Every code sample in these docs is a `CodeBlock`.\n *\n * @privateRemarks\n * A block of source: a snippet in a blog post, a command in a README, a config example.\n * A plain surface with an optional filename bar — no fake window chrome.\n *\n * <CodeBlock title=\"queries/keys.ts\" language=\"TypeScript\" code={source} highlightLines={[3, 4]} />\n */\nexport const CodeBlock = forwardRef<HTMLElement, CodeBlockProps>(function CodeBlock(\n {\n code,\n language,\n title,\n showLineNumbers = false,\n highlightLines,\n copyable = true,\n wrap = false,\n copyLabel = 'Copy code',\n className,\n ...rest\n },\n ref,\n) {\n const [copy, setCopy] = useState<CopyState>('idle')\n const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)\n const captionId = useId()\n const source = code.replace(/\\n+$/, '')\n const lines = source.split('\\n')\n const highlighted = new Set(highlightLines)\n const hasHeader = Boolean(title) || Boolean(language) || copyable\n\n useEffect(\n () => () => {\n clearTimeout(timer.current)\n },\n [],\n )\n\n const onCopy = async () => {\n clearTimeout(timer.current)\n try {\n await navigator.clipboard.writeText(source)\n setCopy('copied')\n } catch {\n setCopy('failed')\n }\n timer.current = setTimeout(() => {\n setCopy('idle')\n }, 2000)\n }\n\n return (\n <figure\n ref={ref}\n className={cx(styles.codeBlock, className)}\n data-kiln-component=\"\"\n data-wrap={wrap || undefined}\n data-line-numbers={showLineNumbers || undefined}\n aria-labelledby={title ? captionId : undefined}\n {...rest}\n >\n {hasHeader ? (\n <figcaption className={styles.header}>\n {title ? (\n <span id={captionId} className={styles.title}>\n {title}\n </span>\n ) : null}\n {language ? <span className={styles.language}>{language}</span> : null}\n {copyable ? (\n <Button\n variant=\"ghost\"\n tone=\"neutral\"\n size=\"sm\"\n className={styles.copy}\n leadingIcon={copy === 'copied' ? <CheckIcon /> : <CopyIcon />}\n aria-label={copyLabel}\n onClick={() => {\n void onCopy()\n }}\n data-state={copy}\n >\n {copy === 'copied' ? 'Copied' : copy === 'failed' ? 'Copy failed' : 'Copy'}\n </Button>\n ) : null}\n </figcaption>\n ) : null}\n {/* A scrollable region must be reachable by keyboard. */}\n <pre className={styles.pre} tabIndex={0}>\n <code className={styles.code}>\n {lines.map((line, i) => (\n <span\n key={i}\n className={styles.line}\n data-line={showLineNumbers ? i + 1 : undefined}\n data-highlighted={highlighted.has(i + 1) || undefined}\n >\n {line}\n {i < lines.length - 1 ? '\\n' : null}\n </span>\n ))}\n </code>\n </pre>\n {copyable ? (\n <span className={styles.visuallyHidden} role=\"status\" aria-live=\"polite\">\n {copy === 'copied' ? 'Copied to clipboard' : copy === 'failed' ? 'Copy failed' : ''}\n </span>\n ) : null}\n </figure>\n )\n})\n"],"mappings":";;;;;;;AAkDA,IAAaE,IAAYC,EAAwC,SAC/D,EACE,SACA,aACA,UACA,qBAAkB,IAClB,mBACA,cAAW,IACX,UAAO,IACP,eAAY,aACZ,cACA,GAAGC,KAEL,GACA;CACA,IAAM,CAACC,GAAMC,KAAWC,EAAoB,MAAM,GAC5CC,IAAQC,EAAkD,KAAA,CAAS,GACnEC,IAAYC,EAAM,GAClBC,IAASC,EAAK,QAAQ,QAAQ,EAAE,GAChCC,IAAQF,EAAO,MAAM,IAAI,GACzBG,IAAc,IAAI,IAAIC,CAAc,GACpCC,IAAY,EAAQC,KAAU,EAAQC,KAAaC;CAEzD,cACc;EACV,aAAaZ,EAAM,OAAO;CAC5B,GACA,CAAC,CACH;CAEA,IAAMa,IAAS,YAAY;EACzB,aAAab,EAAM,OAAO;EAC1B,IAAI;GAEF,AADA,MAAM,UAAU,UAAU,UAAUI,CAAM,GAC1CN,EAAQ,QAAQ;EAClB,QAAQ;GACN,EAAQ,QAAQ;EAClB;EACA,EAAM,UAAU,iBAAiB;GAC/B,EAAQ,MAAM;EAChB,GAAG,GAAI;CACT;CAEA,OACE,kBAAC,UAAD;EACO;EACL,WAAWgB,EAAGtB,EAAO,WAAWuB,CAAS;EACzC,uBAAoB;EACpB,aAAWC,KAAQ,KAAA;EACnB,qBAAmBC,KAAmB,KAAA;EACtC,mBAAiBP,IAAQR,IAAY,KAAA;EACrC,GAAIN;EAPN,UAAA;GASGa,IACC,kBAAC,cAAD;IAAY,WAAWjB,EAAO;IAA9B,UAAA;KACGkB,IACC,kBAAC,QAAD;MAAM,IAAIR;MAAW,WAAWV,EAAO;MACpC,UAAAkB;KACG,CAAA,IACJ;KACHC,IAAW,kBAAC,QAAD;MAAM,WAAWnB,EAAO;MAAW,UAAAmB;KAAe,CAAA,IAAI;KACjEC,IACC,kBAACM,GAAD;MACE,SAAQ;MACR,MAAK;MACL,MAAK;MACL,WAAW1B,EAAO;MAClB,aAAiC2B,EAApBtB,MAAS,WAAYuB,IAAgBC,GAAjB,CAAY,CAAe;MAC5D,cAAYC;MACZ,eAAe;OACb,EAAY;MACd;MACA,cAAYzB;MAEX,UAAAA,MAAS,WAAW,WAAWA,MAAS,WAAW,gBAAgB;KAC9D,CAAA,IACN;IACM;GACV,CAAA,IAAA;GAEJ,kBAAC,OAAD;IAAK,WAAWL,EAAO;IAAK,UAAU;IACpC,UAAA,kBAAC,QAAD;KAAM,WAAWA,EAAO;KACrB,UAAAc,EAAM,KAAK,GAAM,MAChB,kBAAC,QAAD;MAEE,WAAWd,EAAO;MAClB,aAAWyB,IAAkBM,IAAI,IAAI,KAAA;MACrC,oBAAkBhB,EAAY,IAAIgB,IAAI,CAAC,KAAK,KAAA;MAJ9C,UAAA,CAMGC,GACAD,IAAIjB,EAAM,SAAS,IAAI,OAAO,IAC3B;KAPC,GAAAiB,CAOD,CACP;IACG,CAAA;GACH,CAAA;GACJX,IACC,kBAAC,QAAD;IAAM,WAAWpB,EAAO;IAAgB,MAAK;IAAS,aAAU;IAC7D,UAAAK,MAAS,WAAW,wBAAwBA,MAAS,WAAW,gBAAgB;GAC7E,CAAA,IACJ;EACE;;AAEZ,CAAC"}
|
|
1
|
+
{"version":3,"file":"CodeBlock.js","names":["styles","CodeBlock_module_default","CodeBlock","forwardRef","rest","copy","setCopy","useState","timer","useRef","captionId","useId","source","trimTrailingNewlines","code","lines","lineTokens","tokens","line","token","i","highlighted","highlightLines","hasHeader","title","language","copyable","onCopy","cx","className","wrap","showLineNumbers","Button","jsx","CheckIcon","CopyIcon","copyLabel","j"],"sources":["../../../../src/components/display/CodeBlock/CodeBlock.tsx"],"sourcesContent":["import {\n forwardRef,\n useEffect,\n useId,\n useRef,\n useState,\n type HTMLAttributes,\n type ReactNode,\n} from 'react'\nimport { CheckIcon, CopyIcon } from '#icons'\nimport { Button } from '#components/actions/Button'\nimport { cx } from '#utils/cx'\nimport { trimTrailingNewlines } from '#utils/newlines'\nimport styles from './CodeBlock.module.css'\n\n/**\n * What a piece of highlighted source is. Each kind is coloured from the theme's own tokens, so\n * highlighted code follows the theme and the colour mode.\n */\nexport type CodeTokenType =\n | 'keyword'\n | 'string'\n | 'comment'\n | 'constant'\n | 'function'\n | 'type'\n | 'tag'\n | 'attribute'\n | 'punctuation'\n\n/** A run of source text and its kind. A token with no `type` is plain text. */\nexport interface CodeToken {\n content: string\n type?: CodeTokenType\n}\n\nexport interface CodeBlockProps extends Omit<HTMLAttributes<HTMLElement>, 'title'> {\n /** The source, verbatim. Trailing newlines are trimmed. */\n code: string\n /** Shown as a label (\"TypeScript\", \"bash\"). It doesn't turn on highlighting: `tokens` does. */\n language?: string\n /**\n * Syntax highlighting for `code`: one array of tokens per line, as `highlight` from\n * `@mitcsutt/kiln-ui/highlight` returns them. Without it, or when the tokens don't spell out\n * `code` line for line (say, tokens from a previous `code`), the code is plain.\n */\n tokens?: CodeToken[][]\n /** Filename or caption shown in the header: `src/queries/keys.ts`. */\n title?: ReactNode\n /** Number the lines in a gutter (not copied with a selection). */\n showLineNumbers?: boolean\n /** 1-based line numbers to mark with the theme's highlight colour. */\n highlightLines?: number[]\n /** Show a copy button. Default `true`. */\n copyable?: boolean\n /** Wrap long lines instead of scrolling sideways. */\n wrap?: boolean\n /** Accessible name of the copy button. Default \"Copy code\". */\n copyLabel?: string\n}\n\ntype CopyState = 'idle' | 'copied' | 'failed'\n\n/**\n * A block of source with an optional filename bar, line numbers, highlighted lines and a copy\n * button.\n *\n * @remarks\n * `CodeBlock` shows code: a snippet in an article, a command, a config example. It's a plain\n * surface with an optional title bar, never fake window chrome. It's plain unless you pass\n * `tokens`: `highlight` from `@mitcsutt/kiln-ui/highlight` turns JavaScript, JSX, TypeScript and\n * TSX into tokens, coloured from the theme. Every code sample in these docs is a `CodeBlock`.\n *\n * ## Highlighting\n *\n * `highlight(code, language)` resolves to the tokens for `code`, or to `undefined` for a language\n * other than `js`, `jsx`, `ts` or `tsx` (or `javascript` and `typescript`), which `CodeBlock`\n * shows as plain code. It runs [Shiki](https://shiki.style), an optional peer dependency, so\n * install `shiki` to use it. The main `@mitcsutt/kiln-ui` entry never loads it.\n *\n * Keywords and tags take the accent, strings, constants, functions and types take the theme's\n * tones, and comments and punctuation are muted. A custom theme needs no code palette of its own.\n *\n * The tokens are plain data, so highlight wherever the code is ready. In a server component or\n * at build time, `await` it and pass the tokens to the client component that renders the\n * `CodeBlock`, and the browser downloads no highlighter. In the browser, call it in an effect, as\n * in the example above: Shiki and the grammar load on the first call.\n *\n * ```tsx\n * import { highlight } from '@mitcsutt/kiln-ui/highlight'\n *\n * export async function Snippet({ source }: { source: string }) {\n * const tokens = await highlight(source, 'tsx')\n * return <SnippetBlock code={source} tokens={tokens} />\n * }\n * ```\n *\n * @privateRemarks\n * A block of source: a snippet in a blog post, a command in a README, a config example.\n * A plain surface with an optional filename bar — no fake window chrome.\n *\n * <CodeBlock title=\"queries/keys.ts\" language=\"TypeScript\" code={source} highlightLines={[3, 4]} />\n */\nexport const CodeBlock = forwardRef<HTMLElement, CodeBlockProps>(function CodeBlock(\n {\n code,\n language,\n tokens,\n title,\n showLineNumbers = false,\n highlightLines,\n copyable = true,\n wrap = false,\n copyLabel = 'Copy code',\n className,\n ...rest\n },\n ref,\n) {\n const [copy, setCopy] = useState<CopyState>('idle')\n const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)\n const captionId = useId()\n const source = trimTrailingNewlines(code)\n const lines = source.split('\\n')\n const lineTokens =\n tokens?.length === lines.length &&\n tokens.every((line, i) => line.map((token) => token.content).join('') === lines[i])\n ? tokens\n : undefined\n const highlighted = new Set(highlightLines)\n const hasHeader = Boolean(title) || Boolean(language) || copyable\n\n useEffect(\n () => () => {\n clearTimeout(timer.current)\n },\n [],\n )\n\n const onCopy = async () => {\n clearTimeout(timer.current)\n try {\n await navigator.clipboard.writeText(source)\n setCopy('copied')\n } catch {\n setCopy('failed')\n }\n timer.current = setTimeout(() => {\n setCopy('idle')\n }, 2000)\n }\n\n return (\n <figure\n ref={ref}\n className={cx(styles.codeBlock, className)}\n data-kiln-component=\"\"\n data-wrap={wrap || undefined}\n data-line-numbers={showLineNumbers || undefined}\n aria-labelledby={title ? captionId : undefined}\n {...rest}\n >\n {hasHeader ? (\n <figcaption className={styles.header}>\n {title ? (\n <span id={captionId} className={styles.title}>\n {title}\n </span>\n ) : null}\n {language ? <span className={styles.language}>{language}</span> : null}\n {copyable ? (\n <Button\n variant=\"ghost\"\n tone=\"neutral\"\n size=\"sm\"\n className={styles.copy}\n leadingIcon={copy === 'copied' ? <CheckIcon /> : <CopyIcon />}\n aria-label={copyLabel}\n onClick={() => {\n void onCopy()\n }}\n data-state={copy}\n >\n {copy === 'copied' ? 'Copied' : copy === 'failed' ? 'Copy failed' : 'Copy'}\n </Button>\n ) : null}\n </figcaption>\n ) : null}\n {/* A scrollable region must be reachable by keyboard. */}\n <pre className={styles.pre} tabIndex={0}>\n <code className={styles.code}>\n {lines.map((line, i) => (\n <span\n key={i}\n className={styles.line}\n data-line={showLineNumbers ? i + 1 : undefined}\n data-highlighted={highlighted.has(i + 1) || undefined}\n >\n {lineTokens?.[i]\n ? lineTokens[i].map((token, j) =>\n token.type ? (\n <span key={j} className={styles.token} data-token={token.type}>\n {token.content}\n </span>\n ) : (\n token.content\n ),\n )\n : line}\n {i < lines.length - 1 ? '\\n' : null}\n </span>\n ))}\n </code>\n </pre>\n {copyable ? (\n <span className={styles.visuallyHidden} role=\"status\" aria-live=\"polite\">\n {copy === 'copied' ? 'Copied to clipboard' : copy === 'failed' ? 'Copy failed' : ''}\n </span>\n ) : null}\n </figure>\n )\n})\n"],"mappings":";;;;;;;;AAuGA,IAAaE,IAAYC,EAAwC,SAC/D,EACE,SACA,aACA,WACA,UACA,qBAAkB,IAClB,mBACA,cAAW,IACX,UAAO,IACP,eAAY,aACZ,cACA,GAAGC,KAEL,GACA;CACA,IAAM,CAACC,GAAMC,KAAWC,EAAoB,MAAM,GAC5CC,IAAQC,EAAkD,KAAA,CAAS,GACnEC,IAAYC,EAAM,GAClBC,IAASC,EAAqBC,CAAI,GAClCC,IAAQH,EAAO,MAAM,IAAI,GACzBI,IACJC,GAAQ,WAAWF,EAAM,UACzBE,EAAO,OAAO,GAAM,MAAMC,EAAK,KAAK,MAAUC,EAAM,OAAO,CAAC,CAAC,KAAK,EAAE,MAAMJ,EAAMK,EAAE,IAC9EH,IACA,KAAA,GACAI,IAAc,IAAI,IAAIC,CAAc,GACpCC,IAAY,EAAQC,KAAU,EAAQC,KAAaC;CAEzD,cACc;EACV,aAAalB,EAAM,OAAO;CAC5B,GACA,CAAC,CACH;CAEA,IAAMmB,IAAS,YAAY;EACzB,aAAanB,EAAM,OAAO;EAC1B,IAAI;GAEF,AADA,MAAM,UAAU,UAAU,UAAUI,CAAM,GAC1CN,EAAQ,QAAQ;EAClB,QAAQ;GACN,EAAQ,QAAQ;EAClB;EACA,EAAM,UAAU,iBAAiB;GAC/B,EAAQ,MAAM;EAChB,GAAG,GAAI;CACT;CAEA,OACE,kBAAC,UAAD;EACO;EACL,WAAWsB,EAAG5B,EAAO,WAAW6B,CAAS;EACzC,uBAAoB;EACpB,aAAWC,KAAQ,KAAA;EACnB,qBAAmBC,KAAmB,KAAA;EACtC,mBAAiBP,IAAQd,IAAY,KAAA;EACrC,GAAIN;EAPN,UAAA;GASGmB,IACC,kBAAC,cAAD;IAAY,WAAWvB,EAAO;IAA9B,UAAA;KACGwB,IACC,kBAAC,QAAD;MAAM,IAAId;MAAW,WAAWV,EAAO;MACpC,UAAAwB;KACG,CAAA,IACJ;KACHC,IAAW,kBAAC,QAAD;MAAM,WAAWzB,EAAO;MAAW,UAAAyB;KAAe,CAAA,IAAI;KACjEC,IACC,kBAACM,GAAD;MACE,SAAQ;MACR,MAAK;MACL,MAAK;MACL,WAAWhC,EAAO;MAClB,aAAiCiC,EAApB5B,MAAS,WAAY6B,IAAgBC,GAAjB,CAAY,CAAe;MAC5D,cAAYC;MACZ,eAAe;OACb,EAAY;MACd;MACA,cAAY/B;MAEX,UAAAA,MAAS,WAAW,WAAWA,MAAS,WAAW,gBAAgB;KAC9D,CAAA,IACN;IACM;GACV,CAAA,IAAA;GAEJ,kBAAC,OAAD;IAAK,WAAWL,EAAO;IAAK,UAAU;IACpC,UAAA,kBAAC,QAAD;KAAM,WAAWA,EAAO;KACrB,UAAAe,EAAM,KAAK,GAAM,MAChB,kBAAC,QAAD;MAEE,WAAWf,EAAO;MAClB,aAAW+B,IAAkBX,IAAI,IAAI,KAAA;MACrC,oBAAkBC,EAAY,IAAID,IAAI,CAAC,KAAK,KAAA;MAJ9C,UAAA,CAMGJ,IAAaI,KACVJ,EAAWI,EAAE,CAAC,KAAK,GAAO,MACxBD,EAAM,OACJ,kBAAC,QAAD;OAAc,WAAWnB,EAAO;OAAO,cAAYmB,EAAM;OACtD,UAAAA,EAAM;MACH,GAFKkB,CAEL,IAENlB,EAAM,OAEV,IACAD,GACHE,IAAIL,EAAM,SAAS,IAAI,OAAO,IAC3B;KAjBC,GAAAK,CAiBD,CACP;IACG,CAAA;GACH,CAAA;GACJM,IACC,kBAAC,QAAD;IAAM,WAAW1B,EAAO;IAAgB,MAAK;IAAS,aAAU;IAC7D,UAAAK,MAAS,WAAW,wBAAwBA,MAAS,WAAW,gBAAgB;GAC7E,CAAA,IACJ;EACE;;AAEZ,CAAC"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
//#region src/components/display/CodeBlock/CodeBlock.module.css
|
|
2
|
-
var e = "kiln-CodeBlock__codeBlock", t = "kiln-CodeBlock__header", n = "kiln-CodeBlock__title", r = "kiln-CodeBlock__language", i = "kiln-CodeBlock__copy", a = "kiln-CodeBlock__pre", o = "kiln-CodeBlock__code", s = "kiln-CodeBlock__line", c = "kiln-
|
|
2
|
+
var e = "kiln-CodeBlock__codeBlock", t = "kiln-CodeBlock__header", n = "kiln-CodeBlock__title", r = "kiln-CodeBlock__language", i = "kiln-CodeBlock__copy", a = "kiln-CodeBlock__pre", o = "kiln-CodeBlock__code", s = "kiln-CodeBlock__line", c = "kiln-CodeBlock__token", l = "kiln-CodeBlock__visuallyHidden", u = {
|
|
3
3
|
codeBlock: e,
|
|
4
4
|
header: t,
|
|
5
5
|
title: n,
|
|
@@ -8,9 +8,10 @@ var e = "kiln-CodeBlock__codeBlock", t = "kiln-CodeBlock__header", n = "kiln-Cod
|
|
|
8
8
|
pre: a,
|
|
9
9
|
code: o,
|
|
10
10
|
line: s,
|
|
11
|
-
|
|
11
|
+
token: c,
|
|
12
|
+
visuallyHidden: l
|
|
12
13
|
};
|
|
13
14
|
//#endregion
|
|
14
|
-
export { o as code, e as codeBlock, i as copy,
|
|
15
|
+
export { o as code, e as codeBlock, i as copy, u as default, t as header, r as language, s as line, a as pre, n as title, c as token, l as visuallyHidden };
|
|
15
16
|
|
|
16
17
|
//# sourceMappingURL=CodeBlock.module.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CodeBlock.module.js","names":["codeBlock","header","title","language","copy","pre","code","line","visuallyHidden","CodeBlock_module_default"],"sources":["../../../../src/components/display/CodeBlock/CodeBlock.module.css"],"sourcesContent":["/*\n * Component tokens (themes may set any of these on [data-theme]):\n * --codeblock-bg default var(--color-surface-sunken)\n * --codeblock-radius default var(--radius-surface)\n * --codeblock-size default var(--text-xs)\n * --codeblock-highlight highlighted line fill, default var(--color-highlight)\n *\n * One edge: a hairline. The header is separated by the same rule, not a fill.\n */\n/* Zero-specificity reset, so a surrounding flow (Prose, Stack) or a className can space it. */\n:where(.codeBlock) {\n margin: 0;\n}\n.codeBlock {\n --_pad: var(--space-4);\n\n display: flex;\n flex-direction: column;\n min-inline-size: 0;\n overflow: hidden;\n border: var(--border-width) solid var(--color-line);\n border-radius: var(--codeblock-radius, var(--radius-surface));\n background: var(--codeblock-bg, var(--color-surface-sunken));\n color: var(--color-ink);\n}\n\n.header {\n display: flex;\n align-items: center;\n gap: var(--space-3);\n min-block-size: calc(var(--control-sm) + var(--space-2));\n padding-inline: var(--_pad) var(--space-2);\n border-block-end: var(--border-width) solid var(--color-line);\n font-family: var(--font-text);\n font-size: var(--text-xs);\n line-height: var(--leading-snug);\n}\n\n.title {\n min-inline-size: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n color: var(--color-ink);\n font-weight: var(--label-weight);\n}\n\n.language {\n color: var(--color-ink-subtle);\n}\n.title + .language::before {\n content: '·';\n margin-inline-end: var(--space-3);\n}\n\n.copy {\n margin-inline-start: auto;\n color: var(--color-ink-muted);\n}\n.copy[data-state='copied'] {\n color: var(--tone-positive-text);\n}\n.copy[data-state='failed'] {\n color: var(--tone-critical-text);\n}\n\n.pre {\n margin: 0;\n padding-block: var(--space-3);\n overflow-x: auto;\n font-family: var(--font-mono);\n font-stretch: var(--mono-stretch);\n font-size: var(--codeblock-size, var(--text-xs));\n font-variant-ligatures: none;\n line-height: 1.7;\n tab-size: 2;\n white-space: pre;\n scrollbar-width: thin;\n}\n.pre:focus-visible {\n outline: 2px solid var(--color-focus);\n outline-offset: -2px;\n}\n\n.code {\n display: inline-block;\n min-inline-size: 100%;\n font: inherit;\n}\n\n.line {\n display: block;\n position: relative;\n padding-inline: var(--_pad);\n}\n\n.codeBlock[data-line-numbers] .line {\n padding-inline-start: calc(var(--_pad) + 4ch);\n}\n.codeBlock[data-line-numbers] .line::before {\n content: attr(data-line);\n position: absolute;\n inset-inline-start: 0;\n inline-size: calc(var(--_pad) + 2.5ch);\n padding-inline-end: 1ch;\n text-align: end;\n color: var(--color-ink-subtle);\n font-variant-numeric: tabular-nums;\n user-select: none;\n}\n\n.line[data-highlighted] {\n background: var(--codeblock-highlight, var(--color-highlight));\n color: var(--color-highlight-ink);\n}\n.line[data-highlighted]::before {\n color: inherit;\n}\n\n.codeBlock[data-wrap] .pre {\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n}\n.codeBlock[data-wrap] .code {\n display: block;\n}\n\n.visuallyHidden {\n position: absolute;\n inline-size: 1px;\n block-size: 1px;\n margin: -1px;\n padding: 0;\n overflow: hidden;\n clip-path: inset(50%);\n white-space: nowrap;\n border: 0;\n}\n"],"mappings":""}
|
|
1
|
+
{"version":3,"file":"CodeBlock.module.js","names":["codeBlock","header","title","language","copy","pre","code","line","token","visuallyHidden","CodeBlock_module_default"],"sources":["../../../../src/components/display/CodeBlock/CodeBlock.module.css"],"sourcesContent":["/*\n * Component tokens (themes may set any of these on [data-theme]):\n * --codeblock-bg default var(--color-surface-sunken)\n * --codeblock-radius default var(--radius-surface)\n * --codeblock-size default var(--text-xs)\n * --codeblock-highlight highlighted line fill, default var(--color-highlight)\n *\n * One edge: a hairline. The header is separated by the same rule, not a fill.\n */\n/* Zero-specificity reset, so a surrounding flow (Prose, Stack) or a className can space it. */\n:where(.codeBlock) {\n margin: 0;\n}\n.codeBlock {\n --_pad: var(--space-4);\n\n display: flex;\n flex-direction: column;\n min-inline-size: 0;\n overflow: hidden;\n border: var(--border-width) solid var(--color-line);\n border-radius: var(--codeblock-radius, var(--radius-surface));\n background: var(--codeblock-bg, var(--color-surface-sunken));\n color: var(--color-ink);\n}\n\n.header {\n display: flex;\n align-items: center;\n gap: var(--space-3);\n min-block-size: calc(var(--control-sm) + var(--space-2));\n padding-inline: var(--_pad) var(--space-2);\n border-block-end: var(--border-width) solid var(--color-line);\n font-family: var(--font-text);\n font-size: var(--text-xs);\n line-height: var(--leading-snug);\n}\n\n.title {\n min-inline-size: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n color: var(--color-ink);\n font-weight: var(--label-weight);\n}\n\n.language {\n color: var(--color-ink-subtle);\n}\n.title + .language::before {\n content: '·';\n margin-inline-end: var(--space-3);\n}\n\n.copy {\n margin-inline-start: auto;\n color: var(--color-ink-muted);\n}\n.copy[data-state='copied'] {\n color: var(--tone-positive-text);\n}\n.copy[data-state='failed'] {\n color: var(--tone-critical-text);\n}\n\n.pre {\n margin: 0;\n padding-block: var(--space-3);\n overflow-x: auto;\n font-family: var(--font-mono);\n font-stretch: var(--mono-stretch);\n font-size: var(--codeblock-size, var(--text-xs));\n font-variant-ligatures: none;\n line-height: 1.7;\n tab-size: 2;\n white-space: pre;\n scrollbar-width: thin;\n}\n.pre:focus-visible {\n outline: 2px solid var(--color-focus);\n outline-offset: -2px;\n}\n\n.code {\n display: inline-block;\n min-inline-size: 100%;\n font: inherit;\n}\n\n.line {\n display: block;\n position: relative;\n padding-inline: var(--_pad);\n}\n\n.codeBlock[data-line-numbers] .line {\n padding-inline-start: calc(var(--_pad) + 4ch);\n}\n.codeBlock[data-line-numbers] .line::before {\n content: attr(data-line);\n position: absolute;\n inset-inline-start: 0;\n inline-size: calc(var(--_pad) + 2.5ch);\n padding-inline-end: 1ch;\n text-align: end;\n color: var(--color-ink-subtle);\n font-variant-numeric: tabular-nums;\n user-select: none;\n}\n\n/*\n * Highlighted tokens (the `tokens` prop) take the theme's own text colours, so a theme needs no\n * code palette of its own: keywords in the accent, strings, constants and names in the tones.\n */\n.token[data-token='keyword'],\n.token[data-token='tag'] {\n color: var(--color-accent-text);\n}\n.token[data-token='string'] {\n color: var(--tone-positive-text);\n}\n.token[data-token='constant'] {\n color: var(--tone-caution-text);\n}\n.token[data-token='function'],\n.token[data-token='type'] {\n color: var(--tone-info-text);\n}\n.token[data-token='attribute'],\n.token[data-token='punctuation'] {\n color: var(--color-ink-muted);\n}\n.token[data-token='comment'] {\n color: var(--color-ink-subtle);\n}\n\n.line[data-highlighted] {\n background: var(--codeblock-highlight, var(--color-highlight));\n color: var(--color-highlight-ink);\n}\n.line[data-highlighted]::before,\n.line[data-highlighted] .token {\n color: inherit;\n}\n\n.codeBlock[data-wrap] .pre {\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n}\n.codeBlock[data-wrap] .code {\n display: block;\n}\n\n.visuallyHidden {\n position: absolute;\n inline-size: 1px;\n block-size: 1px;\n margin: -1px;\n padding: 0;\n overflow: hidden;\n clip-path: inset(50%);\n white-space: nowrap;\n border: 0;\n}\n"],"mappings":""}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { CodeBlock } from './CodeBlock.js';
|
|
2
|
-
export type { CodeBlockProps } from './CodeBlock.js';
|
|
2
|
+
export type { CodeBlockProps, CodeToken, CodeTokenType } from './CodeBlock.js';
|
|
@@ -2,6 +2,8 @@ import { type HTMLAttributes, type LiHTMLAttributes, type ReactNode } from 'reac
|
|
|
2
2
|
import type { CategoryColor } from '../../../utils/tokens.js';
|
|
3
3
|
export type ListDensity = 'compact' | 'regular';
|
|
4
4
|
export type ListElement = 'ul' | 'ol';
|
|
5
|
+
/** `surface` and `raised` set the list on one sheet, like `Card`'s `outline` and `raised`. */
|
|
6
|
+
export type ListSurface = 'none' | 'surface' | 'raised';
|
|
5
7
|
export interface ListProps extends HTMLAttributes<HTMLUListElement> {
|
|
6
8
|
/** Hairline rules between rows. Default `true`. */
|
|
7
9
|
divided?: boolean;
|
|
@@ -9,6 +11,12 @@ export interface ListProps extends HTMLAttributes<HTMLUListElement> {
|
|
|
9
11
|
density?: ListDensity;
|
|
10
12
|
/** `ol` when order is the content (a leaderboard). Default `ul`. */
|
|
11
13
|
as?: ListElement;
|
|
14
|
+
/**
|
|
15
|
+
* Set the rows on a sheet so they read as rows, not text on the page: `surface` is the
|
|
16
|
+
* surface fill inside a hairline edge, `raised` stands on the theme's surface shadow. Rows run
|
|
17
|
+
* edge to edge, clipped to the sheet's corners. Default `none`.
|
|
18
|
+
*/
|
|
19
|
+
surface?: ListSurface;
|
|
12
20
|
}
|
|
13
21
|
export interface ListItemProps extends Omit<LiHTMLAttributes<HTMLLIElement>, 'color'> {
|
|
14
22
|
/** Hover affordance. Implied by `asChild`. */
|
|
@@ -4,14 +4,15 @@ import { createContext as n, forwardRef as r, useContext as i } from "react";
|
|
|
4
4
|
import { jsx as a } from "react/jsx-runtime";
|
|
5
5
|
import { Slot as o } from "radix-ui";
|
|
6
6
|
//#region src/components/display/List/List.tsx
|
|
7
|
-
var s = r(function({ divided: n = !0, density: r = "regular", as: i = "ul",
|
|
7
|
+
var s = r(function({ divided: n = !0, density: r = "regular", as: i = "ul", surface: o = "none", className: s, ...c }, l) {
|
|
8
8
|
return /* @__PURE__ */ a(i, {
|
|
9
|
-
ref:
|
|
9
|
+
ref: l,
|
|
10
10
|
role: "list",
|
|
11
|
-
className: e(t.list,
|
|
11
|
+
className: e(t.list, s),
|
|
12
12
|
"data-divided": n || void 0,
|
|
13
13
|
"data-density": r,
|
|
14
|
-
|
|
14
|
+
"data-surface": o === "none" ? void 0 : o,
|
|
15
|
+
...c
|
|
15
16
|
});
|
|
16
17
|
}), c = n(!1), l = r(function({ interactive: n = !1, asChild: r = !1, selected: i = !1, highlighted: s = !1, muted: l = !1, color: u, className: d, children: f, ...p }, m) {
|
|
17
18
|
let h = r ? o.Root : "div";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"List.js","names":["styles","List_module_default","ListRoot","forwardRef","Comp","rest","cx","className","divided","density","RowIsControl","createContext","ListItem","Row","asChild","Slot","interactive","selected","highlighted","muted","color","ListLeading","useContext","ListContent","ListDescription","ListTrailing","List"],"sources":["../../../../src/components/display/List/List.tsx"],"sourcesContent":["import {\n createContext,\n forwardRef,\n useContext,\n type HTMLAttributes,\n type LiHTMLAttributes,\n type ReactNode,\n} from 'react'\nimport { Slot } from 'radix-ui'\nimport { cx } from '#utils/cx'\nimport type { CategoryColor } from '#utils/tokens'\nimport styles from './List.module.css'\n\nexport type ListDensity = 'compact' | 'regular'\nexport type ListElement = 'ul' | 'ol'\n\nexport interface ListProps extends HTMLAttributes<HTMLUListElement> {\n /** Hairline rules between rows. Default `true`. */\n divided?: boolean\n /** Row height. Default `regular`. */\n density?: ListDensity\n /** `ol` when order is the content (a leaderboard). Default `ul`. */\n as?: ListElement\n}\n\n/**\n * The workhorse row list. Leading, content and trailing slots, divided by hairlines, never boxed\n * in cards.\n *\n * @remarks\n * `List` is for rows of similar things: trips, people, invoices, recent activity. Each row has up\n * to three slots: `List.Leading` (an avatar, an icon, a rank), `List.Content` (a title and\n * `List.Description`), and `List.Trailing` (an amount, a time, an action). Rows are divided by\n * hairlines.\n *\n * @privateRemarks\n * The workhorse row list: leaderboards, team members, invoices, recent work. Rows are\n * `Leading · Content · Trailing`, divided by hairlines, never boxed in cards.\n *\n * <List><List.Item><List.Leading>1</List.Leading><List.Content>…</List.Content></List.Item></List>\n */\nconst ListRoot = forwardRef<HTMLUListElement, ListProps>(function List(\n { divided = true, density = 'regular', as: Comp = 'ul', className, ...rest },\n ref,\n) {\n return (\n <Comp\n // @ts-expect-error — ul/ol share HTMLOListElement-compatible attributes; ref type differs only nominally\n ref={ref}\n role=\"list\"\n className={cx(styles.list, className)}\n data-divided={divided || undefined}\n data-density={density}\n {...rest}\n />\n )\n})\n\n/**\n * Whether the row is the asChild link or button. Its slots are then spans, the only content a\n * button may hold; elsewhere they're divs, so block components (Stat, Stack) fit in them.\n */\nconst RowIsControl = createContext(false)\n\nexport interface ListItemProps extends Omit<LiHTMLAttributes<HTMLLIElement>, 'color'> {\n /** Hover affordance. Implied by `asChild`. */\n interactive?: boolean\n /**\n * Make the whole row one link or button: the single child becomes the row, so there\n * is exactly one interactive element and no nested anchors.\n * `<List.Item asChild><a href=\"/projects/atlas\">…slots…</a></List.Item>`\n */\n asChild?: boolean\n /** The current item (accent-soft fill) — e.g. the open page in a list of links. */\n selected?: boolean\n /** Draw attention to a row with `--color-highlight` — typically \"you\". */\n highlighted?: boolean\n /** Out of play (eliminated, archived, past): every ink drops to the muted step, which still reads. */\n muted?: boolean\n /** A categorical colour drawn as a rail at the row's start: whose row it is, matching their `Tag`. */\n color?: CategoryColor\n children?: ReactNode\n}\n\nconst ListItem = forwardRef<HTMLLIElement, ListItemProps>(function ListItem(\n {\n interactive = false,\n asChild = false,\n selected = false,\n highlighted = false,\n muted = false,\n color,\n className,\n children,\n ...rest\n },\n ref,\n) {\n const Row = asChild ? Slot.Root : 'div'\n return (\n <li\n ref={ref}\n className={cx(styles.item, className)}\n data-interactive={interactive || asChild || undefined}\n data-selected={selected || undefined}\n data-highlighted={highlighted || undefined}\n data-muted={muted || undefined}\n data-color={color}\n {...rest}\n >\n <RowIsControl.Provider value={asChild}>\n <Row className={styles.row}>{children}</Row>\n </RowIsControl.Provider>\n </li>\n )\n})\n\n/** Rank, avatar or icon at the start of the row. Numbers are tabular and right-aligned. */\nconst ListLeading = forwardRef<HTMLElement, HTMLAttributes<HTMLElement>>(function ListLeading(\n { className, ...rest },\n ref,\n) {\n const Comp = useContext(RowIsControl) ? 'span' : 'div'\n // @ts-expect-error — polymorphic ref across span/div is safe here\n return <Comp ref={ref} className={cx(styles.leading, className)} {...rest} />\n})\n\n/** Title (the direct text) and an optional `List.Description` below it. */\nconst ListContent = forwardRef<HTMLElement, HTMLAttributes<HTMLElement>>(function ListContent(\n { className, ...rest },\n ref,\n) {\n const Comp = useContext(RowIsControl) ? 'span' : 'div'\n // @ts-expect-error — polymorphic ref across span/div is safe here\n return <Comp ref={ref} className={cx(styles.content, className)} {...rest} />\n})\n\nconst ListDescription = forwardRef<HTMLElement, HTMLAttributes<HTMLElement>>(\n function ListDescription({ className, ...rest }, ref) {\n const Comp = useContext(RowIsControl) ? 'span' : 'div'\n // @ts-expect-error — polymorphic ref across span/div is safe here\n return <Comp ref={ref} className={cx(styles.description, className)} {...rest} />\n },\n)\n\n/** Value, meta or actions at the end of the row. Numbers are tabular. */\nconst ListTrailing = forwardRef<HTMLElement, HTMLAttributes<HTMLElement>>(function ListTrailing(\n { className, ...rest },\n ref,\n) {\n const Comp = useContext(RowIsControl) ? 'span' : 'div'\n // @ts-expect-error — polymorphic ref across span/div is safe here\n return <Comp ref={ref} className={cx(styles.trailing, className)} {...rest} />\n})\n\nexport const List = Object.assign(ListRoot, {\n Item: ListItem,\n Leading: ListLeading,\n Content: ListContent,\n Description: ListDescription,\n Trailing: ListTrailing,\n})\n"],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"file":"List.js","names":["styles","List_module_default","ListRoot","forwardRef","Comp","rest","cx","className","divided","density","surface","RowIsControl","createContext","ListItem","Row","asChild","Slot","interactive","selected","highlighted","muted","color","ListLeading","useContext","ListContent","ListDescription","ListTrailing","List"],"sources":["../../../../src/components/display/List/List.tsx"],"sourcesContent":["import {\n createContext,\n forwardRef,\n useContext,\n type HTMLAttributes,\n type LiHTMLAttributes,\n type ReactNode,\n} from 'react'\nimport { Slot } from 'radix-ui'\nimport { cx } from '#utils/cx'\nimport type { CategoryColor } from '#utils/tokens'\nimport styles from './List.module.css'\n\nexport type ListDensity = 'compact' | 'regular'\nexport type ListElement = 'ul' | 'ol'\n/** `surface` and `raised` set the list on one sheet, like `Card`'s `outline` and `raised`. */\nexport type ListSurface = 'none' | 'surface' | 'raised'\n\nexport interface ListProps extends HTMLAttributes<HTMLUListElement> {\n /** Hairline rules between rows. Default `true`. */\n divided?: boolean\n /** Row height. Default `regular`. */\n density?: ListDensity\n /** `ol` when order is the content (a leaderboard). Default `ul`. */\n as?: ListElement\n /**\n * Set the rows on a sheet so they read as rows, not text on the page: `surface` is the\n * surface fill inside a hairline edge, `raised` stands on the theme's surface shadow. Rows run\n * edge to edge, clipped to the sheet's corners. Default `none`.\n */\n surface?: ListSurface\n}\n\n/**\n * The workhorse row list. Leading, content and trailing slots, divided by hairlines, never boxed\n * in cards.\n *\n * @remarks\n * `List` is for rows of similar things: trips, people, invoices, recent activity. Each row has up\n * to three slots: `List.Leading` (an avatar, an icon, a rank), `List.Content` (a title and\n * `List.Description`), and `List.Trailing` (an amount, a time, an action). Rows are divided by\n * hairlines.\n *\n * ## On a surface\n *\n * On a busy page, or a theme whose canvas has a texture, `surface=\"surface\"` sets the whole list\n * on one sheet, and `surface=\"raised\"` stands it on the theme's surface shadow. The rows stay rows\n * on it and run edge to edge: a highlighted, selected or hovered row is a band across the sheet,\n * and a categorical rail is the row's edge, following the sheet's corners at either end.\n *\n * @privateRemarks\n * The workhorse row list: leaderboards, team members, invoices, recent work. Rows are\n * `Leading · Content · Trailing`, divided by hairlines, never boxed in cards.\n *\n * <List><List.Item><List.Leading>1</List.Leading><List.Content>…</List.Content></List.Item></List>\n */\nconst ListRoot = forwardRef<HTMLUListElement, ListProps>(function List(\n { divided = true, density = 'regular', as: Comp = 'ul', surface = 'none', className, ...rest },\n ref,\n) {\n return (\n <Comp\n // @ts-expect-error — ul/ol share HTMLOListElement-compatible attributes; ref type differs only nominally\n ref={ref}\n role=\"list\"\n className={cx(styles.list, className)}\n data-divided={divided || undefined}\n data-density={density}\n data-surface={surface === 'none' ? undefined : surface}\n {...rest}\n />\n )\n})\n\n/**\n * Whether the row is the asChild link or button. Its slots are then spans, the only content a\n * button may hold; elsewhere they're divs, so block components (Stat, Stack) fit in them.\n */\nconst RowIsControl = createContext(false)\n\nexport interface ListItemProps extends Omit<LiHTMLAttributes<HTMLLIElement>, 'color'> {\n /** Hover affordance. Implied by `asChild`. */\n interactive?: boolean\n /**\n * Make the whole row one link or button: the single child becomes the row, so there\n * is exactly one interactive element and no nested anchors.\n * `<List.Item asChild><a href=\"/projects/atlas\">…slots…</a></List.Item>`\n */\n asChild?: boolean\n /** The current item (accent-soft fill) — e.g. the open page in a list of links. */\n selected?: boolean\n /** Draw attention to a row with `--color-highlight` — typically \"you\". */\n highlighted?: boolean\n /** Out of play (eliminated, archived, past): every ink drops to the muted step, which still reads. */\n muted?: boolean\n /** A categorical colour drawn as a rail at the row's start: whose row it is, matching their `Tag`. */\n color?: CategoryColor\n children?: ReactNode\n}\n\nconst ListItem = forwardRef<HTMLLIElement, ListItemProps>(function ListItem(\n {\n interactive = false,\n asChild = false,\n selected = false,\n highlighted = false,\n muted = false,\n color,\n className,\n children,\n ...rest\n },\n ref,\n) {\n const Row = asChild ? Slot.Root : 'div'\n return (\n <li\n ref={ref}\n className={cx(styles.item, className)}\n data-interactive={interactive || asChild || undefined}\n data-selected={selected || undefined}\n data-highlighted={highlighted || undefined}\n data-muted={muted || undefined}\n data-color={color}\n {...rest}\n >\n <RowIsControl.Provider value={asChild}>\n <Row className={styles.row}>{children}</Row>\n </RowIsControl.Provider>\n </li>\n )\n})\n\n/** Rank, avatar or icon at the start of the row. Numbers are tabular and right-aligned. */\nconst ListLeading = forwardRef<HTMLElement, HTMLAttributes<HTMLElement>>(function ListLeading(\n { className, ...rest },\n ref,\n) {\n const Comp = useContext(RowIsControl) ? 'span' : 'div'\n // @ts-expect-error — polymorphic ref across span/div is safe here\n return <Comp ref={ref} className={cx(styles.leading, className)} {...rest} />\n})\n\n/** Title (the direct text) and an optional `List.Description` below it. */\nconst ListContent = forwardRef<HTMLElement, HTMLAttributes<HTMLElement>>(function ListContent(\n { className, ...rest },\n ref,\n) {\n const Comp = useContext(RowIsControl) ? 'span' : 'div'\n // @ts-expect-error — polymorphic ref across span/div is safe here\n return <Comp ref={ref} className={cx(styles.content, className)} {...rest} />\n})\n\nconst ListDescription = forwardRef<HTMLElement, HTMLAttributes<HTMLElement>>(\n function ListDescription({ className, ...rest }, ref) {\n const Comp = useContext(RowIsControl) ? 'span' : 'div'\n // @ts-expect-error — polymorphic ref across span/div is safe here\n return <Comp ref={ref} className={cx(styles.description, className)} {...rest} />\n },\n)\n\n/** Value, meta or actions at the end of the row. Numbers are tabular. */\nconst ListTrailing = forwardRef<HTMLElement, HTMLAttributes<HTMLElement>>(function ListTrailing(\n { className, ...rest },\n ref,\n) {\n const Comp = useContext(RowIsControl) ? 'span' : 'div'\n // @ts-expect-error — polymorphic ref across span/div is safe here\n return <Comp ref={ref} className={cx(styles.trailing, className)} {...rest} />\n})\n\nexport const List = Object.assign(ListRoot, {\n Item: ListItem,\n Leading: ListLeading,\n Content: ListContent,\n Description: ListDescription,\n Trailing: ListTrailing,\n})\n"],"mappings":";;;;;;AAwDA,IAAME,IAAWC,EAAwC,SACvD,EAAE,aAAU,IAAM,aAAU,WAAW,IAAIC,IAAO,MAAM,aAAU,QAAQ,cAAW,GAAGC,KACxF,GACA;CACA,OACE,kBAACD,GAAD;EAEO;EACL,MAAK;EACL,WAAWE,EAAGN,EAAO,MAAMO,CAAS;EACpC,gBAAcC,KAAW,KAAA;EACzB,gBAAcC;EACd,gBAAcC,MAAY,SAAS,KAAA,IAAYA;EAC/C,GAAIL;CACL,CAAA;AAEL,CAAC,GAMKM,IAAeC,EAAc,EAAK,GAsBlCC,IAAWV,EAAyC,SACxD,EACE,iBAAc,IACd,aAAU,IACV,cAAW,IACX,iBAAc,IACd,WAAQ,IACR,UACA,cACA,aACA,GAAGE,KAEL,GACA;CACA,IAAMS,IAAMC,IAAUC,EAAK,OAAO;CAClC,OACE,kBAAC,MAAD;EACO;EACL,WAAWV,EAAGN,EAAO,MAAMO,CAAS;EACpC,oBAAkBU,KAAeF,KAAW,KAAA;EAC5C,iBAAeG,KAAY,KAAA;EAC3B,oBAAkBC,KAAe,KAAA;EACjC,cAAYC,KAAS,KAAA;EACrB,cAAYC;EACZ,GAAIhB;EAEJ,UAAA,kBAACM,EAAa,UAAd;GAAuB,OAAOI;GAC5B,UAAA,kBAACD,GAAD;IAAK,WAAWd,EAAO;IAAM;GAAc,CAAA;EACtB,CAAA;CACrB,CAAA;AAER,CAAC,GAGKsB,IAAcnB,EAAqD,SACvE,EAAE,cAAW,GAAGE,KAChB,GACA;CACA,IAAMD,IAAOmB,EAAWZ,CAAY,IAAI,SAAS;CAEjD,OAAO,kBAACP,GAAD;EAAW;EAAK,WAAWE,EAAGN,EAAO,SAASO,CAAS;EAAG,GAAIF;CAAO,CAAA;AAC9E,CAAC,GAGKmB,IAAcrB,EAAqD,SACvE,EAAE,cAAW,GAAGE,KAChB,GACA;CACA,IAAMD,IAAOmB,EAAWZ,CAAY,IAAI,SAAS;CAEjD,OAAO,kBAACP,GAAD;EAAW;EAAK,WAAWE,EAAGN,EAAO,SAASO,CAAS;EAAG,GAAIF;CAAO,CAAA;AAC9E,CAAC,GAEKoB,IAAkBtB,EACtB,SAAyB,EAAE,cAAW,GAAGE,KAAQ,GAAK;CACpD,IAAMD,IAAOmB,EAAWZ,CAAY,IAAI,SAAS;CAEjD,OAAO,kBAACP,GAAD;EAAW;EAAK,WAAWE,EAAGN,EAAO,aAAaO,CAAS;EAAG,GAAIF;CAAO,CAAA;AAClF,CACF,GAGMqB,IAAevB,EAAqD,SACxE,EAAE,cAAW,GAAGE,KAChB,GACA;CACA,IAAMD,IAAOmB,EAAWZ,CAAY,IAAI,SAAS;CAEjD,OAAO,kBAACP,GAAD;EAAW;EAAK,WAAWE,EAAGN,EAAO,UAAUO,CAAS;EAAG,GAAIF;CAAO,CAAA;AAC/E,CAAC,GAEYsB,IAAO,OAAO,OAAOzB,GAAU;CAC1C,MAAMW;CACN,SAASS;CACT,SAASE;CACT,aAAaC;CACb,UAAUC;AACZ,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"List.module.js","names":["list","item","row","leading","content","description","trailing","List_module_default"],"sources":["../../../../src/components/display/List/List.module.css"],"sourcesContent":["/*\n * Component tokens (themes may set any of these on [data-theme]):\n * --list-inset default var(--space-2) — row padding-inline
|
|
1
|
+
{"version":3,"file":"List.module.js","names":["list","item","row","leading","content","description","trailing","List_module_default"],"sources":["../../../../src/components/display/List/List.module.css"],"sourcesContent":["/*\n * Component tokens (themes may set any of these on [data-theme]):\n * --list-inset default var(--space-2), var(--space-4) on a surface — row padding-inline\n * --list-row-radius default var(--radius-field), square on a surface — row fills\n * --list-trailing-size default: the row's text size (same as the title)\n * --list-rail-width default var(--space-1) — the categorical rail of a `color` row\n * --list-surface-bg default var(--color-surface), var(--color-surface-raised) when `raised`\n * --list-surface-radius default var(--radius-surface) — the sheet of a `surface` list\n * --highlight-tone-{positive,caution,critical,info}-text, --highlight-accent-text\n * tone text inside a highlighted row (shared with Table)\n *\n * Structure: li.item > .row (a div, or the asChild link/button) > slots. The row carries\n * padding, hover and focus so a linked row is one target edge to edge. Slots are divs, so\n * block components (Stat, Stack, a sized Media) fit in them, and spans inside an asChild row,\n * the only content a button may hold.\n */\n.list {\n --_inset: var(--space-2);\n --_row-radius: var(--radius-field);\n\n display: flex;\n flex-direction: column;\n margin: 0;\n padding: 0;\n list-style: none;\n font-family: var(--font-text);\n color: var(--color-ink);\n}\n\n/*\n * On a surface: the whole list on one sheet, with Card's edges (`outline` for surface,\n * `raised` for raised), so rows read as rows on a textured or busy canvas. Rows run edge to\n * edge and the sheet clips them to its radius, so a fill is a band across the sheet, and a\n * categorical rail sits flush with the sheet's inner edge and follows its corners on the\n * first and last rows. Rows take a wider inset so text clears the edge.\n */\n.list[data-surface] {\n --_inset: var(--space-4);\n --_row-radius: 0px;\n\n overflow: hidden;\n border: var(--border-width) solid var(--color-line);\n border-radius: var(--list-surface-radius, var(--radius-surface));\n background: var(--list-surface-bg, var(--color-surface));\n}\n.list[data-surface='raised'] {\n border-color: var(--color-line-strong);\n background: var(--list-surface-bg, var(--color-surface-raised));\n box-shadow: var(--shadow-surface);\n}\n\n.item {\n --_py: var(--space-3);\n --_fill: transparent;\n --_fill-ink: inherit;\n\n position: relative;\n min-inline-size: 0;\n}\n.list[data-density='compact'] > .item {\n --_py: var(--space-2);\n}\n\n.list[data-divided] > .item + .item {\n border-block-start: var(--border-width) solid var(--color-line);\n}\n/* A filled row doesn't need rules either side — the fill is its edge. */\n.list[data-divided] > .item:is([data-highlighted], [data-selected]),\n.list[data-divided] > .item:is([data-highlighted], [data-selected]) + .item {\n border-block-start-color: transparent;\n}\n\n.row {\n display: flex;\n align-items: center;\n gap: var(--space-3);\n inline-size: 100%;\n min-inline-size: 0;\n margin: 0;\n padding-block: var(--_py);\n padding-inline: var(--list-inset, var(--_inset));\n border: 0;\n border-radius: var(--list-row-radius, var(--_row-radius));\n background: var(--_fill);\n color: var(--_fill-ink);\n font: inherit;\n text-align: start;\n text-decoration: none;\n transition: background-color var(--dur-1) var(--ease-out);\n}\n\n.item[data-selected] {\n --_fill: var(--color-accent-soft);\n --_fill-ink: var(--color-ink);\n}\n.item[data-highlighted] {\n --_fill: var(--color-highlight);\n --_fill-ink: var(--color-highlight-ink);\n --_band-positive: var(\n --highlight-tone-positive-text,\n color-mix(in oklab, var(--tone-positive-text) 75%, var(--color-highlight-ink))\n );\n --_band-caution: var(\n --highlight-tone-caution-text,\n color-mix(in oklab, var(--tone-caution-text) 75%, var(--color-highlight-ink))\n );\n --_band-critical: var(\n --highlight-tone-critical-text,\n color-mix(in oklab, var(--tone-critical-text) 75%, var(--color-highlight-ink))\n );\n --_band-info: var(\n --highlight-tone-info-text,\n color-mix(in oklab, var(--tone-info-text) 75%, var(--color-highlight-ink))\n );\n --_band-accent: var(\n --highlight-accent-text,\n color-mix(in oklab, var(--color-accent-text) 75%, var(--color-highlight-ink))\n );\n}\n\n/*\n * A filled row re-points the colour roles for its content, so a muted description, a\n * tone-coloured Numeral or a LiveIndicator stays legible on the fill (gold in Fiesta\n * night). The item publishes fill/ink under private names and the row swaps the roles —\n * redefining a role from itself on one element would be a var() cycle (see Section).\n */\n.item:is([data-highlighted], [data-selected]) > .row {\n --color-ink: var(--_fill-ink);\n --color-ink-muted: color-mix(in oklab, var(--_fill-ink), var(--_fill) 25%);\n --color-ink-subtle: color-mix(in oklab, var(--_fill-ink), var(--_fill) 35%);\n}\n.item[data-highlighted] > .row {\n --tone-positive-text: var(--_band-positive);\n --tone-caution-text: var(--_band-caution);\n --tone-critical-text: var(--_band-critical);\n --tone-info-text: var(--_band-info);\n --color-accent-text: var(--_band-accent);\n /* Soft fills (a soft Badge or Tag) tint the band, like Section's accent band, so the\n re-pointed tone texts above stay legible on them in every mode. */\n --tone-positive-soft: color-mix(in oklab, var(--_band-positive) 8%, var(--_fill));\n --tone-caution-soft: color-mix(in oklab, var(--_band-caution) 8%, var(--_fill));\n --tone-critical-soft: color-mix(in oklab, var(--_band-critical) 8%, var(--_fill));\n --tone-info-soft: color-mix(in oklab, var(--_band-info) 8%, var(--_fill));\n --color-accent-soft: color-mix(in oklab, var(--_band-accent) 8%, var(--_fill));\n}\n\n.item[data-interactive] > .row {\n cursor: pointer;\n}\n\n/* Muted: out of play. The title drops to the muted step, which still clears AA. */\n.item[data-muted] > .row {\n --color-ink: var(--color-ink-muted);\n\n color: var(--color-ink-muted);\n}\n\n/* A categorical rail in the row's start inset: whose row it is, in their Tag's colour. */\n.item[data-color]::before {\n content: '';\n position: absolute;\n inset-block: var(--space-1);\n inset-inline-start: 0;\n inline-size: var(--list-rail-width, var(--space-1));\n border-radius: var(--radius-chip);\n background: var(--_cat);\n}\n/* On a sheet the rail is an edge of the row: full height, square, and over the rule above it. */\n.list[data-surface] > .item[data-color]::before {\n inset-block: 0;\n border-radius: 0;\n}\n.list[data-surface][data-divided] > .item + .item[data-color]::before {\n inset-block-start: calc(var(--border-width) * -1);\n}\n.item[data-color='1'] {\n --_cat: var(--color-cat-1);\n}\n.item[data-color='2'] {\n --_cat: var(--color-cat-2);\n}\n.item[data-color='3'] {\n --_cat: var(--color-cat-3);\n}\n.item[data-color='4'] {\n --_cat: var(--color-cat-4);\n}\n.item[data-color='5'] {\n --_cat: var(--color-cat-5);\n}\n.item[data-color='6'] {\n --_cat: var(--color-cat-6);\n}\n.item[data-color='7'] {\n --_cat: var(--color-cat-7);\n}\n.item[data-color='8'] {\n --_cat: var(--color-cat-8);\n}\n.item[data-color] > .row {\n padding-inline-start: calc(var(--list-inset, var(--_inset)) + var(--space-2));\n}\n@media (hover: hover) {\n .item[data-interactive]:not([data-highlighted], [data-selected]) > .row:hover {\n background: var(--color-surface-sunken);\n }\n .item[data-interactive]:is([data-highlighted], [data-selected]) > .row:hover {\n background: color-mix(in oklch, var(--_fill) 88%, var(--color-ink));\n }\n}\n.row:focus-visible {\n outline: 2px solid var(--color-focus);\n outline-offset: -2px;\n}\n\n/* ── Slots ───────────────────────────────────────────────────────────────── */\n\n.leading {\n display: inline-flex;\n flex: none;\n align-items: center;\n justify-content: flex-end;\n min-inline-size: 2ch;\n color: var(--color-ink-muted);\n font-family: var(--font-numeric);\n font-stretch: var(--numeric-stretch);\n font-weight: var(--label-weight);\n font-variant-numeric: tabular-nums lining-nums;\n font-size: var(--text-md);\n}\n.item[data-highlighted] .leading {\n color: inherit;\n}\n\n.content {\n display: flex;\n flex: 1 1 auto;\n flex-direction: column;\n gap: calc(var(--space-1) / 2);\n min-inline-size: 0;\n font-size: var(--text-md);\n font-weight: var(--label-weight);\n line-height: var(--leading-snug);\n /* Wrap between words; break inside one only when a single word can't fit the column. */\n overflow-wrap: break-word;\n}\n\n.description {\n color: var(--color-ink-muted);\n font-size: var(--text-sm);\n font-weight: 400;\n}\n\n.trailing {\n --stat-align: end;\n --stat-container-type: normal;\n\n display: inline-flex;\n flex: none;\n align-items: center;\n justify-content: flex-end;\n gap: var(--space-2);\n margin-inline-start: auto;\n text-align: end;\n /*\n * The row's text face and size, with tabular figures: a bare \"7 tasks\" sits level with the\n * title instead of looming in the numeric face. Put a <Numeral>/<Amount> here for figures\n * that should carry the numeric face (receipt mono in Ledger).\n */\n font-variant-numeric: tabular-nums lining-nums;\n font-size: var(--list-trailing-size, var(--text-md));\n font-weight: var(--label-weight);\n line-height: var(--leading-snug);\n white-space: nowrap;\n}\n"],"mappings":""}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { List } from './List.js';
|
|
2
|
-
export type { ListProps, ListItemProps, ListDensity, ListElement } from './List.js';
|
|
2
|
+
export type { ListProps, ListItemProps, ListDensity, ListElement, ListSurface } from './List.js';
|
|
@@ -17,6 +17,8 @@ export interface MediaProps extends Omit<HTMLAttributes<HTMLElement>, 'children'
|
|
|
17
17
|
/**
|
|
18
18
|
* A fixed height, for a small image that sits inline beside text (a flag by a name, a logo
|
|
19
19
|
* in a row). The width follows `ratio`, or the image's own ratio when `ratio` is `auto`.
|
|
20
|
+
* With a fixed `ratio`, the image also gets `width` and `height` attributes, so its box is
|
|
21
|
+
* known before it loads; with `auto`, pass them in `imgProps`.
|
|
20
22
|
* Default: none, so the frame fills its container.
|
|
21
23
|
*/
|
|
22
24
|
size?: MediaSize;
|
|
@@ -3,49 +3,65 @@ import t from "./Media.module.js";
|
|
|
3
3
|
import { forwardRef as n, useEffect as r, useRef as i, useState as a } from "react";
|
|
4
4
|
import { jsx as o, jsxs as s } from "react/jsx-runtime";
|
|
5
5
|
//#region src/components/display/Media/Media.tsx
|
|
6
|
-
var c =
|
|
7
|
-
|
|
6
|
+
var c = {
|
|
7
|
+
xs: 16,
|
|
8
|
+
sm: 20,
|
|
9
|
+
md: 24,
|
|
10
|
+
lg: 32,
|
|
11
|
+
xl: 96
|
|
12
|
+
};
|
|
13
|
+
function l(e, t) {
|
|
14
|
+
if (e === void 0 || t === "auto") return {};
|
|
15
|
+
let [n, r] = t.split("/").map(Number), i = c[e];
|
|
16
|
+
return {
|
|
17
|
+
width: Math.round(i * n / r),
|
|
18
|
+
height: i
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
var u = n(function({ src: n, alt: c, ratio: u = "auto", fit: d = "cover", radius: f = "media", size: p, dimmed: m = !1, caption: h, fallback: g, loading: _ = "lazy", imgProps: v, className: y, ...b }, x) {
|
|
22
|
+
let [S, C] = a(!1), w = i(null);
|
|
8
23
|
r(() => {
|
|
9
|
-
let e =
|
|
10
|
-
|
|
24
|
+
let e = w.current;
|
|
25
|
+
C(!!(e?.complete && e.naturalWidth === 0 && typeof e.currentSrc == "string" && e.currentSrc !== ""));
|
|
11
26
|
}, [n]);
|
|
12
|
-
let
|
|
13
|
-
return /* @__PURE__ */ s(
|
|
14
|
-
ref:
|
|
15
|
-
className: e(t.media,
|
|
16
|
-
"data-ratio":
|
|
17
|
-
"data-fit":
|
|
18
|
-
"data-radius":
|
|
19
|
-
"data-size":
|
|
20
|
-
"data-dimmed":
|
|
21
|
-
"data-failed":
|
|
22
|
-
...
|
|
23
|
-
children: [/* @__PURE__ */ o(
|
|
27
|
+
let T = h === void 0 ? p ? "span" : "div" : "figure", E = p ? "span" : "div";
|
|
28
|
+
return /* @__PURE__ */ s(T, {
|
|
29
|
+
ref: x,
|
|
30
|
+
className: e(t.media, y),
|
|
31
|
+
"data-ratio": u,
|
|
32
|
+
"data-fit": d,
|
|
33
|
+
"data-radius": f,
|
|
34
|
+
"data-size": p,
|
|
35
|
+
"data-dimmed": m || void 0,
|
|
36
|
+
"data-failed": S || void 0,
|
|
37
|
+
...b,
|
|
38
|
+
children: [/* @__PURE__ */ o(E, {
|
|
24
39
|
className: t.frame,
|
|
25
|
-
children:
|
|
40
|
+
children: S ? /* @__PURE__ */ o(E, {
|
|
26
41
|
className: t.fallback,
|
|
27
42
|
role: c ? "img" : void 0,
|
|
28
43
|
"aria-label": c || void 0,
|
|
29
|
-
children:
|
|
44
|
+
children: g
|
|
30
45
|
}) : /* @__PURE__ */ o("img", {
|
|
31
|
-
ref:
|
|
46
|
+
ref: w,
|
|
32
47
|
className: t.image,
|
|
33
48
|
src: n,
|
|
34
49
|
alt: c,
|
|
35
|
-
loading:
|
|
50
|
+
loading: _,
|
|
36
51
|
decoding: "async",
|
|
37
52
|
onError: () => {
|
|
38
|
-
|
|
53
|
+
C(!0);
|
|
39
54
|
},
|
|
40
|
-
...
|
|
55
|
+
...l(p, u),
|
|
56
|
+
...v
|
|
41
57
|
})
|
|
42
|
-
}),
|
|
58
|
+
}), h === void 0 ? null : /* @__PURE__ */ o("figcaption", {
|
|
43
59
|
className: t.caption,
|
|
44
|
-
children:
|
|
60
|
+
children: h
|
|
45
61
|
})]
|
|
46
62
|
});
|
|
47
63
|
});
|
|
48
64
|
//#endregion
|
|
49
|
-
export {
|
|
65
|
+
export { u as Media };
|
|
50
66
|
|
|
51
67
|
//# sourceMappingURL=Media.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Media.js","names":["styles","Media_module_default","Media","forwardRef","rest","failed","setFailed","useState","imgRef","useRef","img","src","Root","caption","
|
|
1
|
+
{"version":3,"file":"Media.js","names":["styles","Media_module_default","SIZE_PX","sizeHints","size","ratio","w","h","height","Media","forwardRef","rest","failed","setFailed","useState","imgRef","useRef","img","src","Root","caption","Box","cx","className","fit","radius","dimmed","alt","fallback","imgProps"],"sources":["../../../../src/components/display/Media/Media.tsx"],"sourcesContent":["import {\n forwardRef,\n useEffect,\n useRef,\n useState,\n type HTMLAttributes,\n type ImgHTMLAttributes,\n type ReactNode,\n} from 'react'\nimport { cx } from '#utils/cx'\nimport styles from './Media.module.css'\n\nexport type MediaRatio = '1/1' | '4/3' | '3/2' | '16/9' | '21/9' | '3/4' | '2/3' | 'auto'\nexport type MediaFit = 'cover' | 'contain'\nexport type MediaRadius = 'none' | 'media' | 'surface'\n/** Fixed heights for inline thumbnails: about 16, 20, 24, 32 and 96px. */\nexport type MediaSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl'\n\n/** Nominal heights in px, for the image's width/height hints; the CSS sets the real size. */\nconst SIZE_PX: Record<MediaSize, number> = { xs: 16, sm: 20, md: 24, lg: 32, xl: 96 }\n\n/**\n * The width and height attributes a sized image carries, so the browser knows its box before it\n * loads (and audits don't flag it). Only known with a fixed ratio: an `auto` frame takes the\n * image's own ratio, so pass `imgProps.width`/`height` there.\n */\nfunction sizeHints(size: MediaSize | undefined, ratio: MediaRatio) {\n if (size === undefined || ratio === 'auto') return {}\n const [w, h] = ratio.split('/').map(Number) as [number, number]\n const height = SIZE_PX[size]\n return { width: Math.round((height * w) / h), height }\n}\n\nexport interface MediaProps extends Omit<HTMLAttributes<HTMLElement>, 'children'> {\n src: string\n /** Required. Describe the image; pass `\"\"` only when it's purely decorative. */\n alt: string\n /** Frame ratio. The image is fitted inside. Default `auto` (the image's own ratio). */\n ratio?: MediaRatio\n /** Default `cover`. Use `contain` for logos, diagrams and screenshots that must not crop. */\n fit?: MediaFit\n /** Corner role. Default `media`. */\n radius?: MediaRadius\n /**\n * A fixed height, for a small image that sits inline beside text (a flag by a name, a logo\n * in a row). The width follows `ratio`, or the image's own ratio when `ratio` is `auto`.\n * With a fixed `ratio`, the image also gets `width` and `height` attributes, so its box is\n * known before it loads; with `auto`, pass them in `imgProps`.\n * Default: none, so the frame fills its container.\n */\n size?: MediaSize\n /**\n * Out of play: the image fades toward grey but stays recognisable, like a knocked-out\n * team's flag or a past event's photo. Pair it with a muted `List.Item` or `Table.Row`.\n */\n dimmed?: boolean\n /** Renders a `<figure>` with this as its `<figcaption>`. */\n caption?: ReactNode\n /** Shown inside the frame if the image fails to load. Default: an empty sunken frame. */\n fallback?: ReactNode\n /** Default `lazy`. Use `eager` for the image above the fold. */\n loading?: 'lazy' | 'eager'\n /** Extra attributes for the `<img>` (srcSet, sizes, width/height, fetchPriority). */\n imgProps?: Omit<ImgHTMLAttributes<HTMLImageElement>, 'src' | 'alt' | 'loading'>\n}\n\n/**\n * A framed image with a fixed ratio, lazy loading, a caption, and a calm failure state.\n *\n * @remarks\n * `Media` frames an image: it holds a ratio while loading, fits the image, loads lazily, and when\n * the image fails it keeps its shape and shows a quiet fallback instead of a broken-image icon.\n *\n * @privateRemarks\n * A framed image: fixed ratio, fitted, lazy by default, with a caption and a graceful\n * failure state. Screenshots, company logos, scanned documents.\n *\n * <Media src=\"/images/dashboard.png\" alt=\"Revenue dashboard, September\" ratio=\"16/9\" caption=\"Dashboard, 2026\" />\n */\nexport const Media = forwardRef<HTMLElement, MediaProps>(function Media(\n {\n src,\n alt,\n ratio = 'auto',\n fit = 'cover',\n radius = 'media',\n size,\n dimmed = false,\n caption,\n fallback,\n loading = 'lazy',\n imgProps,\n className,\n ...rest\n },\n ref,\n) {\n const [failed, setFailed] = useState(false)\n const imgRef = useRef<HTMLImageElement>(null)\n\n // Reset when the source changes; also catch an error that fired before hydration.\n useEffect(() => {\n const img = imgRef.current\n setFailed(\n Boolean(\n img?.complete &&\n img.naturalWidth === 0 &&\n typeof img.currentSrc === 'string' &&\n img.currentSrc !== '',\n ),\n )\n }, [src])\n\n // A sized thumbnail is phrasing content (spans), so it can sit in a button or a line of text.\n const Root = caption !== undefined ? 'figure' : size ? 'span' : 'div'\n const Box = size ? 'span' : 'div'\n return (\n <Root\n // @ts-expect-error — polymorphic ref across figure/div is safe here\n ref={ref}\n className={cx(styles.media, className)}\n data-ratio={ratio}\n data-fit={fit}\n data-radius={radius}\n data-size={size}\n data-dimmed={dimmed || undefined}\n data-failed={failed || undefined}\n {...rest}\n >\n <Box className={styles.frame}>\n {failed ? (\n <Box\n className={styles.fallback}\n role={alt ? 'img' : undefined}\n aria-label={alt || undefined}\n >\n {fallback}\n </Box>\n ) : (\n <img\n ref={imgRef}\n className={styles.image}\n src={src}\n alt={alt}\n loading={loading}\n decoding=\"async\"\n onError={() => {\n setFailed(true)\n }}\n {...sizeHints(size, ratio)}\n {...imgProps}\n />\n )}\n </Box>\n {caption !== undefined ? <figcaption className={styles.caption}>{caption}</figcaption> : null}\n </Root>\n )\n})\n"],"mappings":";;;;;AAmBA,IAAME,IAAqC;CAAE,IAAI;CAAI,IAAI;CAAI,IAAI;CAAI,IAAI;CAAI,IAAI;AAAG;AAOpF,SAASC,EAAU,GAA6B,GAAmB;CACjE,IAAIC,MAAS,KAAA,KAAaC,MAAU,QAAQ,OAAO,CAAC;CACpD,IAAM,CAACC,GAAGC,KAAKF,EAAM,MAAM,GAAG,CAAC,CAAC,IAAI,MAAM,GACpCG,IAASN,EAAQE;CACvB,OAAO;EAAE,OAAO,KAAK,MAAOI,IAASF,IAAKC,CAAC;EAAG;CAAO;AACvD;AAgDA,IAAaE,IAAQC,EAAoC,SACvD,EACE,QACA,QACA,WAAQ,QACR,SAAM,SACN,YAAS,SACT,SACA,YAAS,IACT,YACA,aACA,aAAU,QACV,aACA,cACA,GAAGC,KAEL,GACA;CACA,IAAM,CAACC,GAAQC,KAAaC,EAAS,EAAK,GACpCC,IAASC,EAAyB,IAAI;CAG5C,QAAgB;EACd,IAAMC,IAAMF,EAAO;EACnB,EACE,GACEE,GAAK,YACLA,EAAI,iBAAiB,KACrB,OAAOA,EAAI,cAAe,YAC1BA,EAAI,eAAe,GAEvB;CACF,GAAG,CAACC,CAAG,CAAC;CAGR,IAAMC,IAAOC,MAAY,KAAA,IAAuBhB,IAAO,SAAS,QAA3B,UAC/BiB,IAAMjB,IAAO,SAAS;CAC5B,OACE,kBAACe,GAAD;EAEO;EACL,WAAWG,EAAGtB,EAAO,OAAOuB,CAAS;EACrC,cAAYlB;EACZ,YAAUmB;EACV,eAAaC;EACb,aAAWrB;EACX,eAAasB,KAAU,KAAA;EACvB,eAAad,KAAU,KAAA;EACvB,GAAID;EAVN,UAAA,CAYE,kBAACU,GAAD;GAAK,WAAWrB,EAAO;GACpB,UAAAY,IACC,kBAACS,GAAD;IACE,WAAWrB,EAAO;IAClB,MAAM2B,IAAM,QAAQ,KAAA;IACpB,cAAYA,KAAO,KAAA;IAElB,UAAAC;GACE,CAAA,IAEL,kBAAC,OAAD;IACE,KAAKb;IACL,WAAWf,EAAO;IACb;IACA;IACI;IACT,UAAS;IACT,eAAe;KACb,EAAU,EAAI;IAChB;IACA,GAAIG,EAAUC,GAAMC,CAAK;IACzB,GAAIwB;GACL,CAAA;EAEA,CAAA,GACJT,MAAY,KAAA,IAA4E,OAAhE,kBAAC,cAAD;GAAY,WAAWpB,EAAO;GAAU,UAAAoB;EAAoB,CAAA,CACjF;;AAEV,CAAC"}
|