pgkiln 0.53.0 → 0.55.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/SECURITY.md +2 -0
- package/db/migrations/127_builder_language.sql +16 -0
- package/docs/apex-feature-parity.md +7 -7
- package/docs/guide/03-builder.md +62 -8
- package/docs/guide/12-development.md +50 -3
- package/examples/gallery/README.md +37 -6
- package/examples/gallery/gallery.json +23 -1
- package/examples/gallery/samples/sample-calendar.sql +172 -0
- package/examples/gallery/samples/sample-cards.sql +157 -0
- package/examples/gallery/samples/sample-charts.sql +265 -0
- package/examples/gallery/samples/sample-dynamic-actions.sql +177 -0
- package/examples/gallery/samples/sample-grids.sql +151 -0
- package/examples/gallery/samples/sample-reporting.sql +151 -0
- package/examples/gallery/samples/ux-patterns.sql +295 -0
- package/examples/gallery/utilities/announcements.json +24 -0
- package/examples/gallery/utilities/lookup-lists.json +28 -0
- package/examples/gallery/utilities/release-notes.json +41 -0
- package/package.json +1 -1
- package/public/app.css +2 -0
- package/public/builder.css +39 -1
- package/public/worksheet.css +3 -2
- package/src/builder/advisor.ts +1 -1
- package/src/builder/ai-agents.ts +5 -5
- package/src/builder/ai-builder.ts +3 -3
- package/src/builder/api.ts +1 -1
- package/src/builder/autorest.ts +2 -2
- package/src/builder/availability.ts +1 -1
- package/src/builder/backups.ts +1 -1
- package/src/builder/changelog.ts +1 -1
- package/src/builder/data-modeler.ts +4 -4
- package/src/builder/data-security.ts +1 -1
- package/src/builder/dataload.ts +1 -1
- package/src/builder/datapump.ts +1 -1
- package/src/builder/ddl-tools.ts +2 -2
- package/src/builder/designer.ts +134 -124
- package/src/builder/diagnostics.ts +2 -2
- package/src/builder/errors.ts +10 -12
- package/src/builder/feedback.ts +1 -1
- package/src/builder/gallery.ts +94 -47
- package/src/builder/globalization.ts +1 -1
- package/src/builder/home.ts +59 -57
- package/src/builder/i18n/de.ts +675 -0
- package/src/builder/i18n/en.ts +678 -0
- package/src/builder/i18n/es.ts +675 -0
- package/src/builder/i18n/fr.ts +675 -0
- package/src/builder/i18n/nl.ts +675 -0
- package/src/builder/i18n/pt.ts +675 -0
- package/src/builder/i18n.ts +116 -0
- package/src/builder/language.ts +74 -0
- package/src/builder/launchpad.ts +13 -8
- package/src/builder/object-browser.ts +1 -1
- package/src/builder/plugins.ts +1 -1
- package/src/builder/querybuilder.ts +1 -1
- package/src/builder/quicksql.ts +1 -1
- package/src/builder/rest-modules.ts +2 -2
- package/src/builder/routes.ts +88 -66
- package/src/builder/sampledata.ts +1 -1
- package/src/builder/scheduling.ts +1 -1
- package/src/builder/scripts.ts +1 -1
- package/src/builder/search.ts +1 -1
- package/src/builder/shared.ts +5 -5
- package/src/builder/sql-charts.ts +2 -2
- package/src/builder/sql-monitor.ts +2 -2
- package/src/builder/sql-tools.ts +6 -0
- package/src/builder/sql-utilities.ts +3 -3
- package/src/builder/sql.ts +24 -22
- package/src/builder/static-files.ts +1 -1
- package/src/builder/subscriptions.ts +1 -1
- package/src/builder/supporting.ts +1 -1
- package/src/builder/table-editor.ts +1 -1
- package/src/builder/team.ts +7 -5
- package/src/builder/themeroller.ts +1 -1
- package/src/builder/top-sql.ts +1 -1
- package/src/builder/ui.ts +87 -77
- package/src/builder/unload.ts +1 -1
- package/src/builder/wizards.ts +1 -1
- package/src/builder/workflow-migrate.ts +1 -1
- package/src/builder/workingcopies.ts +2 -2
- package/src/builder/worksheet.ts +1 -1
- package/src/builder/workspace-home.ts +62 -59
- package/src/builder/workspaces.ts +2 -1
- package/src/runtime/charts.ts +4 -1
- package/src/runtime/regions.ts +8 -5
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
-- =====================================================================
|
|
2
|
+
-- Sample app "UX Pattern Catalog" (App Gallery → Sample Apps)
|
|
3
|
+
--
|
|
4
|
+
-- The look of pgkiln's theme in one place, to copy from:
|
|
5
|
+
-- page 2 region templates (standard, plain, collapsible, title bar,
|
|
6
|
+
-- buttons container, hero, inline dialog and drawer, wizard,
|
|
7
|
+
-- carousel);
|
|
8
|
+
-- page 3 region template options;
|
|
9
|
+
-- page 4 buttons: hot, sizes, shapes, colours, a menu and a badge;
|
|
10
|
+
-- page 5 items and form layouts, with the item template options;
|
|
11
|
+
-- page 6 content patterns: alerts, badges and tags, colour classes,
|
|
12
|
+
-- text styles, lists, media lists, timelines, comments, avatars,
|
|
13
|
+
-- metric cards and empty states;
|
|
14
|
+
-- page 7 page patterns: a modal dialog (page 8), a drawer (page 9) and a
|
|
15
|
+
-- page with a side column (page 10).
|
|
16
|
+
-- Every page says which template, option or class it uses.
|
|
17
|
+
--
|
|
18
|
+
-- Installed by an administrator from the App Gallery, or with
|
|
19
|
+
-- psql "$DATABASE_URL" -1 -f examples/gallery/samples/ux-patterns.sql
|
|
20
|
+
-- then open /a/ux-patterns and sign in as demo / demo.
|
|
21
|
+
-- =====================================================================
|
|
22
|
+
|
|
23
|
+
create or replace function pg_temp.page_id(p_alias text, p_page int) returns int language sql stable as
|
|
24
|
+
$$ select p.id from meta.page p join meta.app a on a.id = p.app_id where a.alias = p_alias and p.page_no = p_page $$;
|
|
25
|
+
create or replace function pg_temp.region_id(p_alias text, p_page int, p_static text) returns int language sql stable as
|
|
26
|
+
$$ select r.id from meta.region r where r.page_id = pg_temp.page_id(p_alias, p_page) and r.static_id = p_static $$;
|
|
27
|
+
|
|
28
|
+
-- 1. Schema and data ---------------------------------------------------------
|
|
29
|
+
create schema ux_patterns;
|
|
30
|
+
|
|
31
|
+
create table ux_patterns.person (
|
|
32
|
+
id int generated always as identity primary key,
|
|
33
|
+
name text not null,
|
|
34
|
+
role text not null,
|
|
35
|
+
status text not null check (status in ('Active', 'Away', 'Left')),
|
|
36
|
+
joined date not null,
|
|
37
|
+
note text
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
insert into ux_patterns.person (name, role, status, joined, note) values
|
|
41
|
+
('Alex Morgan', 'Product owner', 'Active', current_date - 400, 'Sets the priorities of the team.'),
|
|
42
|
+
('Sam Lee', 'Developer', 'Active', current_date - 200, 'Builds the reports.'),
|
|
43
|
+
('Robin Diaz', 'Designer', 'Away', current_date - 90, 'Back next week.'),
|
|
44
|
+
('Kim Novak', 'Tester', 'Active', current_date - 30, 'Writes the browser tests.'),
|
|
45
|
+
('Jo Park', 'Analyst', 'Left', current_date - 700, 'Moved to another team.');
|
|
46
|
+
|
|
47
|
+
-- 2. The application's database role ----------------------------------------
|
|
48
|
+
do $$ begin
|
|
49
|
+
if not exists (select 1 from pg_roles where rolname = 'app_ux_patterns') then create role app_ux_patterns nologin; end if;
|
|
50
|
+
end $$;
|
|
51
|
+
grant app_ux_patterns to pgkiln_runtime;
|
|
52
|
+
grant usage on schema ux_patterns to app_ux_patterns;
|
|
53
|
+
grant select on all tables in schema ux_patterns to app_ux_patterns;
|
|
54
|
+
|
|
55
|
+
-- 3. The application ----------------------------------------------------------
|
|
56
|
+
insert into meta.app (alias, name, home_page, authentication, db_role)
|
|
57
|
+
values ('ux-patterns', 'UX Pattern Catalog', 1, 'app_users', 'app_ux_patterns');
|
|
58
|
+
|
|
59
|
+
insert into meta.app_user (app_id, username, password_hash, roles)
|
|
60
|
+
select id, 'demo', meta.hash_password('demo'), '{}' from meta.app where alias = 'ux-patterns';
|
|
61
|
+
|
|
62
|
+
insert into meta.page (app_id, page_no, name, title, parent_page, mode, dialog_position, dialog_size, page_template)
|
|
63
|
+
select a.id, p.no, p.name, p.name, p.parent, p.mode, p.pos, p.size, p.tpl
|
|
64
|
+
from meta.app a, (values
|
|
65
|
+
(1, 'UX Pattern Catalog', null, 'normal', 'center', 'medium', 'standard'),
|
|
66
|
+
(2, 'Region templates', 1, 'normal', 'center', 'medium', 'standard'),
|
|
67
|
+
(3, 'Region options', 1, 'normal', 'center', 'medium', 'standard'),
|
|
68
|
+
(4, 'Buttons', 1, 'normal', 'center', 'medium', 'standard'),
|
|
69
|
+
(5, 'Items and forms', 1, 'normal', 'center', 'medium', 'standard'),
|
|
70
|
+
(6, 'Content patterns', 1, 'normal', 'center', 'medium', 'standard'),
|
|
71
|
+
(7, 'Page patterns', 1, 'normal', 'center', 'medium', 'standard'),
|
|
72
|
+
(8, 'A modal dialog', 7, 'modal', 'center', 'small', 'standard'),
|
|
73
|
+
(9, 'A drawer', 7, 'modal', 'right', 'medium', 'standard'),
|
|
74
|
+
(10, 'A side column', 7, 'normal', 'center', 'medium', 'left_side_column')
|
|
75
|
+
) as p (no, name, parent, mode, pos, size, tpl)
|
|
76
|
+
where a.alias = 'ux-patterns';
|
|
77
|
+
|
|
78
|
+
insert into meta.nav_entry (app_id, seq, label, icon, target_page)
|
|
79
|
+
select a.id, n.seq, n.label, n.icon, n.page
|
|
80
|
+
from meta.app a, (values
|
|
81
|
+
(10, 'Home', 'home', 1), (20, 'Region templates', 'layers', 2), (30, 'Region options', 'sliders', 3), (40, 'Buttons', 'target', 4),
|
|
82
|
+
(50, 'Items and forms', 'edit', 5), (60, 'Content patterns', 'grid', 6), (70, 'Page patterns', 'file', 7)
|
|
83
|
+
) as n (seq, label, icon, page)
|
|
84
|
+
where a.alias = 'ux-patterns';
|
|
85
|
+
|
|
86
|
+
insert into meta.list (app_id, name, type, description)
|
|
87
|
+
select a.id, l.name, 'static', l.description
|
|
88
|
+
from meta.app a, (values
|
|
89
|
+
('UX_TOPICS', 'The pages of the catalog'),
|
|
90
|
+
('UX_STEPS', 'Steps of a wizard (wizard progress on page 2)'),
|
|
91
|
+
('UX_SHORTCUTS', 'A small list for the list templates on page 6')
|
|
92
|
+
) as l (name, description)
|
|
93
|
+
where a.alias = 'ux-patterns';
|
|
94
|
+
|
|
95
|
+
insert into meta.list_entry (app_id, list_name, seq, label, icon, target_page, badge, description)
|
|
96
|
+
select a.id, e.list, e.seq, e.label, e.icon, e.page, e.badge, e.description
|
|
97
|
+
from meta.app a, (values
|
|
98
|
+
('UX_TOPICS', 10, 'Region templates', 'layers', 2, null, 'Standard, plain, collapsible, title bar, buttons container, hero, inline dialog and drawer, wizard, carousel.'),
|
|
99
|
+
('UX_TOPICS', 20, 'Region options', 'sliders', 3, null, 'Accent border, flat, borderless, compact, no padding, scrolling, stretched, centred, hidden header.'),
|
|
100
|
+
('UX_TOPICS', 30, 'Buttons', 'target', 4, null, 'Hot and normal, small and large, pill, outline, link, full width, success and danger, a menu and a badge.'),
|
|
101
|
+
('UX_TOPICS', 40, 'Items and forms', 'edit', 5, null, 'Every common item type in a form, and the item template options.'),
|
|
102
|
+
('UX_TOPICS', 50, 'Content patterns', 'grid', 6, null, 'Alerts, badges, colours, text, lists, media lists, timelines, comments, avatars, metrics and empty states.'),
|
|
103
|
+
('UX_TOPICS', 60, 'Page patterns', 'file', 7, null, 'A modal dialog, a drawer and a page with a side column.'),
|
|
104
|
+
('UX_STEPS', 10, 'Choose', null, 2, null, null),
|
|
105
|
+
('UX_STEPS', 20, 'Details', null, 5, null, null),
|
|
106
|
+
('UX_STEPS', 30, 'Confirm', null, 7, null, null),
|
|
107
|
+
('UX_SHORTCUTS', 10, 'Buttons', 'target', 4, '9', 'Every button style.'),
|
|
108
|
+
('UX_SHORTCUTS', 20, 'Items', 'edit', 5, '18', 'Every common item type.'),
|
|
109
|
+
('UX_SHORTCUTS', 30, 'Pages', 'file', 7, '3', 'Dialogs, drawers and side columns.')
|
|
110
|
+
) as e (list, seq, label, icon, page, badge, description)
|
|
111
|
+
where a.alias = 'ux-patterns';
|
|
112
|
+
|
|
113
|
+
insert into meta.region (page_id, seq, title, type, columns, template, template_options, source, config, static_id, position)
|
|
114
|
+
select pg_temp.page_id('ux-patterns', r.page), r.seq, r.title, r.type, r.cols, r.template, r.topts::text[], r.source, r.config::jsonb, r.sid, r.pos
|
|
115
|
+
from (values
|
|
116
|
+
-- 1 Home
|
|
117
|
+
(1, 10, 'UX Pattern Catalog', 'static', 12, 'hero', '{}',
|
|
118
|
+
'<p>The building blocks of pgkiln''s theme, each with the name of the template, option or class that makes it. Open a page in the Page Designer to see how it is made, or copy the HTML of a static region. Everything follows the application''s style (Theme Roller), light and dark mode, and fits a phone.</p>',
|
|
119
|
+
'{"icon": "layers"}', null, null),
|
|
120
|
+
(1, 20, 'Topics', 'list', 12, 'plain', '{}', null, '{"list": "UX_TOPICS", "template": "cards"}', null, null),
|
|
121
|
+
|
|
122
|
+
-- 2 Region templates
|
|
123
|
+
(2, 5, 'About this page', 'static', 12, 'plain', '{}',
|
|
124
|
+
'<p class="lead">A region''s <strong>template</strong> (Page Designer → region → Appearance) sets its frame. The title of each region below names its template.</p>',
|
|
125
|
+
'{}', null, null),
|
|
126
|
+
(2, 10, 'Standard', 'static', 4, 'standard', '{}', '<p>A card with a header (title and buttons) and a body. The default.</p>', '{}', null, null),
|
|
127
|
+
(2, 20, 'Plain', 'static', 4, 'plain', '{}', '<p><strong>Plain</strong>: no frame and no header, the content only. Good for introductions and KPI tiles.</p>', '{}', null, null),
|
|
128
|
+
(2, 30, 'Collapsible', 'static', 4, 'collapsible', '{}', '<p>A standard card whose body folds away when you click its header.</p>', '{}', null, null),
|
|
129
|
+
(2, 40, 'Title bar', 'static', 12, 'title_bar', '{}', '<p class="muted">The title and the buttons on one line over the content, without a card.</p>', '{}', 'title-bar', null),
|
|
130
|
+
(2, 50, 'Hero', 'static', 12, 'hero', '{}', '<p>A large banner with an icon (the attribute <code>icon</code>), the title, a text and buttons: the start of a home page.</p>', '{"icon": "star"}', null, null),
|
|
131
|
+
(2, 60, 'Buttons container', 'static', 12, 'buttons_container', '{}', '<p class="muted">Buttons only, after any text: the inline dialog and drawer open from here.</p>', '{}', 'buttons', null),
|
|
132
|
+
(2, 70, 'Inline dialog', 'static', 12, 'inline_dialog', '{}', '<p>Hidden until a dynamic action <em>Open region</em> (or a link to its id) opens it. Escape closes it.</p>', '{}', 'dialog', null),
|
|
133
|
+
(2, 80, 'Inline drawer', 'static', 12, 'inline_drawer', '{}', '<p>The same as an inline dialog, docked to the right edge of the screen.</p>', '{}', 'drawer', null),
|
|
134
|
+
(2, 90, 'Carousel', 'display_selector', 12, 'plain', '{}', null, '{"style": "carousel", "interval": 8, "show_all": false}', null, null),
|
|
135
|
+
(2, 91, 'First slide', 'static', 12, 'standard', '{}', '<p>A display selector with <code>"style": "carousel"</code> shows one region at a time and turns every few seconds.</p>', '{"display_selector": true}', null, null),
|
|
136
|
+
(2, 92, 'Second slide', 'static', 12, 'standard', '{}', '<p>Its slides are the regions with <code>"display_selector": true</code> after it.</p>', '{"display_selector": true}', null, null),
|
|
137
|
+
(2, 93, 'Third slide', 'static', 12, 'standard', '{}', '<p>Without the carousel style, the display selector shows tabs.</p>', '{"display_selector": true}', null, null),
|
|
138
|
+
(2, 100, 'Wizard progress', 'list', 12, 'plain', '{}', null, '{"list": "UX_STEPS", "template": "wizard_progress"}', null, null),
|
|
139
|
+
(2, 110, 'Wizard', 'static', 12, 'wizard', '{}', '<p>A narrow, centred card for one step of a wizard, under a list with the template <code>wizard_progress</code>.</p>', '{}', null, null),
|
|
140
|
+
|
|
141
|
+
-- 3 Region options
|
|
142
|
+
(3, 5, 'About this page', 'static', 12, 'plain', '{}',
|
|
143
|
+
'<p class="lead"><strong>Template options</strong> (Page Designer → region → Appearance) add classes from a fixed list to a region. They combine: a region can be compact and flat at once.</p>',
|
|
144
|
+
'{}', null, null),
|
|
145
|
+
(3, 10, 'Accent top border (to-accent)', 'static', 4, 'standard', '{to-accent}', '<p>A top border in the accent colour.</p>', '{}', null, null),
|
|
146
|
+
(3, 20, 'Flat (to-flat)', 'static', 4, 'standard', '{to-flat}', '<p>No shadow.</p>', '{}', null, null),
|
|
147
|
+
(3, 30, 'No border or background (to-borderless)', 'static', 4, 'standard', '{to-borderless}', '<p>Transparent, no border or shadow.</p>', '{}', null, null),
|
|
148
|
+
(3, 40, 'Compact (to-compact)', 'static', 4, 'standard', '{to-compact}', '<p>Less padding in the header and body.</p>', '{}', null, null),
|
|
149
|
+
(3, 50, 'Centred text (to-center)', 'static', 4, 'standard', '{to-center}', '<p>The text of the body is centred.</p>', '{}', null, null),
|
|
150
|
+
(3, 60, 'Hidden header (to-hide-header)', 'static', 4, 'standard', '{to-hide-header}', '<p>The header is hidden but still read by screen readers.</p>', '{}', null, null),
|
|
151
|
+
(3, 70, 'Scrolling body (to-scroll)', 'static', 6, 'standard', '{to-scroll,to-stretch}',
|
|
152
|
+
'<p>The body is at most 24 rem high and scrolls.</p><p>Line two.</p><p>Line three.</p><p>Line four.</p><p>Line five.</p><p>Line six.</p><p>Line seven.</p><p>Line eight.</p><p>Line nine.</p><p>Line ten.</p><p>Line eleven.</p><p>Line twelve.</p><p>Line thirteen.</p><p>Line fourteen.</p>',
|
|
153
|
+
'{}', null, null),
|
|
154
|
+
(3, 80, 'Stretched to the row height (to-stretch)', 'static', 6, 'standard', '{to-stretch}', '<p>Regions side by side get the same height: this one is as high as the scrolling region beside it.</p>', '{}', null, null),
|
|
155
|
+
(3, 90, 'No body padding (to-no-padding)', 'report', 12, 'standard', '{to-no-padding}',
|
|
156
|
+
E'select name, role, status from ux_patterns.person order by name', '{"searchable": false}', null, null),
|
|
157
|
+
|
|
158
|
+
-- 4 Buttons
|
|
159
|
+
(4, 5, 'About this page', 'static', 12, 'plain', '{}',
|
|
160
|
+
'<p class="lead">A button is <strong>hot</strong> (the main action) or normal; its <strong>template options</strong> set the size, shape and colour. A button with action <em>Menu</em> opens a list of links, and a <strong>badge</strong> shows a short value on it. These buttons do nothing: their action is <em>Defined by dynamic action</em> and none is defined.</p>',
|
|
161
|
+
'{}', null, null),
|
|
162
|
+
(4, 10, 'Hot and normal', 'static', 6, 'standard', '{}', null, '{}', 'b-kind', null),
|
|
163
|
+
(4, 20, 'Sizes', 'static', 6, 'standard', '{}', null, '{}', 'b-size', null),
|
|
164
|
+
(4, 30, 'Shapes', 'static', 6, 'standard', '{}', null, '{}', 'b-shape', null),
|
|
165
|
+
(4, 40, 'Colours', 'static', 6, 'standard', '{}', null, '{}', 'b-colour', null),
|
|
166
|
+
(4, 50, 'Full width (to-block)', 'static', 6, 'standard', '{}', null, '{}', 'b-block', null),
|
|
167
|
+
(4, 60, 'Menu and badge', 'static', 6, 'standard', '{}', null, '{}', 'b-menu', null),
|
|
168
|
+
|
|
169
|
+
-- 5 Items and forms
|
|
170
|
+
(5, 5, 'About this page', 'static', 12, 'plain', '{}',
|
|
171
|
+
'<p class="lead">Items in a region form a grid of fields: two per row on wide screens, one on a phone. A textarea, rich text and items with <code>"wide": true</code> or the option <code>to-stretch</code> take the whole row. Required items are marked with a <strong>*</strong>; the help text goes under the field. Nothing here is saved.</p>',
|
|
172
|
+
'{}', null, null),
|
|
173
|
+
(5, 10, 'Text and numbers', 'static', 12, 'standard', '{}', null, '{}', 'f-text', null),
|
|
174
|
+
(5, 20, 'Choices', 'static', 12, 'standard', '{}', null, '{}', 'f-choice', null),
|
|
175
|
+
(5, 30, 'Dates and more', 'static', 12, 'standard', '{}', null, '{}', 'f-date', null),
|
|
176
|
+
(5, 40, 'Item template options', 'static', 12, 'standard', '{}', null, '{}', 'f-options', null),
|
|
177
|
+
|
|
178
|
+
-- 6 Content patterns
|
|
179
|
+
(6, 5, 'About this page', 'static', 12, 'plain', '{}',
|
|
180
|
+
'<p class="lead">Patterns for content. The first regions are static HTML with the theme''s classes (the content security policy allows no inline styles, so classes are the way to style); the others are list and template component regions on a query.</p>',
|
|
181
|
+
'{}', null, null),
|
|
182
|
+
(6, 10, 'Alerts', 'static', 6, 'standard', '{}',
|
|
183
|
+
'<div class="alert alert-success u-mb1">Saved: <code>alert alert-success</code>.</div><div class="alert alert-warning u-mb1">Check this: <code>alert alert-warning</code>.</div><div class="alert alert-error u-mb1">Something went wrong: <code>alert alert-error</code>.</div>',
|
|
184
|
+
'{}', null, null),
|
|
185
|
+
(6, 20, 'Tags and colour classes', 'static', 6, 'standard', '{}',
|
|
186
|
+
'<p><span class="tag">tag</span> <span class="tag tag-info">tag-info</span> <span class="tag tag-warning">tag-warning</span> <span class="tag tag-error">tag-error</span></p><p><span class="tc-badge tc-badge-success">success</span> <span class="tc-badge tc-badge-info">info</span> <span class="tc-badge tc-badge-warning">warning</span> <span class="tc-badge tc-badge-danger">danger</span> <span class="tc-badge tc-badge-neutral">neutral</span></p><p><span class="u-color-1-subtle">u-color-1-subtle</span> <span class="u-color-4-subtle">u-color-4-subtle</span> <span class="u-color-7-subtle">u-color-7-subtle</span> <span class="u-color-success-subtle">u-color-success-subtle</span> <span class="u-color-warning-subtle">u-color-warning-subtle</span></p>',
|
|
187
|
+
'{}', null, null),
|
|
188
|
+
(6, 30, 'Text', 'static', 12, 'standard', '{}',
|
|
189
|
+
'<p class="lead">A lead paragraph (<code>lead</code>) introduces a page.</p><p>Body text, with <strong>bold</strong>, <em>emphasis</em>, <code>code</code> and a <a href="#">link</a>.</p><p class="muted">Muted text (<code>muted</code>) for things of less importance.</p>',
|
|
190
|
+
'{}', null, null),
|
|
191
|
+
(6, 40, 'List as links', 'list', 4, 'standard', '{}', null, '{"list": "UX_SHORTCUTS", "template": "links"}', null, null),
|
|
192
|
+
(6, 50, 'List as badges', 'list', 8, 'standard', '{}', null, '{"list": "UX_SHORTCUTS", "template": "badges"}', null, null),
|
|
193
|
+
(6, 60, 'List as tabs', 'list', 12, 'standard', '{}', null, '{"list": "UX_SHORTCUTS", "template": "tabs"}', null, null),
|
|
194
|
+
(6, 70, 'Media list (ut_media_list)', 'template_component', 6, 'standard', '{}',
|
|
195
|
+
E'select name as title, role || '' · '' || status as description, upper(left(name, 1) || substr(name, strpos(name, '' '') + 1, 1)) as initials,\n status as badge, case status when ''Active'' then ''success'' when ''Away'' then ''warning'' else ''neutral'' end as state\n from ux_patterns.person order by name',
|
|
196
|
+
'{"component": "ut_media_list", "display": "multiple"}', null, null),
|
|
197
|
+
(6, 80, 'Timeline (ut_timeline)', 'template_component', 6, 'standard', '{}',
|
|
198
|
+
E'select name || '' joined as '' || lower(role) as title, to_char(joined, ''YYYY-MM-DD'') as "when", status as who, note as body,\n case status when ''Active'' then ''success'' when ''Away'' then ''warning'' else ''neutral'' end as state\n from ux_patterns.person order by joined desc',
|
|
199
|
+
'{"component": "ut_timeline", "display": "multiple"}', null, null),
|
|
200
|
+
(6, 90, 'Comments (ut_comments)', 'template_component', 6, 'standard', '{}',
|
|
201
|
+
E'select name as "user", upper(left(name, 1) || substr(name, strpos(name, '' '') + 1, 1)) as initials, to_char(joined, ''YYYY-MM-DD'') as date, note as comment, role as actions\n from ux_patterns.person where status <> ''Left'' order by joined desc',
|
|
202
|
+
'{"component": "ut_comments", "display": "multiple"}', null, null),
|
|
203
|
+
(6, 100, 'Avatars (ut_avatar)', 'template_component', 6, 'standard', '{}',
|
|
204
|
+
E'select name, upper(left(name, 1) || substr(name, strpos(name, '' '') + 1, 1)) as initials from ux_patterns.person order by name',
|
|
205
|
+
'{"component": "ut_avatar", "display": "multiple", "attributes": {"SIZE": "large"}}', null, null),
|
|
206
|
+
(6, 110, 'Metric cards (ut_metric_card)', 'template_component', 12, 'standard', '{}',
|
|
207
|
+
E'select ''People'' as label, count(*)::text as value, null as unit, count(*) filter (where joined > current_date - 365) || '' joined this year'' as change, ''up'' as trend, ''up'' as good from ux_patterns.person\nunion all\nselect ''Away'', count(*) filter (where status = ''Away'')::text, ''people'', null, ''flat'', ''down'' from ux_patterns.person\nunion all\nselect ''Left'', count(*) filter (where status = ''Left'')::text, ''people'', ''one more than last year'', ''up'', ''down'' from ux_patterns.person',
|
|
208
|
+
'{"component": "ut_metric_card", "display": "multiple"}', null, null),
|
|
209
|
+
(6, 120, 'KPI tiles (cards, style metric)', 'cards', 12, 'plain', '{}',
|
|
210
|
+
E'select status as title, count(*)::text as badge, case status when ''Active'' then ''check-circle'' when ''Away'' then ''clock'' else ''logout'' end as icon\n from ux_patterns.person group by status order by status',
|
|
211
|
+
'{"style": "metric"}', null, null),
|
|
212
|
+
(6, 130, 'Empty state of a report', 'report', 6, 'standard', '{}',
|
|
213
|
+
E'select name, role from ux_patterns.person where false', '{"searchable": false, "empty": "Nobody here yet: a report with no rows shows its empty text."}', null, null),
|
|
214
|
+
(6, 140, 'Empty state of cards', 'cards', 6, 'standard', '{}',
|
|
215
|
+
E'select name as title from ux_patterns.person where false', '{"empty": "No cards to show: a cards region with no rows shows its empty text."}', null, null),
|
|
216
|
+
|
|
217
|
+
-- 7 Page patterns
|
|
218
|
+
(7, 5, 'About this page', 'static', 12, 'plain', '{}',
|
|
219
|
+
'<p class="lead">A page''s <strong>mode</strong> and <strong>dialog position</strong> make it a modal dialog or a drawer; its <strong>page template</strong> can add a side column. The buttons open examples of each.</p>',
|
|
220
|
+
'{}', null, null),
|
|
221
|
+
(7, 10, 'Dialogs, drawers and columns', 'static', 12, 'buttons_container', '{}', null, '{}', 'p-buttons', null),
|
|
222
|
+
(8, 10, 'A modal dialog', 'static', 12, 'plain', '{}',
|
|
223
|
+
'<p>This page has the mode <em>Modal dialog</em> with the size <em>small</em>. It opens over the page that linked to it; on a phone it fills the screen. Opened directly, it is an ordinary page.</p>',
|
|
224
|
+
'{}', null, null),
|
|
225
|
+
(9, 10, 'A drawer', 'static', 12, 'plain', '{}',
|
|
226
|
+
'<p>This page is a modal dialog with the position <em>right</em>: a drawer that slides in from the right edge. Top, bottom and left drawers work the same way.</p>',
|
|
227
|
+
'{}', null, null),
|
|
228
|
+
(10, 10, 'The side column', 'static', 12, 'standard', '{}',
|
|
229
|
+
'<p>This region has the position <code>left</code>. On a page with the template <em>Left side column</em> it shows beside the body; on a narrow screen above it.</p>',
|
|
230
|
+
'{}', null, 'left'),
|
|
231
|
+
(10, 20, 'The body', 'static', 12, 'standard', '{}',
|
|
232
|
+
'<p>The regions of the body use the 12-column grid as usual. A faceted search beside its report is the typical use of a side column.</p>',
|
|
233
|
+
'{}', null, null)
|
|
234
|
+
) as r (page, seq, title, type, cols, template, topts, source, config, sid, pos);
|
|
235
|
+
|
|
236
|
+
-- buttons
|
|
237
|
+
insert into meta.button (page_id, region_id, seq, name, label, action, hot, target_page, template_options, menu, badge)
|
|
238
|
+
select pg_temp.page_id('ux-patterns', b.page), pg_temp.region_id('ux-patterns', b.page, b.region), b.seq, b.name, b.label, b.action, b.hot, b.target,
|
|
239
|
+
b.topts::text[], b.menu::jsonb, b.badge
|
|
240
|
+
from (values
|
|
241
|
+
(2, 'title-bar', 10, 'TB_EDIT', 'Edit', 'da', false, null, '{}', null, null),
|
|
242
|
+
(2, 'title-bar', 20, 'TB_ADD', 'Add', 'da', true, null, '{}', null, null),
|
|
243
|
+
(2, 'buttons', 10, 'OPEN_DIALOG', 'Open the inline dialog', 'da', false, null, '{}', null, null),
|
|
244
|
+
(2, 'buttons', 20, 'OPEN_DRAWER', 'Open the inline drawer', 'da', false, null, '{}', null, null),
|
|
245
|
+
(4, 'b-kind', 10, 'NORMAL', 'Normal', 'da', false, null, '{}', null, null),
|
|
246
|
+
(4, 'b-kind', 20, 'HOT', 'Hot', 'da', true, null, '{}', null, null),
|
|
247
|
+
(4, 'b-size', 10, 'SMALL', 'Small (to-small)', 'da', false, null, '{to-small}', null, null),
|
|
248
|
+
(4, 'b-size', 20, 'MEDIUM', 'Normal', 'da', false, null, '{}', null, null),
|
|
249
|
+
(4, 'b-size', 30, 'LARGE', 'Large (to-large)', 'da', false, null, '{to-large}', null, null),
|
|
250
|
+
(4, 'b-shape', 10, 'PILL', 'Pill (to-pill)', 'da', false, null, '{to-pill}', null, null),
|
|
251
|
+
(4, 'b-shape', 20, 'OUTLINE', 'Outline (to-outline)', 'da', false, null, '{to-outline}', null, null),
|
|
252
|
+
(4, 'b-shape', 30, 'LINK', 'Link (to-link)', 'da', false, null, '{to-link}', null, null),
|
|
253
|
+
(4, 'b-colour', 10, 'SUCCESS', 'Success (to-success)', 'da', false, null, '{to-success}', null, null),
|
|
254
|
+
(4, 'b-colour', 20, 'DANGER', 'Danger (to-danger)', 'da', false, null, '{to-danger}', null, null),
|
|
255
|
+
(4, 'b-colour', 30, 'HOT_PILL', 'Hot pill', 'da', true, null, '{to-pill}', null, null),
|
|
256
|
+
(4, 'b-block', 10, 'BLOCK', 'A full-width button', 'da', true, null, '{to-block}', null, null),
|
|
257
|
+
(4, 'b-menu', 10, 'MENU', 'Go to', 'menu', false, null, '{}',
|
|
258
|
+
'[{"label": "Region templates", "page": 2, "icon": "layers"}, {"label": "Items and forms", "page": 5, "icon": "edit"}, {"label": "Content patterns", "page": 6, "icon": "grid"}]', null),
|
|
259
|
+
(4, 'b-menu', 20, 'INBOX', 'Inbox', 'da', false, null, '{}', null, '3'),
|
|
260
|
+
(7, 'p-buttons', 10, 'MODAL', 'Open a modal dialog', 'redirect', true, 8, '{}', null, null),
|
|
261
|
+
(7, 'p-buttons', 20, 'DRAWER', 'Open a drawer', 'redirect', false, 9, '{}', null, null),
|
|
262
|
+
(7, 'p-buttons', 30, 'SIDE', 'A page with a side column', 'redirect', false, 10, '{}', null, null)
|
|
263
|
+
) as b (page, region, seq, name, label, action, hot, target, topts, menu, badge);
|
|
264
|
+
|
|
265
|
+
insert into meta.dynamic_action (page_id, seq, name, event, trigger_element, action, affected_region_id)
|
|
266
|
+
select pg_temp.page_id('ux-patterns', 2), d.seq, d.name, 'click', d.button, 'open_region', pg_temp.region_id('ux-patterns', 2, d.region)
|
|
267
|
+
from (values (10, 'Open the inline dialog', 'OPEN_DIALOG', 'dialog'), (20, 'Open the inline drawer', 'OPEN_DRAWER', 'drawer')) as d (seq, name, button, region);
|
|
268
|
+
|
|
269
|
+
-- items
|
|
270
|
+
insert into meta.item (page_id, region_id, seq, name, label, type, lov, default_value, required, help, config, template_options)
|
|
271
|
+
select pg_temp.page_id('ux-patterns', 5), pg_temp.region_id('ux-patterns', 5, i.region), i.seq, i.name, i.label, i.type, i.lov, i.def, i.req, i.help,
|
|
272
|
+
i.config::jsonb, i.topts::text[]
|
|
273
|
+
from (values
|
|
274
|
+
('f-text', 10, 'P5_NAME', 'Name (text, required)', 'text', null, null, true, 'Required items are marked with a *.', '{}', '{}'),
|
|
275
|
+
('f-text', 20, 'P5_EMAIL', 'E-mail (email)', 'email', null, null, false, null, '{}', '{}'),
|
|
276
|
+
('f-text', 30, 'P5_AMOUNT', 'Amount (number)', 'number', null, '1250.5', false, 'With the format mask 999G990D00.', '{"format_mask": "999G990D00"}', '{}'),
|
|
277
|
+
('f-text', 40, 'P5_PASSWORD', 'Password (password)', 'password', null, null, false, null, '{"reveal": true}', '{}'),
|
|
278
|
+
('f-text', 50, 'P5_NOTES', 'Notes (textarea)', 'textarea', null, null, false, 'A textarea always takes the whole row.', '{"rows": 3}', '{}'),
|
|
279
|
+
('f-choice', 10, 'P5_STATUS', 'Status (select)', 'select', 'STATIC:Active,Away,Left', 'Active', false, null, '{}', '{}'),
|
|
280
|
+
('f-choice', 20, 'P5_SIZE', 'Size (radio)', 'radio', 'STATIC:Small;S,Medium;M,Large;L', 'M', false, null, '{}', '{}'),
|
|
281
|
+
('f-choice', 30, 'P5_DAYS', 'Days (checkbox group)', 'checkbox_group', 'STATIC:Mon,Tue,Wed,Thu,Fri', 'Mon:Wed', false, 'Several values, colon separated.', '{}', '{}'),
|
|
282
|
+
('f-choice', 40, 'P5_ACTIVE', 'Active (switch)', 'switch', null, 'true', false, null, '{}', '{}'),
|
|
283
|
+
('f-choice', 50, 'P5_AGREE', 'I agree (checkbox)', 'checkbox', null, null, false, null, '{}', '{}'),
|
|
284
|
+
('f-choice', 60, 'P5_RATING', 'Rating (rating)', 'rating', null, '4', false, null, '{}', '{}'),
|
|
285
|
+
('f-date', 10, 'P5_DAY', 'Day (date)', 'date', null, null, false, 'With the date picker.', '{}', '{}'),
|
|
286
|
+
('f-date', 20, 'P5_PERIOD', 'Period (date range)', 'daterange', null, null, false, null, '{}', '{}'),
|
|
287
|
+
('f-date', 30, 'P5_COLOUR', 'Colour (color)', 'color', null, '#2f6fde', false, null, '{}', '{}'),
|
|
288
|
+
('f-date', 40, 'P5_TAGS', 'Tags (combobox)', 'combobox', 'STATIC:Urgent,Customer,Internal', null, false, 'Free text with suggestions; several values become tags.', '{}', '{}'),
|
|
289
|
+
('f-date', 50, 'P5_SHOWN', 'Display only (display)', 'display', null, 'A value that cannot be changed', false, null, '{}', '{}'),
|
|
290
|
+
('f-options', 10, 'P5_STRETCH', 'Stretch (to-stretch)', 'text', null, null, false, 'Takes the whole row of the form grid.', '{}', '{to-stretch}'),
|
|
291
|
+
('f-options', 20, 'P5_LARGE', 'Large field (to-large)', 'text', null, null, false, null, '{}', '{to-large}'),
|
|
292
|
+
('f-options', 30, 'P5_QUIET', 'Quiet (to-quiet)', 'text', null, null, false, 'No border until the field has the focus.', '{}', '{to-quiet}'),
|
|
293
|
+
('f-options', 40, 'P5_BOLD', 'Bold value (to-bold)', 'display', null, 'A bold value', false, null, '{}', '{to-bold}'),
|
|
294
|
+
('f-options', 50, 'P5_HIDDEN_LABEL', 'Hidden label (to-hide-label)', 'text', null, 'The label is only for screen readers', false, null, '{}', '{to-hide-label}')
|
|
295
|
+
) as i (region, seq, name, label, type, lov, def, req, help, config, topts);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{"blueprint": 1, "name": "Announcements", "alias": "announcements", "schema": "announcements",
|
|
2
|
+
"tables": [
|
|
3
|
+
{"name": "announcement", "label": "Announcements", "columns": [
|
|
4
|
+
{"name": "title", "type": "text", "required": true},
|
|
5
|
+
{"name": "body", "type": "text"},
|
|
6
|
+
{"name": "kind", "type": "text", "required": true, "values": ["Information", "Maintenance", "Outage", "Release"]},
|
|
7
|
+
{"name": "audience", "type": "text", "required": true, "values": ["Everyone", "Developers", "Administrators"]},
|
|
8
|
+
{"name": "start_date", "type": "date", "required": true},
|
|
9
|
+
{"name": "end_date", "type": "date"},
|
|
10
|
+
{"name": "published", "type": "boolean"}]}],
|
|
11
|
+
"pages": [
|
|
12
|
+
{"type": "cards", "table": "announcement", "page": 2, "label": "Notice board", "icon": "megaphone"},
|
|
13
|
+
{"type": "report_form", "table": "announcement", "page": 3, "form_page": 4, "label": "Manage announcements", "icon": "edit"},
|
|
14
|
+
{"type": "calendar", "table": "announcement", "page": 5, "label": "Schedule", "icon": "calendar"},
|
|
15
|
+
{"type": "blank", "page": 6, "label": "Showing them in your apps", "icon": "help",
|
|
16
|
+
"text": "Announcements keeps the notices of a workspace in one place: planned maintenance, outages, new releases.\n\nTo show the current ones in another application, let its database role read them (once, in SQL Commands or a migration script):\n\ngrant usage on schema announcements to app_my_app;\ngrant select on announcements.announcement to app_my_app;\n\nThen add a Cards region (or a list) to its home page with this query:\n\nselect title, kind as subtitle, body from announcements.announcement where published and current_date between start_date and coalesce(end_date, start_date) and audience in ('Everyone', 'Developers') order by start_date desc"}],
|
|
17
|
+
"dashboard": true,
|
|
18
|
+
"sample_data": [
|
|
19
|
+
{"table": "announcement", "columns": ["title", "body", "kind", "audience", "start_date", "end_date", "published"], "rows": [
|
|
20
|
+
["Database upgrade on Saturday", "The applications are unavailable on Saturday from 8:00 to 10:00.", "Maintenance", "Everyone", "2026-10-24", "2026-10-24", true],
|
|
21
|
+
["Order desk 1.5", "Faster search and a new order form.", "Release", "Everyone", "2026-10-27", "2026-11-03", true],
|
|
22
|
+
["New developer accounts", "Ask the administrators for an account in the Development workspace.", "Information", "Developers", "2026-10-01", "2026-12-31", true],
|
|
23
|
+
["Mail delays", "Mail from the applications was late on Monday morning; everything has been sent.", "Outage", "Everyone", "2026-10-19", "2026-10-20", true],
|
|
24
|
+
["Backups move", "Draft: the backups move to the new storage in November.", "Maintenance", "Administrators", "2026-11-10", null, false]]}]}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{"blueprint": 1, "name": "Lookup Lists", "alias": "lookup-lists", "schema": "lookup_lists",
|
|
2
|
+
"tables": [
|
|
3
|
+
{"name": "list", "label": "Lists", "columns": [
|
|
4
|
+
{"name": "name", "type": "text", "required": true, "unique": true},
|
|
5
|
+
{"name": "description", "type": "text"},
|
|
6
|
+
{"name": "used_by", "type": "text"}]},
|
|
7
|
+
{"name": "list_value", "label": "Values", "columns": [
|
|
8
|
+
{"name": "list_id", "references": "list", "required": true},
|
|
9
|
+
{"name": "code", "type": "text", "required": true},
|
|
10
|
+
{"name": "display", "type": "text", "required": true},
|
|
11
|
+
{"name": "seq", "type": "integer"},
|
|
12
|
+
{"name": "active", "type": "boolean"}]}],
|
|
13
|
+
"pages": [
|
|
14
|
+
{"type": "report_form", "table": "list", "page": 2, "form_page": 3, "label": "Lists", "icon": "list"},
|
|
15
|
+
{"type": "master_detail", "table": "list", "page": 4, "label": "Lists and values", "icon": "layers"},
|
|
16
|
+
{"type": "grid", "table": "list_value", "page": 5, "label": "All values", "icon": "grid"},
|
|
17
|
+
{"type": "blank", "page": 6, "label": "Using the lists", "icon": "help",
|
|
18
|
+
"text": "Lookup Lists keeps the lists of values that several applications share (countries, priorities, statuses) as data, so they change without changing an application.\n\nTo use a list in another application of the workspace, let that application's database role read the values (once, in SQL Commands or a migration script):\n\ngrant usage on schema lookup_lists to app_my_app;\ngrant select on lookup_lists.list, lookup_lists.list_value to app_my_app;\n\nThen give an item or a grid column a list of values like this one:\n\nselect v.display, v.code from lookup_lists.list_value v join lookup_lists.list l on l.id = v.list_id where l.name = 'PRIORITIES' and coalesce(v.active, true) order by v.seq, v.display\n\nor make it a shared list of values (Shared Components, Lists of values) and use LOV:PRIORITIES everywhere."}],
|
|
19
|
+
"dashboard": false,
|
|
20
|
+
"sample_data": [
|
|
21
|
+
{"table": "list", "columns": ["id", "name", "description", "used_by"], "rows": [
|
|
22
|
+
[1, "PRIORITIES", "Priorities of tasks and tickets", "every application"],
|
|
23
|
+
[2, "STATUSES", "The steps of a request", "requests, approvals"],
|
|
24
|
+
[3, "COUNTRIES", "Countries we deliver to", "orders, contacts"]]},
|
|
25
|
+
{"table": "list_value", "columns": ["list_id", "code", "display", "seq", "active"], "rows": [
|
|
26
|
+
[1, "LOW", "Low", 10, true], [1, "NORMAL", "Normal", 20, true], [1, "HIGH", "High", 30, true], [1, "URGENT", "Urgent", 40, false],
|
|
27
|
+
[2, "NEW", "New", 10, true], [2, "OPEN", "In progress", 20, true], [2, "DONE", "Done", 30, true], [2, "CANCELLED", "Cancelled", 40, true],
|
|
28
|
+
[3, "NL", "Netherlands", 10, true], [3, "BE", "Belgium", 20, true], [3, "DE", "Germany", 30, true], [3, "FR", "France", 40, true]]}]}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{"blueprint": 1, "name": "Release Notes", "alias": "release-notes", "schema": "release_notes",
|
|
2
|
+
"tables": [
|
|
3
|
+
{"name": "application", "label": "Applications", "columns": [
|
|
4
|
+
{"name": "name", "type": "text", "required": true, "unique": true},
|
|
5
|
+
{"name": "owner", "type": "text"},
|
|
6
|
+
{"name": "repository", "type": "text"}]},
|
|
7
|
+
{"name": "release", "label": "Releases", "columns": [
|
|
8
|
+
{"name": "application_id", "references": "application", "required": true},
|
|
9
|
+
{"name": "version", "type": "text", "required": true},
|
|
10
|
+
{"name": "status", "type": "text", "required": true, "values": ["Planned", "In test", "Released", "Withdrawn"]},
|
|
11
|
+
{"name": "release_date", "type": "date"},
|
|
12
|
+
{"name": "summary", "type": "text"}]},
|
|
13
|
+
{"name": "change", "label": "Changes", "columns": [
|
|
14
|
+
{"name": "release_id", "references": "release", "required": true},
|
|
15
|
+
{"name": "kind", "type": "text", "required": true, "values": ["Feature", "Fix", "Change", "Security"]},
|
|
16
|
+
{"name": "title", "type": "text", "required": true},
|
|
17
|
+
{"name": "ticket", "type": "text"},
|
|
18
|
+
{"name": "breaking", "type": "boolean"}]}],
|
|
19
|
+
"pages": [
|
|
20
|
+
{"type": "report_form", "table": "application", "page": 2, "form_page": 3, "label": "Applications", "icon": "layers"},
|
|
21
|
+
{"type": "report_form", "table": "release", "page": 4, "form_page": 5, "label": "Releases", "icon": "flag"},
|
|
22
|
+
{"type": "master_detail", "table": "release", "page": 6, "label": "Releases and changes", "icon": "list"},
|
|
23
|
+
{"type": "facets", "table": "change", "page": 7, "label": "Find changes", "icon": "filter"},
|
|
24
|
+
{"type": "calendar", "table": "release", "page": 8, "label": "Release calendar", "icon": "calendar"}],
|
|
25
|
+
"dashboard": true,
|
|
26
|
+
"sample_data": [
|
|
27
|
+
{"table": "application", "columns": ["id", "name", "owner", "repository"], "rows": [
|
|
28
|
+
[1, "Order desk", "Sales team", "git.example.com/sales/order-desk"],
|
|
29
|
+
[2, "Leave planner", "HR team", "git.example.com/hr/leave-planner"]]},
|
|
30
|
+
{"table": "release", "columns": ["id", "application_id", "version", "status", "release_date", "summary"], "rows": [
|
|
31
|
+
[1, 1, "1.4.0", "Released", "2026-09-15", "Discounts per customer group."],
|
|
32
|
+
[2, 1, "1.5.0", "In test", "2026-10-27", "Faster search, a new order form."],
|
|
33
|
+
[3, 2, "2.0.0", "Planned", "2026-11-17", "Approvals by the team lead."]]},
|
|
34
|
+
{"table": "change", "columns": ["release_id", "kind", "title", "ticket", "breaking"], "rows": [
|
|
35
|
+
[1, "Feature", "Discounts per customer group", "SALES-112", false],
|
|
36
|
+
[1, "Fix", "Totals rounded to cents", "SALES-118", false],
|
|
37
|
+
[2, "Change", "The order form is one page", "SALES-130", false],
|
|
38
|
+
[2, "Feature", "Search by customer number", "SALES-131", false],
|
|
39
|
+
[2, "Security", "Sessions end after 8 hours", "SALES-135", false],
|
|
40
|
+
[3, "Feature", "Team leads approve leave", "HR-40", true],
|
|
41
|
+
[3, "Change", "Leave is counted in hours", "HR-41", true]]}]}
|
package/package.json
CHANGED
package/public/app.css
CHANGED
|
@@ -597,6 +597,7 @@ table.report { width: 100%; border-collapse: collapse; font-size: 0.9rem; }
|
|
|
597
597
|
.col-slot { position: relative; height: 100%; flex: 0 1 24px; }
|
|
598
598
|
.col { position: absolute; left: 0; right: 0; background: var(--c); border-radius: 4px 4px 0 0; }
|
|
599
599
|
.col-value { position: absolute; left: 0; right: 0; text-align: center; font-size: 0.72rem; color: var(--muted); transform: translateY(-1.3em); font-variant-numeric: tabular-nums; pointer-events: none; }
|
|
600
|
+
.col-value[data-value]::after { content: attr(data-value); }
|
|
600
601
|
.col.neg { border-radius: 0 0 4px 4px; }
|
|
601
602
|
/* range: a floating column from low to high */
|
|
602
603
|
.col.range-col { border-radius: 4px; }
|
|
@@ -1924,6 +1925,7 @@ a.search-hit:hover, a.search-hit:focus-visible { background: var(--accent-soft);
|
|
|
1924
1925
|
.feedback-list > li, .feedback-admin > li { border-top: 1px solid var(--border); padding: 0.75rem 0; }
|
|
1925
1926
|
.feedback-list > li:first-child, .feedback-admin > li:first-child { border-top: 0; }
|
|
1926
1927
|
.feedback-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 0.35rem; overflow-wrap: anywhere; }
|
|
1928
|
+
.feedback-page > .region + .region { margin-top: 1rem; }
|
|
1927
1929
|
.feedback-body { white-space: pre-wrap; overflow-wrap: anywhere; margin: 0.4rem 0; }
|
|
1928
1930
|
.feedback-response { background: var(--surface-2); border-radius: var(--radius); padding: 0.5rem 0.75rem; }
|
|
1929
1931
|
.feedback-response p { white-space: pre-wrap; overflow-wrap: anywhere; margin: 0.25rem 0 0; }
|
package/public/builder.css
CHANGED
|
@@ -706,7 +706,7 @@ kbd { display: inline-block; padding: 0 0.35em; border: 1px solid var(--border);
|
|
|
706
706
|
.ab-home { display: grid; grid-template-columns: minmax(0, 1fr); gap: 0; }
|
|
707
707
|
.ab-main { min-width: 0; }
|
|
708
708
|
.ab-tiles { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 14px; margin-bottom: 20px; }
|
|
709
|
-
.ab-tiles-wrap { grid-template-columns: repeat(auto-
|
|
709
|
+
.ab-tiles-wrap { grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr)); }
|
|
710
710
|
.ab-tile {
|
|
711
711
|
position: relative; display: grid; grid-template-columns: auto; align-content: start; gap: 4px; min-height: 132px; padding: 16px 16px 18px;
|
|
712
712
|
background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); color: var(--text); text-decoration: none;
|
|
@@ -782,6 +782,7 @@ kbd { display: inline-block; padding: 0 0.35em; border: 1px solid var(--border);
|
|
|
782
782
|
|
|
783
783
|
@media (min-width: 1280px) {
|
|
784
784
|
/* the side column runs along the right edge, as in APEX */
|
|
785
|
+
.ide .ide-page:has(> .ab-home) { max-width: none; }
|
|
785
786
|
.ab-home { grid-template-columns: minmax(0, 1fr) 288px; margin: calc(-1 * var(--page-py)) calc(-1 * var(--page-px)) -48px 0; min-height: calc(100vh - var(--bar-h) - 28px); }
|
|
786
787
|
.ab-main { padding: var(--page-py) var(--page-px) 48px 0; }
|
|
787
788
|
.ab-side { padding: 18px 20px 24px; background: var(--surface); border-left: 1px solid var(--border); }
|
|
@@ -1397,6 +1398,7 @@ pre.wrap-pre { white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
|
1397
1398
|
.wh-stats a.stat { color: inherit; text-decoration: none; }
|
|
1398
1399
|
.wh-stats a.stat:hover { border-color: color-mix(in srgb, var(--accent) 45%, var(--border)); }
|
|
1399
1400
|
.wh-stats a.stat span { color: var(--accent); }
|
|
1401
|
+
.wh-stats { grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); }
|
|
1400
1402
|
.wh-top-head { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8px; margin-top: 8px; }
|
|
1401
1403
|
.wh-top-head h2 { margin: 0; }
|
|
1402
1404
|
.wh-timeframe { display: inline-flex; }
|
|
@@ -1525,3 +1527,39 @@ pre.wrap-pre { white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
|
1525
1527
|
.mail-answer { overflow-wrap: anywhere; font-family: var(--font-mono, monospace); font-size: 0.85em; }
|
|
1526
1528
|
.host-list { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 6px; }
|
|
1527
1529
|
.host-list code { overflow-wrap: anywhere; }
|
|
1530
|
+
/* Regions (and a table before one) stacked in a block container keep 1rem between them; grids that space their
|
|
1531
|
+
regions with a gap reset it below. */
|
|
1532
|
+
:where(.ide :is(.region, .table-wrap)) + :where(.region) { margin-top: 1rem; }
|
|
1533
|
+
.sc-grid > .region, .qs-columns > .region { margin-top: 0; }
|
|
1534
|
+
:where(.ide td) > .btn + .btn { margin-inline-start: 0.25rem; }
|
|
1535
|
+
.role-chip + .role-chip { margin-inline-start: 0.3rem; }
|
|
1536
|
+
|
|
1537
|
+
/* (sprint 120) App Gallery: four tabs with their counts; two by two on a phone */
|
|
1538
|
+
.gal-count { display: inline-block; min-width: 1.5em; padding: 0 5px; border-radius: 999px; background: var(--surface-3); color: var(--muted); font-size: 12px; font-weight: 600; text-align: center; }
|
|
1539
|
+
.gal-tabs a[aria-current] .gal-count { background: var(--surface); color: var(--text); }
|
|
1540
|
+
@media (max-width: 600px) {
|
|
1541
|
+
.gal-tabs { display: grid; grid-template-columns: 1fr 1fr; width: 100%; }
|
|
1542
|
+
.gal-tabs a { text-align: center; border-radius: 0; }
|
|
1543
|
+
.gal-tabs a:first-child { border-radius: var(--radius) 0 0 0; }
|
|
1544
|
+
.gal-tabs a:nth-child(2) { border-radius: 0 var(--radius) 0 0; }
|
|
1545
|
+
.gal-tabs a:nth-child(3) { border-left: 1px solid var(--border); border-top: 0; border-radius: 0 0 0 var(--radius); }
|
|
1546
|
+
.gal-tabs a:last-child { border-top: 0; border-radius: 0 0 var(--radius) 0; }
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
/* ---------------------------------------------------------------- (sprint 121) builder language: status bar menu, sign-in links */
|
|
1550
|
+
.ide-status .ide-lang { flex: none; }
|
|
1551
|
+
.ide-status .ide-lang > summary { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; color: inherit; padding: 2px 4px; border-radius: 4px; }
|
|
1552
|
+
.ide-status .ide-lang > summary:hover, .ide-status .ide-lang[open] > summary { color: var(--chrome-text); background: rgb(255 255 255 / 8%); }
|
|
1553
|
+
.ide-status .ide-lang > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
|
|
1554
|
+
/* the status bar clips (overflow: hidden): the menu opens fixed, above it */
|
|
1555
|
+
.ide-status .ide-lang .menu-panel { position: fixed; top: auto; bottom: 34px; left: auto; right: auto; min-width: 200px; font-size: 13px; white-space: normal; }
|
|
1556
|
+
.ide-lang-form button { width: 100%; }
|
|
1557
|
+
.ide-lang-form button[aria-current="true"] { font-weight: 600; }
|
|
1558
|
+
.ide-lang-form .icon, .ide-lang-pad { width: 1em; height: 1em; flex: none; display: inline-block; }
|
|
1559
|
+
.signin-langs ul { display: flex; flex-wrap: wrap; justify-content: center; gap: 0.25rem 1rem; margin: 0; padding: 0.75rem 0 0; list-style: none; font-size: 0.85rem; }
|
|
1560
|
+
.signin-langs a { color: var(--muted); text-decoration: none; }
|
|
1561
|
+
.signin-langs a:hover { color: var(--accent); text-decoration: underline; }
|
|
1562
|
+
.signin-langs a[aria-current="true"] { color: var(--text); font-weight: 600; }
|
|
1563
|
+
@media (max-width: 640px) { .ide-status .ide-lang .menu-panel { left: 0.5rem; right: 0.5rem; min-width: 0; } }
|
|
1564
|
+
/* long compound words of other languages (Workspace-Dienstprogramme) wrap instead of widening a phone's page */
|
|
1565
|
+
.ab-tile > *, .stat span { min-width: 0; overflow-wrap: anywhere; hyphens: auto; }
|
package/public/worksheet.css
CHANGED
|
@@ -263,8 +263,9 @@ details.menu.obj-menu > summary::before, details.menu.obj-menu > summary::after
|
|
|
263
263
|
.lp-chips li { display: flex; align-items: center; border: 1px solid var(--border); border-radius: 999px; background: var(--surface); padding-left: 10px; }
|
|
264
264
|
.lp-chips a { display: inline-flex; align-items: center; gap: 6px; text-decoration: none; color: var(--text); padding: 4px 0; font-size: 13px; }
|
|
265
265
|
.lp-recent li { padding-right: 10px; border-style: dashed; }
|
|
266
|
-
.lp-tabs { display: flex; gap: 0; border-bottom: 1px solid var(--border); margin-bottom: 10px; }
|
|
267
|
-
.lp-tabs [role=tab] { border: 0; background: none; color: var(--muted); padding: 7px 12px; font: inherit; cursor: pointer; }
|
|
266
|
+
.lp-tabs { display: flex; gap: 0; border-bottom: 1px solid var(--border); margin-bottom: 10px; overflow-x: auto; scrollbar-width: none; }
|
|
267
|
+
.lp-tabs [role=tab] { border: 0; background: none; color: var(--muted); padding: 7px 12px; font: inherit; cursor: pointer; white-space: nowrap; }
|
|
268
|
+
@media (max-width: 480px) { .lp-tabs [role=tab] { padding: 7px 8px; } }
|
|
268
269
|
.lp-tabs [role=tab][aria-selected=true] { color: var(--text); box-shadow: inset 0 -2px 0 var(--hot); }
|
|
269
270
|
.js .lp-group > h2 { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
|
|
270
271
|
.lp-list { list-style: none; margin: 0 0 16px; padding: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 260px), 1fr)); gap: 8px; }
|
package/src/builder/advisor.ts
CHANGED
|
@@ -453,7 +453,7 @@ export async function advisorRoutes(app: FastifyInstance) {
|
|
|
453
453
|
<span class="hit-snippet">${f.entry ? html`<b>${f.field}:</b> ` : ''}${f.message}</span>
|
|
454
454
|
</li>`)}</ul>`
|
|
455
455
|
: html`<div class="alert alert-success" role="status">No problems found.</div>`;
|
|
456
|
-
const main = html`${appHeader(a, 'advisor')}
|
|
456
|
+
const main = html`${appHeader(a, 'advisor', s)}
|
|
457
457
|
${region('Advisor', html`<p class="muted u-mt0">Checked ${checked} pieces of SQL as the role <code>${a.db_role ?? '(owner)'}</code> (planned with EXPLAIN and rolled back, nothing is run), and every reference to pages, items, lists of values, authorization schemes and report layouts.
|
|
458
458
|
${plpgsqlCheck ? 'PL/pgSQL functions were checked with plpgsql_check.' : html`Install the <code>plpgsql_check</code> extension to check PL/pgSQL functions too.`}</p>
|
|
459
459
|
<p><span class="tag tag-error">${count('error')} errors</span> <span class="tag tag-warning">${count('warning')} warnings</span> <span class="tag tag-info">${count('info')} notes</span>
|
package/src/builder/ai-agents.ts
CHANGED
|
@@ -223,7 +223,7 @@ export async function aiAgentRoutes(app: FastifyInstance) {
|
|
|
223
223
|
const agents = (a.ai_agents ?? []) as AiAgent[];
|
|
224
224
|
const tools = (a.ai_tools ?? []) as AiTool[];
|
|
225
225
|
const problems = definitionProblems(agents, tools);
|
|
226
|
-
const main = html`${appHeader(a, 'shared')}
|
|
226
|
+
const main = html`${appHeader(a, 'shared', s)}
|
|
227
227
|
<div class="title-row"><h1>AI Agents</h1></div>
|
|
228
228
|
<p class="muted u-mt0">An agent answers in an AI assistant region and acts only through the tools you give it: SQL queries (read-only, as the application's database role, so row level security applies), PL/pgSQL functions, REST data sources, JavaScript actions in the browser, and links to pages (pgkiln builds the URL and its checksum, only for pages the user may open). What users type and the tools' results go to the agent's AI service. <a href="${BASE}/apps/${a.id}/ai">AI usage</a> lists every tool call.</p>
|
|
229
229
|
${problems.length ? html`<div class="alert alert-error" role="alert">${problems.join(' ')}</div>` : ''}
|
|
@@ -250,7 +250,7 @@ export async function aiAgentRoutes(app: FastifyInstance) {
|
|
|
250
250
|
const x = name ? ((a.ai_agents ?? []) as AiAgent[]).find((g) => g.name === name) : {};
|
|
251
251
|
if (!x) return reply.code(404).send('Not found');
|
|
252
252
|
const title = name || 'New agent';
|
|
253
|
-
const main = html`${appHeader(a, 'shared')}<div class="title-row"><h1 class="ai-name">${title}</h1></div>${region('AI agent', await agentForm(a, s, x, name, []))}`;
|
|
253
|
+
const main = html`${appHeader(a, 'shared', s)}<div class="title-row"><h1 class="ai-name">${title}</h1></div>${region('AI agent', await agentForm(a, s, x, name, []))}`;
|
|
254
254
|
return send(reply, s, shell(s, title, crumbs(a, title), main));
|
|
255
255
|
});
|
|
256
256
|
|
|
@@ -273,7 +273,7 @@ export async function aiAgentRoutes(app: FastifyInstance) {
|
|
|
273
273
|
return [];
|
|
274
274
|
});
|
|
275
275
|
if (problems.length) {
|
|
276
|
-
const main = html`${appHeader(a, 'shared')}<div class="title-row"><h1 class="ai-name">${original || 'New agent'}</h1></div>${region('AI agent', await agentForm(a, s, x, original, problems))}`;
|
|
276
|
+
const main = html`${appHeader(a, 'shared', s)}<div class="title-row"><h1 class="ai-name">${original || 'New agent'}</h1></div>${region('AI agent', await agentForm(a, s, x, original, problems))}`;
|
|
277
277
|
return send(reply.code(422), s, shell(s, original || 'New agent', crumbs(a, original || 'New agent'), main));
|
|
278
278
|
}
|
|
279
279
|
if (original && original !== x.name)
|
|
@@ -305,7 +305,7 @@ export async function aiAgentRoutes(app: FastifyInstance) {
|
|
|
305
305
|
const x = name ? ((a.ai_tools ?? []) as AiTool[]).find((t) => t.name === name) : {};
|
|
306
306
|
if (!x) return reply.code(404).send('Not found');
|
|
307
307
|
const title = name || 'New tool';
|
|
308
|
-
const main = html`${appHeader(a, 'shared')}<div class="title-row"><h1 class="ai-name">${title}</h1></div>${region('AI tool', await toolForm(a, s, x, name, []))}`;
|
|
308
|
+
const main = html`${appHeader(a, 'shared', s)}<div class="title-row"><h1 class="ai-name">${title}</h1></div>${region('AI tool', await toolForm(a, s, x, name, []))}`;
|
|
309
309
|
return send(reply, s, shell(s, title, crumbs(a, title), main));
|
|
310
310
|
});
|
|
311
311
|
|
|
@@ -337,7 +337,7 @@ export async function aiAgentRoutes(app: FastifyInstance) {
|
|
|
337
337
|
return [];
|
|
338
338
|
});
|
|
339
339
|
if (problems.length) {
|
|
340
|
-
const main = html`${appHeader(a, 'shared')}<div class="title-row"><h1 class="ai-name">${original || 'New tool'}</h1></div>${region('AI tool', await toolForm(a, s, x, original, problems))}`;
|
|
340
|
+
const main = html`${appHeader(a, 'shared', s)}<div class="title-row"><h1 class="ai-name">${original || 'New tool'}</h1></div>${region('AI tool', await toolForm(a, s, x, original, problems))}`;
|
|
341
341
|
return send(reply.code(422), s, shell(s, original || 'New tool', crumbs(a, original || 'New tool'), main));
|
|
342
342
|
}
|
|
343
343
|
flash(s, 'AI tool saved.');
|