kmcom-nuxt-layers 1.1.9 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Fluid Mode — Container-Query Based Grid Utilities
3
+ *
4
+ * These utilities complement .mastmain/.basesection and are safe to use
5
+ * alongside them — they only take effect when their class is applied.
6
+ *
7
+ * Design goals:
8
+ * - Auto-fit columns that respond to their *container* width, not the viewport
9
+ * - No fixed breakpoints — layout adapts continuously
10
+ * - Consistent gap matching the base grid gap custom property
11
+ */
12
+
13
+ /**
14
+ * .fluid-grid — auto-fit grid that fills its container.
15
+ *
16
+ * Columns shrink no smaller than --fluid-col-min (default 16rem / 256px).
17
+ * Use `--fluid-col-min` to tune the minimum column width per-instance.
18
+ *
19
+ * @example
20
+ * <div class="fluid-grid" style="--fluid-col-min: 20rem">…</div>
21
+ */
22
+ .fluid-grid {
23
+ --fluid-col-min: 16rem;
24
+ display: grid;
25
+ grid-template-columns: repeat(auto-fit, minmax(var(--fluid-col-min), 1fr));
26
+ gap: var(--grid-gap, clamp(0.75rem, 1.5vw, 1.5rem));
27
+ }
28
+
29
+ /**
30
+ * .fluid-grid-2 through .fluid-grid-4 — named column-count variants.
31
+ *
32
+ * These use container queries so the number of columns degrades gracefully
33
+ * when the container is too narrow.
34
+ */
35
+ .fluid-grid-2 {
36
+ --fluid-col-min: 14rem;
37
+ display: grid;
38
+ grid-template-columns: repeat(auto-fit, minmax(var(--fluid-col-min), 1fr));
39
+ gap: var(--grid-gap, clamp(0.75rem, 1.5vw, 1.5rem));
40
+
41
+ @container (width >= 30rem) {
42
+ grid-template-columns: repeat(2, 1fr);
43
+ }
44
+ }
45
+
46
+ .fluid-grid-3 {
47
+ --fluid-col-min: 14rem;
48
+ display: grid;
49
+ grid-template-columns: repeat(auto-fit, minmax(var(--fluid-col-min), 1fr));
50
+ gap: var(--grid-gap, clamp(0.75rem, 1.5vw, 1.5rem));
51
+
52
+ @container (width >= 44rem) {
53
+ grid-template-columns: repeat(3, 1fr);
54
+ }
55
+ }
56
+
57
+ .fluid-grid-4 {
58
+ --fluid-col-min: 12rem;
59
+ display: grid;
60
+ grid-template-columns: repeat(auto-fit, minmax(var(--fluid-col-min), 1fr));
61
+ gap: var(--grid-gap, clamp(0.75rem, 1.5vw, 1.5rem));
62
+
63
+ @container (width >= 52rem) {
64
+ grid-template-columns: repeat(4, 1fr);
65
+ }
66
+ }
67
+
68
+ /**
69
+ * LayoutContainer size utilities
70
+ *
71
+ * Used by the <LayoutContainer> component. Each class constrains the element
72
+ * width and centres it within its parent.
73
+ */
74
+ .layout-container-content {
75
+ max-width: 65ch;
76
+ width: 100%;
77
+ margin-inline: auto;
78
+ box-sizing: border-box;
79
+ }
80
+
81
+ .layout-container-wide {
82
+ max-width: 90rem;
83
+ width: 100%;
84
+ margin-inline: auto;
85
+ box-sizing: border-box;
86
+ }
87
+
88
+ .layout-container-fluid {
89
+ max-width: 100%;
90
+ width: 100%;
91
+ box-sizing: border-box;
92
+ }
93
+
94
+ .layout-container-full {
95
+ /* Escape parent padding to reach the viewport edge */
96
+ width: 100vw;
97
+ max-width: 100vw;
98
+ margin-inline: calc(var(--grid-padding, clamp(1rem, 2.5vw, 2rem)) * -1);
99
+ box-sizing: border-box;
100
+ }
@@ -1 +1,2 @@
1
1
  @import '#layers/layout/app/assets/css/layout/grids.css';
