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.
- package/docs/MIGRATION-v6.5.0.md +87 -0
- package/docs/MIGRATION.md +2 -1
- package/docs/USAGE.md +35 -10
- package/layers/content/app/components/content/Figure.vue +1 -1
- package/layers/content/app/composables/useContentSection.ts +1 -1
- package/layers/core/app/app.config.ts +7 -3
- package/layers/core/app/components/LoadingScreen.vue +35 -21
- package/layers/core/app/composables/useCache.ts +2 -2
- package/layers/core/app/composables/useErrorLog.ts +5 -5
- package/layers/core/app/composables/useLoading.ts +19 -10
- package/layers/core/app/plugins/loading.client.ts +11 -3
- package/layers/core/app/types/loading.ts +10 -3
- package/layers/core/app/utils/loading.ts +6 -15
- package/layers/core/nuxt.config.ts +14 -14
- package/layers/core/server/utils/app-config.ts +17 -0
- package/layers/data/auth/app/middleware/auth.ts +1 -1
- package/layers/data/forms/app/components/Form/Contact.vue +1 -1
- package/layers/data/forms/server/api/contact.post.ts +16 -8
- package/layers/data/forms/server/api/forms/status.get.ts +3 -0
- package/layers/data/forms/{app → shared}/utils/contact-schema.ts +2 -1
- package/layers/data/metadata/server/api/metadata/search.get.ts +27 -7
- package/layers/data/metadata/server/api/metadata/status.get.ts +4 -1
- package/layers/data/metadata/server/api/metadata/sync.post.ts +13 -3
- package/layers/data/metadata/server/utils/metadata/resolve-record.ts +3 -3
- package/layers/delivery/feeds/nuxt.config.ts +6 -1
- package/layers/delivery/feeds/package.json +2 -2
- package/layers/delivery/feeds/server/routes/feed/demo.get.ts +2 -2
- package/layers/delivery/feeds/server/utils/content-adapter.ts +1 -1
- package/layers/delivery/feeds/server/utils/feed-service.ts +2 -2
- package/layers/design-system/theming/app/plugins/theme.client.ts +7 -0
- package/layers/design-system/theming/nuxt.config.ts +0 -2
- package/layers/motion/scroll/app/plugins/locomotive-scroll.client.ts +3 -2
- package/layers/render/canvas/nuxt.config.ts +0 -4
- package/layers/render/shader/nuxt.config.ts +0 -4
- package/layers/structure/routing/app/middleware/01.maintenance.global.ts +4 -1
- package/layers/structure/routing/app/middleware/02.governance.global.ts +1 -1
- package/layers/structure/routing/nuxt.config.ts +19 -12
- package/layers/structure/routing/server/api/feature-flags.get.ts +4 -2
- package/package.json +35 -5
- package/docs/FALLOW-COMPLEXITY-DUPLICATION-AUDIT.md +0 -64
- package/docs/IMPROVE-AUDIT-README.md +0 -29
- package/docs/IMPROVE-AUDIT-RESULTS.md +0 -51
- package/docs/IMPROVE-DEEP-AUDIT-RESULTS.md +0 -81
- package/docs/fallow-refactor/apps-debug.md +0 -27
- package/docs/fallow-refactor/apps-playground.md +0 -46
- package/docs/fallow-refactor/apps-visual-identity.md +0 -41
- package/docs/fallow-refactor/layers-animations.md +0 -34
- package/docs/fallow-refactor/layers-canvas.md +0 -32
- package/docs/fallow-refactor/layers-content.md +0 -33
- package/docs/fallow-refactor/layers-core.md +0 -39
- package/docs/fallow-refactor/layers-feeds.md +0 -39
- package/docs/fallow-refactor/layers-forms.md +0 -30
- package/docs/fallow-refactor/layers-layout.md +0 -42
- package/docs/fallow-refactor/layers-mailer.md +0 -32
- package/docs/fallow-refactor/layers-motion.md +0 -27
- package/docs/fallow-refactor/layers-navigation.md +0 -31
- package/docs/fallow-refactor/layers-page-transitions.md +0 -30
- package/docs/fallow-refactor/layers-routing.md +0 -33
- package/docs/fallow-refactor/layers-scripts.md +0 -35
- package/docs/fallow-refactor/layers-scroll.md +0 -38
- package/docs/fallow-refactor/layers-seo.md +0 -32
- package/docs/fallow-refactor/layers-shader.md +0 -53
- package/docs/fallow-refactor/layers-theme.md +0 -33
- package/docs/fallow-refactor/layers-transitions.md +0 -27
- package/docs/fallow-refactor/layers-typography.md +0 -29
- package/docs/fallow-refactor/layers-visual.md +0 -34
- package/docs/superpowers/plans/2026-09-03-layout-layer-fluid-rebuild.md +0 -1850
- package/docs/superpowers/plans/2026-09-03-vue-element-system.md +0 -1019
- package/docs/superpowers/plans/2026-09-04-grid-layer.md +0 -977
- package/docs/superpowers/specs/2026-08-30-layer-taxonomy-tier-0-1-design.md +0 -533
- package/docs/superpowers/specs/2026-08-30-layer-taxonomy-tier-2-design.md +0 -154
- package/docs/superpowers/specs/2026-08-30-layer-taxonomy-tier-3-design.md +0 -109
- package/docs/superpowers/specs/2026-08-30-layer-taxonomy-tier-4-design.md +0 -124
- package/docs/superpowers/specs/2026-08-30-layer-taxonomy-tier-5-design.md +0 -121
- package/docs/superpowers/specs/2026-08-30-layer-taxonomy-tier-6-design.md +0 -77
- package/docs/superpowers/specs/2026-08-30-layer-taxonomy-tier-7-design.md +0 -73
- package/docs/superpowers/specs/2026-08-31-swiss-grid-layer-design.md +0 -190
- package/docs/superpowers/specs/2026-09-03-layout-layer-fluid-rebuild-design.md +0 -312
- package/docs/superpowers/specs/2026-09-03-vue-element-system-design.md +0 -342
- package/docs/superpowers/specs/2026-09-04-grid-layer-design.md +0 -236
- package/layers/content/app/utils/galleryImageMeta.test.ts +0 -30
- package/layers/core/app/components/Element.test.ts +0 -90
- package/layers/core/app/components/ElementTw.test.ts +0 -64
- package/layers/core/app/composables/useLayoutAttrs.test.ts +0 -77
- package/layers/core/app/utils/browserInfo.test.ts +0 -81
- package/layers/core/app/utils/featureClasses.test.ts +0 -101
- package/layers/core/app/utils/loading.test.ts +0 -48
- package/layers/core/app/utils/resolveElementStyle.test.ts +0 -99
- package/layers/core/app/utils/scrollGuardClamp.test.ts +0 -36
- package/layers/core/app/utils/styleToTailwindClasses.test.ts +0 -31
- package/layers/data/forms/app/utils/contact-schema.test.ts +0 -75
- package/layers/data/forms/app/utils/fieldProps.test.ts +0 -141
- package/layers/data/metadata/providers/comicvine/server/utils/metadata-comicvine/normalise.test.ts +0 -201
- package/layers/data/metadata/providers/google-books/server/utils/metadata-google-books/client.test.ts +0 -76
- package/layers/data/metadata/providers/google-books/server/utils/metadata-google-books/normalise.test.ts +0 -144
- package/layers/data/metadata/providers/openlibrary/server/utils/metadata-openlibrary/client.test.ts +0 -25
- package/layers/data/metadata/providers/openlibrary/server/utils/metadata-openlibrary/normalise.test.ts +0 -144
- package/layers/data/metadata/providers/themoviedb/server/utils/metadata-tmdb/client.test.ts +0 -29
- package/layers/data/metadata/providers/themoviedb/server/utils/metadata-tmdb/normalise.test.ts +0 -162
- package/layers/data/metadata/server/utils/metadata/cache.test.ts +0 -39
- package/layers/data/metadata/server/utils/metadata/provider-fetch.test.ts +0 -27
- package/layers/data/metadata/server/utils/metadata/provider-registry.test.ts +0 -54
- package/layers/data/metadata/server/utils/metadata/resolve-record.test.ts +0 -87
- package/layers/data/metadata/server/utils/metadata/search.test.ts +0 -204
- package/layers/delivery/feeds/server/utils/content-adapter.test.ts +0 -67
- package/layers/delivery/feeds/server/utils/feed-author.test.ts +0 -71
- package/layers/delivery/feeds/server/utils/feed-config.test.ts +0 -184
- package/layers/delivery/feeds/server/utils/formats.test.ts +0 -71
- package/layers/delivery/seo/app/utils/seoConfig.test.ts +0 -58
- package/layers/design-system/theming/server/utils/accent-css.test.ts +0 -71
- package/layers/design-system/theming/server/utils/fouc-config.test.ts +0 -78
- package/layers/design-system/typography/app/components/Typography/Headline.test.ts +0 -36
- package/layers/design-system/typography/app/components/Typography/index.test.ts +0 -31
- package/layers/design-system/typography/app/composables/color.test.ts +0 -30
- package/layers/design-system/typography/app/composables/typography.test.ts +0 -53
- package/layers/design-system/visual/app/components/HoverDimGroup.test.ts +0 -38
- package/layers/design-system/visual/app/composables/tint.test.ts +0 -22
- package/layers/design-system/visual/app/utils/colorTokens.test.ts +0 -51
- package/layers/design-system/visual/app/utils/gradientStyle.test.ts +0 -75
- package/layers/design-system/visual/app/utils/responsiveSizes.test.ts +0 -71
- package/layers/motion/animations/app/components/Motion/Staggered.test.ts +0 -40
- package/layers/motion/scroll/app/composables/useReducedMotion.test.ts +0 -23
- package/layers/motion/scroll/app/utils/parallax.test.ts +0 -24
- package/layers/structure/grid/app/composables/useGridTracks.test.ts +0 -94
- package/layers/structure/grid/app/types/tracks.test.ts +0 -32
- package/layers/structure/grid/app/utils/placement.test.ts +0 -19
- package/layers/structure/layout/app/utils/gridPlacementStyle.test.ts +0 -269
- package/layers/structure/navigation/app/utils/site.test.ts +0 -39
- package/layers/structure/routing/app/utils/resolveRoute.test.ts +0 -47
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import { defineComponent, h } from 'vue'
|
|
2
|
-
import { mount } from '@vue/test-utils'
|
|
3
|
-
import { describe, expect, it } from 'vite-plus/test'
|
|
4
|
-
|
|
5
|
-
import Element from './Element.vue'
|
|
6
|
-
|
|
7
|
-
// Stands in for a real component (e.g. UBadge) whose own `color` prop is
|
|
8
|
-
// semantic, not a raw CSS value — the same name Element uses for its own
|
|
9
|
-
// raw-CSS text colour passthrough.
|
|
10
|
-
const ColorPropStub = defineComponent({
|
|
11
|
-
props: { color: { type: String, default: undefined } },
|
|
12
|
-
setup(props) {
|
|
13
|
-
return () => h('div', { 'data-received-color': props.color }, 'stub')
|
|
14
|
-
},
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
describe('Element', () => {
|
|
18
|
-
it('renders as a div by default', () => {
|
|
19
|
-
const wrapper = mount(Element)
|
|
20
|
-
expect(wrapper.element.tagName).toBe('DIV')
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
it('renders as the tag passed via the as prop', () => {
|
|
24
|
-
const wrapper = mount(Element, { props: { as: 'section' } })
|
|
25
|
-
expect(wrapper.element.tagName).toBe('SECTION')
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
it('renders slot content', () => {
|
|
29
|
-
const wrapper = mount(Element, { slots: { default: 'hello' } })
|
|
30
|
-
expect(wrapper.text()).toBe('hello')
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
it('merges layout, sizing, and surface styles into one style attribute', () => {
|
|
34
|
-
const wrapper = mount(Element, {
|
|
35
|
-
props: {
|
|
36
|
-
flex: true,
|
|
37
|
-
w: '80px',
|
|
38
|
-
bg: 'var(--ui-bg-elevated)',
|
|
39
|
-
},
|
|
40
|
-
})
|
|
41
|
-
const style = wrapper.attributes('style') ?? ''
|
|
42
|
-
expect(style).toContain('display: flex')
|
|
43
|
-
expect(style).toContain('width: 80px')
|
|
44
|
-
expect(style).toContain('background: var(--ui-bg-elevated)')
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
it('applies container-axis grid styles', () => {
|
|
48
|
-
const wrapper = mount(Element, {
|
|
49
|
-
props: { grid: true, cols: 12 },
|
|
50
|
-
})
|
|
51
|
-
const style = wrapper.attributes('style') ?? ''
|
|
52
|
-
expect(style).toContain('display: grid')
|
|
53
|
-
expect(style).toContain('grid-template-columns: repeat(12, 1fr)')
|
|
54
|
-
expect(style).not.toContain('grid-column')
|
|
55
|
-
expect(style).not.toContain('grid-row')
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
it('gives hidden priority over grid so display: none is not overwritten', () => {
|
|
59
|
-
const wrapper = mount(Element, {
|
|
60
|
-
props: { grid: true, hidden: true },
|
|
61
|
-
})
|
|
62
|
-
const style = wrapper.attributes('style') ?? ''
|
|
63
|
-
expect(style).toContain('display: none')
|
|
64
|
-
expect(style).not.toContain('display: grid')
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
it('reaches margin and padding through useLayoutAttrs', () => {
|
|
68
|
-
const wrapper = mount(Element, {
|
|
69
|
-
props: { m: 'md', p: 'lg' },
|
|
70
|
-
})
|
|
71
|
-
const style = wrapper.attributes('style') ?? ''
|
|
72
|
-
expect(style).toContain('margin: var(--space-md)')
|
|
73
|
-
expect(style).toContain('padding: var(--space-lg)')
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
it('passes componentProps to the resolved component, past a colliding prop name', () => {
|
|
77
|
-
const wrapper = mount(Element, {
|
|
78
|
-
props: {
|
|
79
|
-
as: ColorPropStub,
|
|
80
|
-
color: 'crimson',
|
|
81
|
-
componentProps: { color: 'error' },
|
|
82
|
-
},
|
|
83
|
-
})
|
|
84
|
-
// Element's own `color` still resolves to raw CSS text colour...
|
|
85
|
-
expect(wrapper.attributes('style') ?? '').toContain('color: crimson')
|
|
86
|
-
// ...while the stub's semantic `color` prop comes from componentProps, not
|
|
87
|
-
// from Element's own `color`.
|
|
88
|
-
expect(wrapper.attributes('data-received-color')).toBe('error')
|
|
89
|
-
})
|
|
90
|
-
})
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import { defineComponent, h } from 'vue'
|
|
2
|
-
import { mount } from '@vue/test-utils'
|
|
3
|
-
import { describe, expect, it } from 'vite-plus/test'
|
|
4
|
-
|
|
5
|
-
import ElementTw from './ElementTw.vue'
|
|
6
|
-
|
|
7
|
-
const ColorPropStub = defineComponent({
|
|
8
|
-
props: { color: { type: String, default: undefined } },
|
|
9
|
-
setup(props) {
|
|
10
|
-
return () => h('div', { 'data-received-color': props.color }, 'stub')
|
|
11
|
-
},
|
|
12
|
-
})
|
|
13
|
-
|
|
14
|
-
describe('ElementTw', () => {
|
|
15
|
-
it('renders as a div by default', () => {
|
|
16
|
-
const wrapper = mount(ElementTw)
|
|
17
|
-
expect(wrapper.element.tagName).toBe('DIV')
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
it('renders as the tag passed via the as prop', () => {
|
|
21
|
-
const wrapper = mount(ElementTw, { props: { as: 'section' } })
|
|
22
|
-
expect(wrapper.element.tagName).toBe('SECTION')
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
it('renders slot content', () => {
|
|
26
|
-
const wrapper = mount(ElementTw, { slots: { default: 'hello' } })
|
|
27
|
-
expect(wrapper.text()).toBe('hello')
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
it('emits merged layout, sizing, and surface styles as tailwind arbitrary-property classes', () => {
|
|
31
|
-
const wrapper = mount(ElementTw, {
|
|
32
|
-
props: {
|
|
33
|
-
flex: true,
|
|
34
|
-
w: '80px',
|
|
35
|
-
bg: 'var(--ui-bg-elevated)',
|
|
36
|
-
},
|
|
37
|
-
})
|
|
38
|
-
const classes = wrapper.classes()
|
|
39
|
-
expect(classes).toContain('[display:flex]')
|
|
40
|
-
expect(classes).toContain('[width:80px]')
|
|
41
|
-
expect(classes).toContain('[background:var(--ui-bg-elevated)]')
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
it('gives hidden priority over grid so display:none is not overwritten', () => {
|
|
45
|
-
const wrapper = mount(ElementTw, {
|
|
46
|
-
props: { grid: true, hidden: true },
|
|
47
|
-
})
|
|
48
|
-
const classes = wrapper.classes()
|
|
49
|
-
expect(classes).toContain('[display:none]')
|
|
50
|
-
expect(classes).not.toContain('[display:grid]')
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
it('passes componentProps to the resolved component, past a colliding prop name', () => {
|
|
54
|
-
const wrapper = mount(ElementTw, {
|
|
55
|
-
props: {
|
|
56
|
-
as: ColorPropStub,
|
|
57
|
-
color: 'crimson',
|
|
58
|
-
componentProps: { color: 'error' },
|
|
59
|
-
},
|
|
60
|
-
})
|
|
61
|
-
expect(wrapper.classes()).toContain('[color:crimson]')
|
|
62
|
-
expect(wrapper.attributes('data-received-color')).toBe('error')
|
|
63
|
-
})
|
|
64
|
-
})
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs'
|
|
2
|
-
|
|
3
|
-
import { describe, expect, it } from 'vite-plus/test'
|
|
4
|
-
|
|
5
|
-
import type { Spacing } from '../types/tokens'
|
|
6
|
-
import { useLayoutAttrs } from './useLayoutAttrs'
|
|
7
|
-
|
|
8
|
-
// spacingVar() builds the custom-property name by string interpolation, so a token
|
|
9
|
-
// with no matching declaration resolves to an undefined var() — silent in CSS.
|
|
10
|
-
describe('useLayoutAttrs spacing tokens', () => {
|
|
11
|
-
const css = readFileSync(
|
|
12
|
-
new URL('../assets/css/tokens/spacing-fluid.css', import.meta.url),
|
|
13
|
-
'utf8'
|
|
14
|
-
)
|
|
15
|
-
// Record, not an array, so typecheck fails if the Spacing union gains a member.
|
|
16
|
-
const tokens: Record<Exclude<Spacing, 'none'>, true> = {
|
|
17
|
-
xs: true,
|
|
18
|
-
sm: true,
|
|
19
|
-
md: true,
|
|
20
|
-
lg: true,
|
|
21
|
-
xl: true,
|
|
22
|
-
'2xl': true,
|
|
23
|
-
'3xl': true,
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
it('resolves every token to a property spacing-fluid.css declares', () => {
|
|
27
|
-
for (const token of Object.keys(tokens) as Spacing[]) {
|
|
28
|
-
const { style } = useLayoutAttrs(() => ({ gap: token }))
|
|
29
|
-
const name = /^var\((--[\w-]+)\)$/.exec(String(style.value.gap))?.[1]
|
|
30
|
-
expect(css).toMatch(new RegExp(`^\\s*${name}:`, 'm'))
|
|
31
|
-
}
|
|
32
|
-
})
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
describe('useLayoutAttrs margin support', () => {
|
|
36
|
-
it('sets margin from a static token', () => {
|
|
37
|
-
const { style } = useLayoutAttrs(() => ({ m: 'md' }))
|
|
38
|
-
expect(style.value.margin).toBe('var(--space-md)')
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
it('sets marginInline from px-equivalent mx', () => {
|
|
42
|
-
const { style } = useLayoutAttrs(() => ({ mx: 'lg' }))
|
|
43
|
-
expect(style.value.marginInline).toBe('var(--space-lg)')
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
it('sets marginBlock from my', () => {
|
|
47
|
-
const { style } = useLayoutAttrs(() => ({ my: 'sm' }))
|
|
48
|
-
expect(style.value.marginBlock).toBe('var(--space-sm)')
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
it('resolves none to 0px for margin', () => {
|
|
52
|
-
const { style } = useLayoutAttrs(() => ({ m: 'none' }))
|
|
53
|
-
expect(style.value.margin).toBe('0px')
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
it('omits margin keys entirely when not passed', () => {
|
|
57
|
-
const { style } = useLayoutAttrs(() => ({ gap: 'md' }))
|
|
58
|
-
expect(style.value.margin).toBeUndefined()
|
|
59
|
-
expect(style.value.marginInline).toBeUndefined()
|
|
60
|
-
expect(style.value.marginBlock).toBeUndefined()
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
it('does not regress existing gap/align/justify/padding behavior', () => {
|
|
64
|
-
const { style } = useLayoutAttrs(() => ({
|
|
65
|
-
gap: 'md',
|
|
66
|
-
align: 'center',
|
|
67
|
-
justify: 'between',
|
|
68
|
-
p: 'lg',
|
|
69
|
-
}))
|
|
70
|
-
expect(style.value).toEqual({
|
|
71
|
-
gap: 'var(--space-md)',
|
|
72
|
-
padding: 'var(--space-lg)',
|
|
73
|
-
alignItems: 'center',
|
|
74
|
-
justifyContent: 'space-between',
|
|
75
|
-
})
|
|
76
|
-
})
|
|
77
|
-
})
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vite-plus/test'
|
|
2
|
-
|
|
3
|
-
import { getBrowserVersionParts, isBrowserAtLeast, parseBrowserInfo } from './browserInfo'
|
|
4
|
-
|
|
5
|
-
describe('parseBrowserInfo', () => {
|
|
6
|
-
// In a node vitest environment `import.meta.client` is not defined by Nuxt's
|
|
7
|
-
// build macros, so it is falsy and the function always takes its SSR/default
|
|
8
|
-
// branch regardless of the user agent passed in.
|
|
9
|
-
it('returns the unknown/default shape when import.meta.client is falsy', () => {
|
|
10
|
-
expect(parseBrowserInfo('Mozilla/5.0 Chrome/120.0.0.0')).toEqual({
|
|
11
|
-
name: 'unknown',
|
|
12
|
-
version: '0',
|
|
13
|
-
engine: 'unknown',
|
|
14
|
-
os: 'unknown',
|
|
15
|
-
})
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
it('returns the same default shape with no user agent argument', () => {
|
|
19
|
-
expect(parseBrowserInfo()).toEqual({
|
|
20
|
-
name: 'unknown',
|
|
21
|
-
version: '0',
|
|
22
|
-
engine: 'unknown',
|
|
23
|
-
os: 'unknown',
|
|
24
|
-
})
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
it('returns the same default shape with a null user agent', () => {
|
|
28
|
-
expect(parseBrowserInfo(null)).toEqual({
|
|
29
|
-
name: 'unknown',
|
|
30
|
-
version: '0',
|
|
31
|
-
engine: 'unknown',
|
|
32
|
-
os: 'unknown',
|
|
33
|
-
})
|
|
34
|
-
})
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
describe('getBrowserVersionParts', () => {
|
|
38
|
-
it('parses a major.minor version string', () => {
|
|
39
|
-
expect(getBrowserVersionParts('120.5')).toEqual({ major: 120, minor: 5 })
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
it('defaults minor to 0 when only a major version is given', () => {
|
|
43
|
-
expect(getBrowserVersionParts('120')).toEqual({ major: 120, minor: 0 })
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
it('defaults both parts to 0 for a non-numeric version', () => {
|
|
47
|
-
expect(getBrowserVersionParts('abc')).toEqual({ major: 0, minor: 0 })
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
it('defaults both parts to 0 for an empty string', () => {
|
|
51
|
-
expect(getBrowserVersionParts('')).toEqual({ major: 0, minor: 0 })
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
it('parses extra version segments, ignoring anything past minor', () => {
|
|
55
|
-
expect(getBrowserVersionParts('120.5.9.1')).toEqual({ major: 120, minor: 5 })
|
|
56
|
-
})
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
describe('isBrowserAtLeast', () => {
|
|
60
|
-
it('returns false when the minimum version has no major segment', () => {
|
|
61
|
-
expect(isBrowserAtLeast('120.0', '')).toBe(false)
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
it('returns true when the current major exceeds the required major', () => {
|
|
65
|
-
expect(isBrowserAtLeast('121.0', '120.5')).toBe(true)
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
it('returns false when the current major is behind the required major', () => {
|
|
69
|
-
expect(isBrowserAtLeast('119.9', '120.0')).toBe(false)
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
it('compares minor versions when majors are equal', () => {
|
|
73
|
-
expect(isBrowserAtLeast('120.5', '120.5')).toBe(true)
|
|
74
|
-
expect(isBrowserAtLeast('120.6', '120.5')).toBe(true)
|
|
75
|
-
expect(isBrowserAtLeast('120.4', '120.5')).toBe(false)
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
it('treats a missing minimum minor as 0', () => {
|
|
79
|
-
expect(isBrowserAtLeast('120.0', '120')).toBe(true)
|
|
80
|
-
})
|
|
81
|
-
})
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vite-plus/test'
|
|
2
|
-
|
|
3
|
-
import type { FeatureDetection } from '../types/detection'
|
|
4
|
-
import { getFeatureClassNames } from './featureClasses'
|
|
5
|
-
|
|
6
|
-
function createFeatures(overrides: Partial<FeatureDetection> = {}): FeatureDetection {
|
|
7
|
-
return {
|
|
8
|
-
grid: false,
|
|
9
|
-
subgrid: false,
|
|
10
|
-
containerQueries: false,
|
|
11
|
-
has: false,
|
|
12
|
-
aspectRatio: false,
|
|
13
|
-
logicalProperties: false,
|
|
14
|
-
backdropFilter: false,
|
|
15
|
-
intersectionObserver: false,
|
|
16
|
-
resizeObserver: false,
|
|
17
|
-
serviceWorker: false,
|
|
18
|
-
webGL: false,
|
|
19
|
-
darkMode: false,
|
|
20
|
-
reducedMotion: false,
|
|
21
|
-
highContrast: false,
|
|
22
|
-
webp: false,
|
|
23
|
-
avif: false,
|
|
24
|
-
...overrides,
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
describe('getFeatureClassNames', () => {
|
|
29
|
-
it('emits the "no-" variant for every unsupported CSS feature and no extras', () => {
|
|
30
|
-
expect(getFeatureClassNames(createFeatures())).toEqual([
|
|
31
|
-
'no-grid',
|
|
32
|
-
'no-subgrid',
|
|
33
|
-
'no-container-queries',
|
|
34
|
-
'no-has',
|
|
35
|
-
'no-aspect-ratio',
|
|
36
|
-
'no-backdrop-filter',
|
|
37
|
-
])
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
it('emits the "supports-" variant for every supported CSS feature plus all extras', () => {
|
|
41
|
-
expect(
|
|
42
|
-
getFeatureClassNames(
|
|
43
|
-
createFeatures({
|
|
44
|
-
grid: true,
|
|
45
|
-
subgrid: true,
|
|
46
|
-
containerQueries: true,
|
|
47
|
-
has: true,
|
|
48
|
-
aspectRatio: true,
|
|
49
|
-
backdropFilter: true,
|
|
50
|
-
intersectionObserver: true,
|
|
51
|
-
resizeObserver: true,
|
|
52
|
-
serviceWorker: true,
|
|
53
|
-
webGL: true,
|
|
54
|
-
webp: true,
|
|
55
|
-
avif: true,
|
|
56
|
-
})
|
|
57
|
-
)
|
|
58
|
-
).toEqual([
|
|
59
|
-
'supports-grid',
|
|
60
|
-
'supports-subgrid',
|
|
61
|
-
'supports-container-queries',
|
|
62
|
-
'supports-has',
|
|
63
|
-
'supports-aspect-ratio',
|
|
64
|
-
'supports-backdrop-filter',
|
|
65
|
-
'has-intersection-observer',
|
|
66
|
-
'has-resize-observer',
|
|
67
|
-
'has-service-worker',
|
|
68
|
-
'has-webgl',
|
|
69
|
-
'supports-webp',
|
|
70
|
-
'supports-avif',
|
|
71
|
-
])
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
it('only includes extra classes for the features that are truthy', () => {
|
|
75
|
-
const classes = getFeatureClassNames(createFeatures({ intersectionObserver: true, avif: true }))
|
|
76
|
-
|
|
77
|
-
expect(classes).toContain('has-intersection-observer')
|
|
78
|
-
expect(classes).toContain('supports-avif')
|
|
79
|
-
expect(classes).not.toContain('has-resize-observer')
|
|
80
|
-
expect(classes).not.toContain('supports-webp')
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
it('mixes supported and unsupported CSS features independently', () => {
|
|
84
|
-
const classes = getFeatureClassNames(createFeatures({ grid: true, has: true }))
|
|
85
|
-
|
|
86
|
-
expect(classes).toEqual([
|
|
87
|
-
'supports-grid',
|
|
88
|
-
'no-subgrid',
|
|
89
|
-
'no-container-queries',
|
|
90
|
-
'supports-has',
|
|
91
|
-
'no-aspect-ratio',
|
|
92
|
-
'no-backdrop-filter',
|
|
93
|
-
])
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
it('ignores keys that are not part of either class map (e.g. darkMode)', () => {
|
|
97
|
-
const classes = getFeatureClassNames(createFeatures({ darkMode: true, highContrast: true }))
|
|
98
|
-
expect(classes.join(' ')).not.toContain('dark')
|
|
99
|
-
expect(classes.join(' ')).not.toContain('contrast')
|
|
100
|
-
})
|
|
101
|
-
})
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vite-plus/test'
|
|
2
|
-
|
|
3
|
-
import { approachProgress, resolveLoadingConfig, trickleProgress } from './loading'
|
|
4
|
-
|
|
5
|
-
const FRAME = 1 / 60
|
|
6
|
-
|
|
7
|
-
describe('resolveLoadingConfig', () => {
|
|
8
|
-
it('fills unset keys with defaults but keeps falsy values that were set', () => {
|
|
9
|
-
expect(resolveLoadingConfig(undefined)).toMatchObject({ enabled: false, minDuration: 1500 })
|
|
10
|
-
expect(resolveLoadingConfig({ loading: { minDuration: 0, label: '' } })).toMatchObject({
|
|
11
|
-
enabled: false,
|
|
12
|
-
minDuration: 0,
|
|
13
|
-
label: '',
|
|
14
|
-
})
|
|
15
|
-
})
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
describe('trickleProgress', () => {
|
|
19
|
-
it('starts at 0, rises with time, and stays below the cap', () => {
|
|
20
|
-
expect(trickleProgress(0, 1200)).toBe(0)
|
|
21
|
-
expect(trickleProgress(-500, 1200)).toBe(0)
|
|
22
|
-
expect(trickleProgress(1200, 1200)).toBeGreaterThan(trickleProgress(600, 1200))
|
|
23
|
-
expect(trickleProgress(60_000, 1200)).toBeLessThanOrEqual(90)
|
|
24
|
-
})
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
describe('approachProgress', () => {
|
|
28
|
-
it('counts 0 → 100 in small per-frame steps and lands exactly on the target', () => {
|
|
29
|
-
let current = 0
|
|
30
|
-
let frames = 0
|
|
31
|
-
while (current < 100) {
|
|
32
|
-
const next = approachProgress(current, 100, FRAME)
|
|
33
|
-
expect(next).toBeGreaterThan(current)
|
|
34
|
-
expect(Math.floor(next) - Math.floor(current)).toBeLessThanOrEqual(3)
|
|
35
|
-
current = next
|
|
36
|
-
frames++
|
|
37
|
-
}
|
|
38
|
-
expect(current).toBe(100)
|
|
39
|
-
// A visible count of at least ~0.7s, not a jump
|
|
40
|
-
expect(frames).toBeGreaterThan(40)
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
it('never moves backwards or overshoots', () => {
|
|
44
|
-
expect(approachProgress(60, 40, FRAME)).toBe(60)
|
|
45
|
-
expect(approachProgress(0, 90, -0.008)).toBe(0)
|
|
46
|
-
expect(approachProgress(99.9, 100, 1)).toBe(100)
|
|
47
|
-
})
|
|
48
|
-
})
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vite-plus/test'
|
|
2
|
-
|
|
3
|
-
import { resolveElementStyle } from './resolveElementStyle'
|
|
4
|
-
|
|
5
|
-
describe('resolveElementStyle display mode', () => {
|
|
6
|
-
it('returns no display when no mode prop is set', () => {
|
|
7
|
-
expect(resolveElementStyle({}).display).toBeUndefined()
|
|
8
|
-
})
|
|
9
|
-
|
|
10
|
-
it.each([
|
|
11
|
-
['block', 'block'],
|
|
12
|
-
['flex', 'flex'],
|
|
13
|
-
['grid', 'grid'],
|
|
14
|
-
['hidden', 'none'],
|
|
15
|
-
] as const)('resolves %s to display: %s', (prop, display) => {
|
|
16
|
-
expect(resolveElementStyle({ [prop]: true }).display).toBe(display)
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
it('prioritizes hidden > grid > flex > block', () => {
|
|
20
|
-
expect(resolveElementStyle({ hidden: true, grid: true, flex: true, block: true }).display).toBe(
|
|
21
|
-
'none'
|
|
22
|
-
)
|
|
23
|
-
expect(resolveElementStyle({ grid: true, flex: true, block: true }).display).toBe('grid')
|
|
24
|
-
expect(resolveElementStyle({ flex: true, block: true }).display).toBe('flex')
|
|
25
|
-
})
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
describe('resolveElementStyle grid container axis', () => {
|
|
29
|
-
it('returns no grid-template properties when grid is not set', () => {
|
|
30
|
-
const style = resolveElementStyle({ cols: 12 })
|
|
31
|
-
expect(style.gridTemplateColumns).toBeUndefined()
|
|
32
|
-
expect(style.gridTemplateRows).toBeUndefined()
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
it('converts numeric cols/rows to repeat() tracks when grid is set', () => {
|
|
36
|
-
const style = resolveElementStyle({ grid: true, cols: 12, rows: 4 })
|
|
37
|
-
expect(style.gridTemplateColumns).toBe('repeat(12, 1fr)')
|
|
38
|
-
expect(style.gridTemplateRows).toBe('repeat(4, 1fr)')
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
it('passes a string track through unchanged', () => {
|
|
42
|
-
expect(resolveElementStyle({ grid: true, cols: '1fr 2fr 1fr' }).gridTemplateColumns).toBe(
|
|
43
|
-
'1fr 2fr 1fr'
|
|
44
|
-
)
|
|
45
|
-
})
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
describe('resolveElementStyle prop mapping', () => {
|
|
49
|
-
it('maps sizing, surface and interaction props to their CSS properties 1:1', () => {
|
|
50
|
-
expect(
|
|
51
|
-
resolveElementStyle({
|
|
52
|
-
w: '10px',
|
|
53
|
-
h: '20px',
|
|
54
|
-
minW: '1px',
|
|
55
|
-
maxW: '2px',
|
|
56
|
-
minH: '3px',
|
|
57
|
-
maxH: '4px',
|
|
58
|
-
aspect: '16 / 9',
|
|
59
|
-
bg: 'red',
|
|
60
|
-
color: 'blue',
|
|
61
|
-
border: '1px solid',
|
|
62
|
-
radius: '4px',
|
|
63
|
-
shadow: '0 0 1px black',
|
|
64
|
-
cursor: 'pointer',
|
|
65
|
-
select: 'none',
|
|
66
|
-
pointer: 'none',
|
|
67
|
-
})
|
|
68
|
-
).toMatchObject({
|
|
69
|
-
width: '10px',
|
|
70
|
-
height: '20px',
|
|
71
|
-
minWidth: '1px',
|
|
72
|
-
maxWidth: '2px',
|
|
73
|
-
minHeight: '3px',
|
|
74
|
-
maxHeight: '4px',
|
|
75
|
-
aspectRatio: '16 / 9',
|
|
76
|
-
background: 'red',
|
|
77
|
-
color: 'blue',
|
|
78
|
-
border: '1px solid',
|
|
79
|
-
borderRadius: '4px',
|
|
80
|
-
boxShadow: '0 0 1px black',
|
|
81
|
-
cursor: 'pointer',
|
|
82
|
-
userSelect: 'none',
|
|
83
|
-
pointerEvents: 'none',
|
|
84
|
-
})
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
it('merges layout attrs (gap/align/padding) into the same object', () => {
|
|
88
|
-
expect(resolveElementStyle({ flex: true, gap: 'md', align: 'center', p: 'sm' })).toMatchObject({
|
|
89
|
-
display: 'flex',
|
|
90
|
-
gap: 'var(--space-md)',
|
|
91
|
-
alignItems: 'center',
|
|
92
|
-
padding: 'var(--space-sm)',
|
|
93
|
-
})
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
it('leaves every property undefined when no props are set', () => {
|
|
97
|
-
expect(Object.values(resolveElementStyle({})).every((v) => v === undefined)).toBe(true)
|
|
98
|
-
})
|
|
99
|
-
})
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vite-plus/test'
|
|
2
|
-
|
|
3
|
-
import { isOverflowing, shouldExclude } from './scrollGuardClamp'
|
|
4
|
-
|
|
5
|
-
describe('isOverflowing', () => {
|
|
6
|
-
it('is true when scrollWidth exceeds the viewport width', () => {
|
|
7
|
-
expect(isOverflowing(1200, 1024)).toBe(true)
|
|
8
|
-
})
|
|
9
|
-
|
|
10
|
-
it('is false when scrollWidth is within the viewport width', () => {
|
|
11
|
-
expect(isOverflowing(800, 1024)).toBe(false)
|
|
12
|
-
})
|
|
13
|
-
|
|
14
|
-
it('is false when scrollWidth equals the viewport width', () => {
|
|
15
|
-
expect(isOverflowing(1024, 1024)).toBe(false)
|
|
16
|
-
})
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
describe('shouldExclude', () => {
|
|
20
|
-
it('is true when any selector matches', () => {
|
|
21
|
-
const matches = (sel: string) => sel === '.carousel'
|
|
22
|
-
expect(shouldExclude(matches, ['.overflow-intent', '.carousel'])).toBe(true)
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
it('is false when no selector matches', () => {
|
|
26
|
-
const matches = () => false
|
|
27
|
-
expect(shouldExclude(matches, ['.carousel', '.overflow-intent'])).toBe(false)
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
it('swallows an invalid selector and treats it as non-matching', () => {
|
|
31
|
-
const matches = () => {
|
|
32
|
-
throw new DOMException('invalid selector')
|
|
33
|
-
}
|
|
34
|
-
expect(shouldExclude(matches, ['::not-a-real-selector'])).toBe(false)
|
|
35
|
-
})
|
|
36
|
-
})
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vite-plus/test'
|
|
2
|
-
|
|
3
|
-
import { styleToTailwindClasses } from './styleToTailwindClasses'
|
|
4
|
-
|
|
5
|
-
describe('styleToTailwindClasses', () => {
|
|
6
|
-
it('converts a camelCase property into a kebab-case arbitrary-property class', () => {
|
|
7
|
-
expect(styleToTailwindClasses({ background: 'red' })).toEqual(['[background:red]'])
|
|
8
|
-
expect(styleToTailwindClasses({ gridTemplateColumns: 'repeat(12, 1fr)' })).toEqual([
|
|
9
|
-
'[grid-template-columns:repeat(12,_1fr)]',
|
|
10
|
-
])
|
|
11
|
-
})
|
|
12
|
-
|
|
13
|
-
it('escapes spaces in the value with underscores', () => {
|
|
14
|
-
expect(styleToTailwindClasses({ boxShadow: '0 1px 2px rgba(0,0,0,.1)' })).toEqual([
|
|
15
|
-
'[box-shadow:0_1px_2px_rgba(0,0,0,.1)]',
|
|
16
|
-
])
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
it('skips undefined, null, and empty-string values', () => {
|
|
20
|
-
expect(
|
|
21
|
-
styleToTailwindClasses({ width: undefined, height: null as unknown as undefined, color: '' })
|
|
22
|
-
).toEqual([])
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
it('preserves declaration order across multiple properties', () => {
|
|
26
|
-
expect(styleToTailwindClasses({ display: 'flex', width: '80px' })).toEqual([
|
|
27
|
-
'[display:flex]',
|
|
28
|
-
'[width:80px]',
|
|
29
|
-
])
|
|
30
|
-
})
|
|
31
|
-
})
|