canopycms 0.0.67 → 0.0.68-int.100

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 (268) hide show
  1. package/README.md +21 -14
  2. package/dist/ai/handler.js +8 -0
  3. package/dist/api/admin-branch-health.js +17 -20
  4. package/dist/api/admin.d.ts +30 -9
  5. package/dist/api/admin.js +32 -2
  6. package/dist/api/assets.js +84 -76
  7. package/dist/api/branch-create-window.d.ts +12 -0
  8. package/dist/api/branch-create-window.js +13 -0
  9. package/dist/api/branch-review.d.ts +3 -3
  10. package/dist/api/branch-review.js +14 -5
  11. package/dist/api/branch-status.d.ts +5 -5
  12. package/dist/api/branch-status.js +26 -6
  13. package/dist/api/branch-withdraw.d.ts +2 -2
  14. package/dist/api/branch-withdraw.js +7 -2
  15. package/dist/api/branch.d.ts +15 -2
  16. package/dist/api/branch.js +111 -92
  17. package/dist/api/client.d.ts +18 -6
  18. package/dist/api/client.js +79 -12
  19. package/dist/api/content.d.ts +6 -5
  20. package/dist/api/content.js +44 -41
  21. package/dist/api/entries.js +16 -15
  22. package/dist/api/github-sync.d.ts +10 -1
  23. package/dist/api/github-sync.js +20 -10
  24. package/dist/api/index.d.ts +1 -1
  25. package/dist/api/reference-options.js +1 -1
  26. package/dist/api/resolve-references.js +14 -40
  27. package/dist/api/settings-helpers.d.ts +1 -1
  28. package/dist/api/settings-helpers.js +1 -4
  29. package/dist/api/user.d.ts +3 -0
  30. package/dist/api/user.js +3 -0
  31. package/dist/assets/asset-url.d.ts +22 -16
  32. package/dist/assets/asset-url.js +42 -25
  33. package/dist/assets/factory.d.ts +5 -0
  34. package/dist/assets/factory.js +5 -0
  35. package/dist/assets/index.d.ts +1 -1
  36. package/dist/assets/materialize.d.ts +122 -0
  37. package/dist/assets/materialize.js +305 -0
  38. package/dist/assets/store-local.d.ts +4 -0
  39. package/dist/assets/store-local.js +11 -0
  40. package/dist/assets/store-s3.d.ts +10 -1
  41. package/dist/assets/store-s3.js +55 -3
  42. package/dist/assets/transform-directives.d.ts +52 -13
  43. package/dist/assets/transform-directives.js +87 -27
  44. package/dist/assets/transform.d.ts +3 -3
  45. package/dist/assets/transform.js +13 -8
  46. package/dist/assets/types.d.ts +14 -1
  47. package/dist/auth/file-based-auth-cache.js +7 -8
  48. package/dist/authorization/content.d.ts +5 -4
  49. package/dist/authorization/content.js +5 -5
  50. package/dist/authorization/path.d.ts +11 -6
  51. package/dist/authorization/path.js +11 -6
  52. package/dist/authorization/types.d.ts +2 -2
  53. package/dist/branch-health.d.ts +7 -1
  54. package/dist/branch-health.js +8 -3
  55. package/dist/branch-metadata.d.ts +5 -5
  56. package/dist/branch-metadata.js +44 -36
  57. package/dist/branch-provisioning.d.ts +167 -0
  58. package/dist/branch-provisioning.js +501 -0
  59. package/dist/branch-schema-cache.d.ts +5 -3
  60. package/dist/branch-schema-cache.js +29 -11
  61. package/dist/branch-sparse.d.ts +23 -0
  62. package/dist/branch-sparse.js +118 -0
  63. package/dist/branch-workspace.d.ts +42 -3
  64. package/dist/branch-workspace.js +240 -94
  65. package/dist/build/asset-refs.d.ts +93 -0
  66. package/dist/build/asset-refs.js +251 -0
  67. package/dist/build-identity.d.ts +3 -0
  68. package/dist/build-identity.js +13 -0
  69. package/dist/cli/asset-refs.d.ts +30 -0
  70. package/dist/cli/asset-refs.js +168 -0
  71. package/dist/cli/cli.d.ts +2 -1
  72. package/dist/cli/cli.js +4843 -1870
  73. package/dist/cli/generate-ai-content.js +1584 -444
  74. package/dist/cli/github-app-manifest.d.ts +4 -3
  75. package/dist/cli/github-app-manifest.js +4 -3
  76. package/dist/cli/init.js +30 -27
  77. package/dist/cli/migrate.js +12 -8
  78. package/dist/cli/sync.js +83 -37
  79. package/dist/cli/template-files/Dockerfile.cms.template +8 -0
  80. package/dist/cli/template-files/cdk-app.ts.template +4 -0
  81. package/dist/cli/template-files/cms-stack.ts.template +23 -12
  82. package/dist/cli/template-files/deploy-cms.yml.template +2 -0
  83. package/dist/config/helpers.js +1 -2
  84. package/dist/config/schemas/config.d.ts +19 -32
  85. package/dist/config/schemas/config.js +9 -11
  86. package/dist/config/schemas/field.js +1 -0
  87. package/dist/config/schemas/media.d.ts +4 -13
  88. package/dist/config/schemas/media.js +5 -8
  89. package/dist/config/schemas/url.d.ts +4 -20
  90. package/dist/config/schemas/url.js +6 -22
  91. package/dist/config/types.d.ts +30 -20
  92. package/dist/config/validation.d.ts +6 -0
  93. package/dist/config/validation.js +37 -0
  94. package/dist/content-listing.d.ts +12 -12
  95. package/dist/content-listing.js +10 -6
  96. package/dist/content-reader.d.ts +11 -2
  97. package/dist/content-reader.js +22 -14
  98. package/dist/content-store.d.ts +37 -9
  99. package/dist/content-store.js +70 -43
  100. package/dist/content-tree.d.ts +1 -1
  101. package/dist/content-tree.js +2 -2
  102. package/dist/context.d.ts +27 -9
  103. package/dist/context.js +154 -70
  104. package/dist/editor/BranchManager.d.ts +7 -1
  105. package/dist/editor/BranchManager.js +29 -14
  106. package/dist/editor/CanopyEditor.d.ts +1 -1
  107. package/dist/editor/CanopyEditor.js +2 -6
  108. package/dist/editor/Editor.d.ts +4 -3
  109. package/dist/editor/Editor.js +24 -20
  110. package/dist/editor/FormRenderer.js +34 -18
  111. package/dist/editor/admin/SystemHealthPanel.js +47 -7
  112. package/dist/editor/components/BranchesDrawer.d.ts +9 -0
  113. package/dist/editor/components/BranchesDrawer.js +4 -0
  114. package/dist/editor/components/NoEditPermissionNotice.d.ts +10 -0
  115. package/dist/editor/components/NoEditPermissionNotice.js +17 -0
  116. package/dist/editor/context/AssetContext.d.ts +18 -35
  117. package/dist/editor/context/AssetContext.js +20 -29
  118. package/dist/editor/context/index.d.ts +1 -1
  119. package/dist/editor/context/index.js +1 -1
  120. package/dist/editor/editor-config.d.ts +1 -2
  121. package/dist/editor/editor-config.js +0 -23
  122. package/dist/editor/editor-utils.d.ts +24 -21
  123. package/dist/editor/editor-utils.js +75 -62
  124. package/dist/editor/fields/BlockField.d.ts +1 -0
  125. package/dist/editor/fields/BlockField.js +6 -4
  126. package/dist/editor/fields/CodeField.d.ts +1 -0
  127. package/dist/editor/fields/CodeField.js +2 -2
  128. package/dist/editor/fields/DateTimeField.d.ts +1 -0
  129. package/dist/editor/fields/DateTimeField.js +2 -2
  130. package/dist/editor/fields/FieldDescription.d.ts +16 -0
  131. package/dist/editor/fields/FieldDescription.js +11 -0
  132. package/dist/editor/fields/ImageField.d.ts +1 -0
  133. package/dist/editor/fields/ImageField.js +3 -2
  134. package/dist/editor/fields/InlineGroupField.js +4 -1
  135. package/dist/editor/fields/MarkdownField.d.ts +1 -0
  136. package/dist/editor/fields/MarkdownField.js +115 -20
  137. package/dist/editor/fields/NumberField.d.ts +1 -0
  138. package/dist/editor/fields/NumberField.js +2 -2
  139. package/dist/editor/fields/NumberListField.d.ts +1 -0
  140. package/dist/editor/fields/NumberListField.js +2 -2
  141. package/dist/editor/fields/ObjectField.d.ts +1 -0
  142. package/dist/editor/fields/ObjectField.js +5 -2
  143. package/dist/editor/fields/ReferenceField.d.ts +1 -0
  144. package/dist/editor/fields/ReferenceField.js +5 -4
  145. package/dist/editor/fields/SelectField.d.ts +1 -0
  146. package/dist/editor/fields/SelectField.js +2 -2
  147. package/dist/editor/fields/StringListField.d.ts +1 -0
  148. package/dist/editor/fields/StringListField.js +2 -2
  149. package/dist/editor/fields/TextField.d.ts +1 -0
  150. package/dist/editor/fields/TextField.js +2 -2
  151. package/dist/editor/fields/ToggleField.d.ts +1 -0
  152. package/dist/editor/fields/ToggleField.js +7 -4
  153. package/dist/editor/fields/entry-link/InsertEntryLink.d.ts +1 -1
  154. package/dist/editor/fields/entry-link/InsertEntryLink.js +3 -2
  155. package/dist/editor/fields/mdx-jsx-support.d.ts +1 -0
  156. package/dist/editor/fields/mdx-jsx-support.js +137 -0
  157. package/dist/editor/hooks/create-branch-request.d.ts +20 -0
  158. package/dist/editor/hooks/create-branch-request.js +52 -0
  159. package/dist/editor/hooks/useBranchActions.d.ts +13 -2
  160. package/dist/editor/hooks/useBranchActions.js +48 -17
  161. package/dist/editor/hooks/useBranchManager.d.ts +11 -0
  162. package/dist/editor/hooks/useBranchManager.js +80 -2
  163. package/dist/editor/hooks/useBranchesData.d.ts +6 -0
  164. package/dist/editor/hooks/useBranchesData.js +9 -3
  165. package/dist/editor/hooks/useCommentSystem.js +25 -6
  166. package/dist/editor/hooks/useDraftManager.d.ts +16 -1
  167. package/dist/editor/hooks/useDraftManager.js +193 -36
  168. package/dist/editor/hooks/useEntriesData.js +3 -3
  169. package/dist/editor/hooks/useEntryManager.d.ts +2 -1
  170. package/dist/editor/hooks/useEntryManager.js +34 -19
  171. package/dist/editor/media/AssetCard.d.ts +1 -1
  172. package/dist/editor/media/crop-math.d.ts +3 -4
  173. package/dist/editor/media/crop-math.js +11 -22
  174. package/dist/editor/media/editor-image-src.d.ts +7 -0
  175. package/dist/editor/media/editor-image-src.js +12 -0
  176. package/dist/editor/preview-asset-base.d.ts +8 -0
  177. package/dist/editor/preview-asset-base.js +13 -0
  178. package/dist/editor/preview-bridge.d.ts +8 -1
  179. package/dist/editor/preview-bridge.js +15 -2
  180. package/dist/editor/preview-path.d.ts +10 -0
  181. package/dist/editor/preview-path.js +20 -0
  182. package/dist/editor/theme.js +2 -2
  183. package/dist/entry-schema-registry.d.ts +4 -4
  184. package/dist/entry-schema-registry.js +10 -6
  185. package/dist/entry-schema.d.ts +27 -4
  186. package/dist/entry-schema.js +20 -3
  187. package/dist/git-manager.d.ts +173 -6
  188. package/dist/git-manager.js +594 -76
  189. package/dist/github-service.d.ts +8 -0
  190. package/dist/github-service.js +5 -1
  191. package/dist/http/handler.js +37 -13
  192. package/dist/http/index.d.ts +2 -0
  193. package/dist/http/index.js +2 -0
  194. package/dist/http/router.d.ts +2 -0
  195. package/dist/http/router.js +2 -1
  196. package/dist/http/types.d.ts +2 -0
  197. package/dist/http/worker-not-ready.d.ts +10 -0
  198. package/dist/http/worker-not-ready.js +24 -0
  199. package/dist/operating-mode/client-unsafe-strategy.js +0 -6
  200. package/dist/operating-mode/types.d.ts +1 -4
  201. package/dist/paths/branch-name.d.ts +5 -0
  202. package/dist/paths/branch-name.js +9 -0
  203. package/dist/paths/branch.d.ts +6 -2
  204. package/dist/paths/branch.js +16 -10
  205. package/dist/paths/index.d.ts +3 -3
  206. package/dist/paths/index.js +3 -3
  207. package/dist/paths/normalize.d.ts +7 -0
  208. package/dist/paths/normalize.js +9 -0
  209. package/dist/paths/validation.js +0 -2
  210. package/dist/reference-resolver.d.ts +5 -21
  211. package/dist/reference-resolver.js +9 -44
  212. package/dist/resolve-canopy-user.js +2 -1
  213. package/dist/schema/schema-store.d.ts +54 -31
  214. package/dist/schema/schema-store.js +77 -31
  215. package/dist/server.d.ts +24 -9
  216. package/dist/server.js +22 -8
  217. package/dist/services.d.ts +18 -6
  218. package/dist/services.js +85 -70
  219. package/dist/settings-workspace.d.ts +23 -3
  220. package/dist/settings-workspace.js +197 -45
  221. package/dist/static/seo.d.ts +2 -14
  222. package/dist/static/seo.js +2 -25
  223. package/dist/submission-attribution.d.ts +73 -0
  224. package/dist/submission-attribution.js +221 -0
  225. package/dist/sync-core.d.ts +12 -1
  226. package/dist/sync-core.js +31 -13
  227. package/dist/task-queue/worker-status.d.ts +8 -0
  228. package/dist/task-queue/worker-status.js +17 -0
  229. package/dist/types.d.ts +31 -2
  230. package/dist/utils/content-write-lock.d.ts +18 -7
  231. package/dist/utils/content-write-lock.js +18 -9
  232. package/dist/utils/debug.d.ts +8 -0
  233. package/dist/utils/debug.js +10 -2
  234. package/dist/utils/git.d.ts +32 -0
  235. package/dist/utils/git.js +42 -0
  236. package/dist/utils/occ-json-write.js +10 -2
  237. package/dist/utils/provision-log.d.ts +25 -0
  238. package/dist/utils/provision-log.js +49 -0
  239. package/dist/utils/provisioning-lock.d.ts +35 -5
  240. package/dist/utils/provisioning-lock.js +73 -12
  241. package/dist/utils/request-timing.d.ts +25 -0
  242. package/dist/utils/request-timing.js +101 -0
  243. package/dist/utils/sanitize-href.d.ts +8 -22
  244. package/dist/utils/sanitize-href.js +11 -28
  245. package/dist/utils/url-prefix.d.ts +30 -0
  246. package/dist/utils/url-prefix.js +61 -0
  247. package/dist/validation/entry-validator.js +9 -3
  248. package/dist/version.d.ts +1 -0
  249. package/dist/version.js +3 -0
  250. package/dist/worker/canopy-state.d.ts +45 -0
  251. package/dist/worker/canopy-state.js +75 -0
  252. package/dist/worker/cms-worker.d.ts +8 -3
  253. package/dist/worker/cms-worker.js +38 -2
  254. package/dist/worker/git-sync.d.ts +30 -12
  255. package/dist/worker/git-sync.js +253 -58
  256. package/dist/worker/history-rewrite.d.ts +1 -1
  257. package/dist/worker/history-rewrite.js +1 -1
  258. package/dist/worker/provisioned-workspace.d.ts +35 -0
  259. package/dist/worker/provisioned-workspace.js +50 -0
  260. package/dist/worker/rebase.d.ts +1 -1
  261. package/dist/worker/rebase.js +95 -23
  262. package/dist/worker/remote-git-maintenance.d.ts +3 -0
  263. package/dist/worker/remote-git-maintenance.js +11 -0
  264. package/dist/worker/sparse-cone.d.ts +20 -0
  265. package/dist/worker/sparse-cone.js +132 -0
  266. package/dist/worker/task-runner.js +39 -4
  267. package/dist/worker/worker-context.d.ts +6 -1
  268. package/package.json +2 -1
