pgkiln 0.49.0 → 0.51.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/SECURITY.md +3 -0
  2. package/db/migrations/119_workspace_home.sql +30 -0
  3. package/db/migrations/120_datasets.sql +87 -0
  4. package/docs/apex-feature-parity.md +7 -6
  5. package/docs/guide/01-installation.md +5 -0
  6. package/docs/guide/03-builder.md +140 -11
  7. package/docs/guide/04-pages-and-regions.md +25 -1
  8. package/docs/guide/12-development.md +47 -11
  9. package/examples/hr/hr_63_datasets.sql +29 -0
  10. package/package.json +1 -1
  11. package/public/app.css +6 -0
  12. package/public/builder.css +95 -3
  13. package/public/code-editor.css +32 -79
  14. package/public/code-editor.js +58 -3
  15. package/public/data-modeler.css +17 -14
  16. package/public/worksheet.css +14 -7
  17. package/public/worksheet.js +119 -3
  18. package/src/app.ts +5 -1
  19. package/src/builder/changelog.ts +1 -1
  20. package/src/builder/data-modeler.ts +1 -1
  21. package/src/builder/datasets.ts +472 -0
  22. package/src/builder/db-users.ts +2 -2
  23. package/src/builder/home.ts +12 -7
  24. package/src/builder/instance.ts +2 -0
  25. package/src/builder/object-browser.ts +4 -4
  26. package/src/builder/querybuilder.ts +1 -1
  27. package/src/builder/quicksql.ts +1 -1
  28. package/src/builder/reporter.ts +52 -3
  29. package/src/builder/routes.ts +3 -3
  30. package/src/builder/scheduling.ts +2 -2
  31. package/src/builder/scripts.ts +1 -1
  32. package/src/builder/sql-charts.ts +1 -1
  33. package/src/builder/sql-monitor.ts +1 -1
  34. package/src/builder/subscriptions.ts +1 -0
  35. package/src/builder/table-editor.ts +2 -2
  36. package/src/builder/ui.ts +7 -5
  37. package/src/builder/worksheet.ts +171 -23
  38. package/src/builder/workspace-home.ts +283 -0
  39. package/src/builder/workspaces.ts +7 -4
  40. package/src/datasets.ts +367 -0
  41. package/src/i18n/ar.ts +16 -0
  42. package/src/i18n/cs.ts +16 -0
  43. package/src/i18n/da.ts +16 -0
  44. package/src/i18n/de.ts +16 -0
  45. package/src/i18n/el.ts +16 -0
  46. package/src/i18n/es.ts +16 -0
  47. package/src/i18n/fi.ts +16 -0
  48. package/src/i18n/fr.ts +16 -0
  49. package/src/i18n/he.ts +16 -0
  50. package/src/i18n/it.ts +16 -0
  51. package/src/i18n/ja.ts +16 -0
  52. package/src/i18n/ko.ts +16 -0
  53. package/src/i18n/nb.ts +16 -0
  54. package/src/i18n/pl.ts +16 -0
  55. package/src/i18n/pt.ts +16 -0
  56. package/src/i18n/ru.ts +16 -0
  57. package/src/i18n/sv.ts +16 -0
  58. package/src/i18n/tr.ts +16 -0
  59. package/src/i18n/uk.ts +16 -0
  60. package/src/i18n/zh.ts +16 -0
  61. package/src/i18n.ts +32 -0
  62. package/src/instance.ts +1 -0
  63. package/src/release-check.ts +139 -0
  64. package/src/reporting-app.ts +101 -0
  65. package/src/runtime/data-reporter.ts +199 -32
  66. package/src/session.ts +7 -0
  67. package/src/sqlworksheet.ts +9 -6
  68. package/src/subscriptions.ts +2 -1
  69. package/src/worksheet-txn.ts +270 -0
  70. package/src/workspace-export.ts +24 -4
@@ -21,6 +21,9 @@ src/
21
21
  env.ts .env loader (imported first)
22
22
  migrate.ts applies db/migrations and examples (scripts/migrate.ts, pgkiln migrate); logs each run that applies
23
23
  instance.ts instance settings (meta.setting over environment variables, cached 30 s) and the configuration overview
24
+ release-check.ts (sprint 109) "pgkiln X is available": meta.setting 'release_check' over PGKILN_RELEASE_CHECK (off by default);
25
+ GitHub's releases/latest (PGKILN_RELEASE_URL in tests), 5 s timeout, 1 MB, cached a day per process,
26
+ never under node --test without PGKILN_RELEASE_URL; parseRelease keeps a semver and an https link only
24
27
  or fails a file in public.pgkiln_install_log
25
28
  appfiles.ts application export as one file per component (dir layout, static ids) and back
26
29
  blueprint.ts blueprints (migration 063): checkBlueprint (names, types, references, pages, sample rows), tableOrder,
@@ -68,10 +71,22 @@ src/
68
71
  xml.ts safe XML reader (no DTDs or entities, limits) and xmlTable(): rows from a repeating element (unit tested)
69
72
  sqlscript.ts SQL scripts: splitScript() (statements, line numbers and offsets, psql commands), runScript() (stop/continue, transaction, savepoints)
70
73
  sqlworksheet.ts (sprint 91) the worksheet's runs: pickStatements() (at the cursor, a selection, all), runStatements() (a log per
