@monster-orange/den 0.2.8 → 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.
- package/CHANGELOG.md +1547 -0
- package/README.md +4 -2
- package/bin/den +2 -1
- package/docs/distributions.md +507 -4
- package/make/den.mk +19 -4
- package/make/journeys.mk +2 -1
- package/make/local_dev.mk +6 -13
- package/make/secrets-1password.mk +18 -0
- package/make/test.mk +1 -1
- package/package.json +9 -2
- package/schemas/den.plugin.v1.json +156 -0
- package/schemas/den.theme.v1.json +269 -0
- package/scripts/auth-configure.ts +16 -8
- package/scripts/heal-config.ts +0 -5
- package/scripts/setup-1password.ts +41 -19
- package/scripts/setup-secrets.ts +67 -101
- package/src/pillars/api/global-setup.ts +1 -1
- package/src/pillars/journeys/sandbox.ts +23 -18
- package/src/pillars/journeys/tools/browser-session.ts +2 -2
- package/src/scripts/setup-default-workspace.ts +2 -2
- package/src/shared/auth/headless-login.ts +1 -1
- package/src/shared/auth/sudo-vault.ts +93 -31
- package/src/shared/auth/token-providers/index.ts +7 -3
- package/src/shared/brand.ts +7 -0
- package/src/shared/config-error.ts +33 -3
- package/src/shared/constants.ts +3 -24
- package/src/shared/den-config.ts +141 -5
- package/src/shared/edge/config.ts +147 -21
- package/src/shared/edge/exec.ts +31 -0
- package/src/shared/edge/included.ts +169 -0
- package/src/shared/edge/render.ts +69 -14
- package/src/shared/edge/runtime.ts +128 -121
- package/src/shared/edge/status.ts +68 -8
- package/src/shared/edge/tls.ts +328 -0
- package/src/shared/edge/types.ts +25 -9
- package/src/shared/environment-manager/default-workspace.ts +16 -1
- package/src/shared/environment-manager/health.ts +73 -38
- package/src/shared/environment-manager/scope.ts +168 -0
- package/src/shared/environment-manager/service-runner.ts +789 -43
- package/src/shared/environment-manager/up-targets.ts +132 -0
- package/src/shared/health/rollup.ts +48 -0
- package/src/shared/open-url.ts +21 -2
- package/src/shared/org-sources.ts +78 -59
- package/src/shared/org.ts +53 -5
- package/src/shared/plugins/manifest.ts +406 -0
- package/src/shared/plugins/store.ts +358 -0
- package/src/shared/process/legacy-adopt.ts +285 -0
- package/src/shared/process/process-group.ts +202 -0
- package/src/shared/process/spawn-streaming.ts +7 -0
- package/src/shared/secrets/doppler.ts +1 -1
- package/src/shared/secrets/index.ts +279 -50
- package/src/shared/secrets/none.ts +56 -0
- package/src/shared/secrets/strategies.ts +202 -0
- package/src/shared/secrets/types.ts +1 -1
- package/src/shared/smoke/git-remote.ts +40 -0
- package/src/shared/stacks/manifest-commands.ts +1 -1
- package/src/shared/stacks/namespace.ts +178 -0
- package/src/shared/stacks/owners.ts +303 -0
- package/src/shared/stacks/render.ts +8 -3
- package/src/shared/stacks/runtime.ts +100 -16
- package/src/shared/stacks/status.ts +63 -12
- package/src/shared/strategies/registry.ts +89 -0
- package/src/shared/theme/contract.ts +538 -0
- package/src/shared/types/index.ts +12 -3
- package/src/ui/cli/create.ts +24 -7
- package/src/ui/cli/doctor-command.ts +131 -0
- package/src/ui/cli/edge-command.ts +56 -10
- package/src/ui/cli/flag-parser.ts +7 -2
- package/src/ui/cli/health-display.ts +25 -0
- package/src/ui/cli/index.ts +548 -60
- package/src/ui/cli/notice.ts +20 -0
- package/src/ui/cli/plugin-command.ts +849 -0
- package/src/ui/cli/stack-command.ts +63 -2
- package/src/ui/cli/test-runner.ts +11 -9
- package/src/ui/cli/theme-command.ts +136 -0
- package/src/ui/cli/web-command.ts +47 -23
- package/src/ui/mcp/api.ts +28 -2
- package/src/ui/mcp/scope.ts +22 -1
- package/src/ui/mcp/tools.ts +50 -7
- package/src/ui/server/appearance-prefs.ts +95 -0
- package/src/ui/server/daemon-reaper.ts +29 -1
- package/src/ui/server/engine-stamp.ts +83 -0
- package/src/ui/server/github-service.ts +23 -44
- package/src/ui/server/graph-page.ts +20 -0
- package/src/ui/server/graph-prefs.ts +44 -1
- package/src/ui/server/http-errors.ts +293 -0
- package/src/ui/server/http-helpers.ts +22 -3
- package/src/ui/server/index.ts +532 -51
- package/src/ui/server/public/appearance.js +531 -0
- package/src/ui/server/public/edge-page.js +248 -0
- package/src/ui/server/public/edge.html +251 -0
- package/src/ui/server/public/environments.html +65 -83
- package/src/ui/server/public/error-page.js +247 -0
- package/src/ui/server/public/error.html +260 -0
- package/src/ui/server/public/graph-model.js +332 -14
- package/src/ui/server/public/graph.css +141 -47
- package/src/ui/server/public/graph.js +223 -44
- package/src/ui/server/public/header.js +396 -600
- package/src/ui/server/public/health-fit.js +248 -0
- package/src/ui/server/public/health-rollup.js +128 -0
- package/src/ui/server/public/i18n/es.js +287 -0
- package/src/ui/server/public/i18n/ja.js +276 -0
- package/src/ui/server/public/index.html +139 -438
- package/src/ui/server/public/local-dev.html +3468 -4044
- package/src/ui/server/public/logs.html +71 -208
- package/src/ui/server/public/panel.js +14 -2
- package/src/ui/server/public/release.html +175 -389
- package/src/ui/server/public/services.html +596 -770
- package/src/ui/server/public/settings-page.js +759 -0
- package/src/ui/server/public/settings-registry.js +521 -0
- package/src/ui/server/public/settings.html +590 -0
- package/src/ui/server/public/shell.js +3869 -0
- package/src/ui/server/public/topbar.js +1413 -0
- package/src/ui/server/public/version-label.js +12 -0
- package/src/ui/server/public/workspace-picker.js +74 -34
- package/src/ui/server/readonly-routes.ts +12 -1
- package/src/ui/server/services-list.ts +94 -17
- package/src/ui/server/settings-distribution.ts +74 -0
- package/src/ui/server/shell-prefs.ts +192 -0
- package/src/ui/server/theme-routes.ts +292 -0
- package/src/ui/server/theme-runtime.ts +407 -0
- package/src/ui/server/ui-assets.ts +16 -1
- package/src/ui/server/ui-language.ts +50 -0
- package/src/ui/server/workspace-routes.ts +45 -3
- package/themes/den/den-theme.json +72 -0
- package/themes/den/favicon.ico +0 -0
- 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 { "&": "&", "<": "<", ">": ">", '"': """ }[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
|
+
})();
|