kmcom-nuxt-layers 1.3.1 → 1.5.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/layers/content/app/components/Blog/List.vue +5 -1
  2. package/layers/content/app/components/Gallery/AmbientImage.vue +5 -12
  3. package/layers/content/app/components/Gallery/Detail.vue +8 -6
  4. package/layers/content/app/components/Gallery/Grid.vue +11 -3
  5. package/layers/content/app/components/Portfolio/ColorPalette.vue +1 -4
  6. package/layers/content/app/components/Portfolio/Detail.vue +6 -1
  7. package/layers/content/app/components/Portfolio/List.vue +5 -1
  8. package/layers/content/app/components/content/Figure.vue +1 -7
  9. package/layers/content/nuxt.config.ts +16 -5
  10. package/layers/content/package.json +5 -5
  11. package/layers/core/app/assets/css/main.css +5 -0
  12. package/layers/core/app/composables/useCache.ts +8 -4
  13. package/layers/core/app/composables/useErrorLog.ts +9 -5
  14. package/layers/core/app/composables/useScrollGuard.ts +4 -2
  15. package/layers/core/app/plugins/feature-detection.client.ts +1 -1
  16. package/layers/core/app/plugins/init.ts +2 -1
  17. package/layers/core/app/plugins/scroll-guard.client.ts +4 -1
  18. package/layers/core/app.config.ts +0 -9
  19. package/layers/forms/app/components/Form/Contact.vue +16 -7
  20. package/layers/forms/nuxt.config.ts +18 -0
  21. package/layers/forms/package.json +2 -0
  22. package/layers/layout/app/components/Layout/Container.vue +1 -4
  23. package/layers/layout/app/components/Layout/Grid/Debug.vue +0 -1
  24. package/layers/layout/app/components/Layout/Grid/Item.vue +12 -6
  25. package/layers/layout/app/components/Layout/Main.vue +1 -4
  26. package/layers/layout/app/components/Layout/Page/Container.vue +3 -1
  27. package/layers/layout/app/components/Layout/Page/Header.vue +16 -7
  28. package/layers/layout/app/components/Layout/Section/Grid.vue +1 -4
  29. package/layers/layout/app/components/Layout/Section/Sidebar.vue +6 -1
  30. package/layers/layout/app/components/Layout/Section/Stack.vue +1 -1
  31. package/layers/layout/app/composables/useGridConfig.ts +6 -1
  32. package/layers/motion/app/components/Motion/HorizontalScroll.vue +61 -0
  33. package/layers/motion/app/components/Motion/PinnedSection.vue +77 -0
  34. package/layers/motion/app/components/Motion/ScrollProgress.vue +8 -56
  35. package/layers/motion/app/components/Motion/ScrollScene.vue +121 -0
  36. package/layers/motion/app/components/Motion/ScrollStep.vue +45 -0
  37. package/layers/motion/app/components/Motion/TextReveal.vue +28 -63
  38. package/layers/motion/app/composables/useScrollSteps.ts +41 -0
  39. package/layers/motion/app/composables/useSectionProgress.ts +58 -0
  40. package/layers/motion/app/composables/useSmoothScroll.ts +3 -2
  41. package/layers/motion/app/plugins/locomotive-scroll.client.ts +6 -6
  42. package/layers/motion/nuxt.config.ts +6 -0
  43. package/layers/motion/package.json +2 -1
  44. package/layers/routing/app/app.config.ts +20 -0
  45. package/layers/routing/app/composables/useFeatures.ts +12 -0
  46. package/layers/routing/app/composables/useMaintenance.ts +7 -0
  47. package/layers/routing/app/composables/useRoutingConfig.ts +20 -0
  48. package/layers/routing/app/middleware/01.maintenance.global.ts +6 -0
  49. package/layers/routing/app/middleware/02.governance.global.ts +25 -0
  50. package/layers/routing/app/plugins/feature-flags.client.ts +15 -0
  51. package/layers/routing/app/plugins/scroll-routing.client.ts +21 -0
  52. package/layers/routing/app/types/route-meta.d.ts +6 -0
  53. package/layers/routing/app/types/routing.ts +48 -0
  54. package/layers/routing/nuxt.config.ts +27 -0
  55. package/layers/routing/package.json +6 -0
  56. package/layers/shader/app/components/Preset/ThemeAurora.client.vue +86 -0
  57. package/layers/shader/app/components/Preset/ThemeBubble.client.vue +87 -0
  58. package/layers/shader/app/components/Preset/ThemeFlow.client.vue +86 -0
  59. package/layers/shader/app/components/Preset/ThemeGradient.client.vue +87 -0
  60. package/layers/shader/app/components/Preset/ThemeLavaLamp.client.vue +86 -0
  61. package/layers/shader/app/components/Preset/ThemePlasma.client.vue +86 -0
  62. package/layers/shader/app/components/Preset/ThemeWave.client.vue +86 -0
  63. package/layers/shader/app/components/Shader/Background.client.vue +15 -0
  64. package/layers/shader/app/composables/useAmbientMaterials.ts +306 -0
  65. package/layers/shader/app/composables/useThemeColors.ts +52 -0
  66. package/layers/shader/app/utils/tsl/oklch.ts +12 -6
  67. package/layers/theme/app/assets/css/theme.css +19 -14
  68. package/layers/theme/app/components/ThemePicker/AccentButton.vue +2 -2
  69. package/layers/theme/app/components/ThemePicker/Colors.vue +2 -4
  70. package/layers/theme/app/components/ThemePicker/Menu.vue +4 -13
  71. package/layers/theme/app/components/ThemePicker/MenuButton.vue +1 -7
  72. package/layers/theme/app/composables/useAccentColor.ts +38 -0
  73. package/layers/theme/app/composables/useTheme.ts +14 -0
  74. package/layers/theme/app/composables/useThemeContrast.ts +34 -0
  75. package/layers/theme/app/composables/useThemeMotion.ts +34 -0
  76. package/layers/theme/app/composables/useThemePreferences.ts +3 -156
  77. package/layers/theme/app/composables/useThemeTransparency.ts +41 -0
  78. package/layers/theme/app/plugins/theme.client.ts +3 -3
  79. package/layers/theme/app/types/theme.ts +4 -0
  80. package/layers/theme/nuxt.config.ts +7 -0
  81. package/layers/ui/app/app.config.ts +44 -0
  82. package/layers/ui/app/assets/css/main.css +14 -0
  83. package/layers/ui/app/components/Accent/Blob.vue +29 -0
  84. package/layers/ui/app/components/Accent/Scene.vue +38 -0
  85. package/layers/ui/app/components/Gradient/Background.vue +22 -0
  86. package/layers/ui/app/components/Gradient/Text.vue +22 -0
  87. package/layers/ui/app/components/Progress/Bar.vue +25 -0
  88. package/layers/ui/app/components/Progress/Circular.vue +69 -0
  89. package/layers/ui/app/components/Tint/Overlay.vue +25 -0
  90. package/layers/ui/app/components/Typography/CodeBlock.vue +2 -1
  91. package/layers/ui/app/components/Typography/Headline.vue +2 -1
  92. package/layers/ui/app/components/Typography/QuoteBlock.vue +2 -1
  93. package/layers/ui/app/components/Typography/TextStroke.vue +18 -16
  94. package/layers/ui/app/composables/accent.ts +51 -0
  95. package/layers/ui/app/composables/gradient.ts +79 -0
  96. package/layers/ui/app/composables/tint.ts +20 -0
  97. package/layers/ui/app/types/accent.ts +17 -0
  98. package/layers/ui/app/types/gradient.ts +27 -0
  99. package/layers/ui/app/types/tint.ts +25 -0
  100. package/package.json +37 -31
  101. package/layers/motion/app/utils/gsapAnimations.ts +0 -122
  102. package/layers/ui/app.config.ts +0 -12
