pgkiln 0.42.0 → 0.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/db/migrations/099_layout_positions.sql +29 -0
  2. package/db/migrations/100_generated_labels.sql +381 -0
  3. package/db/migrations/101_theme_choice.sql +15 -0
  4. package/docs/apex-feature-parity.md +36 -9
  5. package/docs/guide/03-builder.md +82 -21
  6. package/docs/guide/04-pages-and-regions.md +55 -5
  7. package/docs/guide/05-items.md +1 -1
  8. package/docs/guide/09-reference.md +5 -1
  9. package/docs/guide/12-development.md +2 -1
  10. package/docs/guide/14-globalization.md +5 -1
  11. package/package.json +1 -1
  12. package/public/app.css +80 -0
  13. package/public/app.js +189 -1
  14. package/public/builder.css +112 -0
  15. package/public/builder.js +280 -3
  16. package/src/builder/appsheets.ts +53 -13
  17. package/src/builder/appwizard.ts +5 -3
  18. package/src/builder/arrange.ts +133 -17
  19. package/src/builder/components.ts +28 -22
  20. package/src/builder/designer.ts +67 -18
  21. package/src/builder/forms.ts +4 -1
  22. package/src/builder/object-browser.ts +342 -0
  23. package/src/builder/pwa.ts +3 -3
  24. package/src/builder/routes.ts +85 -26
  25. package/src/builder/shared.ts +103 -1
  26. package/src/builder/sql.ts +13 -65
  27. package/src/builder/ui.ts +21 -6
  28. package/src/i18n/ar.ts +18 -0
  29. package/src/i18n/cs.ts +18 -0
  30. package/src/i18n/da.ts +18 -0
  31. package/src/i18n/de.ts +18 -0
  32. package/src/i18n/el.ts +18 -0
  33. package/src/i18n/es.ts +18 -0
  34. package/src/i18n/fi.ts +18 -0
  35. package/src/i18n/fr.ts +18 -0
  36. package/src/i18n/he.ts +18 -0
  37. package/src/i18n/it.ts +18 -0
  38. package/src/i18n/ja.ts +18 -0
  39. package/src/i18n/ko.ts +18 -0
  40. package/src/i18n/nb.ts +18 -0
  41. package/src/i18n/pl.ts +18 -0
  42. package/src/i18n/pt.ts +18 -0
  43. package/src/i18n/ru.ts +18 -0
  44. package/src/i18n/sv.ts +18 -0
  45. package/src/i18n/tr.ts +18 -0
  46. package/src/i18n/uk.ts +18 -0
  47. package/src/i18n/zh.ts +18 -0
  48. package/src/i18n.ts +36 -0
  49. package/src/metadata.ts +5 -1
  50. package/src/runtime/account.ts +5 -5
  51. package/src/runtime/devbar.ts +16 -13
  52. package/src/runtime/engine.ts +6 -0
  53. package/src/runtime/facets.ts +34 -3
  54. package/src/runtime/format.ts +52 -0
  55. package/src/runtime/items.ts +27 -0
  56. package/src/runtime/locale.ts +7 -1
  57. package/src/runtime/regions.ts +36 -2
  58. package/src/runtime/render.ts +27 -6
  59. package/src/runtime/report.ts +141 -81
  60. package/src/runtime/routes.ts +39 -7
