bsmnt 0.12.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.
Files changed (188) hide show
  1. package/dist/application/add-integration/index.d.ts.map +1 -1
  2. package/dist/application/add-integration/index.js +23 -0
  3. package/dist/application/add-integration/index.js.map +1 -1
  4. package/dist/domain/consent.d.ts +17 -0
  5. package/dist/domain/consent.d.ts.map +1 -0
  6. package/dist/domain/consent.js +13 -0
  7. package/dist/domain/consent.js.map +1 -0
  8. package/dist/domain/integration.d.ts +19 -2
  9. package/dist/domain/integration.d.ts.map +1 -1
  10. package/dist/domain/integration.js +10 -1
  11. package/dist/domain/integration.js.map +1 -1
  12. package/dist/domain/jobs.d.ts +17 -0
  13. package/dist/domain/jobs.d.ts.map +1 -0
  14. package/dist/domain/jobs.js +13 -0
  15. package/dist/domain/jobs.js.map +1 -0
  16. package/dist/domain/skills.d.ts +23 -2
  17. package/dist/domain/skills.d.ts.map +1 -1
  18. package/dist/domain/skills.js +23 -2
  19. package/dist/domain/skills.js.map +1 -1
  20. package/dist/domain/templates.d.ts +7 -1
  21. package/dist/domain/templates.d.ts.map +1 -1
  22. package/dist/domain/templates.js +2 -0
  23. package/dist/domain/templates.js.map +1 -1
  24. package/dist/index.js +34 -9
  25. package/dist/index.js.map +1 -1
  26. package/dist/infrastructure/create/setup-agent.d.ts.map +1 -1
  27. package/dist/infrastructure/create/setup-agent.js +11 -4
  28. package/dist/infrastructure/create/setup-agent.js.map +1 -1
  29. package/dist/infrastructure/integrations/inject-integration.d.ts.map +1 -1
  30. package/dist/infrastructure/integrations/inject-integration.js +36 -2
  31. package/dist/infrastructure/integrations/inject-integration.js.map +1 -1
  32. package/dist/modules/features/consent/ketch/config.d.ts +18 -0
  33. package/dist/modules/features/consent/ketch/config.d.ts.map +1 -0
  34. package/dist/modules/features/consent/ketch/config.js +24 -0
  35. package/dist/modules/features/consent/ketch/config.js.map +1 -0
  36. package/dist/modules/features/consent/ketch/mergers/biome.d.ts +16 -0
  37. package/dist/modules/features/consent/ketch/mergers/biome.d.ts.map +1 -0
  38. package/dist/modules/features/consent/ketch/mergers/biome.js +31 -0
  39. package/dist/modules/features/consent/ketch/mergers/biome.js.map +1 -0
  40. package/dist/modules/features/consent/ketch/mergers/check-integration.d.ts +13 -0
  41. package/dist/modules/features/consent/ketch/mergers/check-integration.d.ts.map +1 -0
  42. package/dist/modules/features/consent/ketch/mergers/check-integration.js +64 -0
  43. package/dist/modules/features/consent/ketch/mergers/check-integration.js.map +1 -0
  44. package/dist/modules/features/consent/ketch/mergers/env-example.d.ts +12 -0
  45. package/dist/modules/features/consent/ketch/mergers/env-example.d.ts.map +1 -0
  46. package/dist/modules/features/consent/ketch/mergers/env-example.js +28 -0
  47. package/dist/modules/features/consent/ketch/mergers/env-example.js.map +1 -0
  48. package/dist/modules/features/consent/ketch/mergers/footer.d.ts +17 -0
  49. package/dist/modules/features/consent/ketch/mergers/footer.d.ts.map +1 -0
  50. package/dist/modules/features/consent/ketch/mergers/footer.js +40 -0
  51. package/dist/modules/features/consent/ketch/mergers/footer.js.map +1 -0
  52. package/dist/modules/features/consent/ketch/mergers/knip.d.ts +18 -0
  53. package/dist/modules/features/consent/ketch/mergers/knip.d.ts.map +1 -0
  54. package/dist/modules/features/consent/ketch/mergers/knip.js +37 -0
  55. package/dist/modules/features/consent/ketch/mergers/knip.js.map +1 -0
  56. package/dist/modules/features/consent/ketch/mergers/layout.d.ts +25 -0
  57. package/dist/modules/features/consent/ketch/mergers/layout.d.ts.map +1 -0
  58. package/dist/modules/features/consent/ketch/mergers/layout.js +55 -0
  59. package/dist/modules/features/consent/ketch/mergers/layout.js.map +1 -0
  60. package/dist/modules/features/jobs/ashby/config.d.ts +19 -0
  61. package/dist/modules/features/jobs/ashby/config.d.ts.map +1 -0
  62. package/dist/modules/features/jobs/ashby/config.js +29 -0
  63. package/dist/modules/features/jobs/ashby/config.js.map +1 -0
  64. package/dist/modules/features/jobs/ashby/mergers/check-integration.d.ts +14 -0
  65. package/dist/modules/features/jobs/ashby/mergers/check-integration.d.ts.map +1 -0
  66. package/dist/modules/features/jobs/ashby/mergers/check-integration.js +78 -0
  67. package/dist/modules/features/jobs/ashby/mergers/check-integration.js.map +1 -0
  68. package/dist/modules/features/jobs/ashby/mergers/env-example.d.ts +12 -0
  69. package/dist/modules/features/jobs/ashby/mergers/env-example.d.ts.map +1 -0
  70. package/dist/modules/features/jobs/ashby/mergers/env-example.js +27 -0
  71. package/dist/modules/features/jobs/ashby/mergers/env-example.js.map +1 -0
  72. package/dist/modules/features/jobs/ashby/mergers/knip.d.ts +18 -0
  73. package/dist/modules/features/jobs/ashby/mergers/knip.d.ts.map +1 -0
  74. package/dist/modules/features/jobs/ashby/mergers/knip.js +41 -0
  75. package/dist/modules/features/jobs/ashby/mergers/knip.js.map +1 -0
  76. package/dist/paths.d.ts +12 -0
  77. package/dist/paths.d.ts.map +1 -1
  78. package/dist/paths.js +12 -0
  79. package/dist/paths.js.map +1 -1
  80. package/package.json +3 -1
  81. package/src/agent-skills/ashby-pagebuilder-block/SKILL.md +277 -0
  82. package/src/agent-skills/hubspot-pagebuilder-block/SKILL.md +58 -25
  83. package/src/agent-skills/ketch-consent-check/SKILL.md +197 -0
  84. package/src/agent-skills/ketch-consent-setup/SKILL.md +326 -0
  85. package/src/agent-skills/ketch-consent-setup/references/ui-customization.md +329 -0
  86. package/src/agent-skills/page-builder-block/SKILL.md +150 -0
  87. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/anchor-id-input.tsx +17 -0
  88. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/presentation-input.tsx +146 -0
  89. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/spacing-steps-input.tsx +84 -0
  90. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/fetchers/layout.ts +6 -3
  91. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/icons.ts +8 -0
  92. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/page-builder-config.ts +1 -1
  93. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/presentation.ts +47 -15
  94. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/queries.ts +12 -0
  95. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/reusable/blog-content.ts +5 -5
  96. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/reusable/description.ts +3 -3
  97. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/reusable/gated-content.ts +3 -3
  98. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/reusable/hero.ts +3 -3
  99. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/reusable/rich-text-block.ts +3 -3
  100. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/singleton/content-collection.ts +3 -3
  101. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/index.ts +8 -0
  102. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/layout/spacing-settings.ts +120 -0
  103. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/anchor.ts +75 -0
  104. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/block.ts +46 -0
  105. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/presentation.ts +44 -0
  106. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/spacing.ts +164 -0
  107. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/singletons.ts +7 -0
  108. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/structure.ts +4 -4
  109. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/anchor-id-input.tsx +17 -0
  110. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/presentation-input.tsx +146 -0
  111. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/spacing-steps-input.tsx +84 -0
  112. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/fetchers/layout.ts +4 -2
  113. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/icons.ts +8 -0
  114. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/page-builder-config.ts +1 -1
  115. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/presentation.ts +47 -15
  116. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/queries.ts +12 -0
  117. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/blog-content.ts +5 -5
  118. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/description.ts +3 -3
  119. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/gated-content.ts +3 -3
  120. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/hero.ts +3 -3
  121. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/rich-text-block.ts +3 -3
  122. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/singleton/content-collection.ts +3 -3
  123. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/index.ts +8 -0
  124. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/layout/spacing-settings.ts +120 -0
  125. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/anchor.ts +75 -0
  126. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/block.ts +46 -0
  127. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/presentation.ts +44 -0
  128. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/spacing.ts +164 -0
  129. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/singletons.ts +7 -0
  130. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/structure.ts +4 -4
  131. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/tags.ts +2 -0
  132. package/src/modules/features/consent/ketch/files/lib/integrations/check-integration.ts +25 -0
  133. package/src/modules/features/consent/ketch/files/lib/integrations/ketch/README.md +70 -0
  134. package/src/modules/features/consent/ketch/files/lib/integrations/ketch/categories.ts +20 -0
  135. package/src/modules/features/consent/ketch/files/lib/integrations/ketch/consent-store.ts +107 -0
  136. package/src/modules/features/consent/ketch/files/lib/integrations/ketch/env.ts +20 -0
  137. package/src/modules/features/consent/ketch/files/lib/integrations/ketch/ketch-experience.css +136 -0
  138. package/src/modules/features/consent/ketch/files/lib/integrations/ketch/preferences-button.tsx +51 -0
  139. package/src/modules/features/consent/ketch/files/lib/integrations/ketch/script.tsx +8 -0
  140. package/src/modules/features/consent/ketch/files/lib/integrations/ketch/types.ts +15 -0
  141. package/src/modules/features/consent/ketch/files/lib/integrations/ketch/use-consent.ts +26 -0
  142. package/src/modules/features/jobs/ashby/files/_sanity-adapter/job-board-block.ts +53 -0
  143. package/src/modules/features/jobs/ashby/files/lib/integrations/ashby/README.md +134 -0
  144. package/src/modules/features/jobs/ashby/files/lib/integrations/ashby/env.ts +3 -0
  145. package/src/modules/features/jobs/ashby/files/lib/integrations/ashby/index.ts +40 -0
  146. package/src/modules/features/jobs/ashby/files/lib/integrations/ashby/normalize.ts +124 -0
  147. package/src/modules/features/jobs/ashby/files/lib/integrations/ashby/types.ts +20 -0
  148. package/src/modules/features/jobs/ashby/files/lib/integrations/check-integration.ts +25 -0
  149. package/src/templates/next-default/lib/utils/README.md +1 -0
  150. package/src/templates/next-default/lib/utils/image-sizes.ts +64 -25
  151. package/src/templates/next-experiments/lib/utils/README.md +1 -0
  152. package/src/templates/next-experiments/lib/utils/image-sizes.ts +64 -25
  153. package/src/templates/next-pagebuilder/AGENTS.md +129 -0
  154. package/src/templates/next-pagebuilder/CLAUDE.md +3 -0
  155. package/src/templates/next-pagebuilder/README.md +70 -0
  156. package/src/templates/next-pagebuilder/app/(content)/layout.tsx +2 -0
  157. package/src/templates/next-pagebuilder/components/layout/spacing-scale/index.tsx +18 -0
  158. package/src/templates/next-pagebuilder/components/page-builder/components/article-content/index.tsx +1 -1
  159. package/src/templates/next-pagebuilder/components/page-builder/components/content-collection/index.tsx +2 -2
  160. package/src/templates/next-pagebuilder/components/page-builder/components/description.tsx +3 -7
  161. package/src/templates/next-pagebuilder/components/page-builder/components/gated-content/index.tsx +2 -2
  162. package/src/templates/next-pagebuilder/components/page-builder/components/hero.tsx +3 -7
  163. package/src/templates/next-pagebuilder/components/page-builder/components/rich-text-block.tsx +3 -5
  164. package/src/templates/next-pagebuilder/components/page-builder/registry.tsx +70 -0
  165. package/src/templates/next-pagebuilder/components/page-builder/renderer.tsx +33 -49
  166. package/src/templates/next-pagebuilder/components/page-builder/section.tsx +35 -0
  167. package/src/templates/next-pagebuilder/components/page-document/index.tsx +2 -2
  168. package/src/templates/next-pagebuilder/lib/integrations/sanity/sanity.types.ts +92 -4
  169. package/src/templates/next-pagebuilder/lib/utils/image-sizes.ts +64 -25
  170. package/src/templates/next-pagebuilder-cache/AGENTS.md +136 -0
  171. package/src/templates/next-pagebuilder-cache/CLAUDE.md +3 -0
  172. package/src/templates/next-pagebuilder-cache/README.md +75 -0
  173. package/src/templates/next-pagebuilder-cache/app/(content)/layout.tsx +2 -0
  174. package/src/templates/next-pagebuilder-cache/components/layout/spacing-scale/index.tsx +46 -0
  175. package/src/templates/next-pagebuilder-cache/components/page-builder/components/article-content/index.tsx +1 -1
  176. package/src/templates/next-pagebuilder-cache/components/page-builder/components/content-collection/index.tsx +2 -2
  177. package/src/templates/next-pagebuilder-cache/components/page-builder/components/description.tsx +3 -7
  178. package/src/templates/next-pagebuilder-cache/components/page-builder/components/gated-content/index.tsx +2 -2
  179. package/src/templates/next-pagebuilder-cache/components/page-builder/components/hero.tsx +3 -7
  180. package/src/templates/next-pagebuilder-cache/components/page-builder/components/rich-text-block.tsx +3 -5
  181. package/src/templates/next-pagebuilder-cache/components/page-builder/registry.tsx +70 -0
  182. package/src/templates/next-pagebuilder-cache/components/page-builder/renderer.tsx +33 -49
  183. package/src/templates/next-pagebuilder-cache/components/page-builder/section.tsx +35 -0
  184. package/src/templates/next-pagebuilder-cache/components/page-document/index.tsx +2 -2
  185. package/src/templates/next-pagebuilder-cache/lib/integrations/sanity/sanity.types.ts +92 -4
  186. package/src/templates/next-pagebuilder-cache/lib/utils/image-sizes.ts +64 -25
  187. package/src/templates/next-webgl/lib/utils/README.md +1 -0
  188. package/src/templates/next-webgl/lib/utils/image-sizes.ts +64 -25
