@bluevolt-tech/lumen 2.0.1 → 2.2.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 (56) hide show
  1. package/README.md +37 -23
  2. package/dist/components/_shared/adopt-styles.d.ts +8 -0
  3. package/dist/components/_shared/date-range-validator.d.ts +86 -0
  4. package/dist/components/_shared/dropdown-controller.d.ts +60 -0
  5. package/dist/components/_shared/selectable-card.d.ts +10 -0
  6. package/dist/components/bv-alert-banner/bv-alert-banner.js +19 -17
  7. package/dist/components/bv-avatar/bv-avatar.js +19 -11
  8. package/dist/components/bv-badge/bv-badge.js +19 -11
  9. package/dist/components/bv-button/bv-button.js +18 -16
  10. package/dist/components/bv-category-pill/bv-category-pill.js +18 -16
  11. package/dist/components/bv-checkbox/bv-checkbox.js +19 -11
  12. package/dist/components/bv-chip/bv-chip.js +19 -11
  13. package/dist/components/bv-course-detail/bv-course-detail.js +20 -34
  14. package/dist/components/bv-course-detail-modal/bv-course-detail-modal.js +24 -90
  15. package/dist/components/bv-course-grid-card/bv-course-grid-card.d.ts +2 -6
  16. package/dist/components/bv-course-grid-card/bv-course-grid-card.js +98 -105
  17. package/dist/components/bv-course-list-card/bv-course-list-card.d.ts +2 -6
  18. package/dist/components/bv-course-list-card/bv-course-list-card.js +98 -105
  19. package/dist/components/bv-course-outline/bv-course-outline.js +19 -11
  20. package/dist/components/bv-date-range-input/bv-date-range-input.d.ts +10 -6
  21. package/dist/components/bv-date-range-input/bv-date-range-input.js +602 -125
  22. package/dist/components/bv-date-range-picker/bv-date-range-picker.d.ts +71 -4
  23. package/dist/components/bv-date-range-picker/bv-date-range-picker.js +441 -76
  24. package/dist/components/bv-empty-state/bv-empty-state.js +20 -28
  25. package/dist/components/bv-filter-dropdown/bv-filter-dropdown.d.ts +1 -4
  26. package/dist/components/bv-filter-dropdown/bv-filter-dropdown.js +80 -53
  27. package/dist/components/bv-filter-panel/bv-filter-panel.js +20 -22
  28. package/dist/components/bv-icon/bv-icon.js +19 -11
  29. package/dist/components/bv-input/bv-input.js +18 -16
  30. package/dist/components/bv-input-dropdown/bv-input-dropdown.d.ts +1 -4
  31. package/dist/components/bv-input-dropdown/bv-input-dropdown.js +79 -78
  32. package/dist/components/bv-modal/bv-modal.js +19 -11
  33. package/dist/components/bv-page-title/bv-page-title.js +19 -6
  34. package/dist/components/bv-pagination/bv-pagination.d.ts +1 -0
  35. package/dist/components/bv-pagination/bv-pagination.js +23 -186
  36. package/dist/components/bv-portal-header/bv-portal-header.js +25 -83
  37. package/dist/components/bv-portal-layout/bv-portal-layout.js +19 -11
  38. package/dist/components/bv-progress-bar/bv-progress-bar.js +19 -11
  39. package/dist/components/bv-promo-banner/bv-promo-banner.js +19 -11
  40. package/dist/components/bv-radio/bv-radio.js +19 -11
  41. package/dist/components/bv-searchbox/bv-searchbox.js +19 -11
  42. package/dist/components/bv-segmented-control/bv-segmented-control.js +18 -16
  43. package/dist/components/bv-select/bv-select.d.ts +1 -4
  44. package/dist/components/bv-select/bv-select.js +78 -44
  45. package/dist/components/bv-split-button/bv-split-button.d.ts +1 -4
  46. package/dist/components/bv-split-button/bv-split-button.js +78 -42
  47. package/dist/components/bv-star-rating/bv-star-rating.js +19 -11
  48. package/dist/components/bv-table/bv-table.d.ts +5 -0
  49. package/dist/components/bv-table/bv-table.js +70 -19
  50. package/dist/components/bv-toggle/bv-toggle.js +19 -11
  51. package/dist/components/bv-tooltip/bv-tooltip.js +20 -17
  52. package/dist/components/bv-user-row/bv-user-row.js +22 -50
  53. package/dist/components/bv-view-switcher/bv-view-switcher.js +19 -11
  54. package/dist/themes/base.css +1 -1
  55. package/package.json +8 -1
  56. package/themes/base.css +1 -1
@@ -1,8 +1,170 @@
1
1
  // css-inline:/home/runner/work/BV_lumen/BV_lumen/components/bv-date-range-input/bv-date-range-input.css
2
- var bv_date_range_input_default = ":host {\n display: block;\n position: relative;\n font-family: var(--bv-font-family-base);\n}\n\n/* \u2500\u2500 Label \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.field-label {\n display: block;\n font-size: var(--bv-font-size-sm);\n line-height: 20px;\n font-weight: var(--bv-font-weight-semibold);\n color: var(--bv-color-neutral-800);\n margin-bottom: 6px;\n}\n\n.required-mark {\n color: var(--bv-color-danger);\n margin-left: 2px;\n}\n\n/* \u2500\u2500 Trigger \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.trigger {\n display: flex;\n align-items: center;\n justify-content: space-between;\n width: 100%;\n height: 44px;\n box-sizing: border-box;\n padding: 0 var(--bv-spacing-md);\n background: var(--bv-color-neutral-0);\n border: 1px solid var(--bv-color-neutral-300);\n border-radius: var(--bv-radius-lg);\n cursor: pointer;\n font-family: inherit;\n font-size: var(--bv-font-size-sm);\n color: var(--bv-color-neutral-800);\n text-align: left;\n transition:\n border-color var(--bv-transition-fast),\n box-shadow var(--bv-transition-fast);\n}\n\n.trigger:focus-visible {\n outline: none;\n border-color: var(--bv-color-primary);\n box-shadow:\n 0 0 0 3px var(--bv-color-primary-light),\n 0 1px 2px 0 rgba(16, 24, 40, 0.05);\n}\n\n:host([open]) .trigger {\n border-color: var(--bv-color-primary);\n box-shadow:\n 0 0 0 3px var(--bv-color-primary-light),\n 0 1px 2px 0 rgba(16, 24, 40, 0.05);\n}\n\n.trigger-display {\n flex: 1;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.trigger-display.is-placeholder {\n color: var(--bv-color-neutral-400);\n}\n\n.trigger-icon {\n display: flex;\n flex-shrink: 0;\n margin-left: var(--bv-spacing-sm);\n color: var(--bv-color-neutral-400);\n transition: color var(--bv-transition-fast);\n}\n\n:host([open]) .trigger-icon {\n color: var(--bv-color-primary);\n}\n\n/* \u2500\u2500 Picker popup \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.picker-popup {\n display: none;\n position: absolute;\n top: calc(100% + 4px);\n left: 0;\n z-index: 300;\n}\n\n:host([open]) .picker-popup {\n display: block;\n}\n";
2
+ var bv_date_range_input_default = ":host {\n display: block;\n position: relative;\n font-family: var(--bv-font-family-base);\n}\n\n/* \u2500\u2500 Label \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.field-label {\n display: block;\n font-size: var(--bv-font-size-sm);\n line-height: 20px;\n font-weight: var(--bv-font-weight-semibold);\n color: var(--bv-color-neutral-800);\n margin-bottom: 6px;\n}\n\n.required-mark {\n color: var(--bv-color-danger);\n margin-left: 2px;\n}\n\n/* \u2500\u2500 Trigger \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.trigger {\n display: flex;\n align-items: center;\n justify-content: space-between;\n width: 100%;\n height: 44px;\n box-sizing: border-box;\n padding: 0 var(--bv-spacing-md);\n background: var(--bv-color-neutral-0);\n border: 1px solid var(--bv-color-neutral-300);\n border-radius: var(--bv-radius-lg);\n cursor: pointer;\n font-family: inherit;\n font-size: var(--bv-font-size-sm);\n color: var(--bv-color-neutral-800);\n text-align: left;\n transition:\n border-color var(--bv-transition-fast),\n box-shadow var(--bv-transition-fast);\n}\n\n.trigger:focus-visible {\n outline: none;\n border-color: var(--bv-color-primary);\n box-shadow:\n 0 0 0 3px var(--bv-color-primary-light),\n 0 1px 2px 0 rgba(16, 24, 40, 0.05);\n}\n\n:host([open]) .trigger {\n border-color: var(--bv-color-primary);\n box-shadow:\n 0 0 0 3px var(--bv-color-primary-light),\n 0 1px 2px 0 rgba(16, 24, 40, 0.05);\n}\n\n/* BTN-10191 \u2014 reflected from the internal picker's validity state */\n.trigger.invalid,\n.trigger.invalid:focus-visible,\n:host([open]) .trigger.invalid {\n border-color: var(--bv-color-danger);\n box-shadow: 0 0 0 3px var(--bv-color-danger-light);\n}\n\n.trigger-display {\n flex: 1;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.trigger-display.is-placeholder {\n color: var(--bv-color-neutral-400);\n}\n\n.trigger-icon {\n display: flex;\n flex-shrink: 0;\n margin-left: var(--bv-spacing-sm);\n color: var(--bv-color-neutral-400);\n transition: color var(--bv-transition-fast);\n}\n\n:host([open]) .trigger-icon {\n color: var(--bv-color-primary);\n}\n\n/* \u2500\u2500 Picker popup \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.picker-popup {\n display: none;\n position: absolute;\n top: calc(100% + 4px);\n left: 0;\n z-index: 300;\n}\n\n:host([open]) .picker-popup {\n display: block;\n}\n";
3
+
4
+ // components/_shared/adopt-styles.ts
5
+ var _sheetCache = /* @__PURE__ */ new Map();
6
+ function adoptStyles(shadow, css) {
7
+ if ("adoptedStyleSheets" in ShadowRoot.prototype && typeof CSSStyleSheet !== "undefined" && "replaceSync" in CSSStyleSheet.prototype) {
8
+ let sheet = _sheetCache.get(css);
9
+ if (!sheet) {
10
+ sheet = new CSSStyleSheet();
11
+ sheet.replaceSync(css);
12
+ _sheetCache.set(css, sheet);
13
+ }
14
+ shadow.adoptedStyleSheets = [sheet];
15
+ return;
16
+ }
17
+ const style = document.createElement("style");
18
+ style.textContent = css;
19
+ shadow.appendChild(style);
20
+ }
21
+
22
+ // components/_shared/dropdown-controller.ts
23
+ function bindDropdownController(host, options = {}) {
24
+ if (!host.shadowRoot) {
25
+ throw new Error("bindDropdownController requires the host to have an open shadow root.");
26
+ }
27
+ const syncExpanded = () => {
28
+ var _a;
29
+ (_a = options.trigger) == null ? void 0 : _a.setAttribute("aria-expanded", host.hasAttribute("open") ? "true" : "false");
30
+ };
31
+ const open = () => {
32
+ var _a;
33
+ if (host.hasAttribute("open")) return;
34
+ (_a = options.onBeforeOpen) == null ? void 0 : _a.call(options);
35
+ host.setAttribute("open", "");
36
+ syncExpanded();
37
+ host.dispatchEvent(new CustomEvent("bv-open", { bubbles: true, composed: true }));
38
+ };
39
+ const closeWithReason = (reason) => {
40
+ var _a;
41
+ if (!host.hasAttribute("open")) return;
42
+ if (options.canClose && !options.canClose(reason)) return;
43
+ host.removeAttribute("open");
44
+ syncExpanded();
45
+ (_a = options.onAfterClose) == null ? void 0 : _a.call(options);
46
+ };
47
+ const close = () => closeWithReason("programmatic");
48
+ const toggle = () => {
49
+ if (host.hasAttribute("open")) closeWithReason("programmatic");
50
+ else open();
51
+ };
52
+ const offClick = (e) => {
53
+ if (!host.hasAttribute("open")) return;
54
+ if (e.composedPath().includes(host)) return;
55
+ closeWithReason("outside-click");
56
+ };
57
+ document.addEventListener("click", offClick);
58
+ const onKeyDown = (e) => {
59
+ if (e.key === "Escape") closeWithReason("escape");
60
+ };
61
+ host.addEventListener("keydown", onKeyDown);
62
+ const observer = new MutationObserver(syncExpanded);
63
+ observer.observe(host, { attributes: true, attributeFilter: ["open"] });
64
+ const cleanup = () => {
65
+ observer.disconnect();
66
+ document.removeEventListener("click", offClick);
67
+ host.removeEventListener("keydown", onKeyDown);
68
+ };
69
+ syncExpanded();
70
+ return { open, close, toggle, cleanup };
71
+ }
3
72
 
