@jungherz-de/notionkit-elements 1.8.0 β†’ 1.10.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 (32) hide show
  1. package/README.md +13 -13
  2. package/dist/components/nk-app.js +8 -1
  3. package/dist/components/nk-btn.js +17 -3
  4. package/dist/components/nk-calendar-view.js +138 -0
  5. package/dist/components/nk-calendar.js +356 -0
  6. package/dist/components/nk-dialog.js +150 -0
  7. package/dist/components/nk-menu.js +2 -14
  8. package/dist/components/nk-peek.js +80 -5
  9. package/dist/components/nk-sidebar.js +68 -6
  10. package/dist/components/nk-table-view.js +9 -3
  11. package/dist/components/nk-tooltip.js +138 -0
  12. package/dist/components/nk-workspace-switcher.js +1 -1
  13. package/dist/components/shared/dates-a32DcW1l.js +67 -0
  14. package/dist/components/shared/floating-CxHxy-Cb.js +37 -0
  15. package/dist/notionkit-elements.esm.js +1102 -91
  16. package/dist/notionkit-elements.js +1105 -90
  17. package/dist/notionkit-elements.min.js +1 -1
  18. package/package.json +3 -3
  19. package/src/components/data/nk-calendar-view.js +136 -0
  20. package/src/components/data/nk-table-view.js +9 -3
  21. package/src/components/forms/nk-btn.js +17 -3
  22. package/src/components/forms/nk-calendar.js +354 -0
  23. package/src/components/overlays/nk-dialog.js +148 -0
  24. package/src/components/overlays/nk-menu.js +2 -14
  25. package/src/components/overlays/nk-peek.js +80 -5
  26. package/src/components/overlays/nk-tooltip.js +136 -0
  27. package/src/components/shell/nk-app.js +8 -1
  28. package/src/components/shell/nk-sidebar.js +68 -6
  29. package/src/components/shell/nk-workspace-switcher.js +1 -1
  30. package/src/index.js +4 -0
  31. package/src/util/dates.js +65 -0
  32. package/src/util/floating.js +35 -0
package/README.md CHANGED
@@ -1,17 +1,17 @@
1
1
  <h1 align="center">🧩 NotionKit Elements</h1>
2
2
 
3
3
  <p align="center">
4
- <a href="https://www.npmjs.com/package/@jungherz-de/notionkit-elements"><img src="https://img.shields.io/badge/version-1.8.0-2383e2?style=flat-square" alt="Version"></a>
4
+ <a href="https://www.npmjs.com/package/@jungherz-de/notionkit-elements"><img src="https://img.shields.io/badge/version-1.10.0-2383e2?style=flat-square" alt="Version"></a>
5
5
  <a href="#"><img src="https://img.shields.io/badge/vanilla_JS-no_dependencies-448361?style=flat-square" alt="Vanilla JS"></a>
6
- <a href="#"><img src="https://img.shields.io/badge/elements-83-529cca?style=flat-square" alt="83 Elements"></a>
6
+ <a href="#"><img src="https://img.shields.io/badge/elements-87-529cca?style=flat-square" alt="87 Elements"></a>
7
7
  <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-9065b0?style=flat-square" alt="MIT License"></a>
8
- <a href="CHANGELOG.md"><img src="https://img.shields.io/badge/changelog-v1.8.0-lightgrey?style=flat-square" alt="Changelog"></a>
8
+ <a href="CHANGELOG.md"><img src="https://img.shields.io/badge/changelog-v1.10.0-lightgrey?style=flat-square" alt="Changelog"></a>
9
9
  <a href="https://www.npmjs.com/package/@jungherz-de/notionkit-elements"><img src="https://img.shields.io/badge/npm-%40jungherz--de%2Fnotionkit--elements-cb3837?style=flat-square&logo=npm" alt="npm"></a>
10
10
  </p>
11
11
 
12
12
  <p align="center">
13
13
  <strong>Drop-in Web Components for <a href="https://github.com/JUNGHERZ/NotionKit">NotionKit CSS</a></strong><br>
14
- 83 vanilla JavaScript custom elements wrapping NotionKit's calm, document-centric workspace look.<br>
14
+ 87 vanilla JavaScript custom elements wrapping NotionKit's calm, document-centric workspace look.<br>
15
15
  Shadow DOM &middot; Native form participation &middot; Pixel parity with the class markup &middot; Zero dependencies.
