pgkiln 0.49.0 → 0.51.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 (70) hide show
  1. package/SECURITY.md +3 -0
  2. package/db/migrations/119_workspace_home.sql +30 -0
  3. package/db/migrations/120_datasets.sql +87 -0
  4. package/docs/apex-feature-parity.md +7 -6
  5. package/docs/guide/01-installation.md +5 -0
  6. package/docs/guide/03-builder.md +140 -11
  7. package/docs/guide/04-pages-and-regions.md +25 -1
  8. package/docs/guide/12-development.md +47 -11
  9. package/examples/hr/hr_63_datasets.sql +29 -0
  10. package/package.json +1 -1
  11. package/public/app.css +6 -0
  12. package/public/builder.css +95 -3
  13. package/public/code-editor.css +32 -79
  14. package/public/code-editor.js +58 -3
  15. package/public/data-modeler.css +17 -14
  16. package/public/worksheet.css +14 -7
  17. package/public/worksheet.js +119 -3
  18. package/src/app.ts +5 -1
  19. package/src/builder/changelog.ts +1 -1
  20. package/src/builder/data-modeler.ts +1 -1
  21. package/src/builder/datasets.ts +472 -0
  22. package/src/builder/db-users.ts +2 -2
  23. package/src/builder/home.ts +12 -7
  24. package/src/builder/instance.ts +2 -0
  25. package/src/builder/object-browser.ts +4 -4
  26. package/src/builder/querybuilder.ts +1 -1
  27. package/src/builder/quicksql.ts +1 -1
  28. package/src/builder/reporter.ts +52 -3
  29. package/src/builder/routes.ts +3 -3
  30. package/src/builder/scheduling.ts +2 -2
  31. package/src/builder/scripts.ts +1 -1
  32. package/src/builder/sql-charts.ts +1 -1
  33. package/src/builder/sql-monitor.ts +1 -1
  34. package/src/builder/subscriptions.ts +1 -0
  35. package/src/builder/table-editor.ts +2 -2
  36. package/src/builder/ui.ts +7 -5
  37. package/src/builder/worksheet.ts +171 -23
  38. package/src/builder/workspace-home.ts +283 -0
  39. package/src/builder/workspaces.ts +7 -4
  40. package/src/datasets.ts +367 -0
  41. package/src/i18n/ar.ts +16 -0
  42. package/src/i18n/cs.ts +16 -0
  43. package/src/i18n/da.ts +16 -0
  44. package/src/i18n/de.ts +16 -0
  45. package/src/i18n/el.ts +16 -0
  46. package/src/i18n/es.ts +16 -0
  47. package/src/i18n/fi.ts +16 -0
  48. package/src/i18n/fr.ts +16 -0
  49. package/src/i18n/he.ts +16 -0
  50. package/src/i18n/it.ts +16 -0
  51. package/src/i18n/ja.ts +16 -0
  52. package/src/i18n/ko.ts +16 -0
  53. package/src/i18n/nb.ts +16 -0
  54. package/src/i18n/pl.ts +16 -0
  55. package/src/i18n/pt.ts +16 -0
  56. package/src/i18n/ru.ts +16 -0
  57. package/src/i18n/sv.ts +16 -0
  58. package/src/i18n/tr.ts +16 -0
  59. package/src/i18n/uk.ts +16 -0
  60. package/src/i18n/zh.ts +16 -0
  61. package/src/i18n.ts +32 -0
  62. package/src/instance.ts +1 -0
  63. package/src/release-check.ts +139 -0
  64. package/src/reporting-app.ts +101 -0
  65. package/src/runtime/data-reporter.ts +199 -32
  66. package/src/session.ts +7 -0
  67. package/src/sqlworksheet.ts +9 -6
  68. package/src/subscriptions.ts +2 -1
  69. package/src/worksheet-txn.ts +270 -0
  70. package/src/workspace-export.ts +24 -4
@@ -8,19 +8,20 @@
8
8
  .dm-toolbar .btn[disabled] { opacity: 0.5; }
9
9
  .dm-status { min-width: 0; }
10
10
  .dm-stage { position: relative; }
