kmcom-nuxt-layers 2.2.12 → 2.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.
Files changed (102) hide show
  1. package/docs/FEEDS.md +1 -2
  2. package/layers/animations/app/composables/useMagneticElement.ts +11 -9
  3. package/layers/animations/app/composables/useTiltEffect.ts +11 -9
  4. package/layers/animations/app/utils/pointerMotion.ts +31 -0
  5. package/layers/canvas/app/components/ShaderCanvas.vue +2 -2
  6. package/layers/content/app/composables/useCollectionItems.ts +28 -0
  7. package/layers/content/app/composables/useGalleryItems.ts +8 -14
  8. package/layers/content/app/composables/usePortfolioItems.ts +10 -18
  9. package/layers/core/app/composables/useBrowser.ts +9 -82
  10. package/layers/core/app/composables/useFeatures.ts +3 -27
  11. package/layers/core/app/plugins/init.ts +157 -135
  12. package/layers/core/app/utils/browserInfo.ts +115 -0
  13. package/layers/core/app/utils/featureClasses.ts +40 -0
  14. package/layers/core/app/utils/helpers.test.ts +51 -0
  15. package/layers/feeds/app/components/Feeds/Index.vue +1 -1
  16. package/layers/feeds/app/components/Feeds/RouteCard.vue +3 -9
  17. package/layers/feeds/app/utils/feed-catalog.ts +9 -4
  18. package/layers/feeds/nuxt.config.ts +0 -1
  19. package/layers/feeds/server/utils/content-adapter.test.ts +68 -0
  20. package/layers/feeds/server/utils/content-adapter.ts +2 -22
  21. package/layers/feeds/server/utils/feed-author.ts +32 -0
  22. package/layers/feeds/server/utils/feed-config.ts +88 -0
  23. package/layers/feeds/server/utils/feed-service.ts +11 -30
  24. package/layers/feeds/server/utils/feed-xml.ts +26 -0
  25. package/layers/feeds/server/utils/formats/rss.ts +10 -15
  26. package/layers/feeds/server/utils/formats.test.ts +71 -0
  27. package/layers/forms/app/components/Form/Field.vue +42 -30
  28. package/layers/forms/app/utils/fieldProps.ts +65 -0
  29. package/layers/layout/app/components/Layout/Grid/Item.vue +29 -146
  30. package/layers/layout/app/utils/gridPlacementStyle.ts +195 -0
  31. package/layers/mailer/app/types/mailer.ts +7 -25
  32. package/layers/mailer/server/utils/email.ts +28 -13
  33. package/layers/mailer/server/utils/hooks.ts +1 -20
  34. package/layers/navigation/app/composables/useSite.ts +2 -9
  35. package/layers/navigation/app/utils/site.ts +26 -0
  36. package/layers/routing/app/utils/resolveRoute.test.ts +47 -0
  37. package/layers/routing/app/utils/resolveRoute.ts +19 -10
  38. package/layers/scripts/app/composables/useAnalytics.ts +8 -41
  39. package/layers/scripts/app/composables/useGtm.ts +6 -13
  40. package/layers/scripts/app/utils/scriptClients.ts +70 -0
  41. package/layers/scroll/app/composables/useSmoothScroll.ts +9 -43
  42. package/layers/scroll/app/utils/scroll.ts +103 -0
  43. package/layers/seo/app/composables/useSeoConfig.ts +3 -9
  44. package/layers/seo/app/utils/seoConfig.ts +38 -0
  45. package/layers/shader/app/components/Material/AmbientAurora.client.vue +11 -33
  46. package/layers/shader/app/components/Material/AmbientFlow.client.vue +10 -37
  47. package/layers/shader/app/components/Material/AmbientGradientMesh.client.vue +10 -37
  48. package/layers/shader/app/components/Material/AmbientNebula.client.vue +12 -37
  49. package/layers/shader/app/components/Material/AmbientOcean.client.vue +9 -33
  50. package/layers/shader/app/components/Material/Gradient.client.vue +25 -46
  51. package/layers/shader/app/components/Material/Image.client.vue +10 -55
  52. package/layers/shader/app/components/Material/Node.client.vue +18 -5
  53. package/layers/shader/app/components/Material/Noise.client.vue +9 -43
  54. package/layers/shader/app/components/Preset/ThemeBubble.client.vue +2 -1
  55. package/layers/shader/app/components/Preset/ThemeFlow.client.vue +2 -1
  56. package/layers/shader/app/components/Preset/ThemeGradient.client.vue +2 -1
  57. package/layers/shader/app/components/Preset/ThemeLavaLamp.client.vue +2 -1
  58. package/layers/shader/app/components/Preset/ThemePlasma.client.vue +2 -1
  59. package/layers/shader/app/components/Preset/ThemeWave.client.vue +2 -1
  60. package/layers/shader/app/components/Shader/Background.client.vue +44 -24
  61. package/layers/shader/app/composables/useAmbientMaterials.ts +5 -1
  62. package/layers/shader/app/composables/useShader.ts +38 -23
  63. package/layers/shader/app/composables/useShaderGraph.ts +11 -6
  64. package/layers/shader/app/composables/useShaderMixBlend.ts +4 -4
  65. package/layers/shader/app/composables/useShaderRuntime.ts +0 -1
  66. package/layers/shader/app/composables/useShaderVec2.ts +2 -4
  67. package/layers/shader/app/composables/useThemePreset.ts +34 -8
  68. package/layers/shader/app/composables/useUniformWatchers.ts +15 -0
  69. package/layers/shader/app/composables/useUniforms.ts +0 -1
  70. package/layers/shader/app/shaders/common/blend.ts +4 -4
  71. package/layers/shader/app/shaders/common/effects.ts +38 -21
  72. package/layers/shader/app/shaders/common/grain.ts +46 -49
  73. package/layers/shader/app/shaders/common/lighting.ts +17 -15
  74. package/layers/shader/app/shaders/common/math.ts +2 -4
  75. package/layers/shader/app/shaders/common/nodes.ts +17 -0
  76. package/layers/shader/app/shaders/common/palette.ts +21 -11
  77. package/layers/shader/app/shaders/common/patterns.ts +25 -14
  78. package/layers/shader/app/shaders/common/shapes.ts +97 -88
  79. package/layers/shader/app/shaders/common/uv.ts +33 -34
  80. package/layers/shader/app/shaders/createMaterial.ts +92 -78
  81. package/layers/shader/app/shaders/layers/paperShading.ts +22 -10
  82. package/layers/shader/app/shaders/layers/shaderGradient.ts +46 -21
  83. package/layers/shader/app/utils/tsl/tween.ts +2 -4
  84. package/layers/shader/package.json +5 -1
  85. package/layers/starter/app/components/StarterDesignSystem.vue +1913 -0
  86. package/layers/starter/app/components/StarterHome.vue +407 -0
  87. package/layers/starter/nuxt.config.ts +15 -0
  88. package/layers/starter/package.json +10 -0
  89. package/layers/theme/app/components/ThemePicker/Menu.vue +3 -25
  90. package/layers/theme/app/composables/useThemePreferenceModels.ts +39 -0
  91. package/layers/theme/server/plugins/theme-fouc.ts +1 -92
  92. package/layers/theme/server/utils/accent-css.ts +75 -0
  93. package/layers/typography/app/composables/typography.ts +3 -7
  94. package/layers/visual/app/composables/accent.ts +2 -9
  95. package/layers/visual/app/composables/gradient.ts +33 -46
  96. package/layers/visual/app/composables/picture.ts +2 -79
  97. package/layers/visual/app/utils/colorTokens.ts +23 -0
  98. package/layers/visual/app/utils/gradientStyle.ts +41 -0
  99. package/layers/visual/app/utils/responsiveSizes.ts +49 -0
  100. package/package.json +17 -5
  101. package/layers/feeds/app/utils/feed-catalog.test.ts +0 -71
  102. package/layers/feeds/server/routes/feed/discovery.get.ts +0 -31
