@keyfunc/keybricks-ds 2.3.1 → 2.4.2

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 (83) hide show
  1. package/DEPLOY.md +11 -5
  2. package/DESIGN.md +50 -1
  3. package/INTEGRATION.md +33 -6
  4. package/_adherence.oxlintrc.json +35 -13
  5. package/assets/logos/datasource/dat.png +0 -0
  6. package/assets/logos/datasource/excel.png +0 -0
  7. package/assets/logos/datasource/ftp.png +0 -0
  8. package/assets/logos/datasource/hierarchy.png +0 -0
  9. package/assets/logos/datasource/hive.png +0 -0
  10. package/assets/logos/datasource/json.png +0 -0
  11. package/assets/logos/datasource/mariadb.png +0 -0
  12. package/assets/logos/datasource/mongodb.png +0 -0
  13. package/assets/logos/datasource/mysql.png +0 -0
  14. package/assets/logos/datasource/odbc.png +0 -0
  15. package/assets/logos/datasource/oracle.png +0 -0
  16. package/assets/logos/datasource/postgresql.png +0 -0
  17. package/assets/logos/datasource/rest-api.png +0 -0
  18. package/assets/logos/datasource/sap-alt.png +0 -0
  19. package/assets/logos/datasource/sap.png +0 -0
  20. package/assets/logos/datasource/soap.png +0 -0
  21. package/assets/logos/datasource/sql-server.png +0 -0
  22. package/components/_cards/app-chrome.card.html +33 -21
  23. package/components/_cards/context-menu.card.html +7 -2
  24. package/components/_cards/icons-0-usage.card.html +88 -0
  25. package/components/_cards/icons-4-datasource.card.html +5 -0
  26. package/components/_cards/relative-time-picker.card.html +45 -0
  27. package/components/_cards/segmented-control.card.html +9 -3
  28. package/components/_cards/smart-table.card.html +4 -1
  29. package/components/_cards/tabs.card.html +5 -2
  30. package/components/_shared/src-data.js +1 -1
  31. package/components/_shared/src-feedback.js +1 -1
  32. package/components/_shared/src-forms.js +1 -1
  33. package/components/_shared/src-navigation.js +1 -1
  34. package/components/_shared/src-tabs.js +1 -1
  35. package/components/_shared/view-source.js +12 -12
  36. package/components/_vue/index.mjs +5 -1
  37. package/components/data/SmartTable.d.ts +7 -2
  38. package/components/data/SmartTable.jsx +12 -4
  39. package/components/data/SmartTable.vue +12 -4
  40. package/components/feedback/ContextMenu.d.ts +3 -2
  41. package/components/feedback/ContextMenu.jsx +8 -4
  42. package/components/feedback/ContextMenu.vue +9 -6
  43. package/components/feedback/Menu.d.ts +1 -1
  44. package/components/feedback/feedback.prompt.md +1 -1
  45. package/components/forms/RelativeTimePicker.d.ts +39 -0
  46. package/components/forms/RelativeTimePicker.jsx +89 -0
  47. package/components/forms/RelativeTimePicker.prompt.md +72 -0
  48. package/components/forms/RelativeTimePicker.vue +81 -0
  49. package/components/forms/forms.prompt.md +3 -0
  50. package/components/forms/relative-time.d.ts +12 -0
  51. package/components/forms/relative-time.js +90 -0
  52. package/components/index.d.ts +2 -0
  53. package/components/index.js +5 -3
  54. package/components/navigation/SidePanel.d.ts +22 -0
  55. package/components/navigation/SidePanel.jsx +23 -0
  56. package/components/navigation/SidePanel.vue +21 -0
  57. package/components/navigation/Sidebar.d.ts +23 -7
  58. package/components/navigation/Sidebar.jsx +38 -37
  59. package/components/navigation/Sidebar.vue +40 -39
  60. package/components/navigation/TopBar.d.ts +28 -3
  61. package/components/navigation/TopBar.jsx +54 -7
  62. package/components/navigation/TopBar.vue +26 -8
  63. package/components/navigation/UserMenu.vue +27 -0
  64. package/components/navigation/chrome-tones.js +22 -0
  65. package/components/navigation/nav-model.js +21 -18
  66. package/components/navigation/navigation.prompt.md +2 -0
  67. package/components/tabs/CountBadge.vue +12 -0
  68. package/components/tabs/PanelTabs.vue +7 -3
  69. package/components/tabs/PillTabs.vue +7 -3
  70. package/components/tabs/SegmentedControl.d.ts +4 -2
  71. package/components/tabs/SegmentedControl.jsx +5 -3
  72. package/components/tabs/SegmentedControl.vue +4 -3
  73. package/components/tabs/Tabs.d.ts +14 -4
  74. package/components/tabs/Tabs.jsx +35 -20
  75. package/components/tabs/Tabs.vue +7 -3
  76. package/components/tabs/tabs.prompt.md +2 -2
  77. package/package.json +2 -2
  78. package/readme.md +6 -5
  79. package/readme.zh-CN.md +4 -3
  80. package/tokens/layout.css +4 -3
  81. package/tokens/tokens.d.ts +2 -0
  82. package/tokens/tokens.js +4 -1
  83. package/tokens/tokens.json +8 -1
package/DEPLOY.md CHANGED
@@ -55,11 +55,11 @@ deploys the documentation site. Watch it under *Build → Pipelines*.
55
55
  **5. Cut the first release.**
56
56
 
57
57
  ```bash
58
- git tag v2.3.1 && git push --tags
58
+ git tag v2.4.2 && git push --tags
59
59
  ```
60
60
 
61
61
  That tag runs `generated:check` → `version:check` → `npm publish`. Confirm under *Deploy →
62
- Package Registry*: `@keyfunc/keybricks-ds` 2.3.1.
62
+ Package Registry*: `@keyfunc/keybricks-ds` 2.4.0.
63
63
 
64
64
  **6. Hand developers two things:** the Pages URL, and the `.npmrc` snippet in §4 below.
65
65
 
@@ -112,7 +112,7 @@ import tokens from '@keyfunc/keybricks-ds/tokens.json' // cross-
112
112
  ```
113
113
 
114
114
  Deep imports are the better default in Vue and in any app that cares about tree-shaking —
115
- a barrel of 122 exports asks the bundler to prove each one is unused.
115
+ a barrel of 130 exports asks the bundler to prove each one is unused.
116
116
 
117
117
  ## 3. Publishing (maintainers)
118
118
 
@@ -130,6 +130,12 @@ git push && git push --tags
130
130
  `.gitlab-ci.yml` then runs: `generated:check` → `version:check` (tag must equal
131
131
  `package.json` version) → `npm publish` → `pages`.
132
132
 
133
+ **Changelog.** Every release gets an entry at the top of `changelog.js` (version, ISO date, and
134
+ items tagged `new` / `improved` / `fixed` / `breaking` / `removed`, each in English, Chinese and
135
+ Japanese). The documentation site renders it as the *Changelog* section under Overview. While a
136
+ version is still being built its entry carries `status: "unreleased"`; on release, drop the status
137
+ and set the date. Migration details stay in `INTEGRATION.md`; the changelog is the short summary.
138
+
133
139
  **Versioning policy.** A token's *value* changing is a minor; a token being *renamed or
134
140
  removed* is a major, because consumer code references `var(--name)` directly. Adding a
135
141
  component or a prop is a minor. Changing a component's default appearance is a major —
@@ -156,7 +162,7 @@ GitLab injects `CI_JOB_TOKEN` — use that instead and no secret is stored anywh
156
162
  **Install — straight from git** (no registry, no publish; works today)
157
163
 
158
164
  ```json