4
73
  // css-inline:/home/runner/work/BV_lumen/BV_lumen/components/bv-date-range-picker/bv-date-range-picker.css
5
- var bv_date_range_picker_default = ":host {\n display: inline-block;\n font-family: var(--bv-font-family-base);\n}\n\n/* \u2500\u2500 Picker shell \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.picker {\n display: flex;\n background: var(--bv-color-neutral-0);\n border: 1px solid var(--bv-color-neutral-200);\n border-radius: var(--bv-radius-xl);\n box-shadow: var(--bv-shadow-lg);\n overflow: hidden;\n user-select: none;\n}\n\n/* \u2500\u2500 Sidebar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.sidebar {\n display: flex;\n flex-direction: column;\n width: 152px;\n flex-shrink: 0;\n padding: var(--bv-spacing-sm) 0;\n border-right: 1px solid var(--bv-color-neutral-200);\n}\n\n.preset-btn {\n display: block;\n width: 100%;\n padding: 10px var(--bv-spacing-md);\n text-align: left;\n background: none;\n border: none;\n font-family: inherit;\n font-size: var(--bv-font-size-sm);\n font-weight: var(--bv-font-weight-regular);\n color: var(--bv-color-neutral-700);\n cursor: pointer;\n transition:\n background var(--bv-transition-fast),\n color var(--bv-transition-fast);\n white-space: nowrap;\n}\n\n.preset-btn:hover {\n background: var(--bv-color-secondary);\n color: var(--bv-color-neutral-900);\n}\n\n.sidebar-sep {\n margin: var(--bv-spacing-xs) 0;\n border: none;\n border-top: 1px solid var(--bv-color-neutral-200);\n}\n\n.preset-btn.active {\n background: var(--bv-color-secondary-light);\n color: var(--bv-color-neutral-900);\n font-weight: var(--bv-font-weight-medium);\n}\n\n/* \u2500\u2500 Main panel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.main {\n display: flex;\n flex-direction: column;\n flex: 1;\n}\n\n/* \u2500\u2500 Calendars \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.calendars {\n display: flex;\n border-bottom: 1px solid var(--bv-color-neutral-200);\n}\n\n.calendar {\n padding: var(--bv-spacing-md) var(--bv-spacing-lg);\n flex: 1;\n}\n\n.calendar--left {\n border-right: 1px solid var(--bv-color-neutral-200);\n}\n\n/* \u2500\u2500 Calendar header \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.calendar-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n margin-bottom: var(--bv-spacing-md);\n}\n\n.calendar-title {\n font-size: var(--bv-font-size-sm);\n font-weight: var(--bv-font-weight-semibold);\n color: var(--bv-color-neutral-800);\n}\n\n.nav-btn {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n padding: 0;\n background: none;\n border: none;\n border-radius: var(--bv-radius-md);\n color: var(--bv-color-neutral-500);\n cursor: pointer;\n transition:\n background var(--bv-transition-fast),\n color var(--bv-transition-fast);\n flex-shrink: 0;\n}\n\n.nav-btn:hover {\n background: var(--bv-color-secondary);\n color: var(--bv-color-neutral-800);\n}\n\n/* \u2500\u2500 Weekdays row \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.weekdays {\n display: grid;\n grid-template-columns: repeat(7, 36px);\n gap: 0;\n margin-bottom: var(--bv-spacing-xs);\n}\n\n.weekday {\n display: flex;\n align-items: center;\n justify-content: center;\n height: 28px;\n font-size: var(--bv-font-size-xs);\n font-weight: var(--bv-font-weight-medium);\n color: var(--bv-color-neutral-500);\n}\n\n/* \u2500\u2500 Days grid \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.days-grid {\n display: grid;\n grid-template-columns: repeat(7, 36px);\n gap: 0;\n}\n\n/* \u2500\u2500 Day cell \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.day {\n position: relative;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 36px;\n height: 36px;\n padding: 0;\n border: none;\n background: none;\n cursor: pointer;\n font-family: inherit;\n font-size: var(--bv-font-size-sm);\n font-weight: var(--bv-font-weight-regular);\n color: var(--bv-color-neutral-700);\n border-radius: 0;\n transition: background var(--bv-transition-fast);\n}\n\n.day:hover:not(.start):not(.end) .day-inner {\n background: var(--bv-color-secondary);\n color: var(--bv-color-neutral-900);\n}\n\n.day--blank {\n pointer-events: none;\n}\n\n.day.today:not(.start):not(.end) .day-inner::after {\n content: '';\n position: absolute;\n bottom: 2px;\n left: 50%;\n transform: translateX(-50%);\n width: 4px;\n height: 4px;\n border-radius: var(--bv-radius-full);\n background: var(--bv-color-primary);\n}\n\n/* \u2500\u2500 Range band \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.day.in-range {\n background: var(--bv-color-primary-light);\n}\n\n.day.in-range.week-start,\n.day.range-end.week-start {\n border-radius: var(--bv-radius-full) 0 0 var(--bv-radius-full);\n}\n\n.day.in-range.week-end,\n.day.range-start.week-end {\n border-radius: 0 var(--bv-radius-full) var(--bv-radius-full) 0;\n}\n\n.day.range-start {\n background: linear-gradient(to right, transparent 50%, var(--bv-color-primary-light) 50%);\n}\n\n.day.range-end {\n background: linear-gradient(to left, transparent 50%, var(--bv-color-primary-light) 50%);\n}\n\n/* \u2500\u2500 Day inner (circle) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.day-inner {\n position: relative;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 32px;\n height: 32px;\n border-radius: var(--bv-radius-full);\n z-index: 1;\n transition:\n background var(--bv-transition-fast),\n color var(--bv-transition-fast);\n}\n\n.day.start .day-inner,\n.day.end .day-inner {\n background: var(--bv-color-primary);\n color: var(--bv-color-neutral-0);\n font-weight: var(--bv-font-weight-semibold);\n}\n\n.day.start:hover .day-inner,\n.day.end:hover .day-inner {\n background: var(--bv-color-primary-dark);\n}\n\n/* \u2500\u2500 5-year warning banner \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.banner-wrap {\n display: none;\n padding: var(--bv-spacing-lg);\n}\n\n.picker--with-banner .banner-wrap {\n display: block;\n}\n\n/* Footer loses its top padding when the banner is shown above it */\n.picker--with-banner .footer {\n padding-top: 0;\n}\n\n/* \u2500\u2500 Footer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.footer {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: var(--bv-spacing-lg);\n gap: var(--bv-spacing-md);\n}\n\n/* \u2500\u2500 Date inputs \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.date-inputs {\n display: flex;\n align-items: center;\n gap: var(--bv-spacing-sm);\n}\n\n.date-input {\n width: 112px;\n height: 36px;\n padding: 0 var(--bv-spacing-sm);\n border: 1px solid var(--bv-color-neutral-300);\n border-radius: var(--bv-radius-lg);\n font-family: inherit;\n font-size: var(--bv-font-size-sm);\n color: var(--bv-color-neutral-700);\n text-align: center;\n outline: none;\n transition:\n border-color var(--bv-transition-fast),\n box-shadow var(--bv-transition-fast);\n}\n\n.date-input:focus {\n border-color: var(--bv-color-primary);\n box-shadow: 0 0 0 3px var(--bv-color-primary-light);\n}\n\n.date-input::placeholder {\n color: var(--bv-color-neutral-400);\n}\n\n.date-sep {\n color: var(--bv-color-neutral-400);\n font-size: var(--bv-font-size-sm);\n}\n\n/* \u2500\u2500 Action buttons \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.footer-actions {\n display: flex;\n align-items: center;\n gap: var(--bv-spacing-sm);\n flex-shrink: 0;\n}\n\n.btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n height: 36px;\n padding: 0 var(--bv-spacing-md);\n border-radius: var(--bv-radius-lg);\n font-family: inherit;\n font-size: var(--bv-font-size-sm);\n font-weight: var(--bv-font-weight-semibold);\n cursor: pointer;\n white-space: nowrap;\n transition:\n background var(--bv-transition-fast),\n border-color var(--bv-transition-fast),\n color var(--bv-transition-fast);\n}\n\n.btn-cancel {\n background: var(--bv-color-neutral-0);\n border: 1px solid var(--bv-color-neutral-300);\n color: var(--bv-color-neutral-700);\n}\n\n.btn-cancel:hover {\n background: var(--bv-color-secondary);\n border-color: var(--bv-color-neutral-400);\n}\n\n.btn-apply {\n background: var(--bv-color-primary);\n border: 1px solid var(--bv-color-primary);\n color: var(--bv-color-neutral-0);\n}\n\n.btn-apply:hover:not(:disabled) {\n background: var(--bv-color-primary-dark);\n border-color: var(--bv-color-primary-dark);\n}\n\n.btn-apply:disabled {\n background: var(--bv-color-neutral-300);\n border-color: var(--bv-color-neutral-300);\n cursor: not-allowed;\n}\n";
74
+ var bv_date_range_picker_default = ":host {\n display: inline-block;\n font-family: var(--bv-font-family-base);\n}\n\n/* \u2500\u2500 Picker shell \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.picker {\n display: flex;\n background: var(--bv-color-neutral-0);\n border: 1px solid var(--bv-color-neutral-200);\n border-radius: var(--bv-radius-xl);\n box-shadow: var(--bv-shadow-lg);\n overflow: hidden;\n user-select: none;\n}\n\n/* \u2500\u2500 Sidebar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.sidebar {\n display: flex;\n flex-direction: column;\n width: 152px;\n flex-shrink: 0;\n padding: var(--bv-spacing-sm) 0;\n border-right: 1px solid var(--bv-color-neutral-200);\n}\n\n.preset-btn {\n display: block;\n width: 100%;\n padding: 10px var(--bv-spacing-md);\n text-align: left;\n background: none;\n border: none;\n font-family: inherit;\n font-size: var(--bv-font-size-sm);\n font-weight: var(--bv-font-weight-regular);\n color: var(--bv-color-neutral-700);\n cursor: pointer;\n transition:\n background var(--bv-transition-fast),\n color var(--bv-transition-fast);\n white-space: nowrap;\n}\n\n.preset-btn:hover {\n background: var(--bv-color-secondary);\n color: var(--bv-color-neutral-900);\n}\n\n.sidebar-sep {\n margin: var(--bv-spacing-xs) 0;\n border: none;\n border-top: 1px solid var(--bv-color-neutral-200);\n}\n\n.preset-btn.active {\n background: var(--bv-color-secondary-light);\n color: var(--bv-color-neutral-900);\n font-weight: var(--bv-font-weight-medium);\n}\n\n/* \u2500\u2500 Main panel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.main {\n display: flex;\n flex-direction: column;\n flex: 1;\n}\n\n/* \u2500\u2500 Calendars \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.calendars {\n display: flex;\n border-bottom: 1px solid var(--bv-color-neutral-200);\n}\n\n.calendar {\n padding: var(--bv-spacing-md) var(--bv-spacing-lg);\n flex: 1;\n}\n\n.calendar--left {\n border-right: 1px solid var(--bv-color-neutral-200);\n}\n\n/* \u2500\u2500 Calendar header \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.calendar-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n margin-bottom: var(--bv-spacing-md);\n}\n\n.calendar-title {\n font-size: var(--bv-font-size-sm);\n font-weight: var(--bv-font-weight-semibold);\n color: var(--bv-color-neutral-800);\n}\n\n.nav-btn {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n padding: 0;\n background: none;\n border: none;\n border-radius: var(--bv-radius-md);\n color: var(--bv-color-neutral-500);\n cursor: pointer;\n transition:\n background var(--bv-transition-fast),\n color var(--bv-transition-fast);\n flex-shrink: 0;\n}\n\n.nav-btn:hover {\n background: var(--bv-color-secondary);\n color: var(--bv-color-neutral-800);\n}\n\n/* \u2500\u2500 Weekdays row \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.weekdays {\n display: grid;\n grid-template-columns: repeat(7, 36px);\n gap: 0;\n margin-bottom: var(--bv-spacing-xs);\n}\n\n.weekday {\n display: flex;\n align-items: center;\n justify-content: center;\n height: 28px;\n font-size: var(--bv-font-size-xs);\n font-weight: var(--bv-font-weight-medium);\n color: var(--bv-color-neutral-500);\n}\n\n/* \u2500\u2500 Days grid \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.days-grid {\n display: grid;\n grid-template-columns: repeat(7, 36px);\n gap: 0;\n}\n\n/* \u2500\u2500 Day cell \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.day {\n position: relative;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 36px;\n height: 36px;\n padding: 0;\n border: none;\n background: none;\n cursor: pointer;\n font-family: inherit;\n font-size: var(--bv-font-size-sm);\n font-weight: var(--bv-font-weight-regular);\n color: var(--bv-color-neutral-700);\n border-radius: 0;\n transition: background var(--bv-transition-fast);\n}\n\n.day:hover:not(.start):not(.end) .day-inner {\n background: var(--bv-color-secondary);\n color: var(--bv-color-neutral-900);\n}\n\n.day--blank {\n pointer-events: none;\n}\n\n.day.today:not(.start):not(.end) .day-inner::after {\n content: '';\n position: absolute;\n bottom: 2px;\n left: 50%;\n transform: translateX(-50%);\n width: 4px;\n height: 4px;\n border-radius: var(--bv-radius-full);\n background: var(--bv-color-primary);\n}\n\n/* \u2500\u2500 Range band \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.day.in-range {\n background: var(--bv-color-primary-light);\n}\n\n.day.in-range.week-start,\n.day.range-end.week-start {\n border-radius: var(--bv-radius-full) 0 0 var(--bv-radius-full);\n}\n\n.day.in-range.week-end,\n.day.range-start.week-end {\n border-radius: 0 var(--bv-radius-full) var(--bv-radius-full) 0;\n}\n\n.day.range-start {\n background: linear-gradient(to right, transparent 50%, var(--bv-color-primary-light) 50%);\n}\n\n.day.range-end {\n background: linear-gradient(to left, transparent 50%, var(--bv-color-primary-light) 50%);\n}\n\n/* \u2500\u2500 Day inner (circle) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.day-inner {\n position: relative;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 32px;\n height: 32px;\n border-radius: var(--bv-radius-full);\n z-index: 1;\n transition:\n background var(--bv-transition-fast),\n color var(--bv-transition-fast);\n}\n\n.day.start .day-inner,\n.day.end .day-inner {\n background: var(--bv-color-primary);\n color: var(--bv-color-neutral-0);\n font-weight: var(--bv-font-weight-semibold);\n}\n\n.day.start:hover .day-inner,\n.day.end:hover .day-inner {\n background: var(--bv-color-primary-dark);\n}\n\n/* \u2500\u2500 Alert banner (BTN-10312 \u2014 unified for all validation errors) \u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.banner-wrap {\n display: none;\n padding: var(--bv-spacing-lg);\n}\n\n.picker--with-banner .banner-wrap {\n display: block;\n}\n\n/* Footer loses its top padding when the banner is shown above it */\n.picker--with-banner .footer {\n padding-top: 0;\n}\n\n/* \u2500\u2500 Footer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.footer {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: var(--bv-spacing-lg);\n gap: var(--bv-spacing-md);\n}\n\n/* \u2500\u2500 Date inputs \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.date-inputs {\n display: flex;\n align-items: center;\n gap: var(--bv-spacing-sm);\n}\n\n.date-input {\n width: 112px;\n height: 36px;\n padding: 0 var(--bv-spacing-sm);\n border: 1px solid var(--bv-color-neutral-300);\n border-radius: var(--bv-radius-lg);\n font-family: inherit;\n font-size: var(--bv-font-size-sm);\n color: var(--bv-color-neutral-700);\n text-align: center;\n outline: none;\n transition:\n border-color var(--bv-transition-fast),\n box-shadow var(--bv-transition-fast);\n}\n\n.date-input:focus {\n border-color: var(--bv-color-primary);\n box-shadow: 0 0 0 3px var(--bv-color-primary-light);\n}\n\n.date-input::placeholder {\n color: var(--bv-color-neutral-400);\n}\n\n.date-sep {\n color: var(--bv-color-neutral-400);\n font-size: var(--bv-font-size-sm);\n}\n\n/* \u2500\u2500 Action buttons \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.footer-actions {\n display: flex;\n align-items: center;\n gap: var(--bv-spacing-sm);\n flex-shrink: 0;\n}\n\n.btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n height: 36px;\n padding: 0 var(--bv-spacing-md);\n border-radius: var(--bv-radius-lg);\n font-family: inherit;\n font-size: var(--bv-font-size-sm);\n font-weight: var(--bv-font-weight-semibold);\n cursor: pointer;\n white-space: nowrap;\n transition:\n background var(--bv-transition-fast),\n border-color var(--bv-transition-fast),\n color var(--bv-transition-fast);\n}\n\n.btn-cancel {\n background: var(--bv-color-neutral-0);\n border: 1px solid var(--bv-color-neutral-300);\n color: var(--bv-color-neutral-700);\n}\n\n.btn-cancel:hover {\n background: var(--bv-color-secondary);\n border-color: var(--bv-color-neutral-400);\n}\n\n.btn-apply {\n background: var(--bv-color-primary);\n border: 1px solid var(--bv-color-primary);\n color: var(--bv-color-neutral-0);\n}\n\n.btn-apply:hover:not(:disabled) {\n background: var(--bv-color-primary-dark);\n border-color: var(--bv-color-primary-dark);\n}\n\n.btn-apply:disabled {\n background: var(--bv-color-neutral-300);\n border-color: var(--bv-color-neutral-300);\n cursor: not-allowed;\n}\n\n/* \u2500\u2500 Validation error states (BTN-10191 / BTN-10312) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n/*\n * `.invalid` is applied to input fields only when the field's error resolves\n * to severity 'error' (blocking). Non-blocking severities (warning/primary/\n * success) surface via the banner but do not paint the input red.\n */\n.date-input.invalid,\n.date-input.invalid:focus {\n border-color: var(--bv-color-danger);\n box-shadow: 0 0 0 3px var(--bv-color-danger-light);\n}\n\n.day--disabled,\n.day[disabled] {\n color: var(--bv-color-neutral-300);\n cursor: not-allowed;\n pointer-events: none;\n}\n\n.day--disabled .day-inner,\n.day[disabled] .day-inner {\n color: var(--bv-color-neutral-300);\n}\n";
75
+
76
+ // components/_shared/date-range-validator.ts
77
+ var ALLOWED_SEVERITIES = ["error", "warning", "primary", "success"];
78
+ function normalizeSeverity(value) {
79
+ if (value && ALLOWED_SEVERITIES.includes(value)) {
80
+ return value;
81
+ }
82
+ return "error";
83
+ }
84
+ function validateDateRange(input, config = {}) {
85
+ const { start, end } = input;
86
+ const { min, max, maxRangeYears, validateOrder, validateFormat } = config;
87
+ const resolvedMax = max === "today" ? isoToday() : max;
88
+ let startError;
89
+ let endError;
90
+ const startDate = start ? parseIso(start) : null;
91
+ const endDate = end ? parseIso(end) : null;
92
+ if (validateFormat) {
93
+ if (start && !startDate) startError = "format";
94
+ if (end && !endDate) endError = "format";
95
+ }
96
+ if (!startError && startDate) {
97
+ if (min && belowMin(startDate, min)) startError = "min";
98
+ else if (resolvedMax && aboveMax(startDate, resolvedMax)) startError = "max";
99
+ }
100
+ if (!endError && endDate) {
101
+ if (min && belowMin(endDate, min)) endError = "min";
102
+ else if (resolvedMax && aboveMax(endDate, resolvedMax)) endError = "max";
103
+ }
104
+ let formError;
105
+ const bothFieldsClean = startDate && endDate && !startError && !endError;
106
+ if (bothFieldsClean) {
107
+ if (validateOrder && startDate > endDate) {
108
+ formError = "order";
109
+ } else if (maxRangeYears !== void 0 && exceedsYearSpan(startDate, endDate, maxRangeYears)) {
110
+ formError = "range-limit";
111
+ }
112
+ }
113
+ const valid = !startError && !endError && !formError;
114
+ const startSev = startError ? severityOfFieldError(startError, config) : null;
115
+ const endSev = endError ? severityOfFieldError(endError, config) : null;
116
+ const formSev = formError ? severityOfFormError(formError, config) : null;
117
+ const hasBlockingError = startSev === "error" || endSev === "error" || formSev === "error";
118
+ const applyEnabled = !hasBlockingError && !!startDate && !!endDate;
119
+ return {
120
+ valid,
121
+ applyEnabled,
122
+ ...startError && { startError },
123
+ ...endError && { endError },
124
+ ...formError && { formError }
125
+ };
126
+ }
127
+ function severityOfFieldError(err, config) {
128
+ if (err === "format") return normalizeSeverity(config.formatSeverity);
129
+ if (err === "min") return normalizeSeverity(config.minSeverity);
130
+ return normalizeSeverity(config.maxSeverity);
131
+ }
132
+ function severityOfFormError(err, config) {
133
+ if (err === "order") return normalizeSeverity(config.orderSeverity);
134
+ return normalizeSeverity(config.rangeLimitSeverity);
135
+ }
136
+ function parseIso(s) {
137
+ const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(s);
138
+ if (!m) return null;
139
+ const [, yStr, mStr, dStr] = m;
140
+ const y = Number(yStr);
141
+ const mo = Number(mStr) - 1;
142
+ const d = Number(dStr);
143
+ const dt = new Date(y, mo, d);
144
+ if (dt.getFullYear() !== y || dt.getMonth() !== mo || dt.getDate() !== d) return null;
145
+ dt.setHours(0, 0, 0, 0);
146
+ return dt;
147
+ }
148
+ function isoToday() {
149
+ const now = /* @__PURE__ */ new Date();
150
+ const y = now.getFullYear();
151
+ const m = String(now.getMonth() + 1).padStart(2, "0");
152
+ const d = String(now.getDate()).padStart(2, "0");
153
+ return `${y}-${m}-${d}`;
154
+ }
155
+ function belowMin(date, minIso) {
156
+ const minDate = parseIso(minIso);
157
+ return !!minDate && date < minDate;
158
+ }
159
+ function aboveMax(date, maxIso) {
160
+ const maxDate = parseIso(maxIso);
161
+ return !!maxDate && date > maxDate;
162
+ }
163
+ function exceedsYearSpan(start, end, years) {
164
+ const cutoff = new Date(start);
165
+ cutoff.setFullYear(cutoff.getFullYear() + years);
166
+ return end > cutoff;
167
+ }
6
168
 
