@anyknown/ui 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +121 -0
  3. package/dist/components/action-bar/ActionBar.d.ts +28 -0
  4. package/dist/components/action-bar/ActionBar.js +73 -0
  5. package/dist/components/badge/Badge.d.ts +28 -0
  6. package/dist/components/badge/Badge.js +79 -0
  7. package/dist/components/button/Button.d.ts +7 -0
  8. package/dist/components/button/Button.js +86 -0
  9. package/dist/components/card/Card.d.ts +2 -0
  10. package/dist/components/card/Card.js +17 -0
  11. package/dist/components/checkbox/Checkbox.d.ts +8 -0
  12. package/dist/components/checkbox/Checkbox.js +102 -0
  13. package/dist/components/code-block/CodeBlock.d.ts +10 -0
  14. package/dist/components/code-block/CodeBlock.js +103 -0
  15. package/dist/components/composer/Composer.d.ts +27 -0
  16. package/dist/components/composer/Composer.js +278 -0
  17. package/dist/components/data-table/DataTable.d.ts +38 -0
  18. package/dist/components/data-table/DataTable.js +256 -0
  19. package/dist/components/dialog/Dialog.d.ts +36 -0
  20. package/dist/components/dialog/Dialog.js +77 -0
  21. package/dist/components/diff-viewer/DiffViewer.d.ts +13 -0
  22. package/dist/components/diff-viewer/DiffViewer.js +133 -0
  23. package/dist/components/dropdown/DropdownMenu.d.ts +43 -0
  24. package/dist/components/dropdown/DropdownMenu.js +86 -0
  25. package/dist/components/dropzone/Dropzone.d.ts +32 -0
  26. package/dist/components/dropzone/Dropzone.js +242 -0
  27. package/dist/components/empty-state/EmptyState.d.ts +9 -0
  28. package/dist/components/empty-state/EmptyState.js +41 -0
  29. package/dist/components/file-row/FileRow.d.ts +32 -0
  30. package/dist/components/file-row/FileRow.js +153 -0
  31. package/dist/components/handoff-receipt/HandoffReceipt.d.ts +15 -0
  32. package/dist/components/handoff-receipt/HandoffReceipt.js +147 -0
  33. package/dist/components/input/Input.d.ts +48 -0
  34. package/dist/components/input/Input.js +64 -0
  35. package/dist/components/interaction-card/InteractionCard.d.ts +59 -0
  36. package/dist/components/interaction-card/InteractionCard.js +238 -0
  37. package/dist/components/kbd/Kbd.d.ts +9 -0
  38. package/dist/components/kbd/Kbd.js +43 -0
  39. package/dist/components/label/Field.d.ts +11 -0
  40. package/dist/components/label/Field.js +28 -0
  41. package/dist/components/label/Label.d.ts +7 -0
  42. package/dist/components/label/Label.js +21 -0
  43. package/dist/components/label/fieldContext.d.ts +26 -0
  44. package/dist/components/label/fieldContext.js +16 -0
  45. package/dist/components/message/Message.d.ts +29 -0
  46. package/dist/components/message/Message.js +94 -0
  47. package/dist/components/password-input/PasswordInput.d.ts +20 -0
  48. package/dist/components/password-input/PasswordInput.js +141 -0
  49. package/dist/components/popover/Popover.d.ts +38 -0
  50. package/dist/components/popover/Popover.js +37 -0
  51. package/dist/components/progress/Progress.d.ts +36 -0
  52. package/dist/components/progress/Progress.js +200 -0
  53. package/dist/components/radio/Radio.d.ts +7 -0
  54. package/dist/components/radio/Radio.js +89 -0
  55. package/dist/components/radio/RadioGroup.d.ts +21 -0
  56. package/dist/components/radio/RadioGroup.js +34 -0
  57. package/dist/components/reasoning-fold/ReasoningFold.d.ts +10 -0
  58. package/dist/components/reasoning-fold/ReasoningFold.js +92 -0
  59. package/dist/components/recovery-key/RecoveryKey.d.ts +17 -0
  60. package/dist/components/recovery-key/RecoveryKey.js +132 -0
  61. package/dist/components/select/Select.d.ts +31 -0
  62. package/dist/components/select/Select.js +209 -0
  63. package/dist/components/skeleton/Skeleton.d.ts +20 -0
  64. package/dist/components/skeleton/Skeleton.js +57 -0
  65. package/dist/components/switch/Switch.d.ts +7 -0
  66. package/dist/components/switch/Switch.js +108 -0
  67. package/dist/components/tabs/Tabs.d.ts +28 -0
  68. package/dist/components/tabs/Tabs.js +133 -0
  69. package/dist/components/text/Text.d.ts +7 -0
  70. package/dist/components/text/Text.js +31 -0
  71. package/dist/components/textarea/Textarea.d.ts +7 -0
  72. package/dist/components/textarea/Textarea.js +25 -0
  73. package/dist/components/toast/Toast.d.ts +56 -0
  74. package/dist/components/toast/Toast.js +175 -0
  75. package/dist/components/tool-card/ToolCard.d.ts +55 -0
  76. package/dist/components/tool-card/ToolCard.js +281 -0
  77. package/dist/components/tooltip/Tooltip.d.ts +12 -0
  78. package/dist/components/tooltip/Tooltip.js +38 -0
  79. package/dist/components/voice-indicator/VoiceIndicator.d.ts +7 -0
  80. package/dist/components/voice-indicator/VoiceIndicator.js +51 -0
  81. package/dist/index.d.ts +55 -0
  82. package/dist/index.js +38 -0
  83. package/dist/lib/diff.d.ts +28 -0
  84. package/dist/lib/diff.js +133 -0
  85. package/dist/lib/format.d.ts +2 -0
  86. package/dist/lib/format.js +24 -0
  87. package/dist/lib/format.test.d.ts +1 -0
  88. package/dist/lib/format.test.js +37 -0
  89. package/dist/lib/mergeRefs.d.ts +2 -0
  90. package/dist/lib/mergeRefs.js +8 -0
  91. package/dist/lib/motion.d.ts +1 -0
  92. package/dist/lib/motion.js +10 -0
  93. package/dist/lib/paths.d.ts +3 -0
  94. package/dist/lib/paths.js +11 -0
  95. package/dist/lib/popup.d.ts +29 -0
  96. package/dist/lib/popup.js +27 -0
  97. package/dist/lib/progress.d.ts +2 -0
  98. package/dist/lib/progress.js +28 -0
  99. package/dist/lib/silk.d.ts +66 -0
  100. package/dist/lib/silk.js +337 -0
  101. package/dist/lib/styled.d.ts +29 -0
  102. package/dist/lib/styled.js +30 -0
  103. package/dist/lib/svgId.d.ts +1 -0
  104. package/dist/lib/svgId.js +4 -0
  105. package/dist/lib/useAnimationFrame.d.ts +1 -0
  106. package/dist/lib/useAnimationFrame.js +21 -0
  107. package/dist/lib/useControllableState.d.ts +1 -0
  108. package/dist/lib/useControllableState.js +14 -0
  109. package/dist/lib/useCopy.d.ts +4 -0
  110. package/dist/lib/useCopy.js +19 -0
  111. package/dist/lib/voice.d.ts +2 -0
  112. package/dist/lib/voice.js +30 -0
  113. package/dist/lib/weave.d.ts +28 -0
  114. package/dist/lib/weave.js +83 -0
  115. package/dist/scrollbar.css +29 -0
  116. package/dist/themes.stylex.d.ts +49 -0
  117. package/dist/themes.stylex.js +52 -0
  118. package/dist/tokens.css +109 -0
  119. package/dist/tokens.stylex.d.ts +126 -0
  120. package/dist/tokens.stylex.js +149 -0
  121. package/package.json +81 -0
