@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,32 @@
1
+ import { type ReactNode } from "react";
2
+ export type Rejection = {
3
+ file: File;
4
+ reason: "size" | "type" | "count";
5
+ };
6
+ export type DropzoneProps = {
7
+ onFiles: (files: File[]) => void;
8
+ onReject?: (rejections: Rejection[]) => void;
9
+ maxSize?: number;
10
+ multiple?: boolean;
11
+ accept?: string;
12
+ disabled?: boolean;
13
+ title?: ReactNode;
14
+ hint?: ReactNode;
15
+ pickLabel?: string;
16
+ children?: ReactNode;
17
+ };
18
+ export declare function Dropzone({ onFiles, onReject, maxSize, multiple, accept, disabled, title, hint, pickLabel, children, }: DropzoneProps): import("react").JSX.Element;
19
+ export type UploadJob = {
20
+ id: string;
21
+ name: string;
22
+ size: number;
23
+ state: "queued" | "encrypting" | "uploading" | "done" | "failed";
24
+ progress?: number;
25
+ error?: string;
26
+ };
27
+ export type UploadListProps = {
28
+ jobs: UploadJob[];
29
+ onCancel?: (id: string) => void;
30
+ label?: string;
31
+ };
32
+ export declare function UploadList({ jobs, onCancel, label }: UploadListProps): import("react").JSX.Element;
@@ -0,0 +1,242 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import * as stylex from "@stylexjs/stylex";
3
+ import { useRef, useState } from "react";
4
+ import { reset } from "../../lib/styled.js";
5
+ import { formatBytes } from "../../lib/format.js";
6
+ import { color, font, motion, radius, space, text } from "../../tokens.stylex.js";
7
+ const REDUCED = "@media (prefers-reduced-motion: reduce)";
8
+ const sew = stylex.keyframes({ to: { strokeDashoffset: -13 } });
9
+ const styles = stylex.create({
10
+ zone: {
11
+ position: "relative",
12
+ display: "grid",
13
+ justifyItems: "center",
14
+ gap: space.xs,
15
+ borderRadius: radius.lg,
16
+ paddingBlock: space.xl,
17
+ paddingInline: space.lg,
18
+ textAlign: "center",
19
+ fontFamily: font.body,
20
+ backgroundColor: "transparent",
21
+ transitionProperty: "background-color",
22
+ transitionDuration: { default: "140ms", [REDUCED]: "0s" },
23
+ },
24
+ over: { backgroundColor: color.accentSubtle },
25
+ disabled: { opacity: 0.5, cursor: "not-allowed" },
26
+ stitch: { position: "absolute", inset: 0, width: "100%", height: "100%", pointerEvents: "none" },
27
+ seam: {
28
+ fill: "none",
29
+ stroke: color.borderStrong,
30
+ strokeWidth: 2,
31
+ strokeDasharray: "7 6",
32
+ transitionProperty: "stroke",
33
+ transitionDuration: { default: "140ms", [REDUCED]: "0s" },
34
+ },
35
+ seamOver: {
36
+ stroke: color.accent,
37
+ animationName: { default: sew, [REDUCED]: "none" },
38
+ animationDuration: "0.5s",
39
+ animationTimingFunction: "linear",
40
+ animationIterationCount: "infinite",
41
+ },
42
+ icon: {
43
+ color: color.textFaint,
44
+ transitionProperty: "color",
45
+ transitionDuration: { default: "140ms", [REDUCED]: "0s" },
46
+ },
47
+ iconOver: { color: color.accent },
48
+ title: { margin: 0, fontSize: text.sm, color: color.text },
49
+ hint: { color: color.textMuted, fontSize: "0.78rem" },
50
+ pick: {
51
+ backgroundColor: color.surface,
52
+ borderWidth: 1,
53
+ borderStyle: "solid",
54
+ borderColor: { default: color.border, ":hover": color.borderStrong },
55
+ borderRadius: radius.md,
56
+ color: color.text,
57
+ fontFamily: font.body,
58
+ fontSize: "0.82rem",
59
+ fontWeight: 500,
60
+ lineHeight: 1,
61
+ paddingBlock: space.xxs,
62
+ paddingInline: space.xs,
63
+ cursor: "pointer",
64
+ marginTop: space.xxs,
65
+ outline: { default: "none", ":focus-visible": `2px solid ${color.focusRing}` },
66
+ outlineOffset: 1,
67
+ },
68
+ hiddenInput: {
69
+ position: "absolute",
70
+ width: 1,
71
+ height: 1,
72
+ padding: 0,
73
+ margin: -1,
74
+ overflow: "hidden",
75
+ clipPath: "inset(50%)",
76
+ whiteSpace: "nowrap",
77
+ borderWidth: 0,
78
+ },
79
+ jobs: {
80
+ listStyle: "none",
81
+ margin: 0,
82
+ padding: 0,
83
+ borderWidth: 1,
84
+ borderStyle: "solid",
85
+ borderColor: color.border,
86
+ borderRadius: radius.lg,
87
+ backgroundColor: color.surface,
88
+ overflow: "hidden",
89
+ fontFamily: font.body,
90
+ },
91
+ empty: {
92
+ position: "absolute",
93
+ width: 1,
94
+ height: 1,
95
+ padding: 0,
96
+ margin: -1,
97
+ overflow: "hidden",
98
+ clipPath: "inset(50%)",
99
+ whiteSpace: "nowrap",
100
+ borderWidth: 0,
101
+ },
102
+ job: {
103
+ display: "grid",
104
+ gridTemplateColumns: "minmax(0, 1fr) auto",
105
+ gap: `${space.xxs} ${space.xs}`,
106
+ paddingBlock: space.xs,
107
+ paddingInline: space.xs,
108
+ borderBottomWidth: 1,
109
+ borderBottomStyle: "solid",
110
+ borderBottomColor: color.border,
111
+ fontSize: text.sm,
112
+ color: color.text,
113
+ ":last-child": { borderBottomWidth: 0 },
114
+ },
115
+ jobError: { backgroundColor: color.dangerSubtle },
116
+ name: { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
117
+ cancel: {
118
+ cursor: "pointer",
119
+ color: { default: color.textMuted, ":hover": color.text },
120
+ backgroundColor: { default: "transparent", ":hover": color.accentSubtle },
121
+ width: "1.4rem",
122
+ height: "1.4rem",
123
+ display: "grid",
124
+ placeItems: "center",
125
+ borderRadius: radius.sm,
126
+ justifySelf: "end",
127
+ outline: { default: "none", ":focus-visible": `2px solid ${color.focusRing}` },
128
+ },
129
+ track: {
130
+ gridColumn: "1 / -1",
131
+ height: "0.25rem",
132
+ borderRadius: radius.full,
133
+ backgroundColor: color.border,
134
+ overflow: "hidden",
135
+ },
136
+ fill: (percent) => ({ width: `${percent}%` }),
137
+ bar: {
138
+ display: "block",
139
+ height: "100%",
140
+ backgroundColor: color.accent,
141
+ borderRadius: radius.full,
142
+ transitionProperty: "width",
143
+ transitionDuration: { default: motion.normal, [REDUCED]: "0s" },
144
+ transitionTimingFunction: "linear",
145
+ },
146
+ status: { gridColumn: "1 / -1", fontSize: "0.75rem", color: color.textMuted },
147
+ statusError: { color: color.danger },
148
+ });
149
+ function UploadIcon({ over }) {
150
+ return (_jsxs("svg", { width: "30", height: "30", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.6", "aria-hidden": "true", ...stylex.props(styles.icon, over && styles.iconOver), children: [_jsx("path", { d: "M4 16.2A4.5 4.5 0 0 1 6.6 8a6 6 0 0 1 11.6 1.6A4 4 0 0 1 18 17" }), _jsx("path", { d: "M12 12v9m0-9 3.5 3.5M12 12l-3.5 3.5" })] }));
151
+ }
152
+ function isFileDrag(event) {
153
+ return Array.from(event.dataTransfer?.types ?? []).includes("Files");
154
+ }
155
+ export function Dropzone({ onFiles, onReject, maxSize = Number.POSITIVE_INFINITY, multiple = true, accept, disabled = false, title = "把檔案拖到這裡上傳", hint = "加密在你的裝置上完成,才會離開瀏覽器。", pickLabel = "選擇檔案", children, }) {
156
+ const input = useRef(null);
157
+ const depth = useRef(0);
158
+ const [over, setOver] = useState(false);
159
+ function matchesAccept(file) {
160
+ if (!accept)
161
+ return true;
162
+ return accept
163
+ .split(",")
164
+ .map((pattern) => pattern.trim().toLowerCase())
165
+ .filter(Boolean)
166
+ .some((pattern) => {
167
+ if (pattern.startsWith("."))
168
+ return file.name.toLowerCase().endsWith(pattern);
169
+ if (pattern.endsWith("/*"))
170
+ return file.type.startsWith(pattern.slice(0, -1));
171
+ return file.type === pattern;
172
+ });
173
+ }
174
+ // Drops bypass the input's own `accept`/`multiple`, so both are enforced here.
175
+ function accepted(files) {
176
+ const passed = [];
177
+ const rejected = [];
178
+ for (const file of files) {
179
+ if (file.size > maxSize)
180
+ rejected.push({ file, reason: "size" });
181
+ else if (!matchesAccept(file))
182
+ rejected.push({ file, reason: "type" });
183
+ else if (!multiple && passed.length === 1)
184
+ rejected.push({ file, reason: "count" });
185
+ else
186
+ passed.push(file);
187
+ }
188
+ if (passed.length > 0)
189
+ onFiles(passed);
190
+ if (rejected.length > 0)
191
+ onReject?.(rejected);
192
+ }
193
+ return (_jsxs("div", { onDragEnter: (event) => {
194
+ if (!isFileDrag(event))
195
+ return;
196
+ event.preventDefault();
197
+ if (disabled)
198
+ return;
199
+ depth.current += 1;
200
+ setOver(true);
201
+ }, onDragOver: (event) => {
202
+ if (!isFileDrag(event))
203
+ return;
204
+ // Always prevented: without it the browser handles the drop itself and
205
+ // navigates the window to the file, taking the app's state with it.
206
+ event.preventDefault();
207
+ event.dataTransfer.dropEffect = disabled ? "none" : "copy";
208
+ }, onDragLeave: (event) => {
209
+ if (!isFileDrag(event))
210
+ return;
211
+ depth.current = Math.max(0, depth.current - 1);
212
+ if (depth.current === 0)
213
+ setOver(false);
214
+ }, onDrop: (event) => {
215
+ if (!isFileDrag(event))
216
+ return;
217
+ event.preventDefault();
218
+ depth.current = 0;
219
+ setOver(false);
220
+ if (disabled)
221
+ return;
222
+ accepted(Array.from(event.dataTransfer.files));
223
+ }, ...stylex.props(styles.zone, over && styles.over, disabled && styles.disabled), children: [_jsx("svg", { "aria-hidden": "true", ...stylex.props(styles.stitch), children: _jsx("rect", { x: "1.5", y: "1.5", width: "calc(100% - 3px)", height: "calc(100% - 3px)", rx: "11", ...stylex.props(styles.seam, over && styles.seamOver) }) }), _jsx(UploadIcon, { over: over }), children ?? (_jsxs(_Fragment, { children: [_jsx("p", { ...stylex.props(styles.title), children: title }), _jsx("small", { ...stylex.props(styles.hint), children: hint })] })), _jsx("button", { type: "button", disabled: disabled, onClick: () => input.current?.click(), ...stylex.props(styles.pick), children: pickLabel }), _jsx("input", { ref: input, type: "file", "aria-hidden": "true", tabIndex: -1, multiple: multiple, accept: accept, disabled: disabled, onChange: (event) => {
224
+ accepted(Array.from(event.currentTarget.files ?? []));
225
+ event.currentTarget.value = "";
226
+ }, ...stylex.props(styles.hiddenInput) })] }));
227
+ }
228
+ const JOB_STATE_LABEL = {
229
+ queued: "排隊中",
230
+ encrypting: "加密中",
231
+ uploading: "上傳中",
232
+ done: "完成",
233
+ failed: "失敗",
234
+ };
235
+ export function UploadList({ jobs, onCancel, label = "上傳中的檔案" }) {
236
+ return (_jsx("ul", { "aria-live": "polite", "aria-label": label, ...stylex.props(styles.jobs, jobs.length === 0 && styles.empty), children: jobs.map((job) => {
237
+ const failed = job.state === "failed";
238
+ return (_jsxs("li", { ...stylex.props(styles.job, failed && styles.jobError), children: [_jsx("span", { ...stylex.props(styles.name), children: job.name }), onCancel != null && job.state !== "done" && (_jsx("button", { type: "button", "aria-label": `取消上傳 ${job.name}`, onClick: () => onCancel(job.id), ...stylex.props(reset.control, styles.cancel), children: "\u2715" })), !failed && job.progress != null && (_jsx("div", { role: "progressbar", "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": Math.round(job.progress), "aria-valuetext": `${job.name} ${JOB_STATE_LABEL[job.state]} ${Math.round(job.progress)}%`, ...stylex.props(styles.track), children: _jsx("b", { ...stylex.props(styles.bar, styles.fill(job.progress)) }) })), _jsx("span", { ...stylex.props(styles.status, failed && styles.statusError), children: failed
239
+ ? (job.error ?? `超過上限(${formatBytes(job.size)}),沒有上傳。`)
240
+ : `${JOB_STATE_LABEL[job.state]} · ${formatBytes(job.size)}` })] }, job.id));
241
+ }) }));
242
+ }
@@ -0,0 +1,9 @@
1
+ import type { ReactNode } from "react";
2
+ export type EmptyStateProps = {
3
+ icon?: ReactNode;
4
+ title: ReactNode;
5
+ description?: ReactNode;
6
+ action?: ReactNode;
7
+ headingLevel?: 1 | 2 | 3 | 4 | 5 | 6;
8
+ };
9
+ export declare function EmptyState({ icon, title, description, action, headingLevel }: EmptyStateProps): import("react").JSX.Element;
@@ -0,0 +1,41 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import * as stylex from "@stylexjs/stylex";
3
+ import { color, font, radius, space, text } from "../../tokens.stylex.js";
4
+ const styles = stylex.create({
5
+ root: {
6
+ display: "grid",
7
+ justifyItems: "center",
8
+ gap: space.xs,
9
+ textAlign: "center",
10
+ paddingBlock: space.xl,
11
+ paddingInline: space.lg,
12
+ borderWidth: 1,
13
+ borderStyle: "dashed",
14
+ borderColor: color.border,
15
+ borderRadius: radius.lg,
16
+ fontFamily: font.body,
17
+ },
18
+ icon: {
19
+ display: "grid",
20
+ placeItems: "center",
21
+ width: "2.6rem",
22
+ height: "2.6rem",
23
+ borderRadius: radius.full,
24
+ backgroundColor: color.accentSubtle,
25
+ color: color.accent,
26
+ },
27
+ title: {
28
+ fontFamily: font.display,
29
+ fontSize: text.base,
30
+ fontWeight: 500,
31
+ lineHeight: text.leadingSnug,
32
+ margin: 0,
33
+ color: color.text,
34
+ },
35
+ description: { margin: 0, fontSize: text.sm, color: color.textMuted, maxWidth: "18rem" },
36
+ action: { marginTop: space.xxs },
37
+ });
38
+ export function EmptyState({ icon, title, description, action, headingLevel = 3 }) {
39
+ const Heading = `h${headingLevel}`;
40
+ return (_jsxs("div", { ...stylex.props(styles.root), children: [icon != null && (_jsx("span", { "aria-hidden": "true", ...stylex.props(styles.icon), children: icon })), _jsx(Heading, { ...stylex.props(styles.title), children: title }), description != null && _jsx("p", { ...stylex.props(styles.description), children: description }), action != null && _jsx("div", { ...stylex.props(styles.action), children: action })] }));
41
+ }
@@ -0,0 +1,32 @@
1
+ import type { ReactNode } from "react";
2
+ export type FileItem = {
3
+ kind: "file" | "folder";
4
+ name: string;
5
+ size?: number;
6
+ mtime?: string;
7
+ mime?: string;
8
+ };
9
+ export type FileRowAction = {
10
+ icon: ReactNode;
11
+ label: string;
12
+ onAction: () => void;
13
+ };
14
+ export type FileRowProps = {
15
+ item: FileItem;
16
+ selected?: boolean;
17
+ onSelectChange?: (selected: boolean) => void;
18
+ onOpen?: () => void;
19
+ actions?: FileRowAction[];
20
+ state?: "idle" | "encrypting" | "uploading";
21
+ progress?: number;
22
+ icon?: ReactNode;
23
+ selectLabel?: (name: string) => string;
24
+ };
25
+ export declare function FileRow({ item, selected, onSelectChange, onOpen, actions, state, progress, icon, selectLabel, }: FileRowProps): import("react").JSX.Element;
26
+ export type FileListProps = {
27
+ label: string;
28
+ selectedCount?: number;
29
+ selectedLabel?: (count: number) => string;
30
+ children: ReactNode;
31
+ };
32
+ export declare function FileList({ label, selectedCount, selectedLabel, children, }: FileListProps): import("react").JSX.Element;
@@ -0,0 +1,153 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import * as stylex from "@stylexjs/stylex";
3
+ import { reset } from "../../lib/styled.js";
4
+ import { formatBytes } from "../../lib/format.js";
5
+ import { color, font, radius, space, text } from "../../tokens.stylex.js";
6
+ const REDUCED = "@media (prefers-reduced-motion: reduce)";
7
+ const spin = stylex.keyframes({ to: { rotate: "360deg" } });
8
+ const styles = stylex.create({
9
+ list: {
10
+ borderWidth: 1,
11
+ borderStyle: "solid",
12
+ borderColor: color.border,
13
+ borderRadius: radius.lg,
14
+ backgroundColor: color.surface,
15
+ overflow: "hidden",
16
+ },
17
+ row: {
18
+ display: "grid",
19
+ gridTemplateColumns: "2rem 1.5rem minmax(0, 1fr) 5.5rem 6.5rem 4.6rem",
20
+ alignItems: "center",
21
+ gap: space.xxs,
22
+ height: "2.6rem",
23
+ paddingInline: space.xxs,
24
+ borderBottomWidth: 1,
25
+ borderBottomStyle: "solid",
26
+ borderBottomColor: color.border,
27
+ fontFamily: font.body,
28
+ fontSize: text.sm,
29
+ color: color.text,
30
+ cursor: "default",
31
+ userSelect: "none",
32
+ backgroundColor: { default: "transparent", ":hover": color.accentSubtle },
33
+ "--ak-row-affordance": { default: "0", ":hover": "1", ":focus-within": "1" },
34
+ ":last-child": { borderBottomWidth: 0 },
35
+ outline: { default: "none", ":focus-visible": `2px solid ${color.focusRing}` },
36
+ outlineOffset: -2,
37
+ },
38
+ selected: { backgroundColor: color.accentSubtle, "--ak-row-affordance": "1" },
39
+ busy: { color: color.textMuted, cursor: "progress" },
40
+ checkCell: { display: "grid", placeItems: "center" },
41
+ check: {
42
+ justifySelf: "center",
43
+ width: "1rem",
44
+ height: "1rem",
45
+ margin: 0,
46
+ accentColor: color.accent,
47
+ opacity: { default: "var(--ak-row-affordance, 0)", ":focus-visible": 1 },
48
+ },
49
+ icon: { color: color.textMuted },
50
+ folderIcon: { color: color.accent },
51
+ name: { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
52
+ size: {
53
+ fontFamily: font.mono,
54
+ fontSize: "0.78rem",
55
+ lineHeight: 1,
56
+ fontVariantNumeric: "tabular-nums",
57
+ color: color.textMuted,
58
+ textAlign: "end",
59
+ },
60
+ mtime: { fontSize: "0.78rem", color: color.textMuted },
61
+ actions: {
62
+ display: "flex",
63
+ justifyContent: "end",
64
+ gap: "0.15rem",
65
+ opacity: { default: "var(--ak-row-affordance, 0)", ":focus-within": 1 },
66
+ },
67
+ action: {
68
+ cursor: "pointer",
69
+ width: "1.6rem",
70
+ height: "1.6rem",
71
+ display: "grid",
72
+ placeItems: "center",
73
+ borderRadius: radius.sm,
74
+ color: { default: color.textMuted, ":hover": color.text },
75
+ backgroundColor: { default: "transparent", ":hover": color.surface },
76
+ outline: { default: "none", ":focus-visible": `2px solid ${color.focusRing}` },
77
+ },
78
+ count: {
79
+ fontFamily: font.body,
80
+ fontSize: "0.78rem",
81
+ color: color.textMuted,
82
+ margin: 0,
83
+ marginTop: space.xxs,
84
+ },
85
+ busyCell: {
86
+ gridColumn: "4 / 7",
87
+ display: "flex",
88
+ alignItems: "center",
89
+ gap: space.xs,
90
+ fontSize: "0.75rem",
91
+ },
92
+ track: {
93
+ flex: 1,
94
+ height: "0.2rem",
95
+ borderRadius: radius.full,
96
+ backgroundColor: color.border,
97
+ overflow: "hidden",
98
+ },
99
+ fill: (percent) => ({ width: `${percent}%` }),
100
+ bar: { display: "block", height: "100%", backgroundColor: color.accent },
101
+ spinner: {
102
+ width: 12,
103
+ height: 12,
104
+ flex: "none",
105
+ borderWidth: 1.5,
106
+ borderStyle: "solid",
107
+ borderColor: color.border,
108
+ borderTopColor: color.accent,
109
+ borderRadius: radius.full,
110
+ animationName: { default: spin, [REDUCED]: "none" },
111
+ animationDuration: "1.2s",
112
+ animationTimingFunction: "linear",
113
+ animationIterationCount: "infinite",
114
+ },
115
+ });
116
+ function FolderIcon() {
117
+ return (_jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.8", "aria-hidden": "true", ...stylex.props(styles.icon, styles.folderIcon), children: _jsx("path", { d: "M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7Z" }) }));
118
+ }
119
+ function FileIcon() {
120
+ return (_jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.8", "aria-hidden": "true", ...stylex.props(styles.icon), children: [_jsx("path", { d: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6Z" }), _jsx("path", { d: "M14 2v6h6M8 13h8M8 17h5" })] }));
121
+ }
122
+ export function FileRow({ item, selected = false, onSelectChange, onOpen, actions = [], state = "idle", progress = 0, icon, selectLabel = (name) => `選取 ${name}`, }) {
123
+ const busy = state !== "idle";
124
+ function onKeyDown(event) {
125
+ // Only the row's own keys; anything bubbling from the checkbox or an
126
+ // action button belongs to that control.
127
+ if (busy || event.target !== event.currentTarget)
128
+ return;
129
+ if (event.key === " ") {
130
+ event.preventDefault();
131
+ onSelectChange?.(!selected);
132
+ }
133
+ else if (event.key === "Enter") {
134
+ event.preventDefault();
135
+ onOpen?.();
136
+ }
137
+ }
138
+ if (busy) {
139
+ return (_jsxs("div", { role: "row", tabIndex: 0, "aria-busy": "true", "aria-selected": selected, ...stylex.props(styles.row, styles.busy), children: [_jsx("span", { role: "gridcell", "aria-label": selectLabel(item.name) }), _jsx("span", { role: "gridcell", "aria-hidden": "true", children: icon ?? (item.kind === "folder" ? _jsx(FolderIcon, {}) : _jsx(FileIcon, {})) }), _jsx("span", { role: "gridcell", ...stylex.props(styles.name), children: item.name }), _jsx("span", { role: "gridcell", "aria-colspan": 3, ...stylex.props(styles.busyCell), children: state === "encrypting" ? (_jsxs(_Fragment, { children: [_jsx("span", { "aria-hidden": "true", ...stylex.props(styles.spinner) }), "\u52A0\u5BC6\u4E2D"] })) : (_jsxs(_Fragment, { children: [_jsx("span", { role: "progressbar", "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": Math.round(progress), "aria-valuetext": `${item.name} 上傳中 ${Math.round(progress)}%`, ...stylex.props(styles.track), children: _jsx("b", { ...stylex.props(styles.bar, styles.fill(progress)) }) }), `${Math.round(progress)}%`] })) })] }));
140
+ }
141
+ return (_jsxs("div", { role: "row", tabIndex: 0, "aria-selected": selected, onClick: (event) => {
142
+ // The second click of a double-click must not undo the first's toggle.
143
+ if (event.detail > 1)
144
+ return;
145
+ onSelectChange?.(!selected);
146
+ }, onDoubleClick: () => onOpen?.(), onKeyDown: onKeyDown, ...stylex.props(styles.row, selected && styles.selected), children: [_jsx("span", { role: "gridcell", ...stylex.props(styles.checkCell), children: _jsx("input", { type: "checkbox", checked: selected, "aria-label": selectLabel(item.name), onClick: (event) => event.stopPropagation(), onChange: (event) => onSelectChange?.(event.currentTarget.checked), ...stylex.props(styles.check) }) }), _jsx("span", { role: "gridcell", "aria-hidden": "true", children: icon ?? (item.kind === "folder" ? _jsx(FolderIcon, {}) : _jsx(FileIcon, {})) }), _jsx("span", { role: "gridcell", ...stylex.props(styles.name), children: item.name }), _jsx("span", { role: "gridcell", ...stylex.props(styles.size), children: item.kind === "folder" || item.size == null ? "—" : formatBytes(item.size) }), _jsx("span", { role: "gridcell", ...stylex.props(styles.mtime), children: item.mtime ?? "" }), _jsx("span", { role: "gridcell", ...stylex.props(styles.actions), children: actions.map((action) => (_jsx("button", { type: "button", "aria-label": action.label, onClick: (event) => {
147
+ event.stopPropagation();
148
+ action.onAction();
149
+ }, ...stylex.props(reset.control, styles.action), children: action.icon }, action.label))) })] }));
150
+ }
151
+ export function FileList({ label, selectedCount, selectedLabel = (count) => `已選取 ${count} 個項目`, children, }) {
152
+ return (_jsxs(_Fragment, { children: [_jsx("div", { role: "grid", "aria-label": label, "aria-multiselectable": "true", ...stylex.props(styles.list), children: children }), selectedCount != null && (_jsx("p", { "aria-live": "polite", ...stylex.props(styles.count), children: selectedCount > 0 ? selectedLabel(selectedCount) : "" }))] }));
153
+ }
@@ -0,0 +1,15 @@
1
+ import { type ReactNode } from "react";
2
+ export type HandoffReason = "soft-threshold" | "hard-limit" | "state-transition";
3
+ export type HandoffReceiptProps = {
4
+ at: string;
5
+ ctxPercent: number;
6
+ reason?: HandoffReason;
7
+ memory: {
8
+ count: number;
9
+ items?: string[];
10
+ };
11
+ ledgerCount: number;
12
+ handoffSummary?: ReactNode;
13
+ defaultOpen?: boolean;
14
+ };
15
+ export declare function HandoffReceipt({ at, ctxPercent, reason, memory, ledgerCount, handoffSummary, defaultOpen, }: HandoffReceiptProps): import("react").JSX.Element;