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.
Files changed (65) hide show
  1. package/docs/MIGRATION-v6.3.0.md +234 -0
  2. package/docs/MIGRATION.md +2 -0
  3. package/docs/component-levels.md +116 -0
  4. package/layers/content/app/components/Blog/ItemPage.vue +2 -7
  5. package/layers/content/app/components/Blog/List.vue +9 -2
  6. package/layers/content/app/components/Blog/Page.vue +9 -19
  7. package/layers/content/app/components/Gallery/Detail.vue +1 -0
  8. package/layers/content/app/components/Gallery/Grid.vue +9 -2
  9. package/layers/content/app/components/Gallery/ImageDetail.vue +0 -6
  10. package/layers/content/app/components/Gallery/ImagePage.vue +10 -6
  11. package/layers/content/app/components/Gallery/Page.vue +9 -19
  12. package/layers/content/app/components/Gallery/SlugPage.vue +2 -7
  13. package/layers/content/app/components/NuxtContent/Detail.vue +20 -11
  14. package/layers/content/app/components/NuxtContent/IndexTemplate.vue +21 -0
  15. package/layers/content/app/components/NuxtContent/List.vue +6 -1
  16. package/layers/content/app/components/Portfolio/ColorPalette.vue +2 -2
  17. package/layers/content/app/components/Portfolio/ItemPage.vue +2 -6
  18. package/layers/content/app/components/Portfolio/List.vue +19 -8
  19. package/layers/content/app/components/Portfolio/Page.vue +9 -19
  20. package/layers/content/app/composables/useContentItemSeo.ts +19 -0
  21. package/layers/content/app/composables/useContentSection.ts +9 -0
  22. package/layers/content/app/utils/galleryImageMeta.test.ts +30 -0
  23. package/layers/content/app/utils/galleryImageMeta.ts +11 -0
  24. package/layers/content/nuxt.config.ts +1 -1
  25. package/layers/core/app/assets/css/core.css +5 -11
  26. package/layers/core/app/assets/css/tokens/z-index.css +16 -0
  27. package/layers/core/app/components/DiagnosticsPage.vue +41 -41
  28. package/layers/core/app/components/LoadingScreen.vue +2 -0
  29. package/layers/delivery/feeds/app/components/Feeds/Index.vue +36 -75
  30. package/layers/delivery/feeds/app/components/Feeds/RouteCard.vue +6 -8
  31. package/layers/design-system/theming/nuxt.config.ts +1 -3
  32. package/layers/design-system/visual/app/components/Base/Modal.vue +34 -86
  33. package/layers/design-system/visual/app/components/HoverDimGroup.vue +46 -0
  34. package/layers/design-system/visual/app/components/Progress/Bar.vue +9 -29
  35. package/layers/motion/animations/app/components/Motion/Cursor.vue +1 -1
  36. package/layers/motion/animations/app/components/Motion/Marquee.vue +5 -0
  37. package/layers/motion/animations/app/components/Motion/MarqueeText.vue +2 -1
  38. package/layers/motion/animations/app/components/Motion/Staggered.test.ts +40 -0
  39. package/layers/motion/animations/app/components/Motion/Staggered.vue +37 -42
  40. package/layers/motion/animations/app/components/Motion/TextReveal.vue +8 -1
  41. package/layers/motion/animations/app/components/Motion/VelocityEffect.vue +6 -0
  42. package/layers/motion/animations/app/composables/useCountUp.ts +6 -0
  43. package/layers/motion/animations/app/composables/useCursorFollower.ts +4 -1
  44. package/layers/motion/animations/app/utils/pointerMotion.ts +7 -2
  45. package/layers/motion/scroll/app/components/Motion/HorizontalScroll.vue +9 -0
  46. package/layers/motion/scroll/app/components/Motion/Parallax.vue +13 -12
  47. package/layers/motion/scroll/app/components/Motion/PinnedSection.vue +16 -9
  48. package/layers/motion/scroll/app/composables/useReducedMotion.test.ts +23 -0
  49. package/layers/motion/scroll/app/composables/useReducedMotion.ts +14 -0
  50. package/layers/motion/scroll/app/plugins/locomotive-scroll.client.ts +9 -1
  51. package/layers/motion/scroll/app/utils/parallax.test.ts +24 -0
  52. package/layers/motion/scroll/app/utils/parallax.ts +16 -0
  53. package/layers/motion/scroll/nuxt.config.ts +1 -1
  54. package/layers/structure/layout/app/components/Layout/Grid/Debug.vue +1 -1
  55. package/layers/structure/layout/app/components/Layout/Page/index.vue +9 -10
  56. package/layers/structure/navigation/app/components/Links/Group.vue +12 -38
  57. package/layers/structure/navigation/app/components/Links/Named.vue +9 -0
  58. package/layers/structure/navigation/app/components/Mast/Footer.vue +1 -1
  59. package/layers/structure/navigation/app/components/Mast/Nav.vue +3 -1
  60. package/layers/structure/navigation/app/components/Mast/NavModal.vue +34 -39
  61. package/layers/structure/navigation/app/components/Site/Subtitle.vue +10 -0
  62. package/layers/structure/navigation/app/components/Site/Title.vue +8 -0
  63. package/layers/structure/navigation/nuxt.config.ts +7 -1
  64. package/package.json +1 -1
  65. /package/layers/{design-system/theming/app/assets/css/tokens.css → core/app/assets/css/tokens/elevation-motion.css} +0 -0
