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.
Files changed (61) hide show
  1. package/docs/adr/0001-grid-replaces-layout-swiss-grid.md +30 -0
  2. package/docs/adr/0002-content-config-is-app-owned.md +27 -0
  3. package/docs/adr/0003-modular-step-system-replaces-fluid-clamps.md +58 -0
  4. package/layers/core/app/assets/css/tokens/spacing-fluid.css +24 -19
  5. package/layers/core/app/components/Element.test.ts +2 -2
  6. package/layers/core/app/components/Element.vue +2 -66
  7. package/layers/core/app/components/ElementTw.vue +2 -66
  8. package/layers/core/app/composables/useElementStyle.ts +5 -59
  9. package/layers/core/app/composables/useLayoutAttrs.test.ts +35 -5
  10. package/layers/core/app/composables/useLayoutAttrs.ts +21 -21
  11. package/layers/core/app/types/element.ts +41 -18
  12. package/layers/core/app/utils/resolveElementStyle.test.ts +99 -0
  13. package/layers/core/app/utils/resolveElementStyle.ts +52 -0
  14. package/layers/data/metadata/providers/comicvine/server/utils/metadata-comicvine/client.ts +1 -0
  15. package/layers/data/metadata/providers/comicvine/server/utils/metadata-comicvine/provider.ts +0 -10
  16. package/layers/data/metadata/providers/google-books/server/utils/metadata-google-books/client.ts +12 -11
  17. package/layers/data/metadata/providers/google-books/server/utils/metadata-google-books/provider.ts +0 -7
  18. package/layers/data/metadata/providers/openlibrary/server/utils/metadata-openlibrary/client.ts +12 -23
  19. package/layers/data/metadata/providers/openlibrary/server/utils/metadata-openlibrary/provider.ts +0 -10
  20. package/layers/data/metadata/providers/themoviedb/server/utils/metadata-tmdb/client.ts +4 -7
  21. package/layers/data/metadata/providers/themoviedb/server/utils/metadata-tmdb/provider.ts +0 -6
  22. package/layers/data/metadata/server/api/metadata/lookup.get.ts +7 -42
  23. package/layers/data/metadata/server/api/metadata/sync.post.ts +4 -49
  24. package/layers/data/metadata/server/utils/metadata/provider-fetch.test.ts +27 -0
  25. package/layers/data/metadata/server/utils/metadata/provider-fetch.ts +14 -0
  26. package/layers/data/metadata/server/utils/metadata/resolve-record.test.ts +87 -0
  27. package/layers/data/metadata/server/utils/metadata/resolve-record.ts +60 -0
  28. package/layers/data/metadata/shared/types/providers.ts +3 -2
  29. package/layers/design-system/theming/app/types/designSystem.ts +21 -17
  30. package/layers/design-system/theming/nuxt.config.ts +1 -1
  31. package/layers/design-system/typography/app/app.config.ts +26 -0
  32. package/layers/design-system/typography/app/assets/css/typography.css +216 -187
  33. package/layers/design-system/typography/app/components/Typography/CodeBlock.vue +5 -5
  34. package/layers/design-system/typography/app/components/Typography/Headline.test.ts +36 -0
  35. package/layers/design-system/typography/app/components/Typography/Headline.vue +21 -22
  36. package/layers/design-system/typography/app/components/Typography/QuoteBlock.vue +5 -5
  37. package/layers/design-system/typography/app/components/Typography/RepeatText.vue +7 -6
  38. package/layers/design-system/typography/app/components/Typography/index.test.ts +31 -0
  39. package/layers/design-system/typography/app/components/Typography/index.vue +11 -9
  40. package/layers/design-system/typography/app/composables/color.test.ts +30 -0
  41. package/layers/design-system/typography/app/composables/color.ts +9 -2
  42. package/layers/design-system/typography/app/composables/typography.test.ts +53 -0
  43. package/layers/design-system/typography/app/composables/typography.ts +28 -43
  44. package/layers/design-system/typography/app/types/typography.ts +5 -32
  45. package/layers/design-system/visual/app/composables/tint.test.ts +22 -0
  46. package/layers/design-system/visual/app/composables/tint.ts +6 -7
  47. package/layers/render/shader/nuxt.config.ts +1 -1
  48. package/layers/structure/grid/app/assets/css/grid.css +11 -7
  49. package/layers/structure/navigation/app/components/Links/Named.vue +1 -1
  50. package/package.json +5 -4
  51. package/layers/core/app/composables/useElementGrid.test.ts +0 -33
  52. package/layers/core/app/composables/useElementGrid.ts +0 -29
  53. package/layers/core/app/composables/useElementInteraction.test.ts +0 -27
  54. package/layers/core/app/composables/useElementInteraction.ts +0 -17
  55. package/layers/core/app/composables/useElementLayout.test.ts +0 -41
  56. package/layers/core/app/composables/useElementLayout.ts +0 -26
  57. package/layers/core/app/composables/useElementSizing.test.ts +0 -39
  58. package/layers/core/app/composables/useElementSizing.ts +0 -25
  59. package/layers/core/app/composables/useElementSurface.test.ts +0 -33
  60. package/layers/core/app/composables/useElementSurface.ts +0 -21
  61. package/layers/design-system/theming/app/composables/useThemePreferences.ts +0 -5
