@anyknown/ui 0.1.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 (121) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +121 -0
  3. package/dist/components/action-bar/ActionBar.d.ts +28 -0
  4. package/dist/components/action-bar/ActionBar.js +73 -0
  5. package/dist/components/badge/Badge.d.ts +28 -0
  6. package/dist/components/badge/Badge.js +79 -0
  7. package/dist/components/button/Button.d.ts +7 -0
  8. package/dist/components/button/Button.js +86 -0
  9. package/dist/components/card/Card.d.ts +2 -0
  10. package/dist/components/card/Card.js +17 -0
  11. package/dist/components/checkbox/Checkbox.d.ts +8 -0
  12. package/dist/components/checkbox/Checkbox.js +102 -0
  13. package/dist/components/code-block/CodeBlock.d.ts +10 -0
  14. package/dist/components/code-block/CodeBlock.js +103 -0
  15. package/dist/components/composer/Composer.d.ts +27 -0
  16. package/dist/components/composer/Composer.js +278 -0
  17. package/dist/components/data-table/DataTable.d.ts +38 -0
  18. package/dist/components/data-table/DataTable.js +256 -0
  19. package/dist/components/dialog/Dialog.d.ts +36 -0
  20. package/dist/components/dialog/Dialog.js +77 -0
  21. package/dist/components/diff-viewer/DiffViewer.d.ts +13 -0
  22. package/dist/components/diff-viewer/DiffViewer.js +133 -0
  23. package/dist/components/dropdown/DropdownMenu.d.ts +43 -0
  24. package/dist/components/dropdown/DropdownMenu.js +86 -0
  25. package/dist/components/dropzone/Dropzone.d.ts +32 -0
  26. package/dist/components/dropzone/Dropzone.js +242 -0
  27. package/dist/components/empty-state/EmptyState.d.ts +9 -0
  28. package/dist/components/empty-state/EmptyState.js +41 -0
  29. package/dist/components/file-row/FileRow.d.ts +32 -0
  30. package/dist/components/file-row/FileRow.js +153 -0
  31. package/dist/components/handoff-receipt/HandoffReceipt.d.ts +15 -0
  32. package/dist/components/handoff-receipt/HandoffReceipt.js +147 -0
  33. package/dist/components/input/Input.d.ts +48 -0
  34. package/dist/components/input/Input.js +64 -0
  35. package/dist/components/interaction-card/InteractionCard.d.ts +59 -0
  36. package/dist/components/interaction-card/InteractionCard.js +238 -0
  37. package/dist/components/kbd/Kbd.d.ts +9 -0
  38. package/dist/components/kbd/Kbd.js +43 -0
  39. package/dist/components/label/Field.d.ts +11 -0
  40. package/dist/components/label/Field.js +28 -0
  41. package/dist/components/label/Label.d.ts +7 -0
  42. package/dist/components/label/Label.js +21 -0
  43. package/dist/components/label/fieldContext.d.ts +26 -0
  44. package/dist/components/label/fieldContext.js +16 -0
  45. package/dist/components/message/Message.d.ts +29 -0
  46. package/dist/components/message/Message.js +94 -0
  47. package/dist/components/password-input/PasswordInput.d.ts +20 -0
  48. package/dist/components/password-input/PasswordInput.js +141 -0
  49. package/dist/components/popover/Popover.d.ts +38 -0
  50. package/dist/components/popover/Popover.js +37 -0
  51. package/dist/components/progress/Progress.d.ts +36 -0
  52. package/dist/components/progress/Progress.js +200 -0
  53. package/dist/components/radio/Radio.d.ts +7 -0
  54. package/dist/components/radio/Radio.js +89 -0
  55. package/dist/components/radio/RadioGroup.d.ts +21 -0
  56. package/dist/components/radio/RadioGroup.js +34 -0
  57. package/dist/components/reasoning-fold/ReasoningFold.d.ts +10 -0
  58. package/dist/components/reasoning-fold/ReasoningFold.js +92 -0
  59. package/dist/components/recovery-key/RecoveryKey.d.ts +17 -0
  60. package/dist/components/recovery-key/RecoveryKey.js +132 -0
  61. package/dist/components/select/Select.d.ts +31 -0
  62. package/dist/components/select/Select.js +209 -0
  63. package/dist/components/skeleton/Skeleton.d.ts +20 -0
  64. package/dist/components/skeleton/Skeleton.js +57 -0
  65. package/dist/components/switch/Switch.d.ts +7 -0
  66. package/dist/components/switch/Switch.js +108 -0
  67. package/dist/components/tabs/Tabs.d.ts +28 -0
  68. package/dist/components/tabs/Tabs.js +133 -0
  69. package/dist/components/text/Text.d.ts +7 -0
  70. package/dist/components/text/Text.js +31 -0
  71. package/dist/components/textarea/Textarea.d.ts +7 -0
  72. package/dist/components/textarea/Textarea.js +25 -0
  73. package/dist/components/toast/Toast.d.ts +56 -0
  74. package/dist/components/toast/Toast.js +175 -0
  75. package/dist/components/tool-card/ToolCard.d.ts +55 -0
  76. package/dist/components/tool-card/ToolCard.js +281 -0
  77. package/dist/components/tooltip/Tooltip.d.ts +12 -0
  78. package/dist/components/tooltip/Tooltip.js +38 -0
  79. package/dist/components/voice-indicator/VoiceIndicator.d.ts +7 -0
  80. package/dist/components/voice-indicator/VoiceIndicator.js +51 -0
  81. package/dist/index.d.ts +55 -0
  82. package/dist/index.js +38 -0
  83. package/dist/lib/diff.d.ts +28 -0
  84. package/dist/lib/diff.js +133 -0
  85. package/dist/lib/format.d.ts +2 -0
  86. package/dist/lib/format.js +24 -0
  87. package/dist/lib/format.test.d.ts +1 -0
  88. package/dist/lib/format.test.js +37 -0
  89. package/dist/lib/mergeRefs.d.ts +2 -0
  90. package/dist/lib/mergeRefs.js +8 -0
  91. package/dist/lib/motion.d.ts +1 -0
  92. package/dist/lib/motion.js +10 -0
  93. package/dist/lib/paths.d.ts +3 -0
  94. package/dist/lib/paths.js +11 -0
  95. package/dist/lib/popup.d.ts +29 -0
  96. package/dist/lib/popup.js +27 -0
  97. package/dist/lib/progress.d.ts +2 -0
  98. package/dist/lib/progress.js +28 -0
  99. package/dist/lib/silk.d.ts +66 -0
  100. package/dist/lib/silk.js +337 -0
  101. package/dist/lib/styled.d.ts +29 -0
  102. package/dist/lib/styled.js +30 -0
  103. package/dist/lib/svgId.d.ts +1 -0
  104. package/dist/lib/svgId.js +4 -0
  105. package/dist/lib/useAnimationFrame.d.ts +1 -0
  106. package/dist/lib/useAnimationFrame.js +21 -0
  107. package/dist/lib/useControllableState.d.ts +1 -0
  108. package/dist/lib/useControllableState.js +14 -0
  109. package/dist/lib/useCopy.d.ts +4 -0
  110. package/dist/lib/useCopy.js +19 -0
  111. package/dist/lib/voice.d.ts +2 -0
  112. package/dist/lib/voice.js +30 -0
  113. package/dist/lib/weave.d.ts +28 -0
  114. package/dist/lib/weave.js +83 -0
  115. package/dist/scrollbar.css +29 -0
  116. package/dist/themes.stylex.d.ts +49 -0
  117. package/dist/themes.stylex.js +52 -0
  118. package/dist/tokens.css +109 -0
  119. package/dist/tokens.stylex.d.ts +126 -0
  120. package/dist/tokens.stylex.js +149 -0
  121. package/package.json +81 -0
