@anmol-srv/sigil 0.11.0 → 0.12.1

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.
@@ -1,7 +1,17 @@
1
1
  // Sigil GUI — vanilla JS. Onboarding wizard + dashboard.
2
+ import { toast } from './toast.js';
3
+ import { connectorCard, dbFlowRow, setFlowRow } from './components.js';
4
+
2
5
  const $ = (sel, root = document) => root.querySelector(sel);
3
6
  const $$ = (sel, root = document) => root.querySelectorAll(sel);
4
7
 
8
+ // Onboarding machine step (SCREAMING_SNAKE) → wizard section id.
9
+ const MACHINE_TO_STEP = { CONNECTORS: 'connectors', PROVIDER: 'llm', EMBEDDING: 'embedding', DATABASE: 'database', FINISH: 'finish' };
10
+ async function persistStep(step, status, data = {}) {
11
+ try { await rpc('onboardingAdvance', { step, status, data }); }
12
+ catch (err) { /* non-fatal: state persistence is best-effort */ void err; }
13
+ }
14
+
5
15
  // ── RPC ──────────────────────────────────────────────────────────────
6
16
  async function rpc(method, params = {}) {
7
17
  const res = await fetch('/api/v1/rpc', {
@@ -12,7 +22,10 @@ async function rpc(method, params = {}) {
12
22
  });
13
23
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
14
24
  const body = await res.json();
15
- if (!body.ok) throw new Error(body.error?.message || 'rpc error');
25
+ if (!body.ok) {
26
+ const e = body.error || {};
27
+ throw Object.assign(new Error(e.message || 'rpc error'), { code: e.code, hint: e.hint });
28
+ }
16
29
  return body.data;
17
30
  }
18
31
 
@@ -45,27 +58,27 @@ async function copyToClipboard(text) {
45
58
  // ════════════════════════════════════════════════════════════════════
46
59
  // ONBOARDING WIZARD
47
60
  // ════════════════════════════════════════════════════════════════════
48
- const wizardState = { step: 'welcome', llmProvider: null, embProvider: null, llmProviders: [], embProviders: [] };
61
+ const wizardState = { step: 'connectors', llmProvider: null, embProvider: null, llmProviders: [], embProviders: [], connectorsLoaded: false, dbInit: false, connectedCount: 0 };
62
+
63
+ const STEP_ORDER = ['connectors', 'llm', 'embedding', 'database', 'finish'];
49
64
 
50
65
  function setOnbStep(stepId) {
51
66
  wizardState.step = stepId;
52
- // Update step list state — done/active/future
53
- const order = ['welcome', 'database', 'llm', 'embedding', 'finish'];
54
- const idx = order.indexOf(stepId);
67
+ const idx = STEP_ORDER.indexOf(stepId);
55
68
  $$('.onboarding-step').forEach((el) => {
56
- const i = order.indexOf(el.dataset.obStep);
69
+ const i = STEP_ORDER.indexOf(el.dataset.obStep);
57
70
  el.classList.remove('active', 'done', 'future');
58
71
  if (i < idx) el.classList.add('done');
59
72
  else if (i === idx) el.classList.add('active');
60
73
  else el.classList.add('future');
61
74
  });
62
- // Show only the active step
63
75
  $$('.wizard-step').forEach((el) => el.classList.toggle('active', el.dataset.step === stepId));
64
- // Lazy-fetch provider lists when entering those steps
76
+ // Lazy-fetch per-step data when first entering a step.
77
+ if (stepId === 'connectors' && !wizardState.connectorsLoaded) loadConnectors();
65
78
  if (stepId === 'llm' && !wizardState.llmProviders.length) loadLlmProviders();
66
79
  if (stepId === 'embedding' && !wizardState.embProviders.length) loadEmbeddingProviders();
80
+ if (stepId === 'database' && !wizardState.dbInit) initDbStep();
67
81
  if (stepId === 'finish') renderFinish();
68
- // Scroll content to top
69
82
  document.querySelector('.onboarding-content')?.scrollTo(0, 0);
70
83
  }
71
84
 
@@ -77,37 +90,96 @@ async function loadOnboardingState() {
77
90
  return;
78
91
  }
79
92
  $('#onboarding').hidden = false;
80
- // Pre-fill DB step's "next" enabled if already done
81
- if (state.steps.database.done) {
82
- $('#ob-db-next').disabled = false;
83
- }
84
- if (state.steps.llm.done) {
85
- $('#ob-llm-next').disabled = false;
86
- }
87
- if (state.steps.embedding.done) {
88
- $('#ob-emb-next').disabled = false;
89
- }
90
- } catch (err) {
91
- // Could not reach daemon — show welcome anyway
93
+ if (state.steps.database.done) $('#ob-db-next').disabled = false;
94
+ if (state.steps.llm.done) $('#ob-llm-next').disabled = false;
95
+ if (state.steps.embedding.done) $('#ob-emb-next').disabled = false;
96
+ // Resume at the machine's current step (refresh mid-wizard → same place).
97
+ const resume = MACHINE_TO_STEP[state.machine?.currentStep];
98
+ if (resume && resume !== 'connectors') setOnbStep(resume);
99
+ else setOnbStep('connectors');
100
+ } catch {
101
+ // Could not reach daemon — show the first step anyway.
92
102
  $('#onboarding').hidden = false;
93
103
  }
94
104
  }