2
+ @import '#layers/layout/app/assets/css/layout/modes/fluid.css';
@@ -0,0 +1,49 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * LayoutContainer — constrained content width wrapper.
4
+ *
5
+ * Provides four standard container widths that sit within the grid:
6
+ *
7
+ * | size | max-width | use case |
8
+ * |---------|-----------|----------------------------------|
9
+ * | content | 65ch | Long-form prose, articles |
10
+ * | wide | 90rem | Cards, media-rich sections |
11
+ * | fluid | 100% | Full bleed within grid padding |
12
+ * | full | 100vw | True full-bleed (escapes padding)|
13
+ *
14
+ * The container is centred with `margin-inline: auto` for `content` and `wide`.
15
+ *
16
+ * @prop {GridContainerSize} size — Container width variant (default: 'wide')
17
+ * @prop {string} tag — HTML element to render (default: 'div')
18
+ *
19
+ * @example
20
+ * <LayoutContainer size="content">
21
+ * <p>Readable prose constrained to ~65 characters wide.</p>
22
+ * </LayoutContainer>
23
+ */
24
+
25
+ import type { GridContainerSize } from '#layers/layout/app/types/layouts'
26
+
27
+ interface Props {
28
+ size?: GridContainerSize
29
+ tag?: string
30
+ }
31
+
32
+ const { size = 'wide', tag = 'div' } = defineProps<Props>()
33
+
34
+ const sizeClass: Record<GridContainerSize, string> = {
35
+ content: 'layout-container-content',
36
+ wide: 'layout-container-wide',
37
+ fluid: 'layout-container-fluid',
38
+ full: 'layout-container-full',
39
+ }
40
+ </script>
41
+
42
+ <template>
43
+ <component
44
+ :is="tag"
45
+ :class="sizeClass[size]"
46
+ >
47
+ <slot />
48
+ </component>
49
+ </template>
@@ -34,22 +34,35 @@ const handleKeydown = (event: KeyboardEvent) => {
34
34
  }
35
35
  }
36
36
 
37
+ // Track column count in JS so v-for renders the correct number of divs.
38
+ // Mirrors the same breakpoints as mastmain (48rem = 768px, 80rem = 1280px).
39
+ const cols = ref(6)
40
+
41
+ const updateCols = () => {
42
+ if (window.matchMedia('(min-width: 80rem)').matches) cols.value = 18
43
+ else if (window.matchMedia('(min-width: 48rem)').matches) cols.value = 12
44
+ else cols.value = 6
45
+ }
46
+
37
47
  onMounted(() => {
48
+ updateCols()
49
+ window.addEventListener('resize', updateCols)
38
50
  window.addEventListener('keydown', handleKeydown)
39
51
  })
40
52
 
41
53
  onUnmounted(() => {
54
+ window.removeEventListener('resize', updateCols)
42
55
  window.removeEventListener('keydown', handleKeydown)
43
56
  })
44
57
 
45
- // Use CSS variables to match the responsive grid
58
+ // Do NOT set --grid-cols here as an inline style — that would override the
59
+ // CSS media queries in <style scoped> below. Let CSS own the variable.
46
60
  const style = computed(() => ({
47
61
  display: 'grid',
48
- gridTemplateColumns: 'repeat(var(--grid-cols, 6), 1fr)',
62
+ gridTemplateColumns: `repeat(${cols.value}, 1fr)`,
49
63
  gap,
50
- paddingInline: 'var(--grid-padding, clamp(1rem, 2.5vw, 2rem))',
64
+ paddingInline: 'clamp(1rem, 2.5vw, 2rem)',
51
65
  pointerEvents: 'none' as const,
52
- '--grid-cols': '6',
53
66
  }))
54
67
 
55
68
  defineExpose({ toggle })
