pgkiln 0.41.0 → 0.43.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 (68) hide show
  1. package/db/migrations/097_generated_app_quality.sql +911 -0
  2. package/db/migrations/098_app_activity.sql +37 -0
  3. package/db/migrations/099_layout_positions.sql +29 -0
  4. package/docs/apex-feature-parity.md +38 -11
  5. package/docs/guide/03-builder.md +133 -31
  6. package/docs/guide/04-pages-and-regions.md +46 -3
  7. package/docs/guide/05-items.md +1 -1
  8. package/docs/guide/09-reference.md +10 -3
  9. package/docs/guide/11-from-apex.md +1 -1
  10. package/docs/guide/12-development.md +3 -2
  11. package/docs/guide/14-globalization.md +7 -3
  12. package/docs/guide/17-mobile.md +1 -1
  13. package/package.json +1 -1
  14. package/public/app.css +233 -33
  15. package/public/app.js +263 -0
  16. package/public/builder.css +142 -2
  17. package/public/builder.js +311 -3
  18. package/src/builder/appfromfile.ts +5 -4
  19. package/src/builder/appsheets.ts +23 -9
  20. package/src/builder/appwizard.ts +215 -70
  21. package/src/builder/arrange.ts +133 -17
  22. package/src/builder/blueprints.ts +2 -1
  23. package/src/builder/components.ts +29 -23
  24. package/src/builder/designer.ts +67 -18
  25. package/src/builder/forms.ts +4 -1
  26. package/src/builder/home.ts +2 -32
  27. package/src/builder/newapp.ts +182 -5
  28. package/src/builder/object-browser.ts +342 -0
  29. package/src/builder/pwa.ts +3 -3
  30. package/src/builder/routes.ts +86 -51
  31. package/src/builder/shared.ts +103 -1
  32. package/src/builder/sql.ts +13 -65
  33. package/src/builder/ui.ts +21 -6
  34. package/src/i18n/ar.ts +17 -0
  35. package/src/i18n/cs.ts +17 -0
  36. package/src/i18n/da.ts +17 -0
  37. package/src/i18n/de.ts +17 -0
  38. package/src/i18n/el.ts +17 -0
  39. package/src/i18n/es.ts +17 -0
  40. package/src/i18n/fi.ts +17 -0
  41. package/src/i18n/fr.ts +17 -0
  42. package/src/i18n/he.ts +17 -0
  43. package/src/i18n/it.ts +17 -0
  44. package/src/i18n/ja.ts +17 -0
  45. package/src/i18n/ko.ts +17 -0
  46. package/src/i18n/nb.ts +17 -0
  47. package/src/i18n/pl.ts +17 -0
  48. package/src/i18n/pt.ts +17 -0
  49. package/src/i18n/ru.ts +17 -0
  50. package/src/i18n/sv.ts +17 -0
  51. package/src/i18n/tr.ts +17 -0
  52. package/src/i18n/uk.ts +17 -0
  53. package/src/i18n/zh.ts +17 -0
  54. package/src/i18n.ts +34 -0
  55. package/src/icons.ts +2 -1
  56. package/src/layout.ts +2 -2
  57. package/src/metadata.ts +5 -1
  58. package/src/runtime/charts.ts +8 -3
  59. package/src/runtime/devbar.ts +35 -14
  60. package/src/runtime/engine.ts +13 -4
  61. package/src/runtime/format.ts +52 -0
  62. package/src/runtime/items.ts +27 -0
  63. package/src/runtime/locale.ts +5 -2
  64. package/src/runtime/regions.ts +61 -4
  65. package/src/runtime/render.ts +38 -8
  66. package/src/runtime/report.ts +149 -88
  67. package/src/runtime/routes.ts +34 -3
  68. package/src/runtime/styles.ts +5 -2
package/public/app.js CHANGED
@@ -828,6 +828,15 @@ const pgkilnUnsaved = (() => {
828
828
  });
829
829
  drawer.addEventListener('change', () => toggle.setAttribute('aria-expanded', String(navOpen())));
830
830
  }