71
- statement, psql command tags, notices, error line and column), explainStatement() (EXPLAIN / rolled-back ANALYZE)
74
+ statement, psql command tags, notices, error line and column), explainStatement() (EXPLAIN / rolled-back ANALYZE;
75
+ a savepoint inside a worksheet transaction)
76
+ worksheet-txn.ts (sprint 107) the worksheet's transaction mode: an in-memory registry of pinned pg.Clients per (builder session,
77
+ worksheet tab), beginTxn() / endTxn() (caps per developer and server), accountRun() (statement count, DDL kept for
78
+ the Change Log until COMMIT), outsideTxnOnly(), sweepTxns() (idle timeout, ended sessions); onSessionEnd and
79
+ onClosePools roll back; WORKSHEET_TXN_IDLE_SECONDS / _MAX_PER_DEVELOPER / _MAX
72
80
  datamodel.ts (sprint 98) Data Modeler diagrams: readModel() (catalog), boxSize(), fkPath() (crow's feet), autoLayout(), diagramSvg()
73
81
  changelog.ts (sprint 106, migration 118) the change log: isSchemaDdl(), ddlTarget() (schema and object), changeName()
74
82
  (add_bonus, create_index_x), recordChanges() (grouped per schema), Flyway / Liquibase / plain SQL writers
83
+ datasets.ts (sprint 110, migration 120) Data Reporter datasets: checkDataset() (names, joins to earlier tables, columns
84
+ and filters on its tables, whitelisted operators; with a catalog: existing objects), datasetSql() (quoted
85
+ identifiers, literal values), readSchemaCatalog() (tables, views, columns, foreign keys), proposeJoin() /
86
+ proposeDataset(), storeDataset(), appDatasets() (the runtime's read), pgkiln-dataset/1 files
87
+ reporting-app.ts (sprint 110) reporting applications: createReportingApp() (createApp, SELECT-only grants on the datasets'
88
+ objects, type reporting, roles viewer/editor/administrator as schemes EDITOR and ADMIN, page 1 a Data
89
+ Reporter), reportingApps(), reportingAppCount() (meta.reporting_app_count, for the workspace home)
75
90
  tabledef.ts (sprint 95) a table as a definition: readTable(), checkTable(), tableSql() (CREATE, or ALTER from the catalog's version)
76
91
  sqlformat.ts (sprint 93) formatSql(): the worksheet's Format (clauses per line, keyword case; strings and comments kept)
77
92
  sqlplan.ts (sprint 92) EXPLAIN (FORMAT JSON) as a tree: parsePlan() (self time, slowest step, misestimates), planText()
@@ -180,7 +195,9 @@ src/
180
195
  template-region.ts template_component region
181
196
  tasks.ts task list region and task actions (approvals); (094) the vacation rule panel and route
182
197
  data-reporter.ts Data Reporter region (migration 057): sources from the region's config, checkDef (offered columns, whitelists),
183
- reportQuery/chartQuery, the list and editor (GET form), save/delete routes (meta.save_data_report)
198
+ reportQuery/chartQuery, the list and editor (GET form), save/delete routes (meta.save_data_report);
199
+ (sprint 110) dataset sources (resolveSources), edit_authz / admin_authz / publishing, pins (a preference),
200
+ publish / pin routes, the stable URL GET /a/:alias/reports/:id
184
201
  form-designer.ts Form Designer region (migration 106): sources from the region's config, checkFormDef (offered columns, displays,
185
202
  actions), forms as cards, a form's rows, a row's form (lost update hash), the designer; row/design/delete routes
186
203
  workflows.ts workflow console region and its actions
@@ -212,7 +229,12 @@ src/
212
229
  routes.ts sign-in, app home, settings, activity, developers, create (POST, via newapp.ts)/import (POST)
213
230
  wizards.ts create page wizards: step 2 forms per page type (defaults from meta.wizard_defaults), POST → meta.generate_page
214
231
  (the generators are PL/pgSQL in migration 047: catalog, defaults, form/cards/calendar/chart/map/facets/master-detail)
215
- home.ts App Builder home (tiles, applications report/cards, Recent), Create, Import, Dashboard, Utilities
232
+ home.ts App Builder home at /builder/apps and /builder (tiles, applications report/cards, Recent; /builder redirects
233
+ to the Workspace home when meta.workspace.builder_home = 'home'), Create, Import, Dashboard, Utilities
234
+ workspace-home.ts (sprint 109, migration 119) the Workspace home /builder/home, opened after sign-in: tool cards (Data Reporter
235
+ when GET /builder/reporter exists), workspace message edited in place (workspaceAdmin), Summary, Top
236
+ applications / users / developers over meta.developer.home_timeframe, System message (src/release-check.ts),
237
+ POST timeframe, message, release-check; releaseCheckRegion for Instance settings
216
238
  newapp.ts creating an application (schema, role app_<alias>, Home page, first user): blank app, from a file, from tables
217
239
  appfromfile.ts Create → From a file: upload (src/dataload.ts parsing), proposed table/columns, one transaction:
218
240
  app + table + rows (loadRows) + pages (meta.generate_page: report and form, chart, facets)
@@ -249,7 +271,8 @@ src/
249
271
  worksheet's Chart tab, SQL Workshop → Charts (meta.sql_chart, 110; read-only runs)
250
272
  sql-tools.ts (sprint 96) SQL_TOOLS: every SQL Workshop tool with its group, tab and description (home, More menu, search)
251
273
  worksheet.ts (sprint 91) the SQL Worksheet (Database Actions' SQL; /sql/commands too): navigator (pickSchema, objects with
252
- columns, objectStatements), toolbar, output tabs, history (meta.sql_history, 108), plan trees, Run as an app's role, (sprint 93) Save / Rename / Download as SQL Scripts, bind and substitution values, POST /sql/worksheet/format; GET /sql/worksheet/navigator
274
+ columns, objectStatements), toolbar, output tabs, history (meta.sql_history, 108), plan trees, Run as an app's role, (sprint 93) Save / Rename / Download as SQL Scripts, bind and substitution values, POST /sql/worksheet/format; GET /sql/worksheet/navigator;
275
+ (sprint 107) transaction mode: the txn field, Commit / Rollback (mode=commit|rollback), txnBadge(), GET/POST /sql/worksheet/txn (JSON)
253
276
  object-browser.ts (sprint 75) the Object Browser: objects by type, system objects hidden, a table in tabs, column actions as SQL to review
254
277
  table-editor.ts (sprint 95) Table Properties: GET/POST /sql/objects/table (create, edit ?o=, ?template=), fromForm(), Preview / Apply
255
278
  (one transaction) / Open in the worksheet
@@ -299,7 +322,11 @@ src/
299
322
  pwa.ts Settings → Progressive Web App (icon upload)
300
323
  themeroller.ts Settings → Theme Roller: style variants (add, edit, rename, delete), default style, users may choose
301
324
  subscriptions.ts Shared Components → Subscriptions: subscribe, refresh, unsubscribe, subscribers and publish; the note under a component
302
- reporter.ts page designer: Data Reporter settings (sources: table or view, offered columns, labels, masks; sharing)
325
+ reporter.ts page designer: Data Reporter settings (sources: table or view, offered columns, labels, masks; sharing;
326
+ (sprint 110) a dataset as a source, who may edit, who may moderate, publishing)
327
+ datasets.ts (sprint 110) App Builder → Data Reporter (/builder/reporter): datasets (new: schema, tables; a dataset's
328
+ joins, columns, row filter, query and preview), files (download, export all, import), Create reporting
329
+ application
303
330
  form-designer.ts page designer: Form Designer settings (designers' scheme; sources: key, actions, offered columns, labels, lists of values)
304
331
  workingcopies.ts Working copies: list and create, compare with differences, merge or refresh with conflict choices, delete
305
332
  rest.ts REST module endpoints list and curl example (Shared Components)
@@ -339,14 +366,18 @@ src/
339
366
  public/
340
367
  app.css theme (light/dark, responsive; --font, --font-size, --radius for style variants; template option classes to-*)
341
368
  app.js client runtime: dialogs (dialog_closed actions), popup LOVs, dynamic actions (focus, classes, messages), grids (add/duplicate rows, master-detail refresh, move/resize columns, copy/paste of cell ranges), menus, lazy regions, maps (Leaflet layers, marker clusters, heat layer, layer legend, area/distance filter) (no inline JS)
342
- code-editor.js, .css builder code editor: enhances <textarea data-code>, highlighting, suggestions (no dependencies)
369
+ code-editor.js, .css builder code editor: enhances <textarea data-code>, highlighting, suggestions (no dependencies);
370
+ (sprint 108) the current line, and read-only <pre data-code-view="sql|plpgsql|json|html"> highlighted;
371
+ its colours are the --ce-* tokens in builder.css
343
372
  palette.js (sprint 96) the builder's search dialog (Ctrl+K), recently visited SQL Workshop pages, the Launchpad's tabs
344
373
  sql-monitor.js (sprint 102) the SQL Monitor: reloads the sessions' live part every 5 / 15 / 60 s, Pause (localStorage pgkiln.sqlMonitor)
345
374
  data-modeler.js, .css (sprint 98) the Data Modeler: drag boxes, undo/redo, save, zoom/fit, auto layout, mini-map, SVG/PNG export, print
346
375
  worksheet.js, .css (sprint 91) the SQL Worksheet: run keys with the cursor, output tabs, navigator swap and search, drag and drop, splitters;
347
- (92) grid sort, value dialog, history filter; (93) worksheet tabs in localStorage, Files, binds dialog, format, find, case
376
+ (92) grid sort, value dialog, history filter; (93) worksheet tabs in localStorage, Files, binds dialog, format, find, case;
377
+ (107) Autocommit / Transaction per tab, the badge from the server, beforeunload, rollback on closing a tab
348
378
  builder.css builder only: IDE look (dark chrome, icon rail, panes); design tokens at the top (colour,
349
- elevation, radius, type, motion; light and dark sets, sprint 105), then the shared controls
379
+ elevation, radius, type, motion; light and dark sets, sprint 105; code editor --ce-*, sprint 108),
380
+ then the shared controls
350
381
  builder.js builder only: tabs, component tree, property filter, drag and drop on the layout
351
382
  builder-icons.svg builder only: icons of the rail, toolbar and designer (b-*); b-kiln is the brand mark (brandMark() in ui.ts)
352
383
  test/
@@ -377,6 +408,10 @@ test/
377
408
  object-actions.test.ts (sprint 94) object menus: Quick DDL, Drop with dependents, Add index / trigger / policy, the panel, not-found
378
409
  data-modeler.test.ts (sprint 98) Data Modeler: "hr core" with its FK lines, saved positions, SVG, objects, DDL, dictionary, geometry
379
410
  worksheet.test.ts (sprint 91) the SQL Worksheet: statement at the cursor, Run Script log, plans, notices, history, navigator, Run as
411
+ worksheet-txn.test.ts (sprint 107) transaction mode: the tab's transaction, Commit / Rollback, typed COMMIT, aborted, idle timeout, refusals,
412
+ Explain in it, Change Log at COMMIT, the JSON endpoint, sign-out
413
+ datasets.test.ts (sprint 110) datasets: checks and SQL, proposed joins, the builder's pages, files, the workspace export;
414
+ a reporting application in two clicks, an editor publishes, a viewer opens the stable URL, pins
380
415
  quicksql.test.ts Quick SQL parser and DDL generator
381
416
  xml.test.ts XML reader: rows, attributes, paths, refused DTDs and entities, limits
382
417
  printing.test.ts report PDFs
@@ -422,9 +457,10 @@ test/
422
457
  ai-mock.ts a local mock of the Claude Messages API (SSE stream) and OpenAI Chat Completions
423
458
  helpers.ts a cookie-keeping test browser
424
459
  e2e/responsive.test.ts browser tests at phone/tablet/desktop widths (Playwright)
425
- e2e/code-editor.test.ts the code editor in a browser: highlighting, keys, suggestions, touch, screen readers
460
+ e2e/code-editor.test.ts the code editor in a browser: highlighting, keys, suggestions, touch, screen readers, syntax colours at AA in both themes (sprint 108)
426
461
  e2e/data-modeler.test.ts (sprint 98) the Data Modeler in a browser: drag, save, reload, undo/redo, zoom, mini-map, exports, phone
427
- e2e/worksheet.test.ts (sprint 91) the SQL Worksheet in a browser: run keys, tabs, navigator, drag and drop, splitters, phone, no JavaScript
462
+ e2e/worksheet.test.ts (sprint 91) the SQL Worksheet in a browser: run keys, tabs, navigator, drag and drop, splitters, phone, no JavaScript;
463
+ (107) transaction mode: badge, reload, Rollback, closing a tab, Autocommit switch, beforeunload
428
464
  e2e/items.test.ts sprint 26 item types in a browser: editors, tags, stars, dates, reveal; without JavaScript
429
465
  e2e/designer.test.ts page designer: panes per width, drag and drop, keyboard, Arrange buttons, builder theme
430
466
  e2e/calendar.test.ts calendar drag and drop and create on click, view switching, chart drill-down
@@ -475,7 +511,7 @@ CI (`.github/workflows/ci.yml`) runs these jobs against PostgreSQL 17:
475
511
  - **test**: typecheck and `npm test` on a fresh database;
476
512
  - **e2e**: the browser tests, uploading the screenshots as an artifact; `test/e2e/accessibility.test.ts` runs
477
513
  axe-core (WCAG 2.1 A and AA rules) on every page of the HR example (light, dark, Iris) and the builder's main pages
478
- and allows no violation;
514
+ (in both builder themes, with the SQL Workshop and the wizards, since sprint 108) and allows no violation;
479
515
  - **docker**: builds the image and starts the compose stack as a user would.
480
516
 
481
517
  - **upgrade**: installs each earlier release (`v0.31.0` …) with its HR example (`git archive <tag> | tar -x`,
@@ -0,0 +1,29 @@
1
+ -- =====================================================================
2
+ -- HR example, part 63: a Data Reporter dataset (docs/guide/04-pages-and-
3
+ -- regions.md, "Datasets and reporting applications")
4
+ --
5
+ -- The dataset "hr" of the Default workspace: employees (hr.emp) joined to
6
+ -- their department (hr.dept, a left join on the foreign key deptno), the
7
+ -- columns business users may see with labels, format masks and
8
+ -- descriptions, and a row filter: active employees only. In the builder,
9
+ -- App Builder → Data Reporter → Create reporting application makes a
10
+ -- reporting application on it in two clicks; datasets are installation
11
+ -- data (a file of their own, and the workspace export), not part of the
12
+ -- HR application's export.
13
+ -- =====================================================================
14
+
15
+ insert into meta.dataset (workspace_id, name, label, description, schema_name, tables, columns, filters, created_by)
16
+ values (1, 'hr', 'HR', 'Active employees with their department.', 'hr',
17
+ '[{"alias": "t1", "table": "emp"},
18
+ {"alias": "t2", "table": "dept", "join": {"to": "t1", "type": "left", "on": [{"column": "deptno", "ref": "deptno"}]}}]',
19
+ '[{"name": "empno", "table": "t1", "column": "empno", "label": "No."},
20
+ {"name": "ename", "table": "t1", "column": "ename", "label": "Name"},
21
+ {"name": "job", "table": "t1", "column": "job", "label": "Job"},
22
+ {"name": "hiredate", "table": "t1", "column": "hiredate", "label": "Hire date"},
23
+ {"name": "sal", "table": "t1", "column": "sal", "label": "Salary", "format": "FML999G990D00", "description": "Monthly salary"},
24
+ {"name": "comm", "table": "t1", "column": "comm", "label": "Commission", "format": "FML999G990D00"},
25
+ {"name": "dname", "table": "t2", "column": "dname", "label": "Department"},
26
+ {"name": "loc", "table": "t2", "column": "loc", "label": "Location", "description": "Where the department is"}]',
27
+ '[{"table": "t1", "column": "active", "op": "eq", "value": "true"}]',
28
+ 'admin')
29
+ on conflict (workspace_id, name) do nothing;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pgkiln",
3
- "version": "0.49.0",
3
+ "version": "0.51.0",
4
4
  "description": "Open source low-code, SQL-driven application builder for PostgreSQL, inspired by Oracle APEX",
