pgkiln 0.31.0 → 0.36.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 (131) hide show
  1. package/README.md +2 -2
  2. package/SECURITY.md +10 -0
  3. package/db/migrations/076_form_lost_update.sql +50 -0
  4. package/db/migrations/077_warn_unsaved.sql +41 -0
  5. package/db/migrations/078_url_region.sql +12 -0
  6. package/db/migrations/079_developer_toolbar.sql +53 -0
  7. package/db/migrations/080_search.sql +326 -0
  8. package/db/migrations/081_mail.sql +621 -0
  9. package/db/migrations/082_app_backups.sql +73 -0
  10. package/db/migrations/083_availability.sql +156 -0
  11. package/db/migrations/084_feedback.sql +46 -0
  12. package/db/migrations/085_page_behaviour.sql +65 -0
  13. package/db/migrations/086_data_settings.sql +599 -0
  14. package/db/migrations/087_login_page.sql +175 -0
  15. package/db/migrations/088_region_templates.sql +18 -0
  16. package/db/migrations/089_page_groups.sql +330 -0
  17. package/docs/apex-feature-parity.md +61 -39
  18. package/docs/guide/01-installation.md +1 -0
  19. package/docs/guide/03-builder.md +154 -4
  20. package/docs/guide/04-pages-and-regions.md +157 -3
  21. package/docs/guide/05-items.md +47 -0
  22. package/docs/guide/06-processing.md +210 -2
  23. package/docs/guide/07-dynamic-actions.md +60 -2
  24. package/docs/guide/08-security.md +56 -4
  25. package/docs/guide/09-reference.md +21 -0
  26. package/docs/guide/11-from-apex.md +9 -2
  27. package/docs/guide/12-development.md +40 -2
  28. package/docs/guide/14-globalization.md +15 -0
  29. package/docs/guide/15-extensions.md +1 -1
  30. package/docs/guide/17-mobile.md +3 -1
  31. package/docs/guide/18-cli.md +32 -1
  32. package/docs/guide/20-ai-agents.md +5 -2
  33. package/examples/hr/hr_50_search.sql +47 -0
  34. package/examples/hr/hr_51_mail.sql +70 -0
  35. package/examples/hr/hr_52_feedback.sql +15 -0
  36. package/examples/hr/hr_53_data_settings.sql +108 -0
  37. package/examples/hr/hr_54_region_templates.sql +67 -0
  38. package/examples/hr/hr_55_page_groups.sql +24 -0
  39. package/package.json +3 -1
  40. package/public/app.css +122 -3
  41. package/public/app.js +496 -13
  42. package/public/builder.css +22 -0
  43. package/public/builder.js +77 -0
  44. package/public/code-editor.css +3 -3
  45. package/public/sw.js +5 -3
  46. package/src/app.ts +10 -0
  47. package/src/appfiles.ts +13 -0
  48. package/src/automations.ts +12 -0
  49. package/src/builder/advisor.ts +68 -14
  50. package/src/builder/arrange.ts +2 -2
  51. package/src/builder/availability.ts +74 -0
  52. package/src/builder/backups.ts +134 -0
  53. package/src/builder/components.ts +123 -16
  54. package/src/builder/ddl-tools.ts +114 -0
  55. package/src/builder/designer.ts +35 -4
  56. package/src/builder/feedback.ts +82 -0
  57. package/src/builder/forms.ts +10 -5
  58. package/src/builder/home.ts +3 -0
  59. package/src/builder/locks.ts +15 -5
  60. package/src/builder/login-page.ts +82 -0
  61. package/src/builder/mail.ts +425 -0
  62. package/src/builder/newapp.ts +1 -1
  63. package/src/builder/region-help.ts +73 -0
  64. package/src/builder/region-settings.ts +19 -3
  65. package/src/builder/routes.ts +60 -5
  66. package/src/builder/search.ts +4 -4
  67. package/src/builder/shared.ts +3 -2
  68. package/src/builder/sql.ts +3 -0
  69. package/src/builder/ui.ts +9 -3
  70. package/src/cli/apps.ts +46 -5
  71. package/src/cli/main.ts +66 -5
  72. package/src/cli/mcp.ts +58 -4
  73. package/src/cli/replace.ts +40 -4
  74. package/src/ddl.ts +509 -0
  75. package/src/geocode.ts +44 -0
  76. package/src/i18n/ar.ts +56 -0
  77. package/src/i18n/cs.ts +56 -0
  78. package/src/i18n/da.ts +56 -0
  79. package/src/i18n/de.ts +56 -0
  80. package/src/i18n/el.ts +56 -0
  81. package/src/i18n/es.ts +56 -0
  82. package/src/i18n/fi.ts +56 -0
  83. package/src/i18n/fr.ts +56 -0
  84. package/src/i18n/he.ts +56 -0
  85. package/src/i18n/it.ts +56 -0
  86. package/src/i18n/ja.ts +56 -0
  87. package/src/i18n/ko.ts +56 -0
  88. package/src/i18n/nb.ts +56 -0
  89. package/src/i18n/pl.ts +56 -0
  90. package/src/i18n/pt.ts +56 -0
  91. package/src/i18n/ru.ts +56 -0
  92. package/src/i18n/sv.ts +56 -0
  93. package/src/i18n/tr.ts +56 -0
  94. package/src/i18n/uk.ts +56 -0
  95. package/src/i18n/zh.ts +56 -0
  96. package/src/i18n.ts +112 -0
  97. package/src/icons.ts +2 -1
  98. package/src/layout.ts +3 -2
  99. package/src/mail/mail.ts +148 -0
  100. package/src/mail/providers.ts +444 -0
  101. package/src/metadata.ts +34 -10
  102. package/src/runtime/ajax.ts +97 -0
  103. package/src/runtime/authz.ts +1 -1
  104. package/src/runtime/availability.ts +37 -0
  105. package/src/runtime/context.ts +46 -6
  106. package/src/runtime/data-export.ts +94 -0
  107. package/src/runtime/data-items.ts +194 -0
  108. package/src/runtime/devbar.ts +184 -0
  109. package/src/runtime/display-selector.ts +12 -4
  110. package/src/runtime/engine.ts +26 -1
  111. package/src/runtime/feedback.ts +128 -0
  112. package/src/runtime/grid.ts +170 -26
  113. package/src/runtime/items.ts +8 -0
  114. package/src/runtime/lists.ts +11 -1
  115. package/src/runtime/password-reset.ts +171 -0
  116. package/src/runtime/processes.ts +83 -2
  117. package/src/runtime/pwa.ts +7 -10
  118. package/src/runtime/regions.ts +41 -0
  119. package/src/runtime/render.ts +48 -7
  120. package/src/runtime/report-views.ts +4 -2
  121. package/src/runtime/report.ts +36 -22
  122. package/src/runtime/rest.ts +4 -0
  123. package/src/runtime/routes.ts +94 -37
  124. package/src/runtime/search.ts +252 -0
  125. package/src/runtime/signin.ts +80 -0
  126. package/src/runtime/url-region.ts +93 -0
  127. package/src/security.ts +11 -1
  128. package/src/server.ts +2 -0
  129. package/src/session.ts +8 -2
  130. package/src/static-version.ts +34 -0
  131. package/src/workingcopy.ts +2 -2
