@ziyarago/ui 0.9.0 → 0.11.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 (40) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +7 -7
  3. package/THIRD_PARTY_NOTICES.md +2 -2
  4. package/assets/fonts/jetbrains-mono/jetbrains-mono-cyrillic-ext.woff2 +0 -0
  5. package/assets/fonts/jetbrains-mono/jetbrains-mono-cyrillic.woff2 +0 -0
  6. package/assets/fonts/jetbrains-mono/jetbrains-mono-latin-ext.woff2 +0 -0
  7. package/assets/fonts/jetbrains-mono/jetbrains-mono-latin.woff2 +0 -0
  8. package/assets/fonts/manrope/manrope-cyrillic-ext.woff2 +0 -0
  9. package/assets/fonts/manrope/manrope-cyrillic.woff2 +0 -0
  10. package/assets/fonts/manrope/manrope-latin-ext.woff2 +0 -0
  11. package/assets/fonts/manrope/manrope-latin.woff2 +0 -0
  12. package/dist/llms-full.txt +89 -2
  13. package/dist/llms.txt +8 -3
  14. package/dist/manifest.json +745 -1
  15. package/dist/style.css +30 -5
  16. package/docs/INTEGRATION.md +3 -3
  17. package/docs/MIGRATION.md +3 -1
  18. package/package.json +2 -2
  19. package/src/compat/index.js +3 -2
  20. package/src/compat/vite.js +1 -1
  21. package/src/core/bools.js +1 -1
  22. package/src/core/components2.js +3 -3
  23. package/src/core/components7.js +13 -0
  24. package/src/core/pages.js +5 -1
  25. package/src/i18n/components/QiblaCompass.js +5 -0
  26. package/src/i18n/components/index.js +2 -0
  27. package/src/meta/domain.js +10 -0
  28. package/src/meta/forms.js +4 -0
  29. package/src/meta/groups.js +2 -2
  30. package/src/meta/pages.js +53 -0
  31. package/src/meta/templates.json +549 -1
  32. package/src/react/index.js +5 -0
  33. package/src/styles/components5.css +1 -1
  34. package/src/styles/components8.css +17 -1
  35. package/src/styles/effects.css +2 -1
  36. package/src/styles/fonts.css +9 -1
  37. package/src/vue/index.js +1 -1
  38. package/types/react.d.ts +291 -0
  39. package/types/vue-global.d.ts +5 -0
  40. package/types/vue.d.ts +208 -0
package/dist/style.css CHANGED
@@ -1,8 +1,16 @@
1
1
  /* @ziyarago/ui · generated by scripts/build.mjs — do not edit */
2
- @import url('https://fonts.googleapis.com/css2?family=Manrope:wght@200;300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap');
3
2
 
4
- /* ./fonts.css */
5
3
 
4
+ /* ./fonts.css */
5
+ /* Manrope and JetBrains Mono (SIL OFL 1.1), self-hosted variable fonts from Google Fonts, latin and cyrillic subsets. No request to Google, works under a strict CSP. */
6
+ @font-face{font-family:'JetBrains Mono';font-style:normal;font-weight:400 600;font-display:swap;src:url('../assets/fonts/jetbrains-mono/jetbrains-mono-cyrillic-ext.woff2') format('woff2');unicode-range:U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F}
7
+ @font-face{font-family:'JetBrains Mono';font-style:normal;font-weight:400 600;font-display:swap;src:url('../assets/fonts/jetbrains-mono/jetbrains-mono-cyrillic.woff2') format('woff2');unicode-range:U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116}
8
+ @font-face{font-family:'JetBrains Mono';font-style:normal;font-weight:400 600;font-display:swap;src:url('../assets/fonts/jetbrains-mono/jetbrains-mono-latin-ext.woff2') format('woff2');unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF}
9
+ @font-face{font-family:'JetBrains Mono';font-style:normal;font-weight:400 600;font-display:swap;src:url('../assets/fonts/jetbrains-mono/jetbrains-mono-latin.woff2') format('woff2');unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD}
10
+ @font-face{font-family:'Manrope';font-style:normal;font-weight:200 800;font-display:swap;src:url('../assets/fonts/manrope/manrope-cyrillic-ext.woff2') format('woff2');unicode-range:U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F}
11
+ @font-face{font-family:'Manrope';font-style:normal;font-weight:200 800;font-display:swap;src:url('../assets/fonts/manrope/manrope-cyrillic.woff2') format('woff2');unicode-range:U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116}
12
+ @font-face{font-family:'Manrope';font-style:normal;font-weight:200 800;font-display:swap;src:url('../assets/fonts/manrope/manrope-latin-ext.woff2') format('woff2');unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF}
13
+ @font-face{font-family:'Manrope';font-style:normal;font-weight:200 800;font-display:swap;src:url('../assets/fonts/manrope/manrope-latin.woff2') format('woff2');unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD}
6
14
  /* Thmanyah — free incl. commercial, https://font.thmanyah.com. Files in assets/fonts/thmanyah. Adjust URLs to your bundler.
7
15
  Thmanyah is visually heavier than Manrope at the same weight and has no 600 face. Weight ranges map the UI weights (300-900) so Arabic does not look bold:
8
16
  200-300 Light, 400-600 Regular, 700 Medium, 800 Bold, 900 Black. */
@@ -372,7 +380,7 @@
372
380
  .zg-pgroot{display:block;min-width:0;width:100%}.zg-pg{display:flex;flex-direction:column;gap:16px;min-width:0}.zg-pg__r{min-width:0}
373
381
  .zg-pg__auto{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,var(--min,340px)),1fr));gap:16px;align-items:start;min-width:0}
374
382
  .zg-pg__two{display:flex;flex-wrap:wrap;gap:16px;align-items:flex-start;min-width:0}.zg-pg__main{flex:999 1 460px;min-width:0;display:flex;flex-direction:column;gap:16px}.zg-pg__side{flex:1 1 300px;min-width:0;display:flex;flex-direction:column;gap:16px}.zg-pg__aside{flex:1 1 260px;min-width:0;max-width:100%;display:flex;flex-direction:column;gap:16px}
375
- .zg-pgauth{position:relative;min-height:100%;display:grid;place-items:center;padding:24px}.zg-pgauth__bg{position:absolute;inset:0;overflow:hidden}.zg-pgauth__c{position:relative;width:100%;max-width:440px}
383
+ .zg-pgauth{position:relative;min-height:100vh;min-height:100dvh;display:grid}.zg-pgauth__bg{position:absolute;inset:0;overflow:hidden}.zg-pgauth__c{position:relative;display:grid;place-items:center;padding:24px;min-height:inherit}.zg-pgauth .zg-auth{min-height:0;padding:0;width:100%;background:transparent;display:grid;place-items:center}.zg-pgauth .zg-auth__card{margin-inline:auto}
376
384
 
