bsmnt 0.4.3 → 0.6.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 (266) hide show
  1. package/README.md +1 -0
  2. package/dist/application/add-hooks/index.d.ts.map +1 -1
  3. package/dist/application/add-hooks/index.js +1 -0
  4. package/dist/application/add-hooks/index.js.map +1 -1
  5. package/dist/application/add-integration/index.d.ts +33 -8
  6. package/dist/application/add-integration/index.d.ts.map +1 -1
  7. package/dist/application/add-integration/index.js +221 -90
  8. package/dist/application/add-integration/index.js.map +1 -1
  9. package/dist/application/create-project/build-context.d.ts +3 -1
  10. package/dist/application/create-project/build-context.d.ts.map +1 -1
  11. package/dist/application/create-project/build-context.js +2 -1
  12. package/dist/application/create-project/build-context.js.map +1 -1
  13. package/dist/application/create-project/index.d.ts +3 -1
  14. package/dist/application/create-project/index.d.ts.map +1 -1
  15. package/dist/application/create-project/index.js +4 -2
  16. package/dist/application/create-project/index.js.map +1 -1
  17. package/dist/application/create-project/map-selection-to-registry.js +1 -1
  18. package/dist/application/create-project/map-selection-to-registry.js.map +1 -1
  19. package/dist/application/install-skills/index.js +1 -1
  20. package/dist/application/install-skills/index.js.map +1 -1
  21. package/dist/application/shared/project-detection.d.ts +2 -2
  22. package/dist/application/shared/project-detection.d.ts.map +1 -1
  23. package/dist/application/shared/project-detection.js +2 -2
  24. package/dist/application/shared/project-detection.js.map +1 -1
  25. package/dist/core/create/execute-plan.d.ts +4 -0
  26. package/dist/core/create/execute-plan.d.ts.map +1 -1
  27. package/dist/core/create/execute-plan.js +8 -0
  28. package/dist/core/create/execute-plan.js.map +1 -1
  29. package/dist/core/create/internal-ids.d.ts +4 -1
  30. package/dist/core/create/internal-ids.d.ts.map +1 -1
  31. package/dist/core/create/internal-ids.js +6 -1
  32. package/dist/core/create/internal-ids.js.map +1 -1
  33. package/dist/core/create/types.d.ts +2 -0
  34. package/dist/core/create/types.d.ts.map +1 -1
  35. package/dist/domain/cms.d.ts +0 -7
  36. package/dist/domain/cms.d.ts.map +1 -1
  37. package/dist/domain/cms.js.map +1 -1
  38. package/dist/domain/forms.d.ts +17 -0
  39. package/dist/domain/forms.d.ts.map +1 -0
  40. package/dist/domain/forms.js +13 -0
  41. package/dist/domain/forms.js.map +1 -0
  42. package/dist/domain/integration.d.ts +30 -0
  43. package/dist/domain/integration.d.ts.map +1 -0
  44. package/dist/domain/integration.js +12 -0
  45. package/dist/domain/integration.js.map +1 -0
  46. package/dist/domain/skills.d.ts +58 -0
  47. package/dist/domain/skills.d.ts.map +1 -1
  48. package/dist/domain/skills.js +52 -0
  49. package/dist/domain/skills.js.map +1 -1
  50. package/dist/index.js +114 -38
  51. package/dist/index.js.map +1 -1
  52. package/dist/infrastructure/agents/register-sanity-mcp.js +9 -6
  53. package/dist/infrastructure/agents/register-sanity-mcp.js.map +1 -1
  54. package/dist/infrastructure/create/apply-package-json.d.ts.map +1 -1
  55. package/dist/infrastructure/create/apply-package-json.js +19 -0
  56. package/dist/infrastructure/create/apply-package-json.js.map +1 -1
  57. package/dist/infrastructure/create/executor.d.ts.map +1 -1
  58. package/dist/infrastructure/create/executor.js +5 -0
  59. package/dist/infrastructure/create/executor.js.map +1 -1
  60. package/dist/infrastructure/create/install-dependencies.d.ts.map +1 -1
  61. package/dist/infrastructure/create/install-dependencies.js +4 -0
  62. package/dist/infrastructure/create/install-dependencies.js.map +1 -1
  63. package/dist/infrastructure/create/lifecycle-registry.d.ts.map +1 -1
  64. package/dist/infrastructure/create/lifecycle-registry.js +2 -0
  65. package/dist/infrastructure/create/lifecycle-registry.js.map +1 -1
  66. package/dist/infrastructure/create/merge-registry.d.ts.map +1 -1
  67. package/dist/infrastructure/create/merge-registry.js +8 -0
  68. package/dist/infrastructure/create/merge-registry.js.map +1 -1
  69. package/dist/infrastructure/create/setup-agent.d.ts +2 -0
  70. package/dist/infrastructure/create/setup-agent.d.ts.map +1 -1
  71. package/dist/infrastructure/create/setup-agent.js +32 -8
  72. package/dist/infrastructure/create/setup-agent.js.map +1 -1
  73. package/dist/infrastructure/create/setup-dotenvx.d.ts +41 -0
  74. package/dist/infrastructure/create/setup-dotenvx.d.ts.map +1 -0
  75. package/dist/infrastructure/create/setup-dotenvx.js +502 -0
  76. package/dist/infrastructure/create/setup-dotenvx.js.map +1 -0
  77. package/dist/infrastructure/create/setup-sanity.d.ts +13 -1
  78. package/dist/infrastructure/create/setup-sanity.d.ts.map +1 -1
  79. package/dist/infrastructure/create/setup-sanity.js +74 -31
  80. package/dist/infrastructure/create/setup-sanity.js.map +1 -1
  81. package/dist/infrastructure/integrations/inject-integration.d.ts +3 -3
  82. package/dist/infrastructure/integrations/inject-integration.d.ts.map +1 -1
  83. package/dist/infrastructure/integrations/inject-integration.js +81 -37
  84. package/dist/infrastructure/integrations/inject-integration.js.map +1 -1
  85. package/dist/modules/features/cms/sanity/config.d.ts.map +1 -1
  86. package/dist/modules/features/cms/sanity/config.js +13 -6
  87. package/dist/modules/features/cms/sanity/config.js.map +1 -1
  88. package/dist/modules/features/cms/sanity/index.js +8 -2
  89. package/dist/modules/features/cms/sanity/index.js.map +1 -1
  90. package/dist/modules/features/cms/sanity/mergers/ci-workflow.d.ts +13 -0
  91. package/dist/modules/features/cms/sanity/mergers/ci-workflow.d.ts.map +1 -0
  92. package/dist/modules/features/cms/sanity/mergers/ci-workflow.js +38 -0
  93. package/dist/modules/features/cms/sanity/mergers/ci-workflow.js.map +1 -0
  94. package/dist/modules/features/cms/sanity/mergers/knip.d.ts +12 -0
  95. package/dist/modules/features/cms/sanity/mergers/knip.d.ts.map +1 -0
  96. package/dist/modules/features/cms/sanity/mergers/knip.js +38 -0
  97. package/dist/modules/features/cms/sanity/mergers/knip.js.map +1 -0
  98. package/dist/modules/features/cms/sanity/mergers/layout.d.ts.map +1 -1
  99. package/dist/modules/features/cms/sanity/mergers/layout.js +17 -3
  100. package/dist/modules/features/cms/sanity/mergers/layout.js.map +1 -1
  101. package/dist/modules/features/cms/sanity/mergers/sitemap.d.ts +3 -3
  102. package/dist/modules/features/cms/sanity/mergers/sitemap.d.ts.map +1 -1
  103. package/dist/modules/features/cms/sanity/mergers/sitemap.js +48 -62
  104. package/dist/modules/features/cms/sanity/mergers/sitemap.js.map +1 -1
  105. package/dist/modules/features/cms/sanity-pagebuilder/config.d.ts.map +1 -1
  106. package/dist/modules/features/cms/sanity-pagebuilder/config.js +8 -5
  107. package/dist/modules/features/cms/sanity-pagebuilder/config.js.map +1 -1
  108. package/dist/modules/features/cms/sanity-pagebuilder/index.js +2 -2
  109. package/dist/modules/features/cms/sanity-pagebuilder/index.js.map +1 -1
  110. package/dist/modules/features/env/dotenvx/config.d.ts +13 -0
  111. package/dist/modules/features/env/dotenvx/config.d.ts.map +1 -0
  112. package/dist/modules/features/env/dotenvx/config.js +47 -0
  113. package/dist/modules/features/env/dotenvx/config.js.map +1 -0
  114. package/dist/modules/features/forms/hubspot/config.d.ts +31 -0
  115. package/dist/modules/features/forms/hubspot/config.d.ts.map +1 -0
  116. package/dist/modules/features/forms/hubspot/config.js +25 -0
  117. package/dist/modules/features/forms/hubspot/config.js.map +1 -0
  118. package/dist/paths.d.ts +16 -0
  119. package/dist/paths.d.ts.map +1 -1
  120. package/dist/paths.js +16 -0
  121. package/dist/paths.js.map +1 -1
  122. package/package.json +21 -9
  123. package/src/agent-skills/json-ld/SKILL.md +85 -0
  124. package/src/agent-skills/manage-env/SKILL.md +67 -0
  125. package/src/assets/check-node-version.mjs +22 -0
  126. package/src/modules/features/cms/sanity/files/app/blog/[slug]/page.tsx +48 -21
  127. package/src/modules/features/cms/sanity/files/app/blog/page.tsx +54 -0
  128. package/src/modules/features/cms/sanity/files/app/feed.xml/route.ts +36 -0
  129. package/src/modules/features/cms/sanity/files/components/sanity/structured-data.tsx +83 -0
  130. package/src/modules/features/cms/sanity/files/lib/integrations/sanity/components/disable-draft-mode.tsx +3 -3
  131. package/src/modules/features/cms/sanity/files/lib/integrations/sanity/queries.ts +14 -0
  132. package/src/modules/features/cms/sanity/files/lib/integrations/sanity/sanity.config.ts +19 -3
  133. package/src/modules/features/cms/sanity/files/lib/integrations/sanity/sanity.types.ts +1 -0
  134. package/src/modules/features/cms/sanity/files/lib/integrations/sanity/schemas/article.ts +7 -7
  135. package/src/modules/features/cms/sanity/files/lib/integrations/sanity/schemas/index.ts +36 -1
  136. package/src/modules/features/cms/sanity/files/lib/integrations/sanity/schemas/metadata.ts +12 -1
  137. package/src/modules/features/cms/sanity/files/lib/integrations/sanity/schemas/structured-data-blocks.ts +416 -0
  138. package/src/modules/features/cms/sanity/files/lib/integrations/sanity/schemas/structured-data.ts +100 -0
  139. package/src/modules/features/cms/sanity/files/lib/integrations/sanity/structure.ts +37 -2
  140. package/src/modules/features/cms/sanity/files/lib/integrations/sanity/utils/page-json-ld.ts +208 -0
  141. package/src/modules/features/cms/sanity/files/lib/scripts/setup-ci-secrets.ts +117 -0
  142. package/src/modules/features/cms/sanity/files/lib/utils/rss.ts +96 -0
  143. package/src/modules/features/cms/sanity-pagebuilder/files/.knip.json +8 -0
  144. package/src/modules/features/cms/sanity-pagebuilder/files/app/api/[[...slug]]/route.ts +100 -0
  145. package/src/modules/features/cms/sanity-pagebuilder/files/app/api/draft-mode/disable/route.ts +7 -0
  146. package/src/modules/features/cms/sanity-pagebuilder/files/app/api/draft-mode/enable/route.ts +20 -0
  147. package/src/modules/features/cms/sanity-pagebuilder/files/app/api/revalidate/route.ts +126 -0
  148. package/src/modules/features/cms/sanity-pagebuilder/files/app/feed.xml/route.ts +40 -0
  149. package/src/modules/features/cms/sanity-pagebuilder/files/app/sitemap.md/route.ts +124 -0
  150. package/src/modules/features/cms/sanity-pagebuilder/files/app/studio/[[...tool]]/page.tsx +8 -0
  151. package/src/modules/features/cms/sanity-pagebuilder/files/components/sanity/draft-mode-toggle.tsx +27 -0
  152. package/src/modules/features/cms/sanity-pagebuilder/files/components/sanity/rich-text.tsx +87 -0
  153. package/src/modules/features/cms/sanity-pagebuilder/files/components/sanity/visual-editing.tsx +28 -0
  154. package/src/modules/features/cms/sanity-pagebuilder/files/components/ui/sanity-image/index.tsx +41 -0
  155. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/check-integration.ts +5 -0
  156. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/client.ts +27 -0
  157. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/disable-draft-mode.tsx +23 -0
  158. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/page-builder-input.tsx +39 -0
  159. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/page-category-input.tsx +50 -0
  160. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/same-as-input.tsx +66 -0
  161. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/slug-field.tsx +18 -0
  162. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/slug-input.tsx +14 -0
  163. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/confirm-publish-action.ts +40 -0
  164. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/env.ts +34 -0
  165. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/fetchers/layout.ts +67 -0
  166. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/fetchers/redirects.ts +17 -0
  167. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/icons.ts +79 -0
  168. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/live/index.tsx +88 -0
  169. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/markdown-proxy.config.ts +50 -0
  170. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/page-builder-config.ts +130 -0
  171. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/page-category.ts +28 -0
  172. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/page-tree-pane.tsx +420 -0
  173. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/pinned-pages.ts +54 -0
  174. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/presentation.ts +119 -0
  175. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/queries.ts +407 -0
  176. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/sanity.cli.ts +29 -0
  177. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/sanity.config.ts +113 -0
  178. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/index.ts +4 -0
  179. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/reusable/blog-content.ts +89 -0
  180. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/reusable/description.ts +29 -0
  181. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/reusable/hero.ts +28 -0
  182. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/singleton/content-collection.ts +45 -0
  183. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/content/author.ts +70 -0
  184. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/content/blog-category.ts +55 -0
  185. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/index.ts +124 -0
  186. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/layout/company-data.ts +70 -0
  187. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/layout/footer.ts +79 -0
  188. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/layout/navbar.ts +75 -0
  189. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/layout/redirect.ts +120 -0
  190. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/layout/structured-data.ts +124 -0
  191. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/link.ts +125 -0
  192. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/logo-field.ts +9 -0
  193. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/metadata.ts +79 -0
  194. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/nav-objects.ts +192 -0
  195. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/page-builder.ts +39 -0
  196. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/page-folder.ts +124 -0
  197. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/page.ts +242 -0
  198. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/richText.ts +63 -0
  199. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/structured-data-blocks.ts +436 -0
  200. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/singletons.ts +51 -0
  201. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/structure.ts +330 -0
  202. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/utils/image.ts +8 -0
  203. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/utils/link.ts +140 -0
  204. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/utils/page-builder-markdown.ts +81 -0
  205. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/utils/page-json-ld.ts +208 -0
  206. package/src/modules/features/cms/sanity-pagebuilder/files/lib/scripts/sanity-typegen.ts +45 -0
  207. package/src/modules/features/cms/sanity-pagebuilder/files/lib/utils/check-redirect.ts +30 -0
  208. package/src/modules/features/cms/sanity-pagebuilder/files/lib/utils/json-ld.tsx +521 -0
  209. package/src/modules/features/cms/sanity-pagebuilder/files/lib/utils/metadata.ts +167 -0
  210. package/src/modules/features/cms/sanity-pagebuilder/files/lib/utils/rss.ts +96 -0
  211. package/src/modules/features/cms/sanity-pagebuilder/files/lib/utils/sitemap.ts +40 -0
  212. package/src/modules/features/cms/sanity-pagebuilder/files/proxy.ts +81 -0
  213. package/src/modules/features/env/dotenvx/files/decrypt-env.mjs +45 -0
  214. package/src/modules/features/env/dotenvx/files/gitleaks.yml +25 -0
  215. package/src/modules/features/env/dotenvx/files/instrumentation.ts +14 -0
  216. package/src/modules/features/env/dotenvx/files/push-keys.mjs +129 -0
  217. package/src/modules/features/env/dotenvx/files/save-key.mjs +108 -0
  218. package/src/modules/features/env/dotenvx/files/setup-env.mjs +67 -0
  219. package/src/modules/features/env/dotenvx/files/vercelignore +4 -0
  220. package/src/modules/features/env/dotenvx/files/write-env.mjs +47 -0
  221. package/src/templates/next-default/.env.example +14 -6
  222. package/src/templates/next-default/.github/workflows/ci.yml +83 -0
  223. package/src/templates/next-default/.vscode/extensions.json +3 -0
  224. package/src/templates/next-default/README.md +53 -0
  225. package/src/templates/next-default/knip.json +14 -0
  226. package/src/templates/next-default/lefthook.yml +5 -0
  227. package/src/templates/next-default/lib/utils/json-ld.tsx +327 -21
  228. package/src/templates/next-default/next.config.ts +6 -0
  229. package/src/templates/next-default/package.json +8 -4
  230. package/src/templates/next-experiments/.env.example +14 -6
  231. package/src/templates/next-experiments/.github/workflows/ci.yml +83 -0
  232. package/src/templates/next-experiments/.vscode/extensions.json +3 -0
  233. package/src/templates/next-experiments/README.md +53 -0
  234. package/src/templates/next-experiments/knip.json +14 -0
  235. package/src/templates/next-experiments/lefthook.yml +30 -0
  236. package/src/templates/next-experiments/lib/utils/json-ld.tsx +327 -21
  237. package/src/templates/next-experiments/next.config.ts +6 -0
  238. package/src/templates/next-experiments/package.json +10 -2
  239. package/src/templates/next-pagebuilder/.env.example +19 -1
  240. package/src/templates/next-pagebuilder/.github/workflows/ci.yml +90 -0
  241. package/src/templates/next-pagebuilder/.vscode/extensions.json +3 -0
  242. package/src/templates/next-pagebuilder/README.md +65 -0
  243. package/src/templates/next-pagebuilder/app/(content)/[[...slug]]/page.tsx +6 -1
  244. package/src/templates/next-pagebuilder/app/(content)/layout.tsx +3 -12
  245. package/src/templates/next-pagebuilder/components/layout/footer/index.tsx +11 -10
  246. package/src/templates/next-pagebuilder/components/layout/header/index.tsx +5 -3
  247. package/src/templates/next-pagebuilder/components/layout/json-ld/index.tsx +38 -26
  248. package/src/templates/next-pagebuilder/components/layout/wrapper/index.tsx +3 -11
  249. package/src/templates/next-pagebuilder/components/page-builder/components/content-collection/index.tsx +1 -1
  250. package/src/templates/next-pagebuilder/components/page-document/index.tsx +38 -0
  251. package/src/templates/next-pagebuilder/knip.json +27 -0
  252. package/src/templates/next-pagebuilder/lefthook.yml +5 -0
  253. package/src/templates/next-pagebuilder/lib/integrations/sanity/sanity.types.ts +36 -2
  254. package/src/templates/next-pagebuilder/lib/scripts/setup-ci-secrets.ts +117 -0
  255. package/src/templates/next-pagebuilder/next.config.ts +6 -0
  256. package/src/templates/next-pagebuilder/package.json +9 -3
  257. package/src/templates/next-webgl/.env.example +14 -6
  258. package/src/templates/next-webgl/.github/workflows/ci.yml +83 -0
  259. package/src/templates/next-webgl/.vscode/extensions.json +3 -0
  260. package/src/templates/next-webgl/README.md +53 -0
  261. package/src/templates/next-webgl/knip.json +13 -0
  262. package/src/templates/next-webgl/lefthook.yml +5 -0
  263. package/src/templates/next-webgl/lib/utils/json-ld.tsx +327 -21
  264. package/src/templates/next-webgl/next.config.ts +6 -0
  265. package/src/templates/next-webgl/package.json +8 -4
  266. package/src/modules/features/cms/sanity/files/lib/utils/json-ld.tsx +0 -244
