@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.
Files changed (70) hide show
  1. package/README.md +6 -7
  2. package/dist/brand.css +111 -124
  3. package/dist/components/badge/Badge.d.ts +11 -3
  4. package/dist/components/badge/Badge.js +21 -3
  5. package/dist/components/button/Button.d.ts +3 -2
  6. package/dist/components/button/Button.js +32 -48
  7. package/dist/components/checkbox/Checkbox.js +24 -48
  8. package/dist/components/data-table/DataTable.d.ts +7 -1
  9. package/dist/components/data-table/DataTable.js +34 -34
  10. package/dist/components/dialog/Dialog.d.ts +6 -1
  11. package/dist/components/dialog/Dialog.js +11 -3
  12. package/dist/components/empty-state/EmptyState.js +1 -1
  13. package/dist/components/ghost/Ghost.d.ts +16 -0
  14. package/dist/components/ghost/Ghost.js +50 -0
  15. package/dist/components/group/Group.d.ts +66 -0
  16. package/dist/components/group/Group.js +166 -0
  17. package/dist/components/handoff-receipt/HandoffReceipt.js +12 -42
  18. package/dist/components/icon/Chevron.d.ts +9 -0
  19. package/dist/components/icon/Chevron.js +6 -0
  20. package/dist/components/icon/icon.d.ts +35 -0
  21. package/dist/components/icon/icon.js +10 -0
  22. package/dist/components/icon-button/IconButton.d.ts +17 -0
  23. package/dist/components/icon-button/IconButton.js +66 -0
  24. package/dist/components/live-dot/LiveDot.d.ts +7 -0
  25. package/dist/components/live-dot/LiveDot.js +35 -0
  26. package/dist/components/message/Message.d.ts +3 -1
  27. package/dist/components/message/Message.js +2 -2
  28. package/dist/components/page/Page.d.ts +64 -0
  29. package/dist/components/page/Page.js +163 -0
  30. package/dist/components/popover/Popover.js +1 -1
  31. package/dist/components/progress/Progress.js +49 -104
  32. package/dist/components/radio/Radio.js +17 -28
  33. package/dist/components/segmented/Segmented.d.ts +12 -0
  34. package/dist/components/segmented/Segmented.js +43 -0
  35. package/dist/components/settings-rows/SettingsRows.d.ts +25 -0
  36. package/dist/components/settings-rows/SettingsRows.js +63 -0
  37. package/dist/components/slider/Slider.d.ts +19 -0
  38. package/dist/components/slider/Slider.js +103 -0
  39. package/dist/components/spin/Spin.d.ts +5 -0
  40. package/dist/components/spin/Spin.js +25 -0
  41. package/dist/components/status-chip/StatusChip.d.ts +18 -0
  42. package/dist/components/status-chip/StatusChip.js +68 -0
  43. package/dist/components/switch/Switch.js +20 -63
  44. package/dist/components/table/Table.d.ts +39 -0
  45. package/dist/components/table/Table.js +125 -0
  46. package/dist/components/table/TableCells.d.ts +52 -0
  47. package/dist/components/table/TableCells.js +103 -0
  48. package/dist/components/tabs/Tabs.js +16 -48
  49. package/dist/components/text/Text.js +2 -2
  50. package/dist/components/toast/Toast.js +7 -8
  51. package/dist/index.d.ts +14 -0
  52. package/dist/index.js +14 -0
  53. package/dist/lib/layers.d.ts +7 -0
  54. package/dist/lib/layers.js +12 -0
  55. package/dist/themes.stylex.d.ts +54 -206
  56. package/dist/themes.stylex.js +55 -157
  57. package/dist/tokens.css +63 -46
  58. package/dist/tokens.stylex.d.ts +41 -54
  59. package/dist/tokens.stylex.js +64 -85
  60. package/package.json +2 -2
  61. package/dist/lib/paths.d.ts +0 -3
  62. package/dist/lib/paths.js +0 -11
  63. package/dist/lib/progress.d.ts +0 -2
  64. package/dist/lib/progress.js +0 -28
  65. package/dist/lib/silk.d.ts +0 -66
  66. package/dist/lib/silk.js +0 -337
  67. package/dist/lib/svgId.d.ts +0 -1
  68. package/dist/lib/svgId.js +0 -4
  69. package/dist/lib/weave.d.ts +0 -28
  70. 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