@@ -0,0 +1,146 @@
1
+ import { Box, Card, Flex, Select, Stack, Text } from "@sanity/ui";
2
+ import { useEffect, useState } from "react";
3
+ import { type ObjectInputProps, set, setIfMissing, useClient } from "sanity";
4
+ import { apiVersion } from "@/lib/integrations/sanity/env";
5
+ import {
6
+ paddingBottomIcon,
7
+ paddingTopIcon,
8
+ type SanityStudioIcon,
9
+ } from "@/lib/integrations/sanity/icons";
10
+ import {
11
+ getDefaultSpacingStep,
12
+ resolveSpacingSteps,
13
+ type SpacingStep,
14
+ type SpacingStepSource,
15
+ spacingStepOptions,
16
+ } from "@/lib/integrations/sanity/schemas/shared/spacing";
17
+
18
+ const SPACING_STEPS_QUERY = `coalesce(
19
+ *[_id == "drafts.spacingSettings"][0],
20
+ *[_id == "spacingSettings"][0]
21
+ ).steps[]{ _key, name, mobile, tablet, desktop, isDefault }`;
22
+
23
+ type PaddingField = "paddingTop" | "paddingBottom";
24
+
25
+ type PresentationValue = Partial<Record<PaddingField, string>>;
26
+
27
+ function useSpacingSteps() {
28
+ const client = useClient({ apiVersion });
29
+ const [steps, setSteps] = useState<SpacingStep[]>(() =>
30
+ resolveSpacingSteps(null),
31
+ );
32
+
33
+ useEffect(() => {
34
+ let active = true;
35
+ client
36
+ .fetch<SpacingStepSource[] | null>(SPACING_STEPS_QUERY)
37
+ .then((rows) => {
38
+ if (active) setSteps(resolveSpacingSteps(rows));
39
+ })
40
+ .catch(() => setSteps(resolveSpacingSteps(null)));
41
+ return () => {
42
+ active = false;
43
+ };
44
+ }, [client]);
45
+
46
+ return steps;
47
+ }
48
+
49
+ type SpacingRowProps = {
50
+ name: PaddingField;
51
+ label: string;
52
+ icon: SanityStudioIcon;
53
+ options: Array<{ value: string; title: string }>;
54
+ value: string;
55
+ onSelect: (name: PaddingField, next: string) => void;
56
+ };
57
+
58
+ function SpacingRow({
59
+ name,
60
+ label,
61
+ icon,
62
+ options,
63
+ value,
64
+ onSelect,
65
+ }: SpacingRowProps) {
66
+ const Icon = icon;
67
+
68
+ return (
69
+ <Flex align="center" gap={2}>
70
+ <Card
71
+ padding={2}
72
+ radius={2}
73
+ tone="transparent"
74
+ border
75
+ title={label}
76
+ aria-hidden
77
+ >
78
+ <Text size={2}>
79
+ <Icon />
80
+ </Text>
81
+ </Card>
82
+ <Box flex={1}>
83
+ <Select
84
+ aria-label={label}
85
+ fontSize={1}
86
+ padding={3}
87
+ radius={2}
88
+ value={value}
89
+ onChange={(event) => onSelect(name, event.currentTarget.value)}
90
+ >
91
+ {options.map((option) => (
92
+ <option key={option.value} value={option.value}>
93
+ {option.title}
94
+ </option>
95
+ ))}
96
+ </Select>
97
+ </Box>
98
+ </Flex>
99
+ );
100
+ }
101
+
102
+ export function PresentationInput(props: ObjectInputProps) {
103
+ const value = props.value as PresentationValue | undefined;
104
+ const steps = useSpacingSteps();
105
+ const options = spacingStepOptions(steps);
106
+ const fallback = getDefaultSpacingStep(steps).key;
107
+
108
+ const resolve = (key: string | undefined) =>
109
+ steps.some((step) => step.key === key) ? (key as string) : fallback;
110
+
111
+ const handleSelect = (name: PaddingField, next: string) => {
112
+ props.onChange([setIfMissing({}), set(next, [name])]);
113
+ };
114
+
115
+ return (
116
+ <Stack space={3}>
117
+ <Flex gap={3} wrap="wrap">
118
+ <Box flex={1} style={{ minWidth: 200 }}>
119
+ <SpacingRow
120
+ name="paddingTop"
121
+ label="Padding top"
122
+ icon={paddingTopIcon}
123
+ options={options}
124
+ value={resolve(value?.paddingTop)}
125
+ onSelect={handleSelect}
126
+ />
127
+ </Box>
128
+ <Box flex={1} style={{ minWidth: 200 }}>
129
+ <SpacingRow
130
+ name="paddingBottom"
131
+ label="Padding bottom"
132
+ icon={paddingBottomIcon}
133
+ options={options}
134
+ value={resolve(value?.paddingBottom)}
135
+ onSelect={handleSelect}
136
+ />
137
+ </Box>
138
+ </Flex>
139
+ <Text size={1} muted>
140
+ Spacing above and below this block, from the shared scale (edit the steps
141
+ in Layout → Spacing Scale). Untouched blocks follow whichever step is
142
+ marked as the default.
143
+ </Text>
144
+ </Stack>
145
+ );
146
+ }
@@ -0,0 +1,84 @@
1
+ import { Button, Card, Stack, Text } from "@sanity/ui";
2
+ import { useEffect, useRef } from "react";
3
+ import { type ArrayOfObjectsInputProps, set } from "sanity";
4
+ import { defaultSpacingSteps } from "@/lib/integrations/sanity/schemas/shared/spacing";
5
+
6
+ type StepValue = { _key?: string; isDefault?: boolean };
7
+
8
+ const defaultKeysOf = (value: StepValue[] | undefined) =>
9
+ (value ?? [])
10
+ .filter((step) => step?.isDefault === true)
11
+ .flatMap((step) => (step._key ? [step._key] : []));
12
+
13
+ export function SpacingStepsInput(props: ArrayOfObjectsInputProps) {
14
+ const value = props.value as StepValue[] | undefined;
15
+ const { onChange } = props;
16
+
17
+ const defaultKeys = defaultKeysOf(value);
18
+ const defaultKeysId = defaultKeys.join(",");
19
+ const lastSingleDefault = useRef<string | undefined>(defaultKeys[0]);
20
+
21
+ useEffect(() => {
22
+ const keys = defaultKeysId ? defaultKeysId.split(",") : [];
23
+
24
+ if (keys.length < 2) {
25
+ lastSingleDefault.current = keys[0];
26
+ return;
27
+ }
28
+
29
+ const keep = keys.find((key) => key !== lastSingleDefault.current) ?? keys[0];
30
+ lastSingleDefault.current = keep;
31
+
32
+ onChange(
33
+ keys
34
+ .filter((key) => key !== keep)
35
+ .map((key) => set(false, [{ _key: key }, "isDefault"])),
36
+ );
37
+ }, [defaultKeysId, onChange]);
38
+
39
+ const seed = () => {
40
+ onChange(
41
+ set(
42
+ defaultSpacingSteps.map((step) => ({
43
+ _key: step.key,
44
+ _type: "spacingStep",
45
+ name: step.name,
46
+ mobile: step.mobile,
47
+ tablet: step.tablet,
48
+ desktop: step.desktop,
49
+ isDefault: step.isDefault,
50
+ })),
51
+ ),
52
+ );
53
+ };
54
+
55
+ return (
56
+ <Stack space={3}>
57
+ {props.renderDefault(props)}
58
+ {value?.length ? null : (
59
+ <Card padding={3} radius={2} border tone="primary">
60
+ <Stack space={3}>
61
+ <Text size={1}>
62
+ No steps yet, so blocks fall back to the built-in scale:{" "}
63
+ {defaultSpacingSteps
64
+ .map(
65
+ (step) =>
66
+ `${step.name} ${step.desktop}px${
67
+ step.isDefault ? " (default)" : ""
68
+ }`,
69
+ )
70
+ .join(" · ")}
71
+ .
72
+ </Text>
73
+ <Button
74
+ text="Add the built-in steps"
75
+ tone="primary"
76
+ mode="ghost"
77
+ onClick={seed}
78
+ />
79
+ </Stack>
80
+ </Card>
81
+ )}
82
+ </Stack>
83
+ );
84
+ }
@@ -12,16 +12,18 @@ export type NavbarData = NonNullable<LayoutData["navbar"]>
12
12
  export type FooterData = NonNullable<LayoutData["footer"]>