@@ -0,0 +1,420 @@
1
+ import { MagnifyingGlassIcon } from "@phosphor-icons/react/dist/ssr";
2
+ import {
3
+ Badge,
4
+ Box,
5
+ Card,
6
+ Flex,
7
+ Spinner,
8
+ Stack,
9
+ Text,
10
+ TextInput,
11
+ } from "@sanity/ui";
12
+ import { useEffect, useRef, useState } from "react";
13
+ import { asyncScheduler, from, merge, of } from "rxjs";
14
+ import { switchMap, throttleTime } from "rxjs/operators";
15
+ import { type SanityClient, useClient } from "sanity";
16
+ import { usePaneRouter } from "sanity/structure";
17
+ import { apiVersion } from "./env";
18
+ import { pageFolderIcon, pinIcon } from "./icons";
19
+ import {
20
+ getPageCategoryIcon,
21
+ type PageBuilderGroupName,
22
+ } from "./page-builder-config";
23
+ import { pageCategoryExpression } from "./page-category";
24
+ import {
25
+ PINNED_PAGE_ENTRIES,
26
+ PINNED_PAGE_IDS,
27
+ type PinnedPageEntry,
28
+ } from "./pinned-pages";
29
+
30
+ const NO_MATCHES = "No matches found";
31
+ const NO_PAGES_OR_FOLDERS = "No pages or folders here yet";
32
+
33
+ interface PageTreeItem {
34
+ _id: string;
35
+ _originalId?: string;
36
+ _type: "page" | "pageFolder";
37
+ title?: string | null;
38
+ slug?: string | null;
39
+ pageType?: PageBuilderGroupName | null;
40
+ }
41
+
42
+ // `_originalId` is prefixed `drafts.` when an unpublished edit exists.
43
+ const isDraftItem = (item: PageTreeItem) =>
44
+ !!item._originalId?.startsWith("drafts.");
45
+
46
+ type ActiveState = "leaf" | "ancestor" | "none";
47
+
48
+ const PAGE_TREE_ITEM_ID_PREFIX = {
49
+ page: "page-tree-",
50
+ pageFolder: "page-folder-tree-",
51
+ } as const;
52
+
53
+ const PAGE_TREE_PANE_STYLE_ID = "page-tree-pane-style";
54
+
55
+ // Constrain the tree pane width so the slug/title rows stay readable. Runs once
56
+ // (id-guarded) from inside the component so the injection lives in React's tree.
57
+ const usePaneWidthStyle = () => {
58
+ useEffect(() => {
59
+ if (
60
+ typeof document === "undefined" ||
61
+ document.getElementById(PAGE_TREE_PANE_STYLE_ID)
62
+ ) {
63
+ return;
64
+ }
65
+ const style = document.createElement("style");
66
+ style.id = PAGE_TREE_PANE_STYLE_ID;
67
+ style.textContent = `[data-ui="Pane"][id^="page-tree-"]{max-width:350px;}`;
68
+ document.head.appendChild(style);
69
+ }, []);
70
+ };
71
+
72
+ const PAGE_TREE_QUERY = `
73
+ *[
74
+ (
75
+ _type == "pageFolder" &&
76
+ (
77
+ ($folderId == null && !defined(parentFolder._ref)) ||
78
+ parentFolder._ref == $folderId
79
+ )
80
+ ) ||
81
+ (
82
+ _type == "page" &&
83
+ (
84
+ ($folderId == null && !defined(pageFolder._ref)) ||
85
+ pageFolder._ref == $folderId
86
+ )
87
+ )
88
+ ]{
89
+ _id,
90
+ _originalId,
91
+ _type,
92
+ title,
93
+ "slug": slug.current,
94
+ "pageType": ${pageCategoryExpression}
95
+ }
96
+ `;
97
+
98
+ const normalizeId = (id: string) => id.replace(/^drafts\./, "");
99
+
100
+ const getItemId = (item: Pick<PageTreeItem, "_id" | "_type">) =>
101
+ `${PAGE_TREE_ITEM_ID_PREFIX[item._type]}${normalizeId(item._id)}`;
102
+
103
+ const sortItems = (items: PageTreeItem[]) =>
104
+ [...items].sort((a, b) => {
105
+ if (a._type !== b._type) return a._type === "page" ? -1 : 1;
106
+ // Pages without a slug (e.g. the homepage) float to the top.
107
+ if (!a.slug && a._type === "page") return -1;
108
+ if (!b.slug && b._type === "page") return 1;
109
+ return (a.title ?? "").localeCompare(b.title ?? "");
110
+ });
111
+
112
+ interface PageTreePaneProps {
113
+ options?: Record<string, unknown>;
114
+ }
115
+
116
+ export const PageTreePane = ({ options }: PageTreePaneProps) => {
117
+ const folderId = (options?.folderId as string | null | undefined) ?? null;
118
+
119
+ usePaneWidthStyle();
120
+
121
+ const [items, setItems] = useState<PageTreeItem[]>([]);
122
+ const [loading, setLoading] = useState(true);
123
+ const [query, setQuery] = useState("");
124
+
125
+ const baseClient = useClient({ apiVersion });
126
+ // Pinned to drafts so the tree shows in-progress edits and the Draft badge.
127
+ const clientRef = useRef<SanityClient | null>(null);
128
+ if (!clientRef.current) {
129
+ clientRef.current = baseClient.withConfig({ perspective: "drafts" });
130
+ }
131
+ const client = clientRef.current;
132
+
133
+ const { ChildLink, routerPanesState, groupIndex } = usePaneRouter();
134
+ const activeChildId = routerPanesState[groupIndex + 1]?.[0]?.id ?? null;
135
+ const isLeafPane = routerPanesState.length === groupIndex + 2;
136
+
137
+ useEffect(() => {
138
+ setLoading(true);
139
+ // Initial fetch, then refetch on each folder-scoped mutation to stay live.
140
+ const subscription = merge(
141
+ of(null),
142
+ client
143
+ .listen<PageTreeItem>(
144
+ folderId == null
145
+ ? `*[_type in ["page", "pageFolder"] && !defined(parentFolder._ref) && !defined(pageFolder._ref)]`
146
+ : `*[_type in ["page", "pageFolder"] && (parentFolder._ref == $folderId || pageFolder._ref == $folderId)]`,
147
+ { folderId },
148
+ { visibility: "query", events: ["mutation"], includeResult: false },
149
+ )
150
+ .pipe(
151
+ // leading+trailing: react to the first edit at once, coalesce the rest.
152
+ throttleTime(300, asyncScheduler, {
153
+ leading: true,
154
+ trailing: true,
155
+ }),
156
+ ),
157
+ )
158
+ .pipe(
159
+ switchMap(() =>
160
+ from(client.fetch<PageTreeItem[]>(PAGE_TREE_QUERY, { folderId })),
161
+ ),
162
+ )
163
+ .subscribe({
164
+ next: (data) => {
165
+ setItems(data);
166
+ setLoading(false);
167
+ },
168
+ error: () => setLoading(false),
169
+ });
170
+
171
+ return () => subscription.unsubscribe();
172
+ }, [folderId, client]);
173
+
174
+ // Pinned pages get their own section, so drop them from the dynamic list.
175
+ const visible =
176
+ folderId == null
177
+ ? items.filter((item) => !PINNED_PAGE_IDS.has(item._id))
178
+ : items;
179
+ const sorted = sortItems(visible);
180
+ const term = query.toLowerCase().trim();
181
+ const filtered = term
182
+ ? sorted.filter(
183
+ (item) =>
184
+ item.title?.toLowerCase().includes(term) ||
185
+ item.slug?.toLowerCase().includes(term),
186
+ )
187
+ : sorted;
188
+ const showPinned = folderId == null && !term;
189
+
190
+ return (
191
+ <Flex direction="column" style={{ height: "100%" }}>
192
+ <PageTreeSearch
193
+ query={query}
194
+ onQueryChange={setQuery}
195
+ autoFocus={folderId == null}
196
+ />
197
+ <Box style={{ borderBottom: "1px solid var(--card-border-color)" }} />
198
+ {showPinned && (
199
+ <Stack>
200
+ {PINNED_PAGE_ENTRIES.map((entry) => {
201
+ const activeState: ActiveState =
202
+ activeChildId === entry.id
203
+ ? isLeafPane
204
+ ? "leaf"
205
+ : "ancestor"
206
+ : "none";
207
+ return (
208
+ <PinnedRow
209
+ key={entry.id}
210
+ entry={entry}
211
+ ChildLink={ChildLink}
212
+ activeState={activeState}
213
+ />
214
+ );
215
+ })}
216
+ </Stack>
217
+ )}
218
+ <TreeBody
219
+ loading={loading}
220
+ items={filtered}
221
+ emptyMessage={term ? NO_MATCHES : NO_PAGES_OR_FOLDERS}
222
+ ChildLink={ChildLink}
223
+ activeChildId={activeChildId}
224
+ isLeafPane={isLeafPane}
225
+ />
226
+ </Flex>
227
+ );
228
+ };
229
+
230
+ interface PageTreeSearchProps {
231
+ query: string;
232
+ onQueryChange: (value: string) => void;
233
+ autoFocus?: boolean;
234
+ }
235
+
236
+ const PageTreeSearch = ({
237
+ query,
238
+ onQueryChange,
239
+ autoFocus,
240
+ }: PageTreeSearchProps) => (
241
+ <Box padding={3}>
242
+ <TextInput
243
+ icon={MagnifyingGlassIcon}
244
+ placeholder="Filter pages and folders…"
245
+ value={query}
246
+ onChange={(e) => onQueryChange(e.currentTarget.value)}
247
+ clearButton={!!query}
248
+ onClear={() => onQueryChange("")}
249
+ autoFocus={autoFocus}
250
+ />
251
+ </Box>
252
+ );
253
+
254
+ interface TreeBodyProps {
255
+ loading: boolean;
256
+ items: PageTreeItem[];
257
+ emptyMessage: string;
258
+ ChildLink: ReturnType<typeof usePaneRouter>["ChildLink"];
259
+ activeChildId: string | null;
260
+ isLeafPane: boolean;
261
+ }
262
+
263
+ const TreeBody = ({
264
+ loading,
265
+ items,
266
+ emptyMessage,
267
+ ChildLink,
268
+ activeChildId,
269
+ isLeafPane,
270
+ }: TreeBodyProps) => (
271
+ <Box style={{ flex: 1, overflowY: "auto" }}>
272
+ {loading && <Loading />}
273
+ {!loading && items.length === 0 && <Empty message={emptyMessage} />}
274
+ {!loading && items.length > 0 && (
275
+ <Stack>
276
+ {items.map((item) => {
277
+ const isActive = getItemId(item) === activeChildId;
278
+ let activeState: ActiveState = "none";
279
+ if (isActive) activeState = isLeafPane ? "leaf" : "ancestor";
280
+ return (
281
+ <ItemRow
282
+ key={item._id}
283
+ item={item}
284
+ ChildLink={ChildLink}
285
+ activeState={activeState}
286
+ />
287
+ );
288
+ })}
289
+ </Stack>
290
+ )}
291
+ </Box>
292
+ );
293
+
294
+ const Loading = () => (
295
+ <Flex justify="center" padding={4}>
296
+ <Spinner />
297
+ </Flex>
298
+ );
299
+
300
+ const Empty = ({ message }: { message: string }) => (
301
+ <Flex padding={4} justify="center">
302
+ <Text muted size={1}>
303
+ {message}
304
+ </Text>
305
+ </Flex>
306
+ );
307
+
308
+ interface ItemRowProps {
309
+ item: PageTreeItem;
310
+ ChildLink: ReturnType<typeof usePaneRouter>["ChildLink"];
311
+ activeState: ActiveState;
312
+ }
313
+
314
+ const ItemIcon = ({ item }: { item: PageTreeItem }) => {
315
+ const Icon =
316
+ item._type === "pageFolder"
317
+ ? pageFolderIcon
318
+ : getPageCategoryIcon(item.pageType);
319
+ return <Icon />;
320
+ };
321
+
322
+ const ItemRow = ({ item, ChildLink, activeState }: ItemRowProps) => {
323
+ const fallback = item._type === "page" ? "Untitled Page" : "Untitled Folder";
324
+ const isLeaf = activeState === "leaf";
325
+ const isAncestor = activeState === "ancestor";
326
+
327
+ return (
328
+ <ChildLink childId={getItemId(item)}>
329
+ <Card
330
+ as="div"
331
+ padding={3}
332
+ radius={0}
333
+ pressed={isLeaf}
334
+ selected={isAncestor}
335
+ tone={isLeaf ? "primary" : "default"}
336
+ style={{
337
+ cursor: "pointer",
338
+ display: "block",
339
+ ...(isLeaf && {
340
+ backgroundColor: "var(--card-badge-primary-bg-color)",
341
+ color: "var(--card-badge-primary-fg-color)",
342
+ }),
343
+ ...(isAncestor && {
344
+ backgroundColor: "var(--card-muted-bg-color)",
345
+ }),
346
+ }}
347
+ >
348
+ <Flex align="center" gap={3}>
349
+ <Box style={{ flexShrink: 0 }}>
350
+ <ItemIcon item={item} />
351
+ </Box>
352
+ <Stack space={2} flex={1}>
353
+ <Flex align="center" gap={2}>
354
+ <Text size={1} weight="medium">
355
+ {item.title ?? fallback}
356
+ </Text>
357
+ {isDraftItem(item) && (
358
+ <Badge fontSize={0} mode="outline" tone="caution">
359
+ Draft
360
+ </Badge>
361
+ )}
362
+ </Flex>
363
+ {item.slug && (
364
+ <Text size={0} muted weight="regular">
365
+ /{item.slug}
366
+ </Text>
367
+ )}
368
+ </Stack>
369
+ </Flex>
370
+ </Card>
371
+ </ChildLink>
372
+ );
373
+ };
374
+
375
+ interface PinnedRowProps {
376
+ entry: PinnedPageEntry;
377
+ ChildLink: ReturnType<typeof usePaneRouter>["ChildLink"];
378
+ activeState: ActiveState;
379
+ }
380
+
381
+ const PinnedRow = ({ entry, ChildLink, activeState }: PinnedRowProps) => {
382
+ const isLeaf = activeState === "leaf";
383
+ const isAncestor = activeState === "ancestor";
384
+ const Icon = pinIcon;
385
+
386
+ return (
387
+ <ChildLink childId={entry.id}>
388
+ <Card
389
+ as="div"
390
+ padding={3}
391
+ radius={0}
392
+ pressed={isLeaf}
393
+ selected={isAncestor}
394
+ tone={isLeaf ? "primary" : "default"}
395
+ style={{
396
+ cursor: "pointer",
397
+ display: "block",
398
+ ...(isLeaf && {
399
+ backgroundColor: "var(--card-badge-primary-bg-color)",
400
+ color: "var(--card-badge-primary-fg-color)",
401
+ }),
402
+ ...(isAncestor && {
403
+ backgroundColor: "var(--card-muted-bg-color)",
404
+ }),
405
+ }}
406
+ >
407
+ <Flex align="center" gap={3}>
408
+ <Box style={{ flexShrink: 0 }}>
409
+ <Icon />
410
+ </Box>
411
+ <Stack space={2} flex={1}>
412
+ <Text size={1} weight="medium">
413
+ {entry.title}
414
+ </Text>
415
+ </Stack>
416
+ </Flex>
417
+ </Card>
418
+ </ChildLink>
419
+ );
420
+ };
@@ -0,0 +1,54 @@
1
+ import { pageIcon, type SanityStudioIcon } from "./icons";
2
+
3
+ export type PinnedPageEntry = {
4
+ /** Sanity document _id. Doubles as the Studio template id. */
5
+ id: string;
6
+ /** Sanity document draft _id (`drafts.${id}`). */
7
+ draftId: string;
8
+ /** Title shown in Studio listings and used as the initial `title` value. */
9
+ title: string;
10
+ /** Icon shown in Studio. */
11
+ icon: SanityStudioIcon;
12
+ /** Public URL path. Used for link resolution + presentation. */
13
+ path: string;
14
+ /** Slug.current seeded into the template. `null` means no slug (homepage). */
15
+ slug: string | null;
16
+ };
17
+
18
+ export const HOMEPAGE_PINNED_PAGE: PinnedPageEntry = {
19
+ id: "page-homepage",
20
+ draftId: "drafts.page-homepage",
21
+ title: "Homepage",
22
+ icon: pageIcon,
23
+ path: "/",
24
+ slug: null,
25
+ };
26
+
27
+ export const BLOG_INDEX_PINNED_PAGE: PinnedPageEntry = {
28
+ id: "page-blog-index",
29
+ draftId: "drafts.page-blog-index",
30
+ title: "Blog Index",
31
+ icon: pageIcon,
32
+ path: "/blog",
33
+ slug: "blog",
34
+ };
35
+
36
+ export const PINNED_PAGE_ENTRIES: PinnedPageEntry[] = [
37
+ HOMEPAGE_PINNED_PAGE,
38
+ BLOG_INDEX_PINNED_PAGE,
39
+ ];
40
+
41
+ export const PINNED_PAGE_IDS: ReadonlySet<string> = new Set(
42
+ PINNED_PAGE_ENTRIES.flatMap((entry) => [entry.id, entry.draftId]),
43
+ );
44
+
45
+ const pinnedPageById = new Map(
46
+ PINNED_PAGE_ENTRIES.flatMap((entry) => [
47
+ [entry.id, entry] as const,
48
+ [entry.draftId, entry] as const,
49
+ ]),
50
+ );
51
+
52
+ /** Look up a pinned page entry by either its published or draft id. */
53
+ export const getPinnedPageById = (id: string | null | undefined) =>
54
+ id ? (pinnedPageById.get(id) ?? null) : null;
@@ -0,0 +1,119 @@
1
+ import type { Observable } from "rxjs";
2
+ import { map } from "rxjs/operators";
3
+ import type { DocumentStore } from "sanity";
4
+ import {
5
+ type DocumentLocationsState,
6
+ defineDocuments,
7
+ presentationTool,
8
+ } from "sanity/presentation";
9
+ import { previewURL } from "./env";
10
+ import { getPinnedPageById, HOMEPAGE_PINNED_PAGE } from "./pinned-pages";
11
+
12
+ const MAX_FOLDER_DEPTH = 6;
13
+
14
+ const buildNestedSlugExpression = (
15
+ parentReferenceField: string,
16
+ leafSlugField: string,
17
+ ) => {
18
+ const cases = Array.from({ length: MAX_FOLDER_DEPTH }, (_, index) => {
19
+ const depth = MAX_FOLDER_DEPTH - index;
20
+ const parentSlugRefs = Array.from({ length: depth }, (_, parentIndex) => {
21
+ const remainingDepth = depth - parentIndex - 1;
22
+ return `${parentReferenceField}${"->parentFolder".repeat(remainingDepth)}->slug.current`;
23
+ });
24
+
25
+ const pathDefinedChecks = parentSlugRefs
26
+ .map((field) => `defined(${field})`)
27
+ .join(" && ");
28
+
29
+ const childPath = [...parentSlugRefs, leafSlugField].join(' + "/" + ');
30
+ const folderRootPath = parentSlugRefs.join(' + "/" + ');
31
+
32
+ return [
33
+ `${pathDefinedChecks} && ${leafSlugField} == "/" => ${folderRootPath}`,
34
+ `${pathDefinedChecks} && defined(${leafSlugField}) => ${childPath}`,
35
+ ];
36
+ }).flat();
37
+
38
+ return `select(${cases.join(",\n ")},${leafSlugField})`;
39
+ };
40
+
41
+ const pageResolvedSlugExpression = buildNestedSlugExpression(
42
+ "pageFolder",
43
+ "slug.current",
44
+ );
45
+
46
+ const CONTENT_TYPE_PREFIXES: Record<string, string> = {
47
+ blogContent: "blog",
48
+ };
49
+
50
+ const resolveSlugLocation = <T extends { title?: string }>(
51
+ documentStore: DocumentStore,
52
+ id: string,
53
+ projection: string,
54
+ toHref: (doc: T) => string | null,
55
+ fallbackTitle: string,
56
+ ): Observable<DocumentLocationsState> =>
57
+ documentStore
58
+ .listenQuery(`*[_id == $id][0]{ ${projection} }`, { id }, {})
59
+ .pipe(
60
+ map((doc: T | null) => {
61
+ const href = doc ? toHref(doc) : null;
62
+ if (!href) return { locations: [] };
63
+ return { locations: [{ title: doc?.title || fallbackTitle, href }] };
64
+ }),
65
+ );
66
+
67
+ export const presentation = presentationTool({
68
+ name: "preview",
69
+ title: "Preview",
70
+ resolve: {
71
+ mainDocuments: defineDocuments([
72
+ {
73
+ route: "/",
74
+ filter: `_type == "page" && _id == "${HOMEPAGE_PINNED_PAGE.id}"`,
75
+ },
76
+ ...Object.entries(CONTENT_TYPE_PREFIXES).map(([type, prefix]) => ({
77
+ route: `/${prefix}/:slug`,
78
+ filter: `_type == "${type}" && slug.current == $slug`,
79
+ })),
80
+ {
81
+ route: "/:slug+",
82
+ filter: `_type == "page" && ${pageResolvedSlugExpression} == $slug`,
83
+ },
84
+ ]),
85
+ locations: (params, { documentStore }) => {
86
+ if (params.type === "page") {
87
+ const pinned = getPinnedPageById(params.id);
88
+ if (pinned) {
89
+ return { locations: [{ title: pinned.title, href: pinned.path }] };
90
+ }
91
+ return resolveSlugLocation<{ title?: string; resolvedSlug?: string }>(
92
+ documentStore,
93
+ params.id,
94
+ `title, "resolvedSlug": ${pageResolvedSlugExpression}`,
95
+ (doc) => (doc.resolvedSlug ? `/${doc.resolvedSlug}` : null),
96
+ "Page",
97
+ );
98
+ }
99
+
100
+ const prefix = CONTENT_TYPE_PREFIXES[params.type];
101
+ if (!prefix) return null;
102
+
103
+ return resolveSlugLocation<{ title?: string; slug?: string }>(
104
+ documentStore,
105
+ params.id,
106
+ `title, "slug": slug.current`,
107
+ (doc) => (doc.slug ? `/${prefix}/${doc.slug}` : null),
108
+ "Untitled",
109
+ );
110
+ },
111
+ },
112
+ previewUrl: {
113
+ origin: previewURL,
114
+ draftMode: {
115
+ enable: "/api/draft-mode/enable",
116
+ disable: "/api/draft-mode/disable",
117
+ },
118
+ },
119
+ });