kmcom-nuxt-layers 2.6.3 → 2.7.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/ARCHITECTURE.md +21 -17
- package/docs/FEEDS.md +42 -34
- package/docs/IMPROVE-AUDIT-README.md +9 -10
- package/docs/IMPROVE-AUDIT-RESULTS.md +16 -17
- package/docs/IMPROVE-DEEP-AUDIT-RESULTS.md +29 -29
- package/docs/LAYERS-FIXES.md +1 -0
- package/docs/LAYOUT.md +91 -86
- package/docs/MIGRATION.md +93 -60
- package/docs/UPAGE-GRID-INTEGRATION.md +12 -10
- package/docs/USAGE.md +29 -38
- package/layers/animations/app/utils/pointerMotion.ts +1 -4
- package/layers/baseline/app/components/BaselineStatus.vue +13 -13
- package/layers/baseline/app/plugins/baseline-status.client.ts +4 -1
- package/layers/canvas/app/composables/useRendererCapabilities.ts +2 -2
- package/layers/content/app/components/Blog/Card.vue +1 -0
- package/layers/content/app/components/Gallery/Card.vue +1 -1
- package/layers/content/app/components/Gallery/Detail.vue +8 -1
- package/layers/content/app/components/Portfolio/Card.vue +1 -0
- package/layers/content/app/components/Portfolio/ColorPalette.vue +8 -1
- package/layers/content/app/components/Portfolio/Typography.vue +12 -2
- package/layers/content/app/composables/useGalleryItems.ts +3 -2
- package/layers/content/app/composables/usePortfolioItems.ts +3 -2
- package/layers/core/app/components/DiagnosticsPage.vue +15 -3
- package/layers/core/app/utils/featureClasses.test.ts +1 -3
- package/layers/core/app/utils/featureClasses.ts +1 -3
- package/layers/core/app/utils/helpers.test.ts +6 -2
- package/layers/core/nuxt.config.ts +2 -0
- package/layers/feeds/app/assets/css/feeds.css +3 -2
- package/layers/feeds/app/components/Feeds/Index.vue +36 -6
- package/layers/feeds/app/components/Feeds/RouteCard.vue +1 -3
- package/layers/feeds/app/plugins/feed-head.ts +2 -2
- package/layers/feeds/package.json +1 -1
- package/layers/feeds/public/feed/style.css +9 -9
- package/layers/feeds/server/utils/content-adapter.test.ts +3 -4
- package/layers/feeds/server/utils/content-adapter.ts +1 -1
- package/layers/feeds/server/utils/feed-author.test.ts +3 -3
- package/layers/feeds/server/utils/feed-config.test.ts +28 -6
- package/layers/feeds/server/utils/feed-config.ts +1 -4
- package/layers/feeds/server/utils/feed-service.ts +1 -1
- package/layers/feeds/server/utils/formats/rss.ts +1 -1
- package/layers/feeds/server/utils/formats.test.ts +1 -1
- package/layers/forms/app/utils/fieldProps.ts +1 -2
- package/layers/forms/server/api/contact.post.ts +4 -1
- package/layers/layout/app/assets/css/layout/grids.css +12 -3
- package/layers/layout/app/assets/css/layout/modes/fluid.css +9 -0
- package/layers/layout/app/components/Layout/Section/Sidebar.vue +11 -4
- package/layers/layout/app/utils/gridPlacementStyle.test.ts +3 -9
- package/layers/layout/app/utils/gridPlacementStyle.ts +10 -7
- package/layers/mailer/server/utils/email.ts +4 -3
- package/layers/mailer/server/utils/hooks.ts +1 -1
- package/layers/metadata/app/composables/useMetadataItem.ts +6 -2
- package/layers/metadata/app/composables/useMetadataSearch.ts +6 -2
- package/layers/metadata/providers/comicvine/server/utils/metadata-comicvine/client.ts +5 -5
- package/layers/metadata/providers/comicvine/server/utils/metadata-comicvine/provider.ts +11 -3
- package/layers/metadata/providers/google-books/server/utils/metadata-google-books/client.ts +9 -6
- package/layers/metadata/providers/google-books/server/utils/metadata-google-books/provider.ts +6 -3
- package/layers/metadata/providers/openlibrary/server/utils/metadata-openlibrary/normalise.test.ts +3 -1
- package/layers/metadata/providers/openlibrary/server/utils/metadata-openlibrary/provider.ts +4 -2
- package/layers/metadata/providers/themoviedb/server/utils/metadata-tmdb/client.test.ts +1 -3
- package/layers/metadata/providers/themoviedb/server/utils/metadata-tmdb/provider.ts +3 -1
- package/layers/metadata/server/api/metadata/status.get.ts +13 -18
- package/layers/metadata/server/utils/metadata/provider-registry.test.ts +1 -1
- package/layers/metadata/server/utils/metadata/search.test.ts +18 -18
- package/layers/metadata/server/utils/metadata/search.ts +16 -8
- package/layers/navigation/app/utils/site.test.ts +1 -3
- package/layers/scroll/app/components/Motion/PinnedSection.vue +11 -1
- package/layers/scroll/app/composables/useSectionProgress.ts +1 -1
- package/layers/scroll/app/plugins/locomotive-scroll.client.ts +6 -0
- package/layers/scroll/app/utils/scroll.ts +5 -1
- package/layers/seo/app/utils/seoConfig.test.ts +1 -1
- package/layers/shader/app/assets/css/shader.css +1 -1
- package/layers/shader/app/components/Material/AmbientAurora.client.vue +16 -4
- package/layers/shader/app/components/Material/AmbientFlow.client.vue +21 -5
- package/layers/shader/app/components/Material/AmbientGradientMesh.client.vue +21 -5
- package/layers/shader/app/components/Material/AmbientNebula.client.vue +21 -5
- package/layers/shader/app/components/Material/AmbientOcean.client.vue +16 -4
- package/layers/shader/app/components/Material/Gradient.client.vue +1 -1
- package/layers/shader/app/components/Material/Image.client.vue +1 -1
- package/layers/shader/app/components/Material/Noise.client.vue +11 -3
- package/layers/shader/app/composables/useAmbientMaterials.ts +0 -1
- package/layers/shader/app/shaders/common/blend.ts +5 -1
- package/layers/shader/app/shaders/common/grain.ts +5 -1
- package/layers/shader/app/shaders/common/lighting.ts +1 -6
- package/layers/shader/app/shaders/common/noise.ts +3 -1
- package/layers/shader/app/shaders/common/patterns.ts +1 -1
- package/layers/shader/app/shaders/common/shapes.ts +5 -1
- package/layers/shader/app/shaders/common/uv.ts +19 -2
- package/layers/shader/app/shaders/createMaterial.ts +11 -6
- package/layers/shader/app/shaders/layers/shaderGradient.ts +2 -12
- package/layers/shader/app/shaders/types.ts +1 -1
- package/layers/shader/app/types/uniforms.ts +1 -0
- package/layers/shader/app/utils/tsl/animation.ts +34 -36
- package/layers/starter/app/components/StarterDesignSystem.vue +120 -120
- package/layers/starter/app/components/StarterHome.vue +233 -208
- package/layers/theme/app/assets/css/theme.css +1 -1
- package/layers/theme/server/utils/accent-css.test.ts +17 -2
- package/layers/theme/server/utils/fouc-config.test.ts +7 -3
- package/layers/theme/server/utils/fouc-config.ts +24 -12
- package/layers/transitions/app/assets/css/transitions.css +6 -0
- package/layers/typography/app/assets/css/typography.css +313 -0
- package/layers/typography/app/components/Typography/Headline.vue +23 -14
- package/layers/typography/app/components/Typography/HeadlineScreen.vue +5 -12
- package/layers/typography/app/components/Typography/index.vue +4 -0
- package/layers/typography/app/composables/typography.ts +45 -5
- package/layers/typography/app/types/typography.ts +36 -7
- package/layers/typography/nuxt.config.ts +2 -0
- package/layers/visual/app/app.config.ts +1 -1
- package/layers/visual/app/assets/css/visual.css +1 -1
- package/layers/visual/app/composables/accent.ts +1 -2
- package/layers/visual/app/composables/tint.ts +1 -2
- package/layers/visual/app/types/media.ts +2 -2
- package/layers/visual/app/utils/gradientStyle.test.ts +1 -2
- package/layers/visual/app/utils/responsiveSizes.ts +0 -1
- package/package.json +33 -13
|
@@ -12,16 +12,16 @@
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
:root {
|
|
15
|
-
--feed-bg: var(--ui-bg, #
|
|
15
|
+
--feed-bg: var(--ui-bg, #fff);
|
|
16
16
|
--feed-bg-elevated: var(--ui-bg-elevated, #f8fafc);
|
|
17
17
|
--feed-bg-accented: var(--ui-bg-accented, #f1f5f9);
|
|
18
18
|
--feed-border: var(--ui-border, #e2e8f0);
|
|
19
19
|
--feed-text: var(--ui-text, #0f172a);
|
|
20
20
|
--feed-text-muted: var(--ui-text-muted, #64748b);
|
|
21
21
|
--feed-text-toned: var(--ui-text-toned, #475569);
|
|
22
|
-
--feed-primary: var(--ui-color-primary-500, oklch(62.3% 0.214 259.
|
|
23
|
-
--feed-primary-text: var(--ui-color-primary-600, oklch(54.6% 0.214 259.
|
|
24
|
-
--feed-primary-bg: var(--ui-color-primary-50, oklch(97% 0.03 259.
|
|
22
|
+
--feed-primary: var(--ui-color-primary-500, oklch(62.3% 0.214 259.5deg));
|
|
23
|
+
--feed-primary-text: var(--ui-color-primary-600, oklch(54.6% 0.214 259.5deg));
|
|
24
|
+
--feed-primary-bg: var(--ui-color-primary-50, oklch(97% 0.03 259.5deg));
|
|
25
25
|
--feed-radius: var(--ui-radius, 0.375rem);
|
|
26
26
|
--feed-font: var(--font-sans, ui-sans-serif, system-ui, -apple-system, sans-serif);
|
|
27
27
|
}
|
|
@@ -35,9 +35,9 @@
|
|
|
35
35
|
--feed-text: var(--ui-text, #f8fafc);
|
|
36
36
|
--feed-text-muted: var(--ui-text-muted, #94a3b8);
|
|
37
37
|
--feed-text-toned: var(--ui-text-toned, #cbd5e1);
|
|
38
|
-
--feed-primary: var(--ui-color-primary-400, oklch(71% 0.18 259.
|
|
39
|
-
--feed-primary-text: var(--ui-color-primary-400, oklch(71% 0.18 259.
|
|
40
|
-
--feed-primary-bg: var(--ui-color-primary-950, oklch(22% 0.06 259.
|
|
38
|
+
--feed-primary: var(--ui-color-primary-400, oklch(71% 0.18 259.5deg));
|
|
39
|
+
--feed-primary-text: var(--ui-color-primary-400, oklch(71% 0.18 259.5deg));
|
|
40
|
+
--feed-primary-bg: var(--ui-color-primary-950, oklch(22% 0.06 259.5deg));
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
|
|
@@ -160,7 +160,7 @@ a:hover {
|
|
|
160
160
|
border-radius: var(--feed-radius);
|
|
161
161
|
background-color: var(--feed-primary);
|
|
162
162
|
padding: 0.5rem 1.125rem;
|
|
163
|
-
color: #
|
|
163
|
+
color: #fff;
|
|
164
164
|
font-weight: 600;
|
|
165
165
|
font-size: 0.875rem;
|
|
166
166
|
line-height: 1;
|
|
@@ -169,7 +169,7 @@ a:hover {
|
|
|
169
169
|
|
|
170
170
|
.feed-subscribe:hover {
|
|
171
171
|
opacity: 0.85;
|
|
172
|
-
color: #
|
|
172
|
+
color: #fff;
|
|
173
173
|
text-decoration: none;
|
|
174
174
|
}
|
|
175
175
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { describe, expect, it, beforeEach, vi } from 'vitest'
|
|
2
|
-
|
|
3
1
|
import type { H3Event } from 'h3'
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import { getContentFeedItems } from './content-adapter'
|
|
4
5
|
|
|
5
6
|
const queryCollectionMock = vi.hoisted(() => vi.fn())
|
|
6
7
|
|
|
@@ -8,8 +9,6 @@ vi.mock('@nuxt/content/nitro', () => ({
|
|
|
8
9
|
queryCollection: queryCollectionMock,
|
|
9
10
|
}))
|
|
10
11
|
|
|
11
|
-
import { getContentFeedItems } from './content-adapter'
|
|
12
|
-
|
|
13
12
|
describe('getContentFeedItems', () => {
|
|
14
13
|
beforeEach(() => {
|
|
15
14
|
queryCollectionMock.mockReset()
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { queryCollection } from '@nuxt/content/nitro'
|
|
2
2
|
import type { H3Event } from 'h3'
|
|
3
3
|
|
|
4
|
-
import type { FeedItem } from './types'
|
|
5
4
|
import { resolveFeedAuthor, resolveFeedDate } from './feed-author'
|
|
5
|
+
import type { FeedItem } from './types'
|
|
6
6
|
|
|
7
7
|
type FeedSourceAuthor = {
|
|
8
8
|
name?: string | undefined
|
|
@@ -25,9 +25,9 @@ describe('resolveFeedAuthor', () => {
|
|
|
25
25
|
})
|
|
26
26
|
|
|
27
27
|
it('ignores an authors array entry with an empty name and falls through', () => {
|
|
28
|
-
expect(
|
|
29
|
-
|
|
30
|
-
)
|
|
28
|
+
expect(resolveFeedAuthor({ authors: [{ name: '' }], author: 'Kieran Mansfield' })).toBe(
|
|
29
|
+
'Kieran Mansfield'
|
|
30
|
+
)
|
|
31
31
|
})
|
|
32
32
|
|
|
33
33
|
it('ignores an empty string author and falls through to object author', () => {
|
|
@@ -51,7 +51,11 @@ describe('resolveFeedCollection', () => {
|
|
|
51
51
|
|
|
52
52
|
describe('createFeedConfig', () => {
|
|
53
53
|
it('uses "My Site" as the title fallback with no collection label', () => {
|
|
54
|
-
const config = createFeedConfig({
|
|
54
|
+
const config = createFeedConfig({
|
|
55
|
+
site: {},
|
|
56
|
+
siteUrl: 'https://example.com',
|
|
57
|
+
collection: undefined,
|
|
58
|
+
})
|
|
55
59
|
expect(config.title).toBe('My Site')
|
|
56
60
|
})
|
|
57
61
|
|
|
@@ -65,7 +69,11 @@ describe('createFeedConfig', () => {
|
|
|
65
69
|
})
|
|
66
70
|
|
|
67
71
|
it('defaults description to an empty string', () => {
|
|
68
|
-
const config = createFeedConfig({
|
|
72
|
+
const config = createFeedConfig({
|
|
73
|
+
site: {},
|
|
74
|
+
siteUrl: 'https://example.com',
|
|
75
|
+
collection: undefined,
|
|
76
|
+
})
|
|
69
77
|
expect(config.description).toBe('')
|
|
70
78
|
})
|
|
71
79
|
|
|
@@ -80,7 +88,9 @@ describe('createFeedConfig', () => {
|
|
|
80
88
|
|
|
81
89
|
it('builds an author object when the site has a named author', () => {
|
|
82
90
|
const config = createFeedConfig({
|
|
83
|
-
site: {
|
|
91
|
+
site: {
|
|
92
|
+
author: { name: 'Kieran Mansfield', email: 'hi@example.com', link: 'https://example.com' },
|
|
93
|
+
},
|
|
84
94
|
siteUrl: 'https://example.com',
|
|
85
95
|
collection: undefined,
|
|
86
96
|
})
|
|
@@ -92,7 +102,11 @@ describe('createFeedConfig', () => {
|
|
|
92
102
|
})
|
|
93
103
|
|
|
94
104
|
it('omits author when the site author has no name', () => {
|
|
95
|
-
const config = createFeedConfig({
|
|
105
|
+
const config = createFeedConfig({
|
|
106
|
+
site: {},
|
|
107
|
+
siteUrl: 'https://example.com',
|
|
108
|
+
collection: undefined,
|
|
109
|
+
})
|
|
96
110
|
expect(config.author).toBeUndefined()
|
|
97
111
|
})
|
|
98
112
|
|
|
@@ -115,7 +129,11 @@ describe('createFeedConfig', () => {
|
|
|
115
129
|
})
|
|
116
130
|
|
|
117
131
|
it('defaults favicon to /favicon.ico', () => {
|
|
118
|
-
const config = createFeedConfig({
|
|
132
|
+
const config = createFeedConfig({
|
|
133
|
+
site: {},
|
|
134
|
+
siteUrl: 'https://example.com',
|
|
135
|
+
collection: undefined,
|
|
136
|
+
})
|
|
119
137
|
expect(config.favicon).toBe('/favicon.ico')
|
|
120
138
|
})
|
|
121
139
|
|
|
@@ -138,7 +156,11 @@ describe('createFeedConfig', () => {
|
|
|
138
156
|
})
|
|
139
157
|
|
|
140
158
|
it('omits copyright when there is no explicit copyright and no author name', () => {
|
|
141
|
-
const config = createFeedConfig({
|
|
159
|
+
const config = createFeedConfig({
|
|
160
|
+
site: {},
|
|
161
|
+
siteUrl: 'https://example.com',
|
|
162
|
+
collection: undefined,
|
|
163
|
+
})
|
|
142
164
|
expect(config.copyright).toBeUndefined()
|
|
143
165
|
})
|
|
144
166
|
})
|
|
@@ -54,10 +54,7 @@ export function resolveFeedSiteConfig(appConfig: AppFeedConfig, requestUrl: URL)
|
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
export function resolveFeedCollection(
|
|
58
|
-
feedConfig: FeedLayerConfig,
|
|
59
|
-
collection?: string
|
|
60
|
-
) {
|
|
57
|
+
export function resolveFeedCollection(feedConfig: FeedLayerConfig, collection?: string) {
|
|
61
58
|
return collection ?? feedConfig?.defaultCollection ?? 'blog'
|
|
62
59
|
}
|
|
63
60
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import type { H3Event } from 'h3'
|
|
2
2
|
|
|
3
|
-
import type { FeedConfig, FeedItem } from './types'
|
|
4
3
|
import {
|
|
5
4
|
createFeedConfig,
|
|
6
5
|
resolveFeedCollection,
|
|
7
6
|
resolveFeedLimit,
|
|
8
7
|
resolveFeedSiteConfig,
|
|
9
8
|
} from './feed-config'
|
|
9
|
+
import type { FeedConfig, FeedItem } from './types'
|
|
10
10
|
|
|
11
11
|
export async function buildFeed(
|
|
12
12
|
event: H3Event,
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { Feed } from 'feed'
|
|
2
2
|
|
|
3
|
-
import type { FeedConfig, FeedItem } from '../types'
|
|
4
3
|
import {
|
|
5
4
|
applyFeedStylesheet,
|
|
6
5
|
resolveFeedAuthorPayload,
|
|
7
6
|
resolveFeedItemId,
|
|
8
7
|
resolveFeedItemLink,
|
|
9
8
|
} from '../feed-xml'
|
|
9
|
+
import type { FeedConfig, FeedItem } from '../types'
|
|
10
10
|
|
|
11
11
|
export function toRSS(items: FeedItem[], config: FeedConfig): string {
|
|
12
12
|
const author = resolveFeedAuthorPayload(config.author)
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
2
|
|
|
3
|
-
import type { FeedConfig, FeedItem } from './types'
|
|
4
3
|
import { toAtom } from './formats/atom'
|
|
5
4
|
import { toJSONFeed } from './formats/json'
|
|
6
5
|
import { toRSS } from './formats/rss'
|
|
6
|
+
import type { FeedConfig, FeedItem } from './types'
|
|
7
7
|
|
|
8
8
|
const config: FeedConfig = {
|
|
9
9
|
title: 'Nuxt Layers Feed',
|
|
@@ -23,7 +23,10 @@ function checkRateLimit(ip: string): boolean {
|
|
|
23
23
|
export default defineEventHandler(async (event) => {
|
|
24
24
|
const ip = getRequestIP(event, { xForwardedFor: true }) ?? 'unknown'
|
|
25
25
|
if (!checkRateLimit(ip)) {
|
|
26
|
-
throw createError({
|
|
26
|
+
throw createError({
|
|
27
|
+
statusCode: 429,
|
|
28
|
+
statusMessage: 'Too many requests. Please try again later.',
|
|
29
|
+
})
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
const body = await readBody(event)
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
min-width: 0;
|
|
83
83
|
max-width: 100%;
|
|
84
84
|
overflow-wrap: break-word;
|
|
85
|
-
|
|
85
|
+
overflow-wrap: break-word;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
/* On mobile, allow sections to grow beyond 12 rows if needed */
|
|
@@ -102,52 +102,62 @@
|
|
|
102
102
|
/* Line height utilities */
|
|
103
103
|
.leading-rhythm-4 {
|
|
104
104
|
--rhythm: 0.25rem;
|
|
105
|
+
|
|
105
106
|
line-height: calc(var(--rhythm) * 4); /* 1rem */
|
|
106
107
|
}
|
|
107
108
|
|
|
108
109
|
.leading-rhythm-5 {
|
|
109
110
|
--rhythm: 0.25rem;
|
|
111
|
+
|
|
110
112
|
line-height: calc(var(--rhythm) * 5); /* 1.25rem */
|
|
111
113
|
}
|
|
112
114
|
|
|
113
115
|
.leading-rhythm-6 {
|
|
114
116
|
--rhythm: 0.25rem;
|
|
117
|
+
|
|
115
118
|
line-height: calc(var(--rhythm) * 6); /* 1.5rem */
|
|
116
119
|
}
|
|
117
120
|
|
|
118
121
|
.leading-rhythm-7 {
|
|
119
122
|
--rhythm: 0.25rem;
|
|
123
|
+
|
|
120
124
|
line-height: calc(var(--rhythm) * 7); /* 1.75rem */
|
|
121
125
|
}
|
|
122
126
|
|
|
123
127
|
.leading-rhythm-8 {
|
|
124
128
|
--rhythm: 0.25rem;
|
|
129
|
+
|
|
125
130
|
line-height: calc(var(--rhythm) * 8); /* 2rem */
|
|
126
131
|
}
|
|
127
132
|
|
|
128
133
|
/* Vertical spacing utilities */
|
|
129
134
|
.space-rhythm-1 {
|
|
130
135
|
--rhythm: 0.25rem;
|
|
136
|
+
|
|
131
137
|
margin-block: var(--rhythm); /* 0.25rem */
|
|
132
138
|
}
|
|
133
139
|
|
|
134
140
|
.space-rhythm-2 {
|
|
135
141
|
--rhythm: 0.25rem;
|
|
142
|
+
|
|
136
143
|
margin-block: calc(var(--rhythm) * 2); /* 0.5rem */
|
|
137
144
|
}
|
|
138
145
|
|
|
139
146
|
.space-rhythm-4 {
|
|
140
147
|
--rhythm: 0.25rem;
|
|
148
|
+
|
|
141
149
|
margin-block: calc(var(--rhythm) * 4); /* 1rem */
|
|
142
150
|
}
|
|
143
151
|
|
|
144
152
|
.space-rhythm-6 {
|
|
145
153
|
--rhythm: 0.25rem;
|
|
154
|
+
|
|
146
155
|
margin-block: calc(var(--rhythm) * 6); /* 1.5rem */
|
|
147
156
|
}
|
|
148
157
|
|
|
149
158
|
.space-rhythm-8 {
|
|
150
159
|
--rhythm: 0.25rem;
|
|
160
|
+
|
|
151
161
|
margin-block: calc(var(--rhythm) * 8); /* 2rem */
|
|
152
162
|
}
|
|
153
163
|
|
|
@@ -165,8 +175,7 @@
|
|
|
165
175
|
& > h4,
|
|
166
176
|
& > h5,
|
|
167
177
|
& > h6 {
|
|
168
|
-
margin-block
|
|
169
|
-
margin-block-end: calc(var(--rhythm) * 4); /* 1rem after headings */
|
|
178
|
+
margin-block: calc(var(--rhythm) * 8) calc(var(--rhythm) * 4); /* 2rem before, 1rem after headings */
|
|
170
179
|
line-height: calc(var(--rhythm) * 8);
|
|
171
180
|
}
|
|
172
181
|
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
.fluid-grid {
|
|
23
23
|
--fluid-col-min: 16rem;
|
|
24
|
+
|
|
24
25
|
display: grid;
|
|
25
26
|
grid-template-columns: repeat(auto-fit, minmax(var(--fluid-col-min), 1fr));
|
|
26
27
|
gap: var(--grid-gap, clamp(0.75rem, 1.5vw, 1.5rem));
|
|
@@ -34,6 +35,7 @@
|
|
|
34
35
|
*/
|
|
35
36
|
.fluid-grid-2 {
|
|
36
37
|
--fluid-col-min: 14rem;
|
|
38
|
+
|
|
37
39
|
display: grid;
|
|
38
40
|
grid-template-columns: repeat(auto-fit, minmax(var(--fluid-col-min), 1fr));
|
|
39
41
|
gap: var(--grid-gap, clamp(0.75rem, 1.5vw, 1.5rem));
|
|
@@ -45,6 +47,7 @@
|
|
|
45
47
|
|
|
46
48
|
.fluid-grid-3 {
|
|
47
49
|
--fluid-col-min: 14rem;
|
|
50
|
+
|
|
48
51
|
display: grid;
|
|
49
52
|
grid-template-columns: repeat(auto-fit, minmax(var(--fluid-col-min), 1fr));
|
|
50
53
|
gap: var(--grid-gap, clamp(0.75rem, 1.5vw, 1.5rem));
|
|
@@ -56,6 +59,7 @@
|
|
|
56
59
|
|
|
57
60
|
.fluid-grid-4 {
|
|
58
61
|
--fluid-col-min: 12rem;
|
|
62
|
+
|
|
59
63
|
display: grid;
|
|
60
64
|
grid-template-columns: repeat(auto-fit, minmax(var(--fluid-col-min), 1fr));
|
|
61
65
|
gap: var(--grid-gap, clamp(0.75rem, 1.5vw, 1.5rem));
|
|
@@ -73,6 +77,7 @@
|
|
|
73
77
|
*/
|
|
74
78
|
.layout-container-content {
|
|
75
79
|
box-sizing: border-box;
|
|
80
|
+
container-type: inline-size;
|
|
76
81
|
margin-inline: auto;
|
|
77
82
|
width: 100%;
|
|
78
83
|
max-width: 65ch;
|
|
@@ -80,6 +85,7 @@
|
|
|
80
85
|
|
|
81
86
|
.layout-container-wide {
|
|
82
87
|
box-sizing: border-box;
|
|
88
|
+
container-type: inline-size;
|
|
83
89
|
margin-inline: auto;
|
|
84
90
|
width: 100%;
|
|
85
91
|
max-width: 90rem;
|
|
@@ -87,13 +93,16 @@
|
|
|
87
93
|
|
|
88
94
|
.layout-container-fluid {
|
|
89
95
|
box-sizing: border-box;
|
|
96
|
+
container-type: inline-size;
|
|
90
97
|
width: 100%;
|
|
91
98
|
max-width: 100%;
|
|
92
99
|
}
|
|
93
100
|
|
|
94
101
|
.layout-container-full {
|
|
95
102
|
box-sizing: border-box;
|
|
103
|
+
container-type: inline-size;
|
|
96
104
|
margin-inline: calc(var(--grid-padding, clamp(1rem, 2.5vw, 2rem)) * -1);
|
|
105
|
+
|
|
97
106
|
/* Escape parent padding to reach the viewport edge */
|
|
98
107
|
width: 100vw;
|
|
99
108
|
max-width: 100vw;
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Two-column layout where the sidebar takes a clamped width and main content
|
|
6
6
|
* fills the rest. Uses `minmax(min, max) 1fr` (or reversed for right sidebar).
|
|
7
7
|
*
|
|
8
|
-
* @prop {string} sidebarMin - Minimum sidebar width (default: '
|
|
8
|
+
* @prop {string} sidebarMin - Minimum sidebar width (default: '9.375rem')
|
|
9
9
|
* @prop {string} sidebarMax - Maximum sidebar width (default: '25%')
|
|
10
10
|
* @prop {boolean} reverse - Put sidebar on the right (default: false)
|
|
11
11
|
* @prop {boolean} fullHeight - Force 100svh on the section (default: false)
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
const {
|
|
31
|
-
sidebarMin = '
|
|
31
|
+
sidebarMin = '9.375rem',
|
|
32
32
|
sidebarMax = '25%',
|
|
33
33
|
reverse = false,
|
|
34
34
|
fullHeight = false,
|
|
@@ -57,14 +57,21 @@
|
|
|
57
57
|
<style scoped>
|
|
58
58
|
.sidebar-inner {
|
|
59
59
|
display: grid;
|
|
60
|
-
grid-template-columns: minmax(var(--sidebar-min,
|
|
60
|
+
grid-template-columns: minmax(var(--sidebar-min, 9.375rem), var(--sidebar-max, 25%)) 1fr;
|
|
61
61
|
gap: var(--grid-gap, clamp(0.75rem, 1.5vw, 1.5rem));
|
|
62
|
+
container-type: inline-size;
|
|
62
63
|
width: 100%;
|
|
63
64
|
height: 100%;
|
|
65
|
+
|
|
66
|
+
/* Collapse to a single column when the container itself is too narrow
|
|
67
|
+
for the sidebar split, regardless of viewport size. */
|
|
68
|
+
@container (width < 30rem) {
|
|
69
|
+
grid-template-columns: 1fr;
|
|
70
|
+
}
|
|
64
71
|
}
|
|
65
72
|
|
|
66
73
|
.sidebar-inner.sidebar-reverse {
|
|
67
|
-
grid-template-columns: 1fr minmax(var(--sidebar-min,
|
|
74
|
+
grid-template-columns: 1fr minmax(var(--sidebar-min, 9.375rem), var(--sidebar-max, 25%));
|
|
68
75
|
}
|
|
69
76
|
|
|
70
77
|
.sidebar-inner.sidebar-reverse .sidebar-aside {
|
|
@@ -92,9 +92,7 @@ describe('resolveBleedStyles', () => {
|
|
|
92
92
|
})
|
|
93
93
|
|
|
94
94
|
it('handles "left" bleed direction with a numeric colSpan', () => {
|
|
95
|
-
expect(
|
|
96
|
-
resolveBleedStyles({ bleed: 'left', colSpan: 4, rowStart: 2, rowSpan: 1 })
|
|
97
|
-
).toEqual({
|
|
95
|
+
expect(resolveBleedStyles({ bleed: 'left', colSpan: 4, rowStart: 2, rowSpan: 1 })).toEqual({
|
|
98
96
|
gridColumn: '1 / span 4',
|
|
99
97
|
marginInlineStart: 'calc(-1 * var(--grid-padding))',
|
|
100
98
|
gridRow: '2 / span 1',
|
|
@@ -102,9 +100,7 @@ describe('resolveBleedStyles', () => {
|
|
|
102
100
|
})
|
|
103
101
|
|
|
104
102
|
it('handles "left" bleed direction without a numeric colSpan', () => {
|
|
105
|
-
expect(
|
|
106
|
-
resolveBleedStyles({ bleed: 'left', colSpan: 'full', rowSpan: 1 })
|
|
107
|
-
).toEqual({
|
|
103
|
+
expect(resolveBleedStyles({ bleed: 'left', colSpan: 'full', rowSpan: 1 })).toEqual({
|
|
108
104
|
gridColumn: '1 / -1',
|
|
109
105
|
marginInlineStart: 'calc(-1 * var(--grid-padding))',
|
|
110
106
|
gridRow: 'auto / span 1',
|
|
@@ -112,9 +108,7 @@ describe('resolveBleedStyles', () => {
|
|
|
112
108
|
})
|
|
113
109
|
|
|
114
110
|
it('handles "right" bleed direction', () => {
|
|
115
|
-
expect(
|
|
116
|
-
resolveBleedStyles({ bleed: 'right', colStart: 3, rowSpan: 2 })
|
|
117
|
-
).toEqual({
|
|
111
|
+
expect(resolveBleedStyles({ bleed: 'right', colStart: 3, rowSpan: 2 })).toEqual({
|
|
118
112
|
gridColumn: '3 / -1',
|
|
119
113
|
marginInlineEnd: 'calc(-1 * var(--grid-padding))',
|
|
120
114
|
gridRow: 'auto / span 2',
|
|
@@ -149,13 +149,16 @@ export function buildGridPlacementStyle(input: GridPlacementInput): PlacementSty
|
|
|
149
149
|
const rowStart = resolveDefaultPlacement(input.rowStart, undefined)
|
|
150
150
|
const rowSpan = resolveDefaultPlacement(input.rowSpan, 1)
|
|
151
151
|
|
|
152
|
-
Object.assign(
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
152
|
+
Object.assign(
|
|
153
|
+
style,
|
|
154
|
+
resolveBleedStyles({
|
|
155
|
+
bleed: input.bleed,
|
|
156
|
+
colStart,
|
|
157
|
+
colSpan,
|
|
158
|
+
rowStart,
|
|
159
|
+
rowSpan,
|
|
160
|
+
})
|
|
161
|
+
)
|
|
159
162
|
|
|
160
163
|
if (!input.bleed) {
|
|
161
164
|
if (colSpan === 'full') {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { Resend } from 'resend'
|
|
2
|
-
|
|
3
1
|
import type { ContactEmailData } from '#layers/mailer/shared/contact'
|
|
2
|
+
import { Resend } from 'resend'
|
|
4
3
|
|
|
5
4
|
type MailerTransport = {
|
|
6
5
|
from: string
|
|
@@ -36,7 +35,9 @@ export async function sendContactEmail(data: ContactEmailData) {
|
|
|
36
35
|
}
|
|
37
36
|
|
|
38
37
|
const resend = new Resend(transport.apiKey)
|
|
39
|
-
const { data: result, error } = await resend.emails.send(
|
|
38
|
+
const { data: result, error } = await resend.emails.send(
|
|
39
|
+
buildContactEmailPayload(data, transport)
|
|
40
|
+
)
|
|
40
41
|
|
|
41
42
|
if (error) return { success: false as const, error }
|
|
42
43
|
return { success: true as const, messageId: result?.id ?? '' }
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import type { MetadataProviderId, MetadataRecord } from '#layers/metadata/shared/types'
|
|
2
2
|
|
|
3
|
-
export function useMetadataItem(
|
|
3
|
+
export function useMetadataItem(
|
|
4
|
+
provider: MetadataProviderId,
|
|
5
|
+
providerId: string,
|
|
6
|
+
resourceType?: string
|
|
7
|
+
) {
|
|
4
8
|
return useAsyncData(
|
|
5
9
|
`metadata-item:${provider}:${providerId}`,
|
|
6
10
|
() =>
|
|
7
11
|
$fetch('/api/metadata/lookup', {
|
|
8
12
|
query: { provider, providerId, resourceType },
|
|
9
|
-
}) as Promise<MetadataRecord
|
|
13
|
+
}) as Promise<MetadataRecord>
|
|
10
14
|
)
|
|
11
15
|
}
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
MetadataMediaType,
|
|
3
|
+
MetadataProviderId,
|
|
4
|
+
MetadataSearchResult,
|
|
5
|
+
} from '#layers/metadata/shared/types'
|
|
2
6
|
|
|
3
7
|
type UseMetadataSearchOptions = {
|
|
4
8
|
mediaType?: MetadataMediaType
|
|
@@ -20,6 +24,6 @@ export function useMetadataSearch(query: MaybeRef<string>, options: UseMetadataS
|
|
|
20
24
|
limit: options.limit,
|
|
21
25
|
},
|
|
22
26
|
}) as Promise<MetadataSearchResult[]>,
|
|
23
|
-
{ watch: [q], immediate: false }
|
|
27
|
+
{ watch: [q], immediate: false }
|
|
24
28
|
)
|
|
25
29
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ComicVineApiResponse } from './types'
|
|
1
|
+
import type { ComicVineApiResponse, ComicVineIssue, ComicVineVolume } from './types'
|
|
2
2
|
|
|
3
3
|
const FIELD_LIST_ISSUE =
|
|
4
4
|
'id,name,issue_number,description,deck,cover_date,store_date,site_detail_url,image,volume,person_credits,publisher'
|
|
@@ -29,7 +29,7 @@ async function comicVineFetch<T>(
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
export async function searchComicVineIssues(query: string, limit = 10) {
|
|
32
|
-
return comicVineFetch<
|
|
32
|
+
return comicVineFetch<ComicVineIssue[]>('/search/', {
|
|
33
33
|
query,
|
|
34
34
|
resources: 'issue',
|
|
35
35
|
limit: String(limit),
|
|
@@ -38,7 +38,7 @@ export async function searchComicVineIssues(query: string, limit = 10) {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
export async function searchComicVineVolumes(query: string, limit = 10) {
|
|
41
|
-
return comicVineFetch<
|
|
41
|
+
return comicVineFetch<ComicVineVolume[]>('/search/', {
|
|
42
42
|
query,
|
|
43
43
|
resources: 'volume',
|
|
44
44
|
limit: String(limit),
|
|
@@ -47,13 +47,13 @@ export async function searchComicVineVolumes(query: string, limit = 10) {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
export async function fetchComicVineIssue(id: string) {
|
|
50
|
-
return comicVineFetch<
|
|
50
|
+
return comicVineFetch<ComicVineIssue>(`/issue/4000-${id}/`, {
|
|
51
51
|
field_list: FIELD_LIST_ISSUE,
|
|
52
52
|
})
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
export async function fetchComicVineVolume(id: string) {
|
|
56
|
-
return comicVineFetch<
|
|
56
|
+
return comicVineFetch<ComicVineVolume>(`/volume/4050-${id}/`, {
|
|
57
57
|
field_list: FIELD_LIST_VOLUME,
|
|
58
58
|
})
|
|
59
59
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { MetadataProvider } from '#layers/metadata/shared/types'
|
|
2
2
|
|
|
3
|
+
import type { ComicVineIssue, ComicVineVolume } from './types'
|
|
4
|
+
|
|
3
5
|
// Matches CJK, Hangul, Arabic, Hebrew, Thai, Devanagari, Cyrillic, etc.
|
|
4
6
|
const NON_LATIN_RE = /[Ͱ-ϿЀ-ӿ-ۿऀ-ॿ -鿿가-豈-]/
|
|
5
7
|
|
|
@@ -44,13 +46,19 @@ export const comicVineProvider: MetadataProvider = {
|
|
|
44
46
|
async lookup({ providerId, resourceType = 'issue' }) {
|
|
45
47
|
if (resourceType === 'volume') {
|
|
46
48
|
const res = await fetchComicVineVolume(providerId)
|
|
47
|
-
return normaliseComicVineVolume(res.results as
|
|
49
|
+
return normaliseComicVineVolume(res.results as ComicVineVolume)
|
|
48
50
|
}
|
|
49
51
|
const res = await fetchComicVineIssue(providerId)
|
|
50
|
-
return normaliseComicVineIssue(res.results as
|
|
52
|
+
return normaliseComicVineIssue(res.results as ComicVineIssue)
|
|
51
53
|
},
|
|
52
54
|
|
|
53
55
|
async sync({ providerId, resourceType = 'issue' }) {
|
|
54
|
-
|
|
56
|
+
const record = await comicVineProvider.lookup({
|
|
57
|
+
provider: 'comicvine',
|
|
58
|
+
providerId,
|
|
59
|
+
resourceType,
|
|
60
|
+
})
|
|
61
|
+
if (!record) throw new MetadataProviderError('comicvine', `No record found for ${providerId}`)
|
|
62
|
+
return record
|
|
55
63
|
},
|
|
56
64
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import type { MetadataMediaType } from '#layers/metadata/shared/types'
|
|
2
|
+
|
|
1
3
|
import type { GoogleBooksSearchResponse, GoogleBooksVolume } from './types'
|
|
2
4
|
|
|
3
5
|
const BASE = 'https://www.googleapis.com/books/v1'
|
|
4
6
|
|
|
5
|
-
export function googleBooksMediaType(categories?: string[]):
|
|
7
|
+
export function googleBooksMediaType(categories?: string[]): MetadataMediaType {
|
|
6
8
|
const joined = (categories ?? []).join(' ').toLowerCase()
|
|
7
9
|
if (joined.includes('manga')) return 'manga'
|
|
8
10
|
if (joined.includes('comic') || joined.includes('graphic novel')) return 'graphic-novel'
|
|
@@ -10,11 +12,12 @@ export function googleBooksMediaType(categories?: string[]): import('#layers/met
|
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
export function googleBooksCoverUrl(url: string, width = 800): string {
|
|
13
|
-
return
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
return (
|
|
16
|
+
url
|
|
17
|
+
.replace('http://', 'https://')
|
|
18
|
+
.replace(/&zoom=\d+/, '&zoom=3')
|
|
19
|
+
.replace('&edge=curl', '') + `&fife=w${width}`
|
|
20
|
+
)
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
function getApiKey(): string | undefined {
|