kmcom-nuxt-layers 6.4.0 → 6.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/docs/MIGRATION-v6.5.0.md +87 -0
  2. package/docs/MIGRATION.md +2 -1
  3. package/docs/USAGE.md +35 -10
  4. package/layers/content/app/components/content/Figure.vue +1 -1
  5. package/layers/content/app/composables/useContentSection.ts +1 -1
  6. package/layers/core/app/app.config.ts +7 -3
  7. package/layers/core/app/components/LoadingScreen.vue +35 -21
  8. package/layers/core/app/composables/useCache.ts +2 -2
  9. package/layers/core/app/composables/useErrorLog.ts +5 -5
  10. package/layers/core/app/composables/useLoading.ts +19 -10
  11. package/layers/core/app/plugins/loading.client.ts +11 -3
  12. package/layers/core/app/types/loading.ts +10 -3
  13. package/layers/core/app/utils/loading.ts +6 -15
  14. package/layers/core/nuxt.config.ts +14 -14
  15. package/layers/core/server/utils/app-config.ts +17 -0
  16. package/layers/data/auth/app/middleware/auth.ts +1 -1
  17. package/layers/data/forms/app/components/Form/Contact.vue +1 -1
  18. package/layers/data/forms/server/api/contact.post.ts +16 -8
  19. package/layers/data/forms/server/api/forms/status.get.ts +3 -0
  20. package/layers/data/forms/{app → shared}/utils/contact-schema.ts +2 -1
  21. package/layers/data/metadata/server/api/metadata/search.get.ts +27 -7
  22. package/layers/data/metadata/server/api/metadata/status.get.ts +4 -1
  23. package/layers/data/metadata/server/api/metadata/sync.post.ts +13 -3
  24. package/layers/data/metadata/server/utils/metadata/resolve-record.ts +3 -3
  25. package/layers/delivery/feeds/nuxt.config.ts +6 -1
  26. package/layers/delivery/feeds/package.json +2 -2
  27. package/layers/delivery/feeds/server/routes/feed/demo.get.ts +2 -2
  28. package/layers/delivery/feeds/server/utils/content-adapter.ts +1 -1
  29. package/layers/delivery/feeds/server/utils/feed-service.ts +2 -2
  30. package/layers/design-system/theming/app/plugins/theme.client.ts +7 -0
  31. package/layers/design-system/theming/nuxt.config.ts +0 -2
  32. package/layers/motion/scroll/app/plugins/locomotive-scroll.client.ts +3 -2
  33. package/layers/render/canvas/nuxt.config.ts +0 -4
  34. package/layers/render/shader/nuxt.config.ts +0 -4
  35. package/layers/structure/routing/app/middleware/01.maintenance.global.ts +4 -1
  36. package/layers/structure/routing/app/middleware/02.governance.global.ts +1 -1
  37. package/layers/structure/routing/nuxt.config.ts +19 -12
  38. package/layers/structure/routing/server/api/feature-flags.get.ts +4 -2
  39. package/package.json +35 -5
  40. package/docs/FALLOW-COMPLEXITY-DUPLICATION-AUDIT.md +0 -64
  41. package/docs/IMPROVE-AUDIT-README.md +0 -29
  42. package/docs/IMPROVE-AUDIT-RESULTS.md +0 -51
  43. package/docs/IMPROVE-DEEP-AUDIT-RESULTS.md +0 -81
  44. package/docs/fallow-refactor/apps-debug.md +0 -27
  45. package/docs/fallow-refactor/apps-playground.md +0 -46
  46. package/docs/fallow-refactor/apps-visual-identity.md +0 -41
  47. package/docs/fallow-refactor/layers-animations.md +0 -34
  48. package/docs/fallow-refactor/layers-canvas.md +0 -32
  49. package/docs/fallow-refactor/layers-content.md +0 -33
  50. package/docs/fallow-refactor/layers-core.md +0 -39
  51. package/docs/fallow-refactor/layers-feeds.md +0 -39
  52. package/docs/fallow-refactor/layers-forms.md +0 -30
  53. package/docs/fallow-refactor/layers-layout.md +0 -42
  54. package/docs/fallow-refactor/layers-mailer.md +0 -32
  55. package/docs/fallow-refactor/layers-motion.md +0 -27
  56. package/docs/fallow-refactor/layers-navigation.md +0 -31
  57. package/docs/fallow-refactor/layers-page-transitions.md +0 -30
  58. package/docs/fallow-refactor/layers-routing.md +0 -33
  59. package/docs/fallow-refactor/layers-scripts.md +0 -35
  60. package/docs/fallow-refactor/layers-scroll.md +0 -38
  61. package/docs/fallow-refactor/layers-seo.md +0 -32
  62. package/docs/fallow-refactor/layers-shader.md +0 -53
  63. package/docs/fallow-refactor/layers-theme.md +0 -33
  64. package/docs/fallow-refactor/layers-transitions.md +0 -27
  65. package/docs/fallow-refactor/layers-typography.md +0 -29
  66. package/docs/fallow-refactor/layers-visual.md +0 -34
  67. package/docs/superpowers/plans/2026-09-03-layout-layer-fluid-rebuild.md +0 -1850
  68. package/docs/superpowers/plans/2026-09-03-vue-element-system.md +0 -1019
  69. package/docs/superpowers/plans/2026-09-04-grid-layer.md +0 -977
  70. package/docs/superpowers/specs/2026-08-30-layer-taxonomy-tier-0-1-design.md +0 -533
  71. package/docs/superpowers/specs/2026-08-30-layer-taxonomy-tier-2-design.md +0 -154
  72. package/docs/superpowers/specs/2026-08-30-layer-taxonomy-tier-3-design.md +0 -109
  73. package/docs/superpowers/specs/2026-08-30-layer-taxonomy-tier-4-design.md +0 -124
  74. package/docs/superpowers/specs/2026-08-30-layer-taxonomy-tier-5-design.md +0 -121
  75. package/docs/superpowers/specs/2026-08-30-layer-taxonomy-tier-6-design.md +0 -77
  76. package/docs/superpowers/specs/2026-08-30-layer-taxonomy-tier-7-design.md +0 -73
  77. package/docs/superpowers/specs/2026-08-31-swiss-grid-layer-design.md +0 -190
  78. package/docs/superpowers/specs/2026-09-03-layout-layer-fluid-rebuild-design.md +0 -312
  79. package/docs/superpowers/specs/2026-09-03-vue-element-system-design.md +0 -342
  80. package/docs/superpowers/specs/2026-09-04-grid-layer-design.md +0 -236
  81. package/layers/content/app/utils/galleryImageMeta.test.ts +0 -30
  82. package/layers/core/app/components/Element.test.ts +0 -90
  83. package/layers/core/app/components/ElementTw.test.ts +0 -64
  84. package/layers/core/app/composables/useLayoutAttrs.test.ts +0 -77
  85. package/layers/core/app/utils/browserInfo.test.ts +0 -81
  86. package/layers/core/app/utils/featureClasses.test.ts +0 -101
  87. package/layers/core/app/utils/loading.test.ts +0 -48
  88. package/layers/core/app/utils/resolveElementStyle.test.ts +0 -99
  89. package/layers/core/app/utils/scrollGuardClamp.test.ts +0 -36
  90. package/layers/core/app/utils/styleToTailwindClasses.test.ts +0 -31
  91. package/layers/data/forms/app/utils/contact-schema.test.ts +0 -75
  92. package/layers/data/forms/app/utils/fieldProps.test.ts +0 -141
  93. package/layers/data/metadata/providers/comicvine/server/utils/metadata-comicvine/normalise.test.ts +0 -201
  94. package/layers/data/metadata/providers/google-books/server/utils/metadata-google-books/client.test.ts +0 -76
  95. package/layers/data/metadata/providers/google-books/server/utils/metadata-google-books/normalise.test.ts +0 -144
  96. package/layers/data/metadata/providers/openlibrary/server/utils/metadata-openlibrary/client.test.ts +0 -25
  97. package/layers/data/metadata/providers/openlibrary/server/utils/metadata-openlibrary/normalise.test.ts +0 -144
  98. package/layers/data/metadata/providers/themoviedb/server/utils/metadata-tmdb/client.test.ts +0 -29
  99. package/layers/data/metadata/providers/themoviedb/server/utils/metadata-tmdb/normalise.test.ts +0 -162
  100. package/layers/data/metadata/server/utils/metadata/cache.test.ts +0 -39
  101. package/layers/data/metadata/server/utils/metadata/provider-fetch.test.ts +0 -27
  102. package/layers/data/metadata/server/utils/metadata/provider-registry.test.ts +0 -54
  103. package/layers/data/metadata/server/utils/metadata/resolve-record.test.ts +0 -87
  104. package/layers/data/metadata/server/utils/metadata/search.test.ts +0 -204
  105. package/layers/delivery/feeds/server/utils/content-adapter.test.ts +0 -67
  106. package/layers/delivery/feeds/server/utils/feed-author.test.ts +0 -71
  107. package/layers/delivery/feeds/server/utils/feed-config.test.ts +0 -184
  108. package/layers/delivery/feeds/server/utils/formats.test.ts +0 -71
  109. package/layers/delivery/seo/app/utils/seoConfig.test.ts +0 -58
  110. package/layers/design-system/theming/server/utils/accent-css.test.ts +0 -71
  111. package/layers/design-system/theming/server/utils/fouc-config.test.ts +0 -78
  112. package/layers/design-system/typography/app/components/Typography/Headline.test.ts +0 -36
  113. package/layers/design-system/typography/app/components/Typography/index.test.ts +0 -31
  114. package/layers/design-system/typography/app/composables/color.test.ts +0 -30
  115. package/layers/design-system/typography/app/composables/typography.test.ts +0 -53
  116. package/layers/design-system/visual/app/components/HoverDimGroup.test.ts +0 -38
  117. package/layers/design-system/visual/app/composables/tint.test.ts +0 -22
  118. package/layers/design-system/visual/app/utils/colorTokens.test.ts +0 -51
  119. package/layers/design-system/visual/app/utils/gradientStyle.test.ts +0 -75
  120. package/layers/design-system/visual/app/utils/responsiveSizes.test.ts +0 -71
  121. package/layers/motion/animations/app/components/Motion/Staggered.test.ts +0 -40
  122. package/layers/motion/scroll/app/composables/useReducedMotion.test.ts +0 -23
  123. package/layers/motion/scroll/app/utils/parallax.test.ts +0 -24
  124. package/layers/structure/grid/app/composables/useGridTracks.test.ts +0 -94
  125. package/layers/structure/grid/app/types/tracks.test.ts +0 -32
  126. package/layers/structure/grid/app/utils/placement.test.ts +0 -19
  127. package/layers/structure/layout/app/utils/gridPlacementStyle.test.ts +0 -269
  128. package/layers/structure/navigation/app/utils/site.test.ts +0 -39
  129. package/layers/structure/routing/app/utils/resolveRoute.test.ts +0 -47