7
169
  // css-inline:/home/runner/work/BV_lumen/BV_lumen/components/bv-alert-banner/bv-alert-banner.css
8
170
  var bv_alert_banner_default = ":host {\n --banner-bg: var(--bv-color-neutral-0);\n --banner-border: var(--bv-color-neutral-200);\n --banner-color: var(--bv-color-neutral-700);\n --banner-color-subtle: var(--bv-color-neutral-500);\n --banner-icon-color: var(--bv-color-neutral-500);\n\n display: block;\n font-family: var(--bv-font-family-base);\n}\n\n:host([hidden]) {\n display: none;\n}\n\n:host([variant='primary']) {\n --banner-bg: var(--bv-color-primary-50);\n --banner-border: color-mix(in srgb, var(--bv-color-primary) 40%, var(--bv-color-neutral-0));\n --banner-color: var(--bv-color-primary);\n --banner-color-subtle: var(--bv-color-primary);\n --banner-icon-color: var(--bv-color-primary);\n}\n\n:host([variant='error']) {\n --banner-bg: color-mix(in srgb, var(--bv-color-danger-light) 35%, var(--bv-color-neutral-0));\n --banner-border: color-mix(in srgb, var(--bv-color-danger) 45%, var(--bv-color-neutral-0));\n --banner-color: var(--bv-color-danger-dark);\n --banner-color-subtle: var(--bv-color-danger-dark);\n --banner-icon-color: var(--bv-color-danger);\n}\n\n:host([variant='warning']) {\n --banner-bg: color-mix(in srgb, var(--bv-color-warning-light) 35%, var(--bv-color-neutral-0));\n --banner-border: color-mix(in srgb, var(--bv-color-warning) 50%, var(--bv-color-neutral-0));\n --banner-color: var(--bv-color-warning-dark);\n --banner-color-subtle: var(--bv-color-warning-dark);\n --banner-icon-color: var(--bv-color-warning);\n}\n\n:host([variant='success']) {\n --banner-bg: color-mix(in srgb, var(--bv-color-success-light) 30%, var(--bv-color-neutral-0));\n --banner-border: color-mix(in srgb, var(--bv-color-success) 40%, var(--bv-color-neutral-0));\n --banner-color: var(--bv-color-success-dark);\n --banner-color-subtle: var(--bv-color-success-dark);\n --banner-icon-color: var(--bv-color-success);\n}\n\n/* \u2500\u2500 Banner \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.banner {\n display: flex;\n align-items: flex-start;\n gap: var(--bv-spacing-md);\n padding: var(--bv-spacing-md);\n background: var(--banner-bg);\n border: 1px solid var(--banner-border);\n border-radius: var(--bv-radius-xl);\n}\n\n/* \u2500\u2500 Icon \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.banner-icon {\n display: flex;\n flex-shrink: 0;\n margin-top: 0;\n color: var(--banner-icon-color);\n line-height: 0;\n}\n\n/* \u2500\u2500 Content \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.banner-content {\n flex: 1;\n display: flex;\n flex-direction: column;\n gap: var(--bv-spacing-xs);\n min-width: 0;\n}\n\n.banner-heading {\n margin: 0;\n font-size: var(--bv-font-size-sm);\n font-weight: var(--bv-font-weight-semibold);\n line-height: 20px;\n color: var(--banner-color);\n}\n\n.banner-heading[hidden] {\n display: none;\n}\n\n.banner-message {\n margin: 0;\n font-size: var(--bv-font-size-sm);\n font-weight: var(--bv-font-weight-regular);\n line-height: 20px;\n color: var(--banner-color);\n}\n\n.banner-message[hidden] {\n display: none;\n}\n\n/* \u2500\u2500 Actions \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.banner-actions {\n display: flex;\n align-items: center;\n gap: var(--bv-spacing-md);\n margin-top: var(--bv-spacing-xs);\n}\n\n.banner-actions[hidden] {\n display: none;\n}\n\n.action-btn {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n background: none;\n border: none;\n padding: 0;\n cursor: pointer;\n font-family: inherit;\n font-size: var(--bv-font-size-sm);\n font-weight: var(--bv-font-weight-semibold);\n line-height: 20px;\n color: var(--banner-color);\n transition: opacity 0.15s;\n}\n\n.action-btn[hidden] {\n display: none;\n}\n\n.action-btn:hover {\n opacity: 0.75;\n}\n\n.action-btn:focus-visible {\n outline: 2px solid var(--banner-color);\n outline-offset: 2px;\n border-radius: var(--bv-radius-sm);\n}\n\n/* \u2500\u2500 Close button \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.close-btn {\n display: flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n width: 32px;\n height: 32px;\n margin: -6px -6px 0 0;\n background: none;\n border: none;\n border-radius: var(--bv-radius-lg);\n cursor: pointer;\n color: var(--banner-icon-color);\n transition: background 0.15s;\n padding: 0;\n}\n\n.close-btn[hidden] {\n display: none;\n}\n\n.close-btn:hover {\n background: color-mix(in srgb, var(--banner-border) 60%, var(--banner-bg));\n}\n\n.close-btn:focus-visible {\n outline: 2px solid var(--banner-color);\n outline-offset: 2px;\n}\n";
