@monster-orange/den 0.2.7 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/CHANGELOG.md +1655 -0
  2. package/README.md +4 -2
  3. package/bin/den +2 -1
  4. package/docs/distributions.md +619 -3
  5. package/make/den.mk +19 -4
  6. package/make/journeys.mk +2 -1
  7. package/make/local_dev.mk +6 -13
  8. package/make/secrets-1password.mk +18 -0
  9. package/make/test.mk +1 -1
  10. package/package.json +9 -2
  11. package/schemas/den.plugin.v1.json +156 -0
  12. package/schemas/den.theme.v1.json +269 -0
  13. package/scripts/auth-configure.ts +16 -8
  14. package/scripts/heal-config.ts +0 -5
  15. package/scripts/setup-1password.ts +41 -19
  16. package/scripts/setup-secrets.ts +67 -101
  17. package/src/pillars/api/global-setup.ts +1 -1
  18. package/src/pillars/journeys/sandbox.ts +23 -18
  19. package/src/pillars/journeys/tools/browser-session.ts +2 -2
  20. package/src/scripts/setup-default-workspace.ts +2 -2
  21. package/src/shared/agent-actor.ts +38 -3
  22. package/src/shared/auth/headless-login.ts +1 -1
  23. package/src/shared/auth/sudo-vault.ts +93 -31
  24. package/src/shared/auth/token-providers/index.ts +7 -3
  25. package/src/shared/brand.ts +7 -0
  26. package/src/shared/config-error.ts +63 -0
  27. package/src/shared/constants.ts +3 -24
  28. package/src/shared/den-config.ts +185 -7
  29. package/src/shared/edge/config.ts +147 -21
  30. package/src/shared/edge/exec.ts +31 -0
  31. package/src/shared/edge/included.ts +169 -0
  32. package/src/shared/edge/render.ts +69 -14
  33. package/src/shared/edge/runtime.ts +128 -121
  34. package/src/shared/edge/status.ts +68 -8
  35. package/src/shared/edge/tls.ts +328 -0
  36. package/src/shared/edge/types.ts +25 -9
  37. package/src/shared/environment-manager/catalog.ts +52 -1
  38. package/src/shared/environment-manager/default-workspace.ts +26 -3
  39. package/src/shared/environment-manager/git-remote.ts +49 -0
  40. package/src/shared/environment-manager/health.ts +73 -38
  41. package/src/shared/environment-manager/scope.ts +168 -0
  42. package/src/shared/environment-manager/service-runner.ts +789 -43
  43. package/src/shared/environment-manager/up-targets.ts +132 -0
  44. package/src/shared/environment-manager/workspace-manager.ts +67 -25
  45. package/src/shared/environment-manager/workspace-org.ts +26 -11
  46. package/src/shared/health/rollup.ts +48 -0
  47. package/src/shared/manifest/service-links.ts +20 -8
  48. package/src/shared/open-url.ts +21 -2
  49. package/src/shared/org-sources.ts +328 -0
  50. package/src/shared/org.ts +53 -5
  51. package/src/shared/plugins/manifest.ts +406 -0
  52. package/src/shared/plugins/store.ts +358 -0
  53. package/src/shared/process/legacy-adopt.ts +285 -0
  54. package/src/shared/process/process-group.ts +202 -0
  55. package/src/shared/process/spawn-streaming.ts +7 -0
  56. package/src/shared/secrets/doppler.ts +1 -1
  57. package/src/shared/secrets/index.ts +279 -50
  58. package/src/shared/secrets/none.ts +56 -0
  59. package/src/shared/secrets/strategies.ts +202 -0
  60. package/src/shared/secrets/types.ts +1 -1
  61. package/src/shared/smoke/git-remote.ts +40 -0
  62. package/src/shared/stacks/manifest-commands.ts +1 -1
  63. package/src/shared/stacks/namespace.ts +178 -0
  64. package/src/shared/stacks/owners.ts +303 -0
  65. package/src/shared/stacks/render.ts +8 -3
  66. package/src/shared/stacks/runtime.ts +100 -16
  67. package/src/shared/stacks/status.ts +63 -12
  68. package/src/shared/strategies/registry.ts +89 -0
  69. package/src/shared/theme/contract.ts +538 -0
  70. package/src/shared/types/index.ts +24 -5
  71. package/src/ui/cli/clone-plan.ts +154 -23
  72. package/src/ui/cli/create.ts +24 -7
  73. package/src/ui/cli/doctor-command.ts +131 -0
  74. package/src/ui/cli/edge-command.ts +56 -10
  75. package/src/ui/cli/flag-parser.ts +62 -3
  76. package/src/ui/cli/health-display.ts +25 -0
  77. package/src/ui/cli/index.ts +649 -72
  78. package/src/ui/cli/notice.ts +20 -0
  79. package/src/ui/cli/org-command.ts +12 -2
  80. package/src/ui/cli/plugin-command.ts +849 -0
  81. package/src/ui/cli/stack-command.ts +63 -2
  82. package/src/ui/cli/test-runner.ts +11 -9
  83. package/src/ui/cli/theme-command.ts +136 -0
  84. package/src/ui/cli/up-args.ts +15 -0
  85. package/src/ui/cli/web-command.ts +47 -23
  86. package/src/ui/mcp/api.ts +28 -2
  87. package/src/ui/mcp/scope.ts +298 -0
  88. package/src/ui/mcp/server.ts +70 -8
  89. package/src/ui/mcp/tools.ts +151 -24
  90. package/src/ui/server/appearance-prefs.ts +95 -0
  91. package/src/ui/server/audit.ts +90 -3
  92. package/src/ui/server/daemon-reaper.ts +29 -1
  93. package/src/ui/server/engine-stamp.ts +83 -0
  94. package/src/ui/server/github-service.ts +29 -45
  95. package/src/ui/server/graph-page.ts +20 -0
  96. package/src/ui/server/graph-prefs.ts +44 -1
  97. package/src/ui/server/http-errors.ts +293 -0
  98. package/src/ui/server/http-helpers.ts +22 -3
  99. package/src/ui/server/index.ts +590 -45
  100. package/src/ui/server/public/appearance.js +531 -0
  101. package/src/ui/server/public/edge-page.js +248 -0
  102. package/src/ui/server/public/edge.html +251 -0
  103. package/src/ui/server/public/environments.html +65 -83
  104. package/src/ui/server/public/error-page.js +247 -0
  105. package/src/ui/server/public/error.html +260 -0
  106. package/src/ui/server/public/graph-model.js +332 -14
  107. package/src/ui/server/public/graph.css +141 -47
  108. package/src/ui/server/public/graph.js +223 -44
  109. package/src/ui/server/public/header.js +396 -600
  110. package/src/ui/server/public/health-fit.js +248 -0
  111. package/src/ui/server/public/health-rollup.js +128 -0
  112. package/src/ui/server/public/i18n/es.js +287 -0
  113. package/src/ui/server/public/i18n/ja.js +276 -0
  114. package/src/ui/server/public/index.html +139 -438
  115. package/src/ui/server/public/local-dev.html +3468 -4044
  116. package/src/ui/server/public/logs.html +71 -208
  117. package/src/ui/server/public/panel.js +15 -3
  118. package/src/ui/server/public/release.html +175 -389
  119. package/src/ui/server/public/services.html +596 -770
  120. package/src/ui/server/public/settings-page.js +759 -0
  121. package/src/ui/server/public/settings-registry.js +521 -0
  122. package/src/ui/server/public/settings.html +590 -0
  123. package/src/ui/server/public/shell.js +3869 -0
  124. package/src/ui/server/public/topbar.js +1413 -0
  125. package/src/ui/server/public/version-label.js +12 -0
  126. package/src/ui/server/public/workspace-picker.js +74 -34
  127. package/src/ui/server/readonly-routes.ts +24 -4
  128. package/src/ui/server/release-service.ts +25 -3
  129. package/src/ui/server/services-list.ts +94 -17
  130. package/src/ui/server/settings-distribution.ts +74 -0
  131. package/src/ui/server/shell-prefs.ts +192 -0
  132. package/src/ui/server/theme-routes.ts +292 -0
  133. package/src/ui/server/theme-runtime.ts +407 -0
  134. package/src/ui/server/ui-assets.ts +16 -1
  135. package/src/ui/server/ui-language.ts +50 -0
  136. package/src/ui/server/workspace-routes.ts +55 -9
  137. package/themes/den/den-theme.json +72 -0
  138. package/themes/den/favicon.ico +0 -0
  139. package/themes/den/mark.svg +6 -0
