camox 0.42.0 → 0.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/bin/camox.mjs +9 -1
  2. package/dist/core/components/lexical/{GradientNode.js → HighlightNode.js} +34 -34
  3. package/dist/core/components/lexical/InlineLexicalEditor.js +14 -14
  4. package/dist/core/components/lexical/InlineStylesPlugin.js +25 -9
  5. package/dist/core/components/lexical/SelectionBroadcaster.js +7 -7
  6. package/dist/core/components/lexical/SidebarLexicalEditor.js +24 -24
  7. package/dist/core/components/lexical/TextLinkPopover.js +191 -208
  8. package/dist/core/components/lexical/editorConfig.js +2 -2
  9. package/dist/core/createApp.d.ts +31 -4
  10. package/dist/core/createApp.js +2 -1
  11. package/dist/core/createBlock.js +24 -10
  12. package/dist/core/createLayout.d.ts +16 -3
  13. package/dist/core/createLayout.js +15 -15
  14. package/dist/core/derivedRoutes.js +1 -1
  15. package/dist/core/dom.d.ts +17 -0
  16. package/dist/core/dom.js +22 -0
  17. package/dist/core/editing/createEditableBlock.d.ts +135 -1
  18. package/dist/core/editing/createEditableBlock.js +209 -74
  19. package/dist/core/lib/contentType.d.ts +5 -0
  20. package/dist/core/lib/contentType.js +11 -0
  21. package/dist/core/lib/fieldTypes.js +1 -0
  22. package/dist/core/lib/iconTypes.d.ts +13 -0
  23. package/dist/core/lib/icons.d.ts +6 -0
  24. package/dist/core/lib/icons.js +130 -0
  25. package/dist/core/lib/lexicalReact.js +4 -4
  26. package/dist/core/lib/lexicalState.js +8 -8
  27. package/dist/core/lib/modifierFormats.js +2 -2
  28. package/dist/core/lib/textLinks.js +5 -2
  29. package/dist/core/lib/textStyles.d.ts +3 -3
  30. package/dist/core/lib/textStyles.js +9 -14
  31. package/dist/core/pageDestinations.js +37 -0
  32. package/dist/features/content/components/AssetCard.js +104 -68
  33. package/dist/features/og/imageResponse.node.js +11 -9
  34. package/dist/features/preview/CamoxPreview.js +148 -146
  35. package/dist/features/preview/commentsEnabled.js +2 -2
  36. package/dist/features/preview/components/AddBlockSidebar.js +1 -1
  37. package/dist/features/preview/components/AssetLightbox.js +17 -6
  38. package/dist/features/preview/components/AttachedComments.js +224 -87
  39. package/dist/features/preview/components/BlockActionsPopover.js +1 -1
  40. package/dist/features/preview/components/CommentTargetQuote.js +22 -21
  41. package/dist/features/preview/components/CreatePageModal.js +4 -4
  42. package/dist/features/preview/components/DerivedLayoutDialog.js +36 -21
  43. package/dist/features/preview/components/DerivedLayoutSidebar.js +45 -26
  44. package/dist/features/preview/components/DerivedPageInfoSidebar.js +38 -26
  45. package/dist/features/preview/components/FieldOverlayStyles.js +29 -9
  46. package/dist/features/preview/components/FieldToolbar.js +12 -12
  47. package/dist/features/preview/components/IconFieldEditor.js +231 -0
  48. package/dist/features/preview/components/ItemFieldsEditor.js +21 -14
  49. package/dist/features/preview/components/LinkFieldEditor.js +212 -209
  50. package/dist/features/preview/components/OverlayTracker.js +1 -1
  51. package/dist/features/preview/components/PageEditorSidebar.js +1 -1
  52. package/dist/features/preview/components/PageInfoSidebar.js +28 -28
  53. package/dist/features/preview/components/PagePicker.js +46 -46
  54. package/dist/features/preview/components/PageTree.js +1 -1
  55. package/dist/features/preview/components/PreviewPanel.js +1 -1
  56. package/dist/features/preview/components/PreviewToolbar.js +224 -225
  57. package/dist/features/preview/components/PublicationRow.js +8 -4
  58. package/dist/features/preview/components/SendFeedbackDialog.js +83 -0
  59. package/dist/features/preview/components/useRepeatableItemActions.js +1 -1
  60. package/dist/features/preview/overlayHighlights.js +54 -0
  61. package/dist/features/preview/previewCommentsStore.js +54 -34
  62. package/dist/features/preview/previewSelection.js +28 -23
  63. package/dist/features/preview/publication.js +10 -0
  64. package/dist/features/preview/usePageComments.js +39 -0
  65. package/dist/features/provider/CoreCamoxProvider.js +38 -6
  66. package/dist/features/provider/LocalhostPreviewProvider.js +81 -45
  67. package/dist/features/provider/components/CommandPalette.js +35 -84
  68. package/dist/features/runtime/pageApp.js +32 -28
  69. package/dist/features/runtime/pageServer.js +4 -1
  70. package/dist/features/runtime/runtime.d.ts +2 -0
  71. package/dist/features/runtime/runtime.js +45 -21
  72. package/dist/features/studio/components/EnvironmentMenu.js +76 -63
  73. package/dist/features/studio/components/Navbar.js +53 -80
  74. package/dist/features/vite/auth.js +1 -50
  75. package/dist/features/vite/iconGeneration.js +16 -0
  76. package/dist/features/vite/layoutGeneration.js +7 -3
  77. package/dist/features/vite/vite.d.ts +4 -2
  78. package/dist/features/vite/vite.js +24 -3
  79. package/dist/hooks/use-page-destinations.js +53 -0
  80. package/dist/lib/auth.js +40 -20
  81. package/dist/lib/preview-handoff.js +13 -0
  82. package/dist/lib/queries.js +12 -1
  83. package/dist/lib/server-auth-cookie.js +21 -0
  84. package/dist/lib/use-project-room.js +5 -2
  85. package/dist/studio-overlays.css +19 -15
  86. package/dist/studio.css +1 -1
  87. package/package.json +9 -4
  88. package/skills/camox/SKILL.md +38 -9
  89. package/skills/camox/references/block-definitions.md +107 -286
  90. package/skills/camox/references/cli-common.md +16 -4
  91. package/skills/camox/references/cli-content.md +158 -0
  92. package/skills/camox/references/cli-environments.md +1 -1
  93. package/skills/camox/references/dom-integrations.md +93 -0
  94. package/skills/camox/references/field-styling.md +50 -0
  95. package/skills/camox/references/layout-definitions.md +33 -147
  96. package/skills/camox/references/routed-layouts.md +71 -0
  97. package/skills/camoxify/SKILL.md +66 -0
  98. package/skills/camox/references/cli-blocks.md +0 -78
  99. package/skills/camox/references/cli-pages.md +0 -53
