@cplieger/web-terminal-ui 3.4.5 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +53 -23
  2. package/css/00-tokens.css +8 -2
  3. package/css/01-scope.css +56 -0
  4. package/css/{02-app.css → 02-terminal.css} +64 -239
  5. package/css/{03-primitives.css → 10-primitives.css} +21 -19
  6. package/css/20-connection-banner.css +42 -0
  7. package/css/21-scroll-to-bottom.css +65 -0
  8. package/css/22-context-menu.css +45 -0
  9. package/css/23-toolbar.css +181 -0
  10. package/css/{05-tabs.css → 30-tabs.css} +96 -57
  11. package/css/{06-mobile.css → 31-switcher.css} +66 -81
  12. package/css/{07-animations.css → 40-animations.css} +15 -15
  13. package/css/MANIFEST +19 -11
  14. package/css/MANIFEST.single +12 -0
  15. package/css/MANIFEST.tabbed +16 -0
  16. package/css/MANIFEST.touch +12 -0
  17. package/css/{01-reset.css → page.css} +18 -19
  18. package/package.json +21 -5
  19. package/scaffold/index.html +5 -0
  20. package/src/features/connection-banner.ts +3 -0
  21. package/src/features/context-menu.ts +6 -33
  22. package/src/features/menu-position.ts +50 -0
  23. package/src/features/{tabs.ts → tabs/index.ts} +207 -636
  24. package/src/features/tabs/model.ts +334 -0
  25. package/src/features/tabs/strip.ts +101 -0
  26. package/src/features/tabs/switcher.ts +129 -0
  27. package/src/index.ts +1 -0
  28. package/src/kernel/a11y.ts +30 -5
  29. package/src/kernel/conn-state.ts +20 -3
  30. package/src/kernel/kernel.ts +162 -59
  31. package/src/kernel/types.ts +65 -15
  32. package/src/presets/agent-tabbed.ts +15 -0
  33. package/src/presets/single.ts +27 -0
  34. package/src/presets/tabbed.ts +42 -0
  35. package/src/presets/touch.ts +13 -0
  36. package/src/presets.ts +12 -76
  37. package/src/viewport.ts +28 -15
  38. package/css/04-feedback.css +0 -180
@@ -1,26 +1,27 @@
1
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.
2
+ The kernel builds named layout regions (wt-region-*) as siblings of the
3
+ terminal surface (.term, styled in 02-terminal.css); features mount chrome
4
+ into region slots. The regions are transparent groupings (display: contents)
5
+ so each feature's element floats over the root-filling terminal, and an
6
+ absent feature contributes nothing.
7
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.
8
+ The stylesheet is layered by concern and concatenated in manifest order:
9
+ tokens (00) -> scope + layout modes (01) -> terminal core (02) -> these
10
+ shared primitives (10) -> per-feature chrome (20-23, 30-31, one file per
11
+ feature so a per-preset manifest ships only what its composition mounts) ->
12
+ animations (40, where every .wt-animate-gated transition/keyframe lives, so
13
+ motion is opt-in from one place). Cascade order is the manifest file order.
13
14
 
14
15
  Primitives are the building blocks every feature reuses: the transparent
15
16
  layout regions, the 44px touch button, and the toast surface. */
16
17
 
