kmcom-nuxt-layers 6.0.0 → 6.2.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/USAGE.md +8 -8
- package/docs/adr/0003-modular-step-system-replaces-fluid-clamps.md +58 -0
- package/layers/core/app/app.config.ts +6 -3
- 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/LoadingScreen.vue +142 -76
- package/layers/core/app/composables/useLayoutAttrs.test.ts +35 -5
- package/layers/core/app/composables/useLayoutAttrs.ts +1 -1
- package/layers/core/app/composables/useLoading.ts +57 -36
- package/layers/core/app/plugins/loading.client.ts +21 -27
- package/layers/core/app/types/loading.ts +18 -8
- package/layers/core/app/utils/index.ts +1 -0
- package/layers/core/app/utils/loading.test.ts +48 -0
- package/layers/core/app/utils/loading.ts +53 -0
- 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/motion/scroll/app/plugins/locomotive-scroll.client.ts +22 -0
- package/layers/structure/grid/app/assets/css/grid.css +11 -7
- package/package.json +1 -1
|
@@ -1,24 +1,17 @@
|
|
|
1
1
|
// plugins/loading.client.ts
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
enabled?: boolean
|
|
5
|
-
minDuration?: number
|
|
6
|
-
maxDuration?: number
|
|
7
|
-
background?: string
|
|
8
|
-
textColor?: string
|
|
9
|
-
zIndex?: number
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
type CoreLayerConfig = {
|
|
13
|
-
loading?: LoadingConfig
|
|
14
|
-
}
|
|
3
|
+
import { resolveLoadingConfig } from '../utils/loading'
|
|
15
4
|
|
|
16
5
|
/**
|
|
17
6
|
* Loading screen initialization plugin
|
|
18
7
|
*
|
|
19
|
-
*
|
|
20
|
-
* 1.
|
|
21
|
-
*
|
|
8
|
+
* Client-side only. Once the app has mounted (hydration done) it:
|
|
9
|
+
* 1. Starts the progress count, timed from navigation start - the SSR'd screen has been up
|
|
10
|
+
* since first paint, so the count catches up smoothly rather than starting late
|
|
11
|
+
* 2. Waits for fonts and the initial page's images, capped at `maxDuration`
|
|
12
|
+
* 3. Marks loading done - the count then finishes to 100 before the screen exits
|
|
13
|
+
*
|
|
14
|
+
* Starting after mount keeps the counter at the SSR'd 0 through hydration (no mismatch).
|
|
22
15
|
*
|
|
23
16
|
* Configuration via app.config.ts:
|
|
24
17
|
* - Set `coreLayer.loading.enabled: false` to disable
|
|
@@ -26,26 +19,27 @@ type CoreLayerConfig = {
|
|
|
26
19
|
export default defineNuxtPlugin({
|
|
27
20
|
name: 'core:loading',
|
|
28
21
|
setup(nuxtApp) {
|
|
29
|
-
const
|
|
30
|
-
const coreLayer = config.coreLayer as CoreLayerConfig | undefined
|
|
22
|
+
const { enabled, maxDuration } = resolveLoadingConfig(useAppConfig().coreLayer)
|
|
31
23
|
|
|
32
24
|
// Check if loading is enabled
|
|
33
|
-
if (
|
|
25
|
+
if (!enabled) {
|
|
34
26
|
return // Disabled, skip initialization
|
|
35
27
|
}
|
|
36
28
|
|
|
37
29
|
const { startLoading, stopLoading } = useLoading()
|
|
38
30
|
|
|
39
|
-
//
|
|
40
|
-
startLoading()
|
|
41
|
-
|
|
42
|
-
// Stop loading when app is fully mounted
|
|
31
|
+
// Not awaited: app:mounted handlers run serially, so waiting here would hold up other layers
|
|
43
32
|
nuxtApp.hook('app:mounted', () => {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
33
|
+
startLoading(0)
|
|
34
|
+
void Promise.race([
|
|
35
|
+
Promise.all([pageLoaded(), document.fonts?.ready]),
|
|
36
|
+
new Promise((resolve) => setTimeout(resolve, maxDuration)),
|
|
37
|
+
]).then(stopLoading)
|
|
49
38
|
})
|
|
50
39
|
},
|
|
51
40
|
})
|
|
41
|
+
|
|
42
|
+
function pageLoaded(): Promise<unknown> {
|
|
43
|
+
if (document.readyState === 'complete') return Promise.resolve()
|
|
44
|
+
return new Promise((resolve) => window.addEventListener('load', resolve, { once: true }))
|
|
45
|
+
}
|
|
@@ -6,10 +6,12 @@
|
|
|
6
6
|
export type LoadingConfig = {
|
|
7
7
|
/** Master toggle - set to false to disable completely */
|
|
8
8
|
enabled: boolean
|
|
9
|
-
/** Minimum time
|
|
9
|
+
/** Minimum time (ms, from navigation start) the count runs before it may reach 100 */
|
|
10
10
|
minDuration: number
|
|
11
|
-
/**
|
|
11
|
+
/** Longest wait (ms, after the app mounts) for fonts and images before finishing anyway */
|
|
12
12
|
maxDuration: number
|
|
13
|
+
/** Small caption above the counter - empty string hides it */
|
|
14
|
+
label: string
|
|
13
15
|
/** Background color */
|
|
14
16
|
background: string
|
|
15
17
|
/** Progress text color */
|
|
@@ -18,6 +20,12 @@ export type LoadingConfig = {
|
|
|
18
20
|
zIndex: number
|
|
19
21
|
}
|
|
20
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Boot loading screen lifecycle, shared so other layers can hold their own motion until the
|
|
25
|
+
* page is revealed: covering the page → playing its exit → gone (or never mounted)
|
|
26
|
+
*/
|
|
27
|
+
export type LoadingScreenState = 'visible' | 'leaving' | 'hidden'
|
|
28
|
+
|
|
21
29
|
/**
|
|
22
30
|
* Loading state interface
|
|
23
31
|
*/
|
|
@@ -32,15 +40,17 @@ export type LoadingState = {
|
|
|
32
40
|
* Loading composable return interface
|
|
33
41
|
*/
|
|
34
42
|
export type UseLoadingReturn = {
|
|
35
|
-
/**
|
|
43
|
+
/** True until the displayed progress has counted up to 100 (readonly) */
|
|
36
44
|
isLoading: Readonly<Ref<boolean>>
|
|
37
|
-
/**
|
|
45
|
+
/** Displayed progress 0-100, eased every frame - never jumps or moves backwards (readonly) */
|
|
38
46
|
progress: Readonly<Ref<number>>
|
|
39
|
-
/**
|
|
40
|
-
|
|
41
|
-
/**
|
|
47
|
+
/** Boot loading screen lifecycle (readonly) */
|
|
48
|
+
screen: Readonly<Ref<LoadingScreenState>>
|
|
49
|
+
/** Start simulated progress, timed from `since` (a `performance.now()` value; default now) */
|
|
50
|
+
startLoading: (since?: number) => void
|
|
51
|
+
/** Mark work done - the count finishes to 100 once `minDuration` has passed */
|
|
42
52
|
stopLoading: () => void
|
|
43
|
-
/**
|
|
53
|
+
/** Raise the progress floor (0-99) - only `stopLoading` completes */
|
|
44
54
|
updateProgress: (value: number) => void
|
|
45
55
|
/** Execute async function with loading state */
|
|
46
56
|
withLoading: (fn: () => Promise<void>) => Promise<void>
|
|
@@ -0,0 +1,48 @@
|
|
|
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: true, minDuration: 1500 })
|
|
10
|
+
expect(resolveLoadingConfig({ loading: { minDuration: 0, label: '' } })).toMatchObject({
|
|
11
|
+
enabled: true,
|
|
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
|
+
})
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { LoadingConfig } from '../types/loading'
|
|
2
|
+
|
|
3
|
+
/** Fallbacks for keys app.config leaves unset - core's app.config ships the same values. */
|
|
4
|
+
const LOADING_DEFAULTS: LoadingConfig = {
|
|
5
|
+
enabled: true,
|
|
6
|
+
minDuration: 1500,
|
|
7
|
+
maxDuration: 3000,
|
|
8
|
+
label: 'Loading',
|
|
9
|
+
background: '#0a0a0a',
|
|
10
|
+
textColor: '#ffffff',
|
|
11
|
+
zIndex: 10000,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Reads `coreLayer.loading` from app config, filling unset keys with the defaults. */
|
|
15
|
+
export function resolveLoadingConfig(coreLayer: unknown): LoadingConfig {
|
|
16
|
+
const loading = (coreLayer as { loading?: Partial<LoadingConfig> } | undefined)?.loading
|
|
17
|
+
return { ...LOADING_DEFAULTS, ...loading }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/*
|
|
21
|
+
* Pure easing math behind `useLoading`'s progress counter. `useLoading` runs these once per
|
|
22
|
+
* animation frame: `trickleProgress` moves the target, `approachProgress` moves the displayed
|
|
23
|
+
* value toward it, so the counter climbs smoothly instead of jumping between random values.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/** Simulated progress stays below this until loading actually finishes. */
|
|
27
|
+
const TRICKLE_CAP = 90
|
|
28
|
+
/** Time constant (s) of the displayed value's exponential ease toward its target. */
|
|
29
|
+
const EASE = 0.15
|
|
30
|
+
/** Speed band (%/s) for the displayed value: no crawling at the tail, no leaps on big jumps. */
|
|
31
|
+
const MIN_SPEED = 24
|
|
32
|
+
const MAX_SPEED = 140
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Simulated progress `elapsed` ms after loading started: quick at first, then slowing as it
|
|
36
|
+
* approaches the cap. `tau` (ms) sets the pace — ~63% of the cap is reached after `tau`.
|
|
37
|
+
*/
|
|
38
|
+
export function trickleProgress(elapsed: number, tau: number): number {
|
|
39
|
+
return TRICKLE_CAP * (1 - Math.exp(-Math.max(elapsed, 0) / tau))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Advances `current` toward `target` over `dt` seconds. Eases out as the gap closes, within the
|
|
44
|
+
* speed band, and never moves backwards or past `target`. A rAF timestamp can precede the
|
|
45
|
+
* `performance.now()` the loop started from, so `dt <= 0` is a no-op rather than a step back.
|
|
46
|
+
*/
|
|
47
|
+
export function approachProgress(current: number, target: number, dt: number): number {
|
|
48
|
+
const gap = target - current
|
|
49
|
+
if (gap <= 0 || dt <= 0) return current
|
|
50
|
+
const eased = gap * (1 - Math.exp(-dt / EASE))
|
|
51
|
+
const step = Math.min(Math.max(eased, MIN_SPEED * dt), MAX_SPEED * dt)
|
|
52
|
+
return Math.min(current + step, target)
|
|
53
|
+
}
|
|
@@ -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
|
+
})
|