tablewalk 0.0.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 (97) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +553 -0
  3. package/dist/adapters/adapter.js +372 -0
  4. package/dist/adapters/connect.js +33 -0
  5. package/dist/adapters/mysql.js +951 -0
  6. package/dist/adapters/postgres.js +1000 -0
  7. package/dist/adapters/sqlite.js +781 -0
  8. package/dist/client/agent.js +262 -0
  9. package/dist/client/app.js +973 -0
  10. package/dist/client/arrange.js +254 -0
  11. package/dist/client/ask.js +133 -0
  12. package/dist/client/breakdown.js +317 -0
  13. package/dist/client/clauses.js +390 -0
  14. package/dist/client/columns.js +98 -0
  15. package/dist/client/complete.js +437 -0
  16. package/dist/client/compose.js +166 -0
  17. package/dist/client/composer.css +495 -0
  18. package/dist/client/composer.js +1972 -0
  19. package/dist/client/connections.js +234 -0
  20. package/dist/client/connmanager.js +962 -0
  21. package/dist/client/connurl.js +188 -0
  22. package/dist/client/core.js +893 -0
  23. package/dist/client/deeplink.js +270 -0
  24. package/dist/client/delete.js +144 -0
  25. package/dist/client/diagram.js +885 -0
  26. package/dist/client/dropdown.js +279 -0
  27. package/dist/client/export.js +456 -0
  28. package/dist/client/features.css +524 -0
  29. package/dist/client/findvalue.js +169 -0
  30. package/dist/client/grid.js +205 -0
  31. package/dist/client/handoff.js +153 -0
  32. package/dist/client/help.css +145 -0
  33. package/dist/client/help.js +881 -0
  34. package/dist/client/history.js +222 -0
  35. package/dist/client/index.html +116 -0
  36. package/dist/client/insert.js +151 -0
  37. package/dist/client/menu.js +160 -0
  38. package/dist/client/nested.js +255 -0
  39. package/dist/client/page.css +713 -0
  40. package/dist/client/page.js +1345 -0
  41. package/dist/client/pagebuilder.js +1222 -0
  42. package/dist/client/pagemarks.js +95 -0
  43. package/dist/client/palette.js +374 -0
  44. package/dist/client/peek.js +254 -0
  45. package/dist/client/picker.js +139 -0
  46. package/dist/client/pins.js +140 -0
  47. package/dist/client/prompt.js +129 -0
  48. package/dist/client/record.js +707 -0
  49. package/dist/client/schemaexport.js +242 -0
  50. package/dist/client/schematext.js +125 -0
  51. package/dist/client/shape.js +178 -0
  52. package/dist/client/shapecheck.js +129 -0
  53. package/dist/client/skeleton.js +139 -0
  54. package/dist/client/sql.css +126 -0
  55. package/dist/client/sql.js +398 -0
  56. package/dist/client/sqlcomplete.js +163 -0
  57. package/dist/client/sqlsaved.js +107 -0
  58. package/dist/client/style.css +2711 -0
  59. package/dist/client/summary.js +259 -0
  60. package/dist/client/table.js +1035 -0
  61. package/dist/client/template.js +539 -0
  62. package/dist/client/theme.js +74 -0
  63. package/dist/client/tour.js +324 -0
  64. package/dist/client/undo.js +105 -0
  65. package/dist/client/url.js +166 -0
  66. package/dist/client/value.js +223 -0
  67. package/dist/client/views.js +215 -0
  68. package/dist/client/virtual.js +176 -0
  69. package/dist/client/welcome.js +170 -0
  70. package/dist/client/write.js +414 -0
  71. package/dist/server/changeimpact.js +195 -0
  72. package/dist/server/connections.js +615 -0
  73. package/dist/server/constraints.js +62 -0
  74. package/dist/server/credentials.js +230 -0
  75. package/dist/server/fixture.js +199 -0
  76. package/dist/server/graph.js +194 -0
  77. package/dist/server/impact.js +48 -0
  78. package/dist/server/index.js +2204 -0
  79. package/dist/server/journal.js +173 -0
  80. package/dist/server/layouts.js +128 -0
  81. package/dist/server/mcp.js +2840 -0
  82. package/dist/server/shapeonly.js +91 -0
  83. package/dist/shared/breakdown.js +231 -0
  84. package/dist/shared/breakdowntext.js +257 -0
  85. package/dist/shared/diff.js +130 -0
  86. package/dist/shared/like.js +29 -0
  87. package/dist/shared/lint.js +149 -0
  88. package/dist/shared/order.js +133 -0
  89. package/dist/shared/page.js +932 -0
  90. package/dist/shared/query.js +831 -0
  91. package/dist/shared/recordview.js +343 -0
  92. package/dist/shared/schema.js +377 -0
  93. package/dist/shared/sqlsaved.js +67 -0
  94. package/dist/shared/view.js +981 -0
  95. package/dist/shared/viewtext.js +273 -0
  96. package/dist/shared/vocabulary.js +164 -0
  97. package/package.json +57 -0