5
5
  "keywords": [
6
6
  "postgresql",
package/public/app.css CHANGED
@@ -2042,3 +2042,9 @@ a.search-hit:hover, a.search-hit:focus-visible { background: var(--accent-soft);
2042
2042
  .t-footer .icon { width: 1rem; height: 1rem; }
2043
2043
  @media print { .t-footer { display: none; } }
2044
2044
  .list-card:hover { border-top-color: var(--card-color); }
2045
+
2046
+ /* (sprint 110) Data Reporter: a saved report's link and actions, column descriptions */
2047
+ .reporter-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem 0.6rem; margin: 0 0 0.75rem; min-width: 0; }
2048
+ .reporter-actions .buttons { margin: 0; }
2049
+ .reporter-link { flex: 1 1 14rem; min-width: 0; font-family: var(--mono, monospace); font-size: 0.85rem; }
2050
+ .reporter-coldesc { display: block; font-size: 0.78rem; }
@@ -9,6 +9,8 @@
9
9
  --accent ≥ 4.8:1 on every surface, --muted ≥ 5.6:1, in both themes
10
10
  - elevation: --shadow-xs … --shadow-lg; radius: --radius-sm / --radius / --radius-lg
11
11
  - type: --fs-xs … --fs-xl; motion: --ease and --dur (none under prefers-reduced-motion)
12
+ - code (sprint 108): --ce-* for public/code-editor.js and read-only <pre data-code-view>; every
13
+ syntax colour ≥ 4.5:1 on --ce-bg in both themes (test/e2e/code-editor.test.ts checks it)
12
14
  The light set first, the dark set for data-theme="dark" and for "System" when
13
15
  the OS prefers dark. The chrome (rail, toolbar, status bar) is dark in both
14
16
  themes (the same colours), a shade deeper than the page in the dark one. */
@@ -59,10 +61,31 @@
59
61
  --block: #ffffff;
60
62
  --block-head: #f7f5f2;
61
63
  --code-bg: #fbfaf8;
64
+ --field-border: var(--border-strong);
65
+
66
+ /* code editor (sprint 108, public/code-editor.js): every token colour is at least 4.5:1 on --ce-bg
67
+ (keywords take the ember; the others cool hues so they stay apart) */
62
68
  --ce-bg: #fdfcfb;
63
69
  --ce-gutter: #f5f3f0;
64
70
  --ce-gutter-text: #6b645d;
65
- --field-border: var(--border-strong);
71
+ --ce-gutter-active: #1c1917;
72
+ --ce-text: #1c1917;
73
+ --ce-kw: #b23a0a;
74
+ --ce-type: #0e6b66;
75
+ --ce-fn: #2457a6;
76
+ --ce-str: #3a7516;
77
+ --ce-num: #8a3ea0;
78
+ --ce-com: #736c66;
79
+ --ce-bind: #b02a5c;
80
+ --ce-qid: #80590a;
81
+ --ce-op: #5c5650;
82
+ --ce-tag: #2457a6;
83
+ --ce-attr: #b23a0a;
84
+ --ce-err: #c0262d;
85
+ --ce-match: rgb(234 88 12 / 16%);
86
+ --ce-match-ring: rgb(194 65 12 / 55%);
87
+ --ce-line: rgb(28 25 23 / 4.5%);
88
+ --ce-select: rgb(234 88 12 / 20%);
66
89
 
67
90
  /* shape, elevation, type, motion */
68
91
  --radius-sm: 6px;
@@ -91,7 +114,10 @@
91
114
  --bg: #181715; --surface: #1f1d1b; --surface-2: #242220; --surface-3: #2d2b28; --text: #edebe8; --muted: #a8a19a;
92
115
  --border: #312e2b; --border-strong: #423e39; --accent: #ff9152; --accent-text: #1a0b03; --accent-soft: #3a1f12; --ring: rgb(255 145 82 / 30%); --sel-bg: #4a2816; --sel-bar: #ff9152;
93
116
  --gold: #ff9152; --canvas: #151412; --canvas-dot: #2a2825; --block: #222120; --block-head: #292725; --code-bg: #1a1917; --warn: #e0a740;
94
- --ce-bg: #1a1917; --ce-gutter: #1f1d1b; --ce-gutter-text: #8f8880;
117
+ --ce-bg: #1a1917; --ce-gutter: #1f1d1b; --ce-gutter-text: #8f8880; --ce-gutter-active: #e9e6e3; --ce-text: #e9e6e3;
118
+ --ce-kw: #ff9152; --ce-type: #5cc8bd; --ce-fn: #8ab4ff; --ce-str: #a5d68a; --ce-num: #d4a8ff; --ce-com: #938b83; --ce-bind: #ff8fb8;
119
+ --ce-qid: #e6c26a; --ce-op: #b8b0a8; --ce-tag: #8ab4ff; --ce-attr: #ff9152; --ce-err: #ff7a70;
120
+ --ce-match: rgb(255 145 82 / 22%); --ce-match-ring: rgb(255 145 82 / 60%); --ce-line: rgb(255 255 255 / 4.5%); --ce-select: rgb(255 145 82 / 28%);
95
121
  --shadow-xs: 0 1px 1px rgb(0 0 0 / 30%);
96
122
  --shadow-sm: 0 1px 2px rgb(0 0 0 / 40%), inset 0 1px 0 rgb(255 255 255 / 3%);
97
123
  --shadow-md: 0 4px 16px -2px rgb(0 0 0 / 50%), inset 0 1px 0 rgb(255 255 255 / 4%);
@@ -105,7 +131,10 @@
105
131
  --bg: #181715; --surface: #1f1d1b; --surface-2: #242220; --surface-3: #2d2b28; --text: #edebe8; --muted: #a8a19a;
106
132
  --border: #312e2b; --border-strong: #423e39; --accent: #ff9152; --accent-text: #1a0b03; --accent-soft: #3a1f12; --ring: rgb(255 145 82 / 30%); --sel-bg: #4a2816; --sel-bar: #ff9152;
107
133
  --gold: #ff9152; --canvas: #151412; --canvas-dot: #2a2825; --block: #222120; --block-head: #292725; --code-bg: #1a1917; --warn: #e0a740;
108
- --ce-bg: #1a1917; --ce-gutter: #1f1d1b; --ce-gutter-text: #8f8880;
134
+ --ce-bg: #1a1917; --ce-gutter: #1f1d1b; --ce-gutter-text: #8f8880; --ce-gutter-active: #e9e6e3; --ce-text: #e9e6e3;
135
+ --ce-kw: #ff9152; --ce-type: #5cc8bd; --ce-fn: #8ab4ff; --ce-str: #a5d68a; --ce-num: #d4a8ff; --ce-com: #938b83; --ce-bind: #ff8fb8;
136
+ --ce-qid: #e6c26a; --ce-op: #b8b0a8; --ce-tag: #8ab4ff; --ce-attr: #ff9152; --ce-err: #ff7a70;
137
+ --ce-match: rgb(255 145 82 / 22%); --ce-match-ring: rgb(255 145 82 / 60%); --ce-line: rgb(255 255 255 / 4.5%); --ce-select: rgb(255 145 82 / 28%);
109
138
  --shadow-xs: 0 1px 1px rgb(0 0 0 / 30%);
110
139
  --shadow-sm: 0 1px 2px rgb(0 0 0 / 40%), inset 0 1px 0 rgb(255 255 255 / 3%);
111
140
  --shadow-md: 0 4px 16px -2px rgb(0 0 0 / 50%), inset 0 1px 0 rgb(255 255 255 / 4%);
@@ -1301,3 +1330,66 @@ pre.wrap-pre { white-space: pre-wrap; overflow-wrap: anywhere; }
1301
1330
  .rm-main { display: grid; gap: 1rem; min-width: 0; }
1302
1331
  .rm-tree .tree code { font-size: 0.75rem; min-width: 3.4rem; }
1303
1332
  @media (max-width: 760px) { .rm-layout { grid-template-columns: minmax(0, 1fr); } }
1333
+
1334
+ /* ------------------------------------------------------------ page polish (sprint 108): Object Browser, wizards, SQL Monitor */
1335
+ /* Object Browser: the tree in the builder's type scale, the selected object with the ember bar */
1336
+ .ob-folder .tree { font-size: var(--fs-base); }
1337
+ .ob-folder .tree a { padding-block: 0.28rem; }
1338
+ .ob-folder .tree a[aria-current="page"] { background: var(--sel-bg); color: var(--sel-text); box-shadow: inset 2px 0 0 var(--sel-bar); font-weight: 600; }
1339
+ .ob-folder > summary { border-radius: var(--radius-sm); }
1340
+ .ob-folder > summary:hover { background: var(--surface-3); }
1341
+ .ob-folder > summary .icon { color: var(--muted); }
1342
+ /* wizards: the icon picker looks like a select; a colour field is a swatch, not a bar */
1343
+ .ide .icon-picker > summary { border-color: var(--field-border); border-radius: var(--radius-sm); box-shadow: var(--shadow-xs); transition: border-color var(--dur) var(--ease); }
1344
+ .ide .icon-picker > summary:hover { border-color: color-mix(in srgb, var(--text) 22%, var(--border-strong)); }
1345
+ .ide .icon-picker > summary::after {
1346
+ content: ""; width: 14px; height: 14px; margin-left: 0.15rem;
1347
+ background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23857d75' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m7 10 5 5 5-5'/%3E%3C/svg%3E") center / 14px no-repeat;
1348
+ }
1349
+ .ide .icon-picker[open] > summary { border-color: var(--accent); box-shadow: 0 0 0 3px var(--ring); }
1350
+ .ide .icon-picker-panel { border-color: var(--border-strong); border-radius: var(--radius); box-shadow: var(--shadow-md); background: var(--surface); }
1351
+ .ide .field input[type=color]:not(.swatch) {
1352
+ width: 4.5rem; height: 34px; min-height: 0; padding: 3px; cursor: pointer;
1353
+ border: 1px solid var(--field-border); border-radius: var(--radius-sm); background: var(--surface); box-shadow: var(--shadow-xs);
1354
+ }
1355
+ .ide .field input[type=color]::-webkit-color-swatch-wrapper { padding: 0; }
1356
+ .ide .field input[type=color]::-webkit-color-swatch { border: 0; border-radius: 4px; }
1357
+ .ide .field input[type=color]::-moz-color-swatch { border: 0; border-radius: 4px; }
1358
+ /* SQL Monitor: the refresh controls on one row */
1359
+ .sm-toolbar:not([hidden]) { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; margin: 0.25rem 0 0.75rem; }
1360
+ .sm-toolbar select { width: auto; }
1361
+ .sm-toolbar [aria-pressed="true"] { background: var(--accent-soft); color: var(--accent); }
1362
+
1363
+ /* (sprint 109, WS-1) the Workspace home (/builder/home) */
1364
+ .wh-title { margin: 0 0 14px; font-size: var(--fs-xl); overflow-wrap: anywhere; }
1365
+ .wh-summary h2, .wh-top-head h2 { margin: 0 0 10px; }
1366
+ .wh-stats a.stat { color: inherit; text-decoration: none; }
1367
+ .wh-stats a.stat:hover { border-color: color-mix(in srgb, var(--accent) 45%, var(--border)); }
1368
+ .wh-stats a.stat span { color: var(--accent); }
1369
+ .wh-top-head { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8px; margin-top: 8px; }
1370
+ .wh-top-head h2 { margin: 0; }
1371
+ .wh-timeframe { display: inline-flex; }
1372
+ .wh-top { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr)); gap: 16px; margin-top: 10px; }
1373
+ .wh-top > .region { margin: 0; min-width: 0; }
1374
+ .wh-message { margin-bottom: 16px; }
1375
+ .wh-message-text { margin: 0 0 8px; white-space: pre-line; overflow-wrap: anywhere; }
1376
+ .wh-edit > summary { list-style: none; display: inline-flex; align-items: center; gap: 6px; cursor: pointer; }
1377
+ .wh-edit > summary::-webkit-details-marker { display: none; }
1378
+ .wh-edit[open] > summary { margin-bottom: 10px; }
1379
+ .wh-release { display: flex; gap: 0.6rem; align-items: flex-start; }
1380
+ .wh-release p { margin: 0.2rem 0 0; overflow-wrap: anywhere; }
1381
+ .wh-release-form { margin-top: 10px; }
1382
+ @media (max-width: 640px) {
1383
+ .wh-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
1384
+ .wh-stats .stat { padding: 0.6rem 0.75rem; }
1385
+ .wh-stats .stat b { font-size: 1.2rem; }
1386
+ }
1387
+
1388
+ /* (sprint 110) App Builder → Data Reporter: datasets */
1389
+ .dataset-checks { display: flex; flex-direction: column; gap: 0.35rem; margin: 0.25rem 0 0.5rem; overflow-wrap: anywhere; }
1390
+ .dataset-filter { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-bottom: 0.4rem; }
1391
+ .dataset-filter > select, .dataset-filter > input { flex: 1 1 10rem; min-width: 0; }
1392
+ .dataset-columns input { min-width: 0; width: 100%; }
1393
+ .dataset-page pre.code { white-space: pre-wrap; overflow-wrap: anywhere; }
1394
+ .dataset-schema { flex-wrap: wrap; }
1395
+ .dataset-schema > select { flex: 1 1 10rem; min-width: 0; }
@@ -2,79 +2,23 @@
2
2
  transparent and lies exactly over the highlighted copy (.ce-code), so both
