@bluevolt-tech/lumen 2.1.0 → 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.
@@ -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,27 @@ 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
+ (_a = options.onAfterClose) == null ? void 0 : _a.call(options);
44
46
  };
47
+ const close = () => closeWithReason("programmatic");
45
48
  const toggle = () => {
46
- if (host.hasAttribute("open")) close();
49
+ if (host.hasAttribute("open")) closeWithReason("programmatic");
47
50
  else open();
48
51
  };
49
52
  const offClick = (e) => {
50
53
  if (!host.hasAttribute("open")) return;
51
- const target = e.target;
52
- if (host.contains(target) || shadow.contains(target)) return;
53
- close();
54
+ if (e.composedPath().includes(host)) return;
55
+ closeWithReason("outside-click");
54
56
  };
55
57
  document.addEventListener("click", offClick);
56
58
  const onKeyDown = (e) => {
57
- if (e.key === "Escape") close();
59
+ if (e.key === "Escape") closeWithReason("escape");
58
60
  };
59
61
  host.addEventListener("keydown", onKeyDown);
60
62
  const observer = new MutationObserver(syncExpanded);
@@ -69,9 +71,16 @@ function bindDropdownController(host, options = {}) {
69
71
  }
70
72
 
71
73
  // 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";
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";
73
75
 
74
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
+ }
75
84
  function validateDateRange(input, config = {}) {
76
85
  const { start, end } = input;
77
86
  const { min, max, maxRangeYears, validateOrder, validateFormat } = config;
@@ -102,7 +111,11 @@ function validateDateRange(input, config = {}) {
102
111
  }
103
112
  }
104
113
  const valid = !startError && !endError && !formError;
