bsmnt 0.11.0 → 0.13.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.
Files changed (217) 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/files/components/ui/sanity-image/index.tsx +36 -4
  88. package/src/modules/features/cms/sanity/files/lib/integrations/sanity/components/rich-text.tsx +7 -1
  89. package/src/modules/features/cms/sanity-pagebuilder/files/components/sanity/rich-text.tsx +7 -1
  90. package/src/modules/features/cms/sanity-pagebuilder/files/components/ui/sanity-image/index.tsx +36 -4
  91. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/anchor-id-input.tsx +17 -0
  92. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/page-builder-input.tsx +26 -7
  93. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/page-builder-insert-menu.tsx +242 -0
  94. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/presentation-input.tsx +146 -0
  95. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/spacing-steps-input.tsx +84 -0
  96. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/use-studio-preview-urls.ts +62 -0
  97. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/fetchers/layout.ts +6 -3
  98. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/icons.ts +8 -0
  99. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/page-builder-config.ts +72 -34
  100. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/queries.ts +12 -0
  101. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/index.ts +1 -0
  102. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/reusable/blog-content.ts +3 -3
  103. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/reusable/description.ts +3 -3
  104. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/reusable/gated-content.ts +3 -3
  105. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/reusable/hero.ts +3 -3
  106. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/reusable/rich-text-block.ts +3 -3
  107. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/singleton/content-collection.ts +3 -3
  108. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/studio-preview.ts +37 -0
  109. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/index.ts +11 -0
  110. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/layout/spacing-settings.ts +120 -0
  111. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/anchor.ts +75 -0
  112. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/block.ts +46 -0
  113. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/presentation.ts +44 -0
  114. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/spacing.ts +164 -0
  115. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/singletons.ts +9 -0
  116. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/structure.ts +5 -0
  117. package/src/modules/features/cms/sanity-pagebuilder-cache/files/components/sanity/rich-text.tsx +7 -1
  118. package/src/modules/features/cms/sanity-pagebuilder-cache/files/components/ui/sanity-image/index.tsx +36 -4
  119. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/anchor-id-input.tsx +17 -0
  120. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/page-builder-input.tsx +26 -7
  121. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/page-builder-insert-menu.tsx +242 -0
  122. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/presentation-input.tsx +146 -0
  123. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/spacing-steps-input.tsx +84 -0
  124. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/use-studio-preview-urls.ts +62 -0
  125. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/fetchers/layout.ts +4 -2
  126. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/icons.ts +8 -0
  127. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/page-builder-config.ts +72 -34
  128. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/queries.ts +12 -0
  129. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/index.ts +1 -0
  130. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/blog-content.ts +3 -3
  131. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/description.ts +3 -3
  132. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/gated-content.ts +3 -3
  133. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/hero.ts +3 -3
  134. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/rich-text-block.ts +3 -3
  135. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/singleton/content-collection.ts +3 -3
  136. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/studio-preview.ts +37 -0
  137. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/index.ts +11 -0
  138. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/layout/spacing-settings.ts +120 -0
  139. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/anchor.ts +75 -0
  140. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/block.ts +46 -0
  141. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/presentation.ts +44 -0
  142. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/spacing.ts +164 -0
  143. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/singletons.ts +9 -0
  144. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/structure.ts +5 -0
  145. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/tags.ts +2 -0
  146. package/src/modules/features/consent/ketch/files/lib/integrations/check-integration.ts +25 -0
  147. package/src/modules/features/consent/ketch/files/lib/integrations/ketch/README.md +70 -0
  148. package/src/modules/features/consent/ketch/files/lib/integrations/ketch/categories.ts +20 -0
  149. package/src/modules/features/consent/ketch/files/lib/integrations/ketch/consent-store.ts +107 -0
  150. package/src/modules/features/consent/ketch/files/lib/integrations/ketch/env.ts +20 -0
  151. package/src/modules/features/consent/ketch/files/lib/integrations/ketch/ketch-experience.css +136 -0
  152. package/src/modules/features/consent/ketch/files/lib/integrations/ketch/preferences-button.tsx +51 -0
  153. package/src/modules/features/consent/ketch/files/lib/integrations/ketch/script.tsx +8 -0
  154. package/src/modules/features/consent/ketch/files/lib/integrations/ketch/types.ts +15 -0
  155. package/src/modules/features/consent/ketch/files/lib/integrations/ketch/use-consent.ts +26 -0
  156. package/src/modules/features/jobs/ashby/files/_sanity-adapter/job-board-block.ts +53 -0
  157. package/src/modules/features/jobs/ashby/files/lib/integrations/ashby/README.md +134 -0
  158. package/src/modules/features/jobs/ashby/files/lib/integrations/ashby/env.ts +3 -0
  159. package/src/modules/features/jobs/ashby/files/lib/integrations/ashby/index.ts +40 -0
  160. package/src/modules/features/jobs/ashby/files/lib/integrations/ashby/normalize.ts +124 -0
  161. package/src/modules/features/jobs/ashby/files/lib/integrations/ashby/types.ts +20 -0
  162. package/src/modules/features/jobs/ashby/files/lib/integrations/check-integration.ts +25 -0
  163. package/src/templates/next-default/.biome/plugins/no-fill-without-sizes.grit +11 -0
  164. package/src/templates/next-default/biome.json +2 -1
  165. package/src/templates/next-default/components/ui/image/index.tsx +20 -12
  166. package/src/templates/next-default/next.config.ts +10 -4
  167. package/src/templates/next-experiments/.biome/plugins/no-fill-without-sizes.grit +11 -0
  168. package/src/templates/next-experiments/biome.json +2 -1
  169. package/src/templates/next-experiments/components/ui/image/index.tsx +20 -12
  170. package/src/templates/next-experiments/next.config.ts +10 -4
  171. package/src/templates/next-pagebuilder/.biome/plugins/no-fill-without-sizes.grit +11 -0
  172. package/src/templates/next-pagebuilder/AGENTS.md +129 -0
  173. package/src/templates/next-pagebuilder/CLAUDE.md +3 -0
  174. package/src/templates/next-pagebuilder/README.md +70 -0
  175. package/src/templates/next-pagebuilder/app/(content)/layout.tsx +2 -0
  176. package/src/templates/next-pagebuilder/biome.json +2 -1
  177. package/src/templates/next-pagebuilder/components/layout/spacing-scale/index.tsx +18 -0
  178. package/src/templates/next-pagebuilder/components/page-builder/components/article-content/index.tsx +6 -1
  179. package/src/templates/next-pagebuilder/components/page-builder/components/article-content/related-post-item.tsx +1 -0
  180. package/src/templates/next-pagebuilder/components/page-builder/components/content-collection/index.tsx +2 -2
  181. package/src/templates/next-pagebuilder/components/page-builder/components/description.tsx +3 -7
  182. package/src/templates/next-pagebuilder/components/page-builder/components/gated-content/index.tsx +2 -2
  183. package/src/templates/next-pagebuilder/components/page-builder/components/hero.tsx +3 -7
  184. package/src/templates/next-pagebuilder/components/page-builder/components/rich-text-block.tsx +3 -5
  185. package/src/templates/next-pagebuilder/components/page-builder/registry.tsx +70 -0
  186. package/src/templates/next-pagebuilder/components/page-builder/renderer.tsx +33 -49
  187. package/src/templates/next-pagebuilder/components/page-builder/section.tsx +35 -0
  188. package/src/templates/next-pagebuilder/components/page-document/index.tsx +1 -1
  189. package/src/templates/next-pagebuilder/components/ui/image/index.tsx +20 -12
  190. package/src/templates/next-pagebuilder/lib/integrations/sanity/sanity.types.ts +92 -4
  191. package/src/templates/next-pagebuilder/next.config.ts +4 -3
  192. package/src/templates/next-pagebuilder-cache/.biome/plugins/no-fill-without-sizes.grit +11 -0
  193. package/src/templates/next-pagebuilder-cache/AGENTS.md +136 -0
  194. package/src/templates/next-pagebuilder-cache/CLAUDE.md +3 -0
  195. package/src/templates/next-pagebuilder-cache/README.md +75 -0
  196. package/src/templates/next-pagebuilder-cache/app/(content)/layout.tsx +2 -0
  197. package/src/templates/next-pagebuilder-cache/biome.json +2 -1
  198. package/src/templates/next-pagebuilder-cache/components/layout/spacing-scale/index.tsx +46 -0
  199. package/src/templates/next-pagebuilder-cache/components/page-builder/components/article-content/index.tsx +6 -1
  200. package/src/templates/next-pagebuilder-cache/components/page-builder/components/article-content/related-post-item.tsx +1 -0
  201. package/src/templates/next-pagebuilder-cache/components/page-builder/components/content-collection/index.tsx +2 -2
  202. package/src/templates/next-pagebuilder-cache/components/page-builder/components/description.tsx +3 -7
  203. package/src/templates/next-pagebuilder-cache/components/page-builder/components/gated-content/index.tsx +2 -2
  204. package/src/templates/next-pagebuilder-cache/components/page-builder/components/hero.tsx +3 -7
  205. package/src/templates/next-pagebuilder-cache/components/page-builder/components/rich-text-block.tsx +3 -5
  206. package/src/templates/next-pagebuilder-cache/components/page-builder/registry.tsx +70 -0
  207. package/src/templates/next-pagebuilder-cache/components/page-builder/renderer.tsx +33 -49
  208. package/src/templates/next-pagebuilder-cache/components/page-builder/section.tsx +35 -0
  209. package/src/templates/next-pagebuilder-cache/components/page-document/index.tsx +1 -1
  210. package/src/templates/next-pagebuilder-cache/components/ui/image/index.tsx +21 -12
  211. package/src/templates/next-pagebuilder-cache/lib/integrations/sanity/sanity.types.ts +92 -4
  212. package/src/templates/next-pagebuilder-cache/lib/utils/image-sizes.ts +72 -0
  213. package/src/templates/next-pagebuilder-cache/next.config.ts +4 -3
  214. package/src/templates/next-webgl/.biome/plugins/no-fill-without-sizes.grit +11 -0
  215. package/src/templates/next-webgl/biome.json +2 -1
  216. package/src/templates/next-webgl/components/ui/image/index.tsx +20 -12
  217. package/src/templates/next-webgl/next.config.ts +10 -4
