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.
- package/docs/FEEDS.md +1 -2
- package/layers/animations/app/composables/useMagneticElement.ts +11 -9
- package/layers/animations/app/composables/useTiltEffect.ts +11 -9
- package/layers/animations/app/utils/pointerMotion.ts +31 -0
- package/layers/canvas/app/components/ShaderCanvas.vue +2 -2
- package/layers/content/app/composables/useCollectionItems.ts +28 -0
- package/layers/content/app/composables/useGalleryItems.ts +8 -14
- package/layers/content/app/composables/usePortfolioItems.ts +10 -18
- package/layers/core/app/composables/useBrowser.ts +9 -82
- package/layers/core/app/composables/useFeatures.ts +3 -27
- package/layers/core/app/plugins/init.ts +157 -135
- package/layers/core/app/utils/browserInfo.ts +115 -0
- package/layers/core/app/utils/featureClasses.ts +40 -0
- package/layers/core/app/utils/helpers.test.ts +51 -0
- package/layers/feeds/app/components/Feeds/Index.vue +1 -1
- package/layers/feeds/app/components/Feeds/RouteCard.vue +3 -9
- package/layers/feeds/app/utils/feed-catalog.ts +9 -4
- package/layers/feeds/nuxt.config.ts +0 -1
- package/layers/feeds/server/utils/content-adapter.test.ts +68 -0
- package/layers/feeds/server/utils/content-adapter.ts +2 -22
- package/layers/feeds/server/utils/feed-author.ts +32 -0
- package/layers/feeds/server/utils/feed-config.ts +88 -0
- package/layers/feeds/server/utils/feed-service.ts +11 -30
- package/layers/feeds/server/utils/feed-xml.ts +26 -0
- package/layers/feeds/server/utils/formats/rss.ts +10 -15
- package/layers/feeds/server/utils/formats.test.ts +71 -0
- package/layers/forms/app/components/Form/Field.vue +42 -30
- package/layers/forms/app/utils/fieldProps.ts +65 -0
- package/layers/layout/app/components/Layout/Grid/Item.vue +29 -146
- package/layers/layout/app/utils/gridPlacementStyle.ts +195 -0
- package/layers/mailer/app/types/mailer.ts +7 -25
- package/layers/mailer/server/utils/email.ts +28 -13
- package/layers/mailer/server/utils/hooks.ts +1 -20
- package/layers/navigation/app/composables/useSite.ts +2 -9
- package/layers/navigation/app/utils/site.ts +26 -0
- package/layers/routing/app/utils/resolveRoute.test.ts +47 -0
- package/layers/routing/app/utils/resolveRoute.ts +19 -10
- package/layers/scripts/app/composables/useAnalytics.ts +8 -41
- package/layers/scripts/app/composables/useGtm.ts +6 -13
- package/layers/scripts/app/utils/scriptClients.ts +70 -0
- package/layers/scroll/app/composables/useSmoothScroll.ts +9 -43
- package/layers/scroll/app/utils/scroll.ts +103 -0
- package/layers/seo/app/composables/useSeoConfig.ts +3 -9
- package/layers/seo/app/utils/seoConfig.ts +38 -0
- package/layers/shader/app/components/Material/AmbientAurora.client.vue +11 -33
- package/layers/shader/app/components/Material/AmbientFlow.client.vue +10 -37
- package/layers/shader/app/components/Material/AmbientGradientMesh.client.vue +10 -37
- package/layers/shader/app/components/Material/AmbientNebula.client.vue +12 -37
- package/layers/shader/app/components/Material/AmbientOcean.client.vue +9 -33
- package/layers/shader/app/components/Material/Gradient.client.vue +25 -46
- package/layers/shader/app/components/Material/Image.client.vue +10 -55
- package/layers/shader/app/components/Material/Node.client.vue +18 -5
- package/layers/shader/app/components/Material/Noise.client.vue +9 -43
- package/layers/shader/app/components/Preset/ThemeBubble.client.vue +2 -1
- package/layers/shader/app/components/Preset/ThemeFlow.client.vue +2 -1
- package/layers/shader/app/components/Preset/ThemeGradient.client.vue +2 -1
- package/layers/shader/app/components/Preset/ThemeLavaLamp.client.vue +2 -1
- package/layers/shader/app/components/Preset/ThemePlasma.client.vue +2 -1
- package/layers/shader/app/components/Preset/ThemeWave.client.vue +2 -1
- package/layers/shader/app/components/Shader/Background.client.vue +44 -24
- package/layers/shader/app/composables/useAmbientMaterials.ts +5 -1
- package/layers/shader/app/composables/useShader.ts +38 -23
- package/layers/shader/app/composables/useShaderGraph.ts +11 -6
- package/layers/shader/app/composables/useShaderMixBlend.ts +4 -4
- package/layers/shader/app/composables/useShaderRuntime.ts +0 -1
- package/layers/shader/app/composables/useShaderVec2.ts +2 -4
- package/layers/shader/app/composables/useThemePreset.ts +34 -8
- package/layers/shader/app/composables/useUniformWatchers.ts +15 -0
- package/layers/shader/app/composables/useUniforms.ts +0 -1
- package/layers/shader/app/shaders/common/blend.ts +4 -4
- package/layers/shader/app/shaders/common/effects.ts +38 -21
- package/layers/shader/app/shaders/common/grain.ts +46 -49
- package/layers/shader/app/shaders/common/lighting.ts +17 -15
- package/layers/shader/app/shaders/common/math.ts +2 -4
- package/layers/shader/app/shaders/common/nodes.ts +17 -0
- package/layers/shader/app/shaders/common/palette.ts +21 -11
- package/layers/shader/app/shaders/common/patterns.ts +25 -14
- package/layers/shader/app/shaders/common/shapes.ts +97 -88
- package/layers/shader/app/shaders/common/uv.ts +33 -34
- package/layers/shader/app/shaders/createMaterial.ts +92 -78
- package/layers/shader/app/shaders/layers/paperShading.ts +22 -10
- package/layers/shader/app/shaders/layers/shaderGradient.ts +46 -21
- package/layers/shader/app/utils/tsl/tween.ts +2 -4
- package/layers/shader/package.json +5 -1
- package/layers/starter/app/components/StarterDesignSystem.vue +1913 -0
- package/layers/starter/app/components/StarterHome.vue +407 -0
- package/layers/starter/nuxt.config.ts +15 -0
- package/layers/starter/package.json +10 -0
- package/layers/theme/app/components/ThemePicker/Menu.vue +3 -25
- package/layers/theme/app/composables/useThemePreferenceModels.ts +39 -0
- package/layers/theme/server/plugins/theme-fouc.ts +1 -92
- package/layers/theme/server/utils/accent-css.ts +75 -0
- package/layers/typography/app/composables/typography.ts +3 -7
- package/layers/visual/app/composables/accent.ts +2 -9
- package/layers/visual/app/composables/gradient.ts +33 -46
- package/layers/visual/app/composables/picture.ts +2 -79
- package/layers/visual/app/utils/colorTokens.ts +23 -0
- package/layers/visual/app/utils/gradientStyle.ts +41 -0
- package/layers/visual/app/utils/responsiveSizes.ts +49 -0
- package/package.json +17 -5
- package/layers/feeds/app/utils/feed-catalog.test.ts +0 -71
- 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
|
+
})
|
|
@@ -1,30 +1,8 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
|
|
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 =
|
|
15
|
-
|
|
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
|
|
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
|
+
}
|