@sken-ds/primitives 0.3.5 → 0.3.7

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 CHANGED
@@ -1,14 +1,16 @@
1
1
  import { t as e } from "./sken-datatable-BtR-NwRN.js";
2
2
  import { SkenButton as t } from "./sken-button.js";
3
3
  import { SkenCheckbox as n } from "./sken-checkbox.js";
4
- import { SkenInput as r } from "./sken-input.js";
5
- import { SkenNumberInput as i } from "./sken-number-input.js";
6
- import { SkenTextarea as a } from "./sken-textarea.js";
7
- import { SkenSwitch as o } from "./sken-switch.js";
8
- import { SkenFilterChip as s } from "./sken-filter-chip.js";
9
- import { SkenCombobox as c } from "./sken-combobox.js";
10
- import { SkenComboboxItem as l } from "./sken-combobox-item.js";
11
- import { SkenComboboxGroup as u } from "./sken-combobox-group.js";
12
- import { SkenDatePicker as d } from "./sken-date-picker.js";
13
- import { t as f } from "./sken-dialog-hRtML2jW.js";
14
- export { t as SkenButton, n as SkenCheckbox, c as SkenCombobox, u as SkenComboboxGroup, l as SkenComboboxItem, e as SkenDataTable, d as SkenDatePicker, f as SkenDialog, s as SkenFilterChip, r as SkenInput, i as SkenNumberInput, o as SkenSwitch, a as SkenTextarea };
4
+ import { SkenFilterChip as r } from "./sken-filter-chip.js";
5
+ import { SkenCombobox as i } from "./sken-combobox.js";
6
+ import { SkenComboboxGroup as a } from "./sken-combobox-group.js";
7
+ import { SkenComboboxItem as o } from "./sken-combobox-item.js";
8
+ import { SkenDatePicker as s } from "./sken-date-picker.js";
9
+ import { t as c } from "./sken-dialog-hRtML2jW.js";
10
+ import { SkenInput as l } from "./sken-input.js";
11
+ import { SkenLayoutRegion as u } from "./sken-layout-region.js";
12
+ import { SkenLayout as d } from "./sken-layout.js";
13
+ import { SkenNumberInput as f } from "./sken-number-input.js";
14
+ import { SkenSwitch as p } from "./sken-switch.js";
15
+ import { SkenTextarea as m } from "./sken-textarea.js";
16
+ export { t as SkenButton, n as SkenCheckbox, i as SkenCombobox, a as SkenComboboxGroup, o as SkenComboboxItem, e as SkenDataTable, s as SkenDatePicker, c as SkenDialog, r as SkenFilterChip, l as SkenInput, d as SkenLayout, u as SkenLayoutRegion, f as SkenNumberInput, p as SkenSwitch, m as SkenTextarea };
@@ -3,18 +3,28 @@ import { LitElement as t, css as n, html as r } from "lit";
3
3
  import { customElement as i, property as a } from "lit/decorators.js";
4
4
  //#region src/components/sken-button.ts
