pmx-canvas 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/Readme.md +9 -4
  3. package/dist/canvas/global.css +397 -0
  4. package/dist/canvas/index.js +89 -89
  5. package/dist/canvas/surface-theme.css +160 -0
  6. package/dist/json-render/index.css +1 -1
  7. package/dist/types/client/nodes/iframe-document-url.d.ts +1 -0
  8. package/dist/types/client/nodes/use-surface-frame.d.ts +13 -0
  9. package/dist/types/client/state/canvas-store.d.ts +1 -0
  10. package/dist/types/client/state/iframe-mode.d.ts +26 -0
  11. package/dist/types/client/types.d.ts +2 -1
  12. package/dist/types/server/canvas-db.d.ts +2 -1
  13. package/dist/types/server/html-surface.d.ts +8 -1
  14. package/dist/types/shared/themes.d.ts +27 -0
  15. package/docs/RELEASE.md +10 -4
  16. package/docs/cli.md +1 -1
  17. package/docs/environment.md +2 -1
  18. package/docs/http-api.md +29 -0
  19. package/docs/plans/plan-009-tech-debt-backlog.md +9 -1
  20. package/docs/screenshot.png +0 -0
  21. package/package.json +1 -1
  22. package/skills/pmx-canvas/SKILL.md +32 -4
  23. package/skills/pmx-canvas/references/full-reference.md +3 -3
  24. package/skills/pmx-canvas-testing/SKILL.md +3 -0
  25. package/src/cli/index.ts +3 -2
  26. package/src/client/App.tsx +201 -53
  27. package/src/client/index.tsx +5 -0
  28. package/src/client/nodes/ExtAppFrame.tsx +36 -1
  29. package/src/client/nodes/HtmlNode.tsx +5 -1
  30. package/src/client/nodes/McpAppNode.tsx +9 -2
  31. package/src/client/nodes/iframe-document-url.ts +62 -9
  32. package/src/client/nodes/use-surface-frame.ts +53 -0
  33. package/src/client/state/canvas-store.ts +5 -0
  34. package/src/client/state/iframe-mode.ts +118 -0
  35. package/src/client/state/sse-bridge.ts +8 -2
  36. package/src/client/theme/global.css +397 -0
  37. package/src/client/theme/surface-theme.css +160 -0
  38. package/src/client/types.ts +3 -1
  39. package/src/server/canvas-db.ts +3 -2
  40. package/src/server/html-surface.ts +17 -3
  41. package/src/server/operations/ops/canvas-wire.ts +6 -1
  42. package/src/server/server.ts +59 -2
  43. package/src/shared/themes.ts +58 -0
package/CHANGELOG.md CHANGED
@@ -3,6 +3,55 @@
3
3
  All notable changes to `pmx-canvas` are documented here. This project follows
