@njeybe/nativ 2.2.0 → 2.3.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 (138) hide show
  1. package/CHANGELOG.md +14 -1
  2. package/README.md +15 -0
  3. package/dist/server/studio-ui/document.d.ts +3 -0
  4. package/dist/server/studio-ui/document.js +22 -0
  5. package/dist/server/studio-ui/pages/benchmarks/index.d.ts +2 -0
  6. package/dist/server/studio-ui/pages/benchmarks/index.js +11 -0
  7. package/dist/server/studio-ui/pages/benchmarks/script.d.ts +1 -0
  8. package/dist/server/studio-ui/pages/benchmarks/script.js +104 -0
  9. package/dist/server/studio-ui/pages/benchmarks/styles.d.ts +1 -0
  10. package/dist/server/studio-ui/pages/benchmarks/styles.js +30 -0
  11. package/dist/server/studio-ui/pages/database/browser.d.ts +1 -0
  12. package/dist/server/studio-ui/pages/database/browser.js +326 -0
  13. package/dist/server/studio-ui/pages/database/connection.d.ts +1 -0
  14. package/dist/server/studio-ui/pages/database/connection.js +158 -0
  15. package/dist/server/studio-ui/pages/database/data-shell.d.ts +1 -0
  16. package/dist/server/studio-ui/pages/database/data-shell.js +165 -0
  17. package/dist/server/studio-ui/pages/database/drift.d.ts +1 -0
  18. package/dist/server/studio-ui/pages/database/drift.js +301 -0
  19. package/dist/server/studio-ui/pages/database/events.d.ts +1 -0
  20. package/dist/server/studio-ui/pages/database/events.js +231 -0
  21. package/dist/server/studio-ui/pages/database/explorer.d.ts +1 -0
  22. package/dist/server/studio-ui/pages/database/explorer.js +183 -0
  23. package/dist/server/studio-ui/pages/database/index.d.ts +2 -0
  24. package/dist/server/studio-ui/pages/database/index.js +28 -0
  25. package/dist/server/studio-ui/pages/database/markup.d.ts +1 -0
  26. package/dist/server/studio-ui/pages/database/markup.js +32 -0
  27. package/dist/server/studio-ui/pages/database/state.d.ts +2 -0
  28. package/dist/server/studio-ui/pages/database/state.js +57 -0
  29. package/dist/server/studio-ui/pages/database/styles.d.ts +1 -0
  30. package/dist/server/studio-ui/pages/database/styles.js +118 -0
  31. package/dist/server/studio-ui/pages/flow/index.d.ts +2 -0
  32. package/dist/server/studio-ui/pages/flow/index.js +11 -0
  33. package/dist/server/studio-ui/pages/flow/layout.d.ts +1 -0
  34. package/dist/server/studio-ui/pages/flow/layout.js +105 -0
  35. package/dist/server/studio-ui/pages/flow/plan-view.d.ts +1 -0
  36. package/dist/server/studio-ui/pages/flow/plan-view.js +67 -0
  37. package/dist/server/studio-ui/pages/flow/script.d.ts +1 -0
  38. package/dist/server/studio-ui/pages/flow/script.js +61 -0
  39. package/dist/server/studio-ui/pages/flow/styles.d.ts +1 -0
  40. package/dist/server/studio-ui/pages/flow/styles.js +59 -0
  41. package/dist/server/studio-ui/pages/flow/timeline-model.d.ts +1 -0
  42. package/dist/server/studio-ui/pages/flow/timeline-model.js +95 -0
  43. package/dist/server/studio-ui/pages/flow/timeline-view.d.ts +1 -0
  44. package/dist/server/studio-ui/pages/flow/timeline-view.js +92 -0
  45. package/dist/server/studio-ui/pages/home/index.d.ts +2 -0
  46. package/dist/server/studio-ui/pages/home/index.js +11 -0
  47. package/dist/server/studio-ui/pages/home/script.d.ts +1 -0
  48. package/dist/server/studio-ui/pages/home/script.js +160 -0
  49. package/dist/server/studio-ui/pages/home/styles.d.ts +1 -0
  50. package/dist/server/studio-ui/pages/home/styles.js +48 -0
  51. package/dist/server/studio-ui/pages/index.d.ts +2 -0
  52. package/dist/server/studio-ui/pages/index.js +17 -0
  53. package/dist/server/studio-ui/pages/tasks/index.d.ts +2 -0
  54. package/dist/server/studio-ui/pages/tasks/index.js +11 -0
  55. package/dist/server/studio-ui/pages/tasks/script.d.ts +1 -0
  56. package/dist/server/studio-ui/pages/tasks/script.js +96 -0
  57. package/dist/server/studio-ui/pages/tasks/styles.d.ts +1 -0
  58. package/dist/server/studio-ui/pages/tasks/styles.js +37 -0
  59. package/dist/server/studio-ui/pages/team/index.d.ts +2 -0
  60. package/dist/server/studio-ui/pages/team/index.js +11 -0
  61. package/dist/server/studio-ui/pages/team/script.d.ts +1 -0
  62. package/dist/server/studio-ui/pages/team/script.js +112 -0
  63. package/dist/server/studio-ui/pages/team/styles.d.ts +1 -0
  64. package/dist/server/studio-ui/pages/team/styles.js +22 -0
  65. package/dist/server/studio-ui/pages/types.d.ts +13 -0
  66. package/dist/server/studio-ui/pages/types.js +1 -0
  67. package/dist/server/studio-ui/pages/worktrees/index.d.ts +2 -0
  68. package/dist/server/studio-ui/pages/worktrees/index.js +11 -0
  69. package/dist/server/studio-ui/pages/worktrees/script.d.ts +1 -0
  70. package/dist/server/studio-ui/pages/worktrees/script.js +104 -0
  71. package/dist/server/studio-ui/pages/worktrees/styles.d.ts +1 -0
  72. package/dist/server/studio-ui/pages/worktrees/styles.js +15 -0
  73. package/dist/server/studio-ui/shell/actions.d.ts +1 -0
  74. package/dist/server/studio-ui/shell/actions.js +49 -0
  75. package/dist/server/studio-ui/shell/boot.d.ts +2 -0
  76. package/dist/server/studio-ui/shell/boot.js +13 -0
  77. package/dist/server/studio-ui/shell/console.d.ts +1 -0
  78. package/dist/server/studio-ui/shell/console.js +194 -0
  79. package/dist/server/studio-ui/shell/core.d.ts +1 -0
  80. package/dist/server/studio-ui/shell/core.js +50 -0
  81. package/dist/server/studio-ui/shell/detail-view.d.ts +1 -0
  82. package/dist/server/studio-ui/shell/detail-view.js +80 -0
  83. package/dist/server/studio-ui/shell/detail.d.ts +1 -0
  84. package/dist/server/studio-ui/shell/detail.js +83 -0
  85. package/dist/server/studio-ui/shell/dispatch.d.ts +1 -0
  86. package/dist/server/studio-ui/shell/dispatch.js +156 -0
  87. package/dist/server/studio-ui/shell/events-tools.d.ts +1 -0
  88. package/dist/server/studio-ui/shell/events-tools.js +94 -0
  89. package/dist/server/studio-ui/shell/format.d.ts +1 -0
  90. package/dist/server/studio-ui/shell/format.js +151 -0
  91. package/dist/server/studio-ui/shell/icons.d.ts +2 -0
  92. package/dist/server/studio-ui/shell/icons.js +20 -0
  93. package/dist/server/studio-ui/shell/markup-head.d.ts +1 -0
  94. package/dist/server/studio-ui/shell/markup-head.js +43 -0
  95. package/dist/server/studio-ui/shell/markup-panels.d.ts +2 -0
  96. package/dist/server/studio-ui/shell/markup-panels.js +7 -0
  97. package/dist/server/studio-ui/shell/markup-tools.d.ts +1 -0
  98. package/dist/server/studio-ui/shell/markup-tools.js +222 -0
  99. package/dist/server/studio-ui/shell/model.d.ts +1 -0
  100. package/dist/server/studio-ui/shell/model.js +192 -0
  101. package/dist/server/studio-ui/shell/paint.d.ts +1 -0
  102. package/dist/server/studio-ui/shell/paint.js +154 -0
  103. package/dist/server/studio-ui/shell/palette-view.d.ts +1 -0
  104. package/dist/server/studio-ui/shell/palette-view.js +60 -0
  105. package/dist/server/studio-ui/shell/palette.d.ts +1 -0
  106. package/dist/server/studio-ui/shell/palette.js +80 -0
  107. package/dist/server/studio-ui/shell/pipeline-state.d.ts +1 -0
  108. package/dist/server/studio-ui/shell/pipeline-state.js +152 -0
  109. package/dist/server/studio-ui/shell/proposals.d.ts +1 -0
  110. package/dist/server/studio-ui/shell/proposals.js +197 -0
  111. package/dist/server/studio-ui/shell/pure.d.ts +3 -0
  112. package/dist/server/studio-ui/shell/pure.js +9 -0
  113. package/dist/server/studio-ui/shell/router.d.ts +1 -0
  114. package/dist/server/studio-ui/shell/router.js +95 -0
  115. package/dist/server/studio-ui/shell/sidebar-view.d.ts +1 -0
  116. package/dist/server/studio-ui/shell/sidebar-view.js +38 -0
  117. package/dist/server/studio-ui/shell/sync.d.ts +1 -0
  118. package/dist/server/studio-ui/shell/sync.js +120 -0
  119. package/dist/server/studio-ui/shell/toasts-format.d.ts +1 -0
  120. package/dist/server/studio-ui/shell/toasts-format.js +89 -0
  121. package/dist/server/studio-ui/shell/triage.d.ts +1 -0
  122. package/dist/server/studio-ui/shell/triage.js +210 -0
  123. package/dist/server/studio-ui/styles/console.d.ts +1 -0
  124. package/dist/server/studio-ui/styles/console.js +83 -0
  125. package/dist/server/studio-ui/styles/dialogs.d.ts +1 -0
  126. package/dist/server/studio-ui/styles/dialogs.js +118 -0
  127. package/dist/server/studio-ui/styles/responsive.d.ts +1 -0
  128. package/dist/server/studio-ui/styles/responsive.js +28 -0
  129. package/dist/server/studio-ui/styles/shell.d.ts +1 -0
  130. package/dist/server/studio-ui/styles/shell.js +117 -0
  131. package/dist/server/studio-ui/styles/strategist.d.ts +1 -0
  132. package/dist/server/studio-ui/styles/strategist.js +77 -0
  133. package/dist/server/studio-ui/styles/tokens-base.d.ts +1 -0
  134. package/dist/server/studio-ui/styles/tokens-base.js +131 -0
  135. package/dist/server/studio-ui.d.ts +1 -10
  136. package/dist/server/studio-ui.js +63 -5314
  137. package/package.json +2 -2
  138. package/plugin/.claude-plugin/plugin.json +1 -1