3
3
  must share font, size, line height, padding and tab size. */
4
4
  :root {
5
- --ce-bg: #fbfcfd;
6
- --ce-gutter: #f1f3f6;
7
- --ce-gutter-text: #5d6876;
8
- --ce-text: #1b2330;
9
- --ce-kw: #8b2bb9;
10
- --ce-type: #0b6e8a;
11
- --ce-fn: #1d5fc4;
12
- --ce-str: #1d7a3a;
13
- --ce-num: #b35400;
14
- --ce-com: #6b7685;
15
- --ce-bind: #b0306a;
16
- --ce-qid: #7a5b00;
17
- --ce-tag: #1d5fc4;
18
- --ce-attr: #8b2bb9;
19
- --ce-err: #c23028;
20
- --ce-match: rgb(11 99 197 / 18%);
21
- --ce-select: rgb(11 99 197 / 22%);
5
+ /* the colours (--ce-bg, --ce-kw, …, --ce-line, --ce-select) are tokens in builder.css, light and dark */
22
6
  --ce-font: 0.84rem;
23
7
  --ce-lh: 20px; /* whole pixels: fractional line heights drift apart over thousands of lines */
24
8
  }
25
- @media (prefers-color-scheme: dark) {
26
- :root:not([data-theme="light"]) {
27
- --ce-bg: #12161d;
28
- --ce-gutter: #171c24;
29
- --ce-gutter-text: #8f99a8;
30
- --ce-text: #e3e7ed;
31
- --ce-kw: #d39bff;
32
- --ce-type: #5fd0e6;
33
- --ce-fn: #82b6ff;
34
- --ce-str: #8fd694;
35
- --ce-num: #ffb46b;
36
- --ce-com: #8591a3;
37
- --ce-bind: #ff8cc0;
38
- --ce-qid: #e8c66a;
39
- --ce-tag: #82b6ff;
40
- --ce-attr: #d39bff;
41
- --ce-err: #ff7a70;
42
- --ce-match: rgb(105 164 255 / 25%);
43
- --ce-select: rgb(105 164 255 / 30%);
44
- }
45
- }
46
- :root[data-theme="dark"] {
47
- --ce-bg: #12161d;
48
- --ce-gutter: #171c24;
49
- --ce-gutter-text: #8f99a8;
50
- --ce-text: #e3e7ed;
51
- --ce-kw: #d39bff;
52
- --ce-type: #5fd0e6;
53
- --ce-fn: #82b6ff;
54
- --ce-str: #8fd694;
55
- --ce-num: #ffb46b;
56
- --ce-com: #8591a3;
57
- --ce-bind: #ff8cc0;
58
- --ce-qid: #e8c66a;
59
- --ce-tag: #82b6ff;
60
- --ce-attr: #d39bff;
61
- --ce-err: #ff7a70;
62
- --ce-match: rgb(105 164 255 / 25%);
63
- --ce-select: rgb(105 164 255 / 30%);
64
- }
65
9
  @media (pointer: coarse) {
66
10
  :root { --ce-font: 16px; --ce-lh: 24px; } /* the same as other fields: no zoom on focus in iOS */
67
11
  }