@@ -0,0 +1,200 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import * as stylex from "@stylexjs/stylex";
3
+ import { useCallback, useMemo, useState } from "react";
4
+ import { BALL_STRANDS, ballDashOffset } from "../../lib/progress.js";
5
+ 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";
9
+ const REDUCED = "@media (prefers-reduced-motion: reduce)";
10
+ 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;
18
+ const RING = 60;
19
+ const BAND = 9;
20
+ const spin = stylex.keyframes({ from: { rotate: "0deg" }, to: { rotate: "360deg" } });
21
+ const sweep = stylex.keyframes({ from: { insetInlineStart: "-32%" }, to: { insetInlineStart: "100%" } });
22
+ const styles = stylex.create({
23
+ svg: { display: "block" },
24
+ track: {
25
+ display: "flex",
26
+ alignItems: "center",
27
+ width: "100%",
28
+ height: TRACK_H,
29
+ boxSizing: "border-box",
30
+ padding: PAD,
31
+ borderWidth: 1,
32
+ borderStyle: "solid",
33
+ borderColor: color.border,
34
+ borderRadius: radius.full,
35
+ backgroundColor: color.bg,
36
+ overflow: "hidden",
37
+ },
38
+ body: {
39
+ position: "relative",
40
+ height: CLOTH_H,
41
+ borderRadius: radius.full,
42
+ overflow: "hidden",
43
+ filter: yarn.shadow,
44
+ transitionProperty: "width",
45
+ transitionDuration: { default: motion.fast, [REDUCED]: "0s" },
46
+ transitionTimingFunction: "linear",
47
+ },
48
+ grow: (percent) => ({ width: `${percent}%` }),
49
+ window: {
50
+ width: "32%",
51
+ insetInlineStart: 0,
52
+ animationName: { default: sweep, [REDUCED]: "none" },
53
+ animationDuration: "1.8s",
54
+ animationTimingFunction: "linear",
55
+ animationIterationCount: "infinite",
56
+ },
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
+ tidy: { display: "grid", gap: space.xs },
67
+ stage: {
68
+ fontFamily: font.mono,
69
+ fontSize: "0.72rem",
70
+ fontWeight: 500,
71
+ lineHeight: 1,
72
+ letterSpacing: "0.04em",
73
+ color: color.textMuted,
74
+ fontVariantNumeric: "tabular-nums",
75
+ },
76
+ ringWrap: { position: "relative", display: "inline-grid", placeItems: "center" },
77
+ ringTrack: { fill: "none", stroke: color.bone },
78
+ ringCloth: { filter: yarn.shadow },
79
+ spin: {
80
+ transformBox: "fill-box",
81
+ transformOrigin: "center",
82
+ animationName: { default: spin, [REDUCED]: "none" },
83
+ animationDuration: "0.9s",
84
+ animationTimingFunction: "linear",
85
+ animationIterationCount: "infinite",
86
+ },
87
+ strand: {
88
+ fill: "none",
89
+ stroke: color.accent,
90
+ strokeWidth: 1.5,
91
+ strokeLinecap: "round",
92
+ strokeDasharray: 100,
93
+ },
94
+ statusHost: { display: "inline-flex" },
95
+ srOnly: {
96
+ position: "absolute",
97
+ width: 1,
98
+ height: 1,
99
+ padding: 0,
100
+ margin: -1,
101
+ overflow: "hidden",
102
+ clipPath: "inset(50%)",
103
+ whiteSpace: "nowrap",
104
+ borderWidth: 0,
105
+ },
106
+ value: {
107
+ position: "absolute",
108
+ fontFamily: font.mono,
109
+ fontSize: "0.75rem",
110
+ fontWeight: 500,
111
+ lineHeight: 1,
112
+ color: color.text,
113
+ fontVariantNumeric: "tabular-nums",
114
+ },
115
+ });
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
+ 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("");
156
+ }
157
+ 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
+ const clampPercent = (value) => Math.max(0, Math.min(100, value));
166
+ export function Spinner({ size = "md", label = "載入中" }) {
167
+ const maskId = useSvgId("ak-spin");
168
+ const px = SPINNER_SIZES[size];
169
+ 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 })] }));
172
+ }
173
+ export function Progress({ value, valueText, stages = STAGES, ...rest }) {
174
+ if (value == null)
175
+ return _jsx(ProgressTidy, { stages: stages, valueText: valueText, ...rest });
176
+ const percent = clampPercent(value);
177
+ 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
+ }
179
+ // 不定量:沒有真的進度可報,所以不給 aria-valuenow,也不顯示百分比。
180
+ // 一段布在軌道上走完再走一次,底下是現在在做什麼。
181
+ function ProgressTidy({ stages, valueText, ...rest }) {
182
+ const [step, setStep] = useState(0);
183
+ const cycle = useCallback((node) => {
184
+ if (!node)
185
+ return;
186
+ const id = setInterval(() => setStep((current) => current + 1), 1800);
187
+ return () => clearInterval(id);
188
+ }, []);
189
+ return (_jsxs("div", { ref: cycle, role: "progressbar", "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuetext": valueText, ...rest, ...styled(rest, styles.tidy), children: [_jsx(Bar, {}), _jsx("span", { ...stylex.props(styles.stage), children: stages[step % stages.length] })] }));
190
+ }
191
+ export function ProgressBall({ value, size = 48, valueText, ...rest }) {
192
+ 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))) }));
194
+ }
195
+ export function ProgressRing({ value, size = RING, valueText, ...rest }) {
196
+ 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)}%` })] }));
200
+ }
@@ -0,0 +1,7 @@
1
+ import { type ComponentProps, type ReactNode } from "react";
2
+ export type RadioProps = Omit<ComponentProps<"input">, "type" | "value" | "children"> & {
3
+ value: string;
4
+ label?: ReactNode;
5
+ description?: ReactNode;
6
+ };
7
+ export declare function Radio({ value, label, description, onChange, disabled, ...props }: RadioProps): import("react").JSX.Element;
@@ -0,0 +1,89 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import * as stylex from "@stylexjs/stylex";
3
+ import { useId } from "react";
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";
8
+ import { useFieldControl } from "../label/fieldContext.js";
9
+ import { useRadioGroup } from "./RadioGroup.js";
10
+ 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
+ const styles = stylex.create({
16
+ root: {
17
+ display: "flex",
18
+ gap: space.xs,
19
+ alignItems: "flex-start",
20
+ cursor: { default: "pointer", ":has(:disabled)": "not-allowed" },
21
+ opacity: { default: 1, ":has(:disabled)": 0.5 },
22
+ fontFamily: font.body,
23
+ },
24
+ card: {
25
+ borderWidth: 1,
26
+ borderStyle: "solid",
27
+ borderColor: color.border,
28
+ borderRadius: radius.md,
29
+ paddingBlock: space.sm,
30
+ paddingInline: space.sm,
31
+ transitionProperty: "border-color, background-color",
32
+ transitionDuration: { default: motion.fast, [REDUCED]: "0s" },
33
+ },
34
+ cardOn: { borderColor: color.accent, backgroundColor: color.accentSubtle },
35
+ input: { position: "absolute", opacity: 0, width: "1.05rem", height: "1.05rem", margin: 0 },
36
+ dot: {
37
+ flex: "none",
38
+ width: "1.05rem",
39
+ height: "1.05rem",
40
+ marginTop: "0.16rem",
41
+ borderWidth: 1,
42
+ borderStyle: "solid",
43
+ borderColor: color.borderStrong,
44
+ borderRadius: radius.full,
45
+ backgroundColor: color.surface,
46
+ transitionProperty: "border-color",
47
+ transitionDuration: { default: motion.fast, [REDUCED]: "0s" },
48
+ outline: { default: "none", ":has(:focus-visible)": `2px solid ${color.focusRing}` },
49
+ outlineOffset: 2,
50
+ },
51
+ 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)",
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 },
69
+ labelText: { display: "block", fontWeight: 500, fontSize: text.sm, color: color.text },
70
+ description: { display: "block", fontSize: text.xs, color: color.textMuted },
71
+ });
72
+ const BUCKETS = [styles.y0, styles.y1, styles.y2, styles.y3, styles.y4];
73
+ export function Radio({ value, label, description, onChange, disabled, ...props }) {
74
+ const group = useRadioGroup();
75
+ const clipId = useSvgId("ak-lens");
76
+ const base = useId();
77
+ const labelId = `${base}label`;
78
+ const descriptionId = `${base}description`;
79
+ const { invalid: _invalid, id: _fieldId, ...field } = useFieldControl(props);
80
+ const checked = group.value === value;
81
+ const describedBy = [description != null ? descriptionId : null, field["aria-describedby"]]
82
+ .filter(Boolean)
83
+ .join(" ");
84
+ return (_jsxs("label", { ...stylex.props(styles.root, group.variant === "card" && styles.card, group.variant === "card" && checked && styles.cardOn), children: [_jsxs("span", { ...stylex.props(styles.dot, checked && styles.dotOn), children: [_jsx("input", { type: "radio", ...props, ...field, name: group.name, value: value, checked: checked, "aria-labelledby": label != null ? labelId : undefined, "aria-describedby": describedBy || undefined, disabled: disabled ?? field.disabled ?? group.disabled, onChange: (event) => {
85
+ if (event.currentTarget.checked)
86
+ group.select(value);
87
+ 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 }))] }))] }));
89
+ }
@@ -0,0 +1,21 @@
1
+ import { type ComponentProps, type ReactNode } from "react";
2
+ type RadioGroupContextValue = {
3
+ name: string;
4
+ value: string;
5
+ disabled: boolean;
6
+ variant: "plain" | "card";
7
+ select: (value: string) => void;
8
+ };
9
+ export declare const RadioGroupContext: import("react").Context<RadioGroupContextValue | null>;
10
+ export declare function useRadioGroup(): RadioGroupContextValue;
11
+ export type RadioGroupProps = Omit<ComponentProps<"fieldset">, "onChange" | "defaultValue"> & {
12
+ legend?: ReactNode;
13
+ name?: string;
14
+ value?: string;
15
+ defaultValue?: string;
16
+ variant?: "plain" | "card";
17
+ onValueChange?: (value: string) => void;
18
+ children: ReactNode;
19
+ };
20
+ export declare function RadioGroup({ legend, name, value, defaultValue, variant, onValueChange, disabled, children, ...props }: RadioGroupProps): import("react").JSX.Element;
21
+ export {};
@@ -0,0 +1,34 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import * as stylex from "@stylexjs/stylex";
3
+ import { createContext, useContext, useId, useMemo } from "react";
4
+ import { styled } from "../../lib/styled.js";
5
+ import { useControllableState } from "../../lib/useControllableState.js";
6
+ import { color, font, space, text } from "../../tokens.stylex.js";
7
+ export const RadioGroupContext = createContext(null);
8
+ export function useRadioGroup() {
9
+ const group = useContext(RadioGroupContext);
10
+ if (!group)
11
+ throw new Error("Radio must be rendered inside a RadioGroup");
12
+ return group;
13
+ }
14
+ const styles = stylex.create({
15
+ // borderWidth 而不是 border:0 —— StyleX 0.19 會靜默丟掉 border 簡寫,fieldset 的
16
+ // 原生 2px groove 邊框就留在畫面上
17
+ fieldset: { borderWidth: 0, margin: 0, padding: 0, display: "grid", gap: space.sm },
18
+ legend: {
19
+ padding: 0,
20
+ marginBottom: space.xxs,
21
+ fontFamily: font.body,
22
+ fontSize: text.sm,
23
+ fontWeight: 500,
24
+ lineHeight: text.leadingSnug,
25
+ color: color.text,
26
+ },
27
+ });
28
+ export function RadioGroup({ legend, name, value, defaultValue = "", variant = "plain", onValueChange, disabled = false, children, ...props }) {
29
+ const fallbackName = useId();
30
+ const resolvedName = name ?? fallbackName;
31
+ const [selected, select] = useControllableState(value, defaultValue, onValueChange);
32
+ const context = useMemo(() => ({ name: resolvedName, value: selected, disabled, variant, select }), [resolvedName, selected, disabled, variant, select]);
33
+ return (_jsxs("fieldset", { ...props, disabled: disabled, ...styled(props, styles.fieldset), children: [legend != null && _jsx("legend", { ...stylex.props(styles.legend), children: legend }), _jsx(RadioGroupContext, { value: context, children: children })] }));
34
+ }
@@ -0,0 +1,10 @@
1
+ import { type ReactNode } from "react";
2
+ export type ReasoningFoldProps = {
3
+ streaming?: boolean;
4
+ durationSec?: number;
5
+ defaultOpen?: boolean;
6
+ streamingLabel?: string;
7
+ onToggle?: (open: boolean) => void;
8
+ children: ReactNode;
9
+ };
10
+ export declare function ReasoningFold({ streaming, durationSec, defaultOpen, streamingLabel, onToggle, children, }: ReasoningFoldProps): import("react").JSX.Element;
@@ -0,0 +1,92 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import * as stylex from "@stylexjs/stylex";
3
+ import { useEffect, useId, useRef, useState } from "react";
4
+ import { reset } from "../../lib/styled.js";
5
+ import { color, radius, space, text } from "../../tokens.stylex.js";
6
+ const REDUCED = "@media (prefers-reduced-motion: reduce)";
7
+ const AUTO_COLLAPSE_MS = 1000;
8
+ const shimmer = stylex.keyframes({ to: { backgroundPosition: "-200% 0" } });
9
+ const styles = stylex.create({
10
+ fold: { display: "grid" },
11
+ row: {
12
+ display: "inline-flex",
13
+ alignItems: "center",
14
+ gap: space.xxs,
15
+ fontSize: text.xs,
16
+ lineHeight: text.leadingSnug,
17
+ color: { default: color.textMuted, ":hover": color.text },
18
+ cursor: "pointer",
19
+ paddingBlock: "0.2rem",
20
+ justifySelf: "start",
21
+ borderRadius: radius.sm,
22
+ outline: { default: "none", ":focus-visible": `2px solid ${color.focusRing}` },
23
+ outlineOffset: 2,
24
+ },
25
+ chevron: {
26
+ color: color.textFaint,
27
+ flex: "none",
28
+ transitionProperty: "rotate",
29
+ transitionDuration: { default: "140ms", [REDUCED]: "0s" },
30
+ },
31
+ chevronOpen: { rotate: "90deg" },
32
+ body: {
33
+ marginBlockStart: space.xs,
34
+ paddingInlineStart: `calc(12px + ${space.xxs})`,
35
+ borderInlineStartWidth: 2,
36
+ borderInlineStartStyle: "solid",
37
+ borderInlineStartColor: color.border,
38
+ marginInlineStart: 5,
39
+ fontSize: text.xs,
40
+ lineHeight: text.leadingRelaxed,
41
+ fontStyle: "italic",
42
+ color: color.textMuted,
43
+ },
44
+ shimmer: {
45
+ backgroundImage: {
46
+ default: `linear-gradient(90deg, ${color.textMuted} 30%, ${color.textFaint} 50%, ${color.textMuted} 70%)`,
47
+ [REDUCED]: "none",
48
+ },
49
+ backgroundSize: "200% 100%",
50
+ backgroundClip: { default: "text", [REDUCED]: "border-box" },
51
+ color: { default: "transparent", [REDUCED]: color.textMuted },
52
+ animationName: { default: shimmer, [REDUCED]: "none" },
53
+ animationDuration: "1.6s",
54
+ animationTimingFunction: "linear",
55
+ animationIterationCount: "infinite",
56
+ },
57
+ });
58
+ function Chevron({ open }) {
59
+ return (_jsx("svg", { width: "11", height: "11", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", "aria-hidden": "true", ...stylex.props(styles.chevron, open && styles.chevronOpen), children: _jsx("path", { d: "m9 6 6 6-6 6" }) }));
60
+ }
61
+ export function ReasoningFold({ streaming = false, durationSec, defaultOpen = false, streamingLabel = "思考中…", onToggle, children, }) {
62
+ const bodyId = useId();
63
+ const row = useRef(null);
64
+ const body = useRef(null);
65
+ const [userToggled, setUserToggled] = useState(false);
66
+ const [open, setOpen] = useState(defaultOpen || streaming);
67
+ const [wasStreaming, setWasStreaming] = useState(streaming);
68
+ const [justFinished, setJustFinished] = useState(false);
69
+ if (wasStreaming !== streaming) {
70
+ setWasStreaming(streaming);
71
+ setJustFinished(!streaming);
72
+ if (streaming && !userToggled)
73
+ setOpen(true);
74
+ }
75
+ useEffect(() => {
76
+ if (userToggled || streaming || !justFinished)
77
+ return;
78
+ const timer = setTimeout(() => {
79
+ setOpen(false);
80
+ if (body.current?.contains(document.activeElement))
81
+ row.current?.focus();
82
+ setJustFinished(false);
83
+ }, AUTO_COLLAPSE_MS);
84
+ return () => clearTimeout(timer);
85
+ }, [streaming, userToggled, justFinished]);
86
+ const label = streaming ? (_jsx("span", { ...stylex.props(styles.shimmer), children: streamingLabel })) : durationSec != null ? (`思考了 ${durationSec} 秒`) : ("思考過程");
87
+ return (_jsxs("div", { ...stylex.props(styles.fold), children: [_jsxs("button", { type: "button", ref: row, "aria-expanded": open, "aria-controls": bodyId, onClick: () => {
88
+ setUserToggled(true);
89
+ setOpen((value) => !value);
90
+ onToggle?.(!open);
91
+ }, ...stylex.props(reset.control, styles.row), children: [_jsx(Chevron, { open: open }), label] }), _jsx("div", { id: bodyId, ref: body, hidden: !open, ...stylex.props(styles.body), children: children })] }));
92
+ }
@@ -0,0 +1,17 @@
1
+ import { type ReactNode } from "react";
2
+ export type RecoveryKeyProps = {
3
+ value: string;
4
+ ack?: boolean;
5
+ onAckChange?: (ack: boolean) => void;
6
+ filename?: string;
7
+ intro?: ReactNode;
8
+ warning?: ReactNode;
9
+ ackLabel?: ReactNode;
10
+ revealLabel?: string;
11
+ hideLabel?: string;
12
+ veilLabel?: string;
13
+ copyLabel?: string;
14
+ copiedLabel?: string;
15
+ downloadLabel?: string;
16
+ };
17
+ export declare function RecoveryKey({ value, ack, onAckChange, filename, intro, warning, ackLabel, revealLabel, hideLabel, veilLabel, copyLabel, copiedLabel, downloadLabel, }: RecoveryKeyProps): import("react").JSX.Element;
@@ -0,0 +1,132 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import * as stylex from "@stylexjs/stylex";
3
+ import { Fragment, useState } from "react";
4
+ import { useCopy } from "../../lib/useCopy.js";
5
+ import { color, font, motion, radius, space, text } from "../../tokens.stylex.js";
6
+ import { Checkbox } from "../checkbox/Checkbox.js";
7
+ const REDUCED = "@media (prefers-reduced-motion: reduce)";
8
+ const styles = stylex.create({
9
+ card: {
10
+ backgroundColor: color.surface,
11
+ borderWidth: 1,
12
+ borderStyle: "solid",
13
+ borderColor: color.border,
14
+ borderRadius: radius.lg,
15
+ padding: space.md,
16
+ display: "grid",
17
+ gap: space.sm,
18
+ fontFamily: font.body,
19
+ },
20
+ intro: { margin: 0, fontSize: text.xs, color: color.textMuted },
21
+ keyBox: {
22
+ position: "relative",
23
+ backgroundColor: color.bg,
24
+ borderWidth: 1,
25
+ borderStyle: "solid",
26
+ borderColor: color.border,
27
+ borderRadius: radius.lg,
28
+ padding: space.md,
29
+ cursor: "pointer",
30
+ outline: { default: "none", ":focus-visible": `2px solid ${color.focusRing}` },
31
+ outlineOffset: -1,
32
+ "--ak-key-blur": { default: "blur(7px)", ":hover": "none", ":focus-visible": "none" },
33
+ "--ak-veil-opacity": { default: "1", ":hover": "0", ":focus-visible": "0" },
34
+ },
35
+ keyRevealed: { "--ak-key-blur": "none", "--ak-veil-opacity": "0" },
36
+ separator: {
37
+ position: "absolute",
38
+ width: 1,
39
+ height: 1,
40
+ padding: 0,
41
+ margin: -1,
42
+ overflow: "hidden",
43
+ clipPath: "inset(50%)",
44
+ whiteSpace: "nowrap",
45
+ borderWidth: 0,
46
+ },
47
+ groups: {
48
+ display: "flex",
49
+ flexWrap: "wrap",
50
+ gap: `${space.xxs} ${space.xs}`,
51
+ justifyContent: "center",
52
+ fontFamily: font.mono,
53
+ fontSize: text.base,
54
+ fontWeight: 500,
55
+ lineHeight: text.leadingRelaxed,
56
+ letterSpacing: "0.06em",
57
+ userSelect: "all",
58
+ color: color.text,
59
+ filter: "var(--ak-key-blur)",
60
+ transitionProperty: "filter",
61
+ transitionDuration: { default: "160ms", [REDUCED]: "0s" },
62
+ },
63
+ veil: {
64
+ position: "absolute",
65
+ inset: 0,
66
+ display: "grid",
67
+ placeItems: "center",
68
+ fontSize: text.xs,
69
+ color: color.textMuted,
70
+ pointerEvents: "none",
71
+ opacity: "var(--ak-veil-opacity)",
72
+ },
73
+ actions: { display: "flex", gap: space.xs, justifyContent: "center", flexWrap: "wrap" },
74
+ button: {
75
+ display: "inline-flex",
76
+ alignItems: "center",
77
+ gap: space.xxs,
78
+ backgroundColor: color.surface,
79
+ borderWidth: 1,
80
+ borderStyle: "solid",
81
+ borderColor: { default: color.border, ":hover": color.borderStrong },
82
+ borderRadius: radius.md,
83
+ color: color.text,
84
+ fontFamily: font.body,
85
+ fontSize: "0.82rem",
86
+ fontWeight: 500,
87
+ lineHeight: 1,
88
+ paddingBlock: space.xxs,
89
+ paddingInline: space.xs,
90
+ cursor: "pointer",
91
+ transitionProperty: "border-color, color",
92
+ transitionDuration: { default: motion.fast, [REDUCED]: "0s" },
93
+ outline: { default: "none", ":focus-visible": `2px solid ${color.focusRing}` },
94
+ outlineOffset: 1,
95
+ },
96
+ copied: { color: color.accent, borderColor: color.accent },
97
+ warning: {
98
+ display: "flex",
99
+ gap: space.xs,
100
+ backgroundColor: color.warningSubtle,
101
+ color: color.text,
102
+ borderWidth: 1,
103
+ borderStyle: "solid",
104
+ borderColor: `color-mix(in srgb, ${color.warning} 40%, transparent)`,
105
+ borderRadius: radius.md,
106
+ paddingBlock: space.xs,
107
+ paddingInline: space.xs,
108
+ fontSize: "0.82rem",
109
+ },
110
+ warningIcon: { color: color.warning, flex: "none", marginTop: "0.1rem" },
111
+ warningText: { margin: 0 },
112
+ });
113
+ function CopyIcon() {
114
+ return (_jsxs("svg", { width: "13", height: "13", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", "aria-hidden": "true", children: [_jsx("rect", { x: "9", y: "9", width: "12", height: "12", rx: "2" }), _jsx("path", { d: "M5 15V5a2 2 0 0 1 2-2h10" })] }));
115
+ }
116
+ function DownloadIcon() {
117
+ return (_jsx("svg", { width: "13", height: "13", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", "aria-hidden": "true", children: _jsx("path", { d: "M12 3v12m0 0 4-4m-4 4-4-4M4 21h16" }) }));
118
+ }
119
+ function WarningIcon() {
120
+ return (_jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", "aria-hidden": "true", ...stylex.props(styles.warningIcon), children: [_jsx("path", { d: "m12 3 10 18H2L12 3Z" }), _jsx("path", { d: "M12 10v4m0 3h.01" })] }));
121
+ }
122
+ export function RecoveryKey({ value, ack, onAckChange, filename = "anyknown-storage-recovery-key.txt", intro = "這是你的復原金鑰。忘記 passphrase 時,它是唯一能開回 vault 的東西 — 只會顯示這一次。", warning = "我們沒有你的金鑰副本,遺失就無法復原。把它抄在紙上,或存進密碼管理器。", ackLabel = "我已把復原金鑰抄下並存放在安全的地方。", revealLabel = "顯示復原金鑰", hideLabel = "隱藏復原金鑰", veilLabel = "hover 或點一下顯示", copyLabel = "複製", copiedLabel = "✓ 已複製", downloadLabel = "下載 .txt", }) {
123
+ const [revealed, setRevealed] = useState(false);
124
+ const { copied, copy } = useCopy();
125
+ function download() {
126
+ const url = URL.createObjectURL(new Blob([`${value}\n`], { type: "text/plain" }));
127
+ const anchor = Object.assign(document.createElement("a"), { href: url, download: filename });
128
+ anchor.click();
129
+ setTimeout(() => URL.revokeObjectURL(url), 0);
130
+ }
131
+ return (_jsxs("div", { ...stylex.props(styles.card), children: [_jsx("p", { ...stylex.props(styles.intro), children: intro }), _jsxs("div", { ...stylex.props(styles.keyBox, revealed && styles.keyRevealed), children: [_jsx("div", { ...stylex.props(styles.groups), children: value.split("-").map((group, index) => (_jsxs(Fragment, { children: [index > 0 && _jsx("span", { ...stylex.props(styles.separator), children: "-" }), _jsx("span", { children: group })] }, `${group}-${index}`))) }), _jsx("span", { "aria-hidden": "true", ...stylex.props(styles.veil), children: veilLabel })] }), _jsxs("div", { ...stylex.props(styles.actions), children: [_jsx("button", { type: "button", "aria-pressed": revealed, onClick: () => setRevealed((shown) => !shown), ...stylex.props(styles.button), children: revealed ? hideLabel : revealLabel }), _jsxs("button", { type: "button", onClick: () => copy(value), ...stylex.props(styles.button, copied && styles.copied), children: [!copied && _jsx(CopyIcon, {}), copied ? copiedLabel : copyLabel] }), _jsxs("button", { type: "button", onClick: download, ...stylex.props(styles.button), children: [_jsx(DownloadIcon, {}), downloadLabel] })] }), _jsxs("div", { role: "note", ...stylex.props(styles.warning), children: [_jsx(WarningIcon, {}), _jsx("p", { ...stylex.props(styles.warningText), children: warning })] }), _jsx(Checkbox, { checked: ack, onCheckedChange: onAckChange, label: ackLabel })] }));
132
+ }
@@ -0,0 +1,31 @@
1
+ import { type ReactNode } from "react";
2
+ export type SelectItemProps = {
3
+ value: string;
4
+ hint?: string;
5
+ disabled?: boolean;
6
+ textValue?: string;
7
+ children: ReactNode;
8
+ };
9
+ export declare function SelectItem(_props: SelectItemProps): null;
10
+ export type SelectGroupProps = {
11
+ label: string;
12
+ children: ReactNode;
13
+ };
14
+ export declare function SelectGroup(_props: SelectGroupProps): null;
15
+ export type SelectProps = {
16
+ value?: string | string[];
17
+ defaultValue?: string | string[];
18
+ onValueChange?: (value: never) => void;
19
+ placeholder?: string;
20
+ searchPlaceholder?: string;
21
+ searchLabel?: string;
22
+ emptyLabel?: (query: string) => ReactNode;
23
+ multiple?: boolean;
24
+ searchable?: boolean;
25
+ disabled?: boolean;
26
+ name?: string;
27
+ "aria-label"?: string;
28
+ "aria-labelledby"?: string;
29
+ children: ReactNode;
30
+ };
31
+ export declare function Select({ value, defaultValue, onValueChange, placeholder, searchPlaceholder, searchLabel, emptyLabel, multiple, searchable, disabled, name, children, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, }: SelectProps): import("react").JSX.Element;