@ziyarago/ui 0.3.0 → 0.5.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 (94) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +23 -2
  3. package/dist/llms-full.txt +32 -3
  4. package/dist/llms.txt +3 -2
  5. package/dist/manifest.json +123 -6
  6. package/dist/style.css +41 -0
  7. package/package.json +2 -2
  8. package/src/core/components.js +15 -14
  9. package/src/core/components2.js +37 -36
  10. package/src/core/components3.js +29 -7
  11. package/src/core/components4.js +23 -22
  12. package/src/core/components6.js +8 -7
  13. package/src/core/components7.js +10 -9
  14. package/src/core/index.js +1 -1
  15. package/src/core/pages.js +1 -0
  16. package/src/i18n/components/ActionCard.js +5 -0
  17. package/src/i18n/components/Actionsheet.js +5 -0
  18. package/src/i18n/components/Alert.js +5 -0
  19. package/src/i18n/components/Breadcrumbs.js +5 -0
  20. package/src/i18n/components/Calendar.js +5 -0
  21. package/src/i18n/components/CallCard.js +5 -0
  22. package/src/i18n/components/CallRow.js +5 -0
  23. package/src/i18n/components/Carousel.js +5 -0
  24. package/src/i18n/components/ChartCard.js +5 -0
  25. package/src/i18n/components/ChatAi.js +5 -0
  26. package/src/i18n/components/Chip.js +5 -0
  27. package/src/i18n/components/ChipInput.js +5 -0
  28. package/src/i18n/components/CodeInput.js +5 -0
  29. package/src/i18n/components/Combobox.js +5 -0
  30. package/src/i18n/components/CommandPalette.js +5 -0
  31. package/src/i18n/components/ConfirmDialog.js +5 -0
  32. package/src/i18n/components/ContactPanel.js +5 -0
  33. package/src/i18n/components/CopyButton.js +2 -0
  34. package/src/i18n/components/DataGroup.js +5 -0
  35. package/src/i18n/components/DataState.js +5 -0
  36. package/src/i18n/components/DataTable.js +5 -0
  37. package/src/i18n/components/DatePicker.js +5 -0
  38. package/src/i18n/components/DealCard.js +5 -0
  39. package/src/i18n/components/Drawer.js +5 -0
  40. package/src/i18n/components/ErrorPage.js +5 -0
  41. package/src/i18n/components/FileUpload.js +5 -0
  42. package/src/i18n/components/FullscreenDialog.js +5 -0
  43. package/src/i18n/components/Gallery.js +5 -0
  44. package/src/i18n/components/GuestCard.js +5 -0
  45. package/src/i18n/components/ImageUpload.js +5 -0
  46. package/src/i18n/components/ImageViewer.js +5 -0
  47. package/src/i18n/components/IncomingCall.js +5 -0
  48. package/src/i18n/components/InlineEdit.js +5 -0
  49. package/src/i18n/components/IntegrationCard.js +5 -0
  50. package/src/i18n/components/LoginForm.js +5 -0
  51. package/src/i18n/components/Map.js +5 -0
  52. package/src/i18n/components/Modal.js +5 -0
  53. package/src/i18n/components/MultiSelect.js +5 -0
  54. package/src/i18n/components/NotificationBell.js +5 -0
  55. package/src/i18n/components/NumberInput.js +5 -0
  56. package/src/i18n/components/Pagination.js +5 -0
  57. package/src/i18n/components/PasswordInput.js +5 -0
  58. package/src/i18n/components/Popconfirm.js +5 -0
  59. package/src/i18n/components/PricingCard.js +5 -0
  60. package/src/i18n/components/RamadanTracker.js +5 -0
  61. package/src/i18n/components/RangeCalendar.js +5 -0
  62. package/src/i18n/components/RangeSlider.js +5 -0
  63. package/src/i18n/components/Rating.js +5 -0
  64. package/src/i18n/components/RichEditor.js +5 -0
  65. package/src/i18n/components/Screensaver.js +5 -0
  66. package/src/i18n/components/SearchInput.js +5 -0
  67. package/src/i18n/components/SideNav.js +5 -0
  68. package/src/i18n/components/Snackbar.js +5 -0
  69. package/src/i18n/components/Sortable.js +5 -0
  70. package/src/i18n/components/Spinner.js +5 -0
  71. package/src/i18n/components/StatusPage.js +5 -0
  72. package/src/i18n/components/StepDialog.js +5 -0
  73. package/src/i18n/components/StepForm.js +5 -0
  74. package/src/i18n/components/TabTiles.js +5 -0
  75. package/src/i18n/components/Tag.js +5 -0
  76. package/src/i18n/components/TopNav.js +5 -0
  77. package/src/i18n/components/Tour.js +5 -0
  78. package/src/i18n/components/TourCard.js +5 -0
  79. package/src/i18n/components/TourPopover.js +5 -0
  80. package/src/i18n/components/TreeSelect.js +5 -0
  81. package/src/i18n/components/Validation.js +5 -0
  82. package/src/i18n/components/index.js +130 -0
  83. package/src/i18n/index.js +8 -6
  84. package/src/meta/domain.js +20 -0
  85. package/src/meta/groups.js +1 -1
  86. package/src/meta/pages.js +9 -1
  87. package/src/meta/templates.json +1 -1
  88. package/src/react/index.js +1 -0
  89. package/src/styles/components8.css +41 -0
  90. package/src/vue/index.js +1 -1
  91. package/types/i18n.d.ts +4 -2
  92. package/types/react.d.ts +80 -2
  93. package/types/vue-global.d.ts +1 -0
  94. package/types/vue.d.ts +83 -2