@@ -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
+ }
@@ -0,0 +1,62 @@
1
+ import { useEffect, useState } from "react";
2
+ import { useClient } from "sanity";
3
+ import { apiVersion } from "@/lib/integrations/sanity/env";
4
+
5
+ const PREVIEW_ASSETS_QUERY = `*[
6
+ _type == "studioPreview" &&
7
+ key in $keys &&
8
+ defined(preview.asset._ref)
9
+ ] | order(_updatedAt desc) {
10
+ key,
11
+ "url": preview.asset->url
12
+ }`;
13
+
14
+ type PreviewDocument = {
15
+ key: string | null;
16
+ url: string | null;
17
+ };
18
+
19
+ export type StudioPreviewUrls = Record<string, string>;
20
+
21
+ const SEPARATOR = " ";
22
+
23
+ /** `undefined` while in flight, so callers can tell loading from "no preview". */
24
+ export function useStudioPreviewUrls(keys: readonly string[]) {
25
+ const client = useClient({ apiVersion });
26
+ const [urls, setUrls] = useState<StudioPreviewUrls>();
27
+ // Keys off contents, not array identity — callers pass a freshly mapped array.
28
+ const keyList = keys.join(SEPARATOR);
29
+
30
+ useEffect(() => {
31
+ if (!keyList) {
32
+ setUrls({});
33
+ return;
34
+ }
35
+
36
+ let cancelled = false;
37
+
38
+ client
39
+ .fetch<PreviewDocument[]>(PREVIEW_ASSETS_QUERY, {
40
+ keys: keyList.split(SEPARATOR),
41
+ })
42
+ .then((documents) => {
43
+ if (cancelled) return;
44
+ const next: StudioPreviewUrls = {};
45
+ for (const document of documents) {
46
+ if (document.key && document.url && !next[document.key]) {
47
+ next[document.key] = document.url;
48
+ }
49
+ }
50
+ setUrls(next);
51
+ })
52
+ .catch(() => {
53
+ if (!cancelled) setUrls({});
54
+ });
55
+
56
+ return () => {
57
+ cancelled = true;
58
+ };
59
+ }, [client, keyList]);
60
+
61
+ return urls;
62
+ }
@@ -8,6 +8,7 @@ export type NavbarData = NonNullable<LayoutData["navbar"]>;
8
8
  export type FooterData = NonNullable<LayoutData["footer"]>;