@@ -0,0 +1,407 @@
1
+ <script setup lang="ts">
2
+ useSeoMeta({
3
+ title: 'kmcom-nuxt-layers Starter',
4
+ description:
5
+ 'A production-ready Nuxt starter powered by kmcom-nuxt-layers. Composable, modular, and ready to ship.',
6
+ })
7
+
8
+ const { gsap, ScrollTrigger } = useGsap()
9
+ gsap.registerPlugin(ScrollTrigger)
10
+
11
+ const heroRef = ref<HTMLElement | null>(null)
12
+ const sectionsRef = ref<HTMLElement | null>(null)
13
+
14
+ onMounted(() => {
15
+ if (heroRef.value) {
16
+ gsap.from(Array.from(heroRef.value.children), {
17
+ y: 24,
18
+ opacity: 0,
19
+ duration: 0.7,
20
+ stagger: 0.1,
21
+ ease: 'power3.out',
22
+ })
23
+ }
24
+
25
+ if (sectionsRef.value) {
26
+ gsap.from(sectionsRef.value.querySelectorAll('.layer-card'), {
27
+ scrollTrigger: {
28
+ trigger: sectionsRef.value,
29
+ start: 'top 85%',
30
+ toggleActions: 'play none none none',
31
+ },
32
+ y: 32,
33
+ opacity: 0,
34
+ scale: 0.97,
35
+ duration: 0.5,
36
+ stagger: { amount: 0.5 },
37
+ ease: 'power3.out',
38
+ })
39
+ }
40
+ })
41
+
42
+ const appConfig = useAppConfig()
43
+ const activeLayers = appConfig.layers as Record<string, boolean>
44
+
45
+ type LayerDef = {
46
+ name: string
47
+ key: string
48
+ description: string
49
+ icon: string
50
+ features: string[]
51
+ borderColor: string
52
+ bgColor: string
53
+ iconBg: string
54
+ iconColor: string
55
+ }
56
+
57
+ const coreLayers: LayerDef[] = [
58
+ {
59
+ name: 'Core',
60
+ key: 'core',
61
+ description: 'Foundation utilities, error handling, device detection, PWA support',
62
+ icon: 'i-lucide-box',
63
+ features: ['Browser Detection', 'Screen Info', 'Network Status', 'Loading States', 'PWA'],
64
+ borderColor: 'border-blue-500/30',
65
+ bgColor: 'bg-blue-500/5',
66
+ iconBg: 'bg-blue-500/10',
67
+ iconColor: 'text-blue-500',
68
+ },
69
+ {
70
+ name: 'UI',
71
+ key: 'ui',
72
+ description: 'Typography, navigation, visual primitives and the design system',
73
+ icon: 'i-lucide-palette',
74
+ features: ['Typography', 'Color Tokens', 'Navigation', 'Visual Primitives', 'Toast', 'Modal'],
75
+ borderColor: 'border-pink-500/30',
76
+ bgColor: 'bg-pink-500/5',
77
+ iconBg: 'bg-pink-500/10',
78
+ iconColor: 'text-pink-500',
79
+ },
80
+ {
81
+ name: 'Layout',
82
+ key: 'layout',
83
+ description: 'Swiss Grid system, sections, and page containers',
84
+ icon: 'i-lucide-layout',
85
+ features: ['18-Col Grid', 'Hero Section', 'Split Section', 'Gallery Section', 'Grid Debug'],
86
+ borderColor: 'border-amber-500/30',
87
+ bgColor: 'bg-amber-500/5',
88
+ iconBg: 'bg-amber-500/10',
89
+ iconColor: 'text-amber-500',
90
+ },
91
+ {
92
+ name: 'Motion',
93
+ key: 'motion',
94
+ description: 'GSAP, Lenis smooth scroll, page transitions and animation effects',
95
+ icon: 'i-lucide-sparkles',
96
+ features: ['GSAP', 'ScrollTrigger', 'Lenis', 'Marquee', 'Magnetic', 'Tilt', 'Cursor'],
97
+ borderColor: 'border-emerald-500/30',
98
+ bgColor: 'bg-emerald-500/5',
99
+ iconBg: 'bg-emerald-500/10',
100
+ iconColor: 'text-emerald-500',
101
+ },
102
+ {
103
+ name: 'Forms',
104
+ key: 'forms',
105
+ description: 'Config-driven form fields with Zod validation and type inference',
106
+ icon: 'i-lucide-file-input',
107
+ features: ['Dynamic Fields', 'Zod Validation', 'Type Inference', 'Auto Icons', 'Contact Form'],
108
+ borderColor: 'border-cyan-500/30',
109
+ bgColor: 'bg-cyan-500/5',
110
+ iconBg: 'bg-cyan-500/10',
111
+ iconColor: 'text-cyan-500',
112
+ },
113
+ {
114
+ name: 'Theme',
115
+ key: 'theme',
116
+ description: 'Dark mode, accent color palettes and design token system',
117
+ icon: 'i-lucide-swatch-book',
118
+ features: ['Dark Mode', 'Accent Colors', 'Color Tokens', 'Color Mode'],
119
+ borderColor: 'border-neutral-400/30',
120
+ bgColor: 'bg-neutral-400/5',
121
+ iconBg: 'bg-neutral-400/10',
122
+ iconColor: 'text-neutral-400',
123
+ },
124
+ {
125
+ name: 'Content',
126
+ key: 'content',
127
+ description: 'Markdown CMS with typed collections via @nuxt/content',
128
+ icon: 'i-lucide-file-text',
129
+ features: ['Markdown', 'Collections', 'Frontmatter', 'Content API', 'Zod Schemas'],
130
+ borderColor: 'border-rose-500/30',
131
+ bgColor: 'bg-rose-500/5',
132
+ iconBg: 'bg-rose-500/10',
133
+ iconColor: 'text-rose-500',
134
+ },
135
+ {
136
+ name: 'Scripts',
137
+ key: 'scripts',
138
+ description: 'Third-party script management with performance-safe loading',
139
+ icon: 'i-lucide-code',
140
+ features: ['Lazy Loading', 'Analytics', 'Consent-Aware', 'Performance-Safe'],
141
+ borderColor: 'border-indigo-500/30',
142
+ bgColor: 'bg-indigo-500/5',
143
+ iconBg: 'bg-indigo-500/10',
144
+ iconColor: 'text-indigo-500',
145
+ },
146
+ {
147
+ name: 'Routing',
148
+ key: 'routing',
149
+ description: 'Advanced routing utilities, typed middleware and navigation helpers',
150
+ icon: 'i-lucide-route',
151
+ features: ['Typed Routes', 'Middleware', 'Route Guards', 'Navigation Utils'],
152
+ borderColor: 'border-teal-500/30',
153
+ bgColor: 'bg-teal-500/5',
154
+ iconBg: 'bg-teal-500/10',
155
+ iconColor: 'text-teal-500',
156
+ },
157
+ {
158
+ name: 'SEO',
159
+ key: 'seo',
160
+ description: 'Meta tags, Open Graph, JSON-LD, sitemap and robots.txt via @nuxtjs/seo',
161
+ icon: 'i-lucide-search',
162
+ features: ['Meta Tags', 'Open Graph', 'JSON-LD', 'Sitemap', 'Robots.txt', 'Schema.org'],
163
+ borderColor: 'border-sky-500/30',
164
+ bgColor: 'bg-sky-500/5',
165
+ iconBg: 'bg-sky-500/10',
166
+ iconColor: 'text-sky-500',
167
+ },
168
+ {
169
+ name: 'Feeds',
170
+ key: 'feeds',
171
+ description: 'RSS and feed aggregation with display components',
172
+ icon: 'i-lucide-rss',
173
+ features: ['RSS Feeds', 'Feed Display', 'Feed Parser'],
174
+ borderColor: 'border-orange-500/30',
175
+ bgColor: 'bg-orange-500/5',
176
+ iconBg: 'bg-orange-500/10',
177
+ iconColor: 'text-orange-500',
178
+ },
179
+ ]
180
+
181
+ const addonLayers: LayerDef[] = [
182
+ {
183
+ name: 'Canvas',
184
+ key: 'canvas',
185
+ description: 'WebGL/WebGPU canvas foundation required by the shader layer',
186
+ icon: 'i-lucide-monitor',
187
+ features: ['WebGL', 'WebGPU', 'Canvas Composables', '3D Foundation'],
188
+ borderColor: 'border-purple-500/30',
189
+ bgColor: 'bg-purple-500/5',
190
+ iconBg: 'bg-purple-500/10',
191
+ iconColor: 'text-purple-500',
192
+ },
193
+ {
194
+ name: 'Shader',
195
+ key: 'shader',
196
+ description: 'Three.js + TresJS shader system with TSL, noise and post-processing',
197
+ icon: 'i-lucide-shapes',
198
+ features: ['TSL Shaders', 'Noise', 'Gradients', 'Fresnel', 'Post-Processing', 'WebGPU'],
199
+ borderColor: 'border-violet-500/30',
200
+ bgColor: 'bg-violet-500/5',
201
+ iconBg: 'bg-violet-500/10',
202
+ iconColor: 'text-violet-500',
203
+ },
204
+ ]
205
+
206
+ const marqueeItems = [
207
+ 'Core', '◆', 'UI', '◆', 'Layout', '◆', 'Motion', '◆', 'Forms', '◆',
208
+ 'Theme', '◆', 'Content', '◆', 'SEO', '◆', 'Scripts', '◆', 'Routing', '◆', 'Feeds', '◆',
209
+ ]
210
+ </script>
211
+
212
+ <template>
213
+ <div class="min-h-screen">
214
+ <UContainer class="px-8 pb-4 pt-8">
215
+ <!-- Hero -->
216
+ <div ref="heroRef" class="space-y-6 py-8 text-center">
217
+ <UBadge variant="subtle" color="primary" size="lg" icon="i-lucide-layers">
218
+ kmcom-nuxt-layers
219
+ </UBadge>
220
+ <h1 class="text-highlighted text-4xl font-bold sm:text-5xl lg:text-6xl">
221
+ Nuxt Layer Architecture
222
+ </h1>
223
+ <p class="text-muted mx-auto max-w-2xl text-lg">
224
+ A production-ready starter built on composable Nuxt layers. Toggle layers in
225
+ <code class="text-primary">layers.config.ts</code> to enable or disable capabilities.
226
+ </p>
227
+ <div class="flex flex-wrap justify-center gap-3">
228
+ <UButton
229
+ to="/design-system"
230
+ size="xl"
231
+ icon="i-lucide-layout-template"
232
+ trailing-icon="i-lucide-arrow-right"
233
+ >
234
+ Design System
235
+ </UButton>
236
+ <UButton
237
+ to="https://github.com/kieranmansfield/starter-template"
238
+ target="_blank"
239
+ size="xl"
240
+ icon="i-simple-icons-github"
241
+ color="neutral"
242
+ variant="subtle"
243
+ >
244
+ View on GitHub
245
+ </UButton>
246
+ </div>
247
+ </div>
248
+ </UContainer>
249
+
250
+ <!-- Marquee divider -->
251
+ <div class="border-default overflow-hidden border-y py-3">
252
+ <MotionMarquee
253
+ :speed="45"
254
+ :pause-on-hover="false"
255
+ gap="3rem"
256
+ velocity-based
257
+ :velocity-sensitivity="0.6"
258
+ >
259
+ <span
260
+ v-for="item in marqueeItems"
261
+ :key="item"
262
+ class="text-muted whitespace-nowrap text-xs font-semibold uppercase tracking-widest"
263
+ >{{ item }}</span>
264
+ </MotionMarquee>
265
+ </div>
266
+
267
+ <div ref="sectionsRef">
268
+ <UContainer class="px-8 pb-8 pt-10">
269
+ <div class="space-y-12">
270
+
271
+ <!-- Core Layers -->
272
+ <section>
273
+ <div class="mb-6">
274
+ <h2 class="text-highlighted text-xl font-semibold">Core Layers</h2>
275
+ <p class="text-muted mt-1 text-sm">
276
+ The solid foundation — enabled by default, no extra dependencies required.
277
+ </p>
278
+ </div>
279
+ <div class="grid gap-5 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
280
+ <div
281
+ v-for="layer in coreLayers"
282
+ :key="layer.key"
283
+ class="layer-card rounded-xl border-2 p-5"
284
+ :class="[layer.borderColor, layer.bgColor]"
285
+ >
286
+ <div class="flex h-full flex-col">
287
+ <div class="mb-4 flex items-center gap-3">
288
+ <div
289
+ class="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg"
290
+ :class="layer.iconBg"
291
+ >
292
+ <UIcon :name="layer.icon" class="text-lg" :class="layer.iconColor" />
293
+ </div>
294
+ <div>
295
+ <h3 class="text-highlighted text-sm font-semibold leading-tight">
296
+ {{ layer.name }}
297
+ </h3>
298
+ <UBadge
299
+ :color="activeLayers[layer.key] ? 'success' : 'neutral'"
300
+ variant="subtle"
301
+ size="sm"
302
+ class="mt-0.5"
303
+ >
304
+ {{ activeLayers[layer.key] ? 'Active' : 'Disabled' }}
305
+ </UBadge>
306
+ </div>
307
+ </div>
308
+ <p class="text-muted mb-4 grow text-xs leading-relaxed">
309
+ {{ layer.description }}
310
+ </p>
311
+ <div class="flex flex-wrap gap-1">
312
+ <UBadge
313
+ v-for="feature in layer.features"
314
+ :key="feature"
315
+ variant="subtle"
316
+ color="neutral"
317
+ size="sm"
318
+ >
319
+ {{ feature }}
320
+ </UBadge>
321
+ </div>
322
+ </div>
323
+ </div>
324
+ </div>
325
+ </section>
326
+
327
+ <!-- Add-on Layers -->
328
+ <section>
329
+ <div class="mb-6">
330
+ <h2 class="text-highlighted text-xl font-semibold">Add-on Layers</h2>
331
+ <p class="text-muted mt-1 text-sm">
332
+ Optional capabilities. Enable in <code class="text-primary">layers.config.ts</code>
333
+ and install the required peer dependencies.
334
+ </p>
335
+ </div>
336
+ <div class="grid gap-5 md:grid-cols-2 lg:grid-cols-3">
337
+ <div
338
+ v-for="layer in addonLayers"
339
+ :key="layer.key"
340
+ class="layer-card rounded-xl border-2 p-5 opacity-70 transition-opacity"
341
+ :class="[
342
+ layer.borderColor,
343
+ layer.bgColor,
344
+ activeLayers[layer.key] ? 'opacity-100' : 'opacity-60',
345
+ ]"
346
+ >
347
+ <div class="flex h-full flex-col">
348
+ <div class="mb-4 flex items-center gap-3">
349
+ <div
350
+ class="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg"
351
+ :class="layer.iconBg"
352
+ >
353
+ <UIcon :name="layer.icon" class="text-lg" :class="layer.iconColor" />
354
+ </div>
355
+ <div>
356
+ <h3 class="text-highlighted text-sm font-semibold leading-tight">
357
+ {{ layer.name }}
358
+ </h3>
359
+ <UBadge
360
+ :color="activeLayers[layer.key] ? 'success' : 'neutral'"
361
+ variant="subtle"
362
+ size="sm"
363
+ class="mt-0.5"
364
+ >
365
+ {{ activeLayers[layer.key] ? 'Active' : 'Disabled' }}
366
+ </UBadge>
367
+ </div>
368
+ </div>
369
+ <p class="text-muted mb-4 grow text-xs leading-relaxed">
370
+ {{ layer.description }}
371
+ </p>
372
+ <div class="flex flex-wrap gap-1">
373
+ <UBadge
374
+ v-for="feature in layer.features"
375
+ :key="feature"
376
+ variant="subtle"
377
+ color="neutral"
378
+ size="sm"
379
+ >
380
+ {{ feature }}
381
+ </UBadge>
382
+ </div>
383
+ </div>
384
+ </div>
385
+ </div>
386
+ </section>
387
+
388
+ <!-- Quick Links -->
389
+ <div class="flex flex-wrap justify-center gap-4 pt-2">
390
+ <UButton to="/design-system" variant="outline" icon="i-lucide-layout-template">
391
+ Design System Reference
392
+ </UButton>
393
+ <UButton
394
+ to="https://github.com/kieranmansfield/starter-template"
395
+ target="_blank"
396
+ variant="outline"
397
+ icon="i-simple-icons-github"
398
+ >
399
+ View Source
400
+ </UButton>
401
+ </div>
402
+
403
+ </div>
404
+ </UContainer>
405
+ </div>
406
+ </div>
407
+ </template>
@@ -0,0 +1,15 @@
1
+ // Starter layer — showcase components for the boilerplate starter template
2
+ export default defineNuxtConfig({
3
+ $meta: {
4
+ name: 'starter',
5
+ },
6
+
7
+ extends: ['../core', '../ui', '../layout', '../motion'],
8
+
9
+ compatibilityDate: '2026-06-20',
10
+
11
+ typescript: {
12
+ typeCheck: false,
13
+ strict: true,
14
+ },
15
+ })
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "kmcom-layer-starter",
3
+ "type": "module",
4
+ "version": "0.0.1",
5
+ "main": "./nuxt.config.ts",
6
+ "scripts": {
7
+ "typecheck": "vue-tsc --noEmit -p ../../tsconfig.typecheck.json",
8
+ "lint": "eslint ."
9
+ }
10
+ }
@@ -1,30 +1,8 @@
1
1
  <script setup lang="ts">