@@ -0,0 +1,108 @@
1
+ -- =====================================================================
2
+ -- HR example, part 53: data and settings (migration 086;
3
+ -- docs/guide/06-processing.md "Application settings", "User preferences",
4
+ -- "Data export"; docs/guide/14-globalization.md "Dynamic translations";
5
+ -- docs/guide/05-items.md "Display image", "Geocoded address")
6
+ --
7
+ -- Application settings SUPPORT_EMAIL and LEAVE_APPROVAL (Shared
8
+ -- Components → Application settings), read with meta.get_app_setting.
9
+ -- Dynamic translations of the department names in Dutch and German:
10
+ -- meta.lang(dname). Page 43 "Data and settings" (Administration):
11
+ -- - P43_DEPTNO, a department (names through meta.lang), kept per user
12
+ -- across sessions ({"persist": "user"});
13
+ -- - P43_EMPNO, an employee of that department, also kept per user, and
14
+ -- P43_PHOTO, the employee's photo from hr.emp.photo (display image);
15
+ -- - P43_ADDRESS, a geocoded address whose position goes into P43_LOCATION
16
+ -- (the "Check address" button shows where the server has GEOCODER_URL);
17
+ -- - Excel and CSV downloads of the department's employees (the download
18
+ -- process with {"export": …}, APEX_DATA_EXPORT).
19
+ -- =====================================================================
20
+
21
+ insert into meta.app_setting (app_id, name, value, description, required, valid_values)
22
+ select a.id, s.name, s.value, s.description, s.required, s.valid_values
23
+ from meta.app a, (values
24
+ ('SUPPORT_EMAIL', 'hr-support@example.com', 'Where users write with questions; shown on page 43.', true, null::text[]),
25
+ ('LEAVE_APPROVAL', 'manager', 'Who approves leave requests: the manager, or the HR department.', true, '{manager,hr}'::text[])
26
+ ) as s (name, value, description, required, valid_values)
27
+ where a.alias = 'hr';
28
+
29
+ insert into meta.dynamic_translation (app_id, language, source, target)
30
+ select a.id, t.language, t.source, t.target
31
+ from meta.app a, (values
32
+ ('nl', 'ACCOUNTING', 'BOEKHOUDING'), ('nl', 'RESEARCH', 'ONDERZOEK'), ('nl', 'SALES', 'VERKOOP'), ('nl', 'OPERATIONS', 'BEDRIJFSVOERING'),
33
+ ('de', 'ACCOUNTING', 'BUCHHALTUNG'), ('de', 'RESEARCH', 'FORSCHUNG'), ('de', 'SALES', 'VERTRIEB'), ('de', 'OPERATIONS', 'BETRIEB')
34
+ ) as t (language, source, target)
35
+ where a.alias = 'hr';
36
+
37
+ insert into meta.page (app_id, page_no, name, title, parent_page)
38
+ select id, 43, 'Data and settings', 'Data and settings', 1 from meta.app where alias = 'hr';
39
+
40
+ insert into meta.region (page_id, seq, title, type, columns, template)
41
+ select p.id, r.seq, r.title, 'static', 12, 'standard'
42
+ from meta.page p join meta.app a on a.id = p.app_id, (values (10, 'Employee'), (20, 'Visit address')) as r (seq, title)
43
+ where a.alias = 'hr' and p.page_no = 43;
44
+
45
+ insert into meta.item (page_id, region_id, seq, name, label, type, lov, help, config)
46
+ select p.id, (select r.id from meta.region r where r.page_id = p.id and r.title = i.region), i.seq, i.name, i.label, i.type, i.lov, i.help, i.config::jsonb
47
+ from meta.page p join meta.app a on a.id = p.app_id, (values
48
+ ('Employee', 10, 'P43_DEPTNO', 'Department', 'select',
49
+ 'select meta.lang(dname), deptno from hr.dept order by 1', 'Remembered for you, also after you sign out.',
50
+ '{"persist": "user", "submit_on_change": true, "null_label": "- All -"}'),
51
+ ('Employee', 20, 'P43_EMPNO', 'Employee', 'select',
52
+ 'select initcap(ename), empno from hr.emp where :P43_DEPTNO is null or deptno = :P43_DEPTNO::int order by 1', null,
53
+ '{"persist": "user", "submit_on_change": true, "null_label": "- Choose -"}'),
54
+ ('Employee', 30, 'P43_PHOTO', 'Photo', 'image', null, null,
55
+ '{"sql": "select photo, photo_mime from hr.emp where empno = :P43_EMPNO::int", "alt": "Photo of the employee", "width": 160}'),
56
+ ('Employee', 40, 'P43_SUPPORT', 'Questions?', 'display', null, 'The application setting SUPPORT_EMAIL.', '{}'),
57
+ ('Visit address', 50, 'P43_ADDRESS', 'Address', 'address', null, 'Type the address and choose it from the list.',
58
+ '{"coordinates_item": "P43_LOCATION"}'),
59
+ ('Visit address', 60, 'P43_LOCATION', 'Position', 'location', null, null, '{}')
60
+ ) as i (region, seq, name, label, type, lov, help, config)
61
+ where a.alias = 'hr' and p.page_no = 43;
62
+
63
+ insert into meta.button (page_id, region_id, seq, name, label, action)
64
+ select p.id, (select r.id from meta.region r where r.page_id = p.id and r.title = 'Employee'), b.seq, b.name, b.label, 'submit'
65
+ from meta.page p join meta.app a on a.id = p.app_id, (values (10, 'EXPORT_XLSX', 'Download Excel'), (20, 'EXPORT_CSV', 'Download CSV')) as b (seq, name, label)
66
+ where a.alias = 'hr' and p.page_no = 43;
67
+
68
+ insert into meta.process (page_id, seq, name, type, point, code, when_button, config)
69
+ select p.id, x.seq, x.name, x.type, x.point, x.code, x.button, x.config::jsonb
70
+ from meta.page p join meta.app a on a.id = p.app_id, (values
71
+ (10, 'Support address', 'sql', 'load', 'select meta.get_app_setting(''SUPPORT_EMAIL'') as p43_support', null, '{}'),
72
+ (20, 'Employees as Excel', 'download', 'submit',
73
+ 'select e.empno, initcap(e.ename) as name, initcap(e.job) as job, e.hiredate, e.sal as salary, meta.lang(d.dname) as department
74
+ from hr.emp e left join hr.dept d on d.deptno = e.deptno
75
+ where :P43_DEPTNO is null or e.deptno = :P43_DEPTNO::int
76
+ order by e.ename', 'EXPORT_XLSX', '{"export": "xlsx", "file_name": "employees", "title": "Employees"}'),
77
+ (30, 'Employees as CSV', 'download', 'submit',
78
+ 'select e.empno, initcap(e.ename) as name, initcap(e.job) as job, e.hiredate, e.sal as salary, meta.lang(d.dname) as department
79
+ from hr.emp e left join hr.dept d on d.deptno = e.deptno
80
+ where :P43_DEPTNO is null or e.deptno = :P43_DEPTNO::int
81
+ order by e.ename', 'EXPORT_CSV', '{"export": "csv", "file_name": "employees"}')
82
+ ) as x (seq, name, type, point, code, button, config)
83
+ where a.alias = 'hr' and p.page_no = 43;
84
+
85
+ insert into meta.nav_entry (app_id, parent_id, seq, label, icon, target_page)
86
+ select a.id, n.id, 40, 'Data and settings', 'settings', 43
87
+ from meta.app a join meta.nav_entry n on n.app_id = a.id and n.label = 'Administration' and n.parent_id is null
88
+ where a.alias = 'hr';
89
+
90
+ insert into meta.translation (app_id, language, source, target)
91
+ select a.id, 'nl', t.source, t.target
92
+ from meta.app a, (values
93
+ ('Data and settings', 'Gegevens en instellingen'),
94
+ ('Employee', 'Medewerker'),
95
+ ('Visit address', 'Bezoekadres'),
96
+ ('Department', 'Afdeling'),
97
+ ('Photo', 'Foto'),
98
+ ('Questions?', 'Vragen?'),
99
+ ('Address', 'Adres'),
100
+ ('Position', 'Positie'),
101
+ ('Download Excel', 'Excel downloaden'),
102
+ ('Download CSV', 'CSV downloaden'),
103
+ ('Remembered for you, also after you sign out.', 'Wordt voor u onthouden, ook na afmelden.'),
104
+ ('The application setting SUPPORT_EMAIL.', 'De applicatie-instelling SUPPORT_EMAIL.'),
105
+ ('Type the address and choose it from the list.', 'Typ het adres en kies het uit de lijst.')
106
+ ) as t (source, target)
107
+ where a.alias = 'hr'
108
+ on conflict do nothing;
@@ -0,0 +1,67 @@
1
+ -- =====================================================================
2
+ -- HR example, part 54: region templates (migration 088;
3
+ -- docs/guide/04-pages-and-regions.md "Region templates")
4
+ --
5
+ -- Page 44 "Region templates" (Administration):
6
+ -- - a hero region (an icon, the title, text and a button);
7
+ -- - a title bar with buttons; a buttons container;
8
+ -- - "Leave policy" opens an inline dialog and "Help" an inline drawer
9
+ -- (dynamic actions "Open region"; a link to #R<id> opens them too);
10
+ -- - a carousel (a display selector with style carousel) over three
11
+ -- regions, turning every 8 seconds;
12
+ -- - the steps of onboarding as a wizard progress list above a wizard
13
+ -- region.
14
+ -- =====================================================================
15
+
16
+ insert into meta.page (app_id, page_no, name, title, parent_page)
17
+ select id, 44, 'Region templates', 'Region templates', 1 from meta.app where alias = 'hr';
18
+
19
+ insert into meta.list (app_id, name, type, description)
20
+ select id, 'HR_ONBOARDING_STEPS', 'static', 'The steps of onboarding (a wizard progress list on page 44)' from meta.app where alias = 'hr';
21
+
22
+ insert into meta.list_entry (app_id, list_name, seq, label, target_page)
23
+ select a.id, 'HR_ONBOARDING_STEPS', e.seq, e.label, e.page
24
+ from meta.app a, (values (10, 'Personal details', 44), (20, 'Contract', 3), (30, 'Equipment', 28), (40, 'Done', 1)) as e (seq, label, page)
25
+ where a.alias = 'hr';
26
+
27
+ insert into meta.region (page_id, seq, title, type, columns, template, source, config)
28
+ select p.id, r.seq, r.title, r.type, 12, r.template, r.source, r.config::jsonb
29
+ from meta.page p join meta.app a on a.id = p.app_id, (values
30
+ (10, 'Welcome to HR', 'static', 'hero',
31
+ '<p>Everything about the people in the company: employees, departments, leave and reviews.</p>', '{"icon": "users"}'),
32
+ (20, 'Employees this month', 'static', 'title_bar', '<p class="muted">Hires and departures of the current month.</p>', '{}'),
33
+ (30, 'Actions', 'static', 'buttons_container', null, '{}'),
34
+ (40, 'Leave policy', 'static', 'inline_dialog',
35
+ '<p>Everyone has 25 days of leave a year. Ask your manager at least two weeks ahead; approvals come by e-mail.</p>', '{}'),
36
+ (50, 'Help', 'static', 'inline_drawer',
37
+ '<p>This page shows the region templates: hero, title bar, buttons container, inline dialog and drawer, carousel and wizard.</p>', '{}'),
38
+ (60, 'Highlights', 'display_selector', 'plain', null, '{"style": "carousel", "interval": 8, "show_all": false}'),
39
+ (61, 'New colleagues', 'static', 'standard', '<p>Three people joined the Research department this month.</p>', '{"display_selector": true}'),
40
+ (62, 'Anniversaries', 'static', 'standard', '<p>KING celebrates 45 years with the company.</p>', '{"display_selector": true}'),
41
+ (63, 'Open positions', 'static', 'standard', '<p>Two analysts and a clerk: see the intranet.</p>', '{"display_selector": true}'),
42
+ (70, 'Onboarding steps', 'list', 'plain', null, '{"list": "HR_ONBOARDING_STEPS", "template": "wizard_progress"}'),
43
+ (80, 'Personal details', 'static', 'wizard', '<p>Step 1: check your name, address and bank account in My account.</p>', '{}')
44
+ ) as r (seq, title, type, template, source, config)
45
+ where a.alias = 'hr' and p.page_no = 44;
46
+
47
+ insert into meta.button (page_id, region_id, seq, name, label, action, hot, target_page)
48
+ select p.id, (select r.id from meta.region r where r.page_id = p.id and r.title = b.region), b.seq, b.name, b.label, b.action, b.hot, b.target
49
+ from meta.page p join meta.app a on a.id = p.app_id, (values
50
+ ('Welcome to HR', 10, 'EMPLOYEES', 'See the employees', 'redirect', true, 2),
51
+ ('Employees this month', 20, 'POLICY', 'Leave policy', 'da', false, null),
52
+ ('Actions', 30, 'HELP', 'Help', 'da', false, null)
53
+ ) as b (region, seq, name, label, action, hot, target)
54
+ where a.alias = 'hr' and p.page_no = 44;
55
+
56
+ insert into meta.dynamic_action (page_id, seq, name, event, trigger_element, action, affected_region_id)
57
+ select p.id, d.seq, d.name, 'click', d.button, 'open_region', (select r.id from meta.region r where r.page_id = p.id and r.title = d.region)
58
+ from meta.page p join meta.app a on a.id = p.app_id, (values
59
+ (10, 'Open the leave policy', 'POLICY', 'Leave policy'),
60
+ (20, 'Open the help', 'HELP', 'Help')
61
+ ) as d (seq, name, button, region)
62
+ where a.alias = 'hr' and p.page_no = 44;
63
+
64
+ insert into meta.nav_entry (app_id, parent_id, seq, label, icon, target_page)
65
+ select a.id, n.id, 50, 'Region templates', 'layers', 44
66
+ from meta.app a join meta.nav_entry n on n.app_id = a.id and n.label = 'Administration' and n.parent_id is null
67
+ where a.alias = 'hr';
@@ -0,0 +1,24 @@
1
+ -- =====================================================================
2
+ -- HR example, part 55: page groups (migration 089;
3
+ -- docs/guide/03-builder.md "Page groups")
4
+ --
5
+ -- The HR pages in four groups, shown and filtered in the App Builder's
6
+ -- page list (Shared Components → Page groups).
7
+ -- =====================================================================
8
+ insert into meta.page_group (app_id, name, description)
9
+ select a.id, g.name, g.description
10
+ from meta.app a, (values
11
+ ('Employees', 'Employees, departments and their reviews'),
12
+ ('Leave', 'Leave requests, the calendar and the planner'),
13
+ ('Administration', 'Pages for administrators'),
14
+ ('Showcase', 'Pages that show one feature each')
15
+ ) as g (name, description)
16
+ where a.alias = 'hr';
17
+
18
+ update meta.page p set page_group = g.name
19
+ from meta.app a, (values
20
+ ('Employees', array[2, 3, 4, 5, 8, 11, 19, 20, 27]),
21
+ ('Leave', array[6, 7, 12, 22, 24]),
22
+ ('Administration', array[9, 10, 43, 44])
23
+ ) as g (name, pages)
24
+ where a.alias = 'hr' and p.app_id = a.id and p.page_no = any (g.pages);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pgkiln",
3
- "version": "0.31.0",
3
+ "version": "0.36.0",
4
4
  "description": "Open source low-code, SQL-driven application builder for PostgreSQL, inspired by Oracle APEX",
5
5
  "keywords": [
6
6
  "postgresql",
@@ -73,6 +73,7 @@
73
73
  "ldapts": "^9.2.0",
74
74
  "leaflet": "^1.9.4",
75
75
  "lucide-static": "^1.52.0",
76
+ "nodemailer": "^10.0.12",
76
77
  "openai": "^7.28.0",
77
78
  "pdfkit": "^0.20.2",
78
79
  "pg": "^8.23.0",
@@ -81,6 +82,7 @@
81
82
  },
82
83
  "devDependencies": {
83
84
  "@types/node": "^26.6.3",
85
+ "@types/nodemailer": "^8.0.2",
84
86
  "@types/pdfkit": "^0.17.6",
85
87
  "@types/pg": "^8.23.1",
86
88
  "axe-core": "^4.14.0",
package/public/app.css CHANGED
@@ -254,7 +254,43 @@ details.menu > summary::-webkit-details-marker { display: none; }
254
254
 
255
255
  /* ---------- regions ---------- */
256
256
  .region { min-width: 0; }
257
- .region-standard, .region-collapsible { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); box-shadow: var(--shadow); }
257
+ .region-standard, .region-collapsible, .region-wizard, .region-inline_dialog, .region-inline_drawer { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); box-shadow: var(--shadow); }
258
+ /* (088) more region templates */
259
+ .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
+ background: var(--accent-soft); color: var(--text); }
261
+ .region-hero-icon .icon { width: 3rem; height: 3rem; color: var(--accent); }
262
+ .region-hero-text h2 { font-size: 1.75rem; margin: 0 0 0.35rem; }
263
+ .region-hero-buttons, .region-title-buttons, .region-buttons { display: flex; flex-wrap: wrap; gap: 0.5rem; }
264
+ @media (max-width: 640px) { .region-hero { grid-template-columns: 1fr; padding: 1.25rem 1rem; } }
265
+ .region-title-bar { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 0.5rem 1rem; padding: 0.25rem 0; border-bottom: 1px solid var(--border); margin-bottom: 0.75rem; }
266
+ .region-title-bar h2 { margin: 0; font-size: 1.15rem; }
267
+ .region-buttons_container .region-buttons { justify-content: flex-end; }
268
+ .region-wizard { max-width: 640px; width: 100%; margin-left: auto; margin-right: auto; }
269
+ dialog[data-inline-region] { padding: 0; max-width: min(640px, calc(100vw - 2rem)); width: 100%; color: var(--text); }
270
+ dialog[data-inline-region]::backdrop { background: rgb(0 0 0 / 45%); }
271
+ dialog[data-inline-region] .region-header { display: flex; align-items: center; justify-content: space-between; gap: 1rem; }
272
+ dialog.region-inline_drawer { margin: 0 0 0 auto; height: 100vh; max-height: 100vh; max-width: min(480px, 100vw); border-radius: 0; }
273
+ /* without JavaScript: a link to #R<id> shows it */
274
+ dialog[data-inline-region]:target { display: block; position: fixed; inset: 2rem 0 auto; margin: 0 auto; z-index: 1000; }
275
+ dialog.region-inline_drawer:target { inset: 0 0 0 auto; }
276
+ /* (088) carousel (display selector style) and wizard progress (list template) */
277
+ .rds-carousel { display: flex; align-items: center; justify-content: center; gap: 0.75rem; }
278
+ .rds-carousel .rds-list { gap: 0.4rem; }
279
+ .rds-carousel .rds-tab { font-size: 0; width: 0.75rem; height: 0.75rem; padding: 0; border-radius: 50%; background: var(--border); min-height: 0; }
280
+ .rds-carousel .rds-tab.is-current { background: var(--accent); }
281
+ .rds-carousel .rds-tab:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
282
+ .wizard-progress ol { list-style: none; margin: 0 0 1rem; padding: 0; display: flex; flex-wrap: wrap; gap: 0.5rem 0; counter-reset: none; }
283
+ .wizard-step { flex: 1 1 0; min-width: 6rem; display: flex; flex-direction: column; align-items: center; text-align: center; gap: 0.3rem; position: relative; color: var(--muted); font-size: 0.85rem; }
284
+ .wizard-step::before { content: ""; position: absolute; top: 0.95rem; left: -50%; right: 50%; border-top: 2px solid var(--border); z-index: 0; }
285
+ .wizard-step:first-child::before { display: none; }
286
+ .wizard-step a { color: inherit; text-decoration: none; display: contents; }
287
+ .wizard-step-no { position: relative; z-index: 1; display: grid; place-items: center; width: 1.9rem; height: 1.9rem; border-radius: 50%; border: 2px solid var(--border); background: var(--surface); font-weight: 600; }
288
+ .wizard-step-no .icon { width: 1rem; height: 1rem; }
289
+ .wizard-step.is-done, .wizard-step.is-current { color: var(--text); }
290
+ .wizard-step.is-done .wizard-step-no { border-color: var(--accent); color: var(--accent); }
291
+ .wizard-step.is-done::before, .wizard-step.is-current::before { border-top-color: var(--accent); }
292
+ .wizard-step.is-current .wizard-step-no { background: var(--accent); border-color: var(--accent); color: var(--accent-text, #fff); }
293
+ .wizard-step.is-current .wizard-step-label { font-weight: 600; }
258
294
  .region-header { display: flex; justify-content: space-between; align-items: center; gap: 1rem; min-height: 50px; padding: 0.6rem 1rem; border-bottom: 1px solid var(--border); }
259
295
  .region-collapsible summary.region-header { cursor: pointer; list-style: none; }
260
296
  .region-collapsible summary.region-header::-webkit-details-marker { display: none; }
@@ -265,6 +301,9 @@ details.menu > summary::-webkit-details-marker { display: none; }
265
301
  .region-toolbar { display: flex; justify-content: flex-end; margin-bottom: 0.75rem; }
266
302
  .region-static p:first-child { margin-top: 0; }
267
303
  .region-static p:last-child { margin-bottom: 0; }
304
+ /* (078) URL region: the framed page fills the region's width; its height is a class per region (PageCss) */
305
+ .url-frame { display: block; width: 100%; border: 1px solid var(--border, #d0d7de); border-radius: 6px; background: var(--surface, #fff); }
306
+ .url-open { margin: 0.5rem 0 0; font-size: 0.875rem; }
268
307
  .columns { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 420px), 1fr)); gap: 1rem; margin-top: 1rem; }
269
308
  .columns .region { margin: 0; }
270
309
 
@@ -807,6 +846,14 @@ fieldset.prop-group > legend { font-size: 0.75rem; text-transform: uppercase; le
807
846
  .grid-table tr.deleted td { opacity: 0.5; text-decoration: line-through; }
808
847
  .grid-table tr.grid-new { background: color-mix(in srgb, var(--accent-soft) 45%, transparent); }
809
848
  .grid-table tr.dirty td:first-child { box-shadow: inset 3px 0 0 var(--accent); }
849
+ /* the single row view: one row as a form */
850
+ .grid-record { padding: 0.9rem 1rem; border: 1px solid var(--border); border-radius: var(--radius, 8px); display: grid; gap: 0.9rem; }
851
+ .grid-record .grid-field { display: grid; gap: 0.3rem; align-content: start; }
852
+ .grid-record .grid-value { min-height: 1.5rem; overflow-wrap: anywhere; }
853
+ .grid-record.grid-new { background: color-mix(in srgb, var(--accent-soft) 45%, transparent); }
854
+ .grid-record.dirty { box-shadow: inset 3px 0 0 var(--accent); }
855
+ .grid-record.deleted .form-grid { opacity: 0.5; text-decoration: line-through; }
856
+ .grid-record-nav { justify-content: center; }
810
857
  .js .grid-template { display: none; }
811
858
  .grid-table .icon { width: 14px; height: 14px; }
812
859
  /* columns a user hid stay in the form (their values still post) */
@@ -1003,9 +1050,19 @@ input[type=color] { width: 4rem; min-height: 2.4rem; padding: 0.15rem; }
1003
1050
  /* single sign-on buttons on the login page */
1004
1051
  .sso-buttons { display: flex; flex-direction: column; gap: 0.5rem; }
1005
1052
  .sso-buttons .btn { padding: 0.6rem; }
1006
- .login-card .or { display: flex; align-items: center; gap: 0.75rem; color: var(--muted); font-size: 0.85rem; }
1007
- .login-card .or::before, .login-card .or::after { content: ""; flex: 1; border-top: 1px solid var(--border); }
1053
+ .login-card .or, .login-region .or { display: flex; align-items: center; gap: 0.75rem; color: var(--muted); font-size: 0.85rem; }
1054
+ .login-card .or::before, .login-card .or::after, .login-region .or::before, .login-region .or::after { content: ""; flex: 1; border-top: 1px solid var(--border); }
1008
1055
  .login-form { display: flex; flex-direction: column; gap: 1rem; }
1056
+ .login-forgot { align-self: center; font-size: 0.9rem; }
1057
+ /* (087) minimal pages (no header and navigation) and an application's own login page */
1058
+ .t-minimal-main { width: 100%; max-width: 1100px; margin: 0 auto; padding: 1.5rem 1rem; }
1059
+ .login-body .t-minimal-main { max-width: 420px; padding: 1rem; }
1060
+ .login-region { display: flex; flex-direction: column; gap: 1rem; }
1061
+ .login-region .btn { padding: 0.6rem; }
1062
+ .t-minimal .t-titlebar { background: none; border: 0; padding: 0 0 1rem; }
1063
+ .t-minimal .t-content { padding: 0; max-width: none; }
1064
+ /* on the sign-in backdrop the title sits on the header colour, like the built-in page's card title */
1065
+ .login-body .t-titlebar h1 { color: var(--header-text); text-align: center; }
1009
1066
 
1010
1067
  /* ---------- preferences: theme switch, language picker ---------- */
1011
1068
  .segmented { display: inline-flex; border: 1px solid var(--border); border-radius: 999px; overflow: hidden; margin-top: 0.35rem; }
@@ -1416,6 +1473,12 @@ td.tc-cell { overflow-wrap: anywhere; }
1416
1473
 
1417
1474
  .qr { display: flex; flex-direction: column; align-items: flex-start; gap: 0.3rem; padding: 0.3rem 0; }
1418
1475
  .qr-code { max-width: 100%; height: auto; border-radius: 4px; }
1476
+ /* (086) display image and address items */
1477
+ .display-image { padding: 0.3rem 0; }
1478
+ .display-image img { max-width: 100%; height: auto; border-radius: 4px; }
1479
+ .geocode-places { list-style: none; margin: 0.3rem 0 0; padding: 0.3rem 0; border: 1px solid var(--border); border-radius: 4px; background: var(--surface); }
1480
+ .geocode-places li { padding: 0.15rem 0.6rem; }
1481
+ .geocode-places .link-button { text-align: start; }
1419
1482
 
1420
1483
  /* ---------- lists (list regions, navigation bar) ---------- */
1421
1484
  .list-links { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.1rem; }
@@ -1550,3 +1613,59 @@ dialog.crop-dialog::backdrop { background: rgb(15 20 30 / 55%); }
1550
1613
  .crop-handle { position: absolute; right: -8px; bottom: -8px; width: 16px; height: 16px; border-radius: 50%; background: #fff; border: 2px solid var(--accent); cursor: nwse-resize; }
1551
1614
  .crop-stage { overflow: hidden; }
1552
1615
  .crop-dialog .buttons { display: flex; justify-content: flex-end; gap: 0.5rem; }
1616
+
1617
+ /* (079) Developer toolbar: shown to developers with a builder session in the same browser.
1618
+ Fixed at the bottom left, collapsible (a <details>, so it works without JavaScript). */
1619
+ .devbar { position: fixed; left: 0.75rem; bottom: 0.75rem; z-index: 900; max-width: calc(100vw - 1.5rem);
1620
+ background: #1b2330; color: #fff; border-radius: var(--radius); box-shadow: 0 4px 16px rgb(0 0 0 / 25%); font-size: 0.8125rem; }
1621
+ .devbar summary { cursor: pointer; padding: 0.4rem 0.75rem; font-weight: 600; list-style: none; }
1622
+ .devbar summary::-webkit-details-marker { display: none; }
1623
+ .devbar summary::before { content: '▸ '; }
1624
+ .devbar[open] summary::before { content: '▾ '; }
1625
+ .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; }
1626
+ .devbar a { color: #9cc8ff; }
1627
+ .devbar a:focus-visible, .devbar button:focus-visible, .devbar select:focus-visible { outline: 2px solid #9cc8ff; outline-offset: 2px; }
1628
+ .devbar-debug { display: flex; align-items: center; gap: 0.35rem; margin: 0; }
1629
+ .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; }
1630
+ .devbar-debug button { cursor: pointer; }
1631
+ .devbar-page .t-main { max-width: 72rem; margin: 0 auto; padding: 1rem; }
1632
+ .devbar-facts { display: grid; grid-template-columns: max-content 1fr; gap: 0.25rem 1rem; }
1633
+ .devbar-facts dd { margin: 0; overflow-wrap: anywhere; }
1634
+ .devbar-session code { overflow-wrap: anywhere; }
1635
+ @media print { .devbar { display: none; } }
1636
+
1637
+ /* (080) Search region: one field, results grouped per search configuration */
1638
+ .search-form { margin-bottom: 1rem; }
1639
+ .search-group + .search-group { margin-top: 1rem; }
1640
+ .search-group h3 { font-size: 0.875rem; text-transform: uppercase; letter-spacing: 0.04em; color: var(--muted); margin: 0 0 0.4rem; }
1641
+ .search-hits { list-style: none; margin: 0; padding: 0; border: 1px solid var(--border); border-radius: var(--radius); }
1642
+ .search-hits li + li { border-top: 1px solid var(--border); }
1643
+ .search-hit { display: flex; align-items: flex-start; gap: 0.75rem; padding: 0.6rem 0.8rem; color: inherit; text-decoration: none; }
1644
+ a.search-hit:hover, a.search-hit:focus-visible { background: var(--accent-soft); }
1645
+ .search-icon { flex: none; color: var(--accent); }
1646
+ .search-text { display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; flex: 1; overflow-wrap: anywhere; }
1647
+ .search-title { font-weight: 600; }
1648
+ .search-subtitle, .search-description { color: var(--muted); font-size: 0.875rem; }
1649
+ .search-hit mark { background: #fff3a3; color: inherit; border-radius: 2px; padding: 0 1px; }
1650
+ :root[data-theme="dark"] .search-hit mark { background: #6b5a00; }
1651
+ @media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) .search-hit mark { background: #6b5a00; } }
1652
+ .search-hit .badge-pill { flex: none; }
1653
+ .search-more, .search-empty { margin: 0.4rem 0 0; }
1654
+
1655
+ /* (084) Feedback: the users' page and the builder's list */
1656
+ .feedback-form .choices { display: flex; flex-wrap: wrap; gap: 0.5rem 1rem; }
1657
+ .feedback-list, .feedback-admin { list-style: none; margin: 0; padding: 0; }
1658
+ .feedback-list > li, .feedback-admin > li { border-top: 1px solid var(--border); padding: 0.75rem 0; }
1659
+ .feedback-list > li:first-child, .feedback-admin > li:first-child { border-top: 0; }
1660
+ .feedback-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 0.35rem; overflow-wrap: anywhere; }
1661
+ .feedback-body { white-space: pre-wrap; overflow-wrap: anywhere; margin: 0.4rem 0; }
1662
+ .feedback-response { background: var(--surface-2); border-radius: var(--radius); padding: 0.5rem 0.75rem; }
1663
+ .feedback-response p { white-space: pre-wrap; overflow-wrap: anywhere; margin: 0.25rem 0 0; }
1664
+
1665
+ /* (085) Session timeout warning */
1666
+ .session-dialog { max-width: min(28rem, calc(100vw - 2rem)); border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); color: var(--text); padding: 1.25rem; }
1667
+ .session-dialog::backdrop { background: rgb(0 0 0 / 40%); }
1668
+ .session-dialog p { margin: 0 0 1rem; }
1669
+ /* (sprint 59) Quick edit from the developer toolbar */
1670
+ .quick-edit .qe-target { outline: 2px dashed var(--accent); outline-offset: 2px; cursor: pointer; }
1671
+ .devbar-quick[aria-pressed="true"] { background: var(--accent); color: var(--accent-text); }