@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,209 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Combobox } from "@base-ui/react/combobox";
3
+ import * as stylex from "@stylexjs/stylex";
4
+ import { Children, isValidElement, useId, useMemo, useState } from "react";
5
+ import { reset } from "../../lib/styled.js";
6
+ import { popupStyles } from "../../lib/popup.js";
7
+ import { color, font, motion, radius, space, text } from "../../tokens.stylex.js";
8
+ const REDUCED = "@media (prefers-reduced-motion: reduce)";
9
+ export function SelectItem(_props) {
10
+ return null;
11
+ }
12
+ export function SelectGroup(_props) {
13
+ return null;
14
+ }
15
+ function toOption(element) {
16
+ const { value, hint, disabled, textValue, children } = element.props;
17
+ return {
18
+ value,
19
+ label: children,
20
+ text: textValue ?? (typeof children === "string" ? children : value),
21
+ hint,
22
+ disabled,
23
+ };
24
+ }
25
+ function collect(children) {
26
+ const options = [];
27
+ const groups = [];
28
+ for (const child of Children.toArray(children)) {
29
+ if (!isValidElement(child))
30
+ continue;
31
+ if (child.type === SelectGroup) {
32
+ const group = child;
33
+ const items = Children.toArray(group.props.children)
34
+ .filter((c) => isValidElement(c) && c.type === SelectItem)
35
+ .map(toOption);
36
+ groups.push({ value: group.props.label, items });
37
+ options.push(...items);
38
+ }
39
+ else if (child.type === SelectItem) {
40
+ options.push(toOption(child));
41
+ }
42
+ }
43
+ return { options, groups };
44
+ }
45
+ const styles = stylex.create({
46
+ trigger: {
47
+ width: "100%",
48
+ minHeight: "2.25rem",
49
+ // multiple 的 trigger 是 div(不是原生 button,拿不到 UA 的 border-box)
50
+ boxSizing: "border-box",
51
+ display: "flex",
52
+ alignItems: "center",
53
+ gap: space.xxs,
54
+ flexWrap: "wrap",
55
+ backgroundColor: color.surface,
56
+ borderWidth: 1,
57
+ borderStyle: "solid",
58
+ borderColor: { default: color.border, ":hover": color.borderStrong },
59
+ borderRadius: radius.md,
60
+ paddingBlock: space.xxs,
61
+ paddingInline: space.xs,
62
+ fontFamily: font.body,
63
+ fontSize: text.sm,
64
+ color: color.text,
65
+ cursor: "pointer",
66
+ textAlign: "start",
67
+ transitionProperty: "border-color",
68
+ transitionDuration: { default: motion.fast, [REDUCED]: "0s" },
69
+ outline: { default: "none", ":focus-visible": `2px solid ${color.focusRing}` },
70
+ outlineOffset: -1,
71
+ },
72
+ placeholder: { color: color.textFaint },
73
+ caret: { marginInlineStart: "auto", color: color.textFaint, flex: "none" },
74
+ chip: {
75
+ display: "inline-flex",
76
+ alignItems: "center",
77
+ gap: space.xxs,
78
+ backgroundColor: color.accentSubtle,
79
+ color: color.text,
80
+ borderRadius: radius.sm,
81
+ paddingBlock: "0.1rem",
82
+ paddingInline: space.xxs,
83
+ fontSize: text.xs,
84
+ },
85
+ chipRemove: {
86
+ cursor: "pointer",
87
+ color: color.textMuted,
88
+ lineHeight: 1,
89
+ outline: { default: "none", ":focus-visible": `2px solid ${color.focusRing}` },
90
+ outlineOffset: 1,
91
+ borderRadius: radius.sm,
92
+ },
93
+ search: {
94
+ display: "flex",
95
+ alignItems: "center",
96
+ gap: space.xs,
97
+ borderBottomWidth: 1,
98
+ borderBottomStyle: "solid",
99
+ borderBottomColor: color.border,
100
+ paddingBlock: space.xs,
101
+ paddingInline: space.sm,
102
+ },
103
+ searchIcon: { color: color.textFaint, flex: "none" },
104
+ searchInput: {
105
+ flex: 1,
106
+ fontFamily: font.body,
107
+ fontSize: text.sm,
108
+ color: color.text,
109
+ "::placeholder": { color: color.textFaint },
110
+ },
111
+ srOnly: {
112
+ position: "absolute",
113
+ width: 1,
114
+ height: 1,
115
+ padding: 0,
116
+ margin: -1,
117
+ overflow: "hidden",
118
+ clipPath: "inset(50%)",
119
+ whiteSpace: "nowrap",
120
+ },
121
+ list: { maxHeight: "14rem", overflowY: "auto", padding: space.xxs, margin: 0 },
122
+ groupLabel: {
123
+ fontFamily: font.mono,
124
+ fontSize: "0.65rem",
125
+ fontWeight: 600,
126
+ lineHeight: 1,
127
+ letterSpacing: "0.08em",
128
+ textTransform: "uppercase",
129
+ color: color.textFaint,
130
+ paddingBlock: space.xxs,
131
+ paddingInline: space.xs,
132
+ },
133
+ option: {
134
+ display: "flex",
135
+ alignItems: "center",
136
+ gap: space.xs,
137
+ borderRadius: radius.sm,
138
+ paddingBlock: "0.42rem",
139
+ paddingInline: space.xs,
140
+ fontSize: text.sm,
141
+ cursor: "pointer",
142
+ outline: "none",
143
+ userSelect: "none",
144
+ },
145
+ optionHighlighted: {
146
+ backgroundColor: color.accentSubtle,
147
+ outline: `2px solid ${color.focusRing}`,
148
+ outlineOffset: -2,
149
+ "@media (forced-colors: active)": { outline: "2px solid Highlight" },
150
+ },
151
+ optionDisabled: { opacity: 0.5, cursor: "not-allowed" },
152
+ hint: { color: color.textFaint, fontSize: "0.72rem" },
153
+ tick: { marginInlineStart: "auto", color: color.accent, display: "flex" },
154
+ empty: {
155
+ paddingBlock: space.md,
156
+ paddingInline: space.sm,
157
+ fontSize: text.xs,
158
+ color: color.textMuted,
159
+ textAlign: "center",
160
+ },
161
+ });
162
+ function CaretIcon() {
163
+ return (_jsx("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", "aria-hidden": "true", children: _jsx("path", { d: "m6 9 6 6 6-6" }) }));
164
+ }
165
+ function SearchIcon() {
166
+ 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" })] }));
167
+ }
168
+ function TickIcon() {
169
+ return (_jsx("svg", { width: "13", height: "13", 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" }) }));
170
+ }
171
+ function OptionRow({ option }) {
172
+ return (_jsxs(Combobox.Item, { value: option, disabled: option.disabled, className: (state) => stylex.props(styles.option, state.highlighted && styles.optionHighlighted, state.disabled && styles.optionDisabled).className ?? "", children: [_jsx("span", { children: option.label }), option.hint != null && _jsx("small", { ...stylex.props(styles.hint), children: option.hint }), _jsx(Combobox.ItemIndicator, { ...stylex.props(styles.tick), children: _jsx(TickIcon, {}) })] }));
173
+ }
174
+ export function Select({ value, defaultValue, onValueChange, placeholder = "選擇…", searchPlaceholder = "搜尋…", searchLabel = "搜尋選項", emptyLabel, multiple = false, searchable = true, disabled, name, children, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, }) {
175
+ const base = useId();
176
+ const labelId = `${base}label`;
177
+ const triggerId = `${base}trigger`;
178
+ const nameId = ariaLabelledBy ?? (ariaLabel != null ? labelId : undefined);
179
+ const triggerLabelledBy = nameId ? `${nameId} ${triggerId}` : undefined;
180
+ const { options, groups } = useMemo(() => collect(children), [children]);
181
+ const byValue = useMemo(() => new Map(options.map((o) => [o.value, o])), [options]);
182
+ const isControlled = value !== undefined;
183
+ const [internal, setInternal] = useState(defaultValue ?? (multiple ? [] : ""));
184
+ const current = isControlled ? value : internal;
185
+ const selectedList = useMemo(() => (Array.isArray(current) ? current : [])
186
+ .map((v) => byValue.get(v))
187
+ .filter((o) => o != null), [current, byValue]);
188
+ const selectedOne = typeof current === "string" && current !== "" ? (byValue.get(current) ?? null) : null;
189
+ const selected = multiple ? selectedList : selectedOne;
190
+ const [query, setQuery] = useState("");
191
+ const notify = onValueChange;
192
+ const emit = (next) => {
193
+ if (!isControlled)
194
+ setInternal(next);
195
+ notify?.(next);
196
+ };
197
+ const items = groups.length > 0 ? groups : options;
198
+ return (_jsxs(Combobox.Root, { items: items, multiple: multiple, value: selected, disabled: disabled, name: name, filter: searchable ? undefined : null, itemToStringLabel: (item) => item.text, itemToStringValue: (item) => item.value, onInputValueChange: setQuery, onValueChange: (next) => {
199
+ if (multiple)
200
+ emit((next ?? []).map((o) => o.value));
201
+ else
202
+ emit(next?.value ?? "");
203
+ }, children: [ariaLabel != null && ariaLabelledBy == null && (_jsx("span", { id: labelId, hidden: true, children: ariaLabel })), _jsxs(Combobox.Trigger, { id: triggerId, "aria-labelledby": triggerLabelledBy, nativeButton: !multiple, render: multiple ? _jsx("div", {}) : undefined, ...stylex.props(styles.trigger), children: [multiple ? (selectedList.length === 0 ? (_jsx("span", { ...stylex.props(styles.placeholder), children: placeholder })) : (selectedList.map((option) => (_jsxs("span", { ...stylex.props(styles.chip), children: [option.label, _jsx("button", { type: "button", "aria-label": `移除 ${option.text}`, onPointerDown: (event) => event.stopPropagation(), onMouseDown: (event) => event.stopPropagation(), onClick: (event) => {
204
+ event.stopPropagation();
205
+ emit(selectedList.filter((o) => o.value !== option.value).map((o) => o.value));
206
+ }, ...stylex.props(reset.control, styles.chipRemove), children: "\u00D7" })] }, option.value))))) : (_jsx(Combobox.Value, { children: (v) => v ? _jsx("span", { children: v.label }) : _jsx("span", { ...stylex.props(styles.placeholder), children: placeholder }) })), _jsx(Combobox.Icon, { ...stylex.props(styles.caret), children: _jsx(CaretIcon, {}) })] }), _jsx(Combobox.Portal, { children: _jsx(Combobox.Positioner, { align: "start", sideOffset: 6, ...stylex.props(popupStyles.positioner), children: _jsxs(Combobox.Popup, { "aria-labelledby": nameId, ...stylex.props(popupStyles.surface, popupStyles.anchorWidth), children: [searchable ? (_jsxs("div", { ...stylex.props(styles.search), children: [_jsx("span", { ...stylex.props(styles.searchIcon), children: _jsx(SearchIcon, {}) }), _jsx(Combobox.Input, { placeholder: searchPlaceholder, "aria-label": searchLabel, ...stylex.props(reset.control, styles.searchInput) })] })) : (_jsx(Combobox.Input, { "aria-labelledby": nameId, "aria-label": nameId ? undefined : searchLabel, ...stylex.props(styles.srOnly) })), _jsx(Combobox.List, { ...stylex.props(styles.list), children: groups.length > 0
207
+ ? (group) => (_jsxs(Combobox.Group, { items: group.items, children: [_jsx(Combobox.GroupLabel, { ...stylex.props(styles.groupLabel), children: group.value }), _jsx(Combobox.Collection, { children: (option) => _jsx(OptionRow, { option: option }, option.value) })] }, group.value))
208
+ : (option) => _jsx(OptionRow, { option: option }, option.value) }), _jsx(Combobox.Empty, { ...stylex.props(styles.empty), children: emptyLabel ? emptyLabel(query) : `找不到「${query}」。` })] }) }) })] }));
209
+ }
@@ -0,0 +1,20 @@
1
+ import type { ComponentProps } from "react";
2
+ import { type StyleArg } from "../../lib/styled.js";
3
+ export type SkeletonProps = Omit<ComponentProps<"div">, "children" | "width" | "height"> & {
4
+ shape?: "line" | "block" | "circle";
5
+ width?: string | number;
6
+ height?: string | number;
7
+ size?: string | number;
8
+ sx?: StyleArg;
9
+ };
10
+ export declare function Skeleton({ shape, width, height, size, sx, ...props }: SkeletonProps): import("react").JSX.Element;
11
+ export type SkeletonGroupProps = ComponentProps<"div"> & {
12
+ label: string;
13
+ sx?: StyleArg;
14
+ };
15
+ export declare function SkeletonGroup({ label, children, sx, ...props }: SkeletonGroupProps): import("react").JSX.Element;
16
+ export type ThreadSkeletonProps = {
17
+ messages?: number;
18
+ label?: string;
19
+ };
20
+ export declare function ThreadSkeleton({ messages, label }: ThreadSkeletonProps): import("react").JSX.Element;
@@ -0,0 +1,57 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import * as stylex from "@stylexjs/stylex";
3
+ import { styled } from "../../lib/styled.js";
4
+ import { color, radius, space } from "../../tokens.stylex.js";
5
+ const REDUCED = "@media (prefers-reduced-motion: reduce)";
6
+ const shimmer = stylex.keyframes({
7
+ from: { backgroundPosition: "180% 0" },
8
+ to: { backgroundPosition: "-80% 0" },
9
+ });
10
+ const styles = stylex.create({
11
+ bone: {
12
+ backgroundColor: color.bone,
13
+ borderRadius: radius.sm,
14
+ backgroundImage: {
15
+ default: `linear-gradient(100deg, transparent 30%, ${color.sheen} 50%, transparent 70%)`,
16
+ [REDUCED]: "none",
17
+ },
18
+ backgroundSize: "220% 100%",
19
+ animationName: { default: shimmer, [REDUCED]: "none" },
20
+ animationDuration: { default: "1.6s", [REDUCED]: "2.4s" },
21
+ animationTimingFunction: { default: "linear", [REDUCED]: "ease-in-out" },
22
+ animationIterationCount: "infinite",
23
+ },
24
+ line: { height: "0.8rem" },
25
+ circle: { borderRadius: radius.full },
26
+ size: (width, height) => ({ width, height }),
27
+ group: { display: "grid", gap: space.xxs },
28
+ srOnly: {
29
+ position: "absolute",
30
+ width: 1,
31
+ height: 1,
32
+ padding: 0,
33
+ margin: -1,
34
+ overflow: "hidden",
35
+ clipPath: "inset(50%)",
36
+ whiteSpace: "nowrap",
37
+ borderWidth: 0,
38
+ },
39
+ });
40
+ export function Skeleton({ shape = "line", width, height, size, sx, ...props }) {
41
+ const resolvedWidth = shape === "circle" ? (size ?? width ?? "2.25rem") : (width ?? "100%");
42
+ const resolvedHeight = shape === "circle" ? (size ?? height ?? "2.25rem") : (height ?? "");
43
+ return (_jsx("div", { "aria-hidden": "true", ...props, ...styled(props, styles.bone, shape === "line" && styles.line, shape === "circle" && styles.circle, styles.size(resolvedWidth, resolvedHeight || null), sx) }));
44
+ }
45
+ export function SkeletonGroup({ label, children, sx, ...props }) {
46
+ return (_jsxs("div", { role: "status", "aria-label": label, ...props, ...styled(props, styles.group, sx), children: [_jsx("span", { ...stylex.props(styles.srOnly), children: label }), children] }));
47
+ }
48
+ export function ThreadSkeleton({ messages = 2, label = "thread 載入中" }) {
49
+ return (_jsx(SkeletonGroup, { label: label, sx: threadStyles.thread, children: Array.from({ length: messages }, (_, index) => (_jsxs("div", { ...stylex.props(threadStyles.turn), children: [_jsx(Skeleton, { sx: threadStyles.userBubble, width: "55%", height: "2.4rem" }), _jsxs("div", { ...stylex.props(threadStyles.assistant), children: [_jsx(Skeleton, { shape: "circle", size: "1.6rem" }), _jsxs("div", { ...stylex.props(threadStyles.stack), children: [_jsx(Skeleton, { width: "96%" }), _jsx(Skeleton, { width: "88%" }), _jsx(Skeleton, { width: "47%" })] })] })] }, index))) }));
50
+ }
51
+ const threadStyles = stylex.create({
52
+ thread: { display: "grid", gap: space.sm },
53
+ turn: { display: "grid", gap: space.sm },
54
+ userBubble: { justifySelf: "end", borderRadius: `${radius.lg} ${radius.lg} ${radius.sm} ${radius.lg}` },
55
+ assistant: { display: "flex", gap: space.xs, alignItems: "flex-start" },
56
+ stack: { display: "grid", gap: space.xxs, flex: 1 },
57
+ });
@@ -0,0 +1,7 @@
1
+ import { type ComponentProps, type ReactNode } from "react";
2
+ export type SwitchProps = Omit<ComponentProps<"input">, "type" | "role" | "children"> & {
3
+ label?: ReactNode;
4
+ description?: ReactNode;
5
+ onCheckedChange?: (checked: boolean) => void;
6
+ };
7
+ export declare function Switch({ label, description, onCheckedChange, checked, defaultChecked, onChange, ...props }: SwitchProps): import("react").JSX.Element;
@@ -0,0 +1,108 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import * as stylex from "@stylexjs/stylex";
3
+ import { useId } from "react";
4
+ import { styled } from "../../lib/styled.js";
5
+ import { useSvgId } from "../../lib/svgId.js";
6
+ import { useControllableState } from "../../lib/useControllableState.js";
7
+ import { buildWeave, weaveRand } from "../../lib/weave.js";
8
+ import { color, font, motion, radius, space, text, yarn } from "../../tokens.stylex.js";
9
+ import { useFieldControl } from "../label/fieldContext.js";
10
+ const REDUCED = "@media (prefers-reduced-motion: reduce)";
11
+ const DARK = "@media (prefers-color-scheme: dark)";
12
+ // 藥丸 42×20(1:1 CSS px),module scope 織一次(固定種子 → 恆定;純幾何,SSR 安全)
13
+ const CLOTH = buildWeave({ w: 42, h: 20, ox: 1, oy: 1 }, weaveRand());
14
+ const styles = stylex.create({
15
+ root: {
16
+ display: "flex",
17
+ gap: space.sm,
18
+ alignItems: "flex-start",
19
+ justifyContent: "space-between",
20
+ cursor: { default: "pointer", ":has(:disabled)": "not-allowed" },
21
+ opacity: { default: 1, ":has(:disabled)": 0.5 },
22
+ fontFamily: font.body,
23
+ },
24
+ input: { position: "absolute", opacity: 0, width: "2.75rem", height: "1.4rem", margin: 0 },
25
+ track: {
26
+ flex: "none",
27
+ width: "2.75rem",
28
+ height: "1.4rem",
29
+ marginTop: "0.1rem",
30
+ borderRadius: radius.full,
31
+ outline: { default: "none", ":has(:focus-visible)": `2px solid ${color.focusRing}` },
32
+ outlineOffset: 3,
33
+ },
34
+ svg: { width: "100%", height: "100%", display: "block", overflow: "visible" },
35
+ guide: { stroke: color.borderStrong, strokeWidth: 1.5, strokeLinecap: "round", fill: "none" },
36
+ // 開:布從左織進來(wipe clip 的 width 推進,紗本身不動 — 同一塊布被逐漸織出);
37
+ // 關:整塊布 scale 到 0 收掉,width/opacity 等 scale 跑完才歸零。
38
+ // 藥丸形狀是另一支固定 clip:左圓角從第一幀就完整,前緣是織布機的直線 fell。
39
+ cloth: {
40
+ opacity: 0,
41
+ scale: "0",
42
+ transformBox: "fill-box",
43
+ transformOrigin: "center",
44
+ transitionProperty: "scale, opacity",
45
+ transitionDuration: { default: "200ms, 0s", [REDUCED]: "0s" },
46
+ transitionTimingFunction: "ease-in",
47
+ transitionDelay: { default: "0s, 200ms", [REDUCED]: "0s" },
48
+ filter: {
49
+ default: "drop-shadow(0 1px 1.5px rgba(18, 60, 49, 0.30))",
50
+ [DARK]: "drop-shadow(0 1px 2px rgba(0, 0, 0, 0.45))",
51
+ },
52
+ },
53
+ clothOn: {
54
+ opacity: 1,
55
+ scale: "1",
56
+ transitionDuration: { default: "0s, 0s", [REDUCED]: "0s" },
57
+ transitionDelay: "0s",
58
+ },
59
+ pillclip: {
60
+ width: 0,
61
+ transitionProperty: "width",
62
+ transitionDuration: { default: "0s", [REDUCED]: "0s" },
63
+ transitionDelay: { default: "200ms", [REDUCED]: "0s" },
64
+ },
65
+ pillclipOn: {
66
+ width: 42,
67
+ transitionDuration: { default: "240ms", [REDUCED]: "0s" },
68
+ transitionTimingFunction: "cubic-bezier(0.32, 0.85, 0.45, 1)",
69
+ transitionDelay: "0s",
70
+ },
71
+ yarns: { fill: "none", strokeLinecap: "round" },
72
+ un: { stroke: yarn.un },
73
+ sh: { stroke: yarn.sh, opacity: 0.5 },
74
+ y0: { stroke: yarn.y0 },
75
+ y1: { stroke: yarn.y1 },
76
+ y2: { stroke: yarn.y2 },
77
+ y3: { stroke: yarn.y3 },
78
+ y4: { stroke: yarn.y4 },
79
+ hi: { stroke: yarn.hi, opacity: 0.45 },
80
+ thumb: {
81
+ fill: color.bg,
82
+ stroke: color.borderStrong,
83
+ strokeWidth: 1.6,
84
+ translate: "0 0",
85
+ transitionProperty: "translate, stroke, fill",
86
+ transitionDuration: { default: `240ms, ${motion.normal}, ${motion.normal}`, [REDUCED]: "0s" },
87
+ transitionTimingFunction: "cubic-bezier(0.16, 1, 0.3, 1)",
88
+ },
89
+ thumbOn: { translate: "22px 0", fill: color.surface, stroke: color.accent },
90
+ labelText: { display: "block", fontWeight: 500, fontSize: text.sm, color: color.text },
91
+ description: { display: "block", fontSize: text.xs, color: color.textMuted },
92
+ });
93
+ const BUCKETS = [styles.y0, styles.y1, styles.y2, styles.y3, styles.y4];
94
+ export function Switch({ label, description, onCheckedChange, checked, defaultChecked, onChange, ...props }) {
95
+ const clipId = useSvgId("ak-weave");
96
+ const base = useId();
97
+ const labelId = `${base}label`;
98
+ const descriptionId = `${base}description`;
99
+ const { invalid, ...field } = useFieldControl(props);
100
+ const describedBy = [description != null ? descriptionId : null, field["aria-describedby"]]
101
+ .filter(Boolean)
102
+ .join(" ");
103
+ const [isOn, setOn] = useControllableState(checked, defaultChecked ?? false, onCheckedChange);
104
+ return (_jsxs("label", { ...stylex.props(styles.root), children: [(label != null || description != null) && (_jsxs("span", { children: [label != null && (_jsx("span", { id: labelId, ...stylex.props(styles.labelText), children: label })), description != null && (_jsx("span", { id: descriptionId, ...stylex.props(styles.description), children: description }))] })), _jsxs("span", { ...stylex.props(styles.track), children: [_jsx("input", { type: "checkbox", role: "switch", ...props, ...field, "aria-invalid": invalid || undefined, "aria-labelledby": label != null ? labelId : undefined, "aria-describedby": describedBy || undefined, checked: isOn, "aria-checked": isOn, onChange: (event) => {
105
+ setOn(event.currentTarget.checked);
106
+ onChange?.(event);
107
+ }, ...styled(props, styles.input) }), _jsxs("svg", { viewBox: "0 0 44 22", "aria-hidden": "true", ...stylex.props(styles.svg), children: [_jsx("path", { d: "M11,11 H33", ...stylex.props(styles.guide) }), _jsxs("g", { ...stylex.props(styles.cloth, isOn && styles.clothOn), children: [_jsxs("defs", { children: [_jsx("clipPath", { id: `${clipId}p`, children: _jsx("rect", { x: "1", y: "1", width: "42", height: "20", rx: "10" }) }), _jsx("clipPath", { id: clipId, children: _jsx("rect", { x: "1", y: "-3", height: "28", ...stylex.props(styles.pillclip, isOn && styles.pillclipOn) }) })] }), _jsx("g", { clipPath: `url(#${clipId}p)`, children: _jsxs("g", { clipPath: `url(#${clipId})`, ...stylex.props(styles.yarns), children: [_jsx("g", { ...stylex.props(styles.un), children: CLOTH.under.map((t, i) => (_jsx("path", { d: t.d, strokeWidth: t.sw }, i))) }), _jsx("g", { ...stylex.props(styles.sh), children: CLOTH.seams.map((t, i) => (_jsx("path", { d: t.d, strokeWidth: t.sw }, i))) }), CLOTH.face.map((t, i) => (_jsx("path", { d: t.d, strokeWidth: t.sw, ...stylex.props(BUCKETS[t.bucket]) }, i))), _jsx("g", { ...stylex.props(styles.hi), children: CLOTH.hi.map((t, i) => (_jsx("path", { d: t.d, strokeWidth: t.sw }, i))) })] }) })] }), _jsx("circle", { cx: "11", cy: "11", r: "8.4", ...stylex.props(styles.thumb, isOn && styles.thumbOn) })] })] })] }));
108
+ }
@@ -0,0 +1,28 @@
1
+ import { type ReactNode } from "react";
2
+ export type TabsProps = {
3
+ value?: string;
4
+ defaultValue?: string;
5
+ onValueChange?: (value: string) => void;
6
+ variant?: "underline" | "pills";
7
+ className?: string;
8
+ children: ReactNode;
9
+ };
10
+ export declare function Tabs({ variant, children, ...props }: TabsProps): import("react").JSX.Element;
11
+ export type TabsListProps = {
12
+ "aria-label": string;
13
+ className?: string;
14
+ children: ReactNode;
15
+ };
16
+ export declare function TabsList({ children, ...rest }: TabsListProps): import("react").JSX.Element;
17
+ export type TabsTabProps = {
18
+ value: string;
19
+ disabled?: boolean;
20
+ children: ReactNode;
21
+ };
22
+ export declare function TabsTab({ children, ...rest }: TabsTabProps): import("react").JSX.Element;
23
+ export type TabsPanelProps = {
24
+ value: string;
25
+ children: ReactNode;
26
+ className?: string;
27
+ };
28
+ export declare function TabsPanel({ children, ...rest }: TabsPanelProps): import("react").JSX.Element;
@@ -0,0 +1,133 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Tabs as BaseTabs } from "@base-ui/react/tabs";
3
+ import * as stylex from "@stylexjs/stylex";
4
+ import { createContext, useCallback, useContext, useMemo, useState } from "react";
5
+ import { styled } from "../../lib/styled.js";
6
+ import { useSvgId } from "../../lib/svgId.js";
7
+ import { buildWeave, weaveRand } from "../../lib/weave.js";
8
+ import { color, font, motion, radius, space, text, yarnSubtle } from "../../tokens.stylex.js";
9
+ const REDUCED = "@media (prefers-reduced-motion: reduce)";
10
+ const VariantContext = createContext("underline");
11
+ const styles = stylex.create({
12
+ root: { display: "grid", gap: space.xs },
13
+ list: { display: "flex", gap: space.xxs, position: "relative" },
14
+ underlineList: {
15
+ borderBottomWidth: 1,
16
+ borderBottomStyle: "solid",
17
+ borderBottomColor: color.border,
18
+ },
19
+ pillsList: {
20
+ // 只包住自己的 pills:整條拉滿時,4px 的內距在一大片空白旁邊會看起來像沒有間距
21
+ width: "fit-content",
22
+ maxWidth: "100%",
23
+ backgroundColor: color.surface,
24
+ borderWidth: 1,
25
+ borderStyle: "solid",
26
+ borderColor: color.border,
27
+ borderRadius: radius.md,
28
+ padding: space.xxs,
29
+ gap: "0.15rem",
30
+ },
31
+ tab: {
32
+ backgroundColor: "transparent",
33
+ borderWidth: 0,
34
+ fontFamily: font.body,
35
+ fontSize: text.sm,
36
+ fontWeight: 500,
37
+ lineHeight: text.leadingSnug,
38
+ color: { default: color.textMuted, ":hover": color.text },
39
+ paddingBlock: space.xxs,
40
+ paddingInline: space.xs,
41
+ borderRadius: radius.sm,
42
+ cursor: "pointer",
43
+ transitionProperty: "color, background-color",
44
+ transitionDuration: { default: motion.fast, [REDUCED]: "0s" },
45
+ outline: { default: "none", ":focus-visible": `2px solid ${color.focusRing}` },
46
+ outlineOffset: 2,
47
+ },
48
+ tabDisabled: { opacity: 0.4, cursor: "not-allowed", color: color.textFaint },
49
+ tabSelectedUnderline: { color: color.accent },
50
+ tabSelectedPills: { color: color.accent },
51
+ pillTab: { zIndex: 1 },
52
+ indicator: {
53
+ position: "absolute",
54
+ bottom: -1,
55
+ insetInlineStart: 0,
56
+ height: 2,
57
+ width: "var(--active-tab-width)",
58
+ translate: "var(--active-tab-left)",
59
+ backgroundColor: color.accent,
60
+ transitionProperty: "translate, width",
61
+ transitionDuration: { default: "240ms", [REDUCED]: "0s" },
62
+ transitionTimingFunction: "cubic-bezier(0.16, 1, 0.3, 1)",
63
+ },
64
+ // pills:布織滿整條 tablist,選取的高亮是一個取景窗(clip rect)滑到選中的 tab —
65
+ // 窗動、布不動,透出的織紋是連續的同一塊布(TEXTURE-GUIDE §3、tabs/NOTES.md)。
66
+ // Indicator 只當 --active-tab-* 的載體,鋪滿整條 list、自己不動。
67
+ pillLayer: { position: "absolute", inset: 0, pointerEvents: "none" },
68
+ pillSvg: { display: "block", width: "100%", height: "100%" },
69
+ pillWindow: {
70
+ x: "var(--active-tab-left)",
71
+ y: "var(--active-tab-top)",
72
+ width: "var(--active-tab-width)",
73
+ height: "var(--active-tab-height)",
74
+ rx: radius.sm,
75
+ transitionProperty: "x, width",
76
+ transitionDuration: { default: "240ms", [REDUCED]: "0s" },
77
+ transitionTimingFunction: "cubic-bezier(0.16, 1, 0.3, 1)",
78
+ },
79
+ cloth: { fill: "none", strokeLinecap: "round" },
80
+ un: { stroke: yarnSubtle.un },
81
+ sh: { stroke: yarnSubtle.sh, opacity: 0.5 },
82
+ y0: { stroke: yarnSubtle.y0 },
83
+ y1: { stroke: yarnSubtle.y1 },
84
+ y2: { stroke: yarnSubtle.y2 },
85
+ y3: { stroke: yarnSubtle.y3 },
86
+ y4: { stroke: yarnSubtle.y4 },
87
+ hi: { stroke: yarnSubtle.hi, opacity: 0.45 },
88
+ panel: {
89
+ fontFamily: font.body,
90
+ fontSize: text.sm,
91
+ color: color.textMuted,
92
+ paddingBlock: space.xxs,
93
+ borderRadius: radius.sm,
94
+ outline: { default: "none", ":focus-visible": `2px solid ${color.focusRing}` },
95
+ outlineOffset: 2,
96
+ },
97
+ });
98
+ export function Tabs({ variant = "underline", children, ...props }) {
99
+ return (_jsx(VariantContext, { value: variant, children: _jsx(BaseTabs.Root, { ...props, ...styled(props, styles.root), children: children }) }));
100
+ }
101
+ const BUCKETS = [styles.y0, styles.y1, styles.y2, styles.y3, styles.y4];
102
+ function PillCloth({ w, h }) {
103
+ const clipId = useSvgId("ak-pill");
104
+ const cloth = useMemo(() => buildWeave({ w, h }, weaveRand()), [w, h]);
105
+ return (_jsxs("svg", { viewBox: `0 0 ${w} ${h}`, "aria-hidden": "true", ...stylex.props(styles.pillSvg), children: [_jsx("defs", { children: _jsx("clipPath", { id: clipId, children: _jsx("rect", { ...stylex.props(styles.pillWindow) }) }) }), _jsxs("g", { clipPath: `url(#${clipId})`, ...stylex.props(styles.cloth), children: [_jsx("g", { ...stylex.props(styles.un), children: cloth.under.map((t, i) => (_jsx("path", { d: t.d, strokeWidth: t.sw }, i))) }), _jsx("g", { ...stylex.props(styles.sh), children: cloth.seams.map((t, i) => (_jsx("path", { d: t.d, strokeWidth: t.sw }, i))) }), cloth.face.map((t, i) => (_jsx("path", { d: t.d, strokeWidth: t.sw, ...stylex.props(BUCKETS[t.bucket]) }, i))), _jsx("g", { ...stylex.props(styles.hi), children: cloth.hi.map((t, i) => (_jsx("path", { d: t.d, strokeWidth: t.sw }, i))) })] })] }));
106
+ }
107
+ export function TabsList({ children, ...rest }) {
108
+ const variant = useContext(VariantContext);
109
+ const [size, setSize] = useState(null);
110
+ // 要織滿 tablist 就得先量它:ref callback + ResizeObserver(不用 effect,SSR 上零副作用)。
111
+ // 同尺寸 early return,resize 才重織。
112
+ const measure = useCallback((node) => {
113
+ if (!node)
114
+ return;
115
+ const read = () => setSize((current) => {
116
+ const w = node.clientWidth;
117
+ const h = node.clientHeight;
118
+ return current != null && current.w === w && current.h === h ? current : { w, h };
119
+ });
120
+ read();
121
+ const ro = new ResizeObserver(read);
122
+ ro.observe(node);
123
+ return () => ro.disconnect();
124
+ }, []);
125
+ return (_jsxs(BaseTabs.List, { ...rest, ref: variant === "pills" ? measure : undefined, ...styled(rest, styles.list, variant === "underline" ? styles.underlineList : styles.pillsList), children: [children, variant === "underline" && _jsx(BaseTabs.Indicator, { ...stylex.props(styles.indicator) }), variant === "pills" && size != null && size.w > 0 && (_jsx(BaseTabs.Indicator, { ...stylex.props(styles.pillLayer), children: _jsx(PillCloth, { w: size.w, h: size.h }) }))] }));
126
+ }
127
+ export function TabsTab({ children, ...rest }) {
128
+ const variant = useContext(VariantContext);
129
+ return (_jsx(BaseTabs.Tab, { ...rest, className: (state) => stylex.props(styles.tab, variant === "pills" && styles.pillTab, state.disabled && styles.tabDisabled, state.active && (variant === "underline" ? styles.tabSelectedUnderline : styles.tabSelectedPills)).className ?? "", children: children }));
130
+ }
131
+ export function TabsPanel({ children, ...rest }) {
132
+ return (_jsx(BaseTabs.Panel, { ...rest, ...styled(rest, styles.panel), children: children }));
133
+ }
@@ -0,0 +1,7 @@
1
+ import type { ComponentProps, ElementType } from "react";
2
+ type TextProps = ComponentProps<"p"> & {
3
+ as?: ElementType;
4
+ variant?: "display" | "title" | "body" | "caption" | "mono";
5
+ };
6
+ export declare function Text({ as: Tag, variant, ...props }: TextProps): import("react").JSX.Element;
7
+ export {};
@@ -0,0 +1,31 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import * as stylex from "@stylexjs/stylex";
3
+ import { styled } from "../../lib/styled.js";
4
+ import { color, font, text } from "../../tokens.stylex.js";
5
+ const styles = stylex.create({
6
+ base: {
7
+ margin: 0,
8
+ fontFamily: font.body,
9
+ color: color.text,
10
+ lineHeight: text.leadingNormal,
11
+ },
12
+ display: {
13
+ fontFamily: font.display,
14
+ fontSize: text.display,
15
+ fontWeight: 500,
16
+ lineHeight: text.leadingTight,
17
+ letterSpacing: "-0.01em",
18
+ },
19
+ title: {
20
+ fontFamily: font.display,
21
+ fontSize: text.xl,
22
+ fontWeight: 500,
23
+ lineHeight: text.leadingTight,
24
+ },
25
+ body: { fontSize: text.base },
26
+ caption: { fontSize: text.sm, color: color.textMuted },
27
+ mono: { fontFamily: font.mono, fontSize: text.sm },
28
+ });
29
+ export function Text({ as: Tag = "p", variant = "body", ...props }) {
30
+ return _jsx(Tag, { ...props, ...styled(props, styles.base, styles[variant]) });
31
+ }
@@ -0,0 +1,7 @@
1
+ import type { ComponentProps } from "react";
2
+ export type TextareaProps = ComponentProps<"textarea"> & {
3
+ autoGrow?: boolean;
4
+ maxRows?: number;
5
+ invalid?: boolean;
6
+ };
7
+ export declare function Textarea({ autoGrow, maxRows, invalid, ...props }: TextareaProps): import("react").JSX.Element;
@@ -0,0 +1,25 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import * as stylex from "@stylexjs/stylex";
3
+ import { styled } from "../../lib/styled.js";
4
+ import { space, text } from "../../tokens.stylex.js";
5
+ import { controlStyles } from "../input/Input.js";
6
+ import { useFieldControl } from "../label/fieldContext.js";
7
+ const styles = stylex.create({
8
+ base: {
9
+ minHeight: "4.5rem",
10
+ paddingBlock: space.xs,
11
+ paddingInline: space.sm,
12
+ lineHeight: text.leadingRelaxed,
13
+ resize: "vertical",
14
+ },
15
+ autoGrow: {
16
+ fieldSizing: "content",
17
+ resize: { default: "vertical", "@supports (field-sizing: content)": "none" },
18
+ },
19
+ maxRows: (rows) => ({ maxHeight: `calc(${rows} * 1.6em + ${space.md})` }),
20
+ });
21
+ export function Textarea({ autoGrow, maxRows, invalid, ...props }) {
22
+ const { invalid: fieldInvalid, ...field } = useFieldControl(props);
23
+ const isInvalid = invalid ?? fieldInvalid;
24
+ return (_jsx("textarea", { ...props, ...field, "aria-invalid": isInvalid || undefined, ...styled(props, controlStyles.base, styles.base, autoGrow && styles.autoGrow, maxRows != null && styles.maxRows(maxRows), isInvalid && controlStyles.invalid) }));
25
+ }