kmcom-nuxt-layers 1.7.4 → 1.7.6

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.
@@ -12,7 +12,7 @@ const asBlog = (item: unknown) => item as BlogCollectionItem
12
12
  <NuxtContentDetail collection="blog" :slug not-found-message="Blog post not found">
13
13
  <template #headline="{ item }">
14
14
  <div class="flex items-center gap-3 text-sm text-muted">
15
- <time v-if="asBlog(item).date">{{ new Date(asBlog(item).date).toLocaleDateString() }}</time>
15
+ <time v-if="asBlog(item).date">{{ new Date(asBlog(item).date).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }) }}</time>
16
16
  <UBadge v-if="asBlog(item).badge" color="primary" variant="subtle">
17
17
  {{ asBlog(item).badge }}
18
18
  </UBadge>
@@ -8,5 +8,11 @@
8
8
 
9
9
  @config '#layers/core/tailwind.config.js';
10
10
 
11
+ /* Force Tailwind 4 to eagerly scan all layer and app component files upfront.
12
+ Without this, dev mode compiles CSS lazily (per-file as Vite requests them),
13
+ causing an HMR update mid-render that produces a visible layout shift. */
14
+ @source '../../../../*/app/**/*.{vue,ts}';
15
+ @source '../../../../../apps/*/app/**/*.{vue,ts}';
16
+
11
17
  @import '#layers/core/app/assets/css/layout.css';
12
18
  @import '#layers/core/app/assets/css/base.css';
