bsmnt 0.13.0 → 0.13.1
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/package.json +1 -1
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/page-builder-config.ts +1 -1
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/presentation.ts +47 -15
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/reusable/blog-content.ts +2 -2
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/structure.ts +4 -4
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/page-builder-config.ts +1 -1
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/presentation.ts +47 -15
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/blog-content.ts +2 -2
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/structure.ts +4 -4
- package/src/templates/next-default/lib/utils/README.md +1 -0
- package/src/templates/next-default/lib/utils/image-sizes.ts +64 -25
- package/src/templates/next-experiments/lib/utils/README.md +1 -0
- package/src/templates/next-experiments/lib/utils/image-sizes.ts +64 -25
- package/src/templates/next-pagebuilder/components/page-document/index.tsx +1 -1
- package/src/templates/next-pagebuilder/lib/utils/image-sizes.ts +64 -25
- package/src/templates/next-pagebuilder-cache/components/page-document/index.tsx +1 -1
- package/src/templates/next-pagebuilder-cache/lib/utils/image-sizes.ts +64 -25
- package/src/templates/next-webgl/lib/utils/README.md +1 -0
- package/src/templates/next-webgl/lib/utils/image-sizes.ts +64 -25
package/package.json
CHANGED
package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/presentation.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
presentationTool,
|
|
8
8
|
} from "sanity/presentation";
|
|
9
9
|
import { previewURL } from "./env";
|
|
10
|
+
import { pageBuilderReferenceMembers } from "./page-builder-config";
|
|
10
11
|
import { getPinnedPageById, HOMEPAGE_PINNED_PAGE } from "./pinned-pages";
|
|
11
12
|
|
|
12
13
|
const MAX_FOLDER_DEPTH = 6;
|
|
@@ -43,10 +44,52 @@ const pageResolvedSlugExpression = buildNestedSlugExpression(
|
|
|
43
44
|
"slug.current",
|
|
44
45
|
);
|
|
45
46
|
|
|
46
|
-
const
|
|
47
|
-
|
|
47
|
+
const PAGE_BUILDER_DOCUMENT_TYPES = new Set<string>(
|
|
48
|
+
pageBuilderReferenceMembers.map((member) => member.documentType),
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const HOST_PAGES_QUERY = `*[_type == "page" && references($id)]{
|
|
52
|
+
_id,
|
|
53
|
+
title,
|
|
54
|
+
"resolvedSlug": ${pageResolvedSlugExpression}
|
|
55
|
+
}`;
|
|
56
|
+
|
|
57
|
+
type HostPage = {
|
|
58
|
+
_id: string;
|
|
59
|
+
title?: string;
|
|
60
|
+
resolvedSlug?: string;
|
|
48
61
|
};
|
|
49
62
|
|
|
63
|
+
const isDraftId = (id: string) => id.startsWith("drafts.");
|
|
64
|
+
|
|
65
|
+
const resolveHostPageLocations = (
|
|
66
|
+
documentStore: DocumentStore,
|
|
67
|
+
id: string,
|
|
68
|
+
): Observable<DocumentLocationsState> =>
|
|
69
|
+
documentStore.listenQuery(HOST_PAGES_QUERY, { id }, {}).pipe(
|
|
70
|
+
map((pages: HostPage[] | null) => {
|
|
71
|
+
const deduped = new Map<string, HostPage>();
|
|
72
|
+
for (const page of pages ?? []) {
|
|
73
|
+
const publishedId = page._id.replace(/^drafts\./, "");
|
|
74
|
+
const existing = deduped.get(publishedId);
|
|
75
|
+
if (!existing || isDraftId(existing._id)) {
|
|
76
|
+
deduped.set(publishedId, page);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
locations: [...deduped.values()].flatMap((page) => {
|
|
82
|
+
const pinned = getPinnedPageById(page._id);
|
|
83
|
+
const href =
|
|
84
|
+
pinned?.path ??
|
|
85
|
+
(page.resolvedSlug ? `/${page.resolvedSlug}` : null);
|
|
86
|
+
if (!href) return [];
|
|
87
|
+
return [{ title: page.title || pinned?.title || "Page", href }];
|
|
88
|
+
}),
|
|
89
|
+
};
|
|
90
|
+
}),
|
|
91
|
+
);
|
|
92
|
+
|
|
50
93
|
const resolveSlugLocation = <T extends { title?: string }>(
|
|
51
94
|
documentStore: DocumentStore,
|
|
52
95
|
id: string,
|
|
@@ -73,10 +116,6 @@ export const presentation = presentationTool({
|
|
|
73
116
|
route: "/",
|
|
74
117
|
filter: `_type == "page" && _id == "${HOMEPAGE_PINNED_PAGE.id}"`,
|
|
75
118
|
},
|
|
76
|
-
...Object.entries(CONTENT_TYPE_PREFIXES).map(([type, prefix]) => ({
|
|
77
|
-
route: `/${prefix}/:slug`,
|
|
78
|
-
filter: `_type == "${type}" && slug.current == $slug`,
|
|
79
|
-
})),
|
|
80
119
|
{
|
|
81
120
|
route: "/:slug+",
|
|
82
121
|
filter: `_type == "page" && ${pageResolvedSlugExpression} == $slug`,
|
|
@@ -97,16 +136,9 @@ export const presentation = presentationTool({
|
|
|
97
136
|
);
|
|
98
137
|
}
|
|
99
138
|
|
|
100
|
-
|
|
101
|
-
if (!prefix) return null;
|
|
139
|
+
if (!PAGE_BUILDER_DOCUMENT_TYPES.has(params.type)) return null;
|
|
102
140
|
|
|
103
|
-
return
|
|
104
|
-
documentStore,
|
|
105
|
-
params.id,
|
|
106
|
-
`title, "slug": slug.current`,
|
|
107
|
-
(doc) => (doc.slug ? `/${prefix}/${doc.slug}` : null),
|
|
108
|
-
"Untitled",
|
|
109
|
-
);
|
|
141
|
+
return resolveHostPageLocations(documentStore, params.id);
|
|
110
142
|
},
|
|
111
143
|
},
|
|
112
144
|
previewUrl: {
|
|
@@ -4,7 +4,7 @@ import { defineBlock } from "@/lib/integrations/sanity/schemas/shared/block";
|
|
|
4
4
|
|
|
5
5
|
export const blogContent = defineBlock({
|
|
6
6
|
name: "blogContent",
|
|
7
|
-
title: "Blog
|
|
7
|
+
title: "Blog Posts",
|
|
8
8
|
icon: contentIcon,
|
|
9
9
|
fields: [
|
|
10
10
|
defineField({
|
|
@@ -80,7 +80,7 @@ export const blogContent = defineBlock({
|
|
|
80
80
|
},
|
|
81
81
|
prepare({ title, date, media }) {
|
|
82
82
|
return {
|
|
83
|
-
title: title || "Blog
|
|
83
|
+
title: title || "Blog Posts",
|
|
84
84
|
subtitle: date || "No date set",
|
|
85
85
|
media,
|
|
86
86
|
};
|
package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/structure.ts
CHANGED
|
@@ -252,7 +252,7 @@ export const structure: StructureResolver = (S, context) =>
|
|
|
252
252
|
|
|
253
253
|
S.divider(),
|
|
254
254
|
|
|
255
|
-
// GROUP 2: Content -- authors, blog categories
|
|
255
|
+
// GROUP 2: Content -- blog posts, authors, blog categories
|
|
256
256
|
S.listItem()
|
|
257
257
|
.title("Content")
|
|
258
258
|
.icon(contentFolderIcon)
|
|
@@ -260,6 +260,9 @@ export const structure: StructureResolver = (S, context) =>
|
|
|
260
260
|
S.list()
|
|
261
261
|
.title("Content")
|
|
262
262
|
.items([
|
|
263
|
+
S.documentTypeListItem("blogContent")
|
|
264
|
+
.title("Blog Posts")
|
|
265
|
+
.icon(contentIcon),
|
|
263
266
|
S.documentTypeListItem("author")
|
|
264
267
|
.title("Authors")
|
|
265
268
|
.icon(authorIcon),
|
|
@@ -299,9 +302,6 @@ export const structure: StructureResolver = (S, context) =>
|
|
|
299
302
|
S.documentTypeListItem("gatedContent")
|
|
300
303
|
.title("Gated Content")
|
|
301
304
|
.icon(gatedContentIcon),
|
|
302
|
-
S.documentTypeListItem("blogContent")
|
|
303
|
-
.title("Blog Content")
|
|
304
|
-
.icon(contentIcon),
|
|
305
305
|
S.divider(),
|
|
306
306
|
S.documentTypeListItem("studioPreview")
|
|
307
307
|
.title("Component Previews")
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
presentationTool,
|
|
8
8
|
} from "sanity/presentation";
|
|
9
9
|
import { previewURL } from "./env";
|
|
10
|
+
import { pageBuilderReferenceMembers } from "./page-builder-config";
|
|
10
11
|
import { getPinnedPageById, HOMEPAGE_PINNED_PAGE } from "./pinned-pages";
|
|
11
12
|
|
|
12
13
|
const MAX_FOLDER_DEPTH = 6;
|
|
@@ -43,10 +44,52 @@ const pageResolvedSlugExpression = buildNestedSlugExpression(
|
|
|
43
44
|
"slug.current",
|
|
44
45
|
);
|
|
45
46
|
|
|
46
|
-
const
|
|
47
|
-
|
|
47
|
+
const PAGE_BUILDER_DOCUMENT_TYPES = new Set<string>(
|
|
48
|
+
pageBuilderReferenceMembers.map((member) => member.documentType),
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const HOST_PAGES_QUERY = `*[_type == "page" && references($id)]{
|
|
52
|
+
_id,
|
|
53
|
+
title,
|
|
54
|
+
"resolvedSlug": ${pageResolvedSlugExpression}
|
|
55
|
+
}`;
|
|
56
|
+
|
|
57
|
+
type HostPage = {
|
|
58
|
+
_id: string;
|
|
59
|
+
title?: string;
|
|
60
|
+
resolvedSlug?: string;
|
|
48
61
|
};
|
|
49
62
|
|
|
63
|
+
const isDraftId = (id: string) => id.startsWith("drafts.");
|
|
64
|
+
|
|
65
|
+
const resolveHostPageLocations = (
|
|
66
|
+
documentStore: DocumentStore,
|
|
67
|
+
id: string,
|
|
68
|
+
): Observable<DocumentLocationsState> =>
|
|
69
|
+
documentStore.listenQuery(HOST_PAGES_QUERY, { id }, {}).pipe(
|
|
70
|
+
map((pages: HostPage[] | null) => {
|
|
71
|
+
const deduped = new Map<string, HostPage>();
|
|
72
|
+
for (const page of pages ?? []) {
|
|
73
|
+
const publishedId = page._id.replace(/^drafts\./, "");
|
|
74
|
+
const existing = deduped.get(publishedId);
|
|
75
|
+
if (!existing || isDraftId(existing._id)) {
|
|
76
|
+
deduped.set(publishedId, page);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
locations: [...deduped.values()].flatMap((page) => {
|
|
82
|
+
const pinned = getPinnedPageById(page._id);
|
|
83
|
+
const href =
|
|
84
|
+
pinned?.path ??
|
|
85
|
+
(page.resolvedSlug ? `/${page.resolvedSlug}` : null);
|
|
86
|
+
if (!href) return [];
|
|
87
|
+
return [{ title: page.title || pinned?.title || "Page", href }];
|
|
88
|
+
}),
|
|
89
|
+
};
|
|
90
|
+
}),
|
|
91
|
+
);
|
|
92
|
+
|
|
50
93
|
const resolveSlugLocation = <T extends { title?: string }>(
|
|
51
94
|
documentStore: DocumentStore,
|
|
52
95
|
id: string,
|
|
@@ -73,10 +116,6 @@ export const presentation = presentationTool({
|
|
|
73
116
|
route: "/",
|
|
74
117
|
filter: `_type == "page" && _id == "${HOMEPAGE_PINNED_PAGE.id}"`,
|
|
75
118
|
},
|
|
76
|
-
...Object.entries(CONTENT_TYPE_PREFIXES).map(([type, prefix]) => ({
|
|
77
|
-
route: `/${prefix}/:slug`,
|
|
78
|
-
filter: `_type == "${type}" && slug.current == $slug`,
|
|
79
|
-
})),
|
|
80
119
|
{
|
|
81
120
|
route: "/:slug+",
|
|
82
121
|
filter: `_type == "page" && ${pageResolvedSlugExpression} == $slug`,
|
|
@@ -97,16 +136,9 @@ export const presentation = presentationTool({
|
|
|
97
136
|
);
|
|
98
137
|
}
|
|
99
138
|
|
|
100
|
-
|
|
101
|
-
if (!prefix) return null;
|
|
139
|
+
if (!PAGE_BUILDER_DOCUMENT_TYPES.has(params.type)) return null;
|
|
102
140
|
|
|
103
|
-
return
|
|
104
|
-
documentStore,
|
|
105
|
-
params.id,
|
|
106
|
-
`title, "slug": slug.current`,
|
|
107
|
-
(doc) => (doc.slug ? `/${prefix}/${doc.slug}` : null),
|
|
108
|
-
"Untitled",
|
|
109
|
-
);
|
|
141
|
+
return resolveHostPageLocations(documentStore, params.id);
|
|
110
142
|
},
|
|
111
143
|
},
|
|
112
144
|
previewUrl: {
|
|
@@ -4,7 +4,7 @@ import { defineBlock } from "@/lib/integrations/sanity/schemas/shared/block";
|
|
|
4
4
|
|
|
5
5
|
export const blogContent = defineBlock({
|
|
6
6
|
name: "blogContent",
|
|
7
|
-
title: "Blog
|
|
7
|
+
title: "Blog Posts",
|
|
8
8
|
icon: contentIcon,
|
|
9
9
|
fields: [
|
|
10
10
|
defineField({
|
|
@@ -80,7 +80,7 @@ export const blogContent = defineBlock({
|
|
|
80
80
|
},
|
|
81
81
|
prepare({ title, date, media }) {
|
|
82
82
|
return {
|
|
83
|
-
title: title || "Blog
|
|
83
|
+
title: title || "Blog Posts",
|
|
84
84
|
subtitle: date || "No date set",
|
|
85
85
|
media,
|
|
86
86
|
};
|
package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/structure.ts
CHANGED
|
@@ -252,7 +252,7 @@ export const structure: StructureResolver = (S, context) =>
|
|
|
252
252
|
|
|
253
253
|
S.divider(),
|
|
254
254
|
|
|
255
|
-
// GROUP 2: Content -- authors, blog categories
|
|
255
|
+
// GROUP 2: Content -- blog posts, authors, blog categories
|
|
256
256
|
S.listItem()
|
|
257
257
|
.title("Content")
|
|
258
258
|
.icon(contentFolderIcon)
|
|
@@ -260,6 +260,9 @@ export const structure: StructureResolver = (S, context) =>
|
|
|
260
260
|
S.list()
|
|
261
261
|
.title("Content")
|
|
262
262
|
.items([
|
|
263
|
+
S.documentTypeListItem("blogContent")
|
|
264
|
+
.title("Blog Posts")
|
|
265
|
+
.icon(contentIcon),
|
|
263
266
|
S.documentTypeListItem("author")
|
|
264
267
|
.title("Authors")
|
|
265
268
|
.icon(authorIcon),
|
|
@@ -299,9 +302,6 @@ export const structure: StructureResolver = (S, context) =>
|
|
|
299
302
|
S.documentTypeListItem("gatedContent")
|
|
300
303
|
.title("Gated Content")
|
|
301
304
|
.icon(gatedContentIcon),
|
|
302
|
-
S.documentTypeListItem("blogContent")
|
|
303
|
-
.title("Blog Content")
|
|
304
|
-
.icon(contentIcon),
|
|
305
305
|
S.divider(),
|
|
306
306
|
S.documentTypeListItem("studioPreview")
|
|
307
307
|
.title("Component Previews")
|
|
@@ -45,6 +45,7 @@ export const metadata = generatePageMetadata({ title: 'About' })
|
|
|
45
45
|
// next/image sizes — keys mirror the element's Tailwind variants
|
|
46
46
|
buildImageSizes({ base: '100vw', desktop: '796px' })
|
|
47
47
|
buildGridImageSizes({ base: 1, tablet: 2, desktop: 3 })
|
|
48
|
+
buildGridImageSizes({ base: 1, tablet: 2, desktop: 3 }, { maxWidth: 1440 })
|
|
48
49
|
|
|
49
50
|
// Refs — in a component, use the hook: an inline mergeRefs([...]) is a new
|
|
50
51
|
// callback each render, so React detaches and reattaches every ref.
|
|
@@ -15,40 +15,55 @@ export type ByBreakpoint<
|
|
|
15
15
|
B extends BreakpointMap = typeof BASEMENT_BREAKPOINTS,
|
|
16
16
|
> = { base: T } & Partial<Record<Extract<keyof B, string>, T>>
|
|
17
17
|
|
|
18
|
+
export type ImageSizesOptions<
|
|
19
|
+
B extends BreakpointMap = typeof BASEMENT_BREAKPOINTS,
|
|
20
|
+
> = { breakpoints?: B }
|
|
21
|
+
|
|
22
|
+
export type GridImageSizesOptions<
|
|
23
|
+
B extends BreakpointMap = typeof BASEMENT_BREAKPOINTS,
|
|
24
|
+
> = ImageSizesOptions<B> & {
|
|
25
|
+
/** The grid container's `max-width` in px, past which columns stop growing. */
|
|
26
|
+
maxWidth?: number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type Condition<T> = { min: number; value: T }
|
|
30
|
+
|
|
18
31
|
function columnWidth(columns: number): string {
|
|
19
32
|
return `${Math.round(100 / columns)}vw`
|
|
20
33
|
}
|
|
21
34
|
|
|
35
|
+
function toConditions<T>(
|
|
36
|
+
overrides: Readonly<Record<string, T | undefined>>,
|
|
37
|
+
scale: BreakpointMap
|
|
38
|
+
): Condition<T>[] {
|
|
39
|
+
return Object.entries(overrides).flatMap(([name, value]) => {
|
|
40
|
+
const min = scale[name]
|
|
41
|
+
return value !== undefined && min !== undefined ? [{ min, value }] : []
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
22
45
|
/**
|
|
23
46
|
* Widest first, since `sizes` takes the first matching condition and the last
|
|
24
47
|
* entry must be bare. `min-width` matches the Tailwind variant's boundary; a
|
|
25
48
|
* `max-width: <next> - 1` one would leave a fractional-pixel gap.
|
|
26
49
|
*/
|
|
27
|
-
function
|
|
28
|
-
|
|
29
|
-
overrides: Readonly<Record<string, string | undefined>>,
|
|
30
|
-
scale: BreakpointMap
|
|
31
|
-
): string {
|
|
32
|
-
const conditions: string[] = []
|
|
33
|
-
|
|
34
|
-
for (const [name, min] of Object.entries(scale).sort(
|
|
35
|
-
([, a], [, b]) => b - a
|
|
36
|
-
)) {
|
|
37
|
-
const value = overrides[name]
|
|
38
|
-
if (value !== undefined) {
|
|
39
|
-
conditions.push(`(min-width: ${min}px) ${value}`)
|
|
40
|
-
}
|
|
41
|
-
}
|
|
50
|
+
function joinSizes(base: string, conditions: Condition<string>[]): string {
|
|
51
|
+
const sorted = [...conditions].sort((a, b) => b.min - a.min)
|
|
42
52
|
|
|
43
|
-
return [
|
|
53
|
+
return [
|
|
54
|
+
...sorted.map(({ min, value }) => `(min-width: ${min}px) ${value}`),
|
|
55
|
+
base,
|
|
56
|
+
].join(", ")
|
|
44
57
|
}
|
|
45
58
|
|
|
46
59
|
/** Builds `sizes` from the CSS length the image renders at per breakpoint. */
|
|
47
60
|
export function buildImageSizes<
|
|
48
61
|
B extends BreakpointMap = typeof BASEMENT_BREAKPOINTS,
|
|
49
|
-
>(widths: ByBreakpoint<string, B>,
|
|
62
|
+
>(widths: ByBreakpoint<string, B>, options: ImageSizesOptions<B> = {}): string {
|
|
50
63
|
const { base, ...overrides } = widths
|
|
51
|
-
|
|
64
|
+
const scale = options.breakpoints ?? BASEMENT_BREAKPOINTS
|
|
65
|
+
|
|
66
|
+
return joinSizes(base, toConditions<string>(overrides, scale))
|
|
52
67
|
}
|
|
53
68
|
|
|
54
69
|
/**
|
|
@@ -58,15 +73,39 @@ export function buildImageSizes<
|
|
|
58
73
|
*/
|
|
59
74
|
export function buildGridImageSizes<
|
|
60
75
|
B extends BreakpointMap = typeof BASEMENT_BREAKPOINTS,
|
|
61
|
-
>(
|
|
76
|
+
>(
|
|
77
|
+
columns: ByBreakpoint<number, B>,
|
|
78
|
+
options: GridImageSizesOptions<B> = {}
|
|
79
|
+
): string {
|
|
80
|
+
const { maxWidth, breakpoints } = options
|
|
62
81
|
const { base, ...overrides } = columns
|
|
63
|
-
const
|
|
82
|
+
const steps = toConditions<number>(
|
|
83
|
+
overrides,
|
|
84
|
+
breakpoints ?? BASEMENT_BREAKPOINTS
|
|
85
|
+
)
|
|
86
|
+
const cap =
|
|
87
|
+
maxWidth !== undefined && Number.isFinite(maxWidth) && maxWidth > 0
|
|
88
|
+
? maxWidth
|
|
89
|
+
: undefined
|
|
90
|
+
|
|
91
|
+
// The cap rarely lands on a breakpoint, so it needs its own condition.
|
|
92
|
+
if (cap !== undefined && !steps.some(({ min }) => min === cap)) {
|
|
93
|
+
const below = steps
|
|
94
|
+
.filter(({ min }) => min < cap)
|
|
95
|
+
.sort((a, b) => a.min - b.min)
|
|
96
|
+
.at(-1)
|
|
64
97
|
|
|
65
|
-
|
|
66
|
-
if (count !== undefined) {
|
|
67
|
-
widths[name] = columnWidth(count)
|
|
68
|
-
}
|
|
98
|
+
steps.push({ min: cap, value: below?.value ?? base })
|
|
69
99
|
}
|
|
70
100
|
|
|
71
|
-
return
|
|
101
|
+
return joinSizes(
|
|
102
|
+
columnWidth(base),
|
|
103
|
+
steps.map(({ min, value }) => ({
|
|
104
|
+
min,
|
|
105
|
+
value:
|
|
106
|
+
cap !== undefined && min >= cap
|
|
107
|
+
? `${Math.round(cap / value)}px`
|
|
108
|
+
: columnWidth(value),
|
|
109
|
+
}))
|
|
110
|
+
)
|
|
72
111
|
}
|
|
@@ -45,6 +45,7 @@ export const metadata = generatePageMetadata({ title: 'About' })
|
|
|
45
45
|
// next/image sizes — keys mirror the element's Tailwind variants
|
|
46
46
|
buildImageSizes({ base: '100vw', desktop: '796px' })
|
|
47
47
|
buildGridImageSizes({ base: 1, tablet: 2, desktop: 3 })
|
|
48
|
+
buildGridImageSizes({ base: 1, tablet: 2, desktop: 3 }, { maxWidth: 1440 })
|
|
48
49
|
|
|
49
50
|
// Refs — in a component, use the hook: an inline mergeRefs([...]) is a new
|
|
50
51
|
// callback each render, so React detaches and reattaches every ref.
|
|
@@ -15,40 +15,55 @@ export type ByBreakpoint<
|
|
|
15
15
|
B extends BreakpointMap = typeof BASEMENT_BREAKPOINTS,
|
|
16
16
|
> = { base: T } & Partial<Record<Extract<keyof B, string>, T>>
|
|
17
17
|
|
|
18
|
+
export type ImageSizesOptions<
|
|
19
|
+
B extends BreakpointMap = typeof BASEMENT_BREAKPOINTS,
|
|
20
|
+
> = { breakpoints?: B }
|
|
21
|
+
|
|
22
|
+
export type GridImageSizesOptions<
|
|
23
|
+
B extends BreakpointMap = typeof BASEMENT_BREAKPOINTS,
|
|
24
|
+
> = ImageSizesOptions<B> & {
|
|
25
|
+
/** The grid container's `max-width` in px, past which columns stop growing. */
|
|
26
|
+
maxWidth?: number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type Condition<T> = { min: number; value: T }
|
|
30
|
+
|
|
18
31
|
function columnWidth(columns: number): string {
|
|
19
32
|
return `${Math.round(100 / columns)}vw`
|
|
20
33
|
}
|
|
21
34
|
|
|
35
|
+
function toConditions<T>(
|
|
36
|
+
overrides: Readonly<Record<string, T | undefined>>,
|
|
37
|
+
scale: BreakpointMap
|
|
38
|
+
): Condition<T>[] {
|
|
39
|
+
return Object.entries(overrides).flatMap(([name, value]) => {
|
|
40
|
+
const min = scale[name]
|
|
41
|
+
return value !== undefined && min !== undefined ? [{ min, value }] : []
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
22
45
|
/**
|
|
23
46
|
* Widest first, since `sizes` takes the first matching condition and the last
|
|
24
47
|
* entry must be bare. `min-width` matches the Tailwind variant's boundary; a
|
|
25
48
|
* `max-width: <next> - 1` one would leave a fractional-pixel gap.
|
|
26
49
|
*/
|
|
27
|
-
function
|
|
28
|
-
|
|
29
|
-
overrides: Readonly<Record<string, string | undefined>>,
|
|
30
|
-
scale: BreakpointMap
|
|
31
|
-
): string {
|
|
32
|
-
const conditions: string[] = []
|
|
33
|
-
|
|
34
|
-
for (const [name, min] of Object.entries(scale).sort(
|
|
35
|
-
([, a], [, b]) => b - a
|
|
36
|
-
)) {
|
|
37
|
-
const value = overrides[name]
|
|
38
|
-
if (value !== undefined) {
|
|
39
|
-
conditions.push(`(min-width: ${min}px) ${value}`)
|
|
40
|
-
}
|
|
41
|
-
}
|
|
50
|
+
function joinSizes(base: string, conditions: Condition<string>[]): string {
|
|
51
|
+
const sorted = [...conditions].sort((a, b) => b.min - a.min)
|
|
42
52
|
|
|
43
|
-
return [
|
|
53
|
+
return [
|
|
54
|
+
...sorted.map(({ min, value }) => `(min-width: ${min}px) ${value}`),
|
|
55
|
+
base,
|
|
56
|
+
].join(", ")
|
|
44
57
|
}
|
|
45
58
|
|
|
46
59
|
/** Builds `sizes` from the CSS length the image renders at per breakpoint. */
|
|
47
60
|
export function buildImageSizes<
|
|
48
61
|
B extends BreakpointMap = typeof BASEMENT_BREAKPOINTS,
|
|
49
|
-
>(widths: ByBreakpoint<string, B>,
|
|
62
|
+
>(widths: ByBreakpoint<string, B>, options: ImageSizesOptions<B> = {}): string {
|
|
50
63
|
const { base, ...overrides } = widths
|
|
51
|
-
|
|
64
|
+
const scale = options.breakpoints ?? BASEMENT_BREAKPOINTS
|
|
65
|
+
|
|
66
|
+
return joinSizes(base, toConditions<string>(overrides, scale))
|
|
52
67
|
}
|
|
53
68
|
|
|
54
69
|
/**
|
|
@@ -58,15 +73,39 @@ export function buildImageSizes<
|
|
|
58
73
|
*/
|
|
59
74
|
export function buildGridImageSizes<
|
|
60
75
|
B extends BreakpointMap = typeof BASEMENT_BREAKPOINTS,
|
|
61
|
-
>(
|
|
76
|
+
>(
|
|
77
|
+
columns: ByBreakpoint<number, B>,
|
|
78
|
+
options: GridImageSizesOptions<B> = {}
|
|
79
|
+
): string {
|
|
80
|
+
const { maxWidth, breakpoints } = options
|
|
62
81
|
const { base, ...overrides } = columns
|
|
63
|
-
const
|
|
82
|
+
const steps = toConditions<number>(
|
|
83
|
+
overrides,
|
|
84
|
+
breakpoints ?? BASEMENT_BREAKPOINTS
|
|
85
|
+
)
|
|
86
|
+
const cap =
|
|
87
|
+
maxWidth !== undefined && Number.isFinite(maxWidth) && maxWidth > 0
|
|
88
|
+
? maxWidth
|
|
89
|
+
: undefined
|
|
90
|
+
|
|
91
|
+
// The cap rarely lands on a breakpoint, so it needs its own condition.
|
|
92
|
+
if (cap !== undefined && !steps.some(({ min }) => min === cap)) {
|
|
93
|
+
const below = steps
|
|
94
|
+
.filter(({ min }) => min < cap)
|
|
95
|
+
.sort((a, b) => a.min - b.min)
|
|
96
|
+
.at(-1)
|
|
64
97
|
|
|
65
|
-
|
|
66
|
-
if (count !== undefined) {
|
|
67
|
-
widths[name] = columnWidth(count)
|
|
68
|
-
}
|
|
98
|
+
steps.push({ min: cap, value: below?.value ?? base })
|
|
69
99
|
}
|
|
70
100
|
|
|
71
|
-
return
|
|
101
|
+
return joinSizes(
|
|
102
|
+
columnWidth(base),
|
|
103
|
+
steps.map(({ min, value }) => ({
|
|
104
|
+
min,
|
|
105
|
+
value:
|
|
106
|
+
cap !== undefined && min >= cap
|
|
107
|
+
? `${Math.round(cap / value)}px`
|
|
108
|
+
: columnWidth(value),
|
|
109
|
+
}))
|
|
110
|
+
)
|
|
72
111
|
}
|
|
@@ -90,7 +90,7 @@ export const PageDocument = ({
|
|
|
90
90
|
// Editor-selected page-level structured data (FAQ, Event, custom, etc.).
|
|
91
91
|
const pageJsonLd = buildPageJsonLd(page.metadata?.jsonLd)
|
|
92
92
|
|
|
93
|
-
// Blog posts (pages with a Blog
|
|
93
|
+
// Blog posts (pages with a Blog Posts block) emit Article JSON-LD with
|
|
94
94
|
// fields derived from the post — no Custom block needed. Skipped when the
|
|
95
95
|
// editor already supplied an Article (via a Custom block) to avoid duplicates.
|
|
96
96
|
const blogBlock = page.pageBuilder?.find(
|
|
@@ -15,40 +15,55 @@ export type ByBreakpoint<
|
|
|
15
15
|
B extends BreakpointMap = typeof BASEMENT_BREAKPOINTS,
|
|
16
16
|
> = { base: T } & Partial<Record<Extract<keyof B, string>, T>>
|
|
17
17
|
|
|
18
|
+
export type ImageSizesOptions<
|
|
19
|
+
B extends BreakpointMap = typeof BASEMENT_BREAKPOINTS,
|
|
20
|
+
> = { breakpoints?: B }
|
|
21
|
+
|
|
22
|
+
export type GridImageSizesOptions<
|
|
23
|
+
B extends BreakpointMap = typeof BASEMENT_BREAKPOINTS,
|
|
24
|
+
> = ImageSizesOptions<B> & {
|
|
25
|
+
/** The grid container's `max-width` in px, past which columns stop growing. */
|
|
26
|
+
maxWidth?: number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type Condition<T> = { min: number; value: T }
|
|
30
|
+
|
|
18
31
|
function columnWidth(columns: number): string {
|
|
19
32
|
return `${Math.round(100 / columns)}vw`
|
|
20
33
|
}
|
|
21
34
|
|
|
35
|
+
function toConditions<T>(
|
|
36
|
+
overrides: Readonly<Record<string, T | undefined>>,
|
|
37
|
+
scale: BreakpointMap
|
|
38
|
+
): Condition<T>[] {
|
|
39
|
+
return Object.entries(overrides).flatMap(([name, value]) => {
|
|
40
|
+
const min = scale[name]
|
|
41
|
+
return value !== undefined && min !== undefined ? [{ min, value }] : []
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
22
45
|
/**
|
|
23
46
|
* Widest first, since `sizes` takes the first matching condition and the last
|
|
24
47
|
* entry must be bare. `min-width` matches the Tailwind variant's boundary; a
|
|
25
48
|
* `max-width: <next> - 1` one would leave a fractional-pixel gap.
|
|
26
49
|
*/
|
|
27
|
-
function
|
|
28
|
-
|
|
29
|
-
overrides: Readonly<Record<string, string | undefined>>,
|
|
30
|
-
scale: BreakpointMap
|
|
31
|
-
): string {
|
|
32
|
-
const conditions: string[] = []
|
|
33
|
-
|
|
34
|
-
for (const [name, min] of Object.entries(scale).sort(
|
|
35
|
-
([, a], [, b]) => b - a
|
|
36
|
-
)) {
|
|
37
|
-
const value = overrides[name]
|
|
38
|
-
if (value !== undefined) {
|
|
39
|
-
conditions.push(`(min-width: ${min}px) ${value}`)
|
|
40
|
-
}
|
|
41
|
-
}
|
|
50
|
+
function joinSizes(base: string, conditions: Condition<string>[]): string {
|
|
51
|
+
const sorted = [...conditions].sort((a, b) => b.min - a.min)
|
|
42
52
|
|
|
43
|
-
return [
|
|
53
|
+
return [
|
|
54
|
+
...sorted.map(({ min, value }) => `(min-width: ${min}px) ${value}`),
|
|
55
|
+
base,
|
|
56
|
+
].join(", ")
|
|
44
57
|
}
|
|
45
58
|
|
|
46
59
|
/** Builds `sizes` from the CSS length the image renders at per breakpoint. */
|
|
47
60
|
export function buildImageSizes<
|
|
48
61
|
B extends BreakpointMap = typeof BASEMENT_BREAKPOINTS,
|
|
49
|
-
>(widths: ByBreakpoint<string, B>,
|
|
62
|
+
>(widths: ByBreakpoint<string, B>, options: ImageSizesOptions<B> = {}): string {
|
|
50
63
|
const { base, ...overrides } = widths
|
|
51
|
-
|
|
64
|
+
const scale = options.breakpoints ?? BASEMENT_BREAKPOINTS
|
|
65
|
+
|
|
66
|
+
return joinSizes(base, toConditions<string>(overrides, scale))
|
|
52
67
|
}
|
|
53
68
|
|
|
54
69
|
/**
|
|
@@ -58,15 +73,39 @@ export function buildImageSizes<
|
|
|
58
73
|
*/
|
|
59
74
|
export function buildGridImageSizes<
|
|
60
75
|
B extends BreakpointMap = typeof BASEMENT_BREAKPOINTS,
|
|
61
|
-
>(
|
|
76
|
+
>(
|
|
77
|
+
columns: ByBreakpoint<number, B>,
|
|
78
|
+
options: GridImageSizesOptions<B> = {}
|
|
79
|
+
): string {
|
|
80
|
+
const { maxWidth, breakpoints } = options
|
|
62
81
|
const { base, ...overrides } = columns
|
|
63
|
-
const
|
|
82
|
+
const steps = toConditions<number>(
|
|
83
|
+
overrides,
|
|
84
|
+
breakpoints ?? BASEMENT_BREAKPOINTS
|
|
85
|
+
)
|
|
86
|
+
const cap =
|
|
87
|
+
maxWidth !== undefined && Number.isFinite(maxWidth) && maxWidth > 0
|
|
88
|
+
? maxWidth
|
|
89
|
+
: undefined
|
|
90
|
+
|
|
91
|
+
// The cap rarely lands on a breakpoint, so it needs its own condition.
|
|
92
|
+
if (cap !== undefined && !steps.some(({ min }) => min === cap)) {
|
|
93
|
+
const below = steps
|
|
94
|
+
.filter(({ min }) => min < cap)
|
|
95
|
+
.sort((a, b) => a.min - b.min)
|
|
96
|
+
.at(-1)
|
|
64
97
|
|
|
65
|
-
|
|
66
|
-
if (count !== undefined) {
|
|
67
|
-
widths[name] = columnWidth(count)
|
|
68
|
-
}
|
|
98
|
+
steps.push({ min: cap, value: below?.value ?? base })
|
|
69
99
|
}
|
|
70
100
|
|
|
71
|
-
return
|
|
101
|
+
return joinSizes(
|
|
102
|
+
columnWidth(base),
|
|
103
|
+
steps.map(({ min, value }) => ({
|
|
104
|
+
min,
|
|
105
|
+
value:
|
|
106
|
+
cap !== undefined && min >= cap
|
|
107
|
+
? `${Math.round(cap / value)}px`
|
|
108
|
+
: columnWidth(value),
|
|
109
|
+
}))
|
|
110
|
+
)
|
|
72
111
|
}
|
|
@@ -92,7 +92,7 @@ export const PageDocument = ({
|
|
|
92
92
|
// Editor-selected page-level structured data (FAQ, Event, custom, etc.).
|
|
93
93
|
const pageJsonLd = buildPageJsonLd(page.metadata?.jsonLd)
|
|
94
94
|
|
|
95
|
-
// Blog posts (pages with a Blog
|
|
95
|
+
// Blog posts (pages with a Blog Posts block) emit Article JSON-LD with
|
|
96
96
|
// fields derived from the post — no Custom block needed. Skipped when the
|
|
97
97
|
// editor already supplied an Article (via a Custom block) to avoid duplicates.
|
|
98
98
|
const blogBlock = page.pageBuilder?.find(
|
|
@@ -15,40 +15,55 @@ export type ByBreakpoint<
|
|
|
15
15
|
B extends BreakpointMap = typeof BASEMENT_BREAKPOINTS,
|
|
16
16
|
> = { base: T } & Partial<Record<Extract<keyof B, string>, T>>
|
|
17
17
|
|
|
18
|
+
export type ImageSizesOptions<
|
|
19
|
+
B extends BreakpointMap = typeof BASEMENT_BREAKPOINTS,
|
|
20
|
+
> = { breakpoints?: B }
|
|
21
|
+
|
|
22
|
+
export type GridImageSizesOptions<
|
|
23
|
+
B extends BreakpointMap = typeof BASEMENT_BREAKPOINTS,
|
|
24
|
+
> = ImageSizesOptions<B> & {
|
|
25
|
+
/** The grid container's `max-width` in px, past which columns stop growing. */
|
|
26
|
+
maxWidth?: number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type Condition<T> = { min: number; value: T }
|
|
30
|
+
|
|
18
31
|
function columnWidth(columns: number): string {
|
|
19
32
|
return `${Math.round(100 / columns)}vw`
|
|
20
33
|
}
|
|
21
34
|
|
|
35
|
+
function toConditions<T>(
|
|
36
|
+
overrides: Readonly<Record<string, T | undefined>>,
|
|
37
|
+
scale: BreakpointMap
|
|
38
|
+
): Condition<T>[] {
|
|
39
|
+
return Object.entries(overrides).flatMap(([name, value]) => {
|
|
40
|
+
const min = scale[name]
|
|
41
|
+
return value !== undefined && min !== undefined ? [{ min, value }] : []
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
22
45
|
/**
|
|
23
46
|
* Widest first, since `sizes` takes the first matching condition and the last
|
|
24
47
|
* entry must be bare. `min-width` matches the Tailwind variant's boundary; a
|
|
25
48
|
* `max-width: <next> - 1` one would leave a fractional-pixel gap.
|
|
26
49
|
*/
|
|
27
|
-
function
|
|
28
|
-
|
|
29
|
-
overrides: Readonly<Record<string, string | undefined>>,
|
|
30
|
-
scale: BreakpointMap
|
|
31
|
-
): string {
|
|
32
|
-
const conditions: string[] = []
|
|
33
|
-
|
|
34
|
-
for (const [name, min] of Object.entries(scale).sort(
|
|
35
|
-
([, a], [, b]) => b - a
|
|
36
|
-
)) {
|
|
37
|
-
const value = overrides[name]
|
|
38
|
-
if (value !== undefined) {
|
|
39
|
-
conditions.push(`(min-width: ${min}px) ${value}`)
|
|
40
|
-
}
|
|
41
|
-
}
|
|
50
|
+
function joinSizes(base: string, conditions: Condition<string>[]): string {
|
|
51
|
+
const sorted = [...conditions].sort((a, b) => b.min - a.min)
|
|
42
52
|
|
|
43
|
-
return [
|
|
53
|
+
return [
|
|
54
|
+
...sorted.map(({ min, value }) => `(min-width: ${min}px) ${value}`),
|
|
55
|
+
base,
|
|
56
|
+
].join(", ")
|
|
44
57
|
}
|
|
45
58
|
|
|
46
59
|
/** Builds `sizes` from the CSS length the image renders at per breakpoint. */
|
|
47
60
|
export function buildImageSizes<
|
|
48
61
|
B extends BreakpointMap = typeof BASEMENT_BREAKPOINTS,
|
|
49
|
-
>(widths: ByBreakpoint<string, B>,
|
|
62
|
+
>(widths: ByBreakpoint<string, B>, options: ImageSizesOptions<B> = {}): string {
|
|
50
63
|
const { base, ...overrides } = widths
|
|
51
|
-
|
|
64
|
+
const scale = options.breakpoints ?? BASEMENT_BREAKPOINTS
|
|
65
|
+
|
|
66
|
+
return joinSizes(base, toConditions<string>(overrides, scale))
|
|
52
67
|
}
|
|
53
68
|
|
|
54
69
|
/**
|
|
@@ -58,15 +73,39 @@ export function buildImageSizes<
|
|
|
58
73
|
*/
|
|
59
74
|
export function buildGridImageSizes<
|
|
60
75
|
B extends BreakpointMap = typeof BASEMENT_BREAKPOINTS,
|
|
61
|
-
>(
|
|
76
|
+
>(
|
|
77
|
+
columns: ByBreakpoint<number, B>,
|
|
78
|
+
options: GridImageSizesOptions<B> = {}
|
|
79
|
+
): string {
|
|
80
|
+
const { maxWidth, breakpoints } = options
|
|
62
81
|
const { base, ...overrides } = columns
|
|
63
|
-
const
|
|
82
|
+
const steps = toConditions<number>(
|
|
83
|
+
overrides,
|
|
84
|
+
breakpoints ?? BASEMENT_BREAKPOINTS
|
|
85
|
+
)
|
|
86
|
+
const cap =
|
|
87
|
+
maxWidth !== undefined && Number.isFinite(maxWidth) && maxWidth > 0
|
|
88
|
+
? maxWidth
|
|
89
|
+
: undefined
|
|
90
|
+
|
|
91
|
+
// The cap rarely lands on a breakpoint, so it needs its own condition.
|
|
92
|
+
if (cap !== undefined && !steps.some(({ min }) => min === cap)) {
|
|
93
|
+
const below = steps
|
|
94
|
+
.filter(({ min }) => min < cap)
|
|
95
|
+
.sort((a, b) => a.min - b.min)
|
|
96
|
+
.at(-1)
|
|
64
97
|
|
|
65
|
-
|
|
66
|
-
if (count !== undefined) {
|
|
67
|
-
widths[name] = columnWidth(count)
|
|
68
|
-
}
|
|
98
|
+
steps.push({ min: cap, value: below?.value ?? base })
|
|
69
99
|
}
|
|
70
100
|
|
|
71
|
-
return
|
|
101
|
+
return joinSizes(
|
|
102
|
+
columnWidth(base),
|
|
103
|
+
steps.map(({ min, value }) => ({
|
|
104
|
+
min,
|
|
105
|
+
value:
|
|
106
|
+
cap !== undefined && min >= cap
|
|
107
|
+
? `${Math.round(cap / value)}px`
|
|
108
|
+
: columnWidth(value),
|
|
109
|
+
}))
|
|
110
|
+
)
|
|
72
111
|
}
|
|
@@ -45,6 +45,7 @@ export const metadata = generatePageMetadata({ title: 'About' })
|
|
|
45
45
|
// next/image sizes — keys mirror the element's Tailwind variants
|
|
46
46
|
buildImageSizes({ base: '100vw', desktop: '796px' })
|
|
47
47
|
buildGridImageSizes({ base: 1, tablet: 2, desktop: 3 })
|
|
48
|
+
buildGridImageSizes({ base: 1, tablet: 2, desktop: 3 }, { maxWidth: 1440 })
|
|
48
49
|
|
|
49
50
|
// Refs — in a component, use the hook: an inline mergeRefs([...]) is a new
|
|
50
51
|
// callback each render, so React detaches and reattaches every ref.
|
|
@@ -15,40 +15,55 @@ export type ByBreakpoint<
|
|
|
15
15
|
B extends BreakpointMap = typeof BASEMENT_BREAKPOINTS,
|
|
16
16
|
> = { base: T } & Partial<Record<Extract<keyof B, string>, T>>
|
|
17
17
|
|
|
18
|
+
export type ImageSizesOptions<
|
|
19
|
+
B extends BreakpointMap = typeof BASEMENT_BREAKPOINTS,
|
|
20
|
+
> = { breakpoints?: B }
|
|
21
|
+
|
|
22
|
+
export type GridImageSizesOptions<
|
|
23
|
+
B extends BreakpointMap = typeof BASEMENT_BREAKPOINTS,
|
|
24
|
+
> = ImageSizesOptions<B> & {
|
|
25
|
+
/** The grid container's `max-width` in px, past which columns stop growing. */
|
|
26
|
+
maxWidth?: number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type Condition<T> = { min: number; value: T }
|
|
30
|
+
|
|
18
31
|
function columnWidth(columns: number): string {
|
|
19
32
|
return `${Math.round(100 / columns)}vw`
|
|
20
33
|
}
|
|
21
34
|
|
|
35
|
+
function toConditions<T>(
|
|
36
|
+
overrides: Readonly<Record<string, T | undefined>>,
|
|
37
|
+
scale: BreakpointMap
|
|
38
|
+
): Condition<T>[] {
|
|
39
|
+
return Object.entries(overrides).flatMap(([name, value]) => {
|
|
40
|
+
const min = scale[name]
|
|
41
|
+
return value !== undefined && min !== undefined ? [{ min, value }] : []
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
22
45
|
/**
|
|
23
46
|
* Widest first, since `sizes` takes the first matching condition and the last
|
|
24
47
|
* entry must be bare. `min-width` matches the Tailwind variant's boundary; a
|
|
25
48
|
* `max-width: <next> - 1` one would leave a fractional-pixel gap.
|
|
26
49
|
*/
|
|
27
|
-
function
|
|
28
|
-
|
|
29
|
-
overrides: Readonly<Record<string, string | undefined>>,
|
|
30
|
-
scale: BreakpointMap
|
|
31
|
-
): string {
|
|
32
|
-
const conditions: string[] = []
|
|
33
|
-
|
|
34
|
-
for (const [name, min] of Object.entries(scale).sort(
|
|
35
|
-
([, a], [, b]) => b - a
|
|
36
|
-
)) {
|
|
37
|
-
const value = overrides[name]
|
|
38
|
-
if (value !== undefined) {
|
|
39
|
-
conditions.push(`(min-width: ${min}px) ${value}`)
|
|
40
|
-
}
|
|
41
|
-
}
|
|
50
|
+
function joinSizes(base: string, conditions: Condition<string>[]): string {
|
|
51
|
+
const sorted = [...conditions].sort((a, b) => b.min - a.min)
|
|
42
52
|
|
|
43
|
-
return [
|
|
53
|
+
return [
|
|
54
|
+
...sorted.map(({ min, value }) => `(min-width: ${min}px) ${value}`),
|
|
55
|
+
base,
|
|
56
|
+
].join(", ")
|
|
44
57
|
}
|
|
45
58
|
|
|
46
59
|
/** Builds `sizes` from the CSS length the image renders at per breakpoint. */
|
|
47
60
|
export function buildImageSizes<
|
|
48
61
|
B extends BreakpointMap = typeof BASEMENT_BREAKPOINTS,
|
|
49
|
-
>(widths: ByBreakpoint<string, B>,
|
|
62
|
+
>(widths: ByBreakpoint<string, B>, options: ImageSizesOptions<B> = {}): string {
|
|
50
63
|
const { base, ...overrides } = widths
|
|
51
|
-
|
|
64
|
+
const scale = options.breakpoints ?? BASEMENT_BREAKPOINTS
|
|
65
|
+
|
|
66
|
+
return joinSizes(base, toConditions<string>(overrides, scale))
|
|
52
67
|
}
|
|
53
68
|
|
|
54
69
|
/**
|
|
@@ -58,15 +73,39 @@ export function buildImageSizes<
|
|
|
58
73
|
*/
|
|
59
74
|
export function buildGridImageSizes<
|
|
60
75
|
B extends BreakpointMap = typeof BASEMENT_BREAKPOINTS,
|
|
61
|
-
>(
|
|
76
|
+
>(
|
|
77
|
+
columns: ByBreakpoint<number, B>,
|
|
78
|
+
options: GridImageSizesOptions<B> = {}
|
|
79
|
+
): string {
|
|
80
|
+
const { maxWidth, breakpoints } = options
|
|
62
81
|
const { base, ...overrides } = columns
|
|
63
|
-
const
|
|
82
|
+
const steps = toConditions<number>(
|
|
83
|
+
overrides,
|
|
84
|
+
breakpoints ?? BASEMENT_BREAKPOINTS
|
|
85
|
+
)
|
|
86
|
+
const cap =
|
|
87
|
+
maxWidth !== undefined && Number.isFinite(maxWidth) && maxWidth > 0
|
|
88
|
+
? maxWidth
|
|
89
|
+
: undefined
|
|
90
|
+
|
|
91
|
+
// The cap rarely lands on a breakpoint, so it needs its own condition.
|
|
92
|
+
if (cap !== undefined && !steps.some(({ min }) => min === cap)) {
|
|
93
|
+
const below = steps
|
|
94
|
+
.filter(({ min }) => min < cap)
|
|
95
|
+
.sort((a, b) => a.min - b.min)
|
|
96
|
+
.at(-1)
|
|
64
97
|
|
|
65
|
-
|
|
66
|
-
if (count !== undefined) {
|
|
67
|
-
widths[name] = columnWidth(count)
|
|
68
|
-
}
|
|
98
|
+
steps.push({ min: cap, value: below?.value ?? base })
|
|
69
99
|
}
|
|
70
100
|
|
|
71
|
-
return
|
|
101
|
+
return joinSizes(
|
|
102
|
+
columnWidth(base),
|
|
103
|
+
steps.map(({ min, value }) => ({
|
|
104
|
+
min,
|
|
105
|
+
value:
|
|
106
|
+
cap !== undefined && min >= cap
|
|
107
|
+
? `${Math.round(cap / value)}px`
|
|
108
|
+
: columnWidth(value),
|
|
109
|
+
}))
|
|
110
|
+
)
|
|
72
111
|
}
|