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,150 @@
1
+ ---
2
+ name: page-builder-block
3
+ description: >-
4
+ Create or modify a Sanity page-builder block in a basement page-builder
5
+ project so it follows the repo conventions: defined with `defineBlock`, drawn
6
+ through the renderer's block registry, Anchor ID (generated from the title
7
+ when empty), Presentation Mode, and spacing owned by the frame that instances
8
+ the block rather than the block itself. Use when asked to "add a block to the
9
+ page builder", "create a new section/component for Sanity", "add anchor links
10
+ to a block", "control the spacing of a block from Sanity", or when reviewing
11
+ an existing block for these rules.
12
+ ---
13
+
14
+ # Page-builder block conventions
15
+
16
+ Blocks are **documents** referenced from the `pageBuilder` array, drawn by
17
+ `components/page-builder/renderer.tsx` through the registry in
18
+ `components/page-builder/registry.tsx`. Read one existing block end to end
19
+ before writing a new one — `hero` is the smallest complete example:
20
+
21
+ - schema: `lib/integrations/sanity/schemas/components/reusable/hero.ts`
22
+ - component: `components/page-builder/components/hero.tsx`
23
+ - registration: `lib/integrations/sanity/schemas/components/index.ts`,
24
+ `lib/integrations/sanity/schemas/index.ts`,
25
+ `lib/integrations/sanity/page-builder-config.ts`,
26
+ `lib/integrations/sanity/structure.ts`,
27
+ `components/page-builder/registry.tsx`
28
+
29
+ A block exists in two halves — a schema and a component — and each half has one
30
+ place that makes it a block rather than an ordinary document or a loose React
31
+ component. Use them and the conventions below come for free; skip them and a
32
+ typecheck fails.
33
+
34
+ ## Non-negotiables
35
+
36
+ 1. **Define the schema with `defineBlock`, never `defineType`.**
37
+ `lib/integrations/sanity/schemas/shared/block.ts` implies
38
+ `type: "document"` and appends Anchor ID + Presentation Mode as the last two
39
+ fields:
40
+
41
+ ```ts
42
+ import { defineField } from "sanity"
43
+ import { defineBlock } from "@/lib/integrations/sanity/schemas/shared/block"
44
+
45
+ export const featureGrid = defineBlock({
46
+ name: "featureGrid",
47
+ title: "Feature Grid",
48
+ icon: featureGridIcon,
49
+ fields: [
50
+ // ...content fields...
51
+ ],
52
+ preview: { /* ... */ },
53
+ })
54
+ ```
55
+
56
+ What it adds:
57
+ - `anchorId` — optional. Left empty, the anchor is **generated from the
58
+ block's title** at render time (`resolveAnchorId` in
59
+ `lib/integrations/sanity/schemas/shared/anchor.ts` walks `headline`,
60
+ `heading`, `teaserHeading`, `title`, `label` — never `internalTitle`, which
61
+ is Studio-only) and de-duplicated per page. A value typed by hand always
62
+ wins. Name your block's headline field one of those and it works for free.
63
+ - `presentation` — object with `paddingTop` / `paddingBottom`, each storing
64
+ the `_key` of a step from the shared scale. No manual px, ever. A block
65
+ that never touches it follows whichever step is marked as the default.
66
+
67
+ Blocks that declare `groups` get a **Presentation** tab for the two fields,
68
+ added automatically — don't list it in `groups` yourself. A block with no
69
+ groups shows them inline, at the bottom of the form.
70
+
71
+ 2. **Register the component in the block registry.** One entry per block, keyed
72
+ by schema type; the renderer has no switch to extend:
73
+
74
+ ```tsx
75
+ featureGrid: {
76
+ render: (block) => <FeatureGrid block={block} />,
77
+ isEmpty: (block) => !block.items?.length,
78
+ },
79
+ ```
80
+
81
+ - `render` receives the block narrowed to its own `_type`, plus a
82
+ `BlockContext` for page-level values a block doesn't own (`searchParams`,
83
+ `pinnedCategory`). Take the second argument only if you need it.
84
+ - `isEmpty` is optional and declares when the block has nothing to show. The
85
+ renderer then skips the block **and** its `<Section>`, so an unfilled block
86
+ leaves no padded gap behind and doesn't claim an anchor id. Don't
87
+ early-return `null` from the component instead — the renderer has to know
88
+ before it wraps anything.
89
+
90
+ The registry's key set is typed as exactly `PageBuilderDocumentType`, so a
91
+ block registered in `page-builder-config.ts` without an entry here — or an
92
+ entry here for a block that was never registered there — is a typecheck
93
+ error.
94
+
95
+ 3. **Never hardcode vertical padding inside a block component.** The renderer
96
+ wraps every block in `<Section>` (`components/page-builder/section.tsx`),
97
+ which renders the `<section>` element, applies the anchor `id` and the
98
+ padding. A block component returns its content only — no outer `<section>`,
99
+ no `py-*` / `pt-*` / `pb-*` on its root. Internal gaps between a block's own
100
+ parts are fine (`gap-*`, `space-y-*`, `mt-*` on children).
101
+
102
+ 4. **The spacing scale is content, not code.** Editors own it at **Layout →
103
+ Spacing Scale** (`spacingSettings`): a list of steps, each with a name, px
104
+ per breakpoint (mobile / tablet / desktop, empty reuses Desktop) and one
105
+ marked as the default — ticking a second one unticks the previous, so there
106
+ is always exactly one. They add, rename or remove steps without touching
107
+ code; when the list is empty the field offers an "Add the built-in steps"
108
+ button that seeds it.
109
+ `lib/integrations/sanity/schemas/shared/spacing.ts` holds the five built-in
110
+ steps (XS · SM · MD · LG · XL, MD default) used to seed the document and as
111
+ the fallback when it is empty, plus the resolver and the CSS builder.
112
+
113
+ Resolved values ship as CSS custom properties from
114
+ `components/layout/spacing-scale/index.tsx` (rendered by
115
+ `app/(content)/layout.tsx`), one `--section-space-<step key>` per step plus
116
+ `--section-space-default`, re-declared inside `min-width: 768px` and
117
+ `1440px` media queries. `Section` only references
118
+ `var(--section-space-<key>, var(--section-space-default, 2.5rem))`, so a
119
+ deleted or unknown step degrades to the default instead of breaking, and
120
+ **changing a value never requires a deploy**.
121
+
122
+ Publishing `spacingSettings` busts the layout cache: it is in
123
+ `SANITY_LAYOUT_TAGS` (`fetchers/layout.ts`, or `tags.ts` on the Cache
124
+ Components variant), same as navbar and footer.
125
+
126
+ 5. **Regenerate types after a schema change:** `bun run sanity:typegen`. The
127
+ page query spreads `...` for non-gated blocks, so new fields flow through
128
+ automatically — except `gatedContent`, whose GROQ branch lists fields
129
+ explicitly (`lib/integrations/sanity/queries.ts`). Add new fields there by
130
+ hand if the block is gated.
131
+
132
+ ## Checklist for a new block
133
+
134
+ - [ ] Schema in `schemas/components/reusable/<block>.ts` (or `singleton/` for a
135
+ one-instance block), built with `defineBlock` and carrying a `preview`.
136
+ - [ ] Icon in `lib/integrations/sanity/icons.ts` (phosphor duotone).
137
+ - [ ] Exported from `schemas/components/index.ts` and registered in
138
+ `schemas/index.ts` (`schema.types`).
139
+ - [ ] Member added to `page-builder-config.ts`: the `PageBuilderDocumentType`
140
+ union plus an entry in `reusablePageBuilderReferenceMembers` carrying
141
+ **both** axes — `pageTypes` (which page documents may host it) and
142
+ `category` (what the block is, one of `componentCategories`; `misc` only
143
+ when nothing fits).
144
+ - [ ] Listed in `structure.ts` under the right folder.
145
+ - [ ] Component in `components/page-builder/components/<block>.tsx`, typed with
146
+ `Extract<PageBuilderBlock, { _type: "<block>" }>`, no outer section, no
147
+ vertical padding. Variants via `cva`/`cn` when the block has variants.
148
+ - [ ] Entry in `components/page-builder/registry.tsx`, with `isEmpty` when the
149
+ block can be left unfilled.
150
+ - [ ] `bun run sanity:typegen && bun run typecheck && bun run lint && bun run build && bun run knip`.
@@ -2,7 +2,8 @@ import { getImageDimensions } from "@sanity/asset-utils";
2
2
  import { Image, type ImageProps } from "@/components/ui/image";