159
- "@keyfunc/keybricks-ds": "git+ssh://git@gitlab.epm2demo.cn/Felix/keybricks-ds.git#v2.3.1"
165
+ "@keyfunc/keybricks-ds": "git+ssh://git@gitlab.epm2demo.cn/Felix/keybricks-ds.git#v2.4.2"
160
166
  ```
161
167
 
162
168
  Because the package has no build step, a git dependency needs no `prepare` script — npm
@@ -206,7 +212,7 @@ semantic layer. That is what makes dark mode cost nothing.
206
212
  ## Runtime checks
207
213
 
208
214
  `_tests/` holds browser pages that exercise the real components: `react-runtime.html` (the compiled
209
- bundle), `table-selection.html` (Table loaded from source: checkbox spacing at every width, code chips, two-line rows, sorting), `vue-runtime.html` (the `.vue` files, loaded with vue3-sfc-loader), `cards-interaction*.html`
215
+ bundle), `table-selection.html` (Table loaded from source: checkbox spacing at every width, code chips, two-line rows, sorting), `smart-table-density.html` (SmartTable from source: default / compact rows, the Row height menu, controlled density), `relative-time-picker.html` (RelativeTimePicker from source: helpers, tiers, dialog, keyboard), `vue-runtime.html` (the `.vue` files, loaded with vue3-sfc-loader), `cards-interaction*.html`
210
216
  (clicks through the documentation demos) and `icon-rules.html` (the Keybricks glyphs against the drawing rules). Serve the repo root (`npx serve .`), open a page and read the
211
217
  console — every line starts with PASS or FAIL. Run them after changing a component in either framework.
212
218
  `measure-cards.html` is a tool, not a test: it measures each documentation page at its card width so the
package/DESIGN.md CHANGED
@@ -138,6 +138,48 @@ and menu row is the tier's control text; a `Badge` or `Tag` is its own small obj
138
138
  exception. In content, a secondary annotation (a timestamp beside a name, a count beside a label)
139
139
  may be `--text-xs`. Never put a 14px label and a 12px control on the same baseline.
140
140
 
141
+ ### Count badges on tabs and segments
142
+
143
+ A `Tabs`, `PanelTabs` or `PillTabs` tab, or a `SegmentedControl` option, may carry `count` (`{ label: "Submission", count: 3 }`).
144
+ It renders 6px right of the label as a small full-radius badge: `--count-h` tall (sm 16 · md 18 · lg 20),
145
+ 6px sides, **12px tabular figures in the UI font** (digits are Latin, so the size is not bumped in CJK).
146
+ The fill is the alpha wash `--neutral-alpha-8` — it darkens whatever it sits on by the same step, so the
147
+ badge reads on a white tab, on the #f5f5f5 active pill and in dark mode alike (a solid `--bg-hover` vanished on
148
+ the active pill). Selected is told from unselected by the text alone: `--text-heading` vs `--text-body` —
149
+ never `--text-secondary`, which fails 4.5:1 on a grey fill at 12px. Under the blue active underline tab the
150
+ badge stays neutral, so the blue is only the tab's own label and line. A count of 0 or none draws nothing; `showZero` (on the control, or per option) draws a muted
151
+ 0 so a filter set keeps its shape. The selected value is still matched by label — a count is decoration,
152
+ never identity. Counts never appear in `iconOnly` segments.
153
+
154
+ ### App chrome
155
+
156
+ The shell is `Sidebar` + optional `SidePanel` + `TopBar`, all 56px at the top edge. The rail is 240px (64px
157
+ collapsed), with a 56px brand head without a rule, a 2px-gap list of 36px items and a pinned footer
158
+ (Management, Knowledge Base) above a version line. Items that open a panel (`trailing: true`) carry a
159
+ 14px chevron; while a panel is open the rail collapses to its 64px icon form and the panel docks beside
160
+ it at 240px, in the **same tone**. Tones: `recessed` (default — grey rail, the active item is a white tile
161
+ with a hairline ring), `flush` (white rail), `contrast` (rail scoped to the dark theme) and `custom`
162
+ (dark scope on one of ten `SIDEBAR_COLORS`). Pick one tone per product and pass it to both
163
+ `Sidebar` and `SidePanel`. The top bar holds the bare search on the left and, on the right, the dark
164
+ AI Assistant button, notifications, help and the user trigger, which opens `UserMenu` (identity,
165
+ appearance switch, rows).
166
+
167
+ ### Menus: action vs single choice
168
+
169
+ `ContextMenu` and `Menu` serve two kinds of list, and the row spacing says which one it is.
170
+
171
+ - **Action menu** (Edit · Duplicate · Delete): no item is selected. Rows are flush (`gap: 0`); only the
172
+ hovered row is filled.
173
+ - **Single-choice menu** (section switcher, language picker): pass `selected: true` on the current item. It
174
+ gets a `--bg-hover` fill and `--weight-medium` `--text-heading` — **no checkmark**, the fill is the
175
+ mark. Rows are announced as `menuitemradio` with `aria-checked`. As soon as any item is selected the
176
+ rows are spaced **2px apart** (`gap: 2px`), the same rhythm as `Sidebar`, `Tree` and `DocTree`, so a
177
+ hovered row's fill never runs into the selected row's fill.
178
+
179
+ Dividers keep their 4px top and bottom margin in both kinds, independent of the row gap. Don't mix the
180
+ two: a menu that both switches state and runs actions puts the actions after a divider, and the choice
181
+ rows still carry `selected`.
182
+
141
183
  ### Table headers
142
184
 
143
185
  A header cell is the **same size as its data cells**, weight 600, `--text-secondary`, sentence
@@ -270,12 +312,19 @@ so two controls that share a row can never disagree. `md` is the default on `:ro
270
312
  | `--cell-px` | 8 | 12 | 16 | horizontal padding of a table cell |
271
313
  | `--check-size` | 14 | 16 | 16 | checkbox and radio box |
272
314
  | `--tag-h` | 20 | 24 | 28 | `Tag` |
315
+ | `--count-h` | 16 | 18 | 20 | count badge beside a segment / pill label |
273
316
  | `--switch-w` × `--switch-h` | 28 × 16 | 36 × 20 | 44 × 24 | `Switch` track |
274
317
 
275
318
  Where each tier belongs:
276
319
 
277
320
  - **sm** — dense data grids (`SmartTable`, `Spreadsheet` and `DataPreview` set it themselves),
278
321
  controls inside pipeline nodes, a toolbar that must fit a 32px row.