@@ -1,4 +1,4 @@
1
- import type { CanopyConfig, FlatSchemaItem } from '../config/index.js';
1
+ import type { FlatSchemaItem } from '../config/index.js';
2
2
  import type { EditorCollection } from './Editor.js';
3
3
  /**
4
4
  * Build hierarchical editor collections from the flattened schema.
@@ -10,4 +10,3 @@ import type { EditorCollection } from './Editor.js';
10
10
  * shown as separate nodes in the navigation tree.
11
11
  */
12
12
  export declare const buildEditorCollections: (flatSchema: FlatSchemaItem[]) => EditorCollection[];
13
- export declare const buildPreviewBaseByCollection: (config: Pick<CanopyConfig, "contentRoot">, flatSchema: FlatSchemaItem[]) => Record<string, string>;
@@ -1,14 +1,3 @@
1
- import { trimSlashes } from '../paths/normalize.js';
2
- const normalizeContentRoot = (value) => {
3
- return trimSlashes(value ?? 'content');
4
- };
5
- const stripContentRoot = (logicalPath, contentRoot) => {
6
- const prefix = contentRoot ? `${contentRoot}/` : '';
7
- if (prefix && logicalPath.startsWith(prefix)) {
8
- return logicalPath.slice(prefix.length);
9
- }
10
- return logicalPath;
11
- };
12
1
  /**
13
2
  * Build hierarchical editor collections from the flattened schema.
14
3
  * Uses logicalPath as IDs to match API responses.
@@ -64,15 +53,3 @@ export const buildEditorCollections = (flatSchema) => {
64
53
  // This now includes the content root collection itself
65
54
  return buildTree(undefined);
66
55
  };
67
- export const buildPreviewBaseByCollection = (config, flatSchema) => {
68
- const contentRoot = normalizeContentRoot(config.contentRoot);
69
- const flat = flatSchema;
70
- const map = {};
71
- for (const item of flat) {
72
- // Strip content root from all items and normalize to preview URL
73
- const base = stripContentRoot(item.logicalPath, contentRoot);
74
- const normalizedBase = base ? `/${base}` : '/';
75
- map[item.logicalPath] = normalizedBase;
76
- }
77
- return map;
78
- };
@@ -3,36 +3,38 @@ import type { ContentFormat, FlatSchemaItem } from '../config/index.js';
3
3
  import type { FormValue } from './FormRenderer.js';
4
4
  import type { EditorEntry, EditorCollection } from './Editor.js';
5
5
  import type { TreeNodeData } from '@mantine/core';
6
- import { normalizeCollectionPath } from '../paths/normalize.js';
7
- /** @internal Exported for tests. */
8
- export { normalizeCollectionPath };
9
6
  export interface PreviewContext {
10
7
  branchName?: string;
11
- previewBaseByCollection?: Record<string, string>;
8
+ /** `editor.previewBase`: routes by root entry path, collection path or name; `false` is no page. */
9
+ previewBaseByCollection?: Record<string, string | false>;
10
+ /** `editor.previewPrefix`: where the host mounts the pages the preview pane loads. */
11
+ previewPrefix?: string;
12
12
  }
