@sken-ds/primitives 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/sken-layout.js +25 -5
- package/dist/sken-layout.js.map +1 -1
- package/package.json +2 -2
package/dist/sken-layout.js
CHANGED
|
@@ -16,13 +16,33 @@ var c = {
|
|
|
16
16
|
footer: "var(--sken-muted)"
|
|
17
17
|
}, u = class extends t {
|
|
18
18
|
constructor(...e) {
|
|
19
|
-
super(...e), this.variant = "header-content", this.topbarMode = "fixed", this.sidebarPosition = "start", this.sidebarSize = "md", this.density = "none";
|
|
19
|
+
super(...e), this.variant = "header-content", this.topbarMode = "fixed", this.sidebarPosition = "start", this.sidebarSize = "md", this.density = "none", this.#e = null;
|
|
20
20
|
}
|
|
21
|
-
|
|
21
|
+
#e;
|
|
22
|
+
get #t() {
|
|
22
23
|
return s[this.variant];
|
|
23
24
|
}
|
|
24
|
-
get #
|
|
25
|
-
return this.#
|
|
25
|
+
get #n() {
|
|
26
|
+
return this.#t.includes("sidebar");
|
|
27
|
+
}
|
|
28
|
+
connectedCallback() {
|
|
29
|
+
super.connectedCallback(), this.#r(), this.#e = new MutationObserver(() => this.#r()), this.#e.observe(this, {
|
|
30
|
+
childList: !0,
|
|
31
|
+
subtree: !1
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
firstUpdated(e) {
|
|
35
|
+
super.firstUpdated(e), this.#r();
|
|
36
|
+
}
|
|
37
|
+
disconnectedCallback() {
|
|
38
|
+
super.disconnectedCallback(), this.#e?.disconnect(), this.#e = null;
|
|
39
|
+
}
|
|
40
|
+
#r() {
|
|
41
|
+
for (let e of Array.from(this.children)) {
|
|
42
|
+
if (!(e instanceof HTMLElement) || e.tagName.toLowerCase() !== "sken-layout-region") continue;
|
|
43
|
+
let t = e.getAttribute("name");
|
|
44
|
+
t && e.getAttribute("slot") !== t && e.setAttribute("slot", t);
|
|
45
|
+
}
|
|
26
46
|
}
|
|
27
47
|
static {
|
|
28
48
|
this.styles = n`
|
|
@@ -240,7 +260,7 @@ var c = {
|
|
|
240
260
|
`;
|
|
241
261
|
}
|
|
242
262
|
render() {
|
|
243
|
-
let e = this.#
|
|
263
|
+
let e = this.#t, t = this.#n, n = [
|
|
244
264
|
"body",
|
|
245
265
|
t ? "with-sidebar" : "",
|
|
246
266
|
t && this.sidebarPosition === "end" ? "sidebar-end" : ""
|
package/dist/sken-layout.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sken-layout.js","names":["#activeRoles","#hasSidebar"],"sources":["../src/components/sken-layout.ts"],"sourcesContent":["// ── <sken-layout> — structural layout primitive ────────────────────\n// Defines the page-level structure of a multi-region interface\n// (topbar, primary nav, sidebar, content, footer) without imposing\n// what lives inside each region. Composition by contract:\n//\n// <sken-layout variant=\"header-sidebar\">\n// <sken-layout-region name=\"topbar\">…</sken-layout-region>\n// <sken-layout-region name=\"primary-nav\">…</sken-layout-region>\n// <sken-layout-region name=\"sidebar\">…</sken-layout-region>\n// <sken-layout-region name=\"content\">…</sken-layout-region>\n// </sken-layout>\n//\n// Design notes (v0.3.10 — fix for SKEN-LAYOUT-REGIONS-RENDERING.md):\n// - The shadow root projects the consumer's light-DOM regions via\n// NAMED slots, one per role (topbar, primary-nav, sidebar,\n// content, footer). Each named slot sits inside a wrapper\n// element (e.g. <div class=\"topbar\">…</div>) that is the actual\n// grid item of the host's CSS grid. The wrapper is needed\n// because the consumer's <sken-layout-region> is `display:\n// contents` and would otherwise collapse the grid item away.\n// - Only the slots the active variant includes are rendered.\n// The single source of truth is SkenLayoutVariantRoles in\n// @sken-ds/contracts. Roles the variant does not include\n// simply do not appear in the shadow root, so the host's\n// grid template (which is fixed per variant) renders the\n// remaining rows at the height of their content.\n// - This is a behavioural fix over the previous design (single\n// unnamed slot + variant-aware `display: none` rules on\n// `::slotted()`), which placed every region inside the\n// `.body` sub-grid and therefore prevented the host's\n// `grid-template-areas` from placing them in the correct\n// row (the footer ended up next to the topbar in row 0).\n//\n// See ADR-0006 for the substrate decision. See the\n// @sken-ds/contracts types `SkenLayoutProps`, `SkenLayoutSlots`,\n// `SkenLayoutVariant`, `SkenLayoutVariantRoles` for the contract\n// surface.\n\nimport type {\n SkenLayoutDensity,\n SkenLayoutRegionRole,\n SkenLayoutSidebarPosition,\n SkenLayoutSidebarSize,\n SkenLayoutTopbarMode,\n SkenLayoutVariant,\n SkenLayoutVariantRolesMap,\n} from '@sken-ds/contracts'\nimport { SkenLayoutVariantRoles } from '@sken-ds/contracts'\nimport { LitElement, css, html, unsafeCSS } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\nimport './sken-layout-region.js'\n\n/**\n * Sidebar width in `rem` per size concept. The product can\n * override with `--sken-layout-sidebar-width` (CSS custom\n * property) if it needs a different value.\n */\nconst SIDEBAR_WIDTH: Record<SkenLayoutSidebarSize, string> = {\n sm: '12rem',\n md: '16rem',\n lg: '20rem',\n}\n\n/**\n * Per-cell background tokens. The values reference the\n * Sken color tokens, not raw colors, so a theme switch\n * (`data-theme=\"dark\"`) recolors every cell without the\n * primitive knowing about it.\n *\n * Mapping rationale:\n * - topbar is on `--sken-card` (white) because it is the\n * \"elevated\" chrome; the border-bottom separates it from\n * the page below.\n * - primary-nav is on `--sken-background` (page colour) so\n * it sits flush with the page; the border-bottom separates\n * it from the workspace.\n * - sidebar is on `--sken-muted` (subtle off-white) with a\n * border-inline-end; it is a \"rail\", not a \"page\".\n * - content is on `--sken-background`; no border (the\n * workspace is unbounded).\n * - footer is on `--sken-muted` with a border-top; it is\n * the same role as the sidebar (chrome).\n */\nconst REGION_BACKGROUND: Record<SkenLayoutRegionRole, string> = {\n topbar: 'var(--sken-card)',\n 'primary-nav': 'var(--sken-background)',\n sidebar: 'var(--sken-muted)',\n content: 'var(--sken-background)',\n footer: 'var(--sken-muted)',\n}\n\n@customElement('sken-layout')\nexport class SkenLayout extends LitElement {\n @property({ reflect: true }) variant: SkenLayoutVariant = 'header-content'\n @property({ attribute: 'topbar-mode', reflect: true }) topbarMode: SkenLayoutTopbarMode = 'fixed'\n @property({ attribute: 'sidebar-position', reflect: true })\n sidebarPosition: SkenLayoutSidebarPosition = 'start'\n @property({ attribute: 'sidebar-size', reflect: true }) sidebarSize: SkenLayoutSidebarSize = 'md'\n @property({ reflect: true }) density: SkenLayoutDensity = 'none'\n\n /**\n * Roles the active variant renders in the shadow root. Sourced\n * from {@link SkenLayoutVariantRoles}. Used to drive the\n * template (only the slots the variant includes are projected)\n * and to apply the per-variant `with-sidebar` class on the\n * body sub-grid wrapper.\n */\n get #activeRoles(): readonly SkenLayoutRegionRole[] {\n return (SkenLayoutVariantRoles as SkenLayoutVariantRolesMap)[this.variant]\n }\n\n /** True if the active variant renders a sidebar region. */\n get #hasSidebar(): boolean {\n return this.#activeRoles.includes('sidebar')\n }\n\n static styles = css`\n /* ── The host ────────────────────────────────────────────────\n The host owns the page-level grid. The topbar / primary-nav\n / footer sit in their own rows; the body is a sub-grid that\n holds the sidebar (when present) and the content. */\n :host {\n display: grid;\n block-size: 100dvh;\n grid-template-columns: 1fr;\n grid-template-rows: auto 1fr;\n grid-template-areas:\n 'topbar'\n 'body';\n background: var(--sken-background, Canvas);\n color: var(--sken-foreground, CanvasText);\n font-family: var(--sken-family-sans, system-ui, sans-serif);\n }\n\n /* ── Per-cell grid items ─────────────────────────────────────\n Each named slot sits inside a wrapper element that is the\n actual grid item. The wrapper's class matches the role\n (topbar, primary-nav, body, footer). The variant rules\n below assign the right 'grid-area' to each role. The\n 'display: contents' <sken-layout-region> is projected\n through the named <slot> and inherits the wrapper's\n background, border, and padding. */\n .topbar {\n grid-area: topbar;\n background: var(\n --sken-layout-region-topbar-background,\n ${unsafeCSS(REGION_BACKGROUND.topbar)}\n );\n padding-block: var(--sken-layout-region-topbar-padding-block, 0);\n padding-inline: var(--sken-layout-region-topbar-padding-inline, 0);\n border-bottom: 1px solid var(--sken-border);\n }\n .primary-nav {\n grid-area: primary-nav;\n background: var(\n --sken-layout-region-primary-nav-background,\n ${unsafeCSS(REGION_BACKGROUND['primary-nav'])}\n );\n padding-block: var(--sken-layout-region-primary-nav-padding-block, 0);\n padding-inline: var(--sken-layout-region-primary-nav-padding-inline, 0);\n border-bottom: 1px solid var(--sken-border);\n }\n .body {\n display: grid;\n grid-template-columns: 1fr;\n grid-template-areas: 'content';\n min-block-size: 0; /* allow children to shrink + scroll */\n }\n .body.with-sidebar {\n grid-template-columns: var(--sken-layout-sidebar-width, 16rem) 1fr;\n grid-template-areas: 'sidebar content';\n }\n .body.with-sidebar.sidebar-end {\n grid-template-columns: 1fr var(--sken-layout-sidebar-width, 16rem);\n grid-template-areas: 'content sidebar';\n }\n .sidebar {\n grid-area: sidebar;\n min-block-size: 0;\n background: var(\n --sken-layout-region-sidebar-background,\n ${unsafeCSS(REGION_BACKGROUND.sidebar)}\n );\n padding-block: var(--sken-layout-region-sidebar-padding-block, 0);\n padding-inline: var(--sken-layout-region-sidebar-padding-inline, 0);\n border-inline-end: 1px solid var(--sken-border);\n }\n .content {\n grid-area: content;\n min-block-size: 0;\n background: var(\n --sken-layout-region-content-background,\n ${unsafeCSS(REGION_BACKGROUND.content)}\n );\n padding-block: var(--sken-layout-region-content-padding-block, 0);\n padding-inline: var(--sken-layout-region-content-padding-inline, 0);\n }\n .footer {\n grid-area: footer;\n background: var(\n --sken-layout-region-footer-background,\n ${unsafeCSS(REGION_BACKGROUND.footer)}\n );\n padding-block: var(--sken-layout-region-footer-padding-block, 0);\n padding-inline: var(--sken-layout-region-footer-padding-inline, 0);\n border-top: 1px solid var(--sken-border);\n }\n\n /* ── Density: comfortable ─────────────────────────────────────\n When density=comfortable, the layout applies the Sken\n default rhythm: 8/16px on chrome cells, 16/24px on the\n content area, 12/16px on the footer. The values come\n from the spacing scale (--sken-2, --sken-3, --sken-4,\n --sken-6) so a single token change ripples through\n every cell. A consumer override on any\n --sken-layout-region-{role}-padding-{block|inline}\n always wins over the density preset. */\n :host([density='comfortable']) .topbar,\n :host([density='comfortable']) .primary-nav,\n :host([density='comfortable']) .content,\n :host([density='comfortable']) .sidebar,\n :host([density='comfortable']) .footer {\n --sken-layout-region-topbar-padding-block: var(--sken-2);\n --sken-layout-region-topbar-padding-inline: var(--sken-4);\n --sken-layout-region-primary-nav-padding-block: var(--sken-2);\n --sken-layout-region-primary-nav-padding-inline: var(--sken-4);\n --sken-layout-region-sidebar-padding-block: var(--sken-4);\n --sken-layout-region-sidebar-padding-inline: var(--sken-4);\n --sken-layout-region-content-padding-block: var(--sken-6);\n --sken-layout-region-content-padding-inline: var(--sken-6);\n --sken-layout-region-footer-padding-block: var(--sken-3);\n --sken-layout-region-footer-padding-inline: var(--sken-4);\n }\n\n /* ── Density: compact ────────────────────────────────────────\n When density=compact, every per-cell padding is halved\n from the comfortable value. We use calc(var(--sken-N) *\n 0.5) instead of hardcoded half-values so a future\n token change automatically cascades. As with\n comfortable, a consumer override on any\n --sken-layout-region-{role}-padding-{block|inline}\n always wins. */\n :host([density='compact']) .topbar,\n :host([density='compact']) .primary-nav,\n :host([density='compact']) .content,\n :host([density='compact']) .sidebar,\n :host([density='compact']) .footer {\n --sken-layout-region-topbar-padding-block: calc(var(--sken-2) * 0.5);\n --sken-layout-region-topbar-padding-inline: calc(var(--sken-4) * 0.5);\n --sken-layout-region-primary-nav-padding-block: calc(var(--sken-2) * 0.5);\n --sken-layout-region-primary-nav-padding-inline: calc(var(--sken-4) * 0.5);\n --sken-layout-region-sidebar-padding-block: calc(var(--sken-4) * 0.5);\n --sken-layout-region-sidebar-padding-inline: calc(var(--sken-4) * 0.5);\n --sken-layout-region-content-padding-block: calc(var(--sken-6) * 0.5);\n --sken-layout-region-content-padding-inline: calc(var(--sken-6) * 0.5);\n --sken-layout-region-footer-padding-block: calc(var(--sken-3) * 0.5);\n --sken-layout-region-footer-padding-inline: calc(var(--sken-4) * 0.5);\n }\n\n /* ── Grid templates per variant ───────────────────────────────\n Each variant declares which rows the host grid reserves.\n The default host grid is 2 rows (topbar + body); variants\n that include primary-nav or footer add more rows. Rows\n with no rendered content (e.g. footer on a variant that\n does not include a footer) collapse to 0 because the\n variant does not render the matching wrapper, and an\n empty 'auto' row in a grid takes the height of its\n content (which is 0). */\n :host([variant='full-bleed']) {\n grid-template-areas: 'body';\n grid-template-rows: 1fr;\n }\n :host([variant='header-content']) {\n grid-template-areas:\n 'topbar'\n 'body';\n grid-template-rows: auto 1fr;\n }\n :host([variant='sidebar-content']) {\n grid-template-rows: 1fr;\n grid-template-areas: 'body';\n }\n :host([variant='header-sidebar']) {\n grid-template-rows: auto auto 1fr;\n grid-template-areas:\n 'topbar'\n 'primary-nav'\n 'body';\n }\n :host([variant='header-sidebar-footer']) {\n grid-template-rows: auto auto 1fr auto;\n grid-template-areas:\n 'topbar'\n 'primary-nav'\n 'body'\n 'footer';\n }\n\n /* ── Topbar mode ──────────────────────────────────────────────\n 'fixed': the host is 100dvh and the body scrolls under the\n topbar. This is the default for admin consoles.\n 'auto': the topbar scrolls away with the page (marketing,\n landing). */\n :host([topbar-mode='auto']) {\n block-size: auto;\n min-block-size: 100dvh;\n }\n\n /* ── Sidebar position ─────────────────────────────────────────\n When the sidebar sits on the inline-end side, the border\n moves to the inline-start side. The default rule above\n already paints the border on the inline-end; here we\n swap it. */\n :host([sidebar-position='end']) .sidebar {\n border-inline-end: none;\n border-inline-start: 1px solid var(--sken-border);\n }\n\n /* ── Sidebar width override ─────────────────────────────────── */\n :host([sidebar-size='sm']) {\n --sken-layout-sidebar-width: ${unsafeCSS(SIDEBAR_WIDTH.sm)};\n }\n :host([sidebar-size='md']) {\n --sken-layout-sidebar-width: ${unsafeCSS(SIDEBAR_WIDTH.md)};\n }\n :host([sidebar-size='lg']) {\n --sken-layout-sidebar-width: ${unsafeCSS(SIDEBAR_WIDTH.lg)};\n }\n `\n\n override render() {\n const roles = this.#activeRoles\n const hasSidebar = this.#hasSidebar\n const bodyClasses = [\n 'body',\n hasSidebar ? 'with-sidebar' : '',\n hasSidebar && this.sidebarPosition === 'end' ? 'sidebar-end' : '',\n ]\n .filter(Boolean)\n .join(' ')\n\n return html`\n ${\n roles.includes('topbar')\n ? html`<div class=\"topbar\" part=\"topbar\">\n <slot name=\"topbar\"></slot>\n </div>`\n : ''\n }\n ${\n roles.includes('primary-nav')\n ? html`<div class=\"primary-nav\" part=\"primary-nav\">\n <slot name=\"primary-nav\"></slot>\n </div>`\n : ''\n }\n <div class=${bodyClasses} part=\"body\">\n ${\n hasSidebar\n ? html`<div class=\"sidebar\" part=\"sidebar\">\n <slot name=\"sidebar\"></slot>\n </div>`\n : ''\n }\n <div class=\"content\" part=\"content\">\n <slot name=\"content\"></slot>\n </div>\n </div>\n ${\n roles.includes('footer')\n ? html`<div class=\"footer\" part=\"footer\">\n <slot name=\"footer\"></slot>\n </div>`\n : ''\n }\n `\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'sken-layout': SkenLayout\n }\n}\n"],"mappings":";;;;;;AAyDA,IAAM,IAAuD;CAC3D,IAAI;CACJ,IAAI;CACJ,IAAI;AACN,GAsBM,IAA0D;CAC9D,QAAQ;CACR,eAAe;CACf,SAAS;CACT,SAAS;CACT,QAAQ;AACV,GAGa,IAAN,cAAyB,EAAW;;EAMiB,aALA,KAAA,UAAA,kBACgC,KAAA,aAAA,SAE7C,KAAA,kBAAA,SACgD,KAAA,cAAA,MACnC,KAAA,UAAA;;CAS1D,IAAIA,KAAgD;EAClD,OAAQ,EAAqD,KAAK;CACpE;CAGA,IAAIC,KAAuB;EACzB,OAAO,KAAKD,GAAa,SAAS,SAAS;CAC7C;;EAEgB,KAAA,SAAA,CAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UA8BX,EAAU,EAAkB,MAAM,EAAE;;;;;;;;;;UAUpC,EAAU,EAAkB,cAAc,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;UAyB5C,EAAU,EAAkB,OAAO,EAAE;;;;;;;;;;;UAWrC,EAAU,EAAkB,OAAO,EAAE;;;;;;;;;UASrC,EAAU,EAAkB,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qCAuHT,EAAU,EAAc,EAAE,EAAE;;;qCAG5B,EAAU,EAAc,EAAE,EAAE;;;qCAG5B,EAAU,EAAc,EAAE,EAAE;;;;CAI/D,SAAkB;EAChB,IAAM,IAAQ,KAAKA,IACb,IAAa,KAAKC,IAClB,IAAc;GAClB;GACA,IAAa,iBAAiB;GAC9B,KAAc,KAAK,oBAAoB,QAAQ,gBAAgB;EACjE,CAAC,CACE,OAAO,OAAO,CAAC,CACf,KAAK,GAAG;EAEX,OAAO,CAAI;QAEP,EAAM,SAAS,QAAQ,IACnB,CAAI;;sBAGJ,GACL;QAEC,EAAM,SAAS,aAAa,IACxB,CAAI;;sBAGJ,GACL;mBACY,EAAY;UAErB,IACI,CAAI;;wBAGJ,GACL;;;;;QAMD,EAAM,SAAS,QAAQ,IACnB,CAAI;;sBAGJ,GACL;;CAEL;AACF;AA5RG,EAAA,CAAA,EAAS,EAAE,SAAS,GAAK,CAAC,CAAA,GAAA,EAAA,WAAA,WAAA,KAAA,CAAA,GAC1B,EAAA,CAAA,EAAS;CAAE,WAAW;CAAe,SAAS;AAAK,CAAC,CAAA,GAAA,EAAA,WAAA,cAAA,KAAA,CAAA,GACpD,EAAA,CAAA,EAAS;CAAE,WAAW;CAAoB,SAAS;AAAK,CAAC,CAAA,GAAA,EAAA,WAAA,mBAAA,KAAA,CAAA,GAEzD,EAAA,CAAA,EAAS;CAAE,WAAW;CAAgB,SAAS;AAAK,CAAC,CAAA,GAAA,EAAA,WAAA,eAAA,KAAA,CAAA,GACrD,EAAA,CAAA,EAAS,EAAE,SAAS,GAAK,CAAC,CAAA,GAAA,EAAA,WAAA,WAAA,KAAA,CAAA,GAP5B,IAAA,EAAA,CAAA,EAAc,aAAa,CAAA,GAAA,CAAA"}
|
|
1
|
+
{"version":3,"file":"sken-layout.js","names":["#activeRoles","#hasSidebar","#syncRegionSlotAttributes","#regionSlotObserver"],"sources":["../src/components/sken-layout.ts"],"sourcesContent":["// ── <sken-layout> — structural layout primitive ────────────────────\n// Defines the page-level structure of a multi-region interface\n// (topbar, primary nav, sidebar, content, footer) without imposing\n// what lives inside each region. Composition by contract:\n//\n// <sken-layout variant=\"header-sidebar\">\n// <sken-layout-region name=\"topbar\">…</sken-layout-region>\n// <sken-layout-region name=\"primary-nav\">…</sken-layout-region>\n// <sken-layout-region name=\"sidebar\">…</sken-layout-region>\n// <sken-layout-region name=\"content\">…</sken-layout-region>\n// </sken-layout>\n//\n// Design notes (v0.3.10 — fix for SKEN-LAYOUT-REGIONS-RENDERING.md):\n// - The shadow root projects the consumer's light-DOM regions via\n// NAMED slots, one per role (topbar, primary-nav, sidebar,\n// content, footer). Each named slot sits inside a wrapper\n// element (e.g. <div class=\"topbar\">…</div>) that is the actual\n// grid item of the host's CSS grid. The wrapper is needed\n// because the consumer's <sken-layout-region> is `display:\n// contents` and would otherwise collapse the grid item away.\n// - Only the slots the active variant includes are rendered.\n// The single source of truth is SkenLayoutVariantRoles in\n// @sken-ds/contracts. Roles the variant does not include\n// simply do not appear in the shadow root, so the host's\n// grid template (which is fixed per variant) renders the\n// remaining rows at the height of their content.\n// - This is a behavioural fix over the previous design (single\n// unnamed slot + variant-aware `display: none` rules on\n// `::slotted()`), which placed every region inside the\n// `.body` sub-grid and therefore prevented the host's\n// `grid-template-areas` from placing them in the correct\n// row (the footer ended up next to the topbar in row 0).\n//\n// See ADR-0006 for the substrate decision. See the\n// @sken-ds/contracts types `SkenLayoutProps`, `SkenLayoutSlots`,\n// `SkenLayoutVariant`, `SkenLayoutVariantRoles` for the contract\n// surface.\n\nimport type {\n SkenLayoutDensity,\n SkenLayoutRegionRole,\n SkenLayoutSidebarPosition,\n SkenLayoutSidebarSize,\n SkenLayoutTopbarMode,\n SkenLayoutVariant,\n SkenLayoutVariantRolesMap,\n} from '@sken-ds/contracts'\nimport { SkenLayoutVariantRoles } from '@sken-ds/contracts'\nimport { LitElement, css, html, unsafeCSS, type PropertyValues } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\nimport './sken-layout-region.js'\n\n/**\n * Sidebar width in `rem` per size concept. The product can\n * override with `--sken-layout-sidebar-width` (CSS custom\n * property) if it needs a different value.\n */\nconst SIDEBAR_WIDTH: Record<SkenLayoutSidebarSize, string> = {\n sm: '12rem',\n md: '16rem',\n lg: '20rem',\n}\n\n/**\n * Per-cell background tokens. The values reference the\n * Sken color tokens, not raw colors, so a theme switch\n * (`data-theme=\"dark\"`) recolors every cell without the\n * primitive knowing about it.\n *\n * Mapping rationale:\n * - topbar is on `--sken-card` (white) because it is the\n * \"elevated\" chrome; the border-bottom separates it from\n * the page below.\n * - primary-nav is on `--sken-background` (page colour) so\n * it sits flush with the page; the border-bottom separates\n * it from the workspace.\n * - sidebar is on `--sken-muted` (subtle off-white) with a\n * border-inline-end; it is a \"rail\", not a \"page\".\n * - content is on `--sken-background`; no border (the\n * workspace is unbounded).\n * - footer is on `--sken-muted` with a border-top; it is\n * the same role as the sidebar (chrome).\n */\nconst REGION_BACKGROUND: Record<SkenLayoutRegionRole, string> = {\n topbar: 'var(--sken-card)',\n 'primary-nav': 'var(--sken-background)',\n sidebar: 'var(--sken-muted)',\n content: 'var(--sken-background)',\n footer: 'var(--sken-muted)',\n}\n\n@customElement('sken-layout')\nexport class SkenLayout extends LitElement {\n @property({ reflect: true }) variant: SkenLayoutVariant = 'header-content'\n @property({ attribute: 'topbar-mode', reflect: true }) topbarMode: SkenLayoutTopbarMode = 'fixed'\n @property({ attribute: 'sidebar-position', reflect: true })\n sidebarPosition: SkenLayoutSidebarPosition = 'start'\n @property({ attribute: 'sidebar-size', reflect: true }) sidebarSize: SkenLayoutSidebarSize = 'md'\n @property({ reflect: true }) density: SkenLayoutDensity = 'none'\n\n /**\n * MutationObserver that watches the host's light DOM for\n * `<sken-layout-region>` children. For each region, if the\n * consumer (or a framework adapter) did not set the HTML\n * `slot=\"<role>\"` attribute, we set it ourselves. This is\n * defense-in-depth: the host's shadow root uses 5 named\n * `<slot>` elements (one per role) and the browser's\n * slot-projection mechanism only projects a light-DOM child\n * into a named slot if the child carries the matching\n * `slot=\"<name>\"` attribute. Without it, every named slot\n * receives zero assigned elements and the regions render\n * as empty wrappers — the page looks almost empty.\n *\n * A future framework adapter (React, Svelte, plain HTML) can\n * either set the `slot` attribute on its own (the documented\n * contract) or rely on this observer to do it. Setting it\n * explicitly is still preferred for clarity, but the\n * observer is the safety net that prevents the silent\n * \"everything renders empty\" failure mode that v0.4.0\n * shipped with.\n */\n #regionSlotObserver: MutationObserver | null = null\n\n /**\n * Roles the active variant renders in the shadow root. Sourced\n * from {@link SkenLayoutVariantRoles}. Used to drive the\n * template (only the slots the variant includes are projected)\n * and to apply the per-variant `with-sidebar` class on the\n * body sub-grid wrapper.\n */\n get #activeRoles(): readonly SkenLayoutRegionRole[] {\n return (SkenLayoutVariantRoles as SkenLayoutVariantRolesMap)[this.variant]\n }\n\n /** True if the active variant renders a sidebar region. */\n get #hasSidebar(): boolean {\n return this.#activeRoles.includes('sidebar')\n }\n\n override connectedCallback(): void {\n super.connectedCallback()\n // Walk the current children synchronously (so the first\n // render after connection already has the slot= attribute\n // in place), then attach the observer to catch future\n // mutations (the consumer can add or remove regions\n // dynamically, e.g. when switching the active variant in\n // a shared layout component).\n this.#syncRegionSlotAttributes()\n this.#regionSlotObserver = new MutationObserver(() => this.#syncRegionSlotAttributes())\n this.#regionSlotObserver.observe(this, { childList: true, subtree: false })\n }\n\n /**\n * Lit lifecycle hook called after the first render. We use\n * it as a second-pass sync point because some regions may\n * have been added to the host before the host was\n * connected, with their `name` property set programmatically\n * (e.g. via the Vue adapter setting `region.name = 'topbar'`\n * before the region is upgraded to a custom element). The\n * `name` property is `reflect: true`, so the attribute is\n * written only after the region is connected and Lit runs\n * its first render — which is exactly what `firstUpdated`\n * gives us. Without this second pass, a region whose `name`\n * was set as a property (not as an attribute) would not\n * have its `slot` attribute set by the time the browser\n * tries to project it.\n */\n protected override firstUpdated(_changedProperties: PropertyValues): void {\n super.firstUpdated(_changedProperties)\n this.#syncRegionSlotAttributes()\n }\n\n override disconnectedCallback(): void {\n super.disconnectedCallback()\n this.#regionSlotObserver?.disconnect()\n this.#regionSlotObserver = null\n }\n\n /**\n * For each direct `<sken-layout-region>` child of the host,\n * set the `slot=\"<name>\"` HTML attribute if it is missing\n * or does not match the region's role. The browser's\n * slot-projection mechanism reads this attribute to project\n * the child into the matching named `<slot>` inside the\n * shadow root.\n *\n * We only touch direct children (not the entire subtree)\n * because the contract is that `<sken-layout-region>` lives\n * as a direct child of `<sken-layout>`; nested regions\n * are not a supported pattern.\n */\n #syncRegionSlotAttributes(): void {\n for (const child of Array.from(this.children)) {\n if (!(child instanceof HTMLElement)) continue\n if (child.tagName.toLowerCase() !== 'sken-layout-region') continue\n const role = child.getAttribute('name')\n if (!role) continue\n if (child.getAttribute('slot') === role) continue\n child.setAttribute('slot', role)\n }\n }\n\n static styles = css`\n /* ── The host ────────────────────────────────────────────────\n The host owns the page-level grid. The topbar / primary-nav\n / footer sit in their own rows; the body is a sub-grid that\n holds the sidebar (when present) and the content. */\n :host {\n display: grid;\n block-size: 100dvh;\n grid-template-columns: 1fr;\n grid-template-rows: auto 1fr;\n grid-template-areas:\n 'topbar'\n 'body';\n background: var(--sken-background, Canvas);\n color: var(--sken-foreground, CanvasText);\n font-family: var(--sken-family-sans, system-ui, sans-serif);\n }\n\n /* ── Per-cell grid items ─────────────────────────────────────\n Each named slot sits inside a wrapper element that is the\n actual grid item. The wrapper's class matches the role\n (topbar, primary-nav, body, footer). The variant rules\n below assign the right 'grid-area' to each role. The\n 'display: contents' <sken-layout-region> is projected\n through the named <slot> and inherits the wrapper's\n background, border, and padding. */\n .topbar {\n grid-area: topbar;\n background: var(\n --sken-layout-region-topbar-background,\n ${unsafeCSS(REGION_BACKGROUND.topbar)}\n );\n padding-block: var(--sken-layout-region-topbar-padding-block, 0);\n padding-inline: var(--sken-layout-region-topbar-padding-inline, 0);\n border-bottom: 1px solid var(--sken-border);\n }\n .primary-nav {\n grid-area: primary-nav;\n background: var(\n --sken-layout-region-primary-nav-background,\n ${unsafeCSS(REGION_BACKGROUND['primary-nav'])}\n );\n padding-block: var(--sken-layout-region-primary-nav-padding-block, 0);\n padding-inline: var(--sken-layout-region-primary-nav-padding-inline, 0);\n border-bottom: 1px solid var(--sken-border);\n }\n .body {\n display: grid;\n grid-template-columns: 1fr;\n grid-template-areas: 'content';\n min-block-size: 0; /* allow children to shrink + scroll */\n }\n .body.with-sidebar {\n grid-template-columns: var(--sken-layout-sidebar-width, 16rem) 1fr;\n grid-template-areas: 'sidebar content';\n }\n .body.with-sidebar.sidebar-end {\n grid-template-columns: 1fr var(--sken-layout-sidebar-width, 16rem);\n grid-template-areas: 'content sidebar';\n }\n .sidebar {\n grid-area: sidebar;\n min-block-size: 0;\n background: var(\n --sken-layout-region-sidebar-background,\n ${unsafeCSS(REGION_BACKGROUND.sidebar)}\n );\n padding-block: var(--sken-layout-region-sidebar-padding-block, 0);\n padding-inline: var(--sken-layout-region-sidebar-padding-inline, 0);\n border-inline-end: 1px solid var(--sken-border);\n }\n .content {\n grid-area: content;\n min-block-size: 0;\n background: var(\n --sken-layout-region-content-background,\n ${unsafeCSS(REGION_BACKGROUND.content)}\n );\n padding-block: var(--sken-layout-region-content-padding-block, 0);\n padding-inline: var(--sken-layout-region-content-padding-inline, 0);\n }\n .footer {\n grid-area: footer;\n background: var(\n --sken-layout-region-footer-background,\n ${unsafeCSS(REGION_BACKGROUND.footer)}\n );\n padding-block: var(--sken-layout-region-footer-padding-block, 0);\n padding-inline: var(--sken-layout-region-footer-padding-inline, 0);\n border-top: 1px solid var(--sken-border);\n }\n\n /* ── Density: comfortable ─────────────────────────────────────\n When density=comfortable, the layout applies the Sken\n default rhythm: 8/16px on chrome cells, 16/24px on the\n content area, 12/16px on the footer. The values come\n from the spacing scale (--sken-2, --sken-3, --sken-4,\n --sken-6) so a single token change ripples through\n every cell. A consumer override on any\n --sken-layout-region-{role}-padding-{block|inline}\n always wins over the density preset. */\n :host([density='comfortable']) .topbar,\n :host([density='comfortable']) .primary-nav,\n :host([density='comfortable']) .content,\n :host([density='comfortable']) .sidebar,\n :host([density='comfortable']) .footer {\n --sken-layout-region-topbar-padding-block: var(--sken-2);\n --sken-layout-region-topbar-padding-inline: var(--sken-4);\n --sken-layout-region-primary-nav-padding-block: var(--sken-2);\n --sken-layout-region-primary-nav-padding-inline: var(--sken-4);\n --sken-layout-region-sidebar-padding-block: var(--sken-4);\n --sken-layout-region-sidebar-padding-inline: var(--sken-4);\n --sken-layout-region-content-padding-block: var(--sken-6);\n --sken-layout-region-content-padding-inline: var(--sken-6);\n --sken-layout-region-footer-padding-block: var(--sken-3);\n --sken-layout-region-footer-padding-inline: var(--sken-4);\n }\n\n /* ── Density: compact ────────────────────────────────────────\n When density=compact, every per-cell padding is halved\n from the comfortable value. We use calc(var(--sken-N) *\n 0.5) instead of hardcoded half-values so a future\n token change automatically cascades. As with\n comfortable, a consumer override on any\n --sken-layout-region-{role}-padding-{block|inline}\n always wins. */\n :host([density='compact']) .topbar,\n :host([density='compact']) .primary-nav,\n :host([density='compact']) .content,\n :host([density='compact']) .sidebar,\n :host([density='compact']) .footer {\n --sken-layout-region-topbar-padding-block: calc(var(--sken-2) * 0.5);\n --sken-layout-region-topbar-padding-inline: calc(var(--sken-4) * 0.5);\n --sken-layout-region-primary-nav-padding-block: calc(var(--sken-2) * 0.5);\n --sken-layout-region-primary-nav-padding-inline: calc(var(--sken-4) * 0.5);\n --sken-layout-region-sidebar-padding-block: calc(var(--sken-4) * 0.5);\n --sken-layout-region-sidebar-padding-inline: calc(var(--sken-4) * 0.5);\n --sken-layout-region-content-padding-block: calc(var(--sken-6) * 0.5);\n --sken-layout-region-content-padding-inline: calc(var(--sken-6) * 0.5);\n --sken-layout-region-footer-padding-block: calc(var(--sken-3) * 0.5);\n --sken-layout-region-footer-padding-inline: calc(var(--sken-4) * 0.5);\n }\n\n /* ── Grid templates per variant ───────────────────────────────\n Each variant declares which rows the host grid reserves.\n The default host grid is 2 rows (topbar + body); variants\n that include primary-nav or footer add more rows. Rows\n with no rendered content (e.g. footer on a variant that\n does not include a footer) collapse to 0 because the\n variant does not render the matching wrapper, and an\n empty 'auto' row in a grid takes the height of its\n content (which is 0). */\n :host([variant='full-bleed']) {\n grid-template-areas: 'body';\n grid-template-rows: 1fr;\n }\n :host([variant='header-content']) {\n grid-template-areas:\n 'topbar'\n 'body';\n grid-template-rows: auto 1fr;\n }\n :host([variant='sidebar-content']) {\n grid-template-rows: 1fr;\n grid-template-areas: 'body';\n }\n :host([variant='header-sidebar']) {\n grid-template-rows: auto auto 1fr;\n grid-template-areas:\n 'topbar'\n 'primary-nav'\n 'body';\n }\n :host([variant='header-sidebar-footer']) {\n grid-template-rows: auto auto 1fr auto;\n grid-template-areas:\n 'topbar'\n 'primary-nav'\n 'body'\n 'footer';\n }\n\n /* ── Topbar mode ──────────────────────────────────────────────\n 'fixed': the host is 100dvh and the body scrolls under the\n topbar. This is the default for admin consoles.\n 'auto': the topbar scrolls away with the page (marketing,\n landing). */\n :host([topbar-mode='auto']) {\n block-size: auto;\n min-block-size: 100dvh;\n }\n\n /* ── Sidebar position ─────────────────────────────────────────\n When the sidebar sits on the inline-end side, the border\n moves to the inline-start side. The default rule above\n already paints the border on the inline-end; here we\n swap it. */\n :host([sidebar-position='end']) .sidebar {\n border-inline-end: none;\n border-inline-start: 1px solid var(--sken-border);\n }\n\n /* ── Sidebar width override ─────────────────────────────────── */\n :host([sidebar-size='sm']) {\n --sken-layout-sidebar-width: ${unsafeCSS(SIDEBAR_WIDTH.sm)};\n }\n :host([sidebar-size='md']) {\n --sken-layout-sidebar-width: ${unsafeCSS(SIDEBAR_WIDTH.md)};\n }\n :host([sidebar-size='lg']) {\n --sken-layout-sidebar-width: ${unsafeCSS(SIDEBAR_WIDTH.lg)};\n }\n `\n\n override render() {\n const roles = this.#activeRoles\n const hasSidebar = this.#hasSidebar\n const bodyClasses = [\n 'body',\n hasSidebar ? 'with-sidebar' : '',\n hasSidebar && this.sidebarPosition === 'end' ? 'sidebar-end' : '',\n ]\n .filter(Boolean)\n .join(' ')\n\n return html`\n ${\n roles.includes('topbar')\n ? html`<div class=\"topbar\" part=\"topbar\">\n <slot name=\"topbar\"></slot>\n </div>`\n : ''\n }\n ${\n roles.includes('primary-nav')\n ? html`<div class=\"primary-nav\" part=\"primary-nav\">\n <slot name=\"primary-nav\"></slot>\n </div>`\n : ''\n }\n <div class=${bodyClasses} part=\"body\">\n ${\n hasSidebar\n ? html`<div class=\"sidebar\" part=\"sidebar\">\n <slot name=\"sidebar\"></slot>\n </div>`\n : ''\n }\n <div class=\"content\" part=\"content\">\n <slot name=\"content\"></slot>\n </div>\n </div>\n ${\n roles.includes('footer')\n ? html`<div class=\"footer\" part=\"footer\">\n <slot name=\"footer\"></slot>\n </div>`\n : ''\n }\n `\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'sken-layout': SkenLayout\n }\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sken-ds/primitives",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"registry": "https://registry.npmjs.org",
|
|
6
6
|
"access": "public"
|
|
@@ -92,7 +92,7 @@
|
|
|
92
92
|
"@tanstack/match-sorter-utils": "^9.0.0",
|
|
93
93
|
"@tanstack/table-core": "^9.0.0",
|
|
94
94
|
"lit": "^3.3.3",
|
|
95
|
-
"@sken-ds/contracts": "0.4.
|
|
95
|
+
"@sken-ds/contracts": "0.4.1"
|
|
96
96
|
},
|
|
97
97
|
"devDependencies": {
|
|
98
98
|
"@vitest/coverage-v8": "^4.1.10",
|