@@ -0,0 +1,158 @@
1
+ # Managing Pages and Blocks with the Camox CLI
2
+
3
+ Read [cli-common.md](cli-common.md) first for `{{CAMOX_CMD}}`, help verification, and draft-first rules.
4
+
5
+ Use these commands for Pages, metadata, Layout assignments, Block instances and their Content, draft review, and publishing. To change Block schemas or rendering, read [Block Definitions](block-definitions.md); to create or change Layout Definitions, follow the code-definition routing in [the Camox skill](../SKILL.md).
6
+
7
+ ## Pages
8
+
9
+ ### Create a Page with an existing Layout
10
+
11
+ ```sh
12
+ {{CAMOX_CMD}} layouts list
13
+ {{CAMOX_CMD}} pages create --path-segment about --layout-id 39
14
+ ```
15
+
16
+ Use `--parent-page-id <ID>` to nest the Page under another Page.
17
+
18
+ ### Read and edit SEO metadata
19
+
20
+ ```sh
21
+ {{CAMOX_CMD}} pages get --path /about
22
+ {{CAMOX_CMD}} pages update --path /about --meta-title "About us" --meta-description "Meet the team."
23
+ {{CAMOX_CMD}} pages update --id 25 --meta-description ""
24
+ {{CAMOX_CMD}} pages update --id 25 --ai-seo on
25
+ {{CAMOX_CMD}} pages update --id 25 --ai-seo off
26
+ ```
27
+
28
+ Updates accept exactly one of `--id` or `--path` and require at least one update
29
+ field. Omitted fields are preserved; an empty string clears a metadata field.
30
+ Setting either metadata field disables automatic SEO for both fields and cannot
31
+ be combined with `--ai-seo on`. Disabling AI preserves current metadata; enabling
32
+ it schedules asynchronous generation, so read the Page again to inspect results.
33
+
34
+ Metadata edits affect only the draft, even with `--production`. Read published
35
+ metadata with `pages get --live`; publish only after user approval.
36
+
37
+ ## Comments
38
+
39
+ List a Page's comments, then use a returned comment `id` to resolve one on that same Page:
40
+
41
+ ```sh
42
+ {{CAMOX_CMD}} comments list --page-id 25
43
+ {{CAMOX_CMD}} comments resolve --page-id 25 --id <COMMENT_UUID>
44
+ ```
45
+
46
+ `--page-id` is the numeric Page ID (find it with `pages get --path /about`); `--id` is the comment's UUID, not a Block or Page ID. Resolution changes the comment's status, not the Page's draft or published content. Check `comments resolve --help` before writing, per [CLI common guidance](cli-common.md). Use `--json` for structured output; `--project`, `--production`, and `--cwd` select the same project, environment, and working directory as other CLI commands.
47
+
48
+ ## Blocks
49
+
50
+ ### Inspect before changing
51
+
52
+ Look up the Page before creating or moving Blocks so you have its `id`, current Blocks, and any sibling ids. Describe the Block type before constructing Content:
53
+
54
+ ```sh
55
+ {{CAMOX_CMD}} pages get --path /
56
+ {{CAMOX_CMD}} blocks describe --type hero
57
+ ```
58
+
59
+ ### Create and position a Block
60
+
61
+ ```sh
62
+ {{CAMOX_CMD}} blocks create --page-id 25 --type hero --content '{...}' --position first
63
+ ```
64
+
65
+ Creation initializes omitted Content and Settings fields from the Block definition's defaults, including when `--content '{}'` is supplied. Explicit field values—including `""`, `null`, `false`, and `[]`—are preserved; defaults do not merge into a supplied Link object. Omitted Repeaters initialize their default item count (or minimum count) with item defaults; supplying an array uses exactly those items, and `[]` creates none. Asset placeholders are not stored. Existing synced Block data still takes precedence for synced types.
66
+
67
+ Supply reference-specific copy, links, assets, and items directly as instance Content. Definition defaults should remain neutral, reusable examples—not a way to populate a reference site.
68
+
69
+ `blocks create` and `blocks move` accept the same positioning flags. Pass at most one:
70
+
71
+ | Flag | Meaning |
72
+ | ------------------------- | ----------------------------------------------- |
73
+ | `--position first` | Put the Block first. |
74
+ | `--position last` | Put the Block last; this is the create default. |
75
+ | `--after-id <ID>` | Put it immediately after a sibling Block. |
76
+ | `--before-id <ID>` | Put it immediately before a sibling Block. |
77
+ | `--after-position <KEY>` | Insert after a known fractional-index key. |
78
+ | `--before-position <KEY>` | Insert before a known fractional-index key. |
79
+
80
+ Prefer `--position`, `--after-id`, and `--before-id`. A move requires one positioning flag.
81
+
82
+ ### Edit Block Content
83
+
84
+ Content patches merge ordinary fields, so send only fields that should change:
85
+
86
+ ```sh
87
+ {{CAMOX_CMD}} pages get --path /pricing
88
+ {{CAMOX_CMD}} blocks edit --id 314 --content '{"headline": "New headline"}'
89
+ ```
90
+
91
+ ### Edit repeatable Content safely
92
+
93
+ A repeatable field's array is replacement-shaped: omitting an existing item deletes it and cascades to its file references, settings, and nested items. Fetch the Block, preserve every existing item with `_itemId`, and add changes only to the intended entries:
94
+
95
+ ```sh
96
+ {{CAMOX_CMD}} blocks get --id 99
97
+
98
+ {{CAMOX_CMD}} blocks edit --id 99 --content '{
99
+ "items": [
100
+ {"_itemId": 401},
101
+ {"_itemId": 402, "answer": "Updated answer."},
102
+ {"_itemId": 403}
103
+ ]
104
+ }'
105
+ ```
106
+
107
+ The response's `repeatableItems` contains each item's `id`, `fieldName`, `parentItemId`, and Content. Use the same `_itemId` pattern recursively for nested repeatables. Fetch multiple Blocks together when useful:
108
+
109
+ ```sh
110
+ {{CAMOX_CMD}} blocks get-many --id 99 --id 100 --id 101
111
+ ```
112
+
113
+ `blocks get-many` returns the same bundle shape as `blocks get`, in requested-id order.
114
+
115
+ ## Review and publishing
116
+
117
+ For curated page-content blocks, publish or discard changes at the Page level. Layout-owned blocks use layout publishing; curated Page publishing includes its Layout by default, as described below. Singleton and derived routes have no curated Page record: use layout publishing, not `pages publish` (see [Routed layouts](routed-layouts.md)).
118
+
119
+ ### Review draft and live state
120
+
121
+ ```sh
122
+ {{CAMOX_CMD}} pages get --path /pricing # draft
123
+ {{CAMOX_CMD}} pages get --path /pricing --live # published snapshot
124
+ {{CAMOX_CMD}} blocks get --id 314 # draft
125
+ {{CAMOX_CMD}} blocks get --id 314 --live # published snapshot
126
+ ```
127
+
128
+ Live reads fail when the Page or Block has never been published. `--live` is not a write target and does not publish anything.
129
+
130
+ ### Publish after approval
131
+
132
+ ```sh
133
+ {{CAMOX_CMD}} pages publish --path /pricing
134
+ ```
135
+
136
+ `pages publish` accepts exactly one of `--id` or `--path`. It publishes the Page's current draft and, by default, its Layout. Publishing the Layout is a no-op when it has no pending changes. Use `--no-layout` only when the user explicitly does not want pending Layout changes published with the Page:
137
+
138
+ ```sh
139
+ {{CAMOX_CMD}} pages publish --path /pricing --no-layout
140
+ ```
141
+
142
+ Publish only after the user asks or approves, not automatically after editing a draft.
143
+
144
+ ### Unpublish or discard drafts
145
+
146
+ Remove a Page from live without deleting its draft:
147
+
148
+ ```sh
149
+ {{CAMOX_CMD}} pages unpublish --path /pricing
150
+ ```
151
+
152
+ Reset a draft to its current live snapshot without changing live:
153
+
154
+ ```sh
155
+ {{CAMOX_CMD}} pages discard-changes --path /pricing
156
+ ```
157
+
158
+ Both commands accept exactly one of `--id` or `--path`. Discarding fails if the Page has never been published.
@@ -1,6 +1,6 @@
1
1
  # Managing Environments with the Camox CLI