68
12
 
69
13
  .ce {
70
14
  position: relative; display: grid; grid-template-columns: auto minmax(0, 1fr);
71
- min-width: 0; max-width: 100%; border: 1px solid var(--border); border-radius: 6px; background: var(--ce-bg);
15
+ min-width: 0; max-width: 100%; border: 1px solid var(--field-border, var(--border)); border-radius: var(--radius-sm, 6px); background: var(--ce-bg);
72
16
  }
73
- .ce:focus-within { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
17
+ .ce:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px var(--ring, transparent); }
74
18
  .ce-gutter {
75
19
  --ce-digits: 2;
76
20
  width: calc(var(--ce-digits) * 1ch + 1.1rem); overflow: hidden; position: relative;
77
- background: var(--ce-gutter); border-right: 1px solid var(--border); border-radius: 6px 0 0 0;
21
+ background: var(--ce-gutter); border-right: 1px solid var(--border); border-radius: var(--radius-sm, 6px) 0 0 0;
78
22
  color: var(--ce-gutter-text); user-select: none;
79
23
  }
80
24
  .ce-gutter, .ce .ce-code, .ce textarea.ce-input {
@@ -82,6 +26,11 @@
82
26
  font-variant-ligatures: none; letter-spacing: 0; font-kerning: none;
83
27
  }