13
- /**
14
- * Builds the preview iframe `src` for an entry, prefixed with the deployment `basePath`
15
- * (`CanopyClientConfig.basePath`, e.g. `/preview-123`) when configured.
16
- *
17
- * This matters twice: the raw `<iframe src>` (`PreviewFrame` in preview-bridge.tsx) 404s
18
- * without the prefix under a basePath, and `resolvePreviewPath` there compares the same
19
- * string against `window.location.pathname` -- which browsers report WITH the basePath --
20
- * so an unprefixed `previewSrc` also breaks draft sync / click-to-focus even when the
21
- * iframe itself resolves.
22
- *
23
- * Applied via `joinUrlPrefix`: a no-op when `basePath` is unset, and passes an
24
- * already-absolute `previewSrc` (a cross-origin override) through untouched.
25
- */
26
- export declare const buildPreviewSrc: (entry: {
13
+ type PreviewEntry = {
27
14
  collectionPath?: string;
28
15
  collectionName?: string;
29
16
  slug?: string;
30
17
  itemType?: string;
31
18
  previewSrc?: string;
32
- }, context: PreviewContext & {
19
+ };
20
+ /**
21
+ * Builds the preview iframe `src` for an entry: its route under `previewPrefix`, under the
22
+ * deployment `basePath` (`CanopyClientConfig.basePath`, e.g. `/preview-123`), in the host's
23
+ * trailing-slash form, with `?branch=`. An absolute prefix skips the `basePath`. An absolute route
24
+ * (from `previewBaseByCollection`) skips all three and gets only `?branch=`. An entry's own
25
+ * `previewSrc` gets only the `basePath`.
26
+ *
27
+ * The result must equal the framed page's own URL: the `<iframe src>` (`PreviewFrame` in
28
+ * preview-bridge.tsx) 404s without the prefixes, and a URL the host redirects costs a round trip
29
+ * on every load. `trailingSlash` defaults to the value `withCanopy` inlines at build time.
30
+ *
31
+ * `undefined` means the entry has no page, so the editor frames nothing rather than another page.
32
+ */
33
+ export declare const buildPreviewSrc: (entry: PreviewEntry, context: PreviewContext & {
33
34
  contentRoot?: string;
34
35
  basePath?: string;
35
- }) => string;
36
+ trailingSlash?: boolean;
37
+ }) => string | undefined;
36
38
  export declare const normalizeContentPayload: (raw: unknown) => FormValue;