package/public/app.js CHANGED
@@ -3642,7 +3642,7 @@ function areaFilter(map, f) {
3642
3642
  const bar = document.querySelector('.devbar');
3643
3643
  if (!bar) return;
3644
3644
  const KEY = 'pgkiln.devbar';
3645
- let prefs = { dock: 'bottom', autohide: true };
3645
+ let prefs = { dock: 'bottom', autohide: true, compact: false };
3646
3646
  try { prefs = { ...prefs, ...JSON.parse(localStorage.getItem(KEY) || '{}') }; } catch {}
3647
3647
  if (!['bottom', 'top', 'left', 'right'].includes(prefs.dock)) prefs.dock = 'bottom';
3648
3648
  const save = () => { try { localStorage.setItem(KEY, JSON.stringify(prefs)); } catch {} };
@@ -3650,6 +3650,10 @@ function areaFilter(map, f) {
3650
3650
  settings?.closest('li')?.removeAttribute('hidden');
3651
3651
  const apply = () => {
3652
3652
  bar.dataset.dock = prefs.dock;
3653
+ // (sprint 79) "Icons only": the tools without their texts
3654
+ bar.toggleAttribute('data-compact', !!prefs.compact);
3655
+ const compact = bar.querySelector('input[name="devbar-compact"]');
3656
+ if (compact) compact.checked = !!prefs.compact;
3653
3657
  bar.querySelectorAll('input[name="devbar-dock"]').forEach((r) => { r.checked = r.value === prefs.dock; });
3654
3658
  const auto = bar.querySelector('input[name="devbar-autohide"]');
3655
3659
  if (auto) auto.checked = !!prefs.autohide;
@@ -3658,6 +3662,7 @@ function areaFilter(map, f) {
3658
3662
  bar.addEventListener('change', (e) => {
3659
3663
  if (e.target.name === 'devbar-dock') prefs.dock = e.target.value;
3660
3664
  else if (e.target.name === 'devbar-autohide') prefs.autohide = e.target.checked;
3665
+ else if (e.target.name === 'devbar-compact') prefs.compact = e.target.checked;
3661
3666
  else return;
3662
3667
  save();
3663
3668
  apply();
@@ -3725,3 +3730,186 @@ function areaFilter(map, f) {
3725
3730
  }, true);
3726
3731
  document.addEventListener('keydown', (e) => { if (on && e.key === 'Escape') set(false); });
3727
3732
  })();
3733
+
3734
+ // (sprint 76) the page's texts for the report menus below (render.ts CLIENT_TEXTS)
3735
+ const reportTexts = (() => {
3736
+ try {
3737
+ return JSON.parse(document.getElementById('pgkiln-meta')?.textContent || '{}').texts || {};
3738
+ } catch {
3739
+ return {};
3740
+ }
3741
+ })();
3742
+
3743
+ // (sprint 76) The interactive report's Actions menu: an entry opens its form in a dialog (APEX: the
3744
+ // Actions dialogs); the form fields stay the same elements, moved into the dialog and back on close.
3745
+ (() => {
3746
+ const close = (details) => { if (details) details.open = false; };
3747
+ document.addEventListener('toggle', (e) => {
3748
+ const item = e.target;
3749
+ if (!(item instanceof HTMLDetailsElement) || !item.matches('.ir-item[data-ir-dialog]') || !item.open) return;
3750
+ const body = item.querySelector(':scope > .ir-body');
3751
+ const menu = item.closest('details.actions-menu');
3752
+ if (!body || typeof HTMLDialogElement !== 'function') return;
3753
+ item.open = false;
3754
+ menu?.querySelectorAll('details.ir-sub[open]').forEach(close);
3755
+ close(menu);
3756
+ const dialog = document.createElement('dialog');
3757
+ dialog.className = 'ir-dialog';
3758
+ const title = document.createElement('h2');
3759
+ title.textContent = item.querySelector('summary span')?.textContent ?? '';
3760
+ const x = document.createElement('button');
3761
+ x.type = 'button';
3762
+ x.className = 'icon-button ir-dialog-close';
3763
+ x.setAttribute('aria-label', reportTexts['common.dismiss'] || 'Close');
3764
+ x.textContent = '×';
3765
+ x.addEventListener('click', () => dialog.close());
3766
+ const head = document.createElement('div');
3767
+ head.className = 'ir-dialog-head';
3768
+ head.append(title, x);
3769
+ dialog.append(head, body);
3770
+ document.body.append(dialog);
3771
+ dialog.addEventListener('close', () => {
3772
+ item.append(body);
3773
+ dialog.remove();
3774
+ menu?.querySelector('summary')?.focus();
3775
+ });
3776
+ dialog.showModal();
3777
+ body.querySelector('select, input, a, button')?.focus();
3778
+ }, true);
3779
+ // one submenu open at a time
3780
+ document.addEventListener('toggle', (e) => {
3781
+ const sub = e.target;
3782
+ if (!(sub instanceof HTMLDetailsElement) || !sub.matches('.ir-sub') || !sub.open) return;
3783
+ sub.parentElement?.parentElement?.querySelectorAll(':scope > li > details.ir-sub[open]').forEach((d) => { if (d !== sub) d.open = false; });
3784
+ }, true);
3785
+ })();
3786
+
3787
+ // (sprint 76) A report column heading opens a menu: sort ascending or descending, hide, control break, and
3788
+ // the column's values (with counts, a search box) to filter by; the sort arrow still sorts at once.
3789
+ (() => {
3790
+ let open = null;
3791
+ const shut = () => { open?.remove(); open = null; };
3792
+ document.addEventListener('click', (e) => {
3793
+ const a = e.target.closest?.('th[data-col] > a');
3794
+ if (!a) {
3795
+ if (open && !open.contains(e.target)) shut();
3796
+ return;
3797
+ }
3798
+ const th = a.parentElement;
3799
+ // the sort indicator keeps sorting
3800
+ if (e.target.closest('.sort-ind') && th.getAttribute('aria-sort') !== 'none') return;
3801
+ e.preventDefault();
3802
+ shut();
3803
+ const menu = document.createElement('div');
3804
+ menu.className = 'th-menu menu-panel';
3805
+ menu.setAttribute('role', 'dialog');
3806
+ menu.setAttribute('aria-label', th.dataset.label);
3807
+ const t = (k, d) => reportTexts[k] || d;
3808
+ const link = (href, text) => { const l = document.createElement('a'); l.href = href; l.textContent = text; return l; };
3809
+ const actions = document.createElement('div');
3810
+ actions.className = 'th-menu-actions';
3811
+ actions.append(link(th.dataset.asc, `▲ ${t('report.sort_asc', 'Sort ascending')}`), link(th.dataset.desc, `▼ ${t('report.sort_desc', 'Sort descending')}`),
3812
+ link(th.dataset.hide, t('report.hide', 'Hide column')), link(th.dataset.break, t('report.break', 'Control break')));
3813
+ const search = document.createElement('input');
3814
+ search.type = 'search';
3815
+ search.placeholder = t('report.filter_values', 'Filter the values');
3816
+ search.setAttribute('aria-label', search.placeholder);
3817
+ const list = document.createElement('ul');
3818
+ list.className = 'th-menu-values';
3819
+ menu.append(actions, search, list);
3820
+ document.body.append(menu);
3821
+ const box = th.getBoundingClientRect();
3822
+ menu.style.setProperty('position', 'absolute');
3823
+ menu.style.setProperty('top', `${window.scrollY + box.bottom + 4}px`);
3824
+ menu.style.setProperty('left', `${Math.max(8, Math.min(window.scrollX + box.left, window.scrollX + document.documentElement.clientWidth - 288))}px`);
3825
+ open = menu;
3826
+ const here = new URL(location.href);
3827
+ const url = new URL(th.dataset.values, location.href);
3828
+ here.searchParams.forEach((v, k) => url.searchParams.append(k, v));
3829
+ url.searchParams.set('col', th.dataset.col);
3830
+ fetch(url, { headers: { accept: 'application/json' }, credentials: 'same-origin' })
3831
+ .then((r) => (r.ok ? r.json() : { values: [] }))
3832
+ .then(({ values }) => {
3833
+ if (!values?.length) {
3834
+ const li = document.createElement('li');
3835
+ li.className = 'muted';
3836
+ li.textContent = t('report.no_values', 'No values');
3837
+ list.append(li);
3838
+ return;
3839
+ }
3840
+ for (const { value, count } of values) {
3841
+ if (value === null) continue;
3842
+ const target = new URL(location.href);
3843
+ for (const k of [...target.searchParams.keys()]) if (/^r\d+_(p|k)$/.test(k)) target.searchParams.delete(k);
3844
+ target.searchParams.append(th.dataset.filter, `${th.dataset.col}|eq|${value}`);
3845
+ const li = document.createElement('li');
3846
+ const l = link(target.toString(), value);
3847
+ const n = document.createElement('span');
3848
+ n.className = 'muted';
3849
+ n.textContent = String(count);
3850
+ l.append(n);
3851
+ li.append(l);
3852
+ list.append(li);
3853
+ }
3854
+ })
3855
+ .catch(() => {});
3856
+ search.addEventListener('input', () => {
3857
+ const q = search.value.trim().toLowerCase();
3858
+ list.querySelectorAll('li').forEach((li) => { li.hidden = !!q && !li.textContent.toLowerCase().includes(q); });
3859
+ });
3860
+ actions.querySelector('a')?.focus();
3861
+ menu.addEventListener('keydown', (k) => { if (k.key === 'Escape') { shut(); a.focus(); } });
3862
+ });
3863
+ window.addEventListener('resize', shut);
3864
+ })();
3865
+
3866
+ // (sprint 76) the report pager's rows per page: sent on change
3867
+ document.addEventListener('change', (e) => {
3868
+ if (e.target.matches?.('select[data-go]')) e.target.form?.submit();
3869
+ });
3870
+
3871
+ // (sprint 76) A date item in the application's date format: the calendar button opens the browser's date
3872
+ // picker on a hidden date field; the chosen date is written into the text field in the mask.
3873
+ (() => {
3874
+ const pad = (n) => String(n).padStart(2, '0');
3875
+ const TOKEN = /"[^"]*"|YYYY|YY|MONTH|MON|MM|DD/gi;
3876
+ const format = (iso, mask, months) => {
3877
+ const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(iso);
3878
+ if (!m) return '';
3879
+ const [y, mo, d] = m.slice(1).map(Number);
3880
+ return mask.replace(TOKEN, (tok) => {
3881
+ if (tok.startsWith('"')) return tok.slice(1, -1);
3882
+ const up = tok.toUpperCase();
3883
+ const cased = (s) => (tok === up ? s.toUpperCase() : tok[1] && tok[1] !== up[1] ? s[0].toUpperCase() + s.slice(1) : s);
3884
+ if (up === 'YYYY') return String(y);
3885
+ if (up === 'YY') return pad(y % 100);
3886
+ if (up === 'MM') return pad(mo);
3887
+ if (up === 'DD') return pad(d);
3888
+ if (up === 'MONTH') return cased(months.long[mo - 1]);
3889
+ if (up === 'MON') return cased(months.short[mo - 1]);
3890
+ return tok;
3891
+ });
3892
+ };
3893
+ document.addEventListener('click', (e) => {
3894
+ const btn = e.target.closest?.('[data-date-pick]');
3895
+ if (!btn) return;
3896
+ const native = document.querySelector(`input[data-date-for="${CSS.escape(btn.dataset.datePick)}"]`);
3897
+ if (!native) return;
3898
+ try {
3899
+ native.showPicker();
3900
+ } catch {
3901
+ native.focus();
3902
+ }
3903
+ });
3904
+ document.addEventListener('change', (e) => {
3905
+ const native = e.target;
3906
+ if (!native.matches?.('input[data-date-for]')) return;
3907
+ const text = document.getElementById(native.dataset.dateFor);
3908
+ if (!text) return;
3909
+ let months = { long: [], short: [] };
3910
+ try { months = JSON.parse(text.dataset.months || '{}'); } catch {}
3911
+ text.value = format(native.value, text.dataset.dateMask || 'YYYY-MM-DD', months);
3912
+ text.dispatchEvent(new Event('change', { bubbles: true }));
3913
+ text.focus();
3914
+ });
3915
+ })();
@@ -44,6 +44,7 @@
44
44
  --bg: #1d1c1b; --surface: #2a2927; --surface-2: #31302d; --surface-3: #3b3936; --text: #e9e6e1; --muted: #aaa59d;
45
45
  --border: #44423e; --accent: #62b2ea; --accent-text: #0b1824; --accent-soft: #233a4c; --gold: #e3b75e;
46
46
  --canvas: #211f1e; --block: #33322f; --block-head: #3d3b38; --code-bg: #242321; --warn: #e0a740;
47
+ --pd-c-region: #7fb4ea; --pd-c-item: #7ccf8c; --pd-c-button: #f0a35e; --pd-c-da: #c49cf0; --pd-c-process: #ee8fbf;
47
48
  color-scheme: dark;
48
49
  }
49
50
  }