322
+ `SmartTable` adds a **row density** on top of the tier: `default` rows are the tier's `--bar-h`
323
+ (36 / 40 / 48) with `--cell-px`; `compact` rows are the tier's `--control-h` (28 / 32 / 40 — the
324
+ same as a Spreadsheet cell) with the Spreadsheet's 8px cell padding — `--cell-px` is already 8 at
325
+ sm, so there only the height changes. Header and footer rows follow; the toolbar does not. Users switch it from the toolbar's *Row height* menu (a single-choice menu); `density`
326
+ controls it, `defaultDensity` seeds it. Pills (20), checkboxes (16) and avatars (20) all centre in
327
+ a 28px row. There is no taller option: long text truncates in every density.
279
328
  - **md** — everything else: app toolbars, filter bars, tables, long forms, settings pages.
280
329
  - **lg** — dialogs (`Modal` sets it), short forms, wizards, onboarding, a page with one task.
281
330
 
@@ -412,7 +461,7 @@ The list that prevents the most rework. These are absolute.
412
461
  - **Do not show a drop indicator where the drop is impossible.** No half-row boxes on a
413
462
  container, no insert rules on a node that can't take children.
414
463
  - **Do not build a new primitive when one exists.** Check the component index in
415
- `INTEGRATION.md` first — there are 117.
464
+ `INTEGRATION.md` first — there are 121.
416
465
  - **Do not use emoji as icons.** Use `<Icon name="..." />` (330 glyphs) or `<Unicon>` (1,206).
417
466
  - **Do not invent new component variants.** If `Button` lacks the variant you want, that is a
418
467
  design-system request, not a local override.
package/INTEGRATION.md CHANGED
@@ -31,7 +31,7 @@ Not using a registry yet? A git dependency works with no publishing at all — t
31
31
  has no build step, so npm can use the checkout as-is:
32
32
 
33
33
  ```json
34
- "@keyfunc/keybricks-ds": "git+ssh://git@gitlab.epm2demo.cn/Felix/keybricks-ds.git#v2.3.1"
34
+ "@keyfunc/keybricks-ds": "git+ssh://git@gitlab.epm2demo.cn/Felix/keybricks-ds.git#v2.4.2"
35
35
  ```
36
36
 
37
37
  **Source-only package.** `.jsx` and `.vue` files are published as authored, so your
@@ -162,7 +162,7 @@ Text, weight and tracking follow the tier through `--control-text` / `--control-
162
162
  weight becomes 600 and all tracking is 0 — nothing to configure. `DESIGN.md` has the full token
163
163
  table and the rules behind it.
164
164
 
165
- ## Component index (117 components)
165
+ ## Component index (121 components)
166
166
  Import path is `@keyfunc/keybricks-ds/components/<group>/<Name>.jsx` (React) or `.vue` (Vue 3).
167
167
  Every component has a sibling `.d.ts` with its full props contract, and each group has a
168
168
  `.prompt.md` with usage notes.
@@ -220,6 +220,7 @@ import Textarea from '@keyfunc/keybricks-ds/components/forms/Textarea.vue'
220
220
  | `Slider` | Range input with a filled track, drag value bubble and optional min/max labels. |
221
221
  | `DatePicker` | Text field plus a month-grid popover. Values are `YYYY-MM-DD` strings; `locale` localises the month title, weekday initials and field text. |
222
222
  | `Dropzone` | File drop target with hover, drag-over, uploaded and error states. |
223
+ | `RelativeTimePicker` | "How long ago" filter for logs, monitoring and inboxes: a field-shaped trigger opening a small dialog with an amount input, a unit select (minute … month, optional "Any time" → `null`) and a live "From" preview. Pure helpers in `forms/relative-time.js` (`resolveRelativeTime`, `normalizeRelativeTime` …). `labels` localises every string. |
223
224
 
224
225
  ### `data/` — tables and structured values
225
226
  | Component | What it's for |
@@ -238,7 +239,9 @@ import Textarea from '@keyfunc/keybricks-ds/components/forms/Textarea.vue'
238
239
  | Component | What it's for |
239
240
  | --- | --- |
240
241
  | `Sidebar` | The app's primary nav: collapsible sections, active state, footer items. `KEYBRICKS_NAV` is the product's real nav tree. |
242
+ | `SidePanel` | 240px panel docked beside the sidebar (Documents, Management), in the rail's tone. |
241
243
  | `TopBar` | Global header — search, notifications, theme toggle, avatar. |
244
+ | `UserMenu` | Account menu from the top bar's user trigger: identity, appearance switch, rows. |
242
245
  | `PageHeading` | Page title with optional subtitle and right-aligned actions. |
243
246
  | `PageToolbar` | Secondary bar under the heading: filters, view toggles, bulk actions. |
244
247
  | `Breadcrumb` | Path trail; collapses to an ellipsis dropdown past `maxItems`. |
@@ -249,9 +252,10 @@ import Textarea from '@keyfunc/keybricks-ds/components/forms/Textarea.vue'
249
252
  ### `tabs/` — switching views
250
253
  | Component | What it's for |
251
254
  | --- | --- |
252
- | `Tabs` | Page-level underline tabs. |
253
- | `PanelTabs` | Compact tabs inside a detail panel or drawer. |
254
- | `PillTabs` | Filter/status tabs as pills. |
255
+ | `Tabs` | Page-level underline tabs; `{ label, count }` adds a count badge. |
256
+ | `PanelTabs` | Compact tabs inside a detail panel or drawer; `{ label, count }` adds a count badge. |
257
+ | `PillTabs` | Filter/status tabs as pills; `{ label, count }` adds a count badge. |
258
+ | `CountBadge` | The count badge beside a segment / pill label (`--count-h`, 12px tabular figures); used by `SegmentedControl` and `PillTabs`, exported for custom filter bars. |
255
259
  | `SegmentedControl` | Switches views of the same data, applies instantly — not a saved form value (`RadioGroup`) and not page navigation (`Tabs`). Reads the size tier, `icon` per option, `iconOnly`, per-option or whole-control `disabled`, `fullWidth`. ← → keyboard. |
256
260
  | `ViewToggle` | List/grid icon toggle. |
257
261
 
@@ -490,7 +494,7 @@ a migration note in the release. Token renames are major because your own code r
490
494
  Pin a minor range and update deliberately:
491
495
 
492
496
  ```json