84
28
  .ce-nums { position: absolute; top: 0; left: 0; right: 0; white-space: pre; text-align: right; padding: 0.5rem 0.45rem 0 0; will-change: transform; }
29
+ /* the caret's line (sprint 108): a band behind the text and its number in the gutter, while the editor has the focus */
30
+ .ce-cur, .ce-cur-num { position: absolute; left: 0; right: 0; top: 0; height: var(--ce-lh); will-change: transform; display: none; }
31
+ .ce-cur { background: var(--ce-line); }
32
+ .ce-cur-num { padding-right: 0.45rem; text-align: right; color: var(--ce-gutter-active); background: var(--ce-gutter); font-weight: 650; }
33
+ .ce:focus-within :is(.ce-cur, .ce-cur-num):not([hidden]) { display: block; }
85
34
  .ce-main { position: relative; min-width: 0; }
86
35
  .ce .ce-hl { position: absolute; inset: 0; overflow: hidden; pointer-events: none; }
87
36
  .ce .ce-code {
@@ -99,29 +48,33 @@
99
48
  .ce textarea.ce-input:focus-visible { outline: none; }
100
49
  .ce textarea.sql-editor { min-height: 18rem; }
101
50
 
102
- /* tokens */
103
- .ce-code .t-kw { color: var(--ce-kw); font-weight: 600; }
104
- .ce-code .t-type { color: var(--ce-type); }
105
- .ce-code .t-fn { color: var(--ce-fn); }
106
- .ce-code .t-str { color: var(--ce-str); }
107
- .ce-code .t-key { color: var(--ce-fn); }
108
- .ce-code .t-num { color: var(--ce-num); }
109
- .ce-code .t-com { color: var(--ce-com); font-style: italic; }
110
- .ce-code .t-bind, .ce-code .t-sub { color: var(--ce-bind); font-weight: 600; }
111
- .ce-code .t-qid { color: var(--ce-qid); }
112
- .ce-code .t-dol { color: var(--ce-str); font-weight: 600; }
113
- .ce-code .t-tag { color: var(--ce-tag); }
114
- .ce-code .t-attr { color: var(--ce-attr); }
115
- .ce-code .t-ent { color: var(--ce-num); }
116
- .ce-code .t-err { color: var(--ce-err); text-decoration: wavy underline; }
117
- .ce-code .t-op, .ce-code .t-pun, .ce-code .t-br, .ce-code .t-id { color: var(--ce-text); }
118
- .ce-code .t-match { background: var(--ce-match); border-radius: 2px; outline: 1px solid var(--ce-match); }
51
+ /* tokens: in the editor and in read-only code (<pre data-code-view>, sprint 108) */
52
+ :is(.ce-code, .ce-view) .t-kw { color: var(--ce-kw); font-weight: 600; }
53
+ :is(.ce-code, .ce-view) .t-type { color: var(--ce-type); }
54
+ :is(.ce-code, .ce-view) .t-fn { color: var(--ce-fn); }
55
+ :is(.ce-code, .ce-view) .t-str { color: var(--ce-str); }
56
+ :is(.ce-code, .ce-view) .t-key { color: var(--ce-fn); }
57
+ :is(.ce-code, .ce-view) .t-num { color: var(--ce-num); }
58
+ :is(.ce-code, .ce-view) .t-com { color: var(--ce-com); font-style: italic; }
59
+ :is(.ce-code, .ce-view) .t-bind, :is(.ce-code, .ce-view) .t-sub { color: var(--ce-bind); font-weight: 600; }
60
+ :is(.ce-code, .ce-view) .t-qid { color: var(--ce-qid); }
61
+ :is(.ce-code, .ce-view) .t-dol { color: var(--ce-str); font-weight: 600; }
62
+ :is(.ce-code, .ce-view) .t-tag { color: var(--ce-tag); }
63
+ :is(.ce-code, .ce-view) .t-attr { color: var(--ce-attr); }
64
+ :is(.ce-code, .ce-view) .t-ent { color: var(--ce-num); }
65
+ :is(.ce-code, .ce-view) .t-err { color: var(--ce-err); text-decoration: wavy underline; }
66
+ :is(.ce-code, .ce-view) .t-op { color: var(--ce-op); }
67
+ :is(.ce-code, .ce-view) .t-pun, :is(.ce-code, .ce-view) .t-br, :is(.ce-code, .ce-view) .t-id { color: var(--ce-text); }
68
+ .ce-code .t-match { background: var(--ce-match); border-radius: 2px; outline: 1px solid var(--ce-match-ring); }
69
+
70
+ pre.ce-view { background: var(--ce-bg); color: var(--ce-text); border-color: var(--border); }
71
+ pre.ce-view::selection, pre.ce-view ::selection { background: var(--ce-select); }
119
72
 
120
73
  /* status bar: language, hint, caret position, check */
121
74
  .ce-bar {
122
75
  grid-column: 1 / -1; display: flex; flex-wrap: wrap; align-items: center; gap: 0.2rem 0.75rem;
123
76
  padding: 0.2rem 0.5rem; border-top: 1px solid var(--border); background: var(--ce-gutter);
124
- border-radius: 0 0 6px 6px; font-size: 0.75rem; color: var(--muted); min-width: 0;
77
+ border-radius: 0 0 var(--radius-sm, 6px) var(--radius-sm, 6px); font-size: 0.75rem; color: var(--muted); min-width: 0;
125
78
  }
126
79
  .ce-lang { font-weight: 650; letter-spacing: 0.02em; }
127
80
  .ce-hint { flex: 1 1 12rem; min-width: 0; }
@@ -138,7 +91,7 @@
138
91
  position: absolute; z-index: 30; left: 0; top: 0; margin: 0; padding: 0.25rem 0; list-style: none;
139
92
  width: max-content; min-width: 14rem; max-width: min(34rem, calc(100% - 0.5rem)); max-height: 15rem; overflow: auto;
140
93
  background: var(--surface); color: var(--text); border: 1px solid var(--border); border-radius: 6px;
141
- box-shadow: 0 8px 24px rgb(16 24 40 / 18%); font-size: 0.82rem;
94
+ box-shadow: var(--shadow-lg, 0 8px 24px rgb(16 24 40 / 18%)); font-size: 0.82rem;
142
95
  }
