@keyfunc/keybricks-ds 2.4.8 → 2.4.11

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 (38) hide show
  1. package/DEPLOY.md +10 -7
  2. package/INTEGRATION.md +17 -1
  3. package/assets/fonts/inter/InterVariable-Italic.woff2 +0 -0
  4. package/assets/fonts/inter/InterVariable.woff2 +0 -0
  5. package/assets/fonts/inter/LICENSE.txt +92 -0
  6. package/assets/fonts/inter/README.md +13 -0
  7. package/changelog.js +95 -0
  8. package/components/_cards/smart-table.card.html +3 -1
  9. package/components/_shared/src-data.js +1 -1
  10. package/components/data/SmartTable.d.ts +5 -0
  11. package/components/data/SmartTable.jsx +5 -2
  12. package/components/data/SmartTable.vue +4 -4
  13. package/covers.js +104 -0
  14. package/gallery.html +133 -0
  15. package/guidelines/_color-lab.js +46 -0
  16. package/guidelines/brand-logo.html +9 -0
  17. package/guidelines/colors-list.html +86 -0
  18. package/guidelines/colors-palette.html +85 -0
  19. package/guidelines/colors-schemes.html +68 -0
  20. package/guidelines/colors-transparency.html +59 -0
  21. package/guidelines/foundations-borders.html +10 -0
  22. package/guidelines/foundations-chrome.html +17 -0
  23. package/guidelines/foundations-motion.html +17 -0
  24. package/guidelines/foundations-radii.html +14 -0
  25. package/guidelines/foundations-size-tiers.html +88 -0
  26. package/guidelines/foundations-spacing.html +19 -0
  27. package/guidelines/interaction-states.html +25 -0
  28. package/guidelines/type-extended.html +11 -0
  29. package/guidelines/type-mono.html +10 -0
  30. package/guidelines/type-scale.html +15 -0
  31. package/guidelines/type-weights.html +12 -0
  32. package/index.html +597 -0
  33. package/package.json +8 -2
  34. package/readme.md +1 -1
  35. package/readme.zh-CN.md +1 -1
  36. package/site-nav.js +1 -0
  37. package/tokens/fonts.css +19 -4
  38. package/tokens/hover.css +2 -0
@@ -18,6 +18,7 @@ export interface SmartTableField {
18
18
  }
19
19
 
20
20
  /** Notion/Airtable-style database grid: field-type header icons, hue pills, person/date/checkbox/url/progress cells, row hover + expand, inline editing, + New row, footer aggregates, view toolbar.
21
+ * The "+ New" row is the system's add-row: --action-primary text and 13px plus at --weight-medium; hover --bg-inset with --action-primary-hover; a focusable role="button" (Enter / Space).
21
22
  * Frozen edges like Spreadsheet: the header row sticks to the top and the row-number column to the left of the scroll area (give `height` for vertical scrolling). */
