@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.
- package/LICENSE +21 -0
- package/README.md +121 -0
- package/dist/components/action-bar/ActionBar.d.ts +28 -0
- package/dist/components/action-bar/ActionBar.js +73 -0
- package/dist/components/badge/Badge.d.ts +28 -0
- package/dist/components/badge/Badge.js +79 -0
- package/dist/components/button/Button.d.ts +7 -0
- package/dist/components/button/Button.js +86 -0
- package/dist/components/card/Card.d.ts +2 -0
- package/dist/components/card/Card.js +17 -0
- package/dist/components/checkbox/Checkbox.d.ts +8 -0
- package/dist/components/checkbox/Checkbox.js +102 -0
- package/dist/components/code-block/CodeBlock.d.ts +10 -0
- package/dist/components/code-block/CodeBlock.js +103 -0
- package/dist/components/composer/Composer.d.ts +27 -0
- package/dist/components/composer/Composer.js +278 -0
- package/dist/components/data-table/DataTable.d.ts +38 -0
- package/dist/components/data-table/DataTable.js +256 -0
- package/dist/components/dialog/Dialog.d.ts +36 -0
- package/dist/components/dialog/Dialog.js +77 -0
- package/dist/components/diff-viewer/DiffViewer.d.ts +13 -0
- package/dist/components/diff-viewer/DiffViewer.js +133 -0
- package/dist/components/dropdown/DropdownMenu.d.ts +43 -0
- package/dist/components/dropdown/DropdownMenu.js +86 -0
- package/dist/components/dropzone/Dropzone.d.ts +32 -0
- package/dist/components/dropzone/Dropzone.js +242 -0
- package/dist/components/empty-state/EmptyState.d.ts +9 -0
- package/dist/components/empty-state/EmptyState.js +41 -0
- package/dist/components/file-row/FileRow.d.ts +32 -0
- package/dist/components/file-row/FileRow.js +153 -0
- package/dist/components/handoff-receipt/HandoffReceipt.d.ts +15 -0
- package/dist/components/handoff-receipt/HandoffReceipt.js +147 -0
- package/dist/components/input/Input.d.ts +48 -0
- package/dist/components/input/Input.js +64 -0
- package/dist/components/interaction-card/InteractionCard.d.ts +59 -0
- package/dist/components/interaction-card/InteractionCard.js +238 -0
- package/dist/components/kbd/Kbd.d.ts +9 -0
- package/dist/components/kbd/Kbd.js +43 -0
- package/dist/components/label/Field.d.ts +11 -0
- package/dist/components/label/Field.js +28 -0
- package/dist/components/label/Label.d.ts +7 -0
- package/dist/components/label/Label.js +21 -0
- package/dist/components/label/fieldContext.d.ts +26 -0
- package/dist/components/label/fieldContext.js +16 -0
- package/dist/components/message/Message.d.ts +29 -0
- package/dist/components/message/Message.js +94 -0
- package/dist/components/password-input/PasswordInput.d.ts +20 -0
- package/dist/components/password-input/PasswordInput.js +141 -0
- package/dist/components/popover/Popover.d.ts +38 -0
- package/dist/components/popover/Popover.js +37 -0
- package/dist/components/progress/Progress.d.ts +36 -0
- package/dist/components/progress/Progress.js +200 -0
- package/dist/components/radio/Radio.d.ts +7 -0
- package/dist/components/radio/Radio.js +89 -0
- package/dist/components/radio/RadioGroup.d.ts +21 -0
- package/dist/components/radio/RadioGroup.js +34 -0
- package/dist/components/reasoning-fold/ReasoningFold.d.ts +10 -0
- package/dist/components/reasoning-fold/ReasoningFold.js +92 -0
- package/dist/components/recovery-key/RecoveryKey.d.ts +17 -0
- package/dist/components/recovery-key/RecoveryKey.js +132 -0
- package/dist/components/select/Select.d.ts +31 -0
- package/dist/components/select/Select.js +209 -0
- package/dist/components/skeleton/Skeleton.d.ts +20 -0
- package/dist/components/skeleton/Skeleton.js +57 -0
- package/dist/components/switch/Switch.d.ts +7 -0
- package/dist/components/switch/Switch.js +108 -0
- package/dist/components/tabs/Tabs.d.ts +28 -0
- package/dist/components/tabs/Tabs.js +133 -0
- package/dist/components/text/Text.d.ts +7 -0
- package/dist/components/text/Text.js +31 -0
- package/dist/components/textarea/Textarea.d.ts +7 -0
- package/dist/components/textarea/Textarea.js +25 -0
- package/dist/components/toast/Toast.d.ts +56 -0
- package/dist/components/toast/Toast.js +175 -0
- package/dist/components/tool-card/ToolCard.d.ts +55 -0
- package/dist/components/tool-card/ToolCard.js +281 -0
- package/dist/components/tooltip/Tooltip.d.ts +12 -0
- package/dist/components/tooltip/Tooltip.js +38 -0
- package/dist/components/voice-indicator/VoiceIndicator.d.ts +7 -0
- package/dist/components/voice-indicator/VoiceIndicator.js +51 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.js +38 -0
- package/dist/lib/diff.d.ts +28 -0
- package/dist/lib/diff.js +133 -0
- package/dist/lib/format.d.ts +2 -0
- package/dist/lib/format.js +24 -0
- package/dist/lib/format.test.d.ts +1 -0
- package/dist/lib/format.test.js +37 -0
- package/dist/lib/mergeRefs.d.ts +2 -0
- package/dist/lib/mergeRefs.js +8 -0
- package/dist/lib/motion.d.ts +1 -0
- package/dist/lib/motion.js +10 -0
- package/dist/lib/paths.d.ts +3 -0
- package/dist/lib/paths.js +11 -0
- package/dist/lib/popup.d.ts +29 -0
- package/dist/lib/popup.js +27 -0
- package/dist/lib/progress.d.ts +2 -0
- package/dist/lib/progress.js +28 -0
- package/dist/lib/silk.d.ts +66 -0
- package/dist/lib/silk.js +337 -0
- package/dist/lib/styled.d.ts +29 -0
- package/dist/lib/styled.js +30 -0
- package/dist/lib/svgId.d.ts +1 -0
- package/dist/lib/svgId.js +4 -0
- package/dist/lib/useAnimationFrame.d.ts +1 -0
- package/dist/lib/useAnimationFrame.js +21 -0
- package/dist/lib/useControllableState.d.ts +1 -0
- package/dist/lib/useControllableState.js +14 -0
- package/dist/lib/useCopy.d.ts +4 -0
- package/dist/lib/useCopy.js +19 -0
- package/dist/lib/voice.d.ts +2 -0
- package/dist/lib/voice.js +30 -0
- package/dist/lib/weave.d.ts +28 -0
- package/dist/lib/weave.js +83 -0
- package/dist/scrollbar.css +29 -0
- package/dist/themes.stylex.d.ts +49 -0
- package/dist/themes.stylex.js +52 -0
- package/dist/tokens.css +109 -0
- package/dist/tokens.stylex.d.ts +126 -0
- package/dist/tokens.stylex.js +149 -0
- 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;
|