@ziyarago/ui 0.15.3 → 0.16.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.
- package/CHANGELOG.md +6 -41
- package/dist/llms-full.txt +9 -3
- package/dist/llms.txt +2 -2
- package/dist/manifest.json +22 -6
- package/dist/style.css +7 -0
- package/package.json +1 -1
- package/src/core/components2.js +26 -14
- package/src/core/keys.js +37 -0
- package/src/internal.js +1 -1
- package/src/meta/forms.js +5 -3
- package/src/meta/overlay.js +3 -2
- package/src/react/index.js +1 -1
- package/src/styles/components8.css +7 -0
- package/types/react.d.ts +11 -1
- package/types/vue.d.ts +11 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
## [0.16.0](https://github.com/ziyarago/ui/compare/v0.15.3...v0.16.0) (2026-10-21)
|
|
2
|
+
|
|
3
|
+
### Features
|
|
4
|
+
|
|
5
|
+
* keyboard and ARIA for Combobox, MultiSelect, Calendar and DatePicker, Tooltip Escape, client-side today, changelog cleanup ([ec905ac](https://github.com/ziyarago/ui/commit/ec905ace4be6d59c884b62941a697b53a2d236b7))
|
|
6
|
+
|
|
1
7
|
## [0.15.3](https://github.com/ziyarago/ui/compare/v0.15.2...v0.15.3) (2026-10-21)
|
|
2
8
|
|
|
3
9
|
### Bug Fixes
|
|
@@ -117,44 +123,3 @@
|
|
|
117
123
|
### Features
|
|
118
124
|
|
|
119
125
|
* publish @ziyarago/ui and @ziyarago/ui-mcp to npm ([bc42145](https://github.com/ziyarago/ui/commit/bc42145f7750fdd8071a29bc088b8366a1adb391))
|
|
120
|
-
|
|
121
|
-
# Changelog
|
|
122
|
-
|
|
123
|
-
All notable changes follow [Keep a Changelog](https://keepachangelog.com) and [SemVer](https://semver.org). Pre-1.0: minor versions may break.
|
|
124
|
-
|
|
125
|
-
## 0.1.0 — first release
|
|
126
|
-
|
|
127
|
-
### Added
|
|
128
|
-
- Docs site SEO: canonical, Open Graph, Twitter and JSON-LD tags per page, generated `sitemap.xml` and `robots.txt`, 404 page with noindex, og.png, favicon; Google Analytics 4 and Yandex Metrika loaded from build variables.
|
|
129
|
-
- Deployment of the docs to your own server over SSH (releases directory, atomic switch, rollback): `deploy/`, `.github/workflows/deploy-docs.yml`, `docs/DEPLOYMENT.md`.
|
|
130
|
-
- Display settings: `useUiTheme()` gains `setMotion('auto|on|off')`, `setEffects('auto|full|lite')` and `setBackground(name)`; attributes `data-motion`, `data-fx`, `data-bg`; no-flash init script in the Nuxt module.
|
|
131
|
-
- Page backgrounds (`aurora`, `sunset`, `ocean`, `emerald`, `rose`, `mesh`) and gradient cards (`<UiCard variant="gradient" tone intensity>`; 8 tones, soft and vivid, dark and light).
|
|
132
|
-
- API metadata for all 182 components in `src/meta/` (props with every value, defaults, slots, events), validated against the render code by `npm run check`; shown as API reference tables in the docs.
|
|
133
|
-
- AI tooling: `dist/manifest.json`, `llms.txt`, `llms-full.txt` and the `@ziyarago/ui-mcp` package (MCP server + `init` for AGENTS.md, Claude skill, Cursor rule).
|
|
134
|
-
- Automated releases: GitHub Actions + semantic-release (version from commit messages), npm publish with provenance and docs deploy to ziyara.vip. See CONTRIBUTING.md.
|
|
135
|
-
- Page templates gallery in the docs site (`/docs/templates`): 17 live templates in the app shell with top or side menu, slot highlighting and an inspector (accepted components, props, example) for 89 page slots and 5 shell slots.
|
|
136
|
-
- `size` prop (`xs | sm | md | lg | xl`, default `md`) on controls and density-bearing components (about 120). Adapters set `data-size` on the root; `md` keeps the previous look. Scale tokens `--zg-sz-*` live in `tokens.css`, per-group overrides in `src/styles/sizes/`.
|
|
137
|
-
- Popup surfaces (`--zg-pop`), stacking tokens (`--zg-z-bar`, `--zg-z-pop`, `--zg-z-modal`) and entrance motion for selects, calendars, menus, hover cards, modals and drawers.
|
|
138
|
-
- Docs pages for Fonts and Colors (families, tokens, weight mapping, swatches in both themes and both palettes).
|
|
139
|
-
- Documentation site in `site/` (Nuxt): landing, getting started, theming, 182 component pages with live previews and Vue/React code.
|
|
140
|
-
- gluestack parity, 16 components: Heading, Text, Box, Center, HStack, VStack, Grid, Link, Pressable, Icon, Portal, LiquidGlass, BottomSheet, Actionsheet, ImageViewer, ChatAi.
|
|
141
|
-
- Nuxt module `@ziyarago/ui/nuxt` (auto-registered components, CSS, theme attributes, SSR).
|
|
142
|
-
- Framework-neutral core with Vue 3 (`@ziyarago/ui/vue`) and React 18+ (`@ziyarago/ui/react`) adapters.
|
|
143
|
-
- 160+ components in 12 groups: actions, forms, surfaces, data, navigation, layout, CRM, hotels, tours and 360°, prayer and Ramadan.
|
|
144
|
-
- 16 page templates with named slots (`PageDashboard`, `PageCrm`, `PageBookings`, `PageCatalog`, `PageDetail`, `PageSettings`, `PageUserList`, `PageProfile`, `PageForm`, `PageOrder`, `PageAccount`, `PagePricing`, `PageFaq`, `PagePatterns`, `PageAuth`, `PageStatus`).
|
|
145
|
-
- Two menus in one shell: top and side, with a collapsible sidebar.
|
|
146
|
-
- OKLCH tokens, palettes `gold` and `tamam`, dark and light themes, RTL, Thmanyah and Manrope.
|
|
147
|
-
- Validation rules (`rules`) and `ValidatedField`.
|
|
148
|
-
- `UiMap`, `UiTour` with spotlight, `UiSortable`, `UiTreeSelect`, `UiRichEditor`.
|
|
149
|
-
- `tokens/tokens.json` for Flutter and native clients; `dist/style.css` single-file stylesheet.
|
|
150
|
-
|
|
151
|
-
### Known gaps
|
|
152
|
-
- Props are typed for 9 core components; the rest accept `Record<string, any>`.
|
|
153
|
-
- Installed from the packed tarball into clean Vite+Vue, Vite+React and Nuxt 3 apps: all build, Nuxt SSR renders. No automated tests yet.
|
|
154
|
-
- Light theme and RTL are covered by tokens but not yet screenshot-tested for every component.
|
|
155
|
-
|
|
156
|
-
### Fixed before release
|
|
157
|
-
- Arabic looked bold: Thmanyah has no 600 face and is heavier than Manrope. `fonts.css` now maps weights with ranges (200-300 Light, 400-600 Regular, 700 Medium, 800 Bold, 900 Black).
|
|
158
|
-
- Light theme: stronger card separation (`--zg-s1/s2/s3`, `--zg-stroke`, `--zg-rim`, `--zg-shadow-1`); `UiCard variant="inverse"` re-scopes tokens to dark so content inside stays readable; bell counter and incoming-call button text contrast.
|
|
159
|
-
- `Page*` templates with `title` crashed in Vue/React (`C.PageHeader` was called as a function).
|
|
160
|
-
- `useUiTheme()` and `UiMap` touched `document` during SSR.
|
package/dist/llms-full.txt
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
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, 102 free components and 6 page templates (more in @ziyarago/ui-pro).
|
|
4
4
|
|
|
5
|
-
Version 0.
|
|
5
|
+
Version 0.16.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'
|
|
@@ -674,6 +674,7 @@ Events:
|
|
|
674
674
|
Notes:
|
|
675
675
|
- Selecting from the list is the only way to set a value: free text is never emitted.
|
|
676
676
|
- The list closes on outside click and on Escape.
|
|
677
|
+
- Keyboard (WAI-ARIA combobox): the input has role=combobox with aria-expanded, aria-controls and aria-activedescendant; focus stays in the input, Arrow Down and Up open the list and move the active option (wraps, skips disabled), Enter chooses it, Escape closes.
|
|
677
678
|
|
|
678
679
|
## UiTreeSelect (forms, Pro)
|
|
679
680
|
Dropdown with a searchable tree. Use for hierarchical choices such as regions, categories or org units.
|
|
@@ -836,12 +837,14 @@ Props:
|
|
|
836
837
|
- `min`: Date | string | number. Earliest selectable day; earlier days are disabled.
|
|
837
838
|
- `max`: Date | string | number. Latest selectable day; later days are disabled.
|
|
838
839
|
- `disabledDate`: (date: Date) => boolean. Return true to disable a specific day.
|
|
840
|
+
- `today`: Date | string | number. The day marked as today. By default it is read from the browser clock after mount (nothing is marked in server-rendered HTML), so server and client never disagree near midnight or across time zones. Pass it to mark a day in the HTML too.
|
|
839
841
|
- `events`: Record<string, string> = "{}". Marked days: keys are YYYY-MM-DD, values are the tooltip text.
|
|
840
842
|
- `size`: 'xs' | 'sm' | 'md' | 'lg' | 'xl' = "md". Day cell and header size.
|
|
841
843
|
Events:
|
|
842
844
|
- change: Called with a Date object (local time) for the clicked day.
|
|
843
845
|
Notes:
|
|
844
846
|
- The event payload is a Date, not a string: convert to ISO yourself if the model is a string.
|
|
847
|
+
- Keyboard: one tab stop on the day grid; arrows move by day or week, Home and End to the week start and end, PageUp and PageDown change month (Shift: year), Enter or Space picks. Without a selected date the visible month is the current month, which server and client can disagree on right at a month boundary; pass modelValue or today to make it deterministic.
|
|
845
848
|
|
|
846
849
|
## UiRangeCalendar (forms, Pro)
|
|
847
850
|
Month grid for picking a date range with hover preview. Use for stays, trips and report periods.
|
|
@@ -866,6 +869,7 @@ Props:
|
|
|
866
869
|
- `min`: Date | string | number. Earliest selectable day, forwarded to the calendar.
|
|
867
870
|
- `max`: Date | string | number. Latest selectable day, forwarded to the calendar.
|
|
868
871
|
- `disabledDate`: (date: Date) => boolean. Return true to disable a specific day, forwarded to the calendar.
|
|
872
|
+
- `today`: Date | string | number. The day marked as today in the calendar; by default read from the browser clock after mount. Forwarded to the calendar.
|
|
869
873
|
- `required`: boolean = false. Adds a red asterisk to the label. Does not validate.
|
|
870
874
|
- `label`: string. Label shown above the control.
|
|
871
875
|
- `hint`: string. Helper text under the control. Hidden while error is set.
|
|
@@ -877,7 +881,7 @@ Events:
|
|
|
877
881
|
- change: Called with a Date object (local midnight) for the chosen day. The popup closes after picking.
|
|
878
882
|
Notes:
|
|
879
883
|
- The event payload is a Date, not a string: convert to ISO yourself if the model is a string.
|
|
880
|
-
- The popup closes on outside click and on Escape.
|
|
884
|
+
- The popup closes on outside click and on Escape (focus returns to the trigger). Arrow Down or Up on the trigger opens it and focus moves into the calendar, which uses the Calendar keys.
|
|
881
885
|
|
|
882
886
|
## UiTimePicker (forms)
|
|
883
887
|
Hour and minute selects for a time of day. Use for opening hours and appointment times.
|
|
@@ -1596,6 +1600,7 @@ Slots:
|
|
|
1596
1600
|
- default: The element the tooltip is attached to.
|
|
1597
1601
|
Notes:
|
|
1598
1602
|
- Pure CSS bubble; it cannot contain markup. For rich content use HoverCard.
|
|
1603
|
+
- Escape hides the bubble while it is shown (hover or focus) without moving focus; it stays visible while the pointer is over the trigger or the bubble, and returns on the next hover or focus.
|
|
1599
1604
|
|
|
1600
1605
|
## UiDropdown (overlay)
|
|
1601
1606
|
Button that opens a menu of actions; use for "more" menus and action lists.
|
|
@@ -1696,11 +1701,12 @@ Events:
|
|
|
1696
1701
|
- close: Close icon.
|
|
1697
1702
|
|
|
1698
1703
|
## UiScreensaver (overlay, Pro)
|
|
1699
|
-
Full-screen idle screen with a large clock, date and
|
|
1704
|
+
Full-screen idle screen with a large clock, date and an optional logo; show it after inactivity.
|
|
1700
1705
|
Props:
|
|
1701
1706
|
- `open`: boolean = false. Shows the screensaver when true; renders nothing when false.
|
|
1702
1707
|
- `locale`: string = "en-GB". BCP 47 locale for the date line.
|
|
1703
1708
|
- `hint`: string. Small hint under the date, e.g. "Tap to continue".
|
|
1709
|
+
- `logo`: string. URL of an image shown above the clock. Use an absolute URL or a root-relative path such as "/brand/logo.svg". Nothing is shown when it is not set, so the screensaver never requests a missing file.
|
|
1704
1710
|
- `idleMs`: number = 0. When greater than 0 and the screensaver is closed, a hidden sentinel listens for pointer, key, wheel and touch activity and calls idle after this many milliseconds without any. 0 disables it.
|
|
1705
1711
|
Events:
|
|
1706
1712
|
- close: Called on click or any key (including Escape) while open.
|
package/dist/llms.txt
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
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, 102 free components and 6 page templates (more in @ziyarago/ui-pro).
|
|
4
4
|
|
|
5
|
-
Version 0.
|
|
5
|
+
Version 0.16.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'
|
|
@@ -170,7 +170,7 @@ Dialogs, sheets and popovers.
|
|
|
170
170
|
- UiImageViewer (Pro): Full-screen lightbox for one or several images with previous/next buttons and a counter.
|
|
171
171
|
- UiTour (Pro): Guided product tour that dims the page, highlights a target element and shows a step popover.
|
|
172
172
|
- UiTourPopover (Pro): Step card of a tour with title, text, step counter and Back/Next buttons; used by Tour and usable standalone.
|
|
173
|
-
- UiScreensaver (Pro): Full-screen idle screen with a large clock, date and
|
|
173
|
+
- UiScreensaver (Pro): Full-screen idle screen with a large clock, date and an optional logo; show it after inactivity.
|
|
174
174
|
|
|
175
175
|
## Navigation
|
|
176
176
|
Menus, tabs and paging.
|
package/dist/manifest.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ziyarago/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"homepage": "https://ziyara.vip",
|
|
5
5
|
"imports": {
|
|
6
6
|
"vue": "import { UiButton } from '@ziyarago/ui/vue'",
|
|
@@ -7834,7 +7834,8 @@
|
|
|
7834
7834
|
},
|
|
7835
7835
|
"notes": [
|
|
7836
7836
|
"Selecting from the list is the only way to set a value: free text is never emitted.",
|
|
7837
|
-
"The list closes on outside click and on Escape."
|
|
7837
|
+
"The list closes on outside click and on Escape.",
|
|
7838
|
+
"Keyboard (WAI-ARIA combobox): the input has role=combobox with aria-expanded, aria-controls and aria-activedescendant; focus stays in the input, Arrow Down and Up open the list and move the active option (wraps, skips disabled), Enter chooses it, Escape closes."
|
|
7838
7839
|
]
|
|
7839
7840
|
},
|
|
7840
7841
|
{
|
|
@@ -8480,6 +8481,10 @@
|
|
|
8480
8481
|
"type": "(date: Date) => boolean",
|
|
8481
8482
|
"description": "Return true to disable a specific day."
|
|
8482
8483
|
},
|
|
8484
|
+
"today": {
|
|
8485
|
+
"type": "Date | string | number",
|
|
8486
|
+
"description": "The day marked as today. By default it is read from the browser clock after mount (nothing is marked in server-rendered HTML), so server and client never disagree near midnight or across time zones. Pass it to mark a day in the HTML too."
|
|
8487
|
+
},
|
|
8483
8488
|
"events": {
|
|
8484
8489
|
"type": "Record<string, string>",
|
|
8485
8490
|
"default": "{}",
|
|
@@ -8502,7 +8507,8 @@
|
|
|
8502
8507
|
"change": "Called with a Date object (local time) for the clicked day."
|
|
8503
8508
|
},
|
|
8504
8509
|
"notes": [
|
|
8505
|
-
"The event payload is a Date, not a string: convert to ISO yourself if the model is a string."
|
|
8510
|
+
"The event payload is a Date, not a string: convert to ISO yourself if the model is a string.",
|
|
8511
|
+
"Keyboard: one tab stop on the day grid; arrows move by day or week, Home and End to the week start and end, PageUp and PageDown change month (Shift: year), Enter or Space picks. Without a selected date the visible month is the current month, which server and client can disagree on right at a month boundary; pass modelValue or today to make it deterministic."
|
|
8506
8512
|
]
|
|
8507
8513
|
},
|
|
8508
8514
|
{
|
|
@@ -8596,6 +8602,11 @@
|
|
|
8596
8602
|
"forwarded": true,
|
|
8597
8603
|
"description": "Return true to disable a specific day, forwarded to the calendar."
|
|
8598
8604
|
},
|
|
8605
|
+
"today": {
|
|
8606
|
+
"type": "Date | string | number",
|
|
8607
|
+
"forwarded": true,
|
|
8608
|
+
"description": "The day marked as today in the calendar; by default read from the browser clock after mount. Forwarded to the calendar."
|
|
8609
|
+
},
|
|
8599
8610
|
"required": {
|
|
8600
8611
|
"type": "boolean",
|
|
8601
8612
|
"default": false,
|
|
@@ -8644,7 +8655,7 @@
|
|
|
8644
8655
|
},
|
|
8645
8656
|
"notes": [
|
|
8646
8657
|
"The event payload is a Date, not a string: convert to ISO yourself if the model is a string.",
|
|
8647
|
-
"The popup closes on outside click and on Escape."
|
|
8658
|
+
"The popup closes on outside click and on Escape (focus returns to the trigger). Arrow Down or Up on the trigger opens it and focus moves into the calendar, which uses the Calendar keys."
|
|
8648
8659
|
]
|
|
8649
8660
|
},
|
|
8650
8661
|
{
|
|
@@ -11378,7 +11389,8 @@
|
|
|
11378
11389
|
"default": "The element the tooltip is attached to."
|
|
11379
11390
|
},
|
|
11380
11391
|
"notes": [
|
|
11381
|
-
"Pure CSS bubble; it cannot contain markup. For rich content use HoverCard."
|
|
11392
|
+
"Pure CSS bubble; it cannot contain markup. For rich content use HoverCard.",
|
|
11393
|
+
"Escape hides the bubble while it is shown (hover or focus) without moving focus; it stays visible while the pointer is over the trigger or the bubble, and returns on the next hover or focus."
|
|
11382
11394
|
]
|
|
11383
11395
|
},
|
|
11384
11396
|
{
|
|
@@ -11688,7 +11700,7 @@
|
|
|
11688
11700
|
"tier": "pro",
|
|
11689
11701
|
"implemented": false,
|
|
11690
11702
|
"package": "@ziyarago/ui-pro",
|
|
11691
|
-
"description": "Full-screen idle screen with a large clock, date and
|
|
11703
|
+
"description": "Full-screen idle screen with a large clock, date and an optional logo; show it after inactivity.",
|
|
11692
11704
|
"props": {
|
|
11693
11705
|
"open": {
|
|
11694
11706
|
"type": "boolean",
|
|
@@ -11704,6 +11716,10 @@
|
|
|
11704
11716
|
"type": "string",
|
|
11705
11717
|
"description": "Small hint under the date, e.g. \"Tap to continue\"."
|
|
11706
11718
|
},
|
|
11719
|
+
"logo": {
|
|
11720
|
+
"type": "string",
|
|
11721
|
+
"description": "URL of an image shown above the clock. Use an absolute URL or a root-relative path such as \"/brand/logo.svg\". Nothing is shown when it is not set, so the screensaver never requests a missing file."
|
|
11722
|
+
},
|
|
11707
11723
|
"idleMs": {
|
|
11708
11724
|
"type": "number",
|
|
11709
11725
|
"default": 0,
|
package/dist/style.css
CHANGED
|
@@ -570,6 +570,13 @@
|
|
|
570
570
|
.zg-sr{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0}
|
|
571
571
|
.zg-select__opt:focus-visible{outline:3px solid var(--zg-brand-soft);outline-offset:-3px;background:var(--zg-s2)}
|
|
572
572
|
.zg-seg button:focus-visible,.zg-tabs button:focus-visible,.zg-code-b pre:focus-visible{outline:3px solid var(--zg-brand-soft);outline-offset:2px}
|
|
573
|
+
.zg-select__opt[data-active],.zg-select__opt[data-active]:hover{background:var(--zg-s2);outline:3px solid var(--zg-brand-soft);outline-offset:-3px}
|
|
574
|
+
.zg-tip:hover::after,.zg-tip:focus-visible::after{pointer-events:auto}
|
|
575
|
+
.zg-tip:hover::before{content:"";position:absolute;left:0;right:0;bottom:100%;height:10px}
|
|
576
|
+
.zg-tip--bottom:hover::before{bottom:auto;top:100%}
|
|
577
|
+
.zg-tip--left:hover::before,.zg-tip--right:hover::before{top:0;bottom:0;height:auto;width:10px}
|
|
578
|
+
.zg-tip--left:hover::before{left:auto;right:100%}.zg-tip--right:hover::before{right:auto;left:100%}
|
|
579
|
+
.zg-tip[data-tip-off]::after{opacity:0!important;pointer-events:none!important}.zg-tip[data-tip-off]::before{display:none}
|
|
573
580
|
|
|
574
581
|
|
|
575
582
|
/* ./sizes/typography.css */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ziyarago/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
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, 102 free components and 6 page templates (more in @ziyarago/ui-pro).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ui",
|
package/src/core/components2.js
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
// @ziyarago/ui · components, part 2 — Forms, Surfaces, Actions, Data, Navigation, Layout, CRM, Hotels.
|
|
2
2
|
import { icon, cx, ddRef, pick, INPUT_ATTRS } from './components.js';
|
|
3
3
|
import { trap } from './dialog.js';
|
|
4
|
-
import { listKeys, openKeys, tabIdx, toTrigger, trigger } from './keys.js';
|
|
4
|
+
import { listKeys, openKeys, tabIdx, toTrigger, trigger, calKeys, dateKey } from './keys.js';
|
|
5
5
|
const tipId = s => { let x = 5381; for (const c of String(s)) x = (x * 33 ^ c.charCodeAt(0)) >>> 0; return 'zg-tip-' + x.toString(36); };
|
|
6
|
-
let tipN = 0;
|
|
6
|
+
let tipN = 0, cbN = 0;
|
|
7
|
+
const tipScroll = el => { if (el && el.scrollIntoView) el.scrollIntoView({ block: 'nearest' }); };
|
|
7
8
|
// Same text twice on one page would repeat the id: the later tooltip gets a unique one (browser only, after mount).
|
|
8
9
|
const tipRef = el => { const s = el && el.lastElementChild; if (!s || !s.id || typeof document === 'undefined') return; const same = document.querySelectorAll('[id="' + s.id + '"]'); if (same.length > 1 && same[0] !== s) { s.id += '-' + (++tipN); el.setAttribute('aria-describedby', s.id); } };
|
|
10
|
+
// WCAG 1.4.13: while the tip is shown (hover or keyboard focus) Escape hides it without moving focus; it comes back on the next hover or focus.
|
|
11
|
+
const tipEsc = el => ev => { if (ev.key === 'Escape') el.setAttribute('data-tip-off', ''); };
|
|
12
|
+
const tipShow = e => { const el = e.currentTarget; if (e.type === 'mouseenter' || e.type === 'focus') el.removeAttribute('data-tip-off'); if (!el._zgTip) { el._zgTip = tipEsc(el); document.addEventListener('keydown', el._zgTip); } };
|
|
13
|
+
const tipHide = (e, still) => { const el = e.currentTarget; if (el.matches(still)) return; if (el._zgTip) document.removeEventListener('keydown', el._zgTip); el._zgTip = null; el.removeAttribute('data-tip-off'); };
|
|
9
14
|
import { t, getUiLocale, intlLocale } from '../i18n/index.js';
|
|
10
15
|
const dloc = p => p.locale || (getUiLocale() === 'en' ? 'en-GB' : intlLocale());
|
|
11
16
|
const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
|
|
@@ -35,7 +40,7 @@ export const more = {
|
|
|
35
40
|
/* ── Actions ── */
|
|
36
41
|
Dropdown: { state: { open: false }, render: ({ h, props: p, state, set }) => h('div', { class: 'zg-dd' }, h('button', { type: 'button', class: cx('zg-btn', p.size && 'zg-btn--' + p.size), 'aria-haspopup': 'menu', 'aria-expanded': String(state.open && !p.disabled), disabled: p.disabled, onClick: () => set({ open: !state.open }), onKeydown: openKeys(state.open && !p.disabled, () => set({ open: true })) }, p.icon ? icon(h, p.icon, 16) : null, p.label, icon(h, 'chevron-down', 14)), state.open && !p.disabled ? h('div', { class: 'zg-menu', role: 'menu', 'data-zg-dd': '', ref: ddRef(() => set({ open: false }), { focus: true }), onKeydown: listKeys('[role=menuitem]', { wrap: true, onTab: e => { const t = trigger(e.currentTarget); t && t.focus(); set({ open: false }); } }) }, ...(p.items || []).map((it, i, all) => it.separator ? h('div', { key: i, class: 'zg-menu__sep', role: 'separator' }) : h('button', { key: i, type: 'button', role: 'menuitem', tabIndex: tabIdx(all.map(x => ({ disabled: x.separator || x.disabled })), () => false)(i), disabled: it.disabled, 'aria-disabled': it.disabled ? 'true' : undefined, class: cx('zg-menu__i', it.tone === 'danger' && 'zg-menu__i--danger'), onClick: e => { toTrigger(e); set({ open: false }); it.onClick && it.onClick(it); p.onSelect && p.onSelect(it.value); } }, it.icon ? icon(h, it.icon, 16) : null, it.label, it.shortcut ? h('span', { class: 'zg-kbd' }, it.shortcut) : null))) : null) },
|
|
37
42
|
Menu: { render: ({ h, props: p }) => p.open === false ? null : h('div', { class: 'zg-menu zg-menu--static', role: 'menu', 'data-zg-dd': '', ref: p.open === true && p.onClose ? ddRef(p.onClose, { self: true, focus: true }) : undefined, onKeydown: listKeys('[role=menuitem]', { wrap: true, onTab: () => p.onClose && p.onClose() }) }, ...(p.items || []).map((it, i, all) => it.separator ? h('div', { key: i, class: 'zg-menu__sep', role: 'separator' }) : h('button', { key: i, type: 'button', role: 'menuitem', tabIndex: tabIdx(all.map(x => ({ disabled: x.separator || x.disabled })), () => false)(i), disabled: it.disabled, 'aria-disabled': it.disabled ? 'true' : undefined, class: cx('zg-menu__i', it.tone === 'danger' && 'zg-menu__i--danger'), onClick: () => { it.onClick && it.onClick(it); p.onSelect && p.onSelect(it.value); } }, it.icon ? icon(h, it.icon, 16) : null, it.label, it.shortcut ? h('span', { class: 'zg-kbd' }, it.shortcut) : null))) },
|
|
38
|
-
Tooltip: { render: ({ h, props: p, children }) => h('span', { class: cx('zg-tip', p.placement && p.placement !== 'top' && 'zg-tip--' + p.placement), 'data-tip': p.text, tabIndex: 0, 'aria-describedby': p.text ? tipId(p.text) : undefined, ref: p.text ? tipRef : undefined }, ...children, p.text ? h('span', { id: tipId(p.text), role: 'tooltip', class: 'zg-sr' }, p.text) : null) },
|
|
43
|
+
Tooltip: { render: ({ h, props: p, children }) => h('span', { class: cx('zg-tip', p.placement && p.placement !== 'top' && 'zg-tip--' + p.placement), 'data-tip': p.text, tabIndex: 0, 'aria-describedby': p.text ? tipId(p.text) : undefined, ref: p.text ? tipRef : undefined, onMouseenter: tipShow, onFocus: tipShow, onMouseleave: e => tipHide(e, ':focus-visible'), onBlur: e => tipHide(e, ':hover') }, ...children, p.text ? h('span', { id: tipId(p.text), role: 'tooltip', class: 'zg-sr' }, p.text) : null) },
|
|
39
44
|
Popover: { state: { open: false }, render: ({ C, h, props: p, state, set, children }) => h('span', { class: 'zg-dd' }, h(C.Button, { size: p.size, onClick: () => set({ open: !state.open }) }, p.label), state.open ? h('div', { class: 'zg-pop zg-glass', 'data-zg-dd': '', ref: ddRef(() => set({ open: false })) }, ...children) : null) },
|
|
40
45
|
CopyButton: { state: { done: false, err: false }, render: ({ C, h, props: p, state, set }) => { const reset = () => setTimeout(() => set({ done: false, err: false }), 1500), fail = e => { set({ done: false, err: true }); p.onError && p.onError(e); reset(); }, ok = () => { set({ done: true, err: false }); p.onCopy && p.onCopy(p.value); reset(); };
|
|
41
46
|
const msg = state.err ? t('CopyButton', 'failed') : state.done ? t('CopyButton', 'copied') : '';
|
|
@@ -62,21 +67,28 @@ export const more = {
|
|
|
62
67
|
FileUpload: { state: { drag: false }, render: ({ h, props: p, state, set }) => { const ac = String(p.accept || '').split(',').map(s => s.trim().toLowerCase()).filter(Boolean), okType = f => !ac.length || ac.some(a => a[0] === '.' ? f.name.toLowerCase().endsWith(a) : a.endsWith('/*') ? (f.type || '').toLowerCase().startsWith(a.slice(0, -1)) : (f.type || '').toLowerCase() === a), rej = (f, r) => p.onReject && p.onReject(f, r),
|
|
63
68
|
take = list => { const ok = []; for (const f of list) { if (!okType(f)) rej(f, 'type'); else if (p.maxSize && f.size > p.maxSize) rej(f, 'size'); else ok.push(f); } if (!p.multiple) ok.slice(1).forEach(f => rej(f, 'count')); const out = p.multiple ? ok : ok.slice(0, 1); if (out.length && p.onChange) p.onChange(out); }, pick = e => { const i = e.currentTarget.querySelector('input'); if (i) i.click(); };
|
|
64
69
|
return h('div', { class: 'zg-field' }, p.label ? h('span', { class: 'zg-label' }, p.label) : null, h('div', { class: 'zg-fdrop', role: 'button', tabIndex: 0, 'aria-label': p.title || t('FileUpload', 'title'), 'data-drag': state.drag ? '' : undefined, onClick: pick, onKeydown: e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); pick(e); } }, onDragenter: e => { e.preventDefault(); if (!state.drag) set({ drag: true }); }, onDragover: e => { e.preventDefault(); if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy'; if (!state.drag) set({ drag: true }); }, onDragleave: e => { if (!e.currentTarget.contains(e.relatedTarget)) set({ drag: false }); }, onDrop: e => { e.preventDefault(); set({ drag: false }); take([...(e.dataTransfer ? e.dataTransfer.files : [])]); } }, icon(h, p.image ? 'image-up' : 'upload', 24), h('b', null, p.title || t('FileUpload', 'title')), h('span', { class: 'zg-hint' }, p.hint || t('FileUpload', 'hint')), h('input', { type: 'file', multiple: p.multiple, accept: p.accept, tabIndex: -1, 'aria-hidden': 'true', style: { display: 'none' }, onClick: e => e.stopPropagation(), onChange: e => { take([...e.target.files]); e.target.value = ''; } })), p.error ? h('span', { class: 'zg-hint zg-hint--error', role: 'alert' }, p.error) : null); } },
|
|
65
|
-
Combobox: { state: { open: false, q: '' }, render: ({ h, props: p, state, set }) => { const
|
|
66
|
-
|
|
67
|
-
|
|
70
|
+
Combobox: { state: { open: false, q: '', ai: -1, id: '' }, render: ({ h, props: p, state, set }) => { const all = p.options || [], cur = all.find(o => o.value === p.modelValue), f = all.filter(o => !state.q || (p.filter ? p.filter(o, state.q) : o.label.toLowerCase().includes(state.q.toLowerCase()))), open = state.open && !p.disabled, id = state.id, oid = i => id + '-o' + i, lid = id + '-l';
|
|
71
|
+
const op = o => set({ open: true, id: id || 'zg-cb' + (++cbN), ...o }), pickO = o => { set({ open: false, q: '', ai: -1 }); p.onChange && p.onChange(o.value); };
|
|
72
|
+
const step = d => { const ok = f.map((o, i) => i).filter(i => !f[i].disabled); if (!ok.length) return; const k = ok.indexOf(state.ai); op({ ai: ok[k < 0 ? (d > 0 ? 0 : ok.length - 1) : (k + d + ok.length) % ok.length] }); };
|
|
73
|
+
const keys = e => { if (e.altKey || e.ctrlKey || e.metaKey || p.disabled) return;
|
|
74
|
+
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { e.preventDefault(); if (!open) { const ok = f.map((o, i) => i).filter(i => !f[i].disabled), i = f.findIndex(o => o.value === p.modelValue && !o.disabled); op({ q: '', ai: i >= 0 ? i : ok.length ? (e.key === 'ArrowDown' ? ok[0] : ok[ok.length - 1]) : -1 }); } else step(e.key === 'ArrowDown' ? 1 : -1); }
|
|
75
|
+
else if (e.key === 'Enter' && open && f[state.ai] && !f[state.ai].disabled) { e.preventDefault(); pickO(f[state.ai]); } };
|
|
76
|
+
return lab(h, p, h('div', { class: 'zg-select' }, field(h, p, icon(h, 'search', 16), h('input', { role: 'combobox', 'aria-autocomplete': 'list', 'aria-haspopup': 'listbox', 'aria-expanded': String(open), 'aria-controls': open ? lid : undefined, 'aria-activedescendant': open && f[state.ai] ? oid(state.ai) : undefined, autoComplete: 'off', value: open ? state.q : (cur ? cur.label : ''), disabled: p.disabled, placeholder: p.placeholder || t('Combobox', 'placeholder'), onFocus: () => op({ q: '', ai: -1 }), onKeydown: keys, onInput: e => op({ q: e.target.value, ai: -1 }) }), [p.clearable && cur && !p.disabled ? h('button', { key: 'x', type: 'button', class: 'zg-ibtn zg-ibtn--sm zg-ibtn--ghost', 'aria-label': t('Combobox', 'clear'), onMouseDown: e => e.preventDefault(), onClick: () => { set({ open: false, q: '', ai: -1 }); p.onChange && p.onChange(null); } }, icon(h, 'x', 14)) : null, icon(h, 'chevron-down', 16)]),
|
|
77
|
+
open ? h('div', { id: lid, class: 'zg-select__list', role: 'listbox', 'aria-label': p.label || p.placeholder, 'data-zg-dd': '', ref: ddRef(() => set({ open: false, q: '', ai: -1 })) }, ...(f.length ? f.map((o, i) => h('button', { key: o.value, id: oid(i), type: 'button', role: 'option', tabIndex: -1, class: 'zg-select__opt', 'data-active': i === state.ai ? '' : undefined, ref: i === state.ai ? tipScroll : undefined, disabled: o.disabled, 'aria-disabled': o.disabled ? 'true' : undefined, 'aria-selected': String(o.value === p.modelValue), onMouseDown: e => e.preventDefault(), onClick: () => pickO(o) }, o.label)) : [h('div', { key: 'e', class: 'zg-empty' }, p.emptyText || '—')])) : null)); } },
|
|
68
78
|
MultiSelect: { state: { open: false }, render: ({ h, props: p, state, set }) => { const v = p.modelValue || [], tog = x => p.onChange && p.onChange(v.includes(x) ? v.filter(i => i !== x) : [...v, x]);
|
|
69
|
-
return h('div', { class: 'zg-field' }, p.label ? h('span', { class: 'zg-label' }, p.label) : null, h('div', { class: 'zg-select' }, h('button', { type: 'button', class: 'zg-btn zg-select__trigger', 'aria-haspopup': 'listbox', 'aria-expanded': String(state.open && !p.disabled), disabled: p.disabled, style: { height: 'var(--zg-h-lg)' }, onClick: () => set({ open: !state.open }) }, h('span', null, v.length ? t('MultiSelect', 'selected', { n: v.length }) : (p.placeholder || '—')), icon(h, 'chevron-down', 16)),
|
|
70
|
-
state.open && !p.disabled ? h('div', { class: 'zg-select__list', role: 'listbox', 'aria-label': p.label || p.placeholder, 'aria-multiselectable': 'true', 'data-zg-dd': '', ref: ddRef(() => set({ open: false })) }, ...(p.options || []).map(o => h('button', { key: o.value, type: 'button', role: 'option', class: 'zg-select__opt', disabled: o.disabled, 'aria-disabled': o.disabled ? 'true' : undefined, 'aria-selected': String(v.includes(o.value)), onClick: () => tog(o.value) }, h('span', { class: 'zg-check__box', style: { width: '18px', height: '18px', borderRadius: '6px', background: v.includes(o.value) ? 'var(--zg-brand)' : 'transparent' } }, v.includes(o.value) ? icon(h, 'check', 12) : null), h('span', { style: { flex: 1, marginInlineStart: '10px' } }, o.label)))) : null)); } },
|
|
79
|
+
return h('div', { class: 'zg-field' }, p.label ? h('span', { class: 'zg-label' }, p.label) : null, h('div', { class: 'zg-select' }, h('button', { type: 'button', class: 'zg-btn zg-select__trigger', 'aria-haspopup': 'listbox', 'aria-expanded': String(state.open && !p.disabled), disabled: p.disabled, style: { height: 'var(--zg-h-lg)' }, onClick: () => set({ open: !state.open }), onKeydown: openKeys(state.open && !p.disabled, () => set({ open: true })) }, h('span', null, v.length ? t('MultiSelect', 'selected', { n: v.length }) : (p.placeholder || '—')), icon(h, 'chevron-down', 16)),
|
|
80
|
+
state.open && !p.disabled ? h('div', { class: 'zg-select__list', role: 'listbox', 'aria-label': p.label || p.placeholder, 'aria-multiselectable': 'true', 'data-zg-dd': '', ref: ddRef(() => set({ open: false }), { focus: true }), onKeydown: listKeys('[role=option]', { onTab: e => { const t = trigger(e.currentTarget); t && t.focus(); set({ open: false }); } }) }, ...(p.options || []).map((o, i, all) => h('button', { key: o.value, type: 'button', role: 'option', tabIndex: tabIdx(all, x => v.includes(x.value))(i), class: 'zg-select__opt', disabled: o.disabled, 'aria-disabled': o.disabled ? 'true' : undefined, 'aria-selected': String(v.includes(o.value)), onClick: () => tog(o.value) }, h('span', { class: 'zg-check__box', style: { width: '18px', height: '18px', borderRadius: '6px', background: v.includes(o.value) ? 'var(--zg-brand)' : 'transparent' } }, v.includes(o.value) ? icon(h, 'check', 12) : null), h('span', { style: { flex: 1, marginInlineStart: '10px' } }, o.label)))) : null)); } },
|
|
71
81
|
PhoneInput: { render: ({ C, h, props: p }) => { const CC = p.countries || [{ value: '+966', label: 'SA +966' }, { value: '+998', label: 'UZ +998' }, { value: '+7', label: 'RU +7' }, { value: '+90', label: 'TR +90' }, { value: '+20', label: 'EG +20' }], v = p.modelValue || {};
|
|
72
82
|
return h('div', { class: 'zg-field' }, p.label ? h('span', { class: 'zg-label' }, p.label) : null, h('div', { style: { display: 'grid', gridTemplateColumns: '130px minmax(0,1fr)', gap: '8px', alignItems: 'start' } }, h(C.Select, { options: CC, modelValue: v.code || CC[0].value, onChange: c => p.onChange && p.onChange({ ...v, code: c }) }), field(h, p, icon(h, 'phone', 16), h('input', { type: 'tel', inputMode: 'tel', value: v.number || '', placeholder: p.placeholder ?? '90 123 45 67', onInput: e => p.onChange && p.onChange({ ...v, number: e.target.value }) }))), typeof p.error === 'string' && p.error ? h('span', { class: 'zg-hint zg-hint--error', role: 'alert' }, p.error) : p.hint ? h('span', { class: 'zg-hint' }, p.hint) : null); } },
|
|
73
|
-
Calendar: { state: { ym: null }, render: ({ h, props: p, state, set }) => { const s = pd(p.modelValue), b = state.ym ? new Date(state.ym) : (s || new Date()), y = b.getFullYear(), m = b.getMonth(), loc = dloc(p), ws = p.weekStartsOn ?? 1, off = (new Date(y, m, 1).getDay() - ws + 7) % 7, dn = new Date(y, m + 1, 0).getDate(), today = new
|
|
83
|
+
Calendar: { state: { ym: null, fd: null, today: null }, render: ({ h, props: p, state, set }) => { const s = pd(p.modelValue), now = pd(p.today) || (state.today ? new Date(state.today) : null), b = state.ym ? new Date(state.ym) : (s || now || new Date()), y = b.getFullYear(), m = b.getMonth(), loc = dloc(p), ws = p.weekStartsOn ?? 1, off = (new Date(y, m, 1).getDay() - ws + 7) % 7, dn = new Date(y, m + 1, 0).getDate(), today = now, ml = new Intl.DateTimeFormat(loc, { month: 'long', year: 'numeric' }).format(b);
|
|
74
84
|
const wd = Array.from({ length: 7 }, (_, i) => new Intl.DateTimeFormat(loc, { weekday: 'narrow' }).format(new Date(2023, 0, 1 + ((i + ws) % 7))));
|
|
75
|
-
const same = (a, d) => a && a.getFullYear() === y && a.getMonth() === m && a.getDate() === d, ev = p.events || {}, lo = pd(p.min), hi = pd(p.max), day0 = x => new Date(x.getFullYear(), x.getMonth(), x.getDate()),
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
85
|
+
const same = (a, d) => a && a.getFullYear() === y && a.getMonth() === m && a.getDate() === d, ev = p.events || {}, lo = pd(p.min), hi = pd(p.max), day0 = x => new Date(x.getFullYear(), x.getMonth(), x.getDate()), blockedAt = t => !!((lo && t < day0(lo)) || (hi && t > day0(hi)) || (p.disabledDate && p.disabledDate(t))), blocked = d => blockedAt(new Date(y, m, d));
|
|
86
|
+
const mk = d => dateKey(new Date(y, m, d)), fk = state.fd && state.fd.slice(0, 7) === mk(1).slice(0, 7) ? +state.fd.slice(8) : 0;
|
|
87
|
+
const fd = [fk, same(s, s && s.getDate()) ? s.getDate() : 0, same(today, today && today.getDate()) ? today.getDate() : 0, 1].find(d => d && !blocked(d)) || Array.from({ length: dn }, (_, i) => i + 1).find(d => !blocked(d));
|
|
88
|
+
return h('div', { class: 'zg-cal', ref: p.today == null && state.today == null ? (el => { if (el) Promise.resolve().then(() => set({ today: Date.now() })); }) : undefined }, h('div', { class: 'zg-cal__h' }, h('button', { type: 'button', class: 'zg-ibtn zg-ibtn--sm zg-ibtn--ghost', 'aria-label': t('Calendar', 'prev'), onClick: () => set({ ym: new Date(y, m - 1, 1).getTime() }) }, icon(h, 'chevron-left', 16)), h('b', { 'aria-live': 'polite' }, ml), h('button', { type: 'button', class: 'zg-ibtn zg-ibtn--sm zg-ibtn--ghost', 'aria-label': t('Calendar', 'next'), onClick: () => set({ ym: new Date(y, m + 1, 1).getTime() }) }, icon(h, 'chevron-right', 16))),
|
|
89
|
+
h('div', { class: 'zg-cal__g', role: 'group', 'aria-label': ml, onKeydown: calKeys({ ws, blocked: blockedAt, show: (t, k) => set({ ym: new Date(t.getFullYear(), t.getMonth(), 1).getTime(), fd: k }) }) }, ...wd.map((d, i) => h('span', { key: 'w' + i, class: 'zg-cal__w' }, d)), ...Array.from({ length: off }, (_, i) => h('span', { key: 'o' + i })), ...Array.from({ length: dn }, (_, i) => { const d = i + 1, key = mk(d); return h('button', { key, type: 'button', class: 'zg-cal__d', tabIndex: d === fd ? 0 : -1, 'data-date': key, 'aria-pressed': String(!!same(s, d)), 'aria-current': same(today, d) ? 'date' : undefined, 'data-today': String(!!same(today, d)), 'data-ev': ev[key] ? '' : undefined, title: ev[key] || undefined, disabled: blocked(d) || undefined, onFocus: () => state.fd !== key && set({ fd: key }), onClick: e => { if (blocked(d)) return; if (e.currentTarget.closest('[data-zg-pick]')) toTrigger(e); p.onChange && p.onChange(new Date(y, m, d)); } }, d); }))); } },
|
|
90
|
+
DatePicker: { state: { open: false }, render: ({ C, h, props: p, state, set }) => lab(h, p, h('div', { class: 'zg-select' }, h('button', { type: 'button', class: 'zg-btn zg-select__trigger', 'aria-haspopup': 'dialog', 'aria-expanded': String(state.open && !p.disabled), disabled: p.disabled, style: { height: 'var(--zg-h-lg)' }, onClick: () => set({ open: !state.open }), onKeydown: openKeys(state.open && !p.disabled, () => set({ open: true })) }, h('span', { style: { display: 'flex', gap: '10px', alignItems: 'center' } }, icon(h, 'calendar', 16), p.modelValue ? new Intl.DateTimeFormat(dloc(p), { dateStyle: 'medium' }).format(pd(p.modelValue) || new Date()) : (p.placeholder || t('DatePicker', 'placeholder'))), h('span', { style: { display: 'flex', alignItems: 'center', gap: '6px' } }, p.clearable && p.modelValue && !p.disabled ? h('span', { role: 'button', tabIndex: 0, class: 'zg-ibtn zg-ibtn--sm zg-ibtn--ghost', 'aria-label': t('DatePicker', 'clear'), onClick: e => { e.stopPropagation(); p.onChange && p.onChange(null); }, onKeydown: e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); e.stopPropagation(); p.onChange && p.onChange(null); } } }, icon(h, 'x', 14)) : null, icon(h, 'chevron-down', 16))),
|
|
91
|
+
state.open && !p.disabled ? h('div', { class: 'zg-select__list', role: 'dialog', 'aria-label': p.label || p.placeholder || t('DatePicker', 'placeholder'), 'data-zg-dd': '', 'data-zg-pick': '', ref: ddRef(() => set({ open: false }), { focus: true }), style: { padding: '12px' }, onFocusout: e => { const r = e.currentTarget.parentElement; if (e.relatedTarget && r && !r.contains(e.relatedTarget)) set({ open: false }); } }, h(C.Calendar, { modelValue: p.modelValue, locale: p.locale, weekStartsOn: p.weekStartsOn, min: p.min, max: p.max, today: p.today, disabledDate: p.disabledDate, onChange: d => { set({ open: false }); p.onChange && p.onChange(d); } })) : null)) },
|
|
80
92
|
TimePicker: { render: ({ C, h, props: p }) => { const [H0, M0] = (p.modelValue || '09:00').split(':'), H = String(H0 ?? '00').padStart(2, '0'), M = String(M0 ?? '00').padStart(2, '0'); const hs = Array.from({ length: 24 }, (_, i) => { const v = String(i).padStart(2, '0'); return { value: v, label: v }; }), st = Math.max(1, Math.min(60, Math.floor(+p.minuteStep) || 15)), mv = Array.from({ length: Math.ceil(60 / st) }, (_, i) => String(i * st).padStart(2, '0')); if (!mv.includes(M)) { mv.push(M); mv.sort(); } const ms = mv.map(v => ({ value: v, label: v }));
|
|
81
93
|
return h('div', { class: 'zg-field' }, p.label ? h('span', { class: 'zg-label' }, p.label) : null, h('div', { style: { display: 'flex', gap: '8px', alignItems: 'center' } }, h('div', { style: { flex: 1 } }, h(C.Select, { options: hs, disabled: p.disabled, modelValue: H, onChange: v => p.onChange && p.onChange(v + ':' + M) })), h('b', null, ':'), h('div', { style: { flex: 1 } }, h(C.Select, { options: ms, disabled: p.disabled, modelValue: M, onChange: v => p.onChange && p.onChange(H + ':' + v) })))); } },
|
|
82
94
|
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) },
|
package/src/core/keys.js
CHANGED
|
@@ -53,3 +53,40 @@ export const openKeys = (isOpen, open) => e => { if (!isOpen && !mod(e) && (e.ke
|
|
|
53
53
|
|
|
54
54
|
/** Focus the entry point of a freshly opened popup: the item with tabindex 0, else the first one. */
|
|
55
55
|
export const focusIn = el => { const t = el.querySelector('[tabindex="0"]') || el.querySelector('button:not([disabled])'); if (t && t.focus) t.focus({ preventScroll: true }); };
|
|
56
|
+
|
|
57
|
+
const pad2 = n => String(n).padStart(2, '0');
|
|
58
|
+
/** YYYY-MM-DD of a local date, the value of a day cell's data-date attribute. */
|
|
59
|
+
export const dateKey = d => d.getFullYear() + '-' + pad2(d.getMonth() + 1) + '-' + pad2(d.getDate());
|
|
60
|
+
/**
|
|
61
|
+
* Month grid of day buttons carrying data-date="YYYY-MM-DD" and a roving tabindex. Put it on the grid element.
|
|
62
|
+
* Arrows move one day (Left/Right mirrored in RTL) or one week, Home/End jump to the start/end of the week, PageUp/PageDown change month (Shift: year).
|
|
63
|
+
* Blocked days are skipped. When the target day is in another month, show(date, key) must make that month visible; the cell is focused once it renders.
|
|
64
|
+
*/
|
|
65
|
+
export const calKeys = o => e => {
|
|
66
|
+
if (mod(e)) return;
|
|
67
|
+
const root = e.currentTarget, cell = e.target.closest && e.target.closest('[data-date]');
|
|
68
|
+
if (!cell) return;
|
|
69
|
+
const [Y, M, D] = cell.getAttribute('data-date').split('-').map(Number), ws = o.ws ?? 1, rtl = isRtl(root), day = (n) => new Date(Y, M - 1, D + n);
|
|
70
|
+
const month = n => { const f = new Date(Y, M - 1 + n, 1); return new Date(f.getFullYear(), f.getMonth(), Math.min(D, new Date(f.getFullYear(), f.getMonth() + 1, 0).getDate())); };
|
|
71
|
+
const wk = (new Date(Y, M - 1, D).getDay() - ws + 7) % 7;
|
|
72
|
+
let t, unit = 1;
|
|
73
|
+
switch (e.key) {
|
|
74
|
+
case 'ArrowRight': t = day(rtl ? -1 : 1); unit = rtl ? -1 : 1; break;
|
|
75
|
+
case 'ArrowLeft': t = day(rtl ? 1 : -1); unit = rtl ? 1 : -1; break;
|
|
76
|
+
case 'ArrowDown': t = day(7); unit = 7; break;
|
|
77
|
+
case 'ArrowUp': t = day(-7); unit = -7; break;
|
|
78
|
+
case 'Home': t = day(-wk); break;
|
|
79
|
+
case 'End': t = day(6 - wk); unit = -1; break;
|
|
80
|
+
case 'PageUp': t = month(e.shiftKey ? -12 : -1); break;
|
|
81
|
+
case 'PageDown': t = month(e.shiftKey ? 12 : 1); break;
|
|
82
|
+
default: return;
|
|
83
|
+
}
|
|
84
|
+
e.preventDefault();
|
|
85
|
+
for (let i = 0; i < 62 && o.blocked && o.blocked(t); i++) t = new Date(t.getFullYear(), t.getMonth(), t.getDate() + unit);
|
|
86
|
+
if (o.blocked && o.blocked(t)) return;
|
|
87
|
+
const k = dateKey(t), find = () => root.querySelector('[data-date="' + k + '"]:not(:disabled)');
|
|
88
|
+
const el = find();
|
|
89
|
+
if (el) { el.focus(); return; }
|
|
90
|
+
o.show(t, k);
|
|
91
|
+
let n = 0; const tick = () => { const x = find(); if (x) x.focus(); else if (++n < 12) setTimeout(tick, 16); }; setTimeout(tick, 0);
|
|
92
|
+
};
|
package/src/internal.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// It re-exports the helpers the component definitions are built from, so a plugin does not copy them.
|
|
3
3
|
export { cx, pick, INPUT_ATTRS, DZ, dzp, dzf, dzb, ddRef, icon, fmt } from './core/components.js';
|
|
4
4
|
export { trap, life } from './core/dialog.js';
|
|
5
|
-
export { rove, tabIdx, listKeys, openKeys, focusIn, toTrigger, trigger } from './core/keys.js';
|
|
5
|
+
export { rove, tabIdx, listKeys, openKeys, focusIn, toTrigger, trigger, calKeys, dateKey } from './core/keys.js';
|
|
6
6
|
export { rules } from './core/components4.js';
|
|
7
7
|
export { page } from './core/pages.js';
|
|
8
8
|
export { t, getUiLocale, intlLocale, defineMessages } from './i18n/index.js';
|
package/src/meta/forms.js
CHANGED
|
@@ -207,7 +207,7 @@ export default {
|
|
|
207
207
|
size: size(),
|
|
208
208
|
},
|
|
209
209
|
events: change('Called with the value of the chosen option.'),
|
|
210
|
-
notes: ['Selecting from the list is the only way to set a value: free text is never emitted.', 'The list closes on outside click and on Escape.'],
|
|
210
|
+
notes: ['Selecting from the list is the only way to set a value: free text is never emitted.', 'The list closes on outside click and on Escape.', 'Keyboard (WAI-ARIA combobox): the input has role=combobox with aria-expanded, aria-controls and aria-activedescendant; focus stays in the input, Arrow Down and Up open the list and move the active option (wraps, skips disabled), Enter chooses it, Escape closes.'],
|
|
211
211
|
},
|
|
212
212
|
TreeSelect: {
|
|
213
213
|
description: 'Dropdown with a searchable tree. Use for hierarchical choices such as regions, categories or org units.',
|
|
@@ -359,11 +359,12 @@ export default {
|
|
|
359
359
|
min: { type: 'Date | string | number', description: 'Earliest selectable day; earlier days are disabled.' },
|
|
360
360
|
max: { type: 'Date | string | number', description: 'Latest selectable day; later days are disabled.' },
|
|
361
361
|
disabledDate: { type: '(date: Date) => boolean', description: 'Return true to disable a specific day.' },
|
|
362
|
+
today: { type: 'Date | string | number', description: 'The day marked as today. By default it is read from the browser clock after mount (nothing is marked in server-rendered HTML), so server and client never disagree near midnight or across time zones. Pass it to mark a day in the HTML too.' },
|
|
362
363
|
events: { type: 'Record<string, string>', default: '{}', description: 'Marked days: keys are YYYY-MM-DD, values are the tooltip text.' },
|
|
363
364
|
size: size('Day cell and header size.'),
|
|
364
365
|
},
|
|
365
366
|
events: change('Called with a Date object (local time) for the clicked day.'),
|
|
366
|
-
notes: ['The event payload is a Date, not a string: convert to ISO yourself if the model is a string.'],
|
|
367
|
+
notes: ['The event payload is a Date, not a string: convert to ISO yourself if the model is a string.', 'Keyboard: one tab stop on the day grid; arrows move by day or week, Home and End to the week start and end, PageUp and PageDown change month (Shift: year), Enter or Space picks. Without a selected date the visible month is the current month, which server and client can disagree on right at a month boundary; pass modelValue or today to make it deterministic.'],
|
|
367
368
|
},
|
|
368
369
|
RangeCalendar: {
|
|
369
370
|
description: 'Month grid for picking a date range with hover preview. Use for stays, trips and report periods.',
|
|
@@ -387,6 +388,7 @@ export default {
|
|
|
387
388
|
min: { type: 'Date | string | number', forwarded: true, description: 'Earliest selectable day, forwarded to the calendar.' },
|
|
388
389
|
max: { type: 'Date | string | number', forwarded: true, description: 'Latest selectable day, forwarded to the calendar.' },
|
|
389
390
|
disabledDate: { type: '(date: Date) => boolean', forwarded: true, description: 'Return true to disable a specific day, forwarded to the calendar.' },
|
|
391
|
+
today: { type: 'Date | string | number', forwarded: true, description: 'The day marked as today in the calendar; by default read from the browser clock after mount. Forwarded to the calendar.' },
|
|
390
392
|
required: { type: 'boolean', default: false, forwarded: true, description: 'Adds a red asterisk to the label. Does not validate.' },
|
|
391
393
|
label, hint, error,
|
|
392
394
|
placeholder: placeholder('Shown when no date is set. Defaults to "Select date".'),
|
|
@@ -394,7 +396,7 @@ export default {
|
|
|
394
396
|
size: size(),
|
|
395
397
|
},
|
|
396
398
|
events: change('Called with a Date object (local midnight) for the chosen day. The popup closes after picking.'),
|
|
397
|
-
notes: ['The event payload is a Date, not a string: convert to ISO yourself if the model is a string.', 'The popup closes on outside click and on Escape.'],
|
|
399
|
+
notes: ['The event payload is a Date, not a string: convert to ISO yourself if the model is a string.', 'The popup closes on outside click and on Escape (focus returns to the trigger). Arrow Down or Up on the trigger opens it and focus moves into the calendar, which uses the Calendar keys.'],
|
|
398
400
|
},
|
|
399
401
|
TimePicker: {
|
|
400
402
|
description: 'Hour and minute selects for a time of day. Use for opening hours and appointment times.',
|
package/src/meta/overlay.js
CHANGED
|
@@ -155,7 +155,7 @@ export default {
|
|
|
155
155
|
size: size('bubble padding and font'),
|
|
156
156
|
},
|
|
157
157
|
slots: { default: 'The element the tooltip is attached to.' },
|
|
158
|
-
notes: ['Pure CSS bubble; it cannot contain markup. For rich content use HoverCard.'],
|
|
158
|
+
notes: ['Pure CSS bubble; it cannot contain markup. For rich content use HoverCard.', 'Escape hides the bubble while it is shown (hover or focus) without moving focus; it stays visible while the pointer is over the trigger or the bubble, and returns on the next hover or focus.'],
|
|
159
159
|
},
|
|
160
160
|
|
|
161
161
|
Dropdown: {
|
|
@@ -257,11 +257,12 @@ export default {
|
|
|
257
257
|
},
|
|
258
258
|
|
|
259
259
|
Screensaver: {
|
|
260
|
-
description: 'Full-screen idle screen with a large clock, date and
|
|
260
|
+
description: 'Full-screen idle screen with a large clock, date and an optional logo; show it after inactivity.',
|
|
261
261
|
props: {
|
|
262
262
|
open: open('screensaver'),
|
|
263
263
|
locale: { type: 'string', default: 'en-GB', description: 'BCP 47 locale for the date line.' },
|
|
264
264
|
hint: { type: 'string', description: 'Small hint under the date, e.g. "Tap to continue".' },
|
|
265
|
+
logo: { type: 'string', description: 'URL of an image shown above the clock. Use an absolute URL or a root-relative path such as "/brand/logo.svg". Nothing is shown when it is not set, so the screensaver never requests a missing file.' },
|
|
265
266
|
idleMs: { type: 'number', default: 0, description: 'When greater than 0 and the screensaver is closed, a hidden sentinel listens for pointer, key, wheel and touch activity and calls idle after this many milliseconds without any. 0 disables it.' },
|
|
266
267
|
},
|
|
267
268
|
events: { close: 'Called on click or any key (including Escape) while open.', idle: 'Called once the idleMs period passes without activity while closed; set open to true in the handler.' },
|
package/src/react/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import { components, registerComponents, subscribeUiLocale, getUiVersion } from
|
|
|
6
6
|
|
|
7
7
|
const C = {};
|
|
8
8
|
const useIsoLayoutEffect = typeof document === 'undefined' ? useEffect : useLayoutEffect;
|
|
9
|
-
const EV = { keydown: 'KeyDown', keyup: 'KeyUp', dragenter: 'DragEnter', dragover: 'DragOver', dragleave: 'DragLeave', pointerdown: 'PointerDown', pointerup: 'PointerUp', pointermove: 'PointerMove', mousedown: 'MouseDown', mouseup: 'MouseUp', mouseenter: 'MouseEnter', mouseleave: 'MouseLeave', mousemove: 'MouseMove', mouseover: 'MouseOver', mouseout: 'MouseOut', dragstart: 'DragStart', dragend: 'DragEnd', dblclick: 'DoubleClick', contextmenu: 'ContextMenu', pointerenter: 'PointerEnter', pointerleave: 'PointerLeave', pointercancel: 'PointerCancel', touchstart: 'TouchStart', touchend: 'TouchEnd', touchmove: 'TouchMove', focusin: '
|
|
9
|
+
const EV = { keydown: 'KeyDown', keyup: 'KeyUp', dragenter: 'DragEnter', dragover: 'DragOver', dragleave: 'DragLeave', pointerdown: 'PointerDown', pointerup: 'PointerUp', pointermove: 'PointerMove', mousedown: 'MouseDown', mouseup: 'MouseUp', mouseenter: 'MouseEnter', mouseleave: 'MouseLeave', mousemove: 'MouseMove', mouseover: 'MouseOver', mouseout: 'MouseOut', dragstart: 'DragStart', dragend: 'DragEnd', dblclick: 'DoubleClick', contextmenu: 'ContextMenu', pointerenter: 'PointerEnter', pointerleave: 'PointerLeave', pointercancel: 'PointerCancel', touchstart: 'TouchStart', touchend: 'TouchEnd', touchmove: 'TouchMove', focusin: 'Focus', focusout: 'Blur' };
|
|
10
10
|
const hh = (t, p, ...k) => {
|
|
11
11
|
if (p && typeof t === 'string') { const q = {}; for (const key in p) { const m = /^on([A-Za-z]+)$/.exec(key), n = m && EV[m[1].toLowerCase()]; q[n ? 'on' + n : key] = p[key]; } p = q; }
|
|
12
12
|
if (p && 'class' in p) { const { class: cls, ...rest } = p; p = { ...rest, className: cls }; }
|
|
@@ -113,3 +113,10 @@
|
|
|
113
113
|
.zg-sr{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0}
|
|
114
114
|
.zg-select__opt:focus-visible{outline:3px solid var(--zg-brand-soft);outline-offset:-3px;background:var(--zg-s2)}
|
|
115
115
|
.zg-seg button:focus-visible,.zg-tabs button:focus-visible,.zg-code-b pre:focus-visible{outline:3px solid var(--zg-brand-soft);outline-offset:2px}
|
|
116
|
+
.zg-select__opt[data-active],.zg-select__opt[data-active]:hover{background:var(--zg-s2);outline:3px solid var(--zg-brand-soft);outline-offset:-3px}
|
|
117
|
+
.zg-tip:hover::after,.zg-tip:focus-visible::after{pointer-events:auto}
|
|
118
|
+
.zg-tip:hover::before{content:"";position:absolute;left:0;right:0;bottom:100%;height:10px}
|
|
119
|
+
.zg-tip--bottom:hover::before{bottom:auto;top:100%}
|
|
120
|
+
.zg-tip--left:hover::before,.zg-tip--right:hover::before{top:0;bottom:0;height:auto;width:10px}
|
|
121
|
+
.zg-tip--left:hover::before{left:auto;right:100%}.zg-tip--right:hover::before{right:auto;left:100%}
|
|
122
|
+
.zg-tip[data-tip-off]::after{opacity:0!important;pointer-events:none!important}.zg-tip[data-tip-off]::before{display:none}
|
package/types/react.d.ts
CHANGED
|
@@ -382,6 +382,7 @@ export const UiButton: ComponentType<UiButtonProps>;
|
|
|
382
382
|
/**
|
|
383
383
|
* Month grid for picking one date. Use inline, or inside a popover via DatePicker.
|
|
384
384
|
* Note: The event payload is a Date, not a string: convert to ISO yourself if the model is a string.
|
|
385
|
+
* Note: Keyboard: one tab stop on the day grid; arrows move by day or week, Home and End to the week start and end, PageUp and PageDown change month (Shift: year), Enter or Space picks. Without a selected date the visible month is the current month, which server and client can disagree on right at a month boundary; pass modelValue or today to make it deterministic.
|
|
385
386
|
*/
|
|
386
387
|
export interface UiCalendarProps {
|
|
387
388
|
/** Selected date: a Date, ISO string or timestamp. Also sets the visible month. A date-only string like "2026-10-21" is read as a local date, so the day does not shift with the time zone. */
|
|
@@ -402,6 +403,8 @@ export interface UiCalendarProps {
|
|
|
402
403
|
max?: Date | string | number;
|
|
403
404
|
/** Return true to disable a specific day. */
|
|
404
405
|
disabledDate?: (date: Date) => boolean;
|
|
406
|
+
/** The day marked as today. By default it is read from the browser clock after mount (nothing is marked in server-rendered HTML), so server and client never disagree near midnight or across time zones. Pass it to mark a day in the HTML too. */
|
|
407
|
+
today?: Date | string | number;
|
|
405
408
|
/**
|
|
406
409
|
* Marked days: keys are YYYY-MM-DD, values are the tooltip text.
|
|
407
410
|
* @default {}
|
|
@@ -630,6 +633,7 @@ export const UiCodeInput: ComponentType<UiCodeInputProps>;
|
|
|
630
633
|
* Searchable single-choice dropdown. Use for long option lists where typing to filter is faster than scrolling.
|
|
631
634
|
* Note: Selecting from the list is the only way to set a value: free text is never emitted.
|
|
632
635
|
* Note: The list closes on outside click and on Escape.
|
|
636
|
+
* Note: Keyboard (WAI-ARIA combobox): the input has role=combobox with aria-expanded, aria-controls and aria-activedescendant; focus stays in the input, Arrow Down and Up open the list and move the active option (wraps, skips disabled), Enter chooses it, Escape closes.
|
|
633
637
|
*/
|
|
634
638
|
export interface UiComboboxProps {
|
|
635
639
|
/** Value of the selected option. */
|
|
@@ -926,7 +930,7 @@ export const UiDataTable: ComponentType<UiDataTableProps>;
|
|
|
926
930
|
/**
|
|
927
931
|
* Field with a calendar popup for choosing a date. Use in forms where a single date is needed.
|
|
928
932
|
* Note: The event payload is a Date, not a string: convert to ISO yourself if the model is a string.
|
|
929
|
-
* Note: The popup closes on outside click and on Escape.
|
|
933
|
+
* Note: The popup closes on outside click and on Escape (focus returns to the trigger). Arrow Down or Up on the trigger opens it and focus moves into the calendar, which uses the Calendar keys.
|
|
930
934
|
*/
|
|
931
935
|
export interface UiDatePickerProps {
|
|
932
936
|
/** Selected date (ISO string recommended, e.g. 2026-10-21). */
|
|
@@ -961,6 +965,11 @@ export interface UiDatePickerProps {
|
|
|
961
965
|
* @remarks Forwarded to the inner element.
|
|
962
966
|
*/
|
|
963
967
|
disabledDate?: (date: Date) => boolean;
|
|
968
|
+
/**
|
|
969
|
+
* The day marked as today in the calendar; by default read from the browser clock after mount. Forwarded to the calendar.
|
|
970
|
+
* @remarks Forwarded to the inner element.
|
|
971
|
+
*/
|
|
972
|
+
today?: Date | string | number;
|
|
964
973
|
/**
|
|
965
974
|
* Adds a red asterisk to the label. Does not validate.
|
|
966
975
|
* @default false
|
|
@@ -3663,6 +3672,7 @@ export const UiToggleGroup: ComponentType<UiToggleGroupProps>;
|
|
|
3663
3672
|
/**
|
|
3664
3673
|
* Short text bubble shown on hover or focus of the wrapped element; use for icon-only controls and hints.
|
|
3665
3674
|
* Note: Pure CSS bubble; it cannot contain markup. For rich content use HoverCard.
|
|
3675
|
+
* Note: Escape hides the bubble while it is shown (hover or focus) without moving focus; it stays visible while the pointer is over the trigger or the bubble, and returns on the next hover or focus.
|
|
3666
3676
|
*/
|
|
3667
3677
|
export interface UiTooltipProps {
|
|
3668
3678
|
/** Tooltip text. */
|
package/types/vue.d.ts
CHANGED
|
@@ -402,6 +402,7 @@ export const UiButton: UiVueComponent<UiButtonProps, UiButtonSlots>;
|
|
|
402
402
|
/**
|
|
403
403
|
* Month grid for picking one date. Use inline, or inside a popover via DatePicker.
|
|
404
404
|
* Note: The event payload is a Date, not a string: convert to ISO yourself if the model is a string.
|
|
405
|
+
* Note: Keyboard: one tab stop on the day grid; arrows move by day or week, Home and End to the week start and end, PageUp and PageDown change month (Shift: year), Enter or Space picks. Without a selected date the visible month is the current month, which server and client can disagree on right at a month boundary; pass modelValue or today to make it deterministic.
|
|
405
406
|
*/
|
|
406
407
|
export interface UiCalendarProps {
|
|
407
408
|
/** Selected date: a Date, ISO string or timestamp. Also sets the visible month. A date-only string like "2026-10-21" is read as a local date, so the day does not shift with the time zone. */
|
|
@@ -422,6 +423,8 @@ export interface UiCalendarProps {
|
|
|
422
423
|
max?: Date | string | number;
|
|
423
424
|
/** Return true to disable a specific day. */
|
|
424
425
|
disabledDate?: (date: Date) => boolean;
|
|
426
|
+
/** The day marked as today. By default it is read from the browser clock after mount (nothing is marked in server-rendered HTML), so server and client never disagree near midnight or across time zones. Pass it to mark a day in the HTML too. */
|
|
427
|
+
today?: Date | string | number;
|
|
425
428
|
/**
|
|
426
429
|
* Marked days: keys are YYYY-MM-DD, values are the tooltip text.
|
|
427
430
|
* @default {}
|
|
@@ -656,6 +659,7 @@ export const UiCodeInput: UiVueComponent<UiCodeInputProps, UiCodeInputSlots>;
|
|
|
656
659
|
* Searchable single-choice dropdown. Use for long option lists where typing to filter is faster than scrolling.
|
|
657
660
|
* Note: Selecting from the list is the only way to set a value: free text is never emitted.
|
|
658
661
|
* Note: The list closes on outside click and on Escape.
|
|
662
|
+
* Note: Keyboard (WAI-ARIA combobox): the input has role=combobox with aria-expanded, aria-controls and aria-activedescendant; focus stays in the input, Arrow Down and Up open the list and move the active option (wraps, skips disabled), Enter chooses it, Escape closes.
|
|
659
663
|
*/
|
|
660
664
|
export interface UiComboboxProps {
|
|
661
665
|
/** Value of the selected option. */
|
|
@@ -963,7 +967,7 @@ export const UiDataTable: UiVueComponent<UiDataTableProps, UiDataTableSlots>;
|
|
|
963
967
|
/**
|
|
964
968
|
* Field with a calendar popup for choosing a date. Use in forms where a single date is needed.
|
|
965
969
|
* Note: The event payload is a Date, not a string: convert to ISO yourself if the model is a string.
|
|
966
|
-
* Note: The popup closes on outside click and on Escape.
|
|
970
|
+
* Note: The popup closes on outside click and on Escape (focus returns to the trigger). Arrow Down or Up on the trigger opens it and focus moves into the calendar, which uses the Calendar keys.
|
|
967
971
|
*/
|
|
968
972
|
export interface UiDatePickerProps {
|
|
969
973
|
/** Selected date (ISO string recommended, e.g. 2026-10-21). */
|
|
@@ -998,6 +1002,11 @@ export interface UiDatePickerProps {
|
|
|
998
1002
|
* @remarks Forwarded to the inner element.
|
|
999
1003
|
*/
|
|
1000
1004
|
disabledDate?: (date: Date) => boolean;
|
|
1005
|
+
/**
|
|
1006
|
+
* The day marked as today in the calendar; by default read from the browser clock after mount. Forwarded to the calendar.
|
|
1007
|
+
* @remarks Forwarded to the inner element.
|
|
1008
|
+
*/
|
|
1009
|
+
today?: Date | string | number;
|
|
1001
1010
|
/**
|
|
1002
1011
|
* Adds a red asterisk to the label. Does not validate.
|
|
1003
1012
|
* @default false
|
|
@@ -3692,6 +3701,7 @@ export const UiToggleGroup: UiVueComponent<UiToggleGroupProps, UiToggleGroupSlot
|
|
|
3692
3701
|
/**
|
|
3693
3702
|
* Short text bubble shown on hover or focus of the wrapped element; use for icon-only controls and hints.
|
|
3694
3703
|
* Note: Pure CSS bubble; it cannot contain markup. For rich content use HoverCard.
|
|
3704
|
+
* Note: Escape hides the bubble while it is shown (hover or focus) without moving focus; it stays visible while the pointer is over the trigger or the bubble, and returns on the next hover or focus.
|
|
3695
3705
|
*/
|
|
3696
3706
|
export interface UiTooltipProps {
|
|
3697
3707
|
/** Tooltip text. */
|