pgkiln 0.31.0 → 0.35.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.
- package/README.md +2 -2
- package/SECURITY.md +10 -0
- package/db/migrations/076_form_lost_update.sql +50 -0
- package/db/migrations/077_warn_unsaved.sql +41 -0
- package/db/migrations/078_url_region.sql +12 -0
- package/db/migrations/079_developer_toolbar.sql +53 -0
- package/db/migrations/080_search.sql +326 -0
- package/db/migrations/081_mail.sql +621 -0
- package/db/migrations/082_app_backups.sql +73 -0
- package/db/migrations/083_availability.sql +156 -0
- package/db/migrations/084_feedback.sql +46 -0
- package/db/migrations/085_page_behaviour.sql +65 -0
- package/db/migrations/086_data_settings.sql +599 -0
- package/docs/apex-feature-parity.md +55 -35
- package/docs/guide/01-installation.md +1 -0
- package/docs/guide/03-builder.md +118 -2
- package/docs/guide/04-pages-and-regions.md +115 -0
- package/docs/guide/05-items.md +47 -0
- package/docs/guide/06-processing.md +210 -2
- package/docs/guide/07-dynamic-actions.md +60 -2
- package/docs/guide/08-security.md +13 -2
- package/docs/guide/09-reference.md +21 -0
- package/docs/guide/11-from-apex.md +5 -1
- package/docs/guide/12-development.md +33 -1
- package/docs/guide/14-globalization.md +15 -0
- package/docs/guide/15-extensions.md +1 -1
- package/docs/guide/18-cli.md +30 -1
- package/docs/guide/20-ai-agents.md +3 -2
- package/examples/hr/hr_50_search.sql +47 -0
- package/examples/hr/hr_51_mail.sql +70 -0
- package/examples/hr/hr_52_feedback.sql +15 -0
- package/examples/hr/hr_53_data_settings.sql +108 -0
- package/package.json +3 -1
- package/public/app.css +62 -0
- package/public/app.js +408 -11
- package/public/builder.css +11 -0
- package/public/builder.js +27 -0
- package/src/app.ts +8 -0
- package/src/appfiles.ts +12 -0
- package/src/automations.ts +12 -0
- package/src/builder/advisor.ts +54 -14
- package/src/builder/arrange.ts +2 -2
- package/src/builder/availability.ts +74 -0
- package/src/builder/backups.ts +134 -0
- package/src/builder/components.ts +109 -15
- package/src/builder/designer.ts +8 -3
- package/src/builder/feedback.ts +82 -0
- package/src/builder/forms.ts +4 -1
- package/src/builder/home.ts +3 -0
- package/src/builder/locks.ts +15 -5
- package/src/builder/mail.ts +425 -0
- package/src/builder/newapp.ts +1 -1
- package/src/builder/region-help.ts +71 -0
- package/src/builder/routes.ts +44 -1
- package/src/builder/search.ts +4 -4
- package/src/builder/shared.ts +3 -2
- package/src/builder/ui.ts +7 -1
- package/src/cli/apps.ts +46 -5
- package/src/cli/main.ts +21 -5
- package/src/cli/mcp.ts +16 -4
- package/src/cli/replace.ts +36 -4
- package/src/geocode.ts +44 -0
- package/src/i18n/ar.ts +39 -0
- package/src/i18n/cs.ts +39 -0
- package/src/i18n/da.ts +39 -0
- package/src/i18n/de.ts +39 -0
- package/src/i18n/el.ts +39 -0
- package/src/i18n/es.ts +39 -0
- package/src/i18n/fi.ts +39 -0
- package/src/i18n/fr.ts +39 -0
- package/src/i18n/he.ts +39 -0
- package/src/i18n/it.ts +39 -0
- package/src/i18n/ja.ts +39 -0
- package/src/i18n/ko.ts +39 -0
- package/src/i18n/nb.ts +39 -0
- package/src/i18n/pl.ts +39 -0
- package/src/i18n/pt.ts +39 -0
- package/src/i18n/ru.ts +39 -0
- package/src/i18n/sv.ts +39 -0
- package/src/i18n/tr.ts +39 -0
- package/src/i18n/uk.ts +39 -0
- package/src/i18n/zh.ts +39 -0
- package/src/i18n.ts +78 -0
- package/src/mail/mail.ts +148 -0
- package/src/mail/providers.ts +444 -0
- package/src/metadata.ts +26 -8
- package/src/runtime/ajax.ts +97 -0
- package/src/runtime/authz.ts +1 -1
- package/src/runtime/availability.ts +37 -0
- package/src/runtime/context.ts +46 -6
- package/src/runtime/data-export.ts +94 -0
- package/src/runtime/data-items.ts +194 -0
- package/src/runtime/devbar.ts +182 -0
- package/src/runtime/engine.ts +26 -1
- package/src/runtime/feedback.ts +128 -0
- package/src/runtime/items.ts +8 -0
- package/src/runtime/processes.ts +83 -2
- package/src/runtime/regions.ts +8 -0
- package/src/runtime/render.ts +33 -7
- package/src/runtime/rest.ts +4 -0
- package/src/runtime/routes.ts +59 -5
- package/src/runtime/search.ts +252 -0
- package/src/runtime/url-region.ts +93 -0
- package/src/security.ts +11 -1
- package/src/server.ts +2 -0
- package/src/session.ts +8 -2
- package/src/workingcopy.ts +2 -2
|
@@ -68,6 +68,7 @@ src/
|
|
|
68
68
|
css.ts PageCss: data-dependent styles as classes in the page's nonce'd <style> (CSP)
|
|
69
69
|
metadata.ts types + loaders for apps and pages (components of excluded build options are left out here)
|
|
70
70
|
maptiles.ts map tile server URL, attribution and CSP origin
|
|
71
|
+
geocode.ts the installation's geocoder for address items (GEOCODER_URL, Nominatim search API)
|
|
71
72
|
mvt.ts Mapbox Vector Tile encoding for map layers served as tiles
|
|
72
73
|
yamltext.ts the text style of the directory export: a strict YAML subset, written and read
|
|
73
74
|
webclient.ts outgoing HTTP to web services: allow-list, address checks at connect time (SSRF), redirects, limits
|
|
@@ -80,6 +81,11 @@ src/
|
|
|
80
81
|
webrequests.ts web requests from SQL (meta.web_request, migration 052): runPending() after each sql page process
|
|
81
82
|
(same transaction), webRequestTick() for committed ones (automations scheduler), retention purge;
|
|
82
83
|
calls go through websources.ts call()/invoke() (allow-list, SSRF checks, credentials)
|
|
84
|
+
mail/ e-mail (migration 081): providers.ts (one deliver() per provider: SMTP through nodemailer with a password
|
|
85
|
+
or XOAUTH2, Microsoft Graph sendMail, Amazon SES v2 raw MIME with signV4, Resend, Postmark, SendGrid,
|
|
86
|
+
Mailgun, log; MailError says whether a retry may help; client-credentials token cache; settingsProblems),
|
|
87
|
+
mail.ts (sendWith(): service enabled, allowed sender, secret decrypted; mailTick(): claim with SKIP LOCKED,
|
|
88
|
+
send, retry after 1/4/16/64 minutes, purge after 30 days; the NOTIFY pgkiln_mail listener)
|
|
83
89
|
ai/ AI services (migration 060): types.ts (the provider interface, AiError kinds),
|
|
84
90
|
anthropic.ts (Claude through @anthropic-ai/sdk: streamed, effort, structured outputs via
|
|
85
91
|
output_config.format, refusal fallbacks, stop_reason checks, typed SDK errors), openai.ts (the openai
|
|
@@ -142,6 +148,21 @@ src/
|
|
|
142
148
|
session, formatAnswer (escaped), send/clear routes
|
|
143
149
|
ai-filter.ts natural-language filters on a report ("ai_filter"): columns → structured output → checked → r<id>_f/q/s/d
|
|
144
150
|
pdf.ts report PDFs with report layouts (pdfkit); rows from a cursor in batches (tablePdf takes batches)
|
|
151
|
+
search.ts search region and search configurations (migration 080): words as ILIKE parameters, table/sql/list
|
|
152
|
+
sources as the app role in savepoints, marked results, configColumns/searchConfigProblems for the Advisor
|
|
153
|
+
url-region.ts URL region (migration 078): literal origins only (urlTarget), &ITEM. URL-encoded, sandbox and height,
|
|
154
|
+
frameOrigins() for the CSP's frame-src (security.ts)
|
|
155
|
+
ajax.ts Ajax callbacks (migration 085: POST …/ajax/:name, point "ajax" processes, x01 … x10 binds) and the
|
|
156
|
+
download dynamic action (POST …/da/:id/download)
|
|
157
|
+
availability.ts availabilityRefusal(): who may use an application (migration 083); loadContext, the login page
|
|
158
|
+
and REST modules ask it (PostgREST: meta.api_check)
|
|
159
|
+
feedback.ts users' feedback page and the navigation bar entry (migration 084)
|
|
160
|
+
data-items.ts display image and address items (migration 086): imageUrl (https origins into the CSP's img-src),
|
|
161
|
+
GET …/image/:item (a BLOB from the item's query), POST …/geocode/:item, and items kept per user
|
|
162
|
+
(loadPreferences / savePreferences through meta.get_preference / set_preference)
|
|
163
|
+
data-export.ts the download process's {"export": …}: meta.data_export for csv/json/xml/html, Excel and PDF here
|
|
164
|
+
devbar.ts developer toolbar and session state viewer (migration 079): the pgkiln_devbar token of a builder
|
|
165
|
+
session (ensureDevbar/devbarOf), the toolbar markup, the viewer, clear and per-session debug routes
|
|
145
166
|
builder/
|
|
146
167
|
components.ts property spec of every component (drives the property editor)
|
|
147
168
|
ui.ts IDE shell (icon rail, toolbar, breadcrumb, status bar), builder theme, form helpers, CSRF check, app tabs
|
|
@@ -158,6 +179,14 @@ src/
|
|
|
158
179
|
appwizard.ts Create → From pasted data (kept as a temp file, then the From a file steps) and From existing tables
|
|
159
180
|
(a schema's tables/views → report+form or report pages, navigation, dashboard)
|
|
160
181
|
forms.ts generic component property form (lookups, render, save)
|
|
182
|
+
backups.ts Application → Backups (migration 082): list, back up now, download, restore (replaceApp), keep N;
|
|
183
|
+
backupTick() for the scheduler's daily backups of changed applications
|
|
184
|
+
availability.ts Application → Availability (migration 083): status, message, users, build status (Run only is
|
|
185
|
+
enforced by locks.ts blockingLock); availabilityBadge() in the application header
|
|
186
|
+
feedback.ts Application → Feedback (migration 084): the setting, the list with filters, status and answers
|
|
187
|
+
region-help.ts the Attributes (JSON) help per region type, with links to the guide
|
|
188
|
+
mail.ts Workspace utilities → Mail services (administrators: providers, write-only secret, allowed senders,
|
|
189
|
+
applications with From and daily limit, test) and Applications → Activity → Mail (retry, cancel)
|
|
161
190
|
shared.ts Shared Components and access control
|
|
162
191
|
designer.ts page designer: component tree (with computations and branches), layout canvas and gallery, property editor, toolbar
|
|
163
192
|
arrange.ts page designer layout changes: move, column span, create from the gallery, undo / redo
|
|
@@ -377,5 +406,8 @@ browser enhancements at the end of `public/app.js` (the item must work without t
|
|
|
377
406
|
1. Update `CHANGELOG.md` and the version in `package.json`.
|
|
378
407
|
2. Merge to `main`; CI must be green (every job). The `main` branch should be protected on
|
|
379
408
|
GitHub (Settings → Branches → rule for `main`: require the CI checks).
|
|
380
|
-
3. Tag: `git tag -a vX.Y.Z -m vX.Y.Z && git push origin vX.Y.Z`.
|
|
409
|
+
3. Tag: `git tag -a vX.Y.Z -m vX.Y.Z && git push origin vX.Y.Z`. The tag publishes the Docker image
|
|
410
|
+
(`docker-publish.yml`: `pgkiln/pgkiln` and `ghcr.io/pgkiln/pgkiln`, amd64 and arm64) and the npm package
|
|
411
|
+
(`npm-publish.yml`, trusted publishing with provenance; it refuses a tag that doesn't match
|
|
412
|
+
`package.json`). Then `gh release create vX.Y.Z` with the changelog section as notes.
|
|
381
413
|
4. Add the new tag to the `upgrade` job's matrix in `.github/workflows/ci.yml` (see above).
|
|
@@ -98,6 +98,21 @@ raise exception '%', meta.message('LEAVE_OVERLAP', to_char(p_start, 'DD-MM-YYYY'
|
|
|
98
98
|
|
|
99
99
|
The HR sample's dashboard uses them for its key figures (`examples/hr/hr_04_i18n.sql`).
|
|
100
100
|
|
|
101
|
+
### Dynamic translations
|
|
102
|
+
|
|
103
|
+
For **values from tables** (department names, statuses, list of values entries), use dynamic
|
|
104
|
+
translations (APEX: *Dynamic Translations*, `APEX_LANG.LANG`): **Shared Components → Dynamic
|
|
105
|
+
translations** holds a language, the text as stored and its translation, and `meta.lang` returns
|
|
106
|
+
the text in the session's language:
|
|
107
|
+
|
|
108
|
+
```sql
|
|
109
|
+
select meta.lang(dname) as d, deptno as r from hr.dept order by 1 -- a list of values
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
An exact language match wins, then the base language (`nl-BE` → `nl`), else the text as it is.
|
|
113
|
+
`meta.lang(text, 'de')` asks for another language. Dynamic translations are exported with the
|
|
114
|
+
application (`globalization/dynamic-translations.yaml` in an application's files).
|
|
115
|
+
|
|
101
116
|
### Date formats
|
|
102
117
|
|
|
103
118
|
Dates and timestamps in reports, grids and display items are formatted with a **mask**:
|
|
@@ -109,7 +109,7 @@ Audit triggers record such changes as made by `nobody`, which is what you want f
|
|
|
109
109
|
| **[pgmq](https://github.com/pgmq/pgmq)** | A message queue in Postgres for background work handled by a worker service | Like AWS SQS, transactional |
|
|
110
110
|
| **pg_partman** | Partition big append-only tables such as `meta.activity_log` by month | Only needed at high volume |
|
|
111
111
|
| **[plv8](https://github.com/plv8/plv8)** | Server-side JavaScript functions | Like APEX's MLE JavaScript. Maintained upstream, but some hosts (e.g. Supabase on PostgreSQL 17) no longer offer it, so check before depending on it |
|
|
112
|
-
| **[pg_smtp_client](https://github.com/brianpursley/pg_smtp_client)** | Sending e-mail from SQL,
|
|
112
|
+
| **[pg_smtp_client](https://github.com/brianpursley/pg_smtp_client)** | Sending e-mail synchronously from SQL, outside an application | pgkiln itself sends mail queued with `meta.send_mail` after the commit, through SMTP, Microsoft Graph, Amazon SES or an HTTP API ([chapter 6](06-processing.md#sending-e-mail)); this extension talks SMTP from the database itself |
|
|
113
113
|
|
|
114
114
|
## Security notes
|
|
115
115
|
|
package/docs/guide/18-cli.md
CHANGED
|
@@ -35,7 +35,7 @@ any (see [what is not exported](03-builder.md#export-format)).
|
|
|
35
35
|
| `pgkiln migrate [--example <name>]` | Applies the migrations that were not applied yet, then optionally `examples/<name>/` (same as `npm run db:migrate` / `npm run example:hr`; `--root` and `--seed` as in `scripts/migrate.ts`) |
|
|
36
36
|
| `pgkiln apps [--json]` | Lists the applications: alias, number of pages, name |
|
|
37
37
|
| `pgkiln export <alias> [--format json\|dir\|text] [--out <path>]` | `json` (default): the `pgkiln/2` document with sorted keys, to `--out` or standard output. `dir`: a directory (default `./<alias>`), see below. `text`: the same directory in YAML with the code inline ([text files](#text-files-apexlang)) |
|
|
38
|
-
| `pgkiln import <path> [--alias <alias>] [--replace]` | Imports a JSON export, an application directory or a `.zip` of one. `--alias` gives the copy another alias. `--replace` updates the application with that alias in place |
|
|
38
|
+
| `pgkiln import <path> [--alias <alias>] [--replace] [--check [--force] \| --dry-run]` | Imports a JSON export, an application directory or a `.zip` of one. `--alias` gives the copy another alias. `--replace` updates the application with that alias in place (after a [backup](03-builder.md#backups)). `--check` checks the application before it commits, `--dry-run` only checks ([below](#checking-before-an-import)) |
|
|
39
39
|
| `pgkiln diff <alias> <path> [--name-only \| --quiet]` | What differs between the application in the database and a directory (or JSON file, or zip) |
|
|
40
40
|
| `pgkiln mcp` | An MCP server on standard input/output for AI coding agents, see [chapter 20](20-ai-agents.md) |
|
|
41
41
|
| `pgkiln plugin build <dir> [-o <file>]` | Builds a plug-in file (`pgkiln-plugin/2`) from a source directory, see [plug-ins](04-pages-and-regions.md#plug-ins-with-their-own-code) |
|
|
@@ -187,6 +187,9 @@ the files any more are removed. What belongs to this installation stays:
|
|
|
187
187
|
[chapter 19](19-rest-data-sources.md#web-credentials)).
|
|
188
188
|
- the **style variant each user chose** ([chapter 14](14-globalization.md#style-variants-theme-roller));
|
|
189
189
|
a style that the file no longer has falls back to the default.
|
|
190
|
+
- its **availability and build status** ([chapter 3](03-builder.md#availability-and-build-status)): exports
|
|
191
|
+
leave them out, so a deployment doesn't open an application you closed, or close one;
|
|
192
|
+
- its **backups**: the version the import replaces is the newest one ([chapter 3](03-builder.md#backups)).
|
|
190
193
|
|
|
191
194
|
It all happens in one transaction; any error leaves the application unchanged. Without an
|
|
192
195
|
application with that alias, `--replace` simply imports, so the same command deploys the first
|
|
@@ -201,6 +204,32 @@ pgkiln diff hr apps/hr # on the target: what would cha
|
|
|
201
204
|
pgkiln import apps/hr --replace # deploy
|
|
202
205
|
```
|
|
203
206
|
|
|
207
|
+
### Checking before an import
|
|
208
|
+
|
|
209
|
+
An import doesn't need the application's tables, columns and functions to exist (neither does APEX's):
|
|
210
|
+
a page whose SQL names a missing table imports fine and fails when it runs. `--check` asks the
|
|
211
|
+
[Advisor](03-builder.md#advisor) first, inside the import's transaction, before anything is committed:
|
|
212
|
+
every SQL fragment is planned as the application's database role (unknown tables, columns,
|
|
213
|
+
functions and types, missing grants), PL/pgSQL blocks are compiled, and references to pages,
|
|
214
|
+
items, lists of values, schemes and search configurations are checked.
|
|
215
|
+
|
|
216
|
+
```sh
|
|
217
|
+
pgkiln import apps/hr --replace --check # errors: nothing changes, exit code 1
|
|
218
|
+
pgkiln import apps/hr --replace --dry-run # only check (exit code 0 without errors)
|
|
219
|
+
pgkiln import apps/hr --replace --check --force # report, import anyway
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
```text
|
|
223
|
+
ERROR page 2, region Employees (Source): relation "hr.employee" does not exist
|
|
224
|
+
WARNING page 3, item P3_SAL (Default value): …
|
|
225
|
+
Check: 1 error(s), 1 warning(s), 0 note(s).
|
|
226
|
+
Not imported: the check found 1 error(s). Fix them, or use --force.
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Deploy the database changes (your migration scripts) before the application, so the check sees
|
|
230
|
+
the tables the new version needs. The MCP server's `import_app` checks by default; the builder's
|
|
231
|
+
Import page has the check ticked.
|
|
232
|
+
|
|
204
233
|
`export --format dir` into an existing directory removes the files of deleted components and
|
|
205
234
|
leaves dot files (`.git`, `.gitattributes`) alone; it refuses a non-empty directory without a
|
|
206
235
|
`pgkiln.json`, so a typo can't empty the wrong folder.
|
|
@@ -53,7 +53,7 @@ directory the agent started the server in (the project).
|
|
|
53
53
|
| `recent_errors` | the latest errors and refused requests from the activity log | no |
|
|
54
54
|
| `export_app` | writes an application to a directory (default `apps/<alias>`, format `text`) | files on disk |
|
|
55
55
|
| `diff_app` | what an import of a directory would change (A/D/M with unified diffs); also reports files it can't read | no |
|
|
56
|
-
| `import_app` | imports a directory, zip or JSON file; `replace: true` updates the application in place | the database |
|
|
56
|
+
| `import_app` | imports a directory, zip or JSON file; `replace: true` updates the application in place. It checks the application first (the Advisor, in the import's transaction): errors refuse the import and are listed; `force: true` imports anyway, `dry_run: true` only checks | the database |
|
|
57
57
|
|
|
58
58
|
The files are the [directory format](18-cli.md#the-directory-format) of the command line, in the
|
|
59
59
|
[text style](18-cli.md#text-files-apexlang): one YAML file per component, with SQL, PL/pgSQL and
|
|
@@ -67,7 +67,8 @@ users, sessions and saved reports kept, supporting objects and plug-in install S
|
|
|
67
67
|
`describe_schema` for the tables; `search_docs` before guessing a property or region type.
|
|
68
68
|
2. `export_app` to `apps/<alias>`, then edit the YAML files there.
|
|
69
69
|
3. `diff_app`, so you (and it) see exactly what will change.
|
|
70
|
-
4. `import_app` with `replace: true`.
|
|
70
|
+
4. `import_app` with `replace: true`. When it names errors (a missing column, a typo in SQL), the agent fixes
|
|
71
|
+
the files and imports again; nothing was changed. Open the page in the browser to check.
|
|
71
72
|
|
|
72
73
|
Tables, views and functions are ordinary PostgreSQL objects: an agent changes them in your
|
|
73
74
|
migration scripts, not in the application files (`run_query` can't change them anyway).
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
-- =====================================================================
|
|
2
|
+
-- HR example, part 50: search configurations and the search region
|
|
3
|
+
-- (migration 080; docs/guide/04-pages-and-regions.md, search region)
|
|
4
|
+
--
|
|
5
|
+
-- Page 42 "Search": one field searches three sources as you type:
|
|
6
|
+
-- - EMPLOYEES: a query (employees with their department), each result
|
|
7
|
+
-- opens the employee's form (page 3);
|
|
8
|
+
-- - DEPARTMENTS: the table hr.dept, each result opens page 5;
|
|
9
|
+
-- - SHORTCUTS: the entries of the list HR_SHORTCUTS the user may see.
|
|
10
|
+
-- =====================================================================
|
|
11
|
+
|
|
12
|
+
insert into meta.search_config (app_id, name, label, seq, source_type, source, searchable_columns,
|
|
13
|
+
title_column, subtitle_column, badge_column, icon, link)
|
|
14
|
+
select a.id, c.name, c.label, c.seq, c.source_type, c.source, c.searchable, c.title, c.subtitle, c.badge, c.icon, c.link::jsonb
|
|
15
|
+
from meta.app a, (values
|
|
16
|
+
('EMPLOYEES', 'Employees', 10, 'sql',
|
|
17
|
+
$q$select e.empno, initcap(e.ename) as name, initcap(e.job) as job, initcap(d.dname) as department
|
|
18
|
+
from hr.emp e left join hr.dept d on d.deptno = e.deptno$q$,
|
|
19
|
+
'{name,job,department}'::text[], 'name', 'job', 'department', 'user', '{"page": 3, "items": {"P3_EMPNO": "#EMPNO#"}}'),
|
|
20
|
+
('DEPARTMENTS', 'Departments', 20, 'table', 'hr.dept', '{dname,loc}'::text[], 'dname', 'loc', null, 'building',
|
|
21
|
+
'{"page": 5, "items": {"P5_DEPTNO": "#DEPTNO#"}}'),
|
|
22
|
+
('SHORTCUTS', 'Shortcuts', 30, 'list', 'HR_SHORTCUTS', '{}'::text[], null, null, null, 'link', null)
|
|
23
|
+
) as c (name, label, seq, source_type, source, searchable, title, subtitle, badge, icon, link)
|
|
24
|
+
where a.alias = 'hr';
|
|
25
|
+
|
|
26
|
+
insert into meta.page (app_id, page_no, name, title, parent_page)
|
|
27
|
+
select id, 42, 'Search', 'Search', 1 from meta.app where alias = 'hr';
|
|
28
|
+
|
|
29
|
+
insert into meta.region (page_id, seq, title, type, columns, template, config)
|
|
30
|
+
select p.id, 10, 'Search', 'search', 12, 'standard',
|
|
31
|
+
'{"configs": ["EMPLOYEES", "DEPARTMENTS", "SHORTCUTS"], "placeholder": "Search employees, departments and shortcuts…"}'
|
|
32
|
+
from meta.page p join meta.app a on a.id = p.app_id where a.alias = 'hr' and p.page_no = 42;
|
|
33
|
+
|
|
34
|
+
insert into meta.nav_entry (app_id, seq, label, icon, target_page)
|
|
35
|
+
select id, 2, 'Search', 'search', 42 from meta.app where alias = 'hr';
|
|
36
|
+
|
|
37
|
+
insert into meta.translation (app_id, language, source, target)
|
|
38
|
+
select a.id, 'nl', t.source, t.target
|
|
39
|
+
from meta.app a, (values
|
|
40
|
+
('Search', 'Zoeken'),
|
|
41
|
+
('Employees', 'Medewerkers'),
|
|
42
|
+
('Departments', 'Afdelingen'),
|
|
43
|
+
('Shortcuts', 'Snelkoppelingen'),
|
|
44
|
+
('Search employees, departments and shortcuts…', 'Zoek medewerkers, afdelingen en snelkoppelingen…')
|
|
45
|
+
) as t (source, target)
|
|
46
|
+
where a.alias = 'hr'
|
|
47
|
+
on conflict do nothing;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
-- =====================================================================
|
|
2
|
+
-- HR example, part 51: e-mail (migration 081; docs/guide/06-processing.md,
|
|
3
|
+
-- "Sending e-mail")
|
|
4
|
+
--
|
|
5
|
+
-- The example sends through a mail service of provider "log": the server
|
|
6
|
+
-- writes each mail to its log and sends nothing, so the example works on
|
|
7
|
+
-- any installation. An administrator points HR at a real service under
|
|
8
|
+
-- Workspace utilities → Mail services (SMTP, Microsoft 365, Amazon SES,
|
|
9
|
+
-- Resend, Postmark, SendGrid or Mailgun).
|
|
10
|
+
--
|
|
11
|
+
-- The template LEAVE_DECIDED tells an employee that a leave request was
|
|
12
|
+
-- approved or rejected; a trigger queues it from the running application
|
|
13
|
+
-- when the status changes (the decision's transaction: a rollback sends
|
|
14
|
+
-- nothing). A mail that can't be queued never stops the decision.
|
|
15
|
+
-- =====================================================================
|
|
16
|
+
|
|
17
|
+
insert into meta.mail_service (name, description, provider, senders, settings)
|
|
18
|
+
values ('DEVELOPMENT_LOG', 'Writes mail to the server log; sends nothing', 'log', '{@example.com}', '{"default_from": "hr@example.com"}')
|
|
19
|
+
on conflict (name) do nothing;
|
|
20
|
+
|
|
21
|
+
insert into meta.app_mail_service (app_id, service_id, from_address, max_per_day)
|
|
22
|
+
select a.id, s.id, 'hr@example.com', 500
|
|
23
|
+
from meta.app a, meta.mail_service s
|
|
24
|
+
where a.alias = 'hr' and s.name = 'DEVELOPMENT_LOG'
|
|
25
|
+
on conflict (app_id) do nothing;
|
|
26
|
+
|
|
27
|
+
insert into meta.email_template (app_id, name, subject, html_body, text_body)
|
|
28
|
+
select id, 'LEAVE_DECIDED', 'Your leave request was #STATUS#',
|
|
29
|
+
$h$<p>Dear #NAME#,</p>
|
|
30
|
+
<p>Your leave request from <b>#START#</b> to <b>#END#</b> was <b>#STATUS#</b> by #DECIDED_BY#.</p>
|
|
31
|
+
<p>Kind regards,<br>HR</p>$h$,
|
|
32
|
+
$t$Dear #NAME#,
|
|
33
|
+
|
|
34
|
+
Your leave request from #START# to #END# was #STATUS# by #DECIDED_BY#.
|
|
35
|
+
|
|
36
|
+
Kind regards,
|
|
37
|
+
HR$t$
|
|
38
|
+
from meta.app where alias = 'hr';
|
|
39
|
+
|
|
40
|
+
create function hr.mail_leave_decided() returns trigger
|
|
41
|
+
language plpgsql security definer set search_path = hr, pg_catalog as $$
|
|
42
|
+
declare
|
|
43
|
+
v_email text;
|
|
44
|
+
v_name text;
|
|
45
|
+
begin
|
|
46
|
+
if new.status not in ('APPROVED', 'REJECTED') or meta.app_id() is null then
|
|
47
|
+
return new;
|
|
48
|
+
end if;
|
|
49
|
+
select a.email, initcap(e.ename) into v_email, v_name
|
|
50
|
+
from hr.emp e join meta.account a on lower(a.username) = lower(e.username)
|
|
51
|
+
where e.empno = new.empno;
|
|
52
|
+
if v_email is null then
|
|
53
|
+
return new;
|
|
54
|
+
end if;
|
|
55
|
+
perform meta.send_mail_template('LEAVE_DECIDED', jsonb_build_object(
|
|
56
|
+
'NAME', v_name,
|
|
57
|
+
'STATUS', lower(new.status),
|
|
58
|
+
'START', to_char(new.start_date, 'DD Mon YYYY'),
|
|
59
|
+
'END', to_char(new.end_date, 'DD Mon YYYY'),
|
|
60
|
+
'DECIDED_BY', coalesce(new.decided_by, 'HR')), v_email);
|
|
61
|
+
return new;
|
|
62
|
+
exception when others then
|
|
63
|
+
raise warning 'hr.mail_leave_decided: %', sqlerrm;
|
|
64
|
+
return new;
|
|
65
|
+
end
|
|
66
|
+
$$;
|
|
67
|
+
|
|
68
|
+
create trigger leave_decided_mail after update of status on hr.leave_request
|
|
69
|
+
for each row when (old.status is distinct from new.status)
|
|
70
|
+
execute function hr.mail_leave_decided();
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
-- =====================================================================
|
|
2
|
+
-- HR example, part 52: feedback (migration 084; docs/guide/03-builder.md,
|
|
3
|
+
-- "Feedback")
|
|
4
|
+
--
|
|
5
|
+
-- Users send feedback about a page from the navigation bar's Feedback link;
|
|
6
|
+
-- developers answer under Application → Activity → Feedback. One answered
|
|
7
|
+
-- example from the user king.
|
|
8
|
+
-- =====================================================================
|
|
9
|
+
|
|
10
|
+
update meta.app set feedback = true where alias = 'hr';
|
|
11
|
+
|
|
12
|
+
insert into meta.feedback (app_id, page_no, username, kind, rating, body, status, response, responded_by, responded_at)
|
|
13
|
+
select id, 2, 'king', 'idea', 4, 'Could the employee list show the hire date as well?', 'done',
|
|
14
|
+
'Added in the column list: open Actions → Columns and show "Hiredate".', 'admin', now()
|
|
15
|
+
from meta.app where alias = 'hr';
|
|
@@ -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;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pgkiln",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.35.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
|
@@ -265,6 +265,9 @@ details.menu > summary::-webkit-details-marker { display: none; }
|
|
|
265
265
|
.region-toolbar { display: flex; justify-content: flex-end; margin-bottom: 0.75rem; }
|
|
266
266
|
.region-static p:first-child { margin-top: 0; }
|
|
267
267
|
.region-static p:last-child { margin-bottom: 0; }
|
|
268
|
+
/* (078) URL region: the framed page fills the region's width; its height is a class per region (PageCss) */
|
|
269
|
+
.url-frame { display: block; width: 100%; border: 1px solid var(--border, #d0d7de); border-radius: 6px; background: var(--surface, #fff); }
|
|
270
|
+
.url-open { margin: 0.5rem 0 0; font-size: 0.875rem; }
|
|
268
271
|
.columns { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 420px), 1fr)); gap: 1rem; margin-top: 1rem; }
|
|
269
272
|
.columns .region { margin: 0; }
|
|
270
273
|
|
|
@@ -1416,6 +1419,12 @@ td.tc-cell { overflow-wrap: anywhere; }
|
|
|
1416
1419
|
|
|
1417
1420
|
.qr { display: flex; flex-direction: column; align-items: flex-start; gap: 0.3rem; padding: 0.3rem 0; }
|
|
1418
1421
|
.qr-code { max-width: 100%; height: auto; border-radius: 4px; }
|
|
1422
|
+
/* (086) display image and address items */
|
|
1423
|
+
.display-image { padding: 0.3rem 0; }
|
|
1424
|
+
.display-image img { max-width: 100%; height: auto; border-radius: 4px; }
|
|
1425
|
+
.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); }
|
|
1426
|
+
.geocode-places li { padding: 0.15rem 0.6rem; }
|
|
1427
|
+
.geocode-places .link-button { text-align: start; }
|
|
1419
1428
|
|
|
1420
1429
|
/* ---------- lists (list regions, navigation bar) ---------- */
|
|
1421
1430
|
.list-links { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.1rem; }
|
|
@@ -1550,3 +1559,56 @@ dialog.crop-dialog::backdrop { background: rgb(15 20 30 / 55%); }
|
|
|
1550
1559
|
.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
1560
|
.crop-stage { overflow: hidden; }
|
|
1552
1561
|
.crop-dialog .buttons { display: flex; justify-content: flex-end; gap: 0.5rem; }
|
|
1562
|
+
|
|
1563
|
+
/* (079) Developer toolbar: shown to developers with a builder session in the same browser.
|
|
1564
|
+
Fixed at the bottom left, collapsible (a <details>, so it works without JavaScript). */
|
|
1565
|
+
.devbar { position: fixed; left: 0.75rem; bottom: 0.75rem; z-index: 900; max-width: calc(100vw - 1.5rem);
|
|
1566
|
+
background: #1b2330; color: #fff; border-radius: var(--radius); box-shadow: 0 4px 16px rgb(0 0 0 / 25%); font-size: 0.8125rem; }
|
|
1567
|
+
.devbar summary { cursor: pointer; padding: 0.4rem 0.75rem; font-weight: 600; list-style: none; }
|
|
1568
|
+
.devbar summary::-webkit-details-marker { display: none; }
|
|
1569
|
+
.devbar summary::before { content: '▸ '; }
|
|
1570
|
+
.devbar[open] summary::before { content: '▾ '; }
|
|
1571
|
+
.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; }
|
|
1572
|
+
.devbar a { color: #9cc8ff; }
|
|
1573
|
+
.devbar a:focus-visible, .devbar button:focus-visible, .devbar select:focus-visible { outline: 2px solid #9cc8ff; outline-offset: 2px; }
|
|
1574
|
+
.devbar-debug { display: flex; align-items: center; gap: 0.35rem; margin: 0; }
|
|
1575
|
+
.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; }
|
|
1576
|
+
.devbar-debug button { cursor: pointer; }
|
|
1577
|
+
.devbar-page .t-main { max-width: 72rem; margin: 0 auto; padding: 1rem; }
|
|
1578
|
+
.devbar-facts { display: grid; grid-template-columns: max-content 1fr; gap: 0.25rem 1rem; }
|
|
1579
|
+
.devbar-facts dd { margin: 0; overflow-wrap: anywhere; }
|
|
1580
|
+
.devbar-session code { overflow-wrap: anywhere; }
|
|
1581
|
+
@media print { .devbar { display: none; } }
|
|
1582
|
+
|
|
1583
|
+
/* (080) Search region: one field, results grouped per search configuration */
|
|
1584
|
+
.search-form { margin-bottom: 1rem; }
|
|
1585
|
+
.search-group + .search-group { margin-top: 1rem; }
|
|
1586
|
+
.search-group h3 { font-size: 0.875rem; text-transform: uppercase; letter-spacing: 0.04em; color: var(--muted); margin: 0 0 0.4rem; }
|
|
1587
|
+
.search-hits { list-style: none; margin: 0; padding: 0; border: 1px solid var(--border); border-radius: var(--radius); }
|
|
1588
|
+
.search-hits li + li { border-top: 1px solid var(--border); }
|
|
1589
|
+
.search-hit { display: flex; align-items: flex-start; gap: 0.75rem; padding: 0.6rem 0.8rem; color: inherit; text-decoration: none; }
|
|
1590
|
+
a.search-hit:hover, a.search-hit:focus-visible { background: var(--accent-soft); }
|
|
1591
|
+
.search-icon { flex: none; color: var(--accent); }
|
|
1592
|
+
.search-text { display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; flex: 1; overflow-wrap: anywhere; }
|
|
1593
|
+
.search-title { font-weight: 600; }
|
|
1594
|
+
.search-subtitle, .search-description { color: var(--muted); font-size: 0.875rem; }
|
|
1595
|
+
.search-hit mark { background: #fff3a3; color: inherit; border-radius: 2px; padding: 0 1px; }
|
|
1596
|
+
:root[data-theme="dark"] .search-hit mark { background: #6b5a00; }
|
|
1597
|
+
@media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) .search-hit mark { background: #6b5a00; } }
|
|
1598
|
+
.search-hit .badge-pill { flex: none; }
|
|
1599
|
+
.search-more, .search-empty { margin: 0.4rem 0 0; }
|
|
1600
|
+
|
|
1601
|
+
/* (084) Feedback: the users' page and the builder's list */
|
|
1602
|
+
.feedback-form .choices { display: flex; flex-wrap: wrap; gap: 0.5rem 1rem; }
|
|
1603
|
+
.feedback-list, .feedback-admin { list-style: none; margin: 0; padding: 0; }
|
|
1604
|
+
.feedback-list > li, .feedback-admin > li { border-top: 1px solid var(--border); padding: 0.75rem 0; }
|
|
1605
|
+
.feedback-list > li:first-child, .feedback-admin > li:first-child { border-top: 0; }
|
|
1606
|
+
.feedback-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 0.35rem; overflow-wrap: anywhere; }
|
|
1607
|
+
.feedback-body { white-space: pre-wrap; overflow-wrap: anywhere; margin: 0.4rem 0; }
|
|
1608
|
+
.feedback-response { background: var(--surface-2); border-radius: var(--radius); padding: 0.5rem 0.75rem; }
|
|
1609
|
+
.feedback-response p { white-space: pre-wrap; overflow-wrap: anywhere; margin: 0.25rem 0 0; }
|
|
1610
|
+
|
|
1611
|
+
/* (085) Session timeout warning */
|
|
1612
|
+
.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; }
|
|
1613
|
+
.session-dialog::backdrop { background: rgb(0 0 0 / 40%); }
|
|
1614
|
+
.session-dialog p { margin: 0 0 1rem; }
|