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,321 @@
1
+ <script lang="ts">
2
+ import Copy from '@lucide/svelte/icons/copy';
3
+ import Eraser from '@lucide/svelte/icons/eraser';
4
+ import Contrast from '@lucide/svelte/icons/contrast';
5
+ import ExternalLink from '@lucide/svelte/icons/external-link';
6
+ import toast from 'svelte-french-toast';
7
+ import { ROWS, COLS, ON, OFF, type AvatarArt } from '../avatars/types.ts';
8
+ import {
9
+ cellsToPixels,
10
+ normalizeName,
11
+ toModuleSource,
12
+ toIssueUrl
13
+ } from '../avatars/serialize.ts';
14
+ import Avatar from './Avatar.svelte';
15
+ import Button from './Button.svelte';
16
+
17
+ // Picked randomly so the hint varies each time the page loads.
18
+ const NAME_PROMPTS = [
19
+ 'dragon',
20
+ 'unicorn',
21
+ 'cactus',
22
+ 'comet',
23
+ 'pretzel',
24
+ 'volcano',
25
+ 'narwhal',
26
+ 'waffle',
27
+ 'satellite',
28
+ 'pineapple',
29
+ 'kraken',
30
+ 'yeti',
31
+ 'moose',
32
+ 'lighthouse',
33
+ 'toaster',
34
+ 'hedgehog',
35
+ 'walrus',
36
+ 'pumpkin',
37
+ 'jellyfish',
38
+ 'tornado',
39
+ 'igloo',
40
+ 'boomerang'
41
+ ];
42
+ const namePlaceholder = NAME_PROMPTS[Math.floor(Math.random() * NAME_PROMPTS.length)];
43
+
44
+ // Same shape `decode()` produces, so the two round-trip.
45
+ let cells = $state<number[]>(Array(ROWS * COLS).fill(OFF));
46
+ let avatarName = $state('');
47
+
48
+ const slug = $derived(normalizeName(avatarName));
49
+ const art = $derived<AvatarArt>({ name: slug || 'my-avatar', pixels: cellsToPixels(cells) });
50
+ // Contributing needs a name and at least one lit pixel — a blank sprite is no sprite.
51
+ const canContribute = $derived(slug.length > 0 && cells.some((c) => c === ON));
52
+
53
+ // Capturing the pointer on the grid is what lets one stroke paint every cell it crosses.
54
+ let gridEl = $state<HTMLDivElement | null>(null);
55
+ let painting = false;
56
+ let paintValue: number = ON;
57
+
58
+ // Mirrors Avatar.svelte's bezel so the user sees the real framing while drawing.
59
+ const RING_CELLS = Array.from({ length: 100 }, (_, i) => ({
60
+ r: Math.floor(i / 10),
61
+ c: i % 10
62
+ })).filter(({ r, c }) => r === 0 || r === 9 || c === 0 || c === 9);
63
+
64
+ function cellAt(e: PointerEvent): number | null {
65
+ if (!gridEl) return null;
66
+ const rect = gridEl.getBoundingClientRect();
67
+ const c = Math.floor(((e.clientX - rect.left) / rect.width) * COLS);
68
+ const r = Math.floor(((e.clientY - rect.top) / rect.height) * ROWS);
69
+ if (r < 0 || r >= ROWS || c < 0 || c >= COLS) return null;
70
+ return r * COLS + c;
71
+ }
72
+
73
+ function onpointerdown(e: PointerEvent) {
74
+ const i = cellAt(e);
75
+ if (i == null) return;
76
+ (e.currentTarget as HTMLElement).setPointerCapture?.(e.pointerId);
77
+ painting = true;
78
+ paintValue = cells[i] === ON ? OFF : ON;
79
+ cells[i] = paintValue;
80
+ }
81
+
82
+ function onpointermove(e: PointerEvent) {
83
+ if (!painting) return;
84
+ const i = cellAt(e);
85
+ if (i != null) cells[i] = paintValue;
86
+ }
87
+
88
+ function stopPainting() {
89
+ painting = false;
90
+ }
91
+
92
+ function clear() {
93
+ cells = Array(ROWS * COLS).fill(OFF);
94
+ }
95
+
96
+ function invert() {
97
+ cells = cells.map((c) => (c === ON ? OFF : ON));
98
+ }
99
+
100
+ async function copyModule() {
101
+ try {
102
+ await navigator.clipboard.writeText(toModuleSource(art));
103
+ toast('Copied — paste it into the GitHub issue');
104
+ } catch (err) {
105
+ toast.error(`Copy failed: ${(err as Error).message}`);
106
+ }
107
+ }
108
+ </script>
109
+
110
+ <section class="editor">
111
+ <div class="head">
112
+ <h2>Draw your avatar</h2>
113
+ </div>
114
+
115
+ <div class="body">
116
+ <div class="grid-frame">
117
+ {#each RING_CELLS as { r, c }, i (i)}
118
+ <span class="ring-cell" style="grid-row:{r + 1};grid-column:{c + 1}" aria-hidden="true"
119
+ ></span>
120
+ {/each}
121
+ <div
122
+ class="grid"
123
+ bind:this={gridEl}
124
+ role="img"
125
+ aria-label="8 by 8 pixel drawing canvas"
126
+ {onpointerdown}
127
+ {onpointermove}
128
+ onpointerup={stopPainting}
129
+ onpointercancel={stopPainting}
130
+ onlostpointercapture={stopPainting}
131
+ >
132
+ {#each cells as cell, i (i)}
133
+ <span class="cell" class:on={cell === ON}></span>
134
+ {/each}
135
+ </div>
136
+ </div>
137
+
138
+ <div class="side">
139
+ <div class="preview">
140
+ <div class="preview-label">Preview</div>
141
+ <div class="preview-row">
142
+ <Avatar {art} name={art.name} scale={6} />
143
+ <Avatar {art} name={art.name} scale={3} />
144
+ </div>
145
+ </div>
146
+
147
+ <label class="name-field">
148
+ <span class="preview-label">Name</span>
149
+ <input
150
+ type="text"
151
+ bind:value={avatarName}
152
+ placeholder={namePlaceholder}
153
+ spellcheck="false"
154
+ autocapitalize="off"
155
+ autocorrect="off"
156
+ autocomplete="off"
157
+ maxlength="24"
158
+ />
159
+ </label>
160
+
161
+ <div class="tools">
162
+ <Button size="sm" icon={Eraser} onclick={clear}>Clear</Button>
163
+ <Button size="sm" icon={Contrast} onclick={invert}>Invert</Button>
164
+ </div>
165
+ </div>
166
+ </div>
167
+
168
+ <div class="foot">
169
+ <span class="hint">Draw something, then open an issue to get it into the official set.</span>
170
+ <div class="actions">
171
+ <Button size="sm" icon={Copy} disabled={!canContribute} onclick={copyModule}>Copy</Button>
172
+ <Button
173
+ size="sm"
174
+ variant="primary"
175
+ icon={ExternalLink}
176
+ disabled={!canContribute}
177
+ href={toIssueUrl(art)}
178
+ target="_blank"
179
+ rel="noopener"
180
+ >
181
+ Open GitHub issue
182
+ </Button>
183
+ </div>
184
+ </div>
185
+ </section>
186
+
187
+ <style>
188
+ .editor {
189
+ width: 100%;
190
+ display: flex;
191
+ flex-direction: column;
192
+ background: var(--bg-card);
193
+ border: 1px solid var(--rule);
194
+ overflow: hidden;
195
+ }
196
+ .head {
197
+ display: flex;
198
+ align-items: center;
199
+ padding: 14px 18px;
200
+ border-bottom: 1px solid var(--ink);
201
+ background: var(--ink);
202
+ }
203
+ .head h2 {
204
+ margin: 0;
205
+ font-family: var(--font-display);
206
+ font-weight: 700;
207
+ font-size: 17px;
208
+ text-transform: uppercase;
209
+ letter-spacing: 0.06em;
210
+ color: var(--bg);
211
+ }
212
+ .body {
213
+ display: flex;
214
+ flex-wrap: wrap;
215
+ gap: 18px;
216
+ padding: 18px;
217
+ }
218
+ .grid-frame {
219
+ display: grid;
220
+ grid-template-columns: repeat(10, 40px);
221
+ grid-template-rows: repeat(10, 40px);
222
+ width: 400px;
223
+ height: 400px;
224
+ flex: none;
225
+ border: 1px solid var(--ink);
226
+ background: var(--bg);
227
+ }
228
+ .ring-cell {
229
+ box-sizing: border-box;
230
+ padding: 0 1px 1px 0;
231
+ background-clip: content-box;
232
+ background-color: var(--rule-soft);
233
+ opacity: 0.4; /* more muted than the paintable off cells — not part of the art */
234
+ }
235
+ /* A stroke paints by pointer position, so cells need no individual handlers. */
236
+ .grid {
237
+ display: grid;
238
+ grid-template-columns: repeat(8, 1fr);
239
+ grid-template-rows: repeat(8, 1fr);
240
+ grid-column: 2 / 10;
241
+ grid-row: 2 / 10;
242
+ cursor: crosshair;
243
+ touch-action: none; /* drawing on touch shouldn't scroll the page */
244
+ }
245
+ .cell {
246
+ box-sizing: border-box;
247
+ padding: 0 1px 1px 0;
248
+ background-clip: content-box;
249
+ background-color: var(--rule-soft); /* unlit LED — faint */
250
+ }
251
+ .cell.on {
252
+ background-color: var(--ink);
253
+ }
254
+ .side {
255
+ flex: 1;
256
+ min-width: 160px;
257
+ display: flex;
258
+ flex-direction: column;
259
+ gap: 16px;
260
+ }
261
+ .preview-label {
262
+ font-size: 11px;
263
+ text-transform: uppercase;
264
+ letter-spacing: 0.06em;
265
+ color: var(--ink-faint);
266
+ font-family: var(--font-mono);
267
+ }
268
+ .preview-row {
269
+ display: flex;
270
+ align-items: flex-end;
271
+ gap: 10px;
272
+ margin-top: 6px;
273
+ }
274
+ .name-field {
275
+ display: flex;
276
+ flex-direction: column;
277
+ gap: 6px;
278
+ }
279
+ .name-field input {
280
+ font-family: var(--font-mono);
281
+ font-size: 13px;
282
+ padding: 8px 12px;
283
+ border: 1px solid var(--ink);
284
+ background: var(--bg);
285
+ color: var(--ink);
286
+ }
287
+ .name-field input::placeholder {
288
+ color: var(--ink-faint);
289
+ }
290
+ .name-field input:focus {
291
+ outline: none;
292
+ box-shadow: inset 3px 3px 0 var(--rule-soft);
293
+ }
294
+ .tools {
295
+ display: flex;
296
+ gap: 8px;
297
+ }
298
+ .foot {
299
+ display: flex;
300
+ align-items: center;
301
+ justify-content: space-between;
302
+ gap: 12px;
303
+ flex-wrap: wrap;
304
+ padding: 14px 18px;
305
+ border-top: 1px solid var(--ink);
306
+ }
307
+ .hint {
308
+ font-family: var(--font-mono);
309
+ font-size: 12px;
310
+ color: var(--ink-faint);
311
+ flex: 1;
312
+ min-width: 180px;
313
+ }
314
+ .actions {
315
+ display: flex;
316
+ gap: 8px;
317
+ }
318
+ .actions :global(.btn) {
319
+ white-space: nowrap;
320
+ }
321
+ </style>
@@ -0,0 +1,113 @@
1
+ <script lang="ts">
2
+ import { Toaster } from 'svelte-french-toast';
3
+ import AppBar from './AppBar.svelte';
4
+ import Avatar from './Avatar.svelte';
5
+ import AvatarEditor from './AvatarEditor.svelte';
6
+ import { avatars } from '../avatars/index.ts';
7
+ </script>
8
+
9
+ <div class="page">
10
+ <AppBar>
11
+ <span class="title">Avatars</span>
12
+ </AppBar>
13
+
14
+ <main class="content">
15
+ <AvatarEditor />
16
+
17
+ <section class="catalog">
18
+ <header>
19
+ <h2>The set</h2>
20
+ <p>{avatars.length} hand-crafted 8×8 sprites</p>
21
+ </header>
22
+ <ul>
23
+ {#each avatars as art (art.name)}
24
+ <li>
25
+ <Avatar {art} name={art.name} scale={8} interactive />
26
+ <span class="label">{art.name}</span>
27
+ </li>
28
+ {/each}
29
+ </ul>
30
+ </section>
31
+ </main>
32
+ </div>
33
+
34
+ <!-- This page renders outside AppShell, which hosts the app-wide Toaster. -->
35
+ <Toaster
36
+ toastOptions={{
37
+ style:
38
+ 'border:1px solid var(--ink); background:var(--bg-card); color:var(--ink); box-shadow:4px 4px 0 var(--ink); font-family:var(--font-mono); font-size:13px;'
39
+ }}
40
+ />
41
+
42
+ <style>
43
+ .page {
44
+ display: flex;
45
+ flex-direction: column;
46
+ min-height: 100vh;
47
+ }
48
+ .title {
49
+ display: inline-flex;
50
+ align-items: center;
51
+ padding: 0 14px;
52
+ font-family: var(--font-mono);
53
+ font-size: 12px;
54
+ text-transform: uppercase;
55
+ letter-spacing: 0.08em;
56
+ color: var(--ink-soft);
57
+ }
58
+ .content {
59
+ flex: 1;
60
+ display: flex;
61
+ flex-direction: column;
62
+ align-items: center;
63
+ gap: 24px;
64
+ padding: 32px 20px;
65
+ }
66
+ .content > :global(*) {
67
+ width: 100%;
68
+ max-width: 720px;
69
+ }
70
+ .catalog {
71
+ background: var(--bg-card);
72
+ border: 1px solid var(--rule);
73
+ padding: 18px;
74
+ }
75
+ .catalog header {
76
+ margin-bottom: 20px;
77
+ }
78
+ .catalog h2 {
79
+ margin: 0;
80
+ font-family: var(--font-display);
81
+ font-weight: 700;
82
+ font-size: 17px;
83
+ text-transform: uppercase;
84
+ letter-spacing: 0.06em;
85
+ }
86
+ .catalog header p {
87
+ margin: 4px 0 0;
88
+ font-family: var(--font-mono);
89
+ font-size: 12px;
90
+ color: var(--ink-soft);
91
+ }
92
+ ul {
93
+ list-style: none;
94
+ margin: 0;
95
+ padding: 0;
96
+ display: grid;
97
+ grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
98
+ gap: 20px;
99
+ }
100
+ li {
101
+ display: flex;
102
+ flex-direction: column;
103
+ align-items: center;
104
+ gap: 8px;
105
+ }
106
+ .label {
107
+ font-family: var(--font-mono);
108
+ font-size: 11px;
109
+ text-transform: uppercase;
110
+ letter-spacing: 0.08em;
111
+ color: var(--ink-soft);
112
+ }
113
+ </style>
@@ -0,0 +1,32 @@
1
+ <script lang="ts">
2
+ import GitBranch from '@lucide/svelte/icons/git-branch';
3
+
4
+ let { branch }: { branch: string } = $props();
5
+ </script>
6
+
7
+ <div class="branch" title="Branch checked out in the container">
8
+ <GitBranch size={12} /><span>{branch}</span>
9
+ </div>
10
+
11
+ <style>
12
+ .branch {
13
+ margin-top: 6px;
14
+ display: inline-flex;
15
+ align-items: center;
16
+ gap: 5px;
17
+ max-width: 100%;
18
+ font-family: var(--font-mono);
19
+ font-size: 12px;
20
+ color: var(--ink);
21
+ border: 1px solid var(--ink-faint);
22
+ padding: 2px 7px;
23
+ }
24
+ .branch :global(svg) {
25
+ flex: none;
26
+ }
27
+ .branch span {
28
+ overflow: hidden;
29
+ text-overflow: ellipsis;
30
+ white-space: nowrap;
31
+ }
32
+ </style>
@@ -0,0 +1,41 @@
1
+ <script lang="ts">
2
+ import Package from '@lucide/svelte/icons/package';
3
+ </script>
4
+
5
+ <a class="brand" href="/"
6
+ ><span class="logo"><Package size={22} /></span><span>Codebay</span><span class="tagline"
7
+ >devcontainer manager</span
8
+ ></a
9
+ >
10
+
11
+ <style>
12
+ .brand {
13
+ display: inline-flex;
14
+ align-items: baseline;
15
+ gap: 11px;
16
+ font-family: var(--font-display);
17
+ font-weight: 700;
18
+ font-size: 22px;
19
+ text-transform: uppercase;
20
+ letter-spacing: 0.04em;
21
+ color: inherit;
22
+ text-decoration: none;
23
+ }
24
+ .logo {
25
+ display: inline-flex;
26
+ align-items: center;
27
+ align-self: center;
28
+ }
29
+ .tagline {
30
+ font-family: var(--font-sans);
31
+ font-size: 12px;
32
+ font-weight: 500;
33
+ letter-spacing: 0.08em;
34
+ color: var(--ink-soft);
35
+ }
36
+ @media (max-width: 640px) {
37
+ .tagline {
38
+ display: none;
39
+ }
40
+ }
41
+ </style>
@@ -0,0 +1,161 @@
1
+ <script lang="ts">
2
+ import type { Component, Snippet } from 'svelte';
3
+
4
+ type Variant = 'default' | 'primary' | 'danger';
5
+ type Size = 'sm' | 'md' | 'lg';
6
+
7
+ let {
8
+ variant = 'default',
9
+ size = 'md',
10
+ ghost = false,
11
+ icon,
12
+ iconSize,
13
+ href,
14
+ disabled = false,
15
+ onclick,
16
+ children,
17
+ ...rest
18
+ }: {
19
+ variant?: Variant;
20
+ size?: Size;
21
+ /** Dashed-outline treatment that fills on hover — used for secondary links. */
22
+ ghost?: boolean;
23
+ /** Optional leading icon component, e.g. a lucide icon like `Plus`. */
24
+ icon?: Component<{ size?: number }>;
25
+ /** Override the icon size; defaults to a size that scales with `size`. */
26
+ iconSize?: number;
27
+ /** When set, the button renders as an `<a>` link instead of a `<button>`. */
28
+ href?: string;
29
+ disabled?: boolean;
30
+ onclick?: (e: MouseEvent) => void;
31
+ children?: Snippet;
32
+ [key: string]: unknown;
33
+ } = $props();
34
+
35
+ const Icon = $derived(icon);
36
+ const resolvedIconSize = $derived(iconSize ?? (size === 'lg' ? 18 : size === 'sm' ? 13 : 15));
37
+ </script>
38
+
39
+ {#if href}
40
+ <a
41
+ class="btn {variant} {size}"
42
+ class:ghost
43
+ href={disabled ? undefined : href}
44
+ aria-disabled={disabled || undefined}
45
+ tabindex={disabled ? -1 : undefined}
46
+ {...rest}
47
+ >
48
+ {#if Icon}<Icon size={resolvedIconSize} />{/if}
49
+ {@render children?.()}
50
+ </a>
51
+ {:else}
52
+ <button class="btn {variant} {size}" class:ghost {onclick} {disabled} {...rest}>
53
+ {#if Icon}<Icon size={resolvedIconSize} />{/if}
54
+ {@render children?.()}
55
+ </button>
56
+ {/if}
57
+
58
+ <style>
59
+ .btn {
60
+ display: inline-flex;
61
+ align-items: center;
62
+ justify-content: center;
63
+ gap: 7px;
64
+ font-family: var(--font-mono);
65
+ font-weight: 600;
66
+ text-transform: uppercase;
67
+ letter-spacing: 0.06em;
68
+ border: 1px solid var(--ink);
69
+ background: var(--bg-card);
70
+ color: var(--ink);
71
+ cursor: pointer;
72
+ text-decoration: none;
73
+ }
74
+ .btn:disabled {
75
+ opacity: 0.4;
76
+ cursor: not-allowed;
77
+ }
78
+ /* `disabled` has no native effect on an <a> — the href branch above drops the
79
+ href entirely when disabled, but it still needs the same visual treatment
80
+ and to be excluded from the tab order / assistive-tech actions. */
81
+ .btn[aria-disabled='true'] {
82
+ opacity: 0.4;
83
+ cursor: not-allowed;
84
+ pointer-events: none;
85
+ }
86
+
87
+ .btn.sm {
88
+ padding: 7px 12px;
89
+ font-size: 12px;
90
+ }
91
+ .btn.md {
92
+ padding: 9px 16px;
93
+ font-size: 13px;
94
+ }
95
+ .btn.lg {
96
+ padding: 12px 22px;
97
+ font-size: 15px;
98
+ }
99
+
100
+ .btn.default:hover:not(:disabled) {
101
+ background: var(--ink);
102
+ color: var(--bg);
103
+ }
104
+
105
+ .btn.primary {
106
+ font-weight: 700;
107
+ background: var(--ink);
108
+ color: var(--bg);
109
+ }
110
+ .btn.primary:hover:not(:disabled) {
111
+ background: var(--bg-card);
112
+ color: var(--ink);
113
+ }
114
+
115
+ .btn.ghost {
116
+ background: transparent;
117
+ border-style: dashed;
118
+ }
119
+ .btn.ghost:hover:not(:disabled) {
120
+ background: var(--ink);
121
+ color: var(--bg);
122
+ border-style: solid;
123
+ }
124
+
125
+ .btn.danger {
126
+ color: var(--danger);
127
+ border-color: var(--danger);
128
+ }
129
+ .btn.danger:hover:not(:disabled) {
130
+ color: var(--danger);
131
+ background-color: var(--danger-soft);
132
+ border-color: transparent;
133
+ /* Slowly marching red "ants" border, drawn as four animated edge gradients. */
134
+ background-image:
135
+ linear-gradient(90deg, var(--danger) 50%, transparent 0),
136
+ linear-gradient(90deg, var(--danger) 50%, transparent 0),
137
+ linear-gradient(0deg, var(--danger) 50%, transparent 0),
138
+ linear-gradient(0deg, var(--danger) 50%, transparent 0);
139
+ background-repeat: repeat-x, repeat-x, repeat-y, repeat-y;
140
+ background-size:
141
+ 14px 2px,
142
+ 14px 2px,
143
+ 2px 14px,
144
+ 2px 14px;
145
+ background-position:
146
+ 0 0,
147
+ 0 100%,
148
+ 0 0,
149
+ 100% 0;
150
+ animation: marching-ants 2s linear infinite;
151
+ }
152
+ @keyframes marching-ants {
153
+ to {
154
+ background-position:
155
+ 28px 0,
156
+ -28px 100%,
157
+ 0 -28px,
158
+ 100% 28px;
159
+ }
160
+ }
161
+ </style>