5
5
  var o = class extends t {
6
- constructor(...e) {
7
- super(...e), this.variant = "primary", this.size = "md", this.disabled = !1, this.type = "button", this.ariaLabel = null, this.#e = (e) => {
6
+ static {
7
+ this.formAssociated = !0;
8
+ }
9
+ #e;
10
+ constructor() {
11
+ super(), this.#e = null, this.variant = "primary", this.size = "md", this.disabled = !1, this.type = "button", this.ariaLabel = null, this.#t = (e) => {
8
12
  if (this.disabled) {
9
13
  e.preventDefault(), e.stopImmediatePropagation();
10
14
  return;
11
15
  }
12
- this.dispatchEvent(new CustomEvent("sken-click", {
16
+ let t = this.#e?.form;
17
+ this.type === "submit" && t ? (e.preventDefault(), t.requestSubmit()) : this.type === "reset" && t && (e.preventDefault(), t.reset()), this.dispatchEvent(new CustomEvent("sken-click", {
13
18
  detail: { originalEvent: e },
14
19
  bubbles: !0,
15
20
  composed: !0
16
21
  }));
17
22
  };
23
+ try {
24
+ this.#e = this.attachInternals();
25
+ } catch {
26
+ this.#e = null;
27
+ }
18
28
  }
19
29
  static {
20
30
  this.styles = n`
@@ -107,13 +117,13 @@ var o = class extends t {
107
117
  part="button"
108
118
  ?disabled=${this.disabled}
109
119
  type=${this.type}
110
- @click=${this.#e}
120
+ @click=${this.#t}
111
121
  >
112
122
  <slot></slot>
113
123
  </button>
114
124
  `;
115
125
  }
116
- #e;
126
+ #t;
117
127
  };
118
128
  e([a({ reflect: !0 })], o.prototype, "variant", void 0), e([a({ reflect: !0 })], o.prototype, "size", void 0), e([a({
119
129
  reflect: !0,
@@ -1 +1 @@
1
- {"version":3,"file":"sken-button.js","names":["#handleClick"],"sources":["../src/components/sken-button.ts"],"sourcesContent":["// ── <sken-button> — framework-ready Web Component ────────────────────\n// Wraps a native <button> with token-driven styles. Contract lives in\n// @sken-ds/contracts; this file is the implementation.\n//\n// Requires @sken-ds/theme/css loaded: every --sken-* variable this\n// component reads comes from the theme. If the theme is missing,\n// styles fall back to initial / inherited values and the button\n// looks broken — intentional, not silent. ADR-0006 / ADR-0004.\n\nimport { LitElement, html, css } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\nimport type { SkenButtonVariant, SkenButtonSize } from '@sken-ds/contracts'\n\n@customElement('sken-button')\nexport class SkenButton extends LitElement {\n @property({ reflect: true }) variant: SkenButtonVariant = 'primary'\n @property({ reflect: true }) size: SkenButtonSize = 'md'\n @property({ reflect: true, type: Boolean }) disabled = false\n @property() type: 'button' | 'submit' | 'reset' = 'button'\n\n // `string | null` matches `lib.dom.d.ts`; the contract normalizes\n // to `string | undefined` for the public API.\n @property({ attribute: 'aria-label' }) override ariaLabel: string | null = null\n\n static styles = css`\n :host {\n display: inline-block;\n }\n\n button {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: var(--sken-2);\n box-sizing: border-box;\n border: 1px solid transparent;\n border-radius: var(--sken-md);\n background: transparent;\n color: inherit;\n font-family: var(--sken-family-sans);\n font-size: var(--sken-size-md);\n font-weight: 500;\n line-height: 1.25;\n text-decoration: none;\n white-space: nowrap;\n cursor: pointer;\n transition:\n background-color 120ms ease,\n border-color 120ms ease,\n color 120ms ease;\n padding: var(--sken-2) var(--sken-4);\n min-height: 2.25rem;\n }\n\n /* ── Sizes ────────────────────────────────────────────────── */\n :host([size='sm']) button {\n padding: var(--sken-1) var(--sken-3);\n font-size: var(--sken-size-sm);\n min-height: 1.75rem;\n }\n :host([size='md']) button {\n /* Default styles above. */\n }\n :host([size='lg']) button {\n padding: var(--sken-3) var(--sken-5);\n font-size: var(--sken-size-lg);\n min-height: 2.75rem;\n }\n\n /* ── Variants ─────────────────────────────────────────────── */\n :host([variant='primary']) button {\n background: var(--sken-primary);\n color: var(--sken-primary-foreground);\n }\n :host([variant='primary']) button:hover:not(:disabled) {\n background: var(--sken-primary-hover);\n }\n :host([variant='primary']) button:active:not(:disabled) {\n background: var(--sken-primary-active);\n }\n\n :host([variant='secondary']) button {\n background: var(--sken-transparent);\n color: var(--sken-foreground);\n border-color: var(--sken-border);\n }\n :host([variant='secondary']) button:hover:not(:disabled) {\n background: var(--sken-muted);\n }\n\n :host([variant='ghost']) button {\n color: var(--sken-foreground);\n }\n :host([variant='ghost']) button:hover:not(:disabled) {\n background: var(--sken-muted);\n }\n\n /* ── States ───────────────────────────────────────────────── */\n button:focus-visible {\n outline: 2px solid var(--sken-ring, currentColor);\n outline-offset: 2px;\n }\n button:disabled {\n cursor: not-allowed;\n opacity: var(--sken-disabled);\n }\n `\n\n protected override render() {\n return html`\n <button\n part=\"button\"\n ?disabled=${this.disabled}\n type=${this.type}\n @click=${this.#handleClick}\n >\n <slot></slot>\n </button>\n `\n }\n\n // `composed: true` is required for the event to cross the shadow\n // boundary; `preventDefault` + `stopImmediatePropagation` on a\n // disabled button suppresses the native submit-on-Enter behavior.\n #handleClick = (event: MouseEvent | KeyboardEvent) => {\n if (this.disabled) {\n event.preventDefault()\n event.stopImmediatePropagation()\n return\n }\n this.dispatchEvent(\n new CustomEvent('sken-click', {\n detail: { originalEvent: event },\n bubbles: true,\n composed: true,\n }),\n )\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'sken-button': SkenButton\n }\n}\n"],"mappings":";;;;AAcO,IAAM,IAAN,cAAyB,EAAW;;EA8GzB,aA7G0C,KAAA,UAAA,WACN,KAAA,OAAA,MACG,KAAA,WAAA,IACL,KAAA,OAAA,UAIyB,KAAA,YAAA,MAsG3D,KAAA,MAAA,MAAsC;GACpD,IAAI,KAAK,UAAU;IAEjB,AADA,EAAM,eAAe,GACrB,EAAM,yBAAyB;IAC/B;GACF;GACA,KAAK,cACH,IAAI,YAAY,cAAc;IAC5B,QAAQ,EAAE,eAAe,EAAM;IAC/B,SAAS;IACT,UAAU;GACZ,CAAC,CACH;EACF;;;EAjHgB,KAAA,SAAA,CAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoFnB,SAA4B;EAC1B,OAAO,CAAI;;;oBAGK,KAAK,SAAS;eACnB,KAAK,KAAK;iBACR,KAAKA,GAAa;;;;;CAKjC;CAKA;AAcF;AA3HG,EAAA,CAAA,EAAS,EAAE,SAAS,GAAK,CAAC,CAAA,GAAA,EAAA,WAAA,WAAA,KAAA,CAAA,GAC1B,EAAA,CAAA,EAAS,EAAE,SAAS,GAAK,CAAC,CAAA,GAAA,EAAA,WAAA,QAAA,KAAA,CAAA,GAC1B,EAAA,CAAA,EAAS;CAAE,SAAS;CAAM,MAAM;AAAQ,CAAC,CAAA,GAAA,EAAA,WAAA,YAAA,KAAA,CAAA,GACzC,EAAA,CAAA,EAAS,CAAA,GAAA,EAAA,WAAA,QAAA,KAAA,CAAA,GAIT,EAAA,CAAA,EAAS,EAAE,WAAW,aAAa,CAAC,CAAA,GAAA,EAAA,WAAA,aAAA,KAAA,CAAA,GATtC,IAAA,EAAA,CAAA,EAAc,aAAa,CAAA,GAAA,CAAA"}
1
+ {"version":3,"file":"sken-button.js","names":["#internals","#handleClick"],"sources":["../src/components/sken-button.ts"],"sourcesContent":["// ── <sken-button> — framework-ready Web Component ────────────────────\n// Wraps a native <button> with token-driven styles. Contract lives in\n// @sken-ds/contracts; this file is the implementation.\n//\n// Requires @sken-ds/theme/css loaded: every --sken-* variable this\n// component reads comes from the theme. If the theme is missing,\n// styles fall back to initial / inherited values and the button\n// looks broken — intentional, not silent. ADR-0006 / ADR-0004.\n\nimport { LitElement, html, css } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\nimport type { SkenButtonVariant, SkenButtonSize } from '@sken-ds/contracts'\n\n@customElement('sken-button')\nexport class SkenButton extends LitElement {\n // Form-associated custom element: opt in to the ElementInternals\n // API so this primitive can reach its surrounding <form> via\n // `internals.form`. We do NOT publish a value to FormData\n // (this is a button, not an input), but we DO need form\n // access for the `submit` and `reset` click bridges below.\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. Nullable to\n // gracefully degrade in test environments without\n // ElementInternals.\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 @property({ reflect: true }) variant: SkenButtonVariant = 'primary'\n @property({ reflect: true }) size: SkenButtonSize = 'md'\n @property({ reflect: true, type: Boolean }) disabled = false\n @property() type: 'button' | 'submit' | 'reset' = 'button'\n\n // `string | null` matches `lib.dom.d.ts`; the contract normalizes\n // to `string | undefined` for the public API.\n @property({ attribute: 'aria-label' }) override ariaLabel: string | null = null\n\n static styles = css`\n :host {\n display: inline-block;\n }\n\n button {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: var(--sken-2);\n box-sizing: border-box;\n border: 1px solid transparent;\n border-radius: var(--sken-md);\n background: transparent;\n color: inherit;\n font-family: var(--sken-family-sans);\n font-size: var(--sken-size-md);\n font-weight: 500;\n line-height: 1.25;\n text-decoration: none;\n white-space: nowrap;\n cursor: pointer;\n transition:\n background-color 120ms ease,\n border-color 120ms ease,\n color 120ms ease;\n padding: var(--sken-2) var(--sken-4);\n min-height: 2.25rem;\n }\n\n /* ── Sizes ────────────────────────────────────────────────── */\n :host([size='sm']) button {\n padding: var(--sken-1) var(--sken-3);\n font-size: var(--sken-size-sm);\n min-height: 1.75rem;\n }\n :host([size='md']) button {\n /* Default styles above. */\n }\n :host([size='lg']) button {\n padding: var(--sken-3) var(--sken-5);\n font-size: var(--sken-size-lg);\n min-height: 2.75rem;\n }\n\n /* ── Variants ─────────────────────────────────────────────── */\n :host([variant='primary']) button {\n background: var(--sken-primary);\n color: var(--sken-primary-foreground);\n }\n :host([variant='primary']) button:hover:not(:disabled) {\n background: var(--sken-primary-hover);\n }\n :host([variant='primary']) button:active:not(:disabled) {\n background: var(--sken-primary-active);\n }\n\n :host([variant='secondary']) button {\n background: var(--sken-transparent);\n color: var(--sken-foreground);\n border-color: var(--sken-border);\n }\n :host([variant='secondary']) button:hover:not(:disabled) {\n background: var(--sken-muted);\n }\n\n :host([variant='ghost']) button {\n color: var(--sken-foreground);\n }\n :host([variant='ghost']) button:hover:not(:disabled) {\n background: var(--sken-muted);\n }\n\n /* ── States ───────────────────────────────────────────────── */\n button:focus-visible {\n outline: 2px solid var(--sken-ring, currentColor);\n outline-offset: 2px;\n }\n button:disabled {\n cursor: not-allowed;\n opacity: var(--sken-disabled);\n }\n `\n\n protected override render() {\n return html`\n <button\n part=\"button\"\n ?disabled=${this.disabled}\n type=${this.type}\n @click=${this.#handleClick}\n >\n <slot></slot>\n </button>\n `\n }\n\n // `composed: true` is required for the `sken-click` event to\n // cross the shadow boundary; `preventDefault` +\n // `stopImmediatePropagation` on a disabled button suppresses the\n // native submit-on-Enter behavior.\n //\n // For `type=\"submit\"` and `type=\"reset\"` we ALSO bridge the\n // shadow boundary so the surrounding <form> receives the\n // submit / reset. Without this, the inner <button type=\"submit\">'s\n // default action submits the (notional) form inside the shadow\n // root, which doesn't exist — no `submit` event ever reaches the\n // outer light-DOM form. `internals.form.requestSubmit()` is the\n // modern, non-deprecated API; it dispatches a real `submit` event\n // on the form and runs constraint validation. `form.reset()` is\n // the standard form reset method. We `preventDefault()` to\n // suppress the inner button's default action (submit/reset the\n // shadow form) so the bridge is the only path. Without it we\n // would get a phantom submit to a nonexistent form AND a\n // requestSubmit to the real one. (Tested with happy-dom: phantom\n // submit is a no-op there because the shadow form doesn't exist,\n // but on a real browser it would dispatch a submit event to a\n // formless button — harmless but noisy.)\n //\n // The `sken-click` event is dispatched for every type so\n // consumers can listen to it regardless of whether they put the\n // handler on the button or on the form. Consumers that want\n // submit-only handling should listen to the form's `submit`\n // event, not the button's click — same as native HTML.\n #handleClick = (event: MouseEvent | KeyboardEvent) => {\n if (this.disabled) {\n event.preventDefault()\n event.stopImmediatePropagation()\n return\n }\n const form = this.#internals?.form\n if (this.type === 'submit' && form) {\n event.preventDefault()\n form.requestSubmit()\n } else if (this.type === 'reset' && form) {\n event.preventDefault()\n form.reset()\n }\n this.dispatchEvent(\n new CustomEvent('sken-click', {\n detail: { originalEvent: event },\n bubbles: true,\n composed: true,\n }),\n )\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'sken-button': SkenButton\n }\n}\n"],"mappings":";;;;AAcO,IAAM,IAAN,cAAyB,EAAW;;EAOjB,KAAA,iBAAA;;CAOxB;CAEA,cAAc;EA8IE,AA7Id,MAAM,GAH8B,KAAA,KAAA,MAWoB,KAAA,UAAA,WACN,KAAA,OAAA,MACG,KAAA,WAAA,IACL,KAAA,OAAA,UAIyB,KAAA,YAAA,MA8H3D,KAAA,MAAA,MAAsC;GACpD,IAAI,KAAK,UAAU;IAEjB,AADA,EAAM,eAAe,GACrB,EAAM,yBAAyB;IAC/B;GACF;GACA,IAAM,IAAO,KAAKA,IAAY;GAQ9B,AAPI,KAAK,SAAS,YAAY,KAC5B,EAAM,eAAe,GACrB,EAAK,cAAc,KACV,KAAK,SAAS,WAAW,MAClC,EAAM,eAAe,GACrB,EAAK,MAAM,IAEb,KAAK,cACH,IAAI,YAAY,cAAc;IAC5B,QAAQ,EAAE,eAAe,EAAM;IAC/B,SAAS;IACT,UAAU;GACZ,CAAC,CACH;EACF;EAjKE,IAAI;GACF,KAAKA,KAAa,KAAK,gBAAgB;EACzC,QAAQ;GACN,KAAKA,KAAa;EACpB;CACF;;EAWgB,KAAA,SAAA,CAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoFnB,SAA4B;EAC1B,OAAO,CAAI;;;oBAGK,KAAK,SAAS;eACnB,KAAK,KAAK;iBACR,KAAKC,GAAa;;;;;CAKjC;CA6BA;AAsBF;AA3JG,EAAA,CAAA,EAAS,EAAE,SAAS,GAAK,CAAC,CAAA,GAAA,EAAA,WAAA,WAAA,KAAA,CAAA,GAC1B,EAAA,CAAA,EAAS,EAAE,SAAS,GAAK,CAAC,CAAA,GAAA,EAAA,WAAA,QAAA,KAAA,CAAA,GAC1B,EAAA,CAAA,EAAS;CAAE,SAAS;CAAM,MAAM;AAAQ,CAAC,CAAA,GAAA,EAAA,WAAA,YAAA,KAAA,CAAA,GACzC,EAAA,CAAA,EAAS,CAAA,GAAA,EAAA,WAAA,QAAA,KAAA,CAAA,GAIT,EAAA,CAAA,EAAS,EAAE,WAAW,aAAa,CAAC,CAAA,GAAA,EAAA,WAAA,aAAA,KAAA,CAAA,GAjCtC,IAAA,EAAA,CAAA,EAAc,aAAa,CAAA,GAAA,CAAA"}
@@ -0,0 +1,30 @@
1
+ import { t as e } from "./decorate-D4FKzyDw.js";
2
+ import { LitElement as t, css as n, html as r } from "lit";
3
+ import { customElement as i, property as a } from "lit/decorators.js";
4
+ //#region src/components/sken-layout-region.ts
5
+ var o = class extends t {
6
+ constructor(...e) {
7
+ super(...e), this.name = "content";
8
+ }
9
+ static {
10
+ this.styles = n`
11
+ /* 'display: contents' makes the region transparent in the
12
+ layout: it does not introduce a wrapper box, so the
13
+ consumer's children are sized and positioned as if they
14
+ were direct children of the host grid. The product can
15
+ override with 'display: block' (or anything else) if it
16
+ wants the region to be a real box. */
17
+ :host {
18
+ display: contents;
19
+ }
20
+ `;
21
+ }
22
+ render() {
23
+ return r`<slot></slot>`;
24
+ }
25
+ };
26
+ e([a({ reflect: !0 })], o.prototype, "name", void 0), o = e([i("sken-layout-region")], o);
27
+ //#endregion
28
+ export { o as SkenLayoutRegion };
29
+
30
+ //# sourceMappingURL=sken-layout-region.js.map
@@ -0,0 +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 { LitElement, html, css } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\nimport type { SkenLayoutRegionRole } from '@sken-ds/contracts'\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"}
@@ -0,0 +1,205 @@
1
+ import { t as e } from "./decorate-D4FKzyDw.js";
2
+ import "./sken-layout-region.js";
3
+ import { LitElement as t, css as n, html as r, unsafeCSS as i } from "lit";
4
+ import { customElement as a, property as o } from "lit/decorators.js";
5
+ //#region src/components/sken-layout.ts
6
+ var s = {
7
+ sm: "12rem",
8
+ md: "16rem",
9
+ lg: "20rem"
10
+ }, c = class extends t {
11
+ constructor(...e) {
12
+ super(...e), this.variant = "header-content", this.topbarMode = "fixed", this.sidebarPosition = "start", this.sidebarSize = "md";
13
+ }
14
+ static {
15
+ this.styles = n`
16
+ /* ── The host ────────────────────────────────────────────────
17
+ The host owns the page-level grid. The body row is a
18
+ sub-grid; the topbar / primary-nav / footer sit in their
19
+ own rows. The shadow DOM owns the layout; the slot inside
20
+ projects the consumer's regions into the grid cells. */
21
+ :host {
22
+ display: grid;
23
+ block-size: 100dvh;
24
+ grid-template-columns: 1fr;
25
+ grid-template-rows: auto 1fr;
26
+ grid-template-areas:
27
+ 'topbar'
28
+ 'body';
29
+ background: var(--sken-background, Canvas);
30
+ color: var(--sken-foreground, CanvasText);
31
+ font-family: var(--sken-family-sans, system-ui, sans-serif);
32
+ }
33
+
34
+ .body {
35
+ display: grid;
36
+ grid-template-columns: 1fr;
37
+ grid-template-areas: 'content';
38
+ min-block-size: 0; /* allow children to shrink + scroll */
39
+ }
40
+
41
+ .body.with-sidebar {
42
+ grid-template-columns: var(--sken-layout-sidebar-width, 16rem) 1fr;
43
+ grid-template-areas: 'sidebar content';
44
+ }
45
+ .body.with-sidebar.sidebar-end {
46
+ grid-template-columns: 1fr var(--sken-layout-sidebar-width, 16rem);
47
+ grid-template-areas: 'content sidebar';
48
+ }
49
+
50
+ /* ── Per-region grid placement ───────────────────────────────
51
+ The light-DOM <sken-layout-region> children are projected
52
+ here through the unnamed <slot>. The grid-area rule per
53
+ name positions each one in the right cell. We select the
54
+ slotted custom elements by attribute, which works
55
+ transparently across the shadow boundary. */
56
+ ::slotted(sken-layout-region[name='topbar']) {
57
+ grid-area: topbar;
58
+ }
59
+ ::slotted(sken-layout-region[name='primary-nav']) {
60
+ grid-area: primary-nav;
61
+ }
62
+ ::slotted(sken-layout-region[name='sidebar']) {
63
+ grid-area: sidebar;
64
+ min-block-size: 0;
65
+ border-inline-end: 1px solid
66
+ var(--sken-border, color-mix(in srgb, currentColor 12%, transparent));
67
+ }
68
+ ::slotted(sken-layout-region[name='content']) {
69
+ grid-area: content;
70
+ min-block-size: 0;
71
+ }
72
+ ::slotted(sken-layout-region[name='footer']) {
73
+ grid-area: footer;
74
+ border-top: 1px solid
75
+ var(--sken-border, color-mix(in srgb, currentColor 12%, transparent));
76
+ }
77
+
78
+ /* ── Grid templates per variant ─────────────────────────────── */
79
+ :host([variant='full-bleed']) {
80
+ grid-template-areas: 'body';
81
+ grid-template-rows: 1fr;
82
+ }
83
+ :host([variant='header-content']) {
84
+ grid-template-areas:
85
+ 'topbar'
86
+ 'body';
87
+ grid-template-rows: auto 1fr;
88
+ }
89
+ :host([variant='sidebar-content']) {
90
+ grid-template-rows: 1fr;
91
+ grid-template-areas: 'body';
92
+ }
93
+ :host([variant='sidebar-content']) .body {
94
+ grid-template-columns: var(--sken-layout-sidebar-width, 16rem) 1fr;
95
+ grid-template-areas: 'sidebar content';
96
+ }
97
+ :host([variant='header-sidebar']) {
98
+ grid-template-rows: auto auto 1fr;
99
+ grid-template-areas:
100
+ 'topbar'
101
+ 'primary-nav'
102
+ 'body';
103
+ }
104
+ :host([variant='header-sidebar-footer']) {
105
+ grid-template-rows: auto auto 1fr auto;
106
+ grid-template-areas:
107
+ 'topbar'
108
+ 'primary-nav'
109
+ 'body'
110
+ 'footer';
111
+ }
112
+
113
+ /* ── Topbar mode ──────────────────────────────────────────────
114
+ 'fixed': the host is 100dvh and the body scrolls under the
115
+ topbar. This is the default for admin consoles.
116
+ 'auto': the topbar scrolls away with the page (marketing,
117
+ landing). */
118
+ :host([topbar-mode='auto']) {
119
+ block-size: auto;
120
+ min-block-size: 100dvh;
121
+ }
122
+
123
+ /* ── 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.) */
129
+ :host([sidebar-position='end']) ::slotted(sken-layout-region[name='sidebar']) {
130
+ border-inline-end: none;
131
+ border-inline-start: 1px solid
132
+ var(--sken-border, color-mix(in srgb, currentColor 12%, transparent));
133
+ }
134
+
135
+ /* ── Sidebar width override ─────────────────────────────────── */
136
+ :host([sidebar-size='sm']) {
137
+ --sken-layout-sidebar-width: ${i(s.sm)};
138
+ }
139
+ :host([sidebar-size='md']) {
140
+ --sken-layout-sidebar-width: ${i(s.md)};
141
+ }
142
+ :host([sidebar-size='lg']) {
143
+ --sken-layout-sidebar-width: ${i(s.lg)};
144
+ }
145
+
146
+ /* ── Variant-aware region hiding ─────────────────────────────
147
+ Roles the active variant does not include are hidden
148
+ outright. This way a consumer that always renders the
149
+ same set of regions (e.g. a shared layout component) does
150
+ not get phantom space when a page uses a different
151
+ variant. */
152
+ :host([variant='full-bleed']) ::slotted(sken-layout-region[name='topbar']),
153
+ :host([variant='full-bleed']) ::slotted(sken-layout-region[name='primary-nav']),
154
+ :host([variant='full-bleed']) ::slotted(sken-layout-region[name='sidebar']),
155
+ :host([variant='full-bleed']) ::slotted(sken-layout-region[name='footer']) {
156
+ display: none;
157
+ }
158
+
159
+ :host([variant='header-content']) ::slotted(sken-layout-region[name='primary-nav']),
160
+ :host([variant='header-content']) ::slotted(sken-layout-region[name='sidebar']),
161
+ :host([variant='header-content']) ::slotted(sken-layout-region[name='footer']) {
162
+ display: none;
163
+ }
164
+
165
+ :host([variant='sidebar-content']) ::slotted(sken-layout-region[name='topbar']),
166
+ :host([variant='sidebar-content']) ::slotted(sken-layout-region[name='primary-nav']),
167
+ :host([variant='sidebar-content']) ::slotted(sken-layout-region[name='footer']) {
168
+ display: none;
169
+ }
170
+
171
+ :host([variant='header-sidebar']) ::slotted(sken-layout-region[name='footer']) {
172
+ display: none;
173
+ }
174
+ `;
175
+ }
176
+ render() {
177
+ let e = this.#e(this.variant), t = [
178
+ "body",
179
+ e ? "with-sidebar" : "",
180
+ e && this.sidebarPosition === "end" ? "sidebar-end" : ""
181
+ ].filter(Boolean).join(" ");
182
+ return r`
183
+ <div class=${t} part="body">
184
+ <slot></slot>
185
+ </div>
186
+ `;
187
+ }
188
+ #e(e) {
189
+ return e === "sidebar-content" || e === "header-sidebar" || e === "header-sidebar-footer";
190
+ }
191
+ };
192
+ e([o({ reflect: !0 })], c.prototype, "variant", void 0), e([o({
193
+ attribute: "topbar-mode",
194
+ reflect: !0
195
+ })], c.prototype, "topbarMode", void 0), e([o({
196
+ attribute: "sidebar-position",
197
+ reflect: !0
198
+ })], c.prototype, "sidebarPosition", void 0), e([o({
199
+ attribute: "sidebar-size",
200
+ reflect: !0
201
+ })], c.prototype, "sidebarSize", void 0), c = e([a("sken-layout")], c);
202
+ //#endregion
203
+ export { c as SkenLayout };
204
+
205
+ //# sourceMappingURL=sken-layout.js.map
@@ -0,0 +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} 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@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\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 ───────────────────────────────\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 ::slotted(sken-layout-region[name='topbar']) {\n grid-area: topbar;\n }\n ::slotted(sken-layout-region[name='primary-nav']) {\n grid-area: primary-nav;\n }\n ::slotted(sken-layout-region[name='sidebar']) {\n grid-area: sidebar;\n min-block-size: 0;\n border-inline-end: 1px solid\n var(--sken-border, color-mix(in srgb, currentColor 12%, transparent));\n }\n ::slotted(sken-layout-region[name='content']) {\n grid-area: content;\n min-block-size: 0;\n }\n ::slotted(sken-layout-region[name='footer']) {\n grid-area: footer;\n border-top: 1px solid\n var(--sken-border, color-mix(in srgb, currentColor 12%, transparent));\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 The 'with-sidebar' class is applied via JS in render() so\n we can include the 'sidebar-end' modifier in the same\n selector chain. (You cannot combine the [sidebar-position]\n attribute with the '.with-sidebar' class in a single\n selector, because the class is conditional.) */\n :host([sidebar-position='end']) ::slotted(sken-layout-region[name='sidebar']) {\n border-inline-end: none;\n border-inline-start: 1px solid\n var(--sken-border, color-mix(in srgb, currentColor 12%, transparent));\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":";;;;;AAmDA,IAAM,IAAuD;CAC3D,IAAI;CACJ,IAAI;CACJ,IAAI;AACN,GAGa,IAAN,cAAyB,EAAW;;EAKoD,aAJnC,KAAA,UAAA,kBACgC,KAAA,aAAA,SAE7C,KAAA,kBAAA,SACgD,KAAA,cAAA;;;EAE7E,KAAA,SAAA,CAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qCA0HgB,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;AAhMG,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,GANvD,IAAA,EAAA,CAAA,EAAc,aAAa,CAAA,GAAA,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sken-ds/primitives",
3
- "version": "0.3.5",
3
+ "version": "0.3.7",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org",
6
6
  "access": "public"
@@ -31,6 +31,14 @@
31
31
  "types": "./dist/sken-input.d.ts",
32
32
  "import": "./dist/sken-input.js"
33
33
  },
34
+ "./sken-layout": {
35
+ "types": "./dist/sken-layout.d.ts",
36
+ "import": "./dist/sken-layout.js"
37
+ },
38
+ "./sken-layout-region": {
39
+ "types": "./dist/sken-layout-region.d.ts",
40
+ "import": "./dist/sken-layout-region.js"
41
+ },
34
42
  "./sken-number-input": {
35
43
  "types": "./dist/sken-number-input.d.ts",
36
44
  "import": "./dist/sken-number-input.js"
@@ -84,7 +92,7 @@
84
92
  "@tanstack/match-sorter-utils": "^9.0.0",
85
93
  "@tanstack/table-core": "^9.0.0",
86
94
  "lit": "^3.3.3",
87
- "@sken-ds/contracts": "0.3.1"
95
+ "@sken-ds/contracts": "0.3.7"
88
96
  },
89
97
  "devDependencies": {
90
98
  "@vitest/coverage-v8": "^4.1.10",