sqlseed-web 0.2.4__py3-none-any.whl

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 (87) hide show
  1. sqlseed_web/AGENTS.md +106 -0
  2. sqlseed_web/__init__.py +24 -0
  3. sqlseed_web/__main__.py +8 -0
  4. sqlseed_web/_application.py +205 -0
  5. sqlseed_web/ai_settings.py +290 -0
  6. sqlseed_web/api.py +1102 -0
  7. sqlseed_web/app.py +26 -0
  8. sqlseed_web/managed_worker.py +184 -0
  9. sqlseed_web/operation_errors.py +42 -0
  10. sqlseed_web/plugin_environment.py +231 -0
  11. sqlseed_web/plugin_management.py +322 -0
  12. sqlseed_web/plugin_process.py +131 -0
  13. sqlseed_web/runtime_lifecycle.py +130 -0
  14. sqlseed_web/runtime_session.py +97 -0
  15. sqlseed_web/settings_environment.py +368 -0
  16. sqlseed_web/sqlite_target.py +86 -0
  17. sqlseed_web/state.py +348 -0
  18. sqlseed_web/static/AGENTS.md +180 -0
  19. sqlseed_web/static/ai.css +57 -0
  20. sqlseed_web/static/configs.css +50 -0
  21. sqlseed_web/static/date-picker.css +230 -0
  22. sqlseed_web/static/disclosure.css +149 -0
  23. sqlseed_web/static/graph-clarity.css +103 -0
  24. sqlseed_web/static/index.html +29 -0
  25. sqlseed_web/static/js/api.js +228 -0
  26. sqlseed_web/static/js/app.js +97 -0
  27. sqlseed_web/static/js/dropdown.js +432 -0
  28. sqlseed_web/static/js/filepicker.js +203 -0
  29. sqlseed_web/static/js/genform.js +1066 -0
  30. sqlseed_web/static/js/labels.js +195 -0
  31. sqlseed_web/static/js/pages/browse.js +209 -0
  32. sqlseed_web/static/js/pages/configs.js +424 -0
  33. sqlseed_web/static/js/pages/connect.js +332 -0
  34. sqlseed_web/static/js/pages/heal.js +395 -0
  35. sqlseed_web/static/js/pages/meta.js +110 -0
  36. sqlseed_web/static/js/pages/runs.js +293 -0
  37. sqlseed_web/static/js/pages/settings.js +942 -0
  38. sqlseed_web/static/js/pages/wizard.js +751 -0
  39. sqlseed_web/static/js/pages/workbench.js +3123 -0
  40. sqlseed_web/static/js/tree.js +126 -0
  41. sqlseed_web/static/js/workbench/ai-eligibility.js +33 -0
  42. sqlseed_web/static/js/workbench/ai-handoff.js +31 -0
  43. sqlseed_web/static/js/workbench/ai-stream.js +116 -0
  44. sqlseed_web/static/js/workbench/ai.js +888 -0
  45. sqlseed_web/static/js/workbench/connection.js +508 -0
  46. sqlseed_web/static/js/workbench/date-picker.js +445 -0
  47. sqlseed_web/static/js/workbench/dependency-view.js +119 -0
  48. sqlseed_web/static/js/workbench/editor.js +1236 -0
  49. sqlseed_web/static/js/workbench/focus.js +11 -0
  50. sqlseed_web/static/js/workbench/graph-layout.js +332 -0
  51. sqlseed_web/static/js/workbench/graph.js +970 -0
  52. sqlseed_web/static/js/workbench/guidance.js +29 -0
  53. sqlseed_web/static/js/workbench/model.js +124 -0
  54. sqlseed_web/static/js/workbench/plugin-management.js +512 -0
  55. sqlseed_web/static/js/workbench/preview-scroll-layout.js +94 -0
  56. sqlseed_web/static/js/workbench/preview.js +572 -0
  57. sqlseed_web/static/js/workbench/provider-guide.js +33 -0
  58. sqlseed_web/static/js/workbench/recovery.js +28 -0
  59. sqlseed_web/static/js/workbench/scroll-lock.js +26 -0
  60. sqlseed_web/static/js/workbench/session.js +174 -0
  61. sqlseed_web/static/js/workbench/table-data.js +186 -0
  62. sqlseed_web/static/js/workbench/ui.js +262 -0
  63. sqlseed_web/static/navigation.css +92 -0
  64. sqlseed_web/static/preview.css +29 -0
  65. sqlseed_web/static/runs.css +53 -0
  66. sqlseed_web/static/scrollbars.css +42 -0
  67. sqlseed_web/static/settings.css +108 -0
  68. sqlseed_web/static/style.css +3382 -0
  69. sqlseed_web/static/table-data.css +27 -0
  70. sqlseed_web/static/workbench.css +509 -0
  71. sqlseed_web/supervised_plugins.py +173 -0
  72. sqlseed_web/supervisor.py +238 -0
  73. sqlseed_web/workbench.py +381 -0
  74. sqlseed_web/workbench_ai.py +887 -0
  75. sqlseed_web/workbench_ai_relations.py +285 -0
  76. sqlseed_web/workbench_ai_stream.py +172 -0
  77. sqlseed_web/workbench_data.py +163 -0
  78. sqlseed_web/workbench_execution.py +199 -0
  79. sqlseed_web/workbench_runtime.py +1218 -0
  80. sqlseed_web/workbench_schema.py +277 -0
  81. sqlseed_web/workbench_store.py +458 -0
  82. sqlseed_web/worker_control.py +192 -0
  83. sqlseed_web-0.2.4.dist-info/METADATA +105 -0
  84. sqlseed_web-0.2.4.dist-info/RECORD +87 -0
  85. sqlseed_web-0.2.4.dist-info/WHEEL +4 -0
  86. sqlseed_web-0.2.4.dist-info/entry_points.txt +2 -0
  87. sqlseed_web-0.2.4.dist-info/licenses/LICENSE +679 -0