831
+ // (097) a long menu shows the current page's entry: the menu scrolls, not the page
832
+ {
833
+ const nav = document.getElementById('t-nav');
834
+ const current = nav?.querySelector('a[aria-current="page"]');
835
+ if (nav && current && nav.scrollHeight > nav.clientHeight) {
836
+ const top = current.getBoundingClientRect().top - nav.getBoundingClientRect().top;
837
+ if (top < 0 || top + current.offsetHeight > nav.clientHeight) nav.scrollTop += top - nav.clientHeight / 3;
838
+ }
839
+ }
831
840
 
832
841
  // ------------------------------------------------------------ modal dialogs
833
842
  const POSITIONS = ['center', 'left', 'right', 'top', 'bottom'];
@@ -3606,6 +3615,77 @@ function areaFilter(map, f) {
3606
3615
  check();
3607
3616
  })();
3608
3617
 
3618
+ // (sprint 72) Install app (PWA): the header's button appears when the browser offers the installation.
3619
+ (() => {
3620
+ const button = document.querySelector('[data-pwa-install]');
3621
+ if (!button) return;
3622
+ let offer = null;
3623
+ window.addEventListener('beforeinstallprompt', (e) => {
3624
+ e.preventDefault();
3625
+ offer = e;
3626
+ button.hidden = false;
3627
+ });
3628
+ button.addEventListener('click', async () => {
3629
+ if (!offer) return;
3630
+ offer.prompt();
3631
+ await offer.userChoice.catch(() => null);
3632
+ offer = null;
3633
+ button.hidden = true;
3634
+ });
3635
+ window.addEventListener('appinstalled', () => { button.hidden = true; });
3636
+ })();
3637
+
3638
+ // (sprint 72) The developer toolbar: docked where the developer chose (bottom, top, left, right;
3639
+ // kept in this browser), shrunk to a slim tab after a few seconds without use (unless turned off),
3640
+ // back when pointed at or focused. Its panels close when another opens or on Escape.
3641
+ (() => {
3642
+ const bar = document.querySelector('.devbar');
3643
+ if (!bar) return;
3644
+ const KEY = 'pgkiln.devbar';
3645
+ let prefs = { dock: 'bottom', autohide: true };
3646
+ try { prefs = { ...prefs, ...JSON.parse(localStorage.getItem(KEY) || '{}') }; } catch {}
3647
+ if (!['bottom', 'top', 'left', 'right'].includes(prefs.dock)) prefs.dock = 'bottom';
3648
+ const save = () => { try { localStorage.setItem(KEY, JSON.stringify(prefs)); } catch {} };
3649
+ const settings = bar.querySelector('.devbar-settings');
3650
+ settings?.closest('li')?.removeAttribute('hidden');
3651
+ const apply = () => {
3652
+ bar.dataset.dock = prefs.dock;
3653
+ bar.querySelectorAll('input[name="devbar-dock"]').forEach((r) => { r.checked = r.value === prefs.dock; });
3654
+ const auto = bar.querySelector('input[name="devbar-autohide"]');
3655
+ if (auto) auto.checked = !!prefs.autohide;
3656
+ };
3657
+ apply();
3658
+ bar.addEventListener('change', (e) => {
3659
+ if (e.target.name === 'devbar-dock') prefs.dock = e.target.value;
3660
+ else if (e.target.name === 'devbar-autohide') prefs.autohide = e.target.checked;
3661
+ else return;
3662
+ save();
3663
+ apply();
3664
+ });
3665
+ const menus = [...bar.querySelectorAll('details.devbar-menu')];
3666
+ menus.forEach((m) => m.addEventListener('toggle', () => { if (m.open) menus.forEach((o) => { if (o !== m) o.open = false; }); }));
3667
+ document.addEventListener('keydown', (e) => {
3668
+ if (e.key === 'Escape' && menus.some((m) => m.open)) {
3669
+ const open = menus.find((m) => m.open);
3670
+ open.open = false;
3671
+ open.querySelector('summary')?.focus();
3672
+ }
3673
+ });
3674
+ document.addEventListener('click', (e) => { if (!bar.contains(e.target)) menus.forEach((m) => { m.open = false; }); });
3675
+ let timer = 0;
3676
+ const busy = () => bar.matches(':hover') || bar.contains(document.activeElement) || menus.some((m) => m.open) || !!bar.querySelector('[aria-pressed="true"]');
3677
+ const later = () => {
3678
+ clearTimeout(timer);
3679
+ if (prefs.autohide) timer = setTimeout(() => { if (!busy()) bar.classList.add('is-idle'); else later(); }, 3000);
3680
+ };
3681
+ const wake = () => { bar.classList.remove('is-idle'); later(); };
3682
+ bar.addEventListener('pointerenter', wake);
3683
+ bar.addEventListener('focusin', wake);
3684
+ bar.addEventListener('pointerleave', later);
3685
+ bar.addEventListener('change', wake);
3686
+ later();
3687
+ })();
3688
+
3609
3689
  // (sprint 59) Quick edit from the developer toolbar (APEX: Quick Edit): while it is on, pointing at a
