@sken-ds/primitives 0.3.7 → 0.3.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,2 +1,2 @@
1
- import { t as e } from "./sken-datatable-BtR-NwRN.js";
1
+ import { t as e } from "./sken-datatable-ChDjB_T_.js";
2
2
  export { e as SkenDataTable };
@@ -1 +1 @@
1
- {"version":3,"file":"sken-date-picker.js","names":[],"sources":["../src/components/sken-date-picker.ts"],"sourcesContent":["/**\n * `<sken-date-picker>` — Sken contract implementation backed by\n * Siemens iX 5's `<ix-date-input>`.\n *\n * The Sken contract (`SkenDatePickerProps`, `SkenDatePickerEmits`,\n * `skenDatePickerAria`) lives in `@sken-ds/contracts`. This component\n * is a thin Lit 3 wrapper that:\n *\n * 1. Accepts the Sken contract props on a `<sken-date-picker>`\n * custom element registered via `@customElement`.\n * 2. Renders a single `<ix-date-input>` with the Sken-shaped\n * `value` mapped to iX's `value` prop, the Sken `label`\n * rendered as an `<ix-label>` child, and the Sken\n * `helperText` rendered as an `<ix-typography>` child in\n * the `end` slot.\n * 3. Translates iX's `valueChange` event into Sken's\n * `sken-change` CustomEvent whose `detail.value` is the new\n * ISO 8601 date string or `null`.\n *\n * Subpath: import from `@sken-ds/primitives/sken-date-picker` to\n * register the custom element.\n *\n * Consumer boot sequence (same as `<sken-combobox>`):\n *\n * 1. `import '@siemens/ix/dist/siemens-ix/siemens-ix.css'`\n * 2. `defineCustomElements(window)` for both `@siemens/ix/loader`\n * and `@siemens/ix-icons/loader`, BEFORE any\n * `<sken-date-picker>` renders (Stencil does not re-hydrate).\n * 3. `import '@sken-ds/primitives/ix-bridge.css'` once at app boot\n * so the date-picker popover (`<ix-date-time-card>`,\n * `<ix-date-picker>`) renders in Sken colours instead of\n * iX classic light values. The consumer is responsible for\n * setting `data-theme` on a common ancestor.\n */\nimport { LitElement, html } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\n\nimport type { SkenDatePickerProps } from '@sken-ds/contracts'\n\n@customElement('sken-date-picker')\nexport class SkenDatePicker\n extends LitElement\n implements\n Pick<\n SkenDatePickerProps,\n | 'value'\n | 'label'\n | 'helperText'\n | 'placeholder'\n | 'disabled'\n | 'format'\n | 'minDate'\n | 'maxDate'\n | 'locale'\n >\n{\n /** Controlled value (ISO 8601 `yyyy-MM-dd`). `null` means no selection. */\n @property({ attribute: false })\n value: string | null = null\n\n /** Visible label rendered above the input. */\n @property() label = ''\n\n /** Helper text rendered below the input. */\n @property({ attribute: 'helper-text' })\n helperText = ''\n\n /** Placeholder shown when the input is empty. */\n @property() placeholder = ''\n\n /** Disabled state. @default false */\n @property({ type: Boolean })\n disabled = false\n\n /** Display format. Luxon-style tokens. @default \"yyyy-MM-dd\" */\n @property() format = 'yyyy-MM-dd'\n\n /** Earliest selectable date (ISO 8601 `yyyy-MM-dd`). */\n @property({ attribute: 'min-date' })\n minDate?: string\n\n /** Latest selectable date (ISO 8601 `yyyy-MM-dd`). */\n @property({ attribute: 'max-date' })\n maxDate?: string\n\n /** BCP 47 locale tag. */\n @property() locale?: string\n\n /**\n * Translate iX's `valueChange` event into Sken's `sken-change`\n * CustomEvent. iX 5 emits `valueChange` with `detail` set to\n * a `string | undefined` directly (not an object). We pass\n * the value through as-is so the consumer gets the same\n * string format they passed in; an empty value maps to\n * `null` so the Sken contract stays `string | null`.\n */\n private onIxValueChange = (e: Event) => {\n const detail = (e as CustomEvent<string | undefined>).detail\n const next = detail == null || detail === '' ? null : detail\n if (next === this.value) return\n this.value = next\n this.dispatchEvent(\n new CustomEvent<{ value: string | null }>('sken-change', {\n detail: { value: next },\n bubbles: true,\n composed: true,\n }),\n )\n }\n\n // Light DOM so Sken tokens (CSS custom properties) cascade\n // from the document root into the iX shadow tree. The re-skin\n // mapping (`--ix-*` → Sken tokens) lives in\n // `@sken-ds/primitives/ix-bridge.css`, which the consumer\n // imports once at app boot.\n protected override createRenderRoot(): HTMLElement {\n return this as unknown as HTMLElement\n }\n\n protected override render() {\n return html`\n <ix-date-input\n .value=${this.value ?? ''}\n format=${this.format}\n ?disabled=${this.disabled}\n placeholder=${this.placeholder}\n min-date=${this.minDate ?? ''}\n max-date=${this.maxDate ?? ''}\n locale=${this.locale ?? ''}\n data-testid=\"sken-date-picker\"\n @valueChange=${this.onIxValueChange}\n >\n ${this.label ? html`<ix-label>${this.label}</ix-label>` : null}\n ${this.helperText\n ? html`<ix-typography slot=\"end\">${this.helperText}</ix-typography>`\n : null}\n </ix-date-input>\n `\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'sken-date-picker': SkenDatePicker\n }\n}\n"],"mappings":";;;;AAwCO,IAAM,IAAN,cACG,EAcV;;EAyC6B,aAtCJ,KAAA,QAAA,MAGH,KAAA,QAAA,IAIP,KAAA,aAAA,IAGa,KAAA,cAAA,IAIf,KAAA,WAAA,IAGU,KAAA,SAAA,cAqBM,KAAA,mBAAA,MAAa;GACtC,IAAM,IAAU,EAAsC,QAChD,IAAO,KAAU,QAAQ,MAAW,KAAK,OAAO;GAClD,MAAS,KAAK,UAClB,KAAK,QAAQ,GACb,KAAK,cACH,IAAI,YAAsC,eAAe;IACvD,QAAQ,EAAE,OAAO,EAAK;IACtB,SAAS;IACT,UAAU;GACZ,CAAC,CACH;EACF;;CAOA,mBAAmD;EACjD,OAAO;CACT;CAEA,SAA4B;EAC1B,OAAO,CAAI;;iBAEE,KAAK,SAAS,GAAG;iBACjB,KAAK,OAAO;oBACT,KAAK,SAAS;sBACZ,KAAK,YAAY;mBACpB,KAAK,WAAW,GAAG;mBACnB,KAAK,WAAW,GAAG;iBACrB,KAAK,UAAU,GAAG;;uBAEZ,KAAK,gBAAgB;;UAElC,KAAK,QAAQ,CAAI,aAAa,KAAK,MAAM,eAAe,KAAK;UAC7D,KAAK,aACH,CAAI,6BAA6B,KAAK,WAAW,oBACjD,KAAK;;;CAGf;AACF;AAlFG,EAAA,CAAA,EAAS,EAAE,WAAW,GAAM,CAAC,CAAA,GAAA,EAAA,WAAA,SAAA,KAAA,CAAA,GAI7B,EAAA,CAAA,EAAS,CAAA,GAAA,EAAA,WAAA,SAAA,KAAA,CAAA,GAGT,EAAA,CAAA,EAAS,EAAE,WAAW,cAAc,CAAC,CAAA,GAAA,EAAA,WAAA,cAAA,KAAA,CAAA,GAIrC,EAAA,CAAA,EAAS,CAAA,GAAA,EAAA,WAAA,eAAA,KAAA,CAAA,GAGT,EAAA,CAAA,EAAS,EAAE,MAAM,QAAQ,CAAC,CAAA,GAAA,EAAA,WAAA,YAAA,KAAA,CAAA,GAI1B,EAAA,CAAA,EAAS,CAAA,GAAA,EAAA,WAAA,UAAA,KAAA,CAAA,GAGT,EAAA,CAAA,EAAS,EAAE,WAAW,WAAW,CAAC,CAAA,GAAA,EAAA,WAAA,WAAA,KAAA,CAAA,GAIlC,EAAA,CAAA,EAAS,EAAE,WAAW,WAAW,CAAC,CAAA,GAAA,EAAA,WAAA,WAAA,KAAA,CAAA,GAIlC,EAAA,CAAA,EAAS,CAAA,GAAA,EAAA,WAAA,UAAA,KAAA,CAAA,GA/CX,IAAA,EAAA,CAAA,EAAc,kBAAkB,CAAA,GAAA,CAAA"}
1
+ {"version":3,"file":"sken-date-picker.js","names":[],"sources":["../src/components/sken-date-picker.ts"],"sourcesContent":["/**\n * `<sken-date-picker>` — Sken contract implementation backed by\n * Siemens iX 5's `<ix-date-input>`.\n *\n * The Sken contract (`SkenDatePickerProps`, `SkenDatePickerEmits`,\n * `skenDatePickerAria`) lives in `@sken-ds/contracts`. This component\n * is a thin Lit 3 wrapper that:\n *\n * 1. Accepts the Sken contract props on a `<sken-date-picker>`\n * custom element registered via `@customElement`.\n * 2. Renders a single `<ix-date-input>` with the Sken-shaped\n * `value` mapped to iX's `value` prop, the Sken `label`\n * rendered as an `<ix-label>` child, and the Sken\n * `helperText` rendered as an `<ix-typography>` child in\n * the `end` slot.\n * 3. Translates iX's `valueChange` event into Sken's\n * `sken-change` CustomEvent whose `detail.value` is the new\n * ISO 8601 date string or `null`.\n *\n * Subpath: import from `@sken-ds/primitives/sken-date-picker` to\n * register the custom element.\n *\n * Consumer boot sequence (same as `<sken-combobox>`):\n *\n * 1. `import '@siemens/ix/dist/siemens-ix/siemens-ix.css'`\n * 2. `defineCustomElements(window)` for both `@siemens/ix/loader`\n * and `@siemens/ix-icons/loader`, BEFORE any\n * `<sken-date-picker>` renders (Stencil does not re-hydrate).\n * 3. `import '@sken-ds/primitives/ix-bridge.css'` once at app boot\n * so the date-picker popover (`<ix-date-time-card>`,\n * `<ix-date-picker>`) renders in Sken colours instead of\n * iX classic light values. The consumer is responsible for\n * setting `data-theme` on a common ancestor.\n */\nimport { LitElement, html } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\n\nimport type { SkenDatePickerProps } from '@sken-ds/contracts'\n\n@customElement('sken-date-picker')\nexport class SkenDatePicker\n extends LitElement\n implements\n Pick<\n SkenDatePickerProps,\n | 'value'\n | 'label'\n | 'helperText'\n | 'placeholder'\n | 'disabled'\n | 'format'\n | 'minDate'\n | 'maxDate'\n | 'locale'\n >\n{\n /** Controlled value (ISO 8601 `yyyy-MM-dd`). `null` means no selection. */\n @property({ attribute: false })\n value: string | null = null\n\n /** Visible label rendered above the input. */\n @property() label = ''\n\n /** Helper text rendered below the input. */\n @property({ attribute: 'helper-text' })\n helperText = ''\n\n /** Placeholder shown when the input is empty. */\n @property() placeholder = ''\n\n /** Disabled state. @default false */\n @property({ type: Boolean })\n disabled = false\n\n /** Display format. Luxon-style tokens. @default \"yyyy-MM-dd\" */\n @property() format = 'yyyy-MM-dd'\n\n /** Earliest selectable date (ISO 8601 `yyyy-MM-dd`). */\n @property({ attribute: 'min-date' })\n minDate?: string\n\n /** Latest selectable date (ISO 8601 `yyyy-MM-dd`). */\n @property({ attribute: 'max-date' })\n maxDate?: string\n\n /** BCP 47 locale tag. */\n @property() locale?: string\n\n /**\n * Translate iX's `valueChange` event into Sken's `sken-change`\n * CustomEvent. iX 5 emits `valueChange` with `detail` set to\n * a `string | undefined` directly (not an object). We pass\n * the value through as-is so the consumer gets the same\n * string format they passed in; an empty value maps to\n * `null` so the Sken contract stays `string | null`.\n */\n private onIxValueChange = (e: Event) => {\n const detail = (e as CustomEvent<string | undefined>).detail\n const next = detail == null || detail === '' ? null : detail\n if (next === this.value) return\n this.value = next\n this.dispatchEvent(\n new CustomEvent<{ value: string | null }>('sken-change', {\n detail: { value: next },\n bubbles: true,\n composed: true,\n }),\n )\n }\n\n // Light DOM so Sken tokens (CSS custom properties) cascade\n // from the document root into the iX shadow tree. The re-skin\n // mapping (`--ix-*` → Sken tokens) lives in\n // `@sken-ds/primitives/ix-bridge.css`, which the consumer\n // imports once at app boot.\n protected override createRenderRoot(): HTMLElement {\n return this as unknown as HTMLElement\n }\n\n protected override render() {\n return html`\n <ix-date-input\n .value=${this.value ?? ''}\n format=${this.format}\n ?disabled=${this.disabled}\n placeholder=${this.placeholder}\n min-date=${this.minDate ?? ''}\n max-date=${this.maxDate ?? ''}\n locale=${this.locale ?? ''}\n data-testid=\"sken-date-picker\"\n @valueChange=${this.onIxValueChange}\n >\n ${this.label ? html`<ix-label>${this.label}</ix-label>` : null}\n ${\n this.helperText\n ? html`<ix-typography slot=\"end\">${this.helperText}</ix-typography>`\n : null\n }\n </ix-date-input>\n `\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'sken-date-picker': SkenDatePicker\n }\n}\n"],"mappings":";;;;AAwCO,IAAM,IAAN,cACG,EAcV;;EAyC6B,aAtCJ,KAAA,QAAA,MAGH,KAAA,QAAA,IAIP,KAAA,aAAA,IAGa,KAAA,cAAA,IAIf,KAAA,WAAA,IAGU,KAAA,SAAA,cAqBM,KAAA,mBAAA,MAAa;GACtC,IAAM,IAAU,EAAsC,QAChD,IAAO,KAAU,QAAQ,MAAW,KAAK,OAAO;GAClD,MAAS,KAAK,UAClB,KAAK,QAAQ,GACb,KAAK,cACH,IAAI,YAAsC,eAAe;IACvD,QAAQ,EAAE,OAAO,EAAK;IACtB,SAAS;IACT,UAAU;GACZ,CAAC,CACH;EACF;;CAOA,mBAAmD;EACjD,OAAO;CACT;CAEA,SAA4B;EAC1B,OAAO,CAAI;;iBAEE,KAAK,SAAS,GAAG;iBACjB,KAAK,OAAO;oBACT,KAAK,SAAS;sBACZ,KAAK,YAAY;mBACpB,KAAK,WAAW,GAAG;mBACnB,KAAK,WAAW,GAAG;iBACrB,KAAK,UAAU,GAAG;;uBAEZ,KAAK,gBAAgB;;UAElC,KAAK,QAAQ,CAAI,aAAa,KAAK,MAAM,eAAe,KAAK;UAE7D,KAAK,aACD,CAAI,6BAA6B,KAAK,WAAW,oBACjD,KACL;;;CAGP;AACF;AApFG,EAAA,CAAA,EAAS,EAAE,WAAW,GAAM,CAAC,CAAA,GAAA,EAAA,WAAA,SAAA,KAAA,CAAA,GAI7B,EAAA,CAAA,EAAS,CAAA,GAAA,EAAA,WAAA,SAAA,KAAA,CAAA,GAGT,EAAA,CAAA,EAAS,EAAE,WAAW,cAAc,CAAC,CAAA,GAAA,EAAA,WAAA,cAAA,KAAA,CAAA,GAIrC,EAAA,CAAA,EAAS,CAAA,GAAA,EAAA,WAAA,eAAA,KAAA,CAAA,GAGT,EAAA,CAAA,EAAS,EAAE,MAAM,QAAQ,CAAC,CAAA,GAAA,EAAA,WAAA,YAAA,KAAA,CAAA,GAI1B,EAAA,CAAA,EAAS,CAAA,GAAA,EAAA,WAAA,UAAA,KAAA,CAAA,GAGT,EAAA,CAAA,EAAS,EAAE,WAAW,WAAW,CAAC,CAAA,GAAA,EAAA,WAAA,WAAA,KAAA,CAAA,GAIlC,EAAA,CAAA,EAAS,EAAE,WAAW,WAAW,CAAC,CAAA,GAAA,EAAA,WAAA,WAAA,KAAA,CAAA,GAIlC,EAAA,CAAA,EAAS,CAAA,GAAA,EAAA,WAAA,UAAA,KAAA,CAAA,GA/CX,IAAA,EAAA,CAAA,EAAc,kBAAkB,CAAA,GAAA,CAAA"}
@@ -90,8 +90,12 @@ var f = i(class extends n {
90
90
  animation: sken-dialog-backdrop-enter 150ms ease-out;
91
91
  }
92
92
  @keyframes sken-dialog-backdrop-enter {
93
- from { opacity: 0; }
94
- to { opacity: 1; }
93
+ from {
94
+ opacity: 0;
95
+ }
96
+ to {
97
+ opacity: 1;
98
+ }
95
99
  }
96
100
 
97
101
  .panel {
@@ -107,9 +111,15 @@ var f = i(class extends n {
107
111
  font-family: var(--sken-font-sans, system-ui, sans-serif);
108
112
  }
109
113
 
110
- .panel.sm { width: 400px; }
111
- .panel.md { width: 560px; }
112
- .panel.lg { width: 800px; }
114
+ .panel.sm {
115
+ width: 400px;
116
+ }
117
+ .panel.md {
118
+ width: 560px;
119
+ }
120
+ .panel.lg {
121
+ width: 800px;
122
+ }
113
123
 
114
124
  header {
115
125
  display: flex;
@@ -158,10 +168,15 @@ var f = i(class extends n {
158
168
  border-radius: 6px;
159
169
  color: inherit;
160
170
  opacity: 0.6;
161
- transition: opacity 100ms, background 100ms;
171
+ transition:
172
+ opacity 100ms,
173
+ background 100ms;
162
174
  flex-shrink: 0;
163
175
  }
164
- .close-button:hover { opacity: 1; background: rgba(0, 0, 0, 0.05); }
176
+ .close-button:hover {
177
+ opacity: 1;
178
+ background: rgba(0, 0, 0, 0.05);
179
+ }
165
180
  .close-button:focus-visible {
166
181
  outline: 2px solid var(--sken-color-focus, #2e6fc6);
167
182
  outline-offset: 1px;
@@ -211,16 +226,16 @@ var f = i(class extends n {
211
226
  >
212
227
  <div class=${f(e)}>
213
228
  ${t ? s`<header>
214
- <slot name="title" @slotchange=${this.#u}></slot>
215
- ${this.dismissable === "dismissable" ? s`<button
216
- class="close-button"
217
- type="button"
218
- aria-label="Close dialog"
219
- @click=${this.#c}
220
- >
221
-
222
- </button>` : c}
223
- </header>` : c}
229
+ <slot name="title" @slotchange=${this.#u}></slot>
230
+ ${this.dismissable === "dismissable" ? s`<button
231
+ class="close-button"
232
+ type="button"
233
+ aria-label="Close dialog"
234
+ @click=${this.#c}
235
+ >
236
+
237
+ </button>` : c}
238
+ </header>` : c}
224
239
  <div class="body">
225
240
  <slot></slot>
226
241
  </div>
@@ -240,4 +255,4 @@ e([u({
240
255
  //#endregion
241
256
  export { p as t };
242
257
 
243
- //# sourceMappingURL=sken-dialog-hRtML2jW.js.map
258
+ //# sourceMappingURL=sken-dialog-CLqZu1WQ.js.map
@@ -0,0 +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":";;;;;AAKG,IAAM,IAAEA,EAAE,cAAc,EAAC;CAAC,YAAY,GAAE;EAAC,IAAG,MAAMC,CAAC,GAAEA,EAAE,SAAOC,EAAE,aAAqBD,EAAE,SAAZ,WAAkBA,EAAE,SAAS,SAAO,GAAE,MAAM,MAAM,oGAAoG;CAAC;CAAC,OAAO,GAAE;EAAC,OAAM,MAAI,OAAO,KAAK,CAAC,CAAC,CAAC,QAAO,MAAG,EAAE,EAAE,CAAC,CAAC,KAAK,GAAG,IAAE;CAAG;CAAC,OAAO,GAAE,CAAC,IAAG;EAAC,IAAY,KAAK,OAAd,KAAK,GAAY;GAAC,KAAK,qBAAG,IAAI,IAAE,GAAW,EAAE,YAAX,KAAK,MAAgB,KAAK,KAAG,IAAI,IAAI,EAAE,QAAQ,KAAK,GAAG,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,QAAO,MAAQ,MAAL,EAAM,CAAC;GAAG,KAAI,IAAM,KAAK,GAAE,EAAE,MAAI,CAAC,KAAK,IAAI,IAAI,CAAC,KAAG,KAAK,GAAG,IAAI,CAAC;GAAE,OAAO,KAAK,OAAO,CAAC;EAAC;EAAC,IAAM,IAAE,EAAE,QAAQ;EAAU,KAAI,IAAM,KAAK,KAAK,IAAG,KAAK,MAAI,EAAE,OAAO,CAAC,GAAE,KAAK,GAAG,OAAO,CAAC;EAAG,KAAI,IAAM,KAAK,GAAE;GAAC,IAAM,IAAE,CAAC,CAAC,EAAE;GAAG,MAAI,KAAK,GAAG,IAAI,CAAC,KAAG,KAAK,IAAI,IAAI,CAAC,MAAI,KAAG,EAAE,IAAI,CAAC,GAAE,KAAK,GAAG,IAAI,CAAC,MAAI,EAAE,OAAO,CAAC,GAAE,KAAK,GAAG,OAAO,CAAC;EAAG;EAAC,OAAOA;CAAC;AAAC,CAAC,GC8B7tB,IAAN,cAAyB,EAAW;;EA8UnB,aA7U6B,KAAA,OAAA,IACC,KAAA,OAAA,MACc,KAAA,cAAA,eACA,KAAA,YAAA,MACY,KAAA,kBAAA,MAM/C,KAAA,KAAA,MAEK,KAAA,KAAA,MAExB,KAAA,KAAA,IAGA,KAAA,KAAA,IAqNC,KAAA,MAAA,MAAmB;GAC9B,IAAI,KAAK,gBAAgB,cAAc;IAErC,EAAE,eAAe;IACjB;GACF;GAIA,KAAKE,GAAU,cAAc,QAAQ;EACvC,GAIuB,KAAA,WAAA;GACrB,KAAKA,GAAU,aAAa;EAC9B,GAKY,KAAA,MAAA,MAAwB;GAC9B,EAAE,WAAW,KAAK,WAClB,KAAK,gBAAgB,gBACzB,KAAKA,GAAU,cAAc,UAAU;EACzC,GAI6B,KAAA,WAAA;GAC3B,KAAKA,GAAU,cAAc,cAAc;EAC7C,GAwEsB,KAAA,MAAA,MAAmB;GACvC,IAAM,IAAO,EAAE;GAEf,AADA,KAAKC,KAAY,EAAK,cAAc,EAAE,SAAS,GAAK,CAAC,CAAC,CAAC,SAAS,GAChE,KAAK,cAAc;EACrB;;CAvUA;CAEA;CAEA;CAGA;;EAEgB,KAAA,SAAA,CAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuInB,oBAA0B;EAIxB,AAHA,MAAM,kBAAkB,GAExB,KAAKC,KAAkB,KAAK,YAC5B,KAAKC,KAAuB,KAAK;CACnC;CAEA,uBAA6B;EAE3B,AADA,MAAM,qBAAqB,GACvB,KAAKC,MAAa,KAAKF,OAIvB,KAAKC,MACL,KAAKA,GAAqB,eAAe,KAAKD,KAE9C,KAAKA,GAAgB,aAAa,MAAM,KAAKC,EAAoB,IAEjE,KAAKD,GAAgB,YAAY,IAAI,GAEvC,KAAKE,KAAY;CAErB;CAEA,QAAQ,GAAqC;EACtC,EAAQ,IAAI,MAAM,KAClB,KAAK,YAEN,KAAK,OACP,KAAKC,GAAW,IACP,KAAK,QAAQ,QACtB,KAAK,QAAQ,MAAM;CAEvB;CAGA,KAAmB;EAKjB,AAJI,KAAKD,OAAc,MAAS,KAAK,eAAe,SAAS,SAC3D,SAAS,KAAK,YAAY,IAAI,GAC9B,KAAKA,KAAY,KAEd,KAAK,QAAQ,SAChB,KAAK,QAAQ,UAAU,GACvB,KAAKJ,GAAU,aAAa,GAE5B,qBAAqB;GACnB,AAAI,KAAK,gBAAgB,iBAAiB,KAAK,eAC7C,KAAK,aAAa,MAAM,IAKxB,KAHmB,QAAQ,cACzB,4EAEF,CAAA,EAAO,MAAM;EAEjB,CAAC;CAEL;CAYA,YAAY,IAAgC,gBAAsB;EAChE,KAAKA,GAAU,cAAc,CAAM;CACrC;CAMA;CAcA;CAOA;CAQA;CAQA,GAAU,GAAc,GAAwB;EAC9C,KAAK,cACH,IAAI,YAAY,GAAM;GACpB;GACA,SAAS;GACT,UAAU;EACZ,CAAC,CACH;CACF;CAEA,SAAS;EACP,IAAM,IAAe;GACnB,OAAO;IACN,KAAK,OAAO;EACf,GAMM,IAAa,KAAKC,MAAa,KAAK,gBAAgB,eAMpD,IAAgB,KAAK,aAAa;EACxC,OAAO,CAAI;;qBAEM,KAAiB,EAAQ;2BACnB,KAAK,mBAAmB,EAAQ;kBACzC,KAAKK,GAAU;iBAChB,KAAKC,GAAS;iBACd,KAAKC,GAAS;;qBAEV,EAAS,CAAY,EAAE;YAEhC,IACI,CAAI;mDAC+B,KAAKC,GAAmB;oBAEzD,KAAK,gBAAgB,gBACjB,CAAI;;;;iCAIO,KAAKC,GAAe;;;mCAI/B,EACL;6BAED,EACL;;;;;;;;;;CAUT;CAEA;AAKF;AAlVG,EAAA,CAAA,EAAS;CAAE,SAAS;CAAM,MAAM;AAAQ,CAAC,CAAA,GAAA,EAAA,WAAA,QAAA,KAAA,CAAA,GACzC,EAAA,CAAA,EAAS,EAAE,SAAS,GAAK,CAAC,CAAA,GAAA,EAAA,WAAA,QAAA,KAAA,CAAA,GAC1B,EAAA,CAAA,EAAS,EAAE,SAAS,GAAK,CAAC,CAAA,GAAA,EAAA,WAAA,eAAA,KAAA,CAAA,GAC1B,EAAA,CAAA,EAAS,EAAE,WAAW,aAAa,CAAC,CAAA,GAAA,EAAA,WAAA,aAAA,KAAA,CAAA,GACpC,EAAA,CAAA,EAAS,EAAE,WAAW,mBAAmB,CAAC,CAAA,GAAA,EAAA,WAAA,mBAAA,KAAA,CAAA,GAE1C,EAAA,CAAA,EAAM,QAAQ,CAAA,GAAA,EAAA,WAAA,WAAA,KAAA,CAAA,GACd,EAAA,CAAA,EAAM,eAAe,CAAA,GAAA,EAAA,WAAA,gBAAA,KAAA,CAAA,GATvB,IAAA,EAAA,CAAA,EAAc,aAAa,CAAA,GAAA,CAAA"}
@@ -1,2 +1,2 @@
1
- import { t as e } from "./sken-dialog-hRtML2jW.js";
1
+ import { t as e } from "./sken-dialog-CLqZu1WQ.js";
2
2
  export { e as SkenDialog };
@@ -63,7 +63,13 @@ var s = class extends t {
63
63
  * whatever Inter decides, which inflates block glyphs
64
64
  * like the multiplication sign. The system sans keeps
65
65
  * the × proportional to the rest of the chip. */
66
- font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
66
+ font-family:
67
+ ui-sans-serif,
68
+ system-ui,
69
+ -apple-system,
70
+ 'Segoe UI',
71
+ Roboto,
72
+ sans-serif;
67
73
  font-size: 0.75rem;
68
74
  }
69
75
  .close:hover {
@@ -79,16 +85,16 @@ var s = class extends t {
79
85
  render() {
80
86
  let e = this.label, t = this.closeAriaLabel || (e ? `Remove ${e}` : "Remove");
81
87
  return r`
82
- <span class="label" part="label">
83
- ${e || r`<slot></slot>`}
84
- </span>
88
+ <span class="label" part="label"> ${e || r`<slot></slot>`} </span>
85
89
  ${!this.disabled && !this.hideCloseButton ? r`<button
86
- type="button"
87
- class="close"
88
- part="close"
89
- aria-label=${t}
90
- @click=${this._onClose}
91
- >×</button>` : null}
90
+ type="button"
91
+ class="close"
92
+ part="close"
93
+ aria-label=${t}
94
+ @click=${this._onClose}
95
+ >
96
+ ×
97
+ </button>` : null}
92
98
  `;
93
99
  }
94
100
  updated(e) {
@@ -1 +1 @@
1
- {"version":3,"file":"sken-filter-chip.js","names":[],"sources":["../src/components/sken-filter-chip.ts"],"sourcesContent":["/**\n * `<sken-filter-chip>` — Sken removable label primitive.\n *\n * A compact, removable label used to represent a selected value\n * inside a parent control. The pattern is the same one\n * `<nord-tag>` uses inside `<nord-combobox multiple>`: a\n * pill-shaped label with a close button on the right. The chip\n * is independent of the parent — it does not know which control\n * selected it, it only knows how to render its label and emit\n * a `sken-close` event when the user activates the close button.\n *\n * <sken-filter-chip @sken-close=${this._onRemove}>\n * Detector de humo\n * </sken-filter-chip>\n *\n * Reusability:\n *\n * - `<sken-combobox>` in `mode=\"multiple\"` + `display=\"chips\"`\n * renders one chip per selected item in its trigger.\n * - `<sken-tag-input>` (future) renders chips for typed tags.\n * - A filter bar can render chips for each applied filter.\n *\n * In all of these the parent owns the state, the chip owns the\n * visual + the close affordance, and the parent listens for\n * `sken-close` to mutate its state and re-render. The chip\n * never removes itself from the DOM — that is the parent's job.\n *\n * Why a real Shadow DOM (not the marker pattern):\n *\n * `<sken-combobox-item>` is a marker (renders nothing, parent\n * introspects it). The filter chip is the opposite: it has a\n * visual presence in the light DOM consumer's layout. Putting\n * the visual in light DOM would mean the consumer's CSS bleeds\n * in (Tailwind resets, global box-sizing). Shadow DOM scopes\n * the visual; the parent sets `--sken-*` tokens on a common\n * ancestor and the chip inherits.\n *\n * A11y:\n *\n * - The close button is a real `<button type=\"button\">` so it\n * is keyboard-activatable (Space / Enter) without extra work.\n * - The chip's accessible name is built from the visible label\n * plus the close button's `aria-label`, which defaults to\n * `Remove {label}` so a screen reader announces both pieces.\n * - The chip itself is not a focus stop; focus stays on the\n * parent control. The chip is purely visual + the close\n * button is focusable when the parent moves focus into it\n * (e.g. via the parent control's keyboard model).\n */\nimport { LitElement, html, css, type PropertyValues } from 'lit'\nimport { customElement, property, query } from 'lit/decorators.js'\n\n@customElement('sken-filter-chip')\nexport class SkenFilterChip extends LitElement {\n /** Visible text on the chip. Falls back to the slot content. */\n @property() label = ''\n\n /** Disabled state. Hides the close button. @default false */\n @property({ type: Boolean, reflect: true })\n disabled = false\n\n /**\n * Hide the close button. Use this for read-only \"applied\n * filter\" chips that the user can see but not remove from\n * this control.\n * @default false\n */\n @property({ attribute: 'hide-close-button', type: Boolean })\n hideCloseButton = false\n\n /**\n * Accessible name for the close button. Defaults to\n * `Remove {label}` so the user knows which chip they are\n * removing when chips are read in sequence.\n */\n @property({ attribute: 'close-aria-label' }) closeAriaLabel = ''\n\n @query('.close') private _closeButton!: HTMLButtonElement\n\n // ── render ───────────────────────────────────────────────────────\n\n static override styles = css`\n :host {\n display: inline-flex;\n align-items: center;\n gap: 0.25rem;\n max-inline-size: 100%;\n padding-inline: 0.5rem;\n block-size: 1.5rem;\n border-radius: 999px;\n /* Token-driven, so the chip inherits the brand without\n * the primitive having to know the palette. The defaults\n * here match the Figma reference for the \"applied filter\"\n * pill in the Legrand Care system. */\n background: var(--sken-muted, #f4f4f5);\n color: var(--sken-foreground, #152b48);\n font-family: var(--sken-family-sans, inherit);\n font-size: 0.75rem;\n line-height: 1;\n white-space: nowrap;\n user-select: none;\n }\n :host([disabled]) {\n opacity: var(--sken-disabled, 0.5);\n cursor: not-allowed;\n }\n .label {\n overflow: hidden;\n text-overflow: ellipsis;\n min-inline-size: 0;\n }\n .close {\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 padding: 0;\n margin: 0;\n border: 0;\n border-radius: 999px;\n background: transparent;\n color: var(--sken-muted-foreground, #4f6076);\n font: inherit;\n line-height: 1;\n cursor: pointer;\n /* Same font fix as the combobox trigger icons: without\n * an explicit font-family the close glyph renders in\n * whatever Inter decides, which inflates block glyphs\n * like the multiplication sign. The system sans keeps\n * the × proportional to the rest of the chip. */\n font-family: ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif;\n font-size: 0.75rem;\n }\n .close:hover {\n background: var(--sken-border, #dfdfe3);\n color: var(--sken-foreground, #152b48);\n }\n .close:focus-visible {\n outline: 2px solid var(--sken-ring, #2e6fc6);\n outline-offset: 1px;\n }\n `\n\n protected override render() {\n // Visible label: the prop wins if set, else we fall back to\n // the default slot. The slot wins only if the prop is empty.\n // We use a conditional template instead of `labelText || slot`\n // because lit-html will skip a falsy interpolation but the\n // slot template is itself a valid TemplateResult — using the\n // short-circuit OR discards the slot entirely.\n const labelText = this.label\n const closeLabel = this.closeAriaLabel || (labelText ? `Remove ${labelText}` : 'Remove')\n return html`\n <span class=\"label\" part=\"label\">\n ${labelText ? labelText : html`<slot></slot>`}\n </span>\n ${!this.disabled && !this.hideCloseButton\n ? html`<button\n type=\"button\"\n class=\"close\"\n part=\"close\"\n aria-label=${closeLabel}\n @click=${this._onClose}\n >×</button>`\n : null}\n `\n }\n\n protected override updated(_changed: PropertyValues): void {\n // After every render, mirror the rendered text into the\n // host's `title` so the browser tooltip matches the chip\n // label (mirrors iX 5's `<ix-filter-chip title=\"…\">`).\n // Cheap O(1) read of the slot/label that just rendered.\n if (this._closeButton || this.hideCloseButton || this.disabled) {\n const text = this.label || this.textContent?.trim() || ''\n if (text) this.title = text\n }\n }\n\n // ── events ───────────────────────────────────────────────────────\n\n /** User clicked the close button. Parent must update its state. */\n private _onClose = (e: MouseEvent): void => {\n e.stopPropagation()\n // No `detail` option: that leaves `event.detail` as\n // `undefined` (the contract), instead of `null` (which it\n // would be if we passed `detail: undefined` explicitly).\n this.dispatchEvent(new CustomEvent('sken-close', {\n bubbles: true,\n composed: true,\n }))\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'sken-filter-chip': SkenFilterChip\n }\n}\n"],"mappings":";;;;AAqDO,IAAM,IAAN,cAA6B,EAAW;;EAmIzB,aAjIA,KAAA,QAAA,IAIT,KAAA,WAAA,IASO,KAAA,kBAAA,IAO4C,KAAA,iBAAA,IA6G1C,KAAA,YAAA,MAAwB;GAK1C,AAJA,EAAE,gBAAgB,GAIlB,KAAK,cAAc,IAAI,YAAY,cAAc;IAC/C,SAAS;IACT,UAAU;GACZ,CAAC,CAAC;EACJ;;;EAhHyB,KAAA,SAAA,CAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgE5B,SAA4B;EAO1B,IAAM,IAAY,KAAK,OACjB,IAAa,KAAK,mBAAmB,IAAY,UAAU,MAAc;EAC/E,OAAO,CAAI;;UAEL,KAAwB,CAAI,gBAAgB;;QAE9C,CAAC,KAAK,YAAY,CAAC,KAAK,kBACtB,CAAI;;;;yBAIW,EAAW;qBACf,KAAK,SAAS;yBAEzB,KAAK;;CAEb;CAEA,QAA2B,GAAgC;EAKzD,IAAI,KAAK,gBAAgB,KAAK,mBAAmB,KAAK,UAAU;GAC9D,IAAM,IAAO,KAAK,SAAS,KAAK,aAAa,KAAK,KAAK;GACvD,AAAI,MAAM,KAAK,QAAQ;EACzB;CACF;AAeF;AA3IG,EAAA,CAAA,EAAS,CAAA,GAAA,EAAA,WAAA,SAAA,KAAA,CAAA,GAGT,EAAA,CAAA,EAAS;CAAE,MAAM;CAAS,SAAS;AAAK,CAAC,CAAA,GAAA,EAAA,WAAA,YAAA,KAAA,CAAA,GASzC,EAAA,CAAA,EAAS;CAAE,WAAW;CAAqB,MAAM;AAAQ,CAAC,CAAA,GAAA,EAAA,WAAA,mBAAA,KAAA,CAAA,GAQ1D,EAAA,CAAA,EAAS,EAAE,WAAW,mBAAmB,CAAC,CAAA,GAAA,EAAA,WAAA,kBAAA,KAAA,CAAA,GAE1C,EAAA,CAAA,EAAM,QAAQ,CAAA,GAAA,EAAA,WAAA,gBAAA,KAAA,CAAA,GAzBhB,IAAA,EAAA,CAAA,EAAc,kBAAkB,CAAA,GAAA,CAAA"}
1
+ {"version":3,"file":"sken-filter-chip.js","names":[],"sources":["../src/components/sken-filter-chip.ts"],"sourcesContent":["/**\n * `<sken-filter-chip>` — Sken removable label primitive.\n *\n * A compact, removable label used to represent a selected value\n * inside a parent control. The pattern is the same one\n * `<nord-tag>` uses inside `<nord-combobox multiple>`: a\n * pill-shaped label with a close button on the right. The chip\n * is independent of the parent — it does not know which control\n * selected it, it only knows how to render its label and emit\n * a `sken-close` event when the user activates the close button.\n *\n * <sken-filter-chip @sken-close=${this._onRemove}>\n * Detector de humo\n * </sken-filter-chip>\n *\n * Reusability:\n *\n * - `<sken-combobox>` in `mode=\"multiple\"` + `display=\"chips\"`\n * renders one chip per selected item in its trigger.\n * - `<sken-tag-input>` (future) renders chips for typed tags.\n * - A filter bar can render chips for each applied filter.\n *\n * In all of these the parent owns the state, the chip owns the\n * visual + the close affordance, and the parent listens for\n * `sken-close` to mutate its state and re-render. The chip\n * never removes itself from the DOM — that is the parent's job.\n *\n * Why a real Shadow DOM (not the marker pattern):\n *\n * `<sken-combobox-item>` is a marker (renders nothing, parent\n * introspects it). The filter chip is the opposite: it has a\n * visual presence in the light DOM consumer's layout. Putting\n * the visual in light DOM would mean the consumer's CSS bleeds\n * in (Tailwind resets, global box-sizing). Shadow DOM scopes\n * the visual; the parent sets `--sken-*` tokens on a common\n * ancestor and the chip inherits.\n *\n * A11y:\n *\n * - The close button is a real `<button type=\"button\">` so it\n * is keyboard-activatable (Space / Enter) without extra work.\n * - The chip's accessible name is built from the visible label\n * plus the close button's `aria-label`, which defaults to\n * `Remove {label}` so a screen reader announces both pieces.\n * - The chip itself is not a focus stop; focus stays on the\n * parent control. The chip is purely visual + the close\n * button is focusable when the parent moves focus into it\n * (e.g. via the parent control's keyboard model).\n */\nimport { LitElement, css, html, type PropertyValues } from 'lit'\nimport { customElement, property, query } from 'lit/decorators.js'\n\n@customElement('sken-filter-chip')\nexport class SkenFilterChip extends LitElement {\n /** Visible text on the chip. Falls back to the slot content. */\n @property() label = ''\n\n /** Disabled state. Hides the close button. @default false */\n @property({ type: Boolean, reflect: true })\n disabled = false\n\n /**\n * Hide the close button. Use this for read-only \"applied\n * filter\" chips that the user can see but not remove from\n * this control.\n * @default false\n */\n @property({ attribute: 'hide-close-button', type: Boolean })\n hideCloseButton = false\n\n /**\n * Accessible name for the close button. Defaults to\n * `Remove {label}` so the user knows which chip they are\n * removing when chips are read in sequence.\n */\n @property({ attribute: 'close-aria-label' }) closeAriaLabel = ''\n\n @query('.close') private _closeButton!: HTMLButtonElement\n\n // ── render ───────────────────────────────────────────────────────\n\n static override styles = css`\n :host {\n display: inline-flex;\n align-items: center;\n gap: 0.25rem;\n max-inline-size: 100%;\n padding-inline: 0.5rem;\n block-size: 1.5rem;\n border-radius: 999px;\n /* Token-driven, so the chip inherits the brand without\n * the primitive having to know the palette. The defaults\n * here match the Figma reference for the \"applied filter\"\n * pill in the Legrand Care system. */\n background: var(--sken-muted, #f4f4f5);\n color: var(--sken-foreground, #152b48);\n font-family: var(--sken-family-sans, inherit);\n font-size: 0.75rem;\n line-height: 1;\n white-space: nowrap;\n user-select: none;\n }\n :host([disabled]) {\n opacity: var(--sken-disabled, 0.5);\n cursor: not-allowed;\n }\n .label {\n overflow: hidden;\n text-overflow: ellipsis;\n min-inline-size: 0;\n }\n .close {\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 padding: 0;\n margin: 0;\n border: 0;\n border-radius: 999px;\n background: transparent;\n color: var(--sken-muted-foreground, #4f6076);\n font: inherit;\n line-height: 1;\n cursor: pointer;\n /* Same font fix as the combobox trigger icons: without\n * an explicit font-family the close glyph renders in\n * whatever Inter decides, which inflates block glyphs\n * like the multiplication sign. The system sans keeps\n * the × proportional to the rest of the chip. */\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 }\n .close:hover {\n background: var(--sken-border, #dfdfe3);\n color: var(--sken-foreground, #152b48);\n }\n .close:focus-visible {\n outline: 2px solid var(--sken-ring, #2e6fc6);\n outline-offset: 1px;\n }\n `\n\n protected override render() {\n // Visible label: the prop wins if set, else we fall back to\n // the default slot. The slot wins only if the prop is empty.\n // We use a conditional template instead of `labelText || slot`\n // because lit-html will skip a falsy interpolation but the\n // slot template is itself a valid TemplateResult — using the\n // short-circuit OR discards the slot entirely.\n const labelText = this.label\n const closeLabel = this.closeAriaLabel || (labelText ? `Remove ${labelText}` : 'Remove')\n return html`\n <span class=\"label\" part=\"label\"> ${labelText ? labelText : html`<slot></slot>`} </span>\n ${\n !this.disabled && !this.hideCloseButton\n ? html`<button\n type=\"button\"\n class=\"close\"\n part=\"close\"\n aria-label=${closeLabel}\n @click=${this._onClose}\n >\n ×\n </button>`\n : null\n }\n `\n }\n\n protected override updated(_changed: PropertyValues): void {\n // After every render, mirror the rendered text into the\n // host's `title` so the browser tooltip matches the chip\n // label (mirrors iX 5's `<ix-filter-chip title=\"…\">`).\n // Cheap O(1) read of the slot/label that just rendered.\n if (this._closeButton || this.hideCloseButton || this.disabled) {\n const text = this.label || this.textContent?.trim() || ''\n if (text) this.title = text\n }\n }\n\n // ── events ───────────────────────────────────────────────────────\n\n /** User clicked the close button. Parent must update its state. */\n private _onClose = (e: MouseEvent): void => {\n e.stopPropagation()\n // No `detail` option: that leaves `event.detail` as\n // `undefined` (the contract), instead of `null` (which it\n // would be if we passed `detail: undefined` explicitly).\n this.dispatchEvent(\n new CustomEvent('sken-close', {\n bubbles: true,\n composed: true,\n }),\n )\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'sken-filter-chip': SkenFilterChip\n }\n}\n"],"mappings":";;;;AAqDO,IAAM,IAAN,cAA6B,EAAW;;EA2IzB,aAzIA,KAAA,QAAA,IAIT,KAAA,WAAA,IASO,KAAA,kBAAA,IAO4C,KAAA,iBAAA,IAqH1C,KAAA,YAAA,MAAwB;GAK1C,AAJA,EAAE,gBAAgB,GAIlB,KAAK,cACH,IAAI,YAAY,cAAc;IAC5B,SAAS;IACT,UAAU;GACZ,CAAC,CACH;EACF;;;EA1HyB,KAAA,SAAA,CAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsE5B,SAA4B;EAO1B,IAAM,IAAY,KAAK,OACjB,IAAa,KAAK,mBAAmB,IAAY,UAAU,MAAc;EAC/E,OAAO,CAAI;0CAC2B,KAAwB,CAAI,gBAAgB;QAE9E,CAAC,KAAK,YAAY,CAAC,KAAK,kBACpB,CAAI;;;;2BAIW,EAAW;uBACf,KAAK,SAAS;;;yBAIzB,KACL;;CAEL;CAEA,QAA2B,GAAgC;EAKzD,IAAI,KAAK,gBAAgB,KAAK,mBAAmB,KAAK,UAAU;GAC9D,IAAM,IAAO,KAAK,SAAS,KAAK,aAAa,KAAK,KAAK;GACvD,AAAI,MAAM,KAAK,QAAQ;EACzB;CACF;AAiBF;AArJG,EAAA,CAAA,EAAS,CAAA,GAAA,EAAA,WAAA,SAAA,KAAA,CAAA,GAGT,EAAA,CAAA,EAAS;CAAE,MAAM;CAAS,SAAS;AAAK,CAAC,CAAA,GAAA,EAAA,WAAA,YAAA,KAAA,CAAA,GASzC,EAAA,CAAA,EAAS;CAAE,WAAW;CAAqB,MAAM;AAAQ,CAAC,CAAA,GAAA,EAAA,WAAA,mBAAA,KAAA,CAAA,GAQ1D,EAAA,CAAA,EAAS,EAAE,WAAW,mBAAmB,CAAC,CAAA,GAAA,EAAA,WAAA,kBAAA,KAAA,CAAA,GAE1C,EAAA,CAAA,EAAM,QAAQ,CAAA,GAAA,EAAA,WAAA,gBAAA,KAAA,CAAA,GAzBhB,IAAA,EAAA,CAAA,EAAc,kBAAkB,CAAA,GAAA,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"sken-input.js","names":["#internalValue","#syncFormValue","#internals","#slotStartObserver","#slotEndObserver","#visibilityObserver","#adjustPaddings","#handleInput","#handleChange","#handleFocus","#handleBlur","#handleKeydown"],"sources":["../src/components/sken-input.ts"],"sourcesContent":["// ── <sken-input> — framework-ready Web Component ─────────────────────\n// Wraps a native <input> with token-driven styles. Controlled and\n// uncontrolled value, every standard <input> type, free form\n// integration. Requires @sken-ds/theme/css (ADR-0006).\n\nimport { LitElement, html, css } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\nimport type { SkenInputType, SkenInputSize } from '@sken-ds/contracts'\n\n@customElement('sken-input')\nexport class SkenInput extends LitElement {\n // Form-associated custom element: opt in to the ElementInternals\n // API so this primitive can participate in a surrounding\n // <form>'s submit/reset lifecycle. Without this flag, the\n // browser treats the host as an opaque element that does not\n // contribute to FormData and does not block Enter-to-submit\n // (the implicit submission rule for single text-like input\n // does not match against opaque elements). Lit docs:\n // https://lit.dev/docs/components/form/\n static formAssociated = true\n\n // The ElementInternals instance. Created in the constructor\n // via attachInternals(), which the browser only makes\n // available because `formAssociated` is true. We use it for:\n // - setFormValue() so the form picks up the current value.\n // - form reset (formResetCallback) for uncontrolled mode.\n // - reflecting `disabled` to the form's :disabled selector\n // (formDisabledCallback).\n // Nullable because some test environments (older happy-dom\n // versions, jsdom) do not implement ElementInternals. In\n // those environments form association is a no-op.\n #internals: ElementInternals | null = null\n\n constructor() {\n super()\n // attachInternals() is only callable when formAssociated is\n // true. The static flag is set above, so this resolves to a\n // real ElementInternals in any spec-compliant browser\n // (Chrome 77+, Firefox 98+, Safari 16.4+). If the host\n // environment does not implement it, the call throws — we\n // catch and degrade gracefully so SSR / tests that do not\n // need form association still work.\n try {\n this.#internals = this.attachInternals()\n } catch {\n this.#internals = null\n }\n }\n\n @property({ reflect: true }) type: SkenInputType = 'text'\n @property({ reflect: true }) size: SkenInputSize = 'md'\n @property() value: string | undefined = undefined\n @property({ attribute: 'default-value' }) defaultValue: string | undefined = undefined\n @property() placeholder: string | undefined = undefined\n @property({ reflect: true, type: Boolean }) disabled = false\n @property({ reflect: true, type: Boolean }) readonly = false\n @property({ reflect: true, type: Boolean }) required = false\n @property({ reflect: true, type: Boolean }) invalid = false\n @property({ attribute: 'described-by-id' }) describedById: string | undefined = undefined\n @property() name: string | undefined = undefined\n @property() autocomplete: AutoFill | undefined = undefined\n @property({ attribute: 'max-length', type: Number }) maxLength: number | undefined = undefined\n @property({ attribute: 'min-length', type: Number }) minLength: number | undefined = undefined\n @property() pattern: string | undefined = undefined\n\n // Internal value for uncontrolled mode. Seeded from\n // `defaultValue` on first connection; from then on the DOM\n // input keeps the source of truth.\n #internalValue: string = ''\n\n static styles = css`\n :host {\n display: inline-block;\n inline-size: 100%;\n }\n\n .input-wrapper {\n position: relative;\n display: flex;\n align-items: center;\n inline-size: 100%;\n block-size: 100%;\n }\n\n input {\n box-sizing: border-box;\n inline-size: 100%;\n border: 1px solid var(--sken-border);\n border-radius: var(--sken-md);\n background: var(--sken-input);\n color: var(--sken-foreground);\n font-family: var(--sken-family-sans);\n font-size: var(--sken-size-md);\n line-height: 1.25;\n text-overflow: ellipsis;\n padding-block: var(--sken-2);\n padding-inline: var(--sken-3);\n min-block-size: 2.25rem;\n transition:\n border-color 120ms ease,\n box-shadow 120ms ease;\n }\n\n input::placeholder {\n color: var(--sken-muted-foreground);\n opacity: 1; /* Firefox placeholder is <1 by default */\n }\n\n /* ── Slot containers ──────────────────────────────────────\n Position: absolute so the slotted content floats over the\n input without consuming flex space. The input's padding is\n auto-adjusted in JS to keep the value text from overlapping.\n pointer-events: none on the wrapper, auto on the slotted\n content, so only the visible icon/button receives clicks. */\n .slot {\n position: absolute;\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 1;\n pointer-events: none;\n color: var(--sken-muted-foreground);\n }\n\n .slot ::slotted(*) {\n pointer-events: auto;\n }\n\n .slot-start {\n inset-inline-start: 0.5rem;\n }\n\n .slot-end {\n inset-inline-end: 0.5rem;\n }\n\n /* ── Sizes ────────────────────────────────────────────────── */\n :host([size='sm']) input {\n padding-block: var(--sken-1);\n padding-inline: var(--sken-2);\n font-size: var(--sken-size-sm);\n min-block-size: 1.75rem;\n }\n :host([size='md']) input {\n /* Default styles above. */\n }\n :host([size='lg']) input {\n padding-block: var(--sken-3);\n padding-inline: var(--sken-4);\n font-size: var(--sken-size-lg);\n min-block-size: 2.75rem;\n }\n\n /* ── States ───────────────────────────────────────────────── */\n .input-wrapper:hover input:not(:disabled):not(:read-only) {\n border-color: var(--sken-foreground);\n }\n .input-wrapper:focus-within input {\n outline: 2px solid var(--sken-ring, currentColor);\n outline-offset: 2px;\n border-color: var(--sken-primary);\n }\n input:disabled {\n cursor: not-allowed;\n opacity: var(--sken-disabled);\n }\n input:read-only {\n background: var(--sken-muted);\n }\n :host([invalid]) input {\n border-color: var(--sken-destructive);\n }\n :host([invalid]) .input-wrapper:focus-within input {\n outline-color: var(--sken-destructive);\n }\n `\n\n override connectedCallback(): void {\n super.connectedCallback()\n if (this.value === undefined) {\n this.#internalValue = this.defaultValue ?? ''\n }\n // Publish the initial value to the surrounding form so that\n // FormData(form) and the implicit-submit-on-Enter rule see\n // the primitive as a real text input from the very first\n // render. Cheap (no allocation in the hot path after this).\n this.#syncFormValue()\n }\n\n override disconnectedCallback(): void {\n super.disconnectedCallback()\n this.#slotStartObserver?.disconnect()\n this.#slotEndObserver?.disconnect()\n this.#visibilityObserver?.disconnect()\n }\n\n // ── Form-association callbacks ─────────────────────────────────\n // Called by the browser when the form this primitive is\n // associated with is reset (either via form.reset() or by\n // clicking a <button type=\"reset\">). For uncontrolled mode we\n // restore the seed value; for controlled mode the consumer's\n // prop is the source of truth, so we re-render and let\n // Lit's render path re-publish the controlled value.\n formResetCallback(): void {\n if (this.value === undefined) {\n this.#internalValue = this.defaultValue ?? ''\n const inputEl = this.renderRoot.querySelector('input') as HTMLInputElement | null\n if (inputEl) inputEl.value = this.#internalValue\n this.#syncFormValue()\n } else {\n this.requestUpdate()\n }\n }\n\n // Called by the browser when the form's :disabled state\n // changes (e.g. a fieldset ancestor is disabled, or the\n // primitive gets associated with a disabled form). Mirrors\n // the `disabled` state onto the inner <input> so the visual\n // and the form submission are consistent.\n formDisabledCallback(isDisabled: boolean): void {\n // Reflect onto the prop so Lit re-renders and the\n // :host([disabled]) CSS rule kicks in. The inner <input>\n // picks up `?disabled=${this.disabled}` in the template.\n this.disabled = isDisabled\n }\n\n // Publish the current value to the form via ElementInternals.\n // Called on every change so FormData and submit/reset are\n // always in sync with the visible state.\n #syncFormValue(): void {\n if (!this.#internals) return\n const value = this.value ?? this.#internalValue\n // If the primitive has no `name`, FormData(form) skips it\n // anyway, so setFormValue is harmless but pointless. We\n // still call it to keep the implicit-submit rule consistent\n // (the spec checks the existence of a single text-like\n // submitter, not the name).\n this.#internals.setFormValue(value)\n }\n\n protected override firstUpdated(): void {\n const startEl = this.renderRoot.querySelector('.slot-start') as HTMLElement | null\n const endEl = this.renderRoot.querySelector('.slot-end') as HTMLElement | null\n const inputEl = this.renderRoot.querySelector('input') as HTMLInputElement | null\n if (!startEl || !endEl || !inputEl) return\n\n // MutationObserver on each slot container watches for\n // children being added/removed/attributes-changed. Cheap\n // because the subtree is just one or two nodes.\n this.#slotStartObserver = new MutationObserver(() => this.#adjustPaddings())\n this.#slotStartObserver.observe(startEl, { childList: true, subtree: true, attributes: true })\n this.#slotEndObserver = new MutationObserver(() => this.#adjustPaddings())\n this.#slotEndObserver.observe(endEl, { childList: true, subtree: true, attributes: true })\n\n // IntersectionObserver catches the case where the input is\n // hidden at first connect (e.g. inside a closed <details> or\n // an off-screen tab). The slot's bounding rect is 0 in that\n // case, so the padding would be wrong until the user opens\n // the parent. Re-measure when the host becomes visible.\n this.#visibilityObserver = new IntersectionObserver(entries => {\n for (const entry of entries) {\n if (entry.isIntersecting) this.#adjustPaddings()\n }\n })\n this.#visibilityObserver.observe(this)\n\n // Also listen for the native slotchange event as a belt-and-\n // braces in case the MutationObserver misses something\n // (e.g. text node changes inside the slotted element).\n const startSlot = this.renderRoot.querySelector('slot[name=\"start\"]') as HTMLSlotElement | null\n const endSlot = this.renderRoot.querySelector('slot[name=\"end\"]') as HTMLSlotElement | null\n startSlot?.addEventListener('slotchange', this.#adjustPaddings)\n endSlot?.addEventListener('slotchange', this.#adjustPaddings)\n\n // First measurement.\n this.#adjustPaddings()\n }\n\n // The gap between a slot's edge and the input's text. iX uses\n // 0.5rem; we keep that because it balances the visual weight of\n // a 16px icon against the input's 0.75rem default padding-inline.\n // Module-scoped (not class-scoped) because TS forbids static\n // private fields on classes that use the legacy decorators.\n static SLOT_AIR = '0.5rem'\n\n // Two rAFs: the first lets Lit finish painting the slot, the\n // second lets the browser compute the slotted element's box.\n // Without the second, getBoundingClientRect() on a freshly-\n // assigned icon returns 0.\n #adjustPaddings = (): void => {\n requestAnimationFrame(() => {\n requestAnimationFrame(() => {\n const inputEl = this.renderRoot.querySelector('input') as HTMLInputElement | null\n if (!inputEl) return\n const startEl = this.renderRoot.querySelector('.slot-start') as HTMLElement | null\n const endEl = this.renderRoot.querySelector('.slot-end') as HTMLElement | null\n // iX rule: padding = slot width + half a rem of air.\n // If the slot is empty, reset to the CSS default (no\n // inline style).\n const startWidth = startEl?.getBoundingClientRect().width ?? 0\n const endWidth = endEl?.getBoundingClientRect().width ?? 0\n inputEl.style.paddingInlineStart = startWidth > 0\n ? `calc(${startWidth}px + ${SkenInput.SLOT_AIR})`\n : ''\n inputEl.style.paddingInlineEnd = endWidth > 0\n ? `calc(${endWidth}px + ${SkenInput.SLOT_AIR})`\n : ''\n })\n })\n }\n\n // Observer handles. Nullable because they are not created until\n // firstUpdated (which only runs in the browser, not during\n // server-side rendering or unit tests that don't mount).\n #slotStartObserver: MutationObserver | null = null\n #slotEndObserver: MutationObserver | null = null\n #visibilityObserver: IntersectionObserver | null = null\n\n protected override render() {\n const value = this.value ?? this.#internalValue\n return html`\n <div class=\"input-wrapper\" part=\"wrapper\">\n <div class=\"slot slot-start\" part=\"slot-start\">\n <slot name=\"start\"></slot>\n </div>\n <input\n part=\"input\"\n .value=${value}\n type=${this.type}\n placeholder=${this.placeholder ?? ''}\n ?disabled=${this.disabled}\n ?readonly=${this.readonly}\n ?required=${this.required}\n aria-invalid=${this.invalid ? 'true' : 'false'}\n aria-describedby=${this.describedById ?? ''}\n name=${this.name ?? ''}\n autocomplete=${this.autocomplete ?? 'off'}\n maxlength=${this.maxLength ?? ''}\n minlength=${this.minLength ?? ''}\n pattern=${this.pattern ?? ''}\n @input=${this.#handleInput}\n @change=${this.#handleChange}\n @focus=${this.#handleFocus}\n @blur=${this.#handleBlur}\n @keydown=${this.#handleKeydown}\n />\n <div class=\"slot slot-end\" part=\"slot-end\">\n <slot name=\"end\"></slot>\n </div>\n </div>\n `\n }\n\n // composed: true on every event so the framework adapter outside\n // the shadow boundary can listen.\n #handleInput = (event: Event) => {\n const target = event.target as HTMLInputElement\n const newValue = target.value\n if (this.value === undefined) this.#internalValue = newValue\n // Publish to the surrounding form so the new value is\n // included in FormData and submit-on-Enter sees the latest\n // text. Per-keystroke is fine: setFormValue is cheap.\n this.#syncFormValue()\n this.dispatchEvent(\n new CustomEvent<string>('sken-input', { detail: newValue, bubbles: true, composed: true }),\n )\n }\n\n #handleChange = (event: Event) => {\n const target = event.target as HTMLInputElement\n // Commit path: re-publish in case the browser mutated the\n // value (autofill, paste) without an intermediate input\n // event.\n this.#syncFormValue()\n this.dispatchEvent(\n new CustomEvent<string>('sken-change', {\n detail: target.value,\n bubbles: true,\n composed: true,\n }),\n )\n }\n\n #handleFocus = (event: FocusEvent) => {\n this.dispatchEvent(\n new CustomEvent<FocusEvent>('sken-focus', { detail: event, bubbles: true, composed: true }),\n )\n }\n\n #handleBlur = (event: FocusEvent) => {\n this.dispatchEvent(\n new CustomEvent<FocusEvent>('sken-blur', { detail: event, bubbles: true, composed: true }),\n )\n }\n\n // Wire Enter-on-the-inner-input to the host's associated form.\n //\n // The browser's implicit submission algorithm only sees the host\n // (the form-associated custom element), not the inner <input>\n // inside the shadow root. So pressing Enter in the inner input\n // reaches the form's submit algorithm via the host's keydown\n // listener but does NOT trigger a submit unless the form has a\n // default submit button. We bridge the shadow boundary here:\n // when the user presses Enter on the inner input, we call\n // `internals.form.requestSubmit()` ourselves. This dispatches\n // the form's submit event with the host as submitter, exactly\n // as if a native <input> had triggered it.\n //\n // We also `preventDefault()` so the browser's own handling of\n // Enter (which would otherwise insert a newline, in textarea,\n // or do nothing useful in input) does not double-dispatch.\n // `isComposing` guards against IME composition — pressing Enter\n // to confirm a kanji candidate should NOT submit the form.\n // `shiftKey` is allowed through for textareas (newline); single-\n // line inputs ignore it.\n #handleKeydown = (event: KeyboardEvent) => {\n if (event.key !== 'Enter' || event.isComposing) return\n if (this.disabled || this.readonly) return\n // preventDefault runs even if `internals.form` is null\n // (e.g. older test environments without ElementInternals\n // support, or the host is detached from a form). This keeps\n // the observable behaviour consistent: pressing Enter on\n // the inner input of a form-associated host is \"consumed\"\n // by the primitive, regardless of whether the surrounding\n // form machinery is available.\n event.preventDefault()\n const form = this.#internals?.form\n if (!form) return\n // requestSubmit() is the modern, non-deprecated way to\n // dispatch submit. It runs the form's submit handlers and\n // runs constraint validation. submitter defaults to the\n // form itself; consumers that need a specific submitter\n // can pass it explicitly.\n form.requestSubmit()\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'sken-input': SkenInput\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"sken-input.js","names":["#internalValue","#syncFormValue","#internals","#slotStartObserver","#slotEndObserver","#visibilityObserver","#adjustPaddings","#handleInput","#handleChange","#handleFocus","#handleBlur","#handleKeydown"],"sources":["../src/components/sken-input.ts"],"sourcesContent":["// ── <sken-input> — framework-ready Web Component ─────────────────────\n// Wraps a native <input> with token-driven styles. Controlled and\n// uncontrolled value, every standard <input> type, free form\n// integration. Requires @sken-ds/theme/css (ADR-0006).\n\nimport type { SkenInputSize, SkenInputType } from '@sken-ds/contracts'\nimport { LitElement, css, html } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\n\n@customElement('sken-input')\nexport class SkenInput extends LitElement {\n // Form-associated custom element: opt in to the ElementInternals\n // API so this primitive can participate in a surrounding\n // <form>'s submit/reset lifecycle. Without this flag, the\n // browser treats the host as an opaque element that does not\n // contribute to FormData and does not block Enter-to-submit\n // (the implicit submission rule for single text-like input\n // does not match against opaque elements). Lit docs:\n // https://lit.dev/docs/components/form/\n static formAssociated = true\n\n // The ElementInternals instance. Created in the constructor\n // via attachInternals(), which the browser only makes\n // available because `formAssociated` is true. We use it for:\n // - setFormValue() so the form picks up the current value.\n // - form reset (formResetCallback) for uncontrolled mode.\n // - reflecting `disabled` to the form's :disabled selector\n // (formDisabledCallback).\n // Nullable because some test environments (older happy-dom\n // versions, jsdom) do not implement ElementInternals. In\n // those environments form association is a no-op.\n #internals: ElementInternals | null = null\n\n constructor() {\n super()\n // attachInternals() is only callable when formAssociated is\n // true. The static flag is set above, so this resolves to a\n // real ElementInternals in any spec-compliant browser\n // (Chrome 77+, Firefox 98+, Safari 16.4+). If the host\n // environment does not implement it, the call throws — we\n // catch and degrade gracefully so SSR / tests that do not\n // need form association still work.\n try {\n this.#internals = this.attachInternals()\n } catch {\n this.#internals = null\n }\n }\n\n @property({ reflect: true }) type: SkenInputType = 'text'\n @property({ reflect: true }) size: SkenInputSize = 'md'\n @property() value: string | undefined = undefined\n @property({ attribute: 'default-value' }) defaultValue: string | undefined = undefined\n @property() placeholder: string | undefined = undefined\n @property({ reflect: true, type: Boolean }) disabled = false\n @property({ reflect: true, type: Boolean }) readonly = false\n @property({ reflect: true, type: Boolean }) required = false\n @property({ reflect: true, type: Boolean }) invalid = false\n @property({ attribute: 'described-by-id' }) describedById: string | undefined = undefined\n @property() name: string | undefined = undefined\n @property() autocomplete: AutoFill | undefined = undefined\n @property({ attribute: 'max-length', type: Number }) maxLength: number | undefined = undefined\n @property({ attribute: 'min-length', type: Number }) minLength: number | undefined = undefined\n @property() pattern: string | undefined = undefined\n\n // Internal value for uncontrolled mode. Seeded from\n // `defaultValue` on first connection; from then on the DOM\n // input keeps the source of truth.\n #internalValue: string = ''\n\n static styles = css`\n :host {\n display: inline-block;\n inline-size: 100%;\n }\n\n .input-wrapper {\n position: relative;\n display: flex;\n align-items: center;\n inline-size: 100%;\n block-size: 100%;\n }\n\n input {\n box-sizing: border-box;\n inline-size: 100%;\n border: 1px solid var(--sken-border);\n border-radius: var(--sken-md);\n background: var(--sken-input);\n color: var(--sken-foreground);\n font-family: var(--sken-family-sans);\n font-size: var(--sken-size-md);\n line-height: 1.25;\n text-overflow: ellipsis;\n padding-block: var(--sken-2);\n padding-inline: var(--sken-3);\n min-block-size: 2.25rem;\n transition:\n border-color 120ms ease,\n box-shadow 120ms ease;\n }\n\n input::placeholder {\n color: var(--sken-muted-foreground);\n opacity: 1; /* Firefox placeholder is <1 by default */\n }\n\n /* ── Slot containers ──────────────────────────────────────\n Position: absolute so the slotted content floats over the\n input without consuming flex space. The input's padding is\n auto-adjusted in JS to keep the value text from overlapping.\n pointer-events: none on the wrapper, auto on the slotted\n content, so only the visible icon/button receives clicks. */\n .slot {\n position: absolute;\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 1;\n pointer-events: none;\n color: var(--sken-muted-foreground);\n }\n\n .slot ::slotted(*) {\n pointer-events: auto;\n }\n\n .slot-start {\n inset-inline-start: 0.5rem;\n }\n\n .slot-end {\n inset-inline-end: 0.5rem;\n }\n\n /* ── Sizes ────────────────────────────────────────────────── */\n :host([size='sm']) input {\n padding-block: var(--sken-1);\n padding-inline: var(--sken-2);\n font-size: var(--sken-size-sm);\n min-block-size: 1.75rem;\n }\n :host([size='md']) input {\n /* Default styles above. */\n }\n :host([size='lg']) input {\n padding-block: var(--sken-3);\n padding-inline: var(--sken-4);\n font-size: var(--sken-size-lg);\n min-block-size: 2.75rem;\n }\n\n /* ── States ───────────────────────────────────────────────── */\n .input-wrapper:hover input:not(:disabled):not(:read-only) {\n border-color: var(--sken-foreground);\n }\n .input-wrapper:focus-within input {\n outline: 2px solid var(--sken-ring, currentColor);\n outline-offset: 2px;\n border-color: var(--sken-primary);\n }\n input:disabled {\n cursor: not-allowed;\n opacity: var(--sken-disabled);\n }\n input:read-only {\n background: var(--sken-muted);\n }\n :host([invalid]) input {\n border-color: var(--sken-destructive);\n }\n :host([invalid]) .input-wrapper:focus-within input {\n outline-color: var(--sken-destructive);\n }\n `\n\n override connectedCallback(): void {\n super.connectedCallback()\n if (this.value === undefined) {\n this.#internalValue = this.defaultValue ?? ''\n }\n // Publish the initial value to the surrounding form so that\n // FormData(form) and the implicit-submit-on-Enter rule see\n // the primitive as a real text input from the very first\n // render. Cheap (no allocation in the hot path after this).\n this.#syncFormValue()\n }\n\n override disconnectedCallback(): void {\n super.disconnectedCallback()\n this.#slotStartObserver?.disconnect()\n this.#slotEndObserver?.disconnect()\n this.#visibilityObserver?.disconnect()\n }\n\n // ── Form-association callbacks ─────────────────────────────────\n // Called by the browser when the form this primitive is\n // associated with is reset (either via form.reset() or by\n // clicking a <button type=\"reset\">). For uncontrolled mode we\n // restore the seed value; for controlled mode the consumer's\n // prop is the source of truth, so we re-render and let\n // Lit's render path re-publish the controlled value.\n formResetCallback(): void {\n if (this.value === undefined) {\n this.#internalValue = this.defaultValue ?? ''\n const inputEl = this.renderRoot.querySelector('input') as HTMLInputElement | null\n if (inputEl) inputEl.value = this.#internalValue\n this.#syncFormValue()\n } else {\n this.requestUpdate()\n }\n }\n\n // Called by the browser when the form's :disabled state\n // changes (e.g. a fieldset ancestor is disabled, or the\n // primitive gets associated with a disabled form). Mirrors\n // the `disabled` state onto the inner <input> so the visual\n // and the form submission are consistent.\n formDisabledCallback(isDisabled: boolean): void {\n // Reflect onto the prop so Lit re-renders and the\n // :host([disabled]) CSS rule kicks in. The inner <input>\n // picks up `?disabled=${this.disabled}` in the template.\n this.disabled = isDisabled\n }\n\n // Publish the current value to the form via ElementInternals.\n // Called on every change so FormData and submit/reset are\n // always in sync with the visible state.\n #syncFormValue(): void {\n if (!this.#internals) return\n const value = this.value ?? this.#internalValue\n // If the primitive has no `name`, FormData(form) skips it\n // anyway, so setFormValue is harmless but pointless. We\n // still call it to keep the implicit-submit rule consistent\n // (the spec checks the existence of a single text-like\n // submitter, not the name).\n this.#internals.setFormValue(value)\n }\n\n protected override firstUpdated(): void {\n const startEl = this.renderRoot.querySelector('.slot-start') as HTMLElement | null\n const endEl = this.renderRoot.querySelector('.slot-end') as HTMLElement | null\n const inputEl = this.renderRoot.querySelector('input') as HTMLInputElement | null\n if (!startEl || !endEl || !inputEl) return\n\n // MutationObserver on each slot container watches for\n // children being added/removed/attributes-changed. Cheap\n // because the subtree is just one or two nodes.\n this.#slotStartObserver = new MutationObserver(() => this.#adjustPaddings())\n this.#slotStartObserver.observe(startEl, { childList: true, subtree: true, attributes: true })\n this.#slotEndObserver = new MutationObserver(() => this.#adjustPaddings())\n this.#slotEndObserver.observe(endEl, { childList: true, subtree: true, attributes: true })\n\n // IntersectionObserver catches the case where the input is\n // hidden at first connect (e.g. inside a closed <details> or\n // an off-screen tab). The slot's bounding rect is 0 in that\n // case, so the padding would be wrong until the user opens\n // the parent. Re-measure when the host becomes visible.\n this.#visibilityObserver = new IntersectionObserver((entries) => {\n for (const entry of entries) {\n if (entry.isIntersecting) this.#adjustPaddings()\n }\n })\n this.#visibilityObserver.observe(this)\n\n // Also listen for the native slotchange event as a belt-and-\n // braces in case the MutationObserver misses something\n // (e.g. text node changes inside the slotted element).\n const startSlot = this.renderRoot.querySelector('slot[name=\"start\"]') as HTMLSlotElement | null\n const endSlot = this.renderRoot.querySelector('slot[name=\"end\"]') as HTMLSlotElement | null\n startSlot?.addEventListener('slotchange', this.#adjustPaddings)\n endSlot?.addEventListener('slotchange', this.#adjustPaddings)\n\n // First measurement.\n this.#adjustPaddings()\n }\n\n // The gap between a slot's edge and the input's text. iX uses\n // 0.5rem; we keep that because it balances the visual weight of\n // a 16px icon against the input's 0.75rem default padding-inline.\n // Module-scoped (not class-scoped) because TS forbids static\n // private fields on classes that use the legacy decorators.\n static SLOT_AIR = '0.5rem'\n\n // Two rAFs: the first lets Lit finish painting the slot, the\n // second lets the browser compute the slotted element's box.\n // Without the second, getBoundingClientRect() on a freshly-\n // assigned icon returns 0.\n #adjustPaddings = (): void => {\n requestAnimationFrame(() => {\n requestAnimationFrame(() => {\n const inputEl = this.renderRoot.querySelector('input') as HTMLInputElement | null\n if (!inputEl) return\n const startEl = this.renderRoot.querySelector('.slot-start') as HTMLElement | null\n const endEl = this.renderRoot.querySelector('.slot-end') as HTMLElement | null\n // iX rule: padding = slot width + half a rem of air.\n // If the slot is empty, reset to the CSS default (no\n // inline style).\n const startWidth = startEl?.getBoundingClientRect().width ?? 0\n const endWidth = endEl?.getBoundingClientRect().width ?? 0\n inputEl.style.paddingInlineStart =\n startWidth > 0 ? `calc(${startWidth}px + ${SkenInput.SLOT_AIR})` : ''\n inputEl.style.paddingInlineEnd =\n endWidth > 0 ? `calc(${endWidth}px + ${SkenInput.SLOT_AIR})` : ''\n })\n })\n }\n\n // Observer handles. Nullable because they are not created until\n // firstUpdated (which only runs in the browser, not during\n // server-side rendering or unit tests that don't mount).\n #slotStartObserver: MutationObserver | null = null\n #slotEndObserver: MutationObserver | null = null\n #visibilityObserver: IntersectionObserver | null = null\n\n protected override render() {\n const value = this.value ?? this.#internalValue\n return html`\n <div class=\"input-wrapper\" part=\"wrapper\">\n <div class=\"slot slot-start\" part=\"slot-start\">\n <slot name=\"start\"></slot>\n </div>\n <input\n part=\"input\"\n .value=${value}\n type=${this.type}\n placeholder=${this.placeholder ?? ''}\n ?disabled=${this.disabled}\n ?readonly=${this.readonly}\n ?required=${this.required}\n aria-invalid=${this.invalid ? 'true' : 'false'}\n aria-describedby=${this.describedById ?? ''}\n name=${this.name ?? ''}\n autocomplete=${this.autocomplete ?? 'off'}\n maxlength=${this.maxLength ?? ''}\n minlength=${this.minLength ?? ''}\n pattern=${this.pattern ?? ''}\n @input=${this.#handleInput}\n @change=${this.#handleChange}\n @focus=${this.#handleFocus}\n @blur=${this.#handleBlur}\n @keydown=${this.#handleKeydown}\n />\n <div class=\"slot slot-end\" part=\"slot-end\">\n <slot name=\"end\"></slot>\n </div>\n </div>\n `\n }\n\n // composed: true on every event so the framework adapter outside\n // the shadow boundary can listen.\n #handleInput = (event: Event) => {\n const target = event.target as HTMLInputElement\n const newValue = target.value\n if (this.value === undefined) this.#internalValue = newValue\n // Publish to the surrounding form so the new value is\n // included in FormData and submit-on-Enter sees the latest\n // text. Per-keystroke is fine: setFormValue is cheap.\n this.#syncFormValue()\n this.dispatchEvent(\n new CustomEvent<string>('sken-input', { detail: newValue, bubbles: true, composed: true }),\n )\n }\n\n #handleChange = (event: Event) => {\n const target = event.target as HTMLInputElement\n // Commit path: re-publish in case the browser mutated the\n // value (autofill, paste) without an intermediate input\n // event.\n this.#syncFormValue()\n this.dispatchEvent(\n new CustomEvent<string>('sken-change', {\n detail: target.value,\n bubbles: true,\n composed: true,\n }),\n )\n }\n\n #handleFocus = (event: FocusEvent) => {\n this.dispatchEvent(\n new CustomEvent<FocusEvent>('sken-focus', { detail: event, bubbles: true, composed: true }),\n )\n }\n\n #handleBlur = (event: FocusEvent) => {\n this.dispatchEvent(\n new CustomEvent<FocusEvent>('sken-blur', { detail: event, bubbles: true, composed: true }),\n )\n }\n\n // Wire Enter-on-the-inner-input to the host's associated form.\n //\n // The browser's implicit submission algorithm only sees the host\n // (the form-associated custom element), not the inner <input>\n // inside the shadow root. So pressing Enter in the inner input\n // reaches the form's submit algorithm via the host's keydown\n // listener but does NOT trigger a submit unless the form has a\n // default submit button. We bridge the shadow boundary here:\n // when the user presses Enter on the inner input, we call\n // `internals.form.requestSubmit()` ourselves. This dispatches\n // the form's submit event with the host as submitter, exactly\n // as if a native <input> had triggered it.\n //\n // We also `preventDefault()` so the browser's own handling of\n // Enter (which would otherwise insert a newline, in textarea,\n // or do nothing useful in input) does not double-dispatch.\n // `isComposing` guards against IME composition — pressing Enter\n // to confirm a kanji candidate should NOT submit the form.\n // `shiftKey` is allowed through for textareas (newline); single-\n // line inputs ignore it.\n #handleKeydown = (event: KeyboardEvent) => {\n if (event.key !== 'Enter' || event.isComposing) return\n if (this.disabled || this.readonly) return\n // preventDefault runs even if `internals.form` is null\n // (e.g. older test environments without ElementInternals\n // support, or the host is detached from a form). This keeps\n // the observable behaviour consistent: pressing Enter on\n // the inner input of a form-associated host is \"consumed\"\n // by the primitive, regardless of whether the surrounding\n // form machinery is available.\n event.preventDefault()\n const form = this.#internals?.form\n if (!form) return\n // requestSubmit() is the modern, non-deprecated way to\n // dispatch submit. It runs the form's submit handlers and\n // runs constraint validation. submitter defaults to the\n // form itself; consumers that need a specific submitter\n // can pass it explicitly.\n form.requestSubmit()\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'sken-input': SkenInput\n }\n}\n"],"mappings":"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"}
@@ -7,9 +7,15 @@ var s = {
7
7
  sm: "12rem",
8
8
  md: "16rem",
9
9
  lg: "20rem"
10
- }, c = class extends t {
10
+ }, c = {
11
+ topbar: "var(--sken-card)",
12
+ "primary-nav": "var(--sken-background)",
13
+ sidebar: "var(--sken-muted)",
14
+ content: "var(--sken-background)",
15
+ footer: "var(--sken-muted)"
16
+ }, l = class extends t {
11
17
  constructor(...e) {
12
- super(...e), this.variant = "header-content", this.topbarMode = "fixed", this.sidebarPosition = "start", this.sidebarSize = "md";
18
+ super(...e), this.variant = "header-content", this.topbarMode = "fixed", this.sidebarPosition = "start", this.sidebarSize = "md", this.density = "none";
13
19
  }
14
20
  static {
15
21
  this.styles = n`
@@ -47,32 +53,118 @@ var s = {
47
53
  grid-template-areas: 'content sidebar';
48
54
  }
49
55
 
50
- /* ── Per-region grid placement ───────────────────────────────
56
+ /* ── Per-region grid placement + DS-driven surfaces ──────────
51
57
  The light-DOM <sken-layout-region> children are projected
52
58
  here through the unnamed <slot>. The grid-area rule per
53
59
  name positions each one in the right cell. We select the
54
60
  slotted custom elements by attribute, which works
55
- transparently across the shadow boundary. */
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. */
56
87
  ::slotted(sken-layout-region[name='topbar']) {
57
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);
58
93
  }
59
94
  ::slotted(sken-layout-region[name='primary-nav']) {
60
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);
61
103
  }
62
104
  ::slotted(sken-layout-region[name='sidebar']) {
63
105
  grid-area: sidebar;
64
106
  min-block-size: 0;
65
- border-inline-end: 1px solid
66
- var(--sken-border, color-mix(in srgb, currentColor 12%, transparent));
107
+ background: var(--sken-layout-region-sidebar-background, ${i(c.sidebar)});
108
+ padding-block: var(--sken-layout-region-sidebar-padding-block, 0);
109
+ padding-inline: var(--sken-layout-region-sidebar-padding-inline, 0);
110
+ border-inline-end: 1px solid var(--sken-border);
67
111
  }
68
112
  ::slotted(sken-layout-region[name='content']) {
69
113
  grid-area: content;
70
114
  min-block-size: 0;
115
+ background: var(--sken-layout-region-content-background, ${i(c.content)});
116
+ padding-block: var(--sken-layout-region-content-padding-block, 0);
117
+ padding-inline: var(--sken-layout-region-content-padding-inline, 0);
71
118
  }
72
119
  ::slotted(sken-layout-region[name='footer']) {
73
120
  grid-area: footer;
74
- border-top: 1px solid
75
- var(--sken-border, color-mix(in srgb, currentColor 12%, transparent));
121
+ background: var(--sken-layout-region-footer-background, ${i(c.footer)});
122
+ padding-block: var(--sken-layout-region-footer-padding-block, 0);
123
+ padding-inline: var(--sken-layout-region-footer-padding-inline, 0);
124
+ border-top: 1px solid var(--sken-border);
125
+ }
126
+
127
+ /* ── Density: comfortable ─────────────────────────────────────
128
+ When density=comfortable, the layout applies the Sken
129
+ default rhythm: 8/16px on chrome cells, 16/24px on the
130
+ content area, 12/16px on the footer. The values come
131
+ from the spacing scale (--sken-2, --sken-3, --sken-4,
132
+ --sken-6) so a single token change ripples through
133
+ every cell. A consumer override on any
134
+ --sken-layout-region-{role}-padding-{block|inline}
135
+ always wins over the density preset. */
136
+ :host([density='comfortable']) ::slotted(sken-layout-region) {
137
+ --sken-layout-region-topbar-padding-block: var(--sken-2);
138
+ --sken-layout-region-topbar-padding-inline: var(--sken-4);
139
+ --sken-layout-region-primary-nav-padding-block: var(--sken-2);
140
+ --sken-layout-region-primary-nav-padding-inline: var(--sken-4);
141
+ --sken-layout-region-sidebar-padding-block: var(--sken-4);
142
+ --sken-layout-region-sidebar-padding-inline: var(--sken-4);
143
+ --sken-layout-region-content-padding-block: var(--sken-6);
144
+ --sken-layout-region-content-padding-inline: var(--sken-6);
145
+ --sken-layout-region-footer-padding-block: var(--sken-3);
146
+ --sken-layout-region-footer-padding-inline: var(--sken-4);
147
+ }
148
+
149
+ /* ── Density: compact ────────────────────────────────────────
150
+ When density=compact, every per-cell padding is halved
151
+ from the comfortable value. We use calc(var(--sken-N) *
152
+ 0.5) instead of hardcoded half-values so a future
153
+ token change automatically cascades. As with
154
+ comfortable, a consumer override on any
155
+ --sken-layout-region-{role}-padding-{block|inline}
156
+ always wins. */
157
+ :host([density='compact']) ::slotted(sken-layout-region) {
158
+ --sken-layout-region-topbar-padding-block: calc(var(--sken-2) * 0.5);
159
+ --sken-layout-region-topbar-padding-inline: calc(var(--sken-4) * 0.5);
160
+ --sken-layout-region-primary-nav-padding-block: calc(var(--sken-2) * 0.5);
161
+ --sken-layout-region-primary-nav-padding-inline: calc(var(--sken-4) * 0.5);
162
+ --sken-layout-region-sidebar-padding-block: calc(var(--sken-4) * 0.5);
163
+ --sken-layout-region-sidebar-padding-inline: calc(var(--sken-4) * 0.5);
164
+ --sken-layout-region-content-padding-block: calc(var(--sken-6) * 0.5);
165
+ --sken-layout-region-content-padding-inline: calc(var(--sken-6) * 0.5);
166
+ --sken-layout-region-footer-padding-block: calc(var(--sken-3) * 0.5);
167
+ --sken-layout-region-footer-padding-inline: calc(var(--sken-4) * 0.5);
76
168
  }
77
169
 
78
170
  /* ── Grid templates per variant ─────────────────────────────── */
@@ -121,15 +213,13 @@ var s = {
121
213
  }
122
214
 
123
215
  /* ── Sidebar position ─────────────────────────────────────────
124
- The 'with-sidebar' class is applied via JS in render() so
125
- we can include the 'sidebar-end' modifier in the same
126
- selector chain. (You cannot combine the [sidebar-position]
127
- attribute with the '.with-sidebar' class in a single
128
- selector, because the class is conditional.) */
216
+ When the sidebar sits on the inline-end side, the border
217
+ moves to the inline-start side. The default rule above
218
+ already paints the border on the inline-end; here we
219
+ swap it. */
129
220
  :host([sidebar-position='end']) ::slotted(sken-layout-region[name='sidebar']) {
130
221
  border-inline-end: none;
131
- border-inline-start: 1px solid
132
- var(--sken-border, color-mix(in srgb, currentColor 12%, transparent));
222
+ border-inline-start: 1px solid var(--sken-border);
133
223
  }
134
224
 
135
225
  /* ── Sidebar width override ─────────────────────────────────── */
@@ -189,17 +279,17 @@ var s = {
189
279
  return e === "sidebar-content" || e === "header-sidebar" || e === "header-sidebar-footer";
190
280
  }
191
281
  };
192
- e([o({ reflect: !0 })], c.prototype, "variant", void 0), e([o({
282
+ e([o({ reflect: !0 })], l.prototype, "variant", void 0), e([o({
193
283
  attribute: "topbar-mode",
194
284
  reflect: !0
195
- })], c.prototype, "topbarMode", void 0), e([o({
285
+ })], l.prototype, "topbarMode", void 0), e([o({
196
286
  attribute: "sidebar-position",
197
287
  reflect: !0
198
- })], c.prototype, "sidebarPosition", void 0), e([o({
288
+ })], l.prototype, "sidebarPosition", void 0), e([o({
199
289
  attribute: "sidebar-size",
200
290
  reflect: !0
201
- })], c.prototype, "sidebarSize", void 0), c = e([a("sken-layout")], c);
291
+ })], l.prototype, "sidebarSize", void 0), e([o({ reflect: !0 })], l.prototype, "density", void 0), l = e([a("sken-layout")], l);
202
292
  //#endregion
203
- export { c as SkenLayout };
293
+ export { l as SkenLayout };
204
294
 
205
295
  //# sourceMappingURL=sken-layout.js.map