codebay 0.0.1 → 0.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.
Files changed (202) hide show
  1. package/.mochi/import-css/index-0nwv3654.css +18 -0
  2. package/.mochi/import-css/index-xndx4980.css +54 -0
  3. package/.mochi/manifest.json +1608 -0
  4. package/.mochi/server-island.js +1 -0
  5. package/.mochi/svelte-client/HydratableIsland-ps09gjy2.js +1 -0
  6. package/.mochi/svelte-client/_hydrate-AppShell_2rtnxn6pgp2rh-62w5zwww.js +2 -0
  7. package/.mochi/svelte-client/_hydrate-AvatarsView_257s9yq1wwr4f-jmm501zf.js +13 -0
  8. package/.mochi/svelte-client/_hydrate-InstanceView_2ma45l9bybfhl-ewk4n2c0.js +1 -0
  9. package/.mochi/svelte-client/_hydrate-SettingsView_8hmarwmf8onq-e7e89md0.js +23 -0
  10. package/.mochi/svelte-client/chunk-jx5cekhh.js +2 -0
  11. package/.mochi/svelte-client/chunk-k3my1snp.js +2 -0
  12. package/.mochi/svelte-client/chunk-kgn1b2t1.js +4 -0
  13. package/.mochi/svelte-client/chunk-ry7q5zk5.js +1 -0
  14. package/.mochi/svelte-client/chunk-x843txjc.js +2 -0
  15. package/.mochi/svelte-client/chunk-zjcg8kw5.js +2 -0
  16. package/.mochi/svelte-compile/App-8k1gyxdc.server.js +1167 -0
  17. package/.mochi/svelte-compile/Avatars-f7c6rp96.server.js +351 -0
  18. package/.mochi/svelte-compile/ClientStats-z3tn9ad6.server.js +99 -0
  19. package/.mochi/svelte-compile/DefaultError-wh77vebf.server.js +52 -0
  20. package/.mochi/svelte-compile/Instance-yvps12zg.server.js +355 -0
  21. package/.mochi/svelte-compile/Settings-jwg3httb.server.js +1169 -0
  22. package/.mochi/svelte-compile/chunk-4kwy539a.js +14 -0
  23. package/.mochi/svelte-compile/chunk-9bjk7yx6.js +81 -0
  24. package/.mochi/svelte-compile/chunk-dqgstte4.js +52 -0
  25. package/.mochi/svelte-compile/chunk-sxqys3nw.js +1668 -0
  26. package/.mochi/svelte-compile/chunk-v6796ga4.js +3295 -0
  27. package/.mochi/svelte-compile/chunk-we0ggtws.js +34 -0
  28. package/.mochi/svelte-css/AppBar-3bxinv4pabv8l.css +1 -0
  29. package/.mochi/svelte-css/AppShell-36mpg5smnbmuw.css +1 -0
  30. package/.mochi/svelte-css/Avatar-24th63awm9mna.css +1 -0
  31. package/.mochi/svelte-css/AvatarEditor-1ryrs1md5g36y.css +1 -0
  32. package/.mochi/svelte-css/AvatarsView-3hq37u4dgtljn.css +1 -0
  33. package/.mochi/svelte-css/BackLink-2wlu0eovz9j86.css +1 -0
  34. package/.mochi/svelte-css/BranchBox-1yqu1q2mgnjwz.css +1 -0
  35. package/.mochi/svelte-css/Brand-1la7zw5uhcbl4.css +1 -0
  36. package/.mochi/svelte-css/Button-2vtbrzb5kgu25.css +1 -0
  37. package/.mochi/svelte-css/CheckmarkIcon-3qlazj2oc50jc.css +1 -0
  38. package/.mochi/svelte-css/ClientStats-19qzckq40p2q9.css +1 -0
  39. package/.mochi/svelte-css/CoinButton-1rej3sh3tkjud.css +1 -0
  40. package/.mochi/svelte-css/CredMenu-9hwgeptp7crb.css +1 -0
  41. package/.mochi/svelte-css/DashboardView-1cy4xpqhja9sx.css +1 -0
  42. package/.mochi/svelte-css/DefaultError-2b0daamydxgb2.css +1 -0
  43. package/.mochi/svelte-css/ErrorIcon-2x0tzsqxskw28.css +1 -0
  44. package/.mochi/svelte-css/FolderBrowser-1x428zb3f2kti.css +1 -0
  45. package/.mochi/svelte-css/HealthBox-er2her0jukwl.css +1 -0
  46. package/.mochi/svelte-css/IdeBar-2qoqi2ya9zdn3.css +1 -0
  47. package/.mochi/svelte-css/IdeLoader-26rlko8e00j3g.css +1 -0
  48. package/.mochi/svelte-css/InstanceCard-16f77aysm7lgp.css +1 -0
  49. package/.mochi/svelte-css/InstanceView-wq85bxtusont.css +1 -0
  50. package/.mochi/svelte-css/LoaderIcon-soq5wt3p05ye.css +1 -0
  51. package/.mochi/svelte-css/PortsBox-1t25b5yg7jva3.css +1 -0
  52. package/.mochi/svelte-css/SettingsCog-jftyadvo64qo.css +1 -0
  53. package/.mochi/svelte-css/SettingsView-3bbkzsvnrth3v.css +1 -0
  54. package/.mochi/svelte-css/Skeleton-8k69usiju43h.css +1 -0
  55. package/.mochi/svelte-css/StatusBadge-3mq0j7z6aq4j.css +1 -0
  56. package/.mochi/svelte-css/ThemePicker-1ir8jyocm8mvi.css +1 -0
  57. package/.mochi/svelte-css/ToastBar-3havgkmtofntq.css +1 -0
  58. package/.mochi/svelte-css/ToastIcon-2z5wrkc95khg0.css +1 -0
  59. package/.mochi/svelte-css/ToastMessage-2jfwb2og2lfa8.css +1 -0
  60. package/.mochi/svelte-css/ToastWrapper-1fbnym10rm2nv.css +1 -0
  61. package/.mochi/svelte-css/Toaster-kk60j193854n.css +1 -0
  62. package/.mochi/svelte-css/TopBar-2q87li6yt8ids.css +1 -0
  63. package/LICENSE +21 -0
  64. package/README.md +33 -3
  65. package/bin/codebay.ts +22 -0
  66. package/migrations/0001_init.sql +20 -0
  67. package/migrations/0002_port_forwards.sql +9 -0
  68. package/migrations/0003_options.sql +4 -0
  69. package/migrations/0004_instance_image.sql +1 -0
  70. package/package.json +87 -9
  71. package/public/sounds/done.wav +0 -0
  72. package/public/sounds/waiting.wav +0 -0
  73. package/src/__fixtures__/HelloWorld.svelte +4 -0
  74. package/src/additional-svelte-typings.d.ts +1 -0
  75. package/src/api.ts +61 -0
  76. package/src/avatars/anchor.ts +17 -0
  77. package/src/avatars/bear.ts +17 -0
  78. package/src/avatars/bee.ts +17 -0
  79. package/src/avatars/bob-marley.ts +17 -0
  80. package/src/avatars/car.ts +17 -0
  81. package/src/avatars/carpathians.ts +17 -0
  82. package/src/avatars/cat-body.ts +17 -0
  83. package/src/avatars/cat-sits.ts +17 -0
  84. package/src/avatars/cat.ts +17 -0
  85. package/src/avatars/celtic-cross.ts +17 -0
  86. package/src/avatars/cherry.ts +17 -0
  87. package/src/avatars/cotton.ts +17 -0
  88. package/src/avatars/crab.ts +17 -0
  89. package/src/avatars/crown.ts +17 -0
  90. package/src/avatars/diamond.ts +17 -0
  91. package/src/avatars/dog.ts +17 -0
  92. package/src/avatars/elephant.ts +17 -0
  93. package/src/avatars/fish.ts +17 -0
  94. package/src/avatars/fleur-de-lis.ts +17 -0
  95. package/src/avatars/flower.ts +17 -0
  96. package/src/avatars/fox.ts +17 -0
  97. package/src/avatars/frog.ts +17 -0
  98. package/src/avatars/ghost.ts +17 -0
  99. package/src/avatars/giraffe.ts +17 -0
  100. package/src/avatars/glasses.ts +17 -0
  101. package/src/avatars/heart.ts +17 -0
  102. package/src/avatars/index.ts +128 -0
  103. package/src/avatars/invader.ts +17 -0
  104. package/src/avatars/kangaroo.ts +17 -0
  105. package/src/avatars/key.ts +17 -0
  106. package/src/avatars/lightning.ts +17 -0
  107. package/src/avatars/mushroom.ts +17 -0
  108. package/src/avatars/octopus.ts +17 -0
  109. package/src/avatars/owl.ts +17 -0
  110. package/src/avatars/penguin.ts +17 -0
  111. package/src/avatars/planet.ts +17 -0
  112. package/src/avatars/rabbit.ts +17 -0
  113. package/src/avatars/rat.ts +17 -0
  114. package/src/avatars/robot.ts +17 -0
  115. package/src/avatars/rocket.ts +17 -0
  116. package/src/avatars/sea.ts +17 -0
  117. package/src/avatars/serialize.ts +65 -0
  118. package/src/avatars/skull.ts +17 -0
  119. package/src/avatars/snail.ts +17 -0
  120. package/src/avatars/snake.ts +17 -0
  121. package/src/avatars/snakegame.ts +17 -0
  122. package/src/avatars/star.ts +17 -0
  123. package/src/avatars/svelte.ts +17 -0
  124. package/src/avatars/target.ts +17 -0
  125. package/src/avatars/tree.ts +17 -0
  126. package/src/avatars/types.ts +24 -0
  127. package/src/avatars/ukraine.ts +17 -0
  128. package/src/avatars/whale.ts +17 -0
  129. package/src/avatars/wheat.ts +17 -0
  130. package/src/avatars/yinyan.ts +17 -0
  131. package/src/components/AppBar.svelte +35 -0
  132. package/src/components/AppShell.svelte +302 -0
  133. package/src/components/Avatar.svelte +235 -0
  134. package/src/components/AvatarEditor.svelte +321 -0
  135. package/src/components/AvatarsView.svelte +113 -0
  136. package/src/components/BranchBox.svelte +32 -0
  137. package/src/components/Brand.svelte +41 -0
  138. package/src/components/Button.svelte +161 -0
  139. package/src/components/CoinButton.svelte +121 -0
  140. package/src/components/CredMenu.svelte +171 -0
  141. package/src/components/DashboardView.svelte +198 -0
  142. package/src/components/FolderBrowser.svelte +635 -0
  143. package/src/components/HealthBox.svelte +161 -0
  144. package/src/components/IdeBar.svelte +186 -0
  145. package/src/components/IdeLoader.svelte +135 -0
  146. package/src/components/InstanceCard.svelte +220 -0
  147. package/src/components/InstanceView.svelte +565 -0
  148. package/src/components/PortsBox.svelte +73 -0
  149. package/src/components/SettingsCog.svelte +22 -0
  150. package/src/components/SettingsView.svelte +1484 -0
  151. package/src/components/Skeleton.svelte +43 -0
  152. package/src/components/StatusBadge.svelte +54 -0
  153. package/src/components/ThemePicker.svelte +77 -0
  154. package/src/components/TopBar.svelte +64 -0
  155. package/src/components/UiShowcase.svelte +578 -0
  156. package/src/components/ui-showcase/ComponentDemo.svelte +85 -0
  157. package/src/container-injections/attention-hooks.ts +117 -0
  158. package/src/container-injections/claude-aliases.ts +43 -0
  159. package/src/container-injections/claude-code-credentials.ts +145 -0
  160. package/src/container-injections/claude-code-custom.ts +125 -0
  161. package/src/container-injections/claude-no-coauthor.ts +35 -0
  162. package/src/container-injections/claude-skip-permissions.ts +38 -0
  163. package/src/container-injections/claude-statusline.ts +109 -0
  164. package/src/container-injections/git-identity.ts +63 -0
  165. package/src/container-injections/git-safe-directory.ts +18 -0
  166. package/src/container-injections/github-credentials.ts +162 -0
  167. package/src/container-injections/host-env-vars.ts +108 -0
  168. package/src/container-injections/tmux.ts +67 -0
  169. package/src/fontsource.d.ts +3 -0
  170. package/src/index.ts +81 -0
  171. package/src/lib/auth.server.ts +95 -0
  172. package/src/lib/bridge.server.ts +34 -0
  173. package/src/lib/config.server.ts +122 -0
  174. package/src/lib/crypto.server.ts +12 -0
  175. package/src/lib/db.server.ts +196 -0
  176. package/src/lib/devcontainer.server.ts +503 -0
  177. package/src/lib/docker-client.server.ts +61 -0
  178. package/src/lib/docker.server.ts +139 -0
  179. package/src/lib/exec.server.ts +92 -0
  180. package/src/lib/git.server.ts +68 -0
  181. package/src/lib/health.server.ts +105 -0
  182. package/src/lib/injections.server.ts +72 -0
  183. package/src/lib/instances.server.ts +552 -0
  184. package/src/lib/picker.server.ts +46 -0
  185. package/src/lib/ports.server.ts +9 -0
  186. package/src/lib/proxy.server.ts +170 -0
  187. package/src/lib/repo-url.ts +60 -0
  188. package/src/lib/spawn.server.ts +14 -0
  189. package/src/lib/theme.server.ts +22 -0
  190. package/src/live.ts +50 -0
  191. package/src/pages/App.svelte +14 -0
  192. package/src/pages/Avatars.svelte +7 -0
  193. package/src/pages/Instance.svelte +9 -0
  194. package/src/pages/Settings.svelte +73 -0
  195. package/src/pages/UI.svelte +10 -0
  196. package/src/routes.ts +440 -0
  197. package/src/settings.ts +14 -0
  198. package/src/shell.html +100 -0
  199. package/src/sound.ts +69 -0
  200. package/src/theme.ts +24 -0
  201. package/src/types.ts +79 -0
  202. package/svelte.config.js +5 -0
