@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,256 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import * as stylex from "@stylexjs/stylex";
3
+ import { useCallback, useEffect, useRef, useState } from "react";
4
+ import { reset } from "../../lib/styled.js";
5
+ import { color, font, motion, radius, space, text } from "../../tokens.stylex.js";
6
+ const REDUCED = "@media (prefers-reduced-motion: reduce)";
7
+ const styles = stylex.create({
8
+ toolbar: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: space.md },
9
+ filter: { position: "relative", width: "min(18rem, 100%)" },
10
+ filterIcon: {
11
+ position: "absolute",
12
+ insetInlineStart: space.xs,
13
+ insetBlockStart: "50%",
14
+ translate: "0 -50%",
15
+ color: color.textFaint,
16
+ pointerEvents: "none",
17
+ display: "flex",
18
+ },
19
+ filterInput: {
20
+ width: "100%",
21
+ boxSizing: "border-box",
22
+ backgroundColor: color.surface,
23
+ borderWidth: 1,
24
+ borderStyle: "solid",
25
+ borderColor: { default: color.border, ":hover": color.borderStrong, ":focus-visible": color.focusRing },
26
+ borderRadius: radius.md,
27
+ color: color.text,
28
+ fontFamily: font.body,
29
+ fontSize: text.sm,
30
+ paddingBlock: space.xxs,
31
+ paddingInlineStart: space.xl,
32
+ paddingInlineEnd: space.xs,
33
+ minHeight: "1.9rem",
34
+ transitionProperty: "border-color",
35
+ transitionDuration: { default: motion.fast, [REDUCED]: "0s" },
36
+ outline: { default: "none", ":focus-visible": `2px solid ${color.focusRing}` },
37
+ outlineOffset: -1,
38
+ "::placeholder": { color: color.textFaint },
39
+ },
40
+ count: { fontFamily: font.mono, fontSize: "0.72rem", lineHeight: 1, color: color.textFaint },
41
+ wrap: {
42
+ outline: { default: "none", ":focus-visible": `2px solid ${color.focusRing}` },
43
+ outlineOffset: -2,
44
+ backgroundColor: color.surface,
45
+ borderWidth: 1,
46
+ borderStyle: "solid",
47
+ borderColor: color.border,
48
+ borderRadius: radius.lg,
49
+ maxHeight: "20rem",
50
+ overflow: "auto",
51
+ marginTop: space.xs,
52
+ },
53
+ table: { width: "100%", borderCollapse: "collapse", fontFamily: font.body, fontSize: text.sm },
54
+ th: {
55
+ position: "sticky",
56
+ insetBlockStart: 0,
57
+ zIndex: 1,
58
+ backgroundColor: color.surface,
59
+ boxShadow: `inset 0 -1px ${color.border}`,
60
+ textAlign: "start",
61
+ padding: 0,
62
+ whiteSpace: "nowrap",
63
+ },
64
+ thCheck: { width: "2rem", paddingBlock: space.xxs, paddingInline: space.xs },
65
+ sort: {
66
+ display: "flex",
67
+ alignItems: "center",
68
+ gap: space.xxs,
69
+ width: "100%",
70
+ boxSizing: "border-box",
71
+ cursor: "pointer",
72
+ paddingBlock: space.xs,
73
+ paddingInline: space.xs,
74
+ fontFamily: font.mono,
75
+ fontSize: "0.68rem",
76
+ fontWeight: 600,
77
+ lineHeight: 1,
78
+ letterSpacing: "0.07em",
79
+ textTransform: "uppercase",
80
+ color: { default: color.textMuted, ":hover": color.text },
81
+ borderRadius: radius.sm,
82
+ outline: { default: "none", ":focus-visible": `2px solid ${color.focusRing}` },
83
+ outlineOffset: -2,
84
+ },
85
+ plainHeader: {
86
+ display: "block",
87
+ paddingBlock: space.xs,
88
+ paddingInline: space.xs,
89
+ fontFamily: font.mono,
90
+ fontSize: "0.68rem",
91
+ fontWeight: 600,
92
+ lineHeight: 1,
93
+ letterSpacing: "0.07em",
94
+ textTransform: "uppercase",
95
+ color: color.textMuted,
96
+ },
97
+ arrow: { color: color.accent, fontSize: "0.6rem" },
98
+ row: {
99
+ backgroundColor: { default: "transparent", ":hover": color.bg },
100
+ },
101
+ rowSelected: { backgroundColor: color.accentSubtle },
102
+ td: {
103
+ borderTopWidth: 1,
104
+ borderTopStyle: "solid",
105
+ borderTopColor: color.border,
106
+ paddingBlock: space.xxs,
107
+ paddingInline: space.xs,
108
+ verticalAlign: "middle",
109
+ color: color.text,
110
+ },
111
+ tdCheck: { width: "2rem" },
112
+ mono: { fontFamily: font.mono, fontSize: "0.78rem", color: color.textMuted, whiteSpace: "nowrap" },
113
+ editable: { cursor: "text" },
114
+ empty: { color: color.textFaint },
115
+ cellInput: {
116
+ width: "100%",
117
+ fontFamily: font.body,
118
+ fontSize: text.sm,
119
+ boxShadow: `inset 0 0 0 2px ${color.focusRing}`,
120
+ borderRadius: radius.sm,
121
+ paddingInline: "0.2rem",
122
+ marginInline: "-0.2rem",
123
+ backgroundColor: color.surface,
124
+ color: color.text,
125
+ },
126
+ checkbox: {
127
+ accentColor: color.accent,
128
+ width: "0.85rem",
129
+ height: "0.85rem",
130
+ cursor: "pointer",
131
+ outline: { default: "none", ":focus-visible": `2px solid ${color.focusRing}` },
132
+ outlineOffset: 1,
133
+ },
134
+ emptyRow: {
135
+ paddingBlock: space.xl,
136
+ paddingInline: space.md,
137
+ textAlign: "center",
138
+ color: color.textMuted,
139
+ borderTopWidth: 1,
140
+ borderTopStyle: "solid",
141
+ borderTopColor: color.border,
142
+ },
143
+ clear: {
144
+ color: color.accent,
145
+ cursor: "pointer",
146
+ textDecorationLine: "underline",
147
+ textUnderlineOffset: 2,
148
+ outline: { default: "none", ":focus-visible": `2px solid ${color.focusRing}` },
149
+ outlineOffset: 2,
150
+ },
151
+ });
152
+ function SearchIcon() {
153
+ return (_jsxs("svg", { width: "13", height: "13", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", "aria-hidden": "true", children: [_jsx("circle", { cx: "11", cy: "11", r: "7" }), _jsx("path", { d: "m20 20-3.5-3.5" })] }));
154
+ }
155
+ // Entering edit mode is a user gesture (double-click), so moving focus into the
156
+ // cell is expected — but not via autoFocus, which fires on mount page-wide.
157
+ function focusOnMount(node) {
158
+ node?.focus();
159
+ node?.select();
160
+ }
161
+ function cellValue(column, row) {
162
+ return column.value?.(row) ?? "";
163
+ }
164
+ export function DataTable({ rows, total, rowKey, columns, label, filter = "", onFilterChange, filterPlaceholder = "過濾…", sort = null, onSortChange, selected, onSelectedChange, emptyState, onClearFilter, countLabel = (shown, total) => `${shown} / ${total}`, selectLabel = (key) => `選取 ${key}`, selectAllLabel = "全選", editLabel = (column, key) => `編輯 ${key} 的 ${column}`, clearLabel = "清除過濾", }) {
165
+ const [editing, setEditing] = useState(null);
166
+ const cancelling = useRef(false);
167
+ const [draft, setDraft] = useState("");
168
+ const selectAll = useRef(null);
169
+ const visible = rows;
170
+ const keys = visible.map(rowKey);
171
+ const selectedCount = selected ? keys.filter((key) => selected.has(key)).length : 0;
172
+ const allSelected = keys.length > 0 && selectedCount === keys.length;
173
+ useEffect(() => {
174
+ if (selectAll.current)
175
+ selectAll.current.indeterminate = selectedCount > 0 && !allSelected;
176
+ }, [selectedCount, allSelected]);
177
+ // A row that disappears while being edited would otherwise strand the draft
178
+ // and re-open with it when the row comes back. Adjusted during render rather
179
+ // than in an effect, so the stale cell never gets painted.
180
+ if (editing != null && !keys.includes(editing.key))
181
+ setEditing(null);
182
+ function startEdit(key, col, value) {
183
+ setEditing({ key, col });
184
+ setDraft(value);
185
+ }
186
+ // Returning focus to the cell keeps a keyboard user's place after Enter/Escape,
187
+ // but moving focus off the still-mounted input would fire its blur commit.
188
+ function leaveEdit(cell) {
189
+ cancelling.current = true;
190
+ setEditing(null);
191
+ cell?.focus();
192
+ cancelling.current = false;
193
+ }
194
+ const commit = useCallback((column, row, next, cell) => {
195
+ if (cancelling.current)
196
+ return;
197
+ cancelling.current = true;
198
+ setEditing(null);
199
+ cell?.focus();
200
+ cancelling.current = false;
201
+ if (next !== cellValue(column, row))
202
+ column.onCommit?.(row, next);
203
+ }, []);
204
+ function toggleSort(column) {
205
+ if (!column.sortable)
206
+ return;
207
+ if (sort?.col !== column.id)
208
+ onSortChange?.({ col: column.id, dir: "asc" });
209
+ else
210
+ onSortChange?.(sort.dir === "asc" ? { col: column.id, dir: "desc" } : null);
211
+ }
212
+ function toggleAll(checked) {
213
+ const next = new Set(selected ?? []);
214
+ for (const key of keys) {
215
+ if (checked)
216
+ next.add(key);
217
+ else
218
+ next.delete(key);
219
+ }
220
+ onSelectedChange?.(next);
221
+ }
222
+ return (_jsxs("div", { children: [onFilterChange != null && (_jsxs("div", { ...stylex.props(styles.toolbar), children: [_jsxs("div", { ...stylex.props(styles.filter), children: [_jsx("span", { ...stylex.props(styles.filterIcon), children: _jsx(SearchIcon, {}) }), _jsx("input", { type: "search", "aria-label": filterPlaceholder, placeholder: filterPlaceholder, value: filter, onChange: (event) => onFilterChange(event.currentTarget.value), ...stylex.props(styles.filterInput) })] }), _jsx("span", { "aria-live": "polite", ...stylex.props(styles.count), children: countLabel(visible.length, total ?? rows.length) })] })), _jsx("div", { tabIndex: 0, role: "region", "aria-label": label, ...stylex.props(styles.wrap), children: _jsxs("table", { "aria-label": label, ...stylex.props(styles.table), children: [_jsx("thead", { children: _jsxs("tr", { children: [selected != null && (_jsx("th", { scope: "col", ...stylex.props(styles.th, styles.thCheck), children: _jsx("input", { ref: selectAll, type: "checkbox", checked: allSelected, "aria-label": selectAllLabel, onChange: (event) => toggleAll(event.currentTarget.checked), ...stylex.props(styles.checkbox) }) })), columns.map((column) => (_jsx("th", { scope: "col", "aria-sort": sort?.col === column.id ? (sort.dir === "asc" ? "ascending" : "descending") : undefined, ...stylex.props(styles.th), children: column.sortable ? (_jsxs("button", { type: "button", onClick: () => toggleSort(column), ...stylex.props(reset.control, styles.sort), children: [column.header, sort?.col === column.id && (_jsx("span", { "aria-hidden": "true", ...stylex.props(styles.arrow), children: sort.dir === "asc" ? "▲" : "▼" }))] })) : (_jsx("span", { ...stylex.props(styles.plainHeader), children: column.header })) }, column.id)))] }) }), _jsx("tbody", { children: visible.length === 0 ? (_jsx("tr", { children: _jsxs("td", { colSpan: columns.length + (selected != null ? 1 : 0), ...stylex.props(styles.emptyRow), children: [emptyState?.(filter) ?? `找不到符合「${filter}」的資料。`, onClearFilter != null && (_jsxs(_Fragment, { children: [" ", _jsx("button", { type: "button", onClick: onClearFilter, ...stylex.props(reset.control, styles.clear), children: clearLabel })] }))] }) })) : (visible.map((row) => {
223
+ const key = rowKey(row);
224
+ const isSelected = selected?.has(key) ?? false;
225
+ return (_jsxs("tr", { ...stylex.props(styles.row, isSelected && styles.rowSelected), children: [selected != null && (_jsx("td", { ...stylex.props(styles.td, styles.tdCheck), children: _jsx("input", { type: "checkbox", checked: isSelected, "aria-label": selectLabel(key), onChange: (event) => {
226
+ const next = new Set(selected);
227
+ if (event.currentTarget.checked)
228
+ next.add(key);
229
+ else
230
+ next.delete(key);
231
+ onSelectedChange?.(next);
232
+ }, ...stylex.props(styles.checkbox) }) })), columns.map((column) => {
233
+ const isEditing = editing?.key === key && editing.col === column.id;
234
+ const value = cellValue(column, row);
235
+ return (_jsx("td", { tabIndex: column.editable ? 0 : undefined, onDoubleClick: column.editable ? () => startEdit(key, column.id, value) : undefined, onKeyDown: column.editable && !isEditing
236
+ ? (event) => {
237
+ if (event.key !== "Enter" && event.key !== "F2")
238
+ return;
239
+ event.preventDefault();
240
+ startEdit(key, column.id, value);
241
+ }
242
+ : undefined, ...stylex.props(styles.td, column.mono && styles.mono, column.editable && styles.editable), children: isEditing ? (_jsx("input", { ref: focusOnMount, "aria-label": editLabel(column.header, key), value: draft, onChange: (event) => setDraft(event.currentTarget.value), onBlur: (event) => commit(column, row, draft, event.currentTarget.closest("td")), onKeyDown: (event) => {
243
+ const cell = event.currentTarget.closest("td");
244
+ if (event.key === "Enter") {
245
+ event.preventDefault();
246
+ commit(column, row, draft, cell);
247
+ }
248
+ else if (event.key === "Escape") {
249
+ event.preventDefault();
250
+ leaveEdit(cell);
251
+ }
252
+ }, ...stylex.props(reset.control, styles.cellInput) })) : ((column.cell?.(row) ??
253
+ (value === "" ? _jsx("span", { ...stylex.props(styles.empty), children: "\u2014" }) : value))) }, column.id));
254
+ })] }, key));
255
+ })) })] }) })] }));
256
+ }
@@ -0,0 +1,36 @@
1
+ import { type ReactElement, type ReactNode } from "react";
2
+ export type DialogProps = {
3
+ open?: boolean;
4
+ defaultOpen?: boolean;
5
+ onOpenChange?: (open: boolean) => void;
6
+ children: ReactNode;
7
+ };
8
+ export declare function Dialog({ children, ...props }: DialogProps): import("react").JSX.Element;
9
+ export declare function DialogTrigger({ children }: {
10
+ children: ReactElement;
11
+ }): import("react").JSX.Element;
12
+ export declare function DialogClose({ children }: {
13
+ children: ReactElement;
14
+ }): import("react").JSX.Element;
15
+ export declare function DialogActions({ children }: {
16
+ children: ReactNode;
17
+ }): import("react").JSX.Element;
18
+ export type DialogContentProps = {
19
+ title: ReactNode;
20
+ description?: ReactNode;
21
+ children?: ReactNode;
22
+ };
23
+ export declare function DialogContent({ title, description, children }: DialogContentProps): import("react").JSX.Element;
24
+ export type ConfirmDialogProps = {
25
+ open?: boolean;
26
+ defaultOpen?: boolean;
27
+ onOpenChange?: (open: boolean) => void;
28
+ trigger?: ReactElement;
29
+ title: ReactNode;
30
+ description?: ReactNode;
31
+ danger?: boolean;
32
+ confirmLabel?: string;
33
+ cancelLabel?: string;
34
+ onConfirm: () => void;
35
+ };
36
+ export declare function ConfirmDialog({ trigger, title, description, danger, confirmLabel, cancelLabel, onConfirm, ...props }: ConfirmDialogProps): import("react").JSX.Element;
@@ -0,0 +1,77 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { AlertDialog } from "@base-ui/react/alert-dialog";
3
+ import { Dialog as BaseDialog } from "@base-ui/react/dialog";
4
+ import * as stylex from "@stylexjs/stylex";
5
+ import { useRef } from "react";
6
+ import { color, font, radius, shadow, space, text } from "../../tokens.stylex.js";
7
+ import { Button } from "../button/Button.js";
8
+ const REDUCED = "@media (prefers-reduced-motion: reduce)";
9
+ const grow = stylex.keyframes({
10
+ from: { opacity: 0, scale: 0.96 },
11
+ to: { opacity: 1, scale: 1 },
12
+ });
13
+ const styles = stylex.create({
14
+ backdrop: {
15
+ position: "fixed",
16
+ inset: 0,
17
+ zIndex: 70,
18
+ backgroundColor: "rgba(24, 22, 19, 0.35)",
19
+ backdropFilter: "blur(2px)",
20
+ },
21
+ viewport: {
22
+ position: "fixed",
23
+ inset: 0,
24
+ zIndex: 71,
25
+ display: "grid",
26
+ placeItems: "center",
27
+ padding: space.md,
28
+ },
29
+ popup: {
30
+ backgroundColor: color.surface,
31
+ color: color.text,
32
+ borderWidth: 1,
33
+ borderStyle: "solid",
34
+ borderColor: color.border,
35
+ borderRadius: radius.lg,
36
+ boxShadow: shadow.popover,
37
+ padding: space.lg,
38
+ width: "min(24rem, calc(100vw - 2rem))",
39
+ maxHeight: "calc(100vh - 2rem)",
40
+ overflowY: "auto",
41
+ fontFamily: font.body,
42
+ animationName: { default: grow, [REDUCED]: "none" },
43
+ animationDuration: "160ms",
44
+ animationTimingFunction: "ease-out",
45
+ outline: { default: "none", ":focus-visible": `2px solid ${color.focusRing}` },
46
+ outlineOffset: -2,
47
+ },
48
+ title: {
49
+ fontFamily: font.display,
50
+ fontSize: text.lg,
51
+ fontWeight: 500,
52
+ lineHeight: text.leadingSnug,
53
+ margin: 0,
54
+ marginBottom: space.xxs,
55
+ },
56
+ description: { fontSize: text.sm, color: color.textMuted, margin: 0, marginBottom: space.md },
57
+ actions: { display: "flex", justifyContent: "flex-end", gap: space.xs, marginTop: space.md },
58
+ });
59
+ export function Dialog({ children, ...props }) {
60
+ return _jsx(BaseDialog.Root, { ...props, children: children });
61
+ }
62
+ export function DialogTrigger({ children }) {
63
+ return _jsx(BaseDialog.Trigger, { render: children });
64
+ }
65
+ export function DialogClose({ children }) {
66
+ return _jsx(BaseDialog.Close, { render: children });
67
+ }
68
+ export function DialogActions({ children }) {
69
+ return _jsx("div", { ...stylex.props(styles.actions), children: children });
70
+ }
71
+ export function DialogContent({ title, description, children }) {
72
+ return (_jsxs(BaseDialog.Portal, { children: [_jsx(BaseDialog.Backdrop, { ...stylex.props(styles.backdrop) }), _jsx(BaseDialog.Viewport, { ...stylex.props(styles.viewport), children: _jsxs(BaseDialog.Popup, { ...stylex.props(styles.popup), children: [_jsx(BaseDialog.Title, { ...stylex.props(styles.title), children: title }), description != null && (_jsx(BaseDialog.Description, { ...stylex.props(styles.description), children: description })), children] }) })] }));
73
+ }
74
+ export function ConfirmDialog({ trigger, title, description, danger = false, confirmLabel = "確認", cancelLabel = "取消", onConfirm, ...props }) {
75
+ const cancelRef = useRef(null);
76
+ return (_jsxs(AlertDialog.Root, { ...props, children: [trigger != null && _jsx(AlertDialog.Trigger, { render: trigger }), _jsxs(AlertDialog.Portal, { children: [_jsx(AlertDialog.Backdrop, { ...stylex.props(styles.backdrop) }), _jsx(AlertDialog.Viewport, { ...stylex.props(styles.viewport), children: _jsxs(AlertDialog.Popup, { initialFocus: danger ? cancelRef : undefined, ...stylex.props(styles.popup), children: [_jsx(AlertDialog.Title, { ...stylex.props(styles.title), children: title }), description != null && (_jsx(AlertDialog.Description, { ...stylex.props(styles.description), children: description })), _jsxs("div", { ...stylex.props(styles.actions), children: [_jsx(AlertDialog.Close, { render: _jsx(Button, { ref: cancelRef, variant: "ghost" }), children: cancelLabel }), _jsx(AlertDialog.Close, { render: _jsx(Button, { variant: danger ? "danger" : "primary" }), onClick: onConfirm, children: confirmLabel })] })] }) })] })] }));
77
+ }
@@ -0,0 +1,13 @@
1
+ export type DiffFile = {
2
+ path: string;
3
+ before?: string;
4
+ after?: string;
5
+ kind?: "modified" | "added" | "deleted";
6
+ };
7
+ export type DiffViewerProps = {
8
+ file: DiffFile;
9
+ collapseContext?: number;
10
+ wordDiff?: boolean;
11
+ foldLabel?: (count: number) => string;
12
+ };
13
+ export declare function DiffViewer({ file, collapseContext, wordDiff, foldLabel, }: DiffViewerProps): import("react").JSX.Element;
@@ -0,0 +1,133 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import * as stylex from "@stylexjs/stylex";
3
+ import { Fragment, useId, useMemo, useState } from "react";
4
+ import { reset } from "../../lib/styled.js";
5
+ import { buildDiffRows, collapseRows, countChanges, diffKind } from "../../lib/diff.js";
6
+ import { color, font, motion, radius, space, text } from "../../tokens.stylex.js";
7
+ const REDUCED = "@media (prefers-reduced-motion: reduce)";
8
+ const unfold = stylex.keyframes({
9
+ from: { opacity: 0, translate: "0 -3px" },
10
+ to: { opacity: 1, translate: "0 0" },
11
+ });
12
+ const styles = stylex.create({
13
+ diff: {
14
+ backgroundColor: color.surface,
15
+ borderWidth: 1,
16
+ borderStyle: "solid",
17
+ borderColor: color.border,
18
+ borderRadius: radius.lg,
19
+ overflow: "hidden",
20
+ },
21
+ head: {
22
+ display: "flex",
23
+ alignItems: "center",
24
+ gap: space.xs,
25
+ borderBottomWidth: 1,
26
+ borderBottomStyle: "solid",
27
+ borderBottomColor: color.border,
28
+ paddingBlock: space.xs,
29
+ paddingInline: space.sm,
30
+ fontFamily: font.mono,
31
+ fontSize: "0.8rem",
32
+ fontWeight: 500,
33
+ lineHeight: 1,
34
+ color: color.text,
35
+ },
36
+ dot: { width: "0.5rem", height: "0.5rem", borderRadius: radius.full, flex: "none" },
37
+ dotModified: { backgroundColor: color.warning },
38
+ dotAdded: { backgroundColor: color.accent },
39
+ dotDeleted: { backgroundColor: color.danger },
40
+ stat: { marginInlineStart: "auto", fontSize: "0.72rem", color: color.textMuted },
41
+ plus: { color: color.success, fontWeight: 600 },
42
+ minus: { color: color.danger, fontWeight: 600 },
43
+ body: { overflowX: "auto", outline: { default: "none", ":focus-visible": `2px solid ${color.focusRing}` } },
44
+ line: {
45
+ display: "flex",
46
+ fontFamily: font.mono,
47
+ fontSize: text.code,
48
+ lineHeight: 1.55,
49
+ whiteSpace: "pre",
50
+ minWidth: "max-content",
51
+ },
52
+ lineAdd: { backgroundColor: color.successSubtle },
53
+ lineDel: { backgroundColor: color.dangerSubtle },
54
+ number: {
55
+ flex: "none",
56
+ width: "2.6rem",
57
+ textAlign: "end",
58
+ paddingInlineEnd: space.xs,
59
+ color: color.textFaint,
60
+ userSelect: "none",
61
+ fontSize: "11px",
62
+ lineHeight: 1.85,
63
+ },
64
+ sign: { flex: "none", width: "1.1rem", textAlign: "center", userSelect: "none", color: color.textFaint },
65
+ signAdd: { color: color.success },
66
+ signDel: { color: color.danger },
67
+ code: { paddingInlineEnd: space.md },
68
+ mark: { backgroundColor: "transparent", color: "inherit", borderRadius: 2 },
69
+ markAdd: { backgroundColor: color.successHl },
70
+ markDel: { backgroundColor: color.dangerHl },
71
+ fold: {
72
+ display: "flex",
73
+ alignItems: "center",
74
+ gap: space.xs,
75
+ width: "100%",
76
+ boxSizing: "border-box",
77
+ cursor: "pointer",
78
+ backgroundColor: { default: color.bg, ":hover": color.accentSubtle },
79
+ borderBlockWidth: 1,
80
+ borderBlockStyle: "solid",
81
+ borderBlockColor: color.border,
82
+ paddingBlock: space.xxs,
83
+ paddingInline: space.sm,
84
+ fontFamily: font.mono,
85
+ fontSize: "0.74rem",
86
+ lineHeight: 1,
87
+ color: { default: color.textMuted, ":hover": color.text },
88
+ outline: { default: "none", ":focus-visible": `2px solid ${color.focusRing}` },
89
+ outlineOffset: -2,
90
+ },
91
+ chevron: {
92
+ transitionProperty: "rotate",
93
+ transitionDuration: { default: motion.fast, [REDUCED]: "0s" },
94
+ transitionTimingFunction: "ease-out",
95
+ },
96
+ chevronOpen: { rotate: "90deg" },
97
+ foldLines: {
98
+ overflow: "hidden",
99
+ animationName: { default: unfold, [REDUCED]: "none" },
100
+ animationDuration: "160ms",
101
+ animationTimingFunction: "ease-out",
102
+ },
103
+ srOnly: {
104
+ position: "absolute",
105
+ width: 1,
106
+ height: 1,
107
+ padding: 0,
108
+ margin: -1,
109
+ overflow: "hidden",
110
+ clipPath: "inset(50%)",
111
+ whiteSpace: "nowrap",
112
+ borderWidth: 0,
113
+ },
114
+ });
115
+ const KIND_LABEL = { modified: "已修改", added: "新增檔案", deleted: "已刪除" };
116
+ const SIGN = { context: " ", add: "+", del: "−" };
117
+ const PREFIX = { context: "", add: "新增行 ", del: "刪除行 " };
118
+ function Line({ row }) {
119
+ return (_jsxs("div", { ...stylex.props(styles.line, row.type === "add" && styles.lineAdd, row.type === "del" && styles.lineDel), children: [_jsx("span", { "aria-hidden": "true", ...stylex.props(styles.number), children: row.beforeNo ?? "" }), _jsx("span", { "aria-hidden": "true", ...stylex.props(styles.number), children: row.afterNo ?? "" }), _jsx("span", { "aria-hidden": "true", ...stylex.props(styles.sign, row.type === "add" && styles.signAdd, row.type === "del" && styles.signDel), children: SIGN[row.type] }), _jsxs("span", { ...stylex.props(styles.code), children: [PREFIX[row.type] !== "" && _jsx("span", { ...stylex.props(styles.srOnly), children: PREFIX[row.type] }), row.segments.map((segment, index) => segment.marked ? (_jsx("mark", { ...stylex.props(styles.mark, row.type === "add" ? styles.markAdd : styles.markDel), children: segment.text }, index)) : (_jsx(Fragment, { children: segment.text }, index)))] })] }));
120
+ }
121
+ function Fold({ rows, label }) {
122
+ const id = useId();
123
+ const [open, setOpen] = useState(false);
124
+ return (_jsxs(_Fragment, { children: [_jsxs("button", { type: "button", "aria-expanded": open, "aria-controls": id, onClick: () => setOpen((value) => !value), ...stylex.props(reset.control, styles.fold), children: [_jsx("svg", { width: "10", height: "10", 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" }) }), label] }), _jsx("div", { id: id, hidden: !open, ...stylex.props(open && styles.foldLines), children: rows.map((row, index) => (_jsx(Line, { row: row }, index))) })] }));
125
+ }
126
+ export function DiffViewer({ file, collapseContext = 3, wordDiff = true, foldLabel = (count) => `⋯ ${count} 行未變動`, }) {
127
+ const kind = file.kind ?? diffKind(file.before, file.after);
128
+ const { blocks, added, removed } = useMemo(() => {
129
+ const rows = buildDiffRows(file.before ?? "", file.after ?? "", { wordDiff });
130
+ return { blocks: collapseRows(rows, collapseContext), ...countChanges(rows) };
131
+ }, [file.before, file.after, wordDiff, collapseContext]);
132
+ return (_jsxs("div", { ...stylex.props(styles.diff), children: [_jsxs("div", { ...stylex.props(styles.head), children: [_jsx("span", { "aria-hidden": "true", ...stylex.props(styles.dot, kind === "modified" && styles.dotModified, kind === "added" && styles.dotAdded, kind === "deleted" && styles.dotDeleted) }), _jsx("span", { ...stylex.props(styles.srOnly), children: KIND_LABEL[kind] }), file.path, _jsxs("span", { ...stylex.props(styles.stat), children: [_jsx("b", { ...stylex.props(styles.plus), children: `+${added}` }), " ", _jsx("b", { ...stylex.props(styles.minus), children: `−${removed}` })] })] }), _jsx("div", { tabIndex: 0, role: "region", "aria-label": `${file.path} 的變更`, ...stylex.props(styles.body), children: blocks.map((block, index) => block.kind === "fold" ? (_jsx(Fold, { rows: block.rows, label: foldLabel(block.rows.length) }, `${file.path}-${index}`)) : (block.rows.map((row, rowIndex) => _jsx(Line, { row: row }, `${index}-${rowIndex}`)))) })] }));
133
+ }
@@ -0,0 +1,43 @@
1
+ import type { ReactNode } from "react";
2
+ export type DropdownItemProps = {
3
+ icon?: ReactNode;
4
+ shortcut?: string;
5
+ variant?: "danger";
6
+ disabled?: boolean;
7
+ closeOnClick?: boolean;
8
+ onSelect?: () => void;
9
+ children: ReactNode;
10
+ };
11
+ export declare function DropdownItem({ icon, shortcut, variant, onSelect, children, ...rest }: DropdownItemProps): import("react").JSX.Element;
12
+ export type DropdownCheckboxItemProps = {
13
+ checked?: boolean;
14
+ defaultChecked?: boolean;
15
+ onCheckedChange?: (checked: boolean) => void;
16
+ disabled?: boolean;
17
+ closeOnClick?: boolean;
18
+ children: ReactNode;
19
+ };
20
+ export declare function DropdownCheckboxItem({ children, ...rest }: DropdownCheckboxItemProps): import("react").JSX.Element;
21
+ export type DropdownGroupProps = {
22
+ label?: string;
23
+ children: ReactNode;
24
+ };
25
+ export declare function DropdownGroup({ label, children }: DropdownGroupProps): import("react").JSX.Element;
26
+ export declare function DropdownSeparator(): import("react").JSX.Element;
27
+ export type DropdownSubProps = {
28
+ label: ReactNode;
29
+ icon?: ReactNode;
30
+ disabled?: boolean;
31
+ children: ReactNode;
32
+ };
33
+ export declare function DropdownSub({ label, icon, disabled, children }: DropdownSubProps): import("react").JSX.Element;
34
+ export type DropdownMenuProps = {
35
+ trigger: ReactNode;
36
+ open?: boolean;
37
+ defaultOpen?: boolean;
38
+ onOpenChange?: (open: boolean) => void;
39
+ side?: "top" | "bottom" | "left" | "right";
40
+ align?: "start" | "center" | "end";
41
+ children: ReactNode;
42
+ };
43
+ export declare function DropdownMenu({ trigger, open, defaultOpen, onOpenChange, side, align, children, }: DropdownMenuProps): import("react").JSX.Element;
@@ -0,0 +1,86 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Menu } from "@base-ui/react/menu";
3
+ import * as stylex from "@stylexjs/stylex";
4
+ import { popupStyles } from "../../lib/popup.js";
5
+ import { color, font, radius, space, text } from "../../tokens.stylex.js";
6
+ const REDUCED = "@media (prefers-reduced-motion: reduce)";
7
+ const styles = stylex.create({
8
+ popup: { minWidth: "14rem", padding: space.xxs, margin: 0 },
9
+ subPopup: { animationDuration: { default: "120ms", [REDUCED]: "0s" } },
10
+ item: {
11
+ position: "relative",
12
+ display: "flex",
13
+ alignItems: "center",
14
+ gap: "0.55rem",
15
+ borderRadius: radius.sm,
16
+ paddingBlock: "0.42rem",
17
+ paddingInline: space.xs,
18
+ fontFamily: font.body,
19
+ fontSize: text.sm,
20
+ color: color.text,
21
+ cursor: "pointer",
22
+ outline: "none",
23
+ userSelect: "none",
24
+ },
25
+ highlighted: {
26
+ backgroundColor: color.accentSubtle,
27
+ outline: `2px solid ${color.focusRing}`,
28
+ outlineOffset: -2,
29
+ "@media (forced-colors: active)": { outline: "2px solid Highlight" },
30
+ },
31
+ disabled: { opacity: 0.5, cursor: "not-allowed" },
32
+ danger: { color: color.danger },
33
+ icon: { color: color.textMuted, flex: "none", display: "flex" },
34
+ shortcut: {
35
+ marginInlineStart: "auto",
36
+ fontFamily: font.mono,
37
+ fontSize: "0.68rem",
38
+ lineHeight: 1,
39
+ color: color.textFaint,
40
+ },
41
+ arrow: { marginInlineStart: "auto", color: color.textFaint, display: "flex" },
42
+ tick: { width: "0.9rem", flex: "none", color: color.accent, display: "flex", justifyContent: "center" },
43
+ groupLabel: {
44
+ fontFamily: font.mono,
45
+ fontSize: "0.65rem",
46
+ fontWeight: 600,
47
+ lineHeight: 1,
48
+ letterSpacing: "0.08em",
49
+ textTransform: "uppercase",
50
+ color: color.textFaint,
51
+ paddingBlock: space.xxs,
52
+ paddingInline: space.xs,
53
+ },
54
+ separator: {
55
+ borderTopWidth: 1,
56
+ borderTopStyle: "solid",
57
+ borderTopColor: color.border,
58
+ marginBlock: space.xxs,
59
+ marginInline: space.xxs,
60
+ },
61
+ });
62
+ function ChevronRight() {
63
+ return (_jsx("svg", { width: "11", height: "11", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", "aria-hidden": "true", children: _jsx("path", { d: "m9 6 6 6-6 6" }) }));
64
+ }
65
+ function Tick() {
66
+ return (_jsx("svg", { width: "12", height: "12", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "2", "aria-hidden": "true", children: _jsx("path", { d: "m2.5 8.5 4 4 7-9" }) }));
67
+ }
68
+ const itemClassName = (variant) => (state) => stylex.props(styles.item, variant === "danger" && styles.danger, state.highlighted && styles.highlighted, state.disabled && styles.disabled).className ?? "";
69
+ export function DropdownItem({ icon, shortcut, variant, onSelect, children, ...rest }) {
70
+ return (_jsxs(Menu.Item, { ...rest, "aria-keyshortcuts": shortcut, onClick: onSelect, className: itemClassName(variant), children: [icon != null && (_jsx("span", { "aria-hidden": "true", ...stylex.props(styles.icon), children: icon })), children, shortcut != null && (_jsx("span", { "aria-hidden": "true", ...stylex.props(styles.shortcut), children: shortcut }))] }));
71
+ }
72
+ export function DropdownCheckboxItem({ children, ...rest }) {
73
+ return (_jsxs(Menu.CheckboxItem, { ...rest, className: itemClassName(), children: [_jsx("span", { ...stylex.props(styles.tick), children: _jsx(Menu.CheckboxItemIndicator, { children: _jsx(Tick, {}) }) }), children] }));
74
+ }
75
+ export function DropdownGroup({ label, children }) {
76
+ return (_jsxs(Menu.Group, { children: [label != null && _jsx(Menu.GroupLabel, { ...stylex.props(styles.groupLabel), children: label }), children] }));
77
+ }
78
+ export function DropdownSeparator() {
79
+ return _jsx(Menu.Separator, { ...stylex.props(styles.separator) });
80
+ }
81
+ export function DropdownSub({ label, icon, disabled, children }) {
82
+ return (_jsxs(Menu.SubmenuRoot, { children: [_jsxs(Menu.SubmenuTrigger, { disabled: disabled, className: itemClassName(), children: [icon != null && (_jsx("span", { "aria-hidden": "true", ...stylex.props(styles.icon), children: icon })), label, _jsx("span", { ...stylex.props(styles.arrow), children: _jsx(ChevronRight, {}) })] }), _jsx(Menu.Portal, { children: _jsx(Menu.Positioner, { align: "start", side: "inline-end", sideOffset: 2, alignOffset: -6, ...stylex.props(popupStyles.positioner), children: _jsx(Menu.Popup, { ...stylex.props(popupStyles.surface, styles.popup, styles.subPopup), children: children }) }) })] }));
83
+ }
84
+ export function DropdownMenu({ trigger, open, defaultOpen, onOpenChange, side = "bottom", align = "start", children, }) {
85
+ return (_jsxs(Menu.Root, { open: open, defaultOpen: defaultOpen, onOpenChange: onOpenChange, children: [_jsx(Menu.Trigger, { render: trigger }), _jsx(Menu.Portal, { children: _jsx(Menu.Positioner, { side: side, align: align, sideOffset: 6, ...stylex.props(popupStyles.positioner), children: _jsx(Menu.Popup, { ...stylex.props(popupStyles.surface, styles.popup), children: children }) }) })] }));
86
+ }