17
- .wt-region,
18
- .wt-slot {
18
+ :where(.wt-root) .wt-region,
19
+ :where(.wt-root) .wt-slot {
19
20
  display: contents;
20
21
  }
21
22
 
22
23
  /* ===== Shared button primitive (44px touch target) ===== */
23
- .wt-btn {
24
+ :where(.wt-root) .wt-btn {
24
25
  display: inline-flex;
25
26
  align-items: center;
26
27
  justify-content: center;
@@ -36,11 +37,11 @@
36
37
  user-select: none;
37
38
  }
38
39
 
39
- .wt-btn:active {
40
+ :where(.wt-root) .wt-btn:active {
40
41
  background: var(--text-active);
41
42
  }
42
43
 
43
- .wt-btn svg {
44
+ :where(.wt-root) .wt-btn svg {
44
45
  width: var(--icon);
45
46
  height: var(--icon);
46
47
  stroke: currentcolor;
@@ -54,9 +55,10 @@
54
55
  Top-centre on every form factor (both desktop and the tabbed mobile UI dock a
55
56
  bar at the bottom, so a bottom toast would collide with it; top-centre is
56
57
  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;
58
+ notch and follows iOS's visual-viewport shift. Absolute against .wt-root,
59
+ like all chrome (identical geometry in wt-viewport mode; see 01-scope.css). */
60
+ :where(.wt-root) .wt-toast {
61
+ position: absolute;
60
62
  top: calc(var(--safe-top, 0px) + var(--vv-top, 0px) + var(--sp-3));
61
63
  left: 50%;
62
64
  transform: translateX(-50%);
@@ -77,7 +79,7 @@
77
79
  opacity: 0;
78
80
  }
79
81
 
80
- .wt-toast.visible {
82
+ :where(.wt-root) .wt-toast.visible {
81
83
  display: block;
82
84
  opacity: 1;
83
85
  }
@@ -0,0 +1,42 @@
1
+ /* ===== Connection banner (connectionBanner feature) =====
2
+ Top-centre on every form factor (same rationale as the toast: a bottom bar is
3
+ always docked now). Absolute against .wt-root, like all chrome (identical
4
+ geometry in wt-viewport mode; see 01-scope.css). */
5
+ :where(.wt-root) .wt-conn-banner {
6
+ position: absolute;
7
+ top: calc(var(--safe-top, 0px) + var(--vv-top, 0px) + var(--sp-3));
8
+ left: 50%;
9
+ transform: translateX(-50%);
10
+ max-width: calc(100% - 2 * var(--sp-3));
11
+ padding: var(--sp-1) var(--sp-3);
12
+ background: var(--surface);
13
+ backdrop-filter: var(--blur);
14
+ border: 1px solid var(--border);
15
+ border-radius: var(--radius);
16
+ color: var(--text);
17
+ font-family: var(--font-ui);
18
+ font-size: 0.8125rem;
19
+ font-weight: 500;
20
+ text-align: center;
21
+ z-index: 20;
22
+ pointer-events: none; /* passive status surface: never steal taps from the terminal beneath */
23
+ display: none;
24
+ }
25
+
26
+ :where(.wt-root) .wt-conn-banner.visible {
27
+ display: block;
28
+ }
29
+
30
+ :where(.wt-root) .wt-conn-banner[data-state="reconnecting"] {
31
+ border-color: oklch(70% 0.15 60deg / 40%);
32
+ }
33
+
34
+ :where(.wt-root) .wt-conn-banner[data-state="offline"] {
35
+ color: #f55;
36
+ border-color: oklch(60% 0.2 25deg / 50%);
37
+ }
38
+
39
+ :where(.wt-root) .wt-conn-banner[data-state="restarted"] {
40
+ color: #fa5;
41
+ border-color: oklch(70% 0.15 60deg / 40%);
42
+ }
@@ -0,0 +1,65 @@
1
+ /* ===== Scroll-to-bottom (scrollToBottom feature) =====
2
+ A compact floating icon button docked in the bottom-right corner, shown only
3
+ while the user has scrolled up. Same button on both form factors (the old
4
+ desktop bottom-centre text pill was dropped in the design-system switch);
5
+ each form factor just lifts it above its own bottom bar. Size + icon
6
+ centering come from the .wt-btn primitive (a 44px icon button); this rule
7
+ adds only the floating chrome and placement. Absolute against .wt-root, like
8
+ all chrome (identical geometry in wt-viewport mode; see 01-scope.css). */
9
+ :where(.wt-root) .wt-scroll-bottom {
10
+ position: absolute;
11
+ right: var(--sp-3);
12
+ bottom: calc(var(--safe-bottom, 0px) + var(--sp-3) + var(--kb-inset, 0px));
13
+ background: var(--surface);
14
+ backdrop-filter: var(--blur);
15
+ border: 1px solid var(--border);
16
+ /* Match the tab "+" buttons (and every other .wt-btn), which round to
17
+ --radius-inner. Both are 44px boxes, so matching the corner radius makes
18
+ the FAB read as the same-sized button as the new-tab "+" beside/above it
19
+ rather than a rounder, different-looking control. */
20
+ border-radius: var(--radius-inner);
21
+ /* Full --text icon, matching the switcher's keyboard/switch/"+" icons (which
22
+ use .wt-btn's default color). Was a muted 70% mix that read as grey next to
23
+ those white bar icons. */
24
+ color: var(--text);
25
+ z-index: 10;
26
+ display: none;
27
+ }
28
+
29
+ /* The scroll slot gets .scrolled-up toggled by the feature; show the button. */
30
+ :where(.wt-root) .wt-slot-scroll.scrolled-up .wt-scroll-bottom {
31
+ display: inline-flex;
32
+ }
33
+
34
+ /* Hover brightens the border; press fills the background (the .wt-btn :active
35
+ fill would otherwise be masked by this button's own surface fill). The icon
36
+ is already full --text, so hover no longer needs to change it. */
37
+ :where(.wt-root) .wt-scroll-bottom:hover {
38
+ border-color: color-mix(in oklch, var(--text) 35%, transparent);
39
+ }
40
+
41
+ :where(.wt-root) .wt-scroll-bottom:active {
42
+ background: var(--text-active);
43
+ }
44
+
45
+ /* Mobile + tabbed: lift above the switcher bar and the keyboard (WS3), and pull
46
+ the right edge in to --sp-2 so the button sits directly above the switcher's
47
+ "+" (whose right edge is inset by the bar's --sp-2 padding). Both are 44px
48
+ border-box boxes, so matching the right inset lines them up in one vertical
49
+ column (they were 4px out: the FAB's base --sp-3 vs the "+"'s --sp-2). */
50
+ @media (pointer: coarse) {
51
+ :where(.wt-root.wt-tabbed) .wt-scroll-bottom {
52
+ right: var(--sp-2);
53
+ bottom: calc(
54
+ var(--wt-switcher-h) + var(--safe-bottom, 0px) + var(--kb-inset, 0px) + var(--sp-2)
55
+ );
56
+ }
57
+ }
58
+
59
+ /* Desktop + tabbed: the tab strip docks at the bottom, so lift the button above
60
+ it with a clear gap. */
61
+ @media (pointer: fine) {
62
+ :where(.wt-root.wt-tabbed) .wt-scroll-bottom {
63
+ bottom: calc(var(--wt-tabbar-h, 44px) + var(--sp-3));
64
+ }
65
+ }
@@ -0,0 +1,45 @@
1
+ /* ===== Context menu (contextMenu feature) =====
2
+ Absolute against .wt-root, like all chrome; the shared point-anchored
3
+ positioner (src/features/menu-position.ts) converts its viewport-clamped
4
+ coordinates into root-relative offsets, so placement is identical in both
5
+ layout modes. */
6
+ :where(.wt-root) .wt-ctx-menu {
7
+ position: absolute;
8
+ display: none;
9
+ flex-direction: column;
10
+ background: var(--surface);
11
+ backdrop-filter: var(--blur);
12
+ border: 1px solid var(--border);
13
+ border-radius: var(--radius);
14
+ padding: var(--sp-1);
15
+ z-index: 30;
16
+ }
17
+
18
+ :where(.wt-root) .wt-ctx-menu.visible {
19
+ display: flex;
20
+ }
21
+
22
+ :where(.wt-root) .wt-ctx-menu button {
23
+ background: transparent;
24
+ color: var(--text);
25
+ border: none;
26
+ border-radius: var(--radius-inner);
27
+ font-family: var(--font-ui);
28
+ font-size: 0.8125rem;
29
+ font-weight: 500;
30
+ padding: var(--sp-1) var(--sp-3);
31
+ cursor: pointer;
32
+ text-align: left;
33
+ }
34
+
35
+ :where(.wt-root) .wt-ctx-menu button:active {
36
+ background: var(--text-active);
37
+ }
38
+
39
+ @media (pointer: coarse) {
40
+ :where(.wt-root) .wt-ctx-menu button {
41
+ min-height: var(--touch-target);
42
+ padding-block: var(--sp-2);
43
+ touch-action: manipulation;
44
+ }
45
+ }
@@ -0,0 +1,181 @@
1
+ /* ===== Key toolbar (mobileToolbar feature; touch devices only) =====
2
+ Uses interpolate-size: allow-keywords (on .wt-root) to animate
3
+ width/height between a fixed collapsed size and auto (intrinsic).
4
+ Toggle is first in DOM so it's at top-left of the grid — visible
5
+ when the container clips to 1-cell size. Container is right-anchored
6
+ so it grows leftward on open. */
7
+ :where(.wt-root) .key-toolbar {
8
+ display: contents;
9
+ }
10
+
11
+ /* Touch-only toolbar children: the on-screen Esc/Tab/arrows/hamburger
12
+ are useless on desktop where the user has a real keyboard. Hidden by
13
+ default; the @media (pointer: coarse) block below restores them. */
14
+ :where(.wt-root) .kb-key,
15
+ :where(.wt-root) .kb-toggle {
16
+ display: none;
17
+ }
18
+
19
+ @media (pointer: coarse) {
20
+ :where(.wt-root) .key-toolbar {
21
+ /* Absolute against .wt-root (identical geometry in wt-viewport mode; see
22
+ 01-scope.css). --vv-top keeps it following iOS's visual-viewport shift. */
23
+ position: absolute;
24
+ top: calc(var(--safe-top) + var(--sp-2) + var(--vv-top, 0px));
25
+ right: var(--sp-2);
26
+ z-index: 10;
27
+ display: grid;
28
+ grid-template-columns: repeat(4, var(--touch-target)) var(--touch-target);
29
+ grid-template-rows: var(--touch-target) var(--touch-target);
30
+ place-content: start end;
31
+ gap: var(--sp-1);
32
+ padding: var(--sp-1);
33
+ background: var(--surface);
34
+ backdrop-filter: var(--blur);
35
+ border: 1px solid var(--border);
36
+ border-radius: var(--radius);
37
+ overflow: hidden;
38
+ width: auto;
39
+ height: auto;
40
+ transition:
41
+ width var(--dur-enter) var(--ease-standard),
42
+ height var(--dur-enter) var(--ease-standard);
43
+ }
44
+
45
+ /* Collapsed: fixed size = 1 button + padding + border. Content is
46
+ right-aligned (justify-content:end) so the toggle (col 5) stays at
47
+ the right edge, visible within the clipped area. --pill-outer
48
+ includes the 2px border (box-sizing:border-box); omitting it left
49
+ the content box 2px short, pinning the button top-right with
50
+ unequal bottom/left gaps. */
51
+ :where(.wt-root) .key-toolbar.collapsed {
52
+ width: var(--pill-outer);
53
+ height: var(--pill-outer);
54
+ }
55
+
56
+ /* Grid placement: toggle at col 5 row 1 (top-right). */
57
+ :where(.wt-root) .kb-toggle {
58
+ grid-area: 1 / 5;
59
+ position: relative;
60
+ }
61
+ :where(.wt-root) .kb-r1c1 {
62
+ grid-area: 1 / 1;
63
+ }
64
+ :where(.wt-root) .kb-r1c2 {
65
+ grid-area: 1 / 2;
66
+ }
67
+ :where(.wt-root) .kb-r1c3 {
68
+ grid-area: 1 / 3;
69
+ }
70
+ :where(.wt-root) .kb-r1c4 {
71
+ grid-area: 1 / 4;
72
+ }
73
+ :where(.wt-root) .kb-r2c1 {
74
+ grid-area: 2 / 1;
75
+ }
76
+ :where(.wt-root) .kb-r2c2 {
77
+ grid-area: 2 / 2;
78
+ }
79
+ :where(.wt-root) .kb-r2c3 {
80
+ grid-area: 2 / 3;
81
+ }
82
+ :where(.wt-root) .kb-r2c4 {
83
+ grid-area: 2 / 4;
84
+ }
85
+
86
+ /* Keys fade in/out during open/close. The container's width/height
87
+ transition handles the background slide; opacity handles the
88
+ individual button appearance. Stagger the fade slightly after
89
+ the container starts growing (delay on open) and before it
90
+ finishes shrinking (no delay on close). */
91
+ :where(.wt-root) .kb-key {
92
+ opacity: 1;
93
+ transition: opacity var(--dur-standard) var(--ease-enter) 0.05s;
94
+ }
95
+
96
+ :where(.wt-root) .key-toolbar.collapsed .kb-key {
97
+ opacity: 0;
98
+ transition: opacity var(--dur-exit) var(--ease-exit);
99
+ }
100
+
101
+ /* Shared button */
102
+ :where(.wt-root) .key-toolbar button {
103
+ width: var(--touch-target);
104
+ height: var(--touch-target);
105
+ background: transparent;
106
+ color: var(--text);
107
+ border: none;
108
+ border-radius: var(--radius-inner);
109
+ font-size: 0.9375rem;
110
+ font-family: var(--font-ui);
111
+ font-weight: 500;
112
+ cursor: pointer;
113
+ touch-action: manipulation;
114
+ user-select: none;
115
+ display: flex;
116
+ align-items: center;
117
+ justify-content: center;
118
+ }
119
+
120
+ :where(.wt-root) .key-toolbar button:active {
121
+ background: var(--text-active);
122
+ }
123
+
124
+ /* Sticky Ctrl, armed: inverted fill so the toggle state reads clearly
125
+ in the monochrome palette (stays filled while pressed). */
126
+ :where(.wt-root) .kb-key.armed,
127
+ :where(.wt-root) .kb-key.armed:active {
128
+ background: var(--text);
129
+ color: var(--bg);
130
+ }
131
+
132
+ :where(.wt-root) .key-toolbar button svg {
133
+ width: var(--icon);
134
+ height: var(--icon);
135
+ stroke: currentcolor;
136
+ fill: none;
137
+ stroke-width: 2;
138
+ stroke-linecap: round;
139
+ stroke-linejoin: round;
140
+ }
141
+
142
+ /* Toggle icon crossfade. Both icons are stacked absolute so they
143
+ can independently fade + rotate during the open<->close
144
+ transition. inset:0 + margin:auto centers them within the
145
+ button (works because the SVGs carry explicit width/height); a
146
+ bare `position: absolute` would pin them at the top-left, giving
147
+ the toggle visibly unequal left/right padding when collapsed. */
148
+ :where(.wt-root) .kb-toggle .icon-hamburger,
149
+ :where(.wt-root) .kb-toggle .icon-close {
150
+ position: absolute;
151
+ inset: 0;
152
+ margin: auto;
153
+ transition:
154
+ opacity var(--dur-standard) linear,
155
+ transform var(--dur-standard) linear;
156
+ }
157
+
158
+ :where(.wt-root) .kb-toggle .icon-close {
159
+ opacity: 1;
160
+ transform: rotate(0deg);
161
+ }
162
+ :where(.wt-root) .kb-toggle .icon-hamburger {
163
+ opacity: 0;
164
+ transform: rotate(-90deg);
165
+ }
166
+ :where(.wt-root) .key-toolbar.collapsed .kb-toggle .icon-close {
167
+ opacity: 0;
168
+ transform: rotate(90deg);
169
+ }
170
+ :where(.wt-root) .key-toolbar.collapsed .kb-toggle .icon-hamburger {
171
+ opacity: 1;
172
+ transform: rotate(0deg);
173
+ }
174
+
175
+ /* No-transition on first render */
176
+ :where(.wt-root) .key-toolbar.no-transition,
177
+ :where(.wt-root) .key-toolbar.no-transition .kb-toggle .icon-hamburger,
178
+ :where(.wt-root) .key-toolbar.no-transition .kb-toggle .icon-close {
179
+ transition: none;
180
+ }
181
+ }