16
16
  </p>
17
17
 
@@ -28,7 +28,7 @@
28
28
 
29
29
  ## ✨ What is NotionKit Elements?
30
30
 
31
- NotionKit Elements is the companion library to [NotionKit CSS](https://github.com/JUNGHERZ/NotionKit). It provides **83 Web Components** – from buttons and form controls over the sidebar, page tree and page shell with its properties to the settings modal, the bottom sheet, the side peek, ⌘K palette, database views with Notion’s toolbar and AI thread – that encapsulate the class markup of the foundation into simple, declarative custom elements.
31
+ NotionKit Elements is the companion library to [NotionKit CSS](https://github.com/JUNGHERZ/NotionKit). It provides **87 Web Components** – from buttons, form controls and the date picker over the sidebar, page tree and page shell with its properties to the settings modal, the bottom sheet, the side peek, dialogs, tooltips, ⌘K palette, database views – a calendar among them – with Notion’s toolbar and AI thread – that encapsulate the class markup of the foundation into simple, declarative custom elements.
32
32
 
33
33
  It is the **app layer** of the NotionKit family – three layers, one design language: [NotionKit](https://notionkit.jungherz.com) is the pure-CSS foundation, NotionKit Elements wraps it into web components for application UIs, and [NotionKit Web](https://notionkit-web.jungherz.com) is the Astro template for complete websites on top of the same foundation. The word stem stays the same across the layers: `.nk-callout` becomes `<nk-callout>`, a modifier class becomes an attribute, a state class becomes a boolean attribute.
34
34
 
@@ -56,10 +56,10 @@ It is the **app layer** of the NotionKit family – three layers, one design lan
56
56
  |---|---|
57
57
  | 🎯 **Pixel parity** | Every element renders identically to its class markup – verified by a Playwright pixel test in light and dark; the reference app differs by 0.00 % from the class version |
58
58
  | πŸ”Œ **Shadow DOM** | Each root adopts the NotionKit *components* sheet via `adoptedStyleSheets`; tokens are inherited from the document, so one `:root { --nk-accent: … }` re-brands everything |
59
- | 🧩 **83 Elements** | Forms with copy field and image picker, content blocks with bookmarks, app shell & tree with the ☰ drawer, page shell with properties, panels, avatars and steps, overlays (modal, sheet, side peek, ⌘K palette, menus that float in and become sheets on a phone, popover, emoji picker, toast), database table, board & list with toolbar and filter pills, comments, AI thread |
59
+ | 🧩 **87 Elements** | Forms with copy field, image picker and date picker, content blocks with bookmarks, app shell & tree with the ☰ drawer and the collapsing sidebar, page shell with properties, panels, avatars and steps, overlays (modal, sheet, dialog, resizable side peek, tooltip, ⌘K palette, menus that float in and become sheets on a phone, popover, emoji picker, toast), database table, board, list & calendar with toolbar and filter pills, comments, AI thread |
60
60
  | πŸŽ›οΈ **Form participation** | Input, textarea, select, switch, check, radio, slider, segmented and model card work natively with `<form>` via `ElementInternals`; radios group across shadow roots |
61
61
  | πŸŒ— **Theme sync** | One observer mirrors `data-theme` on `<html>` into every element |
62
- | πŸͺΆ **Lightweight** | 225 KB minified / 44 KB gzipped (IIFE, foundation sheet included), no external dependencies |
62
+ | πŸͺΆ **Lightweight** | 265 KB minified / 52 KB gzipped (IIFE, foundation sheet included), no external dependencies |
63
63
  | πŸ“¦ **Four bundle shapes** | IIFE, minified IIFE, ESM, and per-component ESM entries on a stable `base.js`, which import NotionKit's sheet instead of carrying a copy |
64
64
  | πŸ€– **AI-ready** | `SKILL.md` with copy-paste markup, six app skeletons and the rules – an agent given only that file built a working app in the release test |
65
65
 
@@ -71,8 +71,8 @@ It is the **app layer** of the NotionKit family – three layers, one design lan
71
71
  <!DOCTYPE html>
72
72
  <html lang="en" data-theme="light">
73
73
  <head>
74
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit@1.8.0/notionkit.min.css">
75
- <script src="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit-elements@1.8.0/dist/notionkit-elements.min.js"></script>
74
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit@1.10.0/notionkit.min.css">
75
+ <script src="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit-elements@1.10.0/dist/notionkit-elements.min.js"></script>
76
76
  </head>
77
77
  <body class="nk-body">
78
78
  <nk-field label="Display name" desc="Shown next to your comments">
@@ -102,18 +102,18 @@ The per-component files import NotionKit's sheet as `@jungherz-de/notionkit/noti
102
102
 
103
103
  Do not mix the full bundle with the per-component files: each brings its own `NkElement`. With the bundle, take `componentsSheet` and `NkElement` from the bundle (`NotionKitElements.componentsSheet` from the `<script>` build).
104
104
 
105
- Two prerequisites, always: `notionkit.css` on the document (the peer dependency, `>= 1.8.0`; 1.8.0 brings the side peek, the bookmark, the copy field and pictures on the profile row, 1.7.0 the sheet, menus that float in and become sheets on a phone, the sidebar drawer, the database toolbar with filter pills and the steps, 1.7.1 minified files that carry all of it, 1.6.0 page properties, page options, list view, panels, prose and the avatar, 1.5.1 the safe areas, 1.5.0 the Notion-2025 tokens, the nine tag colours and the filled inputs; from 1.5.0 on, Elements and the foundation share one version number – the elements are built and tested against the same release) and `class="nk-body"` on `<body>`. The elements ship no visual CSS of their own – every rule comes from the foundation. The bundle injects the design tokens once as `@layer notionkit-defaults`, so any plain `:root { --nk-accent: … }` of yours re-brands every element in both themes.
105
+ Two prerequisites, always: `notionkit.css` on the document (the peer dependency, `>= 1.10.0`; 1.10.0 brings the collapsing sidebar, the resizable side peek, the dialog and the tooltip, 1.9.0 the date picker and the calendar view, 1.8.0 the side peek, the bookmark, the copy field and pictures on the profile row, 1.7.0 the sheet, menus that float in and become sheets on a phone, the sidebar drawer, the database toolbar with filter pills and the steps, 1.7.1 minified files that carry all of it, 1.6.0 page properties, page options, list view, panels, prose and the avatar, 1.5.1 the safe areas, 1.5.0 the Notion-2025 tokens, the nine tag colours and the filled inputs; from 1.5.0 on, Elements and the foundation share one version number – the elements are built and tested against the same release) and `class="nk-body"` on `<body>`. The elements ship no visual CSS of their own – every rule comes from the foundation. The bundle injects the design tokens once as `@layer notionkit-defaults`, so any plain `:root { --nk-accent: … }` of yours re-brands every element in both themes.
106
106
 
107
107
  ## Elements
108
108
 
109
109
  | Group | Elements |
110
110
  |---|---|
111
- | Forms & controls | `nk-btn` `nk-input` `nk-textarea` `nk-select` `nk-switch` `nk-check` `nk-radio` `nk-slider` `nk-field` `nk-fields` `nk-copy-field` `nk-image-picker` |
111
+ | Forms & controls | `nk-btn` `nk-input` `nk-textarea` `nk-select` `nk-switch` `nk-check` `nk-radio` `nk-slider` `nk-field` `nk-fields` `nk-copy-field` `nk-image-picker` `nk-calendar` |
112
112
  | Content | `nk-tag` `nk-progress` `nk-callout` `nk-bookmark` `nk-divider` `nk-heading` `nk-toggle` `nk-todo` `nk-kbd` `nk-code` `nk-quote` |
113
113
  | App shell & navigation | `nk-app` `nk-sidebar` `nk-workspace-switcher` `nk-section-label` `nk-tree` `nk-tree-item` `nk-topbar` `nk-breadcrumb` `nk-theme-toggle` `nk-tab-bar` `nk-tab-bar-item` |
114
114
  | Page shell & blocks | `nk-page` `nk-page-cover` `nk-page-title` `nk-page-actions` `nk-props` `nk-prop` `nk-panels` `nk-panel` `nk-avatar` `nk-block-host` `nk-banner` `nk-empty` `nk-skeleton` `nk-synced` `nk-tabs` `nk-tab` `nk-segmented` `nk-steps` `nk-stats` `nk-stat` `nk-avatar-group` `nk-mention` `nk-template-btn` `nk-model-card` `nk-profile-row` `nk-danger-zone` `nk-member-list` `nk-member-row` |
115
- | Overlays | `nk-modal` `nk-sheet` `nk-peek` `nk-settings-pane` `nk-settings-user` `nk-cmdk` `nk-menu` `nk-menu-item` `nk-pop` `nk-emoji-picker` `nk-toast` |
116
- | Data & collaboration | `nk-database` `nk-table-view` `nk-board-view` `nk-list-view` `nk-filter-bar` `nk-comments` `nk-comment` `nk-ai-thread` `nk-ai-msg` `nk-ai-input-row` (+ the exported `renderPropertyCell()`) |
115
+ | Overlays | `nk-modal` `nk-sheet` `nk-dialog` `nk-peek` `nk-tooltip` `nk-settings-pane` `nk-settings-user` `nk-cmdk` `nk-menu` `nk-menu-item` `nk-pop` `nk-emoji-picker` `nk-toast` |
116
+ | Data & collaboration | `nk-database` `nk-table-view` `nk-board-view` `nk-list-view` `nk-calendar-view` `nk-filter-bar` `nk-comments` `nk-comment` `nk-ai-thread` `nk-ai-msg` `nk-ai-input-row` (+ the exported `renderPropertyCell()`) |
117
117
 
118
118
  Static content goes in through attributes and slots, dynamic data through properties (`tree.data`, `database.rows`, `palette.commands`). Interactions fire custom events with fixed names: `nk-select`, `nk-change`, `nk-view-change`, `nk-command`, `nk-toggle`, `nk-submit`, `nk-action`. No fetching, no two-way binding – the elements render what they get.
119
119
 
@@ -8,15 +8,22 @@ import '@jungherz-de/notionkit/notionkit-styles.js';
8
8
  // </nk-app>
9
9
  // β†’ <div class="nk-app"><aside class="nk-sidebar">…</aside><main class="nk-main">…</main></div>
10
10
  // The outermost element of a workspace app: a full-height flex row.
11
+ // `peek-inset` (NotionKit 1.10.0) gives the main column a right padding as
12
+ // wide as the side peek – <nk-peek inset> sets it while it is open.
11
13
  class NkApp extends NkElement {
14
+ static get observedAttributes() { return ['peek-inset']; }
15
+
12
16
  render() {
13
- const app = this.createElement('div', ['nk-app']);
17
+ const app = this._app = this.createElement('div', ['nk-app']);
18
+ app.classList.toggle('peek-inset', this.getBoolAttr('peek-inset'));
14
19
  app.appendChild(this.createElement('slot', [], { name: 'sidebar' }));
15
20
  const main = this.createElement('main', ['nk-main']);
16
21
  main.appendChild(document.createElement('slot'));
17
22
  app.appendChild(main);
18
23
  this._wrapper.appendChild(app);
19
24
  }
25
+
26
+ onAttributeChanged() { this._app.classList.toggle('peek-inset', this.getBoolAttr('peek-inset')); }
20
27
  }
21
28
 
22
29
  customElements.define('nk-app', NkApp);
@@ -6,7 +6,8 @@ import '@jungherz-de/notionkit/notionkit-styles.js';
6
6
  // <nk-btn variant="topbar">⭐</nk-btn> β†’ <button class="nk-topbar-btn">
7
7
  // <nk-btn variant="tool" active>Filter</nk-btn> β†’ <button class="nk-db-tool active"> (slot="tools" of nk-database)
8
8
  // <nk-btn variant="sidebar" aria-label="Menu">☰</nk-btn> β†’ <button class="nk-topbar-btn nk-sidebar-toggle">:
9
- // shown on phones only, it opens the page's <nk-sidebar> as a drawer.
9
+ // on a phone it opens the page's <nk-sidebar> as a drawer; on the desktop
10
+ // it shows while that sidebar is collapsed (.collapsed) and expands it.
10
11
  //
11
12
  // Modifier classes become attributes. A slotted <svg> is sized by the
12
13
  // `.nk-btn ::slotted(svg)` twin; pass the icon itself, never wrapped.
@@ -52,11 +53,17 @@ class NkBtn extends NkElement {
52
53
 
53
54
  /** The sidebar the ☰ opens: the one in its own app, else the page's. */
54
55
  _sidebar() { return this.closest('nk-app')?.querySelector('nk-sidebar') ?? document.querySelector('nk-sidebar'); }
56
+ _sidebarCollapsed() { return this.getAttribute('variant') === 'sidebar' && !!this._sidebar()?.hasAttribute('collapsed'); }
55
57
 
56
58
  setupEvents() {
57
59
  this._onClick = (e) => {
58
60
  if (this.getBoolAttr('disabled')) { e.preventDefault(); e.stopPropagation(); return; }
59
- if (this.getAttribute('variant') === 'sidebar') { this._sidebar()?.toggle?.(); return; }
61
+ if (this.getAttribute('variant') === 'sidebar') {
62
+ const sidebar = this._sidebar();
63
+ if (sidebar?.hasAttribute('collapsed') && !matchMedia('(max-width: 860px)').matches) sidebar.expand?.();
64
+ else sidebar?.toggle?.();
65
+ return;
66
+ }
60
67
  // A button inside a shadow root is not a submit button of the outer
61
68
  // form; forward the intent to the form the host sits in.
62
69
  const type = this.getAttribute('type');
@@ -69,13 +76,20 @@ class NkBtn extends NkElement {
69
76
  this._onToggle = (e) => {
70
77
  if (this.getAttribute('variant') === 'sidebar' && e.target === this._sidebar()) this._btn.setAttribute('aria-expanded', e.detail.open ? 'true' : 'false');
71
78
  };
79
+ // On the desktop the ☰ shows while its sidebar is collapsed.
80
+ this._onCollapse = (e) => {
81
+ if (this.getAttribute('variant') === 'sidebar' && e.target === this._sidebar()) this._btn.classList.toggle('collapsed', e.detail.collapsed);
82
+ };
72
83
  this._btn.addEventListener('click', this._onClick);
73
84
  document.addEventListener('nk-toggle', this._onToggle);
85
+ document.addEventListener('nk-collapse', this._onCollapse);
86
+ if (this.getAttribute('variant') === 'sidebar') this._btn.className = this._computeClasses().join(' ');
74
87
  }
75
88
 
76
89
  teardownEvents() {
77
90
  this._btn?.removeEventListener('click', this._onClick);
78
91
  document.removeEventListener('nk-toggle', this._onToggle);
92
+ document.removeEventListener('nk-collapse', this._onCollapse);
79
93
  }
80
94
 
81
95
  onAttributeChanged(name) {
@@ -104,7 +118,7 @@ class NkBtn extends NkElement {
104
118
  const variant = this.getAttribute('variant');
105
119
  if (variant === 'topbar') return ['nk-topbar-btn'];
106
120
  if (variant === 'share') return ['nk-topbar-btn', 'nk-share-btn'];
107
- if (variant === 'sidebar') return ['nk-topbar-btn', 'nk-sidebar-toggle'];
121
+ if (variant === 'sidebar') return this._sidebarCollapsed() ? ['nk-topbar-btn', 'nk-sidebar-toggle', 'collapsed'] : ['nk-topbar-btn', 'nk-sidebar-toggle'];
108
122
  if (variant === 'tool') return this.getBoolAttr('active') ? ['nk-db-tool', 'active'] : ['nk-db-tool'];
109
123
  const classes = ['nk-btn'];
110
124
  if (VARIANTS.includes(variant)) classes.push(variant);
@@ -0,0 +1,138 @@
1
+ import { NkElement } from './base.js';
2
+ import { C as CHEVRONS, i as isoOf, r as resolveLocale, f as firstWeekday, m as monthGrid, p as parseDay, w as weekLabel, a as weekdayNames, b as isoWeek, s as stepMonth } from './shared/dates-a32DcW1l.js';
3
+ import '@jungherz-de/notionkit/notionkit-styles.js';
4
+
5
+ // <nk-calendar-view name="calendar" label="πŸ“… Calendar" date-key="due" weeks></nk-calendar-view>
6
+ // The fourth database view (NotionKit 1.9.0): a month, the rows as cards on
7
+ // their dates. `date-key` names the date column (default: the first one),
8
+ // `title-key` the title (default: the title column); dates are YYYY-MM-DD or
9
+ // D.M.YYYY, a range { start, end } stands on its start. month (YYYY-MM;
10
+ // default today's), weeks (the ISO week in front of each row), week-start
11
+ // (0 = Sunday … 6 = Saturday; default from the locale), weekend (weekdays
12
+ // washed like the days of other months, "6,0"), today, locale and the texts
13
+ // today-label, prev-label, next-label, week-label.
14
+ // Standalone: view.columns = […]; view.rows = […]. Inside <nk-database> the
15
+ // database pushes the data. A card fires nk-select { row, id, value } like a
16
+ // row of the table, Today and β€Ή β€Ί nk-month { month }.
17
+ // β†’ <div class="nk-calendar-view weeks"><div class="cv-head"><div class="cv-title">May 2026</div><button class="cal-nav">Today</button>β€Ή β€Ί</div>
18
+ // <div class="cv-grid"><div class="cv-wd">W</div>…<div class="cv-week">21</div><div class="cv-day today"><span class="cv-num">20</span><button class="cv-item">πŸ—ƒοΈ …</button></div>…</div></div>
19
+ class NkCalendarView extends NkElement {
20
+ static get observedAttributes() {
21
+ return ['name', 'label', 'badge', 'count', 'date-key', 'title-key', 'month', 'weeks', 'week-start', 'weekend', 'today', 'locale',
22
+ 'today-label', 'prev-label', 'next-label', 'week-label'];
23
+ }
24
+
25
+ render() {
26
+ this._box = this.createElement('div', ['nk-calendar-view']);
27
+ const head = this.createElement('div', ['cv-head']);
28
+ this._title = this.createElement('div', ['cv-title']);
29
+ this._todayBtn = this.createElement('button', ['cal-nav'], { type: 'button' });
30
+ this._prev = this.createElement('button', ['cal-nav'], { type: 'button' });
31
+ this._prev.innerHTML = CHEVRONS.prev;
32
+ this._next = this.createElement('button', ['cal-nav'], { type: 'button' });
33
+ this._next.innerHTML = CHEVRONS.next;
34
+ head.append(this._title, this._todayBtn, this._prev, this._next);
35
+ this._grid = this.createElement('div', ['cv-grid']);
36
+ this._box.append(head, this._grid);
37
+ this._wrapper.appendChild(this._box);
38
+ this._columns ??= []; this._rows ??= [];
39
+ this._month ??= this._attrMonth() ?? this._todayIso().slice(0, 7);
40
+ this._render();
41
+ this.closest('nk-database')?.requestSync?.();
42
+ }
43
+
44
+ _attrMonth() { const m = this.getAttribute('month'); return /^\d{4}-\d{2}$/.test(m || '') ? m : null; }
45
+ _todayIso() { return this.getAttribute('today') || isoOf(new Date()); }
46
+ _dateColumn() {
47
+ const key = this.getAttribute('date-key');
48
+ return this._columns.find(c => c.key === key) || this._columns.find(c => c.type === 'date') || null;
49
+ }
50
+ _titleColumn() {
51
+ const key = this.getAttribute('title-key');
52
+ return this._columns.find(c => c.key === key) || this._columns.find(c => c.title) || this._columns[0] || null;
53
+ }
54
+
55
+ _render() {
56
+ if (!this._grid) return;
57
+ const locale = resolveLocale(this.getAttribute('locale')), weeks = this.getBoolAttr('weeks');
58
+ const n = Number(this.getAttribute('week-start'));
59
+ const ws = this.hasAttribute('week-start') && Number.isInteger(n) && n >= 0 && n <= 6 ? n : firstWeekday(locale);
60
+ const g = monthGrid(this._month, ws), today = this._todayIso();
61
+ const weekend = new Set((this.getAttribute('weekend') || '').split(',').map(s => s.trim()).filter(Boolean).map(Number));
62
+ const dateCol = this._dateColumn(), titleCol = this._titleColumn(), byDay = {};
63
+ if (dateCol) for (const row of this._rows) {
64
+ const d = parseDay(row[dateCol.key]);
65
+ if (d) (byDay[isoOf(d)] ||= []).push(row);
66
+ }
67
+ this._box.classList.toggle('weeks', weeks);
68
+ this._title.textContent = new Intl.DateTimeFormat(locale, { month: 'long', year: 'numeric' }).format(g.first);
69
+ this._todayBtn.textContent = this.getAttribute('today-label') || 'Today';
70
+ this._prev.setAttribute('aria-label', this.getAttribute('prev-label') || 'Previous month');
71
+ this._next.setAttribute('aria-label', this.getAttribute('next-label') || 'Next month');
72
+
73
+ const cell = (cls, text) => { const el = this.createElement('div', [cls]); el.textContent = text; return el; };
74
+ const nodes = [];
75
+ if (weeks) nodes.push(cell('cv-wd', this.getAttribute('week-label') || weekLabel(locale)));
76
+ for (const w of weekdayNames(locale, ws)) nodes.push(cell('cv-wd', w));
77
+ for (let i = 0; i < g.rows * 7; i++) {
78
+ const d = g.day(i), iso = isoOf(d);
79
+ if (weeks && i % 7 === 0) nodes.push(cell('cv-week', isoWeek(d)));
80
+ const day = this.createElement('div', ['cv-day', d.getMonth() !== g.m - 1 && 'out', weekend.has(d.getDay()) && 'off', iso === today && 'today'].filter(Boolean));
81
+ const num = this.createElement('span', ['cv-num']);
82
+ num.textContent = d.getDate();
83
+ day.appendChild(num);
84
+ for (const row of byDay[iso] || []) {
85
+ const item = this.createElement('button', ['cv-item'], { type: 'button', 'data-id': row.id ?? '' });
86
+ const title = titleCol ? String(row[titleCol.key] ?? '') : '';
87
+ item.textContent = row.icon ? `${row.icon} ${title}` : title;
88
+ day.appendChild(item);
89
+ }
90
+ nodes.push(day);
91
+ }
92
+ this._grid.replaceChildren(...nodes);
93
+ }
94
+
95
+ _goMonth(month) {
96
+ if (month === this._month) return;
97
+ this._month = month;
98
+ this._render();
99
+ this.emit('nk-month', { month });
100
+ }
101
+
102
+ setupEvents() {
103
+ this._onClick = (e) => {
104
+ if (e.target.closest('.cal-nav') === this._todayBtn) { this._goMonth(this._todayIso().slice(0, 7)); return; }
105
+ if (e.target.closest('.cal-nav') === this._prev) { this._goMonth(stepMonth(this._month, -1)); return; }
106
+ if (e.target.closest('.cal-nav') === this._next) { this._goMonth(stepMonth(this._month, 1)); return; }
107
+ const item = e.target.closest('.cv-item');
108
+ if (item) { const row = this._rowById(item.dataset.id); this.emit('nk-select', { row, id: row?.id, value: row?.id }); }
109
+ };
110
+ this._box.addEventListener('click', this._onClick);
111
+ }
112
+
113
+ teardownEvents() { this._box?.removeEventListener('click', this._onClick); }
114
+
115
+ _rowById(id) { return this._rows.find(r => String(r.id) === String(id)); }
116
+
117
+ onAttributeChanged(name) {
118
+ if (name === 'month') this._month = this._attrMonth() ?? this._month;
119
+ this._render();
120
+ if (name === 'label' || name === 'badge' || name === 'count') this.closest('nk-database')?.requestSync?.();
121
+ }
122
+
123
+ get name() { return this.getAttribute('name') || 'calendar'; }
124
+ /** The month shown, YYYY-MM. */
125
+ get month() { return this._month; }
126
+ set month(v) { if (!/^\d{4}-\d{2}$/.test(v)) return; if (this._grid) this._goMonth(v); else this._month = v; }
127
+ get columns() { return this._columns; }
128
+ set columns(v) { this._columns = Array.isArray(v) ? v : []; this._render(); }
129
+ get rows() { return this._rows; }
130
+ set rows(v) { this._rows = Array.isArray(v) ? v : []; this._render(); }
131
+ set data({ columns, rows }) { this.setData(columns, rows); }
132
+ setData(columns, rows) { this._columns = columns || []; this._rows = rows || []; this._render(); }
133
+ refresh() { this._render(); }
134
+ }
135
+
136
+ customElements.define('nk-calendar-view', NkCalendarView);
137
+
138
+ export { NkCalendarView };