@@ -0,0 +1,147 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
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";
7
+ const REDUCED = "@media (prefers-reduced-motion: reduce)";
8
+ // 卡面 = 淺色布(TEXTURE-GUIDE §3、handoff-receipt/NOTES.md):沒有 background,
9
+ // 實心由紗織成;動態取「僅展示面」子集 — hover 帶動 + 光澤帶,無窩、無掃光。
10
+ // 布是固定高度 480 的長布,展開只是露出同一塊布更多 → 高度變化零重織、織紋恆定。
11
+ const styles = stylex.create({
12
+ receipt: {
13
+ position: "relative",
14
+ overflow: "hidden",
15
+ borderWidth: 1,
16
+ borderStyle: "solid",
17
+ borderColor: color.border,
18
+ borderRadius: radius.md,
19
+ },
20
+ face: { position: "absolute", top: 0, left: 0, pointerEvents: "none" },
21
+ row: {
22
+ // 注意:StyleX 0.19 會靜默丟掉 `all: unset`(編不出任何規則),原生按鈕的
23
+ // buttonface 底色與 outset 邊框會蓋住布 —— 要逐項重設
24
+ appearance: "none",
25
+ backgroundColor: "transparent",
26
+ borderWidth: 0,
27
+ position: "relative",
28
+ display: "flex",
29
+ alignItems: "center",
30
+ gap: space.xs,
31
+ width: "100%",
32
+ boxSizing: "border-box",
33
+ paddingBlock: space.xs,
34
+ paddingInline: space.sm,
35
+ cursor: "pointer",
36
+ fontFamily: font.body,
37
+ fontSize: text.xs,
38
+ color: { default: color.textMuted, ":hover": color.text },
39
+ borderRadius: radius.md,
40
+ outline: { default: "none", ":focus-visible": `2px solid ${color.focusRing}` },
41
+ outlineOffset: -2,
42
+ },
43
+ rule: {
44
+ flex: 1,
45
+ borderTopWidth: 1,
46
+ borderTopStyle: "dashed",
47
+ borderTopColor: color.borderStrong,
48
+ minWidth: space.md,
49
+ transitionProperty: "border-color",
50
+ transitionDuration: { default: "300ms", [REDUCED]: "0s" },
51
+ },
52
+ ruleOpen: { borderTopStyle: "solid", borderTopColor: color.accent },
53
+ label: { whiteSpace: "nowrap", display: "flex", alignItems: "center", gap: space.xxs },
54
+ mono: { fontFamily: font.mono, fontSize: "0.76rem", lineHeight: 1 },
55
+ knot: { flex: "none", overflow: "visible" },
56
+ lead: {
57
+ stroke: color.textFaint,
58
+ transitionProperty: "opacity",
59
+ transitionDuration: { default: motion.normal, [REDUCED]: "0s" },
60
+ },
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 },
72
+ chevron: {
73
+ flex: "none",
74
+ color: color.textFaint,
75
+ transitionProperty: "rotate",
76
+ transitionDuration: { default: "160ms", [REDUCED]: "0s" },
77
+ transitionTimingFunction: "ease",
78
+ },
79
+ chevronOpen: { rotate: "180deg" },
80
+ // 展開 = 0fr → 1fr(雙向平滑,不是 display 硬切);內容 opacity 跟進
81
+ bodyWrap: {
82
+ position: "relative",
83
+ display: "grid",
84
+ gridTemplateRows: "0fr",
85
+ transitionProperty: "grid-template-rows",
86
+ transitionDuration: { default: "240ms", [REDUCED]: "0s" },
87
+ transitionTimingFunction: "cubic-bezier(0.16, 1, 0.3, 1)",
88
+ },
89
+ bodyWrapOpen: { gridTemplateRows: "1fr" },
90
+ bodyClip: { overflow: "hidden", minHeight: 0 },
91
+ body: {
92
+ borderTopWidth: 1,
93
+ borderTopStyle: "solid",
94
+ borderTopColor: color.border,
95
+ paddingBlock: space.xs,
96
+ paddingInline: space.sm,
97
+ fontFamily: font.body,
98
+ fontSize: text.xs,
99
+ display: "grid",
100
+ gap: space.xxs,
101
+ opacity: 0,
102
+ transitionProperty: "opacity",
103
+ transitionDuration: { default: "200ms", [REDUCED]: "0s" },
104
+ transitionTimingFunction: "ease-out",
105
+ },
106
+ bodyOpen: { opacity: 1 },
107
+ check: { display: "flex", alignItems: "baseline", gap: space.xxs, margin: 0 },
108
+ checkIcon: { flex: "none", color: color.accent, translate: "0 2px" },
109
+ checkTitle: { fontWeight: 500, whiteSpace: "nowrap", color: color.text },
110
+ checkText: { color: color.textMuted },
111
+ summary: {
112
+ margin: 0,
113
+ color: color.textMuted,
114
+ borderInlineStartWidth: 2,
115
+ borderInlineStartStyle: "solid",
116
+ borderInlineStartColor: color.border,
117
+ paddingInlineStart: space.xs,
118
+ },
119
+ });
120
+ const REASON_LABEL = {
121
+ "soft-threshold": "",
122
+ "hard-limit": "(硬上限)",
123
+ "state-transition": "(狀態切換)",
124
+ };
125
+ function CheckIcon() {
126
+ return (_jsx("svg", { width: "13", height: "13", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", "aria-hidden": "true", ...stylex.props(styles.checkIcon), children: _jsx("path", { d: "m5 13 4 4L19 7" }) }));
127
+ }
128
+ export function HandoffReceipt({ at, ctxPercent, reason = "soft-threshold", memory, ledgerCount, handoffSummary, defaultOpen = false, }) {
129
+ const bodyId = useId();
130
+ 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 })] }) }) })] }));
147
+ }
@@ -0,0 +1,48 @@
1
+ import * as stylex from "@stylexjs/stylex";
2
+ import type { ComponentProps, ReactNode } from "react";
3
+ export declare const controlStyles: Readonly<{
4
+ readonly base: Readonly<{
5
+ readonly width: stylex.StyleXClassNameFor<"width", "100%">;
6
+ readonly boxSizing: stylex.StyleXClassNameFor<"boxSizing", "border-box">;
7
+ readonly backgroundColor: stylex.StyleXClassNameFor<"backgroundColor", string>;
8
+ readonly borderWidth: stylex.StyleXClassNameFor<"borderWidth", 1>;
9
+ readonly borderStyle: stylex.StyleXClassNameFor<"borderStyle", "solid">;
10
+ readonly borderColor: stylex.StyleXClassNameFor<"borderColor", string>;
11
+ readonly borderRadius: stylex.StyleXClassNameFor<"borderRadius", string>;
12
+ readonly color: stylex.StyleXClassNameFor<"color", string>;
13
+ readonly fontFamily: stylex.StyleXClassNameFor<"fontFamily", string>;
14
+ readonly fontSize: stylex.StyleXClassNameFor<"fontSize", string>;
15
+ readonly lineHeight: stylex.StyleXClassNameFor<"lineHeight", string>;
16
+ readonly transitionProperty: stylex.StyleXClassNameFor<"transitionProperty", "border-color">;
17
+ readonly transitionDuration: stylex.StyleXClassNameFor<"transitionDuration", string>;
18
+ readonly transitionTimingFunction: stylex.StyleXClassNameFor<"transitionTimingFunction", string>;
19
+ readonly outline: stylex.StyleXClassNameFor<"outline", "none" | `2px solid ${stylex.StyleXVar<string>}`>;
20
+ readonly outlineOffset: stylex.StyleXClassNameFor<"outlineOffset", -1>;
21
+ readonly cursor: stylex.StyleXClassNameFor<"cursor", "auto" | "not-allowed">;
22
+ readonly opacity: stylex.StyleXClassNameFor<"opacity", 1 | 0.5>;
23
+ readonly "::placeholder": stylex.StyleXClassNameFor<"::placeholder", {
24
+ readonly color: stylex.StyleXVar<string>;
25
+ }>;
26
+ }>;
27
+ readonly invalid: Readonly<{
28
+ readonly borderColor: stylex.StyleXClassNameFor<"borderColor", string>;
29
+ readonly outlineColor: stylex.StyleXClassNameFor<"outlineColor", string>;
30
+ }>;
31
+ readonly md: Readonly<{
32
+ readonly minHeight: stylex.StyleXClassNameFor<"minHeight", "2.25rem">;
33
+ readonly paddingBlock: stylex.StyleXClassNameFor<"paddingBlock", string>;
34
+ readonly paddingInline: stylex.StyleXClassNameFor<"paddingInline", string>;
35
+ }>;
36
+ readonly sm: Readonly<{
37
+ readonly minHeight: stylex.StyleXClassNameFor<"minHeight", "1.75rem">;
38
+ readonly paddingBlock: stylex.StyleXClassNameFor<"paddingBlock", string>;
39
+ readonly paddingInline: stylex.StyleXClassNameFor<"paddingInline", string>;
40
+ readonly fontSize: stylex.StyleXClassNameFor<"fontSize", string>;
41
+ }>;
42
+ }>;
43
+ export type InputProps = Omit<ComponentProps<"input">, "size"> & {
44
+ size?: "sm" | "md";
45
+ invalid?: boolean;
46
+ leadingIcon?: ReactNode;
47
+ };
48
+ export declare function Input({ size, invalid, leadingIcon, ...props }: InputProps): import("react").JSX.Element;
@@ -0,0 +1,64 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import * as stylex from "@stylexjs/stylex";
3
+ import { styled } from "../../lib/styled.js";
4
+ import { color, font, motion, radius, space, text } from "../../tokens.stylex.js";
5
+ import { useFieldControl } from "../label/fieldContext.js";
6
+ export const controlStyles = stylex.create({
7
+ base: {
8
+ width: "100%",
9
+ // 沒有 border-box,minHeight 2.25rem 是「內容」36px,再加 padding+border → 54px;
10
+ // 元件不能靠 app 端有沒有 `*{box-sizing:border-box}` reset
11
+ boxSizing: "border-box",
12
+ backgroundColor: color.surface,
13
+ borderWidth: 1,
14
+ borderStyle: "solid",
15
+ borderColor: {
16
+ default: color.border,
17
+ ":hover:not(:disabled)": color.borderStrong,
18
+ ":focus-visible": color.focusRing,
19
+ },
20
+ borderRadius: radius.md,
21
+ color: color.text,
22
+ fontFamily: font.body,
23
+ fontSize: text.sm,
24
+ // 單行控件的行高只要裝得下字:1.5 會把 md 撐到 39px,跟 button/select 的
25
+ // 2.25rem 對不齊(Textarea 自己蓋回 leadingRelaxed,多行照樣好讀)
26
+ lineHeight: text.leadingTight,
27
+ transitionProperty: "border-color",
28
+ transitionDuration: { default: motion.fast, "@media (prefers-reduced-motion: reduce)": "0s" },
29
+ transitionTimingFunction: motion.ease,
30
+ outline: { default: "none", ":focus-visible": `2px solid ${color.focusRing}` },
31
+ outlineOffset: -1,
32
+ cursor: { default: "auto", ":disabled": "not-allowed" },
33
+ opacity: { default: 1, ":disabled": 0.5 },
34
+ "::placeholder": { color: color.textFaint },
35
+ },
36
+ invalid: {
37
+ borderColor: { default: color.danger, ":hover:not(:disabled)": color.danger },
38
+ outlineColor: color.danger,
39
+ },
40
+ md: { minHeight: "2.25rem", paddingBlock: space.xs, paddingInline: space.sm },
41
+ sm: { minHeight: "1.75rem", paddingBlock: space.xxs, paddingInline: space.xs, fontSize: text.xs },
42
+ });
43
+ const styles = stylex.create({
44
+ affix: { position: "relative", display: "block", width: "100%" },
45
+ icon: {
46
+ position: "absolute",
47
+ insetInlineStart: space.xs,
48
+ insetBlockStart: "50%",
49
+ transform: "translateY(-50%)",
50
+ display: "flex",
51
+ color: color.textFaint,
52
+ pointerEvents: "none",
53
+ },
54
+ withIconMd: { paddingInlineStart: space.xl },
55
+ withIconSm: { paddingInlineStart: space.lg },
56
+ });
57
+ export function Input({ size = "md", invalid, leadingIcon, ...props }) {
58
+ const { invalid: fieldInvalid, ...field } = useFieldControl(props);
59
+ const isInvalid = invalid ?? fieldInvalid;
60
+ const input = (_jsx("input", { ...props, ...field, "aria-invalid": isInvalid || undefined, ...styled(props, controlStyles.base, controlStyles[size], isInvalid && controlStyles.invalid, Boolean(leadingIcon) && (size === "sm" ? styles.withIconSm : styles.withIconMd)) }));
61
+ if (!leadingIcon)
62
+ return input;
63
+ return (_jsxs("span", { ...stylex.props(styles.affix), children: [_jsx("span", { "aria-hidden": "true", ...stylex.props(styles.icon), children: leadingIcon }), input] }));
64
+ }
@@ -0,0 +1,59 @@
1
+ import { type ReactNode } from "react";
2
+ export type PermissionReply = "once" | {
3
+ always: string;
4
+ } | {
5
+ reject: true;
6
+ message?: string;
7
+ };
8
+ export type PermissionReceipt = {
9
+ text: string;
10
+ rejected?: boolean;
11
+ };
12
+ export type PermissionCardProps = {
13
+ verb: string;
14
+ subject: string;
15
+ scope?: string;
16
+ policyHint?: ReactNode;
17
+ blockingLabel?: string;
18
+ onReply?: (reply: PermissionReply) => void;
19
+ resolved?: PermissionReceipt;
20
+ };
21
+ export declare function PermissionCard({ verb, subject, scope, policyHint, blockingLabel, onReply, resolved, }: PermissionCardProps): import("react").JSX.Element;
22
+ export type DecisionOption = {
23
+ value: string;
24
+ label: string;
25
+ description?: string;
26
+ recommended?: boolean;
27
+ };
28
+ export type DecisionBlock = {
29
+ kind: "markdown";
30
+ id: string;
31
+ text: string;
32
+ } | {
33
+ kind: "options";
34
+ id: string;
35
+ label?: string;
36
+ multiple?: boolean;
37
+ required?: boolean;
38
+ options: DecisionOption[];
39
+ } | {
40
+ kind: "text";
41
+ id: string;
42
+ label?: string;
43
+ placeholder?: string;
44
+ required?: boolean;
45
+ };
46
+ export type DecisionAnswer = Record<string, string | string[]>;
47
+ export type DecisionCardProps = {
48
+ title: string;
49
+ blocks: DecisionBlock[];
50
+ blocking?: boolean;
51
+ deadlineLabel?: string;
52
+ submitLabel?: string;
53
+ recommendedLabel?: string;
54
+ onAnswer?: (answer: DecisionAnswer) => void;
55
+ resolved?: {
56
+ text: string;
57
+ };
58
+ };
59
+ export declare function DecisionCard({ title, blocks, blocking, deadlineLabel, submitLabel, recommendedLabel, onAnswer, resolved, }: DecisionCardProps): import("react").JSX.Element;
@@ -0,0 +1,238 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import * as stylex from "@stylexjs/stylex";
3
+ import { useId, useState } from "react";
4
+ import { color, font, motion, radius, space, text } from "../../tokens.stylex.js";
5
+ import { Button } from "../button/Button.js";
6
+ import { Checkbox } from "../checkbox/Checkbox.js";
7
+ import { Radio } from "../radio/Radio.js";
8
+ import { RadioGroup } from "../radio/RadioGroup.js";
9
+ const REDUCED = "@media (prefers-reduced-motion: reduce)";
10
+ const styles = stylex.create({
11
+ card: {
12
+ backgroundColor: color.surface,
13
+ borderWidth: 1,
14
+ borderStyle: "solid",
15
+ borderColor: color.border,
16
+ borderRadius: radius.lg,
17
+ paddingBlock: space.sm,
18
+ paddingInline: space.md,
19
+ display: "grid",
20
+ gap: space.xs,
21
+ fontFamily: font.body,
22
+ transitionProperty: "border-color",
23
+ transitionDuration: { default: "180ms", [REDUCED]: "0s" },
24
+ },
25
+ permissionPending: { borderColor: color.warning },
26
+ decisionPending: { borderColor: color.accent },
27
+ head: { display: "flex", alignItems: "center", gap: space.xs },
28
+ headIconWarning: { flex: "none", color: color.warning },
29
+ headIconAccent: { flex: "none", color: color.accent },
30
+ verb: { fontSize: text.sm, fontWeight: 500, lineHeight: text.leadingSnug, color: color.text },
31
+ state: {
32
+ marginInlineStart: "auto",
33
+ fontFamily: font.mono,
34
+ fontSize: "0.68rem",
35
+ fontWeight: 600,
36
+ lineHeight: 1,
37
+ letterSpacing: "0.06em",
38
+ textTransform: "uppercase",
39
+ whiteSpace: "nowrap",
40
+ },
41
+ stateWarning: { color: color.warning },
42
+ stateAccent: { color: color.accent },
43
+ stateQuiet: { color: color.textFaint },
44
+ body: { display: "grid", gap: space.xs },
45
+ mono: {
46
+ fontFamily: font.mono,
47
+ fontSize: "0.82rem",
48
+ lineHeight: text.leadingNormal,
49
+ backgroundColor: color.bg,
50
+ borderWidth: 1,
51
+ borderStyle: "solid",
52
+ borderColor: color.border,
53
+ borderRadius: radius.sm,
54
+ paddingBlock: space.xs,
55
+ paddingInline: space.xs,
56
+ overflowX: "auto",
57
+ whiteSpace: "pre",
58
+ margin: 0,
59
+ color: color.text,
60
+ outline: { default: "none", ":focus-visible": `2px solid ${color.focusRing}` },
61
+ outlineOffset: -2,
62
+ },
63
+ actions: { display: "flex", gap: space.xs, flexWrap: "wrap" },
64
+ shortcut: {
65
+ fontFamily: font.mono,
66
+ fontSize: "0.68rem",
67
+ fontWeight: 500,
68
+ opacity: 0.65,
69
+ marginInlineStart: space.xxs,
70
+ },
71
+ policy: {
72
+ display: "flex",
73
+ alignItems: "center",
74
+ gap: space.xxs,
75
+ fontSize: "0.78rem",
76
+ color: color.textMuted,
77
+ borderTopWidth: 1,
78
+ borderTopStyle: "solid",
79
+ borderTopColor: color.border,
80
+ paddingTop: space.xs,
81
+ margin: 0,
82
+ },
83
+ policyIcon: { flex: "none", color: color.textFaint },
84
+ question: {
85
+ fontSize: "0.95rem",
86
+ fontWeight: 500,
87
+ lineHeight: text.leadingSnug,
88
+ margin: 0,
89
+ color: color.text,
90
+ },
91
+ markdown: { fontSize: text.sm, lineHeight: text.leadingRelaxed, margin: 0, color: color.textMuted },
92
+ options: {
93
+ display: "grid",
94
+ gap: space.xxs,
95
+ margin: 0,
96
+ padding: 0,
97
+ borderWidth: 0,
98
+ listStyle: "none",
99
+ },
100
+ srOnly: {
101
+ position: "absolute",
102
+ width: 1,
103
+ height: 1,
104
+ padding: 0,
105
+ margin: -1,
106
+ overflow: "hidden",
107
+ clipPath: "inset(50%)",
108
+ whiteSpace: "nowrap",
109
+ borderWidth: 0,
110
+ },
111
+ recommended: {
112
+ fontFamily: font.mono,
113
+ fontSize: "0.62rem",
114
+ fontWeight: 600,
115
+ lineHeight: 1,
116
+ letterSpacing: "0.05em",
117
+ textTransform: "uppercase",
118
+ color: color.accent,
119
+ backgroundColor: color.accentSubtle,
120
+ borderRadius: radius.sm,
121
+ paddingBlock: "0.15rem",
122
+ paddingInline: space.xxs,
123
+ marginInlineStart: space.xxs,
124
+ verticalAlign: "middle",
125
+ },
126
+ free: {
127
+ width: "100%",
128
+ boxSizing: "border-box",
129
+ backgroundColor: color.bg,
130
+ borderWidth: 1,
131
+ borderStyle: "solid",
132
+ borderColor: { default: color.border, ":focus-visible": color.focusRing },
133
+ borderRadius: radius.sm,
134
+ color: color.text,
135
+ fontFamily: font.body,
136
+ fontSize: text.sm,
137
+ paddingBlock: space.xs,
138
+ paddingInline: space.xs,
139
+ resize: "vertical",
140
+ minHeight: "2.4rem",
141
+ transitionProperty: "border-color",
142
+ transitionDuration: { default: motion.fast, [REDUCED]: "0s" },
143
+ outline: { default: "none", ":focus-visible": `2px solid ${color.focusRing}` },
144
+ outlineOffset: -1,
145
+ "::placeholder": { color: color.textFaint },
146
+ },
147
+ receipt: {
148
+ display: "flex",
149
+ alignItems: "center",
150
+ gap: space.xs,
151
+ fontSize: text.sm,
152
+ color: color.textMuted,
153
+ margin: 0,
154
+ },
155
+ receiptIcon: { flex: "none", color: color.accent },
156
+ receiptIconRejected: { color: color.danger },
157
+ receiptMono: { fontFamily: font.mono },
158
+ });
159
+ function LockIcon() {
160
+ return (_jsxs("svg", { width: "15", height: "15", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", "aria-hidden": "true", ...stylex.props(styles.headIconWarning), children: [_jsx("rect", { x: "4", y: "10", width: "16", height: "10", rx: "2" }), _jsx("path", { d: "M8 10V7a4 4 0 0 1 8 0v3" })] }));
161
+ }
162
+ function DecideIcon() {
163
+ return (_jsxs("svg", { width: "15", height: "15", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", "aria-hidden": "true", ...stylex.props(styles.headIconAccent), children: [_jsx("path", { d: "M9 18h6M10 21h4" }), _jsx("path", { d: "M12 3a6 6 0 0 0-4 10.5c.7.6 1 1.5 1 2.5h6c0-1 .3-1.9 1-2.5A6 6 0 0 0 12 3Z" })] }));
164
+ }
165
+ function InfoIcon() {
166
+ return (_jsxs("svg", { width: "13", height: "13", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", "aria-hidden": "true", ...stylex.props(styles.policyIcon), children: [_jsx("circle", { cx: "12", cy: "12", r: "9" }), _jsx("path", { d: "M12 8v4m0 4h.01" })] }));
167
+ }
168
+ function ReceiptIcon({ rejected }) {
169
+ return (_jsx("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", "aria-hidden": "true", ...stylex.props(styles.receiptIcon, rejected && styles.receiptIconRejected), children: _jsx("path", { d: "m5 13 4 4L19 7" }) }));
170
+ }
171
+ export function PermissionCard({ verb, subject, scope = "這個指令", policyHint, blockingLabel = "等你才能繼續", onReply, resolved, }) {
172
+ // Plain Enter is left to the focused button's own activation; only the
173
+ // card-level shortcuts are intercepted.
174
+ function onKeyDown(event) {
175
+ if (resolved)
176
+ return;
177
+ if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) {
178
+ event.preventDefault();
179
+ onReply?.({ always: scope });
180
+ }
181
+ else if (event.key === "Escape") {
182
+ event.preventDefault();
183
+ onReply?.({ reject: true });
184
+ }
185
+ }
186
+ return (_jsxs("div", { role: "group", "aria-label": `${verb}:${subject}`, ...stylex.props(styles.card, !resolved && styles.permissionPending), children: [_jsxs("div", { ...stylex.props(styles.head), children: [_jsx(LockIcon, {}), _jsx("span", { ...stylex.props(styles.verb), children: verb }), _jsx("span", { ...stylex.props(styles.state, resolved ? styles.stateQuiet : styles.stateWarning), children: resolved ? "已回覆" : blockingLabel })] }), _jsx("p", { "aria-live": "polite", ...stylex.props(resolved ? styles.receipt : styles.srOnly), children: resolved != null && (_jsxs(_Fragment, { children: [_jsx(ReceiptIcon, { rejected: resolved.rejected }), resolved.text, " · ", _jsx("code", { ...stylex.props(styles.receiptMono), children: subject })] })) }), resolved ? null : (_jsxs("div", { ...stylex.props(styles.body), children: [_jsx("pre", { tabIndex: 0, role: "region", "aria-label": verb, ...stylex.props(styles.mono), children: subject }), _jsxs("div", { ...stylex.props(styles.actions), children: [_jsxs(Button, { onKeyDown: onKeyDown, onClick: () => onReply?.("once"), children: ["\u5141\u8A31\u4E00\u6B21", _jsx("kbd", { "aria-hidden": "true", ...stylex.props(styles.shortcut), children: "\u23CE" })] }), _jsxs(Button, { variant: "secondary", "aria-keyshortcuts": "Meta+Enter Control+Enter", onKeyDown: onKeyDown, onClick: () => onReply?.({ always: scope }), children: ["\u7E3D\u662F\u5141\u8A31", _jsx("kbd", { "aria-hidden": "true", ...stylex.props(styles.shortcut), children: "\u2318\u23CE" })] }), _jsxs(Button, { variant: "dangerGhost", "aria-keyshortcuts": "Escape", onKeyDown: onKeyDown, onClick: () => onReply?.({ reject: true }), children: ["\u62D2\u7D55", _jsx("kbd", { "aria-hidden": "true", ...stylex.props(styles.shortcut), children: "Esc" })] })] }), policyHint != null && (_jsxs("p", { ...stylex.props(styles.policy), children: [_jsx(InfoIcon, {}), policyHint] }))] }))] }));
187
+ }
188
+ function optionLabel(option) {
189
+ if (!option.recommended)
190
+ return option.label;
191
+ return (_jsxs(_Fragment, { children: [option.label, _jsx("span", { ...stylex.props(styles.recommended), children: "\u5EFA\u8B70" })] }));
192
+ }
193
+ export function DecisionCard({ title, blocks, blocking = false, deadlineLabel, submitLabel = "送出決定", recommendedLabel = "照建議", onAnswer, resolved, }) {
194
+ const base = useId();
195
+ const [answer, setAnswer] = useState({});
196
+ const missing = blocks.some((block) => {
197
+ if (block.kind === "markdown" || !block.required)
198
+ return false;
199
+ const value = answer[block.id];
200
+ return value == null || value.length === 0;
201
+ });
202
+ const recommended = {};
203
+ for (const block of blocks) {
204
+ if (block.kind !== "options")
205
+ continue;
206
+ const picks = block.options.filter((option) => option.recommended).map((option) => option.value);
207
+ if (picks.length === 0)
208
+ continue;
209
+ recommended[block.id] = block.multiple ? picks : picks[0];
210
+ }
211
+ const hasRecommendation = Object.keys(recommended).length > 0;
212
+ const merged = { ...answer, ...recommended };
213
+ const recommendationMissing = blocks.some((block) => {
214
+ if (block.kind === "markdown" || !block.required)
215
+ return false;
216
+ const value = merged[block.id];
217
+ return value == null || value.length === 0;
218
+ });
219
+ function set(id, value) {
220
+ setAnswer((current) => ({ ...current, [id]: value }));
221
+ }
222
+ return (_jsxs("div", { ...stylex.props(styles.card, !resolved && blocking && styles.decisionPending), children: [_jsxs("div", { ...stylex.props(styles.head), children: [_jsx(DecideIcon, {}), _jsx("span", { ...stylex.props(styles.verb), children: "\u6C7A\u5B9A" }), _jsx("span", { ...stylex.props(styles.state, resolved ? styles.stateQuiet : blocking ? styles.stateAccent : styles.stateQuiet), children: resolved ? "已決定" : blocking ? "等你才能繼續" : (deadlineLabel ?? "等你") })] }), _jsx("p", { "aria-live": "polite", ...stylex.props(resolved ? styles.receipt : styles.srOnly), children: resolved != null && (_jsxs(_Fragment, { children: [_jsx(ReceiptIcon, {}), resolved.text] })) }), resolved ? null : (_jsxs("div", { ...stylex.props(styles.body), children: [_jsx("p", { ...stylex.props(styles.question), children: title }), blocks.map((block) => {
223
+ if (block.kind === "markdown") {
224
+ return (_jsx("p", { ...stylex.props(styles.markdown), children: block.text }, block.id));
225
+ }
226
+ if (block.kind === "text") {
227
+ return (_jsx("textarea", { rows: 1, "aria-label": block.label ?? title, placeholder: block.placeholder, required: block.required, value: answer[block.id] ?? "", onChange: (event) => set(block.id, event.currentTarget.value), ...stylex.props(styles.free) }, block.id));
228
+ }
229
+ const selected = answer[block.id];
230
+ // label 沒給時,組名走 aria-label(視覺上不重複卡片標題)
231
+ const groupLabel = block.label == null ? title : undefined;
232
+ if (block.multiple) {
233
+ const picks = Array.isArray(selected) ? selected : [];
234
+ return (_jsxs("fieldset", { "aria-label": groupLabel, "aria-required": block.required || undefined, ...stylex.props(styles.options), children: [block.label != null && _jsx("legend", { ...stylex.props(styles.question), children: block.label }), block.options.map((option) => (_jsx(Checkbox, { name: `${base}${block.id}`, value: option.value, checked: picks.includes(option.value), onCheckedChange: (on) => set(block.id, on ? [...picks, option.value] : picks.filter((v) => v !== option.value)), label: optionLabel(option), description: option.description }, option.value)))] }, block.id));
235
+ }
236
+ return (_jsx(RadioGroup, { name: `${base}${block.id}`, legend: block.label, "aria-label": groupLabel, "aria-required": block.required || undefined, variant: "card", value: typeof selected === "string" ? selected : "", onValueChange: (value) => set(block.id, value), children: block.options.map((option) => (_jsx(Radio, { value: option.value, label: optionLabel(option), description: option.description }, option.value))) }, block.id));
237
+ }), _jsxs("div", { ...stylex.props(styles.actions), children: [_jsx(Button, { disabled: missing, onClick: () => onAnswer?.(answer), children: submitLabel }), hasRecommendation && (_jsx(Button, { variant: "secondary", disabled: recommendationMissing, onClick: () => onAnswer?.(merged), children: recommendedLabel }))] })] }))] }));
238
+ }
@@ -0,0 +1,9 @@
1
+ import { type ComponentProps, type ReactNode } from "react";
2
+ export declare const KbdToneContext: import("react").Context<"default" | "inverted">;
3
+ export type KbdProps = ComponentProps<"kbd">;
4
+ export declare function Kbd(props: KbdProps): import("react").JSX.Element;
5
+ export type KbdGroupProps = Omit<ComponentProps<"span">, "children"> & {
6
+ keys: string[];
7
+ separator?: ReactNode;
8
+ };
9
+ export declare function KbdGroup({ keys, separator, ...props }: KbdGroupProps): import("react").JSX.Element;
@@ -0,0 +1,43 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import * as stylex from "@stylexjs/stylex";
3
+ import { Fragment, createContext, useContext } from "react";
4
+ import { styled } from "../../lib/styled.js";
5
+ import { color, font, radius, space } from "../../tokens.stylex.js";
6
+ export const KbdToneContext = createContext("default");
7
+ const styles = stylex.create({
8
+ key: {
9
+ display: "inline-grid",
10
+ placeItems: "center",
11
+ minWidth: "1.4rem",
12
+ height: "1.4rem",
13
+ paddingInline: space.xxs,
14
+ backgroundColor: color.surface,
15
+ borderWidth: 1,
16
+ borderStyle: "solid",
17
+ borderColor: color.border,
18
+ borderBottomColor: color.borderStrong,
19
+ borderRadius: radius.sm,
20
+ boxShadow: `0 1px 0 ${color.borderStrong}`,
21
+ fontFamily: font.mono,
22
+ fontSize: "0.72rem",
23
+ fontWeight: 500,
24
+ lineHeight: 1,
25
+ color: color.textMuted,
26
+ },
27
+ inverted: {
28
+ backgroundColor: "color-mix(in srgb, currentColor 16%, transparent)",
29
+ borderColor: "color-mix(in srgb, currentColor 28%, transparent)",
30
+ borderBottomColor: "color-mix(in srgb, currentColor 28%, transparent)",
31
+ boxShadow: "none",
32
+ color: "inherit",
33
+ },
34
+ combo: { display: "inline-flex", gap: "0.2rem", alignItems: "center" },
35
+ sequence: { display: "inline-flex", gap: space.xxs, alignItems: "center", color: color.textFaint },
36
+ });
37
+ export function Kbd(props) {
38
+ const tone = useContext(KbdToneContext);
39
+ return _jsx("kbd", { ...props, ...styled(props, styles.key, tone === "inverted" && styles.inverted) });
40
+ }
41
+ export function KbdGroup({ keys, separator, ...props }) {
42
+ return (_jsx("span", { ...props, ...styled(props, separator == null ? styles.combo : styles.sequence), children: keys.map((key, index) => (_jsxs(Fragment, { children: [index > 0 && separator, _jsx(Kbd, { children: key })] }, `${key}-${index}`))) }));
43
+ }
@@ -0,0 +1,11 @@
1
+ import { type ComponentProps, type ReactNode } from "react";
2
+ export type FieldProps = Omit<ComponentProps<"div">, "children"> & {
3
+ label?: ReactNode;
4
+ help?: ReactNode;
5
+ error?: ReactNode;
6
+ required?: boolean;
7
+ optional?: boolean;
8
+ disabled?: boolean;
9
+ children: ReactNode;
10
+ };
11
+ export declare function Field({ label, help, error, required, optional, disabled, children, ...props }: FieldProps): import("react").JSX.Element;