pgkiln 0.51.0 → 0.53.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 +1 -1
- package/SECURITY.md +10 -1
- package/db/migrations/121_ai_agents.sql +94 -0
- package/db/migrations/122_team_development.sql +186 -0
- package/db/migrations/123_workflow_outcomes.sql +407 -0
- package/db/migrations/124_component_details.sql +51 -0
- package/db/migrations/125_app_change.sql +23 -0
- package/db/migrations/126_mail_queue_network_access.sql +119 -0
- package/docs/apex-feature-parity.md +50 -19
- package/docs/guide/01-installation.md +8 -4
- package/docs/guide/03-builder.md +197 -10
- package/docs/guide/04-pages-and-regions.md +71 -21
- package/docs/guide/05-items.md +7 -1
- package/docs/guide/06-processing.md +129 -9
- package/docs/guide/07-dynamic-actions.md +34 -0
- package/docs/guide/08-security.md +55 -0
- package/docs/guide/09-reference.md +5 -2
- package/docs/guide/11-from-apex.md +3 -1
- package/docs/guide/12-development.md +87 -16
- package/docs/guide/14-globalization.md +49 -1
- package/docs/guide/18-cli.md +21 -1
- package/docs/guide/19-rest-data-sources.md +12 -0
- package/docs/guide/20-ai-agents.md +12 -4
- package/examples/hr/README.md +2 -0
- package/examples/hr/hr_64_ai_agent.sql +97 -0
- package/examples/hr/hr_65_ask_reports.sql +21 -0
- package/examples/hr/hr_66_team_development.sql +58 -0
- package/examples/hr/hr_67_workflow_outcomes.sql +113 -0
- package/examples/hr/hr_68_component_details.sql +96 -0
- package/examples/hr/hr_70_data_security.sql +79 -0
- package/package.json +1 -1
- package/public/app.css +135 -21
- package/public/app.js +134 -20
- package/public/builder.css +133 -1
- package/public/builder.js +51 -0
- package/src/ai/chat.ts +18 -18
- package/src/ai/service.ts +35 -11
- package/src/ai/types.ts +7 -2
- package/src/app.ts +15 -0
- package/src/builder/advisor.ts +6 -3
- package/src/builder/ai-agents.ts +372 -0
- package/src/builder/ai-builder.ts +3 -3
- package/src/builder/ai.ts +13 -9
- package/src/builder/app-updates.ts +55 -0
- package/src/builder/assistant.ts +22 -7
- package/src/builder/components.ts +38 -9
- package/src/builder/data-security.ts +182 -0
- package/src/builder/designer.ts +8 -3
- package/src/builder/feedback.ts +6 -1
- package/src/builder/home.ts +16 -5
- package/src/builder/locks.ts +7 -1
- package/src/builder/mail-queue.ts +212 -0
- package/src/builder/mail.ts +72 -45
- package/src/builder/network-access.ts +140 -0
- package/src/builder/object-actions.ts +1 -0
- package/src/builder/object-browser.ts +1 -1
- package/src/builder/region-help.ts +2 -2
- package/src/builder/rest-catalogs.ts +19 -4
- package/src/builder/routes.ts +4 -1
- package/src/builder/shared.ts +1 -1
- package/src/builder/sql.ts +2 -0
- package/src/builder/team.ts +991 -0
- package/src/builder/themeroller.ts +47 -6
- package/src/builder/ui.ts +7 -1
- package/src/builder/websources.ts +1 -1
- package/src/builder/workflow-migrate.ts +210 -0
- package/src/builder/workflows.ts +29 -6
- package/src/builder/workspace-home.ts +2 -0
- package/src/builder/workspace-mail.ts +121 -0
- package/src/builder/workspace-transfer.ts +1 -1
- package/src/builder/workspaces.ts +1 -0
- package/src/cli/main.ts +25 -3
- package/src/cli/mcp.ts +40 -4
- package/src/cli/only.ts +52 -0
- package/src/cli/replace.ts +11 -1
- package/src/datasec.ts +343 -0
- package/src/i18n/ar.ts +17 -0
- package/src/i18n/cs.ts +17 -0
- package/src/i18n/da.ts +17 -0
- package/src/i18n/de.ts +17 -0
- package/src/i18n/el.ts +17 -0
- package/src/i18n/es.ts +17 -0
- package/src/i18n/fi.ts +17 -0
- package/src/i18n/fr.ts +17 -0
- package/src/i18n/he.ts +17 -0
- package/src/i18n/it.ts +17 -0
- package/src/i18n/ja.ts +17 -0
- package/src/i18n/ko.ts +17 -0
- package/src/i18n/nb.ts +17 -0
- package/src/i18n/pl.ts +17 -0
- package/src/i18n/pt.ts +17 -0
- package/src/i18n/ru.ts +17 -0
- package/src/i18n/sv.ts +17 -0
- package/src/i18n/tr.ts +17 -0
- package/src/i18n/uk.ts +17 -0
- package/src/i18n/zh.ts +17 -0
- package/src/i18n.ts +34 -0
- package/src/instance.ts +2 -0
- package/src/mail/mail.ts +50 -11
- package/src/mail/providers.ts +6 -3
- package/src/metadata.ts +11 -3
- package/src/network-acl.ts +126 -0
- package/src/objectstore.ts +3 -0
- package/src/runtime/ai-agents.ts +365 -0
- package/src/runtime/ai-filter.ts +385 -107
- package/src/runtime/assistant.ts +64 -11
- package/src/runtime/context.ts +2 -0
- package/src/runtime/engine.ts +3 -1
- package/src/runtime/ir-phrases.ts +812 -0
- package/src/runtime/ir-plan.ts +449 -0
- package/src/runtime/items.ts +1 -1
- package/src/runtime/regions.ts +6 -2
- package/src/runtime/render.ts +5 -3
- package/src/runtime/report.ts +1 -1
- package/src/runtime/styles.ts +54 -12
- package/src/runtime/tasks.ts +63 -12
- package/src/task-events.ts +232 -0
- package/src/team-samples.ts +74 -0
- package/src/team.ts +342 -0
- package/src/webclient.ts +45 -4
- package/src/webrequests.ts +5 -0
- package/src/websources.ts +30 -12
- package/src/workflow.ts +135 -18
- package/src/workspace-export.ts +16 -1
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
-- =====================================================================
|
|
2
|
+
-- HR example, part 66: Team Development (docs/guide/03-builder.md,
|
|
3
|
+
-- "Team Development")
|
|
4
|
+
--
|
|
5
|
+
-- What Builder → Team Development → Set up installs in the Default
|
|
6
|
+
-- workspace (the label groups Type and Priority, milestones 1.0 and 1.1,
|
|
7
|
+
-- a bug report template and a canned response), and one issue about the
|
|
8
|
+
-- HR application: "Salary rounding on page 12", a Bug of milestone 1.1,
|
|
9
|
+
-- assigned to admin. The Page Designer of page 12 shows it, and the
|
|
10
|
+
-- Workspace home counts it until it is closed. Issues are workspace data
|
|
11
|
+
-- (the workspace export), not part of the HR application's export.
|
|
12
|
+
-- =====================================================================
|
|
13
|
+
|
|
14
|
+
insert into meta.team_label_group (workspace_id, name, description, seq) values
|
|
15
|
+
(1, 'Type', 'What kind of work the issue is.', 10),
|
|
16
|
+
(1, 'Priority', 'How soon it should be done.', 20)
|
|
17
|
+
on conflict (workspace_id, lower(name)) do nothing;
|
|
18
|
+
|
|
19
|
+
insert into meta.team_label (workspace_id, group_id, name, color, description, seq)
|
|
20
|
+
select 1, g.id, l.name, l.color, l.description, l.seq
|
|
21
|
+
from (values ('Type', 'Bug', 'red', 'Something does not work as it should.', 10),
|
|
22
|
+
('Type', 'Feature', 'blue', 'New functionality.', 20),
|
|
23
|
+
('Type', 'Task', 'teal', 'Work to do that is neither a bug nor a feature.', 30),
|
|
24
|
+
('Priority', 'High', 'orange', 'Do this first.', 10),
|
|
25
|
+
('Priority', 'Medium', 'yellow', 'Do this soon.', 20),
|
|
26
|
+
('Priority', 'Low', 'gray', 'Do this when there is time.', 30)) as l(grp, name, color, description, seq)
|
|
27
|
+
join meta.team_label_group g on g.workspace_id = 1 and g.name = l.grp
|
|
28
|
+
on conflict (workspace_id, lower(name)) do nothing;
|
|
29
|
+
|
|
30
|
+
insert into meta.team_milestone (workspace_id, name, description, due_date) values
|
|
31
|
+
(1, '1.0', 'The first release.', current_date + 30),
|
|
32
|
+
(1, '1.1', 'The first update after the release.', current_date + 90)
|
|
33
|
+
on conflict (workspace_id, lower(name)) do nothing;
|
|
34
|
+
|
|
35
|
+
insert into meta.team_template (workspace_id, kind, name, title, body, labels, seq) values
|
|
36
|
+
(1, 'issue', 'Bug report', 'Bug: ', E'## What happens\n\n\n## What should happen\n\n\n## Steps to reproduce\n\n1. \n2. \n', '{Bug}', 10),
|
|
37
|
+
(1, 'response', 'Thanks, we will look at it', null, 'Thank you for reporting this. We will look at it and keep this issue up to date.', '{}', 10)
|
|
38
|
+
on conflict (workspace_id, kind, lower(name)) do nothing;
|
|
39
|
+
|
|
40
|
+
with issue as (
|
|
41
|
+
insert into meta.team_issue (workspace_id, title, body, milestone_id, app_id, page_no, created_by)
|
|
42
|
+
select 1, 'Salary rounding on page 12',
|
|
43
|
+
E'The leave calendar shows daily salary costs rounded to whole euros; finance expects cents.\n\n'
|
|
44
|
+
|| E'- Open **page 12** (Leave calendar)\n- Compare a day with the salary in `hr.emp`\n\n@admin can you have a look?',
|
|
45
|
+
(select id from meta.team_milestone where workspace_id = 1 and name = '1.1'),
|
|
46
|
+
a.id, 12, 'admin'
|
|
47
|
+
from meta.app a
|
|
48
|
+
where a.alias = 'hr'
|
|
49
|
+
and not exists (select 1 from meta.team_issue i where i.workspace_id = 1 and i.title = 'Salary rounding on page 12')
|
|
50
|
+
returning id
|
|
51
|
+
), labelled as (
|
|
52
|
+
insert into meta.team_issue_label (issue_id, label_id)
|
|
53
|
+
select issue.id, l.id from issue join meta.team_label l on l.workspace_id = 1 and l.name in ('Bug', 'Medium')
|
|
54
|
+
), assigned as (
|
|
55
|
+
insert into meta.team_issue_assignee (issue_id, username)
|
|
56
|
+
select issue.id, 'admin' from issue where exists (select 1 from meta.developer where username = 'admin')
|
|
57
|
+
)
|
|
58
|
+
insert into meta.team_issue_event (issue_id, username, action) select id, 'admin', 'created' from issue;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
-- =====================================================================
|
|
2
|
+
-- HR example, part 67: custom task outcomes, task events and
|
|
3
|
+
-- notifications, a Send E-Mail step, drafts and versions of a workflow
|
|
4
|
+
-- (migration 123; docs/guide/06-processing.md, "Approvals" and "Workflows")
|
|
5
|
+
--
|
|
6
|
+
-- SALARY_DECISION is a task with three outcomes instead of Approve / Reject:
|
|
7
|
+
-- Yes, No and Undecided. Its owners (role admin) get an e-mail when a task
|
|
8
|
+
-- is created (notification SALARY_TASK); when one is completed an event
|
|
9
|
+
-- records the decision as a notification for the initiator.
|
|
10
|
+
--
|
|
11
|
+
-- SALARY_CHANGE asks for that decision and branches on its outcome:
|
|
12
|
+
-- 1. APPROVE the task (an administrator decides)
|
|
13
|
+
-- 2. DECIDE YES: apply the new salary; UNDECIDED: ask again; otherwise the end
|
|
14
|
+
-- 3. APPLY the new salary (SQL)
|
|
15
|
+
-- 4. TELL an e-mail to the employee (version 2: a send_mail step)
|
|
16
|
+
-- Version 1 had no TELL step and is inactive; version 2 is active; version 3
|
|
17
|
+
-- is in development with a draft step (BUDGET_CHECK) that is still a
|
|
18
|
+
-- placeholder: it is drawn dashed, and can't be activated until it is
|
|
19
|
+
-- replaced. Instances of version 1 can be migrated to version 2 under
|
|
20
|
+
-- Shared Components → Workflows → SALARY_CHANGE → Migrate running instances.
|
|
21
|
+
--
|
|
22
|
+
-- Start one from SQL (e.g. the SQL Workshop, signed in to the application):
|
|
23
|
+
-- select meta.start_workflow('SALARY_CHANGE', '7499', '{"ENAME": "Allen", "NEW_SAL": 1760}')
|
|
24
|
+
-- and decide on it under My tasks (page 14) as king.
|
|
25
|
+
-- =====================================================================
|
|
26
|
+
|
|
27
|
+
insert into meta.email_template (app_id, name, subject, html_body, text_body)
|
|
28
|
+
select id, 'SALARY_TASK', 'To decide: #SUBJECT#',
|
|
29
|
+
$h$<p>A salary change waits for your decision: <b>#SUBJECT#</b>.</p>
|
|
30
|
+
<p>Requested by #INITIATOR#. Decide under <i>My tasks</i> in HR Demo.</p>$h$,
|
|
31
|
+
$t$A salary change waits for your decision: #SUBJECT#.
|
|
32
|
+
|
|
33
|
+
Requested by #INITIATOR#. Decide under "My tasks" in HR Demo.$t$
|
|
34
|
+
from meta.app where alias = 'hr'
|
|
35
|
+
union all
|
|
36
|
+
select id, 'SALARY_CHANGED', 'Your salary changes to #NEW_SAL#',
|
|
37
|
+
$h$<p>Dear #ENAME#,</p><p>Your new salary of <b>#NEW_SAL#</b> was approved by #TASK_APPROVER#.</p><p>Kind regards,<br>HR</p>$h$,
|
|
38
|
+
$t$Dear #ENAME#,
|
|
39
|
+
|
|
40
|
+
Your new salary of #NEW_SAL# was approved by #TASK_APPROVER#.
|
|
41
|
+
|
|
42
|
+
Kind regards,
|
|
43
|
+
HR$t$
|
|
44
|
+
from meta.app where alias = 'hr';
|
|
45
|
+
|
|
46
|
+
insert into meta.task_definition (app_id, name, type, subject, owner_roles, admin_role, priority, due_in, outcomes, events, notifications)
|
|
47
|
+
select id, 'SALARY_DECISION', 'approval', 'Salary of &ENAME. to &NEW_SAL.', '{admin}'::text[], 'admin', 2, '5 days',
|
|
48
|
+
$o$[{"name": "YES", "label": "Yes", "colour": "success"},
|
|
49
|
+
{"name": "NO", "label": "No", "colour": "danger"},
|
|
50
|
+
{"name": "UNDECIDED", "label": "Undecided", "colour": "warning"}]$o$::jsonb,
|
|
51
|
+
$e$[{"on": "complete", "type": "sql", "code": "select hr.salary_decided(:DETAIL_PK::int, :OUTCOME, :INITIATOR)"}]$e$::jsonb,
|
|
52
|
+
'{"create": "SALARY_TASK"}'::jsonb
|
|
53
|
+
from meta.app where alias = 'hr';
|
|
54
|
+
|
|
55
|
+
-- Notifications are for other users, which RLS forbids the app: security definer, like the onboarding ones.
|
|
56
|
+
create function hr.salary_decided(p_empno int, p_outcome text, p_initiator text) returns void
|
|
57
|
+
language plpgsql security definer set search_path = hr, pg_catalog as $$
|
|
58
|
+
begin
|
|
59
|
+
insert into hr.notification (username, message)
|
|
60
|
+
select p_initiator, format('Salary change of %s: %s.', initcap(e.ename), lower(p_outcome))
|
|
61
|
+
from hr.emp e where e.empno = p_empno;
|
|
62
|
+
end
|
|
63
|
+
$$;
|
|
64
|
+
create function hr.salary_change_apply(p_empno int, p_sal numeric) returns timestamptz
|
|
65
|
+
language plpgsql security definer set search_path = hr, pg_catalog as $$
|
|
66
|
+
begin
|
|
67
|
+
if p_sal is null or p_sal <= 0 then
|
|
68
|
+
raise exception 'A salary is more than 0.';
|
|
69
|
+
end if;
|
|
70
|
+
update hr.emp set sal = p_sal where empno = p_empno;
|
|
71
|
+
return now();
|
|
72
|
+
end
|
|
73
|
+
$$;
|
|
74
|
+
-- the e-mail address of an employee's account (the accounts are pgkiln's, not the application's)
|
|
75
|
+
create function hr.employee_email(p_empno int) returns text
|
|
76
|
+
language sql stable security definer set search_path = hr, pg_catalog as $$
|
|
77
|
+
select a.email from hr.emp e join meta.account a on lower(a.username) = lower(e.username) where e.empno = p_empno and a.active
|
|
78
|
+
$$;
|
|
79
|
+
revoke all on function hr.salary_decided(int, text, text), hr.salary_change_apply(int, numeric), hr.employee_email(int) from public;
|
|
80
|
+
grant execute on function hr.salary_decided(int, text, text), hr.salary_change_apply(int, numeric), hr.employee_email(int) to hr_app;
|
|
81
|
+
|
|
82
|
+
-- version 1, then version 2 (with the e-mail) becomes active, then version 3 in development (a draft step)
|
|
83
|
+
insert into meta.workflow_definition (app_id, name, title, description, admin_role, steps)
|
|
84
|
+
select id, 'SALARY_CHANGE', 'Salary of &ENAME. to &NEW_SAL.', 'A salary change, decided by an administrator (Yes, No or Undecided).', 'admin',
|
|
85
|
+
$s$[
|
|
86
|
+
{"name": "APPROVE", "type": "task", "task": "SALARY_DECISION"},
|
|
87
|
+
{"name": "DECIDE", "type": "switch", "cases": [{"outcome": "YES", "next": "APPLY"}, {"outcome": "UNDECIDED", "next": "APPROVE"}], "otherwise": "END"},
|
|
88
|
+
{"name": "APPLY", "type": "sql", "code": "select hr.salary_change_apply(:DETAIL_PK::int, :NEW_SAL::numeric) as applied_at", "next": "END"},
|
|
89
|
+
{"name": "END", "type": "end"}
|
|
90
|
+
]$s$::jsonb
|
|
91
|
+
from meta.app where alias = 'hr';
|
|
92
|
+
|
|
93
|
+
select meta.new_workflow_version(d.id) from meta.workflow_definition d join meta.app a on a.id = d.app_id where a.alias = 'hr' and d.name = 'SALARY_CHANGE';
|
|
94
|
+
update meta.workflow_definition d set dev_steps = $s$[
|
|
95
|
+
{"name": "APPROVE", "type": "task", "task": "SALARY_DECISION"},
|
|
96
|
+
{"name": "DECIDE", "type": "switch", "cases": [{"outcome": "YES", "next": "APPLY"}, {"outcome": "UNDECIDED", "next": "APPROVE"}], "otherwise": "END"},
|
|
97
|
+
{"name": "APPLY", "type": "sql", "code": "select hr.salary_change_apply(:DETAIL_PK::int, :NEW_SAL::numeric) as applied_at"},
|
|
98
|
+
{"name": "TELL", "type": "send_mail", "to": "select hr.employee_email(:DETAIL_PK::int)", "template": "SALARY_CHANGED", "next": "END"},
|
|
99
|
+
{"name": "END", "type": "end"}
|
|
100
|
+
]$s$::jsonb
|
|
101
|
+
from meta.app a where a.id = d.app_id and a.alias = 'hr' and d.name = 'SALARY_CHANGE';
|
|
102
|
+
select meta.activate_workflow_version(d.id) from meta.workflow_definition d join meta.app a on a.id = d.app_id where a.alias = 'hr' and d.name = 'SALARY_CHANGE';
|
|
103
|
+
|
|
104
|
+
select meta.new_workflow_version(d.id) from meta.workflow_definition d join meta.app a on a.id = d.app_id where a.alias = 'hr' and d.name = 'SALARY_CHANGE';
|
|
105
|
+
update meta.workflow_definition d set dev_steps = $s$[
|
|
106
|
+
{"name": "BUDGET_CHECK", "type": "draft", "note": "check the department's budget", "next": "APPROVE"},
|
|
107
|
+
{"name": "APPROVE", "type": "task", "task": "SALARY_DECISION"},
|
|
108
|
+
{"name": "DECIDE", "type": "switch", "cases": [{"outcome": "YES", "next": "APPLY"}, {"outcome": "UNDECIDED", "next": "APPROVE"}], "otherwise": "END"},
|
|
109
|
+
{"name": "APPLY", "type": "sql", "code": "select hr.salary_change_apply(:DETAIL_PK::int, :NEW_SAL::numeric) as applied_at"},
|
|
110
|
+
{"name": "TELL", "type": "send_mail", "to": "select hr.employee_email(:DETAIL_PK::int)", "template": "SALARY_CHANGED", "next": "END"},
|
|
111
|
+
{"name": "END", "type": "end"}
|
|
112
|
+
]$s$::jsonb
|
|
113
|
+
from meta.app a where a.id = d.app_id and a.alias = 'hr' and d.name = 'SALARY_CHANGE';
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
-- =====================================================================
|
|
2
|
+
-- HR example, part 68: component details of APEX 26.1 (migration 124;
|
|
3
|
+
-- docs/guide/05-items.md "Popup LOV", docs/guide/07-dynamic-actions.md
|
|
4
|
+
-- "Wait popup" and "Menu entry chosen")
|
|
5
|
+
--
|
|
6
|
+
-- Page 47 "Component details":
|
|
7
|
+
-- * P47_REF, a popup LOV over 6,000 reference numbers: the dialog loads
|
|
8
|
+
-- the next 100 as the list scrolls (no More button).
|
|
9
|
+
-- * RECALC, a submit button whose process takes two seconds: a dynamic
|
|
10
|
+
-- action shows the wait popup on its click; the next page removes it.
|
|
11
|
+
-- * CHECK ("Check in the background"): wait popup, an Ajax call (execute_sql,
|
|
12
|
+
-- a second and a half) that fills P47_RESULT, remove wait popup.
|
|
13
|
+
-- * ACTIONS, a menu button: a link, and two entries that run dynamic
|
|
14
|
+
-- actions (event menu_entry): COUNT (the same Ajax call with a wait
|
|
15
|
+
-- popup) and HELLO (a success message).
|
|
16
|
+
-- =====================================================================
|
|
17
|
+
|
|
18
|
+
insert into meta.page (app_id, page_no, name, title, parent_page)
|
|
19
|
+
select id, 47, 'Component details', 'Component details', 1 from meta.app where alias = 'hr';
|
|
20
|
+
|
|
21
|
+
insert into meta.region (page_id, seq, title, type, columns, template, source)
|
|
22
|
+
select p.id, 10, 'Component details', 'static', 12, 'standard',
|
|
23
|
+
'<p>A popup list of values that loads more rows as you scroll, a wait popup around a long process and an Ajax call, and a menu button whose entries run dynamic actions.</p>'
|
|
24
|
+
from meta.page p join meta.app a on a.id = p.app_id
|
|
25
|
+
where a.alias = 'hr' and p.page_no = 47;
|
|
26
|
+
|
|
27
|
+
insert into meta.item (page_id, region_id, seq, name, label, type, lov, config, help)
|
|
28
|
+
select p.id, r.id, i.seq, i.name, i.label, i.type, i.lov, i.config::jsonb, i.help
|
|
29
|
+
from meta.page p join meta.app a on a.id = p.app_id
|
|
30
|
+
join meta.region r on r.page_id = p.id and r.title = 'Component details', (values
|
|
31
|
+
(10, 'P47_REF', 'Reference', 'popup_lov',
|
|
32
|
+
'select ''REF-'' || lpad(n::text, 5, ''0'') || '' · '' || (array[''Accounting'', ''Research'', ''Sales'', ''Operations''])[1 + n % 4] as name, n as ref
|
|
33
|
+
from generate_series(1, 6000) n
|
|
34
|
+
order by n',
|
|
35
|
+
'{"page_size": 100}', '6,000 reference numbers: scroll the dialog to load more.'),
|
|
36
|
+
(20, 'P47_RESULT', 'Result', 'text', null, '{}', 'Filled by the Ajax call behind Check in the background, or Actions → Count employees.')
|
|
37
|
+
) as i (seq, name, label, type, lov, config, help)
|
|
38
|
+
where a.alias = 'hr' and p.page_no = 47;
|
|
39
|
+
|
|
40
|
+
insert into meta.button (page_id, region_id, seq, name, label, action, hot, menu)
|
|
41
|
+
select p.id, r.id, b.seq, b.name, b.label, b.action, b.hot, b.menu::jsonb
|
|
42
|
+
from meta.page p join meta.app a on a.id = p.app_id
|
|
43
|
+
join meta.region r on r.page_id = p.id and r.title = 'Component details', (values
|
|
44
|
+
(10, 'RECALC', 'Recalculate', 'submit', true, null),
|
|
45
|
+
(20, 'CHECK', 'Check in the background', 'da', false, null),
|
|
46
|
+
(30, 'ACTIONS', 'Actions', 'menu', false,
|
|
47
|
+
'[{"label": "Count employees", "action": "COUNT", "icon": "users"},
|
|
48
|
+
{"label": "Say hello", "action": "HELLO", "icon": "bell"},
|
|
49
|
+
{"label": "Pick an employee", "page": 26, "icon": "search"}]')
|
|
50
|
+
) as b (seq, name, label, action, hot, menu)
|
|
51
|
+
where a.alias = 'hr' and p.page_no = 47;
|
|
52
|
+
|
|
53
|
+
-- the long process: two seconds on RECALC
|
|
54
|
+
insert into meta.process (page_id, seq, name, type, point, code, when_button, success_message)
|
|
55
|
+
select p.id, 10, 'Recalculate (slow)', 'sql', 'submit', 'select pg_sleep(2)', 'RECALC', 'Recalculated.'
|
|
56
|
+
from meta.page p join meta.app a on a.id = p.app_id
|
|
57
|
+
where a.alias = 'hr' and p.page_no = 47;
|
|
58
|
+
|
|
59
|
+
insert into meta.dynamic_action (page_id, seq, name, event, trigger_element, action, affected_items, code, message)
|
|
60
|
+
select p.id, d.seq, d.name, d.event, d.trig, d.action, d.affected, d.code, d.message
|
|
61
|
+
from meta.page p join meta.app a on a.id = p.app_id, (values
|
|
62
|
+
(10, 'Wait while recalculating', 'click', 'RECALC', 'wait_popup', null, null, 'Recalculating…'),
|
|
63
|
+
(20, 'Wait for the check', 'click', 'CHECK', 'wait_popup', null, null, 'Checking…'),
|
|
64
|
+
(30, 'The check', 'click', 'CHECK', 'execute_sql', 'P47_RESULT',
|
|
65
|
+
'select (select count(*) from hr.emp) || '' employees, checked at '' || to_char(clock_timestamp(), ''HH24:MI:SS'') as p47_result from pg_sleep(1.5)', null),
|
|
66
|
+
(40, 'Check done', 'click', 'CHECK', 'remove_wait_popup', null, null, null),
|
|
67
|
+
(50, 'Wait for the count', 'menu_entry', 'COUNT', 'wait_popup', null, null, 'Counting employees…'),
|
|
68
|
+
(60, 'The count', 'menu_entry', 'COUNT', 'execute_sql', 'P47_RESULT',
|
|
69
|
+
'select (select count(*) from hr.emp) || '' employees, counted at '' || to_char(clock_timestamp(), ''HH24:MI:SS'') as p47_result from pg_sleep(1.5)', null),
|
|
70
|
+
(70, 'Count done', 'menu_entry', 'COUNT', 'remove_wait_popup', null, null, null),
|
|
71
|
+
(80, 'Hello', 'menu_entry', 'HELLO', 'show_success', null, null, 'Hello from a menu entry.')
|
|
72
|
+
) as d (seq, name, event, trig, action, affected, code, message)
|
|
73
|
+
where a.alias = 'hr' and p.page_no = 47;
|
|
74
|
+
|
|
75
|
+
insert into meta.nav_entry (app_id, seq, label, icon, target_page)
|
|
76
|
+
select id, 34, 'Component details', 'list', 47 from meta.app where alias = 'hr';
|
|
77
|
+
|
|
78
|
+
insert into meta.translation (app_id, language, source, target)
|
|
79
|
+
select a.id, 'nl', t.source, t.target
|
|
80
|
+
from meta.app a, (values
|
|
81
|
+
('Component details', 'Componentdetails'),
|
|
82
|
+
('Reference', 'Referentie'),
|
|
83
|
+
('Result', 'Resultaat'),
|
|
84
|
+
('Recalculate', 'Herberekenen'),
|
|
85
|
+
('Check in the background', 'Op de achtergrond controleren'),
|
|
86
|
+
('Actions', 'Acties'),
|
|
87
|
+
('Count employees', 'Medewerkers tellen'),
|
|
88
|
+
('Say hello', 'Hallo zeggen'),
|
|
89
|
+
('Recalculating…', 'Herberekenen…'),
|
|
90
|
+
('Checking…', 'Controleren…'),
|
|
91
|
+
('Counting employees…', 'Medewerkers tellen…'),
|
|
92
|
+
('Recalculated.', 'Herberekend.'),
|
|
93
|
+
('Hello from a menu entry.', 'Hallo vanuit een menu-item.')
|
|
94
|
+
) as t(source, target)
|
|
95
|
+
where a.alias = 'hr'
|
|
96
|
+
on conflict do nothing;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
-- =====================================================================
|
|
2
|
+
-- HR example, part 70: data security per table (sprint 119, WS-13;
|
|
3
|
+
-- docs/guide/08-security.md "Data security per table")
|
|
4
|
+
--
|
|
5
|
+
-- hr.emp_masked, the masking view that SQL Workshop › hr.emp › Security
|
|
6
|
+
-- writes when sal and comm are masked (empty) for everyone but the
|
|
7
|
+
-- application role manager. It runs as hr_masking, so row level security
|
|
8
|
+
-- on hr.emp would still apply inside it. hr_app keeps SELECT on hr.emp
|
|
9
|
+
-- because the other pages read it: the Security page warns that the mask
|
|
10
|
+
-- holds only where the view is read.
|
|
11
|
+
--
|
|
12
|
+
-- Page 48 "Salaries (masked)": a report on the view. Signed in as KING
|
|
13
|
+
-- (manager) it shows salaries; as ALLEN (no manager role) they are empty.
|
|
14
|
+
-- =====================================================================
|
|
15
|
+
|
|
16
|
+
do $role$ begin
|
|
17
|
+
if not exists (select from pg_roles where rolname = 'hr_masking') then
|
|
18
|
+
create role hr_masking nologin;
|
|
19
|
+
end if;
|
|
20
|
+
end $role$;
|
|
21
|
+
grant hr_masking to current_user;
|
|
22
|
+
grant usage on schema hr to hr_masking;
|
|
23
|
+
grant select on hr.emp to hr_masking;
|
|
24
|
+
grant hr_app to hr_masking;
|
|
25
|
+
|
|
26
|
+
create view hr.emp_masked with (security_barrier) as
|
|
27
|
+
select t.empno,
|
|
28
|
+
t.ename,
|
|
29
|
+
t.job,
|
|
30
|
+
t.mgr,
|
|
31
|
+
t.hiredate,
|
|
32
|
+
case when (select meta.has_role('manager')) then t.sal end as sal,
|
|
33
|
+
case when (select meta.has_role('manager')) then t.comm end as comm,
|
|
34
|
+
t.deptno,
|
|
35
|
+
t.active,
|
|
36
|
+
t.username,
|
|
37
|
+
t.photo,
|
|
38
|
+
t.photo_name,
|
|
39
|
+
t.photo_mime,
|
|
40
|
+
t.work_location
|
|
41
|
+
from hr.emp t;
|
|
42
|
+
alter view hr.emp_masked owner to hr_masking;
|
|
43
|
+
comment on view hr.emp_masked is 'pgkiln masking view of emp: {"table":"emp","roles":["manager"],"cols":{"comm":{"mask":"null","see":["manager"],"others":false},"sal":{"mask":"null","see":["manager"],"others":false}}}';
|
|
44
|
+
grant select on hr.emp_masked to hr_app;
|
|
45
|
+
|
|
46
|
+
do $check$ begin
|
|
47
|
+
if exists (select from pg_roles r where r.rolname = 'hr_masking'
|
|
48
|
+
and (r.rolsuper or r.rolbypassrls or pg_has_role(r.oid, (select relowner from pg_class where oid = 'hr.emp'::regclass), 'USAGE'))) then
|
|
49
|
+
raise exception 'the masking role hr_masking would bypass row level security on hr.emp';
|
|
50
|
+
end if;
|
|
51
|
+
end $check$;
|
|
52
|
+
|
|
53
|
+
insert into meta.page (app_id, page_no, name, title, parent_page)
|
|
54
|
+
select id, 48, 'Salaries (masked)', 'Salaries (masked)', 1 from meta.app where alias = 'hr';
|
|
55
|
+
|
|
56
|
+
insert into meta.region (page_id, seq, title, type, columns, template, source, config)
|
|
57
|
+
select p.id, r.seq, r.title, r.type, 12, 'standard', r.source, r.config::jsonb
|
|
58
|
+
from meta.page p join meta.app a on a.id = p.app_id, (values
|
|
59
|
+
(10, 'Salaries (masked)', 'static',
|
|
60
|
+
'<p>This report reads <code>hr.emp_masked</code>: salary and commission are only shown to the role <b>manager</b>. Open SQL Workshop › hr.emp › Security to change who sees what.</p>', '{}'),
|
|
61
|
+
(20, 'Employees', 'report',
|
|
62
|
+
'select ename, job, sal, comm from hr.emp_masked where active order by ename',
|
|
63
|
+
'{"page_size": 15, "headings": {"ename": "Name", "job": "Job", "sal": "Salary", "comm": "Commission"}}')
|
|
64
|
+
) as r (seq, title, type, source, config)
|
|
65
|
+
where a.alias = 'hr' and p.page_no = 48;
|
|
66
|
+
|
|
67
|
+
insert into meta.nav_entry (app_id, seq, label, icon, target_page)
|
|
68
|
+
select id, 35, 'Salaries (masked)', 'shield', 48 from meta.app where alias = 'hr';
|
|
69
|
+
|
|
70
|
+
insert into meta.translation (app_id, language, source, target)
|
|
71
|
+
select a.id, 'nl', t.source, t.target
|
|
72
|
+
from meta.app a, (values
|
|
73
|
+
('Salaries (masked)', 'Salarissen (gemaskeerd)'),
|
|
74
|
+
('Employees', 'Medewerkers'),
|
|
75
|
+
('Salary', 'Salaris'),
|
|
76
|
+
('Commission', 'Commissie')
|
|
77
|
+
) as t(source, target)
|
|
78
|
+
where a.alias = 'hr'
|
|
79
|
+
on conflict do nothing;
|
package/package.json
CHANGED
package/public/app.css
CHANGED
|
@@ -155,8 +155,8 @@ html[data-style="iris"][data-theme="dark"] {
|
|
|
155
155
|
--hot: #ece9e6;
|
|
156
156
|
--hot-text: #1a1817;
|
|
157
157
|
}
|
|
158
|
-
html[data-style="iris"] .t-header { box-shadow: inset 0 3px 0 var(--accent); }
|
|
159
|
-
html[data-style="iris"] .t-nav { border-
|
|
158
|
+
html[data-style="iris"] .t-header { box-shadow: inset 0 3px 0 var(--strip, var(--accent)); }
|
|
159
|
+
html[data-style="iris"] .t-nav { border-inline-end-color: transparent; }
|
|
160
160
|
html[data-style="iris"] .t-titlebar { background: transparent; border-bottom: 0; padding: 1.25rem 1.5rem 0.25rem; }
|
|
161
161
|
html[data-style="iris"] .t-titlebar h1 { font-size: 2rem; font-weight: 700; letter-spacing: -0.02em; line-height: 1.2; }
|
|
162
162
|
html[data-style="iris"] .region-header h2 { font-size: 1.05rem; }
|
|
@@ -225,7 +225,7 @@ html[data-style="lavender"][data-theme="dark"] {
|
|
|
225
225
|
--shadow: 0 1px 2px rgb(0 0 0 / 30%), 0 2px 8px rgb(0 0 0 / 25%);
|
|
226
226
|
}
|
|
227
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"] {
|
|
228
|
+
html[data-style="lavender"] .t-nav a[aria-current="page"] { border-inline-start-color: transparent; border-radius: calc(var(--radius) * 0.75); }
|
|
229
229
|
html[data-style="lavender"] .t-nav a, html[data-style="lavender"] .t-nav summary { border-radius: calc(var(--radius) * 0.75); }
|
|
230
230
|
html[data-style="lavender"] .region-header { border-bottom-color: transparent; }
|
|
231
231
|
html[data-style="lavender"] .region-header h2 { letter-spacing: -0.01em; }
|
|
@@ -243,8 +243,8 @@ h2 { font-size: 1rem; margin: 0; font-weight: 600; }
|
|
|
243
243
|
.lead { font-size: 1.05rem; margin: 0; }
|
|
244
244
|
[hidden] { display: none !important; }
|
|
245
245
|
.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
|
|
246
|
-
.skip-link { position: absolute;
|
|
247
|
-
.skip-link:focus {
|
|
246
|
+
.skip-link { position: absolute; inset-inline-start: -9999px; top: 0; z-index: 100; background: var(--surface); padding: 0.5rem 1rem; }
|
|
247
|
+
.skip-link:focus { inset-inline-start: 0.5rem; top: 0.5rem; }
|
|
248
248
|
.icon { width: 18px; height: 18px; flex: none; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
|
|
249
249
|
/* (0.31) icon modifiers, like Font APEX's: "users lg", "refresh spin", "truck flip-h success" */
|
|
250
250
|
.icon.icon-xs { width: 0.75em; height: 0.75em; } .icon.icon-sm { width: 0.875em; height: 0.875em; }
|
|
@@ -285,15 +285,15 @@ details.menu { position: relative; }
|
|
|
285
285
|
details.menu > summary { list-style: none; }
|
|
286
286
|
details.menu > summary::-webkit-details-marker { display: none; }
|
|
287
287
|
.menu-panel {
|
|
288
|
-
position: absolute; z-index: 30; top: calc(100% + 6px);
|
|
288
|
+
position: absolute; z-index: 30; top: calc(100% + 6px); inset-inline-start: 0; min-width: 240px;
|
|
289
289
|
background: var(--surface); color: var(--text); border: 1px solid var(--border);
|
|
290
290
|
border-radius: var(--radius); box-shadow: 0 8px 24px rgb(16 24 40 / 16%); padding: 0.4rem 0;
|
|
291
291
|
max-height: min(80vh, 42rem); overflow: auto;
|
|
292
292
|
}
|
|
293
|
-
.menu-panel.align-right, .actions-menu .menu-panel {
|
|
293
|
+
.menu-panel.align-right, .actions-menu .menu-panel { inset-inline-start: auto; inset-inline-end: 0; }
|
|
294
294
|
.menu-section { padding: 0.5rem 0.9rem; display: flex; flex-direction: column; gap: 0.4rem; }
|
|
295
295
|
.menu-section + .menu-section { border-top: 1px solid var(--border); }
|
|
296
|
-
.menu-links a, .menu-links button { color: var(--text); text-decoration: none; padding: 0.2rem 0; display: flex; align-items: center; gap: 0.45rem; background: none; border: 0; font: inherit; cursor: pointer; text-align:
|
|
296
|
+
.menu-links a, .menu-links button { color: var(--text); text-decoration: none; padding: 0.2rem 0; display: flex; align-items: center; gap: 0.45rem; background: none; border: 0; font: inherit; cursor: pointer; text-align: start; }
|
|
297
297
|
.menu-links a:hover, .menu-links button:hover { color: var(--accent); }
|
|
298
298
|
.menu-links .icon { width: 1em; height: 1em; }
|
|
299
299
|
.filter-row { display: grid; grid-template-columns: 1fr 1fr; gap: 0.4rem; min-width: 320px; }
|
|
@@ -308,21 +308,31 @@ details.menu > summary::-webkit-details-marker { display: none; }
|
|
|
308
308
|
body.nav-collapsed .t-body { grid-template-columns: 0 minmax(0, 1fr); }
|
|
309
309
|
body.nav-collapsed .t-nav { visibility: hidden; }
|
|
310
310
|
}
|
|
311
|
-
.t-nav { background: var(--nav-bg); border-
|
|
311
|
+
.t-nav { background: var(--nav-bg); border-inline-end: 1px solid var(--border); overflow: hidden auto; }
|
|
312
312
|
.t-nav ul { list-style: none; margin: 0; padding: 0.5rem; }
|
|
313
|
-
|
|
313
|
+
/* (sprint 117) a subtree hangs on a guide line; the open trail's line in the accent */
|
|
314
|
+
.t-nav ul ul { padding: 0; padding-inline-start: 0.35rem; margin-inline-start: 1.05rem; border-inline-start: 1px solid var(--nav-guide, color-mix(in srgb, var(--nav-text, var(--text)) 16%, transparent)); }
|
|
315
|
+
.t-nav details:has(> summary.in-trail) > ul { border-inline-start-color: color-mix(in srgb, var(--nav-current, var(--accent)) 55%, transparent); }
|
|
314
316
|
.t-nav a, .t-nav summary {
|
|
315
317
|
display: flex; align-items: center; gap: 0.65rem; padding: 0.5rem 0.65rem; border-radius: 6px;
|
|
318
|
+
/* (sprint 117) the bar at the inline start (left, or right in RTL) marks the current page */
|
|
319
|
+
border-inline-start: 3px solid transparent; padding-inline-start: calc(0.65rem - 3px);
|
|
316
320
|
color: var(--nav-text, var(--text)); text-decoration: none; cursor: pointer; white-space: nowrap; font-size: 0.93rem;
|
|
317
321
|
}
|
|
318
322
|
.t-nav summary { list-style: none; }
|
|
319
323
|
.t-nav summary::-webkit-details-marker { display: none; }
|
|
320
|
-
.t-nav summary::after { content: "›"; margin-
|
|
324
|
+
.t-nav summary::after { content: "›"; margin-inline-start: auto; color: var(--nav-muted, var(--muted)); transition: transform 0.15s; }
|
|
321
325
|
.t-nav details[open] > summary::after { transform: rotate(90deg); }
|
|
326
|
+
/* RTL: the bidi algorithm mirrors "›" to "‹"; opened, it turns the other way to point down */
|
|
327
|
+
[dir="rtl"] .t-nav details[open] > summary::after { transform: rotate(-90deg); }
|
|
322
328
|
.t-nav a:hover, .t-nav summary:hover { background: var(--nav-hover, var(--surface-3)); }
|
|
323
|
-
/* (sprint
|
|
324
|
-
.t-nav a
|
|
325
|
-
|
|
329
|
+
/* (sprint 117) the selected entry (keyboard focus) gets a ring inside the menu, which scrolls and would clip one outside */
|
|
330
|
+
.t-nav a:focus-visible, .t-nav summary:focus-visible { outline: 2px solid var(--nav-current, var(--accent)); outline-offset: -2px; }
|
|
331
|
+
/* (sprint 72) --nav-* let a style have a dark menu (Iris); without them the menu takes the page's colours.
|
|
332
|
+
(sprint 117) Current page: filled, the accent bar and text. Its parent entries (the trail): bold, the menu's text
|
|
333
|
+
colour with an accent icon, no fill, so the two are told apart at a glance. */
|
|
334
|
+
.t-nav a[aria-current="page"] { background: var(--nav-active); color: var(--nav-current, var(--accent)); font-weight: 600; border-inline-start-color: var(--accent); }
|
|
335
|
+
.t-nav .in-trail { color: var(--nav-text, var(--text)); font-weight: 600; }
|
|
326
336
|
.t-nav .icon { color: var(--nav-muted, var(--muted)); }
|
|
327
337
|
.t-nav a[aria-current="page"] .icon, .t-nav .in-trail .icon { color: var(--nav-current, var(--accent)); }
|
|
328
338
|
|
|
@@ -440,7 +450,8 @@ textarea.code { font-family: var(--mono); font-size: 0.84rem; line-height: 1.45;
|
|
|
440
450
|
.has-error input, .has-error select, .has-error textarea { border-color: var(--danger); }
|
|
441
451
|
.error { color: var(--danger); font-size: 0.82rem; font-weight: 500; }
|
|
442
452
|
.default-submit { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }
|
|
443
|
-
|
|
453
|
+
/* (sprint 117) a lighter processing indicator: a slight dim after 150 ms (no flicker on quick answers), a bar on regions (below) */
|
|
454
|
+
[aria-busy="true"] { opacity: 0.8; cursor: progress; transition: opacity 0.2s ease 0.15s; }
|
|
444
455
|
|
|
445
456
|
/* ---------- buttons ---------- */
|
|
446
457
|
.buttons { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; }
|
|
@@ -902,10 +913,11 @@ pre.source { font-family: var(--mono); font-size: 0.82rem; background: var(--sur
|
|
|
902
913
|
.t-body { grid-template-columns: minmax(0, 1fr); }
|
|
903
914
|
/* Drawer: opened by JS (body.nav-open) or, without JS, by the #t-nav link (:target). */
|
|
904
915
|
.t-nav {
|
|
905
|
-
position: fixed; z-index: 25; top: 52px; bottom: 0;
|
|
916
|
+
position: fixed; z-index: 25; top: 52px; bottom: 0; inset-inline-start: 0; width: min(300px, 85vw);
|
|
906
917
|
box-shadow: 8px 0 24px rgb(0 0 0 / 20%); transition: transform 0.2s ease, visibility 0.2s;
|
|
907
918
|
transform: translateX(-100%); visibility: hidden;
|
|
908
919
|
}
|
|
920
|
+
[dir="rtl"] .t-nav { transform: translateX(100%); box-shadow: -8px 0 24px rgb(0 0 0 / 20%); }
|
|
909
921
|
body.nav-open .t-nav, .t-nav:target { transform: none; visibility: visible; }
|
|
910
922
|
body.nav-open .t-nav-backdrop { display: block; position: fixed; inset: 52px 0 0 0; z-index: 24; background: rgb(15 20 30 / 40%); }
|
|
911
923
|
.t-regions > .region { grid-column: span 12 !important; }
|
|
@@ -950,7 +962,7 @@ pre.source { font-family: var(--mono); font-size: 0.82rem; background: var(--sur
|
|
|
950
962
|
.report-reflow tbody tr:hover { background: var(--surface); }
|
|
951
963
|
.report-reflow td {
|
|
952
964
|
display: grid; grid-template-columns: minmax(90px, 38%) minmax(0, 1fr); gap: 0.75rem;
|
|
953
|
-
border: 0; padding: 0.3rem 0.8rem; white-space: normal; max-width: none; text-align:
|
|
965
|
+
border: 0; padding: 0.3rem 0.8rem; white-space: normal; max-width: none; text-align: start !important; overflow-wrap: anywhere;
|
|
954
966
|
}
|
|
955
967
|
.report-reflow td::before { content: attr(data-label); font-size: 0.8rem; font-weight: 650; color: var(--muted); }
|
|
956
968
|
.report-reflow td.row-edit { width: auto; text-align: left; }
|
|
@@ -992,14 +1004,14 @@ fieldset.prop-group > legend { font-size: 0.75rem; text-transform: uppercase; le
|
|
|
992
1004
|
body.nav-top .t-body { display: block; }
|
|
993
1005
|
body.nav-top .t-nav {
|
|
994
1006
|
position: sticky; top: 52px; z-index: 18; visibility: visible; overflow: visible;
|
|
995
|
-
border-
|
|
1007
|
+
border-inline-end: 0; border-bottom: 1px solid var(--border);
|
|
996
1008
|
}
|
|
997
1009
|
body.nav-top .t-nav > ul { display: flex; gap: 0.25rem; padding: 0.3rem 0.75rem; }
|
|
998
1010
|
body.nav-top .t-nav li { position: relative; }
|
|
999
|
-
body.nav-top .t-nav > ul > li > a[aria-current="page"] { box-shadow: inset 0 -3px 0 var(--accent); }
|
|
1011
|
+
body.nav-top .t-nav > ul > li > a[aria-current="page"] { box-shadow: inset 0 -3px 0 var(--accent); border-inline-start-color: transparent; }
|
|
1000
1012
|
body.nav-top .t-nav details > ul {
|
|
1001
|
-
position: absolute; top: calc(100% + 4px);
|
|
1002
|
-
background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); box-shadow: 0 8px 24px rgb(16 24 40 / 16%);
|
|
1013
|
+
position: absolute; top: calc(100% + 4px); inset-inline-start: 0; margin: 0; min-width: 220px; padding: 0.4rem;
|
|
1014
|
+
background: var(--nav-bg, var(--surface)); border: 1px solid var(--border); border-radius: var(--radius); box-shadow: 0 8px 24px rgb(16 24 40 / 16%);
|
|
1003
1015
|
}
|
|
1004
1016
|
body.nav-top .t-nav details > summary::after { transform: rotate(90deg); }
|
|
1005
1017
|
}
|
|
@@ -2048,3 +2060,105 @@ a.search-hit:hover, a.search-hit:focus-visible { background: var(--accent-soft);
|
|
|
2048
2060
|
.reporter-actions .buttons { margin: 0; }
|
|
2049
2061
|
.reporter-link { flex: 1 1 14rem; min-width: 0; font-family: var(--mono, monospace); font-size: 0.85rem; }
|
|
2050
2062
|
.reporter-coldesc { display: block; font-size: 0.78rem; }
|
|
2063
|
+
|
|
2064
|
+
/* (121) AI agents: links a navigate tool built under an answer; tool arguments in the AI usage log */
|
|
2065
|
+
.assistant-links { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: 0.4rem; }
|
|
2066
|
+
.assistant-links .btn { max-width: 100%; white-space: normal; overflow-wrap: anywhere; }
|
|
2067
|
+
.ai-args { overflow-wrap: anywhere; word-break: break-word; }
|
|
2068
|
+
|
|
2069
|
+
/* (sprint 112) the report's settings as chips under "Ask in your own words" (src/runtime/ai-filter.ts): Edit unfolds a small form */
|
|
2070
|
+
.ask-chips { margin: 0.6rem 0 0; padding: 0; list-style: none; }
|
|
2071
|
+
.ask-chip { flex-wrap: wrap; max-width: 100%; min-width: 0; overflow-wrap: anywhere; padding-right: 0.2rem; }
|
|
2072
|
+
.ask-chip-text { min-width: 0; }
|
|
2073
|
+
.ask-edit > summary { cursor: pointer; font-size: 0.78rem; text-decoration: underline; list-style: none; padding: 0 0.2rem; }
|
|
2074
|
+
.ask-edit > summary::-webkit-details-marker { display: none; }
|
|
2075
|
+
.ask-edit[open] { flex-basis: 100%; }
|
|
2076
|
+
.ask-edit-body { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 0.4rem; padding: 0.4rem 0.2rem 0.3rem; }
|
|
2077
|
+
.ask-field { display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; flex: 1 1 8rem; max-width: 100%; }
|
|
2078
|
+
.ask-field label { font-size: 0.75rem; }
|
|
2079
|
+
.ask-field select, .ask-field input { min-width: 0; width: 100%; box-sizing: border-box; }
|
|
2080
|
+
.ask-chip-x { background: none; border: 0; color: inherit; font-size: 1rem; line-height: 1; padding: 0.15rem 0.4rem; border-radius: 99px; cursor: pointer; }
|
|
2081
|
+
.ask-chip-x:hover, .ask-chip-x:focus-visible { background: var(--surface); }
|
|
2082
|
+
/* (123) workflows: draft and send e-mail steps; custom task outcomes */
|
|
2083
|
+
.wf-send_mail rect { stroke: var(--success); stroke-width: 2; }
|
|
2084
|
+
.wf-draft rect { fill: var(--surface-2, var(--surface)); stroke: var(--muted); stroke-width: 1.5; stroke-dasharray: 6 4; }
|
|
2085
|
+
.wf-draft .wf-name, .wf-draft .wf-type { font-style: italic; }
|
|
2086
|
+
.btn.to-warning { background: var(--warning-soft); border-color: color-mix(in srgb, #b86e00 45%, var(--border)); color: var(--text); }
|
|
2087
|
+
.btn.to-warning:hover { background: color-mix(in srgb, var(--warning-soft) 80%, #b86e00); }
|
|
2088
|
+
.wf-export { display: flex; flex-wrap: wrap; gap: 0.4rem; margin: 0.5rem 0 0; }
|
|
2089
|
+
.wf-migrate-map { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 16rem), 1fr)); gap: 0.5rem 1rem; }
|
|
2090
|
+
.wf-migrate-map label { display: flex; flex-direction: column; gap: 0.2rem; overflow-wrap: anywhere; }
|
|
2091
|
+
.wf-migrate-map select { max-width: 100%; }
|
|
2092
|
+
.wf-problem { color: var(--danger); overflow-wrap: anywhere; }
|
|
2093
|
+
|
|
2094
|
+
/* (sprint 115) the popup LOV's end of list while the next page loads, and the wait popup (dynamic action wait_popup) */
|
|
2095
|
+
.popup-lov-sentinel { margin: 0.5rem 0 0; text-align: center; font-size: 0.875rem; }
|
|
2096
|
+
dialog.wait-popup { border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); color: var(--text); box-shadow: var(--shadow); padding: 1.25rem 1.5rem; max-width: min(360px, calc(100vw - 2rem)); display: none; }
|
|
2097
|
+
dialog.wait-popup[open] { display: flex; flex-direction: column; align-items: center; gap: 0.75rem; }
|
|
2098
|
+
dialog.wait-popup:focus { outline: none; }
|
|
2099
|
+
dialog.wait-popup::backdrop { background: rgb(15 20 30 / 35%); cursor: wait; }
|
|
2100
|
+
.wait-popup-spinner { width: 1.75rem; height: 1.75rem; border-radius: 50%; border: 2.5px solid color-mix(in srgb, var(--accent) 18%, transparent); border-top-color: var(--accent); }
|
|
2101
|
+
.wait-popup-text { margin: 0; text-align: center; overflow-wrap: anywhere; }
|
|
2102
|
+
@media (prefers-reduced-motion: no-preference) { .wait-popup-spinner { animation: icon-spin 0.8s cubic-bezier(0.45, 0.05, 0.55, 0.95) infinite; } }
|
|
2103
|
+
|
|
2104
|
+
/* ---------- (sprint 117, APEX 26.2) Pillar Light / Pillar Dark navigation appearances ----------
|
|
2105
|
+
body.nav-pillar + nav-pillar-light / nav-pillar-dark (theme.appearance or a Theme Roller style's,
|
|
2106
|
+
src/runtime/styles.ts). Header and menu read as one column ("pillar"); a header pillar strip in
|
|
2107
|
+
--strip (Theme Roller), else the accent; the current page is a filled pill in the accent, its
|
|
2108
|
+
parents bold with an accent guide line. Light: the page's own surfaces (dark ones in dark mode).
|
|
2109
|
+
Dark: header and menu in the header colour. Contrast: menu text ≥ 7:1, the pill's text ≥ 4.5:1. */
|
|
2110
|
+
body.nav-pillar .t-header { box-shadow: inset 0 4px 0 var(--strip, var(--accent)); }
|
|
2111
|
+
body.nav-pillar .t-nav a, body.nav-pillar .t-nav summary { border-radius: 999px; }
|
|
2112
|
+
body.nav-pillar .t-nav a[aria-current="page"] { background: var(--accent); color: var(--accent-text); border-inline-start-color: transparent; }
|
|
2113
|
+
body.nav-pillar .t-nav a[aria-current="page"] .icon { color: var(--accent-text); }
|
|
2114
|
+
body.nav-pillar .t-nav a:focus-visible, body.nav-pillar .t-nav summary:focus-visible { outline-color: var(--nav-text); }
|
|
2115
|
+
body.nav-pillar-light {
|
|
2116
|
+
--header: var(--surface-2); --header-text: var(--text);
|
|
2117
|
+
--nav-bg: var(--surface-2); --nav-text: var(--text); --nav-muted: var(--muted);
|
|
2118
|
+
--nav-hover: var(--surface-3); --nav-active: var(--accent-soft); --nav-current: var(--accent);
|
|
2119
|
+
}
|
|
2120
|
+
body.nav-pillar-light .t-header { border-bottom: 1px solid var(--border); }
|
|
2121
|
+
body.nav-pillar-light .t-nav { border-inline-end-color: var(--border); }
|
|
2122
|
+
body.nav-pillar-light .icon-button:hover, body.nav-pillar-light .t-user > summary:hover,
|
|
2123
|
+
body.nav-pillar-light .t-navbar > ul > li > a:hover, body.nav-pillar-light .t-navbar > ul > li > details > summary:hover { background: var(--surface-3); }
|
|
2124
|
+
body.nav-pillar-light .t-navbar a[aria-current="page"] { background: var(--accent-soft); }
|
|
2125
|
+
body.nav-pillar-dark {
|
|
2126
|
+
--nav-bg: var(--header); --nav-text: var(--header-text);
|
|
2127
|
+
--nav-muted: color-mix(in srgb, var(--header-text) 72%, var(--header));
|
|
2128
|
+
--nav-hover: color-mix(in srgb, var(--header) 86%, #ffffff); --nav-active: color-mix(in srgb, var(--header) 76%, #ffffff);
|
|
2129
|
+
--nav-current: var(--header-text);
|
|
2130
|
+
--nav-guide: color-mix(in srgb, var(--header-text) 22%, transparent);
|
|
2131
|
+
}
|
|
2132
|
+
body.nav-pillar-dark .t-nav { border-inline-end-color: transparent; }
|
|
2133
|
+
body.nav-pillar-dark .t-nav .in-trail .icon, body.nav-pillar-dark .t-nav a[aria-current="page"] .icon { color: inherit; }
|
|
2134
|
+
body.nav-pillar-dark .t-nav details:has(> summary.in-trail) > ul { border-inline-start-color: color-mix(in srgb, var(--strip, var(--accent)) 70%, var(--header-text)); }
|
|
2135
|
+
@media (min-width: 1024px) {
|
|
2136
|
+
body.nav-pillar.nav-top .t-nav > ul > li > a[aria-current="page"] { box-shadow: none; }
|
|
2137
|
+
body.nav-pillar-dark.nav-top .t-nav { border-bottom-color: transparent; }
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
/* ---------- (sprint 117, APEX 26.2) subtle colour utilities: a tinted background with readable text,
|
|
2141
|
+
in light and dark mode (text mostly the page's text colour, so contrast holds for every hue).
|
|
2142
|
+
u-color-1-subtle … u-color-15-subtle and u-color-{success,danger,warning,info,neutral}-subtle. */
|
|
2143
|
+
[class*="-subtle"][class*="u-color-"] {
|
|
2144
|
+
background: color-mix(in srgb, var(--u-hue) 14%, var(--surface)); color: color-mix(in srgb, var(--u-hue) 38%, var(--text));
|
|
2145
|
+
border-color: color-mix(in srgb, var(--u-hue) 32%, var(--surface));
|
|
2146
|
+
}
|
|
2147
|
+
.u-color-1-subtle { --u-hue: #309fdb; } .u-color-2-subtle { --u-hue: #13b6cf; } .u-color-3-subtle { --u-hue: #2ebfbc; }
|
|
2148
|
+
.u-color-4-subtle { --u-hue: #3caf85; } .u-color-5-subtle { --u-hue: #81bb5f; } .u-color-6-subtle { --u-hue: #c9cf1f; }
|
|
2149
|
+
.u-color-7-subtle { --u-hue: #f2bf2c; } .u-color-8-subtle { --u-hue: #f18b48; } .u-color-9-subtle { --u-hue: #e95b54; }
|
|
2150
|
+
.u-color-10-subtle { --u-hue: #e85d88; } .u-color-11-subtle { --u-hue: #ca589d; } .u-color-12-subtle { --u-hue: #8f5ea8; }
|
|
2151
|
+
.u-color-13-subtle { --u-hue: #5a68ad; } .u-color-14-subtle { --u-hue: #627c94; } .u-color-15-subtle { --u-hue: #8a7a6b; }
|
|
2152
|
+
.u-color-success-subtle { --u-hue: var(--success); } .u-color-danger-subtle { --u-hue: var(--danger); }
|
|
2153
|
+
.u-color-warning-subtle { --u-hue: #d18b00; } .u-color-info-subtle { --u-hue: var(--accent); } .u-color-neutral-subtle { --u-hue: var(--muted); }
|
|
2154
|
+
|
|
2155
|
+
/* ---------- (sprint 117) the processing bar on a busy region (dynamic actions, refreshes) */
|
|
2156
|
+
.region[aria-busy="true"] { position: relative; }
|
|
2157
|
+
.region[aria-busy="true"]::after {
|
|
2158
|
+
content: ""; position: absolute; z-index: 2; top: 0; inset-inline: 0; height: 3px; border-radius: var(--radius) var(--radius) 0 0; pointer-events: none;
|
|
2159
|
+
background: linear-gradient(90deg, transparent, var(--accent), transparent) 0 0 / 40% 100% no-repeat, color-mix(in srgb, var(--accent) 15%, transparent);
|
|
2160
|
+
}
|
|
2161
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
2162
|
+
.region[aria-busy="true"]::after { animation: busy-slide 1.2s cubic-bezier(0.45, 0.05, 0.55, 0.95) infinite; }
|
|
2163
|
+
}
|
|
2164
|
+
@keyframes busy-slide { from { background-position: -60% 0, 0 0; } to { background-position: 160% 0, 0 0; } }
|