@@ -40,8 +40,8 @@ const bgStyle = computed(() => ({
40
40
  class="size-8 rounded-full ring-1 ring-offset-2 transition-shadow duration-200"
41
41
  :class="[
42
42
  active
43
- ? 'ring-2 ring-primary ring-offset-[var(--ui-bg)] shadow-lg'
44
- : 'ring-transparent hover:ring-muted ring-offset-[var(--ui-bg)] hover:shadow-md',
43
+ ? 'ring-2 ring-primary ring-offset-bg shadow-lg'
44
+ : 'ring-transparent hover:ring-muted ring-offset-bg hover:shadow-md',
45
45
  ]"
46
46
  :style="bgStyle"
47
47
  :aria-label="`Set accent color to ${color}`"
@@ -2,11 +2,9 @@
2
2
  import type { AccentColor } from '#layers/theme/app/types/theme'
3
3
 
4
4
  const appConfig = useAppConfig()
5
- const { activeAccent, setAccent } = useThemePreferences()
5
+ const { activeAccent, setAccent } = useAccentColor()
6
6
 
7
- const accents = computed(
8
- () => (appConfig as any).themeLayer?.accents as AccentColor[] ?? [],
9
- )
7
+ const accents = computed(() => ((appConfig as any).themeLayer?.accents as AccentColor[]) ?? [])
10
8
  </script>
11
9
 
12
10
  <template>
@@ -9,7 +9,7 @@ const {
9
9
  effectiveHighContrast,
10
10
  effectiveReducedMotion,
11
11
  effectiveReducedTransparency,
12
- } = useThemePreferences()
12
+ } = useTheme()
13
13
 
14
14
  const contrastModel = computed({
15
15
  get: () => effectiveHighContrast.value,
@@ -50,10 +50,7 @@ const transparencyModel = computed({
50
50
  <li class="flex w-full flex-row items-center justify-between py-1">
51
51
  <div>
52
52
  <h2 class="text-lg font-semibold">High Contrast</h2>
53
- <p
54
- v-if="contrastOverride === 'system'"
55
- class="text-sm text-muted"
56
- >
53
+ <p v-if="contrastOverride === 'system'" class="text-sm text-muted">
57
54
  Following system
58
55
  </p>
59
56
  </div>
@@ -68,10 +65,7 @@ const transparencyModel = computed({
68
65
  <li class="flex w-full flex-row items-center justify-between py-1">
69
66
  <div>
70
67
  <h2 class="text-lg font-semibold">Reduce Motion</h2>
71
- <p
72
- v-if="motionOverride === 'system'"
73
- class="text-sm text-muted"
74
- >
68
+ <p v-if="motionOverride === 'system'" class="text-sm text-muted">
75
69
  Following system
76
70
  </p>
77
71
  </div>
@@ -86,10 +80,7 @@ const transparencyModel = computed({
86
80
  <li class="flex w-full flex-row items-center justify-between py-1">
87
81
  <div>
88
82
  <h2 class="text-lg font-semibold">Reduce Transparency</h2>
89
- <p
90
- v-if="transparencyOverride === 'system'"
91
- class="text-sm text-muted"
92
- >
83
+ <p v-if="transparencyOverride === 'system'" class="text-sm text-muted">
93
84
  Following system
94
85
  </p>
95
86
  </div>
@@ -1,9 +1,3 @@
1
1
  <template>
2
- <UButton
3
- icon="i-lucide-swatch-book"
4
- variant="ghost"
5
- square
6
- aria-label="Theme picker"
7
- size="xl"
8
- />
2
+ <UButton icon="i-lucide-swatch-book" variant="ghost" square aria-label="Theme picker" size="xl" />
9
3
  </template>
@@ -0,0 +1,38 @@
1
+ import { createSharedComposable, useLocalStorage } from '@vueuse/core'
2
+ import type { AccentColor } from '#layers/theme/app/types/theme'
3
+
4
+ export const useAccentColor = createSharedComposable(() => {
5
+ const appConfig = useAppConfig()
6
+
7
+ const defaultAccent = ((appConfig as any).themeLayer?.defaultAccent ?? 'blue') as AccentColor
8
+ const accent = useLocalStorage<AccentColor>('theme-colour', defaultAccent)
9
+
10
+ const pageAccent = ref<AccentColor | null>(null)
11
+ const activeAccent = computed<AccentColor>(() => pageAccent.value ?? accent.value)
12
+
13
+ function setAccent(color: AccentColor) {
14
+ accent.value = color
15
+ }
16
+
17
+ function setPageAccent(color: AccentColor | null) {
18
+ pageAccent.value = color
19
+ }
20
+
21
+ if (import.meta.client) {
22
+ watch(
23
+ activeAccent,
24
+ (color) => {
25
+ document.documentElement.setAttribute('data-theme-colour', color)
26
+ },
27
+ { immediate: true }
28
+ )
29
+ }
30
+
31
+ return {
32
+ accent: readonly(accent),
33
+ setAccent,
34
+ pageAccent: readonly(pageAccent),
35
+ setPageAccent,
36
+ activeAccent,
37
+ }
38
+ })
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Unified theme facade.
3
+ *
4
+ * Composes useAccentColor, useThemeContrast, useThemeMotion, and
5
+ * useThemeTransparency. Each sub-composable is individually shared via
6
+ * createSharedComposable, so calling useTheme() or any sub-composable
7
+ * directly always returns the same reactive instances.
8
+ */
9
+ export const useTheme = () => ({
10
+ ...useAccentColor(),
11
+ ...useThemeContrast(),
12
+ ...useThemeMotion(),
13
+ ...useThemeTransparency(),
14
+ })
@@ -0,0 +1,34 @@
1
+ import { createSharedComposable, useLocalStorage, usePreferredContrast } from '@vueuse/core'
2
+ import type { PreferenceOverride } from '#layers/theme/app/types/theme'
3
+
4
+ export const useThemeContrast = createSharedComposable(() => {
5
+ const contrastOverride = useLocalStorage<PreferenceOverride>('theme-contrast', 'system')
6
+ const systemContrast = usePreferredContrast()
7
+
8
+ const effectiveHighContrast = computed(() => {
9
+ if (contrastOverride.value === 'on') return true
10
+ if (contrastOverride.value === 'off') return false
11
+ return systemContrast.value === 'more'
12
+ })
13
+
14
+ function setContrastOverride(value: PreferenceOverride) {
15
+ contrastOverride.value = value
16
+ }
17
+
18
+ if (import.meta.client) {
19
+ watch(
20
+ effectiveHighContrast,
21
+ (high) => {
22
+ document.documentElement.setAttribute('data-theme-contrast', high ? 'high' : 'standard')
23
+ },
24
+ { immediate: true }
25
+ )
26
+ }
27
+
28
+ return {
29
+ contrastOverride: readonly(contrastOverride),
30
+ setContrastOverride,
31
+ effectiveHighContrast,
32
+ systemContrast,
33
+ }
34
+ })
@@ -0,0 +1,34 @@
1
+ import { createSharedComposable, useLocalStorage, usePreferredReducedMotion } from '@vueuse/core'
2
+ import type { PreferenceOverride } from '#layers/theme/app/types/theme'
3
+
4
+ export const useThemeMotion = createSharedComposable(() => {
5
+ const motionOverride = useLocalStorage<PreferenceOverride>('theme-motion', 'system')
6
+ const systemMotion = usePreferredReducedMotion()
7
+
8
+ const effectiveReducedMotion = computed(() => {
9
+ if (motionOverride.value === 'on') return true
10
+ if (motionOverride.value === 'off') return false
11
+ return systemMotion.value === 'reduce'
12
+ })
13
+
14
+ function setMotionOverride(value: PreferenceOverride) {
15
+ motionOverride.value = value
16
+ }
17
+
18
+ if (import.meta.client) {
19
+ watch(
20
+ effectiveReducedMotion,
21
+ (reduced) => {
22
+ document.documentElement.setAttribute('data-theme-motion', reduced ? 'reduced' : 'full')
23
+ },
24
+ { immediate: true }
25
+ )
26
+ }
27
+
28
+ return {
29
+ motionOverride: readonly(motionOverride),
30
+ setMotionOverride,
31
+ effectiveReducedMotion,
32
+ systemMotion,
33
+ }
34
+ })
@@ -1,158 +1,5 @@
1
- import {
2
- createSharedComposable,
3
- useLocalStorage,
4
- usePreferredContrast,
5
- usePreferredReducedMotion,
6
- usePreferredReducedTransparency,
7
- } from '@vueuse/core'
8
- import type { AccentColor, PreferenceOverride } from '#layers/theme/app/types/theme'
9
-
10
- const HTML_CLASSES = {
11
- highContrast: 'high-contrast',
12
- reduceMotion: 'reduce-motion',
13
- reduceTransparency: 'reduce-transparency',
14
- } as const
15
-
16
1
  /**
17
- * Central composable for managing theme preferences.
18
- *
19
- * Wrapped with `createSharedComposable` so all callers share a single
20
- * reactive instance — changes in the popover menu are immediately
21
- * reflected on the page and vice versa.
22
- *
23
- * Accent colors are applied purely via a `[data-theme]` attribute on `<html>`.
24
- * The server plugin injects a `<style>` tag with `[data-theme="X"]` CSS rules
25
- * that override Nuxt UI's `@layer theme` variables — no appConfig mutation needed.
2
+ * @deprecated Use `useTheme()` instead.
3
+ * Kept as a backward-compatible alias.
26
4
  */
27
- export const useThemePreferences = createSharedComposable(() => {
28
- const appConfig = useAppConfig()
29
-
30
- // --- Accent Color (user preference, persisted) ---
31
- const defaultAccent = ((appConfig as any).themeLayer?.defaultAccent ?? 'blue') as AccentColor
32
- const accent = useLocalStorage<AccentColor>('theme-accent', defaultAccent)
33
-
34
- // --- Page-level Accent Override (runtime only, not persisted) ---
35
- const pageAccent = ref<AccentColor | null>(null)
36
- const activeAccent = computed<AccentColor>(() => pageAccent.value ?? accent.value)
37
-
38
- // --- User Overrides (localStorage) ---
39
- const contrastOverride = useLocalStorage<PreferenceOverride>(
40
- 'theme-contrast',
41
- 'system',
42
- )
43
- const motionOverride = useLocalStorage<PreferenceOverride>(
44
- 'theme-motion',
45
- 'system',
46
- )
47
- const transparencyOverride = useLocalStorage<PreferenceOverride>(
48
- 'theme-transparency',
49
- 'system',
50
- )
51
-
52
- // --- System Preferences (VueUse media queries) ---
53
- const systemContrast = usePreferredContrast()
54
- const systemMotion = usePreferredReducedMotion()
55
- const systemTransparency = usePreferredReducedTransparency()
56
-
57
- // --- Effective (merged) Preferences ---
58
- const effectiveHighContrast = computed(() => {
59
- if (contrastOverride.value === 'on') return true
60
- if (contrastOverride.value === 'off') return false
61
- return systemContrast.value === 'more'
62
- })
63
-
64
- const effectiveReducedMotion = computed(() => {
65
- if (motionOverride.value === 'on') return true
66
- if (motionOverride.value === 'off') return false
67
- return systemMotion.value === 'reduce'
68
- })
69
-
70
- const effectiveReducedTransparency = computed(() => {
71
- if (transparencyOverride.value === 'on') return true
72
- if (transparencyOverride.value === 'off') return false
73
- return systemTransparency.value === 'reduce'
74
- })
75
-
76
- // --- DOM Side Effects ---
77
- function applyAccent(color: AccentColor) {
78
- if (!import.meta.client) return
79
- // Setting data-theme activates the matching [data-theme="X"] CSS rule
80
- // from the server-injected <style> tag, which overrides Nuxt UI's
81
- // @layer theme variables. No appConfig mutation needed (avoids HMR loops).
82
- document.documentElement.setAttribute('data-theme', color)
83
- }
84
-
85
- function applyPreferenceClasses() {
86
- if (!import.meta.client) return
87
- const cl = document.documentElement.classList
88
-
89
- cl.toggle(HTML_CLASSES.highContrast, effectiveHighContrast.value)
90
- cl.toggle(HTML_CLASSES.reduceMotion, effectiveReducedMotion.value)
91
- cl.toggle(HTML_CLASSES.reduceTransparency, effectiveReducedTransparency.value)
92
- }
93
-
94
- // --- Setters ---
95
- function setAccent(color: AccentColor) {
96
- accent.value = color
97
- }
98
-
99
- function setPageAccent(color: AccentColor | null) {
100
- pageAccent.value = color
101
- }
102
-
103
- function setContrastOverride(value: PreferenceOverride) {
104
- contrastOverride.value = value
105
- }
106
-
107
- function setMotionOverride(value: PreferenceOverride) {
108
- motionOverride.value = value
109
- }
110
-
111
- function setTransparencyOverride(value: PreferenceOverride) {
112
- transparencyOverride.value = value
113
- }
114
-
115
- // --- Watchers ---
116
- if (import.meta.client) {
117
- // Apply accent on change (uses activeAccent = page override or user pref)
118
- watch(activeAccent, (color) => applyAccent(color), { immediate: true })
119
-
120
- // Apply preference classes on any effective change
121
- watch(
122
- [effectiveHighContrast, effectiveReducedMotion, effectiveReducedTransparency],
123
- () => applyPreferenceClasses(),
124
- { immediate: true },
125
- )
126
- }
127
-
128
- return {
129
- // Accent (user preference)
130
- accent: readonly(accent),
131
- setAccent,
132
-
133
- // Page override
134
- pageAccent: readonly(pageAccent),
135
- setPageAccent,
136
-
137
- // Effective accent (page override wins over user preference)
138
- activeAccent,
139
-
140
- // Overrides
141
- contrastOverride: readonly(contrastOverride),
142
- motionOverride: readonly(motionOverride),
143
- transparencyOverride: readonly(transparencyOverride),
144
- setContrastOverride,
145
- setMotionOverride,
146
- setTransparencyOverride,
147
-
148
- // Effective (computed booleans)
149
- effectiveHighContrast,
150
- effectiveReducedMotion,
151
- effectiveReducedTransparency,
152
-
153
- // System values (for display)
154
- systemContrast,
155
- systemMotion,
156
- systemTransparency,
157
- }
158
- })
5
+ export const useThemePreferences = useTheme
@@ -0,0 +1,41 @@
1
+ import {
2
+ createSharedComposable,
3
+ useLocalStorage,
4
+ usePreferredReducedTransparency,
5
+ } from '@vueuse/core'
6
+ import type { PreferenceOverride } from '#layers/theme/app/types/theme'
7
+
8
+ export const useThemeTransparency = createSharedComposable(() => {
9
+ const transparencyOverride = useLocalStorage<PreferenceOverride>('theme-transparency', 'system')
10
+ const systemTransparency = usePreferredReducedTransparency()
11
+
12
+ const effectiveReducedTransparency = computed(() => {
13
+ if (transparencyOverride.value === 'on') return true
14
+ if (transparencyOverride.value === 'off') return false
15
+ return systemTransparency.value === 'reduce'
16
+ })
17
+
18
+ function setTransparencyOverride(value: PreferenceOverride) {
19
+ transparencyOverride.value = value
20
+ }
21
+
22
+ if (import.meta.client) {
23
+ watch(
24
+ effectiveReducedTransparency,
25
+ (reduced) => {
26
+ document.documentElement.setAttribute(
27
+ 'data-theme-transparency',
28
+ reduced ? 'reduced' : 'full'
29
+ )
30
+ },
31
+ { immediate: true }
32
+ )
33
+ }
34
+
35
+ return {
36
+ transparencyOverride: readonly(transparencyOverride),
37
+ setTransparencyOverride,
38
+ effectiveReducedTransparency,
39
+ systemTransparency,
40
+ }
41
+ })
@@ -1,5 +1,5 @@
1
1
  export default defineNuxtPlugin(() => {
2
- // Initialize the shared composable — applies appConfig.ui.colors.primary,
3
- // cleans up FOUC inline styles, and applies preference classes on <html>.
4
- useThemePreferences()
2
+ // Initialize shared composables — applies data-theme-colour, data-theme-contrast,
3
+ // data-theme-motion, and data-theme-transparency on <html> on first load.
4
+ useTheme()
5
5
  })
@@ -26,6 +26,10 @@ export type AccentColor =
26
26
 
27
27
  export type PreferenceOverride = 'system' | 'on' | 'off'
28
28
 
29
+ export type ThemeContrast = 'high' | 'standard'
30
+ export type ThemeMotion = 'reduced' | 'full'
31
+ export type ThemeTransparency = 'reduced' | 'full'
32
+
29
33
  export interface ThemePreferences {
30
34
  accent: AccentColor
31
35
  contrast: PreferenceOverride
@@ -12,10 +12,17 @@ export default defineNuxtConfig({
12
12
 
13
13
  plugins: ['#layers/theme/app/plugins/theme.client.ts'],
14
14
 
15
+ colorMode: {
16
+ dataValue: 'theme-mode',
17
+ storageKey: 'theme-mode',
18
+ },
19
+
15
20
  appConfig: {
16
21
  ui: {
17
22
  colors: {
18
23
  primary: 'blue',
24
+ secondary: 'indigo',
25
+ info: 'sky',
19
26
  },
20
27
  },
21
28
  themeLayer: {
@@ -0,0 +1,44 @@
1
+ export default defineAppConfig({
2
+ uiLayer: {
3
+ gradients: {
4
+ brand: { shape: 'linear', direction: 'to-br', from: { color: 'primary', shade: 500 }, to: { color: 'secondary', shade: 600 } },
5
+ subtle: { shape: 'linear', direction: 'to-b', from: { color: 'primary', shade: 100, opacity: 50 }, to: { color: 'transparent' } },
6
+ hero: { shape: 'radial', from: { color: 'primary', shade: 400, opacity: 40 }, to: { color: 'transparent' } },
7
+ },
8
+ accentScenes: {
9
+ hero: {
10
+ blobs: [
11
+ { x: 20, y: 30, size: '50rem', blur: '3xl', opacity: 25, color: 'primary', shade: 400 },
12
+ { x: 80, y: 70, size: '40rem', blur: '3xl', opacity: 20, color: 'secondary', shade: 500 },
13
+ ],
14
+ },
15
+ corner: {
16
+ blobs: [
17
+ { x: 100, y: 0, size: '35rem', blur: '2xl', opacity: 30, color: 'primary', shade: 500 },
18
+ ],
19
+ },
20
+ scattered: {
21
+ blobs: [
22
+ { x: 15, y: 20, size: '30rem', blur: 'xl', opacity: 20, color: 'primary', shade: 400 },
23
+ { x: 85, y: 30, size: '25rem', blur: 'xl', opacity: 15, color: 'secondary', shade: 400 },
24
+ { x: 50, y: 80, size: '35rem', blur: '2xl', opacity: 18, color: 'info', shade: 400 },
25
+ ],
26
+ },
27
+ minimal: {
28
+ blobs: [
29
+ { x: 50, y: 50, size: '60rem', blur: '3xl', opacity: 10, color: 'primary', shade: 500 },
30
+ ],
31
+ },
32
+ },
33
+ },
34
+ })
35
+
36
+ declare module '@nuxt/schema' {
37
+ interface AppConfigInput {
38
+ uiLayer?: {
39
+ name?: string
40
+ gradients?: Record<string, import('./types/gradient').GradientConfig>
41
+ accentScenes?: Record<string, import('./types/accent').AccentSceneConfig>
42
+ }
43
+ }
44
+ }
@@ -2,3 +2,17 @@
2
2
  background-color: var(--ui-bg);
3
3
  color: var(--ui-text);
4
4
  }
5
+
6
+ .gradient-text {
7
+ -webkit-background-clip: text;
8
+ background-clip: text;
9
+ color: transparent;
10
+ }
11
+
12
+ .accent-blob {
13
+ transition: background-color 400ms ease, opacity 300ms ease;
14
+ }
15
+
16
+ [data-theme-transparency="reduced"] .accent-blob {
17
+ opacity: 0 !important;
18
+ }
@@ -0,0 +1,29 @@
1
+ <script setup lang="ts">
2
+ import type { AccentBlobColor, BlobBlur } from '../../types/accent'
3
+
4
+ const props = withDefaults(
5
+ defineProps<{
6
+ x: number
7
+ y: number
8
+ size?: string
9
+ blur?: BlobBlur
10
+ opacity?: number
11
+ color?: AccentBlobColor
12
+ shade?: 50 | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 | 950
13
+ customColor?: string
14
+ }>(),
15
+ {
16
+ size: '40rem',
17
+ blur: '3xl',
18
+ opacity: 25,
19
+ color: 'primary',
20
+ shade: 500,
21
+ }
22
+ )
23
+
24
+ const { style } = useAccentBlob(props)
25
+ </script>
26
+
27
+ <template>
28
+ <div class="accent-blob" :style="style" aria-hidden="true" />
29
+ </template>
@@ -0,0 +1,38 @@
1
+ <script setup lang="ts">
2
+ import type { AccentSceneConfig, BlobConfig } from '../../types/accent'
3
+
4
+ const props = withDefaults(
5
+ defineProps<{
6
+ preset?: string
7
+ blobs?: BlobConfig[]
8
+ tag?: string
9
+ }>(),
10
+ {
11
+ tag: 'div',
12
+ }
13
+ )
14
+
15
+ const appConfig = useAppConfig()
16
+
17
+ const resolvedBlobs = computed((): BlobConfig[] => {
18
+ if (props.blobs) return props.blobs
19
+ if (props.preset) {
20
+ const scenes = (appConfig.uiLayer as Record<string, unknown> | undefined)?.accentScenes as
21
+ | Record<string, AccentSceneConfig>
22
+ | undefined
23
+ return scenes?.[props.preset]?.blobs ?? []
24
+ }
25
+ return []
26
+ })
27
+ </script>
28
+
29
+ <template>
30
+ <component :is="tag" class="relative overflow-clip isolate">
31
+ <div class="absolute inset-0 pointer-events-none" aria-hidden="true">
32
+ <AccentBlob v-for="(blob, i) in resolvedBlobs" :key="i" v-bind="blob" />
33
+ </div>
34
+ <div class="relative z-10">
35
+ <slot />
36
+ </div>
37
+ </component>
38
+ </template>
@@ -0,0 +1,22 @@
1
+ <script setup lang="ts">
2
+ import type { GradientConfig } from '../../types/gradient'
3
+
4
+ const props = withDefaults(
5
+ defineProps<{
6
+ preset?: string
7
+ config?: GradientConfig
8
+ tag?: string
9
+ }>(),
10
+ {
11
+ tag: 'div',
12
+ }
13
+ )
14
+
15
+ const { style } = useGradient(computed(() => props.preset ?? props.config ?? 'brand'))
16
+ </script>
17
+
18
+ <template>
19
+ <component :is="tag" :style="style">
20
+ <slot />
21
+ </component>
22
+ </template>
@@ -0,0 +1,22 @@
1
+ <script setup lang="ts">
2
+ import type { GradientConfig } from '../../types/gradient'
3
+
4
+ const props = withDefaults(
5
+ defineProps<{
6
+ preset?: string
7
+ config?: GradientConfig
8
+ tag?: string
9
+ }>(),
10
+ {
11
+ tag: 'span',
12
+ }
13
+ )
14
+
15
+ const { style } = useGradient(computed(() => props.preset ?? props.config ?? 'brand'))
16
+ </script>
17
+
18
+ <template>
19
+ <component :is="tag" :style="style" class="gradient-text inline-block">
20
+ <slot />
21
+ </component>
22
+ </template>
@@ -0,0 +1,25 @@
1
+ <script setup lang="ts">
2
+ const {
3
+ progress = null,
4
+ color,
5
+ size,
6
+ orientation,
7
+ status,
8
+ animation,
9
+ inverted,
10
+ } = defineProps<{
11
+ progress?: number | null
12
+ color?: 'primary' | 'secondary' | 'success' | 'info' | 'warning' | 'error' | 'neutral'
13
+ size?: '2xs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl'
14
+ orientation?: 'horizontal' | 'vertical'
15
+ status?: boolean
16
+ animation?: 'carousel' | 'carousel-inverse' | 'swing' | 'elastic'
17
+ inverted?: boolean
18
+ }>()
19
+
20
+ const modelValue = computed(() => (progress != null ? progress * 100 : undefined))
21
+ </script>
22
+
23
+ <template>
24
+ <UProgress :model-value="modelValue" :color :size :orientation :status :animation :inverted />
25
+ </template>