kmcom-nuxt-layers 6.0.0 → 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/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/composables/useLayoutAttrs.test.ts +35 -5
- package/layers/core/app/composables/useLayoutAttrs.ts +1 -1
- package/layers/core/app/utils/resolveElementStyle.test.ts +2 -2
- package/layers/design-system/theming/app/types/designSystem.ts +21 -17
- 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 +4 -4
- package/layers/design-system/typography/app/components/Typography/Headline.test.ts +36 -0
- package/layers/design-system/typography/app/components/Typography/Headline.vue +20 -21
- package/layers/design-system/typography/app/components/Typography/QuoteBlock.vue +4 -4
- 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 +10 -8
- 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/structure/grid/app/assets/css/grid.css +11 -7
- package/package.json +1 -1
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# 0003 — One modular step scale replaces the fluid type and spacing clamps
|
|
2
|
+
|
|
3
|
+
Status: accepted (2026-10-20)
|
|
4
|
+
|
|
5
|
+
## Context
|
|
6
|
+
|
|
7
|
+
Type and spacing were two unrelated sets of hand-tuned `clamp()` ranges: 13 `text-fluid-*`
|
|
8
|
+
utilities (plus 13 `-cq` copies) snapped by `round(down, …, 0.25rem)`, and 8 `--fluid-space-*`
|
|
9
|
+
tokens (plus 7 `-cq` copies). No ratio linked one size to the next, so every new size was
|
|
10
|
+
another clamp to tune, and the `-cq` sets had no consumers. The reference system (matuzo.at,
|
|
11
|
+
analysed in `matuzo-typography-system-1.md`) gets better proportions from two ratios.
|
|
12
|
+
|
|
13
|
+
## Decision
|
|
14
|
+
|
|
15
|
+
Every size is a step on a scale:
|
|
16
|
+
|
|
17
|
+
- **Type:** `font-size = round(--type-base × 1.26^--step, 0.0625rem)`. Base is `1rem` below
|
|
18
|
+
48rem and `1.2rem` from 48rem; body is step 1 (20 → 24px). `h1` uses a fluid display step,
|
|
19
|
+
5 → 7 across 360 → 1280px, cast from a length with `tan(atan2(…, 1rem))`. Utilities are
|
|
20
|
+
`text-step-sm`, `text-step-0` … `text-step-12`, `text-step-display`.
|
|
21
|
+
- **Space:** `--space-* = --space-base × 1.414^n`, with `--space-base` running 11.3 → 16px
|
|
22
|
+
across 320 → 1280px — one step of growth, not the reference's two (8 → 16px), so phones keep
|
|
23
|
+
the old token sizes. The `Spacing` union is unchanged; only the values behind it moved.
|
|
24
|
+
- **Grid:** the `grid` layer's `--fs` shares the type scale's endpoint tokens (`--type-min`,
|
|
25
|
+
`--type-max`) but stays continuous, so its rhythm unit does not inherit the 48rem jump.
|
|
26
|
+
|
|
27
|
+
It is a parallel namespace: Tailwind's `--text-*`, Nuxt UI's `--ui-*` and the `html` font-size
|
|
28
|
+
are untouched. Tokens are plain `:root` properties, not `@theme`, so `var(--step)` resolves on
|
|
29
|
+
the element using it.
|
|
30
|
+
|
|
31
|
+
## Consequences
|
|
32
|
+
|
|
33
|
+
- The top end is coarser: `TypographyHeadline` levels step down from the fluid display step
|
|
34
|
+
(97/77/61/48/38/30px at 1280px, 51 → 16px at 320px) instead of the old 144/120/96/76/36/24px,
|
|
35
|
+
which also overflowed phone widths. Plain h1–h6 use the reading scale (display, 3, 2, 1, 0, 0).
|
|
36
|
+
- Step line heights fall smoothly with size (sm 1.5 … body 1.7, h2/h3 1.2 … display 0.95) and
|
|
37
|
+
now reach components: `useTypography` only emits `leading-*` when asked.
|
|
38
|
+
- Body text grows from 16px to 20/24px, and body line-height from 1.5 to 1.7. Nuxt UI
|
|
39
|
+
components with explicit `text-*` sizes are unaffected; anything that inherits body size
|
|
40
|
+
(card bodies, plain prose) grows. Heading defaults skip `[data-slot]` headings, so Nuxt UI's
|
|
41
|
+
own titles (UModal, USlideover) inherit instead of jumping to a heading step.
|
|
42
|
+
- Every step jumps ×1.2 at 48rem because the base switches there (h1 ≈ 62 → 75px at 768px).
|
|
43
|
+
- Spacing matches the old tokens on phones and stays close on desktop for xs–xl; 2xl/3xl top out
|
|
44
|
+
smaller than before (64/90px vs 80/112px).
|
|
45
|
+
- `--space-*` steps are substituted at `:root`, so per-subtree spacing retuning is not
|
|
46
|
+
supported. Type retuning is: `--type-base` and `--type-scale` are read per element.
|
|
47
|
+
- The display step is normalised by `1rem` and its `vw` term is scaled by root / 16px, so its
|
|
48
|
+
exponent depends only on viewport width and every size grows with a larger root font size.
|
|
49
|
+
(Unscaled, the exponent fell as the root grew: h1 was only 1.02× larger at a 24px root.)
|
|
50
|
+
- The old `--heading-text-scale-damp` multiplier is gone. As written it enlarged steps ≥ 7
|
|
51
|
+
under OS text-scale > 100% instead of damping them; a true damp would break "visibly larger
|
|
52
|
+
at a larger root".
|
|
53
|
+
- `--fs` stays unregistered: a registered `<length>` holding `rem` on `:root`, read by the
|
|
54
|
+
root's own line-height, is a dependency cycle in Gecko that freezes the grid unit.
|
|
55
|
+
- Nuxt UI prose is retuned onto the scale (typography `app.config.ts`): prose headings take
|
|
56
|
+
steps (h1 4, h2 3, h3 2, h4 1) and `p`/`li` keep the body's 1.7 leading. Otherwise the
|
|
57
|
+
24px copy sat at or above Nuxt UI's fixed `text-2xl`/`text-xl` prose headings. This needs
|
|
58
|
+
`text-step-*` registered as a font-size group in tailwind-merge (`ui.tv.twMergeConfig`).
|
|
@@ -1,25 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Fluid spacing scale —
|
|
3
|
-
*
|
|
2
|
+
* Fluid spacing scale — one ratio, same idea as typography.css's type scale:
|
|
3
|
+
* space = --space-base × --space-scale ^ n
|
|
4
|
+
* 1.414 ≈ 2^(1/2), so two steps double a gap. --space-base runs 11.3px at ≤320px to 16px
|
|
5
|
+
* at ≥1280px — one step of growth (√2), not the reference's two (8 → 16px), so phones keep
|
|
6
|
+
* the old fluid tokens' sizes (xs 8 · md 16 · xl 32 · 3xl 64px at 320px). The rem term in the
|
|
7
|
+
* middle keeps it zoom-safe (WCAG 1.4.4).
|
|
4
8
|
* Backs the `Spacing` token (layers/core/app/types/tokens.ts) consumed by
|
|
5
|
-
* useLayoutAttrs(). --
|
|
9
|
+
* useLayoutAttrs(). --space-none is 0, not a step.
|
|
10
|
+
* Plain :root properties, not @theme: --space-* is not a Tailwind namespace, and
|
|
11
|
+
* xs/md/xl declared there would risk colliding with --container-* size names.
|
|
12
|
+
*
|
|
13
|
+
* Trap: each step substitutes --space-base here at :root, so overriding --space-base
|
|
14
|
+
* on a subtree does NOT change --space-md inside it. Per-subtree spacing retuning is
|
|
15
|
+
* not a supported extension point (type retuning is — see typography.css).
|
|
6
16
|
*/
|
|
7
17
|
:root {
|
|
8
|
-
--
|
|
9
|
-
--
|
|
10
|
-
--fluid-space-sm: clamp(0.75rem, 0.7rem + 0.25vi, 1rem);
|
|
11
|
-
--fluid-space-md: clamp(1rem, 0.9rem + 0.5vi, 1.5rem);
|
|
12
|
-
--fluid-space-lg: clamp(1.5rem, 1.3rem + 1vi, 2.5rem);
|
|
13
|
-
--fluid-space-xl: clamp(2rem, 1.7rem + 1.5vi, 3.5rem);
|
|
14
|
-
--fluid-space-2xl: clamp(3rem, 2.4rem + 3vi, 5rem);
|
|
15
|
-
--fluid-space-3xl: clamp(4rem, 3rem + 5vi, 7rem);
|
|
18
|
+
--space-scale: 1.414;
|
|
19
|
+
--space-base: clamp(0.7071rem, 0.6095rem + 0.4882vw, 1rem);
|
|
16
20
|
|
|
17
|
-
/*
|
|
18
|
-
--
|
|
19
|
-
--
|
|
20
|
-
--
|
|
21
|
-
--
|
|
22
|
-
--
|
|
23
|
-
--
|
|
24
|
-
--
|
|
21
|
+
/* n = −1 … 5 */
|
|
22
|
+
--space-none: 0px;
|
|
23
|
+
--space-xs: calc(var(--space-base) * pow(var(--space-scale), -1));
|
|
24
|
+
--space-sm: calc(var(--space-base) * pow(var(--space-scale), 0));
|
|
25
|
+
--space-md: calc(var(--space-base) * pow(var(--space-scale), 1));
|
|
26
|
+
--space-lg: calc(var(--space-base) * pow(var(--space-scale), 2));
|
|
27
|
+
--space-xl: calc(var(--space-base) * pow(var(--space-scale), 3));
|
|
28
|
+
--space-2xl: calc(var(--space-base) * pow(var(--space-scale), 4));
|
|
29
|
+
--space-3xl: calc(var(--space-base) * pow(var(--space-scale), 5));
|
|
25
30
|
}
|
|
@@ -69,8 +69,8 @@ describe('Element', () => {
|
|
|
69
69
|
props: { m: 'md', p: 'lg' },
|
|
70
70
|
})
|
|
71
71
|
const style = wrapper.attributes('style') ?? ''
|
|
72
|
-
expect(style).toContain('margin: var(--
|
|
73
|
-
expect(style).toContain('padding: var(--
|
|
72
|
+
expect(style).toContain('margin: var(--space-md)')
|
|
73
|
+
expect(style).toContain('padding: var(--space-lg)')
|
|
74
74
|
})
|
|
75
75
|
|
|
76
76
|
it('passes componentProps to the resolved component, past a colliding prop name', () => {
|
|
@@ -1,21 +1,51 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
|
|
1
3
|
import { describe, expect, it } from 'vite-plus/test'
|
|
2
4
|
|
|
5
|
+
import type { Spacing } from '../types/tokens'
|
|
3
6
|
import { useLayoutAttrs } from './useLayoutAttrs'
|
|
4
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
|
+
|
|
5
35
|
describe('useLayoutAttrs margin support', () => {
|
|
6
36
|
it('sets margin from a static token', () => {
|
|
7
37
|
const { style } = useLayoutAttrs(() => ({ m: 'md' }))
|
|
8
|
-
expect(style.value.margin).toBe('var(--
|
|
38
|
+
expect(style.value.margin).toBe('var(--space-md)')
|
|
9
39
|
})
|
|
10
40
|
|
|
11
41
|
it('sets marginInline from px-equivalent mx', () => {
|
|
12
42
|
const { style } = useLayoutAttrs(() => ({ mx: 'lg' }))
|
|
13
|
-
expect(style.value.marginInline).toBe('var(--
|
|
43
|
+
expect(style.value.marginInline).toBe('var(--space-lg)')
|
|
14
44
|
})
|
|
15
45
|
|
|
16
46
|
it('sets marginBlock from my', () => {
|
|
17
47
|
const { style } = useLayoutAttrs(() => ({ my: 'sm' }))
|
|
18
|
-
expect(style.value.marginBlock).toBe('var(--
|
|
48
|
+
expect(style.value.marginBlock).toBe('var(--space-sm)')
|
|
19
49
|
})
|
|
20
50
|
|
|
21
51
|
it('resolves none to 0px for margin', () => {
|
|
@@ -38,8 +68,8 @@ describe('useLayoutAttrs margin support', () => {
|
|
|
38
68
|
p: 'lg',
|
|
39
69
|
}))
|
|
40
70
|
expect(style.value).toEqual({
|
|
41
|
-
gap: 'var(--
|
|
42
|
-
padding: 'var(--
|
|
71
|
+
gap: 'var(--space-md)',
|
|
72
|
+
padding: 'var(--space-lg)',
|
|
43
73
|
alignItems: 'center',
|
|
44
74
|
justifyContent: 'space-between',
|
|
45
75
|
})
|
|
@@ -47,7 +47,7 @@ function resolveDefault<T>(value: ResponsiveValue<T> | T | undefined): T | undef
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
function spacingVar(token: Spacing): string {
|
|
50
|
-
return token === 'none' ? '0px' : `var(--
|
|
50
|
+
return token === 'none' ? '0px' : `var(--space-${token})`
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
function applySpacing(
|
|
@@ -87,9 +87,9 @@ describe('resolveElementStyle prop mapping', () => {
|
|
|
87
87
|
it('merges layout attrs (gap/align/padding) into the same object', () => {
|
|
88
88
|
expect(resolveElementStyle({ flex: true, gap: 'md', align: 'center', p: 'sm' })).toMatchObject({
|
|
89
89
|
display: 'flex',
|
|
90
|
-
gap: 'var(--
|
|
90
|
+
gap: 'var(--space-md)',
|
|
91
91
|
alignItems: 'center',
|
|
92
|
-
padding: 'var(--
|
|
92
|
+
padding: 'var(--space-sm)',
|
|
93
93
|
})
|
|
94
94
|
})
|
|
95
95
|
|
|
@@ -7,19 +7,21 @@
|
|
|
7
7
|
* (theme adapter component overrides, standalone token exports) will import
|
|
8
8
|
* it directly.
|
|
9
9
|
*/
|
|
10
|
-
import type {
|
|
10
|
+
import type { TypeStep } from '#layers/typography/app/types/typography'
|
|
11
11
|
import type { UiColors } from '#types'
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Design-system tokens — first real config-object source of truth (Phase 5,
|
|
15
15
|
* nuxt-declarative-layout-system-spec.md §21). Colours key off the existing
|
|
16
16
|
* `UiColors` union rather than inventing new names; typography keys off the
|
|
17
|
-
* existing `
|
|
17
|
+
* existing `TypeStep` modular scale/CSS vars in layers/typography — this section
|
|
18
18
|
* only gives them a config-object home so a Nuxt UI theme adapter and CSS
|
|
19
19
|
* custom properties can derive from one place.
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
// Unrounded size of one step — typography.css's text-step-* utilities also round() it.
|
|
23
|
+
const stepSize = (exponent: number | string) =>
|
|
24
|
+
`calc(var(--type-base) * pow(var(--type-scale), ${exponent}))`
|
|
23
25
|
|
|
24
26
|
export const designSystem = {
|
|
25
27
|
colors: {
|
|
@@ -59,20 +61,22 @@ export const designSystem = {
|
|
|
59
61
|
},
|
|
60
62
|
|
|
61
63
|
typography: {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
64
|
+
sm: stepSize(-1),
|
|
65
|
+
0: stepSize(0),
|
|
66
|
+
1: stepSize(1),
|
|
67
|
+
2: stepSize(2),
|
|
68
|
+
3: stepSize(3),
|
|
69
|
+
4: stepSize(4),
|
|
70
|
+
5: stepSize(5),
|
|
71
|
+
6: stepSize(6),
|
|
72
|
+
7: stepSize(7),
|
|
73
|
+
8: stepSize(8),
|
|
74
|
+
9: stepSize(9),
|
|
75
|
+
10: stepSize(10),
|
|
76
|
+
11: stepSize(11),
|
|
77
|
+
12: stepSize(12),
|
|
78
|
+
display: stepSize('var(--step-display)'),
|
|
79
|
+
} satisfies Record<TypeStep, string>,
|
|
76
80
|
|
|
77
81
|
motion: {
|
|
78
82
|
duration: {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { TYPE_STEP_CLASSES } from './composables/typography'
|
|
2
|
+
|
|
3
|
+
// Nuxt UI prose (Nuxt Content) on the modular scale. Nuxt UI pins prose headings to
|
|
4
|
+
// text-4xl/2xl/xl/lg and p/li to leading-7 with no size, so with a 20/24px body the copy sat
|
|
5
|
+
// at or above h2/h3 size on a cramped 28px line. Headings take their step instead (h1 one step
|
|
6
|
+
// above h2, not the display step — content pages already show a page-header title); copy keeps
|
|
7
|
+
// inheriting the body step, with the body's 1.7 leading.
|
|
8
|
+
export default defineAppConfig({
|
|
9
|
+
ui: {
|
|
10
|
+
// tailwind-merge reads unknown text-* classes as a text colour, so a step class would drop
|
|
11
|
+
// a component's colour class and keep its text-{size}. Register them as font sizes.
|
|
12
|
+
tv: {
|
|
13
|
+
twMergeConfig: {
|
|
14
|
+
extend: { classGroups: { 'font-size': Object.values(TYPE_STEP_CLASSES) } },
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
prose: {
|
|
18
|
+
h1: { slots: { base: 'text-step-4' } },
|
|
19
|
+
h2: { slots: { base: 'text-step-3' } },
|
|
20
|
+
h3: { slots: { base: 'text-step-2' } },
|
|
21
|
+
h4: { slots: { base: 'text-step-1' } },
|
|
22
|
+
p: { base: 'leading-[1.7]' },
|
|
23
|
+
li: { base: 'leading-[1.7]' },
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
})
|