pgkiln 0.41.0 → 0.43.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 (68) hide show
  1. package/db/migrations/097_generated_app_quality.sql +911 -0
  2. package/db/migrations/098_app_activity.sql +37 -0
  3. package/db/migrations/099_layout_positions.sql +29 -0
  4. package/docs/apex-feature-parity.md +38 -11
  5. package/docs/guide/03-builder.md +133 -31
  6. package/docs/guide/04-pages-and-regions.md +46 -3
  7. package/docs/guide/05-items.md +1 -1
  8. package/docs/guide/09-reference.md +10 -3
  9. package/docs/guide/11-from-apex.md +1 -1
  10. package/docs/guide/12-development.md +3 -2
  11. package/docs/guide/14-globalization.md +7 -3
  12. package/docs/guide/17-mobile.md +1 -1
  13. package/package.json +1 -1
  14. package/public/app.css +233 -33
  15. package/public/app.js +263 -0
  16. package/public/builder.css +142 -2
  17. package/public/builder.js +311 -3
  18. package/src/builder/appfromfile.ts +5 -4
  19. package/src/builder/appsheets.ts +23 -9
  20. package/src/builder/appwizard.ts +215 -70
  21. package/src/builder/arrange.ts +133 -17
  22. package/src/builder/blueprints.ts +2 -1
  23. package/src/builder/components.ts +29 -23
  24. package/src/builder/designer.ts +67 -18
  25. package/src/builder/forms.ts +4 -1
  26. package/src/builder/home.ts +2 -32
  27. package/src/builder/newapp.ts +182 -5
  28. package/src/builder/object-browser.ts +342 -0
  29. package/src/builder/pwa.ts +3 -3
  30. package/src/builder/routes.ts +86 -51
  31. package/src/builder/shared.ts +103 -1
  32. package/src/builder/sql.ts +13 -65
  33. package/src/builder/ui.ts +21 -6
  34. package/src/i18n/ar.ts +17 -0
  35. package/src/i18n/cs.ts +17 -0
  36. package/src/i18n/da.ts +17 -0
  37. package/src/i18n/de.ts +17 -0
  38. package/src/i18n/el.ts +17 -0
  39. package/src/i18n/es.ts +17 -0
  40. package/src/i18n/fi.ts +17 -0
  41. package/src/i18n/fr.ts +17 -0
  42. package/src/i18n/he.ts +17 -0
  43. package/src/i18n/it.ts +17 -0
  44. package/src/i18n/ja.ts +17 -0
  45. package/src/i18n/ko.ts +17 -0
  46. package/src/i18n/nb.ts +17 -0
  47. package/src/i18n/pl.ts +17 -0
  48. package/src/i18n/pt.ts +17 -0
  49. package/src/i18n/ru.ts +17 -0
  50. package/src/i18n/sv.ts +17 -0
  51. package/src/i18n/tr.ts +17 -0
  52. package/src/i18n/uk.ts +17 -0
  53. package/src/i18n/zh.ts +17 -0
  54. package/src/i18n.ts +34 -0
  55. package/src/icons.ts +2 -1
  56. package/src/layout.ts +2 -2
  57. package/src/metadata.ts +5 -1
  58. package/src/runtime/charts.ts +8 -3
  59. package/src/runtime/devbar.ts +35 -14
  60. package/src/runtime/engine.ts +13 -4
  61. package/src/runtime/format.ts +52 -0
  62. package/src/runtime/items.ts +27 -0
  63. package/src/runtime/locale.ts +5 -2
  64. package/src/runtime/regions.ts +61 -4
  65. package/src/runtime/render.ts +38 -8
  66. package/src/runtime/report.ts +149 -88
  67. package/src/runtime/routes.ts +34 -3
  68. package/src/runtime/styles.ts +5 -2
