kmcom-nuxt-layers 6.2.0 → 6.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/MIGRATION-v6.3.0.md +234 -0
- package/docs/MIGRATION.md +2 -0
- package/docs/component-levels.md +116 -0
- package/layers/content/app/components/Blog/ItemPage.vue +2 -7
- package/layers/content/app/components/Blog/List.vue +9 -2
- package/layers/content/app/components/Blog/Page.vue +9 -19
- package/layers/content/app/components/Gallery/Detail.vue +1 -0
- package/layers/content/app/components/Gallery/Grid.vue +9 -2
- package/layers/content/app/components/Gallery/ImageDetail.vue +0 -6
- package/layers/content/app/components/Gallery/ImagePage.vue +10 -6
- package/layers/content/app/components/Gallery/Page.vue +9 -19
- package/layers/content/app/components/Gallery/SlugPage.vue +2 -7
- package/layers/content/app/components/NuxtContent/Detail.vue +20 -11
- package/layers/content/app/components/NuxtContent/IndexTemplate.vue +21 -0
- package/layers/content/app/components/NuxtContent/List.vue +6 -1
- package/layers/content/app/components/Portfolio/ColorPalette.vue +2 -2
- package/layers/content/app/components/Portfolio/ItemPage.vue +2 -6
- package/layers/content/app/components/Portfolio/List.vue +19 -8
- package/layers/content/app/components/Portfolio/Page.vue +9 -19
- package/layers/content/app/composables/useContentItemSeo.ts +19 -0
- package/layers/content/app/composables/useContentSection.ts +9 -0
- package/layers/content/app/utils/galleryImageMeta.test.ts +30 -0
- package/layers/content/app/utils/galleryImageMeta.ts +11 -0
- package/layers/content/nuxt.config.ts +1 -1
- package/layers/core/app/assets/css/core.css +5 -11
- package/layers/core/app/assets/css/tokens/z-index.css +16 -0
- package/layers/core/app/components/DiagnosticsPage.vue +41 -41
- package/layers/core/app/components/LoadingScreen.vue +2 -0
- package/layers/delivery/feeds/app/components/Feeds/Index.vue +36 -75
- package/layers/delivery/feeds/app/components/Feeds/RouteCard.vue +6 -8
- package/layers/design-system/theming/nuxt.config.ts +1 -3
- package/layers/design-system/visual/app/components/Base/Modal.vue +34 -86
- package/layers/design-system/visual/app/components/HoverDimGroup.vue +46 -0
- package/layers/design-system/visual/app/components/Progress/Bar.vue +9 -29
- package/layers/motion/animations/app/components/Motion/Cursor.vue +1 -1
- package/layers/motion/animations/app/components/Motion/Marquee.vue +5 -0
- package/layers/motion/animations/app/components/Motion/MarqueeText.vue +2 -1
- package/layers/motion/animations/app/components/Motion/Staggered.test.ts +40 -0
- package/layers/motion/animations/app/components/Motion/Staggered.vue +37 -42
- package/layers/motion/animations/app/components/Motion/TextReveal.vue +8 -1
- package/layers/motion/animations/app/components/Motion/VelocityEffect.vue +6 -0
- package/layers/motion/animations/app/composables/useCountUp.ts +6 -0
- package/layers/motion/animations/app/composables/useCursorFollower.ts +4 -1
- package/layers/motion/animations/app/utils/pointerMotion.ts +7 -2
- package/layers/motion/scroll/app/components/Motion/HorizontalScroll.vue +9 -0
- package/layers/motion/scroll/app/components/Motion/Parallax.vue +13 -12
- package/layers/motion/scroll/app/components/Motion/PinnedSection.vue +16 -9
- package/layers/motion/scroll/app/composables/useReducedMotion.test.ts +23 -0
- package/layers/motion/scroll/app/composables/useReducedMotion.ts +14 -0
- package/layers/motion/scroll/app/plugins/locomotive-scroll.client.ts +9 -1
- package/layers/motion/scroll/app/utils/parallax.test.ts +24 -0
- package/layers/motion/scroll/app/utils/parallax.ts +16 -0
- package/layers/motion/scroll/nuxt.config.ts +1 -1
- package/layers/structure/layout/app/components/Layout/Grid/Debug.vue +1 -1
- package/layers/structure/layout/app/components/Layout/Page/index.vue +9 -10
- package/layers/structure/navigation/app/components/Links/Group.vue +12 -38
- package/layers/structure/navigation/app/components/Links/Named.vue +9 -0
- package/layers/structure/navigation/app/components/Mast/Footer.vue +1 -1
- package/layers/structure/navigation/app/components/Mast/Nav.vue +3 -1
- package/layers/structure/navigation/app/components/Mast/NavModal.vue +34 -39
- package/layers/structure/navigation/app/components/Site/Subtitle.vue +10 -0
- package/layers/structure/navigation/app/components/Site/Title.vue +8 -0
- package/layers/structure/navigation/nuxt.config.ts +7 -1
- package/package.json +1 -1
- /package/layers/{design-system/theming/app/assets/css/tokens.css → core/app/assets/css/tokens/elevation-motion.css} +0 -0
|
@@ -20,18 +20,13 @@
|
|
|
20
20
|
|
|
21
21
|
<template>
|
|
22
22
|
<div
|
|
23
|
-
class="feeds-shell relative overflow-hidden rounded-4xl border
|
|
24
|
-
style="container-type: inline-size"
|
|
23
|
+
class="feeds-shell border-default bg-muted @container relative overflow-hidden rounded-4xl border shadow-xl"
|
|
25
24
|
>
|
|
26
25
|
<div class="pointer-events-none absolute inset-0">
|
|
26
|
+
<div class="bg-primary/20 absolute top-0 -right-20 h-72 w-72 rounded-full blur-3xl" />
|
|
27
|
+
<div class="bg-secondary/10 absolute bottom-0 -left-20 h-80 w-80 rounded-full blur-3xl" />
|
|
27
28
|
<div
|
|
28
|
-
class="absolute top-0
|
|
29
|
-
/>
|
|
30
|
-
<div
|
|
31
|
-
class="absolute bottom-0 -left-20 h-80 w-80 rounded-full bg-sky-400/12 blur-3xl dark:bg-sky-500/10"
|
|
32
|
-
/>
|
|
33
|
-
<div
|
|
34
|
-
class="absolute inset-x-8 top-0 h-px bg-linear-to-r from-transparent via-orange-400/35 to-transparent"
|
|
29
|
+
class="via-primary/35 absolute inset-x-8 top-0 h-px bg-linear-to-r from-transparent to-transparent"
|
|
35
30
|
/>
|
|
36
31
|
</div>
|
|
37
32
|
|
|
@@ -39,19 +34,17 @@
|
|
|
39
34
|
<div class="feeds-split grid gap-6">
|
|
40
35
|
<div class="space-y-6">
|
|
41
36
|
<div
|
|
42
|
-
class="inline-flex items-center gap-2 rounded-full border
|
|
37
|
+
class="border-default bg-default/80 text-primary inline-flex items-center gap-2 rounded-full border px-3 py-1 text-xs font-semibold tracking-[0.35em] uppercase shadow-sm backdrop-blur"
|
|
43
38
|
>
|
|
44
39
|
<UIcon name="i-lucide-rss" class="text-sm" />
|
|
45
40
|
Feeds layer
|
|
46
41
|
</div>
|
|
47
42
|
|
|
48
43
|
<div class="space-y-3">
|
|
49
|
-
<h1
|
|
50
|
-
class="feeds-h1 max-w-3xl font-semibold tracking-tight text-slate-950 dark:text-slate-50"
|
|
51
|
-
>
|
|
44
|
+
<h1 class="feeds-h1 text-highlighted max-w-3xl font-semibold tracking-tight">
|
|
52
45
|
Feed catalog for {{ catalog.site.title }}
|
|
53
46
|
</h1>
|
|
54
|
-
<p class="feeds-body max-w-2xl leading-7
|
|
47
|
+
<p class="feeds-body text-toned max-w-2xl leading-7">
|
|
55
48
|
Configured in <code class="font-mono text-[0.9em]">app.config.ts</code>, validated
|
|
56
49
|
against <code class="font-mono text-[0.9em]">content.config.ts</code>, and rendered as
|
|
57
50
|
a human-readable index for readers and site owners.
|
|
@@ -60,82 +53,56 @@
|
|
|
60
53
|
|
|
61
54
|
<div class="feeds-cards-grid gap-3">
|
|
62
55
|
<div
|
|
63
|
-
class="
|
|
56
|
+
class="border-default bg-default/75 rounded-2xl border p-4 shadow-sm backdrop-blur"
|
|
64
57
|
>
|
|
65
|
-
<p
|
|
66
|
-
|
|
67
|
-
>
|
|
68
|
-
Default
|
|
69
|
-
</p>
|
|
70
|
-
<p class="mt-2 text-lg font-semibold text-slate-950 dark:text-slate-100">
|
|
58
|
+
<p class="text-muted text-xs font-semibold tracking-[0.3em] uppercase">Default</p>
|
|
59
|
+
<p class="text-highlighted mt-2 text-lg font-semibold">
|
|
71
60
|
{{ catalog.feed.defaultCollection }}
|
|
72
61
|
</p>
|
|
73
|
-
<p class="mt-1 text-xs
|
|
74
|
-
Shorthand routes point here.
|
|
75
|
-
</p>
|
|
62
|
+
<p class="text-muted mt-1 text-xs">Shorthand routes point here.</p>
|
|
76
63
|
</div>
|
|
77
64
|
|
|
78
65
|
<div
|
|
79
|
-
class="
|
|
66
|
+
class="border-default bg-default/75 rounded-2xl border p-4 shadow-sm backdrop-blur"
|
|
80
67
|
>
|
|
81
|
-
<p
|
|
82
|
-
|
|
83
|
-
>
|
|
84
|
-
Exposed
|
|
85
|
-
</p>
|
|
86
|
-
<p class="mt-2 text-lg font-semibold text-slate-950 dark:text-slate-100">
|
|
68
|
+
<p class="text-muted text-xs font-semibold tracking-[0.3em] uppercase">Exposed</p>
|
|
69
|
+
<p class="text-highlighted mt-2 text-lg font-semibold">
|
|
87
70
|
{{ catalog.collectionGroups.length }} collections
|
|
88
71
|
</p>
|
|
89
|
-
<p class="mt-1 text-xs
|
|
90
|
-
Published in the feed catalog.
|
|
91
|
-
</p>
|
|
72
|
+
<p class="text-muted mt-1 text-xs">Published in the feed catalog.</p>
|
|
92
73
|
</div>
|
|
93
74
|
|
|
94
75
|
<div
|
|
95
|
-
class="
|
|
76
|
+
class="border-default bg-default/75 rounded-2xl border p-4 shadow-sm backdrop-blur"
|
|
96
77
|
>
|
|
97
|
-
<p
|
|
98
|
-
|
|
99
|
-
>
|
|
100
|
-
Limit
|
|
101
|
-
</p>
|
|
102
|
-
<p class="mt-2 text-lg font-semibold text-slate-950 dark:text-slate-100">
|
|
78
|
+
<p class="text-muted text-xs font-semibold tracking-[0.3em] uppercase">Limit</p>
|
|
79
|
+
<p class="text-highlighted mt-2 text-lg font-semibold">
|
|
103
80
|
{{ catalog.feed.limit }} items
|
|
104
81
|
</p>
|
|
105
|
-
<p class="mt-1 text-xs
|
|
106
|
-
Applied to each feed response.
|
|
107
|
-
</p>
|
|
82
|
+
<p class="text-muted mt-1 text-xs">Applied to each feed response.</p>
|
|
108
83
|
</div>
|
|
109
84
|
</div>
|
|
110
85
|
|
|
111
86
|
<div
|
|
112
87
|
v-if="catalog.site.url || catalog.site.description"
|
|
113
|
-
class="flex flex-wrap items-center gap-3 rounded-2xl border
|
|
88
|
+
class="border-default bg-default/70 flex flex-wrap items-center gap-3 rounded-2xl border px-4 py-3 text-sm shadow-sm backdrop-blur"
|
|
114
89
|
>
|
|
115
|
-
<span
|
|
116
|
-
|
|
117
|
-
>
|
|
118
|
-
Site
|
|
119
|
-
</span>
|
|
120
|
-
<span v-if="catalog.site.description" class="text-slate-700 dark:text-slate-300">
|
|
90
|
+
<span class="text-muted text-xs font-semibold tracking-[0.3em] uppercase"> Site </span>
|
|
91
|
+
<span v-if="catalog.site.description" class="text-toned">
|
|
121
92
|
{{ catalog.site.description }}
|
|
122
93
|
</span>
|
|
123
94
|
<code
|
|
124
95
|
v-if="catalog.site.url"
|
|
125
|
-
class="
|
|
96
|
+
class="bg-elevated text-muted rounded-full px-2 py-1 font-mono text-xs"
|
|
126
97
|
>
|
|
127
98
|
{{ catalog.site.url }}
|
|
128
99
|
</code>
|
|
129
100
|
</div>
|
|
130
101
|
</div>
|
|
131
102
|
|
|
132
|
-
<aside
|
|
133
|
-
class="rounded-[1.5rem] border border-slate-200/80 bg-white/75 p-4 shadow-sm backdrop-blur dark:border-slate-800 dark:bg-slate-900/45"
|
|
134
|
-
>
|
|
103
|
+
<aside class="border-default bg-default/75 rounded-3xl border p-4 shadow-sm backdrop-blur">
|
|
135
104
|
<div class="flex items-center justify-between gap-3">
|
|
136
|
-
<p
|
|
137
|
-
class="text-[0.68rem] font-semibold tracking-[0.35em] text-slate-500 uppercase dark:text-slate-400"
|
|
138
|
-
>
|
|
105
|
+
<p class="text-muted text-xs font-semibold tracking-[0.35em] uppercase">
|
|
139
106
|
Global routes
|
|
140
107
|
</p>
|
|
141
108
|
<UBadge color="neutral" variant="subtle" size="xs">
|
|
@@ -161,18 +128,14 @@
|
|
|
161
128
|
<div class="space-y-4">
|
|
162
129
|
<div class="flex flex-wrap items-end justify-between gap-4">
|
|
163
130
|
<div>
|
|
164
|
-
<p
|
|
165
|
-
class="text-[0.68rem] font-semibold tracking-[0.35em] text-slate-500 uppercase dark:text-slate-400"
|
|
166
|
-
>
|
|
131
|
+
<p class="text-muted text-xs font-semibold tracking-[0.35em] uppercase">
|
|
167
132
|
Collection routes
|
|
168
133
|
</p>
|
|
169
|
-
<h2
|
|
170
|
-
class="mt-2 text-2xl font-semibold tracking-tight text-slate-950 dark:text-slate-100"
|
|
171
|
-
>
|
|
134
|
+
<h2 class="text-highlighted mt-2 text-2xl font-semibold tracking-tight">
|
|
172
135
|
Each exposed collection gets RSS, Atom, and JSON.
|
|
173
136
|
</h2>
|
|
174
137
|
</div>
|
|
175
|
-
<p class="max-w-lg text-sm
|
|
138
|
+
<p class="text-muted max-w-lg text-sm">
|
|
176
139
|
Add or remove collections in <code class="font-mono text-[0.9em]">app.config.ts</code>
|
|
177
140
|
and the catalog updates automatically.
|
|
178
141
|
</p>
|
|
@@ -183,21 +146,19 @@
|
|
|
183
146
|
v-for="group in catalog.collectionGroups"
|
|
184
147
|
:key="group.collection"
|
|
185
148
|
:ui="{ body: 'p-0' }"
|
|
186
|
-
class="
|
|
149
|
+
class="border-default bg-default/80 overflow-hidden shadow-sm backdrop-blur"
|
|
187
150
|
>
|
|
188
151
|
<div
|
|
189
|
-
class="flex flex-wrap items-start justify-between gap-4 border-b
|
|
152
|
+
class="border-default flex flex-wrap items-start justify-between gap-4 border-b px-5 py-4"
|
|
190
153
|
>
|
|
191
154
|
<div>
|
|
192
|
-
<p
|
|
193
|
-
class="text-[0.68rem] font-semibold tracking-[0.35em] text-slate-500 uppercase dark:text-slate-400"
|
|
194
|
-
>
|
|
155
|
+
<p class="text-muted text-xs font-semibold tracking-[0.35em] uppercase">
|
|
195
156
|
Collection
|
|
196
157
|
</p>
|
|
197
|
-
<h3 class="mt-2 text-xl font-semibold
|
|
158
|
+
<h3 class="text-highlighted mt-2 text-xl font-semibold">
|
|
198
159
|
{{ group.label }}
|
|
199
160
|
</h3>
|
|
200
|
-
<p class="mt-1 font-mono text-sm
|
|
161
|
+
<p class="text-muted mt-1 font-mono text-sm">
|
|
201
162
|
{{ group.collection }}
|
|
202
163
|
</p>
|
|
203
164
|
</div>
|
|
@@ -215,13 +176,13 @@
|
|
|
215
176
|
<UCard
|
|
216
177
|
v-else
|
|
217
178
|
:ui="{ body: 'p-0' }"
|
|
218
|
-
class="border-
|
|
179
|
+
class="border-accented bg-default/70 border-dashed shadow-none"
|
|
219
180
|
>
|
|
220
181
|
<div class="space-y-2 p-5">
|
|
221
|
-
<p class="text-sm font-medium
|
|
182
|
+
<p class="text-highlighted text-sm font-medium">
|
|
222
183
|
No collection feeds are currently exposed.
|
|
223
184
|
</p>
|
|
224
|
-
<p class="text-
|
|
185
|
+
<p class="text-muted text-sm">
|
|
225
186
|
Add page collections to
|
|
226
187
|
<code class="font-mono text-[0.9em]">feedsLayer.feed.collections</code>
|
|
227
188
|
in <code class="font-mono text-[0.9em]">app.config.ts</code>.
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
}
|
|
17
17
|
> = {
|
|
18
18
|
index: {
|
|
19
|
-
strip: 'bg-
|
|
20
|
-
dot: 'bg-
|
|
19
|
+
strip: 'bg-accented',
|
|
20
|
+
dot: 'bg-inverted/40',
|
|
21
21
|
},
|
|
22
22
|
rss: {
|
|
23
23
|
strip: 'bg-orange-400',
|
|
@@ -50,23 +50,21 @@
|
|
|
50
50
|
<UCard
|
|
51
51
|
:to="route.path"
|
|
52
52
|
:ui="{ body: 'p-0' }"
|
|
53
|
-
class="group @container relative overflow-hidden
|
|
53
|
+
class="group border-default bg-default/90 hover:border-accented @container relative overflow-hidden shadow-none transition duration-200 hover:-translate-y-0.5 hover:shadow-md"
|
|
54
54
|
>
|
|
55
55
|
<div class="absolute inset-y-0 left-0 w-1" :class="routeTone.strip" />
|
|
56
56
|
<div :class="compact ? 'flex items-center gap-3 p-3' : 'flex items-center gap-3 p-4'">
|
|
57
57
|
<span class="h-2.5 w-2.5 rounded-full" :class="routeTone.dot" />
|
|
58
58
|
<div class="min-w-0 flex-1">
|
|
59
59
|
<div class="flex items-start justify-between gap-3">
|
|
60
|
-
<p class="
|
|
60
|
+
<p class="text-highlighted font-medium">
|
|
61
61
|
{{ route.label }}
|
|
62
62
|
</p>
|
|
63
|
-
<code
|
|
64
|
-
class="shrink-0 rounded-full bg-slate-100 px-2 py-1 font-mono text-[0.72rem] text-slate-600 dark:bg-slate-800 dark:text-slate-300"
|
|
65
|
-
>
|
|
63
|
+
<code class="bg-elevated text-muted shrink-0 rounded-full px-2 py-1 font-mono text-xs">
|
|
66
64
|
{{ route.path }}
|
|
67
65
|
</code>
|
|
68
66
|
</div>
|
|
69
|
-
<p class="mt-1 text-xs
|
|
67
|
+
<p class="text-muted mt-1 text-xs">
|
|
70
68
|
{{ getRouteDescription() }}
|
|
71
69
|
</p>
|
|
72
70
|
</div>
|
|
@@ -25,9 +25,7 @@ export default defineNuxtConfig({
|
|
|
25
25
|
'#layers/theming/types': `${import.meta.dirname}/app/types`,
|
|
26
26
|
},
|
|
27
27
|
|
|
28
|
-
// tokens
|
|
29
|
-
// CSS single-build-root rule (see .claude/rules/nuxt-layers.md) it must fold into core.css's
|
|
30
|
-
// @import chain via relative path instead, or its @theme block is silently inert.
|
|
28
|
+
// Shadow/duration tokens live in core (core/app/assets/css/tokens/elevation-motion.css).
|
|
31
29
|
css: ['#layers/theming/app/assets/css/theme.css'],
|
|
32
30
|
|
|
33
31
|
plugins: ['#layers/theming/app/plugins/theme.client.ts'],
|
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
/**
|
|
3
|
-
* BaseModal — overlay-compatible modal
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* 1. Copy or extend this component
|
|
7
|
-
* 2. Add your props and content in the slots
|
|
8
|
-
* 3. Register with createModal() in a composable
|
|
3
|
+
* BaseModal — overlay-compatible modal preset on UModal, which supplies the dialog role,
|
|
4
|
+
* focus trap, Escape, portal and transitions. This adds a size scale and a footer slot that
|
|
5
|
+
* receives `dismiss`.
|
|
9
6
|
*
|
|
10
7
|
* Example:
|
|
11
8
|
* // components/ConfirmModal.vue
|
|
@@ -31,13 +28,13 @@
|
|
|
31
28
|
close: []
|
|
32
29
|
}>()
|
|
33
30
|
|
|
34
|
-
const sizeClass
|
|
35
|
-
sm: 'max-w-sm',
|
|
36
|
-
md: 'max-w-md',
|
|
37
|
-
lg: 'max-w-lg',
|
|
38
|
-
xl: 'max-w-xl',
|
|
39
|
-
full: '
|
|
40
|
-
}
|
|
31
|
+
const sizeClass = {
|
|
32
|
+
sm: 'sm:max-w-sm',
|
|
33
|
+
md: 'sm:max-w-md',
|
|
34
|
+
lg: 'sm:max-w-lg',
|
|
35
|
+
xl: 'sm:max-w-xl',
|
|
36
|
+
full: '',
|
|
37
|
+
} as const
|
|
41
38
|
|
|
42
39
|
function dismiss() {
|
|
43
40
|
emit('update:open', false)
|
|
@@ -46,77 +43,28 @@
|
|
|
46
43
|
</script>
|
|
47
44
|
|
|
48
45
|
<template>
|
|
49
|
-
<
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
<p v-if="description" class="text-muted mt-0.5 text-sm">
|
|
74
|
-
{{ description }}
|
|
75
|
-
</p>
|
|
76
|
-
</div>
|
|
77
|
-
</slot>
|
|
78
|
-
<button
|
|
79
|
-
class="text-muted hover:text-default ml-4 cursor-pointer border-0 bg-transparent p-1 transition-colors"
|
|
80
|
-
aria-label="Close"
|
|
81
|
-
@click="dismiss"
|
|
82
|
-
>
|
|
83
|
-
<UIcon name="lucide:x" class="size-5" />
|
|
84
|
-
</button>
|
|
85
|
-
</div>
|
|
86
|
-
|
|
87
|
-
<!-- Body -->
|
|
88
|
-
<div class="px-6 py-5">
|
|
89
|
-
<slot />
|
|
90
|
-
</div>
|
|
91
|
-
|
|
92
|
-
<!-- Footer -->
|
|
93
|
-
<div v-if="$slots.footer" class="border-default flex justify-end gap-3 border-t px-6 py-4">
|
|
94
|
-
<slot name="footer" :dismiss />
|
|
95
|
-
</div>
|
|
96
|
-
</div>
|
|
97
|
-
</div>
|
|
98
|
-
</Transition>
|
|
46
|
+
<UModal
|
|
47
|
+
:open
|
|
48
|
+
v-bind="{
|
|
49
|
+
...(title !== undefined && { title }),
|
|
50
|
+
...(description !== undefined && { description }),
|
|
51
|
+
}"
|
|
52
|
+
:fullscreen="size === 'full'"
|
|
53
|
+
:ui="{
|
|
54
|
+
overlay: 'z-(--z-modal)',
|
|
55
|
+
content: `z-(--z-modal) ${sizeClass[size]}`,
|
|
56
|
+
footer: 'justify-end gap-3',
|
|
57
|
+
}"
|
|
58
|
+
@update:open="(value) => !value && dismiss()"
|
|
59
|
+
>
|
|
60
|
+
<template v-if="$slots.header" #header>
|
|
61
|
+
<slot name="header" />
|
|
62
|
+
</template>
|
|
63
|
+
<template v-if="$slots.default" #body>
|
|
64
|
+
<slot />
|
|
65
|
+
</template>
|
|
66
|
+
<template v-if="$slots.footer" #footer>
|
|
67
|
+
<slot name="footer" :dismiss />
|
|
68
|
+
</template>
|
|
69
|
+
</UModal>
|
|
99
70
|
</template>
|
|
100
|
-
|
|
101
|
-
<style scoped>
|
|
102
|
-
.base-modal-enter-active,
|
|
103
|
-
.base-modal-leave-active {
|
|
104
|
-
transition: opacity 200ms ease;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
.base-modal-enter-from,
|
|
108
|
-
.base-modal-leave-to {
|
|
109
|
-
opacity: 0;
|
|
110
|
-
}
|
|
111
|
-
.base-modal-enter-active .bg-\(--ui-bg\),
|
|
112
|
-
.base-modal-leave-active .bg-\(--ui-bg\) {
|
|
113
|
-
transition:
|
|
114
|
-
opacity 200ms ease,
|
|
115
|
-
transform 200ms ease;
|
|
116
|
-
}
|
|
117
|
-
.base-modal-enter-from .bg-\(--ui-bg\),
|
|
118
|
-
.base-modal-leave-to .bg-\(--ui-bg\) {
|
|
119
|
-
transform: scale(0.96) translateY(4px);
|
|
120
|
-
opacity: 0;
|
|
121
|
-
}
|
|
122
|
-
</style>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cloneVNode, defineComponent, Fragment, h, ref, type VNode } from 'vue'
|
|
3
|
+
|
|
4
|
+
export default defineComponent({
|
|
5
|
+
name: 'HoverDimGroup',
|
|
6
|
+
inheritAttrs: false,
|
|
7
|
+
props: {
|
|
8
|
+
tag: { type: String, default: 'div' },
|
|
9
|
+
dimOpacity: { type: Number, default: 0.3 },
|
|
10
|
+
duration: { type: Number, default: 200 },
|
|
11
|
+
},
|
|
12
|
+
setup(props, { slots, attrs }) {
|
|
13
|
+
const hoveredIndex = ref<number | null>(null)
|
|
14
|
+
|
|
15
|
+
return () => {
|
|
16
|
+
const children = (slots.default?.() ?? [])
|
|
17
|
+
.flatMap((node) => (node.type === Fragment ? (node.children as VNode[]) : [node]))
|
|
18
|
+
.filter((node) => typeof node.type === 'string' || typeof node.type === 'object')
|
|
19
|
+
|
|
20
|
+
const styledChildren = children.map((vnode, index) =>
|
|
21
|
+
cloneVNode(vnode, {
|
|
22
|
+
onMouseenter: () => {
|
|
23
|
+
hoveredIndex.value = index
|
|
24
|
+
},
|
|
25
|
+
style: {
|
|
26
|
+
opacity:
|
|
27
|
+
hoveredIndex.value !== null && hoveredIndex.value !== index ? props.dimOpacity : 1,
|
|
28
|
+
transition: `opacity ${props.duration}ms ease-out`,
|
|
29
|
+
},
|
|
30
|
+
})
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
return h(
|
|
34
|
+
props.tag,
|
|
35
|
+
{
|
|
36
|
+
...attrs,
|
|
37
|
+
onMouseleave: () => {
|
|
38
|
+
hoveredIndex.value = null
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
styledChildren
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
})
|
|
46
|
+
</script>
|
|
@@ -1,35 +1,15 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
|
|
3
|
-
progress = null,
|
|
4
|
-
color = undefined,
|
|
5
|
-
size = undefined,
|
|
6
|
-
orientation = undefined,
|
|
7
|
-
status = undefined,
|
|
8
|
-
animation = undefined,
|
|
9
|
-
inverted = undefined,
|
|
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
|
-
}>()
|
|
2
|
+
import type { ProgressProps } from '@nuxt/ui'
|
|
19
3
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
...(status !== undefined && { status }),
|
|
28
|
-
...(animation !== undefined && { animation }),
|
|
29
|
-
...(inverted !== undefined && { inverted }),
|
|
30
|
-
}))
|
|
4
|
+
// The one decision over UProgress: progress is 0–1 (null = indeterminate). Every other
|
|
5
|
+
// UProgress prop stays typed here but falls through as an attr (@vue-ignore).
|
|
6
|
+
/* eslint-disable vue/no-unused-properties -- @vue-ignore props fall through as attrs */
|
|
7
|
+
const { progress = null } = defineProps<
|
|
8
|
+
{ progress?: number | null } & /* @vue-ignore */ Omit<ProgressProps, 'modelValue'>
|
|
9
|
+
>()
|
|
10
|
+
/* eslint-enable vue/no-unused-properties */
|
|
31
11
|
</script>
|
|
32
12
|
|
|
33
13
|
<template>
|
|
34
|
-
<UProgress
|
|
14
|
+
<UProgress :model-value="progress == null ? null : progress * 100" />
|
|
35
15
|
</template>
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
<template>
|
|
53
53
|
<Teleport to="body">
|
|
54
54
|
<div
|
|
55
|
-
class="motion-cursor pointer-events-none fixed inset-0 z-
|
|
55
|
+
class="motion-cursor pointer-events-none fixed inset-0 z-(--z-cursor) transition-opacity duration-300"
|
|
56
56
|
:style="{ opacity: visible ? 1 : 0 }"
|
|
57
57
|
aria-hidden="true"
|
|
58
58
|
>
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
}>()
|
|
51
51
|
|
|
52
52
|
const { gsap } = useGsap()
|
|
53
|
+
const reduced = useReducedMotion()
|
|
53
54
|
const { velocity: scrollVelocity, direction: scrollDirection } = useSmoothScroll()
|
|
54
55
|
|
|
55
56
|
const containerRef = useTemplateRef<HTMLElement>('containerRef')
|
|
@@ -78,9 +79,13 @@
|
|
|
78
79
|
return direction === 'left' ? parseFloat(x) % mod : Math.abs(parseFloat(x) % mod)
|
|
79
80
|
}),
|
|
80
81
|
},
|
|
82
|
+
paused: reduced.value,
|
|
81
83
|
})
|
|
82
84
|
})
|
|
83
85
|
|
|
86
|
+
// Reduced motion holds the marquee still (and resumes it if the preference is turned off)
|
|
87
|
+
watch(reduced, (r) => tweenRef.value?.paused(r))
|
|
88
|
+
|
|
84
89
|
// Watch velocity changes for velocity-based animation
|
|
85
90
|
watch(
|
|
86
91
|
[scrollVelocity, scrollDirection],
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
}>()
|
|
34
34
|
|
|
35
35
|
const { gsap, ScrollTrigger } = useGsap()
|
|
36
|
+
const reduced = useReducedMotion()
|
|
36
37
|
const { velocityFactor } = useMarqueeVelocity({ damping, stiffness, velocityMapping })
|
|
37
38
|
|
|
38
39
|
const containerRef = useTemplateRef<HTMLElement[]>('containerRef')
|
|
@@ -71,7 +72,7 @@
|
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
function animate() {
|
|
74
|
-
if (isPaused.value) return
|
|
75
|
+
if (isPaused.value || reduced.value) return
|
|
75
76
|
|
|
76
77
|
const now = performance.now()
|
|
77
78
|
const delta = animLastTime ? now - animLastTime : 16
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { mount } from '@vue/test-utils'
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vite-plus/test'
|
|
4
|
+
|
|
5
|
+
import Staggered from './Staggered.vue'
|
|
6
|
+
|
|
7
|
+
// Auto-imported from the scroll layer in Nuxt; stubbed here (no Nuxt context in the 'vue' project)
|
|
8
|
+
const reduced = ref(false)
|
|
9
|
+
vi.stubGlobal('useReducedMotion', () => reduced)
|
|
10
|
+
|
|
11
|
+
describe('MotionStaggered', () => {
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
reduced.value = false
|
|
14
|
+
vi.useFakeTimers()
|
|
15
|
+
})
|
|
16
|
+
afterEach(() => vi.useRealTimers())
|
|
17
|
+
|
|
18
|
+
it('renders each child once and reveals every one after the stagger', () => {
|
|
19
|
+
const wrapper = mount(Staggered, {
|
|
20
|
+
props: { staggerDelay: 50 },
|
|
21
|
+
slots: { default: '<p>a</p><p>b</p><p>c</p>' },
|
|
22
|
+
})
|
|
23
|
+
const items = wrapper.findAll('p')
|
|
24
|
+
expect(items).toHaveLength(3)
|
|
25
|
+
expect(items.every((p) => !p.classes('animated'))).toBe(true)
|
|
26
|
+
|
|
27
|
+
vi.advanceTimersByTime(100)
|
|
28
|
+
expect(items.map((p) => p.classes('animated'))).toEqual([true, true, true])
|
|
29
|
+
expect(items[0]?.classes()).toContain('animate-fade-in')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('reveals every child at once, without the animation class, under reduced motion', () => {
|
|
33
|
+
reduced.value = true
|
|
34
|
+
const wrapper = mount(Staggered, { slots: { default: '<p>a</p><p>b</p>' } })
|
|
35
|
+
const items = wrapper.findAll('p')
|
|
36
|
+
expect(items.map((p) => p.classes('animated'))).toEqual([true, true])
|
|
37
|
+
expect(items[0]?.classes()).not.toContain('animate-fade-in')
|
|
38
|
+
expect(wrapper.attributes('data-reduced')).toBe('')
|
|
39
|
+
})
|
|
40
|
+
})
|