@zeyos/zx 4.2.0 → 4.3.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 (52) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +5 -0
  3. package/dist/types/components/calendar/calendar.d.ts +19 -2
  4. package/dist/types/components/dialog/dialog.d.ts +6 -1
  5. package/dist/types/components/dropdown/dropdown.d.ts +5 -0
  6. package/dist/types/components/launcher/launcher.d.ts +5 -0
  7. package/dist/types/components/message/message.d.ts +9 -1
  8. package/dist/types/components/modal/modal.d.ts +44 -7
  9. package/dist/types/components/sheet/sheet.d.ts +22 -12
  10. package/dist/types/core/overlay-host.d.ts +12 -0
  11. package/dist/zx-zeyos.esm.js +138 -32
  12. package/dist/zx-zeyos.esm.min.js +1 -1
  13. package/dist/zx.css +259 -132
  14. package/dist/zx.esm.js +277 -82
  15. package/dist/zx.esm.min.js +3 -3
  16. package/dist/zx.global.js +277 -82
  17. package/dist/zx.global.min.js +3 -3
  18. package/dist/zx.min.css +1 -1
  19. package/docs/api.json +36 -0
  20. package/docs/llms.md +37 -18
  21. package/docs/llms.txt +1 -1
  22. package/package.json +1 -1
  23. package/src/components/account-menu/account-menu.css +1 -1
  24. package/src/components/app-rail/app-rail.css +1 -1
  25. package/src/components/calendar/calendar.css +66 -24
  26. package/src/components/calendar/calendar.js +114 -19
  27. package/src/components/context-menu/context-menu.css +2 -2
  28. package/src/components/context-menu/context-menu.js +5 -1
  29. package/src/components/date-picker/date-picker.css +18 -11
  30. package/src/components/datebox/date-range-box.css +4 -2
  31. package/src/components/datebox/datebox.css +4 -2
  32. package/src/components/dialog/dialog.css +4 -3
  33. package/src/components/dialog/dialog.js +2 -1
  34. package/src/components/dropdown/dropdown.css +4 -2
  35. package/src/components/dropdown/dropdown.js +7 -2
  36. package/src/components/launcher/launcher.css +10 -8
  37. package/src/components/launcher/launcher.js +9 -2
  38. package/src/components/menu-button/menu-button.css +2 -2
  39. package/src/components/message/message.css +7 -16
  40. package/src/components/message/message.js +17 -11
  41. package/src/components/modal/modal.css +6 -4
  42. package/src/components/modal/modal.js +99 -25
  43. package/src/components/select/select.css +9 -7
  44. package/src/components/sheet/sheet.css +25 -7
  45. package/src/components/sheet/sheet.js +43 -23
  46. package/src/components/table/table.css +5 -3
  47. package/src/components/tag-picker/tag-picker.css +5 -3
  48. package/src/components/tooltip/tooltip.css +4 -2
  49. package/src/components/tooltip/tooltip.js +4 -1
  50. package/src/core/overlay-host.js +23 -0
  51. package/styles/tokens/dark.css +4 -0
  52. package/styles/tokens/semantic.css +83 -8
package/docs/api.json CHANGED
@@ -2750,6 +2750,12 @@
2750
2750
  "type": "Node|string|number|{toElement: () => Node|null}|null",
2751
2751
  "default": "null",
2752
2752
  "description": "Body content."
2753
+ },
2754
+ {
2755
+ "name": "scope",
2756
+ "type": "Element|string|null",
2757
+ "default": "null",
2758
+ "description": "Element whose nearest Zx theme scope owns the overlay; defaults to the opener."
2753
2759
  }
2754
2760
  ],
2755
2761
  "events": [
@@ -3082,6 +3088,12 @@
3082
3088
  "type": "boolean",
3083
3089
  "default": "false",
3084
3090
  "description": "Whether a click within the panel closes it."
3091
+ },
3092
+ {
3093
+ "name": "scope",
3094
+ "type": "Element|string|null",
3095
+ "default": "null",
3096
+ "description": "Element whose nearest Zx theme scope owns the panel; defaults to the anchor."
3085
3097
  }
3086
3098
  ],
3087
3099
  "events": [
@@ -4587,6 +4599,12 @@
4587
4599
  "type": "false|{move?: string, open?: string, close?: string}",
4588
4600
  "default": null,
4589
4601
  "description": "Visible keyboard hints, or false."
4602
+ },
4603
+ {
4604
+ "name": "scope",
4605
+ "type": "Element|string|null",
4606
+ "default": "null",
4607
+ "description": "Element whose nearest Zx theme scope owns an internally created launcher; defaults to the opener."
4590
4608
  }
4591
4609
  ],
4592
4610
  "events": [
@@ -4969,6 +4987,12 @@
4969
4987
  "type": "boolean",
4970
4988
  "default": "true",
4971
4989
  "description": "Whether to render a close button."
4990
+ },
4991
+ {
4992
+ "name": "scope",
4993
+ "type": "Element|string|null",
4994
+ "default": "null",
4995
+ "description": "Element whose nearest Zx theme scope owns a floating toast."
4972
4996
  }
4973
4997
  ],
4974
4998
  "events": [],
@@ -5005,6 +5029,12 @@
5005
5029
  "type": "boolean",
5006
5030
  "default": "false",
5007
5031
  "description": "Whether to destroy the modal after it closes."
5032
+ },
5033
+ {
5034
+ "name": "scope",
5035
+ "type": "Element|string|null",
5036
+ "default": "null",
5037
+ "description": "Element whose nearest Zx theme scope owns the overlay; defaults to the opener."
5008
5038
  }
5009
5039
  ],
5010
5040
  "events": [
@@ -6985,6 +7015,12 @@
6985
7015
  "type": "boolean",
6986
7016
  "default": "false",
6987
7017
  "description": "Whether to destroy the sheet after it closes."
7018
+ },
7019
+ {
7020
+ "name": "scope",
7021
+ "type": "Element|string|null",
7022
+ "default": "null",
7023
+ "description": "Element whose nearest Zx theme scope owns a floating sheet; defaults to the opener."
6988
7024
  }