493
- "@keyfunc/keybricks-ds": "~2.3.1"
497
+ "@keyfunc/keybricks-ds": "~2.4.0"
494
498
  ```
495
499
 
496
500
  ### Migrating from 1.x to 2.0
@@ -516,6 +520,29 @@ Pin a minor range and update deliberately:
516
520
  - **Tokens outside CSS.** `tokens.json` / `tokens.js` / `tokens.d.ts` gained the tier tokens
517
521
  (`--control-*`, `--bar-h`, `--group-gap`, …) — 352 light tokens in all (2.0.2 adds `--danger-soft-hover`).
518
522
 
523
+ ### 2.4.2
524
+
525
+ - **New `RelativeTimePicker`** (forms) — the relative-time filter from the product, contributed by the app team and brought onto the system rules: inline token styling (the separate `relative-time.css` is gone), `Field` labels in the dialog, `data-field` hover, `.5` disabled opacity like every other field. Unit labels may be `{ one, other }` and follow `Intl.PluralRules` of `locale` ("1 hour ago" / "2 hours ago"; a CJK label stays one string); the amount field is a plain numeric text field (`inputmode="numeric"`, no native spinners). Props unchanged: `value` / `onChange(value, date)` (Vue `v-model` + `@change`), `allowUnlimited`, `labels`, `locale`, `referenceDate`, `formatDate`, `formatValue`, `size`. Helpers in `forms/relative-time.js`. Also brought in from the 2.4.1 merge: the app team's original files (`relative-time.js`, types, prompt) are kept; only the styling moved inline.
526
+
527
+ ### 2.4.1
528
+
529
+ - **`SmartTable` row density.** `density: "default" | "compact"` (+ `defaultDensity`, `onDensityChange`; Vue `v-model:density` / `@density-change`). Default rows stay the tier's `--bar-h`; compact rows are the tier's `--control-h` (28 at sm, the Spreadsheet's cell height) with 8px cell padding, header and footer included. The toolbar gains a *Row height* single-choice menu. `rowHeight` still works and overrides density.
530
+
531
+ ### 2.4.0
532
+
533
+ - **App Chrome matches the product.** `Sidebar`: 56px brand head without a rule, `--text-md` brand, a 2px-gap list, `trailing: true` items with a chevron (they open a side panel), Collapse / Expand item (chevron-left / panel-left), pinned footer and an optional `version` line. New `tone` prop — `"recessed"` (grey rail, active item a white tile with a hairline ring; **the new default**), `"flush"` (the old white look), `"contrast"` (dark-scoped rail), `"custom"` + `color` (`SIDEBAR_COLORS` or any hex). `KEYBRICKS_NAV` is the product's nav (Home … Task Execution); new `KEYBRICKS_NAV_FOOTER` (Management, Knowledge Base) is the default footer (was Help / Settings / Dark Mode) and `KEYBRICKS_MGMT` holds the Management panel groups. To keep the old look pass `tone="flush"` and your own `footerItems`.
534
+ - **Data-source logos:** 17 logo images (databases, SAP, protocols, file types) in `assets/logos/datasource/<id>.png`, 500×500 PNG, listed on the new *Data Source Logos* icons page. They are plain images, not `Icon` glyphs — use them with `<img>`.
535
+ - **New `SidePanel`**: the 240px panel docked beside the sidebar (Documents, Management), same tone as the rail, opens from 0 width with a fade; 56px header with title, actions and close.
536
+ - **`TopBar`**: dark **AI Assistant** (`ai` icon) first, then notifications and help `IconButton`s, then a user trigger (`user={{ name, email }}`) that opens a menu. The bell is no longer `lg`. **New `UserMenu`**: avatar, name, email, an Appearance row (icon-only light / dark / system) and menu rows.
537
+
538
+ - **`ContextMenu` / `Menu` single-choice state.** Items accept `selected: true` (section switcher, language picker): `--bg-hover` fill, `--weight-medium` `--text-heading`, no checkmark; rows become `menuitemradio` with `aria-checked`. When any item is selected the rows are spaced 2px apart (as in `Sidebar`, `Tree`, `DocTree`); action menus with nothing selected keep flush rows. Dividers keep 4px above and below.
539
+
540
+ - **Docs:** new *Icon Usage* page at the top of Icons (monochrome vs coloured glyphs: IconTile hues, Tree/DocTree document hues, status, marks). The documentation site itself now follows the system rules — neutral selected segments, sentence-case labels, sm-tier toolbar controls, 32px Sidebar-spec nav rows.
541
+ ### 2.3.2
542
+
543
+ - **Counts on `Tabs`, `PanelTabs`, `PillTabs` and `SegmentedControl`.** Tabs / options accept `{ label, count?, showZero? }`; a count renders as a `CountBadge` 6px right of the label — `--count-h` tall (new tier token: sm 16 · md 18 · lg 20), 6px sides, full radius, 12px tabular figures; fill `--neutral-alpha-8` on any surface, text `--text-body`, `--text-heading` when selected. 0 or no count hides it; `showZero` (control or option) draws a muted 0. Selection is still matched by label. `CountBadge` is exported (React `Tabs.jsx`, Vue `CountBadge.vue`).
544
+ - Additive only.
545
+
519
546
  ### 2.3.1
520
547
 
521
548
  - **`Table` row open, tree rows and labels.** `onRowOpen(row, index, event)` (Vue `@row-open`) makes the icon tile and title of a two-line cell open the row (hand cursor; Enter / Space on the title). `tree` with nested `children` (or `hasChildren` for lazy rows) indents the two-line column 24px per level with an expand arrow — `expandedKeys` / `defaultExpandedKeys` / `onExpandChange(row, expanded, keys)` (Vue `v-model:expandedKeys`, `@expand-change`), `rowKey` (default `id`); sorting reorders siblings only. `labels: { sortAsc, sortDesc, sortClear, more, expand, collapse }` translates every word the table draws (English by default). Indices reported by selection and callbacks are depth-first over all rows — unchanged for flat tables.
@@ -17,6 +17,7 @@
17
17
  {
18
18
  "group": [
19
19
  "assets/icons/**",
20
+ "changelog.js",
20
21
  "components/**",
21
22
  "components/_shared/**",
22
23
  "components/buttons/**",
@@ -29,6 +30,7 @@
29
30
  "components/navigation/**",
30
31
  "components/pipeline/**",
31
32
  "components/tabs/**",
33
+ "covers.js",
32
34
  "site-nav.js",
33
35
  "tokens/**"
34
36
  ],
@@ -403,6 +405,10 @@
403
405
  "selector": "JSXOpeningElement[name.name='Radio'] > JSXAttribute[name.name='size'] > Literal[value!=/^(?:sm|md|lg)$/]",
404
406
  "message": "<Radio> size must be one of 'sm' | 'md' | 'lg'."
405
407
  },
408
+ {
409
+ "selector": "JSXOpeningElement[name.name='RelativeTimeValue'] > JSXAttribute > JSXIdentifier[name!=/^(?:amount|unit|key|ref|className|style|children)$/]",
410
+ "message": "<RelativeTimeValue> doesn't accept that prop. Declared props: amount, unit."
411
+ },
406
412
  {
407
413
  "selector": "JSXOpeningElement[name.name='Ribbon3DSeries'] > JSXAttribute > JSXIdentifier[name!=/^(?:name|values|color|key|ref|className|style|children)$/]",
408
414
  "message": "<Ribbon3DSeries> doesn't accept that prop. Declared props: name, values, color."
@@ -428,8 +434,8 @@
428
434
  "message": "<SearchInput> size must be one of 'sm' | 'md' | 'lg'."
429
435
  },
430
436
  {
431
- "selector": "JSXOpeningElement[name.name='SegmentedControl'] > JSXAttribute > JSXIdentifier[name!=/^(?:options|value|onChange|size|iconOnly|disabled|fullWidth|style|key|ref|className|style|children)$/]",
432
- "message": "<SegmentedControl> doesn't accept that prop. Declared props: options, value, onChange, size, iconOnly, disabled, fullWidth, style."
437
+ "selector": "JSXOpeningElement[name.name='SegmentedControl'] > JSXAttribute > JSXIdentifier[name!=/^(?:options|value|onChange|size|iconOnly|disabled|fullWidth|showZero|style|key|ref|className|style|children)$/]",
438
+ "message": "<SegmentedControl> doesn't accept that prop. Declared props: options, value, onChange, size, iconOnly, disabled, fullWidth, showZero, style."
433
439
  },
434
440
  {
435
441
  "selector": "JSXOpeningElement[name.name='SegmentedControl'] > JSXAttribute[name.name='size'] > Literal[value!=/^(?:sm|md|lg)$/]",
@@ -444,8 +450,20 @@
444
450
  "message": "<Select> size must be one of 'sm' | 'md' | 'lg'."
445
451
  },
446
452
  {
447
- "selector": "JSXOpeningElement[name.name='SidebarItem'] > JSXAttribute > JSXIdentifier[name!=/^(?:icon|label|children|open|key|ref|className|style|children)$/]",
448
- "message": "<SidebarItem> doesn't accept that prop. Declared props: icon, label, children, open."
453
+ "selector": "JSXOpeningElement[name.name='SidebarItem'] > JSXAttribute > JSXIdentifier[name!=/^(?:icon|label|trailing|children|key|ref|className|style|children)$/]",
454
+ "message": "<SidebarItem> doesn't accept that prop. Declared props: icon, label, trailing, children."
455
+ },
456
+ {
457
+ "selector": "JSXOpeningElement[name.name='SidePanel'] > JSXAttribute > JSXIdentifier[name!=/^(?:open|title|actions|onClose|closeLabel|closeIcon|tone|color|width|size|children|style|key|ref|className|style|children)$/]",
458
+ "message": "<SidePanel> doesn't accept that prop. Declared props: open, title, actions, onClose, closeLabel, closeIcon, tone, color, width, size, children, style."
459
+ },
460
+ {
461
+ "selector": "JSXOpeningElement[name.name='SidePanel'] > JSXAttribute[name.name='tone'] > Literal[value!=/^(?:recessed|flush|contrast|custom)$/]",
462
+ "message": "<SidePanel> tone must be one of 'recessed' | 'flush' | 'contrast' | 'custom'."
463
+ },
464
+ {
465
+ "selector": "JSXOpeningElement[name.name='SidePanel'] > JSXAttribute[name.name='size'] > Literal[value!=/^(?:sm|md|lg)$/]",
466
+ "message": "<SidePanel> size must be one of 'sm' | 'md' | 'lg'."
449
467
  },
450
468
  {
451
469
  "selector": "JSXOpeningElement[name.name='Slider'] > JSXAttribute > JSXIdentifier[name!=/^(?:value|min|max|step|labels|showValue|unit|disabled|width|onChange|style|key|ref|className|style|children)$/]",
@@ -512,12 +530,8 @@
512
530
  "message": "<DataTableTwoLine> doesn't accept that prop. Declared props: icon, hue, description, mark, more."
513
531
  },
514
532
  {
515
- "selector": "JSXOpeningElement[name.name='Tabs'] > JSXAttribute > JSXIdentifier[name!=/^(?:tabs|active|onChange|size|style|key|ref|className|style|children)$/]",
516
- "message": "<Tabs> doesn't accept that prop. Declared props: tabs, active, onChange, size, style."
517
- },
518
- {
519
- "selector": "JSXOpeningElement[name.name='Tabs'] > JSXAttribute[name.name='size'] > Literal[value!=/^(?:sm|md|lg)$/]",
520
- "message": "<Tabs> size must be one of 'sm' | 'md' | 'lg'."
533
+ "selector": "JSXOpeningElement[name.name='TabItem'] > JSXAttribute > JSXIdentifier[name!=/^(?:label|count|showZero|key|ref|className|style|children)$/]",
534
+ "message": "<TabItem> doesn't accept that prop. Declared props: label, count, showZero."
521
535
  },
522
536
  {
523
537
  "selector": "JSXOpeningElement[name.name='Tag'] > JSXAttribute > JSXIdentifier[name!=/^(?:children|size|onRemove|tone|style|key|ref|className|style|children)$/]",
@@ -540,8 +554,8 @@
540
554
  "message": "<TimelineItem> tone must be one of 'success' | 'danger' | 'warning' | 'info' | 'neutral'."
541
555
  },
542
556
  {
543
- "selector": "JSXOpeningElement[name.name='TopBar'] > JSXAttribute > JSXIdentifier[name!=/^(?:placeholder|actions|onSearch|style|key|ref|className|style|children)$/]",
544
- "message": "<TopBar> doesn't accept that prop. Declared props: placeholder, actions, onSearch, style."
557
+ "selector": "JSXOpeningElement[name.name='TopBar'] > JSXAttribute > JSXIdentifier[name!=/^(?:placeholder|onSearch|actions|aiLabel|onAi|onNotifications|onHelp|notificationsLabel|helpLabel|user|userMenu|style|key|ref|className|style|children)$/]",
558
+ "message": "<TopBar> doesn't accept that prop. Declared props: placeholder, onSearch, actions, aiLabel, onAi, onNotifications, onHelp, notificationsLabel, helpLabel, user, userMenu, style."
545
559
  },
546
560
  {
547
561
  "selector": "JSXOpeningElement[name.name='TreeItem'] > JSXAttribute > JSXIdentifier[name!=/^(?:key|title|label|icon|hue|count|tag|children|disabled|disableCheckbox|selectable|checkable|isLeaf|key|ref|className|style|children)$/]",
@@ -775,6 +789,9 @@
775
789
  "Radio": {
776
790
  "replaces": []
777
791
  },
792
+ "RelativeTimeValue": {
793
+ "replaces": []
794
+ },
778
795
  "Ribbon3DSeries": {
779
796
  "replaces": []
780
797
  },
@@ -799,6 +816,9 @@
799
816
  "SidebarItem": {
800
817
  "replaces": []
801
818
  },
819
+ "SidePanel": {
820
+ "replaces": []
821
+ },
802
822
  "Slider": {
803
823
  "replaces": []
804
824
  },
@@ -826,7 +846,7 @@
826
846
  "DataTableTwoLine": {
827
847
  "replaces": []
828
848
  },
829
- "Tabs": {
849
+ "TabItem": {
830
850
  "replaces": []
831
851
  },
832
852
  "Tag": {
@@ -949,6 +969,7 @@
949
969
  "--control-px",
950
970
  "--control-text",
951
971
  "--control-tracking",
972
+ "--count-h",
952
973
  "--cyan-100",
953
974
  "--cyan-200",
954
975
  "--cyan-300",
@@ -1530,6 +1551,7 @@
1530
1551
  "--cell-px": "spacing",
1531
1552
  "--check-size": "spacing",
1532
1553
  "--tag-h": "spacing",
1554
+ "--count-h": "spacing",
1533
1555
  "--switch-w": "spacing",
1534
1556
  "--switch-h": "spacing",
1535
1557
  "--title-cell-py": "spacing",
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -1,4 +1,4 @@
1
- <!-- @dsCard group="Components" viewport="700x510" name="App Chrome" subtitle="Sidebar · TopBar · PageToolbar · DocTree" -->
1
+ <!-- @dsCard group="Components" viewport="700x1490" name="App Chrome" subtitle="Sidebar tones · SidePanel · TopBar · UserMenu · PageToolbar" -->
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>
@@ -9,28 +9,40 @@
9
9
  <style>body{margin:0;background:var(--bg-app);font-family:var(--font-sans);overflow:hidden;box-sizing:border-box;padding:18px}</style></head>
10
10
  <body><div id="root"></div>
11
11
  <script type="text/babel">
12
- const { Sidebar, KEYBRICKS_NAV, TopBar, PageToolbar, DocTree, Button } = window.KeybricksDesignSystem_71231a;
12
+ const { Sidebar, SidePanel, TopBar, UserMenu, PageToolbar, SegmentedControl, SearchInput, Tree, Button, IconButton, SIDEBAR_COLORS, KEYBRICKS_MGMT } = window.KeybricksDesignSystem_71231a;
13
+ const LOGO = "../../assets/keybricks-logo.png";
13
14
  function Sheet(){
14
- const [active,setActive]=React.useState("Home");
15
- const [col,setCol]=React.useState(false);
16
- return <div style={{display:"flex",height:420,overflow:"hidden"}}>
17
- <Sidebar items={KEYBRICKS_NAV} active={active} onSelect={setActive} collapsed={col} onToggleCollapse={()=>setCol(!col)} logo="../../assets/keybricks-logo.png"/>
18
- <DocTree active="Financial Analysis" items={[
19
- {label:"Q1 2026",kind:"folder"},{label:"Q4 2025",kind:"folder"},
20
- {label:"Financial Analysis",hue:"orange"},{label:"Revenue Analysis",hue:"green"},
21
- {label:"Budget 2026",kind:"folder"},{label:"Monthly Journal",hue:"purple",icon:"book-open"},{label:"Variance Report",hue:"blue"},
22
- ]}/>
23
- <div style={{flex:1,display:"flex",flexDirection:"column",minWidth:0}}>
24
- <TopBar/>
25
- <PageToolbar title="Oracle Adapter" onBack={()=>{}} actions={<React.Fragment>
26
- <Button variant="ghost" icon="settings-2">Custom Parameters</Button>
27
- <Button variant="ghost" icon="play">Run Test</Button>
28
- <Button icon="send">Submit</Button>
29
- </React.Fragment>}/>
30
- <div style={{flex:1}}></div>
31
- </div>
15
+ const [active,setActive]=React.useState("Home"),[col,setCol]=React.useState(true),[panel,setPanel]=React.useState(null),[tab,setTab]=React.useState("All"),[theme,setTheme]=React.useState("System");
16
+ const pick=(l)=>{ if(l==="Documents"||l==="Management"){ setPanel(panel===l?null:l); } else { setActive(l); setPanel(null); } };
17
+ return <div style={{display:"flex",flexDirection:"column",gap:28}}>
18
+ <section><div className="kb-h">Shell — Sidebar (recessed) · SidePanel · TopBar</div>
19
+ <div style={{overflowX:"auto",border:"1px solid var(--border-card)",borderRadius:"var(--radius-lg)"}}><div style={{display:"flex",height:580,minWidth:800,overflow:"hidden"}}>
20
+ <Sidebar logo={LOGO} version="Version 2.4.0" active={panel||active} onSelect={pick} collapsed={col||!!panel} onToggleCollapse={()=>setCol(!col)}/>
21
+ <SidePanel open={!!panel} title={panel} size="sm" onClose={()=>setPanel(null)} actions={panel==="Documents"?<IconButton icon="maximize-2" muted label="Open page"/>:null}>
22
+ {panel==="Documents"?<div style={{display:"flex",flexDirection:"column",gap:8,padding:"0 16px 8px"}}>
23
+ <SegmentedControl fullWidth options={["Recent","Favorites","All"]} value={tab} onChange={setTab}/>
24
+ <SearchInput bare placeholder="Search documents..."/>
25
+ <Tree showIcon defaultExpanded={["b"]} items={[{key:"b",title:"Budget 2026",children:[{key:"b1",title:"Opex by Department"},{key:"b2",title:"Headcount Plan"}]},{key:"c",title:"Consolidation"}]}/>
26
+ </div>:<div style={{display:"flex",flexDirection:"column",gap:2,padding:8}}>{KEYBRICKS_MGMT.map(g=><div key={g.label} style={{height:36,display:"flex",alignItems:"center",gap:10,padding:"0 10px",fontSize:"var(--text-base)",color:"var(--text-body)"}}>{g.label}</div>)}</div>}
27
+ </SidePanel>
28
+ <div style={{flex:1,display:"flex",flexDirection:"column",minWidth:0}}>
29
+ <TopBar user={{name:"Felix Wang",email:"felix@keyfunc.com"}} userMenu={<UserMenu name="Felix Wang" email="felix@keyfunc.com" theme={theme} onThemeChange={setTheme} items={[{label:"Profile",icon:"user"},{label:"Settings",icon:"settings"},{divider:true},{label:"Sign out",icon:"log-out",danger:true}]}/>}/>
30
+ <PageToolbar title="Oracle Adapter" onBack={()=>{}} actions={<Button icon="send">Submit</Button>}/>
31
+ <div style={{flex:1,background:"var(--bg-app)"}}></div>
32
+ </div>
33
+ </div>
34
+ </div>
35
+ <div className="note">The shell is 800px wide and scrolls sideways in this card; it starts with the rail collapsed — expand it with the first item. Documents and Management carry a trailing chevron: they open a SidePanel, and the rail collapses to 64px while it is open. The user trigger opens a UserMenu with the appearance switch.</div>
36
+ </section>
37
+ <section><div className="kb-h">Tones — tone="recessed" (default) · "flush" · "contrast" · "custom"</div>
38
+ <div style={{display:"flex",gap:12,flexWrap:"wrap"}}>
39
+ {[["recessed"],["flush"],["contrast"],["custom","navy"],["custom","forest"],["custom","plum"]].map(([t,cl])=><div key={t+(cl||"")} style={{display:"flex",flexDirection:"column",gap:6}}><span className="lbl">{t}{cl?" · "+cl:""}</span>
40
+ <div style={{height:300,overflow:"hidden",border:"1px solid var(--border-card)",borderRadius:"var(--radius-md)"}}><Sidebar logo={LOGO} tone={t} color={cl} active="Documents" version="Version 2.4.0" items={[{icon:"house",label:"Home"},{icon:"file-text",label:"Documents",trailing:true},{icon:"database",label:"Data Integration"}]} footerItems={[{icon:"settings",label:"Management",trailing:true}]} style={{width:200}}/></div></div>)}
41
+ </div>
42
+ <div className="note">Custom colours: {Object.keys(SIDEBAR_COLORS).join(" · ")} — or any hex. Contrast and custom rails are scoped to the dark theme, so the components inside follow.</div>
43
+ </section>
32
44
  </div>;
33
45
  }
34
46
  ReactDOM.createRoot(document.getElementById("root")).render(<Sheet/>);
35
- if (window.KBViewSource) setTimeout(function(){ KBViewSource.mount("#root", [{"name":"Sidebar.jsx","path":"../navigation/Sidebar.jsx"},{"name":"TopBar.jsx","path":"../navigation/TopBar.jsx"},{"name":"DocTree.jsx","path":"../navigation/DocTree.jsx"},{"name":"Button.jsx","path":"../buttons/Button.jsx"}]); }, 0);
47
+ if (window.KBViewSource) setTimeout(function(){ KBViewSource.mount("#root", [{"name":"Sidebar.jsx","path":"../navigation/Sidebar.jsx"},{"name":"SidePanel.jsx","path":"../navigation/SidePanel.jsx"},{"name":"TopBar.jsx","path":"../navigation/TopBar.jsx","vue":"../navigation/TopBar.vue","vueName":"TopBar.vue"},{"name":"UserMenu","path":"../navigation/TopBar.jsx","vue":"../navigation/UserMenu.vue","vueName":"UserMenu.vue"},{"name":"chrome-tones.js","path":"../navigation/chrome-tones.js","reactOnly":false}]); }, 0);
36
48
  </script></body></html>
@@ -1,4 +1,4 @@
1
- <!-- @dsCard group="Components" viewport="700x920" name="Context Menu" subtitle="Icons · plain text · danger item · long list" -->
1
+ <!-- @dsCard group="Components" viewport="700x920" name="Context Menu" subtitle="Action vs single-choice spacing · selected state · icons · plain text · danger item · long list" -->
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>
@@ -12,7 +12,12 @@
12
12
  <script type="text/babel">
13
13
  const { ContextMenu } = window.KeybricksDesignSystem_71231a;
14
14
  const { KBTiers: Tiers } = window;
15
- function Sheet(){return <div className="grid">
15
+ function Sheet(){const [sec,setSec]=React.useState("Overview");const [lang,setLang]=React.useState("English");return <div className="grid">
16
+ <div style={{flexBasis:"100%",display:"flex",flexDirection:"column",gap:8}}><span className="lbl">Row spacing — an action menu keeps its rows flush; a single-choice menu (one item <code>selected</code>) spaces them 2px apart, so a hovered row never touches the selected one</span><div style={{display:"flex",gap:20,alignItems:"flex-start",flexWrap:"wrap"}}>
17
+ <div className="col"><span className="lbl">Action menu · gap 0</span><ContextMenu items={[{label:"Edit",icon:"edit-2"},{label:"Duplicate",icon:"copy"},{label:"Export",icon:"download"},{divider:true},{label:"Delete",icon:"trash-2",danger:true}]}/></div>
18
+ <div className="col"><span className="lbl">Single choice · gap 2 · no checkmark</span><ContextMenu items={["Overview","Data Sources","Pipelines","Reports"].map(l=>({label:l,selected:l===sec}))} onSelect={setSec}/></div>
19
+ <div className="col"><span className="lbl">Language picker</span><ContextMenu items={[{label:"English",selected:lang==="English"},{label:"简体中文",selected:lang==="简体中文"},{label:"日本語",selected:lang==="日本語"},{divider:true},{label:"Follow system",icon:"monitor"}]} onSelect={(l)=>l!=="Follow system"&&setLang(l)}/></div>
20
+ </div></div>
16
21
  <div className="col"><span className="lbl">With icons</span><ContextMenu items={[{label:"Preview",icon:"eye"},{label:"Duplicate",icon:"copy"},{divider:true},{label:"Delete",icon:"trash-2",danger:true}]}/></div>
17
22
  <div className="col"><span className="lbl">Plain text, no icons</span><ContextMenu items={[{label:"Rename"},{label:"Move to..."},{label:"Archive"}]}/></div>
18
23
  <div className="col"><span className="lbl">Danger only</span><ContextMenu items={[{label:"Remove from workflow",icon:"x",danger:true}]}/></div>
@@ -0,0 +1,88 @@
1
+ <!-- @dsCard group="Icons" viewport="700x2250" name="Icon Usage" subtitle="Monochrome glyphs · category tiles · document hues · status · marks · icons inside components — when an icon gets colour" -->
2
+ <!DOCTYPE html>
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
+ <script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
5
+ <script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" integrity="sha384-u6aeetuaXnQ38mYT8rp6sbXaQe3NL9t+IBXmnYxwkUI2Hw4bsp2Wvmx4yRQF1uAm" crossorigin="anonymous"></script>
6
+ <script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
7
+ <script src="../../_ds_bundle.js"></script>
8
+ <script src="../_shared/card-kit.js"></script>
9
+ <script src="../_shared/view-source.js"></script>
10
+ <style>body{margin:0;background:var(--bg-surface)}.cap{display:flex;flex-direction:column;align-items:center;gap:6px;min-width:56px}.cap span{font-size:var(--text-xs);line-height:var(--leading-xs);color:var(--text-secondary);white-space:nowrap}.rules{margin:0;padding-left:18px;display:flex;flex-direction:column;gap:6px;font-size:var(--text-base);line-height:var(--leading-base);color:var(--text-body)}.rules code{font-size:12px;white-space:nowrap}</style></head>
11
+ <body><div id="root"></div>
12
+ <script type="text/babel">
13
+ const { Icon, IconTile, StatusBadge, Alert, Button, IconButton, PaletteItem, ListRow, ActivityItem, DocTree } = window.KeybricksDesignSystem_71231a;
14
+ const LEAF = [["orange", "var(--cat-transform)"], ["green", "var(--cat-source)"], ["purple", "var(--cat-merge)"], ["blue", "var(--info-fg)"], ["teal", "var(--cat-output)"], ["gray", "var(--text-muted)"]];
15
+ const HUES = [["source", "table"], ["join", "shuffle"], ["transform", "funnel"], ["merge", "merge"], ["output", "layers"]];
16
+ const Cap = ({ label, children }) => <div className="cap">{children}<span>{label}</span></div>;
17
+ const Glyph = ({ name, color, label }) => <Cap label={label}><span style={{ color, display: "inline-flex" }}><Icon name={name} size={20} /></span></Cap>;
18
+ function Sheet() {
19
+ return <div className="kb-sheet">
20
+ <KBSec title="1 · Monochrome glyph — the default" note="Almost every icon is a single-colour stroke glyph that takes the colour of its text. Colour comes from the text token, never from the icon itself.">
21
+ <KBRow k="text colour">
22
+ <Glyph name="file-text" color="var(--text-heading)" label="heading" />
23
+ <Glyph name="file-text" color="var(--text-body)" label="body" />
24
+ <Glyph name="file-text" color="var(--text-secondary)" label="secondary" />
25
+ <Glyph name="file-text" color="var(--text-muted)" label="muted / disabled" />
26
+ <Glyph name="file-text" color="var(--action-primary)" label="action" />
27
+ </KBRow>
28
+ <KBRow k="on a fill"><Button icon="download">Export</Button><Button variant="dark" icon="ai-sparkles">AI Assistant</Button><Button variant="danger" icon="trash-2">Delete</Button><IconButton icon="ellipsis" label="More" /></KBRow>
29
+ </KBSec>
30
+ <KBSec title="2 · Category tile — IconTile" note="The coloured square is a category, not decoration: each hue is one node type (--cat-*-bg behind --cat-* glyph). Dark mode swaps both for a low-alpha wash automatically.">
31
+ <KBRow k="palette · 40 r14">{HUES.map(([h, i]) => <Cap key={h} label={h}><IconTile icon={i} hue={h} /></Cap>)}</KBRow>
32
+ <KBRow k="node · 48 r16">{HUES.map(([h, i]) => <Cap key={h} label={h}><IconTile icon={i} hue={h} kind="node" /></Cap>)}</KBRow>
33
+ <KBRow k="row · 32 r8">
34
+ <Cap label="hue"><IconTile icon="table" hue="join" kind="row" /></Cap>
35
+ <Cap label="hue"><IconTile icon="box" hue="merge" kind="row" /></Cap>
36
+ <Cap label="neutral"><IconTile icon="folder" hue="neutral" kind="row" /></Cap>
37
+ <Cap label="no hue (hairline)"><IconTile icon="file-text" kind="row" /></Cap>
38
+ </KBRow>
39
+ </KBSec>
40
+ <KBSec title="3 · Document glyph — Tree / DocTree hue" note="The one place a bare glyph is coloured: a document leaf in Tree or DocTree takes a hue that tells the document type apart. Folders stay muted. Pass hue on the item; the component maps it to the token.">
41
+ <KBRow k="leaf hue">{LEAF.map(([h, v]) => <Cap key={h} label={h}><span style={{ color: v, display: "inline-flex" }}><Icon name="file-text" size={16} /></span></Cap>)}</KBRow>
42
+ <KBRow k="DocTree" style={{ display: "block" }}><div style={{ width: 240, border: "1px solid var(--border-card)", borderRadius: "var(--radius-lg)", overflow: "hidden" }}><DocTree title="Documents" active="Revenue Analysis" items={[{ label: "Q1 2026", kind: "folder" }, { label: "Revenue Analysis", hue: "green" }, { label: "Cost Allocation", hue: "orange" }, { label: "FX Rates Model", hue: "purple" }, { label: "Cash Flow Forecast", hue: "blue" }, { label: "Notes", hue: "gray" }]} /></div></KBRow>
43
+ </KBSec>
44
+ <KBSec title="4 · Status" note="Status colour is reserved for state: success, warning, danger, info, pending. The glyph sits in a solid circle (StatusBadge) or leads a tinted message (Alert).">
45
+ <KBRow k="StatusBadge">
46
+ <Cap label="success"><StatusBadge tone="success" icon="check" /></Cap>
47
+ <Cap label="danger"><StatusBadge tone="danger" icon="x" /></Cap>
48
+ <Cap label="warning"><StatusBadge tone="warning" icon="bell" /></Cap>
49
+ <Cap label="pending"><StatusBadge tone="pending" icon="clock" /></Cap>
50
+ <Cap label="info"><StatusBadge tone="info" icon="info" /></Cap>
51
+ <Cap label="node dot · 13"><StatusBadge tone="danger" size={13} /></Cap>
52
+ </KBRow>
53
+ <KBRow k="Alert" style={{ flexDirection: "column", alignItems: "stretch", gap: 8 }}>
54
+ <Alert tone="info" title="Import scheduled">Runs daily at 06:00.</Alert>
55
+ <Alert tone="success" title="Sync complete">312 rows updated.</Alert>
56
+ <Alert tone="warning" title="Mapping incomplete">2 accounts have no target.</Alert>
57
+ <Alert tone="danger" title="Connection failed">The Oracle source did not respond.</Alert>
58
+ </KBRow>
59
+ </KBSec>
60
+ <KBSec title="5 · Marks" note="A 14px mark next to a title flags one property of the row. Favourite is the only filled glyph in the system.">
61
+ <KBRow k="favourite">
62
+ <span style={{ display: "inline-flex", alignItems: "center", gap: 4, fontSize: "var(--text-base)", color: "var(--text-heading)" }}>FX Rates Model<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" strokeWidth="2" strokeLinejoin="round" style={{ color: "var(--warning)" }}><path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z" /></svg></span>
63
+ <span style={{ fontSize: "var(--text-xs)", color: "var(--text-secondary)" }}>Table <code>mark: "favorite"</code> · --warning</span>
64
+ </KBRow>
65
+ <KBRow k="AI">
66
+ <Glyph name="ai" color="var(--action-primary)" label="ai" />
67
+ <Glyph name="ai-sparkles" color="var(--action-primary)" label="ai-sparkles" />
68
+ </KBRow>
69
+ </KBSec>
70
+ <KBSec title="6 · Inside components" note="These components choose the tile and colour for you — pass an icon and, where it applies, a hue.">
71
+ <KBRow k="PaletteItem"><div style={{ display: "flex", gap: 8 }}><PaletteItem icon="table" hue="source" label="Detail Sheet" /><PaletteItem icon="shuffle" hue="join" label="Association Match" /><PaletteItem icon="funnel" hue="transform" label="Filter" /><PaletteItem icon="layers" hue="output" label="Output" /></div></KBRow>
72
+ <KBRow k="ActivityItem" style={{ display: "block" }}><ActivityItem icon="upload" title="Opening balances imported" meta="Felix Wang · 09:10" action="Open" /></KBRow>
73
+ <KBRow k="ListRow" style={{ display: "block" }}><ListRow icon="workflow" title="Month-end close" meta="6 steps" trailing="6" /></KBRow>
74
+ </KBSec>
75
+ <KBSec title="Rules">
76
+ <ul className="rules">
77
+ <li>Default to a monochrome glyph in the text's own colour. Add colour only when it means something.</li>
78
+ <li>Category hue → <code>IconTile</code> (or a component that uses it). A bare glyph gets a hue only as a document leaf, through the <code>hue</code> of a Tree or DocTree item.</li>
79
+ <li>Status colour only for state, always with a shape (circle, tinted message) — never colour alone.</li>
80
+ <li>Tile and glyph always come as a pair: <code>--cat-x-bg</code> behind <code>--cat-x</code>. Don't mix hues or invent tints.</li>
81
+ <li>Glyphs on a solid fill (buttons, status badges) use <code>--on-accent</code>; components do this for you.</li>
82
+ </ul>
83
+ </KBSec>
84
+ </div>;
85
+ }
86
+ ReactDOM.createRoot(document.getElementById("root")).render(<Sheet />);
87
+ if (window.KBViewSource) setTimeout(function () { KBViewSource.mount("#root", [{ "name": "Badge.jsx (IconTile · StatusBadge)", "path": "../display/Badge.jsx", "vue": "../display/IconTile.vue", "vueName": "IconTile.vue" }, { "name": "Icon.jsx", "path": "../icons/Icon.jsx" }]); }, 0);
88
+ </script></body></html>