2
2
 
3
- Read [cli-common.md](cli-common.md) first. Replace `{{CAMOX_CMD}}` as described there and verify commands with `--help`.
3
+ Read [cli-common.md](cli-common.md) first for `{{CAMOX_CMD}}`, help verification, and draft-first rules.
4
4
 
5
5
  Every Project has an isolated dev Environment for each developer and a shared production Environment. Each Environment independently contains draft and live sources.
6
6
 
@@ -0,0 +1,93 @@
1
+ # DOM integrations in preview and published pages
2
+
3
+ Use this reference for block DOM listeners, scrolling, browser scripts, and third-party widgets. It can be read independently of [Block Definitions](block-definitions.md); read that reference only if also changing the block schema or field rendering API.
4
+
5
+ **Do not use component-global `window` or `document` to target a block's DOM.** Camox preview (including block thumbnails) renders blocks into an iframe through a React portal, but component code executes in the editor's JavaScript realm. Global scroll listeners observe the editor; scripts appended to the global document load there instead of beside your block.
6
+
7
+ Use `getElementContext` from `camox/dom` inside an effect or event handler, passing a rendered element. It returns `{ window, document }` from the element's `ownerDocument` and `defaultView`, or `null` for a missing element or a document without a window. It never falls back to globals, requires no provider, and works in regular rendering too. Don't access DOM APIs during SSR.
8
+
9
+ ## Scroll listeners
10
+
11
+ ```tsx
12
+ import { getElementContext } from "camox/dom";
13
+ import { useEffect, useRef, useState } from "react";
14
+
15
+ function Header() {
16
+ const root = useRef<HTMLElement>(null);
17
+ const [scrolled, setScrolled] = useState(false);
18
+
19
+ useEffect(() => {
20
+ const context = getElementContext(root.current);
21
+ if (!context) return;
22
+
23
+ const { window } = context;
24
+ const onScroll = () => setScrolled(window.scrollY > 40);
25
+ onScroll();
26
+ window.addEventListener("scroll", onScroll, { passive: true });
27
+ return () => window.removeEventListener("scroll", onScroll);
28
+ }, []);
29
+
30
+ return (
31
+ <header ref={root} data-scrolled={scrolled}>
32
+ Navigation
33
+ </header>
34
+ );
35
+ }
36
+ ```
37
+
38
+ Read viewport dimensions, query elements, and attach resize listeners through the same context. If the root is conditionally mounted or replaced, rerun setup for the new element (for example, store it using a callback ref and include it in the effect dependencies).
39
+
40
+ ## Third-party scripts and widgets
41
+
42
+ Resolve context from the widget container, create scripts with `context.document.createElement("script")`, and append them to `context.document.head`. Initialize the SDK exposed on `context.window`, not the editor global. Deduplicate script loading **per document**, not with one module-global promise: preview, thumbnails, and the ordinary page can each have their own SDK instance.
43
+
44
+ For example, a script loader owned by your integration can use:
45
+
46
+ ```ts
47
+ const loads = new WeakMap<Document, Map<string, Promise<void>>>();
48
+
49
+ function loadScript(document: Document, src: string): Promise<void> {
50
+ let scripts = loads.get(document);
51
+ if (!scripts) {
52
+ scripts = new Map();
53
+ loads.set(document, scripts);
54
+ }
55
+ const existing = scripts.get(src);
56
+ if (existing) return existing;
57
+
58
+ const script = document.createElement("script");
59
+ script.src = src;
60
+ script.async = true;
61
+ const loaded = new Promise<void>((resolve, reject) => {
62
+ script.onload = () => resolve();
63
+ script.onerror = () => {
64
+ scripts.delete(src);
65
+ script.remove();
66
+ reject(new Error(`Failed to load ${src}`));
67
+ };
68
+ });
69
+ scripts.set(src, loaded);
70
+ document.head.appendChild(script);
71
+ return loaded;
72
+ }
73
+ ```
74
+
75
+ In the component's effect, await loading before initializing the widget against the rendered container. Handle load errors, ignore completion after cleanup, and destroy the widget instance on unmount using its SDK's teardown API. Keep a shared script when other blocks may still use it. Avoid duplicate initialization across remounts and React Strict Mode.
76
+
77
+ **Resolving the right window does not move your JavaScript objects into its realm.** Some SDKs use `instanceof Object` or similar constructor checks. Cal, for example, can reject configuration created by the editor even when its script loaded in the correct iframe. For JSON-compatible configuration only, construct a target-realm copy:
78
+
79
+ ```ts
80
+ const context = getElementContext(root.current);
81
+ if (!context) return;
82
+
83
+ const config = context.window.JSON.parse(
84
+ JSON.stringify({
85
+ layout: "month_view",
86
+ useSlotsViewOnSmallScreen: "true",
87
+ theme: "dark",
88
+ }),
89
+ );
90
+ // Pass config to the Cal instance loaded on context.window.
91
+ ```
92
+
93
+ This is not a general cloning solution: functions, DOM nodes, SDK instances, circular structures, and other non-JSON values need SDK-specific handling. Libraries that capture globals at import time are not automatically fixed by this helper. Test DOM integrations in both authenticated preview and regular page rendering.
@@ -0,0 +1,50 @@
1
+ # Inline field formatting and styling
2
+
3
+ Use this reference to customize inline text, links, and highlights on an existing `block.Field` or `item.Field`. Read [Block Definitions](block-definitions.md) only if also changing the block schema or other field rendering APIs.
4
+
5
+ ## String formatting
6
+
7
+ String fields support inline formatting in both editing and published output: `**bold**`, `*italic*` (or `_italic_`), `***bold italic***`, `<s>strikethrough</s>`, `<highlight>highlight text</highlight>`, and text links like `[label](https://example.com)` or internal page links inserted by the editor. Highlight can contain other formatting, links, and line breaks. Content remains a string. The editor uses explicit `<strong>` and `<em>` tags when Markdown delimiters cannot represent a selection losslessly (for example, formatting that includes leading/trailing whitespace). These are built-in formatting tags, not arbitrary HTML.
8
+
9
+ ## Rendering and appearance
10
+
11
+ Spread the field's render-prop `props` onto the element to preserve inline editing. `props.children` contains the rendered content:
12
+
13
+ ```tsx
14
+ <myBlock.Field name="title">{(props) => <h1 {...props} />}</myBlock.Field>
15
+ ```
16
+
17
+ Camox owns inline markup so editing and published output have the same appearance. Customize appearance with `textStyle` and `linkStyle` (these replace the old `components` prop):
18
+
19
+ ```tsx
20
+ <myBlock.Field
21
+ name="description"
22
+ textStyle={({ bold, italic, highlight }) => {
23
+ if (highlight) {
24
+ return {
25
+ style: {
26
+ backgroundImage: "linear-gradient(to right, orange, deeppink)",
27
+ backgroundClip: "text",
28
+ WebkitBackgroundClip: "text",
29
+ WebkitTextFillColor: "transparent",
30
+ },
31
+ };
32
+ }
33
+ return {
34
+ className: bold ? "text-primary" : undefined,
35
+ style: bold && italic ? { letterSpacing: "0.02em" } : undefined,
36
+ };
37
+ }}
38
+ linkStyle={({ external }) => ({
39
+ className: external ? "text-blue-600" : "text-primary font-medium",
40
+ })}
41
+ >
42
+ {(props) => <p {...props} />}
43
+ </myBlock.Field>
44
+ ```
45
+
46
+ - `textStyle` receives `{ bold, italic, strikethrough, highlight }` and returns `{ className?, style? }` or `undefined`. For text runs, `highlight` is false and the other flags describe explicit content formatting, not inherited heading styles. Unmarked text keeps its parent's typography.
47
+ - `linkStyle` receives `{ target, href, external, pageId }` and returns the same appearance-only object. Links are underlined by default; override `style.textDecorationLine` to change that.
48
+ - Camox also calls `textStyle` once for each shared highlight wrapper with `{ highlight: true, bold: false, italic: false, strikethrough: false }`. That result styles the whole range; its children receive their own text-run calls. `highlight` identifies the wrapper being styled, not whether a run is inside a highlight. This keeps one continuous background when bold/italic formatting changes within the range. Highlights default to the site's `--primary` text color, falling back to `currentColor`. Return a custom `style.color` to change it, or use a clipped background with transparent text fill to render a gradient, as shown above.
49
+ - Styles apply to inline children; the outer element's `className` stays on that element and is inherited normally. Explicit formatting defaults (combined italic, strikethrough, highlight color) use inline styles; use `style` to override those defaults rather than competing utility classes.
50
+ - These props accept appearance only, never replacement elements, children, or event handlers. They also work on `item.Field` inside repeaters. The sidebar editor shows default formatting; field-specific styles apply on the page.
@@ -1,14 +1,14 @@
1
- # Creating Camox Layout Definitions
1
+ # Creating Camox Curated Layout Definitions
2
2
 