@@ -58,22 +71,8 @@ defineExpose({ toggle })
58
71
  <template>
59
72
  <Teleport to="body">
60
73
  <div v-if="visible" :style class="grid-debug z-9999 fixed inset-0" aria-hidden="true">
61
- <div v-for="i in 18" :key="i" :style="{ backgroundColor: color }" class="h-full" />
74
+ <div v-for="i in cols" :key="i" :style="{ backgroundColor: color }" class="h-full" />
62
75
  </div>
63
76
  </Teleport>
64
77
  </template>
65
78
 
66
- <style scoped>
67
- /* Match responsive breakpoints from mastmain */
68
- @media (width >= 48rem) {
69
- .grid-debug {
70
- --grid-cols: 12;
71
- }
72
- }
73
-
74
- @media (width >= 80rem) {
75
- .grid-debug {
76
- --grid-cols: 18;
77
- }
78
- }
79
- </style>
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * @prop {string} as - HTML element to render (default: 'div')
9
9
  * @prop {number | ResponsiveValue} colStart - Starting column (1-18)
10
- * @prop {number | ResponsiveValue} colSpan - Number of columns to span (default: 1)
10
+ * @prop {ColSpanValue | ResponsiveValue} colSpan - Columns to span (default: 'full')
11
11
  * @prop {number | ResponsiveValue} rowStart - Starting row (1-12)
12
12
  * @prop {number | ResponsiveValue} rowSpan - Number of rows to span (default: 1)
13
13
  * @prop {Alignment} align - Vertical alignment (align-self): start, center, end, stretch
@@ -29,6 +29,7 @@
29
29
  * </BaseGridItem>
30
30
  */
31
31
 
32
+ type ColSpanValue = number | 'full'
32
33
  type Alignment = 'start' | 'center' | 'end' | 'stretch'
33
34
  type LayerName = 'back' | 'mid' | 'front' | 'top'
34
35
  type BleedDirection = 'left' | 'right' | 'both'