4
4
  [Semantic Versioning](https://semver.org/).
5
5
 
6
+ ## [0.4.2] - 2026-08-04
7
+
8
+ ### Added
9
+
10
+ - Five new themes — `midnight`, `sepia`, `arctic`, `ember`, and `forest` —
11
+ alongside `dark`, `light`, and `high-contrast`, selectable from a new theme
12
+ picker in the toolbar, via `--theme=...`, or `PMX_CANVAS_THEME`.
13
+ - The toolbar now collapses on narrow screens and mobile into a single compact
14
+ bar with a "⋯" menu holding every secondary action as labeled touch-friendly
15
+ rows, replacing the wrapped two-bar layout.
16
+ - Amp orbs are auto-detected: when the server runs with `AMP_ORB` set, the
17
+ embedded canvas skips the (there-unreliable) iframe probe and uses `srcdoc`
18
+ mode directly.
19
+
20
+ ### Changed
21
+
22
+ - Refreshed the bundled skill's accuracy: tool tables verified against the
23
+ live 22-tool MCP surface, stale composite counts corrected, and the browser
24
+ guidance now documents the toolbar theme picker and the narrow-screen More
25
+ menu.
26
+
27
+ ### Fixed
28
+
29
+ - HTML surfaces now inline their theme stylesheet instead of linking it, so
30
+ srcdoc-rendered surfaces in nested-iframe hosts render styled instead of
31
+ blank (dark-on-dark).
32
+
33
+ ## [0.4.1] - 2026-08-04
34
+
35
+ ### Added
36
+
37
+ - Iframe-backed nodes (HTML, graph, json-render, MCP apps) now render inside
38
+ nested-iframe hosts like Amp orb portals: the canvas probes at boot whether
39
+ the embedding context loads iframe `src` URLs at all, and falls back to
40
+ fetching same-origin surfaces and rendering them inline via `srcdoc` when it
41
+ does not. Force a mode with `/workbench?iframe-mode=srcdoc` (or `=src`).
42
+
43
+ ### Fixed
44
+
45
+ - Ext-app nodes now survive a daemon restart without a full panel reload
46
+ (Finding S): the browser revalidates its frame-document URLs on every
47
+ reconnect and automatically re-mints them against the new server process
48
+ when they have gone stale.
49
+ - Best-effort mitigation for residual black ext-app tiles in WebKit host
50
+ panels (Finding N): after an app finishes booting, the canvas briefly nudges
51
+ the iframe's layout size so WebKit must re-rasterize the frame — the same
52
+ mechanism as the manual enlarge-then-close unstick, without the reload. The
53
+ nudge is recorded as `repaint-nudge` in the ext-app recovery trail.
54
+
6
55
  ## [0.4.0] - 2026-08-04
7
56
 
8
57
  ### Breaking
@@ -2831,6 +2880,8 @@ otherwise have to discover by trial and error.
2831
2880
  - Regression coverage for snapshot flat-`id` aliases on both MCP and
2832
2881
  HTTP surfaces, plus async / top-level-`await` WebView script bodies.
2833
2882
 
2883
+ [0.4.2]: https://github.com/pskoett/pmx-canvas/releases/tag/v0.4.2
2884
+ [0.4.1]: https://github.com/pskoett/pmx-canvas/releases/tag/v0.4.1
2834
2885
  [0.4.0]: https://github.com/pskoett/pmx-canvas/releases/tag/v0.4.0
2835
2886
  [0.3.4]: https://github.com/pskoett/pmx-canvas/releases/tag/v0.3.4
2836
2887
  [0.3.3]: https://github.com/pskoett/pmx-canvas/releases/tag/v0.3.3
package/Readme.md CHANGED
@@ -144,10 +144,15 @@ surfaces (the core never imports a host SDK).
144
144
  built-in browser pane alongside the PMX MCP server, same zero-adapter recipe
145
145
  as Codex: open `http://localhost:4313/workbench` in the app's browser and the
146
146
  live board, pins, and Ghost Cursor all work in-panel.
147
- - **Amp orb portals** (and other buffering proxies) — the canvas detects when
148
- the live event stream is being buffered by the proxy and automatically
149
- switches to a polling transport, so the board boots and stays live through
150
- the portal with no proxy shims (`/workbench?transport=poll` forces it).
147
+ - **Amp orb portals** (and other buffering proxies / nested-iframe embeds) —
148
+ the canvas detects when the live event stream is being buffered by the proxy
149
+ and automatically switches to a polling transport, so the board boots and
150
+ stays live through the portal with no proxy shims
151
+ (`/workbench?transport=poll` forces it). It also detects when the nested
152
+ iframe embed blocks `src`-URL iframes entirely and renders HTML, graph,
153
+ json-render, and app surfaces inline via `srcdoc` instead
154
+ (`?iframe-mode=srcdoc` forces it), so iframe-backed nodes work in the portal
155
+ too.
151
156
 
152
157
  The contract is host-agnostic, so a new host plugs in the same way: map its
153
158
  hooks, canvas, and session APIs onto PMX's AX primitives — no core changes.
@@ -171,6 +171,281 @@
171
171
  --c-annotation: #ffff00;
172
172
  }
173
173
 
