@bakery-framework/plugin-db-explorer 2.0.0-alpha.9 → 2.0.0-rc.2

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 (49) hide show
  1. package/package.json +5 -5
  2. package/src/access.ts +53 -17
  3. package/src/client/api.ts +48 -6
  4. package/src/client/bulk.ts +31 -7
  5. package/src/client/cell.ts +5 -5
  6. package/src/client/confirm.ts +11 -12
  7. package/src/client/csv-commit.ts +25 -5
  8. package/src/client/csv-map.ts +7 -7
  9. package/src/client/csv-model.ts +61 -8
  10. package/src/client/csv-pick.ts +1 -1
  11. package/src/client/csv-preview.ts +2 -2
  12. package/src/client/csv.ts +2 -2
  13. package/src/client/dom.ts +3 -5
  14. package/src/client/edit-session.ts +7 -7
  15. package/src/client/editors.ts +5 -5
  16. package/src/client/filter-builder.ts +6 -6
  17. package/src/client/fk.ts +9 -9
  18. package/src/client/grid-body.ts +4 -4
  19. package/src/client/grid-header.ts +2 -2
  20. package/src/client/grid-rowbar.ts +1 -1
  21. package/src/client/grid.ts +8 -9
  22. package/src/client/meta.ts +6 -6
  23. package/src/client/page.ts +4 -4
  24. package/src/client/panel.ts +8 -8
  25. package/src/client/relations.ts +4 -4
  26. package/src/client/save.ts +4 -4
  27. package/src/client/sidebar.ts +6 -6
  28. package/src/client/state.ts +4 -4
  29. package/src/client/statusbar.ts +6 -6
  30. package/src/client/structure.ts +8 -8
  31. package/src/client/tabs.ts +10 -10
  32. package/src/client/tabstrip.ts +3 -3
  33. package/src/client.ts +17 -17
  34. package/src/endpoints/common.ts +25 -6
  35. package/src/endpoints/graph.ts +5 -5
  36. package/src/endpoints/import.ts +5 -5
  37. package/src/endpoints/read.ts +70 -10
  38. package/src/endpoints/rows.ts +219 -14
  39. package/src/identity.ts +122 -52
  40. package/src/index.ts +1 -1
  41. package/src/policy.ts +4 -4
  42. package/src/preview.ts +5 -5
  43. package/src/setup.ts +16 -10
  44. package/src/shared/coerce.ts +21 -22
  45. package/src/shared/csv.ts +5 -5
  46. package/src/shared/filters.ts +6 -6
  47. package/src/shared/plan.ts +7 -7
  48. package/src/shell.ts +3 -3
  49. package/src/validate.ts +10 -10
@@ -1,14 +1,14 @@
1
1
  /**
2
2
  * The import wizard's state, with no DOM anywhere in it.
3
3
  *
4
- * The wizard has five steps and the interesting behaviour is all in the
5
- * transitions between them — a re-pick *moves* a database column rather than
4
+ * The wizard has five steps and the interesting behavior is all in the
5
+ * transitions between them: a re-pick *moves* a database column rather than
6
6
  * duplicating it, an unmapped NOT NULL column blocks, `empty → NULL` defaults
7
7
  * differently per kind, and the preview recomputes from the mapping every
8
8
  * time. None of that is testable through a rendered `<select>`, so none of it
9
9
  * lives there. `csv.ts` is the view; this is the machine.
10
10
  *
11
- * Parsing, coercion and mapping are **not reimplemented** — `shared/csv.ts`,
11
+ * Parsing, coercion and mapping are **not reimplemented**: `shared/csv.ts`,
12
12
  * `shared/coerce.ts` and `shared/plan.ts` are the same modules the server runs,
13
13
  * which is the only way the preview and the outcome agree.
14
14
  */