@@ -0,0 +1,30 @@
1
+ # 0001 — `grid` replaces `layout`'s Swiss grid; don't merge them piecemeal
2
+
3
+ Status: accepted (2026-10-20)
4
+
5
+ ## Context
6
+
7
+ Tier 2 has two grid systems:
8
+
9
+ - `layout` — fixed 6/12/18 columns by breakpoint (`LayoutGridItem`, `useGridConfig`,
10
+ `layoutLayer.ui.grid`). Seven `content` components and `Hero`/`Split`/`Gallery` assume 18
11
+ numbered columns.
12
+ - `grid` — data-driven tracks (`useGridTracks`), named presets, rhythm derived from `--fs`,
13
+ named-line placement. No layer consumes it yet; only two playground pages do.
14
+
15
+ `docs/superpowers/specs/2026-09-04-grid-layer-design.md` created `grid` as the replacement and
16
+ put `layout`'s grid components out of scope, deferring their migration.
17
+
18
+ ## Decision
19
+
20
+ Treat the overlap as an in-flight migration, not accidental duplication. Do not fold either
21
+ system into the other, share a preset registry, or rename config namespaces in isolation.
22
+ Migrating `layout` to consume `grid` is its own brainstormed pass, covering `LayoutGridItem`,
23
+ the `content` components, and the 18-column assumptions.
24
+
25
+ ## Consequences
26
+
27
+ - `layoutLayer.ui.grid` (including the z-index `layers` that `navigation` reads through
28
+ `useGridConfig().useZIndex`) is consumer-facing app config and stays as is until that pass.
29
+ - Architecture reviews should not re-suggest merging `layout` and `grid` or picking a single
30
+ "owner" for the `swiss` preset.
@@ -0,0 +1,27 @@
1
+ # 0002 — Each app owns its `content.config.ts`; don't hoist collections into the layer
2
+
3
+ Status: accepted (2026-10-20)
4
+
5
+ ## Context
6
+
7
+ `apps/playground/content.config.ts` redeclares the four collections that
8
+ `layers/content/content.config.ts` also defines, which looks like duplication.
9
+
10
+ `@nuxt/content` 3.15.2 (`loadContentConfig`) loads `content.config.ts` from every layer in
11
+ `nuxt.options._layers` and merges collections by name, with the app last. Each collection is
12
+ tagged `__rootDir` = the config's own layer directory, and source scanning is rooted there. A
13
+ collection defined only in the layer therefore reads `layers/content/content/**`, never the
14
+ app's `content/` folder.
15
+
16
+ ## Decision
17
+
18
+ Each consumer app declares its own collections. The layer's `content.config.ts` serves standalone
19
+ runs only, and it is not in the published package `files`. Do not try to remove the playground
20
+ copy, wrap `defineCollection` in layer-exported factories, or point `source` at the app from the
21
+ layer.
22
+
23
+ ## Consequences
24
+
25
+ - Drift risk is limited to four glob strings per app; `layers/content/CLAUDE.md` holds the
26
+ copy-paste config.
27
+ - Schemas must still never be passed to `defineCollection` (c12/jiti validator issue).
@@ -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 — clamp() + cqi container-relative variant, same
3
- * pattern as layers/typography's fluid type scale (see typography.css).
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(). --fluid-space-none is 0, not clamp() — no scaling needed.
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
- --fluid-space-none: 0px;
9
- --fluid-space-xs: clamp(0.5rem, 0.46rem + 0.2vi, 0.625rem);
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
- /* Container-relative counterparts (cqi) — opt-in inside a `container-type` ancestor. */
18
- --fluid-space-xs-cq: clamp(0.5rem, 0.46rem + 2cqi, 0.625rem);
19
- --fluid-space-sm-cq: clamp(0.75rem, 0.65rem + 2.5cqi, 1rem);
20
- --fluid-space-md-cq: clamp(1rem, 0.8rem + 5cqi, 1.5rem);
21
- --fluid-space-lg-cq: clamp(1.5rem, 1.1rem + 10cqi, 2.5rem);
22
- --fluid-space-xl-cq: clamp(2rem, 1.4rem + 15cqi, 3.5rem);
23
- --fluid-space-2xl-cq: clamp(3rem, 2rem + 25cqi, 5rem);
24
- --fluid-space-3xl-cq: clamp(4rem, 2.5rem + 37.5cqi, 7rem);
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(--fluid-space-md)')
73
- expect(style).toContain('padding: var(--fluid-space-lg)')
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', () => {
@@ -2,73 +2,9 @@
2
2
  import { useElementStyle } from '../composables/useElementStyle'