@@ -0,0 +1,231 @@
1
+ // Database Studio event wiring.
2
+ export const databaseEventsScript = String.raw ` // ─── Events: Database Studio ───────────────────────────────────────────────
3
+ function setDbTab(tab, focus) {
4
+ state.tab = tab;
5
+ renderDb();
6
+ if (focus) $('tab-' + tab).focus();
7
+ if (tab === 'data' && (!state.dataResult || state.dataResult.entity !== state.dataEntity)) {
8
+ fetchDataRecords();
9
+ }
10
+ }
11
+
12
+ document.querySelectorAll('.subtab').forEach(function (t) {
13
+ t.addEventListener('click', function () { setDbTab(t.getAttribute('data-tab')); });
14
+ t.addEventListener('keydown', function (e) {
15
+ var order = ['explorer', 'data', 'drift', 'sql'];
16
+ var i = order.indexOf(state.tab);
17
+ if (e.key === 'ArrowRight') { e.preventDefault(); setDbTab(order[(i + 1) % 4], true); }
18
+ else if (e.key === 'ArrowLeft') { e.preventDefault(); setDbTab(order[(i + 3) % 4], true); }
19
+ else if (e.key === 'Home') { e.preventDefault(); setDbTab('explorer', true); }
20
+ else if (e.key === 'End') { e.preventDefault(); setDbTab('sql', true); }
21
+ });
22
+ });
23
+
24
+ document.querySelectorAll('.env-seg-btn').forEach(function (btn) {
25
+ btn.addEventListener('click', function () {
26
+ var mode = btn.getAttribute('data-env-mode');
27
+ if (mode) {
28
+ state.envMode = mode;
29
+ if (mode === 'prod') state.dataEnv = 'prod';
30
+ else if (mode === 'dev') state.dataEnv = 'dev';
31
+ state.dataResult = null;
32
+ renderDb();
33
+ if (state.tab === 'data') fetchDataRecords();
34
+ }
35
+ });
36
+ });
37
+
38
+ $('btn-settings').addEventListener('click', openSettings);
39
+ $('btn-export').addEventListener('click', function () {
40
+ var btn = $('btn-export');
41
+ var st = state.status || {};
42
+ var sourceEnv = st.dev && st.dev.connected ? 'dev' : (st.prod && st.prod.connected ? 'prod' : 'dev');
43
+ if (!window.confirm('Export the ' + sourceEnv.toUpperCase() + ' schema (structure only, no credentials or data) to .ai/db_schema.json?\nThis overwrites the tables in the current contract.')) return;
44
+ btn.disabled = true;
45
+ postJson('/api/export-contract', { sourceEnv: sourceEnv }).then(function (r) {
46
+ var n = r.exportedEntitiesCount != null ? r.exportedEntitiesCount : r.exportedTablesCount;
47
+ toast('Exported ' + n + ' ' + entityNoun(st[sourceEnv], true).toLowerCase() + ' to ' + r.filePath, 'ok');
48
+ if (state.diffTarget === 'contract') load(true);
49
+ }, function (e) { toast('Export failed: ' + e.message); }).then(function () { btn.disabled = false; });
50
+ });
51
+
52
+ $('view').addEventListener('input', function (e) {
53
+ if (e.target && e.target.id === 'search') {
54
+ state.search = e.target.value;
55
+ var pos = e.target.selectionStart;
56
+ renderDb();
57
+ var s = $('search');
58
+ if (s) { s.focus(); try { s.setSelectionRange(pos, pos); } catch (err) { /* ignore */ } }
59
+ }
60
+ if (e.target && e.target.id === 'data-search') {
61
+ state.dataSearch = e.target.value;
62
+ clearTimeout(state._dataSearchTimeout);
63
+ state._dataSearchTimeout = setTimeout(function () {
64
+ state.dataPage = 1;
65
+ fetchDataRecords();
66
+ }, 350);
67
+ }
68
+ });
69
+
70
+ $('view').addEventListener('change', function (e) {
71
+ if (e.target && e.target.id === 'diff-target') { state.diffTarget = e.target.value; load(false); }
72
+ if (e.target && e.target.id === 'data-entity-select') {
73
+ state.dataEntity = e.target.value;
74
+ state.dataPage = 1;
75
+ state.dataSort = '';
76
+ state.dataSearch = '';
77
+ state.dataResult = null;
78
+ fetchDataRecords();
79
+ }
80
+ if (e.target && e.target.id === 'data-limit-select') {
81
+ state.dataLimit = Number(e.target.value);
82
+ state.dataPage = 1;
83
+ fetchDataRecords();
84
+ }
85
+ });
86
+
87
+ $('view').addEventListener('mouseover', function (e) {
88
+ var el = e.target.closest && e.target.closest('[data-media]');
89
+ if (el) showThumb(el);
90
+ });
91
+ $('view').addEventListener('mouseout', function (e) {
92
+ var el = e.target.closest && e.target.closest('[data-media]');
93
+ if (el && !el.contains(e.relatedTarget)) hideThumb();
94
+ });
95
+ $('view').addEventListener('focusin', function (e) {
96
+ if (e.target.hasAttribute && e.target.hasAttribute('data-media')) showThumb(e.target);
97
+ });
98
+ $('view').addEventListener('focusout', hideThumb);
99
+ window.addEventListener('scroll', hideThumb, { passive: true });
100
+
101
+ $('view').addEventListener('click', function (e) {
102
+ var sortTh = e.target.closest('th.sortable');
103
+ if (sortTh) {
104
+ var col = sortTh.getAttribute('data-sort-col');
105
+ if (col) {
106
+ if (state.dataSort === col) {
107
+ state.dataOrder = state.dataOrder === 'asc' ? 'desc' : 'asc';
108
+ } else {
109
+ state.dataSort = col;
110
+ state.dataOrder = 'asc';
111
+ }
112
+ fetchDataRecords();
113
+ return;
114
+ }
115
+ }
116
+
117
+ var el = e.target.closest('button');
118
+ if (!el) return;
119
+ if (el.hasAttribute('data-media')) {
120
+ // Keep the <details> card from toggling when the badge sits in a summary row.
121
+ e.preventDefault();
122
+ openMedia(Number(el.getAttribute('data-media')));
123
+ return;
124
+ }
125
+ var action = el.getAttribute('data-action');
126
+ if (action === 'open-settings') openSettings();
127
+ else if (action === 'copy-sql') copyText(state.sql, el);
128
+ else if (action === 'edit-record') openEditRecordModal(Number(el.getAttribute('data-row')));
129
+ else if (action === 'delete-record') handleDeleteRecord(Number(el.getAttribute('data-row')));
130
+ else if (action === 'preview-url') {
131
+ var url = el.getAttribute('data-url');
132
+ $('media-title').textContent = 'Image Preview';
133
+ $('media-sub').textContent = url;
134
+ $('media-thumb').innerHTML = '<img src="' + esc(url) + '" style="max-width:100%;max-height:300px">';
135
+ $('media-details').innerHTML = '<dt>Source URL</dt><dd><code>' + esc(url) + '</code></dd>';
136
+ openDialog($('media-dialog'));
137
+ }
138
+ else if (action === 'view-json') {
139
+ var raw = el.getAttribute('data-raw');
140
+ toast(raw);
141
+ }
142
+
143
+ if (el.id === 'btn-insert-record') openInsertRecordModal();
144
+ else if (el.id === 'btn-data-refresh') fetchDataRecords();
145
+ else if (el.id === 'btn-page-prev') {
146
+ state.dataPage = Math.max(1, state.dataPage - 1);
147
+ fetchDataRecords();
148
+ }
149
+ else if (el.id === 'btn-page-next') {
150
+ state.dataPage++;
151
+ fetchDataRecords();
152
+ }
153
+
154
+ var envToggle = el.getAttribute('data-env-toggle');
155
+ if (envToggle) { state.mobileEnv = envToggle; renderDb(); }
156
+ });
157
+
158
+ $('btn-save-record').addEventListener('click', handleSaveRecordSubmit);
159
+
160
+ $('challenge-input').addEventListener('input', function (e) {
161
+ var val = e.target.value.trim();
162
+ var entity = state.pendingMutation ? state.pendingMutation.entity : '';
163
+ $('btn-confirm-challenge').disabled = (val !== entity);
164
+ });
165
+
166
+ $('btn-confirm-challenge').addEventListener('click', function () {
167
+ if (!state.pendingMutation) return;
168
+ var m = state.pendingMutation;
169
+ var inputVal = $('challenge-input').value.trim();
170
+ if (inputVal !== m.entity) return;
171
+ executeMutationDirect(m.action, m.entity, m.pk, m.payload, true, inputVal);
172
+ });
173
+
174
+ $('conn-form').addEventListener('click', function (e) {
175
+ var el = e.target.closest('button');
176
+ if (!el) return;
177
+ var toggle = el.getAttribute('data-toggle');
178
+ if (toggle) {
179
+ var input = $('url-' + toggle);
180
+ var show = input.type === 'password';
181
+ input.type = show ? 'text' : 'password';
182
+ el.textContent = show ? 'Hide' : 'Show';
183
+ el.setAttribute('aria-pressed', String(show));
184
+ }
185
+ var pingEnv = el.getAttribute('data-ping');
186
+ if (pingEnv) {
187
+ el.disabled = true;
188
+ ping(pingEnv).then(function (ok) { el.disabled = false; if (ok) load(false).then(renderModalStatus); });
189
+ }
190
+ var copyEnv = el.getAttribute('data-copy-masked');
191
+ if (copyEnv && state.status && state.status[copyEnv]) copyText(state.status[copyEnv].maskedUrl, el);
192
+ });
193
+
194
+ ['dev', 'prod'].forEach(function (env) {
195
+ $('engine-' + env).addEventListener('change', function () { setEngineMode(env); });
196
+ $('pengine-' + env).addEventListener('change', function () {
197
+ var port = $('pport-' + env);
198
+ // Swap the port only when it still holds the other engine's default.
199
+ if (!port.value.trim() || port.value.trim() === DEFAULT_PORT.mysql || port.value.trim() === DEFAULT_PORT.postgresql) {
200
+ port.value = DEFAULT_PORT[$('pengine-' + env).value];
201
+ }
202
+ });
203
+ document.querySelector('[data-xampp="' + env + '"]').addEventListener('click', function () { applyXamppDefaults(env); });
204
+ });
205
+
206
+ $('btn-save-conn').addEventListener('click', function () {
207
+ var envs = ['dev', 'prod'].filter(hasInput);
208
+ if (!envs.length) { $('conn-dialog').close(); return; }
209
+ var btn = $('btn-save-conn');
210
+ btn.disabled = true;
211
+ Promise.all(envs.map(ping)).then(function (results) {
212
+ btn.disabled = false;
213
+ load(false).then(renderModalStatus);
214
+ if (results.every(Boolean)) { $('conn-dialog').close(); toast('Connections saved to in-memory session', 'ok'); }
215
+ });
216
+ });
217
+
218
+ // Clear any typed secrets whenever the modal closes.
219
+ $('conn-dialog').addEventListener('close', function () {
220
+ ['dev', 'prod'].forEach(function (env) {
221
+ var input = $('url-' + env);
222
+ input.value = ''; input.type = 'password';
223
+ $('fsproject-' + env).value = '';
224
+ $('fsemu-' + env).value = '';
225
+ PARAM_FIELDS.forEach(function (f) { $(f + '-' + env).value = ''; });
226
+ var t = document.querySelector('[data-toggle="' + env + '"]');
227
+ if (t) { t.textContent = 'Show'; t.setAttribute('aria-pressed', 'false'); }
228
+ });
229
+ });
230
+
231
+ `;
@@ -0,0 +1 @@
1
+ export declare const databaseExplorerScript: string;
@@ -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;