@vialiq/web-components 0.1.3 → 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,68 @@
1
+ import { type PropertyValues, type TemplateResult } from 'lit';
2
+ import { ViElement } from '../base/vi-element.js';
3
+ /**
4
+ * Button visual variant.
5
+ * Defined here, not in ViElement — each component owns its variant type
6
+ * (functional composition, not inheritance). Different components may
7
+ * support different subsets of these values.
8
+ */
9
+ export type ButtonVariant = 'primary' | 'secondary' | 'danger' | 'success' | 'info' | 'ghost';
10
+ /**
11
+ * Button size scale. Controls padding and font-size via CSS custom properties.
12
+ * xs → sm → md (default) → lg
13
+ */
14
+ export type ButtonSize = 'xs' | 'sm' | 'md' | 'lg';
15
+ /**
16
+ * Icon placement relative to the label.
17
+ * 'start' (default) places the icon before the text; 'end' places it after.
18
+ * Controlled via CSS `order` — no extra wrapper elements needed.
19
+ */
20
+ export type ButtonIconPlacement = 'start' | 'end';
21
+ declare const ViButton_base: typeof ViElement & (new (...args: any[]) => import("../base/focusable-mixin.js").FocusableInterface);
22
+ /**
23
+ * vi-button
24
+ * Self-styled button component using Flux UI token fallbacks.
25
+ *
26
+ * @element vi-button
27
+ * @attr variant - Visual variant: primary | secondary | danger | success | info
28
+ * @attr size - Size scale: xs | sm | md | lg (default: md)
29
+ * @attr icon-placement - Icon slot position: start | end (default: start)
30
+ * @attr disabled - Disables the button
31
+ * @attr full-width - Stretches button to fill its container
32
+ * @attr icon-only - Styles the button for an icon-only layout
33
+ *
34
+ * @slot - Button label (text / content)
35
+ * @slot icon - A single icon (vi-icon or any inline SVG)
36
+ *
37
+ * @csspart button - The inner <button> element
38
+ * @csspart icon - The icon slot wrapper
39
+ * @csspart label - The label span
40
+ */
41
+ export declare class ViButton extends ViButton_base {
42
+ static styles: import("lit").CSSResult;
43
+ protected get _focusableElement(): HTMLButtonElement | null;
44
+ /** Visual variant. */
45
+ accessor variant: ButtonVariant;
46
+ /** Size scale — controls padding and font-size. */
47
+ accessor size: ButtonSize;
48
+ /** Icon placement: 'start' (before label) or 'end' (after label). CSS order handles it — no DOM changes on toggle. */
49
+ accessor iconPlacement: ButtonIconPlacement;
50
+ /** When true, stretches the button to fill the width of its container. */
51
+ accessor fullWidth: boolean;
52
+ /** When true, styles the button for an icon-only layout (typically square with equal padding). */
53
+ accessor iconOnly: boolean;
54
+ /** Disables the button. */
55
+ accessor disabled: boolean;
56
+ private accessor _hasIcon;
57
+ updated(changed: PropertyValues): void;
58
+ private onIconSlotChange;
59
+ private onClick;
60
+ render(): TemplateResult;
61
+ }
62
+ declare global {
63
+ interface HTMLElementTagNameMap {
64
+ 'vi-button': ViButton;
65
+ }
66
+ }
67
+ export {};
68
+ //# sourceMappingURL=vi-button.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vi-button.d.ts","sourceRoot":"","sources":["../../../../libs/web-components/src/button/vi-button.ts"],"names":[],"mappings":"AAAA,OAAO,EAAwB,KAAK,cAAc,EAAE,KAAK,cAAc,EAAE,MAAM,KAAK,CAAC;AAGrF,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAGlD;;;;;GAKG;AACH,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,WAAW,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,GAAG,OAAO,CAAC;AAE9F;;;GAGG;AACH,MAAM,MAAM,UAAU,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAEnD;;;;GAIG;AACH,MAAM,MAAM,mBAAmB,GAAG,OAAO,GAAG,KAAK,CAAC;;AAElD;;;;;;;;;;;;;;;;;;GAkBG;AACH,qBACa,QAAS,SAAQ,aAAyB;IACrD,OAAgB,MAAM,0BAAmC;IAEzD,cAAuB,iBAAiB,IAAI,iBAAiB,GAAG,IAAI,CAEnE;IAED,sBAAsB;IACqB,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAa;IAEvF,mDAAmD;IACR,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAQ;IAE5E,sHAAsH;IAC9C,QAAQ,CAAC,aAAa,EAAE,mBAAmB,CAAW;IAE9H,0EAA0E;IACL,QAAQ,CAAC,SAAS,UAAS;IAEhG,kGAAkG;IAC9B,QAAQ,CAAC,QAAQ,UAAS;IAE9F,2BAA2B;IACiB,QAAQ,CAAC,QAAQ,UAAS;IAE7D,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAElC,OAAO,CAAC,OAAO,EAAE,cAAc,GAAG,IAAI;IAe/C,OAAO,CAAC,gBAAgB;IAKxB,OAAO,CAAC,OAAO;IAON,MAAM,IAAI,cAAc;CAuBlC;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,qBAAqB;QAC7B,WAAW,EAAE,QAAQ,CAAC;KACvB;CACF"}
@@ -1,158 +1,9 @@
1
1
  import { unsafeCSS, css, html } from 'lit';
2
2
  import { customElement, property, state } from 'lit/decorators.js';
3
+ import { F as FocusableMixin } from '../focusable-mixin-CmxOyPX5.js';
3
4
  import { V as ViElement } from '../vi-element-C6GfDPs3.js';
