flexdesk 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/css/base.css +1484 -181
  2. package/css/flexdesk.css +1311 -18
  3. package/css/overrides.css +44 -0
  4. package/css/tokens.css +45 -0
  5. package/dist/charts.js +5 -3
  6. package/dist/charts.js.map +1 -1
  7. package/dist/{chunk-DVU44T77.js → chunk-ELXVW542.js} +196 -75
  8. package/dist/chunk-ELXVW542.js.map +7 -0
  9. package/dist/chunk-LH5TSOZW.js +1237 -0
  10. package/dist/chunk-LH5TSOZW.js.map +7 -0
  11. package/dist/{chunk-TLZUUFOE.js → chunk-O5OHMWBB.js} +10 -2
  12. package/dist/chunk-O5OHMWBB.js.map +7 -0
  13. package/dist/{chunk-CT4YXXLP.js → chunk-QIU5S2RU.js} +371 -73
  14. package/dist/chunk-QIU5S2RU.js.map +7 -0
  15. package/dist/chunk-QNQHQ24V.js +408 -0
  16. package/dist/chunk-QNQHQ24V.js.map +7 -0
  17. package/dist/{chunk-DRYCDMEG.js → chunk-XKDTIT4Q.js} +168 -12
  18. package/dist/chunk-XKDTIT4Q.js.map +7 -0
  19. package/dist/editor.js +3 -380
  20. package/dist/editor.js.map +3 -3
  21. package/dist/flexdesk.css +1311 -18
  22. package/dist/tiles.js +168 -41
  23. package/dist/tiles.js.map +2 -2
  24. package/dist/tokens.css +45 -0
  25. package/dist/widgets.js +44 -14
  26. package/dist/widgets.js.map +2 -2
  27. package/dist/wm.js +2983 -142
  28. package/dist/wm.js.map +4 -4
  29. package/package.json +3 -2
  30. package/src/charts/chart_types.js +167 -0
  31. package/src/charts/plotly_wrapper.js +178 -10
  32. package/src/editor/notebook_tab_bar.js +39 -3
  33. package/src/tiles/tile_base.js +143 -35
  34. package/src/tiles/tile_grid.js +52 -1
  35. package/src/tiling/command_palette.js +71 -18
  36. package/src/tiling/desktops.js +36 -12
  37. package/src/tiling/keymap.js +24 -4
  38. package/src/tiling/shell.js +135 -24
  39. package/src/tiling/tab_strip.js +184 -0
  40. package/src/tiling/tile_breadcrumb.js +34 -2
  41. package/src/tiling/tile_renderer.js +1386 -21
  42. package/src/tiling/tile_tab_menu.js +101 -0
  43. package/src/tiling/tile_tree.js +82 -0
  44. package/src/tiling/wm.js +2352 -74
  45. package/src/ui/components/action_dropdown.js +34 -3
  46. package/src/ui/components/autocomplete_field.js +65 -13
  47. package/src/ui/components/context_menu.js +79 -8
  48. package/src/ui/components/data_table.js +508 -84
  49. package/src/ui/components/managed_window.js +928 -36
  50. package/src/ui/components/modal.js +214 -8
  51. package/dist/chunk-CT4YXXLP.js.map +0 -7
  52. package/dist/chunk-DRYCDMEG.js.map +0 -7
  53. package/dist/chunk-DVU44T77.js.map +0 -7
  54. package/dist/chunk-TLZUUFOE.js.map +0 -7
  55. package/dist/chunk-UCJ2WD4D.js +0 -625
  56. package/dist/chunk-UCJ2WD4D.js.map +0 -7
