codebay 0.10.0 → 0.11.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 (101) hide show
  1. package/.mochi/manifest.json +193 -167
  2. package/.mochi/svelte-client/HydratableIsland-dammyce5.js +1 -0
  3. package/.mochi/svelte-client/_hydrate-AppShell_2rtnxn6pgp2rh-v65ctd99.js +2 -0
  4. package/.mochi/svelte-client/_hydrate-AvatarsView_257s9yq1wwr4f-htjfv8q9.js +14 -0
  5. package/.mochi/svelte-client/_hydrate-InstanceView_2ma45l9bybfhl-v07seqge.js +1 -0
  6. package/.mochi/svelte-client/_hydrate-SettingsView_8hmarwmf8onq-hj9yvqc4.js +38 -0
  7. package/.mochi/svelte-client/chunk-0zkdsrth.js +2 -0
  8. package/.mochi/svelte-client/{chunk-bn731rda.js → chunk-566rqq4c.js} +2 -2
  9. package/.mochi/svelte-client/chunk-79aj59e0.js +2 -0
  10. package/.mochi/svelte-client/{chunk-wvtkwfsh.js → chunk-czkxd74n.js} +1 -1
  11. package/.mochi/svelte-client/{chunk-p20tkpde.js → chunk-hxydjtda.js} +1 -1
  12. package/.mochi/svelte-client/chunk-qx1defbm.js +2 -0
  13. package/.mochi/svelte-client/chunk-r3wxmmc0.js +2 -0
  14. package/.mochi/svelte-client/{chunk-j72r8pkk.js → chunk-vs6pd2b4.js} +2 -2
  15. package/.mochi/svelte-client/{chunk-t0ap7q89.js → chunk-wjagstre.js} +1 -1
  16. package/.mochi/svelte-client/{chunk-0fjx3z3r.js → chunk-y9yy1004.js} +1 -1
  17. package/.mochi/svelte-compile/{App-59cdj86f.server.js → App-mzn3dqjm.server.js} +182 -72
  18. package/.mochi/svelte-compile/{Avatars-fmagfyj2.server.js → Avatars-c0k5jx8q.server.js} +3 -6
  19. package/.mochi/svelte-compile/{Instance-s1gqwzmx.server.js → Instance-hj76qnha.server.js} +4 -4
  20. package/.mochi/svelte-compile/{Settings-k447mt0x.server.js → Settings-tvc5yp08.server.js} +39 -28
  21. package/.mochi/svelte-compile/chunk-ed46m58e.js +22 -0
  22. package/.mochi/svelte-compile/{chunk-th2dbqvc.js → chunk-gz6hv1h5.js} +5 -1
  23. package/.mochi/svelte-compile/chunk-zd49axkf.js +65 -0
  24. package/.mochi/svelte-css/{AppBar-3bxinv4pabv8l.css → AppBar-1gtu2me3fni8e.css} +1 -1
  25. package/.mochi/svelte-css/AvatarEditor-1mjvec6bwdkv8.css +1 -0
  26. package/.mochi/svelte-css/{AvatarsView-2llo48w0wt0xn.css → AvatarsView-26ya4ep5n1ze7.css} +1 -1
  27. package/.mochi/svelte-css/Button-208u50wkms8ya.css +1 -0
  28. package/.mochi/svelte-css/CoinButton-2727devjtsn10.css +1 -0
  29. package/.mochi/svelte-css/CredMenu-7qowd1oxujet.css +1 -0
  30. package/.mochi/svelte-css/DashboardView-17a0b8epjurhb.css +1 -0
  31. package/.mochi/svelte-css/FolderBrowser-i0uj58dph27f.css +1 -0
  32. package/.mochi/svelte-css/HealthBox-r0giuhjn2wj4.css +1 -0
  33. package/.mochi/svelte-css/IdeBar-1mtgemqfuvqzi.css +1 -0
  34. package/.mochi/svelte-css/InstanceCard-25z7nas7y98zk.css +1 -0
  35. package/.mochi/svelte-css/InstanceFilter-2vglmyy0yrbbc.css +1 -0
  36. package/.mochi/svelte-css/InstanceView-oozmmctp8m3p.css +1 -0
  37. package/.mochi/svelte-css/SettingsCog-1jdeprd3ilczz.css +1 -0
  38. package/.mochi/svelte-css/{SettingsView-n3oy75uonzp2.css → SettingsView-bccw34agd2bj.css} +1 -1
  39. package/.mochi/svelte-css/StatusBadge-2eisdtnrsnfdc.css +1 -0
  40. package/.mochi/svelte-css/TerminalPane-2ywc86rhtmr9l.css +1 -0
  41. package/.mochi/svelte-css/ThemePicker-3fw4synubier0.css +1 -0
  42. package/migrations/0008_config_migrated.sql +1 -0
  43. package/package.json +8 -8
  44. package/src/components/AppBar.svelte +2 -2
  45. package/src/components/AppShell.svelte +71 -7
  46. package/src/components/AvatarEditor.svelte +10 -10
  47. package/src/components/AvatarsView.svelte +0 -0
  48. package/src/components/Button.svelte +13 -9
  49. package/src/components/CoinButton.svelte +2 -2
  50. package/src/components/CredMenu.svelte +8 -6
  51. package/src/components/DashboardView.svelte +2 -2
  52. package/src/components/FolderBrowser.svelte +39 -36
  53. package/src/components/HealthBox.svelte +5 -4
  54. package/src/components/IdeBar.svelte +286 -112
  55. package/src/components/InstanceCard.svelte +7 -7
  56. package/src/components/InstanceFilter.svelte +6 -6
  57. package/src/components/InstanceView.svelte +27 -21
  58. package/src/components/SettingsCog.svelte +4 -3
  59. package/src/components/SettingsView.svelte +7 -11
  60. package/src/components/StatusBadge.svelte +8 -6
  61. package/src/components/TerminalPane.svelte +19 -7
  62. package/src/components/ThemePicker.svelte +150 -8
  63. package/src/components/TopBar.svelte +1 -1
  64. package/src/components/UiShowcase.svelte +300 -64
  65. package/src/lib/db.server.ts +7 -3
  66. package/src/lib/devcontainer.server.ts +211 -38
  67. package/src/lib/docker.server.ts +10 -0
  68. package/src/lib/git.server.ts +28 -0
  69. package/src/lib/instances.server.ts +117 -18
  70. package/src/lib/tab-nav.ts +30 -0
  71. package/src/routes.ts +12 -0
  72. package/src/shell.html +56 -23
  73. package/src/theme.ts +8 -1
  74. package/src/toast.ts +9 -0
  75. package/src/types.ts +7 -0
  76. package/.mochi/svelte-client/HydratableIsland-aebn1fa1.js +0 -1
  77. package/.mochi/svelte-client/_hydrate-AppShell_2rtnxn6pgp2rh-rx6arrv5.js +0 -2
  78. package/.mochi/svelte-client/_hydrate-AvatarsView_257s9yq1wwr4f-jpjdmgpe.js +0 -14
  79. package/.mochi/svelte-client/_hydrate-InstanceView_2ma45l9bybfhl-c9y47dat.js +0 -1
  80. package/.mochi/svelte-client/_hydrate-SettingsView_8hmarwmf8onq-d61feq89.js +0 -38
  81. package/.mochi/svelte-client/chunk-8xvr02wh.js +0 -2
  82. package/.mochi/svelte-client/chunk-9143wy3j.js +0 -2
  83. package/.mochi/svelte-client/chunk-k37wj1jm.js +0 -2
  84. package/.mochi/svelte-client/chunk-pc0fm37c.js +0 -2
  85. package/.mochi/svelte-compile/chunk-3t7v4hk0.js +0 -52
  86. package/.mochi/svelte-compile/chunk-gkjgybb2.js +0 -15
  87. package/.mochi/svelte-css/AvatarEditor-2mdq8ss346drw.css +0 -1
  88. package/.mochi/svelte-css/Button-oharop2xz6am.css +0 -1
  89. package/.mochi/svelte-css/CoinButton-1rej3sh3tkjud.css +0 -1
  90. package/.mochi/svelte-css/CredMenu-9hwgeptp7crb.css +0 -1
  91. package/.mochi/svelte-css/DashboardView-1cy4xpqhja9sx.css +0 -1
  92. package/.mochi/svelte-css/FolderBrowser-2pxp9z1o7zixk.css +0 -1
  93. package/.mochi/svelte-css/HealthBox-er2her0jukwl.css +0 -1
  94. package/.mochi/svelte-css/IdeBar-3bgr3m5z0lpeg.css +0 -1
  95. package/.mochi/svelte-css/InstanceCard-3vnltxnbxkvay.css +0 -1
  96. package/.mochi/svelte-css/InstanceFilter-3q6x0jt0hcgpz.css +0 -1
  97. package/.mochi/svelte-css/InstanceView-1pd7j16bd2b16.css +0 -1
  98. package/.mochi/svelte-css/SettingsCog-jftyadvo64qo.css +0 -1
  99. package/.mochi/svelte-css/StatusBadge-3mq0j7z6aq4j.css +0 -1
  100. package/.mochi/svelte-css/TerminalPane-3bwgk6ulzq6b3.css +0 -1
  101. package/.mochi/svelte-css/ThemePicker-1ir8jyocm8mvi.css +0 -1
