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.
- package/docs/LAYOUT.md +517 -0
- package/layers/core/app/composables/useScrollGuard.ts +2 -2
- package/layers/layout/app/assets/css/layout/grids.css +18 -18
- package/layers/layout/app/assets/css/layout/modes/fluid.css +100 -0
- package/layers/layout/app/assets/css/main.css +1 -0
- package/layers/layout/app/components/Layout/Container.vue +49 -0
- package/layers/layout/app/components/Layout/Grid/Debug.vue +18 -19
- package/layers/layout/app/components/Layout/Grid/Item.vue +43 -19
- package/layers/layout/app/components/Layout/Main.vue +36 -0
- package/layers/layout/app/components/Layout/Page/index.vue +69 -0
- package/layers/layout/app/components/Layout/Section/Grid.vue +49 -0
- package/layers/layout/app/components/Layout/Section/Sidebar.vue +72 -0
- package/layers/layout/app/components/Layout/Section/Stack.vue +63 -0
- package/layers/layout/app/composables/useGridConfig.ts +28 -1
- package/layers/layout/app/types/layouts.ts +42 -9
- package/layers/layout/app.config.ts +114 -20
- package/layers/motion/app/plugins/locomotive-scroll.client.ts +41 -21
- package/package.json +1 -1
|
@@ -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
|
+
}
|
|
@@ -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
|
-
//
|
|
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:
|
|
62
|
+
gridTemplateColumns: `repeat(${cols.value}, 1fr)`,
|
|
49
63
|
gap,
|
|
50
|
-
paddingInline: '
|
|
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
|
|
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 {
|
|
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?:
|
|
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'
|
|
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 ??
|
|
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,
|
|
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
|
-
|
|
118
|
-
if (bleed === 'both') {
|
|
121
|
+
if (props.bleed) {
|
|
122
|
+
if (props.bleed === 'both') {
|
|
119
123
|
styles.gridColumn = '1 / -1'
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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>
|