@anyknown/ui 0.6.1 → 0.8.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.
Files changed (70) hide show
  1. package/README.md +6 -7
  2. package/dist/brand.css +111 -124
  3. package/dist/components/badge/Badge.d.ts +11 -3
  4. package/dist/components/badge/Badge.js +21 -3
  5. package/dist/components/button/Button.d.ts +3 -2
  6. package/dist/components/button/Button.js +32 -48
  7. package/dist/components/checkbox/Checkbox.js +24 -48
  8. package/dist/components/data-table/DataTable.d.ts +7 -1
  9. package/dist/components/data-table/DataTable.js +34 -34
  10. package/dist/components/dialog/Dialog.d.ts +6 -1
  11. package/dist/components/dialog/Dialog.js +11 -3
  12. package/dist/components/empty-state/EmptyState.js +1 -1
  13. package/dist/components/ghost/Ghost.d.ts +16 -0
  14. package/dist/components/ghost/Ghost.js +50 -0
  15. package/dist/components/group/Group.d.ts +66 -0
  16. package/dist/components/group/Group.js +166 -0
  17. package/dist/components/handoff-receipt/HandoffReceipt.js +12 -42
  18. package/dist/components/icon/Chevron.d.ts +9 -0
  19. package/dist/components/icon/Chevron.js +6 -0
  20. package/dist/components/icon/icon.d.ts +35 -0
  21. package/dist/components/icon/icon.js +10 -0
  22. package/dist/components/icon-button/IconButton.d.ts +17 -0
  23. package/dist/components/icon-button/IconButton.js +66 -0
  24. package/dist/components/live-dot/LiveDot.d.ts +7 -0
  25. package/dist/components/live-dot/LiveDot.js +35 -0
  26. package/dist/components/message/Message.d.ts +3 -1
  27. package/dist/components/message/Message.js +2 -2
  28. package/dist/components/page/Page.d.ts +64 -0
  29. package/dist/components/page/Page.js +163 -0
  30. package/dist/components/popover/Popover.js +1 -1
  31. package/dist/components/progress/Progress.js +49 -104
  32. package/dist/components/radio/Radio.js +17 -28
  33. package/dist/components/segmented/Segmented.d.ts +12 -0
  34. package/dist/components/segmented/Segmented.js +43 -0
  35. package/dist/components/settings-rows/SettingsRows.d.ts +25 -0
  36. package/dist/components/settings-rows/SettingsRows.js +63 -0
  37. package/dist/components/slider/Slider.d.ts +19 -0
  38. package/dist/components/slider/Slider.js +103 -0
  39. package/dist/components/spin/Spin.d.ts +5 -0
  40. package/dist/components/spin/Spin.js +25 -0
  41. package/dist/components/status-chip/StatusChip.d.ts +18 -0
  42. package/dist/components/status-chip/StatusChip.js +68 -0
  43. package/dist/components/switch/Switch.js +20 -63
  44. package/dist/components/table/Table.d.ts +39 -0
  45. package/dist/components/table/Table.js +125 -0
  46. package/dist/components/table/TableCells.d.ts +52 -0
  47. package/dist/components/table/TableCells.js +103 -0
  48. package/dist/components/tabs/Tabs.js +16 -48
  49. package/dist/components/text/Text.js +2 -2
  50. package/dist/components/toast/Toast.js +7 -8
  51. package/dist/index.d.ts +14 -0
  52. package/dist/index.js +14 -0
  53. package/dist/lib/layers.d.ts +7 -0
  54. package/dist/lib/layers.js +12 -0
  55. package/dist/themes.stylex.d.ts +54 -206
  56. package/dist/themes.stylex.js +55 -157
  57. package/dist/tokens.css +63 -46
  58. package/dist/tokens.stylex.d.ts +41 -54
  59. package/dist/tokens.stylex.js +64 -85
  60. package/package.json +2 -2
  61. package/dist/lib/paths.d.ts +0 -3
  62. package/dist/lib/paths.js +0 -11
  63. package/dist/lib/progress.d.ts +0 -2
  64. package/dist/lib/progress.js +0 -28
  65. package/dist/lib/silk.d.ts +0 -66
  66. package/dist/lib/silk.js +0 -337
  67. package/dist/lib/svgId.d.ts +0 -1
  68. package/dist/lib/svgId.js +0 -4
  69. package/dist/lib/weave.d.ts +0 -28
  70. package/dist/lib/weave.js +0 -83