@@ -3,16 +3,21 @@
3
3
  status,
4
4
  hasItems = true,
5
5
  emptyMessage = 'No items found',
6
+ errorMessage = "These items couldn't be loaded. Try again shortly.",
6
7
  } = defineProps<{
7
- status: string
8
+ status: 'idle' | 'pending' | 'success' | 'error'
8
9
  hasItems?: boolean
9
10
  emptyMessage?: string
11
+ errorMessage?: string
10
12
  }>()
11
13
  </script>
12
14
 
13
15
  <template>
14
16
  <div>
15
17
  <USkeleton v-if="status === 'pending'" class="h-64 w-full" />
18
+ <slot v-else-if="status === 'error'" name="error">
19
+ <UAlert color="error" variant="subtle" icon="lucide:circle-alert" :title="errorMessage" />
20
+ </slot>
16
21
  <template v-else-if="hasItems">
17
22
  <slot />
18
23
  </template>
@@ -17,7 +17,7 @@
17
17
  <template>
18
18
  <div v-if="colors.length" class="space-y-6">
19
19
  <h3 class="text-highlighted text-2xl font-semibold tracking-tight">Palette</h3>
20
- <LinksGroup class="palette-grid gap-3">
20
+ <HoverDimGroup class="palette-grid gap-3">
21
21
  <div v-for="color in colors" :key="color.value">
22
22
  <div
23
23
  class="border-default/20 flex aspect-2/3 cursor-default flex-col justify-between rounded-lg border p-4 shadow-lg"
@@ -30,7 +30,7 @@
30
30
  <p class="font-mono text-xs uppercase opacity-50">{{ color.value }}</p>
31
31
  </div>
32
32
  </div>
33
- </LinksGroup>
33
+ </HoverDimGroup>
34
34
  </div>
35
35
  </template>
36
36
 
@@ -1,5 +1,4 @@
1
1
  <script setup lang="ts">
2
- import type { ContentLayerConfig } from '#layers/content/app/types/content'
3
2
  import type { PageCollections } from '@nuxt/content'
4
3
 
5
4
  const { slug, collection = 'portfolio' } = defineProps<{
@@ -7,11 +6,8 @@
7
6
  collection?: keyof PageCollections
8
7
  }>()
9
8
 