@@ -5,9 +5,12 @@
5
5
  import RotateCw from '@lucide/svelte/icons/rotate-cw';
6
6
  import Terminal from '@lucide/svelte/icons/terminal';
7
7
  import LayoutTemplate from '@lucide/svelte/icons/layout-template';
8
+ import ChevronLeft from '@lucide/svelte/icons/chevron-left';
9
+ import ChevronRight from '@lucide/svelte/icons/chevron-right';
8
10
  import Avatar from './Avatar.svelte';
9
11
  import AppBar from './AppBar.svelte';
10
12
  import { withPopupMarker } from '../lib/popup-nav.ts';
13
+ import { nextTabIndex } from '../lib/tab-nav.ts';
11
14
 
12
15
  let {
13
16
  running,
@@ -33,62 +36,164 @@
33
36
  oncommitrename: (id: string) => void;
34
37
  oncancelrename: () => void;
35
38
  } = $props();
39
+
40
+ const activeIndex = $derived(running.findIndex((i) => i.id === active));
41
+ // Falls back to the first tab so the strip always offers exactly one tab stop,
42
+ // even when `active` names an instance that has since stopped.
43
+ const focusIndex = $derived(activeIndex >= 0 ? activeIndex : 0);
44
+
45
+ let viewport = $state<HTMLDivElement | null>(null);
46
+ let content = $state<HTMLDivElement | null>(null);
47
+ let atStart = $state(true);
48
+ let atEnd = $state(true);
49
+ // `$state` rather than a plain object so `bind:this` into it is a tracked write —
50
+ // otherwise Svelte warns, and the scroll-into-view effect below misses the first bind.
51
+ const tabEls = $state<Record<string, HTMLButtonElement | null>>({});
52
+
53
+ // Both true means everything fits, so the chevrons stay out of a bar that only has 44px.
54
+ const overflowing = $derived(!atStart || !atEnd);
55
+
56
+ // 1px of slack: fractional layout widths leave scrollLeft a hair short of the true end.
57
+ function measure() {
58
+ if (!viewport) return;
59
+ atStart = viewport.scrollLeft <= 1;
60
+ atEnd = viewport.scrollLeft + viewport.clientWidth >= viewport.scrollWidth - 1;
61
+ }
62
+
63
+ function select(index: number) {
64
+ const inst = running[index];
65
+ if (!inst) return;
66
+ onselect(inst.id);
67
+ tabEls[inst.id]?.focus();
68
+ }
69
+
70
+ function onTabKeydown(e: KeyboardEvent) {
71
+ const next = nextTabIndex(activeIndex, e.key, running.length);
72
+ if (next === null) return;
73
+ e.preventDefault();
74
+ select(next);
75
+ }
76
+
77
+ function page(direction: -1 | 1) {
78
+ if (!viewport) return;
79
+ viewport.scrollBy({ left: direction * Math.max(160, viewport.clientWidth * 0.7) });
80
+ }
81
+
82
+ // Observing the content too, not just the viewport — adding or removing a tab moves
83
+ // scrollWidth without ever resizing the scroll container.
84
+ $effect(() => {
85
+ const vp = viewport;
86
+ const ct = content;
87
+ if (!vp || !ct) return;
88
+ measure();
89
+ const ro = new ResizeObserver(measure);
90
+ ro.observe(vp);
91
+ ro.observe(ct);
92
+ vp.addEventListener('scroll', measure, { passive: true });
93
+ return () => {
94
+ ro.disconnect();
95
+ vp.removeEventListener('scroll', measure);
96
+ };
97
+ });
98
+
99
+ // Keyboard selection can land on a tab that's scrolled out of sight.
100
+ $effect(() => {
101
+ tabEls[active]?.scrollIntoView({ block: 'nearest', inline: 'nearest' });
102
+ });
36
103
  </script>
