bsmnt 0.10.0 → 0.12.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 (343) hide show
  1. package/dist/application/add-integration/index.js.map +1 -1
  2. package/dist/application/shared/node-version.js.map +1 -1
  3. package/dist/core/create/internal-ids.d.ts +3 -0
  4. package/dist/core/create/internal-ids.d.ts.map +1 -1
  5. package/dist/core/create/internal-ids.js +4 -0
  6. package/dist/core/create/internal-ids.js.map +1 -1
  7. package/dist/domain/agents.d.ts.map +1 -1
  8. package/dist/domain/animations.d.ts.map +1 -1
  9. package/dist/domain/cms.d.ts.map +1 -1
  10. package/dist/domain/forms.d.ts.map +1 -1
  11. package/dist/domain/hooks.d.ts.map +1 -1
  12. package/dist/domain/skills.d.ts.map +1 -1
  13. package/dist/domain/templates.d.ts +10 -0
  14. package/dist/domain/templates.d.ts.map +1 -1
  15. package/dist/domain/templates.js +5 -0
  16. package/dist/domain/templates.js.map +1 -1
  17. package/dist/index.js +18 -5
  18. package/dist/index.js.map +1 -1
  19. package/dist/infrastructure/agents/register-sanity-mcp.js +3 -8
  20. package/dist/infrastructure/agents/register-sanity-mcp.js.map +1 -1
  21. package/dist/infrastructure/agents/register-storybook-mcp.d.ts +18 -0
  22. package/dist/infrastructure/agents/register-storybook-mcp.d.ts.map +1 -0
  23. package/dist/infrastructure/agents/register-storybook-mcp.js +157 -0
  24. package/dist/infrastructure/agents/register-storybook-mcp.js.map +1 -0
  25. package/dist/infrastructure/create/lifecycle-registry.d.ts.map +1 -1
  26. package/dist/infrastructure/create/lifecycle-registry.js +39 -1
  27. package/dist/infrastructure/create/lifecycle-registry.js.map +1 -1
  28. package/dist/infrastructure/create/merge-registry.d.ts.map +1 -1
  29. package/dist/infrastructure/create/merge-registry.js +4 -0
  30. package/dist/infrastructure/create/merge-registry.js.map +1 -1
  31. package/dist/infrastructure/create/sanity-auth.js.map +1 -1
  32. package/dist/infrastructure/create/setup-agent.d.ts +6 -1
  33. package/dist/infrastructure/create/setup-agent.d.ts.map +1 -1
  34. package/dist/infrastructure/create/setup-agent.js +23 -21
  35. package/dist/infrastructure/create/setup-agent.js.map +1 -1
  36. package/dist/infrastructure/create/setup-dotenvx.js.map +1 -1
  37. package/dist/infrastructure/create/setup-remote.js.map +1 -1
  38. package/dist/infrastructure/create/setup-sanity.d.ts.map +1 -1
  39. package/dist/infrastructure/create/setup-sanity.js +12 -1
  40. package/dist/infrastructure/create/setup-sanity.js.map +1 -1
  41. package/dist/infrastructure/integrations/inject-integration.d.ts.map +1 -1
  42. package/dist/infrastructure/integrations/inject-integration.js +2 -0
  43. package/dist/infrastructure/integrations/inject-integration.js.map +1 -1
  44. package/dist/infrastructure/update/notifier.js.map +1 -1
  45. package/dist/modules/features/cms/index.d.ts.map +1 -1
  46. package/dist/modules/features/cms/index.js +2 -0
  47. package/dist/modules/features/cms/index.js.map +1 -1
  48. package/dist/modules/features/cms/sanity/config.d.ts.map +1 -1
  49. package/dist/modules/features/cms/sanity/config.js +1 -0
  50. package/dist/modules/features/cms/sanity/config.js.map +1 -1
  51. package/dist/modules/features/cms/sanity/index.d.ts.map +1 -1
  52. package/dist/modules/features/cms/sanity/index.js +10 -1
  53. package/dist/modules/features/cms/sanity/index.js.map +1 -1
  54. package/dist/modules/features/cms/sanity/mergers/check-integration.d.ts.map +1 -1
  55. package/dist/modules/features/cms/sanity/mergers/check-integration.js +15 -6
  56. package/dist/modules/features/cms/sanity/mergers/check-integration.js.map +1 -1
  57. package/dist/modules/features/cms/sanity/mergers/knip.d.ts.map +1 -1
  58. package/dist/modules/features/cms/sanity/mergers/knip.js +1 -0
  59. package/dist/modules/features/cms/sanity/mergers/knip.js.map +1 -1
  60. package/dist/modules/features/cms/sanity/mergers/layout.js.map +1 -1
  61. package/dist/modules/features/cms/sanity/mergers/package-json.d.ts +12 -0
  62. package/dist/modules/features/cms/sanity/mergers/package-json.d.ts.map +1 -0
  63. package/dist/modules/features/cms/sanity/mergers/package-json.js +19 -0
  64. package/dist/modules/features/cms/sanity/mergers/package-json.js.map +1 -0
  65. package/dist/modules/features/cms/sanity-pagebuilder/index.d.ts.map +1 -1
  66. package/dist/modules/features/cms/sanity-pagebuilder/index.js +7 -1
  67. package/dist/modules/features/cms/sanity-pagebuilder/index.js.map +1 -1
  68. package/dist/modules/features/cms/sanity-pagebuilder-cache/config.d.ts +14 -0
  69. package/dist/modules/features/cms/sanity-pagebuilder-cache/config.d.ts.map +1 -0
  70. package/dist/modules/features/cms/sanity-pagebuilder-cache/config.js +50 -0
  71. package/dist/modules/features/cms/sanity-pagebuilder-cache/config.js.map +1 -0
  72. package/dist/modules/features/cms/sanity-pagebuilder-cache/index.d.ts +3 -0
  73. package/dist/modules/features/cms/sanity-pagebuilder-cache/index.d.ts.map +1 -0
  74. package/dist/modules/features/cms/sanity-pagebuilder-cache/index.js +55 -0
  75. package/dist/modules/features/cms/sanity-pagebuilder-cache/index.js.map +1 -0
  76. package/dist/modules/presets/index.d.ts.map +1 -1
  77. package/dist/modules/presets/index.js +2 -0
  78. package/dist/modules/presets/index.js.map +1 -1
  79. package/dist/modules/presets/next-pagebuilder-cache.d.ts +3 -0
  80. package/dist/modules/presets/next-pagebuilder-cache.d.ts.map +1 -0
  81. package/dist/modules/presets/next-pagebuilder-cache.js +10 -0
  82. package/dist/modules/presets/next-pagebuilder-cache.js.map +1 -0
  83. package/dist/paths.d.ts +6 -0
  84. package/dist/paths.d.ts.map +1 -1
  85. package/dist/paths.js +6 -0
  86. package/dist/paths.js.map +1 -1
  87. package/package.json +5 -2
  88. package/src/modules/features/cms/sanity/files/components/ui/sanity-image/index.tsx +36 -4
  89. package/src/modules/features/cms/sanity/files/lib/integrations/check-integration.ts +7 -6
  90. package/src/modules/features/cms/sanity/files/lib/integrations/sanity/README.md +1 -1
  91. package/src/modules/features/cms/sanity/files/lib/integrations/sanity/components/rich-text.tsx +8 -1
  92. package/src/modules/features/cms/sanity/files/lib/integrations/sanity/env.ts +2 -2
  93. package/src/modules/features/cms/sanity-pagebuilder/files/app/api/[[...slug]]/route.ts +4 -0
  94. package/src/modules/features/cms/sanity-pagebuilder/files/app/api/revalidate/route.ts +172 -24
  95. package/src/modules/features/cms/sanity-pagebuilder/files/app/feed.xml/route.ts +14 -7
  96. package/src/modules/features/cms/sanity-pagebuilder/files/app/sitemap.md/route.ts +2 -2
  97. package/src/modules/features/cms/sanity-pagebuilder/files/app/studio/[[...tool]]/page.tsx +16 -1
  98. package/src/modules/features/cms/sanity-pagebuilder/files/components/sanity/rich-text.tsx +8 -1
  99. package/src/modules/features/cms/sanity-pagebuilder/files/components/sanity/visual-editing.tsx +2 -2
  100. package/src/modules/features/cms/sanity-pagebuilder/files/components/ui/sanity-image/index.tsx +36 -4
  101. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/check-integration.ts +6 -5
  102. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/client.ts +8 -0
  103. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/page-builder-input.tsx +26 -7
  104. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/page-builder-insert-menu.tsx +242 -0
  105. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/slug-notices.tsx +6 -16
  106. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/use-studio-preview-urls.ts +62 -0
  107. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/env.ts +2 -2
  108. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/fetchers/layout.ts +22 -52
  109. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/icons.ts +2 -2
  110. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/live/index.tsx +3 -1
  111. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/page-builder-config.ts +78 -33
  112. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/page-tree-pane.tsx +70 -57
  113. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/queries.ts +119 -29
  114. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/revalidate-paths.ts +70 -0
  115. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/index.ts +2 -0
  116. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/reusable/rich-text-block.ts +39 -0
  117. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/studio-preview.ts +37 -0
  118. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/index.ts +5 -0
  119. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/page.ts +12 -0
  120. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/singletons.ts +2 -0
  121. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/structure.ts +5 -0
  122. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/tags.ts +56 -0
  123. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/utils/page-builder-markdown.ts +12 -0
  124. package/src/modules/features/cms/sanity-pagebuilder/files/lib/utils/sitemap.ts +11 -8
  125. package/src/modules/features/cms/sanity-pagebuilder-cache/files/.knip.json +8 -0
  126. package/src/modules/features/cms/sanity-pagebuilder-cache/files/app/api/[[...slug]]/route.ts +112 -0
  127. package/src/modules/features/cms/sanity-pagebuilder-cache/files/app/api/draft-mode/disable/route.ts +7 -0
  128. package/src/modules/features/cms/sanity-pagebuilder-cache/files/app/api/draft-mode/enable/route.ts +20 -0
  129. package/src/modules/features/cms/sanity-pagebuilder-cache/files/app/api/revalidate/route.ts +292 -0
  130. package/src/modules/features/cms/sanity-pagebuilder-cache/files/app/feed.xml/route.ts +66 -0
  131. package/src/modules/features/cms/sanity-pagebuilder-cache/files/app/sitemap.md/route.ts +132 -0
  132. package/src/modules/features/cms/sanity-pagebuilder-cache/files/app/studio/[[...tool]]/layout.tsx +26 -0
  133. package/src/modules/features/cms/sanity-pagebuilder-cache/files/app/studio/[[...tool]]/page.tsx +23 -0
  134. package/src/modules/features/cms/sanity-pagebuilder-cache/files/components/sanity/draft-mode-toggle.tsx +27 -0
  135. package/src/modules/features/cms/sanity-pagebuilder-cache/files/components/sanity/rich-text.tsx +94 -0
  136. package/src/modules/features/cms/sanity-pagebuilder-cache/files/components/sanity/visual-editing.tsx +11 -0
  137. package/src/modules/features/cms/sanity-pagebuilder-cache/files/components/ui/sanity-image/index.tsx +83 -0
  138. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/check-integration.ts +6 -0
  139. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/client.ts +37 -0
  140. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/block-actions-item.tsx +81 -0
  141. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/disable-draft-mode.tsx +23 -0
  142. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/page-builder-input.tsx +58 -0
  143. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/page-builder-insert-menu.tsx +242 -0
  144. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/page-category-input.tsx +50 -0
  145. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/same-as-input.tsx +66 -0
  146. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/slug-field.tsx +24 -0
  147. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/slug-input.tsx +14 -0
  148. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/slug-notices.tsx +125 -0
  149. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/use-studio-preview-urls.ts +62 -0
  150. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/confirm-publish-action.ts +40 -0
  151. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/env.ts +34 -0
  152. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/fetchers/layout.ts +39 -0
  153. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/fetchers/redirects.ts +22 -0
  154. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/icons.ts +81 -0
  155. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/live/index.tsx +140 -0
  156. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/markdown-proxy.config.ts +50 -0
  157. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/page-builder-config.ts +182 -0
  158. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/page-category.ts +28 -0
  159. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/page-tree-pane.tsx +431 -0
  160. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/pinned-pages.ts +54 -0
  161. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/presentation.ts +119 -0
  162. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/queries.ts +548 -0
  163. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/revalidate-paths.ts +70 -0
  164. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/sanity.cli.ts +29 -0
  165. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/sanity.config.ts +116 -0
  166. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/index.ts +7 -0
  167. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/blog-content.ts +89 -0
  168. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/description.ts +29 -0
  169. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/gated-content.ts +178 -0
  170. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/hero.ts +28 -0
  171. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/rich-text-block.ts +39 -0
  172. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/singleton/content-collection.ts +45 -0
  173. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/studio-preview.ts +37 -0
  174. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/content/author.ts +70 -0
  175. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/content/blog-category.ts +55 -0
  176. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/index.ts +137 -0
  177. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/layout/company-data.ts +70 -0
  178. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/layout/footer.ts +79 -0
  179. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/layout/navbar.ts +75 -0
  180. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/layout/redirect.ts +152 -0
  181. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/layout/structured-data.ts +124 -0
  182. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/link.ts +125 -0
  183. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/logo-field.ts +9 -0
  184. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/metadata.ts +132 -0
  185. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/nav-objects.ts +192 -0
  186. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/page-builder.ts +43 -0
  187. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/page-folder.ts +124 -0
  188. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/page.ts +265 -0
  189. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/richText.ts +63 -0
  190. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/structured-data-blocks.ts +436 -0
  191. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/singletons.ts +53 -0
  192. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/structure.ts +339 -0
  193. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/tags.ts +89 -0
  194. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/utils/image.ts +8 -0
  195. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/utils/link.ts +140 -0
  196. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/utils/page-builder-markdown.ts +113 -0
  197. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/utils/page-json-ld.ts +208 -0
  198. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/scripts/sanity-typegen.ts +36 -0
  199. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/utils/check-redirect.ts +30 -0
  200. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/utils/json-ld.tsx +522 -0
  201. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/utils/metadata.ts +189 -0
  202. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/utils/rss.ts +96 -0
  203. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/utils/sitemap.ts +58 -0
  204. package/src/modules/features/cms/sanity-pagebuilder-cache/files/proxy.ts +90 -0
  205. package/src/modules/features/env/dotenvx/files/vercelignore +5 -0
  206. package/src/templates/next-default/.biome/plugins/no-fill-without-sizes.grit +11 -0
  207. package/src/templates/next-default/.env.example +2 -2
  208. package/src/templates/next-default/.github/workflows/ci.yml +1 -1
  209. package/src/templates/next-default/.vscode/settings.json +0 -2
  210. package/src/templates/next-default/biome.json +2 -1
  211. package/src/templates/next-default/components/ui/image/index.tsx +21 -15
  212. package/src/templates/next-default/knip.json +1 -1
  213. package/src/templates/next-default/lefthook.yml +1 -1
  214. package/src/templates/next-default/lib/utils/README.md +18 -5
  215. package/src/templates/next-default/lib/utils/image-sizes.ts +72 -0
  216. package/src/templates/next-default/lib/utils/merge-refs.ts +56 -0
  217. package/src/templates/next-default/lib/utils/strings.ts +1 -51
  218. package/src/templates/next-default/next.config.ts +12 -7
  219. package/src/templates/next-default/package.json +2 -5
  220. package/src/templates/next-experiments/.biome/plugins/no-fill-without-sizes.grit +11 -0
  221. package/src/templates/next-experiments/.env.example +2 -2
  222. package/src/templates/next-experiments/.github/workflows/ci.yml +1 -1
  223. package/src/templates/next-experiments/.vscode/settings.json +0 -2
  224. package/src/templates/next-experiments/app/page.tsx +1 -1
  225. package/src/templates/next-experiments/biome.json +2 -1
  226. package/src/templates/next-experiments/components/ui/image/index.tsx +21 -15
  227. package/src/templates/next-experiments/knip.json +1 -1
  228. package/src/templates/next-experiments/lefthook.yml +1 -1
  229. package/src/templates/next-experiments/lib/utils/README.md +18 -5
  230. package/src/templates/next-experiments/lib/utils/image-sizes.ts +72 -0
  231. package/src/templates/next-experiments/lib/utils/merge-refs.ts +56 -0
  232. package/src/templates/next-experiments/lib/utils/strings.ts +1 -51
  233. package/src/templates/next-experiments/next.config.ts +12 -7
  234. package/src/templates/next-experiments/package.json +2 -5
  235. package/src/templates/next-pagebuilder/.biome/plugins/no-fill-without-sizes.grit +11 -0
  236. package/src/templates/next-pagebuilder/.env.example +1 -0
  237. package/src/templates/next-pagebuilder/.github/workflows/ci.yml +1 -1
  238. package/src/templates/next-pagebuilder/.vscode/settings.json +0 -2
  239. package/src/templates/next-pagebuilder/README.md +64 -0
  240. package/src/templates/next-pagebuilder/app/sitemap.xml/route.ts +3 -1
  241. package/src/templates/next-pagebuilder/biome.json +4 -2
  242. package/src/templates/next-pagebuilder/components/layout/footer/index.tsx +3 -6
  243. package/src/templates/next-pagebuilder/components/layout/header/index.tsx +3 -2
  244. package/src/templates/next-pagebuilder/components/layout/json-ld/index.tsx +3 -8
  245. package/src/templates/next-pagebuilder/components/page-builder/components/article-content/index.tsx +5 -0
  246. package/src/templates/next-pagebuilder/components/page-builder/components/article-content/related-post-item.tsx +1 -0
  247. package/src/templates/next-pagebuilder/components/page-builder/components/content-collection/content-filters.tsx +23 -40
  248. package/src/templates/next-pagebuilder/components/page-builder/components/content-collection/index.tsx +88 -58
  249. package/src/templates/next-pagebuilder/components/page-builder/components/content-collection/skeleton.tsx +4 -16
  250. package/src/templates/next-pagebuilder/components/page-builder/components/content-collection/types.ts +14 -1
  251. package/src/templates/next-pagebuilder/components/page-builder/components/rich-text-block.tsx +16 -0
  252. package/src/templates/next-pagebuilder/components/page-builder/renderer.tsx +21 -7
  253. package/src/templates/next-pagebuilder/components/page-document/index.tsx +1 -0
  254. package/src/templates/next-pagebuilder/components/ui/image/index.tsx +21 -12
  255. package/src/templates/next-pagebuilder/knip.json +3 -2
  256. package/src/templates/next-pagebuilder/lefthook.yml +1 -1
  257. package/src/templates/next-pagebuilder/lib/integrations/sanity/sanity.types.ts +1221 -977
  258. package/src/templates/next-pagebuilder/lib/utils/image-sizes.ts +72 -0
  259. package/src/templates/next-pagebuilder/lib/utils/merge-refs.ts +56 -0
  260. package/src/templates/next-pagebuilder/next.config.ts +6 -6
  261. package/src/templates/next-pagebuilder/package.json +4 -6
  262. package/src/templates/next-pagebuilder-cache/.biome/plugins/README.md +21 -0
  263. package/src/templates/next-pagebuilder-cache/.biome/plugins/no-anchor-element.grit +12 -0
  264. package/src/templates/next-pagebuilder-cache/.biome/plugins/no-fill-without-sizes.grit +11 -0
  265. package/src/templates/next-pagebuilder-cache/.biome/plugins/no-relative-parent-imports.grit +10 -0
  266. package/src/templates/next-pagebuilder-cache/.biome/plugins/no-unnecessary-forwardref.grit +9 -0
  267. package/src/templates/next-pagebuilder-cache/.env.example +40 -0
  268. package/src/templates/next-pagebuilder-cache/.github/PULL_REQUEST_TEMPLATE.md +13 -0
  269. package/src/templates/next-pagebuilder-cache/.github/workflows/ci.yml +105 -0
  270. package/src/templates/next-pagebuilder-cache/.vscode/extensions.json +19 -0
  271. package/src/templates/next-pagebuilder-cache/.vscode/settings.json +103 -0
  272. package/src/templates/next-pagebuilder-cache/.zed/settings.json +47 -0
  273. package/src/templates/next-pagebuilder-cache/README.md +212 -0
  274. package/src/templates/next-pagebuilder-cache/_gitignore +65 -0
  275. package/src/templates/next-pagebuilder-cache/app/(content)/[[...slug]]/page.tsx +124 -0
  276. package/src/templates/next-pagebuilder-cache/app/(content)/layout.tsx +28 -0
  277. package/src/templates/next-pagebuilder-cache/app/actions/unlock-gated-content.ts +53 -0
  278. package/src/templates/next-pagebuilder-cache/app/favicon.ico +0 -0
  279. package/src/templates/next-pagebuilder-cache/app/layout.tsx +83 -0
  280. package/src/templates/next-pagebuilder-cache/app/robots.ts +13 -0
  281. package/src/templates/next-pagebuilder-cache/app/sitemap.xml/route.ts +84 -0
  282. package/src/templates/next-pagebuilder-cache/biome.json +258 -0
  283. package/src/templates/next-pagebuilder-cache/components/layout/footer/index.tsx +127 -0
  284. package/src/templates/next-pagebuilder-cache/components/layout/header/components/cta-button.tsx +28 -0
  285. package/src/templates/next-pagebuilder-cache/components/layout/header/components/mega-menu-panel.tsx +90 -0
  286. package/src/templates/next-pagebuilder-cache/components/layout/header/components/nav-item-renderer.tsx +98 -0
  287. package/src/templates/next-pagebuilder-cache/components/layout/header/components/nav-leaf-item.tsx +33 -0
  288. package/src/templates/next-pagebuilder-cache/components/layout/header/components/types.ts +7 -0
  289. package/src/templates/next-pagebuilder-cache/components/layout/header/header-client.tsx +110 -0
  290. package/src/templates/next-pagebuilder-cache/components/layout/header/index.tsx +32 -0
  291. package/src/templates/next-pagebuilder-cache/components/layout/json-ld/index.tsx +79 -0
  292. package/src/templates/next-pagebuilder-cache/components/layout/wrapper/index.tsx +33 -0
  293. package/src/templates/next-pagebuilder-cache/components/page-builder/components/article-content/index.tsx +83 -0
  294. package/src/templates/next-pagebuilder-cache/components/page-builder/components/article-content/related-post-item.tsx +28 -0
  295. package/src/templates/next-pagebuilder-cache/components/page-builder/components/content-collection/content-card.tsx +66 -0
  296. package/src/templates/next-pagebuilder-cache/components/page-builder/components/content-collection/content-filters.tsx +76 -0
  297. package/src/templates/next-pagebuilder-cache/components/page-builder/components/content-collection/content-grid.tsx +40 -0
  298. package/src/templates/next-pagebuilder-cache/components/page-builder/components/content-collection/content-pagination-nav.tsx +71 -0
  299. package/src/templates/next-pagebuilder-cache/components/page-builder/components/content-collection/index.tsx +306 -0
  300. package/src/templates/next-pagebuilder-cache/components/page-builder/components/content-collection/skeleton.tsx +41 -0
  301. package/src/templates/next-pagebuilder-cache/components/page-builder/components/content-collection/types.ts +32 -0
  302. package/src/templates/next-pagebuilder-cache/components/page-builder/components/description.tsx +17 -0
  303. package/src/templates/next-pagebuilder-cache/components/page-builder/components/gated-content/index.tsx +169 -0
  304. package/src/templates/next-pagebuilder-cache/components/page-builder/components/hero.tsx +17 -0
  305. package/src/templates/next-pagebuilder-cache/components/page-builder/components/rich-text-block.tsx +16 -0
  306. package/src/templates/next-pagebuilder-cache/components/page-builder/renderer.tsx +71 -0
  307. package/src/templates/next-pagebuilder-cache/components/page-builder/types.ts +25 -0
  308. package/src/templates/next-pagebuilder-cache/components/page-document/index.tsx +147 -0
  309. package/src/templates/next-pagebuilder-cache/components/ui/image/index.tsx +225 -0
  310. package/src/templates/next-pagebuilder-cache/components/ui/link/index.tsx +152 -0
  311. package/src/templates/next-pagebuilder-cache/components.json +24 -0
  312. package/src/templates/next-pagebuilder-cache/knip.json +35 -0
  313. package/src/templates/next-pagebuilder-cache/lefthook.yml +25 -0
  314. package/src/templates/next-pagebuilder-cache/lib/integrations/sanity/sanity.types.ts +1588 -0
  315. package/src/templates/next-pagebuilder-cache/lib/scripts/setup-ci-secrets.ts +117 -0
  316. package/src/templates/next-pagebuilder-cache/lib/styles/cn.ts +5 -0
  317. package/src/templates/next-pagebuilder-cache/lib/styles/global.css +69 -0
  318. package/src/templates/next-pagebuilder-cache/lib/utils/base-url.ts +17 -0
  319. package/src/templates/next-pagebuilder-cache/lib/utils/format-date.ts +8 -0
  320. package/src/templates/next-pagebuilder-cache/lib/utils/image-sizes.ts +72 -0
  321. package/src/templates/next-pagebuilder-cache/lib/utils/slug-tag.ts +6 -0
  322. package/src/templates/next-pagebuilder-cache/next.config.ts +177 -0
  323. package/src/templates/next-pagebuilder-cache/package.json +62 -0
  324. package/src/templates/next-pagebuilder-cache/postcss.config.mjs +65 -0
  325. package/src/templates/next-pagebuilder-cache/svg.d.ts +6 -0
  326. package/src/templates/next-pagebuilder-cache/tsconfig.json +38 -0
  327. package/src/templates/next-webgl/.biome/plugins/no-fill-without-sizes.grit +11 -0
  328. package/src/templates/next-webgl/.env.example +2 -2
  329. package/src/templates/next-webgl/.github/workflows/ci.yml +1 -1
  330. package/src/templates/next-webgl/.vscode/settings.json +0 -2
  331. package/src/templates/next-webgl/biome.json +2 -1
  332. package/src/templates/next-webgl/components/ui/image/index.tsx +21 -15
  333. package/src/templates/next-webgl/knip.json +1 -1
  334. package/src/templates/next-webgl/lefthook.yml +1 -1
  335. package/src/templates/next-webgl/lib/utils/README.md +18 -5
  336. package/src/templates/next-webgl/lib/utils/image-sizes.ts +72 -0
  337. package/src/templates/next-webgl/lib/utils/merge-refs.ts +56 -0
  338. package/src/templates/next-webgl/lib/utils/strings.ts +1 -51
  339. package/src/templates/next-webgl/next.config.ts +12 -7
  340. package/src/templates/next-webgl/package.json +4 -7
  341. package/src/templates/next-experiments/tsconfig.tsbuildinfo +0 -1
  342. package/src/templates/next-pagebuilder/app/actions/refresh.ts +0 -5
  343. package/src/templates/next-webgl/tsconfig.tsbuildinfo +0 -1