10
- const appConfig = useAppConfig()
11
- const contentLayer = appConfig.contentLayer as ContentLayerConfig | undefined
12
- if (contentLayer?.sections?.portfolio === false) {
13
- throw createError({ statusCode: 404, statusMessage: 'Not Found' })
14
- }
9
+ useContentSection('portfolio')
10
+ await useContentItemSeo(collection, slug)
15
11
  </script>
16
12
 
17
13
  <template>
@@ -3,19 +3,30 @@
3
3
 
4
4
  import type { PortfolioQueryOptions } from '../../types/content'
5
5
 
6
- const { options = {}, collection = 'portfolio' } = defineProps<{
6
+ const {
7
+ items: passed = undefined,
8
+ options = {},
9
+ collection = 'portfolio',
10
+ } = defineProps<{
11
+ items?: Collections[keyof Collections][]
7
12
  options?: PortfolioQueryOptions
8
13
  collection?: keyof Collections
9
14
  }>()
10
15
 
11
16
  const { featured, tags, limit } = options
12
- const { data: items, status } = await useCollectionItems(collection, {
13
- tags,
14
- limit,
15
- sortKey: 'year',
16
- sortDirection: 'desc',
17
- filter: featured !== undefined ? (item) => fieldFeatured(item) === featured : undefined,
18
- })
17
+ // Fetch only when the parent didn't pass items (e.g. a related-items list it already has).
18
+ // ponytail: decided once at setup — toggling `items` after mount won't start or stop the fetch
19
+ const fetched = passed
20
+ ? undefined
21
+ : await useCollectionItems(collection, {
22
+ tags,
23
+ limit,
24
+ sortKey: 'year',
25
+ sortDirection: 'desc',
26
+ filter: featured !== undefined ? (item) => fieldFeatured(item) === featured : undefined,
27
+ })
28
+ const items = computed(() => passed ?? fetched?.data.value)
29
+ const status = computed(() => (passed ? 'success' : (fetched?.status.value ?? 'idle')))
19
30
 
20
31
  function fieldFeatured(item: unknown): boolean {
21
32
  return typeof item === 'object' && item !== null && 'featured' in item
@@ -1,25 +1,15 @@
1
1
  <script setup lang="ts">
2
- import type { ContentLayerConfig } from '#layers/content/app/types/content'
2
+ const { title = 'Portfolio', description = 'Featured projects and work' } = defineProps<{
3
+ title?: string
4
+ description?: string
5
+ }>()
3
6
 
4
- const appConfig = useAppConfig()
5
- const contentLayer = appConfig.contentLayer as ContentLayerConfig | undefined
6
- if (contentLayer?.sections?.portfolio === false) {
7
- throw createError({ statusCode: 404, statusMessage: 'Not Found' })
8
- }
9
-
10
- useSeoMeta({
11
- title: 'Portfolio',
12
- description: 'Featured projects and work',
13
- })
7
+ useContentSection('portfolio')
8
+ useSeoMeta({ title: () => title, description: () => description })
14
9
  </script>
15
10
 
16
11
  <template>
17
- <LayoutSection>
18
- <LayoutGridItem>
19
- <UPageHeader title="Portfolio" description="Featured projects and work" />
20
- <UPageBody>
21
- <PortfolioList />
22
- </UPageBody>
23
- </LayoutGridItem>
24
- </LayoutSection>
12
+ <NuxtContentIndexTemplate :title :description>
13
+ <PortfolioList />
14
+ </NuxtContentIndexTemplate>
25
15
  </template>
@@ -0,0 +1,19 @@
1
+ import type { Collections } from '@nuxt/content'
2
+
3
+ /**
4
+ * Page meta for a content item. Called by the *Page components rather than the detail
5
+ * organisms, so an organism never sets page meta. Shares useCollectionItem's cache key with
6
+ * the organism that renders the item.
7
+ *
8
+ * Meta is registered before anything is awaited — Nuxt's instance is gone after an `await`
9
+ * inside a plain composable. Returns the async data so the caller can `await` it.
10
+ */
11
+ export function useContentItemSeo(collection: keyof Collections, slug: string) {
12
+ const result = useCollectionItem(collection, slug)
13
+ useSeoMeta({
14
+ title: () => result.data.value?.title,
15
+ description: () => result.data.value?.description,
16
+ ogImage: () => (result.data.value as { image?: string } | null)?.image,
17
+ })
18
+ return result
19
+ }
@@ -0,0 +1,9 @@
1
+ import type { ContentLayerConfig } from '../types/content'
2
+
3
+ /** 404s the current route when `contentLayer.sections.<section>` is switched off in app.config. */
4
+ export function useContentSection(section: keyof NonNullable<ContentLayerConfig['sections']>) {
5
+ const contentLayer = useAppConfig().contentLayer as ContentLayerConfig | undefined
6
+ if (contentLayer?.sections?.[section] === false) {
7
+ throw createError({ statusCode: 404, statusMessage: 'Not Found' })
8
+ }
9
+ }
@@ -0,0 +1,30 @@
1
+ import type { GalleryCollectionItem } from '@nuxt/content'
2
+ import { describe, expect, it } from 'vite-plus/test'
3
+
4
+ import { galleryImageMeta } from './galleryImageMeta'
5
+
6
+ const gallery = (images: Array<Record<string, string>>) =>
7
+ ({ title: 'Autumn', images }) as unknown as GalleryCollectionItem
8
+
9
+ describe('galleryImageMeta', () => {
10
+ it("prefers the image's own title and caption", () => {
11
+ const item = gallery([{ src: '/a.jpg', alt: 'Leaf', title: 'Red leaf', caption: 'Oct' }])
12
+ expect(galleryImageMeta(item, 0)).toEqual({
13
+ title: 'Red leaf',
14
+ description: 'Oct',
15
+ ogImage: '/a.jpg',
16
+ })
17
+ })
18
+
19
+ it('falls back to alt text, then the gallery title', () => {
20
+ expect(galleryImageMeta(gallery([{ src: '/a.jpg', alt: 'Leaf' }]), 0)).toMatchObject({
21
+ title: 'Leaf',
22
+ description: 'Autumn',
23
+ })
24
+ })
25
+
26
+ it('names a missing image by its position', () => {
27
+ expect(galleryImageMeta(gallery([]), 2).title).toBe('Image 3')
28
+ expect(galleryImageMeta(null, 0).title).toBe('Image 1')
29
+ })
30
+ })
@@ -0,0 +1,11 @@
1
+ import type { GalleryCollectionItem } from '@nuxt/content'
2
+
3
+ /** Page meta for one image of a gallery: its own title/caption, falling back to the gallery's. */
4
+ export function galleryImageMeta(item: GalleryCollectionItem | null | undefined, index: number) {
5
+ const image = item?.images?.[index]
6
+ return {
7
+ title: image?.title || image?.alt || `Image ${index + 1}`,
8
+ description: image?.caption || item?.title,
9
+ ogImage: image?.src,
10
+ }
11
+ }
@@ -13,7 +13,7 @@ export default defineNuxtConfig({
13
13
  name: 'content',
14
14
  },
15
15
 
16
- extends: ['../core'],
16
+ extends: ['../core', '../structure/layout', '../design-system/visual'],
17
17
 
18
18
  alias: {
19
19
  '#layers/content': import.meta.dirname,
@@ -33,6 +33,7 @@
33
33
  @import '#layers/core/app/assets/css/tokens/breakpoints.css';
34
34
  @import '#layers/core/app/assets/css/tokens/spacing-fluid.css';
35
35
  @import '#layers/core/app/assets/css/tokens/container.css';
36
+ @import '#layers/core/app/assets/css/tokens/z-index.css';
36
37
 
37
38
  /* Relative path, not the '#layers/typography' alias — that alias only exists
38
39
  when the typography layer is in a consumer's `extends`. This file is a CSS
@@ -41,14 +42,7 @@
41
42
  only extend core, since it lives in the same single published package. */
42
43
  @import '../../../../design-system/typography/app/assets/css/typography.css';
43
44
 
44
- /* Same relative-path requirement as typography.css above. tokens.css defines
45
- an `@theme static` block (radius/shadow/duration) — per the CSS
46
- single-build-root rule, that only merges into Tailwind's theme if Tailwind's
47
- own PostCSS pipeline actually processes the file via @import from this one
48
- root. It previously was NOT imported here — only registered as a standalone
49
- stylesheet via theming/nuxt.config.ts's `css:` array — so its @theme block
50
- was silently inert: the browser just ignores the unrecognized at-rule, and
51
- every rounded-, shadow-, duration- utility fell back to Tailwind's stock
52
- defaults instead of this file's values. Confirmed via compiled dev output:
53
- the custom --shadow-sm value never appeared; Tailwind's built-in default did. */
54
- @import '../../../../design-system/theming/app/assets/css/tokens.css';
45
+ /* Shadow/duration tokens. Lived in theming until the 2026-10 atomic audit — moved here because
46
+ sub-atomic tokens belong to the foundation, and an @theme block only merges into Tailwind's
47
+ theme when it's in this one build root's @import chain anyway. */
48
+ @import '#layers/core/app/assets/css/tokens/elevation-motion.css';
@@ -0,0 +1,16 @@
1
+ /**
2
+ * UI stacking scale, for layers that can't reach layout's useZIndex() (it reads
3
+ * layoutLayer.ui.grid.layers from app.config). The shared steps mirror that config —
4
+ * tests/vitest/z-index-scale.test.ts fails if they drift. Use as `z-(--z-modal)`, never a raw z-9999.
5
+ */
6
+ :root {
7
+ --z-header: 100;
8
+ --z-dropdown: 200;
9
+ --z-overlay: 300;
10
+ --z-modal: 400;
11
+ --z-toast: 500;
12
+
13
+ /* Above everything interactive — not in layout's scale, only these consumers need them */
14
+ --z-cursor: 600;
15
+ --z-debug: 700;
16
+ }
@@ -31,7 +31,7 @@
31
31
  <div class="space-y-8">
32
32
  <div class="text-center">
33
33
  <h1 class="mb-2 text-4xl font-bold">Core Layer Diagnostics</h1>
34
- <p class="text-gray-500 dark:text-gray-400">Feature detection and system information</p>
34
+ <p class="text-muted">Feature detection and system information</p>
35
35
  </div>
36
36
 
37
37
  <UCard>
@@ -40,33 +40,33 @@
40
40
  </template>
41
41
  <div class="diag-grid-9 gap-3">
42
42
  <div class="flex items-center gap-2">
43
- <span v-if="grid" class="text-green-500">✓</span>
44
- <span v-else class="text-red-500">✗</span>
43
+ <span v-if="grid" class="text-success">✓</span>
44
+ <span v-else class="text-error">✗</span>
45
45
  <span>CSS Grid</span>
46
46
  </div>
47
47
  <div class="flex items-center gap-2">
48
- <span v-if="subgrid" class="text-green-500">✓</span>
49
- <span v-else class="text-red-500">✗</span>
48
+ <span v-if="subgrid" class="text-success">✓</span>
49
+ <span v-else class="text-error">✗</span>
50
50
  <span>Subgrid</span>
51
51
  </div>
52
52
  <div class="flex items-center gap-2">
53
- <span v-if="containerQueries" class="text-green-500">✓</span>
54
- <span v-else class="text-red-500">✗</span>
53
+ <span v-if="containerQueries" class="text-success">✓</span>
54
+ <span v-else class="text-error">✗</span>
55
55
  <span>Container Queries</span>
56
56
  </div>
57
57
  <div class="flex items-center gap-2">
58
- <span v-if="has" class="text-green-500">✓</span>
59
- <span v-else class="text-red-500">✗</span>
58
+ <span v-if="has" class="text-success">✓</span>
59
+ <span v-else class="text-error">✗</span>
60
60
  <span>:has() Selector</span>
61
61
  </div>
62
62
  <div class="flex items-center gap-2">
63
- <span v-if="aspectRatio" class="text-green-500">✓</span>
64
- <span v-else class="text-red-500">✗</span>
63
+ <span v-if="aspectRatio" class="text-success">✓</span>
64
+ <span v-else class="text-error">✗</span>
65
65
  <span>Aspect Ratio</span>
66
66
  </div>
67
67
  <div class="flex items-center gap-2">
68
- <span v-if="backdropFilter" class="text-green-500">✓</span>
69
- <span v-else class="text-red-500">✗</span>
68
+ <span v-if="backdropFilter" class="text-success">✓</span>
69
+ <span v-else class="text-error">✗</span>
70
70
  <span>Backdrop Filter</span>
71
71
  </div>
72
72
  </div>
@@ -78,13 +78,13 @@
78
78
  </template>
79
79
  <div class="diag-grid-8 gap-3">
80
80
  <div class="flex items-center gap-2">
81
- <span v-if="webp" class="text-green-500">✓</span>
82
- <span v-else class="text-red-500">✗</span>
81
+ <span v-if="webp" class="text-success">✓</span>
82
+ <span v-else class="text-error">✗</span>
83
83
  <span>WebP</span>
84
84
  </div>
85
85
  <div class="flex items-center gap-2">
86
- <span v-if="avif" class="text-green-500">✓</span>
87
- <span v-else class="text-red-500">✗</span>
86
+ <span v-if="avif" class="text-success">✓</span>
87
+ <span v-else class="text-error">✗</span>
88
88
  <span>AVIF</span>
89
89
  </div>
90
90
  </div>
@@ -96,23 +96,23 @@
96
96
  </template>
97
97
  <div class="diag-grid-9 gap-3">
98
98
  <div class="flex items-center gap-2">
99
- <span v-if="intersectionObserver" class="text-green-500">✓</span>
100
- <span v-else class="text-red-500">✗</span>
99
+ <span v-if="intersectionObserver" class="text-success">✓</span>
100
+ <span v-else class="text-error">✗</span>
101
101
  <span>IntersectionObserver</span>
102
102
  </div>
103
103
  <div class="flex items-center gap-2">
104
- <span v-if="resizeObserver" class="text-green-500">✓</span>
105
- <span v-else class="text-red-500">✗</span>
104
+ <span v-if="resizeObserver" class="text-success">✓</span>
105
+ <span v-else class="text-error">✗</span>
106
106
  <span>ResizeObserver</span>
107
107
  </div>
108
108
  <div class="flex items-center gap-2">
109
- <span v-if="serviceWorker" class="text-green-500">✓</span>
110
- <span v-else class="text-red-500">✗</span>
109
+ <span v-if="serviceWorker" class="text-success">✓</span>
110
+ <span v-else class="text-error">✗</span>
111
111
  <span>Service Worker</span>
112
112
  </div>
113
113
  <div class="flex items-center gap-2">
114
- <span v-if="webGL" class="text-green-500">✓</span>
115
- <span v-else class="text-red-500">✗</span>
114
+ <span v-if="webGL" class="text-success">✓</span>
115
+ <span v-else class="text-error">✗</span>
116
116
  <span>WebGL</span>
117
117
  </div>
118
118
  </div>
@@ -139,8 +139,8 @@
139
139
  <div class="space-y-2">
140
140
  <div class="flex items-center gap-2">
141
141
  <span class="w-24 font-medium">Status:</span>
142
- <span v-if="isOnline" class="text-green-500">Online</span>
143
- <span v-else class="text-red-500">Offline</span>
142
+ <span v-if="isOnline" class="text-success">Online</span>
143
+ <span v-else class="text-error">Offline</span>
144
144
  </div>
145
145
  <div class="flex items-center gap-2">
146
146
  <span class="w-24 font-medium">Type:</span>
@@ -150,10 +150,10 @@
150
150
  <span class="w-24 font-medium">Quality:</span>
151
151
  <span
152
152
  :class="{
153
- 'text-green-500': connectionQuality === 'excellent',
154
- 'text-yellow-500': connectionQuality === 'good',
155
- 'text-red-500': connectionQuality === 'poor',
156
- 'text-gray-500': connectionQuality === 'offline',
153
+ 'text-success': connectionQuality === 'excellent',
154
+ 'text-warning': connectionQuality === 'good',
155
+ 'text-error': connectionQuality === 'poor',
156
+ 'text-muted': connectionQuality === 'offline',
157
157
  }"
158
158
  >
159
159
  {{ connectionQuality }}
@@ -161,13 +161,13 @@
161
161
  </div>
162
162
  <div class="flex items-center gap-2">
163
163
  <span class="w-24 font-medium">Speed:</span>
164
- <span v-if="isFast" class="text-green-500">Fast</span>
165
- <span v-else-if="isSlow" class="text-red-500">Slow</span>
164
+ <span v-if="isFast" class="text-success">Fast</span>
165
+ <span v-else-if="isSlow" class="text-error">Slow</span>
166
166
  <span v-else>Normal</span>
167
167
  </div>
168
168
  <div v-if="saveData" class="flex items-center gap-2">
169
169
  <span class="w-24 font-medium">Data Saver:</span>
170
- <span class="text-yellow-500">Enabled</span>
170
+ <span class="text-warning">Enabled</span>
171
171
  </div>
172
172
  </div>
173
173
  </UCard>
@@ -188,8 +188,8 @@
188
188
  </div>
189
189
  <div class="flex items-center gap-2">
190
190
  <span class="w-24 font-medium">Hydrated:</span>
191
- <span v-if="isHydrated" class="text-green-500">Yes</span>
192
- <span v-else class="text-yellow-500">No</span>
191
+ <span v-if="isHydrated" class="text-success">Yes</span>
192
+ <span v-else class="text-warning">No</span>
193
193
  </div>
194
194
  </div>
195
195
  </UCard>
@@ -201,17 +201,17 @@
201
201
  <div class="space-y-2">
202
202
  <div class="flex items-center gap-2">
203
203
  <span class="w-24 font-medium">Installed:</span>
204
- <span v-if="isInstalled" class="text-green-500">Yes</span>
205
- <span v-else class="text-gray-500">No</span>
204
+ <span v-if="isInstalled" class="text-success">Yes</span>
205
+ <span v-else class="text-muted">No</span>
206
206
  </div>
207
207
  <div class="flex items-center gap-2">
208
208
  <span class="w-24 font-medium">Installable:</span>
209
- <span v-if="canInstall" class="text-green-500">Yes</span>
210
- <span v-else class="text-gray-500">No</span>
209
+ <span v-if="canInstall" class="text-success">Yes</span>
210
+ <span v-else class="text-muted">No</span>
211
211
  </div>
212
212
  <div v-if="needRefresh" class="flex items-center gap-2">
213
213
  <span class="w-24 font-medium">Update:</span>
214
- <span class="text-yellow-500">Available</span>
214
+ <span class="text-warning">Available</span>
215
215
  </div>
216
216
  </div>
217
217
  </UCard>
@@ -92,6 +92,7 @@
92
92
  'Helvetica Neue',
93
93
  Arial,
94
94
  sans-serif;
95
+
95
96
  /* `backwards`, not `both` - a held end state would block the exit transition below */
96
97
  animation: loading-enter 0.9s cubic-bezier(0.16, 1, 0.3, 1) 0.1s backwards;
97
98
  }
@@ -110,6 +111,7 @@
110
111
  font-stretch: ultra-expanded;
111
112
  font-size: clamp(3rem, 35cqw + 1rem, 18rem);
112
113
  line-height: 0.8;
114
+
113
115
  /* Equal-width digits - the counter doesn't jitter as it ticks */
114
116
  font-variant-numeric: tabular-nums;
115
117
  }