13
13
  export type CompanyData = NonNullable<LayoutData["companyData"]>
14
14
  export type StructuredData = NonNullable<LayoutData["structuredData"]>
15
+ export type SpacingSettings = NonNullable<LayoutData["spacingSettings"]>
15
16
 
16
17
  const EMPTY_LAYOUT: LayoutData = {
17
18
  navbar: null,
18
19
  footer: null,
19
20
  companyData: null,
20
21
  structuredData: null,
22
+ spacingSettings: null,
21
23
  }
22
24
 
23
- // One entry for all four layout singletons: header, footer and json-ld call this
24
- // with the same options, so they share one fetch instead of four.
25
+ // One entry for every layout singleton: header, footer, json-ld and the spacing
26
+ // scale call this with the same options, so they share one fetch.
25
27
  //
26
28
  // Don't cap the empty branch — this renders in every route's shell, and a nested
27
29
  // lifetime propagates upwards as a min().
@@ -2,6 +2,9 @@ import type { Icon, IconProps } from "@phosphor-icons/react";
2
2
  import {
3
3
  AppWindowIcon,
4
4
  ArrowBendUpRightIcon,
5
+ ArrowLineDownIcon,
6
+ ArrowLineUpIcon,
7
+ ArrowsOutLineVerticalIcon,
5
8
  ArticleNyTimesIcon,
6
9
  BracketsCurlyIcon,
7
10
  BriefcaseIcon,
@@ -75,6 +78,11 @@ export const richTextIcon = asDuotoneIcon(TextTIcon);
75
78
  export const contentIcon = asDuotoneIcon(ArticleNyTimesIcon);
76
79
  export const gatedContentIcon = asDuotoneIcon(LockKeyIcon);
77
80
 
81
+ export const spacingSettingsIcon = asDuotoneIcon(ArrowsOutLineVerticalIcon);
82
+
83
+ export const paddingTopIcon = asDuotoneIcon(ArrowLineUpIcon);
84
+ export const paddingBottomIcon = asDuotoneIcon(ArrowLineDownIcon);
85
+
78
86
  export const pageIcon = asDuotoneIcon(FileTextIcon);
79
87
  export const pagesFolderIcon = asDuotoneIcon(BrowsersIcon);
80
88
  export const pageFolderIcon = asDuotoneIcon(FolderOpenIcon);
@@ -88,7 +88,7 @@ const reusablePageBuilderReferenceMembers: PageBuilderReferenceMember[] = [
88
88
  {
89
89
  documentType: "blogContent",
90
90
  name: "blogContentReference",
91
- title: "Blog Content",
91
+ title: "Blog Posts",
92
92
  pageTypes: ["blogEntry"],
93
93
  category: "content",
94
94
  },
@@ -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 CONTENT_TYPE_PREFIXES: Record<string, string> = {
47
- blogContent: "blog",
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
- const prefix = CONTENT_TYPE_PREFIXES[params.type];
101
- if (!prefix) return null;
139
+ if (!PAGE_BUILDER_DOCUMENT_TYPES.has(params.type)) return null;
102
140
 
103
- return resolveSlugLocation<{ title?: string; slug?: string }>(
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: {
@@ -96,6 +96,8 @@ const pageBuilderProjection = `
96
96
  _type == "gatedContent" => {
97
97
  _id,
98
98
  _type,
99
+ anchorId,
100
+ presentation,
99
101
  internalTitle,
100
102
  teaserHeading,
101
103
  teaserBody[]{
@@ -533,6 +535,16 @@ export const LAYOUT_QUERY = defineQuery(`
533
535
  },
534
536
  "structuredData": *[_type == "structuredData"][0]{
535
537
  ${structuredDataProjection}
538
+ },
539
+ "spacingSettings": *[_type == "spacingSettings"][0]{
540
+ steps[]{
541
+ _key,
542
+ name,
543
+ mobile,
544
+ tablet,
545
+ desktop,
546
+ isDefault
547
+ }
536
548
  }
537
549
  }
538
550
  `);
@@ -1,10 +1,10 @@
1
- import { defineField, defineType } from "sanity";
1
+ import { defineField } from "sanity";
2
2
  import { contentIcon } from "@/lib/integrations/sanity/icons";
3
+ import { defineBlock } from "@/lib/integrations/sanity/schemas/shared/block";
3
4
 
4
- export const blogContent = defineType({
5
+ export const blogContent = defineBlock({
5
6
  name: "blogContent",
6
- title: "Blog Content",
7
- type: "document",
7
+ title: "Blog Posts",
8
8
  icon: contentIcon,
9
9
  fields: [
10
10
  defineField({
@@ -80,7 +80,7 @@ export const blogContent = defineType({
80
80
  },
81
81
  prepare({ title, date, media }) {
82
82
  return {
83
- title: title || "Blog Content",
83
+ title: title || "Blog Posts",
84
84
  subtitle: date || "No date set",
85
85
  media,
86
86
  };
@@ -1,10 +1,10 @@
1
- import { defineField, defineType } from "sanity";
1
+ import { defineField } from "sanity";
2
2
  import { descriptionIcon } from "@/lib/integrations/sanity/icons";
3
+ import { defineBlock } from "@/lib/integrations/sanity/schemas/shared/block";
3
4
 
4
- export const description = defineType({
5
+ export const description = defineBlock({
5
6
  name: "description",
6
7
  title: "Description",
7
- type: "document",
8
8
  icon: descriptionIcon,
9
9
  fields: [
10
10
  defineField({
@@ -1,5 +1,6 @@
1
- import { defineField, defineType } from "sanity";
1
+ import { defineField } from "sanity";
2
2
  import { gatedContentIcon } from "@/lib/integrations/sanity/icons";
3
+ import { defineBlock } from "@/lib/integrations/sanity/schemas/shared/block";
3
4
 
4
5
  type GatedContentParent = { kind?: string };
5
6
 
@@ -19,10 +20,9 @@ const requiredForKind =
19
20
  return isEmpty ? message : true;
20
21
  };
21
22
 
22
- export const gatedContent = defineType({
23
+ export const gatedContent = defineBlock({
23
24
  name: "gatedContent",
24
25
  title: "Gated Content",
25
- type: "document",
26
26
  icon: gatedContentIcon,
27
27
  description:
28
28
  "Content hidden behind a lead-capture gate. Everyone sees the teaser; the payload is withheld from the page until the visitor submits the form.",
@@ -1,10 +1,10 @@
1
- import { defineField, defineType } from "sanity";
1
+ import { defineField } from "sanity";
2
2
  import { heroIcon } from "@/lib/integrations/sanity/icons";
3
+ import { defineBlock } from "@/lib/integrations/sanity/schemas/shared/block";
3
4
 
4
- export const hero = defineType({
5
+ export const hero = defineBlock({
5
6
  name: "hero",
6
7
  title: "Heading",
7
- type: "document",
8
8
  icon: heroIcon,
9
9
  fields: [
10
10
  defineField({
@@ -1,10 +1,10 @@
1
- import { defineField, defineType } from "sanity";
1
+ import { defineField } from "sanity";
2
2
  import { richTextIcon } from "@/lib/integrations/sanity/icons";
3
+ import { defineBlock } from "@/lib/integrations/sanity/schemas/shared/block";
3
4
 
4
- export const richTextBlock = defineType({
5
+ export const richTextBlock = defineBlock({
5
6
  name: "richTextBlock",
6
7
  title: "Rich Text",
7
- type: "document",
8
8
  icon: richTextIcon,
9
9
  fields: [
10
10
  defineField({
@@ -1,6 +1,7 @@
1
- import { defineField, defineType } from "sanity";
1
+ import { defineField } from "sanity";
2
2
  import type { SanityStudioIcon } from "@/lib/integrations/sanity/icons";
3
3
  import { pageListIcon } from "@/lib/integrations/sanity/icons";
4
+ import { defineBlock } from "@/lib/integrations/sanity/schemas/shared/block";
4
5
 
5
6
  function createContentCollection(config: {
6
7
  name: string;
@@ -8,10 +9,9 @@ function createContentCollection(config: {
8
9
  icon: SanityStudioIcon;
9
10
  label: string;
10
11
  }) {
11
- return defineType({
12
+ return defineBlock({
12
13
  name: config.name,
13
14
  title: config.title,
14
- type: "document",
15
15
  icon: config.icon,
16
16
  description: `This component contains all the ${config.label}`,
17
17
  fields: [
@@ -23,6 +23,7 @@ import { companyData } from "./layout/company-data";
23
23
  import { footer } from "./layout/footer";
24
24
  import { navbar } from "./layout/navbar";
25
25
  import { redirect } from "./layout/redirect";
26
+ import { spacingSettings, spacingStep } from "./layout/spacing-settings";
26
27
  import { structuredData } from "./layout/structured-data";
27
28
  import { externalLink, link, pageReference } from "./shared/link";
28
29
  import { metadata } from "./shared/metadata";
@@ -35,6 +36,7 @@ import {
35
36
  import { page } from "./shared/page";
36
37
  import { pageBuilder } from "./shared/page-builder";
37
38
  import { pageFolder } from "./shared/page-folder";
39
+ import { presentation } from "./shared/presentation";
38
40
  import { richText } from "./shared/richText";
39
41
  import {
40
42
  customJsonLd,
@@ -74,9 +76,12 @@ export {
74
76
  pageBuilder,
75
77
  pageFolder,
76
78
  pageReference,
79
+ presentation,
77
80
  redirect,
78
81
  richText,
79
82
  softwareApplicationJsonLd,
83
+ spacingSettings,
84
+ spacingStep,
80
85
  structuredData,
81
86
  studioPreview,
82
87
  videoJsonLd,
@@ -91,6 +96,7 @@ export const schema: { types: SchemaTypeDefinition[] } = {
91
96
  link,
92
97
  metadata,
93
98
  pageBuilder,
99
+ presentation,
94
100
  richText,
95
101
 
96
102
  // Page-level JSON-LD blocks (selectable in metadata)
@@ -122,6 +128,8 @@ export const schema: { types: SchemaTypeDefinition[] } = {
122
128
  navbar,
123
129
  footer,
124
130
  structuredData,
131
+ spacingStep,
132
+ spacingSettings,
125
133
 
126
134
  // Layout collections (managed lists, not singletons)
127
135
  redirect,
@@ -0,0 +1,120 @@
1
+ import { defineArrayMember, defineField, defineType } from "sanity";
2
+ import { SpacingStepsInput } from "@/lib/integrations/sanity/components/spacing-steps-input";
3
+ import { spacingSettingsIcon } from "@/lib/integrations/sanity/icons";
4
+ import { defaultSpacingSteps } from "@/lib/integrations/sanity/schemas/shared/spacing";
5
+
6
+ const pxField = (
7
+ name: "mobile" | "tablet" | "desktop",
8
+ title: string,
9
+ description: string,
10
+ isRequired = false,
11
+ ) =>
12
+ defineField({
13
+ name,
14
+ title,
15
+ type: "number",
16
+ description,
17
+ validation: (Rule) => {
18
+ const range = Rule.min(0).max(400).integer();
19
+ return isRequired ? range.required() : range;
20
+ },
21
+ });
22
+
23
+ export const spacingStep = defineType({
24
+ name: "spacingStep",
25
+ title: "Spacing step",
26
+ type: "object",
27
+ fields: [
28
+ defineField({
29
+ name: "name",
30
+ title: "Name",
31
+ type: "string",
32
+ description: "Shown in the Presentation Mode dropdown on every block.",
33
+ validation: (Rule) => Rule.required().max(24),
34
+ }),
35
+ pxField("desktop", "Desktop", "From 1440px up. Required.", true),
36
+ pxField("tablet", "Tablet", "From 768px up. Empty reuses Desktop."),
37
+ pxField("mobile", "Mobile", "Below 768px. Empty reuses Desktop."),
38
+ defineField({
39
+ name: "isDefault",
40
+ title: "Use as default",
41
+ type: "boolean",
42
+ description:
43
+ "Blocks that never touch Presentation Mode use this step. Exactly one step can be the default.",
44
+ initialValue: false,
45
+ }),
46
+ ],
47
+ preview: {
48
+ select: {
49
+ name: "name",
50
+ mobile: "mobile",
51
+ tablet: "tablet",
52
+ desktop: "desktop",
53
+ isDefault: "isDefault",
54
+ },
55
+ prepare({ name, mobile, tablet, desktop, isDefault }) {
56
+ const px = [mobile ?? desktop, tablet ?? desktop, desktop]
57
+ .map((value) => (typeof value === "number" ? `${value}px` : "—"))
58
+ .join(" / ");
59
+
60
+ return {
61
+ title: `${name || "Untitled"}${isDefault ? " · default" : ""}`,
62
+ subtitle: `${px} (mobile / tablet / desktop)`,
63
+ };
64
+ },
65
+ },
66
+ });
67
+
68
+ export const spacingSettings = defineType({
69
+ name: "spacingSettings",
70
+ title: "Spacing Scale",
71
+ type: "document",
72
+ icon: spacingSettingsIcon,
73
+ fields: [
74
+ defineField({
75
+ name: "steps",
76
+ title: "Spacing steps",
77
+ description:
78
+ "The vertical spacing every block can pick from. Add as many as you need and mark one as the default. Left empty, the built-in scale is used.",
79
+ type: "array",
80
+ components: { input: SpacingStepsInput },
81
+ of: [defineArrayMember({ type: "spacingStep" })],
82
+ initialValue: defaultSpacingSteps.map((step) => ({
83
+ _key: step.key,
84
+ _type: "spacingStep",
85
+ name: step.name,
86
+ mobile: step.mobile,
87
+ tablet: step.tablet,
88
+ desktop: step.desktop,
89
+ isDefault: step.isDefault,
90
+ })),
91
+ validation: (Rule) =>
92
+ Rule.custom((steps: unknown) => {
93
+ if (!Array.isArray(steps) || steps.length === 0) return true;
94
+
95
+ const defaults = steps.filter(
96
+ (step) => (step as { isDefault?: boolean })?.isDefault === true,
97
+ );
98
+
99
+ if (defaults.length === 0) return "Mark one step as the default";
100
+ if (defaults.length > 1) return "Only one step can be the default";
101
+
102
+ const names = steps.map((step) =>
103
+ (step as { name?: string })?.name?.trim().toLowerCase(),
104
+ );
105
+
106
+ return new Set(names).size === names.length
107
+ ? true
108
+ : "Two steps share the same name";
109
+ }),
110
+ }),
111
+ ],
112
+ preview: {
113
+ prepare() {
114
+ return {
115
+ title: "Spacing Scale",
116
+ subtitle: "Section padding steps per breakpoint",
117
+ };
118
+ },
119
+ },
120
+ });