package/css/base.css CHANGED
@@ -1131,6 +1131,13 @@ body.twm-dt-col-resizing {
1131
1131
  .twm-collapsible-content {
1132
1132
  display: none;
1133
1133
  }
1134
+ /* C8. The rule `toggleCategory()` has always assumed and no stylesheet has ever
1135
+ * provided. Without it the collapsible primitive is non-functional as shipped:
1136
+ * the toggle adds `.visible` and nothing acts on the class, so a category that
1137
+ * "opens" stays `display: none`. One rule; it was simply missing. */
1138
+ .twm-collapsible-content.visible {
1139
+ display: block;
1140
+ }
1134
1141
  /* ============================================
1135
1142
  Autocomplete Field Component (legacy-variable style)
1136
1143
  ============================================ */
@@ -2342,230 +2349,806 @@ body.twm-window-maximized .twm-resize-handle {
2342
2349
  cursor: default;
2343
2350
  }
2344
2351
 
2345
- /* ── from results_widgets.css ───────────────────────── */
2346
- /* Error State */
2347
- .twm-tile-error {
2352
+ /* ═══════════════════════════════════════════════════════════════════════
2353
+ @flexdesk/tiles THE TILE GRID'S CHROME
2354
+
2355
+ THIS STYLESHEET HAD NEVER EXISTED. `src/tiles/` was extracted from
2356
+ EcoAgent's `ui/js/results/` with its JavaScript and without its CSS, so
2357
+ every consumer of `@flexdesk/tiles` got a `TileGrid` that ran, dragged,
2358
+ resized, saved layouts — and drew a column of unstyled `<div>`s.
2359
+ `TileGrid._init()` sets `--grid-columns`, `--grid-row-height` and
2360
+ `--grid-gap` on the grid element (`src/tiles/tile_grid.js`), and until
2361
+ these rules landed NOTHING IN ANY STYLESHEET READ THEM: no
2362
+ `display: grid`, no `grid-template-columns`, no `grid-auto-rows`. That is
2363
+ the CSS-side half of this repository's signature bug — not a class the JS
2364
+ sets and no rule defines, but the reverse, three custom properties written
2365
+ by the JS into a void.
2366
+
2367
+ THE CLASS NAMES ARE UNPREFIXED, deliberately and unusually. Everything
2368
+ else in this file is `twm-`; these are not, because the names are compiled
2369
+ into the JavaScript that was extracted (`className = 'tile-grid'`,
2370
+ `'tile-header'`, `classList.add('dragging')` …) and renaming them upstream
2371
+ would be a breaking change to every consumer for a cosmetic gain. The one
2372
+ place the prefix DOES appear — `.twm-tile-error`, below — is the exception
2373
+ that proves it: `showError()` emits the prefixed spelling, so the donor's
2374
+ `.tile-error` rule is NOT ported here. Porting it would have written a
2375
+ dead rule directly beside a live class, which is the mistake this comment
2376
+ exists to stop the next reader from repeating.
2377
+
2378
+ Values come from `tokens.css`, so swapping the tokens restyles the tiles
2379
+ with everything else. Two colour literals survive the port and each says at
2380
+ its own rule why: a light accent, for which there is no token (the nearest,
2381
+ `--accent-bright`, is a border colour on this background rather than a text
2382
+ one), and the error red already shipped in `.twm-tile-error`.
2383
+ Ported from EcoAgent `ui/css/results_widgets.css` (the donor still owns its
2384
+ own copy — it does not load this file).
2385
+ ═══════════════════════════════════════════════════════════════════════ */
2386
+
2387
+ /* ── the grid container ──────────────────────────────────────────────── */
2388
+ /* The three custom properties are set by `TileGrid._init()` as inline style
2389
+ on this element; the fallbacks are the constructor's defaults, so a grid
2390
+ element built by hand still lays out. */
2391
+ .tile-grid {
2392
+ display: grid;
2393
+ grid-template-columns: repeat(var(--grid-columns, 12), 1fr);
2394
+ grid-auto-rows: var(--grid-row-height, 80px);
2395
+ gap: var(--grid-gap, 16px);
2396
+ padding: var(--grid-gap, 16px) 0 0 0;
2397
+ /* A floor rather than a height: the rows are what size the grid. It
2398
+ matters only when the board is empty, and an empty board that is zero
2399
+ pixels tall is a board you cannot drop the first tile onto. */
2400
+ min-height: 400px;
2401
+ position: relative;
2402
+ }
2403
+
2404
+ /* ── the built-in toolbar ────────────────────────────────────────────── */
2405
+ /* `_buildToolbar()` emits `tile-grid-toolbar tile-grid-toolbar--bottom` and no
2406
+ other variant, so the divider lives on the modifier: a rule that put a
2407
+ `border-bottom` on the base class — as the donor's did, when the toolbar sat
2408
+ above the grid — draws a line along the wrong edge here. A consumer that
2409
+ passes `showToolbar: false` and builds its own toolbar gets none of this. */
2410
+ .tile-grid-toolbar {
2348
2411
  display: flex;
2349
- flex-direction: column;
2412
+ justify-content: space-between;
2350
2413
  align-items: center;
2351
- justify-content: center;
2352
- height: 100%;
2353
2414
  gap: var(--space-md);
2354
- color: #ff6b6b;
2355
- font-size: var(--font-size-sm);
2415
+ padding: var(--space-lg) var(--space-xl);
2416
+ background: rgba(255, 255, 255, 0.02);
2356
2417
  }
2357
- /* ===================================================================
2358
- REUSABLE ACTION DROPDOWN COMPONENT
2359
- Used by Add Scenario, Add Widget, and other action dropdowns
2360
- =================================================================== */
2361
2418
 
2362
- .twm-action-dropdown-menu {
2363
- /* position/top/left set by ActionDropdown.position() via inline style */
2364
- position: fixed;
2365
- min-width: 220px;
2366
- padding: var(--space-md);
2367
- background: linear-gradient(145deg, rgba(30, 32, 38, 0.98), rgba(24, 26, 32, 0.96));
2368
- border: 1px solid rgba(255, 255, 255, 0.12);
2369
- border-radius: 10px;
2370
- box-shadow: 0 12px 32px rgba(0, 0, 0, 0.6);
2371
- z-index: var(--z-toast);
2372
- opacity: 0;
2373
- pointer-events: none;
2374
- transition: opacity 0.16s ease;
2375
- }
2376
- .twm-action-dropdown-menu[hidden] {
2377
- display: none !important;
2419
+ .tile-grid-toolbar--bottom {
2420
+ border-top: 1px solid var(--border-subtle);
2378
2421
  }
2379
- /* Dropdown options */
2380
- .twm-action-dropdown-option {
2422
+
2423
+ /* `toolbar-group`, `toolbar-btn`, `btn-text` and `dropdown-arrow` are names
2424
+ generic enough that a consumer almost certainly has its own. They are
2425
+ scoped under the toolbar rather than claimed globally — a library that
2426
+ defines a bare `.toolbar-btn` restyles buttons it has never met. */
2427
+ .tile-grid-toolbar .toolbar-group {
2381
2428
  display: flex;
2382
2429
  align-items: center;
2383
- gap: 10px;
2384
- width: 100%;
2385
- padding: 10px 12px;
2386
- margin-bottom: var(--space-xs);
2387
- background: rgba(255, 255, 255, 0.03);
2388
- border: 1px solid transparent;
2389
- border-radius: var(--radius-md);
2390
- color: rgba(255, 255, 255, 0.85);
2430
+ gap: var(--space-md);
2431
+ }
2432
+
2433
+ .tile-grid-toolbar .add-widget-container {
2434
+ position: relative;
2435
+ }
2436
+
2437
+ .tile-grid-toolbar .toolbar-btn {
2438
+ display: inline-flex;
2439
+ align-items: center;
2440
+ gap: var(--space-xs);
2441
+ padding: var(--space-xs) var(--space-lg);
2442
+ background: rgba(255, 255, 255, 0.04);
2443
+ border: 1px solid var(--border-default);
2444
+ border-radius: var(--radius-sm);
2445
+ color: var(--text-body);
2391
2446
  font-family: inherit;
2392
- font-size: var(--font-size-md);
2447
+ font-size: var(--font-size-sm);
2393
2448
  cursor: pointer;
2394
- text-align: left;
2395
- box-sizing: border-box;
2396
2449
  transition: background var(--transition-normal), border-color var(--transition-normal);
2397
2450
  }
2398
- .twm-action-dropdown-option:last-child {
2399
- margin-bottom: 0;
2400
- }
2401
- .twm-action-dropdown-option:hover,
2402
- .twm-action-dropdown-option:focus {
2403
- background: rgba(255, 255, 255, 0.1);
2404
- border-color: rgba(255, 255, 255, 0.08);
2405
- outline: none;
2406
- }
2407
- .twm-action-dropdown-option .twm-option-icon {
2408
- font-size: 20px;
2409
- color: #78b9ff;
2410
- flex-shrink: 0;
2451
+
2452
+ .tile-grid-toolbar .toolbar-btn:hover {
2453
+ background: var(--state-hover);
2454
+ border-color: var(--border-strong);
2411
2455
  }
2412
- .twm-action-dropdown-option:hover .twm-option-icon,
2413
- .twm-action-dropdown-option:focus .twm-option-icon {
2414
- color: #9ecfff;
2456
+
2457
+ .tile-grid-toolbar .toolbar-btn .material-symbols-outlined {
2458
+ font-size: var(--font-size-xl);
2415
2459
  }
2416
- .twm-action-dropdown-option .twm-option-label {
2417
- font-weight: 500;
2460
+
2461
+ .tile-grid-toolbar .btn-text {
2418
2462
  white-space: nowrap;
2419
- overflow: hidden;
2420
- text-overflow: ellipsis;
2421
2463
  }
2422
- .twm-action-dropdown-option .twm-option-desc {
2423
- font-size: var(--font-size-xs);
2424
- color: rgba(255, 255, 255, 0.5);
2425
- white-space: nowrap;
2426
- overflow: hidden;
2427
- text-overflow: ellipsis;
2464
+
2465
+ .tile-grid-toolbar .dropdown-arrow {
2466
+ opacity: 0.6;
2428
2467
  }
2429
2468
 
2430
- /* ── from code.css ───────────────────────── */
2431
- /* Code language tabs */
2432
- .twm-code-tabs-header {
2433
- display: flex;
2434
- align-items: center;
2435
- height: 35px;
2436
- background: #2a2a2a;
2437
- border-bottom: 1px solid var(--surface-0);
2438
- gap: var(--space-xs);
2439
- flex: 0 0 auto;
2469
+ /* `--main` is the ONLY group `_buildToolbar()` emits, and all three buttons
2470
+ sit inside it, so it takes the strip's whole width — `space-between` on the
2471
+ toolbar has nothing to space until a consumer adds a second group. */
2472
+ .tile-grid-toolbar .toolbar-group--main {
2473
+ flex: 1;
2440
2474
  }
2441
- .twm-code-tabs {
2442
- display: flex;
2443
- gap: var(--space-xxs);
2444
- height: 35px;
2475
+
2476
+ /* Add Widget is the toolbar's one primary verb, so it is the one that carries
2477
+ the accent. */
2478
+ .add-widget-btn {
2479
+ background: rgba(0, 122, 204, 0.15);
2480
+ border-color: rgba(0, 122, 204, 0.3);
2481
+ color: var(--text-bright);
2445
2482
  }
2446
- .twm-code-tab {
2447
- height: 35px;
2448
- padding: 0 12px;
2449
- display: flex;
2450
- align-items: center;
2451
- background: transparent;
2452
- color: var(--text-muted);
2453
- border: none;
2454
- border-radius: var(--radius-sm) 3px 0 0;
2455
- cursor: pointer;
2456
- font-size: var(--font-size-sm);
2457
- transition: all var(--transition-normal);
2458
- position: relative;
2483
+
2484
+ .add-widget-btn:hover {
2485
+ background: rgba(0, 122, 204, 0.25);
2486
+ border-color: rgba(0, 122, 204, 0.5);
2459
2487
  }
2460
- .twm-code-tab:hover {
2461
- background: #333;
2462
- color: #ccc;
2488
+
2489
+ /* The other two are secondary — Export is an aside and Reset Layout throws the
2490
+ board away, and neither should read as the thing to press. */
2491
+ .export-btn,
2492
+ .reset-layout-btn {
2493
+ color: var(--text-muted);
2463
2494
  }
2464
2495
 
2465
- /* ── from global_search.css ───────────────────────── */
2466
- /* Search wrapper - contains icon, input, hint */
2467
- .twm-global-search-wrapper {
2468
- display: flex;
2469
- align-items: center;
2470
- gap: var(--space-sm);
2471
- height: 26px;
2472
- min-width: 200px;
2473
- max-width: 320px;
2474
- width: 100%;
2475
- padding: 0 10px;
2476
- background: #2b2b2c;
2477
- border: 1px solid #3a3a3b;
2478
- border-radius: 5px;
2479
- transition: border-color var(--transition-normal), background var(--transition-normal);
2496
+ .export-btn:hover,
2497
+ .reset-layout-btn:hover {
2498
+ color: var(--text-body);
2480
2499
  }
2481
- .twm-global-search-wrapper:focus-within {
2482
- border-color: var(--accent-bright);
2500
+
2501
+ /* ── the tile ────────────────────────────────────────────────────────── */
2502
+ /* Flat, one 1 px border, no radius and no shadow: dense expert-tool chrome,
2503
+ the register of a trading terminal rather than a card wall. The donor also
2504
+ carried `margin: -1px 0 0 -1px` so that adjacent borders collapsed into one
2505
+ line — which is only true at `gap: 0`, and this grid's default gap is 16 px,
2506
+ where the same rule just shifts every tile a pixel up and left. Dropped on
2507
+ purpose; a consumer running gap-0 can add it back in one rule. */
2508
+ .tile {
2483
2509
  background: var(--surface-1);
2510
+ border: 1px solid var(--border-default);
2511
+ border-radius: 0;
2512
+ display: flex;
2513
+ flex-direction: column;
2514
+ position: relative;
2515
+ overflow: hidden;
2516
+ height: 100%;
2517
+ min-height: 0;
2518
+ transition: border-color var(--transition-normal);
2484
2519
  }
2485
- /* Dropdown container */
2486
- .twm-global-search-dropdown {
2487
- position: absolute;
2488
- top: calc(100% + 4px);
2489
- left: 50%;
2490
- transform: translateX(-50%);
2491
- width: 320px;
2492
- max-height: 400px;
2493
- overflow-y: auto;
2494
- background: #2b2b2c;
2495
- border: 1px solid #3a3a3b;
2496
- border-radius: var(--radius-md);
2497
- box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
2498
- z-index: 3500;
2520
+
2521
+ .tile:hover {
2522
+ border-color: var(--border-strong);
2523
+ box-shadow: none;
2499
2524
  }
2500
- /* Scrollbar styling for dropdown */
2501
- .twm-global-search-dropdown::-webkit-scrollbar {
2502
- width: 6px;
2525
+
2526
+ .tile.dragging {
2527
+ opacity: 0.5;
2528
+ z-index: var(--z-dropdown);
2529
+ box-shadow: var(--shadow-lg);
2503
2530
  }
2504
- .twm-global-search-dropdown::-webkit-scrollbar-track {
2505
- background: transparent;
2531
+
2532
+ .tile.resizing {
2533
+ z-index: var(--z-dropdown);
2534
+ border-color: var(--accent-bright);
2535
+ box-shadow: 0 0 0 2px rgba(0, 122, 204, 0.3);
2506
2536
  }
2507
- .twm-global-search-dropdown::-webkit-scrollbar-thumb {
2508
- background: #4a4a4a;
2509
- border-radius: var(--radius-sm);
2537
+
2538
+ /* Selected — set by `_onConfigClick()` while the config dialog is open, and
2539
+ cleared from every sibling by the same handler. #78b9ff is a literal
2540
+ because there is no light-accent token: `--accent-bright` is #007acc, which
2541
+ is a border colour on this background and not a text one. */
2542
+ .tile.tile--selected {
2543
+ border-color: #78b9ff;
2544
+ box-shadow: 0 0 0 2px rgba(120, 185, 255, 0.3), var(--shadow-md);
2510
2545
  }
2511
- .twm-global-search-dropdown::-webkit-scrollbar-thumb:hover {
2512
- background: #5a5a5a;
2546
+
2547
+ .tile.tile--selected .tile-header {
2548
+ background: rgba(120, 185, 255, 0.1);
2513
2549
  }
2514
2550
 
2515
- /* ── from zoom_controls.css ───────────────────────── */
2516
- /**
2517
- * Zoom Controls Styles
2518
- *
2519
- * Styles for the canvas zoom control buttons and global zoom indicator.
2520
- * Buttons: positioned in the bottom-right corner of the canvas.
2521
- * Indicator: positioned in the global bottom-right of the application.
2522
- */
2551
+ .tile.tile--selected .tile-title {
2552
+ color: #78b9ff;
2553
+ }
2523
2554
 
2524
- /* Ensure .twm-left-top is a positioning context for the zoom controls */
2525
- .twm-left-top {
2526
- position: relative;
2555
+ /* Read-only. `_buildChrome()`'s read-only branch emits no drag handle, no
2556
+ cogwheel, no ⋯ menu, no remove button and no resize handle, so there is
2557
+ nothing here to hide — what is left is to stop the tile advertising the
2558
+ affordances it no longer has. */
2559
+ .tile--readonly .tile-header {
2560
+ cursor: default;
2527
2561
  }
2528
2562
 
2529
- /* ── from managed_window.css ───────────────────────── */
2530
- /**
2531
- * Managed Window Styles
2532
- * Styles for draggable/resizable window component with taskbar integration.
2533
- */
2563
+ .tile--readonly:hover {
2564
+ border-color: var(--border-default);
2565
+ }
2534
2566
 
2535
- /* ========== Window Container ========== */
2567
+ /* ── the header strip ────────────────────────────────────────────────── */
2568
+ .tile-header {
2569
+ display: flex;
2570
+ align-items: center;
2571
+ gap: var(--space-xs);
2572
+ padding: 0 var(--space-sm);
2573
+ background: var(--surface-0);
2574
+ border-bottom: 1px solid var(--border-default);
2575
+ min-height: 22px;
2576
+ flex-shrink: 0;
2577
+ }
2536
2578
 
2537
- .twm-managed-window {
2538
- position: fixed;
2539
- background: #1a1a1a;
2540
- border: 1px solid var(--surface-3);
2541
- border-radius: 0;
2542
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
2579
+ .tile-drag-handle {
2543
2580
  display: flex;
2544
- flex-direction: column;
2545
- overflow: hidden;
2546
- min-width: 200px;
2547
- min-height: 100px;
2548
- /* Enable transitions for minimize/maximize animations */
2549
- transition: transform 0.2s ease-out, opacity 0.2s ease-out, box-shadow var(--transition-normal);
2581
+ align-items: center;
2582
+ justify-content: center;
2583
+ width: 16px;
2584
+ height: 18px;
2585
+ color: rgba(255, 255, 255, 0.3);
2586
+ cursor: grab;
2587
+ transition: color var(--transition-normal);
2550
2588
  }
2551
- .twm-managed-window:focus-within {
2552
- border-color: #505050;
2589
+
2590
+ .tile-drag-handle:hover {
2591
+ color: rgba(255, 255, 255, 0.6);
2553
2592
  }
2554
- /* Minimize animation class — targets taskbar item via CSS custom properties */
2555
- .twm-managed-window--minimizing {
2556
- transform: translate(var(--mw-target-x, 0px), var(--mw-target-y, 100vh)) scale(var(--mw-target-scale, 0.1));
2557
- opacity: 0;
2558
- pointer-events: none;
2593
+
2594
+ .tile-drag-handle:active {
2595
+ cursor: grabbing;
2559
2596
  }
2560
- /* Restore animation class — animates from taskbar item position */
2561
- .twm-managed-window--restoring {
2562
- animation: windowRestore 0.25s cubic-bezier(0.16, 1, 0.3, 1) forwards;
2597
+
2598
+ .tile-drag-handle .material-symbols-outlined {
2599
+ font-size: var(--font-size-lg);
2563
2600
  }
2564
- /* ========== Backdrop (for modal windows) ========== */
2565
2601
 
2566
- .twm-managed-window__backdrop {
2567
- position: fixed;
2568
- /* Position between top bar (35px) and bottom bar (20px) */
2602
+ /* `cursor: default`, where the donor said `pointer`. In EcoAgent the title
2603
+ opened the config panel; in this library that moved to the cogwheel alone
2604
+ ("Config opens ONLY via the explicit cogwheel button" `_buildChrome`), so
2605
+ a pointer cursor here promises a click that does nothing. */
2606
+ .tile-title {
2607
+ flex: 1;
2608
+ min-width: 0;
2609
+ font-size: var(--font-size-xs);
2610
+ font-weight: var(--font-weight-semibold);
2611
+ color: var(--text-body);
2612
+ text-transform: uppercase;
2613
+ letter-spacing: 0.04em;
2614
+ white-space: nowrap;
2615
+ overflow: hidden;
2616
+ text-overflow: ellipsis;
2617
+ cursor: default;
2618
+ padding: 0;
2619
+ border-radius: 0;
2620
+ line-height: 1;
2621
+ }
2622
+
2623
+ .tile-controls {
2624
+ display: flex;
2625
+ align-items: center;
2626
+ gap: var(--space-xs);
2627
+ margin-left: var(--space-md);
2628
+ flex-shrink: 0;
2629
+ }
2630
+
2631
+ /* One geometry for every icon button in the strip. They differ only in what
2632
+ they turn when you hover them, which is the next block. */
2633
+ .tile-config-btn,
2634
+ .tile-menu-btn,
2635
+ .tile-expand-btn,
2636
+ .tile-remove-btn {
2637
+ display: flex;
2638
+ align-items: center;
2639
+ justify-content: center;
2640
+ width: 20px;
2641
+ height: 20px;
2642
+ padding: 0;
2643
+ background: transparent;
2644
+ border: none;
2645
+ border-radius: var(--radius-sm);
2646
+ color: rgba(255, 255, 255, 0.4);
2647
+ cursor: pointer;
2648
+ transition: background var(--transition-normal), color var(--transition-normal);
2649
+ }
2650
+
2651
+ .tile-config-btn:hover,
2652
+ .tile-menu-btn:hover,
2653
+ .tile-expand-btn:hover {
2654
+ background: var(--state-hover);
2655
+ color: #78b9ff;
2656
+ }
2657
+
2658
+ .tile-remove-btn:hover {
2659
+ background: var(--state-hover);
2660
+ color: #ff6b6b;
2661
+ }
2662
+
2663
+ .tile-config-btn .material-symbols-outlined,
2664
+ .tile-menu-btn .material-symbols-outlined,
2665
+ .tile-expand-btn .material-symbols-outlined,
2666
+ .tile-remove-btn .material-symbols-outlined {
2667
+ font-size: var(--font-size-xl);
2668
+ }
2669
+
2670
+ /* The ⓘ button carries its own inline `style` from `_buildChrome`, and inline
2671
+ style beats any rule in this file — background, border, padding and colour
2672
+ are already decided there and CANNOT be changed from here. So this rule adds
2673
+ only what the inline style leaves open. A hover background written here
2674
+ would be a rule that never applies: exactly the dead rule the header comment
2675
+ warns about, arrived at from the other direction. */
2676
+ .tile-info-btn {
2677
+ width: 20px;
2678
+ height: 20px;
2679
+ border-radius: var(--radius-sm);
2680
+ flex-shrink: 0;
2681
+ }
2682
+
2683
+ /* ── the ⋯ menu ──────────────────────────────────────────────────────── */
2684
+ /* Rendered from `TileBase.menuItems()`; an empty list emits no wrapper at all,
2685
+ so none of this draws for a consumer that overrides the hook to `[]`. */
2686
+ .tile-menu-wrapper {
2687
+ position: relative;
2688
+ }
2689
+
2690
+ .tile-menu-dropdown {
2691
+ position: absolute;
2692
+ top: 100%;
2693
+ right: 0;
2694
+ z-index: var(--z-dropdown);
2695
+ min-width: 180px;
2696
+ padding: var(--space-xs) 0;
2697
+ background: var(--surface-2);
2698
+ border: 1px solid rgba(255, 255, 255, 0.1);
2699
+ border-radius: var(--radius-md);
2700
+ box-shadow: var(--shadow-md);
2701
+ }
2702
+
2703
+ /* `!important`, matching `.twm-action-dropdown-menu[hidden]` below and for the
2704
+ same reason: `hidden` is the JS's only way of closing this menu
2705
+ (`menuDropdown.hidden = !menuDropdown.hidden`), and the UA implements the
2706
+ attribute as the weakest possible rule — any consumer selector that sets a
2707
+ display on the dropdown would leave it open with nothing to close it. */
2708
+ .tile-menu-dropdown[hidden] {
2709
+ display: none !important;
2710
+ }
2711
+
2712
+ .tile-menu-item {
2713
+ display: flex;
2714
+ align-items: center;
2715
+ gap: var(--space-md);
2716
+ width: 100%;
2717
+ padding: var(--space-sm) var(--space-lg);
2718
+ background: none;
2719
+ border: none;
2720
+ color: var(--text-secondary);
2721
+ font-family: inherit;
2722
+ font-size: var(--font-size-sm);
2723
+ text-align: left;
2724
+ cursor: pointer;
2725
+ white-space: nowrap;
2726
+ transition: background var(--transition-fast);
2727
+ }
2728
+
2729
+ .tile-menu-item:hover {
2730
+ background: var(--state-active);
2731
+ color: var(--text-bright);
2732
+ }
2733
+
2734
+ .tile-menu-item .material-symbols-outlined {
2735
+ font-size: var(--font-size-xl);
2736
+ color: rgba(255, 255, 255, 0.5);
2737
+ }
2738
+
2739
+ .tile-menu-item:hover .material-symbols-outlined {
2740
+ color: #78b9ff;
2741
+ }
2742
+
2743
+ /* ── the content area ────────────────────────────────────────────────── */
2744
+ /* `min-height: 0` is the load-bearing declaration, not the padding: a flex
2745
+ child's default `min-height: auto` refuses to shrink below its content, so
2746
+ without it a tall table or a chart pushes the tile past its grid rows
2747
+ instead of scrolling inside them. */
2748
+ .tile-content {
2749
+ flex: 1;
2750
+ padding: var(--space-lg);
2751
+ overflow: hidden;
2752
+ position: relative;
2753
+ min-height: 0;
2754
+ display: flex;
2755
+ flex-direction: column;
2756
+ }
2757
+
2758
+ /* A chart library that measures its container on draw gets a container with a
2759
+ real size. The `!important` is the only lever there is: Plotly writes width
2760
+ and height INLINE onto `.js-plotly-plot` when it draws, an inline
2761
+ declaration outranks any selector, and `!important` is what outranks it —
2762
+ without which a chart keeps the size it had when the tile was half as wide. */
2763
+ .tile-content > [class*="-container"] {
2764
+ flex: 1;
2765
+ min-height: 0;
2766
+ width: 100%;
2767
+ position: relative;
2768
+ }
2769
+
2770
+ .tile-content .js-plotly-plot,
2771
+ .tile-content .plot-container.plotly,
2772
+ .tile-content .svg-container {
2773
+ width: 100% !important;
2774
+ height: 100% !important;
2775
+ }
2776
+
2777
+ /* ── resize handle and drag ghost ────────────────────────────────────── */
2778
+ .tile-resize-handle {
2779
+ position: absolute;
2780
+ background: transparent;
2781
+ z-index: var(--z-sticky);
2782
+ }
2783
+
2784
+ .tile-resize-handle.se {
2785
+ bottom: 0;
2786
+ right: 0;
2787
+ width: 24px;
2788
+ height: 24px;
2789
+ cursor: se-resize;
2790
+ display: flex;
2791
+ align-items: center;
2792
+ justify-content: center;
2793
+ color: rgba(255, 255, 255, 0.2);
2794
+ transition: color var(--transition-normal);
2795
+ }
2796
+
2797
+ .tile-resize-handle.se:hover {
2798
+ color: rgba(255, 255, 255, 0.5);
2799
+ }
2800
+
2801
+ .tile-resize-handle .material-symbols-outlined {
2802
+ font-size: var(--font-size-xl);
2803
+ transform: rotate(-45deg);
2804
+ }
2805
+
2806
+ /* The drop preview, positioned by `_updateGhost()` in grid coordinates. */
2807
+ .tile-ghost {
2808
+ position: absolute;
2809
+ background: rgba(0, 122, 204, 0.15);
2810
+ border: 2px dashed rgba(0, 122, 204, 0.5);
2811
+ border-radius: var(--radius-sm);
2812
+ pointer-events: none;
2813
+ z-index: 50;
2814
+ transition: transform 0.1s ease;
2815
+ }
2816
+
2817
+ /* ── the three content states ────────────────────────────────────────── */
2818
+ /* `showLoading()`, `showEmpty()` and `showError()` each replace the whole of
2819
+ `.tile-content`, so all three centre themselves in it. */
2820
+ .tile-loading {
2821
+ display: flex;
2822
+ flex-direction: column;
2823
+ align-items: center;
2824
+ justify-content: center;
2825
+ height: 100%;
2826
+ gap: var(--space-md);
2827
+ color: rgba(255, 255, 255, 0.5);
2828
+ font-size: var(--font-size-sm);
2829
+ }
2830
+
2831
+ .tile-loading .material-symbols-outlined {
2832
+ font-size: 24px;
2833
+ }
2834
+
2835
+ /* Scoped, because `.spinning` on its own is a name a consumer is entitled to
2836
+ use for its own spinner. `spin` is not scopeable — `@keyframes` names are
2837
+ global — and a consumer that defines its own `spin` later in the cascade
2838
+ simply spins this icon its way, which is harmless. */
2839
+ .tile-loading .spinning {
2840
+ animation: spin 1s linear infinite;
2841
+ }
2842
+
2843
+ @keyframes spin {
2844
+ from { transform: rotate(0deg); }
2845
+ to { transform: rotate(360deg); }
2846
+ }
2847
+
2848
+ .tile-empty {
2849
+ display: flex;
2850
+ flex-direction: column;
2851
+ align-items: center;
2852
+ justify-content: center;
2853
+ height: 100%;
2854
+ gap: var(--space-md);
2855
+ color: rgba(255, 255, 255, 0.4);
2856
+ font-size: var(--font-size-sm);
2857
+ text-align: center;
2858
+ }
2859
+
2860
+ .tile-empty .material-symbols-outlined {
2861
+ font-size: 32px;
2862
+ opacity: 0.5;
2863
+ }
2864
+
2865
+ /* THE ERROR STATE IS PREFIXED AND THE OTHER TWO ARE NOT. `showError()` emits
2866
+ `.twm-tile-error` where `showEmpty()` emits `.tile-empty` — an asymmetry
2867
+ that is now a fixed fact of the wire, because this rule shipped before the
2868
+ rest of the block did. The donor's unprefixed `.tile-error` is deliberately
2869
+ NOT ported: it would sit here looking correct and matching nothing. */
2870
+ .twm-tile-error {
2871
+ display: flex;
2872
+ flex-direction: column;
2873
+ align-items: center;
2874
+ justify-content: center;
2875
+ height: 100%;
2876
+ gap: var(--space-md);
2877
+ color: #ff6b6b;
2878
+ font-size: var(--font-size-sm);
2879
+ text-align: center;
2880
+ }
2881
+
2882
+ .twm-tile-error .material-symbols-outlined {
2883
+ font-size: 32px;
2884
+ }
2885
+ /* ===================================================================
2886
+ REUSABLE ACTION DROPDOWN COMPONENT
2887
+ Used by Add Scenario, Add Widget, and other action dropdowns
2888
+ =================================================================== */
2889
+
2890
+ .twm-action-dropdown-menu {
2891
+ /* position/top/left set by ActionDropdown.position() via inline style */
2892
+ position: fixed;
2893
+ min-width: 220px;
2894
+ padding: var(--space-md);
2895
+ background: linear-gradient(145deg, rgba(30, 32, 38, 0.98), rgba(24, 26, 32, 0.96));
2896
+ border: 1px solid rgba(255, 255, 255, 0.12);
2897
+ border-radius: 10px;
2898
+ box-shadow: 0 12px 32px rgba(0, 0, 0, 0.6);
2899
+ z-index: var(--z-toast);
2900
+ opacity: 0;
2901
+ pointer-events: none;
2902
+ transition: opacity 0.16s ease;
2903
+ }
2904
+ .twm-action-dropdown-menu[hidden] {
2905
+ display: none !important;
2906
+ }
2907
+ /* Dropdown options */
2908
+ .twm-action-dropdown-option {
2909
+ display: flex;
2910
+ align-items: center;
2911
+ gap: 10px;
2912
+ width: 100%;
2913
+ padding: 10px 12px;
2914
+ margin-bottom: var(--space-xs);
2915
+ background: rgba(255, 255, 255, 0.03);
2916
+ border: 1px solid transparent;
2917
+ border-radius: var(--radius-md);
2918
+ color: rgba(255, 255, 255, 0.85);
2919
+ font-family: inherit;
2920
+ font-size: var(--font-size-md);
2921
+ cursor: pointer;
2922
+ text-align: left;
2923
+ box-sizing: border-box;
2924
+ transition: background var(--transition-normal), border-color var(--transition-normal);
2925
+ }
2926
+ .twm-action-dropdown-option:last-child {
2927
+ margin-bottom: 0;
2928
+ }
2929
+ .twm-action-dropdown-option:hover,
2930
+ .twm-action-dropdown-option:focus {
2931
+ background: rgba(255, 255, 255, 0.1);
2932
+ border-color: rgba(255, 255, 255, 0.08);
2933
+ outline: none;
2934
+ }
2935
+ .twm-action-dropdown-option .twm-option-icon {
2936
+ font-size: 20px;
2937
+ color: #78b9ff;
2938
+ flex-shrink: 0;
2939
+ }
2940
+ .twm-action-dropdown-option:hover .twm-option-icon,
2941
+ .twm-action-dropdown-option:focus .twm-option-icon {
2942
+ color: #9ecfff;
2943
+ }
2944
+ .twm-action-dropdown-option .twm-option-label {
2945
+ font-weight: 500;
2946
+ white-space: nowrap;
2947
+ overflow: hidden;
2948
+ text-overflow: ellipsis;
2949
+ }
2950
+ .twm-action-dropdown-option .twm-option-desc {
2951
+ font-size: var(--font-size-xs);
2952
+ color: rgba(255, 255, 255, 0.5);
2953
+ white-space: nowrap;
2954
+ overflow: hidden;
2955
+ text-overflow: ellipsis;
2956
+ }
2957
+
2958
+ /* ── from code.css ───────────────────────── */
2959
+ /* Code language tabs */
2960
+ .twm-code-tabs-header {
2961
+ display: flex;
2962
+ align-items: center;
2963
+ height: 35px;
2964
+ background: #2a2a2a;
2965
+ border-bottom: 1px solid var(--surface-0);
2966
+ gap: var(--space-xs);
2967
+ flex: 0 0 auto;
2968
+ }
2969
+ .twm-code-tabs {
2970
+ display: flex;
2971
+ gap: var(--space-xxs);
2972
+ height: 35px;
2973
+ }
2974
+ .twm-code-tab {
2975
+ height: 35px;
2976
+ padding: 0 12px;
2977
+ display: flex;
2978
+ align-items: center;
2979
+ background: transparent;
2980
+ color: var(--text-muted);
2981
+ border: none;
2982
+ border-radius: var(--radius-sm) 3px 0 0;
2983
+ cursor: pointer;
2984
+ font-size: var(--font-size-sm);
2985
+ transition: all var(--transition-normal);
2986
+ position: relative;
2987
+ }
2988
+ .twm-code-tab:hover {
2989
+ background: #333;
2990
+ color: #ccc;
2991
+ }
2992
+
2993
+ /* ── from global_search.css ───────────────────────── */
2994
+ /* Search wrapper - contains icon, input, hint */
2995
+ .twm-global-search-wrapper {
2996
+ display: flex;
2997
+ align-items: center;
2998
+ gap: var(--space-sm);
2999
+ height: 26px;
3000
+ min-width: 200px;
3001
+ max-width: 320px;
3002
+ width: 100%;
3003
+ padding: 0 10px;
3004
+ background: #2b2b2c;
3005
+ border: 1px solid #3a3a3b;
3006
+ border-radius: 5px;
3007
+ transition: border-color var(--transition-normal), background var(--transition-normal);
3008
+ }
3009
+ .twm-global-search-wrapper:focus-within {
3010
+ border-color: var(--accent-bright);
3011
+ background: var(--surface-1);
3012
+ }
3013
+ /* Dropdown container */
3014
+ .twm-global-search-dropdown {
3015
+ position: absolute;
3016
+ top: calc(100% + 4px);
3017
+ left: 50%;
3018
+ transform: translateX(-50%);
3019
+ width: 320px;
3020
+ max-height: 400px;
3021
+ overflow-y: auto;
3022
+ background: #2b2b2c;
3023
+ border: 1px solid #3a3a3b;
3024
+ border-radius: var(--radius-md);
3025
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
3026
+ z-index: 3500;
3027
+ }
3028
+ /* Scrollbar styling for dropdown */
3029
+ .twm-global-search-dropdown::-webkit-scrollbar {
3030
+ width: 6px;
3031
+ }
3032
+ .twm-global-search-dropdown::-webkit-scrollbar-track {
3033
+ background: transparent;
3034
+ }
3035
+ .twm-global-search-dropdown::-webkit-scrollbar-thumb {
3036
+ background: #4a4a4a;
3037
+ border-radius: var(--radius-sm);
3038
+ }
3039
+ .twm-global-search-dropdown::-webkit-scrollbar-thumb:hover {
3040
+ background: #5a5a5a;
3041
+ }
3042
+
3043
+ /* ── from zoom_controls.css ───────────────────────── */
3044
+ /**
3045
+ * Zoom Controls Styles
3046
+ *
3047
+ * Styles for the canvas zoom control buttons and global zoom indicator.
3048
+ * Buttons: positioned in the bottom-right corner of the canvas.
3049
+ * Indicator: positioned in the global bottom-right of the application.
3050
+ */
3051
+
3052
+ /* Ensure .twm-left-top is a positioning context for the zoom controls */
3053
+ .twm-left-top {
3054
+ position: relative;
3055
+ }
3056
+
3057
+ /* ── from managed_window.css ───────────────────────── */
3058
+ /**
3059
+ * Managed Window Styles
3060
+ * Styles for draggable/resizable window component with taskbar integration.
3061
+ */
3062
+
3063
+ /* ========== Window Container ========== */
3064
+
3065
+ .twm-managed-window {
3066
+ position: fixed;
3067
+ background: #1a1a1a;
3068
+ border: 1px solid var(--surface-3);
3069
+ border-radius: 0;
3070
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
3071
+ display: flex;
3072
+ flex-direction: column;
3073
+ overflow: hidden;
3074
+ min-width: 200px;
3075
+ min-height: 100px;
3076
+ /* Enable transitions for minimize/maximize animations */
3077
+ transition: transform 0.2s ease-out, opacity 0.2s ease-out, box-shadow var(--transition-normal);
3078
+ }
3079
+ /* C3. Containment. `--contained` swaps `position: fixed` for `absolute` so the
3080
+ * window is placed relative to its container, which must be a containing block
3081
+ * established with `position: relative`/`absolute` and NOTHING ELSE.
3082
+ *
3083
+ * This is the most dangerous rule in the containment work. `transform`,
3084
+ * `filter`, `contain` and `will-change` on the container ALSO create a
3085
+ * containing block — and they additionally capture `position: fixed`
3086
+ * descendants. DataTable's filter dropdown (`position: fixed; z-index: 10001`)
3087
+ * and the autocomplete dropdown (`z-index: 10000000`) deliberately escape their
3088
+ * tile to the viewport; under a transformed ancestor they become
3089
+ * container-relative and are clipped by the container's `overflow: hidden`.
3090
+ * The symptom is "the filter dropdown is cut in half" and the cause is three
3091
+ * files away. */
3092
+ .twm-managed-window--contained {
3093
+ position: absolute;
3094
+ }
3095
+ .twm-managed-window__backdrop--contained {
3096
+ position: absolute;
3097
+ top: 0;
3098
+ left: 0;
3099
+ right: 0;
3100
+ bottom: 0;
3101
+ }
3102
+ /* The layer a contained window is mounted into. `position: relative` and
3103
+ * `overflow: hidden` ONLY — see the warning above. */
3104
+ .tbl-window-layer {
3105
+ position: absolute;
3106
+ inset: 0;
3107
+ overflow: hidden;
3108
+ }
3109
+ /* C6. An extra titlebar button (the table cogwheel), sized like the built-ins. */
3110
+ .twm-managed-window__btn--custom .material-symbols-outlined {
3111
+ font-size: 13px;
3112
+ line-height: 1;
3113
+ }
3114
+ .twm-managed-window:focus-within {
3115
+ border-color: #505050;
3116
+ }
3117
+ /* Minimize animation class — targets taskbar item via CSS custom properties */
3118
+ .twm-managed-window--minimizing {
3119
+ transform: translate(var(--mw-target-x, 0px), var(--mw-target-y, 100vh)) scale(var(--mw-target-scale, 0.1));
3120
+ opacity: 0;
3121
+ pointer-events: none;
3122
+ }
3123
+ /* Restore animation class — animates from taskbar item position */
3124
+ .twm-managed-window--restoring {
3125
+ animation: windowRestore 0.25s cubic-bezier(0.16, 1, 0.3, 1) forwards;
3126
+ }
3127
+ /* C27. THE KEYFRAMES THE RULE ABOVE HAS ALWAYS NAMED AND NOTHING EVER DEFINED.
3128
+ `animation: windowRestore ...` referred to an `@keyframes` block that does
3129
+ not exist anywhere in either stylesheet, which CSS treats as no animation at
3130
+ all — so the restore was an instant pop while its mirror image, the minimise,
3131
+ played for 200ms. The fifth instance of this codebase's signature defect: one
3132
+ side names a thing the other side has never heard of, nothing throws, and no
3133
+ check in any of the three consumers can see it. Deliberately the exact
3134
+ inverse of `--minimizing`, from the same three custom properties, so the two
3135
+ halves of the gesture cannot describe different geometry. */
3136
+ @keyframes windowRestore {
3137
+ from {
3138
+ transform: translate(var(--mw-target-x, 0px), var(--mw-target-y, 100vh))
3139
+ scale(var(--mw-target-scale, 0.1));
3140
+ opacity: 0;
3141
+ }
3142
+ to {
3143
+ transform: none;
3144
+ opacity: 1;
3145
+ }
3146
+ }
3147
+ /* ========== Backdrop (for modal windows) ========== */
3148
+
3149
+ .twm-managed-window__backdrop {
3150
+ position: fixed;
3151
+ /* Position between top bar (35px) and bottom bar (20px) */
2569
3152
  top: 35px;
2570
3153
  left: 0;
2571
3154
  right: 0;
@@ -3437,3 +4020,723 @@ button.twm-about-dialog__btn--close:focus {
3437
4020
  .twm-top-nav__label { display: none; }
3438
4021
  .twm-top-nav__btn { padding: 0 10px; }
3439
4022
  }
4023
+
4024
+
4025
+ /* ══ RESTORED MIRROR ═════════════════════════════════════════════════
4026
+ * `css/flexdesk.css` is generated from this file by `scripts/css_extract.py`,
4027
+ * which is not in the repository — so the two are kept in step BY HAND, and
4028
+ * this block is that mirror re-established after the working tree lost its
4029
+ * uncommitted changes. Every rule below was recovered from the built
4030
+ * stylesheet and is already live in `css/flexdesk.css`; they are grouped
4031
+ * here rather than threaded back into their original sections because the
4032
+ * generator, not this file, decides where they end up.
4033
+ * ════════════════════════════════════════════════════════════════════ */
4034
+
4035
+ /* Selected rows. `_updateRowSelection()` (src/ui/components/data_table.js:2226)
4036
+ has always toggled `.selected` on the row, and no stylesheet has ever
4037
+ defined it — so `selectable: true`, the component's headline affordance,
4038
+ has selected INVISIBLY in every consumer since the class was written.
4039
+ Painted on `td` rather than `tr` to match the hover rule above: the zebra
4040
+ stripe sits on `tr`, and a `tr` background would fight it at equal
4041
+ specificity instead of covering it.
4042
+
4043
+ AND IT HAS TO OUT-SPECIFY HOVER, not merely follow it. Source order only
4044
+ decides a TIE, and there is none here: the hover rule above is (0,3,2) —
4045
+ `:not()` contributes the specificity of its argument — against (0,2,3) for
4046
+ the plain form. Written that way, pointing at the row you had chosen
4047
+ un-highlights it, which reads as the selection being lost on mouse-over. The
4048
+ first selector below carries the same `:not()` and lands at (0,3,3); the
4049
+ second keeps the readonly tables, which hover never claimed, working. */
4050
+ .twm-preview-table:not(.twm-preview-table--readonly) tbody tr.selected td,
4051
+ .twm-preview-table tbody tr.selected td {
4052
+ background: var(--state-selected, rgba(0, 122, 204, 0.15));
4053
+ }
4054
+
4055
+ padding: 0;
4056
+ /* A BOX, not "whatever the glyph turned out to be". The rule had padding
4057
+ * and no dimensions, so the button's size was the icon's size — and the
4058
+ * icon's size was nobody's decision at all (see below). In a 35px top bar
4059
+ * that produced a ~32px control that visibly outweighed everything beside
4060
+ * it. */
4061
+ width: 22px;
4062
+ height: 22px;
4063
+ /* THE GLYPH SIZE CAME ACROSS AS NOTHING.
4064
+ *
4065
+ * EcoAgent sets it — `main_new.css:4700`, `.panel-toggle-btn
4066
+ * .material-symbols-outlined { font-size: var(--font-size-xl) }` — and the
4067
+ * extraction that produced this stylesheet took the button rule and left that
4068
+ * one behind. Material Symbols then falls back to its own default of 24px, so
4069
+ * every consumer of the library has been drawing this control half again as
4070
+ * large as its author intended. Same family as the `--ea-*` variables that
4071
+ * arrived without their definitions, and as `.twm-collapsible-content.visible`:
4072
+ * an extraction that split a rule from the rule it depended on.
4073
+ *
4074
+ * `font-size` IS the glyph size for an icon font. `width`/`height` on the
4075
+ * button does not constrain it — it overflows instead. */
4076
+ .twm-panel-toggle-btn .material-symbols-outlined {
4077
+ font-size: var(--font-size-xl);
4078
+ line-height: 1;
4079
+ }
4080
+
4081
+ /* `#open()` adds `visible` and `#close()` takes it off again
4082
+ (`ui/components/autocomplete_field.js`), and until now NEITHER stylesheet
4083
+ said what the class meant — while the dropdown above is `display: none`. So
4084
+ the suggestion list has never been shown to anybody, in any consumer, for as
4085
+ long as the component has existed: the provider ran, the items rendered, and
4086
+ the element stayed hidden. This is the same shape as
4087
+ `.twm-action-dropdown-menu.visible`, which is the pattern the component was
4088
+ written against. */
4089
+ .twm-autocomplete-field__dropdown.visible {
4090
+ display: block;
4091
+ }
4092
+
4093
+ /* `#renderDropdown()` marks the keyboard cursor's row `active` and
4094
+ `#moveSelection()` moves it — with no rule to draw it. Arrowing down the list
4095
+ therefore moved something invisible, and Enter picked a row the user had no
4096
+ way to see. `:hover` above is the mouse's answer to the same question; this
4097
+ is the keyboard's, and it is deliberately the stronger of the two so that
4098
+ moving the pointer across the list does not hide where the cursor is. */
4099
+ .twm-autocomplete-field__item.active {
4100
+ background-color: var(--state-selected, rgba(0, 122, 204, 0.15));
4101
+ box-shadow: inset 2px 0 0 var(--accent-bright, #007acc);
4102
+ }
4103
+
4104
+ /* A `<button role="menuitem">`, not a div — that is what makes the menu
4105
+ * operable by keyboard without re-implementing Enter, Space and focus. The
4106
+ * first five declarations are the button reset that buys it: without them the
4107
+ * UA paints its own background, border and centred text over everything below. */
4108
+ appearance: none;
4109
+ background: none;
4110
+ border: 0;
4111
+ width: 100%;
4112
+ text-align: left;
4113
+ color: inherit;
4114
+ .twm-context-menu-item:hover:not(.disabled),
4115
+ .twm-context-menu-item:focus-visible {
4116
+ outline: none;
4117
+ /* A DISABLED ITEM MUST LOOK DISABLED.
4118
+ *
4119
+ * `showContextMenu` has always added `.disabled` and bound no click listener to
4120
+ * it — the behaviour was right and there was no rule for the class anywhere, so
4121
+ * a disabled item rendered at full opacity, kept `cursor: pointer`, still
4122
+ * highlighted on hover, and did nothing when clicked. That is the exact shape of
4123
+ * the `.twm-collapsible-content.visible` bug: a class the JS sets, a stylesheet
4124
+ * that has never heard of it, and a feature that looks live and is not. */
4125
+ .twm-context-menu-item.disabled {
4126
+ opacity: 0.4;
4127
+ cursor: default;
4128
+ color: var(--text-muted);
4129
+ }
4130
+
4131
+ .twm-context-menu-item.disabled.twm-delete-node { color: var(--text-muted); }
4132
+
4133
+ /* THE OPEN STATE. `ActionDropdown.open()` un-hides the menu and then adds
4134
+ `.visible` on the next frame to fade it in (action_dropdown.js:172-180) —
4135
+ and no rule for that class existed, so the menu unhid itself into
4136
+ `opacity: 0; pointer-events: none` and stayed there. Every ActionDropdown in
4137
+ the library, "Add Widget" included, opened invisible and un-clickable.
4138
+ The same defect `.twm-collapsible-content.visible` had, in the same shape:
4139
+ the JS toggles a class the stylesheet never grew. */
4140
+ .twm-action-dropdown-menu.visible {
4141
+ opacity: 1;
4142
+ pointer-events: auto;
4143
+ }
4144
+
4145
+ /* The option's text column. `_renderOptions` wraps the label and the
4146
+ description in `<span class="option-text">` (action_dropdown.js:125-128) and
4147
+ both children are inline, so a described option rendered as
4148
+ "Add Widget Put a widget on this desktop" on one run-together line. The
4149
+ description is a SECOND LINE by construction; this is what makes it one.
4150
+ `min-width: 0` so the ellipsis on the two children has something to bite on
4151
+ inside the flex row. */
4152
+ .twm-action-dropdown-option .option-text {
4153
+ display: flex;
4154
+ flex-direction: column;
4155
+ min-width: 0;
4156
+ }
4157
+
4158
+ /* C11. Aero snap — the preview shown while a drag is over an edge zone.
4159
+ * Absolutely positioned in the SAME coordinate space the window uses, so it is
4160
+ * the container when contained and the page otherwise. Below the window's own
4161
+ * z-index and above everything else in the container, so it reads as a target
4162
+ * behind the thing being dragged. */
4163
+ .twm-snap-preview {
4164
+ position: absolute;
4165
+ z-index: 5999;
4166
+ pointer-events: none;
4167
+ background: rgba(23, 119, 179, 0.18);
4168
+ border: 1px solid var(--accent-bright);
4169
+ box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08);
4170
+ transition: left 0.09s ease, top 0.09s ease, width 0.09s ease, height 0.09s ease;
4171
+ }
4172
+
4173
+ /* C15. A CONTROLLER'S preview is positioned in VIEWPORT pixels, because its
4174
+ * rectangle came from hit-testing somebody else's DOM (`getBoundingClientRect`)
4175
+ * rather than from `_bounds()`. `position: fixed` is what makes those numbers
4176
+ * mean what they say; leaving it `absolute` under `document.body` would be
4177
+ * right only until the page scrolls. */
4178
+ .twm-snap-preview--viewport {
4179
+ position: fixed;
4180
+ }
4181
+
4182
+ /* A window whose position came from a snap rather than from a drag. Nothing
4183
+ * depends on it visually; it is there so a consumer can tell the two apart. */
4184
+ .twm-managed-window--snapped { }
4185
+
4186
+ /* R3. HALF TRANSPARENT THE MOMENT THE DRAG LEAVES THE TILE IT CAME FROM.
4187
+ *
4188
+ * The one signal that separates "I am moving this window around on its own
4189
+ * pane" from "letting go now will put this window into the tree somewhere".
4190
+ * Inside its origin pane the window is fully opaque and nothing is armed; the
4191
+ * moment the pointer crosses out of it the window goes translucent and the drop
4192
+ * preview starts drawing what a release would produce.
4193
+ *
4194
+ * No transition of its own: `.twm-managed-window` already transitions `opacity`
4195
+ * over 0.2s for the minimize animation, so the fade comes for free — and adding
4196
+ * a second, faster one here would make the same property animate at two speeds
4197
+ * depending on which class won. Translucent, not ghostly: the window is still
4198
+ * the thing being aimed, and at 0.5 the pane underneath reads through it while
4199
+ * its title bar stays legible. */
4200
+ .twm-managed-window--detached {
4201
+ opacity: 0.5;
4202
+ }
4203
+
4204
+ /* A title-bar button the WM has suppressed for the window's current state — the
4205
+ * minimize button on a maximised window it promoted out of a tile. `!important`
4206
+ * because `button.twm-managed-window__btn` sets `display: flex` and an author
4207
+ * declaration of `display` defeats the `hidden` attribute outright; a class
4208
+ * that loses the specificity tie is a control that stays on screen. */
4209
+ .twm-managed-window__btn--suppressed { display: none !important; }
4210
+
4211
+
4212
+ /* C6. An extra titlebar button (the table cogwheel), sized like the built-ins.
4213
+ * R7. And the maximize button, when a consumer has given it a glyph instead of
4214
+ * the built-in square — the WM does, because for a window it lifted out of a
4215
+ * tile that button means "back to tile". A Material Symbol renders at its
4216
+ * `font-size` and Google's own sheet says 24px, which is three times the height
4217
+ * of a 20px title-bar button: the rule has to be a DESCENDANT selector like
4218
+ * this one to out-specify it, and a class on the glyph element itself would
4219
+ * quietly lose. */
4220
+ .twm-managed-window__btn--custom .material-symbols-outlined,
4221
+ .twm-managed-window__btn--maximize .material-symbols-outlined {
4222
+ /* The eight handles' geometry. Without these the elements exist, are
4223
+ * `position: absolute`, and have NO width, height or offset — so every window
4224
+ * in every consumer is unresizable and nothing says why. Edges are 6px, corners
4225
+ * 12px and sit above the edges, because a corner that loses to an edge makes
4226
+ * diagonal resize a matter of luck. */
4227
+ .twm-managed-window__resize--n,
4228
+ .twm-managed-window__resize--s { left: 0; right: 0; height: 6px; cursor: ns-resize; }
4229
+
4230
+ .twm-managed-window__resize--n { top: -3px; }
4231
+
4232
+ .twm-managed-window__resize--s { bottom: -3px; }
4233
+
4234
+ .twm-managed-window__resize--e,
4235
+ .twm-managed-window__resize--w { top: 0; bottom: 0; width: 6px; cursor: ew-resize; }
4236
+
4237
+ .twm-managed-window__resize--e { right: -3px; }
4238
+
4239
+ .twm-managed-window__resize--w { left: -3px; }
4240
+
4241
+ .twm-managed-window__resize--ne,
4242
+ .twm-managed-window__resize--nw,
4243
+ .twm-managed-window__resize--se,
4244
+ .twm-managed-window__resize--sw { width: 12px; height: 12px; z-index: calc(var(--z-sticky) + 1); }
4245
+
4246
+ .twm-managed-window__resize--ne { top: -3px; right: -3px; cursor: nesw-resize; }
4247
+
4248
+ .twm-managed-window__resize--nw { top: -3px; left: -3px; cursor: nwse-resize; }
4249
+
4250
+ .twm-managed-window__resize--se { bottom: -3px; right: -3px; cursor: nwse-resize; }
4251
+
4252
+ .twm-managed-window__resize--sw { bottom: -3px; left: -3px; cursor: nesw-resize; }
4253
+
4254
+ /* A maximised or snapped window is not resized by dragging its edges. */
4255
+ .twm-managed-window--maximized .twm-managed-window__resize { display: none; }
4256
+
4257
+
4258
+ /* C26. A LEADING GLYPH ON AN ACTION BUTTON — `openModal({actions:[{icon}]})`.
4259
+ *
4260
+ * Material Symbols is a FONT. It renders at `font-size`, which defaults to
4261
+ * 24px, and the button's `min-height` does not constrain it — so without this
4262
+ * the glyph overflows a 22px action button and the row reads as ragged. The
4263
+ * declaration is written TWICE, once as a single class and once compounded
4264
+ * with `.material-symbols-outlined`, because a consumer that loads Google's
4265
+ * hosted stylesheet loads it AFTER this one: `.material-symbols-outlined
4266
+ * {font-size:24px}` is a single class too, so it ties on specificity and wins
4267
+ * on source order. The compound selector is what actually holds. */
4268
+ .twm-btn__glyph,
4269
+ .twm-btn__glyph.material-symbols-outlined {
4270
+ font-size: var(--font-size-xl, 16px);
4271
+ line-height: 1;
4272
+ flex: 0 0 auto;
4273
+ }
4274
+
4275
+ .twm-btn__label { line-height: 1; }
4276
+
4277
+ /* R1. THE CONTAINING BLOCK A DRAGGED WINDOW ESCAPES INTO. A window promoted
4278
+ * in place is contained to its leaf wrap (C21), which clips it; for the
4279
+ * length of a drag the WM re-parents it here instead, so it can be carried
4280
+ * across tile boundaries. Its `left`/`top` then resolve against this box,
4281
+ * and without a `position` they would resolve against whatever positioned
4282
+ * ancestor the embedder happens to have — usually `.twm-shell__main`, which
4283
+ * is the same rectangle, and occasionally the page, which is not.
4284
+ *
4285
+ * `relative` and nothing else, for the reason `.twm-leaf` gives at length:
4286
+ * `transform`, `filter` and `contain` would also capture the
4287
+ * `position: fixed` dropdowns that deliberately escape their tile. */
4288
+ position: relative;
4289
+ /* C21. THE CONTAINING BLOCK for a window promoted in place. `overflow:
4290
+ * hidden` below is what actually confines it; this is what makes its
4291
+ * coordinates mean the pane rather than the page.
4292
+ *
4293
+ * `position: relative` and NOTHING ELSE — `transform`, `filter`, `contain`
4294
+ * and `will-change` also create a containing block AND capture
4295
+ * `position: fixed` descendants, which would clip the dropdowns DataTable
4296
+ * opens on purpose. */
4297
+ position: relative;
4298
+ /* NO `text-transform` HERE, and the absence is the decision. This element used
4299
+ * to be lower-cased, which reads as a house style until you notice whose text
4300
+ * it is: the embedder's. `tile_renderer` puts a content factory's `title` in
4301
+ * here, and in a data application that is a record's display name — typed by a
4302
+ * user who capitalised "Customer Orders" on purpose. A framework may style its
4303
+ * own chrome however it likes; it may not rewrite its embedder's data on the
4304
+ * way to the screen. `.twm-leaf__tab-label` shows the same strings and lost the
4305
+ * same rule for the same reason. */
4306
+ /* C19. The tile's own glyph, left of the title, so a pane and the floating
4307
+ * window it promotes into look like the same thing — `.twm-managed-window__icon`
4308
+ * has always drawn one. Sized to the BUTTONS at the other end of the strip
4309
+ * rather than to the title, because those are the other icons in this bar.
4310
+ *
4311
+ * AND THE GLYPH HAS TO BE TOLD, exactly as on `.twm-leaf__btn` below: Material
4312
+ * Symbols is a font and renders at `font-size`, which defaults to 24px. Leaving
4313
+ * this off does not give a slightly large icon — it gives one half again as
4314
+ * tall as the 22px bar it sits in, which pushes the chrome open. */
4315
+ /* THE COMPOUND SELECTOR IS THE POINT.
4316
+ *
4317
+ * The element is `<span class="twm-leaf__icon material-symbols-outlined">`, and
4318
+ * Google's own stylesheet — which every consumer loads, AFTER this one — sets
4319
+ * `.material-symbols-outlined { font-size: 24px }`. Two single-class selectors
4320
+ * tie on specificity, so the LAST one wins, and the last one is theirs. A rule
4321
+ * written `.twm-leaf__icon { font-size: 14px }` therefore exists, is valid,
4322
+ * looks correct in review, and does nothing.
4323
+ *
4324
+ * `.twm-leaf__icon.material-symbols-outlined` is two classes and wins outright.
4325
+ * The same tie is why `.twm-panel-toggle-btn .material-symbols-outlined` works
4326
+ * — a descendant selector is also two — and it is the fourth outing of the same
4327
+ * defect family in this codebase: a rule for a class the other side already
4328
+ * claims. */
4329
+ .twm-leaf__icon,
4330
+ .twm-leaf__icon.material-symbols-outlined {
4331
+ flex: 0 0 auto;
4332
+ font-size: 14px;
4333
+ line-height: 1;
4334
+ color: var(--text-muted);
4335
+ font-variation-settings: 'opsz' 20, 'wght' 400;
4336
+ }
4337
+
4338
+ /* Hidden, not empty, for a kind that has no glyph — the framework's own
4339
+ * `panel:*` tiles are in nobody's taxonomy. The chrome is a flex row with a
4340
+ * gap, and an empty span still takes its gap: the title would sit a glyph's
4341
+ * width off the left margin with nothing in front of it. */
4342
+ .twm-leaf__icon[hidden] { display: none; }
4343
+
4344
+ .twm-leaf__actions { display: inline-flex; gap: 2px; align-items: center; }
4345
+
4346
+ /* C18. The embedder's own buttons, to the LEFT of the structural ones and
4347
+ * separated from them — these act on the CONTENT, those act on the TILE. */
4348
+ .twm-leaf__actions--content {
4349
+ margin-left: auto;
4350
+ padding-right: var(--space-sm);
4351
+ margin-right: var(--space-sm);
4352
+ border-right: 1px solid var(--border-subtle);
4353
+ }
4354
+
4355
+ .twm-leaf__actions--content[hidden] { display: none; }
4356
+
4357
+ /* THE GLYPH HAS TO BE TOLD. Material Symbols ships at `font-size: 24px`, and
4358
+ * the button is 18px square — so every icon overflowed its own button, the
4359
+ * strip read as oversized next to a 22px chrome bar, and the buttons visually
4360
+ * collided. `font-size` IS the glyph size for an icon font; `width`/`height` on
4361
+ * the button does not constrain it. */
4362
+ .twm-leaf__btn .material-symbols-outlined {
4363
+ font-size: 14px;
4364
+ line-height: 1;
4365
+ /* The variable-font axes the family exposes. At 14px the default optical
4366
+ * size (24) draws strokes too heavy to read as a small control. */
4367
+ font-variation-settings: 'opsz' 20, 'wght' 400;
4368
+ }
4369
+
4370
+ .twm-leaf__btn:hover:not(:disabled) { background: var(--state-hover); color: var(--text-primary); }
4371
+
4372
+ /* C20. A verb that applies to this kind of pane but not right now — greyed and
4373
+ * explained by its tooltip, rather than removed. `:disabled` alone would still
4374
+ * take the hover highlight above, which reads as clickable. */
4375
+ .twm-leaf__btn--disabled,
4376
+ .twm-leaf__btn:disabled {
4377
+ opacity: 0.35;
4378
+ cursor: default;
4379
+ }
4380
+
4381
+ /* R8. THE SAME STRIP, IN A WINDOW. A pane floated into a managed window takes
4382
+ * its tabs with it — *"to window includes the tab-strip"* — and the strip in
4383
+ * the window is the strip from the pane: same component, same vocabulary, same
4384
+ * height. So it is the same rule, with a second selector, rather than a second
4385
+ * block that would drift from this one the first time either was touched.
4386
+ * `.twm-window-tabbar` is always the `--top` variant (a window's chrome is its
4387
+ * title bar, and the tabs go under it), so it appears in that block too. */
4388
+ .twm-leaf__tabbar,
4389
+ .twm-window-tabbar {
4390
+ /* RESTORED. This block's DECLARATIONS were missing — the selector list
4391
+ * opened a brace and the next rule's selectors were swallowed as its body,
4392
+ * which also took `--hidden` down with it: a CSS parser recovers at the
4393
+ * matching `}`, so BOTH rules were dead. `css/flexdesk.css` (the sheet
4394
+ * `build.mjs` actually ships, mirrored by hand because `css_extract.py` is
4395
+ * not in this repository) still carried them, which is why no consumer
4396
+ * loading `dist/` ever saw it — and why nothing caught it. What a consumer
4397
+ * loading `base.css` got was a tab strip with no height, no background and
4398
+ * `flex-shrink: 1`, and a `--hidden` modifier that hid nothing. */
4399
+ flex: 0 0 auto;
4400
+ display: flex;
4401
+ align-items: stretch;
4402
+ height: 22px;
4403
+ background: var(--surface-1);
4404
+ border-top: 1px solid var(--border-default);
4405
+ padding: 0 4px;
4406
+ overflow: hidden;
4407
+ user-select: none;
4408
+ font-size: var(--font-size-xs);
4409
+ }
4410
+ .twm-leaf__tabbar--hidden,
4411
+ .twm-window-tabbar--hidden { display: none; }
4412
+
4413
+ /* Lower-casing removed here too — a tab label is the same embedder-supplied
4414
+ * title as `.twm-leaf__title`, just in the strip at the bottom of the leaf. */
4415
+
4416
+ /* ══ C22. THE TOP TAB LAYOUT ═════════════════════════════════════════
4417
+ *
4418
+ * `tabLayout: 'top'` puts the strip BETWEEN the chrome and the body and draws
4419
+ * it with `NotebookTabBar` — the editor's tab bar — instead of the spreadsheet
4420
+ * strip above. The bar element is the same `.twm-leaf__tabbar`, so everything
4421
+ * it already carries (the fixed height, `--hidden`, `flex: 0 0 auto`) still
4422
+ * applies; this modifier only turns the strip the right way up.
4423
+ *
4424
+ * The border moves with it. A strip at the bottom of a tile is separated from
4425
+ * the body above it by a top border; a strip at the top is separated from the
4426
+ * body BELOW it by a bottom one, and keeping the top border as well would draw
4427
+ * a second line a pixel under the chrome's own. */
4428
+ .twm-leaf__tabbar--top,
4429
+ .twm-window-tabbar {
4430
+ border-top: none;
4431
+ border-bottom: 1px solid var(--border-default);
4432
+ /* The editor tabs are a touch taller than a 22px spreadsheet tab: they
4433
+ * carry a glyph, a label and a close button on one line, and 22px leaves
4434
+ * the close button touching the label's descenders. */
4435
+ height: 26px;
4436
+ padding: 0;
4437
+ /* `overflow: hidden` on the bottom strip clips the horizontal scroll the
4438
+ * tab list wants. The list scrolls itself (below); the BAR must not. */
4439
+ overflow: visible;
4440
+ }
4441
+
4442
+ /* `NotebookTabBar.mount()` assigns `className = 'tabs notebook-tabs'` to
4443
+ * whatever container it is handed (`notebook_tab_bar.js:61`) — it does not add
4444
+ * to it — which is why the renderer mounts it into a child of the bar rather
4445
+ * than into the bar. The child is that container, and it fills the bar. */
4446
+ .twm-leaf__tabbar--top > .notebook-tabs,
4447
+ .twm-window-tabbar > .notebook-tabs { flex: 1 1 auto; min-width: 0; }
4448
+
4449
+ /* A window's body is what the content mounts into, below the strip. It is
4450
+ * the box `.twm-managed-window__content` used to be for a window with no
4451
+ * tabs, so it carries the same three declarations — without them a grid
4452
+ * inside a floated pane sizes to its rows rather than to the window and the
4453
+ * strip is pushed off the bottom edge. */
4454
+ .twm-window-body { flex: 1 1 auto; min-height: 0; min-width: 0; }
4455
+
4456
+
4457
+ /* ══ NotebookTabBar ══════════════════════════════════════════════════
4458
+ *
4459
+ * A CLASS ONE SIDE KNOWS AND THE OTHER HAD NEVER HEARD OF — the fifth in this
4460
+ * codebase and the first one found before it shipped. `NotebookTabBar` has
4461
+ * rendered `.tabs` / `.tab` / `.tab-icon` / `.twm-tab-title` / `.tab-dirty` /
4462
+ * `.twm-tab-close` / `.nb-context-menu` since it was extracted, and NOT ONE of
4463
+ * those selectors existed in any FlexDesk stylesheet: the component imports
4464
+ * cleanly, mounts without error, is fully interactive, and draws as a row of
4465
+ * unstyled inline text with a `×` after each label. Nothing throws and no test
4466
+ * can see it, which is the signature of the whole family.
4467
+ *
4468
+ * The visual language is the tile chrome's, not an editor's: `--surface-1` bar,
4469
+ * `--surface-0` active tab so it reads as continuous with the body under it,
4470
+ * square corners, `--font-size-xs`. A tab bar drawn in one house style with the
4471
+ * chrome an inch above it in another is how a pane stops looking like one
4472
+ * object. */
4473
+ .tabs.notebook-tabs {
4474
+ display: flex;
4475
+ align-items: stretch;
4476
+ height: 100%;
4477
+ min-width: 0;
4478
+ background: var(--surface-1);
4479
+ user-select: none;
4480
+ font-size: var(--font-size-xs);
4481
+ /* SCROLL, DO NOT WRAP OR SHRINK. The tabs keep their width and the strip
4482
+ * scrolls under them — a tab that shrinks to nothing as you open the tenth
4483
+ * one is a tab you cannot read or aim at. The scrollbar itself is hidden:
4484
+ * it is a 26px strip, and a horizontal bar in it would eat a third of the
4485
+ * label. Wheel and drag still scroll it, and DragReorder auto-scrolls the
4486
+ * edges while a tab is being dragged (`drag_reorder.js:_autoScrollEdge`). */
4487
+ overflow-x: auto;
4488
+ overflow-y: hidden;
4489
+ scrollbar-width: none;
4490
+ }
4491
+
4492
+ .tabs.notebook-tabs::-webkit-scrollbar { display: none; }
4493
+
4494
+ .notebook-tabs > .tab {
4495
+ position: relative;
4496
+ flex: 0 0 auto;
4497
+ display: inline-flex;
4498
+ align-items: center;
4499
+ gap: var(--space-xs);
4500
+ max-width: 200px;
4501
+ padding: 0 var(--space-sm);
4502
+ background: var(--surface-1);
4503
+ color: var(--text-muted);
4504
+ border: none;
4505
+ border-right: 1px solid var(--border-subtle);
4506
+ cursor: pointer;
4507
+ line-height: 1;
4508
+ white-space: nowrap;
4509
+ }
4510
+
4511
+ .notebook-tabs > .tab:hover { background: var(--surface-2); color: var(--text-primary); }
4512
+
4513
+ /* The active tab is the body's colour and loses the line under it, so the tab
4514
+ * and the pane it shows read as one surface. The accent sits on TOP — the edge
4515
+ * furthest from the body — for the same reason a browser puts it there: it
4516
+ * marks the tab, not the boundary. */
4517
+ .notebook-tabs > .tab.active {
4518
+ background: var(--surface-0);
4519
+ color: var(--text-bright);
4520
+ }
4521
+
4522
+ .notebook-tabs > .tab.active::before {
4523
+ content: '';
4524
+ position: absolute;
4525
+ inset: 0 0 auto 0;
4526
+ height: 2px;
4527
+ pointer-events: none;
4528
+ background: var(--accent-bright, #1683c4);
4529
+ }
4530
+
4531
+ /* A PREVIEW tab — opened by a single click and replaced by the next one.
4532
+ * Italic is the convention every editor uses for "this one is not staying". */
4533
+ .notebook-tabs > .tab.tab--preview { font-style: italic; }
4534
+
4535
+ /* THE GLYPH HAS TO BE TOLD, and it has to be told in TWO selectors.
4536
+ *
4537
+ * `<span class="material-symbols-outlined tab-icon">` carries two classes, and
4538
+ * Google's stylesheet — loaded by every consumer, AFTER this one — sets
4539
+ * `.material-symbols-outlined { font-size: 24px }`. A single-class rule here
4540
+ * ties on specificity and loses to whichever came last, which is theirs. The
4541
+ * compound selector is two classes and wins outright. Fourth outing of this
4542
+ * exact defect; it is why `.twm-leaf__icon` is written the same way. */
4543
+ .notebook-tabs .tab-icon,
4544
+ .notebook-tabs .tab-icon.material-symbols-outlined {
4545
+ flex: 0 0 auto;
4546
+ font-size: 14px;
4547
+ line-height: 1;
4548
+ font-variation-settings: 'opsz' 20, 'wght' 400;
4549
+ }
4550
+
4551
+ .notebook-tabs .tab-icon[hidden] { display: none; }
4552
+
4553
+ /* The label. NOT lower-cased and not capitalised: it is the embedder's own
4554
+ * string — a record's display name, typed by a user who capitalised it on
4555
+ * purpose — for the same reason `.twm-leaf__title` lost that rule.
4556
+ *
4557
+ * AND IT NEEDS ITS OWN `line-height`, WHICH IS THE FIX FOR CLIPPED DESCENDERS.
4558
+ * `.tab` sets `line-height: 1` so the row's boxes stack predictably in a 26px
4559
+ * strip; this span sets `overflow: hidden` for the ellipsis, which makes it a
4560
+ * scroll container whose content box is exactly `1 × font-size` tall. Anything
4561
+ * drawn below the baseline — the tail of a `g`, a `y`, a `p` — falls outside
4562
+ * that box and is CLIPPED. It reads as a tab label with its bottom shaved off,
4563
+ * which is precisely how it was reported ("fix the cut-off text in the tabs -
4564
+ * cut off at bottom"), and it is invisible on any label that happens to have no
4565
+ * descender in it.
4566
+ *
4567
+ * 1.45 is the smallest value that clears the descender at every size this strip
4568
+ * is used at; the flex row still centres the span, so the tab's own height and
4569
+ * every other box in it are unchanged. */
4570
+ .notebook-tabs .twm-tab-title {
4571
+ overflow: hidden;
4572
+ text-overflow: ellipsis;
4573
+ letter-spacing: 0.02em;
4574
+ line-height: 1.45;
4575
+ }
4576
+
4577
+ /* Unsaved work. A dot rather than a `*`, and it takes the close button's slot
4578
+ * rather than adding to the row — in most editors the two swap on hover, but a
4579
+ * dot that MOVES the close button as it appears makes the button a target that
4580
+ * shifts under the pointer. */
4581
+ .notebook-tabs .tab-dirty {
4582
+ flex: 0 0 auto;
4583
+ font-size: 9px;
4584
+ line-height: 1;
4585
+ color: var(--accent-bright, #1683c4);
4586
+ }
4587
+
4588
+ .notebook-tabs .twm-tab-close {
4589
+ flex: 0 0 auto;
4590
+ display: inline-flex;
4591
+ align-items: center;
4592
+ justify-content: center;
4593
+ width: 14px;
4594
+ height: 14px;
4595
+ padding: 0;
4596
+ background: transparent;
4597
+ border: none;
4598
+ border-radius: var(--radius-sm);
4599
+ color: inherit;
4600
+ /* A `×` CHARACTER, not a Material Symbol — the component writes the glyph
4601
+ * itself (`notebook_tab_bar.js:168`), so this one is sized by `font-size`
4602
+ * on the button and there is no icon font to out-specify. */
4603
+ font-size: 13px;
4604
+ line-height: 1;
4605
+ opacity: 0.55;
4606
+ cursor: pointer;
4607
+ }
4608
+
4609
+ .notebook-tabs .twm-tab-close:hover {
4610
+ opacity: 1;
4611
+ background: var(--state-hover);
4612
+ color: var(--text-bright);
4613
+ }
4614
+
4615
+ /* ── Reordering ──────────────────────────────────────────────────────
4616
+ * `DragReorder` puts `.dragging` on the tab under the pointer and inserts a
4617
+ * `.twm-tab-drop-indicator` element at the position it would land
4618
+ * (`drag_reorder.js:_start`, `_ensureIndicator`). Neither had a rule, so a
4619
+ * drag looked exactly like a click that did nothing until the tab arrived
4620
+ * somewhere else. */
4621
+ .notebook-tabs > .tab.dragging { opacity: 0.4; }
4622
+
4623
+ .twm-tab-drop-indicator {
4624
+ flex: 0 0 auto;
4625
+ align-self: stretch;
4626
+ width: 2px;
4627
+ background: var(--accent-bright, #1683c4);
4628
+ pointer-events: none;
4629
+ }
4630
+
4631
+ /* ── C33. TWO MORE OF THE CLASS-ONE-SIDE-KNOWS FAMILY ────────────────
4632
+ *
4633
+ * Both of these have been set by JavaScript since the day they were
4634
+ * written and defined by NO stylesheet in this repository, so a
4635
+ * cross-pane drop has never drawn anything anywhere:
4636
+ *
4637
+ * `.notebook-tabs--drop-target` `notebook_tab_bar.js`,
4638
+ * `#onStripDragOver`
4639
+ * `.notebook-editor-container--drop-target` `../editor/editor_pane.js`,
4640
+ * its own `dragover`
4641
+ *
4642
+ * They are fixed here rather than left alone because C33 adds a THIRD
4643
+ * `--drop-target` affordance beside them (`.twm-leaf__tabbar--drop-target`,
4644
+ * in `css/overrides.css` with the other `.twm-leaf*` rules), and shipping a
4645
+ * live one next to two dead ones is how the family keeps growing. Same
4646
+ * declarations as the tile strip's, deliberately: three surfaces that mean
4647
+ * "let go here and this list takes it" should look like one thing.
4648
+ *
4649
+ * Neither direction of this defect is caught by tooling. `tools/css_check.mjs`
4650
+ * in the Tables consumer greps the JS side — a class the JS sets that no sheet
4651
+ * defines — but only for `tbl-`-prefixed names against that repository's own
4652
+ * stylesheet, so nothing here is in its scope at all. */
4653
+ .notebook-tabs--drop-target,
4654
+ .notebook-editor-container--drop-target {
4655
+ background: color-mix(in srgb, var(--accent-bright, #1683c4) 18%, transparent);
4656
+ box-shadow: inset 0 0 0 1px var(--accent-bright, #1683c4);
4657
+ }
4658
+
4659
+ /* ── The tab bar's own context menu ──────────────────────────────────
4660
+ * Reached only by a consumer that lets the component open it — the tile
4661
+ * renderer suppresses it in favour of the WM's single tab menu, so these rules
4662
+ * are for `EditorPane` and anyone else mounting a `NotebookTabBar` directly.
4663
+ * Same geometry and the same tokens as `.twm-context-menu`, because two
4664
+ * context menus in one application that disagree about their own padding is
4665
+ * how a UI stops feeling assembled. */
4666
+ .nb-context-menu {
4667
+ position: fixed;
4668
+ z-index: 10050;
4669
+ min-width: 180px;
4670
+ padding: var(--space-xxs) 0;
4671
+ background: var(--surface-2);
4672
+ border: 1px solid var(--border-strong);
4673
+ border-radius: var(--radius-sm);
4674
+ box-shadow: 0 6px 20px rgba(0, 0, 0, 0.45);
4675
+ font-size: var(--font-size-xs);
4676
+ /* Opened hidden and revealed on the next frame (`#showContextMenu` adds
4677
+ * `.visible` in a rAF) so it can fade rather than snap — the same
4678
+ * open-then-reveal the collapsible primitive uses, and the same reason its
4679
+ * `.visible` rule had to exist before anything worked. */
4680
+ opacity: 0;
4681
+ transition: opacity 90ms ease-out;
4682
+ }
4683
+
4684
+ .nb-context-menu.visible { opacity: 1; }
4685
+
4686
+ .nb-context-menu__item {
4687
+ display: flex;
4688
+ align-items: center;
4689
+ gap: var(--space-sm);
4690
+ width: 100%;
4691
+ padding: var(--space-xs) var(--space-md);
4692
+ background: transparent;
4693
+ border: none;
4694
+ color: var(--text-primary);
4695
+ text-align: left;
4696
+ cursor: pointer;
4697
+ white-space: nowrap;
4698
+ }
4699
+
4700
+ .nb-context-menu__item:hover { background: var(--state-hover); }
4701
+
4702
+ .nb-context-menu__item .material-symbols-outlined {
4703
+ font-size: 14px;
4704
+ line-height: 1;
4705
+ color: var(--text-muted);
4706
+ font-variation-settings: 'opsz' 20, 'wght' 400;
4707
+ }
4708
+
4709
+ .nb-context-menu__item--danger { color: var(--color-danger, #c74e4e); }
4710
+
4711
+ .nb-context-menu__item--danger .material-symbols-outlined { color: inherit; }
4712
+
4713
+ /* DISABLED MEANS DISABLED. `.twm-context-menu-item.disabled` was the third
4714
+ * instance of this family — the JS had always set the class, no rule existed,
4715
+ * and a disabled item rendered at full opacity with a pointer cursor and did
4716
+ * nothing when clicked. `#showContextMenu` binds no listener on a disabled
4717
+ * item, so `pointer-events: none` is belt to that braces. */
4718
+ .nb-context-menu__item--disabled {
4719
+ opacity: 0.4;
4720
+ cursor: default;
4721
+ pointer-events: none;
4722
+ }
4723
+
4724
+ .nb-context-menu__shortcut {
4725
+ margin-left: auto;
4726
+ padding-left: var(--space-lg);
4727
+ color: var(--text-dim);
4728
+ }
4729
+
4730
+ .nb-context-menu__separator {
4731
+ height: 1px;
4732
+ margin: var(--space-xxs) 0;
4733
+ background: var(--border-subtle);
4734
+ }
4735
+
4736
+ /* R1. The containing block an ESCAPED window resolves against — a window
4737
+ * dragged out of its pane is re-parented here for the length of the drag.
4738
+ * `relative` only, for the reason `.twm-leaf` documents: `transform`, `filter`,
4739
+ * `contain` and `will-change` each create a containing block AND trap
4740
+ * `position: fixed` descendants, which would clip every dropdown the grid opens
4741
+ * on purpose. */
4742
+ .twm-root { position: relative; }