@@ -51,6 +52,7 @@
51
52
  --bg: #1d1c1b; --surface: #2a2927; --surface-2: #31302d; --surface-3: #3b3936; --text: #e9e6e1; --muted: #aaa59d;
52
53
  --border: #44423e; --accent: #62b2ea; --accent-text: #0b1824; --accent-soft: #233a4c; --gold: #e3b75e;
53
54
  --canvas: #211f1e; --block: #33322f; --block-head: #3d3b38; --code-bg: #242321; --warn: #e0a740;
55
+ --pd-c-region: #7fb4ea; --pd-c-item: #7ccf8c; --pd-c-button: #f0a35e; --pd-c-da: #c49cf0; --pd-c-process: #ee8fbf;
54
56
  color-scheme: dark;
55
57
  }
56
58
 
@@ -900,3 +902,113 @@ details.prop-group-more[open] > summary { margin-bottom: 8px; }
900
902
  .progress-spinner { width: 1.25rem; height: 1.25rem; border-radius: 50%; border: 3px solid var(--border); border-top-color: var(--accent); animation: progress-spin 0.8s linear infinite; }
901
903
  @keyframes progress-spin { to { transform: rotate(360deg); } }
902
904
  @media (prefers-reduced-motion: reduce) { .progress-spinner { animation: none; } }