174
+ :root[data-theme="midnight"] {
175
+ /* ── Core palette (midnight · deep indigo night) ─────────── */
176
+ --c-bg: #0A0D1C;
177
+ --c-panel: #12172E;
178
+ --c-panel-soft: #0D1224;
179
+ --c-line: #232B4A;
180
+ --c-text: #E6E9F7;
181
+ --c-text-soft: #C6CCE8;
182
+ --c-muted: #8D96BC;
183
+ --c-dim: #5B6383;
184
+ --c-accent: #8B96FF;
185
+ --c-ok: #34D399;
186
+ --c-warn: #F5C452;
187
+ --c-warn-alt: #FFB300;
188
+ --c-danger: #FF6B8A;
189
+ --c-purple: #B388FF;
190
+ --c-thinking: #B388FF;
191
+ --c-subagent: #22D3EE;
192
+ --c-input-bg: #0D1224;
193
+ --c-dot-grid: rgba(139, 150, 255, 0.08);
194
+ --c-panel-overlay: rgba(10, 13, 28, 0.6);
195
+ --c-panel-glass: rgba(18, 23, 46, 0.92);
196
+ --c-shadow: rgba(0, 0, 0, 0.3);
197
+ --c-shadow-heavy: rgba(0, 0, 0, 0.5);
198
+ --c-glow-accent: rgba(139, 150, 255, 0.15);
199
+ --c-surface-hover: rgba(255, 255, 255, 0.06);
200
+ --c-surface-subtle: rgba(255, 255, 255, 0.03);
201
+ --c-surface-th: rgba(255, 255, 255, 0.04);
202
+ --c-accent-8: rgba(139, 150, 255, 0.08);
203
+ --c-accent-10: rgba(139, 150, 255, 0.1);
204
+ --c-accent-12: rgba(139, 150, 255, 0.12);
205
+ --c-accent-15: rgba(139, 150, 255, 0.15);
206
+ --c-accent-25: rgba(139, 150, 255, 0.25);
207
+ --c-accent-30: rgba(139, 150, 255, 0.3);
208
+ --c-accent-40: rgba(139, 150, 255, 0.4);
209
+ --c-ok-10: rgba(52, 211, 153, 0.1);
210
+ --c-ok-12: rgba(52, 211, 153, 0.12);
211
+ --c-ok-20: rgba(52, 211, 153, 0.2);
212
+ --c-ok-25: rgba(52, 211, 153, 0.25);
213
+ --c-warn-10: rgba(245, 196, 82, 0.1);
214
+ --c-warn-12: rgba(245, 196, 82, 0.12);
215
+ --c-warn-15: rgba(245, 196, 82, 0.15);
216
+ --c-warn-20: rgba(245, 196, 82, 0.2);
217
+ --c-warn-22: rgba(245, 196, 82, 0.22);
218
+ --c-warn-30: rgba(245, 196, 82, 0.3);
219
+ --c-warn-60: rgba(245, 196, 82, 0.6);
220
+ --c-danger-12: rgba(255, 107, 138, 0.12);
221
+ --c-line-30: rgba(35, 43, 74, 0.3);
222
+ --c-contrast-fg: #0A0D1C;
223
+ --c-accent-hover: #A3ACFF;
224
+ --c-warn-hover: #F7D06E;
225
+ --c-canvas-wash: linear-gradient(180deg, rgba(255, 255, 255, 0.02), rgba(0, 0, 0, 0));
226
+ --c-annotation: #EDEDF7;
227
+ }
228
+
229
+ :root[data-theme="sepia"] {
230
+ /* ── Core palette (sepia · warm amber paper) ─────────────── */
231
+ --c-bg: #F2E7D5;
232
+ --c-panel: #EBDCC2;
233
+ --c-panel-soft: #E8D8BC;
234
+ --c-line: #D3C1A0;
235
+ --c-text: #2E2416;
236
+ --c-text-soft: #554833;
237
+ --c-muted: #77664C;
238
+ --c-dim: #978870;
239
+ --c-accent: #B4632C;
240
+ --c-ok: #3E7D4F;
241
+ --c-warn: #A97B1E;
242
+ --c-warn-alt: #8F6A0D;
243
+ --c-danger: #B3402E;
244
+ --c-purple: #8456B0;
245
+ --c-thinking: #8456B0;
246
+ --c-subagent: #0E7C7B;
247
+ --c-input-bg: #F7EEDD;
248
+ --c-dot-grid: rgba(46, 36, 22, 0.08);
249
+ --c-panel-overlay: rgba(242, 231, 213, 0.88);
250
+ --c-panel-glass: rgba(235, 220, 194, 0.88);
251
+ --c-shadow: rgba(46, 36, 22, 0.08);
252
+ --c-shadow-heavy: rgba(46, 36, 22, 0.16);
253
+ --c-glow-accent: rgba(180, 99, 44, 0.12);
254
+ --c-surface-hover: rgba(46, 36, 22, 0.06);
255
+ --c-surface-subtle: rgba(46, 36, 22, 0.03);
256
+ --c-surface-th: rgba(46, 36, 22, 0.04);
257
+ --c-accent-8: rgba(180, 99, 44, 0.08);
258
+ --c-accent-10: rgba(180, 99, 44, 0.1);
259
+ --c-accent-12: rgba(180, 99, 44, 0.12);
260
+ --c-accent-15: rgba(180, 99, 44, 0.15);
261
+ --c-accent-25: rgba(180, 99, 44, 0.25);
262
+ --c-accent-30: rgba(180, 99, 44, 0.3);
263
+ --c-accent-40: rgba(180, 99, 44, 0.4);
264
+ --c-ok-10: rgba(62, 125, 79, 0.1);
265
+ --c-ok-12: rgba(62, 125, 79, 0.12);
266
+ --c-ok-20: rgba(62, 125, 79, 0.2);
267
+ --c-ok-25: rgba(62, 125, 79, 0.25);
268
+ --c-warn-10: rgba(169, 123, 30, 0.1);
269
+ --c-warn-12: rgba(169, 123, 30, 0.12);
270
+ --c-warn-15: rgba(169, 123, 30, 0.15);
271
+ --c-warn-20: rgba(169, 123, 30, 0.2);
272
+ --c-warn-22: rgba(169, 123, 30, 0.22);
273
+ --c-warn-30: rgba(169, 123, 30, 0.3);
274
+ --c-warn-60: rgba(169, 123, 30, 0.6);
275
+ --c-danger-12: rgba(179, 64, 46, 0.12);
276
+ --c-line-30: rgba(211, 193, 160, 0.5);
277
+ --c-contrast-fg: #F2E7D5;
278
+ --c-accent-hover: #C4763F;
279
+ --c-warn-hover: #BD9037;
280
+ --c-canvas-wash: linear-gradient(180deg, rgba(255, 255, 255, 0.12), rgba(46, 36, 22, 0.02));
281
+ --c-annotation: #2E2416;
282
+ }
283
+
284
+ :root[data-theme="arctic"] {
285
+ /* ── Core palette (arctic · cool slate + ice) ────────────── */
286
+ --c-bg: #20242E;
287
+ --c-panel: #2A2F3C;
288
+ --c-panel-soft: #242936;
289
+ --c-line: #3A4152;
290
+ --c-text: #E9EDF4;
291
+ --c-text-soft: #CBD3E1;
292
+ --c-muted: #939DB1;
293
+ --c-dim: #667082;
294
+ --c-accent: #7FC8DE;
295
+ --c-ok: #74C596;
296
+ --c-warn: #E8C476;
297
+ --c-warn-alt: #E0A93F;
298
+ --c-danger: #D9707C;
299
+ --c-purple: #B48EAD;
300
+ --c-thinking: #B48EAD;
301
+ --c-subagent: #6FD6C4;
302
+ --c-input-bg: #242936;
303
+ --c-dot-grid: rgba(127, 200, 222, 0.08);
304
+ --c-panel-overlay: rgba(32, 36, 46, 0.6);
305
+ --c-panel-glass: rgba(42, 47, 60, 0.92);
306
+ --c-shadow: rgba(0, 0, 0, 0.3);
307
+ --c-shadow-heavy: rgba(0, 0, 0, 0.5);
308
+ --c-glow-accent: rgba(127, 200, 222, 0.15);
309
+ --c-surface-hover: rgba(255, 255, 255, 0.06);
310
+ --c-surface-subtle: rgba(255, 255, 255, 0.03);
311
+ --c-surface-th: rgba(255, 255, 255, 0.04);
312
+ --c-accent-8: rgba(127, 200, 222, 0.08);
313
+ --c-accent-10: rgba(127, 200, 222, 0.1);
314
+ --c-accent-12: rgba(127, 200, 222, 0.12);
315
+ --c-accent-15: rgba(127, 200, 222, 0.15);
316
+ --c-accent-25: rgba(127, 200, 222, 0.25);
317
+ --c-accent-30: rgba(127, 200, 222, 0.3);
318
+ --c-accent-40: rgba(127, 200, 222, 0.4);
319
+ --c-ok-10: rgba(116, 197, 150, 0.1);
320
+ --c-ok-12: rgba(116, 197, 150, 0.12);
321
+ --c-ok-20: rgba(116, 197, 150, 0.2);
322
+ --c-ok-25: rgba(116, 197, 150, 0.25);
323
+ --c-warn-10: rgba(232, 196, 118, 0.1);
324
+ --c-warn-12: rgba(232, 196, 118, 0.12);
325
+ --c-warn-15: rgba(232, 196, 118, 0.15);
326
+ --c-warn-20: rgba(232, 196, 118, 0.2);
327
+ --c-warn-22: rgba(232, 196, 118, 0.22);
328
+ --c-warn-30: rgba(232, 196, 118, 0.3);
329
+ --c-warn-60: rgba(232, 196, 118, 0.6);
330
+ --c-danger-12: rgba(217, 112, 124, 0.12);
331
+ --c-line-30: rgba(58, 65, 82, 0.3);
332
+ --c-contrast-fg: #20242E;
333
+ --c-accent-hover: #9AD5E7;
334
+ --c-warn-hover: #EDD194;
335
+ --c-canvas-wash: linear-gradient(180deg, rgba(255, 255, 255, 0.02), rgba(0, 0, 0, 0));
336
+ --c-annotation: #ECEFF4;
337
+ }
338
+
339
+ :root[data-theme="ember"] {
340
+ /* ── Core palette (ember · charcoal + vivid warm orange) ─── */
341
+ --c-bg: #0E0E11;
342
+ --c-panel: #17181D;
343
+ --c-panel-soft: #121317;
344
+ --c-line: #2A2C34;
345
+ --c-text: #ECEDF0;
346
+ --c-text-soft: #C9CCD4;
347
+ --c-muted: #8E939F;
348
+ --c-dim: #5F646E;
349
+ --c-accent: #FF5C2E;
350
+ --c-ok: #4CC38A;
351
+ --c-warn: #F5B944;
352
+ --c-warn-alt: #FFB300;
353
+ --c-danger: #F0566A;
354
+ --c-purple: #A78BFA;
355
+ --c-thinking: #A78BFA;
356
+ --c-subagent: #38BDF8;
357
+ --c-input-bg: #121317;
358
+ --c-dot-grid: rgba(255, 92, 46, 0.08);
359
+ --c-panel-overlay: rgba(14, 14, 17, 0.6);
360
+ --c-panel-glass: rgba(23, 24, 29, 0.92);
361
+ --c-shadow: rgba(0, 0, 0, 0.3);
362
+ --c-shadow-heavy: rgba(0, 0, 0, 0.5);
363
+ --c-glow-accent: rgba(255, 92, 46, 0.15);
364
+ --c-surface-hover: rgba(255, 255, 255, 0.06);
365
+ --c-surface-subtle: rgba(255, 255, 255, 0.03);
366
+ --c-surface-th: rgba(255, 255, 255, 0.04);
367
+ --c-accent-8: rgba(255, 92, 46, 0.08);
368
+ --c-accent-10: rgba(255, 92, 46, 0.1);
369
+ --c-accent-12: rgba(255, 92, 46, 0.12);
370
+ --c-accent-15: rgba(255, 92, 46, 0.15);
371
+ --c-accent-25: rgba(255, 92, 46, 0.25);
372
+ --c-accent-30: rgba(255, 92, 46, 0.3);
373
+ --c-accent-40: rgba(255, 92, 46, 0.4);
374
+ --c-ok-10: rgba(76, 195, 138, 0.1);
375
+ --c-ok-12: rgba(76, 195, 138, 0.12);
376
+ --c-ok-20: rgba(76, 195, 138, 0.2);
377
+ --c-ok-25: rgba(76, 195, 138, 0.25);
378
+ --c-warn-10: rgba(245, 185, 68, 0.1);
379
+ --c-warn-12: rgba(245, 185, 68, 0.12);
380
+ --c-warn-15: rgba(245, 185, 68, 0.15);
381
+ --c-warn-20: rgba(245, 185, 68, 0.2);
382
+ --c-warn-22: rgba(245, 185, 68, 0.22);
383
+ --c-warn-30: rgba(245, 185, 68, 0.3);
384
+ --c-warn-60: rgba(245, 185, 68, 0.6);
385
+ --c-danger-12: rgba(240, 86, 106, 0.12);
386
+ --c-line-30: rgba(42, 44, 52, 0.3);
387
+ --c-contrast-fg: #0E0E11;
388
+ --c-accent-hover: #FF7A52;
389
+ --c-warn-hover: #F7C868;
390
+ --c-canvas-wash: linear-gradient(180deg, rgba(255, 255, 255, 0.02), rgba(0, 0, 0, 0));
391
+ --c-annotation: #F2F2F4;
392
+ }
393
+
394
+ :root[data-theme="forest"] {
395
+ /* ── Core palette (forest · deep green) ──────────────────── */
396
+ --c-bg: #0C1712;
397
+ --c-panel: #14231B;
398
+ --c-panel-soft: #101D16;
399
+ --c-line: #26402F;
400
+ --c-text: #E4EFE7;
401
+ --c-text-soft: #C4D6CA;
402
+ --c-muted: #8BA695;
403
+ --c-dim: #5D7566;
404
+ --c-accent: #5CCF8F;
405
+ --c-ok: #74D99F;
406
+ --c-warn: #E0B84F;
407
+ --c-warn-alt: #D9A421;
408
+ --c-danger: #E57373;
409
+ --c-purple: #A88FE0;
410
+ --c-thinking: #A88FE0;
411
+ --c-subagent: #4FD8CE;
412
+ --c-input-bg: #101D16;
413
+ --c-dot-grid: rgba(92, 207, 143, 0.08);
414
+ --c-panel-overlay: rgba(12, 23, 18, 0.6);
415
+ --c-panel-glass: rgba(20, 35, 27, 0.92);
416
+ --c-shadow: rgba(0, 0, 0, 0.3);
417
+ --c-shadow-heavy: rgba(0, 0, 0, 0.5);
418
+ --c-glow-accent: rgba(92, 207, 143, 0.15);
419
+ --c-surface-hover: rgba(255, 255, 255, 0.06);
420
+ --c-surface-subtle: rgba(255, 255, 255, 0.03);
421
+ --c-surface-th: rgba(255, 255, 255, 0.04);
422
+ --c-accent-8: rgba(92, 207, 143, 0.08);
423
+ --c-accent-10: rgba(92, 207, 143, 0.1);
424
+ --c-accent-12: rgba(92, 207, 143, 0.12);
425
+ --c-accent-15: rgba(92, 207, 143, 0.15);
426
+ --c-accent-25: rgba(92, 207, 143, 0.25);
427
+ --c-accent-30: rgba(92, 207, 143, 0.3);
428
+ --c-accent-40: rgba(92, 207, 143, 0.4);
429
+ --c-ok-10: rgba(116, 217, 159, 0.1);
430
+ --c-ok-12: rgba(116, 217, 159, 0.12);
431
+ --c-ok-20: rgba(116, 217, 159, 0.2);
432
+ --c-ok-25: rgba(116, 217, 159, 0.25);
433
+ --c-warn-10: rgba(224, 184, 79, 0.1);
434
+ --c-warn-12: rgba(224, 184, 79, 0.12);
435
+ --c-warn-15: rgba(224, 184, 79, 0.15);
436
+ --c-warn-20: rgba(224, 184, 79, 0.2);
437
+ --c-warn-22: rgba(224, 184, 79, 0.22);
438
+ --c-warn-30: rgba(224, 184, 79, 0.3);
439
+ --c-warn-60: rgba(224, 184, 79, 0.6);
440
+ --c-danger-12: rgba(229, 115, 115, 0.12);
441
+ --c-line-30: rgba(38, 64, 47, 0.3);
442
+ --c-contrast-fg: #0C1712;
443
+ --c-accent-hover: #7BDAA4;
444
+ --c-warn-hover: #E8C871;
445
+ --c-canvas-wash: linear-gradient(180deg, rgba(255, 255, 255, 0.02), rgba(0, 0, 0, 0));
446
+ --c-annotation: #EDF5EF;
447
+ }
448
+
174
449
  * {
175
450
  box-sizing: border-box;
176
451
  margin: 0;
@@ -669,6 +944,7 @@ body,
669
944
  }