- return (_jsxs(Badge, { variant: variant, ...props, 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" }) }) }))] }));
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 { type ComponentProps } from "react";
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?: "primary" | "secondary" | "ghost" | "danger" | "dangerGhost";
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, jsxs as _jsxs } from "react/jsx-runtime";
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, yarn, yarnDanger, yarnGhost, yarnSecondary, } from "../../tokens.stylex.js";
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: 8, // = radius.md;織線 clip 的 rx 同步寫死在 RADIUS
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
- // filter = 整塊布的落影(§3.3);ghost 是疏織,不落影
55
- primary: { color: color.accentText, filter: yarn.shadow },
56
- secondary: { color: color.text, filter: yarnSecondary.shadow },
57
- ghost: { color: color.textMuted, filter: yarnGhost.shadow },
58
- danger: { color: color.accentText, filter: yarnDanger.shadow },
59
- // 「白底紅字」的織體版:份量走 ghost 疏織(不與 primary 打架),
60
- // 語意走 danger token(和 interaction-card 收據列 rejected 的 ✓ 同一個 token)
61
- dangerGhost: { color: color.danger, filter: yarnGhost.shadow },
62
- silk: {
63
- position: "absolute",
64
- inset: 0,
65
- width: "100%",
66
- height: "100%",
67
- overflow: "hidden",
68
- pointerEvents: "none",
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
- // ref callback + cleanup(React 19)管生命週期,不用 effect;
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 { buildThread, buildWeave, weaveRand } from "../../lib/weave.js";
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
- // 布與縫線在 module scope 織一次(固定種子、固定尺寸 → 恆定;純幾何,SSR 安全)。
13
- // 格子顯示 14.8 CSS px(1.05rem − 2px border),viewBox 24 → s = 24/14.8,
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.surface,
46
- overflow: "hidden",
47
- transitionProperty: "border-color",
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
- // 勾選 = 布織進格子:clip rect 的 width 從左往右推進,紗本身不動;
55
- // 縫線掛同一個 clip — 前緣推到哪,布和勾一起被織出來
56
- weave: {
57
- width: 0,
58
- height: 24,
59
- transitionProperty: "width",
60
- transitionDuration: { default: motion.normal, [REDUCED]: "0s" },
61
- transitionTimingFunction: "cubic-bezier(0.32, 0.85, 0.45, 1)",
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
- weaveOn: { width: 24 },
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) }), _jsxs("svg", { viewBox: "0 0 24 24", "aria-hidden": "true", ...stylex.props(styles.svg), children: [_jsx("defs", { children: _jsx("clipPath", { id: clipId, children: _jsx("rect", { rx: "3", ...stylex.props(styles.weave, filled && styles.weaveOn) }) }) }), _jsxs("g", { clipPath: `url(#${clipId})`, children: [_jsxs("g", { ...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))) })] }), _jsxs("g", { ...stylex.props(styles.mark), children: [_jsx("path", { d: indeterminate ? DASH_D : CHECK_D, strokeWidth: "2.1", transform: "translate(0.35 0.5)", ...stylex.props(styles.threadShadow) }), _jsx("path", { d: indeterminate ? DASH_D : CHECK_D, strokeWidth: "1.95", ...stylex.props(styles.threadCore) })] })] })] })] }), (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 }))] }))] }));
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) })] })), _jsx("div", { tabIndex: 0, role: "region", "aria-label": label, ...stylex.props(styles.wrap), children: _jsxs("table", { "aria-label": label, ...stylex.props(styles.table), children: [_jsx("thead", { children: _jsxs("tr", { children: [selected != null && (_jsx("th", { scope: "col", ...stylex.props(styles.th, styles.thCheck), children: _jsx("input", { ref: selectAll, type: "checkbox", checked: allSelected, "aria-label": selectAllLabel, onChange: (event) => toggleAll(event.currentTarget.checked), ...stylex.props(styles.checkbox) }) })), columns.map((column) => (_jsx("th", { scope: "col", "aria-sort": sort?.col === column.id ? (sort.dir === "asc" ? "ascending" : "descending") : undefined, ...stylex.props(styles.th), children: column.sortable ? (_jsxs("button", { type: "button", onClick: () => toggleSort(column), ...stylex.props(reset.control, styles.sort), children: [column.header, sort?.col === column.id && (_jsx("span", { "aria-hidden": "true", ...stylex.props(styles.arrow), children: sort.dir === "asc" ? "▲" : "▼" }))] })) : (_jsx("span", { ...stylex.props(styles.plainHeader), children: column.header })) }, column.id)))] }) }), _jsx("tbody", { children: visible.length === 0 ? (_jsx("tr", { children: _jsxs("td", { colSpan: columns.length + (selected != null ? 1 : 0), ...stylex.props(styles.emptyRow), children: [emptyState?.(filter) ?? `找不到符合「${filter}」的資料。`, onClearFilter != null && (_jsxs(_Fragment, { children: [" ", _jsx("button", { type: "button", onClick: onClearFilter, ...stylex.props(reset.control, styles.clear), children: clearLabel })] }))] }) })) : (visible.map((row) => {
223
- const key = rowKey(row);
224
- const isSelected = selected?.has(key) ?? false;
225
- return (_jsxs("tr", { ...stylex.props(styles.row, isSelected && styles.rowSelected), children: [selected != null && (_jsx("td", { ...stylex.props(styles.td, styles.tdCheck), children: _jsx("input", { type: "checkbox", checked: isSelected, "aria-label": selectLabel(key), onChange: (event) => {
226
- const next = new Set(selected);
227
- if (event.currentTarget.checked)
228
- next.add(key);
229
- else
230
- next.delete(key);
231
- onSelectedChange?.(next);
232
- }, ...stylex.props(styles.checkbox) }) })), columns.map((column) => {
233
- const isEditing = editing?.key === key && editing.col === column.id;
234
- const value = cellValue(column, row);
235
- return (_jsx("td", { tabIndex: column.editable ? 0 : undefined, onDoubleClick: column.editable ? () => startEdit(key, column.id, value) : undefined, onKeyDown: column.editable && !isEditing
236
- ? (event) => {
237
- if (event.key !== "Enter" && event.key !== "F2")
238
- return;
239
- event.preventDefault();
240
- startEdit(key, column.id, value);
241
- }
242
- : undefined, ...stylex.props(styles.td, column.mono && styles.mono, column.editable && styles.editable), children: isEditing ? (_jsx("input", { ref: focusOnMount, "aria-label": editLabel(column.header, key), value: draft, onChange: (event) => setDraft(event.currentTarget.value), onBlur: (event) => commit(column, row, draft, event.currentTarget.closest("td")), onKeyDown: (event) => {
243
- const cell = event.currentTarget.closest("td");
244
- if (event.key === "Enter") {
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
- commit(column, row, draft, cell);
240
+ startEdit(key, column.id, value);
247
241
  }
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
- })) })] }) })] }));
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: 500,
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);
@@ -27,7 +27,7 @@ const styles = stylex.create({
27
27
  title: {
28
28
  fontFamily: font.display,
29
29
  fontSize: text.base,
30
- fontWeight: 500,
30
+ fontWeight: 600,
31
31
  lineHeight: text.leadingSnug,
32
32
  margin: 0,
33
33
  color: color.text,
@@ -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 {};