@thomasfarineau/anvil 0.0.2 → 0.0.4

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 (45) hide show
  1. package/README.md +21 -191
  2. package/dist/cli.cjs +557 -0
  3. package/package.json +26 -10
  4. package/src/client/config.schema.json +58 -6
  5. package/src/client/index.d.ts +60 -1
  6. package/src/rust/src/lib.rs +633 -21
  7. package/src/template/_gitignore +1 -0
  8. package/src/template/capabilities/default.json +7 -1
  9. package/src/template/config.json +2 -2
  10. package/src/template/frontends/react-js/src/App.jsx +183 -0
  11. package/src/template/frontends/react-js/src/index.html +12 -0
  12. package/src/template/frontends/react-js/src/main.jsx +5 -0
  13. package/src/template/frontends/react-js/vite.config.js +10 -0
  14. package/src/template/frontends/react-ts/src/App.tsx +190 -0
  15. package/src/template/frontends/react-ts/src/index.html +12 -0
  16. package/src/template/frontends/react-ts/src/main.tsx +5 -0
  17. package/src/template/frontends/react-ts/tsconfig.json +14 -0
  18. package/src/template/frontends/react-ts/vite.config.ts +10 -0
  19. package/src/template/frontends/solid-js/src/App.jsx +190 -0
  20. package/src/template/frontends/solid-js/src/index.html +12 -0
  21. package/src/template/frontends/solid-js/src/main.jsx +5 -0
  22. package/src/template/frontends/solid-js/vite.config.js +10 -0
  23. package/src/template/frontends/solid-ts/src/App.tsx +193 -0
  24. package/src/template/frontends/solid-ts/src/index.html +12 -0
  25. package/src/template/frontends/solid-ts/src/main.tsx +5 -0
  26. package/src/template/frontends/solid-ts/tsconfig.json +15 -0
  27. package/src/template/frontends/solid-ts/vite.config.ts +10 -0
  28. package/src/template/{src → frontends/vanilla-js/src}/index.html +110 -178
  29. package/src/template/frontends/vanilla-ts/src/index.html +51 -0
  30. package/src/template/frontends/vanilla-ts/src/main.ts +193 -0
  31. package/src/template/frontends/vanilla-ts/tsconfig.json +13 -0
  32. package/src/template/frontends/vanilla-ts/vite.config.ts +8 -0
  33. package/src/template/frontends/vue-js/src/App.vue +155 -0
  34. package/src/template/frontends/vue-js/src/index.html +12 -0
  35. package/src/template/frontends/vue-js/src/main.js +5 -0
  36. package/src/template/frontends/vue-js/vite.config.js +10 -0
  37. package/src/template/frontends/vue-ts/src/App.vue +158 -0
  38. package/src/template/frontends/vue-ts/src/index.html +12 -0
  39. package/src/template/frontends/vue-ts/src/main.ts +5 -0
  40. package/src/template/frontends/vue-ts/tsconfig.json +13 -0
  41. package/src/template/frontends/vue-ts/vite.config.ts +10 -0
  42. package/src/template/{src → shared}/api.js +38 -1
  43. package/src/template/shared/logo.svg +6 -0
  44. package/src/template/shared/style.css +226 -0
  45. package/src/cli.cjs +0 -352
