@waggylabs/yumekit 0.5.1-beta.8 → 0.5.1-beta.9

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 (37) hide show
  1. package/CHANGELOG.md +8 -2
  2. package/dist/components/y-code/y-code.d.ts +2 -0
  3. package/dist/components/y-code.d.ts +2 -0
  4. package/dist/components/y-code.js +81 -3
  5. package/dist/components/y-color.js +16 -2
  6. package/dist/components/y-colorpicker.js +16 -2
  7. package/dist/components/y-data-grid.js +186 -48
  8. package/dist/components/y-date.js +572 -39
  9. package/dist/components/y-datepicker/y-datepicker.d.ts +9 -0
  10. package/dist/components/y-datepicker.d.ts +9 -0
  11. package/dist/components/y-datepicker.js +572 -39
  12. package/dist/components/y-drawer.js +4 -1
  13. package/dist/components/y-help.js +3 -11
  14. package/dist/components/y-paginator.js +16 -2
  15. package/dist/components/y-popover.js +2 -10
  16. package/dist/components/y-select.js +16 -2
  17. package/dist/components/y-sidebar/y-sidebar.d.ts +9 -0
  18. package/dist/components/y-sidebar.d.ts +9 -0
  19. package/dist/components/y-sidebar.js +24 -1
  20. package/dist/components/y-theme.js +29 -3
  21. package/dist/index.js +348 -56
  22. package/dist/modules/helpers.d.ts +14 -0
  23. package/dist/modules/helpers.js +24 -0
  24. package/dist/modules/helpers.test.js +46 -0
  25. package/dist/styles/kepler-amber.css +220 -0
  26. package/dist/styles/kepler-dark.css +220 -0
  27. package/dist/styles/kepler-galaxy.css +220 -0
  28. package/dist/styles/kepler-light.css +220 -0
  29. package/dist/styles/kepler-matrix.css +220 -0
  30. package/dist/styles/neobrutalist-dark.css +1 -1
  31. package/dist/styles/neobrutalist.css +1 -1
  32. package/dist/styles/slate-dark.css +215 -0
  33. package/dist/styles/slate-light.css +215 -0
  34. package/dist/styles/variables.css +19 -0
  35. package/dist/yumekit.min.js +1 -1
  36. package/llm.txt +12 -9
  37. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -37,7 +37,7 @@ Delete any empty sections before publishing.
37
37
 
38
38
  - `y-checkbox` / `y-radio` — checked-state color hooks so themes can fill the control on selection: `--component-checkbox-checked-background` / `-checked-border-color` / `-checked-icon-color` and `--component-radio-background` / `-checked-background` / `-checked-border-color` / `-checked-dot-color`. Each falls back to the matching unchecked value, so existing themes are unchanged. The design-system themes now use them — checked checkboxes (and Shadcn/Bootstrap/Primer radios) fill with the primary color and draw the check/dot in the primary-inverse color, matching each system's native look.
39
39
 
40
- - Several theme color variations including the `Catppuccin` and `Nord` color schemes, two `Rose` color themes, two `Neobrutalist` themes, as well as several new themes based on some of the most popular design systems. This includes Material, Carbon, Ant, Shadcn, Primer, and Bootstrap.
40
+ - Several theme color variations including `Slate`, `Rose`, the `Catppuccin` and `Nord` color schemes, two `Neobrutalist` themes, as well as several new themes based on some of the most popular design systems. This includes Material, Carbon, Ant, Shadcn, Primer, Bootstrap, and a few throwback themes inspired by Yumekit's spiritual ancestor: `Kepler UI`.
41
41
 
42
42
  - `y-tabs` — new `variant="accent"` style: minimal tabs where the active tab shows a primary-colored indicator border on its content-facing edge (bottom for top tabs, etc.). The default bordered style is unchanged. Adds `--component-tabs-accent-width` (indicator thickness). The border-width token is normalized to `--component-tabs-border-width` (matching `--component-tabs-border-color`); the legacy `--component-tab-border-width` is still honored as a fallback.
43
43
 
@@ -83,11 +83,17 @@ Delete any empty sections before publishing.
83
83
 
84
84
  ### Fixed
85
85
 