@@ -1,71 +0,0 @@
1
- import twColors from 'tailwindcss/colors'
2
- import { describe, expect, it } from 'vite-plus/test'
3
-
4
- import { buildAccentCSS } from './accent-css'
5
-
6
- const ACCENTS = [
7
- 'red',
8
- 'orange',
9
- 'amber',
10
- 'yellow',
11
- 'lime',
12
- 'green',
13
- 'emerald',
14
- 'teal',
15
- 'cyan',
16
- 'sky',
17
- 'blue',
18
- 'indigo',
19
- 'violet',
20
- 'purple',
21
- 'fuchsia',
22
- 'pink',
23
- 'rose',
24
- ] as const
25
-
26
- const SHADES = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950] as const
27
-
28
- describe('buildAccentCSS', () => {
29
- const css = buildAccentCSS()
30
-
31
- it('emits one html[data-theme-colour] rule per accent', () => {
32
- for (const accent of ACCENTS) {
33
- expect(css).toContain(`html[data-theme-colour="${accent}"]{`)
34
- }
35
- expect(css.match(/html\[data-theme-colour="/g)).toHaveLength(ACCENTS.length)
36
- })
37
-
38
- it('never emits the literal string "undefined"', () => {
39
- expect(css).not.toContain('undefined')
40
- })
41
-
42
- it('includes every shade for the primary palette of each accent', () => {
43
- for (const accent of ACCENTS) {
44
- const ruleMatch = css.match(
45
- new RegExp(`html\\[data-theme-colour="${accent}"\\]\\{([^}]*)\\}`)
46
- )
47
- expect(ruleMatch).not.toBeNull()
48
- const rule = ruleMatch?.[1] ?? ''
49
-
50
- for (const shade of SHADES) {
51
- expect(rule).toContain(`--ui-color-primary-${shade}:`)
52
- }
53
- }
54
- })
55
-
56
- it('uses the actual tailwind colour values for the primary palette', () => {
57
- const redPalette = twColors.red as unknown as Record<number, string>
58
- expect(css).toContain(`--ui-color-primary-500:${redPalette[500]};`)
59
- expect(css).toContain(`--ui-color-primary-50:${redPalette[50]};`)
60
- })
61
-
62
- it('includes secondary and info palettes derived from the accent map', () => {
63
- // blue -> secondary: indigo, info: sky
64
- const indigoPalette = twColors.indigo as unknown as Record<number, string>
65
- const skyPalette = twColors.sky as unknown as Record<number, string>
66
-
67
- const blueRule = css.match(/html\[data-theme-colour="blue"\]\{([^}]*)\}/)?.[1] ?? ''
68
- expect(blueRule).toContain(`--ui-color-secondary-500:${indigoPalette[500]};`)
69
- expect(blueRule).toContain(`--ui-color-info-500:${skyPalette[500]};`)
70
- })
71
- })
@@ -1,78 +0,0 @@
1
- import { describe, expect, it } from 'vite-plus/test'
2
-
3
- import { buildThemeInitScript } from './fouc-config'
4
-
5
- describe('buildThemeInitScript', () => {
6
- const script = buildThemeInitScript('blue')
7
-
8
- it('wraps the whole script in a try/catch IIFE', () => {
9
- expect(script.startsWith('(function(){try{')).toBe(true)
10
- expect(script.endsWith('}catch(e){}})()')).toBe(true)
11
- })
12
-
13
- it('captures document.documentElement as h', () => {
14
- expect(script).toContain('var h=document.documentElement;')
15
- })
16
-
17
- it('reads theme-colour from localStorage and falls back to the default accent', () => {
18
- expect(script).toContain(
19
- "var c=localStorage.getItem('theme-colour');h.setAttribute('data-theme-colour',c?JSON.parse(c):'blue');"
20
- )
21
- })
22
-
23
- it('uses the provided default accent verbatim, not a hardcoded one', () => {
24
- const other = buildThemeInitScript('rose')
25
- expect(other).toContain("h.setAttribute('data-theme-colour',c?JSON.parse(c):'rose');")
26
- expect(other).not.toContain("'blue'")
27
- })
28
-
29
- it('emits a JSON-parsed tri-state block for theme-contrast', () => {
30
- expect(script).toContain("var ct=localStorage.getItem('theme-contrast');")
31
- expect(script).toContain("var ctv=ct?JSON.parse(ct):'system';")
32
- expect(script).toContain("if(ctv==='on'){h.setAttribute('data-theme-contrast','high')}")
33
- expect(script).toContain(
34
- "else if(ctv==='off'){h.setAttribute('data-theme-contrast','standard')}"
35
- )
36
- expect(script).toContain(
37
- "else{h.setAttribute('data-theme-contrast',(window.matchMedia&&window.matchMedia('(prefers-contrast:more)').matches)?'high':'standard')}"
38
- )
39
- })
40
-
41
- it('emits a JSON-parsed tri-state block for theme-motion', () => {
42
- expect(script).toContain("var m=localStorage.getItem('theme-motion');")
43
- expect(script).toContain("if(mv==='on'){h.setAttribute('data-theme-motion','reduced')}")
44
- expect(script).toContain("else if(mv==='off'){h.setAttribute('data-theme-motion','full')}")
45
- expect(script).toContain(
46
- "window.matchMedia('(prefers-reduced-motion:reduce)').matches)?'reduced':'full'"
47
- )
48
- })
49
-
50
- it('emits a JSON-parsed tri-state block for theme-transparency', () => {
51
- expect(script).toContain("var t=localStorage.getItem('theme-transparency');")
52
- expect(script).toContain("if(tv==='on'){h.setAttribute('data-theme-transparency','reduced')}")
53
- expect(script).toContain(
54
- "else if(tv==='off'){h.setAttribute('data-theme-transparency','full')}"
55
- )
56
- expect(script).toContain(
57
- "window.matchMedia('(prefers-reduced-transparency:reduce)').matches)?'reduced':'full'"
58
- )
59
- })
60
-
61
- it('emits a plain (non-JSON) tri-state block for theme-mode using dark/light conditions', () => {
62
- expect(script).toContain("var dm=localStorage.getItem('theme-mode');")
63
- expect(script).toContain("var dmv=dm||'system';")
64
- expect(script).toContain("if(dmv==='dark'){h.setAttribute('data-theme-mode','dark')}")
65
- expect(script).toContain("else if(dmv==='light'){h.setAttribute('data-theme-mode','light')}")
66
- expect(script).toContain(
67
- "window.matchMedia('(prefers-color-scheme:dark)').matches)?'dark':'light'"
68
- )
69
- })
70
-
71
- it('does not JSON.parse the theme-mode value (jsonParsed: false)', () => {
72
- expect(script).not.toContain('JSON.parse(dm)')
73
- })
74
-
75
- it('produces valid, evaluable JavaScript', () => {
76
- expect(() => new Function(script)).not.toThrow()
77
- })
78
- })
@@ -1,36 +0,0 @@
1
- import { mount } from '@vue/test-utils'
2
- import { describe, expect, it } from 'vite-plus/test'
3
-
4
- import Headline from './Headline.vue'
5
-
6
- const render = (props: Record<string, unknown>) =>
7
- mount(Headline, { props, slots: { default: 'Title' } })
8
- const classesOf = (props: Record<string, unknown>) => render(props).classes()
9
- const styleOf = (props: Record<string, unknown>) => render(props).attributes('style') ?? ''
10
-
11
- describe('Headline step', () => {
12
- it('steps each level down from the display step by default', () => {
13
- expect(styleOf({ level: 1 })).toContain('--step: var(--step-display)')
14
- expect(styleOf({ level: 2 })).toContain('--step: calc(var(--step-display) - 1)')
15
- expect(styleOf({ level: 6 })).toContain('--step: calc(var(--step-display) - 5)')
16
- expect(classesOf({ level: 2 }).filter((c) => c.startsWith('text-step-'))).toEqual([])
17
- })
18
-
19
- it('drops the default step when an explicit size is given', () => {
20
- const wrapper = render({ level: 2, size: 'sm' })
21
- expect(wrapper.classes()).toContain('text-sm')
22
- expect(wrapper.attributes('style') ?? '').not.toContain('--step')
23
- })
24
-
25
- it('leaves line height to the step unless leading is given', () => {
26
- expect(classesOf({ level: 2 }).filter((c) => c.startsWith('leading-'))).toEqual([])
27
- expect(classesOf({ level: 2, leading: 'leading-tight' })).toContain('leading-tight')
28
- })
29
-
30
- it('lets an explicit step win over size and the default', () => {
31
- const wrapper = render({ level: 2, size: 'sm', step: 5 })
32
- expect(wrapper.classes()).toContain('text-step-5')
33
- expect(wrapper.classes()).not.toContain('text-sm')
34
- expect(wrapper.attributes('style') ?? '').not.toContain('--step')
35
- })
36
- })
@@ -1,31 +0,0 @@
1
- import { mount } from '@vue/test-utils'
2
- import { describe, expect, it } from 'vite-plus/test'
3
-
4
- import ElementTw from '#layers/core/app/components/ElementTw.vue'
5
-
6
- import Typography from './index.vue'
7
-
8
- const classesOf = (props: Record<string, unknown> = {}) =>
9
- mount(Typography, {
10
- props,
11
- slots: { default: 'Body copy' },
12
- global: { components: { ElementTw } },
13
- }).classes()
14
-
15
- describe('Typography size', () => {
16
- it('inherits the surrounding size by default instead of pinning text-base', () => {
17
- expect(classesOf()).not.toContain('text-base')
18
- })
19
-
20
- it('inherits the body line height by default', () => {
21
- expect(classesOf().filter((c) => c.startsWith('leading-'))).toEqual([])
22
- })
23
-
24
- it('still applies an explicit size', () => {
25
- expect(classesOf({ size: 'lg' })).toContain('text-lg')
26
- })
27
-
28
- it('applies an explicit step', () => {
29
- expect(classesOf({ step: 1 })).toContain('text-step-1')
30
- })
31
- })
@@ -1,30 +0,0 @@
1
- import { ref } from 'vue'
2
- import { describe, expect, it } from 'vite-plus/test'
3
-
4
- import { useColor } from './color'
5
-
6
- describe('useColor', () => {
7
- it('builds a class from a plain value', () => {
8
- expect(useColor('primary', 'text').value).toBe('text-primary')
9
- })
10
-
11
- it('falls back to default when color is undefined', () => {
12
- expect(useColor(undefined, 'bg').value).toBe('bg-default')
13
- })
14
-
15
- it('tracks a ref', () => {
16
- const color = ref<'primary' | 'error'>('primary')
17
- const cls = useColor(color)
18
- color.value = 'error'
19
- expect(cls.value).toBe('text-error')
20
- })
21
-
22
- it('tracks a getter', () => {
23
- let color: 'primary' | 'error' = 'primary'
24
- const state = ref(0)
25
- const cls = useColor(() => (state.value, color))
26
- color = 'error'
27
- state.value++
28
- expect(cls.value).toBe('text-error')
29
- })
30
- })
@@ -1,53 +0,0 @@
1
- import { readFileSync } from 'node:fs'
2
-
3
- import { describe, expect, it } from 'vite-plus/test'
4
-
5
- import { TYPE_STEP_CLASSES, useTypography } from './typography'
6
-
7
- // Tailwind only emits a class that appears literally in source AND has an @utility — a map
8
- // entry without a utility renders unstyled, a utility without an entry is unreachable from the
9
- // components. Both fail silently, so pin the two lists to each other.
10
- const css = readFileSync(new URL('../assets/css/typography.css', import.meta.url), 'utf8')
11
- const utilities = [...css.matchAll(/@utility\s+(text-step-[\w-]+)/g)].map(([, name]) => name)
12
- const mapped = Object.values(TYPE_STEP_CLASSES)
13
-
14
- describe('TYPE_STEP_CLASSES ↔ typography.css', () => {
15
- it('has an @utility for every mapped class', () => {
16
- for (const cls of mapped) expect(utilities).toContain(cls)
17
- })
18
-
19
- it('maps every text-step @utility', () => {
20
- for (const name of utilities) expect(mapped).toContain(name)
21
- })
22
-
23
- it('names each class after its step', () => {
24
- for (const [step, cls] of Object.entries(TYPE_STEP_CLASSES)) {
25
- expect(cls).toBe(`text-step-${step}`)
26
- }
27
- })
28
- })
29
-
30
- describe('useTypography step', () => {
31
- const classesOf = (props: Parameters<typeof useTypography>[0]) =>
32
- useTypography(props).classes.value.split(' ')
33
-
34
- it('emits step 0 rather than treating it as unset', () => {
35
- expect(classesOf({ step: 0 })).toContain('text-step-0')
36
- })
37
-
38
- it('wins over size instead of emitting two font sizes', () => {
39
- const classes = classesOf({ step: 3, size: 'sm' })
40
- expect(classes).toContain('text-step-3')
41
- expect(classes).not.toContain('text-sm')
42
- })
43
-
44
- it('falls back to size when no step is given', () => {
45
- expect(classesOf({ size: 'sm' })).toContain('text-sm')
46
- })
47
-
48
- // A leading-* class sorts after text-step-* and would override the step's line height.
49
- it('emits no leading class unless one is given', () => {
50
- expect(classesOf({ step: 3 }).filter((c) => c.startsWith('leading-'))).toEqual([])
51
- expect(classesOf({ step: 3, leading: 'leading-tight' })).toContain('leading-tight')
52
- })
53
- })
@@ -1,38 +0,0 @@
1
- import { mount } from '@vue/test-utils'
2
- import { defineComponent } from 'vue'
3
- import { describe, expect, it } from 'vite-plus/test'
4
-
5
- import HoverDimGroup from './HoverDimGroup.vue'
6
-
7
- // Same template shape as MastNavModal: <template v-for> around a <slot> with fallback content,
8
- // which compiles to fragments nested two deep
9
- const Parent = defineComponent({
10
- components: { HoverDimGroup },
11
- data: () => ({ ids: ['a', 'b', 'c'] }),
12
- template: `
13
- <HoverDimGroup tag="nav">
14
- <template v-for="id in ids" :key="id">
15
- <slot name="link" :id="id"><button>{{ id }}</button></slot>
16
- </template>
17
- </HoverDimGroup>`,
18
- })
19
-
20
- describe('HoverDimGroup', () => {
21
- it('renders and dims children nested in v-for and slot fragments', async () => {
22
- const wrapper = mount(Parent)
23
- const items = wrapper.findAll('nav > button')
24
- expect(items.map((b) => b.text())).toEqual(['a', 'b', 'c'])
25
-
26
- await items[0]?.trigger('mouseenter')
27
- expect(items.map((b) => b.attributes('style'))).toEqual([
28
- expect.stringContaining('opacity: 1'),
29
- expect.stringContaining('opacity: 0.3'),
30
- expect.stringContaining('opacity: 0.3'),
31
- ])
32
- })
33
-
34
- it('uses slot content when the parent passes it', () => {
35
- const wrapper = mount(Parent, { slots: { link: '<a href="#">x</a>' } })
36
- expect(wrapper.findAll('nav > a')).toHaveLength(3)
37
- })
38
- })
@@ -1,22 +0,0 @@
1
- import { describe, expect, it } from 'vite-plus/test'
2
-
3
- import { useTint } from './tint'
4
-
5
- describe('useTint', () => {
6
- it('resolves a slot to its ui color token with the level opacity', () => {
7
- expect(useTint({ color: 'primary', level: 'light' }).style.value).toEqual({
8
- backgroundColor: 'var(--ui-color-primary-500)',
9
- opacity: 0.1,
10
- })
11
- })
12
-
13
- it('honours shade and the white/black literals', () => {
14
- expect(
15
- useTint({ color: 'error', shade: 200, level: 'subtle' }).style.value.backgroundColor
16
- ).toBe('var(--ui-color-error-200)')
17
- expect(useTint({ color: 'white', level: 'heavy' }).style.value).toEqual({
18
- backgroundColor: '#ffffff',
19
- opacity: 0.5,
20
- })
21
- })
22
- })
@@ -1,51 +0,0 @@
1
- import { describe, expect, it } from 'vite-plus/test'
2
-
3
- import { resolveUiColorToken } from './colorTokens'
4
-
5
- describe('resolveUiColorToken', () => {
6
- it('defaults to primary-500 when no options are given', () => {
7
- expect(resolveUiColorToken({})).toBe('var(--ui-color-primary-500)')
8
- })
9
-
10
- it('uses the provided color and shade', () => {
11
- expect(resolveUiColorToken({ color: 'secondary', shade: 200 })).toBe(
12
- 'var(--ui-color-secondary-200)'
13
- )
14
- })
15
-
16
- it('applies opacity via color-mix', () => {
17
- expect(resolveUiColorToken({ color: 'primary', shade: 500, opacity: 50 })).toBe(
18
- 'color-mix(in srgb, var(--ui-color-primary-500) 50%, transparent)'
19
- )
20
- })
21
-
22
- it('resolves custom color to customColor value', () => {
23
- expect(resolveUiColorToken({ color: 'custom', customColor: '#123456' })).toBe('#123456')
24
- })
25
-
26
- it('resolves custom color to transparent when customColor is missing', () => {
27
- expect(resolveUiColorToken({ color: 'custom' })).toBe('transparent')
28
- })
29
-
30
- it('resolves transparent color regardless of other options', () => {
31
- expect(resolveUiColorToken({ color: 'transparent', shade: 900, opacity: 10 })).toBe(
32
- 'transparent'
33
- )
34
- })
35
-
36
- it('resolves white without opacity to a hex value', () => {
37
- expect(resolveUiColorToken({ color: 'white' })).toBe('#ffffff')
38
- })
39
-
40
- it('resolves white with opacity to an rgb() value', () => {
41
- expect(resolveUiColorToken({ color: 'white', opacity: 40 })).toBe('rgb(255 255 255 / 0.4)')
42
- })
43
-
44
- it('resolves black without opacity to a hex value', () => {
45
- expect(resolveUiColorToken({ color: 'black' })).toBe('#000000')
46
- })
47
-
48
- it('resolves black with opacity to an rgb() value', () => {
49
- expect(resolveUiColorToken({ color: 'black', opacity: 25 })).toBe('rgb(0 0 0 / 0.25)')
50
- })
51
- })
@@ -1,75 +0,0 @@
1
- import { describe, expect, it } from 'vite-plus/test'
2
-
3
- import type { GradientConfig } from '../types/gradient'
4
- import { buildGradientStyle, resolveGradientColor } from './gradientStyle'
5
-
6
- describe('resolveGradientColor', () => {
7
- it('delegates to resolveUiColorToken for a gradient stop', () => {
8
- expect(resolveGradientColor({ color: 'primary', shade: 500 })).toBe(
9
- 'var(--ui-color-primary-500)'
10
- )
11
- })
12
-
13
- it('applies opacity for a gradient stop', () => {
14
- expect(resolveGradientColor({ color: 'secondary', shade: 300, opacity: 50 })).toBe(
15
- 'color-mix(in srgb, var(--ui-color-secondary-300) 50%, transparent)'
16
- )
17
- })
18
- })
19
-
20
- describe('buildGradientStyle', () => {
21
- const from = { color: 'primary', shade: 500 } as const
22
- const to = { color: 'secondary', shade: 500 } as const
23
-
24
- it('builds a linear gradient defaulting direction to "to bottom right"', () => {
25
- const cfg: GradientConfig = { from, to }
26
- expect(buildGradientStyle(cfg)).toEqual({
27
- backgroundImage:
28
- 'linear-gradient(to bottom right, var(--ui-color-primary-500), var(--ui-color-secondary-500))',
29
- })
30
- })
31
-
32
- it('maps every named linear direction', () => {
33
- const directions: Array<[NonNullable<GradientConfig['direction']>, string]> = [
34
- ['to-t', 'to top'],
35
- ['to-tr', 'to top right'],
36
- ['to-r', 'to right'],
37
- ['to-br', 'to bottom right'],
38
- ['to-b', 'to bottom'],
39
- ['to-bl', 'to bottom left'],
40
- ['to-l', 'to left'],
41
- ['to-tl', 'to top left'],
42
- ]
43
-
44
- for (const [direction, expected] of directions) {
45
- const cfg: GradientConfig = { from, to, direction }
46
- expect(buildGradientStyle(cfg)).toEqual({
47
- backgroundImage: `linear-gradient(${expected}, var(--ui-color-primary-500), var(--ui-color-secondary-500))`,
48
- })
49
- }
50
- })
51
-
52
- it('includes the via stop between from and to when provided', () => {
53
- const via = { color: 'info', shade: 400 } as const
54
- const cfg: GradientConfig = { from, to, via }
55
- expect(buildGradientStyle(cfg)).toEqual({
56
- backgroundImage:
57
- 'linear-gradient(to bottom right, var(--ui-color-primary-500), var(--ui-color-info-400), var(--ui-color-secondary-500))',
58
- })
59
- })
60
-
61
- it('builds a radial gradient', () => {
62
- const cfg: GradientConfig = { from, to, shape: 'radial' }
63
- expect(buildGradientStyle(cfg)).toEqual({
64
- backgroundImage:
65
- 'radial-gradient(circle, var(--ui-color-primary-500), var(--ui-color-secondary-500))',
66
- })
67
- })
68
-
69
- it('builds a conic gradient', () => {
70
- const cfg: GradientConfig = { from, to, shape: 'conic' }
71
- expect(buildGradientStyle(cfg)).toEqual({
72
- backgroundImage: 'conic-gradient(var(--ui-color-primary-500), var(--ui-color-secondary-500))',
73
- })
74
- })
75
- })
@@ -1,71 +0,0 @@
1
- import { describe, expect, it } from 'vite-plus/test'
2
-
3
- import type { ResponsiveSizes } from '../types/media'
4
- import { buildResponsiveSizesQueries } from './responsiveSizes'
5
-
6
- describe('buildResponsiveSizesQueries', () => {
7
- it('returns only the default query when no breakpoints are set', () => {
8
- const sizes: ResponsiveSizes = { default: '100vw' }
9
- expect(buildResponsiveSizesQueries(sizes)).toEqual(['100vw'])
10
- })
11
-
12
- it('always places the default query last', () => {
13
- const sizes: ResponsiveSizes = { default: '100vw', md: '50vw' }
14
- const queries = buildResponsiveSizesQueries(sizes)
15
- expect(queries.at(-1)).toBe('100vw')
16
- })
17
-
18
- it('sorts tailwind breakpoints from largest to smallest min-width', () => {
19
- const sizes: ResponsiveSizes = {
20
- default: '100vw',
21
- sm: '90vw',
22
- lg: '50vw',
23
- '2xl': '25vw',
24
- }
25
-
26
- expect(buildResponsiveSizesQueries(sizes)).toEqual([
27
- '(min-width: 1536px) 25vw',
28
- '(min-width: 1024px) 50vw',
29
- '(min-width: 640px) 90vw',
30
- '100vw',
31
- ])
32
- })
33
-
34
- it('mixes device, phone, and tablet breakpoints sorted by pixel value', () => {
35
- const sizes: ResponsiveSizes = {
36
- default: '100vw',
37
- wide: '20vw',
38
- desktop: '40vw',
39
- 'phone-md': '95vw',
40
- }
41
-
42
- expect(buildResponsiveSizesQueries(sizes)).toEqual([
43
- '(min-width: 1920px) 20vw',
44
- '(min-width: 1024px) 40vw',
45
- '(min-width: 375px) 95vw',
46
- '100vw',
47
- ])
48
- })
49
-
50
- it('prepends landscape before portrait, both before breakpoints', () => {
51
- const sizes: ResponsiveSizes = {
52
- default: '100vw',
53
- landscape: '80vw',
54
- portrait: '90vw',
55
- md: '50vw',
56
- }
57
-
58
- expect(buildResponsiveSizesQueries(sizes)).toEqual([
59
- '(orientation: landscape) 80vw',
60
- '(orientation: portrait) 90vw',
61
- '(min-width: 768px) 50vw',
62
- '100vw',
63
- ])
64
- })
65
-
66
- it('omits orientation queries when not provided', () => {
67
- const sizes: ResponsiveSizes = { default: '100vw' }
68
- const queries = buildResponsiveSizesQueries(sizes)
69
- expect(queries.some((q) => q.includes('orientation'))).toBe(false)
70
- })
71
- })
@@ -1,40 +0,0 @@
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
- })
@@ -1,23 +0,0 @@
1
- import { computed, ref } from 'vue'
2
- import { describe, expect, it, vi } from 'vite-plus/test'
3
-
4
- const system = ref<'reduce' | 'no-preference'>('no-preference')
5
- vi.mock('@vueuse/core', () => ({ usePreferredReducedMotion: () => system }))
6
- const state = ref<'on' | 'off' | 'system'>('system')
7
- vi.stubGlobal('useState', () => state)
8
- vi.stubGlobal('computed', computed)
9
-
10
- const { useReducedMotion } = await import('./useReducedMotion')
11
-
12
- describe('useReducedMotion', () => {
13
- it.each([
14
- ['system', 'no-preference', false],
15
- ['system', 'reduce', true],
16
- ['on', 'no-preference', true],
17
- ['off', 'reduce', false],
18
- ] as const)('override %s + OS %s → %s', (override, os, expected) => {
19
- state.value = override
20
- system.value = os
21
- expect(useReducedMotion().value).toBe(expected)
22
- })
23
- })
@@ -1,24 +0,0 @@
1
- import { describe, expect, it } from 'vite-plus/test'
2
-
3
- import { parallaxMotionVars } from './parallax'
4
-
5
- const base = { speed: 0.5, direction: 'vertical', rotate: 0, scale: 0 } as const
6
-
7
- describe('parallaxMotionVars', () => {
8
- it('moves up on y for vertical, right on x for horizontal', () => {
9
- expect(parallaxMotionVars(base)).toEqual({ y: -100 })
10
- expect(parallaxMotionVars({ ...base, direction: 'horizontal' })).toEqual({ x: 100 })
11
- })
12
-
13
- it('adds rotation and scale only when set', () => {
14
- expect(parallaxMotionVars({ ...base, rotate: 15, scale: 0.2 })).toEqual({
15
- y: -100,
16
- rotation: 15,
17
- scale: 1.2,
18
- })
19
- })
20
-
21
- it('returns nothing to animate at zero speed', () => {
22
- expect(parallaxMotionVars({ ...base, speed: 0 })).toEqual({})
23
- })
24
- })