3610
3690
  // region, item or button outlines it and a click opens it in the Page Designer (in the builder's tab);
3611
3691
  // Esc or the button again turns it off. The toolbar is only there for developers of the application.
@@ -3645,3 +3725,186 @@ function areaFilter(map, f) {
3645
3725
  }, true);
3646
3726
  document.addEventListener('keydown', (e) => { if (on && e.key === 'Escape') set(false); });
3647
3727
  })();
3728
+
3729
+ // (sprint 76) the page's texts for the report menus below (render.ts CLIENT_TEXTS)
3730
+ const reportTexts = (() => {
3731
+ try {
3732
+ return JSON.parse(document.getElementById('pgkiln-meta')?.textContent || '{}').texts || {};
3733
+ } catch {
3734
+ return {};
3735
+ }
3736
+ })();
3737
+
3738
+ // (sprint 76) The interactive report's Actions menu: an entry opens its form in a dialog (APEX: the
3739
+ // Actions dialogs); the form fields stay the same elements, moved into the dialog and back on close.
3740
+ (() => {
3741
+ const close = (details) => { if (details) details.open = false; };
3742
+ document.addEventListener('toggle', (e) => {
3743
+ const item = e.target;
3744
+ if (!(item instanceof HTMLDetailsElement) || !item.matches('.ir-item[data-ir-dialog]') || !item.open) return;
3745
+ const body = item.querySelector(':scope > .ir-body');
3746
+ const menu = item.closest('details.actions-menu');
3747
+ if (!body || typeof HTMLDialogElement !== 'function') return;
3748
+ item.open = false;
3749
+ menu?.querySelectorAll('details.ir-sub[open]').forEach(close);
3750
+ close(menu);
3751
+ const dialog = document.createElement('dialog');
3752
+ dialog.className = 'ir-dialog';
3753
+ const title = document.createElement('h2');
3754
+ title.textContent = item.querySelector('summary span')?.textContent ?? '';
3755
+ const x = document.createElement('button');
3756
+ x.type = 'button';
3757
+ x.className = 'icon-button ir-dialog-close';
3758
+ x.setAttribute('aria-label', reportTexts['common.dismiss'] || 'Close');
3759
+ x.textContent = '×';
3760
+ x.addEventListener('click', () => dialog.close());
3761
+ const head = document.createElement('div');
3762
+ head.className = 'ir-dialog-head';
3763
+ head.append(title, x);
3764
+ dialog.append(head, body);
3765
+ document.body.append(dialog);
3766
+ dialog.addEventListener('close', () => {
3767
+ item.append(body);
3768
+ dialog.remove();
3769
+ menu?.querySelector('summary')?.focus();
3770
+ });
3771
+ dialog.showModal();
3772
+ body.querySelector('select, input, a, button')?.focus();
3773
+ }, true);
3774
+ // one submenu open at a time
3775
+ document.addEventListener('toggle', (e) => {
3776
+ const sub = e.target;
3777
+ if (!(sub instanceof HTMLDetailsElement) || !sub.matches('.ir-sub') || !sub.open) return;
3778
+ sub.parentElement?.parentElement?.querySelectorAll(':scope > li > details.ir-sub[open]').forEach((d) => { if (d !== sub) d.open = false; });
3779
+ }, true);
3780
+ })();
3781
+
3782
+ // (sprint 76) A report column heading opens a menu: sort ascending or descending, hide, control break, and
3783
+ // the column's values (with counts, a search box) to filter by; the sort arrow still sorts at once.
3784
+ (() => {
3785
+ let open = null;
3786
+ const shut = () => { open?.remove(); open = null; };
3787
+ document.addEventListener('click', (e) => {
3788
+ const a = e.target.closest?.('th[data-col] > a');
3789
+ if (!a) {
3790
+ if (open && !open.contains(e.target)) shut();
3791
+ return;
3792
+ }
3793
+ const th = a.parentElement;
3794
+ // the sort indicator keeps sorting
3795
+ if (e.target.closest('.sort-ind') && th.getAttribute('aria-sort') !== 'none') return;
3796
+ e.preventDefault();
3797
+ shut();
3798
+ const menu = document.createElement('div');
3799
+ menu.className = 'th-menu menu-panel';
3800
+ menu.setAttribute('role', 'dialog');
3801
+ menu.setAttribute('aria-label', th.dataset.label);
3802
+ const t = (k, d) => reportTexts[k] || d;
3803
+ const link = (href, text) => { const l = document.createElement('a'); l.href = href; l.textContent = text; return l; };
3804
+ const actions = document.createElement('div');
3805
+ actions.className = 'th-menu-actions';
3806
+ actions.append(link(th.dataset.asc, `▲ ${t('report.sort_asc', 'Sort ascending')}`), link(th.dataset.desc, `▼ ${t('report.sort_desc', 'Sort descending')}`),
3807
+ link(th.dataset.hide, t('report.hide', 'Hide column')), link(th.dataset.break, t('report.break', 'Control break')));
3808
+ const search = document.createElement('input');
3809
+ search.type = 'search';
3810
+ search.placeholder = t('report.filter_values', 'Filter the values');
3811
+ search.setAttribute('aria-label', search.placeholder);
3812
+ const list = document.createElement('ul');
3813
+ list.className = 'th-menu-values';
3814
+ menu.append(actions, search, list);
3815
+ document.body.append(menu);
3816
+ const box = th.getBoundingClientRect();
3817
+ menu.style.setProperty('position', 'absolute');
3818
+ menu.style.setProperty('top', `${window.scrollY + box.bottom + 4}px`);
3819
+ menu.style.setProperty('left', `${Math.max(8, Math.min(window.scrollX + box.left, window.scrollX + document.documentElement.clientWidth - 288))}px`);
3820
+ open = menu;
3821
+ const here = new URL(location.href);
3822
+ const url = new URL(th.dataset.values, location.href);
3823
+ here.searchParams.forEach((v, k) => url.searchParams.append(k, v));
3824
+ url.searchParams.set('col', th.dataset.col);
3825
+ fetch(url, { headers: { accept: 'application/json' }, credentials: 'same-origin' })
3826
+ .then((r) => (r.ok ? r.json() : { values: [] }))
3827
+ .then(({ values }) => {
3828
+ if (!values?.length) {
3829
+ const li = document.createElement('li');
3830
+ li.className = 'muted';
3831
+ li.textContent = t('report.no_values', 'No values');
3832
+ list.append(li);
3833
+ return;
3834
+ }
3835
+ for (const { value, count } of values) {
3836
+ if (value === null) continue;
3837
+ const target = new URL(location.href);
3838
+ for (const k of [...target.searchParams.keys()]) if (/^r\d+_(p|k)$/.test(k)) target.searchParams.delete(k);
3839
+ target.searchParams.append(th.dataset.filter, `${th.dataset.col}|eq|${value}`);
3840
+ const li = document.createElement('li');
3841
+ const l = link(target.toString(), value);
3842
+ const n = document.createElement('span');
3843
+ n.className = 'muted';
3844
+ n.textContent = String(count);
3845
+ l.append(n);
3846
+ li.append(l);
3847
+ list.append(li);
3848
+ }
3849
+ })
3850
+ .catch(() => {});
3851
+ search.addEventListener('input', () => {
3852
+ const q = search.value.trim().toLowerCase();
3853
+ list.querySelectorAll('li').forEach((li) => { li.hidden = !!q && !li.textContent.toLowerCase().includes(q); });
3854
+ });
3855
+ actions.querySelector('a')?.focus();
3856
+ menu.addEventListener('keydown', (k) => { if (k.key === 'Escape') { shut(); a.focus(); } });
3857
+ });
3858
+ window.addEventListener('resize', shut);
3859
+ })();
3860
+
3861
+ // (sprint 76) the report pager's rows per page: sent on change
3862
+ document.addEventListener('change', (e) => {
3863
+ if (e.target.matches?.('select[data-go]')) e.target.form?.submit();
3864
+ });
3865
+
3866
+ // (sprint 76) A date item in the application's date format: the calendar button opens the browser's date
3867
+ // picker on a hidden date field; the chosen date is written into the text field in the mask.
3868
+ (() => {
3869
+ const pad = (n) => String(n).padStart(2, '0');
3870
+ const TOKEN = /"[^"]*"|YYYY|YY|MONTH|MON|MM|DD/gi;
3871
+ const format = (iso, mask, months) => {
3872
+ const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(iso);
3873
+ if (!m) return '';
3874
+ const [y, mo, d] = m.slice(1).map(Number);
3875
+ return mask.replace(TOKEN, (tok) => {
3876
+ if (tok.startsWith('"')) return tok.slice(1, -1);
3877
+ const up = tok.toUpperCase();
3878
+ const cased = (s) => (tok === up ? s.toUpperCase() : tok[1] && tok[1] !== up[1] ? s[0].toUpperCase() + s.slice(1) : s);
3879
+ if (up === 'YYYY') return String(y);
3880
+ if (up === 'YY') return pad(y % 100);
3881
+ if (up === 'MM') return pad(mo);
3882
+ if (up === 'DD') return pad(d);
3883
+ if (up === 'MONTH') return cased(months.long[mo - 1]);
3884
+ if (up === 'MON') return cased(months.short[mo - 1]);
3885
+ return tok;
3886
+ });
3887
+ };
3888
+ document.addEventListener('click', (e) => {
3889
+ const btn = e.target.closest?.('[data-date-pick]');
3890
+ if (!btn) return;
3891
+ const native = document.querySelector(`input[data-date-for="${CSS.escape(btn.dataset.datePick)}"]`);
3892
+ if (!native) return;
3893
+ try {
3894
+ native.showPicker();
3895
+ } catch {
3896
+ native.focus();
3897
+ }
3898
+ });
3899
+ document.addEventListener('change', (e) => {
3900
+ const native = e.target;
3901
+ if (!native.matches?.('input[data-date-for]')) return;
3902
+ const text = document.getElementById(native.dataset.dateFor);
3903
+ if (!text) return;
3904
+ let months = { long: [], short: [] };
3905
+ try { months = JSON.parse(text.dataset.months || '{}'); } catch {}
3906
+ text.value = format(native.value, text.dataset.dateMask || 'YYYY-MM-DD', months);
3907
+ text.dispatchEvent(new Event('change', { bubbles: true }));
3908
+ text.focus();
3909
+ });
3910
+ })();
@@ -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
 