@@ -0,0 +1,28 @@
1
+ // A new editable plan, never an automatic retry or a mutation of run history.
2
+ export function remainingRun(run) {
3
+ const blocked=reason=>({ok:false,reason});
4
+ if(run.status!=='error')return blocked('只有已结束的失败记录可计算剩余数量。');
5
+ if(run.execution?.mode && run.execution.mode!=='append')return blocked('清空生成需重新核对完整计划,不能按追加数量恢复。');
6
+ if(run.row_counts_exact!==true || run.count_complete===false)return blocked('提交数量不确定,请先核对数据库,不能自动计算剩余数量。');
7
+ const configured=run.document?.tables,results=run.tables;
8
+ const count=value=>Number.isSafeInteger(value)&&value>=0;
9
+ if(!Array.isArray(configured)||!configured.length||!Array.isArray(results)||results.length!==configured.length)
10
+ return blocked('逐表结果不完整,请核对数据库后调整配置。');
11
+ const names=new Set(),remaining=[],tableDrafts={};let committed=0;
12
+ for(const table of configured) {
13
+ const matches=results.filter(result=>result.name===table.name),result=matches[0];
14
+ if(names.has(table.name)||matches.length!==1||!count(table.count)||!count(result.rows_inserted)
15
+ ||result.requested_count!==table.count||result.rows_inserted>table.count
16
+ ||!['done','error','not_run'].includes(result.status)
17
+ ||(result.status==='done'&&result.rows_inserted!==table.count)
18
+ ||(result.status==='not_run'&&result.rows_inserted!==0))
19
+ return blocked('逐表提交数量或状态不一致,请先核对数据库。');
20
+ names.add(table.name);committed+=result.rows_inserted;
21
+ const copy=structuredClone(table);copy.clear_before=false;
22
+ if(table.count>result.rows_inserted)remaining.push({...copy,count:table.count-result.rows_inserted});
23
+ else tableDrafts[table.name]=copy;
24
+ }
25
+ if(!count(run.rows_inserted)||committed!==run.rows_inserted)return blocked('总提交数量与逐表结果不一致,请先核对数据库。');
26
+ if(!remaining.length)return blocked('计划行数已全部提交;请处理运行错误,无需再次生成。');
27
+ return {ok:true,document:{...structuredClone(run.document),tables:remaining},tableDrafts};
28
+ }
@@ -0,0 +1,26 @@
1
+ // Overlapping dialog owners share one lock and restore the original viewport.
2
+ let owners = 0;
3
+ let saved = null;
4
+
5
+ export function lockPageScroll() {
6
+ const root = document.documentElement;
7
+ if (owners === 0) {
8
+ const scrolling = document.scrollingElement || root;
9
+ saved = {root, scrolling, locked: root.classList.contains('wb-scroll-locked'),
10
+ top: scrolling.scrollTop, left: scrolling.scrollLeft};
11
+ root.classList.add('wb-scroll-locked');
12
+ }
13
+ owners++;
14
+ let released = false;
15
+ return () => {
16
+ if (released) return;
17
+ released = true;
18
+ owners--;
19
+ if (owners || !saved) return;
20
+ const snapshot = saved;
21
+ saved = null;
22
+ if (!snapshot.locked) snapshot.root.classList.remove('wb-scroll-locked');
23
+ snapshot.scrolling.scrollTop = snapshot.top;
24
+ snapshot.scrolling.scrollLeft = snapshot.left;
25
+ };
26
+ }
@@ -0,0 +1,174 @@
1
+ import { WorkbenchDocument } from './model.js';
2
+ function validatePreviewCount(count) {
3
+ if (!Number.isInteger(count) || count < 1 || count > 100) {
4
+ throw new Error('预览数量必须是 1–100 之间的整数');
5
+ }
6
+ }
7
+
8
+ // Transport injected so race and snapshot tests exercise real session behavior.
9
+ export class WorkbenchSession {
10
+ constructor(connId, schema, request) {
11
+ this.connId = connId;
12
+ this.model = new WorkbenchDocument(schema);
13
+ this.request = request;
14
+ this.saving = false;
15
+ this.submitting = false;
16
+ this.previewSequence = 0;
17
+ this.name = '数据生成配置';
18
+ }
19
+ async save(name = this.name) {
20
+ if (this.saving) {
21
+ throw new Error('正在保存,请稍候');
22
+ }
23
+ const model = this.model,
24
+ epoch = model.epoch,
25
+ lifecycle = model.lifecycleVersion;
26
+ const payload = {
27
+ ...model.payload(name),
28
+ conn_id: this.connId
29
+ };
30
+ const previous = model.saved;
31
+ if (previous) {
32
+ payload.revision = previous.revision;
33
+ }
34
+ this.saving = true;
35
+ try {
36
+ const saved = await this.request(`/api/workbench/drafts${previous ? "/" + encodeURIComponent(previous.id) : ''}`, payload, previous ? 'PUT' : 'POST');
37
+ if (lifecycle !== model.lifecycleVersion) {
38
+ throw new Error('保存期间此配置已被删除或重命名,请核对当前状态后重试');
39
+ }
40
+ model.markSaved(saved, epoch);
41
+ if (this.model === model && model.epoch === epoch) {
42
+ this.name = name;
43
+ }
44
+ return saved;
45
+ } finally {
46
+ this.saving = false;
47
+ }
48
+ }
49
+ async check(preview = false, count = 3) {
50
+ validatePreviewCount(count);
51
+ const model = this.model,
52
+ epoch = model.epoch;
53
+ const payload = {
54
+ conn_id: this.connId,
55
+ document: model.payload(this.name).document,
56
+ schema_hash: model.schema.schema_hash,
57
+ count
58
+ };
59
+ const sequence = preview ? ++this.previewSequence : null;
60
+ const result = await this.request(`/api/workbench/${preview ? 'preview' : 'check'}`, payload, 'POST');
61
+ if (preview && sequence !== this.previewSequence) {
62
+ return null;
63
+ }
64
+ return this.model === model && model.acceptCheck(result, epoch, preview) ? result : null;
65
+ }
66
+ async previewTable(tableName, count = 3) {
67
+ validatePreviewCount(count);
68
+ const model = this.model,
69
+ epoch = model.epoch;
70
+ if (!model.schema.tables.some(table => table.name === tableName)) {
71
+ throw new Error(`数据库中不存在表:${tableName}`);
72
+ }
73
+ const document = model.payload(this.name).document;
74
+ // Follow the actual generation prerequisites. An unselected parent is read
75
+ // from existing rows, so its ancestors are not part of this preview plan.
76
+ // Keep the full root settings/associations intact for backend validation.
77
+ const selected = new Set(document.tables.map(table => table.name));
78
+ const needed = new Set([tableName]);
79
+ const edges = [...(model.schema.edges || [])];
80
+ for (const association of document.associations || []) {
81
+ for (const target of association.target_tables || []) {
82
+ edges.push({
83
+ source: association.source_table,
84
+ target
85
+ });
86
+ }
87
+ }
88
+ for (const name of needed) {
89
+ for (const edge of edges) {
90
+ if (edge.target === name && selected.has(edge.source)) {
91
+ needed.add(edge.source);
92
+ }
93
+ }
94
+ }
95
+ document.tables = document.tables.filter(table => needed.has(table.name));
96
+ if (!document.tables.some(table => table.name === tableName)) {
97
+ document.tables.push(structuredClone(model.table(tableName)));
98
+ }
99
+ const payload = {
100
+ conn_id: this.connId,
101
+ document,
102
+ schema_hash: model.schema.schema_hash,
103
+ count
104
+ };
105
+ const sequence = ++this.previewSequence;
106
+ const result = await this.request('/api/workbench/preview', payload, 'POST');
107
+ if (sequence !== this.previewSequence || this.model !== model || epoch !== model.epoch || model.errors.size) {
108
+ return null;
109
+ }
110
+ const refreshed = new Set(document.tables.map(table => table.name));
111
+ const samples = Object.fromEntries(Object.entries(model.samples).filter(([name]) => !refreshed.has(name)));
112
+ const issues = model.previewIssues.filter(issue => issue.table && !refreshed.has(issue.table));
113
+ return model.acceptPreview({
114
+ ...result,
115
+ samples: {
116
+ ...samples,
117
+ ...result.samples
118
+ },
119
+ issues: [...issues, ...(result.issues || [])]
120
+ }, epoch) ? result : null;
121
+ }
122
+ open(draft) {
123
+ if (draft.target_key !== this.model.schema.target_key) {
124
+ throw new Error('此配置属于另一数据库,请先连接对应数据库');
125
+ }
126
+ const model = new WorkbenchDocument(this.model.schema, draft.document);
127
+ model.restoreView(draft.view_state);
128
+ model.markSaved(draft, draft.schema_hash === model.schema.schema_hash ? model.epoch : -1);
129
+ this.model = model;
130
+ this.name = draft.name;
131
+ }
132
+ async executionPlan(execution) {
133
+ const model = this.model;
134
+ if (!model.saved || model.dirty || !model.check) {
135
+ throw new Error('请先保存并检查当前配置');
136
+ }
137
+ return this.request('/api/workbench/execution-plan', {
138
+ conn_id: this.connId,
139
+ draft_id: model.saved.id,
140
+ revision: model.saved.revision,
141
+ schema_hash: model.schema.schema_hash,
142
+ config_hash: model.check.config_hash,
143
+ execution
144
+ }, 'POST');
145
+ }
146
+ async run(execution, planHash) {
147
+ if (this.submitting) {
148
+ throw new Error('运行正在提交,请稍候');
149
+ }
150
+ if (!this.model.canRun()) {
151
+ throw new Error('请先保存当前配置并完成依赖检查');
152
+ }
153
+ const model = this.model;
154
+ const payload = {
155
+ conn_id: this.connId,
156
+ draft_id: model.saved.id,
157
+ revision: model.saved.revision,
158
+ schema_hash: model.schema.schema_hash,
159
+ config_hash: model.check.config_hash
160
+ };
161
+ if (execution) {
162
+ payload.execution = structuredClone(execution);
163
+ }
164
+ if (planHash) {
165
+ payload.plan_hash = planHash;
166
+ }
167
+ this.submitting = true;
168
+ try {
169
+ return await this.request('/api/workbench/runs', payload, 'POST');
170
+ } finally {
171
+ this.submitting = false;
172
+ }
173
+ }
174
+ }
@@ -0,0 +1,186 @@
1
+ import { h, api } from '../api.js';
2
+ import { modal, button, valueText } from './ui.js';
3
+
4
+ /** Read current database rows without changing the active connection or generation document. */
5
+ export function openTableData({
6
+ connId = null,
7
+ runId = null,
8
+ table,
9
+ targetKey,
10
+ targetLabel = '',
11
+ isCurrent = () => true
12
+ }) {
13
+ let closed = false,
14
+ busy = false,
15
+ controller = null,
16
+ result = null;
17
+ let connection = runId ? null : connId;
18
+ const limit = 50;
19
+ const dialog = modal('数据库当前数据', {
20
+ wide: true,
21
+ onClose: () => {
22
+ closed = true;
23
+ controller?.abort();
24
+ }
25
+ });
26
+ dialog.el.classList.add('wb-table-data');
27
+ const target = h('p', {
28
+ class: 'mono wb-table-data-target'
29
+ }, targetLabel);
30
+ const status = h('p', {
31
+ class: 'wb-table-data-status',
32
+ role: 'status',
33
+ 'aria-live': 'polite'
34
+ }, '正在读取数据库…');
35
+ const error = h('p', {
36
+ class: 'wb-table-data-error',
37
+ role: 'alert'
38
+ });
39
+ const records = h('section', {
40
+ class: 'wb-table-data-records',
41
+ 'aria-label': `${table} 当前记录`
42
+ });
43
+ const page = h('span', {
44
+ class: 'muted'
45
+ });
46
+ const refreshed = h('p', {
47
+ class: 'muted wb-table-data-read-at'
48
+ });
49
+ const ordering = h('p', {
50
+ class: 'muted wb-table-data-order'
51
+ });
52
+ const refreshButton = button('刷新数据', () => load(result?.offset || 0));
53
+ const previous = button('上一页', () => load(Math.max(0, (result?.offset || 0) - limit)));
54
+ const next = button('下一页', () => load((result?.offset || 0) + limit));
55
+ dialog.body.append(h('div', {
56
+ class: 'wb-table-data-heading'
57
+ }, h('h3', {
58
+ class: 'mono'
59
+ }, table), refreshButton), target, h('p', {
60
+ class: 'muted'
61
+ }, '查询时表内的实际记录,可能包含原有、本次提交及后续变化的数据;不是某次运行的数据快照。'), status, error, records, h('div', {
62
+ class: 'wb-table-data-pagination'
63
+ }, page, h('div', {}, previous, next)), refreshed, ordering);
64
+ dialog.actions.append(button('关闭', dialog.close));
65
+ const live = () => !closed && dialog.el.isConnected && isCurrent();
66
+ function setBusy(value) {
67
+ busy = value;
68
+ refreshButton.disabled = value;
69
+ previous.disabled = value || !result || result.offset === 0;
70
+ next.disabled = value || !result || result.offset + result.limit >= result.total;
71
+ records.setAttribute('aria-busy', String(value));
72
+ }
73
+ function displayValue(value) {
74
+ const text = valueText(value);
75
+ return text.length > 160 ? h('details', {
76
+ class: 'wb-table-data-value'
77
+ }, h('summary', {}, `${text.slice(0, 80)}… 展开完整值`), h('pre', {}, text)) : text;
78
+ }
79
+ function render(data) {
80
+ function databaseDialectLabel() {
81
+ if (data.dialect === 'postgresql') {
82
+ return 'PostgreSQL';
83
+ } else if (data.dialect === 'sqlite') {
84
+ return 'SQLite';
85
+ } else {
86
+ return data.dialect;
87
+ }
88
+ }
89
+ target.textContent = `${databaseDialectLabel()} · ${data.target_label}`;
90
+ const grid = h('table', {}, h('caption', {}, `${table} · 数据库当前数据`), h('thead', {}, h('tr', {}, ...data.columns.map(column => h('th', {
91
+ scope: 'col'
92
+ }, h('span', {
93
+ class: 'mono'
94
+ }, column.name), h('small', {}, [column.type, column.is_primary_key ? 'PK' : null].filter(Boolean).join(' · ')))))), h('tbody', {}, ...data.rows.map(row => h('tr', {}, ...data.columns.map(column => h('td', {}, displayValue(row[column.name])))))));
95
+ function emptyDataPage() {
96
+ if (data.rows.length) {
97
+ return [];
98
+ } else {
99
+ return [h('p', {
100
+ class: 'wb-table-data-empty'
101
+ }, data.total ? '本页暂无记录,请返回上一页或刷新。' : '表中暂无记录。')];
102
+ }
103
+ }
104
+ records.replaceChildren(h('div', {
105
+ class: 'wb-table-data-scroll',
106
+ tabindex: 0,
107
+ 'aria-label': `${table} 当前数据,可横向滚动`
108
+ }, grid), ...emptyDataPage());
109
+ page.textContent = `${data.rows.length ? "" + (data.offset + 1) + "–" + (data.offset + data.rows.length) : '0'} / ${data.total} 行 · 每页 ${limit} 行`;
110
+ const date = new Date(data.read_at);
111
+ refreshed.textContent = `读取时间:${Number.isNaN(date.getTime()) ? data.read_at : date.toLocaleString('zh-CN', {
112
+ hour12: false
113
+ })}`;
114
+ ordering.textContent = data.order_by?.length ? `按主键 ${data.order_by.join('、')} 排序;数据库变化时,不同页的内容可能随之变化。` : '此表没有主键,分页顺序可能变化。';
115
+ }
116
+ async function load(offset = 0) {
117
+ if (!live() || busy) {
118
+ return;
119
+ }
120
+ controller = new AbortController();
121
+ setBusy(true);
122
+ error.textContent = '';
123
+ status.textContent = result ? '正在刷新,暂时保留上次读取的记录…' : '正在读取数据库…';
124
+ try {
125
+ if (!(await ensureConnection())) {
126
+ return;
127
+ }
128
+ const params = new URLSearchParams({
129
+ limit: String(limit),
130
+ offset: String(offset)
131
+ });
132
+ if (runId) {
133
+ params.set('run_id', runId);
134
+ }
135
+ const data = await api(`/api/workbench/connections/${encodeURIComponent(connection)}/tables/${encodeURIComponent(table)}/data?${params}`, {
136
+ signal: controller.signal
137
+ });
138
+ if (!live()) {
139
+ return;
140
+ }
141
+ if (data.target_key !== targetKey || data.table !== table) {
142
+ throw new Error('返回数据的目标或表不匹配,请重新打开。');
143
+ }
144
+ result = data;
145
+ render(data);
146
+ status.textContent = '已读取数据库当前数据。';
147
+ } catch (error_) {
148
+ if (!live() || error_.name === 'AbortError') return;
149
+ if (runId && error_.status === 404) connection = null;
150
+ error.textContent = `无法读取数据:${error_.message}`;
151
+ status.textContent = result ? '仍显示上次读取的记录。' : '尚未读取到数据库记录。';
152
+ } finally {
153
+ if (live()) {
154
+ setBusy(false);
155
+ }
156
+ }
157
+ async function ensureConnection() {
158
+ if (!connection && runId) {
159
+ const matched = await api(`/api/workbench/runs/${encodeURIComponent(runId)}/data-connections`, {
160
+ signal: controller.signal
161
+ });
162
+ if (!live()) {
163
+ return false;
164
+ }
165
+ if (matched.target_key !== targetKey) {
166
+ throw new Error('运行记录的数据库目标已变化,请重新打开。');
167
+ }
168
+ connection = matched.connections?.[0]?.conn_id || null;
169
+ if (!connection) {
170
+ throw new Error('请先连接此运行的相同数据库:关闭面板,通过顶栏连接数据库,然后重新查看。');
171
+ }
172
+ }
173
+ if (!connection) {
174
+ throw new Error('连接已失效,请重新连接数据库后查看。');
175
+ }
176
+ return true;
177
+ }
178
+ }
179
+ const ready = load();
180
+ return {
181
+ dialog,
182
+ ready,
183
+ refresh: () => load(result?.offset || 0),
184
+ close: dialog.close
185
+ };
186
+ }
@@ -0,0 +1,262 @@
1
+ import { cycleFocus } from "./focus.js";
2
+ import { h } from '../api.js';
3
+ import { lockPageScroll } from './scroll-lock.js';
4
+ const paths = {
5
+ sparkles: 'm12 3 2.5 6.5L21 12l-6.5 2.5L12 21l-2.5-6.5L3 12l6.5-2.5ZM20 2v4m-2-2h4',
6
+ schema: 'M12 7v5M5.5 17v-5h13v5',
7
+ check: 'm3 6 2 2 4-4M12 6h9M3 13h3m6 0h9M3 20h3m6 0h9',
8
+ fields: 'M3 9h18M9 9v11M3 14h18',
9
+ save: 'M4 3h13l4 4v14H3V3zM7 3v6h10V3M7 21v-8h10v8',
10
+ download: 'M12 3v12m-5-5 5 5 5-5M4 16v5h16v-5',
11
+ upload: 'M12 16V4m-5 5 5-5 5 5M4 16v5h16v-5',
12
+ code: 'm8 6-6 6 6 6m8-12 6 6-6 6m-3-14-2 16',
13
+ refresh: 'M3 11a9 9 0 0 1 15.4-6.4L21 7M21 3v4h-4M21 13a9 9 0 0 1-15.4 6.4L3 17M3 21v-4h4',
14
+ database: 'M4 5v7c0 4 16 4 16 0V5M4 12v7c0 4 16 4 16 0',
15
+ link: 'm10 13 4-4m-6 7-1 1a4 4 0 0 1-6-6l4-4a4 4 0 0 1 6 0m2 0 1-1a4 4 0 0 1 6 6l-4 4a4 4 0 0 1-6 0',
16
+ search: 'm15 15 5 5',
17
+ arrow: 'M3 12h17m-5-5 5 5-5 5',
18
+ edit: 'm15 4 5 5M4 20l5-1L21 7l-5-5L4 14Z',
19
+ reset: 'M3 4v6h6M3 10a9 9 0 1 1 .6 7M12 7v5l3 2',
20
+ sliders: 'M4 6h6m4 0h6M4 12h10m4 0h2M4 18h2m4 0h10M10 3v6m4 0v6M6 15v6'
21
+ };
22
+ export function icon(name) {
23
+ name = {
24
+ checklist: 'check',
25
+ table: 'fields',
26
+ db: 'database',
27
+ settings: 'sliders'
28
+ }[name] || name;
29
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
30
+ for (const [key, value] of Object.entries({
31
+ class: 'icon',
32
+ viewBox: '0 0 24 24',
33
+ width: 16,
34
+ height: 16,
35
+ fill: 'none',
36
+ stroke: 'currentColor',
37
+ 'stroke-width': 1.5,
38
+ 'stroke-linecap': 'round',
39
+ 'stroke-linejoin': 'round',
40
+ 'aria-hidden': 'true'
41
+ })) {
42
+ svg.setAttribute(key, value);
43
+ }
44
+ const shape = (tag, attrs, text) => {
45
+ const element = document.createElementNS('http://www.w3.org/2000/svg', tag);
46
+ for (const [key, value] of Object.entries(attrs)) {
47
+ element.setAttribute(key, value);
48
+ }
49
+ if (text) {
50
+ element.textContent = text;
51
+ }
52
+ svg.append(element);
53
+ };
54
+ if (name === 'derive') {
55
+ shape('text', {
56
+ x: 2,
57
+ y: 18,
58
+ fill: 'currentColor',
59
+ stroke: 'none',
60
+ 'font-family': 'Georgia,serif',
61
+ 'font-style': 'italic',
62
+ 'font-size': 20
63
+ }, 'ƒx');
64
+ return svg;
65
+ }
66
+ if (name === 'schema') {
67
+ for (const [x, y, width, height] of [[8, 2, 8, 5], [2, 17, 7, 5], [15, 17, 7, 5]]) {
68
+ shape('rect', {
69
+ x,
70
+ y,
71
+ width,
72
+ height,
73
+ rx: 1
74
+ });
75
+ }
76
+ }
77
+ if (name === 'fields') {
78
+ shape('rect', {
79
+ x: 3,
80
+ y: 4,
81
+ width: 18,
82
+ height: 16,
83
+ rx: 2
84
+ });
85
+ }
86
+ if (name === 'database') {
87
+ shape('ellipse', {
88
+ cx: 12,
89
+ cy: 5,
90
+ rx: 8,
91
+ ry: 3
92
+ });
93
+ }
94
+ if (name === 'search') {
95
+ shape('circle', {
96
+ cx: 10,
97
+ cy: 10,
98
+ r: 6
99
+ });
100
+ }
101
+ shape('path', {
102
+ d: paths[name] || paths.fields,
103
+ ...(name === 'link' ? {
104
+ transform: 'translate(1 0)'
105
+ } : {})
106
+ });
107
+ return svg;
108
+ }
109
+ export function button(label, action, {
110
+ glyph,
111
+ primary = false,
112
+ small = false,
113
+ plain = false,
114
+ class: extraClass = '',
115
+ ...attrs
116
+ } = {}) {
117
+ let baseClass;
118
+ if (plain) {
119
+ baseClass = 'wb-button';
120
+ } else {
121
+ baseClass = `btn wb-button${primary ? ' primary wb-primary' : ''}${small ? ' small wb-small' : ''}`;
122
+ }
123
+ if (action?.databaseAction) {
124
+ attrs['data-db-action'] = '';
125
+ }
126
+ return h('button', {
127
+ type: 'button',
128
+ class: `${baseClass}${extraClass ? ' ' + extraClass : ''}`,
129
+ onclick: action,
130
+ ...attrs
131
+ }, glyph ? icon(glyph) : null, label);
132
+ }
133
+ export function download(name, text, type = 'application/json') {
134
+ const url = URL.createObjectURL(new Blob([text], {
135
+ type
136
+ }));
137
+ const a = h('a', {
138
+ href: url,
139
+ download: name
140
+ });
141
+ document.body.append(a);
142
+ a.click();
143
+ a.remove();
144
+ setTimeout(() => URL.revokeObjectURL(url), 1000);
145
+ }
146
+ let activeModalClose = null;
147
+ export function modal(title, {
148
+ onClose,
149
+ wide = false,
150
+ drawer = false
151
+ } = {}) {
152
+ activeModalClose?.();
153
+ let closed = false;
154
+ const previous = document.activeElement;
155
+ const unlockScroll = lockPageScroll();
156
+ const background = [...document.body.children].map(element => ({
157
+ element,
158
+ inert: Boolean(element.inert)
159
+ }));
160
+ const body = h('div', {
161
+ class: `${drawer ? 'drawer-body' : 'modal-body'} wb-modal-body`
162
+ }),
163
+ actions = h('div', {
164
+ class: `${drawer ? 'drawer-footer' : 'modal-footer'} wb-modal-actions`
165
+ });
166
+ const close = () => {
167
+ if (closed) {
168
+ return;
169
+ }
170
+ closed = true;
171
+ overlay.remove();
172
+ document.removeEventListener('keydown', key);
173
+ unlockScroll();
174
+ for (const {
175
+ element,
176
+ inert
177
+ } of background) {
178
+ element.inert = inert;
179
+ }
180
+ if (activeModalClose === close) {
181
+ activeModalClose = null;
182
+ }
183
+ onClose?.();
184
+ previous?.focus?.({
185
+ preventScroll: true
186
+ });
187
+ };
188
+ const header = h('header', {
189
+ class: drawer ? 'drawer-head' : 'modal-head'
190
+ }, h('h2', {}, title), h('button', {
191
+ type: 'button',
192
+ class: 'close',
193
+ 'aria-label': '关闭',
194
+ onclick: close
195
+ }, '×'));
196
+ const panel = h('section', {
197
+ class: `${drawer ? 'drawer' : 'modal'} wb-modal${drawer ? ' wb-drawer' : ''}${wide ? ' modal-wide wb-modal-wide' : ''}`,
198
+ role: 'dialog',
199
+ 'aria-modal': 'true',
200
+ 'aria-label': title
201
+ }, header, body, actions);
202
+ const overlay = h('div', {
203
+ class: 'overlay open wb-overlay'
204
+ }, panel);
205
+ overlay.onclick = event => {
206
+ if (event.target === overlay) {
207
+ close();
208
+ }
209
+ };
210
+ function key(event) {
211
+ if (event.key === 'Escape') {
212
+ close();
213
+ }
214
+ if (event.key === 'Tab') {
215
+ const summaryOf = details => [...details.children].find(child => child.tagName === 'SUMMARY');
216
+ const controls = [...overlay.querySelectorAll('button,input,textarea,a[href],summary')].filter(el => {
217
+ if (el.disabled || el.getAttribute('tabindex') === '-1') {
218
+ return false;
219
+ }
220
+ if (el.tagName === 'SUMMARY' && (el.parentElement?.tagName !== 'DETAILS' || summaryOf(el.parentElement) !== el)) {
221
+ return false;
222
+ }
223
+ for (let ancestor = el; ancestor && ancestor !== overlay; ancestor = ancestor.parentElement) {
224
+ if (ancestor.hidden || ancestor.getAttribute('hidden') !== null) {
225
+ return false;
226
+ }
227
+ if (ancestor.tagName === 'DETAILS' && !ancestor.open && !summaryOf(ancestor)?.contains(el)) {
228
+ return false;
229
+ }
230
+ }
231
+ return true;
232
+ });
233
+ cycleFocus(event, controls);
234
+ }
235
+ }
236
+ for (const {
237
+ element
238
+ } of background) {
239
+ element.inert = true;
240
+ }
241
+ document.body.append(overlay);
242
+ document.addEventListener('keydown', key);
243
+ overlay.querySelector('button')?.focus();
244
+ activeModalClose = close;
245
+ return {
246
+ body,
247
+ actions,
248
+ close,
249
+ el: panel,
250
+ overlay,
251
+ header
252
+ };
253
+ }
254
+ export function valueText(value) {
255
+ if (value === null) {
256
+ return 'NULL';
257
+ }
258
+ if (value === undefined) {
259
+ return '—';
260
+ }
261
+ return typeof value === 'object' ? JSON.stringify(value) : String(value);
262
+ }