package/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ ## [0.5.0](https://github.com/ziyarago/ui/compare/v0.4.0...v0.5.0) (2026-10-21)
2
+
3
+ ### Features
4
+
5
+ * AuthShowcase login layouts (photo, light, tint) and PageAuth variants ([4ea5503](https://github.com/ziyarago/ui/commit/4ea5503e223f2c651292372be91987538f0517c8))
6
+
7
+ ## [0.4.0](https://github.com/ziyarago/ui/compare/v0.3.0...v0.4.0) (2026-10-21)
8
+
9
+ ### Features
10
+
11
+ * per-component translations (en, ar, ru free; 13 more languages in Pro pack), screenshots in README ([5fc8d9f](https://github.com/ziyarago/ui/commit/5fc8d9ffd85be7cacf42c5413561159d287d0316))
12
+
1
13
  ## [0.3.0](https://github.com/ziyarago/ui/compare/v0.2.0...v0.3.0) (2026-10-21)
2
14
 
3
15
  ### Features
package/README.md CHANGED
@@ -1,9 +1,30 @@
1
1
  # @ziyarago/ui
2
2
 
3
- Glass design system for ZiyaraGo products (Umrah / Hajj). Vue 3 **and** React adapters over one framework-neutral core. Reference: `ziyarago/glass-ui-kit`.
3
+ [![npm](https://img.shields.io/npm/v/@ziyarago/ui?color=c89b5e)](https://www.npmjs.com/package/@ziyarago/ui) [![license](https://img.shields.io/npm/l/@ziyarago/ui?color=555)](LICENSE) [![docs](https://img.shields.io/badge/docs-ziyara.vip-c89b5e)](https://ziyara.vip)
4
+
5
+ <p align="center"><a href="https://ziyara.vip"><img src="https://raw.githubusercontent.com/ziyarago/ui/main/docs/images/hero-dark.png" alt="ziyarago/ui: glass design system for Vue, Nuxt and React" width="100%"></a></p>
6
+
7
+ Glass design system for Vue 3, Nuxt and React: 183 components, 17 page templates, OKLCH tokens, dark and light, RTL and Arabic, 16 interface languages. One framework-neutral core with thin adapters. Built for Umrah and Hajj products, usable anywhere.
8
+
9
+ <table>
10
+ <tr>
11
+ <td width="50%"><img src="https://raw.githubusercontent.com/ziyarago/ui/main/docs/images/hero-light.png" alt="Light theme"><br><sub>Light and dark themes</sub></td>
12
+ <td width="50%"><img src="https://raw.githubusercontent.com/ziyarago/ui/main/docs/images/hero-rtl.png" alt="Arabic RTL"><br><sub>Arabic, RTL mirrored</sub></td>
13
+ </tr>
14
+ <tr>
15
+ <td><img src="https://raw.githubusercontent.com/ziyarago/ui/main/docs/images/components.png" alt="Component catalog"><br><sub>183 components with props and live examples</sub></td>
16
+ <td><img src="https://raw.githubusercontent.com/ziyarago/ui/main/docs/images/templates.png" alt="Page templates"><br><sub>17 page templates</sub></td>
17
+ </tr>
18
+ <tr>
19
+ <td><img src="https://raw.githubusercontent.com/ziyarago/ui/main/docs/images/button-docs.png" alt="Props docs"><br><sub>Every prop and variant documented</sub></td>
20
+ <td><img src="https://raw.githubusercontent.com/ziyarago/ui/main/docs/images/backgrounds.png" alt="Animated backgrounds"><br><sub>Animated backgrounds and gradient cards</sub></td>
21
+ </tr>
22
+ </table>
23
+
24
+ Files:
4
25
 
5
26
  ```
6
- src/core/ components.js … components6.js, pages.js — 160+ components and 16 page templates as { state, slots, render(ctx) }; no framework imports
27
+ src/core/ components*.js, pages.js: 183 components and 17 page templates as { state, slots, render(ctx) }; no framework imports
7
28
  src/vue/ Vue 3 adapter (UiButton, UiAppShell … + install(app))
8
29
  src/react/ React adapter (same names)
9
30
  src/theme.js useUiTheme(): theme · palette · dir → <html> attributes, persisted
@@ -1,8 +1,8 @@
1
1
  # @ziyarago/ui
2
2
 
3
- > 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, 160+ components and 16 page templates.
3
+ > 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, 183 components and 17 page templates.
4
4
 
5
- Version 0.3.0. Docs: https://ziyara.vip . Manifest (machine readable): https://ziyara.vip/manifest.json
5
+ Version 0.5.0. Docs: https://ziyara.vip . Manifest (machine readable): https://ziyara.vip/manifest.json
6
6
 
7
7
  ## Setup
8
8
  - import { UiButton } from '@ziyarago/ui/vue'
@@ -2110,6 +2110,27 @@ Slots:
2110
2110
  Notes:
2111
2111
  - logo and aside are props in both Vue and React (pass a VNode / element).
2112
2112
 
2113
+ ## UiAuthShowcase (domain)
2114
+ Sign-in layout with a media panel: dark photo card with floating avatars, light gradient card with a social-proof pill, or a tinted panel with a glass caption. The default slot is the form.
2115
+ Props:
2116
+ - `variant`: 'photo' | 'light' | 'tint' = "photo". 'photo': one framed card, media on the left. 'light': gradient form card, proof pill below, glass caption on the media. 'tint': like light, with a coloured panel, a heading on the media and cyan accent.
2117
+ - `title` (required): string. Form heading.
2118
+ - `subtitle`: string. Text under the heading.
2119
+ - `logo`: VNode. Logo node above the heading.
2120
+ - `footer`: VNode. Content under the fields, for example "No account? Sign up".
2121
+ - `image`: string. URL of the media image. Without it the panel shows an accent gradient.
2122
+ - `headline`: string. Large text on the media panel (photo and tint).
2123
+ - `caption`: string. Small text on the media panel (photo) or inside the glass bar (light, tint).
2124
+ - `quote`: string. Line at the bottom of the media panel; photo only.
2125
+ - `chips`: string[]. Pill labels inside the glass bar; light and tint.
2126
+ - `people`: { name: string; src?: string }[]. Avatars: floating on the photo, stacked in the proof pill.
2127
+ - `proof`: string. Social-proof title, e.g. "Join 20k+ users"; light and tint. Shows the pill under the form.
2128
+ - `proofHint`: string. Second line of the proof pill.
2129
+ Slots:
2130
+ - default: Form fields and buttons. React passes them as children.
2131
+ Notes:
2132
+ - logo, footer, image, people are props in both Vue and React. The media panel is hidden below the form on narrow screens (stacks under it).
2133
+
2113
2134
  ## UiPageDashboard (pages)
2114
2135
  Dashboard template: greeting hero, KPI tiles, occupancy chart, sales funnel, top list and activity feed.
2115
2136
  Props:
@@ -2351,7 +2372,15 @@ Notes:
2351
2372
  ## UiPageAuth (pages)
2352
2373
  Sign-in page template without the app shell, as a centered card or a split layout.
2353
2374
  Props:
2354
- - `layout`: 'card' | 'split' = "card". 'split' uses AuthSplit with a side panel; anything else renders a centered AuthLayout card over the background.
2375
+ - `layout`: 'card' | 'split' | 'photo' | 'light' | 'tint' = "card". 'split' uses AuthSplit with a side panel. 'photo', 'light' and 'tint' use AuthShowcase with a media panel. Anything else renders a centered AuthLayout card over the background.
2376
+ - `image`: string. Media image URL; photo, light and tint layouts.
2377
+ - `headline`: string. Large text on the media panel; photo and tint.
2378
+ - `caption`: string. Small text on the media panel or glass bar.
2379
+ - `quote`: string. Bottom line of the media panel; photo only.
2380
+ - `chips`: string[]. Pill labels in the glass bar; light and tint.
2381
+ - `people`: { name: string; src?: string }[]. Avatars on the media panel or in the proof pill.
2382
+ - `proof`: string. Social-proof title under the form; light and tint.
2383
+ - `proofHint`: string. Second line of the proof pill.
2355
2384
  - `title`: string. Form heading.
2356
2385
  - `subtitle`: string. Text under the heading.
2357
2386
  Slots:
package/dist/llms.txt CHANGED
@@ -1,8 +1,8 @@
1
1
  # @ziyarago/ui
2
2
 
3
- > 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, 160+ components and 16 page templates.
3
+ > 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, 183 components and 17 page templates.
4
4
 
5
- Version 0.3.0. Docs: https://ziyara.vip . Manifest (machine readable): https://ziyara.vip/manifest.json
5
+ Version 0.5.0. Docs: https://ziyara.vip . Manifest (machine readable): https://ziyara.vip/manifest.json
6
6
 
7
7
  ## Setup
8
8
  - import { UiButton } from '@ziyarago/ui/vue'
@@ -214,6 +214,7 @@ Umrah, Hajj, hotel and CRM blocks.
214
214
  - UiCallCard: Call recording card with status badge, a playback progress bar and duration.
215
215
  - UiIncomingCall: Glass banner for an incoming call with Decline and Answer buttons.
216
216
  - UiAuthSplit: Two-column sign-in layout with a brand panel on the left and the form on the right.
217
+ - UiAuthShowcase: Sign-in layout with a media panel: dark photo card with floating avatars, light gradient card with a social-proof pill, or a tinted panel with a glass caption. The default slot is the form.
217
218
 
218
219
  ## Page templates
219
220
  Ready layouts with named slots.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ziyarago/ui",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "homepage": "https://ziyara.vip",
5
5
  "imports": {
6
6
  "vue": "import { UiButton } from '@ziyarago/ui/vue'",
@@ -251,7 +251,8 @@
251
251
  "CallRow",
252
252
  "CallCard",
253
253
  "IncomingCall",
254
- "AuthSplit"
254
+ "AuthSplit",
255
+ "AuthShowcase"
255
256
  ]
256
257
  },
257
258
  {
@@ -2273,7 +2274,7 @@
2273
2274
  "id": "auth",
2274
2275
  "label": "Authentication (6)",
2275
2276
  "component": "UiPageAuth",
2276
- "description": "Six sign-in screens in two layouts: centered card and split with a side panel. Always dark theme.",
2277
+ "description": "Seven sign-in screens in five layouts: centered card, split with a side panel, and three with a media panel (photo, light, tint).",
2277
2278
  "shell": false,
2278
2279
  "slots": [
2279
2280
  {
@@ -10050,6 +10051,79 @@
10050
10051
  "logo and aside are props in both Vue and React (pass a VNode / element)."
10051
10052
  ]
10052
10053
  },
10054
+ {
10055
+ "name": "AuthShowcase",
10056
+ "tag": "UiAuthShowcase",
10057
+ "group": "domain",
10058
+ "description": "Sign-in layout with a media panel: dark photo card with floating avatars, light gradient card with a social-proof pill, or a tinted panel with a glass caption. The default slot is the form.",
10059
+ "props": {
10060
+ "variant": {
10061
+ "type": "'photo' | 'light' | 'tint'",
10062
+ "values": [
10063
+ "photo",
10064
+ "light",
10065
+ "tint"
10066
+ ],
10067
+ "default": "photo",
10068
+ "description": "'photo': one framed card, media on the left. 'light': gradient form card, proof pill below, glass caption on the media. 'tint': like light, with a coloured panel, a heading on the media and cyan accent."
10069
+ },
10070
+ "title": {
10071
+ "type": "string",
10072
+ "required": true,
10073
+ "description": "Form heading."
10074
+ },
10075
+ "subtitle": {
10076
+ "type": "string",
10077
+ "description": "Text under the heading."
10078
+ },
10079
+ "logo": {
10080
+ "type": "VNode",
10081
+ "description": "Logo node above the heading."
10082
+ },
10083
+ "footer": {
10084
+ "type": "VNode",
10085
+ "description": "Content under the fields, for example \"No account? Sign up\"."
10086
+ },
10087
+ "image": {
10088
+ "type": "string",
10089
+ "description": "URL of the media image. Without it the panel shows an accent gradient."
10090
+ },
10091
+ "headline": {
10092
+ "type": "string",
10093
+ "description": "Large text on the media panel (photo and tint)."
10094
+ },
10095
+ "caption": {
10096
+ "type": "string",
10097
+ "description": "Small text on the media panel (photo) or inside the glass bar (light, tint)."
10098
+ },
10099
+ "quote": {
10100
+ "type": "string",
10101
+ "description": "Line at the bottom of the media panel; photo only."
10102
+ },
10103
+ "chips": {
10104
+ "type": "string[]",
10105
+ "description": "Pill labels inside the glass bar; light and tint."
10106
+ },
10107
+ "people": {
10108
+ "type": "{ name: string; src?: string }[]",
10109
+ "description": "Avatars: floating on the photo, stacked in the proof pill."
10110
+ },
10111
+ "proof": {
10112
+ "type": "string",
10113
+ "description": "Social-proof title, e.g. \"Join 20k+ users\"; light and tint. Shows the pill under the form."
10114
+ },
10115
+ "proofHint": {
10116
+ "type": "string",
10117
+ "description": "Second line of the proof pill."
10118
+ }
10119
+ },
10120
+ "slots": {
10121
+ "default": "Form fields and buttons. React passes them as children."
10122
+ },
10123
+ "notes": [
10124
+ "logo, footer, image, people are props in both Vue and React. The media panel is hidden below the form on narrow screens (stacks under it)."
10125
+ ]
10126
+ },
10053
10127
  {
10054
10128
  "name": "PageDashboard",
10055
10129
  "tag": "UiPageDashboard",
@@ -10548,13 +10622,56 @@
10548
10622
  "props": {
10549
10623
  "layout": {
10550
10624
  "forwarded": true,
10551
- "type": "'card' | 'split'",
10625
+ "type": "'card' | 'split' | 'photo' | 'light' | 'tint'",
10552
10626
  "values": [
10553
10627
  "card",
10554
- "split"
10628
+ "split",
10629
+ "photo",
10630
+ "light",
10631
+ "tint"
10555
10632
  ],
10556
10633
  "default": "card",
10557
- "description": "'split' uses AuthSplit with a side panel; anything else renders a centered AuthLayout card over the background."
10634
+ "description": "'split' uses AuthSplit with a side panel. 'photo', 'light' and 'tint' use AuthShowcase with a media panel. Anything else renders a centered AuthLayout card over the background."
10635
+ },
10636
+ "image": {
10637
+ "forwarded": true,
10638
+ "type": "string",
10639
+ "description": "Media image URL; photo, light and tint layouts."
10640
+ },
10641
+ "headline": {
10642
+ "forwarded": true,
10643
+ "type": "string",
10644
+ "description": "Large text on the media panel; photo and tint."
10645
+ },
10646
+ "caption": {
10647
+ "forwarded": true,
10648
+ "type": "string",
10649
+ "description": "Small text on the media panel or glass bar."
10650
+ },
10651
+ "quote": {
10652
+ "forwarded": true,
10653
+ "type": "string",
10654
+ "description": "Bottom line of the media panel; photo only."
10655
+ },
10656
+ "chips": {
10657
+ "forwarded": true,
10658
+ "type": "string[]",
10659
+ "description": "Pill labels in the glass bar; light and tint."
10660
+ },
10661
+ "people": {
10662
+ "forwarded": true,
10663
+ "type": "{ name: string; src?: string }[]",
10664
+ "description": "Avatars on the media panel or in the proof pill."
10665
+ },
10666
+ "proof": {
10667
+ "forwarded": true,
10668
+ "type": "string",
10669
+ "description": "Social-proof title under the form; light and tint."
10670
+ },
10671
+ "proofHint": {
10672
+ "forwarded": true,
10673
+ "type": "string",
10674
+ "description": "Second line of the proof pill."
10558
10675
  },
10559
10676
  "title": {
10560
10677
  "forwarded": true,
package/dist/style.css CHANGED
@@ -494,6 +494,47 @@
494
494
  /* CommandPalette active row, dialog focus */
495
495
  .zg-menu__i.is-active,.zg-menu__i[aria-selected="true"]{background:var(--zg-s2)}
496
496
  .zg-modal:focus,.zg-sheet:focus,.zg-fsd:focus,.zg-saver:focus{outline:0}
497
+ /* 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)}
499
+ .zg-ash--tint{--ash-acc:oklch(.7 .13 215)}
500
+ .zg-ash__title{margin:0;font:300 clamp(28px,3.4vw,40px)/1.1 var(--zg-font);letter-spacing:-.03em}
501
+ .zg-ash__sub{margin:10px 0 0;color:var(--zg-text-2);line-height:1.55}
502
+ .zg-ash__head{display:flex;flex-direction:column;gap:6px}
503
+ .zg-ash__fields{display:flex;flex-direction:column;gap:14px;margin-top:22px}
504
+ .zg-ash__foot{margin-top:18px;color:var(--zg-text-2);font-size:14px;text-align:center}
505
+ .zg-ash__form{width:100%;max-width:400px;margin-inline:auto}
506
+ .zg-ash__media{position:relative;overflow:hidden;background:linear-gradient(150deg,color-mix(in oklch,var(--ash-acc) 55%,var(--zg-s2)),color-mix(in oklch,var(--ash-acc) 15%,var(--zg-s1)));background-size:cover;background-position:center}
507
+ .zg-ash--photo .zg-ash__frame{width:100%;max-width:1160px;display:grid;grid-template-columns:minmax(0,1.1fr) minmax(0,1fr);border-radius:var(--ash-r);overflow:hidden;background:var(--zg-s1);box-shadow:var(--zg-rim),var(--zg-shadow-1);min-height:560px}
508
+ .zg-ash--photo .zg-ash__media::before{content:"";position:absolute;inset:0;background:linear-gradient(90deg,color-mix(in oklch,var(--zg-bg) 15%,transparent),var(--zg-s1) 100%),linear-gradient(0deg,color-mix(in oklch,var(--zg-bg) 70%,transparent),transparent 55%)}
509
+ .zg-ash__copy{position:relative;z-index:1;padding:clamp(24px,5vw,64px);padding-top:clamp(60px,12vw,150px);max-width:480px}
510
+ .zg-ash__copy h2{margin:0 0 14px;font:500 clamp(30px,4vw,46px)/1.08 var(--zg-font);letter-spacing:-.03em}
511
+ .zg-ash__copy p{margin:0;color:var(--zg-text-2);font-size:18px;line-height:1.5}
512
+ .zg-ash__float{position:absolute;z-index:1;border-radius:50%;box-shadow:0 0 28px color-mix(in oklch,var(--zg-brand) 55%,transparent)}
513
+ .zg-ash__float--a{inset-inline-start:7%;top:60%}.zg-ash__float--b{inset-inline-start:52%;top:50%}.zg-ash__float--c{inset-inline-start:36%;top:76%}
514
+ @media(prefers-reduced-motion:no-preference){.zg-ash__float{animation:zg-ash-bob 6s ease-in-out infinite alternate}.zg-ash__float--b{animation-delay:-2s}.zg-ash__float--c{animation-delay:-4s}}
515
+ @keyframes zg-ash-bob{to{transform:translateY(-10px)}}
516
+ .zg-ash__quote{position:absolute;z-index:1;inset-inline-start:clamp(24px,5vw,64px);bottom:28px;color:var(--zg-text-1)}
517
+ .zg-ash--photo .zg-ash__side{display:grid;place-items:center;padding:clamp(24px,5vw,56px)}
518
+ .zg-ash__grid{width:100%;max-width:1180px;display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:16px}
519
+ .zg-ash__col{display:flex;flex-direction:column;gap:16px;min-width:0}
520
+ .zg-ash__card{flex:1;display:grid;align-content:center;padding:clamp(24px,4vw,48px);border-radius:var(--ash-r);background:linear-gradient(165deg,var(--zg-s1) 40%,color-mix(in oklch,var(--ash-acc) 26%,var(--zg-s1)));box-shadow:var(--zg-rim)}
521
+ .zg-ash--light .zg-ash__head,.zg-ash--tint .zg-ash__head{align-items:center;text-align:center}
522
+ .zg-ash--tint .zg-ash__card{background:var(--zg-s1)}
523
+ .zg-ash--tint .zg-ash__title{font-weight:400}
524
+ .zg-ash__proof{display:flex;align-items:center;gap:14px;padding:14px 18px;border-radius:var(--ash-r);background:var(--zg-s1);box-shadow:var(--zg-rim)}
525
+ .zg-ash__stack{display:inline-flex}.zg-ash__stack .zg-avatar{margin-inline-end:-10px;box-shadow:0 0 0 2px var(--zg-s1)}
526
+ .zg-ash__proofT{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px;margin-inline-start:10px}.zg-ash__proofT span{color:var(--zg-text-3);font-size:13px}
527
+ .zg-ash--light .zg-ash__grid>.zg-ash__media,.zg-ash--tint .zg-ash__grid>.zg-ash__media{border-radius:var(--ash-r);min-height:520px;display:flex;flex-direction:column;justify-content:flex-end;box-shadow:var(--zg-rim)}
528
+ .zg-ash__mh{position:absolute;inset-inline:0;top:0;margin:0;padding:clamp(20px,3vw,36px);font:400 clamp(24px,2.8vw,34px)/1.2 var(--zg-font);color:#fff;text-shadow:0 1px 16px rgb(0 0 0/.25)}
529
+ .zg-ash__glass{position:relative;margin:12px;padding:18px;border-radius:calc(var(--ash-r) - 8px);display:flex;flex-direction:column;gap:12px;background:color-mix(in oklch,var(--zg-bg) 38%,transparent);backdrop-filter:blur(22px) saturate(1.4);-webkit-backdrop-filter:blur(22px) saturate(1.4);box-shadow:var(--zg-rim);color:var(--zg-text-1)}
530
+ .zg-ash__glass p{margin:0;color:var(--zg-text-2)}
531
+ .zg-ash__chips{display:flex;gap:10px;flex-wrap:wrap}
532
+ .zg-ash__chip{flex:1;text-align:center;padding:14px 18px;border-radius:999px;box-shadow:inset 0 0 0 1px color-mix(in oklch,var(--zg-text-1) 40%,transparent)}
533
+ @media(max-width:860px){.zg-ash--photo .zg-ash__frame,.zg-ash__grid{grid-template-columns:1fr}.zg-ash--photo .zg-ash__media{min-height:300px}.zg-ash__grid>.zg-ash__media{min-height:320px!important;order:2}}
534
+ /* default media art when no image is given */
535
+ .zg-ash__media::after{content:"";position:absolute;inset:0;pointer-events:none;background:radial-gradient(circle at 78% 24%,color-mix(in oklch,#fff 55%,transparent) 0,transparent 30%),radial-gradient(circle at 18% 78%,color-mix(in oklch,var(--ash-acc) 70%,transparent) 0,transparent 38%),conic-gradient(from 210deg at 62% 48%,transparent 0 .08turn,color-mix(in oklch,#fff 22%,transparent) .1turn .18turn,transparent .2turn 1turn)}
536
+ .zg-ash__media[style*="url("]::after{display:none}
537
+ .zg-ash--light .zg-ash__media{--ash-acc:oklch(.72 .08 80)}
497
538
 
498
539
 
499
540
  /* ./sizes/typography.css */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ziyarago/ui",
3
- "version": "0.3.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, 160+ components and 16 page templates.",
3
+ "version": "0.5.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, 183 components and 17 page templates.",
5
5
  "keywords": [
6
6
  "ui",
7
7
  "design-system",
@@ -2,6 +2,7 @@
2
2
  // Each component: { state?, render({ h, props, children, slots, state, set }) }.
3
3
  // h(tag, props, ...children) is supplied by the Vue / React adapter. No hooks inside.
4
4
  import { trap } from './dialog.js';
5
+ import { t } from '../i18n/index.js';
5
6
  export const cx = (...a) => a.filter(Boolean).join(' ');
6
7
  // Copies the listed props that are set; used to forward native attributes and listeners to the real <input>/<textarea>.
7
8
  export const pick = (p, keys) => { const o = {}; for (const k of keys) if (p[k] !== undefined) o[k] = p[k]; return o; };
@@ -78,22 +79,22 @@ export const components = {
78
79
  p.name ? h('input', { type: 'hidden', name: p.name, value: p.modelValue ?? '' }) : null, p.error ? h('span', { class: 'zg-hint zg-hint--error', role: 'alert' }, p.error) : p.hint ? h('span', { class: 'zg-hint' }, p.hint) : null); } },
79
80
  CodeInput: { render: ({ h, props: p }) => { const n = p.length || 6, v = p.modelValue || '';
80
81
  return h('div', { class: 'zg-code', style: { '--n': n } }, ...Array.from({ length: n }, (_, i) => h('span', { key: i, class: cx('zg-code__cell', i === v.length && 'zg-code__cell--on') }, v[i] || '')),
81
- h('input', { value: v, inputMode: 'numeric', autoComplete: 'one-time-code', maxLength: n, 'aria-label': p.label || 'Code', onInput: e => { const x = e.target.value.replace(/\D/g, '').slice(0, n); p.onChange && p.onChange(x); if (x.length === n && v.length !== n) p.onComplete && p.onComplete(x); } })); } },
82
+ h('input', { value: v, inputMode: 'numeric', autoComplete: 'one-time-code', maxLength: n, 'aria-label': p.label || t('CodeInput', 'label'), onInput: e => { const x = e.target.value.replace(/\D/g, '').slice(0, n); p.onChange && p.onChange(x); if (x.length === n && v.length !== n) p.onComplete && p.onComplete(x); } })); } },
82
83
  Stepper: { render: ({ h, props: p }) => h('div', { class: 'zg-stepper' }, ...(p.steps || []).flatMap((s, i) => [i ? h('span', { key: 'b' + i, class: 'zg-step__bar' }) : null, h('span', { key: i, class: cx('zg-step', i < p.current && 'zg-step--done', i === p.current && 'zg-step--cur') }, h('span', { class: 'zg-step__n' }, i < p.current ? icon(h, 'check', 14) : i + 1), s)])) },
83
84
  Card: { slots: ['header', 'footer'], render: ({ h, props: p, children, slots }) => h('section', { class: cx('zg-card', 'zg-glass', p.variant === 'inverse' && 'zg-card--inverse', p.variant === 'gradient' && 'zg-card--grad', p.variant === 'gradient' && 'zg-card--t-' + (p.tone || 'brand'), p.variant === 'gradient' && p.intensity === 'vivid' && 'zg-card--vivid', p.variant === 'outline' && 'zg-card--outline', p.padding === 'none' && 'zg-card--flush', p.interactive && 'zg-card--click'), 'data-theme': p.variant === 'inverse' ? 'dark' : (p.variant === 'gradient' && p.intensity === 'vivid' ? (['brand', 'warning', undefined].includes(p.tone) ? 'light' : 'dark') : undefined), onClick: p.onClick, tabIndex: p.interactive && p.onClick ? 0 : undefined, onKeydown: p.interactive && p.onClick ? e => { if (e.key === 'Enter' && e.target === e.currentTarget) p.onClick(e); } : undefined },
84
85
  slots.header ? h('div', { class: 'zg-card__head' }, slots.header) : (p.title || slots.actions) ? h('div', { class: 'zg-card__head' }, h('div', { style: { flex: 1, minWidth: 0 } }, h('h3', { class: 'zg-card__title' }, p.title), p.subtitle ? h('div', { class: 'zg-card__sub' }, p.subtitle) : null), slots.actions || null) : null, ...children, slots.footer ? h('div', { class: 'zg-card__foot' }, slots.footer) : null) },
85
86
  Modal: { slots: ['title', 'subtitle'], render: ({ h, props: p, children, slots }) => { const foot = slots.footer || p.footer, sub = slots.subtitle || p.subtitle, ttl = slots.title || p.title; return !p.open ? null : h('div', { class: 'zg-modal-scrim', onClick: p.closeOnBackdrop === false ? undefined : p.onClose }, h('div', { class: 'zg-modal', role: 'dialog', 'aria-modal': 'true', 'aria-label': p.title, style: p.width ? { maxWidth: p.width } : undefined, ref: trap({ onClose: p.closeOnEsc === false ? undefined : p.onClose }), onClick: e => e.stopPropagation() },
86
- h('div', { class: 'zg-modal__head' }, sub ? h('div', { style: { flex: 1, minWidth: 0 } }, h('h3', null, ttl), h('div', { class: 'zg-modal__sub' }, sub)) : h('h3', null, ttl), p.closable === false ? null : h('button', { type: 'button', class: 'zg-ibtn zg-ibtn--sm zg-ibtn--ghost', 'aria-label': 'Close', onClick: p.onClose }, icon(h, 'x'))), h('div', { class: 'zg-modal__body' }, ...children), foot ? h('div', { class: 'zg-modal__foot' }, foot) : null)); } },
87
+ h('div', { class: 'zg-modal__head' }, sub ? h('div', { style: { flex: 1, minWidth: 0 } }, h('h3', null, ttl), h('div', { class: 'zg-modal__sub' }, sub)) : h('h3', null, ttl), p.closable === false ? null : h('button', { type: 'button', class: 'zg-ibtn zg-ibtn--sm zg-ibtn--ghost', 'aria-label': t('Modal', 'close'), onClick: p.onClose }, icon(h, 'x'))), h('div', { class: 'zg-modal__body' }, ...children), foot ? h('div', { class: 'zg-modal__foot' }, foot) : null)); } },
87
88
  Drawer: { render: ({ h, props: p, children, slots }) => { const foot = slots.footer || p.footer; return !p.open ? null : h('div', { class: cx('zg-modal-scrim', 'zg-drawer-scrim', 'zg-drawer-scrim--' + (p.side || 'end')), onClick: p.onClose }, h('div', { class: 'zg-modal zg-drawer', role: 'dialog', 'aria-modal': 'true', 'aria-label': p.title, ref: trap({ onClose: p.onClose }), onClick: e => e.stopPropagation() },
88
- h('div', { class: 'zg-modal__head' }, h('h3', null, p.title), h('button', { type: 'button', class: 'zg-ibtn zg-ibtn--sm zg-ibtn--ghost', 'aria-label': 'Close', onClick: p.onClose }, icon(h, 'x'))), h('div', { class: 'zg-modal__body' }, ...children), foot ? h('div', { class: 'zg-modal__foot' }, foot) : null)); } },
89
+ h('div', { class: 'zg-modal__head' }, h('h3', null, p.title), h('button', { type: 'button', class: 'zg-ibtn zg-ibtn--sm zg-ibtn--ghost', 'aria-label': t('Drawer', 'close'), onClick: p.onClose }, icon(h, 'x'))), h('div', { class: 'zg-modal__body' }, ...children), foot ? h('div', { class: 'zg-modal__foot' }, foot) : null)); } },
89
90
  Toast: { render: ({ h, props: p, children }) => h('div', { class: cx('zg-toast', 'zg-toast--' + (p.tone || 'info')), role: 'status' }, h('span', { class: 'zg-toast__icon' }, icon(h, { success: 'check', danger: 'circle-alert', warning: 'triangle-alert' }[p.tone] || 'info', 18)), h('div', null, p.title ? h('b', null, p.title) : null, h('span', null, ...children))) },
90
- Breadcrumbs: { render: ({ h, props: p }) => h('nav', { class: 'zg-crumbs', 'aria-label': 'Breadcrumb' }, ...(p.items || []).flatMap((it, i, a) => [i ? (p.separator === 'slash' || p.separator === 'dot' ? h('span', { key: 's' + i, class: 'zg-crumbs__sep', 'aria-hidden': 'true' }, p.separator === 'slash' ? '/' : '\u00b7') : icon(h, 'chevron-right', 14)) : null, i === a.length - 1 ? h('b', { key: i, 'aria-current': 'page' }, it.label) : h('a', { key: i, href: it.href || '#' }, it.label)])) },
91
+ Breadcrumbs: { render: ({ h, props: p }) => h('nav', { class: 'zg-crumbs', 'aria-label': t('Breadcrumbs', 'label') }, ...(p.items || []).flatMap((it, i, a) => [i ? (p.separator === 'slash' || p.separator === 'dot' ? h('span', { key: 's' + i, class: 'zg-crumbs__sep', 'aria-hidden': 'true' }, p.separator === 'slash' ? '/' : '\u00b7') : icon(h, 'chevron-right', 14)) : null, i === a.length - 1 ? h('b', { key: i, 'aria-current': 'page' }, it.label) : h('a', { key: i, href: it.href || '#' }, it.label)])) },
91
92
  Pagination: { render: ({ h, props: p }) => { const total = Math.max(1, Math.floor(+p.pages) || 1), cur = Math.min(total, Math.max(1, +p.page || 1)), s = Math.max(0, p.siblings == null ? 1 : Math.floor(+p.siblings) || 0), go = n => p.onChange && n >= 1 && n <= total && n !== p.page && p.onChange(n);
92
93
  const set = new Set([1, total]); for (let i = cur - s; i <= cur + s; i++) if (i >= 1 && i <= total) set.add(i);
93
94
  const nums = [...set].sort((a, b) => a - b), items = []; nums.forEach((n, i) => { const prev = nums[i - 1]; if (prev && n - prev === 2) items.push(prev + 1); else if (prev && n - prev > 2) items.push('…'); items.push(n); });
94
- return h('nav', { class: 'zg-pager', 'aria-label': p.label || 'Pagination' }, p.hideArrows ? null : h('button', { key: 'prev', type: 'button', class: 'zg-pager__nav', 'aria-label': (p.labels && p.labels.prev) || 'Previous page', disabled: cur <= 1, onClick: () => go(cur - 1) }, icon(h, 'chevron-left', 16)),
95
- ...items.map((n, i) => n === '…' ? h('span', { key: 'e' + i, class: 'zg-pager__gap', 'aria-hidden': 'true' }, '…') : h('button', { key: n, type: 'button', 'aria-label': ((p.labels && p.labels.page) || 'Page') + ' ' + n, 'aria-current': n === p.page ? 'page' : undefined, onClick: () => go(n) }, n)),
96
- p.hideArrows ? null : h('button', { key: 'next', type: 'button', class: 'zg-pager__nav', 'aria-label': (p.labels && p.labels.next) || 'Next page', disabled: cur >= total, onClick: () => go(cur + 1) }, icon(h, 'chevron-right', 16))); } },
95
+ return h('nav', { class: 'zg-pager', 'aria-label': p.label || t('Pagination', 'label') }, p.hideArrows ? null : h('button', { key: 'prev', type: 'button', class: 'zg-pager__nav', 'aria-label': (p.labels && p.labels.prev) || t('Pagination', 'prev'), disabled: cur <= 1, onClick: () => go(cur - 1) }, icon(h, 'chevron-left', 16)),
96
+ ...items.map((n, i) => n === '…' ? h('span', { key: 'e' + i, class: 'zg-pager__gap', 'aria-hidden': 'true' }, '…') : h('button', { key: n, type: 'button', 'aria-label': ((p.labels && p.labels.page) || t('Pagination', 'page')) + ' ' + n, 'aria-current': n === p.page ? 'page' : undefined, onClick: () => go(n) }, n)),
97
+ p.hideArrows ? null : h('button', { key: 'next', type: 'button', class: 'zg-pager__nav', 'aria-label': (p.labels && p.labels.next) || t('Pagination', 'next'), disabled: cur >= total, onClick: () => go(cur + 1) }, icon(h, 'chevron-right', 16))); } },
97
98
  Progress: { render: ({ h, props: p }) => h('div', { class: cx('zg-progress', p.tone && 'zg-progress--' + p.tone) }, (p.label || p.meta) ? h('div', { class: 'zg-progress__row' }, h('span', null, p.label), h('span', { class: 'zg-mono' }, p.meta)) : null, h('div', { class: 'zg-progress__track', role: 'progressbar', 'aria-valuenow': p.value, 'aria-valuemin': 0, 'aria-valuemax': 100 }, h('div', { class: 'zg-progress__bar', style: { width: Math.max(0, Math.min(100, p.value || 0)) + '%' } }))) },
98
99
  Ring: { render: ({ h, props: p }) => { const s = { xs: 72, sm: 120, md: 180, lg: 240, xl: 300 }[p.size] || (Number(p.size) > 0 ? Number(p.size) : 180), th = p.thickness || Math.max(8, Math.round(s * .078)), ticks = p.ticks !== false, v = Math.max(0, Math.min(100, p.value || 0)) / 100, col = p.color || 'var(--zg-brand)';
99
100
  const r = (s - th) / 2 - (ticks ? 12 : 0), cir = 2 * Math.PI * r, tk = [];
@@ -105,14 +106,14 @@ export const components = {
105
106
  KpiTile: { render: ({ h, props: p }) => h('div', { class: cx('zg-kpi', p.variant && 'zg-kpi--' + p.variant) }, h('span', { class: 'zg-kpi__label' }, p.label), h('span', { class: 'zg-kpi__value zg-num' }, p.value), (p.sub || p.delta) ? h('span', { class: 'zg-kpi__sub' }, p.delta ? h('span', { class: cx('zg-delta', String(p.delta).startsWith('-') ? 'zg-delta--down' : 'zg-delta--up') }, p.delta + ' ') : null, p.sub) : null) },
106
107
  DataTable: { render: ({ h, props: p, slots }) => { const cs = c => (c.align || c.width) ? { textAlign: c.align, width: c.width } : undefined, cols = p.columns || [], rows = p.rows || [], sel = p.selected || [], all = rows.length && rows.every(r => sel.includes(r.id));
107
108
  const th = c => h('th', { key: c.key, 'aria-sort': p.sortKey === c.key ? (p.sortDir === 'desc' ? 'descending' : 'ascending') : undefined, class: c.num ? 'num' : undefined, style: cs(c) }, c.sortable ? h('button', { type: 'button', onClick: () => p.onSort && p.onSort(c.key, p.sortKey === c.key && p.sortDir !== 'desc' ? 'desc' : 'asc') }, c.label, p.sortKey === c.key ? icon(h, p.sortDir === 'desc' ? 'arrow-down' : 'arrow-up', 12) : null) : c.label);
108
- return h('div', { class: 'zg-table-wrap' }, h('table', { class: 'zg-table' }, h('thead', null, h('tr', null, p.onSelect ? h('th', { style: { width: '44px' } }, h('button', { type: 'button', role: 'checkbox', class: 'zg-check', 'aria-checked': String(!!all), 'aria-label': (p.labels && p.labels.selectAll) || 'Select all', onClick: () => p.onSelect(all ? [] : rows.map(r => r.id)) }, h('span', { class: 'zg-check__box' }, all ? icon(h, 'check', 14) : null))) : null, ...cols.map(th))),
109
- h('tbody', null, ...(rows.length ? rows.map((r, ri) => h('tr', { key: r.id, 'aria-selected': sel.includes(r.id) ? 'true' : undefined, class: p.onRowClick ? 'zg-tr--click' : undefined, onClick: p.onRowClick ? () => p.onRowClick(r, ri) : undefined }, p.onSelect ? h('td', null, h('button', { type: 'button', role: 'checkbox', class: 'zg-check', 'aria-checked': String(sel.includes(r.id)), 'aria-label': (p.labels && p.labels.selectRow) || 'Select row', onClick: () => p.onSelect(sel.includes(r.id) ? sel.filter(i => i !== r.id) : [...sel, r.id]) }, h('span', { class: 'zg-check__box' }, sel.includes(r.id) ? icon(h, 'check', 14) : null))) : null, ...cols.map(c => h('td', { key: c.key, class: c.num ? 'num' : undefined, style: cs(c) }, c.render ? c.render(r[c.key], r, h) : (r[c.key] ?? '—'))))) : [h('tr', { key: 'e' }, h('td', { colSpan: cols.length + 1 }, h('div', { class: 'zg-empty' }, slots.empty || p.empty || '—')))])))); } },
109
+ return h('div', { class: 'zg-table-wrap' }, h('table', { class: 'zg-table' }, h('thead', null, h('tr', null, p.onSelect ? h('th', { style: { width: '44px' } }, h('button', { type: 'button', role: 'checkbox', class: 'zg-check', 'aria-checked': String(!!all), 'aria-label': (p.labels && p.labels.selectAll) || t('DataTable', 'selectAll'), onClick: () => p.onSelect(all ? [] : rows.map(r => r.id)) }, h('span', { class: 'zg-check__box' }, all ? icon(h, 'check', 14) : null))) : null, ...cols.map(th))),
110
+ h('tbody', null, ...(rows.length ? rows.map((r, ri) => h('tr', { key: r.id, 'aria-selected': sel.includes(r.id) ? 'true' : undefined, class: p.onRowClick ? 'zg-tr--click' : undefined, onClick: p.onRowClick ? () => p.onRowClick(r, ri) : undefined }, p.onSelect ? h('td', null, h('button', { type: 'button', role: 'checkbox', class: 'zg-check', 'aria-checked': String(sel.includes(r.id)), 'aria-label': (p.labels && p.labels.selectRow) || t('DataTable', 'selectRow'), onClick: () => p.onSelect(sel.includes(r.id) ? sel.filter(i => i !== r.id) : [...sel, r.id]) }, h('span', { class: 'zg-check__box' }, sel.includes(r.id) ? icon(h, 'check', 14) : null))) : null, ...cols.map(c => h('td', { key: c.key, class: c.num ? 'num' : undefined, style: cs(c) }, c.render ? c.render(r[c.key], r, h) : (r[c.key] ?? '—'))))) : [h('tr', { key: 'e' }, h('td', { colSpan: cols.length + 1 }, h('div', { class: 'zg-empty' }, slots.empty || p.empty || '—')))])))); } },
110
111
  KanbanColumn: { render: ({ h, props: p, children }) => h('div', { class: 'zg-kcol zg-dm' }, h('div', { class: 'zg-kcol__head' }, p.title, h('span', { class: 'zg-badge' }, p.count ?? children.length)), ...children) },
111
112
  KanbanCard: { render: ({ h, props: p, children }) => h('div', { class: 'zg-kcard zg-dm' }, h('b', { style: { fontSize: dzf(14) } }, p.title), p.sub ? h('span', { class: 'zg-hint' }, p.sub) : null, ...children, p.tag ? h('span', { class: cx('zg-badge', p.tone && 'zg-badge--' + p.tone), style: { alignSelf: 'flex-start' } }, p.tag) : null) },
112
113
  Timeline: { render: ({ h, props: p }) => h('div', { class: 'zg-timeline' }, ...(p.items || []).map((it, i, a) => h('div', { key: i, class: 'zg-tl' }, h('div', { class: 'zg-tl__rail' }, h('span', { class: 'zg-tl__dot' }), i < a.length - 1 ? h('span', { class: 'zg-tl__line' }) : null), h('div', { class: 'zg-tl__body' }, h('b', null, it.title), it.text ? h('span', null, it.text) : null, h('span', { class: 'zg-tl__time' }, it.time))))) },
113
- TopNav: { render: ({ h, props: p, slots }) => h('header', { class: 'zg-topnav' }, h('div', { class: 'zg-topnav__brand' }, p.brand), h('nav', { class: 'zg-topnav__items', 'aria-label': 'Main' }, ...(p.items || []).map(it => nav(h, it, p.value, p.onChange))), h('div', { class: 'zg-topnav__actions' }, slots.actions || p.actions || null)) },
114
+ TopNav: { render: ({ h, props: p, slots }) => h('header', { class: 'zg-topnav' }, h('div', { class: 'zg-topnav__brand' }, p.brand), h('nav', { class: 'zg-topnav__items', 'aria-label': t('TopNav', 'label') }, ...(p.items || []).map(it => nav(h, it, p.value, p.onChange))), h('div', { class: 'zg-topnav__actions' }, slots.actions || p.actions || null)) },
114
115
  SideNav: { render: ({ h, props: p, slots }) => h('aside', { class: cx('zg-sidenav', p.collapsed && 'zg-sidenav--collapsed') }, h('div', { class: 'zg-sidenav__brand' }, p.brand), ...(p.items || []).map(it => it.group ? h('div', { key: it.group, class: 'zg-sidenav__group', style: p.collapsed ? { display: 'none' } : undefined }, it.group) : nav(h, it, p.value, p.onChange, p.collapsed)), h('div', { class: 'zg-sidenav__sp' }), slots.footer || p.footer || null,
115
- p.onToggle ? h('button', { type: 'button', class: 'zg-navitem', onClick: p.onToggle, 'aria-label': 'Toggle sidebar' }, icon(h, p.collapsed ? 'panel-left-open' : 'panel-left-close', 18), h('span', null, p.collapseLabel || 'Collapse')) : null) },
116
+ p.onToggle ? h('button', { type: 'button', class: 'zg-navitem', onClick: p.onToggle, 'aria-label': t('SideNav', 'toggle') }, icon(h, p.collapsed ? 'panel-left-open' : 'panel-left-close', 18), h('span', null, p.collapseLabel || t('SideNav', 'collapse'))) : null) },
116
117
  AppShell: { state: { collapsed: false }, render: ({ C, h, props: p, children, slots, state, set }) => {
117
118
  const brand = p.brand, top = (p.nav || 'top') === 'top', col = p.collapsed ?? state.collapsed, toggle = () => p.onCollapse ? p.onCollapse(!col) : set({ collapsed: !state.collapsed });
118
119
  return h('div', { class: cx('zg-root', 'zg-shell', top && 'zg-shell--top', p.frame && 'zg-shell--frame'), style: { background: 'var(--zg-bg)' } },
@@ -124,9 +125,9 @@ export const components = {
124
125
  HotelCard: { render: ({ h, props: p }) => h('article', { class: 'zg-hotel zg-dm' }, h('div', { class: 'zg-hotel__img', style: { backgroundImage: p.image ? `url(${p.image})` : undefined } }, h('div', { class: 'zg-hotel__tags' }, ...(p.tags || []).map(t => h('span', { key: t, class: 'zg-badge zg-badge--brand' }, t)))), h('div', { class: 'zg-hotel__body' },
125
126
  h('div', { class: 'zg-hotel__row' }, h('div', { style: { minWidth: 0 } }, h('b', { style: { fontSize: dzf(17) } }, p.name), h('div', { class: 'zg-hint' }, '★'.repeat(p.stars || 0), ' · ', p.distance)), h('span', { class: 'zg-badge zg-badge--success', style: { marginInlineStart: 'auto' } }, p.rating)),
126
127
  h('div', { class: 'zg-hotel__row', style: { alignItems: 'center' } }, h('span', { class: 'zg-hint' }, 'SAR / ', p.night || 'night'), h('div', { class: 'zg-hotel__price' }, h('div', { class: 'zg-num', style: { fontSize: dzf(26), fontWeight: 300 } }, fmt.num(p.price))))) ) },
127
- TourCard: { render: ({ C, h, props: p, slots }) => { const seats = Number(p.seats) || 0, booked = Number(p.booked) || 0; return h('article', { class: 'zg-card zg-glass zg-dm' }, h('div', { class: 'zg-card__head' }, h('div', { style: { flex: 1 } }, h('h3', { class: 'zg-card__title' }, p.name), h('div', { class: 'zg-card__sub' }, [p.city, p.sessions != null && p.sessions !== '' ? p.sessions + ' sessions' : null].filter(Boolean).join(' · '))), h('span', { class: 'zg-badge zg-badge--info' }, '360°')), h(C.Progress, { value: seats > 0 ? Math.min(100, booked / seats * 100) : 0, label: 'Seats', meta: `${booked}/${seats}` }), slots.actions || null); } },
128
+ TourCard: { render: ({ C, h, props: p, slots }) => { const seats = Number(p.seats) || 0, booked = Number(p.booked) || 0; return h('article', { class: 'zg-card zg-glass zg-dm' }, h('div', { class: 'zg-card__head' }, h('div', { style: { flex: 1 } }, h('h3', { class: 'zg-card__title' }, p.name), h('div', { class: 'zg-card__sub' }, [p.city, p.sessions != null && p.sessions !== '' ? p.sessions + ' sessions' : null].filter(Boolean).join(' · '))), h('span', { class: 'zg-badge zg-badge--info' }, '360°')), h(C.Progress, { value: seats > 0 ? Math.min(100, booked / seats * 100) : 0, label: t('TourCard', 'seats'), meta: `${booked}/${seats}` }), slots.actions || null); } },
128
129
  PrayerTimes: { render: ({ h, props: p }) => h('div', { class: 'zg-pt zg-dm' }, ...(p.items || []).map((it, i) => { it = it || {}; return h('div', { key: i, class: 'zg-pt__i', 'aria-current': i === p.next ? 'true' : undefined }, it.name, h('b', null, it.time)); })) },
129
- RamadanTracker: { render: ({ h, props: p }) => { const day = Number(p.day) || 0; return h('div', { class: 'zg-rt zg-dm', role: 'img', 'aria-label': day ? `Day ${day} of 30` : 'Ramadan tracker' }, ...Array.from({ length: 30 }, (_, i) => { const n = i + 1; return h('i', { key: n, 'data-s': n === day ? 'cur' : n < day ? 'past' : n > 20 ? 'last' : undefined, 'data-odd': n > 20 && n % 2 ? '' : undefined, title: String(n) }); })); } },
130
- CallRow: { render: ({ C, h, props: p }) => h('div', { class: 'zg-call zg-dm' }, h(C.Avatar, { name: p.name, size: dzp(p, 36) }), h('div', { style: { minWidth: 0 } }, h('b', { style: { fontSize: dzf(14) } }, p.name), h('div', { class: 'zg-hint' }, [p.status, fmt.dur(p.duration, p.durUnits)].filter(Boolean).join(' · '))), h(C.IconButton, { icon: 'play', label: (p.labels && p.labels.play) || 'Play', size: dzb(p), onClick: p.onPlay })) },
130
+ RamadanTracker: { render: ({ h, props: p }) => { const day = Number(p.day) || 0; return h('div', { class: 'zg-rt zg-dm', role: 'img', 'aria-label': day ? t('RamadanTracker', 'day', { day }) : t('RamadanTracker', 'label') }, ...Array.from({ length: 30 }, (_, i) => { const n = i + 1; return h('i', { key: n, 'data-s': n === day ? 'cur' : n < day ? 'past' : n > 20 ? 'last' : undefined, 'data-odd': n > 20 && n % 2 ? '' : undefined, title: String(n) }); })); } },
131
+ CallRow: { render: ({ C, h, props: p }) => h('div', { class: 'zg-call zg-dm' }, h(C.Avatar, { name: p.name, size: dzp(p, 36) }), h('div', { style: { minWidth: 0 } }, h('b', { style: { fontSize: dzf(14) } }, p.name), h('div', { class: 'zg-hint' }, [p.status, fmt.dur(p.duration, p.durUnits)].filter(Boolean).join(' · '))), h(C.IconButton, { icon: 'play', label: (p.labels && p.labels.play) || t('CallRow', 'play'), size: dzb(p), onClick: p.onPlay })) },
131
132
  };
132
133
  export { fmt };