105
- const applyEnabled = valid && !!startDate && !!endDate;
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;
106
119
  return {
107
120
  valid,
108
121
  applyEnabled,
@@ -111,6 +124,15 @@ function validateDateRange(input, config = {}) {
111
124
  ...formError && { formError }
112
125
  };
113
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
+ }
114
136
  function parseIso(s) {
115
137
  const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(s);
116
138
  if (!m) return null;
@@ -437,7 +459,13 @@ var BvDateRangePicker = class extends HTMLElement {
437
459
  "error-order",
438
460
  "error-format",
439
461
  "range-limit-message",
440
- "range-limit-heading"
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"
441
469
  ];
442
470
  _rendered = false;
443
471
  _today;
@@ -461,12 +489,22 @@ var BvDateRangePicker = class extends HTMLElement {
461
489
  _startInput = null;
462
490
  _endInput = null;
463
491
  _applyBtn = null;
464
- _bannerEl = null;
465
- _startErrorEl = null;
466
- _endErrorEl = null;
467
- _formErrorEl = 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;
468
498
  _leftDayMap = /* @__PURE__ */ new Map();
469
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;
470
508
  constructor() {
471
509
  super();
472
510
  const now = /* @__PURE__ */ new Date();
@@ -501,10 +539,12 @@ var BvDateRangePicker = class extends HTMLElement {
501
539
  if (name === "start") {
502
540
  this._start = val ?? "";
503
541
  this._startRawInvalid = "";
542
+ this._lastEditedField = null;
504
543
  }
505
544
  if (name === "end") {
506
545
  this._end = val ?? "";
507
546
  this._endRawInvalid = "";
547
+ this._lastEditedField = null;
508
548
  }
509
549
  if (name === "preset") this._preset = val ?? "";
510
550
  if (!this._rendered) return;
@@ -515,6 +555,30 @@ var BvDateRangePicker = class extends HTMLElement {
515
555
  }
516
556
  this._syncValidity();
517
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;
577
+ this._syncCalendars();
578
+ this._syncInputs();
579
+ this._syncPresets();
580
+ this._syncValidity();
581
+ }
518
582
  _render() {
519
583
  if (this._rendered) return;
520
584
  this._rendered = true;
@@ -598,31 +662,11 @@ var BvDateRangePicker = class extends HTMLElement {
598
662
  this._applyBtn = applyBtn;
599
663
  actions.append(cancelBtn, applyBtn);
600
664
  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
665
  const bannerWrap = document.createElement("div");
621
666
  bannerWrap.className = "banner-wrap";
622
667
  const banner = document.createElement("bv-alert-banner");
623
- banner.setAttribute("variant", "warning");
624
668
  banner.hidden = true;
625
- this._bannerEl = banner;
669
+ this._alertBannerEl = banner;
626
670
  bannerWrap.appendChild(banner);
627
671
  main.append(calsWrap, bannerWrap, footer);
628
672
  picker.append(sidebar, main);
@@ -795,6 +839,7 @@ var BvDateRangePicker = class extends HTMLElement {
795
839
  this._preset = "custom";
796
840
  this._picking = true;
797
841
  this._hover = "";
842
+ this._lastEditedField = "start";
798
843
  } else {
799
844
  if (iso < this._start) {
800
845
  this._end = this._start;
@@ -805,6 +850,7 @@ var BvDateRangePicker = class extends HTMLElement {
805
850
  this._picking = false;
806
851
  this._hover = "";
807
852
  this._preset = "custom";
853
+ this._lastEditedField = "end";
808
854
  }
809
855
  this._startRawInvalid = "";
810
856
  this._endRawInvalid = "";
@@ -829,6 +875,7 @@ var BvDateRangePicker = class extends HTMLElement {
829
875
  this._hover = "";
830
876
  this._startRawInvalid = "";
831
877
  this._endRawInvalid = "";
878
+ this._lastEditedField = null;
832
879
  if (start) {
833
880
  const parts = start.split("-");
834
881
  const y = parseInt(parts[0], 10);
@@ -844,6 +891,7 @@ var BvDateRangePicker = class extends HTMLElement {
844
891
  this._syncValidity();
845
892
  }
846
893
  _onInputBlur(input, field) {
894
+ this._lastEditedField = field;
847
895
  const raw = input.value;
848
896
  if (raw === "") {
849
897
  if (field === "start") {
@@ -907,9 +955,9 @@ var BvDateRangePicker = class extends HTMLElement {
907
955
  },
908
956
  cfg
909
957
  );
910
- this._applyErrorStates();
958
+ this._paintInputInvalidState();
959
+ this._applyAlertBanner(cfg);
911
960
  this._applyApplyState();
912
- this._applyBannerState();
913
961
  this._applyCalendarDisabledState();
914
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) {
915
963
  this.dispatchEvent(
@@ -931,88 +979,140 @@ var BvDateRangePicker = class extends HTMLElement {
931
979
  max,
932
980
  maxRangeYears: Number.isFinite(maxRangeYears) ? maxRangeYears : void 0,
933
981
  validateOrder: this.hasAttribute("validate-order"),
934
- validateFormat: this.hasAttribute("validate-format")
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"))
935
988
  };
936
989
  }
937
- _applyErrorStates() {
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() {
938
997
  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
- }
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");
949
1010
  }
950
1011
  }
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
- }
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;
1028
+ const pickerEl = this.shadowRoot.querySelector(".picker");
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;
960
1036
  }
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
- }
1037
+ if (active.kind === "form" && active.error === "range-limit" && this.getAttribute("show-range-limit-banner") === "false") {
1038
+ this._alertBannerEl.hidden = true;
1039
+ pickerEl == null ? void 0 : pickerEl.classList.remove("picker--with-banner");
1040
+ return;
969
1041
  }
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;
970
1076
  }
971
1077
  _resolveFieldError(err, field) {
972
1078
  if (err === "format") {
973
- return this.getAttribute("error-format") ?? "Enter a valid date in MM/DD/YYYY format";
1079
+ const custom2 = this.getAttribute("error-format");
1080
+ if (custom2) return this._interpolateField(custom2, field);
1081
+ return `Enter a valid ${field} date`;
974
1082
  }
975
1083
  if (err === "min") {
976
1084
  const minIso = this.getAttribute("min") ?? "";
977
1085
  const minDisplay = formatDisplay(minIso);
978
1086
  const custom2 = this.getAttribute("error-min");
979
- if (custom2) return custom2.replace("{min}", minDisplay).replace("{field}", field);
1087
+ if (custom2) return this._interpolateField(custom2, field).replace(/\{min\}/g, minDisplay);
980
1088
  return `${capitalize(field)} date must be on or after ${minDisplay}`;
981
1089
  }
982
1090
  const maxAttr = this.getAttribute("max") ?? "";
983
1091
  const resolvedMax = maxAttr === "today" ? isoToday() : maxAttr;
984
1092
  const maxDisplay = formatDisplay(resolvedMax);
985
1093
  const custom = this.getAttribute("error-max");
986
- if (custom) return custom.replace("{max}", maxDisplay).replace("{field}", field);
1094
+ if (custom) return this._interpolateField(custom, field).replace(/\{max\}/g, maxDisplay);
987
1095
  if (maxAttr === "today") return `${capitalize(field)} date cannot be in the future`;
988
1096
  return `${capitalize(field)} date cannot be after ${maxDisplay}`;
989
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
+ }
990
1108
  _resolveOrderError() {
991
- return this.getAttribute("error-order") ?? "Start date must be on or before end date";
1109
+ return this.getAttribute("error-order") || "Start date must be on or before end date";
992
1110
  }
993
1111
  _applyApplyState() {
994
1112
  if (this._applyBtn) {
995
1113
  this._applyBtn.disabled = !this._validity.applyEnabled;
996
1114
  }
997
1115
  }
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
1116
  _applyCalendarDisabledState() {
1017
1117
  const minIso = this.getAttribute("min") || "";
1018
1118
  const maxAttr = this.getAttribute("max") || "";
@@ -1048,6 +1148,7 @@ var BvDateRangePicker = class extends HTMLElement {
1048
1148
  var _a;
1049
1149
  const ev = e;
1050
1150
  if ((_a = ev.inputType) == null ? void 0 : _a.startsWith("delete")) return;
1151
+ this._lastEditedField = field;
1051
1152
  const digits = input.value.replace(/[^\d]/g, "");
1052
1153
  let formatted;
1053
1154
  if (digits.length <= 2) {
@@ -1106,7 +1207,13 @@ var PICKER_PASSTHROUGH_ATTRS = [
1106
1207
  "error-order",
1107
1208
  "error-format",
1108
1209
  "range-limit-message",
1109
- "range-limit-heading"
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"
1110
1217
  ];
1111
1218
  function isoToDisplay(iso) {
1112
1219
  if (!iso) return "";
@@ -1120,6 +1227,8 @@ var BvDateRangeInput = class extends HTMLElement {
1120
1227
  "end",
1121
1228
  "placeholder",
1122
1229
  "required",
1230
+ // BTN-10312 — local (not passed through to picker); gates popup close.
1231
+ "keep-open-while-invalid",
1123
1232
  ...PICKER_PASSTHROUGH_ATTRS
1124
1233
  ];
1125
1234
  _rendered = false;
@@ -1128,6 +1237,12 @@ var BvDateRangeInput = class extends HTMLElement {
1128
1237
  _triggerEl = null;
1129
1238
  _pickerEl = null;
1130
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 };
1131
1246
  constructor() {
1132
1247
  super();
1133
1248
  adoptStyles(this.attachShadow({ mode: "open" }), bv_date_range_input_default);
@@ -1223,13 +1338,15 @@ var BvDateRangeInput = class extends HTMLElement {
1223
1338
  });
1224
1339
  picker.addEventListener("bv-validity-change", (e) => {
1225
1340
  const detail = e.detail;
1341
+ this._pickerValidity = detail;
1342
+ const hasBlockingError = !detail.applyEnabled && !!(detail.startError || detail.endError || detail.formError);
1226
1343
  if (this._triggerEl) {
1227
- if (detail.valid) {
1228
- this._triggerEl.classList.remove("invalid");
1229
- this._triggerEl.removeAttribute("aria-invalid");
1230
- } else {
1344
+ if (hasBlockingError) {
1231
1345
  this._triggerEl.classList.add("invalid");
1232
1346
  this._triggerEl.setAttribute("aria-invalid", "true");
1347
+ } else {
1348
+ this._triggerEl.classList.remove("invalid");
1349
+ this._triggerEl.removeAttribute("aria-invalid");
1233
1350
  }
1234
1351
  }
1235
1352
  this.dispatchEvent(
@@ -1251,6 +1368,28 @@ var BvDateRangeInput = class extends HTMLElement {
1251
1368
  this._pickerEl.setAttribute("start", this.getAttribute("start") ?? "");
1252
1369
  this._pickerEl.setAttribute("end", this.getAttribute("end") ?? "");
1253
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();
1254
1393
  }
1255
1394
  });
1256
1395
  }