4
5
 
5
- /**
6
- * FocusableMixin
7
- *
8
- * Applies to all interactive Vi components. Provides:
9
- *
10
- * 1. `delegatesFocus: true` on the shadow root — when the host is Tab-focused
11
- * or `.focus()` is called on it, the browser routes focus to the inner
12
- * native control. Also activates `:focus` and `:focus-within` on the host.
13
- *
14
- * 2. A `focus()` override that delegates to `_focusableElement` so callers
15
- * can do `myInput.focus()` and it Just Works without knowing shadow internals.
16
- *
17
- * ─────────────────────────────────────────────────────────────────────────
18
- * ARCHITECTURE: host is the tab stop
19
- * ─────────────────────────────────────────────────────────────────────────
20
- *
21
- * Host: tabIndex = 0 ← consumer-visible tab stop
22
- * Inner element: tabindex="-1" ← NOT directly in tab order; only reachable
23
- * via host delegation
24
- * delegatesFocus: true ← routes host focus → inner element
25
- *
26
- * This means:
27
- * - Tab → lands on host → delegatesFocus → inner element gets visual focus
28
- * - `:host(:focus)` and `:host(:focus-within)` both work correctly
29
- * - `element.focus()` calls our override → inner element focused explicitly
30
- * - Consumer sets tabindex="-1" on host to remove from tab order entirely
31
- * - Consumer sets tabindex="2" for explicit positioning — just works
32
- *
33
- * CRITICAL: Every component using this mixin MUST set tabindex="-1" on its
34
- * inner native element in render() to prevent double-tab. Failing to do so
35
- * creates two tab stops for a single logical control.
36
- *
37
- * DISABLED: When the `disabled` prop changes, the component MUST sync the
38
- * host's tabIndex:
39
- *
40
- * override updated(changed: PropertyValues) {
41
- * super.updated(changed);
42
- * if (changed.has('disabled')) {
43
- * this.tabIndex = this.disabled ? -1 : (previous tabIndex value or 0);
44
- * }
45
- * }
46
- *
47
- * Usage:
48
- * class ViInput extends FocusableMixin(ViElement) {
49
- * protected override get _focusableElement() {
50
- * return this.shadowRoot?.querySelector('input') ?? null;
51
- * }
52
- * }
53
- */ function FocusableMixin(Base) {
54
- class FocusableMixinClass extends Base {
55
- /**
56
- * Spread existing shadow root options so we don't clobber `mode: 'open'`
57
- * or any other options already set by a base class or another mixin.
58
- * `override` is omitted: TypeScript cannot verify the static side of the
59
- * generic `Base` constructor has `shadowRootOptions`, so we re-declare
60
- * without override (the property is still inherited at runtime).
61
- */ // eslint-disable-next-line @typescript-eslint/no-explicit-any
62
- static shadowRootOptions = {
63
- ...Base.shadowRootOptions,
64
- delegatesFocus: true
65
- };
66
- /**
67
- * The tabIndex to restore when transitioning from disabled → enabled.
68
- * Snapshotted in connectedCallback and updated whenever we save before
69
- * disabling, so we can honour custom consumer tabindex values (e.g. 2)
70
- * rather than blindly restoring to 0.
71
- */ _savedTabIndex = 0;
72
- connectedCallback() {
73
- super.connectedCallback();
74
- // The host is the user-visible tab stop. Custom elements are NOT in the
75
- // tab order by default (tabIndex = -1), so we must explicitly opt in.
76
- // Only set the default if the consumer hasn't already specified a value.
77
- // `tabIndex` is a reflected IDL attribute — both attribute sets and
78
- // programmatic sets (`element.tabIndex = 2`) always reflect to the
79
- // `tabindex` attribute, so `hasAttribute` is a complete guard for both.
80
- // tabindex="-1" → remove from tab order entirely (e.g. inside a focus trap)
81
- // tabindex="0" → participate (same as our default)
82
- // tabindex="2" → explicit ordering position
83
- // Note: connectedCallback is used (not constructor) to avoid the
84
- // "DOMException: The result must not have attributes" error during upgrade.
85
- if (!this.hasAttribute('tabindex')) {
86
- this.tabIndex = 0;
87
- }
88
- // Snapshot the current effective tabIndex so _setHostFocusable(true)
89
- // can restore it rather than blindly resetting to 0.
90
- this._savedTabIndex = this.tabIndex;
91
- }
92
- /**
93
- * Centralizes the tabIndex policy for enabled/disabled state.
94
- *
95
- * enabled=true → restore the tabIndex that was in effect before disabling
96
- * (respects consumer tabindex="2", tabindex="-1", etc.)
97
- * enabled=false → tabIndex = -1 (host skipped by Tab; whole component unreachable)
98
- *
99
- * The pre-disable tabIndex is saved so that a consumer who set tabindex="2"
100
- * gets back tabindex="2" after re-enabling, not a hardcoded 0.
101
- *
102
- * All components with a `disabled` prop MUST call this in `updated()`:
103
- *
104
- * if (changed.has('disabled')) this._setHostFocusable(!this.disabled);
105
- */ _setHostFocusable(enabled) {
106
- if (enabled) {
107
- this.tabIndex = this._savedTabIndex;
108
- } else {
109
- // Only snapshot when we are actually in an enabled state; if this is
110
- // called repeatedly while disabled (tabIndex already -1) we must not
111
- // overwrite the real saved value with -1.
112
- if (this.tabIndex !== -1) {
113
- this._savedTabIndex = this.tabIndex;
114
- }
115
- this.tabIndex = -1;
116
- }
117
- }
118
- /**
119
- * Subclasses MUST override this getter to return the specific inner element
120
- * that should receive programmatic focus.
121
- * Returning `null` before first render is safe — `focus()` no-ops.
122
- */ get _focusableElement() {
123
- return null;
124
- }
125
- /**
126
- * Public focus() override.
127
- *
128
- * Explicitly delegates to `_focusableElement` when available. While `delegatesFocus: true`
129
- * handles click routing, relying solely on native programmatic focus hands control
130
- * to the browser, which blindly targets the *first* focusable element in
131
- * shadow DOM order — not necessarily the intended one.
132
- *
133
- * Example: a vi-input might render a "Clear" <button> before the <input>
134
- * in the DOM. Native focus would land on the clear button; this explicit
135
- * call guarantees focus lands on the <input> regardless of DOM order.
136
- *
137
- * If called before the first render (when `_focusableElement` is null),
138
- * it safely falls back to `super.focus()`.
139
- */ focus(options) {
140
- const target = this._focusableElement;
141
- if (target) {
142
- target.focus(options);
143
- } else {
144
- // Fallback to native behavior if called before first render
145
- super.focus(options);
146
- }
147
- }
148
- }
149
- // Cast required: TypeScript cannot reconcile LitElement's private fields
150
- // with the anonymous class return type. `as unknown as` is the standard
151
- // pattern recommended by both the TypeScript and Lit teams for mixins.
152
- return FocusableMixinClass;
153
- }
154
-
155
- const buttonStyles = "@charset \"UTF-8\";@layer reset,components,utilities;@layer components{.button{display:inline-flex;align-items:center;justify-content:center;gap:var(--vi-spacing-xs, 8px);border:var(--vi-border-width-thin, 1px) solid transparent;border-radius:var(--vi-button-shape-border-radius, var(--vi-border-radius-md, 4px));padding:var(--vi-button-spacing-padding-block, var(--vi-spacing-sm, 16px)) var(--vi-button-spacing-padding-inline, var(--vi-spacing-md, 24px));font-size:var(--vi-button-typography-font-size, var(--vi-font-size-base, 16px));font-weight:var(--vi-button-typography-font-weight, var(--vi-font-weight-semibold, 600));line-height:var(--vi-line-height-tight, 1.2);cursor:pointer;-webkit-user-select:none;user-select:none;transition:opacity var(--vi-button-effect-transition-duration, .16s) ease}}:host{display:inline-block}:host([size=xs]){--vi-button-spacing-padding-block: 2px;--vi-button-spacing-padding-inline: 8px;--vi-button-typography-font-size: var(--vi-font-size-xs, 12px)}:host([size=sm]){--vi-button-spacing-padding-block: 4px;--vi-button-spacing-padding-inline: 12px;--vi-button-typography-font-size: var(--vi-font-size-sm, 14px)}:host([size=lg]){--vi-button-spacing-padding-block: 12px;--vi-button-spacing-padding-inline: 24px;--vi-button-typography-font-size: var(--vi-font-size-lg, 18px)}:host([full-width]){display:block}:host([full-width]) .button{width:100%}:host([icon-only]) .button{aspect-ratio:1/1;padding:var(--vi-button-spacing-padding-block, var(--vi-spacing-sm, 16px));justify-content:center}:host([icon-only]) .label{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}:host([variant=primary]) .button{background-color:var(--vi-button-surface-primary-background-color, var(--vi-color-primary, #3676d0));color:var(--vi-button-surface-primary-text-color, var(--vi-color-grey-100, #f5f5f5))}:host([variant=secondary]) .button{background-color:var(--vi-button-surface-secondary-background-color, var(--vi-color-secondary, #f0f4f8));color:var(--vi-button-surface-secondary-text-color, var(--vi-color-foreground, #111827));border-color:var(--vi-color-border, #e5e7eb)}:host([variant=danger]) .button{background-color:var(--vi-button-surface-danger-background-color, var(--vi-color-error, #ef4444));color:var(--vi-button-surface-danger-text-color, var(--vi-color-grey-100, #f5f5f5))}:host([variant=success]) .button{background-color:var(--vi-button-surface-success-background-color, var(--vi-color-success, #489167));color:var(--vi-button-surface-success-text-color, var(--vi-color-grey-100, #f5f5f5))}:host([variant=info]) .button{background-color:var(--vi-button-surface-info-background-color, var(--vi-color-info, #3676d0));color:var(--vi-button-surface-info-text-color, var(--vi-color-grey-100, #f5f5f5))}:host([variant=ghost]) .button{background-color:var(--vi-button-surface-ghost-background-color, transparent);color:var(--vi-button-surface-ghost-text-color, var(--vi-color-primary, #3676d0))}:host([disabled]) .button,.button:disabled{opacity:.6;cursor:not-allowed}.button:not(:disabled){box-shadow:var(--vi-button-effect-shadow-raised, inset 0 1px 0 rgba(255, 255, 255, .14), 0 2px 4px rgba(0, 0, 0, .18), 0 1px 2px rgba(0, 0, 0, .08))}.button:hover:not(:disabled){opacity:.92;box-shadow:var(--vi-button-effect-shadow-hover, inset 0 1px 0 rgba(255, 255, 255, .18), 0 4px 8px rgba(0, 0, 0, .22), 0 2px 4px rgba(0, 0, 0, .1));transform:translateY(-1px)}.button:active:not(:disabled){box-shadow:var(--vi-button-effect-shadow-pressed, inset 0 2px 4px rgba(0, 0, 0, .22), 0 1px 1px rgba(0, 0, 0, .06));transform:translateY(1px);opacity:1}:host([variant=ghost]) .button:hover:not(:disabled){background-color:#0000000a;text-decoration:underline;opacity:1}:host([variant=ghost]) .button:not(:disabled){box-shadow:none}:host([variant=ghost]) .button:active:not(:disabled){box-shadow:inset 0 1px 3px #00000024;transform:translateY(0)}@media(prefers-reduced-motion:reduce){.button{transform:none!important;transition-property:color,background-color,border-color,box-shadow,opacity!important}}.icon{order:-1;display:inline-flex;flex-shrink:0}.icon[hidden]{display:none}:host([icon-placement=end]) .icon{order:1}::slotted(vi-icon),::slotted(svg){--vi-icon-size: 1em;width:1em;height:1em}.label{flex:1 1 auto;min-width:0}";
6
+ const buttonStyles = "@charset \"UTF-8\";@layer reset,components,utilities;@layer components{.button{display:inline-flex;align-items:center;justify-content:center;gap:var(--vi-spacing-xs, 8px);border:var(--vi-border-width-thin, 1px) solid transparent;border-radius:var(--vi-button-shape-border-radius, var(--vi-border-radius-md, 4px));padding:var(--vi-button-spacing-padding-block, var(--vi-spacing-sm, 16px)) var(--vi-button-spacing-padding-inline, var(--vi-spacing-md, 24px));font-size:var(--vi-button-typography-font-size, var(--vi-font-size-base, 16px));font-weight:var(--vi-button-typography-font-weight, var(--vi-font-weight-semibold, 600));line-height:var(--vi-line-height-tight, 1.2);cursor:pointer;-webkit-user-select:none;user-select:none;transition:opacity var(--vi-button-effect-transition-duration, .16s) ease,box-shadow var(--vi-button-effect-transition-duration, .16s) ease,transform var(--vi-button-effect-transition-duration, .16s) ease}}:host{display:inline-block}:host([size=xs]){--vi-button-spacing-padding-block: 2px;--vi-button-spacing-padding-inline: 8px;--vi-button-typography-font-size: var(--vi-font-size-xs, 12px)}:host([size=sm]){--vi-button-spacing-padding-block: 4px;--vi-button-spacing-padding-inline: 12px;--vi-button-typography-font-size: var(--vi-font-size-sm, 14px)}:host([size=lg]){--vi-button-spacing-padding-block: 12px;--vi-button-spacing-padding-inline: 24px;--vi-button-typography-font-size: var(--vi-font-size-lg, 18px)}:host([full-width]){display:block}:host([full-width]) .button{width:100%}:host([icon-only]) .button{aspect-ratio:1/1;padding:var(--vi-button-spacing-padding-block, var(--vi-spacing-sm, 16px));justify-content:center}:host([icon-only]) .label{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}:host([variant=primary]) .button{background-color:var(--vi-button-surface-primary-background-color, var(--vi-color-primary, #3676d0));color:var(--vi-button-surface-primary-text-color, var(--vi-color-grey-100, #f5f5f5))}:host([variant=secondary]) .button{background-color:var(--vi-button-surface-secondary-background-color, var(--vi-color-secondary, #f0f4f8));color:var(--vi-button-surface-secondary-text-color, var(--vi-color-foreground, #111827));border-color:var(--vi-color-border, #e5e7eb)}:host([variant=danger]) .button{background-color:var(--vi-button-surface-danger-background-color, var(--vi-color-error, #ef4444));color:var(--vi-button-surface-danger-text-color, var(--vi-color-grey-100, #f5f5f5))}:host([variant=success]) .button{background-color:var(--vi-button-surface-success-background-color, var(--vi-color-success, #489167));color:var(--vi-button-surface-success-text-color, var(--vi-color-grey-100, #f5f5f5))}:host([variant=info]) .button{background-color:var(--vi-button-surface-info-background-color, var(--vi-color-info, #3676d0));color:var(--vi-button-surface-info-text-color, var(--vi-color-grey-100, #f5f5f5))}:host([variant=ghost]) .button{background-color:var(--vi-button-surface-ghost-background-color, transparent);color:var(--vi-button-surface-ghost-text-color, var(--vi-color-primary, #3676d0))}:host([disabled]) .button,.button:disabled{opacity:.6;cursor:not-allowed}.button:not(:disabled){box-shadow:var(--vi-button-effect-shadow-raised, inset 0 1px 0 rgba(255, 255, 255, .14), 0 2px 4px rgba(0, 0, 0, .18), 0 1px 2px rgba(0, 0, 0, .08))}.button:hover:not(:disabled){opacity:.92;box-shadow:var(--vi-button-effect-shadow-hover, inset 0 1px 0 rgba(255, 255, 255, .18), 0 4px 8px rgba(0, 0, 0, .22), 0 2px 4px rgba(0, 0, 0, .1));transform:translateY(-1px)}.button:active:not(:disabled){box-shadow:var(--vi-button-effect-shadow-pressed, inset 0 2px 4px rgba(0, 0, 0, .22), 0 1px 1px rgba(0, 0, 0, .06));transform:translateY(1px);opacity:1}:host([variant=ghost]) .button:hover:not(:disabled){background-color:#0000000a;text-decoration:underline;opacity:1}:host([variant=ghost]) .button:not(:disabled){box-shadow:none}:host([variant=ghost]) .button:active:not(:disabled){box-shadow:inset 0 1px 3px #00000024;transform:translateY(0)}.button:focus-visible:not(:disabled){outline:var(--vi-border-width-base, 2px) solid var(--vi-button-focus-ring-color, var(--vi-color-primary, #3676d0));outline-offset:3px;box-shadow:var(--vi-button-effect-shadow-raised, inset 0 1px 0 rgba(255, 255, 255, .14), 0 2px 4px rgba(0, 0, 0, .18), 0 1px 2px rgba(0, 0, 0, .08)),0 0 0 3px var(--vi-button-focus-ring-glow, var(--vi-color-blue-200, #cee6ff))}@media(prefers-reduced-motion:reduce){.button{transform:none!important;transition-property:color,background-color,border-color,box-shadow,opacity!important}}.icon{order:-1;display:inline-flex;flex-shrink:0}.icon[hidden]{display:none}:host([icon-placement=end]) .icon{order:1}::slotted(vi-icon),::slotted(svg){--vi-icon-size: 1em;width:1em;height:1em}.label{flex:1 1 auto;min-width:0}";
156
7
 