2
- const {
3
- contrastOverride,
4
- motionOverride,
5
- transparencyOverride,
6
- setContrastOverride,
7
- setMotionOverride,
8
- setTransparencyOverride,
9
- effectiveHighContrast,
10
- effectiveReducedMotion,
11
- effectiveReducedTransparency,
12
- } = useTheme()
2
+ import { useThemePreferenceModels } from '../../composables/useThemePreferenceModels'
13
3
 
14
- const contrastModel = computed({
15
- get: () => effectiveHighContrast.value,
16
- set: (val: boolean) => setContrastOverride(val ? 'on' : 'system'),
17
- })
18
-
19
- const motionModel = computed({
20
- get: () => effectiveReducedMotion.value,
21
- set: (val: boolean) => setMotionOverride(val ? 'on' : 'system'),
22
- })
23
-
24
- const transparencyModel = computed({
25
- get: () => effectiveReducedTransparency.value,
26
- set: (val: boolean) => setTransparencyOverride(val ? 'on' : 'system'),
27
- })
4
+ const { contrastOverride, motionOverride, transparencyOverride, contrastModel, motionModel, transparencyModel } =
5
+ useThemePreferenceModels()
28
6
  </script>
29
7
 
30
8
  <template>
@@ -0,0 +1,39 @@
1
+ import { computed } from 'vue'
2
+
3
+ export function useThemePreferenceModels() {
4
+ const {
5
+ contrastOverride,
6
+ motionOverride,
7
+ transparencyOverride,
8
+ setContrastOverride,
9
+ setMotionOverride,
10
+ setTransparencyOverride,
11
+ effectiveHighContrast,
12
+ effectiveReducedMotion,
13
+ effectiveReducedTransparency,
14
+ } = useTheme()
15
+
16
+ const contrastModel = computed({
17
+ get: () => effectiveHighContrast.value,
18
+ set: (value: boolean) => setContrastOverride(value ? 'on' : 'system'),
19
+ })
20
+
21
+ const motionModel = computed({
22
+ get: () => effectiveReducedMotion.value,
23
+ set: (value: boolean) => setMotionOverride(value ? 'on' : 'system'),
24
+ })
25
+
26
+ const transparencyModel = computed({
27
+ get: () => effectiveReducedTransparency.value,
28
+ set: (value: boolean) => setTransparencyOverride(value ? 'on' : 'system'),
29
+ })
30
+
31
+ return {
32
+ contrastOverride,
33
+ motionOverride,
34
+ transparencyOverride,
35
+ contrastModel,
36
+ motionModel,
37
+ transparencyModel,
38
+ }
39
+ }
@@ -1,6 +1,4 @@
1
- import twColors from 'tailwindcss/colors'
2
-
3
- type DefaultColors = typeof twColors
1
+ import { buildAccentCSS } from '../utils/accent-css'
4
2
 
