@bluevolt-tech/lumen 2.1.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -21,8 +21,7 @@ function adoptStyles(shadow, css) {
21
21
 
22
22
  // components/_shared/dropdown-controller.ts
23
23
  function bindDropdownController(host, options = {}) {
24
- const shadow = host.shadowRoot;
25
- if (!shadow) {
24
+ if (!host.shadowRoot) {
26
25
  throw new Error("bindDropdownController requires the host to have an open shadow root.");
27
26
  }
28
27
  const syncExpanded = () => {
@@ -37,24 +36,34 @@ function bindDropdownController(host, options = {}) {
37
36
  syncExpanded();
38
37
  host.dispatchEvent(new CustomEvent("bv-open", { bubbles: true, composed: true }));
39
38
  };
40
- const close = () => {
39
+ const closeWithReason = (reason) => {
40
+ var _a;
41
41
  if (!host.hasAttribute("open")) return;
42
+ if (options.canClose && !options.canClose(reason)) return;
42
43
  host.removeAttribute("open");
43
44
  syncExpanded();
45
+ host.dispatchEvent(
46
+ new CustomEvent("bv-dropdown-close", {
47
+ bubbles: true,
48
+ composed: true,
49
+ detail: { reason }
50
+ })
51
+ );
52
+ (_a = options.onAfterClose) == null ? void 0 : _a.call(options);
44
53
  };
54
+ const close = () => closeWithReason("programmatic");
45
55
  const toggle = () => {
46
- if (host.hasAttribute("open")) close();
56
+ if (host.hasAttribute("open")) closeWithReason("programmatic");
47
57
  else open();
48
58
  };
49
59
  const offClick = (e) => {
50
60
  if (!host.hasAttribute("open")) return;
51
- const target = e.target;
52
- if (host.contains(target) || shadow.contains(target)) return;
53
- close();
61
+ if (e.composedPath().includes(host)) return;
62
+ closeWithReason("outside-click");
54
63
  };
55
64
  document.addEventListener("click", offClick);
56
65
  const onKeyDown = (e) => {
57
- if (e.key === "Escape") close();
66
+ if (e.key === "Escape") closeWithReason("escape");
58
67
  };
59
68
  host.addEventListener("keydown", onKeyDown);
60
69
  const observer = new MutationObserver(syncExpanded);
@@ -69,9 +78,16 @@ function bindDropdownController(host, options = {}) {
69
78
  }
70
79
 
71
80
  // css-inline:/home/runner/work/BV_lumen/BV_lumen/components/bv-date-range-picker/bv-date-range-picker.css
72
- 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\n/* \u2500\u2500 BTN-10191 \xB7 validation error states \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\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.error-messages {\n padding: var(--bv-spacing-2xs) 0 0 0;\n}\n\n.input-error,\n.form-error {\n color: var(--bv-color-danger);\n font-size: var(--bv-font-size-xs);\n line-height: 1.25;\n margin-top: var(--bv-spacing-2xs);\n}\n\n.form-error {\n font-size: var(--bv-font-size-sm);\n margin-top: var(--bv-spacing-xs);\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";
81
+ 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";
73
82
 
74
83
  // components/_shared/date-range-validator.ts
84
+ var ALLOWED_SEVERITIES = ["error", "warning", "primary", "success"];
85
+ function normalizeSeverity(value) {
86
+ if (value && ALLOWED_SEVERITIES.includes(value)) {
87
+ return value;
88
+ }
89
+ return "error";
90
+ }
75
91
  function validateDateRange(input, config = {}) {
76
92
  const { start, end } = input;
77
93
  const { min, max, maxRangeYears, validateOrder, validateFormat } = config;
@@ -102,7 +118,11 @@ function validateDateRange(input, config = {}) {
102
118
  }
103
119
  }
104
120
  const valid = !startError && !endError && !formError;
105
- const applyEnabled = valid && !!startDate && !!endDate;
121
+ const startSev = startError ? severityOfFieldError(startError, config) : null;
122
+ const endSev = endError ? severityOfFieldError(endError, config) : null;
123
+ const formSev = formError ? severityOfFormError(formError, config) : null;
124
+ const hasBlockingError = startSev === "error" || endSev === "error" || formSev === "error";
125
+ const applyEnabled = !hasBlockingError && !!startDate && !!endDate;
106
126
  return {
107
127
  valid,
108
128
  applyEnabled,
@@ -111,6 +131,15 @@ function validateDateRange(input, config = {}) {
111
131
  ...formError && { formError }
112
132
  };
113
133
  }
134
+ function severityOfFieldError(err, config) {
135
+ if (err === "format") return normalizeSeverity(config.formatSeverity);
136
+ if (err === "min") return normalizeSeverity(config.minSeverity);
137
+ return normalizeSeverity(config.maxSeverity);
138
+ }
139
+ function severityOfFormError(err, config) {
140
+ if (err === "order") return normalizeSeverity(config.orderSeverity);
141
+ return normalizeSeverity(config.rangeLimitSeverity);
142
+ }
114
143
  function parseIso(s) {
115
144
  const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(s);
116
145
  if (!m) return null;
@@ -437,7 +466,13 @@ var BvDateRangePicker = class extends HTMLElement {
437
466
  "error-order",
438
467
  "error-format",
439
468
  "range-limit-message",
440
- "range-limit-heading"
469
+ "range-limit-heading",
470
+ // BTN-10312 — per-error severity (default error = blocks Apply + red banner)
471
+ "error-min-severity",
472
+ "error-max-severity",
473
+ "error-format-severity",
474
+ "error-order-severity",
475
+ "range-limit-severity"
441
476
  ];
442
477
  _rendered = false;
443
478
  _today;
@@ -461,12 +496,22 @@ var BvDateRangePicker = class extends HTMLElement {
461
496
  _startInput = null;
462
497
  _endInput = null;
463
498
  _applyBtn = null;
464
- _bannerEl = null;
465
- _startErrorEl = null;
466
- _endErrorEl = null;
467
- _formErrorEl = null;
499
+ /**
500
+ * Unified alert banner (BTN-10312). Shows AT MOST ONE error at a time. When
501
+ * multiple errors are simultaneously active, the banner reflects the
502
+ * most-recently-edited field (see `_lastEditedField`).
503
+ */
504
+ _alertBannerEl = null;
468
505
  _leftDayMap = /* @__PURE__ */ new Map();
469
506
  _rightDayMap = /* @__PURE__ */ new Map();
507
+ /**
508
+ * BTN-10312 — tracks which field the user most-recently interacted with.
509
+ * When both `startError` and `endError` are active, the banner shows the
510
+ * error corresponding to this field. Set to `null` when no field has been
511
+ * touched (initial render, or external attribute set); the resolver falls
512
+ * back to a stable precedence in that case.
513
+ */
514
+ _lastEditedField = null;
470
515
  constructor() {
471
516
  super();
472
517
  const now = /* @__PURE__ */ new Date();
@@ -501,10 +546,12 @@ var BvDateRangePicker = class extends HTMLElement {
501
546
  if (name === "start") {
502
547
  this._start = val ?? "";
503
548
  this._startRawInvalid = "";
549
+ this._lastEditedField = null;
504
550
  }
505
551
  if (name === "end") {
506
552
  this._end = val ?? "";
507
553
  this._endRawInvalid = "";
554
+ this._lastEditedField = null;
508
555
  }
509
556
  if (name === "preset") this._preset = val ?? "";
510
557
  if (!this._rendered) return;
@@ -515,6 +562,30 @@ var BvDateRangePicker = class extends HTMLElement {
515
562
  }
516
563
  this._syncValidity();
517
564
  }
565
+ /**
566
+ * BTN-10312 — public reset of the picker's internal draft state to the
567
+ * currently committed `start` / `end` / `preset` attributes. Called by
568
+ * `bv-date-range-input` when the popup closes without Apply (Cancel,
569
+ * Escape, outside-click) so the next open shows the last-committed value
570
+ * cleanly, with no leftover error banners from the discarded edit.
571
+ *
572
+ * The external value is not modified — this only clears the draft.
573
+ */
574
+ resetDraft() {
575
+ this._start = this.getAttribute("start") ?? "";
576
+ this._end = this.getAttribute("end") ?? "";
577
+ this._preset = this.getAttribute("preset") ?? "";
578
+ this._startRawInvalid = "";
579
+ this._endRawInvalid = "";
580
+ this._picking = false;
581
+ this._hover = "";
582
+ this._lastEditedField = null;
583
+ if (!this._rendered) return;
584
+ this._syncCalendars();
585
+ this._syncInputs();
586
+ this._syncPresets();
587
+ this._syncValidity();
588
+ }
518
589
  _render() {
519
590
  if (this._rendered) return;
520
591
  this._rendered = true;
@@ -598,31 +669,11 @@ var BvDateRangePicker = class extends HTMLElement {
598
669
  this._applyBtn = applyBtn;
599
670
  actions.append(cancelBtn, applyBtn);
600
671
  footer.append(inputsWrap, actions);
601
- const errorsWrap = document.createElement("div");
602
- errorsWrap.className = "error-messages";
603
- const startErrorEl = document.createElement("div");
604
- startErrorEl.className = "input-error input-error--start";
605
- startErrorEl.setAttribute("role", "alert");
606
- startErrorEl.hidden = true;
607
- this._startErrorEl = startErrorEl;
608
- const endErrorEl = document.createElement("div");
609
- endErrorEl.className = "input-error input-error--end";
610
- endErrorEl.setAttribute("role", "alert");
611
- endErrorEl.hidden = true;
612
- this._endErrorEl = endErrorEl;
613
- const formErrorEl = document.createElement("div");
614
- formErrorEl.className = "form-error";
615
- formErrorEl.setAttribute("role", "alert");
616
- formErrorEl.hidden = true;
617
- this._formErrorEl = formErrorEl;
618
- errorsWrap.append(startErrorEl, endErrorEl, formErrorEl);
619
- footer.insertBefore(errorsWrap, actions);
620
672
  const bannerWrap = document.createElement("div");
621
673
  bannerWrap.className = "banner-wrap";
622
674
  const banner = document.createElement("bv-alert-banner");
623
- banner.setAttribute("variant", "warning");
624
675
  banner.hidden = true;
625
- this._bannerEl = banner;
676
+ this._alertBannerEl = banner;
626
677
  bannerWrap.appendChild(banner);
627
678
  main.append(calsWrap, bannerWrap, footer);
628
679
  picker.append(sidebar, main);
@@ -795,6 +846,7 @@ var BvDateRangePicker = class extends HTMLElement {
795
846
  this._preset = "custom";
796
847
  this._picking = true;
797
848
  this._hover = "";
849
+ this._lastEditedField = "start";
798
850
  } else {
799
851
  if (iso < this._start) {
800
852
  this._end = this._start;
@@ -805,6 +857,7 @@ var BvDateRangePicker = class extends HTMLElement {
805
857
  this._picking = false;
806
858
  this._hover = "";
807
859
  this._preset = "custom";
860
+ this._lastEditedField = "end";
808
861
  }
809
862
  this._startRawInvalid = "";
810
863
  this._endRawInvalid = "";
@@ -829,6 +882,7 @@ var BvDateRangePicker = class extends HTMLElement {
829
882
  this._hover = "";
830
883
  this._startRawInvalid = "";
831
884
  this._endRawInvalid = "";
885
+ this._lastEditedField = null;
832
886
  if (start) {
833
887
  const parts = start.split("-");
834
888
  const y = parseInt(parts[0], 10);
@@ -844,6 +898,7 @@ var BvDateRangePicker = class extends HTMLElement {
844
898
  this._syncValidity();
845
899
  }
846
900
  _onInputBlur(input, field) {
901
+ this._lastEditedField = field;
847
902
  const raw = input.value;
848
903
  if (raw === "") {
849
904
  if (field === "start") {
@@ -907,9 +962,9 @@ var BvDateRangePicker = class extends HTMLElement {
907
962
  },
908
963
  cfg
909
964
  );
910
- this._applyErrorStates();
965
+ this._paintInputInvalidState();
966
+ this._applyAlertBanner(cfg);
911
967
  this._applyApplyState();
912
- this._applyBannerState();
913
968
  this._applyCalendarDisabledState();
914
969
  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) {
915
970
  this.dispatchEvent(
@@ -931,88 +986,140 @@ var BvDateRangePicker = class extends HTMLElement {
931
986
  max,
932
987
  maxRangeYears: Number.isFinite(maxRangeYears) ? maxRangeYears : void 0,
933
988
  validateOrder: this.hasAttribute("validate-order"),
934
- validateFormat: this.hasAttribute("validate-format")
989
+ validateFormat: this.hasAttribute("validate-format"),
990
+ minSeverity: normalizeSeverity(this.getAttribute("error-min-severity")),
991
+ maxSeverity: normalizeSeverity(this.getAttribute("error-max-severity")),
992
+ formatSeverity: normalizeSeverity(this.getAttribute("error-format-severity")),
993
+ orderSeverity: normalizeSeverity(this.getAttribute("error-order-severity")),
994
+ rangeLimitSeverity: normalizeSeverity(this.getAttribute("range-limit-severity"))
935
995
  };
936
996
  }
937
- _applyErrorStates() {
997
+ /**
998
+ * BTN-10312 — apply the `.invalid` + `aria-invalid` state to input fields
999
+ * whose field-level error is currently blocking (severity `'error'`).
1000
+ * Non-blocking severities (warning/primary/success) do not paint the input
1001
+ * red so the visual matches the semantic ("not a fatal error").
1002
+ */
1003
+ _paintInputInvalidState() {
938
1004
  const v = this._validity;
939
- this._paintInputError(this._startInput, this._startErrorEl, v.startError, "start");
940
- this._paintInputError(this._endInput, this._endErrorEl, v.endError, "end");
941
- if (this._formErrorEl) {
942
- if (v.formError === "order") {
943
- this._formErrorEl.textContent = this._resolveOrderError();
944
- this._formErrorEl.hidden = false;
945
- } else {
946
- this._formErrorEl.textContent = "";
947
- this._formErrorEl.hidden = true;
948
- }
1005
+ const cfg = this._readConfig();
1006
+ this._paintOne(this._startInput, v.startError ? severityOfFieldError(v.startError, cfg) : null);
1007
+ this._paintOne(this._endInput, v.endError ? severityOfFieldError(v.endError, cfg) : null);
1008
+ }
1009
+ _paintOne(input, sev) {
1010
+ if (!input) return;
1011
+ if (sev === "error") {
1012
+ input.classList.add("invalid");
1013
+ input.setAttribute("aria-invalid", "true");
1014
+ } else {
1015
+ input.classList.remove("invalid");
1016
+ input.removeAttribute("aria-invalid");
949
1017
  }
950
1018
  }
951
- _paintInputError(input, errorEl, err, field) {
952
- if (input) {
953
- if (err) {
954
- input.classList.add("invalid");
955
- input.setAttribute("aria-invalid", "true");
956
- } else {
957
- input.classList.remove("invalid");
958
- input.removeAttribute("aria-invalid");
959
- }
1019
+ /**
1020
+ * BTN-10312 — single alert banner. Picks the "active" error following the
1021
+ * precedence:
1022
+ *
1023
+ * 1. If a form error is active, show it (form errors don't coexist with
1024
+ * field errors by validator design).
1025
+ * 2. If both field errors are active, show the one for
1026
+ * `_lastEditedField` (falling back to `start` if `null`).
1027
+ * 3. Else show whichever single field error is active.
1028
+ * 4. Else hide the banner.
1029
+ *
1030
+ * `show-range-limit-banner="false"` still suppresses the range-limit
1031
+ * banner specifically (existing consumer contract from BTN-10191).
1032
+ */
1033
+ _applyAlertBanner(cfg) {
1034
+ if (!this._alertBannerEl) return;
1035
+ const pickerEl = this.shadowRoot.querySelector(".picker");
1036
+ const active = this._resolveActiveError();
1037
+ if (!active) {
1038
+ this._alertBannerEl.hidden = true;
1039
+ this._alertBannerEl.removeAttribute("heading");
1040
+ this._alertBannerEl.removeAttribute("message");
1041
+ pickerEl == null ? void 0 : pickerEl.classList.remove("picker--with-banner");
1042
+ return;
960
1043
  }
961
- if (errorEl) {
962
- if (err) {
963
- errorEl.textContent = this._resolveFieldError(err, field);
964
- errorEl.hidden = false;
965
- } else {
966
- errorEl.textContent = "";
967
- errorEl.hidden = true;
968
- }
1044
+ if (active.kind === "form" && active.error === "range-limit" && this.getAttribute("show-range-limit-banner") === "false") {
1045
+ this._alertBannerEl.hidden = true;
1046
+ pickerEl == null ? void 0 : pickerEl.classList.remove("picker--with-banner");
1047
+ return;
969
1048
  }
1049
+ const variant = active.kind === "field" ? severityOfFieldError(active.error, cfg) : severityOfFormError(active.error, cfg);
1050
+ this._alertBannerEl.setAttribute("variant", variant);
1051
+ if (active.kind === "form" && active.error === "range-limit") {
1052
+ const yearsAttr = this.getAttribute("max-range-years") ?? "";
1053
+ const heading = this.getAttribute("range-limit-heading") || (yearsAttr ? `Date range exceeds ${yearsAttr} years` : "Date range exceeds the maximum");
1054
+ const message = this.getAttribute("range-limit-message") || "Only CSV download is available for this date range. Live report generation is not supported.";
1055
+ this._alertBannerEl.setAttribute("heading", heading.replace(/\{N\}/g, yearsAttr));
1056
+ this._alertBannerEl.setAttribute("message", message.replace(/\{N\}/g, yearsAttr));
1057
+ } else {
1058
+ this._alertBannerEl.removeAttribute("heading");
1059
+ const message = active.kind === "field" ? this._resolveFieldError(active.error, active.field) : this._resolveOrderError();
1060
+ this._alertBannerEl.setAttribute("message", message);
1061
+ }
1062
+ this._alertBannerEl.hidden = false;
1063
+ pickerEl == null ? void 0 : pickerEl.classList.add("picker--with-banner");
1064
+ }
1065
+ /**
1066
+ * Pick which error the banner should reflect. See `_applyAlertBanner` for
1067
+ * the precedence rules.
1068
+ */
1069
+ _resolveActiveError() {
1070
+ const v = this._validity;
1071
+ if (v.formError) return { kind: "form", error: v.formError };
1072
+ if (v.startError && v.endError) {
1073
+ const winner = this._lastEditedField ?? "start";
1074
+ return {
1075
+ kind: "field",
1076
+ error: winner === "start" ? v.startError : v.endError,
1077
+ field: winner
1078
+ };
1079
+ }
1080
+ if (v.startError) return { kind: "field", error: v.startError, field: "start" };
1081
+ if (v.endError) return { kind: "field", error: v.endError, field: "end" };
1082
+ return null;
970
1083
  }
971
1084
  _resolveFieldError(err, field) {
972
1085
  if (err === "format") {
973
- return this.getAttribute("error-format") ?? "Enter a valid date in MM/DD/YYYY format";
1086
+ const custom2 = this.getAttribute("error-format");
1087
+ if (custom2) return this._interpolateField(custom2, field);
1088
+ return `Enter a valid ${field} date`;
974
1089
  }
975
1090
  if (err === "min") {
976
1091
  const minIso = this.getAttribute("min") ?? "";
977
1092
  const minDisplay = formatDisplay(minIso);
978
1093
  const custom2 = this.getAttribute("error-min");
979
- if (custom2) return custom2.replace("{min}", minDisplay).replace("{field}", field);
1094
+ if (custom2) return this._interpolateField(custom2, field).replace(/\{min\}/g, minDisplay);
980
1095
  return `${capitalize(field)} date must be on or after ${minDisplay}`;
981
1096
  }
982
1097
  const maxAttr = this.getAttribute("max") ?? "";
983
1098
  const resolvedMax = maxAttr === "today" ? isoToday() : maxAttr;
984
1099
  const maxDisplay = formatDisplay(resolvedMax);
985
1100
  const custom = this.getAttribute("error-max");
986
- if (custom) return custom.replace("{max}", maxDisplay).replace("{field}", field);
1101
+ if (custom) return this._interpolateField(custom, field).replace(/\{max\}/g, maxDisplay);
987
1102
  if (maxAttr === "today") return `${capitalize(field)} date cannot be in the future`;
988
1103
  return `${capitalize(field)} date cannot be after ${maxDisplay}`;
989
1104
  }
1105
+ /**
1106
+ * BTN-10312 — interpolate `{field}` (always lowercase) and `{Field}` (always
1107
+ * capitalized) in a user-provided template. Before this change, the historical
1108
+ * behavior was that `{field}` in custom copy resolved lowercase while the
1109
+ * built-in defaults capitalized inline — inconsistent. Now the split is
1110
+ * explicit: `{field}` = lowercase, `{Field}` = capitalized.
1111
+ */
1112
+ _interpolateField(template, field) {
1113
+ return template.replace(/\{Field\}/g, capitalize(field)).replace(/\{field\}/g, field);
1114
+ }
990
1115
  _resolveOrderError() {
991
- return this.getAttribute("error-order") ?? "Start date must be on or before end date";
1116
+ return this.getAttribute("error-order") || "Start date must be on or before end date";
992
1117
  }
993
1118
  _applyApplyState() {
994
1119
  if (this._applyBtn) {
995
1120
  this._applyBtn.disabled = !this._validity.applyEnabled;
996
1121
  }
997
1122
  }
998
- _applyBannerState() {
999
- if (!this._bannerEl) return;
1000
- const pickerEl = this.shadowRoot.querySelector(".picker");
1001
- const bannerAllowed = this.getAttribute("show-range-limit-banner") !== "false";
1002
- const show = this._validity.formError === "range-limit" && bannerAllowed;
1003
- if (show) {
1004
- const yearsAttr = this.getAttribute("max-range-years") ?? "";
1005
- const heading = this.getAttribute("range-limit-heading") ?? (yearsAttr ? `Date range exceeds ${yearsAttr} years` : "Date range exceeds the maximum");
1006
- const message = this.getAttribute("range-limit-message") ?? "Only CSV download is available for this date range. Live report generation is not supported.";
1007
- this._bannerEl.setAttribute("heading", heading.replace("{N}", yearsAttr));
1008
- this._bannerEl.setAttribute("message", message.replace("{N}", yearsAttr));
1009
- this._bannerEl.hidden = false;
1010
- pickerEl == null ? void 0 : pickerEl.classList.add("picker--with-banner");
1011
- } else {
1012
- this._bannerEl.hidden = true;
1013
- pickerEl == null ? void 0 : pickerEl.classList.remove("picker--with-banner");
1014
- }
1015
- }
1016
1123
  _applyCalendarDisabledState() {
1017
1124
  const minIso = this.getAttribute("min") || "";
1018
1125
  const maxAttr = this.getAttribute("max") || "";
@@ -1048,6 +1155,7 @@ var BvDateRangePicker = class extends HTMLElement {
1048
1155
  var _a;
1049
1156
  const ev = e;
1050
1157
  if ((_a = ev.inputType) == null ? void 0 : _a.startsWith("delete")) return;
1158
+ this._lastEditedField = field;
1051
1159
  const digits = input.value.replace(/[^\d]/g, "");
1052
1160
  let formatted;
1053
1161
  if (digits.length <= 2) {
@@ -1106,7 +1214,13 @@ var PICKER_PASSTHROUGH_ATTRS = [
1106
1214
  "error-order",
1107
1215
  "error-format",
1108
1216
  "range-limit-message",
1109
- "range-limit-heading"
1217
+ "range-limit-heading",
1218
+ // BTN-10312 — per-error severity attributes
1219
+ "error-min-severity",
1220
+ "error-max-severity",
1221
+ "error-format-severity",
1222
+ "error-order-severity",
1223
+ "range-limit-severity"
1110
1224
  ];
1111
1225
  function isoToDisplay(iso) {
1112
1226
  if (!iso) return "";
@@ -1120,6 +1234,8 @@ var BvDateRangeInput = class extends HTMLElement {
1120
1234
  "end",
1121
1235
  "placeholder",
1122
1236
  "required",
1237
+ // BTN-10312 — local (not passed through to picker); gates popup close.
1238
+ "keep-open-while-invalid",
1123
1239
  ...PICKER_PASSTHROUGH_ATTRS
1124
1240
  ];
1125
1241
  _rendered = false;
@@ -1128,6 +1244,12 @@ var BvDateRangeInput = class extends HTMLElement {
1128
1244
  _triggerEl = null;
1129
1245
  _pickerEl = null;
1130
1246
  _dropdown = null;
1247
+ /**
1248
+ * BTN-10312 — cached validity from the internal picker. `canClose` on the
1249
+ * dropdown controller consults this to gate popup close when
1250
+ * `keep-open-while-invalid` is set.
1251
+ */
1252
+ _pickerValidity = { valid: true, applyEnabled: false };
1131
1253
  constructor() {
1132
1254
  super();
1133
1255
  adoptStyles(this.attachShadow({ mode: "open" }), bv_date_range_input_default);
@@ -1188,9 +1310,8 @@ var BvDateRangeInput = class extends HTMLElement {
1188
1310
  icon.className = "trigger-icon";
1189
1311
  icon.innerHTML = CALENDAR_ICON;
1190
1312
  trigger.append(display, icon);
1191
- trigger.addEventListener("click", (e) => {
1313
+ trigger.addEventListener("click", () => {
1192
1314
  var _a;
1193
- e.stopPropagation();
1194
1315
  (_a = this._dropdown) == null ? void 0 : _a.toggle();
1195
1316
  });
1196
1317
  const popup = document.createElement("div");
@@ -1223,13 +1344,15 @@ var BvDateRangeInput = class extends HTMLElement {
1223
1344
  });
1224
1345
  picker.addEventListener("bv-validity-change", (e) => {
1225
1346
  const detail = e.detail;
1347
+ this._pickerValidity = detail;
1348
+ const hasBlockingError = !detail.applyEnabled && !!(detail.startError || detail.endError || detail.formError);
1226
1349
  if (this._triggerEl) {
1227
- if (detail.valid) {
1228
- this._triggerEl.classList.remove("invalid");
1229
- this._triggerEl.removeAttribute("aria-invalid");
1230
- } else {
1350
+ if (hasBlockingError) {
1231
1351
  this._triggerEl.classList.add("invalid");
1232
1352
  this._triggerEl.setAttribute("aria-invalid", "true");
1353
+ } else {
1354
+ this._triggerEl.classList.remove("invalid");
1355
+ this._triggerEl.removeAttribute("aria-invalid");
1233
1356
  }
1234
1357
  }
1235
1358
  this.dispatchEvent(
@@ -1251,6 +1374,28 @@ var BvDateRangeInput = class extends HTMLElement {
1251
1374
  this._pickerEl.setAttribute("start", this.getAttribute("start") ?? "");
1252
1375
  this._pickerEl.setAttribute("end", this.getAttribute("end") ?? "");
1253
1376
  }
1377
+ },
1378
+ // BTN-10312 — gate popup close when the consumer opted in via
1379
+ // `keep-open-while-invalid` AND the picker has a blocking error.
1380
+ // Only *implicit* dismissals (Escape / outside-click) are gated —
1381
+ // explicit user actions (Cancel button, Apply, trigger toggle) always
1382
+ // close so the user can never get trapped in an invalid popup.
1383
+ // Non-blocking errors (warning/primary/success severity) do NOT block
1384
+ // close either: the value externally is still the last-committed one.
1385
+ canClose: (reason) => {
1386
+ if (reason === "programmatic") return true;
1387
+ if (!this.hasAttribute("keep-open-while-invalid")) return true;
1388
+ const v = this._pickerValidity;
1389
+ const hasBlockingError = !v.applyEnabled && !!(v.startError || v.endError || v.formError);
1390
+ return !hasBlockingError;
1391
+ },
1392
+ // BTN-10312 — after every successful close (including Apply, Cancel,
1393
+ // Escape, outside-click), reset the picker's internal draft state so the
1394
+ // next open shows the last-committed value cleanly. Apply cases end up
1395
+ // being a no-op because the draft already equals the committed value.
1396
+ onAfterClose: () => {
1397
+ var _a;
1398
+ (_a = this._pickerEl) == null ? void 0 : _a.resetDraft();
1254
1399
  }
1255
1400
  });
1256
1401
  }