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
@@ -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
+ }
@@ -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
+ }
@@ -12,16 +12,18 @@ export type NavbarData = NonNullable<LayoutData["navbar"]>
12
12
  export type FooterData = NonNullable<LayoutData["footer"]>
13
13
  export type CompanyData = NonNullable<LayoutData["companyData"]>
14
14
  export type StructuredData = NonNullable<LayoutData["structuredData"]>
15
+ export type SpacingSettings = NonNullable<LayoutData["spacingSettings"]>
15
16
 
16
17
  const EMPTY_LAYOUT: LayoutData = {
17
18
  navbar: null,
18
19
  footer: null,
19
20
  companyData: null,
20
21
  structuredData: null,
22
+ spacingSettings: null,
21
23
  }
22
24
 
23
- // One entry for all four layout singletons: header, footer and json-ld call this
24
- // with the same options, so they share one fetch instead of four.
25
+ // One entry for every layout singleton: header, footer, json-ld and the spacing
26
+ // scale call this with the same options, so they share one fetch.
25
27
  //
26
28
  // Don't cap the empty branch — this renders in every route's shell, and a nested
27
29
  // lifetime propagates upwards as a min().
@@ -2,6 +2,9 @@ import type { Icon, IconProps } from "@phosphor-icons/react";
2
2
  import {
3
3
  AppWindowIcon,
4
4
  ArrowBendUpRightIcon,
5
+ ArrowLineDownIcon,
6
+ ArrowLineUpIcon,
7
+ ArrowsOutLineVerticalIcon,
5
8
  ArticleNyTimesIcon,
6
9
  BracketsCurlyIcon,
7
10
  BriefcaseIcon,
@@ -75,6 +78,11 @@ export const richTextIcon = asDuotoneIcon(TextTIcon);
75
78
  export const contentIcon = asDuotoneIcon(ArticleNyTimesIcon);
76
79
  export const gatedContentIcon = asDuotoneIcon(LockKeyIcon);
77
80
 
81
+ export const spacingSettingsIcon = asDuotoneIcon(ArrowsOutLineVerticalIcon);
82
+
83
+ export const paddingTopIcon = asDuotoneIcon(ArrowLineUpIcon);
84
+ export const paddingBottomIcon = asDuotoneIcon(ArrowLineDownIcon);
85
+
78
86
  export const pageIcon = asDuotoneIcon(FileTextIcon);
79
87
  export const pagesFolderIcon = asDuotoneIcon(BrowsersIcon);
80
88
  export const pageFolderIcon = asDuotoneIcon(FolderOpenIcon);