37
104
 
38
105
  <AppBar>
39
106
  {#if running.length > 0}
40
- <nav class="tabs">
41
- {#each running as inst (inst.id)}
42
- <div
43
- class="tab"
44
- class:active={inst.id === active}
45
- class:attn-done={inst.id !== active && attention[inst.id] === 'done'}
46
- class:attn-waiting={inst.id !== active && attention[inst.id] === 'waiting'}
107
+ <div class="strip">
108
+ {#if overflowing}
109
+ <button
110
+ type="button"
111
+ class="page"
112
+ disabled={atStart}
113
+ onclick={() => page(-1)}
114
+ aria-label="Scroll tabs left"><ChevronLeft size={16} /></button
47
115
  >
48
- {#if editingId === inst.id}
49
- <div class="tab-label editing">
50
- <Avatar
51
- name={inst.name}
52
- art={findAvatar(inst.avatar ?? undefined) ?? null}
53
- scale={4}
54
- />
55
- <!-- svelte-ignore a11y_autofocus -->
56
- <input
57
- class="tab-name-edit"
58
- bind:value={editingName}
59
- autofocus
60
- onfocus={(e) => (e.currentTarget as HTMLInputElement).select()}
61
- onblur={() => oncommitrename(inst.id)}
62
- onkeydown={(e) => {
63
- if (e.key === 'Enter') (e.currentTarget as HTMLInputElement).blur();
64
- else if (e.key === 'Escape') oncancelrename();
65
- }}
66
- />
67
- </div>
68
- {:else}
69
- <button
70
- type="button"
71
- class="tab-label"
72
- onclick={() => onselect(inst.id)}
73
- ondblclick={() => onstartrename(inst)}
74
- title={inst.name}
75
- >
76
- <Avatar
77
- name={inst.name}
78
- art={findAvatar(inst.avatar ?? undefined) ?? null}
79
- scale={4}
80
- />
81
- <span class="tab-name">{inst.name}</span>
82
- {#if inst.mode === 'terminal'}
83
- <span class="tab-mode" title="Terminal-only instance"><Terminal size={13} /></span>
84
- {:else}
85
- <span class="tab-mode" title="Full IDE instance"><LayoutTemplate size={13} /></span>
86
- {/if}
87
- </button>
88
- {/if}
116
+ {/if}
117
+ <div class="viewport" bind:this={viewport}>
118
+ <div class="tabs" role="tablist" aria-label="Instances" bind:this={content}>
119
+ {#each running as inst, i (inst.id)}
120
+ {#if editingId === inst.id}
121
+ <div class="tab editing">
122
+ <Avatar
123
+ name={inst.name}
124
+ art={findAvatar(inst.avatar ?? undefined) ?? null}
125
+ scale={3}
126
+ />
127
+ <!-- svelte-ignore a11y_autofocus -->
128
+ <input
129
+ class="tab-name-edit"
130
+ bind:value={editingName}
131
+ autofocus
132
+ onfocus={(e) => (e.currentTarget as HTMLInputElement).select()}
133
+ onblur={() => oncommitrename(inst.id)}
134
+ onkeydown={(e) => {
135
+ if (e.key === 'Enter') (e.currentTarget as HTMLInputElement).blur();
136
+ else if (e.key === 'Escape') oncancelrename();
137
+ }}
138
+ />
139
+ </div>
140
+ {:else}
141
+ <button
142
+ bind:this={tabEls[inst.id]}
143
+ type="button"
144
+ class="tab"
145
+ class:active={inst.id === active}
146
+ id="tab-{inst.id}"
147
+ role="tab"
148
+ aria-selected={inst.id === active}
149
+ tabindex={i === focusIndex ? 0 : -1}
150
+ onclick={() => onselect(inst.id)}
151
+ ondblclick={() => onstartrename(inst)}
152
+ onkeydown={onTabKeydown}
153
+ title={inst.name}
154
+ >
155
+ <Avatar
156
+ name={inst.name}
157
+ art={findAvatar(inst.avatar ?? undefined) ?? null}
158
+ scale={3}
159
+ />
160
+ <span class="tab-name">{inst.name}</span>
161
+ <span
162
+ class="tab-mode"
163
+ aria-label={inst.mode === 'terminal'
164
+ ? 'Terminal-only instance'
165
+ : 'Full IDE instance'}
166
+ >
167
+ {#if inst.mode === 'terminal'}
168
+ <Terminal size={11} />
169
+ {:else}
170
+ <LayoutTemplate size={11} />
171
+ {/if}
172
+ </span>
173
+ <!-- The focused tab never lights up, so a lit LED always means "needs your eyes". -->
174
+ {#if inst.id !== active && attention[inst.id]}
175
+ <span
176
+ class="attn {attention[inst.id]}"
177
+ aria-label={attention[inst.id] === 'waiting'
178
+ ? 'Claude is waiting for input'
179
+ : 'Claude finished'}
180
+ ></span>
181
+ {/if}
182
+ </button>
183
+ {/if}
184
+ {/each}
89
185
  </div>
90
- {/each}
91
- </nav>
186
+ </div>
187
+ {#if overflowing}
188
+ <button
189
+ type="button"
190
+ class="page trailing"
191
+ disabled={atEnd}
192
+ onclick={() => page(1)}
193
+ aria-label="Scroll tabs right"><ChevronRight size={16} /></button
194
+ >
195
+ {/if}
196
+ </div>
92
197
  {/if}
93
198
  <div class="right">
94
199
  {#if onreload}
@@ -139,77 +244,110 @@
139
244
  }
140
245
  .cog:hover,
141
246
  .reload:hover {
142
- background: var(--ink);
143
- color: var(--bg);
247
+ background: var(--fill);
248
+ color: var(--fill-ink);
144
249
  }
145
- .tabs {
250
+
251
+ .strip {
146
252
  display: flex;
147
253
  align-items: stretch;
148
- overflow-x: auto;
149
254
  min-width: 0;
150
255
  }
151
- .tab {
152
- display: inline-flex;
153
- align-items: stretch;
154
- border-right: 1px solid var(--rule-soft);
155
- }
156
- .tab.active {
157
- background: var(--ink);
158
- }
159
- /* The focused tab never gets these, so a pulsing tab always means "needs your eyes". */
160
- .tab.attn-done {
161
- animation: attn-pulse-green 1.8s ease-in-out infinite;
162
- }
163
- .tab.attn-waiting {
164
- animation: attn-pulse-amber 1.8s ease-in-out infinite;
165
- }
166
- @keyframes attn-pulse-green {
167
- 0%,
168
- 100% {
169
- background: transparent;
170
- box-shadow: inset 0 -2px 0 color-mix(in srgb, var(--attn-done) 40%, transparent);
171
- }
172
- 50% {
173
- background: color-mix(in srgb, var(--attn-done) 32%, transparent);
174
- box-shadow: inset 0 -2px 0 var(--attn-done);
175
- }
256
+ /* The native scrollbar would eat into a 44px bar, so the chevrons replace it. */
257
+ .viewport {
258
+ display: flex;
259
+ min-width: 0;
260
+ overflow-x: auto;
261
+ scroll-behavior: smooth;
262
+ scrollbar-width: none;
176
263
  }
177
- @keyframes attn-pulse-amber {
178
- 0%,
179
- 100% {
180
- background: transparent;
181
- box-shadow: inset 0 -2px 0 color-mix(in srgb, var(--attn-waiting) 40%, transparent);
182
- }
183
- 50% {
184
- background: color-mix(in srgb, var(--attn-waiting) 32%, transparent);
185
- box-shadow: inset 0 -2px 0 var(--attn-waiting);
186
- }
264
+ .viewport::-webkit-scrollbar {
265
+ display: none;
187
266
  }
188
267
  @media (prefers-reduced-motion: reduce) {
189
- .tab.attn-done,
190
- .tab.attn-waiting {
191
- animation-duration: 0s;
192
- background: color-mix(in srgb, var(--attn-done) 28%, transparent);
193
- }
194
- .tab.attn-waiting {
195
- background: color-mix(in srgb, var(--attn-waiting) 28%, transparent);
268
+ .viewport {
269
+ scroll-behavior: auto;
196
270
  }
197
271
  }
198
- .tab-label {
272
+ .tabs {
273
+ display: flex;
274
+ align-items: stretch;
275
+ flex: none;
276
+ }
277
+
278
+ .page {
279
+ display: inline-flex;
280
+ align-items: center;
281
+ justify-content: center;
282
+ width: 28px;
283
+ flex: none;
199
284
  appearance: none;
200
- background: transparent;
285
+ margin: 0;
286
+ padding: 0;
201
287
  border: 0;
288
+ border-right: 1px solid var(--rule);
289
+ background: var(--bg-card);
290
+ color: var(--ink);
202
291
  cursor: pointer;
203
- color: var(--ink-soft);
204
- font-family: var(--font-mono);
292
+ }
293
+ .page.trailing {
294
+ border-right: 0;
295
+ border-left: 1px solid var(--rule);
296
+ }
297
+ .page:hover:not(:disabled) {
298
+ background: var(--fill);
299
+ color: var(--fill-ink);
300
+ }
301
+ .page:disabled {
302
+ opacity: var(--dim);
303
+ cursor: default;
304
+ }
305
+ .page:focus-visible {
306
+ outline: 2px solid var(--ink);
307
+ outline-offset: -2px;
308
+ }
309
+
310
+ .tab {
205
311
  display: inline-flex;
206
312
  align-items: center;
207
313
  gap: 10px;
208
- padding: 0 12px;
314
+ flex: none;
209
315
  max-width: 240px;
210
- font-size: 12px;
316
+ appearance: none;
317
+ margin: 0;
318
+ padding: 0 12px;
319
+ border: 0;
320
+ border-right: 1px solid var(--rule);
321
+ background: transparent;
322
+ color: var(--ink-soft);
323
+ font-family: var(--font-display);
324
+ font-weight: 700;
325
+ font-size: 13px;
211
326
  text-transform: uppercase;
212
327
  letter-spacing: 0.06em;
328
+ cursor: pointer;
329
+ /* Quantized like the instance cards — motion in this theme reads as digital. */
330
+ transition: background 0.08s steps(2);
331
+ }
332
+ /* Inversion is the house hover idiom, but it's also the active state here, so hovering
333
+ gets a tint instead — that keeps idle → hover → active legible as three steps. */
334
+ .tab:not(.active):hover {
335
+ background: color-mix(in srgb, var(--ink) 12%, transparent);
336
+ color: var(--ink);
337
+ }
338
+ .tab.active {
339
+ background: var(--fill);
340
+ color: var(--fill-ink);
341
+ cursor: default;
342
+ /* The key has sunk into the chassis; this strip is the slot it dropped out of. */
343
+ box-shadow: inset 0 3px 0 var(--bg);
344
+ }
345
+ .tab:focus-visible {
346
+ outline: 2px solid var(--ink);
347
+ outline-offset: -2px;
348
+ }
349
+ .tab.active:focus-visible {
350
+ outline-color: var(--fill-ink);
213
351
  }
214
352
  .tab-name {
215
353
  white-space: nowrap;
@@ -219,29 +357,65 @@
219
357
  .tab-mode {
220
358
  display: inline-flex;
221
359
  align-items: center;
360
+ justify-content: center;
222
361
  flex: none;
223
- opacity: 0.75;
362
+ width: 18px;
363
+ height: 18px;
364
+ border: 1px solid currentColor;
365
+ opacity: 0.7;
224
366
  }
225
- .tab:not(.active) .tab-label:hover {
226
- color: var(--ink);
367
+
368
+ .attn {
369
+ flex: none;
370
+ width: 10px;
371
+ height: 10px;
372
+ border: 1px solid var(--edge);
373
+ animation: tab-attn-pulse 1.4s ease-in-out infinite;
227
374
  }
228
- .tab.active .tab-label {
229
- color: var(--bg);
375
+ .attn.done {
376
+ background: var(--attn-done);
377
+ color: var(--attn-done);
230
378
  }
231
- .tab-label.editing {
379
+ .attn.waiting {
380
+ background: var(--attn-waiting);
381
+ color: var(--attn-waiting);
382
+ }
383
+ /* Redeclared rather than shared with InstanceCard — Svelte scopes keyframes per component. */
384
+ @keyframes tab-attn-pulse {
385
+ 0%,
386
+ 100% {
387
+ box-shadow: 0 0 0 0 transparent;
388
+ filter: brightness(0.85);
389
+ }
390
+ 50% {
391
+ box-shadow: 0 0 6px 2px color-mix(in srgb, currentColor 60%, transparent);
392
+ filter: brightness(1.15);
393
+ }
394
+ }
395
+ @media (prefers-reduced-motion: reduce) {
396
+ .attn {
397
+ animation: none;
398
+ filter: none;
399
+ }
400
+ }
401
+
402
+ .tab.editing {
232
403
  cursor: default;
233
404
  }
234
405
  .tab-name-edit {
235
- width: 130px;
406
+ width: 148px;
236
407
  font-family: var(--font-mono);
237
408
  font-size: 12px;
238
409
  text-transform: uppercase;
239
410
  letter-spacing: 0.06em;
240
411
  margin: 0;
241
- padding: 2px 4px;
242
- border: 1px solid var(--ink-soft);
412
+ padding: 3px 6px;
413
+ border: 1px solid var(--ink);
243
414
  background: var(--bg);
244
415
  color: var(--ink);
245
- outline: none;
416
+ }
417
+ .tab-name-edit:focus-visible {
418
+ outline: 2px solid var(--ink);
419
+ outline-offset: -2px;
246
420
  }
247
421
  </style>
@@ -142,7 +142,7 @@
142
142
  }
143
143
  .card:hover:not(.busy) {
144
144
  transform: translate(-1px, -1px);
145
- box-shadow: 6px 6px 0 var(--ink);
145
+ box-shadow: 6px 6px 0 var(--shadow);
146
146
  }
147
147
  /* Translucent scrim dims the card body and blocks clicks on the buttons beneath. */
148
148
  .busy-overlay {
@@ -161,15 +161,15 @@
161
161
  text-transform: uppercase;
162
162
  letter-spacing: 0.1em;
163
163
  padding: 4px 9px;
164
- border: 1px solid var(--ink);
165
- background: var(--ink);
166
- color: var(--bg);
164
+ border: 1px solid var(--fill);
165
+ background: var(--fill);
166
+ color: var(--fill-ink);
167
167
  animation: lcd-blink 1.1s steps(1) infinite;
168
168
  }
169
169
  @keyframes lcd-blink {
170
170
  50% {
171
171
  background: transparent;
172
- color: var(--ink);
172
+ color: var(--fill);
173
173
  }
174
174
  }
175
175
  @media (prefers-reduced-motion: reduce) {
@@ -186,7 +186,7 @@
186
186
  flex: none;
187
187
  width: 14px;
188
188
  height: 14px;
189
- border: 1px solid var(--ink);
189
+ border: 1px solid var(--edge);
190
190
  }
191
191
  .attn.attn-done {
192
192
  background: var(--attn-done);
@@ -266,7 +266,7 @@
266
266
  font-size: 12px;
267
267
  color: var(--ink);
268
268
  background: var(--bg);
269
- border: 1px solid var(--ink);
269
+ border: 1px solid var(--edge);
270
270
  padding: 8px 10px;
271
271
  max-height: 80px;
272
272
  overflow: auto;
@@ -30,7 +30,7 @@
30
30
  .picker {
31
31
  display: inline-flex;
32
32
  flex: none;
33
- border: 1px solid var(--ink);
33
+ border: 1px solid var(--edge);
34
34
  background: var(--bg-card);
35
35
  }
36
36
  .segment {
@@ -49,16 +49,16 @@
49
49
  cursor: pointer;
50
50
  }
51
51
  .segment + .segment {
52
- border-left: 1px solid var(--ink);
52
+ border-left: 1px solid var(--edge);
53
53
  }
54
54
  .segment:hover:not(.active) {
55
- background: var(--ink);
56
- color: var(--bg);
55
+ background: var(--fill);
56
+ color: var(--fill-ink);
57
57
  }
58
58
  .segment.active {
59
59
  font-weight: 700;
60
- background: var(--ink);
61
- color: var(--bg);
60
+ background: var(--fill);
61
+ color: var(--fill-ink);
62
62
  cursor: default;
63
63
  }
64
64
  .segment:focus-visible {
@@ -11,6 +11,7 @@
11
11
  import { enhance } from 'mochi-framework';
12
12
  import type { MochiEnhanceOptions } from 'mochi-framework';
13
13
  import { onBackLinkClick, installPopupBackTrap } from '../lib/popup-nav.ts';
14
+ import { syncTheme } from '../theme.ts';
14
15
  import { tick } from 'svelte';
15
16
 
16
17
  let { id, injectionChecks = 0 }: { id: string; injectionChecks?: number } = $props();
@@ -61,6 +62,8 @@
61
62
  } else if (msg.type === 'health' && msg.data.id === id) {
62
63
  health = msg.data.health;
63
64
  lastFetchedAt = Date.now();
65
+ } else if (msg.type === 'theme') {
66
+ syncTheme(msg.data.value);
64
67
  }
65
68
  })
66
69
  );
@@ -323,12 +326,13 @@
323
326
  letter-spacing: 0.06em;
324
327
  color: var(--ink);
325
328
  text-decoration: none;
326
- border: 1px solid var(--ink);
329
+ border: 1px solid var(--edge);
327
330
  padding: 7px 12px;
328
331
  }
329
332
  .open:hover {
330
- background: var(--ink);
331
- color: var(--bg);
333
+ background: var(--fill);
334
+ border-color: var(--fill);
335
+ color: var(--fill-ink);
332
336
  }
333
337
  .restart {
334
338
  display: inline-flex;
@@ -341,16 +345,17 @@
341
345
  letter-spacing: 0.06em;
342
346
  color: var(--ink);
343
347
  background: var(--bg);
344
- border: 1px solid var(--ink);
348
+ border: 1px solid var(--edge);
345
349
  padding: 7px 12px;
346
350
  cursor: pointer;
347
351
  }
348
352
  .restart:hover:not(:disabled) {
349
- background: var(--ink);
350
- color: var(--bg);
353
+ background: var(--fill);
354
+ border-color: var(--fill);
355
+ color: var(--fill-ink);
351
356
  }
352
357
  .restart:disabled {
353
- opacity: 0.5;
358
+ opacity: var(--dim-panel);
354
359
  cursor: not-allowed;
355
360
  }
356
361
  .restart-err {
@@ -406,17 +411,17 @@
406
411
  text-transform: uppercase;
407
412
  letter-spacing: 0.08em;
408
413
  padding: 4px 9px;
409
- background: var(--bg);
410
- color: var(--ink);
411
- border: 1px solid var(--bg);
414
+ background: var(--fill-ink);
415
+ color: var(--fill);
416
+ border: 1px solid var(--fill-ink);
412
417
  cursor: pointer;
413
418
  }
414
419
  .rebuild:hover:not(:disabled) {
415
420
  background: transparent;
416
- color: var(--bg);
421
+ color: var(--fill-ink);
417
422
  }
418
423
  .rebuild:disabled {
419
- opacity: 0.5;
424
+ opacity: var(--dim-panel);
420
425
  cursor: not-allowed;
421
426
  }
422
427
  .ports-body {
@@ -499,14 +504,15 @@
499
504
  text-transform: uppercase;
500
505
  letter-spacing: 0.06em;
501
506
  padding: 6px 14px;
502
- border: 1px solid var(--ink);
507
+ border: 1px solid var(--edge);
503
508
  background: var(--bg-card);
504
509
  color: var(--ink);
505
510
  cursor: pointer;
506
511
  }
507
512
  .add button:hover {
508
- background: var(--ink);
509
- color: var(--bg);
513
+ background: var(--fill);
514
+ border-color: var(--fill);
515
+ color: var(--fill-ink);
510
516
  }
511
517
  .port-err {
512
518
  margin: 10px 0 0;
@@ -549,18 +555,18 @@
549
555
  text-transform: uppercase;
550
556
  letter-spacing: 0.08em;
551
557
  padding: 4px 9px;
552
- background: var(--bg);
553
- color: var(--ink);
554
- border: 1px solid var(--bg);
558
+ background: var(--fill-ink);
559
+ color: var(--fill);
560
+ border: 1px solid var(--fill-ink);
555
561
  cursor: pointer;
556
562
  }
557
563
  .copy:hover:not(:disabled) {
558
564
  background: transparent;
559
- color: var(--bg);
565
+ color: var(--fill-ink);
560
566
  }
561
567
  .copy.active {
562
568
  background: transparent;
563
- color: var(--bg);
569
+ color: var(--fill-ink);
564
570
  }
565
571
  /* The dot, not the inverted fill, is what separates "following" from a mere hover. */
566
572
  .dot {
@@ -571,7 +577,7 @@
571
577
  background: var(--attn-done);
572
578
  }
573
579
  .copy:disabled {
574
- opacity: 0.5;
580
+ opacity: var(--dim-panel);
575
581
  cursor: not-allowed;
576
582
  }
577
583
  .logs {
@@ -19,12 +19,13 @@
19
19
  justify-content: center;
20
20
  width: 32px;
21
21
  height: 32px;
22
- border: 1px solid var(--ink);
22
+ border: 1px solid var(--edge);
23
23
  background: var(--bg-card);
24
24
  color: var(--ink);
25
25
  }
26
26
  .cog:hover {
27
- background: var(--ink);
28
- color: var(--bg);
27
+ background: var(--fill);
28
+ border-color: var(--fill);
29
+ color: var(--fill-ink);
29
30
  }
30
31
  </style>