bsmnt 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. package/dist/application/add-integration/index.d.ts.map +1 -1
  2. package/dist/application/add-integration/index.js +23 -0
  3. package/dist/application/add-integration/index.js.map +1 -1
  4. package/dist/domain/consent.d.ts +17 -0
  5. package/dist/domain/consent.d.ts.map +1 -0
  6. package/dist/domain/consent.js +13 -0
  7. package/dist/domain/consent.js.map +1 -0
  8. package/dist/domain/integration.d.ts +19 -2
  9. package/dist/domain/integration.d.ts.map +1 -1
  10. package/dist/domain/integration.js +10 -1
  11. package/dist/domain/integration.js.map +1 -1
  12. package/dist/domain/jobs.d.ts +17 -0
  13. package/dist/domain/jobs.d.ts.map +1 -0
  14. package/dist/domain/jobs.js +13 -0
  15. package/dist/domain/jobs.js.map +1 -0
  16. package/dist/domain/skills.d.ts +23 -2
  17. package/dist/domain/skills.d.ts.map +1 -1
  18. package/dist/domain/skills.js +23 -2
  19. package/dist/domain/skills.js.map +1 -1
  20. package/dist/domain/templates.d.ts +7 -1
  21. package/dist/domain/templates.d.ts.map +1 -1
  22. package/dist/domain/templates.js +2 -0
  23. package/dist/domain/templates.js.map +1 -1
  24. package/dist/index.js +34 -9
  25. package/dist/index.js.map +1 -1
  26. package/dist/infrastructure/create/setup-agent.d.ts.map +1 -1
  27. package/dist/infrastructure/create/setup-agent.js +11 -4
  28. package/dist/infrastructure/create/setup-agent.js.map +1 -1
  29. package/dist/infrastructure/integrations/inject-integration.d.ts.map +1 -1
  30. package/dist/infrastructure/integrations/inject-integration.js +36 -2
  31. package/dist/infrastructure/integrations/inject-integration.js.map +1 -1
  32. package/dist/modules/features/consent/ketch/config.d.ts +18 -0
  33. package/dist/modules/features/consent/ketch/config.d.ts.map +1 -0
  34. package/dist/modules/features/consent/ketch/config.js +24 -0
  35. package/dist/modules/features/consent/ketch/config.js.map +1 -0
  36. package/dist/modules/features/consent/ketch/mergers/biome.d.ts +16 -0
  37. package/dist/modules/features/consent/ketch/mergers/biome.d.ts.map +1 -0
  38. package/dist/modules/features/consent/ketch/mergers/biome.js +31 -0
  39. package/dist/modules/features/consent/ketch/mergers/biome.js.map +1 -0
  40. package/dist/modules/features/consent/ketch/mergers/check-integration.d.ts +13 -0
  41. package/dist/modules/features/consent/ketch/mergers/check-integration.d.ts.map +1 -0
  42. package/dist/modules/features/consent/ketch/mergers/check-integration.js +64 -0
  43. package/dist/modules/features/consent/ketch/mergers/check-integration.js.map +1 -0
  44. package/dist/modules/features/consent/ketch/mergers/env-example.d.ts +12 -0
  45. package/dist/modules/features/consent/ketch/mergers/env-example.d.ts.map +1 -0
  46. package/dist/modules/features/consent/ketch/mergers/env-example.js +28 -0
  47. package/dist/modules/features/consent/ketch/mergers/env-example.js.map +1 -0
  48. package/dist/modules/features/consent/ketch/mergers/footer.d.ts +17 -0
  49. package/dist/modules/features/consent/ketch/mergers/footer.d.ts.map +1 -0
  50. package/dist/modules/features/consent/ketch/mergers/footer.js +40 -0
  51. package/dist/modules/features/consent/ketch/mergers/footer.js.map +1 -0
  52. package/dist/modules/features/consent/ketch/mergers/knip.d.ts +18 -0
  53. package/dist/modules/features/consent/ketch/mergers/knip.d.ts.map +1 -0
  54. package/dist/modules/features/consent/ketch/mergers/knip.js +37 -0
  55. package/dist/modules/features/consent/ketch/mergers/knip.js.map +1 -0
  56. package/dist/modules/features/consent/ketch/mergers/layout.d.ts +25 -0
  57. package/dist/modules/features/consent/ketch/mergers/layout.d.ts.map +1 -0
  58. package/dist/modules/features/consent/ketch/mergers/layout.js +55 -0
  59. package/dist/modules/features/consent/ketch/mergers/layout.js.map +1 -0
  60. package/dist/modules/features/jobs/ashby/config.d.ts +19 -0
  61. package/dist/modules/features/jobs/ashby/config.d.ts.map +1 -0
  62. package/dist/modules/features/jobs/ashby/config.js +29 -0
  63. package/dist/modules/features/jobs/ashby/config.js.map +1 -0
  64. package/dist/modules/features/jobs/ashby/mergers/check-integration.d.ts +14 -0
  65. package/dist/modules/features/jobs/ashby/mergers/check-integration.d.ts.map +1 -0
  66. package/dist/modules/features/jobs/ashby/mergers/check-integration.js +78 -0
  67. package/dist/modules/features/jobs/ashby/mergers/check-integration.js.map +1 -0
  68. package/dist/modules/features/jobs/ashby/mergers/env-example.d.ts +12 -0
  69. package/dist/modules/features/jobs/ashby/mergers/env-example.d.ts.map +1 -0
  70. package/dist/modules/features/jobs/ashby/mergers/env-example.js +27 -0
  71. package/dist/modules/features/jobs/ashby/mergers/env-example.js.map +1 -0
  72. package/dist/modules/features/jobs/ashby/mergers/knip.d.ts +18 -0
  73. package/dist/modules/features/jobs/ashby/mergers/knip.d.ts.map +1 -0
  74. package/dist/modules/features/jobs/ashby/mergers/knip.js +41 -0
  75. package/dist/modules/features/jobs/ashby/mergers/knip.js.map +1 -0
  76. package/dist/paths.d.ts +12 -0
  77. package/dist/paths.d.ts.map +1 -1
  78. package/dist/paths.js +12 -0
  79. package/dist/paths.js.map +1 -1
  80. package/package.json +3 -1
  81. package/src/agent-skills/ashby-pagebuilder-block/SKILL.md +277 -0
  82. package/src/agent-skills/hubspot-pagebuilder-block/SKILL.md +58 -25
  83. package/src/agent-skills/ketch-consent-check/SKILL.md +197 -0
  84. package/src/agent-skills/ketch-consent-setup/SKILL.md +326 -0
  85. package/src/agent-skills/ketch-consent-setup/references/ui-customization.md +329 -0
  86. package/src/agent-skills/page-builder-block/SKILL.md +150 -0
  87. package/src/modules/features/cms/sanity/files/components/ui/sanity-image/index.tsx +36 -4
  88. package/src/modules/features/cms/sanity/files/lib/integrations/sanity/components/rich-text.tsx +7 -1
  89. package/src/modules/features/cms/sanity-pagebuilder/files/components/sanity/rich-text.tsx +7 -1
  90. package/src/modules/features/cms/sanity-pagebuilder/files/components/ui/sanity-image/index.tsx +36 -4
  91. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/anchor-id-input.tsx +17 -0
  92. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/page-builder-input.tsx +26 -7
  93. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/page-builder-insert-menu.tsx +242 -0
  94. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/presentation-input.tsx +146 -0
  95. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/spacing-steps-input.tsx +84 -0
  96. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/use-studio-preview-urls.ts +62 -0
  97. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/fetchers/layout.ts +6 -3
  98. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/icons.ts +8 -0
  99. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/page-builder-config.ts +72 -34
  100. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/queries.ts +12 -0
  101. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/index.ts +1 -0
  102. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/reusable/blog-content.ts +3 -3
  103. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/reusable/description.ts +3 -3
  104. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/reusable/gated-content.ts +3 -3
  105. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/reusable/hero.ts +3 -3
  106. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/reusable/rich-text-block.ts +3 -3
  107. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/singleton/content-collection.ts +3 -3
  108. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/studio-preview.ts +37 -0
  109. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/index.ts +11 -0
  110. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/layout/spacing-settings.ts +120 -0
  111. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/anchor.ts +75 -0
  112. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/block.ts +46 -0
  113. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/presentation.ts +44 -0
  114. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/spacing.ts +164 -0
  115. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/singletons.ts +9 -0
  116. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/structure.ts +5 -0
  117. package/src/modules/features/cms/sanity-pagebuilder-cache/files/components/sanity/rich-text.tsx +7 -1
  118. package/src/modules/features/cms/sanity-pagebuilder-cache/files/components/ui/sanity-image/index.tsx +36 -4
  119. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/anchor-id-input.tsx +17 -0
  120. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/page-builder-input.tsx +26 -7
  121. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/page-builder-insert-menu.tsx +242 -0
  122. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/presentation-input.tsx +146 -0
  123. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/spacing-steps-input.tsx +84 -0
  124. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/use-studio-preview-urls.ts +62 -0
  125. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/fetchers/layout.ts +4 -2
  126. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/icons.ts +8 -0
  127. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/page-builder-config.ts +72 -34
  128. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/queries.ts +12 -0
  129. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/index.ts +1 -0
  130. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/blog-content.ts +3 -3
  131. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/description.ts +3 -3
  132. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/gated-content.ts +3 -3
  133. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/hero.ts +3 -3
  134. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/rich-text-block.ts +3 -3
  135. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/singleton/content-collection.ts +3 -3
  136. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/studio-preview.ts +37 -0
  137. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/index.ts +11 -0
  138. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/layout/spacing-settings.ts +120 -0
  139. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/anchor.ts +75 -0
  140. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/block.ts +46 -0
  141. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/presentation.ts +44 -0
  142. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/spacing.ts +164 -0
  143. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/singletons.ts +9 -0
  144. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/structure.ts +5 -0
  145. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/tags.ts +2 -0
  146. package/src/modules/features/consent/ketch/files/lib/integrations/check-integration.ts +25 -0
  147. package/src/modules/features/consent/ketch/files/lib/integrations/ketch/README.md +70 -0
  148. package/src/modules/features/consent/ketch/files/lib/integrations/ketch/categories.ts +20 -0
  149. package/src/modules/features/consent/ketch/files/lib/integrations/ketch/consent-store.ts +107 -0
  150. package/src/modules/features/consent/ketch/files/lib/integrations/ketch/env.ts +20 -0
  151. package/src/modules/features/consent/ketch/files/lib/integrations/ketch/ketch-experience.css +136 -0
  152. package/src/modules/features/consent/ketch/files/lib/integrations/ketch/preferences-button.tsx +51 -0
  153. package/src/modules/features/consent/ketch/files/lib/integrations/ketch/script.tsx +8 -0
  154. package/src/modules/features/consent/ketch/files/lib/integrations/ketch/types.ts +15 -0
  155. package/src/modules/features/consent/ketch/files/lib/integrations/ketch/use-consent.ts +26 -0
  156. package/src/modules/features/jobs/ashby/files/_sanity-adapter/job-board-block.ts +53 -0
  157. package/src/modules/features/jobs/ashby/files/lib/integrations/ashby/README.md +134 -0
  158. package/src/modules/features/jobs/ashby/files/lib/integrations/ashby/env.ts +3 -0
  159. package/src/modules/features/jobs/ashby/files/lib/integrations/ashby/index.ts +40 -0
  160. package/src/modules/features/jobs/ashby/files/lib/integrations/ashby/normalize.ts +124 -0
  161. package/src/modules/features/jobs/ashby/files/lib/integrations/ashby/types.ts +20 -0
  162. package/src/modules/features/jobs/ashby/files/lib/integrations/check-integration.ts +25 -0
  163. package/src/templates/next-default/.biome/plugins/no-fill-without-sizes.grit +11 -0
  164. package/src/templates/next-default/biome.json +2 -1
  165. package/src/templates/next-default/components/ui/image/index.tsx +20 -12
  166. package/src/templates/next-default/next.config.ts +10 -4
  167. package/src/templates/next-experiments/.biome/plugins/no-fill-without-sizes.grit +11 -0
  168. package/src/templates/next-experiments/biome.json +2 -1
  169. package/src/templates/next-experiments/components/ui/image/index.tsx +20 -12
  170. package/src/templates/next-experiments/next.config.ts +10 -4
  171. package/src/templates/next-pagebuilder/.biome/plugins/no-fill-without-sizes.grit +11 -0
  172. package/src/templates/next-pagebuilder/AGENTS.md +129 -0
  173. package/src/templates/next-pagebuilder/CLAUDE.md +3 -0
  174. package/src/templates/next-pagebuilder/README.md +70 -0
  175. package/src/templates/next-pagebuilder/app/(content)/layout.tsx +2 -0
  176. package/src/templates/next-pagebuilder/biome.json +2 -1
  177. package/src/templates/next-pagebuilder/components/layout/spacing-scale/index.tsx +18 -0
  178. package/src/templates/next-pagebuilder/components/page-builder/components/article-content/index.tsx +6 -1
  179. package/src/templates/next-pagebuilder/components/page-builder/components/article-content/related-post-item.tsx +1 -0
  180. package/src/templates/next-pagebuilder/components/page-builder/components/content-collection/index.tsx +2 -2
  181. package/src/templates/next-pagebuilder/components/page-builder/components/description.tsx +3 -7
  182. package/src/templates/next-pagebuilder/components/page-builder/components/gated-content/index.tsx +2 -2
  183. package/src/templates/next-pagebuilder/components/page-builder/components/hero.tsx +3 -7
  184. package/src/templates/next-pagebuilder/components/page-builder/components/rich-text-block.tsx +3 -5
  185. package/src/templates/next-pagebuilder/components/page-builder/registry.tsx +70 -0
  186. package/src/templates/next-pagebuilder/components/page-builder/renderer.tsx +33 -49
  187. package/src/templates/next-pagebuilder/components/page-builder/section.tsx +35 -0
  188. package/src/templates/next-pagebuilder/components/page-document/index.tsx +1 -1
  189. package/src/templates/next-pagebuilder/components/ui/image/index.tsx +20 -12
  190. package/src/templates/next-pagebuilder/lib/integrations/sanity/sanity.types.ts +92 -4
  191. package/src/templates/next-pagebuilder/next.config.ts +4 -3
  192. package/src/templates/next-pagebuilder-cache/.biome/plugins/no-fill-without-sizes.grit +11 -0
  193. package/src/templates/next-pagebuilder-cache/AGENTS.md +136 -0
  194. package/src/templates/next-pagebuilder-cache/CLAUDE.md +3 -0
  195. package/src/templates/next-pagebuilder-cache/README.md +75 -0
  196. package/src/templates/next-pagebuilder-cache/app/(content)/layout.tsx +2 -0
  197. package/src/templates/next-pagebuilder-cache/biome.json +2 -1
  198. package/src/templates/next-pagebuilder-cache/components/layout/spacing-scale/index.tsx +46 -0
  199. package/src/templates/next-pagebuilder-cache/components/page-builder/components/article-content/index.tsx +6 -1
  200. package/src/templates/next-pagebuilder-cache/components/page-builder/components/article-content/related-post-item.tsx +1 -0
  201. package/src/templates/next-pagebuilder-cache/components/page-builder/components/content-collection/index.tsx +2 -2
  202. package/src/templates/next-pagebuilder-cache/components/page-builder/components/description.tsx +3 -7
  203. package/src/templates/next-pagebuilder-cache/components/page-builder/components/gated-content/index.tsx +2 -2
  204. package/src/templates/next-pagebuilder-cache/components/page-builder/components/hero.tsx +3 -7
  205. package/src/templates/next-pagebuilder-cache/components/page-builder/components/rich-text-block.tsx +3 -5
  206. package/src/templates/next-pagebuilder-cache/components/page-builder/registry.tsx +70 -0
  207. package/src/templates/next-pagebuilder-cache/components/page-builder/renderer.tsx +33 -49
  208. package/src/templates/next-pagebuilder-cache/components/page-builder/section.tsx +35 -0
  209. package/src/templates/next-pagebuilder-cache/components/page-document/index.tsx +1 -1
  210. package/src/templates/next-pagebuilder-cache/components/ui/image/index.tsx +21 -12
  211. package/src/templates/next-pagebuilder-cache/lib/integrations/sanity/sanity.types.ts +92 -4
  212. package/src/templates/next-pagebuilder-cache/lib/utils/image-sizes.ts +72 -0
  213. package/src/templates/next-pagebuilder-cache/next.config.ts +4 -3
  214. package/src/templates/next-webgl/.biome/plugins/no-fill-without-sizes.grit +11 -0
  215. package/src/templates/next-webgl/biome.json +2 -1
  216. package/src/templates/next-webgl/components/ui/image/index.tsx +20 -12
  217. package/src/templates/next-webgl/next.config.ts +10 -4