905
+
906
+ /* (sprint 74) the default-password warning: dismissible; a dot on the avatar */
907
+ .alert-dismissible { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; }
908
+ .alert-dismissible form { margin: 0; }
909
+ .rail-avatar.has-dot { position: relative; }
910
+ .rail-avatar.has-dot::after { content: ""; position: absolute; top: 2px; right: 2px; width: 9px; height: 9px; border-radius: 50%; background: var(--danger, #c23028); box-shadow: 0 0 0 2px var(--rail-bg, #1b2330); }
911
+ .weak-note { color: var(--danger, #c23028) !important; }
912
+
913
+ /* (sprint 74) Shared Components: groups of component types with counts; a list per type */
914
+ .sc-head, .sc-list-head { display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap; margin-bottom: 1rem; }
915
+ .sc-list-head h1 { margin: 0; }
916
+ .sc-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(17rem, 1fr)); gap: 1rem; align-items: start; }
917
+ .sc-group .region-header h2 { display: flex; align-items: center; gap: 0.5rem; }
918
+ .sc-links { list-style: none; margin: 0; padding: 0.4rem 0; }
919
+ .sc-links li { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; padding: 0.3rem 1rem; }
920
+ .sc-links a { overflow-wrap: anywhere; min-width: 0; }
921
+ .sc-count { font-size: 0.78rem; font-weight: 600; color: var(--muted); background: var(--surface-3); border-radius: 99px; padding: 0.05rem 0.5rem; }
922
+
923
+ /* (sprint 74) application Settings in tabs */
924
+ .settings-tabs { display: flex; flex-wrap: wrap; gap: 0.25rem; border-bottom: 1px solid var(--border); margin: 0 0 1rem; }
925
+ .settings-tabs a { padding: 0.5rem 0.85rem; color: var(--muted); text-decoration: none; border-bottom: 2px solid transparent; margin-bottom: -1px; }
926
+ .settings-tabs a:hover { color: var(--text); }
927
+ .settings-tabs a[aria-current="page"] { color: var(--text); border-bottom-color: var(--accent); font-weight: 600; }
928
+ .settings-body { display: grid; gap: 1rem; max-width: 64rem; }
929
+ .settings-form { display: grid; gap: 1rem; }
930
+
931
+ /* (sprint 74) the application home: search, view and Create page above the pages; Tasks on the right */
932
+ .app-home { display: grid; grid-template-columns: minmax(0, 1fr) 15rem; gap: 1rem; align-items: start; }
933
+ @media (max-width: 1100px) { .app-home { grid-template-columns: 1fr; } }
934
+ .app-home-main { display: grid; gap: 1rem; min-width: 0; }
935
+ .pages-bar { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; }
936
+ .pages-bar .search { flex: 1 1 16rem; margin: 0; }
937
+ .pages-bar .search input { flex: 1; min-width: 0; }
938
+ .view-toggle a { display: inline-flex; align-items: center; gap: 0.3rem; }
939
+ .page-icons { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr)); gap: 0.6rem; }
940
+ .page-icon { display: grid; gap: 0.15rem; padding: 0.7rem 0.8rem; border: 1px solid var(--border); border-radius: var(--radius); color: var(--text); text-decoration: none; overflow-wrap: anywhere; }
941
+ .page-icon:hover, .page-icon:focus-visible { border-color: var(--accent); }
942
+ .page-icon-no { font-size: 1.3rem; font-weight: 700; color: var(--accent); }
943
+ .app-tasks .sc-links a { display: inline-flex; align-items: center; gap: 0.45rem; }
944
+
945
+ /* (sprint 75) SQL Workshop: main tools as tabs and a More menu (no horizontal scrolling); the Object Browser */
946
+ .sw-tabs { overflow: visible; flex-wrap: wrap; }
947
+ .sw-more { position: relative; }
948
+ .sw-more > summary { list-style: none; cursor: pointer; }
949
+ .sw-more > summary::-webkit-details-marker { display: none; }
950
+ .sw-more .menu-panel a[aria-current] { font-weight: 600; }
951
+ .ob-head { display: flex; flex-wrap: wrap; align-items: flex-end; justify-content: space-between; gap: 0.75rem; margin-bottom: 1rem; }
952
+ .ob-schema { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; }
953
+ .ob-schema .label { margin: 0; }
954
+ .ob-create > summary { list-style: none; }
955
+ .ob-create > summary::-webkit-details-marker { display: none; }
956
+ .ob-filter { padding: 0.5rem; }
957
+ .ob-filter input { width: 100%; }
958
+ .ob-folder > summary { display: flex; align-items: center; gap: 0.45rem; padding: 0.4rem 0.6rem; cursor: pointer; font-weight: 600; list-style: none; }
959
+ .ob-folder > summary::-webkit-details-marker { display: none; }
960
+ .ob-folder > summary .kind { margin-left: auto; }
961
+ .ob-folder .tree { padding-left: 0.75rem; }
962
+ .ob-tiles { display: grid; grid-template-columns: repeat(auto-fill, minmax(13rem, 1fr)); gap: 0.5rem; margin-bottom: 0.75rem; }
963
+ .ob-title { display: flex; align-items: center; gap: 0.5rem; margin: 0 0 0.5rem; overflow-wrap: anywhere; }
964
+ .ob-actions { display: grid; gap: 0.6rem; margin-top: 1rem; }
965
+ .ob-action { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; }
966
+ .ob-action input:not([type=checkbox]) { width: 10rem; }
967
+
968
+ /* (sprint 77) the page template's display positions and a region's button positions on the layout */
969
+ .pd-zone { display: grid; gap: 0.3em; margin-bottom: 10px; }
970
+ .pd-zone-label { color: var(--muted); font-size: 0.74em; font-weight: 650; letter-spacing: 0.06em; text-transform: uppercase; }
971
+ .pd-zone-label small { text-transform: none; letter-spacing: 0; font-weight: 400; }
972
+ .pd-zone.is-empty .pd-grid-end { padding: 0.3em; }
973
+ .pd-columns { display: grid; gap: 10px; align-items: start; }
974
+ .pd-columns > .pd-zone { margin-bottom: 0; min-width: 0; }
975
+ .pd-columns:has(> .pd-zone-left) { grid-template-columns: minmax(9rem, 1fr) 3fr; }
976
+ .pd-columns:has(> .pd-zone-right) { grid-template-columns: 3fr minmax(9rem, 1fr); }
977
+ .pd-columns:has(> .pd-zone-left):has(> .pd-zone-right) { grid-template-columns: minmax(9rem, 1fr) 3fr minmax(9rem, 1fr); }
978
+ .pd-columns + .pd-zone { margin-top: 10px; }
979
+ .pd-zone-left .pd-grid > .pd-region, .pd-zone-right .pd-grid > .pd-region { grid-column: 1 / -1; }
980
+ .pd-slots { display: flex; flex-wrap: wrap; gap: 0.3em; }
981
+ .pd-slots-bottom > [data-slot="next"] { margin-left: auto; }
982
+ .pd-slot-spare { display: none; min-height: 1.6em; }
983
+ .pd-dragging-button .pd-slot-spare { display: flex; }
984
+ .pd-canvas:not(.pd-dragging-button) .pd-slots:not(:has(> .pd-slot:not(.pd-slot-spare))) { display: none; }
985
+ @media (max-width: 640px) { .pd-canvas .pd-columns:has(> .pd-zone) { grid-template-columns: minmax(0, 1fr); } }
986
+
987
+ /* (sprint 77) the rendering tree's context menu, the Copy to other page dialog, component colours */
988
+ .pd-menu { position: fixed; z-index: 60; min-width: 13rem; padding: 4px; background: var(--surface, #fff); color: var(--text); border: 1px solid var(--border); border-radius: 4px; box-shadow: 0 6px 20px rgb(0 0 0 / 0.18); font-size: 13px; }
989
+ .pd-menu [role=menuitem] { display: block; width: 100%; padding: 0.35rem 0.6rem; border: 0; border-radius: 3px; background: none; color: inherit; font: inherit; text-align: left; cursor: pointer; }
990
+ .pd-menu [role=menuitem]:is(:hover, :focus-visible) { background: var(--sel-bg); color: var(--sel-text); outline: none; }
991
+ .pd-menu [role=menuitem].is-danger { color: var(--danger); }
992
+ .pd-menu [role=separator] { height: 1px; margin: 4px 2px; background: var(--border); }
993
+ .pd-dialog { width: min(28rem, calc(100vw - 32px)); padding: 1rem 1.1rem; border: 1px solid var(--border); border-radius: 6px; background: var(--surface, #fff); color: var(--text); }
994
+ .pd-dialog::backdrop { background: rgb(0 0 0 / 0.35); }
995
+ .pd-dialog h2 { margin: 0 0 0.5rem; font-size: 1.05rem; }
996
+ .pd-dialog-buttons { display: flex; justify-content: flex-end; gap: 0.5rem; margin-top: 1rem; }
997
+ .pd-node[data-kind="region"] .icon { color: var(--pd-c-region, #2f6fb3); }
998
+ .pd-node[data-kind="item"] .icon { color: var(--pd-c-item, #2e8540); }
999
+ .pd-node[data-kind="button"] .icon { color: var(--pd-c-button, #b4570f); }
1000
+ .pd-node[data-kind="dynamic_action"] .icon { color: var(--pd-c-da, #8a4fbf); }
1001
+ .pd-node[data-kind="process"] .icon, .pd-node[data-kind="computation"] .icon { color: var(--pd-c-process, #a23b72); }
1002
+
1003
+ /* (sprint 77) the property editor: Common / All, Go to group, the Template Options button; the page number */
1004
+ .pe-more { display: contents; }
1005
+ .pe-show-common [data-more] { display: none; }
1006
+ .pe-show-common fieldset.pe-group-more { display: none; }
1007
+ .pe-view { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; padding: 4px 8px 6px; }
1008
+ .pe-view .segmented { margin-top: 0; }
1009
+ .pe-view .segmented button { font-size: 12px; padding: 2px 8px; min-height: 0; }
1010
+ .pe-goto { font-size: 12px; max-width: 12rem; }
1011
+ .pe-options-open .btn { justify-self: start; }
1012
+ .pd-goto-no input { width: 5.5em; font-variant-numeric: tabular-nums; }
1013
+ /* on narrower screens the previous / next arrows and the page list do it */
1014
+ @media (max-width: 1279px) { .pd-goto-no { display: none; } }