157
8
  function applyDecs2203RFactory() {
158
9
  function createAddInitializerMethod(initializers, decoratorFinishedRef) {
@@ -686,7 +537,7 @@ new class extends _identity {
686
537
  class="button"
687
538
  part="button"
688
539
  type="button"
689
- tabindex="-1"
540
+ tabindex="0"
690
541
  ?disabled=${disabled}
691
542
  @click=${onClick}
692
543
  >
@@ -0,0 +1,155 @@
1
+ /**
2
+ * FocusableMixin
3
+ *
4
+ * Applies to all interactive Vi components. Provides:
5
+ *
6
+ * 1. `delegatesFocus: true` on the shadow root — when the host is Tab-focused
7
+ * or `.focus()` is called on it, the browser routes focus to the inner
8
+ * native control. Also activates `:focus` and `:focus-within` on the host.
9
+ *
10
+ * 2. A `focus()` override that delegates to `_focusableElement` so callers
11
+ * can do `myInput.focus()` and it Just Works without knowing shadow internals.
12
+ *
13
+ * ─────────────────────────────────────────────────────────────────────────
14
+ * ARCHITECTURE: host is the tab stop
15
+ * ─────────────────────────────────────────────────────────────────────────
16
+ *
17
+ * Host: tabIndex = 0 ← consumer-visible light-DOM tab stop
18
+ * Inner element: tabindex="0" ← participates in shadow root's own tab
19
+ * order so :focus-visible fires reliably
20
+ * delegatesFocus: true ← routes host focus → inner element
21
+ *
22
+ * This means:
23
+ * - Tab → lands on host → delegatesFocus → inner element gets visual focus
24
+ * - `:host(:focus)` and `:host(:focus-within)` both activate correctly
25
+ * - `:focus-visible` on the inner element fires reliably for all browsers
26
+ * (keyboard vs mouse distinction works without browser-specific hacks)
27
+ * - `element.focus()` calls our override → inner element focused explicitly
28
+ * - Consumer sets tabindex="-1" on host to remove from tab order entirely
29
+ * - Consumer sets tabindex="2" for explicit ordering — just works
30
+ *
31
+ * NOTE: tabindex="0" on the inner element does NOT create a second light-DOM
32
+ * tab stop. Shadow DOM children only participate in the shadow root's local
33
+ * tab order; the host remains the single entry point from the outer document.
34
+ * The difference from tabindex="-1" is that :focus-visible propagation through
35
+ * delegatesFocus is more consistent when the delegated target is a proper
36
+ * sequential-focus participant.
37
+ *
38
+ * DISABLED: When the `disabled` prop changes, the component MUST sync the
39
+ * host's tabIndex:
40
+ *
41
+ * override updated(changed: PropertyValues) {
42
+ * super.updated(changed);
43
+ * if (changed.has('disabled')) {
44
+ * this.tabIndex = this.disabled ? -1 : (previous tabIndex value or 0);
45
+ * }
46
+ * }
47
+ *
48
+ * Usage:
49
+ * class ViInput extends FocusableMixin(ViElement) {
50
+ * protected override get _focusableElement() {
51
+ * return this.shadowRoot?.querySelector('input') ?? null;
52
+ * }
53
+ * }
54
+ */ function FocusableMixin(Base) {
55
+ class FocusableMixinClass extends Base {
56
+ /**
57
+ * Spread existing shadow root options so we don't clobber `mode: 'open'`
58
+ * or any other options already set by a base class or another mixin.
59
+ * `override` is omitted: TypeScript cannot verify the static side of the
60
+ * generic `Base` constructor has `shadowRootOptions`, so we re-declare
61
+ * without override (the property is still inherited at runtime).
62
+ */ static shadowRootOptions = {
63
+ ...Base.shadowRootOptions,
64
+ delegatesFocus: true
65
+ };
66
+ /**
67
+ * The tabIndex to restore when transitioning from disabled → enabled.
68
+ * Snapshotted in connectedCallback and updated whenever we save before
69
+ * disabling, so we can honour custom consumer tabindex values (e.g. 2)
70
+ * rather than blindly restoring to 0.
71
+ */ _savedTabIndex = 0;
72
+ connectedCallback() {
73
+ super.connectedCallback();
74
+ // The host is the user-visible tab stop. Custom elements are NOT in the
75
+ // tab order by default (tabIndex = -1), so we must explicitly opt in.
76
+ // Only set the default if the consumer hasn't already specified a value.
77
+ // `tabIndex` is a reflected IDL attribute — both attribute sets and
78
+ // programmatic sets (`element.tabIndex = 2`) always reflect to the
79
+ // `tabindex` attribute, so `hasAttribute` is a complete guard for both.
80
+ // tabindex="-1" → remove from tab order entirely (e.g. inside a focus trap)
81
+ // tabindex="0" → participate (same as our default)
82
+ // tabindex="2" → explicit ordering position
83
+ // Note: connectedCallback is used (not constructor) to avoid the
84
+ // "DOMException: The result must not have attributes" error during upgrade.
85
+ if (!this.hasAttribute('tabindex')) {
86
+ this.tabIndex = 0;
87
+ }
88
+ // Snapshot the current effective tabIndex so _setHostFocusable(true)
89
+ // can restore it rather than blindly resetting to 0.
90
+ this._savedTabIndex = this.tabIndex;
91
+ }
92
+ /**
93
+ * Centralizes the tabIndex policy for enabled/disabled state.
94
+ *
95
+ * enabled=true → restore the tabIndex that was in effect before disabling
96
+ * (respects consumer tabindex="2", tabindex="-1", etc.)
97
+ * enabled=false → tabIndex = -1 (host skipped by Tab; whole component unreachable)
98
+ *
99
+ * The pre-disable tabIndex is saved so that a consumer who set tabindex="2"
100
+ * gets back tabindex="2" after re-enabling, not a hardcoded 0.
101
+ *
102
+ * All components with a `disabled` prop MUST call this in `updated()`:
103
+ *
104
+ * if (changed.has('disabled')) this._setHostFocusable(!this.disabled);
105
+ */ _setHostFocusable(enabled) {
106
+ if (enabled) {
107
+ this.tabIndex = this._savedTabIndex;
108
+ } else {
109
+ // Only snapshot when we are actually in an enabled state; if this is
110
+ // called repeatedly while disabled (tabIndex already -1) we must not
111
+ // overwrite the real saved value with -1.
112
+ if (this.tabIndex !== -1) {
113
+ this._savedTabIndex = this.tabIndex;
114
+ }
115
+ this.tabIndex = -1;
116
+ }
117
+ }
118
+ /**
119
+ * Subclasses MUST override this getter to return the specific inner element
120
+ * that should receive programmatic focus.
121
+ * Returning `null` before first render is safe — `focus()` no-ops.
122
+ */ get _focusableElement() {
123
+ return null;
124
+ }
125
+ /**
126
+ * Public focus() override.
127
+ *
128
+ * Explicitly delegates to `_focusableElement` when available. While `delegatesFocus: true`
129
+ * handles click routing, relying solely on native programmatic focus hands control
130
+ * to the browser, which blindly targets the *first* focusable element in
131
+ * shadow DOM order — not necessarily the intended one.
132
+ *
133
+ * Example: a vi-input might render a "Clear" <button> before the <input>
134
+ * in the DOM. Native focus would land on the clear button; this explicit
135
+ * call guarantees focus lands on the <input> regardless of DOM order.
136
+ *
137
+ * If called before the first render (when `_focusableElement` is null),
138
+ * it safely falls back to `super.focus()`.
139
+ */ focus(options) {
140
+ const target = this._focusableElement;
141
+ if (target) {
142
+ target.focus(options);
143
+ } else {
144
+ // Fallback to native behavior if called before first render
145
+ super.focus(options);
146
+ }
147
+ }
148
+ }
149
+ // Cast required: TypeScript cannot reconcile LitElement's private fields
150
+ // with the anonymous class return type. `as unknown as` is the standard
151
+ // pattern recommended by both the TypeScript and Lit teams for mixins.
152
+ return FocusableMixinClass;
153
+ }
154
+
155
+ export { FocusableMixin as F };
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Icon Registry
3
+ *
4
+ * A Map-based store for SvgIconDef objects sourced from @vialiq/icons.
5
+ * Icons must be explicitly registered before <vi-icon> can render them.
6
+ * Only the icons you register end up in your bundle — full tree-shaking.
7
+ *
8
+ * Usage:
9
+ * import { registerIcons } from '@vialiq/web-components';
10
+ * import { checkIcon } from '@vialiq/icons/check';
11
+ *
12
+ * registerIcons([checkIcon]);
13
+ * // <vi-icon name="check"></vi-icon>
14
+ */
15
+ export interface SvgIconDef {
16
+ name: string;
17
+ data: string;
18
+ }
19
+ /**
20
+ * Register one or more icons. Call this before using <vi-icon>.
21
+ *
22
+ * @param icons - Icon definitions from @vialiq/icons (trusted source only).
23
+ */
24
+ export declare function registerIcons(icons: SvgIconDef | SvgIconDef[]): void;
25
+ /**
26
+ * Look up a registered icon by name.
27
+ */
28
+ export declare function getIcon(name: string): SvgIconDef | undefined;
29
+ //# sourceMappingURL=registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../../../libs/web-components/src/icons/registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAyBD;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,UAAU,GAAG,UAAU,EAAE,GAAG,IAAI,CAMpE;AAED;;GAEG;AACH,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS,CAE5D"}
@@ -0,0 +1,47 @@
1
+ import { type PropertyValues, type TemplateResult } from 'lit';
2
+ import { ViElement } from '../base/vi-element.js';
3
+ /**
4
+ * vi-icon
5
+ *
6
+ * Renders a named SVG icon from the registry. Icons must be registered first
7
+ * by importing their definition from @vialiq/icons and calling registerIcons():
8
+ *
9
+ * import { checkIcon } from '@vialiq/icons/check';
10
+ * import { registerIcons } from '@vialiq/web-components';
11
+ * registerIcons([checkIcon]);
12
+ * // <vi-icon name="check"></vi-icon>
13
+ *
14
+ * @element vi-icon
15
+ * @attr name - The icon name to render (must be registered)
16
+ * @attr size - Width/height in px (default: 24)
17
+ * @attr label - Accessible label; omit for decorative icons
18
+ */
19
+ export declare class ViIcon extends ViElement {
20
+ static styles: import("lit").CSSResult;
21
+ /**
22
+ * The registered icon name to render.
23
+ * @attr
24
+ */
25
+ accessor name: string;
26
+ /**
27
+ * Size in pixels applied as a CSS custom property.
28
+ * @attr
29
+ */
30
+ accessor size: number;
31
+ /**
32
+ * Accessible label. When set the SVG gets role="img" + aria-label.
33
+ * When omitted the icon is treated as decorative (aria-hidden).
34
+ * @attr
35
+ */
36
+ accessor label: string;
37
+ private accessor _icon;
38
+ updated(changedProperties: PropertyValues): void;
39
+ firstUpdated(changedProperties: PropertyValues): void;
40
+ render(): TemplateResult;
41
+ }
42
+ declare global {
43
+ interface HTMLElementTagNameMap {
44
+ 'vi-icon': ViIcon;
45
+ }
46
+ }
47
+ //# sourceMappingURL=vi-icon.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vi-icon.d.ts","sourceRoot":"","sources":["../../../../libs/web-components/src/icons/vi-icon.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,KAAK,cAAc,EAAE,KAAK,cAAc,EAAE,MAAM,KAAK,CAAC;AAGnF,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAGlD;;;;;;;;;;;;;;;GAeG;AACH,qBACa,MAAO,SAAQ,SAAS;IACnC,OAAgB,MAAM,0BAkBpB;IAEF;;;OAGG;IACwC,QAAQ,CAAC,IAAI,SAAM;IAE9D;;;OAGG;IACyB,QAAQ,CAAC,IAAI,SAAM;IAE/C;;;;OAIG;IACyB,QAAQ,CAAC,KAAK,SAAM;IAEvC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAqC;IAE3D,OAAO,CAAC,iBAAiB,EAAE,cAAc,GAAG,IAAI;IAiBhD,YAAY,CAAC,iBAAiB,EAAE,cAAc,GAAG,IAAI;IAQrD,MAAM,IAAI,cAAc;CAoBlC;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,qBAAqB;QAC7B,SAAS,EAAE,MAAM,CAAC;KACnB;CACF"}
package/index.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ export { ViElement } from './base/vi-element.js';
2
+ export type { ViSize, ViStatus } from './base/vi-element.js';
3
+ export type { ControlStatus } from './base/validity-mixin.js';
4
+ export { ViButton } from './button/vi-button.js';
5
+ export type { ButtonVariant, ButtonSize, ButtonIconPlacement } from './button/vi-button.js';
6
+ export { ViInput } from './input/vi-input.js';
7
+ export type { InputType } from './input/vi-input.js';
8
+ export { ViIcon } from './icons/vi-icon.js';
9
+ export { registerIcons, getIcon } from './icons/registry.js';
10
+ export type { SvgIconDef } from './icons/registry.js';
11
+ //# sourceMappingURL=index.d.ts.map
package/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export { V as ViElement } from './vi-element-C6GfDPs3.js';
2
2
  export { ViButton } from './button/vi-button.js';
3
+ export { ViInput } from './input/vi-input.js';
3
4
  export { ViIcon } from './icons/vi-icon.js';
4
5
  export { getIcon, registerIcons } from './icons/registry.js';
@@ -0,0 +1,3 @@
1
+ export { ViInput } from './vi-input.js';
2
+ export type { InputType } from './vi-input.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../libs/web-components/src/input/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AACxC,YAAY,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC"}
@@ -0,0 +1,89 @@
1
+ import { type PropertyValues, type TemplateResult } from 'lit';
2
+ import { type ControlStatus } from '../base/validity-mixin.js';
3
+ import { ViElement } from '../base/vi-element.js';
4
+ /**
5
+ * Supported input types.
6
+ * Constrained to the subset that renders as a single-line text field —
7
+ * multi-line (textarea) and specialised pickers (date, color, file) are
8
+ * separate components.
9
+ */
10
+ export type InputType = 'text' | 'email' | 'password' | 'search' | 'tel' | 'url' | 'number';
11
+ declare const ViInput_base: typeof ViElement & (new (...args: any[]) => import("../base/focusable-mixin.js").FocusableInterface) & (new (...args: any[]) => import("../base/validity-mixin.js").ValidityInterface);
12
+ /**
13
+ * vi-input
14
+ * Form-associated single-line text input using Flux UI token fallbacks.
15
+ *
16
+ * @element vi-input
17
+ *
18
+ * @attr {InputType} type - Input type (default: text)
19
+ * @attr {string} placeholder - Native input placeholder
20
+ * @attr {string} name - Form field name
21
+ * @attr {string} value - Current value
22
+ * @attr {boolean} disabled - Disables the input (reflected)
23
+ * @attr {boolean} readonly - Makes the input read-only (reflected)
24
+ * @attr {boolean} required - Marks the field as required (reflected)
25
+ * @attr {ControlStatus} status - Visual state: 'default' | 'valid' | 'invalid' (reflected)
26
+ *
27
+ * @slot helper - Helper text shown below the input.
28
+ *
29
+ * @fires {CustomEvent<{value:string}>} vialiq-input - Every keystroke. Bubbles, composed.
30
+ * @fires {CustomEvent<{value:string}>} vialiq-change - Value committed (blur). Bubbles, composed.
31
+ * @fires {Event} invalid - Cancelable; fires when checkValidity() fails.
32
+ *
33
+ * @csspart field - The outer `<div>` wrapper
34
+ * @csspart input - The native `<input>` element
35
+ * @csspart helper - The persistent helper slot wrapper
36
+ * @csspart validation - The validation message span (error or success)
37
+ *
38
+ * @cssprop [--vi-input-border-color] - Border colour (default: `$color-border` token)
39
+ * @cssprop [--vi-input-focus-ring-color] - Focus ring colour (default: `$color-primary`)
40
+ * @cssprop [--vi-input-background-color] - Background (default: `$color-background`)
41
+ * @cssprop [--vi-input-text-color] - Text colour (default: `$color-foreground`)
42
+ * @cssprop [--vi-input-placeholder-color] - Placeholder colour (default: `$color-grey-400`)
43
+ * @cssprop [--vi-input-helper-color] - Helper text colour (default: `$color-grey-500`)
44
+ * @cssprop [--vi-input-error-color] - Error text colour (default: `$color-error`)
45
+ * @cssprop [--vi-input-success-color] - Success message colour (default: `$color-success`)
46
+ * @cssprop [--vi-input-shape-border-radius] - Border radius (default: `$border-radius-md`)
47
+ * @cssprop [--vi-input-spacing-padding-block] - Vertical padding (default: `$spacing-xs`)
48
+ * @cssprop [--vi-input-spacing-padding-inline] - Horizontal padding (default: `$spacing-sm`)
49
+ * @cssprop [--vi-input-typography-font-size] - Font size (default: `$font-size-base`)
50
+ */
51
+ export declare class ViInput extends ViInput_base {
52
+ static formAssociated: boolean;
53
+ static styles: import("lit").CSSResult;
54
+ protected readonly _internals: ElementInternals;
55
+ protected get _focusableElement(): HTMLInputElement | null;
56
+ accessor status: ControlStatus;
57
+ accessor required: boolean;
58
+ accessor validityMessage: string;
59
+ /** Input type. Controls the keyboard/picker on mobile and browser validation hints. */
60
+ accessor type: InputType;
61
+ /** Native input placeholder text. */
62
+ accessor placeholder: string;
63
+ /** Form field name. Submitted with the form when set. */
64
+ accessor name: string;
65
+ /** Current value. Synced to ElementInternals for form participation. */
66
+ accessor value: string;
67
+ /** When true, disables the input and removes it from the tab order. */
68
+ accessor disabled: boolean;
69
+ /** When true, the value cannot be edited but is still submitted. */
70
+ accessor readonly: boolean;
71
+ protected _testValidity(): Partial<ValidityStateFlags>;
72
+ updated(changed: PropertyValues): void;
73
+ /** Resets value and validation state when the associated form resets. */
74
+ formResetCallback(): void;
75
+ /** Keeps disabled in sync when a containing fieldset or form is disabled. */
76
+ formDisabledCallback(disabled: boolean): void;
77
+ private _onInput;
78
+ private _onChange;
79
+ private get _helperContent();
80
+ private get _validationMessage();
81
+ render(): TemplateResult;
82
+ }
83
+ declare global {
84
+ interface HTMLElementTagNameMap {
85
+ 'vi-input': ViInput;
86
+ }
87
+ }
88
+ export {};
89
+ //# sourceMappingURL=vi-input.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vi-input.d.ts","sourceRoot":"","sources":["../../../../libs/web-components/src/input/vi-input.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,cAAc,EACnB,KAAK,cAAc,EACpB,MAAM,KAAK,CAAC;AAGb,OAAO,EAAiB,KAAK,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC9E,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAIlD;;;;;GAKG;AACH,MAAM,MAAM,SAAS,GACjB,MAAM,GACN,OAAO,GACP,UAAU,GACV,QAAQ,GACR,KAAK,GACL,KAAK,GACL,QAAQ,CAAC;;AAEb;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,qBACa,OAAQ,SAAQ,YAAwC;IACnE,MAAM,CAAC,cAAc,UAAQ;IAC7B,OAAgB,MAAM,0BAEpB;IAEF,SAAS,CAAC,QAAQ,CAAC,UAAU,mBAA0B;IAEvD,cAAuB,iBAAiB,IAAI,gBAAgB,GAAG,IAAI,CAElE;IAI4B,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAa;IAC5B,QAAQ,CAAC,QAAQ,UAAS;IAC1D,QAAQ,CAAC,eAAe,SAAM;IAI1C,uFAAuF;IAC5C,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAU;IAE7E,qCAAqC;IACzB,QAAQ,CAAC,WAAW,SAAM;IAEtC,yDAAyD;IAC7C,QAAQ,CAAC,IAAI,SAAM;IAE/B,wEAAwE;IAC5D,QAAQ,CAAC,KAAK,SAAM;IAEhC,uEAAuE;IAC3B,QAAQ,CAAC,QAAQ,UAAS;IAEtE,oEAAoE;IACxB,QAAQ,CAAC,QAAQ,UAAS;IAOtE,SAAS,CAAC,aAAa,IAAI,OAAO,CAAC,kBAAkB,CAAC;IAmC7C,OAAO,CAAC,OAAO,EAAE,cAAc,GAAG,IAAI;IAU/C,yEAAyE;IACzE,iBAAiB,IAAI,IAAI;IAMzB,6EAA6E;IAC7E,oBAAoB,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI;IAM7C,OAAO,CAAC,QAAQ;IAahB,OAAO,CAAC,SAAS;IAejB,OAAO,KAAK,cAAc,GAIzB;IAED,OAAO,KAAK,kBAAkB,GAgB7B;IAEQ,MAAM,IAAI,cAAc;CAkClC;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,qBAAqB;QAC7B,UAAU,EAAE,OAAO,CAAC;KACrB;CACF"}