kmcom-nuxt-layers 5.0.8 → 6.1.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/adr/0001-grid-replaces-layout-swiss-grid.md +30 -0
- package/docs/adr/0002-content-config-is-app-owned.md +27 -0
- package/docs/adr/0003-modular-step-system-replaces-fluid-clamps.md +58 -0
- package/layers/core/app/assets/css/tokens/spacing-fluid.css +24 -19
- package/layers/core/app/components/Element.test.ts +2 -2
- package/layers/core/app/components/Element.vue +2 -66
- package/layers/core/app/components/ElementTw.vue +2 -66
- package/layers/core/app/composables/useElementStyle.ts +5 -59
- package/layers/core/app/composables/useLayoutAttrs.test.ts +35 -5
- package/layers/core/app/composables/useLayoutAttrs.ts +21 -21
- package/layers/core/app/types/element.ts +41 -18
- package/layers/core/app/utils/resolveElementStyle.test.ts +99 -0
- package/layers/core/app/utils/resolveElementStyle.ts +52 -0
- package/layers/data/metadata/providers/comicvine/server/utils/metadata-comicvine/client.ts +1 -0
- package/layers/data/metadata/providers/comicvine/server/utils/metadata-comicvine/provider.ts +0 -10
- package/layers/data/metadata/providers/google-books/server/utils/metadata-google-books/client.ts +12 -11
- package/layers/data/metadata/providers/google-books/server/utils/metadata-google-books/provider.ts +0 -7
- package/layers/data/metadata/providers/openlibrary/server/utils/metadata-openlibrary/client.ts +12 -23
- package/layers/data/metadata/providers/openlibrary/server/utils/metadata-openlibrary/provider.ts +0 -10
- package/layers/data/metadata/providers/themoviedb/server/utils/metadata-tmdb/client.ts +4 -7
- package/layers/data/metadata/providers/themoviedb/server/utils/metadata-tmdb/provider.ts +0 -6
- package/layers/data/metadata/server/api/metadata/lookup.get.ts +7 -42
- package/layers/data/metadata/server/api/metadata/sync.post.ts +4 -49
- package/layers/data/metadata/server/utils/metadata/provider-fetch.test.ts +27 -0
- package/layers/data/metadata/server/utils/metadata/provider-fetch.ts +14 -0
- package/layers/data/metadata/server/utils/metadata/resolve-record.test.ts +87 -0
- package/layers/data/metadata/server/utils/metadata/resolve-record.ts +60 -0
- package/layers/data/metadata/shared/types/providers.ts +3 -2
- package/layers/design-system/theming/app/types/designSystem.ts +21 -17
- package/layers/design-system/theming/nuxt.config.ts +1 -1
- package/layers/design-system/typography/app/app.config.ts +26 -0
- package/layers/design-system/typography/app/assets/css/typography.css +216 -187
- package/layers/design-system/typography/app/components/Typography/CodeBlock.vue +5 -5
- package/layers/design-system/typography/app/components/Typography/Headline.test.ts +36 -0
- package/layers/design-system/typography/app/components/Typography/Headline.vue +21 -22
- package/layers/design-system/typography/app/components/Typography/QuoteBlock.vue +5 -5
- package/layers/design-system/typography/app/components/Typography/RepeatText.vue +7 -6
- package/layers/design-system/typography/app/components/Typography/index.test.ts +31 -0
- package/layers/design-system/typography/app/components/Typography/index.vue +11 -9
- package/layers/design-system/typography/app/composables/color.test.ts +30 -0
- package/layers/design-system/typography/app/composables/color.ts +9 -2
- package/layers/design-system/typography/app/composables/typography.test.ts +53 -0
- package/layers/design-system/typography/app/composables/typography.ts +28 -43
- package/layers/design-system/typography/app/types/typography.ts +5 -32
- package/layers/design-system/visual/app/composables/tint.test.ts +22 -0
- package/layers/design-system/visual/app/composables/tint.ts +6 -7
- package/layers/render/shader/nuxt.config.ts +1 -1
- package/layers/structure/grid/app/assets/css/grid.css +11 -7
- package/layers/structure/navigation/app/components/Links/Named.vue +1 -1
- package/package.json +5 -4
- package/layers/core/app/composables/useElementGrid.test.ts +0 -33
- package/layers/core/app/composables/useElementGrid.ts +0 -29
- package/layers/core/app/composables/useElementInteraction.test.ts +0 -27
- package/layers/core/app/composables/useElementInteraction.ts +0 -17
- package/layers/core/app/composables/useElementLayout.test.ts +0 -41
- package/layers/core/app/composables/useElementLayout.ts +0 -26
- package/layers/core/app/composables/useElementSizing.test.ts +0 -39
- package/layers/core/app/composables/useElementSizing.ts +0 -25
- package/layers/core/app/composables/useElementSurface.test.ts +0 -33
- package/layers/core/app/composables/useElementSurface.ts +0 -21
- package/layers/design-system/theming/app/composables/useThemePreferences.ts +0 -5
|
@@ -0,0 +1,36 @@
|
|
|
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,9 +1,12 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
|
+
// Explicit import so Headline mounts under vitest's 'vue' project (no Nuxt auto-imports)
|
|
3
|
+
// — same exemption as ElementTw.vue (eslint.config.mjs).
|
|
4
|
+
import { computed } from 'vue'
|
|
5
|
+
|
|
2
6
|
import { useColor } from '../../composables/color'
|
|
3
7
|
import { useTypography } from '../../composables/typography'
|
|
4
8
|
import type { UiColors } from '../../types/colors'
|
|
5
9
|
import type {
|
|
6
|
-
FluidFontSize,
|
|
7
10
|
FontFamily,
|
|
8
11
|
FontLeading,
|
|
9
12
|
FontSize,
|
|
@@ -13,6 +16,7 @@
|
|
|
13
16
|
FontWidth,
|
|
14
17
|
TextAlign,
|
|
15
18
|
TextTransform,
|
|
19
|
+
TypeStep,
|
|
16
20
|
} from '../../types/typography'
|
|
17
21
|
|
|
18
22
|
const {
|
|
@@ -20,13 +24,13 @@
|
|
|
20
24
|
weight = 'font-bold',
|
|
21
25
|
width = 'font-stretch-normal',
|
|
22
26
|
slant = 'normal',
|
|
23
|
-
leading =
|
|
27
|
+
leading = undefined,
|
|
24
28
|
tracking = 'tracking-tight',
|
|
25
29
|
align = 'left',
|
|
26
30
|
transform = 'none',
|
|
27
31
|
color = undefined,
|
|
28
32
|
size = undefined,
|
|
29
|
-
|
|
33
|
+
step = undefined,
|
|
30
34
|
font = undefined,
|
|
31
35
|
} = defineProps<{
|
|
32
36
|
level?: 1 | 2 | 3 | 4 | 5 | 6
|
|
@@ -39,43 +43,38 @@
|
|
|
39
43
|
transform?: TextTransform
|
|
40
44
|
color?: UiColors
|
|
41
45
|
size?: FontSize
|
|
42
|
-
|
|
46
|
+
step?: TypeStep
|
|
43
47
|
font?: FontFamily
|
|
44
48
|
}>()
|
|
45
49
|
const tag = computed(() => `h${level}` as const)
|
|
46
50
|
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const appliedFluidSize = computed(() =>
|
|
59
|
-
size !== undefined ? undefined : (fluidSize ?? defaultFluidSizes[level])
|
|
60
|
-
)
|
|
51
|
+
// Display hierarchy by default: each level sits one scale step below the previous, starting
|
|
52
|
+
// at the fluid display step, so every level stays fluid (h1 51 → 97px, h2 40 → 77px …). Set as
|
|
53
|
+
// --step rather than a class so typography.css's h1–h6 base rules size it. Only when neither
|
|
54
|
+
// `step` nor `size` is passed; an explicit step beats size.
|
|
55
|
+
const defaultStepStyle = computed(() => {
|
|
56
|
+
if (step !== undefined || size !== undefined) return undefined
|
|
57
|
+
const below = level - 1
|
|
58
|
+
return { '--step': below ? `calc(var(--step-display) - ${below})` : 'var(--step-display)' }
|
|
59
|
+
})
|
|
61
60
|
|
|
62
61
|
const { classes } = useTypography(() => ({
|
|
63
62
|
weight,
|
|
64
63
|
width,
|
|
65
64
|
slant,
|
|
66
|
-
leading,
|
|
65
|
+
...(leading !== undefined && { leading }),
|
|
67
66
|
tracking,
|
|
68
67
|
align,
|
|
69
68
|
transform,
|
|
70
69
|
...(font !== undefined && { font }),
|
|
71
70
|
...(size !== undefined && { size }),
|
|
72
|
-
...(
|
|
71
|
+
...(step !== undefined && { step }),
|
|
73
72
|
}))
|
|
74
|
-
const colorClass = useColor(color, 'text')
|
|
73
|
+
const colorClass = useColor(() => color, 'text')
|
|
75
74
|
</script>
|
|
76
75
|
|
|
77
76
|
<template>
|
|
78
|
-
<component :is="tag" :class="[classes, colorClass]">
|
|
77
|
+
<component :is="tag" :class="[classes, colorClass]" :style="defaultStepStyle">
|
|
79
78
|
<slot />
|
|
80
79
|
</component>
|
|
81
80
|
</template>
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
import { useTypography } from '../../composables/typography'
|
|
4
4
|
import type { UiColors } from '../../types/colors'
|
|
5
5
|
import type {
|
|
6
|
-
FluidFontSize,
|
|
7
6
|
FontFamily,
|
|
8
7
|
FontLeading,
|
|
9
8
|
FontSize,
|
|
@@ -13,6 +12,7 @@
|
|
|
13
12
|
FontWidth,
|
|
14
13
|
TextAlign,
|
|
15
14
|
TextTransform,
|
|
15
|
+
TypeStep,
|
|
16
16
|
} from '../../types/typography'
|
|
17
17
|
|
|
18
18
|
const {
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
transform = 'none',
|
|
26
26
|
color = undefined,
|
|
27
27
|
size = undefined,
|
|
28
|
-
|
|
28
|
+
step = undefined,
|
|
29
29
|
font = undefined,
|
|
30
30
|
} = defineProps<{
|
|
31
31
|
weight?: FontWeight
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
transform?: TextTransform
|
|
38
38
|
color?: UiColors
|
|
39
39
|
size?: FontSize
|
|
40
|
-
|
|
40
|
+
step?: TypeStep
|
|
41
41
|
font?: FontFamily
|
|
42
42
|
}>()
|
|
43
43
|
const { classes } = useTypography(() => ({
|
|
@@ -50,9 +50,9 @@
|
|
|
50
50
|
transform,
|
|
51
51
|
...(font !== undefined && { font }),
|
|
52
52
|
...(size !== undefined && { size }),
|
|
53
|
-
...(
|
|
53
|
+
...(step !== undefined && { step }),
|
|
54
54
|
}))
|
|
55
|
-
const colorClass = useColor(color, 'text')
|
|
55
|
+
const colorClass = useColor(() => color, 'text')
|
|
56
56
|
</script>
|
|
57
57
|
|
|
58
58
|
<template>
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { useRepeatFill } from '../../composables/repeatFill'
|
|
4
4
|
import { useTypography } from '../../composables/typography'
|
|
5
5
|
import type { UiColors } from '../../types/colors'
|
|
6
|
-
import type {
|
|
6
|
+
import type { FontWeight, TypeStep, TypographyTag } from '../../types/typography'
|
|
7
7
|
|
|
8
8
|
const {
|
|
9
9
|
rows = 1,
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
repeat = 'auto',
|
|
13
13
|
separator = '',
|
|
14
14
|
weight = undefined,
|
|
15
|
-
|
|
15
|
+
step = 7,
|
|
16
16
|
color = undefined,
|
|
17
17
|
offset = [],
|
|
18
18
|
letterSpacing = undefined,
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
repeat?: number | 'auto'
|
|
29
29
|
separator?: string
|
|
30
30
|
weight?: FontWeight | FontWeight[]
|
|
31
|
-
|
|
31
|
+
step?: TypeStep | TypeStep[]
|
|
32
32
|
color?: UiColors | UiColors[]
|
|
33
33
|
offset?: string[]
|
|
34
34
|
letterSpacing?: string | string[]
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
|
|
50
50
|
const rowText = (i: number) => pick(text, i) ?? ''
|
|
51
51
|
const rowWeight = (i: number) => pick(weight, i)
|
|
52
|
-
const
|
|
52
|
+
const rowStep = (i: number) => pick(step, i)
|
|
53
53
|
const rowColor = (i: number) => pick(color, i)
|
|
54
54
|
const rowOffset = (i: number) => offset[i] ?? '0'
|
|
55
55
|
const rowLetterSpacing = (i: number) => pick(letterSpacing, i)
|
|
@@ -58,10 +58,11 @@
|
|
|
58
58
|
const rowClasses = computed(() =>
|
|
59
59
|
Array.from({ length: rows }, (_, i) => {
|
|
60
60
|
const weight = rowWeight(i)
|
|
61
|
-
const
|
|
61
|
+
const step = rowStep(i)
|
|
62
62
|
return useTypography({
|
|
63
|
+
leading: 'leading-normal',
|
|
63
64
|
...(weight !== undefined && { weight }),
|
|
64
|
-
...(
|
|
65
|
+
...(step !== undefined && { step }),
|
|
65
66
|
}).classes.value
|
|
66
67
|
})
|
|
67
68
|
)
|
|
@@ -0,0 +1,31 @@
|
|
|
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
|
+
})
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
import { useTypography } from '../../composables/typography'
|
|
4
4
|
import type { UiColors } from '../../types/colors'
|
|
5
5
|
import type {
|
|
6
|
-
FluidFontSize,
|
|
7
6
|
FontFamily,
|
|
8
7
|
FontLeading,
|
|
9
8
|
FontSize,
|
|
@@ -13,6 +12,7 @@
|
|
|
13
12
|
FontWidth,
|
|
14
13
|
TextAlign,
|
|
15
14
|
TextTransform,
|
|
15
|
+
TypeStep,
|
|
16
16
|
TypographyTag,
|
|
17
17
|
} from '../../types/typography'
|
|
18
18
|
|
|
@@ -21,13 +21,15 @@
|
|
|
21
21
|
weight = 'font-normal',
|
|
22
22
|
width = 'font-stretch-normal',
|
|
23
23
|
slant = 'normal',
|
|
24
|
-
leading =
|
|
24
|
+
leading = undefined,
|
|
25
25
|
tracking = 'tracking-normal',
|
|
26
26
|
align = 'left',
|
|
27
27
|
transform = 'none',
|
|
28
28
|
color = undefined,
|
|
29
|
-
size
|
|
30
|
-
|
|
29
|
+
// No default size: body copy inherits the body step (20 → 24px) rather than pinning
|
|
30
|
+
// Tailwind's 16px text-base inside it.
|
|
31
|
+
size = undefined,
|
|
32
|
+
step = undefined,
|
|
31
33
|
font = undefined,
|
|
32
34
|
} = defineProps<{
|
|
33
35
|
tag?: TypographyTag
|
|
@@ -40,22 +42,22 @@
|
|
|
40
42
|
transform?: TextTransform
|
|
41
43
|
color?: UiColors
|
|
42
44
|
size?: FontSize
|
|
43
|
-
|
|
45
|
+
step?: TypeStep
|
|
44
46
|
font?: FontFamily
|
|
45
47
|
}>()
|
|
46
48
|
const { classes } = useTypography(() => ({
|
|
47
49
|
weight,
|
|
48
50
|
width,
|
|
49
51
|
slant,
|
|
50
|
-
leading,
|
|
52
|
+
...(leading !== undefined && { leading }),
|
|
51
53
|
tracking,
|
|
52
54
|
align,
|
|
53
55
|
transform,
|
|
54
|
-
size,
|
|
56
|
+
...(size !== undefined && { size }),
|
|
55
57
|
...(font !== undefined && { font }),
|
|
56
|
-
...(
|
|
58
|
+
...(step !== undefined && { step }),
|
|
57
59
|
}))
|
|
58
|
-
const colorClass = useColor(color, 'text')
|
|
60
|
+
const colorClass = useColor(() => color, 'text')
|
|
59
61
|
</script>
|
|
60
62
|
|
|
61
63
|
<template>
|
|
@@ -0,0 +1,30 @@
|
|
|
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,5 +1,12 @@
|
|
|
1
|
+
import { computed, toValue, type MaybeRefOrGetter } from 'vue'
|
|
2
|
+
|
|
1
3
|
import type { ColorUsage, UiColors } from '../types/colors'
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
+
// Takes a getter (`() => color`) when `color` is a destructured prop — passing the bare
|
|
6
|
+
// value reads it once at setup and the class never updates.
|
|
7
|
+
export function useColor(
|
|
8
|
+
color?: MaybeRefOrGetter<UiColors | undefined>,
|
|
9
|
+
type: ColorUsage = 'text'
|
|
10
|
+
) {
|
|
11
|
+
return computed(() => `${type}-${toValue(color) ?? 'default'}`)
|
|
5
12
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
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,7 +1,6 @@
|
|
|
1
|
-
import { toValue, type MaybeRefOrGetter } from 'vue'
|
|
1
|
+
import { computed, toValue, type MaybeRefOrGetter } from 'vue'
|
|
2
2
|
|
|
3
3
|
import type {
|
|
4
|
-
FluidFontSize,
|
|
5
4
|
FontFamily,
|
|
6
5
|
FontLeading,
|
|
7
6
|
FontSize,
|
|
@@ -11,6 +10,7 @@ import type {
|
|
|
11
10
|
FontWidth,
|
|
12
11
|
TextAlign,
|
|
13
12
|
TextTransform,
|
|
13
|
+
TypeStep,
|
|
14
14
|
} from '../types/typography'
|
|
15
15
|
|
|
16
16
|
type AxisValue = string | number | undefined
|
|
@@ -36,39 +36,25 @@ function getSizeClass(size: FontSize | undefined): string {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
// Literal map, not template-literal interpolation — Tailwind's content scanner
|
|
39
|
-
// looks for complete class-name strings in source, so `text-
|
|
40
|
-
// invisible to it and silently drops the utility.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
sm: 'text-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
'lg-cq': 'text-fluid-lg-cq',
|
|
59
|
-
'xl-cq': 'text-fluid-xl-cq',
|
|
60
|
-
'2xl-cq': 'text-fluid-2xl-cq',
|
|
61
|
-
'3xl-cq': 'text-fluid-3xl-cq',
|
|
62
|
-
'4xl-cq': 'text-fluid-4xl-cq',
|
|
63
|
-
'5xl-cq': 'text-fluid-5xl-cq',
|
|
64
|
-
'6xl-cq': 'text-fluid-6xl-cq',
|
|
65
|
-
'7xl-cq': 'text-fluid-7xl-cq',
|
|
66
|
-
'8xl-cq': 'text-fluid-8xl-cq',
|
|
67
|
-
'9xl-cq': 'text-fluid-9xl-cq',
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function getFluidSizeClass(fluidSize: FluidFontSize | undefined): string {
|
|
71
|
-
return fluidSize ? FLUID_SIZE_CLASSES[fluidSize] : ''
|
|
39
|
+
// looks for complete class-name strings in source, so `text-step-${x}` is
|
|
40
|
+
// invisible to it and silently drops the utility. Kept in sync with
|
|
41
|
+
// typography.css's @utility list by typography.test.ts.
|
|
42
|
+
export const TYPE_STEP_CLASSES: Record<TypeStep, string> = {
|
|
43
|
+
sm: 'text-step-sm',
|
|
44
|
+
0: 'text-step-0',
|
|
45
|
+
1: 'text-step-1',
|
|
46
|
+
2: 'text-step-2',
|
|
47
|
+
3: 'text-step-3',
|
|
48
|
+
4: 'text-step-4',
|
|
49
|
+
5: 'text-step-5',
|
|
50
|
+
6: 'text-step-6',
|
|
51
|
+
7: 'text-step-7',
|
|
52
|
+
8: 'text-step-8',
|
|
53
|
+
9: 'text-step-9',
|
|
54
|
+
10: 'text-step-10',
|
|
55
|
+
11: 'text-step-11',
|
|
56
|
+
12: 'text-step-12',
|
|
57
|
+
display: 'text-step-display',
|
|
72
58
|
}
|
|
73
59
|
|
|
74
60
|
export function useTypography(
|
|
@@ -81,7 +67,7 @@ export function useTypography(
|
|
|
81
67
|
align?: TextAlign
|
|
82
68
|
transform?: TextTransform
|
|
83
69
|
size?: FontSize
|
|
84
|
-
|
|
70
|
+
step?: TypeStep
|
|
85
71
|
font?: FontFamily
|
|
86
72
|
}>
|
|
87
73
|
) {
|
|
@@ -103,20 +89,19 @@ export function useTypography(
|
|
|
103
89
|
fallback: 'not-italic',
|
|
104
90
|
}),
|
|
105
91
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
}),
|
|
92
|
+
// No fallback: a leading-* class sorts after text-step-* and would override the step's
|
|
93
|
+
// line height, so only emit one when asked.
|
|
94
|
+
normalizeAxis(p.leading, { prefix: 'leading' }),
|
|
110
95
|
|
|
111
96
|
normalizeAxis(p.tracking, {
|
|
112
97
|
prefix: 'tracking',
|
|
113
98
|
fallback: 'tracking-normal',
|
|
114
99
|
}),
|
|
115
100
|
|
|
116
|
-
//
|
|
101
|
+
// step is the parallel modular-scale namespace (see typography.css) — takes
|
|
117
102
|
// precedence over size when both are set, rather than emitting two
|
|
118
|
-
// conflicting font-size utilities.
|
|
119
|
-
p.
|
|
103
|
+
// conflicting font-size utilities. Checked against undefined: step 0 is valid.
|
|
104
|
+
p.step !== undefined ? TYPE_STEP_CLASSES[p.step] : getSizeClass(p.size),
|
|
120
105
|
|
|
121
106
|
p.align ? `text-${p.align}` : '',
|
|
122
107
|
|
|
@@ -61,40 +61,13 @@ export type FontSize =
|
|
|
61
61
|
| number
|
|
62
62
|
|
|
63
63
|
/* ======================================================
|
|
64
|
-
|
|
65
|
-
-
|
|
66
|
-
- Companion to text-
|
|
64
|
+
Type Step
|
|
65
|
+
- Step on the modular scale: base × 1.26^step, parallel to Tailwind's --text-*
|
|
66
|
+
- Companion to text-step-* utilities (typography.css)
|
|
67
|
+
- 'sm' is step −1; 'display' is fluid, 5 → 7 with viewport width
|
|
67
68
|
====================================================== */
|
|
68
69
|
|
|
69
|
-
export type
|
|
70
|
-
| 'xs'
|
|
71
|
-
| 'sm'
|
|
72
|
-
| 'md'
|
|
73
|
-
| 'lg'
|
|
74
|
-
| 'xl'
|
|
75
|
-
| '2xl'
|
|
76
|
-
| '3xl'
|
|
77
|
-
| '4xl'
|
|
78
|
-
| '5xl'
|
|
79
|
-
| '6xl'
|
|
80
|
-
| '7xl'
|
|
81
|
-
| '8xl'
|
|
82
|
-
| '9xl'
|
|
83
|
-
// Container-relative (cqi) variants — require a `container-type` ancestor
|
|
84
|
-
// (e.g. `@container`), otherwise cqi resolves against the small viewport.
|
|
85
|
-
| 'xs-cq'
|
|
86
|
-
| 'sm-cq'
|
|
87
|
-
| 'md-cq'
|
|
88
|
-
| 'lg-cq'
|
|
89
|
-
| 'xl-cq'
|
|
90
|
-
| '2xl-cq'
|
|
91
|
-
| '3xl-cq'
|
|
92
|
-
| '4xl-cq'
|
|
93
|
-
| '5xl-cq'
|
|
94
|
-
| '6xl-cq'
|
|
95
|
-
| '7xl-cq'
|
|
96
|
-
| '8xl-cq'
|
|
97
|
-
| '9xl-cq'
|
|
70
|
+
export type TypeStep = 'sm' | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 'display'
|
|
98
71
|
|
|
99
72
|
/* ======================================================
|
|
100
73
|
Font Width
|
|
@@ -0,0 +1,22 @@
|
|
|
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,18 +1,17 @@
|
|
|
1
|
-
import { toValue, type ComputedRef, type CSSProperties, type MaybeRefOrGetter } from 'vue'
|
|
1
|
+
import { computed, toValue, type ComputedRef, type CSSProperties, type MaybeRefOrGetter } from 'vue'
|
|
2
2
|
|
|
3
3
|
import { TINT_LEVEL_OPACITY, type TintConfig } from '../types/tint'
|
|
4
|
+
import { resolveUiColorToken } from '../utils/colorTokens'
|
|
4
5
|
|
|
5
6
|
export function useTint(config: MaybeRefOrGetter<TintConfig>): {
|
|
6
7
|
style: ComputedRef<CSSProperties>
|
|
7
8
|
} {
|
|
8
9
|
const style = computed((): CSSProperties => {
|
|
9
10
|
const { color, shade = 500, level } = toValue(config)
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const opacity = TINT_LEVEL_OPACITY[level] / 100
|
|
15
|
-
return { backgroundColor, opacity }
|
|
11
|
+
return {
|
|
12
|
+
backgroundColor: resolveUiColorToken({ color, shade }),
|
|
13
|
+
opacity: TINT_LEVEL_OPACITY[level] / 100,
|
|
14
|
+
}
|
|
16
15
|
})
|
|
17
16
|
|
|
18
17
|
return { style }
|