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.
- package/css/base.css +1484 -181
- package/css/flexdesk.css +1311 -18
- package/css/overrides.css +44 -0
- package/css/tokens.css +45 -0
- package/dist/charts.js +5 -3
- package/dist/charts.js.map +1 -1
- package/dist/{chunk-DVU44T77.js → chunk-ELXVW542.js} +196 -75
- package/dist/chunk-ELXVW542.js.map +7 -0
- package/dist/chunk-LH5TSOZW.js +1237 -0
- package/dist/chunk-LH5TSOZW.js.map +7 -0
- package/dist/{chunk-TLZUUFOE.js → chunk-O5OHMWBB.js} +10 -2
- package/dist/chunk-O5OHMWBB.js.map +7 -0
- package/dist/{chunk-CT4YXXLP.js → chunk-QIU5S2RU.js} +371 -73
- package/dist/chunk-QIU5S2RU.js.map +7 -0
- package/dist/chunk-QNQHQ24V.js +408 -0
- package/dist/chunk-QNQHQ24V.js.map +7 -0
- package/dist/{chunk-DRYCDMEG.js → chunk-XKDTIT4Q.js} +168 -12
- package/dist/chunk-XKDTIT4Q.js.map +7 -0
- package/dist/editor.js +3 -380
- package/dist/editor.js.map +3 -3
- package/dist/flexdesk.css +1311 -18
- package/dist/tiles.js +168 -41
- package/dist/tiles.js.map +2 -2
- package/dist/tokens.css +45 -0
- package/dist/widgets.js +44 -14
- package/dist/widgets.js.map +2 -2
- package/dist/wm.js +2983 -142
- package/dist/wm.js.map +4 -4
- package/package.json +3 -2
- package/src/charts/chart_types.js +167 -0
- package/src/charts/plotly_wrapper.js +178 -10
- package/src/editor/notebook_tab_bar.js +39 -3
- package/src/tiles/tile_base.js +143 -35
- package/src/tiles/tile_grid.js +52 -1
- package/src/tiling/command_palette.js +71 -18
- package/src/tiling/desktops.js +36 -12
- package/src/tiling/keymap.js +24 -4
- package/src/tiling/shell.js +135 -24
- package/src/tiling/tab_strip.js +184 -0
- package/src/tiling/tile_breadcrumb.js +34 -2
- package/src/tiling/tile_renderer.js +1386 -21
- package/src/tiling/tile_tab_menu.js +101 -0
- package/src/tiling/tile_tree.js +82 -0
- package/src/tiling/wm.js +2352 -74
- package/src/ui/components/action_dropdown.js +34 -3
- package/src/ui/components/autocomplete_field.js +65 -13
- package/src/ui/components/context_menu.js +79 -8
- package/src/ui/components/data_table.js +508 -84
- package/src/ui/components/managed_window.js +928 -36
- package/src/ui/components/modal.js +214 -8
- package/dist/chunk-CT4YXXLP.js.map +0 -7
- package/dist/chunk-DRYCDMEG.js.map +0 -7
- package/dist/chunk-DVU44T77.js.map +0 -7
- package/dist/chunk-TLZUUFOE.js.map +0 -7
- package/dist/chunk-UCJ2WD4D.js +0 -625
- 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
|
-
/*
|
|
2346
|
-
|
|
2347
|
-
|
|
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
|
-
|
|
2412
|
+
justify-content: space-between;
|
|
2350
2413
|
align-items: center;
|
|
2351
|
-
justify-content: center;
|
|
2352
|
-
height: 100%;
|
|
2353
2414
|
gap: var(--space-md);
|
|
2354
|
-
|
|
2355
|
-
|
|
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
|
-
.
|
|
2363
|
-
|
|
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
|
-
|
|
2380
|
-
|
|
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:
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
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-
|
|
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
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
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
|
-
|
|
2413
|
-
.
|
|
2414
|
-
|
|
2456
|
+
|
|
2457
|
+
.tile-grid-toolbar .toolbar-btn .material-symbols-outlined {
|
|
2458
|
+
font-size: var(--font-size-xl);
|
|
2415
2459
|
}
|
|
2416
|
-
|
|
2417
|
-
|
|
2460
|
+
|
|
2461
|
+
.tile-grid-toolbar .btn-text {
|
|
2418
2462
|
white-space: nowrap;
|
|
2419
|
-
overflow: hidden;
|
|
2420
|
-
text-overflow: ellipsis;
|
|
2421
2463
|
}
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
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
|
-
/*
|
|
2431
|
-
|
|
2432
|
-
.
|
|
2433
|
-
|
|
2434
|
-
|
|
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
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
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
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
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
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
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
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
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
|
-
|
|
2482
|
-
|
|
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
|
-
|
|
2486
|
-
.
|
|
2487
|
-
|
|
2488
|
-
|
|
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
|
-
|
|
2501
|
-
.
|
|
2502
|
-
|
|
2525
|
+
|
|
2526
|
+
.tile.dragging {
|
|
2527
|
+
opacity: 0.5;
|
|
2528
|
+
z-index: var(--z-dropdown);
|
|
2529
|
+
box-shadow: var(--shadow-lg);
|
|
2503
2530
|
}
|
|
2504
|
-
|
|
2505
|
-
|
|
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
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
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
|
-
|
|
2512
|
-
|
|
2546
|
+
|
|
2547
|
+
.tile.tile--selected .tile-header {
|
|
2548
|
+
background: rgba(120, 185, 255, 0.1);
|
|
2513
2549
|
}
|
|
2514
2550
|
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
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
|
-
/*
|
|
2525
|
-
|
|
2526
|
-
|
|
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
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
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
|
-
/*
|
|
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
|
-
.
|
|
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
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
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
|
-
|
|
2552
|
-
|
|
2589
|
+
|
|
2590
|
+
.tile-drag-handle:hover {
|
|
2591
|
+
color: rgba(255, 255, 255, 0.6);
|
|
2553
2592
|
}
|
|
2554
|
-
|
|
2555
|
-
.
|
|
2556
|
-
|
|
2557
|
-
opacity: 0;
|
|
2558
|
-
pointer-events: none;
|
|
2593
|
+
|
|
2594
|
+
.tile-drag-handle:active {
|
|
2595
|
+
cursor: grabbing;
|
|
2559
2596
|
}
|
|
2560
|
-
|
|
2561
|
-
.
|
|
2562
|
-
|
|
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
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
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; }
|