@@ -27,6 +27,7 @@ useSeoMeta({
27
27
  <template>
28
28
  <div class="min-h-screen p-8">
29
29
  <UContainer>
30
+ <ClientOnly>
30
31
  <div class="space-y-8">
31
32
  <div class="text-center">
32
33
  <h1 class="text-4xl font-bold mb-2">Core Layer Diagnostics</h1>
@@ -215,6 +216,11 @@ useSeoMeta({
215
216
  </div>
216
217
  </UCard>
217
218
  </div>
219
+
220
+ <template #fallback>
221
+ <div class="text-center text-muted py-16">Loading diagnostics…</div>
222
+ </template>
223
+ </ClientOnly>
218
224
  </UContainer>
219
225
  </div>
220
226
  </template>
@@ -1,17 +1,8 @@
1
1
  /** @type {import('tailwindcss').Config} */
2
2
  module.exports = {
3
- content: [
4
- 'app/components/**/*.{vue,js,ts}',
5
- 'app/composables/**/*.{js,ts}',
6
- 'app/content/**/*.md',
7
- 'app/layouts/**/*.vue',
8
- 'app/pages/**/*.vue',
9
- 'app/plugins/**/*.{js,ts}',
10
- 'app/App.{js,ts,vue}',
11
- 'app/app.{js,ts,vue}',
12
- 'app/Error.{js,ts,vue}',
13
- 'app/error.{js,ts,vue}',
14
- ],
3
+ // No content array — Tailwind 4's Vite plugin auto-discovers all processed files.
4
+ // A content array here would restrict scanning to only these paths (v3 behaviour),
5
+ // causing utilities from ui/layout/theme/etc layers to be absent from the initial CSS.
15
6
 
16
7
  safelist: [
17
8
  // Swiss Grid System - Responsive grid classes for BaseGridItem
@@ -1,4 +1,6 @@
1
1
  <script setup lang="ts">
2
+ defineOptions({ inheritAttrs: false })
3
+
2
4
  const props = withDefaults(
3
5
  defineProps<{
4
6
  /**
@@ -48,7 +50,7 @@ const percentage = computed(() => Math.round(progress.value * 100))
48
50
 
49
51
  <template>
50
52
  <!-- Linear Progress -->
51
- <div v-if="type === 'linear'" class="motion-scroll-progress-linear">
53
+ <div v-if="type === 'linear'" class="motion-scroll-progress-linear" v-bind="$attrs">
52
54
  <div
53
55
  class="w-full rounded-full overflow-hidden"
54
56
  :style="{ height: `${height}px`, backgroundColor: bgColor }"
@@ -69,6 +71,7 @@ const percentage = computed(() => Math.round(progress.value * 100))
69
71
  <!-- Circular Progress -->
70
72
  <ProgressCircular
71
73
  v-else
74
+ v-bind="$attrs"
72
75
  :progress="progress"
73
76
  :size
74
77
  :stroke-width="strokeWidth"
@@ -30,9 +30,7 @@ export default defineNuxtConfig({
30
30
 
31
31
  vite: {
32
32
  optimizeDeps: {
33
- include: [
34
- 'locomotive-scroll',
35
- ]
36
- }
37
- }
33
+ include: ['locomotive-scroll'],
34
+ },
35
+ },
38
36
  })
@@ -2,7 +2,7 @@
2
2
  // @ts-nocheck - TSL types are complex
3
3
  import { DoubleSide } from 'three'
4
4
  import { MeshBasicNodeMaterial } from 'three/webgpu'
5
- import { float, positionLocal, uv, vec3, vec4 } from 'three/tsl'
5
+ import { float, Fn, positionLocal, uv, vec3, vec4 } from 'three/tsl'
6
6
 
7
7
  const { transparent = false } = defineProps<{
8
8
  transparent?: boolean
@@ -28,18 +28,16 @@ watch(
28
28
  const vertex = pipeline.stagesFor('vertex')
29
29
  const ray = pipeline.stagesFor('ray')
30
30
 
31
- // UV chain generators read pipeline.uvNode.value in their stage fn closures
32
- pipeline.uvNode.value = uvStages.reduce((node, { fn }) => fn(node), uv())
33
-
34
- // Ray chain sky/tunnel generators read pipeline.rayNode.value in their closures
35
- pipeline.rayNode.value = ray.reduce((node, { fn }) => fn(node), screenRayNode)
36
-
37
- // Fragment chain
38
- material.colorNode = fragment.reduce((node, { fn }) => fn(node), vec4(0, 0, 0, 1))
31
+ // Wrap each chain in Fn() so TSL .toVar()/.assign() ops (used by noise functions
32
+ // like simplexNoise3d) have a valid stack context. Without this, TSL throws
33
+ // "No stack defined for assign operation".
34
+ pipeline.uvNode.value = Fn(() => uvStages.reduce((node, { fn }) => fn(node), uv()))()
35
+ pipeline.rayNode.value = Fn(() => ray.reduce((node, { fn }) => fn(node), screenRayNode))()
36
+ material.colorNode = Fn(() => fragment.reduce((node, { fn }) => fn(node), vec4(0, 0, 0, 1)))()
39
37
  material.needsUpdate = true
40
38
 
41
39
  if (vertex.length > 0) {
42
- material.positionNode = vertex.reduce((node, { fn }) => fn(node), positionLocal)
40
+ material.positionNode = Fn(() => vertex.reduce((node, { fn }) => fn(node), positionLocal))()
43
41
  material.needsUpdate = true
44
42
  }
45
43
  },
@@ -1,17 +1,23 @@
1
- import { createSharedComposable, useLocalStorage } from '@vueuse/core'
1
+ import { createSharedComposable } from '@vueuse/core'
2
2
  import type { AccentColor } from '#layers/theme/app/types/theme'
3
3
 
4
4
  export const useAccentColor = createSharedComposable(() => {
5
5
  const appConfig = useAppConfig()
6
-
7
6
  const defaultAccent = (appConfig.themeLayer?.defaultAccent ?? 'blue') as AccentColor
8
- const accent = useLocalStorage<AccentColor>('theme-colour', defaultAccent)
7
+
8
+ // useState ensures server value is serialized into the Nuxt payload and reused
9
+ // during hydration, avoiding a mismatch with localStorage (client-only).
10
+ // theme.client.ts reads localStorage after hydration and calls setAccent.
11
+ const accent = useState<AccentColor>('theme-accent', () => defaultAccent)
9
12
 
10
13
  const pageAccent = ref<AccentColor | null>(null)
11
14
  const activeAccent = computed<AccentColor>(() => pageAccent.value ?? accent.value)
12
15
 
13
16
  function setAccent(color: AccentColor) {
14
17
  accent.value = color
18
+ if (import.meta.client) {
19
+ localStorage.setItem('theme-colour', color)
20
+ }
15
21
  }
16
22
 
17
23
  function setPageAccent(color: AccentColor | null) {
@@ -1,8 +1,8 @@
1
- import { createSharedComposable, useLocalStorage, usePreferredContrast } from '@vueuse/core'
1
+ import { createSharedComposable, usePreferredContrast } from '@vueuse/core'
2
2
  import type { PreferenceOverride } from '#layers/theme/app/types/theme'
3
3
 
4
4
  export const useThemeContrast = createSharedComposable(() => {
5
- const contrastOverride = useLocalStorage<PreferenceOverride>('theme-contrast', 'system')
5
+ const contrastOverride = useState<PreferenceOverride>('theme-contrast', () => 'system')
6
6
  const systemContrast = usePreferredContrast()
7
7
 
8
8
  const effectiveHighContrast = computed(() => {
@@ -13,6 +13,9 @@ export const useThemeContrast = createSharedComposable(() => {
13
13
 
14
14
  function setContrastOverride(value: PreferenceOverride) {
15
15
  contrastOverride.value = value
16
+ if (import.meta.client) {
17
+ localStorage.setItem('theme-contrast', value)
18
+ }
16
19
  }
17
20
 
18
21
  return {
@@ -1,8 +1,8 @@
1
- import { createSharedComposable, useLocalStorage, usePreferredReducedMotion } from '@vueuse/core'
1
+ import { createSharedComposable, usePreferredReducedMotion } from '@vueuse/core'
2
2
  import type { PreferenceOverride } from '#layers/theme/app/types/theme'
3
3
 
4
4
  export const useThemeMotion = createSharedComposable(() => {
5
- const motionOverride = useLocalStorage<PreferenceOverride>('theme-motion', 'system')
5
+ const motionOverride = useState<PreferenceOverride>('theme-motion', () => 'system')
6
6
  const systemMotion = usePreferredReducedMotion()
7
7
 
8
8
  const effectiveReducedMotion = computed(() => {
@@ -13,6 +13,9 @@ export const useThemeMotion = createSharedComposable(() => {
13
13
 
14
14
  function setMotionOverride(value: PreferenceOverride) {
15
15
  motionOverride.value = value
16
+ if (import.meta.client) {
17
+ localStorage.setItem('theme-motion', value)
18
+ }
16
19
  }
17
20
 
18
21
  return {
@@ -1,12 +1,11 @@
1
1
  import {
2
2
  createSharedComposable,
3
- useLocalStorage,
4
3
  usePreferredReducedTransparency,
5
4
  } from '@vueuse/core'
6
5
  import type { PreferenceOverride } from '#layers/theme/app/types/theme'
7
6
 
8
7
  export const useThemeTransparency = createSharedComposable(() => {
9
- const transparencyOverride = useLocalStorage<PreferenceOverride>('theme-transparency', 'system')
8
+ const transparencyOverride = useState<PreferenceOverride>('theme-transparency', () => 'system')
10
9
  const systemTransparency = usePreferredReducedTransparency()
11
10
 
12
11
  const effectiveReducedTransparency = computed(() => {
@@ -17,6 +16,9 @@ export const useThemeTransparency = createSharedComposable(() => {
17
16
 
18
17
  function setTransparencyOverride(value: PreferenceOverride) {
19
18
  transparencyOverride.value = value
19
+ if (import.meta.client) {
20
+ localStorage.setItem('theme-transparency', value)
21
+ }
20
22
  }
21
23
 
22
24
  return {
@@ -1,8 +1,25 @@
1
+ import type { AccentColor, PreferenceOverride } from '#layers/theme/app/types/theme'
2
+
1
3
  export default defineNuxtPlugin(() => {
2
- const { activeAccent } = useAccentColor()
3
- const { effectiveHighContrast } = useThemeContrast()
4
- const { effectiveReducedMotion } = useThemeMotion()
5
- const { effectiveReducedTransparency } = useThemeTransparency()
4
+ const { setAccent, activeAccent } = useAccentColor()
5
+ const { setContrastOverride, effectiveHighContrast } = useThemeContrast()
6
+ const { setMotionOverride, effectiveReducedMotion } = useThemeMotion()
7
+ const { setTransparencyOverride, effectiveReducedTransparency } = useThemeTransparency()
8
+
9
+ // Restore persisted preferences. useState defaults were serialized from the
10
+ // server, so hydration matches. We update to the stored values here, after
11
+ // hydration completes, to avoid any mismatch.
12
+ const storedAccent = localStorage.getItem('theme-colour')
13
+ if (storedAccent) setAccent(storedAccent as AccentColor)
14
+
15
+ const storedContrast = localStorage.getItem('theme-contrast')
16
+ if (storedContrast) setContrastOverride(storedContrast as PreferenceOverride)
17
+
18
+ const storedMotion = localStorage.getItem('theme-motion')
19
+ if (storedMotion) setMotionOverride(storedMotion as PreferenceOverride)
20
+
21
+ const storedTransparency = localStorage.getItem('theme-transparency')
22
+ if (storedTransparency) setTransparencyOverride(storedTransparency as PreferenceOverride)
6
23
 
7
24
  watch(activeAccent, (color) => {
8
25
  document.documentElement.setAttribute('data-theme-colour', color)
@@ -1,9 +1,13 @@
1
+ <script setup lang="ts">
2
+ const year = useState('footer-year', () => new Date().getFullYear())
3
+ </script>
4
+
1
5
  <template>
2
6
  <UFooter>
3
7
  <template #bottom>
4
8
  <div class="pt-10">
5
9
  <p class="text-center text-sm text-neutral-300">
6
- Copyright © {{ new Date().getFullYear() }}
10
+ Copyright © {{ year }}
7
11
  </p>
8
12
  </div>
9
13
  </template>
@@ -20,7 +20,7 @@ const radius = computed(() => (size - strokeWidth) / 2)
20
20
  const circumference = computed(() => 2 * Math.PI * radius.value)
21
21
  const strokeDasharray = computed(() => `${progress * circumference.value} ${circumference.value}`)
22
22
 
23
- const gradientId = `progress-circular-gradient-${Math.random().toString(36).slice(2, 9)}`
23
+ const gradientId = `progress-circular-gradient-${useId()}`
24
24
  </script>
25
25
 
26
26
  <template>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "kmcom-nuxt-layers",
3
3
  "private": false,
4
- "version": "1.7.4",
4
+ "version": "1.7.6",
5
5
  "description": "Composable Nuxt 4 layers for building scalable Vue applications",
6
6
  "exports": {
7
7
  "./layers/core": "./layers/core/nuxt.config.ts",
@@ -189,6 +189,7 @@
189
189
  },
190
190
  "scripts": {
191
191
  "dev": "pnpm -F playground dev",
192
+ "dev:debug": "pnpm -F debug dev",
192
193
  "dev:identity": "pnpm -F visual-identity dev",
193
194
  "dev:core": "PLAYGROUND_LAYERS=core pnpm -F playground dev",
194
195
  "dev:ui": "PLAYGROUND_LAYERS=core,ui pnpm -F playground dev",