@cplieger/web-terminal-ui 3.0.4 → 3.2.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/README.md +24 -11
- package/css/00-tokens.css +13 -1
- package/css/02-app.css +24 -120
- package/css/03-primitives.css +82 -0
- package/css/04-feedback.css +166 -0
- package/css/05-tabs.css +312 -0
- package/css/06-mobile.css +335 -0
- package/css/07-animations.css +102 -0
- package/css/MANIFEST +7 -1
- package/package.json +3 -3
- package/src/features/clipboard.ts +10 -0
- package/src/features/connection-banner.ts +1 -1
- package/src/features/context-menu.ts +174 -42
- package/src/features/mobile-toolbar.ts +59 -1
- package/src/features/scroll-to-bottom.ts +22 -6
- package/src/features/tabs.ts +1360 -254
- package/src/kernel/kernel.ts +65 -4
- package/src/kernel/types.ts +8 -0
- package/src/presets.ts +43 -6
- package/src/viewport.ts +16 -1
- package/css/03-v3.css +0 -705
package/README.md
CHANGED
|
@@ -85,26 +85,33 @@ render/connection/scroll modules are single-instance per page (tabs multiplex
|
|
|
85
85
|
sessions over the one kernel). `scaffold/index.html` is a complete reference page
|
|
86
86
|
to copy and adapt.
|
|
87
87
|
|
|
88
|
-
|
|
88
|
+
Four presets are provided (import from `@cplieger/web-terminal-ui/presets`); each
|
|
89
89
|
is a plain feature-array factory, so you can spread and edit it, or hand-pick
|
|
90
90
|
individual features instead:
|
|
91
91
|
|
|
92
92
|
- `presetSingle()` — single-pane desktop UI (context menu, clipboard,
|
|
93
93
|
scroll-to-bottom, predictive echo, connection banner).
|
|
94
94
|
- `presetTouch()` — `presetSingle()` plus the mobile key toolbar.
|
|
95
|
-
- `presetTabbed()` — the
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
95
|
+
- `presetTabbed()` — the generic tabbed UI: `presetTouch()` plus tabs and
|
|
96
|
+
animations. Tabs are label-only (no activity dots); each tab's title follows
|
|
97
|
+
the process window title (OSC 0/2) when the program sets one and keeps it
|
|
98
|
+
updated, otherwise the last command submitted. Requires a server that speaks
|
|
99
|
+
the session API (`/api/sessions` and `/ws?session=`), such as
|
|
100
|
+
`web-terminal-server`.
|
|
101
|
+
- `presetAgentTabbed()` — `presetTabbed()` plus the activity monitor, so each
|
|
102
|
+
tab carries a live status dot (idle / working / done / needs-input) from the
|
|
103
|
+
server's status SSE. Same title behavior as `presetTabbed`. For an agent shell
|
|
104
|
+
such as `vibecli`.
|
|
99
105
|
|
|
100
106
|
### Options
|
|
101
107
|
|
|
102
|
-
| Option | Default | Purpose
|
|
103
|
-
| ----------- | -------------------------- |
|
|
104
|
-
| `features` | _(none — bare kernel)_ | The feature list. Omitted or empty builds only the terminal (no chrome). Use a preset from `./presets` or a hand-picked array.
|
|
105
|
-
| `wsPath` | `"/ws"` | WebSocket endpoint path the engine connects to.
|
|
106
|
-
| `fontReady` | `'14px "MonaspiceNe NFM"'` | CSS font shorthand awaited before the first resize, so the server is sized against the real web font's cell metrics rather than a fallback.
|
|
107
|
-
| `loading` | _(none)_ | A pre-JS loading overlay element (kept in your served HTML so it paints before this module loads); it is faded out and removed once the first frame renders.
|
|
108
|
+
| Option | Default | Purpose |
|
|
109
|
+
| ----------- | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
110
|
+
| `features` | _(none — bare kernel)_ | The feature list. Omitted or empty builds only the terminal (no chrome). Use a preset from `./presets` or a hand-picked array. |
|
|
111
|
+
| `wsPath` | `"/ws"` | WebSocket endpoint path the engine connects to. |
|
|
112
|
+
| `fontReady` | `'14px "MonaspiceNe NFM"'` | CSS font shorthand awaited before the first resize, so the server is sized against the real web font's cell metrics rather than a fallback. |
|
|
113
|
+
| `loading` | _(none)_ | A pre-JS loading overlay element (kept in your served HTML so it paints before this module loads); it is faded out and removed once the first frame renders. |
|
|
114
|
+
| `theme` | _(none)_ | Theme overrides (CSS custom properties on the terminal root): `--accent`, `--tab-hover-bg`, `--tab-active-bg`, `--tab-active-fg`. The library ships neutral defaults. |
|
|
108
115
|
|
|
109
116
|
`createTerminal()` returns a handle: `focus()` re-focuses the terminal input
|
|
110
117
|
(and opens the soft keyboard on touch), and `destroy()` tears every feature down
|
|
@@ -134,6 +141,12 @@ Consumers that ship this UI:
|
|
|
134
141
|
- [`vibekit`](https://github.com/cplieger/vibekit)
|
|
135
142
|
- [`vibecli`](https://github.com/cplieger/vibecli)
|
|
136
143
|
|
|
144
|
+
## Disclaimer
|
|
145
|
+
|
|
146
|
+
This project is built with care and follows security best practices, but it is intended for personal / self-hosted use. No guarantees of fitness for production environments. Use at your own risk.
|
|
147
|
+
|
|
148
|
+
This project was built with AI-assisted tooling using [Claude Opus](https://www.anthropic.com/claude) and [Kiro](https://kiro.dev). The human maintainer defines architecture, supervises implementation, and makes all final decisions.
|
|
149
|
+
|
|
137
150
|
## License
|
|
138
151
|
|
|
139
152
|
GPL-3.0-or-later. See `LICENSE`.
|
package/css/00-tokens.css
CHANGED
|
@@ -7,13 +7,25 @@
|
|
|
7
7
|
--border: oklch(100% 0 0deg / 15%);
|
|
8
8
|
--text: oklch(90% 0.005 280deg);
|
|
9
9
|
--text-active: color-mix(in oklch, var(--text) 15%, transparent);
|
|
10
|
+
/* ===== Themeable tokens (the consumer-configurable "template") =====
|
|
11
|
+
These default to the neutral built-in look; a consumer recolors the UI by
|
|
12
|
+
overriding them via createTerminal's `theme` option (which sets them on the
|
|
13
|
+
terminal root) or its own CSS. vibecli sets a purple palette. Everything
|
|
14
|
+
above is the fixed design palette; everything here is the public theme API. */
|
|
15
|
+
--accent: #a78bfa; /* accent action colour (mobile "+", toggled key button) */
|
|
16
|
+
--tab-hover-bg: var(--text-active); /* hovered inactive tab / pressed controls */
|
|
17
|
+
--tab-active-bg: color-mix(in oklch, var(--text) 22%, var(--surface)); /* active tab fill */
|
|
18
|
+
--tab-active-fg: var(--text); /* active tab text */
|
|
10
19
|
--radius: 0.75rem;
|
|
11
20
|
--radius-inner: 0.5rem;
|
|
12
21
|
--touch-target: 2.75rem; /* 44px — Apple HIG minimum */
|
|
13
22
|
--pill-outer: calc(
|
|
14
23
|
var(--touch-target) + 2 * var(--sp-1) + 2px
|
|
15
24
|
); /* touch-target + 2×padding + 2×border */
|
|
16
|
-
|
|
25
|
+
/* Mobile bottom bar height (a 44px control + the bar's 2×sp-2 padding + its
|
|
26
|
+
1px top border). Bottom-anchored chrome (banner, toast, scroll-to-bottom,
|
|
27
|
+
key grid) lifts by this so it clears the bar. */
|
|
28
|
+
--wt-switcher-h: calc(var(--touch-target) + 2 * var(--sp-2) + 1px);
|
|
17
29
|
--icon: 1.125rem; /* 18px */
|
|
18
30
|
--icon-desktop: 1rem; /* 16px */
|
|
19
31
|
--sp-1: 0.25rem;
|
package/css/02-app.css
CHANGED
|
@@ -10,6 +10,15 @@
|
|
|
10
10
|
above the viewport), so native scroll anchoring is safe to leave on
|
|
11
11
|
and keeps the read position steady on the rare insert-above. */
|
|
12
12
|
overflow-anchor: auto;
|
|
13
|
+
/* .term is the scroll container, so the touch-action intersection for a
|
|
14
|
+
touch on the output stops here (the html/body pan-x pan-y does not reach
|
|
15
|
+
inside a nested scroller). Declare it here too: native one/two-finger
|
|
16
|
+
scroll is allowed, double-tap-zoom is blocked (snappy taps, no 300ms
|
|
17
|
+
delay), and — critically — this does NOT affect native long-press text
|
|
18
|
+
selection (touch-action governs scroll/zoom, not selection). Avoid the
|
|
19
|
+
`manipulation` alias: it breaks pointercancel-on-scroll in Safari
|
|
20
|
+
(WebKit bug 240917), which the switcher-bar gesture relies on. */
|
|
21
|
+
touch-action: pan-x pan-y;
|
|
13
22
|
font-family: var(--font-mono);
|
|
14
23
|
font-size: 14px;
|
|
15
24
|
line-height: 17px;
|
|
@@ -94,8 +103,22 @@
|
|
|
94
103
|
textarea owns input, the keyboard, and IME. Keeping the editable
|
|
95
104
|
element off the scroll content is what lets the first touch-drag
|
|
96
105
|
scroll instead of placing a caret (bug 6), and not re-rendering it
|
|
97
|
-
as an editable keeps a selection alive across redraws (bug 1).
|
|
106
|
+
as an editable keeps a selection alive across redraws (bug 1).
|
|
107
|
+
|
|
108
|
+
The two selection properties below are load-bearing on touch: the
|
|
109
|
+
browser's native long-press-to-select-a-word + OS copy/paste callout
|
|
110
|
+
(iOS Safari and Android Chrome) is our selection UX, so we opt IN
|
|
111
|
+
explicitly and must never let an ancestor override them. `user-select`
|
|
112
|
+
is spec'd as non-inherited but WebKit/Chromium treat it as inherited,
|
|
113
|
+
so a stray `user-select: none` on html/body/.term would silently kill
|
|
114
|
+
selection here; keep it `text` (modern Safari honors the unprefixed
|
|
115
|
+
form, so no `-webkit-` pair is needed). `-webkit-touch-callout: default`
|
|
116
|
+
keeps the iOS callout (a page-level `none` suppresses it) and has no
|
|
117
|
+
standard equivalent. No JS may `preventDefault` a touch/pointer/
|
|
118
|
+
contextmenu over this element or the native selection dies (see the
|
|
119
|
+
kernel focus handling + contextMenu feature). */
|
|
98
120
|
user-select: text;
|
|
121
|
+
-webkit-touch-callout: default;
|
|
99
122
|
}
|
|
100
123
|
|
|
101
124
|
/* IME composition view: floating overlay positioned at the cursor
|
|
@@ -417,70 +440,6 @@
|
|
|
417
440
|
display: none;
|
|
418
441
|
}
|
|
419
442
|
|
|
420
|
-
/* Desktop: scroll-to-bottom is still genuinely useful for jumping back
|
|
421
|
-
after scrolling up to read history, so it surfaces here as a
|
|
422
|
-
bottom-center pill with a label. .key-toolbar.scrolled-up is
|
|
423
|
-
toggled by app.ts, same as on touch. The display+opacity transition
|
|
424
|
-
uses allow-discrete so the fade-out plays cleanly when the user
|
|
425
|
-
clicks (scroll-to-bottom clears .scrolled-up, reverting display:none
|
|
426
|
-
after the opacity has gone to 0). */
|
|
427
|
-
.kb-scroll-bottom {
|
|
428
|
-
position: fixed;
|
|
429
|
-
left: 50%;
|
|
430
|
-
bottom: calc(var(--safe-bottom, 0px) + var(--sp-3));
|
|
431
|
-
transform: translateX(-50%);
|
|
432
|
-
display: none;
|
|
433
|
-
opacity: 0;
|
|
434
|
-
align-items: center;
|
|
435
|
-
gap: var(--sp-1);
|
|
436
|
-
padding: var(--sp-2) var(--sp-3);
|
|
437
|
-
background: var(--surface);
|
|
438
|
-
backdrop-filter: var(--blur);
|
|
439
|
-
border: 1px solid var(--border);
|
|
440
|
-
border-radius: var(--radius);
|
|
441
|
-
color: var(--text);
|
|
442
|
-
font: inherit;
|
|
443
|
-
font-size: 0.875rem;
|
|
444
|
-
font-weight: 500;
|
|
445
|
-
cursor: pointer;
|
|
446
|
-
z-index: 10;
|
|
447
|
-
transition:
|
|
448
|
-
opacity var(--dur-standard) var(--ease-standard),
|
|
449
|
-
background-color var(--dur-micro) var(--ease-standard),
|
|
450
|
-
display var(--dur-standard) allow-discrete;
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
.kb-scroll-bottom::after {
|
|
454
|
-
content: "Scroll to bottom";
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
.kb-scroll-bottom svg {
|
|
458
|
-
width: var(--icon);
|
|
459
|
-
height: var(--icon);
|
|
460
|
-
stroke: currentcolor;
|
|
461
|
-
fill: none;
|
|
462
|
-
stroke-width: 2;
|
|
463
|
-
stroke-linecap: round;
|
|
464
|
-
stroke-linejoin: round;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
.kb-scroll-bottom:hover {
|
|
468
|
-
background: color-mix(in oklch, var(--text) 8%, var(--surface));
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
.kb-scroll-bottom:active {
|
|
472
|
-
background: var(--text-active);
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
.key-toolbar.scrolled-up .kb-scroll-bottom {
|
|
476
|
-
display: inline-flex;
|
|
477
|
-
opacity: 1;
|
|
478
|
-
|
|
479
|
-
@starting-style {
|
|
480
|
-
opacity: 0;
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
|
|
484
443
|
@media (pointer: coarse) {
|
|
485
444
|
.key-toolbar {
|
|
486
445
|
position: fixed;
|
|
@@ -516,11 +475,6 @@
|
|
|
516
475
|
height: var(--pill-outer);
|
|
517
476
|
}
|
|
518
477
|
|
|
519
|
-
/* Collapsed + scrolled up: 2 buttons tall (+2px border). */
|
|
520
|
-
.key-toolbar.collapsed.scrolled-up {
|
|
521
|
-
height: calc(2 * var(--touch-target) + var(--sp-1) + 2 * var(--sp-1) + 2px);
|
|
522
|
-
}
|
|
523
|
-
|
|
524
478
|
/* Grid placement: toggle at col 5 row 1 (top-right). */
|
|
525
479
|
.kb-toggle {
|
|
526
480
|
grid-area: 1 / 5;
|
|
@@ -551,55 +505,6 @@
|
|
|
551
505
|
grid-area: 2 / 4;
|
|
552
506
|
}
|
|
553
507
|
|
|
554
|
-
/* Scroll-bottom: col 5 row 2 (below toggle). Resets the desktop
|
|
555
|
-
FAB-specific properties (fixed position, pill padding, surface
|
|
556
|
-
background, label) so the button collapses back to a grid-area
|
|
557
|
-
icon-only key. */
|
|
558
|
-
.key-toolbar .kb-scroll-bottom {
|
|
559
|
-
position: static;
|
|
560
|
-
transform: none;
|
|
561
|
-
bottom: auto;
|
|
562
|
-
left: auto;
|
|
563
|
-
grid-area: 2 / 5;
|
|
564
|
-
display: none;
|
|
565
|
-
opacity: 0;
|
|
566
|
-
padding: 0;
|
|
567
|
-
background: transparent;
|
|
568
|
-
backdrop-filter: none;
|
|
569
|
-
border: none;
|
|
570
|
-
border-radius: var(--radius-inner);
|
|
571
|
-
font-size: inherit;
|
|
572
|
-
/* Touch override drops the desktop label (::after content: "")
|
|
573
|
-
so only the SVG remains. The base rule sets align-items:center
|
|
574
|
-
but inherits justify-content:flex-start, which would push the
|
|
575
|
-
lone icon to the left edge of the touch-target-sized cell —
|
|
576
|
-
same unequal-padding issue as the toolbar toggle. Center it. */
|
|
577
|
-
justify-content: center;
|
|
578
|
-
transition:
|
|
579
|
-
opacity var(--dur-standard) var(--ease-standard),
|
|
580
|
-
display var(--dur-standard) allow-discrete;
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
.kb-scroll-bottom::after {
|
|
584
|
-
/* No label on touch. Remove the box entirely (not just its text) so
|
|
585
|
-
it isn't an empty flex item — combined with the base rule's
|
|
586
|
-
gap:var(--sp-1) it would shove the lone icon half a gap off-centre. */
|
|
587
|
-
content: "";
|
|
588
|
-
display: none;
|
|
589
|
-
}
|
|
590
|
-
.kb-scroll-bottom:hover {
|
|
591
|
-
background: transparent;
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
.key-toolbar.scrolled-up .kb-scroll-bottom {
|
|
595
|
-
display: flex;
|
|
596
|
-
opacity: 1;
|
|
597
|
-
|
|
598
|
-
@starting-style {
|
|
599
|
-
opacity: 0;
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
|
|
603
508
|
/* Keys fade in/out during open/close. The container's width/height
|
|
604
509
|
transition handles the background slide; opacity handles the
|
|
605
510
|
individual button appearance. Stagger the fade slightly after
|
|
@@ -691,7 +596,6 @@
|
|
|
691
596
|
|
|
692
597
|
/* No-transition on first render */
|
|
693
598
|
.key-toolbar.no-transition,
|
|
694
|
-
.key-toolbar.no-transition .kb-scroll-bottom,
|
|
695
599
|
.key-toolbar.no-transition .kb-toggle .icon-hamburger,
|
|
696
600
|
.key-toolbar.no-transition .kb-toggle .icon-close {
|
|
697
601
|
transition: none;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/* ===== web-terminal-ui design system: primitives =====
|
|
2
|
+
The v3 kernel builds named layout regions (wt-region-*) as siblings of the
|
|
3
|
+
terminal surface (.term, styled in 02-app.css); features mount chrome into
|
|
4
|
+
region slots. The regions are transparent groupings (display: contents) so
|
|
5
|
+
each feature's element floats over the fixed-inset terminal, and an absent
|
|
6
|
+
feature contributes nothing.
|
|
7
|
+
|
|
8
|
+
The stylesheet is layered by concern and concatenated in css/MANIFEST order:
|
|
9
|
+
tokens (00) -> reset (01) -> terminal core + legacy key toolbar (02) -> these
|
|
10
|
+
shared primitives (03) -> per-feature chrome (04 feedback, 05 tabs, 06 mobile)
|
|
11
|
+
-> animations (07, where every .wt-animate-gated transition/keyframe lives, so
|
|
12
|
+
motion is opt-in from one place). Cascade order is the file order here.
|
|
13
|
+
|
|
14
|
+
Primitives are the building blocks every feature reuses: the transparent
|
|
15
|
+
layout regions, the 44px touch button, and the toast surface. */
|
|
16
|
+
|
|
17
|
+
.wt-region,
|
|
18
|
+
.wt-slot {
|
|
19
|
+
display: contents;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/* ===== Shared button primitive (44px touch target) ===== */
|
|
23
|
+
.wt-btn {
|
|
24
|
+
display: inline-flex;
|
|
25
|
+
align-items: center;
|
|
26
|
+
justify-content: center;
|
|
27
|
+
min-width: var(--touch-target);
|
|
28
|
+
min-height: var(--touch-target);
|
|
29
|
+
background: transparent;
|
|
30
|
+
color: var(--text);
|
|
31
|
+
border: none;
|
|
32
|
+
border-radius: var(--radius-inner);
|
|
33
|
+
font: inherit;
|
|
34
|
+
cursor: pointer;
|
|
35
|
+
touch-action: manipulation;
|
|
36
|
+
user-select: none;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.wt-btn:active {
|
|
40
|
+
background: var(--text-active);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.wt-btn svg {
|
|
44
|
+
width: var(--icon);
|
|
45
|
+
height: var(--icon);
|
|
46
|
+
stroke: currentcolor;
|
|
47
|
+
fill: none;
|
|
48
|
+
stroke-width: 2;
|
|
49
|
+
stroke-linecap: round;
|
|
50
|
+
stroke-linejoin: round;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/* ===== Toast (kernel shared primitive) =====
|
|
54
|
+
Top-centre on every form factor (both desktop and the tabbed mobile UI dock a
|
|
55
|
+
bar at the bottom, so a bottom toast would collide with it; top-centre is
|
|
56
|
+
clear and the same everywhere). The vv-top/safe-top offset keeps it below the
|
|
57
|
+
notch and follows iOS's visual-viewport shift. */
|
|
58
|
+
.wt-toast {
|
|
59
|
+
position: fixed;
|
|
60
|
+
top: calc(var(--safe-top, 0px) + var(--vv-top, 0px) + var(--sp-3));
|
|
61
|
+
left: 50%;
|
|
62
|
+
transform: translateX(-50%);
|
|
63
|
+
max-width: calc(100% - 2 * var(--sp-3));
|
|
64
|
+
padding: var(--sp-1) var(--sp-3);
|
|
65
|
+
background: var(--surface);
|
|
66
|
+
backdrop-filter: var(--blur);
|
|
67
|
+
border: 1px solid oklch(70% 0.1 160deg / 30%);
|
|
68
|
+
border-radius: var(--radius);
|
|
69
|
+
color: var(--text);
|
|
70
|
+
font-family: var(--font-ui);
|
|
71
|
+
font-size: 0.8125rem;
|
|
72
|
+
font-weight: 500;
|
|
73
|
+
text-align: center;
|
|
74
|
+
z-index: 40;
|
|
75
|
+
display: none;
|
|
76
|
+
opacity: 0;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.wt-toast.visible {
|
|
80
|
+
display: block;
|
|
81
|
+
opacity: 1;
|
|
82
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/* ===== Feedback + overlay chrome =====
|
|
2
|
+
Transient, non-tab feature surfaces: the connection banner, the
|
|
3
|
+
scroll-to-bottom control, the right-click/long-press context menu, and the
|
|
4
|
+
tab catching-up cue. Grouped because they are all fixed, floating, and
|
|
5
|
+
short-lived; their motion lives in 07-animations.css. */
|
|
6
|
+
|
|
7
|
+
/* ===== Connection banner (connectionBanner feature) =====
|
|
8
|
+
Top-centre on every form factor (same rationale as the toast: a bottom bar is
|
|
9
|
+
always docked now). */
|
|
10
|
+
.wt-conn-banner {
|
|
11
|
+
position: fixed;
|
|
12
|
+
top: calc(var(--safe-top, 0px) + var(--vv-top, 0px) + var(--sp-3));
|
|
13
|
+
left: 50%;
|
|
14
|
+
transform: translateX(-50%);
|
|
15
|
+
max-width: calc(100% - 2 * var(--sp-3));
|
|
16
|
+
padding: var(--sp-1) var(--sp-3);
|
|
17
|
+
background: var(--surface);
|
|
18
|
+
backdrop-filter: var(--blur);
|
|
19
|
+
border: 1px solid var(--border);
|
|
20
|
+
border-radius: var(--radius);
|
|
21
|
+
color: var(--text);
|
|
22
|
+
font-family: var(--font-ui);
|
|
23
|
+
font-size: 0.8125rem;
|
|
24
|
+
font-weight: 500;
|
|
25
|
+
text-align: center;
|
|
26
|
+
z-index: 20;
|
|
27
|
+
display: none;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.wt-conn-banner.visible {
|
|
31
|
+
display: block;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.wt-conn-banner[data-state="reconnecting"] {
|
|
35
|
+
border-color: oklch(70% 0.15 60deg / 40%);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.wt-conn-banner[data-state="offline"] {
|
|
39
|
+
color: #f55;
|
|
40
|
+
border-color: oklch(60% 0.2 25deg / 50%);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.wt-conn-banner[data-state="restarted"] {
|
|
44
|
+
color: #fa5;
|
|
45
|
+
border-color: oklch(70% 0.15 60deg / 40%);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/* ===== Scroll-to-bottom (scrollToBottom feature) =====
|
|
49
|
+
A compact floating icon button docked in the bottom-right corner, shown only
|
|
50
|
+
while the user has scrolled up. Same button on both form factors (the old
|
|
51
|
+
desktop bottom-centre text pill was dropped in the design-system switch, and
|
|
52
|
+
its dead CSS removed from 02-app.css); each form factor just lifts it above
|
|
53
|
+
its own bottom bar. Size + icon centering come from the .wt-btn primitive (a
|
|
54
|
+
44px icon button); this rule adds only the floating chrome and placement. */
|
|
55
|
+
.wt-scroll-bottom {
|
|
56
|
+
position: fixed;
|
|
57
|
+
right: var(--sp-3);
|
|
58
|
+
bottom: calc(var(--safe-bottom, 0px) + var(--sp-3) + var(--kb-inset, 0px));
|
|
59
|
+
background: var(--surface);
|
|
60
|
+
backdrop-filter: var(--blur);
|
|
61
|
+
border: 1px solid var(--border);
|
|
62
|
+
border-radius: var(--radius);
|
|
63
|
+
color: color-mix(in oklch, var(--text) 70%, transparent);
|
|
64
|
+
z-index: 10;
|
|
65
|
+
display: none;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/* The scroll slot gets .scrolled-up toggled by the feature; show the button. */
|
|
69
|
+
.wt-slot-scroll.scrolled-up .wt-scroll-bottom {
|
|
70
|
+
display: inline-flex;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/* Hover brightens the icon + border; press fills the background (the .wt-btn
|
|
74
|
+
:active fill would otherwise be masked by this button's own surface fill). */
|
|
75
|
+
.wt-scroll-bottom:hover {
|
|
76
|
+
border-color: color-mix(in oklch, var(--text) 35%, transparent);
|
|
77
|
+
color: var(--text);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.wt-scroll-bottom:active {
|
|
81
|
+
background: var(--text-active);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* Mobile + tabbed: lift above the switcher bar and the keyboard (WS3). */
|
|
85
|
+
@media (pointer: coarse) {
|
|
86
|
+
.wt-tabbed .wt-scroll-bottom {
|
|
87
|
+
bottom: calc(
|
|
88
|
+
var(--wt-switcher-h) + var(--safe-bottom, 0px) + var(--kb-inset, 0px) + var(--sp-2)
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/* Desktop + tabbed: the tab strip docks at the bottom, so lift the button above
|
|
94
|
+
it with a clear gap. */
|
|
95
|
+
@media (pointer: fine) {
|
|
96
|
+
.wt-tabbed .wt-scroll-bottom {
|
|
97
|
+
bottom: calc(var(--wt-tabbar-h, 44px) + var(--sp-3));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/* ===== Context menu (contextMenu feature) ===== */
|
|
102
|
+
.wt-ctx-menu {
|
|
103
|
+
position: fixed;
|
|
104
|
+
display: none;
|
|
105
|
+
flex-direction: column;
|
|
106
|
+
background: var(--surface);
|
|
107
|
+
backdrop-filter: var(--blur);
|
|
108
|
+
border: 1px solid var(--border);
|
|
109
|
+
border-radius: var(--radius);
|
|
110
|
+
padding: var(--sp-1);
|
|
111
|
+
z-index: 30;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.wt-ctx-menu.visible {
|
|
115
|
+
display: flex;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.wt-ctx-menu button {
|
|
119
|
+
background: transparent;
|
|
120
|
+
color: var(--text);
|
|
121
|
+
border: none;
|
|
122
|
+
border-radius: var(--radius-inner);
|
|
123
|
+
font-family: var(--font-ui);
|
|
124
|
+
font-size: 0.8125rem;
|
|
125
|
+
font-weight: 500;
|
|
126
|
+
padding: var(--sp-1) var(--sp-3);
|
|
127
|
+
cursor: pointer;
|
|
128
|
+
text-align: left;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.wt-ctx-menu button:active {
|
|
132
|
+
background: var(--text-active);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
@media (pointer: coarse) {
|
|
136
|
+
.wt-ctx-menu button {
|
|
137
|
+
min-height: var(--touch-target);
|
|
138
|
+
padding-block: var(--sp-2);
|
|
139
|
+
touch-action: manipulation;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/* ===== Catching-up cue (tabs feature, sections 12/13) =====
|
|
144
|
+
A switched-into tab's cached screen is stale until its resume delta lands, so
|
|
145
|
+
it must not read as live. This unobtrusive cue shows only if the delta has not
|
|
146
|
+
arrived shortly after the switch, and clears on the first screen frame. */
|
|
147
|
+
.wt-catchup {
|
|
148
|
+
position: fixed;
|
|
149
|
+
top: calc(var(--safe-top, 0px) + var(--vv-top, 0px) + 44px);
|
|
150
|
+
left: 50%;
|
|
151
|
+
transform: translateX(-50%);
|
|
152
|
+
padding: 2px var(--sp-2);
|
|
153
|
+
background: var(--surface);
|
|
154
|
+
backdrop-filter: var(--blur);
|
|
155
|
+
border: 1px solid var(--border);
|
|
156
|
+
border-radius: var(--radius);
|
|
157
|
+
color: color-mix(in oklch, var(--text) 75%, transparent);
|
|
158
|
+
font-family: var(--font-ui);
|
|
159
|
+
font-size: 0.75rem;
|
|
160
|
+
z-index: 24;
|
|
161
|
+
display: none;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.wt-catchup.visible {
|
|
165
|
+
display: block;
|
|
166
|
+
}
|