@@ -0,0 +1,3869 @@
1
+ /**
2
+ * The shell (0.3.0-dev.14, rail rebuilt in 0.3.0-dev.17): one top bar and
3
+ * one left rail, the same on every page. A page holds only its content;
4
+ * its markup starts with
5
+ *
6
+ * <div id="den-shell" data-page="<id>"></div>
7
+ * <script src="/shell.js"></script>
8
+ *
9
+ * A classic script, run where it stands: the frame is in the DOM before the
10
+ * page's own scripts look for #health, #env-sel, #version-pill or #ws-pill.
11
+ * header.js then drives it (health row, changelog, workspace and env
12
+ * pickers); appearance.js adds ◐ at the end of the bar.
13
+ *
14
+ * The acceptance bar is the shell prototype (den-merge-notes/shell-proto/
15
+ * den-shell.html): its sizes, weights, spacing and states are ported here
16
+ * as they are, its colors mapped onto the theme tokens (--r-btn is
17
+ * --radius-button, --bar-grad is --bar). scripts/shell-fidelity.mjs
18
+ * measures the two side by side.
19
+ *
20
+ * top bar ☰ (narrow) · brand mark and title · the scope breadcrumb
21
+ * (R4, dev.18): / ORG <org> ▾ / WORKSPACE <ws> ▾ / ENV <env> ▾,
22
+ * each part an .ib button with a .pop menu (an ARIA menu: ↑/↓,
23
+ * Home, End, Enter; Escape and a click outside close it) ·
24
+ * the ⌘K search field (R5) · the status chip (R6) · the Runs
25
+ * chip (R7) · the lock chip (R8) · What's new · ◐ ▾ (R10).
26
+ * shell.js draws them; topbar.js (through header.js) drives the
27
+ * chips, the command palette (R17), the status drawer (R15) and
28
+ * the lock popover; appearance.js the user menu (dev.20).
29
+ * rail Pinned, then the NAV registry by GROUPS (Develop, Verify,
30
+ * Ship, a distribution's own), then Settings and the collapse
31
+ * toggle. One tab stop (the current page); ↑/↓, Home and End
32
+ * move within it, Delete unpins a pinned row. Collapsed: icons
33
+ * only, the headings kept for screen readers. Below 720px it is
34
+ * a drawer behind ☰.
35
+ *
36
+ * The registries and the scope (window.denShell, the spec's NAV and GROUPS):
37
+ * registerGroup({ id, label, order, from? })
38
+ * registerNav({ id, label, icon, group, order, href, badge?(state),
39
+ * when?(state) }) a non-number order warns
40
+ * (console.warn); a page whose group isn't registered
41
+ * waits for it, and warns only if the group is still
42
+ * missing a tick after the queue has drained
43
+ * registerSettingsCategory({ id, label, order, description?, from? })
44
+ * registerSetting({ id, category, label, description, keywords, scope,
45
+ * control, read, write?, keys? }) the SETTINGS registry
46
+ * (R21): the Settings page (/settings) draws every row
47
+ * from it. scope is "user", "workspace" or "distribution";
48
+ * a distribution row is read-only (no write). Any change
49
+ * fires `den:settings` on window. Each row is also a ⌘K
50
+ * palette command (kind "setting", id "setting:<id>"):
51
+ * Enter opens /settings?q=<label>#<category>.
52
+ * settings(), settingsCategories() the registry, categories by order
53
+ * setNavPrefs({ hidden?, order? }), setPins(ids), setCollapsed(bool)
54
+ * Navigation (R21): the rail pages hidden, their order,
55
+ * the pins; saved to /api/settings `shell`
56
+ * addMessages(lang, { key: text }), t(key, vars)
57
+ * setState(partial): health { repo|address: state }, openLogs, …
58
+ * pin(id), unpin(id), isPinned(id): pins are user prefs (/api/settings
59
+ * `shell.pins`), nothing else. Any change fires `den:pins` on window
60
+ * (detail { pins }), and every [data-pin-toggle] repaints from it.
61
+ * setOrg({ id, title }) one org: a static label, no menu (C4)
62
+ * setOrgs([{ id, title, workspaces? }], currentId, onSelect?)
63
+ * two or more: the Org ▾ menu; a pick
64
+ * calls onSelect(id), fires `den:org`
65
+ * setWorkspaces({ label, title, items, focusable, onSelect, onFocus })
66
+ * the Workspace ▾ menu (header.js)
67
+ * syncEnv() repaint Env ▾ from #env-sel
68
+ * registerCommand({ id, label, keywords?, kind?, run })
69
+ * a ⌘K palette row (R17, dev.20). label
70
+ * is a string or a function returning
71
+ * one; keywords a string or an array;
72
+ * kind "action" (the default), "setting"
73
+ * (the Settings registry's rows, dev.19)
74
+ * or any word, shown as the row's kind
75
+ * label. Pages, services and apps are
76
+ * added by the palette itself.
77
+ * unregisterCommand(id), commands()
78
+ * openMenu(anchor, menu) a .pop menu below anchor (the scope
79
+ * parts' and the user menu's)
80
+ *
81
+ * Loading before shell.js: push functions onto window.denShellQueue (an
82
+ * array); each runs with denShell once it exists, and a later push runs at
83
+ * once. `den-shell:ready` fires on window when the shell is up.
84
+ *
85
+ * Every page wraps its content in <main id="den-content">, its one main
86
+ * landmark (the skip link's target). Its one h1 is the page's own (the
87
+ * brand title is a span); a page without one gets a visually hidden h1
88
+ * named from its <title>.
89
+ *
90
+ * data-health-env pins the health row's env on pages that are local by
91
+ * nature (Workspaces, Releases), whatever the env picker says.
92
+ *
93
+ * Every color and font is a theme token.
94
+ */
95
+ (function () {
96
+ "use strict";
97
+ // ── i18n (R19, 0.3.0-dev.39) ───────────────────────────────────────────
98
+ // Every word the shell shows is a key in MESSAGES, looked up with
99
+ // t(key, vars). en is here, the fallback for any key a catalog lacks;
100
+ // every other language is a classic script, /i18n/<lang>.js, that the
101
+ // server loads before this one for a page in that language (with
102
+ // <html lang>, ui-language.ts), adding its table to window.denMessages.
103
+ // A page, a plugin or a distribution adds its own keys with
104
+ // denShell.addMessages(lang, table).
105
+ //
106
+ // {name} a var; a number is formatted by Intl.NumberFormat
107
+ // key.one the plural form t(key, { n }) uses where Intl.PluralRules
108
+ // says "one" for n (any CLDR category works: key.few, …)
109
+ //
110
+ // Names (services, workspaces, repos, themes, languages), logs and
111
+ // command output are never translated. tests/unit/server/i18n.test.ts
112
+ // fails when a key is missing from any catalog.
113
+ var MESSAGES = (window.denMessages = window.denMessages || {});
114
+ MESSAGES.en = {
115
+ "g.pinned": "Pinned",
116
+ "g.develop": "Develop",
117
+ "g.verify": "Verify",
118
+ "g.ship": "Ship",
119
+ "n.workspaces": "Workspaces",
120
+ "n.graph": "Graph",
121
+ "n.services": "Services",
122
+ "n.logs": "Logs",
123
+ "n.tests": "Tests",
124
+ "n.environments": "Environments",
125
+ "n.releases": "Releases",
126
+ "n.edge": "Edge",
127
+ "n.settings": "Settings",
128
+ "k.org": "Org",
129
+ "k.ws": "Workspace",
130
+ "k.env": "Env",
131
+ scope: "Scope",
132
+ "org.title": "Org: {title}",
133
+ "org.titleId": "Org: {title} ({id})",
134
+ "ws.all": "All active",
135
+ "ws.focus": "focus",
136
+ "ws.none": "No active workspaces",
137
+ "ws.manage": "Manage workspaces…",
138
+ "ws.allN": "All active ({n})",
139
+ "ws.allTip":
140
+ "Every active workspace; a service several define is listed once",
141
+ "ws.noneLabel": "none",
142
+ "ws.viewTitle":
143
+ "View: what every page shows ({ws}). A view filter only: it starts, stops and routes nothing.",
144
+ "ws.viewTitleFocus":
145
+ "View: what every page shows ({ws}). A view filter only: it starts, stops and routes nothing, and focus ({focus}) stays the target for commands.",
146
+ "ws.setFocus": "Set {ws} as focus",
147
+ "ws.setFocusTip":
148
+ "Make it the default target for commands (writes workspaces.yml)",
149
+ "ws.viewFailed": "Could not change the view: {msg}",
150
+ "ws.focusFailed": "Could not set the focus: {msg}",
151
+ // A count's singular is its .one (0.3.0-dev.40: en had none, so the org
152
+ // menu said "1 workspaces").
153
+ "org.n": "{n} workspaces",
154
+ "org.n.one": "{n} workspace",
155
+ from: "from {d}",
156
+ "b.logs": "{n} open",
157
+ "b.logs.one": "{n} open",
158
+ "b.down": "{n} down",
159
+ "b.down.one": "{n} down",
160
+ "act.pin": "Pin",
161
+ "act.unpin": "Unpin",
162
+ "act.pinX": "Pin {s}",
163
+ "act.unpinX": "Unpin {s}",
164
+ "toast.pinned": "Pinned {s}",
165
+ "toast.unpinned": "Unpinned {s}",
166
+ collapse: "Collapse",
167
+ expand: "Expand",
168
+ menu: "Menu",
169
+ skip: "Skip to content",
170
+ pages: "Pages",
171
+ // The top bar (dev.20): the prototype's strings.
172
+ "search.ph": "Search pages, services, actions…",
173
+ "status.checking": "Checking…",
174
+ "status.none": "No services",
175
+ "status.unchecked": "{n} not checked",
176
+ "status.all": "All {n} healthy",
177
+ "status.all.one": "{n} healthy",
178
+ "status.some": "{ok}/{n} healthy",
179
+ "status.down": "{n} down",
180
+ "status.down.one": "{n} down",
181
+ "status.title": "{n} checked",
182
+ "status.title.one": "{n} checked",
183
+ "status.pending": "{n} pending",
184
+ "status.pending.one": "{n} pending",
185
+ "status.awaiting": "{n} still checking",
186
+ "status.unknown": "Status unknown",
187
+ "status.unknownTip":
188
+ "Den's server isn't responding, so service health is unknown",
189
+ // What a check said (the words; the URL and the error are output).
190
+ "chk.awaiting": "still checking",
191
+ "chk.pending": "{what} — pending",
192
+ "chk.check": "check",
193
+ "chk.unchecked": "not checked",
194
+ "chk.healthy": "healthy",
195
+ "chk.down": "down",
196
+ "chk.pendingWord": "pending",
197
+ "chk.checking": "checking…",
198
+ ms: "{n} ms",
199
+ "fix.failedAt": "{line} · failed at {at}",
200
+ "fix.start": "Start",
201
+ "fix.startX": "Start {s}",
202
+ "fix.logs": "Logs",
203
+ "fix.logsX": "Logs of {s}",
204
+ "row.name": "{id}, {state}",
205
+ "row.nameMs": "{id}, {state}, {ms}",
206
+ // The status drawer (R15).
207
+ "drawer.title": "Status",
208
+ "drawer.refresh": "Check again",
209
+ "drawer.close": "Close",
210
+ "drawer.attention": "Needs attention",
211
+ "drawer.nothing": "Nothing needs attention.",
212
+ "drawer.everything": "Everything",
213
+ "drawer.okTip": "{ok} of {n} checked services healthy",
214
+ "drawer.okTip.one": "{ok} of {n} checked service healthy",
215
+ "toast.openWs": "Open Workspaces to run this",
216
+ // The command palette (R17).
217
+ "kind.page": "Page",
218
+ "kind.service": "Service",
219
+ "kind.app": "App",
220
+ "kind.action": "Action",
221
+ "kind.setting": "Setting",
222
+ "pal.repos": "{n} repos",
223
+ "pal.repos.one": "{n} repo",
224
+ "pal.restart": "Restart {s}",
225
+ "pal.results": "Results",
226
+ "pal.move": "↑↓ to move",
227
+ "pal.open": "↵ to open",
228
+ "pal.close": "esc to close",
229
+ "pal.none": "No matches",
230
+ "pal.count": "{n} results",
231
+ "pal.count.one": "{n} result",
232
+ // The runs chip and dock (R7, R16).
233
+ "runs.title": "Runs",
234
+ "runs.n": "{n} running",
235
+ "runs.aria": "Runs, {n} running",
236
+ "runs.last": "Last run: {s}",
237
+ "runs.none": "No runs",
238
+ "runs.empty": "Nothing has run yet. Output from any page lands here.",
239
+ "runs.toast": "Nothing has run yet. A page's output shows here.",
240
+ "runs.all": "All runs",
241
+ "runs.allTip": "Showing {ws}'s runs; press for every run",
242
+ "runs.allOn": "Showing every run; press for {ws}'s only",
243
+ "dock.done": "done in {s}s",
244
+ "dock.failed": "failed in {s}s",
245
+ "dock.close": "Hide runs",
246
+ "dock.show": "Show runs",
247
+ "dock.copy": "Copy",
248
+ "dock.copied": "Copied!",
249
+ "dock.clear": "Clear",
250
+ "dock.closeRun": "Close {s}",
251
+ "dock.err": "failed",
252
+ "dock.last": "Last run: {s} ↗",
253
+ "dock.resize": "Drag to resize",
254
+ // The lock chip and its popover (R8).
255
+ "lock.locked": "Locked",
256
+ "lock.unlocked": "Unlocked",
257
+ "lock.title": "Unlock to start, stop and restart services",
258
+ "lock.nopw": "Passwordless sudo (NOPASSWD): no admin password needed",
259
+ "lock.session": "Unlocked for this session",
260
+ "lock.sessionDesc":
261
+ "Unlocked for this session: start, stop and restart work on every page.",
262
+ "lock.cfg":
263
+ "Saving the password is off: set secrets.provider and secrets.refs.sudo in {file}.",
264
+ "lock.update": "Update stored password",
265
+ "lock.saveTo": "Save to {s}",
266
+ "lock.pw": "Admin password",
267
+ "lock.go": "Unlock",
268
+ "lock.checking": "Checking…",
269
+ "lock.note": "Den asks once per session. Agents can't unlock.",
270
+ "lock.again": "Enter the password again…",
271
+ "lock.saveFailed": "Unlocked, but saving to {s} failed: {err}",
272
+ "lock.wrong": "Incorrect password — try again",
273
+ "lock.reqFailed": "Request failed — is the server running?",
274
+ // What's new (R9) and the stale-server pill.
275
+ "v.title": "View changelog",
276
+ "cl.title": "What's new",
277
+ "cl.close": "Close",
278
+ "cl.gotIt": "Got it",
279
+ "cl.none": "No entries found.",
280
+ "cl.copyMd": "Copy as Markdown",
281
+ "cl.copyMdX": "Copy v{v} as Markdown",
282
+ "cl.copySlack": "Copy for Slack",
283
+ "cl.copySlackX": "Copy v{v} for Slack",
284
+ "cl.copyFailed": "Copy failed — try again",
285
+ "cl.latest": "Show latest only",
286
+ "cl.all": "Show all versions ({n} earlier)",
287
+ "cl.all.one": "Show all versions ({n} earlier)",
288
+ "stale.title":
289
+ "This {cli} web server is running code older than your working tree — click to restart and pick up your changes. Running deploys continue on GitHub; an open deploy log reattaches after the reload.",
290
+ "stale.aria": "Stale server: restart",
291
+ "stale.label": "stale server",
292
+ "stale.restart": "Restart",
293
+ "restart.msg": "Restarting {cli} web…",
294
+ "restart.hint": "Reconnecting — this page will reload automatically.",
295
+ "restart.still":
296
+ "Still restarting… if this persists, run {cmd} in a terminal, then reload.",
297
+ // The user menu (R10) and its palette commands (R17).
298
+ "um.label": "Appearance",
299
+ "um.title": "Appearance and theme",
300
+ "um.appearance": "Appearance",
301
+ "um.system": "System",
302
+ "um.dark": "Dark",
303
+ "um.light": "Light",
304
+ "um.theme": "Theme",
305
+ "um.language": "Language",
306
+ "um.all": "All settings",
307
+ "cmd.appearance": "Appearance: {mode}",
308
+ "cmd.theme": "Theme: {name}",
309
+ "cmd.language": "Language: {name}",
310
+ "theme.orgDefault": "Org default ({name})",
311
+ "theme.pinned": "{name} (pinned)",
312
+ "theme.notInstalled":
313
+ "Your theme {id} isn't installed, so {inUse} is in use",
314
+ "theme.invalid":
315
+ "Your theme {id} isn't valid ({why}), so {inUse} is in use",
316
+ "theme.invalidBare": "Your theme {id} isn't valid, so {inUse} is in use",
317
+ // Errors (R26, 0.3.0-dev.38).
318
+ "api.network": "Den's server didn't answer {what}",
319
+ "api.failed": "{what} failed",
320
+ "api.request": "Request {id}",
321
+ "api.retry": "Retry",
322
+ "api.copy": "Copy details",
323
+ "api.copied": "Copied",
324
+ "api.close": "Dismiss",
325
+ "api.retried": "Done",
326
+ "net.down": "Den's server isn't responding — retrying",
327
+ "net.next": "next try in {s}s",
328
+ "net.now": "Retry now",
329
+ "engine.stale":
330
+ "Den was updated to {onDisk}. This server is still running {loaded}. Restart it: {cli} web restart (or your service restart).",
331
+ "engine.title": "Restart needed",
332
+ // The error page (R26a, error-page.js).
333
+ "err.404.title": "Page not found",
334
+ "err.404.sub": "Nothing here at {path}",
335
+ "err.404.lead": "There is no page at this address.",
336
+ "err.404.mean": "Did you mean {page}?",
337
+ "err.404.stale":
338
+ "This page may be new in Den {onDisk}: the server is still running {loaded}.",
339
+ "err.404.plugin":
340
+ "This page belongs to the {name} plugin, which isn't installed in this distribution.",
341
+ "err.404.pluginLocked":
342
+ "This page belongs to the {name} plugin. It is in plugins.lock.json, but its files are missing.",
343
+ "err.404.pluginNoPage":
344
+ "The {name} plugin is installed, but it has no page here.",
345
+ "err.404.workspaces": "Go to Workspaces",
346
+ "err.404.search": "Search",
347
+ "err.404.pages": "Pages",
348
+ "err.500.title": "Something went wrong",
349
+ "err.500.sub": "{what} failed on the server",
350
+ "err.500.lead":
351
+ "The server hit an error answering this page. Retry, or copy the details for a report: the server log has the full error under the same request id.",
352
+ "err.500.what": "What failed",
353
+ "err.500.time": "Time",
354
+ "err.500.id": "Request id",
355
+ "err.500.version": "Version",
356
+ "err.422.title": "Config is invalid",
357
+ "err.422.sub": "The distribution config fails validation",
358
+ "err.422.lead":
359
+ "Pages that read the config wait until it validates. Fix each key below; Settings and Logs still work meanwhile.",
360
+ "err.422.file": "File",
361
+ "err.422.key": "Key",
362
+ "err.422.problem": "Problem",
363
+ "err.422.cmd": "To see the same in a terminal, run:",
364
+ "err.422.reload": "Reload once fixed",
365
+ "err.422.logs": "Open Logs",
366
+ "err.422.settings": "Open Settings",
367
+ // The Settings registry's built-in rows, as the palette shows them
368
+ // (set.<row id>; settings-registry.js keeps the English label).
369
+ "set.appearance.mode": "Mode",
370
+ "set.appearance.theme": "Theme",
371
+ "set.language": "Language",
372
+ "set.appearance.org-theme": "Org default theme",
373
+ "set.editor": "Default IDE",
374
+ "set.editor_overrides": "Per-language IDE",
375
+ "set.terminal": "Default terminal",
376
+ "set.shell.pages": "Pages",
377
+ "set.shell.pins": "Pinned",
378
+ "set.shell.collapsed": "Collapse the rail",
379
+ "set.shell.dock": "Open the runs dock",
380
+ "set.graph.workspaces": "Workspace list",
381
+ "set.graph.groupBy": "Group repos by",
382
+ "set.graph.direction": "Direction",
383
+ "set.graph.group": "Frames",
384
+ "set.graph.density": "Cards",
385
+ "set.graph.panelWidth": "Details panel",
386
+ "set.workspace.editor": "IDE for this workspace",
387
+ "set.cli.local_dev": "Local environment",
388
+ "set.cli.cli_tabs": "Terminal tabs",
389
+ "set.cli.log_window_lines": "Log pane lines",
390
+ "set.cli.web_port": "Your web port",
391
+ "set.distribution.brand": "Product name",
392
+ "set.distribution.github_org": "GitHub org",
393
+ "set.distribution.orgs": "Org labels",
394
+ "set.distribution.web_port": "Web port",
395
+ "set.distribution.environments": "Environments",
396
+ "set.distribution.domain": "Edge domain",
397
+ "err.retry": "Retry",
398
+ "err.copy": "Copy details",
399
+ "err.copied": "Copied",
400
+ };
401
+ // The languages the UI has a catalog for, each named in itself (a name is
402
+ // never translated). The server's list is ui-language.ts's UI_LANGUAGES.
403
+ var LANGUAGES = [
404
+ { id: "en", name: "English" },
405
+ { id: "ja", name: "日本語" },
406
+ { id: "es", name: "Español" },
407
+ ];
408
+ // The page's language: <html lang>, when a catalog for it is loaded.
409
+ var lang = (function () {
410
+ var l = (document.documentElement && document.documentElement.lang) || "en";
411
+ return MESSAGES[l] ? l : "en";
412
+ })();
413
+ function has(table, key) {
414
+ return !!table && Object.prototype.hasOwnProperty.call(table, key);
415
+ }
416
+ var pluralRules = null;
417
+ function pluralOf(n) {
418
+ try {
419
+ pluralRules = pluralRules || new Intl.PluralRules(lang);
420
+ return pluralRules.select(n);
421
+ } catch (e) {
422
+ return n === 1 ? "one" : "other";
423
+ }
424
+ }
425
+ /** A number in the page's language (Intl.NumberFormat, its options). */
426
+ function num(n, opts) {
427
+ try {
428
+ return new Intl.NumberFormat(lang, opts).format(n);
429
+ } catch (e) {
430
+ return String(n);
431
+ }
432
+ }
433
+ /** The catalog's text for key (its plural form for vars.n), or null. */
434
+ function lookup(table, key, vars) {
435
+ if (!has(table, key)) return null;
436
+ if (vars && typeof vars.n === "number") {
437
+ var form = key + "." + pluralOf(vars.n);
438
+ if (has(table, form)) return table[form];
439
+ }
440
+ return table[key];
441
+ }
442
+ function t(key, vars) {
443
+ var s = lookup(MESSAGES[lang], key, vars);
444
+ if (s == null && lang !== "en") s = lookup(MESSAGES.en, key, vars);
445
+ if (s == null) s = key;
446
+ return String(s).replace(/\{(\w+)\}/g, function (m, k) {
447
+ if (!vars || vars[k] == null) return m;
448
+ return typeof vars[k] === "number" ? num(vars[k]) : String(vars[k]);
449
+ });
450
+ }
451
+
452
+ var host = document.getElementById("den-shell");
453
+ if (!host || host.dataset.mounted) return;
454
+ host.dataset.mounted = "1";
455
+ var page = host.dataset.page || "";
456
+ // The page id on the body (den-page-<id>), for CSS from first paint: the
457
+ // Graph page is the Workspaces page's file at /graph (R22, 0.3.0-dev.32).
458
+ if (/^[a-z0-9-]+$/.test(page) && document.body)
459
+ document.body.classList.add("den-page-" + page);
460
+
461
+ function esc(v) {
462
+ return String(v == null ? "" : v).replace(/[&<>"]/g, function (c) {
463
+ return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[c];
464
+ });
465
+ }
466
+
467
+ // ── icons: the prototype's 24px line drawings ─────────────────────────
468
+ var ICONS = {
469
+ overview: '<path d="M4 4h7v7H4zM13 4h7v4h-7zM13 10h7v10h-7zM4 13h7v7H4z"/>',
470
+ graph:
471
+ '<circle cx="6" cy="6" r="2.5"/><circle cx="18" cy="8" r="2.5"/><circle cx="9" cy="18" r="2.5"/><path d="M8.2 7.2 15.6 8M7 8.4l1.6 7.2M16.4 10.2l-5.6 6"/>',
472
+ services:
473
+ '<rect x="4" y="4" width="16" height="6" rx="1.5"/><rect x="4" y="14" width="16" height="6" rx="1.5"/><path d="M8 7h.01M8 17h.01"/>',
474
+ logs: '<path d="M5 5h14M5 9h10M5 13h14M5 17h8"/>',
475
+ tests:
476
+ '<path d="M9 4h6M10 4v5l-5 9a1.5 1.5 0 0 0 1.3 2h11.4a1.5 1.5 0 0 0 1.3-2l-5-9V4"/>',
477
+ envs: '<circle cx="12" cy="12" r="8"/><path d="M4 12h16M12 4c2.5 2.5 2.5 13.5 0 16M12 4c-2.5 2.5-2.5 13.5 0 16"/>',
478
+ releases: '<path d="M12 3v12M7 10l5 5 5-5M5 20h14"/>',
479
+ settings:
480
+ '<circle cx="12" cy="12" r="3"/><path d="M12 3v3M12 18v3M3 12h3M18 12h3M5.6 5.6l2.1 2.1M16.3 16.3l2.1 2.1M5.6 18.4l2.1-2.1M16.3 7.7l2.1-2.1"/>',
481
+ page: '<rect x="5" y="4" width="14" height="16" rx="2"/><path d="M9 9h6M9 13h6"/>',
482
+ edge: '<path d="M12 3l8 4v5c0 4.5-3.4 8.3-8 9-4.6-.7-8-4.5-8-9V7z"/><path d="M8 12h8M12 8v8"/>',
483
+ };
484
+ function icon(name) {
485
+ var d = ICONS[name] || (/^</.test(name || "") ? name : ICONS.page);
486
+ return '<svg viewBox="0 0 24 24" aria-hidden="true">' + d + "</svg>";
487
+ }
488
+
489
+ // ── registries ─────────────────────────────────────────────────────────
490
+ var GROUPS = [];
491
+ var NAV = [];
492
+ function upsert(list, item) {
493
+ for (var i = 0; i < list.length; i++)
494
+ if (list[i].id === item.id) {
495
+ list[i] = item;
496
+ return;
497
+ }
498
+ list.push(item);
499
+ }
500
+ function byOrder(a, b) {
501
+ return (a.order || 0) - (b.order || 0) || (a.id < b.id ? -1 : 1);
502
+ }
503
+ var ID = /^[a-z0-9][a-z0-9-]{0,39}$/;
504
+ /** A registration's `order`: a finite number, else 100 with a warning. */
505
+ function orderOf(kind, x) {
506
+ if (x.order === undefined) return 100;
507
+ if (typeof x.order === "number" && isFinite(x.order)) return x.order;
508
+ warn(
509
+ kind +
510
+ ' "' +
511
+ x.id +
512
+ '": order must be a number (got ' +
513
+ JSON.stringify(x.order) +
514
+ "); using 100",
515
+ );
516
+ return 100;
517
+ }
518
+ function warn(msg) {
519
+ if (typeof console !== "undefined" && console.warn)
520
+ console.warn("denShell: " + msg);
521
+ }
522
+ function hasGroup(id) {
523
+ for (var i = 0; i < GROUPS.length; i++)
524
+ if (GROUPS[i].id === id) return true;
525
+ return false;
526
+ }
527
+ function registerGroup(g) {
528
+ if (!g || !ID.test(g.id || "")) throw new Error("group needs an id");
529
+ upsert(GROUPS, {
530
+ id: g.id,
531
+ label: g.label || g.id,
532
+ order: orderOf("group", g),
533
+ from: g.from || null,
534
+ });
535
+ renderRail();
536
+ }
537
+ function registerNav(n) {
538
+ if (!n || !ID.test(n.id || "") || !n.href || !n.group)
539
+ throw new Error("a page needs an id, an href and a group");
540
+ // A page whose group isn't registered yet is kept, and shows when the
541
+ // group arrives (registerGroup re-renders). Registering a page before
542
+ // its group is a legitimate order (a queue, two scripts), so the
543
+ // warning waits until the queue has drained and a tick has passed.
544
+ upsert(NAV, {
545
+ id: n.id,
546
+ label: n.label || n.id,
547
+ icon: n.icon || "page",
548
+ group: n.group,
549
+ order: orderOf("nav", n),
550
+ href: n.href,
551
+ badge: typeof n.badge === "function" ? n.badge : null,
552
+ when: typeof n.when === "function" ? n.when : null,
553
+ // Other names the palette finds it by (R24a: "Local Dev").
554
+ keywords: Array.isArray(n.keywords) ? n.keywords.map(String) : [],
555
+ from: n.from || null,
556
+ });
557
+ checkOrphansSoon();
558
+ renderRail();
559
+ }
560
+
561
+ // ── orphans: a page or setting whose group or category never came ────
562
+ var settled = false;
563
+ var orphanTimer = null;
564
+ var warnedOrphan = {};
565
+ function checkOrphansSoon() {
566
+ if (!settled || orphanTimer) return;
567
+ orphanTimer = setTimeout(function () {
568
+ orphanTimer = null;
569
+ checkOrphans();
570
+ }, 0);
571
+ }
572
+ function checkOrphans() {
573
+ NAV.forEach(function (n) {
574
+ var key = "nav:" + n.id + ":" + n.group;
575
+ if (hasGroup(n.group) || warnedOrphan[key]) return;
576
+ warnedOrphan[key] = true;
577
+ warn(
578
+ 'nav "' +
579
+ n.id +
580
+ '": group "' +
581
+ n.group +
582
+ '" is not registered; the page shows once it is',
583
+ );
584
+ });
585
+ SETTING_ROWS.forEach(function (r) {
586
+ var key = "setting:" + r.id + ":" + r.category;
587
+ if (hasCategory(r.category) || warnedOrphan[key]) return;
588
+ warnedOrphan[key] = true;
589
+ warn(
590
+ 'setting "' +
591
+ r.id +
592
+ '": category "' +
593
+ r.category +
594
+ '" is not registered; the row shows once it is',
595
+ );
596
+ });
597
+ }
598
+
599
+ // ── SETTINGS (R21): what the Settings page draws, row by row ──────────
600
+ // A category: { id, label, order, description?, from? }. A row: { id,
601
+ // category, label, description, keywords, scope: user|workspace|
602
+ // distribution, control: { type, … }, read(ctx), write?(value, ctx),
603
+ // keys? } (keys: the /api/settings keys it stores, for the inventory).
604
+ var SETTING_CATS = [];
605
+ var SETTING_ROWS = [];
606
+ var SETTING_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
607
+ var SCOPES = ["user", "workspace", "distribution"];
608
+ function hasCategory(id) {
609
+ for (var i = 0; i < SETTING_CATS.length; i++)
610
+ if (SETTING_CATS[i].id === id) return true;
611
+ return false;
612
+ }
613
+ function settingsChanged() {
614
+ try {
615
+ window.dispatchEvent(new CustomEvent("den:settings"));
616
+ } catch (e) {
617
+ /* no CustomEvent: the page re-reads on its own */
618
+ }
619
+ }
620
+ function registerSettingsCategory(c) {
621
+ if (!c || !ID.test(c.id || ""))
622
+ throw new Error("a settings category needs an id");
623
+ upsert(SETTING_CATS, {
624
+ id: c.id,
625
+ label: c.label || c.id,
626
+ order: orderOf("settings category", c),
627
+ description: c.description || "",
628
+ from: c.from || null,
629
+ });
630
+ settingsChanged();
631
+ }
632
+ function registerSetting(r) {
633
+ if (!r || !SETTING_ID.test(r.id || "") || !r.category || !r.label)
634
+ throw new Error(
635
+ 'setting "' +
636
+ (r && r.id) +
637
+ '": a setting needs an id ([A-Za-z0-9._-]), a category and a label',
638
+ );
639
+ var scope = r.scope || "user";
640
+ if (SCOPES.indexOf(scope) < 0)
641
+ throw new Error(
642
+ 'setting "' + r.id + '": scope must be user, workspace or distribution',
643
+ );
644
+ if (typeof r.read !== "function")
645
+ throw new Error('setting "' + r.id + '" needs read(ctx)');
646
+ // The distribution's own config is tracked: shown, never written.
647
+ if (scope === "distribution" && r.write)
648
+ throw new Error(
649
+ 'setting "' +
650
+ r.id +
651
+ '": a distribution setting is read-only (no write)',
652
+ );
653
+ upsert(SETTING_ROWS, {
654
+ id: r.id,
655
+ category: r.category,
656
+ label: r.label,
657
+ description: r.description || "",
658
+ keywords: Array.isArray(r.keywords) ? r.keywords.slice() : [],
659
+ scope: scope,
660
+ control: r.control || { type: "readonly" },
661
+ read: r.read,
662
+ write: typeof r.write === "function" ? r.write : null,
663
+ keys: Array.isArray(r.keys) ? r.keys.slice() : [],
664
+ order: orderOf("setting", r),
665
+ from: r.from || null,
666
+ });
667
+ settingCommand(r);
668
+ checkOrphansSoon();
669
+ settingsChanged();
670
+ }
671
+
672
+ // ── the ⌘K palette's commands (R17, dev.20) ────────────────────────────
673
+ // Pages, services and apps are the palette's own (topbar.js); this holds
674
+ // what a page, a distribution or a plugin adds: actions, and the
675
+ // Settings registry's rows (kind "setting", dev.19).
676
+ var COMMANDS = [];
677
+ function registerCommand(c) {
678
+ if (!c || typeof c.id !== "string" || !c.id || typeof c.run !== "function")
679
+ throw new Error("a command needs an id and a run()");
680
+ if (c.label == null)
681
+ throw new Error('command "' + c.id + '" needs a label');
682
+ upsert(COMMANDS, {
683
+ id: c.id,
684
+ label: c.label,
685
+ keywords: Array.isArray(c.keywords)
686
+ ? c.keywords.slice()
687
+ : c.keywords
688
+ ? [String(c.keywords)]
689
+ : [],
690
+ kind: typeof c.kind === "string" && c.kind ? c.kind : "action",
691
+ run: c.run,
692
+ from: c.from || null,
693
+ });
694
+ }
695
+ function unregisterCommand(id) {
696
+ for (var i = COMMANDS.length - 1; i >= 0; i--)
697
+ if (COMMANDS[i].id === id) COMMANDS.splice(i, 1);
698
+ }
699
+ /**
700
+ * Every SETTINGS row is a palette row too (kind "setting"): Enter opens
701
+ * the Settings page searched for it, at its category
702
+ * (/settings?q=<label>#<category>).
703
+ */
704
+ function settingCommand(r) {
705
+ var cat = r.category;
706
+ // A built-in row's palette label is in the catalogs (set.<id>); a row
707
+ // a distribution or a plugin adds shows its own label. The Settings
708
+ // search (?q=) keeps the row's own label, which that page shows.
709
+ var key = "set." + r.id;
710
+ registerCommand({
711
+ id: "setting:" + r.id,
712
+ label:
713
+ has(MESSAGES.en, key) && MESSAGES.en[key] === r.label
714
+ ? function () {
715
+ return t(key);
716
+ }
717
+ : r.label,
718
+ keywords: (Array.isArray(r.keywords) ? r.keywords : []).concat([
719
+ cat,
720
+ "settings",
721
+ ]),
722
+ kind: "setting",
723
+ run: function () {
724
+ location.assign(
725
+ "/settings?q=" +
726
+ encodeURIComponent(r.label) +
727
+ "#" +
728
+ encodeURIComponent(cat),
729
+ );
730
+ },
731
+ from: r.from || null,
732
+ });
733
+ }
734
+ /** The registered commands, labels resolved (a label may be a function). */
735
+ function commands() {
736
+ return COMMANDS.map(function (c) {
737
+ var label = c.label;
738
+ if (typeof label === "function") {
739
+ try {
740
+ label = label();
741
+ } catch (e) {
742
+ label = c.id;
743
+ }
744
+ }
745
+ return {
746
+ id: c.id,
747
+ label: String(label),
748
+ keywords: c.keywords.slice(),
749
+ kind: c.kind,
750
+ run: c.run,
751
+ from: c.from,
752
+ };
753
+ });
754
+ }
755
+
756
+ // ── state: what badges, when() and the pinned dots read ───────────────
757
+ var LS = "__DEN_CLI__-shell";
758
+ var state = {
759
+ health: {},
760
+ down: 0,
761
+ openLogs: 0,
762
+ pins: [],
763
+ collapsed: false,
764
+ hidden: [],
765
+ order: [],
766
+ // The runs dock (R16a): open or collapsed to its bar, and its height
767
+ // (0: the prototype's 170px). User prefs, like the pins.
768
+ dock: false,
769
+ dockHeight: 0,
770
+ // Whether the edge is configured (the Edge page's when): null until
771
+ // known, the last answer cached so the rail doesn't jump.
772
+ edge: null,
773
+ };
774
+ try {
775
+ var cached = JSON.parse(localStorage.getItem(LS) || "{}");
776
+ if (typeof cached.edge === "boolean") state.edge = cached.edge;
777
+ if (Array.isArray(cached.pins)) state.pins = cached.pins;
778
+ state.collapsed = cached.collapsed === true;
779
+ if (Array.isArray(cached.hidden)) state.hidden = cached.hidden;
780
+ if (Array.isArray(cached.order)) state.order = cached.order;
781
+ state.dock = cached.dock === true;
782
+ if (typeof cached.dockHeight === "number")
783
+ state.dockHeight = cached.dockHeight;
784
+ } catch (e) {
785
+ /* no storage: the server's copy arrives below */
786
+ }
787
+ function remember() {
788
+ try {
789
+ localStorage.setItem(
790
+ LS,
791
+ JSON.stringify({
792
+ pins: state.pins,
793
+ collapsed: state.collapsed,
794
+ hidden: state.hidden,
795
+ order: state.order,
796
+ dock: state.dock,
797
+ dockHeight: state.dockHeight,
798
+ edge: state.edge,
799
+ }),
800
+ );
801
+ } catch (e) {
802
+ /* a convenience only */
803
+ }
804
+ }
805
+ function setState(partial) {
806
+ var before = state.pins.join("\n");
807
+ for (var k in partial)
808
+ if (Object.prototype.hasOwnProperty.call(partial, k))
809
+ state[k] = partial[k];
810
+ applyCollapsed();
811
+ renderRail();
812
+ if (state.pins.join("\n") !== before) pinsChanged();
813
+ }
814
+ /**
815
+ * Any pin change (the rail's ×, Delete, a page's toggle, the server's
816
+ * prefs arriving) is one event, `den:pins` on window, and every pin
817
+ * toggle on the page ([data-pin-toggle]) repaints from it.
818
+ */
819
+ function pinsChanged() {
820
+ if (typeof window.dispatchEvent !== "function") return;
821
+ var detail = { pins: state.pins.slice() };
822
+ var ev;
823
+ try {
824
+ ev = new CustomEvent("den:pins", { detail: detail });
825
+ } catch (e) {
826
+ return;
827
+ }
828
+ window.dispatchEvent(ev);
829
+ }
830
+ if (typeof window.addEventListener === "function")
831
+ window.addEventListener("den:pins", function () {
832
+ syncPinToggles(document);
833
+ });
834
+ /** A pin toggle's look for its id: ★ "Unpin x" pressed, or ☆ "Pin x". */
835
+ function syncPinToggles(root) {
836
+ if (!root || !root.querySelectorAll) return;
837
+ var btns = root.querySelectorAll("[data-pin-toggle]");
838
+ for (var i = 0; i < btns.length; i++) {
839
+ var b = btns[i];
840
+ var id = b.getAttribute("data-pin-toggle");
841
+ var on = isPinned(id);
842
+ var label = t(on ? "act.unpinX" : "act.pinX", { s: id });
843
+ b.textContent = on ? "★" : "☆";
844
+ b.setAttribute("aria-pressed", on ? "true" : "false");
845
+ b.setAttribute("aria-label", label);
846
+ b.title = label;
847
+ }
848
+ }
849
+ function saveShell(patch) {
850
+ remember();
851
+ if (typeof fetch !== "function") return Promise.resolve();
852
+ return fetch("/api/settings", {
853
+ method: "POST",
854
+ headers: { "Content-Type": "application/json" },
855
+ body: JSON.stringify({ shell: patch }),
856
+ })
857
+ .then(function (r) {
858
+ return r && r.ok !== false;
859
+ })
860
+ .catch(function () {
861
+ return false;
862
+ });
863
+ }
864
+ function isPinned(id) {
865
+ return state.pins.indexOf(id) >= 0;
866
+ }
867
+ function pin(id) {
868
+ if (isPinned(id)) return;
869
+ setState({ pins: state.pins.concat([id]) });
870
+ saveShell({ pins: state.pins });
871
+ toast(t("toast.pinned", { s: id }));
872
+ }
873
+ function unpin(id) {
874
+ if (!isPinned(id)) return;
875
+ setState({
876
+ pins: state.pins.filter(function (p) {
877
+ return p !== id;
878
+ }),
879
+ });
880
+ saveShell({ pins: state.pins });
881
+ toast(t("toast.unpinned", { s: id }));
882
+ }
883
+ function ids(list) {
884
+ return (Array.isArray(list) ? list : []).filter(function (x, i, all) {
885
+ return typeof x === "string" && all.indexOf(x) === i;
886
+ });
887
+ }
888
+ /** Navigation (R21): the pages the rail hides and their order. */
889
+ function setNavPrefs(prefs) {
890
+ var patch = {};
891
+ if (prefs && prefs.hidden !== undefined) patch.hidden = ids(prefs.hidden);
892
+ if (prefs && prefs.order !== undefined) patch.order = ids(prefs.order);
893
+ setState(patch);
894
+ return saveShell(patch);
895
+ }
896
+ /** The pins, in this order (the Settings page's Pinned row). */
897
+ function setPins(list) {
898
+ setState({ pins: ids(list) });
899
+ return saveShell({ pins: state.pins });
900
+ }
901
+ function setCollapsed(on) {
902
+ setState({ collapsed: !!on });
903
+ return saveShell({ collapsed: state.collapsed });
904
+ }
905
+ /** The runs dock open or collapsed to its bar (R16a), remembered. */
906
+ function setDock(on) {
907
+ dockOpen = !!on;
908
+ renderDock();
909
+ return rememberDock();
910
+ }
911
+ /** NAV pages in the rail's order: groups by order, then yours, then theirs. */
912
+ function orderedNav() {
913
+ var rank = function (id) {
914
+ var i = state.order.indexOf(id);
915
+ return i < 0 ? Infinity : i;
916
+ };
917
+ var gs = GROUPS.slice().sort(byOrder);
918
+ var out = [];
919
+ gs.forEach(function (g) {
920
+ NAV.filter(function (n) {
921
+ return n.group === g.id;
922
+ })
923
+ .sort(function (a, b) {
924
+ return rank(a.id) - rank(b.id) || byOrder(a, b);
925
+ })
926
+ .forEach(function (n) {
927
+ out.push(n);
928
+ });
929
+ });
930
+ return out;
931
+ }
932
+ var toastTimer = null;
933
+ function toast(msg) {
934
+ if (!document.body || !document.createElement) return;
935
+ var old = document.querySelector && document.querySelector(".den-toast");
936
+ if (old && old.remove) old.remove();
937
+ var el = document.createElement("div");
938
+ el.className = "den-toast";
939
+ if (el.setAttribute) el.setAttribute("role", "status");
940
+ el.textContent = msg;
941
+ if (document.body.appendChild) document.body.appendChild(el);
942
+ clearTimeout(toastTimer);
943
+ toastTimer = setTimeout(function () {
944
+ if (el.remove) el.remove();
945
+ }, 2200);
946
+ }
947
+
948
+ // ── menus (.pop, R18): the scope parts' popovers ─────────────────────
949
+ // A menu is { label, items: [{ heading } | { hr: true } | { label, tag?,
950
+ // checked?, href?, onSelect? }] }. Rows with `checked` are radios (✓ when
951
+ // checked). Keyboard: ↑/↓ (wrapping), Home and End move; Enter or Space
952
+ // picks; Escape and Tab close and give focus back to the button; a click
953
+ // outside closes.
954
+ var openPop = null;
955
+ function closeMenu(refocus) {
956
+ if (!openPop) return;
957
+ var o = openPop;
958
+ openPop = null;
959
+ if (o.el.remove) o.el.remove();
960
+ o.anchor.setAttribute("aria-expanded", "false");
961
+ // aria-controls names the menu only while it exists.
962
+ o.anchor.removeAttribute("aria-controls");
963
+ if (refocus && o.anchor.focus) o.anchor.focus();
964
+ }
965
+ function menuRows(pop) {
966
+ return Array.prototype.slice.call(pop.querySelectorAll(".den-mi"));
967
+ }
968
+ function openMenu(anchor, menu) {
969
+ var again = openPop && openPop.anchor === anchor;
970
+ closeMenu(false);
971
+ if (again || !menu) return;
972
+ var pop = document.createElement("div");
973
+ pop.className = "den-pop";
974
+ pop.tabIndex = -1;
975
+ pop.id = "den-pop-" + (anchor.dataset.scope || "menu");
976
+ pop.setAttribute("role", "menu");
977
+ pop.setAttribute("aria-label", menu.label);
978
+ var html = "";
979
+ var acts = [];
980
+ (menu.items || []).forEach(function (it) {
981
+ if (it.heading != null)
982
+ html +=
983
+ '<div class="den-ml" aria-hidden="true">' +
984
+ esc(it.heading) +
985
+ "</div>";
986
+ else if (it.hr) html += '<div class="den-hr" role="separator"></div>';
987
+ // A line of text (a notice), and an empty slot a later build fills
988
+ // (the user menu's Language, dev.23).
989
+ else if (it.note != null)
990
+ html += '<p class="den-mnote" role="status">' + esc(it.note) + "</p>";
991
+ else if (it.slot != null)
992
+ html +=
993
+ '<div class="den-mslot" data-slot="' +
994
+ esc(it.slot) +
995
+ '"' +
996
+ (it.hidden ? " hidden" : "") +
997
+ "></div>";
998
+ else {
999
+ var radio = it.checked != null;
1000
+ var tag = it.href ? "a" : "button";
1001
+ html +=
1002
+ "<" +
1003
+ tag +
1004
+ ' class="den-mi" tabindex="-1" data-i="' +
1005
+ acts.length +
1006
+ '" role="' +
1007
+ (radio ? "menuitemradio" : "menuitem") +
1008
+ '"' +
1009
+ (radio
1010
+ ? ' aria-checked="' + (it.checked ? "true" : "false") + '"'
1011
+ : "") +
1012
+ (it.href ? ' href="' + esc(it.href) + '"' : ' type="button"') +
1013
+ (it.title ? ' title="' + esc(it.title) + '"' : "") +
1014
+ // With a tag, the name is "default, focus" (the tag's text would
1015
+ // run into the label: "defaultfocus").
1016
+ (it.tag
1017
+ ? ' aria-label="' + esc(it.label + ", " + it.tag) + '"'
1018
+ : "") +
1019
+ '><span class="den-mi-l">' +
1020
+ esc(it.label) +
1021
+ "</span>" +
1022
+ (it.tag
1023
+ ? '<span class="den-mtag" aria-hidden="true">' +
1024
+ esc(it.tag) +
1025
+ "</span>"
1026
+ : "") +
1027
+ "</" +
1028
+ tag +
1029
+ ">";
1030
+ acts.push(it);
1031
+ }
1032
+ });
1033
+ pop.innerHTML = html;
1034
+ document.body.appendChild(pop);
1035
+ openPop = { el: pop, anchor: anchor };
1036
+ anchor.setAttribute("aria-expanded", "true");
1037
+ anchor.setAttribute("aria-controls", pop.id);
1038
+ var r = anchor.getBoundingClientRect();
1039
+ var w = pop.offsetWidth;
1040
+ pop.style.top = r.bottom + 6 + "px";
1041
+ pop.style.left =
1042
+ Math.max(8, Math.min(r.left, window.innerWidth - w - 8)) + "px";
1043
+ pop.addEventListener("click", function (e) {
1044
+ var row = e.target.closest ? e.target.closest(".den-mi") : null;
1045
+ if (!row) return;
1046
+ var it = acts[+row.getAttribute("data-i")];
1047
+ if (!it.href) e.preventDefault();
1048
+ closeMenu(true);
1049
+ if (it.onSelect) it.onSelect(it);
1050
+ });
1051
+ pop.addEventListener("keydown", function (e) {
1052
+ var rows = menuRows(pop);
1053
+ var i = rows.indexOf(document.activeElement);
1054
+ var next =
1055
+ e.key === "ArrowDown"
1056
+ ? (i + 1) % rows.length
1057
+ : e.key === "ArrowUp"
1058
+ ? (i - 1 + rows.length) % rows.length
1059
+ : e.key === "Home"
1060
+ ? 0
1061
+ : e.key === "End"
1062
+ ? rows.length - 1
1063
+ : -1;
1064
+ if (next >= 0 && rows.length) {
1065
+ e.preventDefault();
1066
+ rows[next].focus();
1067
+ } else if (e.key === "Escape") {
1068
+ e.preventDefault();
1069
+ e.stopPropagation();
1070
+ closeMenu(true);
1071
+ } else if (e.key === "Tab") closeMenu(true);
1072
+ else if (e.key === " " && rows[i] && rows[i].tagName === "A") {
1073
+ e.preventDefault();
1074
+ rows[i].click();
1075
+ }
1076
+ });
1077
+ var rows = menuRows(pop);
1078
+ var first = pop.querySelector('.den-mi[aria-checked="true"]') || rows[0];
1079
+ if (first) first.focus();
1080
+ else pop.focus();
1081
+ }
1082
+ document.addEventListener("mousedown", function (e) {
1083
+ if (!openPop) return;
1084
+ var tg = e.target;
1085
+ if (openPop.el.contains(tg) || openPop.anchor.contains(tg)) return;
1086
+ closeMenu(false);
1087
+ });
1088
+ document.addEventListener("keydown", function (e) {
1089
+ if (e.key === "Escape" && openPop) closeMenu(true);
1090
+ });
1091
+ window.addEventListener("resize", function () {
1092
+ closeMenu(false);
1093
+ });
1094
+
1095
+ /** A page's href from the NAV registry (Workspaces manages them). */
1096
+ function navHref(id) {
1097
+ for (var i = 0; i < NAV.length; i++)
1098
+ if (NAV[i].id === id) return NAV[i].href;
1099
+ return "/";
1100
+ }
1101
+ /** A scope part: <button class="den-ib"><k>Key</k> value ▾</button>. */
1102
+ function scopeButton(id, scope, key, value, hidden) {
1103
+ return (
1104
+ '<button type="button" id="' +
1105
+ id +
1106
+ '" class="den-ib den-scope-btn" data-scope="' +
1107
+ scope +
1108
+ '" aria-haspopup="menu" aria-expanded="false"' +
1109
+ (hidden ? " hidden" : "") +
1110
+ '><span class="den-k">' +
1111
+ esc(key) +
1112
+ '</span> <span class="den-v"><span class="den-vn">' +
1113
+ esc(value) +
1114
+ '</span> <span aria-hidden="true">▾</span></span></button>'
1115
+ );
1116
+ }
1117
+
1118
+ // ── Den's own groups and pages ─────────────────────────────────────────
1119
+ [
1120
+ { id: "develop", label: "g.develop", order: 10 },
1121
+ { id: "verify", label: "g.verify", order: 20 },
1122
+ { id: "ship", label: "g.ship", order: 30 },
1123
+ ].forEach(function (g) {
1124
+ GROUPS.push({ id: g.id, label: g.label, order: g.order, from: null });
1125
+ });
1126
+ [
1127
+ // Workspaces (R24a, 0.3.0-dev.35; was Local Dev): run your workspaces.
1128
+ // Its id and route stay (prefs and links keep working); the palette
1129
+ // still finds it as "Local Dev".
1130
+ {
1131
+ id: "local-dev",
1132
+ label: "n.workspaces",
1133
+ icon: "overview",
1134
+ group: "develop",
1135
+ order: 10,
1136
+ href: "/local-dev",
1137
+ keywords: ["Local Dev", "workspaces"],
1138
+ },
1139
+ // The Graph page (R22, 0.3.0-dev.32, Ward 2026-11-05): the graph alone,
1140
+ // full width, after Workspaces as the prototype orders them.
1141
+ {
1142
+ id: "graph",
1143
+ label: "n.graph",
1144
+ icon: "graph",
1145
+ group: "develop",
1146
+ order: 15,
1147
+ href: "/graph",
1148
+ },
1149
+ {
1150
+ id: "services",
1151
+ label: "n.services",
1152
+ icon: "services",
1153
+ group: "develop",
1154
+ order: 20,
1155
+ href: "/services",
1156
+ // R13: down services, from the health row's rollup, live.
1157
+ badge: function (s) {
1158
+ return s.down > 0
1159
+ ? { n: s.down, bad: true, label: t("b.down", { n: s.down }) }
1160
+ : null;
1161
+ },
1162
+ },
1163
+ {
1164
+ id: "logs",
1165
+ label: "n.logs",
1166
+ icon: "logs",
1167
+ group: "develop",
1168
+ order: 30,
1169
+ href: "/logs",
1170
+ badge: function (s) {
1171
+ // Open log streams (Den keeps no new-lines count yet).
1172
+ return s.openLogs > 0
1173
+ ? { n: s.openLogs, label: t("b.logs", { n: s.openLogs }) }
1174
+ : null;
1175
+ },
1176
+ },
1177
+ {
1178
+ id: "tests",
1179
+ label: "n.tests",
1180
+ icon: "tests",
1181
+ group: "verify",
1182
+ order: 10,
1183
+ href: "/tests",
1184
+ },
1185
+ {
1186
+ id: "environments",
1187
+ label: "n.environments",
1188
+ icon: "envs",
1189
+ group: "ship",
1190
+ order: 10,
1191
+ href: "/environments",
1192
+ },
1193
+ {
1194
+ id: "release",
1195
+ label: "n.releases",
1196
+ icon: "releases",
1197
+ group: "ship",
1198
+ order: 20,
1199
+ href: "/release",
1200
+ },
1201
+ // The Edge page (PL-SPEC §10): only once /api/edge says the edge is
1202
+ // configured (state.edge, from /api/edge?probe=1 below).
1203
+ {
1204
+ id: "edge",
1205
+ label: "n.edge",
1206
+ icon: "edge",
1207
+ group: "ship",
1208
+ order: 30,
1209
+ href: "/edge",
1210
+ when: function (s) {
1211
+ return s.edge === true;
1212
+ },
1213
+ },
1214
+ ].forEach(function (n) {
1215
+ NAV.push({
1216
+ id: n.id,
1217
+ label: n.label,
1218
+ icon: n.icon,
1219
+ group: n.group,
1220
+ order: n.order,
1221
+ href: n.href,
1222
+ badge: n.badge || null,
1223
+ when: n.when || null,
1224
+ keywords: n.keywords || [],
1225
+ from: null,
1226
+ });
1227
+ });
1228
+ // The Settings page (R21): every setting, from the SETTINGS registry.
1229
+ var SETTINGS = {
1230
+ id: "settings",
1231
+ label: "n.settings",
1232
+ icon: "settings",
1233
+ href: "/settings",
1234
+ };
1235
+
1236
+ // ── the frame ──────────────────────────────────────────────────────────
1237
+ // The top bar after the scope, in the prototype's order (dev.20): the ⌘K
1238
+ // search field (R5), then .chips: status (R6), Runs (R7), the lock (R8),
1239
+ // What's new (R9) and ◐ ▾ (R10, appearance.js fills #den-user). The
1240
+ // chips start neutral ("Checking…", "Runs", "Locked") and topbar.js
1241
+ // paints them: a cold load never shows a partial red.
1242
+ var chips =
1243
+ '<button type="button" class="den-search" id="den-search" aria-haspopup="dialog" aria-keyshortcuts="Meta+K Control+K" aria-label="' +
1244
+ esc(t("search.ph")) +
1245
+ '"><span class="den-search-ph">' +
1246
+ esc(t("search.ph")) +
1247
+ '</span><kbd aria-hidden="true">⌘K</kbd><span class="den-search-ic" aria-hidden="true">⌕</span></button>' +
1248
+ '<div class="den-chips">' +
1249
+ '<button type="button" class="den-chip" id="den-status" aria-haspopup="dialog" aria-expanded="false" aria-busy="true">' +
1250
+ '<span class="den-dot off" aria-hidden="true"></span><span class="den-n">' +
1251
+ esc(t("status.checking")) +
1252
+ "</span></button>" +
1253
+ '<button type="button" class="den-chip" id="den-runs">' +
1254
+ '<span class="den-dot off" aria-hidden="true"></span><span class="den-n den-lbl-l">' +
1255
+ esc(t("runs.title")) +
1256
+ "</span></button>" +
1257
+ '<button type="button" class="den-chip" id="den-lock" aria-haspopup="dialog" aria-expanded="false" title="' +
1258
+ esc(t("lock.title")) +
1259
+ '"><span class="den-lock-ic" aria-hidden="true">🔒</span><span class="den-lbl-l">' +
1260
+ esc(t("lock.locked")) +
1261
+ "</span></button>" +
1262
+ '<button class="den-ib v-pill" id="version-pill" onclick="openChangelog()" title="' +
1263
+ esc(t("v.title")) +
1264
+ '">' +
1265
+ '<span class="v-txt">v…</span><span class="v-pip" id="v-pip"></span></button>' +
1266
+ '<span class="den-user" id="den-user"></span>' +
1267
+ "</div>";
1268
+ host.className = "den-shell";
1269
+ host.innerHTML =
1270
+ '<a class="den-skip" href="#den-content">' +
1271
+ esc(t("skip")) +
1272
+ "</a>" +
1273
+ '<header class="den-top">' +
1274
+ '<button class="den-ib den-menu-btn" id="den-menu-btn" aria-label="' +
1275
+ esc(t("menu")) +
1276
+ '" aria-expanded="false" aria-controls="den-rail">☰</button>' +
1277
+ '<a class="den-brand" href="/" title="__DEN_TITLE__">' +
1278
+ '<span class="den-mark" id="den-mark" aria-hidden="true"></span>' +
1279
+ // The brand is a name, not a heading: the page's own h1 is its one.
1280
+ '<span class="den-title">__DEN_CLI__</span></a>' +
1281
+ // The prototype's scope breadcrumb (R4): / ORG <org> ▾ / WORKSPACE
1282
+ // <ws> ▾ / ENV <env> ▾. Each part is an .ib button with a .pop menu.
1283
+ // The org is a static label while there is one org (ORG-SPEC C4) and
1284
+ // hidden until header.js knows it; the workspace part is hidden until
1285
+ // header.js knows the workspaces. #env-sel stays as the env's value
1286
+ // (pages read it and listen for its change): the Env menu drives it.
1287
+ '<div class="den-scope" role="group" aria-label="' +
1288
+ esc(t("scope")) +
1289
+ '">' +
1290
+ '<span class="den-sep" aria-hidden="true">/</span>' +
1291
+ '<span id="org-crumb" class="den-ib den-org den-opt" hidden><span class="den-k">' +
1292
+ esc(t("k.org")) +
1293
+ '</span> <span class="den-org-name"></span></span>' +
1294
+ '<span class="den-sep den-org-sep" aria-hidden="true" hidden>/</span>' +
1295
+ scopeButton("ws-pill", "ws", t("k.ws"), "", true) +
1296
+ '<span class="den-sep den-ws-sep" aria-hidden="true" hidden>/</span>' +
1297
+ scopeButton("env-btn", "env", t("k.env"), "local", false) +
1298
+ '<select id="env-sel" hidden aria-hidden="true" tabindex="-1"><option value="local">local</option></select>' +
1299
+ "</div>" +
1300
+ chips +
1301
+ "</header>" +
1302
+ '<nav class="den-rail" id="den-rail" aria-label="' +
1303
+ esc(t("pages")) +
1304
+ '"></nav>';
1305
+
1306
+ var nav = host.querySelector(".den-rail");
1307
+
1308
+ // ── the scope parts (R4) ───────────────────────────────────────────────
1309
+ // Each part's menu comes from its model, set by header.js: setOrgs (the
1310
+ // org menu, only with two or more orgs), setWorkspaces (the view scope),
1311
+ // and the env from #env-sel's options. Choosing never starts, stops or
1312
+ // routes anything: the callbacks only store a preference.
1313
+ var scopeMenus = {
1314
+ org: null,
1315
+ ws: null,
1316
+ env: function () {
1317
+ var sel = document.getElementById("env-sel");
1318
+ if (!sel) return null;
1319
+ var items = [{ heading: t("k.env") }];
1320
+ Array.prototype.forEach.call(sel.options, function (o) {
1321
+ items.push({
1322
+ label: o.textContent,
1323
+ checked: o.value === sel.value,
1324
+ onSelect: function () {
1325
+ if (sel.value === o.value) return;
1326
+ sel.value = o.value;
1327
+ syncEnv();
1328
+ sel.dispatchEvent(new Event("change", { bubbles: true }));
1329
+ },
1330
+ });
1331
+ });
1332
+ return { label: t("k.env"), items: items };
1333
+ },
1334
+ };
1335
+ function scopeMenu(kind) {
1336
+ var m = scopeMenus[kind];
1337
+ return typeof m === "function" ? m() : m;
1338
+ }
1339
+ host.addEventListener("click", function (e) {
1340
+ var b = e.target.closest ? e.target.closest("[data-scope]") : null;
1341
+ if (!b || b.tagName !== "BUTTON") return;
1342
+ openMenu(b, scopeMenu(b.dataset.scope));
1343
+ });
1344
+ host.addEventListener("keydown", function (e) {
1345
+ var b = e.target.closest ? e.target.closest("[data-scope]") : null;
1346
+ if (!b || b.tagName !== "BUTTON") return;
1347
+ if (e.key === "ArrowDown" || e.key === "ArrowUp") {
1348
+ e.preventDefault();
1349
+ if (!openPop || openPop.anchor !== b)
1350
+ openMenu(b, scopeMenu(b.dataset.scope));
1351
+ if (e.key === "ArrowUp" && openPop) {
1352
+ var rows = menuRows(openPop.el);
1353
+ if (rows.length) rows[rows.length - 1].focus();
1354
+ }
1355
+ }
1356
+ });
1357
+ function setValue(btn, value, title) {
1358
+ if (!btn) return;
1359
+ btn.querySelector(".den-vn").textContent = value;
1360
+ if (title != null) btn.title = title;
1361
+ }
1362
+ /** The Env part's value: what #env-sel holds now. */
1363
+ function syncEnv() {
1364
+ var sel = document.getElementById("env-sel");
1365
+ if (sel) setValue(document.getElementById("env-btn"), sel.value);
1366
+ }
1367
+ var envSel = document.getElementById("env-sel");
1368
+ if (envSel) envSel.addEventListener("change", syncEnv);
1369
+
1370
+ /** One org: "/ ORG <title>" as a static label, no ▾ (C4). */
1371
+ function setOrg(org) {
1372
+ var el = document.getElementById("org-crumb");
1373
+ if (!el || !org || !org.title) return false;
1374
+ if (el.tagName === "BUTTON") {
1375
+ var span = document.createElement("span");
1376
+ span.id = "org-crumb";
1377
+ span.className = "den-ib den-org den-opt";
1378
+ span.innerHTML =
1379
+ '<span class="den-k">' +
1380
+ esc(t("k.org")) +
1381
+ '</span> <span class="den-org-name"></span>';
1382
+ el.replaceWith(span);
1383
+ el = span;
1384
+ }
1385
+ scopeMenus.org = null;
1386
+ el.querySelector(".den-org-name").textContent = org.title;
1387
+ el.title = t(org.id ? "org.titleId" : "org.title", {
1388
+ title: org.title,
1389
+ id: org.id,
1390
+ });
1391
+ el.hidden = false;
1392
+ var sep = el.nextElementSibling;
1393
+ if (sep && sep.classList.contains("den-org-sep")) sep.hidden = false;
1394
+ return true;
1395
+ }
1396
+ /**
1397
+ * The orgs that own workspaces (ORG-1b plugs /api/orgs in here). Fewer
1398
+ * than two: the static label (C4). Two or more: an Org ▾ menu; picking
1399
+ * one calls onSelect(id) and fires `den:org` on window. No backend.
1400
+ */
1401
+ function setOrgs(orgs, current, onSelect) {
1402
+ var list = (Array.isArray(orgs) ? orgs : []).filter(function (o) {
1403
+ return o && o.id;
1404
+ });
1405
+ var cur = null;
1406
+ for (var i = 0; i < list.length; i++)
1407
+ if (list[i].id === current) cur = list[i];
1408
+ cur = cur || list[0];
1409
+ var title = function (o) {
1410
+ return o.title || o.label || o.id;
1411
+ };
1412
+ if (list.length < 2)
1413
+ return cur ? setOrg({ id: cur.id, title: title(cur) }) : false;
1414
+ var el = document.getElementById("org-crumb");
1415
+ if (!el) return false;
1416
+ if (el.tagName !== "BUTTON") {
1417
+ var tmp = document.createElement("span");
1418
+ tmp.innerHTML = scopeButton("org-crumb", "org", t("k.org"), "", false);
1419
+ var btn = tmp.firstChild;
1420
+ btn.className += " den-org den-opt";
1421
+ btn.querySelector(".den-vn").className += " den-org-name";
1422
+ el.replaceWith(btn);
1423
+ el = btn;
1424
+ }
1425
+ el.hidden = false;
1426
+ setValue(
1427
+ el,
1428
+ title(cur),
1429
+ t("org.titleId", { title: title(cur), id: cur.id }),
1430
+ );
1431
+ var sep = el.nextElementSibling;
1432
+ if (sep && sep.classList.contains("den-org-sep")) sep.hidden = false;
1433
+ scopeMenus.org = {
1434
+ label: t("k.org"),
1435
+ items: [{ heading: t("k.org") }].concat(
1436
+ list.map(function (o) {
1437
+ return {
1438
+ label: title(o),
1439
+ checked: o.id === cur.id,
1440
+ tag:
1441
+ typeof o.workspaces === "number"
1442
+ ? t("org.n", { n: o.workspaces })
1443
+ : null,
1444
+ onSelect: function () {
1445
+ if (o.id === cur.id) return;
1446
+ setOrgs(list, o.id, onSelect);
1447
+ if (typeof onSelect === "function") onSelect(o.id);
1448
+ try {
1449
+ window.dispatchEvent(
1450
+ new CustomEvent("den:org", { detail: { id: o.id } }),
1451
+ );
1452
+ } catch (e) {
1453
+ /* no CustomEvent: the callback has it */
1454
+ }
1455
+ },
1456
+ };
1457
+ }),
1458
+ ),
1459
+ };
1460
+ return true;
1461
+ }
1462
+ /**
1463
+ * The Workspace part: { label, title, items: [{ value, label, checked,
1464
+ * focused, title? }], onSelect(value) }. "All active" and the active
1465
+ * workspaces, the focused one marked; then, apart, "Set <ws> as focus"
1466
+ * (model.focusable, model.onFocus(name)) and a link to manage them.
1467
+ */
1468
+ function setWorkspaces(model) {
1469
+ var btn = document.getElementById("ws-pill");
1470
+ if (!btn) return;
1471
+ var sep = btn.nextElementSibling;
1472
+ if (!model) {
1473
+ btn.hidden = true;
1474
+ if (sep) sep.hidden = true;
1475
+ scopeMenus.ws = null;
1476
+ return;
1477
+ }
1478
+ setValue(btn, model.label, model.title || null);
1479
+ btn.hidden = false;
1480
+ if (sep) sep.hidden = false;
1481
+ var items = [{ heading: t("k.ws") }];
1482
+ if (!model.items || !model.items.length)
1483
+ items.push({ label: t("ws.none"), href: navHref("local-dev") });
1484
+ (model.items || []).forEach(function (it) {
1485
+ items.push({
1486
+ label: it.label,
1487
+ checked: !!it.checked,
1488
+ tag: it.focused ? t("ws.focus") : null,
1489
+ title: it.title || null,
1490
+ onSelect: function () {
1491
+ if (!it.checked && typeof model.onSelect === "function")
1492
+ model.onSelect(it.value);
1493
+ },
1494
+ });
1495
+ });
1496
+ items.push({ hr: true });
1497
+ // Focus is a separate action (it writes workspaces.yml and retargets
1498
+ // the CLI); picking a view above never moves it.
1499
+ if (model.focusable && typeof model.onFocus === "function")
1500
+ items.push({
1501
+ label: t("ws.setFocus", { ws: model.focusable }),
1502
+ title: t("ws.setFocusTip"),
1503
+ onSelect: function () {
1504
+ model.onFocus(model.focusable);
1505
+ },
1506
+ });
1507
+ items.push({ label: t("ws.manage"), href: navHref("local-dev") });
1508
+ scopeMenus.ws = { label: t("k.ws"), items: items };
1509
+ }
1510
+
1511
+ function healthDot(id) {
1512
+ var s = state.health[id];
1513
+ var cls =
1514
+ s === "down"
1515
+ ? " bad"
1516
+ : s === "degraded"
1517
+ ? " warn"
1518
+ : s === "ok"
1519
+ ? ""
1520
+ : " off";
1521
+ return '<span class="den-dot' + cls + '" aria-hidden="true"></span>';
1522
+ }
1523
+ function pinHref(id) {
1524
+ var repo = id.split("/")[0];
1525
+ return "/services#repo-" + encodeURIComponent(repo);
1526
+ }
1527
+ function item(n, current, extra, attrs) {
1528
+ return (
1529
+ '<a class="den-ni" href="' +
1530
+ esc(n.href) +
1531
+ '" data-nav="' +
1532
+ esc(n.id) +
1533
+ '"' +
1534
+ (current ? ' aria-current="page"' : "") +
1535
+ (attrs || "") +
1536
+ ' title="' +
1537
+ esc(n.title) +
1538
+ '">' +
1539
+ (n.lead || icon(n.icon)) +
1540
+ '<span class="den-lbl' +
1541
+ (n.mono ? " den-mono" : "") +
1542
+ '">' +
1543
+ esc(n.title) +
1544
+ "</span>" +
1545
+ (extra || "") +
1546
+ "</a>"
1547
+ );
1548
+ }
1549
+ function group(id, label, from, body) {
1550
+ return (
1551
+ '<div class="den-rail-group" role="group" aria-labelledby="den-gh-' +
1552
+ esc(id) +
1553
+ '"' +
1554
+ (from ? ' aria-describedby="den-gd-' + esc(id) + '"' : "") +
1555
+ // The name is the label alone ("Acme", not "Acmeacme"): the tag is
1556
+ // in the description ("from acme").
1557
+ '><div class="den-gh"><span id="den-gh-' +
1558
+ esc(id) +
1559
+ '">' +
1560
+ esc(label) +
1561
+ "</span>" +
1562
+ (from
1563
+ ? '<span class="den-tagx" aria-hidden="true" title="' +
1564
+ esc(t("from", { d: from })) +
1565
+ '">' +
1566
+ esc(from) +
1567
+ "</span>"
1568
+ : "") +
1569
+ "</div>" +
1570
+ (from
1571
+ ? '<span class="den-sr" id="den-gd-' +
1572
+ esc(id) +
1573
+ '">' +
1574
+ esc(t("from", { d: from })) +
1575
+ "</span>"
1576
+ : "") +
1577
+ body +
1578
+ "</div>"
1579
+ );
1580
+ }
1581
+ /** The rail's markup from the registries and the state. */
1582
+ function railHtml() {
1583
+ var html = "";
1584
+ if (state.pins.length)
1585
+ html += group(
1586
+ "pinned",
1587
+ t("g.pinned"),
1588
+ null,
1589
+ state.pins
1590
+ .map(function (p) {
1591
+ // The row is the link (named by the id alone, Delete unpins
1592
+ // it); the × beside it is its own control, out of the tab
1593
+ // order, named "Unpin <id>", its glyph hidden from readers.
1594
+ return (
1595
+ '<div class="den-pin-row">' +
1596
+ item(
1597
+ {
1598
+ id: "pin-" + p,
1599
+ href: pinHref(p),
1600
+ title: p,
1601
+ lead: healthDot(p),
1602
+ mono: true,
1603
+ },
1604
+ false,
1605
+ // Holds the ×'s place in the row, as in the prototype.
1606
+ '<span class="den-pin-sp" aria-hidden="true">×</span>',
1607
+ ' data-pin="' + esc(p) + '" aria-keyshortcuts="Delete"',
1608
+ ) +
1609
+ '<button type="button" class="den-pin-x" tabindex="-1" data-unpin="' +
1610
+ esc(p) +
1611
+ '" aria-label="' +
1612
+ esc(t("act.unpinX", { s: p })) +
1613
+ '" title="' +
1614
+ esc(t("act.unpin")) +
1615
+ '"><span aria-hidden="true">×</span></button></div>'
1616
+ );
1617
+ })
1618
+ .join(""),
1619
+ );
1620
+ var ordered = orderedNav();
1621
+ GROUPS.slice()
1622
+ .sort(byOrder)
1623
+ .forEach(function (g) {
1624
+ var items = ordered.filter(function (n) {
1625
+ return (
1626
+ n.group === g.id &&
1627
+ state.hidden.indexOf(n.id) < 0 &&
1628
+ (!n.when || n.when(state))
1629
+ );
1630
+ });
1631
+ if (!items.length) return;
1632
+ html += group(
1633
+ g.id,
1634
+ t(g.label),
1635
+ g.from,
1636
+ items
1637
+ .map(function (n) {
1638
+ var b = n.badge ? n.badge(state) : null;
1639
+ var name = t(n.label);
1640
+ return item(
1641
+ { id: n.id, href: n.href, title: name, icon: n.icon },
1642
+ n.id === page,
1643
+ b
1644
+ ? '<span class="den-bdg' +
1645
+ (b.bad ? " bad" : "") +
1646
+ '" aria-hidden="true">' +
1647
+ esc(b.n) +
1648
+ "</span>"
1649
+ : "",
1650
+ b && b.label
1651
+ ? ' aria-label="' + esc(name + ", " + b.label) + '"'
1652
+ : "",
1653
+ );
1654
+ })
1655
+ .join(""),
1656
+ );
1657
+ });
1658
+ html += '<div class="den-rail-grow"></div>';
1659
+ html += item(
1660
+ {
1661
+ id: SETTINGS.id,
1662
+ href: SETTINGS.href,
1663
+ title: t(SETTINGS.label),
1664
+ icon: SETTINGS.icon,
1665
+ },
1666
+ page === SETTINGS.id,
1667
+ );
1668
+ html +=
1669
+ '<button class="den-ni den-collapse-btn" type="button" data-act="collapse" aria-expanded="' +
1670
+ (state.collapsed ? "false" : "true") +
1671
+ '" title="' +
1672
+ esc(state.collapsed ? t("expand") : t("collapse")) +
1673
+ '"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="' +
1674
+ (state.collapsed ? "M9 6l6 6-6 6" : "M15 6l-6 6 6 6") +
1675
+ '"/></svg><span class="den-lbl">' +
1676
+ esc(state.collapsed ? t("expand") : t("collapse")) +
1677
+ "</span></button>";
1678
+ return html;
1679
+ }
1680
+
1681
+ // One tab stop for the rail: the current page, else the first item.
1682
+ function rove() {
1683
+ var links = Array.prototype.slice.call(nav.querySelectorAll(".den-ni"));
1684
+ var stop = null;
1685
+ for (var i = 0; i < links.length; i++)
1686
+ if (
1687
+ links[i].getAttribute
1688
+ ? links[i].getAttribute("aria-current") === "page"
1689
+ : links[i].attrs && links[i].attrs["aria-current"] === "page"
1690
+ )
1691
+ stop = links[i];
1692
+ if (!stop) stop = links[0];
1693
+ for (var j = 0; j < links.length; j++)
1694
+ links[j].tabIndex = links[j] === stop ? 0 : -1;
1695
+ }
1696
+ var lastHtml = "";
1697
+ function renderRail() {
1698
+ if (!nav) return;
1699
+ var html = railHtml();
1700
+ if (html === lastHtml) return;
1701
+ // Keep keyboard focus on the same item across a re-render.
1702
+ var active = document.activeElement;
1703
+ var keep =
1704
+ active && nav.contains && nav.contains(active) && active.dataset
1705
+ ? active.dataset.nav
1706
+ : null;
1707
+ lastHtml = html;
1708
+ nav.innerHTML = html;
1709
+ rove();
1710
+ if (keep && nav.querySelector) {
1711
+ var again = nav.querySelector('[data-nav="' + keep + '"]');
1712
+ if (again) {
1713
+ again.tabIndex = 0;
1714
+ again.focus();
1715
+ }
1716
+ }
1717
+ }
1718
+
1719
+ /**
1720
+ * Unpin from the rail (Delete on a pinned row, or its ×) and keep focus
1721
+ * in the rail: the next pinned row, else the previous one, else the
1722
+ * first item. One rule for both paths.
1723
+ */
1724
+ function unpinFromRail(pinId) {
1725
+ var pins = Array.prototype.slice.call(
1726
+ nav.querySelectorAll(".den-ni[data-pin]"),
1727
+ );
1728
+ var at = -1;
1729
+ for (var k = 0; k < pins.length; k++)
1730
+ if (pins[k].dataset.pin === pinId) at = k;
1731
+ var to = at < 0 ? null : pins[at + 1] || pins[at - 1] || null;
1732
+ var toNav = to ? to.dataset.nav : null;
1733
+ unpin(pinId);
1734
+ var dest =
1735
+ (toNav && nav.querySelector('[data-nav="' + toNav + '"]')) ||
1736
+ nav.querySelector(".den-ni");
1737
+ if (!dest) return;
1738
+ Array.prototype.forEach.call(nav.querySelectorAll(".den-ni"), function (a) {
1739
+ a.tabIndex = a === dest ? 0 : -1;
1740
+ });
1741
+ dest.focus();
1742
+ }
1743
+
1744
+ // ↑/↓, Home and End move along the rail; Tab still leaves it (except
1745
+ // in the narrow drawer, a modal: Tab stays inside).
1746
+ nav.addEventListener("keydown", function (e) {
1747
+ if (e.key === "Tab" && drawerOpen()) {
1748
+ e.preventDefault();
1749
+ return;
1750
+ }
1751
+ var links = Array.prototype.slice.call(nav.querySelectorAll(".den-ni"));
1752
+ var i = links.indexOf(document.activeElement);
1753
+ if (i < 0) return;
1754
+ var pinId =
1755
+ links[i].dataset && links[i].dataset.pin ? links[i].dataset.pin : null;
1756
+ if (pinId && (e.key === "Delete" || e.key === "Backspace")) {
1757
+ e.preventDefault();
1758
+ unpinFromRail(pinId);
1759
+ return;
1760
+ }
1761
+ var next =
1762
+ e.key === "ArrowDown"
1763
+ ? (i + 1) % links.length
1764
+ : e.key === "ArrowUp"
1765
+ ? (i - 1 + links.length) % links.length
1766
+ : e.key === "Home"
1767
+ ? 0
1768
+ : e.key === "End"
1769
+ ? links.length - 1
1770
+ : -1;
1771
+ if (next < 0) return;
1772
+ e.preventDefault();
1773
+ links[i].tabIndex = -1;
1774
+ links[next].tabIndex = 0;
1775
+ links[next].focus();
1776
+ });
1777
+ nav.addEventListener("click", function (e) {
1778
+ var tgt = e.target;
1779
+ var x = tgt && tgt.closest ? tgt.closest("[data-unpin]") : null;
1780
+ if (x) {
1781
+ e.preventDefault();
1782
+ unpinFromRail(x.getAttribute("data-unpin"));
1783
+ return;
1784
+ }
1785
+ var c = tgt && tgt.closest ? tgt.closest('[data-act="collapse"]') : null;
1786
+ if (c) {
1787
+ setState({ collapsed: !state.collapsed });
1788
+ saveShell({ collapsed: state.collapsed });
1789
+ }
1790
+ });
1791
+
1792
+ function applyCollapsed() {
1793
+ if (document.body && document.body.classList)
1794
+ document.body.classList.toggle("den-rail-collapsed", !!state.collapsed);
1795
+ }
1796
+
1797
+ // ☰ opens the rail as a drawer below 720px: a modal overlay. Focus
1798
+ // moves into the rail (the current item); the top bar and the page are
1799
+ // inert behind a scrim; Escape or the scrim closes it and focus goes back
1800
+ // to ☰; Tab stays inside.
1801
+ var menu = host.querySelector(".den-menu-btn");
1802
+ var scrim = null;
1803
+ function drawerOpen() {
1804
+ return document.body.classList.contains("den-rail-open");
1805
+ }
1806
+ function setInert(on) {
1807
+ var els = [
1808
+ host.querySelector(".den-top"),
1809
+ document.getElementById("den-content"),
1810
+ document.getElementById("den-dock"),
1811
+ ];
1812
+ els.forEach(function (el) {
1813
+ if (!el) return;
1814
+ if (on) el.setAttribute("inert", "");
1815
+ else el.removeAttribute("inert");
1816
+ });
1817
+ }
1818
+ function openDrawer() {
1819
+ document.body.classList.add("den-rail-open");
1820
+ if (menu) menu.setAttribute("aria-expanded", "true");
1821
+ nav.setAttribute("role", "dialog");
1822
+ nav.setAttribute("aria-modal", "true");
1823
+ if (!scrim) {
1824
+ scrim = document.createElement("div");
1825
+ scrim.className = "den-scrim";
1826
+ scrim.addEventListener("click", function () {
1827
+ closeDrawer(true);
1828
+ });
1829
+ }
1830
+ document.body.appendChild(scrim);
1831
+ setInert(true);
1832
+ var stop =
1833
+ nav.querySelector('.den-ni[aria-current="page"]') ||
1834
+ nav.querySelector('.den-ni[tabindex="0"]') ||
1835
+ nav.querySelector(".den-ni");
1836
+ if (stop) stop.focus();
1837
+ }
1838
+ function closeDrawer(refocus) {
1839
+ if (!drawerOpen()) return;
1840
+ document.body.classList.remove("den-rail-open");
1841
+ if (menu) menu.setAttribute("aria-expanded", "false");
1842
+ nav.removeAttribute("role");
1843
+ nav.removeAttribute("aria-modal");
1844
+ if (scrim && scrim.remove) scrim.remove();
1845
+ setInert(false);
1846
+ if (refocus && menu) menu.focus();
1847
+ }
1848
+ if (menu)
1849
+ menu.addEventListener("click", function () {
1850
+ if (drawerOpen()) closeDrawer(true);
1851
+ else openDrawer();
1852
+ });
1853
+ document.addEventListener("keydown", function (e) {
1854
+ if (e.key === "Escape" && drawerOpen() && !openPop) {
1855
+ e.preventDefault();
1856
+ closeDrawer(true);
1857
+ }
1858
+ });
1859
+ // Wider than the drawer's step: the rail is a rail again.
1860
+ window.addEventListener("resize", function () {
1861
+ if (drawerOpen() && window.innerWidth > 720) closeDrawer(false);
1862
+ });
1863
+
1864
+ // ── the page header (R20, 0.3.0-dev.32) ──────────────────────────────
1865
+ // One h1 per page, the page's own (the brand is not a heading). A page
1866
+ // whose content has none gets the prototype's page header (.ph): the h1,
1867
+ // named from its rail entry or its <title> ("__DEN_TITLE__ · Services" →
1868
+ // Services), and a 13px muted sub line with the scope ("org / ws / env",
1869
+ // kept in step with the top bar's scope). A page adds its primary action
1870
+ // with denShell.pageHeader().act and its tabs below it with .tabs (both
1871
+ // empty until a page fills them). Settings and Edge draw their own.
1872
+ var ph = null;
1873
+ function scopeText() {
1874
+ var parts = [];
1875
+ var org = document.getElementById("org-crumb");
1876
+ if (org && !org.hidden) {
1877
+ var on =
1878
+ org.querySelector(".den-org-name") || org.querySelector(".den-vn");
1879
+ if (on && on.textContent) parts.push(on.textContent);
1880
+ }
1881
+ var over = (ph && ph.over) || {};
1882
+ [
1883
+ ["ws-pill", over.ws],
1884
+ ["env-btn", over.env],
1885
+ ].forEach(function (p) {
1886
+ if (p[1]) return parts.push(p[1]);
1887
+ var b = document.getElementById(p[0]);
1888
+ var v = b && !b.hidden ? b.querySelector(".den-vn") : null;
1889
+ if (v && v.textContent) parts.push(v.textContent);
1890
+ });
1891
+ return parts.join(" / ");
1892
+ }
1893
+ function paintPageSub() {
1894
+ if (!ph || ph.subFixed) return;
1895
+ var text = scopeText();
1896
+ if (ph.sub.textContent !== text) ph.sub.textContent = text;
1897
+ }
1898
+ function ensurePageHeader() {
1899
+ var main = document.getElementById("den-content");
1900
+ if (!main || main.querySelector("h1")) return;
1901
+ var label = "";
1902
+ NAV.forEach(function (n) {
1903
+ if (n.id === page) label = t(n.label);
1904
+ });
1905
+ if (!label) {
1906
+ var title = String(document.title || "");
1907
+ label = title.indexOf(" · ") >= 0 ? title.split(" · ").pop() : "";
1908
+ }
1909
+ if (!label) return;
1910
+ var el = document.createElement("div");
1911
+ el.className = "ph den-ph";
1912
+ el.innerHTML =
1913
+ '<div class="den-ph-h"><h1></h1><div class="sub"></div></div>' +
1914
+ '<div class="act" hidden></div>';
1915
+ el.querySelector("h1").textContent = label;
1916
+ main.insertBefore(el, main.firstChild);
1917
+ ph = {
1918
+ el: el,
1919
+ h1: el.querySelector("h1"),
1920
+ sub: el.querySelector(".sub"),
1921
+ act: el.querySelector(".act"),
1922
+ tabs: null,
1923
+ subFixed: false,
1924
+ over: null,
1925
+ };
1926
+ paintPageSub();
1927
+ var scope = host.querySelector(".den-scope");
1928
+ if (scope && typeof MutationObserver === "function")
1929
+ new MutationObserver(paintPageSub).observe(scope, {
1930
+ subtree: true,
1931
+ childList: true,
1932
+ characterData: true,
1933
+ attributes: true,
1934
+ attributeFilter: ["hidden"],
1935
+ });
1936
+ }
1937
+ // ── tabs: the WAI-ARIA tabs pattern, one helper (0.3.0-dev.33) ────────
1938
+ // Every page tablist (a page header's tabs) is wired here: one tab stop (the selected tab is tabindex 0, the rest
1939
+ // -1); ←/→ move and wrap, Home and End go to the ends, and each selects
1940
+ // as it moves (automatic activation); a click selects. Each tab has
1941
+ // aria-controls naming its role=tabpanel, which has aria-labelledby back
1942
+ // to the tab, and only the selected tab's panel shows.
1943
+ var tabSeq = 0;
1944
+ function tabIdOf(el, stem) {
1945
+ if (!el.id) el.id = "den-" + stem + "-" + ++tabSeq;
1946
+ return el.id;
1947
+ }
1948
+ /**
1949
+ * Wire `list` (an element with role=tablist) as tabs. opts:
1950
+ * panelOf(tab) its panel (default: the element its aria-controls
1951
+ * names)
1952
+ * onSelect(tab) after a tab is selected (by click or key)
1953
+ * selected the tab (or its index) to select first (default:
1954
+ * the one with aria-selected="true", else the first)
1955
+ * Returns { select(tabOrIndex, focus?), selected(), tabs() }. Wiring the
1956
+ * same list again replaces the old wiring.
1957
+ */
1958
+ function wireTabs(list, opts) {
1959
+ opts = opts || {};
1960
+ if (!list) return null;
1961
+ if (list._denTabs) list._denTabs.off();
1962
+ list.setAttribute("role", "tablist");
1963
+ function tabs() {
1964
+ return Array.prototype.slice.call(list.querySelectorAll('[role="tab"]'));
1965
+ }
1966
+ function panelOf(tab) {
1967
+ if (typeof opts.panelOf === "function") return opts.panelOf(tab);
1968
+ var id = tab.getAttribute("aria-controls");
1969
+ return id ? document.getElementById(id) : null;
1970
+ }
1971
+ tabs().forEach(function (tab) {
1972
+ var panel = panelOf(tab);
1973
+ tabIdOf(tab, "tab");
1974
+ if (!panel) return;
1975
+ tab.setAttribute("aria-controls", tabIdOf(panel, "tabpanel"));
1976
+ panel.setAttribute("role", "tabpanel");
1977
+ panel.setAttribute("aria-labelledby", tab.id);
1978
+ });
1979
+ function indexOf(t) {
1980
+ return typeof t === "number" ? t : tabs().indexOf(t);
1981
+ }
1982
+ function paint(i) {
1983
+ tabs().forEach(function (tab, j) {
1984
+ var on = j === i;
1985
+ tab.setAttribute("aria-selected", on ? "true" : "false");
1986
+ tab.setAttribute("tabindex", on ? "0" : "-1");
1987
+ tab.classList.toggle("active", on);
1988
+ var panel = panelOf(tab);
1989
+ if (panel) {
1990
+ panel.hidden = !on;
1991
+ panel.classList.toggle("active", on);
1992
+ }
1993
+ });
1994
+ }
1995
+ function select(t, focus) {
1996
+ var all = tabs();
1997
+ var i = indexOf(t);
1998
+ if (i < 0 || !all[i]) return;
1999
+ paint(i);
2000
+ if (focus) all[i].focus();
2001
+ if (typeof opts.onSelect === "function") opts.onSelect(all[i]);
2002
+ }
2003
+ function current() {
2004
+ var all = tabs();
2005
+ for (var i = 0; i < all.length; i++)
2006
+ if (all[i].getAttribute("aria-selected") === "true") return all[i];
2007
+ return all[0] || null;
2008
+ }
2009
+ function onClick(e) {
2010
+ var tab = e.target.closest && e.target.closest('[role="tab"]');
2011
+ if (!tab || !list.contains(tab)) return;
2012
+ e.preventDefault();
2013
+ select(tab, false);
2014
+ }
2015
+ function onKey(e) {
2016
+ var tab = e.target.closest && e.target.closest('[role="tab"]');
2017
+ if (!tab || !list.contains(tab)) return;
2018
+ var all = tabs();
2019
+ var i = all.indexOf(tab);
2020
+ var n = all.length;
2021
+ var next =
2022
+ e.key === "ArrowRight"
2023
+ ? (i + 1) % n
2024
+ : e.key === "ArrowLeft"
2025
+ ? (i - 1 + n) % n
2026
+ : e.key === "Home"
2027
+ ? 0
2028
+ : e.key === "End"
2029
+ ? n - 1
2030
+ : -1;
2031
+ if (next < 0) return;
2032
+ e.preventDefault();
2033
+ select(next, true);
2034
+ }
2035
+ list.addEventListener("click", onClick);
2036
+ list.addEventListener("keydown", onKey);
2037
+ var first =
2038
+ opts.selected != null ? indexOf(opts.selected) : indexOf(current());
2039
+ paint(first < 0 ? 0 : first);
2040
+ var api = {
2041
+ select: select,
2042
+ selected: current,
2043
+ tabs: tabs,
2044
+ off: function () {
2045
+ list.removeEventListener("click", onClick);
2046
+ list.removeEventListener("keydown", onKey);
2047
+ list._denTabs = null;
2048
+ },
2049
+ };
2050
+ list._denTabs = api;
2051
+ return api;
2052
+ }
2053
+ /**
2054
+ * The page header's tabs, made only when a page asks (no empty stub):
2055
+ * spec { label, tabs: [{ id, label, panel }], selected?, onSelect? }.
2056
+ * A .ptabs tablist below the header, wired by wireTabs; each panel is
2057
+ * the page's own element. Asked again, the old tablist is replaced.
2058
+ */
2059
+ function pageTabs(spec) {
2060
+ if (!ph || !spec || !Array.isArray(spec.tabs)) return null;
2061
+ if (ph.tabs) ph.tabs.remove();
2062
+ var list = document.createElement("div");
2063
+ list.className = "ptabs den-ptabs";
2064
+ list.setAttribute("role", "tablist");
2065
+ if (spec.label) list.setAttribute("aria-label", String(spec.label));
2066
+ var panels = [];
2067
+ var sel = 0;
2068
+ spec.tabs.forEach(function (d, i) {
2069
+ var b = document.createElement("button");
2070
+ b.type = "button";
2071
+ b.setAttribute("role", "tab");
2072
+ if (d.id) b.setAttribute("data-tab", String(d.id));
2073
+ b.textContent = String(d.label || d.id || "");
2074
+ list.appendChild(b);
2075
+ panels.push(d.panel || null);
2076
+ if (spec.selected != null && d.id === spec.selected) sel = i;
2077
+ });
2078
+ ph.el.parentNode.insertBefore(list, ph.el.nextSibling);
2079
+ ph.tabs = list;
2080
+ var all = Array.prototype.slice.call(list.querySelectorAll('[role="tab"]'));
2081
+ return wireTabs(list, {
2082
+ panelOf: function (tab) {
2083
+ return panels[all.indexOf(tab)] || null;
2084
+ },
2085
+ selected: sel,
2086
+ onSelect: function (tab) {
2087
+ if (typeof spec.onSelect === "function")
2088
+ spec.onSelect(tab.getAttribute("data-tab"), tab);
2089
+ },
2090
+ });
2091
+ }
2092
+ /**
2093
+ * The page header (R20): { el, h1, sub, act, tabs }, or null on a page
2094
+ * that draws its own. setSub(text) fixes the sub line (null: the scope
2095
+ * again); setScope({ ws?, env? }) keeps the scope line but names the
2096
+ * workspace or env the page shows (Workspaces and Graph show one
2097
+ * workspace, locally); a page shows .act by filling it and unhiding it,
2098
+ * and tabs with tabs(spec) (pageTabs: made only then).
2099
+ */
2100
+ function pageHeader() {
2101
+ if (!ph) return null;
2102
+ return {
2103
+ el: ph.el,
2104
+ h1: ph.h1,
2105
+ sub: ph.sub,
2106
+ act: ph.act,
2107
+ tabs: pageTabs,
2108
+ setSub: function (text) {
2109
+ ph.subFixed = text != null;
2110
+ if (text != null) ph.sub.textContent = String(text);
2111
+ else paintPageSub();
2112
+ },
2113
+ setScope: function (over) {
2114
+ ph.over = over || null;
2115
+ paintPageSub();
2116
+ },
2117
+ };
2118
+ }
2119
+ if (document.readyState === "loading")
2120
+ document.addEventListener("DOMContentLoaded", ensurePageHeader);
2121
+ else ensurePageHeader();
2122
+
2123
+ // Skip to content: the page's <main id="den-content">, its one main
2124
+ // landmark (each page wraps its content in it).
2125
+ var skip = host.querySelector(".den-skip");
2126
+ if (skip)
2127
+ skip.addEventListener("click", function (e) {
2128
+ var el = document.getElementById("den-content");
2129
+ if (!el) return;
2130
+ e.preventDefault();
2131
+ if (!el.hasAttribute("tabindex")) el.setAttribute("tabindex", "-1");
2132
+ el.focus();
2133
+ });
2134
+
2135
+ // The browser-tab icon is the active theme's (dev.22), set here and
2136
+ // nowhere else: no page links one. Until appearance.js knows the theme
2137
+ // (it calls setFavicon with the versioned URL for the mode it draws in,
2138
+ // again on every theme or mode change), the bare route serves the
2139
+ // active theme's, never cached.
2140
+ function setFavicon(href) {
2141
+ var links = document.querySelectorAll('link[rel~="icon"]');
2142
+ var link = document.getElementById("den-favicon");
2143
+ for (var i = 0; i < links.length; i++)
2144
+ if (links[i] !== link) links[i].remove();
2145
+ if (!link) {
2146
+ link = document.createElement("link");
2147
+ link.id = "den-favicon";
2148
+ link.rel = "icon";
2149
+ document.head.appendChild(link);
2150
+ }
2151
+ if (link.getAttribute("href") !== href) link.setAttribute("href", href);
2152
+ }
2153
+ setFavicon(
2154
+ "/theme-favicon?mode=" +
2155
+ (document.documentElement.dataset.mode === "light" ? "light" : "dark"),
2156
+ );
2157
+
2158
+ document.body.classList.add("den-has-shell");
2159
+ applyCollapsed();
2160
+ renderRail();
2161
+
2162
+ // ── the runs dock (R16, 0.3.0-dev.30) ──────────────────────────────────
2163
+ // One output dock for every page, at the foot of the content column
2164
+ // (the prototype's grid area `dock`). A bar: "Runs", the run tabs (mono
2165
+ // 12, a dot that pulses while running) and Show/Hide runs; open, a 170px
2166
+ // <pre> with the selected run's output. It replaces the pages' own output
2167
+ // panes: every run a page starts is a tab here, through denShell.runs.
2168
+ //
2169
+ // runs.add(id, label, opts) a tab (no change to its state if it
2170
+ // exists, beyond what opts set)
2171
+ // runs.start(id, label, opts) add + running + selected; the dock
2172
+ // stays as it is unless opts.open: true
2173
+ // (R16a); opts.select: false keeps the
2174
+ // selection
2175
+ // runs.append(id, text, cls?, { key }?) one line (cls: cmd, err, warn,
2176
+ // done-ok, done-fail, ok, phase); a `key`
2177
+ // replaces that line in place (progress)
2178
+ // runs.finish(id, { ok }) stops the pulse; ok false is an error.
2179
+ // A run with opts.doneLine (the default
2180
+ // for new callers) gets "✓ done in {s}s"
2181
+ // runs.set(id, { label, running, error, actions })
2182
+ // runs.select(id, { open }) (opens only with open: true),
2183
+ // runs.clear(id), runs.remove(id)
2184
+ // runs.show(), hide(), toggle(), isOpen(), has(id), list(), selected(),
2185
+ // runs.text(id), running(), onChange(fn)
2186
+ // runs.scope({ ws } | null) only that workspace's runs show, with
2187
+ // an "All runs" toggle in the bar (R24e)
2188
+ //
2189
+ // opts: closable (default true: a × on the tab), onClose(id) (the page
2190
+ // decides; else the tab goes), onSelect(id), onClear(id), max (lines
2191
+ // kept, 0 = all), actions [{ id, label, title?, run(id) }] (buttons in
2192
+ // the bar for the selected run, e.g. Stop), doneLine, ws (the workspace
2193
+ // or workspaces it ran in, added to the ones it has).
2194
+ //
2195
+ // The bar carries Copy and Clear for the selected run (the panes had
2196
+ // them). R16a (Ward QA 2026-11-05): the dock starts collapsed to its bar.
2197
+ // Open/closed and its height are user prefs (/api/settings `shell.dock`,
2198
+ // `shell.dockHeight`, mirrored with the rest of the shell's prefs for the
2199
+ // first paint), so they survive navigation and reloads, per user. A run
2200
+ // starting never opens it: its tab appears and the Runs chip pulses. Only
2201
+ // the engineer opens it: Show runs, the Runs chip, a tab click, or a page
2202
+ // action whose whole point is output (runs.start/select with open: true).
2203
+ // Drag the dock's top edge to resize it, 60px to half the window.
2204
+ var SS_LAST_RUN = "__DEN_CLI__-last-run";
2205
+ var RUNS = [];
2206
+ var runSel = null;
2207
+ var DOCK_H = 170;
2208
+ var dockOpen = state.dock === true;
2209
+ var dockH = state.dockHeight >= 60 ? state.dockHeight : DOCK_H;
2210
+ var runSeqId = 0;
2211
+ var runListeners = [];
2212
+ /** At most half the window (R16a); at least 60px. */
2213
+ function dockCap(h) {
2214
+ var max = Math.max(60, Math.floor((window.innerHeight || 800) * 0.5));
2215
+ return Math.round(Math.max(60, Math.min(h, max)));
2216
+ }
2217
+ var dock = document.createElement("div");
2218
+ dock.className = "den-dock";
2219
+ dock.id = "den-dock";
2220
+ if (dock.setAttribute) {
2221
+ dock.setAttribute("role", "region");
2222
+ dock.setAttribute("aria-label", t("runs.title"));
2223
+ }
2224
+ function placeDock() {
2225
+ if (document.body && document.body.appendChild)
2226
+ document.body.appendChild(dock);
2227
+ }
2228
+ placeDock();
2229
+ // After the page's content (the foot of the column), once it's parsed.
2230
+ if (document.readyState === "loading")
2231
+ document.addEventListener("DOMContentLoaded", placeDock);
2232
+
2233
+ function runById(id) {
2234
+ for (var i = 0; i < RUNS.length; i++) if (RUNS[i].id === id) return RUNS[i];
2235
+ return null;
2236
+ }
2237
+ // R24e (0.3.0-dev.35): a page showing one workspace scopes the dock to
2238
+ // it (runs.scope({ ws })): only runs started in that workspace show, and
2239
+ // "All runs" (aria-pressed) shows every one. A run's workspaces are what
2240
+ // its page tagged it with (opts.ws); a run with none always shows.
2241
+ var runScope = null;
2242
+ var runAll = false;
2243
+ function inScope(r) {
2244
+ return (
2245
+ !runScope || runAll || !r.ws.length || r.ws.indexOf(runScope.ws) >= 0
2246
+ );
2247
+ }
2248
+ function visibleRuns() {
2249
+ return RUNS.filter(inScope);
2250
+ }
2251
+ function selectedRun() {
2252
+ var shown = visibleRuns();
2253
+ var r = runById(runSel);
2254
+ return (r && inScope(r) ? r : null) || shown[shown.length - 1] || null;
2255
+ }
2256
+ function runsChanged() {
2257
+ runListeners.forEach(function (fn) {
2258
+ try {
2259
+ fn();
2260
+ } catch (e) {
2261
+ /* a listener's problem */
2262
+ }
2263
+ });
2264
+ try {
2265
+ window.dispatchEvent(new CustomEvent("den:runs"));
2266
+ } catch (e) {
2267
+ /* no CustomEvent */
2268
+ }
2269
+ }
2270
+ /** Open/closed changed by the engineer: the pref follows. */
2271
+ function rememberDock() {
2272
+ if (state.dock === dockOpen) return Promise.resolve(true);
2273
+ state.dock = dockOpen;
2274
+ return saveShell({ dock: dockOpen });
2275
+ }
2276
+ /** The server's prefs arrived: the dock follows them. */
2277
+ function dockFromPrefs() {
2278
+ var h = state.dockHeight >= 60 ? state.dockHeight : DOCK_H;
2279
+ if (dockOpen === (state.dock === true) && h === dockH) return;
2280
+ dockOpen = state.dock === true;
2281
+ dockH = h;
2282
+ var out = outEl();
2283
+ if (out) out.style.height = dockCap(dockH) + "px";
2284
+ renderDock();
2285
+ }
2286
+ function secs(ms) {
2287
+ return num(Math.max(0, ms) / 1000, {
2288
+ minimumFractionDigits: 1,
2289
+ maximumFractionDigits: 1,
2290
+ });
2291
+ }
2292
+ function dotClass(r) {
2293
+ if (r.running) return "run";
2294
+ if (r.error) return "bad";
2295
+ if (r.done) return "";
2296
+ return "off";
2297
+ }
2298
+ function runTitle(r) {
2299
+ if (r.running) return r.label;
2300
+ if (r.done && r.endedAt)
2301
+ return (
2302
+ r.label +
2303
+ " · " +
2304
+ t(r.error ? "dock.failed" : "dock.done", {
2305
+ s: secs(r.endedAt - r.startedAt),
2306
+ })
2307
+ );
2308
+ return r.label;
2309
+ }
2310
+ function lastRunHere() {
2311
+ try {
2312
+ var l = JSON.parse(sessionStorage.getItem(SS_LAST_RUN) || "null");
2313
+ var here = location.pathname + location.search;
2314
+ return l && l.page && l.page !== here ? l : null;
2315
+ } catch (e) {
2316
+ return null;
2317
+ }
2318
+ }
2319
+
2320
+ // The bar: redrawn on any run's state change (not on every line).
2321
+ function barHtml() {
2322
+ var sel = selectedRun();
2323
+ var shown = visibleRuns();
2324
+ var tabs = shown.length
2325
+ ? shown
2326
+ .map(function (r) {
2327
+ var on = sel && sel.id === r.id;
2328
+ return (
2329
+ '<span class="den-rtab-w">' +
2330
+ '<button type="button" class="den-rtab' +
2331
+ (r.closable ? " den-x" : "") +
2332
+ '" role="tab" id="den-rtab-' +
2333
+ r.n +
2334
+ '" data-rsel="' +
2335
+ r.n +
2336
+ '" aria-selected="' +
2337
+ (on ? "true" : "false") +
2338
+ '" aria-controls="den-dock-out" tabindex="' +
2339
+ (on ? "0" : "-1") +
2340
+ '" title="' +
2341
+ esc(runTitle(r)) +
2342
+ '"' +
2343
+ (r.closable ? ' aria-keyshortcuts="Delete"' : "") +
2344
+ '><span class="den-dot ' +
2345
+ dotClass(r) +
2346
+ '" aria-hidden="true"></span>' +
2347
+ esc(r.label) +
2348
+ (r.running
2349
+ ? '<span class="den-sr">, ' +
2350
+ esc(t("runs.n", { n: 1 })) +
2351
+ "</span>"
2352
+ : r.error
2353
+ ? '<span class="den-sr">, ' + esc(t("dock.err")) + "</span>"
2354
+ : "") +
2355
+ "</button>" +
2356
+ (r.closable
2357
+ ? '<button type="button" class="den-rtab-x" tabindex="-1" data-rclose="' +
2358
+ r.n +
2359
+ '" aria-label="' +
2360
+ esc(t("dock.closeRun", { s: r.label })) +
2361
+ '" title="' +
2362
+ esc(t("dock.closeRun", { s: r.label })) +
2363
+ '"><span aria-hidden="true">×</span></button>'
2364
+ : "") +
2365
+ "</span>"
2366
+ );
2367
+ })
2368
+ .join("")
2369
+ : "<span>" + esc(t("runs.none")) + "</span>";
2370
+ var acts = "";
2371
+ if (dockOpen && sel) {
2372
+ (sel.actions || []).forEach(function (a, i) {
2373
+ acts +=
2374
+ '<button type="button" class="den-ib" data-ract="' +
2375
+ i +
2376
+ '"' +
2377
+ (a.title ? ' title="' + esc(a.title) + '"' : "") +
2378
+ ">" +
2379
+ esc(a.label) +
2380
+ "</button>";
2381
+ });
2382
+ acts +=
2383
+ '<button type="button" class="den-ib" data-dock="copy">' +
2384
+ esc(t("dock.copy")) +
2385
+ "</button>" +
2386
+ '<button type="button" class="den-ib" data-dock="clear">' +
2387
+ esc(t("dock.clear")) +
2388
+ "</button>";
2389
+ }
2390
+ return (
2391
+ "<b>" +
2392
+ esc(t("runs.title")) +
2393
+ "</b>" +
2394
+ // A tablist only while it holds tabs: an empty one ("No runs") is
2395
+ // a tablist with no tab, which screen readers announce as broken
2396
+ // (0.3.0-dev.34, a consumer's finding). The bar redraws on any run's
2397
+ // state change, so the first tab brings the role back.
2398
+ '<div class="den-runs-l"' +
2399
+ (shown.length
2400
+ ? ' role="tablist" aria-label="' + esc(t("runs.title")) + '"'
2401
+ : "") +
2402
+ ">" +
2403
+ tabs +
2404
+ "</div>" +
2405
+ (acts ? '<span class="den-dock-acts">' + acts + "</span>" : "") +
2406
+ // (No runs at all: nothing to filter, no toggle.)
2407
+ (runScope && RUNS.length
2408
+ ? '<button type="button" class="den-ib" data-dock="all" aria-pressed="' +
2409
+ (runAll ? "true" : "false") +
2410
+ '" title="' +
2411
+ esc(t(runAll ? "runs.allOn" : "runs.allTip", { ws: runScope.ws })) +
2412
+ '">' +
2413
+ esc(t("runs.all")) +
2414
+ "</button>"
2415
+ : "") +
2416
+ '<button type="button" class="den-ib" data-act="dock" aria-expanded="' +
2417
+ (dockOpen ? "true" : "false") +
2418
+ '" aria-controls="den-dock-out">' +
2419
+ esc(dockOpen ? t("dock.close") + " ▾" : t("dock.show") + " ▴") +
2420
+ "</button>"
2421
+ );
2422
+ }
2423
+ var shownRun = null;
2424
+ var pending = [];
2425
+ var rafId = null;
2426
+ function lineSpan(l) {
2427
+ var span = document.createElement("span");
2428
+ if (l.cls) span.className = l.cls;
2429
+ span.textContent = l.text + "\n";
2430
+ l.el = span;
2431
+ return span;
2432
+ }
2433
+ function outEl() {
2434
+ return dock.querySelector ? dock.querySelector("#den-dock-out") : null;
2435
+ }
2436
+ function fillOut() {
2437
+ var out = outEl();
2438
+ if (!out) return;
2439
+ pending = [];
2440
+ if (rafId !== null) {
2441
+ cancelAnimationFrame(rafId);
2442
+ rafId = null;
2443
+ }
2444
+ var sel = selectedRun();
2445
+ shownRun = sel;
2446
+ out.innerHTML = "";
2447
+ if (sel) out.setAttribute("aria-labelledby", "den-rtab-" + sel.n);
2448
+ else out.removeAttribute("aria-labelledby");
2449
+ if (!sel) {
2450
+ out.textContent = t("runs.empty");
2451
+ var last = lastRunHere();
2452
+ if (last) {
2453
+ out.appendChild(document.createTextNode(" "));
2454
+ var a = document.createElement("a");
2455
+ a.href = last.page;
2456
+ a.textContent = t("dock.last", { s: last.label });
2457
+ out.appendChild(a);
2458
+ }
2459
+ return;
2460
+ }
2461
+ var frag = document.createDocumentFragment();
2462
+ sel.lines.forEach(function (l) {
2463
+ frag.appendChild(lineSpan(l));
2464
+ });
2465
+ out.appendChild(frag);
2466
+ if (sel.autoScroll) out.scrollTop = out.scrollHeight;
2467
+ }
2468
+ function flush() {
2469
+ rafId = null;
2470
+ var out = outEl();
2471
+ var batch = pending;
2472
+ pending = [];
2473
+ if (!out || !shownRun || !batch.length) return;
2474
+ var frag = document.createDocumentFragment();
2475
+ batch.forEach(function (l) {
2476
+ if (shownRun.lines.indexOf(l) >= 0) frag.appendChild(lineSpan(l));
2477
+ });
2478
+ out.appendChild(frag);
2479
+ if (shownRun.max)
2480
+ while (out.childElementCount > shownRun.max)
2481
+ out.removeChild(out.firstChild);
2482
+ if (shownRun.autoScroll) out.scrollTop = out.scrollHeight;
2483
+ }
2484
+ function renderDock() {
2485
+ if (!dock.querySelector) return;
2486
+ var bar = dock.querySelector(".den-bar");
2487
+ if (!bar) {
2488
+ dock.innerHTML =
2489
+ '<div class="den-dock-grip" aria-hidden="true" title="' +
2490
+ esc(t("dock.resize")) +
2491
+ '"></div><div class="den-bar"></div>';
2492
+ bar = dock.querySelector(".den-bar");
2493
+ }
2494
+ var focusSel =
2495
+ document.activeElement && dock.contains(document.activeElement)
2496
+ ? document.activeElement.getAttribute("data-rsel") ||
2497
+ document.activeElement.getAttribute("data-act") ||
2498
+ document.activeElement.getAttribute("data-dock")
2499
+ : null;
2500
+ bar.innerHTML = barHtml();
2501
+ dock.classList.toggle("open", dockOpen);
2502
+ var out = outEl();
2503
+ if (dockOpen && !out) {
2504
+ out = document.createElement("pre");
2505
+ out.id = "den-dock-out";
2506
+ out.setAttribute("role", "tabpanel");
2507
+ out.tabIndex = 0;
2508
+ out.style.height = dockCap(dockH) + "px";
2509
+ out.addEventListener("scroll", function () {
2510
+ if (!shownRun) return;
2511
+ shownRun.autoScroll =
2512
+ out.scrollHeight - out.scrollTop <= out.clientHeight + 20;
2513
+ });
2514
+ dock.appendChild(out);
2515
+ fillOut();
2516
+ } else if (!dockOpen && out) {
2517
+ out.remove();
2518
+ shownRun = null;
2519
+ } else if (out && selectedRun() !== shownRun) fillOut();
2520
+ // Keep focus on what had it (a redraw replaces the bar's buttons).
2521
+ if (focusSel) {
2522
+ var back =
2523
+ bar.querySelector('[data-rsel="' + focusSel + '"]') ||
2524
+ bar.querySelector('[data-act="' + focusSel + '"]') ||
2525
+ bar.querySelector('[data-dock="' + focusSel + '"]');
2526
+ if (back) back.focus();
2527
+ }
2528
+ var on = bar.querySelector('.den-rtab[aria-selected="true"]');
2529
+ if (on && on.scrollIntoView)
2530
+ on.scrollIntoView({ block: "nearest", inline: "nearest" });
2531
+ var chip = document.getElementById("den-runs");
2532
+ if (chip) {
2533
+ chip.setAttribute("aria-expanded", dockOpen ? "true" : "false");
2534
+ chip.setAttribute("aria-controls", "den-dock");
2535
+ }
2536
+ }
2537
+
2538
+ function addRun(id, label, opts) {
2539
+ id = String(id);
2540
+ opts = opts || {};
2541
+ var r = runById(id);
2542
+ if (!r) {
2543
+ r = {
2544
+ id: id,
2545
+ n: ++runSeqId,
2546
+ label: String(label == null ? id : label),
2547
+ lines: [],
2548
+ running: false,
2549
+ done: false,
2550
+ error: false,
2551
+ startedAt: Date.now(),
2552
+ endedAt: null,
2553
+ autoScroll: true,
2554
+ closable: true,
2555
+ doneLine: true,
2556
+ max: 0,
2557
+ actions: [],
2558
+ keyed: {},
2559
+ ws: [],
2560
+ ranAt: null,
2561
+ };
2562
+ RUNS.push(r);
2563
+ } else if (label != null) r.label = String(label);
2564
+ // The workspaces it ran in (R24e): every one a page tagged it with.
2565
+ [].concat(opts.ws == null ? [] : opts.ws).forEach(function (w) {
2566
+ if (w && r.ws.indexOf(String(w)) < 0) r.ws.push(String(w));
2567
+ });
2568
+ [
2569
+ "closable",
2570
+ "doneLine",
2571
+ "max",
2572
+ "onClose",
2573
+ "onSelect",
2574
+ "onClear",
2575
+ "actions",
2576
+ ].forEach(function (k) {
2577
+ if (opts[k] !== undefined) r[k] = opts[k];
2578
+ });
2579
+ if (opts.running !== undefined) markRunning(r, !!opts.running);
2580
+ if (opts.error !== undefined) r.error = !!opts.error;
2581
+ return r;
2582
+ }
2583
+ /** Running on or off; when it last ran and ended (a page's last-run column). */
2584
+ function markRunning(r, on) {
2585
+ if (on && !r.running) {
2586
+ r.ranAt = Date.now();
2587
+ r.endedAt = null;
2588
+ } else if (!on && r.running) r.endedAt = Date.now();
2589
+ r.running = on;
2590
+ }
2591
+ var runsApi = {
2592
+ add: function (id, label, opts) {
2593
+ var r = addRun(id, label, opts);
2594
+ if (opts && opts.select) runSel = r.id;
2595
+ if (opts && opts.open === true) {
2596
+ dockOpen = true;
2597
+ rememberDock();
2598
+ }
2599
+ renderDock();
2600
+ runsChanged();
2601
+ return r.id;
2602
+ },
2603
+ start: function (id, label, opts) {
2604
+ opts = opts || {};
2605
+ var r = addRun(id, label, opts);
2606
+ r.running = opts.running !== false;
2607
+ r.done = false;
2608
+ r.error = false;
2609
+ r.startedAt = Date.now();
2610
+ r.ranAt = r.startedAt;
2611
+ r.endedAt = null;
2612
+ if (opts.clear) {
2613
+ r.lines = [];
2614
+ r.keyed = {};
2615
+ if (shownRun === r) shownRun = null;
2616
+ }
2617
+ if (opts.select !== false) runSel = r.id;
2618
+ // R16a: a run starting never opens the dock; open: true is for an
2619
+ // action whose whole point is output (e.g. a service's Logs).
2620
+ if (opts.open === true) {
2621
+ dockOpen = true;
2622
+ rememberDock();
2623
+ }
2624
+ renderDock();
2625
+ runsChanged();
2626
+ return r.id;
2627
+ },
2628
+ append: function (id, text, cls, o) {
2629
+ var r = runById(String(id));
2630
+ if (!r) return;
2631
+ var key = o && o.key;
2632
+ if (key != null && r.keyed[key] && r.lines.indexOf(r.keyed[key]) >= 0) {
2633
+ var l0 = r.keyed[key];
2634
+ l0.text = String(text);
2635
+ if (l0.el && shownRun === r) l0.el.textContent = l0.text + "\n";
2636
+ if (shownRun === r && r.autoScroll) {
2637
+ var o0 = outEl();
2638
+ if (o0) o0.scrollTop = o0.scrollHeight;
2639
+ }
2640
+ return;
2641
+ }
2642
+ var l = { text: String(text), cls: cls || "" };
2643
+ if (key != null) r.keyed[key] = l;
2644
+ r.lines.push(l);
2645
+ if (r.max && r.lines.length > r.max)
2646
+ r.lines.splice(0, r.lines.length - r.max);
2647
+ if (shownRun === r) {
2648
+ pending.push(l);
2649
+ if (rafId === null) rafId = requestAnimationFrame(flush);
2650
+ }
2651
+ },
2652
+ finish: function (id, o) {
2653
+ var r = runById(String(id));
2654
+ if (!r) return;
2655
+ var ok = !(o && o.ok === false);
2656
+ r.running = false;
2657
+ r.done = true;
2658
+ r.error = !ok;
2659
+ r.endedAt = Date.now();
2660
+ if (r.doneLine)
2661
+ runsApi.append(
2662
+ r.id,
2663
+ (ok ? "✓ " : "✗ ") +
2664
+ t(ok ? "dock.done" : "dock.failed", {
2665
+ s: secs(r.endedAt - r.startedAt),
2666
+ }),
2667
+ ok ? "done-ok" : "done-fail",
2668
+ );
2669
+ renderDock();
2670
+ runsChanged();
2671
+ },
2672
+ set: function (id, o) {
2673
+ var r = runById(String(id));
2674
+ if (!r || !o) return;
2675
+ if (o.label != null) r.label = String(o.label);
2676
+ if (o.running !== undefined) markRunning(r, !!o.running);
2677
+ if (o.error !== undefined) r.error = !!o.error;
2678
+ if (o.actions !== undefined) r.actions = o.actions || [];
2679
+ renderDock();
2680
+ runsChanged();
2681
+ },
2682
+ select: function (id, o) {
2683
+ var r = runById(String(id));
2684
+ if (!r) return;
2685
+ runSel = r.id;
2686
+ if (o && o.open === true) {
2687
+ dockOpen = true;
2688
+ rememberDock();
2689
+ }
2690
+ renderDock();
2691
+ },
2692
+ clear: function (id) {
2693
+ var r = runById(String(id == null ? runSel : id));
2694
+ if (!r) return;
2695
+ r.lines = [];
2696
+ r.keyed = {};
2697
+ if (shownRun === r) fillOut();
2698
+ },
2699
+ remove: function (id) {
2700
+ var r = runById(String(id));
2701
+ if (!r) return;
2702
+ RUNS.splice(RUNS.indexOf(r), 1);
2703
+ if (runSel === r.id) {
2704
+ var last = RUNS[RUNS.length - 1];
2705
+ runSel = last ? last.id : null;
2706
+ }
2707
+ if (shownRun === r) shownRun = null;
2708
+ renderDock();
2709
+ runsChanged();
2710
+ },
2711
+ show: function () {
2712
+ dockOpen = true;
2713
+ rememberDock();
2714
+ renderDock();
2715
+ },
2716
+ hide: function () {
2717
+ dockOpen = false;
2718
+ rememberDock();
2719
+ renderDock();
2720
+ },
2721
+ toggle: function () {
2722
+ dockOpen = !dockOpen;
2723
+ rememberDock();
2724
+ renderDock();
2725
+ return dockOpen;
2726
+ },
2727
+ isOpen: function () {
2728
+ return dockOpen;
2729
+ },
2730
+ has: function (id) {
2731
+ return !!runById(String(id));
2732
+ },
2733
+ list: function () {
2734
+ return RUNS.map(function (r) {
2735
+ return {
2736
+ id: r.id,
2737
+ label: r.label,
2738
+ running: r.running,
2739
+ done: r.done,
2740
+ error: r.error,
2741
+ lines: r.lines.length,
2742
+ ws: r.ws.slice(),
2743
+ ranAt: r.ranAt,
2744
+ endedAt: r.endedAt,
2745
+ shown: inScope(r),
2746
+ };
2747
+ });
2748
+ },
2749
+ /**
2750
+ * Scope the dock to one workspace's runs (R24e), or null for every
2751
+ * run. "All runs" in the bar lifts it until the scope changes.
2752
+ */
2753
+ scope: function (o) {
2754
+ var ws = o && o.ws ? String(o.ws) : null;
2755
+ if ((runScope && runScope.ws) === ws) return;
2756
+ runScope = ws ? { ws: ws } : null;
2757
+ runAll = false;
2758
+ renderDock();
2759
+ runsChanged();
2760
+ },
2761
+ selected: function () {
2762
+ var r = selectedRun();
2763
+ return r ? r.id : null;
2764
+ },
2765
+ text: function (id) {
2766
+ var r = runById(String(id == null ? runSel : id)) || selectedRun();
2767
+ return r
2768
+ ? r.lines
2769
+ .map(function (l) {
2770
+ return l.text + "\n";
2771
+ })
2772
+ .join("")
2773
+ : "";
2774
+ },
2775
+ running: function () {
2776
+ return RUNS.filter(function (r) {
2777
+ return r.running;
2778
+ }).length;
2779
+ },
2780
+ onChange: function (fn) {
2781
+ if (typeof fn === "function") runListeners.push(fn);
2782
+ },
2783
+ };
2784
+
2785
+ function copyText(text) {
2786
+ if (navigator.clipboard && navigator.clipboard.writeText)
2787
+ return navigator.clipboard.writeText(text);
2788
+ return new Promise(function (resolve, reject) {
2789
+ var ta = document.createElement("textarea");
2790
+ ta.value = text;
2791
+ ta.style.position = "fixed";
2792
+ ta.style.opacity = "0";
2793
+ document.body.appendChild(ta);
2794
+ ta.select();
2795
+ var ok = false;
2796
+ try {
2797
+ ok = document.execCommand("copy");
2798
+ } catch (e) {
2799
+ ok = false;
2800
+ }
2801
+ ta.remove();
2802
+ if (ok) resolve();
2803
+ else reject(new Error("copy failed"));
2804
+ });
2805
+ }
2806
+ function runOfEl(el, attr) {
2807
+ var n = Number(el.getAttribute(attr));
2808
+ for (var i = 0; i < RUNS.length; i++) if (RUNS[i].n === n) return RUNS[i];
2809
+ return null;
2810
+ }
2811
+ function closeRun(r) {
2812
+ if (typeof r.onClose === "function") r.onClose(r.id);
2813
+ else runsApi.remove(r.id);
2814
+ }
2815
+ if (dock.addEventListener) {
2816
+ dock.addEventListener("click", function (e) {
2817
+ var tgt = e.target && e.target.closest ? e.target : null;
2818
+ if (!tgt) return;
2819
+ var x = tgt.closest("[data-rclose]");
2820
+ if (x) {
2821
+ var rx = runOfEl(x, "data-rclose");
2822
+ if (rx) closeRun(rx);
2823
+ return;
2824
+ }
2825
+ var tab = tgt.closest("[data-rsel]");
2826
+ if (tab) {
2827
+ var rt = runOfEl(tab, "data-rsel");
2828
+ if (!rt) return;
2829
+ runsApi.select(rt.id, { open: true });
2830
+ if (typeof rt.onSelect === "function") rt.onSelect(rt.id);
2831
+ return;
2832
+ }
2833
+ if (tgt.closest('[data-act="dock"]')) {
2834
+ runsApi.toggle();
2835
+ return;
2836
+ }
2837
+ var act = tgt.closest("[data-ract]");
2838
+ var sel = selectedRun();
2839
+ if (act && sel) {
2840
+ var a = (sel.actions || [])[Number(act.getAttribute("data-ract"))];
2841
+ if (a && typeof a.run === "function") a.run(sel.id);
2842
+ return;
2843
+ }
2844
+ var d = tgt.closest("[data-dock]");
2845
+ if (d && d.getAttribute("data-dock") === "all") {
2846
+ runAll = !runAll;
2847
+ renderDock();
2848
+ runsChanged();
2849
+ return;
2850
+ }
2851
+ if (!d || !sel) return;
2852
+ if (d.getAttribute("data-dock") === "clear") {
2853
+ runsApi.clear(sel.id);
2854
+ if (typeof sel.onClear === "function") sel.onClear(sel.id);
2855
+ } else {
2856
+ var btn = d;
2857
+ copyText(runsApi.text(sel.id))
2858
+ .then(function () {
2859
+ btn.textContent = t("dock.copied");
2860
+ setTimeout(function () {
2861
+ btn.textContent = t("dock.copy");
2862
+ }, 1500);
2863
+ })
2864
+ .catch(function () {});
2865
+ }
2866
+ });
2867
+ // The run tabs: one tab stop; ←/→, Home and End move (and select);
2868
+ // Delete closes a closable run.
2869
+ dock.addEventListener("keydown", function (e) {
2870
+ var tab = e.target && e.target.closest && e.target.closest("[data-rsel]");
2871
+ if (!tab) return;
2872
+ var r = runOfEl(tab, "data-rsel");
2873
+ var shown = visibleRuns();
2874
+ var i = shown.indexOf(r);
2875
+ var next =
2876
+ e.key === "ArrowRight"
2877
+ ? (i + 1) % shown.length
2878
+ : e.key === "ArrowLeft"
2879
+ ? (i - 1 + shown.length) % shown.length
2880
+ : e.key === "Home"
2881
+ ? 0
2882
+ : e.key === "End"
2883
+ ? shown.length - 1
2884
+ : -1;
2885
+ if (e.key === "Delete" && r && r.closable) {
2886
+ e.preventDefault();
2887
+ closeRun(r);
2888
+ var stop = dock.querySelector('.den-rtab[tabindex="0"]');
2889
+ if (stop) stop.focus();
2890
+ return;
2891
+ }
2892
+ if (next < 0 || !shown[next]) return;
2893
+ e.preventDefault();
2894
+ runsApi.select(shown[next].id, { open: dockOpen });
2895
+ if (typeof shown[next].onSelect === "function")
2896
+ shown[next].onSelect(shown[next].id);
2897
+ var to = dock.querySelector('[data-rsel="' + shown[next].n + '"]');
2898
+ if (to) to.focus();
2899
+ });
2900
+ // Drag the top edge to resize (Local Dev's handle): 60px to half the
2901
+ // window (R16a), remembered (shell.dockHeight).
2902
+ var drag = null;
2903
+ dock.addEventListener("mousedown", function (e) {
2904
+ if (!e.target.closest || !e.target.closest(".den-dock-grip")) return;
2905
+ var out = outEl();
2906
+ if (!out) return;
2907
+ drag = { y: e.clientY, h: out.offsetHeight };
2908
+ document.body.style.cursor = "ns-resize";
2909
+ document.body.style.userSelect = "none";
2910
+ e.preventDefault();
2911
+ });
2912
+ document.addEventListener("mousemove", function (e) {
2913
+ if (!drag) return;
2914
+ var out = outEl();
2915
+ if (!out) return;
2916
+ dockH = dockCap(drag.h + drag.y - e.clientY);
2917
+ out.style.height = dockH + "px";
2918
+ });
2919
+ document.addEventListener("mouseup", function () {
2920
+ if (!drag) return;
2921
+ drag = null;
2922
+ document.body.style.cursor = "";
2923
+ document.body.style.userSelect = "";
2924
+ if (state.dockHeight !== dockH) {
2925
+ state.dockHeight = dockH;
2926
+ saveShell({ dockHeight: dockH });
2927
+ }
2928
+ });
2929
+ }
2930
+ renderDock();
2931
+
2932
+ // ── Errors people can see and act on (R26c, R26d, MD-2; dev.38) ───────
2933
+ // denShell.api(method, url, body?, opts?) is the one client path for a
2934
+ // call: it resolves with the data (JSON, else text) or throws a typed
2935
+ // error (name "DenApiError": status, code, requestId, method, url,
2936
+ // network, data, retry). The server's errors are { ok: false, error,
2937
+ // code, requestId } (R26b); a 200 with ok:false is an error too. One a
2938
+ // page doesn't catch becomes a toast: the message, the request id,
2939
+ // Retry and Copy details.
2940
+ //
2941
+ // Every same-origin fetch that fails at the network level (the server
2942
+ // stopped or is restarting) raises the banner "Den's server isn't
2943
+ // responding — retrying"; it probes /api/server-info with backoff and
2944
+ // clears itself when the server answers. Meanwhile `den:server` (detail
2945
+ // { up }) fires on window and the status chip reads unknown.
2946
+ //
2947
+ // Banners sit at the top of the page's content (the prototype's
2948
+ // .note-banner): the unreachable one, and the engine one (MD-2: the
2949
+ // server fills data-engine-stale on #den-shell; header.js's server-info
2950
+ // poll calls setEngine).
2951
+ function apiError(message, o) {
2952
+ var e = new Error(message);
2953
+ e.name = "DenApiError";
2954
+ e.denApi = true;
2955
+ e.status = o.status || 0;
2956
+ e.code = o.code || (o.network ? "network" : "http_" + e.status);
2957
+ e.requestId = o.requestId || null;
2958
+ e.method = o.method;
2959
+ e.url = o.url;
2960
+ e.network = !!o.network;
2961
+ e.data = o.data === undefined ? null : o.data;
2962
+ e.retry = o.retry || null;
2963
+ return e;
2964
+ }
2965
+ function pathOf(url) {
2966
+ return String(url || "").split("?")[0];
2967
+ }
2968
+ function api(method, url, body, opts) {
2969
+ opts = opts || {};
2970
+ method = String(method || "GET").toUpperCase();
2971
+ var init = { method: method, headers: {} };
2972
+ if (body !== undefined && body !== null) {
2973
+ init.headers["Content-Type"] = "application/json";
2974
+ init.body = typeof body === "string" ? body : JSON.stringify(body);
2975
+ }
2976
+ if (opts.signal) init.signal = opts.signal;
2977
+ if (opts.headers)
2978
+ for (var h in opts.headers) init.headers[h] = opts.headers[h];
2979
+ var what = method + " " + pathOf(url);
2980
+ var retry =
2981
+ opts.retry ||
2982
+ (method === "GET"
2983
+ ? function () {
2984
+ location.reload();
2985
+ }
2986
+ : function () {
2987
+ return api(method, url, body, opts).then(function () {
2988
+ toast(t("api.retried"));
2989
+ });
2990
+ });
2991
+ return fetch(url, init).then(
2992
+ function (r) {
2993
+ var get = function (k) {
2994
+ return r.headers && r.headers.get ? r.headers.get(k) || "" : "";
2995
+ };
2996
+ var isJson = /json/.test(get("Content-Type"));
2997
+ var parsed = isJson
2998
+ ? r.json().catch(function () {
2999
+ return null;
3000
+ })
3001
+ : typeof r.text === "function"
3002
+ ? r.text()
3003
+ : Promise.resolve(null);
3004
+ return parsed.then(function (data) {
3005
+ var failed =
3006
+ !r.ok || (data && typeof data === "object" && data.ok === false);
3007
+ if (!failed) return data;
3008
+ var msg =
3009
+ data && typeof data === "object" && typeof data.error === "string"
3010
+ ? data.error
3011
+ : typeof data === "string" && data.trim()
3012
+ ? data.trim().slice(0, 300)
3013
+ : t("api.failed", { what: what }) + " (" + r.status + ")";
3014
+ throw apiError(msg, {
3015
+ status: r.status,
3016
+ code: data && typeof data === "object" ? data.code : null,
3017
+ requestId:
3018
+ (data && typeof data === "object" && data.requestId) ||
3019
+ get("X-Request-Id") ||
3020
+ null,
3021
+ method: method,
3022
+ url: url,
3023
+ data: data,
3024
+ retry: retry,
3025
+ });
3026
+ });
3027
+ },
3028
+ function (err) {
3029
+ if (err && err.name === "AbortError") throw err;
3030
+ throw apiError(t("api.network", { what: what }), {
3031
+ network: true,
3032
+ method: method,
3033
+ url: url,
3034
+ retry: retry,
3035
+ });
3036
+ },
3037
+ );
3038
+ }
3039
+ /** The text "Copy details" puts on the clipboard (no stack, no body). */
3040
+ function errorDetails(err) {
3041
+ return [
3042
+ "Den " + (versionText() || "unknown"),
3043
+ (err.method || "GET") + " " + pathOf(err.url),
3044
+ "Status " + (err.network ? "no answer" : err.status),
3045
+ "Code " + (err.code || ""),
3046
+ "Request " + (err.requestId || "none"),
3047
+ "Message " + err.message,
3048
+ "Page " + location.pathname,
3049
+ "Time " + new Date().toISOString(),
3050
+ ].join("\n");
3051
+ }
3052
+ function versionText() {
3053
+ var v =
3054
+ document.querySelector && document.querySelector("#version-pill .v-txt");
3055
+ var s = v ? String(v.textContent || "").replace(/^v/, "") : "";
3056
+ return s && s !== "…" ? s : "";
3057
+ }
3058
+ function copyText(text) {
3059
+ if (navigator.clipboard && navigator.clipboard.writeText)
3060
+ return navigator.clipboard.writeText(text).catch(function () {});
3061
+ return Promise.resolve();
3062
+ }
3063
+ var errToastTimer = null;
3064
+ /** An error toast: message, request id, Retry, Copy details, ×. */
3065
+ function errorToast(err) {
3066
+ if (!document.body || !document.createElement) return;
3067
+ var old = document.querySelector && document.querySelector(".den-toast");
3068
+ if (old && old.remove) old.remove();
3069
+ clearTimeout(toastTimer);
3070
+ clearTimeout(errToastTimer);
3071
+ var el = document.createElement("div");
3072
+ el.className = "den-toast den-toast-err";
3073
+ el.setAttribute("role", "alert");
3074
+ el.innerHTML =
3075
+ '<span class="den-te-ic" aria-hidden="true">⚠</span>' +
3076
+ '<span class="den-te-msg"><span class="den-te-t"></span>' +
3077
+ (err.requestId
3078
+ ? '<span class="den-te-id">' +
3079
+ esc(t("api.request", { id: err.requestId })) +
3080
+ "</span>"
3081
+ : "") +
3082
+ "</span>" +
3083
+ (err.retry
3084
+ ? '<button type="button" class="den-tbtn" data-te="retry">' +
3085
+ esc(t("api.retry")) +
3086
+ "</button>"
3087
+ : "") +
3088
+ '<button type="button" class="den-tbtn" data-te="copy">' +
3089
+ esc(t("api.copy")) +
3090
+ "</button>" +
3091
+ '<button type="button" class="den-tx" data-te="close" aria-label="' +
3092
+ esc(t("api.close")) +
3093
+ '">×</button>';
3094
+ el.querySelector(".den-te-t").textContent = err.message;
3095
+ var hold = false;
3096
+ var arm = function () {
3097
+ clearTimeout(errToastTimer);
3098
+ errToastTimer = setTimeout(function () {
3099
+ if (!hold && el.remove) el.remove();
3100
+ }, 12000);
3101
+ };
3102
+ el.addEventListener("mouseenter", function () {
3103
+ hold = true;
3104
+ });
3105
+ el.addEventListener("mouseleave", function () {
3106
+ hold = false;
3107
+ arm();
3108
+ });
3109
+ el.addEventListener("focusin", function () {
3110
+ hold = true;
3111
+ });
3112
+ el.addEventListener("click", function (e) {
3113
+ var b = e.target.closest && e.target.closest("[data-te]");
3114
+ if (!b) return;
3115
+ var act = b.getAttribute("data-te");
3116
+ if (act === "close") el.remove();
3117
+ else if (act === "copy")
3118
+ copyText(errorDetails(err)).then(function () {
3119
+ b.textContent = t("api.copied");
3120
+ });
3121
+ else if (act === "retry") {
3122
+ el.remove();
3123
+ try {
3124
+ var p = err.retry();
3125
+ if (p && typeof p.catch === "function")
3126
+ p.catch(function (again) {
3127
+ if (again && again.denApi) errorToast(again);
3128
+ });
3129
+ } catch (again) {
3130
+ if (again && again.denApi) errorToast(again);
3131
+ }
3132
+ }
3133
+ });
3134
+ document.body.appendChild(el);
3135
+ arm();
3136
+ }
3137
+ if (typeof window.addEventListener === "function")
3138
+ window.addEventListener("unhandledrejection", function (e) {
3139
+ var r = e && e.reason;
3140
+ if (!r || !r.denApi) return;
3141
+ if (e.preventDefault) e.preventDefault();
3142
+ errorToast(r);
3143
+ });
3144
+
3145
+ // Banners (.note-banner): at the top of the page's content.
3146
+ var BANNERS = {};
3147
+ function bannerHost() {
3148
+ var main =
3149
+ document.getElementById && document.getElementById("den-content");
3150
+ if (!main) return null;
3151
+ var box = document.getElementById("den-banners");
3152
+ if (!box) {
3153
+ box = document.createElement("div");
3154
+ box.id = "den-banners";
3155
+ box.className = "den-banners";
3156
+ box.setAttribute("aria-live", "polite");
3157
+ }
3158
+ if (main.firstChild !== box) main.insertBefore(box, main.firstChild);
3159
+ // A page whose content is already inset (edge, settings) needs none.
3160
+ var pad = 0;
3161
+ try {
3162
+ pad = parseFloat(getComputedStyle(main).paddingLeft) || 0;
3163
+ } catch (e) {
3164
+ pad = 0;
3165
+ }
3166
+ box.classList.toggle("den-banners-inset", pad >= 12);
3167
+ return box;
3168
+ }
3169
+ /** Show (or replace) banner `id`: { title?, text, action?: { label, run } }. */
3170
+ function showBanner(id, b) {
3171
+ BANNERS[id] = b;
3172
+ paintBanners();
3173
+ }
3174
+ function hideBanner(id) {
3175
+ if (!BANNERS[id]) return;
3176
+ delete BANNERS[id];
3177
+ paintBanners();
3178
+ }
3179
+ function paintBanners() {
3180
+ var box = bannerHost();
3181
+ if (!box) {
3182
+ if (document.readyState === "loading")
3183
+ document.addEventListener("DOMContentLoaded", paintBanners, {
3184
+ once: true,
3185
+ });
3186
+ return;
3187
+ }
3188
+ var ids = Object.keys(BANNERS);
3189
+ box.hidden = ids.length === 0;
3190
+ box.innerHTML = ids
3191
+ .map(function (id) {
3192
+ var b = BANNERS[id];
3193
+ return (
3194
+ '<div class="den-banner warn" data-banner="' +
3195
+ esc(id) +
3196
+ '" role="status"><span class="den-bn-ic" aria-hidden="true">⚠</span>' +
3197
+ (b.title ? "<b>" + esc(b.title) + "</b>" : "") +
3198
+ '<span class="den-bn-t">' +
3199
+ esc(b.text) +
3200
+ "</span>" +
3201
+ (b.note ? '<span class="den-bn-n">' + esc(b.note) + "</span>" : "") +
3202
+ (b.action
3203
+ ? '<button type="button" class="den-tbtn" data-bn-act="' +
3204
+ esc(id) +
3205
+ '">' +
3206
+ esc(b.action.label) +
3207
+ "</button>"
3208
+ : "") +
3209
+ "</div>"
3210
+ );
3211
+ })
3212
+ .join("");
3213
+ }
3214
+ document.addEventListener("click", function (e) {
3215
+ var b = e.target && e.target.closest && e.target.closest("[data-bn-act]");
3216
+ if (!b) return;
3217
+ var ban = BANNERS[b.getAttribute("data-bn-act")];
3218
+ if (ban && ban.action) ban.action.run();
3219
+ });
3220
+
3221
+ // MD-2: the engine on disk is not the one this server runs.
3222
+ function setEngine(info) {
3223
+ if (!info || !info.stale) {
3224
+ hideBanner("engine");
3225
+ return;
3226
+ }
3227
+ // The server's message is the English one (it names builds when the
3228
+ // versions match); another language says it from its catalog.
3229
+ showBanner("engine", {
3230
+ title: t("engine.title"),
3231
+ text:
3232
+ (lang === "en" && info.message) ||
3233
+ t("engine.stale", {
3234
+ onDisk: info.onDisk || "?",
3235
+ loaded: info.loaded || "?",
3236
+ cli: "__DEN_CLI__",
3237
+ }),
3238
+ });
3239
+ }
3240
+ if (host.dataset.engineStale) {
3241
+ try {
3242
+ var eng = JSON.parse(host.dataset.engineStale);
3243
+ eng.stale = true;
3244
+ setEngine(eng);
3245
+ } catch (e) {
3246
+ /* a malformed attribute: no banner */
3247
+ }
3248
+ }
3249
+
3250
+ // R26d: the server unreachable.
3251
+ var server = { up: true, delay: 0, timer: null, tick: null, nextAt: 0 };
3252
+ var rawFetch =
3253
+ typeof window.fetch === "function" ? window.fetch.bind(window) : null;
3254
+ function sameOrigin(input) {
3255
+ var u = typeof input === "string" ? input : input && input.url;
3256
+ if (!u) return false;
3257
+ if (u.charAt(0) === "/" && u.charAt(1) !== "/") return true;
3258
+ try {
3259
+ return new URL(u, location.href).origin === location.origin;
3260
+ } catch (e) {
3261
+ return false;
3262
+ }
3263
+ }
3264
+ function serverState(up) {
3265
+ if (server.up === up) return;
3266
+ server.up = up;
3267
+ if (document.body && document.body.classList)
3268
+ document.body.classList.toggle("den-server-down", !up);
3269
+ try {
3270
+ window.dispatchEvent(
3271
+ new CustomEvent("den:server", { detail: { up: up } }),
3272
+ );
3273
+ } catch (e) {
3274
+ /* no CustomEvent */
3275
+ }
3276
+ }
3277
+ function paintDown() {
3278
+ var left = Math.max(0, Math.ceil((server.nextAt - Date.now()) / 1000));
3279
+ showBanner("net", {
3280
+ text: t("net.down"),
3281
+ note: left > 0 ? t("net.next", { s: left }) : "",
3282
+ action: { label: t("net.now"), run: probeNow },
3283
+ });
3284
+ }
3285
+ function serverUnreachable() {
3286
+ if (!server.up) return;
3287
+ serverState(false);
3288
+ server.delay = 1000;
3289
+ scheduleProbe();
3290
+ }
3291
+ function scheduleProbe() {
3292
+ clearTimeout(server.timer);
3293
+ clearInterval(server.tick);
3294
+ server.nextAt = Date.now() + server.delay;
3295
+ paintDown();
3296
+ server.tick = setInterval(paintDown, 1000);
3297
+ server.timer = setTimeout(probe, server.delay);
3298
+ }
3299
+ function probeNow() {
3300
+ clearTimeout(server.timer);
3301
+ probe();
3302
+ }
3303
+ function probe() {
3304
+ if (!rawFetch) return;
3305
+ rawFetch("/api/server-info", { cache: "no-store" }).then(
3306
+ function () {
3307
+ serverBack();
3308
+ },
3309
+ function () {
3310
+ if (server.up) return;
3311
+ server.delay = Math.min(server.delay * 2, 15000);
3312
+ scheduleProbe();
3313
+ },
3314
+ );
3315
+ }
3316
+ function serverBack() {
3317
+ clearTimeout(server.timer);
3318
+ clearInterval(server.tick);
3319
+ hideBanner("net");
3320
+ serverState(true);
3321
+ }
3322
+ if (rawFetch && !window.fetch.__denShell) {
3323
+ var wrapped = function (input, init) {
3324
+ var p = rawFetch(input, init);
3325
+ if (!p || typeof p.then !== "function") return p;
3326
+ return p.then(
3327
+ function (r) {
3328
+ if (!server.up && sameOrigin(input)) serverBack();
3329
+ return r;
3330
+ },
3331
+ function (err) {
3332
+ if (err && err.name === "TypeError" && sameOrigin(input))
3333
+ serverUnreachable();
3334
+ throw err;
3335
+ },
3336
+ );
3337
+ };
3338
+ wrapped.__denShell = true;
3339
+ window.fetch = wrapped;
3340
+ }
3341
+
3342
+ // ── the UI language (R19): a user pref ───────────────────────────────
3343
+ // /api/settings `language` (user.yml), then a reload: the server serves
3344
+ // every page with that language's catalog from its first paint.
3345
+ function setLanguage(l) {
3346
+ if (l === lang) return Promise.resolve(false);
3347
+ return api("POST", "/api/settings", { language: l }).then(function () {
3348
+ location.reload();
3349
+ return true;
3350
+ });
3351
+ }
3352
+ // The tab's title names the page in the page's language.
3353
+ (function () {
3354
+ var label = page === SETTINGS.id ? SETTINGS.label : null;
3355
+ NAV.forEach(function (n) {
3356
+ if (n.id === page) label = n.label;
3357
+ });
3358
+ var title = String(document.title || "");
3359
+ var at = title.lastIndexOf(" · ");
3360
+ if (label && at >= 0)
3361
+ document.title = title.slice(0, at) + " · " + t(label);
3362
+ })();
3363
+
3364
+ window.denShell = {
3365
+ registerGroup: registerGroup,
3366
+ registerNav: registerNav,
3367
+ addMessages: function (l, msgs) {
3368
+ MESSAGES[l] = Object.assign({}, MESSAGES[l] || {}, msgs || {});
3369
+ lastHtml = "";
3370
+ renderRail();
3371
+ },
3372
+ t: t,
3373
+ /** R19: the page's language, the ones with a catalog, and a switch. */
3374
+ lang: function () {
3375
+ return lang;
3376
+ },
3377
+ languages: function () {
3378
+ return LANGUAGES.slice();
3379
+ },
3380
+ setLanguage: setLanguage,
3381
+ /** A number in the page's language (Intl.NumberFormat options). */
3382
+ formatNumber: num,
3383
+ setState: setState,
3384
+ getState: function () {
3385
+ return state;
3386
+ },
3387
+ pin: pin,
3388
+ unpin: unpin,
3389
+ isPinned: isPinned,
3390
+ toast: toast,
3391
+ /** R26c: a call that resolves with its data or throws a DenApiError. */
3392
+ api: api,
3393
+ /** R26c: the error toast for a DenApiError (what an uncaught one gets). */
3394
+ errorToast: errorToast,
3395
+ /** A banner at the top of the content: { title?, text, note?, action? }. */
3396
+ showBanner: showBanner,
3397
+ hideBanner: hideBanner,
3398
+ /** MD-2: { stale, loaded, onDisk, message? } from /api/server-info. */
3399
+ setEngine: setEngine,
3400
+ /** R26d: whether the server answers (false while the banner shows). */
3401
+ serverUp: function () {
3402
+ return server.up;
3403
+ },
3404
+ /** The browser-tab icon (appearance.js: the active theme's, per mode). */
3405
+ setFavicon: setFavicon,
3406
+ /** The org part of the scope: "/ ORG <title>", static (one org). */
3407
+ setOrg: setOrg,
3408
+ setOrgs: setOrgs,
3409
+ setWorkspaces: setWorkspaces,
3410
+ /** Repaint the Env part from #env-sel (after setting its value). */
3411
+ syncEnv: syncEnv,
3412
+ syncPinToggles: syncPinToggles,
3413
+ registerCommand: registerCommand,
3414
+ unregisterCommand: unregisterCommand,
3415
+ commands: commands,
3416
+ /** A .pop menu below `anchor` (the model as the scope menus'). */
3417
+ openMenu: openMenu,
3418
+ closeMenu: function () {
3419
+ closeMenu(false);
3420
+ },
3421
+ groups: function () {
3422
+ return GROUPS.slice().sort(byOrder);
3423
+ },
3424
+ nav: function () {
3425
+ return NAV.slice();
3426
+ },
3427
+ /** NAV in the rail's order (groups, then your order), hidden included. */
3428
+ orderedNav: orderedNav,
3429
+ setNavPrefs: setNavPrefs,
3430
+ setPins: setPins,
3431
+ setCollapsed: setCollapsed,
3432
+ setDock: setDock,
3433
+ /** The runs dock (R16): every page's output, one path. */
3434
+ runs: runsApi,
3435
+ /** The page header (R20): { el, h1, sub, act, tabs(spec), setSub }, or null. */
3436
+ pageHeader: pageHeader,
3437
+ /** Wire a role=tablist as WAI-ARIA tabs (roving tabindex, keys, panels). */
3438
+ wireTabs: wireTabs,
3439
+ registerSettingsCategory: registerSettingsCategory,
3440
+ registerSetting: registerSetting,
3441
+ settings: function () {
3442
+ return SETTING_ROWS.slice();
3443
+ },
3444
+ settingsCategories: function () {
3445
+ return SETTING_CATS.slice().sort(byOrder);
3446
+ },
3447
+ };
3448
+
3449
+ // The ready path for a script that loads before shell.js:
3450
+ // window.denShellQueue is an array of functions, each called with
3451
+ // denShell once it exists (now); a later push runs at once. Then
3452
+ // `den-shell:ready` fires on window.
3453
+ var queued = Array.isArray(window.denShellQueue) ? window.denShellQueue : [];
3454
+ function runQueued(fn) {
3455
+ if (typeof fn !== "function") return;
3456
+ try {
3457
+ fn(window.denShell);
3458
+ } catch (err) {
3459
+ warn("a denShellQueue function threw: " + (err && err.message));
3460
+ }
3461
+ }
3462
+ window.denShellQueue = {
3463
+ push: function () {
3464
+ for (var i = 0; i < arguments.length; i++) runQueued(arguments[i]);
3465
+ return 0;
3466
+ },
3467
+ length: 0,
3468
+ };
3469
+ queued.forEach(runQueued);
3470
+ try {
3471
+ window.dispatchEvent(
3472
+ new CustomEvent("den-shell:ready", { detail: window.denShell }),
3473
+ );
3474
+ } catch (e) {
3475
+ /* no CustomEvent: the queue still ran */
3476
+ }
3477
+ // The queue has drained and ready has fired: a page still without its
3478
+ // group (or a setting without its category) a tick from now warns.
3479
+ settled = true;
3480
+ checkOrphansSoon();
3481
+
3482
+ // The server's prefs win over the local copy.
3483
+ if (typeof fetch === "function")
3484
+ fetch("/api/settings")
3485
+ .then(function (r) {
3486
+ return r.ok ? r.json() : null;
3487
+ })
3488
+ .then(function (s) {
3489
+ if (!s || !s.shell) return;
3490
+ setState({
3491
+ pins: Array.isArray(s.shell.pins) ? s.shell.pins : [],
3492
+ collapsed: s.shell.collapsed === true,
3493
+ hidden: Array.isArray(s.shell.hidden) ? s.shell.hidden : [],
3494
+ order: Array.isArray(s.shell.order) ? s.shell.order : [],
3495
+ dock: s.shell.dock === true,
3496
+ dockHeight:
3497
+ typeof s.shell.dockHeight === "number" ? s.shell.dockHeight : 0,
3498
+ });
3499
+ remember();
3500
+ dockFromPrefs();
3501
+ })
3502
+ .catch(function () {});
3503
+
3504
+ // The Edge page shows once the edge is configured (no Docker asked).
3505
+ if (typeof fetch === "function")
3506
+ fetch("/api/edge?probe=1")
3507
+ .then(function (r) {
3508
+ return r.ok ? r.json() : null;
3509
+ })
3510
+ .then(function (e) {
3511
+ if (!e) return;
3512
+ setState({ edge: e.configured === true });
3513
+ remember();
3514
+ })
3515
+ .catch(function () {});
3516
+
3517
+ var style = document.createElement("style");
3518
+ style.id = "den-shell-styles";
3519
+ style.textContent = [
3520
+ // Layout: the prototype's 48px bar, 216px rail (56px collapsed); a
3521
+ // drawer below 720px.
3522
+ // The prototype's rail is `width: 216px` (56px collapsed) in a
3523
+ // content box: with its padding and border it takes 233px (73px).
3524
+ "body.den-has-shell { --den-top-h: 48px; --den-rail-cw: 216px;",
3525
+ " --den-rail-w: calc(var(--den-rail-cw) + 17px); padding-left: var(--den-rail-w); }",
3526
+ "body.den-has-shell.den-rail-collapsed { --den-rail-cw: 56px; }",
3527
+ // A column on every page (most pages are one already), so the runs
3528
+ // dock sits at the foot even when the content is short.
3529
+ "body.den-has-shell { display: flex; flex-direction: column; min-height: 100vh; }",
3530
+ ".den-shell { display: contents; }",
3531
+ ".den-top, .den-rail { font: 14px/1.45 var(--sans); }",
3532
+ ".den-top :focus-visible, .den-rail :focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }",
3533
+ ".den-skip { position: fixed; left: 8px; top: -40px; z-index: 100; padding: 6px 10px;",
3534
+ " background: var(--surface); color: var(--text); border: 1px solid var(--accent);",
3535
+ " border-radius: var(--radius-button); font: 600 12px var(--sans); text-decoration: none; }",
3536
+ ".den-skip:focus { top: 8px; }",
3537
+ // Top bar (.top)
3538
+ ".den-top { display: flex; align-items: center; gap: 8px; height: var(--den-top-h);",
3539
+ " box-sizing: border-box; padding: 0 12px; flex-shrink: 0; position: relative; z-index: 20;",
3540
+ " margin-left: calc(-1 * var(--den-rail-w)); background: var(--surface);",
3541
+ " border-bottom: 1px solid var(--border); min-width: 0; color: var(--text); }",
3542
+ ".den-top::after { content: ''; position: absolute; left: 0; right: 0; bottom: -1px; height: 2px;",
3543
+ " background: var(--bar, none); pointer-events: none; }",
3544
+ // Brand (.brand, .mark)
3545
+ ".den-brand { display: flex; align-items: center; gap: 8px; color: var(--text);",
3546
+ " text-decoration: none; flex-shrink: 0; white-space: nowrap; }",
3547
+ ".den-title { font: 600 15px var(--display); margin: 0; white-space: nowrap; }",
3548
+ "#den-shell .den-mark { width: 22px; height: 22px; }",
3549
+ "#den-shell .den-mark img { width: 22px; height: 22px; }",
3550
+ "#den-shell .den-mark-text { font: 700 12px/1 var(--mono); }",
3551
+ "#den-shell .den-mark-rounded { border-radius: 6px; }",
3552
+ // Buttons in the bar (.ib)
3553
+ ".den-ib { background: none; border: 1px solid transparent; border-radius: var(--radius-button);",
3554
+ " padding: 4px 8px; cursor: pointer; color: var(--muted); display: inline-flex;",
3555
+ " align-items: center; gap: 6px; white-space: nowrap; font: inherit; }",
3556
+ ".den-ib:hover { background: var(--sunken); color: var(--text); }",
3557
+ ".den-menu-btn { display: none; }",
3558
+ // Scope (.scope, .sep, .k): / Org ▾ / Workspace ▾ / Env ▾ (R4)
3559
+ // The search gives way first (dev.20): the scope keeps its parts whole
3560
+ // (each name is capped below), so they never run into one another.
3561
+ ".den-scope { display: flex; align-items: center; gap: 2px; min-width: 0; flex-shrink: 0; }",
3562
+ ".den-sep { color: var(--faint); }",
3563
+ ".den-scope [hidden] { display: none; }",
3564
+ ".den-scope .den-ib { color: var(--text); font-size: 13px; }",
3565
+ ".den-k { font: 11px var(--mono); color: var(--faint); text-transform: uppercase; letter-spacing: .06em; }",
3566
+ ".den-scope .den-ib[aria-expanded='true'] { background: var(--sunken); }",
3567
+ // A long name ends in an ellipsis; the whole name is in the menu.
3568
+ ".den-scope .den-ib { min-width: 0; }",
3569
+ "#ws-pill .den-vn, #org-crumb .den-vn { display: inline-block; max-width: 200px; overflow: hidden;",
3570
+ " text-overflow: ellipsis; vertical-align: top; }",
3571
+ // Menus (.pop, .ml, .mi, .hr: R18)
3572
+ ".den-pop { position: fixed; z-index: 45; background: var(--raised); border: 1px solid var(--border-strong);",
3573
+ " border-radius: 10px; box-shadow: var(--shadow); padding: 6px; min-width: 220px;",
3574
+ " max-height: calc(100vh - 72px); overflow-y: auto; box-sizing: border-box;",
3575
+ " font: 14px/1.45 var(--sans); color: var(--text); }",
3576
+ ".den-pop:focus { outline: none; }",
3577
+ ".den-pop :focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }",
3578
+ ".den-pop .den-ml { font: 11px var(--mono); color: var(--faint); text-transform: uppercase;",
3579
+ " letter-spacing: .07em; padding: 6px 8px 2px; }",
3580
+ ".den-pop .den-mi { display: flex; width: 100%; align-items: center; gap: 8px; background: none;",
3581
+ " border: 0; padding: 6px 8px; border-radius: 6px; cursor: pointer; text-align: left;",
3582
+ " font: inherit; color: inherit; text-decoration: none; box-sizing: border-box; }",
3583
+ ".den-pop .den-mi:hover, .den-pop .den-mi:focus { background: var(--sunken); }",
3584
+ // The ✓ is drawn, not read (aria-checked says it): alt text "".
3585
+ ".den-pop .den-mi[aria-checked='true']::after { content: '✓' / ''; margin-left: auto; color: var(--accent-text); }",
3586
+ ".den-pop .den-mi-l { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }",
3587
+ ".den-pop .den-mtag { font: 11px var(--mono); color: var(--muted); }",
3588
+ ".den-pop .den-hr { height: 1px; background: var(--border); margin: 6px 0; }",
3589
+ // The page's one main landmark (#den-content) holds all its content
3590
+ // and lays it out as the body did (a column), so pages keep their look.
3591
+ "body.den-has-shell > #den-content { display: flex; flex-direction: column; flex: 1 1 auto;",
3592
+ " min-height: 0; min-width: 0; }",
3593
+ "body.den-has-shell > #den-content:focus { outline: none; }",
3594
+ // The page header (.ph, R20, 0.3.0-dev.32), the prototype's sizes: it
3595
+ // sits in .main's 20px/24px inset (pages keep their own content
3596
+ // full-bleed below it). .ptabs are the page tabs below it.
3597
+ "#den-content > .den-ph { display: flex; align-items: flex-end; gap: 16px; flex-wrap: wrap;",
3598
+ " flex: none; margin: 0 0 16px; padding: 20px 24px 0; font: 14px/1.45 var(--sans); color: var(--text); box-sizing: border-box; }",
3599
+ ".den-ph h1 { font: 600 22px/1.2 var(--display); margin: 0; padding: 0; text-wrap: balance; color: var(--text); }",
3600
+ ".den-ph .sub { color: var(--muted); font-size: 13px; margin: 4px 0 0; padding: 0; min-height: 1.45em; }",
3601
+ ".den-ph .act { margin-left: auto; display: flex; gap: 8px; }",
3602
+ ".den-ph [hidden], .den-ptabs[hidden], [role='tabpanel'][hidden] { display: none !important; }",
3603
+ ".ptabs.den-ptabs { flex: none; margin: 0 24px 18px; }",
3604
+ ".ptabs { display: flex; gap: 2px; border-bottom: 1px solid var(--border); overflow-x: auto; }",
3605
+ ".ptabs [role='tab'] { background: none; border: 0; padding: 8px 12px; color: var(--muted); cursor: pointer;",
3606
+ " border-bottom: 2px solid transparent; margin: 0 0 -1px; white-space: nowrap; font: inherit; font-size: 14px;",
3607
+ " line-height: 1.45; text-decoration: none; }",
3608
+ ".ptabs [role='tab']:hover { color: var(--text); }",
3609
+ ".ptabs [role='tab'][aria-selected='true'] { color: var(--text); border-bottom-color: var(--accent); font-weight: 500; }",
3610
+ // Search (.search, kbd: R5). The prototype's sizes are content-box
3611
+ // (the pages reset everything to border-box).
3612
+ ".den-search { flex: 1 1 260px; max-width: 420px; margin: 0 auto; display: flex; align-items: center;",
3613
+ " gap: 8px; background: var(--sunken); border: 1px solid var(--border); border-radius: var(--radius-button);",
3614
+ " padding: 5px 10px; color: var(--faint); cursor: text; min-width: 0; font: inherit; box-sizing: content-box; }",
3615
+ ".den-search-ph { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: left; }",
3616
+ ".den-search-ic { flex: none; }",
3617
+ ".den-top kbd, .den-pal kbd { font: 11px var(--mono); border: 1px solid var(--border); border-bottom-width: 2px;",
3618
+ " border-radius: 4px; padding: 0 5px; color: var(--muted); background: var(--surface); }",
3619
+ // Chips (.chips, .chip, .dot: R6, R7, R8)
3620
+ ".den-chips { display: flex; align-items: center; gap: 4px; flex-shrink: 0; }",
3621
+ ".den-chip { display: inline-flex; align-items: center; gap: 6px; border: 1px solid var(--border);",
3622
+ " border-radius: 999px; padding: 3px 10px; background: var(--surface); cursor: pointer; font: inherit;",
3623
+ " font-size: 13px; white-space: nowrap; flex-shrink: 0; color: var(--text); }",
3624
+ ".den-chip:hover { border-color: var(--border-strong); }",
3625
+ ".den-chip.bad { border-color: color-mix(in srgb, var(--bad) 50%, var(--border)); }",
3626
+ ".den-chip .den-n { font-variant-numeric: tabular-nums; }",
3627
+ ".den-chip .den-dot, .den-drawer .den-dot, .den-pal .den-dot { margin: 0; }",
3628
+ ".den-chip[aria-expanded='true'] { border-color: var(--border-strong); }",
3629
+ ".den-dot.run { background: var(--accent); animation: den-pulse 1.2s infinite; }",
3630
+ ".den-dot.pending { background: var(--faint); animation: den-pulse 1.2s infinite; }",
3631
+ "@keyframes den-pulse { 50% { opacity: .35; } }",
3632
+ "@media (prefers-reduced-motion: reduce) { .den-dot.run, .den-dot.pending { animation: none; } }",
3633
+ ".den-user { display: contents; }",
3634
+ // Buttons in overlays (.btn, .btn.sm, .btn.primary)
3635
+ ".den-btn { border: 1px solid var(--border); background: var(--surface); border-radius: var(--radius-button);",
3636
+ " padding: 6px 12px; cursor: pointer; font: inherit; font-weight: 500; white-space: nowrap; color: var(--text); }",
3637
+ ".den-btn:hover { border-color: var(--border-strong); }",
3638
+ ".den-btn.primary { background: var(--accent-fill); border-color: var(--accent-fill); color: var(--on-accent); }",
3639
+ ".den-btn.sm { padding: 2px 9px; font-size: 12.5px; }",
3640
+ ".den-btn:disabled { opacity: .6; cursor: default; }",
3641
+ // Status drawer (.scrim, .drawer, .prob-card, .grp, .svl: R15)
3642
+ ".den-scrim { position: fixed; inset: 0; background: rgb(0 0 0 / 0.35); z-index: 40; }",
3643
+ ".den-drawer { position: fixed; top: 0; right: 0; bottom: 0; width: min(420px, 100%); z-index: 41;",
3644
+ " box-sizing: content-box; background: var(--surface); border-left: 1px solid var(--border);",
3645
+ " box-shadow: var(--shadow); display: flex; flex-direction: column; font: 14px/1.45 var(--sans);",
3646
+ " color: var(--text); }",
3647
+ ".den-drawer :focus-visible, .den-pal :focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }",
3648
+ ".den-drawer .den-dh { display: flex; align-items: center; gap: 10px; padding: 14px 16px;",
3649
+ " border-bottom: 1px solid var(--border); }",
3650
+ ".den-drawer .den-dh h2 { margin: 0; font: 600 16px var(--display); flex: 1; }",
3651
+ ".den-drawer .den-db { overflow-y: auto; padding: 12px 16px 24px; display: grid; gap: 16px; align-content: start; }",
3652
+ ".den-drawer .den-none { margin: 0; color: var(--muted); }",
3653
+ ".den-sec { font: 600 13px var(--mono); text-transform: uppercase; letter-spacing: .07em; color: var(--muted);",
3654
+ " margin: 0 0 10px; }",
3655
+ ".den-cards, .den-grps { display: grid; gap: 14px; }",
3656
+ ".den-cards { gap: 8px; }",
3657
+ ".den-prob-card { border: 1px solid color-mix(in srgb, var(--bad) 45%, var(--border));",
3658
+ " background: color-mix(in srgb, var(--bad) 7%, var(--surface)); border-radius: var(--radius);",
3659
+ " padding: 10px 12px; display: grid; gap: 6px; }",
3660
+ ".den-prob-card .den-t { display: flex; gap: 8px; align-items: center; font-weight: 600; }",
3661
+ ".den-prob-card .den-t .den-mono { font-weight: 400; color: var(--muted); }",
3662
+ ".den-prob-card .den-s { color: var(--muted); font-size: 12.5px; overflow-wrap: anywhere; }",
3663
+ ".den-prob-card .den-a { display: flex; gap: 6px; flex-wrap: wrap; }",
3664
+ ".den-grp h3 { margin: 0 0 6px; font: 600 12px var(--mono); color: var(--muted); display: flex;",
3665
+ " justify-content: space-between; text-transform: uppercase; letter-spacing: .06em; }",
3666
+ ".den-svl { display: grid; gap: 2px; }",
3667
+ ".den-svl button { display: flex; align-items: center; gap: 8px; background: none; border: 0; padding: 4px 6px;",
3668
+ " border-radius: 5px; cursor: pointer; text-align: left; font: inherit; color: inherit; min-width: 0; }",
3669
+ ".den-svl button:hover { background: var(--sunken); }",
3670
+ ".den-svl .den-mono { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }",
3671
+ ".den-svl .den-ms { margin-left: auto; font: 12px var(--mono); color: var(--faint);",
3672
+ " font-variant-numeric: tabular-nums; white-space: nowrap; }",
3673
+ // Command palette (.pal-wrap, .pal: R17)
3674
+ ".den-pal-wrap { position: fixed; inset: 0; z-index: 50; display: grid; justify-items: center;",
3675
+ " align-items: start; padding: 12vh 16px 0; background: rgb(0 0 0 / 0.35); font: 14px/1.45 var(--sans);",
3676
+ " color: var(--text); }",
3677
+ ".den-pal { width: min(620px, 100%); box-sizing: content-box; background: var(--raised);",
3678
+ " border: 1px solid var(--border-strong); border-radius: 12px; box-shadow: var(--shadow); overflow: hidden; }",
3679
+ ".den-pal input { width: 100%; box-sizing: border-box; border: 0; border-bottom: 1px solid var(--border);",
3680
+ " background: transparent; padding: 14px 16px; font-family: var(--sans); font-size: 16px; color: var(--text);",
3681
+ // line-height too: a page's `input { font: inherit }` (Settings) must not reach it.
3682
+ " line-height: normal; outline: none; margin: 0; }",
3683
+ ".den-pal ul { list-style: none; margin: 0; padding: 6px; max-height: 50vh; overflow-y: auto; }",
3684
+ ".den-pal li { display: flex; align-items: center; gap: 10px; padding: 7px 10px; border-radius: 7px; cursor: pointer; }",
3685
+ ".den-pal li[aria-selected='true'] { background: color-mix(in srgb, var(--accent) 16%, transparent); }",
3686
+ ".den-pal li .den-pal-l { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }",
3687
+ ".den-pal li .den-kind { margin-left: auto; font: 11px var(--mono); color: var(--faint); text-transform: uppercase;",
3688
+ " letter-spacing: .05em; white-space: nowrap; }",
3689
+ ".den-pal li .den-sub { color: var(--faint); font: 12px var(--mono); white-space: nowrap; }",
3690
+ ".den-pal li.den-pal-empty { cursor: default; color: var(--muted); }",
3691
+ ".den-pal .den-foot { display: flex; gap: 14px; padding: 8px 14px; border-top: 1px solid var(--border);",
3692
+ " color: var(--faint); font-size: 12px; flex-wrap: wrap; }",
3693
+ // The lock popover (.pop form, .pop input, .note: R8)
3694
+ ".den-pop form { display: grid; gap: 8px; padding: 6px 8px 8px; width: 260px; box-sizing: content-box; margin: 0; }",
3695
+ ".den-pop input:not([type='checkbox']) { border: 1px solid var(--border); background: var(--bg); color: var(--text);",
3696
+ " border-radius: var(--radius-button); padding: 6px 8px; font: inherit; font-size: 13.3333px; line-height: normal; }",
3697
+ ".den-pop .den-note, .den-pop .den-mnote { color: var(--muted); font-size: 12.5px; margin: 0; }",
3698
+ ".den-pop .den-mnote { padding: 4px 8px; max-width: 260px; }",
3699
+ ".den-pop .den-lock-save { display: flex; gap: 6px; align-items: center; color: var(--muted); font-size: 12.5px; }",
3700
+ ".den-pop .den-lock-save[hidden], .den-pop .den-lock-err[hidden], .den-mslot[hidden] { display: none; }",
3701
+ ".den-pop .den-lock-err { color: var(--bad); font-size: 12.5px; }",
3702
+ ".den-pop .den-lock-desc { padding: 2px 8px 6px; color: var(--muted); font-size: 12.5px; max-width: 260px; }",
3703
+ // What's new (.ib.whatsnew + .badge-dot)
3704
+ "#den-shell .v-pill { font: inherit; padding: 4px 8px; border: 1px solid transparent;",
3705
+ " border-radius: var(--radius-button); color: var(--muted); flex-shrink: 0; }",
3706
+ "#den-shell .v-pill:hover { background: var(--sunken); color: var(--text); border-color: transparent; }",
3707
+ "#den-shell .v-pip { position: static; width: 7px; height: 7px; border: 0; border-radius: 50%;",
3708
+ " background: var(--accent); }",
3709
+ // ◐ ▾ (the prototype's user menu, an .ib: appearance.js)
3710
+ ".den-top .den-ib[aria-expanded='true'] { background: var(--sunken); color: var(--text); }",
3711
+ // Rail (.rail)
3712
+ ".den-rail { position: fixed; left: 0; top: var(--den-top-h); bottom: 0; width: var(--den-rail-cw);",
3713
+ " box-sizing: content-box; display: flex; flex-direction: column; padding: 10px 8px;",
3714
+ " background: var(--surface); border-right: 1px solid var(--border); overflow-y: auto; z-index: 15;",
3715
+ " color: var(--text); }",
3716
+ ".den-rail-group { display: contents; }",
3717
+ // Group heading (.gh)
3718
+ ".den-gh { font: 11px var(--mono); text-transform: uppercase; letter-spacing: .07em; color: var(--faint);",
3719
+ " padding: 12px 10px 4px; display: flex; justify-content: space-between; }",
3720
+ ".den-rail > .den-rail-group:first-child > .den-gh { padding-top: 2px; }",
3721
+ ".den-tagx { font: 10px var(--mono); color: var(--faint); border: 1px dashed var(--border-strong);",
3722
+ " border-radius: 4px; padding: 0 4px; text-transform: none; letter-spacing: 0; }",
3723
+ // Rail item (.ni)
3724
+ ".den-ni { display: flex; align-items: center; gap: 10px; width: 100%; box-sizing: border-box;",
3725
+ " text-align: left; background: none; border: 0; padding: 6px 10px; border-radius: var(--radius-button);",
3726
+ " cursor: pointer; color: var(--muted); text-decoration: none; position: relative; font: inherit; }",
3727
+ ".den-ni:hover { background: var(--sunken); color: var(--text); }",
3728
+ ".den-ni[aria-current='page'] { background: color-mix(in srgb, var(--accent) 13%, transparent);",
3729
+ " color: var(--text); font-weight: 500; }",
3730
+ ".den-ni[aria-current='page']::before { content: ''; position: absolute; left: -8px; top: 6px;",
3731
+ " bottom: 6px; width: 3px; border-radius: 2px; background: var(--accent); }",
3732
+ ".den-ni svg { width: 18px; height: 18px; flex: none; stroke: currentColor; fill: none;",
3733
+ " stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }",
3734
+ ".den-lbl { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }",
3735
+ ".den-mono { font-family: var(--mono); font-size: 12.5px; }",
3736
+ ".den-bdg { font: 11px var(--mono); padding: 0 6px; border-radius: 999px; background: var(--sunken);",
3737
+ " color: var(--muted); font-variant-numeric: tabular-nums; }",
3738
+ ".den-bdg.bad { background: color-mix(in srgb, var(--bad) 16%, transparent); color: var(--bad); }",
3739
+ ".den-rail-grow { flex: 1; }",
3740
+ ".den-sr { position: absolute; width: 1px; height: 1px; padding: 0; overflow: hidden;",
3741
+ " clip: rect(0 0 0 0); white-space: nowrap; border: 0; }",
3742
+ // Pinned row: the dot in place of the icon (.dot, margin 0 5px)
3743
+ ".den-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--ok); flex: none;",
3744
+ " display: inline-block; margin: 0 5px; }",
3745
+ ".den-dot.bad { background: var(--bad); } .den-dot.warn { background: var(--warn); }",
3746
+ ".den-dot.off { background: var(--faint); }",
3747
+ ".den-pin-row { position: relative; }",
3748
+ ".den-pin-sp { visibility: hidden; font-size: 12px; }",
3749
+ ".den-pin-x { position: absolute; top: 0; bottom: 0; right: 10px; display: flex; align-items: center;",
3750
+ " padding: 0; border: 0; background: none; cursor: pointer; font: 12px var(--sans);",
3751
+ " opacity: 0; color: var(--faint); }",
3752
+ ".den-pin-x:hover { color: var(--text); }",
3753
+ ".den-pin-row:hover .den-pin-x, .den-pin-row:focus-within .den-pin-x { opacity: 1; }",
3754
+ // Collapsed: icons only. The prototype hides headings and labels; they
3755
+ // stay here for screen readers (visually hidden, no space taken).
3756
+ "body.den-rail-collapsed .den-rail .den-lbl, body.den-rail-collapsed .den-rail .den-gh,",
3757
+ "body.den-rail-collapsed .den-rail .den-bdg, body.den-rail-collapsed .den-rail .den-pin-x,",
3758
+ "body.den-rail-collapsed .den-rail .den-pin-sp {",
3759
+ " position: absolute; width: 1px; height: 1px; padding: 0; overflow: hidden;",
3760
+ " clip: rect(0 0 0 0); white-space: nowrap; border: 0; }",
3761
+ // Toast (.toast)
3762
+ ".den-toast { position: fixed; bottom: 70px; left: 50%; transform: translateX(-50%);",
3763
+ " background: var(--raised); border: 1px solid var(--border-strong); border-radius: 8px;",
3764
+ " padding: 8px 14px; box-shadow: var(--shadow); z-index: 60; font: 14px/1.45 var(--sans);",
3765
+ " color: var(--text); }",
3766
+ // The error toast (R26c): .toast with the message, its request id and
3767
+ // the prototype's small buttons (.btn.sm).
3768
+ ".den-toast-err { display: flex; align-items: center; gap: 10px; max-width: min(640px, calc(100vw - 32px));",
3769
+ " border-color: color-mix(in srgb, var(--bad) 45%, var(--border-strong)); }",
3770
+ ".den-toast-err .den-te-ic { color: var(--bad); }",
3771
+ ".den-toast-err .den-te-msg { display: flex; flex-direction: column; min-width: 0; overflow-wrap: anywhere; }",
3772
+ ".den-toast-err .den-te-id { font: 11px var(--mono); color: var(--faint); }",
3773
+ ".den-tbtn { border: 1px solid var(--border); background: var(--surface); color: var(--text);",
3774
+ " border-radius: var(--radius-button); padding: 2px 9px; font: 500 12.5px/1.45 var(--sans);",
3775
+ " cursor: pointer; white-space: nowrap; }",
3776
+ ".den-tbtn:hover { border-color: var(--border-strong); }",
3777
+ ".den-tx { border: 0; background: none; color: var(--faint); cursor: pointer; font: 16px/1 var(--sans); padding: 0 2px; }",
3778
+ ".den-tx:hover { color: var(--text); }",
3779
+ ".den-toast :focus-visible, .den-banners :focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }",
3780
+ // Banners (.note-banner): the unreachable server (R26d) and a newer
3781
+ // engine on disk (MD-2), at the top of the content.
3782
+ ".den-banners { flex: none; margin: 16px 24px 0; display: grid; gap: 8px; font: 14px/1.45 var(--sans); }",
3783
+ ".den-banners.den-banners-inset { margin: 0 0 16px; }",
3784
+ ".den-banners[hidden] { display: none; }",
3785
+ ".den-banner { display: flex; gap: 10px; align-items: baseline; flex-wrap: wrap; font-size: 12.5px;",
3786
+ " color: var(--muted); border: 1px dashed var(--border-strong); border-radius: var(--radius);",
3787
+ " padding: 8px 12px; }",
3788
+ ".den-banner b { color: var(--text); }",
3789
+ ".den-banner.warn { border-color: color-mix(in srgb, var(--warn) 55%, var(--border-strong)); }",
3790
+ ".den-banner .den-bn-ic { color: var(--warn); }",
3791
+ ".den-banner .den-bn-t { color: var(--text); }",
3792
+ ".den-banner .den-bn-n { font: 11px var(--mono); color: var(--faint); }",
3793
+ ".den-banner .den-tbtn { margin-left: auto; align-self: center; }",
3794
+ // The runs dock (.dock, .bar, .runs, .rtab, pre: R16), at the foot of
3795
+ // the content column; content-box as in the prototype.
3796
+ ".den-dock { flex: none; order: 99; position: sticky; bottom: 0; z-index: 14; min-width: 0;",
3797
+ " border-top: 1px solid var(--border); background: var(--surface); font: 14px/1.45 var(--sans);",
3798
+ " color: var(--text); box-sizing: content-box; }",
3799
+ ".den-dock :focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }",
3800
+ ".den-dock .den-bar { display: flex; align-items: center; gap: 10px; box-sizing: content-box;",
3801
+ " padding: 6px max(16px, env(safe-area-inset-right, 0px)) calc(6px + env(safe-area-inset-bottom, 0px)) 16px;",
3802
+ " font-size: 13px; color: var(--muted); }",
3803
+ ".den-dock .den-bar b { color: var(--text); font-weight: 500; }",
3804
+ ".den-dock .den-runs-l { display: flex; gap: 6px; overflow-x: auto; flex: 1; min-width: 0; }",
3805
+ ".den-dock .den-rtab-w { position: relative; display: inline-flex; flex: none; }",
3806
+ ".den-dock .den-rtab { display: inline-flex; gap: 6px; align-items: center; border: 1px solid var(--border);",
3807
+ " border-radius: 6px; padding: 2px 8px; background: var(--bg); cursor: pointer; white-space: nowrap;",
3808
+ " font: 12px var(--mono); color: inherit; box-sizing: content-box; margin: 0; }",
3809
+ ".den-dock .den-rtab[aria-selected='true'] { border-color: var(--accent); }",
3810
+ ".den-dock .den-rtab .den-dot { margin: 0; }",
3811
+ // A run closes from its tab (Local Dev's tabs did): room for the ×.
3812
+ ".den-dock .den-rtab.den-x { padding-right: 22px; }",
3813
+ ".den-dock .den-rtab-x { position: absolute; top: 0; bottom: 0; right: 4px; display: flex; align-items: center;",
3814
+ " padding: 0 3px; border: 0; background: none; cursor: pointer; font: 13px var(--sans); color: var(--faint); }",
3815
+ ".den-dock .den-rtab-x:hover { color: var(--bad); }",
3816
+ ".den-dock .den-dock-acts { display: flex; gap: 2px; flex: none; }",
3817
+ ".den-dock pre { margin: 0; height: 170px; overflow: auto; max-height: 50vh; padding: 10px 16px; font: 12px/1.5 var(--mono);",
3818
+ " background: var(--sunken); border-top: 1px solid var(--border); color: var(--muted);",
3819
+ " box-sizing: content-box; white-space: pre-wrap; word-break: break-all; }",
3820
+ ".den-dock pre a { color: var(--accent-text); }",
3821
+ // The panes' line classes, kept.
3822
+ ".den-dock pre .cmd { color: var(--accent-text); }",
3823
+ ".den-dock pre .err, .den-dock pre .done-fail { color: var(--bad); }",
3824
+ ".den-dock pre .warn { color: var(--warn); }",
3825
+ ".den-dock pre .ok, .den-dock pre .done-ok { color: var(--ok); }",
3826
+ ".den-dock pre .phase { color: var(--text); font-weight: 600; }",
3827
+ ".den-dock-grip { display: none; position: absolute; left: 0; right: 0; top: -3px; height: 6px;",
3828
+ " cursor: ns-resize; z-index: 1; }",
3829
+ ".den-dock.open .den-dock-grip { display: block; }",
3830
+ // Narrow (the prototype's 860 and 720px steps): chip labels hide, the
3831
+ // dots stay; then the scope hides and the search is its icon.
3832
+ // Den's chips are wider than the prototype's (the full version, real
3833
+ // counts): the scope's keys give way from 960px, not 860, so the bar
3834
+ // fits without the scope shrinking.
3835
+ "@media (max-width: 960px) { .den-scope .den-k { display: none; } }",
3836
+ "@media (max-width: 860px) { .den-scope .den-k, .den-scope .den-sep, .den-scope .den-opt { display: none; }",
3837
+ " .den-chip .den-lbl-l { display: none; } }",
3838
+ "@media (max-width: 720px) {",
3839
+ " body.den-has-shell, body.den-has-shell.den-rail-collapsed { --den-rail-w: 0px; --den-rail-cw: 244px; }",
3840
+ " .den-menu-btn { display: inline-flex; }",
3841
+ " .den-rail { transform: translateX(-105%); transition: transform .18s;",
3842
+ " box-shadow: var(--shadow); z-index: 42; }",
3843
+ " body.den-rail-open .den-rail { transform: none; }",
3844
+ " .den-scrim { position: fixed; inset: 0; z-index: 41; background: color-mix(in srgb, var(--bg) 60%, transparent); }",
3845
+ " body.den-rail-collapsed .den-rail .den-lbl, body.den-rail-collapsed .den-rail .den-gh,",
3846
+ " body.den-rail-collapsed .den-rail .den-bdg { position: static; width: auto; height: auto;",
3847
+ " clip: auto; overflow: hidden; }",
3848
+ " .den-collapse-btn { display: none; }",
3849
+ " .den-scope { display: none; }",
3850
+ " .den-search { flex: 0 0 auto; margin: 0 0 0 auto; }",
3851
+ " .den-search-ph, .den-search kbd { display: none; }",
3852
+ " .den-top .v-pill { display: none; }",
3853
+ // Never wider than the window (SA's dev.20 run: 429px at 400): a long
3854
+ // product name gives way first, the stale-server pill keeps its ⚠.
3855
+ " .den-brand { min-width: 0; flex: 0 1 auto; }",
3856
+ " .den-title { min-width: 0; overflow: hidden; text-overflow: ellipsis; }",
3857
+ " .den-top .stale-lbl { display: none; }",
3858
+ " .den-chips { flex-shrink: 0; }",
3859
+ "}",
3860
+ // Below 480px only the mark shows: the title stays the link's name,
3861
+ // visually hidden (SA's dev.21 run).
3862
+ "@media (max-width: 479.98px) {",
3863
+ " .den-title { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;",
3864
+ " overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }",
3865
+ "}",
3866
+ "@media (prefers-reduced-motion: reduce) { .den-rail { transition: none; } }",
3867
+ ].join("\n");
3868
+ document.head.appendChild(style);
3869
+ })();