@@ -58,7 +58,7 @@ export function targetOf(assignment: Assignment): string | null {
58
58
  /**
59
59
  * Does row 0 name the columns?
60
60
  *
61
- * Non-empty, unique, and not a number — the three properties a header row has
61
+ * Non-empty, unique, and not a number: the three properties a header row has
62
62
  * and a data row almost never does. Deliberately conservative in the direction
63
63
  * of *yes*: guessing "header" for a data file loses one row in a preview the
64
64
  * user is looking at, while guessing "data" for a header file maps nothing and
@@ -91,8 +91,8 @@ function square(fields: string[], width: number): string[] {
91
91
  /**
92
92
  * Empty means NULL by default for everything except text.
93
93
  *
94
- * `''` in a numeric or date column is never a value — `coerceValue` refuses it
95
- * outright with `empty_string` — so defaulting the toggle off there guarantees
94
+ * `''` in a numeric or date column is never a value (`coerceValue` refuses it
95
+ * outright with `empty_string`), so defaulting the toggle off there guarantees
96
96
  * a failure the user then has to diagnose. In a text column `''` *is* a value,
97
97
  * and silently turning every blank cell into NULL would be the dashboard's bug
98
98
  * with a checkbox on it.
@@ -161,7 +161,7 @@ export function buildModel(
161
161
  *
162
162
  * Any other header holding the same database column falls back to skip. That
163
163
  * makes a duplicate mapping impossible to express, which is a stronger promise
164
- * than `blockingIssues`' `duplicate_target` — that check stays as the
164
+ * than `blockingIssues`' `duplicate_target`: that check stays as the
165
165
  * server-side guarantee, because the server accepts a mapping this UI did not
166
166
  * build.
167
167
  */
@@ -345,10 +345,54 @@ export interface BuildResult {
345
345
  * walks over fifty thousand rows is the difference between a responsive dialog
346
346
  * and a frozen tab.
347
347
  */
348
+ /**
349
+ * The last build, so a change that cannot alter it does not redo it.
350
+ *
351
+ * One slot, so it is bounded by construction (convention 6). Compared by
352
+ * **reference**, not by value: every update goes through a spread
353
+ * (`{ ...model, onBadRow }`), so an untouched field keeps its identity and a
354
+ * touched one does not. Comparing by value would mean walking the whole file
355
+ * to decide whether to walk the whole file.
356
+ *
357
+ * The key is exactly what this function reads - `headers`, `rows`, `assign`,
358
+ * `emptyToNull` and the columns - and deliberately not `onBadRow`, `ragged`,
359
+ * `delimiter` or `hasHeader`. Those change what the *import* does, never what
360
+ * the records are, and the bad-row policy is a dropdown in the same footer
361
+ * that displays this result. Picking one made the footer coerce every row of
362
+ * the file to arrive at the count it already had on screen.
363
+ *
364
+ * Measured on 50,000 rows of four columns: 153-250 ms per change, on the main
365
+ * thread, for a number that did not move.
366
+ */
367
+ let lastBuild: {
368
+ headers: unknown
369
+ rows: unknown
370
+ assign: unknown
371
+ emptyToNull: unknown
372
+ columns: unknown
373
+ result: BuildResult
374
+ } | null = null
375
+
376
+ /** Test seam (convention 9): one test's file must not answer for another's. */
377
+ export function __resetBuildRecords(): void {
378
+ lastBuild = null
379
+ }
380
+
348
381
  export function buildRecords(
349
382
  model: ImportModel,
350
383
  columns: readonly SchemaColumn[],
351
384
  ): BuildResult {
385
+ if (
386
+ lastBuild !== null &&
387
+ lastBuild.headers === model.headers &&
388
+ lastBuild.rows === model.rows &&
389
+ lastBuild.assign === model.assign &&
390
+ lastBuild.emptyToNull === model.emptyToNull &&
391
+ lastBuild.columns === columns
392
+ ) {
393
+ return lastBuild.result
394
+ }
395
+
352
396
  const feeds = feedsOf(model, columns)
353
397
  const records: Record<string, unknown>[] = []
354
398
  const failures: RowFailure[] = []
@@ -369,7 +413,16 @@ export function buildRecords(
369
413
  records.push(record)
370
414
  })
371
415
 
372
- return { records, failures }
416
+ const result = { records, failures }
417
+ lastBuild = {
418
+ headers: model.headers,
419
+ rows: model.rows,
420
+ assign: model.assign,
421
+ emptyToNull: model.emptyToNull,
422
+ columns,
423
+ result,
424
+ }
425
+ return result
373
426
  }
374
427
 
375
428
  /** RFC 4180 quoting: only when the field needs it, and `"` doubles. */
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Step one of the import wizard: get the text.
3
3
  *
4
- * A file input and a drop zone, and both end in the same `onText` callback —
4
+ * A file input and a drop zone, and both end in the same `onText` callback,
5
5
  * which is the whole reason this is a module rather than two handlers. Nothing
6
6
  * downstream knows or cares which way the file arrived.
7
7
  */
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * A coerced value that differs from the text carries the text as its `title`,
9
9
  * so `"007"` landing in an integer column is visibly `7` and traceably `007`.
10
- * A failure is ringed and shows the reason `coerceValue` gave — the same
10
+ * A failure is ringed and shows the reason `coerceValue` gave: the same
11
11
  * sentence the server would have returned.
12
12
  */
13
13
 
@@ -46,7 +46,7 @@ export function paintPreview(
46
46
  * column it governs is.
47
47
  *
48
48
  * On by default for every kind except text, because `''` is a value in a text
49
- * column and an error in every other one — see `defaultEmptyToNull`.
49
+ * column and an error in every other one. See `defaultEmptyToNull`.
50
50
  */
51
51
  function nullToggleRow(
52
52
  model: ImportModel,
package/src/client/csv.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * The CSV import wizard: pick → sniff → map → preview → commit.
3
3
  *
4
- * This file is the composition and nothing else. Each step is its own module —
5
- * `csv-pick.ts`, `csv-map.ts`, `csv-preview.ts`, `csv-commit.ts` — and
4
+ * This file is the composition and nothing else. Each step is its own module:
5
+ * `csv-pick.ts`, `csv-map.ts`, `csv-preview.ts`, `csv-commit.ts`, and
6
6
  * everything that *decides* anything is in `csv-model.ts` and is pure. What is
7
7
  * left here is one mutable `model` reference and the repaint that follows it.
8
8
  *
package/src/client/dom.ts CHANGED
@@ -2,8 +2,7 @@
2
2
  * The only place this client creates a node.
3
3
  *
4
4
  * **Nothing here writes `innerHTML`, and `client/safety.test.ts` asserts that
5
- * no client module does.** Every value the grid renders is a database row —
6
- * whatever the last writer put there — and the operator's origin owns
5
+ * no client module does.** Every value the grid renders is a database row ( * whatever the last writer put there), and the operator's origin owns
7
6
  * `/api/_db/rows`, so a string that reached the DOM as markup would be stored
8
7
  * XSS to arbitrary row writes. `textContent` is not a style preference; it is
9
8
  * the reason this plugin has no XSS surface. The dashboard's grid, which
@@ -16,7 +15,7 @@ export interface ElOptions {
16
15
  text?: string
17
16
  title?: string
18
17
  id?: string
19
- /** `aria-*`, `role`, `data-*` — anything set through `setAttribute`. */
18
+ /** `aria-*`, `role`, `data-*`: anything set through `setAttribute`. */
20
19
  attrs?: Record<string, string | number | boolean | null | undefined>
21
20
  }
22
21
 
@@ -88,8 +87,7 @@ export function each<T>(
88
87
  /**
89
88
  * A `<table class="grid">` with its heading row already in it.
90
89
  *
91
- * The two lines this replaces — build the table, loop `<th>` into a `<tr>` —
92
- * had been written out three times, twice in `structure.ts` and once in
90
+ * The two lines this replaces (build the table, loop `<th>` into a `<tr>`) * had been written out three times, twice in `structure.ts` and once in
93
91
  * `csv-preview.ts`. Rows are appended by the caller, because the three differ
94
92
  * in what a row is: `structure.ts` has plain text cells, and `csv-preview.ts`
95
93
  * puts a checkbox row directly under the headings.
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * The per-row dirty buffer, and the statement it becomes.
3
3
  *
4
- * **Pure — no DOM, no fetch.** This is where the editing model actually lives,
4
+ * **Pure: no DOM, no fetch.** This is where the editing model actually lives,
5
5
  * so it is the part with tests. The grid is a view over it.
6
6
  *
7
7
  * Three properties it exists to hold:
@@ -11,7 +11,7 @@
11
11
  * latter, which means a reader can observe the row a third of the way
12
12
  * through an edit the user considers atomic.
13
13
  * 2. **The pre-image is kept.** `expect` is built from the row as it was
14
- * *read*, never from a re-read at save time — a re-read would defeat the
14
+ * *read*, never from a re-read at save time: a re-read would defeat the
15
15
  * concurrency check it exists to perform.
16
16
  * 3. **Unchanged columns are dropped**, by `updatePlan`, so two people editing
17
17
  * different columns of one row do not collide.
@@ -23,7 +23,7 @@ import { updatePlan } from '../shared/plan'
23
23
  /**
24
24
  * A row's identity, flattened to a string.
25
25
  *
26
- * `JSON.stringify` over the identity columns in their declared order — the
26
+ * `JSON.stringify` over the identity columns in their declared order: the
27
27
  * order is the server's, from `identity.cols`, so it is stable across pages and
28
28
  * across a composite key's spelling. `null` when the row does not carry every
29
29
  * identity column, which is the same condition `updatePlan` reports as
@@ -50,7 +50,7 @@ export interface RowPlan {
50
50
  /** The pre-image of every changed column that SQL can compare. */
51
51
  expect: Record<string, unknown>
52
52
  /**
53
- * Changed columns SQL cannot compare — `json` and `buffer`. The server
53
+ * Changed columns SQL cannot compare: `json` and `buffer`. The server
54
54
  * refuses these in `expect` and demands `force: true` to write them without
55
55
  * a concurrency check, so the UI has to say so rather than retry blindly.
56
56
  */
@@ -68,7 +68,7 @@ export type KindLookup = (column: string) => ColumnKind | undefined
68
68
  *
69
69
  * Bounded by construction: an entry exists only for a row the user has typed
70
70
  * into, they are dropped on save and on revert, and changing table or page
71
- * clears the whole session — there is no key here a page of data does not
71
+ * clears the whole session. There is no key here a page of data does not
72
72
  * already hold (convention 6).
73
73
  */
74
74
  export class EditSession {
@@ -144,7 +144,7 @@ export class EditSession {
144
144
  * One row's edits as an UPDATE.
145
145
  *
146
146
  * `where` comes from the pre-image and `set` from the edits, which is what
147
- * lets a primary key itself be edited — see `updatePlan`'s own note.
147
+ * lets a primary key itself be edited. See `updatePlan`'s own note.
148
148
  */
149
149
  plan(
150
150
  id: string,
@@ -189,7 +189,7 @@ export interface UndoEntry {
189
189
  * Twenty rather than unlimited because each entry closes over a row's
190
190
  * pre-image, and an unbounded stack of those is exactly the module-level
191
191
  * unbounded cache convention 6 forbids. The pre-image is already being kept for
192
- * the concurrency check, so undo costs nothing extra to record — only to keep.
192
+ * the concurrency check, so undo costs nothing extra to record: only to keep.
193
193
  */
194
194
  export class UndoStack {
195
195
  private readonly entries: UndoEntry[] = []
@@ -28,7 +28,7 @@ export interface EditorHooks {
28
28
  export interface EditorHandle {
29
29
  node: HTMLElement
30
30
  focus: () => void
31
- /** The current value, already NULL-aware. Not coerced — that is the caller's. */
31
+ /** The current value, already NULL-aware. Not coerced: that is the caller's. */
32
32
  read: () => unknown
33
33
  }
34
34
 
@@ -100,7 +100,7 @@ function enumControl(column: SchemaColumn, value: unknown): Control {
100
100
  * A checkbox, and it reads back a real boolean.
101
101
  *
102
102
  * A driver hands a boolean column back as `1`/`0` on SQLite and MySQL, so the
103
- * *initial* state has to accept those spellings — but what leaves here is
103
+ * *initial* state has to accept those spellings, but what leaves here is
104
104
  * `true`/`false`, which is what `coerceValue` wants and what makes `sameValue`
105
105
  * agree that nothing changed when nothing did.
106
106
  */
@@ -123,7 +123,7 @@ function booleanControl(_column: SchemaColumn, value: unknown): Control {
123
123
  *
124
124
  * It refuses an ISO string with a `Z` and refuses one carrying milliseconds, so
125
125
  * the value is trimmed to `YYYY-MM-DDTHH:mm` going in and handed back as a full
126
- * ISO string going out — `coerceValue`'s `date` branch parses either, but the
126
+ * ISO string going out: `coerceValue`'s `date` branch parses either, but the
127
127
  * round trip has to be lossless in the direction the user sees.
128
128
  */
129
129
  function dateControl(_column: SchemaColumn, value: unknown): Control {
@@ -199,8 +199,8 @@ export interface EditorOptions {
199
199
  * An editor for one cell.
200
200
  *
201
201
  * The wrapper owns three things the control does not: the null toggle, the
202
- * validity message, and key forwarding. Validation runs `coerceValue` — the
203
- * same function the server runs — so the message the user sees before sending
202
+ * validity message, and key forwarding. Validation runs `coerceValue` (the
203
+ * same function the server runs), so the message the user sees before sending
204
204
  * is the message they would have got back.
205
205
  */
206
206
  export function createEditor(
@@ -36,7 +36,7 @@ const OPTIONS = FILTER_OPS.map(op => ({ value: op, label: OP_LABELS[op] }))
36
36
  * The bar.
37
37
  *
38
38
  * Every control writes a **whole new list** through `onChange` rather than
39
- * mutating the one it was given — same discipline as the CSV wizard's model,
39
+ * mutating the one it was given: same discipline as the CSV wizard's model,
40
40
  * and the reason a chip's state can never disagree with the URL.
41
41
  */
42
42
  export function filterBar(ctx: FilterBuilderContext): HTMLElement {
@@ -54,7 +54,7 @@ export function filterBar(ctx: FilterBuilderContext): HTMLElement {
54
54
  *
55
55
  * The wire shape is a record keyed by column, so only the last filter on a
56
56
  * column is sent. Rendering both chips and silently dropping one would be a
57
- * screen that disagrees with the query — the one failure mode a filter builder
57
+ * screen that disagrees with the query: the one failure mode a filter builder
58
58
  * must not have.
59
59
  */
60
60
  function clashNote(columns: readonly string[]): HTMLElement {
@@ -130,9 +130,9 @@ function nextOnOp(entry: Filter, op: FilterOp): Filter {
130
130
  }
131
131
 
132
132
  /**
133
- * The operand — **absent** for `null` and `notnull`.
133
+ * The operand: **absent** for `null` and `notnull`.
134
134
  *
135
- * Not disabled, not hidden by CSS: not built at all. A greyed-out box still
135
+ * Not disabled, not hidden by CSS: not built at all. A grayed-out box still
136
136
  * says "there is a value here", and there is not one.
137
137
  */
138
138
  function valueInput(
@@ -169,7 +169,7 @@ function removeButton(
169
169
  *
170
170
  * Exported because they are the two operations the chip callbacks are made of,
171
171
  * and an off-by-one in either would present as "removing one filter removed a
172
- * different one" — a bug worth a test rather than a careful read.
172
+ * different one": a bug worth a test rather than a careful read.
173
173
  */
174
174
  export function withAt(
175
175
  filters: readonly Filter[],
@@ -186,7 +186,7 @@ export function withoutAt(filters: readonly Filter[], index: number): Filter[] {
186
186
  /**
187
187
  * The filter a foreign-key jump becomes.
188
188
  *
189
- * One `eq` per referenced column, which is exactly a row identity — this is the
189
+ * One `eq` per referenced column, which is exactly a row identity: this is the
190
190
  * function that replaced `ViewState.focus`. Values stringify because the wire
191
191
  * carries text and the ORM binds it as a parameter; the comparison happens in
192
192
  * the database, against the column's own type.
package/src/client/fk.ts CHANGED
@@ -15,7 +15,7 @@
15
15
  * per-row and therefore unbounded in principle; the bound is what makes that
16
16
  * safe.
17
17
  *
18
- * The pure half — which column points where, and what key a row implies — is
18
+ * The pure half (which column points where, and what key a row implies) is
19
19
  * separated out so the graph reasoning can be read without the timers.
20
20
  */
21
21
 
@@ -25,8 +25,8 @@ import { type ForeignKeyInfo, type SchemaGraph, sameTable } from './meta'
25
25
  /**
26
26
  * A least-recently-used map, in nine lines.
27
27
  *
28
- * Core has `LRUCache` and this is not a preference for a private copy —
29
- * `bundleModule` externalises every installed package, so importing it leaves
28
+ * Core has `LRUCache` and this is not a preference for a private copy:
29
+ * `bundleModule` externalizes every installed package, so importing it leaves
30
30
  * `@bakery-framework/core/cache/lru` as a bare specifier in the emitted bundle
31
31
  * and the browser cannot resolve it. Client code imports nothing from
32
32
  * `@bakery-framework/*`; see the header of `shared/coerce.ts`.
@@ -101,7 +101,7 @@ export function fkForColumn(
101
101
  * Every foreign-key column of one table, resolved once.
102
102
  *
103
103
  * `fkForColumn` is a linear scan of the whole graph, and the grid asked it per
104
- * cell, per paint — a fifty-row page of twenty columns against forty foreign
104
+ * cell, per paint: a fifty-row page of twenty columns against forty foreign
105
105
  * keys is forty thousand comparisons, each of which had `sameTable` allocate
106
106
  * two lowercased strings, and `repaintRow`/`repaintCell` run it again. The scan
107
107
  * does not depend on the row, so it happens once when the grid is built.
@@ -139,7 +139,7 @@ export function reverseFks(
139
139
  /**
140
140
  * The referenced row's identity, as this row names it.
141
141
  *
142
- * `null` when any participating column is NULL — an optional foreign key with
142
+ * `null` when any participating column is NULL: an optional foreign key with
143
143
  * no value points at nothing, and a predicate containing `NULL` matches no row
144
144
  * anyway, so issuing the lookup would spend a request to learn that.
145
145
  */
@@ -156,7 +156,7 @@ export function fkKeyOf(
156
156
  return key
157
157
  }
158
158
 
159
- /** `table|col|value` — stable across pages, which is what makes caching pay. */
159
+ /** `table|col|value`: stable across pages, which is what makes caching pay. */
160
160
  export function cacheKey(
161
161
  refTable: string,
162
162
  key: Record<string, unknown>,
@@ -171,8 +171,8 @@ export function cacheKey(
171
171
  * What to show for a resolved row: the label column if the table has one, the
172
172
  * identity otherwise.
173
173
  *
174
- * `graph.labels` is the server's pick — the first text column that is not part
175
- * of the identity — so a `users` row reads as `ada` rather than `41`.
174
+ * `graph.labels` is the server's pick (the first text column that is not part
175
+ * of the identity), so a `users` row reads as `ada` rather than `41`.
176
176
  */
177
177
  export function fkLabel(
178
178
  graph: SchemaGraph | null,
@@ -197,7 +197,7 @@ export type Resolved = Record<string, unknown> | null
197
197
  */
198
198
  export class FkResolver {
199
199
  /**
200
- * 500 entries — ten pages of a fifty-row grid with a foreign key on every
200
+ * 500 entries: ten pages of a fifty-row grid with a foreign key on every
201
201
  * row. Past that the earliest are the ones the user has scrolled away from.
202
202
  */
203
203
  private readonly cache = new BoundedCache<Resolved>(500)
@@ -2,8 +2,8 @@
2
2
  * What goes *inside* a cell: the text, the classes, and the foreign-key button.
3
3
  *
4
4
  * Split out of `grid.ts` alongside the header and the row bar. This half is the
5
- * one with no knowledge of the cursor or the editor — it is handed a value and
6
- * returns a node — which is why it could leave.
5
+ * one with no knowledge of the cursor or the editor (it is handed a value and
6
+ * returns a node), which is why it could leave.
7
7
  *
8
8
  * The `<button>` for a foreign key is a real button rather than a styled span,
9
9
  * because it is activated by keyboard and read as an action by a screen reader.
@@ -33,7 +33,7 @@ export interface CellPaintContext {
33
33
  * Write one cell's value into its `<td>`.
34
34
  *
35
35
  * The staged flag is passed in rather than read from a session here, so this
36
- * module needs no `EditSession` — a staged value and a stored value take
36
+ * module needs no `EditSession`: a staged value and a stored value take
37
37
  * exactly the same path and cannot diverge in how they look.
38
38
  */
39
39
  export function paintCell(
@@ -69,7 +69,7 @@ function cellBody(
69
69
  /**
70
70
  * A foreign key as a link, resolving its label on hover.
71
71
  *
72
- * The resolver owns the delay, the cache and the cancellation — see `fk.ts`.
72
+ * The resolver owns the delay, the cache and the cancellation. See `fk.ts`.
73
73
  * All that happens here is arming it on `pointerenter` and disarming it on
74
74
  * `pointerleave`, which is what keeps a sweep across the grid from costing
75
75
  * fifty round trips.
@@ -2,7 +2,7 @@
2
2
  * The grid's header row: the select-all box and the sortable column headings.
3
3
  *
4
4
  * Split out of `grid.ts` when that file passed six hundred lines. It is a clean
5
- * seam because the header depends on nothing the grid mutates — it is built
5
+ * seam because the header depends on nothing the grid mutates: it is built
6
6
  * once from the columns and the current sort, and never repainted. Cursor,
7
7
  * editor and selection state all live on the other side of it.
8
8
  */
@@ -30,7 +30,7 @@ export function buildHead(ctx: HeaderContext): HTMLTableSectionElement {
30
30
 
31
31
  /**
32
32
  * The leading column exists on every table, editable or not: it carries the row
33
- * panel's opener, which a read-only table needs just as much — a forty-column
33
+ * panel's opener, which a read-only table needs just as much. A forty-column
34
34
  * row is unreadable in a grid whether or not it can be changed. The select-all
35
35
  * checkbox joins it only when there is something to select *for*.
36
36
  */
@@ -4,7 +4,7 @@
4
4
  * Under the row rather than in a toolbar, because "which row is this Save for"
5
5
  * has exactly one unambiguous answer and it is proximity. It carries the
6
6
  * changed-column list, the two buttons, and the place a failed save reports
7
- * itself — a 409 belongs next to the row it is about, not in a toast.
7
+ * itself: a 409 belongs next to the row it is about, not in a toast.
8
8
  */
9
9
 
10
10
  import { append, box, button, el } from './dom'
@@ -3,20 +3,20 @@
3
3
  *
4
4
  * Two rules keep this file's functions under biome's complexity ceiling, and
5
5
  * they are worth stating because the old `renderTable` broke both and scored
6
- * 34 with a fraction of the behaviour:
6
+ * 34 with a fraction of the behavior:
7
7
  *
8
8
  * 1. **No function both fetches and renders.** Everything here takes rows it
9
9
  * was handed. `api.ts` does the asking.
10
10
  * 2. **Every loop body is a named function.** `each()` from `dom.ts` takes the
11
11
  * factory; an inline body would fold its branches into the caller's score.
12
12
  *
13
- * Three neighbours carry the parts that do not need the cursor:
13
+ * Three neighbors carry the parts that do not need the cursor:
14
14
  * `grid-header.ts` builds the heading row, `grid-body.ts` paints a cell's
15
15
  * contents, and `grid-rowbar.ts` owns the per-row save strip. The editing model
16
16
  * itself is in `edit-session.ts` and the keyboard model in `cell.ts`, both pure
17
17
  * and both tested. What is left here genuinely needs a DOM.
18
18
  *
19
- * `focus` used to live here too — a row identity carried alongside the filters,
19
+ * `focus` used to live here too: a row identity carried alongside the filters,
20
20
  * because a substring `LIKE` could not name a row. With `eq` in the filter
21
21
  * vocabulary a foreign-key jump is an ordinary filter, and the highlight, the
22
22
  * `focusedMissing()` note and the whole concept are gone.
@@ -79,7 +79,7 @@ export class Grid {
79
79
  this.paintCtx = {
80
80
  table: ctx.table.name,
81
81
  graph: ctx.graph,
82
- // Once per grid, not once per cell per paint — see `fkMapFor`.
82
+ // Once per grid, not once per cell per paint. See `fkMapFor`.
83
83
  fks: fkMapFor(ctx.graph, ctx.table.name),
84
84
  resolver: ctx.resolver,
85
85
  onFollowFk: ctx.onFollowFk,
@@ -174,7 +174,7 @@ export class Grid {
174
174
  /**
175
175
  * Render one cell from the session's current view of it.
176
176
  *
177
- * Called on build, after every stage, and after an editor closes — so a
177
+ * Called on build, after every stage, and after an editor closes, so a
178
178
  * staged value and a saved value take exactly the same path.
179
179
  */
180
180
  private repaintCell(
@@ -248,7 +248,7 @@ export class Grid {
248
248
  // ---------------------------------------------------------------- editing
249
249
 
250
250
  /**
251
- * Open an editor — or explain why not.
251
+ * Open an editor, or explain why not.
252
252
  *
253
253
  * A double-click on a read-only table says what is wrong *here*, at the
254
254
  * moment the user asked. The alternative is a grid that looks editable and
@@ -293,8 +293,7 @@ export class Grid {
293
293
  // write, and a multi-column edit must stay one statement.
294
294
  //
295
295
  // `relatedTarget` is checked because `focusout` also fires for movement
296
- // *inside* the editor — tabbing from the input to the null toggle is one —
297
- // and committing there would close the editor the user is still in.
296
+ // *inside* the editor (tabbing from the input to the null toggle is one), // and committing there would close the editor the user is still in.
298
297
  handle.node.addEventListener('focusout', event => {
299
298
  const next = (event as FocusEvent).relatedTarget
300
299
  if (next instanceof Node && handle.node.contains(next)) return
@@ -425,7 +424,7 @@ export class Grid {
425
424
  return this.slots[index]?.row
426
425
  }
427
426
 
428
- /** The row under the cursor — what a keyboard-only user means by "this row". */
427
+ /** The row under the cursor: what a keyboard-only user means by "this row". */
429
428
  focusedRow(): Record<string, unknown> | undefined {
430
429
  return this.slots[this.cursor.row]?.row
431
430
  }
@@ -2,7 +2,7 @@
2
2
  * What the server said about a column, and the four decisions the grid makes
3
3
  * from it.
4
4
  *
5
- * **Pure — no DOM.** Every branch the editor would otherwise take on a column's
5
+ * **Pure, no DOM.** Every branch the editor would otherwise take on a column's
6
6
  * type is resolved here, once, into a `WidgetKind`, and every consumer
7
7
  * dispatches through a record of small functions rather than an if-chain. A
8
8
  * record of five functions has no cognitive complexity; five `else if`s have
@@ -11,7 +11,7 @@
11
11
  *
12
12
  * The shapes below mirror `endpoints/read.ts` exactly. They are re-declared
13
13
  * rather than imported because that module reaches for the ORM and for
14
- * `node:async_hooks` through its neighbours, and this one is compiled into the
14
+ * `node:async_hooks` through its neighbors, and this one is compiled into the
15
15
  * browser bundle.
16
16
  */
17
17
 
@@ -48,7 +48,7 @@ export interface SchemaTable {
48
48
  rowCount: number
49
49
  columns: SchemaColumn[]
50
50
  identity: Identity
51
- /** Optional so a fixture — and an older server — need not carry them. */
51
+ /** Optional so a fixture (and an older server) need not carry them. */
52
52
  indexes?: SchemaIndex[]
53
53
  isView?: boolean
54
54
  writable: boolean
@@ -87,7 +87,7 @@ export type WidgetKind = 'enum' | 'boolean' | 'date' | 'json' | 'text'
87
87
 
88
88
  /**
89
89
  * `enum` first, because it is a constraint on a `string` column rather than a
90
- * kind of its own — the server reports `kind: 'string'` with an `enum` list,
90
+ * kind of its own: the server reports `kind: 'string'` with an `enum` list,
91
91
  * and a free-text box over a checked list is a guaranteed round trip to a
92
92
  * `not_in_enum` error.
93
93
  */
@@ -124,7 +124,7 @@ export function columnMeta(column: SchemaColumn): ColumnMeta {
124
124
  * `NULL` is rendered as the word and styled as absent, which is the
125
125
  * distinction the whole editor turns on: an empty string is a value and a
126
126
  * blank cell would make the two look identical. Binary is summarised rather
127
- * than dumped — a blob rendered as its bytes is thousands of characters of
127
+ * than dumped: a blob rendered as its bytes is thousands of characters of
128
128
  * noise that also makes the row unselectable.
129
129
  */
130
130
  export function cellText(value: unknown): string {
@@ -139,7 +139,7 @@ function safeJson(value: unknown): string {
139
139
  return JSON.stringify(value) ?? String(value)
140
140
  } catch {
141
141
  // A cycle or a BigInt. The cell still has to render something, and the
142
- // failure is cosmetic — the value itself is untouched.
142
+ // failure is cosmetic: the value itself is untouched.
143
143
  return String(value)
144
144
  }
145
145
  }
@@ -6,7 +6,7 @@
6
6
  * what stopped the entry module growing back into the 197-line
7
7
  * fetch-and-render `renderTable` it replaced.
8
8
  *
9
- * The shape is now three regions rather than two — sidebar, then a column
9
+ * The shape is now three regions rather than two: sidebar, then a column
10
10
  * holding the tab strip, the active view and the status bar. The sidebar and
11
11
  * the status bar outlive a tab switch; only `#main` is replaced. The painters
12
12
  * take callbacks rather than reaching for the entry's state, so the page has no
@@ -78,7 +78,7 @@ export class Page {
78
78
  * The Data view: filters, the bulk toolbar, the grid, the pager.
79
79
  *
80
80
  * `data` is the page the server just returned and `ms` is what it said it
81
- * cost — the status bar's timing is the envelope's own number rather than a
81
+ * cost: the status bar's timing is the envelope's own number rather than a
82
82
  * round trip timed here, so a slow filter is attributable.
83
83
  */
84
84
  paint(
@@ -232,14 +232,14 @@ export class Page {
232
232
  private readOnlyBanner(table: SchemaTable): HTMLElement {
233
233
  return el('p', {
234
234
  class: 'banner warn',
235
- text: `read-only — ${readOnlyReason(this.state, table)}`,
235
+ text: `read-only: ${readOnlyReason(this.state, table)}`,
236
236
  })
237
237
  }
238
238
 
239
239
  /**
240
240
  * The filter builder.
241
241
  *
242
- * A chip per condition — column, operator, value, remove — where there used
242
+ * A chip per condition (column, operator, value, remove) where there used
243
243
  * to be one text box per column that could only ever mean "contains". Any
244
244
  * change resets to page 1, because a filtered result has different pages and
245
245
  * staying on page 7 of a set that now has two is a blank screen.