37
39
  export declare const buildWritePayload: (entry: {
38
40
  collectionPath?: string;
@@ -41,7 +43,7 @@ export declare const buildWritePayload: (entry: {
41
43
  }, value: FormValue) => FormValue;
42
44
  interface BuildEntriesFromListParams {
43
45
  response: ListEntriesResponse;
44
- resolvePreviewSrc: (entry: Pick<CollectionItem, 'collectionPath' | 'collectionName' | 'slug' | 'entryType'>) => string;
46
+ resolvePreviewSrc: (entry: Pick<CollectionItem, 'collectionPath' | 'collectionName' | 'slug' | 'entryType'>) => string | undefined;
45
47
  flatSchema: FlatSchemaItem[];
46
48
  }
47
49
  export declare const buildEntriesFromListResponse: ({ response, resolvePreviewSrc, flatSchema, }: BuildEntriesFromListParams) => EditorEntry[];
@@ -63,3 +65,4 @@ export declare const buildBreadcrumbSegments: (currentEntry: EditorEntry | undef
63
65
  * @returns Record<string, boolean> - Expanded state object where keys are collection node values
64
66
  */
65
67
  export declare const calculatePathToEntry: (entryPath: string | undefined, treeData: TreeNodeData[]) => Record<string, boolean>;
68
+ export {};
@@ -1,84 +1,97 @@
1
1
  // Import directly from normalize to avoid pulling in server-only branch.ts
2
- import { normalizeCollectionPath } from '../paths/normalize.js';
3
- import { isIndexSlug } from '../utils/entry-url.js';
2
+ import { normalizeFilesystemPath } from '../paths/normalize.js';
3
+ import { computeEntryUrl, isIndexSlug } from '../utils/entry-url.js';
4
4
  import { isDataOnlyFormat } from '../utils/format.js';
5
- import { joinUrlPrefix } from '../utils/url-prefix.js';
6
- /** @internal Exported for tests. */
7
- export { normalizeCollectionPath };
5
+ import { isAbsoluteUrl, joinUrlPrefix, matchTrailingSlash, readTrailingSlashEnv, stripTrailingSlashes, } from '../utils/url-prefix.js';
8
6
  /**
9
- * The slug portion of a preview URL, or '' for an index entry -- `resolveUrlPathCandidates`
7
+ * The slug a `previewBase` route is extended by, or '' for an index entry: an index entry's URL
8
+ * is its collection's, the collapse `computeEntryUrl` applies, and `resolveUrlPathCandidates`
10
9
  * refuses `/x/index`, so a preview built that way 404s.
11
- *
12
- * An index entry's URL is its COLLECTION's path, the same collapse `computeEntryUrl`,
13
- * `listEntries`, and `defaultBuildPath` apply.
14
- *
15
- * Kept separate from `computeEntryUrl`, which shares only the index decision: this builder
16
- * must percent-encode each segment and must NOT lowercase, since a preview base is
17
- * adopter-supplied and case-sensitive.
18
10
  */
19
- const encodePreviewSlug = (slug) => (isIndexSlug(slug) ? '' : encodeSlug(slug));
20
- const encodeSlug = (value) => (value ?? '')
11
+ const encodePreviewSlug = (slug) => (isIndexSlug(slug) ? '' : encodeSegments(slug));
12
+ const encodeSegments = (value) => (value ?? '')
21
13
  .split('/')
22
14
  .filter(Boolean)
23
15
  .map((segment) => encodeURIComponent(segment))
24
16
  .join('/');
17
+ /** Splits `url` before its query or fragment, so a path can be extended without entering either. */
18
+ const splitPathSuffix = (url) => {
19
+ const index = url.search(/[?#]/);
20
+ return index === -1 ? [url, ''] : [url.slice(0, index), url.slice(index)];
21
+ };
22
+ /** Adds `?branch=` to the query, ahead of any fragment. */
23
+ const appendBranch = (url, branchName) => {
24
+ if (!branchName)
25
+ return url;
26
+ const hashIndex = url.indexOf('#');
27
+ const beforeHash = hashIndex === -1 ? url : url.slice(0, hashIndex);
28
+ const hash = hashIndex === -1 ? '' : url.slice(hashIndex);
29
+ const separator = beforeHash.includes('?') ? '&' : '?';
30
+ return `${beforeHash}${separator}branch=${encodeURIComponent(branchName)}${hash}`;
31
+ };
25
32
  /**
26
- * Builds the (unprefixed) preview URL -- see `buildPreviewSrc` below, which wraps this with the
27
- * deployment `basePath`. Split out so that prefixing happens exactly once, at the end, uniformly
28
- * across every branch (including the `entry.previewSrc` escape hatch).
33
+ * The `previewBase` value under `key`, or `undefined` for none. An empty value counts as none, and
34
+ * so does anything `Object.prototype` supplies.
29
35
  */
30
- const buildRawPreviewSrc = (entry, { branchName, previewBaseByCollection, contentRoot }) => {
31
- if (entry.previewSrc)
32
- return entry.previewSrc;
33
- const appendBranch = (url) => {
34
- if (!branchName)
35
- return url;
36
- const separator = url.includes('?') ? '&' : '?';
37
- return `${url}${separator}branch=${encodeURIComponent(branchName)}`;
38
- };
39
- // Root-level entries have collectionPath === contentRoot (e.g., 'content')
40
- const isRootEntry = contentRoot && entry.collectionPath === contentRoot;
41
- if (isRootEntry) {
42
- const customPreview = previewBaseByCollection?.[`${contentRoot}/${entry.slug}`];
43
- if (customPreview) {
44
- return appendBranch(customPreview);
45
- }
46
- return appendBranch('/');
47
- }
48
- const base = (entry.collectionPath && previewBaseByCollection?.[entry.collectionPath]) ??
49
- (entry.collectionName && previewBaseByCollection?.[entry.collectionName]);
50
- if (!base) {
51
- // Pass contentRoot through so a non-default (or multi-segment, e.g.
52
- // "cms/content") configured root is stripped too; normalizeCollectionPath
53
- // defaults to 'content' when contentRoot is undefined.
54
- const collectionPath = entry.collectionPath
55
- ? normalizeCollectionPath(entry.collectionPath, contentRoot)
56
- : '';
57
- const encoded = encodePreviewSlug(entry.slug);
58
- const segments = [collectionPath, encoded].filter(Boolean);
59
- const url = segments.length > 0 ? `/${segments.join('/')}` : '/';
60
- return appendBranch(url);
36
+ const previewBaseFor = (bases, key) => bases && key && Object.prototype.hasOwnProperty.call(bases, key) && bases[key] !== ''
37
+ ? bases[key]
38
+ : undefined;
39
+ /**
40
+ * The entry's route on the host site, or `undefined` when it has no page. The first
41
+ * `previewBaseByCollection` key present decides: for a root entry its own path
42
+ * (`<contentRoot>/<slug>`, used as-is), then its collection path, then its collection name (both
43
+ * with the slug appended). The spelling `<parent>/<name>` names both an entry and a same-named
44
+ * sibling collection (a landing entry beside its folder), so below the root it is read only as a
45
+ * collection key, and at the root one key routes both.
46
+ * A `false` value means no page. With no key, the route is the entry's `urlPath`, by the rule
47
+ * `listEntries` publishes it, so a root entry previews at `/<slug>` and only a root index at `/`.
48
+ * Site-relative unless a matching value is absolute.
49
+ */
50
+ const buildPreviewRoute = (entry, { previewBaseByCollection, contentRoot = 'content' }) => {
51
+ const collectionPath = normalizeFilesystemPath(entry.collectionPath ?? '');
52
+ const isRootEntry = collectionPath === normalizeFilesystemPath(contentRoot);
53
+ const entryRoute = previewBaseFor(previewBaseByCollection, isRootEntry && entry.slug ? `${collectionPath}/${entry.slug}` : undefined);
54
+ if (entryRoute !== undefined)
55
+ return entryRoute === false ? undefined : entryRoute;
56
+ const pathBase = previewBaseFor(previewBaseByCollection, collectionPath);
57
+ const base = pathBase ?? previewBaseFor(previewBaseByCollection, entry.collectionName);
58
+ if (base === false)
59
+ return undefined;
60
+ if (base === undefined) {
61
+ const urlPath = computeEntryUrl(collectionPath, entry.slug ?? '', contentRoot);
62
+ return `/${encodeSegments(urlPath)}`;
61
63
  }
62
- const trimmed = base.endsWith('/') ? base.slice(0, -1) : base;
63
64
  const encoded = encodePreviewSlug(entry.slug);
64
- const url = encoded ? `${trimmed}/${encoded}` : trimmed || '/';
65
- return appendBranch(url);
65
+ const [basePathPart, suffix] = splitPathSuffix(base);
66
+ const trimmed = stripTrailingSlashes(basePathPart);
67
+ return `${encoded ? `${trimmed}/${encoded}` : basePathPart || '/'}${suffix}`;
66
68
  };
67
69
  /**
68
- * Builds the preview iframe `src` for an entry, prefixed with the deployment `basePath`
69
- * (`CanopyClientConfig.basePath`, e.g. `/preview-123`) when configured.
70
+ * Builds the preview iframe `src` for an entry: its route under `previewPrefix`, under the
71
+ * deployment `basePath` (`CanopyClientConfig.basePath`, e.g. `/preview-123`), in the host's
72
+ * trailing-slash form, with `?branch=`. An absolute prefix skips the `basePath`. An absolute route
73
+ * (from `previewBaseByCollection`) skips all three and gets only `?branch=`. An entry's own
74
+ * `previewSrc` gets only the `basePath`.
70
75
  *
71
- * This matters twice: the raw `<iframe src>` (`PreviewFrame` in preview-bridge.tsx) 404s
72
- * without the prefix under a basePath, and `resolvePreviewPath` there compares the same
73
- * string against `window.location.pathname` -- which browsers report WITH the basePath --
74
- * so an unprefixed `previewSrc` also breaks draft sync / click-to-focus even when the
75
- * iframe itself resolves.
76
+ * The result must equal the framed page's own URL: the `<iframe src>` (`PreviewFrame` in
77
+ * preview-bridge.tsx) 404s without the prefixes, and a URL the host redirects costs a round trip
78
+ * on every load. `trailingSlash` defaults to the value `withCanopy` inlines at build time.
76
79
  *
77
- * Applied via `joinUrlPrefix`: a no-op when `basePath` is unset, and passes an
78
- * already-absolute `previewSrc` (a cross-origin override) through untouched.
80
+ * `undefined` means the entry has no page, so the editor frames nothing rather than another page.
79
81
  */
80
82
  export const buildPreviewSrc = (entry, context) => {
81
- return joinUrlPrefix(context.basePath, buildRawPreviewSrc(entry, context));
83
+ if (entry.previewSrc)
84
+ return joinUrlPrefix(context.basePath, entry.previewSrc);
85
+ const route = buildPreviewRoute(entry, context);
86
+ if (route === undefined)
87
+ return undefined;
88
+ // An absolute route is another site's URL, so the host's trailing-slash form says nothing
89
+ // about it.
90
+ if (isAbsoluteUrl(route))
91
+ return appendBranch(route, context.branchName);
92
+ const mounted = joinUrlPrefix(context.basePath, joinUrlPrefix(context.previewPrefix, route));
93
+ const shaped = matchTrailingSlash(mounted, context.trailingSlash ?? readTrailingSlashEnv());
94
+ return appendBranch(shaped, context.branchName);
82
95
  };
83
96
  export const normalizeContentPayload = (raw) => {
84
97
  const candidate = raw;
@@ -7,6 +7,7 @@ export interface BlockInstance {
7
7
  type RenderField = (field: FieldConfig, value: unknown, onChange: (v: unknown) => void, path: Array<string | number>) => React.ReactNode;
8
8
  export interface BlockFieldProps {
9
9
  label?: string;
10
+ description?: string;
10
11
  templates: BlockConfig[];
11
12
  value: BlockInstance[];
12
13
  onChange: (blocks: BlockInstance[]) => void;
@@ -1,12 +1,13 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import React, { useMemo, useState } from 'react';
3
+ import React, { useId, useMemo, useState } from 'react';
4
4
  let blockKeyCounter = 0;
5
5
  import { ActionIcon, Button, Group, Paper, Select, Stack, Text } from '@mantine/core';
6
6
  import { DndContext, KeyboardSensor, PointerSensor, useSensor, useSensors, } from '@dnd-kit/core';
7
7
  import { SortableContext, arrayMove, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy, } from '@dnd-kit/sortable';
8
8
  import { CSS } from '@dnd-kit/utilities';
9
9
  import { formatCanopyPath } from '../canopy-path.js';
10
+ import { FieldDescription, groupDescriptionProps } from './FieldDescription.js';
10
11
  const findTemplate = (templates, name) => templates.find((t) => t.name === name);
11
12
  const SortableBlock = ({ id, children }) => {
12
13
  const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
@@ -19,7 +20,8 @@ const SortableBlock = ({ id, children }) => {
19
20
  };
20
21
  return (_jsx(Paper, { ref: setNodeRef, withBorder: true, radius: "md", p: "sm", shadow: "xs", style: style, children: _jsxs(Group, { align: "flex-start", gap: "sm", children: [_jsx(ActionIcon, { variant: "subtle", "aria-label": "Drag to reorder", ...attributes, ...listeners, style: { cursor: 'grab' }, children: "\u21C5" }, "drag-handle"), _jsx("div", { style: { flex: 1, minWidth: 0, width: '100%' }, children: children }, "content")] }) }));
21
22
  };
22
- export const BlockField = ({ label, templates, value, onChange, renderField, path, dataCanopyField, }) => {
23
+ export const BlockField = ({ label, description, templates, value, onChange, renderField, path, dataCanopyField, }) => {
24
+ const descriptionBaseId = useId();
23
25
  const [itemKeys, setItemKeys] = useState(() => value.map(() => `block-${blockKeyCounter++}`));
24
26
  const [pendingTemplate, setPendingTemplate] = useState(null);
25
27
  // Keeps itemKeys in sync with value's length; setState runs directly in render, not an effect.
@@ -69,12 +71,12 @@ export const BlockField = ({ label, templates, value, onChange, renderField, pat
69
71
  moveBlock(oldIndex, newIndex);
70
72
  };
71
73
  const selectableTemplates = useMemo(() => templates.map((t) => ({ value: t.name, label: t.label ?? t.name })), [templates]);
72
- return (_jsx(Paper, { withBorder: true, radius: "md", p: "md", bg: "gray.0", "data-canopy-field": dataCanopyField ?? formatCanopyPath(path), shadow: "xs", children: _jsxs(Stack, { gap: "sm", children: [_jsxs(Group, { justify: "space-between", children: [_jsx(Text, { size: "xs", fw: 700, c: "dimmed", children: label ?? 'Blocks' }), _jsx(Select, { "aria-label": "Add block", placeholder: "Add block...", data: selectableTemplates, value: pendingTemplate, onChange: (next) => {
74
+ return (_jsx(Paper, { withBorder: true, radius: "md", p: "md", bg: "gray.0", "data-canopy-field": dataCanopyField ?? formatCanopyPath(path), shadow: "xs", ...groupDescriptionProps(descriptionBaseId, description), children: _jsxs(Stack, { gap: "sm", children: [_jsxs(Group, { justify: "space-between", children: [_jsx(Text, { size: "xs", fw: 700, c: "dimmed", children: label ?? 'Blocks' }), _jsx(Select, { "aria-label": "Add block", placeholder: "Add block...", data: selectableTemplates, value: pendingTemplate, onChange: (next) => {
73
75
  if (next) {
74
76
  addBlock(next);
75
77
  }
76
78
  setPendingTemplate(null);
77
- }, allowDeselect: true, size: "xs", w: 180 })] }), _jsx(DndContext, { sensors: sensors, onDragEnd: handleDragEnd, children: _jsx(SortableContext, { items: itemKeys, strategy: verticalListSortingStrategy, children: _jsx(Stack, { gap: "sm", children: value.map((block, idx) => {
79
+ }, allowDeselect: true, size: "xs", w: 180 })] }), _jsx(FieldDescription, { baseId: descriptionBaseId, description: description }), _jsx(DndContext, { sensors: sensors, onDragEnd: handleDragEnd, children: _jsx(SortableContext, { items: itemKeys, strategy: verticalListSortingStrategy, children: _jsx(Stack, { gap: "sm", children: value.map((block, idx) => {
78
80
  const template = findTemplate(templates, block.template);
79
81
  const currentPath = [...path, idx];
80
82
  return (_jsx(SortableBlock, { id: itemKeys[idx], children: _jsxs(Stack, { gap: "xs", children: [_jsxs(Group, { justify: "space-between", align: "flex-start", children: [_jsx(Text, { size: "sm", fw: 600, children: template?.label ?? block.template ?? 'Unknown block' }), _jsxs(Group, { gap: 4, children: [_jsx(ActionIcon, { variant: "light", "aria-label": "Move block up", disabled: idx === 0, onClick: () => moveBlock(idx, idx - 1), children: "\u2191" }), _jsx(ActionIcon, { variant: "light", "aria-label": "Move block down", disabled: idx === value.length - 1, onClick: () => moveBlock(idx, idx + 1), children: "\u2193" }), _jsx(Button, { variant: "subtle", color: "red", size: "xs", onClick: () => removeBlock(idx), children: "Remove" })] })] }), template ? (_jsx(Stack, { gap: "sm", children: template.fields.map((f) => (_jsx(React.Fragment, { children: renderField(f, block.value?.[f.name], (next) => updateBlockValue(idx, {
@@ -2,6 +2,7 @@ import React from 'react';
2
2
  export interface CodeFieldProps {
3
3
  id?: string;
4
4
  label?: string;
5
+ description?: string;
5
6
  value: string;
6
7
  onChange: (value: string) => void;
7
8
  language?: string;
@@ -2,8 +2,8 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useId } from 'react';
3
3
  import { Textarea } from '@mantine/core';
4
4
  // Placeholder for Monaco integration; host app can provide custom renderer for production.
5
- export const CodeField = ({ id, label, value, onChange, language, dataCanopyField, }) => {
5
+ export const CodeField = ({ id, label, description, value, onChange, language, dataCanopyField, }) => {
6
6
  const generatedId = useId();
7
7
  const inputId = id ?? generatedId;
8
- return (_jsx(Textarea, { id: inputId, label: label, value: value, onChange: (e) => onChange(e.currentTarget.value), placeholder: language ? `Code (${language})` : 'Code', autosize: true, minRows: 6, size: "sm", "data-canopy-field": dataCanopyField, styles: { input: { fontFamily: 'Menlo, Consolas, monospace' } } }));
8
+ return (_jsx(Textarea, { id: inputId, label: label, description: description, value: value, onChange: (e) => onChange(e.currentTarget.value), placeholder: language ? `Code (${language})` : 'Code', autosize: true, minRows: 6, size: "sm", "data-canopy-field": dataCanopyField, styles: { input: { fontFamily: 'Menlo, Consolas, monospace' } } }));
9
9
  };
@@ -2,6 +2,7 @@ import React from 'react';
2
2
  export interface DateTimeFieldProps {
3
3
  id?: string;
4
4
  label?: string;
5
+ description?: string;
5
6
  value: string;
6
7
  onChange: (value: string) => void;
7
8
  dataCanopyField?: string;
@@ -49,8 +49,8 @@ export function datetimeLocalValueToIso(local) {
49
49
  * round-trip; sub-second precision is not preserved (no milliseconds field),
50
50
  * so a value with non-zero milliseconds is zeroed out once resaved.
51
51
  */
52
- export const DateTimeField = ({ id, label, value, onChange, dataCanopyField, }) => {
52
+ export const DateTimeField = ({ id, label, description, value, onChange, dataCanopyField, }) => {
53
53
  const generatedId = useId();
54
54
  const inputId = id ?? generatedId;
55
- return (_jsx(Input.Wrapper, { id: inputId, label: label, size: "sm", children: _jsx(Input, { type: "datetime-local", step: 1, size: "sm", value: isoToDatetimeLocalValue(value), onChange: (e) => onChange(datetimeLocalValueToIso(e.currentTarget.value)), "data-canopy-field": dataCanopyField }) }));
55
+ return (_jsx(Input.Wrapper, { id: inputId, label: label, description: description, size: "sm", children: _jsx(Input, { type: "datetime-local", step: 1, size: "sm", value: isoToDatetimeLocalValue(value), onChange: (e) => onChange(datetimeLocalValueToIso(e.currentTarget.value)), "data-canopy-field": dataCanopyField }) }));
56
56
  };
@@ -0,0 +1,16 @@
1
+ import React from 'react';
2
+ /** The id Mantine gives a native input's description, so custom fields match it. */
3
+ export declare const fieldDescriptionId: (baseId: string) => string;
4
+ /**
5
+ * Guidance text for fields that don't use a Mantine input's native `description` prop.
6
+ * Renders nothing for an absent or empty description.
7
+ */
8
+ export declare const FieldDescription: React.FC<{
9
+ baseId: string;
10
+ description?: string;
11
+ }>;
12
+ /** Props that tie a custom field's container to its `FieldDescription`; empty without one. */
13
+ export declare const groupDescriptionProps: (baseId: string, description: string | undefined) => {
14
+ role?: "group";
15
+ "aria-describedby"?: string;
16
+ };
@@ -0,0 +1,11 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Input } from '@mantine/core';
3
+ /** The id Mantine gives a native input's description, so custom fields match it. */
4
+ export const fieldDescriptionId = (baseId) => `${baseId}-description`;
5
+ /**
6
+ * Guidance text for fields that don't use a Mantine input's native `description` prop.
7
+ * Renders nothing for an absent or empty description.
8
+ */
9
+ export const FieldDescription = ({ baseId, description, }) => description ? (_jsx(Input.Description, { id: fieldDescriptionId(baseId), children: description })) : null;
10
+ /** Props that tie a custom field's container to its `FieldDescription`; empty without one. */
11
+ export const groupDescriptionProps = (baseId, description) => description ? { role: 'group', 'aria-describedby': fieldDescriptionId(baseId) } : {};
@@ -8,6 +8,7 @@ interface ImageFieldErrors {
8
8
  export interface ImageFieldProps {
9
9
  id?: string;
10
10
  label?: string;
11
+ description?: string;
11
12
  value: ImageFieldValue | undefined;
12
13
  onChange: (value: ImageFieldValue | undefined) => void;
13
14
  /** "W:H" aspect ratio - when set, picking/uploading a new image opens the crop step before the value commits, and a "Crop" button appears on the filled state. */
@@ -11,8 +11,9 @@ import { parseAspectRatio } from '../media/crop-math.js';
11
11
  import { MediaLibrary } from '../media/MediaLibrary.js';
12
12
  import { useAssetUpload } from '../media/useAssetUpload.js';
13
13
  import { ACCEPTED_IMAGE_MIME_TYPES, MAX_UPLOAD_BYTES } from '../media/upload-constants.js';
14
+ import { FieldDescription, groupDescriptionProps } from './FieldDescription.js';
14
15
  const PREVIEW_WIDTH = 320;
15
- export const ImageField = ({ id, label, value, onChange, aspect, altOptional, dataCanopyField, errors, }) => {
16
+ export const ImageField = ({ id, label, description, value, onChange, aspect, altOptional, dataCanopyField, errors, }) => {
16
17
  const generatedId = useId();
17
18
  const inputId = id ?? generatedId;
18
19
  const { baseUrl } = useAssetContext();
@@ -101,7 +102,7 @@ export const ImageField = ({ id, label, value, onChange, aspect, altOptional, da
101
102
  ? assetUrl({ src: value.src }, { baseUrl })
102
103
  : '';
103
104
  const cropInitial = cropRequest?.kind === 'existing' ? value?.crop : undefined;
104
- return (_jsxs(Stack, { gap: 4, "data-canopy-field": dataCanopyField, "data-testid": `image-field-${dataCanopyField}`, children: [label && (_jsx(Text, { size: "sm", fw: 500, children: label })), !hasValue ? (_jsxs(Stack, { gap: "xs", children: [_jsx(Dropzone, { onDrop: (files) => void handleDrop(files), onReject: handleReject, maxSize: MAX_UPLOAD_BYTES, accept: ACCEPTED_IMAGE_MIME_TYPES, loading: upload.uploading, multiple: false, "data-testid": `image-field-dropzone-${dataCanopyField}`, children: _jsxs(Group, { justify: "center", gap: "xs", py: "xs", style: { pointerEvents: 'none' }, children: [_jsx(IconUpload, { size: 18 }), _jsx(Text, { size: "xs", children: "Drop an image here, or click to browse" })] }) }), _jsx(Group, { justify: "center", children: _jsx(Button, { variant: "light", size: "xs", leftSection: _jsx(IconPhoto, { size: 14 }), onClick: () => setPickerOpen(true), "data-testid": `image-field-browse-library-${dataCanopyField}`, children: "Browse library" }) }), (upload.error || dropError) && (_jsx(Alert, { icon: _jsx(IconAlertCircle, { size: 16 }), color: "red", children: upload.error || dropError })), errors?.src && (_jsx(Text, { size: "xs", c: "red", children: errors.src }))] })) : (_jsxs(Stack, { gap: "xs", children: [previewFailed ? (_jsxs("div", { "data-testid": `image-field-preview-fallback-${dataCanopyField}`, style: {
105
+ return (_jsxs(Stack, { gap: 4, "data-canopy-field": dataCanopyField, "data-testid": `image-field-${dataCanopyField}`, ...groupDescriptionProps(inputId, description), children: [label && (_jsx(Text, { size: "sm", fw: 500, children: label })), _jsx(FieldDescription, { baseId: inputId, description: description }), !hasValue ? (_jsxs(Stack, { gap: "xs", children: [_jsx(Dropzone, { onDrop: (files) => void handleDrop(files), onReject: handleReject, maxSize: MAX_UPLOAD_BYTES, accept: ACCEPTED_IMAGE_MIME_TYPES, loading: upload.uploading, multiple: false, "data-testid": `image-field-dropzone-${dataCanopyField}`, children: _jsxs(Group, { justify: "center", gap: "xs", py: "xs", style: { pointerEvents: 'none' }, children: [_jsx(IconUpload, { size: 18 }), _jsx(Text, { size: "xs", children: "Drop an image here, or click to browse" })] }) }), _jsx(Group, { justify: "center", children: _jsx(Button, { variant: "light", size: "xs", leftSection: _jsx(IconPhoto, { size: 14 }), onClick: () => setPickerOpen(true), "data-testid": `image-field-browse-library-${dataCanopyField}`, children: "Browse library" }) }), (upload.error || dropError) && (_jsx(Alert, { icon: _jsx(IconAlertCircle, { size: 16 }), color: "red", children: upload.error || dropError })), errors?.src && (_jsx(Text, { size: "xs", c: "red", children: errors.src }))] })) : (_jsxs(Stack, { gap: "xs", children: [previewFailed ? (_jsxs("div", { "data-testid": `image-field-preview-fallback-${dataCanopyField}`, style: {
105
106
  width: PREVIEW_WIDTH,
106
107
  height: 160,
107
108
  display: 'flex',
@@ -1,8 +1,11 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useId } from 'react';
2
3
  import { Paper, Stack, Text } from '@mantine/core';
3
4
  import { formatCanopyPath } from '../canopy-path.js';
5
+ import { FieldDescription, groupDescriptionProps } from './FieldDescription.js';
4
6
  export const InlineGroupField = ({ label, description, fields, value, onChange, renderField, path, }) => {
5
- return (_jsx(Paper, { withBorder: true, radius: "md", p: "md", bg: "gray.0", shadow: "xs", children: _jsxs(Stack, { gap: "sm", children: [(label || description) && (_jsxs(Stack, { gap: 2, children: [label && (_jsx(Text, { size: "xs", fw: 700, c: "dimmed", children: label })), description && (_jsx(Text, { size: "xs", c: "dimmed", children: description }))] })), _jsx(Stack, { gap: "sm", children: fields.map((field) => {
7
+ const descriptionBaseId = useId();
8
+ return (_jsx(Paper, { withBorder: true, radius: "md", p: "md", bg: "gray.0", shadow: "xs", ...groupDescriptionProps(descriptionBaseId, description), children: _jsxs(Stack, { gap: "sm", children: [(label || description) && (_jsxs(Stack, { gap: 2, children: [label && (_jsx(Text, { size: "xs", fw: 700, c: "dimmed", children: label })), _jsx(FieldDescription, { baseId: descriptionBaseId, description: description })] })), _jsx(Stack, { gap: "sm", children: fields.map((field) => {
6
9
  // Nested inline groups are also transparent — pass the same parent value/onChange
7
10
  if (field.type === 'group') {
8
11
  const childGroup = field;
@@ -2,6 +2,7 @@ import React from 'react';
2
2
  export interface MarkdownFieldProps {
3
3
  id?: string;
4
4
  label?: string;
5
+ description?: string;
5
6
  value: string;
6
7
  onChange: (value: string) => void;
7
8
  dataCanopyField?: string;