@zeyos/zx 4.3.0 → 4.3.4

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 (34) hide show
  1. package/CHANGELOG.md +111 -1
  2. package/README.md +0 -3
  3. package/dist/types/components/calendar/calendar-model.d.ts +22 -0
  4. package/dist/types/components/card-view/card-view.d.ts +1 -1
  5. package/dist/types/components/card-view/record-card.d.ts +47 -5
  6. package/dist/types/components/kanban-view/kanban-policy.d.ts +268 -0
  7. package/dist/types/components/kanban-view/kanban-view.d.ts +669 -24
  8. package/dist/types/core/component.d.ts +6 -1
  9. package/dist/types/index.d.ts +2 -1
  10. package/dist/zx-zeyos.esm.js +35 -11
  11. package/dist/zx-zeyos.esm.min.js +1 -1
  12. package/dist/zx.css +597 -78
  13. package/dist/zx.esm.js +1758 -238
  14. package/dist/zx.esm.min.js +3 -3
  15. package/dist/zx.global.js +1758 -238
  16. package/dist/zx.global.min.js +3 -3
  17. package/dist/zx.min.css +1 -1
  18. package/docs/api.json +214 -2
  19. package/docs/llms.md +30 -30
  20. package/docs/llms.txt +0 -2
  21. package/package.json +1 -2
  22. package/src/components/calendar/calendar-model.js +45 -0
  23. package/src/components/calendar/calendar.css +30 -10
  24. package/src/components/calendar/calendar.js +8 -10
  25. package/src/components/card-view/card-view.css +211 -9
  26. package/src/components/card-view/card-view.js +7 -7
  27. package/src/components/card-view/record-card.js +92 -19
  28. package/src/components/kanban-view/kanban-policy.js +412 -0
  29. package/src/components/kanban-view/kanban-view.css +480 -36
  30. package/src/components/kanban-view/kanban-view.js +1514 -235
  31. package/src/components/message/message.css +9 -10
  32. package/src/core/component.js +8 -3
  33. package/src/index.js +8 -3
  34. package/DESIGN-SYSTEM.md +0 -182
package/docs/api.json CHANGED
@@ -1468,7 +1468,7 @@
1468
1468
  {
1469
1469
  "name": "minCardWidth",
1470
1470
  "type": "number|string",
1471
- "default": "'18rem'",
1471
+ "default": "'15rem'",
1472
1472
  "description": "Responsive minimum card width."
1473
1473
  },
1474
1474
  {
@@ -4330,6 +4330,132 @@
4330
4330
  "default": "'local'",
4331
4331
  "description": "Whether accepted moves update local data."
4332
4332
  },
