@cplieger/web-terminal-ui 4.5.1 → 4.6.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 CHANGED
@@ -8,15 +8,14 @@
8
8
  [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/cplieger/web-terminal-ui/badge)](https://scorecard.dev/viewer/?uri=github.com/cplieger/web-terminal-ui)
9
9
 
10
10
  The reference touch-first browser UI for
11
- [`@cplieger/web-terminal-engine`](https://github.com/cplieger/web-terminal-engine) — the part
12
- that turns the engine's render/scroll/connection/keyboard modules into a
13
- usable terminal on a phone as well as a desktop.
11
+ [`@cplieger/web-terminal-engine`](https://github.com/cplieger/web-terminal-engine).
12
+ It turns the engine's render/scroll/connection/keyboard modules into a usable
13
+ terminal on a phone as well as a desktop.
14
14
 
15
15
  One `createTerminal(root, { features })` call builds the entire terminal UI
16
16
  inside a single container element you provide. A small always-present kernel
17
- (the display output, the hidden keyboard textarea, IME, engine wiring, the
18
- sanitizing input funnel, connection-state, and named layout regions) composes
19
- with opt-in feature modules that own everything above the raw terminal:
17
+ composes with opt-in feature modules that own everything above the raw
18
+ terminal:
20
19
 
21
20
  - a **display-only** terminal output (native text selection survives redraws)
22
21
  and a hidden `<textarea>` that owns the keyboard and IME (the kernel)
@@ -44,7 +43,7 @@ depend on the engine directly and skip this package.
44
43
  npm install @cplieger/web-terminal-ui @cplieger/web-terminal-engine
45
44
  ```
46
45
 
47
- `@cplieger/web-terminal-engine` is a peer dependency — the UI is built on the
46
+ `@cplieger/web-terminal-engine` is a peer dependency: the UI is built on the
48
47
  engine, so the consumer pins the engine version explicitly. Pairing compatibility
49
48
  is governed by the engine's
50
49
  [directional wire contract](https://github.com/cplieger/web-terminal-engine#wire-protocol),
@@ -57,14 +56,14 @@ Serve a CSS bundle matching how you embed the terminal, plus a minimal HTML
57
56
  page that has one empty container element, then call
58
57
  `createTerminal(root, { features })` from your entry module.
59
58
 
60
- **Full-page host** (the terminal IS the page — `web-terminal-server`,
59
+ **Full-page host** (the terminal IS the page, as in `web-terminal-server` and
61
60
  `web-terminal-kiro`): concatenate `css/MANIFEST` into the `style.css` your page
62
61
  links. That reference bundle is `css/page.css` (the page kit: `html/body`
63
62
  reset + the terminal web font's `@font-face`, expecting the font files at
64
63
  `/vendor/fonts/`) plus the complete component set.
65
64
 
66
- **Embedder** (the terminal lives inside your app's layout — a panel, a pane):
67
- concatenate the per-preset manifest matching your composition instead —
65
+ **Embedder** (the terminal lives inside your app's layout, as a panel or pane):
66
+ concatenate the per-preset manifest matching your composition instead:
68
67
  `css/MANIFEST.single`, `css/MANIFEST.touch`, or `css/MANIFEST.tabbed`. These
69
68
  contain ONLY root-scoped component styles: no page reset, no fonts, no
70
69
  document-level rules, nothing to quarantine. Pass `layout: "container"` so the
@@ -99,7 +98,7 @@ entry modules beside it:
99
98
  ```
100
99
 
101
100
  `createTerminal(root, opts?)` builds the entire terminal subtree (the kernel
102
- plus every feature's chrome) inside `root` itself — there is no element-id
101
+ plus every feature's chrome) inside `root` itself. There is no element-id
103
102
  contract for the host page to reproduce, and every style and CSS custom
104
103
  property is scoped to the `wt-root` class it stamps on your element (removed
105
104
  again by `destroy()`). Call it exactly once; the engine's
@@ -109,46 +108,43 @@ to copy and adapt.
109
108
 
110
109
  Four presets are provided; each is a plain feature-array factory, so you can
111
110
  spread and edit it. Import the barrel (`@cplieger/web-terminal-ui/presets`) for
112
- convenience, or a per-preset entry module for the minimal delivered import
113
- graph — `…/presets/single`, `…/presets/touch`, `…/presets/tabbed`,
114
- `…/presets/agent-tabbed` (the barrel statically reaches every feature; the
115
- touch entry, for example, never imports the tabs module). Individual features
111
+ convenience, or a per-preset entry module (`…/presets/single`, `…/presets/touch`,
112
+ `…/presets/tabbed`, `…/presets/agent-tabbed`) for the minimal delivered import
113
+ graph; the barrel statically reaches every feature, while the touch entry, for
114
+ example, never imports the tabs module. Individual features
116
115
  are importable from `…/features/<name>` (`clipboard`, `context-menu`,
117
116
  `scroll-to-bottom`, `predictive-echo`, `connection-banner`, `mobile-toolbar`,
118
117
  `tabs`, `activity-monitor`, `animations`) for hand-picked compositions:
119
118
 
120
- - `presetSingle()` — single-pane desktop UI (context menu, clipboard,
119
+ - `presetSingle()`: single-pane desktop UI (context menu, clipboard,
121
120
  scroll-to-bottom, predictive echo, connection banner).
122
- - `presetTouch()` — `presetSingle()` plus the mobile key toolbar.
123
- - `presetTabbed()` — the generic tabbed UI: `presetTouch()` plus tabs, the
124
- activity monitor, and animations. Each tab's title is OSC-first — it follows
121
+ - `presetTouch()`: `presetSingle()` plus the mobile key toolbar.
122
+ - `presetTabbed()`: the generic tabbed UI, `presetTouch()` plus tabs, the
123
+ activity monitor, and animations. Each tab's title is OSC-first: it follows
125
124
  the process window title (OSC 0/2) when the program sets one and keeps it
126
125
  updated, otherwise the last command submitted. The per-tab activity dot
127
126
  reveals itself only when a session reports OSC 9;4 progress, so a plain shell
128
127
  keeps clean, label-only tabs. Requires a server that speaks the session API
129
128
  (`/api/sessions` and `/ws?session=`), such as `web-terminal-server`.
130
- - `presetAgentTabbed()` — the same feature set as `presetTabbed()` (activity
131
- monitor included), tuned for an agent shell such as `web-terminal-kiro`: with
132
- `preferInputTitle`, each tab's label follows the latest submitted line
133
- (persisted server-side and recovered on reload) and the program's non-empty
134
- but useless OSC 0/2 title is ignored; and with `presumeReports`, the idle
135
- activity dot shows from tab creation instead of waiting for the session's
136
- first OSC 9;4 signal — every session is an agent, so there is nothing to
137
- prove. Its status dots (idle / working / done / needs-input) come from the
138
- same activity monitor, driven by the server's status SSE and its OSC-9
139
- classifier.
129
+ - `presetAgentTabbed()`: the same feature set as `presetTabbed()`, tuned for an
130
+ agent shell such as `web-terminal-kiro`. With `preferInputTitle`, each tab's
131
+ label follows the latest submitted line (persisted server-side and recovered
132
+ on reload) and the program's OSC 0/2 title is ignored. With `presumeReports`,
133
+ the idle activity dot shows from tab creation instead of waiting for the
134
+ session's first OSC 9;4 signal. Its status dots (idle / working / done /
135
+ needs-input) come from the same activity monitor.
140
136
 
141
137
  ### Options
142
138
 
143
- | Option | Default | Purpose |
144
- | -------------- | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
145
- | `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. |
146
- | `layout` | `"viewport"` | How the terminal claims space. `"viewport"`: the root becomes a fixed full-viewport box (the full-page product). `"container"`: the root fills your container element, which becomes the styling and positioning boundary (the embedded case). |
147
- | `wsPath` | `"/ws"` | WebSocket endpoint path the engine connects to. |
148
- | `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. |
149
- | `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. |
150
- | `onFatalError` | _(built-in recovery)_ | Called after a fatal feature setup failure has stopped the connection, released the terminal runtime, and cleared the root. It receives `{ phase, feature, cause }`. Return `true` only after rendering replacement recovery UI into the root; otherwise the kernel shows its Reload page surface. |
151
- | `theme` | _(none)_ | Theme overrides (CSS custom properties on the terminal root): `--accent`, `--tab-bg`, `--tab-hover-bg`, `--tab-active-bg`, `--tab-active-fg`, `--tab-active-border`, plus the activity-dot palette `--status-working`, `--status-done`, `--status-input` (the working and input rings derive from their own tokens). The library ships neutral defaults. |
139
+ | Option | Default | Purpose |
140
+ | -------------- | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
141
+ | `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. |
142
+ | `layout` | `"viewport"` | How the terminal claims space. `"viewport"`: the root becomes a fixed full-viewport box (the full-page product). `"container"`: the root fills your container element, which becomes the styling and positioning boundary (the embedded case). |
143
+ | `wsPath` | `"/ws"` | WebSocket endpoint path the engine connects to. |
144
+ | `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. |
145
+ | `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. |
146
+ | `onFatalError` | _(built-in recovery)_ | Called with `{ phase, feature, cause }` after a fatal feature setup failure; behavior below. |
147
+ | `theme` | _(none)_ | Theme overrides (CSS custom properties on the terminal root): `--accent`, `--tab-bg`, `--tab-hover-bg`, `--tab-active-bg`, `--tab-active-fg`, `--tab-active-border`, plus the activity-dot palette `--status-working`, `--status-done`, `--status-input`. The library ships neutral defaults. |
152
148
 
153
149
  `createTerminal()` returns a handle: `focus()` re-focuses the terminal input
154
150
  (and opens the soft keyboard on touch); `send(bytes)` sends bytes to the active
@@ -178,10 +174,10 @@ replacement recovery UI into the terminal root.
178
174
 
179
175
  The web-terminal family:
180
176
 
181
- - [`web-terminal-engine`](https://github.com/cplieger/web-terminal-engine) — the
177
+ - [`web-terminal-engine`](https://github.com/cplieger/web-terminal-engine): the
182
178
  Go session engine + TypeScript browser renderer this UI is built on (peer
183
179
  dependency).
184
- - [`web-terminal-server`](https://github.com/cplieger/web-terminal-server) — a
180
+ - [`web-terminal-server`](https://github.com/cplieger/web-terminal-server): a
185
181
  ready-to-run container that serves this UI over HTTP + WebSocket for any
186
182
  command.
187
183
 
@@ -190,6 +186,11 @@ Consumers that ship this UI:
190
186
  - [`vibekit`](https://github.com/cplieger/vibekit)
191
187
  - [`web-terminal-kiro`](https://github.com/cplieger/web-terminal-kiro)
192
188
 
189
+ ## Contributing
190
+
191
+ Issues and PRs are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for the
192
+ conventions and how to run the checks locally.
193
+
193
194
  ## Disclaimer
194
195
 
195
196
  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.
@@ -198,4 +199,4 @@ This project was built with AI-assisted tooling using [Claude](https://claude.co
198
199
 
199
200
  ## License
200
201
 
201
- GPL-3.0-or-later. See `LICENSE`.
202
+ GPL-3.0-or-later. See [LICENSE](LICENSE).
package/css/30-tabs.css CHANGED
@@ -104,6 +104,16 @@
104
104
  color: var(--tab-active-fg);
105
105
  }
106
106
 
107
+ /* Press (mousedown / touch-hold): one rung past the hover lift — same recipe,
108
+ deeper mix — so hover -> press reads as a single dimension. After the hover
109
+ rule so it wins while both apply. Scoped like hover: clicking the active tab
110
+ selects nothing (it is already active), so it presses like the static surface
111
+ it is. */
112
+ :where(.wt-root) .wt-tab:not(.wt-tab-active):active {
113
+ background: color-mix(in oklch, var(--tab-bg), var(--text) 25%);
114
+ color: var(--tab-active-fg);
115
+ }
116
+
107
117
  /* The active tab reads clearly above the inactive chips: a stronger fill plus an
108
118
  accent-coloured edge (inactive chips carry a faint white --border; the active
109
119
  one gets the accent equivalent). Themeable via --accent (web-terminal-kiro -> purple). */
@@ -121,6 +131,17 @@
121
131
  overflow: hidden;
122
132
  white-space: nowrap;
123
133
  text-overflow: ellipsis;
134
+ /* Center the INK, not the line box: UI fonts carry more ascent than descent
135
+ (Noto Sans at 13px: 14px over / 4px under the baseline), so the flex-
136
+ centered line box leaves the visible text ~1.5px low — reading as
137
+ baseline-aligned with the activity dot's bottom instead of its center.
138
+ Trimming the box to the cap-height/alphabetic band makes flexbox center
139
+ the glyphs themselves, on every platform font. Descender ink still paints
140
+ (verified: ink overflow survives the overflow: hidden ellipsis clip).
141
+ Non-supporting browsers simply keep the untrimmed box. Shared by all
142
+ three chip labels (this one + the two switcher labels in
143
+ 31-switcher.css). */
144
+ text-box: trim-both cap alphabetic;
124
145
  }
125
146
 
126
147
  /* A little breathing room after the activity dot, before the label. */
@@ -155,6 +176,13 @@
155
176
  color: var(--text);
156
177
  }
157
178
 
179
+ /* Press: deepen the hover box (--text-active is a 15% --text wash; press doubles
180
+ it to 30%) so mousedown visibly registers before the tab disappears. */
181
+ :where(.wt-root) .wt-tab-close:active {
182
+ background: color-mix(in oklch, var(--text) 30%, transparent);
183
+ color: var(--text);
184
+ }
185
+
158
186
  /* On an inactive tab the close appears only on hover, kept in the layout via
159
187
  visibility so the tab width never jumps; on the active tab it is always shown. */
160
188
  :where(.wt-root) .wt-tab:not(.wt-tab-active) .wt-tab-close {
@@ -210,6 +238,13 @@
210
238
  background: var(--tab-active-bg);
211
239
  }
212
240
 
241
+ /* Press: brighten the hover fill with the active-tab fg (the
242
+ .wt-switcher-current press recipe — a lighter press over an already-filled
243
+ control), keeping the accent glyph. */
244
+ :where(.wt-root) .wt-tab-new:active {
245
+ background: color-mix(in oklch, var(--tab-active-bg), var(--tab-active-fg) 12%);
246
+ }
247
+
213
248
  /* The +/x icons are inline SVG so they center exactly in their flex-centered
214
249
  button and stay symmetric regardless of the UI font (fixes the off-center
215
250
  glyph and the lopsided "+"). The + is a touch larger and bolder than the x. */
@@ -329,6 +364,12 @@
329
364
  background: var(--text-active);
330
365
  }
331
366
 
367
+ /* Press: deepen the hover wash to 30% --text (the .wt-tab-close press rung) so
368
+ the click registers before the menu closes. */
369
+ :where(.wt-root) .wt-tab-menu button:enabled:active {
370
+ background: color-mix(in oklch, var(--text) 30%, transparent);
371
+ }
372
+
332
373
  :where(.wt-root) .wt-tab-menu button:disabled {
333
374
  color: color-mix(in oklch, var(--text) 35%, transparent);
334
375
  cursor: default;
@@ -401,15 +442,14 @@
401
442
  border: 1.5px solid color-mix(in oklch, var(--text) 45%, transparent);
402
443
  }
403
444
 
404
- /* Agent working: two INDEPENDENTLY animated layers on one element make the
405
- "alive" read (the status-badge "processing" idiom — Ant Design's processing
406
- badge, the classic CSS live-indicator ripple): the solid disc keeps full
407
- opacity and gently GLOWS (its colors brighten mid-beat), while a ::after
408
- ring — born exactly on the static 2px ring — RIPPLES outward and dissolves,
409
- re-emitted each cycle. The static ring itself never moves, so working keeps
410
- input's exact rest silhouette (a live turn ripples; a turn blocked on the
411
- user is that same silhouette frozen — see input below). Motion separates
412
- working from input; the ring separates both from done's bare disc. The old
445
+ /* Agent working: a solid disc with two animated cues (the status-badge
446
+ "processing" idiom — Ant Design's processing badge, the classic CSS
447
+ live-indicator ripple): the disc keeps full opacity and gently GLOWS (its
448
+ colors brighten mid-beat), while a ::after ring RIPPLES outward off the
449
+ disc's edge and dissolves, re-emitted each cycle. No static ring at rest —
450
+ the disc + live ripple ARE the working read; input freezes that exact ping
451
+ at its birth frame (see input below), so motion separates working from
452
+ input, and the ring separates both from done's bare disc. The old
413
453
  single-layer pulse faded disc + ring together (opacity on the element),
414
454
  which read as a blink and coupled the layers — an element's opacity always
415
455
  multiplies onto its pseudos, so true independence requires animating disc
@@ -420,16 +460,15 @@
420
460
  position: relative;
421
461
  background: var(--status-working);
422
462
  border-color: var(--status-working);
423
- box-shadow: 0 0 0 2px color-mix(in srgb, var(--status-working) 40%, transparent);
424
463
  animation: wt-working-glow 1.2s ease-in-out infinite;
425
464
  }
426
465
 
427
- /* The ripple layer: at scale(1) its 2px border overlays the static ring's
428
- 9->13px band exactly (inset -2px), then the wave detaches, expanding and
429
- fading. transform + opacity only, so it composites without repaints. It
430
- exists only in the working state — no stale layer to pin when a dot changes
431
- status. (The switcher notification dot never shows working — input/done
432
- only — so its position: absolute placement never meets this rule's
466
+ /* The ripple layer: at scale(1) its 2px border hugs the disc's edge (inset
467
+ -2px, the 9->13px band), then the wave detaches, expanding and fading.
468
+ transform + opacity only, so it composites without repaints. It exists only
469
+ in the working state — no stale layer to pin when a dot changes status.
470
+ (The switcher notification dot never shows working — input/done only — so
471
+ its position: absolute placement never meets this rule's
433
472
  position: relative.) */
434
473
  :where(.wt-root) .wt-status-dot[data-status="working"]::after {
435
474
  content: "";
@@ -449,18 +488,19 @@
449
488
  border-color: var(--status-done);
450
489
  }
451
490
 
452
- /* User action required (permission / approval): working's exact silhouette —
453
- solid dot + 2px ring — frozen and recolored (--status-input; the default
454
- orange is shifted away from the old amber so it separates from both
455
- default-lime working and green done). The stillness is the message: the
456
- agent's turn is mid-flight but blocked on you. The ring keeps it distinct
457
- from done's bare disc without leaning on hue; it derives from the same token
458
- at 40% alpha, so a theme override recolors dot + ring together. It persists
459
- until you act on or switch into the tab. */
491
+ /* User action required (permission / approval): the working ping frozen at
492
+ its birth frame — solid dot + the same 2px ring at the ripple's 30% birth
493
+ opacity, motionless (--status-input; the default orange is shifted away
494
+ from the old amber so it separates from both default-lime working and green
495
+ done). The stillness is the message: the agent's turn is mid-flight but
496
+ blocked on you. The ring keeps it distinct from done's bare disc without
497
+ leaning on hue; it derives from the same token, so a theme override
498
+ recolors dot + ring together. It persists until you act on or switch into
499
+ the tab. */
460
500
  :where(.wt-root) .wt-status-dot[data-status="input"] {
461
501
  background: var(--status-input);
462
502
  border-color: var(--status-input);
463
- box-shadow: 0 0 0 2px color-mix(in srgb, var(--status-input) 40%, transparent);
503
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--status-input) 30%, transparent);
464
504
  }
465
505
 
466
506
  /* Exited: a dim hollow dot, static; distinct from idle by its lower opacity. */
@@ -490,13 +530,14 @@
490
530
  }
491
531
  }
492
532
 
493
- /* The ripple wave: one emission per cycle, launching off the static ring and
494
- dissolving by ~2.1x. Opacity starts at the static ring's own 40% weight so
495
- the wave reads as the ring letting go of a copy, not a new brighter object. */
533
+ /* The ripple wave: one emission per cycle, launching off the disc's edge and
534
+ dissolving by ~2.1x. The 0.3 birth opacity matches the input ring's frozen
535
+ weight, so a live ping and a stuck one read as the same object — subtle
536
+ against the bar now that no static ring doubles it at birth. */
496
537
  @keyframes wt-working-ripple {
497
538
  0% {
498
539
  transform: scale(1);
499
- opacity: 0.4;
540
+ opacity: 0.3;
500
541
  }
501
542
 
502
543
  100% {
@@ -509,13 +550,14 @@
509
550
  @media (prefers-reduced-motion: reduce) {
510
551
  :where(.wt-root) .wt-status-dot[data-status="working"] {
511
552
  animation: none;
512
- /* Motion is what separates working from input's frozen silhouette; with
513
- it removed, punch an inner hole (dot -> donut) so working differs from
514
- input (solid ringed disc) and done (bare disc) by shape, not hue alone
515
- (WCAG 1.4.1). The outer ring stays. */
553
+ /* Motion is what separates working from input's frozen ping; with it
554
+ removed, punch an inner hole (dot -> donut) and add a static ring so
555
+ working differs from input (solid ringed disc) and done (bare disc) by
556
+ shape, not hue alone (WCAG 1.4.1). The ring carries the same 30%
557
+ weight as input's. */
516
558
  box-shadow:
517
559
  inset 0 0 0 2px var(--bg),
518
- 0 0 0 2px color-mix(in srgb, var(--status-working) 40%, transparent);
560
+ 0 0 0 2px color-mix(in srgb, var(--status-working) 30%, transparent);
519
561
  }
520
562
 
521
563
  /* The ripple layer disappears entirely (not just paused mid-wave). */
@@ -178,6 +178,8 @@
178
178
  text-overflow: ellipsis;
179
179
  white-space: nowrap;
180
180
  text-align: left;
181
+ /* Ink-centering trim, same as .wt-tab-label (see 30-tabs.css). */
182
+ text-box: trim-both cap alphabetic;
181
183
  }
182
184
 
183
185
  /* Keyboard button: hidden until a keyboardToggle is wired; filled while the key
@@ -418,6 +420,8 @@
418
420
  overflow: hidden;
419
421
  text-overflow: ellipsis;
420
422
  white-space: nowrap;
423
+ /* Ink-centering trim, same as .wt-tab-label (see 30-tabs.css). */
424
+ text-box: trim-both cap alphabetic;
421
425
  }
422
426
 
423
427
  /* The close sits flush at the row's right edge (no gap), vertically centered. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/web-terminal-ui",
3
- "version": "4.5.1",
3
+ "version": "4.6.0",
4
4
  "type": "module",
5
5
  "description": "Reference touch-first browser UI for @cplieger/web-terminal-engine: a composable kernel + feature modules (tabs, activity monitor, mobile toolbar, context menu, IME, predictive echo, viewport handling) and CSS. One createTerminal() call.",
6
6
  "license": "GPL-3.0-or-later",
@@ -53,7 +53,7 @@
53
53
  "happy-dom": "20.11.0",
54
54
  "html-validate": "11.5.6",
55
55
  "knip": "6.27.0",
56
- "prettier": "3.9.5",
56
+ "prettier": "3.9.6",
57
57
  "stylelint": "17.14.1",
58
58
  "stylelint-config-standard": "40.0.0",
59
59
  "typescript": "npm:@typescript/typescript6@6.0.2",