@njeybe/nativ 2.2.0 → 2.3.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.
Files changed (141) hide show
  1. package/CHANGELOG.md +24 -1
  2. package/README.md +15 -0
  3. package/dist/runner/agent-supervisor.d.ts +5 -3
  4. package/dist/runner/agent-supervisor.js +17 -12
  5. package/dist/server/studio-server.js +1 -130
  6. package/dist/server/studio-ui/document.d.ts +3 -0
  7. package/dist/server/studio-ui/document.js +22 -0
  8. package/dist/server/studio-ui/pages/benchmarks/index.d.ts +2 -0
  9. package/dist/server/studio-ui/pages/benchmarks/index.js +11 -0
  10. package/dist/server/studio-ui/pages/benchmarks/script.d.ts +1 -0
  11. package/dist/server/studio-ui/pages/benchmarks/script.js +104 -0
  12. package/dist/server/studio-ui/pages/benchmarks/styles.d.ts +1 -0
  13. package/dist/server/studio-ui/pages/benchmarks/styles.js +30 -0
  14. package/dist/server/studio-ui/pages/database/browser.d.ts +1 -0
  15. package/dist/server/studio-ui/pages/database/browser.js +326 -0
  16. package/dist/server/studio-ui/pages/database/connection.d.ts +1 -0
  17. package/dist/server/studio-ui/pages/database/connection.js +158 -0
  18. package/dist/server/studio-ui/pages/database/data-shell.d.ts +1 -0
  19. package/dist/server/studio-ui/pages/database/data-shell.js +165 -0
  20. package/dist/server/studio-ui/pages/database/drift.d.ts +1 -0
  21. package/dist/server/studio-ui/pages/database/drift.js +301 -0
  22. package/dist/server/studio-ui/pages/database/events.d.ts +1 -0
  23. package/dist/server/studio-ui/pages/database/events.js +231 -0
  24. package/dist/server/studio-ui/pages/database/explorer.d.ts +1 -0
  25. package/dist/server/studio-ui/pages/database/explorer.js +183 -0
  26. package/dist/server/studio-ui/pages/database/index.d.ts +2 -0
  27. package/dist/server/studio-ui/pages/database/index.js +28 -0
  28. package/dist/server/studio-ui/pages/database/markup.d.ts +1 -0
  29. package/dist/server/studio-ui/pages/database/markup.js +32 -0
  30. package/dist/server/studio-ui/pages/database/state.d.ts +2 -0
  31. package/dist/server/studio-ui/pages/database/state.js +57 -0
  32. package/dist/server/studio-ui/pages/database/styles.d.ts +1 -0
  33. package/dist/server/studio-ui/pages/database/styles.js +118 -0
  34. package/dist/server/studio-ui/pages/flow/index.d.ts +2 -0
  35. package/dist/server/studio-ui/pages/flow/index.js +11 -0
  36. package/dist/server/studio-ui/pages/flow/layout.d.ts +1 -0
  37. package/dist/server/studio-ui/pages/flow/layout.js +105 -0
  38. package/dist/server/studio-ui/pages/flow/plan-view.d.ts +1 -0
  39. package/dist/server/studio-ui/pages/flow/plan-view.js +67 -0
  40. package/dist/server/studio-ui/pages/flow/script.d.ts +1 -0
  41. package/dist/server/studio-ui/pages/flow/script.js +61 -0
  42. package/dist/server/studio-ui/pages/flow/styles.d.ts +1 -0
  43. package/dist/server/studio-ui/pages/flow/styles.js +59 -0
  44. package/dist/server/studio-ui/pages/flow/timeline-model.d.ts +1 -0
  45. package/dist/server/studio-ui/pages/flow/timeline-model.js +95 -0
  46. package/dist/server/studio-ui/pages/flow/timeline-view.d.ts +1 -0
  47. package/dist/server/studio-ui/pages/flow/timeline-view.js +92 -0
  48. package/dist/server/studio-ui/pages/home/index.d.ts +2 -0
  49. package/dist/server/studio-ui/pages/home/index.js +11 -0
  50. package/dist/server/studio-ui/pages/home/script.d.ts +1 -0
  51. package/dist/server/studio-ui/pages/home/script.js +160 -0
  52. package/dist/server/studio-ui/pages/home/styles.d.ts +1 -0
  53. package/dist/server/studio-ui/pages/home/styles.js +48 -0
  54. package/dist/server/studio-ui/pages/index.d.ts +2 -0
  55. package/dist/server/studio-ui/pages/index.js +17 -0
  56. package/dist/server/studio-ui/pages/tasks/index.d.ts +2 -0
  57. package/dist/server/studio-ui/pages/tasks/index.js +11 -0
  58. package/dist/server/studio-ui/pages/tasks/script.d.ts +1 -0
  59. package/dist/server/studio-ui/pages/tasks/script.js +96 -0
  60. package/dist/server/studio-ui/pages/tasks/styles.d.ts +1 -0
  61. package/dist/server/studio-ui/pages/tasks/styles.js +37 -0
  62. package/dist/server/studio-ui/pages/team/index.d.ts +2 -0
  63. package/dist/server/studio-ui/pages/team/index.js +11 -0
  64. package/dist/server/studio-ui/pages/team/script.d.ts +1 -0
  65. package/dist/server/studio-ui/pages/team/script.js +112 -0
  66. package/dist/server/studio-ui/pages/team/styles.d.ts +1 -0
  67. package/dist/server/studio-ui/pages/team/styles.js +22 -0
  68. package/dist/server/studio-ui/pages/types.d.ts +13 -0
  69. package/dist/server/studio-ui/pages/types.js +1 -0
  70. package/dist/server/studio-ui/pages/worktrees/index.d.ts +2 -0
  71. package/dist/server/studio-ui/pages/worktrees/index.js +11 -0
  72. package/dist/server/studio-ui/pages/worktrees/script.d.ts +1 -0
  73. package/dist/server/studio-ui/pages/worktrees/script.js +104 -0
  74. package/dist/server/studio-ui/pages/worktrees/styles.d.ts +1 -0
  75. package/dist/server/studio-ui/pages/worktrees/styles.js +15 -0
  76. package/dist/server/studio-ui/shell/actions.d.ts +1 -0
  77. package/dist/server/studio-ui/shell/actions.js +49 -0
  78. package/dist/server/studio-ui/shell/boot.d.ts +2 -0
  79. package/dist/server/studio-ui/shell/boot.js +13 -0
  80. package/dist/server/studio-ui/shell/console.d.ts +1 -0
  81. package/dist/server/studio-ui/shell/console.js +194 -0
  82. package/dist/server/studio-ui/shell/core.d.ts +1 -0
  83. package/dist/server/studio-ui/shell/core.js +50 -0
  84. package/dist/server/studio-ui/shell/detail-view.d.ts +1 -0
  85. package/dist/server/studio-ui/shell/detail-view.js +80 -0
  86. package/dist/server/studio-ui/shell/detail.d.ts +1 -0
  87. package/dist/server/studio-ui/shell/detail.js +83 -0
  88. package/dist/server/studio-ui/shell/dispatch.d.ts +1 -0
  89. package/dist/server/studio-ui/shell/dispatch.js +150 -0
  90. package/dist/server/studio-ui/shell/events-tools.d.ts +1 -0
  91. package/dist/server/studio-ui/shell/events-tools.js +94 -0
  92. package/dist/server/studio-ui/shell/format.d.ts +1 -0
  93. package/dist/server/studio-ui/shell/format.js +151 -0
  94. package/dist/server/studio-ui/shell/icons.d.ts +2 -0
  95. package/dist/server/studio-ui/shell/icons.js +20 -0
  96. package/dist/server/studio-ui/shell/markup-head.d.ts +1 -0
  97. package/dist/server/studio-ui/shell/markup-head.js +43 -0
  98. package/dist/server/studio-ui/shell/markup-panels.d.ts +2 -0
  99. package/dist/server/studio-ui/shell/markup-panels.js +7 -0
  100. package/dist/server/studio-ui/shell/markup-tools.d.ts +1 -0
  101. package/dist/server/studio-ui/shell/markup-tools.js +222 -0
  102. package/dist/server/studio-ui/shell/model.d.ts +1 -0
  103. package/dist/server/studio-ui/shell/model.js +192 -0
  104. package/dist/server/studio-ui/shell/paint.d.ts +1 -0
  105. package/dist/server/studio-ui/shell/paint.js +154 -0
  106. package/dist/server/studio-ui/shell/palette-view.d.ts +1 -0
  107. package/dist/server/studio-ui/shell/palette-view.js +60 -0
  108. package/dist/server/studio-ui/shell/palette.d.ts +1 -0
  109. package/dist/server/studio-ui/shell/palette.js +80 -0
  110. package/dist/server/studio-ui/shell/pipeline-state.d.ts +1 -0
  111. package/dist/server/studio-ui/shell/pipeline-state.js +152 -0
  112. package/dist/server/studio-ui/shell/proposals.d.ts +1 -0
  113. package/dist/server/studio-ui/shell/proposals.js +197 -0
  114. package/dist/server/studio-ui/shell/pure.d.ts +3 -0
  115. package/dist/server/studio-ui/shell/pure.js +9 -0
  116. package/dist/server/studio-ui/shell/router.d.ts +1 -0
  117. package/dist/server/studio-ui/shell/router.js +95 -0
  118. package/dist/server/studio-ui/shell/sidebar-view.d.ts +1 -0
  119. package/dist/server/studio-ui/shell/sidebar-view.js +38 -0
  120. package/dist/server/studio-ui/shell/sync.d.ts +1 -0
  121. package/dist/server/studio-ui/shell/sync.js +120 -0
  122. package/dist/server/studio-ui/shell/toasts-format.d.ts +1 -0
  123. package/dist/server/studio-ui/shell/toasts-format.js +89 -0
  124. package/dist/server/studio-ui/shell/triage.d.ts +1 -0
  125. package/dist/server/studio-ui/shell/triage.js +210 -0
  126. package/dist/server/studio-ui/styles/console.d.ts +1 -0
  127. package/dist/server/studio-ui/styles/console.js +83 -0
  128. package/dist/server/studio-ui/styles/dialogs.d.ts +1 -0
  129. package/dist/server/studio-ui/styles/dialogs.js +118 -0
  130. package/dist/server/studio-ui/styles/responsive.d.ts +1 -0
  131. package/dist/server/studio-ui/styles/responsive.js +28 -0
  132. package/dist/server/studio-ui/styles/shell.d.ts +1 -0
  133. package/dist/server/studio-ui/styles/shell.js +117 -0
  134. package/dist/server/studio-ui/styles/strategist.d.ts +1 -0
  135. package/dist/server/studio-ui/styles/strategist.js +77 -0
  136. package/dist/server/studio-ui/styles/tokens-base.d.ts +1 -0
  137. package/dist/server/studio-ui/styles/tokens-base.js +131 -0
  138. package/dist/server/studio-ui.d.ts +1 -10
  139. package/dist/server/studio-ui.js +63 -5314
  140. package/package.json +2 -2
  141. package/plugin/.claude-plugin/plugin.json +1 -1