@@ -0,0 +1,163 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import * as stylex from "@stylexjs/stylex";
3
+ import { color, corner, font, space, tone, type } from "../../tokens.stylex.js";
4
+ /**
5
+ * The words around the rows: the page's one line (`.page-h`), the label a section wears
6
+ * (`.slab`), the sentence under a group (`.sub` / `.foot-note`), a code block, a stat line.
7
+ */
8
+ const styles = stylex.create({
9
+ head: {
10
+ alignItems: "baseline",
11
+ color: color.text,
12
+ display: "flex",
13
+ flexWrap: "wrap",
14
+ gap: space.sm,
15
+ lineHeight: type.body,
16
+ marginBlock: `0 ${space.xs}`,
17
+ },
18
+ h1: { fontSize: type.t4, fontWeight: 600, lineHeight: type.tight, margin: 0 },
19
+ sp: { flex: 1 },
20
+ summary: { color: color.textMuted, fontSize: type.t2, minWidth: 0 },
21
+ b: { color: color.text, fontFamily: font.mono, fontWeight: 400 },
22
+ slab: {
23
+ alignItems: "center",
24
+ color: color.textMuted,
25
+ display: "flex",
26
+ fontFamily: font.mono,
27
+ fontSize: type.t1,
28
+ gap: space.xs,
29
+ lineHeight: type.body,
30
+ marginBlock: `${space.lg} ${space.xs}`,
31
+ },
32
+ slabFirst: { marginBlockStart: space.md },
33
+ n: { marginInlineStart: "auto" },
34
+ sub: {
35
+ color: color.textMuted,
36
+ fontSize: type.t2,
37
+ lineHeight: type.body,
38
+ marginBlock: `${space.sm} 0`,
39
+ },
40
+ foot: {
41
+ color: color.textMuted,
42
+ fontSize: type.t2,
43
+ lineHeight: type.body,
44
+ marginBlock: `${space.xs} 0`,
45
+ },
46
+ hint: {
47
+ color: color.textMuted,
48
+ fontSize: type.t2,
49
+ lineHeight: type.body,
50
+ marginBlock: `0 ${space.xs}`,
51
+ },
52
+ code: {
53
+ backgroundColor: color.layer3,
54
+ borderRadius: corner.sm,
55
+ color: color.textMuted,
56
+ fontFamily: font.mono,
57
+ fontSize: type.t1,
58
+ lineHeight: 1.6,
59
+ margin: 0,
60
+ maxWidth: "44rem",
61
+ overflowWrap: "break-word",
62
+ paddingBlock: space.xs,
63
+ paddingInline: space.sm,
64
+ whiteSpace: "pre-wrap",
65
+ wordBreak: "break-word",
66
+ },
67
+ panel: {
68
+ backgroundColor: color.surface,
69
+ borderRadius: corner.card,
70
+ boxShadow: `inset 0 0 0 1px ${color.border}`,
71
+ color: color.text,
72
+ fontSize: type.t2,
73
+ lineHeight: type.body,
74
+ },
75
+ padded: { display: "flex", flexDirection: "column", gap: space.xs, padding: space.sm },
76
+ stat: {
77
+ alignItems: "center",
78
+ color: color.textMuted,
79
+ display: "flex",
80
+ flexWrap: "wrap",
81
+ fontSize: type.t2,
82
+ gap: space.xs,
83
+ lineHeight: type.body,
84
+ },
85
+ sbar: {
86
+ backgroundColor: color.layer4,
87
+ borderRadius: 2,
88
+ flex: "none",
89
+ height: 4,
90
+ overflow: "hidden",
91
+ width: "8rem",
92
+ },
93
+ sfill: (percent) => ({
94
+ backgroundColor: color.accent,
95
+ display: "block",
96
+ height: "100%",
97
+ width: `${percent}%`,
98
+ }),
99
+ bars: { alignItems: "flex-end", display: "flex", gap: 2, height: "6rem" },
100
+ bar: {
101
+ backgroundColor: {
102
+ default: `color-mix(in srgb, ${color.accent} 60%, transparent)`,
103
+ ":hover": color.accent,
104
+ },
105
+ borderRadius: "2px 2px 0 0",
106
+ flex: 1,
107
+ minWidth: 0,
108
+ position: "relative",
109
+ },
110
+ barH: (percent) => ({ height: `${percent}%` }),
111
+ bx: {
112
+ color: color.textMuted,
113
+ display: "flex",
114
+ fontFamily: font.mono,
115
+ fontSize: type.t1,
116
+ justifyContent: "space-between",
117
+ marginBlockStart: space.xxs,
118
+ },
119
+ faint: { color: tone.faint },
120
+ });
121
+ export function PageHead({ title, lead, summary, actions, sx }) {
122
+ return (_jsxs("div", { ...stylex.props(styles.head, sx), children: [_jsx("h1", { ...stylex.props(styles.h1), children: title }), lead !== undefined && _jsx("span", { ...stylex.props(styles.summary), children: lead }), _jsx("span", { ...stylex.props(styles.sp) }), summary !== undefined && _jsx("span", { ...stylex.props(styles.summary), children: summary }), actions] }));
123
+ }
124
+ /** A number inside a summary or a stat line: mono, in the text colour. */
125
+ export function B({ children }) {
126
+ return _jsx("b", { ...stylex.props(styles.b), children: children });
127
+ }
128
+ export function SectionLabel({ children, end, first = false, sx }) {
129
+ return (_jsxs("div", { ...stylex.props(styles.slab, first && styles.slabFirst, sx), children: [children, end !== undefined && _jsx("span", { ...stylex.props(styles.n), children: end })] }));
130
+ }
131
+ export function Sub({ children, sx }) {
132
+ return _jsx("p", { ...stylex.props(styles.sub, sx), children: children });
133
+ }
134
+ export function FootNote({ children, sx }) {
135
+ return _jsx("p", { ...stylex.props(styles.foot, sx), children: children });
136
+ }
137
+ export function Hint({ children, sx }) {
138
+ return _jsx("p", { ...stylex.props(styles.hint, sx), children: children });
139
+ }
140
+ export function Snippet({ children, sx }) {
141
+ return _jsx("pre", { ...stylex.props(styles.code, sx), children: children });
142
+ }
143
+ /** `.dpanel`: the bordered surface a section's rows sit on; `padded` when it holds prose. */
144
+ export function Panel({ children, padded = false, sx }) {
145
+ return _jsx("div", { ...stylex.props(styles.panel, padded && styles.padded, sx), children: children });
146
+ }
147
+ export function StatLine({ children, sx }) {
148
+ return _jsx("span", { ...stylex.props(styles.stat, sx), children: children });
149
+ }
150
+ export function StatBar({ percent, label, text }) {
151
+ const value = Math.max(0, Math.min(100, percent));
152
+ return (_jsx("span", { ...stylex.props(styles.sbar),
153
+ // oxlint-disable-next-line jsx-a11y/prefer-tag-over-role -- a 4px reading, not a control
154
+ role: "progressbar", "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": Math.round(value), ...(label ? { "aria-label": label } : {}), ...(text ? { "aria-valuetext": text } : {}), children: _jsx("i", { ...stylex.props(styles.sfill(value)) }) }));
155
+ }
156
+ /** `.bars` + `.bx`: one bar a day, the first and last day named under them. */
157
+ export function Bars({ values, tip, from, to, sx }) {
158
+ const max = Math.max(1, ...values);
159
+ return (_jsxs("div", { ...stylex.props(sx), children: [_jsx("div", { ...stylex.props(styles.bars), "aria-label": `${from} – ${to}`, children: values.map((value, index) => (_jsx("i", { title: tip(value, index), ...stylex.props(styles.bar, styles.barH(Math.round((value / max) * 100))) }, index))) }), _jsxs("div", { ...stylex.props(styles.bx), children: [_jsx("span", { children: from }), _jsx("span", { children: to })] })] }));
160
+ }
161
+ export function Faint({ children, sx }) {
162
+ return _jsx("span", { ...stylex.props(styles.faint, sx), children: children });
163
+ }
@@ -15,7 +15,7 @@ const styles = stylex.create({
15
15
  title: {
16
16
  fontFamily: font.display,
17
17
  fontSize: text.base,
18
- fontWeight: 500,
18
+ fontWeight: 600,
19
19
  lineHeight: text.leadingSnug,
20
20
  margin: 0,
21
21
  marginBottom: space.xxs,
@@ -1,68 +1,52 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import * as stylex from "@stylexjs/stylex";
3
- import { useCallback, useMemo, useState } from "react";
4
- import { BALL_STRANDS, ballDashOffset } from "../../lib/progress.js";
3
+ import { useCallback, useState } from "react";
5
4
  import { styled } from "../../lib/styled.js";
6
- import { useSvgId } from "../../lib/svgId.js";
7
- import { buildWeave, weaveRand } from "../../lib/weave.js";
8
- import { color, font, motion, radius, space, yarn } from "../../tokens.stylex.js";
5
+ import { color, font, motion, radius, space } from "../../tokens.stylex.js";
9
6
  const REDUCED = "@media (prefers-reduced-motion: reduce)";
10
7
  const STAGES = ["掃描對話", "挑出耐久事實", "合併重複", "落盤固定"];
11
- // 進度 = 一個容器裡長出一塊布:布就是 button 那塊(TEXTURE-GUIDE §3、種子 10075、
12
- // 同一組 yarn 色票與落影),容器是凹進去的軌。填充只是寬度往右長,布本身不動也不縮放 ——
13
- // 露出多少就是織到哪裡。環形同理:同一塊布在後面,弧形只是取景框(和 radio 的鏡頭一樣)。
14
- // 全部 CSS,零 rAF。
15
- const TRACK_H = 20;
16
- const PAD = 2;
17
- const CLOTH_H = TRACK_H - 2 - PAD * 2;
8
+ // 進度 = 一條軌加一段填充:軌是 border 色的 4px 帶,填充是實心 accent,
9
+ // 寬度就是讀數。不定量時同一段填充在軌上等速滑過。全部 CSS,零 rAF。
10
+ const TRACK_H = 4;
18
11
  const RING = 60;
19
- const BAND = 9;
12
+ const BAND = 6;
13
+ const SWEEP_W = 40;
20
14
  const spin = stylex.keyframes({ from: { rotate: "0deg" }, to: { rotate: "360deg" } });
21
- const sweep = stylex.keyframes({ from: { insetInlineStart: "-32%" }, to: { insetInlineStart: "100%" } });
15
+ const sweep = stylex.keyframes({
16
+ from: { insetInlineStart: `-${SWEEP_W}%` },
17
+ to: { insetInlineStart: "100%" },
18
+ });
22
19
  const styles = stylex.create({
23
20
  svg: { display: "block" },
24
21
  track: {
25
- display: "flex",
26
- alignItems: "center",
22
+ position: "relative",
27
23
  width: "100%",
28
24
  height: TRACK_H,
29
25
  boxSizing: "border-box",
30
- padding: PAD,
31
- borderWidth: 1,
32
- borderStyle: "solid",
33
- borderColor: color.border,
34
26
  borderRadius: radius.full,
35
- backgroundColor: color.bg,
27
+ backgroundColor: color.border,
36
28
  overflow: "hidden",
37
29
  },
38
- body: {
39
- position: "relative",
40
- height: CLOTH_H,
30
+ fill: {
31
+ position: "absolute",
32
+ top: 0,
33
+ bottom: 0,
34
+ insetInlineStart: 0,
41
35
  borderRadius: radius.full,
42
- overflow: "hidden",
43
- filter: yarn.shadow,
36
+ backgroundColor: color.accent,
44
37
  transitionProperty: "width",
45
38
  transitionDuration: { default: motion.fast, [REDUCED]: "0s" },
46
39
  transitionTimingFunction: "linear",
47
40
  },
48
41
  grow: (percent) => ({ width: `${percent}%` }),
49
42
  window: {
50
- width: "32%",
51
- insetInlineStart: 0,
43
+ width: `${SWEEP_W}%`,
44
+ insetInlineStart: { default: null, [REDUCED]: 0 },
52
45
  animationName: { default: sweep, [REDUCED]: "none" },
53
46
  animationDuration: "1.8s",
54
47
  animationTimingFunction: "linear",
55
48
  animationIterationCount: "infinite",
56
49
  },
57
- cloth: { fill: "none", strokeLinecap: "round" },
58
- fUn: { stroke: yarn.un },
59
- fSh: { stroke: yarn.sh, opacity: 0.5 },
60
- fY0: { stroke: yarn.y0 },
61
- fY1: { stroke: yarn.y1 },
62
- fY2: { stroke: yarn.y2 },
63
- fY3: { stroke: yarn.y3 },
64
- fY4: { stroke: yarn.y4 },
65
- fHi: { stroke: yarn.hi, opacity: 0.45 },
66
50
  tidy: { display: "grid", gap: space.xs },
67
51
  stage: {
68
52
  fontFamily: font.mono,
@@ -74,24 +58,30 @@ const styles = stylex.create({
74
58
  fontVariantNumeric: "tabular-nums",
75
59
  },
76
60
  ringWrap: { position: "relative", display: "inline-grid", placeItems: "center" },
77
- ringTrack: { fill: "none", stroke: color.bone },
78
- ringCloth: { filter: yarn.shadow },
79
- spin: {
61
+ ringTrack: { fill: "none", stroke: color.border },
62
+ ringArc: {
63
+ fill: "none",
64
+ stroke: color.accent,
65
+ strokeLinecap: "round",
66
+ transform: "rotate(-90deg)",
80
67
  transformBox: "fill-box",
81
68
  transformOrigin: "center",
82
- animationName: { default: spin, [REDUCED]: "none" },
83
- animationDuration: "0.9s",
84
- animationTimingFunction: "linear",
85
- animationIterationCount: "infinite",
69
+ transitionProperty: "stroke-dasharray",
70
+ transitionDuration: { default: motion.fast, [REDUCED]: "0s" },
71
+ transitionTimingFunction: "linear",
86
72
  },
87
- strand: {
73
+ spinArc: {
88
74
  fill: "none",
89
- stroke: color.accent,
90
- strokeWidth: 1.5,
75
+ stroke: "currentColor",
91
76
  strokeLinecap: "round",
92
- strokeDasharray: 100,
77
+ transformBox: "fill-box",
78
+ transformOrigin: "center",
79
+ animationName: { default: spin, [REDUCED]: "none" },
80
+ animationDuration: "0.8s",
81
+ animationTimingFunction: "linear",
82
+ animationIterationCount: "infinite",
93
83
  },
94
- statusHost: { display: "inline-flex" },
84
+ statusHost: { display: "inline-flex", color: color.textMuted },
95
85
  srOnly: {
96
86
  position: "absolute",
97
87
  width: 1,
@@ -113,62 +103,17 @@ const styles = stylex.create({
113
103
  fontVariantNumeric: "tabular-nums",
114
104
  },
115
105
  });
116
- const BUCKETS = [styles.fY0, styles.fY1, styles.fY2, styles.fY3, styles.fY4];
117
- function Cloth({ layers }) {
118
- return (_jsxs(_Fragment, { children: [_jsx("g", { ...stylex.props(styles.fUn), children: layers.under.map((strand, index) => (_jsx("path", { d: strand.d, strokeWidth: strand.sw }, index))) }), _jsx("g", { ...stylex.props(styles.fSh), children: layers.seams.map((strand, index) => (_jsx("path", { d: strand.d, strokeWidth: strand.sw }, index))) }), layers.face.map((strand, index) => (_jsx("path", { d: strand.d, strokeWidth: strand.sw, ...stylex.props(BUCKETS[strand.bucket]) }, index))), _jsx("g", { ...stylex.props(styles.fHi), children: layers.hi.map((strand, index) => (_jsx("path", { d: strand.d, strokeWidth: strand.sw }, index))) })] }));
119
- }
120
- // 量到容器內寬才織(ref callback + ResizeObserver,不用 effect);布永遠是整條軌的寬度,
121
- // 所以填充長出來時露出的是同一塊布的更多段,不是把布拉長。
122
106
  function Bar({ percent }) {
123
- const [inner, setInner] = useState(0);
124
- const measure = useCallback((node) => {
125
- if (!node)
126
- return;
127
- const read = () => setInner((current) => {
128
- const next = node.clientWidth - PAD * 2;
129
- return current === next ? current : next;
130
- });
131
- read();
132
- const observer = new ResizeObserver(read);
133
- observer.observe(node);
134
- return () => observer.disconnect();
135
- }, []);
136
- const cloth = useMemo(() => (inner > 0 ? buildWeave({ w: inner, h: CLOTH_H }, weaveRand()) : null), [inner]);
137
- return (_jsx("span", { ref: measure, ...stylex.props(styles.track), children: cloth != null && (_jsx("span", { ...stylex.props(styles.body, percent != null ? styles.grow(percent) : styles.window, styles.cloth), children: _jsx("svg", { width: inner, height: CLOTH_H, viewBox: `0 0 ${inner} ${CLOTH_H}`, "aria-hidden": "true", ...stylex.props(styles.svg, styles.cloth), children: _jsx(Cloth, { layers: cloth }) }) })) }));
138
- }
139
- // 環形:同一塊布在後面,弧形取景框開到 percent(和 radio 的鏡頭同一個邏輯)
140
- function ringSector(percent) {
141
- const centre = RING / 2;
142
- const outer = centre - 1;
143
- const nner = outer - BAND;
144
- const sweepDeg = Math.min(359.99, (percent / 100) * 360);
145
- const a0 = -Math.PI / 2;
146
- const a1 = a0 + (sweepDeg * Math.PI) / 180;
147
- const large = sweepDeg > 180 ? 1 : 0;
148
- const point = (r, angle) => `${(centre + r * Math.cos(angle)).toFixed(2)},${(centre + r * Math.sin(angle)).toFixed(2)}`;
149
- return [
150
- `M${point(outer, a0)}`,
151
- `A${outer},${outer} 0 ${large} 1 ${point(outer, a1)}`,
152
- `L${point(nner, a1)}`,
153
- `A${nner},${nner} 0 ${large} 0 ${point(nner, a0)}`,
154
- "Z",
155
- ].join("");
107
+ return (_jsx("span", { ...stylex.props(styles.track), children: _jsx("span", { ...stylex.props(styles.fill, percent != null ? styles.grow(percent) : styles.window) }) }));
156
108
  }
157
109
  const SPINNER_SIZES = { sm: 18, md: 28, lg: 40 };
158
- // spinner = 一段布做的弧在轉。布在後面不動(每個尺寸在 module scope 各織一塊,
159
- // SSR 安全),轉的是遮罩上那道弧 —— 和 radio 的鏡頭、tabs 的取景窗同一個邏輯。
160
- const SPINNER_CLOTH = {
161
- sm: buildWeave({ w: SPINNER_SIZES.sm, h: SPINNER_SIZES.sm }, weaveRand()),
162
- md: buildWeave({ w: SPINNER_SIZES.md, h: SPINNER_SIZES.md }, weaveRand()),
163
- lg: buildWeave({ w: SPINNER_SIZES.lg, h: SPINNER_SIZES.lg }, weaveRand()),
164
- };
165
110
  const clampPercent = (value) => Math.max(0, Math.min(100, value));
111
+ // spinner = 一段圓弧在轉:pathLength 100,72 長的弧留 28 的缺口。
166
112
  export function Spinner({ size = "md", label = "載入中" }) {
167
- const maskId = useSvgId("ak-spin");
168
113
  const px = SPINNER_SIZES[size];
169
114
  const band = px * 0.135;
170
- const radius = (px - band) / 2 - 0.5;
171
- return (_jsxs("span", { role: "status", "aria-label": label, ...stylex.props(styles.statusHost), children: [_jsxs("svg", { width: px, height: px, viewBox: `0 0 ${px} ${px}`, "aria-hidden": "true", ...stylex.props(styles.svg), children: [_jsx("defs", { children: _jsx("mask", { id: maskId, children: _jsx("circle", { cx: px / 2, cy: px / 2, r: radius, fill: "none", stroke: "#fff", strokeWidth: band, strokeLinecap: "round", pathLength: "100", strokeDasharray: "72 28", ...stylex.props(styles.spin) }) }) }), _jsx("g", { mask: `url(#${maskId})`, ...stylex.props(styles.cloth), children: _jsx(Cloth, { layers: SPINNER_CLOTH[size] }) })] }), _jsx("span", { ...stylex.props(styles.srOnly), children: label })] }));
115
+ const r = (px - band) / 2 - 0.5;
116
+ return (_jsxs("span", { role: "status", "aria-label": label, ...stylex.props(styles.statusHost), children: [_jsx("svg", { width: px, height: px, viewBox: `0 0 ${px} ${px}`, "aria-hidden": "true", ...stylex.props(styles.svg), children: _jsx("circle", { cx: px / 2, cy: px / 2, r: r, strokeWidth: band, pathLength: "100", strokeDasharray: "72 28", ...stylex.props(styles.spinArc) }) }), _jsx("span", { ...stylex.props(styles.srOnly), children: label })] }));
172
117
  }
173
118
  export function Progress({ value, valueText, stages = STAGES, ...rest }) {
174
119
  if (value == null)
@@ -177,7 +122,7 @@ export function Progress({ value, valueText, stages = STAGES, ...rest }) {
177
122
  return (_jsx("div", { role: "progressbar", "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": Math.round(percent), "aria-valuetext": valueText, ...rest, ...styled(rest, styles.svg), children: _jsx(Bar, { percent: percent }) }));
178
123
  }
179
124
  // 不定量:沒有真的進度可報,所以不給 aria-valuenow,也不顯示百分比。
180
- // 一段布在軌道上走完再走一次,底下是現在在做什麼。
125
+ // 一段填充在軌道上走完再走一次,底下是現在在做什麼。
181
126
  function ProgressTidy({ stages, valueText, ...rest }) {
182
127
  const [step, setStep] = useState(0);
183
128
  const cycle = useCallback((node) => {
@@ -190,11 +135,11 @@ function ProgressTidy({ stages, valueText, ...rest }) {
190
135
  }
191
136
  export function ProgressBall({ value, size = 48, valueText, ...rest }) {
192
137
  const percent = clampPercent(value);
193
- return (_jsx("svg", { width: size, height: size, viewBox: "0 0 24 24", role: "progressbar", "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": Math.round(percent), "aria-valuetext": valueText, ...rest, ...styled(rest, styles.svg), children: BALL_STRANDS.map((d, index) => (_jsx("path", { d: d, pathLength: "100", style: { strokeDashoffset: ballDashOffset(percent, index) }, ...stylex.props(styles.strand) }, index))) }));
138
+ const band = size * 0.12;
139
+ const r = size / 2 - band / 2 - 1;
140
+ return (_jsxs("svg", { width: size, height: size, viewBox: `0 0 ${size} ${size}`, role: "progressbar", "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": Math.round(percent), "aria-valuetext": valueText, ...rest, ...styled(rest, styles.svg), children: [_jsx("circle", { cx: size / 2, cy: size / 2, r: r, strokeWidth: band, ...stylex.props(styles.ringTrack) }), _jsx("circle", { cx: size / 2, cy: size / 2, r: r, strokeWidth: band, pathLength: "100", strokeDasharray: `${percent} ${100 - percent}`, ...stylex.props(styles.ringArc) })] }));
194
141
  }
195
142
  export function ProgressRing({ value, size = RING, valueText, ...rest }) {
196
143
  const percent = clampPercent(value);
197
- const clipId = useSvgId("ak-ring");
198
- const cloth = useMemo(() => buildWeave({ w: RING, h: RING }, weaveRand()), []);
199
- return (_jsxs("span", { role: "progressbar", "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": Math.round(percent), "aria-valuetext": valueText, ...rest, ...styled(rest, styles.ringWrap), children: [_jsxs("svg", { width: size, height: size, viewBox: `0 0 ${RING} ${RING}`, "aria-hidden": "true", ...stylex.props(styles.svg), children: [_jsx("defs", { children: _jsx("clipPath", { id: clipId, children: _jsx("path", { d: ringSector(percent) }) }) }), _jsx("circle", { cx: RING / 2, cy: RING / 2, r: RING / 2 - 1 - BAND / 2, strokeWidth: BAND, ...stylex.props(styles.ringTrack) }), _jsx("g", { clipPath: `url(#${clipId})`, ...stylex.props(styles.cloth, styles.ringCloth), children: _jsx(Cloth, { layers: cloth }) })] }), _jsx("span", { "aria-hidden": "true", ...stylex.props(styles.value), children: `${Math.round(percent)}%` })] }));
144
+ return (_jsxs("span", { role: "progressbar", "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": Math.round(percent), "aria-valuetext": valueText, ...rest, ...styled(rest, styles.ringWrap), children: [_jsxs("svg", { width: size, height: size, viewBox: `0 0 ${RING} ${RING}`, "aria-hidden": "true", ...stylex.props(styles.svg), children: [_jsx("circle", { cx: RING / 2, cy: RING / 2, r: RING / 2 - BAND / 2 - 1, strokeWidth: BAND, ...stylex.props(styles.ringTrack) }), _jsx("circle", { cx: RING / 2, cy: RING / 2, r: RING / 2 - BAND / 2 - 1, strokeWidth: BAND, pathLength: "100", strokeDasharray: `${percent} ${100 - percent}`, ...stylex.props(styles.ringArc) })] }), _jsx("span", { "aria-hidden": "true", ...stylex.props(styles.value), children: `${Math.round(percent)}%` })] }));
200
145
  }
@@ -2,16 +2,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import * as stylex from "@stylexjs/stylex";
3
3
  import { useId } from "react";
4
4
  import { styled } from "../../lib/styled.js";
5
- import { useSvgId } from "../../lib/svgId.js";
6
- import { buildWeave, weaveRand } from "../../lib/weave.js";
7
- import { color, font, motion, radius, space, text, yarn } from "../../tokens.stylex.js";
5
+ import { color, font, motion, radius, space, text } from "../../tokens.stylex.js";
8
6
  import { useFieldControl } from "../label/fieldContext.js";
9
7
  import { useRadioGroup } from "./RadioGroup.js";
10
8
  const REDUCED = "@media (prefers-reduced-motion: reduce)";
11
- // 鏡頭 dot:布像背景一樣在後面延伸(與 checkbox 同源幾何,module scope 織一次),
12
- // 圓形只是取景框 — 選中 = 孔徑(circle r)從 0 打開,布本身完全不動。
13
- // 孔徑只開到 r 6.8:維持 radio 的標準構成(外環 + surface 空隙 + 內實心圓)。
14
- const CLOTH = buildWeave({ w: 14.8, h: 14.8, s: 24 / 14.8 }, weaveRand());
15
9
  const styles = stylex.create({
16
10
  root: {
17
11
  display: "flex",
@@ -35,44 +29,39 @@ const styles = stylex.create({
35
29
  input: { position: "absolute", opacity: 0, width: "1.05rem", height: "1.05rem", margin: 0 },
36
30
  dot: {
37
31
  flex: "none",
32
+ boxSizing: "border-box",
33
+ display: "grid",
34
+ placeItems: "center",
38
35
  width: "1.05rem",
39
36
  height: "1.05rem",
40
37
  marginTop: "0.16rem",
41
- borderWidth: 1,
38
+ borderWidth: 1.5,
42
39
  borderStyle: "solid",
43
40
  borderColor: color.borderStrong,
44
41
  borderRadius: radius.full,
45
- backgroundColor: color.surface,
42
+ backgroundColor: color.bg,
46
43
  transitionProperty: "border-color",
47
44
  transitionDuration: { default: motion.fast, [REDUCED]: "0s" },
48
45
  outline: { default: "none", ":has(:focus-visible)": `2px solid ${color.focusRing}` },
49
46
  outlineOffset: 2,
50
47
  },
51
48
  dotOn: { borderColor: color.accent },
52
- svg: { display: "block", width: "100%", height: "100%" },
53
- lens: {
54
- r: "0px",
55
- transitionProperty: "r",
56
- transitionDuration: { default: motion.normal, [REDUCED]: "0s" },
57
- transitionTimingFunction: "cubic-bezier(0.32, 0.85, 0.45, 1)",
49
+ fill: {
50
+ width: "0.55rem",
51
+ height: "0.55rem",
52
+ borderRadius: radius.full,
53
+ backgroundColor: color.accent,
54
+ scale: "0",
55
+ transitionProperty: "scale",
56
+ transitionDuration: { default: "160ms", [REDUCED]: "0s" },
57
+ transitionTimingFunction: "ease-out",
58
58
  },
59
- lensOn: { r: "6.8px" },
60
- yarns: { fill: "none", strokeLinecap: "round" },
61
- un: { stroke: yarn.un },
62
- sh: { stroke: yarn.sh, opacity: 0.5 },
63
- y0: { stroke: yarn.y0 },
64
- y1: { stroke: yarn.y1 },
65
- y2: { stroke: yarn.y2 },
66
- y3: { stroke: yarn.y3 },
67
- y4: { stroke: yarn.y4 },
68
- hi: { stroke: yarn.hi, opacity: 0.45 },
59
+ fillOn: { scale: "1" },
69
60
  labelText: { display: "block", fontWeight: 500, fontSize: text.sm, color: color.text },
70
61
  description: { display: "block", fontSize: text.xs, color: color.textMuted },
71
62
  });
72
- const BUCKETS = [styles.y0, styles.y1, styles.y2, styles.y3, styles.y4];
73
63
  export function Radio({ value, label, description, onChange, disabled, ...props }) {
74
64
  const group = useRadioGroup();
75
- const clipId = useSvgId("ak-lens");
76
65
  const base = useId();
77
66
  const labelId = `${base}label`;
78
67
  const descriptionId = `${base}description`;
@@ -85,5 +74,5 @@ export function Radio({ value, label, description, onChange, disabled, ...props
85
74
  if (event.currentTarget.checked)
86
75
  group.select(value);
87
76
  onChange?.(event);
88
- }, ...styled(props, styles.input) }), _jsxs("svg", { viewBox: "0 0 24 24", "aria-hidden": "true", ...stylex.props(styles.svg), children: [_jsx("defs", { children: _jsx("clipPath", { id: clipId, children: _jsx("circle", { cx: "12", cy: "12", ...stylex.props(styles.lens, checked && styles.lensOn) }) }) }), _jsxs("g", { clipPath: `url(#${clipId})`, ...stylex.props(styles.yarns), children: [_jsx("g", { ...stylex.props(styles.un), children: CLOTH.under.map((t, i) => (_jsx("path", { d: t.d, strokeWidth: t.sw }, i))) }), _jsx("g", { ...stylex.props(styles.sh), children: CLOTH.seams.map((t, i) => (_jsx("path", { d: t.d, strokeWidth: t.sw }, i))) }), CLOTH.face.map((t, i) => (_jsx("path", { d: t.d, strokeWidth: t.sw, ...stylex.props(BUCKETS[t.bucket]) }, i))), _jsx("g", { ...stylex.props(styles.hi), children: CLOTH.hi.map((t, i) => (_jsx("path", { d: t.d, strokeWidth: t.sw }, i))) })] })] })] }), (label != null || description != null) && (_jsxs("span", { children: [label != null && (_jsx("span", { id: labelId, ...stylex.props(styles.labelText), children: label })), description != null && (_jsx("span", { id: descriptionId, ...stylex.props(styles.description), children: description }))] }))] }));
77
+ }, ...styled(props, styles.input) }), _jsx("span", { "aria-hidden": "true", ...stylex.props(styles.fill, checked && styles.fillOn) })] }), (label != null || description != null) && (_jsxs("span", { children: [label != null && (_jsx("span", { id: labelId, ...stylex.props(styles.labelText), children: label })), description != null && (_jsx("span", { id: descriptionId, ...stylex.props(styles.description), children: description }))] }))] }));
89
78
  }