@@ -0,0 +1,5 @@
1
+ import { render } from 'solid-js/web';
2
+ import App from './App.jsx';
3
+ import './style.css';
4
+
5
+ render(() => <App />, document.getElementById('root'));
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from 'vite';
2
+ import solid from 'vite-plugin-solid';
3
+
4
+ export default defineConfig({
5
+ root: 'src',
6
+ plugins: [solid()],
7
+ clearScreen: false,
8
+ server: { port: 5173, strictPort: true },
9
+ build: { outDir: '../dist', emptyOutDir: true, target: 'es2022' },
10
+ });
@@ -0,0 +1,193 @@
1
+ import { For, Show, createSignal, onMount } from 'solid-js';
2
+ import { MC } from './api.js';
3
+ import type { InitStatus, LauncherConfig } from './api.js';
4
+
5
+ export default function App() {
6
+ const [config, setConfig] = createSignal<LauncherConfig | null>(null);
7
+ const [status, setStatus] = createSignal<InitStatus | null>(null);
8
+ const [view, setView] = createSignal<'loading' | 'setup' | 'main'>('loading');
9
+ const [username, setUsername] = createSignal('');
10
+ const [dir, setDir] = createSignal('');
11
+ const [progress, setProgress] = createSignal<
12
+ Record<string, { pct: number; label: string; error: boolean }>
13
+ >({});
14
+ const [installing, setInstalling] = createSignal(false);
15
+ const [running, setRunning] = createSignal<string[]>([]);
16
+ const [message, setMessage] = createSignal('');
17
+ const [error, setError] = createSignal('');
18
+
19
+ onMount(async () => {
20
+ const [cfg, settings, init] = await Promise.all([
21
+ MC.getConfig(),
22
+ MC.getSettings(),
23
+ MC.getInitStatus(),
24
+ ]);
25
+ document.title = cfg.app_name;
26
+ setConfig(cfg);
27
+ setStatus(init);
28
+ setUsername(settings.username ?? '');
29
+ setDir(init.launcher_dir);
30
+ setView(
31
+ init.java_ok && init.instances.every((i) => i.installed)
32
+ ? 'main'
33
+ : 'setup',
34
+ );
35
+
36
+ MC.on.setupProgress(({ step, current, total, label, error: err }) => {
37
+ setProgress((p) => ({
38
+ ...p,
39
+ [step]: {
40
+ pct: total > 0 ? Math.round((current * 100) / total) : 0,
41
+ label,
42
+ error: err,
43
+ },
44
+ }));
45
+ });
46
+ MC.on.setupDone(async () => {
47
+ const s = await MC.getInitStatus();
48
+ setStatus(s);
49
+ setInstalling(false);
50
+ if (s.java_ok && s.instances.every((i) => i.installed)) setView('main');
51
+ });
52
+ MC.on.gameStarting((id) => setRunning((r) => [...r, id]));
53
+ MC.on.gameExit(({ instance_id, code }) => {
54
+ setRunning((r) => r.filter((x) => x !== instance_id));
55
+ setMessage(code === 0 ? 'Session ended.' : `Exited with code ${code}.`);
56
+ });
57
+ });
58
+
59
+ const steps = () => {
60
+ if (!config() || !status()) return [];
61
+ return [
62
+ { id: 'java', name: 'Java JRE', installed: status()!.java_ok },
63
+ ...config()!.instances.map((inst) => ({
64
+ id: inst.id,
65
+ name: inst.name,
66
+ installed:
67
+ status()!.instances.find((i) => i.id === inst.id)?.installed ?? false,
68
+ })),
69
+ ];
70
+ };
71
+
72
+ async function install() {
73
+ if (!dir().trim()) {
74
+ setError('Choose an install folder.');
75
+ return;
76
+ }
77
+ setError('');
78
+ setInstalling(true);
79
+ try {
80
+ const settings = await MC.getSettings();
81
+ await MC.saveSettings({ ...settings, launcher_dir: dir().trim() });
82
+ await MC.runSetup();
83
+ } catch (e) {
84
+ setError(`Error: ${e}`);
85
+ setInstalling(false);
86
+ }
87
+ }
88
+
89
+ async function playOrStop(id: string) {
90
+ if (running().includes(id)) {
91
+ await MC.stop(id).catch(() => {});
92
+ return;
93
+ }
94
+ if (!username().trim()) return;
95
+ const settings = await MC.getSettings();
96
+ await MC.saveSettings({ ...settings, username: username().trim() });
97
+ try {
98
+ await MC.verify(id);
99
+ await MC.play(id);
100
+ } catch (e) {
101
+ setMessage(`Error: ${e}`);
102
+ }
103
+ }
104
+
105
+ return (
106
+ <>
107
+ <Show when={view() === 'setup'}>
108
+ <div id="setup-overlay">
109
+ <div class="card">
110
+ <h1>Setup — {config()!.app_name}</h1>
111
+ <label>Install folder</label>
112
+ <div class="dir-row">
113
+ <input
114
+ type="text"
115
+ value={dir()}
116
+ onInput={(e) => setDir(e.currentTarget.value)}
117
+ />
118
+ <button onClick={async () => setDir(await MC.getDefaultDir())}>
119
+ Default
120
+ </button>
121
+ </div>
122
+ <For each={steps()}>
123
+ {(s) => {
124
+ const p = () => progress()[s.id];
125
+ const pct = () => p()?.pct ?? (s.installed ? 100 : 0);
126
+ return (
127
+ <div class="step">
128
+ <div
129
+ class="step-name"
130
+ style={{ color: s.installed ? '#4caf50' : '#aaa' }}>
131
+ {s.installed ? '✓' : '○'} {s.name}
132
+ </div>
133
+ <div class="bar-track">
134
+ <div
135
+ class="bar-fill"
136
+ style={{
137
+ width: `${pct()}%`,
138
+ background: p()?.error ? '#ef5350' : '#4caf50',
139
+ }}
140
+ />
141
+ </div>
142
+ <div class="step-label">
143
+ {p()?.label ?? (s.installed ? 'Installed' : '')}
144
+ </div>
145
+ </div>
146
+ );
147
+ }}
148
+ </For>
149
+ <button
150
+ class="install-btn"
151
+ disabled={installing()}
152
+ onClick={install}>
153
+ {installing() ? 'Installing…' : 'Install'}
154
+ </button>
155
+ <p class="status" style={{ color: '#ef5350' }}>
156
+ {error()}
157
+ </p>
158
+ </div>
159
+ </div>
160
+ </Show>
161
+
162
+ <Show when={view() === 'main'}>
163
+ <div class="card">
164
+ <div class="brand">
165
+ <Show when={config()!.logo}>
166
+ <img src={`./${config()!.logo}`} alt="" />
167
+ </Show>
168
+ <h1>{config()!.app_name}</h1>
169
+ </div>
170
+ <label>Username</label>
171
+ <input
172
+ type="text"
173
+ maxLength={16}
174
+ placeholder="Steve"
175
+ value={username()}
176
+ onInput={(e) => setUsername(e.currentTarget.value)}
177
+ />
178
+ <For each={config()!.instances}>
179
+ {(inst) => (
180
+ <button
181
+ class="instance-btn"
182
+ disabled={!running().includes(inst.id) && !username().trim()}
183
+ onClick={() => playOrStop(inst.id)}>
184
+ {running().includes(inst.id) ? '■ Stop' : `▶ ${inst.name}`}
185
+ </button>
186
+ )}
187
+ </For>
188
+ <p class="status">{message()}</p>
189
+ </div>
190
+ </Show>
191
+ </>
192
+ );
193
+ }
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Launcher</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="./main.tsx"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,5 @@
1
+ import { render } from 'solid-js/web';
2
+ import App from './App.tsx';
3
+ import './style.css';
4
+
5
+ render(() => <App />, document.getElementById('root')!);
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "jsx": "preserve",
7
+ "jsxImportSource": "solid-js",
8
+ "strict": true,
9
+ "noEmit": true,
10
+ "skipLibCheck": true,
11
+ "useDefineForClassFields": true,
12
+ "lib": ["ES2022", "DOM", "DOM.Iterable"]
13
+ },
14
+ "include": ["src"]
15
+ }
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from 'vite';
2
+ import solid from 'vite-plugin-solid';
3
+
4
+ export default defineConfig({
5
+ root: 'src',
6
+ plugins: [solid()],
7
+ clearScreen: false,
8
+ server: { port: 5173, strictPort: true },
9
+ build: { outDir: '../dist', emptyOutDir: true, target: 'es2022' },
10
+ });
@@ -4,170 +4,7 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>Launcher</title>
7
- <style>
8
- * {
9
- box-sizing: border-box;
10
- margin: 0;
11
- padding: 0;
12
- }
13
- body {
14
- font-family:
15
- system-ui,
16
- -apple-system,
17
- sans-serif;
18
- background: #0d0d0d;
19
- color: #e8e8e8;
20
- height: 100vh;
21
- display: flex;
22
- align-items: center;
23
- justify-content: center;
24
- user-select: none;
25
- }
26
- .card {
27
- background: #111;
28
- border: 1px solid #222;
29
- border-radius: 12px;
30
- padding: 28px 32px;
31
- width: 360px;
32
- }
33
- h1 {
34
- font-size: 1.4rem;
35
- margin-bottom: 4px;
36
- color: #4caf50;
37
- }
38
- p {
39
- font-size: 0.8rem;
40
- color: #555;
41
- margin-bottom: 20px;
42
- }
43
- label {
44
- display: block;
45
- font-size: 0.75rem;
46
- color: #666;
47
- margin-bottom: 6px;
48
- text-transform: uppercase;
49
- letter-spacing: 0.05em;
50
- }
51
- input[type='text'] {
52
- width: 100%;
53
- padding: 8px 12px;
54
- border-radius: 6px;
55
- background: #0a0a0a;
56
- border: 1px solid #333;
57
- color: #e8e8e8;
58
- font-size: 0.9rem;
59
- outline: none;
60
- margin-bottom: 16px;
61
- }
62
- input[type='text']:focus {
63
- border-color: #4caf50;
64
- }
65
- .instance-btn {
66
- width: 100%;
67
- padding: 10px;
68
- border-radius: 8px;
69
- border: none;
70
- background: #1a2e1a;
71
- color: #4caf50;
72
- font-size: 0.9rem;
73
- font-weight: 600;
74
- cursor: pointer;
75
- margin-bottom: 8px;
76
- transition: background 0.15s;
77
- }
78
- .instance-btn:hover:not(:disabled) {
79
- background: #4caf50;
80
- color: #fff;
81
- }
82
- .instance-btn:disabled {
83
- opacity: 0.4;
84
- cursor: not-allowed;
85
- }
86
- .status {
87
- font-size: 0.75rem;
88
- color: #555;
89
- margin-top: 12px;
90
- min-height: 1rem;
91
- }
92
-
93
- /* ── Setup overlay ─────────────────────────────────────── */
94
- #setup-overlay {
95
- position: fixed;
96
- inset: 0;
97
- background: #080808;
98
- display: flex;
99
- align-items: center;
100
- justify-content: center;
101
- }
102
- #setup-overlay .card {
103
- width: 400px;
104
- }
105
- #setup-overlay h1 {
106
- margin-bottom: 20px;
107
- }
108
- .dir-row {
109
- display: flex;
110
- gap: 8px;
111
- margin-bottom: 16px;
112
- }
113
- .dir-row input {
114
- flex: 1;
115
- margin: 0;
116
- }
117
- .dir-row button {
118
- padding: 8px 12px;
119
- border-radius: 6px;
120
- border: 1px solid #333;
121
- background: #111;
122
- color: #888;
123
- font-size: 0.8rem;
124
- cursor: pointer;
125
- }
126
- .step {
127
- margin-bottom: 10px;
128
- }
129
- .step-name {
130
- font-size: 0.85rem;
131
- margin-bottom: 4px;
132
- }
133
- .bar-track {
134
- height: 4px;
135
- background: #222;
136
- border-radius: 2px;
137
- overflow: hidden;
138
- }
139
- .bar-fill {
140
- height: 4px;
141
- background: #4caf50;
142
- transition: width 0.2s;
143
- }
144
- .step-label {
145
- font-size: 0.7rem;
146
- color: #555;
147
- margin-top: 2px;
148
- }
149
- .install-btn {
150
- width: 100%;
151
- padding: 10px;
152
- border-radius: 8px;
153
- border: none;
154
- background: #1a2e1a;
155
- color: #4caf50;
156
- font-weight: 700;
157
- font-size: 0.9rem;
158
- cursor: pointer;
159
- margin-top: 16px;
160
- transition: background 0.15s;
161
- }
162
- .install-btn:hover:not(:disabled) {
163
- background: #4caf50;
164
- color: #fff;
165
- }
166
- .install-btn:disabled {
167
- opacity: 0.4;
168
- cursor: not-allowed;
169
- }
170
- </style>
7
+ <link rel="stylesheet" href="./style.css" />
171
8
  </head>