6989
7025
  ],
6990
7026
  "events": [
package/docs/llms.md CHANGED
@@ -80,14 +80,19 @@ ambient canvas layer; material controls how surfaces respond to that light. The
80
80
  applies to AppIcons, ordinary controls, persistent chrome, raised cards, and every transient overlay: Dropdown, ContextMenu,
81
81
  MenuButton, AccountMenu, AppRail flyouts, Select, TagPicker, date pickers, tooltips, column menus,
82
82
  and toasts use `--zx-color-overlay-surface`; Launcher, Modal, Dialog, and overlay Sheets use the
83
- more opaque `--zx-color-overlay-panel`. A docked Sheet is layout rather than
84
- an overlay and becomes opaque, as do dense data surfaces. Glass and Deep glass progressively raise
85
- translucency, blur, highlights, and elevation; reduced-transparency preferences restore solid
86
- surfaces.
83
+ calmer `--zx-color-overlay-panel`. Both tiers combine a translucent base, responsive specular and
84
+ Aurora tint layers, backdrop filtering, and dedicated state/divider roles; the modal backdrop is a
85
+ separate material token so it does not erase the light the panel should refract. A docked Sheet is
86
+ layout rather than an overlay and becomes opaque, as do dense data surfaces. Glass and Deep glass
87
+ progressively raise translucency, blur, highlights, and elevation; reduced-transparency,
88
+ increased-contrast, forced-colour, and unsupported-filter environments restore solid surfaces.
89
+ Top-layer components remain DOM children of their nearest Zx theme scope, so a theme or material
90
+ set on an application region continues to inherit through its menus, dialogs, tooltips, and toasts.
87
91
 
88
92
  Define a product theme by overriding **semantic** tokens under a `[data-zx-theme="name"]` selector:
89
93
  `--zx-color-bg-page/surface/raised/control/hover/selected`, `--zx-color-glass-chrome/surface`,
90
- `--zx-color-overlay-surface/panel/border`, `--zx-color-border(-strong/-control)`,
94
+ `--zx-color-overlay-surface/panel/backdrop/border/hover/selected/divider/scrim`,
95
+ `--zx-overlay-surface-image/toast-image/panel-image`, `--zx-color-border(-strong/-control)`,
91
96
  `--zx-color-text(-muted/-placeholder)`, `--zx-color-accent(-hover)/on-accent`,
92
97
  `--zx-color-danger/warning/success/info(+ -bg)`, `--zx-focus-ring`, `--zx-control-height/-radius`,
93
98
  `--zx-space-*`, `--zx-radius-*`, `--zx-text-*`. Never use tier-1 palette tokens (`--zx-gray-*`,
@@ -456,11 +461,13 @@ A native-dialog launcher for applications, recent records, commands, and grouped
456
461
  Zx owns ranking, focus, request cancellation, and presentation; ZeyOS owns the catalogue,
457
462
  permissions, recent-history/cache policy, and the route or command ultimately invoked.
458
463
 
459
- - **Constructor** — `new Launcher(null, options)` owns a body-level dialog;
464
+ - **Constructor** — `new Launcher(null, options)` owns a top-layer dialog in the opener's nearest
465
+ Zx theme scope;
460
466
  `new Launcher(existingDialog, options)` enhances and later restores an existing `<dialog>`.
461
467
  - **Options** — `items`, abortable `sources: [{id, label?, minQuery?, when?, order?, load(query, {signal})}]`,
462
468
  `query`, `debounce: 250`, `minQuery: 0`, `maxResults: 100`, `placeholder`, `label`, `emptyText`,
463
- `loadingText`, `shortcut: 'mod+k'|false`, and visible `hints` labels (or `false`).
469
+ `loadingText`, `shortcut: 'mod+k'|false`, visible `hints` labels (or `false`), and `scope` to
470
+ explicitly select an element whose nearest Zx theme scope owns the launcher.
464
471
  - **Items** — `{id, label, description?, keywords?, group?, icon?, badge?, value?, href?, target?,
465
472
  invoke?, pinned?, disabled?, kind?, current?, when?, groupOrder?, itemOrder?}`. `icon` accepts a
466
473
  bundled icon name, Node, or factory. Links stay native so modified and middle clicks work;
@@ -787,8 +794,10 @@ The shared floating region is fixed to the upper-right logical corner by default
787
794
  messages rather than the page, and uses the translucent glass surface tokens with a solid fallback.
788
795
 
789
796
  - **Statics** — `Message.info/success/warning/error(msg, opts)`,
790
- `Message.show(msg, { kind, timeout=4000, closable })` → `{ close() }`,
791
- `Message.progress(text)` → `{ update(pct, text?), done(), fail(text?) }`.
797
+ `Message.show(msg, { kind, timeout=4000, closable, scope? })` → `{ close() }`,
798
+ `Message.progress(text, {scope?})` → `{ update(pct, text?), done(), fail(text?) }`. `scope` is
799
+ an element or selector whose nearest Zx theme scope owns that floating region; without it the
800
+ focused opener supplies the scope.
792
801
  - **Per-message options** — `kind: 'info'`, `timeout`, `closable: true`, `maxVisible: 5` (extras
793
802
  queue, and hovering pauses dismissal).
794
803
  <!-- /doc -->
@@ -797,10 +806,10 @@ messages rather than the page, and uses the translucent glass surface tokens wit
797
806
  ### Modal
798
807
 
799
808
  Thin overlay on the native `<dialog>` element. The surface uses the shared, reading-sized overlay
800
- panel material; the backdrop uses `--zx-color-bg-backdrop`, and focus returns to the opener on close.
809
+ panel material; the backdrop uses `--zx-color-overlay-backdrop`, and focus returns to the opener on close.
801
810
 
802
811
  - **Options** — `content`, `width: 'auto'`, `closable: true`, `lightDismiss: false`,
803
- `destroyOnClose: false`.
812
+ `destroyOnClose: false`, `scope: null` (the opener's nearest Zx theme scope by default).
804
813
  - **Methods** — `open()`, `close(result?)`, `setContent()`, `isOpen()`.
805
814
  - **Events** — `open`, `close {result}`, `cancel` (Esc; preventable with `event.preventDefault()`).
806
815
  <!-- /doc -->
@@ -832,7 +841,7 @@ When a Dock adopts it, the Sheet becomes an opaque layout surface instead.
832
841
  (`true | 'trap-focus' | false`), `backdrop: 'dim'` (`'dim' | 'blur' | 'none'`), `size: null`
833
842
  (CSS length or px number, applied to whichever axis `side` implies), `closeButton: true`,
834
843
  plus everything Dialog takes — `title`, `content`, `buttons`, `closable`, `lightDismiss`
835
- (defaults to `true` here, unlike Modal), `destroyOnClose`.
844
+ (defaults to `true` here, unlike Modal), `destroyOnClose`, `scope`.
836
845
  - **Methods** — `open()`, `close(result?)`, `setSide(side)`, `getSide()`, `setSize(size)`,
837
846
  `isModal()`, `isDocked()`, `isOpen()`, `getSize()`, `snapTo(target)`, plus Dialog's `setTitle()`, `setContent()`,
838
847
  `setButtons()`, `addView()`, `showView()`.
@@ -846,10 +855,12 @@ When a Dock adopts it, the Sheet becomes an opaque layout surface instead.
846
855
  across a handoff: the element moves and reopens in place, so DOM state and listeners survive,
847
856
  and no `open`/`close` is emitted for what is only a change of address.
848
857
  - **Modality** — `true` delegates focus containment, page inertness, Escape, and the backdrop to
849
- the browser through `showModal()`. `'trap-focus'` and `false` open with `show()`, so the page
850
- keeps scrolling and stays interactive; Escape and outside-click are re-implemented, and
851
- `'trap-focus'` adds `focusTrap()` on top. Only a modal sheet renders a `::backdrop`, so
852
- `backdrop` is meaningful only when `modal` is `true`.
858
+ the browser through `showModal()`. Floating `'trap-focus'` and `false` sheets use a manual
859
+ Popover top layer, so they stay viewport-anchored even inside a transformed or clipped theme
860
+ scope while the page keeps scrolling and stays interactive. Escape and outside-click are
861
+ re-implemented, and `'trap-focus'` adds `focusTrap()` on top. Their top-layer backdrop is
862
+ explicitly transparent and non-filtering; `backdrop` is meaningful only when `modal` is `true`.
863
+ A docked Sheet remains a native non-modal dialog in its Dock's flow.
853
864
  - **Resizing and detents** — `resizable` makes the inner edge draggable between `min` and `max`.
854
865
  `snap` gives it detents: a number at or below 1 is a fraction of the viewport along the sheet's
855
866
  axis, anything larger is pixels, and a string is any CSS length (`'320px'`, `'90%'`). With
@@ -920,12 +931,14 @@ alone, `new SheetStack({…})`.
920
931
  ### Dropdown
921
932
 
922
933
  Generic anchored popover in the top layer, constructed as `new Dropdown(anchor, options)`. It
923
- light-dismisses on an outside click or Esc, and mirrors `aria-expanded` onto the anchor.
934
+ light-dismisses on an outside click or Esc, mirrors `aria-expanded` onto the anchor, and stays a
935
+ DOM child of the anchor's nearest Zx theme scope while the browser lifts it into the top layer.
924
936
 
925
937
  - **Options** — `content`,
926
938
  `placement: 'bottom-start'|'bottom-end'|'top-start'|'top-end'|'bottom'|'top'` plus the matching
927
939
  `left*` and `right*` placements, `offset: 4`,
928
- `matchWidth: false`, `openOn: 'click'|'manual'`, `closeOnSelect: false`.
940
+ `matchWidth: false`, `openOn: 'click'|'manual'`, `closeOnSelect: false`, `scope: null` (the
941
+ anchor's nearest Zx theme scope by default).
929
942
  - **Methods** — `open()`, `close()`, `toggle()`, `isOpen()`, `setContent()`, `getPanel()`.
930
943
  - **Events** — `open`, `close`.
931
944
  <!-- /doc -->
@@ -1252,6 +1265,12 @@ windowed loader.
1252
1265
  idempotent, version-guarded `detail.revert()` if persistence fails. With `optimistic: false`, the
1253
1266
  component stays controlled: it emits the proposal without changing data, and the application
1254
1267
  applies an accepted response through `updateEvent()` or `setEvents()`. Zx does not send requests.
1268
+ - **View layout** — agenda days keep every event in one vertically stacked content column. Month
1269
+ uses a continuous seven-day grid with full-height day boundaries and event lanes overlaid below
1270
+ the date labels; today is indicated with accent text instead of a decorative edge highlight.
1271
+ - **Pointer editing** — a timed move preserves the point where the event was grabbed and snaps the
1272
+ resulting start to `slotDuration`. The lower grip stretches the event in its original day column,
1273
+ while pointer capture keeps either interaction active if the pointer leaves the event surface.
1255
1274
  - **Keyboard** — time slots and date grids use arrow-key navigation, Home/End, PageUp/PageDown,
1256
1275
  Enter/Space activation, and roving focus. On an editable event, Space grabs it, arrows propose a
1257
1276
  slot/day move (or change the focused resize grip), Space drops, and Escape cancels. Live status
package/docs/llms.txt CHANGED
@@ -52,7 +52,7 @@ exports and as `window.zx.*`.
52
52
  - Helpers: `formatNumber`/`formatCurrency`/`formatPercent`/`formatFileSize`/`formatRelativeTime` (locale-aware, cached `Intl`), `debounce`/`throttle`, `groupBy`/`sortBy`/`uniqueBy`, `escapeRegExp`/`highlightMatch`, `storage()` (namespaced JSON, degrades to memory), `toCsv`/`downloadBlob`/`copyToClipboard`, `truncate()`/`isTruncated()` (line clamp with the full value on hover).
53
53
 
54
54
  ## Theming
55
- Load `zx.css` once; set `data-zx-theme` (`light`|`dark`|`auto`) and `data-zx-density` (`cozy`|`compact`) on any ancestor. Define a product theme by overriding semantic `--zx-color-*` / `--zx-control-*` tokens under `[data-zx-theme="name"]`. The theme studio controls accent, neutral tint, density, font, type size, radius, control height, and CSS-first glass material. Compact menus, pickers, tooltips, column menus, and toasts share a floating overlay material; Launcher, Modal, Dialog, and overlay Sheets share a more opaque panel material. Docked Sheets and dense data surfaces remain opaque. Never use tier-1 palette tokens or raw color literals in application CSS.
55
+ Load `zx.css` once; set `data-zx-theme` (`light`|`dark`|`auto`) and `data-zx-density` (`cozy`|`compact`) on any ancestor. Define a product theme by overriding semantic `--zx-color-*` / `--zx-control-*` tokens under `[data-zx-theme="name"]`. The theme studio controls accent, neutral tint, density, font, type size, radius, control height, and CSS-first glass material. Compact menus, pickers, tooltips, column menus, and toasts share responsive compact glass; Launcher, Modal, Dialog, and floating Sheets share a calmer reading panel. Both preserve scoped theme inheritance and refract Aurora/page content; docked Sheets and dense data surfaces remain opaque. Never use tier-1 palette tokens or raw color literals in application CSS.
56
56
 
57
57
  ## Talking to ZeyOS
58
58
  Use the dedicated ZeyOS client library **`@zeyos/client`** (`npm install @zeyos/client`) for ZeyOS business data: `createZeyosClient({ platform, auth })`, typed `client.api.list/get/create/update<Resource>` over the full OpenAPI surface, and `normalizeListResult`. Zx accepts the client by injection and does not bundle it. See the `website/layouts/zeyos-invoices.layout.js` layout for a Zx-components-on-@zeyos/client reference. Zx also ships a minimal `Http`/`zeyosService` for ad-hoc in-app `remotecall`, but `@zeyos/client` is the recommended default.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeyos/zx",
3
- "version": "4.2.0",
3
+ "version": "4.3.0",
4
4
  "description": "Dependency-free JavaScript implementation of the Xenon Design System.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -60,7 +60,7 @@
60
60
  min-inline-size: 220px;
61
61
  gap: 2px;
62
62
  margin: calc(-1 * var(--zx-space-1)) calc(-1 * var(--zx-space-1)) var(--zx-space-1);
63
- border-block-end: 1px solid var(--zx-color-border);
63
+ border-block-end: 1px solid var(--zx-color-overlay-divider);
64
64
  padding: var(--zx-space-3);
65
65
  }
66
66
 
@@ -101,7 +101,7 @@
101
101
  }
102
102
  .zx-app-rail__flyout-item:hover,
103
103
  .zx-app-rail__flyout-item:focus-visible,
104
- .zx-app-rail__flyout-item[aria-current="page"] { background: var(--zx-color-bg-hover); color: var(--zx-color-accent); }
104
+ .zx-app-rail__flyout-item[aria-current="page"] { background: var(--zx-color-overlay-hover); color: var(--zx-color-accent); }
105
105
  .zx-app-rail__flyout-item-icon { display: inline-flex; grid-column: 1; }
106
106
  .zx-app-rail__flyout-item-label { grid-column: 2; }
107
107
  .zx-app-rail__flyout-item-badge { grid-column: 3; color: var(--zx-color-text-muted); font-size: var(--zx-text-xs); }
@@ -261,6 +261,7 @@
261
261
  inline-size: 100%;
262
262
  block-size: 100%;
263
263
  min-inline-size: 0;
264
+ min-block-size: 0;
264
265
  overflow: hidden;
265
266
  border: 1px solid color-mix(in srgb, var(--zx-calendar-event-color) 65%, var(--zx-color-border));
266
267
  border-inline-start: 3px solid var(--zx-calendar-event-color);
@@ -310,8 +311,11 @@
310
311
  z-index: 3;
311
312
  display: grid;
312
313
  block-size: 8px;
314
+ min-block-size: 0;
313
315
  place-items: center;
316
+ appearance: none;
314
317
  border: 0;
318
+ border-radius: 0;
315
319
  background: transparent;
316
320
  padding: 0;
317
321
  cursor: ns-resize;
@@ -329,6 +333,7 @@
329
333
  .zx-calendar__event[data-variant="span"] .zx-calendar__event-action,
330
334
  .zx-calendar__event[data-variant="month"] .zx-calendar__event-action {
331
335
  display: flex;
336
+ min-block-size: 0;
332
337
  align-items: center;
333
338
  gap: var(--zx-space-1);
334
339
  padding-block: 2px;
@@ -358,10 +363,17 @@
358
363
  }
359
364
 
360
365
  .zx-calendar__agenda { display: grid; }
361
- .zx-calendar__agenda-day { display: grid; grid-template-columns: minmax(10rem, 22%) minmax(0, 1fr); border-block-end: 1px solid var(--zx-color-border); }
366
+ .zx-calendar__agenda-day {
367
+ display: grid;
368
+ grid-template-columns: minmax(10rem, 22%) minmax(0, 1fr);
369
+ border-block-end: 1px solid var(--zx-color-border);
370
+ }
362
371
  .zx-calendar__agenda-heading {
363
- align-self: start;
372
+ align-self: stretch;
373
+ appearance: none;
364
374
  border: 0;
375
+ border-inline-end: 1px solid var(--zx-color-border);
376
+ border-radius: 0;
365
377
  background: var(--zx-color-bg-muted);
366
378
  color: var(--zx-color-text);
367
379
  padding: var(--zx-space-3);
@@ -371,9 +383,13 @@
371
383
  cursor: pointer;
372
384
  }
373
385
  .zx-calendar__agenda-heading[aria-current="date"] { color: var(--zx-color-accent); }
386
+ .zx-calendar__agenda-events {
387
+ display: grid;
388
+ min-inline-size: 0;
389
+ }
374
390
  .zx-calendar__agenda-empty { margin: 0; color: var(--zx-color-text-muted); padding: var(--zx-space-3); }
375
- .zx-calendar__agenda-day > .zx-calendar__event { border-block-end: 1px solid var(--zx-color-border); }
376
- .zx-calendar__agenda-day > .zx-calendar__event:last-child { border-block-end: 0; }
391
+ .zx-calendar__agenda-events > .zx-calendar__event { border-block-end: 1px solid var(--zx-color-border); }
392
+ .zx-calendar__agenda-events > .zx-calendar__event:last-child { border-block-end: 0; }
377
393
  .zx-calendar__event[data-variant="agenda"] .zx-calendar__event-action {
378
394
  grid-template-columns: minmax(6rem, 18%) minmax(0, 1fr);
379
395
  grid-template-areas: "time title" "time location";
@@ -418,26 +434,13 @@
418
434
  text-transform: uppercase;
419
435
  letter-spacing: .04em;
420
436
  }
421
- .zx-calendar__month-weekdays .zx-calendar__month-weekday { grid-row: 1; }
422
- .zx-calendar__month-weekdays .zx-calendar__month-weekday:nth-last-child(7) { grid-column: 2; }
423
- .zx-calendar__month-weekdays .zx-calendar__month-weekday:nth-last-child(6) { grid-column: 3; }
424
- .zx-calendar__month-weekdays .zx-calendar__month-weekday:nth-last-child(5) { grid-column: 4; }
425
- .zx-calendar__month-weekdays .zx-calendar__month-weekday:nth-last-child(4) { grid-column: 5; }
426
- .zx-calendar__month-weekdays .zx-calendar__month-weekday:nth-last-child(3) { grid-column: 6; }
427
- .zx-calendar__month-weekdays .zx-calendar__month-weekday:nth-last-child(2) { grid-column: 7; }
428
- .zx-calendar__month-weekdays .zx-calendar__month-weekday:nth-last-child(1) { grid-column: 8; }
429
- .zx-calendar__month-weekdays:not(:has(.zx-calendar__month-week-gutter)) .zx-calendar__month-weekday:nth-last-child(7) { grid-column: 1; }
430
- .zx-calendar__month-weekdays:not(:has(.zx-calendar__month-week-gutter)) .zx-calendar__month-weekday:nth-last-child(6) { grid-column: 2; }
431
- .zx-calendar__month-weekdays:not(:has(.zx-calendar__month-week-gutter)) .zx-calendar__month-weekday:nth-last-child(5) { grid-column: 3; }
432
- .zx-calendar__month-weekdays:not(:has(.zx-calendar__month-week-gutter)) .zx-calendar__month-weekday:nth-last-child(4) { grid-column: 4; }
433
- .zx-calendar__month-weekdays:not(:has(.zx-calendar__month-week-gutter)) .zx-calendar__month-weekday:nth-last-child(3) { grid-column: 5; }
434
- .zx-calendar__month-weekdays:not(:has(.zx-calendar__month-week-gutter)) .zx-calendar__month-weekday:nth-last-child(2) { grid-column: 6; }
435
- .zx-calendar__month-weekdays:not(:has(.zx-calendar__month-week-gutter)) .zx-calendar__month-weekday:nth-last-child(1) { grid-column: 7; }
436
437
  .zx-calendar__month-week { min-block-size: 7.5rem; border-block-end: 1px solid var(--zx-color-border); }
437
438
  .zx-calendar__month-week-number {
438
- grid-row: 1 / span 2;
439
+ grid-row: 1;
440
+ appearance: none;
439
441
  border: 0;
440
442
  border-inline-end: 1px solid var(--zx-color-border);
443
+ border-radius: 0;
441
444
  background: var(--zx-color-bg-muted);
442
445
  color: var(--zx-color-text-muted);
443
446
  font: inherit;
@@ -452,11 +455,21 @@
452
455
  }
453
456
  .zx-calendar__month-week:not(:has(.zx-calendar__month-week-number)) .zx-calendar__month-days,
454
457
  .zx-calendar__month-week:not(:has(.zx-calendar__month-week-number)) .zx-calendar__month-events { grid-column: 1; }
455
- .zx-calendar__month-days { grid-row: 1; }
458
+ .zx-calendar__month-days {
459
+ grid-row: 1;
460
+ min-block-size: 7.5rem;
461
+ align-items: stretch;
462
+ }
456
463
  .zx-calendar__month-day {
464
+ display: flex;
465
+ block-size: 100%;
466
+ appearance: none;
467
+ align-items: flex-start;
468
+ justify-content: flex-end;
457
469
  min-block-size: var(--zx-control-height);
458
470
  border: 0;
459
471
  border-inline-end: 1px solid var(--zx-color-border);
472
+ border-radius: 0;
460
473
  background: var(--zx-color-bg-surface);
461
474
  color: var(--zx-color-text);
462
475
  padding: var(--zx-space-2);
@@ -464,12 +477,34 @@
464
477
  text-align: end;
465
478
  cursor: pointer;
466
479
  }
480
+ .zx-calendar__month-day:last-child { border-inline-end: 0; }
467
481
  .zx-calendar__month-day[data-other-month="true"] { color: var(--zx-color-text-muted); background: var(--zx-color-bg-muted); }
468
- .zx-calendar__month-day[aria-current="date"] { color: var(--zx-color-accent); font-weight: 750; box-shadow: inset 0 2px var(--zx-color-accent); }
482
+ .zx-calendar__month-day[aria-current="date"] {
483
+ color: var(--zx-color-accent);
484
+ font-weight: 750;
485
+ }
469
486
  .zx-calendar__month-day:hover:not(:disabled) { background: var(--zx-color-bg-hover); }
470
- .zx-calendar__month-events { grid-row: 2; grid-auto-rows: calc(var(--zx-control-height) - var(--zx-space-2)); align-content: start; gap: 2px 0; padding-block: var(--zx-space-1); }
487
+ .zx-calendar__month-events {
488
+ grid-row: 1;
489
+ z-index: 2;
490
+ grid-auto-rows: calc(var(--zx-control-height) - var(--zx-space-2));
491
+ align-content: start;
492
+ gap: 2px 0;
493
+ padding-block: calc(var(--zx-control-height) + var(--zx-space-1)) var(--zx-space-1);
494
+ pointer-events: none;
495
+ }
496
+ .zx-calendar__month-events > .zx-calendar__event,
497
+ .zx-calendar__month-events > .zx-calendar__more { pointer-events: auto; }
498
+ .zx-calendar__event[data-variant="month"] .zx-calendar__event-action {
499
+ border-color: color-mix(in srgb, var(--zx-calendar-event-color) 55%, var(--zx-color-border));
500
+ border-inline-start-width: 1px;
501
+ border-radius: 0;
502
+ background: color-mix(in srgb, var(--zx-calendar-event-color) 13%, var(--zx-color-bg-surface));
503
+ }
504
+ .zx-calendar__event[data-variant="month"][data-all-day="true"] .zx-calendar__event-time { display: none; }
471
505
  .zx-calendar__more {
472
506
  align-self: center;
507
+ min-block-size: 0;
473
508
  overflow: hidden;
474
509
  border: 0;
475
510
  background: transparent;
@@ -546,7 +581,14 @@
546
581
  .zx-calendar__view-button { flex: 1 0 auto; }
547
582
  .zx-calendar__new span { position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden; clip-path: inset(50%); }
548
583
  .zx-calendar__agenda-day { grid-template-columns: 1fr; }
549
- .zx-calendar__agenda-heading { position: sticky; inset-block-start: 0; z-index: 3; border-block-end: 1px solid var(--zx-color-border); }
584
+ .zx-calendar__agenda-heading {
585
+ position: sticky;
586
+ inset-block-start: 0;
587
+ z-index: 3;
588
+ border-inline-end: 0;
589
+ border-block-end: 1px solid var(--zx-color-border);
590
+ }
591
+ .zx-calendar__agenda-events { grid-column: 1; }
550
592
  .zx-calendar__event[data-variant="agenda"] .zx-calendar__event-action { grid-template-columns: 5.5rem minmax(0, 1fr); }
551
593
  .zx-calendar__year { grid-template-columns: 1fr; padding: var(--zx-space-3); }
552
594
  }
@@ -452,7 +452,11 @@ export class Calendar extends Component {
452
452
  if (!calendarEventIntersects(event, day, next)) return false;
453
453
  return event.start >= day || index === 0;
454
454
  }).sort(compareEvents);
455
- const row = h('section', { class: 'zx-calendar__agenda-day', role: 'row' },
455
+ const row = h('section', {
456
+ class: 'zx-calendar__agenda-day',
457
+ role: 'row',
458
+ dataset: { date: calendarDayKey(day) }
459
+ },
456
460
  h('button', {
457
461
  type: 'button',
458
462
  class: 'zx-calendar__agenda-heading',
@@ -461,11 +465,13 @@ export class Calendar extends Component {
461
465
  ariaCurrent: sameCalendarDay(day, this._now()) ? 'date' : null,
462
466
  disabled: this.options.disabled
463
467
  }, this._format(day, { weekday: 'long', day: 'numeric', month: 'long' })));
468
+ const entries = h('div', { class: 'zx-calendar__agenda-events', role: 'gridcell' });
464
469
  if (!events.length) {
465
- row.append(h('p', { class: 'zx-calendar__agenda-empty', role: 'gridcell' }, this.msg('noEvents')));
470
+ entries.append(h('p', { class: 'zx-calendar__agenda-empty' }, this.msg('noEvents')));
466
471
  } else {
467
- for (const event of events) row.append(this._eventElement(event, { variant: 'agenda', day }));
472
+ for (const event of events) entries.append(this._eventElement(event, { variant: 'agenda', day }));
468
473
  }
474
+ row.append(entries);
469
475
  grid.append(row);
470
476
  }
471
477
  return grid;
@@ -721,6 +727,7 @@ export class Calendar extends Component {
721
727
  dataset: {
722
728
  eventId: key,
723
729
  variant: context.variant,
730
+ allDay: event.allDay ? 'true' : null,
724
731
  draggable: this._canMove(event) ? 'true' : null
725
732
  },
726
733
  style: event.color ? { '--zx-calendar-event-color': event.color } : null
@@ -989,7 +996,9 @@ export class Calendar extends Component {
989
996
  const item = this._eventById(control.dataset.eventId);
990
997
  const action = control.dataset.eventAction === 'resize' ? 'resize' : 'move';
991
998
  if (!item || (action === 'move' ? !this._canMove(item) : !this._canResize(item))) return;
992
- control.setPointerCapture?.(event.pointerId);
999
+ event.preventDefault();
1000
+ this._capturePointer(event.pointerId);
1001
+ const offset = action === 'move' ? this._dragOffsetForEvent(item, event.clientX, event.clientY) : null;
993
1002
  this._pointer = {
994
1003
  type: 'event',
995
1004
  pointerId: event.pointerId,
@@ -999,6 +1008,8 @@ export class Calendar extends Component {
999
1008
  event: cloneCalendarEvent(item),
1000
1009
  action,
1001
1010
  control,
1011
+ timeOffsetMinutes: offset?.minutes ?? 0,
1012
+ dayOffset: offset?.days ?? 0,
1002
1013
  moved: false
1003
1014
  };
1004
1015
  return;
@@ -1007,7 +1018,8 @@ export class Calendar extends Component {
1007
1018
  if (!slot || !this.options.selectable) return;
1008
1019
  const date = this._dateFromTarget(slot);
1009
1020
  if (!date) return;
1010
- slot.setPointerCapture?.(event.pointerId);
1021
+ event.preventDefault();
1022
+ this._capturePointer(event.pointerId);
1011
1023
  this._pointer = {
1012
1024
  type: 'selection',
1013
1025
  pointerId: event.pointerId,
@@ -1041,7 +1053,13 @@ export class Calendar extends Component {
1041
1053
  const wrapper = pointer.control.closest('.zx-calendar__event');
1042
1054
  if (wrapper) {
1043
1055
  wrapper.dataset.state = 'dragging';
1044
- wrapper.style.transform = `translate(${event.clientX - pointer.startX}px, ${event.clientY - pointer.startY}px)`;
1056
+ if (pointer.action === 'resize') {
1057
+ wrapper.style.transform = '';
1058
+ const height = this._timedPreviewHeight(proposed);
1059
+ if (height) wrapper.style.blockSize = height;
1060
+ } else {
1061
+ wrapper.style.transform = `translate(${event.clientX - pointer.startX}px, ${event.clientY - pointer.startY}px)`;
1062
+ }
1045
1063
  wrapper.dataset.preview = this._eventTime(proposed, proposed.start);
1046
1064
  }
1047
1065
  this._announce(this.msg('editPreview', proposed.title,
@@ -1053,6 +1071,7 @@ export class Calendar extends Component {
1053
1071
  const pointer = this._pointer;
1054
1072
  if (!pointer || pointer.pointerId !== event.pointerId) return;
1055
1073
  this._pointer = null;
1074
+ this._releasePointerCapture(pointer.pointerId);
1056
1075
  this._clearSelectionPreview();
1057
1076
  if (!pointer.moved) return;
1058
1077
  event.preventDefault();
@@ -1071,6 +1090,7 @@ export class Calendar extends Component {
1071
1090
  _cancelPointer(event) {
1072
1091
  if (!this._pointer || this._pointer.pointerId !== event.pointerId) return;
1073
1092
  const control = this._pointer.control;
1093
+ this._releasePointerCapture(this._pointer.pointerId);
1074
1094
  this._pointer = null;
1075
1095
  this._clearSelectionPreview();
1076
1096
  control.closest?.('.zx-calendar__event')?.removeAttribute('data-state');
@@ -1079,13 +1099,14 @@ export class Calendar extends Component {
1079
1099
 
1080
1100
  /** @param {CalendarEvent} event @param {'move'|'resize'} action @param {number} x @param {number} y @returns {CalendarEvent|null} */
1081
1101
  _proposalAtPoint(event, action, x, y) {
1082
- const timed = this._timeAtPoint(x, y);
1102
+ const resizeTimed = action === 'resize' && !calendarEventSpansDays(event);
1103
+ const timed = this._timeAtPoint(x, y, resizeTimed ? event.start : null);
1083
1104
  if (timed && !calendarEventSpansDays(event)) {
1084
1105
  if (action === 'move') {
1085
1106
  const duration = event.end.getTime() - event.start.getTime();
1086
1107
  const next = cloneCalendarEvent(event);
1087
- next.start = timed;
1088
- next.end = new Date(timed.getTime() + duration);
1108
+ next.start = addMinutes(timed, -Number(this._pointer?.timeOffsetMinutes ?? 0));
1109
+ next.end = new Date(next.start.getTime() + duration);
1089
1110
  return next;
1090
1111
  }
1091
1112
  const next = cloneCalendarEvent(event);
@@ -1094,7 +1115,8 @@ export class Calendar extends Component {
1094
1115
  }
1095
1116
  const day = this._dayAtPoint(x, y);
1096
1117
  if (!day) return null;
1097
- const difference = calendarDayDifference(startOfCalendarDay(event.start), day);
1118
+ const targetDay = addCalendarDays(day, -Number(this._pointer?.dayOffset ?? 0));
1119
+ const difference = calendarDayDifference(startOfCalendarDay(event.start), targetDay);
1098
1120
  if (action === 'move') return shiftCalendarEvent(event, { days: difference, minutes: 0 });
1099
1121
  const next = cloneCalendarEvent(event);
1100
1122
  const endDay = addCalendarDays(day, 1);
@@ -1102,15 +1124,23 @@ export class Calendar extends Component {
1102
1124
  return next.end > next.start ? next : null;
1103
1125
  }
1104
1126
 
1105
- /** @param {number} x @param {number} y @returns {Date|null} */
1106
- _timeAtPoint(x, y) {
1127
+ /** @param {number} x @param {number} y @param {Date|null} [fixedDay=null] @returns {Date|null} */
1128
+ _timeAtPoint(x, y, fixedDay = null) {
1107
1129
  const slots = this.refs.body.querySelector('.zx-calendar__slots');
1108
1130
  if (!slots) return null;
1109
1131
  const rect = slots.getBoundingClientRect();
1110
- if (x < rect.left || x > rect.right || y < rect.top || y > rect.bottom || rect.width <= 0 || rect.height <= 0) return null;
1132
+ const scroller = slots.closest('.zx-calendar__time-scroller');
1133
+ const viewport = scroller?.getBoundingClientRect();
1134
+ const left = viewport ? Math.max(rect.left, viewport.left) : rect.left;
1135
+ const right = viewport ? Math.min(rect.right, viewport.right) : rect.right;
1136
+ const top = viewport ? Math.max(rect.top, viewport.top) : rect.top;
1137
+ const bottom = viewport ? Math.min(rect.bottom, viewport.bottom) : rect.bottom;
1138
+ if (x < left || x > right || y < top || y > bottom || rect.width <= 0 || rect.height <= 0) return null;
1111
1139
  const columns = Number(slots.closest('[role="grid"]')?.getAttribute('aria-colcount')) || 1;
1112
1140
  const rows = Number(slots.closest('[role="grid"]')?.getAttribute('aria-rowcount')) || 1;
1113
- const column = Math.max(0, Math.min(columns - 1, Math.floor((x - rect.left) / rect.width * columns)));
1141
+ const fixedColumn = fixedDay ? this._timeGridDayIndex(fixedDay) : -1;
1142
+ const column = fixedColumn >= 0 ? fixedColumn
1143
+ : Math.max(0, Math.min(columns - 1, Math.floor((x - rect.left) / rect.width * columns)));
1114
1144
  const row = Math.max(0, Math.min(rows - 1, Math.floor((y - rect.top) / rect.height * rows)));
1115
1145
  const target = slots.querySelector(`[data-row-index="${row}"][data-day-index="${column}"]`);
1116
1146
  return target ? this._dateFromTarget(target) : null;
@@ -1118,11 +1148,6 @@ export class Calendar extends Component {
1118
1148
 
1119
1149
  /** @param {number} x @param {number} y @returns {Date|null} */
1120
1150
  _dayAtPoint(x, y) {
1121
- const elements = typeof document.elementsFromPoint === 'function' ? document.elementsFromPoint(x, y) : [];
1122
- for (const element of elements) {
1123
- const date = element instanceof HTMLElement ? element.dataset.date : null;
1124
- if (date) return parseDayKey(date);
1125
- }
1126
1151
  const allDay = this.refs.body.querySelector('.zx-calendar__all-day-grid');
1127
1152
  if (allDay) {
1128
1153
  const rect = allDay.getBoundingClientRect();
@@ -1132,9 +1157,79 @@ export class Calendar extends Component {
1132
1157
  return parseDayKey(cells[index]?.dataset.date);
1133
1158
  }
1134
1159
  }
1160
+ for (const monthEvents of this.refs.body.querySelectorAll('.zx-calendar__month-events')) {
1161
+ const rect = monthEvents.getBoundingClientRect();
1162
+ if (x < rect.left || x > rect.right || y < rect.top || y > rect.bottom || rect.width <= 0) continue;
1163
+ const start = parseDayKey(monthEvents.closest('.zx-calendar__month-week')?.dataset.weekStart);
1164
+ if (!start) continue;
1165
+ const index = Math.max(0, Math.min(6, Math.floor((x - rect.left) / rect.width * 7)));
1166
+ return addCalendarDays(start, index);
1167
+ }
1168
+ for (const agendaDay of this.refs.body.querySelectorAll('.zx-calendar__agenda-day')) {
1169
+ const rect = agendaDay.getBoundingClientRect();
1170
+ if (x < rect.left || x > rect.right || y < rect.top || y > rect.bottom) continue;
1171
+ return parseDayKey(agendaDay.dataset.date);
1172
+ }
1173
+ const elements = typeof document.elementsFromPoint === 'function' ? document.elementsFromPoint(x, y) : [];
1174
+ for (const element of elements) {
1175
+ const dateTarget = element instanceof HTMLElement ? element.closest('[data-date]') : null;
1176
+ const date = dateTarget instanceof HTMLElement ? dateTarget.dataset.date : null;
1177
+ if (date) return parseDayKey(date);
1178
+ }
1135
1179
  return null;
1136
1180
  }
1137
1181
 
1182
+ /** @param {CalendarEvent} event @param {number} x @param {number} y @returns {{minutes: number, days: number}} */
1183
+ _dragOffsetForEvent(event, x, y) {
1184
+ if (!calendarEventSpansDays(event)) {
1185
+ const timed = this._timeAtPoint(x, y);
1186
+ if (timed) {
1187
+ const slot = normalizeSlot(this.options.slotDuration);
1188
+ const duration = Math.max(0, Math.round((event.end.getTime() - event.start.getTime()) / 60_000));
1189
+ const maxOffset = Math.max(0, duration - slot);
1190
+ const minutes = Math.round((timed.getTime() - event.start.getTime()) / 60_000);
1191
+ return { minutes: Math.max(0, Math.min(maxOffset, minutes)), days: 0 };
1192
+ }
1193
+ }
1194
+ const day = this._dayAtPoint(x, y);
1195
+ const days = day ? calendarDayDifference(startOfCalendarDay(event.start), day) : 0;
1196
+ return { minutes: 0, days: Math.max(0, days) };
1197
+ }
1198
+
1199
+ /** @param {Date} day @returns {number} */
1200
+ _timeGridDayIndex(day) {
1201
+ const key = calendarDayKey(day);
1202
+ const headings = [...this.refs.body.querySelectorAll('.zx-calendar__day-heading')];
1203
+ return headings.findIndex((heading) => heading.dataset.date === key);
1204
+ }
1205
+
1206
+ /** @param {CalendarEvent} event @returns {string|null} */
1207
+ _timedPreviewHeight(event) {
1208
+ const grid = this.refs.body.querySelector('.zx-calendar__time-grid');
1209
+ if (!grid || calendarEventSpansDays(event)) return null;
1210
+ const min = normalizeMinute(this.options.slotMinTime, 0);
1211
+ const max = normalizeMinute(this.options.slotMaxTime, 1440, true);
1212
+ const slot = normalizeSlot(this.options.slotDuration);
1213
+ const start = event.start.getHours() * 60 + event.start.getMinutes();
1214
+ const end = event.end.getHours() * 60 + event.end.getMinutes();
1215
+ const minutes = Math.max(slot, Math.min(max, end) - Math.max(min, start));
1216
+ return `${(minutes / (max - min)) * 100}%`;
1217
+ }
1218
+
1219
+ /** @param {number} pointerId @returns {void} */
1220
+ _capturePointer(pointerId) {
1221
+ try {
1222
+ this.el.setPointerCapture?.(pointerId);
1223
+ } catch {
1224
+ // Synthetic PointerEvents used by smoke tests do not create an active pointer capture target.
1225
+ }
1226
+ }
1227
+
1228
+ /** @param {number} pointerId @returns {void} */
1229
+ _releasePointerCapture(pointerId) {
1230
+ if (this.el.hasPointerCapture?.(pointerId)) this.el.releasePointerCapture?.(pointerId);
1231
+ }
1232
+
1138
1233
  /** @param {Date} start @param {Date} end @returns {void} */
1139
1234
  _previewSelection(start, end) {
1140
1235
  const low = Math.min(start.getTime(), end.getTime());