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.
- package/LICENSE +21 -0
- package/README.md +553 -0
- package/dist/adapters/adapter.js +372 -0
- package/dist/adapters/connect.js +33 -0
- package/dist/adapters/mysql.js +951 -0
- package/dist/adapters/postgres.js +1000 -0
- package/dist/adapters/sqlite.js +781 -0
- package/dist/client/agent.js +262 -0
- package/dist/client/app.js +973 -0
- package/dist/client/arrange.js +254 -0
- package/dist/client/ask.js +133 -0
- package/dist/client/breakdown.js +317 -0
- package/dist/client/clauses.js +390 -0
- package/dist/client/columns.js +98 -0
- package/dist/client/complete.js +437 -0
- package/dist/client/compose.js +166 -0
- package/dist/client/composer.css +495 -0
- package/dist/client/composer.js +1972 -0
- package/dist/client/connections.js +234 -0
- package/dist/client/connmanager.js +962 -0
- package/dist/client/connurl.js +188 -0
- package/dist/client/core.js +893 -0
- package/dist/client/deeplink.js +270 -0
- package/dist/client/delete.js +144 -0
- package/dist/client/diagram.js +885 -0
- package/dist/client/dropdown.js +279 -0
- package/dist/client/export.js +456 -0
- package/dist/client/features.css +524 -0
- package/dist/client/findvalue.js +169 -0
- package/dist/client/grid.js +205 -0
- package/dist/client/handoff.js +153 -0
- package/dist/client/help.css +145 -0
- package/dist/client/help.js +881 -0
- package/dist/client/history.js +222 -0
- package/dist/client/index.html +116 -0
- package/dist/client/insert.js +151 -0
- package/dist/client/menu.js +160 -0
- package/dist/client/nested.js +255 -0
- package/dist/client/page.css +713 -0
- package/dist/client/page.js +1345 -0
- package/dist/client/pagebuilder.js +1222 -0
- package/dist/client/pagemarks.js +95 -0
- package/dist/client/palette.js +374 -0
- package/dist/client/peek.js +254 -0
- package/dist/client/picker.js +139 -0
- package/dist/client/pins.js +140 -0
- package/dist/client/prompt.js +129 -0
- package/dist/client/record.js +707 -0
- package/dist/client/schemaexport.js +242 -0
- package/dist/client/schematext.js +125 -0
- package/dist/client/shape.js +178 -0
- package/dist/client/shapecheck.js +129 -0
- package/dist/client/skeleton.js +139 -0
- package/dist/client/sql.css +126 -0
- package/dist/client/sql.js +398 -0
- package/dist/client/sqlcomplete.js +163 -0
- package/dist/client/sqlsaved.js +107 -0
- package/dist/client/style.css +2711 -0
- package/dist/client/summary.js +259 -0
- package/dist/client/table.js +1035 -0
- package/dist/client/template.js +539 -0
- package/dist/client/theme.js +74 -0
- package/dist/client/tour.js +324 -0
- package/dist/client/undo.js +105 -0
- package/dist/client/url.js +166 -0
- package/dist/client/value.js +223 -0
- package/dist/client/views.js +215 -0
- package/dist/client/virtual.js +176 -0
- package/dist/client/welcome.js +170 -0
- package/dist/client/write.js +414 -0
- package/dist/server/changeimpact.js +195 -0
- package/dist/server/connections.js +615 -0
- package/dist/server/constraints.js +62 -0
- package/dist/server/credentials.js +230 -0
- package/dist/server/fixture.js +199 -0
- package/dist/server/graph.js +194 -0
- package/dist/server/impact.js +48 -0
- package/dist/server/index.js +2204 -0
- package/dist/server/journal.js +173 -0
- package/dist/server/layouts.js +128 -0
- package/dist/server/mcp.js +2840 -0
- package/dist/server/shapeonly.js +91 -0
- package/dist/shared/breakdown.js +231 -0
- package/dist/shared/breakdowntext.js +257 -0
- package/dist/shared/diff.js +130 -0
- package/dist/shared/like.js +29 -0
- package/dist/shared/lint.js +149 -0
- package/dist/shared/order.js +133 -0
- package/dist/shared/page.js +932 -0
- package/dist/shared/query.js +831 -0
- package/dist/shared/recordview.js +343 -0
- package/dist/shared/schema.js +377 -0
- package/dist/shared/sqlsaved.js +67 -0
- package/dist/shared/view.js +981 -0
- package/dist/shared/viewtext.js +273 -0
- package/dist/shared/vocabulary.js +164 -0
- package/package.json +57 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Moving around a result without a mouse.
|
|
3
|
+
*
|
|
4
|
+
* A table of rows you can only click is a table you have to keep leaving the
|
|
5
|
+
* keyboard for. The query bar, the sidebar and the palette are all reachable
|
|
6
|
+
* by key; the grid the whole tool exists to show was the one surface that was
|
|
7
|
+
* not, so reading a wide result meant typing, then reaching for the mouse,
|
|
8
|
+
* then typing again.
|
|
9
|
+
*
|
|
10
|
+
* Arrow keys move a cell cursor, Enter opens the row's record — which is the
|
|
11
|
+
* walk, done from the keyboard. Everything else here follows from those two
|
|
12
|
+
* and from the grid pattern people already know from a spreadsheet.
|
|
13
|
+
*
|
|
14
|
+
* The position is held as `(row, column)` **indices, not elements**. Above a
|
|
15
|
+
* few hundred rows the table is virtualised and the `<tr>` under the cursor is
|
|
16
|
+
* destroyed the moment it scrolls out of the window, so a reference to it is a
|
|
17
|
+
* reference to a node that will be thrown away. Indices survive that; the
|
|
18
|
+
* element is looked up again on every move, and when the row has not been
|
|
19
|
+
* painted yet the scroller is moved and the lookup retried.
|
|
20
|
+
*/
|
|
21
|
+
/** Rows moved by PageUp/PageDown when the viewport cannot be measured. */
|
|
22
|
+
const PAGE_FALLBACK = 20;
|
|
23
|
+
|
|
24
|
+
/** How many frames to wait for the virtualiser to paint a row we scrolled to. */
|
|
25
|
+
const PAINT_TRIES = 4;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {HTMLElement} wrap the element holding the table
|
|
29
|
+
* @param {object} options
|
|
30
|
+
* @param {HTMLTableSectionElement} options.tbody
|
|
31
|
+
* @param {number} options.rowCount logical rows, not painted ones
|
|
32
|
+
* @param {number} options.columnCount data columns
|
|
33
|
+
* @param {number} options.offset leading cells before the first data column
|
|
34
|
+
* @param {(index: number) => void} [options.onOpen] Enter on a row
|
|
35
|
+
*/
|
|
36
|
+
export function initGrid(wrap, { tbody, rowCount, columnCount, offset = 0, onOpen, totalRows, firstRow = 0 }) {
|
|
37
|
+
if (!rowCount || !columnCount) return;
|
|
38
|
+
|
|
39
|
+
const table = wrap.querySelector('table');
|
|
40
|
+
if (!table) return;
|
|
41
|
+
table.setAttribute('role', 'grid');
|
|
42
|
+
|
|
43
|
+
/* How much there is, and where in it this page sits.
|
|
44
|
+
|
|
45
|
+
Without these a screen reader announces "row 3 of 20" on a table of
|
|
46
|
+
63,010 — it can only count the rows in the DOM, and a paged grid puts
|
|
47
|
+
twenty of them there. The visual reader has "20 of 63,010 rows" in the
|
|
48
|
+
header; this is the same sentence for everyone else. `aria-rowindex` on
|
|
49
|
+
each row is what makes the count mean anything: it says which twenty. */
|
|
50
|
+
table.setAttribute('aria-colcount', String(columnCount + offset));
|
|
51
|
+
if (typeof totalRows === 'number') {
|
|
52
|
+
table.setAttribute('aria-rowcount', String(totalRows));
|
|
53
|
+
/* Every row carries its index, including on the first page. A count with
|
|
54
|
+
no indexes to anchor it is worse than neither: the reader is told there
|
|
55
|
+
are sixty-three thousand rows and given no way to know which of them
|
|
56
|
+
these twenty are. Header first — `aria-rowindex` is 1-based across the
|
|
57
|
+
whole grid, and the header is row 1 of it. */
|
|
58
|
+
table.querySelector('thead tr')?.setAttribute('aria-rowindex', '1');
|
|
59
|
+
for (let i = 0; i < rowCount; i += 1) {
|
|
60
|
+
tbody.querySelector(`tr[data-row="${i}"]`)?.setAttribute('aria-rowindex', String(firstRow + i + 2));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let row = -1;
|
|
65
|
+
let col = 0;
|
|
66
|
+
|
|
67
|
+
const scrollerOf = () => wrap.closest('.content') ?? document.scrollingElement;
|
|
68
|
+
const rowAt = (i) => tbody.querySelector(`tr[data-row="${i}"]`);
|
|
69
|
+
const cellAt = (i, c) => rowAt(i)?.children[c + offset] ?? null;
|
|
70
|
+
|
|
71
|
+
/** Rows that fit on screen, for PageUp and PageDown. */
|
|
72
|
+
const pageSize = () => {
|
|
73
|
+
const sample = tbody.querySelector('tr[data-row]');
|
|
74
|
+
const height = sample?.getBoundingClientRect().height;
|
|
75
|
+
const view = scrollerOf()?.clientHeight;
|
|
76
|
+
if (!height || !view) return PAGE_FALLBACK;
|
|
77
|
+
return Math.max(1, Math.floor(view / height) - 1);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Put the cursor on a cell, painting the row first if it does not exist.
|
|
82
|
+
*
|
|
83
|
+
* The retry is what makes this work on a virtualised table: scrolling
|
|
84
|
+
* schedules a repaint rather than performing one, so the row asked for is
|
|
85
|
+
* not in the DOM on the line after the scroll. Rather than guess at a delay,
|
|
86
|
+
* it looks again on the next frame, a few times, and gives up quietly — a
|
|
87
|
+
* cursor that does not move is better than one that throws.
|
|
88
|
+
*/
|
|
89
|
+
const focusCell = (tries = PAINT_TRIES) => {
|
|
90
|
+
const cell = cellAt(row, col);
|
|
91
|
+
if (!cell) {
|
|
92
|
+
if (tries <= 0) return;
|
|
93
|
+
const sample = tbody.querySelector('tr[data-row]');
|
|
94
|
+
const height = sample?.getBoundingClientRect().height ?? 0;
|
|
95
|
+
const scroller = scrollerOf();
|
|
96
|
+
if (scroller && height) {
|
|
97
|
+
scroller.scrollTop = Math.max(0, row * height - scroller.clientHeight / 2);
|
|
98
|
+
}
|
|
99
|
+
requestAnimationFrame(() => focusCell(tries - 1));
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
for (const stale of tbody.querySelectorAll('.cell-cursor')) {
|
|
103
|
+
stale.classList.remove('cell-cursor');
|
|
104
|
+
stale.removeAttribute('tabindex');
|
|
105
|
+
}
|
|
106
|
+
cell.classList.add('cell-cursor');
|
|
107
|
+
cell.tabIndex = 0;
|
|
108
|
+
/* The grid's tab stop moves to the cell, so tabbing in returns to where
|
|
109
|
+
the cursor was left rather than to the top-left corner — and the table
|
|
110
|
+
and the cell are never both stops, which would make Tab pass through
|
|
111
|
+
the same grid twice. */
|
|
112
|
+
table.tabIndex = -1;
|
|
113
|
+
cell.focus({ preventScroll: true });
|
|
114
|
+
/* `nearest` rather than `center`: the cursor usually moves one row, and
|
|
115
|
+
re-centring the table on every arrow press turns a walk down a column
|
|
116
|
+
into the page jumping under you. */
|
|
117
|
+
cell.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const moveTo = (nextRow, nextCol) => {
|
|
121
|
+
row = Math.max(0, Math.min(rowCount - 1, nextRow));
|
|
122
|
+
col = Math.max(0, Math.min(columnCount - 1, nextCol));
|
|
123
|
+
focusCell();
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
/* Reachable by Tab.
|
|
127
|
+
|
|
128
|
+
One stop for the whole grid, not one per cell: a table of twenty rows and
|
|
129
|
+
eight columns is a hundred and sixty tab stops between the query bar and
|
|
130
|
+
whatever is after it, which is not navigation, it is a wall. */
|
|
131
|
+
table.tabIndex = 0;
|
|
132
|
+
table.addEventListener('focus', (e) => {
|
|
133
|
+
if (e.target !== table) return;
|
|
134
|
+
moveTo(row < 0 ? 0 : row, col);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// A click is also a way of saying where the cursor is, so the keyboard and
|
|
138
|
+
// the mouse do not disagree about where you are.
|
|
139
|
+
tbody.addEventListener('mousedown', (e) => {
|
|
140
|
+
const cell = e.target.closest('td');
|
|
141
|
+
const tr = cell?.closest('tr[data-row]');
|
|
142
|
+
if (!cell || !tr) return;
|
|
143
|
+
row = Number(tr.dataset.row);
|
|
144
|
+
col = Math.max(0, [...tr.children].indexOf(cell) - offset);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
table.addEventListener('keydown', (e) => {
|
|
148
|
+
if (e.target.closest('input, textarea, select, [contenteditable]')) return;
|
|
149
|
+
if (e.altKey) return;
|
|
150
|
+
|
|
151
|
+
const jump = e.metaKey || e.ctrlKey;
|
|
152
|
+
switch (e.key) {
|
|
153
|
+
case 'ArrowDown': moveTo(jump ? rowCount - 1 : row + 1, col); break;
|
|
154
|
+
case 'ArrowUp': moveTo(jump ? 0 : row - 1, col); break;
|
|
155
|
+
case 'ArrowRight': moveTo(row, jump ? columnCount - 1 : col + 1); break;
|
|
156
|
+
case 'ArrowLeft': moveTo(row, jump ? 0 : col - 1); break;
|
|
157
|
+
case 'Home': moveTo(jump ? 0 : row, 0); break;
|
|
158
|
+
case 'End': moveTo(jump ? rowCount - 1 : row, columnCount - 1); break;
|
|
159
|
+
case 'PageDown': moveTo(row + pageSize(), col); break;
|
|
160
|
+
case 'PageUp': moveTo(row - pageSize(), col); break;
|
|
161
|
+
case 'Enter':
|
|
162
|
+
if (row >= 0 && onOpen) onOpen(row);
|
|
163
|
+
break;
|
|
164
|
+
case 'Escape':
|
|
165
|
+
/* Back to the query bar rather than nowhere. Escape from a grid means
|
|
166
|
+
"I am done here", and the thing you do next is nearly always type. */
|
|
167
|
+
table.blur();
|
|
168
|
+
document.getElementById('query')?.focus();
|
|
169
|
+
break;
|
|
170
|
+
case 'c':
|
|
171
|
+
if (!jump) return;
|
|
172
|
+
// The cell under the cursor, not the row: someone pressing ⌘C on a
|
|
173
|
+
// reference id wants that id.
|
|
174
|
+
void copyCell(cellAt(row, col));
|
|
175
|
+
break;
|
|
176
|
+
default:
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
e.preventDefault();
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Copy one cell.
|
|
185
|
+
*
|
|
186
|
+
* `title` before `textContent`, because a value the table shortened shows an
|
|
187
|
+
* ellipsis and keeps the whole thing in its tooltip — copying what is drawn
|
|
188
|
+
* would hand back a truncated value that looks complete.
|
|
189
|
+
*/
|
|
190
|
+
async function copyCell(cell) {
|
|
191
|
+
if (!cell) return;
|
|
192
|
+
const text = cell.getAttribute('title') ?? cell.textContent ?? '';
|
|
193
|
+
try {
|
|
194
|
+
await navigator.clipboard.writeText(text);
|
|
195
|
+
flash(cell);
|
|
196
|
+
} catch {
|
|
197
|
+
/* Clipboard access can be refused, and there is nothing useful to say
|
|
198
|
+
about it: the value is on screen and can be selected by hand. */
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function flash(cell) {
|
|
203
|
+
cell.classList.add('cell-copied');
|
|
204
|
+
setTimeout(() => cell.classList.remove('cell-copied'), 400);
|
|
205
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hand the walk to an agent.
|
|
3
|
+
*
|
|
4
|
+
* The MCP server is the agent→database half of this tool. This is the other
|
|
5
|
+
* half: a person walks to something interesting — three joins deep, a filter
|
|
6
|
+
* they arrived at by clicking — and now wants an agent to carry on from
|
|
7
|
+
* exactly there. Retyping the position into a chat window is where that
|
|
8
|
+
* hand-off usually dies, because the query is the easy part and the shape,
|
|
9
|
+
* the connection and the keys are the parts people leave out.
|
|
10
|
+
*
|
|
11
|
+
* What it writes is deliberately not the rows. The rows are on screen and
|
|
12
|
+
* the person can copy them; pasting them into a chat window sends them
|
|
13
|
+
* somewhere, and that should stay a decision rather than a side effect of
|
|
14
|
+
* clicking "copy". What it writes is everything an agent needs to fetch the
|
|
15
|
+
* rows itself, against the same connection, with the same question.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** `name → type` with the markers the rest of the tool uses for keys and references. */
|
|
19
|
+
function columnLine(column) {
|
|
20
|
+
const parts = [column.name];
|
|
21
|
+
if (column.type) parts.push(column.type);
|
|
22
|
+
if (column.primaryKey) parts.push('key');
|
|
23
|
+
if (column.nullable === false && !column.primaryKey) parts.push('not null');
|
|
24
|
+
if (column.references) parts.push(`→ ${column.references.table}.${column.references.column}`);
|
|
25
|
+
return `- ${parts.join(' · ')}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The walk position as markdown, ready to paste into an agent.
|
|
30
|
+
*
|
|
31
|
+
* Pure so it can be pinned without a browser — the failure this feature has
|
|
32
|
+
* is silently omitting a field, and the only way to see that is to read the
|
|
33
|
+
* whole document a test wrote.
|
|
34
|
+
*/
|
|
35
|
+
export function agentContext(position) {
|
|
36
|
+
const { connection, dialect, table, query, columns = [], shown, total, meta, trail = [], record } = position;
|
|
37
|
+
const lines = [];
|
|
38
|
+
|
|
39
|
+
lines.push(`# ${table} — a position in tablewalk`);
|
|
40
|
+
lines.push('');
|
|
41
|
+
lines.push(`Connection \`${connection ?? 'default'}\`${dialect ? ` (${dialect})` : ''}.`);
|
|
42
|
+
lines.push('');
|
|
43
|
+
|
|
44
|
+
if (record) {
|
|
45
|
+
lines.push(`One row of \`${table}\`: **${record.label}**`);
|
|
46
|
+
lines.push('');
|
|
47
|
+
lines.push('```json');
|
|
48
|
+
lines.push(JSON.stringify(record.key));
|
|
49
|
+
lines.push('```');
|
|
50
|
+
lines.push('');
|
|
51
|
+
} else {
|
|
52
|
+
lines.push('The question on screen is:');
|
|
53
|
+
lines.push('');
|
|
54
|
+
lines.push('```');
|
|
55
|
+
lines.push(query);
|
|
56
|
+
lines.push('```');
|
|
57
|
+
lines.push('');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!record && typeof shown === 'number') {
|
|
61
|
+
const of = typeof total === 'number' && total > shown
|
|
62
|
+
? ` of ${total.toLocaleString()} matching`
|
|
63
|
+
: '';
|
|
64
|
+
lines.push(`Showing ${shown.toLocaleString()} row${shown === 1 ? '' : 's'}${of}.`);
|
|
65
|
+
lines.push('');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/* The columns the *query* returned, not the whole table: a projection is
|
|
69
|
+
part of the position, and an agent handed the full table would widen the
|
|
70
|
+
question without being told it had. */
|
|
71
|
+
if (!record && columns.length) {
|
|
72
|
+
lines.push(`Columns on screen: ${columns.join(', ')}`);
|
|
73
|
+
lines.push('');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (meta?.columns?.length) {
|
|
77
|
+
lines.push(`## ${meta.id ?? table}`);
|
|
78
|
+
lines.push('');
|
|
79
|
+
for (const column of meta.columns) lines.push(columnLine(column));
|
|
80
|
+
lines.push('');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/* How they got here. A filter arrived at by three clicks reads as one
|
|
84
|
+
opaque `where` clause, and the trail is what makes it a story again. */
|
|
85
|
+
if (trail.length > 1) {
|
|
86
|
+
lines.push('## The walk');
|
|
87
|
+
lines.push('');
|
|
88
|
+
for (const step of trail) lines.push(`- ${step}`);
|
|
89
|
+
lines.push('');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
lines.push('## Carry on from here');
|
|
93
|
+
lines.push('');
|
|
94
|
+
lines.push(`This database is available over MCP as \`tablewalk\`. ${
|
|
95
|
+
record ? 'To read this row and everything pointing at it:' : 'To see the same rows:'
|
|
96
|
+
}`);
|
|
97
|
+
lines.push('');
|
|
98
|
+
lines.push('```json');
|
|
99
|
+
lines.push(JSON.stringify(
|
|
100
|
+
record
|
|
101
|
+
? { name: 'record', arguments: { connection: connection ?? undefined, table, key: record.key } }
|
|
102
|
+
: { name: 'query', arguments: { connection: connection ?? undefined, q: query, limit: shown ?? 20 } },
|
|
103
|
+
null,
|
|
104
|
+
2,
|
|
105
|
+
));
|
|
106
|
+
lines.push('```');
|
|
107
|
+
lines.push('');
|
|
108
|
+
lines.push(
|
|
109
|
+
'`table` describes a table, `find` says where a name or a value lives, and '
|
|
110
|
+
+ '`query` runs the same language as the box above. The values themselves are '
|
|
111
|
+
+ 'not included here — fetch them if you need them.',
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
return lines.join('\n');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Everything the current walk knows about where it is.
|
|
119
|
+
*
|
|
120
|
+
* Reads `state` rather than taking it apart at each call site, because the
|
|
121
|
+
* two callers — a result and a record — differ in one field and agreeing on
|
|
122
|
+
* the other seven by hand is how they drift.
|
|
123
|
+
*/
|
|
124
|
+
export function positionOf(state, entry, extra = {}) {
|
|
125
|
+
const meta = state.schema?.tables?.find((t) => t.id === entry.table) ?? null;
|
|
126
|
+
return {
|
|
127
|
+
connection: state.activeConnection ?? state.schema?.label ?? null,
|
|
128
|
+
dialect: state.schema?.dialect ?? null,
|
|
129
|
+
table: entry.table,
|
|
130
|
+
query: entry.query ?? entry.table,
|
|
131
|
+
meta,
|
|
132
|
+
/* The trail as it reads in the breadcrumb: the query for a result, the
|
|
133
|
+
row's name for a record. */
|
|
134
|
+
trail: (state.stack ?? []).map((step) => (step.kind === 'table' ? step.query : step.label)),
|
|
135
|
+
...extra,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Copy a position, and say so.
|
|
141
|
+
*
|
|
142
|
+
* The clipboard can be refused — an insecure origin, a permission denied —
|
|
143
|
+
* and a button that silently does nothing is worse than one that says it
|
|
144
|
+
* could not. The text is not shown as a fallback because it is a page long.
|
|
145
|
+
*/
|
|
146
|
+
export async function copyPosition(position, toast) {
|
|
147
|
+
try {
|
|
148
|
+
await navigator.clipboard.writeText(agentContext(position));
|
|
149
|
+
toast('Position copied — paste it into an agent.', 'info');
|
|
150
|
+
} catch {
|
|
151
|
+
toast('The browser refused clipboard access, so nothing was copied.', 'error');
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/* The query reference: the panel behind `?`, and the runnable examples on the
|
|
2
|
+
landing page.
|
|
3
|
+
|
|
4
|
+
In its own file rather than style.css because it is a self-contained feature
|
|
5
|
+
and the stylesheet is already 1600 lines. Nothing here names a colour: every
|
|
6
|
+
value is one of the tokens defined at the top of style.css, so the panel
|
|
7
|
+
follows the dark scheme without a second set of rules to keep in step. */
|
|
8
|
+
|
|
9
|
+
/* ---------- the landing page's examples ---------- */
|
|
10
|
+
|
|
11
|
+
.landing-examples { margin-bottom: 26px; }
|
|
12
|
+
.landing-examples-head {
|
|
13
|
+
display: flex; align-items: baseline; gap: 12px;
|
|
14
|
+
margin-bottom: 10px;
|
|
15
|
+
}
|
|
16
|
+
.landing-examples-head .landing-heading { margin: 0; }
|
|
17
|
+
.help-more { font-size: 11.5px; padding: 3px 9px; }
|
|
18
|
+
|
|
19
|
+
/* Wrapping columns rather than a row: three queries on a narrow window
|
|
20
|
+
otherwise shrink until the text they are teaching is ellipsised, which is
|
|
21
|
+
the one thing an example cannot survive. */
|
|
22
|
+
.help-starters {
|
|
23
|
+
display: grid; gap: 8px;
|
|
24
|
+
grid-template-columns: repeat(auto-fit, minmax(19rem, 1fr));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/* ---------- an example, wherever it appears ---------- */
|
|
28
|
+
|
|
29
|
+
/* A button that looks like a line of query text, because that is what it puts
|
|
30
|
+
in the bar. The border only firms up under the pointer: a page of examples
|
|
31
|
+
each in its own box reads as a form to fill in. */
|
|
32
|
+
.help-example {
|
|
33
|
+
display: flex; flex-direction: column; align-items: flex-start; gap: 3px;
|
|
34
|
+
width: 100%; padding: 9px 11px; text-align: left; cursor: pointer;
|
|
35
|
+
background: var(--surface);
|
|
36
|
+
border: 1px solid var(--border); border-radius: 8px;
|
|
37
|
+
font: inherit; color: var(--text);
|
|
38
|
+
transition: border-color var(--t-hover) var(--ease-out),
|
|
39
|
+
background var(--t-hover) var(--ease-out);
|
|
40
|
+
}
|
|
41
|
+
.help-example:hover,
|
|
42
|
+
.help-example:focus-visible {
|
|
43
|
+
border-color: var(--accent); background: var(--accent-soft); outline: none;
|
|
44
|
+
}
|
|
45
|
+
.help-query {
|
|
46
|
+
font: 12.5px/1.5 var(--mono); color: var(--text);
|
|
47
|
+
/* Long paths wrap instead of scrolling sideways: an example you have to
|
|
48
|
+
scroll to read is one you cannot decide to click. */
|
|
49
|
+
overflow-wrap: anywhere;
|
|
50
|
+
}
|
|
51
|
+
.help-example:hover .help-query,
|
|
52
|
+
.help-example:focus-visible .help-query { color: var(--accent); }
|
|
53
|
+
.help-caption { font-size: 11.5px; color: var(--text-dim); }
|
|
54
|
+
|
|
55
|
+
/* ---------- the panel ---------- */
|
|
56
|
+
|
|
57
|
+
.help-backdrop {
|
|
58
|
+
position: fixed; inset: 0; z-index: 60;
|
|
59
|
+
background: rgb(0 0 0 / 0.28);
|
|
60
|
+
display: flex; justify-content: center;
|
|
61
|
+
padding: 8vh 16px 16px;
|
|
62
|
+
}
|
|
63
|
+
.help-panel {
|
|
64
|
+
width: min(720px, 96vw); max-height: 84vh;
|
|
65
|
+
display: flex; flex-direction: column;
|
|
66
|
+
background: var(--surface);
|
|
67
|
+
border: 1px solid var(--border-strong);
|
|
68
|
+
border-radius: 10px;
|
|
69
|
+
box-shadow: 0 18px 60px rgb(0 0 0 / 0.28);
|
|
70
|
+
overflow: hidden;
|
|
71
|
+
}
|
|
72
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
73
|
+
.help-panel {
|
|
74
|
+
animation: help-in var(--t-panel) var(--ease-out);
|
|
75
|
+
}
|
|
76
|
+
@keyframes help-in {
|
|
77
|
+
from { opacity: 0; transform: translateY(-6px); }
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.help-head {
|
|
82
|
+
display: flex; align-items: flex-start; gap: 12px;
|
|
83
|
+
padding: 14px 16px; flex: none;
|
|
84
|
+
border-bottom: 1px solid var(--border);
|
|
85
|
+
}
|
|
86
|
+
.help-title { margin: 0; font: 600 15px var(--sans); letter-spacing: -0.01em; }
|
|
87
|
+
.help-sub { margin: 3px 0 0; font-size: 12px; color: var(--text-dim); }
|
|
88
|
+
.help-close {
|
|
89
|
+
margin-left: auto; flex: none;
|
|
90
|
+
padding: 2px 8px; line-height: 1.2;
|
|
91
|
+
background: none; border: 1px solid transparent; border-radius: 6px;
|
|
92
|
+
color: var(--text-faint); cursor: pointer; font: 16px var(--mono);
|
|
93
|
+
}
|
|
94
|
+
.help-close:hover, .help-close:focus-visible {
|
|
95
|
+
color: var(--accent); border-color: var(--border); outline: none;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.help-body { overflow-y: auto; padding: 4px 16px 16px; }
|
|
99
|
+
|
|
100
|
+
.help-section { padding: 14px 0; border-bottom: 1px solid var(--border); }
|
|
101
|
+
.help-section:last-child { border-bottom: none; }
|
|
102
|
+
.help-section-title {
|
|
103
|
+
margin: 0 0 4px;
|
|
104
|
+
font: 600 10.5px var(--sans); text-transform: uppercase; letter-spacing: 0.07em;
|
|
105
|
+
color: var(--text-dim);
|
|
106
|
+
}
|
|
107
|
+
.help-note { margin: 0 0 10px; font-size: 12.5px; color: var(--text); max-width: 60ch; }
|
|
108
|
+
|
|
109
|
+
/* Syntax on the left, what it means on the right. A two-column grid rather
|
|
110
|
+
than a table so a long form wraps under itself instead of squeezing the
|
|
111
|
+
meaning into a column an inch wide. */
|
|
112
|
+
.help-grammar {
|
|
113
|
+
margin: 0 0 10px;
|
|
114
|
+
display: grid; grid-template-columns: minmax(9rem, auto) 1fr;
|
|
115
|
+
gap: 3px 14px; align-items: baseline;
|
|
116
|
+
}
|
|
117
|
+
.help-grammar > div { display: contents; }
|
|
118
|
+
.help-grammar dt { margin: 0; }
|
|
119
|
+
.help-grammar dt code {
|
|
120
|
+
font: 12px var(--mono); color: var(--accent);
|
|
121
|
+
background: var(--accent-soft); border-radius: 4px; padding: 1px 5px;
|
|
122
|
+
}
|
|
123
|
+
.help-grammar dd { margin: 0; font-size: 11.5px; color: var(--text-dim); }
|
|
124
|
+
|
|
125
|
+
.help-examples { display: grid; gap: 6px; }
|
|
126
|
+
.help-examples .help-example { background: var(--surface-2); }
|
|
127
|
+
.help-examples .help-example:hover,
|
|
128
|
+
.help-examples .help-example:focus-visible { background: var(--accent-soft); }
|
|
129
|
+
|
|
130
|
+
.help-absent { margin: 0; font-size: 12px; color: var(--text-faint); }
|
|
131
|
+
|
|
132
|
+
@media (max-width: 560px) {
|
|
133
|
+
.help-grammar { grid-template-columns: 1fr; gap: 1px; }
|
|
134
|
+
.help-grammar dd { margin-bottom: 6px; }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/* ---------- the examples, as the summary card's foot ---------- */
|
|
138
|
+
|
|
139
|
+
/* Inside the shape card they are a continuation of it, not a section: the
|
|
140
|
+
heading and the margin that separated two stacked panels were pure cost
|
|
141
|
+
once there was only one panel. */
|
|
142
|
+
.shape-body .landing-examples { margin: 12px 0 0; padding-top: 11px; border-top: 1px solid var(--border); }
|
|
143
|
+
.shape-body .landing-examples-head { margin-bottom: 8px; }
|
|
144
|
+
.shape-body .landing-heading { font: 10px var(--mono); text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-faint); }
|
|
145
|
+
.shape-body .help-example { padding: 7px 10px; }
|