@@ -0,0 +1,183 @@
1
+ // Database Studio explorer and media preview.
2
+ export const databaseExplorerScript = String.raw ` // ─── Database Studio: explorer ─────────────────────────────────────────────
3
+ function diffBadges() {
4
+ var map = { dev: {}, prod: {} };
5
+ var d = state.diff;
6
+ if (!d) return map;
7
+ var lower = function (n) { return String(n).toLowerCase(); };
8
+ d.addedTables.forEach(function (t) { map.dev[lower(t.name)] = 'added'; });
9
+ d.alteredTables.forEach(function (t) { map.dev[lower(t.name)] = 'altered'; if (state.diffTargetUsed === 'prod') map.prod[lower(t.name)] = 'altered'; });
10
+ if (state.diffTargetUsed === 'prod') d.droppedTables.forEach(function (t) { map.prod[lower(t.name)] = 'dropped'; });
11
+ return map;
12
+ }
13
+
14
+ var BADGE_TEXT = { added: 'NEW TABLE', altered: 'ALTERED', dropped: 'DROPPED' };
15
+
16
+ /** IMAGE badge: hover shows a thumbnail tooltip, click opens the media modal. */
17
+ function mediaBadge(t, c) {
18
+ if (!c.isMedia) return '';
19
+ var i = mediaRegistry.push({ table: t.name, field: c.name, type: c.type, media: c.media || null }) - 1;
20
+ return '<button type="button" class="badge badge-media" data-media="' + i + '" aria-label="Preview image field ' + esc(c.name) + '">IMAGE</button>';
21
+ }
22
+
23
+ function tableCard(t, badge) {
24
+ var coll = isCollection(t);
25
+ var fkCols = {};
26
+ (t.foreignKeys || []).forEach(function (fk) { fkCols[String(fk.column).toLowerCase()] = fk; });
27
+ var rows = t.columns.map(function (c) {
28
+ var fk = fkCols[String(c.name).toLowerCase()];
29
+ var tags = (c.primaryKey ? '<span class="badge badge-pk">PK</span> ' : '') +
30
+ (fk ? '<span class="badge badge-fk" title="→ ' + esc(fk.referencedTable + '.' + fk.referencedColumn) + '">FK</span> ' : '') +
31
+ (c.isSubcollection ? '<span class="badge badge-subcol">SUBCOLLECTION</span> ' : '') +
32
+ mediaBadge(t, c);
33
+ var typeCell = esc(c.type) + (c.observedTypes ? ' <span class="muted">(' + esc(c.observedTypes.join(' | ')) + ')</span>' : '');
34
+ var lastCell = coll
35
+ ? (c.presence != null ? Math.round(c.presence * 100) + '%' : '<span class="muted">–</span>')
36
+ : (c.default == null ? '<span class="muted">NULL</span>' : esc(c.default));
37
+ return '<tr><td>' + (tags || '<span class="muted">–</span>') + '</td><td class="mono">' + esc(c.name) +
38
+ (fk ? ' <span class="muted">→ ' + esc(fk.referencedTable + '.' + fk.referencedColumn) + '</span>' : '') + '</td><td class="mono">' + typeCell +
39
+ '</td><td>' + (c.nullable ? 'YES' : '<b>NO</b>') + '</td><td class="mono">' + lastCell + '</td></tr>';
40
+ }).join('');
41
+ var idx = (t.indexes || []).length
42
+ ? '<div class="subhead">Indexes</div><ul class="idx-list">' + t.indexes.map(function (i) {
43
+ return '<li><code>' + esc(i.name) + '</code> (' + esc(i.columns.join(', ')) + ')' + (i.unique ? ' <span class="muted">unique</span>' : '') + '</li>';
44
+ }).join('') + '</ul>'
45
+ : '';
46
+ var docMeta = coll && (t.documentCount != null || t.sampledDocuments != null)
47
+ ? '<p class="desc">' + (t.documentCount != null ? '~' + t.documentCount + ' documents' : 'Document count unavailable') +
48
+ (t.sampledDocuments != null ? ' · fields inferred from ' + t.sampledDocuments + ' sampled' : '') + '</p>'
49
+ : '';
50
+ return '<details class="table-card"><summary><span class="tname">' + esc(t.name) + '</span>' +
51
+ (coll ? '<span class="badge badge-collection">COLLECTION</span>' : '') +
52
+ (badge ? '<span class="badge badge-' + badge + '">' + BADGE_TEXT[badge] + '</span>' : '') +
53
+ '<span class="ccount">' + t.columns.length + (coll ? ' fields' : ' cols') + '</span></summary><div class="table-body">' +
54
+ (t.description ? '<p class="desc">' + esc(t.description) + '</p>' : '') + docMeta +
55
+ '<table class="cols"><thead><tr><th scope="col">Key</th><th scope="col">' + (coll ? 'Field' : 'Name') + '</th><th scope="col">Type</th><th scope="col">Nullable</th><th scope="col">' +
56
+ (coll ? 'Presence' : 'Default') + '</th></tr></thead><tbody>' +
57
+ rows + '</tbody></table>' + idx + '</div></details>';
58
+ }
59
+
60
+ // ─── Database Studio: media preview ────────────────────────────────────────
61
+ var SOURCE_LABEL = { url: 'Image URL', gcs: 'Cloud Storage URI', base64: 'Base64 string', bytes: 'Binary (Bytes)' };
62
+
63
+ function thumbHtml(m) {
64
+ if (m && isPreviewableUrl(m.sampleUrl)) {
65
+ return '<img src="' + esc(m.sampleUrl) + '" alt="" loading="lazy" referrerpolicy="no-referrer" data-thumb>';
66
+ }
67
+ var why = !m ? 'No sample available'
68
+ : m.source === 'gcs' ? 'gs:// URIs need a signed URL to preview'
69
+ : (m.source === 'bytes' || m.source === 'base64') ? 'Binary preview withheld (metadata only)'
70
+ : 'No preview available';
71
+ return '<div class="ph">' + esc(why) + '</div>';
72
+ }
73
+
74
+ // Swap broken thumbnails (auth-protected URLs or blocked by the page's content policy) for a note.
75
+ document.addEventListener('error', function (e) {
76
+ var img = e.target;
77
+ if (img && img.tagName === 'IMG' && img.hasAttribute('data-thumb')) {
78
+ var ph = document.createElement('div');
79
+ ph.className = 'ph';
80
+ ph.textContent = 'Preview unavailable (URL needs auth, was redacted, or is blocked by the studio content policy)';
81
+ img.replaceWith(ph);
82
+ }
83
+ }, true);
84
+
85
+ function mediaSummary(m) {
86
+ if (!m) return '';
87
+ var parts = [m.mimeType || 'unknown type'];
88
+ if (m.width && m.height) parts.push(m.width + '×' + m.height);
89
+ if (m.sizeBytes != null) parts.push(formatBytes(m.sizeBytes));
90
+ return parts.join(' · ');
91
+ }
92
+
93
+ function showThumb(el) {
94
+ var entry = mediaRegistry[Number(el.getAttribute('data-media'))];
95
+ if (!entry) return;
96
+ var pop = $('thumb-pop');
97
+ pop.innerHTML = '<div class="thumb-box">' + thumbHtml(entry.media) + '</div><div class="thumb-meta"><b>' + esc(entry.table + '.' + entry.field) +
98
+ '</b><br>' + esc(mediaSummary(entry.media)) + '</div>';
99
+ pop.hidden = false;
100
+ // Place beside the badge so the field names in the row stay readable; flip left near the edge.
101
+ var r = el.getBoundingClientRect();
102
+ var w = pop.offsetWidth, h = pop.offsetHeight;
103
+ var left = r.right + 12 + w > window.innerWidth ? r.left - w - 12 : r.right + 12;
104
+ var top = Math.min(r.top + r.height / 2 - h / 2, window.innerHeight - h - 8);
105
+ pop.style.left = Math.max(8, left) + 'px';
106
+ pop.style.top = Math.max(8, top) + 'px';
107
+ }
108
+
109
+ function hideThumb() {
110
+ var pop = $('thumb-pop');
111
+ if (pop) { pop.hidden = true; pop.innerHTML = ''; }
112
+ }
113
+
114
+ function openMedia(index) {
115
+ var entry = mediaRegistry[index];
116
+ if (!entry) return;
117
+ hideThumb();
118
+ var m = entry.media;
119
+ $('media-title').textContent = entry.table + '.' + entry.field;
120
+ $('media-sub').textContent = (m ? SOURCE_LABEL[m.source] || m.source : 'Image field') + ' · ' + entry.type;
121
+ $('media-thumb').innerHTML = thumbHtml(m);
122
+ var rows = [];
123
+ if (m) {
124
+ if (m.sampleUrl) rows.push(['Sample ' + (m.source === 'gcs' ? 'URI' : 'URL'), '<code>' + esc(m.sampleUrl) + '</code>']);
125
+ rows.push(['MIME type', esc(m.mimeType || 'unknown')]);
126
+ rows.push(['Size', esc(m.sizeBytes != null ? formatBytes(m.sizeBytes) : 'unknown')]);
127
+ rows.push(['Dimensions', esc(m.width && m.height ? m.width + ' × ' + m.height + ' px' : 'unknown')]);
128
+ // Base64 is truncated to a short prefix server-side; render it as plain text only.
129
+ if (m.base64Head) rows.push(['Base64 (truncated)', '<code>' + esc(m.base64Head) + '</code>']);
130
+ }
131
+ $('media-details').innerHTML = rows.map(function (r) { return '<dt>' + esc(r[0]) + '</dt><dd>' + r[1] + '</dd>'; }).join('');
132
+ openDialog($('media-dialog'));
133
+ }
134
+
135
+ function paneHtml(env, tables, badges) {
136
+ var s = state.status && state.status[env];
137
+ var title = env === 'dev' ? 'Dev / Staging' : 'Production';
138
+ var q = state.search.trim().toLowerCase();
139
+ var filtered = tables.filter(function (t) {
140
+ if (!q) return true;
141
+ if (t.name.toLowerCase().indexOf(q) !== -1) return true;
142
+ return t.columns.some(function (c) { return c.name.toLowerCase().indexOf(q) !== -1; });
143
+ });
144
+ var body;
145
+ if (!s || !s.connected) {
146
+ body = '<div class="empty" style="padding:28px 12px"><p>' + esc(s && s.error ? s.error : 'No database connected.') + '</p>' +
147
+ '<button class="btn small" type="button" data-action="open-settings">Connect</button></div>';
148
+ } else if (!tables.length) {
149
+ body = '<p class="muted">Connected, but this database has no ' + entityNoun(s, true).toLowerCase() + ' yet.</p>';
150
+ } else if (!filtered.length) {
151
+ body = '<p class="muted">No ' + entityNoun(s, true).toLowerCase() + ' or fields match “' + esc(state.search) + '”.</p>';
152
+ } else {
153
+ body = filtered.map(function (t) { return tableCard(t, badges[t.name.toLowerCase()]); }).join('');
154
+ }
155
+ return '<section class="pane card" aria-labelledby="pane-' + env + '">' +
156
+ '<div class="pane-head"><h2 id="pane-' + env + '"><span class="dot ' + (s && s.connected ? 'on' : 'off') + '"></span>' + title + '</h2>' +
157
+ '<span class="meta">' + (s && s.connected ? esc(s.maskedUrl) : '') + '</span></div>' + body + '</section>';
158
+ }
159
+
160
+ function renderExplorer() {
161
+ var st = state.status || {};
162
+ if (!(st.dev && st.dev.connected) && !(st.prod && st.prod.connected)) {
163
+ return emptyState('No database connected', 'No database connected. Click Connection Settings to connect a local or remote database.');
164
+ }
165
+ var badges = diffBadges();
166
+ var panesHtml = '';
167
+ if (state.envMode === 'dev') {
168
+ panesHtml = '<div class="panes panes-full">' + paneHtml('dev', state.schema.devTables || [], badges.dev) + '</div>';
169
+ } else if (state.envMode === 'prod') {
170
+ panesHtml = '<div class="prod-banner"><span><b>Production Focus Active:</b> Browsing production schema and collections. Any data modifications require challenge phrase confirmation.</span>' +
171
+ '<span class="badge badge-prod">PROD</span></div>' +
172
+ '<div class="panes panes-full">' + paneHtml('prod', state.schema.prodTables || [], badges.prod) + '</div>';
173
+ } else {
174
+ panesHtml = '<div class="panes">' + paneHtml('dev', state.schema.devTables || [], badges.dev) + paneHtml('prod', state.schema.prodTables || [], badges.prod) + '</div>';
175
+ }
176
+
177
+ return '<div class="toolbar">' +
178
+ '<label class="sr-only" for="search">Search tables, collections and fields</label>' +
179
+ '<input class="input search" id="search" type="search" placeholder="Search tables, collections or fields…" value="' + esc(state.search) + '">' +
180
+ '</div>' + panesHtml;
181
+ }
182
+
183
+ `;
@@ -0,0 +1,2 @@
1
+ import type { PageModule } from '../types.js';
2
+ export declare const databasePage: PageModule;
@@ -0,0 +1,28 @@
1
+ import { ICON_PATHS } from '../../shell/icons.js';
2
+ import { databaseCss } from './styles.js';
3
+ import { databaseMarkup } from './markup.js';
4
+ import { databaseStateScript, databaseRegisterScript } from './state.js';
5
+ import { databaseDataScript } from './data-shell.js';
6
+ import { databaseExplorerScript } from './explorer.js';
7
+ import { databaseBrowserScript } from './browser.js';
8
+ import { databaseDriftScript } from './drift.js';
9
+ import { databaseConnectionScript } from './connection.js';
10
+ import { databaseEventsScript } from './events.js';
11
+ export const databasePage = {
12
+ id: 'database',
13
+ group: 'system',
14
+ label: 'Database',
15
+ icon: ICON_PATHS.database,
16
+ css: databaseCss,
17
+ markup: databaseMarkup,
18
+ script: [
19
+ databaseStateScript,
20
+ databaseDataScript,
21
+ databaseExplorerScript,
22
+ databaseBrowserScript,
23
+ databaseDriftScript,
24
+ databaseConnectionScript,
25
+ databaseEventsScript,
26
+ databaseRegisterScript,
27
+ ].join(''),
28
+ };
@@ -0,0 +1 @@
1
+ export declare const databaseMarkup: string;
@@ -0,0 +1,32 @@
1
+ // Database Studio panel content; the shell supplies the surrounding section.
2
+ export const databaseMarkup = String.raw ` <div class="panel-head">
3
+ <div class="panel-title">
4
+ <h1>Database Studio</h1>
5
+ <p class="lead">Schema inspector, live data browser, drift tracker and migration preview. Connection strings stay inside this local process.</p>
6
+ </div>
7
+ <div class="panel-actions">
8
+ <button class="btn" id="btn-settings" type="button">Connection Settings</button>
9
+ <button class="btn primary" id="btn-export" type="button">Export Contract</button>
10
+ </div>
11
+ </div>
12
+ <div class="telemetry" id="telemetry" aria-label="Database telemetry"></div>
13
+
14
+ <div class="env-toggle-bar">
15
+ <div class="env-segmented" role="group" aria-label="Environment Focus Mode">
16
+ <button type="button" class="env-seg-btn" data-env-mode="dev" aria-pressed="true">Staging / Dev</button>
17
+ <button type="button" class="env-seg-btn" data-env-mode="prod" aria-pressed="false">Production</button>
18
+ <button type="button" class="env-seg-btn" data-env-mode="split" aria-pressed="false">Split Comparison</button>
19
+ </div>
20
+ </div>
21
+
22
+ <div id="diagnostic-banner" aria-live="polite" hidden></div>
23
+
24
+ <nav class="tabs" role="tablist" aria-label="Database views">
25
+ <button class="subtab" role="tab" id="tab-explorer" aria-controls="view" data-tab="explorer">Schema &amp; Structure <span class="count" id="count-explorer">0</span></button>
26
+ <button class="subtab" role="tab" id="tab-data" aria-controls="view" data-tab="data">Live Data Browser <span class="count" id="count-data">0</span></button>
27
+ <button class="subtab" role="tab" id="tab-drift" aria-controls="view" data-tab="drift">Schema Changes (Live vs Blueprint) <span class="count" id="count-drift">0</span></button>
28
+ <button class="subtab" role="tab" id="tab-sql" aria-controls="view" data-tab="sql">Migration Script Preview <span class="count" id="count-sql">0</span></button>
29
+ </nav>
30
+
31
+ <div id="view" role="tabpanel" tabindex="-1"></div>
32
+ `;
@@ -0,0 +1,2 @@
1
+ export declare const databaseStateScript: string;
2
+ export declare const databaseRegisterScript: string;
@@ -0,0 +1,57 @@
1
+ // Database Studio state and small helpers, private to the Database page.
2
+ export const databaseStateScript = String.raw ` var ENGINE_LABEL = { postgresql: 'PostgreSQL', mysql: 'MySQL', sqlite: 'SQLite', mongodb: 'MongoDB', firestore: 'Firebase Firestore' };
3
+ var NOSQL = { mongodb: true, firestore: true };
4
+ var state = {
5
+ tab: 'explorer',
6
+ envMode: 'dev',
7
+ dbStarted: false,
8
+ loading: true,
9
+ status: null,
10
+ schema: { devTables: [], prodTables: [] },
11
+ diff: null,
12
+ diffError: null,
13
+ diffTarget: 'prod',
14
+ search: '',
15
+ mobileEnv: 'dev',
16
+ sql: '',
17
+ scriptLang: 'sql',
18
+ dataEnv: 'dev',
19
+ dataEntity: '',
20
+ dataPage: 1,
21
+ dataLimit: 25,
22
+ dataSort: '',
23
+ dataOrder: 'asc',
24
+ dataSearch: '',
25
+ dataLoading: false,
26
+ dataResult: null,
27
+ dataError: null,
28
+ editingPk: null,
29
+ pendingMutation: null,
30
+ envInfo: null
31
+ };
32
+ // Media fields rendered in the current view, referenced by index from IMAGE badges.
33
+ var mediaRegistry = [];
34
+
35
+ function isCollection(t) { return !!t && t.entityType === 'collection'; }
36
+ function entityCount(s) { return s.entityCount != null ? s.entityCount : s.tableCount; }
37
+ function entityNoun(s, plural) {
38
+ var coll = s && (s.entityType === 'collection' || NOSQL[s.engine]);
39
+ return coll ? (plural ? 'Collections' : 'Collection') : (plural ? 'Tables' : 'Table');
40
+ }
41
+ /** Noun for drift views: collections when the compared Dev database is a document store. */
42
+ function driftNoun(plural) {
43
+ var dev = state.status && state.status.dev;
44
+ return entityNoun(dev, plural).toLowerCase();
45
+ }
46
+ function isPreviewableUrl(u) { return typeof u === 'string' && /^https?:\/\//i.test(u); }
47
+
48
+ `;
49
+ // Loads on first visit only: the page connects to live databases.
50
+ export const databaseRegisterScript = String.raw `
51
+ Studio.registerPage('database', {
52
+ deps: [],
53
+ onShow: function () {
54
+ if (!state.dbStarted) { state.dbStarted = true; load(false); } else renderDb();
55
+ }
56
+ });
57
+ `;
@@ -0,0 +1 @@
1
+ export declare const databaseCss: string;
@@ -0,0 +1,118 @@
1
+ // Database Studio view styles.
2
+ export const databaseCss = String.raw `/* Database Studio */
3
+ .telemetry { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 4px; }
4
+ .pill { display: inline-flex; align-items: center; gap: 8px; max-width: 100%; padding: 4px 10px; font-size: 13px; background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-control); box-shadow: var(--shadow); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
5
+ .pill b { font-weight: 600; }
6
+ .diagnostic-banner { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 12px 16px; margin: 12px 0 16px; background: var(--danger-bg); border: 1px solid var(--danger-line); border-radius: var(--r-control); }
7
+ .diagnostic-content { display: flex; align-items: center; gap: 10px; font-size: 13.5px; }
8
+ .diagnostic-tag { font-family: var(--mono); font-size: 11px; font-weight: 700; color: var(--danger); background: var(--surface); border: 1px solid var(--danger-line); padding: 0 7px; border-radius: var(--r-control); }
9
+ .diagnostic-msg code { background: var(--surface); padding: 1px 6px; border-radius: 4px; font-family: var(--mono); }
10
+ .diagnostic-actions { display: flex; gap: 8px; }
11
+ .template-helper-box { background: var(--active-bg); border: 1px solid var(--active-line); border-radius: var(--r-control); padding: 12px 14px; margin-bottom: 16px; }
12
+ .template-helper-label { font-size: 12.5px; color: var(--muted); margin-bottom: 8px; }
13
+ .template-helper-label code { color: var(--active); font-weight: 600; }
14
+ .template-chips { display: flex; flex-wrap: wrap; gap: 8px; }
15
+ .template-chip { font-family: var(--mono); font-size: 12.5px; }
16
+ .template-chip:hover { border-color: var(--active); }
17
+ .chip-dot { width: 6px; height: 6px; border-radius: 50%; display: inline-block; }
18
+ .chip-dot.on { background: var(--success); }
19
+ .chip-dot.off { background: var(--danger); }
20
+
21
+ .thumb-pop { position: fixed; z-index: 60; width: 220px; padding: 8px; border-radius: var(--r-card); border: 1px solid var(--border); background: var(--surface); box-shadow: var(--shadow-float); pointer-events: none; }
22
+ .thumb-box { display: grid; place-items: center; min-height: 120px; max-height: 200px; overflow: hidden; border-radius: var(--r-control); background: repeating-conic-gradient(var(--chip) 0% 25%, var(--surface) 0% 50%) 50% / 16px 16px; }
23
+ .thumb-box img { max-width: 100%; max-height: 200px; display: block; }
24
+ .thumb-box .ph { color: var(--muted); font-size: 12px; text-align: center; padding: 12px; }
25
+ .thumb-meta { margin-top: 6px; font-size: 11.5px; color: var(--muted); overflow-wrap: anywhere; }
26
+ .media-modal .thumb-box { min-height: 220px; max-height: 360px; }
27
+ .media-modal .thumb-box img { max-height: 360px; }
28
+ .media-modal dl { display: grid; grid-template-columns: max-content 1fr; gap: 6px 14px; margin: 14px 0 0; font-size: 13px; }
29
+ .media-modal dt { color: var(--muted); }
30
+ .media-modal dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }
31
+ .media-note { margin-top: 12px; font-size: 13px; line-height: 1.5; color: var(--muted); }
32
+
33
+ .toolbar { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin-bottom: 14px; }
34
+ .input { min-height: 32px; padding: 6px 10px; background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-control); min-width: 0; }
35
+ .input:focus { border-color: var(--primary); outline: none; box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 15%, transparent); }
36
+ textarea.input { width: 100%; resize: vertical; line-height: 1.5; }
37
+ .search { flex: 1; min-width: 180px; max-width: 420px; }
38
+ .panes { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
39
+ .panes-full { grid-template-columns: 1fr; }
40
+ .pane { padding: 16px; min-width: 0; }
41
+ .pane-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 12px; }
42
+ .pane-head h2 { margin: 0; font-size: 15.5px; display: flex; align-items: center; gap: 8px; white-space: nowrap; flex: none; }
43
+ .pane-head .meta { color: var(--muted); font-size: 12.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
44
+ .table-card { border: 1px solid var(--border); border-radius: var(--r-control); margin-bottom: 8px; background: var(--surface); }
45
+ .table-card > summary { list-style: none; cursor: pointer; padding: 10px 12px; display: flex; align-items: center; gap: 8px; border-radius: var(--r-control); }
46
+ .table-card > summary::-webkit-details-marker { display: none; }
47
+ .table-card > summary:hover { background: var(--bg); }
48
+ .table-card > summary::before { content: "\25B8"; color: var(--muted); transition: transform .15s; }
49
+ .table-card[open] > summary::before { transform: rotate(90deg); }
50
+ .table-card .tname { font-family: var(--mono); font-weight: 600; overflow: hidden; text-overflow: ellipsis; }
51
+ .table-card .ccount { margin-left: auto; color: var(--muted); font-size: 12.5px; white-space: nowrap; }
52
+ .table-body { padding: 0 12px 12px; overflow-x: auto; }
53
+ .desc { color: var(--muted); margin: 0 0 8px; font-size: 13.5px; line-height: 1.5; }
54
+ table.cols { width: 100%; border-collapse: collapse; font-size: 13px; }
55
+ table.cols th { text-align: left; color: var(--muted); font-weight: 500; font-size: 12.5px; padding: 6px 8px; border-bottom: 1px solid var(--border); white-space: nowrap; }
56
+ table.cols td { padding: 6px 8px; border-bottom: 1px solid var(--border); vertical-align: top; }
57
+ table.cols td.mono { white-space: nowrap; }
58
+ .subhead { font-size: 12.5px; color: var(--muted); margin: 12px 0 6px; text-transform: uppercase; letter-spacing: .05em; }
59
+ .idx-list { margin: 0; padding-left: 18px; font-size: 13px; }
60
+
61
+ .summary-banner { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 12px; padding: 16px; margin-bottom: 16px; }
62
+ .stat { padding: 4px 8px; }
63
+ .stat .n { font-size: 26px; font-weight: 650; line-height: 1.1; font-variant-numeric: tabular-nums; }
64
+ .stat .l { color: var(--muted); font-size: 12.5px; }
65
+ .risk-HIGH { color: var(--danger); } .risk-MEDIUM { color: var(--active); } .risk-LOW { color: var(--success); } .risk-NONE { color: var(--muted); }
66
+ .alert-danger { border: 1px solid var(--danger-line); background: var(--danger-bg); border-radius: var(--r-control); padding: 10px 12px; margin-bottom: 12px; }
67
+ .alert-danger strong { color: var(--danger); }
68
+ .diff-table td.before { color: var(--danger); } .diff-table td.after { color: var(--success); }
69
+ .row-added td { background: var(--success-bg); } .row-dropped td { background: var(--danger-bg); } .row-altered td { background: var(--active-bg); }
70
+ .chip-list { display: flex; flex-wrap: wrap; gap: 6px; }
71
+ .chip { font-family: var(--mono); font-size: 12px; padding: 1px 8px; border-radius: var(--r-control); border: 1px solid var(--chip-border); background: var(--chip); }
72
+
73
+ .sql-wrap { position: relative; padding: 0; overflow: hidden; }
74
+ .sql-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 10px 14px; border-bottom: 1px solid var(--border); }
75
+ pre.sql { margin: 0; padding: 16px; overflow: auto; max-height: 70vh; background: var(--bg); font-family: var(--mono); font-size: 12.5px; line-height: 1.6; white-space: pre; }
76
+ .sql .k { color: var(--primary); font-weight: 600; } .sql .s { color: var(--success); } .sql .c { color: var(--muted); font-style: italic; } .sql .d { color: var(--danger); font-weight: 700; }
77
+ .sql .p { color: var(--active); } .sql .n { color: var(--active); font-weight: 600; } .sql .o { color: var(--primary); }
78
+ .lang { font-family: var(--mono); font-size: 11px; padding: 0 7px; border-radius: var(--r-control); border: 1px solid var(--primary-line); background: var(--primary-tint); color: var(--primary); margin-right: 6px; }
79
+ .copy-wrap { position: relative; display: inline-flex; }
80
+ .tooltip { position: absolute; bottom: calc(100% + 6px); left: 50%; transform: translateX(-50%); background: var(--success); color: #fff; font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: var(--r-control); pointer-events: none; opacity: 0; transition: opacity .15s; white-space: nowrap; }
81
+ .tooltip.show { opacity: 1; }
82
+
83
+ .empty { text-align: center; padding: 48px 24px; }
84
+ .empty .empty-icon { color: var(--primary); margin: 0 auto 12px; }
85
+ .empty h3 { margin: 0 0 6px; font-size: 16px; }
86
+ .empty p { color: var(--muted); margin: 0 0 16px; font-size: 15px; line-height: 1.55; }
87
+
88
+ .env-toggle-bar { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 12px; margin: 14px 0 6px; }
89
+ .env-segmented { display: inline-flex; gap: 2px; padding: 3px; background: var(--chip); border: 1px solid var(--border); border-radius: var(--r-control); }
90
+ .env-seg-btn { border: 1px solid transparent; background: transparent; padding: 4px 14px; border-radius: 5px; cursor: pointer; color: var(--muted); font-size: 13px; font-weight: 600; transition: all .15s ease; }
91
+ .env-seg-btn:hover { color: var(--text); }
92
+ .env-seg-btn[aria-pressed="true"] { background: var(--surface); color: var(--primary); border-color: var(--border); box-shadow: var(--shadow); }
93
+ .env-seg-btn[data-env-mode="prod"][aria-pressed="true"] { color: var(--danger); background: var(--danger-bg); border-color: var(--danger-line); }
94
+ .prod-banner { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 16px; border-radius: var(--r-control); border: 1px solid var(--danger-line); background: var(--danger-bg); font-size: 13px; margin-bottom: 14px; }
95
+ .prod-banner b { color: var(--danger); font-weight: 600; }
96
+
97
+ .tabs { display: flex; gap: 4px; margin: 16px 0; border-bottom: 1px solid var(--border); overflow-x: auto; overflow-y: hidden; }
98
+ .subtab { background: none; border: none; padding: 10px 12px; cursor: pointer; color: var(--muted); border-bottom: 2px solid transparent; margin-bottom: -1px; white-space: nowrap; display: inline-flex; align-items: center; gap: 8px; font-size: 13.5px; font-weight: 500; }
99
+ .subtab:hover { color: var(--text); }
100
+ .subtab[aria-selected="true"] { color: var(--text); border-bottom-color: var(--primary); }
101
+
102
+ .data-toolbar { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 14px; }
103
+ .data-controls { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; flex: 1; min-width: 0; }
104
+ .data-grid-wrap { overflow-x: auto; max-width: 100%; border: 1px solid var(--border); border-radius: var(--r-card); background: var(--surface); box-shadow: var(--shadow); margin-bottom: 14px; min-height: 240px; }
105
+ table.data-table { width: 100%; border-collapse: collapse; font-size: 13px; text-align: left; }
106
+ table.data-table th { background: var(--bg); color: var(--muted); font-weight: 600; font-size: 12.5px; padding: 10px 12px; border-bottom: 1px solid var(--border); position: sticky; top: 0; z-index: 2; white-space: nowrap; user-select: none; }
107
+ table.data-table th.sortable { cursor: pointer; }
108
+ table.data-table th.sortable:hover { color: var(--text); background: var(--chip); }
109
+ table.data-table td { padding: 8px 12px; border-bottom: 1px solid var(--border); max-width: 320px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; vertical-align: middle; }
110
+ table.data-table tr:hover td { background: var(--bg); }
111
+ .th-sort-icon { font-size: 10px; margin-left: 4px; color: var(--primary); }
112
+ .td-actions { display: flex; gap: 6px; align-items: center; white-space: nowrap; }
113
+ .pagination-bar { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 12px; padding: 8px 4px; font-size: 13px; color: var(--muted); }
114
+ .pagination-ctrls { display: flex; align-items: center; gap: 8px; }
115
+ .cell-preview-btn { padding: 1px 6px; font-size: 11px; border-radius: 4px; border: 1px solid var(--primary-line); color: var(--primary); background: var(--primary-tint); cursor: pointer; }
116
+ .cell-preview-btn:hover { background: var(--surface); }
117
+
118
+ `;
@@ -0,0 +1,2 @@
1
+ import type { PageModule } from '../types.js';
2
+ export declare const flowPage: PageModule;
@@ -0,0 +1,11 @@
1
+ import { ICON_PATHS } from '../../shell/icons.js';
2
+ import { flowScript } from './script.js';
3
+ import { flowCss } from './styles.js';
4
+ export const flowPage = {
5
+ id: 'flow',
6
+ group: 'now',
7
+ label: 'Flow',
8
+ icon: ICON_PATHS.flow,
9
+ css: flowCss,
10
+ script: flowScript,
11
+ };
@@ -0,0 +1 @@
1
+ export declare const flowLayoutScript: string;
@@ -0,0 +1,105 @@
1
+ // Plan layout: rounds, lanes, links and the highlight set. Pure, no DOM.
2
+ export const flowLayoutScript = String.raw ` var FLOW = { W: 206, H: 58, GX: 52, GY: 10, PAD: 14, LX: 128, TOP: 30 };
3
+
4
+ function flowAgentKey(t) { return t.agent || 'unassigned'; }
5
+
6
+ function flowGraph(m) {
7
+ var ids = {};
8
+ m.tasks.forEach(function (t) { ids[t.id] = t; });
9
+ var depsIn = function (id) {
10
+ return ids[id].deps.filter(function (d) { return ids[d] && d !== id; });
11
+ };
12
+ var depth = {}, anc = {};
13
+ function getD(id, seen) {
14
+ if (depth[id] != null) return depth[id];
15
+ if (seen[id]) return 0;
16
+ seen[id] = true;
17
+ var d = 0;
18
+ depsIn(id).forEach(function (p) { d = Math.max(d, 1 + getD(p, seen)); });
19
+ delete seen[id];
20
+ depth[id] = d;
21
+ return d;
22
+ }
23
+ function getA(id, seen) {
24
+ if (anc[id]) return anc[id];
25
+ if (seen[id]) return {};
26
+ seen[id] = true;
27
+ var out = {};
28
+ depsIn(id).forEach(function (p) {
29
+ out[p] = true;
30
+ var up = getA(p, seen);
31
+ Object.keys(up).forEach(function (k) { out[k] = true; });
32
+ });
33
+ delete seen[id];
34
+ anc[id] = out;
35
+ return out;
36
+ }
37
+ m.tasks.forEach(function (t) { getD(t.id, {}); getA(t.id, {}); });
38
+ var edges = [], kids = {};
39
+ m.tasks.forEach(function (t) {
40
+ depsIn(t.id).forEach(function (d) {
41
+ var implied = depsIn(t.id).some(function (e) { return e !== d && getA(e, {})[d]; });
42
+ edges.push({ from: d, to: t.id, implied: implied });
43
+ (kids[d] = kids[d] || []).push(t.id);
44
+ });
45
+ });
46
+ return { depth: depth, anc: function (id) { return getA(id, {}); }, edges: edges, kids: kids };
47
+ }
48
+
49
+ function flowPlace(m, g) {
50
+ var lanes = [], cells = {};
51
+ m.tasks.forEach(function (t) {
52
+ var a = flowAgentKey(t);
53
+ if (lanes.indexOf(a) === -1) lanes.push(a);
54
+ var k = a + '|' + g.depth[t.id];
55
+ (cells[k] = cells[k] || []).push(t);
56
+ });
57
+ var rounds = Math.max.apply(null, m.tasks.map(function (t) { return g.depth[t.id]; })) + 1;
58
+ var laneY = {}, laneH = {}, y = FLOW.TOP;
59
+ lanes.forEach(function (l) {
60
+ var stack = 1;
61
+ for (var r = 0; r < rounds; r++) stack = Math.max(stack, (cells[l + '|' + r] || []).length);
62
+ laneY[l] = y;
63
+ laneH[l] = FLOW.PAD * 2 + stack * FLOW.H + (stack - 1) * FLOW.GY;
64
+ y += laneH[l];
65
+ });
66
+ var pos = {};
67
+ Object.keys(cells).forEach(function (k) {
68
+ var bits = k.split('|'), r = Number(bits[1]);
69
+ cells[k].forEach(function (t, i) {
70
+ pos[t.id] = { x: FLOW.LX + 20 + r * (FLOW.W + FLOW.GX), y: laneY[bits[0]] + FLOW.PAD + i * (FLOW.H + FLOW.GY), round: r + 1 };
71
+ });
72
+ });
73
+ return { lanes: lanes, laneY: laneY, laneH: laneH, pos: pos, rounds: rounds,
74
+ width: FLOW.LX + 20 + rounds * (FLOW.W + FLOW.GX), height: y };
75
+ }
76
+
77
+ /** Everything the Plan view draws for one milestone. */
78
+ function flowLayout(m) {
79
+ var g = flowGraph(m);
80
+ var L = flowPlace(m, g);
81
+ L.edges = g.edges;
82
+ L.kids = g.kids;
83
+ L.anc = g.anc;
84
+ L.hidden = g.edges.filter(function (e) { return e.implied; }).length;
85
+ return L;
86
+ }
87
+
88
+ /** The task, everything it needs and everything it unblocks, or null when it is not drawn. */
89
+ function flowFocusSet(L, id) {
90
+ if (!id || !L.pos[id]) return null;
91
+ var hot = {}, down = {}, stack = [id];
92
+ hot[id] = true;
93
+ Object.keys(L.anc(id)).forEach(function (k) { hot[k] = true; });
94
+ while (stack.length) {
95
+ (L.kids[stack.pop()] || []).forEach(function (c) {
96
+ if (down[c]) return;
97
+ down[c] = true;
98
+ hot[c] = true;
99
+ stack.push(c);
100
+ });
101
+ }
102
+ return hot;
103
+ }
104
+
105
+ `;
@@ -0,0 +1 @@
1
+ export declare const flowPlanScript: string;
@@ -0,0 +1,67 @@
1
+ // Plan view: swimlanes per agent, one column per round, bezier links inside the milestone.
2
+ export const flowPlanScript = String.raw ` function flowWires(L, hot, showAll) {
3
+ return L.edges.filter(function (e) { return showAll || !e.implied; }).map(function (e) {
4
+ var a = L.pos[e.from], b = L.pos[e.to];
5
+ var x1 = a.x + FLOW.W, y1 = a.y + FLOW.H / 2, x2 = b.x, y2 = b.y + FLOW.H / 2;
6
+ var dx = Math.max(28, (x2 - x1) / 2);
7
+ var on = hot && hot[e.from] && hot[e.to];
8
+ return '<path class="wire' + (on ? ' hot' : '') + (e.implied ? ' implied' : '') + '" data-from="' + ui.esc(e.from) +
9
+ '" data-to="' + ui.esc(e.to) + '" d="M' + x1 + ' ' + y1 + ' C' + (x1 + dx) + ' ' + y1 + ', ' + (x2 - dx) + ' ' + y2 + ', ' + x2 + ' ' + y2 + '"/>';
10
+ }).join('');
11
+ }
12
+
13
+ function flowBands(m, L) {
14
+ return L.lanes.map(function (l) {
15
+ var count = m.tasks.filter(function (t) { return flowAgentKey(t) === l; }).length;
16
+ return '<div class="lane-band" data-lane="' + ui.esc(l) + '" style="top:' + L.laneY[l] + 'px;height:' + L.laneH[l] + 'px">' +
17
+ '<div class="lane-label">' + ui.agentChip(l === 'unassigned' ? null : l) + '<span class="faint">' + count + (count === 1 ? ' task' : ' tasks') + '</span></div></div>';
18
+ }).join('');
19
+ }
20
+
21
+ function flowRoundHeads(L) {
22
+ var out = '';
23
+ for (var r = 0; r < L.rounds; r++) {
24
+ out += '<div class="round-head" style="left:' + (FLOW.LX + 20 + r * (FLOW.W + FLOW.GX)) + 'px">Round ' + (r + 1) + '</div>';
25
+ }
26
+ return out;
27
+ }
28
+
29
+ function flowNodeTime(t) {
30
+ if (t.status === 'in_progress' && t.startT) return '<span data-since="' + t.startT + '">' + ui.esc(fmt.dur(Studio.now() - t.startT)) + '</span>';
31
+ return t.durationMs ? ui.esc(fmt.dur(t.durationMs)) : '';
32
+ }
33
+
34
+ function flowNodes(m, L, hot, focusId) {
35
+ return m.tasks.map(function (t) {
36
+ var p = L.pos[t.id];
37
+ var cls = 'node ' + t.status + (hot && hot[t.id] ? ' hot' : '') + (focusId === t.id ? ' sel' : '');
38
+ return '<button type="button" class="' + ui.esc(cls) + '" data-act="flow-focus" data-key="' + ui.esc(t.id) + '" data-id="' + ui.esc(t.id) +
39
+ '" data-round="' + p.round + '" aria-pressed="' + (focusId === t.id) + '" title="' + ui.esc(t.title) + '" style="left:' + p.x + 'px;top:' + p.y +
40
+ 'px;width:' + FLOW.W + 'px;height:' + FLOW.H + 'px"><div class="l1">' + ui.statusIcon(t.status) + '<span class="mono faint">' + ui.esc(t.id) +
41
+ '</span><span class="d">' + flowNodeTime(t) + '</span></div><div class="title">' + ui.esc(t.title) + '</div></button>';
42
+ }).join('');
43
+ }
44
+
45
+ function flowLegend() {
46
+ var one = function (s) { return '<span>' + ui.statusIcon(s) + Studio.statusLabel(s) + '</span>'; };
47
+ return '<div class="legend">' + ['completed', 'in_progress', 'pending', 'blocked'].map(one).join('') + '</div>';
48
+ }
49
+
50
+ function flowPlanTools(L, hot, view) {
51
+ var hint = hot ? '<button type="button" class="btn ghost" data-act="flow-clear">Clear highlight</button>'
52
+ : '<span>Click a task to light up what it needs and what it unblocks</span>';
53
+ var implied = L.hidden ? '<label class="check"><input type="checkbox" data-act="flow-implied"' + (view.implied ? ' checked' : '') +
54
+ '> Show ' + L.hidden + ' implied link' + (L.hidden > 1 ? 's' : '') + '</label>' : '';
55
+ return '<div class="fl-bar">' + flowLegend() + '<span class="right">' + hint + implied + '</span></div>';
56
+ }
57
+
58
+ function flowPlan(m, focusId, view) {
59
+ var L = flowLayout(m);
60
+ var hot = flowFocusSet(L, focusId);
61
+ return '<section class="fl-panel" data-mode="plan">' + flowPlanTools(L, hot, view) +
62
+ '<div class="flow-wrap"><div class="canvas' + (hot ? ' focus' : '') + '" style="width:' + L.width + 'px;height:' + (L.height + 10) + 'px">' +
63
+ flowBands(m, L) + flowRoundHeads(L) + '<svg class="wires" width="' + L.width + '" height="' + L.height + '">' +
64
+ flowWires(L, hot, view.implied) + '</svg>' + flowNodes(m, L, hot, hot ? focusId : null) + '</div></div></section>';
65
+ }
66
+
67
+ `;