@keyfunc/keybricks-ds 2.4.7 → 2.4.10

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 (69) hide show
  1. package/DEPLOY.md +9 -6
  2. package/DESIGN.md +4 -4
  3. package/INTEGRATION.md +27 -2
  4. package/_adherence.oxlintrc.json +4 -4
  5. package/assets/fonts/inter/InterVariable-Italic.woff2 +0 -0
  6. package/assets/fonts/inter/InterVariable.woff2 +0 -0
  7. package/assets/fonts/inter/LICENSE.txt +92 -0
  8. package/assets/fonts/inter/README.md +13 -0
  9. package/assets/icons/icon-index.js +1 -1
  10. package/assets/icons/icon-index.json +1 -1
  11. package/changelog.js +91 -0
  12. package/components/_cards/segmented-control.card.html +1 -1
  13. package/components/_cards/table.card.html +139 -6
  14. package/components/_cards/toggles.card.html +27 -27
  15. package/components/_shared/src-data.js +1 -1
  16. package/components/_shared/src-display.js +1 -1
  17. package/components/_shared/src-forms.js +1 -1
  18. package/components/_shared/src-icons.js +1 -1
  19. package/components/_shared/src-navigation.js +1 -1
  20. package/components/_shared/src-tabs.js +1 -1
  21. package/components/data/SmartTable.d.ts +9 -1
  22. package/components/data/SmartTable.jsx +24 -4
  23. package/components/data/SmartTable.vue +18 -5
  24. package/components/data/Spreadsheet.d.ts +5 -1
  25. package/components/data/Spreadsheet.jsx +23 -3
  26. package/components/data/Spreadsheet.vue +18 -5
  27. package/components/data/Table.d.ts +25 -4
  28. package/components/data/Table.jsx +74 -20
  29. package/components/data/Table.vue +56 -14
  30. package/components/display/Tag.d.ts +1 -1
  31. package/components/display/Tag.jsx +1 -1
  32. package/components/display/Tag.vue +1 -1
  33. package/components/forms/Switch.d.ts +2 -2
  34. package/components/forms/Switch.jsx +10 -8
  35. package/components/forms/Switch.vue +5 -4
  36. package/components/forms/forms.prompt.md +1 -1
  37. package/components/icons/Icon.jsx +2 -1
  38. package/components/icons/icon-paths.js +2 -1
  39. package/components/navigation/Breadcrumb.jsx +9 -7
  40. package/components/navigation/Breadcrumb.vue +16 -14
  41. package/components/tabs/CountBadge.vue +1 -1
  42. package/components/tabs/Tabs.d.ts +1 -1
  43. package/components/tabs/Tabs.jsx +1 -1
  44. package/covers.js +104 -0
  45. package/gallery.html +133 -0
  46. package/guidelines/_color-lab.js +46 -0
  47. package/guidelines/brand-logo.html +9 -0
  48. package/guidelines/colors-list.html +86 -0
  49. package/guidelines/colors-palette.html +85 -0
  50. package/guidelines/colors-schemes.html +68 -0
  51. package/guidelines/colors-transparency.html +59 -0
  52. package/guidelines/foundations-borders.html +10 -0
  53. package/guidelines/foundations-chrome.html +17 -0
  54. package/guidelines/foundations-motion.html +17 -0
  55. package/guidelines/foundations-radii.html +14 -0
  56. package/guidelines/foundations-size-tiers.html +88 -0
  57. package/guidelines/foundations-spacing.html +19 -0
  58. package/guidelines/interaction-states.html +25 -0
  59. package/guidelines/type-extended.html +11 -0
  60. package/guidelines/type-mono.html +10 -0
  61. package/guidelines/type-scale.html +15 -0
  62. package/guidelines/type-weights.html +12 -0
  63. package/index.html +597 -0
  64. package/package.json +8 -2
  65. package/readme.md +1 -1
  66. package/readme.zh-CN.md +1 -1
  67. package/site-nav.js +1 -0
  68. package/tokens/fonts.css +19 -4
  69. package/tokens/layout.css +5 -5
@@ -1,4 +1,4 @@
1
- <!-- @dsCard group="Components" viewport="700x6150" name="Table" subtitle="Anatomy · densities · column types · two-line name · sorting · selection · frames · column resize · states · composition" -->
1
+ <!-- @dsCard group="Components" viewport="700x9720" name="Table" subtitle="Anatomy · densities · cell types (display · editable · per tier) · two-line name · sorting · selection · frames · column resize · states · composition" -->
2
2
  <!DOCTYPE html>
3
3
  <html><head><meta charset="utf-8"><script src="../_shared/frame-theme.js"></script><link rel="stylesheet" href="../../styles.css"><link rel="stylesheet" href="../../tokens/hover.css"><link rel="stylesheet" href="../_shared/card-kit.css">
4
4
  <script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
