@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.
@@ -6,7 +6,7 @@ export interface DateRangeChangeDetail {
6
6
  preset: string;
7
7
  }
8
8
  export declare class BvDateRangePicker extends HTMLElement {
9
- static readonly observedAttributes: readonly ["start", "end", "preset", "min", "max", "max-range-years", "validate-order", "validate-format", "show-range-limit-banner", "error-min", "error-max", "error-order", "error-format", "range-limit-message", "range-limit-heading"];
9
+ static readonly observedAttributes: readonly ["start", "end", "preset", "min", "max", "max-range-years", "validate-order", "validate-format", "show-range-limit-banner", "error-min", "error-max", "error-order", "error-format", "range-limit-message", "range-limit-heading", "error-min-severity", "error-max-severity", "error-format-severity", "error-order-severity", "range-limit-severity"];
10
10
  private _rendered;
11
11
  private _today;
12
12
  private _start;
@@ -26,12 +26,22 @@ export declare class BvDateRangePicker extends HTMLElement {
26
26
  private _startInput;
27
27
  private _endInput;
28
28
  private _applyBtn;
29
- private _bannerEl;
30
- private _startErrorEl;
31
- private _endErrorEl;
32
- private _formErrorEl;
29
+ /**
30
+ * Unified alert banner (BTN-10312). Shows AT MOST ONE error at a time. When
31
+ * multiple errors are simultaneously active, the banner reflects the
32
+ * most-recently-edited field (see `_lastEditedField`).
33
+ */
34
+ private _alertBannerEl;
33
35
  private _leftDayMap;
34
36
  private _rightDayMap;
37
+ /**
38
+ * BTN-10312 — tracks which field the user most-recently interacted with.
39
+ * When both `startError` and `endError` are active, the banner shows the
40
+ * error corresponding to this field. Set to `null` when no field has been
41
+ * touched (initial render, or external attribute set); the resolver falls
42
+ * back to a stable precedence in that case.
43
+ */
44
+ private _lastEditedField;
35
45
  constructor();
36
46
  get start(): string;
37
47
  set start(v: string);
@@ -41,6 +51,16 @@ export declare class BvDateRangePicker extends HTMLElement {
41
51
  set preset(v: string);
42
52
  connectedCallback(): void;
43
53
  attributeChangedCallback(name: string, _old: string | null, val: string | null): void;
54
+ /**
55
+ * BTN-10312 — public reset of the picker's internal draft state to the
56
+ * currently committed `start` / `end` / `preset` attributes. Called by
57
+ * `bv-date-range-input` when the popup closes without Apply (Cancel,
58
+ * Escape, outside-click) so the next open shows the last-committed value
59
+ * cleanly, with no leftover error banners from the discarded edit.
60
+ *
61
+ * The external value is not modified — this only clears the draft.
62
+ */
63
+ resetDraft(): void;
44
64
  private _render;
45
65
  private _buildCalendarShell;
46
66
  private _navigate;
@@ -57,12 +77,45 @@ export declare class BvDateRangePicker extends HTMLElement {
57
77
  private _syncPresets;
58
78
  private _syncValidity;
59
79
  private _readConfig;
60
- private _applyErrorStates;
61
- private _paintInputError;
80
+ /**
81
+ * BTN-10312 — apply the `.invalid` + `aria-invalid` state to input fields
82
+ * whose field-level error is currently blocking (severity `'error'`).
83
+ * Non-blocking severities (warning/primary/success) do not paint the input
84
+ * red so the visual matches the semantic ("not a fatal error").
85
+ */
86
+ private _paintInputInvalidState;
87
+ private _paintOne;
88
+ /**
89
+ * BTN-10312 — single alert banner. Picks the "active" error following the
90
+ * precedence:
91
+ *
92
+ * 1. If a form error is active, show it (form errors don't coexist with
93
+ * field errors by validator design).
94
+ * 2. If both field errors are active, show the one for
95
+ * `_lastEditedField` (falling back to `start` if `null`).
96
+ * 3. Else show whichever single field error is active.
97
+ * 4. Else hide the banner.
98
+ *
99
+ * `show-range-limit-banner="false"` still suppresses the range-limit
100
+ * banner specifically (existing consumer contract from BTN-10191).
101
+ */
102
+ private _applyAlertBanner;
103
+ /**
104
+ * Pick which error the banner should reflect. See `_applyAlertBanner` for
105
+ * the precedence rules.
106
+ */
107
+ private _resolveActiveError;
62
108
  private _resolveFieldError;
109
+ /**
110
+ * BTN-10312 — interpolate `{field}` (always lowercase) and `{Field}` (always
111
+ * capitalized) in a user-provided template. Before this change, the historical
112
+ * behavior was that `{field}` in custom copy resolved lowercase while the
113
+ * built-in defaults capitalized inline — inconsistent. Now the split is
114
+ * explicit: `{field}` = lowercase, `{Field}` = capitalized.
115
+ */
116
+ private _interpolateField;
63
117
  private _resolveOrderError;
64
118
  private _applyApplyState;
65
- private _applyBannerState;
66
119
  private _applyCalendarDisabledState;
67
120
  private _resetDayDisabled;
68
121
  private _setupDateMask;
@@ -1,5 +1,5 @@
1
1
  // css-inline:/home/runner/work/BV_lumen/BV_lumen/components/bv-date-range-picker/bv-date-range-picker.css
2
- 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";
2
+ 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";
3
3
 
4
4
  // components/_shared/adopt-styles.ts
5
5
  var _sheetCache = /* @__PURE__ */ new Map();
@@ -20,6 +20,13 @@ function adoptStyles(shadow, css) {
20
20
  }
21
21
 
22
22
  // components/_shared/date-range-validator.ts
23
+ var ALLOWED_SEVERITIES = ["error", "warning", "primary", "success"];
24
+ function normalizeSeverity(value) {
25
+ if (value && ALLOWED_SEVERITIES.includes(value)) {
26
+ return value;
27
+ }
28
+ return "error";
29
+ }
23
30
  function validateDateRange(input, config = {}) {
24
31
  const { start, end } = input;
25
32
  const { min, max, maxRangeYears, validateOrder, validateFormat } = config;
@@ -50,7 +57,11 @@ function validateDateRange(input, config = {}) {
50
57
  }
51
58
  }
52
59
  const valid = !startError && !endError && !formError;
53
- const applyEnabled = valid && !!startDate && !!endDate;
60
+ const startSev = startError ? severityOfFieldError(startError, config) : null;
61
+ const endSev = endError ? severityOfFieldError(endError, config) : null;
62
+ const formSev = formError ? severityOfFormError(formError, config) : null;
63
+ const hasBlockingError = startSev === "error" || endSev === "error" || formSev === "error";
64
+ const applyEnabled = !hasBlockingError && !!startDate && !!endDate;
54
65
  return {
55
66
  valid,
56
67
  applyEnabled,
@@ -59,6 +70,15 @@ function validateDateRange(input, config = {}) {
59
70
  ...formError && { formError }
60
71
  };
61
72
  }
73
+ function severityOfFieldError(err, config) {
74
+ if (err === "format") return normalizeSeverity(config.formatSeverity);
75
+ if (err === "min") return normalizeSeverity(config.minSeverity);
76
+ return normalizeSeverity(config.maxSeverity);
77
+ }
78
+ function severityOfFormError(err, config) {
79
+ if (err === "order") return normalizeSeverity(config.orderSeverity);
80
+ return normalizeSeverity(config.rangeLimitSeverity);
81
+ }
62
82
  function parseIso(s) {
63
83
  const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(s);
64
84
  if (!m) return null;
@@ -385,7 +405,13 @@ var BvDateRangePicker = class extends HTMLElement {
385
405
  "error-order",
386
406
  "error-format",
387
407
  "range-limit-message",
388
- "range-limit-heading"
408
+ "range-limit-heading",
409
+ // BTN-10312 — per-error severity (default error = blocks Apply + red banner)
410
+ "error-min-severity",
411
+ "error-max-severity",
412
+ "error-format-severity",
413
+ "error-order-severity",
414
+ "range-limit-severity"
389
415
  ];
390
416
  _rendered = false;
391
417
  _today;
@@ -409,12 +435,22 @@ var BvDateRangePicker = class extends HTMLElement {
409
435
  _startInput = null;
410
436
  _endInput = null;
411
437
  _applyBtn = null;
412
- _bannerEl = null;
413
- _startErrorEl = null;
414
- _endErrorEl = null;
415
- _formErrorEl = null;
438
+ /**
439
+ * Unified alert banner (BTN-10312). Shows AT MOST ONE error at a time. When
440
+ * multiple errors are simultaneously active, the banner reflects the
441
+ * most-recently-edited field (see `_lastEditedField`).
442
+ */
443
+ _alertBannerEl = null;
416
444
  _leftDayMap = /* @__PURE__ */ new Map();
417
445
  _rightDayMap = /* @__PURE__ */ new Map();
446
+ /**
447
+ * BTN-10312 — tracks which field the user most-recently interacted with.
448
+ * When both `startError` and `endError` are active, the banner shows the
449
+ * error corresponding to this field. Set to `null` when no field has been
450
+ * touched (initial render, or external attribute set); the resolver falls
451
+ * back to a stable precedence in that case.
452
+ */
453
+ _lastEditedField = null;
418
454
  constructor() {
419
455
  super();
420
456
  const now = /* @__PURE__ */ new Date();
@@ -449,10 +485,12 @@ var BvDateRangePicker = class extends HTMLElement {
449
485
  if (name === "start") {
450
486
  this._start = val ?? "";
451
487
  this._startRawInvalid = "";
488
+ this._lastEditedField = null;
452
489
  }
453
490
  if (name === "end") {
454
491
  this._end = val ?? "";
455
492
  this._endRawInvalid = "";
493
+ this._lastEditedField = null;
456
494
  }
457
495
  if (name === "preset") this._preset = val ?? "";
458
496
  if (!this._rendered) return;
@@ -463,6 +501,30 @@ var BvDateRangePicker = class extends HTMLElement {
463
501
  }
464
502
  this._syncValidity();
465
503
  }
504
+ /**
505
+ * BTN-10312 — public reset of the picker's internal draft state to the
506
+ * currently committed `start` / `end` / `preset` attributes. Called by
507
+ * `bv-date-range-input` when the popup closes without Apply (Cancel,
508
+ * Escape, outside-click) so the next open shows the last-committed value
509
+ * cleanly, with no leftover error banners from the discarded edit.
510
+ *
511
+ * The external value is not modified — this only clears the draft.
512
+ */
513
+ resetDraft() {
514
+ this._start = this.getAttribute("start") ?? "";
515
+ this._end = this.getAttribute("end") ?? "";
516
+ this._preset = this.getAttribute("preset") ?? "";
517
+ this._startRawInvalid = "";
518
+ this._endRawInvalid = "";
519
+ this._picking = false;
520
+ this._hover = "";
521
+ this._lastEditedField = null;
522
+ if (!this._rendered) return;
523
+ this._syncCalendars();
524
+ this._syncInputs();
525
+ this._syncPresets();
526
+ this._syncValidity();
527
+ }
466
528
  _render() {
467
529
  if (this._rendered) return;
468
530
  this._rendered = true;
@@ -546,31 +608,11 @@ var BvDateRangePicker = class extends HTMLElement {
546
608
  this._applyBtn = applyBtn;
547
609
  actions.append(cancelBtn, applyBtn);
548
610
  footer.append(inputsWrap, actions);
549
- const errorsWrap = document.createElement("div");
550
- errorsWrap.className = "error-messages";
551
- const startErrorEl = document.createElement("div");
552
- startErrorEl.className = "input-error input-error--start";
553
- startErrorEl.setAttribute("role", "alert");
554
- startErrorEl.hidden = true;
555
- this._startErrorEl = startErrorEl;
556
- const endErrorEl = document.createElement("div");
557
- endErrorEl.className = "input-error input-error--end";
558
- endErrorEl.setAttribute("role", "alert");
559
- endErrorEl.hidden = true;
560
- this._endErrorEl = endErrorEl;
561
- const formErrorEl = document.createElement("div");
562
- formErrorEl.className = "form-error";
563
- formErrorEl.setAttribute("role", "alert");
564
- formErrorEl.hidden = true;
565
- this._formErrorEl = formErrorEl;
566
- errorsWrap.append(startErrorEl, endErrorEl, formErrorEl);
567
- footer.insertBefore(errorsWrap, actions);
568
611
  const bannerWrap = document.createElement("div");
569
612
  bannerWrap.className = "banner-wrap";
570
613
  const banner = document.createElement("bv-alert-banner");
571
- banner.setAttribute("variant", "warning");
572
614
  banner.hidden = true;
573
- this._bannerEl = banner;
615
+ this._alertBannerEl = banner;
574
616
  bannerWrap.appendChild(banner);
575
617
  main.append(calsWrap, bannerWrap, footer);
576
618
  picker.append(sidebar, main);
@@ -743,6 +785,7 @@ var BvDateRangePicker = class extends HTMLElement {
743
785
  this._preset = "custom";
744
786
  this._picking = true;
745
787
  this._hover = "";
788
+ this._lastEditedField = "start";
746
789
  } else {
747
790
  if (iso < this._start) {
748
791
  this._end = this._start;
@@ -753,6 +796,7 @@ var BvDateRangePicker = class extends HTMLElement {
753
796
  this._picking = false;
754
797
  this._hover = "";
755
798
  this._preset = "custom";
799
+ this._lastEditedField = "end";
756
800
  }
757
801
  this._startRawInvalid = "";
758
802
  this._endRawInvalid = "";
@@ -777,6 +821,7 @@ var BvDateRangePicker = class extends HTMLElement {
777
821
  this._hover = "";
778
822
  this._startRawInvalid = "";
779
823
  this._endRawInvalid = "";
824
+ this._lastEditedField = null;
780
825
  if (start) {
781
826
  const parts = start.split("-");
782
827
  const y = parseInt(parts[0], 10);
@@ -792,6 +837,7 @@ var BvDateRangePicker = class extends HTMLElement {
792
837
  this._syncValidity();
793
838
  }
794
839
  _onInputBlur(input, field) {
840
+ this._lastEditedField = field;
795
841
  const raw = input.value;
796
842
  if (raw === "") {
797
843
  if (field === "start") {
@@ -855,9 +901,9 @@ var BvDateRangePicker = class extends HTMLElement {
855
901
  },
856
902
  cfg
857
903
  );
858
- this._applyErrorStates();
904
+ this._paintInputInvalidState();
905
+ this._applyAlertBanner(cfg);
859
906
  this._applyApplyState();
860
- this._applyBannerState();
861
907
  this._applyCalendarDisabledState();
862
908
  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) {
863
909
  this.dispatchEvent(
@@ -879,88 +925,140 @@ var BvDateRangePicker = class extends HTMLElement {
879
925
  max,
880
926
  maxRangeYears: Number.isFinite(maxRangeYears) ? maxRangeYears : void 0,
881
927
  validateOrder: this.hasAttribute("validate-order"),
882
- validateFormat: this.hasAttribute("validate-format")
928
+ validateFormat: this.hasAttribute("validate-format"),
929
+ minSeverity: normalizeSeverity(this.getAttribute("error-min-severity")),
930
+ maxSeverity: normalizeSeverity(this.getAttribute("error-max-severity")),
931
+ formatSeverity: normalizeSeverity(this.getAttribute("error-format-severity")),
932
+ orderSeverity: normalizeSeverity(this.getAttribute("error-order-severity")),
933
+ rangeLimitSeverity: normalizeSeverity(this.getAttribute("range-limit-severity"))
883
934
  };
884
935
  }
885
- _applyErrorStates() {
936
+ /**
937
+ * BTN-10312 — apply the `.invalid` + `aria-invalid` state to input fields
938
+ * whose field-level error is currently blocking (severity `'error'`).
939
+ * Non-blocking severities (warning/primary/success) do not paint the input
940
+ * red so the visual matches the semantic ("not a fatal error").
941
+ */
942
+ _paintInputInvalidState() {
886
943
  const v = this._validity;
887
- this._paintInputError(this._startInput, this._startErrorEl, v.startError, "start");
888
- this._paintInputError(this._endInput, this._endErrorEl, v.endError, "end");
889
- if (this._formErrorEl) {
890
- if (v.formError === "order") {
891
- this._formErrorEl.textContent = this._resolveOrderError();
892
- this._formErrorEl.hidden = false;
893
- } else {
894
- this._formErrorEl.textContent = "";
895
- this._formErrorEl.hidden = true;
896
- }
944
+ const cfg = this._readConfig();
945
+ this._paintOne(this._startInput, v.startError ? severityOfFieldError(v.startError, cfg) : null);
946
+ this._paintOne(this._endInput, v.endError ? severityOfFieldError(v.endError, cfg) : null);
947
+ }
948
+ _paintOne(input, sev) {
949
+ if (!input) return;
950
+ if (sev === "error") {
951
+ input.classList.add("invalid");
952
+ input.setAttribute("aria-invalid", "true");
953
+ } else {
954
+ input.classList.remove("invalid");
955
+ input.removeAttribute("aria-invalid");
897
956
  }
898
957
  }
899
- _paintInputError(input, errorEl, err, field) {
900
- if (input) {
901
- if (err) {
902
- input.classList.add("invalid");
903
- input.setAttribute("aria-invalid", "true");
904
- } else {
905
- input.classList.remove("invalid");
906
- input.removeAttribute("aria-invalid");
907
- }
958
+ /**
959
+ * BTN-10312 — single alert banner. Picks the "active" error following the
960
+ * precedence:
961
+ *
962
+ * 1. If a form error is active, show it (form errors don't coexist with
963
+ * field errors by validator design).
964
+ * 2. If both field errors are active, show the one for
965
+ * `_lastEditedField` (falling back to `start` if `null`).
966
+ * 3. Else show whichever single field error is active.
967
+ * 4. Else hide the banner.
968
+ *
969
+ * `show-range-limit-banner="false"` still suppresses the range-limit
970
+ * banner specifically (existing consumer contract from BTN-10191).
971
+ */
972
+ _applyAlertBanner(cfg) {
973
+ if (!this._alertBannerEl) return;
974
+ const pickerEl = this.shadowRoot.querySelector(".picker");
975
+ const active = this._resolveActiveError();
976
+ if (!active) {
977
+ this._alertBannerEl.hidden = true;
978
+ this._alertBannerEl.removeAttribute("heading");
979
+ this._alertBannerEl.removeAttribute("message");
980
+ pickerEl == null ? void 0 : pickerEl.classList.remove("picker--with-banner");
981
+ return;
908
982
  }
909
- if (errorEl) {
910
- if (err) {
911
- errorEl.textContent = this._resolveFieldError(err, field);
912
- errorEl.hidden = false;
913
- } else {
914
- errorEl.textContent = "";
915
- errorEl.hidden = true;
916
- }
983
+ if (active.kind === "form" && active.error === "range-limit" && this.getAttribute("show-range-limit-banner") === "false") {
984
+ this._alertBannerEl.hidden = true;
985
+ pickerEl == null ? void 0 : pickerEl.classList.remove("picker--with-banner");
986
+ return;
987
+ }
988
+ const variant = active.kind === "field" ? severityOfFieldError(active.error, cfg) : severityOfFormError(active.error, cfg);
989
+ this._alertBannerEl.setAttribute("variant", variant);
990
+ if (active.kind === "form" && active.error === "range-limit") {
991
+ const yearsAttr = this.getAttribute("max-range-years") ?? "";
992
+ const heading = this.getAttribute("range-limit-heading") || (yearsAttr ? `Date range exceeds ${yearsAttr} years` : "Date range exceeds the maximum");
993
+ const message = this.getAttribute("range-limit-message") || "Only CSV download is available for this date range. Live report generation is not supported.";
994
+ this._alertBannerEl.setAttribute("heading", heading.replace(/\{N\}/g, yearsAttr));
995
+ this._alertBannerEl.setAttribute("message", message.replace(/\{N\}/g, yearsAttr));
996
+ } else {
997
+ this._alertBannerEl.removeAttribute("heading");
998
+ const message = active.kind === "field" ? this._resolveFieldError(active.error, active.field) : this._resolveOrderError();
999
+ this._alertBannerEl.setAttribute("message", message);
1000
+ }
1001
+ this._alertBannerEl.hidden = false;
1002
+ pickerEl == null ? void 0 : pickerEl.classList.add("picker--with-banner");
1003
+ }
1004
+ /**
1005
+ * Pick which error the banner should reflect. See `_applyAlertBanner` for
1006
+ * the precedence rules.
1007
+ */
1008
+ _resolveActiveError() {
1009
+ const v = this._validity;
1010
+ if (v.formError) return { kind: "form", error: v.formError };
1011
+ if (v.startError && v.endError) {
1012
+ const winner = this._lastEditedField ?? "start";
1013
+ return {
1014
+ kind: "field",
1015
+ error: winner === "start" ? v.startError : v.endError,
1016
+ field: winner
1017
+ };
917
1018
  }
1019
+ if (v.startError) return { kind: "field", error: v.startError, field: "start" };
1020
+ if (v.endError) return { kind: "field", error: v.endError, field: "end" };
1021
+ return null;
918
1022
  }
919
1023
  _resolveFieldError(err, field) {
920
1024
  if (err === "format") {
921
- return this.getAttribute("error-format") ?? "Enter a valid date in MM/DD/YYYY format";
1025
+ const custom2 = this.getAttribute("error-format");
1026
+ if (custom2) return this._interpolateField(custom2, field);
1027
+ return `Enter a valid ${field} date`;
922
1028
  }
923
1029
  if (err === "min") {
924
1030
  const minIso = this.getAttribute("min") ?? "";
925
1031
  const minDisplay = formatDisplay(minIso);
926
1032
  const custom2 = this.getAttribute("error-min");
927
- if (custom2) return custom2.replace("{min}", minDisplay).replace("{field}", field);
1033
+ if (custom2) return this._interpolateField(custom2, field).replace(/\{min\}/g, minDisplay);
928
1034
  return `${capitalize(field)} date must be on or after ${minDisplay}`;
929
1035
  }
930
1036
  const maxAttr = this.getAttribute("max") ?? "";
931
1037
  const resolvedMax = maxAttr === "today" ? isoToday() : maxAttr;
932
1038
  const maxDisplay = formatDisplay(resolvedMax);
933
1039
  const custom = this.getAttribute("error-max");
934
- if (custom) return custom.replace("{max}", maxDisplay).replace("{field}", field);
1040
+ if (custom) return this._interpolateField(custom, field).replace(/\{max\}/g, maxDisplay);
935
1041
  if (maxAttr === "today") return `${capitalize(field)} date cannot be in the future`;
936
1042
  return `${capitalize(field)} date cannot be after ${maxDisplay}`;
937
1043
  }
1044
+ /**
1045
+ * BTN-10312 — interpolate `{field}` (always lowercase) and `{Field}` (always
1046
+ * capitalized) in a user-provided template. Before this change, the historical
1047
+ * behavior was that `{field}` in custom copy resolved lowercase while the
1048
+ * built-in defaults capitalized inline — inconsistent. Now the split is
1049
+ * explicit: `{field}` = lowercase, `{Field}` = capitalized.
1050
+ */
1051
+ _interpolateField(template, field) {
1052
+ return template.replace(/\{Field\}/g, capitalize(field)).replace(/\{field\}/g, field);
1053
+ }
938
1054
  _resolveOrderError() {
939
- return this.getAttribute("error-order") ?? "Start date must be on or before end date";
1055
+ return this.getAttribute("error-order") || "Start date must be on or before end date";
940
1056
  }
941
1057
  _applyApplyState() {
942
1058
  if (this._applyBtn) {
943
1059
  this._applyBtn.disabled = !this._validity.applyEnabled;
944
1060
  }
945
1061
  }
946
- _applyBannerState() {
947
- if (!this._bannerEl) return;
948
- const pickerEl = this.shadowRoot.querySelector(".picker");
949
- const bannerAllowed = this.getAttribute("show-range-limit-banner") !== "false";
950
- const show = this._validity.formError === "range-limit" && bannerAllowed;
951
- if (show) {
952
- const yearsAttr = this.getAttribute("max-range-years") ?? "";
953
- const heading = this.getAttribute("range-limit-heading") ?? (yearsAttr ? `Date range exceeds ${yearsAttr} years` : "Date range exceeds the maximum");
954
- const message = this.getAttribute("range-limit-message") ?? "Only CSV download is available for this date range. Live report generation is not supported.";
955
- this._bannerEl.setAttribute("heading", heading.replace("{N}", yearsAttr));
956
- this._bannerEl.setAttribute("message", message.replace("{N}", yearsAttr));
957
- this._bannerEl.hidden = false;
958
- pickerEl == null ? void 0 : pickerEl.classList.add("picker--with-banner");
959
- } else {
960
- this._bannerEl.hidden = true;
961
- pickerEl == null ? void 0 : pickerEl.classList.remove("picker--with-banner");
962
- }
963
- }
964
1062
  _applyCalendarDisabledState() {
965
1063
  const minIso = this.getAttribute("min") || "";
966
1064
  const maxAttr = this.getAttribute("max") || "";
@@ -996,6 +1094,7 @@ var BvDateRangePicker = class extends HTMLElement {
996
1094
  var _a;
997
1095
  const ev = e;
998
1096
  if ((_a = ev.inputType) == null ? void 0 : _a.startsWith("delete")) return;
1097
+ this._lastEditedField = field;
999
1098
  const digits = input.value.replace(/[^\d]/g, "");
1000
1099
  let formatted;
1001
1100
  if (digits.length <= 2) {
@@ -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);
@@ -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);