Repository navigation
Expand file tree
/
Copy pathshowcase.html
More file actions
640 lines (615 loc) · 42.3 KB
/
Copy pathshowcase.html
File metadata and controls
640 lines (615 loc) · 42.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>dakit — showcase</title>
<script>
// Same boot script the README prescribes: stored choice wins, else OS preference.
const urlTheme = new URLSearchParams(location.search).get("theme");
const stored = urlTheme === "dark" || urlTheme === "light" ? urlTheme : localStorage.getItem("dakit-theme");
document.documentElement.dataset.theme =
stored === "dark" || stored === "light"
? stored
: (matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light");
</script>
<link rel="stylesheet" href="dist/dakit.css">
<script src="dist/dialogs.js"></script>
<script src="dist/shell.js" defer></script>
<script src="dist/tabs.js" defer></script>
<script src="dist/tag-input.js"></script>
<script src="dist/collection.js"></script>
<style>
/* Showcase chrome only — everything below demonstrates the system itself. */
.page { max-width: var(--dk-size-content-max); margin: 0 auto; padding: var(--dk-space-7) var(--dk-space-6) var(--dk-space-8); display: grid; gap: var(--dk-space-7); }
.topbar { position: sticky; top: 0; z-index: 10; display: flex; align-items: center; gap: var(--dk-space-3); padding: var(--dk-space-3) 0; background: var(--dk-bg-page); border-bottom: 1px solid var(--dk-border-default); }
.topbar .wordmark { margin-right: auto; font-weight: var(--dk-weight-semibold); letter-spacing: -0.01em; }
section { display: grid; gap: var(--dk-space-4); }
section > h2 { border-bottom: 1px solid var(--dk-border-default); padding-bottom: var(--dk-space-2); }
.row { display: flex; flex-wrap: wrap; gap: var(--dk-space-3); align-items: center; }
.grid-swatches { display: grid; grid-template-columns: repeat(auto-fill, minmax(168px, 1fr)); gap: var(--dk-space-2); }
.swatch { border: 1px solid var(--dk-border-default); border-radius: var(--dk-radius-md); overflow: hidden; font-size: var(--dk-text-xs); }
.swatch .chip-color { height: 44px; border-bottom: 1px solid var(--dk-border-default); }
.swatch .chip-meta { padding: var(--dk-space-2); display: grid; gap: 2px; }
.swatch .chip-name { font-family: var(--dk-font-mono); color: var(--dk-text-primary); }
.swatch .chip-value { font-family: var(--dk-font-mono); color: var(--dk-text-muted); }
.ramp { display: grid; gap: var(--dk-space-1); font-size: var(--dk-text-xs); }
.ramp-row { display: flex; align-items: center; gap: var(--dk-space-2); }
.ramp .ramp-swatch { width: 96px; height: 22px; border: 1px solid var(--dk-border-default); border-radius: var(--dk-radius-sm); }
.ramp code { background: none; padding: 0; color: var(--dk-text-muted); }
.scale-space { display: grid; gap: var(--dk-space-1); font-size: var(--dk-text-sm); }
.scale-space .bar { height: 14px; background: var(--dk-accent-default); border-radius: var(--dk-radius-sm); }
.scale-radius { display: flex; flex-wrap: wrap; gap: var(--dk-space-3); font-size: var(--dk-text-xs); text-align: center; }
.scale-radius .box { width: 72px; height: 48px; border: 1px solid var(--dk-border-strong); background: var(--dk-bg-muted); display: grid; place-items: end center; padding-bottom: 2px; }
.type-specimen > div { display: flex; align-items: baseline; gap: var(--dk-space-4); border-bottom: 1px dashed var(--dk-border-default); padding: var(--dk-space-2) 0; }
.type-specimen code { width: 130px; flex: none; background: none; padding: 0; color: var(--dk-text-muted); }
.field-demo { display: grid; gap: var(--dk-space-1); max-width: 320px; }
legend, .caption { color: var(--dk-text-muted); font-size: var(--dk-text-sm); margin-bottom: var(--dk-space-2); }
</style>
</head>
<body>
<div class="topbar">
<span class="wordmark">dakit</span>
<span class="dk-muted" style="font-size: var(--dk-text-sm)">showcase · <span id="theme-label"></span></span>
<button id="theme-toggle" class="dk-button dk-button--secondary dk-button--sm" type="button">Toggle theme</button>
</div>
<main class="page">
<section>
<h2>Semantic roles</h2>
<p class="caption">The app-facing layer. Components and screens use these — never the raw ramps.</p>
<div class="grid-swatches" id="semantic-swatches"></div>
</section>
<section>
<h2>Primitive ramps</h2>
<p class="caption">Source values. For the design system's own use — applications don't touch these.</p>
<div class="row" style="align-items: start">
<div class="ramp" id="ramp-gray"></div>
<div class="ramp" id="ramp-blue"></div>
<div class="ramp" id="ramp-green"></div>
<div class="ramp" id="ramp-amber"></div>
<div class="ramp" id="ramp-red"></div>
</div>
</section>
<section>
<h2>Typography</h2>
<div class="type-specimen">
<div><code>--dk-text-page</code><span style="font-size: var(--dk-text-page); font-weight: var(--dk-weight-bold); line-height: var(--dk-leading-tight); color: var(--dk-text-heading)">Page title 32px</span></div>
<div><code>--dk-text-xl</code><span style="font-size: var(--dk-text-xl); font-weight: var(--dk-weight-semibold); line-height: var(--dk-leading-tight); color: var(--dk-text-heading)">Section title 22px</span></div>
<div><code>--dk-text-lg</code><span style="font-size: var(--dk-text-lg); font-weight: var(--dk-weight-semibold); color: var(--dk-text-heading)">Card heading 16px</span></div>
<div><code>--dk-text-body</code><span style="font-size: var(--dk-text-body)">Body copy 14px — prose reads in Inter.</span></div>
<div><code>--dk-text-md</code><span style="font-size: var(--dk-text-md)">Controls & tables 13px</span></div>
<div><code>--dk-text-sm</code><span style="font-size: var(--dk-text-sm)">Labels & hints 12px</span></div>
<div><code>--dk-text-xs</code><span style="font-size: var(--dk-text-xs)">Chips & badges 11px</span></div>
<div><code>--dk-font-mono</code><span class="dk-mono" style="font-size: var(--dk-text-md)">quantities, timings, IDs: 1,024 rows · 82ms · wf_9f2c</span></div>
</div>
</section>
<section>
<h2>Space & radius</h2>
<div class="row" style="align-items: start; gap: var(--dk-space-7)">
<div class="scale-space" id="scale-space"></div>
<div class="scale-radius" id="scale-radius"></div>
</div>
</section>
<section>
<h2>Buttons</h2>
<div class="row">
<button class="dk-button dk-button--primary">Primary action</button>
<button class="dk-button dk-button--secondary">Secondary</button>
<button class="dk-button dk-button--danger">Delete…</button>
<button class="dk-button dk-button--primary" disabled>Disabled</button>
</div>
<div class="row">
<button class="dk-button dk-button--secondary dk-button--sm">Toolbar sm</button>
<button class="dk-button dk-button--primary">Default md</button>
<button class="dk-button dk-button--primary dk-button--lg">Page-level lg</button>
</div>
</section>
<section>
<h2>Fields</h2>
<div class="row" style="align-items: start">
<div class="field-demo">
<label class="dk-label" for="demo-input">Workflow name</label>
<input class="dk-input" id="demo-input" placeholder="e.g. nightly-sync">
<p class="dk-hint">Lowercase letters, digits, dashes.</p>
</div>
<div class="field-demo">
<label class="dk-label" for="demo-select">Region</label>
<select class="dk-select" id="demo-select"><option>eu-central-1</option><option>us-east-1</option></select>
</div>
<div class="field-demo">
<label class="dk-label" for="demo-notes">Notes</label>
<textarea class="dk-textarea" id="demo-notes" placeholder="Anything worth remembering…"></textarea>
</div>
</div>
</section>
<section>
<h2>Status</h2>
<p class="caption">Status hues report status — status pairs (text on tinted bg) are contrast-checked at build time.</p>
<div class="row">
<span class="dk-badge dk-badge--success"><span class="dk-dot dk-dot--success"></span>Healthy</span>
<span class="dk-badge dk-badge--warning"><span class="dk-dot dk-dot--warning"></span>Due soon</span>
<span class="dk-badge dk-badge--danger"><span class="dk-dot dk-dot--danger"></span>Overdue</span>
<span class="dk-badge dk-badge--info"><span class="dk-dot dk-dot--info"></span>Running</span>
<span class="dk-badge"><span class="dk-dot dk-dot--neutral"></span>Paused</span>
</div>
<div class="row">
<span class="dk-chip">prod</span>
<span class="dk-chip">staging</span>
<span class="dk-chip">wf_9f2c</span>
</div>
</section>
<section>
<h2>Cards & tables</h2>
<div class="dk-card">
<h3 style="margin-bottom: var(--dk-space-2)">Recent runs</h3>
<table class="dk-table">
<thead><tr><th>Workflow</th><th>Status</th><th>Duration</th><th style="text-align: right">Rows</th></tr></thead>
<tbody>
<tr><td class="dk-mono">nightly-sync</td><td><span class="dk-badge dk-badge--success"><span class="dk-dot dk-dot--success"></span>ok</span></td><td class="dk-mono">3m 12s</td><td class="dk-mono" style="text-align: right">1,024</td></tr>
<tr><td class="dk-mono">invoice-parser</td><td><span class="dk-badge dk-badge--warning"><span class="dk-dot dk-dot--warning"></span>retrying</span></td><td class="dk-mono">48s</td><td class="dk-mono" style="text-align: right">96</td></tr>
<tr><td class="dk-mono">webhook-fanout</td><td><span class="dk-badge dk-badge--danger"><span class="dk-dot dk-dot--danger"></span>failed</span></td><td class="dk-mono">2s</td><td class="dk-mono" style="text-align: right">0</td></tr>
</tbody>
</table>
</div>
</section>
<section>
<h2>Form footer</h2>
<div class="dk-card" style="padding: 0">
<div style="padding: var(--dk-space-4)">
<label class="dk-label" for="demo-footer-field">Workflow name</label>
<input class="dk-input" id="demo-footer-field" placeholder="e.g. nightly-sync">
</div>
<div class="dk-form-actions">
<button class="dk-button dk-button--primary">Save changes</button>
<button class="dk-button dk-button--secondary">Cancel</button>
</div>
</div>
</section>
<section>
<h2>Layout: toolbar, register footer, dialog</h2>
<p class="caption">One toolbar above a register (filters left, count, actions right), the register's footer band inside its border, and a dialog that becomes a full-screen sheet at 820px and below. Gaps are the --dk-gap-* roles. Clicking the backdrop closes the dialog (dist/dialogs.js).</p>
<div class="dk-toolbar">
<div class="dk-cluster">
<input class="dk-input" style="width: 220px" type="search" placeholder="Search runs" aria-label="Search runs">
<select class="dk-select" style="width: 160px" aria-label="Status"><option>All statuses</option></select>
<span class="dk-muted" style="font-size: var(--dk-text-sm)">25 runs</span>
</div>
<div class="dk-toolbar__end">
<button class="dk-button dk-button--secondary">Export CSV</button>
<button class="dk-button dk-button--primary" id="demo-dialog-open">Open dialog</button>
</div>
</div>
<div class="dk-card" style="padding: 0; overflow: hidden">
<table class="dk-table">
<thead><tr><th>Run</th><th>Status</th><th>Started</th></tr></thead>
<tbody>
<tr><td>nightly-sync</td><td><span class="dk-dot dk-dot--success"></span> Succeeded</td><td class="dk-mono">2026-10-08 17:56</td></tr>
<tr><td>invoice-parser</td><td><span class="dk-dot dk-dot--danger"></span> Failed</td><td class="dk-mono">2026-10-08 09:22</td></tr>
</tbody>
</table>
<div class="dk-table-footer"><span>2 of 40 runs</span><button class="dk-button dk-button--secondary dk-button--sm">Load more</button></div>
</div>
<dialog class="dk-dialog" id="demo-dialog" aria-labelledby="demo-dialog-title">
<form method="dialog" style="display: contents">
<div class="dk-dialog__head"><h2 id="demo-dialog-title">Revoke token</h2></div>
<div class="dk-dialog__body"><p>The token stops working at once. Agents using it must be given a new one.</p></div>
<div class="dk-dialog__foot">
<button class="dk-button dk-button--secondary" value="cancel">Cancel</button>
<button class="dk-button dk-button--primary" value="ok">Revoke</button>
</div>
</form>
</dialog>
<script>document.getElementById("demo-dialog-open").addEventListener("click", () => document.getElementById("demo-dialog").showModal());</script>
</section>
<section id="collection">
<h2>Action placement: collection header</h2>
<p class="caption">The create action sits right after the title of the collection it creates into, never in the page header's corner; the count and secondary actions trail. On a board or table it follows the filter that scopes it. The page's one create button answers <kbd class="dk-kbd">c</kbd>; an empty collection repeats it as a secondary button. Built by dist/collection.js (dakitCollection.header / createButton and the markup twins). See docs/guidelines.md "Action placement".</p>
<div class="dk-card" style="padding: 0; overflow: hidden" id="demo-collection">
<div style="padding: var(--dk-space-3) var(--dk-space-4); border-bottom: 1px solid var(--dk-border-default); background: var(--dk-bg-muted)" data-demo-collection-head></div>
<table class="dk-table">
<thead><tr><th>Schedule</th><th>Repeats</th><th>Next run</th></tr></thead>
<tbody>
<tr><td>Newsletter draft</td><td>Weekly · Mon</td><td class="dk-mono">2026-10-19</td></tr>
<tr><td>Sponsor invoice chase</td><td>Monthly · 1st</td><td class="dk-mono">2026-11-01</td></tr>
</tbody>
</table>
</div>
<p class="dk-muted" role="status" data-demo-collection-status style="font-size: var(--dk-text-sm)"></p>
<div class="dk-toolbar" style="margin-bottom: 0">
<div class="dk-collection-header__lead">
<div class="dk-tabs" role="radiogroup" aria-label="Filter runs by status">
<button class="dk-tab" type="button" role="radio" aria-checked="true" tabindex="0">All <span class="dk-count">25</span></button>
<button class="dk-tab" type="button" role="radio" aria-checked="false" tabindex="-1">Failed</button>
</div>
<button type="button" class="dk-button dk-button--secondary dk-button--sm dk-collection-create"><span class="dk-collection-create__icon" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M12 5v14M5 12h14"/></svg></span><span>New run</span></button>
</div>
<div class="dk-toolbar__end"><span class="dk-muted dk-mono" style="font-size: var(--dk-text-sm)">25 runs</span><button class="dk-icon-button" type="button" aria-label="Refresh runs" title="Refresh runs"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 12a8 8 0 1 1-2.34-5.66M20 4v4h-4"/></svg></button></div>
</div>
<div class="dk-card" style="padding: 0">
<div style="padding: var(--dk-space-3) var(--dk-space-4); border-bottom: 1px solid var(--dk-border-default); background: var(--dk-bg-muted)" data-demo-collection-empty-head></div>
<div class="dk-empty dk-empty--compact">
<p class="dk-empty__title">No webhooks yet</p>
<p class="dk-empty__desc">A webhook posts each run's result to another service.</p>
<div class="dk-empty__actions" data-demo-collection-empty-action></div>
</div>
</div>
<script>
(() => {
const status = document.querySelector("[data-demo-collection-status]");
const say = (text) => () => { status.textContent = text; };
// The page's one create: primary, with the c shortcut.
document.querySelector("[data-demo-collection-head]").append(dakitCollection.header({
title: "Schedules", level: "h3", meta: "2 schedules",
create: dakitCollection.createButton({ label: "New schedule", onClick: say("New schedule clicked") }),
}));
// A second collection on the same page: secondary, no shortcut.
document.querySelector("[data-demo-collection-empty-head]").innerHTML = dakitCollection.headerMarkup({
title: "Webhooks", level: "h3",
createHtml: dakitCollection.createButtonMarkup({ label: "Add webhook", variant: "secondary", shortcut: false }),
});
document.querySelector("[data-demo-collection-empty-action]").append(dakitCollection.createButton({
label: "Add the first webhook", variant: "secondary", shortcut: false, onClick: say("Add the first webhook clicked"),
}));
})();
</script>
</section>
<section id="tag-input">
<h2>Tag input: multi-select combobox</h2>
<p class="caption">Pick several values from a known vocabulary: chosen values are chips with a remove button, typing filters the rest (label and aliases, case-insensitive), ↑/↓ and Enter add, Backspace on an empty field removes the last chip, Esc closes. Use it instead of a checkbox grid once the vocabulary outgrows a handful of options. Built by dist/tag-input.js (dakitTagInput.create). See docs/tag-input.md.</p>
<div class="row" style="align-items: flex-start">
<div class="field-demo" style="flex: 1 1 280px">
<span class="dk-label" id="demo-tags-needs-label">Needs</span>
<div data-demo-tags-needs></div>
<p class="dk-hint">Try “yt”, “mail” or “zz”.</p>
</div>
<div class="field-demo" style="flex: 1 1 280px">
<span class="dk-label" id="demo-tags-people-label">Reviewers (grouped)</span>
<div data-demo-tags-people></div>
</div>
<div class="field-demo" style="flex: 1 1 280px">
<span class="dk-label" id="demo-tags-off-label">Disabled</span>
<div data-demo-tags-off></div>
</div>
</div>
<p class="dk-muted dk-mono" role="status" data-demo-tags-status style="font-size: var(--dk-text-sm)"></p>
<script>
(() => {
const svg = (d) => () => {
const el = document.createElementNS("http://www.w3.org/2000/svg", "svg");
el.setAttribute("viewBox", "0 0 24 24"); el.setAttribute("fill", "none"); el.setAttribute("stroke", "currentColor");
el.setAttribute("stroke-width", "1.8"); el.setAttribute("stroke-linecap", "round"); el.setAttribute("stroke-linejoin", "round");
el.innerHTML = d; return el;
};
const video = svg('<rect x="3" y="6" width="13" height="12" rx="2"/><path d="M16 10l5-3v10l-5-3"/>');
const mail = svg('<rect x="3" y="5" width="18" height="14" rx="2"/><path d="M3 7l9 6 9-6"/>');
const cal = svg('<rect x="3" y="5" width="18" height="16" rx="2"/><path d="M8 3v4M16 3v4M3 10h18"/>');
const bank = svg('<path d="M3 10l9-6 9 6M5 10v8M19 10v8M9 10v8M15 10v8M3 20h18"/>');
const status = document.querySelector("[data-demo-tags-status]");
const show = (name) => (values, change) => { status.textContent = `${name}: ${change.added ? "+" + change.added : "−" + change.removed} → [${values.join(", ")}]`; };
const needs = [
{ value: "youtube", label: "YouTube", aliases: ["yt", "video"], icon: video },
{ value: "streamyard", label: "StreamYard", aliases: ["stream"], icon: video },
{ value: "zoom", label: "Zoom", icon: video },
{ value: "mailchimp", label: "Mailchimp", aliases: ["newsletter", "email"], icon: mail },
{ value: "gmail", label: "Gmail", aliases: ["email"], icon: mail },
{ value: "luma", label: "Luma", aliases: ["events"], icon: cal },
{ value: "google-calendar", label: "Google Calendar", aliases: ["gcal"], icon: cal },
{ value: "finom", label: "Finom", aliases: ["bank"], icon: bank },
{ value: "accountant", label: "Accountant portal", aliases: ["bookkeeping"], icon: bank },
];
const a = dakitTagInput.create({ labelledBy: "demo-tags-needs-label", label: "Needs", placeholder: "Add a resource…", options: needs, value: ["finom", "youtube"], onChange: show("Needs") });
document.querySelector("[data-demo-tags-needs]").append(a.element);
const people = [
["Data", "Ana Kovač"], ["Data", "Bruno Silva"], ["Data", "Chen Wei"],
["Ops", "Dana Ivanova"], ["Ops", "Emeka Obi"], ["Ops", "Farah Haddad"], ["Ops", "Gita Rao"],
["Design", "Hugo Martin"], ["Design", "Ines Costa"],
].map(([group, label]) => ({ value: label.toLowerCase().replace(/\W+/g, "-"), label, group, icon: label.split(" ").map((w) => w[0]).join("") }));
const b = dakitTagInput.create({ labelledBy: "demo-tags-people-label", label: "Reviewers", placeholder: "Add a reviewer…", options: people, onChange: show("Reviewers") });
document.querySelector("[data-demo-tags-people]").append(b.element);
const c = dakitTagInput.create({ labelledBy: "demo-tags-off-label", label: "Disabled", options: needs, value: ["zoom"], disabled: true });
document.querySelector("[data-demo-tags-off]").append(c.element);
})();
</script>
</section>
<section id="segmented">
<h2>Segmented control</h2>
<p class="caption">The one look for view switchers and list filters (docs/segmented.md): a track of segments, the selected one in the kit selection. Pick the semantics by what it does: route links use <code>aria-current</code>, in-page views a <code>tablist</code>, a single-choice filter a <code>radiogroup</code>. Arrow keys move the choice in the last two. Too many segments scroll inside the track; they never wrap.</p>
<div class="row">
<span class="caption" style="margin: 0">Route views (links):</span>
<nav class="dk-tabs" aria-label="Task views"><a class="dk-tab" href="#segmented" aria-current="page">Open <span class="dk-count">12</span></a><a class="dk-tab" href="#segmented">Waiting <span class="dk-count">4</span></a><a class="dk-tab" href="#segmented">Done</a></nav>
</div>
<div class="row">
<span class="caption" style="margin: 0">In-page views (tablist):</span>
<div class="dk-tabs" role="tablist" aria-label="Run views" id="demo-tablist">
<button class="dk-tab" type="button" role="tab" id="demo-tab-steps" aria-controls="demo-panel-steps" aria-selected="true" tabindex="0">Steps</button>
<button class="dk-tab" type="button" role="tab" id="demo-tab-log" aria-controls="demo-panel-log" aria-selected="false" tabindex="-1">Log</button>
<button class="dk-tab" type="button" role="tab" id="demo-tab-input" aria-controls="demo-panel-input" aria-selected="false" tabindex="-1">Input</button>
<button class="dk-tab" type="button" role="tab" aria-selected="false" aria-disabled="true" tabindex="-1">Output</button>
</div>
</div>
<div class="dk-card dk-card--muted" id="demo-panel-steps" role="tabpanel" aria-labelledby="demo-tab-steps">12 steps, all succeeded.</div>
<div class="dk-card dk-card--muted" id="demo-panel-log" role="tabpanel" aria-labelledby="demo-tab-log" hidden>Log: 1,024 lines.</div>
<div class="dk-card dk-card--muted" id="demo-panel-input" role="tabpanel" aria-labelledby="demo-tab-input" hidden>Input: <span class="dk-mono">{ "since": "2026-10-01" }</span></div>
<div class="row">
<span class="caption" style="margin: 0">List filter (radiogroup):</span>
<div class="dk-tabs" role="radiogroup" aria-label="Filter queue by assignee" id="demo-filter">
<button class="dk-tab" type="button" role="radio" aria-checked="true" tabindex="0">All <span class="dk-count">678</span></button>
<button class="dk-tab" type="button" role="radio" aria-checked="false" tabindex="-1">Grace <span class="dk-count">3</span></button>
<button class="dk-tab" type="button" role="radio" aria-checked="false" tabindex="-1">Valeriia <span class="dk-count">4</span></button>
<button class="dk-tab" type="button" role="radio" aria-checked="false" tabindex="-1">Alexey</button>
</div>
<span class="dk-muted dk-mono" id="demo-filter-out" style="font-size: var(--dk-text-sm)">Showing 678</span>
</div>
<div class="row">
<span class="caption" style="margin: 0">Compact (<code>--sm</code>):</span>
<div class="dk-tabs dk-tabs--sm" role="radiogroup" aria-label="Notifications">
<button class="dk-tab" type="button" role="radio" aria-checked="true" tabindex="0">Unread <span class="dk-count">3</span></button>
<button class="dk-tab" type="button" role="radio" aria-checked="false" tabindex="-1">All</button>
</div>
<span class="caption" style="margin: 0">Multi-select toggles (<code>aria-pressed</code>):</span>
<div class="dk-tabs dk-tabs--sm" role="group" aria-label="Show statuses">
<button class="dk-tab" type="button" aria-pressed="true">Failed</button>
<button class="dk-tab" type="button" aria-pressed="true">Running</button>
<button class="dk-tab" type="button" aria-pressed="false">Succeeded</button>
</div>
</div>
<div class="row">
<span class="caption" style="margin: 0">Overflow (scrolls inside the track):</span>
<div style="max-width: 320px; display: flex; min-width: 0">
<div class="dk-tabs" role="radiogroup" aria-label="Newsletter issues">
<button class="dk-tab" type="button" role="radio" aria-checked="true" tabindex="0">All issues</button>
<button class="dk-tab" type="button" role="radio" aria-checked="false" tabindex="-1">Upcoming <span class="dk-count">5</span></button>
<button class="dk-tab" type="button" role="radio" aria-checked="false" tabindex="-1">Drafting <span class="dk-count">2</span></button>
<button class="dk-tab" type="button" role="radio" aria-checked="false" tabindex="-1">Sent & archive</button>
</div>
</div>
</div>
<script>
(() => {
// The app owns the choice: a click flips the ARIA state (and panels);
// dist/tabs.js only adds keyboard movement and the roving tabindex.
const choose = (group, attr, onPick) => group.addEventListener("click", (e) => {
const seg = e.target.closest(".dk-tab");
if (!seg || seg.getAttribute("aria-disabled") === "true") return;
group.querySelectorAll(".dk-tab").forEach((el) => el.setAttribute(attr, String(el === seg)));
if (onPick) onPick(seg);
});
document.querySelectorAll('#segmented .dk-tabs[role="radiogroup"]').forEach((g) => choose(g, "aria-checked", g.id === "demo-filter"
? (seg) => { document.getElementById("demo-filter-out").textContent = `Showing ${seg.querySelector(".dk-count")?.textContent || 0}`; }
: null));
choose(document.getElementById("demo-tablist"), "aria-selected", (seg) => {
document.querySelectorAll('#segmented [role="tabpanel"]').forEach((p) => { p.hidden = p.id !== seg.getAttribute("aria-controls"); });
});
document.querySelectorAll('#segmented .dk-tabs[role="group"] .dk-tab').forEach((b) => b.addEventListener("click", () => b.setAttribute("aria-pressed", String(b.getAttribute("aria-pressed") !== "true"))));
document.querySelectorAll('#segmented nav.dk-tabs .dk-tab').forEach((a) => a.addEventListener("click", (e) => {
e.preventDefault();
a.parentElement.querySelectorAll(".dk-tab").forEach((el) => (el === a ? el.setAttribute("aria-current", "page") : el.removeAttribute("aria-current")));
}));
})();
</script>
</section>
<section id="loading">
<h2>Loading data for a dialog</h2>
<p class="caption">A dialog that needs data opens only when the data is in (docs/dialog-loading.md). Until then the trigger is busy: same size, spinner after 150ms, a second click ignored. Past 500ms the dialog backdrop appears with a centred spinner; Escape or a backdrop click cancels. A failed load opens nothing and shows a toast with Retry. A row has no room for a spinner, so it gets the overlay at 150ms.</p>
<div class="row">
<button class="dk-button dk-button--primary" type="button" data-demo-load="80">Open run (fast)</button>
<button class="dk-button dk-button--secondary" type="button" data-demo-load="1500">Open run (slow)</button>
<button class="dk-button dk-button--secondary" type="button" data-demo-load="600" data-demo-fail>Open run (fails)</button>
</div>
<div class="row">
<span class="caption" style="margin: 0">Busy states:</span>
<button class="dk-button dk-button--primary" type="button" aria-busy="true" aria-disabled="true">Save changes</button>
<button class="dk-button dk-button--secondary dk-button--sm" type="button" aria-busy="true" aria-disabled="true">Details</button>
<button class="dk-icon-button" type="button" aria-label="Edit" aria-busy="true" aria-disabled="true"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path d="M3 13h3l7-7-3-3-7 7z"/></svg></button>
<span class="dk-spinner" role="status" aria-label="Loading"></span>
<span class="dk-spinner dk-spinner--lg" role="status" aria-label="Loading"></span>
</div>
<div class="dk-card" style="padding: 0; overflow: hidden">
<table class="dk-table">
<thead><tr><th>Run</th><th>Status</th><th>Started</th></tr></thead>
<tbody>
<tr tabindex="0" style="cursor: pointer" data-demo-load="900"><td>nightly-sync</td><td><span class="dk-dot dk-dot--success"></span> Succeeded</td><td class="dk-mono">2026-10-08 17:56</td></tr>
<tr tabindex="0" style="cursor: pointer" data-demo-load="900"><td>invoice-parser</td><td><span class="dk-dot dk-dot--danger"></span> Failed</td><td class="dk-mono">2026-10-08 09:22</td></tr>
</tbody>
</table>
</div>
<dialog class="dk-dialog" id="run-dialog" aria-labelledby="run-dialog-title">
<form method="dialog" style="display: contents">
<div class="dk-dialog__head"><div><h2 id="run-dialog-title"></h2><p id="run-dialog-meta" class="dk-mono"></p></div></div>
<div class="dk-dialog__body"><dl class="dk-dl" id="run-dialog-body"></dl></div>
<div class="dk-dialog__foot"><button class="dk-button dk-button--primary" value="ok">Done</button></div>
</form>
</dialog>
<script>
(() => {
const dialog = document.getElementById("run-dialog");
// A fake API: resolves after `delay`, or fails; honours the abort signal.
const fetchRun = (name, delay, fail) => (signal) => new Promise((ok, no) => {
const t = setTimeout(() => (fail ? no(new Error("503 Service Unavailable")) : ok({ name, id: "r1038", steps: 12, duration: "4m 12s" })), delay);
signal.addEventListener("abort", () => { clearTimeout(t); no(signal.reason); });
});
const render = (run) => {
dialog.querySelector("#run-dialog-title").textContent = run.name;
dialog.querySelector("#run-dialog-meta").textContent = run.id;
dialog.querySelector("#run-dialog-body").innerHTML = "<dt>Steps</dt><dd class=dk-mono></dd><dt>Duration</dt><dd class=dk-mono></dd>";
const dd = dialog.querySelectorAll("#run-dialog-body dd");
dd[0].textContent = run.steps; dd[1].textContent = run.duration;
return dialog;
};
const open = (trigger) => {
const name = trigger.matches("tr") ? trigger.cells[0].textContent : "nightly-sync";
dakitDialogs.openWhenReady(trigger, fetchRun(name, Number(trigger.dataset.demoLoad), trigger.hasAttribute("data-demo-fail")), render, { label: "Loading run…" });
};
document.querySelectorAll("#loading [data-demo-load]").forEach((el) => {
el.addEventListener("click", () => open(el));
if (el.matches("tr")) el.addEventListener("keydown", (e) => { if (e.key === "Enter") open(el); });
});
})();
</script>
</section>
<section>
<h2>App shell, list–detail & device login</h2>
<p class="caption">Full-page demos with every state (docs/shell.md, docs/device-login.md). Add <code>?state=account|notif|rail|rail-notif|drawer</code> or <code>?variant=topbar</code> to the shell, <code>?state=review-warning|approved|notfound…</code> or <code>?code=…</code> to the device page.</p>
<div class="row">
<a class="dk-button dk-button--secondary" href="showcase-shell.html">App shell</a>
<a class="dk-button dk-button--secondary" href="showcase-shell.html?variant=topbar">Topbar shell</a>
<a class="dk-button dk-button--secondary" href="showcase-device.html">Device login</a>
<a class="dk-button dk-button--secondary" href="showcase-split.html?run=r1038">List–detail split</a>
</div>
</section>
<section>
<h2>Workspace components</h2>
<p class="caption">Shared by two or more apps (docs/shell.md §9). Status is fill-only; selection is the accent-soft fill.</p>
<header class="dk-page-header">
<div class="dk-page-header__main">
<nav class="dk-page-header__crumb" aria-label="Breadcrumb"><a href="#">Tasks</a><span aria-hidden="true">/</span><span class="dk-mono">T-1042</span></nav>
<h3 class="dk-page-header__title">Confirm guest for episode 42</h3>
<p class="dk-page-header__subtitle">Podcast · waiting on reply since Tue</p>
</div>
<div class="dk-page-header__actions"><button class="dk-button dk-button--secondary" type="button">Snooze</button><button class="dk-button dk-button--primary" type="button">Mark done</button></div>
</header>
<!-- Performance views only (principle 4): every figure carries a comparison. -->
<div class="dk-stat-strip" aria-label="Issue #212 performance">
<div class="dk-stat"><span class="dk-stat__label">Open rate</span><span class="dk-stat__value">48.3%</span><span class="dk-stat__meta">+2.1 pp vs last 4 issues</span></div>
<div class="dk-stat"><span class="dk-stat__label">Click rate</span><span class="dk-stat__value">6.4%</span><span class="dk-stat__meta">−0.8 pp vs last 4 issues</span></div>
<div class="dk-stat"><span class="dk-stat__label">Unsubscribes</span><span class="dk-stat__value">0.12%</span><span class="dk-stat__meta">median 0.15%</span></div>
</div>
<div class="dk-stack dk-stack--tight">
<div class="dk-callout"><div class="dk-callout__body"><span class="dk-callout__title">Neutral</span><span>Read-only until the import finishes.</span></div></div>
<div class="dk-callout dk-callout--info"><div class="dk-callout__body"><span>Two tasks were reassigned to you.</span></div><div class="dk-callout__actions"><button class="dk-button dk-button--secondary dk-button--sm" type="button">Review</button></div></div>
<div class="dk-callout dk-callout--success"><div class="dk-callout__body"><span>Export finished: 1,024 rows.</span></div></div>
<div class="dk-callout dk-callout--warning"><div class="dk-callout__body"><span class="dk-callout__title">Token expires in 3 days.</span><span>Rotate it before Friday.</span></div></div>
<div class="dk-callout dk-callout--danger"><div class="dk-callout__body"><span>Couldn't load notifications.</span></div><div class="dk-callout__actions"><button class="dk-button dk-button--secondary dk-button--sm" type="button">Retry</button></div></div>
</div>
<div class="row" style="align-items: start; gap: var(--dk-space-7)">
<dl class="dk-dl dk-dl--divided" style="min-width: 320px">
<dt>Machine</dt><dd class="dk-mono">grace@build-box</dd>
<dt>Requested from</dt><dd class="dk-mono">203.0.113.7</dd>
<dt>Expires</dt><dd class="dk-mono">2026-11-13 14:02</dd>
</dl>
<div class="dk-card" style="padding: 0; flex: 1; min-width: 280px"><div class="dk-empty dk-empty--compact"><p class="dk-empty__title">No CLI sessions</p><p class="dk-empty__desc">Run <code>dataops login</code> on a machine to create one.</p><div class="dk-empty__actions"><button class="dk-button dk-button--secondary dk-button--sm" type="button">Read the CLI guide</button></div></div></div>
</div>
<div class="row">
<button class="dk-switch" type="button" role="switch" aria-checked="false" aria-label="Off example"></button>
<button class="dk-switch" type="button" role="switch" aria-checked="true" aria-label="On example"></button>
<span class="dk-avatar dk-avatar--xs">G</span><span class="dk-avatar dk-avatar--sm">GH</span><span class="dk-avatar">GH</span><span class="dk-avatar dk-avatar--lg">GH</span>
<span class="dk-count">4</span><span class="dk-count">99+</span>
<kbd class="dk-kbd">/</kbd><kbd class="dk-kbd">⌘B</kbd>
<button class="dk-icon-button" type="button" aria-label="Notifications, 3 unread"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/></svg><span class="dk-count" aria-hidden="true">3</span></button>
</div>
<div class="dk-code-block" style="max-width: 560px"><pre><code>dataops login --profile prod</code></pre><button class="dk-button dk-button--secondary dk-button--sm" type="button" data-dk-copy>Copy</button></div>
</section>
<section>
<h2>Icons</h2>
<p class="caption">The canonical shared shapes — 24×24 paths rendered at 20px, stroke currentColor 1.8, round caps/joins, no fills (dataops geometry). Copy from here so strokes stay identical across apps; nav and rows always pair icon + text.</p>
<div class="row" id="icon-set" style="gap: var(--dk-space-4)"></div>
<script>
(() => {
const shapes = {
"chevron-right": '<path d="m9 18 6-6-6-6"/>',
"chevron-down": '<path d="m6 9 6 6 6-6"/>',
plus: '<path d="M5 12h14"/><path d="M12 5v14"/>',
search: '<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>',
check: '<path d="M20 6 9 17l-5-5"/>',
"alert-triangle": '<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 20h16a2 2 0 0 0 1.73-2"/><path d="M12 9v4"/><path d="M12 17h.01"/>',
x: '<path d="M18 6 6 18"/><path d="m6 6 12 12"/>',
"external-link": '<path d="M15 3h6v6"/><path d="M10 14 21 3"/><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>',
clock: '<circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/>',
user: '<path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>',
doc: '<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/>',
mail: '<rect width="20" height="16" x="2" y="4" rx="2"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/>',
refresh: '<path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/>'
};
const host = document.getElementById("icon-set");
for (const [name, body] of Object.entries(shapes)) {
const wrap = document.createElement("span");
wrap.style.cssText = "display:inline-flex;flex-direction:column;align-items:center;gap:4px;color:var(--dk-text-primary)";
wrap.innerHTML =
`<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${body}</svg>` +
`<span class="caption">${name}</span>`;
host.appendChild(wrap);
}
})();
</script>
</section>
</main>
<script src="dist/tokens.global.js"></script>
<script>
const { light, primitives } = globalThis.dakitTokens;
const pick = (token, el) => getComputedStyle(el).getPropertyValue(token).trim() || "—";
// Semantic swatches: name + live computed value for the current theme.
const groups = {
"bg": ["page", "surface", "muted", "hover", "inset"],
"text": ["primary", "heading", "muted", "faint", "link", "on-accent"],
"border": ["default", "strong"],
"accent": ["default", "hover", "soft"],
"success": ["bg", "text"],
"warning": ["bg", "text"],
"danger": ["bg", "text"],
"info": ["bg", "text"],
};
const swatchGrid = document.getElementById("semantic-swatches");
function renderSwatches() {
swatchGrid.innerHTML = "";
for (const [group, roles] of Object.entries(groups)) {
for (const role of roles) {
const token = `--dk-${group}-${role}`;
const cell = document.createElement("div");
cell.className = "swatch";
cell.innerHTML = `<div class="chip-color"></div><div class="chip-meta"><span class="chip-name"></span><span class="chip-value"></span></div>`;
cell.querySelector(".chip-color").style.background = `var(${token})`;
cell.querySelector(".chip-name").textContent = token;
swatchGrid.append(cell);
cell.querySelector(".chip-value").textContent = pick(token, cell);
}
}
}
// Primitive ramps, straight from the generated token map.
const ramps = { "ramp-gray": "gray", "ramp-blue": "blue", "ramp-green": "green", "ramp-amber": "amber", "ramp-red": "red" };
for (const [elId, ramp] of Object.entries(ramps)) {
const host = document.getElementById(elId);
for (const [name, value] of Object.entries(primitives)) {
if (!name.startsWith(`--dk-${ramp}-`)) continue;
const row = document.createElement("div");
row.className = "ramp-row";
const sw = document.createElement("span");
sw.className = "ramp-swatch";
sw.style.background = value;
const label = document.createElement("code");
label.textContent = `${name.slice(5)} ${value}`;
row.append(sw, label);
host.append(row);
}
}
// Scales
const spaceHost = document.getElementById("scale-space");
for (let i = 1; i <= 8; i++) {
const row = document.createElement("div");
row.className = "ramp-row";
const bar = document.createElement("span");
bar.className = "bar";
bar.style.width = `var(--dk-space-${i})`;
const label = document.createElement("code");
label.textContent = `space-${i}`;
row.append(bar, label);
spaceHost.append(row);
}
const radiusHost = document.getElementById("scale-radius");
for (const r of ["none", "sm", "md", "lg", "full"]) {
const box = document.createElement("div");
box.innerHTML = `<div class="box" style="border-radius: var(--dk-radius-${r})"></div><code>radius-${r}</code>`;
radiusHost.append(box);
}
// Theme toggle, mirroring the README contract.
const root = document.documentElement;
const label = document.getElementById("theme-label");
const sync = () => { label.textContent = root.dataset.theme; renderSwatches(); };
document.getElementById("theme-toggle").addEventListener("click", () => {
root.dataset.theme = root.dataset.theme === "dark" ? "light" : "dark";
localStorage.setItem("dakit-theme", root.dataset.theme);
sync();
});
sync();
</script>
</body>
</html>