4333
+ {
4334
+ "name": "dragFrom",
4335
+ "type": "'card'|'handle'",
4336
+ "default": "'card'",
4337
+ "description": "Whether the whole card or only its handle starts a pointer drag. Touch drags from the card body require a long press so the board still scrolls."
4338
+ },
4339
+ {
4340
+ "name": "multiMove",
4341
+ "type": "boolean",
4342
+ "default": "true",
4343
+ "description": "Move the whole selection when a selected card is picked up."
4344
+ },
4345
+ {
4346
+ "name": "autoScroll",
4347
+ "type": "boolean",
4348
+ "default": "true",
4349
+ "description": "Scroll the board and columns while dragging near an edge."
4350
+ },
4351
+ {
4352
+ "name": "wipPolicy",
4353
+ "type": "'warn'|'block'",
4354
+ "default": "'warn'",
4355
+ "description": "Whether exceeding a limit warns or refuses."
4356
+ },
4357
+ {
4358
+ "name": "rules",
4359
+ "type": "KanbanRule[]",
4360
+ "default": "[]",
4361
+ "description": "Ordered visual card rules."
4362
+ },
4363
+ {
4364
+ "name": "search",
4365
+ "type": "string",
4366
+ "default": "''",
4367
+ "description": "Board search text."
4368
+ },
4369
+ {
4370
+ "name": "searchFields",
4371
+ "type": "string[]|null",
4372
+ "default": "null",
4373
+ "description": "Field ids searched; null searches visible fields plus the title and subtitle sources."
4374
+ },
4375
+ {
4376
+ "name": "filter",
4377
+ "type": "((record:KanbanRecord,index:number)=>boolean)|null",
4378
+ "default": "null",
4379
+ "description": "Extra card predicate."
4380
+ },
4381
+ {
4382
+ "name": "searchControl",
4383
+ "type": "boolean",
4384
+ "default": "false",
4385
+ "description": "Render the built-in board search control."
4386
+ },
4387
+ {
4388
+ "name": "history",
4389
+ "type": "boolean",
4390
+ "default": "true",
4391
+ "description": "Record committed local moves for undo and redo."
4392
+ },
4393
+ {
4394
+ "name": "historyLimit",
4395
+ "type": "number",
4396
+ "default": "50",
4397
+ "description": "Maximum remembered move steps."
4398
+ },
4399
+ {
4400
+ "name": "historyControls",
4401
+ "type": "boolean",
4402
+ "default": "false",
4403
+ "description": "Render built-in undo and redo controls."
4404
+ },
4405
+ {
4406
+ "name": "columnHeight",
4407
+ "type": "string|null",
4408
+ "default": "null",
4409
+ "description": "CSS length turning each column into a scroll region."
4410
+ },
4411
+ {
4412
+ "name": "allowAdd",
4413
+ "type": "boolean|((column:KanbanColumn,swimlane:KanbanSwimlane|null)=>boolean)",
4414
+ "default": "false",
4415
+ "description": "Render a per-column add control that emits `recordadd`."
4416
+ },
4417
+ {
4418
+ "name": "contextMenu",
4419
+ "type": "boolean|MenuItem[]|((context:KanbanCardContext)=>MenuItem[])",
4420
+ "default": "false",
4421
+ "description": "Card context menu. `true` offers move commands; an array or function appends application items."
4422
+ },
4423
+ {
4424
+ "name": "renderCard",
4425
+ "type": "((context:KanbanCardContext)=>Node|null|undefined)|null",
4426
+ "default": "null",
4427
+ "description": "Replaces card content."
4428
+ },
4429
+ {
4430
+ "name": "renderColumnHeader",
4431
+ "type": "((context:KanbanHeaderContext)=>Node|null|undefined)|null",
4432
+ "default": "null",
4433
+ "description": "Replaces column header content."
4434
+ },
4435
+ {
4436
+ "name": "renderSwimlaneHeader",
4437
+ "type": "((context:KanbanHeaderContext)=>Node|null|undefined)|null",
4438
+ "default": "null",
4439
+ "description": "Replaces lane header content."
4440
+ },
4441
+ {
4442
+ "name": "renderColumnEmpty",
4443
+ "type": "((context:KanbanHeaderContext)=>Node|null|undefined)|null",
4444
+ "default": "null",
4445
+ "description": "Empty-column placeholder."
4446
+ },
4447
+ {
4448
+ "name": "renderDragPreview",
4449
+ "type": "((context:{records:KanbanRecord[],ids:unknown[],count:number})=>Node|null|undefined)|null",
4450
+ "default": "null",
4451
+ "description": "Replaces the floating drag preview."
4452
+ },
4453
+ {
4454
+ "name": "labels",
4455
+ "type": "Record<string,string>",
4456
+ "default": "{}",
4457
+ "description": "Overrides for every visible and announced string."
4458
+ },
4333
4459
  {
4334
4460
  "name": "columnOrder",
4335
4461
  "type": "string[]",
@@ -4385,10 +4511,30 @@
4385
4511
  "payload": "Record<string,unknown>",
4386
4512
  "description": ""
4387
4513
  },