95
105
 
96
- // ── DB step ──────────────────────────────────────────────────────────
106
+ // ── Connectors step ──────────────────────────────────────────────────
107
+ async function loadConnectors() {
108
+ wizardState.connectorsLoaded = true;
109
+ const host = $('#ob-connectors');
110
+ try {
111
+ const { connectors } = await rpc('listConnectors');
112
+ wizardState.connectedCount = connectors.filter((c) => c.status === 'connected').length;
113
+ renderConnectors(connectors);
114
+ } catch (err) {
115
+ host.innerHTML = `<div class="muted">could not load connectors: ${escape(err.message)}</div>`;
116
+ }
117
+ }
118
+
119
+ function renderConnectors(connectors) {
120
+ const host = $('#ob-connectors');
121
+ host.innerHTML = '';
122
+ connectors.forEach((c) => host.appendChild(connectorCard(c, onConnectorAction)));
123
+ }
124
+
125
+ async function onConnectorAction(id, action) {
126
+ const host = $('#ob-connectors');
127
+ const card = host.querySelector(`[data-id="${id}"]`);
128
+ if (action === 'disconnect') {
129
+ try {
130
+ await rpc('disconnectConnector', { id });
131
+ toast({ variant: 'success', message: `${id} disconnected` });
132
+ } catch (err) { toast({ variant: 'error', message: err.message, hint: err.hint, code: err.code }); }
133
+ return loadConnectors();
134
+ }
135
+ // connect / retry → optimistic "connecting" card, then refresh.
136
+ if (card) card.replaceWith(connectorCard({ id, label: id, hint: '', uiState: 'connecting' }, onConnectorAction));
137
+ try {
138
+ await rpc('connectConnector', { id });
139
+ toast({ variant: 'success', message: `${id} connected` });
140
+ } catch (err) {
141
+ toast({ variant: 'error', message: err.message || `could not connect ${id}`, hint: err.hint, code: err.code });
142
+ }
143
+ return loadConnectors();
144
+ }
145
+
146
+ // ── DB step (linear guided flow) ─────────────────────────────────────
147
+ function dbMode() {
148
+ return $('input[name="db-mode"]:checked')?.value || 'url';
149
+ }
150
+
97
151
  $('#db-mode-cards')?.addEventListener('click', (e) => {
98
152
  const card = e.target.closest('[data-db-mode]');
99
- if (!card) return;
153
+ if (!card || card.hidden) return;
100
154
  $$('#db-mode-cards .provider-card').forEach((c) => c.classList.remove('selected'));
101
155
  card.classList.add('selected');
102
156
  card.querySelector('input').checked = true;
103
- $('#ob-db-url').style.display = card.dataset.dbMode === 'url' ? '' : 'none';
104
- $('#ob-db-fields').style.display = card.dataset.dbMode === 'fields' ? '' : 'none';
157
+ const mode = card.dataset.dbMode;
158
+ $('#ob-db-url').style.display = mode === 'url' ? '' : 'none';
159
+ $('#ob-db-fields').style.display = mode === 'fields' ? '' : 'none';
160
+ $('#ob-db-setup').textContent = mode === 'docker' ? 'Create local database' : 'Set up database';
105
161
  });
