canopycms 0.0.67 → 0.0.68-int.101
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.
- package/README.md +24 -17
- package/dist/ai/handler.js +8 -0
- package/dist/api/admin-branch-health.js +17 -20
- package/dist/api/admin.d.ts +30 -9
- package/dist/api/admin.js +32 -2
- package/dist/api/assets.js +84 -76
- package/dist/api/branch-create-window.d.ts +12 -0
- package/dist/api/branch-create-window.js +13 -0
- package/dist/api/branch-review.d.ts +3 -3
- package/dist/api/branch-review.js +14 -5
- package/dist/api/branch-status.d.ts +5 -5
- package/dist/api/branch-status.js +26 -6
- package/dist/api/branch-withdraw.d.ts +2 -2
- package/dist/api/branch-withdraw.js +7 -2
- package/dist/api/branch.d.ts +15 -2
- package/dist/api/branch.js +118 -92
- package/dist/api/client.d.ts +18 -6
- package/dist/api/client.js +79 -12
- package/dist/api/content.d.ts +6 -5
- package/dist/api/content.js +44 -41
- package/dist/api/entries.js +16 -15
- package/dist/api/github-sync.d.ts +19 -1
- package/dist/api/github-sync.js +57 -10
- package/dist/api/index.d.ts +1 -1
- package/dist/api/reference-options.js +1 -1
- package/dist/api/resolve-references.js +14 -40
- package/dist/api/settings-helpers.d.ts +1 -1
- package/dist/api/settings-helpers.js +1 -4
- package/dist/api/user.d.ts +3 -0
- package/dist/api/user.js +3 -0
- package/dist/assets/asset-url.d.ts +22 -16
- package/dist/assets/asset-url.js +42 -25
- package/dist/assets/factory.d.ts +5 -0
- package/dist/assets/factory.js +5 -0
- package/dist/assets/finalize.js +3 -0
- package/dist/assets/index.d.ts +1 -1
- package/dist/assets/materialize.d.ts +153 -0
- package/dist/assets/materialize.js +402 -0
- package/dist/assets/store-local.d.ts +19 -5
- package/dist/assets/store-local.js +69 -10
- package/dist/assets/store-s3.d.ts +38 -7
- package/dist/assets/store-s3.js +128 -22
- package/dist/assets/transform-directives.d.ts +52 -13
- package/dist/assets/transform-directives.js +87 -27
- package/dist/assets/transform.d.ts +3 -3
- package/dist/assets/transform.js +13 -8
- package/dist/assets/types.d.ts +31 -4
- package/dist/auth/file-based-auth-cache.js +7 -8
- package/dist/authorization/content.d.ts +5 -4
- package/dist/authorization/content.js +5 -5
- package/dist/authorization/path.d.ts +11 -6
- package/dist/authorization/path.js +11 -6
- package/dist/authorization/types.d.ts +2 -2
- package/dist/branch-health.d.ts +7 -1
- package/dist/branch-health.js +8 -3
- package/dist/branch-metadata.d.ts +5 -5
- package/dist/branch-metadata.js +44 -36
- package/dist/branch-provisioning.d.ts +167 -0
- package/dist/branch-provisioning.js +501 -0
- package/dist/branch-schema-cache.d.ts +5 -3
- package/dist/branch-schema-cache.js +29 -11
- package/dist/branch-sparse.d.ts +23 -0
- package/dist/branch-sparse.js +118 -0
- package/dist/branch-workspace.d.ts +42 -3
- package/dist/branch-workspace.js +240 -94
- package/dist/build/asset-refs.d.ts +93 -0
- package/dist/build/asset-refs.js +251 -0
- package/dist/build-identity.d.ts +3 -0
- package/dist/build-identity.js +13 -0
- package/dist/cli/asset-refs.d.ts +52 -0
- package/dist/cli/asset-refs.js +194 -0
- package/dist/cli/cli.d.ts +15 -1
- package/dist/cli/cli.js +5928 -2195
- package/dist/cli/configured-asset-store.d.ts +12 -0
- package/dist/cli/configured-asset-store.js +46 -0
- package/dist/cli/generate-ai-content.js +2185 -587
- package/dist/cli/github-app-manifest.d.ts +4 -3
- package/dist/cli/github-app-manifest.js +4 -3
- package/dist/cli/init.js +30 -27
- package/dist/cli/migrate.js +12 -8
- package/dist/cli/sync.js +83 -37
- package/dist/cli/template-files/Dockerfile.cms.template +8 -0
- package/dist/cli/template-files/cdk-app.ts.template +4 -0
- package/dist/cli/template-files/cms-stack.ts.template +23 -12
- package/dist/cli/template-files/deploy-cms.yml.template +2 -0
- package/dist/client.d.ts +1 -1
- package/dist/client.js +1 -1
- package/dist/config/helpers.js +1 -2
- package/dist/config/schemas/config.d.ts +19 -32
- package/dist/config/schemas/config.js +9 -11
- package/dist/config/schemas/field.js +1 -0
- package/dist/config/schemas/media.d.ts +4 -13
- package/dist/config/schemas/media.js +5 -8
- package/dist/config/schemas/url.d.ts +4 -20
- package/dist/config/schemas/url.js +6 -22
- package/dist/config/types.d.ts +30 -20
- package/dist/config/validation.d.ts +6 -0
- package/dist/config/validation.js +37 -0
- package/dist/content-listing.d.ts +12 -12
- package/dist/content-listing.js +10 -6
- package/dist/content-reader.d.ts +11 -2
- package/dist/content-reader.js +22 -14
- package/dist/content-store.d.ts +37 -9
- package/dist/content-store.js +70 -43
- package/dist/content-tree.d.ts +1 -1
- package/dist/content-tree.js +2 -2
- package/dist/context.d.ts +27 -9
- package/dist/context.js +154 -70
- package/dist/editor/BranchManager.d.ts +7 -1
- package/dist/editor/BranchManager.js +29 -14
- package/dist/editor/CanopyEditor.d.ts +1 -1
- package/dist/editor/CanopyEditor.js +2 -6
- package/dist/editor/Editor.d.ts +4 -3
- package/dist/editor/Editor.js +32 -22
- package/dist/editor/FormRenderer.js +34 -18
- package/dist/editor/PreviewFrame.d.ts +24 -0
- package/dist/editor/PreviewFrame.js +140 -0
- package/dist/editor/admin/SystemHealthPanel.js +47 -7
- package/dist/editor/components/BranchesDrawer.d.ts +9 -0
- package/dist/editor/components/BranchesDrawer.js +4 -0
- package/dist/editor/components/NoEditPermissionNotice.d.ts +10 -0
- package/dist/editor/components/NoEditPermissionNotice.js +17 -0
- package/dist/editor/context/AssetContext.d.ts +18 -35
- package/dist/editor/context/AssetContext.js +20 -29
- package/dist/editor/context/index.d.ts +1 -1
- package/dist/editor/context/index.js +1 -1
- package/dist/editor/editor-config.d.ts +1 -2
- package/dist/editor/editor-config.js +0 -23
- package/dist/editor/editor-utils.d.ts +24 -21
- package/dist/editor/editor-utils.js +75 -62
- package/dist/editor/fields/BlockField.d.ts +1 -0
- package/dist/editor/fields/BlockField.js +6 -4
- package/dist/editor/fields/CodeField.d.ts +1 -0
- package/dist/editor/fields/CodeField.js +2 -2
- package/dist/editor/fields/DateTimeField.d.ts +1 -0
- package/dist/editor/fields/DateTimeField.js +2 -2
- package/dist/editor/fields/FieldDescription.d.ts +16 -0
- package/dist/editor/fields/FieldDescription.js +11 -0
- package/dist/editor/fields/ImageField.d.ts +1 -0
- package/dist/editor/fields/ImageField.js +3 -2
- package/dist/editor/fields/InlineGroupField.js +4 -1
- package/dist/editor/fields/MarkdownField.d.ts +1 -0
- package/dist/editor/fields/MarkdownField.js +115 -20
- package/dist/editor/fields/NumberField.d.ts +1 -0
- package/dist/editor/fields/NumberField.js +2 -2
- package/dist/editor/fields/NumberListField.d.ts +1 -0
- package/dist/editor/fields/NumberListField.js +2 -2
- package/dist/editor/fields/ObjectField.d.ts +1 -0
- package/dist/editor/fields/ObjectField.js +5 -2
- package/dist/editor/fields/ReferenceField.d.ts +1 -0
- package/dist/editor/fields/ReferenceField.js +5 -4
- package/dist/editor/fields/SelectField.d.ts +1 -0
- package/dist/editor/fields/SelectField.js +2 -2
- package/dist/editor/fields/StringListField.d.ts +1 -0
- package/dist/editor/fields/StringListField.js +2 -2
- package/dist/editor/fields/TextField.d.ts +1 -0
- package/dist/editor/fields/TextField.js +2 -2
- package/dist/editor/fields/ToggleField.d.ts +1 -0
- package/dist/editor/fields/ToggleField.js +7 -4
- package/dist/editor/fields/entry-link/InsertEntryLink.d.ts +1 -1
- package/dist/editor/fields/entry-link/InsertEntryLink.js +3 -2
- package/dist/editor/fields/mdx-jsx-support.d.ts +1 -0
- package/dist/editor/fields/mdx-jsx-support.js +137 -0
- package/dist/editor/hooks/create-branch-request.d.ts +20 -0
- package/dist/editor/hooks/create-branch-request.js +52 -0
- package/dist/editor/hooks/useBranchActions.d.ts +13 -2
- package/dist/editor/hooks/useBranchActions.js +48 -17
- package/dist/editor/hooks/useBranchManager.d.ts +21 -0
- package/dist/editor/hooks/useBranchManager.js +204 -37
- package/dist/editor/hooks/useBranchesData.d.ts +6 -0
- package/dist/editor/hooks/useBranchesData.js +9 -3
- package/dist/editor/hooks/useCommentSystem.js +25 -6
- package/dist/editor/hooks/useDraftManager.d.ts +16 -1
- package/dist/editor/hooks/useDraftManager.js +193 -36
- package/dist/editor/hooks/useEntriesData.js +3 -3
- package/dist/editor/hooks/useEntryManager.d.ts +2 -1
- package/dist/editor/hooks/useEntryManager.js +34 -19
- package/dist/editor/media/AssetCard.d.ts +1 -1
- package/dist/editor/media/crop-math.d.ts +3 -4
- package/dist/editor/media/crop-math.js +11 -22
- package/dist/editor/media/editor-image-src.d.ts +7 -0
- package/dist/editor/media/editor-image-src.js +12 -0
- package/dist/editor/preview-asset-base.d.ts +8 -0
- package/dist/editor/preview-asset-base.js +13 -0
- package/dist/editor/preview-bridge.d.ts +9 -21
- package/dist/editor/preview-bridge.js +19 -139
- package/dist/editor/preview-path.d.ts +10 -0
- package/dist/editor/preview-path.js +20 -0
- package/dist/editor/theme.js +2 -2
- package/dist/entry-schema-registry.d.ts +4 -4
- package/dist/entry-schema-registry.js +10 -6
- package/dist/entry-schema.d.ts +27 -4
- package/dist/entry-schema.js +20 -3
- package/dist/git-manager.d.ts +173 -6
- package/dist/git-manager.js +594 -76
- package/dist/github-service.d.ts +15 -0
- package/dist/github-service.js +19 -1
- package/dist/http/handler.js +37 -13
- package/dist/http/index.d.ts +2 -0
- package/dist/http/index.js +2 -0
- package/dist/http/router.d.ts +2 -0
- package/dist/http/router.js +2 -1
- package/dist/http/types.d.ts +2 -0
- package/dist/http/worker-not-ready.d.ts +10 -0
- package/dist/http/worker-not-ready.js +24 -0
- package/dist/operating-mode/client-unsafe-strategy.js +0 -6
- package/dist/operating-mode/types.d.ts +1 -4
- package/dist/paths/branch-name.d.ts +5 -0
- package/dist/paths/branch-name.js +9 -0
- package/dist/paths/branch.d.ts +6 -2
- package/dist/paths/branch.js +16 -10
- package/dist/paths/index.d.ts +3 -3
- package/dist/paths/index.js +3 -3
- package/dist/paths/normalize.d.ts +7 -0
- package/dist/paths/normalize.js +9 -0
- package/dist/paths/validation.js +0 -2
- package/dist/preview.d.ts +8 -0
- package/dist/preview.js +7 -0
- package/dist/reference-resolver.d.ts +5 -21
- package/dist/reference-resolver.js +9 -44
- package/dist/resolve-canopy-user.js +2 -1
- package/dist/schema/schema-store.d.ts +54 -31
- package/dist/schema/schema-store.js +77 -31
- package/dist/server.d.ts +64 -9
- package/dist/server.js +48 -8
- package/dist/services.d.ts +18 -6
- package/dist/services.js +85 -70
- package/dist/settings-workspace.d.ts +23 -3
- package/dist/settings-workspace.js +197 -45
- package/dist/static/seo.d.ts +2 -14
- package/dist/static/seo.js +2 -25
- package/dist/submission-attribution.d.ts +73 -0
- package/dist/submission-attribution.js +221 -0
- package/dist/sync-core.d.ts +12 -1
- package/dist/sync-core.js +31 -13
- package/dist/task-queue/worker-status.d.ts +8 -0
- package/dist/task-queue/worker-status.js +17 -0
- package/dist/types.d.ts +31 -2
- package/dist/utils/content-serialize.d.ts +5 -2
- package/dist/utils/content-serialize.js +98 -24
- package/dist/utils/content-write-lock.d.ts +18 -7
- package/dist/utils/content-write-lock.js +18 -9
- package/dist/utils/debug.d.ts +8 -0
- package/dist/utils/debug.js +10 -2
- package/dist/utils/git.d.ts +32 -0
- package/dist/utils/git.js +42 -0
- package/dist/utils/occ-json-write.js +10 -2
- package/dist/utils/provision-log.d.ts +25 -0
- package/dist/utils/provision-log.js +49 -0
- package/dist/utils/provisioning-lock.d.ts +35 -5
- package/dist/utils/provisioning-lock.js +73 -12
- package/dist/utils/request-timing.d.ts +25 -0
- package/dist/utils/request-timing.js +101 -0
- package/dist/utils/sanitize-href.d.ts +8 -22
- package/dist/utils/sanitize-href.js +11 -28
- package/dist/utils/url-prefix.d.ts +30 -0
- package/dist/utils/url-prefix.js +61 -0
- package/dist/utils/yaml-source-splice.d.ts +58 -0
- package/dist/utils/yaml-source-splice.js +515 -0
- package/dist/validation/entry-validator.js +9 -3
- package/dist/version.d.ts +1 -0
- package/dist/version.js +3 -0
- package/dist/worker/canopy-state.d.ts +45 -0
- package/dist/worker/canopy-state.js +75 -0
- package/dist/worker/cms-worker.d.ts +8 -3
- package/dist/worker/cms-worker.js +38 -2
- package/dist/worker/git-sync.d.ts +30 -12
- package/dist/worker/git-sync.js +253 -58
- package/dist/worker/history-rewrite.d.ts +1 -1
- package/dist/worker/history-rewrite.js +1 -1
- package/dist/worker/provisioned-workspace.d.ts +35 -0
- package/dist/worker/provisioned-workspace.js +50 -0
- package/dist/worker/rebase.d.ts +1 -1
- package/dist/worker/rebase.js +95 -23
- package/dist/worker/remote-git-maintenance.d.ts +3 -0
- package/dist/worker/remote-git-maintenance.js +11 -0
- package/dist/worker/sparse-cone.d.ts +20 -0
- package/dist/worker/sparse-cone.js +132 -0
- package/dist/worker/task-runner.js +99 -14
- package/dist/worker/worker-context.d.ts +6 -1
- package/package.json +8 -2
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface NoEditPermissionNoticeProps {
|
|
2
|
+
/** Logical path of the entry the user cannot edit. */
|
|
3
|
+
entryPath: string;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Explains why an entry cannot be edited and whom to ask. An entry's `canEdit` is false only
|
|
7
|
+
* when a path rule (or `defaultPathAccess`) denies edit, so that is the reason it gives; a
|
|
8
|
+
* locked or protected branch shows its own banner in the header and a read-only form instead.
|
|
9
|
+
*/
|
|
10
|
+
export declare function NoEditPermissionNotice({ entryPath }: NoEditPermissionNoticeProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Text } from '@mantine/core';
|
|
3
|
+
/**
|
|
4
|
+
* Explains why an entry cannot be edited and whom to ask. An entry's `canEdit` is false only
|
|
5
|
+
* when a path rule (or `defaultPathAccess`) denies edit, so that is the reason it gives; a
|
|
6
|
+
* locked or protected branch shows its own banner in the header and a read-only form instead.
|
|
7
|
+
*/
|
|
8
|
+
export function NoEditPermissionNotice({ entryPath }) {
|
|
9
|
+
return (_jsx("div", { role: "status", "data-testid": "no-edit-permission-notice", style: {
|
|
10
|
+
display: 'flex',
|
|
11
|
+
alignItems: 'center',
|
|
12
|
+
justifyContent: 'center',
|
|
13
|
+
height: '100%',
|
|
14
|
+
padding: '0 var(--mantine-spacing-md)',
|
|
15
|
+
textAlign: 'center',
|
|
16
|
+
}, children: _jsxs(Text, { size: "sm", c: "dimmed", children: ["You don't have edit access to \"", entryPath, "\". Ask a CanopyCMS admin to grant it in Manage Permissions."] }) }));
|
|
17
|
+
}
|
|
@@ -1,44 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Carries
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* `
|
|
7
|
-
* `EntryLinkContext` exists for the entry-link toolbar button). Unlike
|
|
8
|
-
* `ApiClientContext`, reading this without a provider is safe and intentional:
|
|
9
|
-
* no provider means root-relative URLs, which is the correct default for the
|
|
10
|
-
* common case (editor and site share an origin).
|
|
2
|
+
* Carries the prefix every editor-built asset URL is put behind - MediaLibrary thumbnails,
|
|
3
|
+
* ImageField's preview and crop source, and the MDX editor's image previews. A context (rather
|
|
4
|
+
* than a prop) because the MDX dialog is rendered deep inside MDXEditor's lazy-loaded internals
|
|
5
|
+
* via `imagePlugin`'s `ImageDialog` param, with no prop channel of its own (the same reason
|
|
6
|
+
* `EntryLinkContext` exists for the entry-link toolbar button).
|
|
11
7
|
*/
|
|
12
8
|
import React from 'react';
|
|
9
|
+
/**
|
|
10
|
+
* Where the editor loads asset bytes from: the authenticated raw route under `basePath`, never the
|
|
11
|
+
* public `/assets` space. An editor asks for derivatives no build has seen (a fresh crop, a
|
|
12
|
+
* thumbnail), which the public path need not serve, and the raw route computes them on demand.
|
|
13
|
+
* Also handed to the live preview, through `PreviewFrame`.
|
|
14
|
+
*/
|
|
15
|
+
export declare function authenticatedAssetBase(basePath?: string): string;
|
|
13
16
|
export interface AssetContextValue {
|
|
14
|
-
/**
|
|
15
|
-
baseUrl
|
|
17
|
+
/** The `baseUrl` to pass to `assetUrl` for every asset URL the editor renders. */
|
|
18
|
+
baseUrl: string;
|
|
16
19
|
}
|
|
17
|
-
export interface AssetContextProviderProps
|
|
20
|
+
export interface AssetContextProviderProps {
|
|
18
21
|
children: React.ReactNode;
|
|
19
|
-
/**
|
|
20
|
-
* `CanopyClientConfig.basePath` - the deployment prefix the host app is served under. Used only
|
|
21
|
-
* when `baseUrl` is unset.
|
|
22
|
-
*/
|
|
22
|
+
/** `CanopyClientConfig.basePath` - the deployment prefix the host app is served under. */
|
|
23
23
|
basePath?: string;
|
|
24
24
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
*
|
|
28
|
-
* They are ALTERNATIVES, never composed: whichever is used names where `/assets` is mounted, in
|
|
29
|
-
* full, and a deployment `basePath` cannot apply on top of it. Where the two differ is topology,
|
|
30
|
-
* not precedence - see the asset-mount table in the README.
|
|
31
|
-
*
|
|
32
|
-
* The fallback is kept only so that existing basePath deployments do not break on upgrade,
|
|
33
|
-
* which makes removing it a deliberate decision rather than a cleanup - it is option 1 of
|
|
34
|
-
* `.claude/future-tasks/editor-asset-mount-topology.md`, still open.
|
|
35
|
-
*
|
|
36
|
-
* KNOWN LIMITATION: on a CloudFront/CDK deployment, a basePath does NOT move the asset
|
|
37
|
-
* space (behaviors are anchored at the distribution root), so this fallback prefixes
|
|
38
|
-
* wrongly — requests still resolve, via `withCanopy`'s auto-prefixed `/assets/:path*`
|
|
39
|
-
* rewrite, but bypass the CDN cache and the dedicated transform Lambda. Workaround: set
|
|
40
|
-
* `media.publicBaseUrl` to the distribution origin instead, which takes precedence here.
|
|
41
|
-
*/
|
|
42
|
-
export declare function AssetContextProvider({ baseUrl, basePath, children }: AssetContextProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
43
|
-
/** Safe to call outside a provider - returns `{ baseUrl: undefined }` (root-relative URLs). */
|
|
25
|
+
export declare function AssetContextProvider({ basePath, children }: AssetContextProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
26
|
+
/** Safe to call outside a provider - see the context's default. */
|
|
44
27
|
export declare function useAssetContext(): AssetContextValue;
|
|
@@ -1,41 +1,32 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
/**
|
|
4
|
-
* Carries
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* `
|
|
9
|
-
* `EntryLinkContext` exists for the entry-link toolbar button). Unlike
|
|
10
|
-
* `ApiClientContext`, reading this without a provider is safe and intentional:
|
|
11
|
-
* no provider means root-relative URLs, which is the correct default for the
|
|
12
|
-
* common case (editor and site share an origin).
|
|
4
|
+
* Carries the prefix every editor-built asset URL is put behind - MediaLibrary thumbnails,
|
|
5
|
+
* ImageField's preview and crop source, and the MDX editor's image previews. A context (rather
|
|
6
|
+
* than a prop) because the MDX dialog is rendered deep inside MDXEditor's lazy-loaded internals
|
|
7
|
+
* via `imagePlugin`'s `ImageDialog` param, with no prop channel of its own (the same reason
|
|
8
|
+
* `EntryLinkContext` exists for the entry-link toolbar button).
|
|
13
9
|
*/
|
|
14
10
|
import { createContext, useContext, useMemo } from 'react';
|
|
15
|
-
|
|
11
|
+
import { joinUrlPrefix } from '../../utils/url-prefix.js';
|
|
12
|
+
/** The raw asset route (`api/assets.ts`'s `assetRawRoute`) under the default API base. */
|
|
13
|
+
const RAW_ASSET_ROUTE = '/api/canopycms/assets/raw';
|
|
16
14
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* not precedence - see the asset-mount table in the README.
|
|
22
|
-
*
|
|
23
|
-
* The fallback is kept only so that existing basePath deployments do not break on upgrade,
|
|
24
|
-
* which makes removing it a deliberate decision rather than a cleanup - it is option 1 of
|
|
25
|
-
* `.claude/future-tasks/editor-asset-mount-topology.md`, still open.
|
|
26
|
-
*
|
|
27
|
-
* KNOWN LIMITATION: on a CloudFront/CDK deployment, a basePath does NOT move the asset
|
|
28
|
-
* space (behaviors are anchored at the distribution root), so this fallback prefixes
|
|
29
|
-
* wrongly — requests still resolve, via `withCanopy`'s auto-prefixed `/assets/:path*`
|
|
30
|
-
* rewrite, but bypass the CDN cache and the dedicated transform Lambda. Workaround: set
|
|
31
|
-
* `media.publicBaseUrl` to the distribution origin instead, which takes precedence here.
|
|
15
|
+
* Where the editor loads asset bytes from: the authenticated raw route under `basePath`, never the
|
|
16
|
+
* public `/assets` space. An editor asks for derivatives no build has seen (a fresh crop, a
|
|
17
|
+
* thumbnail), which the public path need not serve, and the raw route computes them on demand.
|
|
18
|
+
* Also handed to the live preview, through `PreviewFrame`.
|
|
32
19
|
*/
|
|
33
|
-
export function
|
|
34
|
-
|
|
35
|
-
|
|
20
|
+
export function authenticatedAssetBase(basePath) {
|
|
21
|
+
return joinUrlPrefix(basePath, RAW_ASSET_ROUTE);
|
|
22
|
+
}
|
|
23
|
+
/** No provider means no `basePath`: the API is mounted at the origin root. */
|
|
24
|
+
const AssetContext = createContext({ baseUrl: authenticatedAssetBase() });
|
|
25
|
+
export function AssetContextProvider({ basePath, children }) {
|
|
26
|
+
const value = useMemo(() => ({ baseUrl: authenticatedAssetBase(basePath) }), [basePath]);
|
|
36
27
|
return _jsx(AssetContext.Provider, { value: value, children: children });
|
|
37
28
|
}
|
|
38
|
-
/** Safe to call outside a provider -
|
|
29
|
+
/** Safe to call outside a provider - see the context's default. */
|
|
39
30
|
export function useAssetContext() {
|
|
40
31
|
return useContext(AssetContext);
|
|
41
32
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { ApiClientProvider, useApiClient, useOptionalApiClient, type ApiClient, type ApiClientProviderProps, } from './ApiClientContext.js';
|
|
2
|
-
export { AssetContextProvider, useAssetContext, type AssetContextValue, type AssetContextProviderProps, } from './AssetContext.js';
|
|
2
|
+
export { AssetContextProvider, authenticatedAssetBase, useAssetContext, type AssetContextValue, type AssetContextProviderProps, } from './AssetContext.js';
|
|
3
3
|
export { SWRProvider, type SWRProviderProps } from './SWRProvider.js';
|
|
4
4
|
export { useEditorLoading, useEditorModals, useEditorPreview } from './EditorStateContext.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { ApiClientProvider, useApiClient, useOptionalApiClient, } from './ApiClientContext.js';
|
|
2
|
-
export { AssetContextProvider, useAssetContext, } from './AssetContext.js';
|
|
2
|
+
export { AssetContextProvider, authenticatedAssetBase, useAssetContext, } from './AssetContext.js';
|
|
3
3
|
export { SWRProvider } from './SWRProvider.js';
|
|
4
4
|
export { useEditorLoading, useEditorModals, useEditorPreview } from './EditorStateContext.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
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
|
-
|
|
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
|
-
}
|
|
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.tsx`)
|
|
28
|
+
* 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
|
-
|
|
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 {
|
|
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
|
|
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) ? '' :
|
|
20
|
-
const
|
|
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
|
-
*
|
|
27
|
-
*
|
|
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
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
65
|
-
|
|
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
|
|
69
|
-
* (`CanopyClientConfig.basePath`, e.g. `/preview-123`)
|
|
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
|
-
*
|
|
72
|
-
* without the
|
|
73
|
-
*
|
|
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.tsx`)
|
|
77
|
+
* 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
|
-
*
|
|
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
|
-
|
|
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,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
|
};
|
|
@@ -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',
|