172
9
  <body>
173
10
  <!-- ── Setup overlay (shown when not installed) ── -->
@@ -191,8 +28,11 @@
191
28
  <!-- ── Main launcher ── -->
192
29
  <div id="main-view" style="display: none">
193
30
  <div class="card">
194
- <h1 id="server-name">Launcher</h1>
195
- <p id="server-sub"></p>
31
+ <div class="brand">
32
+ <img id="logo" src="./logo.svg" alt="" style="display: none" />
33
+ <h1 id="server-name">Launcher</h1>
34
+ </div>
35
+ <p id="server-sub" class="sub"></p>
196
36
 
197
37
  <label>Username</label>
198
38
  <input
@@ -207,7 +47,7 @@
207
47
  </div>
208
48
 
209
49
  <script type="module">
210
- import { MC } from '/api.js';
50
+ import { MC } from './api.js';
211
51
 
212
52
  // ── Boot ────────────────────────────────────────────────────
213
53
  const [config, settings, initStatus] = await Promise.all([
@@ -236,15 +76,39 @@
236
76
  document.getElementById('main-view').style.display = 'flex';
237
77
  document.getElementById('server-name').textContent = config.app_name;
238
78
 
79
+ if (config.logo) {
80
+ const logo = document.getElementById('logo');
81
+ logo.src = `./${config.logo}`;
82
+ logo.style.display = 'block';
83
+ }
84
+
239
85
  const container = document.getElementById('instances-container');
240
86
  for (const inst of config.instances) {
87
+ const row = document.createElement('div');
88
+ row.className = 'instance-row';
89
+
241
90
  const btn = document.createElement('button');
242
91
  btn.className = 'instance-btn';
243
92
  btn.id = `play-${inst.id}`;
244
93
  btn.textContent = `▶ ${inst.name}`;
245
94
  btn.disabled = true;
246
95
  btn.addEventListener('click', () => onPlay(inst.id));
247
- container.appendChild(btn);
96
+ row.appendChild(btn);
97
+
98
+ const modsBtn = document.createElement('button');
99
+ modsBtn.className = 'icon-btn';
100
+ modsBtn.title = 'Mods';
101
+ modsBtn.textContent = '🧩';
102
+ modsBtn.addEventListener('click', () => toggleModsPanel(inst.id));
103
+ row.appendChild(modsBtn);
104
+
105
+ container.appendChild(row);
106
+
107
+ const panel = document.createElement('div');
108
+ panel.className = 'mods-panel';
109
+ panel.id = `mods-${inst.id}`;
110
+ panel.style.display = 'none';
111
+ container.appendChild(panel);
248
112
  }
249
113
 
250
114
  document
@@ -253,12 +117,15 @@
253
117
  updateButtons();
254
118
 
255
119
  MC.on.gameStarting((id) => {
256
- document.getElementById(`play-${id}`).textContent = 'EN JEU';
257
- document.getElementById(`play-${id}`).disabled = true;
120
+ const btn = document.getElementById(`play-${id}`);
121
+ btn.textContent = '■ Stop';
122
+ btn.disabled = false;
123
+ btn.dataset.running = '1';
258
124
  });
259
125
  MC.on.gameExit(({ instance_id, code }) => {
260
- document.getElementById(`play-${instance_id}`).textContent =
261
- `▶ ${config.instances.find((i) => i.id === instance_id)?.name ?? instance_id}`;
126
+ const btn = document.getElementById(`play-${instance_id}`);
127
+ btn.textContent = `▶ ${config.instances.find((i) => i.id === instance_id)?.name ?? instance_id}`;
128
+ delete btn.dataset.running;
262
129
  updateButtons();
263
130
  document.getElementById('game-status').textContent =
264
131
  code === 0 ? 'Session ended.' : `Exited with code ${code}.`;
@@ -280,18 +147,20 @@
280
147
  function updateButtons() {
281
148
  const username = document.getElementById('username-input').value.trim();
282
149
  document.querySelectorAll('.instance-btn').forEach((btn) => {
283
- if (!btn.textContent.includes('EN JEU')) btn.disabled = !username;
150
+ if (!btn.dataset.running) btn.disabled = !username;
284
151
  });
285
152
  }
286
153
 
287
154
  async function onPlay(instanceId) {
155
+ const btn = document.getElementById(`play-${instanceId}`);
156
+ if (btn.dataset.running) {
157
+ await MC.stop(instanceId).catch(() => {});
158
+ return;
159
+ }
288
160
  const username = document.getElementById('username-input').value.trim();
289
161
  if (!username) return;
290
- await MC.saveSettings({
291
- username,
292
- launcher_dir: null,
293
- max_memory: 2048,
294
- });
162
+ const settings = await MC.getSettings();
163
+ await MC.saveSettings({ ...settings, username });
295
164
  try {
296
165
  await MC.verify(instanceId);
297
166
  await MC.play(instanceId);
@@ -300,6 +169,69 @@
300
169
  }
301
170
  }
302
171
 
172
+ // ── Mods panel ───────────────────────────────────────────────
173
+ async function toggleModsPanel(instanceId) {
174
+ const panel = document.getElementById(`mods-${instanceId}`);
175
+ if (panel.style.display !== 'none') {
176
+ panel.style.display = 'none';
177
+ return;
178
+ }
179
+ await renderMods(instanceId);
180
+ panel.style.display = 'block';
181
+ }
182
+
183
+ async function renderMods(instanceId) {
184
+ const panel = document.getElementById(`mods-${instanceId}`);
185
+ const mods = await MC.mods.list(instanceId).catch(() => []);
186
+ panel.innerHTML = '';
187
+
188
+ if (mods.length === 0) {
189
+ panel.innerHTML = '<p class="mods-empty">No mods installed.</p>';
190
+ }
191
+ for (const mod of mods) {
192
+ const row = document.createElement('div');
193
+ row.className = mod.enabled ? 'mod-row' : 'mod-row disabled';
194
+
195
+ const check = document.createElement('input');
196
+ check.type = 'checkbox';
197
+ check.checked = mod.enabled;
198
+ check.addEventListener('change', async () => {
199
+ const action = check.checked ? MC.mods.enable : MC.mods.disable;
200
+ await action(instanceId, mod.file_name).catch(() => {});
201
+ renderMods(instanceId);
202
+ });
203
+ row.appendChild(check);
204
+
205
+ const name = document.createElement('span');
206
+ name.className = 'mod-name';
207
+ name.title = mod.file_name;
208
+ name.textContent = mod.name;
209
+ row.appendChild(name);
210
+
211
+ if (!mod.managed) {
212
+ const del = document.createElement('button');
213
+ del.className = 'icon-btn';
214
+ del.style.padding = '2px 6px';
215
+ del.style.margin = '0';
216
+ del.textContent = '✕';
217
+ del.title = 'Remove';
218
+ del.addEventListener('click', async () => {
219
+ await MC.mods.remove(instanceId, mod.file_name).catch(() => {});
220
+ renderMods(instanceId);
221
+ });
222
+ row.appendChild(del);
223
+ }
224
+ panel.appendChild(row);
225
+ }
226
+
227
+ const open = document.createElement('button');
228
+ open.className = 'icon-btn';
229
+ open.style.marginTop = '6px';
230
+ open.textContent = 'Open mods folder';
231
+ open.addEventListener('click', () => MC.mods.openFolder(instanceId));
232
+ panel.appendChild(open);
233
+ }
234
+
303
235
  // ── Setup page ───────────────────────────────────────────────
304
236
  function showSetup(config, settings, initStatus) {
305
237
  document.getElementById('setup-overlay').style.display = 'flex';
@@ -0,0 +1,51 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Launcher</title>
7
+ <link rel="stylesheet" href="./style.css" />
8
+ </head>
9
+ <body>
10
+ <!-- ── Setup overlay (shown when not installed) ── -->
11
+ <div id="setup-overlay" style="display: none">
12
+ <div class="card">
13
+ <h1 id="setup-title">Initial setup</h1>
14
+
15
+ <label>Install folder</label>
16
+ <div class="dir-row">
17
+ <input id="dir-input" type="text" placeholder="C:\Users\..." />
18
+ <button id="dir-default-btn">Default</button>
19
+ </div>
20
+
21
+ <div id="steps-container"></div>
22
+
23
+ <button id="install-btn" class="install-btn">Install</button>
24
+ <p id="setup-error" class="status" style="color: #ef5350"></p>
25
+ </div>
26
+ </div>
27
+
28
+ <!-- ── Main launcher ── -->
29
+ <div id="main-view" style="display: none">
30
+ <div class="card">
31
+ <div class="brand">
32
+ <img id="logo" src="./logo.svg" alt="" style="display: none" />
33
+ <h1 id="server-name">Launcher</h1>
34
+ </div>
35
+ <p id="server-sub" class="sub"></p>
36
+
37
+ <label>Username</label>
38
+ <input
39
+ id="username-input"
40
+ type="text"
41
+ maxlength="16"
42
+ placeholder="Steve" />
43
+
44
+ <div id="instances-container"></div>
45
+ <p id="game-status" class="status"></p>
46
+ </div>
47
+ </div>
48
+
49
+ <script type="module" src="./main.ts"></script>
50
+ </body>
51
+ </html>