@@ -0,0 +1,51 @@
1
+ "use client"
2
+
3
+ import { cva, type VariantProps } from "class-variance-authority"
4
+ import type { ComponentPropsWithoutRef } from "react"
5
+ import { isKetchConfigured } from "@/lib/integrations/check-integration"
6
+ import { cn } from "@/lib/styles/cn"
7
+ import { useConsent } from "./use-consent"
8
+
9
+ const preferencesButton = cva("w-fit text-left transition-colors", {
10
+ variants: {
11
+ variant: {
12
+ footer: "text-secondary/60 hover:text-secondary",
13
+ strong: "text-secondary hover:text-secondary/60",
14
+ link: "text-secondary underline underline-offset-4 hover:no-underline",
15
+ },
16
+ size: {
17
+ sm: "text-sm",
18
+ md: "text-base",
19
+ },
20
+ },
21
+ defaultVariants: {
22
+ variant: "footer",
23
+ size: "md",
24
+ },
25
+ })
26
+
27
+ type ConsentPreferencesButtonProps = ComponentPropsWithoutRef<"button"> &
28
+ VariantProps<typeof preferencesButton>
29
+
30
+ export const ConsentPreferencesButton = ({
31
+ className,
32
+ variant,
33
+ size,
34
+ children,
35
+ ...props
36
+ }: ConsentPreferencesButtonProps) => {
37
+ const { openPreferences } = useConsent()
38
+
39
+ if (!isKetchConfigured()) return null
40
+
41
+ return (
42
+ <button
43
+ type="button"
44
+ onClick={openPreferences}
45
+ className={cn(preferencesButton({ variant, size }), className)}
46
+ {...props}
47
+ >
48
+ {children ?? "Privacy Choices"}
49
+ </button>
50
+ )
51
+ }
@@ -0,0 +1,8 @@
1
+ import { ketchBootUrl } from "./env"
2
+ import "./ketch-experience.css"
3
+
4
+ export const KetchConsentScript = () => {
5
+ if (!ketchBootUrl) return null
6
+
7
+ return <script async src={ketchBootUrl} />
8
+ }
@@ -0,0 +1,15 @@
1
+ export type KetchPurposeValue = boolean | "true" | "false" | undefined
2
+
3
+ export type KetchConsent = {
4
+ purposes?: Record<string, KetchPurposeValue>
5
+ vendors?: string[]
6
+ }
7
+
8
+ export type KetchJurisdictionEvent = string | { code?: string } | undefined
9
+
10
+ export type KetchSemaphore = unknown[] & { loaded?: boolean }
11
+
12
+ export type KetchWindow = Window & {
13
+ ketch?: (...command: unknown[]) => void
14
+ semaphore?: KetchSemaphore
15
+ }
@@ -0,0 +1,26 @@
1
+ "use client"
2
+
3
+ import { useCallback, useSyncExternalStore } from "react"
4
+ import type { ConsentCategory } from "./categories"
5
+ import { consentStore } from "./consent-store"
6
+
7
+ export const useConsent = () => {
8
+ const { isResolved, jurisdiction, granted } = useSyncExternalStore(
9
+ consentStore.subscribe,
10
+ consentStore.getSnapshot,
11
+ consentStore.getServerSnapshot
12
+ )
13
+
14
+ const hasConsent = useCallback(
15
+ (category: ConsentCategory) => granted[category],
16
+ [granted]
17
+ )
18
+
19
+ return {
20
+ hasConsent,
21
+ granted,
22
+ isResolved,
23
+ jurisdiction,
24
+ openPreferences: consentStore.openPreferences,
25
+ }
26
+ }
@@ -0,0 +1,53 @@
1
+ import { CaseIcon } from "@sanity/icons"
2
+ import { defineField, defineType } from "sanity"
3
+
4
+ export const ashbyJobBoardBlock = defineType({
5
+ name: "ashbyJobBoardBlock",
6
+ title: "Ashby Job Board",
7
+ type: "document",
8
+ icon: CaseIcon,
9
+ description:
10
+ "Lists open roles pulled live from Ashby. There is no content to manage here beyond an optional heading.",
11
+ fields: [
12
+ defineField({
13
+ name: "internalTitle",
14
+ title: "Internal Title",
15
+ type: "string",
16
+ description: "Only used to identify this block in the Studio.",
17
+ validation: (Rule) => Rule.required(),
18
+ }),
19
+ defineField({
20
+ name: "heading",
21
+ title: "Heading",
22
+ type: "string",
23
+ description: "Optional heading shown above the list of positions.",
24
+ }),
25
+ defineField({
26
+ name: "boardName",
27
+ title: "Job Board Name",
28
+ type: "string",
29
+ description:
30
+ "Optional. Overrides the ASHBY_JOB_BOARD_NAME environment variable — the board's slug in its jobs.ashbyhq.com URL.",
31
+ }),
32
+ ],
33
+ preview: {
34
+ select: {
35
+ title: "internalTitle",
36
+ heading: "heading",
37
+ boardName: "boardName",
38
+ },
39
+ prepare({ title, heading, boardName }) {
40
+ return {
41
+ title: title || "Ashby Job Board",
42
+ subtitle: heading || boardName || "Open positions",
43
+ }
44
+ },
45
+ },
46
+ })
47
+
48
+ export const ashbyJobBoardBlockReference = defineType({
49
+ name: "ashbyJobBoardBlockReference",
50
+ title: "Ashby Job Board",
51
+ type: "reference",
52
+ to: [{ type: "ashbyJobBoardBlock" }],
53
+ })
@@ -0,0 +1,134 @@
1
+ # Ashby job board integration
2
+
3
+ Agnostic, reusable [Ashby](https://www.ashbyhq.com/) job-board reader. The core
4
+ (`index.ts`, `normalize.ts`, `types.ts`) has **no CMS dependency** and adds **no
5
+ runtime packages** — it reads Ashby's public Job Posting API and normalizes it
6
+ into a stable `CareerJob[]`.
7
+
8
+ Fetching is **server-only**: `getCareerJobs` uses `react`'s `cache` and Next.js
9
+ `fetch` revalidation, so call it from a Server Component, Route Handler, or
10
+ `generateMetadata` — never from a `"use client"` component.
11
+
12
+ ## 1. Environment
13
+
14
+ Ashby's job-board endpoint is public — no token or private app required. The
15
+ only configuration is the board name, which is the slug in your board's URL
16
+ (`https://jobs.ashbyhq.com/<board-name>`):
17
+
18
+ ```bash
19
+ ASHBY_JOB_BOARD_NAME=your-board
20
+ ```
21
+
22
+ The value is not a secret, so it can stay plaintext in the committed env file.
23
+ It is only read server-side (in `env.ts`); it is never exposed to the client.
24
+ You can also skip the variable entirely and pass the board name explicitly.
25
+
26
+ `env.ts` resolves it once at module scope as `ashbyBoardName`, and
27
+ `isAshbyConfigured()` in `lib/integrations/check-integration.ts` derives from
28
+ that same value — so a "configured" check can never disagree with what
29
+ `getCareerJobs` actually resolves.
30
+
31
+ ## 2. Read the jobs
32
+
33
+ ```tsx
34
+ import { getCareerJobs } from "@/lib/integrations/ashby"
35
+
36
+ export default async function Careers() {
37
+ const jobs = await getCareerJobs()
38
+
39
+ return (
40
+ <ul>
41
+ {jobs.map((job) => (
42
+ <li key={job.id}>
43
+ <a href={job.href}>{job.title}</a>
44
+ </li>
45
+ ))}
46
+ </ul>
47
+ )
48
+ }
49
+ ```
50
+
51
+ Pass a board name to override `ASHBY_JOB_BOARD_NAME` (useful for multi-brand
52
+ sites, or to render a second board on one page):
53
+
54
+ ```tsx
55
+ const jobs = await getCareerJobs("your-other-board")
56
+ ```
57
+
58
+ `getCareerJobs` throws when the board name is missing or when Ashby responds
59
+ with a non-2xx status. Wrap the call so a job-board outage degrades gracefully
60
+ instead of failing the whole page:
61
+
62
+ ```tsx
63
+ let jobs: CareerJob[] = []
64
+ try {
65
+ jobs = await getCareerJobs()
66
+ } catch (error) {
67
+ console.error("Failed to load Ashby jobs", error)
68
+ }
69
+ ```
70
+
71
+ ## 3. What you get
72
+
73
+ `normalize.ts` tolerates both the current (`jobs`) and legacy (`jobPostings`)
74
+ payload shapes, drops unlisted postings, and resolves each field from the
75
+ several key spellings Ashby has used over time. Every optional field is
76
+ `string | null` — never `undefined` — so rendering stays branch-free:
77
+
78
+ | Field | Notes |
79
+ | --- | --- |
80
+ | `id` / `title` / `href` | Required; a posting missing any of them is dropped |
81
+ | `applyUrl` | Direct application link when Ashby exposes one |
82
+ | `location` / `department` / `team` | Resolved from the flat or nested variants |
83
+ | `employmentType` | e.g. `Full-time` |
84
+ | `workType` | `workplaceType`, or derived from `isRemote` |
85
+ | `isRemote` | `true` only when Ashby says so explicitly |
86
+ | `publishedAt` | ISO date string |
87
+ | `descriptionHtml` | Raw HTML — sanitize before injecting |
88
+ | `compensation` | `{ summary, display }`; respect `display` before rendering |
89
+
90
+ ## 4. Caching
91
+
92
+ The core caches the board for 5 minutes via `next: { revalidate: 300 }`, and
93
+ deduplicates calls within a single request via React `cache`.
94
+
95
+ In a **Cache Components** project (`cacheComponents: true`), the fetch-level
96
+ revalidate is ignored — wrap the call in your own cached function instead:
97
+
98
+ ```tsx
99
+ import { cacheLife } from "next/cache"
100
+ import { getCareerJobs } from "@/lib/integrations/ashby"
101
+
102
+ async function getCachedJobs() {
103
+ "use cache"
104
+ cacheLife("minutes")
105
+ return getCareerJobs()
106
+ }
107
+ ```
108
+
109
+ ## 5. Optional: Sanity Studio block
110
+
111
+ When this integration is added to a project that already has Sanity, a
112
+ `sanity/` folder ships alongside the core:
113
+
114
+ - `sanity/job-board-block.ts` — an `ashbyJobBoardBlock` document type with an
115
+ optional `heading` and an optional per-block `boardName` override
116
+
117
+ Register it by adding `ashbyJobBoardBlock` to your schema's document list, then
118
+ render it from your own component.
119
+
120
+ ## 6. Use as a page-builder block
121
+
122
+ In a basement **Sanity page-builder** project, `ashbyJobBoardBlock` can be a
123
+ selectable block. Adding this integration installs the
124
+ **`ashby-pagebuilder-block`** agent skill — ask your coding agent to "wire the
125
+ Ashby job board into the page builder" and it makes the edits and verifies the
126
+ typecheck. To do it by hand:
127
+
128
+ 1. `schemas/index.ts` — import `ashbyJobBoardBlock`, add it to `schema.types`
129
+ 2. `page-builder-config.ts` — add `"ashbyJobBoardBlock"` to
130
+ `PageBuilderDocumentType` and an `ashbyJobBoardBlockReference` member
131
+ 3. `structure.ts` — add `S.documentTypeListItem("ashbyJobBoardBlock")`
132
+ 4. `components/page-builder/renderer.tsx` — add a `case "ashbyJobBoardBlock"`
133
+ that awaits `getCareerJobs(block.boardName)` and renders the list
134
+ 5. `bun run sanity:typegen && bun typecheck`
@@ -0,0 +1,3 @@
1
+ const readBoardName = (value: string | undefined) => value?.trim() ?? ""
2
+
3
+ export const ashbyBoardName = readBoardName(process.env.ASHBY_JOB_BOARD_NAME)
@@ -0,0 +1,40 @@
1
+ import { cache } from "react"
2
+ import { ashbyBoardName } from "./env"
3
+ import { normalizeBoard } from "./normalize"
4
+ import type { CareerJob } from "./types"
5
+
6
+ const ASHBY_JOB_BOARD_API_URL = "https://api.ashbyhq.com/posting-api/job-board"
7
+ const BOARD_REVALIDATE_SECONDS = 300
8
+
9
+ export function resolveAshbyBoardName(boardName?: string | null): string {
10
+ const resolved = boardName?.trim() || ashbyBoardName
11
+
12
+ if (!resolved) {
13
+ throw new Error(
14
+ "Missing Ashby job board name. Pass it to getCareerJobs() or set ASHBY_JOB_BOARD_NAME."
15
+ )
16
+ }
17
+
18
+ return resolved
19
+ }
20
+
21
+ export const getCareerJobs = cache(
22
+ async (boardName?: string | null): Promise<CareerJob[]> => {
23
+ const board = resolveAshbyBoardName(boardName)
24
+
25
+ const response = await fetch(
26
+ `${ASHBY_JOB_BOARD_API_URL}/${encodeURIComponent(board)}?includeCompensation=true`,
27
+ { next: { revalidate: BOARD_REVALIDATE_SECONDS } }
28
+ )
29
+
30
+ if (!response.ok) {
31
+ throw new Error(`Ashby request failed with ${response.status}`)
32
+ }
33
+
34
+ return normalizeBoard(await response.json(), board)
35
+ }
36
+ )
37
+
38
+ export { ashbyBoardName } from "./env"
39
+ export { normalizeBoard } from "./normalize"
40
+ export type { CareerCompensation, CareerJob } from "./types"
@@ -0,0 +1,124 @@
1
+ import type { CareerCompensation, CareerJob } from "./types"
2
+
3
+ const toRecord = (value: unknown): Record<string, unknown> | null =>
4
+ value && typeof value === "object" ? (value as Record<string, unknown>) : null
5
+
6
+ const string = (value: unknown): string | null =>
7
+ typeof value === "string" && value.trim() ? value.trim() : null
8
+
9
+ const getFieldValue = (
10
+ job: Record<string, unknown>,
11
+ key: string
12
+ ): string | null => {
13
+ const value = job[key]
14
+ if (typeof value === "string") return string(value)
15
+
16
+ const nested = toRecord(value)
17
+ if (!nested) return null
18
+
19
+ return string(nested.name) ?? string(nested.title) ?? string(nested.value)
20
+ }
21
+
22
+ const idFromUrl = (url: string | null): string | null => {
23
+ if (!url) return null
24
+
25
+ try {
26
+ const parsed = new URL(url)
27
+ const fromQuery = string(parsed.searchParams.get("ashby_jid"))
28
+ if (fromQuery) return fromQuery
29
+
30
+ const segments = parsed.pathname.split("/").filter(Boolean)
31
+ const last = segments.at(-1)
32
+ if (last === "application") return string(segments.at(-2))
33
+ return string(last)
34
+ } catch {
35
+ return null
36
+ }
37
+ }
38
+
39
+ const getCompensation = (
40
+ job: Record<string, unknown>
41
+ ): CareerCompensation | null => {
42
+ const compensation = toRecord(job.compensation)
43
+ if (!compensation) return null
44
+
45
+ const summary =
46
+ string(compensation.compensationTierSummary) ??
47
+ string(compensation.scrapeableCompensationSalarySummary)
48
+ if (!summary) return null
49
+
50
+ return {
51
+ summary,
52
+ display: job.shouldDisplayCompensationOnJobPostings !== false,
53
+ }
54
+ }
55
+
56
+ const getWorkType = (job: Record<string, unknown>): string | null => {
57
+ const workplaceType = getFieldValue(job, "workplaceType")
58
+ if (workplaceType) return workplaceType
59
+ if (job.isRemote === true) return "Remote"
60
+ return job.isRemote === false ? "On-site" : null
61
+ }
62
+
63
+ const toCareerJob = (value: unknown, boardName: string): CareerJob | null => {
64
+ const job = toRecord(value)
65
+ if (!job) return null
66
+
67
+ if (job.isListed === false) return null
68
+
69
+ const jobUrl = string(job.jobUrl)
70
+ const applyUrl = string(job.applyUrl)
71
+
72
+ const id =
73
+ string(job.id) ??
74
+ string(job.jobId) ??
75
+ string(job.jobPostingId) ??
76
+ string(job.ashbyJid) ??
77
+ idFromUrl(jobUrl) ??
78
+ idFromUrl(applyUrl)
79
+
80
+ const title = getFieldValue(job, "title")
81
+ const href =
82
+ jobUrl ??
83
+ applyUrl ??
84
+ (id
85
+ ? `https://jobs.ashbyhq.com/${encodeURIComponent(boardName)}/${id}`
86
+ : null)
87
+
88
+ if (!(id && title && href)) return null
89
+
90
+ return {
91
+ id,
92
+ title,
93
+ href,
94
+ applyUrl,
95
+ location:
96
+ getFieldValue(job, "location") ?? getFieldValue(job, "locationName"),
97
+ department:
98
+ getFieldValue(job, "department") ?? getFieldValue(job, "departmentName"),
99
+ team: getFieldValue(job, "team") ?? getFieldValue(job, "teamName"),
100
+ employmentType:
101
+ getFieldValue(job, "employmentType") ?? getFieldValue(job, "jobType"),
102
+ workType: getWorkType(job),
103
+ publishedAt: string(job.publishedAt) ?? string(job.publishedDate) ?? null,
104
+ isRemote: job.isRemote === true,
105
+ descriptionHtml: string(job.descriptionHtml),
106
+ compensation: getCompensation(job),
107
+ }
108
+ }
109
+
110
+ export const normalizeBoard = (
111
+ payload: unknown,
112
+ boardName: string
113
+ ): CareerJob[] => {
114
+ const board = toRecord(payload)
115
+ if (!board) return []
116
+
117
+ const jobs = Array.isArray(board.jobs) ? board.jobs : null
118
+ const legacy = Array.isArray(board.jobPostings) ? board.jobPostings : null
119
+ const raw = jobs ?? legacy ?? []
120
+
121
+ return raw
122
+ .map((job) => toCareerJob(job, boardName))
123
+ .filter((job): job is CareerJob => job !== null)
124
+ }
@@ -0,0 +1,20 @@
1
+ export type CareerCompensation = {
2
+ summary: string
3
+ display: boolean
4
+ }
5
+
6
+ export type CareerJob = {
7
+ id: string
8
+ title: string
9
+ href: string
10
+ applyUrl: string | null
11
+ location: string | null
12
+ department: string | null
13
+ team: string | null
14
+ employmentType: string | null
15
+ workType: string | null
16
+ publishedAt: string | null
17
+ isRemote: boolean
18
+ descriptionHtml: string | null
19
+ compensation: CareerCompensation | null
20
+ }
@@ -0,0 +1,25 @@
1
+ import { ashbyBoardName } from "@/lib/integrations/ashby/env"
2
+
3
+ /**
4
+ * Integration Configuration Checker
5
+ *
6
+ * Utilities to check if integrations are configured via environment variables.
7
+ * This helps with tree-shaking unused integrations from the bundle.
8
+ *
9
+ * Usage:
10
+ * ```ts
11
+ * import { isAshbyConfigured } from '@/integrations/check-integration'
12
+ *
13
+ * if (isAshbyConfigured()) {
14
+ * // Only fetch the job board once a board name is set
15
+ * }
16
+ * ```
17
+ */
18
+
19
+ /**
20
+ * Check if the Ashby job board is configured. Derived from env.ts, not a
21
+ * second read of `process.env`.
22
+ */
23
+ export function isAshbyConfigured(): boolean {
24
+ return Boolean(ashbyBoardName)
25
+ }
@@ -0,0 +1,11 @@
1
+ language js
2
+
3
+ or {
4
+ `<Image $attrs />`,
5
+ `<SanityImage $attrs />`
6
+ } as $image where {
7
+ $attrs <: contains `fill`,
8
+ ! $attrs <: contains `fill={false}`,
9
+ ! $attrs <: contains or { `sizes`, `mobileSize`, `desktopSize` },
10
+ register_diagnostic(span=$image, message="A `fill` image has no intrinsic width, so without `sizes` the browser assumes 100vw and requests the largest candidate. Pass `sizes`, or `mobileSize`/`desktopSize`.", severity="error")
11
+ }
@@ -234,7 +234,8 @@
234
234
  "plugins": [
235
235
  "./.biome/plugins/no-anchor-element.grit",
236
236
  "./.biome/plugins/no-unnecessary-forwardref.grit",
237
- "./.biome/plugins/no-relative-parent-imports.grit"
237
+ "./.biome/plugins/no-relative-parent-imports.grit",
238
+ "./.biome/plugins/no-fill-without-sizes.grit"
238
239
  ],
239
240
  "vcs": {
240
241
  "clientKind": "git",
@@ -98,7 +98,6 @@ const getFinalPlaceholder = (
98
98
  * Enhanced Image component with responsive sizing and automatic optimizations.
99
99
  *
100
100
  * Always use this component instead of next/image directly. Provides:
101
- * - Automatic responsive sizes generation
102
101
  * - Smart blur placeholders with aspect ratio support
103
102
  * - Performance optimizations (lazy loading by default)
104
103
  * - Priority flag for LCP images
@@ -147,14 +146,14 @@ export function Image({
147
146
  style,
148
147
  className,
149
148
  objectFit = "cover",
150
- quality = 90,
149
+ quality = 75,
151
150
  alt = "",
152
151
  fill,
153
152
  block = !fill,
154
- width = block ? 1 : undefined,
155
- height = block ? 1 : undefined,
156
- mobileSize = "100vw",
157
- desktopSize = "100vw",
153
+ width,
154
+ height,
155
+ mobileSize,
156
+ desktopSize,
158
157
  sizes,
159
158
  src,
160
159
  unoptimized,
@@ -164,9 +163,18 @@ export function Image({
164
163
  priority = false,
165
164
  ...props
166
165
  }: ImageProps) {
167
- // Generate responsive sizes if not provided
168
- const finalSizes =
169
- sizes || buildImageSizes({ base: mobileSize, desktop: desktopSize })
166
+ const responsiveSizes =
167
+ mobileSize || desktopSize
168
+ ? buildImageSizes({
169
+ base: mobileSize ?? "100vw",
170
+ desktop: desktopSize ?? mobileSize ?? "100vw",
171
+ })
172
+ : undefined
173
+
174
+ const hasIntrinsicSize = width !== undefined && height !== undefined
175
+ const finalSizes = sizes ?? responsiveSizes ?? (hasIntrinsicSize ? undefined : "100vw")
176
+ const finalWidth = width ?? (block ? 1 : undefined)
177
+ const finalHeight = height ?? (block ? 1 : undefined)
170
178
 
171
179
  // Early return after hooks
172
180
  if (!src) return null
@@ -194,8 +202,8 @@ export function Image({
194
202
  <NextImage
195
203
  ref={ref}
196
204
  fill={!block}
197
- {...(width !== undefined && { width })}
198
- {...(height !== undefined && { height })}
205
+ {...(finalWidth !== undefined && { width: finalWidth })}
206
+ {...(finalHeight !== undefined && { height: finalHeight })}
199
207
  priority={priority}
200
208
  quality={quality}
201
209
  alt={alt}
@@ -204,7 +212,7 @@ export function Image({
204
212
  ...style,
205
213
  }}
206
214
  className={cn(className, block && "block w-full")}
207
- sizes={finalSizes}
215
+ {...(finalSizes && { sizes: finalSizes })}
208
216
  src={src}
209
217
  unoptimized={unoptimized || isSvg}
210
218
  draggable={false}
@@ -77,12 +77,18 @@ const nextConfig: NextConfig = {
77
77
  ],
78
78
  },
79
79
  images: {
80
- dangerouslyAllowSVG: true,
81
- remotePatterns: [],
80
+ remotePatterns: [
81
+ {
82
+ protocol: "https",
83
+ hostname: "cdn.sanity.io",
84
+ },
85
+ ],
82
86
  minimumCacheTTL: 60 * 60 * 24 * 30, // 30 days
83
- qualities: [90],
87
+ qualities: [75, 90, 100],
84
88
  formats: ["image/avif", "image/webp"],
85
- deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
89
+ deviceSizes: [
90
+ 640, 750, 828, 1080, 1200, 1320, 1620, 1920, 2048, 2560, 3024, 3456, 3840,
91
+ ],
86
92
  imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
87
93
  },
88
94
  headers: async () => [
@@ -0,0 +1,11 @@
1
+ language js
2
+
3
+ or {
4
+ `<Image $attrs />`,
5
+ `<SanityImage $attrs />`
6
+ } as $image where {
7
+ $attrs <: contains `fill`,
8
+ ! $attrs <: contains `fill={false}`,
9
+ ! $attrs <: contains or { `sizes`, `mobileSize`, `desktopSize` },
10
+ register_diagnostic(span=$image, message="A `fill` image has no intrinsic width, so without `sizes` the browser assumes 100vw and requests the largest candidate. Pass `sizes`, or `mobileSize`/`desktopSize`.", severity="error")
11
+ }
@@ -229,7 +229,8 @@
229
229
  "plugins": [
230
230
  "./.biome/plugins/no-anchor-element.grit",
231
231
  "./.biome/plugins/no-unnecessary-forwardref.grit",
232
- "./.biome/plugins/no-relative-parent-imports.grit"
232
+ "./.biome/plugins/no-relative-parent-imports.grit",
233
+ "./.biome/plugins/no-fill-without-sizes.grit"
233
234
  ],
234
235
  "vcs": {
235
236
  "clientKind": "git",