kmcom-nuxt-layers 1.2.0 → 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.
@@ -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,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>
@@ -32,8 +32,8 @@ export type GridDensity = 'compact' | 'normal' | 'relaxed'
32
32
  export type GridMode = 'swiss' | 'fluid' | 'disabled'
33
33
 
34
34
  export interface GridPresetsItem {
35
- colStart: number | ResponsiveValue<number>
36
- colSpan: number | ResponsiveValue<number>
35
+ colStart?: number | ResponsiveValue<number>
36
+ colSpan: number | 'full' | ResponsiveValue<number>
37
37
  rowStart?: number | ResponsiveValue<number>
38
38
  rowSpan?: number
39
39
  /** Container size applied to the item's content */
@@ -42,15 +42,13 @@ export interface GridPresetsItem {
42
42
  gap?: string
43
43
  /** Vertical rhythm density for this preset */
44
44
  density?: GridDensity
45
+ /** Vertical alignment (align-self) */
46
+ align?: 'start' | 'center' | 'end' | 'stretch'
47
+ /** Horizontal alignment (justify-self) */
48
+ justify?: 'start' | 'center' | 'end' | 'stretch'
45
49
  }
46
50
 
47
- export interface GridPresets {
48
- hero: GridPresetsItem
49
- centered: GridPresetsItem
50
- fullWidth: GridPresetsItem
51
- sidebar: GridPresetsItem
52
- content: GridPresetsItem
53
- }
51
+ export type GridPresets = Record<string, GridPresetsItem>
54
52
 
55
53
  export interface GridConfig {
56
54
  /**
@@ -83,20 +83,36 @@ export default defineAppConfig({
83
83
 
84
84
  // Preset layouts for common patterns
85
85
  presets: {
86
- hero: {
86
+ // Full-viewport hero: full width, all 12 rows
87
+ hero: { colSpan: 'full', rowSpan: 12 },
88
+
89
+ // Centered content column (equal margins both sides)
90
+ // lg: 3 + 12 + 3 = 18 ✓ | md: 1 + 10 + 1 = 12 ✓
91
+ centered: {
87
92
  colStart: { default: 1, md: 2, lg: 4 },
88
93
  colSpan: { default: 6, md: 10, lg: 12 },
89
94
  rowSpan: 12,
90
95
  },
91
- centered: {
92
- colStart: { default: 1, md: 2, lg: 5 },
93
- colSpan: { default: 6, md: 10, lg: 10 },
94
- rowSpan: 12,
96
+
97
+ // Full width (all columns, no bleed)
98
+ fullWidth: { colSpan: 'full' },
99
+
100
+ // Narrow prose column (tighter reading width)
101
+ // lg: 4 + 10 + 4 = 18 ✓ | md: 2 + 8 + 2 = 12 ✓
102
+ prose: {
103
+ colStart: { default: 1, md: 3, lg: 5 },
104
+ colSpan: { default: 6, md: 8, lg: 10 },
95
105
  },
96
- fullWidth: {
97
- colStart: 1,
98
- colSpan: { default: 6, md: 12, lg: 18 },
106
+
107
+ // Wide content (generous width, minimal margins)
108
+ // lg: 1 + 16 + 1 = 18 ✓ | md: 1 + 10 + 1 = 12 ✓
109
+ wide: {
110
+ colStart: { default: 1, md: 2, lg: 2 },
111
+ colSpan: { default: 6, md: 10, lg: 16 },
99
112
  },
113
+
114
+ // Sidebar + content pair (use together)
115
+ // sidebar: cols 1-4 (lg) | content: cols 5-18 (14 cols) → 4+14=18 ✓
100
116
  sidebar: {
101
117
  colStart: { default: 1, md: 1, lg: 1 },
102
118
  colSpan: { default: 6, md: 4, lg: 4 },
@@ -106,7 +122,8 @@ export default defineAppConfig({
106
122
  colSpan: { default: 6, md: 8, lg: 14 },
107
123
  },
108
124
 
109
- // 50/50 vertical split (stacks on mobile)
125
+ // 50/50 vertical splits (stack on mobile)
126
+ // lg: 9+9=18 ✓ | md: 6+6=12 ✓
110
127
  splitLeft: {
111
128
  colStart: 1,
112
129
  colSpan: { default: 6, md: 6, lg: 9 },
@@ -118,7 +135,8 @@ export default defineAppConfig({
118
135
  rowSpan: 12,
119
136
  },
120
137
 
121
- // 25/75 split (stacks on mobile)
138
+ // 25/75 vertical splits (stack on mobile)
139
+ // lg: 5+13=18 ✓ | md: 3+9=12 ✓
122
140
  quarterLeft: {
123
141
  colStart: 1,
124
142
  colSpan: { default: 6, md: 3, lg: 5 },
@@ -130,18 +148,29 @@ export default defineAppConfig({
130
148
  rowSpan: 12,
131
149
  },
132
150
 
133
- // Horizontal 50/50 split (100vw × 50vh each)
134
- halfTop: {
151
+ // 75/25 vertical splits (stack on mobile)
152
+ // lg: 13+5=18 ✓ | md: 9+3=12 ✓
153
+ threeQuarterLeft: {
135
154
  colStart: 1,
136
- colSpan: { default: 6, md: 12, lg: 18 },
137
- rowStart: 1,
138
- rowSpan: 6,
155
+ colSpan: { default: 6, md: 9, lg: 13 },
156
+ rowSpan: 12,
139
157
  },
140
- halfBottom: {
141
- colStart: 1,
142
- colSpan: { default: 6, md: 12, lg: 18 },
143
- rowStart: 7,
144
- rowSpan: 6,
158
+ quarterRight: {
159
+ colStart: { default: 1, md: 10, lg: 14 },
160
+ colSpan: { default: 6, md: 3, lg: 5 },
161
+ rowSpan: 12,
162
+ },
163
+
164
+ // Horizontal 50/50 stacks (100vw × 50vh each)
165
+ halfTop: { colSpan: 'full', rowStart: 1, rowSpan: 6 },
166
+ halfBottom: { colSpan: 'full', rowStart: 7, rowSpan: 6 },
167
+
168
+ // Super centered: full-width, full-height, content perfectly centered
169
+ superCentered: {
170
+ colSpan: 'full',
171
+ rowSpan: 12,
172
+ align: 'center',
173
+ justify: 'center',
145
174
  },
146
175
  },
147
176
  },
@@ -46,17 +46,17 @@ export default defineNuxtPlugin(() => {
46
46
 
47
47
  const router = useRouter()
48
48
 
49
- // Only active on the locomotive-scroll route
49
+ const SMOOTH_SCROLL_ROUTES = ['/locomotive-scroll', '/layout-stacking', '/layout-blind-reveal']
50
+
50
51
  addRouteMiddleware((to, from) => {
51
- if (to.path === '/locomotive-scroll') {
52
+ if (SMOOTH_SCROLL_ROUTES.includes(to.path)) {
52
53
  nextTick(init)
53
- } else if (from?.path === '/locomotive-scroll') {
54
+ } else if (from?.path && SMOOTH_SCROLL_ROUTES.includes(from.path)) {
54
55
  destroy()
55
56
  }
56
57
  })
57
58
 
58
- // Activate immediately if already on that route (e.g. hard refresh)
59
- if (router.currentRoute.value.path === '/locomotive-scroll') {
59
+ if (SMOOTH_SCROLL_ROUTES.includes(router.currentRoute.value.path)) {
60
60
  init()
61
61
  }
62
62
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "kmcom-nuxt-layers",
3
3
  "private": false,
4
- "version": "1.2.0",
4
+ "version": "1.3.0",
5
5
  "description": "Composable Nuxt 4 layers for building scalable Vue applications",
6
6
  "files": [
7
7
  "layers/*/nuxt.config.ts",