pgkiln 0.40.0 → 0.42.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 (73) hide show
  1. package/db/migrations/095_rest_catalogs.sql +59 -0
  2. package/db/migrations/096_developer_email.sql +15 -0
  3. package/db/migrations/097_generated_app_quality.sql +911 -0
  4. package/db/migrations/098_app_activity.sql +37 -0
  5. package/docs/apex-feature-parity.md +15 -12
  6. package/docs/guide/03-builder.md +103 -19
  7. package/docs/guide/04-pages-and-regions.md +37 -4
  8. package/docs/guide/09-reference.md +6 -2
  9. package/docs/guide/11-from-apex.md +1 -1
  10. package/docs/guide/12-development.md +19 -2
  11. package/docs/guide/14-globalization.md +7 -3
  12. package/docs/guide/17-mobile.md +1 -1
  13. package/docs/guide/19-rest-data-sources.md +39 -1
  14. package/examples/hr/hr_59_gantt_editing.sql +21 -0
  15. package/examples/hr/hr_60_tree_charts.sql +46 -0
  16. package/package.json +1 -1
  17. package/public/app.css +219 -33
  18. package/public/app.js +206 -0
  19. package/public/builder.css +31 -3
  20. package/public/builder.js +31 -0
  21. package/public/sw.js +5 -2
  22. package/src/app.ts +6 -0
  23. package/src/builder/appfromfile.ts +5 -4
  24. package/src/builder/appsheets.ts +23 -9
  25. package/src/builder/appwizard.ts +215 -70
  26. package/src/builder/blueprints.ts +2 -1
  27. package/src/builder/components.ts +1 -1
  28. package/src/builder/find-workspace.ts +120 -0
  29. package/src/builder/home.ts +4 -32
  30. package/src/builder/mail.ts +39 -0
  31. package/src/builder/newapp.ts +182 -5
  32. package/src/builder/region-settings.ts +19 -1
  33. package/src/builder/rest-catalogs.ts +424 -0
  34. package/src/builder/routes.ts +36 -34
  35. package/src/builder/shared.ts +2 -0
  36. package/src/builder/workspace-transfer.ts +136 -0
  37. package/src/i18n/ar.ts +15 -0
  38. package/src/i18n/cs.ts +15 -0
  39. package/src/i18n/da.ts +15 -0
  40. package/src/i18n/de.ts +15 -0
  41. package/src/i18n/el.ts +15 -0
  42. package/src/i18n/es.ts +15 -0
  43. package/src/i18n/fi.ts +15 -0
  44. package/src/i18n/fr.ts +15 -0
  45. package/src/i18n/he.ts +15 -0
  46. package/src/i18n/it.ts +15 -0
  47. package/src/i18n/ja.ts +15 -0
  48. package/src/i18n/ko.ts +15 -0
  49. package/src/i18n/nb.ts +15 -0
  50. package/src/i18n/pl.ts +15 -0
  51. package/src/i18n/pt.ts +15 -0
  52. package/src/i18n/ru.ts +15 -0
  53. package/src/i18n/sv.ts +15 -0
  54. package/src/i18n/tr.ts +15 -0
  55. package/src/i18n/uk.ts +15 -0
  56. package/src/i18n/zh.ts +15 -0
  57. package/src/i18n.ts +30 -0
  58. package/src/icons.ts +2 -1
  59. package/src/layout.ts +2 -2
  60. package/src/restcatalog.ts +605 -0
  61. package/src/runtime/charts.ts +304 -15
  62. package/src/runtime/data-reporter.ts +48 -16
  63. package/src/runtime/devbar.ts +35 -14
  64. package/src/runtime/engine.ts +7 -4
  65. package/src/runtime/gantt-move.ts +63 -0
  66. package/src/runtime/locale.ts +2 -2
  67. package/src/runtime/regions.ts +28 -2
  68. package/src/runtime/render.ts +18 -4
  69. package/src/runtime/report.ts +15 -9
  70. package/src/runtime/routes.ts +39 -0
  71. package/src/runtime/styles.ts +5 -2
  72. package/src/websources.ts +11 -3
  73. package/src/workspace-export.ts +240 -0