106
- $('#db-mode-cards .provider-card')?.classList.add('selected');
162
+
163
+ // Probe Docker once; if present, surface the recommended "Local (automatic)"
164
+ // mode and select it by default. Otherwise leave URL selected.
165
+ async function initDbStep() {
166
+ wizardState.dbInit = true;
167
+ try {
168
+ const d = await rpc('dbDockerAvailable');
169
+ const note = $('#ob-db-docker-note');
170
+ if (d.available) {
171
+ const dockerCard = $('#ob-db-mode-docker');
172
+ dockerCard.hidden = false;
173
+ dockerCard.click();
174
+ } else if (note) {
175
+ note.hidden = false;
176
+ note.textContent = `Docker not detected (${d.reason || 'unavailable'}) — use a connection URL or local Postgres.`;
177
+ }
178
+ } catch { /* leave URL mode as the default */ }
179
+ }
107
180
 
108
181
  function obDbParams() {
109
- const isUrl = $('input[name="db-mode"]:checked').value === 'url';
110
- if (isUrl) return { url: $('#ob-db-url-input').value.trim() };
182
+ if (dbMode() === 'url') return { url: $('#ob-db-url-input').value.trim() };
111
183
  return {
112
184
  host: $('#ob-db-host').value.trim(),
113
185
  port: Number($('#ob-db-port').value),
@@ -117,78 +189,97 @@ function obDbParams() {
117
189
  };
118
190
  }
119
191
 
120
- $('#ob-db-test')?.addEventListener('click', async () => {
121
- const out = $('#ob-db-result');
122
- out.hidden = false;
123
- out.className = 'result';
124
- out.textContent = 'testing…';
192
+ function dbFlowInit(rows) {
193
+ const flow = $('#ob-db-flow');
194
+ flow.hidden = false;
195
+ flow.innerHTML = '';
196
+ rows.forEach(([id, label]) => flow.appendChild(dbFlowRow(id, label)));
197
+ return flow;
198
+ }
199
+
200
+ $('#ob-db-setup')?.addEventListener('click', async () => {
201
+ const btn = $('#ob-db-setup');
202
+ btn.disabled = true;
203
+ $('#ob-db-next').disabled = true;
204
+ const mode = dbMode();
125
205
  try {
126
- const data = await rpc('testDbConnection', obDbParams());
127
- out.textContent = JSON.stringify(data, null, 2);
128
- out.classList.add(data.ok ? 'ok' : 'err');
129
- if (data.ok && !data.pgvector) {
130
- $('#ob-db-install-pgv').hidden = false;
131
- $('#ob-db-migrate').hidden = true;
132
- $('#ob-db-next').disabled = true;
133
- out.textContent += '\n\npgvector is not installed yet. Click "Install pgvector".';
134
- } else if (data.ok) {
135
- $('#ob-db-install-pgv').hidden = true;
136
- $('#ob-db-migrate').hidden = false;
137
- out.textContent += '\n\npgvector is installed. Click "Run migrations" to finish.';
206
+ if (mode === 'docker') {
207
+ await runDockerFlow();
138
208
  } else {
139
- $('#ob-db-install-pgv').hidden = true;
140
- $('#ob-db-migrate').hidden = true;
141
- $('#ob-db-next').disabled = true;
209
+ await runUrlFlow();
142
210
  }
143
- } catch (err) {
144
- out.textContent = `ERROR: ${err.message}`;
145
- out.classList.add('err');
211
+ } finally {
212
+ btn.disabled = false;
146
213
  }
147
214
  });
148
215
 
149
- $('#ob-db-install-pgv')?.addEventListener('click', async () => {
150
- const out = $('#ob-db-result');
151
- out.textContent += '\n\nInstalling pgvector…';
216
+ async function runDockerFlow() {
217
+ const flow = dbFlowInit([['provision', 'Create pgvector container'], ['migrate', 'Run migrations']]);
218
+ setFlowRow(flow, 'provision', { phase: 'active', detail: 'pulling image + starting…' });
152
219
  try {
153
- const data = await rpc('ensurePgvector', obDbParams());
154
- if (data.ok && data.installed) {
155
- out.textContent += `\n✓ pgvector ${data.version} installed`;
156
- $('#ob-db-install-pgv').hidden = true;
157
- $('#ob-db-migrate').hidden = false;
158
- } else {
159
- out.textContent += `\n✗ ${data.error || 'unknown'} (${data.stage})`;
160
- }
161
- } catch (err) { out.textContent += `\nERROR: ${err.message}`; }
162
- });
220
+ const r = await rpc('dbProvisionDocker');
221
+ setFlowRow(flow, 'provision', { phase: 'done', detail: `${r.container} :${r.port}${r.reused ? ' (reused)' : ''}` });
222
+ setFlowRow(flow, 'migrate', { phase: 'done', detail: `${r.migrationsRan} migrations · pgvector ✓` });
223
+ await onDbReady({ pgvector: true, migrationsRan: r.migrationsRan, mode: 'docker' });
224
+ } catch (err) {
225
+ setFlowRow(flow, 'provision', { phase: 'error', detail: err.code || 'failed' });
226
+ toast({ variant: 'error', message: err.message, hint: err.hint, code: err.code });
227
+ }
228
+ }
163
229
 
164
- $('#ob-db-migrate')?.addEventListener('click', async () => {
165
- const out = $('#ob-db-result');
166
- out.textContent += '\n\nPersisting connection to ~/.sigil/.env…';
230
+ async function runUrlFlow() {
231
+ const params = obDbParams();
232
+ const flow = dbFlowInit([['test', 'Test connection'], ['pgvector', 'Enable pgvector'], ['migrate', 'Run migrations']]);
233
+ // 1. test
234
+ setFlowRow(flow, 'test', { phase: 'active', detail: 'connecting…' });
235
+ let test;
236
+ try {
237
+ test = await rpc('testDbConnection', params);
238
+ } catch (err) {
239
+ setFlowRow(flow, 'test', { phase: 'error', detail: err.code || 'failed' });
240
+ return toast({ variant: 'error', message: err.message, hint: err.hint, code: err.code });
241
+ }
242
+ if (!test.ok) {
243
+ setFlowRow(flow, 'test', { phase: 'error', detail: test.code || test.stage || 'failed' });
244
+ return toast({ variant: 'error', message: test.error || 'connection failed', hint: test.fixHint, code: test.kind });
245
+ }
246
+ setFlowRow(flow, 'test', { phase: 'done', detail: `${test.provider} · ${test.connectMs}ms` });
247
+ // 2. pgvector
248
+ if (!test.pgvector) {
249
+ setFlowRow(flow, 'pgvector', { phase: 'active', detail: 'installing…' });
250
+ try {
251
+ const pg = await rpc('ensurePgvector', params);
252
+ if (!pg.ok || !pg.installed) throw Object.assign(new Error(pg.error || 'could not enable pgvector'), { hint: pg.fixHint });
253
+ setFlowRow(flow, 'pgvector', { phase: 'done', detail: pg.version ? `v${pg.version}` : 'enabled' });
254
+ } catch (err) {
255
+ setFlowRow(flow, 'pgvector', { phase: 'error', detail: err.code || 'failed' });
256
+ return toast({ variant: 'error', message: err.message, hint: err.hint, code: err.code });
257
+ }
258
+ } else {
259
+ setFlowRow(flow, 'pgvector', { phase: 'done', detail: 'already enabled' });
260
+ }
261
+ // 3. persist + migrate
262
+ setFlowRow(flow, 'migrate', { phase: 'active', detail: 'writing env + migrating…' });
167
263
  try {
168
- const params = obDbParams();
169
264
  if (params.url) {
170
- await rpc('writeEnv', { patch: {
171
- SIGIL_DATABASE_URL: params.url,
172
- SIGIL_DB_HOST: null, SIGIL_DB_PORT: null, SIGIL_DB_NAME: null, SIGIL_DB_USER: null, SIGIL_DB_PASSWORD: null,
173
- } });
265
+ await rpc('writeEnv', { patch: { SIGIL_DATABASE_URL: params.url, SIGIL_DB_HOST: null, SIGIL_DB_PORT: null, SIGIL_DB_NAME: null, SIGIL_DB_USER: null, SIGIL_DB_PASSWORD: null } });
174
266
  } else {
175
- await rpc('writeEnv', { patch: {
176
- SIGIL_DB_HOST: params.host, SIGIL_DB_PORT: String(params.port),
177
- SIGIL_DB_NAME: params.database, SIGIL_DB_USER: params.user, SIGIL_DB_PASSWORD: params.password,
178
- SIGIL_DATABASE_URL: null,
179
- } });
267
+ await rpc('writeEnv', { patch: { SIGIL_DB_HOST: params.host, SIGIL_DB_PORT: String(params.port), SIGIL_DB_NAME: params.database, SIGIL_DB_USER: params.user, SIGIL_DB_PASSWORD: params.password, SIGIL_DATABASE_URL: null } });
180
268
  }
181
- out.textContent += '\n✓ env written. Running migrations…';
182
- // Pass connection params so runMigrations uses a one-shot pool against
183
- // the new URL the daemon's existing pool is still bound to the
184
- // pre-onboarding env (localhost:5432 by default).
185
- const data = await rpc('runMigrations', params);
186
- out.textContent += `\n✓ batch ${data.batchNo}: ${data.ran.length} migrations applied (${data.against})`;
187
- $('#ob-db-next').disabled = false;
269
+ const m = await rpc('runMigrations', params);
270
+ setFlowRow(flow, 'migrate', { phase: 'done', detail: `batch ${m.batchNo} · ${m.ran.length} applied` });
271
+ await onDbReady({ pgvector: true, migrationsRan: m.ran.length, mode: dbMode() });
188
272
  } catch (err) {
189
- out.textContent += `\n✗ ${err.message}`;
273
+ setFlowRow(flow, 'migrate', { phase: 'error', detail: err.code || 'failed' });
274
+ toast({ variant: 'error', message: err.message, hint: err.hint, code: err.code });
190
275
  }
191
- });
276
+ }
277
+
278
+ async function onDbReady(data) {
279
+ $('#ob-db-next').disabled = false;
280
+ toast({ variant: 'success', message: 'Database ready.' });
281
+ await persistStep('DATABASE', 'DONE', data);
282
+ }
192
283
 
193
284
  // ── LLM provider step ───────────────────────────────────────────────
194
285
  async function loadLlmProviders() {
@@ -243,9 +334,11 @@ $('#ob-llm-save')?.addEventListener('click', async () => {
243
334
  out.classList.add('ok');
244
335
  out.textContent += `\n✓ provider responded: "${test.response}"`;
245
336
  $('#ob-llm-next').disabled = false;
337
+ await persistStep('PROVIDER', 'DONE', { llmProvider: wizardState.llmProvider });
246
338
  } else {
247
339
  out.classList.add('err');
248
340
  out.textContent += `\n✗ test failed: ${test.error}`;
341
+ toast({ variant: 'error', message: test.error || 'LLM test failed', hint: test.fixHint, code: test.kind });
249
342
  }
250
343
  } catch (err) {
251
344
  out.classList.add('err');
@@ -396,7 +489,10 @@ $('#ob-emb-save')?.addEventListener('click', async () => {
396
489
  fields,
397
490
  out: $('#ob-emb-result'),
398
491
  conflictHost: $('#ob-emb-fields')?.parentElement,
399
- onSuccess: () => { $('#ob-emb-next').disabled = false; },
492
+ onSuccess: () => {
493
+ $('#ob-emb-next').disabled = false;
494
+ persistStep('EMBEDDING', 'DONE', { provider: wizardState.embProvider });
495
+ },
400
496
  });
401
497
  if (!ok) $('#ob-emb-next').disabled = true;
402
498
  });
@@ -416,16 +512,32 @@ async function renderFinish() {
416
512
  } catch { /* ignore */ }
417
513
  }
418
514
  $('#ob-complete')?.addEventListener('click', async () => {
419
- try { await rpc('markOnboardingComplete'); }
420
- catch { /* ignore */ }
515
+ const installService = $('#ob-always-up')?.checked === true;
516
+ try {
517
+ const r = await rpc('markOnboardingComplete', { installService });
518
+ if (installService && r && r.serviceInstalled === false) {
519
+ toast({ variant: 'info', message: 'Could not install the always-up service on this platform.', hint: 'Sigil still auto-starts on first use; retry with `sigil service install`.' });
520
+ }
521
+ } catch { /* daemon restarts on complete — expected to drop */ }
421
522
  $('#onboarding').hidden = true;
422
- refreshHealth();
523
+ // Daemon is handing off (restart / service). Give it a moment, then refresh.
524
+ setTimeout(() => refreshHealth(), 1500);
423
525
  });
424
526
 
425
527
  // ── Navigation between wizard steps ──────────────────────────────────
426
528
  document.addEventListener('click', (e) => {
427
529
  const n = e.target.closest('[data-ob-next]');
428
- if (n) { e.preventDefault(); setOnbStep(n.dataset.obNext); return; }
530
+ if (n) {
531
+ e.preventDefault();
532
+ // Persist the step we're leaving (connectors is skippable — DONE if any
533
+ // tool connected, else SKIPPED). Provider/Embedding/Database persist on
534
+ // their own success handlers.
535
+ if (wizardState.step === 'connectors') {
536
+ persistStep('CONNECTORS', wizardState.connectedCount > 0 ? 'DONE' : 'SKIPPED', {});
537
+ }
538
+ setOnbStep(n.dataset.obNext);
539
+ return;
540
+ }
429
541
  const b = e.target.closest('[data-ob-back]');
430
542
  if (b) { e.preventDefault(); setOnbStep(b.dataset.obBack); return; }
431
543
  });
@@ -450,7 +562,7 @@ function setRoute(name) {
450
562
  if (name === 'health') refreshHealth();
451
563
  if (name === 'kb') refreshKb();
452
564
  if (name === 'methods') refreshMethods();
453
- if (name === 'settings') refreshEnv();
565
+ if (name === 'settings') { refreshEnv(); refreshSettingsClients(); }
454
566
  if (name === 'devices') refreshDevices();
455
567
  if (name === 'activity') { ensureActivityWs(); loadTraces(); }
456
568
  }
@@ -564,6 +676,46 @@ async function refreshEnv() {
564
676
  }
565
677
  }
566
678
 
679
+ // ── Settings: coding agents ──────────────────────────────────────────
680
+ // Same flow as the onboarding CONNECTORS step, surfaced post-onboarding so
681
+ // users who skipped the step (or completed setup before this card existed)
682
+ // can still wire up Claude Code / Cursor / Codex / Kiro / Hermes.
683
+ async function refreshSettingsClients() {
684
+ const host = $('#settings-connectors');
685
+ if (!host) return;
686
+ try {
687
+ const { connectors } = await rpc('listConnectors');
688
+ host.innerHTML = '';
689
+ if (!connectors.length) {
690
+ host.innerHTML = '<div class="muted">no agents registered</div>';
691
+ return;
692
+ }
693
+ connectors.forEach((c) => host.appendChild(connectorCard(c, onSettingsClientAction)));
694
+ } catch (err) {
695
+ host.innerHTML = `<div class="muted">could not load agents: ${escape(err.message)}</div>`;
696
+ }
697
+ }
698
+
699
+ async function onSettingsClientAction(id, action) {
700
+ const host = $('#settings-connectors');
701
+ const card = host?.querySelector(`[data-id="${id}"]`);
702
+ if (action === 'disconnect') {
703
+ try {
704
+ await rpc('disconnectConnector', { id });
705
+ toast({ variant: 'success', message: `${id} disconnected` });
706
+ } catch (err) { toast({ variant: 'error', message: err.message, hint: err.hint, code: err.code }); }
707
+ return refreshSettingsClients();
708
+ }
709
+ if (card) card.replaceWith(connectorCard({ id, label: id, hint: '', uiState: 'connecting' }, onSettingsClientAction));
710
+ try {
711
+ await rpc('connectConnector', { id });
712
+ toast({ variant: 'success', message: `${id} connected` });
713
+ } catch (err) {
714
+ toast({ variant: 'error', message: err.message || `could not connect ${id}`, hint: err.hint, code: err.code });
715
+ }
716
+ return refreshSettingsClients();
717
+ }
718
+
567
719
  // ── Settings: live provider switcher (LLM + embedding) ───────────────
568
720
  // Reuses the wizard's provider catalogs + the shared applyEmbeddingProvider
569
721
  // gate. "Apply" persists config and restarts the daemon so the new pool /
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Vanilla design-system primitives (translated from Sigil.zip's ui_kit). Each
3
+ * returns a DOM node. Used by the onboarding wizard so every screen is composed
4
+ * from the same on-brand parts: status square+word, connector cards, DB-flow
5
+ * rows. No framework, no build step.
6
+ */
7
+
8
+ const E = (tag, cls, text) => {
9
+ const el = document.createElement(tag);
10
+ if (cls) el.className = cls;
11
+ if (text != null) el.textContent = text;
12
+ return el;
13
+ };
14
+
15
+ /** Status square + lowercase word. kind: ok|warn|danger|muted */
16
+ export function statusDot(kind, word) {
17
+ const wrap = E('span', `status-dot ${kind}`);
18
+ wrap.append(E('span', 'sq'), E('span', null, word));
19
+ return wrap;
20
+ }
21
+
22
+ const CONNECTOR_STATUS = {
23
+ connected: { kind: 'ok', word: 'connected' },
24
+ available: { kind: 'muted', word: 'available' },
25
+ unavailable: { kind: 'muted', word: 'not installed' },
26
+ connecting: { kind: 'warn', word: 'connecting…' },
27
+ error: { kind: 'danger', word: 'error' },
28
+ };
29
+
30
+ /**
31
+ * Connector card. `onAction(id, action)` is called with action 'connect' |
32
+ * 'disconnect' | 'retry'. `state` overrides the derived status (e.g. while a
33
+ * connect is in flight pass 'connecting').
34
+ */
35
+ export function connectorCard(c, onAction) {
36
+ const status = c.uiState || c.status;
37
+ const card = E('div', `connector-card ${status === 'unavailable' ? 'unavailable' : ''}`);
38
+ card.dataset.id = c.id;
39
+
40
+ const top = E('div', 'cc-top');
41
+ top.append(E('div', 'cc-name', c.label));
42
+ const meta = CONNECTOR_STATUS[status] || CONNECTOR_STATUS.available;
43
+ top.append(statusDot(meta.kind, meta.word));
44
+ card.append(top);
45
+
46
+ card.append(E('div', 'cc-hint', c.reason && status === 'error' ? c.reason : c.hint));
47
+
48
+ const actions = E('div', 'cc-actions');
49
+ if (status === 'connected') {
50
+ const b = E('button', 'btn danger', 'Disconnect');
51
+ b.type = 'button';
52
+ b.onclick = () => onAction(c.id, 'disconnect');
53
+ actions.append(b);
54
+ } else if (status === 'connecting') {
55
+ const b = E('button', 'btn', 'Connecting…');
56
+ b.type = 'button'; b.disabled = true;
57
+ actions.append(b);
58
+ } else if (status === 'unavailable') {
59
+ // no action — not installed on this machine
60
+ } else {
61
+ const b = E('button', status === 'error' ? 'btn' : 'btn primary', status === 'error' ? 'Retry' : 'Connect');
62
+ b.type = 'button';
63
+ b.onclick = () => onAction(c.id, status === 'error' ? 'retry' : 'connect');
64
+ actions.append(b);
65
+ }
66
+ card.append(actions);
67
+ return card;
68
+ }
69
+
70
+ /**
71
+ * A single row in the DB guided flow. phase: pending|active|done|error.
72
+ * Returns the row element; update via setFlowRow().
73
+ */
74
+ export function dbFlowRow(id, label) {
75
+ const row = E('div', 'db-flow-row pending');
76
+ row.dataset.row = id;
77
+ row.append(E('span', 'step-sq'));
78
+ row.append(E('span', 'step-label', label));
79
+ row.append(E('span', 'step-detail', ''));
80
+ return row;
81
+ }
82
+
83
+ export function setFlowRow(container, id, { phase, detail } = {}) {
84
+ const row = container.querySelector(`[data-row="${id}"]`);
85
+ if (!row) return;
86
+ if (phase) row.className = `db-flow-row ${phase}`;
87
+ if (detail != null) row.querySelector('.step-detail').textContent = detail;
88
+ }
89
+
90
+ export { E };