@sken-ds/primitives 0.4.6 → 0.5.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.
@@ -1 +1 @@
1
- {"version":3,"file":"sken-input.js","names":["#internalValue","#syncFormValue","#internals","#slotStartObserver","#slotEndObserver","#visibilityObserver","#adjustPaddings","#handleInput","#handleChange","#handleFocus","#handleBlur","#handleKeydown"],"sources":["../src/components/sken-input.ts"],"sourcesContent":["// ── <sken-input> — framework-ready Web Component ─────────────────────\n// Wraps a native <input> with token-driven styles. Controlled and\n// uncontrolled value, every standard <input> type, free form\n// integration. Requires @sken-ds/theme/css (ADR-0006).\n\nimport type { SkenInputSize, SkenInputType } from '@sken-ds/contracts'\nimport { LitElement, css, html } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\n\n@customElement('sken-input')\nexport class SkenInput extends LitElement {\n // Form-associated custom element: opt in to the ElementInternals\n // API so this primitive can participate in a surrounding\n // <form>'s submit/reset lifecycle. Without this flag, the\n // browser treats the host as an opaque element that does not\n // contribute to FormData and does not block Enter-to-submit\n // (the implicit submission rule for single text-like input\n // does not match against opaque elements). Lit docs:\n // https://lit.dev/docs/components/form/\n static formAssociated = true\n\n // The ElementInternals instance. Created in the constructor\n // via attachInternals(), which the browser only makes\n // available because `formAssociated` is true. We use it for:\n // - setFormValue() so the form picks up the current value.\n // - form reset (formResetCallback) for uncontrolled mode.\n // - reflecting `disabled` to the form's :disabled selector\n // (formDisabledCallback).\n // Nullable because some test environments (older happy-dom\n // versions, jsdom) do not implement ElementInternals. In\n // those environments form association is a no-op.\n #internals: ElementInternals | null = null\n\n constructor() {\n super()\n // attachInternals() is only callable when formAssociated is\n // true. The static flag is set above, so this resolves to a\n // real ElementInternals in any spec-compliant browser\n // (Chrome 77+, Firefox 98+, Safari 16.4+). If the host\n // environment does not implement it, the call throws — we\n // catch and degrade gracefully so SSR / tests that do not\n // need form association still work.\n try {\n this.#internals = this.attachInternals()\n } catch {\n this.#internals = null\n }\n }\n\n @property({ reflect: true }) type: SkenInputType = 'text'\n @property({ reflect: true }) size: SkenInputSize = 'md'\n @property() value: string | undefined = undefined\n @property({ attribute: 'default-value' }) defaultValue: string | undefined = undefined\n @property() placeholder: string | undefined = undefined\n @property({ reflect: true, type: Boolean }) disabled = false\n @property({ reflect: true, type: Boolean }) readonly = false\n @property({ reflect: true, type: Boolean }) required = false\n @property({ reflect: true, type: Boolean }) invalid = false\n @property({ attribute: 'described-by-id' }) describedById: string | undefined = undefined\n @property() name: string | undefined = undefined\n @property() autocomplete: AutoFill | undefined = undefined\n @property({ attribute: 'max-length', type: Number }) maxLength: number | undefined = undefined\n @property({ attribute: 'min-length', type: Number }) minLength: number | undefined = undefined\n @property() pattern: string | undefined = undefined\n\n // Internal value for uncontrolled mode. Seeded from\n // `defaultValue` on first connection; from then on the DOM\n // input keeps the source of truth.\n #internalValue: string = ''\n\n static styles = css`\n :host {\n display: inline-block;\n inline-size: 100%;\n }\n\n .input-wrapper {\n position: relative;\n display: flex;\n align-items: center;\n inline-size: 100%;\n block-size: 100%;\n }\n\n input {\n box-sizing: border-box;\n inline-size: 100%;\n border: 1px solid var(--sken-border);\n border-radius: var(--sken-md);\n background: var(--sken-input);\n color: var(--sken-foreground);\n font-family: var(--sken-family-sans);\n font-size: var(--sken-size-md);\n line-height: 1.25;\n text-overflow: ellipsis;\n padding-block: var(--sken-2);\n padding-inline: var(--sken-3);\n min-block-size: 2.25rem;\n transition:\n border-color 120ms ease,\n box-shadow 120ms ease;\n }\n\n input::placeholder {\n color: var(--sken-muted-foreground);\n opacity: 1; /* Firefox placeholder is <1 by default */\n }\n\n /* ── Slot containers ──────────────────────────────────────\n Position: absolute so the slotted content floats over the\n input without consuming flex space. The input's padding is\n auto-adjusted in JS to keep the value text from overlapping.\n pointer-events: none on the wrapper, auto on the slotted\n content, so only the visible icon/button receives clicks. */\n .slot {\n position: absolute;\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 1;\n pointer-events: none;\n color: var(--sken-muted-foreground);\n }\n\n .slot ::slotted(*) {\n pointer-events: auto;\n }\n\n .slot-start {\n inset-inline-start: 0.5rem;\n }\n\n .slot-end {\n inset-inline-end: 0.5rem;\n }\n\n /* ── Sizes ────────────────────────────────────────────────── */\n :host([size='sm']) input {\n padding-block: var(--sken-1);\n padding-inline: var(--sken-2);\n font-size: var(--sken-size-sm);\n min-block-size: 1.75rem;\n }\n :host([size='md']) input {\n /* Default styles above. */\n }\n :host([size='lg']) input {\n padding-block: var(--sken-3);\n padding-inline: var(--sken-4);\n font-size: var(--sken-size-lg);\n min-block-size: 2.75rem;\n }\n\n /* ── States ───────────────────────────────────────────────── */\n .input-wrapper:hover input:not(:disabled):not(:read-only) {\n border-color: var(--sken-foreground);\n }\n .input-wrapper:focus-within input {\n outline: 2px solid var(--sken-ring, currentColor);\n outline-offset: 2px;\n border-color: var(--sken-primary);\n }\n input:disabled {\n cursor: not-allowed;\n opacity: var(--sken-disabled);\n }\n input:read-only {\n background: var(--sken-muted);\n }\n :host([invalid]) input {\n border-color: var(--sken-destructive);\n }\n :host([invalid]) .input-wrapper:focus-within input {\n outline-color: var(--sken-destructive);\n }\n `\n\n override connectedCallback(): void {\n super.connectedCallback()\n if (this.value === undefined) {\n this.#internalValue = this.defaultValue ?? ''\n }\n // Publish the initial value to the surrounding form so that\n // FormData(form) and the implicit-submit-on-Enter rule see\n // the primitive as a real text input from the very first\n // render. Cheap (no allocation in the hot path after this).\n this.#syncFormValue()\n }\n\n override disconnectedCallback(): void {\n super.disconnectedCallback()\n this.#slotStartObserver?.disconnect()\n this.#slotEndObserver?.disconnect()\n this.#visibilityObserver?.disconnect()\n }\n\n // ── Form-association callbacks ─────────────────────────────────\n // Called by the browser when the form this primitive is\n // associated with is reset (either via form.reset() or by\n // clicking a <button type=\"reset\">). For uncontrolled mode we\n // restore the seed value; for controlled mode the consumer's\n // prop is the source of truth, so we re-render and let\n // Lit's render path re-publish the controlled value.\n formResetCallback(): void {\n if (this.value === undefined) {\n this.#internalValue = this.defaultValue ?? ''\n const inputEl = this.renderRoot.querySelector('input') as HTMLInputElement | null\n if (inputEl) inputEl.value = this.#internalValue\n this.#syncFormValue()\n } else {\n this.requestUpdate()\n }\n }\n\n // Called by the browser when the form's :disabled state\n // changes (e.g. a fieldset ancestor is disabled, or the\n // primitive gets associated with a disabled form). Mirrors\n // the `disabled` state onto the inner <input> so the visual\n // and the form submission are consistent.\n formDisabledCallback(isDisabled: boolean): void {\n // Reflect onto the prop so Lit re-renders and the\n // :host([disabled]) CSS rule kicks in. The inner <input>\n // picks up `?disabled=${this.disabled}` in the template.\n this.disabled = isDisabled\n }\n\n // Publish the current value to the form via ElementInternals.\n // Called on every change so FormData and submit/reset are\n // always in sync with the visible state.\n #syncFormValue(): void {\n if (!this.#internals) return\n const value = this.value ?? this.#internalValue\n // If the primitive has no `name`, FormData(form) skips it\n // anyway, so setFormValue is harmless but pointless. We\n // still call it to keep the implicit-submit rule consistent\n // (the spec checks the existence of a single text-like\n // submitter, not the name).\n this.#internals.setFormValue(value)\n }\n\n protected override firstUpdated(): void {\n const startEl = this.renderRoot.querySelector('.slot-start') as HTMLElement | null\n const endEl = this.renderRoot.querySelector('.slot-end') as HTMLElement | null\n const inputEl = this.renderRoot.querySelector('input') as HTMLInputElement | null\n if (!startEl || !endEl || !inputEl) return\n\n // MutationObserver on each slot container watches for\n // children being added/removed/attributes-changed. Cheap\n // because the subtree is just one or two nodes.\n this.#slotStartObserver = new MutationObserver(() => this.#adjustPaddings())\n this.#slotStartObserver.observe(startEl, { childList: true, subtree: true, attributes: true })\n this.#slotEndObserver = new MutationObserver(() => this.#adjustPaddings())\n this.#slotEndObserver.observe(endEl, { childList: true, subtree: true, attributes: true })\n\n // IntersectionObserver catches the case where the input is\n // hidden at first connect (e.g. inside a closed <details> or\n // an off-screen tab). The slot's bounding rect is 0 in that\n // case, so the padding would be wrong until the user opens\n // the parent. Re-measure when the host becomes visible.\n this.#visibilityObserver = new IntersectionObserver((entries) => {\n for (const entry of entries) {\n if (entry.isIntersecting) this.#adjustPaddings()\n }\n })\n this.#visibilityObserver.observe(this)\n\n // Also listen for the native slotchange event as a belt-and-\n // braces in case the MutationObserver misses something\n // (e.g. text node changes inside the slotted element).\n const startSlot = this.renderRoot.querySelector('slot[name=\"start\"]') as HTMLSlotElement | null\n const endSlot = this.renderRoot.querySelector('slot[name=\"end\"]') as HTMLSlotElement | null\n startSlot?.addEventListener('slotchange', this.#adjustPaddings)\n endSlot?.addEventListener('slotchange', this.#adjustPaddings)\n\n // First measurement.\n this.#adjustPaddings()\n }\n\n // The gap between a slot's edge and the input's text. iX uses\n // 0.5rem; we keep that because it balances the visual weight of\n // a 16px icon against the input's 0.75rem default padding-inline.\n // Module-scoped (not class-scoped) because TS forbids static\n // private fields on classes that use the legacy decorators.\n static SLOT_AIR = '0.5rem'\n\n // Two rAFs: the first lets Lit finish painting the slot, the\n // second lets the browser compute the slotted element's box.\n // Without the second, getBoundingClientRect() on a freshly-\n // assigned icon returns 0.\n #adjustPaddings = (): void => {\n requestAnimationFrame(() => {\n requestAnimationFrame(() => {\n const inputEl = this.renderRoot.querySelector('input') as HTMLInputElement | null\n if (!inputEl) return\n const startEl = this.renderRoot.querySelector('.slot-start') as HTMLElement | null\n const endEl = this.renderRoot.querySelector('.slot-end') as HTMLElement | null\n // iX rule: padding = slot width + half a rem of air.\n // If the slot is empty, reset to the CSS default (no\n // inline style).\n const startWidth = startEl?.getBoundingClientRect().width ?? 0\n const endWidth = endEl?.getBoundingClientRect().width ?? 0\n inputEl.style.paddingInlineStart =\n startWidth > 0 ? `calc(${startWidth}px + ${SkenInput.SLOT_AIR})` : ''\n inputEl.style.paddingInlineEnd =\n endWidth > 0 ? `calc(${endWidth}px + ${SkenInput.SLOT_AIR})` : ''\n })\n })\n }\n\n // Observer handles. Nullable because they are not created until\n // firstUpdated (which only runs in the browser, not during\n // server-side rendering or unit tests that don't mount).\n #slotStartObserver: MutationObserver | null = null\n #slotEndObserver: MutationObserver | null = null\n #visibilityObserver: IntersectionObserver | null = null\n\n protected override render() {\n const value = this.value ?? this.#internalValue\n return html`\n <div class=\"input-wrapper\" part=\"wrapper\">\n <div class=\"slot slot-start\" part=\"slot-start\">\n <slot name=\"start\"></slot>\n </div>\n <input\n part=\"input\"\n .value=${value}\n type=${this.type}\n placeholder=${this.placeholder ?? ''}\n ?disabled=${this.disabled}\n ?readonly=${this.readonly}\n ?required=${this.required}\n aria-invalid=${this.invalid ? 'true' : 'false'}\n aria-describedby=${this.describedById ?? ''}\n name=${this.name ?? ''}\n autocomplete=${this.autocomplete ?? 'off'}\n maxlength=${this.maxLength ?? ''}\n minlength=${this.minLength ?? ''}\n pattern=${this.pattern ?? ''}\n @input=${this.#handleInput}\n @change=${this.#handleChange}\n @focus=${this.#handleFocus}\n @blur=${this.#handleBlur}\n @keydown=${this.#handleKeydown}\n />\n <div class=\"slot slot-end\" part=\"slot-end\">\n <slot name=\"end\"></slot>\n </div>\n </div>\n `\n }\n\n // composed: true on every event so the framework adapter outside\n // the shadow boundary can listen.\n #handleInput = (event: Event) => {\n const target = event.target as HTMLInputElement\n const newValue = target.value\n if (this.value === undefined) this.#internalValue = newValue\n // Publish to the surrounding form so the new value is\n // included in FormData and submit-on-Enter sees the latest\n // text. Per-keystroke is fine: setFormValue is cheap.\n this.#syncFormValue()\n this.dispatchEvent(\n new CustomEvent<string>('sken-input', { detail: newValue, bubbles: true, composed: true }),\n )\n }\n\n #handleChange = (event: Event) => {\n const target = event.target as HTMLInputElement\n // Commit path: re-publish in case the browser mutated the\n // value (autofill, paste) without an intermediate input\n // event.\n this.#syncFormValue()\n this.dispatchEvent(\n new CustomEvent<string>('sken-change', {\n detail: target.value,\n bubbles: true,\n composed: true,\n }),\n )\n }\n\n #handleFocus = (event: FocusEvent) => {\n this.dispatchEvent(\n new CustomEvent<FocusEvent>('sken-focus', { detail: event, bubbles: true, composed: true }),\n )\n }\n\n #handleBlur = (event: FocusEvent) => {\n this.dispatchEvent(\n new CustomEvent<FocusEvent>('sken-blur', { detail: event, bubbles: true, composed: true }),\n )\n }\n\n // Wire Enter-on-the-inner-input to the host's associated form.\n //\n // The browser's implicit submission algorithm only sees the host\n // (the form-associated custom element), not the inner <input>\n // inside the shadow root. So pressing Enter in the inner input\n // reaches the form's submit algorithm via the host's keydown\n // listener but does NOT trigger a submit unless the form has a\n // default submit button. We bridge the shadow boundary here:\n // when the user presses Enter on the inner input, we call\n // `internals.form.requestSubmit()` ourselves. This dispatches\n // the form's submit event with the host as submitter, exactly\n // as if a native <input> had triggered it.\n //\n // We also `preventDefault()` so the browser's own handling of\n // Enter (which would otherwise insert a newline, in textarea,\n // or do nothing useful in input) does not double-dispatch.\n // `isComposing` guards against IME composition — pressing Enter\n // to confirm a kanji candidate should NOT submit the form.\n // `shiftKey` is allowed through for textareas (newline); single-\n // line inputs ignore it.\n #handleKeydown = (event: KeyboardEvent) => {\n if (event.key !== 'Enter' || event.isComposing) return\n if (this.disabled || this.readonly) return\n // preventDefault runs even if `internals.form` is null\n // (e.g. older test environments without ElementInternals\n // support, or the host is detached from a form). This keeps\n // the observable behaviour consistent: pressing Enter on\n // the inner input of a form-associated host is \"consumed\"\n // by the primitive, regardless of whether the surrounding\n // form machinery is available.\n event.preventDefault()\n const form = this.#internals?.form\n if (!form) return\n // requestSubmit() is the modern, non-deprecated way to\n // dispatch submit. It runs the form's submit handlers and\n // runs constraint validation. submitter defaults to the\n // form itself; consumers that need a specific submitter\n // can pass it explicitly.\n form.requestSubmit()\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'sken-input': SkenInput\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"sken-input.js","names":["#internalValue","#syncFormValue","#internals","#slotStartObserver","#slotEndObserver","#visibilityObserver","#adjustPaddings","#handleInput","#handleChange","#handleFocus","#handleBlur","#handleKeydown"],"sources":["../src/components/sken-input.ts"],"sourcesContent":["// ── <sken-input> — framework-ready Web Component ─────────────────────\n// Wraps a native <input> with token-driven styles. Controlled and\n// uncontrolled value, every standard <input> type, free form\n// integration. Requires @sken-ds/theme/css (ADR-0006).\n\nimport type { SkenInputSize, SkenInputType } from '@sken-ds/contracts'\nimport { LitElement, css, html } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\n\n@customElement('sken-input')\nexport class SkenInput extends LitElement {\n // Form-associated custom element: opt in to the ElementInternals\n // API so this primitive can participate in a surrounding\n // <form>'s submit/reset lifecycle. Without this flag, the\n // browser treats the host as an opaque element that does not\n // contribute to FormData and does not block Enter-to-submit\n // (the implicit submission rule for single text-like input\n // does not match against opaque elements). Lit docs:\n // https://lit.dev/docs/components/form/\n static formAssociated = true\n\n // The ElementInternals instance. Created in the constructor\n // via attachInternals(), which the browser only makes\n // available because `formAssociated` is true. We use it for:\n // - setFormValue() so the form picks up the current value.\n // - form reset (formResetCallback) for uncontrolled mode.\n // - reflecting `disabled` to the form's :disabled selector\n // (formDisabledCallback).\n // Nullable because some test environments (older happy-dom\n // versions, jsdom) do not implement ElementInternals. In\n // those environments form association is a no-op.\n #internals: ElementInternals | undefined = undefined\n\n constructor() {\n super()\n // attachInternals() is only callable when formAssociated is\n // true. The static flag is set above, so this resolves to a\n // real ElementInternals in any spec-compliant browser\n // (Chrome 77+, Firefox 98+, Safari 16.4+). If the host\n // environment does not implement it, the call throws — we\n // catch and degrade gracefully so SSR / tests that do not\n // need form association still work.\n try {\n this.#internals = this.attachInternals()\n } catch {\n this.#internals = undefined\n }\n }\n\n @property({ reflect: true }) type: SkenInputType = 'text'\n @property({ reflect: true }) size: SkenInputSize = 'md'\n @property() value: string | undefined = undefined\n @property({ attribute: 'default-value' }) defaultValue: string | undefined = undefined\n @property() placeholder: string | undefined = undefined\n @property({ reflect: true, type: Boolean }) disabled = false\n @property({ reflect: true, type: Boolean }) readonly = false\n @property({ reflect: true, type: Boolean }) required = false\n @property({ reflect: true, type: Boolean }) invalid = false\n @property({ attribute: 'described-by-id' }) describedById: string | undefined = undefined\n @property() name: string | undefined = undefined\n @property() autocomplete: AutoFill | undefined = undefined\n @property({ attribute: 'max-length', type: Number }) maxLength: number | undefined = undefined\n @property({ attribute: 'min-length', type: Number }) minLength: number | undefined = undefined\n @property() pattern: string | undefined = undefined\n\n // Internal value for uncontrolled mode. Seeded from\n // `defaultValue` on first connection; from then on the DOM\n // input keeps the source of truth.\n #internalValue: string = ''\n\n static styles = css`\n :host {\n display: inline-block;\n inline-size: 100%;\n }\n\n .input-wrapper {\n position: relative;\n display: flex;\n align-items: center;\n inline-size: 100%;\n block-size: 100%;\n }\n\n input {\n box-sizing: border-box;\n inline-size: 100%;\n border: 1px solid var(--sken-border);\n border-radius: var(--sken-md);\n background: var(--sken-input);\n color: var(--sken-foreground);\n font-family: var(--sken-family-sans);\n font-size: var(--sken-size-md);\n line-height: 1.25;\n text-overflow: ellipsis;\n padding-block: var(--sken-2);\n padding-inline: var(--sken-3);\n min-block-size: 2.25rem;\n transition:\n border-color 120ms ease,\n box-shadow 120ms ease;\n }\n\n input::placeholder {\n color: var(--sken-muted-foreground);\n opacity: 1; /* Firefox placeholder is <1 by default */\n }\n\n /* ── Slot containers ──────────────────────────────────────\n Position: absolute so the slotted content floats over the\n input without consuming flex space. The input's padding is\n auto-adjusted in JS to keep the value text from overlapping.\n pointer-events: none on the wrapper, auto on the slotted\n content, so only the visible icon/button receives clicks. */\n .slot {\n position: absolute;\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 1;\n pointer-events: none;\n color: var(--sken-muted-foreground);\n }\n\n .slot ::slotted(*) {\n pointer-events: auto;\n }\n\n .slot-start {\n inset-inline-start: 0.5rem;\n }\n\n .slot-end {\n inset-inline-end: 0.5rem;\n }\n\n /* ── Sizes ────────────────────────────────────────────────── */\n :host([size='sm']) input {\n padding-block: var(--sken-1);\n padding-inline: var(--sken-2);\n font-size: var(--sken-size-sm);\n min-block-size: 1.75rem;\n }\n :host([size='md']) input {\n /* Default styles above. */\n }\n :host([size='lg']) input {\n padding-block: var(--sken-3);\n padding-inline: var(--sken-4);\n font-size: var(--sken-size-lg);\n min-block-size: 2.75rem;\n }\n\n /* ── States ───────────────────────────────────────────────── */\n .input-wrapper:hover input:not(:disabled):not(:read-only) {\n border-color: var(--sken-foreground);\n }\n .input-wrapper:focus-within input {\n outline: 2px solid var(--sken-ring, currentColor);\n outline-offset: 2px;\n border-color: var(--sken-primary);\n }\n input:disabled {\n cursor: not-allowed;\n opacity: var(--sken-disabled);\n }\n input:read-only {\n background: var(--sken-muted);\n }\n :host([invalid]) input {\n border-color: var(--sken-destructive);\n }\n :host([invalid]) .input-wrapper:focus-within input {\n outline-color: var(--sken-destructive);\n }\n `\n\n override connectedCallback(): void {\n super.connectedCallback()\n if (this.value === undefined) {\n this.#internalValue = this.defaultValue ?? ''\n }\n // Publish the initial value to the surrounding form so that\n // FormData(form) and the implicit-submit-on-Enter rule see\n // the primitive as a real text input from the very first\n // render. Cheap (no allocation in the hot path after this).\n this.#syncFormValue()\n }\n\n override disconnectedCallback(): void {\n super.disconnectedCallback()\n this.#slotStartObserver?.disconnect()\n this.#slotEndObserver?.disconnect()\n this.#visibilityObserver?.disconnect()\n }\n\n // ── Form-association callbacks ─────────────────────────────────\n // Called by the browser when the form this primitive is\n // associated with is reset (either via form.reset() or by\n // clicking a <button type=\"reset\">). For uncontrolled mode we\n // restore the seed value; for controlled mode the consumer's\n // prop is the source of truth, so we re-render and let\n // Lit's render path re-publish the controlled value.\n formResetCallback(): void {\n if (this.value === undefined) {\n this.#internalValue = this.defaultValue ?? ''\n const inputEl = this.renderRoot.querySelector('input') as HTMLInputElement | null\n if (inputEl) inputEl.value = this.#internalValue\n this.#syncFormValue()\n } else {\n this.requestUpdate()\n }\n }\n\n // Called by the browser when the form's :disabled state\n // changes (e.g. a fieldset ancestor is disabled, or the\n // primitive gets associated with a disabled form). Mirrors\n // the `disabled` state onto the inner <input> so the visual\n // and the form submission are consistent.\n formDisabledCallback(isDisabled: boolean): void {\n // Reflect onto the prop so Lit re-renders and the\n // :host([disabled]) CSS rule kicks in. The inner <input>\n // picks up `?disabled=${this.disabled}` in the template.\n this.disabled = isDisabled\n }\n\n // Publish the current value to the form via ElementInternals.\n // Called on every change so FormData and submit/reset are\n // always in sync with the visible state.\n #syncFormValue(): void {\n if (!this.#internals) return\n const value = this.value ?? this.#internalValue\n // If the primitive has no `name`, FormData(form) skips it\n // anyway, so setFormValue is harmless but pointless. We\n // still call it to keep the implicit-submit rule consistent\n // (the spec checks the existence of a single text-like\n // submitter, not the name).\n this.#internals.setFormValue(value)\n }\n\n protected override firstUpdated(): void {\n const startEl = this.renderRoot.querySelector('.slot-start') as HTMLElement | null\n const endEl = this.renderRoot.querySelector('.slot-end') as HTMLElement | null\n const inputEl = this.renderRoot.querySelector('input') as HTMLInputElement | null\n if (!startEl || !endEl || !inputEl) return\n\n // MutationObserver on each slot container watches for\n // children being added/removed/attributes-changed. Cheap\n // because the subtree is just one or two nodes.\n this.#slotStartObserver = new MutationObserver(() => this.#adjustPaddings())\n this.#slotStartObserver.observe(startEl, { childList: true, subtree: true, attributes: true })\n this.#slotEndObserver = new MutationObserver(() => this.#adjustPaddings())\n this.#slotEndObserver.observe(endEl, { childList: true, subtree: true, attributes: true })\n\n // IntersectionObserver catches the case where the input is\n // hidden at first connect (e.g. inside a closed <details> or\n // an off-screen tab). The slot's bounding rect is 0 in that\n // case, so the padding would be wrong until the user opens\n // the parent. Re-measure when the host becomes visible.\n this.#visibilityObserver = new IntersectionObserver((entries) => {\n for (const entry of entries) {\n if (entry.isIntersecting) this.#adjustPaddings()\n }\n })\n this.#visibilityObserver.observe(this)\n\n // Also listen for the native slotchange event as a belt-and-\n // braces in case the MutationObserver misses something\n // (e.g. text node changes inside the slotted element).\n const startSlot = this.renderRoot.querySelector('slot[name=\"start\"]') as HTMLSlotElement | null\n const endSlot = this.renderRoot.querySelector('slot[name=\"end\"]') as HTMLSlotElement | null\n startSlot?.addEventListener('slotchange', this.#adjustPaddings)\n endSlot?.addEventListener('slotchange', this.#adjustPaddings)\n\n // First measurement.\n this.#adjustPaddings()\n }\n\n // The gap between a slot's edge and the input's text. iX uses\n // 0.5rem; we keep that because it balances the visual weight of\n // a 16px icon against the input's 0.75rem default padding-inline.\n // Module-scoped (not class-scoped) because TS forbids static\n // private fields on classes that use the legacy decorators.\n static SLOT_AIR = '0.5rem'\n\n // Two rAFs: the first lets Lit finish painting the slot, the\n // second lets the browser compute the slotted element's box.\n // Without the second, getBoundingClientRect() on a freshly-\n // assigned icon returns 0.\n #adjustPaddings = (): void => {\n requestAnimationFrame(() => {\n requestAnimationFrame(() => {\n const inputEl = this.renderRoot.querySelector('input') as HTMLInputElement | null\n if (!inputEl) return\n const startEl = this.renderRoot.querySelector('.slot-start') as HTMLElement | null\n const endEl = this.renderRoot.querySelector('.slot-end') as HTMLElement | null\n // iX rule: padding = slot width + half a rem of air.\n // If the slot is empty, reset to the CSS default (no\n // inline style).\n const startWidth = startEl?.getBoundingClientRect().width ?? 0\n const endWidth = endEl?.getBoundingClientRect().width ?? 0\n inputEl.style.paddingInlineStart =\n startWidth > 0 ? `calc(${startWidth}px + ${SkenInput.SLOT_AIR})` : ''\n inputEl.style.paddingInlineEnd =\n endWidth > 0 ? `calc(${endWidth}px + ${SkenInput.SLOT_AIR})` : ''\n })\n })\n }\n\n // Observer handles. Nullable because they are not created until\n // firstUpdated (which only runs in the browser, not during\n // server-side rendering or unit tests that don't mount).\n #slotStartObserver: MutationObserver | undefined = undefined\n #slotEndObserver: MutationObserver | undefined = undefined\n #visibilityObserver: IntersectionObserver | undefined = undefined\n\n protected override render() {\n const value = this.value ?? this.#internalValue\n return html`\n <div class=\"input-wrapper\" part=\"wrapper\">\n <div class=\"slot slot-start\" part=\"slot-start\">\n <slot name=\"start\"></slot>\n </div>\n <input\n part=\"input\"\n .value=${value}\n type=${this.type}\n placeholder=${this.placeholder ?? ''}\n ?disabled=${this.disabled}\n ?readonly=${this.readonly}\n ?required=${this.required}\n aria-invalid=${this.invalid ? 'true' : 'false'}\n aria-describedby=${this.describedById ?? ''}\n name=${this.name ?? ''}\n autocomplete=${this.autocomplete ?? 'off'}\n maxlength=${this.maxLength ?? ''}\n minlength=${this.minLength ?? ''}\n pattern=${this.pattern ?? ''}\n @input=${this.#handleInput}\n @change=${this.#handleChange}\n @focus=${this.#handleFocus}\n @blur=${this.#handleBlur}\n @keydown=${this.#handleKeydown}\n />\n <div class=\"slot slot-end\" part=\"slot-end\">\n <slot name=\"end\"></slot>\n </div>\n </div>\n `\n }\n\n // composed: true on every event so the framework adapter outside\n // the shadow boundary can listen.\n #handleInput = (event: Event) => {\n const target = event.target as HTMLInputElement\n const newValue = target.value\n if (this.value === undefined) this.#internalValue = newValue\n // Publish to the surrounding form so the new value is\n // included in FormData and submit-on-Enter sees the latest\n // text. Per-keystroke is fine: setFormValue is cheap.\n this.#syncFormValue()\n this.dispatchEvent(\n new CustomEvent<string>('sken-input', { detail: newValue, bubbles: true, composed: true }),\n )\n }\n\n #handleChange = (event: Event) => {\n const target = event.target as HTMLInputElement\n // Commit path: re-publish in case the browser mutated the\n // value (autofill, paste) without an intermediate input\n // event.\n this.#syncFormValue()\n this.dispatchEvent(\n new CustomEvent<string>('sken-change', {\n detail: target.value,\n bubbles: true,\n composed: true,\n }),\n )\n }\n\n #handleFocus = (event: FocusEvent) => {\n this.dispatchEvent(\n new CustomEvent<FocusEvent>('sken-focus', { detail: event, bubbles: true, composed: true }),\n )\n }\n\n #handleBlur = (event: FocusEvent) => {\n this.dispatchEvent(\n new CustomEvent<FocusEvent>('sken-blur', { detail: event, bubbles: true, composed: true }),\n )\n }\n\n // Wire Enter-on-the-inner-input to the host's associated form.\n //\n // The browser's implicit submission algorithm only sees the host\n // (the form-associated custom element), not the inner <input>\n // inside the shadow root. So pressing Enter in the inner input\n // reaches the form's submit algorithm via the host's keydown\n // listener but does NOT trigger a submit unless the form has a\n // default submit button. We bridge the shadow boundary here:\n // when the user presses Enter on the inner input, we call\n // `internals.form.requestSubmit()` ourselves. This dispatches\n // the form's submit event with the host as submitter, exactly\n // as if a native <input> had triggered it.\n //\n // We also `preventDefault()` so the browser's own handling of\n // Enter (which would otherwise insert a newline, in textarea,\n // or do nothing useful in input) does not double-dispatch.\n // `isComposing` guards against IME composition — pressing Enter\n // to confirm a kanji candidate should NOT submit the form.\n // `shiftKey` is allowed through for textareas (newline); single-\n // line inputs ignore it.\n #handleKeydown = (event: KeyboardEvent) => {\n if (event.key !== 'Enter' || event.isComposing) return\n if (this.disabled || this.readonly) return\n // preventDefault runs even if `internals.form` is null\n // (e.g. older test environments without ElementInternals\n // support, or the host is detached from a form). This keeps\n // the observable behaviour consistent: pressing Enter on\n // the inner input of a form-associated host is \"consumed\"\n // by the primitive, regardless of whether the surrounding\n // form machinery is available.\n event.preventDefault()\n const form = this.#internals?.form\n if (!form) return\n // requestSubmit() is the modern, non-deprecated way to\n // dispatch submit. It runs the form's submit handlers and\n // runs constraint validation. submitter defaults to the\n // form itself; consumers that need a specific submitter\n // can pass it explicitly.\n form.requestSubmit()\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'sken-input': SkenInput\n }\n}\n"],"mappings":"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"}
@@ -16,7 +16,7 @@ var c = {
16
16
  footer: "var(--sken-muted)"
17
17
  }, u = class extends t {
18
18
  constructor(...e) {
19
- super(...e), this.variant = "header-content", this.topbarMode = "fixed", this.sidebarPosition = "start", this.sidebarSize = "md", this.density = "none", this.#e = null, this.#n = this.#t, this.#i = [];
19
+ super(...e), this.variant = "header-content", this.topbarMode = "fixed", this.sidebarPosition = "start", this.sidebarSize = "md", this.density = "none", this.#e = void 0, this.#n = this.#t, this.#i = [];
20
20
  }
21
21
  #e;
22
22
  get #t() {
@@ -59,7 +59,7 @@ var c = {
59
59
  this.#i = [];
60
60
  }
61
61
  disconnectedCallback() {
62
- super.disconnectedCallback(), this.#e?.disconnect(), this.#e = null, this.#o();
62
+ super.disconnectedCallback(), this.#e?.disconnect(), this.#e = void 0, this.#o();
63
63
  }
64
64
  #s() {
65
65
  for (let e of Array.from(this.children)) {
@@ -1 +1 @@
1
- {"version":3,"file":"sken-layout.js","names":["#activeRoles","#visibleRoles","#syncRegionSlotAttributes","#regionSlotObserver","#recomputeVisibleRoles","#wireSlotChangeListeners","#unwireSlotChangeListeners","#slotChangeHandlers"],"sources":["../src/components/sken-layout.ts"],"sourcesContent":["// ── <sken-layout> — structural layout primitive ────────────────────\n// Defines the page-level structure of a multi-region interface\n// (topbar, primary nav, sidebar, content, footer) without imposing\n// what lives inside each region. Composition by contract:\n//\n// <sken-layout variant=\"header-sidebar\">\n// <sken-layout-region name=\"topbar\">…</sken-layout-region>\n// <sken-layout-region name=\"primary-nav\">…</sken-layout-region>\n// <sken-layout-region name=\"sidebar\">…</sken-layout-region>\n// <sken-layout-region name=\"content\">…</sken-layout-region>\n// </sken-layout>\n//\n// Design notes (v0.4.2 — fix for SKEN-LAYOUT-EMPTY-REGIONS.md):\n// - The shadow root projects the consumer's light-DOM regions via\n// NAMED slots, one per role. Each named slot sits inside a wrapper\n// element (e.g. <div class=\"topbar\">…</div>) that is the actual\n// grid item of the host's CSS grid.\n// - Roles the active variant does not include are not rendered at\n// all (the variant drives which slots exist in the shadow root).\n// - Roles the active variant includes but the consumer did not\n// fill are also not rendered. The host's `grid-template-areas`\n// is computed dynamically to exclude the empty rows/columns, so\n// the grid re-flows to fill the available space. This is the\n// third in a series of layout-rendering fixes:\n// - 0.4.0: variant-aware region rendering (only roles the\n// variant includes are emitted).\n// - 0.4.1: slot= attribute on the regions, so the browser\n// projects them to the named slots.\n// - 0.4.2 (this): content-aware grid template, so empty\n// rows/columns collapse.\n// - We detect which slots have content via `slot.assignedElements()`\n// on first render (after the browser has projected the\n// consumer's regions) and on every `slotchange` event (for\n// dynamic content — e.g. a sidebar the consumer can empty at\n// runtime). The result is stored in `#visibleRoles`, which\n// drives both the shadow template and the host's inline\n// `grid-template-areas` style.\n//\n// See ADR-0006 for the substrate decision. See the\n// @sken-ds/contracts types `SkenLayoutProps`, `SkenLayoutSlots`,\n// `SkenLayoutVariant`, `SkenLayoutVariantRoles` for the contract\n// surface.\n\nimport type {\n SkenLayoutDensity,\n SkenLayoutRegionRole,\n SkenLayoutSidebarPosition,\n SkenLayoutSidebarSize,\n SkenLayoutTopbarMode,\n SkenLayoutVariant,\n SkenLayoutVariantRolesMap,\n} from '@sken-ds/contracts'\nimport { SkenLayoutVariantRoles } from '@sken-ds/contracts'\nimport { LitElement, css, html, unsafeCSS, type PropertyValues } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\nimport './sken-layout-region.js'\n\n/**\n * Sidebar width in `rem` per size concept. The product can\n * override with `--sken-layout-sidebar-width` (CSS custom\n * property) if it needs a different value.\n */\nconst SIDEBAR_WIDTH: Record<SkenLayoutSidebarSize, string> = {\n sm: '12rem',\n md: '16rem',\n lg: '20rem',\n}\n\n/**\n * Per-cell background tokens. The values reference the\n * Sken color tokens, not raw colors, so a theme switch\n * (`data-theme=\"dark\"`) recolors every cell without the\n * primitive knowing about it.\n *\n * Mapping rationale:\n * - topbar is on `--sken-card` (white) because it is the\n * \"elevated\" chrome; the border-bottom separates it from\n * the page below.\n * - primary-nav is on `--sken-background` (page colour) so\n * it sits flush with the page; the border-bottom separates\n * it from the workspace.\n * - sidebar is on `--sken-muted` (subtle off-white) with a\n * border-inline-end; it is a \"rail\", not a \"page\".\n * - content is on `--sken-background`; no border (the\n * workspace is unbounded).\n * - footer is on `--sken-muted` with a border-top; it is\n * the same role as the sidebar (chrome).\n */\nconst REGION_BACKGROUND: Record<SkenLayoutRegionRole, string> = {\n topbar: 'var(--sken-card)',\n 'primary-nav': 'var(--sken-background)',\n sidebar: 'var(--sken-muted)',\n content: 'var(--sken-background)',\n footer: 'var(--sken-muted)',\n}\n\n@customElement('sken-layout')\nexport class SkenLayout extends LitElement {\n @property({ reflect: true }) variant: SkenLayoutVariant = 'header-content'\n @property({ attribute: 'topbar-mode', reflect: true }) topbarMode: SkenLayoutTopbarMode = 'fixed'\n @property({ attribute: 'sidebar-position', reflect: true })\n sidebarPosition: SkenLayoutSidebarPosition = 'start'\n @property({ attribute: 'sidebar-size', reflect: true }) sidebarSize: SkenLayoutSidebarSize = 'md'\n @property({ reflect: true }) density: SkenLayoutDensity = 'none'\n\n /**\n * MutationObserver that watches the host's light DOM for\n * `<sken-layout-region>` children. For each region, if the\n * consumer (or a framework adapter) did not set the HTML\n * `slot=\"<role>\"` attribute, we set it ourselves. This is\n * defense-in-depth: the host's shadow root uses 5 named\n * `<slot>` elements (one per role) and the browser's\n * slot-projection mechanism only projects a light-DOM child\n * into a named slot if the child carries the matching\n * `slot=\"<name>\"` attribute. Without it, every named slot\n * receives zero assigned elements and the regions render\n * as empty wrappers — the page looks almost empty.\n *\n * A future framework adapter (React, Svelte, plain HTML) can\n * either set the `slot` attribute on its own (the documented\n * contract) or rely on this observer to do it. Setting it\n * explicitly is still preferred for clarity, but the\n * observer is the safety net that prevents the silent\n * \"everything renders empty\" failure mode that v0.4.0\n * shipped with.\n */\n #regionSlotObserver: MutationObserver | null = null\n\n /**\n * Roles the active variant renders in the shadow root. Sourced\n * from {@link SkenLayoutVariantRoles}. Used to drive the\n * template (only the slots the variant includes are projected)\n * and to apply the per-variant `with-sidebar` class on the\n * body sub-grid wrapper.\n */\n get #activeRoles(): readonly SkenLayoutRegionRole[] {\n return (SkenLayoutVariantRoles as SkenLayoutVariantRolesMap)[this.variant]\n }\n\n /**\n * Roles the host grid should render in the shadow root AND\n * reserve space for in the grid template. This is the\n * intersection of the active variant's roles and the roles\n * whose slot actually has content (i.e. the consumer filled\n * the corresponding `<sken-layout-region>`). When a role is\n * in the variant but the consumer did not fill the slot, the\n * role is excluded — the wrapper is not rendered, and the\n * corresponding row/column in `grid-template-areas` is removed\n * so the surrounding regions re-flow.\n *\n * Updated on `firstUpdated` (after the browser has projected\n * the initial set of regions into the named slots) and on\n * every `slotchange` event (for dynamic content).\n */\n #visibleRoles: readonly SkenLayoutRegionRole[] = this.#activeRoles\n\n /**\n * Compute the current set of visible roles by walking the\n * host's light-DOM children and checking which ones carry\n * a `slot=\"<role>\"` attribute that matches one of the active\n * variant's roles.\n *\n * We deliberately do NOT use `slot.assignedElements()` (which\n * requires the browser to have done the slot projection and\n * is therefore timing-sensitive in shadow-DOM tests) — we\n * read the slot attribute directly on the children. This is\n * observable at any point, including during the first\n * `firstUpdated` call.\n *\n * The `#syncRegionSlotAttributes` method (called from\n * `connectedCallback` and again in `firstUpdated`) ensures\n * that the slot attribute is in place on every child region\n * before this method runs. The slotchange listener is\n * responsible for re-running this method when the consumer\n * mutates the slot attribute at runtime.\n */\n #recomputeVisibleRoles(): void {\n const visible: SkenLayoutRegionRole[] = []\n for (const role of this.#activeRoles) {\n // A role is \"filled\" if at least one direct child of\n // the host has slot=\"<role>\". We do not care about the\n // child's own children (an empty region is still a\n // filled region — the consumer can render a placeholder\n // inside an otherwise empty slot).\n const filled = Array.from(this.children).some(\n (c) => c.getAttribute && c.getAttribute('slot') === role,\n )\n if (filled) visible.push(role)\n }\n // Avoid re-renders if nothing changed.\n if (\n visible.length === this.#visibleRoles.length &&\n visible.every((r, i) => r === this.#visibleRoles[i])\n ) {\n return\n }\n this.#visibleRoles = visible\n this.requestUpdate()\n }\n\n override connectedCallback(): void {\n super.connectedCallback()\n // Walk the current children synchronously (so the first\n // render after connection already has the slot= attribute\n // in place), then attach the observer to catch future\n // mutations (the consumer can add or remove regions\n // dynamically, e.g. when switching the active variant in\n // a shared layout component).\n //\n // The observer also re-runs `#recomputeVisibleRoles`. The\n // MutationObserver fires on every add/remove of a direct\n // child, which is exactly the signal we need to refresh\n // the visible-roles set (the slotchange event is a poor\n // substitute because it only fires for slots that already\n // exist in the shadow root — a region the consumer adds\n // for a slot that the previous render omitted will not\n // fire slotchange because the slot does not exist yet).\n this.#syncRegionSlotAttributes()\n this.#regionSlotObserver = new MutationObserver(() => {\n this.#syncRegionSlotAttributes()\n this.#recomputeVisibleRoles()\n })\n this.#regionSlotObserver.observe(this, { childList: true, subtree: false })\n }\n\n /**\n * Lit lifecycle hook called after the first render. Three\n * things happen here, in order:\n *\n * 1. `#syncRegionSlotAttributes()` — second-pass sync point\n * for regions whose `name` was set as a property (not as\n * an attribute) before the region was connected. The\n * `name` property is `reflect: true`, so the attribute is\n * written only after the region is connected and Lit runs\n * its first render. Without this second pass, a region\n * whose `name` was set as a property (not as an attribute)\n * would not have its `slot` attribute set by the time the\n * browser tries to project it.\n *\n * 2. `#recomputeVisibleRoles()` — first pass at discovering\n * which slots have content. This MUST happen after the\n * browser has done its first slot projection, which is\n * exactly what `firstUpdated` is. Without this, the\n * initial render would assume all variant roles are\n * visible, and the grid template would reserve rows for\n * empty regions. The render that follows this call\n * produces the correct (collapsed) grid template.\n *\n * 3. Wire up `slotchange` listeners on the named slots, so\n * dynamic content (a consumer emptying or filling a slot\n * at runtime) re-triggers the same re-computation.\n */\n protected override firstUpdated(_changedProperties: PropertyValues): void {\n super.firstUpdated(_changedProperties)\n this.#syncRegionSlotAttributes()\n this.#recomputeVisibleRoles()\n this.#wireSlotChangeListeners()\n }\n\n /**\n * Lit lifecycle hook called after every render (including the\n * first). We re-run `#recomputeVisibleRoles` when the variant\n * changes because the set of valid roles (`#activeRoles`)\n * depends on the variant, and a region that was visible under\n * the previous variant may no longer be a member of the new\n * variant. Without this, switching the variant prop would\n * leave stale roles in `#visibleRoles` until the next\n * `slotchange` event.\n *\n * We do NOT call `requestUpdate` here — `#recomputeVisibleRoles`\n * already does that when the visible set actually changed. If\n * the set did not change, no extra render is needed.\n */\n protected override updated(_changedProperties: PropertyValues): void {\n super.updated(_changedProperties)\n if (_changedProperties.has('variant')) {\n this.#recomputeVisibleRoles()\n }\n }\n\n /**\n * Attach a `slotchange` listener to every named slot in the\n * shadow root. The browser fires `slotchange` on a slot\n * whenever the set of light-DOM children projected into it\n * changes (add, remove, or replace). This is a defense in\n * depth alongside the `MutationObserver` (which is the\n * primary signal for adds/removes): the slotchange listener\n * catches a consumer who replaces a region's content with\n * another consumer-defined element while keeping the same\n * `slot=\"<role>\"` attribute (the MutationObserver on\n * childList would not fire for a same-tag-name replacement\n * that swaps the element in place, though in practice the\n * browser usually fires childList anyway for new nodes).\n *\n * The listeners are attached in `firstUpdated` (so the slots\n * exist) and torn down in `disconnectedCallback`.\n */\n #slotChangeHandlers: Array<{ slot: HTMLSlotElement; handler: () => void }> = []\n\n #wireSlotChangeListeners(): void {\n // Tear down the previous batch first (in case the variant\n // changed and the shadow root was re-rendered, replacing\n // the old slot elements with new ones).\n this.#unwireSlotChangeListeners()\n const shadow = this.shadowRoot\n if (!shadow) return\n for (const slot of Array.from(shadow.querySelectorAll('slot'))) {\n const handler = () => this.#recomputeVisibleRoles()\n slot.addEventListener('slotchange', handler)\n this.#slotChangeHandlers.push({ slot, handler })\n }\n }\n\n #unwireSlotChangeListeners(): void {\n for (const { slot, handler } of this.#slotChangeHandlers) {\n slot.removeEventListener('slotchange', handler)\n }\n this.#slotChangeHandlers = []\n }\n\n override disconnectedCallback(): void {\n super.disconnectedCallback()\n this.#regionSlotObserver?.disconnect()\n this.#regionSlotObserver = null\n this.#unwireSlotChangeListeners()\n }\n\n /**\n * For each direct `<sken-layout-region>` child of the host,\n * set the `slot=\"<name>\"` HTML attribute if it is missing\n * or does not match the region's role. The browser's\n * slot-projection mechanism reads this attribute to project\n * the child into the matching named `<slot>` inside the\n * shadow root.\n *\n * We only touch direct children (not the entire subtree)\n * because the contract is that `<sken-layout-region>` lives\n * as a direct child of `<sken-layout>`; nested regions\n * are not a supported pattern.\n */\n #syncRegionSlotAttributes(): void {\n for (const child of Array.from(this.children)) {\n if (!(child instanceof HTMLElement)) continue\n if (child.tagName.toLowerCase() !== 'sken-layout-region') continue\n const role = child.getAttribute('name')\n if (!role) continue\n if (child.getAttribute('slot') === role) continue\n child.setAttribute('slot', role)\n }\n }\n\n static styles = css`\n /* ── The host ────────────────────────────────────────────────\n The host owns the page-level grid. The topbar / primary-nav\n / footer sit in their own rows; the body is a sub-grid that\n holds the sidebar (when present) and the content. */\n :host {\n display: block;\n block-size: 100dvh;\n background: var(--sken-background, Canvas);\n color: var(--sken-foreground, CanvasText);\n font-family: var(--sken-family-sans, system-ui, sans-serif);\n }\n\n /* The actual grid is a child <div class=\"grid\">. We use a\n child rather than the host itself for two reasons:\n\n 1. The grid template is computed per-render based on\n which roles have content (see #visibleRoles and\n render()). Setting grid-template-areas on the host\n via an inline style would work, but the host has a\n handful of other responsibilities (background, color,\n font) and keeping the grid in a child makes the\n separation cleaner.\n\n 2. CSS grid in shadow DOM: putting the grid on a child\n avoids any quirks of the host element's display\n behaviour (the host is a custom element with its own\n display semantics). */\n .grid {\n display: grid;\n block-size: 100%;\n grid-template-columns: 1fr;\n /* The grid template is set per-render via inline style\n using the --sken-layout-grid-areas and\n --sken-layout-grid-rows custom properties (set by the\n render() function). The default values below match\n the header-content variant; the inline style overrides\n them for variants with more or fewer roles, and for\n consumers that have unfilled regions. */\n grid-template-areas: var(--sken-layout-grid-areas, 'topbar' 'body');\n grid-template-rows: var(--sken-layout-grid-rows, 'auto 1fr');\n }\n\n /* ── Per-cell grid items ─────────────────────────────────────\n Each named slot sits inside a wrapper element that is the\n actual grid item. The wrapper's class matches the role\n (topbar, primary-nav, body, footer). The variant rules\n below assign the right 'grid-area' to each role. The\n 'display: contents' <sken-layout-region> is projected\n through the named <slot> and inherits the wrapper's\n background, border, and padding. */\n .topbar {\n grid-area: topbar;\n background: var(\n --sken-layout-region-topbar-background,\n ${unsafeCSS(REGION_BACKGROUND.topbar)}\n );\n padding-block: var(--sken-layout-region-topbar-padding-block, 0);\n padding-inline: var(--sken-layout-region-topbar-padding-inline, 0);\n border-bottom: 1px solid var(--sken-border);\n }\n .primary-nav {\n grid-area: primary-nav;\n background: var(\n --sken-layout-region-primary-nav-background,\n ${unsafeCSS(REGION_BACKGROUND['primary-nav'])}\n );\n padding-block: var(--sken-layout-region-primary-nav-padding-block, 0);\n padding-inline: var(--sken-layout-region-primary-nav-padding-inline, 0);\n border-bottom: 1px solid var(--sken-border);\n }\n .body {\n grid-area: body;\n display: grid;\n grid-template-columns: 1fr;\n grid-template-areas: 'content';\n min-block-size: 0; /* allow children to shrink + scroll */\n }\n .body.with-sidebar {\n grid-template-columns: var(--sken-layout-sidebar-width, 16rem) 1fr;\n grid-template-areas: 'sidebar content';\n }\n .body.with-sidebar.sidebar-end {\n grid-template-columns: 1fr var(--sken-layout-sidebar-width, 16rem);\n grid-template-areas: 'content sidebar';\n }\n .sidebar {\n grid-area: sidebar;\n min-block-size: 0;\n background: var(\n --sken-layout-region-sidebar-background,\n ${unsafeCSS(REGION_BACKGROUND.sidebar)}\n );\n padding-block: var(--sken-layout-region-sidebar-padding-block, 0);\n padding-inline: var(--sken-layout-region-sidebar-padding-inline, 0);\n border-inline-end: 1px solid var(--sken-border);\n }\n .content {\n min-block-size: 0;\n background: var(\n --sken-layout-region-content-background,\n ${unsafeCSS(REGION_BACKGROUND.content)}\n );\n padding-block: var(--sken-layout-region-content-padding-block, 0);\n padding-inline: var(--sken-layout-region-content-padding-inline, 0);\n }\n .footer {\n grid-area: footer;\n background: var(\n --sken-layout-region-footer-background,\n ${unsafeCSS(REGION_BACKGROUND.footer)}\n );\n padding-block: var(--sken-layout-region-footer-padding-block, 0);\n padding-inline: var(--sken-layout-region-footer-padding-inline, 0);\n border-top: 1px solid var(--sken-border);\n }\n\n /* ── Density: comfortable ─────────────────────────────────────\n When density=comfortable, the layout applies the Sken\n default rhythm: 8/16px on chrome cells, 16/24px on the\n content area, 12/16px on the footer. The values come\n from the spacing scale (--sken-2, --sken-3, --sken-4,\n --sken-6) so a single token change ripples through\n every cell. A consumer override on any\n --sken-layout-region-{role}-padding-{block|inline}\n always wins over the density preset. */\n :host([density='comfortable']) .topbar,\n :host([density='comfortable']) .primary-nav,\n :host([density='comfortable']) .content,\n :host([density='comfortable']) .sidebar,\n :host([density='comfortable']) .footer {\n --sken-layout-region-topbar-padding-block: var(--sken-2);\n --sken-layout-region-topbar-padding-inline: var(--sken-4);\n --sken-layout-region-primary-nav-padding-block: var(--sken-2);\n --sken-layout-region-primary-nav-padding-inline: var(--sken-4);\n --sken-layout-region-sidebar-padding-block: var(--sken-4);\n --sken-layout-region-sidebar-padding-inline: var(--sken-4);\n --sken-layout-region-content-padding-block: var(--sken-6);\n --sken-layout-region-content-padding-inline: var(--sken-6);\n --sken-layout-region-footer-padding-block: var(--sken-3);\n --sken-layout-region-footer-padding-inline: var(--sken-4);\n }\n\n /* ── Density: compact ────────────────────────────────────────\n When density=compact, every per-cell padding is halved\n from the comfortable value. We use calc(var(--sken-N) *\n 0.5) instead of hardcoded half-values so a future\n token change automatically cascades. As with\n comfortable, a consumer override on any\n --sken-layout-region-{role}-padding-{block|inline}\n always wins. */\n :host([density='compact']) .topbar,\n :host([density='compact']) .primary-nav,\n :host([density='compact']) .content,\n :host([density='compact']) .sidebar,\n :host([density='compact']) .footer {\n --sken-layout-region-topbar-padding-block: calc(var(--sken-2) * 0.5);\n --sken-layout-region-topbar-padding-inline: calc(var(--sken-4) * 0.5);\n --sken-layout-region-primary-nav-padding-block: calc(var(--sken-2) * 0.5);\n --sken-layout-region-primary-nav-padding-inline: calc(var(--sken-4) * 0.5);\n --sken-layout-region-sidebar-padding-block: calc(var(--sken-4) * 0.5);\n --sken-layout-region-sidebar-padding-inline: calc(var(--sken-4) * 0.5);\n --sken-layout-region-content-padding-block: calc(var(--sken-6) * 0.5);\n --sken-layout-region-content-padding-inline: calc(var(--sken-6) * 0.5);\n --sken-layout-region-footer-padding-block: calc(var(--sken-3) * 0.5);\n --sken-layout-region-footer-padding-inline: calc(var(--sken-4) * 0.5);\n }\n\n /* ── Grid templates per variant ───────────────────────────────\n The grid template is set per-render as an inline style\n (via --sken-layout-grid-areas and\n --sken-layout-grid-rows) based on the visible roles (the\n intersection of the active variant and the roles whose\n slot has content). See render() for the computation.\n The default values on the .grid selector above cover\n header-content (the default variant) when no inline\n style is set (e.g. before the first render completes). */\n\n /* ── Topbar mode ──────────────────────────────────────────────\n 'fixed': the host is 100dvh and the body scrolls under the\n topbar. This is the default for admin consoles.\n 'auto': the topbar scrolls away with the page (marketing,\n landing). */\n :host([topbar-mode='auto']) {\n block-size: auto;\n min-block-size: 100dvh;\n }\n\n /* ── Sidebar position ─────────────────────────────────────────\n When the sidebar sits on the inline-end side, the border\n moves to the inline-start side. The default rule above\n already paints the border on the inline-end; here we\n swap it. */\n :host([sidebar-position='end']) .sidebar {\n border-inline-end: none;\n border-inline-start: 1px solid var(--sken-border);\n }\n\n /* ── Sidebar width override ─────────────────────────────────── */\n :host([sidebar-size='sm']) {\n --sken-layout-sidebar-width: ${unsafeCSS(SIDEBAR_WIDTH.sm)};\n }\n :host([sidebar-size='md']) {\n --sken-layout-sidebar-width: ${unsafeCSS(SIDEBAR_WIDTH.md)};\n }\n :host([sidebar-size='lg']) {\n --sken-layout-sidebar-width: ${unsafeCSS(SIDEBAR_WIDTH.lg)};\n }\n `\n\n override render() {\n // Use #visibleRoles (not #activeRoles) so empty regions\n // are not emitted. #visibleRoles is the active variant\n // intersected with the roles whose slot has content; it\n // is updated by firstUpdated and slotchange handlers.\n const visibleRoles = this.#visibleRoles\n const hasContent = visibleRoles.includes('content')\n const hasSidebar = visibleRoles.includes('sidebar')\n\n // ── Compute the host's grid template ───────────────────────\n // We declare the area names in the order they should appear\n // in the grid (top to bottom). The browser uses this\n // template to lay out the wrappers: each wrapper has\n // `grid-area: <role>`, and the template maps that area to a\n // row. Excluded roles do not appear in the template, and\n // the corresponding wrapper is not rendered, so the row\n // simply does not exist.\n //\n // The order is the same as #activeRoles (which is the\n // order from SkenLayoutVariantRoles). That order is the\n // structural shape of the variant — the consumer picks the\n // variant, we project the order.\n //\n // For the row that holds the main content, the area name\n // is \"body\" (because the wrapper that owns that row is the\n // .body sub-grid, not a .content div). The SSoT for\n // \"which row is the body\" is \"the row whose role is\n // 'content'\" — the wrapper .body is rendered whenever\n // 'content' is visible, and it lives in the row we name\n // \"body\" in the template. This keeps the wrapper\n // structure (one .body that holds sidebar+content) in sync\n // with the grid template (one row named \"body\" that\n // stretches to fill the remaining space).\n const gridTemplateAreas = visibleRoles\n .map((r) => (r === 'content' ? '\"body\"' : `\"${r}\"`))\n .join('\\n')\n\n // Compute grid-template-rows to match. We need one row per\n // visible role. The \"1fr\" row is the body (it stretches to\n // fill the remaining space); the others are \"auto\" (they\n // take the height of their content). The body row is the\n // one whose role in #visibleRoles is 'content'.\n const gridTemplateRows = visibleRoles.map((r) => (r === 'content' ? '1fr' : 'auto')).join(' ')\n\n const bodyClasses = [\n 'body',\n hasSidebar ? 'with-sidebar' : '',\n hasSidebar && this.sidebarPosition === 'end' ? 'sidebar-end' : '',\n ]\n .filter(Boolean)\n .join(' ')\n\n // The grid template is applied as an inline style because\n // it is computed per render (the visible roles change when\n // the consumer adds or removes regions). The CSS in\n // `static styles` sets the default per-variant template, but\n // the inline style overrides it when the visible set\n // differs from the variant's full set.\n const hostStyle =\n `--sken-layout-grid-areas: ${gridTemplateAreas}; ` +\n `--sken-layout-grid-rows: ${gridTemplateRows};`\n\n return html`\n <div class=\"grid\" style=${hostStyle} part=\"grid\">\n ${\n visibleRoles.includes('topbar')\n ? html`<div class=\"topbar\" part=\"topbar\">\n <slot name=\"topbar\"></slot>\n </div>`\n : ''\n }\n ${\n visibleRoles.includes('primary-nav')\n ? html`<div class=\"primary-nav\" part=\"primary-nav\">\n <slot name=\"primary-nav\"></slot>\n </div>`\n : ''\n }\n ${\n hasContent\n ? html`<div class=${bodyClasses} part=\"body\">\n ${\n hasSidebar\n ? html`<div class=\"sidebar\" part=\"sidebar\">\n <slot name=\"sidebar\"></slot>\n </div>`\n : ''\n }\n <div class=\"content\" part=\"content\">\n <slot name=\"content\"></slot>\n </div>\n </div>`\n : ''\n }\n ${\n visibleRoles.includes('footer')\n ? html`<div class=\"footer\" part=\"footer\">\n <slot name=\"footer\"></slot>\n </div>`\n : ''\n }\n </div>\n `\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'sken-layout': SkenLayout\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"sken-layout.js","names":["#activeRoles","#visibleRoles","#syncRegionSlotAttributes","#regionSlotObserver","#recomputeVisibleRoles","#wireSlotChangeListeners","#unwireSlotChangeListeners","#slotChangeHandlers"],"sources":["../src/components/sken-layout.ts"],"sourcesContent":["// ── <sken-layout> — structural layout primitive ────────────────────\n// Defines the page-level structure of a multi-region interface\n// (topbar, primary nav, sidebar, content, footer) without imposing\n// what lives inside each region. Composition by contract:\n//\n// <sken-layout variant=\"header-sidebar\">\n// <sken-layout-region name=\"topbar\">…</sken-layout-region>\n// <sken-layout-region name=\"primary-nav\">…</sken-layout-region>\n// <sken-layout-region name=\"sidebar\">…</sken-layout-region>\n// <sken-layout-region name=\"content\">…</sken-layout-region>\n// </sken-layout>\n//\n// Design notes (v0.4.2 — fix for SKEN-LAYOUT-EMPTY-REGIONS.md):\n// - The shadow root projects the consumer's light-DOM regions via\n// NAMED slots, one per role. Each named slot sits inside a wrapper\n// element (e.g. <div class=\"topbar\">…</div>) that is the actual\n// grid item of the host's CSS grid.\n// - Roles the active variant does not include are not rendered at\n// all (the variant drives which slots exist in the shadow root).\n// - Roles the active variant includes but the consumer did not\n// fill are also not rendered. The host's `grid-template-areas`\n// is computed dynamically to exclude the empty rows/columns, so\n// the grid re-flows to fill the available space. This is the\n// third in a series of layout-rendering fixes:\n// - 0.4.0: variant-aware region rendering (only roles the\n// variant includes are emitted).\n// - 0.4.1: slot= attribute on the regions, so the browser\n// projects them to the named slots.\n// - 0.4.2 (this): content-aware grid template, so empty\n// rows/columns collapse.\n// - We detect which slots have content via `slot.assignedElements()`\n// on first render (after the browser has projected the\n// consumer's regions) and on every `slotchange` event (for\n// dynamic content — e.g. a sidebar the consumer can empty at\n// runtime). The result is stored in `#visibleRoles`, which\n// drives both the shadow template and the host's inline\n// `grid-template-areas` style.\n//\n// See ADR-0006 for the substrate decision. See the\n// @sken-ds/contracts types `SkenLayoutProps`, `SkenLayoutSlots`,\n// `SkenLayoutVariant`, `SkenLayoutVariantRoles` for the contract\n// surface.\n\nimport type {\n SkenLayoutDensity,\n SkenLayoutRegionRole,\n SkenLayoutSidebarPosition,\n SkenLayoutSidebarSize,\n SkenLayoutTopbarMode,\n SkenLayoutVariant,\n SkenLayoutVariantRolesMap,\n} from '@sken-ds/contracts'\nimport { SkenLayoutVariantRoles } from '@sken-ds/contracts'\nimport { LitElement, css, html, unsafeCSS, type PropertyValues } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\nimport './sken-layout-region.js'\n\n/**\n * Sidebar width in `rem` per size concept. The product can\n * override with `--sken-layout-sidebar-width` (CSS custom\n * property) if it needs a different value.\n */\nconst SIDEBAR_WIDTH: Record<SkenLayoutSidebarSize, string> = {\n sm: '12rem',\n md: '16rem',\n lg: '20rem',\n}\n\n/**\n * Per-cell background tokens. The values reference the\n * Sken color tokens, not raw colors, so a theme switch\n * (`data-theme=\"dark\"`) recolors every cell without the\n * primitive knowing about it.\n *\n * Mapping rationale:\n * - topbar is on `--sken-card` (white) because it is the\n * \"elevated\" chrome; the border-bottom separates it from\n * the page below.\n * - primary-nav is on `--sken-background` (page colour) so\n * it sits flush with the page; the border-bottom separates\n * it from the workspace.\n * - sidebar is on `--sken-muted` (subtle off-white) with a\n * border-inline-end; it is a \"rail\", not a \"page\".\n * - content is on `--sken-background`; no border (the\n * workspace is unbounded).\n * - footer is on `--sken-muted` with a border-top; it is\n * the same role as the sidebar (chrome).\n */\nconst REGION_BACKGROUND: Record<SkenLayoutRegionRole, string> = {\n topbar: 'var(--sken-card)',\n 'primary-nav': 'var(--sken-background)',\n sidebar: 'var(--sken-muted)',\n content: 'var(--sken-background)',\n footer: 'var(--sken-muted)',\n}\n\n@customElement('sken-layout')\nexport class SkenLayout extends LitElement {\n @property({ reflect: true }) variant: SkenLayoutVariant = 'header-content'\n @property({ attribute: 'topbar-mode', reflect: true }) topbarMode: SkenLayoutTopbarMode = 'fixed'\n @property({ attribute: 'sidebar-position', reflect: true })\n sidebarPosition: SkenLayoutSidebarPosition = 'start'\n @property({ attribute: 'sidebar-size', reflect: true }) sidebarSize: SkenLayoutSidebarSize = 'md'\n @property({ reflect: true }) density: SkenLayoutDensity = 'none'\n\n /**\n * MutationObserver that watches the host's light DOM for\n * `<sken-layout-region>` children. For each region, if the\n * consumer (or a framework adapter) did not set the HTML\n * `slot=\"<role>\"` attribute, we set it ourselves. This is\n * defense-in-depth: the host's shadow root uses 5 named\n * `<slot>` elements (one per role) and the browser's\n * slot-projection mechanism only projects a light-DOM child\n * into a named slot if the child carries the matching\n * `slot=\"<name>\"` attribute. Without it, every named slot\n * receives zero assigned elements and the regions render\n * as empty wrappers — the page looks almost empty.\n *\n * A future framework adapter (React, Svelte, plain HTML) can\n * either set the `slot` attribute on its own (the documented\n * contract) or rely on this observer to do it. Setting it\n * explicitly is still preferred for clarity, but the\n * observer is the safety net that prevents the silent\n * \"everything renders empty\" failure mode that v0.4.0\n * shipped with.\n */\n #regionSlotObserver: MutationObserver | undefined = undefined\n\n /**\n * Roles the active variant renders in the shadow root. Sourced\n * from {@link SkenLayoutVariantRoles}. Used to drive the\n * template (only the slots the variant includes are projected)\n * and to apply the per-variant `with-sidebar` class on the\n * body sub-grid wrapper.\n */\n get #activeRoles(): readonly SkenLayoutRegionRole[] {\n return (SkenLayoutVariantRoles as SkenLayoutVariantRolesMap)[this.variant]\n }\n\n /**\n * Roles the host grid should render in the shadow root AND\n * reserve space for in the grid template. This is the\n * intersection of the active variant's roles and the roles\n * whose slot actually has content (i.e. the consumer filled\n * the corresponding `<sken-layout-region>`). When a role is\n * in the variant but the consumer did not fill the slot, the\n * role is excluded — the wrapper is not rendered, and the\n * corresponding row/column in `grid-template-areas` is removed\n * so the surrounding regions re-flow.\n *\n * Updated on `firstUpdated` (after the browser has projected\n * the initial set of regions into the named slots) and on\n * every `slotchange` event (for dynamic content).\n */\n #visibleRoles: readonly SkenLayoutRegionRole[] = this.#activeRoles\n\n /**\n * Compute the current set of visible roles by walking the\n * host's light-DOM children and checking which ones carry\n * a `slot=\"<role>\"` attribute that matches one of the active\n * variant's roles.\n *\n * We deliberately do NOT use `slot.assignedElements()` (which\n * requires the browser to have done the slot projection and\n * is therefore timing-sensitive in shadow-DOM tests) — we\n * read the slot attribute directly on the children. This is\n * observable at any point, including during the first\n * `firstUpdated` call.\n *\n * The `#syncRegionSlotAttributes` method (called from\n * `connectedCallback` and again in `firstUpdated`) ensures\n * that the slot attribute is in place on every child region\n * before this method runs. The slotchange listener is\n * responsible for re-running this method when the consumer\n * mutates the slot attribute at runtime.\n */\n #recomputeVisibleRoles(): void {\n const visible: SkenLayoutRegionRole[] = []\n for (const role of this.#activeRoles) {\n // A role is \"filled\" if at least one direct child of\n // the host has slot=\"<role>\". We do not care about the\n // child's own children (an empty region is still a\n // filled region — the consumer can render a placeholder\n // inside an otherwise empty slot).\n const filled = Array.from(this.children).some(\n (c) => c.getAttribute && c.getAttribute('slot') === role,\n )\n if (filled) visible.push(role)\n }\n // Avoid re-renders if nothing changed.\n if (\n visible.length === this.#visibleRoles.length &&\n visible.every((r, i) => r === this.#visibleRoles[i])\n ) {\n return\n }\n this.#visibleRoles = visible\n this.requestUpdate()\n }\n\n override connectedCallback(): void {\n super.connectedCallback()\n // Walk the current children synchronously (so the first\n // render after connection already has the slot= attribute\n // in place), then attach the observer to catch future\n // mutations (the consumer can add or remove regions\n // dynamically, e.g. when switching the active variant in\n // a shared layout component).\n //\n // The observer also re-runs `#recomputeVisibleRoles`. The\n // MutationObserver fires on every add/remove of a direct\n // child, which is exactly the signal we need to refresh\n // the visible-roles set (the slotchange event is a poor\n // substitute because it only fires for slots that already\n // exist in the shadow root — a region the consumer adds\n // for a slot that the previous render omitted will not\n // fire slotchange because the slot does not exist yet).\n this.#syncRegionSlotAttributes()\n this.#regionSlotObserver = new MutationObserver(() => {\n this.#syncRegionSlotAttributes()\n this.#recomputeVisibleRoles()\n })\n this.#regionSlotObserver.observe(this, { childList: true, subtree: false })\n }\n\n /**\n * Lit lifecycle hook called after the first render. Three\n * things happen here, in order:\n *\n * 1. `#syncRegionSlotAttributes()` — second-pass sync point\n * for regions whose `name` was set as a property (not as\n * an attribute) before the region was connected. The\n * `name` property is `reflect: true`, so the attribute is\n * written only after the region is connected and Lit runs\n * its first render. Without this second pass, a region\n * whose `name` was set as a property (not as an attribute)\n * would not have its `slot` attribute set by the time the\n * browser tries to project it.\n *\n * 2. `#recomputeVisibleRoles()` — first pass at discovering\n * which slots have content. This MUST happen after the\n * browser has done its first slot projection, which is\n * exactly what `firstUpdated` is. Without this, the\n * initial render would assume all variant roles are\n * visible, and the grid template would reserve rows for\n * empty regions. The render that follows this call\n * produces the correct (collapsed) grid template.\n *\n * 3. Wire up `slotchange` listeners on the named slots, so\n * dynamic content (a consumer emptying or filling a slot\n * at runtime) re-triggers the same re-computation.\n */\n protected override firstUpdated(_changedProperties: PropertyValues): void {\n super.firstUpdated(_changedProperties)\n this.#syncRegionSlotAttributes()\n this.#recomputeVisibleRoles()\n this.#wireSlotChangeListeners()\n }\n\n /**\n * Lit lifecycle hook called after every render (including the\n * first). We re-run `#recomputeVisibleRoles` when the variant\n * changes because the set of valid roles (`#activeRoles`)\n * depends on the variant, and a region that was visible under\n * the previous variant may no longer be a member of the new\n * variant. Without this, switching the variant prop would\n * leave stale roles in `#visibleRoles` until the next\n * `slotchange` event.\n *\n * We do NOT call `requestUpdate` here — `#recomputeVisibleRoles`\n * already does that when the visible set actually changed. If\n * the set did not change, no extra render is needed.\n */\n protected override updated(_changedProperties: PropertyValues): void {\n super.updated(_changedProperties)\n if (_changedProperties.has('variant')) {\n this.#recomputeVisibleRoles()\n }\n }\n\n /**\n * Attach a `slotchange` listener to every named slot in the\n * shadow root. The browser fires `slotchange` on a slot\n * whenever the set of light-DOM children projected into it\n * changes (add, remove, or replace). This is a defense in\n * depth alongside the `MutationObserver` (which is the\n * primary signal for adds/removes): the slotchange listener\n * catches a consumer who replaces a region's content with\n * another consumer-defined element while keeping the same\n * `slot=\"<role>\"` attribute (the MutationObserver on\n * childList would not fire for a same-tag-name replacement\n * that swaps the element in place, though in practice the\n * browser usually fires childList anyway for new nodes).\n *\n * The listeners are attached in `firstUpdated` (so the slots\n * exist) and torn down in `disconnectedCallback`.\n */\n #slotChangeHandlers: Array<{ slot: HTMLSlotElement; handler: () => void }> = []\n\n #wireSlotChangeListeners(): void {\n // Tear down the previous batch first (in case the variant\n // changed and the shadow root was re-rendered, replacing\n // the old slot elements with new ones).\n this.#unwireSlotChangeListeners()\n const shadow = this.shadowRoot\n if (!shadow) return\n for (const slot of Array.from(shadow.querySelectorAll('slot'))) {\n const handler = () => this.#recomputeVisibleRoles()\n slot.addEventListener('slotchange', handler)\n this.#slotChangeHandlers.push({ slot, handler })\n }\n }\n\n #unwireSlotChangeListeners(): void {\n for (const { slot, handler } of this.#slotChangeHandlers) {\n slot.removeEventListener('slotchange', handler)\n }\n this.#slotChangeHandlers = []\n }\n\n override disconnectedCallback(): void {\n super.disconnectedCallback()\n this.#regionSlotObserver?.disconnect()\n this.#regionSlotObserver = undefined\n this.#unwireSlotChangeListeners()\n }\n\n /**\n * For each direct `<sken-layout-region>` child of the host,\n * set the `slot=\"<name>\"` HTML attribute if it is missing\n * or does not match the region's role. The browser's\n * slot-projection mechanism reads this attribute to project\n * the child into the matching named `<slot>` inside the\n * shadow root.\n *\n * We only touch direct children (not the entire subtree)\n * because the contract is that `<sken-layout-region>` lives\n * as a direct child of `<sken-layout>`; nested regions\n * are not a supported pattern.\n */\n #syncRegionSlotAttributes(): void {\n for (const child of Array.from(this.children)) {\n if (!(child instanceof HTMLElement)) continue\n if (child.tagName.toLowerCase() !== 'sken-layout-region') continue\n const role = child.getAttribute('name')\n if (!role) continue\n if (child.getAttribute('slot') === role) continue\n child.setAttribute('slot', role)\n }\n }\n\n static styles = css`\n /* ── The host ────────────────────────────────────────────────\n The host owns the page-level grid. The topbar / primary-nav\n / footer sit in their own rows; the body is a sub-grid that\n holds the sidebar (when present) and the content. */\n :host {\n display: block;\n block-size: 100dvh;\n background: var(--sken-background, Canvas);\n color: var(--sken-foreground, CanvasText);\n font-family: var(--sken-family-sans, system-ui, sans-serif);\n }\n\n /* The actual grid is a child <div class=\"grid\">. We use a\n child rather than the host itself for two reasons:\n\n 1. The grid template is computed per-render based on\n which roles have content (see #visibleRoles and\n render()). Setting grid-template-areas on the host\n via an inline style would work, but the host has a\n handful of other responsibilities (background, color,\n font) and keeping the grid in a child makes the\n separation cleaner.\n\n 2. CSS grid in shadow DOM: putting the grid on a child\n avoids any quirks of the host element's display\n behaviour (the host is a custom element with its own\n display semantics). */\n .grid {\n display: grid;\n block-size: 100%;\n grid-template-columns: 1fr;\n /* The grid template is set per-render via inline style\n using the --sken-layout-grid-areas and\n --sken-layout-grid-rows custom properties (set by the\n render() function). The default values below match\n the header-content variant; the inline style overrides\n them for variants with more or fewer roles, and for\n consumers that have unfilled regions. */\n grid-template-areas: var(--sken-layout-grid-areas, 'topbar' 'body');\n grid-template-rows: var(--sken-layout-grid-rows, 'auto 1fr');\n }\n\n /* ── Per-cell grid items ─────────────────────────────────────\n Each named slot sits inside a wrapper element that is the\n actual grid item. The wrapper's class matches the role\n (topbar, primary-nav, body, footer). The variant rules\n below assign the right 'grid-area' to each role. The\n 'display: contents' <sken-layout-region> is projected\n through the named <slot> and inherits the wrapper's\n background, border, and padding. */\n .topbar {\n grid-area: topbar;\n background: var(\n --sken-layout-region-topbar-background,\n ${unsafeCSS(REGION_BACKGROUND.topbar)}\n );\n padding-block: var(--sken-layout-region-topbar-padding-block, 0);\n padding-inline: var(--sken-layout-region-topbar-padding-inline, 0);\n border-bottom: 1px solid var(--sken-border);\n }\n .primary-nav {\n grid-area: primary-nav;\n background: var(\n --sken-layout-region-primary-nav-background,\n ${unsafeCSS(REGION_BACKGROUND['primary-nav'])}\n );\n padding-block: var(--sken-layout-region-primary-nav-padding-block, 0);\n padding-inline: var(--sken-layout-region-primary-nav-padding-inline, 0);\n border-bottom: 1px solid var(--sken-border);\n }\n .body {\n grid-area: body;\n display: grid;\n grid-template-columns: 1fr;\n grid-template-areas: 'content';\n min-block-size: 0; /* allow children to shrink + scroll */\n }\n .body.with-sidebar {\n grid-template-columns: var(--sken-layout-sidebar-width, 16rem) 1fr;\n grid-template-areas: 'sidebar content';\n }\n .body.with-sidebar.sidebar-end {\n grid-template-columns: 1fr var(--sken-layout-sidebar-width, 16rem);\n grid-template-areas: 'content sidebar';\n }\n .sidebar {\n grid-area: sidebar;\n min-block-size: 0;\n background: var(\n --sken-layout-region-sidebar-background,\n ${unsafeCSS(REGION_BACKGROUND.sidebar)}\n );\n padding-block: var(--sken-layout-region-sidebar-padding-block, 0);\n padding-inline: var(--sken-layout-region-sidebar-padding-inline, 0);\n border-inline-end: 1px solid var(--sken-border);\n }\n .content {\n min-block-size: 0;\n background: var(\n --sken-layout-region-content-background,\n ${unsafeCSS(REGION_BACKGROUND.content)}\n );\n padding-block: var(--sken-layout-region-content-padding-block, 0);\n padding-inline: var(--sken-layout-region-content-padding-inline, 0);\n }\n .footer {\n grid-area: footer;\n background: var(\n --sken-layout-region-footer-background,\n ${unsafeCSS(REGION_BACKGROUND.footer)}\n );\n padding-block: var(--sken-layout-region-footer-padding-block, 0);\n padding-inline: var(--sken-layout-region-footer-padding-inline, 0);\n border-top: 1px solid var(--sken-border);\n }\n\n /* ── Density: comfortable ─────────────────────────────────────\n When density=comfortable, the layout applies the Sken\n default rhythm: 8/16px on chrome cells, 16/24px on the\n content area, 12/16px on the footer. The values come\n from the spacing scale (--sken-2, --sken-3, --sken-4,\n --sken-6) so a single token change ripples through\n every cell. A consumer override on any\n --sken-layout-region-{role}-padding-{block|inline}\n always wins over the density preset. */\n :host([density='comfortable']) .topbar,\n :host([density='comfortable']) .primary-nav,\n :host([density='comfortable']) .content,\n :host([density='comfortable']) .sidebar,\n :host([density='comfortable']) .footer {\n --sken-layout-region-topbar-padding-block: var(--sken-2);\n --sken-layout-region-topbar-padding-inline: var(--sken-4);\n --sken-layout-region-primary-nav-padding-block: var(--sken-2);\n --sken-layout-region-primary-nav-padding-inline: var(--sken-4);\n --sken-layout-region-sidebar-padding-block: var(--sken-4);\n --sken-layout-region-sidebar-padding-inline: var(--sken-4);\n --sken-layout-region-content-padding-block: var(--sken-6);\n --sken-layout-region-content-padding-inline: var(--sken-6);\n --sken-layout-region-footer-padding-block: var(--sken-3);\n --sken-layout-region-footer-padding-inline: var(--sken-4);\n }\n\n /* ── Density: compact ────────────────────────────────────────\n When density=compact, every per-cell padding is halved\n from the comfortable value. We use calc(var(--sken-N) *\n 0.5) instead of hardcoded half-values so a future\n token change automatically cascades. As with\n comfortable, a consumer override on any\n --sken-layout-region-{role}-padding-{block|inline}\n always wins. */\n :host([density='compact']) .topbar,\n :host([density='compact']) .primary-nav,\n :host([density='compact']) .content,\n :host([density='compact']) .sidebar,\n :host([density='compact']) .footer {\n --sken-layout-region-topbar-padding-block: calc(var(--sken-2) * 0.5);\n --sken-layout-region-topbar-padding-inline: calc(var(--sken-4) * 0.5);\n --sken-layout-region-primary-nav-padding-block: calc(var(--sken-2) * 0.5);\n --sken-layout-region-primary-nav-padding-inline: calc(var(--sken-4) * 0.5);\n --sken-layout-region-sidebar-padding-block: calc(var(--sken-4) * 0.5);\n --sken-layout-region-sidebar-padding-inline: calc(var(--sken-4) * 0.5);\n --sken-layout-region-content-padding-block: calc(var(--sken-6) * 0.5);\n --sken-layout-region-content-padding-inline: calc(var(--sken-6) * 0.5);\n --sken-layout-region-footer-padding-block: calc(var(--sken-3) * 0.5);\n --sken-layout-region-footer-padding-inline: calc(var(--sken-4) * 0.5);\n }\n\n /* ── Grid templates per variant ───────────────────────────────\n The grid template is set per-render as an inline style\n (via --sken-layout-grid-areas and\n --sken-layout-grid-rows) based on the visible roles (the\n intersection of the active variant and the roles whose\n slot has content). See render() for the computation.\n The default values on the .grid selector above cover\n header-content (the default variant) when no inline\n style is set (e.g. before the first render completes). */\n\n /* ── Topbar mode ──────────────────────────────────────────────\n 'fixed': the host is 100dvh and the body scrolls under the\n topbar. This is the default for admin consoles.\n 'auto': the topbar scrolls away with the page (marketing,\n landing). */\n :host([topbar-mode='auto']) {\n block-size: auto;\n min-block-size: 100dvh;\n }\n\n /* ── Sidebar position ─────────────────────────────────────────\n When the sidebar sits on the inline-end side, the border\n moves to the inline-start side. The default rule above\n already paints the border on the inline-end; here we\n swap it. */\n :host([sidebar-position='end']) .sidebar {\n border-inline-end: none;\n border-inline-start: 1px solid var(--sken-border);\n }\n\n /* ── Sidebar width override ─────────────────────────────────── */\n :host([sidebar-size='sm']) {\n --sken-layout-sidebar-width: ${unsafeCSS(SIDEBAR_WIDTH.sm)};\n }\n :host([sidebar-size='md']) {\n --sken-layout-sidebar-width: ${unsafeCSS(SIDEBAR_WIDTH.md)};\n }\n :host([sidebar-size='lg']) {\n --sken-layout-sidebar-width: ${unsafeCSS(SIDEBAR_WIDTH.lg)};\n }\n `\n\n override render() {\n // Use #visibleRoles (not #activeRoles) so empty regions\n // are not emitted. #visibleRoles is the active variant\n // intersected with the roles whose slot has content; it\n // is updated by firstUpdated and slotchange handlers.\n const visibleRoles = this.#visibleRoles\n const hasContent = visibleRoles.includes('content')\n const hasSidebar = visibleRoles.includes('sidebar')\n\n // ── Compute the host's grid template ───────────────────────\n // We declare the area names in the order they should appear\n // in the grid (top to bottom). The browser uses this\n // template to lay out the wrappers: each wrapper has\n // `grid-area: <role>`, and the template maps that area to a\n // row. Excluded roles do not appear in the template, and\n // the corresponding wrapper is not rendered, so the row\n // simply does not exist.\n //\n // The order is the same as #activeRoles (which is the\n // order from SkenLayoutVariantRoles). That order is the\n // structural shape of the variant — the consumer picks the\n // variant, we project the order.\n //\n // For the row that holds the main content, the area name\n // is \"body\" (because the wrapper that owns that row is the\n // .body sub-grid, not a .content div). The SSoT for\n // \"which row is the body\" is \"the row whose role is\n // 'content'\" — the wrapper .body is rendered whenever\n // 'content' is visible, and it lives in the row we name\n // \"body\" in the template. This keeps the wrapper\n // structure (one .body that holds sidebar+content) in sync\n // with the grid template (one row named \"body\" that\n // stretches to fill the remaining space).\n const gridTemplateAreas = visibleRoles\n .map((r) => (r === 'content' ? '\"body\"' : `\"${r}\"`))\n .join('\\n')\n\n // Compute grid-template-rows to match. We need one row per\n // visible role. The \"1fr\" row is the body (it stretches to\n // fill the remaining space); the others are \"auto\" (they\n // take the height of their content). The body row is the\n // one whose role in #visibleRoles is 'content'.\n const gridTemplateRows = visibleRoles.map((r) => (r === 'content' ? '1fr' : 'auto')).join(' ')\n\n const bodyClasses = [\n 'body',\n hasSidebar ? 'with-sidebar' : '',\n hasSidebar && this.sidebarPosition === 'end' ? 'sidebar-end' : '',\n ]\n .filter(Boolean)\n .join(' ')\n\n // The grid template is applied as an inline style because\n // it is computed per render (the visible roles change when\n // the consumer adds or removes regions). The CSS in\n // `static styles` sets the default per-variant template, but\n // the inline style overrides it when the visible set\n // differs from the variant's full set.\n const hostStyle =\n `--sken-layout-grid-areas: ${gridTemplateAreas}; ` +\n `--sken-layout-grid-rows: ${gridTemplateRows};`\n\n return html`\n <div class=\"grid\" style=${hostStyle} part=\"grid\">\n ${\n visibleRoles.includes('topbar')\n ? html`<div class=\"topbar\" part=\"topbar\">\n <slot name=\"topbar\"></slot>\n </div>`\n : ''\n }\n ${\n visibleRoles.includes('primary-nav')\n ? html`<div class=\"primary-nav\" part=\"primary-nav\">\n <slot name=\"primary-nav\"></slot>\n </div>`\n : ''\n }\n ${\n hasContent\n ? html`<div class=${bodyClasses} part=\"body\">\n ${\n hasSidebar\n ? html`<div class=\"sidebar\" part=\"sidebar\">\n <slot name=\"sidebar\"></slot>\n </div>`\n : ''\n }\n <div class=\"content\" part=\"content\">\n <slot name=\"content\"></slot>\n </div>\n </div>`\n : ''\n }\n ${\n visibleRoles.includes('footer')\n ? html`<div class=\"footer\" part=\"footer\">\n <slot name=\"footer\"></slot>\n </div>`\n : ''\n }\n </div>\n `\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'sken-layout': SkenLayout\n }\n}\n"],"mappings":"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"}
@@ -8,7 +8,7 @@ var s = 1, c = !0, l = class extends t {
8
8
  }
9
9
  #e;
10
10
  constructor() {
11
- super(), this.#e = null, this.defaultValue = void 0, this.value = void 0, this.min = void 0, this.max = void 0, this.step = s, this.showStepperButtons = c, this.placeholder = void 0, this.label = void 0, this.required = !1, this.disabled = !1, this.readonly = !1, this.helperText = void 0, this.infoText = void 0, this.warningText = void 0, this.validText = void 0, this.invalidText = void 0, this.textAlignment = "end", this.size = "md", this.invalid = !1, this.describedById = void 0, this.name = void 0, this.#t = "", this.#r = "0.5rem", this.#i = () => {
11
+ super(), this.#e = void 0, this.defaultValue = void 0, this.value = void 0, this.min = void 0, this.max = void 0, this.step = s, this.showStepperButtons = c, this.placeholder = void 0, this.label = void 0, this.required = !1, this.disabled = !1, this.readonly = !1, this.helperText = void 0, this.infoText = void 0, this.warningText = void 0, this.validText = void 0, this.invalidText = void 0, this.textAlignment = "end", this.size = "md", this.invalid = !1, this.describedById = void 0, this.name = void 0, this.#t = "", this.#r = "0.5rem", this.#i = () => {
12
12
  let e = this.renderRoot.querySelector("input"), t = this.renderRoot.querySelector(".steppers"), n = this.renderRoot.querySelector(".slot-end");
13
13
  !e || !n || requestAnimationFrame(() => {
14
14
  if (!e.isConnected) return;
@@ -17,7 +17,7 @@ var s = 1, c = !0, l = class extends t {
17
17
  let c = s + i, l = Math.max(o, c) + a;
18
18
  e.style.paddingInlineEnd = `${l}px`;
19
19
  });
20
- }, this.#a = null, this.#o = null, this.#c = (e) => {
20
+ }, this.#a = void 0, this.#o = void 0, this.#c = (e) => {
21
21
  let t = e.target;
22
22
  this.#t = t.value;
23
23
  let n = this.#v(t.value);
@@ -63,7 +63,7 @@ var s = 1, c = !0, l = class extends t {
63
63
  try {
64
64
  this.#e = this.attachInternals();
65
65
  } catch {
66
- this.#e = null;
66
+ this.#e = void 0;
67
67
  }
68
68
  }
69
69
  #t;
@@ -73,6 +73,9 @@ var s = 1, c = !0, l = class extends t {
73
73
  disconnectedCallback() {
74
74
  super.disconnectedCallback(), this.#a?.disconnect(), this.#o?.disconnect();
75
75
  }
76
+ willUpdate(e) {
77
+ e.has("value") && (this.value === void 0 ? this.#t = this.defaultValue === void 0 ? "" : String(this.defaultValue) : this.#v(this.#t) !== this.value && (this.#t = String(this.value)));
78
+ }
76
79
  formResetCallback() {
77
80
  if (this.value === void 0) {
78
81
  this.#t = this.defaultValue === void 0 ? "" : String(this.defaultValue);
@@ -1 +1 @@
1
- {"version":3,"file":"sken-number-input.js","names":["#rawText","#parseNumber","#syncFormValue","#clamp","#step","#commitValue","#internals","#slotEndObserver","#visibilityObserver","#adjustLayout","#renderMessages","#handleInput","#handleChange","#handleFocus","#handleBlur","#handleKeydown","#handleStepUp","#handleStepDown"],"sources":["../src/components/sken-number-input.ts"],"sourcesContent":["// ── <sken-number-input> — Sken-owned Web Component ─────────────────\n// Lit 3 primitive for numeric input with visible ± stepper buttons,\n// min/max clamping, and the same start/end slot system as\n// <sken-input>. Promoted from the SkenNumberInput PoC story (which\n// was a thin wrap of <ix-number-input>). The PoC proved the UX;\n// this primitive owns the contract.\n\nimport type { SkenNumberInputSize } from '@sken-ds/contracts'\nimport { LitElement, css, html, nothing } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\n\n/**\n * Default step when the consumer does not pass one. Matches the\n * native `<input type=\"number\" step>` default.\n */\nconst DEFAULT_STEP = 1\n\n/**\n * Default value for `showStepperButtons`. The PoC validated that\n * the iX default of `false` is wrong for our use case: consumers\n * expect ± buttons for the numeric fields they use most (Alma\n * demo: quantity, temperature, score). The Sken primitive flips\n * the default to `true`; consumers who want a bare input pass\n * `showStepperButtons={false}` explicitly.\n */\nconst DEFAULT_SHOW_STEPPER_BUTTONS = true\n\n@customElement('sken-number-input')\nexport class SkenNumberInput extends LitElement {\n // Form-associated custom element. Same pattern as SkenInput /\n // SkenTextarea. The numeric value is stringified for\n // FormData (FormData is text-only); undefined becomes the\n // empty string. The implicit-submit-on-Enter rule still\n // applies, so a form with a single number input and a\n // submit button will submit on Enter.\n // https://lit.dev/docs/components/form/\n static formAssociated = true\n\n // ElementInternals handle. Created in the constructor.\n // Nullable to gracefully degrade in test environments.\n #internals: ElementInternals | null = null\n\n constructor() {\n super()\n try {\n this.#internals = this.attachInternals()\n } catch {\n this.#internals = null\n }\n }\n\n // ── Props ─────────────────────────────────────────────────────\n @property({ attribute: 'default-value', type: Number }) defaultValue: number | undefined =\n undefined\n @property({ type: Number }) value: number | undefined = undefined\n @property({ type: Number }) min: number | undefined = undefined\n @property({ type: Number }) max: number | undefined = undefined\n @property({ type: Number }) step: number = DEFAULT_STEP\n @property({ attribute: 'show-stepper-buttons', reflect: true, type: Boolean })\n showStepperButtons = DEFAULT_SHOW_STEPPER_BUTTONS\n @property() placeholder: string | undefined = undefined\n @property() label: string | undefined = undefined\n @property({ reflect: true, type: Boolean }) required = false\n @property({ reflect: true, type: Boolean }) disabled = false\n @property({ reflect: true, type: Boolean }) readonly = false\n @property({ attribute: 'helper-text' }) helperText: string | undefined = undefined\n @property({ attribute: 'info-text' }) infoText: string | undefined = undefined\n @property({ attribute: 'warning-text' }) warningText: string | undefined = undefined\n @property({ attribute: 'valid-text' }) validText: string | undefined = undefined\n @property({ attribute: 'invalid-text' }) invalidText: string | undefined = undefined\n @property({ attribute: 'text-alignment' }) textAlignment: 'start' | 'end' = 'end'\n @property({ reflect: true }) size: SkenNumberInputSize = 'md'\n @property({ reflect: true, type: Boolean }) invalid = false\n @property({ attribute: 'described-by-id' }) describedById: string | undefined = undefined\n @property() name: string | undefined = undefined\n\n // ── Internal state ────────────────────────────────────────────\n /**\n * The raw text in the input. The DOM input is the source of\n * truth for the text (so the user can type a partial value like\n * \"-\", which is not a valid number yet). The numeric `value` is\n * derived from this on commit (blur, Enter, stepper).\n */\n #rawText: string = ''\n\n // ── DOM refs (queried on demand) ────────────────────────────\n\n // ── Lifecycle ─────────────────────────────────────────────────\n override connectedCallback(): void {\n super.connectedCallback()\n // Seed the raw text from the controlled value or the default.\n // We do NOT trigger a re-render here; the property is only\n // set after Lit has applied the host attributes, and the\n // first render will read the right value through this.value.\n if (this.value === undefined) {\n this.#rawText = this.defaultValue !== undefined ? String(this.defaultValue) : ''\n } else {\n this.#rawText = String(this.value)\n }\n // Publish the initial value to the surrounding form.\n this.#syncFormValue()\n }\n\n override disconnectedCallback(): void {\n super.disconnectedCallback()\n this.#slotEndObserver?.disconnect()\n this.#visibilityObserver?.disconnect()\n }\n\n // ── Form-association callbacks ─────────────────────────────\n // formResetCallback: restore uncontrolled seed; re-render\n // controlled so Lit re-publishes the controlled value.\n formResetCallback(): void {\n if (this.value === undefined) {\n this.#rawText = this.defaultValue !== undefined ? String(this.defaultValue) : ''\n const inputEl = this.renderRoot.querySelector('input') as HTMLInputElement | null\n if (inputEl) inputEl.value = this.#rawText\n this.#syncFormValue()\n } else {\n this.requestUpdate()\n }\n }\n\n // formDisabledCallback: mirror the form's :disabled state\n // onto the prop. Lit re-renders; the inner <input> picks up\n // ?disabled=${this.disabled} in the template.\n formDisabledCallback(isDisabled: boolean): void {\n this.disabled = isDisabled\n }\n\n // Publish the current value to the form. FormData is\n // text-only, so we stringify the number (or empty string\n // for undefined). The string roundtrip preserves precision\n // for integers and finite decimals; consumers that need\n // BigInt or arbitrary precision should re-parse on the\n // server side.\n #syncFormValue(): void {\n if (!this.#internals) return\n const value = this.value !== undefined ? String(this.value) : ''\n this.#internals.setFormValue(value)\n }\n\n /**\n * Set up the dynamic layout once the shadow root has the\n * element children we need to measure. Same pattern as\n * SkenInput: MutationObserver on the slot container to\n * catch children being added/removed, IntersectionObserver\n * to catch the case where the input is hidden at first\n * connect (e.g. inside a closed <details> or an off-screen\n * tab), and slotchange as belt-and-braces.\n */\n protected override firstUpdated(): void {\n const endContainer = this.renderRoot.querySelector('.slot-end') as HTMLElement | null\n const endSlot = this.renderRoot.querySelector('slot[name=\"end\"]') as HTMLSlotElement | null\n if (endContainer && endSlot) {\n this.#slotEndObserver = new MutationObserver(() => this.#adjustLayout())\n this.#slotEndObserver.observe(endContainer, {\n childList: true,\n subtree: true,\n attributes: true,\n })\n endSlot.addEventListener('slotchange', this.#adjustLayout)\n }\n this.#visibilityObserver = new IntersectionObserver((entries) => {\n for (const entry of entries) {\n if (entry.isIntersecting) this.#adjustLayout()\n }\n })\n this.#visibilityObserver.observe(this)\n this.#adjustLayout()\n }\n\n /**\n * The gap between a slot's edge and the input's text. 0.5rem\n * is what the SkenInput primitive uses (mirrors the iX rule).\n * The number input needs an extra \"air\" margin because the\n * stepper column is also on the right edge and we don't want\n * the value text to crash into the slot.\n */\n #SLOT_AIR = '0.5rem'\n\n /**\n * Measure the stepper column width and the end slot width, then\n * apply:\n * - The input's padding-inline-end so the value text never\n * overlaps the slot OR the stepper.\n * - The end slot's inset-inline-end so it sits to the LEFT of\n * the stepper (or at the right edge when no stepper).\n *\n * The math:\n * endInset = stepperReserve + air (slot's right edge)\n * paddingInline = max(endInset, stepperReserve) + air\n * If the end slot is empty, its width is 0 and the calc\n * falls back to the stepper-only reservation.\n */\n #adjustLayout = (): void => {\n const inputEl = this.renderRoot.querySelector('input') as HTMLInputElement | null\n const stepperEl = this.renderRoot.querySelector('.steppers') as HTMLElement | null\n const slotEndEl = this.renderRoot.querySelector('.slot-end') as HTMLElement | null\n if (!inputEl || !slotEndEl) return\n\n requestAnimationFrame(() => {\n if (!inputEl.isConnected) return\n const stepperWidth =\n stepperEl && this.showStepperButtons ? stepperEl.getBoundingClientRect().width : 0\n const endWidth = slotEndEl.getBoundingClientRect().width\n const air = parseFloat(getComputedStyle(inputEl).fontSize) * 0.5 // 0.5em in px\n const stepperReserve = stepperWidth\n // The slot's right edge sits at stepperReserve + air from\n // the input's right edge, so it never overlaps the stepper\n // column.\n const endInset = stepperReserve + air\n slotEndEl.style.insetInlineEnd = endInset > 0 ? `${endInset}px` : '0.5rem'\n // The input's padding-inline-end has to be at least the\n // wider of (stepperReserve, endInset + endWidth) so the\n // value text never overlaps either.\n const textReserve = endInset + endWidth\n const padEnd = Math.max(stepperReserve, textReserve) + air\n inputEl.style.paddingInlineEnd = `${padEnd}px`\n })\n }\n\n #slotEndObserver: MutationObserver | null = null\n #visibilityObserver: IntersectionObserver | null = null\n\n // ── Styles ────────────────────────────────────────────────────\n static styles = css`\n :host {\n display: inline-block;\n inline-size: 100%;\n }\n\n .field {\n display: grid;\n gap: 0.375rem;\n }\n\n .label {\n font-size: 0.8125rem;\n font-weight: 500;\n color: var(--sken-foreground);\n }\n\n .label[data-required='true']::after {\n content: ' *';\n color: var(--sken-destructive);\n }\n\n .input-wrapper {\n position: relative;\n display: flex;\n align-items: center;\n inline-size: 100%;\n }\n\n input {\n box-sizing: border-box;\n inline-size: 100%;\n border: 1px solid var(--sken-border);\n border-radius: var(--sken-md);\n background: var(--sken-input);\n color: var(--sken-foreground);\n font-family: var(--sken-family-sans);\n font-size: var(--sken-size-md);\n line-height: 1.25;\n text-overflow: ellipsis;\n padding-block: var(--sken-2);\n padding-inline: var(--sken-3);\n /* When the ± steppers are visible, reserve the right edge\n for them so the value text never overlaps. 1.5rem (button)\n + 2 * 0.25rem (insets) + 0.25rem (air) = 2.25rem. */\n padding-inline-end: var(--sken-3);\n min-block-size: 2.25rem;\n transition:\n border-color 120ms ease,\n box-shadow 120ms ease;\n /* Hide the native steppers: we render our own. */\n -moz-appearance: textfield;\n }\n\n input::-webkit-outer-spin-button,\n input::-webkit-inner-spin-button {\n -webkit-appearance: none;\n margin: 0;\n }\n\n input::placeholder {\n color: var(--sken-muted-foreground);\n opacity: 1;\n }\n\n /* ── Stepper buttons ───────────────────────────────────── */\n /*\n * The stepper column is a vertical pair of + and − buttons\n * anchored to the right edge of the input. Sizes scale with\n * the host's [size] attribute (sm / md / lg) so the buttons\n * match the input's vertical rhythm. Default (md) is 24x20\n * per button. Glyph color is --sken-foreground; hover swaps\n * to --sken-primary. Disabled state uses --sken-disabled.\n */\n .steppers {\n position: absolute;\n inset-block: 0.25rem;\n inset-inline-end: 0.25rem;\n display: flex;\n flex-direction: column;\n gap: 2px;\n align-items: center;\n justify-content: center;\n }\n\n .stepper {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n inline-size: 1.5rem;\n block-size: 1.25rem;\n background: transparent;\n border: 0;\n padding: 0;\n color: var(--sken-foreground);\n cursor: pointer;\n font-family: inherit;\n font-size: 1rem;\n line-height: 1;\n font-weight: 600;\n border-radius: var(--sken-sm, 0.25rem);\n /* The parent .steppers is a flex column; prevent the\n buttons from being squashed when the host's vertical\n rhythm is tight (sm). The explicit block-size wins. */\n flex-shrink: 0;\n transition:\n color 120ms ease,\n background-color 120ms ease;\n }\n\n .stepper:hover:not(:disabled) {\n color: var(--sken-primary);\n /* No background fill on hover. A background would paint\n over the input's right border, hiding it. The color\n change to --sken-primary is enough affordance for a\n 20x24px icon button. */\n background: transparent;\n }\n\n .stepper:focus-visible {\n outline: 2px solid var(--sken-ring, currentColor);\n outline-offset: 1px;\n }\n\n .stepper:disabled {\n cursor: not-allowed;\n opacity: var(--sken-disabled);\n }\n\n /* When the ± steppers are visible, reserve the right edge\n of the input so the value text never overlaps. The\n reservation is 2rem for the md size (default): 1.5rem\n (button) + 2 * 0.25rem (column insets). The sm / lg\n variants below override the button size, so the\n reservation needs to scale too. */\n :host([show-stepper-buttons]) input {\n padding-inline-end: 2rem;\n }\n\n /* ── Stepper sizes ─────────────────────────────────────── */\n :host([size='sm']) .steppers {\n inset-block: 0.125rem;\n inset-inline-end: 0.125rem;\n gap: 1px;\n }\n :host([size='sm']) .stepper {\n inline-size: 1.125rem;\n block-size: 0.875rem;\n font-size: 0.75rem;\n }\n /* sm: 1.125rem (button) + 2 * 0.125rem (insets) = 1.375rem,\n plus 0.125rem of breathing air = 1.5rem. */\n :host([size='sm'][show-stepper-buttons]) input {\n padding-inline-end: 1.5rem;\n }\n\n :host([size='lg']) .steppers {\n inset-block: 0.375rem;\n inset-inline-end: 0.375rem;\n gap: 3px;\n }\n :host([size='lg']) .stepper {\n inline-size: 1.75rem;\n block-size: 1.5rem;\n font-size: 1.125rem;\n }\n /* lg: 1.75rem (button) + 2 * 0.375rem (insets) = 2.5rem,\n plus 0.25rem of breathing air = 2.75rem. */\n :host([size='lg'][show-stepper-buttons]) input {\n padding-inline-end: 2.75rem;\n }\n\n /* Hide steppers on touch devices where they would be hard\n to hit accurately. The user can still use ↑/↓ keys or\n type directly. */\n @media (hover: none) {\n .steppers {\n display: none;\n }\n input {\n padding-inline-end: var(--sken-3);\n }\n :host([size='sm']) input {\n padding-inline-end: var(--sken-2);\n }\n :host([size='lg']) input {\n padding-inline-end: var(--sken-4);\n }\n }\n\n /* ── Slot containers ────────────────────────────────────── */\n .slot {\n position: absolute;\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 1;\n pointer-events: none;\n color: var(--sken-muted-foreground);\n }\n\n .slot ::slotted(*) {\n pointer-events: auto;\n }\n\n .slot-start {\n inset-inline-start: 0.5rem;\n }\n\n /*\n * The end slot's position is set in JS via the inline style\n * (see #adjustLayout). The rule below is a fallback for the\n * initial render before JS has measured the slot, and a\n * sensible default when the slot is empty.\n */\n .slot-end {\n inset-inline-end: 0.5rem;\n }\n\n /* ── States ─────────────────────────────────────────────── */\n .input-wrapper:hover input:not(:disabled):not(:read-only) {\n border-color: var(--sken-foreground);\n }\n\n .input-wrapper:focus-within input {\n outline: 2px solid var(--sken-ring, currentColor);\n outline-offset: 2px;\n border-color: var(--sken-primary);\n }\n\n input:disabled {\n cursor: not-allowed;\n opacity: var(--sken-disabled);\n }\n\n input:read-only {\n background: var(--sken-muted);\n }\n\n :host([invalid]) input {\n border-color: var(--sken-destructive);\n }\n\n :host([invalid]) .input-wrapper:focus-within input {\n outline-color: var(--sken-destructive);\n }\n\n /* ── Sizes ──────────────────────────────────────────────── */\n :host([size='sm']) input {\n padding-block: var(--sken-1);\n padding-inline: var(--sken-2);\n font-size: var(--sken-size-sm);\n /* 2 × 0.875rem (buttons) + 1px (gap) + 2 × 0.125rem\n (insets) = ~2.0625rem. Round up to 2.25rem for visual\n breathing room. */\n min-block-size: 2.25rem;\n }\n :host([size='md']) input {\n /* Default styles above. */\n }\n :host([size='lg']) input {\n padding-block: var(--sken-3);\n padding-inline: var(--sken-4);\n font-size: var(--sken-size-lg);\n /* 2 × 1.5rem (buttons) + 3px (gap) + 2 × 0.375rem\n (insets) = ~3.9375rem. Round up to 4rem. */\n min-block-size: 4rem;\n }\n\n /* ── Validation messages ─────────────────────────────────── */\n .messages {\n display: grid;\n gap: 0.25rem;\n }\n\n .message {\n margin: 0;\n font-size: 0.75rem;\n line-height: 1.4;\n }\n\n .message[data-tone='info'] {\n color: var(--sken-info, #0082ff);\n }\n\n .message[data-tone='warning'] {\n color: var(--sken-warning, #f59e0b);\n }\n\n .message[data-tone='valid'] {\n color: var(--sken-success, #10b981);\n }\n\n .message[data-tone='invalid'] {\n color: var(--sken-destructive, #dc2626);\n }\n\n .message[data-tone='helper'] {\n color: var(--sken-muted-foreground);\n }\n `\n\n // ── Render ────────────────────────────────────────────────────\n protected override render() {\n const messages = this.#renderMessages()\n return html`\n <div class=\"field\" part=\"field\">\n ${\n this.label\n ? html`\n <label class=\"label\" part=\"label\" data-required=${this.required}>\n ${this.label}\n </label>\n `\n : nothing\n }\n <div class=\"input-wrapper\" part=\"wrapper\">\n <div class=\"slot slot-start\" part=\"slot-start\">\n <slot name=\"start\"></slot>\n </div>\n <input\n part=\"input\"\n type=\"number\"\n .value=${this.#rawText}\n placeholder=${this.placeholder ?? ''}\n ?disabled=${this.disabled}\n ?readonly=${this.readonly}\n ?required=${this.required}\n min=${this.min ?? ''}\n max=${this.max ?? ''}\n step=${this.step}\n aria-invalid=${this.invalid ? 'true' : 'false'}\n aria-describedby=${this.describedById ?? ''}\n name=${this.name ?? ''}\n style=\"text-align: ${this.textAlignment}\"\n @input=${this.#handleInput}\n @change=${this.#handleChange}\n @focus=${this.#handleFocus}\n @blur=${this.#handleBlur}\n @keydown=${this.#handleKeydown}\n />\n <div class=\"slot slot-end\" part=\"slot-end\">\n <slot name=\"end\"></slot>\n </div>\n ${\n this.showStepperButtons\n ? html`\n <div class=\"steppers\" part=\"steppers\">\n <button\n class=\"stepper\"\n part=\"stepper stepper-up\"\n type=\"button\"\n aria-label=\"Increment\"\n ?disabled=${\n this.disabled ||\n this.readonly ||\n (this.value !== undefined &&\n this.max !== undefined &&\n this.value >= this.max)\n }\n @click=${this.#handleStepUp}\n >\n +\n </button>\n <button\n class=\"stepper\"\n part=\"stepper stepper-down\"\n type=\"button\"\n aria-label=\"Decrement\"\n ?disabled=${\n this.disabled ||\n this.readonly ||\n (this.value !== undefined &&\n this.min !== undefined &&\n this.value <= this.min)\n }\n @click=${this.#handleStepDown}\n >\n −\n </button>\n </div>\n `\n : nothing\n }\n </div>\n ${\n messages.length > 0\n ? html` <div class=\"messages\" part=\"messages\">${messages}</div> `\n : nothing\n }\n </div>\n `\n }\n\n #renderMessages() {\n const messages: ReturnType<typeof html>[] = []\n if (this.invalidText) {\n messages.push(\n html`<p class=\"message\" data-tone=\"invalid\" part=\"message-invalid\">${this.invalidText}</p>`,\n )\n }\n if (this.warningText) {\n messages.push(\n html`<p class=\"message\" data-tone=\"warning\" part=\"message-warning\">${this.warningText}</p>`,\n )\n }\n if (this.infoText) {\n messages.push(\n html`<p class=\"message\" data-tone=\"info\" part=\"message-info\">${this.infoText}</p>`,\n )\n }\n if (this.validText) {\n messages.push(\n html`<p class=\"message\" data-tone=\"valid\" part=\"message-valid\">${this.validText}</p>`,\n )\n }\n if (this.helperText && messages.length === 0) {\n messages.push(\n html`<p class=\"message\" data-tone=\"helper\" part=\"message-helper\">${this.helperText}</p>`,\n )\n }\n return messages\n }\n\n // ── Event handlers ───────────────────────────────────────────\n #handleInput = (event: Event) => {\n const target = event.target as HTMLInputElement\n this.#rawText = target.value\n // Emit the parsed number (or undefined if empty / invalid).\n const parsed = this.#parseNumber(target.value)\n if (this.value === undefined) {\n // Uncontrolled: store internally, do not mutate this.value\n // because the contract is \"controlled by the consumer\".\n this.#syncFormValue()\n this.dispatchEvent(\n new CustomEvent<number | undefined>('sken-input', {\n detail: parsed,\n bubbles: true,\n composed: true,\n }),\n )\n } else {\n this.#syncFormValue()\n this.dispatchEvent(\n new CustomEvent<number | undefined>('sken-input', {\n detail: parsed,\n bubbles: true,\n composed: true,\n }),\n )\n }\n }\n\n #handleChange = (event: Event) => {\n const target = event.target as HTMLInputElement\n const parsed = this.#parseNumber(target.value)\n // Clamp on commit. If the user typed 150 with max=100, we\n // commit 100 and update the input text.\n const clamped = this.#clamp(parsed)\n if (clamped !== parsed) {\n this.#rawText = clamped !== undefined ? String(clamped) : ''\n target.value = this.#rawText\n }\n this.#syncFormValue()\n this.dispatchEvent(\n new CustomEvent<number | undefined>('sken-change', {\n detail: clamped,\n bubbles: true,\n composed: true,\n }),\n )\n }\n\n #handleFocus = (_event: FocusEvent) => {\n this.dispatchEvent(new CustomEvent('sken-focus', { bubbles: true, composed: true }))\n }\n\n #handleBlur = (_event: FocusEvent) => {\n this.dispatchEvent(new CustomEvent('sken-blur', { bubbles: true, composed: true }))\n }\n\n #handleKeydown = (event: KeyboardEvent) => {\n if (this.disabled || this.readonly) return\n // Shift+Arrow and PageUp/PageDown apply a 10x multiplier to\n // the step. This is the standard \"fast forward\" pattern in\n // numeric inputs across Mature DS (iX, Material, AntD). It\n // lets the consumer reach the target faster when the step\n // is small relative to the typical range.\n const multiplier = event.shiftKey || event.key === 'PageUp' || event.key === 'PageDown' ? 10 : 1\n if (event.key === 'ArrowUp') {\n event.preventDefault()\n this.#step(+1, multiplier)\n } else if (event.key === 'ArrowDown') {\n event.preventDefault()\n this.#step(-1, multiplier)\n } else if (event.key === 'PageUp') {\n event.preventDefault()\n this.#step(+1, 10)\n } else if (event.key === 'PageDown') {\n event.preventDefault()\n this.#step(-1, 10)\n } else if (event.key === 'Home' && this.min !== undefined) {\n event.preventDefault()\n this.#commitValue(this.min)\n } else if (event.key === 'End' && this.max !== undefined) {\n event.preventDefault()\n this.#commitValue(this.max)\n } else if (event.key === 'Enter') {\n // The browser fires a `change` event on Enter for\n // <input type=\"number\">, so we don't need to commit\n // anything manually. We DO need to bridge the shadow\n // boundary so the surrounding <form> receives a submit\n // event — the browser's implicit submission algorithm\n // does not see the inner input. Mirrors SkenInput and\n // SkenTextarea. `isComposing` guards IME composition.\n if (event.isComposing) return\n // preventDefault runs even without ElementInternals,\n // keeping the observable behaviour consistent.\n event.preventDefault()\n const form = this.#internals?.form\n if (!form) return\n form.requestSubmit()\n }\n }\n\n #handleStepUp = () => this.#step(+1)\n #handleStepDown = () => this.#step(-1)\n\n // ── Helpers ───────────────────────────────────────────────────\n /**\n * Increment / decrement the value by `step * multiplier`,\n * clamped at [min, max]. Emits `sken-input` and `sken-change`\n * (we treat the stepper as a commit, not a keystroke). The\n * `multiplier` defaults to 1; the keyboard handler passes 10\n * for Shift+Arrow and PageUp/PageDown (the \"fast forward\"\n * pattern).\n *\n * The current value is read from the rendered <input>, NOT\n * from `this.value`. Reading from the input is robust for\n * both modes:\n * - Controlled: the consumer is async (Vue's reactive update\n * applies on the next tick). By the time the second click\n * arrives, the consumer's `value` prop may still be the\n * pre-click value, but the input's `.value` is already\n * updated by #commitValue from the first click.\n * - Uncontrolled: the primitive owns the value. The input's\n * `.value` is the source of truth; `this.value` is the\n * initial seed and not maintained by the primitive in this\n * mode.\n */\n #step(direction: 1 | -1, multiplier: number = 1) {\n if (this.disabled || this.readonly) return\n const inputEl = this.renderRoot.querySelector('input') as HTMLInputElement | null\n const text = inputEl?.value ?? ''\n const current = text === '' ? (this.value ?? 0) : Number(text)\n if (!Number.isFinite(current)) return\n const next = current + direction * this.step * multiplier\n this.#commitValue(next)\n }\n\n /**\n * Commit a value: clamp, update internal state, emit events.\n * Same path used by the stepper buttons and the keyboard\n * Home / End shortcuts.\n */\n #commitValue(raw: number) {\n const clamped = this.#clamp(raw)\n this.#rawText = clamped !== undefined ? String(clamped) : ''\n // Update the DOM input so the next @input sees the new value\n const inputEl = this.renderRoot.querySelector('input') as HTMLInputElement | null\n if (inputEl) inputEl.value = this.#rawText\n this.#syncFormValue()\n this.dispatchEvent(\n new CustomEvent<number | undefined>('sken-input', {\n detail: clamped,\n bubbles: true,\n composed: true,\n }),\n )\n this.dispatchEvent(\n new CustomEvent<number | undefined>('sken-change', {\n detail: clamped,\n bubbles: true,\n composed: true,\n }),\n )\n }\n\n #clamp(value: number | undefined): number | undefined {\n if (value === undefined || Number.isNaN(value)) return undefined\n let v = value\n if (this.min !== undefined && v < this.min) v = this.min\n if (this.max !== undefined && v > this.max) v = this.max\n return v\n }\n\n #parseNumber(text: string): number | undefined {\n if (text === '' || text === '-') return undefined\n const n = Number(text)\n if (Number.isNaN(n)) return undefined\n return n\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'sken-number-input': SkenNumberInput\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"sken-number-input.js","names":["#rawText","#parseNumber","#syncFormValue","#clamp","#step","#commitValue","#internals","#slotEndObserver","#visibilityObserver","#adjustLayout","#renderMessages","#handleInput","#handleChange","#handleFocus","#handleBlur","#handleKeydown","#handleStepUp","#handleStepDown"],"sources":["../src/components/sken-number-input.ts"],"sourcesContent":["// ── <sken-number-input> — Sken-owned Web Component ─────────────────\n// Lit 3 primitive for numeric input with visible ± stepper buttons,\n// min/max clamping, and the same start/end slot system as\n// <sken-input>. Promoted from the SkenNumberInput PoC story (which\n// was a thin wrap of <ix-number-input>). The PoC proved the UX;\n// this primitive owns the contract.\n\nimport type { SkenNumberInputSize } from '@sken-ds/contracts'\nimport { LitElement, css, html, nothing, type PropertyValues } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\n\n/**\n * Default step when the consumer does not pass one. Matches the\n * native `<input type=\"number\" step>` default.\n */\nconst DEFAULT_STEP = 1\n\n/**\n * Default value for `showStepperButtons`. The PoC validated that\n * the iX default of `false` is wrong for our use case: consumers\n * expect ± buttons for the numeric fields they use most (Alma\n * demo: quantity, temperature, score). The Sken primitive flips\n * the default to `true`; consumers who want a bare input pass\n * `showStepperButtons={false}` explicitly.\n */\nconst DEFAULT_SHOW_STEPPER_BUTTONS = true\n\n@customElement('sken-number-input')\nexport class SkenNumberInput extends LitElement {\n // Form-associated custom element. Same pattern as SkenInput /\n // SkenTextarea. The numeric value is stringified for\n // FormData (FormData is text-only); undefined becomes the\n // empty string. The implicit-submit-on-Enter rule still\n // applies, so a form with a single number input and a\n // submit button will submit on Enter.\n // https://lit.dev/docs/components/form/\n static formAssociated = true\n\n // ElementInternals handle. Created in the constructor.\n // Nullable to gracefully degrade in test environments.\n #internals: ElementInternals | undefined = undefined\n\n constructor() {\n super()\n try {\n this.#internals = this.attachInternals()\n } catch {\n this.#internals = undefined\n }\n }\n\n // ── Props ─────────────────────────────────────────────────────\n @property({ attribute: 'default-value', type: Number }) defaultValue: number | undefined =\n undefined\n @property({ type: Number }) value: number | undefined = undefined\n @property({ type: Number }) min: number | undefined = undefined\n @property({ type: Number }) max: number | undefined = undefined\n @property({ type: Number }) step: number = DEFAULT_STEP\n @property({ attribute: 'show-stepper-buttons', reflect: true, type: Boolean })\n showStepperButtons = DEFAULT_SHOW_STEPPER_BUTTONS\n @property() placeholder: string | undefined = undefined\n @property() label: string | undefined = undefined\n @property({ reflect: true, type: Boolean }) required = false\n @property({ reflect: true, type: Boolean }) disabled = false\n @property({ reflect: true, type: Boolean }) readonly = false\n @property({ attribute: 'helper-text' }) helperText: string | undefined = undefined\n @property({ attribute: 'info-text' }) infoText: string | undefined = undefined\n @property({ attribute: 'warning-text' }) warningText: string | undefined = undefined\n @property({ attribute: 'valid-text' }) validText: string | undefined = undefined\n @property({ attribute: 'invalid-text' }) invalidText: string | undefined = undefined\n @property({ attribute: 'text-alignment' }) textAlignment: 'start' | 'end' = 'end'\n @property({ reflect: true }) size: SkenNumberInputSize = 'md'\n @property({ reflect: true, type: Boolean }) invalid = false\n @property({ attribute: 'described-by-id' }) describedById: string | undefined = undefined\n @property() name: string | undefined = undefined\n\n // ── Internal state ────────────────────────────────────────────\n /**\n * The raw text in the input. The DOM input is the source of\n * truth for the text (so the user can type a partial value like\n * \"-\", which is not a valid number yet). The numeric `value` is\n * derived from this on commit (blur, Enter, stepper).\n */\n #rawText: string = ''\n\n // ── DOM refs (queried on demand) ────────────────────────────\n\n // ── Lifecycle ─────────────────────────────────────────────────\n override connectedCallback(): void {\n super.connectedCallback()\n // Seed the raw text from the controlled value or the default.\n // We do NOT trigger a re-render here; the property is only\n // set after Lit has applied the host attributes, and the\n // first render will read the right value through this.value.\n if (this.value === undefined) {\n this.#rawText = this.defaultValue !== undefined ? String(this.defaultValue) : ''\n } else {\n this.#rawText = String(this.value)\n }\n // Publish the initial value to the surrounding form.\n this.#syncFormValue()\n }\n\n override disconnectedCallback(): void {\n super.disconnectedCallback()\n this.#slotEndObserver?.disconnect()\n this.#visibilityObserver?.disconnect()\n }\n\n protected override willUpdate(changedProperties: PropertyValues<this>): void {\n if (!changedProperties.has('value')) return\n if (this.value === undefined) {\n this.#rawText = this.defaultValue !== undefined ? String(this.defaultValue) : ''\n } else if (this.#parseNumber(this.#rawText) !== this.value) {\n this.#rawText = String(this.value)\n }\n }\n\n // ── Form-association callbacks ─────────────────────────────\n // formResetCallback: restore uncontrolled seed; re-render\n // controlled so Lit re-publishes the controlled value.\n formResetCallback(): void {\n if (this.value === undefined) {\n this.#rawText = this.defaultValue !== undefined ? String(this.defaultValue) : ''\n const inputEl = this.renderRoot.querySelector('input') as HTMLInputElement | null\n if (inputEl) inputEl.value = this.#rawText\n this.#syncFormValue()\n } else {\n this.requestUpdate()\n }\n }\n\n // formDisabledCallback: mirror the form's :disabled state\n // onto the prop. Lit re-renders; the inner <input> picks up\n // ?disabled=${this.disabled} in the template.\n formDisabledCallback(isDisabled: boolean): void {\n this.disabled = isDisabled\n }\n\n // Publish the current value to the form. FormData is\n // text-only, so we stringify the number (or empty string\n // for undefined). The string roundtrip preserves precision\n // for integers and finite decimals; consumers that need\n // BigInt or arbitrary precision should re-parse on the\n // server side.\n #syncFormValue(): void {\n if (!this.#internals) return\n const value = this.value !== undefined ? String(this.value) : ''\n this.#internals.setFormValue(value)\n }\n\n /**\n * Set up the dynamic layout once the shadow root has the\n * element children we need to measure. Same pattern as\n * SkenInput: MutationObserver on the slot container to\n * catch children being added/removed, IntersectionObserver\n * to catch the case where the input is hidden at first\n * connect (e.g. inside a closed <details> or an off-screen\n * tab), and slotchange as belt-and-braces.\n */\n protected override firstUpdated(): void {\n const endContainer = this.renderRoot.querySelector('.slot-end') as HTMLElement | null\n const endSlot = this.renderRoot.querySelector('slot[name=\"end\"]') as HTMLSlotElement | null\n if (endContainer && endSlot) {\n this.#slotEndObserver = new MutationObserver(() => this.#adjustLayout())\n this.#slotEndObserver.observe(endContainer, {\n childList: true,\n subtree: true,\n attributes: true,\n })\n endSlot.addEventListener('slotchange', this.#adjustLayout)\n }\n this.#visibilityObserver = new IntersectionObserver((entries) => {\n for (const entry of entries) {\n if (entry.isIntersecting) this.#adjustLayout()\n }\n })\n this.#visibilityObserver.observe(this)\n this.#adjustLayout()\n }\n\n /**\n * The gap between a slot's edge and the input's text. 0.5rem\n * is what the SkenInput primitive uses (mirrors the iX rule).\n * The number input needs an extra \"air\" margin because the\n * stepper column is also on the right edge and we don't want\n * the value text to crash into the slot.\n */\n #SLOT_AIR = '0.5rem'\n\n /**\n * Measure the stepper column width and the end slot width, then\n * apply:\n * - The input's padding-inline-end so the value text never\n * overlaps the slot OR the stepper.\n * - The end slot's inset-inline-end so it sits to the LEFT of\n * the stepper (or at the right edge when no stepper).\n *\n * The math:\n * endInset = stepperReserve + air (slot's right edge)\n * paddingInline = max(endInset, stepperReserve) + air\n * If the end slot is empty, its width is 0 and the calc\n * falls back to the stepper-only reservation.\n */\n #adjustLayout = (): void => {\n const inputEl = this.renderRoot.querySelector('input') as HTMLInputElement | null\n const stepperEl = this.renderRoot.querySelector('.steppers') as HTMLElement | null\n const slotEndEl = this.renderRoot.querySelector('.slot-end') as HTMLElement | null\n if (!inputEl || !slotEndEl) return\n\n requestAnimationFrame(() => {\n if (!inputEl.isConnected) return\n const stepperWidth =\n stepperEl && this.showStepperButtons ? stepperEl.getBoundingClientRect().width : 0\n const endWidth = slotEndEl.getBoundingClientRect().width\n const air = parseFloat(getComputedStyle(inputEl).fontSize) * 0.5 // 0.5em in px\n const stepperReserve = stepperWidth\n // The slot's right edge sits at stepperReserve + air from\n // the input's right edge, so it never overlaps the stepper\n // column.\n const endInset = stepperReserve + air\n slotEndEl.style.insetInlineEnd = endInset > 0 ? `${endInset}px` : '0.5rem'\n // The input's padding-inline-end has to be at least the\n // wider of (stepperReserve, endInset + endWidth) so the\n // value text never overlaps either.\n const textReserve = endInset + endWidth\n const padEnd = Math.max(stepperReserve, textReserve) + air\n inputEl.style.paddingInlineEnd = `${padEnd}px`\n })\n }\n\n #slotEndObserver: MutationObserver | undefined = undefined\n #visibilityObserver: IntersectionObserver | undefined = undefined\n\n // ── Styles ────────────────────────────────────────────────────\n static styles = css`\n :host {\n display: inline-block;\n inline-size: 100%;\n }\n\n .field {\n display: grid;\n gap: 0.375rem;\n }\n\n .label {\n font-size: 0.8125rem;\n font-weight: 500;\n color: var(--sken-foreground);\n }\n\n .label[data-required='true']::after {\n content: ' *';\n color: var(--sken-destructive);\n }\n\n .input-wrapper {\n position: relative;\n display: flex;\n align-items: center;\n inline-size: 100%;\n }\n\n input {\n box-sizing: border-box;\n inline-size: 100%;\n border: 1px solid var(--sken-border);\n border-radius: var(--sken-md);\n background: var(--sken-input);\n color: var(--sken-foreground);\n font-family: var(--sken-family-sans);\n font-size: var(--sken-size-md);\n line-height: 1.25;\n text-overflow: ellipsis;\n padding-block: var(--sken-2);\n padding-inline: var(--sken-3);\n /* When the ± steppers are visible, reserve the right edge\n for them so the value text never overlaps. 1.5rem (button)\n + 2 * 0.25rem (insets) + 0.25rem (air) = 2.25rem. */\n padding-inline-end: var(--sken-3);\n min-block-size: 2.25rem;\n transition:\n border-color 120ms ease,\n box-shadow 120ms ease;\n /* Hide the native steppers: we render our own. */\n -moz-appearance: textfield;\n }\n\n input::-webkit-outer-spin-button,\n input::-webkit-inner-spin-button {\n -webkit-appearance: none;\n margin: 0;\n }\n\n input::placeholder {\n color: var(--sken-muted-foreground);\n opacity: 1;\n }\n\n /* ── Stepper buttons ───────────────────────────────────── */\n /*\n * The stepper column is a vertical pair of + and − buttons\n * anchored to the right edge of the input. Sizes scale with\n * the host's [size] attribute (sm / md / lg) so the buttons\n * match the input's vertical rhythm. Default (md) is 24x20\n * per button. Glyph color is --sken-foreground; hover swaps\n * to --sken-primary. Disabled state uses --sken-disabled.\n */\n .steppers {\n position: absolute;\n inset-block: 0.25rem;\n inset-inline-end: 0.25rem;\n display: flex;\n flex-direction: column;\n gap: 2px;\n align-items: center;\n justify-content: center;\n }\n\n .stepper {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n inline-size: 1.5rem;\n block-size: 1.25rem;\n background: transparent;\n border: 0;\n padding: 0;\n color: var(--sken-foreground);\n cursor: pointer;\n font-family: inherit;\n font-size: 1rem;\n line-height: 1;\n font-weight: 600;\n border-radius: var(--sken-sm, 0.25rem);\n /* The parent .steppers is a flex column; prevent the\n buttons from being squashed when the host's vertical\n rhythm is tight (sm). The explicit block-size wins. */\n flex-shrink: 0;\n transition:\n color 120ms ease,\n background-color 120ms ease;\n }\n\n .stepper:hover:not(:disabled) {\n color: var(--sken-primary);\n /* No background fill on hover. A background would paint\n over the input's right border, hiding it. The color\n change to --sken-primary is enough affordance for a\n 20x24px icon button. */\n background: transparent;\n }\n\n .stepper:focus-visible {\n outline: 2px solid var(--sken-ring, currentColor);\n outline-offset: 1px;\n }\n\n .stepper:disabled {\n cursor: not-allowed;\n opacity: var(--sken-disabled);\n }\n\n /* When the ± steppers are visible, reserve the right edge\n of the input so the value text never overlaps. The\n reservation is 2rem for the md size (default): 1.5rem\n (button) + 2 * 0.25rem (column insets). The sm / lg\n variants below override the button size, so the\n reservation needs to scale too. */\n :host([show-stepper-buttons]) input {\n padding-inline-end: 2rem;\n }\n\n /* ── Stepper sizes ─────────────────────────────────────── */\n :host([size='sm']) .steppers {\n inset-block: 0.125rem;\n inset-inline-end: 0.125rem;\n gap: 1px;\n }\n :host([size='sm']) .stepper {\n inline-size: 1.125rem;\n block-size: 0.875rem;\n font-size: 0.75rem;\n }\n /* sm: 1.125rem (button) + 2 * 0.125rem (insets) = 1.375rem,\n plus 0.125rem of breathing air = 1.5rem. */\n :host([size='sm'][show-stepper-buttons]) input {\n padding-inline-end: 1.5rem;\n }\n\n :host([size='lg']) .steppers {\n inset-block: 0.375rem;\n inset-inline-end: 0.375rem;\n gap: 3px;\n }\n :host([size='lg']) .stepper {\n inline-size: 1.75rem;\n block-size: 1.5rem;\n font-size: 1.125rem;\n }\n /* lg: 1.75rem (button) + 2 * 0.375rem (insets) = 2.5rem,\n plus 0.25rem of breathing air = 2.75rem. */\n :host([size='lg'][show-stepper-buttons]) input {\n padding-inline-end: 2.75rem;\n }\n\n /* Hide steppers on touch devices where they would be hard\n to hit accurately. The user can still use ↑/↓ keys or\n type directly. */\n @media (hover: none) {\n .steppers {\n display: none;\n }\n input {\n padding-inline-end: var(--sken-3);\n }\n :host([size='sm']) input {\n padding-inline-end: var(--sken-2);\n }\n :host([size='lg']) input {\n padding-inline-end: var(--sken-4);\n }\n }\n\n /* ── Slot containers ────────────────────────────────────── */\n .slot {\n position: absolute;\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 1;\n pointer-events: none;\n color: var(--sken-muted-foreground);\n }\n\n .slot ::slotted(*) {\n pointer-events: auto;\n }\n\n .slot-start {\n inset-inline-start: 0.5rem;\n }\n\n /*\n * The end slot's position is set in JS via the inline style\n * (see #adjustLayout). The rule below is a fallback for the\n * initial render before JS has measured the slot, and a\n * sensible default when the slot is empty.\n */\n .slot-end {\n inset-inline-end: 0.5rem;\n }\n\n /* ── States ─────────────────────────────────────────────── */\n .input-wrapper:hover input:not(:disabled):not(:read-only) {\n border-color: var(--sken-foreground);\n }\n\n .input-wrapper:focus-within input {\n outline: 2px solid var(--sken-ring, currentColor);\n outline-offset: 2px;\n border-color: var(--sken-primary);\n }\n\n input:disabled {\n cursor: not-allowed;\n opacity: var(--sken-disabled);\n }\n\n input:read-only {\n background: var(--sken-muted);\n }\n\n :host([invalid]) input {\n border-color: var(--sken-destructive);\n }\n\n :host([invalid]) .input-wrapper:focus-within input {\n outline-color: var(--sken-destructive);\n }\n\n /* ── Sizes ──────────────────────────────────────────────── */\n :host([size='sm']) input {\n padding-block: var(--sken-1);\n padding-inline: var(--sken-2);\n font-size: var(--sken-size-sm);\n /* 2 × 0.875rem (buttons) + 1px (gap) + 2 × 0.125rem\n (insets) = ~2.0625rem. Round up to 2.25rem for visual\n breathing room. */\n min-block-size: 2.25rem;\n }\n :host([size='md']) input {\n /* Default styles above. */\n }\n :host([size='lg']) input {\n padding-block: var(--sken-3);\n padding-inline: var(--sken-4);\n font-size: var(--sken-size-lg);\n /* 2 × 1.5rem (buttons) + 3px (gap) + 2 × 0.375rem\n (insets) = ~3.9375rem. Round up to 4rem. */\n min-block-size: 4rem;\n }\n\n /* ── Validation messages ─────────────────────────────────── */\n .messages {\n display: grid;\n gap: 0.25rem;\n }\n\n .message {\n margin: 0;\n font-size: 0.75rem;\n line-height: 1.4;\n }\n\n .message[data-tone='info'] {\n color: var(--sken-info, #0082ff);\n }\n\n .message[data-tone='warning'] {\n color: var(--sken-warning, #f59e0b);\n }\n\n .message[data-tone='valid'] {\n color: var(--sken-success, #10b981);\n }\n\n .message[data-tone='invalid'] {\n color: var(--sken-destructive, #dc2626);\n }\n\n .message[data-tone='helper'] {\n color: var(--sken-muted-foreground);\n }\n `\n\n // ── Render ────────────────────────────────────────────────────\n protected override render() {\n const messages = this.#renderMessages()\n return html`\n <div class=\"field\" part=\"field\">\n ${\n this.label\n ? html`\n <label class=\"label\" part=\"label\" data-required=${this.required}>\n ${this.label}\n </label>\n `\n : nothing\n }\n <div class=\"input-wrapper\" part=\"wrapper\">\n <div class=\"slot slot-start\" part=\"slot-start\">\n <slot name=\"start\"></slot>\n </div>\n <input\n part=\"input\"\n type=\"number\"\n .value=${this.#rawText}\n placeholder=${this.placeholder ?? ''}\n ?disabled=${this.disabled}\n ?readonly=${this.readonly}\n ?required=${this.required}\n min=${this.min ?? ''}\n max=${this.max ?? ''}\n step=${this.step}\n aria-invalid=${this.invalid ? 'true' : 'false'}\n aria-describedby=${this.describedById ?? ''}\n name=${this.name ?? ''}\n style=\"text-align: ${this.textAlignment}\"\n @input=${this.#handleInput}\n @change=${this.#handleChange}\n @focus=${this.#handleFocus}\n @blur=${this.#handleBlur}\n @keydown=${this.#handleKeydown}\n />\n <div class=\"slot slot-end\" part=\"slot-end\">\n <slot name=\"end\"></slot>\n </div>\n ${\n this.showStepperButtons\n ? html`\n <div class=\"steppers\" part=\"steppers\">\n <button\n class=\"stepper\"\n part=\"stepper stepper-up\"\n type=\"button\"\n aria-label=\"Increment\"\n ?disabled=${\n this.disabled ||\n this.readonly ||\n (this.value !== undefined &&\n this.max !== undefined &&\n this.value >= this.max)\n }\n @click=${this.#handleStepUp}\n >\n +\n </button>\n <button\n class=\"stepper\"\n part=\"stepper stepper-down\"\n type=\"button\"\n aria-label=\"Decrement\"\n ?disabled=${\n this.disabled ||\n this.readonly ||\n (this.value !== undefined &&\n this.min !== undefined &&\n this.value <= this.min)\n }\n @click=${this.#handleStepDown}\n >\n −\n </button>\n </div>\n `\n : nothing\n }\n </div>\n ${\n messages.length > 0\n ? html` <div class=\"messages\" part=\"messages\">${messages}</div> `\n : nothing\n }\n </div>\n `\n }\n\n #renderMessages() {\n const messages: ReturnType<typeof html>[] = []\n if (this.invalidText) {\n messages.push(\n html`<p class=\"message\" data-tone=\"invalid\" part=\"message-invalid\">${this.invalidText}</p>`,\n )\n }\n if (this.warningText) {\n messages.push(\n html`<p class=\"message\" data-tone=\"warning\" part=\"message-warning\">${this.warningText}</p>`,\n )\n }\n if (this.infoText) {\n messages.push(\n html`<p class=\"message\" data-tone=\"info\" part=\"message-info\">${this.infoText}</p>`,\n )\n }\n if (this.validText) {\n messages.push(\n html`<p class=\"message\" data-tone=\"valid\" part=\"message-valid\">${this.validText}</p>`,\n )\n }\n if (this.helperText && messages.length === 0) {\n messages.push(\n html`<p class=\"message\" data-tone=\"helper\" part=\"message-helper\">${this.helperText}</p>`,\n )\n }\n return messages\n }\n\n // ── Event handlers ───────────────────────────────────────────\n #handleInput = (event: Event) => {\n const target = event.target as HTMLInputElement\n this.#rawText = target.value\n // Emit the parsed number (or undefined if empty / invalid).\n const parsed = this.#parseNumber(target.value)\n if (this.value === undefined) {\n // Uncontrolled: store internally, do not mutate this.value\n // because the contract is \"controlled by the consumer\".\n this.#syncFormValue()\n this.dispatchEvent(\n new CustomEvent<number | undefined>('sken-input', {\n detail: parsed,\n bubbles: true,\n composed: true,\n }),\n )\n } else {\n this.#syncFormValue()\n this.dispatchEvent(\n new CustomEvent<number | undefined>('sken-input', {\n detail: parsed,\n bubbles: true,\n composed: true,\n }),\n )\n }\n }\n\n #handleChange = (event: Event) => {\n const target = event.target as HTMLInputElement\n const parsed = this.#parseNumber(target.value)\n // Clamp on commit. If the user typed 150 with max=100, we\n // commit 100 and update the input text.\n const clamped = this.#clamp(parsed)\n if (clamped !== parsed) {\n this.#rawText = clamped !== undefined ? String(clamped) : ''\n target.value = this.#rawText\n }\n this.#syncFormValue()\n this.dispatchEvent(\n new CustomEvent<number | undefined>('sken-change', {\n detail: clamped,\n bubbles: true,\n composed: true,\n }),\n )\n }\n\n #handleFocus = (_event: FocusEvent) => {\n this.dispatchEvent(new CustomEvent('sken-focus', { bubbles: true, composed: true }))\n }\n\n #handleBlur = (_event: FocusEvent) => {\n this.dispatchEvent(new CustomEvent('sken-blur', { bubbles: true, composed: true }))\n }\n\n #handleKeydown = (event: KeyboardEvent) => {\n if (this.disabled || this.readonly) return\n // Shift+Arrow and PageUp/PageDown apply a 10x multiplier to\n // the step. This is the standard \"fast forward\" pattern in\n // numeric inputs across Mature DS (iX, Material, AntD). It\n // lets the consumer reach the target faster when the step\n // is small relative to the typical range.\n const multiplier = event.shiftKey || event.key === 'PageUp' || event.key === 'PageDown' ? 10 : 1\n if (event.key === 'ArrowUp') {\n event.preventDefault()\n this.#step(+1, multiplier)\n } else if (event.key === 'ArrowDown') {\n event.preventDefault()\n this.#step(-1, multiplier)\n } else if (event.key === 'PageUp') {\n event.preventDefault()\n this.#step(+1, 10)\n } else if (event.key === 'PageDown') {\n event.preventDefault()\n this.#step(-1, 10)\n } else if (event.key === 'Home' && this.min !== undefined) {\n event.preventDefault()\n this.#commitValue(this.min)\n } else if (event.key === 'End' && this.max !== undefined) {\n event.preventDefault()\n this.#commitValue(this.max)\n } else if (event.key === 'Enter') {\n // The browser fires a `change` event on Enter for\n // <input type=\"number\">, so we don't need to commit\n // anything manually. We DO need to bridge the shadow\n // boundary so the surrounding <form> receives a submit\n // event — the browser's implicit submission algorithm\n // does not see the inner input. Mirrors SkenInput and\n // SkenTextarea. `isComposing` guards IME composition.\n if (event.isComposing) return\n // preventDefault runs even without ElementInternals,\n // keeping the observable behaviour consistent.\n event.preventDefault()\n const form = this.#internals?.form\n if (!form) return\n form.requestSubmit()\n }\n }\n\n #handleStepUp = () => this.#step(+1)\n #handleStepDown = () => this.#step(-1)\n\n // ── Helpers ───────────────────────────────────────────────────\n /**\n * Increment / decrement the value by `step * multiplier`,\n * clamped at [min, max]. Emits `sken-input` and `sken-change`\n * (we treat the stepper as a commit, not a keystroke). The\n * `multiplier` defaults to 1; the keyboard handler passes 10\n * for Shift+Arrow and PageUp/PageDown (the \"fast forward\"\n * pattern).\n *\n * The current value is read from the rendered <input>, NOT\n * from `this.value`. Reading from the input is robust for\n * both modes:\n * - Controlled: the consumer is async (Vue's reactive update\n * applies on the next tick). By the time the second click\n * arrives, the consumer's `value` prop may still be the\n * pre-click value, but the input's `.value` is already\n * updated by #commitValue from the first click.\n * - Uncontrolled: the primitive owns the value. The input's\n * `.value` is the source of truth; `this.value` is the\n * initial seed and not maintained by the primitive in this\n * mode.\n */\n #step(direction: 1 | -1, multiplier: number = 1) {\n if (this.disabled || this.readonly) return\n const inputEl = this.renderRoot.querySelector('input') as HTMLInputElement | null\n const text = inputEl?.value ?? ''\n const current = text === '' ? (this.value ?? 0) : Number(text)\n if (!Number.isFinite(current)) return\n const next = current + direction * this.step * multiplier\n this.#commitValue(next)\n }\n\n /**\n * Commit a value: clamp, update internal state, emit events.\n * Same path used by the stepper buttons and the keyboard\n * Home / End shortcuts.\n */\n #commitValue(raw: number) {\n const clamped = this.#clamp(raw)\n this.#rawText = clamped !== undefined ? String(clamped) : ''\n // Update the DOM input so the next @input sees the new value\n const inputEl = this.renderRoot.querySelector('input') as HTMLInputElement | null\n if (inputEl) inputEl.value = this.#rawText\n this.#syncFormValue()\n this.dispatchEvent(\n new CustomEvent<number | undefined>('sken-input', {\n detail: clamped,\n bubbles: true,\n composed: true,\n }),\n )\n this.dispatchEvent(\n new CustomEvent<number | undefined>('sken-change', {\n detail: clamped,\n bubbles: true,\n composed: true,\n }),\n )\n }\n\n #clamp(value: number | undefined): number | undefined {\n if (value === undefined || Number.isNaN(value)) return undefined\n let v = value\n if (this.min !== undefined && v < this.min) v = this.min\n if (this.max !== undefined && v > this.max) v = this.max\n return v\n }\n\n #parseNumber(text: string): number | undefined {\n if (text === '' || text === '-') return undefined\n const n = Number(text)\n if (Number.isNaN(n)) return undefined\n return n\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'sken-number-input': SkenNumberInput\n }\n}\n"],"mappings":"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"}
@@ -8,7 +8,7 @@ var o = class extends t {
8
8
  }
9
9
  #e;
10
10
  constructor() {
11
- super(), this.#e = null, this.size = "md", this.rows = 4, this.cols = void 0, this.maxLength = void 0, this.resize = "vertical", this.placeholder = void 0, this.value = void 0, this.defaultValue = void 0, this.disabled = !1, this.readonly = !1, this.required = !1, this.invalid = !1, this.describedById = void 0, this.name = void 0, this.#t = "", this.#r = (e) => {
11
+ super(), this.#e = void 0, this.size = "md", this.rows = 4, this.cols = void 0, this.maxLength = void 0, this.resize = "vertical", this.placeholder = void 0, this.value = void 0, this.defaultValue = void 0, this.disabled = !1, this.readonly = !1, this.required = !1, this.invalid = !1, this.describedById = void 0, this.name = void 0, this.#t = "", this.#r = (e) => {
12
12
  let t = e.target.value;
13
13
  this.value === void 0 && (this.#t = t), this.#n(), this.dispatchEvent(new CustomEvent("sken-input", {
14
14
  detail: t,
@@ -43,7 +43,7 @@ var o = class extends t {
43
43
  try {
44
44
  this.#e = this.attachInternals();
45
45
  } catch {
46
- this.#e = null;
46
+ this.#e = void 0;
47
47
  }
48
48
  }
49
49
  #t;