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
@@ -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: [
@@ -0,0 +1,37 @@
1
+ import { defineField, defineType } from "sanity";
2
+ import { pageBuilderCardIcon } from "@/lib/integrations/sanity/icons";
3
+ import { pageBuilderBlockPreviewOptions } from "@/lib/integrations/sanity/page-builder-config";
4
+
5
+ export const studioPreview = defineType({
6
+ name: "studioPreview",
7
+ title: "Component Preview",
8
+ type: "document",
9
+ icon: pageBuilderCardIcon,
10
+ fields: [
11
+ defineField({
12
+ name: "key",
13
+ title: "Component",
14
+ type: "string",
15
+ options: { list: pageBuilderBlockPreviewOptions },
16
+ validation: (Rule) => Rule.required(),
17
+ }),
18
+ defineField({
19
+ name: "preview",
20
+ title: "Preview image",
21
+ type: "image",
22
+ options: { hotspot: true },
23
+ validation: (Rule) => Rule.required(),
24
+ }),
25
+ ],
26
+ preview: {
27
+ select: { key: "key", media: "preview" },
28
+ prepare({ key, media }) {
29
+ return {
30
+ title:
31
+ pageBuilderBlockPreviewOptions.find((option) => option.value === key)
32
+ ?.title ?? "Component Preview",
33
+ media,
34
+ };
35
+ },
36
+ },
37
+ });
@@ -15,6 +15,7 @@ import {
15
15
  gatedContent,
16
16
  hero,
17
17
  richTextBlock,
18
+ studioPreview,
18
19
  } from "./components";
19
20
  import { author } from "./content/author";
20
21
  import { blogCategory } from "./content/blog-category";
@@ -22,6 +23,7 @@ import { companyData } from "./layout/company-data";
22
23
  import { footer } from "./layout/footer";
23
24
  import { navbar } from "./layout/navbar";
24
25
  import { redirect } from "./layout/redirect";
26
+ import { spacingSettings, spacingStep } from "./layout/spacing-settings";
25
27
  import { structuredData } from "./layout/structured-data";
26
28
  import { externalLink, link, pageReference } from "./shared/link";
27
29
  import { metadata } from "./shared/metadata";
@@ -34,6 +36,7 @@ import {
34
36
  import { page } from "./shared/page";
35
37
  import { pageBuilder } from "./shared/page-builder";
36
38
  import { pageFolder } from "./shared/page-folder";
39
+ import { presentation } from "./shared/presentation";
37
40
  import { richText } from "./shared/richText";
38
41
  import {
39
42
  customJsonLd,
@@ -73,10 +76,14 @@ export {
73
76
  pageBuilder,
74
77
  pageFolder,
75
78
  pageReference,
79
+ presentation,
76
80
  redirect,
77
81
  richText,
78
82
  softwareApplicationJsonLd,
83
+ spacingSettings,
84
+ spacingStep,
79
85
  structuredData,
86
+ studioPreview,
80
87
  videoJsonLd,
81
88
  };
82
89
 
@@ -89,6 +96,7 @@ export const schema: { types: SchemaTypeDefinition[] } = {
89
96
  link,
90
97
  metadata,
91
98
  pageBuilder,
99
+ presentation,
92
100
  richText,
93
101
 
94
102
  // Page-level JSON-LD blocks (selectable in metadata)
@@ -113,12 +121,15 @@ export const schema: { types: SchemaTypeDefinition[] } = {
113
121
  description,
114
122
  richTextBlock,
115
123
  gatedContent,
124
+ studioPreview,
116
125
 
117
126
  // Layout types (singletons for site-wide layout)
118
127
  companyData,
119
128
  navbar,
120
129
  footer,
121
130
  structuredData,
131
+ spacingStep,
132
+ spacingSettings,
122
133
 
123
134
  // Layout collections (managed lists, not singletons)
124
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
+ });
@@ -0,0 +1,75 @@
1
+ const anchorSourceFields = [
2
+ "headline",
3
+ "heading",
4
+ "teaserHeading",
5
+ "title",
6
+ "label",
7
+ ] as const;
8
+
9
+ const ANCHOR_MAX_LENGTH = 60;
10
+
11
+ export const slugifyAnchor = (raw: string) =>
12
+ raw
13
+ .normalize("NFKD")
14
+ .replace(/[\u0300-\u036f]/g, "")
15
+ .toLowerCase()
16
+ .replace(/[^a-z0-9]+/g, "-")
17
+ .replace(/^-+|-+$/g, "")
18
+ .slice(0, ANCHOR_MAX_LENGTH)
19
+ .replace(/-+$/g, "");
20
+
21
+ export const deriveAnchorId = (block: Record<string, unknown>) => {
22
+ for (const field of anchorSourceFields) {
23
+ const value = block[field];
24
+ if (typeof value === "string" && value.trim()) {
25
+ const slug = slugifyAnchor(value);
26
+ if (slug) return slug;
27
+ }
28
+ }
29
+
30
+ return "";
31
+ };
32
+
33
+ const explicitAnchorId = (block: Record<string, unknown>) =>
34
+ typeof block.anchorId === "string" ? slugifyAnchor(block.anchorId) : "";
35
+
36
+ const NO_RESERVED_IDS: ReadonlySet<string> = new Set();
37
+
38
+ export const reserveAnchorIds = (
39
+ blocks: readonly Record<string, unknown>[],
40
+ ): ReadonlySet<string> => {
41
+ const reserved = new Set<string>();
42
+
43
+ for (const block of blocks) {
44
+ const explicit = explicitAnchorId(block);
45
+ if (explicit) reserved.add(explicit);
46
+ }
47
+
48
+ return reserved;
49
+ };
50
+
51
+ export const resolveAnchorId = (
52
+ block: Record<string, unknown>,
53
+ taken: Set<string>,
54
+ reserved: ReadonlySet<string> = NO_RESERVED_IDS,
55
+ ) => {
56
+ const explicit = explicitAnchorId(block);
57
+ const base = explicit || deriveAnchorId(block);
58
+
59
+ if (!base) return "";
60
+
61
+ const isBlocked = (candidate: string) =>
62
+ taken.has(candidate) || (!explicit && reserved.has(candidate));
63
+
64
+ let candidate = base;
65
+ let suffix = 2;
66
+
67
+ while (isBlocked(candidate)) {
68
+ candidate = `${base}-${suffix}`;
69
+ suffix += 1;
70
+ }
71
+
72
+ taken.add(candidate);
73
+
74
+ return candidate;
75
+ };
@@ -0,0 +1,46 @@
1
+ import {
2
+ type DocumentDefinition,
3
+ defineType,
4
+ type FieldDefinition,
5
+ type FieldGroupDefinition,
6
+ } from "sanity";
7
+ import { presentationFields } from "./presentation";
8
+
9
+ const presentationGroup: FieldGroupDefinition = {
10
+ name: "presentation",
11
+ title: "Presentation",
12
+ };
13
+
14
+ type BlockDefinition = Omit<DocumentDefinition, "type" | "fields" | "groups"> & {
15
+ fields: FieldDefinition[];
16
+ groups?: FieldGroupDefinition[];
17
+ };
18
+
19
+ const withPresentationGroup = (groups: FieldGroupDefinition[] | undefined) => {
20
+ if (!groups?.length) return undefined;
21
+ if (groups.some((group) => group.name === presentationGroup.name)) {
22
+ return groups;
23
+ }
24
+
25
+ return [...groups, presentationGroup];
26
+ };
27
+
28
+ /**
29
+ * Defines a page-builder block — `type: "document"` plus Anchor ID and
30
+ * Presentation Mode. Use instead of `defineType`.
31
+ */
32
+ export const defineBlock = ({ fields, groups, ...rest }: BlockDefinition) => {
33
+ const blockGroups = withPresentationGroup(groups);
34
+
35
+ return defineType({
36
+ ...rest,
37
+ type: "document",
38
+ ...(blockGroups ? { groups: blockGroups } : {}),
39
+ fields: [
40
+ ...fields,
41
+ ...presentationFields(
42
+ blockGroups ? { group: presentationGroup.name } : undefined,
43
+ ),
44
+ ],
45
+ });
46
+ };