@anyknown/ui 0.7.0 → 0.8.1

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 (59) hide show
  1. package/README.md +7 -8
  2. package/dist/brand.css +101 -214
  3. package/dist/components/button/Button.d.ts +3 -2
  4. package/dist/components/button/Button.js +32 -48
  5. package/dist/components/checkbox/Checkbox.js +24 -48
  6. package/dist/components/dialog/Dialog.js +1 -1
  7. package/dist/components/empty-state/EmptyState.js +1 -1
  8. package/dist/components/ghost/Ghost.d.ts +16 -0
  9. package/dist/components/ghost/Ghost.js +50 -0
  10. package/dist/components/group/Group.d.ts +66 -0
  11. package/dist/components/group/Group.js +166 -0
  12. package/dist/components/handoff-receipt/HandoffReceipt.js +12 -42
  13. package/dist/components/icon/Chevron.d.ts +9 -0
  14. package/dist/components/icon/Chevron.js +6 -0
  15. package/dist/components/icon/icon.d.ts +35 -0
  16. package/dist/components/icon/icon.js +10 -0
  17. package/dist/components/icon-button/IconButton.d.ts +17 -0
  18. package/dist/components/icon-button/IconButton.js +66 -0
  19. package/dist/components/input/Input.d.ts +3 -1
  20. package/dist/components/input/Input.js +3 -2
  21. package/dist/components/page/Page.d.ts +64 -0
  22. package/dist/components/page/Page.js +163 -0
  23. package/dist/components/popover/Popover.js +1 -1
  24. package/dist/components/progress/Progress.js +49 -104
  25. package/dist/components/radio/Radio.js +17 -28
  26. package/dist/components/segmented/Segmented.d.ts +12 -0
  27. package/dist/components/segmented/Segmented.js +43 -0
  28. package/dist/components/settings-rows/SettingsRows.d.ts +25 -0
  29. package/dist/components/settings-rows/SettingsRows.js +63 -0
  30. package/dist/components/spin/Spin.d.ts +5 -0
  31. package/dist/components/spin/Spin.js +25 -0
  32. package/dist/components/status-chip/StatusChip.d.ts +18 -0
  33. package/dist/components/status-chip/StatusChip.js +68 -0
  34. package/dist/components/switch/Switch.js +20 -63
  35. package/dist/components/table/Table.d.ts +39 -0
  36. package/dist/components/table/Table.js +125 -0
  37. package/dist/components/table/TableCells.d.ts +52 -0
  38. package/dist/components/table/TableCells.js +103 -0
  39. package/dist/components/tabs/Tabs.js +16 -48
  40. package/dist/components/text/Text.js +2 -2
  41. package/dist/components/toast/Toast.js +7 -8
  42. package/dist/index.d.ts +11 -0
  43. package/dist/index.js +11 -0
  44. package/dist/themes.stylex.d.ts +33 -496
  45. package/dist/themes.stylex.js +25 -253
  46. package/dist/tokens.css +53 -126
  47. package/dist/tokens.stylex.d.ts +36 -54
  48. package/dist/tokens.stylex.js +62 -90
  49. package/package.json +1 -1
  50. package/dist/lib/paths.d.ts +0 -3
  51. package/dist/lib/paths.js +0 -11
  52. package/dist/lib/progress.d.ts +0 -2
  53. package/dist/lib/progress.js +0 -28
  54. package/dist/lib/silk.d.ts +0 -66
  55. package/dist/lib/silk.js +0 -337
  56. package/dist/lib/svgId.d.ts +0 -1
  57. package/dist/lib/svgId.js +0 -4
  58. package/dist/lib/weave.d.ts +0 -28
  59. package/dist/lib/weave.js +0 -83
@@ -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
  }