@@ -10,7 +10,7 @@
10
10
  <style>body{margin:0;background:var(--bg-surface);padding:20px 24px;font-family:var(--font-sans);box-sizing:border-box}.sec{font-size:var(--text-xs);font-weight:600;color:var(--text-muted);padding:20px 0 4px;border-top:1px solid var(--border-subtle);margin-top:18px}.sec:first-child{border-top:none;margin-top:0;padding-top:0}.case{padding:10px 0 4px}.k{font-size:var(--text-xs);color:var(--text-secondary);font-family:var(--font-sans);display:block;padding-bottom:8px}.note{font-size:var(--text-xs);line-height:17px;color:var(--text-secondary);max-width:64ch;padding:6px 0 0}.note code{font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-heading)}.grey{background:var(--bg-app);padding:16px;border-radius:var(--radius-lg)}a{color:var(--link)}a:hover{color:var(--link-hover)}.kb-h-gap{margin:28px 0 12px}.kb-h-gap:first-child{margin-top:0}</style></head>
11
11
  <body><div id="root"></div>
12
12
  <script type="text/babel">
13
- const { Table, Select, Badge, Pagination, EmptyState, Skeleton, IconButton } = window.KeybricksDesignSystem_71231a;
13
+ const { Table, Select, Badge, Pagination, EmptyState, Skeleton, IconButton, Button, Icon, Switch, Checkbox, Input, NumberStepper, DatePicker, Tag, Avatar, AvatarGroup, ProgressBar, Sparkline, DiffValue } = window.KeybricksDesignSystem_71231a;
14
14
  const { KBLive: Live } = window;
15
15
  const { KBTiers: Tiers } = window;
16
16
  const Case = ({ k, children }) => <div className="case"><span className="k">{k}</span>{children}</div>;
@@ -56,6 +56,97 @@ const TREE_ROWS = [
56
56
  ] },
57
57
  { id: "cash", name: "Cash Flow Forecast", description: "13-week rolling forecast", icon: "spreadsheet", hue: "source", details: "Spreadsheet", at: "2026-10-16 08:30" },
58
58
  ];