22
23
  export interface SmartTableProps {
23
24
  fields?: SmartTableField[];
@@ -40,6 +41,10 @@ export interface SmartTableProps {
40
41
  columnWidths?: Record<string, number>;
41
42
  /** Fires on every drag frame with the new px width. Vue: `@column-resize`. */
42
43
  onColumnResize?: (key: string, width: number) => void;
44
+ /** Pin the "+ New" row to the bottom of the scroll area while the body scrolls (needs `height`): position sticky, --bg-surface fill, 1px --border-subtle top rule. Default false. */
45
+ stickyAddRow?: boolean;
46
+ /** UI strings. newRow — the add-row label (default "New"). */
47
+ labels?: { newRow?: string };
43
48
  /** Size tier of the grid region. Defaults to "sm" — a database grid is a dense surface (36px rows, 12px text, 28px toolbar buttons). */
44
49
  size?: "sm" | "md" | "lg";
45
50
  onChange?: (rows: object[]) => void;
@@ -21,7 +21,7 @@ function ResizeHandle({ onMouseDown, onEnter, onLeave }) {
21
21
  }
22
22
  const EDGE_LIT = { borderRight: "1px solid var(--action-primary)", boxShadow: "inset -1px 0 0 var(--action-primary)" }; // overrides the borderRight shorthand (a longhand would leave currentColor behind on leave)
23
23
  const DENSITY = { default: { h: "var(--bar-h)", px: "var(--cell-px)" }, compact: { h: "var(--control-h)", px: "8px" } };
24
- export function SmartTable({ fields = [], rows = [], title = "Grid view", toolbar = true, height, rowHeight, density: densityProp, defaultDensity = "default", onDensityChange, resizable = true, columnWidths, onColumnResize, size = "sm", onChange, style }) {
24
+ export function SmartTable({ fields = [], rows = [], title = "Grid view", toolbar = true, height, rowHeight, density: densityProp, defaultDensity = "default", onDensityChange, resizable = true, columnWidths, onColumnResize, stickyAddRow = false, labels, size = "sm", onChange, style }) {
25
25
  const [data, setData] = React.useState(rows);
26
26
  const [densityState, setDensityState] = React.useState(defaultDensity);
27
27
  const density = DENSITY[densityProp || densityState] ? (densityProp || densityState) : "default";
@@ -48,6 +48,7 @@ export function SmartTable({ fields = [], rows = [], title = "Grid view", toolba
48
48
  const grid = { display: "grid", gridTemplateColumns: widths.join(" "), width: "fit-content", minWidth: "100%" };
49
49
  const cellBase = { display: "flex", alignItems: "center", gap: 5, padding: "0 " + DENSITY[density].px, borderRight: "1px solid var(--border-subtle)", borderBottom: "1px solid var(--border-subtle)", height: rowH, boxSizing: "border-box", overflow: "hidden", whiteSpace: "nowrap", fontFamily: "var(--font-sans)", fontSize: "var(--control-text)", lineHeight: "var(--control-leading)", letterSpacing: "var(--control-tracking)", color: "var(--text-primary)" };
50
50
  const tierIcon = { width: "var(--control-icon)", height: "var(--control-icon)" };
51
+ const addRow = () => { const empty = {}; fields.forEach(f => empty[f.key] = f.type === "multiSelect" ? [] : f.type === "checkbox" ? false : ""); const next = [...data, empty]; setData(next); onChange && onChange(next); };
51
52
  // Frozen edges — zIndex 1 (row-number column) < 2 (header row) < 3 (corner, both axes); the editing input sits inside a body cell, under them.
52
53
  const stickyTop = { position: "sticky", top: 0, zIndex: 2, background: "var(--bg-inset)" };
53
54
  const stickyLeft = { position: "sticky", left: 0, zIndex: 1, background: "var(--bg-inset)" };
@@ -97,7 +98,9 @@ export function SmartTable({ fields = [], rows = [], title = "Grid view", toolba
97
98
  ...fields.map(f => <span key={r + f.key} onMouseEnter={() => setHover(r)} onMouseLeave={() => setHover(-1)} onDoubleClick={() => { if (f.type === "text" || f.type === "number" || f.type === "date" || f.type === "url") { setEditing({ r, key: f.key }); setDraft(String(row[f.key] == null ? "" : row[f.key])); } }} style={{ ...cellBase, position: "relative", background: hover === r ? "var(--bg-inset)" : "transparent" }}>{renderCell(f, row, r)}</span>),
98
99
  <span key={"a" + r} style={{ ...cellBase, background: hover === r ? "var(--bg-inset)" : "transparent" }}></span>,
99
100
  ])}
100
- <button type="button" onClick={() => { const empty = {}; fields.forEach(f => empty[f.key] = f.type === "multiSelect" ? [] : f.type === "checkbox" ? false : ""); const next = [...data, empty]; setData(next); onChange && onChange(next); }} style={{ gridColumn: "1 / -1", display: "flex", alignItems: "center", gap: 6, height: rowH, padding: "0 var(--edge-pad)", border: "none", borderBottom: "1px solid var(--border-subtle)", background: "transparent", cursor: "pointer", color: "var(--text-secondary)", fontFamily: "var(--font-sans)", fontSize: "var(--control-text)", letterSpacing: "var(--control-tracking)", textAlign: "left" }} onMouseEnter={(e) => e.currentTarget.style.background = "var(--bg-inset)"} onMouseLeave={(e) => e.currentTarget.style.background = "transparent"}><Icon name="plus" size={13} />New</button>
101
+ {/* Add row — the same at the end of every table: --action-primary text + 13px plus, medium weight; hover --bg-inset with --action-primary-hover (tokens/hover.css [data-addrow]).
102
+ A focusable role="button": Enter / Space add a row; the global focus ring is drawn inset so the scroll area never clips it. stickyAddRow pins it to the bottom of the scroll area. */}
103
+ <span role="button" tabIndex={0} data-addrow="" onClick={addRow} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); addRow(); } }} style={{ gridColumn: "1 / -1", display: "flex", alignItems: "center", gap: 6, height: rowH, padding: "0 var(--edge-pad)", boxSizing: "border-box", borderBottom: "1px solid var(--border-subtle)", background: `var(--addrow-bg, ${stickyAddRow ? "var(--bg-surface)" : "transparent"})`, color: "var(--addrow-fg, var(--action-primary))", cursor: "pointer", fontFamily: "var(--font-sans)", fontSize: "var(--control-text)", lineHeight: "var(--control-leading)", letterSpacing: "var(--control-tracking)", fontWeight: "var(--weight-medium)", whiteSpace: "nowrap", outlineOffset: -2, transition: "var(--t-hover)", ...(stickyAddRow ? { position: "sticky", bottom: 0, zIndex: 2, borderTop: "1px solid var(--border-subtle)" } : null) }}><Icon name="plus" size={13} style={{ width: 13, height: 13, flexShrink: 0 }} />{(labels && labels.newRow) || "New"}</span>
101
104
  <span style={{ ...cellBase, ...stickyLeft, borderBottom: "none" }}></span>
102
105
  {fields.map(f => <span key={"g" + f.key} style={{ ...cellBase, borderBottom: "none", background: "var(--bg-inset)", justifyContent: "flex-end", fontFamily: "var(--font-sans)", fontVariantNumeric: "tabular-nums", fontSize: "var(--text-xs)", letterSpacing: 0, color: "var(--text-secondary)" }}>{agg(f)}</span>)}
103
106
  <span style={{ ...cellBase, borderBottom: "none", background: "var(--bg-inset)" }}></span>
@@ -8,7 +8,7 @@ import SearchInput from '../forms/SearchInput.vue'
8
8
  import Menu from '../feedback/Menu.vue'
9
9
  // Notion/Airtable-style database grid: field-type header icons, hue pills, person/date/checkbox/url/progress cells, row hover + expand, inline editing, + New row, footer aggregates, view toolbar.
10
10
  const typeIcon = { text: 'type', longText: 'align-left', number: 'hash', select: 'chevron-down', multiSelect: 'tag', date: 'calendar', person: 'user', checkbox: 'check-square', url: 'link', progress: 'bar-chart-2' }
11
- const props = defineProps({ fields: { type: Array, default: () => [] }, rows: { type: Array, default: () => [] }, title: { type: String, default: 'Grid view' }, toolbar: { type: Boolean, default: true }, height: [Number, String], rowHeight: [Number, String], density: String, defaultDensity: { type: String, default: 'default' }, resizable: { type: Boolean, default: true }, columnWidths: Object, size: { type: String, default: 'sm' }, style: Object })
11
+ const props = defineProps({ fields: { type: Array, default: () => [] }, rows: { type: Array, default: () => [] }, title: { type: String, default: 'Grid view' }, toolbar: { type: Boolean, default: true }, height: [Number, String], rowHeight: [Number, String], density: String, defaultDensity: { type: String, default: 'default' }, resizable: { type: Boolean, default: true }, columnWidths: Object, stickyAddRow: Boolean, labels: Object, size: { type: String, default: 'sm' }, style: Object })
12
12
  // A size region that defaults to sm: a database grid is a dense surface, so rows are the sm bar height (36), cells and toolbar read the sm control text (12px, 13px in CJK) and the toolbar's buttons are 28px. Pass size="md" for a roomier grid.
13
13
  // density: 'default' rows are --bar-h (36 / 40 / 48) with --cell-px; 'compact' rows are --control-h (28 / 32 / 40, the Spreadsheet's cell height) with the Spreadsheet's 8px padding (so at sm only the height changes). Header and footer follow. The toolbar's "Row height" menu switches it (emits density-change; v-model:density to control); rowHeight (a raw height) overrides density.
14
14
  // Frozen edges (like Spreadsheet): the field header row is sticky to the top and the 40px row-number column to the left; the corner cell is sticky on both axes. Sticky cells keep the --bg-inset fill so body cells never show through, and the frozen edge is a --border-subtle hairline.
@@ -44,6 +44,7 @@ const cellBase = computed(() => ({ display: 'flex', alignItems: 'center', gap: '
44
44
  function setCell(r, key, v) { data.value[r][key] = v; emit('change', data.value) }
45
45
  function commitEdit(r, f) { setCell(r, f.key, f.type === 'number' && draft.value !== '' && !isNaN(Number(draft.value)) ? Number(draft.value) : draft.value); editing.value = null }
46
46
  function startEdit(r, f, row) { if (['text', 'number', 'date', 'url'].includes(f.type || 'text')) { editing.value = { r, key: f.key }; draft.value = String(row[f.key] == null ? '' : row[f.key]) } }
47
+ const addRowStyle = computed(() => ({ gridColumn: '1 / -1', display: 'flex', alignItems: 'center', gap: '6px', height: rowH.value, padding: '0 var(--edge-pad)', boxSizing: 'border-box', borderBottom: '1px solid var(--border-subtle)', background: `var(--addrow-bg, ${props.stickyAddRow ? 'var(--bg-surface)' : 'transparent'})`, color: 'var(--addrow-fg, var(--action-primary))', cursor: 'pointer', fontFamily: 'var(--font-sans)', fontSize: 'var(--control-text)', lineHeight: 'var(--control-leading)', letterSpacing: 'var(--control-tracking)', fontWeight: 'var(--weight-medium)', whiteSpace: 'nowrap', outlineOffset: '-2px', transition: 'var(--t-hover)', ...(props.stickyAddRow ? { position: 'sticky', bottom: 0, zIndex: 2, borderTop: '1px solid var(--border-subtle)' } : null) }))
47
48
  function addRow() { const empty = {}; props.fields.forEach(f => empty[f.key] = f.type === 'multiSelect' ? [] : f.type === 'checkbox' ? false : ''); data.value.push(empty); emit('change', data.value) }
48
49
  function agg(f) {
49
50
  if (!f.aggregate) return ''
@@ -86,7 +87,8 @@ function agg(f) {
86
87
  </span>
87
88
  <span :style="{ ...cellBase, height: rowH, background: hover === r ? 'var(--bg-inset)' : 'transparent' }" />
88
89
  </template>
89
- <button type="button" @click="addRow" class="kb-st-new" :style="{ gridColumn: '1 / -1', height: rowH }"><Icon name="plus" :size="14" :style="tierIcon" />New</button>
90
+ <!-- Add row: --action-primary + 13px plus, medium; hover --bg-inset / --action-primary-hover (tokens/hover.css [data-addrow]); focusable role="button", Enter / Space add; stickyAddRow pins it to the bottom -->
91
+ <span role="button" tabindex="0" data-addrow="" @click="addRow" @keydown.enter.prevent="addRow" @keydown.space.prevent="addRow" :style="addRowStyle"><Icon name="plus" :size="13" :style="{ width: '13px', height: '13px', flexShrink: 0 }" />{{ (labels && labels.newRow) || 'New' }}</span>
90
92
  <span :style="{ ...cellBase, height: rowH, ...stickyLeft, borderBottom: 'none' }" />
91
93
  <span v-for="f in fields" :key="'g' + f.key" :style="{ ...cellBase, height: rowH, borderBottom: 'none', background: 'var(--bg-inset)', justifyContent: 'flex-end', fontFamily: 'var(--font-sans)', fontVariantNumeric: 'tabular-nums', fontSize: 'var(--text-xs)', letterSpacing: 0, color: 'var(--text-secondary)' }">{{ agg(f) }}</span>
92
94
  <span :style="{ ...cellBase, height: rowH, borderBottom: 'none', background: 'var(--bg-inset)' }" />
@@ -98,6 +100,4 @@ function agg(f) {
98
100
  .kb-pill{display:inline-flex;align-items:center;height:20px;padding:0 8px;border-radius:var(--radius-full);font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-xs);line-height:1;white-space:nowrap}
99
101
  .kb-st-btn{display:inline-flex;align-items:center;gap:5px;height:var(--control-h);padding:0 var(--control-px);border:none;border-radius:var(--radius-md);background:transparent;cursor:pointer;color:var(--text-body);font-family:var(--font-sans);font-size:var(--control-text);letter-spacing:var(--control-tracking)}
100
102
  .kb-st-btn:hover{background:var(--bg-hover)}
101
- .kb-st-new{display:flex;align-items:center;gap:6px;padding:0 var(--edge-pad);border:none;border-bottom:1px solid var(--border-subtle);background:transparent;cursor:pointer;color:var(--text-secondary);font-family:var(--font-sans);font-size:var(--control-text);letter-spacing:var(--control-tracking);text-align:left}
102
- .kb-st-new:hover{background:var(--bg-inset)}
103
103
  </style>
package/covers.js ADDED
@@ -0,0 +1,104 @@
1
+ /* Thumbnail covers for the documentation home page: the simplest form of each component, or one telling part of it.
2
+ Keyed by card file stem. Each entry is [scale, render(h, K)] — K is the design-system namespace. A cover that throws
3
+ falls back to the component's name (see renderTiles in index.html). Covers are inert: no pointer events, no focus. */
4
+ window.KB_COVERS = (function () {
5
+ const L = (n, a = 0) => Array.from({ length: n }, (_, i) => Math.round(50 + 30 * Math.sin(i / 1.6 + a) + i * 4));
6
+ const M = ["Jan", "Feb", "Mar", "Apr", "May", "Jun"];
7
+ const col = (h, gap, kids) => h("div", { style: { display: "flex", flexDirection: "column", gap, alignItems: "flex-start" } }, ...kids);
8
+ const row = (h, gap, kids) => h("div", { style: { display: "flex", gap, alignItems: "center" } }, ...kids);
9
+ return {
10
+ // components
11
+ "accordion": [.8, (h, K) => h("div", { style: { width: 260 } }, h(K.Accordion, { title: "Connection", subtitle: "Host, port, credentials", defaultOpen: false }, "Details"), h(K.Accordion, { title: "Schedule", defaultOpen: false }, "Details"))],
12
+ "action-chip": [1, (h, K) => row(h, 8, [h(K.ActionChip, { tone: "info" }, "Mark as New"), h(K.ActionChip, null, "Skip")])],
13
+ "activity": [.85, (h, K) => h("div", { style: { width: 250 } }, h(K.ActivityItem, { icon: "upload", title: "Import finished", meta: "2 min ago" }), h(K.ActivityItem, { icon: "check", title: "Run completed", meta: "1 h ago" }))],
14
+ "alert": [.8, (h, K) => h("div", { style: { width: 280 } }, h(K.Alert, { tone: "info", title: "Mapping saved" }, "3 fields were updated."))],
15
+ "app-chrome": [.5, (h, K) => h("div", { style: { display: "flex", height: 260, width: 480, border: "1px solid var(--border-card)", borderRadius: "var(--radius-md)", overflow: "hidden" } }, h(K.Sidebar, { logo: "assets/keybricks-logo.png", active: "Home", 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: 180 } }), h("div", { style: { flex: 1, minWidth: 0, display: "flex", flexDirection: "column", background: "var(--bg-app)" } }, h("div", { style: { height: 56, background: "var(--bg-surface)", borderBottom: "1px solid var(--border-card)", display: "flex", alignItems: "center", justifyContent: "flex-end", padding: "0 16px" } }, h(K.Button, { variant: "dark", icon: "ai" }, "AI Assistant"))))],
16
+ "banner": [.7, (h, K) => h("div", { style: { width: 340 } }, h(K.Banner, { tone: "warning", title: "Scheduled maintenance tonight", action: "Details" }))],
17
+ "breadcrumb": [1, (h, K) => h(K.Breadcrumb, { items: ["Home", "Pipelines", "Import"] })],
18
+ "button-1-base": [1, (h, K) => row(h, 8, [h(K.Button, { variant: "primary" }, "Save"), h(K.Button, { variant: "outline" }, "Cancel")])],
19
+ "button-4-group": [1, (h, K) => h(K.ButtonGroup, null, h(K.Button, { variant: "outline" }, "Day"), h(K.Button, { variant: "outline" }, "Week"), h(K.Button, { variant: "outline" }, "Month"))],
20
+ "button-2-states": [1, (h, K) => row(h, 8, [h(K.Button, { variant: "primary", pressed: true }, "Pressed"), h(K.Button, { variant: "primary", disabled: true }, "Disabled")])],
21
+ "button-3-sizes-icons": [1, (h, K) => row(h, 8, [h("div", { "data-size": "sm" }, h(K.Button, { variant: "outline", icon: "plus" }, "Add")), h(K.Button, { variant: "outline", icon: "plus" }, "Add"), h("div", { "data-size": "lg" }, h(K.Button, { variant: "outline", icon: "plus" }, "Add"))])],
22
+ "canvas-zoom-controls": [1, (h, K) => h(K.ZoomControls, { value: 100 })],
23
+ "cards-indicators": [.85, (h, K) => h("div", { style: { width: 220 } }, h(K.StatCard, { label: "Rows imported", value: "12,480" }))],
24
+ "checkbox": [1, (h, K) => col(h, 10, [h(K.Checkbox, { checked: true, label: "Include headers" }), h(K.Checkbox, { checked: false, label: "Skip empty rows" })])],
25
+ "codeblock": [.75, (h, K) => h("div", { style: { width: 300 } }, h(K.CodeBlock, { language: "SQL", code: "SELECT id, amount\nFROM ledger\nWHERE period = '2026-09'" }))],
26
+ "combobox": [1, (h, K) => h(K.Combobox, { options: ["Oracle", "SAP"], value: "Oracle", width: 200 })],
27
+ "context-menu": [.9, (h, K) => h(K.ContextMenu, { items: [{ label: "Preview", icon: "eye" }, { label: "Duplicate", icon: "copy" }, { divider: true }, { label: "Delete", icon: "trash-2", danger: true }] })],
28
+ "date-picker": [1, (h, K) => h(K.DatePicker, { value: "2026-10-22", width: 200 })],
29
+ "diffvalue": [1.1, (h, K) => h(K.DiffValue, { from: "1,200", to: "1,450" })],
30
+ "divider": [1, (h, K) => h("div", { style: { width: 200, display: "flex", flexDirection: "column", gap: 10, fontSize: "var(--text-base)", color: "var(--text-body)" } }, h("span", null, "Connection"), h(K.Divider), h("span", null, "Schedule"), h(K.Divider, { label: "or" }))],
31
+ "drawer": [.5, (h, K) => h(K.Drawer, { open: true, inline: true, title: "Run details", subtitle: "RUN-1890", width: 320, height: 220 }, h("div", { style: { padding: 16 } }, h(K.KeyValue, { label: "Status", value: "Completed" })))],
32
+ "dropdown-menu": [.9, (h, K) => col(h, 6, [h(K.Button, { variant: "outline", icon: "chevron-down", iconPosition: "right" }, "Sort by"), h(K.ContextMenu, { items: [{ label: "Name" }, { label: "Date modified" }, { label: "Size" }] })])],
33
+ "dropzone": [.6, (h, K) => h("div", { style: { width: 320 } }, h(K.Dropzone, { title: "Drop a file to import", hint: "CSV or XLSX, up to 20 MB", height: 140 }))],
34
+ "empty-state": [.7, (h, K) => h(K.EmptyState, { icon: "inbox", title: "No runs yet", hint: "Runs appear here once a pipeline starts." })],
35
+ "filter-bar": [.45, (h, K) => h("div", { style: { width: 520 } }, h(K.FilterBar, { variant: "Standard", adaptCount: 2, defaultExpanded: false }))],
36
+ "icon-button": [1.2, (h, K) => row(h, 4, [h(K.IconButton, { icon: "pencil-line", label: "Edit" }), h(K.IconButton, { icon: "copy", label: "Copy" }), h(K.IconButton, { icon: "ellipsis", label: "More" })])],
37
+ "input": [1, (h, K) => h("div", { style: { width: 220 } }, h(K.Input, { value: "FIN_CURRENCY_DEFAULT", onChange: () => {} }))],
38
+ "keyvalue": [.9, (h, K) => h("div", { style: { width: 240 } }, h(K.KeyValueList, { rows: [{ label: "Source", value: "Oracle EBS" }, { label: "Rows", value: "12,480" }, { label: "Status", value: "Ready" }] }))],
39
+ "kbd": [1.3, (h, K) => h(K.Kbd, { keys: ["⌘", "K"] })],
40
+ "listrow": [.8, (h, K) => h("div", { style: { width: 280 } }, h(K.ListRow, { icon: "database", title: "Oracle EBS", meta: "Last sync 2 min ago" }))],
41
+ "loading": [1, (h, K) => row(h, 16, [h(K.Spinner), h("div", { style: { display: "flex", flexDirection: "column", gap: 8 } }, h(K.Skeleton, { width: 140, height: 12 }), h(K.Skeleton, { width: 100, height: 12 }))])],
42
+ "log-viewer": [.7, (h, K) => h("div", { style: { width: 320 } }, h(K.LogViewer, { lines: ["[14:30] Task started: Import", "[14:30] Reading ledger.csv", "[14:31] 12,480 rows written"], height: 90 }))],
43
+ "modal": [.5, (h, K) => h("div", { style: { width: 360, borderRadius: "var(--radius-lg)", border: "1px solid var(--border-card)", background: "var(--bg-raised)", boxShadow: "var(--shadow-menu)", padding: 20, display: "flex", flexDirection: "column", gap: 16 } }, h("div", { style: { fontSize: "var(--text-md)", fontWeight: "var(--weight-semibold)", color: "var(--text-heading)" } }, "Delete pipeline?"), h("div", { style: { fontSize: "var(--text-base)", color: "var(--text-body)" } }, "This removes 4 nodes and their run history."), row(h, 8, [h("div", { style: { flex: 1 } }), h(K.Button, { variant: "outline" }, "Cancel"), h(K.Button, { variant: "danger" }, "Delete")]))],
44
+ "multi-select": [.9, (h, K) => h(K.MultiSelect, { options: ["HK", "SG", "CN"], value: ["HK", "SG"], width: 220 })],
45
+ "number-stepper": [1.1, (h, K) => h(K.NumberStepper, { value: 100 })],
46
+ "pagination": [.8, (h, K) => h(K.Pagination, { page: 2, pageCount: 5, total: 96 })],
47
+ "people": [1.2, (h, K) => h(K.AvatarGroup, { names: ["Felix Wang", "Luo Lisi", "Guo Kaige", "Ana Silva", "Kim Lee"], max: 4 })],
48
+ "pipeline-node": [.75, (h, K) => h(K.NodeCard, { title: "Join Ledger", subtitle: "2 inputs", icon: "merge", hue: "join", inputs: ["A", "B"] })],
49
+ "pipeline-palette": [.5, (h, K) => h(K.IconTile, { icon: "database", hue: "source" })],
50
+ "radio": [1, (h, K) => col(h, 10, [h(K.Radio, { checked: true, label: "Overwrite" }), h(K.Radio, { checked: false, label: "Append" })])],
51
+ "search": [1, (h, K) => h("div", { style: { width: 220 } }, h(K.SearchInput, { placeholder: "Search pipelines" }))],
52
+ "segmented-control": [1, (h, K) => h(K.SegmentedControl, { options: ["Grid", "List", "Board"], value: "Grid" })],
53
+ "relative-time-picker": [1, (h, K) => h("div", { style: { width: 220 } }, h(K.RelativeTimePicker, { value: { amount: 24, unit: "Hour" } }))],
54
+ "select": [1, (h, K) => h("div", { style: { width: 200 } }, h(K.Select, { options: ["Monthly", "Quarterly"], value: "Monthly" }))],
55
+ "slider": [1, (h, K) => h(K.Slider, { value: 60, width: 200 })],
56
+ "smart-table": [.5, (h, K) => h("div", { style: { width: 440 } }, h(K.SmartTable, { toolbar: false, fields: [{ key: "name", label: "Name", primary: true }, { key: "owner", label: "Owner", type: "person" }, { key: "done", label: "Done", type: "checkbox" }], rows: [{ name: "FX rates", owner: "Felix Wang", done: true }, { name: "Opening balances", owner: "Luo Lisi", done: false }] }))],
57
+ "spreadsheet": [.45, (h, K) => h("div", { style: { width: 460 } }, h(K.Spreadsheet, { rows: 4, toolbar: false, height: 170, data: [[{ v: "Entity" }, { v: "Q3" }, { v: "Q4" }], [{ v: "HK" }, { v: 4100 }, { v: 4600 }], [{ v: "SG" }, { v: 1610 }, { v: 1820 }]] }))],
58
+ "stepper": [.75, (h, K) => h(K.Stepper, { steps: [{ label: "Source" }, { label: "Map" }, { label: "Run" }], current: 1 })],
59
+ "table": [.55, (h, K) => h("div", { style: { width: 400 } }, h(K.Table, { bordered: true, columns: [{ key: "id", label: "Run" }, { key: "doc", label: "Document" }, { key: "st", label: "Status" }], rows: [{ id: "RUN-1890", doc: "FX Rates", st: "Completed" }, { id: "RUN-1891", doc: "Ledger", st: "Running" }] }))],
60
+ "tabs": [.9, (h, K) => h(K.Tabs, { tabs: [{ label: "Import", count: 3 }, "Export", "History"], active: "Import" })],
61
+ "tag": [1.1, (h, K) => row(h, 6, [h(K.Tag, { onRemove: () => {} }, "HK"), h(K.Tag, { tone: "info", onRemove: () => {} }, "Q3")])],
62
+ "textarea": [.9, (h, K) => h("div", { style: { width: 240 } }, h(K.Textarea, { value: "Monthly close notes", rows: 3, onChange: () => {} }))],
63
+ "timeline": [.8, (h, K) => h("div", { style: { width: 240 } }, h(K.Timeline, { items: [{ tone: "success", title: "Run completed", meta: "14:31" }, { title: "Mapping updated", meta: "13:05" }] }))],
64
+ "toast": [.75, (h, K) => h(K.Toast, { tone: "success", title: "Pipeline saved", meta: "Just now" })],
65
+ "toggles": [1.2, (h, K) => row(h, 12, [h(K.Switch, { checked: true }), h(K.Switch, { checked: false })])],
66
+ "tooltip": [1, (h, K) => h("div", { style: { padding: "6px 10px", borderRadius: "var(--radius-md)", background: "var(--action-dark)", color: "var(--action-dark-fg)", fontSize: "var(--text-xs)", lineHeight: "var(--leading-xs)" } }, "Copy to clipboard")],
67
+ "tree": [.8, (h, K) => h("div", { style: { width: 220 } }, h(K.Tree, { defaultExpanded: ["a"], items: [{ key: "a", title: "Assets", children: [{ key: "a1", title: "Cash" }, { key: "a2", title: "Receivables" }] }, { key: "b", title: "Liabilities", children: [{ key: "b1", title: "Payables" }] }] }))],
68
+ // icons — any icon card without a cover falls back to its name; new sets just need an entry here
69
+ "icons-0-usage": [1, (h, K) => h("div", { style: { display: "grid", gridTemplateColumns: "repeat(3, 40px)", gap: 12 } }, ...[["source", "table"], ["join", "shuffle"], ["transform", "funnel"], ["merge", "merge"], ["output", "layers"]].map(([hu, i]) => h(K.IconTile, { key: hu, icon: i, hue: hu })), h("span", { key: "s", style: { display: "flex", alignItems: "center", justifyContent: "center" } }, h(K.StatusBadge, { tone: "success", icon: "check" })))],
70
+ "icons-1-core": [1, (h, K) => h("div", { style: { display: "grid", gridTemplateColumns: "repeat(4, 28px)", gap: 14, color: "var(--text-body)" } }, ...["house", "file-text", "database", "journal", "reconciliation", "consolidation", "ai", "settings"].map(n => h(K.Icon, { key: n, name: n, size: 24 })))],
71
+ "icons-2-feather": [1, (h, K) => h("div", { style: { display: "grid", gridTemplateColumns: "repeat(4, 28px)", gap: 14, color: "var(--text-body)" } }, ...["feather", "anchor", "aperture", "compass", "cpu", "droplet", "globe", "umbrella"].map(n => h(K.Icon, { key: n, name: n, size: 24 })))],
72
+ "icons-2b-lucide": [1, (h, K) => h("div", { style: { display: "grid", gridTemplateColumns: "repeat(4, 28px)", gap: 14, color: "var(--text-body)" } }, ...["calendar-clock", "file-chart-pie", "list-checks", "badge-check", "folder-tree", "square-terminal", "zap", "boxes"].map(n => h(K.Lucide, { key: n, name: n, size: 24 })))],
73
+ "icons-3-unicons": [1, (h, K) => h("div", { style: { display: "grid", gridTemplateColumns: "repeat(4, 28px)", gap: 14, color: "var(--text-body)" } }, ...["chart", "bolt", "cloud", "envelope", "rocket", "star", "user", "wallet"].map(n => h(K.Unicon, { key: n, name: n, size: 24 })))],
74
+ "icons-4-datasource": [1, (h) => h("div", { style: { display: "grid", gridTemplateColumns: "repeat(4, 32px)", gap: 12 } }, ...["oracle", "sap", "sql-server", "mysql", "postgresql", "mongodb", "rest-api", "excel"].map(n => h("img", { key: n, src: "assets/logos/datasource/" + n + ".png", alt: n, width: 32, height: 32, style: { display: "block", borderRadius: "var(--radius-md)" } })))],
75
+ // charts
76
+ "charts-1-sparkline": [1, (h, K) => h(K.Sparkline, { values: L(14), width: 200, height: 56 })],
77
+ "charts-2-line": [.6, (h, K) => h(K.LineChart, { series: [{ name: "Actual", values: L(6) }, { name: "Plan", values: L(6, 1) }], xLabels: M, width: 340, height: 180 })],
78
+ "charts-3-bar": [.6, (h, K) => h(K.BarChart, { series: [{ name: "2025", values: [40, 62, 55, 80] }, { name: "2026", values: [52, 70, 66, 92] }], categories: ["Q1", "Q2", "Q3", "Q4"], width: 340, height: 180 })],
79
+ "charts-4-barlist": [.7, (h, K) => h(K.BarList, { rows: [{ label: "Germany", value: 5200 }, { label: "France", value: 3100 }, { label: "Spain", value: 1100 }], width: 280 })],
80
+ "charts-5-donut": [.7, (h, K) => h(K.DonutChart, { segments: [{ label: "EMEA", value: 42 }, { label: "APAC", value: 31 }, { label: "AMER", value: 27 }], size: 150, centerValue: "100%", legend: false })],
81
+ "charts-6-scatter": [.6, (h, K) => h(K.ScatterChart, { points: [[12, 340, 8], [30, 210, 5], [44, 420, 10], [58, 300, 6], [70, 480, 9], [82, 260, 4]].map(([x, y, r]) => ({ x, y, r })), width: 340, height: 180 })],
82
+ "charts-7-heatmap": [.6, (h, K) => h(K.HeatmapChart, { xLabels: M, yLabels: ["HK", "SG", "CN", "JP"], values: [0, 1, 2, 3].map(r => M.map((_, c) => (r * 7 + c * 5) % 17)), width: 340 })],
83
+ "charts-8-waterfall": [.6, (h, K) => h(K.WaterfallChart, { steps: [{ label: "Revenue", value: 6100 }, { label: "COGS", value: -2400 }, { label: "Opex", value: -1500 }, { label: "Tax", value: -500 }], width: 340, height: 180 })],
84
+ "charts-9-bullet": [.7, (h, K) => h(K.BulletChart, { rows: [{ label: "Revenue", value: 6100, target: 5800 }, { label: "Margin", value: 38, target: 42 }], width: 280 })],
85
+ "charts-10-gauge": [.75, (h, K) => h(K.GaugeChart, { value: 72, label: "Close progress" })],
86
+ "charts-11-funnel": [.55, (h, K) => h(K.FunnelChart, { stages: [{ label: "Imported", value: 1200 }, { label: "Mapped", value: 940 }, { label: "Validated", value: 720 }, { label: "Posted", value: 610 }], width: 280 })],
87
+ "charts-12-treemap": [.6, (h, K) => h(K.TreemapChart, { items: [{ label: "Payroll", value: 42 }, { label: "Rent", value: 18 }, { label: "IT", value: 14 }, { label: "Travel", value: 9 }, { label: "Other", value: 7 }], width: 340, height: 180 })],
88
+ "charts-14-radar": [.6, (h, K) => h(K.RadarChart, { axes: ["Speed", "Quality", "Cost", "Scope", "Risk"].map(label => ({ label, max: 100 })), series: [{ name: "Team A", values: [80, 65, 70, 90, 50] }, { name: "Team B", values: [60, 85, 55, 70, 75] }], size: 200, legend: false })],
89
+ "charts-15-distribution": [.6, (h, K) => h(K.Histogram, { values: Array.from({ length: 120 }, (_, i) => Math.round(50 + 20 * Math.sin(i * 1.7) + (i % 9) * 3)), bins: 12, width: 340, height: 180 })],
90
+ "charts-16-pareto": [.6, (h, K) => h(K.ParetoChart, { items: [{ label: "Mapping", value: 48 }, { label: "Format", value: 26 }, { label: "Dupes", value: 14 }, { label: "Other", value: 8 }], width: 340, height: 180 })],
91
+ "charts-17-candlestick": [.6, (h, K) => h(K.CandlestickChart, { candles: L(14).map((v, i) => { const o = v, c = v + ((i % 3) - 1) * 6; return { date: "D" + i, label: "D" + i, o, c, h: Math.max(o, c) + 5, l: Math.min(o, c) - 5, open: o, close: c, high: Math.max(o, c) + 5, low: Math.min(o, c) - 5 }; }), width: 340, height: 180 })],
92
+ "charts-18-sankey": [.55, (h, K) => h(K.SankeyChart, { nodes: ["Revenue", "COGS", "Opex", "Profit"].map(id => ({ id, label: id })), links: [{ source: "Revenue", target: "COGS", value: 40 }, { source: "Revenue", target: "Opex", value: 25 }, { source: "Revenue", target: "Profit", value: 35 }], width: 360, height: 200 })],
93
+ "charts-19-chord": [.55, (h, K) => h(K.ChordChart, { labels: ["HK", "SG", "CN", "JP"], matrix: [[0, 5, 8, 3], [4, 0, 2, 6], [7, 3, 0, 4], [2, 5, 3, 0]], size: 240 })],
94
+ "charts-20-sunburst": [.55, (h, K) => h(K.SunburstChart, { data: { label: "Total cost", children: [{ label: "People", children: [{ label: "Payroll", value: 40 }, { label: "Benefits", value: 12 }] }, { label: "Facilities", children: [{ label: "Rent", value: 18 }, { label: "Utilities", value: 6 }] }, { label: "IT", value: 14 }] }, size: 240 })],
95
+ "charts-21-worldmap": [.45, (h, K) => h(K.WorldMap, { values: { "United States": 6400, Canada: 2100, Brazil: 1400, "United Kingdom": 4200, Germany: 5200, France: 3100, China: 5800, Japan: 2600, Australia: 1200 }, width: 460, legend: false })],
96
+ "charts-22-gantt": [.55, (h, K) => h(K.GanttChart, { tasks: [{ label: "Extract", start: "2026-03-02", end: "2026-03-05", progress: 1, lane: 0 }, { label: "Map", start: "2026-03-04", end: "2026-03-09", progress: .6, lane: 1 }, { label: "Validate", start: "2026-03-08", end: "2026-03-12", progress: 0, lane: 2 }], width: 380 })],
97
+ "charts-23-bar3d": [.55, (h, K) => h(K.Bar3D, { series: [{ name: "2025", values: [40, 62, 55, 80] }, { name: "2026", values: [52, 70, 66, 92] }], categories: ["Q1", "Q2", "Q3", "Q4"], width: 360, height: 220, interactive: false })],
98
+ "charts-24-pie3d": [.6, (h, K) => h(K.Pie3D, { segments: [{ label: "EMEA", value: 42 }, { label: "APAC", value: 31 }, { label: "AMER", value: 27 }], legend: false, width: 300, height: 200 })],
99
+ "charts-25-surface3d": [.55, (h, K) => h(K.Surface3D, { width: 360, height: 220, interactive: false })],
100
+ "charts-26-scatter3d": [.55, (h, K) => h(K.Scatter3D, { points: Array.from({ length: 30 }, (_, i) => ({ x: (i * 37) % 100, y: (i * 53) % 100, z: (i * 71) % 100, series: ["Alpha", "Beta", "Gamma"][i % 3] })), width: 360, height: 220, legend: false, interactive: false })],
101
+ "charts-27-ribbon3d": [.55, (h, K) => h(K.Ribbon3D, { series: [{ name: "HK", values: L(6) }, { name: "SG", values: L(6, 1) }, { name: "CN", values: L(6, 2) }], xLabels: M, width: 360, height: 220, interactive: false })],
102
+ "charts-99-fullpage": [.5, (h, K) => h("div", { style: { display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10, width: 380 } }, h(K.StatCard, { label: "Revenue", value: "$6.1M" }), h(K.StatCard, { label: "Margin", value: "38%" }), h("div", { style: { gridColumn: "1 / -1" } }, h(K.Sparkline, { values: L(20), width: 380, height: 60 })))]
103
+ };
104
+ })();
package/gallery.html ADDED
@@ -0,0 +1,133 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
3
+ <title>Keybricks Design System</title>
4
+ <script src="tokens/theme.js"></script>
5
+ <link rel="stylesheet" href="styles.css"><link rel="stylesheet" href="tokens/hover.css">
6
+ <style>
7
+ *{box-sizing:border-box}
8
+ body{margin:0;font-family:var(--font-sans);background:var(--bg-app);color:var(--text-body)}
9
+ header{background:var(--bg-surface);border-bottom:1px solid var(--border-card);padding:22px 32px 18px}
10
+ h1{margin:0;font-size:var(--text-2xl);font-weight:var(--weight-semibold);letter-spacing:var(--tracking-2xl);color:var(--text-heading)}
11
+ .sub{margin:4px 0 0;font-size:var(--text-base);color:var(--text-secondary)}
12
+ .bar{display:flex;align-items:center;gap:10px;padding-top:16px;flex-wrap:wrap}
13
+ input[type=search]{height:var(--h-input);flex:1;min-width:200px;max-width:340px;border:1px solid var(--border-control);border-radius:var(--radius-md);background:var(--bg-surface);color:var(--text-primary);font-family:var(--font-sans);font-size:var(--text-xs);padding:0 10px;letter-spacing:var(--tracking-xs)}
14
+ input[type=search]:focus{outline:none;border-color:var(--focus-border);box-shadow:0 0 0 2px var(--focus-ring)}
15
+ .count{font-family:var(--font-sans);font-variant-numeric:tabular-nums;font-size:var(--text-xs);color:var(--text-muted);letter-spacing:var(--tracking-xs)}
16
+ a.doc{margin-left:auto;font-size:var(--text-xs);color:var(--action-primary);text-decoration:none;font-weight:var(--weight-medium);letter-spacing:var(--tracking-xs)}
17
+ a.doc:hover{color:var(--action-primary-hover);text-decoration:underline}
18
+ main{padding:24px 32px 56px}
19
+ h2{margin:28px 0 12px;font-size:var(--text-xs);line-height:var(--leading-xs);font-weight:var(--weight-semibold);color:var(--text-secondary);letter-spacing:var(--tracking-xs)}
20
+ h2:first-child{margin-top:0}
21
+ .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:16px}
22
+ .card{background:var(--bg-surface);border:1px solid var(--border-card);border-radius:var(--radius-lg);overflow:hidden;display:flex;flex-direction:column}
23
+ .card figure{margin:0;height:210px;overflow:hidden;background:var(--bg-surface);border-bottom:1px solid var(--border-subtle);position:relative}
24
+ .card iframe{border:0;position:absolute;top:0;left:0;transform-origin:top left;background:var(--bg-surface)}
25
+ .meta{padding:11px 13px 13px}
26
+ .meta b{display:block;font-size:var(--text-base);font-weight:var(--weight-medium);color:var(--text-heading)}
27
+ .meta span{display:block;font-size:var(--text-xs);color:var(--text-secondary);padding-top:2px;line-height:1.45;letter-spacing:var(--tracking-xs)}
28
+ .meta a{display:inline-block;margin-top:8px;font-size:var(--text-xs);font-family:var(--font-sans);font-variant-numeric:tabular-nums;color:var(--action-primary);text-decoration:none;letter-spacing:var(--tracking-xs)}
29
+ .meta a:hover{text-decoration:underline}
30
+ .empty{padding:40px;text-align:center;color:var(--text-muted);font-size:var(--text-xs);letter-spacing:var(--tracking-xs)}
31
+ </style></head>
32
+ <body>
33
+ <header>
34
+ <h1>Keybricks Design System</h1>
35
+ <p class="sub">121 components in React and Vue 3, on one token layer. Every card below is the real component, not a screenshot.</p>
36
+ <div class="bar">
37
+ <input type="search" id="q" placeholder="Filter components…" autocomplete="off">
38
+ <span class="count" id="count"></span>
39
+ <a class="doc" href="INTEGRATION.md">Integration guide →</a>
40
+ </div>
41
+ </header>
42
+ <main id="out"><div class="empty">Loading…</div></main>
43
+ <script type="application/json" id="cards">
44
+ [{"path":"components/_cards/accordion.card.html","group":"Components","viewport":"700x320","name":"Accordion","subtitle":"Generic collapsible section — settings/FAQ style"},{"path":"components/_cards/action-chip.card.html","group":"Components","viewport":"700x370","name":"Action Chip","subtitle":"Tinted bulk-action chips · five tones · size tiers, Latin and CJK"},{"path":"components/_cards/activity.card.html","group":"Components","viewport":"700x300","name":"Activity","subtitle":"ActivityItem rows · DotActivity feed"},{"path":"components/_cards/alert.card.html","group":"Components","viewport":"700x350","name":"Alert","subtitle":"In-card message — 4 tones, with and without body"},{"path":"components/_cards/app-chrome.card.html","group":"Components","viewport":"700x1490","name":"App Chrome","subtitle":"Sidebar tones · SidePanel · TopBar · UserMenu · PageToolbar"},{"path":"components/_cards/banner.card.html","group":"Components","viewport":"700x300","name":"Banner","subtitle":"Page-level notification — 4 tones · action link · dismissible"},{"path":"components/_cards/breadcrumb.card.html","group":"Components","viewport":"700x468","name":"Breadcrumb","subtitle":"Path trail — hover darkens · long trails collapse to an ellipsis dropdown"},{"path":"components/_cards/button-1-base.card.html","group":"Components","viewport":"700x690","name":"Button","subtitle":"Solid · soft/outline · ghost · text-only · disabled · loading"},{"path":"components/_cards/button-4-group.card.html","group":"Components","viewport":"700x490","name":"Button Group","subtitle":"Segmented group · split button · shortcut hint · badge"},{"path":"components/_cards/button-2-states.card.html","group":"Components","viewport":"1000x810","name":"Button Interaction States","subtitle":"Rest · hover · press · focus · loading · disabled — every variant, live and forced"},{"path":"components/_cards/button-3-sizes-icons.card.html","group":"Components","viewport":"700x720","name":"Button Sizes & Icons","subtitle":"sm 28 · md 32 · lg 40, Latin and CJK · icon placement · full width"},{"path":"components/_cards/canvas-zoom-controls.card.html","group":"Components","viewport":"700x180","name":"Canvas Zoom Controls","subtitle":"Pipeline canvas − / % / + / fit-to-view"},{"path":"components/_cards/cards-indicators.card.html","group":"Components","viewport":"700x450","name":"Cards & Indicators","subtitle":"Work in progress · Card · StatCard · Badge · IconTile · charts"},{"path":"components/_cards/checkbox.card.html","group":"Components","viewport":"700x370","name":"Checkbox","subtitle":"Checked · unchecked · indeterminate · disabled · sizes"},{"path":"components/_cards/codeblock.card.html","group":"Components","viewport":"700x260","name":"Code Block","subtitle":"SQL Preview / JSON export — mono with copy button"},{"path":"components/_cards/combobox.card.html","group":"Components","viewport":"700x430","name":"Combobox","subtitle":"Typeahead single-select — type to filter · keyboard nav · no-results"},{"path":"components/_cards/context-menu.card.html","group":"Components","viewport":"700x920","name":"Context Menu","subtitle":"Icons · plain text · danger item · long list"},{"path":"components/_cards/date-picker.card.html","group":"Components","viewport":"700x520","name":"Date Picker","subtitle":"Input + month-grid popover — today outlined · selected filled"},{"path":"components/_cards/diffvalue.card.html","group":"Components","viewport":"700x220","name":"Diff Value","subtitle":"Before → after change display (config migration)"},{"path":"components/_cards/divider.card.html","group":"Components","viewport":"700x220","name":"Divider","subtitle":"Plain · labeled · vertical"},{"path":"components/_cards/drawer.card.html","group":"Components","viewport":"700x540","name":"Drawer","subtitle":"Slide-out detail panel — header · scrollable body · footer actions"},{"path":"components/_cards/dropdown-menu.card.html","group":"Components","viewport":"700x420","name":"Dropdown Menu","subtitle":"Button · icon-only · ghost text · avatar triggers — icon items & plain-text items"},{"path":"components/_cards/dropzone.card.html","group":"Components","viewport":"924x588","name":"Dropzone","subtitle":"Empty · drag-over · uploaded · error · disabled"},{"path":"components/_cards/empty-state.card.html","group":"Components","viewport":"924x580","name":"Empty State","subtitle":"First-run · no results · error — with and without action"},{"path":"components/_cards/filter-bar.card.html","group":"Components","viewport":"1000x620","name":"Filter Bar (List Report)","subtitle":"Fiori-style filter bar composed with a table — Adapt Filters · Go"},{"path":"components/_cards/icon-button.card.html","group":"Components","viewport":"700x400","name":"Icon Button","subtitle":"Square ghost icon button · sm 28 · md 32 · lg 40 · active / muted / danger / loading / disabled"},{"path":"components/_cards/input.card.html","group":"Components","viewport":"700x720","name":"Input","subtitle":"Default · filled · required · readOnly · disabled · error · hint · focus"},{"path":"components/_cards/keyvalue.card.html","group":"Components","viewport":"700x250","name":"Key/Value Rows","subtitle":"Config summary and confirmation-step rows"},{"path":"components/_cards/kbd.card.html","group":"Components","viewport":"700x180","name":"Keyboard Shortcuts","subtitle":"Kbd — keycap hints for a power-user tool"},{"path":"components/_cards/listrow.card.html","group":"Components","viewport":"700x420","name":"List Row","subtitle":"Expandable workflow rows — collapsed, expanded, with trailing count"},{"path":"components/_cards/loading.card.html","group":"Components","viewport":"700x310","name":"Loading","subtitle":"Spinner sizes · Skeleton text, blocks and table rows"},{"path":"components/_cards/log-viewer.card.html","group":"Components","viewport":"700x370","name":"Log Viewer","subtitle":"Mono run log — info · warn · error lines"},{"path":"components/_cards/modal.card.html","group":"Components","viewport":"700x520","name":"Modal","subtitle":"Default · destructive footer · with form content · no subtitle"},{"path":"components/_cards/multi-select.card.html","group":"Components","viewport":"700x400","name":"Multi Select","subtitle":"Checkbox dropdown — removable tags · +n overflow · clear all"},{"path":"components/_cards/number-stepper.card.html","group":"Components","viewport":"700x240","name":"Number Stepper","subtitle":"Default · at min · at max · disabled"},{"path":"components/_cards/pagination.card.html","group":"Components","viewport":"700x520","name":"Pagination","subtitle":"Row-range label · small set · first/last page states"},{"path":"components/_cards/people.card.html","group":"Components","viewport":"700x300","name":"People","subtitle":"Avatar · AvatarGroup — initials, sizes, real images"},{"path":"components/_cards/pipeline-node.card.html","group":"Components","viewport":"700x1620","name":"Pipeline Node Card","subtitle":"220×90 node — category hues · status badges · ports with hover, drag-to-connect and drop states"},{"path":"components/_cards/pipeline-palette.card.html","group":"Components","viewport":"700x690","name":"Pipeline Palette","subtitle":"240px component drawer — grid/list toggle · collapsible hued categories"},{"path":"components/_cards/radio.card.html","group":"Components","viewport":"700x1120","name":"Radio Button","subtitle":"Single choice from a small set — md/sm · description · disabled · row · card variant · arrow-key navigation"},{"path":"components/_cards/search.card.html","group":"Components","viewport":"700x520","name":"Search Input","subtitle":"Bordered · bare top-bar · with value/clear · disabled"},{"path":"components/_cards/segmented-control.card.html","group":"Components","viewport":"700x1260","name":"Segmented Control","subtitle":"Mutually exclusive views in one inset track — md/sm · counts · icons · icon-only · disabled option · full width · ViewToggle"},{"path":"components/_cards/select.card.html","group":"Components","viewport":"700x430","name":"Select","subtitle":"md / sm · placeholder · disabled · error"},{"path":"components/_cards/relative-time-picker.card.html","group":"Components","viewport":"700x720","name":"Relative Time Picker","subtitle":"Filter by how long ago · md / sm / lg · unlimited · disabled · localised labels","zh":"相对时间选择器","ja":"相対時間ピッカー","subtitleZh":"按\"多久以前\"筛选 · md / sm / lg · 不限时间 · 禁用 · 本地化文案","subtitleJa":"「どれくらい前」で絞り込み · md / sm / lg · 無制限 · 無効 · ローカライズ"},{"path":"components/_cards/slider.card.html","group":"Components","viewport":"700x300","name":"Slider","subtitle":"Filled track · ring knob · value bubble · min/max labels"},{"path":"components/_cards/smart-table.card.html","group":"Components","viewport":"700x1040","name":"Smart Table (Database Grid)","subtitle":"Frozen header and row numbers · default and compact density · typed fields · hue pills · aggregates · + New"},{"path":"components/_cards/spreadsheet.card.html","group":"Components","viewport":"924x560","name":"Spreadsheet","subtitle":"Formula bar · frozen headers · range selection · =SUM() · sheet tabs · status bar"},{"path":"components/_cards/stepper.card.html","group":"Components","viewport":"700x450","name":"Stepper","subtitle":"Multi-step flow header — done · current · upcoming"},{"path":"components/_cards/table.card.html","group":"Components","viewport":"700x6150","name":"Table","subtitle":"Anatomy · densities · column types · two-line name · sorting · selection · frames · column resize · states · composition"},{"path":"components/_cards/tabs.card.html","group":"Components","viewport":"700x810","name":"Tabs","subtitle":"Underline tabs · panel tabs · pill filter tabs · counts"},{"path":"components/_cards/tag.card.html","group":"Components","viewport":"700x300","name":"Tag","subtitle":"Removable filter tags — distinct from Badge/ActionChip"},{"path":"components/_cards/textarea.card.html","group":"Components","viewport":"924x520","name":"Textarea","subtitle":"Default · character count · over the limit · disabled"},{"path":"components/_cards/timeline.card.html","group":"Components","viewport":"700x350","name":"Timeline","subtitle":"Vertical activity timeline — toned icon nodes on a hairline spine"},{"path":"components/_cards/toast.card.html","group":"Components","viewport":"700x450","name":"Toast","subtitle":"Tones · with/without meta · live ToastStack demo"},{"path":"components/_cards/toggles.card.html","group":"Components","viewport":"700x1670","name":"Toggle Switch","subtitle":"Binary settings pill — sizes · semantic red→green · glyph knobs · inline text · captions · async loading"},{"path":"components/_cards/tooltip.card.html","group":"Components","viewport":"700x360","name":"Tooltip","subtitle":"Placements — top · bottom · left · right, on icon and text triggers"},{"path":"components/_cards/tree.card.html","group":"Components","viewport":"700x2820","name":"Tree","subtitle":"Cascading checkboxes · disabled nodes · lines · async load · search · drag · reorder · keyboard"},{"path":"components/_cards/icons-0-usage.card.html","group":"Icons","viewport":"700x2250","name":"Icon Usage","subtitle":"Monochrome glyphs · category tiles · document hues · status · marks · icons inside components — when an icon gets colour","zh":"图标用法","ja":"アイコンの使い方","subtitleZh":"单色图标 · 分类色块 · 文档色 · 状态 · 标记 · 组件内图标——图标什么时候用颜色","subtitleJa":"単色グリフ · カテゴリタイル · ドキュメント色 · ステータス · マーク · コンポーネント内のアイコン — いつ色を付けるか"},{"path":"components/_cards/icons-1-core.card.html","group":"Icons","viewport":"700x1900","name":"Core Set","subtitle":"The 157 stroke glyphs the product itself uses — keyword search · click to download the SVG"},{"path":"components/_cards/icons-2-feather.card.html","group":"Icons","viewport":"700x4400","name":"Feather Library","subtitle":"404 stroke icons (core + all of Feather) · keyword search · click to download the SVG"},{"path":"components/_cards/icons-2b-lucide.card.html","group":"Icons","name":"Lucide Library","viewport":"700x1370","subtitle":"Full Lucide set (1,700+ stroke glyphs, core style) via pinned CDN · keyword search · click to download the SVG","zh":"Lucide 图标库","ja":"Lucideライブラリ","subtitleZh":"完整 Lucide 图标集(1,700+ 线性图标,与核心风格一致,固定版本 CDN)· 关键词搜索 · 点击下载 SVG","subtitleJa":"Lucide 全セット(1,700+ のストロークアイコン、コアと同一スタイル、固定版 CDN)· キーワード検索 · クリックで SVG をダウンロード"},{"path":"components/_cards/icons-3-unicons.card.html","group":"Icons","viewport":"700x1370","name":"Unicons Library","subtitle":"1,206 Unicons line glyphs via CDN webfont · keyword search · click to download the SVG"},{"path":"components/_cards/icons-4-datasource.card.html","group":"Icons","viewport":"700x660","name":"Data Source Logos","subtitle":"17 data-source logo images in assets/logos/datasource/","zh":"数据源图标","ja":"データソースロゴ","subtitleZh":"17 张数据源图标图片,位于 assets/logos/datasource/","subtitleJa":"assets/logos/datasource/ の 17 枚のデータソースロゴ画像"},{"path":"guidelines/brand-logo.html","group":"Brand","viewport":"700x170","name":"Logo & Wordmark","subtitle":"Blue cube bitmap + Inter 600 wordmark, −0.609px tracking"},{"path":"guidelines/colors-list.html","group":"Colors","viewport":"700x2740","name":"Color List","subtitle":"Every color in the system, with name and hex"},{"path":"guidelines/colors-palette.html","group":"Colors","viewport":"700x880","name":"Palette","subtitle":"Grouped by role — hover a dot for its token and hex"},{"path":"guidelines/colors-schemes.html","group":"Colors","viewport":"700x800","name":"Schemes","subtitle":"Sets that work together — surface, text, border, accent"},{"path":"guidelines/colors-transparency.html","group":"Colors","viewport":"700x490","name":"Transparency","subtitle":"Alpha scale — the number is the opacity"},{"path":"guidelines/foundations-borders.html","group":"Foundations","viewport":"700x190","name":"Borders & Shadows","subtitle":"Hairlines carry the UI; shadows only on canvas nodes"},{"path":"guidelines/foundations-chrome.html","group":"Foundations","viewport":"700x180","name":"App Chrome Dimensions","subtitle":"Fixed panel widths & bar heights"},{"path":"guidelines/foundations-motion.html","group":"Foundations","viewport":"700x170","name":"Motion","subtitle":"120/160/240ms · ease-out — hover, rise, pulse, spin"},{"path":"guidelines/foundations-radii.html","group":"Foundations","viewport":"700x200","name":"Corner Radii","subtitle":"4 controls · 8 buttons · 10 cards · 14/16 tiles & nodes · full circles"},{"path":"guidelines/foundations-size-tiers.html","group":"Foundations","viewport":"700x1340","name":"Size Tiers","subtitle":"sm 28 · md 32 · lg 40 — one tier per region, never mixed in a row; CJK one step larger at sm"},{"path":"guidelines/foundations-spacing.html","group":"Foundations","viewport":"700x170","name":"Spacing","subtitle":"4px family · page 24×32 · card 21 · section gap 24 · gaps 8 inside a group, 12/16/24 between groups"},{"path":"guidelines/interaction-states.html","group":"Foundations","viewport":"700x570","name":"Interaction States","subtitle":"The five states every control carries — and where each one is defined"},{"path":"guidelines/type-extended.html","group":"Type","viewport":"700x440","name":"Display Scale","subtitle":"Display sizes 24–60 — tracking follows the same size→tracking curve"},{"path":"guidelines/type-mono.html","group":"Type","viewport":"700x170","name":"Mono / Code Only","subtitle":"Menlo for code blocks, logs, inline code and config-code chips — IDs and amounts stay in Inter with tabular figures"},{"path":"guidelines/type-scale.html","group":"Type","viewport":"700x450","name":"Type Scale","subtitle":"Inter — the 14px scale: 12 captions · 14 body and controls · 16 section titles · 20 page titles; CJK one step larger below 14"},{"path":"guidelines/type-weights.html","group":"Type","viewport":"700x280","name":"Inter Weights","subtitle":"300 numerals · 400 body · 500 UI-medium · 600 titles — 500 becomes 600 in Chinese, Japanese and Korean"},{"path":"components/_cards/charts-1-sparkline.card.html","group":"Charts","viewport":"700x830","name":"Sparkline","subtitle":"Micro trends for stat tiles, table cells, list rows — line · area · bars · curve"},{"path":"components/_cards/charts-2-line.card.html","group":"Charts","viewport":"700x1370","name":"Line & Area","subtitle":"Trends over time — straight, smooth or stepped · stacked area · hover + legend mute"},{"path":"components/_cards/charts-3-bar.card.html","group":"Charts","viewport":"700x2040","name":"Bar Chart","subtitle":"Vertical &amp; horizontal — grouped · stacked · 100% stacked · diverging · combo line"},{"path":"components/_cards/charts-4-barlist.card.html","group":"Charts","viewport":"700x540","name":"Bar List","subtitle":"Horizontal labeled bars — top-N rankings, budget consumption"},{"path":"components/_cards/charts-5-donut.card.html","group":"Charts","viewport":"700x570","name":"Donut & Pie","subtitle":"Composition with a KPI center — hover lifts a segment"},{"path":"components/_cards/charts-6-scatter.card.html","group":"Charts","viewport":"700x480","name":"Scatter & Bubble","subtitle":"x/y correlation, bubble = third dimension — nearest-point tooltip"},{"path":"components/_cards/charts-7-heatmap.card.html","group":"Charts","viewport":"700x480","name":"Heatmap","subtitle":"Matrix density — sequential blue ramp · diverging red/green variance"},{"path":"components/_cards/charts-8-waterfall.card.html","group":"Charts","viewport":"700x470","name":"Waterfall","subtitle":"Finance bridge — signed deltas, re-baselining totals, running-sum connectors"},{"path":"components/_cards/charts-9-bullet.card.html","group":"Charts","viewport":"700x470","name":"Bullet","subtitle":"Actual vs. target rows with a target tick — misses in red"},{"path":"components/_cards/charts-10-gauge.card.html","group":"Charts","viewport":"700x310","name":"Gauge","subtitle":"Semi-circular KPI — flat arc, threshold bands, no needle"},{"path":"components/_cards/charts-11-funnel.card.html","group":"Charts","viewport":"700x620","name":"Funnel","subtitle":"Stage counts with step-to-step conversion — sub-50% steps flagged"},{"path":"components/_cards/charts-12-treemap.card.html","group":"Charts","viewport":"700x450","name":"Treemap","subtitle":"Squarified composition — readable at 15+ items, hover for share"},{"path":"components/_cards/charts-14-radar.card.html","group":"Charts","viewport":"700x490","name":"Radar","subtitle":"Spider chart — a few entities across the same 3–10 measures"},{"path":"components/_cards/charts-15-distribution.card.html","group":"Charts","viewport":"700x760","name":"Histogram & Box Plot","subtitle":"Distribution of one measure · spread and outliers per group"},{"path":"components/_cards/charts-16-pareto.card.html","group":"Charts","viewport":"700x460","name":"Pareto","subtitle":"Sorted bars + cumulative % line — the vital few vs. the trivial many"},{"path":"components/_cards/charts-17-candlestick.card.html","group":"Charts","viewport":"700x730","name":"Candlestick","subtitle":"OHLC K-line — volume band, moving average, hollow-body convention"},{"path":"components/_cards/charts-18-sankey.card.html","group":"Charts","viewport":"700x550","name":"Sankey","subtitle":"Flow diagram — columns derived from the graph, never positioned by hand"},{"path":"components/_cards/charts-19-chord.card.html","group":"Charts","viewport":"700x910","name":"Chord","subtitle":"Circular Sankey — many-to-many, cyclical flows on one ring"},{"path":"components/_cards/charts-20-sunburst.card.html","group":"Charts","viewport":"700x540","name":"Sunburst","subtitle":"Hierarchy as concentric rings — click a ring to zoom, center to go back"},{"path":"components/_cards/charts-21-worldmap.card.html","group":"Charts","viewport":"700x870","name":"World Map","subtitle":"Choropleth on real Natural Earth geometry — hover, select, diverging scale"},{"path":"components/_cards/charts-22-gantt.card.html","group":"Charts","viewport":"700x590","name":"Gantt","subtitle":"Timeline of tasks — weekend tinting, progress fill, today marker, milestones"},{"path":"components/_cards/charts-23-bar3d.card.html","group":"Charts","viewport":"700x790","name":"3D Bars","subtitle":"Cuboid bars, grouped depth rows or stacked — drag to orbit, hover to read"},{"path":"components/_cards/charts-24-pie3d.card.html","group":"Charts","viewport":"700x1270","name":"3D Pie & Donut","subtitle":"Extruded and stepped-stairs slices — drag spins in place, hover lifts a slice"},{"path":"components/_cards/charts-25-surface3d.card.html","group":"Charts","viewport":"700x480","name":"3D Surface","subtitle":"z = f(x,y) heat-shaded grid — drag to orbit, hover reads a cell"},{"path":"components/_cards/charts-26-scatter3d.card.html","group":"Charts","viewport":"700x520","name":"3D Scatter","subtitle":"Points in an orientation cube — drag to orbit, hover pins x·y·z"},{"path":"components/_cards/charts-27-ribbon3d.card.html","group":"Charts","viewport":"700x510","name":"3D Ribbon","subtitle":"Series as extruded depth bands — the classic 3D area · drag to orbit"},{"path":"components/_cards/charts-99-fullpage.card.html","group":"Charts","viewport":"1512x2210","name":"Full-Page Analytics","subtitle":"Reference layout only — every chart type composed into one screen"}]
45
+ </script>
46
+ <script>
47
+ // The card list is inlined above so this page works when opened straight from disk
48
+ // (file:// blocks fetch). When served over http it re-reads _ds_manifest.json instead,
49
+ // so a rebuilt design system shows up without regenerating this file.
50
+ function render(m) {
51
+ var cards = (m.cards || []).filter(function (c) { return !/\/_dir\.card\.html$/.test(c.path || ''); }); // per-directory thumbnail stubs are tooling, not site content
52
+ var scalers = [];
53
+ var groups = {};
54
+ cards.forEach(function (c) {
55
+ var g = c.group || 'Other';
56
+ (groups[g] = groups[g] || []).push(c);
57
+ });
58
+ var order = ['Components', 'Icons', 'Colors', 'Type', 'Spacing', 'Foundations', 'Brand'];
59
+ var names = Object.keys(groups).sort(function (a, b) {
60
+ var ia = order.indexOf(a), ib = order.indexOf(b);
61
+ return (ia < 0 ? 99 : ia) - (ib < 0 ? 99 : ib) || a.localeCompare(b);
62
+ });
63
+ var out = document.getElementById('out');
64
+ out.innerHTML = '';
65
+ names.forEach(function (g) {
66
+ var h = document.createElement('h2');
67
+ h.textContent = g + ' · ' + groups[g].length;
68
+ out.appendChild(h);
69
+ var grid = document.createElement('div');
70
+ grid.className = 'grid';
71
+ grid.dataset.group = g;
72
+ groups[g].sort(function (a, b) { return (a.path || '').localeCompare(b.path || ''); }).forEach(function (c) {
73
+ var vp = (c.viewport || '700x400').split('x');
74
+ var w = +vp[0] || 700, hgt = +vp[1] || 400;
75
+ var el = document.createElement('div');
76
+ el.className = 'card';
77
+ el.dataset.name = ((c.name || '') + ' ' + (c.subtitle || '') + ' ' + g).toLowerCase();
78
+ el.innerHTML = '<figure></figure><div class="meta"><b>' + (c.name || c.path) + '</b>' +
79
+ (c.subtitle ? '<span>' + c.subtitle + '</span>' : '') +
80
+ '<a href="' + c.path + '" target="_blank">open full size ↗</a></div>';
81
+ var fig = el.querySelector('figure');
82
+ var f = document.createElement('iframe');
83
+ f.loading = 'lazy';
84
+ f.src = c.path;
85
+ f.width = w;
86
+ f.height = hgt;
87
+ fig.appendChild(f);
88
+ // Thumbnails scale to fit the fixed 210px tile; "open full size" shows them at 1:1.
89
+ // The figure height never changes, so every meta block starts at the same y in a row.
90
+ scalers.push(function () {
91
+ var s = fig.clientWidth / w;
92
+ f.style.transform = 'scale(' + s + ')';
93
+ });
94
+ grid.appendChild(el);
95
+ });
96
+ out.appendChild(grid);
97
+ });
98
+ document.getElementById('count').textContent = cards.length + ' cards';
99
+
100
+ function rescale() { scalers.forEach(function (fn) { fn(); }); }
101
+ requestAnimationFrame(rescale);
102
+ if (window.ResizeObserver) new ResizeObserver(rescale).observe(out);
103
+ else addEventListener('resize', rescale);
104
+ }
105
+
106
+ var snapshot = { cards: JSON.parse(document.getElementById('cards').textContent) };
107
+ if (location.protocol === 'file:') {
108
+ render(snapshot);
109
+ } else {
110
+ fetch('_ds_manifest.json')
111
+ .then(function (r) { return r.ok ? r.json() : Promise.reject(); })
112
+ .then(render)
113
+ .catch(function () { render(snapshot); });
114
+ }
115
+
116
+ var q = document.getElementById('q');
117
+ q.addEventListener('input', function () {
118
+ var t = q.value.trim().toLowerCase();
119
+ var shown = 0;
120
+ document.querySelectorAll('.card').forEach(function (el) {
121
+ var hit = !t || el.dataset.name.indexOf(t) > -1;
122
+ el.style.display = hit ? '' : 'none';
123
+ if (hit) shown++;
124
+ });
125
+ document.querySelectorAll('.grid').forEach(function (grid) {
126
+ var any = Array.prototype.some.call(grid.children, function (c) { return c.style.display !== 'none'; });
127
+ grid.style.display = any ? '' : 'none';
128
+ if (grid.previousElementSibling) grid.previousElementSibling.style.display = any ? '' : 'none';
129
+ });
130
+ document.getElementById('count').textContent = shown + (t ? ' of ' + document.querySelectorAll('.card').length : '') + ' cards';
131
+ });
132
+ </script>
133
+ </body></html>
@@ -0,0 +1,46 @@
1
+ /* Shared helpers for the colour specimen cards — resolves live token values from the
2
+ loaded stylesheet and computes WCAG contrast, so every label is measured, not typed. */
3
+ window.KBColor = (function () {
4
+ function parse(str) {
5
+ var m = String(str).match(/rgba?\(([^)]+)\)/);
6
+ if (!m) return null;
7
+ var p = m[1].split(/[,\s/]+/).filter(Boolean).map(Number);
8
+ return { r: p[0], g: p[1], b: p[2], a: p.length > 3 ? p[3] : 1 };
9
+ }
10
+ function hex(str) {
11
+ var c = parse(str);
12
+ if (!c) return String(str);
13
+ function h(n) { return ('0' + Math.round(n).toString(16)).slice(-2); }
14
+ return '#' + h(c.r) + h(c.g) + h(c.b);
15
+ }
16
+ function lum(c) {
17
+ var v = [c.r, c.g, c.b].map(function (x) { x /= 255; return x <= .03928 ? x / 12.92 : Math.pow((x + .055) / 1.055, 2.4); });
18
+ return .2126 * v[0] + .7152 * v[1] + .0722 * v[2];
19
+ }
20
+ /** Contrast ratio between two CSS colour strings (alpha is flattened onto `over`). */
21
+ function ratio(fg, bg, over) {
22
+ var a = parse(fg), b = parse(bg);
23
+ if (!a || !b) return 0;
24
+ if (b.a < 1 && over) { var o = parse(over); b = { r: b.r * b.a + o.r * (1 - b.a), g: b.g * b.a + o.g * (1 - b.a), b: b.b * b.a + o.b * (1 - b.a), a: 1 }; }
25
+ if (a.a < 1) { a = { r: a.r * a.a + b.r * (1 - a.a), g: a.g * a.a + b.g * (1 - a.a), b: a.b * a.a + b.b * (1 - a.a), a: 1 }; }
26
+ var l1 = lum(a), l2 = lum(b);
27
+ return (Math.max(l1, l2) + .05) / (Math.min(l1, l2) + .05);
28
+ }
29
+ /** Resolved computed value of a custom property, e.g. token('--blue-600') → "rgb(21, 93, 252)". */
30
+ function token(name, el) {
31
+ var probe = document.createElement('span');
32
+ probe.style.color = 'var(' + name + ')';
33
+ probe.style.display = 'none';
34
+ (el || document.body).appendChild(probe);
35
+ var v = getComputedStyle(probe).color;
36
+ probe.remove();
37
+ return v;
38
+ }
39
+ /** "4.7" plus a grade for normal-size text: AAA ≥7, AA ≥4.5, AA18 ≥3, else Fail. */
40
+ function grade(r) { return r >= 7 ? 'AAA' : r >= 4.5 ? 'AA' : r >= 3 ? 'AA 18px+' : 'Fail'; }
41
+ function best(bg) {
42
+ var w = ratio('rgb(255,255,255)', bg), b = ratio('rgb(10,10,10)', bg);
43
+ return w >= b ? { color: '#ffffff', ratio: w } : { color: '#0a0a0a', ratio: b };
44
+ }
45
+ return { parse: parse, hex: hex, ratio: ratio, token: token, grade: grade, best: best, fix: function (n) { return (Math.round(n * 10) / 10).toFixed(1); } };
46
+ })();
@@ -0,0 +1,9 @@
1
+ <!-- @dsCard group="Brand" viewport="700x170" name="Logo & Wordmark" subtitle="Blue cube bitmap + Inter 600 wordmark, −0.609px tracking" -->
2
+ <!DOCTYPE html>
3
+ <html><head><meta charset="utf-8"><script src="../components/_shared/frame-theme.js"></script><link rel="stylesheet" href="../styles.css"><link rel="stylesheet" href="../tokens/hover.css">
4
+ <style>body{margin:0;padding:16px;font-family:var(--font-sans);background:var(--bg-surface);display:flex;gap:12px;box-sizing:border-box}.p{flex:1;border:1px solid var(--border-card);border-radius:10px;display:flex;align-items:center;justify-content:center;gap:10px;height:128px}.p.dark{background:var(--neutral-950)}.wm{font-weight:600;font-size:15px;letter-spacing:-.609px;color:var(--text-primary)}.dark .wm{color:#fff}.cap{position:absolute;font-size:var(--text-xs)}</style></head>
5
+ <body>
6
+ <div class="p"><img src="../assets/keybricks-logo.png" width="24" height="24" alt=""><span class="wm">Keybricks</span></div>
7
+ <div class="p"><img src="../assets/keybricks-logo.png" width="48" height="48" alt=""></div>
8
+ <div class="p dark"><img src="../assets/keybricks-logo.png" width="24" height="24" alt=""><span class="wm">Keybricks</span></div>
9
+ </body></html>
@@ -0,0 +1,86 @@
1
+ <!-- @dsCard group="Colors" viewport="700x2740" name="Color List" subtitle="Every color in the system, with name and hex" -->
2
+ <!DOCTYPE html>
3
+ <html><head><meta charset="utf-8"><script src="../components/_shared/frame-theme.js"></script><link rel="stylesheet" href="../styles.css"><link rel="stylesheet" href="../tokens/hover.css">
4
+ <script src="_color-lab.js"></script>
5
+ <style>
6
+ html,body{height:100%}body{margin:0;padding:18px;box-sizing:border-box;font-family:var(--font-sans);background:var(--bg-surface);overflow-y:auto}
7
+ .sec{display:flex;align-items:center;gap:10px;padding:0 0 12px}
8
+ .sec:not(:first-child){padding-top:20px}
9
+ .sec b{font-size:var(--text-xs);font-weight:600;color:var(--text-muted);white-space:nowrap}
10
+ .sec i{flex:1;height:1px;background:var(--border-subtle)}
11
+ .cols{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:16px 24px}
12
+ .blk h3{margin:0 0 6px;font-size:var(--text-xs);font-weight:600;color:var(--text-heading)}
13
+ .item{display:flex;align-items:center;gap:8px;height:20px}
14
+ .dot{width:13px;height:13px;border-radius:3px;border:1px solid var(--border-card);box-sizing:border-box;flex-shrink:0}
15
+ .nm{flex:1;font-size:var(--text-xs);color:var(--text-body);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
16
+ .hx{font-family:var(--font-sans);font-variant-numeric:tabular-nums;font-size:var(--text-xs);color:var(--text-secondary);white-space:nowrap}
17
+ </style></head>
18
+ <body>
19
+ <div id="out"></div>
20
+ <script>
21
+ var C = window.KBColor;
22
+ var probe = document.createElement('div');
23
+ probe.setAttribute('data-theme', 'dark');
24
+ probe.style.cssText = 'position:absolute;left:-9999px';
25
+ document.body.appendChild(probe);
26
+ function dk(n){ var s=document.createElement('span'); s.style.color='var('+n+')'; probe.appendChild(s); var v=getComputedStyle(s).color; s.remove(); return v; }
27
+ function show(v){ var p=C.parse(v); return p && p.a<1 ? Math.round(p.a*100)+'% α' : C.hex(v).toUpperCase(); }
28
+
29
+ var TEN = [50,100,200,300,400,500,600,700,800,900,950];
30
+ function ramp(hue, label, steps){
31
+ return [label, (steps||TEN).map(function(s){ return ['--'+hue+'-'+s, hue+'-'+s]; })];
32
+ }
33
+ var sections = [
34
+ ['Base ramps', [
35
+ ramp('neutral','Neutral',[0,50,100,200,300,400,500,600,700,800,900,950]),
36
+ ramp('blue','Blue'), ramp('red','Red'), ramp('orange','Orange'),
37
+ ramp('amber','Amber'), ramp('emerald','Emerald'), ramp('teal','Teal'),
38
+ ramp('cyan','Cyan'), ramp('purple','Purple'), ramp('pink','Pink')
39
+ ]],
40
+ ['Semantic — light theme', [
41
+ ['Surface', [['--bg-app','bg-app'],['--bg-surface','bg-surface'],['--bg-raised','bg-raised'],['--bg-inset','bg-inset'],['--bg-hover','bg-hover'],['--bg-active','bg-active'],['--bg-press','bg-press'],['--bg-scrim','bg-scrim']]],
42
+ ['Text', [['--text-primary','text-primary'],['--text-heading','text-heading'],['--text-strong','text-strong'],['--text-body','text-body'],['--text-secondary','text-secondary'],['--text-muted','text-muted'],['--text-inverse','text-inverse'],['--on-accent','on-accent']]],
43
+ ['Border', [['--border-card','border-card'],['--border-control','border-control'],['--border-subtle','border-subtle'],['--border-strong','border-strong'],['--focus-border','focus-border'],['--focus-ring','focus-ring'],['--selected-border','selected-border'],['--checkbox-border','checkbox-border']]],
44
+ ['Action', [['--action-primary','action-primary'],['--action-primary-hover','action-primary-hover'],['--action-primary-active','action-primary-active'],['--action-dark','action-dark'],['--action-dark-hover','action-dark-hover'],['--action-dark-active','action-dark-active'],['--link','link'],['--link-hover','link-hover']]],
45
+ ['Tint (soft)', [['--soft-bg','soft-bg'],['--soft-hover','soft-hover'],['--soft-active','soft-active'],['--soft-border','soft-border'],['--info-bg','info-bg'],['--info-fg','info-fg']]],
46
+ ['Feedback', [['--success','success'],['--success-strong','success-strong'],['--success-bg','success-bg'],['--success-text','success-text'],['--warning','warning'],['--warning-bg','warning-bg'],['--warning-text','warning-text'],['--danger','danger'],['--danger-bg','danger-bg'],['--danger-text','danger-text'],['--danger-active','danger-active'],['--danger-soft-active','danger-soft-active']]],
47
+ ['Node families', [['--cat-source','cat-source'],['--cat-source-bg','cat-source-bg'],['--cat-join','cat-join'],['--cat-join-bg','cat-join-bg'],['--cat-transform','cat-transform'],['--cat-transform-bg','cat-transform-bg'],['--cat-merge','cat-merge'],['--cat-merge-bg','cat-merge-bg'],['--cat-output','cat-output'],['--cat-output-bg','cat-output-bg']]],
48
+ ['Inert & data-viz', [['--track','track'],['--skeleton','skeleton'],['--dot-neutral','dot-neutral'],['--edge','edge'],['--chart-blue','chart-blue'],['--port-border','port-border'],['--dropzone-border','dropzone-border'],['--dropzone-border-hover','dropzone-border-hover'],['--tooltip-bg','tooltip-bg'],['--tooltip-fg','tooltip-fg']]]
49
+ ]],
50
+ ['Semantic — label pills', [
51
+ ['Fill', [['--hue-gray-bg','gray-bg'],['--hue-blue-bg','blue-bg'],['--hue-green-bg','green-bg'],['--hue-teal-bg','teal-bg'],['--hue-cyan-bg','cyan-bg'],['--hue-purple-bg','purple-bg'],['--hue-pink-bg','pink-bg'],['--hue-red-bg','red-bg'],['--hue-orange-bg','orange-bg'],['--hue-amber-bg','amber-bg']]],
52
+ ['Text', [['--hue-gray-fg','gray-fg'],['--hue-blue-fg','blue-fg'],['--hue-green-fg','green-fg'],['--hue-teal-fg','teal-fg'],['--hue-cyan-fg','cyan-fg'],['--hue-purple-fg','purple-fg'],['--hue-pink-fg','pink-fg'],['--hue-red-fg','red-fg'],['--hue-orange-fg','orange-fg'],['--hue-amber-fg','amber-fg']]],
53
+ ['Neutral alpha', [['--neutral-alpha-2','alpha-2'],['--neutral-alpha-4','alpha-4'],['--neutral-alpha-8','alpha-8'],['--neutral-alpha-12','alpha-12'],['--neutral-alpha-16','alpha-16'],['--neutral-alpha-24','alpha-24'],['--neutral-alpha-32','alpha-32'],['--neutral-alpha-48','alpha-48'],['--neutral-alpha-64','alpha-64'],['--neutral-alpha-80','alpha-80']]],
54
+ ['Brand alpha & overlays', [['--brand-alpha-8','brand-alpha-8'],['--brand-alpha-12','brand-alpha-12'],['--brand-alpha-16','brand-alpha-16'],['--brand-alpha-24','brand-alpha-24'],['--brand-alpha-32','brand-alpha-32'],['--overlay-hover','overlay-hover'],['--overlay-press','overlay-press'],['--overlay-selected','overlay-selected'],['--overlay-drag','overlay-drag']]]
55
+ ]],
56
+ ['Semantic — dark theme', [
57
+ ['Surface', [['--bg-app','bg-app'],['--bg-surface','bg-surface'],['--bg-raised','bg-raised'],['--bg-inset','bg-inset'],['--bg-hover','bg-hover'],['--bg-active','bg-active'],['--bg-press','bg-press']], true],
58
+ ['Text', [['--text-primary','text-primary'],['--text-heading','text-heading'],['--text-strong','text-strong'],['--text-body','text-body'],['--text-secondary','text-secondary'],['--text-muted','text-muted'],['--text-inverse','text-inverse']], true],
59
+ ['Border & action', [['--border-card','border-card'],['--border-control','border-control'],['--border-subtle','border-subtle'],['--border-strong','border-strong'],['--action-primary','action-primary'],['--action-primary-hover','action-primary-hover'],['--action-dark','action-dark'],['--focus-border','focus-border']], true],
60
+ ['Feedback', [['--success','success'],['--success-text','success-text'],['--warning','warning'],['--warning-text','warning-text'],['--danger','danger'],['--danger-text','danger-text'],['--info-fg','info-fg'],['--tooltip-bg','tooltip-bg']], true]
61
+ ]]
62
+ ];
63
+
64
+ var out = document.getElementById('out');
65
+ sections.forEach(function (sec) {
66
+ var h = document.createElement('div');
67
+ h.className = 'sec';
68
+ h.innerHTML = '<b>' + sec[0] + '</b><i></i>';
69
+ out.appendChild(h);
70
+ var cols = document.createElement('div');
71
+ cols.className = 'cols';
72
+ sec[1].forEach(function (blk) {
73
+ var isDark = blk[2] === true;
74
+ var el = document.createElement('div');
75
+ el.className = 'blk';
76
+ var items = blk[1].map(function (t) {
77
+ var v = isDark ? dk(t[0]) : C.token(t[0]);
78
+ return '<div class="item" title="' + t[0] + ' · ' + v + '"><span class="dot" style="background:' + v + '"></span><span class="nm">' + t[1] + '</span><span class="hx">' + show(v) + '</span></div>';
79
+ }).join('');
80
+ el.innerHTML = '<h3>' + blk[0] + '</h3>' + items;
81
+ cols.appendChild(el);
82
+ });
83
+ out.appendChild(cols);
84
+ });
85
+ </script>
86
+ </body></html>