@@ -11,7 +11,7 @@ page, optionally inside a region. Name them `P<page>_<NAME>` (uppercase letters,
11
11
  | `text` | text field | text |
12
12
  | `textarea` | multi-line text (always full width) | text |
13
13
  | `number` | number field (numeric keyboard on phones) | e.g. `1250.50` |
14
- | `date` | date picker | `YYYY-MM-DD` |
14
+ | `date` | date picker: the browser's, or (sprint 76) with an application date format or a date format mask on the item a text field in that format with a calendar button; typed dates are read back in the format | `YYYY-MM-DD` |
15
15
  | `datetime` | date-time picker | `YYYY-MM-DDTHH:MM` |
16
16
  | `email`, `tel`, `url` | typed fields with the matching phone keyboard | text |
17
17
  | `password` | password field; never shown back, empty = unchanged | text |
@@ -274,11 +274,15 @@ Run these as the owner (in the SQL Workshop, `psql` or migrations):
274
274
 
275
275
  | Function | Description |
276
276
  |---|---|
277
- | `meta.generate_crud(app, table, report_page, form_page, label default null, icon default 'table')` | Report page + modal form page + menu entry for a table |
277
+ | `meta.generate_crud(app, table, report_page, form_page, label default null, icon default 'table')` | Report page + modal form page + menu entry for a table, with the column names as labels (kept as it was for existing scripts; `meta.generate_page` *report_form* makes the labelled pages of the wizards) |
278
278
  | `meta.generate_grid(app, table, page, label default null, icon default 'grid')` | Interactive grid page + menu entry |
279
279
  | `meta.generate_page(app, kind, table, page, options default '{}')` | The create page wizards: `kind` is `form`, `cards`, `calendar`, `chart`, `map`, `facets`, `master_detail` (or `report_form`, `grid`). Options left out take the defaults of `meta.wizard_defaults`; JSON `null` means none. Returns the new page's id (below) |
280
280
  | `meta.wizard_defaults(kind, table)` | The options a wizard proposes for a table, from the catalog |
281
281
  | `meta.wizard_catalog(table)` | The columns as the wizards see them: kind (`text`, `number`, `date`, `timestamp`, `boolean`, `point`, `geometry`, `binary`, `other`), key, unique, foreign key and the parent's display column |
282
+ | `meta.wizard_label(name)`, `meta.wizard_table_label(table, plural default true)`, `meta.wizard_column_label(table, column)` | (097) The labels of the wizards: `hiredate` → *Hire Date*, `hr.emp` → *Employees* / *Employee*, `deptno` → *Department*; short comments win ([what the generated pages look like](03-builder.md#what-the-generated-pages-look-like)) |
283
+ | `meta.wizard_report(table, columns default null)` | (097) A report on a table as the wizards make it: `{"source", "hidden", "headings", "formats"}` (foreign keys joined to their parent's display column) |
284
+ | `meta.app_activity(days default 30)` | (098) The current application's page views, sign-ins and errors of the last days (1 to 366, at most 10,000 rows, no IP addresses), for a signed-in user with the `admin` role of that application; others get an error. The Activity page of the Create Application wizard reads it |
285
+ | `meta.wizard_files(table)` | (097) The `bytea` columns and the columns holding their file name, mime type, size and change time |
282
286
  | `meta.export_app(alias)` | The application as JSON (`pgkiln/2` format) |
283
287
  | `meta.import_app(json, alias default null)` | Import an export, optionally under a new alias; returns the new app id |
284
288
  | `meta.hash_password(text)` | A bcrypt hash for `meta.app_user.password_hash` / `meta.developer.password_hash` |
@@ -293,7 +297,7 @@ Options of `meta.generate_page` (all optional; column names must be columns of t
293
297
 
294
298
  | Kind | Options |
295
299
  |---|---|
296
- | all | `label` (page name and menu entry), `icon`, `nav` (add a navigation entry; default true, false for `form`) |
300
+ | all | `label` (page name and menu entry; default the table's plural label, singular for `form`), `form_label` (the title of a form page; default the singular label), `icon`, `nav` (add a navigation entry; default true, false for `form`) |
297
301
  | `form` | `columns` (array), `mode` (`normal` or `modal`), `return_page` |
298
302
  | `cards` | `title`, `subtitle`, `body`, `badge`, `form_page` |
299
303
  | `calendar` | `start`, `end`, `title`, `drag` (boolean), `form_page` |
@@ -419,6 +423,7 @@ navigation entries and application processes have the same `build_option` column
419
423
  | `config` | Attributes per type ([chapter 4](04-pages-and-regions.md)) |
420
424
  | `rest_source` | A REST data source the region reads; `source` is then SQL over `rest` ([chapter 19](19-rest-data-sources.md)) |
421
425
  | `template_options` | text[]: CSS classes from a fixed list ([template options](04-pages-and-regions.md#template-options)) |
426
+ | `position` | `banner`, `breadcrumb`, `left`, `body` (or null), `right`, `footer` ([display positions](04-pages-and-regions.md#side-columns-and-display-positions)) |
422
427
 
423
428
  **`item`**: `page_id`, `region_id`, `seq`, `name`, `label`, `type`, `lov`, `source_column`,
424
429
  `default_value`, `required`, `help`, `readonly_condition`, `authz`, `config` ([chapter 5](05-items.md)).
@@ -426,7 +431,9 @@ navigation entries and application processes have the same `build_option` column
426
431
  **`button`**: `page_id`, `region_id`, `seq`, `name`, `label`, `action` (`submit` / `redirect` / `da` /
427
432
  `document` / `menu`), `target_page`, `target_items` (jsonb), `condition`, `authz`, `hot`, `confirm`,
428
433
  `menu` (jsonb, for `menu`), `badge`, `badge_query` ([chapter 6](06-processing.md)), `template_options`
429
- (text[], [template options](04-pages-and-regions.md#template-options)).
434
+ (text[], [template options](04-pages-and-regions.md#template-options)), `slot` (a region button's
435
+ [button position](04-pages-and-regions.md#button-positions): `create`, `edit`, `change`, `copy`, `help`, `up`,
436
+ `previous`, `close`, `delete`, `next` or `region` / null).
430
437
 
431
438
  **`dynamic_action`**: `page_id`, `seq`, `name`, `event`, `trigger_element`, `condition_type`,
432
439
  `condition_value`, `action`, `affected_items`, `affected_region_id`, `code`, `items_to_submit`,
@@ -62,7 +62,7 @@ What's missing is listed in the [feature parity matrix](../apex-feature-parity.m
62
62
  | Application date format mask | Settings → Globalization → Date format (Oracle-style masks) |
63
63
  | Number format masks (`FML999G999G990D00`) on columns and items | `{"formats": {...}}` on report, grid and cards columns, `{"format_mask": "..."}` on charts and number/display items ([chapter 14](14-globalization.md#number-formats)) |
64
64
  | Automatic Time Zone | Settings → Globalization → Time zone and Automatic time zone; My account → Time zone ([chapter 14](14-globalization.md#time-zones)) |
65
- | Universal Theme's default style (26.1: *Iris*) | Base style *Iris* (the default for new applications) or *Standard* ([chapter 14](14-globalization.md#base-styles-iris-and-standard)) |
65
+ | Universal Theme's default style (26.1: *Iris*) | Base style *Iris* (the default for new applications), *Lavender* or *Standard* ([chapter 14](14-globalization.md#base-styles-iris-lavender-and-standard)) |
66
66
  | Theme styles, *Enable End Users to Choose Theme Style* | Theme style (automatic/light/dark) and *Users may choose light or dark*; Theme Roller style variants with *Users may choose a style* ([chapter 14](14-globalization.md#style-variants-theme-roller)) |
67
67
  | Template Options (regions, buttons) | *Template options* in the Page Designer: CSS classes from a fixed list ([chapter 4](04-pages-and-regions.md#template-options)) |
68
68
  | ORDS | Not needed to serve apps; REST APIs with PostgREST, see [below](#ords-and-postgrest) |
@@ -116,7 +116,7 @@ src/
116
116
  processes.ts download (file or zip from a query, safe headers), workflow processes, configuration checks of chains
117
117
  logic.ts computations, branches (page, URL, function returning a URL, another application) and their conditions
118
118
  render.ts page chrome (nav, breadcrumb), dynamic action JSON, theme (the page's nonce'd <style>, light/dark and style switches)
119
- styles.ts base styles (BASE_STYLES: Iris, Standard; baseStyleOf → html data-style), Theme Roller style variants: fixed lists (fonts, sizes, corners), parseStyle/appStyles checks, the request's
119
+ styles.ts base styles (BASE_STYLES: Iris, Lavender, Standard; baseStyleOf → html data-style), Theme Roller style variants: fixed lists (fonts, sizes, corners), parseStyle/appStyles checks, the request's
120
120
  style (user choice, default), themeCss() (only hex values and constants reach the CSS)
121
121
  template-options.ts template options: the fixed CSS class list per region and button, templateClasses() (unknown values ignored)
122
122
  regions.ts region shell + chart (drill-down links, gauge settings)/cards/dynamic dispatch with row limits, lazy placeholder and cache, buttons (menu buttons, badges)
@@ -210,7 +210,8 @@ src/
210
210
  shared.ts Shared Components and access control
211
211
  designer.ts page designer: component tree (with computations and branches), layout canvas and gallery, property editor, toolbar
212
212
  arrange.ts page designer layout changes: move, column span, create from the gallery, undo / redo
213
- sql.ts SQL Workshop: SQL commands, object browser
213
+ sql.ts SQL Workshop: the home and SQL commands (templates and drafts to review)
214
+ object-browser.ts (sprint 75) the Object Browser: objects by type, system objects hidden, a table in tabs, column actions as SQL to review
214
215
  scripts.ts SQL Workshop → SQL Scripts: editor, upload/download, run, results per statement, run history
215
216
  quicksql.ts SQL Workshop → Quick SQL page (preview, save as script, run)
216
217
  querybuilder.ts SQL Workshop → Query Builder: catalog, joins by foreign key or drawn, functions/group by, buildQuery() from the URL; the canvas is in builder.js
@@ -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 |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pgkiln",
3
- "version": "0.41.0",
3
+ "version": "0.43.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; }
@@ -822,7 +930,7 @@ pre.source { font-family: var(--mono); font-size: 0.82rem; background: var(--sur
822
930
  .card.metric { grid-template-columns: 1fr; row-gap: 0.25rem; }
823
931
  .metric-icon { grid-row: auto; width: 36px; height: 36px; }
824
932
  .metric-value { font-size: 1.35rem; }
825
- .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; }
826
934
  .title-row .buttons { width: 100%; }
827
935
  .title-row .buttons .btn { flex: 1 1 auto; }
828
936
 
@@ -842,7 +950,9 @@ pre.source { font-family: var(--mono); font-size: 0.82rem; background: var(--sur
842
950
  display: grid; grid-template-columns: minmax(90px, 38%) minmax(0, 1fr); gap: 0.75rem;
843
951
  border: 0; padding: 0.3rem 0.8rem; white-space: normal; max-width: none; text-align: left !important; overflow-wrap: anywhere;
844
952
  }
845
- .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; }
846
956
  .report-reflow td.empty { display: block; }
847
957
  .report-reflow td.empty::before { content: none; }
848
958
  .report-reflow td.pre { white-space: pre-wrap; }
@@ -1689,19 +1799,51 @@ dialog.crop-dialog::backdrop { background: rgb(15 20 30 / 55%); }
1689
1799
  .crop-dialog .buttons { display: flex; justify-content: flex-end; gap: 0.5rem; }
1690
1800
 
1691
1801
  /* (079) Developer toolbar: shown to developers with a builder session in the same browser.
1692
- Fixed at the bottom left, collapsible (a <details>, so it works without JavaScript). */
1693
- .devbar { position: fixed; left: 0.75rem; bottom: 0.75rem; z-index: 900; max-width: calc(100vw - 1.5rem);
1694
- background: #1b2330; color: #fff; border-radius: var(--radius); box-shadow: 0 4px 16px rgb(0 0 0 / 25%); font-size: 0.8125rem; }
1695
- .devbar summary { cursor: pointer; padding: 0.4rem 0.75rem; font-weight: 600; list-style: none; }
1696
- .devbar summary::-webkit-details-marker { display: none; }
1697
- .devbar summary::before { content: '▸ '; }
1698
- .devbar[open] summary::before { content: '▾ '; }
1699
- .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; }
1700
- .devbar a { color: #9cc8ff; }
1701
- .devbar a:focus-visible, .devbar button:focus-visible, .devbar select:focus-visible { outline: 2px solid #9cc8ff; outline-offset: 2px; }
1702
- .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%; }
1703
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; }
1704
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; }
1705
1847
  .devbar-page .t-main { max-width: 72rem; margin: 0 auto; padding: 1rem; }
1706
1848
  .devbar-facts { display: grid; grid-template-columns: max-content 1fr; gap: 0.25rem 1rem; }
1707
1849
  .devbar-facts dd { margin: 0; overflow-wrap: anywhere; }
@@ -1743,3 +1885,61 @@ a.search-hit:hover, a.search-hit:focus-visible { background: var(--accent-soft);
1743
1885
  /* (sprint 59) Quick edit from the developer toolbar */
1744
1886
  .quick-edit .qe-target { outline: 2px dashed var(--accent); outline-offset: 2px; cursor: pointer; }
1745
1887
  .devbar-quick[aria-pressed="true"] { background: var(--accent); color: var(--accent-text); }
1888
+
1889
+ /* (sprint 76) the pager: rows per page; a Go button only without JavaScript */
1890
+ .pager-size { display: inline-flex; align-items: center; gap: 0.4rem; margin-left: auto; font-size: 0.85rem; }
1891
+ .pager-size select { min-height: 0; padding: 0.15rem 0.4rem; }
1892
+ .js .no-js-only { display: none; }
1893
+
1894
+ /* (sprint 76) the interactive report's Actions menu: one line per entry, submenus, forms in dialogs */
1895
+ .actions-menu .menu-panel { min-width: 15rem; padding: 0.3rem 0; }
1896
+ .ir-menu { list-style: none; margin: 0; padding: 0; }
1897
+ .ir-item > summary, .ir-link { display: flex; align-items: center; gap: 0.55rem; width: 100%; padding: 0.45rem 0.9rem; cursor: pointer; list-style: none; color: var(--text); text-decoration: none; background: none; border: 0; font: inherit; text-align: left; }
1898
+ .ir-item > summary::-webkit-details-marker { display: none; }
1899
+ .ir-item > summary:hover, .ir-link:hover, .ir-item[open] > summary { background: var(--surface-3); }
1900
+ .ir-item > summary .icon, .ir-link .icon { color: var(--muted); }
1901
+ .ir-more { margin-left: auto; color: var(--muted); }
1902
+ .ir-sub > .ir-menu { padding-left: 1.25rem; }
1903
+ .ir-body { padding: 0.5rem 0.9rem 0.75rem; display: grid; gap: 0.5rem; }
1904
+ .ir-body .menu-section { padding: 0; border: 0; }
1905
+ .ir-columns { border: 0; margin: 0; padding: 0; display: grid; gap: 0.2rem; max-height: 50vh; overflow: auto; }
1906
+ .ir-columns legend { font-weight: 600; margin-bottom: 0.3rem; }
1907
+ .ir-dialog { border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); color: var(--text); padding: 0; width: min(34rem, calc(100vw - 2rem)); box-shadow: 0 12px 32px rgb(0 0 0 / 25%); }
1908
+ .ir-dialog::backdrop { background: rgb(0 0 0 / 35%); }
1909
+ .ir-dialog-head { display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding: 0.75rem 1rem; border-bottom: 1px solid var(--border); }
1910
+ .ir-dialog-head h2 { margin: 0; font-size: 1.05rem; }
1911
+ .ir-dialog .ir-body { padding: 1rem; }
1912
+ .ir-dialog .filter-row { min-width: 0; }
1913
+ /* the search field with its column list (APEX: the magnifier's column menu) */
1914
+ .search-field { display: flex; flex: 1; min-width: 0; border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); }
1915
+ .search-field:focus-within { outline: 2px solid var(--accent); outline-offset: 1px; }
1916
+ .search-field .search-scope { border: 0; border-right: 1px solid var(--border); border-radius: var(--radius) 0 0 var(--radius); max-width: 9rem; background: var(--surface-2); }
1917
+ .search-field input[type=search] { border: 0; flex: 1; min-width: 0; background: transparent; }
1918
+ .search-field input[type=search]:focus { outline: none; }
1919
+ /* a column heading's menu */
1920
+ .th-menu { z-index: 60; width: 17rem; padding: 0.4rem 0; display: grid; gap: 0.3rem; max-height: 70vh; overflow: auto; }
1921
+ .th-menu-actions { display: grid; }
1922
+ .th-menu-actions a, .th-menu-values a { display: flex; justify-content: space-between; gap: 0.5rem; padding: 0.35rem 0.9rem; color: var(--text); text-decoration: none; }
1923
+ .th-menu-actions a:hover, .th-menu-values a:hover, .th-menu a:focus-visible { background: var(--surface-3); }
1924
+ .th-menu input[type=search] { margin: 0.2rem 0.75rem; width: calc(100% - 1.5rem); }
1925
+ .th-menu-values { list-style: none; margin: 0; padding: 0; border-top: 1px solid var(--border); overflow-wrap: anywhere; }
1926
+ .th-menu-values li.muted { padding: 0.35rem 0.9rem; }
1927
+ .js th[data-col] > a { cursor: pointer; }
1928
+
1929
+ /* (sprint 76) a date item in the application's date format: the browser's picker on a hidden date field */
1930
+ .date-field { position: relative; }
1931
+ .date-native { position: absolute; right: 0; bottom: 0; width: 1px; height: 1px; opacity: 0; pointer-events: none; border: 0; padding: 0; }
1932
+
1933
+ /* (sprint 77) display positions: banner, breadcrumb bar, a side column, footer; button positions in a region */
1934
+ .t-pos-banner, .t-pos-breadcrumb { margin-bottom: 1rem; }
1935
+ .t-pos-breadcrumb { padding: 0.5rem 0; border-bottom: 1px solid var(--border); }
1936
+ .t-pos-footer { margin-top: 1rem; padding-top: 1rem; border-top: 1px solid var(--border); }
1937
+ .t-columns { display: grid; gap: 1.25rem; align-items: start; }
1938
+ .t-columns-left { grid-template-columns: minmax(14rem, 18rem) minmax(0, 1fr); }
1939
+ .t-columns-right { grid-template-columns: minmax(0, 1fr) minmax(14rem, 18rem); }
1940
+ .t-columns-right > .t-side { order: 2; }
1941
+ .t-side .t-regions, .t-side { min-width: 0; }
1942
+ .t-side .region { grid-column: 1 / -1; }
1943
+ @media (max-width: 900px) { .t-columns-left, .t-columns-right { grid-template-columns: 1fr; } .t-columns-right > .t-side { order: 0; } }
1944
+ .region-footer-split { display: flex; justify-content: space-between; gap: 0.5rem; flex-wrap: wrap; }
1945
+ .region-footer-split > div { display: flex; gap: 0.5rem; }