@@ -21,14 +183,6 @@ var VARIANT_ICONS = {
21
183
  warning: ICON_ALERT_TRIANGLE,
22
184
  success: ICON_CHECK_CIRCLE
23
185
  };
24
- var _sheet = null;
25
- function getSheet() {
26
- if (!_sheet) {
27
- _sheet = new CSSStyleSheet();
28
- _sheet.replaceSync(bv_alert_banner_default);
29
- }
30
- return _sheet;
31
- }
32
186
  var BvAlertBanner = class extends HTMLElement {
33
187
  static observedAttributes = [
34
188
  "variant",
@@ -48,10 +202,7 @@ var BvAlertBanner = class extends HTMLElement {
48
202
  _closeBtn = null;
49
203
  constructor() {
50
204
  super();
51
- const shadow = this.attachShadow({ mode: "open" });
52
- if ("adoptedStyleSheets" in ShadowRoot.prototype) {
53
- shadow.adoptedStyleSheets = [getSheet()];
54
- }
205
+ adoptStyles(this.attachShadow({ mode: "open" }), bv_alert_banner_default);
55
206
  }
56
207
  get variant() {
57
208
  return this.getAttribute("variant") ?? "default";
@@ -105,11 +256,6 @@ var BvAlertBanner = class extends HTMLElement {
105
256
  if (this._rendered) return;
106
257
  this._rendered = true;
107
258
  const shadow = this.shadowRoot;
108
- if (!shadow.adoptedStyleSheets.length) {
109
- const s = document.createElement("style");
110
- s.textContent = bv_alert_banner_default;
111
- shadow.appendChild(s);
112
- }
113
259
  const banner = document.createElement("div");
114
260
  banner.className = "banner";
115
261
  banner.setAttribute("role", "alert");
@@ -236,14 +382,6 @@ var MONTH_NAMES = [
236
382
  var WEEKDAYS = ["Mo", "Tu", "We", "Th", "Fr", "Sat", "Su"];
237
383
  var CHEVRON_LEFT = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 18l-6-6 6-6"/></svg>`;
238
384
  var CHEVRON_RIGHT = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 18l6-6-6-6"/></svg>`;
239
- var _sheet2 = null;
240
- function getSheet2() {
241
- if (!_sheet2) {
242
- _sheet2 = new CSSStyleSheet();
243
- _sheet2.replaceSync(bv_date_range_picker_default);
244
- }
245
- return _sheet2;
246
- }
247
385
  function dateToIso(d) {
248
386
  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
249
387
  }
@@ -259,6 +397,9 @@ function parseDisplay(str) {
259
397
  const d = match[2].padStart(2, "0");
260
398
  return `${match[3]}-${m}-${d}`;
261
399
  }
400
+ function capitalize(s) {
401
+ return s.charAt(0).toUpperCase() + s.slice(1);
402
+ }
262
403
  function getPresetDates(key) {
263
404
  const now = /* @__PURE__ */ new Date();
264
405
  now.setHours(0, 0, 0, 0);
@@ -301,7 +442,31 @@ function getPresetDates(key) {
301
442
  }
302
443
  }
303
444
  var BvDateRangePicker = class extends HTMLElement {
304
- static observedAttributes = ["start", "end", "preset"];
445
+ static observedAttributes = [
446
+ "start",
447
+ "end",
448
+ "preset",
449
+ // BTN-10191 — opt-in validation config
450
+ "min",
451
+ "max",
452
+ "max-range-years",
453
+ "validate-order",
454
+ "validate-format",
455
+ "show-range-limit-banner",
456
+ // BTN-10191 — configurable error copy (all attributes; defaults apply otherwise)
457
+ "error-min",
458
+ "error-max",
459
+ "error-order",
460
+ "error-format",
461
+ "range-limit-message",
462
+ "range-limit-heading",
463
+ // BTN-10312 — per-error severity (default error = blocks Apply + red banner)
464
+ "error-min-severity",
465
+ "error-max-severity",
466
+ "error-format-severity",
467
+ "error-order-severity",
468
+ "range-limit-severity"
469
+ ];
305
470
  _rendered = false;
306
471
  _today;
307
472
  _start = "";
@@ -311,6 +476,12 @@ var BvDateRangePicker = class extends HTMLElement {
311
476
  _hover = "";
312
477
  _leftYear;
313
478
  _leftMonth;
479
+ // BTN-10191 — raw display strings retained when the user types an
480
+ // unparseable value AND `validate-format` is on. Lets V8 fire without
481
+ // corrupting `_start`/`_end` (which stay strictly ISO for consumers).
482
+ _startRawInvalid = "";
483
+ _endRawInvalid = "";
484
+ _validity = { valid: true, applyEnabled: false };
314
485
  _leftGrid = null;
315
486
  _rightGrid = null;
316
487
  _leftTitle = null;
@@ -318,9 +489,22 @@ var BvDateRangePicker = class extends HTMLElement {
318
489
  _startInput = null;
319
490
  _endInput = null;
320
491
  _applyBtn = null;
321
- _bannerEl = null;
492
+ /**
493
+ * Unified alert banner (BTN-10312). Shows AT MOST ONE error at a time. When
494
+ * multiple errors are simultaneously active, the banner reflects the
495
+ * most-recently-edited field (see `_lastEditedField`).
496
+ */
497
+ _alertBannerEl = null;
322
498
  _leftDayMap = /* @__PURE__ */ new Map();
323
499
  _rightDayMap = /* @__PURE__ */ new Map();
500
+ /**
501
+ * BTN-10312 — tracks which field the user most-recently interacted with.
502
+ * When both `startError` and `endError` are active, the banner shows the
503
+ * error corresponding to this field. Set to `null` when no field has been
504
+ * touched (initial render, or external attribute set); the resolver falls
505
+ * back to a stable precedence in that case.
506
+ */
507
+ _lastEditedField = null;
324
508
  constructor() {
325
509
  super();
326
510
  const now = /* @__PURE__ */ new Date();
@@ -328,10 +512,7 @@ var BvDateRangePicker = class extends HTMLElement {
328
512
  this._today = dateToIso(now);
329
513
  this._leftYear = now.getFullYear();
330
514
  this._leftMonth = now.getMonth();
331
- const shadow = this.attachShadow({ mode: "open" });
332
- if ("adoptedStyleSheets" in ShadowRoot.prototype) {
333
- shadow.adoptedStyleSheets = [getSheet2()];
334
- }
515
+ adoptStyles(this.attachShadow({ mode: "open" }), bv_date_range_picker_default);
335
516
  }
336
517
  get start() {
337
518
  return this._start;
@@ -355,14 +536,48 @@ var BvDateRangePicker = class extends HTMLElement {
355
536
  this._render();
356
537
  }
357
538
  attributeChangedCallback(name, _old, val) {
358
- if (name === "start") this._start = val ?? "";
359
- if (name === "end") this._end = val ?? "";
539
+ if (name === "start") {
540
+ this._start = val ?? "";
541
+ this._startRawInvalid = "";
542
+ this._lastEditedField = null;
543
+ }
544
+ if (name === "end") {
545
+ this._end = val ?? "";
546
+ this._endRawInvalid = "";
547
+ this._lastEditedField = null;
548
+ }
360
549
  if (name === "preset") this._preset = val ?? "";
361
550
  if (!this._rendered) return;
551
+ if (name === "start" || name === "end" || name === "preset") {
552
+ this._syncCalendars();
553
+ this._syncInputs();
554
+ this._syncPresets();
555
+ }
556
+ this._syncValidity();
557
+ }
558
+ /**
559
+ * BTN-10312 — public reset of the picker's internal draft state to the
560
+ * currently committed `start` / `end` / `preset` attributes. Called by
561
+ * `bv-date-range-input` when the popup closes without Apply (Cancel,
562
+ * Escape, outside-click) so the next open shows the last-committed value
563
+ * cleanly, with no leftover error banners from the discarded edit.
564
+ *
565
+ * The external value is not modified — this only clears the draft.
566
+ */
567
+ resetDraft() {
568
+ this._start = this.getAttribute("start") ?? "";
569
+ this._end = this.getAttribute("end") ?? "";
570
+ this._preset = this.getAttribute("preset") ?? "";
571
+ this._startRawInvalid = "";
572
+ this._endRawInvalid = "";
573
+ this._picking = false;
574
+ this._hover = "";
575
+ this._lastEditedField = null;
576
+ if (!this._rendered) return;
362
577
  this._syncCalendars();
363
578
  this._syncInputs();
364
579
  this._syncPresets();
365
- this._syncApply();
580
+ this._syncValidity();
366
581
  }
367
582
  _render() {
368
583
  if (this._rendered) return;
@@ -380,11 +595,6 @@ var BvDateRangePicker = class extends HTMLElement {
380
595
  }
381
596
  }
382
597
  const shadow = this.shadowRoot;
383
- if (!shadow.adoptedStyleSheets.length) {
384
- const s = document.createElement("style");
385
- s.textContent = bv_date_range_picker_default;
386
- shadow.appendChild(s);
387
- }
388
598
  const picker = document.createElement("div");
389
599
  picker.className = "picker";
390
600
  const sidebar = document.createElement("div");
@@ -455,14 +665,8 @@ var BvDateRangePicker = class extends HTMLElement {
455
665
  const bannerWrap = document.createElement("div");
456
666
  bannerWrap.className = "banner-wrap";
457
667
  const banner = document.createElement("bv-alert-banner");
458
- banner.setAttribute("variant", "warning");
459
- banner.setAttribute("heading", "Date range exceeds 5 years");
460
- banner.setAttribute(
461
- "message",
462
- "Only CSV download is available for this date range. Live report generation is not supported."
463
- );
464
668
  banner.hidden = true;
465
- this._bannerEl = banner;
669
+ this._alertBannerEl = banner;
466
670
  bannerWrap.appendChild(banner);
467
671
  main.append(calsWrap, bannerWrap, footer);
468
672
  picker.append(sidebar, main);
@@ -470,8 +674,7 @@ var BvDateRangePicker = class extends HTMLElement {
470
674
  this._syncCalendars();
471
675
  this._syncInputs();
472
676
  this._syncPresets();
473
- this._syncApply();
474
- this._syncBanner();
677
+ this._syncValidity();
475
678
  }
476
679
  _buildCalendarShell(side) {
477
680
  const cal = document.createElement("div");
@@ -636,6 +839,7 @@ var BvDateRangePicker = class extends HTMLElement {
636
839
  this._preset = "custom";
637
840
  this._picking = true;
638
841
  this._hover = "";
842
+ this._lastEditedField = "start";
639
843
  } else {
640
844
  if (iso < this._start) {
641
845
  this._end = this._start;
@@ -646,12 +850,14 @@ var BvDateRangePicker = class extends HTMLElement {
646
850
  this._picking = false;
647
851
  this._hover = "";
648
852
  this._preset = "custom";
853
+ this._lastEditedField = "end";
649
854
  }
855
+ this._startRawInvalid = "";
856
+ this._endRawInvalid = "";
650
857
  this._syncCalendars();
651
858
  this._syncInputs();
652
859
  this._syncPresets();
653
- this._syncApply();
654
- this._syncBanner();
860
+ this._syncValidity();
655
861
  }
656
862
  _selectPreset(key) {
657
863
  if (key === "custom") {
@@ -667,6 +873,9 @@ var BvDateRangePicker = class extends HTMLElement {
667
873
  this._preset = key;
668
874
  this._picking = false;
669
875
  this._hover = "";
876
+ this._startRawInvalid = "";
877
+ this._endRawInvalid = "";
878
+ this._lastEditedField = null;
670
879
  if (start) {
671
880
  const parts = start.split("-");
672
881
  const y = parseInt(parts[0], 10);
@@ -679,21 +888,48 @@ var BvDateRangePicker = class extends HTMLElement {
679
888
  this._syncCalendars();
680
889
  this._syncInputs();
681
890
  this._syncPresets();
682
- this._syncApply();
683
- this._syncBanner();
891
+ this._syncValidity();
684
892
  }
685
893
  _onInputBlur(input, field) {
686
- const iso = parseDisplay(input.value);
894
+ this._lastEditedField = field;
895
+ const raw = input.value;
896
+ if (raw === "") {
897
+ if (field === "start") {
898
+ this._start = "";
899
+ this._startRawInvalid = "";
900
+ } else {
901
+ this._end = "";
902
+ this._endRawInvalid = "";
903
+ }
904
+ this._syncCalendars();
905
+ this._syncPresets();
906
+ this._syncValidity();
907
+ return;
908
+ }
909
+ const iso = parseDisplay(raw);
687
910
  if (iso) {
688
- if (field === "start") this._start = iso;
689
- else this._end = iso;
911
+ if (field === "start") {
912
+ this._start = iso;
913
+ this._startRawInvalid = "";
914
+ } else {
915
+ this._end = iso;
916
+ this._endRawInvalid = "";
917
+ }
690
918
  this._picking = false;
691
919
  this._hover = "";
692
920
  this._preset = "custom";
693
921
  this._syncCalendars();
694
922
  this._syncPresets();
695
- this._syncApply();
696
- this._syncBanner();
923
+ this._syncValidity();
924
+ } else if (this.hasAttribute("validate-format")) {
925
+ if (field === "start") {
926
+ this._start = "";
927
+ this._startRawInvalid = raw;
928
+ } else {
929
+ this._end = "";
930
+ this._endRawInvalid = raw;
931
+ }
932
+ this._syncValidity();
697
933
  } else {
698
934
  input.value = formatDisplay(field === "start" ? this._start : this._end);
699
935
  }
@@ -708,33 +944,211 @@ var BvDateRangePicker = class extends HTMLElement {
708
944
  btn.classList.toggle("active", btn.dataset["preset"] === this._preset);
709
945
  });
710
946
  }
711
- _syncApply() {
712
- if (this._applyBtn) {
713
- this._applyBtn.disabled = !this._start || !this._end;
947
+ // ── BTN-10191 — validation orchestrator ──────────────────────────────
948
+ _syncValidity() {
949
+ const cfg = this._readConfig();
950
+ const previous = this._validity;
951
+ this._validity = validateDateRange(
952
+ {
953
+ start: this._start || this._startRawInvalid,
954
+ end: this._end || this._endRawInvalid
955
+ },
956
+ cfg
957
+ );
958
+ this._paintInputInvalidState();
959
+ this._applyAlertBanner(cfg);
960
+ this._applyApplyState();
961
+ this._applyCalendarDisabledState();
962
+ if (previous.valid !== this._validity.valid || previous.applyEnabled !== this._validity.applyEnabled || previous.startError !== this._validity.startError || previous.endError !== this._validity.endError || previous.formError !== this._validity.formError) {
963
+ this.dispatchEvent(
964
+ new CustomEvent("bv-validity-change", {
965
+ bubbles: true,
966
+ composed: true,
967
+ detail: { ...this._validity }
968
+ })
969
+ );
970
+ }
971
+ }
972
+ _readConfig() {
973
+ const min = this.getAttribute("min") || void 0;
974
+ const max = this.getAttribute("max") || void 0;
975
+ const mryStr = this.getAttribute("max-range-years");
976
+ const maxRangeYears = mryStr !== null && mryStr !== "" ? Number(mryStr) : void 0;
977
+ return {
978
+ min,
979
+ max,
980
+ maxRangeYears: Number.isFinite(maxRangeYears) ? maxRangeYears : void 0,
981
+ validateOrder: this.hasAttribute("validate-order"),
982
+ validateFormat: this.hasAttribute("validate-format"),
983
+ minSeverity: normalizeSeverity(this.getAttribute("error-min-severity")),
984
+ maxSeverity: normalizeSeverity(this.getAttribute("error-max-severity")),
985
+ formatSeverity: normalizeSeverity(this.getAttribute("error-format-severity")),
986
+ orderSeverity: normalizeSeverity(this.getAttribute("error-order-severity")),
987
+ rangeLimitSeverity: normalizeSeverity(this.getAttribute("range-limit-severity"))
988
+ };
989
+ }
990
+ /**
991
+ * BTN-10312 — apply the `.invalid` + `aria-invalid` state to input fields
992
+ * whose field-level error is currently blocking (severity `'error'`).
993
+ * Non-blocking severities (warning/primary/success) do not paint the input
994
+ * red so the visual matches the semantic ("not a fatal error").
995
+ */
996
+ _paintInputInvalidState() {
997
+ const v = this._validity;
998
+ const cfg = this._readConfig();
999
+ this._paintOne(this._startInput, v.startError ? severityOfFieldError(v.startError, cfg) : null);
1000
+ this._paintOne(this._endInput, v.endError ? severityOfFieldError(v.endError, cfg) : null);
1001
+ }
1002
+ _paintOne(input, sev) {
1003
+ if (!input) return;
1004
+ if (sev === "error") {
1005
+ input.classList.add("invalid");
1006
+ input.setAttribute("aria-invalid", "true");
1007
+ } else {
1008
+ input.classList.remove("invalid");
1009
+ input.removeAttribute("aria-invalid");
714
1010
  }
715
1011
  }
716
- _syncBanner() {
717
- if (!this._bannerEl) return;
1012
+ /**
1013
+ * BTN-10312 — single alert banner. Picks the "active" error following the
1014
+ * precedence:
1015
+ *
1016
+ * 1. If a form error is active, show it (form errors don't coexist with
1017
+ * field errors by validator design).
1018
+ * 2. If both field errors are active, show the one for
1019
+ * `_lastEditedField` (falling back to `start` if `null`).
1020
+ * 3. Else show whichever single field error is active.
1021
+ * 4. Else hide the banner.
1022
+ *
1023
+ * `show-range-limit-banner="false"` still suppresses the range-limit
1024
+ * banner specifically (existing consumer contract from BTN-10191).
1025
+ */
1026
+ _applyAlertBanner(cfg) {
1027
+ if (!this._alertBannerEl) return;
718
1028
  const pickerEl = this.shadowRoot.querySelector(".picker");
719
- if (!this._start) {
720
- this._bannerEl.hidden = true;
1029
+ const active = this._resolveActiveError();
1030
+ if (!active) {
1031
+ this._alertBannerEl.hidden = true;
1032
+ this._alertBannerEl.removeAttribute("heading");
1033
+ this._alertBannerEl.removeAttribute("message");
1034
+ pickerEl == null ? void 0 : pickerEl.classList.remove("picker--with-banner");
1035
+ return;
1036
+ }
1037
+ if (active.kind === "form" && active.error === "range-limit" && this.getAttribute("show-range-limit-banner") === "false") {
1038
+ this._alertBannerEl.hidden = true;
721
1039
  pickerEl == null ? void 0 : pickerEl.classList.remove("picker--with-banner");
722
1040
  return;
723
1041
  }
724
- const startDate = /* @__PURE__ */ new Date(this._start + "T00:00:00");
725
- const fiveYearsAgo = /* @__PURE__ */ new Date();
726
- fiveYearsAgo.setFullYear(fiveYearsAgo.getFullYear() - 5);
727
- const show = startDate < fiveYearsAgo;
728
- this._bannerEl.hidden = !show;
729
- pickerEl == null ? void 0 : pickerEl.classList.toggle("picker--with-banner", show);
1042
+ const variant = active.kind === "field" ? severityOfFieldError(active.error, cfg) : severityOfFormError(active.error, cfg);
1043
+ this._alertBannerEl.setAttribute("variant", variant);
1044
+ if (active.kind === "form" && active.error === "range-limit") {
1045
+ const yearsAttr = this.getAttribute("max-range-years") ?? "";
1046
+ const heading = this.getAttribute("range-limit-heading") || (yearsAttr ? `Date range exceeds ${yearsAttr} years` : "Date range exceeds the maximum");
1047
+ const message = this.getAttribute("range-limit-message") || "Only CSV download is available for this date range. Live report generation is not supported.";
1048
+ this._alertBannerEl.setAttribute("heading", heading.replace(/\{N\}/g, yearsAttr));
1049
+ this._alertBannerEl.setAttribute("message", message.replace(/\{N\}/g, yearsAttr));
1050
+ } else {
1051
+ this._alertBannerEl.removeAttribute("heading");
1052
+ const message = active.kind === "field" ? this._resolveFieldError(active.error, active.field) : this._resolveOrderError();
1053
+ this._alertBannerEl.setAttribute("message", message);
1054
+ }
1055
+ this._alertBannerEl.hidden = false;
1056
+ pickerEl == null ? void 0 : pickerEl.classList.add("picker--with-banner");
1057
+ }
1058
+ /**
1059
+ * Pick which error the banner should reflect. See `_applyAlertBanner` for
1060
+ * the precedence rules.
1061
+ */
1062
+ _resolveActiveError() {
1063
+ const v = this._validity;
1064
+ if (v.formError) return { kind: "form", error: v.formError };
1065
+ if (v.startError && v.endError) {
1066
+ const winner = this._lastEditedField ?? "start";
1067
+ return {
1068
+ kind: "field",
1069
+ error: winner === "start" ? v.startError : v.endError,
1070
+ field: winner
1071
+ };
1072
+ }
1073
+ if (v.startError) return { kind: "field", error: v.startError, field: "start" };
1074
+ if (v.endError) return { kind: "field", error: v.endError, field: "end" };
1075
+ return null;
1076
+ }
1077
+ _resolveFieldError(err, field) {
1078
+ if (err === "format") {
1079
+ const custom2 = this.getAttribute("error-format");
1080
+ if (custom2) return this._interpolateField(custom2, field);
1081
+ return `Enter a valid ${field} date`;
1082
+ }
1083
+ if (err === "min") {
1084
+ const minIso = this.getAttribute("min") ?? "";
1085
+ const minDisplay = formatDisplay(minIso);
1086
+ const custom2 = this.getAttribute("error-min");
1087
+ if (custom2) return this._interpolateField(custom2, field).replace(/\{min\}/g, minDisplay);
1088
+ return `${capitalize(field)} date must be on or after ${minDisplay}`;
1089
+ }
1090
+ const maxAttr = this.getAttribute("max") ?? "";
1091
+ const resolvedMax = maxAttr === "today" ? isoToday() : maxAttr;
1092
+ const maxDisplay = formatDisplay(resolvedMax);
1093
+ const custom = this.getAttribute("error-max");
1094
+ if (custom) return this._interpolateField(custom, field).replace(/\{max\}/g, maxDisplay);
1095
+ if (maxAttr === "today") return `${capitalize(field)} date cannot be in the future`;
1096
+ return `${capitalize(field)} date cannot be after ${maxDisplay}`;
1097
+ }
1098
+ /**
1099
+ * BTN-10312 — interpolate `{field}` (always lowercase) and `{Field}` (always
1100
+ * capitalized) in a user-provided template. Before this change, the historical
1101
+ * behavior was that `{field}` in custom copy resolved lowercase while the
1102
+ * built-in defaults capitalized inline — inconsistent. Now the split is
1103
+ * explicit: `{field}` = lowercase, `{Field}` = capitalized.
1104
+ */
1105
+ _interpolateField(template, field) {
1106
+ return template.replace(/\{Field\}/g, capitalize(field)).replace(/\{field\}/g, field);
1107
+ }
1108
+ _resolveOrderError() {
1109
+ return this.getAttribute("error-order") || "Start date must be on or before end date";
1110
+ }
1111
+ _applyApplyState() {
1112
+ if (this._applyBtn) {
1113
+ this._applyBtn.disabled = !this._validity.applyEnabled;
1114
+ }
1115
+ }
1116
+ _applyCalendarDisabledState() {
1117
+ const minIso = this.getAttribute("min") || "";
1118
+ const maxAttr = this.getAttribute("max") || "";
1119
+ const maxIso = maxAttr === "today" ? isoToday() : maxAttr;
1120
+ if (!minIso && !maxIso) {
1121
+ this._leftDayMap.forEach((btn) => this._resetDayDisabled(btn));
1122
+ this._rightDayMap.forEach((btn) => this._resetDayDisabled(btn));
1123
+ return;
1124
+ }
1125
+ const paint = (btn, iso) => {
1126
+ const outOfRange = !!minIso && iso < minIso || !!maxIso && iso > maxIso;
1127
+ if (outOfRange) {
1128
+ btn.disabled = true;
1129
+ btn.setAttribute("aria-disabled", "true");
1130
+ btn.classList.add("day--disabled");
1131
+ } else {
1132
+ this._resetDayDisabled(btn);
1133
+ }
1134
+ };
1135
+ this._leftDayMap.forEach(paint);
1136
+ this._rightDayMap.forEach(paint);
1137
+ }
1138
+ _resetDayDisabled(btn) {
1139
+ btn.disabled = false;
1140
+ btn.removeAttribute("aria-disabled");
1141
+ btn.classList.remove("day--disabled");
730
1142
  }
731
1143
  // Auto-insert slashes after MM and DD digits while the user types forward.
732
- // Also syncs the banner live once the date is fully typed.
1144
+ // Also runs full validity sync on every keystroke — this is what clears the
1145
+ // error state immediately on correction (per BTN-10191 a11y requirement).
733
1146
  _setupDateMask(input, field) {
734
1147
  input.addEventListener("input", (e) => {
735
1148
  var _a;
736
1149
  const ev = e;
737
1150
  if ((_a = ev.inputType) == null ? void 0 : _a.startsWith("delete")) return;
1151
+ this._lastEditedField = field;
738
1152
  const digits = input.value.replace(/[^\d]/g, "");
739
1153
  let formatted;
740
1154
  if (digits.length <= 2) {
@@ -746,17 +1160,19 @@ var BvDateRangePicker = class extends HTMLElement {
746
1160
  formatted = digits.slice(0, 2) + "/" + digits.slice(2, 4) + "/" + digits.slice(4, 8);
747
1161
  }
748
1162
  input.value = formatted;
1163
+ if (field === "start") this._startRawInvalid = "";
1164
+ else this._endRawInvalid = "";
749
1165
  if (formatted.length === 10) {
750
1166
  const iso = parseDisplay(formatted);
751
1167
  if (iso) {
752
1168
  if (field === "start") this._start = iso;
753
1169
  else this._end = iso;
754
- this._syncBanner();
755
1170
  }
756
- } else if (field === "start" && formatted.length < 10) {
757
- this._start = "";
758
- this._syncBanner();
1171
+ } else {
1172
+ if (field === "start") this._start = "";
1173
+ else this._end = "";
759
1174
  }
1175
+ this._syncValidity();
760
1176
  });
761
1177
  }
762
1178
  _apply() {
@@ -779,14 +1195,26 @@ if (!customElements.get("bv-date-range-picker")) {
779
1195
 
780
1196
  // components/bv-date-range-input/bv-date-range-input.ts
781
1197
  var CALENDAR_ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>`;
782
- var _sheet3 = null;
783
- function getSheet3() {
784
- if (!_sheet3) {
785
- _sheet3 = new CSSStyleSheet();
786
- _sheet3.replaceSync(bv_date_range_input_default);
787
- }
788
- return _sheet3;
789
- }
1198
+ var PICKER_PASSTHROUGH_ATTRS = [
1199
+ "min",
1200
+ "max",
1201
+ "max-range-years",
1202
+ "validate-order",
1203
+ "validate-format",
1204
+ "show-range-limit-banner",
1205
+ "error-min",
1206
+ "error-max",
1207
+ "error-order",
1208
+ "error-format",
1209
+ "range-limit-message",
1210
+ "range-limit-heading",
1211
+ // BTN-10312 — per-error severity attributes
1212
+ "error-min-severity",
1213
+ "error-max-severity",
1214
+ "error-format-severity",
1215
+ "error-order-severity",
1216
+ "range-limit-severity"
1217
+ ];
790
1218
  function isoToDisplay(iso) {
791
1219
  if (!iso) return "";
792
1220
  const [y, m, d] = iso.split("-");
@@ -798,19 +1226,26 @@ var BvDateRangeInput = class extends HTMLElement {
798
1226
  "start",
799
1227
  "end",
800
1228
  "placeholder",
801
- "required"
1229
+ "required",
1230
+ // BTN-10312 — local (not passed through to picker); gates popup close.
1231
+ "keep-open-while-invalid",
1232
+ ...PICKER_PASSTHROUGH_ATTRS
802
1233
  ];
803
1234
  _rendered = false;
804
1235
  _labelEl = null;
805
1236
  _displayEl = null;
1237
+ _triggerEl = null;
806
1238
  _pickerEl = null;
807
- _offClick = null;
1239
+ _dropdown = null;
1240
+ /**
1241
+ * BTN-10312 — cached validity from the internal picker. `canClose` on the
1242
+ * dropdown controller consults this to gate popup close when
1243
+ * `keep-open-while-invalid` is set.
1244
+ */
1245
+ _pickerValidity = { valid: true, applyEnabled: false };
808
1246
  constructor() {
809
1247
  super();
810
- const shadow = this.attachShadow({ mode: "open" });
811
- if ("adoptedStyleSheets" in ShadowRoot.prototype) {
812
- shadow.adoptedStyleSheets = [getSheet3()];
813
- }
1248
+ adoptStyles(this.attachShadow({ mode: "open" }), bv_date_range_input_default);
814
1249
  }
815
1250
  get start() {
816
1251
  return this.getAttribute("start") ?? "";
@@ -828,23 +1263,29 @@ var BvDateRangeInput = class extends HTMLElement {
828
1263
  this._render();
829
1264
  }
830
1265
  disconnectedCallback() {
831
- if (this._offClick) document.removeEventListener("click", this._offClick);
1266
+ var _a;
1267
+ (_a = this._dropdown) == null ? void 0 : _a.cleanup();
832
1268
  }
833
- attributeChangedCallback(name, _old, _val) {
1269
+ attributeChangedCallback(name, _old, val) {
834
1270
  if (!this._rendered) return;
835
1271
  if (name === "label" || name === "required") this._syncLabel();
836
- if (name === "start" || name === "end") this._syncDisplay();
837
1272
  if (name === "placeholder") this._syncDisplay();
1273
+ if (name === "start" || name === "end") {
1274
+ this._syncDisplay();
1275
+ if (this._pickerEl) {
1276
+ if (val === null) this._pickerEl.removeAttribute(name);
1277
+ else this._pickerEl.setAttribute(name, val);
1278
+ }
1279
+ }
1280
+ if (PICKER_PASSTHROUGH_ATTRS.includes(name) && this._pickerEl) {
1281
+ if (val === null) this._pickerEl.removeAttribute(name);
1282
+ else this._pickerEl.setAttribute(name, val);
1283
+ }
838
1284
  }
839
1285
  _render() {
840
1286
  if (this._rendered) return;
841
1287
  this._rendered = true;
842
1288
  const shadow = this.shadowRoot;
843
- if (!shadow.adoptedStyleSheets.length) {
844
- const s = document.createElement("style");
845
- s.textContent = bv_date_range_input_default;
846
- shadow.appendChild(s);
847
- }
848
1289
  const label = document.createElement("label");
849
1290
  label.className = "field-label";
850
1291
  this._labelEl = label;
@@ -854,6 +1295,7 @@ var BvDateRangeInput = class extends HTMLElement {
854
1295
  trigger.setAttribute("aria-haspopup", "dialog");
855
1296
  trigger.setAttribute("aria-expanded", "false");
856
1297
  trigger.setAttribute("part", "trigger");
1298
+ this._triggerEl = trigger;
857
1299
  const display = document.createElement("span");
858
1300
  display.className = "trigger-display";
859
1301
  this._displayEl = display;
@@ -862,20 +1304,26 @@ var BvDateRangeInput = class extends HTMLElement {
862
1304
  icon.innerHTML = CALENDAR_ICON;
863
1305
  trigger.append(display, icon);
864
1306
  trigger.addEventListener("click", (e) => {
1307
+ var _a;
865
1308
  e.stopPropagation();
866
- this._toggle();
1309
+ (_a = this._dropdown) == null ? void 0 : _a.toggle();
867
1310
  });
868
1311
  const popup = document.createElement("div");
869
1312
  popup.className = "picker-popup";
870
1313
  const picker = document.createElement("bv-date-range-picker");
871
1314
  picker.setAttribute("start", this.getAttribute("start") ?? "");
872
1315
  picker.setAttribute("end", this.getAttribute("end") ?? "");
1316
+ for (const attr of PICKER_PASSTHROUGH_ATTRS) {
1317
+ const val = this.getAttribute(attr);
1318
+ if (val !== null) picker.setAttribute(attr, val);
1319
+ }
873
1320
  this._pickerEl = picker;
874
1321
  picker.addEventListener("bv-change", (e) => {
1322
+ var _a;
875
1323
  const detail = e.detail;
876
1324
  this.setAttribute("start", detail.start);
877
1325
  this.setAttribute("end", detail.end);
878
- this._close();
1326
+ (_a = this._dropdown) == null ? void 0 : _a.close();
879
1327
  this.dispatchEvent(
880
1328
  new CustomEvent("bv-change", {
881
1329
  bubbles: true,
@@ -885,36 +1333,65 @@ var BvDateRangeInput = class extends HTMLElement {
885
1333
  );
886
1334
  });
887
1335
  picker.addEventListener("bv-cancel", () => {
888
- this._close();
1336
+ var _a;
1337
+ (_a = this._dropdown) == null ? void 0 : _a.close();
1338
+ });
1339
+ picker.addEventListener("bv-validity-change", (e) => {
1340
+ const detail = e.detail;
1341
+ this._pickerValidity = detail;
1342
+ const hasBlockingError = !detail.applyEnabled && !!(detail.startError || detail.endError || detail.formError);
1343
+ if (this._triggerEl) {
1344
+ if (hasBlockingError) {
1345
+ this._triggerEl.classList.add("invalid");
1346
+ this._triggerEl.setAttribute("aria-invalid", "true");
1347
+ } else {
1348
+ this._triggerEl.classList.remove("invalid");
1349
+ this._triggerEl.removeAttribute("aria-invalid");
1350
+ }
1351
+ }
1352
+ this.dispatchEvent(
1353
+ new CustomEvent("bv-validity-change", {
1354
+ bubbles: true,
1355
+ composed: true,
1356
+ detail
1357
+ })
1358
+ );
889
1359
  });
890
1360
  popup.appendChild(picker);
891
1361
  shadow.append(label, trigger, popup);
892
1362
  this._syncLabel();
893
1363
  this._syncDisplay();
894
- this._offClick = (e) => {
895
- if (!this.contains(e.target) && !shadow.contains(e.target)) {
896
- this._close();
1364
+ this._dropdown = bindDropdownController(this, {
1365
+ trigger,
1366
+ onBeforeOpen: () => {
1367
+ if (this._pickerEl) {
1368
+ this._pickerEl.setAttribute("start", this.getAttribute("start") ?? "");
1369
+ this._pickerEl.setAttribute("end", this.getAttribute("end") ?? "");
1370
+ }
1371
+ },
1372
+ // BTN-10312 — gate popup close when the consumer opted in via
1373
+ // `keep-open-while-invalid` AND the picker has a blocking error.
1374
+ // Only *implicit* dismissals (Escape / outside-click) are gated —
1375
+ // explicit user actions (Cancel button, Apply, trigger toggle) always
1376
+ // close so the user can never get trapped in an invalid popup.
1377
+ // Non-blocking errors (warning/primary/success severity) do NOT block
1378
+ // close either: the value externally is still the last-committed one.
1379
+ canClose: (reason) => {
1380
+ if (reason === "programmatic") return true;
1381
+ if (!this.hasAttribute("keep-open-while-invalid")) return true;
1382
+ const v = this._pickerValidity;
1383
+ const hasBlockingError = !v.applyEnabled && !!(v.startError || v.endError || v.formError);
1384
+ return !hasBlockingError;
1385
+ },
1386
+ // BTN-10312 — after every successful close (including Apply, Cancel,
1387
+ // Escape, outside-click), reset the picker's internal draft state so the
1388
+ // next open shows the last-committed value cleanly. Apply cases end up
1389
+ // being a no-op because the draft already equals the committed value.
1390
+ onAfterClose: () => {
1391
+ var _a;
1392
+ (_a = this._pickerEl) == null ? void 0 : _a.resetDraft();
897
1393
  }
898
- };
899
- document.addEventListener("click", this._offClick);
900
- }
901
- _toggle() {
902
- if (this.hasAttribute("open")) this._close();
903
- else this._open();
904
- }
905
- _open() {
906
- var _a;
907
- this.setAttribute("open", "");
908
- (_a = this.shadowRoot.querySelector(".trigger")) == null ? void 0 : _a.setAttribute("aria-expanded", "true");
909
- if (this._pickerEl) {
910
- this._pickerEl.setAttribute("start", this.getAttribute("start") ?? "");
911
- this._pickerEl.setAttribute("end", this.getAttribute("end") ?? "");
912
- }
913
- }
914
- _close() {
915
- var _a;
916
- this.removeAttribute("open");
917
- (_a = this.shadowRoot.querySelector(".trigger")) == null ? void 0 : _a.setAttribute("aria-expanded", "false");
1394
+ });
918
1395
  }
919
1396
  _syncLabel() {
920
1397
  if (!this._labelEl) return;