@@ -212,7 +212,7 @@ Under **Settings → Theme**, *Theme style* is **Automatic** (follow the device,
212
212
  has the same choice. The choice is saved on the account, so it applies in every application that
213
213
  allows it. For public applications it's kept in a cookie.
214
214
 
215
- ## Base styles: Iris and Standard
215
+ ## Base styles: Iris, Lavender and Standard
216
216
 
217
217
  Every application has a **base style** (Settings → Theme → *Base style*; APEX: the Universal Theme's theme style,
218
218
  whose default in 26.1 is *Iris*). It sets the colours of the light and the dark theme, the corners, the font and the
@@ -220,12 +220,16 @@ shadows of the whole application:
220
220
 
221
221
  | Base style | Look |
222
222
  |---|---|
223
- | **Iris** (the default for new applications) | Indigo accent (`#5146d8`, `#a59cff` in dark mode), a deep indigo header, cool light greys, larger corners (12 px), softer shadows, rounded navigation items and pill-shaped primary buttons |
223
+ | **Iris** (the default for new applications) | (0.42, like APEX 26's Iris) A white body, a dark warm-grey header (`#3a3632`) with a thin accent strip and a dark menu, a blue accent (`#2b62c4`, `#84aef2` in dark mode), 4 px corners, regions with a rule under their header, a dark primary button and a large bold page title on the body (no title band) |
224
+ | **Lavender** | pgkiln's Iris until 0.41: indigo accent (`#5146d8`), a deep indigo header, cool light greys, 12 px corners, softer shadows, rounded navigation items and pill-shaped primary buttons |
224
225
  | **Standard** | pgkiln's look until 0.28: blue accent, navy header, 8 px corners. Applications created before 0.29 keep it until you change it |
225
226
 
226
227
  The accent and header colours under Settings → Theme, and the style variants below, change the base style's
227
228
  colours (in the light theme). Choosing a colour equal to a base style's own stores nothing, so switching the base
228
- style later changes it too. All text colours of both base styles meet WCAG AA contrast in light and dark mode.
229
+ style later changes it too. All text colours of the base styles meet WCAG AA contrast in light and dark mode.
230
+
231
+ New applications start in the **light** theme (Settings → Theme → *Mode*), with *Users may choose light or dark*
232
+ on, as in APEX.
229
233
 
230
234
  ## Style variants (Theme Roller)
231
235
 
@@ -10,7 +10,7 @@ the home screen with its own icon, full screen, and working on when the network
10
10
 
11
11
  | Setting | What it does |
12
12
  |---|---|
13
- | Installable | Adds a web app manifest and a service worker to the application. Phones offer "Add to home screen" / "Install"; the app then opens full screen, without the browser bar |
13
+ | Installable | Adds a web app manifest and a service worker to the application. Phones offer "Add to home screen" / "Install"; the app then opens full screen, without the browser bar. (0.42) Where the browser offers the installation (Chrome, Edge), the header shows an **Install app** button (APEX: the navigation bar's Install App entry) |
14
14
  | Name under the icon | The short name on the home screen (the application's name when empty) |
15
15
  | Keep visited pages on the device | Pages a user opened are shown again when there is no connection (with a notice that they may be out of date) |
16
16
  | Keep forms sent without a connection | Forms submitted offline are kept on the device and sent when the connection is back |
@@ -111,7 +111,7 @@ Shared Components → **REST data sources** → + Add.
111
111
  | URL | `https://api.example.com/v1/cities/{city}/weather`: `{name}` is a path parameter. The host is fixed: parameters can only come after it |
112
112
  | Method | `GET`, `POST`, `PUT`, `PATCH`, `DELETE` |
113
113
  | Web credential | The name of a web credential, or empty |
114
- | Parameters | JSON: `[{"name": "city", "in": "path", "default": "&P1_CITY.", "required": true}, {"name": "units", "in": "query", "default": "metric"}]`. `in` is `path`, `query`, `header` or `body`. Defaults may use `&ITEM.` substitutions |
114
+ | Parameters | JSON: `[{"name": "city", "in": "path", "default": "&P1_CITY.", "required": true}, {"name": "units", "in": "query", "default": "metric"}]`. `in` is `path`, `query`, `header` or `body`. Defaults may use `&ITEM.` substitutions. `key`: the service's own name for a query, header or body parameter when it isn't lower case with `_`, e.g. `{"name": "page_size", "in": "query", "key": "pageSize"}`; `Authorization`, `Cookie`, `Host` and the transport headers can't be header parameters |
115
115
  | Headers | JSON: extra request headers, e.g. `{"Accept-Language": "en"}`. Not for secrets (use a web credential); `Authorization`, `Cookie`, `Host` and the transport headers can't be set |
116
116
  | Body | For `POST`, `PUT`, `PATCH`: a JSON template, `{name}` becomes the parameter's value as a JSON string. Empty: the `body` parameters as a JSON object |
117
117
  | Row selector | Where the rows are in the response: `items`, `data.results`, `$.list[*]`. Empty: the response itself (an array, or one object as one row) |
@@ -238,6 +238,44 @@ nothing. Two runs of one source never overlap. The last 100 runs per source are
238
238
  application's synchronisations start switched off. `AUTOMATIONS=off` stops scheduled and queued
239
239
  runs on that server.
240
240
 
241
+ ## REST source catalogs
242
+
243
+ Workspace utilities → **REST Source Catalogs** (APEX: REST Source Catalogs) describes a web service once for the
244
+ workspace: a **base URL** (e.g. `https://api.example.com/v1`) and its **services**, each an endpoint with a path after
245
+ the base URL (`/pets/{pet_id}`), a method, and the same parameters, headers, body, row selector, columns, key columns
246
+ and operations as a [REST data source](#rest-data-sources). Every developer of the workspace manages its catalogs.
247
+
248
+ A catalog comes from:
249
+
250
+ - an **OpenAPI 3.x or Swagger 2.0 document** in JSON (*Import a catalog*: upload `openapi.json`, paste it, or give its
251
+ URL, which the server reads, so the host must be on the [allow-list](#server-configuration-and-the-allow-list));
252
+ - a **catalog file** downloaded from another workspace or installation (*Download* on the catalog's page);
253
+ - **by hand**: a name and base URL, then *Add a service*.
254
+
255
+ Reading an OpenAPI document, pgkiln takes for each `GET`, `POST`, `PUT`, `PATCH` and `DELETE` operation:
256
+
257
+ | From the document | Becomes |
258
+ |---|---|
259
+ | `operationId` (or the method and path) | The service's name, e.g. `listPets` → `LIST_PETS` |
260
+ | `servers[0].url` (variables at their defaults; Swagger 2: `schemes`, `host`, `basePath`) | The base URL. A relative one is read against the document's URL; or type the base URL |
261
+ | `path`, `query` and `header` parameters (also the path's own) | Parameters: `petId` → `pet_id`, with `"key": "petId"` when the service's name differs; a schema `default` (or an `example`) as the default |
262
+ | The scalar properties of a JSON request body | `body` parameters (nested objects and lists need a body template) |
263
+ | The JSON schema of the `200`/`201` (or `default`) response | The row selector (a list, or the list property of an object such as `items`, `data` or `results`) and the columns with their types (`integer`, `number`, `boolean`, `date`, `date-time` → `timestamp`, objects and lists → `json`) |
264
+ | A collection `GET /pets` with `GET`, `PUT`/`PATCH`, `DELETE /pets/{petId}` and `POST /pets` | Key columns and [write-back operations](#writing-back-from-forms-and-grids) on the collection's service, when the item's parameter names a column (`pet_id`, else `id`) |
265
+
266
+ `$ref`s inside the document and `allOf` are followed. What a data source can't use (cookie and form parameters, the
267
+ `Authorization` header, external `$ref`s, `HEAD` and `OPTIONS`) is listed after the import, never dropped silently.
268
+ YAML documents: convert them to JSON first (most services offer `openapi.json` too).
269
+
270
+ **Making a data source from a catalog**: an application's Shared Components → REST data sources → + Add shows *From a
271
+ REST source catalog* when the application's workspace has catalogs: pick a service, optionally another name and one
272
+ of the application's web credentials. The data source is a **copy** (URL = base URL + path): changing or deleting the
273
+ catalog later doesn't change it, and *Replace from a document* on a catalog's page replaces only the catalog's
274
+ services. Credentials never belong to a catalog: each application signs in with its own web credential.
275
+
276
+ Catalogs belong to the workspace (migration 095): they are not part of an application's export, but of the
277
+ [workspace export](03-builder.md#export-and-import).
278
+
241
279
  ## The `invoke_api` process
242
280
 
243
281
  A page process of type `invoke_api` ([chapter 6](06-processing.md#processes)) calls a web service.
@@ -0,0 +1,21 @@
1
+ -- =====================================================================
2
+ -- HR example, part 59: dragging tasks on a Gantt chart
3
+ -- (docs/guide/04-pages-and-regions.md, chart regions)
4
+ --
5
+ -- The office move on page 32 "Project plan" (part 35) becomes editable for
6
+ -- managers: drag a task to new dates or drag its end; the arrow keys do the
7
+ -- same. The update runs as the application's role; a milestone (no end
8
+ -- date) keeps having none.
9
+ -- =====================================================================
10
+
11
+ grant update (starts, ends) on hr.project_task to hr_app;
12
+
13
+ update meta.region r
14
+ set config = r.config || jsonb_build_object(
15
+ 'gantt_move', $q$update hr.project_task
16
+ set starts = :NEW_START::date,
17
+ ends = case when ends is null then null else :NEW_END::date end
18
+ where id = :TASK_ID::int$q$,
19
+ 'gantt_move_authz', 'MANAGER')
20
+ from meta.page p join meta.app a on a.id = p.app_id
21
+ where p.id = r.page_id and a.alias = 'hr' and p.page_no = 32 and r.title = 'Office move';
@@ -0,0 +1,46 @@
1
+ -- =====================================================================
2
+ -- HR example, part 60: treemap, sunburst and timeline charts
3
+ -- (docs/guide/04-pages-and-regions.md, chart regions)
4
+ --
5
+ -- On page 32 "Project plan":
6
+ -- - a treemap of the salaries per department and job (node_id and
7
+ -- parent_id make the hierarchy); a department drills down to its
8
+ -- employees;
9
+ -- - a sunburst of the head count: departments, their jobs and the
10
+ -- employees in them;
11
+ -- - a timeline of the hires, a lane per department.
12
+ -- =====================================================================
13
+
14
+ insert into meta.region (page_id, seq, title, type, columns, template, source, config)
15
+ select p.id, r.seq, r.title, r.type, r.cols, r.tpl, r.source, r.config::jsonb
16
+ from meta.page p join meta.app a on a.id = p.app_id, (values
17
+ (90, 'Salaries by department and job', 'chart', 6, 'standard',
18
+ $q$select d.dname as "Department or job", 0 as "Salary", 'd' || d.deptno as node_id, null as parent_id, d.deptno
19
+ from hr.dept d
20
+ where exists (select 1 from hr.emp e where e.deptno = d.deptno and e.active)
21
+ union all
22
+ select initcap(e.job), sum(e.sal), 'd' || e.deptno || '-' || e.job, 'd' || e.deptno, e.deptno
23
+ from hr.emp e
24
+ where e.active
25
+ group by e.deptno, e.job
26
+ order by 3$q$,
27
+ '{"kind": "treemap", "link": {"page": 2, "items": {"P2_DEPTNO": "#deptno#"}}}'),
28
+ (100, 'Head count', 'chart', 6, 'standard',
29
+ $q$select d.dname as "Name", 0 as "Employees", 'd' || d.deptno as node_id, null as parent_id
30
+ from hr.dept d
31
+ where exists (select 1 from hr.emp e where e.deptno = d.deptno and e.active)
32
+ union all
33
+ select distinct initcap(e.job), 0, 'd' || e.deptno || '-' || e.job, 'd' || e.deptno
34
+ from hr.emp e where e.active
35
+ union all
36
+ select initcap(e.ename), 1, 'e' || e.empno, 'd' || e.deptno || '-' || e.job
37
+ from hr.emp e where e.active
38
+ order by 3$q$,
39
+ '{"kind": "sunburst"}'),
40
+ (110, 'Hires', 'chart', 12, 'standard',
41
+ $q$select initcap(e.ename) as "Employee", e.hiredate as "Hired", null::date as "Until", d.dname as series
42
+ from hr.emp e join hr.dept d using (deptno)
43
+ order by e.hiredate$q$,
44
+ '{"kind": "timeline"}')
45
+ ) r(seq, title, type, cols, tpl, source, config)
46
+ where a.alias = 'hr' and p.page_no = 32;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pgkiln",
3
- "version": "0.40.0",
3
+ "version": "0.42.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
@@ -76,12 +76,100 @@
76
76
  color-scheme: dark;
77
77
  }
78
78
 
79
+ /* ---------- base style "Iris" (theme.base = 'iris'; the default for new
80
+ applications, like APEX 26's Iris theme style): (sprint 72) a light body,
81
+ a dark warm-grey header and menu with a thin accent strip, square-ish
82
+ regions with a rule under their header, 4 px corners, a dark primary button
83
+ and a large page title on the body. Selectors are html[data-style="iris"]:
84
+ they beat the Standard values above, and the application's own colours
85
+ (src/runtime/styles.ts, html:root, later in the page) beat them in turn.
86
+ Contrast: text ≥ 15:1, muted ≥ 5.8:1, accent ≥ 5.5:1, menu text ≥ 10:1. */
87
+ html[data-style="iris"] {
88
+ --bg: #ffffff;
89
+ --surface: #ffffff;
90
+ --surface-2: #f7f6f5;
91
+ --surface-3: #efedeb;
92
+ --text: #1f1c1a;
93
+ --muted: #645e59;
94
+ --border: #e2dfdc;
95
+ --accent: #2b62c4;
96
+ --accent-text: #ffffff;
97
+ --accent-soft: #e9f0fb;
98
+ --header: #3a3632;
99
+ --header-text: #f7f5f3;
100
+ --nav-bg: #2f2b28;
101
+ --nav-text: #ece8e4;
102
+ --nav-muted: #b9b2ab;
103
+ --nav-hover: #403b37;
104
+ --nav-active: #4a443f;
105
+ --nav-current: #ffffff;
106
+ --hot: #2f2b28;
107
+ --hot-text: #ffffff;
108
+ --radius: 4px;
109
+ --font: "Inter", "Segoe UI Variable Text", "Segoe UI", system-ui, -apple-system, Roboto, sans-serif;
110
+ --shadow: none;
111
+ }
112
+ @media (prefers-color-scheme: dark) {
113
+ html[data-style="iris"]:not([data-theme="light"]) {
114
+ --bg: #1a1817;
115
+ --surface: #232120;
116
+ --surface-2: #2a2826;
117
+ --surface-3: #33302d;
118
+ --text: #ece9e6;
119
+ --muted: #aaa49e;
120
+ --border: #3a3633;
121
+ --accent: #84aef2;
122
+ --accent-text: #10131a;
123
+ --accent-soft: #23324a;
124
+ --header: #121110;
125
+ --header-text: #ece9e6;
126
+ --nav-bg: #1d1b1a;
127
+ --nav-text: #e2ded9;
128
+ --nav-muted: #a39c95;
129
+ --nav-hover: #2c2927;
130
+ --nav-active: #36322f;
131
+ --nav-current: #ffffff;
132
+ --hot: #ece9e6;
133
+ --hot-text: #1a1817;
134
+ }
135
+ }
136
+ html[data-style="iris"][data-theme="dark"] {
137
+ --bg: #1a1817;
138
+ --surface: #232120;
139
+ --surface-2: #2a2826;
140
+ --surface-3: #33302d;
141
+ --text: #ece9e6;
142
+ --muted: #aaa49e;
143
+ --border: #3a3633;
144
+ --accent: #84aef2;
145
+ --accent-text: #10131a;
146
+ --accent-soft: #23324a;
147
+ --header: #121110;
148
+ --header-text: #ece9e6;
149
+ --nav-bg: #1d1b1a;
150
+ --nav-text: #e2ded9;
151
+ --nav-muted: #a39c95;
152
+ --nav-hover: #2c2927;
153
+ --nav-active: #36322f;
154
+ --nav-current: #ffffff;
155
+ --hot: #ece9e6;
156
+ --hot-text: #1a1817;
157
+ }
158
+ html[data-style="iris"] .t-header { box-shadow: inset 0 3px 0 var(--accent); }
159
+ html[data-style="iris"] .t-nav { border-right-color: transparent; }
160
+ html[data-style="iris"] .t-titlebar { background: transparent; border-bottom: 0; padding: 1.25rem 1.5rem 0.25rem; }
161
+ html[data-style="iris"] .t-titlebar h1 { font-size: 2rem; font-weight: 700; letter-spacing: -0.02em; line-height: 1.2; }
162
+ html[data-style="iris"] .region-header h2 { font-size: 1.05rem; }
163
+ html[data-style="iris"] .btn-hot { background: var(--hot); border-color: var(--hot); color: var(--hot-text); }
164
+ html[data-style="iris"] .btn-hot:hover { background: color-mix(in srgb, var(--hot) 82%, var(--bg)); }
165
+ html[data-style="iris"] .btn-hot .btn-badge { background: var(--hot-text); color: var(--hot); }
166
+
79
167
  /* ---------- base style "Iris" (theme.base = 'iris'; the default for new
80
168
  applications, like APEX 26.1's Iris theme style). Selectors are
81
- html[data-style="iris"]: they beat the Standard values above, and the
169
+ html[data-style="lavender"]: they beat the Standard values above, and the
82
170
  application's own colours (src/runtime/styles.ts, html:root, later in the
83
171
  page) beat them in turn. Contrast: text ≥ 15:1, muted ≥ 5.8:1, accent ≥ 6.5:1. */
84
- html[data-style="iris"] {
172
+ html[data-style="lavender"] {
85
173
  --bg: #f6f6fb;
86
174
  --surface: #ffffff;
87
175
  --surface-2: #f8f8fc;
@@ -101,7 +189,7 @@ html[data-style="iris"] {
101
189
  --shadow: 0 1px 2px rgb(30 27 75 / 5%), 0 2px 8px rgb(30 27 75 / 6%);
102
190
  }
103
191
  @media (prefers-color-scheme: dark) {
104
- html[data-style="iris"]:not([data-theme="light"]) {
192
+ html[data-style="lavender"]:not([data-theme="light"]) {
105
193
  --bg: #121120;
106
194
  --surface: #1a1930;
107
195
  --surface-2: #201f38;
@@ -119,7 +207,7 @@ html[data-style="iris"] {
119
207
  --shadow: 0 1px 2px rgb(0 0 0 / 30%), 0 2px 8px rgb(0 0 0 / 25%);
120
208
  }
121
209
  }
122
- html[data-style="iris"][data-theme="dark"] {
210
+ html[data-style="lavender"][data-theme="dark"] {
123
211
  --bg: #121120;
124
212
  --surface: #1a1930;
125
213
  --surface-2: #201f38;
@@ -136,13 +224,13 @@ html[data-style="iris"][data-theme="dark"] {
136
224
  --nav-active: #2a2752;
137
225
  --shadow: 0 1px 2px rgb(0 0 0 / 30%), 0 2px 8px rgb(0 0 0 / 25%);
138
226
  }
139
- /* Iris: rounded navigation pills instead of an edge bar, quieter region headers, pill-shaped primary buttons */
140
- html[data-style="iris"] .t-nav a[aria-current="page"] { box-shadow: none; border-radius: calc(var(--radius) * 0.75); }
141
- html[data-style="iris"] .t-nav a, html[data-style="iris"] .t-nav summary { border-radius: calc(var(--radius) * 0.75); }
142
- html[data-style="iris"] .region-header { border-bottom-color: transparent; }
143
- html[data-style="iris"] .region-header h2 { letter-spacing: -0.01em; }
144
- html[data-style="iris"] .btn-hot { border-radius: 999px; }
145
- html[data-style="iris"] :focus-visible { outline-color: var(--accent); }
227
+ /* Lavender: rounded navigation pills instead of an edge bar, quieter region headers, pill-shaped primary buttons */
228
+ html[data-style="lavender"] .t-nav a[aria-current="page"] { box-shadow: none; border-radius: calc(var(--radius) * 0.75); }
229
+ html[data-style="lavender"] .t-nav a, html[data-style="lavender"] .t-nav summary { border-radius: calc(var(--radius) * 0.75); }
230
+ html[data-style="lavender"] .region-header { border-bottom-color: transparent; }
231
+ html[data-style="lavender"] .region-header h2 { letter-spacing: -0.01em; }
232
+ html[data-style="lavender"] .btn-hot { border-radius: 999px; }
233
+ html[data-style="lavender"] :focus-visible { outline-color: var(--accent); }
146
234
 
147
235
  * { box-sizing: border-box; }
148
236
  html { -webkit-text-size-adjust: 100%; }
@@ -225,17 +313,18 @@ details.menu > summary::-webkit-details-marker { display: none; }
225
313
  .t-nav ul ul { padding: 0 0 0 1.4rem; }
226
314
  .t-nav a, .t-nav summary {
227
315
  display: flex; align-items: center; gap: 0.65rem; padding: 0.5rem 0.65rem; border-radius: 6px;
228
- color: var(--text); text-decoration: none; cursor: pointer; white-space: nowrap; font-size: 0.93rem;
316
+ color: var(--nav-text, var(--text)); text-decoration: none; cursor: pointer; white-space: nowrap; font-size: 0.93rem;
229
317
  }
230
318
  .t-nav summary { list-style: none; }
231
319
  .t-nav summary::-webkit-details-marker { display: none; }
232
- .t-nav summary::after { content: "›"; margin-left: auto; color: var(--muted); transition: transform 0.15s; }
320
+ .t-nav summary::after { content: "›"; margin-left: auto; color: var(--nav-muted, var(--muted)); transition: transform 0.15s; }
233
321
  .t-nav details[open] > summary::after { transform: rotate(90deg); }
234
- .t-nav a:hover, .t-nav summary:hover { background: var(--surface-3); }
235
- .t-nav a[aria-current="page"] { background: var(--nav-active); color: var(--accent); font-weight: 600; box-shadow: inset 3px 0 0 var(--accent); }
236
- .t-nav .in-trail { color: var(--accent); }
237
- .t-nav .icon { color: var(--muted); }
238
- .t-nav a[aria-current="page"] .icon, .t-nav .in-trail .icon { color: var(--accent); }
322
+ .t-nav a:hover, .t-nav summary:hover { background: var(--nav-hover, var(--surface-3)); }
323
+ /* (sprint 72) --nav-* let a style have a dark menu (Iris); without them the menu takes the page's colours */
324
+ .t-nav a[aria-current="page"] { background: var(--nav-active); color: var(--nav-current, var(--accent)); font-weight: 600; box-shadow: inset 3px 0 0 var(--accent); }
325
+ .t-nav .in-trail { color: var(--nav-current, var(--accent)); }
326
+ .t-nav .icon { color: var(--nav-muted, var(--muted)); }
327
+ .t-nav a[aria-current="page"] .icon, .t-nav .in-trail .icon { color: var(--nav-current, var(--accent)); }
239
328
 
240
329
  /* ---------- main ---------- */
241
330
  .t-main { min-width: 0; }
@@ -259,6 +348,9 @@ details.menu > summary::-webkit-details-marker { display: none; }
259
348
  .region-hero { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 1rem 1.5rem; padding: 2rem 1.75rem; border-radius: var(--radius);
260
349
  background: var(--accent-soft); color: var(--text); }
261
350
  .region-hero-icon .icon { width: 3rem; height: 3rem; color: var(--accent); }
351
+ /* (097) the generated home page: the icon on a tile in the accent colour */
352
+ .region-hero-icon { display: grid; place-items: center; width: 4.5rem; height: 4.5rem; border-radius: calc(var(--radius) + 4px); background: var(--accent); }
353
+ .region-hero-icon .icon { width: 2.4rem; height: 2.4rem; color: var(--accent-text, #fff); }
262
354
  .region-hero-text h2 { font-size: 1.75rem; margin: 0 0 0.35rem; }
263
355
  .region-hero-buttons, .region-title-buttons, .region-buttons { display: flex; flex-wrap: wrap; gap: 0.5rem; }
264
356
  @media (max-width: 640px) { .region-hero { grid-template-columns: 1fr; padding: 1.25rem 1rem; } }
@@ -401,7 +493,11 @@ details.btn-menu[open] > summary .btn-caret { transform: rotate(-90deg); }
401
493
  table.report { width: 100%; border-collapse: collapse; font-size: 0.9rem; }
402
494
  .report th, .report td { text-align: left; padding: 0.5rem 0.7rem; border-bottom: 1px solid var(--border); white-space: nowrap; }
403
495
  .report tbody tr:last-child td { border-bottom: 0; }
404
- .report th { font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.03em; color: var(--muted); background: var(--surface-2); font-weight: 650; }
496
+ .report th { font-size: 0.85rem; color: var(--muted); background: var(--surface-2); font-weight: 650; }
497
+ /* (097) the edit link of a row: a pencil in a narrow first column */
498
+ .report th.row-edit, .report td.row-edit { width: 2.25rem; text-align: center; padding-left: 0.4rem; padding-right: 0.4rem; }
499
+ .report td.row-edit a { display: inline-grid; place-items: center; width: 1.9rem; height: 1.9rem; border-radius: 6px; color: var(--accent); }
500
+ .report td.row-edit a:hover, .report td.row-edit a:focus-visible { background: var(--accent-soft); }
405
501
  .report th a { color: inherit; text-decoration: none; display: inline-flex; gap: 0.3rem; }
406
502
  .report th a:hover { color: var(--accent); }
407
503
  .sort-ind { font-size: 0.7em; }
@@ -446,7 +542,19 @@ table.report { width: 100%; border-collapse: collapse; font-size: 0.9rem; }
446
542
  .swatch { width: 10px; height: 10px; border-radius: 3px; background: var(--c); flex: none; }
447
543
 
448
544
  /* horizontal bars */
449
- .chart-bar { display: flex; flex-direction: column; gap: 0.45rem; }
545
+ .chart-bar { display: flex; flex-direction: column; gap: 0; }
546
+ .chart-bar > .bar-row { padding: 0.22rem 0; }
547
+ /* (097) gridlines behind the bars and a value axis under them */
548
+ .bar-stack { position: relative; }
549
+ .bar-grid { position: absolute; inset: -0.22rem 0; pointer-events: none; }
550
+ .bar-gridline { position: absolute; top: 0; bottom: 0; border-left: 1px solid var(--chart-grid); }
551
+ .bar-gridline.baseline { border-left-color: var(--chart-axis); }
552
+ .bar-stack > .bar-track { position: relative; }
553
+ .bar-axis { display: grid; grid-template-columns: minmax(70px, 30%) 1fr auto; gap: 0.75rem; font-size: 0.72rem; color: var(--muted); }
554
+ .bar-axis > :last-child { min-width: 3rem; }
555
+ .bar-ticks { position: relative; height: 1.1rem; }
556
+ .bar-ticks > span { position: absolute; top: 0.15rem; transform: translateX(-50%); font-variant-numeric: tabular-nums; white-space: nowrap; }
557
+ .bar-ticks > span:first-child { transform: none; }
450
558
  .bar-row { display: grid; grid-template-columns: minmax(70px, 30%) 1fr auto; gap: 0.75rem; align-items: center; font-size: 0.88rem; }
451
559
  .bar-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--muted); }
452
560
  .bar-stack { display: flex; flex-direction: column; gap: 2px; }
@@ -620,6 +728,41 @@ a.drill > .polar-sector:hover { fill-opacity: 1; }
620
728
  .gantt-bar:hover, .gantt-bar:focus-visible, .gantt-milestone:hover, .gantt-milestone:focus-visible { filter: brightness(1.12); }
621
729
  .gantt-deps { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; pointer-events: none; z-index: 1; }
622
730
  .gantt-dep { fill: none; stroke: var(--muted); stroke-width: 1.25px; }
731
+ /* (sprint 70) treemap, sunburst and timeline */
732
+ .treemap { position: relative; aspect-ratio: 16 / 10; max-height: 26rem; width: 100%; font-size: 0.78rem; }
733
+ .tm-group, .tm-cell { position: absolute; box-sizing: border-box; overflow: hidden; }
734
+ .tm-group { border: 1px solid var(--surface); }
735
+ .tm-title { position: absolute; top: 0; left: 0; right: 0; padding: 0.1rem 0.35rem; font-weight: 650; color: var(--text); background: color-mix(in srgb, var(--c) 42%, var(--surface)); box-shadow: inset 0 -2px 0 var(--c); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; z-index: 1; }
736
+ /* tinted, with the page's text colour: contrast holds in light and dark */
737
+ .tm-cell { display: flex; flex-direction: column; gap: 0.1rem; padding: 0.2rem 0.35rem 0.2rem 0.5rem; border: 1px solid var(--surface); background: color-mix(in srgb, var(--c) 24%, var(--surface)); box-shadow: inset 3px 0 0 var(--c); color: var(--text); text-decoration: none; }
738
+ .tm-leaf { background: color-mix(in srgb, var(--c) 32%, var(--surface)); }
739
+ .tm-cell:hover, .tm-cell:focus-visible { filter: brightness(1.12); }
740
+ .tm-label { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
741
+ .tm-value { opacity: 0.9; font-variant-numeric: tabular-nums; }
742
+ .sb { fill: var(--c); stroke: var(--surface); stroke-width: 0.25; }
743
+ .sb.d2 { fill-opacity: 0.78; } .sb.d3 { fill-opacity: 0.58; } .sb.d4 { fill-opacity: 0.4; }
744
+ a.drill > .sb:hover { filter: brightness(1.12); }
745
+ .timeline { font-size: 0.8rem; }
746
+ .timeline-axis { position: relative; height: 1.5rem; border-bottom: 1px solid var(--chart-axis); font-size: 0.72rem; color: var(--muted); }
747
+ .timeline-axis span { position: absolute; bottom: 0.3rem; padding-left: 3px; white-space: nowrap; font-variant-numeric: tabular-nums; }
748
+ .timeline-axis span.skip { visibility: hidden; }
749
+ @media (max-width: 640px) { .timeline-axis span.odd { visibility: hidden; } }
750
+ .timeline-lane { border-bottom: 1px solid var(--chart-grid); padding-top: 0.25rem; }
751
+ .timeline-name { display: block; font-size: 0.75rem; font-weight: 650; color: var(--muted); }
752
+ .timeline-plot { position: relative; overflow: hidden; }
753
+ .tl-item, .tl-point { position: absolute; display: flex; align-items: center; height: 1.4rem; color: inherit; text-decoration: none; z-index: 2; outline-offset: 2px; }
754
+ .tl-item { min-width: 3px; border-radius: 4px; background: color-mix(in srgb, var(--c) 30%, var(--surface)); box-shadow: inset 3px 0 0 var(--c); }
755
+ .tl-item .tl-label { padding: 0 0.35rem 0 0.5rem; white-space: nowrap; }
756
+ .tl-point { gap: 0.3rem; margin-left: -0.35rem; white-space: nowrap; }
757
+ .tl-dot { width: 0.7rem; height: 0.7rem; flex: none; border-radius: 50%; background: var(--c); }
758
+ .tl-item:hover, .tl-item:focus-visible, .tl-point:hover .tl-dot, .tl-point:focus-visible .tl-dot { filter: brightness(1.12); }
759
+ .gantt-editable [data-task] { cursor: grab; touch-action: none; }
760
+ .gantt-editable .gantt-dragging { cursor: grabbing; z-index: 3; }
761
+ .gantt-editable .gantt-moving { box-shadow: 0 0 0 2px var(--accent); }
762
+ .gantt-handle { position: absolute; top: 0; right: 0; bottom: 0; width: 8px; cursor: ew-resize; background: color-mix(in srgb, var(--surface) 45%, transparent); }
763
+ .gantt-editable [aria-busy="true"], .gantt-editable[aria-busy="true"] { opacity: 0.7; }
764
+ .gantt-status { min-height: 1.3em; margin: 0.4rem 0 0; font-size: 0.85rem; font-variant-numeric: tabular-nums; }
765
+ .gantt-hint { margin: 0.2rem 0 0; font-size: 0.8rem; }
623
766
  .gantt-arrow { position: absolute; width: 0; height: 0; margin: -4px 0 0 -6px; border: 4px solid transparent; border-left: 6px solid var(--muted); border-right-width: 0; pointer-events: none; z-index: 1; }
624
767
 
625
768
  /* data table + tooltip */
@@ -787,7 +930,7 @@ pre.source { font-family: var(--mono); font-size: 0.82rem; background: var(--sur
787
930
  .card.metric { grid-template-columns: 1fr; row-gap: 0.25rem; }
788
931
  .metric-icon { grid-row: auto; width: 36px; height: 36px; }
789
932
  .metric-value { font-size: 1.35rem; }
790
- .bar-row { grid-template-columns: minmax(60px, 35%) 1fr auto; gap: 0.5rem; }
933
+ .bar-row, .bar-axis { grid-template-columns: minmax(60px, 35%) 1fr auto; gap: 0.5rem; }
791
934
  .title-row .buttons { width: 100%; }
792
935
  .title-row .buttons .btn { flex: 1 1 auto; }
793
936
 
@@ -807,7 +950,9 @@ pre.source { font-family: var(--mono); font-size: 0.82rem; background: var(--sur
807
950
  display: grid; grid-template-columns: minmax(90px, 38%) minmax(0, 1fr); gap: 0.75rem;
808
951
  border: 0; padding: 0.3rem 0.8rem; white-space: normal; max-width: none; text-align: left !important; overflow-wrap: anywhere;
809
952
  }
810
- .report-reflow td::before { content: attr(data-label); font-size: 0.78rem; font-weight: 650; color: var(--muted); text-transform: uppercase; letter-spacing: 0.02em; }
953
+ .report-reflow td::before { content: attr(data-label); font-size: 0.8rem; font-weight: 650; color: var(--muted); }
954
+ .report-reflow td.row-edit { width: auto; text-align: left; }
955
+ .report-reflow td.row-edit::before { content: none; }
811
956
  .report-reflow td.empty { display: block; }
812
957
  .report-reflow td.empty::before { content: none; }
813
958
  .report-reflow td.pre { white-space: pre-wrap; }
@@ -1609,6 +1754,15 @@ td.to-col-center, th.to-col-center { text-align: center; }
1609
1754
  .reporter-checks { display: flex; flex-wrap: wrap; gap: 0.3rem 1rem; }
1610
1755
  .reporter-chart { margin-bottom: 0.75rem; }
1611
1756
  .reporter-save { margin-top: 1rem; }
1757
+ .reporter-hello { margin-bottom: 0.5rem; }
1758
+ .reporter-hello p { margin: 0.2rem 0 0; }
1759
+ .reporter-cards { list-style: none; margin: 0 0 0.75rem; padding: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 14rem), 1fr)); gap: 0.6rem; }
1760
+ .reporter-card { display: flex; flex-direction: column; gap: 0.25rem; height: 100%; padding: 0.7rem 0.85rem; border: 1px solid var(--border); border-radius: 8px; background: var(--surface); color: inherit; text-decoration: none; }
1761
+ .reporter-card:hover, .reporter-card:focus-visible { border-color: var(--accent); }
1762
+ .reporter-card strong { color: var(--accent); }
1763
+ .reporter-card .muted { font-size: 0.85rem; }
1764
+ .reporter-card-meta { margin-top: auto; font-size: 0.78rem; color: var(--muted); }
1765
+ .reporter-search { max-width: 30rem; margin: 0.25rem 0 0; }
1612
1766
  @media (max-width: 600px) {
1613
1767
  .reporter-row > .label { flex-basis: 100%; }
1614
1768
  }
@@ -1645,19 +1799,51 @@ dialog.crop-dialog::backdrop { background: rgb(15 20 30 / 55%); }
1645
1799
  .crop-dialog .buttons { display: flex; justify-content: flex-end; gap: 0.5rem; }
1646
1800
 
1647
1801
  /* (079) Developer toolbar: shown to developers with a builder session in the same browser.
1648
- Fixed at the bottom left, collapsible (a <details>, so it works without JavaScript). */
1649
- .devbar { position: fixed; left: 0.75rem; bottom: 0.75rem; z-index: 900; max-width: calc(100vw - 1.5rem);
1650
- background: #1b2330; color: #fff; border-radius: var(--radius); box-shadow: 0 4px 16px rgb(0 0 0 / 25%); font-size: 0.8125rem; }
1651
- .devbar summary { cursor: pointer; padding: 0.4rem 0.75rem; font-weight: 600; list-style: none; }
1652
- .devbar summary::-webkit-details-marker { display: none; }
1653
- .devbar summary::before { content: '▸ '; }
1654
- .devbar[open] summary::before { content: '▾ '; }
1655
- .devbar ul { display: flex; flex-wrap: wrap; align-items: center; gap: 0.25rem 0.75rem; list-style: none; margin: 0; padding: 0 0.75rem 0.5rem; }
1656
- .devbar a { color: #9cc8ff; }
1657
- .devbar a:focus-visible, .devbar button:focus-visible, .devbar select:focus-visible { outline: 2px solid #9cc8ff; outline-offset: 2px; }
1658
- .devbar-debug { display: flex; align-items: center; gap: 0.35rem; margin: 0; }
1802
+ (sprint 72) A compact bar of icons centred at the bottom (APEX's runtime developer toolbar);
1803
+ app.js docks it at the top, left or right and shrinks it to a slim tab when it is not used.
1804
+ The page keeps room for it, so it never covers the menu's last entry or a report's pager. */
1805
+ .devbar { --devbar-size: 2.75rem; position: fixed; z-index: 900; left: 50%; bottom: 0.5rem; transform: translateX(-50%);
1806
+ background: #1b2330; color: #fff; border-radius: 10px; box-shadow: 0 4px 16px rgb(0 0 0 / 25%); font-size: 0.8125rem;
1807
+ transition: transform 0.2s ease, opacity 0.2s ease; }
1808
+ .devbar-tools { display: flex; align-items: center; gap: 0.15rem; list-style: none; margin: 0; padding: 0.25rem; }
1809
+ .devbar .devbar-tool { display: inline-grid; place-items: center; width: 2.25rem; height: 2.25rem; border-radius: 7px; color: #dbe7ff;
1810
+ background: none; border: 0; cursor: pointer; list-style: none; padding: 0; }
1811
+ .devbar-tool::-webkit-details-marker { display: none; }
1812
+ .devbar .devbar-tool:hover, .devbar-menu[open] > .devbar-tool, .devbar-quick[aria-pressed="true"] { background: #2f3b4f; color: #fff; }
1813
+ .devbar-debug-menu.is-on > .devbar-tool { color: #ffd166; }
1814
+ .devbar a, .devbar-pop a { color: #9cc8ff; }
1815
+ .devbar a:focus-visible, .devbar button:focus-visible, .devbar select:focus-visible, .devbar summary:focus-visible, .devbar input:focus-visible { outline: 2px solid #9cc8ff; outline-offset: 2px; }
1816
+ .devbar-menu { position: relative; }
1817
+ .devbar-pop { position: absolute; bottom: calc(100% + 0.6rem); left: 50%; transform: translateX(-50%); min-width: 14rem; max-width: min(22rem, calc(100vw - 2rem));
1818
+ background: #1b2330; border-radius: 8px; box-shadow: 0 4px 16px rgb(0 0 0 / 30%); padding: 0.6rem 0.75rem; display: grid; gap: 0.5rem; }
1819
+ .devbar-debug { display: flex; flex-wrap: wrap; align-items: center; gap: 0.35rem; margin: 0; }
1820
+ .devbar-debug label { flex-basis: 100%; }
1659
1821
  .devbar-debug select, .devbar-debug button { font: inherit; font-size: 0.8125rem; padding: 0.1rem 0.3rem; border-radius: 4px; border: 1px solid #5e6a7a; background: #2b3340; color: #fff; }
1660
1822
  .devbar-debug button { cursor: pointer; }
1823
+ .devbar-dock { border: 0; margin: 0; padding: 0; display: grid; grid-template-columns: 1fr 1fr; gap: 0.25rem 0.75rem; }
1824
+ .devbar-dock legend { padding: 0; margin-bottom: 0.25rem; font-weight: 600; }
1825
+ .devbar-pop .devbar-facts { color: #fff; }
1826
+ /* docked at the top, or on the left or right edge (a column of icons) */
1827
+ .devbar[data-dock="top"] { bottom: auto; top: 0.5rem; }
1828
+ .devbar[data-dock="top"] .devbar-pop { bottom: auto; top: calc(100% + 0.6rem); }
1829
+ .devbar[data-dock="left"], .devbar[data-dock="right"] { left: 0.5rem; bottom: auto; top: 50%; transform: translateY(-50%); }
1830
+ .devbar[data-dock="right"] { left: auto; right: 0.5rem; }
1831
+ .devbar[data-dock="left"] .devbar-tools, .devbar[data-dock="right"] .devbar-tools { flex-direction: column; }
1832
+ .devbar[data-dock="left"] .devbar-pop { bottom: auto; top: 0; left: calc(100% + 0.6rem); transform: none; }
1833
+ .devbar[data-dock="right"] .devbar-pop { bottom: auto; top: 0; left: auto; right: calc(100% + 0.6rem); transform: none; }
1834
+ /* not used for a while: a slim tab at the edge; pointing at it or focusing it brings it back */
1835
+ .devbar.is-idle { opacity: 0.85; }
1836
+ .devbar.is-idle[data-dock="bottom"] { transform: translate(-50%, calc(100% - 6px)); }
1837
+ .devbar.is-idle[data-dock="top"] { transform: translate(-50%, calc(-100% + 6px)); }
1838
+ .devbar.is-idle[data-dock="left"] { transform: translate(calc(-100% + 6px), -50%); }
1839
+ .devbar.is-idle[data-dock="right"] { transform: translate(calc(100% - 6px), -50%); }
1840
+ .devbar.is-idle .devbar-tools { pointer-events: none; }
1841
+ @media (prefers-reduced-motion: reduce) { .devbar { transition: none; } }
1842
+ /* room for the bar: the menu and the page end above it */
1843
+ body:has(.devbar[data-dock="bottom"]) .t-main, body:has(.devbar[data-dock="bottom"]) .t-nav > ul { padding-bottom: 3.75rem; }
1844
+ body:has(.devbar[data-dock="top"]) .t-body { padding-top: 3.25rem; }
1845
+ body:has(.devbar[data-dock="left"]) .t-body { padding-left: 3.25rem; }
1846
+ body:has(.devbar[data-dock="right"]) .t-main { padding-right: 3.5rem; }
1661
1847
  .devbar-page .t-main { max-width: 72rem; margin: 0 auto; padding: 1rem; }
1662
1848
  .devbar-facts { display: grid; grid-template-columns: max-content 1fr; gap: 0.25rem 1rem; }
1663
1849
  .devbar-facts dd { margin: 0; overflow-wrap: anywhere; }