pgkiln 0.88.0 → 0.90.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.
@@ -225,9 +225,16 @@ department. Every installation has the **Default** workspace, which holds the ap
225
225
  - **New applications** (Create, from a file, pasted data, existing tables, a blueprint) and **imports** go into the
226
226
  current workspace; a working copy stays in its main application's workspace. Imports made with the command line
227
227
  (`pgkiln import`) go into Default. Exports don't carry the workspace.
228
- - **Workspace utilities**: tiles for users and sign-in, application groups, the App Gallery, developers, the SQL
229
- Workshop, the dashboard, [static workspace files, remote servers and web credentials](#static-workspace-files-remote-servers-and-web-credentials) (and, for administrators, instance settings, workspaces, AI and mail services and the
230
- installation), with **Cross Application Reports** in the side column (APEX: Cross Application Reports), each over
228
+ - **Workspace utilities** (the rail's **Administration**, `/builder/utilities`; UX-4): the utilities in sections with a
229
+ **filter** field at the top: *People and sign-in* (users, developers, identity providers, LDAP directories, password
230
+ policy), *Connections* ([remote servers, web credentials](#static-workspace-files-remote-servers-and-web-credentials),
231
+ remote databases, REST source catalogs, mail services; for workspace administrators the mail queue and network
232
+ access), *Data and AI* (AI services, vector providers, UI defaults, static workspace files, icons), *Operations*
233
+ (application groups, backups, export and import, the dashboard, OpenTelemetry), the current **workspace** (for its
234
+ administrators: its page, [workspace message and environment banner](#workspace-message-and-environment-banner),
235
+ preferences, extension links, [workspace utilization](#workspace-utilization)) and the **instance** (for
236
+ administrators: instance settings, workspaces, [Database users](#database-users), the installation), then
237
+ **Cross Application Reports** (APEX: Cross Application Reports), each over
231
238
  every application of the current workspace: *Application attributes* (type, sign-in, availability, build status,
232
239
  debug level, language, database role), *Build options*, *Developer comments*, *Authentication* (sign-in methods,
233
240
  identity providers, LDAP, access, the login page, Forgot password), *Authorization schemes*, *Password items* and
@@ -247,7 +254,7 @@ its developers must not reach other teams' applications or data.
247
254
  ### Workspace message and environment banner
248
255
 
249
256
  Administrators and the workspace's administrators set them on the workspace's page (*Manage service*, or the
250
- Administration menu → *Workspace message and environment banner*; APEX: Define Workspace Message, Define Environment
257
+ Administration → *Workspace message and environment banner*; APEX: Define Workspace Message, Define Environment
251
258
  Banner): a **message** (plain text, up to 2000 characters) shown to the workspace's developers on the
252
259
  [Workspace home](#workspace-home) (where they can also edit it in place) and the App Builder home,
253
260
  and an **environment banner**: a label of up to 40 characters in one of eight colours, shown at the top of every
@@ -255,7 +262,7 @@ builder page while the workspace is current.
255
262
 
256
263
  ### Workspace preferences
257
264
 
258
- Administrators and the workspace's administrators set them on the workspace's page (Administration menu →
265
+ Administrators and the workspace's administrators set them on the workspace's page (Administration →
259
266
  *Workspace preferences*; APEX: Set Workspace Preferences):
260
267
 
261
268
  - **Enable the SQL Workshop**: off, every SQL Workshop page (the SQL Worksheet, the Object Browser, SQL scripts, Load and
@@ -349,7 +356,7 @@ says what was added, kept and replaced.
349
356
 
350
357
  ### Workspace utilization
351
358
 
352
- Administration menu → **Workspace utilization** (`/builder/workspaces/<id>/utilization`; APEX: Workspace
359
+ Administration → **Workspace utilization** (`/builder/workspaces/<id>/utilization`; APEX: Workspace
353
360
  Utilization), for administrators and the workspace's administrators: the number of applications, pages, developers,
354
361
  page views of the last 30 days and SQL scripts; per application its pages, regions, items, page views, users and the
355
362
  size of its static files; and the schemas the workspace's database roles use with their tables and storage.
@@ -462,11 +469,14 @@ longer sign in. Delete a workspace only when it is shared and empty; its logins
462
469
  Every builder page has the same frame, modelled on an IDE:
463
470
 
464
471
  - **The icon rail** on the left: Home (the [Workspace home](#workspace-home); the logo leads there too), App Builder,
465
- SQL Workshop, [App Gallery](#app-gallery), [Data Reporter](#data-reporter-datasets-and-reporting-applications), **Search** (on the
466
- pages of an application); at the bottom the **Administration** menu (Workspace utilities; for administrators of the
467
- current workspace *Manage service*: [workspace message and environment banner](#workspace-message-and-environment-banner),
468
- [workspace utilization](#workspace-utilization) and the workspace's page; Manage users and groups, Developers,
469
- Monitor activity, [Database users](#database-users) for administrators, Change my password), Help, and your account menu (the circle with your initials) with the
472
+ SQL Workshop, [App Gallery](#app-gallery), [Data Reporter](#data-reporter-datasets-and-reporting-applications),
473
+ Team Development and **Administration** (the workspace utilities, below); at the bottom Help, **Show labels** and your
474
+ account menu (the circle with your initials). (UX-3) The entry of the page you are on is marked, by the page's
475
+ address (an application's pages under App Builder; users, developers, workspaces, instance settings and the other
476
+ utilities under Administration). *Show labels* puts each entry's name next to its icon (*Hide labels* takes them
477
+ away; kept for your session); without them the name shows when you point at an icon or move to it with the
478
+ keyboard. The search is the field at the top of every page (**Ctrl+K**); an application's own search is its
479
+ **Search** tab. Your account menu has the
470
480
  **builder theme** (*Dark*, the default; *Light*; or *System*, which follows the operating system),
471
481
  Change password and Sign out. The theme is remembered on this device, also after signing out;
472
482
  it only changes the builder, not your applications.
@@ -494,7 +504,9 @@ other sites.
494
504
  (sprint 86) An address under `/builder` that leads nowhere (a deleted application or page, a typo, an
495
505
  application of a workspace you are not a member of) shows a **Page not found** page in the builder's
496
506
  theme with a link to the App Builder home; a server error shows **Something went wrong** (the error
497
- is in the server log, not on the page). Scripts get the plain answers they always got.
507
+ is in the server log, not on the page). Scripts get the plain answers they always got. (UX-10) Signed in, the
508
+ page stays inside the builder (rail, header search, status bar), with a search field filled with the words of the
509
+ address and your recent applications. `/builder/apps/new` and `/builder/apps/create` lead to Create application.
498
510
 
499
511
  ### Choosing the builder language
500
512
 
@@ -548,6 +560,8 @@ names and help of a shared component's form), the links to this guide, and any S
548
560
  (sprint 109; APEX: the workspace home page) The page the builder opens after signing in, `/builder/home`, above the
549
561
  App Builder and the SQL Workshop. Everything on it is of the current [workspace](#workspaces), and it works without
550
562
  JavaScript. The [default-password banner](#signing-in-and-developer-accounts) stays on top until the password is changed.
563
+ (UX-13) For administrators, **Needs attention** under the heading lists what must not wait: failing
564
+ [lockout checks](#lockout-checks) and, with the release check on, a newer pgkiln (details in the System message).
551
565
 
552
566
  - **Tool cards**: App Builder (`/builder/apps`), SQL Workshop (not when the workspace's preferences turn it off),
553
567
  **Data Reporter** (when the server has the Data Reporter tool at `/builder/reporter`) and the [App Gallery](#app-gallery).
@@ -582,12 +596,13 @@ that may not reach the internet. Under the test runner the server never asks Git
582
596
  The App Builder's home page, `/builder/apps` (`/builder` shows it too, unless the workspace's
583
597
  [preferences](#workspace-preferences) send `/builder` to the Workspace home), is laid out like APEX's App Builder:
584
598
 
585
- - **Four tiles**: **Create** and **Import** (each on its own page), the workspace **Dashboard**
586
- (applications, pages, active accounts; page views, users, failed sign-ins and errors of the last
587
- 24 hours, and per application the last 7 days with a link to its Activity page) and
588
- **Workspace Utilities** (users, identity providers, LDAP directories, password policy,
589
- developers, the SQL Workshop, and for administrators [workspaces](#workspaces), AI services and the
590
- [installation log](#installation)). The list, the dashboard and Recent show the current workspace.
599
+ - **The header** (UX-5): the heading *App Builder*, **Create** (the page's one primary action), **Import** and a
600
+ **⋯** menu with the workspace **Dashboard** (applications, pages, active accounts; page views, users, failed
601
+ sign-ins and errors of the last 24 hours, and per application the last 7 days with a link to its Activity page)
602
+ and **Administration** (the [workspace utilities](#workspaces)). The list, the dashboard and Recent show the
603
+ current workspace. Every builder page follows the same pattern: one heading, one primary (accent) button,
604
+ the other actions beside it or in a ⋯ menu; an application's Pages tab has **Create page** as its primary action
605
+ in the application's header (other tabs: **Run**), and its Tasks list only what the page doesn't offer already.
591
606
  - **The applications**, as a report (application id, name, alias, pages, page views of the last 24
592
607
  hours, sign-in, last updated, **updated by**, **working copies**, and Edit / Run) or as cards. **Search** filters the list as you type
593
608
  (by name, alias or id; without JavaScript, press Enter). Click a column heading, or use
@@ -714,19 +729,22 @@ you like. `meta.reporting_app_count(workspace_id)` counts a workspace's reportin
714
729
 
715
730
  ### Creating an application
716
731
 
717
- **Create** (a tile, or `/builder/create`) is one page, like APEX's Create Application wizard. Links at the top
718
- start from a file, pasted data, a blueprint or the App Gallery instead. Then:
732
+ **Create** (the App Builder's primary button, or `/builder/create`) is one page, like APEX's Create Application
733
+ wizard. Links at the top start from a file, pasted data, a blueprint or the App Gallery instead. (UX-7) The name
734
+ comes first, then the data and its pages, features, appearance and sign-in; **Create application** and Cancel stay
735
+ in a footer at the bottom of the window, next to a one-line summary (*12 pages on hr · App users (login page) ·
736
+ English*) that follows your choices. The sections:
719
737
 
720
738
  | Section | Field | Meaning |
721
739
  |---|---|---|
722
- | Data | Pages on the tables of | The parsing schema: an existing schema, or a new, empty one named after the alias. (sprint 87) Choosing a schema lists its tables under Pages at once, with the name and alias proposed for it (unless you already typed them); without JavaScript, *Show its tables* does it |
723
740
  | Application | Name | Display name, shown in the header. Proposed from the schema and selected when you click it, so typing replaces it |
724
- | | Description, Icon | Shown on the home page's hero and the About page |
725
- | Appearance | Theme style, Navigation, Accent colour | Iris, Lavender or Standard; a side or top menu; an own accent colour |
726
- | Pages | a page type per table | *Interactive report with form*, *Faceted search*, *Cards*, *Calendar* (a table with a date column), *Master detail* (a table another table refers to), *Interactive grid*, *Report* or *No page*. Tables with a single-column primary key get a modal form with their report, faceted search, cards and calendar; views and tables without such a key can't have a form or a grid. Proposed: a report with form, an interactive grid for a table of more than 100,000 rows, a report for a view. (sprint 87) A table with [row level security](08-security.md) shows *RLS · n* (its number of policies), or *RLS, no policies* in red: the application's role would see no rows. *Dashboard*: a chart per table |
741
+ | | Alias | Lowercase URL name: `inventory` gives `/a/inventory`. (sprint 87) It follows the name as you type it (*Café Orders* → `cafe-orders`) until you change it yourself |
742
+ | | Icon, Description | Shown on the home page's hero and the About page |
743
+ | Data and pages | Pages on the tables of | The parsing schema: an existing schema, or a new, empty one named after the alias. (sprint 87) Choosing a schema lists its tables at once, with the name and alias proposed for it (unless you already typed them); without JavaScript, *Show its tables* does it (it reloads the page, so choose the schema before typing) |
744
+ | | a page type per table | *Interactive report with form*, *Faceted search*, *Cards*, *Calendar* (a table with a date column), *Master detail* (a table another table refers to), *Interactive grid*, *Report* or *No page*. Tables with a single-column primary key get a modal form with their report, faceted search, cards and calendar; views and tables without such a key can't have a form or a grid. Proposed: a report with form, an interactive grid for a table of more than 100,000 rows, a report for a view. (sprint 87) A table with [row level security](08-security.md) shows *RLS · n* (its number of policies), or *RLS, no policies* in red: the application's role would see no rows. *Dashboard*: a chart per table |
727
745
  | Features | | *About page* (in a Help menu), *Access control* (authorization schemes `CONTRIBUTOR` and `READER` next to `ADMIN`, for the roles admin, contributor and reader), *Activity reporting* (page 9010 for administrators: page views per day and the latest activity, from `meta.app_activity`), *Feedback*, *Theme style selection* (users may choose light or dark), *Install as an app* (PWA) and *Push notifications* (needs `PGKILN_SECRET_KEY`) |
728
- | Settings | Alias | Lowercase URL name: `inventory` gives `/a/inventory`. (sprint 87) It follows the name as you type it (*Café Orders* → `cafe-orders`) until you change it yourself |
729
- | | Authentication | *App users* (a login page and a user list), *HTTP header* (a trusted reverse proxy names the user, see [chapter 8](08-security.md#http-header-authentication-reverse-proxy)), *Database accounts* (PostgreSQL login roles and their passwords, see [chapter 8](08-security.md#database-accounts-postgresql-roles)), *Custom* (your own PL/pgSQL function, see [chapter 8](08-security.md#custom-authentication-a-plpgsql-function)) or *None* (a public app) |
746
+ | Appearance | Theme style, Navigation, Accent colour | Iris, Lavender or Standard; a side or top menu; an own accent colour |
747
+ | Sign-in | Authentication | *App users* (a login page and a user list), *HTTP header* (a trusted reverse proxy names the user, see [chapter 8](08-security.md#http-header-authentication-reverse-proxy)), *Database accounts* (PostgreSQL login roles and their passwords, see [chapter 8](08-security.md#database-accounts-postgresql-roles)), *Custom* (your own PL/pgSQL function, see [chapter 8](08-security.md#custom-authentication-a-plpgsql-function)) or *None* (a public app) |
730
748
  | | First user / Password | The first user; they get the `admin` role. An existing account is reused (its password isn't changed). (sprint 87) Hidden while Authentication is *None* |
731
749
  | | Language | The application's primary language |
732
750
  | Advanced | Start from | Shown when there are **boilerplate** applications (Settings → Application type): their pages, shared components and settings are copied into the new application, which keeps its own name, alias, role and authentication ([application types](#application-types-and-subscriptions)) |
@@ -940,7 +958,11 @@ they are **not** run until you choose to.
940
958
  (sprint 74) Above the pages: a **search** by page name or number (the rows filter while you type),
941
959
  **Report** and **Icons** views, and **Create page** (the page wizards below). **Tasks** on the right: run the
942
960
  application, create pages (also with AI), (sprint 137) **Add feature page** and **Create page as copy**, Shared
943
- Components, page groups, export, backups, supporting objects, lock or delete the application.
961
+ Components, page groups, export, backups, supporting objects and lock the application. (UX-9) The report shows a
962
+ page's mode in the property editor's words (*Normal*, *Modal dialog*), and Group stays empty for a page without one.
963
+ (UX-6) Deleting the application is not a task: it is the **Danger zone** at the bottom of Settings → Definition,
964
+ where you type the application's alias to confirm (the server checks it too). Its pages, shared components,
965
+ working copies and backups go with it, so export it first if you may need it again.
944
966
 
945
967
  The **default-password** warning shows on the App Builder home (and Developers) only; dismiss it with × until the
946
968
  next sign-in. The avatar keeps a red dot until the password is changed.
@@ -1072,6 +1094,16 @@ The page designer has three panes, as in APEX's Page Designer. From 1024 pixels
1072
1094
  side by side; on phones and portrait tablets they are tabs (**Tree**, **Layout**, **Properties**):
1073
1095
  choosing a component opens the Properties tab.
1074
1096
 
1097
+ (UX-2) From 1024 pixels wide the designer is fixed to the window: only the panes scroll, so the toolbar
1098
+ with **Save**, Undo / Redo and the page navigator stays in view, and the property editor's own save button
1099
+ stays at the bottom of its pane. **Ctrl+S** (Cmd+S) saves. A change not saved yet puts a dot on Save, and
1100
+ leaving the page with one asks first. On a window under 800 pixels high the gallery under the layout is only
1101
+ its tabs (*Regions*, *Items*, *Buttons*): choose one to open it, choose the open one to fold it again; the
1102
+ designer remembers which you prefer.
1103
+
1104
+ (UX-6) **Delete page** is in the toolbar's page menu (the wrench), away from Save: type the page number to
1105
+ confirm. Its regions, items, buttons, dynamic actions and processes go with it, as do its lock and comments.
1106
+
1075
1107
  - **Left: the component tree**, with four tabs:
1076
1108
  - **Rendering**: the page, *Pre-Rendering* (branches, computations and processes that run
1077
1109
  before the page is shown), the regions by
@@ -1087,7 +1119,8 @@ choosing a component opens the Properties tab.
1087
1119
  application items), linking to Shared Components.
1088
1120
 
1089
1121
  Icons show which components have an authorization scheme, a condition or a build option; type icons are coloured
1090
- by kind (regions, items, buttons, dynamic actions, processes). Folders open and close
1122
+ by kind (regions, items, buttons, dynamic actions, processes). (UX-12) A component's name comes first: when the
1123
+ pane is narrow its type text is left out (the icon shows the type; hover a component for its name and type). Folders open and close
1091
1124
  with a click; with the keyboard, the arrow keys walk the tree (Left / Right close and open a
1092
1125
  folder), Enter opens a component.
1093
1126
 
@@ -1581,7 +1614,8 @@ on import), downloads it again and removes it. See
1581
1614
  ## Users (the user directory)
1582
1615
 
1583
1616
  **Builder → Users** lists every account with the applications (and roles) it can use. Create
1584
- accounts here (a password is optional for single sign-on-only accounts). Open an account to edit
1617
+ accounts here (a password is optional for single sign-on-only accounts): (UX-8) **Create account** in the header
1618
+ opens a side panel, so the list keeps the page's width. Open an account to edit
1585
1619
  its name and e-mail, set or remove its password, deactivate or delete it, and grant, change or
1586
1620
  revoke access per application. Password changes, deactivation and access changes end the
1587
1621
  affected sessions. See [chapter 8](08-security.md#the-user-directory).
@@ -1790,7 +1824,7 @@ sending mailboxes (an application access policy or RBAC for Applications).
1790
1824
 
1791
1825
  ### Mail queue
1792
1826
 
1793
- Administration menu → **Mail queue** (`/builder/mail-queue`; APEX 26.2: Administration → Mail queue): the delivery
1827
+ Administration → **Mail queue** (`/builder/mail-queue`; APEX 26.2: Administration → Mail queue): the delivery
1794
1828
  health of the mail queue, for administrators (every workspace, or one) and a workspace's administrators (the current
1795
1829
  workspace's applications only; other developers get 403). Filter by period (24 hours, 48 hours, 7 days), workspace and
1796
1830
  application:
@@ -1835,7 +1869,7 @@ applications off or send them to a collector of their own as well (through the a
1835
1869
 
1836
1870
  ### Network access
1837
1871
 
1838
- Administration menu → **Network access** (`/builder/network`; APEX 26.2: workspace network ACLs), for administrators
1872
+ Administration → **Network access** (`/builder/network`; APEX 26.2: workspace network ACLs), for administrators
1839
1873
  (any workspace, `?ws=`) and a workspace's administrators (their workspaces): which web services a workspace's
1840
1874
  applications may call. The installation's list (`PGKILN_REST_ALLOWED_HOSTS`, set in the server's environment, shown
1841
1875
  read-only; the private hosts to administrators only) applies to every workspace. A workspace can choose **Only the
@@ -2121,7 +2155,8 @@ others (Quick SQL, Query Builder, Load and Unload Data, Sample Data, DDL, Compar
2121
2155
 
2122
2156
  (sprint 96; Database Actions: the Launchpad) The home page starts with **Pinned & recently visited**: the pages you
2123
2157
  pinned (a tool, a database object from the Object Browser, a saved worksheet from its file menu; at most 24, per
2124
- workspace) and the SQL Workshop pages you opened last in this browser. Below, the tools are in three tabs,
2158
+ workspace) and the SQL Workshop pages you opened last in this browser (UX-14: each page once, the latest visit
2159
+ first, without the Launchpad itself or a page that is pinned already). Below, the tools are in three tabs,
2125
2160
  **Development**, **Administration** and **Monitoring**, each with a line about what it does and a pin; the **More**
2126
2161
  menu uses the same headings.
2127
2162
 
@@ -2133,6 +2168,12 @@ the SQL Workshop's connection may use. Without JavaScript the header's field ope
2133
2168
  - **SQL Worksheet** (`/builder/sql/worksheet`, sprint 91; Database Actions: SQL; was SQL Commands, whose address
2134
2169
  `/builder/sql/commands` opens the same page): one screen with a **navigator** on the left, the **editor** with a
2135
2170
  toolbar, **output tabs** below and a **status bar**.
2171
+ - (UX-1) The **toolbar is one row**: the worksheet's file menu, **Run Statement**, **Run Script**, **Explain Plan**
2172
+ and **Autotrace** (icons with their name as a tooltip when the window is narrow), **Suggest** and **Check**, the
2173
+ commit mode, fullscreen and a **⋯ menu** with *Continue on error*, Format, Convert case, Find, Load data, Clear
2174
+ and the keyboard shortcuts. **Run as** is in the status bar (its tooltip says who the statements run as).
2175
+ The editor takes about half of the window and the output the other half (drag the splitter; the size is
2176
+ remembered); before the first run the output is only its row of tabs, and choosing a tab opens it.
2136
2177
  - The navigator has a schema (the same one as the Object Browser), an object type (*All Objects*, tables, views,
2137
2178
  materialized views, indexes, sequences, types, functions, procedures, triggers, policies) and a search field.
2138
2179
  A table or view opens to its columns with their types (a key marks the primary key); drag it into the editor
@@ -2217,11 +2258,11 @@ the SQL Workshop's connection may use. Without JavaScript the header's field ope
2217
2258
  remembered for the session; the history and the log keep the statement as written.
2218
2259
  - **Format** (**Ctrl + F7**) lays out the selection or the whole worksheet: a clause per line, the columns of a select
2219
2260
  list and the conditions of a WHERE one per line, joins and subqueries indented, keywords in lower or UPPER case
2220
- (chosen in the **?** menu). Strings, comments and `$$` bodies stay as they are. **Convert case** (**Ctrl + B**)
2261
+ (chosen in the **⋯** menu). Strings, comments and `$$` bodies stay as they are. **Convert case** (**Ctrl + B**)
2221
2262
  cycles the selection (or the word at the cursor) through UPPER, lower and Initial caps. **Find and replace**
2222
2263
  (**Ctrl + F**, **Ctrl + H**) with a regular expression or exact case, **Go to line** (**Ctrl + G**), and
2223
- **Ctrl + /** comments the selected lines out or in. The **?** menu lists every key; **F1** opens this help. The
2224
- upload button opens Load Data for the navigator's schema.
2264
+ **Ctrl + /** comments the selected lines out or in. The **⋯** menu lists every key; **F1** opens this help.
2265
+ *Load data* in the same menu opens Load Data for the navigator's schema.
2225
2266
  - Each statement commits on its own (**Autocommit**, like Database Actions). Statements run as the **owner connection** (an isolated workspace's
2226
2267
  developer login for its developers), on a connection of their own that is closed afterwards, so `SET ROLE` or
2227
2268
  `SET search_path` doesn't leak. **Run as** picks an application of your workspace instead: the statements then
@@ -2414,7 +2455,7 @@ the SQL Workshop's connection may use. Without JavaScript the header's field ope
2414
2455
  connections, memory, row security), the installed extensions and the schemas with their size.
2415
2456
  - **SQL Monitor** (administrators only): see [SQL Monitor](#sql-monitor) below (it replaced the Database Monitor;
2416
2457
  the old address `/builder/sql/utilities/monitor` redirects).
2417
- - **Database Users** (administrators only; Administration menu → *Database users*, `/builder/db-users`; sprint 99,
2458
+ - **Database Users** (administrators only; Administration → *Database users*, `/builder/db-users`; sprint 99,
2418
2459
  Database Actions: Database Users): see [Database users](#database-users).
2419
2460
  - **Schema Export / Import** (administrators only; `/builder/sql/data-pump`; sprint 103, Database Actions: Data
2420
2461
  Pump): see [Schema export and import](#schema-export-and-import).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pgkiln",
3
- "version": "0.88.0",
3
+ "version": "0.90.0",
4
4
  "description": "Open source low-code, SQL-driven application builder for PostgreSQL, inspired by Oracle APEX",
5
5
  "keywords": [
6
6
  "postgresql",
@@ -444,7 +444,9 @@ body.ide {
444
444
  .pd > .tab-panel > .pd-pane-title { display: none; }
445
445
  /* with script the designer fills the window and each pane scrolls on its own */
446
446
  .js .ide-full .ide-main { height: 100vh; height: 100dvh; display: flex; flex-direction: column; }
447
- .js .ide-full .ide-work { flex: 1 1 auto; min-height: 0; }
447
+ /* (UX-2) the work area is the containing block and clips: nothing inside (a screen-reader heading placed
448
+ absolutely, a pane's overflow) can make the page itself scroll */
449
+ .js .ide-full .ide-work { flex: 1 1 auto; min-height: 0; position: relative; overflow: hidden; }
448
450
  .js .ide-full .pd { height: 100%; min-height: 0; }
449
451
  .js .pd-pane { min-height: 0; overflow: auto; }
450
452
  .js .pd-center { display: flex; flex-direction: column; overflow: hidden; }
@@ -484,8 +486,9 @@ body.ide {
484
486
  }
485
487
  .pd-node .icon { width: 16px; height: 16px; color: var(--muted); }
486
488
  a.pd-node:hover { background: var(--surface-3); }
487
- .pd-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
488
- .pd-note { margin-left: auto; padding-left: 8px; max-width: 45%; overflow: hidden; text-overflow: ellipsis; color: var(--muted); font-size: 11.5px; }
489
+ /* (UX-12) the name keeps its room: the type gives way first (the icon tells it, the tooltip names it) */
490
+ .pd-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; flex: 0 1 auto; }
491
+ .pd-note { margin-left: auto; padding-left: 8px; max-width: 45%; min-width: 0; flex: 0 1000 auto; overflow: hidden; text-overflow: clip; white-space: nowrap; color: var(--muted); font-size: 11.5px; }
489
492
  .pd-folder { color: var(--muted); font-weight: 600; cursor: default; }
490
493
  .pd-tag { display: inline-flex; }
491
494
  .pd-tag .icon { width: 13px; height: 13px; color: var(--warn); }
@@ -1671,3 +1674,88 @@ a.cp-type:hover, a.cp-type:focus-visible { border-color: var(--accent); }
1671
1674
  .api-client-actions select { width: auto; min-height: 0; padding-block: 0.2rem; font-size: 0.8125rem; }
1672
1675
  .api-client-actions .inline-form { display: flex; gap: 6px; align-items: center; }
1673
1676
  .api-clients td.actions { text-align: end; }
1677
+
1678
+ /* (UX-2) Page Designer on a short window: the gallery folded to its tabs (builder.js), Save at hand */
1679
+ @media (min-width: 1024px) {
1680
+ .js .pd-gallery.is-folded { height: auto; min-height: 0; overflow: hidden; }
1681
+ .js .pd-gallery.is-folded .tab-panel { display: none; }
1682
+ }
1683
+ .tb-save.is-dirty { position: relative; }
1684
+ .tb-save.is-dirty::after { content: ""; position: absolute; top: 3px; right: 3px; width: 7px; height: 7px; border-radius: 50%; background: var(--hot); }
1685
+ #pd-p-props .component-form > .buttons { position: sticky; bottom: 0; z-index: 1; margin: 0; padding: 8px 0; background: var(--surface); border-top: 1px solid var(--border); }
1686
+ .pd-note.is-cut { display: none; }
1687
+
1688
+ /* (UX-3) the rail with labels next to its icons (the toggle at its bottom, kept in the session) */
1689
+ .rail-toggle { width: 100%; border: 0; background: none; cursor: pointer; font: inherit; }
1690
+ @media (min-width: 900px) {
1691
+ .ide.rail-labels { --rail-w: 196px; }
1692
+ .rail-labels .rail-list { padding: 0 10px; }
1693
+ .rail-labels .rail-link { display: flex; justify-content: flex-start; gap: 12px; padding: 0 12px; }
1694
+ .rail-labels .rail-link .rail-label { position: static; opacity: 1; transform: none; padding: 0; background: none; color: inherit;
1695
+ box-shadow: none; font-size: 13.5px; font-weight: 500; pointer-events: auto; }
1696
+ .rail-labels .rail-logo { margin-left: 18px; }
1697
+ .rail-labels .rail-user { justify-content: flex-start; padding-left: 14px; }
1698
+ }
1699
+
1700
+ /* (UX-4) Administration: the workspace utilities in sections of compact tiles, with a filter (builder.js) */
1701
+ .adm { max-width: 1180px; }
1702
+ .adm .title-row { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 12px; }
1703
+ .adm-filter input { width: min(320px, 100%); }
1704
+ .adm-lede { max-width: 75ch; }
1705
+ .adm-section { margin-top: 22px; }
1706
+ .adm-section h2 { font-size: 15px; margin: 0 0 8px; color: var(--text-2, var(--muted)); text-transform: none; }
1707
+ .adm-tiles { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 250px), 1fr)); gap: 8px; }
1708
+ .adm-tile { display: flex; flex-direction: column; gap: 2px; height: 100%; padding: 10px 12px; border: 1px solid var(--border); border-radius: var(--radius);
1709
+ background: var(--surface); color: var(--text); text-decoration: none; transition: border-color var(--dur) var(--ease), background-color var(--dur) var(--ease); }
1710
+ .adm-tile:hover, .adm-tile:focus-visible { border-color: var(--hot); background: var(--surface-2); }
1711
+ .adm-tile-head { display: flex; align-items: center; gap: 8px; }
1712
+ .adm-tile-head .icon { width: 16px; height: 16px; color: var(--hot); flex: none; }
1713
+ .adm-tile-text { color: var(--muted); font-size: 12.5px; line-height: 1.4; }
1714
+ .adm-reports { columns: 2 260px; }
1715
+
1716
+ /* (UX-5) a builder page's header: one heading, one primary (accent) action, secondary actions and a ⋯ menu */
1717
+ .page-head { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 10px 16px; margin: 0 0 16px; }
1718
+ .page-head h1 { margin: 0; font-size: 22px; }
1719
+ .page-head-sub { margin: 2px 0 0; }
1720
+ .page-head-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
1721
+ .page-head-actions .btn { display: inline-flex; align-items: center; gap: 6px; }
1722
+ .page-head-actions .icon { width: 15px; height: 15px; }
1723
+ /* (UX-13) the Workspace home's "Needs attention" list for administrators, above the tools */
1724
+ .wh-attention { display: block; margin: 0 0 16px; }
1725
+ .wh-attention h2 { margin: 0 0 4px; font-size: 14px; }
1726
+ .wh-attention ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 4px; }
1727
+ .wh-attention .icon { width: 15px; height: 15px; vertical-align: -2px; }
1728
+ /* (UX-6) deleting an application or a page: apart from everyday actions, the name typed to confirm */
1729
+ .danger-box { margin-top: 24px; padding: 12px 16px; border: 1px solid color-mix(in srgb, var(--danger) 45%, var(--border)); border-radius: 8px; background: var(--danger-soft); }
1730
+ .danger-box h2 { margin: 0 0 4px; font-size: 15px; color: var(--danger); }
1731
+ .danger-box p { margin: 0 0 8px; }
1732
+ .danger-form { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 8px; }
1733
+ .danger-form .field { margin: 0; flex: 1 1 14rem; max-width: 22rem; }
1734
+ .menu-section .danger-form { margin-top: 6px; padding-top: 8px; border-top: 1px solid var(--border); }
1735
+ /* (UX-7) Create application: Create always in reach, with a one-line summary */
1736
+ .aw-footer { position: sticky; bottom: 29px; z-index: 15; display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8px 16px;
1737
+ margin-top: 16px; padding: 10px 14px; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); box-shadow: var(--shadow-md); }
1738
+ .aw-footer .buttons { margin: 0; }
1739
+ .aw-summary { margin: 0; min-width: 0; overflow-wrap: anywhere; }
1740
+ .aw-schema-row { align-items: end; }
1741
+ .aw-schema-row .buttons { margin: 0 0 1rem; }
1742
+ /* (UX-10) Page not found inside the builder */
1743
+ .nf-page h1 { margin-top: 8px; }
1744
+ .nf-search { margin: 12px 0 16px; max-width: 32rem; }
1745
+ .nf-page h2 { font-size: 14px; margin: 12px 0 4px; }
1746
+ /* (UX-8) builder reports at laptop width, one rule for all (opt in with .rep-fit): low-value columns (.col-low, under 1150 px of table width) give
1747
+ way first, long text wraps (.col-wrap), and the Actions column (.col-actions) stays on screen at the right */
1748
+ .table-wrap.rep-fit { container-type: inline-size; }
1749
+ @container (max-width: 1150px) { .rep-fit .report .col-low { display: none; } }
1750
+ .ide .report td.col-wrap { white-space: normal; min-width: 12rem; overflow-wrap: anywhere; }
1751
+ .ide .report .col-actions { position: sticky; right: 0; background: var(--surface); box-shadow: inset 1px 0 0 var(--border); }
1752
+ .ide .report th.col-actions { background: var(--surface-2); }
1753
+ /* (UX-8) a side drawer opened from the page header (details.drawer): Create account on Users */
1754
+ .drawer { display: inline-block; }
1755
+ .drawer > summary { list-style: none; cursor: pointer; }
1756
+ .drawer > summary::-webkit-details-marker { display: none; }
1757
+ .drawer-panel { position: fixed; top: 0; right: 0; bottom: 0; z-index: 60; width: min(26rem, 100vw); overflow-y: auto; padding: 16px 18px 24px;
1758
+ background: var(--surface); color: var(--text); border-left: 1px solid var(--border-strong); box-shadow: var(--shadow-lg); text-align: left; }
1759
+ .drawer-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 12px; }
1760
+ .drawer-head h2 { margin: 0; font-size: 16px; }
1761
+ .drawer-panel .buttons { justify-content: flex-end; }
package/public/builder.js CHANGED
@@ -508,6 +508,38 @@
508
508
  const dirty = () => save.classList.add('is-dirty');
509
509
  form.addEventListener('input', dirty);
510
510
  form.addEventListener('change', dirty);
511
+ // (UX-2) Ctrl+S saves; leaving with unsaved changes asks first (saving doesn't)
512
+ let submitting = false;
513
+ form.addEventListener('submit', () => (submitting = true));
514
+ document.addEventListener('keydown', (e) => {
515
+ if ((e.ctrlKey || e.metaKey) && !e.altKey && e.key.toLowerCase() === 's' && !e.target.closest?.('.ws, dialog')) {
516
+ e.preventDefault();
517
+ form.requestSubmit();
518
+ }
519
+ });
520
+ window.addEventListener('beforeunload', (e) => {
521
+ if (!submitting && save.classList.contains('is-dirty')) {
522
+ e.preventDefault();
523
+ e.returnValue = '';
524
+ }
525
+ });
526
+ }
527
+ // (UX-2) on a short window the gallery is only its tabs: choosing a tab opens it, choosing the open one folds it
528
+ const gallery = document.querySelector('.pd-gallery');
529
+ if (gallery) {
530
+ const short = window.matchMedia('(max-height: 800px)');
531
+ const apply = () => gallery.classList.toggle('is-folded', short.matches && store.get('gallery') !== 'open');
532
+ apply();
533
+ short.addEventListener('change', apply);
534
+ gallery.addEventListener('click', (e) => {
535
+ const t = e.target.closest('[role=tab]');
536
+ if (!t || !short.matches) return;
537
+ const folded = gallery.classList.contains('is-folded');
538
+ const wasActive = t.getAttribute('aria-selected') === 'true';
539
+ const open = folded || !wasActive;
540
+ store.set('gallery', open ? 'open' : null);
541
+ gallery.classList.toggle('is-folded', !open);
542
+ }, true); // before the tabs switch, so the tab that was open is known
511
543
  }
512
544
  }
513
545
 
@@ -1011,7 +1043,41 @@
1011
1043
  setupQueryCanvas();
1012
1044
  setupThemePreview();
1013
1045
  setupTabs();
1046
+ // (UX-4) Administration: filter the utilities by their text; sections without a match hide
1047
+ const admQ = document.querySelector('[data-adm-filter]');
1048
+ if (admQ) {
1049
+ admQ.closest('.adm-filter').hidden = false;
1050
+ const page = admQ.closest('.adm');
1051
+ const none = page.querySelector('.adm-none');
1052
+ admQ.addEventListener('input', () => {
1053
+ const q = admQ.value.trim().toLowerCase();
1054
+ let any = false;
1055
+ for (const sec of page.querySelectorAll('.adm-section')) {
1056
+ let shown = 0;
1057
+ for (const li of sec.querySelectorAll('li')) {
1058
+ const hit = !q || li.textContent.toLowerCase().includes(q);
1059
+ li.hidden = !hit;
1060
+ if (hit) shown++;
1061
+ }
1062
+ sec.hidden = !shown;
1063
+ if (shown) any = true;
1064
+ }
1065
+ none.hidden = any;
1066
+ });
1067
+ }
1014
1068
  for (const tree of document.querySelectorAll('.pd-tree')) setupTree(tree);
1069
+ // (UX-12) a node's type text only when it fits whole: the name comes first (the icon and tooltip tell the type)
1070
+ const fitNotes = () => {
1071
+ for (const n of document.querySelectorAll('.pd-tree .pd-note')) {
1072
+ n.classList.remove('is-cut');
1073
+ if (n.scrollWidth > n.clientWidth + 1) n.classList.add('is-cut');
1074
+ }
1075
+ };
1076
+ fitNotes();
1077
+ if ('ResizeObserver' in window) {
1078
+ const pane = document.querySelector('.pd-tree')?.closest('.pd-pane');
1079
+ if (pane) new ResizeObserver(() => requestAnimationFrame(fitNotes)).observe(pane);
1080
+ }
1015
1081
  for (const tree of document.querySelectorAll('.pd-tree[data-tree-menu]')) setupTreeMenu(tree);
1016
1082
  setupProperties();
1017
1083
  setupCanvas();
@@ -1206,7 +1272,30 @@ document.querySelectorAll('input[data-filter-tree]').forEach((el) => {
1206
1272
  sync();
1207
1273
  });
1208
1274
 
1209
- const picker = document.querySelector('form.create-schema select[name="schema"]');
1275
+ // (UX-7) the footer's summary follows the page types, the sign-in and the language
1276
+ const awForm = document.querySelector('form.aw-form');
1277
+ const summary = document.getElementById('aw-summary');
1278
+ const sum = () => {
1279
+ const tpl = document.getElementById('aw-summary-pages');
1280
+ if (!awForm || !summary || !tpl) return;
1281
+ let parts;
1282
+ if (tpl.dataset.schema) {
1283
+ let n = 1;
1284
+ awForm.querySelectorAll('#aw-pages select').forEach((sel) => (n += Number(sel.selectedOptions[0]?.dataset.pages ?? 0)));
1285
+ const chart = awForm.querySelector('#aw-pages input[name="chart"]');
1286
+ if (chart?.checked && n > 1) n += 1;
1287
+ parts = [n === 1 ? tpl.dataset.one : tpl.dataset.many.replace('{n}', String(n))];
1288
+ } else parts = [tpl.dataset.new];
1289
+ for (const name of ['authentication', 'language']) {
1290
+ const sel = awForm.elements.namedItem(name);
1291
+ const text = sel?.selectedOptions?.[0]?.textContent?.replace(/\s*\([a-z-]+\)$/i, '');
1292
+ if (text) parts.push(text);
1293
+ }
1294
+ summary.textContent = parts.join(' · ');
1295
+ };
1296
+ awForm?.addEventListener('change', sum);
1297
+
1298
+ const picker = document.querySelector('select[name="schema"][form="aw-schema-get"]');
1210
1299
  const pages = document.getElementById('aw-pages');
1211
1300
  const hidden = document.getElementById('aw-schema-fields');
1212
1301
  if (!picker || !pages || !hidden) return;
@@ -1231,12 +1320,13 @@ document.querySelectorAll('input[data-filter-tree]').forEach((el) => {
1231
1320
  if (alias && !alias.hasAttribute('data-edited')) alias.value = doc.getElementById('f_alias')?.value ?? alias.value;
1232
1321
  history.replaceState(null, '', url);
1233
1322
  document.getElementById('aw-pages')?.removeAttribute('aria-busy');
1323
+ sum();
1234
1324
  } catch {
1235
1325
  picker.form.submit();
1236
1326
  }
1237
1327
  });
1238
1328
  // the button is for pages without JavaScript
1239
- picker.form.querySelector('button')?.setAttribute('hidden', '');
1329
+ document.querySelector('button[form="aw-schema-get"]')?.setAttribute('hidden', '');
1240
1330
  })();
1241
1331
 
1242
1332
  // (sprint 101, DA-12) SQL Workshop › Scheduling: the schedule builder's five fields and the common schedules fill the
@@ -1335,3 +1425,12 @@ document.querySelectorAll('input[data-filter-tree]').forEach((el) => {
1335
1425
  })();
1336
1426
 
1337
1427
  // (sprint 127) Passkeys: the [data-passkey] buttons are handled by app.js (sprint 131), which every builder page loads too.
1428
+
1429
+ // (UX-8) a side drawer (details.drawer): Close, Escape and a click outside close it; the first field gets the focus
1430
+ document.querySelectorAll('details.drawer').forEach((d) => {
1431
+ const close = () => { d.open = false; d.querySelector('summary')?.focus(); };
1432
+ d.querySelector('[data-drawer-close]')?.addEventListener('click', close);
1433
+ d.addEventListener('toggle', () => { if (d.open) d.querySelector('.drawer-panel input:not([type=hidden]), .drawer-panel select, .drawer-panel textarea')?.focus(); });
1434
+ document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && d.open) close(); });
1435
+ document.addEventListener('click', (e) => { if (d.open && !d.contains(e.target)) d.open = false; });
1436
+ });
package/public/palette.js CHANGED
@@ -18,15 +18,22 @@
18
18
  if (/^\/builder\/sql\/./.test(path) && !/\/(panel|navigator|action)$/.test(path)) {
19
19
  const title = document.title.replace(/ · Builder$/, '');
20
20
  const href = path + location.search;
21
- const list = (store.get('sw.recent') || []).filter((r) => r && r.href !== href);
21
+ // (UX-14) one entry per page (its path): opened again with other parameters, the newest wins
22
+ const list = (store.get('sw.recent') || []).filter((r) => r && typeof r.href === 'string' && r.href.split('?')[0] !== path);
22
23
  list.unshift({ href, title });
23
24
  store.set('sw.recent', list.slice(0, 8));
24
25
  }
25
26
  const recent = document.querySelector('[data-lp-recent]');
26
27
  if (recent) {
27
28
  const list = store.get('sw.recent') || [];
29
+ // (UX-14) not the page you are on, not what is pinned already, each path once
30
+ const pinned = new Set([...document.querySelectorAll('.lp-pins .lp-chips:not([data-lp-recent]) a[href]')].map((a) => a.getAttribute('href').split('?')[0]));
31
+ const seen = new Set([path]);
28
32
  for (const r of list) {
29
33
  if (typeof r?.href !== 'string' || !r.href.startsWith('/builder/')) continue;
34
+ const p = r.href.split('?')[0];
35
+ if (seen.has(p) || pinned.has(p)) continue;
36
+ seen.add(p);
30
37
  const li = document.createElement('li');
31
38
  const a = document.createElement('a');
32
39
  a.href = r.href;
@@ -3,7 +3,7 @@
3
3
  screens: navigator | editor over output tabs, filling the window, with a status bar at the bottom. */
4
4
  .ws-page { display: flex; flex-direction: column; padding: 8px 10px 0; background: var(--bg); }
5
5
  .ws-page > .sw-tabs { margin-bottom: 6px; }
6
- .ws { --ws-nav: 280px; --ws-out: 42vh; display: grid; grid-template-columns: minmax(0, 1fr); gap: 0; background: var(--surface);
6
+ .ws { --ws-nav: 280px; --ws-out: 45%; display: grid; grid-template-columns: minmax(0, 1fr); gap: 0; background: var(--surface);
7
7
  border: 1px solid var(--border); border-radius: var(--radius); min-width: 0; }
8
8
  .ws-default { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); border: 0; padding: 0; }
9
9
  .ws-nav { display: flex; flex-direction: column; min-width: 0; border-bottom: 1px solid var(--border); background: var(--surface-2); }
@@ -32,7 +32,7 @@
32
32
  .ws-obj-actions { display: flex; flex-wrap: wrap; gap: 4px; padding: 4px 10px 8px 34px; }
33
33
  .ws-obj-actions .btn { font-size: 11px; padding: 1px 6px; }
34
34
 
35
- .ws-main { display: flex; flex-direction: column; min-width: 0; min-height: 0; }
35
+ .ws-main { display: flex; flex-direction: column; min-width: 0; min-height: 0; container-type: inline-size; }
36
36
  .ws-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; padding: 6px 8px; border-bottom: 1px solid var(--border); }
37
37
  .ws-toolbar .btn { display: inline-flex; align-items: center; gap: 4px; }
38
38
  .ws-toolbar .icon { width: 15px; height: 15px; }
@@ -99,12 +99,16 @@
99
99
  .js .ide-full .ws-editor .ce { flex: 1 1 auto; min-height: 0; grid-template-rows: minmax(0, 1fr) auto; border: 0; border-radius: 0; }
100
100
  .js .ide-full .ws-editor .ce-main { min-height: 0; }
101
101
  .js .ide-full .ws-editor .ce textarea.ce-input { height: 100%; min-height: 0; resize: none; }
102
- .js .ide-full .ws-output { flex: 0 0 var(--ws-out); }
102
+ .js .ide-full .ws-output { flex: 0 1 var(--ws-out); }
103
+ /* (UX-1) the editor keeps at least ten lines; an empty output is only its tab strip until a run */
104
+ .js .ide-full .ws-editor { min-height: 13.5rem; }
105
+ .js .ide-full .ws-output[data-empty] { flex: 0 0 auto; }
106
+ .js .ide-full .ws-output[data-empty] .ws-pane { display: none; }
103
107
  .js .ide-full .ws-pane { flex: 1 1 auto; }
104
108
  .js .ide-full .ws-nav { overflow: hidden; }
105
109
  }
106
110
  @media (max-width: 899px) {
107
- .ws-toolbar .btn span:not(.sr-only) { display: none; }
111
+ .ws-toolbar .btn span:not(.sr-only):not(.ws-dots) { display: none; }
108
112
  .ws-toolbar .btn-hot span:not(.sr-only) { display: inline; }
109
113
  }
110
114
  /* fullscreen: only the editor and its toolbar */
@@ -299,3 +303,34 @@ details.menu.obj-menu > summary::before, details.menu.obj-menu > summary::after
299
303
  .ws-txn-badge[data-state="open"] { color: var(--warn); border-color: color-mix(in srgb, var(--warn) 55%, var(--border)); background: color-mix(in srgb, var(--warn) 10%, transparent); font-weight: 600; }
300
304
  .ws-txn-badge[data-state="aborted"] { color: var(--danger); border-color: var(--danger); background: var(--danger-soft); font-weight: 600; }
301
305
  .ws-txn-alert { margin: 6px 8px; }
306
+
307
+ /* (UX-1) one toolbar row: the run buttons, Suggest / Check, Commit mode, fullscreen and a ⋯ menu with the rest */
308
+ /* it fits on one row; an open transaction (badge, Commit, Rollback) may take a second one */
309
+ .ws-toolbar > * { flex: none; }
310
+ /* the commit mode's options name it (Autocommit / Transaction): no label beside the select */
311
+ .ws-toolbar .ws-txn .ws-runas > span { display: none; }
312
+ @container (max-width: 950px) {
313
+ .ws-toolbar .btn:not(.btn-hot) > span:not(.sr-only):not(.ws-dots) { display: none; }
314
+ }
315
+ @container (max-width: 640px) {
316
+ .ws-toolbar > * { flex: 0 1 auto; min-width: 0; }
317
+ .ws-toolbar .ws-txn { flex-wrap: wrap; }
318
+ }
319
+ .ws-more .menu-panel { min-width: 18rem; max-height: calc(100dvh - 190px); overflow: auto; }
320
+ /* with script the worksheet tabs name the file: the file menu is its icon */
321
+ .js .ws-toolbar .ws-file-name { display: none; }
322
+ .ws-more-tools { display: flex; flex-direction: column; gap: 2px; }
323
+ .ws-menu-item { display: flex; align-items: center; gap: 8px; width: 100%; padding: 6px 8px; border: 0; border-radius: 6px; background: none;
324
+ color: var(--text); font: inherit; text-align: left; text-decoration: none; cursor: pointer; }
325
+ .ws-menu-item:hover, .ws-menu-item:focus-visible { background: var(--surface-2); }
326
+ .ws-menu-item .icon { width: 15px; height: 15px; }
327
+ .ws-menu-item kbd { margin-left: auto; font-size: 11.5px; color: var(--muted); }
328
+ .ws-more-tools .ws-check { padding: 6px 8px; }
329
+ .ws-more-head { display: block; margin-bottom: 4px; }
330
+ /* the code editor's hint bar under the editor: only while it has something to say (Check's result) */
331
+ .ws-editor .ce-bar:not(:has(.ce-msg:not(:empty))) { display: none; }
332
+ .ws-editor .ce-bar :is(.ce-hint, .ce-suggest, .ce-check) { display: none; }
333
+ /* (UX-1) who the statements run as: a chip at the right of the status bar */
334
+ .ws-status-run { display: flex; align-items: center; gap: 12px; min-width: 0; flex: 1 1 auto; }
335
+ .ws-runas-chip { flex: none; display: inline-flex; align-items: center; gap: 6px; }
336
+ .ws-runas-chip select { padding: 1px 22px 1px 6px; min-height: 0; font-size: 12.5px; }