377
385
 
378
386
  /* ./components6.css */
@@ -494,8 +502,10 @@
494
502
  /* CommandPalette active row, dialog focus */
495
503
  .zg-menu__i.is-active,.zg-menu__i[aria-selected="true"]{background:var(--zg-s2)}
496
504
  .zg-modal:focus,.zg-sheet:focus,.zg-fsd:focus,.zg-saver:focus{outline:0}
505
+ /* auth page templates fill the viewport */
506
+ .zg-split2{min-height:100vh;min-height:100dvh}
497
507
  /* AuthShowcase: photo (dark framed card), light (gradient card + proof pill + glass caption), tint (coloured panel, pill fields) */
498
- .zg-ash{min-height:100%;padding:clamp(12px,3vw,40px);display:grid;place-items:center;--ash-r:28px;--ash-acc:var(--zg-brand)}
508
+ .zg-ash{min-height:100vh;min-height:100dvh;padding:clamp(12px,3vw,40px);display:grid;place-items:center;--ash-r:28px;--ash-acc:var(--zg-brand)}
499
509
  .zg-ash--tint{--ash-acc:oklch(.7 .13 215)}
500
510
  .zg-ash__title{margin:0;font:300 clamp(28px,3.4vw,40px)/1.1 var(--zg-font);letter-spacing:-.03em}
501
511
  .zg-ash__sub{margin:10px 0 0;color:var(--zg-text-2);line-height:1.55}
@@ -544,6 +554,20 @@
544
554
  /* page templates: sticky region */
545
555
  .zg-pg__two--sticky{align-items:stretch}.zg-pg__stick{position:sticky;inset-block-start:16px;align-self:flex-start;width:100%}
546
556
 
557
+ /* QiblaCompass */
558
+ .zg-qb{display:flex;flex-direction:column;align-items:center;gap:8px;max-width:100%;direction:ltr}
559
+ .zg-qb__svg{width:100%;height:auto;display:block}
560
+ .zg-qb__ring{fill:var(--zg-s1);stroke:var(--zg-stroke);stroke-width:1}
561
+ .zg-qb__tk{stroke:var(--zg-text-3);stroke-width:1;stroke-linecap:round}.zg-qb__tk--m{stroke:var(--zg-text-2);stroke-width:1.8}
562
+ .zg-qb__n{fill:var(--zg-red)}.zg-qb__ln{stroke:var(--zg-brand);stroke-width:3;stroke-linecap:round}
563
+ .zg-qb__kb{fill:var(--zg-bg);stroke:var(--zg-brand);stroke-width:2}.zg-qb__kg{fill:var(--zg-brand)}
564
+ .zg-qb__hub{fill:var(--zg-brand)}
565
+ .zg-qb__dial,.zg-qb__ptr{transition:transform .6s cubic-bezier(.2,.8,.2,1)}
566
+ .zg-qb__txt{display:flex;flex-direction:column;align-items:center;line-height:1.2;color:var(--zg-text-2);font-size:calc(13px*var(--dz,1))}
567
+ .zg-qb__txt b{font-size:calc(22px*var(--dz,1));font-weight:600;color:var(--zg-text-1)}.zg-qb__txt small{color:var(--zg-text-3)}
568
+ /* Numbers: tabular figures with a plain zero (no slashed or dotted zero) in KPI values, tables and code cells */
569
+ .zg-num,.zg-mono,.zg-kpi__value,.zg-table .num{font-feature-settings:"zero" 0,"tnum" 1}
570
+
547
571
 
548
572
  /* ./sizes/typography.css */
549
573
  /* size overrides for the typography group. Only [data-size] selectors, see the size scale at the end of tokens.css */
@@ -1433,7 +1457,7 @@
1433
1457
  [data-motion="off"] *,[data-motion="off"] *::before,[data-motion="off"] *::after{animation-duration:.001ms!important;animation-delay:0s!important;animation-iteration-count:1!important;transition-duration:.001ms!important;transition-delay:0s!important;scroll-behavior:auto!important}
1434
1458
 
1435
1459
  /* ── effects lite: flat surfaces for weak browsers and devices ── */
1436
- [data-fx="lite"] *,[data-fx="lite"] *::before,[data-fx="lite"] *::after{-webkit-backdrop-filter:none!important;backdrop-filter:none!important}
1460
+ [data-fx="lite"]:not([data-effects="full"]) *,[data-fx="lite"]:not([data-effects="full"]) *::before,[data-fx="lite"]:not([data-effects="full"]) *::after{-webkit-backdrop-filter:none!important;backdrop-filter:none!important}
1437
1461
  [data-fx="lite"]{--zg-shadow-1:0 2px 8px oklch(0 0 0 / .18);--zg-shadow-2:0 6px 20px oklch(0 0 0 / .28);--zg-brand-glow:transparent}
1438
1462
  [data-fx="lite"][data-theme="light"]{--zg-shadow-1:0 2px 8px oklch(0.4 0.03 60 / .12);--zg-shadow-2:0 6px 20px oklch(0.4 0.03 60 / .18)}
1439
1463
  [data-fx="lite"] .zg-glass{background:var(--zg-pop)}