4514
+ {
4515
+ "name": "movereject",
4516
+ "payload": "Record<string,unknown>",
4517
+ "description": ""
4518
+ },
4388
4519
  {
4389
4520
  "name": "recordaction",
4390
4521
  "payload": "Record<string,unknown>",
4391
4522
  "description": ""
4523
+ },
4524
+ {
4525
+ "name": "recordadd",
4526
+ "payload": "Record<string,unknown>",
4527
+ "description": ""
4528
+ },
4529
+ {
4530
+ "name": "searchchange",
4531
+ "payload": "{search:string}",
4532
+ "description": ""
4533
+ },
4534
+ {
4535
+ "name": "historychange",
4536
+ "payload": "{canUndo:boolean,canRedo:boolean,depth:{undo:number,redo:number}}",
4537
+ "description": ""
4392
4538
  }
4393
4539
  ],
4394
4540
  "methods": [
@@ -4492,7 +4638,73 @@
4492
4638
  "name": "moveRecord",
4493
4639
  "signature": "moveRecord(id, destination)",
4494
4640
  "returns": "this",
4495
- "description": "Proposes and, in local mode, commits a record move. Destination acceptance is checked before a cancelable `recordmove` event. WIP limits are reported but remain advisory."
4641
+ "description": "Proposes and, in local mode, commits a record move. Equivalent to `moveRecords([id], …)`."
4642
+ },
4643
+ {
4644
+ "name": "moveRecords",
4645
+ "signature": "moveRecords(ids, destination)",
4646
+ "returns": "this",
4647
+ "description": "Proposes and, in local mode, commits an atomic move of one or more records. Destination eligibility, transition rules, and work-in-progress policy are all checked before a single cancelable `recordmove` event; the move commits completely or not at all, and the moving records keep their relative order at the destination."
4648
+ },
4649
+ {
4650
+ "name": "canUndo",
4651
+ "signature": "canUndo()",
4652
+ "returns": "boolean",
4653
+ "description": "Whether a committed local move can be reverted."
4654
+ },
4655
+ {
4656
+ "name": "canRedo",
4657
+ "signature": "canRedo()",
4658
+ "returns": "boolean",
4659
+ "description": "Whether a reverted local move can be reapplied."
4660
+ },
4661
+ {
4662
+ "name": "undo",
4663
+ "signature": "undo()",
4664
+ "returns": "this",
4665
+ "description": "Reverts the last committed local move, restoring the exact record order and selection."
4666
+ },
4667
+ {
4668
+ "name": "redo",
4669
+ "signature": "redo()",
4670
+ "returns": "this",
4671
+ "description": "Reapplies the last reverted local move."
4672
+ },
4673
+ {
4674
+ "name": "clearHistory",
4675
+ "signature": "clearHistory()",
4676
+ "returns": "this",
4677
+ "description": "Empties both history stacks."
4678
+ },
4679
+ {
4680
+ "name": "getSearch",
4681
+ "signature": "getSearch()",
4682
+ "returns": "string",
4683
+ "description": "Returns the current board search text."
4684
+ },
4685
+ {
4686
+ "name": "setSearch",
4687
+ "signature": "setSearch(search, [options])",
4688
+ "returns": "this",
4689
+ "description": "Narrows visible cards to those matching a search. Hidden records keep their position in the data, so a card dropped between two visible cards still lands between them."
4690
+ },
4691
+ {
4692
+ "name": "setFilter",
4693
+ "signature": "setFilter(filter)",
4694
+ "returns": "this",
4695
+ "description": "Replaces the extra card predicate. Pass null to clear it."
4696
+ },
4697
+ {
4698
+ "name": "getVisibleRecords",
4699
+ "signature": "getVisibleRecords()",
4700
+ "returns": "KanbanRecord[]",
4701
+ "description": "Returns the records currently rendered, in board order."
4702
+ },
4703
+ {
4704
+ "name": "setData",
4705
+ "signature": "setData(records)",
4706
+ "returns": "this",
4707
+ "description": "Replaces every record. History is dropped because its snapshots describe records the host has just replaced, and reinstating them would resurrect data the application no longer owns."
4496
4708
  },
