kmcom-nuxt-layers 5.0.8 → 6.0.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/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.ts +20 -20
- 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/nuxt.config.ts +1 -1
- package/layers/design-system/typography/app/components/Typography/CodeBlock.vue +1 -1
- package/layers/design-system/typography/app/components/Typography/Headline.vue +1 -1
- package/layers/design-system/typography/app/components/Typography/QuoteBlock.vue +1 -1
- package/layers/design-system/typography/app/components/Typography/index.vue +1 -1
- 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/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/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,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).
|
|
@@ -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
|
|
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
|
-
*
|
|
13
|
-
*
|
|
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
|
-
|
|
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
|
}
|
|
@@ -66,29 +66,29 @@ function applySpacing(
|
|
|
66
66
|
if (resolved) result[cssProp] = spacingVar(resolved)
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
export function
|
|
70
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
85
|
-
|
|
81
|
+
const resolvedAlign = resolveDefault(align)
|
|
82
|
+
if (resolvedAlign) result.alignItems = ALIGN_MAP[resolvedAlign]
|
|
86
83
|
|
|
87
|
-
|
|
88
|
-
|
|
84
|
+
const resolvedJustify = resolveDefault(justify)
|
|
85
|
+
if (resolvedJustify) result.justifyContent = JUSTIFY_MAP[resolvedJustify]
|
|
89
86
|
|
|
90
|
-
|
|
91
|
-
|
|
87
|
+
return result
|
|
88
|
+
}
|
|
92
89
|
|
|
93
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
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(--fluid-space-md)',
|
|
91
|
+
alignItems: 'center',
|
|
92
|
+
padding: 'var(--fluid-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
|
+
})
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { CSSProperties } from 'vue'
|
|
2
|
+
|
|
3
|
+
import { resolveLayoutAttrsStyle } from '../composables/useLayoutAttrs'
|
|
4
|
+
import type { ElementProps } from '../types/element'
|
|
5
|
+
|
|
6
|
+
export type ElementStyleInput = Omit<ElementProps, 'as' | 'componentProps'>
|
|
7
|
+
|
|
8
|
+
const DISPLAY_FOR = {
|
|
9
|
+
hidden: 'none',
|
|
10
|
+
grid: 'grid',
|
|
11
|
+
flex: 'flex',
|
|
12
|
+
block: 'block',
|
|
13
|
+
} as const
|
|
14
|
+
|
|
15
|
+
function track(value: number | string | undefined): string | undefined {
|
|
16
|
+
return typeof value === 'number' ? `repeat(${value}, 1fr)` : value
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Element's whole prop vocabulary → one CSSProperties object. Pure: shared by Element
|
|
21
|
+
* (inline :style) and ElementTw (Tailwind arbitrary-property classes).
|
|
22
|
+
*
|
|
23
|
+
* Grid is container axis only ("I am a grid"). Item placement lives in the layout layer
|
|
24
|
+
* (buildGridPlacementStyle); Element is in core and can't depend on layout.
|
|
25
|
+
*/
|
|
26
|
+
export function resolveElementStyle(props: ElementStyleInput): CSSProperties {
|
|
27
|
+
// hidden > grid > flex > block
|
|
28
|
+
const mode = (['hidden', 'grid', 'flex', 'block'] as const).find((m) => props[m])
|
|
29
|
+
return {
|
|
30
|
+
display: mode && DISPLAY_FOR[mode],
|
|
31
|
+
...resolveLayoutAttrsStyle(props),
|
|
32
|
+
...(props.grid && {
|
|
33
|
+
gridTemplateColumns: track(props.cols),
|
|
34
|
+
gridTemplateRows: track(props.rows),
|
|
35
|
+
}),
|
|
36
|
+
width: props.w,
|
|
37
|
+
height: props.h,
|
|
38
|
+
minWidth: props.minW,
|
|
39
|
+
maxWidth: props.maxW,
|
|
40
|
+
minHeight: props.minH,
|
|
41
|
+
maxHeight: props.maxH,
|
|
42
|
+
aspectRatio: props.aspect,
|
|
43
|
+
background: props.bg,
|
|
44
|
+
color: props.color,
|
|
45
|
+
border: props.border,
|
|
46
|
+
borderRadius: props.radius,
|
|
47
|
+
boxShadow: props.shadow,
|
|
48
|
+
cursor: props.cursor,
|
|
49
|
+
userSelect: props.select,
|
|
50
|
+
pointerEvents: props.pointer,
|
|
51
|
+
}
|
|
52
|
+
}
|
package/layers/data/metadata/providers/comicvine/server/utils/metadata-comicvine/provider.ts
CHANGED
|
@@ -58,14 +58,4 @@ export const comicVineProvider: MetadataProvider = {
|
|
|
58
58
|
const res = await fetchComicVineIssue(providerId)
|
|
59
59
|
return normaliseComicVineIssue(res.results as ComicVineIssue)
|
|
60
60
|
},
|
|
61
|
-
|
|
62
|
-
async sync({ providerId, resourceType = 'issue' }) {
|
|
63
|
-
const record = await comicVineProvider.lookup({
|
|
64
|
-
provider: 'comicvine',
|
|
65
|
-
providerId,
|
|
66
|
-
resourceType,
|
|
67
|
-
})
|
|
68
|
-
if (!record) throw new MetadataProviderError('comicvine', `No record found for ${providerId}`)
|
|
69
|
-
return record
|
|
70
|
-
},
|
|
71
61
|
}
|
package/layers/data/metadata/providers/google-books/server/utils/metadata-google-books/client.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { providerFetch } from '#layers/metadata/server/utils/metadata/provider-fetch'
|
|
1
2
|
import { useRuntimeConfig } from 'nitropack/runtime'
|
|
2
3
|
import type { MetadataMediaType } from '#layers/metadata/shared/types'
|
|
3
4
|
|
|
@@ -29,15 +30,17 @@ export async function searchGoogleBooks(
|
|
|
29
30
|
query: string,
|
|
30
31
|
limit = 10
|
|
31
32
|
): Promise<GoogleBooksSearchResponse> {
|
|
32
|
-
const params: Record<string, string> = {
|
|
33
|
+
const params: Record<string, string> = {
|
|
34
|
+
q: query,
|
|
35
|
+
maxResults: String(limit),
|
|
36
|
+
langRestrict: 'en',
|
|
37
|
+
}
|
|
33
38
|
const apiKey = getApiKey()
|
|
34
39
|
if (apiKey) params.key = apiKey
|
|
35
40
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
throw new MetadataProviderError('google-books', `API error: ${String(err)}`)
|
|
40
|
-
}
|
|
41
|
+
return providerFetch<GoogleBooksSearchResponse>('google-books', `${BASE}/volumes`, {
|
|
42
|
+
query: params,
|
|
43
|
+
})
|
|
41
44
|
}
|
|
42
45
|
|
|
43
46
|
export async function fetchGoogleBooksVolume(id: string): Promise<GoogleBooksVolume> {
|
|
@@ -45,9 +48,7 @@ export async function fetchGoogleBooksVolume(id: string): Promise<GoogleBooksVol
|
|
|
45
48
|
const apiKey = getApiKey()
|
|
46
49
|
if (apiKey) params.key = apiKey
|
|
47
50
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
throw new MetadataProviderError('google-books', `API error: ${String(err)}`)
|
|
52
|
-
}
|
|
51
|
+
return providerFetch<GoogleBooksVolume>('google-books', `${BASE}/volumes/${id}`, {
|
|
52
|
+
query: params,
|
|
53
|
+
})
|
|
53
54
|
}
|
package/layers/data/metadata/providers/google-books/server/utils/metadata-google-books/provider.ts
CHANGED
|
@@ -23,11 +23,4 @@ export const googleBooksProvider: MetadataProvider = {
|
|
|
23
23
|
const volume = await fetchGoogleBooksVolume(providerId)
|
|
24
24
|
return normaliseGoogleBooksVolume(volume)
|
|
25
25
|
},
|
|
26
|
-
|
|
27
|
-
async sync({ providerId }) {
|
|
28
|
-
const record = await googleBooksProvider.lookup({ provider: 'google-books', providerId })
|
|
29
|
-
if (!record)
|
|
30
|
-
throw new MetadataProviderError('google-books', `No record found for ${providerId}`)
|
|
31
|
-
return record
|
|
32
|
-
},
|
|
33
26
|
}
|