@@ -0,0 +1,96 @@
1
+ export interface RssFeedItem {
2
+ title: string;
3
+ link: string;
4
+ description?: string | undefined;
5
+ pubDate?: string | undefined;
6
+ authors?: string[] | undefined;
7
+ category?: string | undefined;
8
+ }
9
+
10
+ export interface RssFeedConfig {
11
+ title: string;
12
+ link: string;
13
+ description: string;
14
+ feedUrl: string;
15
+ language?: string;
16
+ items: RssFeedItem[];
17
+ }
18
+
19
+ function escapeXml(str: string): string {
20
+ return str
21
+ .replace(/&/g, "&")
22
+ .replace(/</g, "&lt;")
23
+ .replace(/>/g, "&gt;")
24
+ .replace(/"/g, "&quot;")
25
+ .replace(/'/g, "&apos;");
26
+ }
27
+
28
+ export function generateRssFeed(config: RssFeedConfig): Response {
29
+ const {
30
+ title,
31
+ link,
32
+ description,
33
+ feedUrl,
34
+ language = "en-us",
35
+ items,
36
+ } = config;
37
+
38
+ const itemsXml = items
39
+ .map((item) => {
40
+ const lines = [
41
+ ` <title>${escapeXml(item.title)}</title>`,
42
+ ` <link>${escapeXml(item.link)}</link>`,
43
+ ` <guid isPermaLink="true">${escapeXml(item.link)}</guid>`,
44
+ ];
45
+
46
+ if (item.description) {
47
+ lines.push(
48
+ ` <description>${escapeXml(item.description)}</description>`,
49
+ );
50
+ }
51
+
52
+ const pubDate = item.pubDate ? new Date(item.pubDate) : null;
53
+ if (pubDate && !Number.isNaN(pubDate.getTime())) {
54
+ lines.push(` <pubDate>${pubDate.toUTCString()}</pubDate>`);
55
+ }
56
+
57
+ for (const author of item.authors ?? []) {
58
+ lines.push(` <dc:creator>${escapeXml(author)}</dc:creator>`);
59
+ }
60
+
61
+ if (item.category) {
62
+ lines.push(` <category>${escapeXml(item.category)}</category>`);
63
+ }
64
+
65
+ return ` <item>\n${lines.join("\n")}\n </item>`;
66
+ })
67
+ .join("\n");
68
+
69
+ const xml = `<?xml version="1.0" encoding="UTF-8"?>
70
+ <rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom">
71
+ <channel>
72
+ <title>${escapeXml(title)}</title>
73
+ <link>${escapeXml(link)}</link>
74
+ <description>${escapeXml(description)}</description>
75
+ <language>${language}</language>
76
+ <atom:link href="${escapeXml(feedUrl)}" rel="self" type="application/rss+xml" />
77
+ ${itemsXml}
78
+ </channel>
79
+ </rss>`;
80
+
81
+ return new Response(xml, {
82
+ headers: {
83
+ "Content-Type": "application/rss+xml",
84
+ },
85
+ });
86
+ }
87
+
88
+ export function rssErrorResponse(): Response {
89
+ return new Response(
90
+ '<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"><channel><title>Error</title></channel></rss>',
91
+ {
92
+ status: 500,
93
+ headers: { "Content-Type": "application/xml" },
94
+ },
95
+ );
96
+ }
@@ -0,0 +1,58 @@
1
+ import { cacheLife, cacheTag } from "next/cache";
2
+ import { PINNED_PAGE_ENTRIES } from "@/lib/integrations/sanity/pinned-pages";
3
+ import { PAGE_SITEMAP_QUERY } from "@/lib/integrations/sanity/queries";
4
+ import type { PAGE_SITEMAP_QUERY_RESULT } from "@/lib/integrations/sanity/sanity.types";
5
+ import { SITEMAP_TAG } from "@/lib/integrations/sanity/tags";
6
+ import { getBaseUrl } from "./base-url";
7
+
8
+ export const getSitemapBaseUrl = getBaseUrl;
9
+
10
+ const getNormalizedPageSlug = (slug: string | null) => {
11
+ if (!slug || slug === "/") return "";
12
+ return slug.replace(/^\/+|\/+$/g, "");
13
+ };
14
+
15
+ export const getPagePath = (slug: string | null) => {
16
+ const normalizedSlug = getNormalizedPageSlug(slug);
17
+ return normalizedSlug ? `/${normalizedSlug}` : "/";
18
+ };
19
+
20
+ export const getPageUrl = (baseUrl: string, slug: string | null) =>
21
+ new URL(getPagePath(slug), baseUrl).toString();
22
+
23
+ export const getMarkdownPath = (baseUrl: string, slug: string | null) => {
24
+ const normalizedSlug = getNormalizedPageSlug(slug);
25
+
26
+ return normalizedSlug
27
+ ? `${baseUrl}/${normalizedSlug}.md`
28
+ : `${baseUrl}/index.md`;
29
+ };
30
+
31
+ // Cached and tagged, not per-request: robots.txt points every crawler here, so an
32
+ // uncached read is a Sanity round trip per crawl hit.
33
+ export const getSitemap = async () => {
34
+ "use cache";
35
+ const sanityModule = await import("@/lib/integrations/sanity/client");
36
+ const client = sanityModule?.client;
37
+
38
+ if (!client) return null;
39
+
40
+ const pages = await client
41
+ .fetch<PAGE_SITEMAP_QUERY_RESULT>(PAGE_SITEMAP_QUERY, {
42
+ pinnedIds: PINNED_PAGE_ENTRIES.map((entry) => entry.id),
43
+ })
44
+ .catch((error) => {
45
+ console.error("Error fetching the sitemap:", error);
46
+ return null;
47
+ });
48
+
49
+ if (pages) {
50
+ cacheTag(SITEMAP_TAG);
51
+ } else {
52
+ // No publish clears a failed read, and this prerenders — an outage at build
53
+ // time would otherwise bake in an empty sitemap for the full ~1y cacheLife.
54
+ cacheLife("hours");
55
+ }
56
+
57
+ return pages;
58
+ };
@@ -0,0 +1,90 @@
1
+ import type { NextRequest } from "next/server";
2
+ import { NextResponse } from "next/server";
3
+ import {
4
+ acceptHeaderRoutes,
5
+ ignoredPaths,
6
+ mdExtensionRoutes,
7
+ } from "@/lib/integrations/sanity/markdown-proxy.config";
8
+
9
+ export function proxy(request: NextRequest) {
10
+ const { pathname } = request.nextUrl;
11
+ const acceptHeader = request.headers.get("accept") || "";
12
+
13
+ if (ignoredPaths.some((p) => pathname.startsWith(p))) {
14
+ return NextResponse.next();
15
+ }
16
+
17
+ // Collapse repeated slashes before any rewrite, or the rewrite below would
18
+ // mask the alias (e.g. /foo//bar.md silently resolving to /foo/bar.md).
19
+ const collapsedPathname = pathname.replace(/\/{2,}/g, "/");
20
+ if (collapsedPathname !== pathname) {
21
+ const url = request.nextUrl.clone();
22
+ url.pathname = collapsedPathname;
23
+ return NextResponse.redirect(url, 308);
24
+ }
25
+
26
+ if (pathname === "/" && acceptHeader.includes("text/markdown")) {
27
+ const url = request.nextUrl.clone();
28
+ url.pathname = "/api/index.md";
29
+ return NextResponse.rewrite(url);
30
+ }
31
+
32
+ // 1. Rewrite .md extension URLs to API routes
33
+ // e.g. /about.md → /api/about.md
34
+ for (const route of mdExtensionRoutes) {
35
+ const match = route.regex.exec(pathname);
36
+ const slug = match?.[1];
37
+ if (slug) {
38
+ const url = request.nextUrl.clone();
39
+ url.pathname = route.apiPath.replace("[slug]", slug);
40
+ return NextResponse.rewrite(url);
41
+ }
42
+ }
43
+
44
+ // 2. Accept: text/markdown — rewrite to markdown API route
45
+ if (acceptHeader.includes("text/markdown")) {
46
+ for (const route of acceptHeaderRoutes) {
47
+ const match = route.regex.exec(pathname);
48
+ const slug = match?.[1];
49
+ if (slug) {
50
+ const url = request.nextUrl.clone();
51
+ url.pathname = route.apiPath.replace("[slug]", slug);
52
+ return NextResponse.rewrite(url);
53
+ }
54
+ }
55
+ }
56
+
57
+ // 3. Add Link header for markdown discoverability on HTML page requests
58
+ const response = NextResponse.next();
59
+
60
+ if (pathname === "/") {
61
+ response.headers.set(
62
+ "Link",
63
+ '</index.md>; rel="alternate"; type="text/markdown"',
64
+ );
65
+ response.headers.set("Vary", "Accept");
66
+ return response;
67
+ }
68
+
69
+ for (const route of acceptHeaderRoutes) {
70
+ const match = route.regex.exec(pathname);
71
+ const slug = match?.[1];
72
+ if (slug) {
73
+ const mdUrl = route.publicPath.replace("[slug]", slug);
74
+ response.headers.set(
75
+ "Link",
76
+ `<${mdUrl}>; rel="alternate"; type="text/markdown"`,
77
+ );
78
+ response.headers.set("Vary", "Accept");
79
+ break;
80
+ }
81
+ }
82
+
83
+ return response;
84
+ }
85
+
86
+ export const config = {
87
+ matcher: [
88
+ "/((?!_next/static|_next/image|favicon.ico|sitemap.xml|sitemap.md|robots.txt).*)",
89
+ ],
90
+ };
@@ -1,4 +1,9 @@
1
1
  # Never upload the private keys or local env files on `vercel deploy`.
2
2
  # (Encrypted .env / .env.production / .env.preview ARE uploaded — they're safe.)
3
+ # Patterns stay unanchored so nested app/package dirs are covered too.
3
4
  .env.keys
4
5
  .env*.local
6
+ # Exception: `vercel pull` writes its own env under .vercel/, and Next traces it
7
+ # into the function bundles — stripping it breaks `deploy --prebuilt` with an
8
+ # ENOENT on the remote build.
9
+ !/.vercel/*.local
@@ -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
+ }
@@ -56,8 +56,8 @@ NEXT_PUBLIC_SANITY_API_READ_TOKEN=""
56
56
  # Optional - for server-side mutations and visual editing
57
57
  # SANITY_PRIVATE_TOKEN=""
58
58
 
59
- # Optional - API version (defaults to 2024-03-15)
60
- # NEXT_PUBLIC_SANITY_API_VERSION="2024-03-15"
59
+ # Optional - API version (defaults to 2025-02-19, the floor for Content Releases)
60
+ # NEXT_PUBLIC_SANITY_API_VERSION="2025-02-19"
61
61
 
62
62
 
63
63
  # ============================================
@@ -41,7 +41,7 @@ jobs:
41
41
  run: bun install --frozen-lockfile
42
42
 
43
43
  - name: Type check
44
- run: bun run typecheck:compare
44
+ run: bun run typecheck
45
45
 
46
46
  - name: Lint
47
47
  run: bun run lint
@@ -58,7 +58,6 @@
58
58
 
59
59
  // General
60
60
  "files.eol": "\n",
61
- // "typescript.experimental.useTsgo": true, // Disabled: tsgo doesn't support Next.js plugin
62
61
  "javascript.suggest.autoImports": true,
63
62
  // Disable Prettier to prevent silent formatter conflicts
64
63
  "prettier.enable": false,
@@ -90,7 +89,6 @@
90
89
 
91
90
  // TypeScript
92
91
  "typescript.suggest.autoImports": true,
93
- "typescript.tsdk": "node_modules/typescript/lib",
94
92
 
95
93
  // Editor labels (fixed for .tsx files)
96
94
  "workbench.editor.customLabels.patterns": {
@@ -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",
@@ -9,8 +9,7 @@
9
9
  import cn from "clsx"
10
10
  import NextImage, { type ImageProps as NextImageProps } from "next/image"
11
11
  import type { CSSProperties, Ref } from "react"
12
-
13
- const DESKTOP_BREAKPOINT = 1440
12
+ import { buildImageSizes } from "@/lib/utils/image-sizes"
14
13
 
15
14
  /**
16
15
  * Enhanced Image component props extending Next.js Image.
@@ -99,7 +98,6 @@ const getFinalPlaceholder = (
99
98
  * Enhanced Image component with responsive sizing and automatic optimizations.
100
99
  *
101
100
  * Always use this component instead of next/image directly. Provides:
102
- * - Automatic responsive sizes generation
103
101
  * - Smart blur placeholders with aspect ratio support
104
102
  * - Performance optimizations (lazy loading by default)
105
103
  * - Priority flag for LCP images
@@ -148,14 +146,14 @@ export function Image({
148
146
  style,
149
147
  className,
150
148
  objectFit = "cover",
151
- quality = 90,
149
+ quality = 75,
152
150
  alt = "",
153
151
  fill,
154
152
  block = !fill,
155
- width = block ? 1 : undefined,
156
- height = block ? 1 : undefined,
157
- mobileSize = "100vw",
158
- desktopSize = "100vw",
153
+ width,
154
+ height,
155
+ mobileSize,
156
+ desktopSize,
159
157
  sizes,
160
158
  src,
161
159
  unoptimized,
@@ -165,10 +163,18 @@ export function Image({
165
163
  priority = false,
166
164
  ...props
167
165
  }: ImageProps) {
168
- // Generate responsive sizes if not provided
169
- const finalSizes =
170
- sizes ||
171
- `(max-width: ${DESKTOP_BREAKPOINT}px) ${mobileSize}, ${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)
172
178
 
173
179
  // Early return after hooks
174
180
  if (!src) return null
@@ -196,8 +202,8 @@ export function Image({
196
202
  <NextImage
197
203
  ref={ref}
198
204
  fill={!block}
199
- {...(width !== undefined && { width })}
200
- {...(height !== undefined && { height })}
205
+ {...(finalWidth !== undefined && { width: finalWidth })}
206
+ {...(finalHeight !== undefined && { height: finalHeight })}
201
207
  priority={priority}
202
208
  quality={quality}
203
209
  alt={alt}
@@ -206,7 +212,7 @@ export function Image({
206
212
  ...style,
207
213
  }}
208
214
  className={cn(className, block && "block w-full")}
209
- sizes={finalSizes}
215
+ {...(finalSizes && { sizes: finalSizes })}
210
216
  src={src}
211
217
  unoptimized={unoptimized || isSvg}
212
218
  draggable={false}
@@ -20,5 +20,5 @@
20
20
  "zustand"
21
21
  ],
22
22
  "ignoreBinaries": ["op", "gh", "vercel"],
23
- "ignoreFiles": [".agents/**", ".claude/**", "hooks/**"]
23
+ "ignoreFiles": [".agents/**", ".claude/**", "hooks/**", "lib/integrations/hubspot/**"]
24
24
  }
@@ -11,7 +11,7 @@ pre-commit:
11
11
  stage_fixed: true
12
12
  typecheck:
13
13
  glob: '*.{ts,tsx}'
14
- run: bunx tsgo --noEmit --incremental false
14
+ run: bunx tsc --noEmit --incremental false
15
15
  stage_fixed: false
16
16
 
17
17
  post-merge:
@@ -5,10 +5,12 @@ Pure utility functions organized by concern.
5
5
  Use explicit imports for clarity and better tree-shaking:
6
6
 
7
7
  ```tsx
8
- import { clamp, lerp, mapRange } from '@/utils/math'
9
- import { slugify } from '@/utils/strings'
10
- import { generatePageMetadata } from '@/utils/metadata'
11
- import { tovw, torem, toem } from '@/utils/viewport'
8
+ import { clamp, lerp, mapRange } from '@/lib/utils/math'
9
+ import { slugify } from '@/lib/utils/strings'
10
+ import { generatePageMetadata } from '@/lib/utils/metadata'
11
+ import { tovw, torem, toem } from '@/lib/utils/viewport'
12
+ import { buildImageSizes, buildGridImageSizes } from '@/lib/utils/image-sizes'
13
+ import { useMergeRefs } from '@/lib/utils/merge-refs'
12
14
  ```
13
15
 
14
16
  ## Modules
@@ -18,8 +20,10 @@ import { tovw, torem, toem } from '@/utils/viewport'
18
20
  | `math` | `clamp`, `lerp`, `mapRange`, `modulo`, `normalize`, `distance` |
19
21
  | `easings` | All easing functions (`easeOutCubic`, etc.) |
20
22
  | `viewport` | `tovw`, `torem`, `toem` |
21
- | `strings` | `slugify`, `mergeRefs`, `capitalizeFirstLetter` |
23
+ | `strings` | `slugify`, `capitalizeFirstLetter`, `twoDigits`, `numberWithCommas` |
22
24
  | `metadata` | `generatePageMetadata` |
25
+ | `image-sizes` | `buildImageSizes`, `buildGridImageSizes` |
26
+ | `merge-refs` | `mergeRefs`, `useMergeRefs`, `assignRef` |
23
27
 
24
28
  ## Common Patterns
25
29
 
@@ -37,4 +41,13 @@ toem(16, 14) // → "1.14em"
37
41
 
38
42
  // SEO
39
43
  export const metadata = generatePageMetadata({ title: 'About' })
44
+
45
+ // next/image sizes — keys mirror the element's Tailwind variants
46
+ buildImageSizes({ base: '100vw', desktop: '796px' })
47
+ buildGridImageSizes({ base: 1, tablet: 2, desktop: 3 })
48
+
49
+ // Refs — in a component, use the hook: an inline mergeRefs([...]) is a new
50
+ // callback each render, so React detaches and reattaches every ref.
51
+ const mergedRef = useMergeRefs([localRef, ref])
52
+ return <input ref={mergedRef} />
40
53
  ```
@@ -0,0 +1,72 @@
1
+ /** Mirrors the theme's `--breakpoint-*` scale, which clears Tailwind's defaults. */
2
+ export const BASEMENT_BREAKPOINTS = {
3
+ mobile: 640,
4
+ tablet: 768,
5
+ "tablet-lg": 1024,
6
+ desktop: 1440,
7
+ "desktop-large": 1920,
8
+ } as const
9
+
10
+ export type BreakpointMap = Readonly<Record<string, number>>
11
+
12
+ /** Mirrors the element's variants: `grid-cols-1 tablet:grid-cols-2` → `{ base: 1, tablet: 2 }`. */
13
+ export type ByBreakpoint<
14
+ T,
15
+ B extends BreakpointMap = typeof BASEMENT_BREAKPOINTS,
16
+ > = { base: T } & Partial<Record<Extract<keyof B, string>, T>>
17
+
18
+ function columnWidth(columns: number): string {
19
+ return `${Math.round(100 / columns)}vw`
20
+ }
21
+
22
+ /**
23
+ * Widest first, since `sizes` takes the first matching condition and the last
24
+ * entry must be bare. `min-width` matches the Tailwind variant's boundary; a
25
+ * `max-width: <next> - 1` one would leave a fractional-pixel gap.
26
+ */
27
+ function toSizes(
28
+ base: string,
29
+ overrides: Readonly<Record<string, string | undefined>>,
30
+ scale: BreakpointMap
31
+ ): string {
32
+ const conditions: string[] = []
33
+
34
+ for (const [name, min] of Object.entries(scale).sort(
35
+ ([, a], [, b]) => b - a
36
+ )) {
37
+ const value = overrides[name]
38
+ if (value !== undefined) {
39
+ conditions.push(`(min-width: ${min}px) ${value}`)
40
+ }
41
+ }
42
+
43
+ return [...conditions, base].join(", ")
44
+ }
45
+
46
+ /** Builds `sizes` from the CSS length the image renders at per breakpoint. */
47
+ export function buildImageSizes<
48
+ B extends BreakpointMap = typeof BASEMENT_BREAKPOINTS,
49
+ >(widths: ByBreakpoint<string, B>, breakpoints?: B): string {
50
+ const { base, ...overrides } = widths
51
+ return toSizes(base, overrides, breakpoints ?? BASEMENT_BREAKPOINTS)
52
+ }
53
+
54
+ /**
55
+ * Builds `sizes` from a grid's column count per breakpoint. Rounded: `sizes`
56
+ * only steers which srcset candidate the browser picks, and gaps make the real
57
+ * column narrower than `100 / N` anyway.
58
+ */
59
+ export function buildGridImageSizes<
60
+ B extends BreakpointMap = typeof BASEMENT_BREAKPOINTS,
61
+ >(columns: ByBreakpoint<number, B>, breakpoints?: B): string {
62
+ const { base, ...overrides } = columns
63
+ const widths: Record<string, string> = {}
64
+
65
+ for (const [name, count] of Object.entries(overrides)) {
66
+ if (count !== undefined) {
67
+ widths[name] = columnWidth(count)
68
+ }
69
+ }
70
+
71
+ return toSizes(columnWidth(base), widths, breakpoints ?? BASEMENT_BREAKPOINTS)
72
+ }
@@ -0,0 +1,56 @@
1
+ import { type Ref, type RefCallback, useRef } from "react"
2
+
3
+ /** Returns the ref's own cleanup callback when it provides one (React 19). */
4
+ export function assignRef<T>(
5
+ ref: Ref<T> | undefined,
6
+ value: T | null
7
+ ): ReturnType<RefCallback<T>> {
8
+ if (typeof ref === "function") {
9
+ return ref(value)
10
+ }
11
+ if (ref) {
12
+ ref.current = value
13
+ }
14
+ return undefined
15
+ }
16
+
17
+ /** Merges object/callback refs into one callback ref, with React 19 ref cleanup. */
18
+ export function mergeRefs<T>(refs: (Ref<T> | undefined)[]): RefCallback<T> {
19
+ return (value) => {
20
+ const cleanups = refs.map((ref) => {
21
+ const cleanup = assignRef(ref, value)
22
+ return typeof cleanup === "function"
23
+ ? cleanup
24
+ : () => assignRef(ref, null)
25
+ })
26
+ return () => {
27
+ for (const cleanup of cleanups) {
28
+ cleanup()
29
+ }
30
+ }
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Memoized {@link mergeRefs}, stable across renders for the same ref list.
36
+ * Compares the list here rather than passing it as `useMemo` deps: React only
37
+ * compares the common prefix, so a list that grows or shrinks would silently
38
+ * reuse the old callback and leave a ref unattached or stale.
39
+ */
40
+ export function useMergeRefs<T>(refs: (Ref<T> | undefined)[]): RefCallback<T> {
41
+ const cache = useRef<{
42
+ refs: (Ref<T> | undefined)[]
43
+ merged: RefCallback<T>
44
+ } | null>(null)
45
+
46
+ if (
47
+ !cache.current ||
48
+ cache.current.refs.length !== refs.length ||
49
+ cache.current.refs.some((ref, i) => ref !== refs[i])
50
+ ) {
51
+ // Snapshot: a caller may hand us a persistent array it mutates in place.
52
+ cache.current = { refs: [...refs], merged: mergeRefs(refs) }
53
+ }
54
+
55
+ return cache.current.merged
56
+ }
@@ -1,11 +1,9 @@
1
1
  /**
2
2
  * String & Object Utilities
3
3
  *
4
- * Helper functions for string manipulation, object operations, and refs.
4
+ * Helper functions for string manipulation and object operations.
5
5
  */
6
6
 
7
- import type { Ref } from "react"
8
-
9
7
  // =============================================================================
10
8
  // STRING UTILITIES
11
9
  // =============================================================================
@@ -196,51 +194,3 @@ export function iterableObject(
196
194
  return value
197
195
  })
198
196
  }
199
-
200
- // =============================================================================
201
- // REF UTILITIES
202
- // =============================================================================
203
-
204
- /**
205
- * Merges multiple React refs into a single ref callback.
206
- *
207
- * Useful when you need to assign multiple refs to the same element,
208
- * such as combining a local ref with a forwarded ref.
209
- *
210
- * @param refs - Refs to merge (functions and ref objects supported)
211
- * @returns A ref callback that updates all provided refs
212
- *
213
- * @example
214
- * ```tsx
215
- * // Component that needs both local and forwarded ref
216
- * function MyComponent({ ref }: { ref: React.Ref<HTMLDivElement> }) {
217
- * const localRef = useRef<HTMLDivElement>(null)
218
- * const mergedRef = mergeRefs(localRef, ref)
219
- *
220
- * return <div ref={mergedRef}>Content</div>
221
- * }
222
- *
223
- * // Forward ref pattern
224
- * const ForwardedComponent = forwardRef(MyComponent)
225
- * ```
226
- */
227
- export function mergeRefs<T>(...refs: (Ref<T> | undefined)[]): Ref<T> {
228
- return (value) => {
229
- const cleanups = refs.reduce<VoidFunction[]>((accumulator, ref) => {
230
- if (typeof ref === "function") {
231
- const cleanup = ref(value)
232
- if (typeof cleanup === "function") {
233
- accumulator.push(cleanup)
234
- }
235
- } else if (ref) {
236
- ref.current = value
237
- }
238
-
239
- return accumulator
240
- }, [])
241
-
242
- return () => {
243
- for (const cleanup of cleanups) cleanup()
244
- }
245
- }
246
- }