@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,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
|
+
}
|