143
96
  .ce-opt { display: flex; align-items: baseline; gap: 0.75rem; padding: 0.2rem 0.6rem 0.2rem 0.4rem; cursor: pointer; white-space: nowrap; }
144
97
  .ce-opt::before {
@@ -15,6 +15,8 @@
15
15
  // - "Check" plans the SQL like the Advisor (POST /builder/code/check)
16
16
  // - screen readers keep the plain textarea; the list is announced through
17
17
  // aria-activedescendant and a polite live region
18
+ // - (sprint 108) read-only code, <pre data-code-view="sql|plpgsql|json|html">, is
19
+ // highlighted with the same lexer and colours (DDL, generated SQL)
18
20
  //
19
21
  // Strict CSP: no inline styles; geometry is set through the CSSOM. Suggestions
20
22
  // and highlighted text are built as text nodes, never parsed as HTML.
@@ -437,15 +439,20 @@
437
439
  gutter.setAttribute('aria-hidden', 'true');
438
440
  const nums = document.createElement('div');
439
441
  nums.className = 'ce-nums';
440
- gutter.append(nums);
442
+ // the caret's line: a band behind the text, its number over the gutter's
443
+ const curNum = document.createElement('div');
444
+ curNum.className = 'ce-cur-num';
445
+ gutter.append(nums, curNum);
441
446
  const main = document.createElement('div');
442
447
  main.className = 'ce-main';
443
448
  const hl = document.createElement('div');
444
449
  hl.className = 'ce-hl';
445
450
  hl.setAttribute('aria-hidden', 'true');
451
+ const cur = document.createElement('div');
452
+ cur.className = 'ce-cur';
446
453
  const inner = document.createElement('pre');
447
454
  inner.className = 'ce-code';
448
- hl.append(inner);
455
+ hl.append(cur, inner);
449
456
  ta.parentNode.insertBefore(wrap, ta);
450
457
  main.append(hl, ta);
451
458
  wrap.append(gutter, main);
@@ -502,7 +509,7 @@
502
509
  ta.setAttribute('aria-autocomplete', 'list');
503
510
  ta.setAttribute('aria-haspopup', 'listbox');
504
511
  ta.setAttribute('aria-controls', list.id);
505
- Object.assign(this, { wrap, gutter, nums, main, hl, inner, list, bar, langEl: lang, hint, pos, msg, check, live, suggest });
512
+ Object.assign(this, { wrap, gutter, nums, cur, curNum, main, hl, inner, list, bar, langEl: lang, hint, pos, msg, check, live, suggest });
506
513
  this.applyLang();
507
514
  }
