bsmnt 0.12.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 (174) hide show
  1. package/dist/application/add-integration/index.d.ts.map +1 -1
  2. package/dist/application/add-integration/index.js +23 -0
  3. package/dist/application/add-integration/index.js.map +1 -1
  4. package/dist/domain/consent.d.ts +17 -0
  5. package/dist/domain/consent.d.ts.map +1 -0
  6. package/dist/domain/consent.js +13 -0
  7. package/dist/domain/consent.js.map +1 -0
  8. package/dist/domain/integration.d.ts +19 -2
  9. package/dist/domain/integration.d.ts.map +1 -1
  10. package/dist/domain/integration.js +10 -1
  11. package/dist/domain/integration.js.map +1 -1
  12. package/dist/domain/jobs.d.ts +17 -0
  13. package/dist/domain/jobs.d.ts.map +1 -0
  14. package/dist/domain/jobs.js +13 -0
  15. package/dist/domain/jobs.js.map +1 -0
  16. package/dist/domain/skills.d.ts +23 -2
  17. package/dist/domain/skills.d.ts.map +1 -1
  18. package/dist/domain/skills.js +23 -2
  19. package/dist/domain/skills.js.map +1 -1
  20. package/dist/domain/templates.d.ts +7 -1
  21. package/dist/domain/templates.d.ts.map +1 -1
  22. package/dist/domain/templates.js +2 -0
  23. package/dist/domain/templates.js.map +1 -1
  24. package/dist/index.js +34 -9
  25. package/dist/index.js.map +1 -1
  26. package/dist/infrastructure/create/setup-agent.d.ts.map +1 -1
  27. package/dist/infrastructure/create/setup-agent.js +11 -4
  28. package/dist/infrastructure/create/setup-agent.js.map +1 -1
  29. package/dist/infrastructure/integrations/inject-integration.d.ts.map +1 -1
  30. package/dist/infrastructure/integrations/inject-integration.js +36 -2
  31. package/dist/infrastructure/integrations/inject-integration.js.map +1 -1
  32. package/dist/modules/features/consent/ketch/config.d.ts +18 -0
  33. package/dist/modules/features/consent/ketch/config.d.ts.map +1 -0
  34. package/dist/modules/features/consent/ketch/config.js +24 -0
  35. package/dist/modules/features/consent/ketch/config.js.map +1 -0
  36. package/dist/modules/features/consent/ketch/mergers/biome.d.ts +16 -0
  37. package/dist/modules/features/consent/ketch/mergers/biome.d.ts.map +1 -0
  38. package/dist/modules/features/consent/ketch/mergers/biome.js +31 -0
  39. package/dist/modules/features/consent/ketch/mergers/biome.js.map +1 -0
  40. package/dist/modules/features/consent/ketch/mergers/check-integration.d.ts +13 -0
  41. package/dist/modules/features/consent/ketch/mergers/check-integration.d.ts.map +1 -0
  42. package/dist/modules/features/consent/ketch/mergers/check-integration.js +64 -0
  43. package/dist/modules/features/consent/ketch/mergers/check-integration.js.map +1 -0
  44. package/dist/modules/features/consent/ketch/mergers/env-example.d.ts +12 -0
  45. package/dist/modules/features/consent/ketch/mergers/env-example.d.ts.map +1 -0
  46. package/dist/modules/features/consent/ketch/mergers/env-example.js +28 -0
  47. package/dist/modules/features/consent/ketch/mergers/env-example.js.map +1 -0
  48. package/dist/modules/features/consent/ketch/mergers/footer.d.ts +17 -0
  49. package/dist/modules/features/consent/ketch/mergers/footer.d.ts.map +1 -0
  50. package/dist/modules/features/consent/ketch/mergers/footer.js +40 -0
  51. package/dist/modules/features/consent/ketch/mergers/footer.js.map +1 -0
  52. package/dist/modules/features/consent/ketch/mergers/knip.d.ts +18 -0
  53. package/dist/modules/features/consent/ketch/mergers/knip.d.ts.map +1 -0
  54. package/dist/modules/features/consent/ketch/mergers/knip.js +37 -0
  55. package/dist/modules/features/consent/ketch/mergers/knip.js.map +1 -0
  56. package/dist/modules/features/consent/ketch/mergers/layout.d.ts +25 -0
  57. package/dist/modules/features/consent/ketch/mergers/layout.d.ts.map +1 -0
  58. package/dist/modules/features/consent/ketch/mergers/layout.js +55 -0
  59. package/dist/modules/features/consent/ketch/mergers/layout.js.map +1 -0
  60. package/dist/modules/features/jobs/ashby/config.d.ts +19 -0
  61. package/dist/modules/features/jobs/ashby/config.d.ts.map +1 -0
  62. package/dist/modules/features/jobs/ashby/config.js +29 -0
  63. package/dist/modules/features/jobs/ashby/config.js.map +1 -0
  64. package/dist/modules/features/jobs/ashby/mergers/check-integration.d.ts +14 -0
  65. package/dist/modules/features/jobs/ashby/mergers/check-integration.d.ts.map +1 -0
  66. package/dist/modules/features/jobs/ashby/mergers/check-integration.js +78 -0
  67. package/dist/modules/features/jobs/ashby/mergers/check-integration.js.map +1 -0
  68. package/dist/modules/features/jobs/ashby/mergers/env-example.d.ts +12 -0
  69. package/dist/modules/features/jobs/ashby/mergers/env-example.d.ts.map +1 -0
  70. package/dist/modules/features/jobs/ashby/mergers/env-example.js +27 -0
  71. package/dist/modules/features/jobs/ashby/mergers/env-example.js.map +1 -0
  72. package/dist/modules/features/jobs/ashby/mergers/knip.d.ts +18 -0
  73. package/dist/modules/features/jobs/ashby/mergers/knip.d.ts.map +1 -0
  74. package/dist/modules/features/jobs/ashby/mergers/knip.js +41 -0
  75. package/dist/modules/features/jobs/ashby/mergers/knip.js.map +1 -0
  76. package/dist/paths.d.ts +12 -0
  77. package/dist/paths.d.ts.map +1 -1
  78. package/dist/paths.js +12 -0
  79. package/dist/paths.js.map +1 -1
  80. package/package.json +3 -1
  81. package/src/agent-skills/ashby-pagebuilder-block/SKILL.md +277 -0
  82. package/src/agent-skills/hubspot-pagebuilder-block/SKILL.md +58 -25
  83. package/src/agent-skills/ketch-consent-check/SKILL.md +197 -0
  84. package/src/agent-skills/ketch-consent-setup/SKILL.md +326 -0
  85. package/src/agent-skills/ketch-consent-setup/references/ui-customization.md +329 -0
  86. package/src/agent-skills/page-builder-block/SKILL.md +150 -0
  87. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/anchor-id-input.tsx +17 -0
  88. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/presentation-input.tsx +146 -0
  89. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/spacing-steps-input.tsx +84 -0
  90. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/fetchers/layout.ts +6 -3
  91. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/icons.ts +8 -0
  92. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/queries.ts +12 -0
  93. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/reusable/blog-content.ts +3 -3
  94. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/reusable/description.ts +3 -3
  95. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/reusable/gated-content.ts +3 -3
  96. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/reusable/hero.ts +3 -3
  97. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/reusable/rich-text-block.ts +3 -3
  98. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/singleton/content-collection.ts +3 -3
  99. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/index.ts +8 -0
  100. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/layout/spacing-settings.ts +120 -0
  101. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/anchor.ts +75 -0
  102. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/block.ts +46 -0
  103. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/presentation.ts +44 -0
  104. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/spacing.ts +164 -0
  105. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/singletons.ts +7 -0
  106. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/anchor-id-input.tsx +17 -0
  107. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/presentation-input.tsx +146 -0
  108. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/spacing-steps-input.tsx +84 -0
  109. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/fetchers/layout.ts +4 -2
  110. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/icons.ts +8 -0
  111. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/queries.ts +12 -0
  112. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/blog-content.ts +3 -3
  113. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/description.ts +3 -3
  114. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/gated-content.ts +3 -3
  115. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/hero.ts +3 -3
  116. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/rich-text-block.ts +3 -3
  117. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/singleton/content-collection.ts +3 -3
  118. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/index.ts +8 -0
  119. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/layout/spacing-settings.ts +120 -0
  120. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/anchor.ts +75 -0
  121. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/block.ts +46 -0
  122. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/presentation.ts +44 -0
  123. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/spacing.ts +164 -0
  124. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/singletons.ts +7 -0
  125. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/tags.ts +2 -0
  126. package/src/modules/features/consent/ketch/files/lib/integrations/check-integration.ts +25 -0
  127. package/src/modules/features/consent/ketch/files/lib/integrations/ketch/README.md +70 -0
  128. package/src/modules/features/consent/ketch/files/lib/integrations/ketch/categories.ts +20 -0
  129. package/src/modules/features/consent/ketch/files/lib/integrations/ketch/consent-store.ts +107 -0
  130. package/src/modules/features/consent/ketch/files/lib/integrations/ketch/env.ts +20 -0
  131. package/src/modules/features/consent/ketch/files/lib/integrations/ketch/ketch-experience.css +136 -0
  132. package/src/modules/features/consent/ketch/files/lib/integrations/ketch/preferences-button.tsx +51 -0
  133. package/src/modules/features/consent/ketch/files/lib/integrations/ketch/script.tsx +8 -0
  134. package/src/modules/features/consent/ketch/files/lib/integrations/ketch/types.ts +15 -0
  135. package/src/modules/features/consent/ketch/files/lib/integrations/ketch/use-consent.ts +26 -0
  136. package/src/modules/features/jobs/ashby/files/_sanity-adapter/job-board-block.ts +53 -0
  137. package/src/modules/features/jobs/ashby/files/lib/integrations/ashby/README.md +134 -0
  138. package/src/modules/features/jobs/ashby/files/lib/integrations/ashby/env.ts +3 -0
  139. package/src/modules/features/jobs/ashby/files/lib/integrations/ashby/index.ts +40 -0
  140. package/src/modules/features/jobs/ashby/files/lib/integrations/ashby/normalize.ts +124 -0
  141. package/src/modules/features/jobs/ashby/files/lib/integrations/ashby/types.ts +20 -0
  142. package/src/modules/features/jobs/ashby/files/lib/integrations/check-integration.ts +25 -0
  143. package/src/templates/next-pagebuilder/AGENTS.md +129 -0
  144. package/src/templates/next-pagebuilder/CLAUDE.md +3 -0
  145. package/src/templates/next-pagebuilder/README.md +70 -0
  146. package/src/templates/next-pagebuilder/app/(content)/layout.tsx +2 -0
  147. package/src/templates/next-pagebuilder/components/layout/spacing-scale/index.tsx +18 -0
  148. package/src/templates/next-pagebuilder/components/page-builder/components/article-content/index.tsx +1 -1
  149. package/src/templates/next-pagebuilder/components/page-builder/components/content-collection/index.tsx +2 -2
  150. package/src/templates/next-pagebuilder/components/page-builder/components/description.tsx +3 -7
  151. package/src/templates/next-pagebuilder/components/page-builder/components/gated-content/index.tsx +2 -2
  152. package/src/templates/next-pagebuilder/components/page-builder/components/hero.tsx +3 -7
  153. package/src/templates/next-pagebuilder/components/page-builder/components/rich-text-block.tsx +3 -5
  154. package/src/templates/next-pagebuilder/components/page-builder/registry.tsx +70 -0
  155. package/src/templates/next-pagebuilder/components/page-builder/renderer.tsx +33 -49
  156. package/src/templates/next-pagebuilder/components/page-builder/section.tsx +35 -0
  157. package/src/templates/next-pagebuilder/components/page-document/index.tsx +1 -1
  158. package/src/templates/next-pagebuilder/lib/integrations/sanity/sanity.types.ts +92 -4
  159. package/src/templates/next-pagebuilder-cache/AGENTS.md +136 -0
  160. package/src/templates/next-pagebuilder-cache/CLAUDE.md +3 -0
  161. package/src/templates/next-pagebuilder-cache/README.md +75 -0
  162. package/src/templates/next-pagebuilder-cache/app/(content)/layout.tsx +2 -0
  163. package/src/templates/next-pagebuilder-cache/components/layout/spacing-scale/index.tsx +46 -0
  164. package/src/templates/next-pagebuilder-cache/components/page-builder/components/article-content/index.tsx +1 -1
  165. package/src/templates/next-pagebuilder-cache/components/page-builder/components/content-collection/index.tsx +2 -2
  166. package/src/templates/next-pagebuilder-cache/components/page-builder/components/description.tsx +3 -7
  167. package/src/templates/next-pagebuilder-cache/components/page-builder/components/gated-content/index.tsx +2 -2
  168. package/src/templates/next-pagebuilder-cache/components/page-builder/components/hero.tsx +3 -7
  169. package/src/templates/next-pagebuilder-cache/components/page-builder/components/rich-text-block.tsx +3 -5
  170. package/src/templates/next-pagebuilder-cache/components/page-builder/registry.tsx +70 -0
  171. package/src/templates/next-pagebuilder-cache/components/page-builder/renderer.tsx +33 -49
  172. package/src/templates/next-pagebuilder-cache/components/page-builder/section.tsx +35 -0
  173. package/src/templates/next-pagebuilder-cache/components/page-document/index.tsx +1 -1
  174. package/src/templates/next-pagebuilder-cache/lib/integrations/sanity/sanity.types.ts +92 -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`.
@@ -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
+ }
@@ -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
+ }
@@ -8,6 +8,7 @@ export type NavbarData = NonNullable<LayoutData["navbar"]>;
8
8
  export type FooterData = NonNullable<LayoutData["footer"]>;
9
9
  export type CompanyData = NonNullable<LayoutData["companyData"]>;
10
10
  export type StructuredData = NonNullable<LayoutData["structuredData"]>;
11
+ export type SpacingSettings = NonNullable<LayoutData["spacingSettings"]>;
11
12
 
12
13
  // Singleton document types that, when published, should bust the layout cache.
13
14
  export const SANITY_LAYOUT_TAGS = [
@@ -15,6 +16,7 @@ export const SANITY_LAYOUT_TAGS = [
15
16
  "footer",
16
17
  "companyData",
17
18
  "structuredData",
19
+ "spacingSettings",
18
20
  ];
19
21
 
20
22
  const EMPTY_LAYOUT: LayoutData = {
@@ -22,11 +24,12 @@ const EMPTY_LAYOUT: LayoutData = {
22
24
  footer: null,
23
25
  companyData: null,
24
26
  structuredData: null,
27
+ spacingSettings: null,
25
28
  };
26
29
 
27
- // One query for all four layout singletons, wrapped in React cache so the header,
28
- // footer and json-ld share a single round trip. Tagged with all four `_type` tags
29
- // because it reads all four documents.
30
+ // One query for every layout singleton, wrapped in React cache so the header,
31
+ // footer, json-ld and spacing scale share a single round trip. Tagged with each
32
+ // `_type` because it reads all of those documents.
30
33
  export const getLayoutData = cache(async (): Promise<LayoutData> => {
31
34
  const { data } = await fetchSanity<LayoutData>({
32
35
  query: LAYOUT_QUERY,
@@ -2,6 +2,9 @@ import type { Icon, IconProps } from "@phosphor-icons/react";
2
2
  import {
3
3
  AppWindowIcon,
4
4
  ArrowBendUpRightIcon,
5
+ ArrowLineDownIcon,
6
+ ArrowLineUpIcon,
7
+ ArrowsOutLineVerticalIcon,
5
8
  ArticleNyTimesIcon,
6
9
  BracketsCurlyIcon,
7
10
  BriefcaseIcon,
@@ -75,6 +78,11 @@ export const richTextIcon = asDuotoneIcon(TextTIcon);
75
78
  export const contentIcon = asDuotoneIcon(ArticleNyTimesIcon);
76
79
  export const gatedContentIcon = asDuotoneIcon(LockKeyIcon);
77
80
 
81
+ export const spacingSettingsIcon = asDuotoneIcon(ArrowsOutLineVerticalIcon);
82
+
83
+ export const paddingTopIcon = asDuotoneIcon(ArrowLineUpIcon);
84
+ export const paddingBottomIcon = asDuotoneIcon(ArrowLineDownIcon);
85
+
78
86
  export const pageIcon = asDuotoneIcon(FileTextIcon);
79
87
  export const pagesFolderIcon = asDuotoneIcon(BrowsersIcon);
80
88
  export const pageFolderIcon = asDuotoneIcon(FolderOpenIcon);
@@ -96,6 +96,8 @@ const pageBuilderProjection = `
96
96
  _type == "gatedContent" => {
97
97
  _id,
98
98
  _type,
99
+ anchorId,
100
+ presentation,
99
101
  internalTitle,
100
102
  teaserHeading,
101
103
  teaserBody[]{
@@ -533,6 +535,16 @@ export const LAYOUT_QUERY = defineQuery(`
533
535
  },
534
536
  "structuredData": *[_type == "structuredData"][0]{
535
537
  ${structuredDataProjection}
538
+ },
539
+ "spacingSettings": *[_type == "spacingSettings"][0]{
540
+ steps[]{
541
+ _key,
542
+ name,
543
+ mobile,
544
+ tablet,
545
+ desktop,
546
+ isDefault
547
+ }
536
548
  }
537
549
  }
538
550
  `);
@@ -1,10 +1,10 @@
1
- import { defineField, defineType } from "sanity";
1
+ import { defineField } from "sanity";
2
2
  import { contentIcon } from "@/lib/integrations/sanity/icons";
3
+ import { defineBlock } from "@/lib/integrations/sanity/schemas/shared/block";
3
4
 
4
- export const blogContent = defineType({
5
+ export const blogContent = defineBlock({
5
6
  name: "blogContent",
6
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: [
@@ -23,6 +23,7 @@ import { companyData } from "./layout/company-data";
23
23
  import { footer } from "./layout/footer";
24
24
  import { navbar } from "./layout/navbar";
25
25
  import { redirect } from "./layout/redirect";
26
+ import { spacingSettings, spacingStep } from "./layout/spacing-settings";
26
27
  import { structuredData } from "./layout/structured-data";
27
28
  import { externalLink, link, pageReference } from "./shared/link";
28
29
  import { metadata } from "./shared/metadata";
@@ -35,6 +36,7 @@ import {
35
36
  import { page } from "./shared/page";
36
37
  import { pageBuilder } from "./shared/page-builder";
37
38
  import { pageFolder } from "./shared/page-folder";
39
+ import { presentation } from "./shared/presentation";
38
40
  import { richText } from "./shared/richText";
39
41
  import {
40
42
  customJsonLd,
@@ -74,9 +76,12 @@ export {
74
76
  pageBuilder,
75
77
  pageFolder,
76
78
  pageReference,
79
+ presentation,
77
80
  redirect,
78
81
  richText,
79
82
  softwareApplicationJsonLd,
83
+ spacingSettings,
84
+ spacingStep,
80
85
  structuredData,
81
86
  studioPreview,
82
87
  videoJsonLd,
@@ -91,6 +96,7 @@ export const schema: { types: SchemaTypeDefinition[] } = {
91
96
  link,
92
97
  metadata,
93
98
  pageBuilder,
99
+ presentation,
94
100
  richText,
95
101
 
96
102
  // Page-level JSON-LD blocks (selectable in metadata)
@@ -122,6 +128,8 @@ export const schema: { types: SchemaTypeDefinition[] } = {
122
128
  navbar,
123
129
  footer,
124
130
  structuredData,
131
+ spacingStep,
132
+ spacingSettings,
125
133
 
126
134
  // Layout collections (managed lists, not singletons)
127
135
  redirect,