9
9
  export type CompanyData = NonNullable<LayoutData["companyData"]>;
10
10
  export type StructuredData = NonNullable<LayoutData["structuredData"]>;
11
+ export type SpacingSettings = NonNullable<LayoutData["spacingSettings"]>;
11
12
 
12
13
  // Singleton document types that, when published, should bust the layout cache.
13
14
  export const SANITY_LAYOUT_TAGS = [
@@ -15,6 +16,7 @@ export const SANITY_LAYOUT_TAGS = [
15
16
  "footer",
16
17
  "companyData",
17
18
  "structuredData",
19
+ "spacingSettings",
18
20
  ];
19
21
 
20
22
  const EMPTY_LAYOUT: LayoutData = {
@@ -22,11 +24,12 @@ const EMPTY_LAYOUT: LayoutData = {
22
24
  footer: null,
23
25
  companyData: null,
24
26
  structuredData: null,
27
+ spacingSettings: null,
25
28
  };
26
29
 
27
- // One query for all four layout singletons, wrapped in React cache so the header,
28
- // footer and json-ld share a single round trip. Tagged with all four `_type` tags
29
- // because it reads all four documents.
30
+ // One query for every layout singleton, wrapped in React cache so the header,
31
+ // footer, json-ld and spacing scale share a single round trip. Tagged with each
32
+ // `_type` because it reads all of those documents.
30
33
  export const getLayoutData = cache(async (): Promise<LayoutData> => {
31
34
  const { data } = await fetchSanity<LayoutData>({
32
35
  query: LAYOUT_QUERY,
@@ -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);
@@ -9,15 +9,35 @@ export type PageBuilderDocumentType =
9
9
  | "blogContent"
10
10
  | (typeof singletonComponents)[number]["schemaType"];
11
11
 
12
+ /**
13
+ * A block declares two orthogonal axes. `pageTypes` is which page documents may
14
+ * host it and drives validation; `category` is what the block structurally is
15
+ * and drives the insert menu. Never collapse them back into one field — that is
16
+ * what made the menu a single flat list.
17
+ */
18
+ export type PageBuilderGroupName = "generic" | "blogEntry";
19
+
20
+ export const componentCategories = [
21
+ { name: "hero", title: "Hero" },
22
+ { name: "text", title: "Text" },
23
+ { name: "list", title: "List" },
24
+ { name: "content", title: "Content" },
25
+ { name: "misc", title: "Miscellaneous" },
26
+ ] as const satisfies ReadonlyArray<{ name: string; title: string }>;
27
+
28
+ export type ComponentCategory = (typeof componentCategories)[number]["name"];
29
+
12
30
  type PageBuilderReferenceMember = {
13
31
  documentType: PageBuilderDocumentType;
14
32
  name: string;
15
33
  title: string;
16
34
  documentId?: string;
17
- groups: PageBuilderGroupName[];
35
+ pageTypes: readonly PageBuilderGroupName[];
36
+ category: ComponentCategory;
18
37
  };
19
38
 
20
- export type PageBuilderGroupName = "generic" | "blogEntry";
39
+ export const blockPreviewKey = (schemaTypeName: string) =>
40
+ `block.${schemaTypeName}`;
21
41
 
22
42
  export const pageBuilderPageTypes: Array<{
23
43
  title: string;
@@ -41,31 +61,36 @@ const reusablePageBuilderReferenceMembers: PageBuilderReferenceMember[] = [
41
61
  documentType: "hero",
42
62
  name: "heroReference",
43
63
  title: "Heading",
44
- groups: ["generic"],
64
+ pageTypes: ["generic"],
65
+ category: "hero",
45
66
  },
46
67
  {
47
68
  documentType: "description",
48
69
  name: "descriptionReference",
49
70
  title: "Description",
50
- groups: ["generic"],
71
+ pageTypes: ["generic"],
72
+ category: "text",
51
73
  },
52
74
  {
53
75
  documentType: "richTextBlock",
54
76
  name: "richTextBlockReference",
55
77
  title: "Rich Text",
56
- groups: ["generic"],
78
+ pageTypes: ["generic"],
79
+ category: "text",
57
80
  },
58
81
  {
59
82
  documentType: "gatedContent",
60
83
  name: "gatedContentReference",
61
84
  title: "Gated Content",
62
- groups: ["generic"],
85
+ pageTypes: ["generic"],
86
+ category: "misc",
63
87
  },
64
88
  {
65
89
  documentType: "blogContent",
66
90
  name: "blogContentReference",
67
91
  title: "Blog Content",
68
- groups: ["blogEntry"],
92
+ pageTypes: ["blogEntry"],
93
+ category: "content",
69
94
  },
70
95
  ];
71
96
 
@@ -75,7 +100,8 @@ const singletonPageBuilderReferenceMembers: PageBuilderReferenceMember[] =
75
100
  name: `${component.schemaType}Reference`,
76
101
  title: component.title,
77
102
  documentId: component.documentId,
78
- groups: ["generic"],
103
+ pageTypes: ["generic"],
104
+ category: component.category,
79
105
  }));
80
106
 
81
107
  export const pageBuilderReferenceMembers: PageBuilderReferenceMember[] = [
@@ -93,25 +119,37 @@ export const getPageBuilderReferenceMemberByDocumentType = (
93
119
  (member) => member.documentType === documentType,
94
120
  );
95
121
 
96
- const isPageBuilderGroupVisible = (
122
+ const isMemberVisibleForPageType = (
123
+ member: PageBuilderReferenceMember,
97
124
  pageType: PageBuilderGroupName | undefined,
98
- group: PageBuilderGroupName,
99
- ) => (pageType ?? "generic") === group;
125
+ ) => member.pageTypes.includes(pageType ?? "generic");
100
126
 
101
127
  export const getVisiblePageBuilderReferenceMembers = (
102
128
  pageType: PageBuilderGroupName | undefined,
103
129
  ) =>
104
130
  pageBuilderReferenceMembers.filter((member) =>
105
- member.groups.some((group) => isPageBuilderGroupVisible(pageType, group)),
131
+ isMemberVisibleForPageType(member, pageType),
106
132
  );
107
133
 
108
- export const getVisiblePageBuilderInsertMenuGroups = (
134
+ export const getVisiblePageBuilderMemberNames = (
109
135
  pageType: PageBuilderGroupName | undefined,
110
136
  ) =>
111
- pageBuilderInsertMenuGroups.filter(
112
- (group) => group.name === (pageType ?? "generic"),
137
+ new Set(
138
+ getVisiblePageBuilderReferenceMembers(pageType).map((member) => member.name),
113
139
  );
114
140
 
141
+ export const getVisiblePageBuilderInsertMenuGroups = (
142
+ pageType: PageBuilderGroupName | undefined,
143
+ ) => {
144
+ const visible = getVisiblePageBuilderMemberNames(pageType);
145
+ return pageBuilderInsertMenuGroups
146
+ .map((group) => ({
147
+ ...group,
148
+ of: group.of.filter((name) => visible.has(name)),
149
+ }))
150
+ .filter((group) => group.of.length > 0);
151
+ };
152
+
115
153
  export const getInvalidPageBuilderDocumentTypes = (
116
154
  pageType: PageBuilderGroupName | undefined,
117
155
  documentTypes: PageBuilderDocumentType[],
@@ -121,24 +159,24 @@ export const getInvalidPageBuilderDocumentTypes = (
121
159
 
122
160
  if (!member) return false;
123
161
 
124
- return !member.groups.some((group) =>
125
- isPageBuilderGroupVisible(pageType, group),
126
- );
162
+ return !isMemberVisibleForPageType(member, pageType);
127
163
  });
128
164
 
129
- export const pageBuilderInsertMenuGroups = [
130
- {
131
- name: "generic",
132
- title: "Generic",
133
- of: pageBuilderReferenceMembers
134
- .filter((member) => member.groups.includes("generic"))
135
- .map((member) => member.name),
136
- },
137
- {
138
- name: "blogEntry",
139
- title: "Blog Entry",
140
- of: pageBuilderReferenceMembers
141
- .filter((member) => member.groups.includes("blogEntry"))
142
- .map((member) => member.name),
143
- },
144
- ];
165
+ export const pageBuilderInsertMenuGroups: Array<{
166
+ name: ComponentCategory;
167
+ title: string;
168
+ of: string[];
169
+ }> = componentCategories.map(({ name, title }) => ({
170
+ name,
171
+ title,
172
+ of: pageBuilderReferenceMembers
173
+ .filter((member) => member.category === name)
174
+ .map((member) => member.name),
175
+ }));
176
+
177
+ export const pageBuilderBlockPreviewOptions = pageBuilderReferenceMembers.map(
178
+ (member) => ({
179
+ title: `Block — ${member.title} · ${member.name}`,
180
+ value: blockPreviewKey(member.name),
181
+ }),
182
+ );
@@ -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
  `);
@@ -4,3 +4,4 @@ export { gatedContent } from "./reusable/gated-content";
4
4
  export { hero } from "./reusable/hero";
5
5
  export { richTextBlock } from "./reusable/rich-text-block";
6
6
  export { blogCollection } from "./singleton/content-collection";
7
+ export { studioPreview } from "./studio-preview";
@@ -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
7
  title: "Blog Content",
7
- type: "document",
8
8
  icon: contentIcon,
9
9
  fields: [
10
10
  defineField({
@@ -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: [