508
515
 
@@ -661,9 +668,27 @@
661
668
  this.digits = digits;
662
669
  this.gutter.style.setProperty('--ce-digits', String(Math.max(2, digits)));
663
670
  }
671
+ this.placeLine();
664
672
  if (this.pop) this.place();
665
673
  }
666
674
 
675
+ /** The current-line band and number: on the caret's line; hidden while a selection spans lines. */
676
+ placeLine() {
677
+ const ta = this.ta;
678
+ const lines = this.lineStarts;
679
+ const a = lowerBound(lines, ta.selectionStart + 1);
680
+ const b = lowerBound(lines, ta.selectionEnd + 1);
681
+ const hide = a !== b;
682
+ this.cur.hidden = hide;
683
+ this.curNum.hidden = hide;
684
+ if (hide) return;
685
+ const { lh, padTop } = this.metrics();
686
+ const y = padTop + (a - 1) * lh - ta.scrollTop;
687
+ this.cur.style.transform = `translateY(${y}px)`;
688
+ this.curNum.style.transform = `translateY(${y}px)`;
689
+ this.curNum.textContent = String(a);
690
+ }
691
+
667
692
  tokenAt(off) {
668
693
  this.sync();
669
694
  const { T, S, E } = this.toks;
@@ -703,6 +728,7 @@
703
728
  this.sync();
704
729
  if (this.pop && this.ta.selectionStart !== this.ta.selectionEnd) this.close();
705
730
  if (this.updateMatch()) this.schedule(true);
731
+ else this.placeLine();
706
732
  const c = this.ta.selectionStart;
707
733
  const line = lowerBound(this.lineStarts, c + 1);
708
734
  this.pos.textContent = `Ln ${line}, Col ${c - this.lineStarts[line - 1] + 1}`;
@@ -997,12 +1023,41 @@
997
1023
  return lo;
998
1024
  }
999
1025
 
1026
+ /** Highlight a read-only <pre data-code-view>: its text, as spans (text nodes only). Long ones stay plain. */
1027
+ function colorize(pre) {
1028
+ const lang = pre.dataset.codeView in LANG_LABEL ? pre.dataset.codeView : 'text';
1029
+ pre.classList.add('ce-view');
1030
+ const text = pre.textContent;
1031
+ if (lang === 'text' || text.length > 200000) return;
1032
+ const { T, S, E } = lex(lang, text);
1033
+ const frag = document.createDocumentFragment();
1034
+ let pos = 0;
1035
+ for (let k = 0; k < T.length; k++) {
1036
+ const st = Math.max(S[k], pos);
1037
+ if (E[k] <= st) continue;
1038
+ if (st > pos) frag.append(text.slice(pos, st));
1039
+ const span = document.createElement('span');
1040
+ span.className = `t-${T[k]}`;
1041
+ span.textContent = text.slice(st, E[k]);
1042
+ frag.append(span);
1043
+ pos = E[k];
1044
+ }
1045
+ if (pos < text.length) frag.append(text.slice(pos));
1046
+ pre.replaceChildren(frag);
1047
+ }
1048
+
1000
1049
  const editors = new WeakMap();
1050
+ const viewed = new WeakSet();
1001
1051
  function enhance(root) {
1002
1052
  for (const ta of (root || document).querySelectorAll('textarea[data-code]')) {
1003
1053
  if (editors.has(ta) || ta.closest('.ce')) continue;
1004
1054
  editors.set(ta, new Editor(ta));
1005
1055
  }
1056
+ for (const pre of (root || document).querySelectorAll('pre[data-code-view]')) {
1057
+ if (viewed.has(pre)) continue;
1058
+ viewed.add(pre);
1059
+ colorize(pre);
1060
+ }
1006
1061
  }
1007
1062
  window.pgkilnCodeEditor = { enhance, editorOf: (ta) => editors.get(ta) };
1008
1063
  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', () => enhance());