4497
4709
  {
4498
4710
  "name": "getViewState",
package/docs/llms.md CHANGED
@@ -31,6 +31,8 @@ const t = new Table(target, options);
31
31
  (always an object, e.g. `{ value, item }`).
32
32
  - Every `emit` also dispatches a **bubbling, composed** `zx-<type>` `CustomEvent` on `t.el`, so
33
33
  `t.el.addEventListener('zx-change', …)` and event delegation work.
34
+ - Mutation boundaries may opt into honoring cancellation of that mirrored DOM event; Kanban's
35
+ `zx-recordmove` does, so delegated hosts can veto a move without a component reference.
34
36
  - `t.destroy()` aborts all listeners (one AbortController), unregisters the element, and removes an
35
37
  owned root. `Component.from(el)` returns the component for a root element (replaces the legacy
36
38
  `el.retrieve('com')`).
@@ -253,28 +255,6 @@ reflection and ElementInternals form association: `<zx-toggle>`, `<zx-check-butt
253
255
  `<zx-checklist>`, `<zx-datebox>`, `<zx-timebox>`, `<zx-search>`, `<zx-groupbox>`, `<zx-tabbox>`,
254
256
  `<zx-table>`, `<zx-dialog>`.
255
257
 
256
- ## Conventions (editing component source)
257
-
258
- Follow `AGENTS.md`. Key rules: never declare instance class fields for state used in `render()`
259
- (render runs inside the base constructor before field initializers); route all DOM listeners
260
- through `this.listen()`; no `innerHTML` outside `h.raw()`; express state via ARIA/`data-*`, not
261
- state classes; small glyph buttons use the shared `.zx-icon-btn` utility; follow the named APG
262
- pattern with `:focus-visible` rings and `prefers-reduced-motion` guards.
263
-
264
- ## File map & commands
265
-
266
- ```
267
- src/components/<name>/ component + CSS src/core/ kernel (component, dom, http, i18n, date…)
268
- website/ marketing page + docs.html (docs.js) specs/ per-component specs
269
- website/demos/ per-component demo modules website/layouts/ application layout examples
270
- tests/ node unit + smoke dist/ built bundles
271
-
272
- npm run serve # http://127.0.0.1:8321/website/docs.html (no build)
273
- npm test # node --test tests/unit/*.test.js + node tests/lint-tokens.js
274
- npm run build # dist/: ESM, global, ZeyOS binding, and CSS assets
275
- node tools/build-module-tokens.js # regenerate styles/tokens/modules.css from src/zeyos/modules.js
276
- ```
277
-
278
258
  ---
279
259
 
280
260
  # Components
@@ -1151,8 +1131,15 @@ portable state as TableView and KanbanView.
1151
1131
  `previewAlt`, `link`, `actions`, `groupBy`, `groupOrder`, `minCardWidth`, `maxColumns`,
1152
1132
  `variant: 'outlined'|'raised'|'filled'`, `loadingCount`, `headingLevel`, and `label`.
1153
1133
  - **Cards** — each record is a focusable `<li>` with a native title link, separate secondary
1154
- actions, optional native multi-select checkbox, labelled metadata, and `aria-selected` state.
1155
- Title/subtitle fields are not duplicated in metadata unless their field sets `duplicate:true`.
1134
+ actions, optional native multi-select checkbox, labelled metadata, and a global ARIA selection
1135
+ description that retains valid listitem semantics.
1136
+ CardView uses a dense collection rhythm: titles clamp to two lines, previews stay at
1137
+ thumbnail/avatar scale, and visible fields become compact labelled metadata chips. Title/subtitle
1138
+ fields are not duplicated in metadata unless their field sets `duplicate:true`.
1139
+ - **Shared card slots** — a visible field may set
1140
+ `view.card.slot: 'eyebrow-start'|'eyebrow-end'|'title-prefix'`. These slots create a semantic
1141
+ eyebrow row or put labelled content immediately before (and outside) the title heading/link.
1142
+ Unknown slots remain ordinary metadata; visibility and field order still control rendering.
1156
1143
  - **Media and URLs** — `preview` accepts a field ID or callback returning an image URL,
1157
1144
  `{src,alt,fit}`, Node, or null. Executable/non-image schemes are rejected and image failures show
1158
1145
  a stable fallback. Title/action links are normalized; `_blank` links receive `noopener`.
@@ -1176,14 +1163,25 @@ cards, advisory work-in-progress limits, and equivalent pointer/programmatic/key
1176
1163
  columns may also declare an advisory `limit`.
1177
1164
  - **Methods** — configure/get/move columns and swim lanes, collapse either axis, and
1178
1165
  `moveRecord(id,{column?,lane?,index?})`. Board-specific order/collapse joins the common view
1179
- state; record order, movement history, and selection do not.
1166
+ state; record order, movement history, and selection do not. Saved preferences for data-derived
1167
+ axes survive an empty/partial result set and reconcile as later records introduce their IDs.
1168
+ - **Dense board layout** — bounded muted columns use a stable 20rem rail with native horizontal
1169
+ overflow, 48px headings, compact counts, 8–12px card padding, and 8px card gaps. Surface cards
1170
+ use an optional square thumbnail and a two-tier hierarchy from shared card field slots:
1171
+ `eyebrow-start`, `eyebrow-end`, and `title-prefix`. Unslotted metadata stays as quiet inline
1172
+ facts. Titles clamp visually to two lines while retaining their complete accessible text. Move
1173
+ and action controls reveal on hover/focus/selection/grab, and remain visible on touch devices.
1180
1174
  - **Movement boundary** — `recordmove {record,id,from,to,column,swimlane,limitExceeded}` is
1181
- cancelable and fires before a commit/request. `accept` is a hard eligibility gate; WIP is visual
1182
- advice. Local mode clones the moved record and never mutates host data. External mode emits only,
1183
- for server validation and optimistic rollback owned by ZeyOS.
1175
+ cancelable and fires before a commit/request; either the component event or bubbling
1176
+ `zx-recordmove` mirror may veto it. `accept` is a hard eligibility gate; WIP is visual advice.
1177
+ Local mode clones the moved record and never mutates host data, and a manual local move clears an
1178
+ active local sort so saved state matches visible order. External mode emits only, for server
1179
+ validation and optimistic rollback owned by ZeyOS.
1184
1180
  - **Keyboard** — focus the move handle; Enter/Space grabs or drops, Escape cancels, Left/Right
1185
1181
  chooses a column, Up/Down chooses a bucket-relative position, and Alt+Up/Down chooses a swim
1186
- lane. A polite live region announces every target and result. Native drag is an enhancement.
1182
+ lane. A polite live region announces every target, result, and reached/exceeded WIP limit.
1183
+ Collapse keeps focus on its control, including when a programmatic collapse hides the focused
1184
+ card. Native drag is an enhancement.
1187
1185
  <!-- /doc -->
1188
1186
 
1189
1187
  <!-- doc:grid -->
@@ -1256,7 +1254,9 @@ windowed loader.
1256
1254
  HTML, and suspicious CSS colour strings are ignored.
1257
1255
  - **Methods** — `getEvents()`, `setEvents()`, `addEvent()`, `updateEvent()`, `removeEvent()`,
1258
1256
  `getDate()`, `setDate()`, `getView()`, `setView()`, `prev()`, `next()`, `today()`,
1259
- `setLoading()`, `focusEvent()`, `destroy()`.
1257
+ `setLoading()`, `focusEvent()`, `destroy()`. `prev()`/`next()` step one visible period and clamp
1258
+ the day of month into the target month, so a 29th–31st anchor cannot skip a shorter month; the
1259
+ same arithmetic is exported as `calendarPageDate()` and `addCalendarMonths()`.
1260
1260
  - **Events** — `dateschange {view,start,end}`, `viewchange {view,oldView}`, `eventclick`,
1261
1261
  `eventdblclick`, `dateclick {date,allDay,view}`, `select {start,end,allDay,view}`,
1262
1262
  `new {date,view}`, `moreclick {date,count}`, `eventschange {events}`, and cancelable
package/docs/llms.txt CHANGED
@@ -16,9 +16,7 @@ exports and as `window.zx.*`.
16
16
  - [Documentation site](https://zx.zeyos.com/docs.html): getting started, every component with a live demo and its JavaScript source, and the application layouts. Machine index: [llms.txt](https://zx.zeyos.com/llms.txt).
17
17
  - [README](../README.md): what Zx is, quick start (ESM + global script), component index, theming, and browser support.
18
18
  - [API reference](llms.md): per-component constructor options, methods, and events for all public components plus the core helpers.
19
- - [Skill](https://zx.zeyos.com/skills/zx/SKILL.md): task-oriented guide — mental model, recipes, ZeyOS HTTP client, theming, gotchas, commands.
20
19
  - [Changelog](../CHANGELOG.md): what changed in each release, including behaviour changes an upgrade has to account for.
21
- - [Contributor contract](https://github.com/zeyos/zx/blob/main/AGENTS.md): conventions for editing component source (token rules, render-time state, listeners, ARIA/APG).
22
20
 
23
21
  ## Components
24
22
  - Inputs: `button()`, `buttonGroup()`, `badge()`/`badgeGroup()`, `CheckButton`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeyos/zx",
3
- "version": "4.3.0",
3
+ "version": "4.3.4",
4
4
  "description": "Dependency-free JavaScript implementation of the Xenon Design System.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -65,7 +65,6 @@
65
65
  "docs/api.json",
66
66
  "CHANGELOG.md",
67
67
  "README.md",
68
- "DESIGN-SYSTEM.md",
69
68
  "LICENSE"
70
69
  ],
71
70
  "engines": {
@@ -70,6 +70,51 @@ export function calendarRange(view, anchor, options = {}) {
70
70
  return { start, end };
71
71
  }
72
72
 
73
+ /**
74
+ * Returns the anchor date one visible period away from `anchor`.
75
+ *
76
+ * Month and year steps clamp the day of month into the target month instead of letting it
77
+ * overflow: `setMonth()` alone turns 31 January into 3 March, which makes February unreachable
78
+ * from a month-end anchor and leaves `prev()` on 31 March landing back inside March.
79
+ * @param {CalendarView} view Current view.
80
+ * @param {Date} anchor Current anchor date.
81
+ * @param {number} direction -1 for the previous period, 1 for the next.
82
+ * @param {{agendaDays?: number}} [options={}] Paging options.
83
+ * @returns {Date}
84
+ */
85
+ export function calendarPageDate(view, anchor, direction, options = {}) {
86
+ const date = validDate(anchor, 'Calendar date');
87
+ const step = integer(direction, 'direction');
88
+ if (view === 'day') date.setDate(date.getDate() + step);
89
+ else if (view === 'agenda') {
90
+ date.setDate(date.getDate() + step * positiveInteger(options.agendaDays ?? 14, 'agendaDays'));
91
+ } else if (view === 'week') date.setDate(date.getDate() + step * 7);
92
+ else if (view === 'month') return addCalendarMonths(date, step);
93
+ else if (view === 'year') return addCalendarMonths(date, step * 12);
94
+ else throw new RangeError(`Unknown calendar view: ${view}`);
95
+ return date;
96
+ }
97
+
98
+ /**
99
+ * Adds whole months, clamping the day of month to the last day the target month actually has.
100
+ * @param {Date} value Local date.
101
+ * @param {number} months Months to add.
102
+ * @returns {Date}
103
+ */
104
+ export function addCalendarMonths(value, months) {
105
+ const date = validDate(value, 'Calendar date');
106
+ const day = date.getDate();
107
+ date.setDate(1);
108
+ date.setMonth(date.getMonth() + integer(months, 'month amount'));
109
+ date.setDate(Math.min(day, daysInCalendarMonth(date.getFullYear(), date.getMonth())));
110
+ return date;
111
+ }
112
+
113
+ /** @param {number} year @param {number} month @returns {number} */
114
+ function daysInCalendarMonth(year, month) {
115
+ return new Date(year, month + 1, 0).getDate();
116
+ }
117
+
73
118
  /**
74
119
  * Normalizes caller records atomically into the Calendar event contract.
75
120
  * @param {Record<string, any>[]} records Source records.
@@ -9,15 +9,20 @@
9
9
  min-inline-size: 0;
10
10
  overflow: hidden;
11
11
  border: 1px solid var(--zx-color-border);
12
- border-radius: var(--zx-radius-lg);
12
+ border-radius: 0;
13
13
  background: var(--zx-color-bg-surface);
14
14
  color: var(--zx-color-text);
15
15
  font-size: var(--zx-text-sm);
16
16
  }
17
17
 
18
+ /*
19
+ * The heading is the only elastic track. With `1fr auto 1fr` the end-justified commands group
20
+ * kept its min-content width inside a narrower `1fr` track and bled back over the centered
21
+ * heading; giving navigation and commands their natural width lets the heading ellipsize instead.
22
+ */
18
23
  .zx-calendar__toolbar {
19
24
  display: grid;
20
- grid-template-columns: 1fr auto 1fr;
25
+ grid-template-columns: auto minmax(0, 1fr) auto;
21
26
  align-items: center;
22
27
  gap: var(--zx-space-3);
23
28
  min-inline-size: 0;
@@ -35,7 +40,14 @@
35
40
 
36
41
  .zx-calendar__navigation { justify-self: start; gap: var(--zx-space-1); }
37
42
  .zx-calendar__commands { justify-self: end; gap: var(--zx-space-3); }
38
- .zx-calendar__views { overflow-x: auto; }
43
+ .zx-calendar__views {
44
+ block-size: var(--zx-control-height);
45
+ gap: 2px;
46
+ overflow-x: auto;
47
+ border-radius: 0;
48
+ background: var(--zx-color-bg-muted);
49
+ padding: 3px;
50
+ }
39
51
 
40
52
  .zx-calendar__toolbar-button,
41
53
  .zx-calendar__view-button,
@@ -59,15 +71,21 @@
59
71
  }
60
72
 
61
73
  .zx-calendar__toolbar-button:has(.zx-icon) { padding-inline: var(--zx-space-2); }
62
- .zx-calendar__view-button { border-inline-end-width: 0; padding-inline: var(--zx-space-3); }
63
- .zx-calendar__view-button:first-child { border-start-start-radius: var(--zx-control-radius); border-end-start-radius: var(--zx-control-radius); }
64
- .zx-calendar__view-button:last-child { border-inline-end-width: 1px; border-start-end-radius: var(--zx-control-radius); border-end-end-radius: var(--zx-control-radius); }
74
+ .zx-calendar__view-button {
75
+ block-size: 100%;
76
+ min-block-size: 0;
77
+ border-color: transparent;
78
+ border-radius: 0;
79
+ background: transparent;
80
+ padding-inline: var(--zx-space-3);
81
+ }
65
82
  .zx-calendar__view-button[aria-pressed="true"] {
66
83
  position: relative;
67
84
  z-index: 1;
68
- border-color: var(--zx-color-accent);
69
- background: var(--zx-color-bg-selected);
70
- color: var(--zx-color-accent);
85
+ border-color: var(--zx-color-border-control);
86
+ background: var(--zx-color-bg-control);
87
+ box-shadow: var(--zx-shadow-2);
88
+ color: var(--zx-color-text);
71
89
  }
72
90
 
73
91
  .zx-calendar__new {
@@ -81,7 +99,7 @@
81
99
  }
82
100
 
83
101
  .zx-calendar__toolbar-button:hover:not(:disabled),
84
- .zx-calendar__view-button:hover:not(:disabled) { background: var(--zx-color-bg-hover); }
102
+ .zx-calendar__view-button:hover:not(:disabled):not([aria-pressed="true"]) { background: var(--zx-color-bg-hover); }
85
103
  .zx-calendar__new:hover:not(:disabled) { background: var(--zx-color-accent-hover); }
86
104
  .zx-calendar button:disabled { cursor: default; opacity: .55; }
87
105
 
@@ -148,6 +166,7 @@
148
166
  min-block-size: calc(var(--zx-control-height) + var(--zx-space-2));
149
167
  place-content: center;
150
168
  border: 0;
169
+ border-radius: 0;
151
170
  border-inline-end: 1px solid var(--zx-color-border);
152
171
  background: var(--zx-color-bg-surface);
153
172
  color: var(--zx-color-text);
@@ -227,6 +246,7 @@
227
246
  .zx-calendar__slot {
228
247
  min-inline-size: 0;
229
248
  border: 0;
249
+ border-radius: 0;
230
250
  border-block-start: 1px solid var(--zx-color-border);
231
251
  border-inline-end: 1px solid var(--zx-color-border);
232
252
  background: var(--zx-color-bg-surface);
@@ -3,8 +3,8 @@ import { h } from '../../core/dom.js';
3
3
  import { icon } from '../../core/icons.js';
4
4
  import {
5
5
  CALENDAR_VIEWS, addCalendarDays, calendarDayDifference, calendarDayKey,
6
- calendarEventIntersects, calendarEventSpansDays, calendarRange, cloneCalendarEvent,
7
- layoutCalendarSpans, layoutTimedCalendarEvents, normalizeCalendarEvents,
6
+ calendarEventIntersects, calendarEventSpansDays, calendarPageDate, calendarRange,
7
+ cloneCalendarEvent, layoutCalendarSpans, layoutTimedCalendarEvents, normalizeCalendarEvents,
8
8
  shiftCalendarEvent, startOfCalendarDay
9
9
  } from './calendar-model.js';
10
10
 
@@ -289,7 +289,9 @@ export class Calendar extends Component {
289
289
  ? changes.durationEditable : current.durationEditable,
290
290
  data: changes.data ?? current.data
291
291
  };
292
- const [next] = normalizeCalendarEvents([source]);
292
+ // `source` is assembled from canonical keys, so the configured readers must not be reapplied
293
+ // here — but `dateUnit` must, or a numeric change is read in the wrong unit.
294
+ const [next] = normalizeCalendarEvents([source], { dateUnit: this.options.dateUnit });
293
295
  next.data = source.data;
294
296
  this._events = this._events.map((event, eventIndex) => eventIndex === index ? next : event);
295
297
  this._mutationVersions.delete(String(id));
@@ -1335,13 +1337,9 @@ export class Calendar extends Component {
1335
1337
 
1336
1338
  /** @param {number} direction @returns {this} */
1337
1339
  _page(direction) {
1338
- const date = new Date(this._date.getTime());
1339
- if (this._view === 'day') date.setDate(date.getDate() + direction);
1340
- else if (this._view === 'agenda') date.setDate(date.getDate() + direction * this.options.agendaDays);
1341
- else if (this._view === 'week') date.setDate(date.getDate() + direction * 7);
1342
- else if (this._view === 'month') date.setMonth(date.getMonth() + direction);
1343
- else date.setFullYear(date.getFullYear() + direction);
1344
- return this.setDate(date);
1340
+ return this.setDate(calendarPageDate(this._view, this._date, direction, {
1341
+ agendaDays: this.options.agendaDays
1342
+ }));
1345
1343
  }
1346
1344
 
1347
1345
  /** @param {{start: Date, end: Date}} range @returns {string} */