@@ -768,7 +770,12 @@ button.is-busy { opacity: 0.7; cursor: progress; }
768
770
  --font: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; --font-size: 15px; --shadow: 0 1px 2px rgb(16 24 40 / 6%), 0 1px 3px rgb(16 24 40 / 6%);
769
771
  color-scheme: light; margin-bottom: 1rem; border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden;
770
772
  background: var(--bg); color: var(--text); font: var(--font-size)/1.5 var(--font); }
771
- .tr-preview-iris { --bg: #f6f6fb; --surface-2: #f8f8fc; --surface-3: #eeeef6; --text: #1d1b2e; --muted: #5f5d75; --border: #e3e2ef;
773
+ .tr-preview-iris { --bg: #ffffff; --surface-2: #f7f6f5; --surface-3: #efedeb; --text: #1f1c1a; --muted: #645e59; --border: #e2dfdc;
774
+ --accent: #2b62c4; --accent-soft: #e9f0fb; --header: #3a3632; --header-text: #f7f5f3; --radius: 4px; --shadow: none;
775
+ --font: "Inter", "Segoe UI Variable Text", "Segoe UI", system-ui, -apple-system, Roboto, sans-serif; }
776
+ .tr-preview-iris .tr-preview-header { box-shadow: inset 0 3px 0 var(--accent); }
777
+ .tr-preview-iris .btn-hot { background: #2f2b28; border-color: #2f2b28; color: #fff; }
778
+ .tr-preview-lavender { --bg: #f6f6fb; --surface-2: #f8f8fc; --surface-3: #eeeef6; --text: #1d1b2e; --muted: #5f5d75; --border: #e3e2ef;
772
779
  --accent: #5146d8; --accent-soft: #eeedfd; --header: #1e1a4d; --header-text: #f1f0ff; --radius: 12px;
773
780
  --font: "Inter", "Segoe UI Variable Text", "Segoe UI", system-ui, -apple-system, Roboto, sans-serif; --shadow: 0 1px 2px rgb(30 27 75 / 5%), 0 2px 8px rgb(30 27 75 / 6%); }
774
781
  .tr-preview-header { display: flex; align-items: center; gap: 0.6rem; padding: 0.6rem 1rem; background: var(--header); color: var(--header-text); font-weight: 700; }
@@ -779,7 +786,7 @@ button.is-busy { opacity: 0.7; cursor: progress; }
779
786
  .tr-preview input { background: var(--surface); color: var(--text); border-color: var(--border); }
780
787
  .tr-preview .btn { background: var(--surface); color: var(--text); border-color: var(--border); border-radius: calc(var(--radius) * 0.75); font: inherit; }
781
788
  .tr-preview .btn-hot { background: var(--accent); border-color: var(--accent); color: var(--accent-text); }
782
- .tr-preview-iris .btn-hot { border-radius: 999px; }
789
+ .tr-preview-lavender .btn-hot { border-radius: 999px; }
783
790
 
784
791
  /* icon picker (forms.ts iconPicker) */
785
792
  .icon-picker > summary { display: inline-flex; align-items: center; gap: 0.5rem; min-height: 34px; padding: 0.3rem 0.6rem; border: 1px solid var(--border); border-radius: 6px; cursor: pointer; list-style: none; background: var(--surface); }
@@ -872,3 +879,136 @@ details.prop-group-more[open] > summary { margin-bottom: 8px; }
872
879
  .gal-foot { display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: 8px; }
873
880
  .gal-foot form { margin: 0; }
874
881
  .ab-group select { min-width: 140px; }
882
+
883
+ /* (sprint 73) the Create Application wizard on one page */
884
+ .create-sources { display: grid; grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr)); gap: 0.5rem; margin: 0.75rem 0 1rem; }
885
+ .create-source { display: flex; gap: 0.6rem; align-items: flex-start; padding: 0.6rem 0.75rem; border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); color: var(--text); text-decoration: none; }
886
+ .create-source:hover, .create-source:focus-visible { border-color: var(--accent); }
887
+ .create-source .icon { color: var(--accent); margin-top: 0.15rem; }
888
+ .create-source span { display: grid; gap: 0.1rem; min-width: 0; overflow-wrap: anywhere; }
889
+ .create-app .region + form, .create-app form .region { margin-top: 1rem; }
890
+ .aw-pages { list-style: none; margin: 0 0 0.5rem; padding: 0; display: grid; gap: 0.4rem; }
891
+ .aw-pages li { display: grid; grid-template-columns: minmax(10rem, 1fr) minmax(12rem, 16rem); gap: 0.15rem 0.75rem; align-items: center; padding: 0.35rem 0; border-bottom: 1px solid var(--border); }
892
+ .aw-pages li > small, .aw-pages li > .muted:last-child { grid-column: 1 / -1; }
893
+ .aw-page-kind { display: inline-flex; align-items: center; gap: 0.4rem; font-weight: 600; overflow-wrap: anywhere; min-width: 0; }
894
+ @media (max-width: 560px) { .aw-pages li { grid-template-columns: 1fr; } }
895
+ .aw-features { display: grid; grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr)); gap: 0 1rem; }
896
+ .aw-advanced > summary { cursor: pointer; list-style: none; }
897
+ .aw-advanced > summary::-webkit-details-marker { display: none; }
898
+ .aw-advanced .region-body { padding: 1rem; }
899
+ /* the overlay while a long form (Create application) is being processed */
900
+ .progress-overlay { position: fixed; inset: 0; z-index: 1000; display: grid; place-items: center; background: rgb(0 0 0 / 35%); }
901
+ .progress-box { display: flex; align-items: center; gap: 0.75rem; padding: 1rem 1.25rem; border-radius: var(--radius); background: var(--surface); color: var(--text); box-shadow: 0 8px 24px rgb(0 0 0 / 25%); }
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; }
903
+ @keyframes progress-spin { to { transform: rotate(360deg); } }
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; } }