@sken-ds/primitives 0.3.8 → 0.4.1
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.
- package/dist/index.js +1 -1
- package/dist/sken-combobox.js +29 -29
- package/dist/sken-combobox.js.map +1 -1
- package/dist/{sken-dialog-CLqZu1WQ.js → sken-dialog-BvRZxwSk.js} +8 -8
- package/dist/{sken-dialog-CLqZu1WQ.js.map → sken-dialog-BvRZxwSk.js.map} +1 -1
- package/dist/sken-dialog.js +1 -1
- package/dist/sken-layout-region.js.map +1 -1
- package/dist/sken-layout.js +124 -117
- package/dist/sken-layout.js.map +1 -1
- package/dist/sken-number-input.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import { SkenComboboxItem as i } from "./sken-combobox-item.js";
|
|
|
6
6
|
import { SkenFilterChip as a } from "./sken-filter-chip.js";
|
|
7
7
|
import { SkenCombobox as o } from "./sken-combobox.js";
|
|
8
8
|
import { SkenDatePicker as s } from "./sken-date-picker.js";
|
|
9
|
-
import { t as c } from "./sken-dialog-
|
|
9
|
+
import { t as c } from "./sken-dialog-BvRZxwSk.js";
|
|
10
10
|
import { SkenInput as l } from "./sken-input.js";
|
|
11
11
|
import { SkenLayoutRegion as u } from "./sken-layout-region.js";
|
|
12
12
|
import { SkenLayout as d } from "./sken-layout.js";
|
package/dist/sken-combobox.js
CHANGED
|
@@ -486,21 +486,21 @@ var c = class extends t {
|
|
|
486
486
|
>
|
|
487
487
|
${o && s.length > 0 ? r`<div class="chip-row" part="chip-row" data-overflow=${this.chipOverflow}>
|
|
488
488
|
${c.map((e) => r`
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
489
|
+
<sken-filter-chip
|
|
490
|
+
label=${e.label}
|
|
491
|
+
data-sken-combobox-chip-value=${e.value}
|
|
492
|
+
@sken-close=${(t) => this._removeChip(e.value, t)}
|
|
493
|
+
></sken-filter-chip>
|
|
494
|
+
`)}
|
|
495
495
|
${l.length > 0 && this.chipOverflow === "count" ? r`<sken-filter-chip
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
496
|
+
class="overflow"
|
|
497
|
+
label="+${l.length}"
|
|
498
|
+
data-sken-combobox-overflow=${l.length}
|
|
499
|
+
hide-close-button
|
|
500
|
+
@click=${(e) => {
|
|
501
501
|
e.stopPropagation(), this._open = !0;
|
|
502
502
|
}}
|
|
503
|
-
|
|
503
|
+
></sken-filter-chip>` : null}
|
|
504
504
|
</div>` : null}
|
|
505
505
|
<div class="input-row">
|
|
506
506
|
<input
|
|
@@ -555,28 +555,28 @@ var c = class extends t {
|
|
|
555
555
|
aria-multiselectable=${n}
|
|
556
556
|
>
|
|
557
557
|
${e.length === 0 ? r`<li class="option" data-disabled="true">No matches</li>` : t.map((t) => r`
|
|
558
|
-
|
|
559
|
-
|
|
558
|
+
${t.label ? r`<li class="group-header" role="presentation">${t.label}</li>` : null}
|
|
559
|
+
${t.items.map((t) => {
|
|
560
560
|
let o = e.indexOf(t), s = o === this._activeIndex, c = i.has(t.value);
|
|
561
561
|
return r`<li
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
562
|
+
id=${`${a}-opt-${o}`}
|
|
563
|
+
class="option"
|
|
564
|
+
role="option"
|
|
565
|
+
data-active=${s}
|
|
566
|
+
data-selected=${c}
|
|
567
|
+
data-disabled=${t.disabled}
|
|
568
|
+
aria-selected=${c}
|
|
569
|
+
aria-disabled=${t.disabled}
|
|
570
|
+
@mouseenter=${() => {
|
|
571
571
|
this._activeIndex = o;
|
|
572
572
|
}}
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
573
|
+
@click=${() => this._selectItem(t)}
|
|
574
|
+
>
|
|
575
|
+
${n ? r`<span class="check" aria-hidden="true">✓</span>` : null}
|
|
576
|
+
<span>${t.label}</span>
|
|
577
|
+
</li>`;
|
|
578
578
|
})}
|
|
579
|
-
|
|
579
|
+
`)}
|
|
580
580
|
</ul>` : null}
|
|
581
581
|
${this.helperText ? r`<div class="helper">${this.helperText}</div>` : null}
|
|
582
582
|
</div>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sken-combobox.js","names":[],"sources":["../src/components/sken-combobox.ts"],"sourcesContent":["/**\n * `<sken-combobox>` — Sken contract implementation.\n *\n * Single + multiple selection. Filterable. Groupable. Tokenised.\n * No third-party vendor in the runtime.\n *\n * The Sken contract (`SkenComboboxProps`, `SkenComboboxItemProps`,\n * `SkenComboboxGroupProps`, `SkenComboboxEmits`,\n * `SkenComboboxValue<M>`, `skenComboboxAria`) lives in\n * `@sken-ds/contracts`. This component is a Lit 3 implementation\n * that:\n *\n * 1. Accepts the Sken contract props (`mode`, `value`,\n * `filterable`, `label`, `helperText`, `placeholder`,\n * `disabled`, `invalid`, `required`, `name`, `size`,\n * `describedById`).\n * 2. Discovers the options at first render by introspecting\n * `<sken-combobox-item>` and `<sken-combobox-group>`\n * children. Each item knows its `value` and `label`; each\n * group knows its `label` and `disabled` state. Items\n * inside a group are associated with that group; items\n * outside any group belong to an implicit \"ungrouped\"\n * bucket rendered at the top of the listbox (no header).\n * 3. Renders a Shadow DOM with: a `<label>` for the visible\n * field label, an `<input role=\"combobox\">` for the\n * trigger, a clear button, a chevron, a listbox panel\n * containing the (optionally grouped, optionally filtered)\n * options, and helper / invalid / warning / valid text.\n * 4. Implements the WAI-ARIA combobox 1.2 pattern: the input\n * has `role=\"combobox\"`, `aria-expanded`, `aria-controls`\n * pointing to the listbox, and `aria-activedescendant`\n * pointing to the currently highlighted option. The listbox\n * has `role=\"listbox\"`, each option has `role=\"option\"` +\n * `aria-selected`, and each group has `role=\"group\"` +\n * `aria-labelledby` pointing to the group's header.\n * 5. Implements the keyboard model: ArrowUp / ArrowDown open\n * and navigate, Home / End jump to the ends, Enter selects,\n * Escape closes (or clears when already closed), Tab moves\n * focus out without selecting.\n * 6. Emits the Sken-shaped `sken-change` event with the new\n * value. The shape of the value depends on `mode`:\n * `string | null` for `single`, `string[]` for `multiple`.\n * Native `change` on the input is suppressed; consumers use\n * `sken-change` for portability.\n *\n * Implementation notes:\n *\n * - The children are introspected lazily on each render so\n * dynamic option lists (Vue v-for, conditional rendering)\n * work without manual registration.\n *\n * - The filter is a case-insensitive substring match against\n * `item.label`. Groups hide themselves when ALL their items\n * are filtered out. An empty filter restores every item.\n *\n * - The listbox is positioned absolutely below the input,\n * matching the input's width. The visual treatment is\n * tokenised via `--sken-*` custom properties; the consumer\n * imports `@sken-ds/theme/css` once at app boot.\n *\n * - The \"marker\" pattern for the children (items and groups\n * render nothing; the parent projects them) is documented in\n * the iX 5 reference and in the comments on the children\n * themselves. The trade-off is that `<sken-combobox-item>`\n * does not appear in the rendered DOM tree as itself.\n */\nimport { LitElement, css, html, type PropertyValues } from 'lit'\nimport { customElement, property, query, state } from 'lit/decorators.js'\n\nimport type {\n SkenComboboxChipOverflow,\n SkenComboboxDisplay,\n SkenComboboxMode,\n SkenComboboxProps,\n SkenComboboxValue,\n} from '@sken-ds/contracts'\n\n// Side-effect import: register the `<sken-filter-chip>`\n// custom element so the combobox can render chips inside its\n// shadow DOM without forcing the consumer to import the chip\n// module explicitly. The chip is a pure consumer-side primitive\n// of the combobox in `display=\"chips\"` mode, so it belongs in\n// the combobox's module graph.\nimport './sken-filter-chip.js'\n\nimport type { SkenComboboxGroup } from './sken-combobox-group'\nimport type { SkenComboboxItem } from './sken-combobox-item'\n\n/**\n * Internal flat model: each item is a row, optionally\n * associated with a group label. The listbox renders a flat\n * list; the group headers are rendered as separator rows when\n * the group becomes visible after filtering.\n */\ninterface ResolvedItem {\n value: string\n label: string\n disabled: boolean\n groupLabel: string | null\n groupDisabled: boolean\n}\n\n@customElement('sken-combobox')\nexport class SkenCombobox extends LitElement implements Omit<SkenComboboxProps, 'value' | 'slots'> {\n // The Sken contract declares `value` as `SkenComboboxValue`\n // (a generic defaulting to `string | null`). This\n // implementation uses `SkenComboboxValue<SkenComboboxMode>`\n // (the full union) so that single-mode and multi-mode\n // values can both flow through `_setValue` without TS\n // narrowing complaints. The runtime contract is\n // identical; the difference is only at the type level.\n // ── public API (mapped to Sken contract) ──────────────────────────\n\n @property({ reflect: true }) mode: SkenComboboxMode = 'single'\n\n /**\n * How the trigger renders the current selection in\n * `mode=\"multiple\"`. See `SkenComboboxDisplay`. No effect\n * in `mode=\"single\"`. @default \"count\"\n */\n @property() display: SkenComboboxDisplay = 'count'\n\n /**\n * How the chip row overflows when `display=\"chips\"` and\n * the chips do not fit on a single line. See\n * `SkenComboboxChipOverflow`. No effect in\n * `mode=\"single\"` or when `display=\"count\"`.\n * @default \"wrap\"\n */\n @property({ attribute: 'chip-overflow' }) chipOverflow: SkenComboboxChipOverflow = 'wrap'\n\n @property({ attribute: false })\n value: SkenComboboxValue<SkenComboboxMode> = null as SkenComboboxValue<SkenComboboxMode>\n\n @property({ type: Boolean }) filterable = true\n\n @property() label = ''\n\n @property({ attribute: 'helper-text' }) helperText = ''\n\n @property() placeholder = ''\n\n @property({ type: Boolean, reflect: true }) disabled = false\n\n @property({ type: Boolean, reflect: true }) invalid = false\n\n @property({ type: Boolean, reflect: true }) required = false\n\n @property() name = ''\n\n @property({ reflect: true }) size: 'sm' | 'md' | 'lg' = 'md'\n\n @property({ attribute: 'described-by-id' }) describedById = ''\n\n // ── internal state ────────────────────────────────────────────────\n\n /** Resolved flat model of all items, with their group labels. */\n @state() private _items: ResolvedItem[] = []\n\n /** Current filter query. Lowercased for case-insensitive match. */\n @state() private _query = ''\n\n /** Whether the listbox is open. */\n @state() private _open = false\n\n /** Index into the visible items (after filtering). -1 = no highlight. */\n @state() private _activeIndex = -1\n\n /**\n * Number of selected chips that fit on the first row of the\n * trigger. The rest are hidden behind the overflow chip in\n * `chipOverflow=\"count\"` mode. `-1` means \"no measurement\n * yet\" (or `chipOverflow` is not `\"count\"`).\n *\n * The measurement runs once per render in `updated()`. It\n * uses the actual rendered widths of the chip DOM nodes,\n * not a guess, so the overflow chip appears exactly when\n * the last visible chip would not fit. On window resize\n * the user re-renders (e.g. by typing, opening the\n * listbox, or any other state change) and the measurement\n * re-runs.\n */\n @state() private _visibleChipCount = -1\n\n @query('input') private _input!: HTMLInputElement\n\n // ── lifecycle ─────────────────────────────────────────────────────\n\n override connectedCallback(): void {\n super.connectedCallback()\n // Re-resolve on any DOM change inside the host. This is how\n // we pick up dynamic option lists (Vue v-for, etc.) without\n // the consumer having to call a register() method.\n this._observer = new MutationObserver(() => {\n this._resolveItems()\n // Force a re-render so the listbox reflects the new\n // items. Without this, mutations that happen after the\n // first render (e.g. Vue mounting children asynchronously)\n // would not trigger a render() pass.\n this.requestUpdate()\n })\n this._observer.observe(this, { childList: true, subtree: true })\n }\n\n override disconnectedCallback(): void {\n super.disconnectedCallback()\n this._observer?.disconnect()\n }\n\n override willUpdate(_changed: PropertyValues): void {\n // Resolving on every update is cheap (a few querySelectorAll\n // calls) and guarantees the model is fresh by the time\n // render() runs.\n this._resolveItems()\n }\n\n override firstUpdated(_changed: PropertyValues): void {\n // Belt-and-braces: re-resolve after the first render. Some\n // frameworks (notably Vue's h() with custom elements) may\n // append children to the host AFTER the Lit lifecycle has\n // run its first render, so the initial willUpdate sees an\n // empty children list. By the time firstUpdated runs, the\n // DOM is settled and a fresh resolve is authoritative.\n this._resolveItems()\n // Some bundlers (Vite) and frameworks (Vue) mount custom\n // element children asynchronously after the host's\n // `connectedCallback`. We poll once on the next microtask\n // to catch any children that were attached between the\n // synchronous render and the next paint. The cost is one\n // extra querySelectorAll; the benefit is that dynamic v-for\n // lists in Vue render correctly on the first paint.\n queueMicrotask(() => {\n if (this._items.length === 0 && this.children.length > 0) {\n this._resolveItems()\n this.requestUpdate()\n }\n })\n }\n\n override updated(_changed: PropertyValues): void {\n super.updated(_changed)\n // Measure the chip row to compute the overflow budget. We\n // only run this in `chipOverflow=\"count\"` mode; for `wrap`\n // and `none`, CSS does the work and the state stays at\n // its initial `-1` (\"no measurement\").\n //\n // The measurement is intentionally simple: sum the\n // rendered widths of the chip DOM nodes (offsetWidth\n // includes padding + border) plus the gap, and stop when\n // the next chip would push the row past the trigger's\n // content box. We do not use ResizeObserver — a chip\n // add/remove, a value change, or any other state update\n // is enough to retrigger the measurement, and the trigger\n // height is constant in this mode so a single read per\n // render is correct.\n //\n // The overflow chip itself is excluded from the\n // measurement (it carries the `.overflow` class). If it\n // were included, the loop would re-add it to the count\n // every render and never converge on a stable `_visibleChipCount`.\n if (this.mode !== 'multiple' || this.display !== 'chips' || this.chipOverflow !== 'count') {\n // No measurement in this mode. Reset the budget so a\n // later switch to `\"count\"` re-measures from scratch.\n if (this._visibleChipCount !== -1) this._visibleChipCount = -1\n return\n }\n const row = this.renderRoot.querySelector<HTMLElement>('.chip-row')\n if (!row) return\n const trigger = this.renderRoot.querySelector<HTMLElement>('.trigger')\n if (!trigger) return\n // Only the real chips, not the overflow sentinel.\n const chips = Array.from(row.querySelectorAll<HTMLElement>('sken-filter-chip:not(.overflow)'))\n if (chips.length === 0) {\n if (this._visibleChipCount !== 0) this._visibleChipCount = 0\n return\n }\n // Available width = trigger clientWidth minus the\n // non-chip siblings (clear + chevron + their gap). We\n // approximate by reading the trigger's content width and\n // subtracting the input's width as a safe lower bound\n // (the input has `flex: 1`, so it shrinks to whatever\n // space is left after the chips).\n const triggerStyle = getComputedStyle(trigger)\n const paddingX =\n (parseFloat(triggerStyle.paddingLeft) || 0) + (parseFloat(triggerStyle.paddingRight) || 0)\n const gap = parseFloat(triggerStyle.gap) || 0\n const triggerInner = trigger.clientWidth - paddingX\n // Walk the chips left-to-right. The row's content can\n // grow indefinitely in `flex-wrap: nowrap` rows; we sum\n // each chip's offsetWidth and stop when adding the next\n // would push us past `triggerInner`. We reserve space\n // for one overflow chip (32px) plus its gap so the budget\n // is honest.\n const OVERFLOW_CHIP_WIDTH = 32 + gap\n let acc = 0\n let visible = 0\n for (let i = 0; i < chips.length; i++) {\n const w = chips[i].offsetWidth\n const need = acc + w + (i > 0 ? gap : 0)\n // If we have to add an overflow chip at the end, we\n // need to reserve space for it. Only reserve if there\n // is at least one chip to hide.\n const remaining = chips.length - (i + 1)\n const reserved = remaining > 0 ? OVERFLOW_CHIP_WIDTH : 0\n if (need + reserved > triggerInner) break\n acc = need\n visible = i + 1\n }\n if (this._visibleChipCount !== visible) {\n this._visibleChipCount = visible\n }\n }\n\n private _observer?: MutationObserver\n\n // ── model: introspect children, build the flat list ──────────────\n\n /**\n * Walk the host's children. For each direct child, classify\n * it as a group (and recurse into it) or as an ungrouped item.\n * The result is a flat list of items with their group label\n * denormalised onto each row. The listbox render uses this\n * flat list directly, which keeps the rendering loop simple\n * and lets the filter operate on a single array.\n */\n private _resolveItems(): void {\n const items: ResolvedItem[] = []\n for (const child of Array.from(this.children)) {\n if (\n customElements.get('sken-combobox-group') &&\n child instanceof customElements.get('sken-combobox-group')!\n ) {\n const group = child as SkenComboboxGroup\n const groupLabel = group.label || ''\n const groupDisabled = group.disabled\n for (const itemEl of Array.from(group.children)) {\n if (\n customElements.get('sken-combobox-item') &&\n itemEl instanceof customElements.get('sken-combobox-item')!\n ) {\n const item = itemEl as SkenComboboxItem\n items.push({\n value: item.value,\n label: item.label,\n disabled: item.disabled || groupDisabled,\n groupLabel,\n groupDisabled,\n })\n }\n }\n } else if (\n customElements.get('sken-combobox-item') &&\n child instanceof customElements.get('sken-combobox-item')!\n ) {\n const item = child as SkenComboboxItem\n items.push({\n value: item.value,\n label: item.label,\n disabled: item.disabled,\n groupLabel: null,\n groupDisabled: false,\n })\n }\n }\n this._items = items\n }\n\n // ── filter ────────────────────────────────────────────────────────\n\n /**\n * Apply the current query to the resolved items. An item is\n * visible when its label contains the query (case-insensitive),\n * or when the query is empty. A group is visible when at\n * least one of its items is visible. Ungrouped items are\n * always at the top.\n */\n private get _visibleItems(): ResolvedItem[] {\n if (!this._query) return this._items\n const q = this._query.toLowerCase()\n return this._items\n .filter((it) => !it.disabled || true) // disabled items are still shown, just not selectable\n .filter((it) => it.label.toLowerCase().includes(q))\n }\n\n /**\n * The visible items, grouped by `groupLabel`. Each group\n * becomes a `{ label, items }` entry; ungrouped items go\n * into a synthetic group with `label = null`.\n */\n private get _visibleGroups(): { label: string | null; items: ResolvedItem[] }[] {\n const visible = this._visibleItems\n const groups = new Map<string | null, ResolvedItem[]>()\n for (const it of visible) {\n const key = it.groupLabel || null\n if (!groups.has(key)) groups.set(key, [])\n groups.get(key)!.push(it)\n }\n // Preserve declaration order. Items inside each group are\n // already in order (we walked the DOM in order). Groups are\n // returned in the order their first item appears.\n const ordered: { label: string | null; items: ResolvedItem[] }[] = []\n for (const [label, items] of groups) {\n ordered.push({ label, items })\n }\n return ordered\n }\n\n // ── selection ─────────────────────────────────────────────────────\n\n private _setValue(next: SkenComboboxValue<SkenComboboxMode>): void {\n if (next === this.value) return\n this.value = next\n this.dispatchEvent(\n new CustomEvent<{ value: SkenComboboxValue<SkenComboboxMode> }>('sken-change', {\n detail: { value: next },\n bubbles: true,\n composed: true,\n }),\n )\n }\n\n /** Click handler on a listbox option. */\n private _selectItem(item: ResolvedItem): void {\n if (item.disabled) return\n if (this.mode === 'multiple') {\n const current: string[] = Array.isArray(this.value) ? [...this.value] : []\n const idx = current.indexOf(item.value)\n if (idx >= 0) current.splice(idx, 1)\n else current.push(item.value)\n this._setValue(current)\n // Stay open in multiple mode. The user may want to pick more.\n this._input.focus()\n } else {\n this._setValue(item.value)\n this._close()\n }\n }\n\n /**\n * Remove a single chip in `multiple` + `display=\"chips\"`\n * mode. Called by the chip's own `sken-close` event. The\n * chip itself does not remove from the DOM — we do, by\n * updating `value`, which triggers a re-render.\n */\n private _removeChip(value: string, e: CustomEvent): void {\n e.stopPropagation()\n if (this.mode !== 'multiple') return\n const current: string[] = Array.isArray(this.value) ? [...this.value] : []\n const idx = current.indexOf(value)\n if (idx < 0) return\n current.splice(idx, 1)\n this._setValue(current)\n // Return focus to the input so the user can keep typing.\n this._input.focus()\n }\n\n // ── open / close ──────────────────────────────────────────────────\n\n private _open_(): void {\n if (this._open || this.disabled) return\n this._open = true\n this._activeIndex = this._firstSelectableIndex()\n }\n\n private _close(): void {\n if (!this._open) return\n this._open = false\n this._activeIndex = -1\n }\n\n // ── keyboard ──────────────────────────────────────────────────────\n\n private _firstSelectableIndex(): number {\n return this._visibleItems.findIndex((it) => !it.disabled)\n }\n\n private _nextSelectableIndex(from: number, dir: 1 | -1): number {\n const items = this._visibleItems\n if (items.length === 0) return -1\n let i = from + dir\n while (i >= 0 && i < items.length) {\n if (!items[i]!.disabled) return i\n i += dir\n }\n return from\n }\n\n private _onKeydown = (e: KeyboardEvent): void => {\n if (this.disabled) return\n switch (e.key) {\n case 'ArrowDown': {\n e.preventDefault()\n if (!this._open) {\n this._open_()\n } else {\n this._activeIndex = this._nextSelectableIndex(this._activeIndex, 1)\n }\n break\n }\n case 'ArrowUp': {\n e.preventDefault()\n if (!this._open) {\n this._open_()\n } else {\n this._activeIndex = this._nextSelectableIndex(this._activeIndex, -1)\n }\n break\n }\n case 'Home': {\n if (!this._open) return\n e.preventDefault()\n this._activeIndex = this._firstSelectableIndex()\n break\n }\n case 'End': {\n if (!this._open) return\n e.preventDefault()\n const items = this._visibleItems\n for (let i = items.length - 1; i >= 0; i--) {\n if (!items[i]!.disabled) {\n this._activeIndex = i\n break\n }\n }\n break\n }\n case 'Enter': {\n if (!this._open) {\n e.preventDefault()\n this._open_()\n return\n }\n const items = this._visibleItems\n const active = items[this._activeIndex]\n if (active && !active.disabled) {\n e.preventDefault()\n this._selectItem(active)\n }\n break\n }\n case 'Escape': {\n if (this._open) {\n e.preventDefault()\n this._close()\n } else if (this.value !== null && this.value !== undefined) {\n // Clear the value when the listbox is already closed.\n // Mirrors the upstream iX 5 pattern (combobox with\n // Escape clears the value when closed).\n e.preventDefault()\n const cleared: SkenComboboxValue<SkenComboboxMode> = this.mode === 'multiple' ? [] : null\n this._setValue(cleared)\n }\n break\n }\n case 'Tab': {\n // Tab moves focus out without selecting. Standard\n // combobox 1.2 pattern.\n this._close()\n break\n }\n }\n }\n\n private _onInput = (e: InputEvent): void => {\n if (!this.filterable) return\n const target = e.target as HTMLInputElement\n this._query = target.value\n if (!this._open) this._open_()\n this._activeIndex = this._firstSelectableIndex()\n }\n\n private _onFocus = (): void => {\n this.dispatchEvent(new FocusEvent('sken-focus', { bubbles: true, composed: true }))\n }\n\n private _onBlur = (): void => {\n // Defer close so a click on an option still registers.\n setTimeout(() => {\n this._close()\n this.dispatchEvent(new FocusEvent('sken-blur', { bubbles: true, composed: true }))\n }, 100)\n }\n\n // ── display value in the trigger ─────────────────────────────────\n\n /**\n * Text shown in the `<input>` element of the trigger.\n * Three branches, in priority order:\n *\n * 1. If the user is typing (a query is active), the input\n * shows the query text — even in `display=\"chips\"` mode.\n * This is the same priority as the original behavior.\n * 2. In `display=\"count\"` mode (default), the trigger shows\n * `\"N selected\"` for a non-empty multi-value. The\n * primitive uses this as a stable-height alternative to\n * chips, useful in dense forms.\n * 3. In `display=\"chips\"` mode, the input itself is empty;\n * the chip row is what conveys the selection. The chips\n * are rendered as separate elements next to the input\n * in the same flex row.\n *\n * Single mode falls through to the existing \"label of the\n * chosen option\" behavior.\n */\n private get _displayValue(): string {\n if (this._query) return this._query\n if (this.mode === 'multiple') {\n const arr = Array.isArray(this.value) ? this.value : []\n if (arr.length === 0) return ''\n // In `display=\"chips\"`, the chip row (rendered in the\n // template) is what the user sees; the input itself\n // stays empty so the cursor can still land at the end of\n // the row for further typing. In `display=\"count\"`, the\n // input shows the count, keeping the trigger height\n // stable across selections.\n if (this.display === 'chips') return ''\n return `${arr.length} selected`\n }\n if (this.value && typeof this.value === 'string') {\n const it = this._items.find((i) => i.value === this.value)\n return it?.label ?? ''\n }\n return ''\n }\n\n // ── render ────────────────────────────────────────────────────────\n\n static override styles = css`\n :host {\n display: inline-block;\n position: relative;\n font-family: var(--sken-family-sans, inherit);\n color: var(--sken-foreground, #152b48);\n inline-size: 100%;\n }\n .field {\n display: grid;\n gap: 0.375rem;\n }\n .label {\n font-size: 0.8125rem;\n font-weight: 500;\n }\n .trigger {\n position: relative;\n display: flex;\n /* The trigger wraps its children: the chip row (if any)\n * goes on its own line, then the input row. Without\n * flex-wrap: wrap here, the chip row would push the input\n * horizontally and the clear + chevron would float at\n * the end of the first chip line. */\n flex-wrap: wrap;\n align-items: center;\n gap: 0.25rem;\n padding-inline: 0.75rem;\n padding-block: var(--sken-2, 0.5rem);\n min-block-size: 2.25rem;\n box-sizing: border-box;\n inline-size: 100%;\n border: 1px solid var(--sken-border, #dfdfe3);\n border-radius: var(--sken-md, 0.5rem);\n background: var(--sken-input, #fff);\n color: var(--sken-foreground, #152b48);\n font-size: var(--sken-size-md, 0.875rem);\n cursor: pointer;\n transition:\n border-color 120ms ease,\n box-shadow 120ms ease;\n }\n .trigger:hover:not([data-disabled='true']) {\n border-color: var(--sken-foreground, #152b48);\n }\n .trigger[data-focused='true'] {\n border-color: var(--sken-primary, #2e6fc6);\n box-shadow: 0 0 0 2px var(--sken-ring, #2e6fc640);\n outline: none;\n }\n .trigger[data-disabled='true'] {\n cursor: not-allowed;\n opacity: var(--sken-disabled, 0.5);\n }\n :host([invalid]) .trigger {\n border-color: var(--sken-destructive, #d4181d);\n }\n input {\n flex: 1;\n min-inline-size: 0;\n border: none;\n background: transparent;\n color: inherit;\n font: inherit;\n outline: none;\n }\n input::placeholder {\n color: var(--sken-muted-foreground, #4f6076);\n }\n .chevron,\n .clear {\n inline-size: 1rem;\n block-size: 1rem;\n flex-shrink: 0;\n color: var(--sken-muted-foreground, #4f6076);\n display: inline-flex;\n align-items: center;\n justify-content: center;\n line-height: 1;\n /* The trigger sets font-size: 0.875rem (--sken-size-md)\n * and inherits Inter. Inter renders block glyphs like\n * the down-chevron (▼, U+25BC) at near full em-box,\n * which makes the chevron look huge next to the\n * narrower clear (×, U+D7) which Arial renders as a\n * thin cross. We pick a system sans explicitly so the\n * two glyphs have similar visual weight, and drop to\n * 0.75rem so the chevron's block shape does not eat\n * the input's vertical padding. */\n font-family:\n ui-sans-serif,\n system-ui,\n -apple-system,\n 'Segoe UI',\n Roboto,\n sans-serif;\n font-size: 0.75rem;\n user-select: none;\n }\n .chevron {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n inline-size: 1rem;\n block-size: 1rem;\n flex-shrink: 0;\n color: var(--sken-muted-foreground, #4f6076);\n transition: transform 120ms ease;\n }\n .chevron svg {\n inline-size: 100%;\n block-size: 100%;\n /* Default stroke-width is fine for 16x16; if the consumer\n * needs a different size, the consumer can set\n * --sken-icon-stroke-width on the host. */\n }\n .chevron[data-open='true'] {\n transform: rotate(180deg);\n }\n .clear {\n cursor: pointer;\n /* Outlined at rest: a 1px border on a transparent\n * background, matching the Nord Design System's tag\n * pattern (see nordhealth.design/components/tag).\n * The outline signals affordance without filling\n * the trigger with a gray pill, so the combobox\n * stays visually calm when the clear is the only\n * thing on the right edge. */\n background: transparent;\n border: 1px solid var(--sken-border, #dfdfe3);\n padding: 0;\n border-radius: 999px;\n }\n .clear:hover {\n background: var(--sken-muted, #f4f4f5);\n border-color: var(--sken-muted-foreground, #4f6076);\n color: var(--sken-foreground, #152b48);\n }\n .listbox {\n position: absolute;\n inset-block-start: calc(100% + 0.25rem);\n inset-inline: 0;\n max-block-size: 16rem;\n overflow-y: auto;\n background: var(--sken-card, #fff);\n border: 1px solid var(--sken-border, #dfdfe3);\n border-radius: var(--sken-md, 0.5rem);\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);\n z-index: 10;\n list-style: none;\n margin: 0;\n padding: 0.25rem 0;\n /* Token-driven scrollbar so the panel matches the design\n * language instead of inheriting the host OS default. The\n * WebKit pseudo-elements cover Safari + older Chromium;\n * scrollbar-width / scrollbar-color cover Firefox +\n * modern Chromium. Both are needed because each engine\n * ignores the other API. */\n scrollbar-width: thin;\n scrollbar-color: var(--sken-border, #dfdfe3) transparent;\n }\n .listbox::-webkit-scrollbar {\n inline-size: 0.5rem;\n block-size: 0.5rem;\n }\n .listbox::-webkit-scrollbar-track {\n background: transparent;\n }\n .listbox::-webkit-scrollbar-thumb {\n background: var(--sken-border, #dfdfe3);\n border-radius: 999px;\n border: 2px solid var(--sken-card, #fff);\n background-clip: padding-box;\n }\n .listbox::-webkit-scrollbar-thumb:hover {\n background: var(--sken-muted-foreground, #4f6076);\n background-clip: padding-box;\n }\n .group-header {\n padding: 0.5rem 0.75rem 0.25rem;\n font-size: 0.75rem;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.04em;\n color: var(--sken-muted-foreground, #4f6076);\n }\n .option {\n padding: 0.5rem 0.75rem;\n cursor: pointer;\n display: flex;\n align-items: center;\n gap: 0.5rem;\n font-size: var(--sken-size-md, 0.875rem);\n }\n .option[data-active='true'] {\n background: var(--sken-muted, #f4f4f5);\n }\n .option[data-selected='true'] {\n font-weight: 500;\n }\n .option[data-disabled='true'] {\n cursor: not-allowed;\n opacity: var(--sken-disabled, 0.5);\n }\n .check {\n inline-size: 1rem;\n block-size: 1rem;\n flex-shrink: 0;\n color: var(--sken-primary, #2e6fc6);\n opacity: 0;\n }\n .option[data-selected='true'] .check {\n opacity: 1;\n }\n .helper {\n font-size: 0.75rem;\n color: var(--sken-muted-foreground, #4f6076);\n }\n .chip-row {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 0.25rem;\n min-inline-size: 0;\n /* The chip row wraps to multiple lines when the chips\n * do not fit. The trigger's own height grows accordingly;\n * for display:chips + chip-overflow:count, the row uses\n * nowrap + overflow:hidden instead, so the trigger\n * height stays constant. */\n flex-basis: 100%;\n row-gap: 0.25rem;\n }\n .chip-row[data-overflow='none'] {\n flex-wrap: nowrap;\n overflow-x: auto;\n /* Hide the scrollbar visually; consumers who need to\n * see overflow can override. Same scrollbar pattern as\n * the listbox. */\n scrollbar-width: none;\n }\n .chip-row[data-overflow='none']::-webkit-scrollbar {\n display: none;\n }\n .chip-row[data-overflow='count'] {\n flex-wrap: nowrap;\n overflow-x: hidden;\n }\n .chip-row sken-filter-chip.overflow {\n /* The overflow chip reuses the same visual treatment as\n * a regular chip but with no close button (set via the\n * hide-close-button attribute on the primitive). */\n background: var(--sken-primary, #2e6fc6);\n color: var(--sken-primary-foreground, #fff);\n }\n .chip-row sken-filter-chip.overflow:hover {\n background: var(--sken-primary, #2e6fc6);\n opacity: 0.92;\n }\n /* The input row (input + clear + chevron) sits on its own\n * line below the chip row. Without this wrapper, the\n * clear + chevron would float to the end of the first\n * chip line in display:chips mode (visually wrong).\n * The input row is always present, even when there are\n * no chips — that way the layout is stable across the\n * \"empty\" and \"with chips\" states. */\n .input-row {\n display: flex;\n align-items: center;\n /* Slightly wider gap than the trigger's general one, so\n * the clear button and the chevron do not feel\n * glued together at the right edge of the input. The\n * extra 4px matches the visual weight of the icons\n * (16x16 each) without making the layout feel sparse. */\n gap: 0.5rem;\n flex-basis: 100%;\n min-inline-size: 0;\n }\n `\n\n protected override render() {\n const visible = this._visibleItems\n const groups = this._visibleGroups\n const isMulti = this.mode === 'multiple'\n const selectedSet = isMulti\n ? new Set(Array.isArray(this.value) ? this.value : [])\n : new Set(this.value ? [this.value as string] : [])\n const listboxId = `sken-combobox-listbox-${Math.random().toString(36).slice(2, 8)}`\n\n // The chip row is only meaningful in `multiple` + `display=\"chips\"`.\n // Single mode never renders chips; the trigger shows the chosen\n // option's label in the input itself.\n const showChips = isMulti && this.display === 'chips'\n const selectedItems = showChips ? this._items.filter((it) => selectedSet.has(it.value)) : []\n\n // Apply the overflow budget. The `_visibleChipCount` is set in\n // `updated()` once the chip row has been rendered; until then\n // it is `-1`, which means \"show them all\" (no measurement yet).\n // After the first measurement, the first `_visibleChipCount`\n // items render as full chips; the rest are hidden behind the\n // overflow chip (only relevant in `chipOverflow=\"count\"`).\n const visibleChipSlice =\n showChips && this._visibleChipCount >= 0\n ? selectedItems.slice(0, this._visibleChipCount)\n : selectedItems\n const hiddenChips =\n showChips && this._visibleChipCount >= 0 ? selectedItems.slice(this._visibleChipCount) : []\n\n return html`\n <div class=\"field\">\n ${this.label ? html`<label class=\"label\" for=\"trigger\">${this.label}</label>` : null}\n <div\n class=\"trigger\"\n data-focused=${this._open}\n data-disabled=${this.disabled}\n @click=${() => !this.disabled && (this._open ? this._close() : this._open_())}\n >\n ${\n showChips && selectedItems.length > 0\n ? html`<div class=\"chip-row\" part=\"chip-row\" data-overflow=${this.chipOverflow}>\n ${visibleChipSlice.map(\n (it) => html`\n <sken-filter-chip\n label=${it.label}\n data-sken-combobox-chip-value=${it.value}\n @sken-close=${(e: CustomEvent) => this._removeChip(it.value, e)}\n ></sken-filter-chip>\n `,\n )}\n ${\n hiddenChips.length > 0 && this.chipOverflow === 'count'\n ? html`<sken-filter-chip\n class=\"overflow\"\n label=\"+${hiddenChips.length}\"\n data-sken-combobox-overflow=${hiddenChips.length}\n hide-close-button\n @click=${(e: MouseEvent) => {\n e.stopPropagation()\n this._open = true\n }}\n ></sken-filter-chip>`\n : null\n }\n </div>`\n : null\n }\n <div class=\"input-row\">\n <input\n id=\"trigger\"\n role=\"combobox\"\n aria-expanded=${this._open}\n aria-controls=${listboxId}\n aria-activedescendant=${this._activeIndex >= 0 ? `${listboxId}-opt-${this._activeIndex}` : ''}\n aria-autocomplete=${this.filterable ? 'list' : 'none'}\n aria-required=${this.required}\n aria-invalid=${this.invalid}\n aria-describedby=${this.describedById}\n ?disabled=${this.disabled}\n .value=${this._displayValue}\n placeholder=${this.placeholder}\n @input=${this._onInput}\n @keydown=${this._onKeydown}\n @focus=${this._onFocus}\n @blur=${this._onBlur}\n />\n ${\n !this.disabled && this.value !== null && (isMulti ? selectedSet.size > 0 : true)\n ? html`<button\n type=\"button\"\n class=\"clear\"\n aria-label=\"Clear selection\"\n @click=${(e: Event) => {\n e.stopPropagation()\n const cleared: SkenComboboxValue<SkenComboboxMode> = isMulti ? [] : null\n this._setValue(cleared)\n this._query = ''\n this._input.focus()\n }}\n >\n ×\n </button>`\n : null\n }\n <span class=\"chevron\" data-open=${this._open} aria-hidden=\"true\">\n <svg\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"1.5\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n >\n <path d=\"M3.5 6.5 L8 11 L12.5 6.5\"></path>\n </svg>\n </span>\n </div>\n </div>\n\n ${\n this._open\n ? html`<ul\n id=${listboxId}\n class=\"listbox\"\n role=\"listbox\"\n aria-multiselectable=${isMulti}\n >\n ${\n visible.length === 0\n ? html`<li class=\"option\" data-disabled=\"true\">No matches</li>`\n : groups.map(\n (g) => html`\n ${\n g.label\n ? html`<li class=\"group-header\" role=\"presentation\">${g.label}</li>`\n : null\n }\n ${g.items.map((it) => {\n const idx = visible.indexOf(it)\n const isActive = idx === this._activeIndex\n const isSelected = selectedSet.has(it.value)\n return html`<li\n id=${`${listboxId}-opt-${idx}`}\n class=\"option\"\n role=\"option\"\n data-active=${isActive}\n data-selected=${isSelected}\n data-disabled=${it.disabled}\n aria-selected=${isSelected}\n aria-disabled=${it.disabled}\n @mouseenter=${() => {\n this._activeIndex = idx\n }}\n @click=${() => this._selectItem(it)}\n >\n ${isMulti ? html`<span class=\"check\" aria-hidden=\"true\">✓</span>` : null}\n <span>${it.label}</span>\n </li>`\n })}\n `,\n )\n }\n </ul>`\n : null\n }\n ${this.helperText ? html`<div class=\"helper\">${this.helperText}</div>` : null}\n </div>\n `\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'sken-combobox': SkenCombobox\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"sken-combobox.js","names":[],"sources":["../src/components/sken-combobox.ts"],"sourcesContent":["/**\n * `<sken-combobox>` — Sken contract implementation.\n *\n * Single + multiple selection. Filterable. Groupable. Tokenised.\n * No third-party vendor in the runtime.\n *\n * The Sken contract (`SkenComboboxProps`, `SkenComboboxItemProps`,\n * `SkenComboboxGroupProps`, `SkenComboboxEmits`,\n * `SkenComboboxValue<M>`, `skenComboboxAria`) lives in\n * `@sken-ds/contracts`. This component is a Lit 3 implementation\n * that:\n *\n * 1. Accepts the Sken contract props (`mode`, `value`,\n * `filterable`, `label`, `helperText`, `placeholder`,\n * `disabled`, `invalid`, `required`, `name`, `size`,\n * `describedById`).\n * 2. Discovers the options at first render by introspecting\n * `<sken-combobox-item>` and `<sken-combobox-group>`\n * children. Each item knows its `value` and `label`; each\n * group knows its `label` and `disabled` state. Items\n * inside a group are associated with that group; items\n * outside any group belong to an implicit \"ungrouped\"\n * bucket rendered at the top of the listbox (no header).\n * 3. Renders a Shadow DOM with: a `<label>` for the visible\n * field label, an `<input role=\"combobox\">` for the\n * trigger, a clear button, a chevron, a listbox panel\n * containing the (optionally grouped, optionally filtered)\n * options, and helper / invalid / warning / valid text.\n * 4. Implements the WAI-ARIA combobox 1.2 pattern: the input\n * has `role=\"combobox\"`, `aria-expanded`, `aria-controls`\n * pointing to the listbox, and `aria-activedescendant`\n * pointing to the currently highlighted option. The listbox\n * has `role=\"listbox\"`, each option has `role=\"option\"` +\n * `aria-selected`, and each group has `role=\"group\"` +\n * `aria-labelledby` pointing to the group's header.\n * 5. Implements the keyboard model: ArrowUp / ArrowDown open\n * and navigate, Home / End jump to the ends, Enter selects,\n * Escape closes (or clears when already closed), Tab moves\n * focus out without selecting.\n * 6. Emits the Sken-shaped `sken-change` event with the new\n * value. The shape of the value depends on `mode`:\n * `string | null` for `single`, `string[]` for `multiple`.\n * Native `change` on the input is suppressed; consumers use\n * `sken-change` for portability.\n *\n * Implementation notes:\n *\n * - The children are introspected lazily on each render so\n * dynamic option lists (Vue v-for, conditional rendering)\n * work without manual registration.\n *\n * - The filter is a case-insensitive substring match against\n * `item.label`. Groups hide themselves when ALL their items\n * are filtered out. An empty filter restores every item.\n *\n * - The listbox is positioned absolutely below the input,\n * matching the input's width. The visual treatment is\n * tokenised via `--sken-*` custom properties; the consumer\n * imports `@sken-ds/theme/css` once at app boot.\n *\n * - The \"marker\" pattern for the children (items and groups\n * render nothing; the parent projects them) is documented in\n * the iX 5 reference and in the comments on the children\n * themselves. The trade-off is that `<sken-combobox-item>`\n * does not appear in the rendered DOM tree as itself.\n */\nimport { LitElement, css, html, type PropertyValues } from 'lit'\nimport { customElement, property, query, state } from 'lit/decorators.js'\n\nimport type {\n SkenComboboxChipOverflow,\n SkenComboboxDisplay,\n SkenComboboxMode,\n SkenComboboxProps,\n SkenComboboxValue,\n} from '@sken-ds/contracts'\n\n// Side-effect import: register the `<sken-filter-chip>`\n// custom element so the combobox can render chips inside its\n// shadow DOM without forcing the consumer to import the chip\n// module explicitly. The chip is a pure consumer-side primitive\n// of the combobox in `display=\"chips\"` mode, so it belongs in\n// the combobox's module graph.\nimport './sken-filter-chip.js'\n\nimport type { SkenComboboxGroup } from './sken-combobox-group'\nimport type { SkenComboboxItem } from './sken-combobox-item'\n\n/**\n * Internal flat model: each item is a row, optionally\n * associated with a group label. The listbox renders a flat\n * list; the group headers are rendered as separator rows when\n * the group becomes visible after filtering.\n */\ninterface ResolvedItem {\n value: string\n label: string\n disabled: boolean\n groupLabel: string | null\n groupDisabled: boolean\n}\n\n@customElement('sken-combobox')\nexport class SkenCombobox extends LitElement implements Omit<SkenComboboxProps, 'value' | 'slots'> {\n // The Sken contract declares `value` as `SkenComboboxValue`\n // (a generic defaulting to `string | null`). This\n // implementation uses `SkenComboboxValue<SkenComboboxMode>`\n // (the full union) so that single-mode and multi-mode\n // values can both flow through `_setValue` without TS\n // narrowing complaints. The runtime contract is\n // identical; the difference is only at the type level.\n // ── public API (mapped to Sken contract) ──────────────────────────\n\n @property({ reflect: true }) mode: SkenComboboxMode = 'single'\n\n /**\n * How the trigger renders the current selection in\n * `mode=\"multiple\"`. See `SkenComboboxDisplay`. No effect\n * in `mode=\"single\"`. @default \"count\"\n */\n @property() display: SkenComboboxDisplay = 'count'\n\n /**\n * How the chip row overflows when `display=\"chips\"` and\n * the chips do not fit on a single line. See\n * `SkenComboboxChipOverflow`. No effect in\n * `mode=\"single\"` or when `display=\"count\"`.\n * @default \"wrap\"\n */\n @property({ attribute: 'chip-overflow' }) chipOverflow: SkenComboboxChipOverflow = 'wrap'\n\n @property({ attribute: false })\n value: SkenComboboxValue<SkenComboboxMode> = null as SkenComboboxValue<SkenComboboxMode>\n\n @property({ type: Boolean }) filterable = true\n\n @property() label = ''\n\n @property({ attribute: 'helper-text' }) helperText = ''\n\n @property() placeholder = ''\n\n @property({ type: Boolean, reflect: true }) disabled = false\n\n @property({ type: Boolean, reflect: true }) invalid = false\n\n @property({ type: Boolean, reflect: true }) required = false\n\n @property() name = ''\n\n @property({ reflect: true }) size: 'sm' | 'md' | 'lg' = 'md'\n\n @property({ attribute: 'described-by-id' }) describedById = ''\n\n // ── internal state ────────────────────────────────────────────────\n\n /** Resolved flat model of all items, with their group labels. */\n @state() private _items: ResolvedItem[] = []\n\n /** Current filter query. Lowercased for case-insensitive match. */\n @state() private _query = ''\n\n /** Whether the listbox is open. */\n @state() private _open = false\n\n /** Index into the visible items (after filtering). -1 = no highlight. */\n @state() private _activeIndex = -1\n\n /**\n * Number of selected chips that fit on the first row of the\n * trigger. The rest are hidden behind the overflow chip in\n * `chipOverflow=\"count\"` mode. `-1` means \"no measurement\n * yet\" (or `chipOverflow` is not `\"count\"`).\n *\n * The measurement runs once per render in `updated()`. It\n * uses the actual rendered widths of the chip DOM nodes,\n * not a guess, so the overflow chip appears exactly when\n * the last visible chip would not fit. On window resize\n * the user re-renders (e.g. by typing, opening the\n * listbox, or any other state change) and the measurement\n * re-runs.\n */\n @state() private _visibleChipCount = -1\n\n @query('input') private _input!: HTMLInputElement\n\n // ── lifecycle ─────────────────────────────────────────────────────\n\n override connectedCallback(): void {\n super.connectedCallback()\n // Re-resolve on any DOM change inside the host. This is how\n // we pick up dynamic option lists (Vue v-for, etc.) without\n // the consumer having to call a register() method.\n this._observer = new MutationObserver(() => {\n this._resolveItems()\n // Force a re-render so the listbox reflects the new\n // items. Without this, mutations that happen after the\n // first render (e.g. Vue mounting children asynchronously)\n // would not trigger a render() pass.\n this.requestUpdate()\n })\n this._observer.observe(this, { childList: true, subtree: true })\n }\n\n override disconnectedCallback(): void {\n super.disconnectedCallback()\n this._observer?.disconnect()\n }\n\n override willUpdate(_changed: PropertyValues): void {\n // Resolving on every update is cheap (a few querySelectorAll\n // calls) and guarantees the model is fresh by the time\n // render() runs.\n this._resolveItems()\n }\n\n override firstUpdated(_changed: PropertyValues): void {\n // Belt-and-braces: re-resolve after the first render. Some\n // frameworks (notably Vue's h() with custom elements) may\n // append children to the host AFTER the Lit lifecycle has\n // run its first render, so the initial willUpdate sees an\n // empty children list. By the time firstUpdated runs, the\n // DOM is settled and a fresh resolve is authoritative.\n this._resolveItems()\n // Some bundlers (Vite) and frameworks (Vue) mount custom\n // element children asynchronously after the host's\n // `connectedCallback`. We poll once on the next microtask\n // to catch any children that were attached between the\n // synchronous render and the next paint. The cost is one\n // extra querySelectorAll; the benefit is that dynamic v-for\n // lists in Vue render correctly on the first paint.\n queueMicrotask(() => {\n if (this._items.length === 0 && this.children.length > 0) {\n this._resolveItems()\n this.requestUpdate()\n }\n })\n }\n\n override updated(_changed: PropertyValues): void {\n super.updated(_changed)\n // Measure the chip row to compute the overflow budget. We\n // only run this in `chipOverflow=\"count\"` mode; for `wrap`\n // and `none`, CSS does the work and the state stays at\n // its initial `-1` (\"no measurement\").\n //\n // The measurement is intentionally simple: sum the\n // rendered widths of the chip DOM nodes (offsetWidth\n // includes padding + border) plus the gap, and stop when\n // the next chip would push the row past the trigger's\n // content box. We do not use ResizeObserver — a chip\n // add/remove, a value change, or any other state update\n // is enough to retrigger the measurement, and the trigger\n // height is constant in this mode so a single read per\n // render is correct.\n //\n // The overflow chip itself is excluded from the\n // measurement (it carries the `.overflow` class). If it\n // were included, the loop would re-add it to the count\n // every render and never converge on a stable `_visibleChipCount`.\n if (this.mode !== 'multiple' || this.display !== 'chips' || this.chipOverflow !== 'count') {\n // No measurement in this mode. Reset the budget so a\n // later switch to `\"count\"` re-measures from scratch.\n if (this._visibleChipCount !== -1) this._visibleChipCount = -1\n return\n }\n const row = this.renderRoot.querySelector<HTMLElement>('.chip-row')\n if (!row) return\n const trigger = this.renderRoot.querySelector<HTMLElement>('.trigger')\n if (!trigger) return\n // Only the real chips, not the overflow sentinel.\n const chips = Array.from(row.querySelectorAll<HTMLElement>('sken-filter-chip:not(.overflow)'))\n if (chips.length === 0) {\n if (this._visibleChipCount !== 0) this._visibleChipCount = 0\n return\n }\n // Available width = trigger clientWidth minus the\n // non-chip siblings (clear + chevron + their gap). We\n // approximate by reading the trigger's content width and\n // subtracting the input's width as a safe lower bound\n // (the input has `flex: 1`, so it shrinks to whatever\n // space is left after the chips).\n const triggerStyle = getComputedStyle(trigger)\n const paddingX =\n (parseFloat(triggerStyle.paddingLeft) || 0) + (parseFloat(triggerStyle.paddingRight) || 0)\n const gap = parseFloat(triggerStyle.gap) || 0\n const triggerInner = trigger.clientWidth - paddingX\n // Walk the chips left-to-right. The row's content can\n // grow indefinitely in `flex-wrap: nowrap` rows; we sum\n // each chip's offsetWidth and stop when adding the next\n // would push us past `triggerInner`. We reserve space\n // for one overflow chip (32px) plus its gap so the budget\n // is honest.\n const OVERFLOW_CHIP_WIDTH = 32 + gap\n let acc = 0\n let visible = 0\n for (let i = 0; i < chips.length; i++) {\n const w = chips[i].offsetWidth\n const need = acc + w + (i > 0 ? gap : 0)\n // If we have to add an overflow chip at the end, we\n // need to reserve space for it. Only reserve if there\n // is at least one chip to hide.\n const remaining = chips.length - (i + 1)\n const reserved = remaining > 0 ? OVERFLOW_CHIP_WIDTH : 0\n if (need + reserved > triggerInner) break\n acc = need\n visible = i + 1\n }\n if (this._visibleChipCount !== visible) {\n this._visibleChipCount = visible\n }\n }\n\n private _observer?: MutationObserver\n\n // ── model: introspect children, build the flat list ──────────────\n\n /**\n * Walk the host's children. For each direct child, classify\n * it as a group (and recurse into it) or as an ungrouped item.\n * The result is a flat list of items with their group label\n * denormalised onto each row. The listbox render uses this\n * flat list directly, which keeps the rendering loop simple\n * and lets the filter operate on a single array.\n */\n private _resolveItems(): void {\n const items: ResolvedItem[] = []\n for (const child of Array.from(this.children)) {\n if (\n customElements.get('sken-combobox-group') &&\n child instanceof customElements.get('sken-combobox-group')!\n ) {\n const group = child as SkenComboboxGroup\n const groupLabel = group.label || ''\n const groupDisabled = group.disabled\n for (const itemEl of Array.from(group.children)) {\n if (\n customElements.get('sken-combobox-item') &&\n itemEl instanceof customElements.get('sken-combobox-item')!\n ) {\n const item = itemEl as SkenComboboxItem\n items.push({\n value: item.value,\n label: item.label,\n disabled: item.disabled || groupDisabled,\n groupLabel,\n groupDisabled,\n })\n }\n }\n } else if (\n customElements.get('sken-combobox-item') &&\n child instanceof customElements.get('sken-combobox-item')!\n ) {\n const item = child as SkenComboboxItem\n items.push({\n value: item.value,\n label: item.label,\n disabled: item.disabled,\n groupLabel: null,\n groupDisabled: false,\n })\n }\n }\n this._items = items\n }\n\n // ── filter ────────────────────────────────────────────────────────\n\n /**\n * Apply the current query to the resolved items. An item is\n * visible when its label contains the query (case-insensitive),\n * or when the query is empty. A group is visible when at\n * least one of its items is visible. Ungrouped items are\n * always at the top.\n */\n private get _visibleItems(): ResolvedItem[] {\n if (!this._query) return this._items\n const q = this._query.toLowerCase()\n return this._items\n .filter((it) => !it.disabled || true) // disabled items are still shown, just not selectable\n .filter((it) => it.label.toLowerCase().includes(q))\n }\n\n /**\n * The visible items, grouped by `groupLabel`. Each group\n * becomes a `{ label, items }` entry; ungrouped items go\n * into a synthetic group with `label = null`.\n */\n private get _visibleGroups(): { label: string | null; items: ResolvedItem[] }[] {\n const visible = this._visibleItems\n const groups = new Map<string | null, ResolvedItem[]>()\n for (const it of visible) {\n const key = it.groupLabel || null\n if (!groups.has(key)) groups.set(key, [])\n groups.get(key)!.push(it)\n }\n // Preserve declaration order. Items inside each group are\n // already in order (we walked the DOM in order). Groups are\n // returned in the order their first item appears.\n const ordered: { label: string | null; items: ResolvedItem[] }[] = []\n for (const [label, items] of groups) {\n ordered.push({ label, items })\n }\n return ordered\n }\n\n // ── selection ─────────────────────────────────────────────────────\n\n private _setValue(next: SkenComboboxValue<SkenComboboxMode>): void {\n if (next === this.value) return\n this.value = next\n this.dispatchEvent(\n new CustomEvent<{ value: SkenComboboxValue<SkenComboboxMode> }>('sken-change', {\n detail: { value: next },\n bubbles: true,\n composed: true,\n }),\n )\n }\n\n /** Click handler on a listbox option. */\n private _selectItem(item: ResolvedItem): void {\n if (item.disabled) return\n if (this.mode === 'multiple') {\n const current: string[] = Array.isArray(this.value) ? [...this.value] : []\n const idx = current.indexOf(item.value)\n if (idx >= 0) current.splice(idx, 1)\n else current.push(item.value)\n this._setValue(current)\n // Stay open in multiple mode. The user may want to pick more.\n this._input.focus()\n } else {\n this._setValue(item.value)\n this._close()\n }\n }\n\n /**\n * Remove a single chip in `multiple` + `display=\"chips\"`\n * mode. Called by the chip's own `sken-close` event. The\n * chip itself does not remove from the DOM — we do, by\n * updating `value`, which triggers a re-render.\n */\n private _removeChip(value: string, e: CustomEvent): void {\n e.stopPropagation()\n if (this.mode !== 'multiple') return\n const current: string[] = Array.isArray(this.value) ? [...this.value] : []\n const idx = current.indexOf(value)\n if (idx < 0) return\n current.splice(idx, 1)\n this._setValue(current)\n // Return focus to the input so the user can keep typing.\n this._input.focus()\n }\n\n // ── open / close ──────────────────────────────────────────────────\n\n private _open_(): void {\n if (this._open || this.disabled) return\n this._open = true\n this._activeIndex = this._firstSelectableIndex()\n }\n\n private _close(): void {\n if (!this._open) return\n this._open = false\n this._activeIndex = -1\n }\n\n // ── keyboard ──────────────────────────────────────────────────────\n\n private _firstSelectableIndex(): number {\n return this._visibleItems.findIndex((it) => !it.disabled)\n }\n\n private _nextSelectableIndex(from: number, dir: 1 | -1): number {\n const items = this._visibleItems\n if (items.length === 0) return -1\n let i = from + dir\n while (i >= 0 && i < items.length) {\n if (!items[i]!.disabled) return i\n i += dir\n }\n return from\n }\n\n private _onKeydown = (e: KeyboardEvent): void => {\n if (this.disabled) return\n switch (e.key) {\n case 'ArrowDown': {\n e.preventDefault()\n if (!this._open) {\n this._open_()\n } else {\n this._activeIndex = this._nextSelectableIndex(this._activeIndex, 1)\n }\n break\n }\n case 'ArrowUp': {\n e.preventDefault()\n if (!this._open) {\n this._open_()\n } else {\n this._activeIndex = this._nextSelectableIndex(this._activeIndex, -1)\n }\n break\n }\n case 'Home': {\n if (!this._open) return\n e.preventDefault()\n this._activeIndex = this._firstSelectableIndex()\n break\n }\n case 'End': {\n if (!this._open) return\n e.preventDefault()\n const items = this._visibleItems\n for (let i = items.length - 1; i >= 0; i--) {\n if (!items[i]!.disabled) {\n this._activeIndex = i\n break\n }\n }\n break\n }\n case 'Enter': {\n if (!this._open) {\n e.preventDefault()\n this._open_()\n return\n }\n const items = this._visibleItems\n const active = items[this._activeIndex]\n if (active && !active.disabled) {\n e.preventDefault()\n this._selectItem(active)\n }\n break\n }\n case 'Escape': {\n if (this._open) {\n e.preventDefault()\n this._close()\n } else if (this.value !== null && this.value !== undefined) {\n // Clear the value when the listbox is already closed.\n // Mirrors the upstream iX 5 pattern (combobox with\n // Escape clears the value when closed).\n e.preventDefault()\n const cleared: SkenComboboxValue<SkenComboboxMode> = this.mode === 'multiple' ? [] : null\n this._setValue(cleared)\n }\n break\n }\n case 'Tab': {\n // Tab moves focus out without selecting. Standard\n // combobox 1.2 pattern.\n this._close()\n break\n }\n }\n }\n\n private _onInput = (e: InputEvent): void => {\n if (!this.filterable) return\n const target = e.target as HTMLInputElement\n this._query = target.value\n if (!this._open) this._open_()\n this._activeIndex = this._firstSelectableIndex()\n }\n\n private _onFocus = (): void => {\n this.dispatchEvent(new FocusEvent('sken-focus', { bubbles: true, composed: true }))\n }\n\n private _onBlur = (): void => {\n // Defer close so a click on an option still registers.\n setTimeout(() => {\n this._close()\n this.dispatchEvent(new FocusEvent('sken-blur', { bubbles: true, composed: true }))\n }, 100)\n }\n\n // ── display value in the trigger ─────────────────────────────────\n\n /**\n * Text shown in the `<input>` element of the trigger.\n * Three branches, in priority order:\n *\n * 1. If the user is typing (a query is active), the input\n * shows the query text — even in `display=\"chips\"` mode.\n * This is the same priority as the original behavior.\n * 2. In `display=\"count\"` mode (default), the trigger shows\n * `\"N selected\"` for a non-empty multi-value. The\n * primitive uses this as a stable-height alternative to\n * chips, useful in dense forms.\n * 3. In `display=\"chips\"` mode, the input itself is empty;\n * the chip row is what conveys the selection. The chips\n * are rendered as separate elements next to the input\n * in the same flex row.\n *\n * Single mode falls through to the existing \"label of the\n * chosen option\" behavior.\n */\n private get _displayValue(): string {\n if (this._query) return this._query\n if (this.mode === 'multiple') {\n const arr = Array.isArray(this.value) ? this.value : []\n if (arr.length === 0) return ''\n // In `display=\"chips\"`, the chip row (rendered in the\n // template) is what the user sees; the input itself\n // stays empty so the cursor can still land at the end of\n // the row for further typing. In `display=\"count\"`, the\n // input shows the count, keeping the trigger height\n // stable across selections.\n if (this.display === 'chips') return ''\n return `${arr.length} selected`\n }\n if (this.value && typeof this.value === 'string') {\n const it = this._items.find((i) => i.value === this.value)\n return it?.label ?? ''\n }\n return ''\n }\n\n // ── render ────────────────────────────────────────────────────────\n\n static override styles = css`\n :host {\n display: inline-block;\n position: relative;\n font-family: var(--sken-family-sans, inherit);\n color: var(--sken-foreground, #152b48);\n inline-size: 100%;\n }\n .field {\n display: grid;\n gap: 0.375rem;\n }\n .label {\n font-size: 0.8125rem;\n font-weight: 500;\n }\n .trigger {\n position: relative;\n display: flex;\n /* The trigger wraps its children: the chip row (if any)\n * goes on its own line, then the input row. Without\n * flex-wrap: wrap here, the chip row would push the input\n * horizontally and the clear + chevron would float at\n * the end of the first chip line. */\n flex-wrap: wrap;\n align-items: center;\n gap: 0.25rem;\n padding-inline: 0.75rem;\n padding-block: var(--sken-2, 0.5rem);\n min-block-size: 2.25rem;\n box-sizing: border-box;\n inline-size: 100%;\n border: 1px solid var(--sken-border, #dfdfe3);\n border-radius: var(--sken-md, 0.5rem);\n background: var(--sken-input, #fff);\n color: var(--sken-foreground, #152b48);\n font-size: var(--sken-size-md, 0.875rem);\n cursor: pointer;\n transition:\n border-color 120ms ease,\n box-shadow 120ms ease;\n }\n .trigger:hover:not([data-disabled='true']) {\n border-color: var(--sken-foreground, #152b48);\n }\n .trigger[data-focused='true'] {\n border-color: var(--sken-primary, #2e6fc6);\n box-shadow: 0 0 0 2px var(--sken-ring, #2e6fc640);\n outline: none;\n }\n .trigger[data-disabled='true'] {\n cursor: not-allowed;\n opacity: var(--sken-disabled, 0.5);\n }\n :host([invalid]) .trigger {\n border-color: var(--sken-destructive, #d4181d);\n }\n input {\n flex: 1;\n min-inline-size: 0;\n border: none;\n background: transparent;\n color: inherit;\n font: inherit;\n outline: none;\n }\n input::placeholder {\n color: var(--sken-muted-foreground, #4f6076);\n }\n .chevron,\n .clear {\n inline-size: 1rem;\n block-size: 1rem;\n flex-shrink: 0;\n color: var(--sken-muted-foreground, #4f6076);\n display: inline-flex;\n align-items: center;\n justify-content: center;\n line-height: 1;\n /* The trigger sets font-size: 0.875rem (--sken-size-md)\n * and inherits Inter. Inter renders block glyphs like\n * the down-chevron (▼, U+25BC) at near full em-box,\n * which makes the chevron look huge next to the\n * narrower clear (×, U+D7) which Arial renders as a\n * thin cross. We pick a system sans explicitly so the\n * two glyphs have similar visual weight, and drop to\n * 0.75rem so the chevron's block shape does not eat\n * the input's vertical padding. */\n font-family:\n ui-sans-serif,\n system-ui,\n -apple-system,\n 'Segoe UI',\n Roboto,\n sans-serif;\n font-size: 0.75rem;\n user-select: none;\n }\n .chevron {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n inline-size: 1rem;\n block-size: 1rem;\n flex-shrink: 0;\n color: var(--sken-muted-foreground, #4f6076);\n transition: transform 120ms ease;\n }\n .chevron svg {\n inline-size: 100%;\n block-size: 100%;\n /* Default stroke-width is fine for 16x16; if the consumer\n * needs a different size, the consumer can set\n * --sken-icon-stroke-width on the host. */\n }\n .chevron[data-open='true'] {\n transform: rotate(180deg);\n }\n .clear {\n cursor: pointer;\n /* Outlined at rest: a 1px border on a transparent\n * background, matching the Nord Design System's tag\n * pattern (see nordhealth.design/components/tag).\n * The outline signals affordance without filling\n * the trigger with a gray pill, so the combobox\n * stays visually calm when the clear is the only\n * thing on the right edge. */\n background: transparent;\n border: 1px solid var(--sken-border, #dfdfe3);\n padding: 0;\n border-radius: 999px;\n }\n .clear:hover {\n background: var(--sken-muted, #f4f4f5);\n border-color: var(--sken-muted-foreground, #4f6076);\n color: var(--sken-foreground, #152b48);\n }\n .listbox {\n position: absolute;\n inset-block-start: calc(100% + 0.25rem);\n inset-inline: 0;\n max-block-size: 16rem;\n overflow-y: auto;\n background: var(--sken-card, #fff);\n border: 1px solid var(--sken-border, #dfdfe3);\n border-radius: var(--sken-md, 0.5rem);\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);\n z-index: 10;\n list-style: none;\n margin: 0;\n padding: 0.25rem 0;\n /* Token-driven scrollbar so the panel matches the design\n * language instead of inheriting the host OS default. The\n * WebKit pseudo-elements cover Safari + older Chromium;\n * scrollbar-width / scrollbar-color cover Firefox +\n * modern Chromium. Both are needed because each engine\n * ignores the other API. */\n scrollbar-width: thin;\n scrollbar-color: var(--sken-border, #dfdfe3) transparent;\n }\n .listbox::-webkit-scrollbar {\n inline-size: 0.5rem;\n block-size: 0.5rem;\n }\n .listbox::-webkit-scrollbar-track {\n background: transparent;\n }\n .listbox::-webkit-scrollbar-thumb {\n background: var(--sken-border, #dfdfe3);\n border-radius: 999px;\n border: 2px solid var(--sken-card, #fff);\n background-clip: padding-box;\n }\n .listbox::-webkit-scrollbar-thumb:hover {\n background: var(--sken-muted-foreground, #4f6076);\n background-clip: padding-box;\n }\n .group-header {\n padding: 0.5rem 0.75rem 0.25rem;\n font-size: 0.75rem;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.04em;\n color: var(--sken-muted-foreground, #4f6076);\n }\n .option {\n padding: 0.5rem 0.75rem;\n cursor: pointer;\n display: flex;\n align-items: center;\n gap: 0.5rem;\n font-size: var(--sken-size-md, 0.875rem);\n }\n .option[data-active='true'] {\n background: var(--sken-muted, #f4f4f5);\n }\n .option[data-selected='true'] {\n font-weight: 500;\n }\n .option[data-disabled='true'] {\n cursor: not-allowed;\n opacity: var(--sken-disabled, 0.5);\n }\n .check {\n inline-size: 1rem;\n block-size: 1rem;\n flex-shrink: 0;\n color: var(--sken-primary, #2e6fc6);\n opacity: 0;\n }\n .option[data-selected='true'] .check {\n opacity: 1;\n }\n .helper {\n font-size: 0.75rem;\n color: var(--sken-muted-foreground, #4f6076);\n }\n .chip-row {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 0.25rem;\n min-inline-size: 0;\n /* The chip row wraps to multiple lines when the chips\n * do not fit. The trigger's own height grows accordingly;\n * for display:chips + chip-overflow:count, the row uses\n * nowrap + overflow:hidden instead, so the trigger\n * height stays constant. */\n flex-basis: 100%;\n row-gap: 0.25rem;\n }\n .chip-row[data-overflow='none'] {\n flex-wrap: nowrap;\n overflow-x: auto;\n /* Hide the scrollbar visually; consumers who need to\n * see overflow can override. Same scrollbar pattern as\n * the listbox. */\n scrollbar-width: none;\n }\n .chip-row[data-overflow='none']::-webkit-scrollbar {\n display: none;\n }\n .chip-row[data-overflow='count'] {\n flex-wrap: nowrap;\n overflow-x: hidden;\n }\n .chip-row sken-filter-chip.overflow {\n /* The overflow chip reuses the same visual treatment as\n * a regular chip but with no close button (set via the\n * hide-close-button attribute on the primitive). */\n background: var(--sken-primary, #2e6fc6);\n color: var(--sken-primary-foreground, #fff);\n }\n .chip-row sken-filter-chip.overflow:hover {\n background: var(--sken-primary, #2e6fc6);\n opacity: 0.92;\n }\n /* The input row (input + clear + chevron) sits on its own\n * line below the chip row. Without this wrapper, the\n * clear + chevron would float to the end of the first\n * chip line in display:chips mode (visually wrong).\n * The input row is always present, even when there are\n * no chips — that way the layout is stable across the\n * \"empty\" and \"with chips\" states. */\n .input-row {\n display: flex;\n align-items: center;\n /* Slightly wider gap than the trigger's general one, so\n * the clear button and the chevron do not feel\n * glued together at the right edge of the input. The\n * extra 4px matches the visual weight of the icons\n * (16x16 each) without making the layout feel sparse. */\n gap: 0.5rem;\n flex-basis: 100%;\n min-inline-size: 0;\n }\n `\n\n protected override render() {\n const visible = this._visibleItems\n const groups = this._visibleGroups\n const isMulti = this.mode === 'multiple'\n const selectedSet = isMulti\n ? new Set(Array.isArray(this.value) ? this.value : [])\n : new Set(this.value ? [this.value as string] : [])\n const listboxId = `sken-combobox-listbox-${Math.random().toString(36).slice(2, 8)}`\n\n // The chip row is only meaningful in `multiple` + `display=\"chips\"`.\n // Single mode never renders chips; the trigger shows the chosen\n // option's label in the input itself.\n const showChips = isMulti && this.display === 'chips'\n const selectedItems = showChips ? this._items.filter((it) => selectedSet.has(it.value)) : []\n\n // Apply the overflow budget. The `_visibleChipCount` is set in\n // `updated()` once the chip row has been rendered; until then\n // it is `-1`, which means \"show them all\" (no measurement yet).\n // After the first measurement, the first `_visibleChipCount`\n // items render as full chips; the rest are hidden behind the\n // overflow chip (only relevant in `chipOverflow=\"count\"`).\n const visibleChipSlice =\n showChips && this._visibleChipCount >= 0\n ? selectedItems.slice(0, this._visibleChipCount)\n : selectedItems\n const hiddenChips =\n showChips && this._visibleChipCount >= 0 ? selectedItems.slice(this._visibleChipCount) : []\n\n return html`\n <div class=\"field\">\n ${this.label ? html`<label class=\"label\" for=\"trigger\">${this.label}</label>` : null}\n <div\n class=\"trigger\"\n data-focused=${this._open}\n data-disabled=${this.disabled}\n @click=${() => !this.disabled && (this._open ? this._close() : this._open_())}\n >\n ${\n showChips && selectedItems.length > 0\n ? html`<div class=\"chip-row\" part=\"chip-row\" data-overflow=${this.chipOverflow}>\n ${visibleChipSlice.map(\n (it) => html`\n <sken-filter-chip\n label=${it.label}\n data-sken-combobox-chip-value=${it.value}\n @sken-close=${(e: CustomEvent) => this._removeChip(it.value, e)}\n ></sken-filter-chip>\n `,\n )}\n ${\n hiddenChips.length > 0 && this.chipOverflow === 'count'\n ? html`<sken-filter-chip\n class=\"overflow\"\n label=\"+${hiddenChips.length}\"\n data-sken-combobox-overflow=${hiddenChips.length}\n hide-close-button\n @click=${(e: MouseEvent) => {\n e.stopPropagation()\n this._open = true\n }}\n ></sken-filter-chip>`\n : null\n }\n </div>`\n : null\n }\n <div class=\"input-row\">\n <input\n id=\"trigger\"\n role=\"combobox\"\n aria-expanded=${this._open}\n aria-controls=${listboxId}\n aria-activedescendant=${this._activeIndex >= 0 ? `${listboxId}-opt-${this._activeIndex}` : ''}\n aria-autocomplete=${this.filterable ? 'list' : 'none'}\n aria-required=${this.required}\n aria-invalid=${this.invalid}\n aria-describedby=${this.describedById}\n ?disabled=${this.disabled}\n .value=${this._displayValue}\n placeholder=${this.placeholder}\n @input=${this._onInput}\n @keydown=${this._onKeydown}\n @focus=${this._onFocus}\n @blur=${this._onBlur}\n />\n ${\n !this.disabled && this.value !== null && (isMulti ? selectedSet.size > 0 : true)\n ? html`<button\n type=\"button\"\n class=\"clear\"\n aria-label=\"Clear selection\"\n @click=${(e: Event) => {\n e.stopPropagation()\n const cleared: SkenComboboxValue<SkenComboboxMode> = isMulti ? [] : null\n this._setValue(cleared)\n this._query = ''\n this._input.focus()\n }}\n >\n ×\n </button>`\n : null\n }\n <span class=\"chevron\" data-open=${this._open} aria-hidden=\"true\">\n <svg\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"1.5\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n >\n <path d=\"M3.5 6.5 L8 11 L12.5 6.5\"></path>\n </svg>\n </span>\n </div>\n </div>\n\n ${\n this._open\n ? html`<ul\n id=${listboxId}\n class=\"listbox\"\n role=\"listbox\"\n aria-multiselectable=${isMulti}\n >\n ${\n visible.length === 0\n ? html`<li class=\"option\" data-disabled=\"true\">No matches</li>`\n : groups.map(\n (g) => html`\n ${\n g.label\n ? html`<li class=\"group-header\" role=\"presentation\">${g.label}</li>`\n : null\n }\n ${g.items.map((it) => {\n const idx = visible.indexOf(it)\n const isActive = idx === this._activeIndex\n const isSelected = selectedSet.has(it.value)\n return html`<li\n id=${`${listboxId}-opt-${idx}`}\n class=\"option\"\n role=\"option\"\n data-active=${isActive}\n data-selected=${isSelected}\n data-disabled=${it.disabled}\n aria-selected=${isSelected}\n aria-disabled=${it.disabled}\n @mouseenter=${() => {\n this._activeIndex = idx\n }}\n @click=${() => this._selectItem(it)}\n >\n ${isMulti ? html`<span class=\"check\" aria-hidden=\"true\">✓</span>` : null}\n <span>${it.label}</span>\n </li>`\n })}\n `,\n )\n }\n </ul>`\n : null\n }\n ${this.helperText ? html`<div class=\"helper\">${this.helperText}</div>` : null}\n </div>\n `\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'sken-combobox': SkenCombobox\n }\n}\n"],"mappings":"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"}
|
|
@@ -228,13 +228,13 @@ var f = i(class extends n {
|
|
|
228
228
|
${t ? s`<header>
|
|
229
229
|
<slot name="title" @slotchange=${this.#u}></slot>
|
|
230
230
|
${this.dismissable === "dismissable" ? s`<button
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
231
|
+
class="close-button"
|
|
232
|
+
type="button"
|
|
233
|
+
aria-label="Close dialog"
|
|
234
|
+
@click=${this.#c}
|
|
235
|
+
>
|
|
236
|
+
✕
|
|
237
|
+
</button>` : c}
|
|
238
238
|
</header>` : c}
|
|
239
239
|
<div class="body">
|
|
240
240
|
<slot></slot>
|
|
@@ -255,4 +255,4 @@ e([u({
|
|
|
255
255
|
//#endregion
|
|
256
256
|
export { p as t };
|
|
257
257
|
|
|
258
|
-
//# sourceMappingURL=sken-dialog-
|
|
258
|
+
//# sourceMappingURL=sken-dialog-BvRZxwSk.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sken-dialog-CLqZu1WQ.js","names":["s","t","r","#dispatch","#hasTitle","#originalParent","#originalNextSibling","#portaled","#showModal","#onCancel","#onClose","#onClick","#onTitleSlotChange","#onCloseButton"],"sources":["../../../node_modules/.pnpm/lit-html@3.3.3/node_modules/lit-html/directives/class-map.js","../src/components/sken-dialog.ts"],"sourcesContent":["import{noChange as t}from\"../lit-html.js\";import{directive as s,Directive as i,PartType as r}from\"../directive.js\";\n/**\n * @license\n * Copyright 2018 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n */const e=s(class extends i{constructor(t){if(super(t),t.type!==r.ATTRIBUTE||\"class\"!==t.name||t.strings?.length>2)throw Error(\"`classMap()` can only be used in the `class` attribute and must be the only part in the attribute.\")}render(t){return\" \"+Object.keys(t).filter(s=>t[s]).join(\" \")+\" \"}update(s,[i]){if(void 0===this.st){this.st=new Set,void 0!==s.strings&&(this.nt=new Set(s.strings.join(\" \").split(/\\s/).filter(t=>\"\"!==t)));for(const t in i)i[t]&&!this.nt?.has(t)&&this.st.add(t);return this.render(i)}const r=s.element.classList;for(const t of this.st)t in i||(r.remove(t),this.st.delete(t));for(const t in i){const s=!!i[t];s===this.st.has(t)||this.nt?.has(t)||(s?(r.add(t),this.st.add(t)):(r.remove(t),this.st.delete(t)))}return t}});export{e as classMap};\n//# sourceMappingURL=class-map.js.map\n","// ── <sken-dialog> — WAI-ARIA modal built on <dialog> ──────────────────\n// A thin Sken wrapper around the native HTML <dialog> element.\n//\n// Why <dialog>:\n// The browser already implements the WAI-ARIA dialog pattern when\n// you call showModal(): focus trap, Tab cycling, ESC handling,\n// ::backdrop pseudo-element, top-layer rendering, and inert\n// siblings. Re-implementing those is what v1 of this file did\n// and it produced a worse result (Math.random() ids, position:\n// fixed hacks, hand-rolled scroll lock, manual ESC listener).\n// v2 uses the platform. Less code, fewer bugs, better a11y.\n//\n// Why controlled only:\n// The consumer owns the `open` state. The primitive never flips\n// it on its own. To close, the consumer listens for `sken-close`\n// and sets `open = false`. Same model as Radix Dialog, Headless\n// UI Dialog, Reach UI Dialog.\n//\n// Portal:\n// On first open, the host element is re-parented into\n// document.body. showModal() requires the <dialog> to be in the\n// top layer; if it sits inside a transformed / overflow:hidden\n// ancestor it can be clipped. Re-parenting is the robust fix.\n// The element is restored to its original parent on disconnect.\n\nimport type {\n SkenDialogCloseReason,\n SkenDialogDismissable,\n SkenDialogSize,\n} from '@sken-ds/contracts'\nimport { LitElement, css, html, nothing } from 'lit'\nimport { customElement, property, query } from 'lit/decorators.js'\nimport { classMap } from 'lit/directives/class-map.js'\n\n@customElement('sken-dialog')\nexport class SkenDialog extends LitElement {\n @property({ reflect: true, type: Boolean }) open = false\n @property({ reflect: true }) size: SkenDialogSize = 'md'\n @property({ reflect: true }) dismissable: SkenDialogDismissable = 'dismissable'\n @property({ attribute: 'aria-label' }) ariaLabel: string | null = null\n @property({ attribute: 'aria-describedby' }) ariaDescribedBy: string | null = null\n\n @query('dialog') private _dialog!: HTMLDialogElement\n @query('.close-button') private _closeButton!: HTMLButtonElement | null\n\n /** Original parent of the host element. Restored on disconnect. */\n #originalParent: Node | null = null\n /** Next sibling in the original parent. Used to restore position. */\n #originalNextSibling: Node | null = null\n /** Has the host been moved into document.body? */\n #portaled = false\n /** Has the title slot been filled? Updated via the slotchange\n * event on the title <slot>. */\n #hasTitle = false\n\n static styles = css`\n :host {\n display: contents;\n }\n\n dialog {\n /* Reset native styles. */\n padding: 0;\n border: none;\n background: transparent;\n color: inherit;\n max-width: none;\n max-height: none;\n margin: auto;\n outline: none;\n }\n\n /* Open animation (entry). The browser fires the open attribute\n on showModal(); with transition-behavior: allow-discrete we\n can animate the top-layer entry. Falls back to no animation\n in browsers without support. */\n dialog[open] {\n animation: sken-dialog-enter 150ms ease-out;\n }\n @keyframes sken-dialog-enter {\n from {\n opacity: 0;\n transform: scale(0.96);\n }\n to {\n opacity: 1;\n transform: scale(1);\n }\n }\n\n /* Backdrop. Native ::backdrop is a pseudo-element on the\n <dialog> when shown via showModal(). */\n dialog::backdrop {\n background: rgba(15, 18, 24, 0.55);\n animation: sken-dialog-backdrop-enter 150ms ease-out;\n }\n @keyframes sken-dialog-backdrop-enter {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n }\n\n .panel {\n background: var(--sken-color-surface, #ffffff);\n color: var(--sken-color-text, #1a1a1a);\n border-radius: 12px;\n box-shadow: 0 24px 48px -12px rgba(0, 0, 0, 0.25);\n display: flex;\n flex-direction: column;\n max-height: 90vh;\n max-width: 90vw;\n overflow: hidden;\n font-family: var(--sken-font-sans, system-ui, sans-serif);\n }\n\n .panel.sm {\n width: 400px;\n }\n .panel.md {\n width: 560px;\n }\n .panel.lg {\n width: 800px;\n }\n\n header {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: 16px;\n padding: 20px 24px 8px;\n }\n\n header ::slotted([slot='title']) {\n font-size: 18px;\n font-weight: 600;\n line-height: 1.3;\n margin: 0;\n }\n\n .body {\n padding: 8px 24px 16px;\n overflow-y: auto;\n flex: 1;\n }\n\n footer {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: 8px;\n padding: 12px 24px 20px;\n }\n\n /* Only render the footer divider if there is content. */\n footer:not(:has(*)) {\n display: none;\n }\n\n .close-button {\n appearance: none;\n background: transparent;\n border: none;\n cursor: pointer;\n width: 32px;\n height: 32px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: 6px;\n color: inherit;\n opacity: 0.6;\n transition:\n opacity 100ms,\n background 100ms;\n flex-shrink: 0;\n }\n .close-button:hover {\n opacity: 1;\n background: rgba(0, 0, 0, 0.05);\n }\n .close-button:focus-visible {\n outline: 2px solid var(--sken-color-focus, #2e6fc6);\n outline-offset: 1px;\n }\n `\n\n connectedCallback(): void {\n super.connectedCallback()\n // Capture original parent so we can restore on disconnect.\n this.#originalParent = this.parentNode\n this.#originalNextSibling = this.nextSibling\n }\n\n disconnectedCallback(): void {\n super.disconnectedCallback()\n if (this.#portaled && this.#originalParent) {\n // Restore original position. If originalNextSibling is null,\n // appendChild puts us at the end of originalParent.\n if (\n this.#originalNextSibling &&\n this.#originalNextSibling.parentNode === this.#originalParent\n ) {\n this.#originalParent.insertBefore(this, this.#originalNextSibling)\n } else {\n this.#originalParent.appendChild(this)\n }\n this.#portaled = false\n }\n }\n\n updated(changed: Map<string, unknown>): void {\n if (!changed.has('open')) return\n if (!this._dialog) return\n\n if (this.open) {\n this.#showModal()\n } else if (this._dialog.open) {\n this._dialog.close()\n }\n }\n\n /** Open the native <dialog>. Re-parents the host to <body> first. */\n #showModal(): void {\n if (this.#portaled === false && this.parentNode !== document.body) {\n document.body.appendChild(this)\n this.#portaled = true\n }\n if (!this._dialog.open) {\n this._dialog.showModal()\n this.#dispatch('sken-opened')\n // Focus the close button if dismissable, else the first focusable.\n queueMicrotask(() => {\n if (this.dismissable === 'dismissable' && this._closeButton) {\n this._closeButton.focus()\n } else {\n const first = this._dialog.querySelector<HTMLElement>(\n 'button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])',\n )\n first?.focus()\n }\n })\n }\n }\n\n /** Programmatic close request. Dispatches `sken-close` with the\n * given reason. Does NOT close the native <dialog> directly —\n * the consumer is expected to set `open = false` in response,\n * which the reactive `updated()` hook will translate into a\n * native close. This keeps the controlled-only contract honest:\n * the consumer always owns the `open` state.\n *\n * Use for cases where the close is initiated by code, not by\n * the user (form submission success, async operation complete,\n * etc.). */\n closeDialog(reason: SkenDialogCloseReason = 'close-button'): void {\n this.#dispatch('sken-close', reason)\n }\n\n /** Native <dialog> event: fired when the user presses ESC. We\n * translate to sken-close with reason 'escape'. The consumer\n * then sets `open = false` which calls _dialog.close() (a\n * no-op since it's already closed by the browser). */\n #onCancel = (e: Event): void => {\n if (this.dismissable === 'persistent') {\n // Prevent the browser from closing. The dialog stays open.\n e.preventDefault()\n return\n }\n // Browser will close. We dispatch sken-close; the consumer's\n // `open` prop will then become false and the next render\n // is a no-op since the dialog is already closed.\n this.#dispatch('sken-close', 'escape')\n }\n\n /** Native <dialog> event: fired on close (ESC, backdrop click,\n * close button, or .close()). We use it to fire sken-closed. */\n #onClose = (): void => {\n this.#dispatch('sken-closed')\n }\n\n /** Click handler on the <dialog> itself. If the click target is\n * the <dialog> (not a descendant), the user clicked the\n * backdrop. We treat it as a close request unless persistent. */\n #onClick = (e: MouseEvent): void => {\n if (e.target !== this._dialog) return\n if (this.dismissable === 'persistent') return\n this.#dispatch('sken-close', 'backdrop')\n }\n\n /** Click handler for the close button. Always visible when\n * dismissable; hidden when persistent. */\n #onCloseButton = (): void => {\n this.#dispatch('sken-close', 'close-button')\n }\n\n /** Lightweight event dispatcher with the conventions: composed\n * (crosses shadow boundary), bubbles (reaches ancestors). */\n #dispatch(name: 'sken-close', detail: SkenDialogCloseReason): void\n #dispatch(name: 'sken-opened' | 'sken-closed', detail?: undefined): void\n #dispatch(name: string, detail?: unknown): void {\n this.dispatchEvent(\n new CustomEvent(name, {\n detail,\n bubbles: true,\n composed: true,\n }),\n )\n }\n\n render() {\n const panelClasses = {\n panel: true,\n [this.size]: true,\n }\n // We render the header only when there is a title slot OR the\n // dialog is dismissable (so the close button has somewhere to\n // live). A persistent dialog with no title renders body + footer\n // directly, no header. Slot presence is detected via\n // slotchange on the title slot.\n const showHeader = this.#hasTitle || this.dismissable === 'dismissable'\n // aria-label is only set when explicitly provided. Otherwise the\n // consumer should set a title slot so the dialog has an\n // accessible name via aria-labelledby. We always set the\n // attribute to a non-empty string (or omit it) so that a missing\n // name is detectable in dev tools.\n const ariaLabelAttr = this.ariaLabel ?? ''\n return html`\n <dialog\n aria-label=${ariaLabelAttr || nothing}\n aria-describedby=${this.ariaDescribedBy || nothing}\n @cancel=${this.#onCancel}\n @close=${this.#onClose}\n @click=${this.#onClick}\n >\n <div class=${classMap(panelClasses)}>\n ${\n showHeader\n ? html`<header>\n <slot name=\"title\" @slotchange=${this.#onTitleSlotChange}></slot>\n ${\n this.dismissable === 'dismissable'\n ? html`<button\n class=\"close-button\"\n type=\"button\"\n aria-label=\"Close dialog\"\n @click=${this.#onCloseButton}\n >\n ✕\n </button>`\n : nothing\n }\n </header>`\n : nothing\n }\n <div class=\"body\">\n <slot></slot>\n </div>\n <footer>\n <slot name=\"actions\"></slot>\n </footer>\n </div>\n </dialog>\n `\n }\n\n #onTitleSlotChange = (e: Event): void => {\n const slot = e.target as HTMLSlotElement\n this.#hasTitle = slot.assignedNodes({ flatten: true }).length > 0\n this.requestUpdate()\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'sken-dialog': SkenDialog\n }\n}\n"],"x_google_ignoreList":[0],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"sken-dialog-BvRZxwSk.js","names":["s","t","r","#dispatch","#hasTitle","#originalParent","#originalNextSibling","#portaled","#showModal","#onCancel","#onClose","#onClick","#onTitleSlotChange","#onCloseButton"],"sources":["../../../node_modules/.pnpm/lit-html@3.3.3/node_modules/lit-html/directives/class-map.js","../src/components/sken-dialog.ts"],"sourcesContent":["import{noChange as t}from\"../lit-html.js\";import{directive as s,Directive as i,PartType as r}from\"../directive.js\";\n/**\n * @license\n * Copyright 2018 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n */const e=s(class extends i{constructor(t){if(super(t),t.type!==r.ATTRIBUTE||\"class\"!==t.name||t.strings?.length>2)throw Error(\"`classMap()` can only be used in the `class` attribute and must be the only part in the attribute.\")}render(t){return\" \"+Object.keys(t).filter(s=>t[s]).join(\" \")+\" \"}update(s,[i]){if(void 0===this.st){this.st=new Set,void 0!==s.strings&&(this.nt=new Set(s.strings.join(\" \").split(/\\s/).filter(t=>\"\"!==t)));for(const t in i)i[t]&&!this.nt?.has(t)&&this.st.add(t);return this.render(i)}const r=s.element.classList;for(const t of this.st)t in i||(r.remove(t),this.st.delete(t));for(const t in i){const s=!!i[t];s===this.st.has(t)||this.nt?.has(t)||(s?(r.add(t),this.st.add(t)):(r.remove(t),this.st.delete(t)))}return t}});export{e as classMap};\n//# sourceMappingURL=class-map.js.map\n","// ── <sken-dialog> — WAI-ARIA modal built on <dialog> ──────────────────\n// A thin Sken wrapper around the native HTML <dialog> element.\n//\n// Why <dialog>:\n// The browser already implements the WAI-ARIA dialog pattern when\n// you call showModal(): focus trap, Tab cycling, ESC handling,\n// ::backdrop pseudo-element, top-layer rendering, and inert\n// siblings. Re-implementing those is what v1 of this file did\n// and it produced a worse result (Math.random() ids, position:\n// fixed hacks, hand-rolled scroll lock, manual ESC listener).\n// v2 uses the platform. Less code, fewer bugs, better a11y.\n//\n// Why controlled only:\n// The consumer owns the `open` state. The primitive never flips\n// it on its own. To close, the consumer listens for `sken-close`\n// and sets `open = false`. Same model as Radix Dialog, Headless\n// UI Dialog, Reach UI Dialog.\n//\n// Portal:\n// On first open, the host element is re-parented into\n// document.body. showModal() requires the <dialog> to be in the\n// top layer; if it sits inside a transformed / overflow:hidden\n// ancestor it can be clipped. Re-parenting is the robust fix.\n// The element is restored to its original parent on disconnect.\n\nimport type {\n SkenDialogCloseReason,\n SkenDialogDismissable,\n SkenDialogSize,\n} from '@sken-ds/contracts'\nimport { LitElement, css, html, nothing } from 'lit'\nimport { customElement, property, query } from 'lit/decorators.js'\nimport { classMap } from 'lit/directives/class-map.js'\n\n@customElement('sken-dialog')\nexport class SkenDialog extends LitElement {\n @property({ reflect: true, type: Boolean }) open = false\n @property({ reflect: true }) size: SkenDialogSize = 'md'\n @property({ reflect: true }) dismissable: SkenDialogDismissable = 'dismissable'\n @property({ attribute: 'aria-label' }) ariaLabel: string | null = null\n @property({ attribute: 'aria-describedby' }) ariaDescribedBy: string | null = null\n\n @query('dialog') private _dialog!: HTMLDialogElement\n @query('.close-button') private _closeButton!: HTMLButtonElement | null\n\n /** Original parent of the host element. Restored on disconnect. */\n #originalParent: Node | null = null\n /** Next sibling in the original parent. Used to restore position. */\n #originalNextSibling: Node | null = null\n /** Has the host been moved into document.body? */\n #portaled = false\n /** Has the title slot been filled? Updated via the slotchange\n * event on the title <slot>. */\n #hasTitle = false\n\n static styles = css`\n :host {\n display: contents;\n }\n\n dialog {\n /* Reset native styles. */\n padding: 0;\n border: none;\n background: transparent;\n color: inherit;\n max-width: none;\n max-height: none;\n margin: auto;\n outline: none;\n }\n\n /* Open animation (entry). The browser fires the open attribute\n on showModal(); with transition-behavior: allow-discrete we\n can animate the top-layer entry. Falls back to no animation\n in browsers without support. */\n dialog[open] {\n animation: sken-dialog-enter 150ms ease-out;\n }\n @keyframes sken-dialog-enter {\n from {\n opacity: 0;\n transform: scale(0.96);\n }\n to {\n opacity: 1;\n transform: scale(1);\n }\n }\n\n /* Backdrop. Native ::backdrop is a pseudo-element on the\n <dialog> when shown via showModal(). */\n dialog::backdrop {\n background: rgba(15, 18, 24, 0.55);\n animation: sken-dialog-backdrop-enter 150ms ease-out;\n }\n @keyframes sken-dialog-backdrop-enter {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n }\n\n .panel {\n background: var(--sken-color-surface, #ffffff);\n color: var(--sken-color-text, #1a1a1a);\n border-radius: 12px;\n box-shadow: 0 24px 48px -12px rgba(0, 0, 0, 0.25);\n display: flex;\n flex-direction: column;\n max-height: 90vh;\n max-width: 90vw;\n overflow: hidden;\n font-family: var(--sken-font-sans, system-ui, sans-serif);\n }\n\n .panel.sm {\n width: 400px;\n }\n .panel.md {\n width: 560px;\n }\n .panel.lg {\n width: 800px;\n }\n\n header {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: 16px;\n padding: 20px 24px 8px;\n }\n\n header ::slotted([slot='title']) {\n font-size: 18px;\n font-weight: 600;\n line-height: 1.3;\n margin: 0;\n }\n\n .body {\n padding: 8px 24px 16px;\n overflow-y: auto;\n flex: 1;\n }\n\n footer {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: 8px;\n padding: 12px 24px 20px;\n }\n\n /* Only render the footer divider if there is content. */\n footer:not(:has(*)) {\n display: none;\n }\n\n .close-button {\n appearance: none;\n background: transparent;\n border: none;\n cursor: pointer;\n width: 32px;\n height: 32px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: 6px;\n color: inherit;\n opacity: 0.6;\n transition:\n opacity 100ms,\n background 100ms;\n flex-shrink: 0;\n }\n .close-button:hover {\n opacity: 1;\n background: rgba(0, 0, 0, 0.05);\n }\n .close-button:focus-visible {\n outline: 2px solid var(--sken-color-focus, #2e6fc6);\n outline-offset: 1px;\n }\n `\n\n connectedCallback(): void {\n super.connectedCallback()\n // Capture original parent so we can restore on disconnect.\n this.#originalParent = this.parentNode\n this.#originalNextSibling = this.nextSibling\n }\n\n disconnectedCallback(): void {\n super.disconnectedCallback()\n if (this.#portaled && this.#originalParent) {\n // Restore original position. If originalNextSibling is null,\n // appendChild puts us at the end of originalParent.\n if (\n this.#originalNextSibling &&\n this.#originalNextSibling.parentNode === this.#originalParent\n ) {\n this.#originalParent.insertBefore(this, this.#originalNextSibling)\n } else {\n this.#originalParent.appendChild(this)\n }\n this.#portaled = false\n }\n }\n\n updated(changed: Map<string, unknown>): void {\n if (!changed.has('open')) return\n if (!this._dialog) return\n\n if (this.open) {\n this.#showModal()\n } else if (this._dialog.open) {\n this._dialog.close()\n }\n }\n\n /** Open the native <dialog>. Re-parents the host to <body> first. */\n #showModal(): void {\n if (this.#portaled === false && this.parentNode !== document.body) {\n document.body.appendChild(this)\n this.#portaled = true\n }\n if (!this._dialog.open) {\n this._dialog.showModal()\n this.#dispatch('sken-opened')\n // Focus the close button if dismissable, else the first focusable.\n queueMicrotask(() => {\n if (this.dismissable === 'dismissable' && this._closeButton) {\n this._closeButton.focus()\n } else {\n const first = this._dialog.querySelector<HTMLElement>(\n 'button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])',\n )\n first?.focus()\n }\n })\n }\n }\n\n /** Programmatic close request. Dispatches `sken-close` with the\n * given reason. Does NOT close the native <dialog> directly —\n * the consumer is expected to set `open = false` in response,\n * which the reactive `updated()` hook will translate into a\n * native close. This keeps the controlled-only contract honest:\n * the consumer always owns the `open` state.\n *\n * Use for cases where the close is initiated by code, not by\n * the user (form submission success, async operation complete,\n * etc.). */\n closeDialog(reason: SkenDialogCloseReason = 'close-button'): void {\n this.#dispatch('sken-close', reason)\n }\n\n /** Native <dialog> event: fired when the user presses ESC. We\n * translate to sken-close with reason 'escape'. The consumer\n * then sets `open = false` which calls _dialog.close() (a\n * no-op since it's already closed by the browser). */\n #onCancel = (e: Event): void => {\n if (this.dismissable === 'persistent') {\n // Prevent the browser from closing. The dialog stays open.\n e.preventDefault()\n return\n }\n // Browser will close. We dispatch sken-close; the consumer's\n // `open` prop will then become false and the next render\n // is a no-op since the dialog is already closed.\n this.#dispatch('sken-close', 'escape')\n }\n\n /** Native <dialog> event: fired on close (ESC, backdrop click,\n * close button, or .close()). We use it to fire sken-closed. */\n #onClose = (): void => {\n this.#dispatch('sken-closed')\n }\n\n /** Click handler on the <dialog> itself. If the click target is\n * the <dialog> (not a descendant), the user clicked the\n * backdrop. We treat it as a close request unless persistent. */\n #onClick = (e: MouseEvent): void => {\n if (e.target !== this._dialog) return\n if (this.dismissable === 'persistent') return\n this.#dispatch('sken-close', 'backdrop')\n }\n\n /** Click handler for the close button. Always visible when\n * dismissable; hidden when persistent. */\n #onCloseButton = (): void => {\n this.#dispatch('sken-close', 'close-button')\n }\n\n /** Lightweight event dispatcher with the conventions: composed\n * (crosses shadow boundary), bubbles (reaches ancestors). */\n #dispatch(name: 'sken-close', detail: SkenDialogCloseReason): void\n #dispatch(name: 'sken-opened' | 'sken-closed', detail?: undefined): void\n #dispatch(name: string, detail?: unknown): void {\n this.dispatchEvent(\n new CustomEvent(name, {\n detail,\n bubbles: true,\n composed: true,\n }),\n )\n }\n\n render() {\n const panelClasses = {\n panel: true,\n [this.size]: true,\n }\n // We render the header only when there is a title slot OR the\n // dialog is dismissable (so the close button has somewhere to\n // live). A persistent dialog with no title renders body + footer\n // directly, no header. Slot presence is detected via\n // slotchange on the title slot.\n const showHeader = this.#hasTitle || this.dismissable === 'dismissable'\n // aria-label is only set when explicitly provided. Otherwise the\n // consumer should set a title slot so the dialog has an\n // accessible name via aria-labelledby. We always set the\n // attribute to a non-empty string (or omit it) so that a missing\n // name is detectable in dev tools.\n const ariaLabelAttr = this.ariaLabel ?? ''\n return html`\n <dialog\n aria-label=${ariaLabelAttr || nothing}\n aria-describedby=${this.ariaDescribedBy || nothing}\n @cancel=${this.#onCancel}\n @close=${this.#onClose}\n @click=${this.#onClick}\n >\n <div class=${classMap(panelClasses)}>\n ${\n showHeader\n ? html`<header>\n <slot name=\"title\" @slotchange=${this.#onTitleSlotChange}></slot>\n ${\n this.dismissable === 'dismissable'\n ? html`<button\n class=\"close-button\"\n type=\"button\"\n aria-label=\"Close dialog\"\n @click=${this.#onCloseButton}\n >\n ✕\n </button>`\n : nothing\n }\n </header>`\n : nothing\n }\n <div class=\"body\">\n <slot></slot>\n </div>\n <footer>\n <slot name=\"actions\"></slot>\n </footer>\n </div>\n </dialog>\n `\n }\n\n #onTitleSlotChange = (e: Event): void => {\n const slot = e.target as HTMLSlotElement\n this.#hasTitle = slot.assignedNodes({ flatten: true }).length > 0\n this.requestUpdate()\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'sken-dialog': SkenDialog\n }\n}\n"],"x_google_ignoreList":[0],"mappings":"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"}
|
package/dist/sken-dialog.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as e } from "./sken-dialog-
|
|
1
|
+
import { t as e } from "./sken-dialog-BvRZxwSk.js";
|
|
2
2
|
export { e as SkenDialog };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sken-layout-region.js","names":[],"sources":["../src/components/sken-layout-region.ts"],"sourcesContent":["// ── <sken-layout-region> — single cell of a <sken-layout> ──────────\n// The host element <sken-layout> places its light-DOM\n// <sken-layout-region> children into the page-level grid via the\n// `name` attribute (and CSS grid-area rules keyed off that name).\n// A region does not impose what lives inside it; it just gives\n// the grid a target to project into and lets the consumer style\n// the contents.\n//\n// Why this exists as a custom element (not a plain <div>):\n// - It lets the host's CSS select the slotted regions by tag +\n// attribute name across the shadow boundary (`::slotted(\n// sken-layout-region[name='topbar'])`), which is more robust\n// than relying on class names that the consumer might forget.\n// - It gives framework adapters a stable tag name to render\n// (Vue's <SkenLayoutRegion> resolves to <sken-layout-region>)\n// so the layout's CSS does not break if the consumer\n// re-organises their template.\n//\n// What the region does NOT do:\n// - It does not impose a default background, padding, or\n// typography. The product (or a future Sken layout pattern)\n// provides those via slot styling.\n// - It does not provide landmark roles. The consumer is\n// responsible for wrapping the contents in <header>, <nav>,\n// <main>, or <footer> as appropriate. A region called\n// `topbar` is *not* implicitly <header role=\"banner\">; the\n// product decides whether the topbar deserves the banner\n// landmark or whether that is reserved for a different layer.\n//\n// See ADR-0006 for the substrate decision. See\n// @sken-ds/contracts `SkenLayoutRegionProps` for the contract\n// surface.\n\nimport {
|
|
1
|
+
{"version":3,"file":"sken-layout-region.js","names":[],"sources":["../src/components/sken-layout-region.ts"],"sourcesContent":["// ── <sken-layout-region> — single cell of a <sken-layout> ──────────\n// The host element <sken-layout> places its light-DOM\n// <sken-layout-region> children into the page-level grid via the\n// `name` attribute (and CSS grid-area rules keyed off that name).\n// A region does not impose what lives inside it; it just gives\n// the grid a target to project into and lets the consumer style\n// the contents.\n//\n// Why this exists as a custom element (not a plain <div>):\n// - It lets the host's CSS select the slotted regions by tag +\n// attribute name across the shadow boundary (`::slotted(\n// sken-layout-region[name='topbar'])`), which is more robust\n// than relying on class names that the consumer might forget.\n// - It gives framework adapters a stable tag name to render\n// (Vue's <SkenLayoutRegion> resolves to <sken-layout-region>)\n// so the layout's CSS does not break if the consumer\n// re-organises their template.\n//\n// What the region does NOT do:\n// - It does not impose a default background, padding, or\n// typography. The product (or a future Sken layout pattern)\n// provides those via slot styling.\n// - It does not provide landmark roles. The consumer is\n// responsible for wrapping the contents in <header>, <nav>,\n// <main>, or <footer> as appropriate. A region called\n// `topbar` is *not* implicitly <header role=\"banner\">; the\n// product decides whether the topbar deserves the banner\n// landmark or whether that is reserved for a different layer.\n//\n// See ADR-0006 for the substrate decision. See\n// @sken-ds/contracts `SkenLayoutRegionProps` for the contract\n// surface.\n\nimport type { SkenLayoutRegionRole } from '@sken-ds/contracts'\nimport { LitElement, css, html } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\n\n@customElement('sken-layout-region')\nexport class SkenLayoutRegion extends LitElement {\n @property({ reflect: true }) name: SkenLayoutRegionRole = 'content'\n\n static styles = css`\n /* 'display: contents' makes the region transparent in the\n layout: it does not introduce a wrapper box, so the\n consumer's children are sized and positioned as if they\n were direct children of the host grid. The product can\n override with 'display: block' (or anything else) if it\n wants the region to be a real box. */\n :host {\n display: contents;\n }\n `\n\n override render() {\n return html`<slot></slot>`\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'sken-layout-region': SkenLayoutRegion\n }\n}\n"],"mappings":";;;;AAsCO,IAAM,IAAN,cAA+B,EAAW;;EACW,aAAA,KAAA,OAAA;;;EAE1C,KAAA,SAAA,CAAG;;;;;;;;;;;;CAYnB,SAAkB;EAChB,OAAO,CAAI;CACb;AACF;AAjBG,EAAA,CAAA,EAAS,EAAE,SAAS,GAAK,CAAC,CAAA,GAAA,EAAA,WAAA,QAAA,KAAA,CAAA,GAF5B,IAAA,EAAA,CAAA,EAAc,oBAAoB,CAAA,GAAA,CAAA"}
|
package/dist/sken-layout.js
CHANGED
|
@@ -2,28 +2,54 @@ import { t as e } from "./decorate-D4FKzyDw.js";
|
|
|
2
2
|
import "./sken-layout-region.js";
|
|
3
3
|
import { LitElement as t, css as n, html as r, unsafeCSS as i } from "lit";
|
|
4
4
|
import { customElement as a, property as o } from "lit/decorators.js";
|
|
5
|
+
import { SkenLayoutVariantRoles as s } from "@sken-ds/contracts";
|
|
5
6
|
//#region src/components/sken-layout.ts
|
|
6
|
-
var
|
|
7
|
+
var c = {
|
|
7
8
|
sm: "12rem",
|
|
8
9
|
md: "16rem",
|
|
9
10
|
lg: "20rem"
|
|
10
|
-
},
|
|
11
|
+
}, l = {
|
|
11
12
|
topbar: "var(--sken-card)",
|
|
12
13
|
"primary-nav": "var(--sken-background)",
|
|
13
14
|
sidebar: "var(--sken-muted)",
|
|
14
15
|
content: "var(--sken-background)",
|
|
15
16
|
footer: "var(--sken-muted)"
|
|
16
|
-
},
|
|
17
|
+
}, u = class extends t {
|
|
17
18
|
constructor(...e) {
|
|
18
|
-
super(...e), this.variant = "header-content", this.topbarMode = "fixed", this.sidebarPosition = "start", this.sidebarSize = "md", this.density = "none";
|
|
19
|
+
super(...e), this.variant = "header-content", this.topbarMode = "fixed", this.sidebarPosition = "start", this.sidebarSize = "md", this.density = "none", this.#e = null;
|
|
20
|
+
}
|
|
21
|
+
#e;
|
|
22
|
+
get #t() {
|
|
23
|
+
return s[this.variant];
|
|
24
|
+
}
|
|
25
|
+
get #n() {
|
|
26
|
+
return this.#t.includes("sidebar");
|
|
27
|
+
}
|
|
28
|
+
connectedCallback() {
|
|
29
|
+
super.connectedCallback(), this.#r(), this.#e = new MutationObserver(() => this.#r()), this.#e.observe(this, {
|
|
30
|
+
childList: !0,
|
|
31
|
+
subtree: !1
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
firstUpdated(e) {
|
|
35
|
+
super.firstUpdated(e), this.#r();
|
|
36
|
+
}
|
|
37
|
+
disconnectedCallback() {
|
|
38
|
+
super.disconnectedCallback(), this.#e?.disconnect(), this.#e = null;
|
|
39
|
+
}
|
|
40
|
+
#r() {
|
|
41
|
+
for (let e of Array.from(this.children)) {
|
|
42
|
+
if (!(e instanceof HTMLElement) || e.tagName.toLowerCase() !== "sken-layout-region") continue;
|
|
43
|
+
let t = e.getAttribute("name");
|
|
44
|
+
t && e.getAttribute("slot") !== t && e.setAttribute("slot", t);
|
|
45
|
+
}
|
|
19
46
|
}
|
|
20
47
|
static {
|
|
21
48
|
this.styles = n`
|
|
22
49
|
/* ── The host ────────────────────────────────────────────────
|
|
23
|
-
The host owns the page-level grid. The
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
projects the consumer's regions into the grid cells. */
|
|
50
|
+
The host owns the page-level grid. The topbar / primary-nav
|
|
51
|
+
/ footer sit in their own rows; the body is a sub-grid that
|
|
52
|
+
holds the sidebar (when present) and the content. */
|
|
27
53
|
:host {
|
|
28
54
|
display: grid;
|
|
29
55
|
block-size: 100dvh;
|
|
@@ -37,13 +63,40 @@ var s = {
|
|
|
37
63
|
font-family: var(--sken-family-sans, system-ui, sans-serif);
|
|
38
64
|
}
|
|
39
65
|
|
|
66
|
+
/* ── Per-cell grid items ─────────────────────────────────────
|
|
67
|
+
Each named slot sits inside a wrapper element that is the
|
|
68
|
+
actual grid item. The wrapper's class matches the role
|
|
69
|
+
(topbar, primary-nav, body, footer). The variant rules
|
|
70
|
+
below assign the right 'grid-area' to each role. The
|
|
71
|
+
'display: contents' <sken-layout-region> is projected
|
|
72
|
+
through the named <slot> and inherits the wrapper's
|
|
73
|
+
background, border, and padding. */
|
|
74
|
+
.topbar {
|
|
75
|
+
grid-area: topbar;
|
|
76
|
+
background: var(
|
|
77
|
+
--sken-layout-region-topbar-background,
|
|
78
|
+
${i(l.topbar)}
|
|
79
|
+
);
|
|
80
|
+
padding-block: var(--sken-layout-region-topbar-padding-block, 0);
|
|
81
|
+
padding-inline: var(--sken-layout-region-topbar-padding-inline, 0);
|
|
82
|
+
border-bottom: 1px solid var(--sken-border);
|
|
83
|
+
}
|
|
84
|
+
.primary-nav {
|
|
85
|
+
grid-area: primary-nav;
|
|
86
|
+
background: var(
|
|
87
|
+
--sken-layout-region-primary-nav-background,
|
|
88
|
+
${i(l["primary-nav"])}
|
|
89
|
+
);
|
|
90
|
+
padding-block: var(--sken-layout-region-primary-nav-padding-block, 0);
|
|
91
|
+
padding-inline: var(--sken-layout-region-primary-nav-padding-inline, 0);
|
|
92
|
+
border-bottom: 1px solid var(--sken-border);
|
|
93
|
+
}
|
|
40
94
|
.body {
|
|
41
95
|
display: grid;
|
|
42
96
|
grid-template-columns: 1fr;
|
|
43
97
|
grid-template-areas: 'content';
|
|
44
98
|
min-block-size: 0; /* allow children to shrink + scroll */
|
|
45
99
|
}
|
|
46
|
-
|
|
47
100
|
.body.with-sidebar {
|
|
48
101
|
grid-template-columns: var(--sken-layout-sidebar-width, 16rem) 1fr;
|
|
49
102
|
grid-template-areas: 'sidebar content';
|
|
@@ -52,73 +105,33 @@ var s = {
|
|
|
52
105
|
grid-template-columns: 1fr var(--sken-layout-sidebar-width, 16rem);
|
|
53
106
|
grid-template-areas: 'content sidebar';
|
|
54
107
|
}
|
|
55
|
-
|
|
56
|
-
/* ── Per-region grid placement + DS-driven surfaces ──────────
|
|
57
|
-
The light-DOM <sken-layout-region> children are projected
|
|
58
|
-
here through the unnamed <slot>. The grid-area rule per
|
|
59
|
-
name positions each one in the right cell. We select the
|
|
60
|
-
slotted custom elements by attribute, which works
|
|
61
|
-
transparently across the shadow boundary.
|
|
62
|
-
|
|
63
|
-
For each cell we apply:
|
|
64
|
-
- background color: the Sken surface token that matches
|
|
65
|
-
the cell's role (see REGION_BACKGROUND above). The
|
|
66
|
-
product can override per cell via
|
|
67
|
-
--sken-layout-region-{role}-background.
|
|
68
|
-
- padding: ZERO by default. The consumer decides
|
|
69
|
-
whether the topbar / sidebar / content / etc. need
|
|
70
|
-
padding and how much. This matches the iX
|
|
71
|
-
convention: the layout owns the grid + chrome
|
|
72
|
-
(background, border), and the cell contents own
|
|
73
|
-
the padding. Set density=comfortable or
|
|
74
|
-
density=compact to opt into the Sken default
|
|
75
|
-
padding scale. Per-cell override via
|
|
76
|
-
--sken-layout-region-{role}-padding-{block|inline}
|
|
77
|
-
(always wins over density).
|
|
78
|
-
- border: a single edge in the role's expected
|
|
79
|
-
direction (border-bottom for chrome above the
|
|
80
|
-
workspace, border-top for chrome below,
|
|
81
|
-
border-inline-end for the sidebar on the start
|
|
82
|
-
side). The sidebar swap for sidebar-position="end"
|
|
83
|
-
is below.
|
|
84
|
-
- min-block-size: 0 so a long content area can scroll
|
|
85
|
-
inside the host's 100dvh viewport instead of pushing
|
|
86
|
-
the layout out. */
|
|
87
|
-
::slotted(sken-layout-region[name='topbar']) {
|
|
88
|
-
grid-area: topbar;
|
|
89
|
-
background: var(--sken-layout-region-topbar-background, ${i(c.topbar)});
|
|
90
|
-
padding-block: var(--sken-layout-region-topbar-padding-block, 0);
|
|
91
|
-
padding-inline: var(--sken-layout-region-topbar-padding-inline, 0);
|
|
92
|
-
border-bottom: 1px solid var(--sken-border);
|
|
93
|
-
}
|
|
94
|
-
::slotted(sken-layout-region[name='primary-nav']) {
|
|
95
|
-
grid-area: primary-nav;
|
|
96
|
-
background: var(
|
|
97
|
-
--sken-layout-region-primary-nav-background,
|
|
98
|
-
${i(c["primary-nav"])}
|
|
99
|
-
);
|
|
100
|
-
padding-block: var(--sken-layout-region-primary-nav-padding-block, 0);
|
|
101
|
-
padding-inline: var(--sken-layout-region-primary-nav-padding-inline, 0);
|
|
102
|
-
border-bottom: 1px solid var(--sken-border);
|
|
103
|
-
}
|
|
104
|
-
::slotted(sken-layout-region[name='sidebar']) {
|
|
108
|
+
.sidebar {
|
|
105
109
|
grid-area: sidebar;
|
|
106
110
|
min-block-size: 0;
|
|
107
|
-
background: var(
|
|
111
|
+
background: var(
|
|
112
|
+
--sken-layout-region-sidebar-background,
|
|
113
|
+
${i(l.sidebar)}
|
|
114
|
+
);
|
|
108
115
|
padding-block: var(--sken-layout-region-sidebar-padding-block, 0);
|
|
109
116
|
padding-inline: var(--sken-layout-region-sidebar-padding-inline, 0);
|
|
110
117
|
border-inline-end: 1px solid var(--sken-border);
|
|
111
118
|
}
|
|
112
|
-
|
|
119
|
+
.content {
|
|
113
120
|
grid-area: content;
|
|
114
121
|
min-block-size: 0;
|
|
115
|
-
background: var(
|
|
122
|
+
background: var(
|
|
123
|
+
--sken-layout-region-content-background,
|
|
124
|
+
${i(l.content)}
|
|
125
|
+
);
|
|
116
126
|
padding-block: var(--sken-layout-region-content-padding-block, 0);
|
|
117
127
|
padding-inline: var(--sken-layout-region-content-padding-inline, 0);
|
|
118
128
|
}
|
|
119
|
-
|
|
129
|
+
.footer {
|
|
120
130
|
grid-area: footer;
|
|
121
|
-
background: var(
|
|
131
|
+
background: var(
|
|
132
|
+
--sken-layout-region-footer-background,
|
|
133
|
+
${i(l.footer)}
|
|
134
|
+
);
|
|
122
135
|
padding-block: var(--sken-layout-region-footer-padding-block, 0);
|
|
123
136
|
padding-inline: var(--sken-layout-region-footer-padding-inline, 0);
|
|
124
137
|
border-top: 1px solid var(--sken-border);
|
|
@@ -133,7 +146,11 @@ var s = {
|
|
|
133
146
|
every cell. A consumer override on any
|
|
134
147
|
--sken-layout-region-{role}-padding-{block|inline}
|
|
135
148
|
always wins over the density preset. */
|
|
136
|
-
:host([density='comfortable'])
|
|
149
|
+
:host([density='comfortable']) .topbar,
|
|
150
|
+
:host([density='comfortable']) .primary-nav,
|
|
151
|
+
:host([density='comfortable']) .content,
|
|
152
|
+
:host([density='comfortable']) .sidebar,
|
|
153
|
+
:host([density='comfortable']) .footer {
|
|
137
154
|
--sken-layout-region-topbar-padding-block: var(--sken-2);
|
|
138
155
|
--sken-layout-region-topbar-padding-inline: var(--sken-4);
|
|
139
156
|
--sken-layout-region-primary-nav-padding-block: var(--sken-2);
|
|
@@ -154,7 +171,11 @@ var s = {
|
|
|
154
171
|
comfortable, a consumer override on any
|
|
155
172
|
--sken-layout-region-{role}-padding-{block|inline}
|
|
156
173
|
always wins. */
|
|
157
|
-
:host([density='compact'])
|
|
174
|
+
:host([density='compact']) .topbar,
|
|
175
|
+
:host([density='compact']) .primary-nav,
|
|
176
|
+
:host([density='compact']) .content,
|
|
177
|
+
:host([density='compact']) .sidebar,
|
|
178
|
+
:host([density='compact']) .footer {
|
|
158
179
|
--sken-layout-region-topbar-padding-block: calc(var(--sken-2) * 0.5);
|
|
159
180
|
--sken-layout-region-topbar-padding-inline: calc(var(--sken-4) * 0.5);
|
|
160
181
|
--sken-layout-region-primary-nav-padding-block: calc(var(--sken-2) * 0.5);
|
|
@@ -167,7 +188,15 @@ var s = {
|
|
|
167
188
|
--sken-layout-region-footer-padding-inline: calc(var(--sken-4) * 0.5);
|
|
168
189
|
}
|
|
169
190
|
|
|
170
|
-
/* ── Grid templates per variant ───────────────────────────────
|
|
191
|
+
/* ── Grid templates per variant ───────────────────────────────
|
|
192
|
+
Each variant declares which rows the host grid reserves.
|
|
193
|
+
The default host grid is 2 rows (topbar + body); variants
|
|
194
|
+
that include primary-nav or footer add more rows. Rows
|
|
195
|
+
with no rendered content (e.g. footer on a variant that
|
|
196
|
+
does not include a footer) collapse to 0 because the
|
|
197
|
+
variant does not render the matching wrapper, and an
|
|
198
|
+
empty 'auto' row in a grid takes the height of its
|
|
199
|
+
content (which is 0). */
|
|
171
200
|
:host([variant='full-bleed']) {
|
|
172
201
|
grid-template-areas: 'body';
|
|
173
202
|
grid-template-rows: 1fr;
|
|
@@ -182,10 +211,6 @@ var s = {
|
|
|
182
211
|
grid-template-rows: 1fr;
|
|
183
212
|
grid-template-areas: 'body';
|
|
184
213
|
}
|
|
185
|
-
:host([variant='sidebar-content']) .body {
|
|
186
|
-
grid-template-columns: var(--sken-layout-sidebar-width, 16rem) 1fr;
|
|
187
|
-
grid-template-areas: 'sidebar content';
|
|
188
|
-
}
|
|
189
214
|
:host([variant='header-sidebar']) {
|
|
190
215
|
grid-template-rows: auto auto 1fr;
|
|
191
216
|
grid-template-areas:
|
|
@@ -217,79 +242,61 @@ var s = {
|
|
|
217
242
|
moves to the inline-start side. The default rule above
|
|
218
243
|
already paints the border on the inline-end; here we
|
|
219
244
|
swap it. */
|
|
220
|
-
:host([sidebar-position='end'])
|
|
245
|
+
:host([sidebar-position='end']) .sidebar {
|
|
221
246
|
border-inline-end: none;
|
|
222
247
|
border-inline-start: 1px solid var(--sken-border);
|
|
223
248
|
}
|
|
224
249
|
|
|
225
250
|
/* ── Sidebar width override ─────────────────────────────────── */
|
|
226
251
|
:host([sidebar-size='sm']) {
|
|
227
|
-
--sken-layout-sidebar-width: ${i(
|
|
252
|
+
--sken-layout-sidebar-width: ${i(c.sm)};
|
|
228
253
|
}
|
|
229
254
|
:host([sidebar-size='md']) {
|
|
230
|
-
--sken-layout-sidebar-width: ${i(
|
|
255
|
+
--sken-layout-sidebar-width: ${i(c.md)};
|
|
231
256
|
}
|
|
232
257
|
:host([sidebar-size='lg']) {
|
|
233
|
-
--sken-layout-sidebar-width: ${i(
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/* ── Variant-aware region hiding ─────────────────────────────
|
|
237
|
-
Roles the active variant does not include are hidden
|
|
238
|
-
outright. This way a consumer that always renders the
|
|
239
|
-
same set of regions (e.g. a shared layout component) does
|
|
240
|
-
not get phantom space when a page uses a different
|
|
241
|
-
variant. */
|
|
242
|
-
:host([variant='full-bleed']) ::slotted(sken-layout-region[name='topbar']),
|
|
243
|
-
:host([variant='full-bleed']) ::slotted(sken-layout-region[name='primary-nav']),
|
|
244
|
-
:host([variant='full-bleed']) ::slotted(sken-layout-region[name='sidebar']),
|
|
245
|
-
:host([variant='full-bleed']) ::slotted(sken-layout-region[name='footer']) {
|
|
246
|
-
display: none;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
:host([variant='header-content']) ::slotted(sken-layout-region[name='primary-nav']),
|
|
250
|
-
:host([variant='header-content']) ::slotted(sken-layout-region[name='sidebar']),
|
|
251
|
-
:host([variant='header-content']) ::slotted(sken-layout-region[name='footer']) {
|
|
252
|
-
display: none;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
:host([variant='sidebar-content']) ::slotted(sken-layout-region[name='topbar']),
|
|
256
|
-
:host([variant='sidebar-content']) ::slotted(sken-layout-region[name='primary-nav']),
|
|
257
|
-
:host([variant='sidebar-content']) ::slotted(sken-layout-region[name='footer']) {
|
|
258
|
-
display: none;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
:host([variant='header-sidebar']) ::slotted(sken-layout-region[name='footer']) {
|
|
262
|
-
display: none;
|
|
258
|
+
--sken-layout-sidebar-width: ${i(c.lg)};
|
|
263
259
|
}
|
|
264
260
|
`;
|
|
265
261
|
}
|
|
266
262
|
render() {
|
|
267
|
-
let e = this.#
|
|
263
|
+
let e = this.#t, t = this.#n, n = [
|
|
268
264
|
"body",
|
|
269
|
-
|
|
270
|
-
|
|
265
|
+
t ? "with-sidebar" : "",
|
|
266
|
+
t && this.sidebarPosition === "end" ? "sidebar-end" : ""
|
|
271
267
|
].filter(Boolean).join(" ");
|
|
272
268
|
return r`
|
|
273
|
-
|
|
274
|
-
|
|
269
|
+
${e.includes("topbar") ? r`<div class="topbar" part="topbar">
|
|
270
|
+
<slot name="topbar"></slot>
|
|
271
|
+
</div>` : ""}
|
|
272
|
+
${e.includes("primary-nav") ? r`<div class="primary-nav" part="primary-nav">
|
|
273
|
+
<slot name="primary-nav"></slot>
|
|
274
|
+
</div>` : ""}
|
|
275
|
+
<div class=${n} part="body">
|
|
276
|
+
${t ? r`<div class="sidebar" part="sidebar">
|
|
277
|
+
<slot name="sidebar"></slot>
|
|
278
|
+
</div>` : ""}
|
|
279
|
+
<div class="content" part="content">
|
|
280
|
+
<slot name="content"></slot>
|
|
281
|
+
</div>
|
|
275
282
|
</div>
|
|
283
|
+
${e.includes("footer") ? r`<div class="footer" part="footer">
|
|
284
|
+
<slot name="footer"></slot>
|
|
285
|
+
</div>` : ""}
|
|
276
286
|
`;
|
|
277
287
|
}
|
|
278
|
-
#e(e) {
|
|
279
|
-
return e === "sidebar-content" || e === "header-sidebar" || e === "header-sidebar-footer";
|
|
280
|
-
}
|
|
281
288
|
};
|
|
282
|
-
e([o({ reflect: !0 })],
|
|
289
|
+
e([o({ reflect: !0 })], u.prototype, "variant", void 0), e([o({
|
|
283
290
|
attribute: "topbar-mode",
|
|
284
291
|
reflect: !0
|
|
285
|
-
})],
|
|
292
|
+
})], u.prototype, "topbarMode", void 0), e([o({
|
|
286
293
|
attribute: "sidebar-position",
|
|
287
294
|
reflect: !0
|
|
288
|
-
})],
|
|
295
|
+
})], u.prototype, "sidebarPosition", void 0), e([o({
|
|
289
296
|
attribute: "sidebar-size",
|
|
290
297
|
reflect: !0
|
|
291
|
-
})],
|
|
298
|
+
})], u.prototype, "sidebarSize", void 0), e([o({ reflect: !0 })], u.prototype, "density", void 0), u = e([a("sken-layout")], u);
|
|
292
299
|
//#endregion
|
|
293
|
-
export {
|
|
300
|
+
export { u as SkenLayout };
|
|
294
301
|
|
|
295
302
|
//# sourceMappingURL=sken-layout.js.map
|
package/dist/sken-layout.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sken-layout.js","names":["#variantHasSidebar"],"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:\n// - We project the light-DOM regions into a shadow-DOM grid via\n// a single <slot>. Inside the shadow we own the grid template\n// + per-name `grid-area` CSS rules; the consumer's regions\n// sit in the slot. Empty regions (no consumer region for a\n// name) simply do not project; the grid still has the area\n// declared but the slot stays empty, so no space is reserved.\n// - We DO NOT use named <slot name=\"topbar\"> etc. because that\n// would force the consumer to keep the regions in a fixed\n// order. A single unnamed <slot> lets the consumer arrange\n// the regions in any order; the grid-area CSS rule per name\n// is what places them in the layout. (A Vue adapter that\n// renders regions via <template v-for> is therefore free.)\n// - For roles the active variant does not include, we hide\n// the region via `display: none` so a consumer that always\n// renders the same set of regions (e.g. from a layout\n// component shared across pages) does not get phantom space.\n// The CSS rule uses `:host([variant=…])` so it does not\n// depend on JS state.\n//\n// See ADR-0006 for the substrate decision. See the\n// @sken-ds/contracts types `SkenLayoutProps`, `SkenLayoutSlots`,\n// `SkenLayoutVariant` for the contract surface.\n\nimport { LitElement, html, css, unsafeCSS } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\nimport type {\n SkenLayoutVariant,\n SkenLayoutTopbarMode,\n SkenLayoutSidebarPosition,\n SkenLayoutSidebarSize,\n SkenLayoutDensity,\n} from '@sken-ds/contracts'\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<string, 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 static styles = css`\n /* ── The host ────────────────────────────────────────────────\n The host owns the page-level grid. The body row is a\n sub-grid; the topbar / primary-nav / footer sit in their\n own rows. The shadow DOM owns the layout; the slot inside\n projects the consumer's regions into the grid cells. */\n :host {\n display: grid;\n block-size: 100dvh;\n grid-template-columns: 1fr;\n grid-template-rows: auto 1fr;\n grid-template-areas:\n 'topbar'\n 'body';\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 .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\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\n /* ── Per-region grid placement + DS-driven surfaces ──────────\n The light-DOM <sken-layout-region> children are projected\n here through the unnamed <slot>. The grid-area rule per\n name positions each one in the right cell. We select the\n slotted custom elements by attribute, which works\n transparently across the shadow boundary.\n\n For each cell we apply:\n - background color: the Sken surface token that matches\n the cell's role (see REGION_BACKGROUND above). The\n product can override per cell via\n --sken-layout-region-{role}-background.\n - padding: ZERO by default. The consumer decides\n whether the topbar / sidebar / content / etc. need\n padding and how much. This matches the iX\n convention: the layout owns the grid + chrome\n (background, border), and the cell contents own\n the padding. Set density=comfortable or\n density=compact to opt into the Sken default\n padding scale. Per-cell override via\n --sken-layout-region-{role}-padding-{block|inline}\n (always wins over density).\n - border: a single edge in the role's expected\n direction (border-bottom for chrome above the\n workspace, border-top for chrome below,\n border-inline-end for the sidebar on the start\n side). The sidebar swap for sidebar-position=\"end\"\n is below.\n - min-block-size: 0 so a long content area can scroll\n inside the host's 100dvh viewport instead of pushing\n the layout out. */\n ::slotted(sken-layout-region[name='topbar']) {\n grid-area: topbar;\n background: var(--sken-layout-region-topbar-background, ${unsafeCSS(REGION_BACKGROUND.topbar)});\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 ::slotted(sken-layout-region[name='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 ::slotted(sken-layout-region[name='sidebar']) {\n grid-area: sidebar;\n min-block-size: 0;\n background: var(--sken-layout-region-sidebar-background, ${unsafeCSS(REGION_BACKGROUND.sidebar)});\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 ::slotted(sken-layout-region[name='content']) {\n grid-area: content;\n min-block-size: 0;\n background: var(--sken-layout-region-content-background, ${unsafeCSS(REGION_BACKGROUND.content)});\n padding-block: var(--sken-layout-region-content-padding-block, 0);\n padding-inline: var(--sken-layout-region-content-padding-inline, 0);\n }\n ::slotted(sken-layout-region[name='footer']) {\n grid-area: footer;\n background: var(--sken-layout-region-footer-background, ${unsafeCSS(REGION_BACKGROUND.footer)});\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']) ::slotted(sken-layout-region) {\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']) ::slotted(sken-layout-region) {\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 :host([variant='full-bleed']) {\n grid-template-areas: 'body';\n grid-template-rows: 1fr;\n }\n :host([variant='header-content']) {\n grid-template-areas:\n 'topbar'\n 'body';\n grid-template-rows: auto 1fr;\n }\n :host([variant='sidebar-content']) {\n grid-template-rows: 1fr;\n grid-template-areas: 'body';\n }\n :host([variant='sidebar-content']) .body {\n grid-template-columns: var(--sken-layout-sidebar-width, 16rem) 1fr;\n grid-template-areas: 'sidebar content';\n }\n :host([variant='header-sidebar']) {\n grid-template-rows: auto auto 1fr;\n grid-template-areas:\n 'topbar'\n 'primary-nav'\n 'body';\n }\n :host([variant='header-sidebar-footer']) {\n grid-template-rows: auto auto 1fr auto;\n grid-template-areas:\n 'topbar'\n 'primary-nav'\n 'body'\n 'footer';\n }\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']) ::slotted(sken-layout-region[name='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 /* ── Variant-aware region hiding ─────────────────────────────\n Roles the active variant does not include are hidden\n outright. This way a consumer that always renders the\n same set of regions (e.g. a shared layout component) does\n not get phantom space when a page uses a different\n variant. */\n :host([variant='full-bleed']) ::slotted(sken-layout-region[name='topbar']),\n :host([variant='full-bleed']) ::slotted(sken-layout-region[name='primary-nav']),\n :host([variant='full-bleed']) ::slotted(sken-layout-region[name='sidebar']),\n :host([variant='full-bleed']) ::slotted(sken-layout-region[name='footer']) {\n display: none;\n }\n\n :host([variant='header-content']) ::slotted(sken-layout-region[name='primary-nav']),\n :host([variant='header-content']) ::slotted(sken-layout-region[name='sidebar']),\n :host([variant='header-content']) ::slotted(sken-layout-region[name='footer']) {\n display: none;\n }\n\n :host([variant='sidebar-content']) ::slotted(sken-layout-region[name='topbar']),\n :host([variant='sidebar-content']) ::slotted(sken-layout-region[name='primary-nav']),\n :host([variant='sidebar-content']) ::slotted(sken-layout-region[name='footer']) {\n display: none;\n }\n\n :host([variant='header-sidebar']) ::slotted(sken-layout-region[name='footer']) {\n display: none;\n }\n `\n\n override render() {\n const hasSidebar = this.#variantHasSidebar(this.variant)\n const bodyClasses = [\n 'body',\n hasSidebar ? 'with-sidebar' : '',\n hasSidebar && this.sidebarPosition === 'end' ? 'sidebar-end' : '',\n ]\n .filter(Boolean)\n .join(' ')\n\n return html`\n <div class=${bodyClasses} part=\"body\">\n <slot></slot>\n </div>\n `\n }\n\n /**\n * Single source of truth for which variants include a sidebar.\n * Used to apply the `.with-sidebar` class on the body grid\n * wrapper. Mirrors the per-variant CSS templates above.\n */\n #variantHasSidebar(variant: SkenLayoutVariant): boolean {\n return variant === 'sidebar-content' || variant === 'header-sidebar' || variant === 'header-sidebar-footer'\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'sken-layout': SkenLayout\n }\n}\n"],"mappings":";;;;;AAoDA,IAAM,IAAuD;CAC3D,IAAI;CACJ,IAAI;CACJ,IAAI;AACN,GAsBM,IAA4C;CAChD,QAAQ;CACR,eAAe;CACf,SAAS;CACT,SAAS;CACT,QAAQ;AACV,GAGa,IAAN,cAAyB,EAAW;;EAMiB,aALA,KAAA,UAAA,kBACgC,KAAA,aAAA,SAE7C,KAAA,kBAAA,SACgD,KAAA,cAAA,MACnC,KAAA,UAAA;;;EAE1C,KAAA,SAAA,CAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gEAoE2C,EAAU,EAAkB,MAAM,EAAE;;;;;;;;;UAS1F,EAAU,EAAkB,cAAc,EAAE;;;;;;;;;iEASW,EAAU,EAAkB,OAAO,EAAE;;;;;;;;iEAQrC,EAAU,EAAkB,OAAO,EAAE;;;;;;gEAMtC,EAAU,EAAkB,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qCA0G/D,EAAU,EAAc,EAAE,EAAE;;;qCAG5B,EAAU,EAAc,EAAE,EAAE;;;qCAG5B,EAAU,EAAc,EAAE,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiC/D,SAAkB;EAChB,IAAM,IAAa,KAAKA,GAAmB,KAAK,OAAO,GACjD,IAAc;GAClB;GACA,IAAa,iBAAiB;GAC9B,KAAc,KAAK,oBAAoB,QAAQ,gBAAgB;EACjE,CAAC,CACE,OAAO,OAAO,CAAC,CACf,KAAK,GAAG;EAEX,OAAO,CAAI;mBACI,EAAY;;;;CAI7B;CAOA,GAAmB,GAAqC;EACtD,OAAO,MAAY,qBAAqB,MAAY,oBAAoB,MAAY;CACtF;AACF;AArRG,EAAA,CAAA,EAAS,EAAE,SAAS,GAAK,CAAC,CAAA,GAAA,EAAA,WAAA,WAAA,KAAA,CAAA,GAC1B,EAAA,CAAA,EAAS;CAAE,WAAW;CAAe,SAAS;AAAK,CAAC,CAAA,GAAA,EAAA,WAAA,cAAA,KAAA,CAAA,GACpD,EAAA,CAAA,EAAS;CAAE,WAAW;CAAoB,SAAS;AAAK,CAAC,CAAA,GAAA,EAAA,WAAA,mBAAA,KAAA,CAAA,GAEzD,EAAA,CAAA,EAAS;CAAE,WAAW;CAAgB,SAAS;AAAK,CAAC,CAAA,GAAA,EAAA,WAAA,eAAA,KAAA,CAAA,GACrD,EAAA,CAAA,EAAS,EAAE,SAAS,GAAK,CAAC,CAAA,GAAA,EAAA,WAAA,WAAA,KAAA,CAAA,GAP5B,IAAA,EAAA,CAAA,EAAc,aAAa,CAAA,GAAA,CAAA"}
|
|
1
|
+
{"version":3,"file":"sken-layout.js","names":["#activeRoles","#hasSidebar","#syncRegionSlotAttributes","#regionSlotObserver"],"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.3.10 — fix for SKEN-LAYOUT-REGIONS-RENDERING.md):\n// - The shadow root projects the consumer's light-DOM regions via\n// NAMED slots, one per role (topbar, primary-nav, sidebar,\n// content, footer). 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. The wrapper is needed\n// because the consumer's <sken-layout-region> is `display:\n// contents` and would otherwise collapse the grid item away.\n// - Only the slots the active variant includes are rendered.\n// The single source of truth is SkenLayoutVariantRoles in\n// @sken-ds/contracts. Roles the variant does not include\n// simply do not appear in the shadow root, so the host's\n// grid template (which is fixed per variant) renders the\n// remaining rows at the height of their content.\n// - This is a behavioural fix over the previous design (single\n// unnamed slot + variant-aware `display: none` rules on\n// `::slotted()`), which placed every region inside the\n// `.body` sub-grid and therefore prevented the host's\n// `grid-template-areas` from placing them in the correct\n// row (the footer ended up next to the topbar in row 0).\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 /** True if the active variant renders a sidebar region. */\n get #hasSidebar(): boolean {\n return this.#activeRoles.includes('sidebar')\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 this.#syncRegionSlotAttributes()\n this.#regionSlotObserver = new MutationObserver(() => this.#syncRegionSlotAttributes())\n this.#regionSlotObserver.observe(this, { childList: true, subtree: false })\n }\n\n /**\n * Lit lifecycle hook called after the first render. We use\n * it as a second-pass sync point because some regions may\n * have been added to the host before the host was\n * connected, with their `name` property set programmatically\n * (e.g. via the Vue adapter setting `region.name = 'topbar'`\n * before the region is upgraded to a custom element). 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 — which is exactly what `firstUpdated`\n * gives us. Without this second pass, a region whose `name`\n * was set as a property (not as an attribute) would not\n * have its `slot` attribute set by the time the browser\n * tries to project it.\n */\n protected override firstUpdated(_changedProperties: PropertyValues): void {\n super.firstUpdated(_changedProperties)\n this.#syncRegionSlotAttributes()\n }\n\n override disconnectedCallback(): void {\n super.disconnectedCallback()\n this.#regionSlotObserver?.disconnect()\n this.#regionSlotObserver = null\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: grid;\n block-size: 100dvh;\n grid-template-columns: 1fr;\n grid-template-rows: auto 1fr;\n grid-template-areas:\n 'topbar'\n 'body';\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 /* ── 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 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 grid-area: 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 Each variant declares which rows the host grid reserves.\n The default host grid is 2 rows (topbar + body); variants\n that include primary-nav or footer add more rows. Rows\n with no rendered content (e.g. footer on a variant that\n does not include a footer) collapse to 0 because the\n variant does not render the matching wrapper, and an\n empty 'auto' row in a grid takes the height of its\n content (which is 0). */\n :host([variant='full-bleed']) {\n grid-template-areas: 'body';\n grid-template-rows: 1fr;\n }\n :host([variant='header-content']) {\n grid-template-areas:\n 'topbar'\n 'body';\n grid-template-rows: auto 1fr;\n }\n :host([variant='sidebar-content']) {\n grid-template-rows: 1fr;\n grid-template-areas: 'body';\n }\n :host([variant='header-sidebar']) {\n grid-template-rows: auto auto 1fr;\n grid-template-areas:\n 'topbar'\n 'primary-nav'\n 'body';\n }\n :host([variant='header-sidebar-footer']) {\n grid-template-rows: auto auto 1fr auto;\n grid-template-areas:\n 'topbar'\n 'primary-nav'\n 'body'\n 'footer';\n }\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 const roles = this.#activeRoles\n const hasSidebar = this.#hasSidebar\n const bodyClasses = [\n 'body',\n hasSidebar ? 'with-sidebar' : '',\n hasSidebar && this.sidebarPosition === 'end' ? 'sidebar-end' : '',\n ]\n .filter(Boolean)\n .join(' ')\n\n return html`\n ${\n roles.includes('topbar')\n ? html`<div class=\"topbar\" part=\"topbar\">\n <slot name=\"topbar\"></slot>\n </div>`\n : ''\n }\n ${\n roles.includes('primary-nav')\n ? html`<div class=\"primary-nav\" part=\"primary-nav\">\n <slot name=\"primary-nav\"></slot>\n </div>`\n : ''\n }\n <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 roles.includes('footer')\n ? html`<div class=\"footer\" part=\"footer\">\n <slot name=\"footer\"></slot>\n </div>`\n : ''\n }\n `\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'sken-layout': SkenLayout\n }\n}\n"],"mappings":";;;;;;AAyDA,IAAM,IAAuD;CAC3D,IAAI;CACJ,IAAI;CACJ,IAAI;AACN,GAsBM,IAA0D;CAC9D,QAAQ;CACR,eAAe;CACf,SAAS;CACT,SAAS;CACT,QAAQ;AACV,GAGa,IAAN,cAAyB,EAAW;;EA6BM,aA5BW,KAAA,UAAA,kBACgC,KAAA,aAAA,SAE7C,KAAA,kBAAA,SACgD,KAAA,cAAA,MACnC,KAAA,UAAA,QAuBX,KAAA,KAAA;;CAA/C;CASA,IAAIA,KAAgD;EAClD,OAAQ,EAAqD,KAAK;CACpE;CAGA,IAAIC,KAAuB;EACzB,OAAO,KAAKD,GAAa,SAAS,SAAS;CAC7C;CAEA,oBAAmC;EAUjC,AATA,MAAM,kBAAkB,GAOxB,KAAKE,GAA0B,GAC/B,KAAKC,KAAsB,IAAI,uBAAuB,KAAKD,GAA0B,CAAC,GACtF,KAAKC,GAAoB,QAAQ,MAAM;GAAE,WAAW;GAAM,SAAS;EAAM,CAAC;CAC5E;CAiBA,aAAgC,GAA0C;EAExE,AADA,MAAM,aAAa,CAAkB,GACrC,KAAKD,GAA0B;CACjC;CAEA,uBAAsC;EAGpC,AAFA,MAAM,qBAAqB,GAC3B,KAAKC,IAAqB,WAAW,GACrC,KAAKA,KAAsB;CAC7B;CAeA,KAAkC;EAChC,KAAK,IAAM,KAAS,MAAM,KAAK,KAAK,QAAQ,GAAG;GAE7C,IADI,EAAE,aAAiB,gBACnB,EAAM,QAAQ,YAAY,MAAM,sBAAsB;GAC1D,IAAM,IAAO,EAAM,aAAa,MAAM;GACjC,KACD,EAAM,aAAa,MAAM,MAAM,KACnC,EAAM,aAAa,QAAQ,CAAI;EACjC;CACF;;EAEgB,KAAA,SAAA,CAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UA8BX,EAAU,EAAkB,MAAM,EAAE;;;;;;;;;;UAUpC,EAAU,EAAkB,cAAc,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;UAyB5C,EAAU,EAAkB,OAAO,EAAE;;;;;;;;;;;UAWrC,EAAU,EAAkB,OAAO,EAAE;;;;;;;;;UASrC,EAAU,EAAkB,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qCAuHT,EAAU,EAAc,EAAE,EAAE;;;qCAG5B,EAAU,EAAc,EAAE,EAAE;;;qCAG5B,EAAU,EAAc,EAAE,EAAE;;;;CAI/D,SAAkB;EAChB,IAAM,IAAQ,KAAKH,IACb,IAAa,KAAKC,IAClB,IAAc;GAClB;GACA,IAAa,iBAAiB;GAC9B,KAAc,KAAK,oBAAoB,QAAQ,gBAAgB;EACjE,CAAC,CACE,OAAO,OAAO,CAAC,CACf,KAAK,GAAG;EAEX,OAAO,CAAI;QAEP,EAAM,SAAS,QAAQ,IACnB,CAAI;;sBAGJ,GACL;QAEC,EAAM,SAAS,aAAa,IACxB,CAAI;;sBAGJ,GACL;mBACY,EAAY;UAErB,IACI,CAAI;;wBAGJ,GACL;;;;;QAMD,EAAM,SAAS,QAAQ,IACnB,CAAI;;sBAGJ,GACL;;CAEL;AACF;AAlXG,EAAA,CAAA,EAAS,EAAE,SAAS,GAAK,CAAC,CAAA,GAAA,EAAA,WAAA,WAAA,KAAA,CAAA,GAC1B,EAAA,CAAA,EAAS;CAAE,WAAW;CAAe,SAAS;AAAK,CAAC,CAAA,GAAA,EAAA,WAAA,cAAA,KAAA,CAAA,GACpD,EAAA,CAAA,EAAS;CAAE,WAAW;CAAoB,SAAS;AAAK,CAAC,CAAA,GAAA,EAAA,WAAA,mBAAA,KAAA,CAAA,GAEzD,EAAA,CAAA,EAAS;CAAE,WAAW;CAAgB,SAAS;AAAK,CAAC,CAAA,GAAA,EAAA,WAAA,eAAA,KAAA,CAAA,GACrD,EAAA,CAAA,EAAS,EAAE,SAAS,GAAK,CAAC,CAAA,GAAA,EAAA,WAAA,WAAA,KAAA,CAAA,GAP5B,IAAA,EAAA,CAAA,EAAc,aAAa,CAAA,GAAA,CAAA"}
|
|
@@ -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 && this.max !== undefined && 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 && this.min !== undefined && 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 } 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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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sken-ds/primitives",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"registry": "https://registry.npmjs.org",
|
|
6
6
|
"access": "public"
|
|
@@ -92,7 +92,7 @@
|
|
|
92
92
|
"@tanstack/match-sorter-utils": "^9.0.0",
|
|
93
93
|
"@tanstack/table-core": "^9.0.0",
|
|
94
94
|
"lit": "^3.3.3",
|
|
95
|
-
"@sken-ds/contracts": "0.
|
|
95
|
+
"@sken-ds/contracts": "0.4.1"
|
|
96
96
|
},
|
|
97
97
|
"devDependencies": {
|
|
98
98
|
"@vitest/coverage-v8": "^4.1.10",
|