670
945
 
671
946
  .toolbar-group {
947
+ position: relative;
672
948
  display: flex;
673
949
  flex-wrap: wrap;
674
950
  align-items: center;
@@ -681,6 +957,127 @@ body,
681
957
  display: block;
682
958
  }
683
959
 
960
+ /* ── Toolbar popover menus (theme picker + narrow-screen More menu) ── */
961
+ .toolbar-menu {
962
+ position: absolute;
963
+ top: calc(100% + 8px);
964
+ right: 0;
965
+ z-index: 1200;
966
+ display: flex;
967
+ flex-direction: column;
968
+ gap: 2px;
969
+ min-width: 224px;
970
+ max-height: min(70vh, 560px);
971
+ overflow-y: auto;
972
+ padding: 6px;
973
+ background: var(--c-panel-glass);
974
+ backdrop-filter: blur(12px);
975
+ border: 1px solid var(--c-line);
976
+ border-radius: var(--radius);
977
+ box-shadow: 0 18px 48px var(--c-shadow-heavy);
978
+ }
979
+
980
+ .toolbar-menu-item {
981
+ display: flex;
982
+ align-items: center;
983
+ gap: 10px;
984
+ width: 100%;
985
+ min-height: 40px;
986
+ padding: 8px 12px;
987
+ background: none;
988
+ border: 1px solid transparent;
989
+ border-radius: var(--radius-sm);
990
+ color: var(--c-text-soft);
991
+ font: inherit;
992
+ font-size: 13px;
993
+ text-align: left;
994
+ cursor: pointer;
995
+ }
996
+
997
+ .toolbar-menu-item:hover {
998
+ background: var(--c-surface-hover);
999
+ color: var(--c-text);
1000
+ border-color: var(--c-line);
1001
+ }
1002
+
1003
+ .toolbar-menu-item.active {
1004
+ color: var(--c-accent);
1005
+ }
1006
+
1007
+ .toolbar-menu-item svg {
1008
+ flex-shrink: 0;
1009
+ }
1010
+
1011
+ .toolbar-menu-check {
1012
+ margin-left: auto;
1013
+ color: var(--c-accent);
1014
+ }
1015
+
1016
+ .toolbar-menu-separator {
1017
+ height: 1px;
1018
+ margin: 4px 8px;
1019
+ background: var(--c-line);
1020
+ flex-shrink: 0;
1021
+ }
1022
+
1023
+ .toolbar-menu-heading {
1024
+ padding: 6px 12px 2px;
1025
+ font-size: 10px;
1026
+ letter-spacing: 0.08em;
1027
+ text-transform: uppercase;
1028
+ color: var(--c-dim);
1029
+ }
1030
+
1031
+ .theme-swatch {
1032
+ display: inline-flex;
1033
+ align-items: center;
1034
+ justify-content: center;
1035
+ width: 18px;
1036
+ height: 18px;
1037
+ border: 1px solid var(--c-line);
1038
+ border-radius: 50%;
1039
+ flex-shrink: 0;
1040
+ }
1041
+
1042
+ .theme-swatch-dot {
1043
+ width: 7px;
1044
+ height: 7px;
1045
+ border-radius: 50%;
1046
+ }
1047
+
1048
+ .toolbar-more-glyph {
1049
+ display: block;
1050
+ font-size: 16px;
1051
+ font-weight: 700;
1052
+ line-height: 1;
1053
+ }
1054
+
1055
+ /* Wide screens: the full two-bar toolbar; the More trigger stays hidden. */
1056
+ .toolbar-collapsible {
1057
+ display: contents;
1058
+ }
1059
+
1060
+ .toolbar-more {
1061
+ display: none;
1062
+ }
1063
+
1064
+ /* Narrow screens / embedded panels: ONE compact bar (brand, status, view
1065
+ controls) — every secondary control folds into the More menu instead of
1066
+ wrapping into a second toolbar row. */
1067
+ @media (max-width: 900px) {
1068
+ .toolbar-collapsible {
1069
+ display: none;
1070
+ }
1071
+
1072
+ .toolbar-actions-bar {
1073
+ display: none;
1074
+ }
1075
+
1076
+ .toolbar-more {
1077
+ display: contents;
1078
+ }
1079
+ }
1080
+
684
1081
  @media (max-width: 900px) {
685
1082
  .toolbar-tooltip {
686
1083
  min-width: 150px;