@@ -0,0 +1,279 @@
1
+ /**
2
+ * A themed listbox.
3
+ *
4
+ * A native `<select>` cannot be styled where it matters: the open menu is
5
+ * drawn by the operating system, in the system font, with the system
6
+ * highlight colour, ignoring the page entirely. On a dark theme that means a
7
+ * white menu with blue selection appearing out of a dark page.
8
+ *
9
+ * It also cannot show two lines per option, which is the actual requirement
10
+ * here — a connection is a name *and* a host, and the host is what tells two
11
+ * similarly named databases apart.
12
+ *
13
+ * Rebuilding a select means rebuilding what a select gives you for free, so
14
+ * this implements the listbox keyboard contract rather than approximating it:
15
+ * arrows move, Home and End jump, Enter and Space choose, Escape closes and
16
+ * returns focus, Tab closes, typing jumps to a matching option. Anything less
17
+ * is a control that looks better and works worse.
18
+ */
19
+ import { el } from './core.js';
20
+
21
+ /**
22
+ * @param {object} options
23
+ * @param {Array<{value: string, label: string, detail?: string, badge?: string, disabled?: boolean, tone?: string}>} options.items
24
+ * @param {string} options.value currently selected value
25
+ * @param {(value: string) => void} options.onChange
26
+ * @param {string} [options.ariaLabel]
27
+ * @param {Array<{label: string, onSelect: () => void}>} [options.actions] pinned at the bottom
28
+ */
29
+ export function dropdown({ items, value, onChange, ariaLabel = 'Select', actions = [] }) {
30
+ /* Reassigned by `choose`, so the control tracks its own state rather than
31
+ depending on the caller to redraw it. */
32
+ let selected = items.find((i) => i.value === value);
33
+
34
+ const button = el('button', {
35
+ type: 'button',
36
+ class: 'dd-button',
37
+ 'aria-haspopup': 'listbox',
38
+ 'aria-expanded': 'false',
39
+ 'aria-label': ariaLabel,
40
+ title: selected?.detail ?? selected?.label ?? '',
41
+ }, [
42
+ el('span', { class: 'dd-value', text: selected?.label ?? '—' }),
43
+ el('span', { class: 'dd-caret', 'aria-hidden': 'true', text: '▾' }),
44
+ ]);
45
+
46
+ const list = el('div', { class: 'dd-list', role: 'listbox', 'aria-label': ariaLabel, hidden: true });
47
+ const root = el('div', { class: 'dd' }, [button, list]);
48
+
49
+ /** Options plus actions, in DOM order, for keyboard traversal. */
50
+ let focusables = [];
51
+ let activeIndex = -1;
52
+
53
+ function build() {
54
+ list.replaceChildren();
55
+ focusables = [];
56
+
57
+ for (const item of items) {
58
+ const option = el('div', {
59
+ class: `dd-option${item.value === value ? ' selected' : ''}${item.disabled ? ' disabled' : ''}${item.tone ? ` tone-${item.tone}` : ''}`,
60
+ role: 'option',
61
+ 'aria-selected': String(item.value === value),
62
+ 'aria-disabled': item.disabled ? 'true' : undefined,
63
+ tabindex: -1,
64
+ onclick: () => !item.disabled && choose(item.value),
65
+ }, [
66
+ el('span', { class: 'dd-check', 'aria-hidden': 'true', text: item.value === value ? '✓' : '' }),
67
+ el('span', { class: 'dd-text' }, [
68
+ el('span', { class: 'dd-label', text: item.label }),
69
+ item.detail ? el('span', { class: 'dd-detail', text: item.detail }) : null,
70
+ ]),
71
+ item.badge ? el('span', { class: 'dd-badge', text: item.badge }) : null,
72
+ ]);
73
+ list.append(option);
74
+ focusables.push({ node: option, run: () => !item.disabled && choose(item.value), text: item.label });
75
+ }
76
+
77
+ if (actions.length) {
78
+ list.append(el('div', { class: 'dd-divider', role: 'presentation' }));
79
+ for (const action of actions) {
80
+ const node = el('div', {
81
+ class: 'dd-option dd-action',
82
+ role: 'option',
83
+ 'aria-selected': 'false',
84
+ tabindex: -1,
85
+ onclick: () => {
86
+ close();
87
+ action.onSelect();
88
+ },
89
+ }, el('span', { class: 'dd-text' }, el('span', { class: 'dd-label', text: action.label })));
90
+ list.append(node);
91
+ focusables.push({ node, run: () => { close(); action.onSelect(); }, text: action.label });
92
+ }
93
+ }
94
+ }
95
+
96
+ function choose(next) {
97
+ close();
98
+ if (next === value) return;
99
+
100
+ /* The control shows what was chosen.
101
+
102
+ It used to only announce the choice and leave the button reading the
103
+ old label — which was invisible for years because every caller rebuilt
104
+ its whole surface in response, so a stale button never survived to be
105
+ seen. The page builder does not rebuild: it writes the value onto a
106
+ draft and stays put, and there the dropdown simply refused to change.
107
+
108
+ A control that does not reflect its own click is not a control. If the
109
+ caller disagrees — a connection that failed to open, say — it redraws
110
+ and says so, which is the same correction it was already making. */
111
+ value = next;
112
+ const chosen = items.find((i) => i.value === value);
113
+ button.querySelector('.dd-value').textContent = chosen?.label ?? '—';
114
+ button.title = chosen?.detail ?? chosen?.label ?? '';
115
+ onChange(next);
116
+ }
117
+
118
+ function setActive(index) {
119
+ if (!focusables.length) return;
120
+ // Wraps, because a list that stops at the end makes you reverse direction
121
+ // to reach the item one step past it.
122
+ activeIndex = (index + focusables.length) % focusables.length;
123
+ focusables.forEach(({ node }, i) => node.classList.toggle('active', i === activeIndex));
124
+ focusables[activeIndex].node.scrollIntoView({ block: 'nearest' });
125
+ }
126
+
127
+ function open() {
128
+ build();
129
+ list.hidden = false;
130
+ button.setAttribute('aria-expanded', 'true');
131
+ position();
132
+ const current = items.findIndex((i) => i.value === value);
133
+ setActive(current >= 0 ? current : 0);
134
+ // Listeners are added on open and removed on close, so a page with a
135
+ // dozen of these does not accumulate a dozen document handlers.
136
+ document.addEventListener('pointerdown', onOutside, true);
137
+ document.addEventListener('keydown', onKey, true);
138
+ }
139
+
140
+ function close() {
141
+ if (list.hidden) return;
142
+ list.hidden = true;
143
+ button.setAttribute('aria-expanded', 'false');
144
+ activeIndex = -1;
145
+ document.removeEventListener('pointerdown', onOutside, true);
146
+ document.removeEventListener('keydown', onKey, true);
147
+ window.removeEventListener('scroll', close, true);
148
+ window.removeEventListener('resize', close);
149
+ }
150
+
151
+ /**
152
+ * Place the open list in the viewport rather than in the flow.
153
+ *
154
+ * An absolutely positioned menu is clipped by any ancestor that scrolls,
155
+ * and the panels this control lives in all scroll — the composer's sidebar
156
+ * clips its overflow precisely so its sections can scroll independently, so
157
+ * a menu opening inside one was being cut in half.
158
+ *
159
+ * Fixed positioning escapes every clip, at the cost of not moving with the
160
+ * page, so the list closes on scroll rather than drifting away from the
161
+ * button that owns it.
162
+ */
163
+ function position() {
164
+ const box = button.getBoundingClientRect();
165
+ /* Sized *before* it is measured, which is the whole subtlety here.
166
+
167
+ The stylesheet gives the list `min-width: 100%` as a sensible default,
168
+ and a `position: fixed` element resolves percentages against the
169
+ viewport rather than against the button it belongs to — so an unsized
170
+ list measures as nearly the width of the screen. Measuring first and
171
+ correcting afterwards meant `width` was that screen-wide figure, the
172
+ clamp below decided the menu could not fit anywhere, and it was pinned
173
+ to the left edge of the window — yards from the control that opened it.
174
+ Visible on any dropdown in a narrow container; the driver picker inside
175
+ the connection dialog is where it was finally obvious. */
176
+ list.style.position = 'fixed';
177
+ list.style.minWidth = `${box.width}px`;
178
+ const width = Math.max(list.offsetWidth, box.width);
179
+ const height = list.offsetHeight;
180
+ // Flipped above the button when there is no room below, and pulled back
181
+ // from the right edge when the menu is wider than the space beside it.
182
+ const below = window.innerHeight - box.bottom;
183
+ list.style.top = below < height + 12 && box.top > height + 12
184
+ ? `${box.top - height - 4}px`
185
+ : `${box.bottom + 4}px`;
186
+ list.style.left = `${Math.max(8, Math.min(box.left, window.innerWidth - width - 12))}px`;
187
+ window.addEventListener('scroll', close, true);
188
+ window.addEventListener('resize', close);
189
+ }
190
+
191
+ function onOutside(e) {
192
+ if (!root.contains(e.target)) close();
193
+ }
194
+
195
+ let typed = '';
196
+ let typedAt = 0;
197
+
198
+ function onKey(e) {
199
+ if (list.hidden) return;
200
+ switch (e.key) {
201
+ case 'Escape':
202
+ e.preventDefault();
203
+ close();
204
+ button.focus();
205
+ return;
206
+ case 'ArrowDown':
207
+ e.preventDefault();
208
+ setActive(activeIndex + 1);
209
+ return;
210
+ case 'ArrowUp':
211
+ e.preventDefault();
212
+ setActive(activeIndex - 1);
213
+ return;
214
+ case 'Home':
215
+ e.preventDefault();
216
+ setActive(0);
217
+ return;
218
+ case 'End':
219
+ e.preventDefault();
220
+ setActive(focusables.length - 1);
221
+ return;
222
+ case 'Enter':
223
+ case ' ':
224
+ e.preventDefault();
225
+ focusables[activeIndex]?.run();
226
+ return;
227
+ case 'Tab':
228
+ // Tab means "leave", not "choose". Closing without selecting matches
229
+ // what a native select does.
230
+ close();
231
+ return;
232
+ default:
233
+ break;
234
+ }
235
+ if (e.key.length === 1 && !e.metaKey && !e.ctrlKey && !e.altKey) {
236
+ // Type-ahead. The one-second window is what makes "de" jump to "demo"
237
+ // rather than treating d and e as two separate jumps.
238
+ const now = Date.now();
239
+ typed = now - typedAt > 1000 ? e.key : typed + e.key;
240
+ typedAt = now;
241
+ const hit = focusables.findIndex((f) => f.text.toLowerCase().startsWith(typed.toLowerCase()));
242
+ if (hit >= 0) setActive(hit);
243
+ }
244
+ }
245
+
246
+ button.addEventListener('click', () => (list.hidden ? open() : close()));
247
+ button.addEventListener('keydown', (e) => {
248
+ if (list.hidden && (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ')) {
249
+ e.preventDefault();
250
+ open();
251
+ }
252
+ });
253
+
254
+ /**
255
+ * Set it from outside, without calling back.
256
+ *
257
+ * `connmanager` has always written `driver.value = parts.driver` when a
258
+ * pasted URL changes the driver, and until now that set an expando property
259
+ * on a `<div>` and did nothing at all. Pasting `postgres@127.0.0.1/db` —
260
+ * which is a *file path*, having no `://` — switched the form to SQLite and
261
+ * left the control still reading "postgres", so the dialog contradicted
262
+ * itself and the error came back as "unable to open database file".
263
+ *
264
+ * No `onChange`: the caller assigning this is the one making the decision,
265
+ * and calling it back would loop through whatever handler it set.
266
+ */
267
+ Object.defineProperty(root, 'value', {
268
+ get: () => value,
269
+ set: (next) => {
270
+ if (next === value) return;
271
+ value = next;
272
+ const chosen = items.find((i) => i.value === value);
273
+ button.querySelector('.dd-value').textContent = chosen?.label ?? '—';
274
+ button.title = chosen?.detail ?? chosen?.label ?? '';
275
+ },
276
+ });
277
+
278
+ return root;
279
+ }