3
3
  import type { ElementProps } from '../types/element'
4
4
 
5
- const {
6
- as = 'div',
7
- componentProps,
8
- block,
9
- flex,
10
- grid: gridMode,
11
- hidden,
12
- cols,
13
- rows,
14
- gap,
15
- align,
16
- justify,
17
- p,
18
- px,
19
- py,
20
- m,
21
- mx,
22
- my,
23
- w,
24
- h,
25
- minW,
26
- maxW,
27
- minH,
28
- maxH,
29
- aspect,
30
- bg,
31
- color,
32
- border,
33
- radius,
34
- shadow,
35
- cursor,
36
- select,
37
- pointer,
38
- } = defineProps<ElementProps>()
5
+ const { as = 'div', componentProps, ...styleProps } = defineProps<ElementProps>()
39
6
 
40
- const style = useElementStyle(() => ({
41
- block,
42
- flex,
43
- grid: gridMode,
44
- hidden,
45
- cols,
46
- rows,
47
- gap,
48
- align,
49
- justify,
50
- p,
51
- px,
52
- py,
53
- m,
54
- mx,
55
- my,
56
- w,
57
- h,
58
- minW,
59
- maxW,
60
- minH,
61
- maxH,
62
- aspect,
63
- bg,
64
- color,
65
- border,
66
- radius,
67
- shadow,
68
- cursor,
69
- select,
70
- pointer,
71
- }))
7
+ const style = useElementStyle(() => styleProps)
72
8
  </script>
73
9
 
74
10
  <template>
@@ -8,73 +8,9 @@
8
8
  import type { ElementProps } from '../types/element'
9
9
  import { styleToTailwindClasses } from '../utils/styleToTailwindClasses'
10
10
 
11
- const {
12
- as = 'div',
13
- componentProps,
14
- block,
15
- flex,
16
- grid: gridMode,
17
- hidden,
18
- cols,
19
- rows,
20
- gap,
21
- align,
22
- justify,
23
- p,
24
- px,
25
- py,
26
- m,
27
- mx,
28
- my,
29
- w,
30
- h,
31
- minW,
32
- maxW,
33
- minH,
34
- maxH,
35
- aspect,
36
- bg,
37
- color,
38
- border,
39
- radius,
40
- shadow,
41
- cursor,
42
- select,
43
- pointer,
44
- } = defineProps<ElementProps>()
11
+ const { as = 'div', componentProps, ...styleProps } = defineProps<ElementProps>()
45
12
 
46
- const style = useElementStyle(() => ({
47
- block,
48
- flex,
49
- grid: gridMode,
50
- hidden,
51
- cols,
52
- rows,
53
- gap,
54
- align,
55
- justify,
56
- p,
57
- px,
58
- py,
59
- m,
60
- mx,
61
- my,
62
- w,
63
- h,
64
- minW,
65
- maxW,
66
- minH,
67
- maxH,
68
- aspect,
69
- bg,
70
- color,
71
- border,
72
- radius,
73
- shadow,
74
- cursor,
75
- select,
76
- pointer,
77
- }))
13
+ const style = useElementStyle(() => styleProps)
78
14
 
79
15
  const twClasses = computed(() => styleToTailwindClasses(style.value))
80
16
  </script>
@@ -1,65 +1,11 @@
1
1
  import { computed, type ComputedRef, type CSSProperties } from 'vue'
2
2
 
3
- import type { ElementProps } from '../types/element'
4
- import { useElementGrid } from './useElementGrid'
5
- import { useElementInteraction } from './useElementInteraction'
6
- import { useElementLayout } from './useElementLayout'
7
- import { useElementSizing } from './useElementSizing'
8
- import { useElementSurface } from './useElementSurface'
9
- import { useLayoutAttrs } from './useLayoutAttrs'
3
+ import { resolveElementStyle, type ElementStyleInput } from '../utils/resolveElementStyle'
10
4
 