5
3
  /**
6
4
  * Nitro render hook — prevents FOUC for all theme preferences by injecting:
@@ -24,95 +22,6 @@ type DefaultColors = typeof twColors
24
22
  * set the same value again — this is idempotent and safe.
25
23
  */
26
24
 
27
- const ACCENTS = [
28
- 'red',
29
- 'orange',
30
- 'amber',
31
- 'yellow',
32
- 'lime',
33
- 'green',
34
- 'emerald',
35
- 'teal',
36
- 'cyan',
37
- 'sky',
38
- 'blue',
39
- 'indigo',
40
- 'violet',
41
- 'purple',
42
- 'fuchsia',
43
- 'pink',
44
- 'rose',
45
- ] as const
46
-
47
- type AccentName = (typeof ACCENTS)[number]
48
-
49
- const SHADES = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950] as const
50
-
51
- /**
52
- * Coordinated three-colour palettes. Each accent colour selects a
53
- * secondary and info (accent) colour that sit in the same temperature
54
- * zone and feel cohesive together.
55
- *
56
- * primary = the user-selected accent
57
- * secondary = a related colour in the same hue family
58
- * info = a third complementary accent
59
- */
60
- const ACCENT_PALETTES: Record<AccentName, { secondary: AccentName; info: AccentName }> = {
61
- red: { secondary: 'rose', info: 'orange' },
62
- orange: { secondary: 'amber', info: 'red' },
63
- amber: { secondary: 'orange', info: 'yellow' },
64
- yellow: { secondary: 'lime', info: 'amber' },
65
- lime: { secondary: 'green', info: 'yellow' },
66
- green: { secondary: 'teal', info: 'emerald' },
67
- emerald: { secondary: 'green', info: 'teal' },
68
- teal: { secondary: 'cyan', info: 'emerald' },
69
- cyan: { secondary: 'sky', info: 'teal' },
70
- sky: { secondary: 'blue', info: 'cyan' },
71
- blue: { secondary: 'indigo', info: 'sky' },
72
- indigo: { secondary: 'violet', info: 'blue' },
73
- violet: { secondary: 'purple', info: 'indigo' },
74
- purple: { secondary: 'fuchsia', info: 'violet' },
75
- fuchsia: { secondary: 'pink', info: 'purple' },
76
- pink: { secondary: 'rose', info: 'fuchsia' },
77
- rose: { secondary: 'pink', info: 'red' },
78
- }
79
-
80
- function getShades(name: AccentName): Record<number, string> | null {
81
- const p = (twColors as DefaultColors)[name]
82
- return !p || typeof p === 'string' ? null : (p as Record<number, string>)
83
- }
84
-
85
- // Pre-compute all CSS rules at startup (once)
86
- function buildAccentCSS(): string {
87
- let css = ''
88
- for (const accent of ACCENTS) {
89
- const primary = getShades(accent)
90
- if (!primary) continue
91
-
92
- const { secondary: secName, info: infoName } = ACCENT_PALETTES[accent]
93
- const secondary = getShades(secName)
94
- const info = getShades(infoName)
95
-
96
- let vars = ''
97
- for (const s of SHADES) {
98
- vars += `--ui-color-primary-${s}:${primary[s]};`
99
- }
100
- if (secondary) {
101
- for (const s of SHADES) {
102
- vars += `--ui-color-secondary-${s}:${secondary[s]};`
103
- }
104
- }
105
- if (info) {
106
- for (const s of SHADES) {
107
- vars += `--ui-color-info-${s}:${info[s]};`
108
- }
109
- }
110
-
111
- css += `html[data-theme-colour="${accent}"]{${vars}}`
112
- }
113
- return css
114
- }
115
-
116
25
  const accentCSS = buildAccentCSS()