11
- .dm-canvas { overflow: auto; max-width: 100%; height: 65vh; min-height: 18rem; border: 1px solid var(--border); border-radius: var(--radius); background: var(--bg); }
11
+ .dm-canvas { overflow: auto; max-width: 100%; height: 65vh; min-height: 18rem; border: 1px solid var(--border); border-radius: var(--radius); background: var(--canvas, var(--bg)); box-shadow: var(--shadow-xs, none); }
12
12
  .dm-canvas:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
13
13
  .dm-svg { display: block; }
14
- .dm-svg .dm-bg { fill: var(--bg); }
15
- .dm-svg .dm-frame { fill: var(--surface); stroke: var(--border); }
16
- .dm-svg .dm-view .dm-frame { fill: var(--surface-2); }
17
- .dm-svg .dm-head { fill: var(--accent-soft); stroke: var(--border); }
18
- .dm-svg .dm-title { fill: var(--text); }
19
- .dm-svg .dm-col { fill: var(--text); }
20
- .dm-svg .dm-type, .dm-svg .dm-kind, .dm-svg .dm-more { fill: var(--muted); }
21
- .dm-svg .dm-key { fill: var(--gold, #b45309); }
22
- .dm-svg .dm-line, .dm-svg .dm-end { stroke: var(--muted); }
23
- .dm-svg .dm-opt { fill: var(--bg); stroke: var(--muted); }
14
+ /* the boxes and lines: no .dm-svg ancestor in the selectors, so the mini-map's <use> copy (a shadow tree) is themed too */
15
+ .dm-svg .dm-bg { fill: var(--canvas, var(--bg)); }
16
+ .dm-frame { fill: var(--block, var(--surface)); stroke: var(--border-strong, var(--border)); }
17
+ .dm-view .dm-frame { fill: var(--surface-2); }
18
+ path.dm-head { fill: var(--accent-soft); stroke: var(--border-strong, var(--border)); }
19
+ .dm-title { fill: var(--text); }
20
+ .dm-col { fill: var(--text); }
21
+ :is(.dm-type, .dm-kind, .dm-more) { fill: var(--muted); }
22
+ .dm-key { fill: var(--gold, #b45309); }
23
+ :is(.dm-line, .dm-end) { stroke: var(--muted); }
24
+ .dm-opt { fill: var(--canvas, var(--bg)); stroke: var(--muted); }
24
25
  .dm-svg .dm-fk:hover .dm-line, .dm-svg .dm-fk:hover .dm-end { stroke: var(--accent); stroke-width: 2.5; }
25
26
  .dm-svg .dm-link:hover .dm-title, .dm-svg .dm-link:focus .dm-title { fill: var(--accent); text-decoration: underline; }
26
27
  .dm-live .dm-box { cursor: grab; touch-action: none; }
@@ -29,9 +30,9 @@
29
30
  .dm-svg .dm-box:focus-visible .dm-frame { stroke: var(--accent); stroke-width: 2.5; }
30
31
  .dm-svg .dm-dragging .dm-frame { stroke: var(--accent); }
31
32
  .dm-svg text { user-select: none; }
32
- .dm-minimap { position: absolute; right: 12px; bottom: 12px; width: 200px; height: 140px; border: 1px solid var(--border); border-radius: var(--radius);
33
- background: var(--surface); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); cursor: pointer; touch-action: none; }
34
- .dm-minimap-bg { fill: var(--surface); }
33
+ .dm-minimap { position: absolute; right: 12px; bottom: 12px; width: 200px; height: 140px; border: 1px solid var(--border-strong, var(--border)); border-radius: var(--radius);
34
+ background: var(--canvas, var(--surface)); box-shadow: var(--shadow-md, 0 2px 8px rgb(0 0 0 / 20%)); cursor: pointer; touch-action: none; overflow: hidden; }
35
+ .dm-minimap-bg { fill: var(--canvas, var(--surface)); }
35
36
  .dm-minimap-view { fill: var(--accent); fill-opacity: 0.15; stroke: var(--accent); stroke-width: 4; vector-effect: non-scaling-stroke; }
36
37
  .dm-legend { margin: 6px 0 12px; }
37
38
  .dm-search-row { display: flex; gap: 6px; flex-wrap: wrap; }
@@ -54,3 +55,5 @@
54
55
  .dm-svg { max-width: 100%; height: auto; }
55
56
  .dm-dict-table { break-inside: avoid-page; }
56
57
  }
58
+ /* sprint 108: the Mini-map toggle shows that it is on */
59
+ .dm-toolbar .btn[aria-pressed="true"] { background: var(--accent-soft); color: var(--accent); border-color: color-mix(in srgb, var(--accent) 45%, var(--border)); }
@@ -60,8 +60,8 @@
60
60
  .ws-grid .null { color: var(--muted); font-style: italic; }
61
61
  .ws-log { margin: 0; padding: 8px; background: var(--code-bg); border: 1px solid var(--border); border-radius: var(--radius); font-size: 12.5px; line-height: 1.45; white-space: pre; overflow: auto; }
62
62
  .ws-log-sql { color: var(--muted); }
63
- .ws-log-ok { color: #2f7d32; font-weight: 600; }
64
- .ws-log-err { color: #c0392b; white-space: pre-wrap; }
63
+ .ws-log-ok { color: var(--success); font-weight: 600; }
64
+ .ws-log-err { color: var(--danger); white-space: pre-wrap; }
65
65
  .ws-log-warn { color: var(--warn); white-space: pre-wrap; }
66
66
  .ws-notices, .ws-history { list-style: none; margin: 0; padding: 0; font-size: 13px; }
67
67
  .ws-notices li, .ws-history li { display: flex; gap: 8px; align-items: baseline; padding: 4px 0; border-bottom: 1px solid var(--border); min-width: 0; }
@@ -72,7 +72,7 @@
72
72
  .ws-status { display: flex; align-items: center; gap: 12px; padding: 4px 10px; border-top: 1px solid var(--border); background: var(--surface-2); font-size: 12.5px; min-width: 0; }
73
73
  .ws-status .icon { width: 13px; height: 13px; vertical-align: -2px; }
74
74
  .ws-count { color: var(--muted); white-space: nowrap; }
75
- .ws-count.is-err { color: #c0392b; }
75
+ .ws-count.is-err { color: var(--danger); }
76
76
  .ws-count.is-warn { color: var(--warn); }
77
77
  .ws-last { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
78
78
  .ws-time { margin-left: auto; }
@@ -156,8 +156,8 @@ th[aria-sort=descending] .ws-sort::after { content: ' ▼'; font-size: 0.75em; }
156
156
  .ws-hist-bar { display: flex; gap: 6px; margin-bottom: 6px; }
157
157
  .ws-hist-bar input { flex: 1; min-width: 0; }
158
158
  .ws-hist-icon .icon { width: 14px; height: 14px; }
159
- .ws-hist-icon.is-ok { color: #2f7d32; }
160
- .ws-hist-icon.is-err { color: #c0392b; }
159
+ .ws-hist-icon.is-ok { color: var(--success); }
160
+ .ws-hist-icon.is-err { color: var(--danger); }
161
161
  .ws-history li { align-items: center; }
162
162
  .ws-history .btn { flex: none; }
163
163
  @media (max-width: 640px) { .ws-hist-meta { display: none; } }
@@ -166,8 +166,8 @@ th[aria-sort=descending] .ws-sort::after { content: ' ▼'; font-size: 0.75em; }
166
166
  .ws-wtabs { display: none; align-items: stretch; gap: 0; overflow-x: auto; border-bottom: 1px solid var(--border); background: var(--surface-2); flex: none; }
167
167
  .js .ws-wtabs { display: flex; }
168
168
  .ws-wtab { display: flex; align-items: center; border-right: 1px solid var(--border); }
169
- .ws-wtab [role=tab] { border: 0; background: none; color: var(--muted); padding: 5px 4px 5px 10px; font: inherit; font-size: 12.5px; cursor: pointer; white-space: nowrap; max-width: 14rem; overflow: hidden; text-overflow: ellipsis; }
170
- .ws-wtab [role=tab][aria-selected=true] { color: var(--text); box-shadow: inset 0 -2px 0 var(--hot); }
169
+ .ws-wtab [data-ws-wtab] { border: 0; background: none; color: var(--muted); padding: 5px 4px 5px 10px; font: inherit; font-size: 12.5px; cursor: pointer; white-space: nowrap; max-width: 14rem; overflow: hidden; text-overflow: ellipsis; }
170
+ .ws-wtab [data-ws-wtab][aria-current=true] { color: var(--text); box-shadow: inset 0 -2px 0 var(--hot); }
171
171
  .ws-wtab-close, .ws-wtab-new { border: 0; background: none; color: var(--muted); cursor: pointer; padding: 2px 8px; font-size: 14px; line-height: 1; }
172
172
  .ws-wtab-close:hover, .ws-wtab-new:hover { color: var(--text); }
173
173
  .ws-file > summary { list-style: none; max-width: 14rem; }
@@ -291,3 +291,10 @@ details.menu.obj-menu > summary::before, details.menu.obj-menu > summary::after
291
291
  .ch-card { border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); padding: 10px 12px; min-width: 0; }
292
292
  .ch-card h2 { font-size: 14px; margin: 0 0 6px; }
293
293
  .ch-edit { max-width: 48rem; margin-top: 16px; }
294
+
295
+ /* (sprint 107) transaction mode: Autocommit / Transaction, the badge, Commit and Rollback */
296
+ .ws-txn { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 6px; min-width: 0; }
297
+ .ws-txn-badge { font-size: 12px; padding: 2px 8px; border-radius: 999px; border: 1px solid var(--border); color: var(--muted); max-width: 22rem; overflow-wrap: anywhere; }
298
+ .ws-txn-badge[data-state="open"] { color: var(--warn); border-color: color-mix(in srgb, var(--warn) 55%, var(--border)); background: color-mix(in srgb, var(--warn) 10%, transparent); font-weight: 600; }
299
+ .ws-txn-badge[data-state="aborted"] { color: var(--danger); border-color: var(--danger); background: var(--danger-soft); font-weight: 600; }
300
+ .ws-txn-alert { margin: 6px 8px; }
@@ -13,6 +13,9 @@
13
13
  // - (sprint 93) worksheets as tabs kept in this browser (unsaved text survives a reload), saved as SQL Scripts
14
14
  // (Ctrl+S), the Files list; bind and substitution values in a dialog; Format (Ctrl+F7), Convert case (Ctrl+B),
15
15
  // find and replace (Ctrl+F, Ctrl+H), go to line (Ctrl+G), comment lines (Ctrl+/), Help (F1)
16
+ // - (sprint 107) transaction mode per worksheet tab: Autocommit / Transaction, Commit and Rollback, a badge with the
17
+ // server's real state (fetched again after a reload and a tab switch, and when the idle timeout is due); leaving the
18
+ // page with an open transaction asks first, closing the tab or switching to Autocommit rolls it back
16
19
  // Strict CSP: no inline styles; sizes are set through the CSSOM.
17
20
  (() => {
18
21
  'use strict';
@@ -266,8 +269,8 @@
266
269
  wrap.className = 'ws-wtab';
267
270
  const b = document.createElement('button');
268
271
  b.type = 'button';
269
- b.setAttribute('role', 'tab');
270
- b.setAttribute('aria-selected', String(t === cur));
272
+ // (sprint 108) buttons in a group, the open one aria-current: a tablist may hold only tabs, not the close buttons
273
+ if (t === cur) b.setAttribute('aria-current', 'true');
271
274
  b.dataset.wsWtab = t.key;
272
275
  b.textContent = `${t.name}${dirty(t) ? '*' : ''}`;
273
276
  b.title = t.scriptId ? `Script “${t.name}”${dirty(t) ? ' (unsaved changes)' : ''}` : 'Not saved as a script';
@@ -288,11 +291,15 @@
288
291
  ta.value = t.text;
289
292
  ta.dispatchEvent(new Event('input', { bubbles: true }));
290
293
  drawTabs();
294
+ txnTabChanged();
291
295
  ta.focus();
292
296
  };
293
297
  const newTab = () => switchTo(add({ key: newKey(), name: untitled(), scriptId: null, text: '', saved: '' }));
294
298
  const closeTab = (t) => {
295
- if (dirty(t) && t.text.trim() && !window.confirm(`Close “${t.name}” without saving?`)) return;
299
+ const txnOpen = t === cur ? txnBox.dataset.state !== 'none' : !!t.txnOpen;
300
+ if (txnOpen && !window.confirm(`“${t.name}” has an open transaction. Close it and roll the transaction back?`)) return;
301
+ if (!txnOpen && dirty(t) && t.text.trim() && !window.confirm(`Close “${t.name}” without saving?`)) return;
302
+ if (t.txn) txnPost(t.key, 'rollback'); // the server knows best whether one is open
296
303
  const i = book.tabs.indexOf(t);
297
304
  book.tabs.splice(i, 1);
298
305
  if (t === cur) {
@@ -340,6 +347,111 @@
340
347
  form.querySelector('.ws-nav-switch').addEventListener('click', (e) => { const b = e.target.closest('[data-ws-navtab]'); if (b) navTab(b.dataset.wsNavtab); });
341
348
  navTab(location.hash === '#ws-files' ? 'files' : 'objects');
342
349
 
350
+ // ------------------------------------------------------------ (sprint 107) transaction mode
351
+ function txnTabChanged() {
352
+ modeSel.value = cur.txn ? 'on' : '';
353
+ txnApply({ state: 'none', statements: 0, startedMs: null, note: null });
354
+ txnFetch();
355
+ }
356
+ const txnBox = form.querySelector('[data-ws-txn]');
357
+ const modeSel = txnBox.querySelector('[data-ws-txn-mode]');
358
+ const badge = txnBox.querySelector('[data-ws-txn-badge]');
359
+ const commitBtn = txnBox.querySelector('[data-ws-txn-commit]');
360
+ const rollbackBtn = txnBox.querySelector('[data-ws-txn-rollback]');
361
+ const commitNote = form.querySelector('[data-ws-commit-note]');
362
+ const idleSeconds = Number(txnBox.dataset.idle) || 300;
363
+ const idleWords = idleSeconds % 60 ? `${idleSeconds} seconds` : `${idleSeconds / 60} minute${idleSeconds === 60 ? '' : 's'}`;
364
+ let txnInfo = { state: txnBox.dataset.state, statements: Number(txnBox.dataset.statements) || 0, startedMs: Number(txnBox.dataset.started) || null,
365
+ lastMs: Number(txnBox.dataset.last) || null, note: txnBox.dataset.note || null };
366
+ let clockSkew = Date.now() - (Number(txnBox.dataset.now) || Date.now());
367
+ let txnTimer = 0;
368
+ const badgeText = (i) => {
369
+ if (i.state === 'none') return i.note || 'No open transaction';
370
+ const min = i.startedMs ? Math.floor((Date.now() - clockSkew - i.startedMs) / 60000) : 0;
371
+ return `${i.state === 'aborted' ? 'Transaction failed' : 'Transaction open'} · ${i.statements} statement${i.statements === 1 ? '' : 's'} · ${min < 1 ? 'under a minute' : `${min} min`}`;
372
+ };
373
+ const txnApply = (i) => {
374
+ txnInfo = i;
375
+ const on = modeSel.value === 'on';
376
+ txnBox.dataset.state = i.state;
377
+ badge.dataset.state = i.state;
378
+ badge.textContent = badgeText(i);
379
+ badge.hidden = !on && i.state === 'none' && !i.note;
380
+ commitBtn.hidden = rollbackBtn.hidden = !on && i.state === 'none';
381
+ commitBtn.disabled = i.state !== 'open';
382
+ rollbackBtn.disabled = i.state === 'none';
383
+ commitNote.textContent = on
384
+ ? `The statements stay in one transaction until Commit or Rollback; it is rolled back after ${idleWords} without a run.`
385
+ : 'Each statement commits on its own.';
386
+ cur.txn = on;
387
+ cur.txnOpen = i.state !== 'none';
388
+ persist();
389
+ clearTimeout(txnTimer);
390
+ if (i.state !== 'none') {
391
+ // the age once a minute; the server again when the idle timeout is due
392
+ const due = i.lastMs ? i.lastMs + idleSeconds * 1000 + 2000 - (Date.now() - clockSkew) : 60000;
393
+ txnTimer = setTimeout(() => (due <= 60000 ? txnFetch() : txnApply(txnInfo)), Math.max(1000, Math.min(due, 60000)));
394
+ }
395
+ };
396
+ const fromServer = (j) => {
397
+ clockSkew = Date.now() - j.now;
398
+ txnApply({ state: j.state, statements: j.statements, startedMs: j.startedMs, lastMs: j.lastMs, note: j.note });
399
+ };
400
+ async function txnFetch() {
401
+ const key = cur.key;
402
+ try {
403
+ const res = await fetch(`/builder/sql/worksheet/txn?tab=${encodeURIComponent(key)}`, { credentials: 'same-origin', headers: { accept: 'application/json' } });
404
+ if (!res.ok) throw new Error(String(res.status));
405
+ const j = await res.json();
406
+ if (key !== cur.key) return; // switched meanwhile
407
+ if (j.state !== 'none') modeSel.value = 'on';
408
+ fromServer(j);
409
+ } catch {
410
+ badge.textContent = 'Transaction state unknown';
411
+ }
412
+ }
413
+ async function txnPost(key, action) {
414
+ try {
415
+ const res = await fetch('/builder/sql/worksheet/txn', { method: 'POST', credentials: 'same-origin', headers: { accept: 'application/json' },
416
+ body: new URLSearchParams({ __csrf: form.elements.__csrf.value, tab: key, action }) });
417
+ const j = await res.json();
418
+ if (key === cur.key) {
419
+ fromServer(j);
420
+ if (j.message) announce(j.message);
421
+ }
422
+ return j;
423
+ } catch {
424
+ announce('The transaction could not be ended: reload the page.');
425
+ return null;
426
+ }
427
+ }
428
+ modeSel.addEventListener('change', async () => {
429
+ if (modeSel.value !== 'on' && txnInfo.state !== 'none') {
430
+ if (!window.confirm('Switching to Autocommit rolls back the open transaction. Roll it back?')) { modeSel.value = 'on'; return; }
431
+ await txnPost(cur.key, 'rollback');
432
+ modeSel.value = '';
433
+ }
434
+ txnApply(txnInfo);
435
+ });
436
+ // a page post (a run, Commit, Rollback) is not leaving: the transaction stays
437
+ let submitting = false;
438
+ form.addEventListener('submit', () => {
439
+ submitting = true;
440
+ setTimeout(() => { submitting = false; }, 3000); // a download doesn't leave the page
441
+ });
442
+ window.addEventListener('pageshow', () => { submitting = false; });
443
+ window.addEventListener('beforeunload', (e) => {
444
+ if (submitting || txnInfo.state === 'none') return;
445
+ e.preventDefault();
446
+ e.returnValue = ''; // the browser asks "Leave site?"; an idle transaction is rolled back after the timeout
447
+ });
448
+ if (!form.dataset.tab || form.dataset.tab !== cur.key) txnTabChanged();
449
+ else {
450
+ if (txnInfo.state !== 'none') modeSel.value = 'on';
451
+ else if (!form.querySelector('[data-ws-txn-mode] option[selected]')) modeSel.value = cur.txn ? 'on' : '';
452
+ txnApply(txnInfo);
453
+ }
454
+
343
455
  // ------------------------------------------------------------ (sprint 97) a chart's settings draw it again
344
456
  form.querySelector('.ch-controls')?.addEventListener('change', () => run('chart'));
345
457
 
@@ -513,6 +625,7 @@
513
625
  const size = (key, px) => {
514
626
  const v = key === 'nav' ? Math.max(160, Math.min(px, window.innerWidth * 0.5)) : Math.max(80, Math.min(px, window.innerHeight * 0.75));
515
627
  form.style.setProperty(key === 'nav' ? '--ws-nav' : '--ws-out', `${Math.round(v)}px`);
628
+ form.querySelector(`[data-ws-split="${key}"]`)?.setAttribute('aria-valuenow', String(Math.round(v)));
516
629
  store.set(key, String(Math.round(v)));
517
630
  return v;
518
631
  };
@@ -520,6 +633,9 @@
520
633
  for (const sp of form.querySelectorAll('[data-ws-split]')) {
521
634
  const key = sp.dataset.wsSplit;
522
635
  const current = () => (key === 'nav' ? form.querySelector('.ws-nav').getBoundingClientRect().width : out.getBoundingClientRect().height);
636
+ // (sprint 108) a focusable separator states its position (in pixels)
637
+ sp.setAttribute('aria-valuemin', '0');
638
+ if (!sp.hasAttribute('aria-valuenow')) sp.setAttribute('aria-valuenow', String(Math.round(current())));
523
639
  sp.addEventListener('pointerdown', (e) => {
524
640
  e.preventDefault();
525
641
  sp.setPointerCapture(e.pointerId);
package/src/app.ts CHANGED
@@ -52,7 +52,9 @@ import { blueprintRoutes } from './builder/blueprints.ts';
52
52
  import { accountRoutes } from './runtime/account.ts';
53
53
  import { taskRoutes } from './runtime/tasks.ts';
54
54
  import { dataReporterRoutes } from './runtime/data-reporter.ts';
55
+ import { datasetRoutes } from './builder/datasets.ts';
55
56
  import { reporterBuilderRoutes } from './builder/reporter.ts';
57
+ import { workspaceHomeRoutes } from './builder/workspace-home.ts';
56
58
  import { formDesignerRoutes } from './runtime/form-designer.ts';
57
59
  import { formDesignerBuilderRoutes } from './builder/form-designer.ts';
58
60
  import { builderErrorPages, builderServerError } from './builder/errors.ts';
@@ -239,6 +241,7 @@ export async function buildApp(opts: { logger?: boolean } = {}) {
239
241
  await app.register(codeEditorRoutes);
240
242
  await app.register(dataReporterRoutes);
241
243
  await app.register(reporterBuilderRoutes);
244
+ await app.register(datasetRoutes);
242
245
  await app.register(formDesignerRoutes);
243
246
  await app.register(formDesignerBuilderRoutes);
244
247
  await app.register(sampleDataRoutes);
@@ -247,6 +250,7 @@ export async function buildApp(opts: { logger?: boolean } = {}) {
247
250
  await app.register(assistantBuilderRoutes);
248
251
  await app.register(aiBuilderRoutes);
249
252
  await app.register(blueprintRoutes);
250
- app.get('/', async (_req, reply) => reply.redirect('/builder'));
253
+ await app.register(workspaceHomeRoutes);
254
+ app.get('/', async (_req, reply) => reply.redirect('/builder/home'));
251
255
  return app;
252
256
  }
@@ -69,7 +69,7 @@ async function listPage(s: Session, q: Record<string, unknown>) {
69
69
  ${list.map((c) => html`<tr>
70
70
  <td data-label="#">${c.seq}</td>
71
71
  <td data-label="Change set" class="wrap-anywhere"><details><summary><code>${flywayName(c)}</code>${c.object_name ? html` <span class="muted small">${c.object_name}</span>` : ''}</summary>
72
- <pre class="source wrap-pre">${c.sql}</pre>
72
+ <pre class="source wrap-pre" data-code-view="sql" tabindex="0" aria-label="SQL">${c.sql}</pre>
73
73
  <form method="post" action="${CHANGES}/${c.id}" class="search u-mwnone">${csrf(s)}
74
74
  <div class="field"><label class="label" for="f_name_${c.id}">Name</label><input id="f_name_${c.id}" name="name" value="${c.name}" pattern="[a-z0-9][a-z0-9_]{0,59}" required></div>
75
75
  <button class="btn btn-sm u-selfend">Rename</button></form>
@@ -355,7 +355,7 @@ export async function dataModelerRoutes(app: FastifyInstance) {
355
355
  return reply.header('content-disposition', `attachment; filename="${fileName(d.name)}.sql"`).type('application/sql; charset=utf-8').send(script);
356
356
  const main = html`<h1 class="u-mb1">SQL Workshop</h1>${workshopTabs('modeler')}
357
357
  ${region(`DDL of ${d.name}`, html`<p class="muted u-mt0">The CREATE statements of the diagram's ${m.tables.length} objects in schema ${d.schema_name}: tables, their keys, indexes, triggers, row level security, policies, comments and grants, and views. Review before running.</p>
358
- <pre class="source" tabindex="0" aria-label="DDL">${script}</pre>
358
+ <pre class="source" data-code-view="sql" tabindex="0" aria-label="DDL">${script}</pre>
359
359
  <form method="post" action="${BASE}/sql">${csrf(s)}<input type="hidden" name="sql" value="${script}">
360
360
  <div class="buttons"><a class="btn" href="${M}/${d.id}">${bicon('back')} Back to the diagram</a><a class="btn" href="${M}/${d.id}/ddl?download=1">${icon('download')} Download</a>
361
361
  <button class="btn btn-hot">${icon('play')} Open in the SQL Worksheet</button></div></form>`)}`;