59
+ /* Cell parts. Controls inherit the table's tier; parts that take a number (Avatar, AvatarGroup, Sparkline) get the tier's value from FIT. */
60
+ const FIT = { sm: { av: 20, sp: [56, 16] }, md: { av: 24, sp: [64, 20] }, lg: { av: 28, sp: [80, 24] } };
61
+ const ROW_ICON = { width: "var(--control-icon)", height: "var(--control-icon)", color: "var(--text-muted)", flexShrink: 0 };
62
+ const IconText = ({ icon, children }) => <span style={{ display: "inline-flex", alignItems: "center", gap: 8, minWidth: 0 }}><Icon name={icon} size={16} style={ROW_ICON} /><span style={{ overflow: "hidden", textOverflow: "ellipsis" }}>{children}</span></span>;
63
+ const Person = ({ n, s }) => <span style={{ display: "inline-flex", alignItems: "center", gap: 8 }}><Avatar name={n} size={s} />{n}</span>;
64
+ const Tags = ({ v }) => <span style={{ display: "inline-flex", gap: 4 }}>{v.map(t => <Tag key={t}>{t}</Tag>)}</span>;
65
+ const Prog = ({ v }) => <span style={{ display: "flex", alignItems: "center", gap: 8 }}><ProgressBar value={v} color={v === 100 ? "var(--success)" : undefined} style={{ flex: 1, minWidth: 48 }} /><span style={{ fontVariantNumeric: "tabular-nums", color: "var(--text-secondary)", minWidth: 34, textAlign: "right" }}>{v}%</span></span>;
66
+ const TONE = { Completed: "success", Failed: "danger", Partial: "warning", Draft: "neutral" };
67
+ const ENT_ROWS = [
68
+ { ent: "Shanghai Subsidiary", owner: "Felix Wang", status: "Completed", tags: ["FY26", "CN"], team: ["Felix Wang", "Luo Lisi", "Guo Kaige", "Ann Li"], p: 100, trend: [4, 5, 5, 6, 7, 9], from: "12,401", to: "12,980", on: true, notify: true, act: "Overwrite" },
69
+ { ent: "Tokyo Branch", owner: "Luo Lisi", status: "Failed", tags: ["FY26", "JP"], team: ["Luo Lisi", "Ann Li"], p: 18, trend: [9, 8, 8, 6, 5, 4], from: "486,300", to: "471,050", on: false, notify: false, act: "Skip" },
70
+ { ent: "Singapore HQ", owner: "Guo Kaige", status: "Partial", tags: ["FY26"], team: ["Guo Kaige", "Felix Wang", "Luo Lisi"], p: 64, trend: [5, 6, 5, 7, 6, 7], from: "3,215", to: "3,215", on: true, notify: false, act: "New" },
71
+ ];
72
+ const DISPLAY_A = [
73
+ { key: "ent", label: "Entity", width: 190, render: v => <IconText icon="dimension-entity">{v}</IconText> },
74
+ { key: "owner", label: "Owner", width: 140, render: v => <Person n={v} s={FIT.md.av} /> },
75
+ { key: "status", label: "Status", width: 110, render: v => <Badge tone={TONE[v]}>{v}</Badge> },
76
+ { key: "tags", label: "Tags", render: v => <Tags v={v} /> },
77
+ { key: "link", label: "", align: "right", width: 100, render: () => <a href="#" onClick={e => e.preventDefault()}>Open report</a> },
78
+ ];
79
+ const DISPLAY_B = [
80
+ { key: "ent", label: "Entity", width: 170 },
81
+ { key: "team", label: "Reviewers", width: 110, render: v => <AvatarGroup names={v} size={FIT.md.av} max={3} /> },
82
+ { key: "p", label: "Progress", width: 150, render: v => <Prog v={v} /> },
83
+ { key: "trend", label: "Trend", width: 96, render: v => <Sparkline values={v} width={FIT.md.sp[0]} height={FIT.md.sp[1]} tone="auto" /> },
84
+ { key: "to", label: "Rows", align: "right", render: (v, r) => r.from === v ? <span style={{ fontVariantNumeric: "tabular-nums" }}>{v}</span> : <DiffValue from={r.from} to={v} mono /> },
85
+ ];
86
+ const LiveSwitch = ({ v }) => <Live init={v}>{(x, set) => <Switch checked={x} onChange={set} />}</Live>;
87
+ const LiveCheck = ({ v }) => <Live init={v}>{(x, set) => <Checkbox checked={x} onChange={set} />}</Live>;
88
+ const LiveSelect = ({ v }) => <Live init={v}>{(x, set) => <Select options={["New", "Overwrite", "Skip"]} value={x} onChange={(e) => set(e && e.target ? e.target.value : e)} style={{ width: "100%" }} />}</Live>;
89
+ const EDIT_A = [
90
+ { key: "ent", label: "Entity", width: 180 },
91
+ { key: "on", label: "Enabled", width: 90, render: v => <LiveSwitch v={v} /> },
92
+ { key: "notify", label: "Notify", width: 80, render: v => <LiveCheck v={v} /> },
93
+ { key: "act", label: "On conflict", width: 150, render: v => <LiveSelect v={v} /> },
94
+ { key: "ops", label: "", align: "right", render: () => <span style={{ display: "inline-flex", alignItems: "center", gap: 4 }}><Button variant="text">Edit</Button><IconButton icon="ellipsis" muted label="Row actions" /></span> },
95
+ ];
96
+ const RATE_ROWS = [
97
+ { code: "FX_RATE_USD_CNY", rate: "7.1284", n: 12, from: "2026-01-01" },
98
+ { code: "FX_RATE_EUR_CNY", rate: "7.7920", n: 6, from: "2026-07-01" },
99
+ ];
100
+ const EDIT_B = [
101
+ { key: "code", label: "Code", code: true, width: 170 },
102
+ { key: "rate", label: "Rate", width: 110, render: v => <Live init={v}>{(x, set) => <Input value={x} onChange={e => set(e.target.value)} style={{ width: "100%", textAlign: "right", fontVariantNumeric: "tabular-nums" }} />}</Live> },
103
+ { key: "n", label: "Periods", width: 130, render: v => <Live init={v}>{(x, set) => <NumberStepper value={x} onChange={set} min={1} max={12} />}</Live> },
104
+ { key: "from", label: "Effective from", width: 180, render: v => <Live init={v}>{(x, set) => <DatePicker value={x} onChange={set} width="100%" />}</Live> },
105
+ ];
106
+ const TIER_ROWS = ENT_ROWS.slice(0, 2).map((r, i) => ({ ...r, ent: ["Shanghai Sub.", "Tokyo Branch"][i] }));
107
+ const TIER_COLS = (t) => [
108
+ { key: "ent", label: "Entity", width: 140, render: v => <IconText icon="dimension-entity">{v}</IconText> },
109
+ { key: "owner", label: "Owner", width: 130, render: v => <Person n={v} s={FIT[t].av} /> },
110
+ { key: "on", label: "Enabled", width: 84, render: v => <LiveSwitch v={v} /> },
111
+ { key: "act", label: "On conflict", width: 160, render: v => <Live init={v}>{(x, set) => <Select options={["New", "Overwrite", "Skip"]} value={x} onChange={(e) => set(e && e.target ? e.target.value : e)} style={{ width: "100%", minWidth: 124 }} />}</Live> },
112
+ { key: "trend", label: "Trend", render: v => <Sparkline values={v} width={FIT[t].sp[0]} height={FIT[t].sp[1]} tone="auto" /> },
113
+ ];
114
+ const SPEC_COLS = [{ key: "part", label: "In the cell", width: 230 }, { key: "sm", label: "sm", mono: true, width: 100 }, { key: "md", label: "md", mono: true, width: 90 }, { key: "lg", label: "lg", mono: true, width: 90 }, { key: "how", label: "Sized by" }];
115
+ const SPEC_ROWS = [
116
+ { part: "Row", sm: "36", md: "40", lg: "48", how: "table size" },
117
+ { part: "Cell text", sm: "12 (13 CJK)", md: "14", lg: "14", how: "table size" },
118
+ { part: "Select · Input · DatePicker · Stepper · Button", sm: "28", md: "32", lg: "40", how: "inherits — no size prop" },
119
+ { part: "Switch", sm: "28×16", md: "32×18", lg: "32×18", how: "inherits" },
120
+ { part: "Checkbox · Radio", sm: "14", md: "16", lg: "16", how: "inherits" },
121
+ { part: "Tag", sm: "20", md: "24", lg: "24", how: "inherits" },
122
+ { part: "Icon beside text", sm: "14", md: "16", lg: "16", how: "--control-icon" },
123
+ { part: "Badge (status)", sm: "20", md: "20", lg: "20", how: "fixed" },
124
+ { part: "Progress track", sm: "6", md: "6", lg: "6", how: "fixed" },
125
+ { part: "Avatar · AvatarGroup", sm: "20", md: "24", lg: "28", how: "pass size" },
126
+ { part: "Sparkline", sm: "56×16", md: "64×20", lg: "80×24", how: "pass width · height" },
127
+ ];
128
+ const PIN_COLS = [{ key: "code", label: "Account", mono: true, width: 110, sortable: true }, { key: "name", label: "Account name", sortable: true }, { key: "ent", label: "Entity", width: 150 }, { key: "amt", label: "Closing balance", mono: true, align: "right", width: 150, sortable: true }];
129
+ const PIN_ROWS = [
130
+ { code: "1001", name: "Cash on hand", ent: "Shanghai Sub.", amt: "12,480.00" },
131
+ { code: "1002", name: "Bank deposits", ent: "Shanghai Sub.", amt: "3,215,906.12" },
132
+ { code: "1122", name: "Accounts receivable", ent: "Tokyo Branch", amt: "842,100.00" },
133
+ { code: "1403", name: "Raw materials", ent: "Singapore HQ", amt: "96,350.40" },
134
+ { code: "2202", name: "Accounts payable", ent: "Tokyo Branch", amt: "486,300.00" },
135
+ { code: "2211", name: "Payroll payable", ent: "Singapore HQ", amt: "128,000.00" },
136
+ ];
137
+ const LOCK_ROWS = [
138
+ { name: "Group (consolidated)", dim: "Entity", req: true },
139
+ { name: "Shanghai Sub.", dim: "Entity" },
140
+ { name: "Tokyo Branch", dim: "Entity" },
141
+ { name: "Actual", dim: "Version" },
142
+ { name: "Budget v1", dim: "Version" },
143
+ { name: "Budget v2", dim: "Version" },
144
+ { name: "Forecast Q3", dim: "Version" },
145
+ ];
146
+ /* The app's rules: one Version at most; the consolidated group is required. The Table only renders what isRowDisabled returns. */
147
+ const lockRule = (sel) => (r, i) => !!r.req || (r.dim === "Version" && sel.some(j => j !== i && LOCK_ROWS[j].dim === "Version"));
148
+ const lockWhy = (r) => r.req ? "Required by this report" : "Version slot already filled";
149
+ const oneVersion = (ids) => { let seen = false; return ids.filter(i => LOCK_ROWS[i].dim !== "Version" || (!seen && (seen = true))); };
59
150
  const ZH_LABELS = { sortAsc: "升序排列", sortDesc: "降序排列", sortClear: "取消排序", more: "更多操作", expand: "展开", collapse: "收起" };
