@anyknown/ui 0.6.1 → 0.8.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/README.md +6 -7
- package/dist/brand.css +111 -124
- package/dist/components/badge/Badge.d.ts +11 -3
- package/dist/components/badge/Badge.js +21 -3
- package/dist/components/button/Button.d.ts +3 -2
- package/dist/components/button/Button.js +32 -48
- package/dist/components/checkbox/Checkbox.js +24 -48
- package/dist/components/data-table/DataTable.d.ts +7 -1
- package/dist/components/data-table/DataTable.js +34 -34
- package/dist/components/dialog/Dialog.d.ts +6 -1
- package/dist/components/dialog/Dialog.js +11 -3
- package/dist/components/empty-state/EmptyState.js +1 -1
- package/dist/components/ghost/Ghost.d.ts +16 -0
- package/dist/components/ghost/Ghost.js +50 -0
- package/dist/components/group/Group.d.ts +66 -0
- package/dist/components/group/Group.js +166 -0
- package/dist/components/handoff-receipt/HandoffReceipt.js +12 -42
- package/dist/components/icon/Chevron.d.ts +9 -0
- package/dist/components/icon/Chevron.js +6 -0
- package/dist/components/icon/icon.d.ts +35 -0
- package/dist/components/icon/icon.js +10 -0
- package/dist/components/icon-button/IconButton.d.ts +17 -0
- package/dist/components/icon-button/IconButton.js +66 -0
- package/dist/components/live-dot/LiveDot.d.ts +7 -0
- package/dist/components/live-dot/LiveDot.js +35 -0
- package/dist/components/message/Message.d.ts +3 -1
- package/dist/components/message/Message.js +2 -2
- package/dist/components/page/Page.d.ts +64 -0
- package/dist/components/page/Page.js +163 -0
- package/dist/components/popover/Popover.js +1 -1
- package/dist/components/progress/Progress.js +49 -104
- package/dist/components/radio/Radio.js +17 -28
- package/dist/components/segmented/Segmented.d.ts +12 -0
- package/dist/components/segmented/Segmented.js +43 -0
- package/dist/components/settings-rows/SettingsRows.d.ts +25 -0
- package/dist/components/settings-rows/SettingsRows.js +63 -0
- package/dist/components/slider/Slider.d.ts +19 -0
- package/dist/components/slider/Slider.js +103 -0
- package/dist/components/spin/Spin.d.ts +5 -0
- package/dist/components/spin/Spin.js +25 -0
- package/dist/components/status-chip/StatusChip.d.ts +18 -0
- package/dist/components/status-chip/StatusChip.js +68 -0
- package/dist/components/switch/Switch.js +20 -63
- package/dist/components/table/Table.d.ts +39 -0
- package/dist/components/table/Table.js +125 -0
- package/dist/components/table/TableCells.d.ts +52 -0
- package/dist/components/table/TableCells.js +103 -0
- package/dist/components/tabs/Tabs.js +16 -48
- package/dist/components/text/Text.js +2 -2
- package/dist/components/toast/Toast.js +7 -8
- package/dist/index.d.ts +14 -0
- package/dist/index.js +14 -0
- package/dist/lib/layers.d.ts +7 -0
- package/dist/lib/layers.js +12 -0
- package/dist/themes.stylex.d.ts +54 -206
- package/dist/themes.stylex.js +55 -157
- package/dist/tokens.css +63 -46
- package/dist/tokens.stylex.d.ts +41 -54
- package/dist/tokens.stylex.js +64 -85
- package/package.json +2 -2
- package/dist/lib/paths.d.ts +0 -3
- package/dist/lib/paths.js +0 -11
- package/dist/lib/progress.d.ts +0 -2
- package/dist/lib/progress.js +0 -28
- package/dist/lib/silk.d.ts +0 -66
- package/dist/lib/silk.js +0 -337
- package/dist/lib/svgId.d.ts +0 -1
- package/dist/lib/svgId.js +0 -4
- package/dist/lib/weave.d.ts +0 -28
- package/dist/lib/weave.js +0 -83
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import * as stylex from "@stylexjs/stylex";
|
|
3
3
|
import { reset, styled } from "../../lib/styled.js";
|
|
4
4
|
import { color, font, radius, space, text } from "../../tokens.stylex.js";
|
|
@@ -42,6 +42,20 @@ const styles = stylex.create({
|
|
|
42
42
|
dotDanger: { color: color.danger },
|
|
43
43
|
dotWarning: { color: color.warning },
|
|
44
44
|
count: { fontFamily: font.mono, fontSize: "0.7rem", fontWeight: 600, lineHeight: 1 },
|
|
45
|
+
pressable: {
|
|
46
|
+
cursor: "pointer",
|
|
47
|
+
backgroundColor: {
|
|
48
|
+
default: "transparent",
|
|
49
|
+
":hover": "color-mix(in srgb, currentColor 10%, transparent)",
|
|
50
|
+
},
|
|
51
|
+
outline: { default: "none", ":focus-visible": `2px solid ${color.focusRing}` },
|
|
52
|
+
outlineOffset: 2,
|
|
53
|
+
},
|
|
54
|
+
pressed: {
|
|
55
|
+
backgroundColor: { default: color.text, ":hover": color.text },
|
|
56
|
+
borderColor: color.text,
|
|
57
|
+
color: color.bg,
|
|
58
|
+
},
|
|
45
59
|
remove: {
|
|
46
60
|
position: "relative",
|
|
47
61
|
display: "grid",
|
|
@@ -74,6 +88,10 @@ const DOT_TONES = {
|
|
|
74
88
|
export function Badge({ variant = "neutral", dot, count, children, sx, ...props }) {
|
|
75
89
|
return (_jsxs("span", { ...props, ...styled(props, styles.base, styles[variant], sx), children: [dot != null && _jsx("span", { "aria-hidden": "true", ...stylex.props(styles.dot, DOT_TONES[dot]) }), children, count != null && _jsx("span", { ...stylex.props(styles.count), children: count })] }));
|
|
76
90
|
}
|
|
77
|
-
export function Chip({ onRemove, removeLabel, variant = "outline", children, ...props }) {
|
|
78
|
-
|
|
91
|
+
export function Chip({ onRemove, removeLabel, onClick, pressed, variant = "outline", children, sx, ref, ...props }) {
|
|
92
|
+
const body = (_jsxs(_Fragment, { children: [children, onRemove != null && (_jsx("button", { type: "button", "aria-label": removeLabel, onClick: onRemove, ...stylex.props(reset.control, styles.remove), children: _jsx("svg", { width: "8", height: "8", viewBox: "0 0 8 8", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", "aria-hidden": "true", children: _jsx("path", { d: "m1 1 6 6M7 1 1 7" }) }) }))] }));
|
|
93
|
+
if (onClick == null) {
|
|
94
|
+
return (_jsx(Badge, { variant: variant, sx: sx, ref: ref, ...props, children: body }));
|
|
95
|
+
}
|
|
96
|
+
return (_jsx("button", { type: "button", "aria-pressed": pressed, onClick: onClick, ...props, ...styled(props, reset.control, styles.base, styles.pressable, styles[variant], pressed === true && styles.pressed, sx), children: body }));
|
|
79
97
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ComponentProps } from "react";
|
|
2
2
|
import { type StyleArg } from "../../lib/styled.js";
|
|
3
|
+
type Variant = "primary" | "secondary" | "ghost" | "danger" | "dangerGhost";
|
|
3
4
|
type ButtonProps = ComponentProps<"button"> & {
|
|
4
|
-
variant?:
|
|
5
|
+
variant?: Variant;
|
|
5
6
|
size?: "xs" | "sm" | "md";
|
|
6
7
|
/** Square, no side padding — for a button whose whole label is one icon. */
|
|
7
8
|
icon?: boolean;
|
|
@@ -1,13 +1,8 @@
|
|
|
1
|
-
import { jsx as _jsx
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import * as stylex from "@stylexjs/stylex";
|
|
3
|
-
import { useCallback } from "react";
|
|
4
3
|
import { assignRef } from "../../lib/mergeRefs.js";
|
|
5
|
-
import { SilkBody } from "../../lib/silk.js";
|
|
6
4
|
import { styled } from "../../lib/styled.js";
|
|
7
|
-
import { color, font, motion, space, text
|
|
8
|
-
// 織成的實心(TEXTURE-GUIDE):沒有 background,實心由紗織成;
|
|
9
|
-
// 動態全由觸點決定 — 帶動 + 窩 + 掃光(press 全套),幾何在 client 端由
|
|
10
|
-
// SilkBody 以固定種子現織(同尺寸恆定)。reduced-motion 時靜態織紋。
|
|
5
|
+
import { color, corner, font, motion, space, text } from "../../tokens.stylex.js";
|
|
11
6
|
const styles = stylex.create({
|
|
12
7
|
base: {
|
|
13
8
|
position: "relative",
|
|
@@ -19,12 +14,12 @@ const styles = stylex.create({
|
|
|
19
14
|
fontSize: text.sm,
|
|
20
15
|
fontWeight: 500,
|
|
21
16
|
lineHeight: text.leadingTight,
|
|
22
|
-
borderRadius:
|
|
17
|
+
borderRadius: corner.btn,
|
|
23
18
|
borderWidth: 0,
|
|
24
19
|
backgroundColor: "transparent",
|
|
25
20
|
cursor: { default: "pointer", ":disabled": "not-allowed" },
|
|
26
21
|
opacity: { default: 1, ":disabled": 0.5 },
|
|
27
|
-
transitionProperty: "color",
|
|
22
|
+
transitionProperty: "color, background-color",
|
|
28
23
|
transitionDuration: { default: motion.fast, "@media (prefers-reduced-motion: reduce)": "0s" },
|
|
29
24
|
outline: { default: "none", ":focus-visible": `2px solid ${color.focusRing}` },
|
|
30
25
|
outlineOffset: 3,
|
|
@@ -51,48 +46,37 @@ const styles = stylex.create({
|
|
|
51
46
|
iconMd: { paddingInline: 0, width: "2.25rem" },
|
|
52
47
|
iconSm: { paddingInline: 0, width: "1.75rem" },
|
|
53
48
|
iconXs: { paddingInline: 0, width: "1.5rem" },
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
49
|
+
primary: {
|
|
50
|
+
color: color.accentText,
|
|
51
|
+
backgroundColor: {
|
|
52
|
+
default: color.accent,
|
|
53
|
+
":hover": `color-mix(in srgb, ${color.accent} 86%, ${color.bg})`,
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
secondary: {
|
|
57
|
+
color: color.text,
|
|
58
|
+
backgroundColor: { default: color.bone, ":hover": color.layer5 },
|
|
59
|
+
},
|
|
60
|
+
ghost: {
|
|
61
|
+
color: color.textMuted,
|
|
62
|
+
backgroundColor: { default: "transparent", ":hover": color.bone },
|
|
63
|
+
},
|
|
64
|
+
danger: {
|
|
65
|
+
color: color.accentText,
|
|
66
|
+
backgroundColor: {
|
|
67
|
+
default: color.danger,
|
|
68
|
+
":hover": `color-mix(in srgb, ${color.danger} 86%, ${color.bg})`,
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
// 「白底紅字」:份量走 ghost,語意走 danger token(和 interaction-card 收據列
|
|
72
|
+
// rejected 的 ✓ 同一個 token)
|
|
73
|
+
dangerGhost: {
|
|
74
|
+
color: color.danger,
|
|
75
|
+
backgroundColor: { default: "transparent", ":hover": color.dangerSubtle },
|
|
69
76
|
},
|
|
70
77
|
label: { position: "relative", display: "inline-flex", alignItems: "center", gap: space.xs },
|
|
71
78
|
});
|
|
72
|
-
const RADIUS = 8;
|
|
73
|
-
function palette(vars) {
|
|
74
|
-
return vars;
|
|
75
|
-
}
|
|
76
|
-
const SILK = {
|
|
77
|
-
primary: { palette: palette(yarn), bandMax: 0.75 },
|
|
78
|
-
secondary: { palette: palette(yarnSecondary), bandMax: 0.5 },
|
|
79
|
-
ghost: { palette: palette(yarnGhost), ghost: true, bandMax: 0.5 },
|
|
80
|
-
danger: { palette: palette(yarnDanger), bandMax: 0.75 },
|
|
81
|
-
dangerGhost: { palette: palette(yarnGhost), ghost: true, bandMax: 0.5 },
|
|
82
|
-
};
|
|
83
79
|
const ICON_SIZE = { xs: "iconXs", sm: "iconSm", md: "iconMd" };
|
|
84
80
|
export function Button({ variant = "primary", size = "md", icon = false, children, ref, sx, ...props }) {
|
|
85
|
-
|
|
86
|
-
// useCallback 鎖 identity,只有 variant 變了才重建織體
|
|
87
|
-
const silk = useCallback((node) => {
|
|
88
|
-
if (!node)
|
|
89
|
-
return;
|
|
90
|
-
const body = new SilkBody(node.parentElement, node, {
|
|
91
|
-
...SILK[variant],
|
|
92
|
-
mode: "press",
|
|
93
|
-
radius: RADIUS,
|
|
94
|
-
});
|
|
95
|
-
return () => body.destroy();
|
|
96
|
-
}, [variant]);
|
|
97
|
-
return (_jsxs("button", { type: "button", ...props, ref: (element) => assignRef(ref, element), ...styled(props, styles.base, styles[size], icon && styles[ICON_SIZE[size]], styles[variant], sx), children: [_jsx("svg", { ref: silk, "aria-hidden": "true", ...stylex.props(styles.silk) }), _jsx("span", { ...stylex.props(styles.label), children: children })] }));
|
|
81
|
+
return (_jsx("button", { type: "button", ...props, ref: (element) => assignRef(ref, element), ...styled(props, styles.base, styles[size], icon && styles[ICON_SIZE[size]], styles[variant], sx), children: _jsx("span", { ...stylex.props(styles.label), children: children }) }));
|
|
98
82
|
}
|
|
@@ -3,26 +3,12 @@ import * as stylex from "@stylexjs/stylex";
|
|
|
3
3
|
import { useCallback, useId } from "react";
|
|
4
4
|
import { assignRef } from "../../lib/mergeRefs.js";
|
|
5
5
|
import { styled } from "../../lib/styled.js";
|
|
6
|
-
import { useSvgId } from "../../lib/svgId.js";
|
|
7
6
|
import { useControllableState } from "../../lib/useControllableState.js";
|
|
8
|
-
import {
|
|
9
|
-
import { color, font, motion, radius, space, text, yarn } from "../../tokens.stylex.js";
|
|
7
|
+
import { color, font, motion, radius, space, text } from "../../tokens.stylex.js";
|
|
10
8
|
import { useFieldControl } from "../label/fieldContext.js";
|
|
11
9
|
const REDUCED = "@media (prefers-reduced-motion: reduce)";
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
// 螢幕上紗的粗細行距與 button 等粗。勾/橫線 = 縫在布上的線,與布共用同一支 rand 序列。
|
|
15
|
-
const rand = weaveRand();
|
|
16
|
-
const CLOTH = buildWeave({ w: 14.8, h: 14.8, s: 24 / 14.8 }, rand);
|
|
17
|
-
const CHECK_D = buildThread([
|
|
18
|
-
[5.83, 12.33],
|
|
19
|
-
[10.05, 16.55],
|
|
20
|
-
[18.18, 7.13],
|
|
21
|
-
], rand);
|
|
22
|
-
const DASH_D = buildThread([
|
|
23
|
-
[6.48, 12],
|
|
24
|
-
[17.53, 12],
|
|
25
|
-
], rand);
|
|
10
|
+
const CHECK_D = "M6 12.4 L10.2 16.6 L18.2 7.4";
|
|
11
|
+
const DASH_D = "M6.5 12 L17.5 12";
|
|
26
12
|
const styles = stylex.create({
|
|
27
13
|
root: {
|
|
28
14
|
display: "flex",
|
|
@@ -35,50 +21,40 @@ const styles = stylex.create({
|
|
|
35
21
|
input: { position: "absolute", opacity: 0, width: "1.05rem", height: "1.05rem", margin: 0 },
|
|
36
22
|
box: {
|
|
37
23
|
flex: "none",
|
|
24
|
+
boxSizing: "border-box",
|
|
38
25
|
width: "1.05rem",
|
|
39
26
|
height: "1.05rem",
|
|
40
27
|
marginTop: "0.16rem",
|
|
41
|
-
borderWidth: 1,
|
|
28
|
+
borderWidth: 1.5,
|
|
42
29
|
borderStyle: "solid",
|
|
43
30
|
borderColor: color.borderStrong,
|
|
44
31
|
borderRadius: radius.sm,
|
|
45
|
-
backgroundColor: color.
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
transitionDuration: { default: motion.normal, [REDUCED]: "0s" },
|
|
32
|
+
backgroundColor: color.bg,
|
|
33
|
+
transitionProperty: "border-color, background-color",
|
|
34
|
+
transitionDuration: { default: motion.fast, [REDUCED]: "0s" },
|
|
49
35
|
outline: { default: "none", ":has(:focus-visible)": `2px solid ${color.focusRing}` },
|
|
50
36
|
outlineOffset: 2,
|
|
51
37
|
},
|
|
52
|
-
boxOn: { borderColor: color.accent },
|
|
38
|
+
boxOn: { borderColor: color.accent, backgroundColor: color.accent },
|
|
39
|
+
boxInvalid: { borderColor: color.danger },
|
|
53
40
|
svg: { display: "block", width: "100%", height: "100%" },
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
41
|
+
mark: {
|
|
42
|
+
fill: "none",
|
|
43
|
+
stroke: color.accentText,
|
|
44
|
+
strokeWidth: 2.4,
|
|
45
|
+
strokeLinecap: "round",
|
|
46
|
+
strokeLinejoin: "round",
|
|
47
|
+
strokeDasharray: 32,
|
|
48
|
+
strokeDashoffset: 32,
|
|
49
|
+
transitionProperty: "stroke-dashoffset",
|
|
50
|
+
transitionDuration: { default: "160ms", [REDUCED]: "0s" },
|
|
51
|
+
transitionTimingFunction: "ease-out",
|
|
62
52
|
},
|
|
63
|
-
|
|
64
|
-
cloth: { fill: "none", strokeLinecap: "round" },
|
|
65
|
-
un: { stroke: yarn.un },
|
|
66
|
-
sh: { stroke: yarn.sh, opacity: 0.5 },
|
|
67
|
-
y0: { stroke: yarn.y0 },
|
|
68
|
-
y1: { stroke: yarn.y1 },
|
|
69
|
-
y2: { stroke: yarn.y2 },
|
|
70
|
-
y3: { stroke: yarn.y3 },
|
|
71
|
-
y4: { stroke: yarn.y4 },
|
|
72
|
-
hi: { stroke: yarn.hi, opacity: 0.45 },
|
|
73
|
-
mark: { fill: "none", strokeLinecap: "round", strokeLinejoin: "round" },
|
|
74
|
-
threadShadow: { stroke: "rgba(9, 20, 16, 0.32)" },
|
|
75
|
-
threadCore: { stroke: color.accentText },
|
|
53
|
+
markOn: { strokeDashoffset: 0 },
|
|
76
54
|
labelText: { display: "block", fontWeight: 500, fontSize: text.sm, color: color.text },
|
|
77
55
|
description: { display: "block", fontSize: text.xs, color: color.textMuted },
|
|
78
56
|
});
|
|
79
|
-
const BUCKETS = [styles.y0, styles.y1, styles.y2, styles.y3, styles.y4];
|
|
80
57
|
export function Checkbox({ indeterminate = false, label, description, onCheckedChange, checked, defaultChecked, onChange, ref, sx, ...props }) {
|
|
81
|
-
const clipId = useSvgId("ak-weave");
|
|
82
58
|
const base = useId();
|
|
83
59
|
const labelId = `${base}label`;
|
|
84
60
|
const descriptionId = `${base}description`;
|
|
@@ -95,8 +71,8 @@ export function Checkbox({ indeterminate = false, label, description, onCheckedC
|
|
|
95
71
|
const describedBy = [description != null ? descriptionId : null, field["aria-describedby"]]
|
|
96
72
|
.filter(Boolean)
|
|
97
73
|
.join(" ");
|
|
98
|
-
return (_jsxs("label", { ...stylex.props(styles.root, sx), children: [_jsxs("span", { ...stylex.props(styles.box, filled && styles.boxOn), children: [_jsx("input", { type: "checkbox", ...props, ...field, "aria-invalid": invalid || undefined, "aria-labelledby": label != null ? labelId : undefined, "aria-describedby": describedBy || undefined, ref: setNode, checked: isChecked, onChange: (event) => {
|
|
74
|
+
return (_jsxs("label", { ...stylex.props(styles.root, sx), children: [_jsxs("span", { ...stylex.props(styles.box, filled && styles.boxOn, invalid && styles.boxInvalid), children: [_jsx("input", { type: "checkbox", ...props, ...field, "aria-invalid": invalid || undefined, "aria-labelledby": label != null ? labelId : undefined, "aria-describedby": describedBy || undefined, ref: setNode, checked: isChecked, onChange: (event) => {
|
|
99
75
|
setChecked(event.currentTarget.checked);
|
|
100
76
|
onChange?.(event);
|
|
101
|
-
}, ...styled(props, styles.input) }),
|
|
77
|
+
}, ...styled(props, styles.input) }), _jsx("svg", { viewBox: "0 0 24 24", "aria-hidden": "true", ...stylex.props(styles.svg), children: _jsx("path", { d: indeterminate ? DASH_D : CHECK_D, ...stylex.props(styles.mark, filled && styles.markOn) }) })] }), (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 }))] }))] }));
|
|
102
78
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
|
+
import { type StyleArg } from "../../lib/styled.js";
|
|
2
3
|
export type SortState = {
|
|
3
4
|
col: string;
|
|
4
5
|
dir: "asc" | "desc";
|
|
@@ -34,5 +35,10 @@ export type DataTableProps<Row> = {
|
|
|
34
35
|
selectAllLabel?: string;
|
|
35
36
|
editLabel?: (column: string, key: string) => string;
|
|
36
37
|
clearLabel?: string;
|
|
38
|
+
/** 捲動區的高度上限。 @default "20rem" */
|
|
39
|
+
maxHeight?: string | number;
|
|
40
|
+
/** 跟著列一起捲的結尾 —— 「載入更多」要待在清單裡,不是待在清單外。 */
|
|
41
|
+
footer?: ReactNode;
|
|
42
|
+
sx?: StyleArg;
|
|
37
43
|
};
|
|
38
|
-
export declare function DataTable<Row>({ rows, total, rowKey, columns, label, filter, onFilterChange, filterPlaceholder, sort, onSortChange, selected, onSelectedChange, emptyState, onClearFilter, countLabel, selectLabel, selectAllLabel, editLabel, clearLabel, }: DataTableProps<Row>): import("react").JSX.Element;
|
|
44
|
+
export declare function DataTable<Row>({ rows, total, rowKey, columns, label, filter, onFilterChange, filterPlaceholder, sort, onSortChange, selected, onSelectedChange, emptyState, onClearFilter, countLabel, selectLabel, selectAllLabel, editLabel, clearLabel, maxHeight, footer, sx, }: DataTableProps<Row>): import("react").JSX.Element;
|
|
@@ -46,10 +46,10 @@ const styles = stylex.create({
|
|
|
46
46
|
borderStyle: "solid",
|
|
47
47
|
borderColor: color.border,
|
|
48
48
|
borderRadius: radius.lg,
|
|
49
|
-
maxHeight: "20rem",
|
|
50
49
|
overflow: "auto",
|
|
51
50
|
marginTop: space.xs,
|
|
52
51
|
},
|
|
52
|
+
capped: (maxHeight) => ({ maxHeight }),
|
|
53
53
|
table: { width: "100%", borderCollapse: "collapse", fontFamily: font.body, fontSize: text.sm },
|
|
54
54
|
th: {
|
|
55
55
|
position: "sticky",
|
|
@@ -161,7 +161,7 @@ function focusOnMount(node) {
|
|
|
161
161
|
function cellValue(column, row) {
|
|
162
162
|
return column.value?.(row) ?? "";
|
|
163
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 = "清除過濾", }) {
|
|
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 = "清除過濾", maxHeight = "20rem", footer, sx, }) {
|
|
165
165
|
const [editing, setEditing] = useState(null);
|
|
166
166
|
const cancelling = useRef(false);
|
|
167
167
|
const [draft, setDraft] = useState("");
|
|
@@ -219,38 +219,38 @@ export function DataTable({ rows, total, rowKey, columns, label, filter = "", on
|
|
|
219
219
|
}
|
|
220
220
|
onSelectedChange?.(next);
|
|
221
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) })] })),
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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") {
|
|
222
|
+
return (_jsxs("div", { ...stylex.props(sx), 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) })] })), _jsxs("div", { tabIndex: 0, role: "region", "aria-label": label, ...stylex.props(styles.wrap, styles.capped(maxHeight)), 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;
|
|
245
239
|
event.preventDefault();
|
|
246
|
-
|
|
240
|
+
startEdit(key, column.id, value);
|
|
247
241
|
}
|
|
248
|
-
|
|
249
|
-
event.
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
+
})) })] }), footer] })] }));
|
|
256
256
|
}
|
|
@@ -20,10 +20,15 @@ export type DialogContentProps = {
|
|
|
20
20
|
title: ReactNode;
|
|
21
21
|
description?: ReactNode;
|
|
22
22
|
children?: ReactNode;
|
|
23
|
+
/** 寬度階;`full` 是沉浸式(整個視窗那麼高)。 @default "sm" */
|
|
24
|
+
size?: "sm" | "md" | "full";
|
|
25
|
+
/** 會捲的那一段 —— 標頭與 children 釘住,只有這裡捲。 */
|
|
26
|
+
body?: ReactNode;
|
|
27
|
+
bodySx?: StyleArg;
|
|
23
28
|
/** 寬高是 popup 自己的事,但要蓋得掉 —— 三欄選擇器那種得自己給 width / maxHeight */
|
|
24
29
|
sx?: StyleArg;
|
|
25
30
|
};
|
|
26
|
-
export declare function DialogContent({ title, description, children, sx }: DialogContentProps): import("react").JSX.Element;
|
|
31
|
+
export declare function DialogContent({ title, description, children, size, body, bodySx, sx, }: DialogContentProps): import("react").JSX.Element;
|
|
27
32
|
export type ConfirmDialogProps = {
|
|
28
33
|
open?: boolean;
|
|
29
34
|
defaultOpen?: boolean;
|
|
@@ -44,10 +44,18 @@ const styles = stylex.create({
|
|
|
44
44
|
outline: { default: "none", ":focus-visible": `2px solid ${color.focusRing}` },
|
|
45
45
|
outlineOffset: -2,
|
|
46
46
|
},
|
|
47
|
+
md: { width: "min(40rem, calc(100vw - 2rem))" },
|
|
48
|
+
full: {
|
|
49
|
+
width: "min(64rem, calc(100vw - 2rem))",
|
|
50
|
+
height: "min(46rem, calc(100vh - 2rem))",
|
|
51
|
+
},
|
|
52
|
+
// 有 body 就換成「標頭釘住、只有 body 捲」:popup 自己不再捲
|
|
53
|
+
split: { display: "flex", flexDirection: "column", overflowY: "hidden" },
|
|
54
|
+
body: { flexGrow: 1, minHeight: 0, overflowY: "auto" },
|
|
47
55
|
title: {
|
|
48
56
|
fontFamily: font.display,
|
|
49
57
|
fontSize: text.lg,
|
|
50
|
-
fontWeight:
|
|
58
|
+
fontWeight: 600,
|
|
51
59
|
lineHeight: text.leadingSnug,
|
|
52
60
|
margin: 0,
|
|
53
61
|
marginBottom: space.xxs,
|
|
@@ -67,8 +75,8 @@ export function DialogClose({ children }) {
|
|
|
67
75
|
export function DialogActions({ children }) {
|
|
68
76
|
return _jsx("div", { ...stylex.props(styles.actions), children: children });
|
|
69
77
|
}
|
|
70
|
-
export function DialogContent({ title, description, children, sx }) {
|
|
71
|
-
return (_jsxs(BaseDialog.Portal, { children: [_jsx(BaseDialog.Backdrop, { ...stylex.props(layerStyles.dialogBackdrop, styles.backdrop) }), _jsx(BaseDialog.Viewport, { ...stylex.props(layerStyles.dialog, styles.viewport), children: _jsxs(BaseDialog.Popup, { ...stylex.props(styles.popup, sx), children: [_jsx(BaseDialog.Title, { ...stylex.props(styles.title), children: title }), description != null && (_jsx(BaseDialog.Description, { ...stylex.props(styles.description), children: description })), children] }) })] }));
|
|
78
|
+
export function DialogContent({ title, description, children, size = "sm", body, bodySx, sx, }) {
|
|
79
|
+
return (_jsxs(BaseDialog.Portal, { children: [_jsx(BaseDialog.Backdrop, { ...stylex.props(layerStyles.dialogBackdrop, styles.backdrop) }), _jsx(BaseDialog.Viewport, { ...stylex.props(layerStyles.dialog, styles.viewport), children: _jsxs(BaseDialog.Popup, { ...stylex.props(styles.popup, size === "md" && styles.md, size === "full" && styles.full, body != null && styles.split, sx), children: [_jsx(BaseDialog.Title, { ...stylex.props(styles.title), children: title }), description != null && (_jsx(BaseDialog.Description, { ...stylex.props(styles.description), children: description })), children, body != null && _jsx("div", { ...stylex.props(styles.body, bodySx), children: body })] }) })] }));
|
|
72
80
|
}
|
|
73
81
|
export function ConfirmDialog({ trigger, title, description, danger = false, confirmLabel = "確認", cancelLabel = "取消", onConfirm, ...props }) {
|
|
74
82
|
const cancelRef = useRef(null);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as stylex from "@stylexjs/stylex";
|
|
2
|
+
import type { ButtonHTMLAttributes, Ref } from "react";
|
|
3
|
+
export type GhostProps = Omit<ButtonHTMLAttributes<HTMLButtonElement>, "type"> & {
|
|
4
|
+
danger?: boolean;
|
|
5
|
+
ref?: Ref<HTMLButtonElement>;
|
|
6
|
+
sx?: stylex.StyleXStyles;
|
|
7
|
+
};
|
|
8
|
+
export declare function Ghost({ danger, sx, ...props }: GhostProps): import("react").JSX.Element;
|
|
9
|
+
export type GhostLinkProps = {
|
|
10
|
+
href: string;
|
|
11
|
+
children: React.ReactNode;
|
|
12
|
+
external?: boolean;
|
|
13
|
+
sx?: stylex.StyleXStyles;
|
|
14
|
+
};
|
|
15
|
+
/** The same word, as a link that leaves the app. */
|
|
16
|
+
export declare function GhostLink({ href, children, external, sx }: GhostLinkProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import * as stylex from "@stylexjs/stylex";
|
|
3
|
+
import { color, corner, font, motion, space, type } from "../../tokens.stylex.js";
|
|
4
|
+
/** `.ghost`: a word that is a button. 28px, muted until the pointer is on it; `danger` is warning. */
|
|
5
|
+
const REDUCED = "@media (prefers-reduced-motion: reduce)";
|
|
6
|
+
const PHONE = "@media (max-width: 45rem)";
|
|
7
|
+
const styles = stylex.create({
|
|
8
|
+
root: {
|
|
9
|
+
alignItems: "center",
|
|
10
|
+
backgroundColor: { default: "transparent", ":hover": color.layer3 },
|
|
11
|
+
borderRadius: corner.sm,
|
|
12
|
+
borderStyle: "none",
|
|
13
|
+
borderWidth: 0,
|
|
14
|
+
color: { default: color.textMuted, ":hover": color.text },
|
|
15
|
+
cursor: { default: "pointer", ":disabled": "default" },
|
|
16
|
+
display: "inline-flex",
|
|
17
|
+
fontFamily: font.body,
|
|
18
|
+
fontSize: type.t2,
|
|
19
|
+
fontWeight: 400,
|
|
20
|
+
gap: space.xxs,
|
|
21
|
+
height: 28,
|
|
22
|
+
lineHeight: type.body,
|
|
23
|
+
margin: 0,
|
|
24
|
+
minHeight: { default: 0, [PHONE]: 40 },
|
|
25
|
+
opacity: { default: 1, ":disabled": 0.4 },
|
|
26
|
+
outline: { default: "none", ":focus-visible": `2px solid ${color.accent}` },
|
|
27
|
+
outlineOffset: 2,
|
|
28
|
+
paddingBlock: 0,
|
|
29
|
+
paddingInline: space.xs,
|
|
30
|
+
textDecoration: "none",
|
|
31
|
+
transitionDuration: { default: motion.fast, [REDUCED]: "0s" },
|
|
32
|
+
transitionProperty: "background-color, color",
|
|
33
|
+
transitionTimingFunction: motion.easeOut,
|
|
34
|
+
whiteSpace: "nowrap",
|
|
35
|
+
},
|
|
36
|
+
danger: {
|
|
37
|
+
backgroundColor: {
|
|
38
|
+
default: "transparent",
|
|
39
|
+
":hover": `color-mix(in srgb, ${color.warning} 10%, transparent)`,
|
|
40
|
+
},
|
|
41
|
+
color: { default: color.warning, ":hover": color.warning },
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
export function Ghost({ danger = false, sx, ...props }) {
|
|
45
|
+
return _jsx("button", { type: "button", ...props, ...stylex.props(styles.root, danger && styles.danger, sx) });
|
|
46
|
+
}
|
|
47
|
+
/** The same word, as a link that leaves the app. */
|
|
48
|
+
export function GhostLink({ href, children, external = false, sx }) {
|
|
49
|
+
return (_jsx("a", { href: href, ...(external ? { target: "_blank", rel: "noreferrer" } : {}), ...stylex.props(styles.root, sx), children: children }));
|
|
50
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import * as stylex from "@stylexjs/stylex";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
type Sx = {
|
|
4
|
+
sx?: stylex.StyleXStyles;
|
|
5
|
+
};
|
|
6
|
+
export declare function Group({ children, sx }: {
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
} & Sx): import("react").JSX.Element;
|
|
9
|
+
export declare function Item({ children, go, sx }: {
|
|
10
|
+
children: ReactNode;
|
|
11
|
+
go?: boolean;
|
|
12
|
+
} & Sx): import("react").JSX.Element;
|
|
13
|
+
export type MarkProps = {
|
|
14
|
+
letter: string;
|
|
15
|
+
tint: string;
|
|
16
|
+
sx?: stylex.StyleXStyles;
|
|
17
|
+
};
|
|
18
|
+
/** The 22px square with the first letter; `tint` is `<colour> <percent>%` for the mix. */
|
|
19
|
+
export declare function Mark({ letter, tint, sx }: MarkProps): import("react").JSX.Element;
|
|
20
|
+
export type StatusProps = {
|
|
21
|
+
children: ReactNode;
|
|
22
|
+
warn?: boolean;
|
|
23
|
+
sx?: stylex.StyleXStyles;
|
|
24
|
+
};
|
|
25
|
+
export declare function Status({ children, warn, sx }: StatusProps): import("react").JSX.Element;
|
|
26
|
+
export declare function Sep(): import("react").JSX.Element;
|
|
27
|
+
export type RowProps = {
|
|
28
|
+
mark?: ReactNode;
|
|
29
|
+
name: ReactNode;
|
|
30
|
+
/** What follows the name after a `·`: a `Status`, a mono value, a tag. */
|
|
31
|
+
children?: ReactNode;
|
|
32
|
+
actions?: ReactNode;
|
|
33
|
+
/** The whole line is the way in; a chevron says so. */
|
|
34
|
+
onPress?: () => void;
|
|
35
|
+
chevron?: boolean;
|
|
36
|
+
sx?: stylex.StyleXStyles;
|
|
37
|
+
};
|
|
38
|
+
export declare function Row({ mark, name, children, actions, onPress, chevron, sx }: RowProps): import("react").JSX.Element;
|
|
39
|
+
export type ExpandProps = {
|
|
40
|
+
open: boolean;
|
|
41
|
+
/** `plain` drops the mark indent, for a form that is not under a row's mark. */
|
|
42
|
+
plain?: boolean;
|
|
43
|
+
children: ReactNode;
|
|
44
|
+
sx?: stylex.StyleXStyles;
|
|
45
|
+
};
|
|
46
|
+
/** `.exp`: what a row opens, unfolding under it in 200ms and taking no room when shut. */
|
|
47
|
+
export declare function Expand({ open, plain, children, sx }: ExpandProps): import("react").JSX.Element;
|
|
48
|
+
export type NoteProps = {
|
|
49
|
+
children: ReactNode;
|
|
50
|
+
faint?: boolean;
|
|
51
|
+
err?: boolean;
|
|
52
|
+
sx?: stylex.StyleXStyles;
|
|
53
|
+
};
|
|
54
|
+
/** A sentence inside an `Expand`: what it means, or what went wrong. */
|
|
55
|
+
export declare function Note({ children, faint, err, sx }: NoteProps): import("react").JSX.Element;
|
|
56
|
+
export declare function Acts({ children, sx }: {
|
|
57
|
+
children: ReactNode;
|
|
58
|
+
} & Sx): import("react").JSX.Element;
|
|
59
|
+
/** `.empty`: nothing here yet, said inside the group in one muted line. */
|
|
60
|
+
export declare function Empty({ children, sx }: {
|
|
61
|
+
children: ReactNode;
|
|
62
|
+
} & Sx): import("react").JSX.Element;
|
|
63
|
+
export declare function Tag({ children, sx }: {
|
|
64
|
+
children: ReactNode;
|
|
65
|
+
} & Sx): import("react").JSX.Element;
|
|
66
|
+
export {};
|