@vialiq/web-components 0.1.2 → 0.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.
@@ -0,0 +1,121 @@
1
+ import { LitElement } from 'lit';
2
+ type Constructor<T = object> = new (...args: any[]) => T;
3
+ /**
4
+ * Type-only declaration of the shape FocusTrapMixin adds to a class.
5
+ * `declare class` emits no runtime code — it is a TS-only contract.
6
+ */
7
+ export declare class FocusTrapInterface {
8
+ /**
9
+ * Activates the focus trap.
10
+ * Queries all focusable elements in the shadow root (and slotted content),
11
+ * focuses the first one (or a provided initial target), and begins
12
+ * intercepting Tab / Shift+Tab to cycle focus within the component.
13
+ *
14
+ * @param initialFocus - Optional element to focus first. Defaults to the
15
+ * first focusable element found in the shadow root.
16
+ */
17
+ protected _activateFocusTrap(initialFocus?: HTMLElement): void;
18
+ /**
19
+ * Deactivates the focus trap.
20
+ * Removes the Tab intercept and restores focus to the element that was
21
+ * focused immediately before `_activateFocusTrap()` was called.
22
+ */
23
+ protected _deactivateFocusTrap(): void;
24
+ }
25
+ /**
26
+ * FocusTrapMixin
27
+ *
28
+ * Traps keyboard focus within a component's shadow root.
29
+ * Intended for overlay components: vi-modal, vi-dialog, vi-drawer.
30
+ *
31
+ * ─────────────────────────────────────────────────────────────────────────
32
+ * HOW IT WORKS
33
+ * ─────────────────────────────────────────────────────────────────────────
34
+ *
35
+ * activate → snapshot pre-trap focus → focus initial element → listen Tab
36
+ * Tab → if on last focusable → wrap to first
37
+ * Shift+Tab → if on first focusable → wrap to last
38
+ * deactivate→ remove listener → restore pre-trap focus
39
+ *
40
+ * ─────────────────────────────────────────────────────────────────────────
41
+ * CRITICAL: shadowRoot.activeElement vs document.activeElement
42
+ * ─────────────────────────────────────────────────────────────────────────
43
+ *
44
+ * Inside a shadow root, `document.activeElement` returns the HOST element
45
+ * (e.g. the `<vi-modal>` tag), NOT the inner focused element.
46
+ * To correctly identify which inner element is focused, ALWAYS use:
47
+ *
48
+ * this.shadowRoot!.activeElement
49
+ *
50
+ * This is the deepest focused element within the shadow boundary.
51
+ * For slotted content (light DOM children), `shadowRoot.activeElement`
52
+ * returns the `<slot>` element, not the focused child — in that case,
53
+ * use `document.activeElement` to get the actual slotted focused element.
54
+ *
55
+ * ─────────────────────────────────────────────────────────────────────────
56
+ * FOCUSABLE ELEMENT COLLECTION
57
+ * ─────────────────────────────────────────────────────────────────────────
58
+ *
59
+ * Two sources are queried and merged:
60
+ *
61
+ * 1. Shadow DOM: shadowRoot.querySelectorAll(FOCUSABLE_SELECTOR)
62
+ * Covers native elements (<button>, <input>, etc.) and vi-* host elements
63
+ * that use FocusableMixin (vi-button, vi-input, etc.).
64
+ *
65
+ * 2. Slotted content: each <slot>.assignedElements({ flatten: true })
66
+ * filtered by FOCUSABLE_SELECTOR.
67
+ * Covers focusable light-DOM children slotted into the component.
68
+ * Example: a <vi-button> slotted into <vi-modal>'s footer slot.
69
+ *
70
+ * Both lists are combined and deduplicated into a single ordered array.
71
+ * DOM order is preserved (querySelector returns elements in tree order).
72
+ *
73
+ * ─────────────────────────────────────────────────────────────────────────
74
+ * USAGE
75
+ * ─────────────────────────────────────────────────────────────────────────
76
+ *
77
+ * class ViModal extends FocusTrapMixin(ViElement) {
78
+ * @property({ type: Boolean, reflect: true }) accessor open = false;
79
+ *
80
+ * override updated(changed: PropertyValues): void {
81
+ * super.updated(changed);
82
+ * if (changed.has('open')) {
83
+ * if (this.open) {
84
+ * // Optional: pass a specific element to focus first.
85
+ * // If omitted, the first focusable in the shadow root is used.
86
+ * this._activateFocusTrap();
87
+ * } else {
88
+ * this._deactivateFocusTrap();
89
+ * }
90
+ * }
91
+ * }
92
+ * }
93
+ *
94
+ * ─────────────────────────────────────────────────────────────────────────
95
+ * ACCESSIBILITY CONTRACT
96
+ * ─────────────────────────────────────────────────────────────────────────
97
+ *
98
+ * - The trapping component MUST have role="dialog" (or "alertdialog") and
99
+ * aria-modal="true" so screen readers also confine their virtual cursor.
100
+ * - Escape key handling is NOT part of this mixin — the component owns that
101
+ * (it is component-specific: vi-modal may close on Escape, vi-drawer may not).
102
+ * - The trap does NOT prevent focus from moving to the browser chrome (URL bar,
103
+ * tab bar) — that is intentional and required by WCAG 2.1 SC 2.1.2.
104
+ *
105
+ * ─────────────────────────────────────────────────────────────────────────
106
+ * COMPOSITION ORDER
107
+ * ─────────────────────────────────────────────────────────────────────────
108
+ *
109
+ * FocusTrapMixin does NOT require FocusableMixin. Most overlay components
110
+ * (vi-modal, vi-drawer) are NOT themselves focusable tab stops — they are
111
+ * focus containers. Use independently:
112
+ *
113
+ * class ViModal extends FocusTrapMixin(ViElement) { ... }
114
+ *
115
+ * If you need both (unusual — a container that is also a tab stop):
116
+ *
117
+ * class ViPanel extends FocusTrapMixin(FocusableMixin(ViElement)) { ... }
118
+ */
119
+ export declare function FocusTrapMixin<T extends Constructor<LitElement>>(Base: T): T & Constructor<FocusTrapInterface>;
120
+ export {};
121
+ //# sourceMappingURL=focus-trap-mixin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"focus-trap-mixin.d.ts","sourceRoot":"","sources":["../../../../libs/web-components/src/base/focus-trap-mixin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,KAAK,CAAC;AAKjC,KAAK,WAAW,CAAC,CAAC,GAAG,MAAM,IAAI,KAAK,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;AAEzD;;;GAGG;AACH,MAAM,CAAC,OAAO,OAAO,kBAAkB;IACrC;;;;;;;;OAQG;IACH,SAAS,CAAC,kBAAkB,CAAC,YAAY,CAAC,EAAE,WAAW,GAAG,IAAI;IAE9D;;;;OAIG;IACH,SAAS,CAAC,oBAAoB,IAAI,IAAI;CACvC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6FG;AACH,wBAAgB,cAAc,CAAC,CAAC,SAAS,WAAW,CAAC,UAAU,CAAC,EAC9D,IAAI,EAAE,CAAC,GACN,CAAC,GAAG,WAAW,CAAC,kBAAkB,CAAC,CA2KrC"}
@@ -0,0 +1,82 @@
1
+ import { LitElement } from 'lit';
2
+ type Constructor<T = object> = new (...args: any[]) => T;
3
+ /**
4
+ * Type-only declaration of the shape FocusableMixin adds to a class.
5
+ * Using `declare class` emits no runtime code — it exists solely to give
6
+ * TypeScript a concrete type to work with when extending the mixin result.
7
+ * This avoids the "anonymous class private member" errors that occur when
8
+ * TypeScript tries to inline LitElement's private fields into the return type.
9
+ */
10
+ export declare class FocusableInterface {
11
+ protected get _focusableElement(): HTMLElement | null;
12
+ focus(options?: FocusOptions): void;
13
+ /**
14
+ * Enables or disables host focus participation.
15
+ * Call this whenever the component's `disabled` state changes so the
16
+ * tabIndex policy stays centralized in the mixin rather than scattered
17
+ * across component `updated()` hooks.
18
+ *
19
+ * override updated(changed: PropertyValues): void {
20
+ * super.updated(changed);
21
+ * if (changed.has('disabled')) this._setHostFocusable(!this.disabled);
22
+ * }
23
+ */
24
+ protected _setHostFocusable(enabled: boolean): void;
25
+ }
26
+ /**
27
+ * FocusableMixin
28
+ *
29
+ * Applies to all interactive Vi components. Provides:
30
+ *
31
+ * 1. `delegatesFocus: true` on the shadow root — when the host is Tab-focused
32
+ * or `.focus()` is called on it, the browser routes focus to the inner
33
+ * native control. Also activates `:focus` and `:focus-within` on the host.
34
+ *
35
+ * 2. A `focus()` override that delegates to `_focusableElement` so callers
36
+ * can do `myInput.focus()` and it Just Works without knowing shadow internals.
37
+ *
38
+ * ─────────────────────────────────────────────────────────────────────────
39
+ * ARCHITECTURE: host is the tab stop
40
+ * ─────────────────────────────────────────────────────────────────────────
41
+ *
42
+ * Host: tabIndex = 0 ← consumer-visible light-DOM tab stop
43
+ * Inner element: tabindex="0" ← participates in shadow root's own tab
44
+ * order so :focus-visible fires reliably
45
+ * delegatesFocus: true ← routes host focus → inner element
46
+ *
47
+ * This means:
48
+ * - Tab → lands on host → delegatesFocus → inner element gets visual focus
49
+ * - `:host(:focus)` and `:host(:focus-within)` both activate correctly
50
+ * - `:focus-visible` on the inner element fires reliably for all browsers
51
+ * (keyboard vs mouse distinction works without browser-specific hacks)
52
+ * - `element.focus()` calls our override → inner element focused explicitly
53
+ * - Consumer sets tabindex="-1" on host to remove from tab order entirely
54
+ * - Consumer sets tabindex="2" for explicit ordering — just works
55
+ *
56
+ * NOTE: tabindex="0" on the inner element does NOT create a second light-DOM
57
+ * tab stop. Shadow DOM children only participate in the shadow root's local
58
+ * tab order; the host remains the single entry point from the outer document.
59
+ * The difference from tabindex="-1" is that :focus-visible propagation through
60
+ * delegatesFocus is more consistent when the delegated target is a proper
61
+ * sequential-focus participant.
62
+ *
63
+ * DISABLED: When the `disabled` prop changes, the component MUST sync the
64
+ * host's tabIndex:
65
+ *
66
+ * override updated(changed: PropertyValues) {
67
+ * super.updated(changed);
68
+ * if (changed.has('disabled')) {
69
+ * this.tabIndex = this.disabled ? -1 : (previous tabIndex value or 0);
70
+ * }
71
+ * }
72
+ *
73
+ * Usage:
74
+ * class ViInput extends FocusableMixin(ViElement) {
75
+ * protected override get _focusableElement() {
76
+ * return this.shadowRoot?.querySelector('input') ?? null;
77
+ * }
78
+ * }
79
+ */
80
+ export declare function FocusableMixin<T extends Constructor<LitElement>>(Base: T): T & Constructor<FocusableInterface>;
81
+ export {};
82
+ //# sourceMappingURL=focusable-mixin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"focusable-mixin.d.ts","sourceRoot":"","sources":["../../../../libs/web-components/src/base/focusable-mixin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,KAAK,CAAC;AAIjC,KAAK,WAAW,CAAC,CAAC,GAAG,MAAM,IAAI,KAAK,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;AAEzD;;;;;;GAMG;AACH,MAAM,CAAC,OAAO,OAAO,kBAAkB;IACrC,SAAS,KAAK,iBAAiB,IAAI,WAAW,GAAG,IAAI,CAAC;IACtD,KAAK,CAAC,OAAO,CAAC,EAAE,YAAY,GAAG,IAAI;IACnC;;;;;;;;;;OAUG;IACH,SAAS,CAAC,iBAAiB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;CACpD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AACH,wBAAgB,cAAc,CAAC,CAAC,SAAS,WAAW,CAAC,UAAU,CAAC,EAC9D,IAAI,EAAE,CAAC,GACN,CAAC,GAAG,WAAW,CAAC,kBAAkB,CAAC,CA8GrC"}
@@ -0,0 +1,80 @@
1
+ /**
2
+ * FOCUSABLE_SELECTOR
3
+ *
4
+ * A CSS selector that matches all tabbable elements within a container.
5
+ *
6
+ * ─────────────────────────────────────────────────────────────────────────
7
+ * WHY CUSTOM ELEMENTS MUST BE LISTED EXPLICITLY
8
+ * ─────────────────────────────────────────────────────────────────────────
9
+ *
10
+ * CSS cannot see through a shadow root. A generic `[tabindex]:not([tabindex="-1"])`
11
+ * will NOT match a `<vi-button>` even though vi-button has tabIndex=-1 on its
12
+ * host and tabIndex=0 on its inner <button>. The inner <button> is inside the
13
+ * shadow root and is invisible to an external querySelector.
14
+ *
15
+ * Each `vi-*` element listed here uses FocusableMixin, which means:
16
+ * - The host has tabIndex=-1 (not a sequential tab stop itself)
17
+ * - `delegatesFocus: true` routes clicks inward
18
+ * - Calling `.focus()` on the host delegates to the inner control
19
+ *
20
+ * So treating the host as the focusable unit is correct — the browser's
21
+ * tab sequence reaches the inner control via the host, and programmatic
22
+ * `.focus()` on the host works because of FocusableMixin.
23
+ *
24
+ * ─────────────────────────────────────────────────────────────────────────
25
+ * MAINTENANCE RULE
26
+ * ─────────────────────────────────────────────────────────────────────────
27
+ *
28
+ * Update this file whenever a new focusable vi-* component ships.
29
+ * This is the SINGLE source of truth. FocusTrapMixin, any roving-tabindex
30
+ * manager, and a11y tests all import from here — do not duplicate this list.
31
+ *
32
+ * When adding a new component:
33
+ * 1. Add its tag to PHASE_1_VI_COMPONENTS or PHASE_2_VI_COMPONENTS below.
34
+ * 2. Uncomment it in the exported FOCUSABLE_SELECTOR when the component ships.
35
+ *
36
+ * ─────────────────────────────────────────────────────────────────────────
37
+ * USAGE
38
+ * ─────────────────────────────────────────────────────────────────────────
39
+ *
40
+ * import { FOCUSABLE_SELECTOR } from '../base/focusable-selector.js';
41
+ *
42
+ * // All focusables in a shadow root (native + vi-* hosts):
43
+ * const focusable = [
44
+ * ...this.shadowRoot!.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR),
45
+ * ];
46
+ *
47
+ * // Include slotted content too (for components that slot focusable children):
48
+ * this.shadowRoot!.querySelectorAll('slot').forEach((slotEl) => {
49
+ * (slotEl as HTMLSlotElement)
50
+ * .assignedElements({ flatten: true })
51
+ * .forEach((el) => {
52
+ * if (el.matches(FOCUSABLE_SELECTOR)) {
53
+ * focusable.push(el as HTMLElement);
54
+ * }
55
+ * });
56
+ * });
57
+ *
58
+ * ─────────────────────────────────────────────────────────────────────────
59
+ * DISABLED EXCLUSION RULES
60
+ * ─────────────────────────────────────────────────────────────────────────
61
+ *
62
+ * Native elements: :not([disabled]) excludes them from the selector.
63
+ * vi-* elements: :not([disabled]) excludes the host — works because vi-*
64
+ * components reflect `disabled` as an attribute via @property({ reflect: true }).
65
+ *
66
+ * tabindex="-1" exclusion: intentionally NOT in this selector.
67
+ * Rationale: elements with tabindex="-1" are programmatically focusable
68
+ * but not in the sequential tab order. Whether to include them in a trap
69
+ * is a caller decision (FocusTrapMixin excludes them via :not([tabindex="-1"])).
70
+ */
71
+ /**
72
+ * A CSS selector matching all tabbable elements:
73
+ * - Native HTML controls (button, input, select, textarea, a, area)
74
+ * - Elements with an explicit non-negative tabindex
75
+ * - All focusable vi-* components (listed explicitly — see above)
76
+ *
77
+ * Disabled elements and elements with tabindex="-1" are excluded.
78
+ */
79
+ export declare const FOCUSABLE_SELECTOR: string;
80
+ //# sourceMappingURL=focusable-selector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"focusable-selector.d.ts","sourceRoot":"","sources":["../../../../libs/web-components/src/base/focusable-selector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqEG;AAiCH;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB,QAInB,CAAC"}
@@ -0,0 +1,62 @@
1
+ /**
2
+ * ifNonEmpty — conditional attribute directive
3
+ *
4
+ * A wrapper around Lit's `ifDefined` that additionally removes the attribute
5
+ * when the value is an empty string. Use this for every optional string
6
+ * attribute on inner native elements where `""` and "absent" have different
7
+ * meaning for browsers and screen readers.
8
+ *
9
+ * Problem with raw `ifDefined`:
10
+ * - `ifDefined(undefined)` → removes the attribute ✅
11
+ * - `ifDefined(null)` → removes the attribute ✅
12
+ * - `ifDefined('')` → sets attribute to "" ❌
13
+ *
14
+ * With `ifNonEmpty`:
15
+ * - `ifNonEmpty(undefined)` → removes the attribute ✅
16
+ * - `ifNonEmpty(null)` → removes the attribute ✅
17
+ * - `ifNonEmpty('')` → removes the attribute ✅
18
+ * - `ifNonEmpty('hello')` → sets attribute to "hello" ✅
19
+ *
20
+ * Why it matters — real screen reader / browser bugs caused by `=""`:
21
+ * - `placeholder=""` → JAWS/NVDA still announce it as an empty placeholder
22
+ * - `aria-label=""` → NVDA reads "blank" instead of deriving the name elsewhere
23
+ * - `aria-describedby=""`→ browsers may still look for id="" element
24
+ * - `title=""` → browsers show an empty tooltip on hover in some engines
25
+ *
26
+ * ---
27
+ *
28
+ * USAGE
29
+ *
30
+ * Import in any shadow template that has optional string attributes:
31
+ *
32
+ * import { ifNonEmpty } from '../base/if-non-empty.js';
33
+ *
34
+ * In the template:
35
+ *
36
+ * // ✅ Use ifNonEmpty for optional string attributes on inner native elements
37
+ * <input
38
+ * placeholder=${ifNonEmpty(this.placeholder)}
39
+ * aria-label=${ifNonEmpty(this.label)}
40
+ * aria-describedby=${ifNonEmpty(this._descriptionId)}
41
+ * />
42
+ *
43
+ * // ❌ Do NOT use for boolean attributes — Lit has ?attr=${bool} for that
44
+ * // ❌ Do NOT use for property bindings — Lit has .prop=${val} for that
45
+ * // ❌ Do NOT use for event bindings — Lit has @event=${handler} for that
46
+ *
47
+ * ---
48
+ *
49
+ * WHEN TO USE vs NOT USE
50
+ *
51
+ * Use ifNonEmpty when:
52
+ * - The attribute is optional (component has a prop that defaults to '')
53
+ * - The inner native element is a standard HTML element (input, button, a, etc.)
54
+ * - The attribute has accessibility or UI meaning when absent vs present
55
+ *
56
+ * Skip ifNonEmpty when:
57
+ * - The attribute is always required (e.g. type="button" is never absent)
58
+ * - You need the attribute to literally be "" (rare, document when intentional)
59
+ * - The binding is to a custom element property — use .prop=${val} instead
60
+ */
61
+ export declare const ifNonEmpty: (value: string | null | undefined) => string | typeof import("lit-html").nothing;
62
+ //# sourceMappingURL=if-non-empty.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"if-non-empty.d.ts","sourceRoot":"","sources":["../../../../libs/web-components/src/base/if-non-empty.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2DG;AACH,eAAO,MAAM,UAAU,GAAI,OAAO,MAAM,GAAG,IAAI,GAAG,SAAS,+CACC,CAAC"}
@@ -0,0 +1,231 @@
1
+ import { LitElement } from 'lit';
2
+ type Constructor<T = object> = new (...args: any[]) => T;
3
+ /**
4
+ * Tri-state visual status for form controls.
5
+ *
6
+ * - `'default'` — no validation styling (neutral / untouched)
7
+ * - `'invalid'` — red border, error colours; set by the form when validation fails
8
+ * - `'valid'` — green border, success colours; set explicitly by the parent when
9
+ * it wants to confirm a correct value (independent of message)
10
+ *
11
+ * Designed to be driven from outside (Angular binding, React prop, plain JS) so
12
+ * the component never decides on its own that it is "valid" — that is always an
13
+ * explicit, intentional signal from the consuming code.
14
+ */
15
+ export type ControlStatus = 'default' | 'valid' | 'invalid';
16
+ /**
17
+ * Type-only declaration of the shape ValidityMixin adds to a class.
18
+ * `declare class` emits no runtime code — it is a TS-only contract.
19
+ *
20
+ * Properties listed here (invalid, required, validityMessage, value) must be
21
+ * declared as reactive `@property` accessors on the subclass. The mixin reads
22
+ * and writes them but cannot create them — they must exist on the concrete class
23
+ * for Lit's reactivity system to pick them up.
24
+ *
25
+ * _internals must be set via `this.attachInternals()` on the subclass.
26
+ * The class must also declare `static formAssociated = true`.
27
+ */
28
+ export declare class ValidityInterface {
29
+ /**
30
+ * Visual/validation state of the control. Reflects as the `[status]` attribute.
31
+ * - `'default'` — no validation styling
32
+ * - `'invalid'` — red border / error colours
33
+ * - `'valid'` — green border / success colours
34
+ *
35
+ * Set by the mixin's constraint-validation methods AND by the consuming
36
+ * code/framework. The parent always controls this — the component never
37
+ * auto-promotes itself to `'valid'`.
38
+ */
39
+ get status(): ControlStatus;
40
+ set status(value: ControlStatus);
41
+ /** Whether a value is required. Drives the valueMissing validity flag. */
42
+ get required(): boolean;
43
+ set required(value: boolean);
44
+ /** The human-readable validation message displayed in the component UI. */
45
+ get validityMessage(): string;
46
+ set validityMessage(value: string);
47
+ /** The current component value. Read by _testValidity for required check. */
48
+ get value(): string;
49
+ set value(v: string);
50
+ /**
51
+ * ElementInternals instance. Subclass MUST declare:
52
+ * protected readonly _internals = this.attachInternals();
53
+ * and set:
54
+ * static formAssociated = true;
55
+ */
56
+ protected readonly _internals: ElementInternals;
57
+ /**
58
+ * Checks if the current value satisfies all constraints.
59
+ * Fires a cancelable 'invalid' event (bubbles: false) if invalid.
60
+ * Does NOT show browser validation tooltip.
61
+ * Returns true if valid.
62
+ */
63
+ checkValidity(): boolean;
64
+ /**
65
+ * Checks validity AND triggers the browser's built-in validation UI
66
+ * (tooltip near the field). Delegates to ElementInternals.reportValidity().
67
+ * Returns true if valid.
68
+ */
69
+ reportValidity(): boolean;
70
+ /**
71
+ * Sets an arbitrary custom validation message.
72
+ * Pass an empty string to clear the custom error and restore validity.
73
+ * Syncs immediately to ElementInternals so native form constraint API works.
74
+ */
75
+ setCustomValidity(message: string): void;
76
+ /**
77
+ * Returns the set of ValidityStateFlags describing why the value is invalid.
78
+ * Return {} (empty object) when the value is valid.
79
+ *
80
+ * The base implementation returns {} (always valid).
81
+ * Subclasses override this to add component-specific checks.
82
+ *
83
+ * Standard flags (all optional, all boolean):
84
+ * valueMissing — required field has no value
85
+ * tooShort — value is shorter than minlength
86
+ * tooLong — value exceeds maxlength
87
+ * rangeUnderflow — numeric value < min
88
+ * rangeOverflow — numeric value > max
89
+ * patternMismatch— value doesn't match pattern
90
+ * typeMismatch — value not well-formed for type (email, url, etc.)
91
+ * badInput — user input cannot be converted to a value at all
92
+ * customError — setCustomValidity() was called with a non-empty string
93
+ * stepMismatch — value doesn't conform to step
94
+ */
95
+ protected _testValidity(): Partial<ValidityStateFlags>;
96
+ }
97
+ /**
98
+ * ValidityMixin
99
+ *
100
+ * Adds the standard form validation API (`checkValidity`, `reportValidity`,
101
+ * `setCustomValidity`) to any form-associated Lit element.
102
+ *
103
+ * Backed by the native `ElementInternals` API so the component participates
104
+ * in `HTMLFormElement` constraint validation, `.elements`, and browser
105
+ * validation UI — exactly like a native `<input>`.
106
+ *
107
+ * ─────────────────────────────────────────────────────────────────────────
108
+ * MINIMUM SUBCLASS REQUIREMENTS
109
+ * ─────────────────────────────────────────────────────────────────────────
110
+ *
111
+ * 1. Declare static formAssociated = true;
112
+ * Makes the browser register this element as a form participant.
113
+ *
114
+ * 2. Attach ElementInternals:
115
+ * protected readonly _internals = this.attachInternals();
116
+ * Must be a field initializer (runs after super() in the constructor).
117
+ *
118
+ * 3. Declare reactive properties (MUST be @property so Lit tracks changes):
119
+ * @property({ reflect: true }) accessor status: ControlStatus = 'default';
120
+ * @property({ type: Boolean, reflect: true }) accessor required = false;
121
+ * @property() accessor validityMessage = '';
122
+ * @property() accessor value = '';
123
+ *
124
+ * 4. Sync value to internals on every value change:
125
+ * override updated(changed: PropertyValues): void {
126
+ * super.updated(changed);
127
+ * if (changed.has('value')) {
128
+ * this._internals.setFormValue(this.value);
129
+ * }
130
+ * }
131
+ *
132
+ * 5. Handle form reset:
133
+ * formResetCallback(): void {
134
+ * this.value = this.getAttribute('value') ?? '';
135
+ * this.status = 'default';
136
+ * this.validityMessage = '';
137
+ * }
138
+ *
139
+ * 6. Handle fieldset/form disable:
140
+ * formDisabledCallback(disabled: boolean): void {
141
+ * this.disabled = disabled;
142
+ * }
143
+ *
144
+ * ─────────────────────────────────────────────────────────────────────────
145
+ * OVERRIDE _testValidity() FOR CUSTOM CONSTRAINTS
146
+ * ─────────────────────────────────────────────────────────────────────────
147
+ *
148
+ * protected override _testValidity(): Partial<ValidityStateFlags> {
149
+ * if (this.required && !this.value) return { valueMissing: true };
150
+ * return {};
151
+ * }
152
+ *
153
+ * Chain constraints for components with multiple rules (e.g. vi-input[type="number"]):
154
+ *
155
+ * protected override _testValidity(): Partial<ValidityStateFlags> {
156
+ * if (this.required && !this.value) return { valueMissing: true };
157
+ * if (this.minlength && this.value.length < this.minlength)
158
+ * return { tooShort: true };
159
+ * if (this.maxlength && this.value.length > this.maxlength)
160
+ * return { tooLong: true };
161
+ * return {};
162
+ * }
163
+ *
164
+ * ─────────────────────────────────────────────────────────────────────────
165
+ * FULL USAGE EXAMPLE — vi-input
166
+ * ─────────────────────────────────────────────────────────────────────────
167
+ *
168
+ * import { property } from 'lit/decorators.js';
169
+ * import { ValidityMixin } from '../base/validity-mixin.js';
170
+ * import { FocusableMixin } from '../base/focusable-mixin.js';
171
+ * import { ViElement } from '../base/vi-element.js';
172
+ *
173
+ * @customElement('vi-input')
174
+ * export class ViInput extends ValidityMixin(FocusableMixin(ViElement)) {
175
+ * static override formAssociated = true;
176
+ * protected readonly _internals = this.attachInternals();
177
+ *
178
+ * @property({ reflect: true }) accessor status: ControlStatus = 'default';
179
+ * @property({ type: Boolean, reflect: true }) accessor required = false;
180
+ * @property() accessor validityMessage = '';
181
+ * @property() accessor value = '';
182
+ *
183
+ * protected override _testValidity(): Partial<ValidityStateFlags> {
184
+ * if (this.required && !this.value) return { valueMissing: true };
185
+ * return {};
186
+ * }
187
+ *
188
+ * override updated(changed: PropertyValues): void {
189
+ * super.updated(changed);
190
+ * if (changed.has('value')) this._internals.setFormValue(this.value);
191
+ * }
192
+ *
193
+ * formResetCallback(): void {
194
+ * this.value = this.getAttribute('value') ?? '';
195
+ * this.status = 'default';
196
+ * this.validityMessage = '';
197
+ * }
198
+ *
199
+ * formDisabledCallback(disabled: boolean): void {
200
+ * this.disabled = disabled;
201
+ * }
202
+ * }
203
+ *
204
+ * ─────────────────────────────────────────────────────────────────────────
205
+ * INVALID EVENT BEHAVIOUR
206
+ * ─────────────────────────────────────────────────────────────────────────
207
+ *
208
+ * checkValidity() fires a cancelable 'invalid' event when validation fails.
209
+ * The event does NOT bubble (matches native form element behaviour).
210
+ * Consumer can suppress the default UI by calling event.preventDefault().
211
+ *
212
+ * Example:
213
+ * myInput.addEventListener('invalid', (e) => {
214
+ * e.preventDefault(); // suppress browser tooltip
215
+ * showMyCustomError(myInput.validityMessage);
216
+ * });
217
+ *
218
+ * ─────────────────────────────────────────────────────────────────────────
219
+ * MIXIN COMPOSITION ORDER
220
+ * ─────────────────────────────────────────────────────────────────────────
221
+ *
222
+ * ValidityMixin should wrap FocusableMixin (outermost):
223
+ * ValidityMixin(FocusableMixin(ViElement))
224
+ *
225
+ * Reason: ValidityMixin only adds methods (checkValidity etc.). It does not
226
+ * touch shadowRootOptions or focus delegation, so order has no side-effects.
227
+ * Convention is: functionality mixins wrap infrastructure mixins.
228
+ */
229
+ export declare function ValidityMixin<T extends Constructor<LitElement>>(Base: T): T & Constructor<ValidityInterface>;
230
+ export {};
231
+ //# sourceMappingURL=validity-mixin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validity-mixin.d.ts","sourceRoot":"","sources":["../../../../libs/web-components/src/base/validity-mixin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,KAAK,CAAC;AAIjC,KAAK,WAAW,CAAC,CAAC,GAAG,MAAM,IAAI,KAAK,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;AAEzD;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,OAAO,GAAG,SAAS,CAAC;AAE5D;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,OAAO,OAAO,iBAAiB;IAOpC;;;;;;;;;OASG;IACH,IAAI,MAAM,IAAI,aAAa,CAAC;IAC5B,IAAI,MAAM,CAAC,KAAK,EAAE,aAAa,EAAE;IAEjC,0EAA0E;IAC1E,IAAI,QAAQ,IAAI,OAAO,CAAC;IACxB,IAAI,QAAQ,CAAC,KAAK,EAAE,OAAO,EAAE;IAE7B,2EAA2E;IAC3E,IAAI,eAAe,IAAI,MAAM,CAAC;IAC9B,IAAI,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE;IAEnC,6EAA6E;IAC7E,IAAI,KAAK,IAAI,MAAM,CAAC;IACpB,IAAI,KAAK,CAAC,CAAC,EAAE,MAAM,EAAE;IAIrB;;;;;OAKG;IACH,SAAS,CAAC,QAAQ,CAAC,UAAU,EAAE,gBAAgB,CAAC;IAIhD;;;;;OAKG;IACH,aAAa,IAAI,OAAO;IAExB;;;;OAIG;IACH,cAAc,IAAI,OAAO;IAEzB;;;;OAIG;IACH,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAIxC;;;;;;;;;;;;;;;;;;OAkBG;IAKH,SAAS,CAAC,aAAa,IAAI,OAAO,CAAC,kBAAkB,CAAC;CACvD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmIG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,WAAW,CAAC,UAAU,CAAC,EAC7D,IAAI,EAAE,CAAC,GACN,CAAC,GAAG,WAAW,CAAC,iBAAiB,CAAC,CA0FpC"}
@@ -0,0 +1,12 @@
1
+ import { LitElement } from 'lit';
2
+ /**
3
+ * Shared base class for Vi web components.
4
+ * Keep this thin — behaviour is added via mixins (FocusableMixin, FocusTrapMixin).
5
+ */
6
+ export declare class ViElement extends LitElement {
7
+ }
8
+ /** Component size scale */
9
+ export type ViSize = 'sm' | 'md' | 'lg';
10
+ /** Semantic status — maps to colour tokens (success, warning, danger, info, neutral) */
11
+ export type ViStatus = 'neutral' | 'success' | 'warning' | 'danger' | 'info';
12
+ //# sourceMappingURL=vi-element.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vi-element.d.ts","sourceRoot":"","sources":["../../../../libs/web-components/src/base/vi-element.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,KAAK,CAAC;AAEjC;;;GAGG;AACH,qBAAa,SAAU,SAAQ,UAAU;CAAG;AAE5C,2BAA2B;AAC3B,MAAM,MAAM,MAAM,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAExC,wFAAwF;AACxF,MAAM,MAAM,QAAQ,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG,QAAQ,GAAG,MAAM,CAAC"}
@@ -0,0 +1,2 @@
1
+ export { ViButton } from './vi-button.js';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../libs/web-components/src/button/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC"}