117
26
 
118
27
  // Blocking init script — restores data-* attributes from localStorage before first paint.
@@ -0,0 +1,75 @@
1
+ import twColors from 'tailwindcss/colors'
2
+
3
+ type DefaultColors = typeof twColors
4
+
5
+ const ACCENTS = [
6
+ 'red',
7
+ 'orange',
8
+ 'amber',
9
+ 'yellow',
10
+ 'lime',
11
+ 'green',
12
+ 'emerald',
13
+ 'teal',
14
+ 'cyan',
15
+ 'sky',
16
+ 'blue',
17
+ 'indigo',
18
+ 'violet',
19
+ 'purple',
20
+ 'fuchsia',
21
+ 'pink',
22
+ 'rose',
23
+ ] as const
24
+
25
+ type AccentName = (typeof ACCENTS)[number]
26
+
27
+ const SHADES = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950] as const
28
+
29
+ const ACCENT_PALETTES: Record<AccentName, { secondary: AccentName; info: AccentName }> = {
30
+ red: { secondary: 'rose', info: 'orange' },
31
+ orange: { secondary: 'amber', info: 'red' },
32
+ amber: { secondary: 'orange', info: 'yellow' },
33
+ yellow: { secondary: 'lime', info: 'amber' },
34
+ lime: { secondary: 'green', info: 'yellow' },
35
+ green: { secondary: 'teal', info: 'emerald' },
36
+ emerald: { secondary: 'green', info: 'teal' },
37
+ teal: { secondary: 'cyan', info: 'emerald' },
38
+ cyan: { secondary: 'sky', info: 'teal' },
39
+ sky: { secondary: 'blue', info: 'cyan' },
40
+ blue: { secondary: 'indigo', info: 'sky' },
41
+ indigo: { secondary: 'violet', info: 'blue' },
42
+ violet: { secondary: 'purple', info: 'indigo' },
43
+ purple: { secondary: 'fuchsia', info: 'violet' },
44
+ fuchsia: { secondary: 'pink', info: 'purple' },
45
+ pink: { secondary: 'rose', info: 'fuchsia' },
46
+ rose: { secondary: 'pink', info: 'red' },
47
+ }
48
+
49
+ function resolveAccentShades(name: AccentName): Record<number, string> | null {
50
+ const palette = (twColors as DefaultColors)[name]
51
+ return !palette || typeof palette === 'string' ? null : (palette as Record<number, string>)
52
+ }
53
+
54
+ function buildColourVariables(prefix: string, colours: Record<number, string>) {
55
+ return SHADES.map((shade) => `--ui-color-${prefix}-${shade}:${colours[shade]};`).join('')
56
+ }
57
+
58
+ function buildAccentRule(accent: AccentName) {
59
+ const primary = resolveAccentShades(accent)
60
+ if (!primary) return ''
61
+
62
+ const { secondary: secondaryName, info: infoName } = ACCENT_PALETTES[accent]
63
+ const secondary = resolveAccentShades(secondaryName)
64
+ const info = resolveAccentShades(infoName)
65
+
66
+ let vars = buildColourVariables('primary', primary)
67
+ if (secondary) vars += buildColourVariables('secondary', secondary)
68
+ if (info) vars += buildColourVariables('info', info)
69
+
70
+ return `html[data-theme-colour="${accent}"]{${vars}}`
71
+ }
72
+
73
+ export function buildAccentCSS(): string {
74
+ return ACCENTS.map(buildAccentRule).join('')
75
+ }