86
+ - `y-sidebar` — nav/footer icons now stay in the same position between the expanded and collapsed states under themes whose borders use a multi-value width (e.g. the Neobrutalist offset border `2px 2px 6px 2px`). The icon-column width is now derived from the sidebar's resolved horizontal border (flat buttons render no border) instead of an invalid `calc()` that multiplied the multi-value token, which had collapsed the column to `auto` and shifted icons.
87
+
88
+ - `y-drawer` — corners closest to the screen's edge are now squared in all themes.
89
+
90
+ - `y-datepicker` — the month and year pickers were reworked. The month picker (`show-days="false"`) now shows a selectable year dropdown in its header (toggled by `show-years`, omitted when `false`) above the twelve months, and the year picker (`show-months="false"`) now bounds its scrollable year grid with two start/end year inputs. Clicking a month or year now selects it and fires `change`; clicking a year in the year picker no longer shifts the visible range.
91
+
86
92
  - `y-theme` — switching themes now clears the previous theme's CSS custom properties from the host before applying the new ones. Variables defined only by the outgoing theme (e.g. a theme-specific token like `--component-tabs-inactive-background`) previously lingered inline and "stuck" until a page reload; they are now removed on every switch.
87
93
 
88
94
  - `y-tabs` — unselected tabs now use a dedicated `--component-tabs-inactive-background` (falling back to `--component-tabs-border-color`, so existing themes are unchanged) instead of always painting the tab background with the border color. This fixes the Neobrutalist themes, where the border color equals the text color and made unselected tab labels unreadable; both Neobrutalist themes now set it to a distinct surface tone.
89
95
 