@@ -0,0 +1,578 @@
1
+ <script lang="ts">
2
+ import Plus from '@lucide/svelte/icons/plus';
3
+ import toast, { Toaster } from 'svelte-french-toast';
4
+ import { avatars } from '../avatars/index.ts';
5
+ import type { AuthProvider, InstanceHealth, PortForward, Preflight } from '../types.ts';
6
+
7
+ let { preflight }: { preflight: Preflight } = $props();
8
+ import Avatar from './Avatar.svelte';
9
+ import Button from './Button.svelte';
10
+ import Brand from './Brand.svelte';
11
+ import CredMenu from './CredMenu.svelte';
12
+ import SettingsCog from './SettingsCog.svelte';
13
+ import TopBar from './TopBar.svelte';
14
+ import IdeLoader from './IdeLoader.svelte';
15
+ import HealthBox from './HealthBox.svelte';
16
+ import BranchBox from './BranchBox.svelte';
17
+ import PortsBox from './PortsBox.svelte';
18
+ import StatusBadge from './StatusBadge.svelte';
19
+ import ComponentDemo from './ui-showcase/ComponentDemo.svelte';
20
+ import type { Instance } from '../types.ts';
21
+
22
+ // The swatch fill reads var(--token) so it auto-syncs with shell.html; `hex` is
23
+ // only a label and always shows the light-theme value.
24
+ const palette: { group: string; swatches: { token: string; hex: string; border?: boolean }[] }[] =
25
+ [
26
+ {
27
+ group: 'Surfaces',
28
+ swatches: [
29
+ { token: '--bg', hex: '#d7d8d2', border: true },
30
+ { token: '--bg-card', hex: '#e7e8e2', border: true },
31
+ { token: '--screen-bg', hex: '#0d0e0a' }
32
+ ]
33
+ },
34
+ {
35
+ group: 'Ink / text',
36
+ swatches: [
37
+ { token: '--ink', hex: '#14150f' },
38
+ { token: '--ink-soft', hex: '#45463e' },
39
+ { token: '--ink-faint', hex: '#7d7e74' }
40
+ ]
41
+ },
42
+ {
43
+ group: 'Rules',
44
+ swatches: [
45
+ { token: '--rule', hex: '#20211a' },
46
+ { token: '--rule-soft', hex: '#b9bab2', border: true }
47
+ ]
48
+ },
49
+ {
50
+ group: 'Semantic',
51
+ swatches: [
52
+ { token: '--warn-bg', hex: '#e4d98f', border: true },
53
+ { token: '--warn-line', hex: '#9c8729' },
54
+ { token: '--warn-ink', hex: '#463b08' },
55
+ { token: '--ok-bg', hex: '#8fbf6e' },
56
+ { token: '--ok-line', hex: '#4c7a2e' },
57
+ { token: '--ok-ink', hex: '#1c3110' },
58
+ { token: '--danger', hex: '#b23a2e' },
59
+ { token: '--danger-soft', hex: '#e3c6c1', border: true },
60
+ { token: '--info', hex: '#388bfd' }
61
+ ]
62
+ },
63
+ {
64
+ group: 'Attention',
65
+ swatches: [
66
+ { token: '--attn-done', hex: '#22c55e' },
67
+ { token: '--attn-waiting', hex: '#f59e0b' }
68
+ ]
69
+ }
70
+ ];
71
+
72
+ let avatarScale = $state(8);
73
+ let avatarName = $state('demo-instance');
74
+ let avatarId = $state('alpha');
75
+ // '' means "pick by id"; otherwise the value is an avatar name from the catalog.
76
+ let avatarArtName = $state('');
77
+ const avatarArt = $derived(avatars.find((a) => a.name === avatarArtName));
78
+
79
+ let btnVariant = $state<'default' | 'primary' | 'danger'>('primary');
80
+ let btnSize = $state<'sm' | 'md' | 'lg'>('md');
81
+ let btnDisabled = $state(false);
82
+ let btnIcon = $state(true);
83
+ let btnLabel = $state('New Instance');
84
+
85
+ let loaderSpeed = $state(1);
86
+ // The stalled state normally needs a 10s wait on a wedged health probe to appear;
87
+ // `stalledAfterMs: 0` renders it straight away so it's inspectable here.
88
+ let loaderStalled = $state(false);
89
+
90
+ // Seed from the server's real preflight so the menu mirrors `/`; the presets
91
+ // and checkboxes below then let you exercise the other aggregate states.
92
+ // svelte-ignore state_referenced_locally
93
+ let providers = $state<AuthProvider[]>($state.snapshot(preflight.auth));
94
+ function presetCred(state: 'ok' | 'warn' | 'error') {
95
+ providers.forEach((p, i) => {
96
+ // warn = mixed: first provider on, the rest off.
97
+ p.available = state === 'ok' || (state === 'warn' && i === 0);
98
+ });
99
+ }
100
+
101
+ let healthLoading = $state(false);
102
+ let healthChecks = $state({
103
+ containerRunning: true,
104
+ codeServerAccessible: true,
105
+ claudeCode: true,
106
+ github: false,
107
+ attentionHooks: true
108
+ });
109
+ // Stamp a fetch time so the "updated Ns ago" readout ticks; "refresh" resets it.
110
+ let healthFetchedAt = $state(Date.now());
111
+ const demoHealth = $derived<InstanceHealth | null>(
112
+ healthLoading
113
+ ? null
114
+ : {
115
+ containerRunning: healthChecks.containerRunning,
116
+ codeServerAccessible: healthChecks.codeServerAccessible,
117
+ injections: [
118
+ { id: 'claude-code-credentials', label: 'Claude Code', ok: healthChecks.claudeCode },
119
+ { id: 'github-credentials', label: 'GitHub CLI', ok: healthChecks.github },
120
+ { id: 'attention-hooks', label: 'Claude hooks', ok: healthChecks.attentionHooks }
121
+ ],
122
+ openPorts: [],
123
+ checkedAt: healthFetchedAt
124
+ }
125
+ );
126
+
127
+ // `openCount` of the `portCount` ports render as published (filled dot); the rest
128
+ // show hollow. Count 0 exercises the empty state (renders nothing).
129
+ let portCount = $state(3);
130
+ let openCount = $state(2);
131
+ const demoPorts = $derived<PortForward[]>(
132
+ Array.from({ length: portCount }, (_, i) => ({
133
+ container_port: 3000 + i,
134
+ host_port: 8001 + i,
135
+ open: i < openCount
136
+ }))
137
+ );
138
+
139
+ let branchName = $state('main');
140
+
141
+ const statuses: Instance['status'][] = ['creating', 'running', 'stopped', 'error'];
142
+ let badgeStatus = $state<Instance['status']>('error');
143
+ </script>
144
+
145
+ <Toaster />
146
+
147
+ <TopBar
148
+ auth={providers}
149
+ canDelete={true}
150
+ ready={true}
151
+ creating={false}
152
+ onNew={() => toast('New instance (demo)')}
153
+ onDeleteAll={() => toast('Delete all (demo)')}
154
+ />
155
+
156
+ <main class="showcase">
157
+ <header>
158
+ <h1>UI</h1>
159
+ <p>Component showcase · dev preview · tweak each component’s props below</p>
160
+ <a class="gallery-link" href="/avatars">→ Avatar sprite gallery</a>
161
+ </header>
162
+
163
+ <div class="grid">
164
+ <details class="palette-card panel" open>
165
+ <summary>
166
+ <span class="palette-title">Color palette</span>
167
+ <span class="palette-hint">Design tokens — tap to expand</span>
168
+ </summary>
169
+ <div class="palette">
170
+ {#each palette as { group, swatches } (group)}
171
+ <div class="swatch-group">
172
+ <h3>{group}</h3>
173
+ <div class="swatches">
174
+ {#each swatches as { token, hex, border } (token)}
175
+ <figure class="swatch">
176
+ <div class="chip" class:bordered={border} style="background: var({token});"></div>
177
+ <figcaption>
178
+ <code>{token}</code>
179
+ <span>{hex}</span>
180
+ </figcaption>
181
+ </figure>
182
+ {/each}
183
+ </div>
184
+ </div>
185
+ {/each}
186
+ </div>
187
+ </details>
188
+
189
+ <ComponentDemo title="Avatar">
190
+ <Avatar id={avatarId} name={avatarName} scale={avatarScale} art={avatarArt} />
191
+ {#snippet controls()}
192
+ <label>
193
+ <span>scale ({avatarScale})</span>
194
+ <input type="range" min="1" max="12" bind:value={avatarScale} />
195
+ </label>
196
+ <label>
197
+ <span>name</span>
198
+ <input type="text" bind:value={avatarName} />
199
+ </label>
200
+ <label>
201
+ <span>id</span>
202
+ <input type="text" bind:value={avatarId} />
203
+ </label>
204
+ <label>
205
+ <span>art</span>
206
+ <select bind:value={avatarArtName}>
207
+ <option value="">(pick by id)</option>
208
+ {#each avatars as art (art.name)}
209
+ <option value={art.name}>{art.name}</option>
210
+ {/each}
211
+ </select>
212
+ </label>
213
+ {/snippet}
214
+ </ComponentDemo>
215
+
216
+ <ComponentDemo title="Button">
217
+ <Button
218
+ variant={btnVariant}
219
+ size={btnSize}
220
+ disabled={btnDisabled}
221
+ icon={btnIcon ? Plus : undefined}
222
+ >
223
+ {btnLabel}
224
+ </Button>
225
+ {#snippet controls()}
226
+ <label>
227
+ <span>variant</span>
228
+ <select bind:value={btnVariant}>
229
+ <option value="default">default</option>
230
+ <option value="primary">primary</option>
231
+ <option value="danger">danger</option>
232
+ </select>
233
+ </label>
234
+ <label>
235
+ <span>size</span>
236
+ <select bind:value={btnSize}>
237
+ <option value="sm">sm</option>
238
+ <option value="md">md</option>
239
+ <option value="lg">lg</option>
240
+ </select>
241
+ </label>
242
+ <label>
243
+ <span>label</span>
244
+ <input type="text" bind:value={btnLabel} />
245
+ </label>
246
+ <label class="inline">
247
+ <input type="checkbox" bind:checked={btnIcon} />
248
+ <span>icon (Plus)</span>
249
+ </label>
250
+ <label class="inline">
251
+ <input type="checkbox" bind:checked={btnDisabled} />
252
+ <span>disabled</span>
253
+ </label>
254
+ {/snippet}
255
+ </ComponentDemo>
256
+
257
+ <ComponentDemo title="CredMenu">
258
+ <CredMenu auth={providers} />
259
+ {#snippet controls()}
260
+ <div class="presets">
261
+ <button type="button" onclick={() => presetCred('ok')}>ok</button>
262
+ <button type="button" onclick={() => presetCred('warn')}>warn</button>
263
+ <button type="button" onclick={() => presetCred('error')}>error</button>
264
+ </div>
265
+ {#each providers as provider (provider.id)}
266
+ <label class="inline">
267
+ <input type="checkbox" bind:checked={provider.available} />
268
+ <span>{provider.label} available</span>
269
+ </label>
270
+ {/each}
271
+ {/snippet}
272
+ </ComponentDemo>
273
+
274
+ <ComponentDemo title="IdeLoader">
275
+ <div class="loader-stage">
276
+ {#key loaderStalled}
277
+ <IdeLoader
278
+ speed={loaderSpeed}
279
+ stalledAfterMs={loaderStalled ? 0 : undefined}
280
+ onoverride={loaderStalled ? () => {} : undefined}
281
+ />
282
+ {/key}
283
+ </div>
284
+ {#snippet controls()}
285
+ <label>
286
+ <span>speed ({loaderSpeed.toFixed(2)}×)</span>
287
+ <input type="range" min="0.25" max="3" step="0.25" bind:value={loaderSpeed} />
288
+ </label>
289
+ <label>
290
+ <input type="checkbox" bind:checked={loaderStalled} />
291
+ <span>stalled (probe never answered)</span>
292
+ </label>
293
+ {/snippet}
294
+ </ComponentDemo>
295
+
296
+ <ComponentDemo title="HealthBox">
297
+ <HealthBox
298
+ health={demoHealth}
299
+ lastFetchedAt={healthLoading ? null : healthFetchedAt}
300
+ injectionChecks={3}
301
+ />
302
+ {#snippet controls()}
303
+ <div class="presets">
304
+ <button type="button" onclick={() => (healthFetchedAt = Date.now())}>refresh now</button>
305
+ </div>
306
+ <label class="inline">
307
+ <input type="checkbox" bind:checked={healthLoading} />
308
+ <span>loading (skeleton)</span>
309
+ </label>
310
+ <label class="inline">
311
+ <input type="checkbox" bind:checked={healthChecks.containerRunning} />
312
+ <span>container running</span>
313
+ </label>
314
+ <label class="inline">
315
+ <input type="checkbox" bind:checked={healthChecks.codeServerAccessible} />
316
+ <span>code-server reachable</span>
317
+ </label>
318
+ <label class="inline">
319
+ <input type="checkbox" bind:checked={healthChecks.claudeCode} />
320
+ <span>Claude Code present</span>
321
+ </label>
322
+ <label class="inline">
323
+ <input type="checkbox" bind:checked={healthChecks.github} />
324
+ <span>GitHub CLI present</span>
325
+ </label>
326
+ <label class="inline">
327
+ <input type="checkbox" bind:checked={healthChecks.attentionHooks} />
328
+ <span>attention hooks present</span>
329
+ </label>
330
+ {/snippet}
331
+ </ComponentDemo>
332
+
333
+ <ComponentDemo title="PortsBox">
334
+ <PortsBox ports={demoPorts} />
335
+ {#snippet controls()}
336
+ <label>
337
+ <span>ports ({portCount})</span>
338
+ <input type="range" min="0" max="6" bind:value={portCount} />
339
+ </label>
340
+ <label>
341
+ <span>open ({openCount})</span>
342
+ <input type="range" min="0" max={portCount} bind:value={openCount} />
343
+ </label>
344
+ {/snippet}
345
+ </ComponentDemo>
346
+
347
+ <ComponentDemo title="BranchBox">
348
+ <BranchBox branch={branchName} />
349
+ {#snippet controls()}
350
+ <label>
351
+ <span>branch</span>
352
+ <input type="text" bind:value={branchName} />
353
+ </label>
354
+ {/snippet}
355
+ </ComponentDemo>
356
+
357
+ <ComponentDemo title="StatusBadge" note="All four states, plus one picked below.">
358
+ <div class="badge-row">
359
+ {#each statuses as s (s)}
360
+ <StatusBadge status={s} />
361
+ {/each}
362
+ </div>
363
+ <div class="badge-row">
364
+ <StatusBadge status={badgeStatus} />
365
+ </div>
366
+ {#snippet controls()}
367
+ <label>
368
+ <span>status</span>
369
+ <select bind:value={badgeStatus}>
370
+ {#each statuses as s (s)}
371
+ <option value={s}>{s}</option>
372
+ {/each}
373
+ </select>
374
+ </label>
375
+ {/snippet}
376
+ </ComponentDemo>
377
+
378
+ <ComponentDemo title="Brand" note="No props — static branding.">
379
+ <Brand />
380
+ </ComponentDemo>
381
+
382
+ <ComponentDemo title="SettingsCog" note="No props — links to /settings.">
383
+ <SettingsCog />
384
+ </ComponentDemo>
385
+ </div>
386
+ </main>
387
+
388
+ <style>
389
+ .showcase {
390
+ min-height: 100vh;
391
+ max-width: 740px;
392
+ margin: 0 auto;
393
+ padding: 32px;
394
+ background: var(--bg);
395
+ color: var(--ink);
396
+ }
397
+ header {
398
+ margin-bottom: 28px;
399
+ }
400
+ h1 {
401
+ margin: 0;
402
+ font-family: var(--font-display);
403
+ font-size: 24px;
404
+ letter-spacing: 0.04em;
405
+ }
406
+ header p {
407
+ margin: 4px 0 0;
408
+ font-family: var(--font-mono);
409
+ font-size: 12px;
410
+ color: var(--ink-soft);
411
+ }
412
+ .gallery-link {
413
+ display: inline-block;
414
+ margin-top: 10px;
415
+ font-family: var(--font-mono);
416
+ font-size: 12px;
417
+ color: var(--ink);
418
+ text-decoration: none;
419
+ border-bottom: 1px solid var(--ink);
420
+ }
421
+ .gallery-link:hover {
422
+ background: var(--ink);
423
+ color: var(--bg);
424
+ }
425
+ .grid {
426
+ display: flex;
427
+ flex-direction: column;
428
+ gap: 24px;
429
+ }
430
+
431
+ /* `.fields` lives in the child ComponentDemo, hence the global selector. */
432
+ .showcase :global(.fields label) {
433
+ display: flex;
434
+ flex-direction: column;
435
+ gap: 4px;
436
+ font-family: var(--font-mono);
437
+ font-size: 11px;
438
+ text-transform: uppercase;
439
+ letter-spacing: 0.06em;
440
+ color: var(--ink-soft);
441
+ }
442
+ .showcase :global(.fields label.inline) {
443
+ flex-direction: row;
444
+ align-items: center;
445
+ gap: 7px;
446
+ }
447
+ .showcase :global(.fields input[type='text']),
448
+ .showcase :global(.fields select) {
449
+ font-family: var(--font-mono);
450
+ font-size: 12px;
451
+ color: var(--ink);
452
+ background: var(--bg);
453
+ border: 1px solid var(--ink);
454
+ padding: 5px 7px;
455
+ }
456
+ .showcase :global(.fields input[type='range']),
457
+ .showcase :global(.fields input[type='checkbox']) {
458
+ accent-color: var(--ink);
459
+ }
460
+ /* IdeLoader is an inset:0 overlay, so give it a sized, positioned stage. */
461
+ .loader-stage {
462
+ position: relative;
463
+ width: 100%;
464
+ height: 180px;
465
+ border: 1px solid var(--rule-soft);
466
+ overflow: hidden;
467
+ }
468
+ .badge-row {
469
+ display: flex;
470
+ flex-wrap: wrap;
471
+ align-items: center;
472
+ gap: 10px;
473
+ }
474
+ .presets {
475
+ display: flex;
476
+ gap: 6px;
477
+ }
478
+ .presets button {
479
+ flex: 1;
480
+ font-family: var(--font-mono);
481
+ font-size: 11px;
482
+ text-transform: uppercase;
483
+ letter-spacing: 0.06em;
484
+ color: var(--ink);
485
+ background: var(--bg-card);
486
+ border: 1px solid var(--ink);
487
+ padding: 5px 0;
488
+ cursor: pointer;
489
+ }
490
+ .presets button:hover {
491
+ background: var(--ink);
492
+ color: var(--bg);
493
+ }
494
+
495
+ .palette-card {
496
+ background: var(--bg-card);
497
+ }
498
+ .palette-card > summary {
499
+ display: flex;
500
+ align-items: baseline;
501
+ justify-content: space-between;
502
+ gap: 12px;
503
+ padding: 10px 14px;
504
+ cursor: pointer;
505
+ user-select: none;
506
+ list-style: none;
507
+ }
508
+ .palette-card > summary::-webkit-details-marker {
509
+ display: none;
510
+ }
511
+ .palette-card[open] > summary {
512
+ border-bottom: 1px solid var(--rule-soft);
513
+ }
514
+ .palette-title {
515
+ font-family: var(--font-display);
516
+ font-size: 16px;
517
+ letter-spacing: 0.04em;
518
+ text-transform: uppercase;
519
+ }
520
+ .palette-hint {
521
+ font-family: var(--font-mono);
522
+ font-size: 11px;
523
+ text-transform: uppercase;
524
+ letter-spacing: 0.06em;
525
+ color: var(--ink-faint);
526
+ }
527
+ .palette-card[open] .palette-hint {
528
+ visibility: hidden;
529
+ }
530
+ .palette {
531
+ display: flex;
532
+ flex-direction: column;
533
+ gap: 18px;
534
+ width: 100%;
535
+ padding: 18px 14px;
536
+ }
537
+ .swatch-group h3 {
538
+ margin: 0 0 8px;
539
+ font-family: var(--font-mono);
540
+ font-size: 11px;
541
+ text-transform: uppercase;
542
+ letter-spacing: 0.08em;
543
+ color: var(--ink-soft);
544
+ }
545
+ .swatches {
546
+ display: grid;
547
+ grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
548
+ gap: 10px;
549
+ }
550
+ .swatch {
551
+ margin: 0;
552
+ display: flex;
553
+ flex-direction: column;
554
+ gap: 5px;
555
+ }
556
+ .swatch .chip {
557
+ height: 44px;
558
+ border: 1px solid transparent;
559
+ }
560
+ .swatch .chip.bordered {
561
+ border-color: var(--rule-soft);
562
+ }
563
+ .swatch figcaption {
564
+ display: flex;
565
+ flex-direction: column;
566
+ gap: 1px;
567
+ font-family: var(--font-mono);
568
+ font-size: 11px;
569
+ line-height: 1.35;
570
+ }
571
+ .swatch figcaption code {
572
+ color: var(--ink);
573
+ }
574
+ .swatch figcaption span {
575
+ color: var(--ink-faint);
576
+ text-transform: uppercase;
577
+ }
578
+ </style>
@@ -0,0 +1,85 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+
4
+ // `controls` is optional — propless components show a note instead.
5
+ let {
6
+ title,
7
+ note,
8
+ children,
9
+ controls
10
+ }: {
11
+ title: string;
12
+ note?: string;
13
+ children: Snippet;
14
+ controls?: Snippet;
15
+ } = $props();
16
+ </script>
17
+
18
+ <section class="demo panel">
19
+ <h2>{title}</h2>
20
+ <div class="preview">
21
+ {@render children()}
22
+ </div>
23
+ {#if controls}
24
+ <details class="controls">
25
+ <summary>Props</summary>
26
+ <div class="fields">
27
+ {@render controls()}
28
+ </div>
29
+ </details>
30
+ {:else}
31
+ <p class="note">{note ?? 'No props.'}</p>
32
+ {/if}
33
+ </section>
34
+
35
+ <style>
36
+ .demo {
37
+ background: var(--bg-card);
38
+ }
39
+ h2 {
40
+ margin: 0;
41
+ padding: 10px 14px;
42
+ border-bottom: 1px solid var(--rule-soft);
43
+ font-family: var(--font-display);
44
+ font-size: 16px;
45
+ letter-spacing: 0.04em;
46
+ text-transform: uppercase;
47
+ }
48
+ .preview {
49
+ display: flex;
50
+ flex-wrap: wrap;
51
+ align-items: center;
52
+ gap: 16px;
53
+ min-height: 96px;
54
+ padding: 24px;
55
+ /* Faint pixel grid so light components read against the panel. */
56
+ background-image: radial-gradient(var(--rule-soft) 0.5px, transparent 0.5px);
57
+ background-size: 8px 8px;
58
+ }
59
+ .controls {
60
+ border-top: 1px solid var(--rule-soft);
61
+ }
62
+ summary {
63
+ padding: 8px 14px;
64
+ font-family: var(--font-mono);
65
+ font-size: 11px;
66
+ text-transform: uppercase;
67
+ letter-spacing: 0.08em;
68
+ color: var(--ink-soft);
69
+ cursor: pointer;
70
+ user-select: none;
71
+ }
72
+ .fields {
73
+ display: flex;
74
+ flex-direction: column;
75
+ gap: 10px;
76
+ padding: 4px 14px 16px;
77
+ }
78
+ .note {
79
+ margin: 0;
80
+ padding: 10px 14px 14px;
81
+ font-family: var(--font-mono);
82
+ font-size: 11px;
83
+ color: var(--ink-faint);
84
+ }
85
+ </style>