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.
- package/layers/content/app/components/Blog/List.vue +5 -1
- package/layers/content/app/components/Gallery/AmbientImage.vue +5 -12
- package/layers/content/app/components/Gallery/Detail.vue +8 -6
- package/layers/content/app/components/Gallery/Grid.vue +11 -3
- package/layers/content/app/components/Portfolio/ColorPalette.vue +1 -4
- package/layers/content/app/components/Portfolio/Detail.vue +6 -1
- package/layers/content/app/components/Portfolio/List.vue +5 -1
- package/layers/content/app/components/content/Figure.vue +1 -7
- package/layers/content/nuxt.config.ts +16 -5
- package/layers/content/package.json +5 -5
- package/layers/core/app/assets/css/main.css +5 -0
- package/layers/core/app/composables/useCache.ts +8 -4
- package/layers/core/app/composables/useErrorLog.ts +9 -5
- package/layers/core/app/composables/useScrollGuard.ts +4 -2
- package/layers/core/app/plugins/feature-detection.client.ts +1 -1
- package/layers/core/app/plugins/init.ts +2 -1
- package/layers/core/app/plugins/scroll-guard.client.ts +4 -1
- package/layers/core/app.config.ts +0 -9
- package/layers/forms/app/components/Form/Contact.vue +16 -7
- package/layers/forms/nuxt.config.ts +18 -0
- package/layers/forms/package.json +2 -0
- package/layers/layout/app/components/Layout/Container.vue +1 -4
- package/layers/layout/app/components/Layout/Grid/Debug.vue +0 -1
- package/layers/layout/app/components/Layout/Grid/Item.vue +12 -6
- package/layers/layout/app/components/Layout/Main.vue +1 -4
- package/layers/layout/app/components/Layout/Page/Container.vue +3 -1
- package/layers/layout/app/components/Layout/Page/Header.vue +16 -7
- package/layers/layout/app/components/Layout/Section/Grid.vue +1 -4
- package/layers/layout/app/components/Layout/Section/Sidebar.vue +6 -1
- package/layers/layout/app/components/Layout/Section/Stack.vue +1 -1
- package/layers/layout/app/composables/useGridConfig.ts +6 -1
- package/layers/motion/app/components/Motion/HorizontalScroll.vue +61 -0
- package/layers/motion/app/components/Motion/PinnedSection.vue +77 -0
- package/layers/motion/app/components/Motion/ScrollProgress.vue +8 -56
- package/layers/motion/app/components/Motion/ScrollScene.vue +121 -0
- package/layers/motion/app/components/Motion/ScrollStep.vue +45 -0
- package/layers/motion/app/components/Motion/TextReveal.vue +28 -63
- package/layers/motion/app/composables/useScrollSteps.ts +41 -0
- package/layers/motion/app/composables/useSectionProgress.ts +58 -0
- package/layers/motion/app/composables/useSmoothScroll.ts +3 -2
- package/layers/motion/app/plugins/locomotive-scroll.client.ts +6 -6
- package/layers/motion/nuxt.config.ts +6 -0
- package/layers/motion/package.json +2 -1
- package/layers/routing/app/app.config.ts +20 -0
- package/layers/routing/app/composables/useFeatures.ts +12 -0
- package/layers/routing/app/composables/useMaintenance.ts +7 -0
- package/layers/routing/app/composables/useRoutingConfig.ts +20 -0
- package/layers/routing/app/middleware/01.maintenance.global.ts +6 -0
- package/layers/routing/app/middleware/02.governance.global.ts +25 -0
- package/layers/routing/app/plugins/feature-flags.client.ts +15 -0
- package/layers/routing/app/plugins/scroll-routing.client.ts +21 -0
- package/layers/routing/app/types/route-meta.d.ts +6 -0
- package/layers/routing/app/types/routing.ts +48 -0
- package/layers/routing/nuxt.config.ts +27 -0
- package/layers/routing/package.json +6 -0
- package/layers/shader/app/components/Preset/ThemeAurora.client.vue +86 -0
- package/layers/shader/app/components/Preset/ThemeBubble.client.vue +87 -0
- package/layers/shader/app/components/Preset/ThemeFlow.client.vue +86 -0
- package/layers/shader/app/components/Preset/ThemeGradient.client.vue +87 -0
- package/layers/shader/app/components/Preset/ThemeLavaLamp.client.vue +86 -0
- package/layers/shader/app/components/Preset/ThemePlasma.client.vue +86 -0
- package/layers/shader/app/components/Preset/ThemeWave.client.vue +86 -0
- package/layers/shader/app/components/Shader/Background.client.vue +15 -0
- package/layers/shader/app/composables/useAmbientMaterials.ts +306 -0
- package/layers/shader/app/composables/useThemeColors.ts +52 -0
- package/layers/shader/app/utils/tsl/oklch.ts +12 -6
- package/layers/theme/app/assets/css/theme.css +19 -14
- package/layers/theme/app/components/ThemePicker/AccentButton.vue +2 -2
- package/layers/theme/app/components/ThemePicker/Colors.vue +2 -4
- package/layers/theme/app/components/ThemePicker/Menu.vue +4 -13
- package/layers/theme/app/components/ThemePicker/MenuButton.vue +1 -7
- package/layers/theme/app/composables/useAccentColor.ts +38 -0
- package/layers/theme/app/composables/useTheme.ts +14 -0
- package/layers/theme/app/composables/useThemeContrast.ts +34 -0
- package/layers/theme/app/composables/useThemeMotion.ts +34 -0
- package/layers/theme/app/composables/useThemePreferences.ts +3 -156
- package/layers/theme/app/composables/useThemeTransparency.ts +41 -0
- package/layers/theme/app/plugins/theme.client.ts +3 -3
- package/layers/theme/app/types/theme.ts +4 -0
- package/layers/theme/nuxt.config.ts +7 -0
- package/layers/ui/app/app.config.ts +44 -0
- package/layers/ui/app/assets/css/main.css +14 -0
- package/layers/ui/app/components/Accent/Blob.vue +29 -0
- package/layers/ui/app/components/Accent/Scene.vue +38 -0
- package/layers/ui/app/components/Gradient/Background.vue +22 -0
- package/layers/ui/app/components/Gradient/Text.vue +22 -0
- package/layers/ui/app/components/Progress/Bar.vue +25 -0
- package/layers/ui/app/components/Progress/Circular.vue +69 -0
- package/layers/ui/app/components/Tint/Overlay.vue +25 -0
- package/layers/ui/app/components/Typography/CodeBlock.vue +2 -1
- package/layers/ui/app/components/Typography/Headline.vue +2 -1
- package/layers/ui/app/components/Typography/QuoteBlock.vue +2 -1
- package/layers/ui/app/components/Typography/TextStroke.vue +18 -16
- package/layers/ui/app/composables/accent.ts +51 -0
- package/layers/ui/app/composables/gradient.ts +79 -0
- package/layers/ui/app/composables/tint.ts +20 -0
- package/layers/ui/app/types/accent.ts +17 -0
- package/layers/ui/app/types/gradient.ts +27 -0
- package/layers/ui/app/types/tint.ts +25 -0
- package/package.json +37 -31
- package/layers/motion/app/utils/gsapAnimations.ts +0 -122
- 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-
|
|
44
|
-
: 'ring-transparent hover:ring-muted ring-offset-
|
|
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 } =
|
|
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
|
-
} =
|
|
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>
|
|
@@ -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
|
-
*
|
|
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 =
|
|
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
|
|
3
|
-
//
|
|
4
|
-
|
|
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>
|