@@ -44,7 +45,7 @@ interface Props {
44
45
  preset?: string
45
46
  as?: string
46
47
  colStart?: number | ResponsiveValue<number>
47
- colSpan?: number | ResponsiveValue<number>
48
+ colSpan?: ColSpanValue | ResponsiveValue<number>
48
49
  rowStart?: number | ResponsiveValue<number>
49
50
  rowSpan?: number | ResponsiveValue<number>
50
51
  align?: Alignment
@@ -57,18 +58,22 @@ interface Props {
57
58
 
58
59
  const props = defineProps<Props>()
59
60
 
60
- const { as = 'div', preset, align, justify, z, layer, bleed, aspect } = props
61
+ const { as = 'div' } = props
61
62
 
62
63
  // Get preset configuration if preset prop is provided
63
64
  const { getPreset } = useGridConfig()
64
- const presetConfig = computed(() => (preset ? getPreset(preset) : undefined))
65
+ const presetConfig = computed(() => (props.preset ? getPreset(props.preset) : undefined))
65
66
 
66
67
  // Merge preset values with explicit props (explicit props take precedence)
67
68
  const colStart = computed(() => props.colStart ?? presetConfig.value?.colStart)
68
- const colSpan = computed(() => props.colSpan ?? presetConfig.value?.colSpan ?? 1)
69
+ const colSpan = computed(() => props.colSpan ?? presetConfig.value?.colSpan ?? 'full')
69
70
  const rowStart = computed(() => props.rowStart ?? presetConfig.value?.rowStart)
70
71
  const rowSpan = computed(() => props.rowSpan ?? presetConfig.value?.rowSpan ?? 1)
71
72
 
73
+ // Preset-aware alignment computed refs
74
+ const align = computed(() => props.align ?? presetConfig.value?.align)
75
+ const justify = computed(() => props.justify ?? presetConfig.value?.justify)
76
+
72
77
  const layerZIndex: Record<LayerName, number> = {
73
78
  back: 0,
74
79
  mid: 10,
@@ -109,20 +114,39 @@ const style = computed(() => {
109
114
  const styles: Record<string, string> = {}
110
115
 
111
116
  const colStartVal = getDefaultValue(colStart.value, undefined)
112
- const colSpanVal = getDefaultValue(colSpan.value, 1)
117
+ const colSpanVal = getDefaultValue(colSpan.value as ColSpanValue | ResponsiveValue<number> | undefined, 'full' as ColSpanValue)
113
118
  const rowStartVal = getDefaultValue(rowStart.value, undefined)
114
119
  const rowSpanVal = getDefaultValue(rowSpan.value, 1)
115
120
 
116
- if (bleed) {
117
- // Bleed is non-responsive — keep as direct inline style
118
- if (bleed === 'both') {
121
+ if (props.bleed) {
122
+ if (props.bleed === 'both') {
119
123
  styles.gridColumn = '1 / -1'
120
- } else if (bleed === 'left') {
121
- styles.gridColumn = `1 / span ${colSpanVal}`
122
- } else if (bleed === 'right') {
124
+ styles.marginInline = 'calc(-1 * var(--grid-padding))'
125
+ } else if (props.bleed === 'left') {
126
+ const spanNum = typeof colSpanVal === 'number' ? colSpanVal : undefined
127
+ styles.gridColumn = spanNum ? `1 / span ${spanNum}` : '1 / -1'
128
+ styles.marginInlineStart = 'calc(-1 * var(--grid-padding))'
129
+ } else if (props.bleed === 'right') {
123
130
  styles.gridColumn = `${colStartVal ?? 'auto'} / -1`
131
+ styles.marginInlineEnd = 'calc(-1 * var(--grid-padding))'
124
132
  }
125
133
  styles.gridRow = `${rowStartVal ?? 'auto'} / span ${rowSpanVal}`
134
+ } else if (colSpanVal === 'full') {
135
+ // 'full' span: use inline gridColumn directly (no CSS var approach needed)
136
+ styles.gridColumn = `${colStartVal ?? 1} / -1`
137
+ // Still set row vars for responsive row support
138
+ styles['--_rs'] = String(rowStartVal ?? 'auto')
139
+ styles['--_re'] = String(rowSpanVal)
140
+
141
+ const mdRowStart = getResponsiveValue(rowStart.value, 'md')
142
+ const lgRowStart = getResponsiveValue(rowStart.value, 'lg')
143
+ const mdRowSpan = getResponsiveValue(rowSpan.value, 'md')
144
+ const lgRowSpan = getResponsiveValue(rowSpan.value, 'lg')
145
+
146
+ if (mdRowStart !== undefined) styles['--_md-rs'] = String(mdRowStart)
147
+ if (lgRowStart !== undefined) styles['--_lg-rs'] = String(lgRowStart)
148
+ if (mdRowSpan !== undefined) styles['--_md-re'] = String(mdRowSpan)
149
+ if (lgRowSpan !== undefined) styles['--_lg-re'] = String(lgRowSpan)
126
150
  } else {
127
151
  // Set CSS custom properties instead of grid-column/grid-row directly.
128
152
  // The <style> block below reads these vars and applies them at each breakpoint,
@@ -134,8 +158,8 @@ const style = computed(() => {
134
158
 
135
159
  const mdColStart = getResponsiveValue(colStart.value, 'md')
136
160
  const lgColStart = getResponsiveValue(colStart.value, 'lg')
137
- const mdColSpan = getResponsiveValue(colSpan.value, 'md')
138
- const lgColSpan = getResponsiveValue(colSpan.value, 'lg')
161
+ const mdColSpan = getResponsiveValue(colSpan.value as ResponsiveValue<number> | undefined, 'md')
162
+ const lgColSpan = getResponsiveValue(colSpan.value as ResponsiveValue<number> | undefined, 'lg')
139
163
  const mdRowStart = getResponsiveValue(rowStart.value, 'md')
140
164
  const lgRowStart = getResponsiveValue(rowStart.value, 'lg')
141
165
  const mdRowSpan = getResponsiveValue(rowSpan.value, 'md')
@@ -152,15 +176,15 @@ const style = computed(() => {
152
176
  }
153
177
 
154
178
  // Content alignment
155
- if (align || justify) {
179
+ if (align.value || justify.value) {
156
180
  styles.display = 'grid'
157
181
  styles.width = '100%'
158
182
  styles.height = '100%'
159
- styles.placeItems = `${align ?? 'stretch'} ${justify ?? 'stretch'}`
183
+ styles.placeItems = `${align.value ?? 'stretch'} ${justify.value ?? 'stretch'}`
160
184
  }
161
185
 
162
186
  // Z-index
163
- const zIndex = z ?? (layer ? layerZIndex[layer] : undefined)
187
+ const zIndex = props.z ?? (props.layer ? layerZIndex[props.layer] : undefined)
164
188
  if (zIndex !== undefined) styles.zIndex = String(zIndex)
165
189
 
166
190
  return styles
@@ -169,8 +193,8 @@ const style = computed(() => {
169
193
  const classes = computed(() => {
170
194
  const classList: string[] = ['gi-placed', '@container', '@container/item']
171
195
 
172
- if (aspect) {
173
- classList.push(aspectClasses[aspect])
196
+ if (props.aspect) {
197
+ classList.push(aspectClasses[props.aspect])
174
198
  }
175
199
 
176
200
  return classList.join(' ')
@@ -0,0 +1,36 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * LayoutMain — grid root container applying the `mastmain` utility.
4
+ *
5
+ * Use this component when you need an explicit wrapper element that owns the
6
+ * Swiss Grid stacking context. The default layout can use it in place of a
7
+ * raw `<main class="mastmain">`.
8
+ *
9
+ * When `mode` is `'disabled'`, falls back to a plain semantic `<main>` so the
10
+ * page renders correctly without grid dependencies.
11
+ *
12
+ * @prop {string} tag — HTML element to render (default: 'main')
13
+ *
14
+ * @example
15
+ * <LayoutMain>
16
+ * <LayoutSection>…</LayoutSection>
17
+ * </LayoutMain>
18
+ */
19
+
20
+ interface Props {
21
+ tag?: string
22
+ }
23
+
24
+ const { tag = 'main' } = defineProps<Props>()
25
+
26
+ const { mode } = useGridConfig()
27
+ </script>
28
+
29
+ <template>
30
+ <component
31
+ :is="tag"
32
+ :class="mode !== 'disabled' ? 'mastmain' : undefined"
33
+ >
34
+ <slot />
35
+ </component>
36
+ </template>
@@ -0,0 +1,69 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * LayoutPage — canonical page wrapper for the Swiss Grid System.
4
+ *
5
+ * This is a fragment component (no wrapper element). The grid root is already
6
+ * provided by MastMain at the layout level via <div class="mastmain">.
7
+ * Adding another mastmain wrapper here would nest grids and misalign everything.
8
+ *
9
+ * Responsibilities:
10
+ * - SEO via useHead()
11
+ * - provides 'pageTitle' to child components
12
+ * - optional visible header (LayoutSection + LayoutPageHeader)
13
+ *
14
+ * LayoutGridDebug is owned by the default layout — do NOT add it here.
15
+ *
16
+ * @prop {string} title — Page title: sets <title> and optional visible heading
17
+ * @prop {string} description — Optional meta description for SEO
18
+ * @prop {boolean} showHeader — Render a LayoutPageHeader block (default: false)
19
+ *
20
+ * @example
21
+ * <LayoutPage title="Home">
22
+ * <LayoutSectionHero>
23
+ * <h1>Welcome</h1>
24
+ * </LayoutSectionHero>
25
+ * </LayoutPage>
26
+ *
27
+ * @example
28
+ * <LayoutPage title="About" description="Learn more." :show-header="true">
29
+ * <LayoutSection>
30
+ * <LayoutGridItem preset="centered">
31
+ * <p>Content here.</p>
32
+ * </LayoutGridItem>
33
+ * </LayoutSection>
34
+ * </LayoutPage>
35
+ */
36
+
37
+ interface Props {
38
+ title: string
39
+ description?: string
40
+ showHeader?: boolean
41
+ }
42
+
43
+ const { title, description, showHeader = false } = defineProps<Props>()
44
+
45
+ useHead({
46
+ title,
47
+ meta: description
48
+ ? [
49
+ { name: 'description', content: description },
50
+ { property: 'og:title', content: title },
51
+ { property: 'og:description', content: description },
52
+ ]
53
+ : undefined,
54
+ })
55
+
56
+ provide('pageTitle', title)
57
+ </script>
58
+
59
+ <template>
60
+ <!-- Optional visible page header — rendered as a grid section -->
61
+ <LayoutSection v-if="showHeader">
62
+ <LayoutGridItem preset="centered">
63
+ <LayoutPageHeader :title :description />
64
+ </LayoutGridItem>
65
+ </LayoutSection>
66
+
67
+ <!-- Page content — direct children of mastmain (via MastMain in the layout) -->
68
+ <slot />
69
+ </template>
@@ -0,0 +1,49 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * LayoutSectionGrid — RAM (Repeat Auto Minmax) pattern
4
+ *
5
+ * Auto-wrapping grid that fits as many columns as possible at `minItemWidth`
6
+ * without media queries. Uses `repeat(auto-fit, minmax(..., 1fr))`.
7
+ *
8
+ * @prop {string} minItemWidth - Minimum column width before wrapping (default: '200px')
9
+ * @prop {boolean} fullHeight - Force 100svh on the section (default: false)
10
+ *
11
+ * @slot default - Grid items
12
+ *
13
+ * @example
14
+ * <LayoutSectionGrid min-item-width="250px">
15
+ * <div>Card 1</div>
16
+ * <div>Card 2</div>
17
+ * <div>Card 3</div>
18
+ * </LayoutSectionGrid>
19
+ */
20
+
21
+ interface Props {
22
+ minItemWidth?: string
23
+ fullHeight?: boolean
24
+ }
25
+
26
+ const { minItemWidth = '200px', fullHeight = false } = defineProps<Props>()
27
+ </script>
28
+
29
+ <template>
30
+ <LayoutSection :full-height>
31
+ <LayoutGridItem>
32
+ <div
33
+ class="ram-grid"
34
+ :style="{ '--min-item-width': minItemWidth }"
35
+ >
36
+ <slot />
37
+ </div>
38
+ </LayoutGridItem>
39
+ </LayoutSection>
40
+ </template>
41
+
42
+ <style scoped>
43
+ .ram-grid {
44
+ display: grid;
45
+ grid-template-columns: repeat(auto-fit, minmax(var(--min-item-width, 200px), 1fr));
46
+ gap: var(--grid-gap, clamp(0.75rem, 1.5vw, 1.5rem));
47
+ width: 100%;
48
+ }
49
+ </style>
@@ -0,0 +1,72 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * LayoutSectionSidebar — Sidebar Says layout
4
+ *
5
+ * Two-column layout where the sidebar takes a clamped width and main content
6
+ * fills the rest. Uses `minmax(min, max) 1fr` (or reversed for right sidebar).
7
+ *
8
+ * @prop {string} sidebarMin - Minimum sidebar width (default: '150px')
9
+ * @prop {string} sidebarMax - Maximum sidebar width (default: '25%')
10
+ * @prop {boolean} reverse - Put sidebar on the right (default: false)
11
+ * @prop {boolean} fullHeight - Force 100svh on the section (default: false)
12
+ *
13
+ * @slot sidebar - Sidebar content
14
+ * @slot default - Main content (fills remaining space)
15
+ *
16
+ * @example
17
+ * <LayoutSectionSidebar sidebar-max="20rem">
18
+ * <template #sidebar><nav>...</nav></template>
19
+ * <article>Main content</article>
20
+ * </LayoutSectionSidebar>
21
+ */
22
+
23
+ interface Props {
24
+ sidebarMin?: string
25
+ sidebarMax?: string
26
+ reverse?: boolean
27
+ fullHeight?: boolean
28
+ }
29
+
30
+ const { sidebarMin = '150px', sidebarMax = '25%', reverse = false, fullHeight = false } = defineProps<Props>()
31
+ </script>
32
+
33
+ <template>
34
+ <LayoutSection :full-height>
35
+ <LayoutGridItem>
36
+ <div
37
+ class="sidebar-inner"
38
+ :class="{ 'sidebar-reverse': reverse }"
39
+ :style="{ '--sidebar-min': sidebarMin, '--sidebar-max': sidebarMax }"
40
+ >
41
+ <div class="sidebar-aside">
42
+ <slot name="sidebar" />
43
+ </div>
44
+ <div class="sidebar-main">
45
+ <slot />
46
+ </div>
47
+ </div>
48
+ </LayoutGridItem>
49
+ </LayoutSection>
50
+ </template>
51
+
52
+ <style scoped>
53
+ .sidebar-inner {
54
+ display: grid;
55
+ grid-template-columns: minmax(var(--sidebar-min, 150px), var(--sidebar-max, 25%)) 1fr;
56
+ gap: var(--grid-gap, clamp(0.75rem, 1.5vw, 1.5rem));
57
+ width: 100%;
58
+ height: 100%;
59
+ }
60
+
61
+ .sidebar-inner.sidebar-reverse {
62
+ grid-template-columns: 1fr minmax(var(--sidebar-min, 150px), var(--sidebar-max, 25%));
63
+ }
64
+
65
+ .sidebar-inner.sidebar-reverse .sidebar-aside {
66
+ order: 2;
67
+ }
68
+
69
+ .sidebar-inner.sidebar-reverse .sidebar-main {
70
+ order: 1;
71
+ }
72
+ </style>
@@ -0,0 +1,63 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * LayoutSectionStack — Pancake Stack layout
4
+ *
5
+ * Full-section wrapper using `auto 1fr auto` row template. Header and footer
6
+ * take natural height; the default slot fills the remaining space.
7
+ *
8
+ * @prop {boolean} fullHeight - Force 100svh on the section (default: true)
9
+ *
10
+ * @slot header - Top-pinned content (auto height)
11
+ * @slot default - Main content area (fills remaining space)
12
+ * @slot footer - Bottom-pinned content (auto height)
13
+ *
14
+ * @example
15
+ * <LayoutSectionStack>
16
+ * <template #header><nav>...</nav></template>
17
+ * <main>Content</main>
18
+ * <template #footer><footer>...</footer></template>
19
+ * </LayoutSectionStack>
20
+ */
21
+
22
+ interface Props {
23
+ fullHeight?: boolean
24
+ }
25
+
26
+ const { fullHeight = true } = defineProps<Props>()
27
+ </script>
28
+
29
+ <template>
30
+ <LayoutSection :full-height>
31
+ <LayoutGridItem>
32
+ <div class="stack-inner">
33
+ <div v-if="$slots.header" class="stack-header">
34
+ <slot name="header" />
35
+ </div>
36
+ <div class="stack-main">
37
+ <slot />
38
+ </div>
39
+ <div v-if="$slots.footer" class="stack-footer">
40
+ <slot name="footer" />
41
+ </div>
42
+ </div>
43
+ </LayoutGridItem>
44
+ </LayoutSection>
45
+ </template>
46
+
47
+ <style scoped>
48
+ .stack-inner {
49
+ display: grid;
50
+ grid-template-rows: auto 1fr auto;
51
+ height: 100%;
52
+ width: 100%;
53
+ }
54
+
55
+ .stack-header,
56
+ .stack-footer {
57
+ /* auto — natural height */
58
+ }
59
+
60
+ .stack-main {
61
+ /* 1fr — grows to fill remaining space */
62
+ }
63
+ </style>