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
|
@@ -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:
|
|
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
|
-
<
|
|
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
|
-
</
|
|
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
|
-
|
|
11
|
-
|
|
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 {
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
2
|
+
const { title = 'Portfolio', description = 'Featured projects and work' } = defineProps<{
|
|
3
|
+
title?: string
|
|
4
|
+
description?: string
|
|
5
|
+
}>()
|
|
3
6
|
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
<
|
|
18
|
-
<
|
|
19
|
-
|
|
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
|
+
}
|
|
@@ -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
|
-
/*
|
|
45
|
-
an
|
|
46
|
-
|
|
47
|
-
|
|
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-
|
|
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-
|
|
44
|
-
<span v-else class="text-
|
|
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-
|
|
49
|
-
<span v-else class="text-
|
|
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-
|
|
54
|
-
<span v-else class="text-
|
|
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-
|
|
59
|
-
<span v-else class="text-
|
|
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-
|
|
64
|
-
<span v-else class="text-
|
|
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-
|
|
69
|
-
<span v-else class="text-
|
|
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-
|
|
82
|
-
<span v-else class="text-
|
|
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-
|
|
87
|
-
<span v-else class="text-
|
|
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-
|
|
100
|
-
<span v-else class="text-
|
|
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-
|
|
105
|
-
<span v-else class="text-
|
|
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-
|
|
110
|
-
<span v-else class="text-
|
|
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-
|
|
115
|
-
<span v-else class="text-
|
|
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-
|
|
143
|
-
<span v-else class="text-
|
|
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-
|
|
154
|
-
'text-
|
|
155
|
-
'text-
|
|
156
|
-
'text-
|
|
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-
|
|
165
|
-
<span v-else-if="isSlow" class="text-
|
|
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-
|
|
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-
|
|
192
|
-
<span v-else class="text-
|
|
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-
|
|
205
|
-
<span v-else class="text-
|
|
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-
|
|
210
|
-
<span v-else class="text-
|
|
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-
|
|
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
|
}
|