kmcom-nuxt-layers 6.3.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 (142) hide show
  1. package/docs/MIGRATION-v6.4.0.md +215 -0
  2. package/docs/MIGRATION-v6.5.0.md +87 -0
  3. package/docs/MIGRATION.md +3 -1
  4. package/docs/USAGE.md +35 -10
  5. package/docs/adr/0003-modular-step-system-replaces-fluid-clamps.md +16 -0
  6. package/docs/component-levels.md +1 -0
  7. package/docs/layout-shift-causes.md +18 -0
  8. package/layers/content/app/components/content/Figure.vue +1 -1
  9. package/layers/content/app/composables/useContentSection.ts +1 -1
  10. package/layers/core/app/app.config.ts +9 -5
  11. package/layers/core/app/app.vue +2 -1
  12. package/layers/core/app/components/LoadingScreen.vue +35 -21
  13. package/layers/core/app/composables/useCache.ts +2 -2
  14. package/layers/core/app/composables/useErrorLog.ts +5 -5
  15. package/layers/core/app/composables/useLoading.ts +19 -10
  16. package/layers/core/app/plugins/loading.client.ts +11 -3
  17. package/layers/core/app/types/loading.ts +10 -3
  18. package/layers/core/app/utils/loading.ts +7 -16
  19. package/layers/core/nuxt.config.ts +14 -14
  20. package/layers/core/server/utils/app-config.ts +17 -0
  21. package/layers/data/auth/app/middleware/auth.ts +1 -1
  22. package/layers/data/forms/app/components/Form/Contact.vue +1 -1
  23. package/layers/data/forms/server/api/contact.post.ts +16 -8
  24. package/layers/data/forms/server/api/forms/status.get.ts +3 -0
  25. package/layers/data/forms/{app → shared}/utils/contact-schema.ts +2 -1
  26. package/layers/data/metadata/server/api/metadata/search.get.ts +27 -7
  27. package/layers/data/metadata/server/api/metadata/status.get.ts +4 -1
  28. package/layers/data/metadata/server/api/metadata/sync.post.ts +13 -3
  29. package/layers/data/metadata/server/utils/metadata/resolve-record.ts +3 -3
  30. package/layers/delivery/feeds/nuxt.config.ts +6 -1
  31. package/layers/delivery/feeds/package.json +2 -2
  32. package/layers/delivery/feeds/server/routes/feed/demo.get.ts +2 -2
  33. package/layers/delivery/feeds/server/utils/content-adapter.ts +1 -1
  34. package/layers/delivery/feeds/server/utils/feed-service.ts +2 -2
  35. package/layers/design-system/theming/app/plugins/theme.client.ts +7 -0
  36. package/layers/design-system/theming/nuxt.config.ts +0 -2
  37. package/layers/design-system/typography/app/assets/css/typography.css +41 -5
  38. package/layers/design-system/visual/app/components/HoverDimGroup.vue +11 -3
  39. package/layers/motion/scroll/app/plugins/locomotive-scroll.client.ts +3 -2
  40. package/layers/render/canvas/nuxt.config.ts +0 -4
  41. package/layers/render/shader/nuxt.config.ts +0 -4
  42. package/layers/structure/navigation/app/app.config.ts +37 -14
  43. package/layers/structure/navigation/app/components/Mast/Nav.vue +14 -13
  44. package/layers/structure/navigation/app/components/Mast/NavModal.vue +83 -28
  45. package/layers/structure/navigation/app/components/Site/Logo.vue +35 -0
  46. package/layers/structure/navigation/app/components/Site/Title.vue +3 -3
  47. package/layers/structure/navigation/app/composables/mastNav.ts +38 -13
  48. package/layers/structure/navigation/app/types/nav.ts +34 -0
  49. package/layers/structure/routing/app/middleware/01.maintenance.global.ts +4 -1
  50. package/layers/structure/routing/app/middleware/02.governance.global.ts +1 -1
  51. package/layers/structure/routing/nuxt.config.ts +19 -12
  52. package/layers/structure/routing/server/api/feature-flags.get.ts +4 -2
  53. package/package.json +35 -5
  54. package/docs/FALLOW-COMPLEXITY-DUPLICATION-AUDIT.md +0 -64
  55. package/docs/IMPROVE-AUDIT-README.md +0 -29
  56. package/docs/IMPROVE-AUDIT-RESULTS.md +0 -51
  57. package/docs/IMPROVE-DEEP-AUDIT-RESULTS.md +0 -81
  58. package/docs/fallow-refactor/apps-debug.md +0 -27
  59. package/docs/fallow-refactor/apps-playground.md +0 -46
  60. package/docs/fallow-refactor/apps-visual-identity.md +0 -41
  61. package/docs/fallow-refactor/layers-animations.md +0 -34
  62. package/docs/fallow-refactor/layers-canvas.md +0 -32
  63. package/docs/fallow-refactor/layers-content.md +0 -33
  64. package/docs/fallow-refactor/layers-core.md +0 -39
  65. package/docs/fallow-refactor/layers-feeds.md +0 -39
  66. package/docs/fallow-refactor/layers-forms.md +0 -30
  67. package/docs/fallow-refactor/layers-layout.md +0 -42
  68. package/docs/fallow-refactor/layers-mailer.md +0 -32
  69. package/docs/fallow-refactor/layers-motion.md +0 -27
  70. package/docs/fallow-refactor/layers-navigation.md +0 -31
  71. package/docs/fallow-refactor/layers-page-transitions.md +0 -30
  72. package/docs/fallow-refactor/layers-routing.md +0 -33
  73. package/docs/fallow-refactor/layers-scripts.md +0 -35
  74. package/docs/fallow-refactor/layers-scroll.md +0 -38
  75. package/docs/fallow-refactor/layers-seo.md +0 -32
  76. package/docs/fallow-refactor/layers-shader.md +0 -53
  77. package/docs/fallow-refactor/layers-theme.md +0 -33
  78. package/docs/fallow-refactor/layers-transitions.md +0 -27
  79. package/docs/fallow-refactor/layers-typography.md +0 -29
  80. package/docs/fallow-refactor/layers-visual.md +0 -34
  81. package/docs/superpowers/plans/2026-09-03-layout-layer-fluid-rebuild.md +0 -1850
  82. package/docs/superpowers/plans/2026-09-03-vue-element-system.md +0 -1019
  83. package/docs/superpowers/plans/2026-09-04-grid-layer.md +0 -977
  84. package/docs/superpowers/specs/2026-08-30-layer-taxonomy-tier-0-1-design.md +0 -533
  85. package/docs/superpowers/specs/2026-08-30-layer-taxonomy-tier-2-design.md +0 -154
  86. package/docs/superpowers/specs/2026-08-30-layer-taxonomy-tier-3-design.md +0 -109
  87. package/docs/superpowers/specs/2026-08-30-layer-taxonomy-tier-4-design.md +0 -124
  88. package/docs/superpowers/specs/2026-08-30-layer-taxonomy-tier-5-design.md +0 -121
  89. package/docs/superpowers/specs/2026-08-30-layer-taxonomy-tier-6-design.md +0 -77
  90. package/docs/superpowers/specs/2026-08-30-layer-taxonomy-tier-7-design.md +0 -73
  91. package/docs/superpowers/specs/2026-08-31-swiss-grid-layer-design.md +0 -190
  92. package/docs/superpowers/specs/2026-09-03-layout-layer-fluid-rebuild-design.md +0 -312
  93. package/docs/superpowers/specs/2026-09-03-vue-element-system-design.md +0 -342
  94. package/docs/superpowers/specs/2026-09-04-grid-layer-design.md +0 -236
  95. package/layers/content/app/utils/galleryImageMeta.test.ts +0 -30
  96. package/layers/core/app/components/Element.test.ts +0 -90
  97. package/layers/core/app/components/ElementTw.test.ts +0 -64
  98. package/layers/core/app/composables/useLayoutAttrs.test.ts +0 -77
  99. package/layers/core/app/utils/browserInfo.test.ts +0 -81
  100. package/layers/core/app/utils/featureClasses.test.ts +0 -101
  101. package/layers/core/app/utils/loading.test.ts +0 -48
  102. package/layers/core/app/utils/resolveElementStyle.test.ts +0 -99
  103. package/layers/core/app/utils/scrollGuardClamp.test.ts +0 -36
  104. package/layers/core/app/utils/styleToTailwindClasses.test.ts +0 -31
  105. package/layers/data/forms/app/utils/contact-schema.test.ts +0 -75
  106. package/layers/data/forms/app/utils/fieldProps.test.ts +0 -141
  107. package/layers/data/metadata/providers/comicvine/server/utils/metadata-comicvine/normalise.test.ts +0 -201
  108. package/layers/data/metadata/providers/google-books/server/utils/metadata-google-books/client.test.ts +0 -76
  109. package/layers/data/metadata/providers/google-books/server/utils/metadata-google-books/normalise.test.ts +0 -144
  110. package/layers/data/metadata/providers/openlibrary/server/utils/metadata-openlibrary/client.test.ts +0 -25
  111. package/layers/data/metadata/providers/openlibrary/server/utils/metadata-openlibrary/normalise.test.ts +0 -144
  112. package/layers/data/metadata/providers/themoviedb/server/utils/metadata-tmdb/client.test.ts +0 -29
  113. package/layers/data/metadata/providers/themoviedb/server/utils/metadata-tmdb/normalise.test.ts +0 -162
  114. package/layers/data/metadata/server/utils/metadata/cache.test.ts +0 -39
  115. package/layers/data/metadata/server/utils/metadata/provider-fetch.test.ts +0 -27
  116. package/layers/data/metadata/server/utils/metadata/provider-registry.test.ts +0 -54
  117. package/layers/data/metadata/server/utils/metadata/resolve-record.test.ts +0 -87
  118. package/layers/data/metadata/server/utils/metadata/search.test.ts +0 -204
  119. package/layers/delivery/feeds/server/utils/content-adapter.test.ts +0 -67
  120. package/layers/delivery/feeds/server/utils/feed-author.test.ts +0 -71
  121. package/layers/delivery/feeds/server/utils/feed-config.test.ts +0 -184
  122. package/layers/delivery/feeds/server/utils/formats.test.ts +0 -71
  123. package/layers/delivery/seo/app/utils/seoConfig.test.ts +0 -58
  124. package/layers/design-system/theming/server/utils/accent-css.test.ts +0 -71
  125. package/layers/design-system/theming/server/utils/fouc-config.test.ts +0 -78
  126. package/layers/design-system/typography/app/components/Typography/Headline.test.ts +0 -36
  127. package/layers/design-system/typography/app/components/Typography/index.test.ts +0 -31
  128. package/layers/design-system/typography/app/composables/color.test.ts +0 -30
  129. package/layers/design-system/typography/app/composables/typography.test.ts +0 -53
  130. package/layers/design-system/visual/app/composables/tint.test.ts +0 -22
  131. package/layers/design-system/visual/app/utils/colorTokens.test.ts +0 -51
  132. package/layers/design-system/visual/app/utils/gradientStyle.test.ts +0 -75
  133. package/layers/design-system/visual/app/utils/responsiveSizes.test.ts +0 -71
  134. package/layers/motion/animations/app/components/Motion/Staggered.test.ts +0 -40
  135. package/layers/motion/scroll/app/composables/useReducedMotion.test.ts +0 -23
  136. package/layers/motion/scroll/app/utils/parallax.test.ts +0 -24
  137. package/layers/structure/grid/app/composables/useGridTracks.test.ts +0 -94
  138. package/layers/structure/grid/app/types/tracks.test.ts +0 -32
  139. package/layers/structure/grid/app/utils/placement.test.ts +0 -19
  140. package/layers/structure/layout/app/utils/gridPlacementStyle.test.ts +0 -269
  141. package/layers/structure/navigation/app/utils/site.test.ts +0 -39
  142. 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,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
- })