@@ -55,7 +55,7 @@ const styles = stylex.create({
55
55
  title: {
56
56
  fontFamily: font.display,
57
57
  fontSize: text.lg,
58
- fontWeight: 500,
58
+ fontWeight: 600,
59
59
  lineHeight: text.leadingSnug,
60
60
  margin: 0,
61
61
  marginBottom: space.xxs,
@@ -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 {};
@@ -0,0 +1,166 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import * as stylex from "@stylexjs/stylex";
3
+ import { color, corner, font, motion, space, tone, type } from "../../tokens.stylex.js";
4
+ import { Chevron } from "../icon/Chevron.js";
5
+ import { icon } from "../icon/icon.js";
6
+ /**
7
+ * `.group` / `.item` / `.row`: a bordered list where each line is a thing (a server, a key, a
8
+ * rule) with its name, its state and its one action on the right. What a row opens unfolds under
9
+ * it in an `Expand`, never over the screen.
10
+ */
11
+ const REDUCED = "@media (prefers-reduced-motion: reduce)";
12
+ const styles = stylex.create({
13
+ group: {
14
+ backgroundColor: color.surface,
15
+ borderRadius: corner.card,
16
+ boxShadow: `inset 0 0 0 1px ${color.border}`,
17
+ color: color.text,
18
+ fontSize: type.t2,
19
+ lineHeight: type.body,
20
+ },
21
+ item: {
22
+ borderRadius: {
23
+ default: 0,
24
+ ":first-child": `${corner.card} ${corner.card} 0 0`,
25
+ ":last-child": `0 0 ${corner.card} ${corner.card}`,
26
+ },
27
+ boxShadow: { default: "none", ":not(:first-child)": `inset 0 1px 0 ${color.border}` },
28
+ overflow: "hidden",
29
+ },
30
+ go: { backgroundColor: { default: "transparent", ":hover": color.layer3 } },
31
+ row: {
32
+ alignItems: "center",
33
+ columnGap: space.sm,
34
+ display: "grid",
35
+ gridTemplateColumns: "minmax(0, 1fr) auto",
36
+ minHeight: 40,
37
+ paddingBlock: space.xs,
38
+ paddingInline: space.sm,
39
+ },
40
+ rowgo: {
41
+ alignItems: "center",
42
+ backgroundColor: "transparent",
43
+ borderRadius: corner.sm,
44
+ borderStyle: "none",
45
+ borderWidth: 0,
46
+ color: "inherit",
47
+ cursor: "pointer",
48
+ display: "flex",
49
+ flexWrap: "wrap",
50
+ fontFamily: "inherit",
51
+ fontSize: "inherit",
52
+ gap: `0 ${space.xs}`,
53
+ lineHeight: "inherit",
54
+ margin: 0,
55
+ minWidth: 0,
56
+ outline: { default: "none", ":focus-visible": `2px solid ${color.accent}` },
57
+ outlineOffset: 2,
58
+ padding: 0,
59
+ textAlign: "start",
60
+ },
61
+ still: { cursor: "default" },
62
+ mark: {
63
+ alignItems: "center",
64
+ borderRadius: corner.sm,
65
+ color: color.text,
66
+ display: "grid",
67
+ flex: "none",
68
+ fontSize: type.t1,
69
+ fontWeight: 600,
70
+ height: 22,
71
+ justifyItems: "center",
72
+ placeItems: "center",
73
+ width: 22,
74
+ },
75
+ markTint: (tint) => ({ backgroundColor: `color-mix(in srgb, ${tint}, ${color.layer3})` }),
76
+ name: {
77
+ flex: "none",
78
+ fontSize: type.t2,
79
+ fontWeight: 500,
80
+ overflow: "hidden",
81
+ textOverflow: "ellipsis",
82
+ whiteSpace: "nowrap",
83
+ },
84
+ sep: { color: tone.faint, flex: "none", fontSize: type.t2 },
85
+ status: {
86
+ alignItems: "center",
87
+ color: color.textMuted,
88
+ display: "inline-flex",
89
+ fontSize: type.t2,
90
+ gap: space.xs,
91
+ minWidth: 0,
92
+ },
93
+ statusText: { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
94
+ warn: { color: color.warning },
95
+ dot: { backgroundColor: color.warning, borderRadius: "50%", flex: "none", height: 6, width: 6 },
96
+ tag: { color: color.textMuted, flex: "none", fontFamily: font.mono, fontSize: type.t2 },
97
+ act: { alignItems: "center", display: "flex", gap: space.xs },
98
+ chevron: { color: tone.faint },
99
+ expand: {
100
+ display: "grid",
101
+ gridTemplateRows: "0fr",
102
+ transitionDuration: { default: motion.normal, [REDUCED]: "0s" },
103
+ transitionProperty: "grid-template-rows",
104
+ transitionTimingFunction: motion.easeOut,
105
+ },
106
+ shown: { gridTemplateRows: "1fr" },
107
+ clip: { minHeight: 0, overflow: "hidden" },
108
+ in: {
109
+ alignItems: "flex-start",
110
+ display: "flex",
111
+ flexDirection: "column",
112
+ gap: space.xs,
113
+ paddingBlock: `0 ${space.sm}`,
114
+ paddingInline: `calc(${space.sm} + 22px + ${space.xs}) ${space.sm}`,
115
+ },
116
+ plain: { paddingBlock: `${space.xs} ${space.sm}`, paddingInline: space.sm },
117
+ text: { color: color.textMuted, fontSize: type.t2, lineHeight: type.body, margin: 0 },
118
+ faint: { color: tone.faint },
119
+ err: { color: color.warning },
120
+ acts: { alignItems: "center", display: "flex", gap: space.xs },
121
+ empty: {
122
+ color: color.textMuted,
123
+ fontSize: type.t2,
124
+ lineHeight: type.body,
125
+ margin: 0,
126
+ padding: space.sm,
127
+ },
128
+ });
129
+ export function Group({ children, sx }) {
130
+ return _jsx("div", { ...stylex.props(styles.group, sx), children: children });
131
+ }
132
+ export function Item({ children, go = false, sx }) {
133
+ return _jsx("div", { ...stylex.props(styles.item, go && styles.go, sx), children: children });
134
+ }
135
+ /** The 22px square with the first letter; `tint` is `<colour> <percent>%` for the mix. */
136
+ export function Mark({ letter, tint, sx }) {
137
+ return (_jsx("div", { "aria-hidden": "true", ...stylex.props(styles.mark, styles.markTint(tint), sx), children: letter }));
138
+ }
139
+ export function Status({ children, warn = false, sx }) {
140
+ return (_jsxs("span", { ...stylex.props(styles.status, warn && styles.warn, sx), children: [warn && _jsx("span", { ...stylex.props(styles.dot), "aria-hidden": "true" }), _jsx("span", { ...stylex.props(styles.statusText), children: children })] }));
141
+ }
142
+ export function Sep() {
143
+ return (_jsx("span", { ...stylex.props(styles.sep), "aria-hidden": "true", children: "\u00B7" }));
144
+ }
145
+ export function Row({ mark, name, children, actions, onPress, chevron = false, sx }) {
146
+ const inner = (_jsxs(_Fragment, { children: [mark, _jsx("span", { ...stylex.props(styles.name), children: name }), children !== undefined && _jsx(Sep, {}), children] }));
147
+ return (_jsxs("div", { ...stylex.props(styles.row, sx), children: [onPress ? (_jsx("button", { type: "button", onClick: onPress, ...stylex.props(styles.rowgo), children: inner })) : (_jsx("div", { ...stylex.props(styles.rowgo, styles.still), children: inner })), _jsxs("div", { ...stylex.props(styles.act), children: [actions, chevron && (_jsx(Chevron, { direction: "right", ...stylex.props(icon.sm, styles.chevron), "aria-hidden": "true" }))] })] }));
148
+ }
149
+ /** `.exp`: what a row opens, unfolding under it in 200ms and taking no room when shut. */
150
+ export function Expand({ open, plain = false, children, sx }) {
151
+ return (_jsx("div", { ...(open ? {} : { inert: true }), ...stylex.props(styles.expand, open && styles.shown, sx), children: _jsx("div", { ...stylex.props(styles.clip), children: _jsx("div", { ...stylex.props(styles.in, plain && styles.plain), children: open ? children : null }) }) }));
152
+ }
153
+ /** A sentence inside an `Expand`: what it means, or what went wrong. */
154
+ export function Note({ children, faint = false, err = false, sx }) {
155
+ return (_jsx("p", { role: err ? "alert" : undefined, ...stylex.props(styles.text, faint && styles.faint, err && styles.err, sx), children: children }));
156
+ }
157
+ export function Acts({ children, sx }) {
158
+ return _jsx("div", { ...stylex.props(styles.acts, sx), children: children });
159
+ }
160
+ /** `.empty`: nothing here yet, said inside the group in one muted line. */
161
+ export function Empty({ children, sx }) {
162
+ return _jsx("p", { ...stylex.props(styles.empty, sx), children: children });
163
+ }
164
+ export function Tag({ children, sx }) {
165
+ return _jsx("span", { ...stylex.props(styles.tag, sx), children: children });
166
+ }
@@ -1,13 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import * as stylex from "@stylexjs/stylex";
3
- import { useCallback, useId, useState } from "react";
4
- import { KNOT_LEAD, KNOT_LOOP } from "../../lib/paths.js";
5
- import { SilkBody } from "../../lib/silk.js";
6
- import { color, font, motion, radius, space, text, yarnSecondary } from "../../tokens.stylex.js";
3
+ import { useId, useState } from "react";
4
+ import { color, corner, font, motion, space, text } from "../../tokens.stylex.js";
7
5
  const REDUCED = "@media (prefers-reduced-motion: reduce)";
8
- // 卡面 = 淺色布(TEXTURE-GUIDE §3、handoff-receipt/NOTES.md):沒有 background,
9
- // 實心由紗織成;動態取「僅展示面」子集 — hover 帶動 + 光澤帶,無窩、無掃光。
10
- // 布是固定高度 480 的長布,展開只是露出同一塊布更多 → 高度變化零重織、織紋恆定。
11
6
  const styles = stylex.create({
12
7
  receipt: {
13
8
  position: "relative",
@@ -15,12 +10,12 @@ const styles = stylex.create({
15
10
  borderWidth: 1,
16
11
  borderStyle: "solid",
17
12
  borderColor: color.border,
18
- borderRadius: radius.md,
13
+ borderRadius: corner.md,
14
+ backgroundColor: color.surfaceRaised,
19
15
  },
20
- face: { position: "absolute", top: 0, left: 0, pointerEvents: "none" },
21
16
  row: {
22
17
  // 注意:StyleX 0.19 會靜默丟掉 `all: unset`(編不出任何規則),原生按鈕的
23
- // buttonface 底色與 outset 邊框會蓋住布 —— 要逐項重設
18
+ // buttonface 底色與 outset 邊框會留在畫面上 —— 要逐項重設
24
19
  appearance: "none",
25
20
  backgroundColor: "transparent",
26
21
  borderWidth: 0,
@@ -36,7 +31,7 @@ const styles = stylex.create({
36
31
  fontFamily: font.body,
37
32
  fontSize: text.xs,
38
33
  color: { default: color.textMuted, ":hover": color.text },
39
- borderRadius: radius.md,
34
+ borderRadius: corner.md,
40
35
  outline: { default: "none", ":focus-visible": `2px solid ${color.focusRing}` },
41
36
  outlineOffset: -2,
42
37
  },
@@ -52,23 +47,13 @@ const styles = stylex.create({
52
47
  ruleOpen: { borderTopStyle: "solid", borderTopColor: color.accent },
53
48
  label: { whiteSpace: "nowrap", display: "flex", alignItems: "center", gap: space.xxs },
54
49
  mono: { fontFamily: font.mono, fontSize: "0.76rem", lineHeight: 1 },
55
- knot: { flex: "none", overflow: "visible" },
56
- lead: {
57
- stroke: color.textFaint,
58
- transitionProperty: "opacity",
50
+ link: {
51
+ flex: "none",
52
+ color: color.textFaint,
53
+ transitionProperty: "color",
59
54
  transitionDuration: { default: motion.normal, [REDUCED]: "0s" },
60
55
  },
61
- leadHidden: { opacity: 0 },
62
- loop: {
63
- stroke: color.accent,
64
- strokeDasharray: 100,
65
- strokeDashoffset: 100,
66
- transitionProperty: "stroke-dashoffset",
67
- transitionDuration: { default: "500ms", [REDUCED]: "0s" },
68
- transitionTimingFunction: "ease",
69
- transitionDelay: { default: "120ms", [REDUCED]: "0s" },
70
- },
71
- loopTied: { strokeDashoffset: 0 },
56
+ linkOpen: { color: color.accent },
72
57
  chevron: {
73
58
  flex: "none",
74
59
  color: color.textFaint,
@@ -128,20 +113,5 @@ function CheckIcon() {
128
113
  export function HandoffReceipt({ at, ctxPercent, reason = "soft-threshold", memory, ledgerCount, handoffSummary, defaultOpen = false, }) {
129
114
  const bodyId = useId();
130
115
  const [open, setOpen] = useState(defaultOpen);
131
- // ref callback + cleanup(React 19)管動態的生命週期,不用 effect;client-only
132
- const face = useCallback((node) => {
133
- if (!node)
134
- return;
135
- const silk = new SilkBody(node.parentElement, node, {
136
- palette: yarnSecondary,
137
- mode: "hover",
138
- bandMax: 0.5,
139
- pitch: 2,
140
- hiCount: 12,
141
- hiSpan: 0.95,
142
- fixedHeight: 480,
143
- });
144
- return () => silk.destroy();
145
- }, []);
146
- return (_jsxs("div", { ...stylex.props(styles.receipt), children: [_jsx("svg", { ref: face, "aria-hidden": "true", ...stylex.props(styles.face) }), _jsxs("button", { type: "button", "aria-expanded": open, "aria-controls": bodyId, onClick: () => setOpen((value) => !value), ...stylex.props(styles.row), children: [_jsx("span", { "aria-hidden": "true", ...stylex.props(styles.rule, open && styles.ruleOpen) }), _jsxs("span", { ...stylex.props(styles.label), children: [_jsxs("svg", { width: "15", height: "13", viewBox: "0 0 15 13", fill: "none", strokeWidth: "1.7", strokeLinecap: "round", "aria-hidden": "true", ...stylex.props(styles.knot), children: [_jsx("path", { d: KNOT_LEAD, ...stylex.props(styles.lead, open && styles.leadHidden) }), _jsx("path", { d: KNOT_LOOP, pathLength: "100", ...stylex.props(styles.loop, open && styles.loopTied) })] }), _jsxs("span", { children: ["換班完成 · ", _jsx("span", { ...stylex.props(styles.mono), children: at }), " · ", _jsx("span", { ...stylex.props(styles.mono), children: `ctx ${ctxPercent}%` }), REASON_LABEL[reason], " → 新 session"] })] }), _jsx("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", "aria-hidden": "true", ...stylex.props(styles.chevron, open && styles.chevronOpen), children: _jsx("path", { d: "m6 9 6 6 6-6" }) }), _jsx("span", { "aria-hidden": "true", ...stylex.props(styles.rule, open && styles.ruleOpen) })] }), _jsx("div", { ...stylex.props(styles.bodyWrap, open && styles.bodyWrapOpen), children: _jsx("div", { ...stylex.props(styles.bodyClip), children: _jsxs("div", { id: bodyId, inert: !open, ...stylex.props(styles.body, open && styles.bodyOpen), children: [_jsxs("p", { ...stylex.props(styles.check), children: [_jsx(CheckIcon, {}), _jsx("b", { ...stylex.props(styles.checkTitle), children: "\u8A18\u61B6" }), _jsxs("span", { ...stylex.props(styles.checkText), children: [`${memory.count} 筆耐久事實已落盤`, memory.items?.length ? `(${memory.items.join("、")})` : "", "\u3002"] })] }), _jsxs("p", { ...stylex.props(styles.check), children: [_jsx(CheckIcon, {}), _jsx("b", { ...stylex.props(styles.checkTitle), children: "\u6458\u8981" }), _jsx("span", { ...stylex.props(styles.checkText), children: "handoff \u5DF2\u4EA4\u7D66\u4E0B\u4E00\u8F2A,\u8B80\u5F8C\u5373\u92B7\u6BC0\u3002" })] }), _jsxs("p", { ...stylex.props(styles.check), children: [_jsx(CheckIcon, {}), _jsx("b", { ...stylex.props(styles.checkTitle), children: "Ledger" }), _jsx("span", { ...stylex.props(styles.checkText), children: `本輪 ${ledgerCount} 條收據可查,不進新 context。` })] }), handoffSummary != null && _jsx("p", { ...stylex.props(styles.summary), children: handoffSummary })] }) }) })] }));
116
+ return (_jsxs("div", { ...stylex.props(styles.receipt), children: [_jsxs("button", { type: "button", "aria-expanded": open, "aria-controls": bodyId, onClick: () => setOpen((value) => !value), ...stylex.props(styles.row), children: [_jsx("span", { "aria-hidden": "true", ...stylex.props(styles.rule, open && styles.ruleOpen) }), _jsxs("span", { ...stylex.props(styles.label), children: [_jsx("svg", { width: "13", height: "13", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", "aria-hidden": "true", ...stylex.props(styles.link, open && styles.linkOpen), children: _jsx("path", { d: "M9 17H7A5 5 0 0 1 7 7h2M15 7h2a5 5 0 0 1 0 10h-2M8 12h8" }) }), _jsxs("span", { children: ["換班完成 · ", _jsx("span", { ...stylex.props(styles.mono), children: at }), " · ", _jsx("span", { ...stylex.props(styles.mono), children: `ctx ${ctxPercent}%` }), REASON_LABEL[reason], " → 新 session"] })] }), _jsx("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", "aria-hidden": "true", ...stylex.props(styles.chevron, open && styles.chevronOpen), children: _jsx("path", { d: "m6 9 6 6 6-6" }) }), _jsx("span", { "aria-hidden": "true", ...stylex.props(styles.rule, open && styles.ruleOpen) })] }), _jsx("div", { ...stylex.props(styles.bodyWrap, open && styles.bodyWrapOpen), children: _jsx("div", { ...stylex.props(styles.bodyClip), children: _jsxs("div", { id: bodyId, inert: !open, ...stylex.props(styles.body, open && styles.bodyOpen), children: [_jsxs("p", { ...stylex.props(styles.check), children: [_jsx(CheckIcon, {}), _jsx("b", { ...stylex.props(styles.checkTitle), children: "\u8A18\u61B6" }), _jsxs("span", { ...stylex.props(styles.checkText), children: [`${memory.count} 筆耐久事實已落盤`, memory.items?.length ? `(${memory.items.join("、")})` : "", "\u3002"] })] }), _jsxs("p", { ...stylex.props(styles.check), children: [_jsx(CheckIcon, {}), _jsx("b", { ...stylex.props(styles.checkTitle), children: "\u6458\u8981" }), _jsx("span", { ...stylex.props(styles.checkText), children: "handoff \u5DF2\u4EA4\u7D66\u4E0B\u4E00\u8F2A,\u8B80\u5F8C\u5373\u92B7\u6BC0\u3002" })] }), _jsxs("p", { ...stylex.props(styles.check), children: [_jsx(CheckIcon, {}), _jsx("b", { ...stylex.props(styles.checkTitle), children: "Ledger" }), _jsx("span", { ...stylex.props(styles.checkText), children: `本輪 ${ledgerCount} 條收據可查,不進新 context。` })] }), handoffSummary != null && _jsx("p", { ...stylex.props(styles.summary), children: handoffSummary })] }) }) })] }));
147
117
  }