90
- - Portaled overlays now inherit the active theme. `y-help` (tour overlay/tooltip), `y-popover` (`portal` mode), and `y-select` (`portal` mode) previously rendered into `document.body`, escaping the `<y-theme>` subtree that scopes the theme's CSS custom properties — so they fell back to the default un-themed palette. They now mount into the nearest enclosing `<y-theme>` (falling back to `document.body` when there isn't one), so the portaled surface matches the active theme and tracks live theme switches. Keep the component inside your `<y-theme>`.
96
+ - Portaled overlays now inherit the active theme. `y-help` (tour overlay/tooltip), `y-popover` (`portal` mode), and `y-select` (`portal` mode) previously rendered into `document.body`, escaping the `<y-theme>` subtree that scopes the theme's CSS custom properties — so they fell back to the default un-themed palette. They now mount into the nearest enclosing `<y-theme>`, walking up across shadow boundaries so the fix also applies when the component is used inside another component's shadow root (e.g. `y-data-grid`'s per-column header menus), falling back to `document.body` when there is no theme ancestor. The portaled surface matches the active theme and tracks live theme switches. Keep the component inside your `<y-theme>`.
91
97
 
92
98
  - Form field components now share one field background. `y-select` (trigger and dropdown) used `base.background.app` while `y-input` / `y-textarea` / `y-color` / `y-date` used `base.background.component`; `select.background` is now `base.background.component` across all themes, so fields match when placed together on a form (most visible in the Material and Carbon themes). The select dropdown panel now also matches menus/popovers.
93
99
 
@@ -1,6 +1,7 @@
1
1
  export class YumeCode extends HTMLElement {
2
2
  static get observedAttributes(): string[];
3
3
  _copiedTimer: any;
4
+ _lineFlashTimer: any;
4
5
  _isExpanded: boolean;
5
6
  connectedCallback(): void;
6
7
  disconnectedCallback(): void;
@@ -63,6 +64,7 @@ export class YumeCode extends HTMLElement {
63
64
  _readTemplateSource(tpl: any): any;
64
65
  _render(): void;
65
66
  _sanitizedHighlightedLines(): any[][];
67
+ _showLineCopiedBadge(line: any): HTMLElement;
66
68
  _tokenizedLines(source: any): {
67
69
  spans: string[];
68
70
  text: string;
@@ -1,6 +1,7 @@
1
1
  export class YumeCode extends HTMLElement {
2
2
  static get observedAttributes(): string[];
3
3
  _copiedTimer: any;
4
+ _lineFlashTimer: any;
4
5
  _isExpanded: boolean;
5
6
  connectedCallback(): void;
6
7
  disconnectedCallback(): void;
@@ -63,6 +64,7 @@ export class YumeCode extends HTMLElement {
63
64
  _readTemplateSource(tpl: any): any;
64
65
  _render(): void;
65
66
  _sanitizedHighlightedLines(): any[][];
67
+ _showLineCopiedBadge(line: any): HTMLElement;
66
68
  _tokenizedLines(source: any): {
67
69
  spans: string[];
68
70
  text: string;
@@ -1681,6 +1681,7 @@ class YumeCode extends HTMLElement {
1681
1681
  super();
1682
1682
  this.attachShadow({ mode: "open" });
1683
1683
  this._copiedTimer = null;
1684
+ this._lineFlashTimer = null;
1684
1685
  this._isExpanded = false;
1685
1686
  }
1686
1687
 
@@ -1694,6 +1695,10 @@ class YumeCode extends HTMLElement {
1694
1695
  clearTimeout(this._copiedTimer);
1695
1696
  this._copiedTimer = null;
1696
1697
  }
1698
+ if (this._lineFlashTimer) {
1699
+ clearTimeout(this._lineFlashTimer);
1700
+ this._lineFlashTimer = null;
1701
+ }
1697
1702
  }
1698
1703
 
1699
1704
  attributeChangedCallback(name, oldValue, newValue) {
@@ -2017,6 +2022,7 @@ class YumeCode extends HTMLElement {
2017
2022
  padding: 0 var(--spacing-small, 8px);
2018
2023
  white-space: ${this.wrap ? "pre-wrap" : "pre"};
2019
2024
  word-break: ${this.wrap ? "break-word" : "normal"};
2025
+ transition: background 0.4s ease;
2020
2026
  }
2021
2027
 
2022
2028
  .line-number {
@@ -2042,9 +2048,36 @@ class YumeCode extends HTMLElement {
2042
2048
  outline: 2px solid var(--primary-content--, #1976d2);
2043
2049
  outline-offset: -2px;
2044
2050
  }
2045
- .line.is-copied {
2051
+ .line.is-copied,
2052
+ :host([line-numbers]) .line[role="button"].is-copied {
2046
2053
  background: var(--component-code-line-copied-bg, var(--success-content-light, rgba(46, 125, 50, 0.12)));
2047
- transition: background 0.4s ease;
2054
+ }
2055
+
2056
+ /* Floating "Copied!" pill anchored to the copied line. Its top is
2057
+ set in JS; it's pinned to the right edge of the viewport so it
2058
+ stays visible regardless of horizontal scroll. */
2059
+ .line-copied-badge {
2060
+ position: absolute;
2061
+ right: var(--spacing-small, 8px);
2062
+ z-index: 2;
2063
+ pointer-events: none;
2064
+ padding: 0 var(--spacing-x-small, 5px);
2065
+ border-radius: var(--radii-full, 999px);
2066
+ background: var(--component-code-line-copied-badge-bg, var(--success-content--, #2e7d32));
2067
+ color: var(--component-code-line-copied-badge-text-color, var(--success-content-inverse, #fff));
2068
+ font-family: var(--font-family-body, sans-serif);
2069
+ font-size: 0.625rem;
2070
+ font-weight: var(--font-weight-body, 400);
2071
+ line-height: 1.6;
2072
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);
2073
+ animation: y-code-copied-badge-in 0.15s ease;
2074
+ }
2075
+ @keyframes y-code-copied-badge-in {
2076
+ from { opacity: 0; transform: translateY(-3px); }
2077
+ to { opacity: 1; transform: none; }
2078
+ }
2079
+ @media (prefers-reduced-motion: reduce) {
2080
+ .line-copied-badge { animation: none; }
2048
2081
  }
2049
2082
 
2050
2083
  .line-content { flex: 1; min-width: 0; }
@@ -2180,8 +2213,22 @@ class YumeCode extends HTMLElement {
2180
2213
  `.line[data-line="${index}"]`,
2181
2214
  );
2182
2215
  if (!line) return;
2216
+
2217
+ if (this._lineFlashTimer) clearTimeout(this._lineFlashTimer);
2218
+ // Clear any in-progress flash first so rapid successive copies don't
2219
+ // strand the highlight on a previously-copied line.
2220
+ this.shadowRoot
2221
+ .querySelectorAll(".line.is-copied")
2222
+ .forEach((el) => el.classList.remove("is-copied"));
2223
+
2183
2224
  line.classList.add("is-copied");
2184
- setTimeout(() => line.classList.remove("is-copied"), 600);
2225
+ const badge = this._showLineCopiedBadge(line);
2226
+
2227
+ this._lineFlashTimer = setTimeout(() => {
2228
+ this._lineFlashTimer = null;
2229
+ line.classList.remove("is-copied");
2230
+ if (badge) badge.remove();
2231
+ }, 1500);
2185
2232
  }
2186
2233
 
2187
2234
  _hasHeaderSlot() {
@@ -2382,6 +2429,37 @@ class YumeCode extends HTMLElement {
2382
2429
  return lines;
2383
2430
  }
2384
2431
 
2432
+ _showLineCopiedBadge(line) {
2433
+ const preWrap = this.shadowRoot.querySelector(".pre-wrap");
2434
+ const pre = this.shadowRoot.querySelector("pre.code");
2435
+ if (!preWrap || !pre) return null;
2436
+
2437
+ const stale = preWrap.querySelector(".line-copied-badge");
2438
+ if (stale) stale.remove();
2439
+
2440
+ const badge = createElement(
2441
+ "span",
2442
+ {
2443
+ class: "line-copied-badge",
2444
+ part: "line-copied-badge",
2445
+ role: "status",
2446
+ },
2447
+ [this.copiedLabel],
2448
+ );
2449
+ preWrap.appendChild(badge);
2450
+
2451
+ // `line.offsetTop` is measured against `.pre-wrap` (the nearest
2452
+ // positioned ancestor); subtract the scroll offset so the badge tracks
2453
+ // the copied line when the block is scrolled vertically (max-lines).
2454
+ const top =
2455
+ line.offsetTop -
2456
+ pre.scrollTop +
2457
+ (line.offsetHeight - badge.offsetHeight) / 2;
2458
+ badge.style.top = `${Math.max(top, 0)}px`;
2459
+
2460
+ return badge;
2461
+ }
2462
+
2385
2463
  _tokenizedLines(source) {
2386
2464
  // Run the language tokenizer, then split the flat token stream into
2387
2465
  // per-line arrays of `{ spans, text }` so the existing line renderer
@@ -1,4 +1,4 @@
1
- import { manageLabelVisibility, createElement, isSafeCssColor, contrastTextColor, parseColorString, rgbToHsv, hsvToRgb, rgbToHex, hsvToHsl, rgbaToHex, clamp, hslToHsv } from '../../modules/helpers.js';
1
+ import { manageLabelVisibility, createElement, isSafeCssColor, contrastTextColor, resolveThemeMountPoint, parseColorString, rgbToHsv, hsvToRgb, rgbToHex, hsvToHsl, rgbaToHex, clamp, hslToHsv } from '../../modules/helpers.js';
2
2
 
3
3
  class YumeInput extends HTMLElement {
4
4
  static formAssociated = true;
@@ -1616,6 +1616,20 @@ class YumeSelect extends HTMLElement {
1616
1616
 
1617
1617
  const portal = document.createElement("div");
1618
1618
  portal.className = "y-select-portal";
1619
+
1620
+ // Custom properties set inline on the host don't cascade into the
1621
+ // portal — it mounts under y-theme/body, not as a descendant of this
1622
+ // element. Forward a z-index override so a select opened inside a
1623
+ // higher-stacked context (e.g. a portaled popover) can lift its
1624
+ // dropdown above that context. The dropdown inherits the value through
1625
+ // the portal's shadow boundary.
1626
+ const zOverride = this.style
1627
+ .getPropertyValue("--component-select-z-index")
1628
+ .trim();
1629
+ if (zOverride) {
1630
+ portal.style.setProperty("--component-select-z-index", zOverride);
1631
+ }
1632
+
1619
1633
  const shadow = portal.attachShadow({ mode: "open" });
1620
1634
  shadow.adoptedStyleSheets = this.shadowRoot.adoptedStyleSheets;
1621
1635
 
@@ -1640,7 +1654,7 @@ class YumeSelect extends HTMLElement {
1640
1654
  }
1641
1655
 
1642
1656
  _resolveMountPoint() {
1643
- return this.closest("y-theme") || document.body;
1657
+ return resolveThemeMountPoint(this);
1644
1658
  }
1645
1659
 
1646
1660
  _openDropdown() {
@@ -1,4 +1,4 @@
1
- import { manageLabelVisibility, createElement, isSafeCssColor, contrastTextColor, parseColorString, rgbToHsv, hsvToRgb, rgbToHex, hsvToHsl, rgbaToHex, clamp, hslToHsv } from '../../modules/helpers.js';
1
+ import { manageLabelVisibility, createElement, isSafeCssColor, contrastTextColor, resolveThemeMountPoint, parseColorString, rgbToHsv, hsvToRgb, rgbToHex, hsvToHsl, rgbaToHex, clamp, hslToHsv } from '../../modules/helpers.js';
2
2
 
3
3
  class YumeInput extends HTMLElement {
4
4
  static formAssociated = true;
@@ -1616,6 +1616,20 @@ class YumeSelect extends HTMLElement {
1616
1616
 
1617
1617
  const portal = document.createElement("div");
1618
1618
  portal.className = "y-select-portal";
1619
+
1620
+ // Custom properties set inline on the host don't cascade into the
1621
+ // portal — it mounts under y-theme/body, not as a descendant of this
1622
+ // element. Forward a z-index override so a select opened inside a
1623
+ // higher-stacked context (e.g. a portaled popover) can lift its
1624
+ // dropdown above that context. The dropdown inherits the value through
1625
+ // the portal's shadow boundary.
1626
+ const zOverride = this.style
1627
+ .getPropertyValue("--component-select-z-index")
1628
+ .trim();
1629
+ if (zOverride) {
1630
+ portal.style.setProperty("--component-select-z-index", zOverride);
1631
+ }
1632
+
1619
1633
  const shadow = portal.attachShadow({ mode: "open" });
1620
1634
  shadow.adoptedStyleSheets = this.shadowRoot.adoptedStyleSheets;
1621
1635
 
@@ -1640,7 +1654,7 @@ class YumeSelect extends HTMLElement {
1640
1654
  }
1641
1655
 
1642
1656
  _resolveMountPoint() {
1643
- return this.closest("y-theme") || document.body;
1657
+ return resolveThemeMountPoint(this);
1644
1658
  }
1645
1659
 
1646
1660
  _openDropdown() {
@@ -1,4 +1,4 @@
1
- import { createElement, isSafeCssColor, manageLabelVisibility, contrastTextColor, getColorVarPair, clamp, measureCSSLength, GAP_TOKEN_MAP } from '../../modules/helpers.js';
1
+ import { createElement, isSafeCssColor, manageLabelVisibility, contrastTextColor, resolveThemeMountPoint, getColorVarPair, clamp, measureCSSLength, GAP_TOKEN_MAP } from '../../modules/helpers.js';
2
2
 
3
3
  /**
4
4
  * Icon registry — a runtime map of icon names to SVG markup strings.
@@ -1875,6 +1875,20 @@ class YumeSelect extends HTMLElement {
1875
1875
 
1876
1876
  const portal = document.createElement("div");
1877
1877
  portal.className = "y-select-portal";
1878
+
1879
+ // Custom properties set inline on the host don't cascade into the
1880
+ // portal — it mounts under y-theme/body, not as a descendant of this
1881
+ // element. Forward a z-index override so a select opened inside a
1882
+ // higher-stacked context (e.g. a portaled popover) can lift its
1883
+ // dropdown above that context. The dropdown inherits the value through
1884
+ // the portal's shadow boundary.
1885
+ const zOverride = this.style
1886
+ .getPropertyValue("--component-select-z-index")
1887
+ .trim();
1888
+ if (zOverride) {
1889
+ portal.style.setProperty("--component-select-z-index", zOverride);
1890
+ }
1891
+
1878
1892
  const shadow = portal.attachShadow({ mode: "open" });
1879
1893
  shadow.adoptedStyleSheets = this.shadowRoot.adoptedStyleSheets;
1880
1894
 
@@ -1899,7 +1913,7 @@ class YumeSelect extends HTMLElement {
1899
1913
  }
1900
1914
 
1901
1915
  _resolveMountPoint() {
1902
- return this.closest("y-theme") || document.body;
1916
+ return resolveThemeMountPoint(this);
1903
1917
  }
1904
1918
 
1905
1919
  _openDropdown() {
@@ -2992,6 +3006,8 @@ class YumeDatepicker extends HTMLElement {
2992
3006
  this._awaitingEnd = false;
2993
3007
  this._startTime = { h: 0, m: 0, s: 0 };
2994
3008
  this._endTime = { h: 0, m: 0, s: 0 };
3009
+ this._yearRangeStart = null;
3010
+ this._yearRangeEnd = null;
2995
3011
  this._mql = null;
2996
3012
  this._isMobile = false;
2997
3013
  this._onMediaChange = this._onMediaChange.bind(this);
@@ -3299,17 +3315,33 @@ class YumeDatepicker extends HTMLElement {
3299
3315
  });
3300
3316
  });
3301
3317
 
3318
+ root.querySelectorAll(".m-year-sel").forEach((sel) => {
3319
+ sel.addEventListener("change", () => {
3320
+ this._viewDate.setFullYear(parseInt(sel.value));
3321
+ this.render();
3322
+ });
3323
+ });
3324
+
3302
3325
  root.querySelectorAll(".month-btn").forEach((btn) => {
3303
3326
  btn.addEventListener("click", () => {
3304
- this._viewDate.setMonth(parseInt(btn.dataset.month));
3305
- this.render();
3327
+ const vd = this._viewDateForSide(btn.dataset.side);
3328
+ this._selectMonth(vd.getFullYear(), parseInt(btn.dataset.month));
3306
3329
  });
3307
3330
  });
3308
3331
 
3309
3332
  root.querySelectorAll(".year-btn").forEach((btn) => {
3310
- btn.addEventListener("click", () => {
3311
- this._viewDate.setFullYear(parseInt(btn.dataset.year));
3312
- this.render();
3333
+ btn.addEventListener("click", () =>
3334
+ this._selectYear(parseInt(btn.dataset.year)),
3335
+ );
3336
+ });
3337
+
3338
+ root.querySelectorAll(".year-range-input").forEach((input) => {
3339
+ input.addEventListener("input", () => {
3340
+ const val = parseInt(input.value, 10);
3341
+ if (isNaN(val)) return;
3342
+ if (input.dataset.bound === "start") this._yearRangeStart = val;
3343
+ else this._yearRangeEnd = val;
3344
+ this._refreshYearGrid();
3313
3345
  });
3314
3346
  });
3315
3347
 
@@ -3406,23 +3438,14 @@ class YumeDatepicker extends HTMLElement {
3406
3438
  const year = vd.getFullYear();
3407
3439
  const month = vd.getMonth();
3408
3440
 
3409
- // Month and year picker modes: simple label, no selects or nav
3410
- if (!this.showDays) {
3411
- let label;
3412
- if (this.showMonths) {
3413
- label = String(year);
3414
- } else {
3415
- const minY = this._minDate()?.getFullYear() ?? year - 10;
3416
- const maxY = this._maxDate()?.getFullYear() ?? year + 10;
3417
- label = `${minY} – ${maxY}`;
3418
- }
3419
- return `
3420
- <div class="cal-header">
3421
- <div class="header-selects">
3422
- <span class="header-label">${label}</span>
3423
- </div>
3424
- </div>
3425
- `;
3441
+ // Month picker: 12-month grid with an optional year dropdown.
3442
+ if (!this.showDays && this.showMonths) {
3443
+ return this._buildMonthPickerHeader(year);
3444
+ }
3445
+
3446
+ // Year picker: start/end year inputs bounding the scrollable grid.
3447
+ if (!this.showDays && !this.showMonths) {
3448
+ return this._buildYearPickerHeader();
3426
3449
  }
3427
3450
 
3428
3451
  const showPrev = !isRange || side === "left";
@@ -3485,6 +3508,27 @@ class YumeDatepicker extends HTMLElement {
3485
3508
  `;
3486
3509
  }
3487
3510
 
3511
+ _buildMonthPickerHeader(year) {
3512
+ if (!this.showYears) return "";
3513
+
3514
+ const minYear = this._minDate()?.getFullYear() ?? year - 50;
3515
+ const maxYear = this._maxDate()?.getFullYear() ?? year + 50;
3516
+ const yearOptions = JSON.stringify(
3517
+ Array.from({ length: maxYear - minYear + 1 }, (_, i) => ({
3518
+ value: String(minYear + i),
3519
+ label: String(minYear + i),
3520
+ })),
3521
+ );
3522
+
3523
+ return `
3524
+ <div class="cal-header">
3525
+ <div class="header-selects">
3526
+ <y-select class="m-year-sel" size="small" value="${year}" options='${yearOptions}'></y-select>
3527
+ </div>
3528
+ </div>
3529
+ `;
3530
+ }
3531
+
3488
3532
  _buildPanel(side, showHours) {
3489
3533
  const vd = this._viewDateForSide(side);
3490
3534
  const isRange = this.mode === "range";
@@ -3574,10 +3618,20 @@ class YumeDatepicker extends HTMLElement {
3574
3618
 
3575
3619
  .month-sel { min-width: 120px; }
3576
3620
  .year-sel { min-width: 80px; }
3621
+ .m-year-sel { min-width: 90px; }
3577
3622
 
3578
- .header-label {
3579
- font-weight: 600;
3580
- white-space: nowrap;
3623
+ /* ---- Year-picker range inputs ---- */
3624
+
3625
+ .cal-header.year-range {
3626
+ justify-content: center;
3627
+ gap: var(--spacing-x-small, 6px);
3628
+ }
3629
+
3630
+ .year-range-input { width: 5.5em; }
3631
+
3632
+ .year-range-sep {
3633
+ color: var(--component-datepicker-header-color);
3634
+ flex-shrink: 0;
3581
3635
  }
3582
3636
 
3583
3637
  /* ---- Day grid ---- */
@@ -3777,31 +3831,57 @@ class YumeDatepicker extends HTMLElement {
3777
3831
  }
3778
3832
 
3779
3833
  _buildYearGrid(vd) {
3834
+ this._ensureYearRange(vd);
3780
3835
  const selected = this._startDate?.getFullYear();
3781
- const minY = this._minDate()?.getFullYear() ?? vd.getFullYear() - 10;
3782
- const maxY = this._maxDate()?.getFullYear() ?? vd.getFullYear() + 10;
3783
- const years = Array.from(
3784
- { length: maxY - minY + 1 },
3785
- (_, i) => minY + i,
3786
- );
3836
+ const lo = Math.min(this._yearRangeStart, this._yearRangeEnd);
3837
+ const hi = Math.max(this._yearRangeStart, this._yearRangeEnd);
3838
+ const years = Array.from({ length: hi - lo + 1 }, (_, i) => lo + i);
3787
3839
  return `
3788
3840
  <div class="year-grid">
3789
3841
  ${years
3790
- .map(
3791
- (y) => `<y-button
3842
+ .map((y) => {
3843
+ const isSelected = y === selected;
3844
+ const disabled = this._isYearDisabled(y);
3845
+ return `<y-button
3792
3846
  class="year-btn"
3793
- style-type="${y === selected ? "filled" : "flat"}"
3794
- color="${y === selected ? this.color : "base"}"
3847
+ style-type="${isSelected ? "filled" : "flat"}"
3848
+ color="${isSelected ? this.color : "base"}"
3795
3849
  size="small"
3796
3850
  padding-mode="square"
3797
3851
  data-year="${y}"
3798
- >${y}</y-button>`,
3799
- )
3852
+ ${disabled ? "disabled" : ""}
3853
+ >${y}</y-button>`;
3854
+ })
3800
3855
  .join("")}
3801
3856
  </div>
3802
3857
  `;
3803
3858
  }
3804
3859
 
3860
+ _buildYearPickerHeader() {
3861
+ this._ensureYearRange(this._viewDate);
3862
+ return `
3863
+ <div class="cal-header year-range">
3864
+ <y-input
3865
+ class="year-range-input"
3866
+ data-bound="start"
3867
+ type="number"
3868
+ size="small"
3869
+ value="${this._yearRangeStart}"
3870
+ aria-label="Start year"
3871
+ ></y-input>
3872
+ <span class="year-range-sep">–</span>
3873
+ <y-input
3874
+ class="year-range-input"
3875
+ data-bound="end"
3876
+ type="number"
3877
+ size="small"
3878
+ value="${this._yearRangeEnd}"
3879
+ aria-label="End year"
3880
+ ></y-input>
3881
+ </div>
3882
+ `;
3883
+ }
3884
+
3805
3885
  _emitChange(source) {
3806
3886
  this._suppressParse = true;
3807
3887
  const value = this._buildValueString();
@@ -3829,6 +3909,16 @@ class YumeDatepicker extends HTMLElement {
3829
3909
  );
3830
3910
  }
3831
3911
 
3912
+ _ensureYearRange(vd) {
3913
+ if (this._yearRangeStart != null && this._yearRangeEnd != null) return;
3914
+ const base =
3915
+ this._startDate && !isNaN(this._startDate)
3916
+ ? this._startDate.getFullYear()
3917
+ : vd.getFullYear();
3918
+ this._yearRangeStart = this._minDate()?.getFullYear() ?? base - 10;
3919
+ this._yearRangeEnd = this._maxDate()?.getFullYear() ?? base + 10;
3920
+ }
3921
+
3832
3922
  _formatDate(date) {
3833
3923
  if (!date || isNaN(date)) return "";
3834
3924
  const pad = (n) => String(n).padStart(2, "0");
@@ -3921,6 +4011,14 @@ class YumeDatepicker extends HTMLElement {
3921
4011
  );
3922
4012
  }
3923
4013
 
4014
+ _isYearDisabled(year) {
4015
+ const min = this._minDate();
4016
+ const max = this._maxDate();
4017
+ if (min && year < min.getFullYear()) return true;
4018
+ if (max && year > max.getFullYear()) return true;
4019
+ return false;
4020
+ }
4021
+
3924
4022
  _maxDate() {
3925
4023
  return this.max ? new Date(this.max) : null;
3926
4024
  }
@@ -3984,6 +4082,23 @@ class YumeDatepicker extends HTMLElement {
3984
4082
  }
3985
4083
  }
3986
4084
 
4085
+ _refreshYearGrid() {
4086
+ const grid = this.shadowRoot.querySelector(".year-grid");
4087
+ if (!grid) return;
4088
+
4089
+ const lo = Math.min(this._yearRangeStart, this._yearRangeEnd);
4090
+ const hi = Math.max(this._yearRangeStart, this._yearRangeEnd);
4091
+ // Skip runaway ranges while the user is still typing a bound.
4092
+ if (hi - lo > 500) return;
4093
+
4094
+ grid.outerHTML = this._buildYearGrid(this._viewDate);
4095
+ this.shadowRoot.querySelectorAll(".year-btn").forEach((btn) => {
4096
+ btn.addEventListener("click", () =>
4097
+ this._selectYear(parseInt(btn.dataset.year)),
4098
+ );
4099
+ });
4100
+ }
4101
+
3987
4102
  _sameDay(a, b) {
3988
4103
  if (!a || !b) return false;
3989
4104
  return (
@@ -4002,6 +4117,33 @@ class YumeDatepicker extends HTMLElement {
4002
4117
  });
4003
4118
  }
4004
4119
 
4120
+ _selectMonth(year, month) {
4121
+ const src =
4122
+ this._startDate && !isNaN(this._startDate) ? this._startDate : null;
4123
+ const day = src ? src.getDate() : 1;
4124
+ const daysInMonth = new Date(year, month + 1, 0).getDate();
4125
+
4126
+ this._startDate = new Date(year, month, Math.min(day, daysInMonth));
4127
+ this._viewDate = new Date(year, month, 1);
4128
+ this._applyTimesToDates();
4129
+ this._emitChange("month");
4130
+ this.render();
4131
+ }
4132
+
4133
+ _selectYear(year) {
4134
+ const src =
4135
+ this._startDate && !isNaN(this._startDate) ? this._startDate : null;
4136
+ const month = src ? src.getMonth() : 0;
4137
+ const day = src ? src.getDate() : 1;
4138
+ const daysInMonth = new Date(year, month + 1, 0).getDate();
4139
+
4140
+ this._startDate = new Date(year, month, Math.min(day, daysInMonth));
4141
+ this._viewDate = new Date(year, month, 1);
4142
+ this._applyTimesToDates();
4143
+ this._emitChange("year");
4144
+ this.render();
4145
+ }
4146
+
4005
4147
  _setupMediaQuery() {
4006
4148
  this._teardownMediaQuery();
4007
4149
  const bp = this._getBreakpointPx();
@@ -8000,15 +8142,7 @@ class YumePopover extends HTMLElement {
8000
8142
  }
8001
8143
 
8002
8144
  _resolveMountPoint() {
8003
- // Portaling escapes ancestor stacking/overflow contexts, but YumeKit
8004
- // delivers a theme as CSS custom properties scoped to the <y-theme>
8005
- // subtree (set inline on that element). Appending to document.body
8006
- // would drop out of that scope, so every --base-* / --component-* /
8007
- // --primary-* lookup falls back to the un-themed default literals.
8008
- // Mount into the nearest enclosing <y-theme> instead so the portaled
8009
- // surface inherits the active theme; fall back to <body> when there
8010
- // is no y-theme ancestor (or it lives across a shadow boundary).
8011
- return this.closest("y-theme") || document.body;
8145
+ return resolveThemeMountPoint(this);
8012
8146
  }
8013
8147
 
8014
8148
  _restoreFocus() {
@@ -10644,6 +10778,10 @@ class YumeDataGrid extends HTMLElement {
10644
10778
  value: currentOp,
10645
10779
  portal: "",
10646
10780
  });
10781
+ opSelect.style.setProperty(
10782
+ "--component-select-z-index",
10783
+ "calc(var(--component-popover-z-index, 7500) + 1)",
10784
+ );
10647
10785
  const valInput = createElement("y-input", {
10648
10786
  size: "small",
10649
10787
  type,