3
- A layout wraps pages in shared structure — a navbar at the top, a footer at the bottom, consistent styling. Each page in the CMS is assigned a layout. This skill covers creating layout **definitions** — the template that describes which blocks surround page content and how to render them.
3
+ A curated layout (`kind: "curated"`, the default) is a reusable shell for editor-created pages, with before/after blocks and page-content `children`. For code-owned singleton pages or derived routes, read [Routed layouts](routed-layouts.md) instead.
4
4
 
5
- ## After defining the layout: assign it when requested
5
+ All editable content lives in blocks; layouts have no editable `content` schema or `.Field` helpers.
6
6
 
7
- A definition only adds a new Layout to the catalog — it doesn't apply it to any Page, and existing Pages keep their current Layout. If the user wanted "the about Page to use the new marketing Layout" or "all docs Pages to use this Layout", follow the umbrella skill's CLI routing to update the relevant Pages after the dev server has picked up the definition. Don't stop after the definition unless the user explicitly requested only a reusable Layout Definition.
7
+ Defining a layout does not assign it to existing Pages. When assignment is requested, wait for dev-server discovery, then follow the [CLI workflow](../SKILL.md#content-and-cli).
8
8
 
9
9
  ## Quick Start
10
10
 
11
- A layout file lives in the app's `src/layouts/` folder, is a `.tsx` file, and exports `layout`:
11
+ Use one `.tsx` file per layout in `src/layouts/`, a named `layout` or `Layout` export (not default), a named function component, and Tailwind styling.
12
12
 
13
13
  ```tsx
14
14
  import { createLayout } from "camox/createLayout";
@@ -17,13 +17,13 @@ import { block as footerBlock } from "../blocks/footer";
17
17
  import { block as heroBlock } from "../blocks/hero";
18
18
 
19
19
  const myLayout = createLayout({
20
- id: "my-layout", // Must match filename (kebab-case)
21
- title: "My Layout", // Human-readable name
22
- description: "When to use this layout",
20
+ id: "my-layout",
21
+ title: "My Layout",
22
+ description: "Marketing pages with shared navigation and footer.",
23
23
  blocks: {
24
- before: [navbarBlock], // Blocks rendered before page content
25
- after: [footerBlock], // Blocks rendered after page content
26
- initial: [heroBlock], // Blocks to pre-populate on the homepage
24
+ before: [navbarBlock],
25
+ after: [footerBlock],
26
+ initial: [heroBlock],
27
27
  },
28
28
  component: MyLayoutComponent,
29
29
  buildMetaTitle: ({ pageMetaTitle, projectName }) => `${pageMetaTitle} | ${projectName}`,
@@ -44,115 +44,33 @@ export { myLayout as layout };
44
44
 
45
45
  ## The `createLayout` options
46
46
 
47
- | Option | Required | Description |
48
- | ---------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
49
- | `id` | yes | Unique kebab-case identifier. Must match the filename without extension. |
50
- | `title` | yes | Display name shown in the CMS UI. |
51
- | `description` | yes | Tells the CMS user (or AI agent) when to pick this layout. Write it as guidance — explain what kind of pages this layout suits. |
52
- | `blocks` | yes | An object with `before`, `after`, and optional `initial` arrays. `before` blocks (must be `layoutOnly: true`) render above the page content, `after` blocks (must be `layoutOnly: true`) render below it, and `initial` blocks (must NOT be `layoutOnly: true`) pre-populate the homepage. See [Block Placement](#block-placement--before-after-and-initial) and [Initial Blocks](#initial-blocks--blocksinitial-optional) below. |
53
- | `component` | yes | A named React function component that renders the layout shell. Receives `{ children }` — the page content. |
54
- | `buildMetaTitle` | yes | A function that builds the `<title>` tag. Receives `{ pageMetaTitle, projectName, pageFullPath }` and returns a string. |
55
- | `buildOgImage` | no | A function that returns a JSX element for generating Open Graph images. Receives `{ title, description, projectName }`. |
47
+ | Option | Required | Description |
48
+ | ---------------- | -------- | --------------------------------------------------------------------------------- |
49
+ | `id` | yes | Unique kebab-case identifier matching the filename without extension. |
50
+ | `title` | yes | CMS display name. |
51
+ | `description` | yes | Guidance for users and agents on which pages suit this layout. |
52
+ | `blocks` | yes | `before`, `after`, and optional `initial` arrays; see placement rules below. |
53
+ | `component` | yes | Named function component receiving `{ children }`. |
54
+ | `buildMetaTitle` | yes | `({ pageMetaTitle, projectName, pageFullPath }) => string` for the browser title. |
55
+ | `buildOgImage` | no | `({ title, description, projectName }) => JSX` for a sharing image. |
56
56
 
57
57
  ## Block Placement — `before`, `after`, and `initial`
58
58
 
59
- The `blocks` object groups every block that belongs to this layout into three slots:
60
-
61
- - **`before`**: rendered above the page content (navbars, banners, announcements)
62
- - **`after`**: rendered below the page content (footers, cookie bars)
63
- - **`initial`** (optional): pre-populated on the homepage when a project is first created — see the dedicated section below
64
-
65
- The blocks in `before` and `after` **must** be marked with `layoutOnly: true` in their block definition — the `createLayout` type rejects non-layout-only blocks in those slots, since layout blocks shouldn't appear in the page-content "add block" picker. Conversely, `initial` accepts only blocks where `layoutOnly` is omitted or `false` — it's for page content, not chrome.
66
-
67
- ```tsx
68
- blocks: {
69
- before: [navbarBlock, announcementBlock],
70
- after: [footerBlock],
71
- }
72
- ```
73
-
74
- For a navbar or footer whose content must stay identical **across different layouts**, also set `synced: true` in its `createBlock` options. `layoutOnly: true` alone only restricts placement; each layout otherwise has independent data. Synced blocks share content, settings, and repeaters and appear purple in the editor. See [Synced blocks](block-definitions.md#synced-blocks-synced-optional).
75
-
76
- You can have multiple blocks in either group, or leave one empty:
77
-
78
- ```tsx
79
- blocks: {
80
- before: [navbarBlock],
81
- after: [], // No blocks after page content
82
- }
83
- ```
84
-
85
- ## The Layout Component
86
-
87
- The component is a named React function that receives `{ children }` and renders the overall page structure. Inside it, use the layout constant's two slot components — `BeforeBlocks` and `AfterBlocks` — to place all of the `before` and `after` blocks, in the order you declared them in the `blocks` config.
88
-
89
- ```tsx
90
- function MyLayoutComponent({ children }: { children: React.ReactNode }) {
91
- return (
92
- <main className="flex min-h-screen flex-col">
93
- <myLayout.BeforeBlocks />
94
- <div className="flex-1">{children}</div>
95
- <myLayout.AfterBlocks />
96
- </main>
97
- );
98
- }
99
- ```
100
-
101
- You don't list individual blocks in the component — `<myLayout.BeforeBlocks />` renders every block in `blocks.before` in order, and `<myLayout.AfterBlocks />` does the same for `blocks.after`. To reorder or add blocks, edit the `blocks` config; the component doesn't need to change.
102
-
103
- The component controls the HTML structure — you decide how to wrap and position the `BeforeBlocks` / `AfterBlocks` slots and the page content. Use Tailwind CSS for styling.
104
-
105
- ## Meta Title — `buildMetaTitle`
106
-
107
- Controls how the browser tab title is built. Receives three parameters:
108
-
109
- - `pageMetaTitle` — the title set on the individual page
110
- - `projectName` — the site/project name
111
- - `pageFullPath` — the full URL path of the page
112
-
113
- Common patterns:
114
-
115
- ```tsx
116
- // "Page Title | Site Name" (most common for content pages)
117
- buildMetaTitle: ({ pageMetaTitle, projectName }) =>
118
- `${pageMetaTitle} | ${projectName}`,
119
-
120
- // "Site Name | Page Title" (common for landing/home pages)
121
- buildMetaTitle: ({ pageMetaTitle, projectName }) =>
122
- `${projectName} | ${pageMetaTitle}`,
123
-
124
- // Just the page title
125
- buildMetaTitle: ({ pageMetaTitle }) => pageMetaTitle,
126
- ```
59
+ - `before` and `after` require blocks with `layoutOnly: true`. Either array may be empty.
60
+ - Render groups with `<myLayout.BeforeBlocks />` and `<myLayout.AfterBlocks />`, not individual blocks. Array order controls rendering order; edit the config to add or reorder blocks.
61
+ - For identical navbar/footer content across layouts, also set `synced: true`. `layoutOnly` restricts placement; it does not enable syncing. See [Synced blocks](block-definitions.md#synced-blocks-synced-optional) for publishing and existing-instance behavior.
127
62
 
128
63
  ## Initial Blocks — `blocks.initial` (optional)
129
64
 
130
- When a project is first set up, Camox creates a homepage automatically. The `blocks.initial` option lets you specify which blocks (and in what order) should be pre-populated on that page. Each block is created with its default content from the block definition.
65
+ `initial` seeds the homepage at project creation, using block defaults in array order. It does not populate existing Pages.
131
66
 
132
- ```tsx
133
- import { block as heroBlock } from "../blocks/hero";
134
- import { block as statisticsBlock } from "../blocks/statistics";
135
-
136
- const myLayout = createLayout({
137
- // ...
138
- blocks: {
139
- before: [navbarBlock],
140
- after: [footerBlock],
141
- initial: [heroBlock, statisticsBlock],
142
- },
143
- });
144
- ```
145
-
146
- - **Only non-layout blocks.** Blocks here must NOT be `layoutOnly: true` — the type system enforces it. Layout blocks (`before`/`after`) are created separately as part of the layout itself.
147
- - **At most one layout per app can define `blocks.initial`.** If multiple layouts define it, the app will crash with a clear error at startup. This is enforced in `createApp`.
148
- - **Order matters.** The blocks appear on the homepage in the order listed.
149
- - **If no layout defines `blocks.initial`**, Camox still creates an empty homepage using the first available layout (with its layout blocks like navbar/footer), but no page blocks are added.
67
+ - Accepts only blocks with `layoutOnly` omitted or `false`.
68
+ - At most one layout per app may define `initial`; multiple definitions cause a startup error.
69
+ - Without `initial`, Camox seeds an empty homepage using the first curated layout. Apps with only singleton/derived layouts do not seed a curated homepage.
150
70
 
151
71
  ## OG Image — `buildOgImage` (optional)
152
72
 
153
- Generates an Open Graph image (the preview shown when sharing links on social media). The function returns a JSX element that gets rendered as a 1200x630 image.
154
-
155
- The JSX here uses **inline styles only** (no Tailwind) because it's rendered by an image generation engine, not a browser. Use `display: "flex"` for layout.
73
+ Returns JSX rendered as a 1200×630 image. Use **inline styles, not Tailwind**, and `display: "flex"` for layout: this runs in an image-generation engine, not a browser.
156
74
 
157
75
  ```tsx
158
76
  buildOgImage: ({ title, description, projectName }) => (
@@ -160,49 +78,17 @@ buildOgImage: ({ title, description, projectName }) => (
160
78
  style={{
161
79
  display: "flex",
162
80
  flexDirection: "column",
163
- justifyContent: "center",
164
- alignItems: "flex-start",
165
81
  width: "100%",
166
82
  height: "100%",
83
+ padding: 60,
167
84
  backgroundColor: "#09090b",
168
- padding: "60px 80px",
85
+ color: "#fafafa",
169
86
  fontFamily: "sans-serif",
170
87
  }}
171
88
  >
172
- {projectName && (
173
- <div style={{ fontSize: 24, color: "#a1a1aa", marginBottom: 24 }}>
174
- {projectName}
175
- </div>
176
- )}
177
- <div
178
- style={{
179
- fontSize: 64,
180
- fontWeight: 700,
181
- color: "#fafafa",
182
- lineHeight: 1.2,
183
- marginBottom: 24,
184
- }}
185
- >
186
- {title}
187
- </div>
188
- {description && (
189
- <div style={{ fontSize: 28, color: "#a1a1aa", lineHeight: 1.5 }}>
190
- {description}
191
- </div>
192
- )}
89
+ {projectName && <div style={{ fontSize: 24 }}>{projectName}</div>}
90
+ <div style={{ fontSize: 64 }}>{title}</div>
91
+ {description && <div style={{ fontSize: 28 }}>{description}</div>}
193
92
  </div>
194
93
  ),
195
94
  ```
196
-
197
- ## Rules and Conventions
198
-
199
- 1. **File = one layout.** One `.tsx` file per layout in `src/layouts/`. The `id` must match the filename (without `.tsx`).
200
- 2. **Named export as `layout`.** Always: `export { myVar as layout }`. Not a default export.
201
- 3. **Named function component.** Use `function MyComponent()`, not an arrow function. Reference it in `createLayout` before its declaration is fine (hoisting).
202
- 4. **Import path is `"camox/createLayout"`.** The `createLayout` function comes from this import.
203
- 5. **Import blocks from `"../blocks/filename"`.** Layout blocks are imported from the blocks directory. Import the named `block` export.
204
- 6. **Use Tailwind CSS for the component.** Style the layout shell with Tailwind utility classes. The OG image function uses inline styles instead.
205
- 7. **Render groups, not individual blocks.** The component places `<layout.BeforeBlocks />` and `<layout.AfterBlocks />` — each renders every block in its group, in declared order. Don't reference blocks individually by name.
206
- 8. **`buildMetaTitle` is required.** Every layout must define how page titles are constructed.
207
- 9. **Description guides layout selection.** Write the `description` to help CMS users choose the right layout for their page — explain what types of pages it's suited for.
208
- 10. **Layout blocks must use `layoutOnly: true`.** Blocks placed in `blocks.before` or `blocks.after` must be defined with `layoutOnly: true` (enforced by the `createLayout` type). Blocks placed in `blocks.initial` must NOT be `layoutOnly: true`.
@@ -0,0 +1,71 @@
1
+ # Singleton pages and derived routes
2
+
3
+ Use this reference for code-owned pages in `src/layouts/`. For editor-created pages with a reusable shell and page-content `children`, read [Layout Definitions](layout-definitions.md) instead.
4
+
5
+ - **`singleton`**: one code-owned page at a fixed file-defined URL. Editors change its blocks, not its existence, URL, or structure.
6
+ - **`derived`**: code-owned routes resolved through a required server-side loader, typically using file parameters such as `$name`.
7
+
8
+ All editable content lives in blocks. Layouts do not expose editable `content` schemas or `.Field` helpers. Read [Block Definitions](block-definitions.md) when creating or changing those blocks.
9
+
10
+ ## Singleton pages
11
+
12
+ Declare a singleton in `src/layouts/pokedex.tsx`. The file identity defines `/pokedex`; `guide.pokedex.tsx` defines `/guide/pokedex`. Singleton identities must contain only literal word/hyphen segments separated by dots—no `$params`. The file plugin maintains the identity and registers the definition.
13
+
14
+ ```tsx
15
+ import { createLayout } from "camox/createLayout";
16
+ import { block as navbarBlock } from "../blocks/navbar";
17
+ import { block as pokedexIntroBlock } from "../blocks/pokedex-intro";
18
+ import { block as footerBlock } from "../blocks/footer";
19
+ import { loadPokemon } from "../lib/pokemon";
20
+ import { PokemonBrowser } from "../components/pokemon-browser";
21
+
22
+ export const Layout = createLayout("pokedex")({
23
+ kind: "singleton",
24
+ title: "Pokédex",
25
+ description: "The code-owned Pokémon directory.",
26
+ blocks: {
27
+ before: [navbarBlock, pokedexIntroBlock],
28
+ after: [footerBlock],
29
+ },
30
+ loader: () => loadPokemon(),
31
+ component: PokedexPage,
32
+ buildMetaTitle: ({ pageMetaTitle }) => pageMetaTitle,
33
+ });
34
+
35
+ function PokedexPage() {
36
+ const pokemon = Layout.useData();
37
+ return (
38
+ <>
39
+ <Layout.BeforeBlocks />
40
+ <main>
41
+ <PokemonBrowser pokemon={pokemon} />
42
+ </main>
43
+ <Layout.AfterBlocks />
44
+ </>
45
+ );
46
+ }
47
+ ```
48
+
49
+ - `loader` is optional. Its awaited result is inferred by `Layout.useData()`. It runs server-side for document requests and client navigation; return JSON-serializable data. Without a loader, `useData()` returns `undefined`.
50
+ - Before/after blocks must be `layoutOnly: true`. Put editable headings, introductions, and other bespoke content in these blocks; keep structural application UI in the component. See [Block Definitions](block-definitions.md).
51
+ - For a navbar or footer whose content must stay identical across different layouts, also set `synced: true` in its `createBlock` options. `layoutOnly: true` alone only restricts placement. See [Synced blocks](block-definitions.md#synced-blocks-synced-optional) for sharing and publishing behavior.
52
+ - No `children` slot or `blocks.initial`: editors do not compose or create these pages.
53
+ - No curated page record is created. Do not use the pages CLI to create, duplicate, move, or assign a singleton.
54
+ - Studio lists singletons as directly navigable pages with block editing and layout publishing, without deletion or URL/layout controls. They also appear in link destination pickers (including inline text links), “Go to page” actions, and the runtime sitemap. Derived route families are not enumerated.
55
+ - Singleton links are saved as fixed internal URLs, not synthetic database page IDs: link fields use `{ type: "external", href: "/pokedex", text: "Pokédex", newTab: false }` (the existing URL representation), while inline text uses `[Pokédex](/pokedex)`. Renaming a code-owned URL requires updating links or adding a redirect. Curated-page links retain their page-ID references.
56
+ - Curated-only mutation surfaces, such as page-parent selectors and the pages record API/CLI, remain separate from the navigable destination list.
57
+ - Publishing exposes the current editable blocks. Unpublishing removes those published blocks, **not** the route or code-rendered UI. Synced blocks retain their existing shared publishing behavior.
58
+ - Code deployment controls route existence. Removing the definition and syncing releases its reserved URL. Existing curated URL collisions or layouts still assigned to curated pages must be resolved explicitly before sync can succeed.
59
+ - Curated pages cannot claim singleton URLs or use singleton/derived layouts. Literal singleton routes take precedence over parameterized derived routes.
60
+ - Current routed-page HTML titles use the definition's `title`; the required `buildMetaTitle` option is retained for API compatibility, but routed-page metadata callbacks are not yet applied.
61
+
62
+ ## Derived routes
63
+
64
+ Use the same file-based form with `kind: "derived"`, for example `createLayout("pokemon.$name")`. Its required `loader: ({ params }) => ...` receives typed `params.name`. Read its result through `Layout.useData()`. Use `throw notFound()` (imported from `camox/createLayout`) for a missing record; other loader errors remain server errors. Like singletons, derived routes have before/after blocks but no page-owned `children` or `blocks.initial`. Unlike singletons, curated URLs retain precedence over derived routes.
65
+
66
+ ## File and component conventions
67
+
68
+ - One `.tsx` file per layout in `src/layouts/`; the identity passed to `createLayout("file-id")` must match the filename without `.tsx`.
69
+ - Use a named `Layout` or `layout` export, not a default export.
70
+ - Use a named function component and Tailwind CSS for the layout shell.
71
+ - Render `<Layout.BeforeBlocks />` and `<Layout.AfterBlocks />` to place the configured groups in order, rather than rendering individual blocks by name.