3
3
  import { urlForImage } from "@/lib/integrations/sanity/utils/image";
4
4
 
5
- interface SanityImageProps extends Omit<ImageProps, "src" | "aspectRatio"> {
5
+ interface SanityImageBaseProps
6
+ extends Omit<ImageProps, "src" | "aspectRatio" | "sizes" | "width" | "height"> {
6
7
  image: {
7
8
  asset?: {
8
9
  _ref: string;
@@ -15,6 +16,38 @@ interface SanityImageProps extends Omit<ImageProps, "src" | "aspectRatio"> {
15
16
  maxWidth?: number;
16
17
  }
17
18
 
19
+ type SanityImageProps = SanityImageBaseProps &
20
+ (
21
+ | { sizes: string; width?: never; height?: never }
22
+ | { width: number; height: number; sizes?: string }
23
+ );
24
+
25
+ const MAX_SOURCE_WIDTH = 3840;
26
+
27
+ const isGifRef = (ref: string) => ref.endsWith("-gif");
28
+
29
+ const getImageSrc = (
30
+ image: SanityImageProps["image"],
31
+ maxWidth: number,
32
+ croppedWidth: number,
33
+ ) => {
34
+ const builder = urlForImage(image);
35
+ const ref = image.asset?._ref ?? "";
36
+
37
+ if (ref.endsWith("-svg")) return builder.url();
38
+
39
+ const requestWidth = Math.min(maxWidth, MAX_SOURCE_WIDTH, croppedWidth);
40
+ const { left, right, top, bottom } = image.crop ?? {};
41
+ const hasCrop = Boolean(left || right || top || bottom);
42
+ const isGif = isGifRef(ref);
43
+
44
+ if (!(isGif || hasCrop) && requestWidth >= croppedWidth) {
45
+ return builder.url();
46
+ }
47
+
48
+ return builder.width(requestWidth).url();
49
+ };
50
+
18
51
  export function SanityImage({
19
52
  image,
20
53
  maxWidth = 1920,
@@ -39,11 +72,10 @@ export function SanityImage({
39
72
 
40
73
  return (
41
74
  <Image
42
- src={urlForImage(image)
43
- .width(Math.min(maxWidth, sourceWidth))
44
- .url()}
75
+ src={getImageSrc(image, maxWidth, sourceWidth)}
45
76
  alt={alt || image.alt || ""}
46
77
  aspectRatio={sourceWidth / sourceHeight}
78
+ unoptimized={isGifRef(image.asset._ref)}
47
79
  {...imageProps}
48
80
  {...props}
49
81
  />
@@ -37,7 +37,13 @@ export function RichText({ content }: RichTextProps) {
37
37
  value={content}
38
38
  components={{
39
39
  types: {
40
- image: ({ value }) => <SanityImage image={value} maxWidth={1920} />,
40
+ image: ({ value }) => (
41
+ <SanityImage
42
+ image={value}
43
+ maxWidth={1920}
44
+ sizes="(min-width: 768px) 768px, 100vw"
45
+ />
46
+ ),
41
47
  },
42
48
  marks: {
43
49
  link: ({ children, value }) => {
@@ -42,7 +42,13 @@ export const RichText = ({ content }: RichTextProps) => {
42
42
  value={content}
43
43
  components={{
44
44
  types: {
45
- image: ({ value }) => <SanityImage image={value} maxWidth={1920} />,
45
+ image: ({ value }) => (
46
+ <SanityImage
47
+ image={value}
48
+ maxWidth={1920}
49
+ sizes="(min-width: 768px) 768px, 100vw"
50
+ />
51
+ ),
46
52
  table: ({ value }) => {
47
53
  const rows = value?.rows as
48
54
  | Array<{ _key?: string; cells?: string[] }>
@@ -2,7 +2,8 @@ import { getImageDimensions } from "@sanity/asset-utils";
2
2
  import { Image, type ImageProps } from "@/components/ui/image";
3
3
  import { urlForImage } from "@/lib/integrations/sanity/utils/image";
4
4
 
5
- interface SanityImageProps extends Omit<ImageProps, "src" | "aspectRatio"> {
5
+ interface SanityImageBaseProps
6
+ extends Omit<ImageProps, "src" | "aspectRatio" | "sizes" | "width" | "height"> {
6
7
  image: {
7
8
  asset?: {
8
9
  _ref: string;
@@ -15,6 +16,38 @@ interface SanityImageProps extends Omit<ImageProps, "src" | "aspectRatio"> {
15
16
  maxWidth?: number;
16
17
  }
17
18
 
19
+ type SanityImageProps = SanityImageBaseProps &
20
+ (
21
+ | { sizes: string; width?: never; height?: never }
22
+ | { width: number; height: number; sizes?: string }
23
+ );
24
+
25
+ const MAX_SOURCE_WIDTH = 3840;
26
+
27
+ const isGifRef = (ref: string) => ref.endsWith("-gif");
28
+
29
+ const getImageSrc = (
30
+ image: SanityImageProps["image"],
31
+ maxWidth: number,
32
+ croppedWidth: number,
33
+ ) => {
34
+ const builder = urlForImage(image);
35
+ const ref = image.asset?._ref ?? "";
36
+
37
+ if (ref.endsWith("-svg")) return builder.url();
38
+
39
+ const requestWidth = Math.min(maxWidth, MAX_SOURCE_WIDTH, croppedWidth);
40
+ const { left, right, top, bottom } = image.crop ?? {};
41
+ const hasCrop = Boolean(left || right || top || bottom);
42
+ const isGif = isGifRef(ref);
43
+
44
+ if (!(isGif || hasCrop) && requestWidth >= croppedWidth) {
45
+ return builder.url();
46
+ }
47
+
48
+ return builder.width(requestWidth).url();
49
+ };
50
+
18
51
  export function SanityImage({
19
52
  image,
20
53
  maxWidth = 1920,
@@ -39,11 +72,10 @@ export function SanityImage({
39
72
 
40
73
  return (
41
74
  <Image
42
- src={urlForImage(image)
43
- .width(Math.min(maxWidth, sourceWidth))
44
- .url()}
75
+ src={getImageSrc(image, maxWidth, sourceWidth)}
45
76
  alt={alt || image.alt || ""}
46
77
  aspectRatio={sourceWidth / sourceHeight}
78
+ unoptimized={isGifRef(image.asset._ref)}
47
79
  {...imageProps}
48
80
  {...props}
49
81
  />
@@ -0,0 +1,17 @@
1
+ import { type StringInputProps, useFormValue } from "sanity";
2
+ import { deriveAnchorId } from "@/lib/integrations/sanity/schemas/shared/anchor";
3
+
4
+ export function AnchorIdInput(props: StringInputProps) {
5
+ const document = useFormValue([]) as Record<string, unknown> | undefined;
6
+ const derived = deriveAnchorId(document ?? {});
7
+
8
+ return props.renderDefault({
9
+ ...props,
10
+ elementProps: {
11
+ ...props.elementProps,
12
+ placeholder: derived
13
+ ? `${derived} — generated from the title`
14
+ : "generated from the title",
15
+ },
16
+ });
17
+ }
@@ -1,25 +1,43 @@
1
1
  import { type ArrayOfObjectsInputProps, useFormValue } from "sanity";
2
+ import { PageBuilderInsertMenu } from "@/lib/integrations/sanity/components/page-builder-insert-menu";
3
+ import { useStudioPreviewUrls } from "@/lib/integrations/sanity/components/use-studio-preview-urls";
2
4
  import {
5
+ blockPreviewKey,
3
6
  getVisiblePageBuilderInsertMenuGroups,
4
- getVisiblePageBuilderReferenceMembers,
7
+ getVisiblePageBuilderMemberNames,
5
8
  type PageBuilderGroupName,
9
+ pageBuilderReferenceMembers,
6
10
  } from "@/lib/integrations/sanity/page-builder-config";
7
11
 
12
+ const BLOCK_PREVIEW_KEYS = pageBuilderReferenceMembers.map((member) =>
13
+ blockPreviewKey(member.name),
14
+ );
15
+
16
+ function useInsertMenuViews() {
17
+ const previewUrls = useStudioPreviewUrls(BLOCK_PREVIEW_KEYS);
18
+
19
+ return [
20
+ {
21
+ name: "grid" as const,
22
+ previewImageUrl: (schemaTypeName: string) =>
23
+ previewUrls?.[blockPreviewKey(schemaTypeName)],
24
+ },
25
+ { name: "list" as const },
26
+ ];
27
+ }
28
+
8
29
  export function PageBuilderInput(props: ArrayOfObjectsInputProps) {
9
30
  const pageTypeValue = useFormValue(["type"]);
10
31
  const pageType =
11
32
  typeof pageTypeValue === "string"
12
33
  ? (pageTypeValue as PageBuilderGroupName)
13
34
  : undefined;
14
-
15
- const visibleMemberNames = new Set(
16
- getVisiblePageBuilderReferenceMembers(pageType).map(
17
- (member) => member.name,
18
- ),
19
- );
35
+ const views = useInsertMenuViews();
36
+ const visibleMemberNames = getVisiblePageBuilderMemberNames(pageType);
20
37
 
21
38
  return props.renderDefault({
22
39
  ...props,
40
+ arrayFunctions: PageBuilderInsertMenu,
23
41
  schemaType: {
24
42
  ...props.schemaType,
25
43
  of: props.schemaType.of.filter(
@@ -32,6 +50,7 @@ export function PageBuilderInput(props: ArrayOfObjectsInputProps) {
32
50
  insertMenu: {
33
51
  ...props.schemaType.options?.insertMenu,
34
52
  groups: getVisiblePageBuilderInsertMenuGroups(pageType),
53
+ views,
35
54
  },
36
55
  },
37
56
  },
@@ -0,0 +1,242 @@
1
+ import { AddIcon, SearchIcon } from "@sanity/icons";
2
+ import {
3
+ Box,
4
+ Button,
5
+ Card,
6
+ Dialog,
7
+ Flex,
8
+ Grid,
9
+ Stack,
10
+ Text,
11
+ TextInput,
12
+ } from "@sanity/ui";
13
+ import { useMemo, useState } from "react";
14
+ import type { ArrayInputFunctionsProps, ArraySchemaType } from "sanity";
15
+
16
+ type ArrayItem = { _key: string };
17
+ type MemberSchemaType = ArraySchemaType["of"][number];
18
+
19
+ type CreateItem = (memberType: MemberSchemaType) => ArrayItem;
20
+
21
+ type InsertMenuSection = {
22
+ name: string;
23
+ title: string;
24
+ members: MemberSchemaType[];
25
+ };
26
+
27
+ const SCROLL_HEIGHT = "min(60vh, 34rem)";
28
+ const TILE_ASPECT_RATIO = "3 / 2";
29
+
30
+ const memberTitle = (member: MemberSchemaType) => member.title ?? member.name;
31
+
32
+ function BlockTile({
33
+ member,
34
+ previewUrl,
35
+ onSelect,
36
+ }: {
37
+ member: MemberSchemaType;
38
+ previewUrl: string | undefined;
39
+ onSelect: () => void;
40
+ }) {
41
+ const Icon = member.icon;
42
+
43
+ return (
44
+ <Card
45
+ as="button"
46
+ border
47
+ onClick={onSelect}
48
+ overflow="hidden"
49
+ padding={0}
50
+ radius={2}
51
+ style={{ cursor: "pointer", textAlign: "left", width: "100%" }}
52
+ type="button"
53
+ >
54
+ <Stack space={0}>
55
+ <Box
56
+ style={{
57
+ aspectRatio: TILE_ASPECT_RATIO,
58
+ background: "var(--card-muted-bg-color)",
59
+ position: "relative",
60
+ }}
61
+ >
62
+ {previewUrl ? (
63
+ // biome-ignore lint/performance/noImgElement: Studio-only CMS asset
64
+ <img
65
+ alt=""
66
+ src={`${previewUrl}?w=640&fit=max&auto=format`}
67
+ style={{
68
+ height: "100%",
69
+ inset: 0,
70
+ objectFit: "contain",
71
+ position: "absolute",
72
+ width: "100%",
73
+ }}
74
+ />
75
+ ) : (
76
+ <Flex
77
+ align="center"
78
+ justify="center"
79
+ style={{ inset: 0, position: "absolute" }}
80
+ >
81
+ {Icon ? <Icon /> : null}
82
+ </Flex>
83
+ )}
84
+ </Box>
85
+ <Box padding={2}>
86
+ <Text size={1} textOverflow="ellipsis" weight="medium">
87
+ {memberTitle(member)}
88
+ </Text>
89
+ </Box>
90
+ </Stack>
91
+ </Card>
92
+ );
93
+ }
94
+
95
+ function InsertMenuDialog({
96
+ sections,
97
+ previewImageUrl,
98
+ onSelect,
99
+ onClose,
100
+ }: {
101
+ sections: InsertMenuSection[];
102
+ previewImageUrl: ((schemaTypeName: string) => string | undefined) | undefined;
103
+ onSelect: (member: MemberSchemaType) => void;
104
+ onClose: () => void;
105
+ }) {
106
+ const [search, setSearch] = useState("");
107
+
108
+ const visibleSections = useMemo(() => {
109
+ const query = search.trim().toLowerCase();
110
+ if (!query) return sections;
111
+ return sections
112
+ .map((section) => ({
113
+ ...section,
114
+ members: section.members.filter((member) =>
115
+ memberTitle(member).toLowerCase().includes(query),
116
+ ),
117
+ }))
118
+ .filter((section) => section.members.length > 0);
119
+ }, [search, sections]);
120
+
121
+ return (
122
+ <Dialog
123
+ header="Components"
124
+ id="page-builder-insert"
125
+ onClose={onClose}
126
+ open
127
+ width={3}
128
+ >
129
+ <Stack padding={4} space={4}>
130
+ <TextInput
131
+ autoFocus
132
+ icon={SearchIcon}
133
+ onChange={(event) => setSearch(event.currentTarget.value)}
134
+ placeholder="Search components…"
135
+ value={search}
136
+ />
137
+ <Box style={{ height: SCROLL_HEIGHT, overflowY: "auto" }}>
138
+ {visibleSections.length === 0 ? (
139
+ <Text muted size={1}>
140
+ No components match “{search}”.
141
+ </Text>
142
+ ) : (
143
+ <Stack space={5}>
144
+ {visibleSections.map((section) => (
145
+ <Stack key={section.name} space={3}>
146
+ <Text muted size={1} weight="semibold">
147
+ {section.title}
148
+ </Text>
149
+ <Grid columns={[2, 3, 4]} gap={3}>
150
+ {section.members.map((member) => (
151
+ <BlockTile
152
+ key={member.name}
153
+ member={member}
154
+ onSelect={() => onSelect(member)}
155
+ previewUrl={previewImageUrl?.(member.name)}
156
+ />
157
+ ))}
158
+ </Grid>
159
+ </Stack>
160
+ ))}
161
+ </Stack>
162
+ )}
163
+ </Box>
164
+ </Stack>
165
+ </Dialog>
166
+ );
167
+ }
168
+
169
+ /**
170
+ * Pass as `arrayFunctions`. Reads `insertMenu.groups` and `insertMenu.views`, so
171
+ * a schema already configuring the built-in menu needs no extra wiring.
172
+ *
173
+ * Replaces the built-in popover, which is anchored to the array grid element and
174
+ * so clips on page scroll and resizes when switching tabs.
175
+ */
176
+ export function PageBuilderInsertMenu(
177
+ props: ArrayInputFunctionsProps<ArrayItem, ArraySchemaType>,
178
+ ) {
179
+ const { onItemAppend, onValueCreate, readOnly, schemaType } = props;
180
+ const [open, setOpen] = useState(false);
181
+
182
+ const insertMenu = schemaType.options?.insertMenu;
183
+ const members = schemaType.of;
184
+
185
+ const sections = useMemo<InsertMenuSection[]>(() => {
186
+ const byName = new Map(members.map((member) => [member.name, member]));
187
+ const groups = insertMenu?.groups;
188
+ if (!groups) {
189
+ return [{ name: "all", title: "Components", members: [...members] }];
190
+ }
191
+ return groups
192
+ .map((group) => ({
193
+ name: group.name,
194
+ title: group.title ?? group.name,
195
+ members: (group.of ?? [])
196
+ .map((name) => byName.get(name))
197
+ .filter((member) => member !== undefined),
198
+ }))
199
+ .filter((section) => section.members.length > 0);
200
+ }, [members, insertMenu]);
201
+
202
+ const previewImageUrl = useMemo(() => {
203
+ const gridView = insertMenu?.views?.find((view) => view.name === "grid");
204
+ return gridView && "previewImageUrl" in gridView
205
+ ? gridView.previewImageUrl
206
+ : undefined;
207
+ }, [insertMenu]);
208
+
209
+ const insert = (member: MemberSchemaType) => {
210
+ onItemAppend((onValueCreate as unknown as CreateItem)(member));
211
+ setOpen(false);
212
+ };
213
+
214
+ const onlyMember = members.length === 1 ? members[0] : undefined;
215
+
216
+ if (schemaType.options?.disableActions?.includes("add")) return null;
217
+
218
+ return (
219
+ <>
220
+ <Grid>
221
+ <Button
222
+ disabled={readOnly === true}
223
+ fontSize={1}
224
+ icon={AddIcon}
225
+ mode="ghost"
226
+ onClick={() => (onlyMember ? insert(onlyMember) : setOpen(true))}
227
+ padding={3}
228
+ text={onlyMember ? "Add item" : "Add item…"}
229
+ />
230
+ </Grid>
231
+
232
+ {open && (
233
+ <InsertMenuDialog
234
+ onClose={() => setOpen(false)}
235
+ onSelect={insert}
236
+ previewImageUrl={previewImageUrl}
237
+ sections={sections}
238
+ />
239
+ )}
240
+ </>
241
+ );
242
+ }