@@ -1451,6 +1475,7 @@ html[data-bg]:not([data-bg="none"]),.zg-bg:not([data-bg="none"]){background-colo
1451
1475
  .zg-bg:not([data-bg="none"]){background-attachment:scroll}
1452
1476
  html[data-bg]:not([data-bg="none"]) .zg-root{background:transparent}
1453
1477
  html[data-motion="on"][data-bg]:not([data-bg="none"]){background-image:none}
1478
+ html[data-motion="on"][data-bg]:not([data-bg="none"]){overflow-x:clip}
1454
1479
  html[data-motion="on"][data-bg]:not([data-bg="none"])::before{content:"";position:fixed;inset:-25%;z-index:-1;pointer-events:none;background-image:var(--zg-bg-image);background-repeat:no-repeat;animation:zg-bg-float 18s ease-in-out infinite alternate,zg-bg-hue 40s linear infinite;will-change:transform,filter}
1455
1480
  @keyframes zg-bg-hue{from{filter:hue-rotate(0deg) saturate(1.15)}to{filter:hue-rotate(360deg) saturate(1.15)}}
1456
1481
  @keyframes zg-bg-float{0%{transform:translate3d(-3%,-2%,0) scale(1) rotate(0deg)}50%{transform:translate3d(4%,3%,0) scale(1.18) rotate(3deg)}100%{transform:translate3d(-2%,5%,0) scale(1.08) rotate(-3deg)}}
@@ -78,7 +78,7 @@ With Tailwind in Nuxt, use the layered import from section 3: set `css: false` i
78
78
  Tailwind 4 puts preflight in `@layer base` and utilities in `@layer utilities`. CSS outside any layer beats every layer, regardless of specificity. So an unlayered `@ziyarago/ui/style.css` next to Tailwind has two problems (both reproduced):
79
79
 
80
80
  1. Utilities on our components lose: `<UiButton class="px-10 rounded-none bg-red-500">` keeps the padding, radius and background of `.zg-btn`.
81
- 2. Order matters: `@import "tailwindcss"; @import "@ziyarago/ui/style.css";` silently dropped the Google Fonts `@import url(...)` from our file (Manrope did not load).
81
+ 2. Order matters: `@import "tailwindcss"; @import "@ziyarago/ui/style.css";` silently dropped the old Google Fonts `@import url(...)`. Since 0.7 the fonts are bundled (`@font-face` with local files), so there is no `@import` to lose.
82
82
 
83
83
  Recipe (single CSS entry, Vite or Nuxt):
84
84
 
@@ -87,7 +87,7 @@ Recipe (single CSS entry, Vite or Nuxt):
87
87
  @import "@ziyarago/ui/style.css" layer(components);
88
88
  ```
89
89
 
90
- Result: preflight < our styles < utilities. `class="w-48"`, `px-10`, `rounded-none`, `bg-red-500` on `UiButton`/`UiCard` all apply; the Google Fonts import is kept. Do not also import the stylesheet from `main.ts`.
90
+ Result: preflight < our styles < utilities. `class="w-48"`, `px-10`, `rounded-none`, `bg-red-500` on `UiButton`/`UiCard` all apply; the fonts load from your own origin. Do not also import the stylesheet from `main.ts`.
91
91
 
92
92
  Put `class="zg-root"` on your app root (`<div id="app" class="zg-root">` or body). It sets font, text color, background and the reduced-motion rule.
93
93
 
@@ -105,7 +105,7 @@ Compared computed styles of every element of every documented example (182 compo
105
105
 
106
106
  After the fix the remaining differences between the two setups are `appearance: button`, `img { max-width: 100% }`, `svg { display: block }` and running animation frames; none is visible.
107
107
 
108
- Fonts: `style.css` pulls Manrope and JetBrains Mono from Google Fonts with an `@import url(...)`, which must be the first statement of the final CSS. If your build moves it (or you self-host), load the fonts yourself (for example `@fontsource-variable/manrope`) and keep `--zg-font` in sync. Thmanyah (Arabic) ships inside the package (`assets/fonts`).
108
+ Fonts: Manrope, JetBrains Mono (latin and cyrillic) and Thmanyah (Arabic) ship inside the package (`assets/fonts`) and `style.css` loads them from there. No request goes to Google, so a strict CSP (`style-src 'self'`, `font-src 'self'`) works. Your bundler must resolve the `url(../assets/fonts/...)` references; Vite and Nuxt do it by default.
109
109
 
110
110
  ## 4. vue-i18n, pinia, vue-router
111
111
 
package/docs/MIGRATION.md CHANGED
@@ -45,6 +45,8 @@ npm i @ziyarago/ui
45
45
 
46
46
  You can later remove `@floating-ui/vue`, `class-variance-authority` and (when no page imports it) `tailwind-merge` from the UI layer; the pages themselves still use `clsx`, `lucide-vue-next` and Tailwind.
47
47
 
48
+ **CSP warning:** icons are fetched at runtime. Under `connect-src 'self'` they disappear unless you self-host them as shown below (`setIconBase`). Do this before shipping to a server with a strict CSP.
49
+
48
50
  Keep `lucide-vue-next`. The new kit takes icons by NAME (`icon="trash-2"`) and fetches Lucide SVGs from `unpkg.com/lucide-static@0.460.0/icons/` at runtime; the compat layer converts your Lucide components to names for you (`iconName()`), including the 9 icons that lucide-vue-next 1.x renamed (`funnel` to `filter`, `circle-question-mark` to `circle-help`, `text-align-start` to `align-left`, ...). To avoid the CDN (offline, CSP, flash on first paint) self-host once:
49
51
 
50
52
  ```bash
@@ -58,7 +60,7 @@ setIconBase("/lucide/");
58
60
 
59
61
  ## 3. CSS
60
62
 
61
- In `src/styles/main.css`, directly after Tailwind, import the kit INTO the `components` layer (so Tailwind utilities such as `w-44`, `p-0`, `h-20`, `bg-red-500` on kit components still win, and the Google Fonts `@import` survives):
63
+ In `src/styles/main.css`, directly after Tailwind, import the kit INTO the `components` layer (so Tailwind utilities such as `w-44`, `p-0`, `h-20`, `bg-red-500` on kit components still win, and the bundled `@font-face` rules stay valid inside the layer; since 0.7 there is no remote `@import` in `style.css`):
62
64
 
63
65
  ```css
64
66
  @import "tailwindcss";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ziyarago/ui",
3
- "version": "0.9.0",
4
- "description": "ZiyaraGo UI — glass design system for Umrah and Hajj products. OKLCH tokens, two palettes, dark and light, RTL, Thmanyah. One framework-neutral core with Vue 3, Nuxt and React adapters, 203 components and 36 page templates.",
3
+ "version": "0.11.0",
4
+ "description": "ZiyaraGo UI — glass design system for Umrah and Hajj products. OKLCH tokens, two palettes, dark and light, RTL, Thmanyah. One framework-neutral core with Vue 3, Nuxt and React adapters, 208 components and 40 page templates.",
5
5
  "keywords": [
6
6
  "ui",
7
7
  "design-system",
@@ -438,6 +438,8 @@ export const UiMultilangInput = defineComponent({
438
438
 
439
439
  // ───────────────────────── theme ─────────────────────────
440
440
  const LEGACY_KEY = 'glass-ui-theme', themeRef = ref('light');
441
+ // Read the storage BEFORE the kit's theme controller initialises: it writes its defaults (dark) to "zg-ui" on first use, which would hide the old choice.
442
+ const boot = (() => { try { return typeof localStorage === 'undefined' ? {} : { legacy: localStorage.getItem(LEGACY_KEY), hasNew: !!JSON.parse(localStorage.getItem('zg-ui') || '{}').theme }; } catch { return {}; } })();
441
443
  let themeInit = false;
442
444
  /** Drop-in for the old useTheme(): { theme, setTheme, toggleTheme }. Also toggles the .dark class (for Tailwind dark:) next to the new data-theme attribute. Reads the old localStorage key once. */
443
445
  export function useTheme() {
@@ -445,8 +447,7 @@ export function useTheme() {
445
447
  if (!themeInit && typeof window !== 'undefined') {
446
448
  themeInit = true;
447
449
  const sync = tm => { themeRef.value = tm; document.documentElement.classList.toggle('dark', tm === 'dark'); try { localStorage.setItem(LEGACY_KEY, tm); } catch { /* storage blocked */ } };
448
- let legacy = null, hasNew = false;
449
- try { legacy = localStorage.getItem(LEGACY_KEY); hasNew = !!JSON.parse(localStorage.getItem('zg-ui') || '{}').theme; } catch { /* storage blocked */ }
450
+ const legacy = boot.legacy, hasNew = boot.hasNew;
450
451
  if (!hasNew) ui.setTheme(legacy === 'light' || legacy === 'dark' ? legacy : (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'));
451
452
  sync(ui.theme);
452
453
  ui.subscribe(s => sync(s.theme));
@@ -11,7 +11,7 @@ export function ziyaragoCompat(options = {}) {
11
11
  enforce: 'pre',
12
12
  resolveId(source, importer) {
13
13
  const norm = source.replace(/\\/g, '/');
14
- if (!importer || norm.startsWith('\0') || norm.startsWith(from)) return null;
14
+ if (!importer || importer.includes('node_modules') || norm.startsWith('\0') || norm.startsWith(from) || !norm.includes('components/ui')) return null;
15
15
  if (importer.replace(/\\/g, '/').includes('/components/ui/')) return null;
16
16
  if (exclude.some(r => r.test(norm))) return null;
17
17
  const m = ui.exec(norm);
package/src/core/bools.js CHANGED
@@ -1,4 +1,4 @@
1
1
  // Names of props that are boolean in EVERY component that documents them. The Vue adapter turns the bare-attribute form (<UiButton block>, which Vue delivers as '') into true for these.
2
2
  // Names that are boolean somewhere and text elsewhere (modelValue, image, brand) are not listed: '' is a legitimate value there.
3
3
  // scripts/check-meta.mjs keeps this list in sync with src/meta.
4
- export const BOOL_PROPS = new Set(['addable', 'allowEmpty', 'autoResize', 'autofocus', 'banner', 'block', 'bold', 'checked', 'clearable', 'closable', 'closeOnBackdrop', 'closeOnEsc', 'collapsed', 'collapsible', 'disabled', 'divider', 'dot', 'external', 'featured', 'frame', 'ghost', 'glass', 'hideArrows', 'hideIcon', 'highlightLast', 'iconOnly', 'indeterminate', 'interactive', 'labelLeft', 'last', 'loading', 'multi', 'multiple', 'open', 'pill', 'plain', 'readonly', 'required', 'resize', 'ring', 'sanitize', 'selectGroups', 'showCount', 'showForgot', 'showValue', 'spellcheck', 'ticks', 'truncate', 'vertical', 'vip', 'wrap']);
4
+ export const BOOL_PROPS = new Set(['addable', 'allowEmpty', 'autoResize', 'autofocus', 'banner', 'block', 'bold', 'checked', 'clearable', 'closable', 'closeOnBackdrop', 'closeOnEsc', 'collapsed', 'collapsible', 'disabled', 'divider', 'dot', 'external', 'featured', 'frame', 'ghost', 'glass', 'hideArrows', 'hideIcon', 'highlightLast', 'iconOnly', 'indeterminate', 'interactive', 'labelLeft', 'last', 'loading', 'multi', 'multiple', 'open', 'pill', 'plain', 'readonly', 'required', 'resize', 'ring', 'sanitize', 'selectGroups', 'showCount', 'showForgot', 'showRemember', 'showValue', 'spellcheck', 'ticks', 'truncate', 'vertical', 'vip', 'wrap']);
@@ -1,5 +1,5 @@
1
1
  // @ziyarago/ui · components, part 2 — Forms, Surfaces, Actions, Data, Navigation, Layout, CRM, Hotels.
2
- import { icon, cx, fmt, dzp, dzf, dzb, ddRef, pick } from './components.js';
2
+ import { icon, cx, fmt, dzp, dzf, dzb, ddRef, pick, INPUT_ATTRS } from './components.js';
3
3
  import { trap } from './dialog.js';
4
4
  import { t, getUiLocale, intlLocale } from '../i18n/index.js';
5
5
  const dloc = p => p.locale || (getUiLocale() === 'en' ? 'en-GB' : intlLocale());
@@ -84,7 +84,7 @@ export const more = {
84
84
  p.showCount ? h('span', { class: 'zg-hint zg-ta__count' }, String((p.modelValue ?? '').length) + (p.maxlength ? ' / ' + p.maxlength : '')) : null]) },
85
85
  NumberInput: { state: { draft: null }, render: ({ C, h, props: p, state, set: put }) => { const v = p.modelValue ?? null, st = p.step || 1, lo = p.min ?? -Infinity, hi = p.max ?? Infinity, parse = s => { const t = String(s).trim().replace(',', '.'); if (t === '') return null; const n = Number(t); return Number.isFinite(n) ? n : NaN; }, emit = n => p.onChange && p.onChange(n), bump = d => { put({ draft: null }); emit(clamp(Math.round(((v ?? 0) + d) * 1e10) / 1e10, lo, hi)); };
86
86
  return lab(h, p, h('span', { class: 'zg-input zg-num-in' }, h(C.IconButton, { icon: 'minus', label: t('NumberInput', 'decrease'), size: 'sm', onClick: () => bump(-st) }), h('input', { value: state.draft ?? (v ?? ''), inputMode: 'decimal', placeholder: p.placeholder, style: { textAlign: 'center' }, onInput: e => { const raw = e.target.value, n = parse(raw); put({ draft: raw }); if (n === null || !Number.isNaN(n)) emit(n); }, onBlur: e => { const n = parse(e.target.value); put({ draft: null }); if (n === null) { if (v !== null) emit(null); } else if (!Number.isNaN(n)) { const c = clamp(n, lo, hi); if (c !== v) emit(c); } } }), h(C.IconButton, { icon: 'plus', label: t('NumberInput', 'increase'), size: 'sm', onClick: () => bump(st) }))); } },
87
- PasswordInput: { state: { show: false }, render: ({ C, h, props: p, state, set }) => lab(h, p, field(h, p, icon(h, 'lock-keyhole', 18), h('input', { type: state.show ? 'text' : 'password', value: p.modelValue ?? '', placeholder: p.placeholder, autoComplete: p.autocomplete || 'current-password', onInput: e => p.onChange && p.onChange(e.target.value) }), h('button', { type: 'button', class: 'zg-ibtn zg-ibtn--sm zg-ibtn--ghost', 'aria-label': state.show ? t('PasswordInput', 'hide') : t('PasswordInput', 'show'), onClick: () => set({ show: !state.show }) }, icon(h, state.show ? 'eye-off' : 'eye', 16)))) },
87
+ PasswordInput: { state: { show: false }, render: ({ C, h, props: p, state, set }) => lab(h, p, field(h, p, icon(h, 'lock-keyhole', 18), h('input', { ...pick(p, INPUT_ATTRS.filter(k => k !== 'autocomplete')), type: state.show ? 'text' : 'password', value: p.modelValue ?? '', placeholder: p.placeholder, autoComplete: p.autocomplete || 'current-password', onInput: e => p.onChange && p.onChange(e.target.value) }), h('button', { type: 'button', class: 'zg-ibtn zg-ibtn--sm zg-ibtn--ghost', 'aria-label': state.show ? t('PasswordInput', 'hide') : t('PasswordInput', 'show'), onClick: () => set({ show: !state.show }) }, icon(h, state.show ? 'eye-off' : 'eye', 16)))) },
88
88
  SearchInput: { render: ({ h, props: p }) => h('span', { class: 'zg-input', style: { '--h': 'var(--zg-h-md)' } }, icon(h, 'search', 16), h('input', { type: 'search', value: p.modelValue ?? '', placeholder: p.placeholder || t('SearchInput', 'placeholder'), onInput: e => p.onChange && p.onChange(e.target.value), onKeyDown: e => e.key === 'Enter' && p.onSearch && p.onSearch(p.modelValue) }), p.modelValue ? h('button', { type: 'button', class: 'zg-ibtn zg-ibtn--sm zg-ibtn--ghost', 'aria-label': t('SearchInput', 'clear'), onClick: () => p.onChange && p.onChange('') }, icon(h, 'x', 14)) : null) },
89
89
  RadioGroup: { render: ({ h, props: p }) => h('div', { class: 'zg-field', role: 'radiogroup', 'aria-disabled': p.disabled ? 'true' : undefined, style: { gap: '10px' } }, p.label ? h('span', { class: 'zg-label' }, p.label) : null,
90
90
  h('div', { class: cx('zg-rg', p.orientation === 'horizontal' && 'zg-rg--row') }, ...(p.options || []).map(o => h('button', { key: o.value, type: 'button', role: 'radio', class: 'zg-check', 'aria-checked': String(o.value === p.modelValue), disabled: p.disabled || o.disabled, 'aria-disabled': p.disabled || o.disabled ? 'true' : undefined, onClick: () => !(p.disabled || o.disabled) && p.onChange && p.onChange(o.value) }, h('span', { class: 'zg-check__box zg-radio' }, o.value === p.modelValue ? h('i') : null), h('span', null, o.label, o.hint ? h('span', { class: 'zg-hint', style: { display: 'block' } }, o.hint) : null)))),
@@ -123,7 +123,7 @@ export const more = {
123
123
  FormField: { render: ({ h, props: p, children }) => h('div', { class: 'zg-field' }, p.label ? h('span', { class: 'zg-label' }, p.label, p.required ? h('span', { style: { color: 'var(--zg-red)' } }, ' *') : null) : null, ...children, p.error ? h('span', { class: 'zg-hint zg-hint--error' }, p.error) : p.hint ? h('span', { class: 'zg-hint' }, p.hint) : null) },
124
124
  LoginForm: { state: { e: '', pw: '', rm: true }, render: ({ C, h, props: p, state, set }) => { const L = { email: t('LoginForm', 'email'), password: t('LoginForm', 'password'), remember: t('LoginForm', 'remember'), forgot: t('LoginForm', 'forgot'), submit: t('LoginForm', 'submit'), ...(p.labels || {}) }; return h('form', { class: 'zg-field', style: { gap: '14px' }, onSubmit: ev => { ev.preventDefault(); p.onSubmit && p.onSubmit({ email: state.e, password: state.pw, remember: state.rm }); } },
125
125
  p.error ? h(C.Alert, { tone: 'danger' }, p.error) : null, h(C.TextField, { label: L.email, icon: 'user-round', modelValue: state.e, onChange: v => set({ e: v }) }), h(C.PasswordInput, { label: L.password, modelValue: state.pw, onChange: v => set({ pw: v }) }),
126
- h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } }, h(C.Checkbox, { label: L.remember, modelValue: state.rm, onChange: v => set({ rm: v }) }), p.showForgot !== false ? h('a', { href: '#', onClick: e => { e.preventDefault(); p.onForgot && p.onForgot(); }, style: { color: 'var(--zg-brand-text)', fontWeight: 600, fontSize: '14px' } }, L.forgot) : null),
126
+ h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } }, p.showRemember !== false ? h(C.Checkbox, { label: L.remember, modelValue: state.rm, onChange: v => set({ rm: v }) }) : h('span', null), p.showForgot !== false ? h('a', { href: '#', onClick: e => { e.preventDefault(); p.onForgot && p.onForgot(); }, style: { color: 'var(--zg-brand-text)', fontWeight: 600, fontSize: '14px' } }, L.forgot) : null),
127
127
  h('button', { type: 'submit', class: 'zg-btn zg-btn--primary zg-btn--lg zg-btn--block', disabled: p.loading }, p.loading ? h('span', { class: 'zg-spin' }) : null, p.submitLabel || L.submit)); } },
128
128
  StepForm: { state: { i: 0 }, render: ({ C, h, props: p, state, set }) => { const st = p.steps || [], i = clamp(state.i, 0, st.length - 1);
129
129
  return h('div', { class: 'zg-field', style: { gap: '18px' } }, h(C.Stepper, { steps: st.map(s => s.label), current: i }), h('div', null, st[i] && st[i].content), h('div', { style: { display: 'flex', gap: '8px', justifyContent: 'flex-end' } }, h(C.Button, { size: p.size, variant: 'ghost', disabled: i === 0, onClick: () => set({ i: i - 1 }) }, t('StepForm', 'back')), h(C.Button, { size: p.size, variant: 'primary', onClick: () => i === st.length - 1 ? p.onFinish && p.onFinish() : set({ i: i + 1 }) }, i === st.length - 1 ? (p.finishLabel || t('StepForm', 'finish')) : t('StepForm', 'next')))); } },
@@ -7,6 +7,19 @@ const sp = v => (typeof v === 'number' ? v + 'px' : v);
7
7
  const stack = (dir) => ({ render: ({ h, props: p, children }) => h(p.as || 'div', { class: 'zg-stack', style: { flexDirection: dir, gap: sp(p.gap ?? 12), alignItems: p.align, justifyContent: p.justify, flexWrap: p.wrap ? 'wrap' : undefined, ...(p.style || {}) } }, ...children) });
8
8
 
9
9
  export const more7 = {
10
+ QiblaCompass: { render: ({ h, props: p }) => {
11
+ const px = { xs: 120, sm: 160, md: 200, lg: 250, xl: 300 }[p.size] || (Number(p.size) > 0 ? Number(p.size) : 200), norm = v => ((Number(v) || 0) % 360 + 360) % 360, b = norm(p.bearing), hd = norm(p.heading), deg = Math.round(b), ticks = [];
12
+ for (let i = 0; i < 72; i++) { const major = i % 9 === 0, a = i * 5 * Math.PI / 180, r1 = 92, r2 = major ? 80 : i % 3 ? 87 : 84; ticks.push(h('line', { key: i, class: major ? 'zg-qb__tk zg-qb__tk--m' : 'zg-qb__tk', x1: +(100 + Math.sin(a) * r1).toFixed(2), y1: +(100 - Math.cos(a) * r1).toFixed(2), x2: +(100 + Math.sin(a) * r2).toFixed(2), y2: +(100 - Math.cos(a) * r2).toFixed(2) })); }
13
+ return h('div', { class: 'zg-qb zg-dm', role: 'img', 'aria-label': t('QiblaCompass', 'label', { deg }), style: { width: px + 'px' } },
14
+ h('svg', { class: 'zg-qb__svg', viewBox: '0 0 200 200', 'aria-hidden': 'true', focusable: 'false' },
15
+ h('circle', { class: 'zg-qb__ring', cx: 100, cy: 100, r: 98 }),
16
+ h('g', { class: 'zg-qb__dial', transform: `rotate(${-hd} 100 100)` }, ...ticks, h('path', { class: 'zg-qb__n', d: 'M100 6 L106 20 L94 20 Z' })),
17
+ h('g', { class: 'zg-qb__ptr', transform: `rotate(${b - hd} 100 100)` },
18
+ h('line', { class: 'zg-qb__ln', x1: 100, y1: 100, x2: 100, y2: 46 }),
19
+ h('rect', { class: 'zg-qb__kb', x: 88, y: 24, width: 24, height: 24, rx: 5 }),
20
+ h('rect', { class: 'zg-qb__kg', x: 88, y: 31, width: 24, height: 4 })),
21
+ h('circle', { class: 'zg-qb__hub', cx: 100, cy: 100, r: 5 })),
22
+ h('div', { class: 'zg-qb__txt' }, h('b', { class: 'zg-num' }, deg + '°'), h('span', null, t('QiblaCompass', 'qibla')), p.distance ? h('small', null, p.distance) : null)); } },
10
23
  Heading: { render: ({ h, props: p, children }) => h('h' + clamp(p.level || 2, 1, 6), { class: cx('zg-heading', 'zg-heading--' + (p.size || 'lg'), p.brand && 'zg-heading--brand') }, ...children) },
11
24
  Text: { render: ({ h, props: p, children }) => h(p.as || 'p', { class: cx('zg-text', 'zg-text--' + (p.size || 'md'), p.tone && 'zg-text--' + p.tone, p.bold && 'zg-text--bold', p.truncate && 'zg-text--truncate') }, ...children) },
12
25
  Box: { render: ({ h, props: p, children }) => h(p.as || 'div', { class: cx('zg-box', p.glass && 'zg-glass'), style: { padding: sp(p.p), ...(p.style || {}) } }, ...children) },
package/src/core/pages.js CHANGED
@@ -56,7 +56,11 @@ const T = Object.fromEntries([
56
56
  page('PageStates', ['header', 'empty', 'noresults', 'forbidden', 'offline', 'error', 'loading', 'success'], ['col', 'header', ['auto', 300, 'empty', 'noresults', 'forbidden'], ['auto', 300, 'offline', 'error', 'loading'], 'success']),
57
57
  page('PageLegal', ['header', 'meta', 'toc', 'document', 'contact'], ['col', 'header', 'meta', ['wrap', ['toc'], ['document', 'contact'], 260, 'sticky:toc']]),
58
58
  page('PageLocations', ['header', 'filters', 'list', 'map'], ['col', 'header', 'filters', ['wrap', ['list'], ['map'], 380, 'sticky:map']]),
59
- page('PageBookingFlow', ['header', 'steps', 'content', 'nav', 'summary'], ['col', 'header', 'steps', ['two', ['content', 'nav'], ['summary'], 'sticky:summary']])
59
+ page('PageBookingFlow', ['header', 'steps', 'content', 'nav', 'summary'], ['col', 'header', 'steps', ['two', ['content', 'nav'], ['summary'], 'sticky:summary']]),
60
+ page('PageItinerary', ['header', 'days', 'timeline', 'transport', 'hotel', 'summary'], ['col', 'header', 'days', ['two', ['timeline', 'transport', 'hotel'], ['summary'], 'sticky:summary']]),
61
+ page('PagePrayer', ['header', 'location', 'today', 'month', 'qibla'], ['col', 'header', 'location', ['two', ['today', 'month'], ['qibla'], 'sticky:qibla']]),
62
+ page('PageGroupManager', ['header', 'trip', 'toolbar', 'bulk', 'pilgrims', 'rooms', 'payments'], ['col', 'header', 'trip', ['two', ['toolbar', 'bulk', 'pilgrims', 'rooms'], ['payments'], 'sticky:payments']]),
63
+ page('PageTourPublic', ['header', 'gallery', 'highlights', 'itinerary', 'included', 'reviews', 'booking'], ['col', 'header', 'gallery', ['two', ['highlights', 'itinerary', 'included', 'reviews'], ['booking'], 'sticky:booking']])
60
64
  ]);
61
65
 
62
66
  T.PageAuth = { slots: ['background', 'logo', 'aside', 'form', 'footer'], render: ({ C, h, props: p, slots: s }) => {
@@ -0,0 +1,5 @@
1
+ export default {
2
+ en: { label: 'Qibla direction, {deg} degrees from north', qibla: 'Qibla' },
3
+ ar: { label: 'اتجاه القبلة، {deg} درجة من الشمال', qibla: 'القبلة' },
4
+ ru: { label: 'Направление киблы, {deg} градусов от севера', qibla: 'Кибла' },
5
+ };
@@ -44,6 +44,7 @@ import Pagination from './Pagination.js';
44
44
  import PasswordInput from './PasswordInput.js';
45
45
  import Popconfirm from './Popconfirm.js';
46
46
  import PricingCard from './PricingCard.js';
47
+ import QiblaCompass from './QiblaCompass.js';
47
48
  import RamadanTracker from './RamadanTracker.js';
48
49
  import RangeCalendar from './RangeCalendar.js';
49
50
  import RangeSlider from './RangeSlider.js';
@@ -112,6 +113,7 @@ defineMessages('Pagination', Pagination);
112
113
  defineMessages('PasswordInput', PasswordInput);
113
114
  defineMessages('Popconfirm', Popconfirm);
114
115
  defineMessages('PricingCard', PricingCard);
116
+ defineMessages('QiblaCompass', QiblaCompass);
115
117
  defineMessages('RamadanTracker', RamadanTracker);
116
118
  defineMessages('RangeCalendar', RangeCalendar);
117
119
  defineMessages('RangeSlider', RangeSlider);
@@ -49,6 +49,16 @@ export default {
49
49
  },
50
50
  notes: ['Exposed as role="img" with aria-label "Day N of 30".'],
51
51
  },
52
+ QiblaCompass: {
53
+ description: 'Compass dial with a Kaaba marker pointing to the Qibla and the bearing in degrees below it.',
54
+ props: {
55
+ bearing: { type: 'number', required: true, default: 0, description: 'Qibla bearing in degrees clockwise from true north, 0 to 360; other values wrap.' },
56
+ heading: { type: 'number', default: 0, description: 'Direction the device points to, in degrees. The dial turns against it so the marker follows the real Qibla; 0 keeps north up.' },
57
+ distance: { type: 'string', description: 'Optional small caption under the bearing, such as "4 210 km to the Kaaba".' },
58
+ size: { type: "'xs' | 'sm' | 'md' | 'lg' | 'xl' | number", values: ['xs', 'sm', 'md', 'lg', 'xl'], default: 'md', description: 'Diameter: xs 120, sm 160, md 200, lg 250, xl 300 px; a number is used as px.' },
59
+ },
60
+ notes: ['Exposed as role="img" with a label that states the bearing. Compute the bearing and heading yourself (geolocation and DeviceOrientation); the component only draws them.'],
61
+ },
52
62
  BookingCard: {
53
63
  description: 'Compact booking summary: code, status, guest, hotel, dates and total.',
54
64
  props: {
package/src/meta/forms.js CHANGED
@@ -78,6 +78,9 @@ export default {
78
78
  props: {
79
79
  modelValue: { type: 'string', model: true, default: '', description: 'Current password.' },
80
80
  label, placeholder: placeholder(), hint, error,
81
+ required: { type: 'boolean', default: false, description: 'Adds a red asterisk to the label and the native required attribute.' },
82
+ name: { type: 'string', forwarded: true, description: 'Native name attribute of the input.' },
83
+ autofocus: { type: 'boolean', forwarded: true, description: 'Focuses the input on mount.' },
81
84
  autocomplete: { type: 'string', default: 'current-password', description: 'Native autocomplete token. Use "new-password" on sign-up and password-change forms so managers offer to generate one.' },
82
85
  size: size(),
83
86
  },
@@ -465,6 +468,7 @@ export default {
465
468
  error: { type: 'string', description: 'Failure message shown in a danger alert above the fields.' },
466
469
  loading: { type: 'boolean', default: false, description: 'Disables the submit button and shows a spinner.' },
467
470
  showForgot: { type: 'boolean', default: true, description: 'Show the "Forgot password?" link.' },
471
+ showRemember: { type: 'boolean', default: true, description: 'Show the "Remember device" checkbox. Hide it when your backend has no such option; the submit payload then keeps remember: true.' },
468
472
  submitLabel: { type: 'string', default: 'Sign in', description: 'Submit button text. Wins over labels.submit.' },
469
473
  labels: { type: '{ email?: string; password?: string; remember?: string; forgot?: string; submit?: string }', default: "{ email: 'Email or phone', password: 'Password', remember: 'Remember device', forgot: 'Forgot password?', submit: 'Sign in' }", description: 'Texts of the form; any missing key keeps its English default.' },
470
474
  size: size(),
@@ -9,8 +9,8 @@ export const GROUPS = [
9
9
  { id: 'overlay', title: 'Overlay', blurb: 'Dialogs, sheets and popovers.', names: ['Modal', 'Drawer', 'ConfirmDialog', 'Popconfirm', 'StepDialog', 'FullscreenDialog', 'BottomSheet', 'Actionsheet', 'Popover', 'HoverCard', 'Tooltip', 'Dropdown', 'Menu', 'ContextMenu', 'CommandBar', 'CommandPalette', 'ImageViewer', 'Tour', 'TourPopover', 'Screensaver'] },
10
10
  { id: 'navigation', title: 'Navigation', blurb: 'Menus, tabs and paging.', names: ['TopNav', 'SideNav', 'SubNav', 'Tabs', 'PillTabs', 'Breadcrumbs', 'Pagination', 'Accordion'] },
11
11
  { id: 'media', title: 'Media and icons', blurb: 'Avatars, images, maps and icons.', names: ['Avatar', 'Icon', 'LazyImage', 'Gallery', 'Carousel', 'Map', 'ProfileHeader', 'ChatAi'] },
12
- { id: 'domain', title: 'Domain', blurb: 'Umrah, Hajj, hotel and CRM blocks.', names: ['HotelCard', 'TourCard', 'PrayerTimes', 'RamadanTracker', 'BookingCard', 'GuestCard', 'RoomGrid', 'RoomCalendar', 'LeadCard', 'DealCard', 'PipelineBoard', 'ContactPanel', 'KanbanColumn', 'KanbanCard', 'CallRow', 'CallCard', 'IncomingCall', 'AuthSplit', 'AuthShowcase', 'SocialAuth'] },
13
- { id: 'pages', title: 'Page templates', blurb: 'Ready layouts with named slots.', names: ['PageDashboard', 'PagePatterns', 'PageCrm', 'PageBookings', 'PageCatalog', 'PageDetail', 'PageSettings', 'PageUserList', 'PageProfile', 'PageForm', 'PageOrder', 'PageAccount', 'PagePricing', 'PageFaq', 'PageAnalytics', 'PageCalendar', 'PageKanban', 'PageBilling', 'PageTeam', 'PageIntegrations', 'PageSearch', 'PageArticle', 'PageChangelog', 'PageHelp', 'PageStates', 'PageLegal', 'PageLocations', 'PageBookingFlow', 'PageNotifications', 'PageInbox', 'PageCheckout', 'PageOnboarding', 'PageLanding', 'PageAuth', 'PageStatus'] },
12
+ { id: 'domain', title: 'Domain', blurb: 'Umrah, Hajj, hotel and CRM blocks.', names: ['HotelCard', 'TourCard', 'PrayerTimes', 'RamadanTracker', 'QiblaCompass', 'BookingCard', 'GuestCard', 'RoomGrid', 'RoomCalendar', 'LeadCard', 'DealCard', 'PipelineBoard', 'ContactPanel', 'KanbanColumn', 'KanbanCard', 'CallRow', 'CallCard', 'IncomingCall', 'AuthSplit', 'AuthShowcase', 'SocialAuth'] },
13
+ { id: 'pages', title: 'Page templates', blurb: 'Ready layouts with named slots.', names: ['PageDashboard', 'PagePatterns', 'PageCrm', 'PageBookings', 'PageCatalog', 'PageDetail', 'PageSettings', 'PageUserList', 'PageProfile', 'PageForm', 'PageOrder', 'PageAccount', 'PagePricing', 'PageFaq', 'PageAnalytics', 'PageCalendar', 'PageKanban', 'PageBilling', 'PageTeam', 'PageIntegrations', 'PageSearch', 'PageArticle', 'PageChangelog', 'PageHelp', 'PageStates', 'PageLegal', 'PageLocations', 'PageBookingFlow', 'PageItinerary', 'PagePrayer', 'PageGroupManager', 'PageTourPublic', 'PageNotifications', 'PageInbox', 'PageCheckout', 'PageOnboarding', 'PageLanding', 'PageAuth', 'PageStatus'] },
14
14
  ];
15
15
 
16
16
  export const groupOf = name => GROUPS.find(g => g.names.includes(name));
package/src/meta/pages.js CHANGED
@@ -425,6 +425,59 @@ export default {
425
425
  },
426
426
  notes: [hdrNote, "Render only the active step in content. The summary stays visible on the right and goes below the step under 860 px."],
427
427
  },
428
+ PageItinerary: {
429
+ description: "Itinerary template: day tabs, a timeline of stops for the chosen day, transport and hotel blocks and a sticky trip summary card.",
430
+ props: header,
431
+ slots: {
432
+ ...headSlots,
433
+ days: "Day switcher; Tabs or PillTabs.",
434
+ timeline: "Stops of the active day with times and places; Card, Timeline, StatRow.",
435
+ transport: "Flights, buses and transfers of the day; Card, StatRow.",
436
+ hotel: "Where the group stays that night; Card, Badge, IconTile.",
437
+ summary: "Trip summary; Card, Descriptions, Progress.",
438
+ },
439
+ notes: [hdrNote, "Render only the active day in timeline, transport and hotel. The summary stays visible on the right and goes below the day under 860 px."],
440
+ },
441
+ PagePrayer: {
442
+ description: "Prayer times template: a location picker, today's times with the next prayer highlighted, a month table and a sticky Qibla compass.",
443
+ props: header,
444
+ slots: {
445
+ ...headSlots,
446
+ location: "City and method pickers; Select, PillSelect, SegmentedControl.",
447
+ today: "Today's prayers and the countdown to the next one; Card, PrayerTimes.",
448
+ month: "Prayer times for the whole month; Card, DataTable.",
449
+ qibla: "Qibla direction; Card, QiblaCompass.",
450
+ },
451
+ notes: [hdrNote, "The Qibla card stays visible on the right and goes below the times under 860 px."],
452
+ },
453
+ PageGroupManager: {
454
+ description: "Group manager template: trip summary, pilgrims table with document and visa status, bulk actions, rooming and a sticky payments summary.",
455
+ props: header,
456
+ slots: {
457
+ ...headSlots,
458
+ trip: "Trip header with dates, route and counters; Card, StatStrip, Progress.",
459
+ toolbar: "Search and status filters; SearchInput, FilterChips.",
460
+ bulk: "Actions for the selected pilgrims; Button, Badge.",
461
+ pilgrims: "Pilgrims table with documents and visa; Card, DataTable, Badge.",
462
+ rooms: "Rooming assignment; Card, RoomGrid.",
463
+ payments: "Payments summary and manager contact; Card, Progress, ContactPanel.",
464
+ },
465
+ notes: [hdrNote, "The payments card stays visible on the right and goes below the table under 860 px."],
466
+ },
467
+ PageTourPublic: {
468
+ description: "Public tour page: header, gradient gallery area, highlights, itinerary accordion, included and excluded lists, reviews and a sticky price and booking card. Has no shell and follows the theme.",
469
+ props: header,
470
+ slots: {
471
+ ...headSlots,
472
+ gallery: "Hero area made of gradients or photos; Gallery, Card.",
473
+ highlights: "Key facts and highlights; StatStrip, IconTile.",
474
+ itinerary: "Day by day program; Accordion.",
475
+ included: "What the price includes and excludes; Card, Descriptions.",
476
+ reviews: "Pilgrim reviews; Card, Rating, Avatar.",
477
+ booking: "Price, dates, seats and the booking button; Card, Button.",
478
+ },
479
+ notes: ["Render the page on its own, without AppShell: it supplies its own width and padding in your layout.", "The booking card stays visible on the right and goes below the content under 860 px."],
480
+ },
428
481
  PageAuth: {
429
482
  description: 'Sign-in page template without the app shell, as a centered card or a split layout.',
430
483
  props: {