11
5
  /**
12
- * Merges the 5 element composables + core's useLayoutAttrs into one style
13
- * object — the vocabulary shared by both Element (inline :style) and
14
- * ElementTw (Tailwind arbitrary-property classes). Takes a getter (not the
15
- * values directly) so callers can pass their own destructured defineProps
16
- * bindings and keep reactivity, same as useLayoutAttrs's own getter param.
6
+ * Reactive wrapper over resolveElementStyle. Takes a getter so callers can pass their
7
+ * `defineProps` result directly (`useElementStyle(() => props)`) and keep reactivity.
17
8
  */
18
- export function useElementStyle(
19
- getProps: () => Omit<ElementProps, 'as' | 'componentProps'>
20
- ): ComputedRef<CSSProperties> {
21
- return computed(() => {
22
- const props = getProps()
23
- return {
24
- ...useElementLayout({
25
- block: props.block,
26
- flex: props.flex,
27
- grid: props.grid,
28
- hidden: props.hidden,
29
- }),
30
- ...useLayoutAttrs(() => ({
31
- gap: props.gap,
32
- align: props.align,
33
- justify: props.justify,
34
- p: props.p,
35
- px: props.px,
36
- py: props.py,
37
- m: props.m,
38
- mx: props.mx,
39
- my: props.my,
40
- })).style.value,
41
- ...useElementGrid({ grid: props.grid, cols: props.cols, rows: props.rows }),
42
- ...useElementSizing({
43
- w: props.w,
44
- h: props.h,
45
- minW: props.minW,
46
- maxW: props.maxW,
47
- minH: props.minH,
48
- maxH: props.maxH,
49
- aspect: props.aspect,
50
- }),
51
- ...useElementSurface({
52
- bg: props.bg,
53
- color: props.color,
54
- border: props.border,
55
- radius: props.radius,
56
- shadow: props.shadow,
57
- }),
58
- ...useElementInteraction({
59
- cursor: props.cursor,
60
- select: props.select,
61
- pointer: props.pointer,
62
- }),
63
- }
64
- })
9
+ export function useElementStyle(getProps: () => ElementStyleInput): ComputedRef<CSSProperties> {
10
+ return computed(() => resolveElementStyle(getProps()))
65
11
  }
@@ -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(--fluid-space-md)')
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(--fluid-space-lg)')
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(--fluid-space-sm)')
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(--fluid-space-md)',
42
- padding: 'var(--fluid-space-lg)',
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(--fluid-space-${token})`
50
+ return token === 'none' ? '0px' : `var(--space-${token})`
51
51
  }
52
52
 
53
53
  function applySpacing(
@@ -66,29 +66,29 @@ function applySpacing(
66
66
  if (resolved) result[cssProp] = spacingVar(resolved)
67
67
  }
68
68
 
69
- export function useLayoutAttrs(input: MaybeRefOrGetter<LayoutAttrsInput>): {
70
- style: ComputedRef<CSSProperties>
71
- } {
72
- const style = computed((): CSSProperties => {
73
- const { gap, align, justify, p, px, py, m, mx, my } = toValue(input)
74
- const result: CSSProperties = {}
69
+ export function resolveLayoutAttrsStyle(input: LayoutAttrsInput): CSSProperties {
70
+ const { gap, align, justify, p, px, py, m, mx, my } = input
71
+ const result: CSSProperties = {}
75
72
 
76
- applySpacing(result, 'gap', gap)
77
- applySpacing(result, 'padding', p)
78
- applySpacing(result, 'paddingInline', px)
79
- applySpacing(result, 'paddingBlock', py)
80
- applySpacing(result, 'margin', m)
81
- applySpacing(result, 'marginInline', mx)
82
- applySpacing(result, 'marginBlock', my)
73
+ applySpacing(result, 'gap', gap)
74
+ applySpacing(result, 'padding', p)
75
+ applySpacing(result, 'paddingInline', px)
76
+ applySpacing(result, 'paddingBlock', py)
77
+ applySpacing(result, 'margin', m)
78
+ applySpacing(result, 'marginInline', mx)
79
+ applySpacing(result, 'marginBlock', my)
83
80
 
84
- const resolvedAlign = resolveDefault(align)
85
- if (resolvedAlign) result.alignItems = ALIGN_MAP[resolvedAlign]
81
+ const resolvedAlign = resolveDefault(align)
82
+ if (resolvedAlign) result.alignItems = ALIGN_MAP[resolvedAlign]
86
83
 
87
- const resolvedJustify = resolveDefault(justify)
88
- if (resolvedJustify) result.justifyContent = JUSTIFY_MAP[resolvedJustify]
84
+ const resolvedJustify = resolveDefault(justify)
85
+ if (resolvedJustify) result.justifyContent = JUSTIFY_MAP[resolvedJustify]
89
86
 
90
- return result
91
- })
87
+ return result
88
+ }
92
89
 
93
- return { style }
90
+ export function useLayoutAttrs(input: MaybeRefOrGetter<LayoutAttrsInput>): {
91
+ style: ComputedRef<CSSProperties>
92
+ } {
93
+ return { style: computed(() => resolveLayoutAttrsStyle(toValue(input))) }
94
94
  }
@@ -1,25 +1,48 @@
1
- import type { Component } from 'vue'
1
+ import type { Component, CSSProperties } from 'vue'
2
2
 
3
- import type { ElementGridProps } from '../composables/useElementGrid'
4
- import type { ElementInteractionProps } from '../composables/useElementInteraction'
5
- import type { ElementLayoutProps } from '../composables/useElementLayout'
6
- import type { ElementSizingProps } from '../composables/useElementSizing'
7
- import type { ElementSurfaceProps } from '../composables/useElementSurface'
8
3
  // Relative, not the '#layers/core' alias: the SFC compiler's macro type
9
4
  // resolution (defineProps<ElementProps>() in Element.vue) walks tsconfig
10
5
  // "paths" directly and doesn't see Nuxt's generated layer aliases outside
11
6
  // a running Nuxt instance — a plain relative import resolves everywhere.
12
7
  import type { LayoutAttrsInput } from '../composables/useLayoutAttrs'
13
8
 
14
- export type ElementProps = ElementLayoutProps &
15
- ElementGridProps &
16
- LayoutAttrsInput &
17
- ElementSizingProps &
18
- ElementSurfaceProps &
19
- ElementInteractionProps & {
20
- as?: string | Component
21
- // Escape hatch for prop names that collide with Element's own (e.g. `color`
22
- // is Element's raw-CSS text colour, but also UBadge's semantic colour prop).
23
- // Non-colliding component props still reach `as` for free via attrs fallthrough.
24
- componentProps?: Record<string, unknown>
25
- }
9
+ // `| undefined` on each is explicit (not just `?`) so component prop objects
10
+ // — which have it too, under exactOptionalPropertyTypes — are assignable here.
11
+ export type ElementProps = LayoutAttrsInput & {
12
+ as?: string | Component
13
+ // Escape hatch for prop names that collide with Element's own (e.g. `color`
14
+ // is Element's raw-CSS text colour, but also UBadge's semantic colour prop).
15
+ // Non-colliding component props still reach `as` for free via attrs fallthrough.
16
+ componentProps?: Record<string, unknown>
17
+
18
+ // Display mode — priority hidden > grid > flex > block
19
+ block?: boolean | undefined
20
+ flex?: boolean | undefined
21
+ grid?: boolean | undefined
22
+ hidden?: boolean | undefined
23
+
24
+ // Grid container tracks (numbers become `repeat(n, 1fr)`; only applied when `grid` is set)
25
+ cols?: number | string | undefined
26
+ rows?: number | string | undefined
27
+
28
+ // Sizing
29
+ w?: CSSProperties['width'] | undefined
30
+ h?: CSSProperties['height'] | undefined
31
+ minW?: CSSProperties['minWidth'] | undefined
32
+ maxW?: CSSProperties['maxWidth'] | undefined
33
+ minH?: CSSProperties['minHeight'] | undefined
34
+ maxH?: CSSProperties['maxHeight'] | undefined
35
+ aspect?: CSSProperties['aspectRatio'] | undefined
36
+
37
+ // Surface
38
+ bg?: CSSProperties['background'] | undefined
39
+ color?: CSSProperties['color'] | undefined
40
+ border?: CSSProperties['border'] | undefined
41
+ radius?: CSSProperties['borderRadius'] | undefined
42
+ shadow?: CSSProperties['boxShadow'] | undefined
43
+
44
+ // Interaction
45
+ cursor?: CSSProperties['cursor'] | undefined
46
+ select?: CSSProperties['userSelect'] | undefined
47
+ pointer?: CSSProperties['pointerEvents'] | undefined
48
+ }