bsmnt 0.13.0 → 0.14.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/dist/modules/features/cms/sanity-pagebuilder/config.d.ts.map +1 -1
- package/dist/modules/features/cms/sanity-pagebuilder/config.js +1 -0
- package/dist/modules/features/cms/sanity-pagebuilder/config.js.map +1 -1
- package/dist/modules/features/cms/sanity-pagebuilder-cache/config.d.ts.map +1 -1
- package/dist/modules/features/cms/sanity-pagebuilder-cache/config.js +1 -0
- package/dist/modules/features/cms/sanity-pagebuilder-cache/config.js.map +1 -1
- package/package.json +1 -1
- package/src/agent-skills/ashby-pagebuilder-block/SKILL.md +29 -11
- package/src/agent-skills/hubspot-pagebuilder-block/SKILL.md +31 -12
- package/src/agent-skills/page-builder-block/SKILL.md +26 -13
- package/src/modules/features/cms/sanity-pagebuilder/files/app/api/revalidate/route.ts +2 -2
- package/src/modules/features/cms/sanity-pagebuilder/files/components/sanity/rich-text.tsx +1 -1
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/block-actions-item.tsx +2 -2
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/component-preview-frame.tsx +124 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/component-preview-pane.tsx +25 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/page-builder-input.tsx +3 -26
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/page-builder-insert-menu.tsx +121 -93
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/icons.ts +2 -2
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/page-builder-config.ts +22 -50
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/presentation.ts +1 -18
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/queries.ts +56 -64
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/sanity.config.ts +4 -8
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/index.ts +0 -1
- 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/schemas/index.ts +1 -4
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/block.ts +4 -4
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/page-builder.ts +4 -18
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/page.ts +11 -37
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/singletons.ts +0 -16
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/structure.ts +35 -33
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/utils/studio-embed.ts +22 -0
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/app/api/revalidate/route.ts +2 -2
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/components/sanity/rich-text.tsx +1 -1
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/block-actions-item.tsx +2 -2
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/component-preview-frame.tsx +124 -0
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/component-preview-pane.tsx +25 -0
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/page-builder-input.tsx +3 -26
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/page-builder-insert-menu.tsx +121 -93
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/icons.ts +2 -2
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/page-builder-config.ts +22 -50
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/presentation.ts +1 -18
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/queries.ts +56 -64
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/sanity.config.ts +4 -8
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/index.ts +0 -1
- 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/schemas/index.ts +1 -4
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/block.ts +4 -4
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/page-builder.ts +4 -18
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/page.ts +11 -37
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/singletons.ts +0 -16
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/structure.ts +35 -33
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/utils/studio-embed.ts +22 -0
- package/src/modules/features/forms/hubspot/files/_sanity-adapter/form-block.ts +4 -10
- package/src/modules/features/forms/hubspot/files/lib/integrations/hubspot/README.md +10 -5
- package/src/modules/features/jobs/ashby/files/_sanity-adapter/job-board-block.ts +0 -7
- package/src/modules/features/jobs/ashby/files/lib/integrations/ashby/README.md +11 -5
- 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/AGENTS.md +19 -7
- package/src/templates/next-pagebuilder/README.md +15 -3
- package/src/templates/next-pagebuilder/app/actions/unlock-gated-content.ts +5 -4
- package/src/templates/next-pagebuilder/app/components/page.tsx +81 -0
- package/src/templates/next-pagebuilder/components/page-builder/components/gated-content/index.tsx +8 -3
- package/src/templates/next-pagebuilder/components/page-builder/library.ts +183 -0
- package/src/templates/next-pagebuilder/components/page-builder/registry.tsx +4 -1
- package/src/templates/next-pagebuilder/components/page-builder/renderer.tsx +3 -1
- package/src/templates/next-pagebuilder/components/page-builder/stage.tsx +10 -0
- package/src/templates/next-pagebuilder/components/page-document/index.tsx +2 -1
- package/src/templates/next-pagebuilder/lib/integrations/sanity/sanity.types.ts +139 -271
- package/src/templates/next-pagebuilder/lib/utils/image-sizes.ts +64 -25
- package/src/templates/next-pagebuilder-cache/AGENTS.md +19 -7
- package/src/templates/next-pagebuilder-cache/README.md +15 -3
- package/src/templates/next-pagebuilder-cache/app/actions/unlock-gated-content.ts +5 -4
- package/src/templates/next-pagebuilder-cache/app/components/page.tsx +91 -0
- package/src/templates/next-pagebuilder-cache/components/page-builder/components/gated-content/index.tsx +8 -3
- package/src/templates/next-pagebuilder-cache/components/page-builder/library.ts +183 -0
- package/src/templates/next-pagebuilder-cache/components/page-builder/registry.tsx +4 -1
- package/src/templates/next-pagebuilder-cache/components/page-builder/renderer.tsx +3 -1
- package/src/templates/next-pagebuilder-cache/components/page-builder/stage.tsx +10 -0
- package/src/templates/next-pagebuilder-cache/components/page-document/index.tsx +2 -1
- package/src/templates/next-pagebuilder-cache/lib/integrations/sanity/sanity.types.ts +139 -271
- 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/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/use-studio-preview-urls.ts +0 -62
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/studio-preview.ts +0 -37
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/use-studio-preview-urls.ts +0 -62
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/studio-preview.ts +0 -37
|
@@ -105,10 +105,15 @@ selectable block. Adding this integration installs the
|
|
|
105
105
|
HubSpot form into the page builder" and it makes the edits and verifies the
|
|
106
106
|
typecheck. To do it by hand:
|
|
107
107
|
|
|
108
|
-
1. `schemas/
|
|
108
|
+
1. `schemas/components/reusable/hubspot-form.ts` — re-declare the adapter
|
|
109
|
+
through `defineBlock`, so the block is an inline object with Anchor ID and
|
|
110
|
+
Presentation Mode; export it from `schemas/components/index.ts` and add it to
|
|
111
|
+
`schema.types` in `schemas/index.ts`
|
|
109
112
|
2. `page-builder-config.ts` — add `"hubspotFormBlock"` to
|
|
110
|
-
`PageBuilderDocumentType` and a `
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
decodes `block.form` and renders `<HubSpotForm />`
|
|
113
|
+
`PageBuilderDocumentType` and a `pageBuilderBlockMembers` entry with
|
|
114
|
+
`pageTypes` and `category`
|
|
115
|
+
3. `components/page-builder/registry.tsx` — one entry keyed `hubspotFormBlock`
|
|
116
|
+
that decodes `block.form` and renders `<HubSpotForm />`
|
|
117
|
+
4. `components/page-builder/library.ts` — a fixture entry, which is what the
|
|
118
|
+
insert menu, Studio → Components and `/components` render
|
|
114
119
|
5. `bun run sanity:typegen && bun typecheck`
|
|
@@ -44,10 +44,3 @@ export const ashbyJobBoardBlock = defineType({
|
|
|
44
44
|
},
|
|
45
45
|
},
|
|
46
46
|
})
|
|
47
|
-
|
|
48
|
-
export const ashbyJobBoardBlockReference = defineType({
|
|
49
|
-
name: "ashbyJobBoardBlockReference",
|
|
50
|
-
title: "Ashby Job Board",
|
|
51
|
-
type: "reference",
|
|
52
|
-
to: [{ type: "ashbyJobBoardBlock" }],
|
|
53
|
-
})
|
|
@@ -125,10 +125,16 @@ selectable block. Adding this integration installs the
|
|
|
125
125
|
Ashby job board into the page builder" and it makes the edits and verifies the
|
|
126
126
|
typecheck. To do it by hand:
|
|
127
127
|
|
|
128
|
-
1. `schemas/
|
|
128
|
+
1. `schemas/components/reusable/ashby-job-board.ts` — re-declare the adapter
|
|
129
|
+
through `defineBlock`, so the block is an inline object with Anchor ID and
|
|
130
|
+
Presentation Mode; export it from `schemas/components/index.ts` and add it to
|
|
131
|
+
`schema.types` in `schemas/index.ts`
|
|
129
132
|
2. `page-builder-config.ts` — add `"ashbyJobBoardBlock"` to
|
|
130
|
-
`PageBuilderDocumentType` and
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
that awaits `getCareerJobs(block.boardName)` and renders
|
|
133
|
+
`PageBuilderDocumentType` and a `pageBuilderBlockMembers` entry with
|
|
134
|
+
`pageTypes` and `category`
|
|
135
|
+
3. `components/page-builder/registry.tsx` — one entry keyed
|
|
136
|
+
`ashbyJobBoardBlock` that awaits `getCareerJobs(block.boardName)` and renders
|
|
137
|
+
the list
|
|
138
|
+
4. `components/page-builder/library.ts` — a fixture entry, which is what the
|
|
139
|
+
insert menu, Studio → Components and `/components` render
|
|
134
140
|
5. `bun run sanity:typegen && bun typecheck`
|
|
@@ -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
|
}
|
|
@@ -11,14 +11,24 @@ page.
|
|
|
11
11
|
Studio with a slug, not a directory. `app/(content)/[[...slug]]/page.tsx`
|
|
12
12
|
resolves every one of them; a hand-written route shadows that resolver and
|
|
13
13
|
becomes a URL no editor can see, move or delete. The only things that belong in
|
|
14
|
-
`app/` are non-page endpoints — the Studio, API handlers, sitemap, feed
|
|
14
|
+
`app/` are non-page endpoints — the Studio, API handlers, sitemap, feed, and
|
|
15
|
+
`app/components`, the Studio's live block preview: it renders fixtures rather
|
|
16
|
+
than content, is gated to Studio embeds by `isStudioEmbed()` and is `noindex`.
|
|
15
17
|
|
|
16
18
|
## Page-builder blocks
|
|
17
19
|
|
|
20
|
+
**Blocks are inline objects, never documents.** A placement belongs to exactly
|
|
21
|
+
one page, so the page's `pageBuilder` array stores the block itself and nothing
|
|
22
|
+
is dereferenced at read time. Don't reintroduce a block-as-document with a
|
|
23
|
+
reference member — that model was removed deliberately: no block was ever
|
|
24
|
+
reused, and it cost a hop on every read plus a second id to address.
|
|
25
|
+
|
|
18
26
|
**A block is `defineBlock` + a registry entry.** Define the schema with
|
|
19
27
|
`defineBlock` (`lib/integrations/sanity/schemas/shared/block.ts`), never
|
|
20
|
-
`defineType` — it implies `type: "
|
|
21
|
-
**Presentation Mode** for you.
|
|
28
|
+
`defineType` — it implies `type: "object"` and appends **Anchor ID** and
|
|
29
|
+
**Presentation Mode** for you. The array member's type name *is* the block's
|
|
30
|
+
`_type`, so the registry, the insert menu and the fixture library all key off
|
|
31
|
+
the same string. Register the component in
|
|
22
32
|
`components/page-builder/registry.tsx`; the renderer is driven by that map, so
|
|
23
33
|
there is no switch to extend.
|
|
24
34
|
|
|
@@ -33,10 +43,12 @@ that was never registered — is a typecheck error.
|
|
|
33
43
|
drives the insert menu; `misc` only when nothing else fits). Never collapse them
|
|
34
44
|
into one field: that is what made the menu a single flat list.
|
|
35
45
|
|
|
36
|
-
**And a
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
46
|
+
**And a fixture.** A block isn't done until `components/page-builder/library.ts`
|
|
47
|
+
has an entry for it: a stand-in value typed as the block's query result, plus a
|
|
48
|
+
one-line note. That entry is the block's preview everywhere — the live pane in
|
|
49
|
+
the insert menu, its row in Studio → Components, and the `/components` route,
|
|
50
|
+
all rendered by the production renderer. Without one the picker just says the
|
|
51
|
+
component has no preview.
|
|
40
52
|
|
|
41
53
|
**Never hardcode vertical padding in a block component.** The renderer wraps
|
|
42
54
|
each block in `<Section>` (`components/page-builder/section.tsx`), which owns
|
|
@@ -18,14 +18,18 @@ bun dev
|
|
|
18
18
|
|
|
19
19
|
## Page-builder blocks
|
|
20
20
|
|
|
21
|
-
A block is two halves — a schema
|
|
22
|
-
|
|
21
|
+
A block is two halves — a schema type and a React component — and each half has
|
|
22
|
+
one place that makes it a block rather than an ordinary object type or a loose
|
|
23
23
|
component. Use them and Anchor ID, Presentation Mode and the spacing scale come
|
|
24
24
|
for free.
|
|
25
25
|
|
|
26
|
+
Blocks are **inline objects** in the page's `pageBuilder` array, never documents
|
|
27
|
+
and never referenced: a placement belongs to exactly one page, so the block
|
|
28
|
+
lives on that page and nothing is dereferenced at read time.
|
|
29
|
+
|
|
26
30
|
**Define the schema with `defineBlock`, never `defineType`**
|
|
27
31
|
(`lib/integrations/sanity/schemas/shared/block.ts`). It implies
|
|
28
|
-
`type: "
|
|
32
|
+
`type: "object"` and appends two fields to every block:
|
|
29
33
|
|
|
30
34
|
- **Anchor ID** — optional. Left empty, the anchor is generated from the block's
|
|
31
35
|
own title at render time (`headline` / `heading` / `teaserHeading` / `title` /
|
|
@@ -54,6 +58,14 @@ block in `<Section>` (`components/page-builder/section.tsx`), which owns the
|
|
|
54
58
|
component returns its content only — no outer `<section>`, no `py-*` / `pt-*` /
|
|
55
59
|
`pb-*` on its root. Internal gaps between a block's own parts are fine.
|
|
56
60
|
|
|
61
|
+
**Every block ships a fixture in `components/page-builder/library.ts`**: a
|
|
62
|
+
stand-in value typed as the block's query result, plus a one-line note. It is
|
|
63
|
+
the block's only preview, and it is rendered by the production renderer at
|
|
64
|
+
`/components` — a route the Studio embeds in an iframe, so the insert menu and
|
|
65
|
+
**Components** in the Studio show the real block rather than a screenshot. The
|
|
66
|
+
route is gated to Studio embeds and `noindex`; a block with no entry simply has
|
|
67
|
+
no preview.
|
|
68
|
+
|
|
57
69
|
### The spacing scale is content, not code
|
|
58
70
|
|
|
59
71
|
Editors own it at **Layout → Spacing Scale**: a list of steps, each with a name
|
|
@@ -10,13 +10,14 @@ export type UnlockResult =
|
|
|
10
10
|
| { ok: true; payload: GatedContentPayload }
|
|
11
11
|
| { ok: false; error: string }
|
|
12
12
|
|
|
13
|
-
type UnlockInput = {
|
|
13
|
+
type UnlockInput = { pageId: string; blockKey: string; email: string }
|
|
14
14
|
|
|
15
15
|
export async function unlockGatedContent({
|
|
16
|
-
|
|
16
|
+
pageId,
|
|
17
|
+
blockKey,
|
|
17
18
|
email,
|
|
18
19
|
}: UnlockInput): Promise<UnlockResult> {
|
|
19
|
-
if (!
|
|
20
|
+
if (!(pageId && blockKey)) return { ok: false, error: "Missing content id." }
|
|
20
21
|
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
|
21
22
|
return { ok: false, error: "Enter a valid email address." }
|
|
22
23
|
}
|
|
@@ -27,7 +28,7 @@ export async function unlockGatedContent({
|
|
|
27
28
|
|
|
28
29
|
const { data } = await fetchSanity<GATED_CONTENT_QUERY_RESULT>({
|
|
29
30
|
query: GATED_CONTENT_QUERY,
|
|
30
|
-
params: {
|
|
31
|
+
params: { pageId, blockKey },
|
|
31
32
|
// stega must stay off: it injects invisible characters into strings, which
|
|
32
33
|
// would corrupt the asset URLs returned for download/playback.
|
|
33
34
|
stega: false,
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { Metadata } from "next"
|
|
2
|
+
import { notFound } from "next/navigation"
|
|
3
|
+
import { SpacingScale } from "@/components/layout/spacing-scale"
|
|
4
|
+
import {
|
|
5
|
+
COMPONENT_LIBRARY,
|
|
6
|
+
COMPONENT_PREVIEW_PARAM,
|
|
7
|
+
getLibraryEntry,
|
|
8
|
+
type LibraryEntry,
|
|
9
|
+
} from "@/components/page-builder/library"
|
|
10
|
+
import { Renderer } from "@/components/page-builder/renderer"
|
|
11
|
+
import { Stage } from "@/components/page-builder/stage"
|
|
12
|
+
import { isStudioEmbed } from "@/lib/utils/studio-embed"
|
|
13
|
+
|
|
14
|
+
export const metadata: Metadata = {
|
|
15
|
+
title: "Components",
|
|
16
|
+
robots: { index: false, follow: false, nocache: true },
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type Props = {
|
|
20
|
+
searchParams: Promise<Record<string, string | string[] | undefined>>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const firstValue = (value: string | string[] | undefined) =>
|
|
24
|
+
Array.isArray(value) ? value[0] : value
|
|
25
|
+
|
|
26
|
+
const PREVIEW_PAGE_ID = "component-preview"
|
|
27
|
+
|
|
28
|
+
const EntryHeader = ({ entry }: { entry: LibraryEntry }) => (
|
|
29
|
+
<header className="sticky top-0 z-10 flex items-baseline gap-3 border-neutral-200 border-b bg-white/85 px-6 py-3 backdrop-blur">
|
|
30
|
+
<h2 className="font-mono text-neutral-900 text-xs uppercase">
|
|
31
|
+
{entry.title}
|
|
32
|
+
</h2>
|
|
33
|
+
<p className="text-neutral-500 text-xs">{entry.notes}</p>
|
|
34
|
+
</header>
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
const PreviewStage = ({ entry }: { entry: LibraryEntry }) => (
|
|
38
|
+
<Stage background={entry.background}>
|
|
39
|
+
<Renderer
|
|
40
|
+
blocks={[entry.block]}
|
|
41
|
+
pageId={PREVIEW_PAGE_ID}
|
|
42
|
+
pinnedCategory={null}
|
|
43
|
+
searchParams={undefined}
|
|
44
|
+
/>
|
|
45
|
+
</Stage>
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
const Page = async ({ searchParams }: Props) => {
|
|
49
|
+
const [params, embedded] = await Promise.all([searchParams, isStudioEmbed()])
|
|
50
|
+
|
|
51
|
+
if (!embedded) notFound()
|
|
52
|
+
|
|
53
|
+
const requested = firstValue(params[COMPONENT_PREVIEW_PARAM])
|
|
54
|
+
const entry = getLibraryEntry(requested)
|
|
55
|
+
|
|
56
|
+
if (requested && !entry) notFound()
|
|
57
|
+
|
|
58
|
+
// Outside the `(content)` group, so `<SpacingScale />` is on this route.
|
|
59
|
+
if (entry) {
|
|
60
|
+
return (
|
|
61
|
+
<main className="flex flex-col">
|
|
62
|
+
<SpacingScale />
|
|
63
|
+
<PreviewStage entry={entry} />
|
|
64
|
+
</main>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<main className="flex flex-col">
|
|
70
|
+
<SpacingScale />
|
|
71
|
+
{COMPONENT_LIBRARY.map((item) => (
|
|
72
|
+
<section key={item.documentType}>
|
|
73
|
+
<EntryHeader entry={item} />
|
|
74
|
+
<PreviewStage entry={item} />
|
|
75
|
+
</section>
|
|
76
|
+
))}
|
|
77
|
+
</main>
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export default Page
|
package/src/templates/next-pagebuilder/components/page-builder/components/gated-content/index.tsx
CHANGED
|
@@ -13,14 +13,15 @@ type GatedContentBlock = Extract<PageBuilderBlock, { _type: "gatedContent" }>
|
|
|
13
13
|
|
|
14
14
|
type GatedContentProps = {
|
|
15
15
|
block: GatedContentBlock
|
|
16
|
+
pageId: string
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
type GateState = "locked" | "loading" | "unlocked"
|
|
19
20
|
|
|
20
21
|
const STORAGE_PREFIX = "gated-content:"
|
|
21
22
|
|
|
22
|
-
export function GatedContent({ block }: GatedContentProps) {
|
|
23
|
-
const storageKey = `${STORAGE_PREFIX}${block.
|
|
23
|
+
export function GatedContent({ block, pageId }: GatedContentProps) {
|
|
24
|
+
const storageKey = `${STORAGE_PREFIX}${pageId}:${block._key}`
|
|
24
25
|
const [state, setState] = useState<GateState>("locked")
|
|
25
26
|
const [payload, setPayload] = useState<GatedContentPayload | null>(null)
|
|
26
27
|
const [error, setError] = useState<string | null>(null)
|
|
@@ -33,7 +34,11 @@ export function GatedContent({ block }: GatedContentProps) {
|
|
|
33
34
|
setState("loading")
|
|
34
35
|
setError(null)
|
|
35
36
|
|
|
36
|
-
const result = await unlockGatedContent({
|
|
37
|
+
const result = await unlockGatedContent({
|
|
38
|
+
pageId,
|
|
39
|
+
blockKey: block._key,
|
|
40
|
+
email,
|
|
41
|
+
})
|
|
37
42
|
|
|
38
43
|
if (!result.ok) {
|
|
39
44
|
setError(result.error)
|