@zauso-ai/capstan-surface-web 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1701 @@
1
+ const attentionQueueStatusOrder = [
2
+ "approval_required",
3
+ "input_required",
4
+ "blocked",
5
+ "failed",
6
+ "paused",
7
+ "cancelled"
8
+ ];
9
+ const supervisionWorkspaceSlots = [
10
+ {
11
+ key: "primary",
12
+ label: "Primary"
13
+ },
14
+ {
15
+ key: "secondary",
16
+ label: "Secondary"
17
+ },
18
+ {
19
+ key: "watchlist",
20
+ label: "Watchlist"
21
+ }
22
+ ];
23
+ export function projectHumanSurface(graph) {
24
+ const policiesByKey = new Map(graph.policies.map((policy) => [policy.key, policy]));
25
+ const resourcesByKey = new Map(graph.resources.map((resource) => [resource.key, resource]));
26
+ const tasksByKey = new Map(graph.tasks.map((task) => [task.key, task]));
27
+ const capabilitiesByResource = groupCapabilitiesByResource(graph.capabilities);
28
+ const routes = [
29
+ projectWorkspaceRoute(graph),
30
+ ...graph.resources.flatMap((resource) => projectResourceRoutes(resource, graph.views, capabilitiesByResource, policiesByKey, tasksByKey)),
31
+ ...graph.resources.flatMap((resource) => projectRelationRoutes(resource, resourcesByKey, graph.views, capabilitiesByResource, policiesByKey, tasksByKey)),
32
+ ...graph.views
33
+ .filter((view) => !view.resource && (view.kind === "workspace" || view.kind === "dashboard"))
34
+ .map((view) => projectStandaloneViewRoute(view, graph.capabilities, policiesByKey, tasksByKey))
35
+ ];
36
+ const uniqueRoutes = dedupeRoutes(routes).sort((left, right) => left.path.localeCompare(right.path));
37
+ return {
38
+ domain: graph.domain,
39
+ summary: {
40
+ resourceCount: graph.resources.length,
41
+ capabilityCount: graph.capabilities.length,
42
+ routeCount: uniqueRoutes.length
43
+ },
44
+ attention: projectGlobalAttention(uniqueRoutes, tasksByKey, resourcesByKey),
45
+ navigation: uniqueRoutes.filter((route) => route.navigable).map((route) => ({
46
+ key: route.key,
47
+ label: route.navigationLabel,
48
+ path: route.path,
49
+ routeKey: route.key
50
+ })),
51
+ routes: uniqueRoutes
52
+ };
53
+ }
54
+ export function renderHumanSurfaceDocument(projection, options = {}) {
55
+ const runtimeModulePath = options.runtimeModulePath ?? "./dist/human-surface/index.js";
56
+ const navigation = projection.navigation
57
+ .map((item, index) => {
58
+ const activeClass = index === 0 ? " is-active" : "";
59
+ return `<a class="capstan-nav-link${activeClass}" href="#${escapeHtml(item.routeKey)}" data-route-nav="${escapeHtml(item.routeKey)}"><span>${escapeHtml(item.label)}</span><span class="capstan-nav-path">${escapeHtml(item.path)}</span></a>`;
60
+ })
61
+ .join("");
62
+ const routes = projection.routes
63
+ .map((route, index) => renderRouteSection(route, index === 0))
64
+ .join("");
65
+ const runtimeConsole = renderRuntimeConsole(projection);
66
+ return `<!doctype html>
67
+ <html lang="en">
68
+ <head>
69
+ <meta charset="utf-8" />
70
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
71
+ <title>${escapeHtml(projection.domain.title)} · Capstan Human Surface</title>
72
+ <style>
73
+ :root {
74
+ color-scheme: light;
75
+ --capstan-bg: #f5f3ef;
76
+ --capstan-panel: rgba(255, 255, 255, 0.9);
77
+ --capstan-border: rgba(27, 31, 35, 0.12);
78
+ --capstan-text: #111827;
79
+ --capstan-muted: #5b6470;
80
+ --capstan-accent: #1356d7;
81
+ --capstan-accent-soft: rgba(19, 86, 215, 0.08);
82
+ --capstan-success: #0f8a5f;
83
+ --capstan-warning: #9a6700;
84
+ --capstan-danger: #a53d2d;
85
+ }
86
+
87
+ * {
88
+ box-sizing: border-box;
89
+ }
90
+
91
+ body {
92
+ margin: 0;
93
+ font-family: "IBM Plex Sans", "Segoe UI", sans-serif;
94
+ background:
95
+ radial-gradient(circle at top left, rgba(19, 86, 215, 0.08), transparent 28%),
96
+ linear-gradient(180deg, #faf7f2 0%, var(--capstan-bg) 100%);
97
+ color: var(--capstan-text);
98
+ }
99
+
100
+ .capstan-shell {
101
+ display: grid;
102
+ grid-template-columns: 280px minmax(0, 1fr);
103
+ min-height: 100vh;
104
+ }
105
+
106
+ .capstan-sidebar {
107
+ padding: 28px 22px;
108
+ border-right: 1px solid var(--capstan-border);
109
+ background: rgba(255, 255, 255, 0.72);
110
+ backdrop-filter: blur(18px);
111
+ position: sticky;
112
+ top: 0;
113
+ align-self: start;
114
+ min-height: 100vh;
115
+ }
116
+
117
+ .capstan-brand {
118
+ margin-bottom: 24px;
119
+ }
120
+
121
+ .capstan-eyebrow {
122
+ display: inline-flex;
123
+ align-items: center;
124
+ gap: 8px;
125
+ border-radius: 999px;
126
+ background: var(--capstan-accent-soft);
127
+ color: var(--capstan-accent);
128
+ padding: 6px 10px;
129
+ font-size: 12px;
130
+ font-weight: 600;
131
+ letter-spacing: 0.04em;
132
+ text-transform: uppercase;
133
+ }
134
+
135
+ .capstan-brand h1 {
136
+ margin: 14px 0 10px;
137
+ font-size: 24px;
138
+ line-height: 1.1;
139
+ }
140
+
141
+ .capstan-brand p {
142
+ margin: 0;
143
+ color: var(--capstan-muted);
144
+ line-height: 1.6;
145
+ }
146
+
147
+ .capstan-nav {
148
+ display: flex;
149
+ flex-direction: column;
150
+ gap: 10px;
151
+ }
152
+
153
+ .capstan-nav-link {
154
+ display: flex;
155
+ flex-direction: column;
156
+ gap: 4px;
157
+ padding: 12px 14px;
158
+ border-radius: 16px;
159
+ border: 1px solid transparent;
160
+ color: inherit;
161
+ text-decoration: none;
162
+ background: rgba(255, 255, 255, 0.6);
163
+ }
164
+
165
+ .capstan-nav-link:hover {
166
+ border-color: var(--capstan-border);
167
+ background: white;
168
+ }
169
+
170
+ .capstan-nav-link.is-active {
171
+ border-color: rgba(19, 86, 215, 0.18);
172
+ background: rgba(19, 86, 215, 0.08);
173
+ }
174
+
175
+ .capstan-nav-path {
176
+ font-family: "IBM Plex Mono", "SFMono-Regular", monospace;
177
+ font-size: 11px;
178
+ color: var(--capstan-muted);
179
+ }
180
+
181
+ .capstan-main {
182
+ padding: 28px 28px 40px;
183
+ }
184
+
185
+ .capstan-summary {
186
+ display: grid;
187
+ grid-template-columns: repeat(3, minmax(0, 1fr));
188
+ gap: 14px;
189
+ margin-bottom: 20px;
190
+ }
191
+
192
+ .capstan-summary-card,
193
+ .capstan-route,
194
+ .capstan-card {
195
+ border: 1px solid var(--capstan-border);
196
+ border-radius: 22px;
197
+ background: var(--capstan-panel);
198
+ box-shadow: 0 18px 50px rgba(15, 23, 42, 0.06);
199
+ }
200
+
201
+ .capstan-summary-card {
202
+ padding: 18px 20px;
203
+ }
204
+
205
+ .capstan-summary-card span {
206
+ display: block;
207
+ color: var(--capstan-muted);
208
+ font-size: 13px;
209
+ margin-bottom: 10px;
210
+ }
211
+
212
+ .capstan-summary-card strong {
213
+ font-size: 28px;
214
+ }
215
+
216
+ .capstan-route {
217
+ padding: 24px;
218
+ margin-bottom: 18px;
219
+ }
220
+
221
+ .capstan-route[hidden] {
222
+ display: none;
223
+ }
224
+
225
+ .capstan-route[data-runtime-state="loading"] .capstan-grid,
226
+ .capstan-route[data-runtime-state="empty"] .capstan-grid,
227
+ .capstan-route[data-runtime-state="error"] .capstan-grid {
228
+ opacity: 0.52;
229
+ }
230
+
231
+ .capstan-route header {
232
+ display: flex;
233
+ justify-content: space-between;
234
+ gap: 18px;
235
+ align-items: flex-start;
236
+ margin-bottom: 20px;
237
+ }
238
+
239
+ .capstan-route h2 {
240
+ margin: 0 0 8px;
241
+ font-size: 24px;
242
+ }
243
+
244
+ .capstan-route p {
245
+ margin: 0;
246
+ color: var(--capstan-muted);
247
+ line-height: 1.6;
248
+ }
249
+
250
+ .capstan-badges {
251
+ display: flex;
252
+ gap: 8px;
253
+ flex-wrap: wrap;
254
+ }
255
+
256
+ .capstan-badge {
257
+ border-radius: 999px;
258
+ padding: 7px 10px;
259
+ font-size: 12px;
260
+ font-weight: 600;
261
+ border: 1px solid var(--capstan-border);
262
+ background: white;
263
+ }
264
+
265
+ .capstan-badge[data-tone="approval_required"] {
266
+ color: var(--capstan-warning);
267
+ background: rgba(154, 103, 0, 0.08);
268
+ }
269
+
270
+ .capstan-badge[data-tone="blocked"] {
271
+ color: var(--capstan-danger);
272
+ background: rgba(165, 61, 45, 0.08);
273
+ }
274
+
275
+ .capstan-badge[data-tone="redacted"] {
276
+ color: var(--capstan-accent);
277
+ background: var(--capstan-accent-soft);
278
+ }
279
+
280
+ .capstan-badge[data-tone="allowed"] {
281
+ color: var(--capstan-success);
282
+ background: rgba(15, 138, 95, 0.08);
283
+ }
284
+
285
+ .capstan-grid {
286
+ display: grid;
287
+ grid-template-columns: 1.5fr 1fr;
288
+ gap: 16px;
289
+ }
290
+
291
+ .capstan-card {
292
+ padding: 18px;
293
+ }
294
+
295
+ .capstan-card h3 {
296
+ margin: 0 0 14px;
297
+ font-size: 16px;
298
+ }
299
+
300
+ .capstan-table {
301
+ width: 100%;
302
+ border-collapse: collapse;
303
+ }
304
+
305
+ .capstan-table th,
306
+ .capstan-table td {
307
+ text-align: left;
308
+ padding: 10px 0;
309
+ border-bottom: 1px solid var(--capstan-border);
310
+ font-size: 14px;
311
+ }
312
+
313
+ .capstan-table th {
314
+ color: var(--capstan-muted);
315
+ font-size: 12px;
316
+ letter-spacing: 0.03em;
317
+ text-transform: uppercase;
318
+ }
319
+
320
+ .capstan-form-grid,
321
+ .capstan-fields {
322
+ display: grid;
323
+ gap: 12px;
324
+ }
325
+
326
+ .capstan-related-grid {
327
+ display: grid;
328
+ gap: 12px;
329
+ }
330
+
331
+ .capstan-attention-grid {
332
+ margin-top: 14px;
333
+ }
334
+
335
+ .capstan-attention-count {
336
+ display: inline-flex;
337
+ align-items: center;
338
+ margin-top: 10px;
339
+ border-radius: 999px;
340
+ padding: 6px 10px;
341
+ font-size: 12px;
342
+ font-weight: 600;
343
+ background: rgba(17, 24, 39, 0.05);
344
+ color: var(--capstan-text);
345
+ }
346
+
347
+ .capstan-attention-count[data-open-count="0"] {
348
+ color: var(--capstan-muted);
349
+ background: rgba(17, 24, 39, 0.04);
350
+ }
351
+
352
+ .capstan-field {
353
+ border: 1px solid var(--capstan-border);
354
+ border-radius: 16px;
355
+ padding: 14px;
356
+ background: rgba(255, 255, 255, 0.78);
357
+ }
358
+
359
+ .capstan-field strong {
360
+ display: block;
361
+ margin-bottom: 6px;
362
+ }
363
+
364
+ .capstan-related-link {
365
+ display: inline-flex;
366
+ align-items: center;
367
+ gap: 8px;
368
+ margin-top: 10px;
369
+ color: var(--capstan-accent);
370
+ text-decoration: none;
371
+ font-size: 13px;
372
+ font-weight: 600;
373
+ }
374
+
375
+ .capstan-related-link:hover {
376
+ text-decoration: underline;
377
+ }
378
+
379
+ .capstan-field span,
380
+ .capstan-action-note,
381
+ .capstan-state-copy {
382
+ color: var(--capstan-muted);
383
+ font-size: 13px;
384
+ line-height: 1.5;
385
+ }
386
+
387
+ .capstan-actions {
388
+ display: flex;
389
+ flex-wrap: wrap;
390
+ gap: 12px;
391
+ }
392
+
393
+ .capstan-action {
394
+ padding: 12px 14px;
395
+ border-radius: 16px;
396
+ border: 1px solid var(--capstan-border);
397
+ background: white;
398
+ }
399
+
400
+ .capstan-action strong {
401
+ display: block;
402
+ margin-bottom: 4px;
403
+ }
404
+
405
+ .capstan-action-button,
406
+ .capstan-state-toggle {
407
+ margin-top: 12px;
408
+ display: inline-flex;
409
+ align-items: center;
410
+ justify-content: center;
411
+ gap: 8px;
412
+ border: 1px solid transparent;
413
+ border-radius: 999px;
414
+ background: var(--capstan-text);
415
+ color: white;
416
+ padding: 9px 12px;
417
+ font-size: 12px;
418
+ font-weight: 600;
419
+ cursor: pointer;
420
+ }
421
+
422
+ .capstan-action-button:hover,
423
+ .capstan-state-toggle:hover {
424
+ background: #1f2937;
425
+ }
426
+
427
+ .capstan-action-button:disabled,
428
+ .capstan-state-toggle:disabled {
429
+ cursor: not-allowed;
430
+ opacity: 0.56;
431
+ }
432
+
433
+ .capstan-state-toggle {
434
+ background: white;
435
+ color: var(--capstan-text);
436
+ border-color: var(--capstan-border);
437
+ }
438
+
439
+ .capstan-state-toggle.is-active {
440
+ background: rgba(19, 86, 215, 0.08);
441
+ color: var(--capstan-accent);
442
+ border-color: rgba(19, 86, 215, 0.2);
443
+ }
444
+
445
+ .capstan-runtime-header {
446
+ display: flex;
447
+ justify-content: space-between;
448
+ gap: 12px;
449
+ align-items: center;
450
+ margin-bottom: 14px;
451
+ }
452
+
453
+ .capstan-runtime-pill {
454
+ display: inline-flex;
455
+ align-items: center;
456
+ padding: 7px 10px;
457
+ border-radius: 999px;
458
+ font-size: 12px;
459
+ font-weight: 600;
460
+ background: rgba(19, 86, 215, 0.08);
461
+ color: var(--capstan-accent);
462
+ }
463
+
464
+ .capstan-runtime-pill[data-route-result-state="idle"] {
465
+ background: rgba(17, 24, 39, 0.04);
466
+ color: var(--capstan-muted);
467
+ }
468
+
469
+ .capstan-runtime-pill[data-route-attention-state="idle"],
470
+ .capstan-runtime-pill[data-route-attention-state="cancelled"] {
471
+ background: rgba(17, 24, 39, 0.04);
472
+ color: var(--capstan-muted);
473
+ }
474
+
475
+ .capstan-runtime-pill[data-route-result-state="completed"] {
476
+ background: rgba(15, 138, 95, 0.08);
477
+ color: var(--capstan-success);
478
+ }
479
+
480
+ .capstan-runtime-pill[data-route-result-state="redacted"] {
481
+ background: var(--capstan-accent-soft);
482
+ color: var(--capstan-accent);
483
+ }
484
+
485
+ .capstan-runtime-pill[data-route-result-state="approval_required"],
486
+ .capstan-runtime-pill[data-route-result-state="not_implemented"] {
487
+ background: rgba(154, 103, 0, 0.08);
488
+ color: var(--capstan-warning);
489
+ }
490
+
491
+ .capstan-runtime-pill[data-route-attention-state="approval_required"],
492
+ .capstan-runtime-pill[data-route-attention-state="input_required"],
493
+ .capstan-runtime-pill[data-route-attention-state="paused"] {
494
+ background: rgba(154, 103, 0, 0.08);
495
+ color: var(--capstan-warning);
496
+ }
497
+
498
+ .capstan-runtime-pill[data-route-result-state="blocked"],
499
+ .capstan-runtime-pill[data-route-result-state="error"] {
500
+ background: rgba(165, 61, 45, 0.08);
501
+ color: var(--capstan-danger);
502
+ }
503
+
504
+ .capstan-runtime-pill[data-route-attention-state="blocked"],
505
+ .capstan-runtime-pill[data-route-attention-state="failed"] {
506
+ background: rgba(165, 61, 45, 0.08);
507
+ color: var(--capstan-danger);
508
+ }
509
+
510
+ .capstan-runtime-toggles {
511
+ display: flex;
512
+ flex-wrap: wrap;
513
+ gap: 8px;
514
+ margin-bottom: 12px;
515
+ }
516
+
517
+ .capstan-states {
518
+ display: grid;
519
+ grid-template-columns: repeat(4, minmax(0, 1fr));
520
+ gap: 12px;
521
+ }
522
+
523
+ .capstan-state {
524
+ border-radius: 18px;
525
+ padding: 16px;
526
+ background: rgba(17, 24, 39, 0.02);
527
+ border: 1px dashed var(--capstan-border);
528
+ }
529
+
530
+ .capstan-state strong {
531
+ display: block;
532
+ margin-bottom: 8px;
533
+ }
534
+
535
+ .capstan-state.is-active {
536
+ border-style: solid;
537
+ border-color: rgba(19, 86, 215, 0.25);
538
+ background: rgba(19, 86, 215, 0.06);
539
+ }
540
+
541
+ .capstan-state-bars {
542
+ display: flex;
543
+ flex-direction: column;
544
+ gap: 6px;
545
+ margin-top: 12px;
546
+ }
547
+
548
+ .capstan-state-bar {
549
+ height: 8px;
550
+ border-radius: 999px;
551
+ background: linear-gradient(90deg, rgba(19, 86, 215, 0.18), rgba(19, 86, 215, 0.05));
552
+ }
553
+
554
+ .capstan-input,
555
+ .capstan-textarea {
556
+ width: 100%;
557
+ margin-top: 10px;
558
+ border: 1px solid var(--capstan-border);
559
+ border-radius: 14px;
560
+ background: white;
561
+ color: var(--capstan-text);
562
+ padding: 10px 12px;
563
+ font: inherit;
564
+ }
565
+
566
+ .capstan-textarea {
567
+ min-height: 110px;
568
+ resize: vertical;
569
+ }
570
+
571
+ .capstan-console {
572
+ border: 1px solid var(--capstan-border);
573
+ border-radius: 22px;
574
+ background: rgba(17, 24, 39, 0.96);
575
+ color: #f8fafc;
576
+ padding: 20px 22px;
577
+ margin-bottom: 18px;
578
+ box-shadow: 0 18px 50px rgba(15, 23, 42, 0.16);
579
+ }
580
+
581
+ .capstan-console header {
582
+ display: flex;
583
+ justify-content: space-between;
584
+ gap: 14px;
585
+ align-items: flex-start;
586
+ margin-bottom: 14px;
587
+ }
588
+
589
+ .capstan-console h2 {
590
+ margin: 0 0 8px;
591
+ font-size: 18px;
592
+ }
593
+
594
+ .capstan-console p {
595
+ margin: 0;
596
+ color: rgba(248, 250, 252, 0.72);
597
+ line-height: 1.5;
598
+ }
599
+
600
+ .capstan-console-grid {
601
+ display: grid;
602
+ grid-template-columns: repeat(3, minmax(0, 1fr));
603
+ gap: 12px;
604
+ margin-bottom: 14px;
605
+ }
606
+
607
+ .capstan-console-card {
608
+ border: 1px solid rgba(255, 255, 255, 0.12);
609
+ border-radius: 18px;
610
+ padding: 14px;
611
+ background: rgba(255, 255, 255, 0.04);
612
+ }
613
+
614
+ .capstan-console-card span {
615
+ display: block;
616
+ font-size: 12px;
617
+ color: rgba(248, 250, 252, 0.68);
618
+ margin-bottom: 6px;
619
+ }
620
+
621
+ .capstan-console-card strong {
622
+ display: block;
623
+ }
624
+
625
+ .capstan-console-actions {
626
+ display: grid;
627
+ grid-template-columns: repeat(3, minmax(0, 1fr));
628
+ gap: 12px;
629
+ margin: 14px 0;
630
+ }
631
+
632
+ .capstan-console-scope-group + .capstan-console-scope-group {
633
+ margin-top: 18px;
634
+ }
635
+
636
+ .capstan-console-scope-grid {
637
+ display: grid;
638
+ grid-template-columns: repeat(2, minmax(0, 1fr));
639
+ gap: 12px;
640
+ margin-top: 12px;
641
+ }
642
+
643
+ .capstan-console-copy {
644
+ margin-top: 10px;
645
+ color: rgba(248, 250, 252, 0.72);
646
+ font-size: 13px;
647
+ line-height: 1.5;
648
+ }
649
+
650
+ .capstan-console-lane-grid {
651
+ display: grid;
652
+ grid-template-columns: repeat(2, minmax(0, 1fr));
653
+ gap: 8px;
654
+ margin-top: 12px;
655
+ }
656
+
657
+ .capstan-console-card .capstan-action-button {
658
+ width: 100%;
659
+ margin-top: 12px;
660
+ }
661
+
662
+ .capstan-console-card .capstan-attention-count {
663
+ margin-top: 10px;
664
+ }
665
+
666
+ .capstan-attention-breadcrumbs {
667
+ margin: 10px 0 12px;
668
+ }
669
+
670
+ .capstan-attention-handoff-controls {
671
+ display: flex;
672
+ flex-wrap: wrap;
673
+ gap: 8px;
674
+ margin: -4px 0 12px;
675
+ }
676
+
677
+ .capstan-attention-handoff-controls .capstan-state-toggle {
678
+ margin-top: 0;
679
+ }
680
+
681
+ .capstan-console-lane-grid .capstan-state-toggle {
682
+ width: 100%;
683
+ margin-top: 0;
684
+ }
685
+
686
+ .capstan-console-workspace-lanes {
687
+ margin-top: 12px;
688
+ }
689
+
690
+ .capstan-console pre {
691
+ margin: 0;
692
+ overflow: auto;
693
+ border-radius: 18px;
694
+ background: rgba(255, 255, 255, 0.04);
695
+ padding: 16px;
696
+ font-family: "IBM Plex Mono", "SFMono-Regular", monospace;
697
+ font-size: 12px;
698
+ line-height: 1.6;
699
+ }
700
+
701
+ .capstan-route-result {
702
+ margin: 0;
703
+ overflow: auto;
704
+ border-radius: 18px;
705
+ background: rgba(17, 24, 39, 0.04);
706
+ border: 1px solid var(--capstan-border);
707
+ padding: 16px;
708
+ font-family: "IBM Plex Mono", "SFMono-Regular", monospace;
709
+ font-size: 12px;
710
+ line-height: 1.6;
711
+ }
712
+
713
+ @media (max-width: 1024px) {
714
+ .capstan-shell {
715
+ grid-template-columns: 1fr;
716
+ }
717
+
718
+ .capstan-sidebar {
719
+ position: static;
720
+ min-height: auto;
721
+ border-right: none;
722
+ border-bottom: 1px solid var(--capstan-border);
723
+ }
724
+
725
+ .capstan-grid,
726
+ .capstan-summary,
727
+ .capstan-console-grid,
728
+ .capstan-console-actions,
729
+ .capstan-console-scope-grid,
730
+ .capstan-console-lane-grid,
731
+ .capstan-states {
732
+ grid-template-columns: 1fr;
733
+ }
734
+ }
735
+ </style>
736
+ </head>
737
+ <body>
738
+ <div class="capstan-shell">
739
+ <aside class="capstan-sidebar">
740
+ <div class="capstan-brand">
741
+ <span class="capstan-eyebrow">Capstan Human Surface</span>
742
+ <h1>${escapeHtml(projection.domain.title)}</h1>
743
+ <p>${escapeHtml(projection.domain.description ?? "Generated from the App Graph with a deterministic human-facing shell.")}</p>
744
+ </div>
745
+ <nav class="capstan-nav">
746
+ ${navigation}
747
+ </nav>
748
+ </aside>
749
+ <main class="capstan-main">
750
+ <section class="capstan-summary">
751
+ <article class="capstan-summary-card"><span>Resources</span><strong>${projection.summary.resourceCount}</strong></article>
752
+ <article class="capstan-summary-card"><span>Capabilities</span><strong>${projection.summary.capabilityCount}</strong></article>
753
+ <article class="capstan-summary-card"><span>Projected Routes</span><strong>${projection.summary.routeCount}</strong></article>
754
+ </section>
755
+ ${runtimeConsole}
756
+ ${routes}
757
+ </main>
758
+ </div>
759
+ <script type="module">
760
+ import { mountHumanSurfaceBrowser } from "${escapeHtml(runtimeModulePath)}";
761
+
762
+ mountHumanSurfaceBrowser(document);
763
+ </script>
764
+ </body>
765
+ </html>
766
+ `;
767
+ }
768
+ function projectWorkspaceRoute(graph) {
769
+ return {
770
+ key: "workspaceHome",
771
+ path: "/",
772
+ title: `${graph.domain.title} Workspace`,
773
+ kind: "workspace",
774
+ navigationLabel: "Workspace",
775
+ navigable: true,
776
+ description: graph.domain.description ??
777
+ "A generated workspace view that summarizes the human-facing routes derived from the App Graph.",
778
+ generated: true,
779
+ actions: graph.capabilities.slice(0, 3).map((capability) => projectAction(capability)),
780
+ states: createStates(`${graph.domain.title} workspace`),
781
+ fields: [],
782
+ relations: [],
783
+ attentionQueues: []
784
+ };
785
+ }
786
+ function projectResourceRoutes(resource, views, capabilitiesByResource, policiesByKey, tasksByKey) {
787
+ const resourceViews = views.filter((view) => view.resource === resource.key);
788
+ const resourceCapabilities = capabilitiesByResource.get(resource.key) ?? [];
789
+ const actions = (capabilitiesByResource.get(resource.key) ?? []).map((capability) => projectAction(capability, policiesByKey.get(capability.policy ?? ""), capability.task ? tasksByKey.get(capability.task) : undefined));
790
+ const listView = resourceViews.find((view) => view.kind === "list");
791
+ const detailView = resourceViews.find((view) => view.kind === "detail");
792
+ const formView = resourceViews.find((view) => view.kind === "form");
793
+ return [
794
+ createResourceRoute(resource, listView, "list", actions, resourceCapabilities),
795
+ createResourceRoute(resource, detailView, "detail", actions, resourceCapabilities),
796
+ createResourceRoute(resource, formView, "form", actions, resourceCapabilities)
797
+ ];
798
+ }
799
+ function projectStandaloneViewRoute(view, capabilities, policiesByKey, tasksByKey) {
800
+ const matchedActions = capabilities
801
+ .filter((capability) => capability.key === view.capability)
802
+ .map((capability) => projectAction(capability, policiesByKey.get(capability.policy ?? ""), capability.task ? tasksByKey.get(capability.task) : undefined));
803
+ return {
804
+ key: view.key,
805
+ path: `/${toKebabCase(view.key)}`,
806
+ title: view.title,
807
+ kind: view.kind,
808
+ navigationLabel: view.title,
809
+ navigable: true,
810
+ description: view.description ??
811
+ "A projected standalone surface derived from the App Graph view definition.",
812
+ ...optionalProperty("resourceKey", view.resource),
813
+ ...optionalProperty("capabilityKey", view.capability),
814
+ generated: false,
815
+ actions: matchedActions,
816
+ states: createStates(view.title),
817
+ fields: [],
818
+ relations: [],
819
+ attentionQueues: projectRouteAttentionQueues(view.key, matchedActions)
820
+ };
821
+ }
822
+ function createResourceRoute(resource, explicitView, kind, actions, capabilities) {
823
+ const routeKey = explicitView?.key ?? `${resource.key}${startCase(kind).replace(/\s+/g, "")}`;
824
+ const matchedCapability = selectRouteCapability(kind, explicitView, capabilities);
825
+ const projectedFields = projectRouteFields(kind, resource, matchedCapability);
826
+ const projectedRelations = projectRouteRelations(resource);
827
+ const projectedAttentionQueues = projectRouteAttentionQueues(routeKey, actions);
828
+ const title = explicitView?.title ?? `${resource.title} ${startCase(kind)}`;
829
+ const description = explicitView?.description ??
830
+ `A generated ${kind} route derived from the "${resource.key}" resource schema.`;
831
+ const table = kind === "list"
832
+ ? {
833
+ columns: projectedFields.slice(0, 4),
834
+ sampleRow: Object.fromEntries(projectedFields.slice(0, 4).map((field) => [field.key, sampleValueForField(field)]))
835
+ }
836
+ : undefined;
837
+ return {
838
+ key: routeKey,
839
+ path: `/resources/${toKebabCase(resource.key)}/${kind}`,
840
+ title,
841
+ kind,
842
+ navigationLabel: title,
843
+ navigable: true,
844
+ description,
845
+ resourceKey: resource.key,
846
+ ...optionalProperty("capabilityKey", explicitView?.capability ?? matchedCapability?.key),
847
+ generated: !explicitView,
848
+ actions,
849
+ states: createStates(title),
850
+ fields: projectedFields,
851
+ relations: projectedRelations,
852
+ attentionQueues: projectedAttentionQueues,
853
+ ...optionalProperty("table", table)
854
+ };
855
+ }
856
+ function projectRelationRoutes(resource, resourcesByKey, views, capabilitiesByResource, policiesByKey, tasksByKey) {
857
+ return Object.entries(resource.relations ?? {}).flatMap(([relationKey, relation]) => {
858
+ const targetResource = resourcesByKey.get(relation.resource);
859
+ if (!targetResource) {
860
+ return [];
861
+ }
862
+ const targetViews = views.filter((view) => view.resource === targetResource.key);
863
+ const targetCapabilities = capabilitiesByResource.get(targetResource.key) ?? [];
864
+ return [
865
+ createRelationRoute(resource, relationKey, relation, targetResource, targetViews, targetCapabilities, policiesByKey, tasksByKey)
866
+ ];
867
+ });
868
+ }
869
+ function createRelationRoute(sourceResource, relationKey, relation, targetResource, targetViews, targetCapabilities, policiesByKey, tasksByKey) {
870
+ const routeReference = createRelationRouteReference(sourceResource, relationKey, relation);
871
+ const kind = relation.kind === "many" ? "list" : "detail";
872
+ const explicitView = targetViews.find((view) => view.kind === kind);
873
+ const matchedCapability = selectRouteCapability(kind, explicitView, targetCapabilities);
874
+ const actions = targetCapabilities.map((capability) => projectAction(capability, policiesByKey.get(capability.policy ?? ""), capability.task ? tasksByKey.get(capability.task) : undefined));
875
+ const projectedFields = projectRouteFields(kind, targetResource, matchedCapability);
876
+ const projectedRelations = projectRouteRelations(targetResource);
877
+ const projectedAttentionQueues = projectRouteAttentionQueues(routeReference.key, actions);
878
+ const table = kind === "list"
879
+ ? {
880
+ columns: projectedFields.slice(0, 4),
881
+ sampleRow: Object.fromEntries(projectedFields.slice(0, 4).map((field) => [field.key, sampleValueForField(field)]))
882
+ }
883
+ : undefined;
884
+ return {
885
+ key: routeReference.key,
886
+ path: routeReference.path,
887
+ title: routeReference.title,
888
+ kind,
889
+ navigationLabel: routeReference.title,
890
+ navigable: false,
891
+ description: relation.description
892
+ ? `${relation.description} This generated route is scoped from the "${sourceResource.key}.${relationKey}" relation.`
893
+ : `A generated ${kind} route scoped from the "${sourceResource.key}.${relationKey}" relation onto the "${targetResource.key}" resource.`,
894
+ resourceKey: targetResource.key,
895
+ ...optionalProperty("capabilityKey", explicitView?.capability ?? matchedCapability?.key),
896
+ sourceResourceKey: sourceResource.key,
897
+ sourceRelationKey: relationKey,
898
+ generated: true,
899
+ actions,
900
+ states: createStates(routeReference.title),
901
+ fields: projectedFields,
902
+ relations: projectedRelations,
903
+ attentionQueues: projectedAttentionQueues,
904
+ ...optionalProperty("table", table)
905
+ };
906
+ }
907
+ function projectRouteFields(kind, resource, capability) {
908
+ const fieldSource = kind === "form"
909
+ ? capability?.input ?? resource.fields
910
+ : capability?.output ?? resource.fields;
911
+ return Object.entries(fieldSource).map(([key, field]) => createHumanSurfaceField(key, field));
912
+ }
913
+ function projectRouteRelations(resource) {
914
+ return Object.entries(resource.relations ?? {}).map(([relationKey, relation]) => {
915
+ const routeReference = createRelationRouteReference(resource, relationKey, relation);
916
+ return {
917
+ key: relationKey,
918
+ label: startCase(relationKey),
919
+ resourceKey: relation.resource,
920
+ kind: relation.kind,
921
+ routeKey: routeReference.key,
922
+ routeTitle: routeReference.title,
923
+ path: routeReference.path,
924
+ ...optionalProperty("description", relation.description)
925
+ };
926
+ });
927
+ }
928
+ function groupCapabilitiesByResource(capabilities) {
929
+ const grouped = new Map();
930
+ for (const capability of capabilities) {
931
+ for (const resourceKey of capability.resources ?? []) {
932
+ const current = grouped.get(resourceKey) ?? [];
933
+ current.push(capability);
934
+ grouped.set(resourceKey, current);
935
+ }
936
+ }
937
+ return grouped;
938
+ }
939
+ function projectAction(capability, policy, task) {
940
+ const policyState = resolvePolicyState(policy);
941
+ return {
942
+ key: capability.key,
943
+ capability: capability.key,
944
+ title: capability.title,
945
+ mode: capability.mode,
946
+ resources: capability.resources ?? [],
947
+ ...optionalProperty("task", capability.task),
948
+ ...optionalProperty("taskKind", task?.kind),
949
+ ...optionalProperty("taskTitle", task?.title),
950
+ label: actionLabelForMode(capability.mode),
951
+ policyState,
952
+ policyLabel: policyLabelForState(policyState),
953
+ note: actionNote(capability, policyState)
954
+ };
955
+ }
956
+ function projectRouteAttentionQueues(routeKey, actions) {
957
+ return actions.flatMap((action) => {
958
+ const taskKey = action.task;
959
+ if (!taskKey || action.taskKind !== "durable") {
960
+ return [];
961
+ }
962
+ return attentionQueueStatusOrder.map((status) => ({
963
+ key: `${routeKey}:${action.key}:${status}`,
964
+ label: attentionQueueLabel(status),
965
+ status,
966
+ actionKey: action.key,
967
+ actionTitle: action.title,
968
+ taskKey,
969
+ taskTitle: action.taskTitle ?? startCase(taskKey),
970
+ filter: {
971
+ taskKey,
972
+ routeKey,
973
+ actionKey: action.key,
974
+ status
975
+ }
976
+ }));
977
+ });
978
+ }
979
+ function projectGlobalAttention(routes, tasksByKey, resourcesByKey) {
980
+ const durableRouteActions = routes.flatMap((route) => route.actions.flatMap((action) => action.task && action.taskKind === "durable"
981
+ ? [
982
+ {
983
+ route,
984
+ action
985
+ }
986
+ ]
987
+ : []));
988
+ if (!durableRouteActions.length) {
989
+ return {
990
+ queues: [],
991
+ presets: []
992
+ };
993
+ }
994
+ const taskPresets = new Map();
995
+ const resourcePresets = new Map();
996
+ const routePresets = new Map();
997
+ for (const { route, action } of durableRouteActions) {
998
+ const taskKey = action.task;
999
+ if (taskKey && !taskPresets.has(taskKey)) {
1000
+ taskPresets.set(taskKey, createAttentionPreset("task", taskKey, tasksByKey.get(taskKey)?.title ?? action.taskTitle ?? startCase(taskKey), `Durable runs started by task:${taskKey}.`, {
1001
+ taskKey
1002
+ }));
1003
+ }
1004
+ for (const resourceKey of new Set([route.resourceKey, route.sourceResourceKey].filter((value) => Boolean(value)))) {
1005
+ if (resourcePresets.has(resourceKey)) {
1006
+ continue;
1007
+ }
1008
+ resourcePresets.set(resourceKey, createAttentionPreset("resource", resourceKey, resourcesByKey.get(resourceKey)?.title ?? startCase(resourceKey), `Durable runs whose route or relation scope touches resource:${resourceKey}.`, {
1009
+ resourceKey
1010
+ }));
1011
+ }
1012
+ if (!routePresets.has(route.key)) {
1013
+ routePresets.set(route.key, createAttentionPreset("route", route.key, route.title, attentionPresetDescriptionForRoute(route), {
1014
+ routeKey: route.key
1015
+ }));
1016
+ }
1017
+ }
1018
+ return {
1019
+ inbox: {
1020
+ key: "workflowAttentionInbox",
1021
+ label: "Open Attention Inbox"
1022
+ },
1023
+ queues: attentionQueueStatusOrder.map((status) => ({
1024
+ key: `workflowAttentionQueue:${status}`,
1025
+ label: attentionQueueLabel(status),
1026
+ status
1027
+ })),
1028
+ presets: [...taskPresets.values(), ...resourcePresets.values(), ...routePresets.values()]
1029
+ };
1030
+ }
1031
+ function createAttentionPreset(scope, key, label, description, filter) {
1032
+ const presetKey = `${scope}:${key}`;
1033
+ return {
1034
+ key: presetKey,
1035
+ label,
1036
+ scope,
1037
+ autoSlotKey: attentionPresetAutoSlotKey(scope),
1038
+ description,
1039
+ filter,
1040
+ inbox: {
1041
+ key: `${presetKey}:inbox`,
1042
+ label: `Open ${label} Attention Inbox`,
1043
+ filter
1044
+ },
1045
+ queues: attentionQueueStatusOrder.map((status) => ({
1046
+ key: `${presetKey}:queue:${status}`,
1047
+ label: attentionQueueLabel(status),
1048
+ status,
1049
+ filter: {
1050
+ ...filter,
1051
+ status
1052
+ }
1053
+ }))
1054
+ };
1055
+ }
1056
+ function attentionPresetDescriptionForRoute(route) {
1057
+ if (route.sourceResourceKey && route.sourceRelationKey) {
1058
+ return `Durable runs scoped to relation route:${route.key} for ${route.sourceResourceKey}.${route.sourceRelationKey} at ${route.path}.`;
1059
+ }
1060
+ return `Durable runs scoped to route:${route.key} at ${route.path}.`;
1061
+ }
1062
+ function createHumanSurfaceField(key, field) {
1063
+ return {
1064
+ key,
1065
+ label: startCase(key),
1066
+ type: field.type,
1067
+ required: field.required ?? false,
1068
+ ...optionalProperty("description", field.description)
1069
+ };
1070
+ }
1071
+ function selectRouteCapability(kind, explicitView, capabilities) {
1072
+ if (explicitView?.capability) {
1073
+ return capabilities.find((capability) => capability.key === explicitView.capability);
1074
+ }
1075
+ switch (kind) {
1076
+ case "list":
1077
+ return capabilities.find((capability) => capability.mode === "read");
1078
+ case "form":
1079
+ return capabilities.find((capability) => capability.mode === "write");
1080
+ case "detail":
1081
+ return (capabilities.find((capability) => capability.mode === "external") ??
1082
+ capabilities.find((capability) => capability.mode === "read"));
1083
+ }
1084
+ }
1085
+ function renderRouteSection(route, isActive) {
1086
+ const routeBadges = [
1087
+ `<span class="capstan-badge">${escapeHtml(route.kind)}</span>`,
1088
+ route.generated
1089
+ ? `<span class="capstan-badge">generated</span>`
1090
+ : `<span class="capstan-badge">graph-defined</span>`,
1091
+ route.sourceResourceKey && route.sourceRelationKey
1092
+ ? `<span class="capstan-badge">relation:${escapeHtml(route.sourceResourceKey)}.${escapeHtml(route.sourceRelationKey)}</span>`
1093
+ : "",
1094
+ route.resourceKey
1095
+ ? `<span class="capstan-badge">resource:${escapeHtml(route.resourceKey)}</span>`
1096
+ : ""
1097
+ ]
1098
+ .filter(Boolean)
1099
+ .join("");
1100
+ const actionCards = route.actions.length
1101
+ ? route.actions
1102
+ .map((action) => `<article class="capstan-action">
1103
+ <strong>${escapeHtml(action.title)}</strong>
1104
+ <div class="capstan-badges">
1105
+ <span class="capstan-badge" data-tone="${escapeHtml(action.policyState)}">${escapeHtml(action.policyLabel)}</span>
1106
+ <span class="capstan-badge">${escapeHtml(action.label)}</span>
1107
+ ${action.task && action.taskKind === "durable"
1108
+ ? `<span class="capstan-badge">workflow:${escapeHtml(action.task)}</span>`
1109
+ : ""}
1110
+ </div>
1111
+ <p class="capstan-action-note">${escapeHtml(action.note)}</p>
1112
+ <button type="button" class="capstan-action-button" data-route-action="${escapeHtml(route.key)}" data-action-key="${escapeHtml(action.key)}">${escapeHtml(action.label)} · ${escapeHtml(action.title)}</button>
1113
+ </article>`)
1114
+ .join("")
1115
+ : `<article class="capstan-action"><strong>No actions yet</strong><p class="capstan-action-note">Add capabilities that reference this route's resource to surface executable actions here.</p></article>`;
1116
+ const content = route.kind === "list" && route.table
1117
+ ? renderTableProjection(route)
1118
+ : renderFieldProjection(route);
1119
+ const relatedRecords = renderRelatedRecords(route);
1120
+ const attentionQueues = renderAttentionQueues(route);
1121
+ const attentionQueueResult = renderAttentionQueueResult(route);
1122
+ return `<section class="capstan-route" id="${escapeHtml(route.key)}" data-route-key="${escapeHtml(route.key)}"${isActive ? "" : " hidden"}>
1123
+ <header>
1124
+ <div>
1125
+ <h2>${escapeHtml(route.title)}</h2>
1126
+ <p>${escapeHtml(route.description)}</p>
1127
+ </div>
1128
+ <div class="capstan-badges">
1129
+ ${routeBadges}
1130
+ <span class="capstan-badge">${escapeHtml(route.path)}</span>
1131
+ </div>
1132
+ </header>
1133
+ <div class="capstan-grid">
1134
+ ${content}
1135
+ <div class="capstan-card">
1136
+ <h3>Capability Actions</h3>
1137
+ <div class="capstan-actions">${actionCards}</div>
1138
+ </div>
1139
+ </div>
1140
+ ${relatedRecords}
1141
+ ${attentionQueues}
1142
+ ${attentionQueueResult}
1143
+ <div class="capstan-card" style="margin-top: 16px;">
1144
+ <div class="capstan-runtime-header">
1145
+ <h3>Route Runtime</h3>
1146
+ <span class="capstan-runtime-pill" data-route-mode-label="${escapeHtml(route.key)}">ready</span>
1147
+ </div>
1148
+ <div class="capstan-runtime-toggles">
1149
+ <button type="button" class="capstan-state-toggle is-active" data-route-mode-target="${escapeHtml(route.key)}" data-route-mode="ready">Ready</button>
1150
+ <button type="button" class="capstan-state-toggle" data-route-mode-target="${escapeHtml(route.key)}" data-route-mode="loading">Loading</button>
1151
+ <button type="button" class="capstan-state-toggle" data-route-mode-target="${escapeHtml(route.key)}" data-route-mode="empty">Empty</button>
1152
+ <button type="button" class="capstan-state-toggle" data-route-mode-target="${escapeHtml(route.key)}" data-route-mode="error">Error</button>
1153
+ </div>
1154
+ <p class="capstan-state-copy" data-route-state-copy="${escapeHtml(route.key)}">${escapeHtml(readyCopyForRoute(route))}</p>
1155
+ <div class="capstan-states">
1156
+ ${renderStateCard(route.key, "Ready", readyCopyForRoute(route), false, "ready", true)}
1157
+ ${renderStateCard(route.key, "Loading", route.states.loading, true, "loading", false)}
1158
+ ${renderStateCard(route.key, "Empty", route.states.empty, false, "empty", false)}
1159
+ ${renderStateCard(route.key, "Error", route.states.error, false, "error", false)}
1160
+ </div>
1161
+ </div>
1162
+ <div class="capstan-card" style="margin-top: 16px;">
1163
+ <div class="capstan-runtime-header">
1164
+ <h3>Execution Result</h3>
1165
+ <span class="capstan-runtime-pill" data-route-result-status="${escapeHtml(route.key)}" data-route-result-state="idle">idle</span>
1166
+ </div>
1167
+ <p class="capstan-state-copy">The last execution payload for this route is captured here so the projected surface, the operator, and future agent consumers stay aligned.</p>
1168
+ <pre class="capstan-route-result" data-route-result-output="${escapeHtml(route.key)}">{
1169
+ "event": "route.idle",
1170
+ "routeKey": ${JSON.stringify(route.key)},
1171
+ "message": "No capability has been executed for this route yet."
1172
+ }</pre>
1173
+ </div>
1174
+ </section>`;
1175
+ }
1176
+ function renderRelatedRecords(route) {
1177
+ if (!route.relations.length) {
1178
+ return "";
1179
+ }
1180
+ const items = route.relations
1181
+ .map((relation) => `<article class="capstan-field">
1182
+ <strong>${escapeHtml(relation.label)}</strong>
1183
+ <span>${escapeHtml(relation.kind)} · resource:${escapeHtml(relation.resourceKey)}${relation.description ? ` · ${escapeHtml(relation.description)}` : ""}</span>
1184
+ <a class="capstan-related-link" href="#${escapeHtml(relation.path)}" data-related-path="${escapeHtml(relation.path)}">Open ${escapeHtml(relation.routeTitle)}</a>
1185
+ </article>`)
1186
+ .join("");
1187
+ return `<div class="capstan-card" style="margin-top: 16px;">
1188
+ <h3>Related Records</h3>
1189
+ <div class="capstan-related-grid">${items}</div>
1190
+ </div>`;
1191
+ }
1192
+ function renderAttentionQueues(route) {
1193
+ if (!route.attentionQueues.length) {
1194
+ return "";
1195
+ }
1196
+ const items = route.attentionQueues
1197
+ .map((queue) => `<article class="capstan-field">
1198
+ <strong>${escapeHtml(queue.label)}</strong>
1199
+ <span>${escapeHtml(queue.actionTitle)} · task:${escapeHtml(queue.taskKey)} · route:${escapeHtml(route.key)}</span>
1200
+ <span class="capstan-attention-count" data-attention-open-count-route="${escapeHtml(route.key)}" data-attention-action-key="${escapeHtml(queue.actionKey)}" data-attention-status="${escapeHtml(queue.status)}">0 open</span>
1201
+ <button type="button" class="capstan-action-button" data-attention-queue="true" data-attention-route-key="${escapeHtml(route.key)}" data-attention-action-key="${escapeHtml(queue.actionKey)}" data-attention-task-key="${escapeHtml(queue.taskKey)}" data-attention-queue-status="${escapeHtml(queue.status)}">Open ${escapeHtml(queue.label)} Queue</button>
1202
+ </article>`)
1203
+ .join("");
1204
+ return `<div class="capstan-card" style="margin-top: 16px;">
1205
+ <h3>Attention Queues</h3>
1206
+ <p class="capstan-state-copy">Durable route actions automatically project grouped approval, input, block, failure, pause, and cancellation queues for operator supervision.</p>
1207
+ <div class="capstan-related-grid capstan-attention-grid">${items}</div>
1208
+ </div>`;
1209
+ }
1210
+ function renderAttentionQueueResult(route) {
1211
+ if (!route.attentionQueues.length) {
1212
+ return "";
1213
+ }
1214
+ return `<div class="capstan-card" style="margin-top: 16px;">
1215
+ <div class="capstan-runtime-header">
1216
+ <h3>Attention Queue Result</h3>
1217
+ <span class="capstan-runtime-pill" data-route-attention-status="${escapeHtml(route.key)}" data-route-attention-state="idle">idle</span>
1218
+ </div>
1219
+ <div class="capstan-badges capstan-attention-breadcrumbs" data-route-attention-handoff="${escapeHtml(route.key)}">
1220
+ <span class="capstan-badge">No Console Handoff</span>
1221
+ </div>
1222
+ <div class="capstan-attention-handoff-controls" data-route-attention-handoff-controls="${escapeHtml(route.key)}"></div>
1223
+ <p class="capstan-state-copy" data-route-attention-handoff-copy="${escapeHtml(route.key)}">Open a task-, resource-, or route-scoped attention preset from the operator console to carry breadcrumb context into this route-local queue lane.</p>
1224
+ <p class="capstan-state-copy">The last attention queue opened from this route is captured here so operators can inspect the exact filter, open count, and matching runs.</p>
1225
+ <pre class="capstan-route-result" data-route-attention-output="${escapeHtml(route.key)}">{
1226
+ "event": "route.attention.idle",
1227
+ "routeKey": ${JSON.stringify(route.key)},
1228
+ "message": "No attention queue lane has been opened for this route yet."
1229
+ }</pre>
1230
+ </div>`;
1231
+ }
1232
+ function renderTableProjection(route) {
1233
+ const rows = route.table?.columns
1234
+ .map((column) => `<th>${escapeHtml(column.label)}</th>`)
1235
+ .join("");
1236
+ const cells = route.table?.columns
1237
+ .map((column) => `<td>${escapeHtml(route.table?.sampleRow[column.key] ?? "")}</td>`)
1238
+ .join("");
1239
+ return `<div class="capstan-card">
1240
+ <h3>List Projection</h3>
1241
+ <table class="capstan-table">
1242
+ <thead><tr>${rows}</tr></thead>
1243
+ <tbody data-route-table-body="${escapeHtml(route.key)}"><tr>${cells}</tr></tbody>
1244
+ </table>
1245
+ </div>`;
1246
+ }
1247
+ function renderFieldProjection(route) {
1248
+ if (route.kind === "form") {
1249
+ const fields = route.fields.length
1250
+ ? route.fields
1251
+ .map((field) => `<label class="capstan-field">
1252
+ <strong>${escapeHtml(field.label)}</strong>
1253
+ <span>${escapeHtml(field.type)}${field.required ? " · required" : ""}${field.description ? ` · ${escapeHtml(field.description)}` : ""}</span>
1254
+ ${renderInputControl(route, field)}
1255
+ </label>`)
1256
+ .join("")
1257
+ : `<div class="capstan-field"><strong>No form fields projected</strong><span>Add resource fields or capability input schemas to make this route interactive.</span></div>`;
1258
+ return `<div class="capstan-card">
1259
+ <h3>Form Projection</h3>
1260
+ <div class="capstan-form-grid">${fields}</div>
1261
+ </div>`;
1262
+ }
1263
+ const fields = route.fields.length
1264
+ ? route.fields
1265
+ .map((field) => `<div class="capstan-field">
1266
+ <strong>${escapeHtml(field.label)}</strong>
1267
+ <span>${escapeHtml(field.type)}${field.required ? " · required" : ""}${field.description ? ` · ${escapeHtml(field.description)}` : ""}</span>
1268
+ ${route.kind === "detail"
1269
+ ? `<div class="capstan-input" style="margin-top: 10px;" data-route-detail-value-route="${escapeHtml(route.key)}" data-field-key="${escapeHtml(field.key)}">${escapeHtml(sampleValueForField(field))}</div>`
1270
+ : ""}
1271
+ </div>`)
1272
+ .join("")
1273
+ : `<div class="capstan-field"><strong>No fields projected</strong><span>This route is driven by higher-level graph semantics rather than a direct resource schema.</span></div>`;
1274
+ const title = route.kind === "detail" ? "Detail Projection" : "Field Projection";
1275
+ return `<div class="capstan-card">
1276
+ <h3>${title}</h3>
1277
+ <div class="capstan-fields">${fields}</div>
1278
+ </div>`;
1279
+ }
1280
+ function renderStateCard(routeKey, title, copy, includeBars, mode, active) {
1281
+ return `<article class="capstan-state${active ? " is-active" : ""}" data-state-card-route="${escapeHtml(routeKey)}" data-state-card-value="${escapeHtml(mode)}">
1282
+ <strong>${escapeHtml(title)}</strong>
1283
+ <p class="capstan-state-copy">${escapeHtml(copy)}</p>
1284
+ ${includeBars
1285
+ ? `<div class="capstan-state-bars"><div class="capstan-state-bar"></div><div class="capstan-state-bar" style="width: 74%;"></div><div class="capstan-state-bar" style="width: 56%;"></div></div>`
1286
+ : ""}
1287
+ </article>`;
1288
+ }
1289
+ function createStates(title) {
1290
+ return {
1291
+ loading: `Loading ${title.toLowerCase()} from the generated human surface runtime.`,
1292
+ empty: `No ${title.toLowerCase()} data is available yet. Connect a capability handler or seed data to populate this route.`,
1293
+ error: `This route is projected, but its backing runtime path has not been connected yet.`
1294
+ };
1295
+ }
1296
+ function resolvePolicyState(policy) {
1297
+ switch (policy?.effect) {
1298
+ case "approve":
1299
+ return "approval_required";
1300
+ case "deny":
1301
+ return "blocked";
1302
+ case "redact":
1303
+ return "redacted";
1304
+ default:
1305
+ return "allowed";
1306
+ }
1307
+ }
1308
+ function readyCopyForRoute(route) {
1309
+ return `Ready to operate ${route.title.toLowerCase()} from the generated human surface.`;
1310
+ }
1311
+ function renderInputControl(route, field) {
1312
+ const sample = escapeHtml(sampleValueForField(field));
1313
+ const routeKey = escapeHtml(route.key);
1314
+ const fieldKey = escapeHtml(field.key);
1315
+ if (field.type === "json") {
1316
+ return `<textarea class="capstan-textarea" data-route-input-key="${routeKey}" data-field-key="${fieldKey}">${sample}</textarea>`;
1317
+ }
1318
+ return `<input class="capstan-input" data-route-input-key="${routeKey}" data-field-key="${fieldKey}" type="${inputTypeForField(field)}" value="${sample}" />`;
1319
+ }
1320
+ function inputTypeForField(field) {
1321
+ switch (field.type) {
1322
+ case "integer":
1323
+ case "number":
1324
+ return "number";
1325
+ case "date":
1326
+ return "date";
1327
+ case "datetime":
1328
+ return "datetime-local";
1329
+ default:
1330
+ return "text";
1331
+ }
1332
+ }
1333
+ function renderRuntimeConsole(projection) {
1334
+ const firstRoute = projection.routes[0];
1335
+ const attentionConsole = renderGlobalAttentionConsole(projection.attention);
1336
+ return `<section class="capstan-console" aria-live="polite">
1337
+ <header>
1338
+ <div>
1339
+ <h2>Operator Console</h2>
1340
+ <p>Navigate between projected routes, preview runtime states, and trigger generated actions without leaving the human surface shell.</p>
1341
+ </div>
1342
+ <span class="capstan-runtime-pill" data-console-mode>ready</span>
1343
+ </header>
1344
+ <div class="capstan-console-grid">
1345
+ <article class="capstan-console-card">
1346
+ <span>Active Route</span>
1347
+ <strong data-console-route>${escapeHtml(firstRoute?.title ?? "none")}</strong>
1348
+ </article>
1349
+ <article class="capstan-console-card">
1350
+ <span>Navigation</span>
1351
+ <strong>${projection.navigation.length} projected entries</strong>
1352
+ </article>
1353
+ <article class="capstan-console-card">
1354
+ <span>Action Reachability</span>
1355
+ <strong>${projection.routes.reduce((count, route) => count + route.actions.length, 0)} surfaced actions</strong>
1356
+ </article>
1357
+ </div>
1358
+ ${attentionConsole}
1359
+ <pre data-console-output>{
1360
+ "event": "human_surface.ready",
1361
+ "activeRoute": ${JSON.stringify(firstRoute?.key ?? "")},
1362
+ "routes": ${projection.routes.length}
1363
+ }</pre>
1364
+ </section>`;
1365
+ }
1366
+ function renderGlobalAttentionConsole(attention) {
1367
+ if (!attention.inbox && !attention.queues.length && !attention.presets.length) {
1368
+ return "";
1369
+ }
1370
+ const inboxCard = attention.inbox
1371
+ ? `<article class="capstan-console-card">
1372
+ <span>Attention Inbox</span>
1373
+ <strong data-console-attention-total>0 open</strong>
1374
+ <button type="button" class="capstan-action-button" data-console-attention-inbox="${escapeHtml(attention.inbox.key)}">${escapeHtml(attention.inbox.label)}</button>
1375
+ </article>`
1376
+ : "";
1377
+ const queueCards = attention.queues
1378
+ .map((queue) => `<article class="capstan-console-card">
1379
+ <span>${escapeHtml(queue.label)}</span>
1380
+ <strong data-console-attention-count="${escapeHtml(queue.status)}">0 open</strong>
1381
+ <button type="button" class="capstan-action-button" data-console-attention-queue="${escapeHtml(queue.status)}">Open ${escapeHtml(queue.label)} Queue</button>
1382
+ </article>`)
1383
+ .join("");
1384
+ const taskPresetGroup = renderAttentionPresetGroup("Task Attention Presets", "Open durable work grouped by task before deciding which route or run to inspect next.", attention.presets.filter((preset) => preset.scope === "task"));
1385
+ const resourcePresetGroup = renderAttentionPresetGroup("Resource Attention Presets", "Open durable work grouped by resource or relation context when one part of the domain needs supervision.", attention.presets.filter((preset) => preset.scope === "resource"));
1386
+ const routePresetGroup = renderAttentionPresetGroup("Route Attention Presets", "Open durable work grouped by projected route when you want to move from global supervision into one concrete operator flow.", attention.presets.filter((preset) => preset.scope === "route"));
1387
+ const supervisionWorkspace = renderSupervisionWorkspace();
1388
+ return `<div class="capstan-card" style="margin-bottom: 18px;">
1389
+ <div class="capstan-runtime-header">
1390
+ <h3>Attention Inbox</h3>
1391
+ <span class="capstan-runtime-pill" data-console-attention-status="idle" data-console-attention-state="idle">idle</span>
1392
+ </div>
1393
+ <p class="capstan-state-copy">Open the global durable-work inbox first when you need to discover approvals, input requests, blocks, failures, pauses, or cancellations before targeting a specific route.</p>
1394
+ <div class="capstan-console-actions">
1395
+ ${inboxCard}
1396
+ ${queueCards}
1397
+ </div>
1398
+ ${taskPresetGroup}
1399
+ ${resourcePresetGroup}
1400
+ ${routePresetGroup}
1401
+ ${supervisionWorkspace}
1402
+ <pre class="capstan-route-result" data-console-attention-output>{
1403
+ "event": "console.attention.idle",
1404
+ "message": "No global attention inbox or queue has been opened yet."
1405
+ }</pre>
1406
+ </div>`;
1407
+ }
1408
+ function renderSupervisionWorkspace() {
1409
+ return `<div class="capstan-console-scope-group">
1410
+ <div class="capstan-runtime-header" style="margin-bottom: 8px;">
1411
+ <h4 style="margin: 0;">Supervision Workspace</h4>
1412
+ <span class="capstan-runtime-pill" data-console-supervision-status="idle" data-console-supervision-state="idle">idle</span>
1413
+ </div>
1414
+ <p class="capstan-console-copy" data-console-supervision-copy>Open a task-, resource-, or route-scoped attention preset to pin a reusable supervision workspace.</p>
1415
+ <div class="capstan-badges capstan-attention-breadcrumbs" data-console-supervision-trail>
1416
+ <span class="capstan-badge">No Pinned Workspace</span>
1417
+ </div>
1418
+ <div class="capstan-console-actions">
1419
+ <article class="capstan-console-card">
1420
+ <span>Pinned Trail</span>
1421
+ <strong data-console-supervision-total>0 open</strong>
1422
+ <button type="button" class="capstan-action-button" data-console-supervision-refresh disabled>Refresh Workspace</button>
1423
+ <button type="button" class="capstan-state-toggle" data-console-supervision-inbox disabled>Open Workspace Inbox</button>
1424
+ <button type="button" class="capstan-state-toggle" data-console-supervision-clear-active disabled>Clear Active</button>
1425
+ <button type="button" class="capstan-state-toggle" data-console-supervision-clear-history disabled>Clear History</button>
1426
+ </article>
1427
+ </div>
1428
+ <div class="capstan-console-lane-grid capstan-console-workspace-lanes">
1429
+ ${attentionQueueStatusOrder
1430
+ .map((status) => `<button
1431
+ type="button"
1432
+ class="capstan-state-toggle"
1433
+ data-console-supervision-queue-status="${escapeHtml(status)}"
1434
+ data-console-supervision-queue-label="${escapeHtml(attentionQueueLabel(status))}"
1435
+ disabled
1436
+ >Open ${escapeHtml(attentionQueueLabel(status))} Queue · 0 open</button>`)
1437
+ .join("")}
1438
+ </div>
1439
+ <div class="capstan-runtime-header" style="margin: 12px 0 8px;">
1440
+ <h4 style="margin: 0;">Slot Attention Summary</h4>
1441
+ <span class="capstan-badge" data-console-supervision-slot-summary-count>0 active</span>
1442
+ </div>
1443
+ <div class="capstan-console-scope-grid" data-console-supervision-slot-summaries>
1444
+ ${supervisionWorkspaceSlots
1445
+ .map((slot) => `<article class="capstan-console-card">
1446
+ <span>No Workspace</span>
1447
+ <strong>${escapeHtml(slot.label)}</strong>
1448
+ <div class="capstan-badges">
1449
+ <span class="capstan-badge">${escapeHtml(supervisionWorkspaceSlotRoleBadge(slot.key))}</span>
1450
+ <span class="capstan-badge">Waiting For Save</span>
1451
+ </div>
1452
+ <p class="capstan-console-copy">${escapeHtml(supervisionWorkspaceSlotSummaryPlaceholderCopy(slot.key))}</p>
1453
+ <span class="capstan-attention-count">0 open</span>
1454
+ <button type="button" class="capstan-action-button" data-console-supervision-slot-summary-open="${escapeHtml(slot.key)}" disabled>Open Slot Summary</button>
1455
+ <button type="button" class="capstan-state-toggle" data-console-supervision-slot-summary-queue="${escapeHtml(slot.key)}" disabled>Open Priority Queue</button>
1456
+ </article>`)
1457
+ .join("")}
1458
+ </div>
1459
+ <div class="capstan-runtime-header" style="margin: 12px 0 8px;">
1460
+ <h4 style="margin: 0;">Workspace Slots</h4>
1461
+ <span class="capstan-badge" data-console-supervision-slot-count>0 named</span>
1462
+ </div>
1463
+ <div class="capstan-console-scope-grid" data-console-supervision-slots>
1464
+ ${supervisionWorkspaceSlots
1465
+ .map((slot) => `<article class="capstan-console-card">
1466
+ <span>Empty Slot</span>
1467
+ <strong>${escapeHtml(slot.label)}</strong>
1468
+ <div class="capstan-badges">
1469
+ <span class="capstan-badge">${escapeHtml(slot.label)} Slot</span>
1470
+ <span class="capstan-badge">${escapeHtml(supervisionWorkspaceSlotRoleBadge(slot.key))}</span>
1471
+ </div>
1472
+ <p class="capstan-console-copy">${escapeHtml(supervisionWorkspaceSlotRoleCopy(slot.key))}</p>
1473
+ <span class="capstan-attention-count">0 open</span>
1474
+ <button type="button" class="capstan-action-button" data-console-supervision-slot-open="${escapeHtml(slot.key)}" disabled>Open Slot</button>
1475
+ <button type="button" class="capstan-state-toggle" data-console-supervision-slot-save="${escapeHtml(slot.key)}" disabled>Save Active Here</button>
1476
+ <button type="button" class="capstan-state-toggle" data-console-supervision-slot-clear="${escapeHtml(slot.key)}" disabled>Clear Slot</button>
1477
+ </article>`)
1478
+ .join("")}
1479
+ </div>
1480
+ <div class="capstan-runtime-header" style="margin: 12px 0 8px;">
1481
+ <h4 style="margin: 0;">Saved Workspaces</h4>
1482
+ <span class="capstan-badge" data-console-supervision-history-count>0 saved</span>
1483
+ </div>
1484
+ <div class="capstan-console-scope-grid" data-console-supervision-history>
1485
+ <article class="capstan-console-card">
1486
+ <span>No Saved Workspaces</span>
1487
+ <strong>Pin an attention trail to recover it later.</strong>
1488
+ </article>
1489
+ </div>
1490
+ </div>`;
1491
+ }
1492
+ function renderAttentionPresetGroup(title, description, presets) {
1493
+ if (!presets.length) {
1494
+ return "";
1495
+ }
1496
+ return `<div class="capstan-console-scope-group">
1497
+ <div class="capstan-runtime-header" style="margin-bottom: 8px;">
1498
+ <h4 style="margin: 0;">${escapeHtml(title)}</h4>
1499
+ <span class="capstan-badge">${presets.length} preset${presets.length === 1 ? "" : "s"}</span>
1500
+ </div>
1501
+ <p class="capstan-console-copy">${escapeHtml(description)}</p>
1502
+ <div class="capstan-console-scope-grid">
1503
+ ${presets.map((preset) => renderAttentionPresetCard(preset)).join("")}
1504
+ </div>
1505
+ </div>`;
1506
+ }
1507
+ function renderAttentionPresetCard(preset) {
1508
+ const queueButtons = preset.queues
1509
+ .map((queue) => `<button
1510
+ type="button"
1511
+ class="capstan-state-toggle"
1512
+ data-console-attention-preset-queue="${escapeHtml(preset.key)}"
1513
+ data-console-attention-preset-status="${escapeHtml(queue.status)}"
1514
+ data-console-attention-preset-queue-label="${escapeHtml(queue.label)}"
1515
+ >Open ${escapeHtml(queue.label)} Queue · 0 open</button>`)
1516
+ .join("");
1517
+ return `<article class="capstan-console-card" data-console-attention-preset-auto-slot="${escapeHtml(preset.autoSlotKey)}">
1518
+ <span>${escapeHtml(attentionPresetScopeLabel(preset.scope))}</span>
1519
+ <strong>${escapeHtml(preset.label)}</strong>
1520
+ <div class="capstan-badges">
1521
+ <span class="capstan-badge">Auto Slot</span>
1522
+ <span class="capstan-badge">${escapeHtml(supervisionWorkspaceSlotLabel(preset.autoSlotKey))} Slot</span>
1523
+ </div>
1524
+ <p class="capstan-console-copy">${escapeHtml(`${preset.description} ${attentionPresetAutoSlotCopy(preset.autoSlotKey)}`)}</p>
1525
+ <span class="capstan-attention-count" data-console-attention-preset-total="${escapeHtml(preset.key)}">0 open</span>
1526
+ <button type="button" class="capstan-action-button" data-console-attention-preset-inbox="${escapeHtml(preset.key)}">${escapeHtml(preset.inbox.label)}</button>
1527
+ <div class="capstan-console-lane-grid">
1528
+ ${queueButtons}
1529
+ </div>
1530
+ </article>`;
1531
+ }
1532
+ function policyLabelForState(state) {
1533
+ switch (state) {
1534
+ case "approval_required":
1535
+ return "approval required";
1536
+ case "blocked":
1537
+ return "blocked";
1538
+ case "redacted":
1539
+ return "redacted";
1540
+ default:
1541
+ return "ready";
1542
+ }
1543
+ }
1544
+ function attentionQueueLabel(status) {
1545
+ switch (status) {
1546
+ case "approval_required":
1547
+ return "Approval Required";
1548
+ case "input_required":
1549
+ return "Input Required";
1550
+ case "blocked":
1551
+ return "Blocked";
1552
+ case "failed":
1553
+ return "Failed";
1554
+ case "paused":
1555
+ return "Paused";
1556
+ case "cancelled":
1557
+ return "Cancelled";
1558
+ }
1559
+ }
1560
+ function attentionPresetScopeLabel(scope) {
1561
+ switch (scope) {
1562
+ case "task":
1563
+ return "Task Attention";
1564
+ case "resource":
1565
+ return "Resource Attention";
1566
+ case "route":
1567
+ return "Route Attention";
1568
+ }
1569
+ }
1570
+ function attentionPresetAutoSlotKey(scope) {
1571
+ switch (scope) {
1572
+ case "task":
1573
+ return "primary";
1574
+ case "resource":
1575
+ return "secondary";
1576
+ case "route":
1577
+ return "watchlist";
1578
+ }
1579
+ }
1580
+ function supervisionWorkspaceSlotLabel(slotKey) {
1581
+ return (supervisionWorkspaceSlots.find((slot) => slot.key === slotKey)?.label ?? startCase(slotKey));
1582
+ }
1583
+ function supervisionWorkspaceSlotRoleBadge(slotKey) {
1584
+ switch (slotKey) {
1585
+ case "primary":
1586
+ return "Task Auto Slot";
1587
+ case "secondary":
1588
+ return "Resource Auto Slot";
1589
+ case "watchlist":
1590
+ return "Route Auto Slot";
1591
+ }
1592
+ }
1593
+ function attentionPresetAutoSlotCopy(slotKey) {
1594
+ return `Opening this preset auto-saves it into the ${supervisionWorkspaceSlotLabel(slotKey)} slot unless you manually replace that slot.`;
1595
+ }
1596
+ function supervisionWorkspaceSlotSummaryPlaceholderCopy(slotKey) {
1597
+ return `When this ${supervisionWorkspaceSlotLabel(slotKey).toLowerCase()} slot is tracking a workspace, the console will show its live open count, new-since-open delta, and highest-priority attention lane here.`;
1598
+ }
1599
+ function supervisionWorkspaceSlotRoleCopy(slotKey) {
1600
+ switch (slotKey) {
1601
+ case "primary":
1602
+ return "Task attention presets auto-save here unless you manually replace the slot.";
1603
+ case "secondary":
1604
+ return "Resource attention presets auto-save here unless you manually replace the slot.";
1605
+ case "watchlist":
1606
+ return "Route attention presets auto-save here unless you manually replace the slot.";
1607
+ }
1608
+ }
1609
+ function actionLabelForMode(mode) {
1610
+ switch (mode) {
1611
+ case "write":
1612
+ return "submit action";
1613
+ case "external":
1614
+ return "launch action";
1615
+ default:
1616
+ return "run action";
1617
+ }
1618
+ }
1619
+ function actionNote(capability, policyState) {
1620
+ const base = capability.description ?? `Execute the "${capability.key}" capability from the projected human surface.`;
1621
+ switch (policyState) {
1622
+ case "approval_required":
1623
+ return `${base} This action is present, but the graph marks it as approval-gated.`;
1624
+ case "blocked":
1625
+ return `${base} The current policy projection blocks direct execution.`;
1626
+ case "redacted":
1627
+ return `${base} Outputs from this action may be redacted before they reach the operator.`;
1628
+ default:
1629
+ return base;
1630
+ }
1631
+ }
1632
+ function sampleValueForField(field) {
1633
+ switch (field.type) {
1634
+ case "integer":
1635
+ return "7";
1636
+ case "number":
1637
+ return "42.5";
1638
+ case "boolean":
1639
+ return "true";
1640
+ case "date":
1641
+ return "2026-03-22";
1642
+ case "datetime":
1643
+ return "2026-03-22T10:00:00Z";
1644
+ case "json":
1645
+ return '{"ok":true}';
1646
+ default:
1647
+ return `${field.label} sample`;
1648
+ }
1649
+ }
1650
+ function optionalProperty(key, value) {
1651
+ return value === undefined ? {} : { [key]: value };
1652
+ }
1653
+ function dedupeRoutes(routes) {
1654
+ const seen = new Set();
1655
+ const unique = [];
1656
+ for (const route of routes) {
1657
+ if (seen.has(route.key)) {
1658
+ continue;
1659
+ }
1660
+ seen.add(route.key);
1661
+ unique.push(route);
1662
+ }
1663
+ return unique;
1664
+ }
1665
+ function createRelationRouteReference(resource, relationKey, relation) {
1666
+ const routeKind = relation.kind === "many" ? "list" : "detail";
1667
+ const relationStem = startCase(relationKey).replace(/\s+/g, "");
1668
+ const routeKindStem = startCase(routeKind).replace(/\s+/g, "");
1669
+ return {
1670
+ key: `${resource.key}${relationStem}Relation${routeKindStem}`,
1671
+ path: `/resources/${toKebabCase(resource.key)}/relations/${toKebabCase(relationKey)}/${routeKind}`,
1672
+ title: `${resource.title} ${startCase(relationKey)} ${startCase(routeKind)}`
1673
+ };
1674
+ }
1675
+ function toKebabCase(value) {
1676
+ return value
1677
+ .replace(/([a-z0-9])([A-Z])/g, "$1-$2")
1678
+ .replace(/[^a-zA-Z0-9]+/g, "-")
1679
+ .replace(/-{2,}/g, "-")
1680
+ .replace(/^-+|-+$/g, "")
1681
+ .toLowerCase();
1682
+ }
1683
+ function startCase(value) {
1684
+ return value
1685
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
1686
+ .replace(/[^a-zA-Z0-9]+/g, " ")
1687
+ .trim()
1688
+ .split(/\s+/)
1689
+ .filter(Boolean)
1690
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
1691
+ .join(" ");
1692
+ }
1693
+ function escapeHtml(value) {
1694
+ return value
1695
+ .replaceAll("&", "&amp;")
1696
+ .replaceAll("<", "&lt;")
1697
+ .replaceAll(">", "&gt;")
1698
+ .replaceAll('"', "&quot;")
1699
+ .replaceAll("'", "&#39;");
1700
+ }
1701
+ //# sourceMappingURL=index.js.map