60
151
  const DOC_ROWS_ZH = [
61
152
  { name: "2026 预算", description: "FY2026 预算周期", icon: "folder", hue: "neutral", details: "3 个文档" },
@@ -73,11 +164,20 @@ function Sheet() {
73
164
  const [srt, setSrt] = React.useState(null);
74
165
  const [exp, setExp] = React.useState(["budget"]);
75
166
  const [opened, setOpened] = React.useState(null);
167
+ const [pin, setPin] = React.useState([3]);
168
+ const [lock, setLock] = React.useState([0, 3]);
169
+ const lockFn = lockRule(lock);
170
+ const lockToggle = (w, v, next) => setLock(w === "all" ? oneVersion(next) : v ? [...lock, w] : lock.filter(i => i !== w));
171
+ const dimCell = (val, r, i) => <span style={{ color: lockFn(r, i) ? "var(--text-muted)" : undefined }}>{val}</span>;
172
+ const LOCK_COLS = [{ key: "name", label: "Member", sortable: true, render: dimCell }, { key: "dim", label: "Dimension", width: 160, render: dimCell }];
173
+ const pinToggle = (w, v) => { if (w === "all") setPin(v ? PIN_ROWS.map((_, i) => i) : []); else setPin(v ? [...pin, w] : pin.filter(i => i !== w)); };
174
+ const [pin2, setPin2] = React.useState([1, 3]);
175
+ const pinToggle2 = (w, v) => { if (w === "all") setPin2(v ? PIN_ROWS.map((_, i) => i) : []); else setPin2(v ? [...pin2, w] : pin2.filter(i => i !== w)); };
76
176
  return <div>
77
177
  <div className="kb-h kb-h-gap">Anatomy</div>
78
178
  <Case k="md — 40px rows · 14px cells · header in weight 600, sentence case · hairline rules">
79
179
  <Table bordered columns={RUNS} rows={RUN_ROWS} />
80
- <div className="note">One rule weight throughout (<code>--border-subtle</code>). The header is set apart by its <code>--bg-table-header</code> tint — never by a heavier line. Config-code columns (<code>code</code>) show a code chip — Menlo 12px on the inset fill; amount and ID columns (<code>mono</code>) keep the UI font with tabular figures; <code>align: "right"</code> for quantities.</div>
180
+ <div className="note">One rule weight throughout (<code>--border-subtle</code>). The header is set apart by its <code>--bg-table-header</code> tint — never by a heavier line. Config-code columns (<code>code</code>) show a code chip — Menlo 12px on the hover fill; amount and ID columns (<code>mono</code>) keep the UI font with tabular figures; <code>align: "right"</code> for quantities.</div>
81
181
  </Case>
82
182
 
83
183
  <div className="kb-h kb-h-gap">Densities</div>
@@ -85,9 +185,30 @@ function Sheet() {
85
185
  <Case k="sm · 36px rows, 12px text — dense grids and the data-preview drawer"><Table size="sm" bordered columns={DENSE} rows={DENSE_ROWS} /></Case>
86
186
  <Case k="sm · CJK — 13px text, regular weight (lang=&quot;zh-CN&quot; on the region)"><div lang="zh-CN"><Table size="sm" bordered columns={[{ key: "code", label: "科目代码", mono: true }, { key: "name", label: "科目名称" }, { key: "dept", label: "成本中心" }, { key: "amt", label: "期末余额", mono: true }]} rows={[{ code: "1001", name: "库存现金", dept: "總部財務", amt: "12,480.00" }, { code: "1002", name: "银行存款", dept: "上海分公司", amt: "3,215,906.12" }, { code: "2202", name: "应付账款", dept: "東京支社", amt: "486,300.00" }]} /></div></Case>
87
187
 
88
- <div className="kb-h kb-h-gap">Column types</div>
89
- <Case k="text · mono · align right · render (Badge) · render (Select)">
90
- <Table bordered columns={[{ ...CFG[1], width: 220 }, { key: "cur", label: "Current Value", align: "right", width: 130 }, { key: "status", label: "Status", width: 120, render: (_, r, i) => <Badge tone={["success", "neutral", "warning"][i]}>{["Match", "Identical", "Conflict"][i]}</Badge> }, { key: "action", label: "Action", width: 160, render: () => <Live init="New">{(v, set) => <Select options={["New", "Overwrite", "Skip"]} value={v} onChange={(e)=>set(e && e.target ? e.target.value : e)} style={{ width: 128 }} />}</Live> }]} rows={CFG_ROWS} />
188
+ <div className="kb-h kb-h-gap">Cell types</div>
189
+ <Case k="text · code · mono · align right — built in (see Anatomy); everything else goes through render">
190
+ <Table bordered columns={[{ ...CFG[1], width: 220 }, { key: "module", label: "Module" }, { key: "cur", label: "Current Value", mono: true, align: "right", width: 130 }]} rows={CFG_ROWS} />
191
+ </Case>
192
+ <Case k="display — icon + text · Avatar + name · Badge status · Tag list · link">
193
+ <Table bordered columns={DISPLAY_A} rows={ENT_ROWS} />
194
+ </Case>
195
+ <Case k="display — AvatarGroup · progress (bar + %) · Sparkline · DiffValue">
196
+ <Table bordered columns={DISPLAY_B} rows={ENT_ROWS} />
197
+ <div className="note">A status reads as a <code>Badge</code> (tinted, fixed 20px); a <code>Tag</code> is a label the user attached. Progress is the 6px <code>ProgressBar</code> without a label, its value beside it in tabular figures; it turns <code>--success</code> at 100%. <code>DiffValue</code> only where the value changed; an unchanged one stays plain.</div>
198
+ </Case>
199
+ <Case k="editable — Switch · Checkbox · Select · row actions (text Button + ⋯)">
200
+ <Table bordered columns={EDIT_A} rows={ENT_ROWS} />
201
+ </Case>
202
+ <Case k="editable — Input · NumberStepper · DatePicker">
203
+ <Table bordered columns={EDIT_B} rows={RATE_ROWS} />
204
+ <div className="note">Controls fill their column (<code>width: 100%</code>) and sit 4px from the row edges — the row is the tier's bar height, the control the tier's control height. For a grid the user edits cell by cell, use <b>Spreadsheet</b> or <b>Smart Table</b> instead.</div>
205
+ </Case>
206
+ <Case k="per tier — the same cells at sm · md · lg">
207
+ {["sm", "md", "lg"].map(t => <div key={t} style={{ paddingBottom: 12 }}><span className="k">{t} · {{ sm: "36", md: "40", lg: "48" }[t]}px rows · Avatar {FIT[t].av} · Sparkline {FIT[t].sp.join("×")}</span><Table size={t} bordered columns={TIER_COLS(t)} rows={TIER_ROWS} /></div>)}
208
+ </Case>
209
+ <Case k="sizes per tier — px">
210
+ <Table size="sm" bordered columns={SPEC_COLS} rows={SPEC_ROWS} />
211
+ <div className="note">Controls inside the table inherit its tier — never pass <code>size</code> to a control in a cell. Parts that take a number don't follow the tier on their own: pass the value for the table's tier from this list (<code>Avatar size</code>, <code>Sparkline width / height</code>). <code>Badge</code> and the progress track stay the same size at every tier.</div>
91
212
  </Case>
92
213
 
93
214
  <div className="kb-h kb-h-gap">Two-line name</div>
@@ -117,6 +238,18 @@ function Sheet() {
117
238
  <Case k="selectable · selected · onToggle — header checkbox is indeterminate on partial">
118
239
  <Table bordered selectable selected={sel} onToggle={toggle} columns={CFG} rows={CFG_ROWS} />
119
240
  </Case>
241
+ <Case k='isRowDisabled · disabledReason · pinSelected="labels" — Selected, Others, Unavailable; pick a Version and the other Versions lock'>
242
+ <Table bordered selectable pinSelected="labels" selected={lock} onToggle={lockToggle} isRowDisabled={lockFn} disabledReason={lockWhy} columns={LOCK_COLS} rows={LOCK_ROWS} />
243
+ <div className="note">The rule lives in the app (one Version at most) and is asked again on every render. Uncheck <b>Actual</b> and every Version unlocks; pick another and the rest lock again. A locked checkbox is 50%, not-allowed, with the reason as its tooltip. The lock never unchecks a row: <b>Group (consolidated)</b> is locked and stays checked, and Select-all / Clear skip it. The header checkbox counts unlocked rows only. With <code>pinSelected="labels"</code>, locked rows that aren't checked form a third group, <b>Unavailable · n</b>, after Others; sort by a column and the three groups keep their order, each sorted on its own. A row moves group as soon as its lock changes; an empty group shows no group row. In <code>pinSelected</code> (divider) they stay with the unchecked rows. The app dims the locked rows' text itself, and applies its own rule to Select-all's <code>next</code> (here: keep one Version). <code>selected</code>: <code>{JSON.stringify(lock)}</code></div>
244
+ </Case>
245
+ <Case k="pinSelected — checked rows move to the top on a grey band, with a 1px rule above and below it">
246
+ <Table bordered selectable pinSelected selected={pin} onToggle={pinToggle} columns={PIN_COLS} rows={PIN_ROWS} />
247
+ <div className="note">Check a row and it moves to the top; uncheck it and it goes back to its place. Pinned rows sit on <code>--bg-inset</code>. While any are pinned, the header rule and the rule closing the group are <code>--border-control</code> (the usual <code>--border-subtle</code> is the header tint itself, so it would not show); the closing rule only appears while both groups have rows. With a sort applied, each group keeps the sort. <code>selected</code> and <code>onToggle</code> keep the original row positions. <code>selected</code>: <code>{JSON.stringify(pin)}</code></div>
248
+ </Case>
249
+ <Case k='pinSelected="labels" — the same band, with a group row above each group'>
250
+ <Table bordered selectable pinSelected="labels" selected={pin2} onToggle={pinToggle2} columns={PIN_COLS} rows={PIN_ROWS} />
251
+ <div className="note">28px group rows on the header tint, 12px semibold: “Selected · n” with <b>Clear</b> (fires <code>onToggle("all", false)</code>) and “Others · n”. The header rule, the group rows' rules and the rule closing the pinned group are <code>--border-control</code>, so the header, each group and the pinned band stay apart. Use it when the checked set is something the user works on (a batch to post, accounts to reconcile). Translate with <code>labels.pinned / others / clear</code>.</div>
252
+ </Case>
120
253
 
121
254
  <div className="kb-h kb-h-gap">Frames</div>
122
255
  <Case k="bordered — table draws its own card frame; use on grey pages">
@@ -18,43 +18,43 @@ function Sheet() {
18
18
  const [v, set] = React.useState({});
19
19
  const b = (k, d = false) => ({ checked: v[k] ?? d, onChange: n => set(s => ({ ...s, [k]: n })) });
20
20
  return <div>
21
- <div className="kb-h kb-h-gap">Sizes &amp; states — sm · md · lg</div>
22
- <Case k="on"><Switch size="sm" {...b("s1", true)} /><Switch {...b("s2", true)} /><Switch size="lg" {...b("s3", true)} /></Case>
23
- <Case k="off"><Switch size="sm" {...b("s4")} /><Switch {...b("s5")} /><Switch size="lg" {...b("s6")} /></Case>
24
- <Case k="disabled"><Switch size="sm" checked disabled /><Switch checked disabled /><Switch size="lg" checked disabled /></Case>
25
- <Case k="label (settings row)"><Switch size="sm" label="Compact rows" {...b("s7", true)} /><Switch label="Debug Mode" {...b("s8", true)} /><Switch size="lg" label="Auto-run" {...b("s9", true)} /></Case>
21
+ <div className="kb-h kb-h-gap">Sizes &amp; states — sm · md (lg keeps the md track)</div>
22
+ <Case k="on"><Switch size="sm" {...b("s1", true)} /><Switch {...b("s2", true)} /></Case>
23
+ <Case k="off"><Switch size="sm" {...b("s4")} /><Switch {...b("s5")} /></Case>
24
+ <Case k="disabled"><Switch size="sm" checked disabled /><Switch checked disabled /></Case>
25
+ <Case k="label (settings row)"><Switch size="sm" label="Compact rows" {...b("s7", true)} /><Switch label="Debug Mode" {...b("s8", true)} /></Case>
26
26
 
27
27
  <div className="kb-h kb-h-gap">Size tiers — Latin and CJK labels</div>
28
28
  <Tiers init={true} render={(t,l,v,set)=><Switch label={l==="en"?"Compact rows":"紧凑行高"} checked={v} onChange={set}/>}/>
29
29
 
30
30
  <div className="kb-h kb-h-gap">Semantic tone — danger off → success on</div>
31
- <Case k='tone="semantic" · on'><Switch size="sm" tone="semantic" {...b("t1", true)} /><Switch tone="semantic" {...b("t2", true)} /><Switch size="lg" tone="semantic" {...b("t3", true)} /></Case>
32
- <Case k="off"><Switch size="sm" tone="semantic" {...b("t4")} /><Switch tone="semantic" {...b("t5")} /><Switch size="lg" tone="semantic" {...b("t6")} /></Case>
31
+ <Case k='tone="semantic" · on'><Switch size="sm" tone="semantic" {...b("t1", true)} /><Switch tone="semantic" {...b("t2", true)} /></Case>
32
+ <Case k="off"><Switch size="sm" tone="semantic" {...b("t4")} /><Switch tone="semantic" {...b("t5")} /></Case>
33
33
 
34
- <div className="kb-h kb-h-gap">Glyph in the knob — md · lg only (sm knob is 12px, too small for a glyph)</div>
35
- <Case k="knobIcon check"><Switch knobIcon="check" {...b("g1", true)} /><Switch size="lg" knobIcon="check" {...b("g2", true)} /></Case>
36
- <Case k="knobIcon {check, x}"><Switch knobIcon={{ on: "check", off: "x" }} {...b("g3")} /><Switch size="lg" knobIcon={{ on: "check", off: "x" }} {...b("g4")} /></Case>
37
- <Case k="knobIcon {sun, moon}"><Switch knobIcon={{ on: "sun", off: "moon" }} {...b("g5", true)} /><Switch size="lg" knobIcon={{ on: "sun", off: "moon" }} {...b("g6", true)} /></Case>
38
- <Case k="knobText {Y, N}"><Switch knobText={{ on: "Y", off: "N" }} {...b("g7", true)} /><Switch size="lg" knobText={{ on: "Y", off: "N" }} {...b("g8", true)} /></Case>
39
- <Case k="knobText — one CJK character {开, 关}"><Switch knobText={{ on: "开", off: "关" }} {...b("g9")} /><Switch size="lg" knobText={{ on: "开", off: "关" }} {...b("g10")} /></Case>
40
- <Case k="semantic + knobIcon"><Switch tone="semantic" knobIcon={{ on: "check", off: "x" }} {...b("g11", true)} /><Switch size="lg" tone="semantic" knobIcon={{ on: "check", off: "x" }} {...b("g12", true)} /></Case>
34
+ <div className="kb-h kb-h-gap">Glyph in the knob — md only (sm knob is 12px, too small for a glyph)</div>
35
+ <Case k="knobIcon check"><Switch knobIcon="check" {...b("g1", true)} /></Case>
36
+ <Case k="knobIcon {check, x}"><Switch knobIcon={{ on: "check", off: "x" }} {...b("g3")} /></Case>
37
+ <Case k="knobIcon {sun, moon}"><Switch knobIcon={{ on: "sun", off: "moon" }} {...b("g5", true)} /></Case>
38
+ <Case k="knobText {Y, N}"><Switch knobText={{ on: "Y", off: "N" }} {...b("g7", true)} /></Case>
39
+ <Case k="knobText — one CJK character {开, 关}"><Switch knobText={{ on: "开", off: "关" }} {...b("g9")} /></Case>
40
+ <Case k="semantic + knobIcon"><Switch tone="semantic" knobIcon={{ on: "check", off: "x" }} {...b("g11", true)} /></Case>
41
41
 
42
- <div className="kb-h kb-h-gap">Text inside the pill — md · lg only</div>
43
- <Case k="ON / OFF"><Switch insideOn="ON" insideOff="OFF" {...b("i1", true)} /><Switch size="lg" insideOn="ON" insideOff="OFF" {...b("i2", true)} /></Case>
44
- <Case k="开 / 关"><Switch insideOn="开" insideOff="关" {...b("i3")} /><Switch size="lg" insideOn="开" insideOff="关" {...b("i4")} /></Case>
45
- <Case k="semantic · Y / N"><Switch tone="semantic" insideOn="Y" insideOff="N" {...b("i5", true)} /><Switch size="lg" tone="semantic" insideOn="Y" insideOff="N" {...b("i6", true)} /></Case>
42
+ <div className="kb-h kb-h-gap">Text inside the pill — md only</div>
43
+ <Case k="ON / OFF"><Switch insideOn="ON" insideOff="OFF" {...b("i1", true)} /></Case>
44
+ <Case k="开 / 关"><Switch insideOn="开" insideOff="关" {...b("i3")} /></Case>
45
+ <Case k="semantic · Y / N"><Switch tone="semantic" insideOn="Y" insideOff="N" {...b("i5", true)} /></Case>
46
46
 
47
- <div className="kb-h kb-h-gap">Captions beside — sm · md · lg (active side reads heading)</div>
48
- <Case k="labelOn"><Switch size="sm" labelOn="Enabled" {...b("l1", true)} /><Switch labelOn="Enabled" {...b("l2", true)} /><Switch size="lg" labelOn="Enabled" {...b("l3", true)} /></Case>
49
- <Case k="labelOff + labelOn"><Switch size="sm" labelOff="Monthly" labelOn="Annual" {...b("l4")} /><Switch labelOff="Monthly" labelOn="Annual" {...b("l5")} /><Switch size="lg" labelOff="Monthly" labelOn="Annual" {...b("l6")} /></Case>
50
- <Case k="semantic · Off / On"><Switch size="sm" tone="semantic" labelOff="Off" labelOn="On" {...b("l7", true)} /><Switch tone="semantic" labelOff="Off" labelOn="On" {...b("l8", true)} /><Switch size="lg" tone="semantic" labelOff="Off" labelOn="On" {...b("l9", true)} /></Case>
47
+ <div className="kb-h kb-h-gap">Captions beside — sm · md (active side reads heading)</div>
48
+ <Case k="labelOn"><Switch size="sm" labelOn="Enabled" {...b("l1", true)} /><Switch labelOn="Enabled" {...b("l2", true)} /></Case>
49
+ <Case k="labelOff + labelOn"><Switch size="sm" labelOff="Monthly" labelOn="Annual" {...b("l4")} /><Switch labelOff="Monthly" labelOn="Annual" {...b("l5")} /></Case>
50
+ <Case k="semantic · Off / On"><Switch size="sm" tone="semantic" labelOff="Off" labelOn="On" {...b("l7", true)} /><Switch tone="semantic" labelOff="Off" labelOn="On" {...b("l8", true)} /></Case>
51
51
 
52
52
  <div className="kb-h kb-h-gap">Loading — sm · md · lg</div>
53
- <Case k="loading · on"><Switch size="sm" checked loading /><Switch checked loading /><Switch size="lg" checked loading /></Case>
54
- <Case k="loading · off"><Switch size="sm" loading /><Switch loading /><Switch size="lg" loading /></Case>
55
- <Case k="semantic · loading"><Switch size="sm" tone="semantic" checked loading /><Switch tone="semantic" checked loading /><Switch size="lg" tone="semantic" checked loading /></Case>
56
- <Case k="beforeChange → resolves 1.2s"><Switch size="sm" beforeChange={() => wait(1200)} {...b("a1", true)} /><Switch beforeChange={() => wait(1200)} {...b("a2", true)} /><Switch size="lg" knobIcon={{ on: "check", off: "x" }} beforeChange={() => wait(1200)} {...b("a3", true)} /></Case>
57
- <Case k="beforeChange → rejects"><Switch size="sm" tone="semantic" beforeChange={() => wait(1200, false)} {...b("a4")} /><Switch tone="semantic" beforeChange={() => wait(1200, false)} {...b("a5")} /><Switch size="lg" tone="semantic" knobIcon={{ on: "check", off: "x" }} beforeChange={() => wait(1200, false)} {...b("a6")} /><span className="cap">stays put after the spinner</span></Case>
53
+ <Case k="loading · on"><Switch size="sm" checked loading /><Switch checked loading /></Case>
54
+ <Case k="loading · off"><Switch size="sm" loading /><Switch loading /></Case>
55
+ <Case k="semantic · loading"><Switch size="sm" tone="semantic" checked loading /><Switch tone="semantic" checked loading /></Case>
56
+ <Case k="beforeChange → resolves 1.2s"><Switch size="sm" beforeChange={() => wait(1200)} {...b("a1", true)} /><Switch beforeChange={() => wait(1200)} {...b("a2", true)} /></Case>
57
+ <Case k="beforeChange → rejects"><Switch size="sm" tone="semantic" beforeChange={() => wait(1200, false)} {...b("a4")} /><Switch tone="semantic" beforeChange={() => wait(1200, false)} {...b("a5")} /><span className="cap">stays put after the spinner</span></Case>
58
58
  </div>;
59
59
  }
60
60
  ReactDOM.createRoot(document.getElementById("root")).render(<Sheet/>);