@@ -0,0 +1,12 @@
1
+ import * as stylex from "@stylexjs/stylex";
2
+ export type SegmentedProps<T extends string> = {
3
+ value: T;
4
+ options: {
5
+ value: T;
6
+ label: string;
7
+ }[];
8
+ onChange: (value: T) => void;
9
+ label: string;
10
+ sx?: stylex.StyleXStyles;
11
+ };
12
+ export declare function Segmented<T extends string>({ value, options, onChange, label, sx }: SegmentedProps<T>): import("react").JSX.Element;
@@ -0,0 +1,43 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import * as stylex from "@stylexjs/stylex";
3
+ import { color, corner, font, motion, space, type } from "../../tokens.stylex.js";
4
+ /** `.seg`: a few words in one outline; the chosen one sits on a layer. */
5
+ const REDUCED = "@media (prefers-reduced-motion: reduce)";
6
+ const PHONE = "@media (max-width: 45rem)";
7
+ const styles = stylex.create({
8
+ root: {
9
+ alignSelf: "flex-start",
10
+ borderRadius: corner.md,
11
+ boxShadow: `inset 0 0 0 1px ${color.border}`,
12
+ display: "inline-flex",
13
+ gap: 2,
14
+ maxWidth: "100%",
15
+ overflowX: { default: "visible", [PHONE]: "auto" },
16
+ padding: 2,
17
+ scrollbarWidth: "none",
18
+ },
19
+ item: {
20
+ backgroundColor: { default: "transparent", ":hover": "transparent" },
21
+ borderRadius: corner.sm,
22
+ borderStyle: "none",
23
+ borderWidth: 0,
24
+ color: { default: color.textMuted, ":hover": color.text },
25
+ cursor: "pointer",
26
+ fontFamily: font.body,
27
+ fontSize: type.t2,
28
+ height: { default: 28, [PHONE]: 40 },
29
+ lineHeight: type.body,
30
+ margin: 0,
31
+ outline: { default: "none", ":focus-visible": `2px solid ${color.accent}` },
32
+ outlineOffset: -2,
33
+ paddingBlock: 0,
34
+ paddingInline: space.sm,
35
+ transitionDuration: { default: motion.fast, [REDUCED]: "0s" },
36
+ transitionProperty: "background-color, color",
37
+ whiteSpace: "nowrap",
38
+ },
39
+ on: { backgroundColor: color.layer4, color: color.text },
40
+ });
41
+ export function Segmented({ value, options, onChange, label, sx }) {
42
+ return (_jsx("div", { "aria-label": label, ...stylex.props(styles.root, sx), children: options.map((one) => (_jsx("button", { type: "button", "aria-pressed": one.value === value, onClick: () => onChange(one.value), ...stylex.props(styles.item, one.value === value && styles.on), children: one.label }, one.value))) }));
43
+ }
@@ -0,0 +1,25 @@
1
+ import * as stylex from "@stylexjs/stylex";
2
+ import type { ReactNode } from "react";
3
+ type Sx = {
4
+ sx?: stylex.StyleXStyles;
5
+ };
6
+ export declare function SettingsRows({ children, sx }: {
7
+ children: ReactNode;
8
+ } & Sx): import("react").JSX.Element;
9
+ export type SettingsRowProps = {
10
+ label: ReactNode;
11
+ help?: ReactNode;
12
+ children?: ReactNode;
13
+ sx?: stylex.StyleXStyles;
14
+ };
15
+ export declare function SettingsRow({ label, help, children, sx }: SettingsRowProps): import("react").JSX.Element;
16
+ /** `.help` inline: a muted reading on a row's label line. */
17
+ export declare function Help({ children, sx }: {
18
+ children: ReactNode;
19
+ } & Sx): import("react").JSX.Element;
20
+ /** `.val`: a mono reading, like `•••• 9b7c`. */
21
+ export declare function Value({ children, sx }: {
22
+ children: ReactNode;
23
+ } & Sx): import("react").JSX.Element;
24
+ export declare function Dot({ sx }: Sx): import("react").JSX.Element;
25
+ export {};
@@ -0,0 +1,63 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import * as stylex from "@stylexjs/stylex";
3
+ import { color, corner, font, space, type } from "../../tokens.stylex.js";
4
+ /**
5
+ * `.srows` / `.srow`: a setting per line, 44px — its name and one line of help on the left, the
6
+ * control on the right. The rows sit on one surface with hairlines between them.
7
+ */
8
+ const styles = stylex.create({
9
+ rows: {
10
+ backgroundColor: color.surface,
11
+ borderRadius: corner.card,
12
+ boxShadow: `inset 0 0 0 1px ${color.border}`,
13
+ color: color.text,
14
+ fontSize: type.t2,
15
+ lineHeight: type.body,
16
+ },
17
+ row: {
18
+ alignItems: "center",
19
+ borderRadius: {
20
+ default: 0,
21
+ ":first-child": `${corner.card} ${corner.card} 0 0`,
22
+ ":last-child": `0 0 ${corner.card} ${corner.card}`,
23
+ },
24
+ boxShadow: { default: "none", ":not(:first-child)": `inset 0 1px 0 ${color.border}` },
25
+ columnGap: space.sm,
26
+ display: "grid",
27
+ gridTemplateColumns: "minmax(0, 1fr) auto",
28
+ minHeight: 44,
29
+ paddingBlock: space.xs,
30
+ paddingInline: space.sm,
31
+ },
32
+ key: { minWidth: 0 },
33
+ label: {
34
+ alignItems: "center",
35
+ display: "flex",
36
+ flexWrap: "wrap",
37
+ fontSize: type.t2,
38
+ fontWeight: 500,
39
+ gap: space.xs,
40
+ minWidth: 0,
41
+ },
42
+ help: { color: color.textMuted, fontSize: type.t2 },
43
+ value: { color: color.textMuted, fontFamily: font.mono, fontSize: type.t2, whiteSpace: "nowrap" },
44
+ ctl: { alignItems: "center", display: "flex", flex: "none", gap: space.xs },
45
+ dot: { backgroundColor: color.accent, borderRadius: "50%", flex: "none", height: 6, width: 6 },
46
+ });
47
+ export function SettingsRows({ children, sx }) {
48
+ return _jsx("div", { ...stylex.props(styles.rows, sx), children: children });
49
+ }
50
+ export function SettingsRow({ label, help, children, sx }) {
51
+ return (_jsxs("div", { ...stylex.props(styles.row, sx), children: [_jsxs("div", { ...stylex.props(styles.key), children: [_jsx("div", { ...stylex.props(styles.label), children: label }), help !== undefined && _jsx("div", { ...stylex.props(styles.help), children: help })] }), children !== undefined && _jsx("div", { ...stylex.props(styles.ctl), children: children })] }));
52
+ }
53
+ /** `.help` inline: a muted reading on a row's label line. */
54
+ export function Help({ children, sx }) {
55
+ return _jsx("span", { ...stylex.props(styles.help, sx), children: children });
56
+ }
57
+ /** `.val`: a mono reading, like `•••• 9b7c`. */
58
+ export function Value({ children, sx }) {
59
+ return _jsx("span", { ...stylex.props(styles.value, sx), children: children });
60
+ }
61
+ export function Dot({ sx }) {
62
+ return _jsx("span", { "aria-hidden": "true", ...stylex.props(styles.dot, sx) });
63
+ }
@@ -0,0 +1,19 @@
1
+ import { type ReactNode } from "react";
2
+ import type { StyleArg } from "../../lib/styled.js";
3
+ export type SliderProps = {
4
+ value: number;
5
+ onChange: (value: number) => void;
6
+ /** @default 0 */
7
+ min?: number;
8
+ /** @default 1 */
9
+ max?: number;
10
+ /** @default 0.01 */
11
+ step?: number;
12
+ label?: ReactNode;
13
+ "aria-label"?: string;
14
+ /** 唸出來的值 —— 0.62 要唸成「多」不是「零點六二」。 */
15
+ valueText?: (value: number) => string;
16
+ disabled?: boolean;
17
+ sx?: StyleArg;
18
+ };
19
+ export declare function Slider({ value, onChange, min, max, step, label, valueText, disabled, sx, "aria-label": ariaLabel, }: SliderProps): import("react").JSX.Element;