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
@@ -1,30 +1,25 @@
1
1
  # Creating Camox Block Definitions
2
2
 
3
- A block is a reusable page section (hero, testimonial, gallery, footer...). Users compose pages by assembling blocks. This skill covers creating block **definitions** — the template that describes a block's schema and rendering. Not the content (an instance of a block).
3
+ A block definition specifies a reusable section's schema and rendering, not an instance's content.
4
4
 
5
- ## After defining the block: place it when requested
6
-
7
- A definition only adds a new _type_ of block to the catalog — it doesn't put one on any Page. Users almost never just want "a definition"; they want a section that actually shows up on the site. Once the Block Definition exists and the dev server has picked it up, follow the umbrella skill's CLI routing to create a Block on the relevant Page. For shared chrome, follow the Layout Definition reference instead. If the user asks for "a hero on the homepage", define the Block Definition and then create the hero Block on the home Page. Don't stop after the definition unless the user explicitly requested only the reusable type.
5
+ When asked to add a section, also place an instance after dev-server discovery using the [CLI workflow](../SKILL.md#content-and-cli). For shared chrome, use [Layout Definitions](layout-definitions.md) instead. Defining a type alone does not place it on a Page.
8
6
 
9
7
  ## Quick Start
10
8
 
11
- A block file lives in the app's `src/blocks/` folder, is a `.tsx` file, and exports `block`:
9
+ Use one `.tsx` file per block in `src/blocks/`, a named `block` export (not default), a named function component, and Tailwind styling.
12
10
 
13
11
  ```tsx
14
12
  import { Type, createBlock } from "camox/createBlock";
15
13
 
16
14
  const myBlock = createBlock({
17
- id: "my-block", // Must match filename (kebab-case)
18
- title: "My Block", // Human-readable name
19
- description: "...", // Tells the AI when/how to use this block
15
+ id: "my-block",
16
+ title: "My Block",
17
+ description: "A page introduction with an editable heading.",
20
18
  content: {
21
- /* ... */
22
- }, // Editable content schema
23
- settings: {
24
- /* ... */
25
- }, // Optional config toggles
19
+ title: Type.String({ default: "Welcome" }),
20
+ },
26
21
  component: MyBlockComponent,
27
- toMarkdown: (c) => [`# ${c.title}`, c.description], // Markdown template
22
+ toMarkdown: (c) => [`# ${c.title}`],
28
23
  });
29
24
 
30
25
  function MyBlockComponent() {
@@ -38,132 +33,83 @@ function MyBlockComponent() {
38
33
  export { myBlock as block };
39
34
  ```
40
35
 
36
+ For DOM listeners, scrolling, scripts, or widgets, read [DOM integrations](dom-integrations.md). Preview DOM lives in an iframe; use `getElementContext` from `camox/dom`, not component-global `window` or `document`.
37
+
41
38
  ## The `createBlock` options
42
39
 
43
- | Option | Required | Description |
44
- | ------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
45
- | `id` | yes | Unique kebab-case identifier. Must match the filename without extension. |
46
- | `title` | yes | Display name shown in the CMS UI. |
47
- | `description` | yes | Tells the AI assistant when to use this block and what content it expects. Write it like guidance for an LLM — be specific about placement, tone, and content guidelines. |
48
- | `content` | yes | An object where each key is a field name and each value is a `Type.*` call. These fields are inline-editable in the CMS. |
49
- | `settings` | no | Same shape as `content`, but for configuration that lives in a settings panel (not inline). Only `Type.Enum` and `Type.Boolean` should be used here. |
50
- | `layoutOnly` | no | If `true`, the block won't appear in the "add block" sheet — it can only be placed inside layouts (e.g. navbar, footer). |
51
- | `synced` | no | Boolean, defaults to `false`. Share content and settings across every instance of this type within an environment. Ideal for a navbar/footer reused across layouts. Synced blocks have purple editor highlights. |
52
- | `component` | yes | A named React function component that renders the block. |
53
- | `toMarkdown` | yes | A builder function `(c, s) => (...)[]` that renders block content as markdown. `c` is a proxy typed on your `content` keys; `s` is a proxy typed on your `settings` keys used to wrap lines in conditionals. Each returned entry becomes a paragraph (joined with `\n\n`). Lines where all referenced fields resolve to empty — or whose condition is false — are omitted. |
40
+ | Option | Required | Description |
41
+ | ------------- | -------- | --------------------------------------------------------------------------------------------- |
42
+ | `id` | yes | Unique kebab-case identifier matching the filename without extension. |
43
+ | `title` | yes | CMS display name. |
44
+ | `description` | yes | Agent guidance on placement, expected content, and tone. |
45
+ | `content` | yes | Field names mapped to `Type.*` schemas for editable content. |
46
+ | `settings` | no | Settings-panel fields; only `Type.Enum` and `Type.Boolean`. |
47
+ | `layoutOnly` | no | If true, restricts placement to layouts and hides the type from the add-block picker. |
48
+ | `synced` | no | Defaults to false. Shares content/settings across instances within an environment; see below. |
49
+ | `component` | yes | Named React function component. |
50
+ | `toMarkdown` | yes | `(c, s) => [...]` markdown builder; see below. |
54
51
 
55
52
  ## Synced blocks (`synced`, optional)
56
53
 
57
- `createBlock({ synced: true, ... })` makes every instance of that block **type** share content and settings, including nested repeater items and their settings. It defaults to `false`: instances are independent unless you opt in. Sharing is scoped to one project environment; development and production never share edits automatically.
54
+ `synced: true` shares content and settings, including nested repeater items and their settings, across every instance of the block **type** within one environment. Dev and production remain independent. Synced blocks have purple editor highlights.
58
55
 
59
- Use `layoutOnly: true, synced: true` for a navbar or footer that must have identical data across different layouts. `layoutOnly` controls where a block can be placed; it does **not** enable syncing. A synced block can also be a normal page-content block.
56
+ For shared navbar/footer data across layouts, use `layoutOnly: true, synced: true`. These flags are independent: synced blocks can also be ordinary page-content blocks.
60
57
 
61
- - Editing any instance updates the shared draft data everywhere. New instances reuse existing data rather than resetting it to defaults.
62
- - Positions, page/layout placement, and deletion remain per instance. Deleting one placement doesn't delete the others.
63
- - Publishing a page or layout containing the block publishes its shared data for all live instances. Unpublished draft edits never leak into the live site.
64
- - Enabling syncing on an existing type keeps the oldest instance's data and applies it to the others. Disabling it leaves each instance with a copy that can then be edited independently.
65
- - Synced blocks have **purple highlights and overlays** in the editor.
58
+ - Editing any instance updates the shared draft everywhere. New instances reuse existing data rather than resetting to defaults.
59
+ - Position, placement, and deletion remain per instance; deleting one placement leaves the others.
60
+ - Publishing a page or layout containing the block publishes shared data for all live instances. Unpublished drafts do not leak into live.
61
+ - Enabling syncing on an existing type applies the oldest instance's data to all instances. Disabling it leaves independently editable copies.
66
62
 
67
63
  ## Markdown Template (`toMarkdown`)
68
64
 
69
- `toMarkdown` is a builder function that controls how block content is rendered as markdown for AI features (summaries, SEO). It receives:
65
+ Required on blocks and repeaters. Receives typed proxies `c` (content) and `s` (settings); returns entries joined with `\n\n` for AI summaries and SEO. Lines whose referenced fields are all empty, or whose condition is false, are omitted.
70
66
 
71
- - `c` — a proxy typed on your `content` keys (`c.title`, `c.description`…)
72
- - `s` — a proxy typed on your `settings` keys, used to wrap lines in conditionals
73
-
74
- …and returns an array of entries. Each entry becomes a paragraph (joined with `\n\n`).
75
-
76
- A bare `c.fieldName` is the default for single-field lines. Use template literals when combining fields or adding markdown syntax:
67
+ Use bare `c.field` for single-field lines and template literals for markdown or combined fields:
77
68
 
78
69
  ```tsx
79
- toMarkdown: (c) => [`# ${c.title}`, c.description, c.cta];
70
+ toMarkdown: (c) => [`> ${c.quote}`, `— ${c.author}, ${c.company}`, c.photo];
80
71
  ```
81
72
 
82
- **Field resolution rules:**
83
-
84
- - **String**: raw text value
85
- - **Link**: `[text](href)`
86
- - **Image**: `![alt](filename)`
87
- - **File**: `[filename](url)`
88
- - **Embed**: raw URL string
89
- - **Repeater**: each item rendered via its own `toMarkdown` (if set on the Repeater options), items joined with `\n\n`
90
- - **ImageList / FileList**: each asset rendered as a markdown bullet
91
- - **Boolean/Enum**: raw string value
92
-
93
- Lines where ALL referenced fields resolve to empty are omitted from output.
94
-
95
- **Examples:**
96
-
97
- ```tsx
98
- // Hero block
99
- createBlock({
100
- toMarkdown: (c) => [`# ${c.title}`, c.description, c.illustration, c.cta],
101
- content: { ... },
102
- })
103
-
104
- // Testimonial — combine fields on one line
105
- createBlock({
106
- toMarkdown: (c) => [`> ${c.quote}`, `— ${c.author}, ${c.title}, ${c.company}`],
107
- content: { ... },
108
- })
109
-
110
- // Statistics — Repeater with its own toMarkdown
111
- createBlock({
112
- toMarkdown: (c) => [`## ${c.subtitle}`, c.description, c.statistics],
113
- content: {
114
- subtitle: Type.String({ default: "..." }),
115
- description: Type.String({ default: "..." }),
116
- statistics: Type.Repeater({
117
- content: {
118
- number: Type.String({ default: "100M+" }),
119
- label: Type.String({ default: "pages served" }),
120
- },
121
- minItems: 4,
122
- maxItems: 8,
123
- toMarkdown: (c) => [`**${c.number}** — ${c.label}`],
124
- }),
125
- },
126
- })
127
- ```
73
+ Field resolution:
128
74
 
129
- The builder is type-safe — accessing a key that doesn't exist on `content` (e.g. `c.titl`) is a TypeScript error.
75
+ | Type | Markdown |
76
+ | -------------------- | ------------------------------------------------ |
77
+ | String | Raw text |
78
+ | Link | `[text](href)` |
79
+ | Image | `![alt](filename)` |
80
+ | File | `[filename](url)` |
81
+ | Embed | Raw URL |
82
+ | Repeater | Each item's own `toMarkdown`, joined with `\n\n` |
83
+ | ImageList / FileList | One markdown bullet per asset |
84
+ | Boolean / Enum | Raw string value |
130
85
 
131
86
  ### Conditional lines from settings
132
87
 
133
- The second `s` argument is a proxy over your `settings` keys. Wrap a line (or array of lines) to make it conditional on a setting:
88
+ Boolean settings wrap one line or an array; Enum settings match a variant:
134
89
 
135
90
  ```tsx
136
- // Boolean setting — include a line only when the setting is true
137
- toMarkdown: (c, s) => [`# ${c.title}`, c.description, s.showCta(c.cta)];
138
-
139
- // Boolean wrapping multiple lines — all included together, or all dropped
140
- toMarkdown: (c, s) => [
141
- `# ${c.title}`,
142
- s.showDetails([c.subtitle, c.description, c.backgroundImage]),
143
- c.cta,
144
- ];
145
-
146
- // Enum — emit different lines depending on the selected variant
147
91
  toMarkdown: (c, s) => [
148
92
  `# ${c.title}`,
93
+ s.showCta(c.cta),
94
+ s.showDetails([c.subtitle, c.description]),
149
95
  s.variant("banner", `**${c.headline}** — ${c.subtext}`),
150
96
  s.variant("inline", `${c.headline}: ${c.cta}`),
151
97
  ];
152
98
  ```
153
99
 
154
- Only `Type.Boolean` and `Type.Enum` settings can be used this way — trying to reference any other setting is a type error. On a `Repeater`'s own `toMarkdown`, `s` refers to the item's own `settings` (not the parent block's).
100
+ Only Boolean/Enum settings support these conditionals. A repeater's `s` refers to the item's settings, not the parent block's.
155
101
 
156
102
  ## Content Field Types
157
103
 
158
- Import `Type` from `"camox/createBlock"`. Every field requires a default value.
104
+ Import `Type` from `"camox/createBlock"`. Supply defaults for scalar fields; images/files get automatic placeholders, lists use `defaultItems`, and repeaters use their item schemas and bounds.
159
105
 
160
106
  ### Type.String
161
107
 
162
- Inline-editable text. The workhorse field type. String fields support inline formatting in both editing and published output: `**bold**`, `*italic*` (or `_italic_`), `***bold italic***`, `<u>underlined</u>`, `<gradient>gradient text</gradient>`, and text links like `[label](https://example.com)` or internal page links inserted by the editor. Gradient 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.
108
+ Inline-editable text supporting formatting and links while remaining a string. See [Field styling](field-styling.md) for formatting syntax and appearance customization.
163
109
 
164
110
  ```tsx
165
111
  Type.String({
166
- default: "Hello world", // Required
112
+ default: "Hello world",
167
113
  title: "Heading", // Optional label
168
114
  maxLength: 280, // Optional
169
115
  minLength: 1, // Optional
@@ -171,19 +117,39 @@ Type.String({
171
117
  });
172
118
  ```
173
119
 
174
- ### Type.Boolean
120
+ ### Type.Icon
175
121
 
176
- A toggle. Use in `settings` for config, or in `content` for user-controlled flags.
122
+ Choose one Iconify collection in Vite config:
123
+
124
+ ```ts
125
+ camox({ projectSlug: "my-project", icons: "lucide" });
126
+ ```
127
+
128
+ Declare a non-nullable icon with a required, prefixed default, then render inline SVG:
177
129
 
178
130
  ```tsx
179
- Type.Boolean({ default: false, title: "Show background" });
131
+ // In content:
132
+ icon: Type.Icon({ default: "lucide:zap", title: "Icon" })
133
+
134
+ // In the component (also supported as <item.Icon> inside repeaters):
135
+ <block.Icon name="icon" className="size-6 text-primary" />
136
+ <block.Icon name="icon" aria-label="Fast" />
180
137
  ```
181
138
 
182
- ### Type.Enum
139
+ Icons are decorative unless given `aria-label` or `aria-labelledby`. Clicking an icon in preview opens the searchable collection picker. Values remain namespaced strings; editors cannot select another collection or an empty value.
140
+
141
+ Run `pnpm exec camox typegen` **before TypeScript checking in CI**, for example `camox typegen && tsc --noEmit`. This resolves Vite config without starting a server or requiring Camox login. It generates `src/camox/icons.gen.d.ts`; ensure your tsconfig includes it. Dev and build regenerate it automatically. Generation requires network access to the pinned Iconify snapshot.
183
142
 
184
- A dropdown with predefined options. Most commonly used in `settings`.
143
+ Camox resolves selected SVGs from that same versioned snapshot and serves them through its API. Server rendering preloads selected/default SVGs into hydration data; no visitor requests to Iconify and no rebuild is needed when content changes. Only the editor fetches collection metadata and picker previews, not a whole SVG catalog.
144
+
145
+ Collection licenses still apply. SVG responses retain author/license attribution. Include any copyright notices, license text, or attribution required by your chosen collection in your site's distributed notices; Iconify does not relicense the icons. Changing collections or migrating existing icon values is not supported in v1.
146
+
147
+ ### Type.Boolean and Type.Enum
148
+
149
+ Use in `settings` for configuration or `content` for editable values. Enum defaults must match an option key.
185
150
 
186
151
  ```tsx
152
+ Type.Boolean({ default: false, title: "Show background" });
187
153
  Type.Enum({
188
154
  default: "left",
189
155
  options: { left: "Left", center: "Center", right: "Right" },
@@ -191,11 +157,9 @@ Type.Enum({
191
157
  });
192
158
  ```
193
159
 
194
- The `default` must be one of the keys in `options`.
195
-
196
160
  ### Type.Link
197
161
 
198
- A link with text, URL (or internal page reference), and new-tab toggle.
162
+ Text, destination, and new-tab toggle. Curated page links store page IDs; singleton links store fixed URLs using the existing `type: "external"` representation, which also supports site-relative URLs: `{ type: "external", href: "/pokedex", text: "Pokédex", newTab: false }`. Both page kinds appear in Studio's destination picker. Inline `[Pokédex](/pokedex)` links stay in the current tab.
199
163
 
200
164
  ```tsx
201
165
  Type.Link({
@@ -204,48 +168,20 @@ Type.Link({
204
168
  });
205
169
  ```
206
170
 
207
- ### Type.Image
171
+ ### Images and files
208
172
 
209
- A single image — render with `block.Image`.
173
+ Files support MIME filtering. Render each type with its matching helper; asset lists are **not** repeaters.
210
174
 
211
175
  ```tsx
212
176
  Type.Image({ title: "Cover photo" });
213
- ```
214
-
215
- ### Type.ImageList
216
-
217
- A flat array of images — render with `block.ImageList` (NOT `block.Repeater`).
218
-
219
- ```tsx
220
177
  Type.ImageList({ defaultItems: 6, title: "Gallery images" });
221
- ```
222
-
223
- ### Type.File
224
-
225
- A single file upload, with MIME type filtering — render with `block.File`.
226
-
227
- ```tsx
228
- Type.File({
229
- accept: ["application/pdf"],
230
- title: "PDF Document",
231
- });
232
- ```
233
-
234
- ### Type.FileList
235
-
236
- A flat array of files — render with `block.FileList` (NOT `block.Repeater`).
237
-
238
- ```tsx
239
- Type.FileList({
240
- accept: ["application/pdf"],
241
- defaultItems: 0,
242
- title: "Documents",
243
- });
178
+ Type.File({ accept: ["application/pdf"], title: "PDF Document" });
179
+ Type.FileList({ accept: ["application/pdf"], defaultItems: 0, title: "Documents" });
244
180
  ```
245
181
 
246
182
  ### Type.Embed
247
183
 
248
- A URL validated against a regex pattern. Used for embedding external content.
184
+ A URL validated against a regex. A nonmatching default throws at definition time.
249
185
 
250
186
  ```tsx
251
187
  Type.Embed({
@@ -255,11 +191,9 @@ Type.Embed({
255
191
  });
256
192
  ```
257
193
 
258
- The `default` must match the `pattern` — an error is thrown at definition time otherwise.
259
-
260
194
  ### Type.Repeater
261
195
 
262
- An array of structured items. Each item is an object with its own fields. This is how you create lists of things (testimonials, features, stats, links...).
196
+ Structured items with their own content, optional settings, and required `toMarkdown`. `minItems` must be at least 1; repeaters cannot be empty. Nest repeaters in an item's `content` when needed.
263
197
 
264
198
  ```tsx
265
199
  Type.Repeater({
@@ -267,91 +201,39 @@ Type.Repeater({
267
201
  name: Type.String({ default: "Feature" }),
268
202
  description: Type.String({ default: "Description" }),
269
203
  },
270
- minItems: 1, // Must be >= 1
204
+ minItems: 1,
271
205
  maxItems: 10,
272
206
  title: "Features",
273
- toMarkdown: (c) => [`### ${c.name}`, c.description], // Required markdown template for each item
207
+ toMarkdown: (c) => [`### ${c.name}`, c.description],
274
208
  });
275
209
  ```
276
210
 
277
- The `toMarkdown` option on Repeater defines how each item is rendered as markdown when the parent block's `toMarkdown` references this field. It is required, same as on the block itself.
278
-
279
- Repeaters can be nested — an item can contain another Repeater:
280
-
281
- ```tsx
282
- columns: Type.Repeater({
283
- content: {
284
- title: Type.String({ default: "Column" }),
285
- links: Type.Repeater({
286
- content: {
287
- link: Type.Link({ default: { text: "Link", href: "#", newTab: false } }),
288
- },
289
- minItems: 1,
290
- maxItems: 999,
291
- toMarkdown: (c) => [c.link],
292
- }),
293
- },
294
- minItems: 2,
295
- maxItems: 4,
296
- title: "Columns",
297
- toMarkdown: (c) => [`### ${c.title}`, c.links],
298
- });
299
- ```
211
+ Reference the repeater field in the parent block's `toMarkdown` to include its rendered items.
300
212
 
301
213
  ## Rendering in the Component
302
214
 
303
- The component is a regular React function. It uses methods on the block constant to render each field. Every field renderer uses a render-prop pattern where the child function receives `(props, data)`:
304
-
305
- - **`props`** — spread onto the rendered element. Contains HTML/React attributes: refs, data attributes, event handlers, and field-specific attributes like `to`, `src`, `alt`, `children`. **Use `props` in the vast majority of cases.**
306
- - **`data`** (second argument) — raw field values for advanced use cases where `props` doesn't suffice (e.g. conditional styling based on a link's href, using an image URL as a CSS background). Not meant to be spread — treat it as an escape hatch.
307
-
308
- ### Rendering String fields — `block.Field`
215
+ Field renderers receive `(props, data)`:
309
216
 
310
- ```tsx
311
- <myBlock.Field name="title">{(props) => <h1 {...props} />}</myBlock.Field>
312
- ```
217
+ - **Spread `props` onto the element** to preserve editor refs, attributes, and handlers, plus field-specific values such as `children`, `to`, `src`, and `alt`.
218
+ - `data` is the raw field value for custom logic, not an object to spread. Prefer `props`; use raw data only where needed.
313
219
 
314
- The `name` must match a key in `content` that is a `Type.String`. Spread `props` onto the element — `props.children` contains the rendered content. This is what makes the field inline-editable in the CMS.
220
+ ### Strings — `block.Field`
315
221
 
316
- 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):
222
+ `name` must select a `Type.String` field. `props.children` contains Camox's rendered inline markup.
317
223
 
318
224
  ```tsx
319
- <myBlock.Field
320
- name="description"
321
- textStyle={({ bold, italic, gradient }) => {
322
- if (gradient) {
323
- return { style: { backgroundImage: "linear-gradient(to right, orange, deeppink)" } };
324
- }
325
- return {
326
- className: bold ? "text-primary" : undefined,
327
- style: bold && italic ? { letterSpacing: "0.02em" } : undefined,
328
- };
329
- }}
330
- linkStyle={({ external }) => ({
331
- className: external ? "text-blue-600" : "text-primary font-medium",
332
- })}
333
- >
334
- {(props) => <p {...props} />}
335
- </myBlock.Field>
336
- ```
337
-
338
- - `textStyle` receives `{ bold, italic, underline, gradient }` and returns `{ className?, style? }` or `undefined`. For text runs, `gradient` is false and the other flags describe explicit content formatting, not inherited heading styles. Unmarked text keeps its parent's typography.
339
- - `linkStyle` receives `{ target, href, external, pageId }` and returns the same appearance-only object. Links are underlined by default; override `style.textDecorationLine` to change that.
340
- - Camox also calls `textStyle` once for each shared gradient wrapper with `{ gradient: true, bold: false, italic: false, underline: false }`. That result styles the whole range; its children receive their own text-run calls. `gradient` identifies the wrapper being styled, not whether a run is inside a gradient. This keeps one continuous background when bold/italic formatting changes within the range. The default gradient uses the site's shadcn chart palette: `--chart-1` → `--chart-2`, following light/dark theme changes. Missing chart tokens fall back to `--primary` / `--muted-foreground`, then `currentColor`, never a hardcoded palette. Set `--camox-gradient-from` and `--camox-gradient-to` in app CSS for app-wide colors, or return `style.backgroundImage` when `gradient` is true.
341
- - Styles apply to inline children; the outer element's `className` stays on that element and is inherited normally. Explicit formatting defaults (combined italic, underline, gradient clipping) use inline styles; use `style` to override those defaults rather than competing utility classes.
342
- - 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.
343
-
344
- When the content needs to be placed inside a more complex structure, use `props.children` explicitly:
225
+ <myBlock.Field name="title">{(props) => <h1 {...props} />}</myBlock.Field>
345
226
 
346
- ```tsx
347
227
  <myBlock.Field name="quote">
348
228
  {(props) => <blockquote {...props}>"{props.children}"</blockquote>}
349
229
  </myBlock.Field>
350
230
  ```
351
231
 
352
- ### Rendering Link fields — `block.Link`
232
+ For `textStyle` and `linkStyle`, read [Field styling](field-styling.md).
353
233
 
354
- Inside the render prop, use the `Link` component from `camox/navigation` instead of a plain `<a>` tag. This enables client-side navigation for internal links. The framework computes `to`, `target`, and `rel` from the link value — just spread `props`.
234
+ ### Links — `block.Link`
235
+
236
+ Use `Link` from `camox/navigation`, not `<a>`, for client-side internal navigation. `props` supplies `to`, `target`, and `rel`; raw `data` exposes `{ text, href, newTab }`.
355
237
 
356
238
  ```tsx
357
239
  import { Link } from "camox/navigation";
@@ -359,51 +241,21 @@ import { Link } from "camox/navigation";
359
241
  <myBlock.Link name="cta">{(props) => <Link {...props} />}</myBlock.Link>;
360
242
  ```
361
243
 
362
- The optional second argument `data` exposes raw values `{ text, href, newTab }` for custom logic:
363
-
364
- ```tsx
365
- <myBlock.Link name="cta">
366
- {(props, { href }) => <Link {...props} className={href === "/" ? "active" : ""} />}
367
- </myBlock.Link>
368
- ```
369
-
370
- ### Rendering Image fields — `block.Image`
244
+ ### Images, files, and embeds
371
245
 
372
246
  ```tsx
373
247
  <myBlock.Image name="cover">{(props) => <img {...props} />}</myBlock.Image>
374
- ```
375
-
376
- `props` includes `src` and `alt`. The optional `data` argument exposes the raw `ImageValue` for cases like background images:
377
-
378
- ```tsx
379
- <myBlock.Image name="cover">
380
- {(_props, { url }) => <div style={{ backgroundImage: `url(${url})` }} />}
381
- </myBlock.Image>
382
- ```
383
-
384
- ### Rendering File fields — `block.File`
385
-
386
- ```tsx
387
248
  <myBlock.File name="document">{(props) => <a {...props}>Download</a>}</myBlock.File>
388
- ```
389
-
390
- `props` includes `href` and `download` (filename). The optional `data` argument exposes the raw `FileValue`.
391
-
392
- ### Rendering Embed fields — `block.Embed`
393
-
394
- ```tsx
395
249
  <myBlock.Embed name="videoUrl">{(props) => <iframe {...props} />}</myBlock.Embed>
396
250
  ```
397
251
 
398
- `props` includes `src`. The optional `data` argument exposes `{ url }` for cases where the URL needs transformation:
252
+ - Image: `props` includes `src`/`alt`; `data` is `ImageValue`.
253
+ - File: `props` includes `href`/`download` (filename); `data` is `FileValue`.
254
+ - Embed: `props` includes `src`; `data` is `{ url }`, available for URL transformation.
399
255
 
400
- ```tsx
401
- <myBlock.Embed name="videoUrl">
402
- {(_props, { url }) => <iframe src={transformUrl(url)} />}
403
- </myBlock.Embed>
404
- ```
256
+ ### Repeaters — `block.Repeater`
405
257
 
406
- ### Rendering Repeater fields — `block.Repeater`
258
+ The callback receives an item-scoped API with the same `.Field`, `.Link`, `.Image`, `.File`, `.Embed`, `.ImageList`, `.FileList`, and `.Repeater` methods.
407
259
 
408
260
  ```tsx
409
261
  <myBlock.Repeater name="features">
@@ -416,53 +268,35 @@ The optional second argument `data` exposes raw values `{ text, href, newTab }`
416
268
  </myBlock.Repeater>
417
269
  ```
418
270
 
419
- Inside a Repeater, the `item` callback argument exposes the same `.Field`, `.Link`, `.Image`, `.File`, `.Embed`, `.ImageList`, `.FileList`, and `.Repeater` methods — scoped to that item. This is how nested repeaters work too:
271
+ Nested repeaters use the item-scoped `.Repeater`:
420
272
 
421
273
  ```tsx
422
274
  <footer.Repeater name="columns">
423
275
  {(column) => (
424
276
  <column.Repeater name="links">
425
- {(linkItem) => <linkItem.Link name="link">{(props) => <Link {...props} />}</linkItem.Link>}
277
+ {(item) => <item.Link name="link">{(props) => <Link {...props} />}</item.Link>}
426
278
  </column.Repeater>
427
279
  )}
428
280
  </footer.Repeater>
429
281
  ```
430
282
 
431
- ### Image and File lists — `block.ImageList` / `block.FileList`
283
+ ### Asset lists — `block.ImageList` / `block.FileList`
432
284
 
433
- For `Type.ImageList` use `block.ImageList`; for `Type.FileList` use `block.FileList` (NOT `block.Repeater`). The render-prop is the same shape as `block.Image` / `block.File` — `(props, data) => …` — invoked once per asset:
285
+ Use the matching helper, **not `block.Repeater`**. Its `(props, data)` callback runs once per asset, with the same shape as `.Image` / `.File`. These helpers also work on repeater items.
434
286
 
435
287
  ```tsx
436
- // Top-level
437
288
  <gallery.ImageList name="images">
438
289
  {(props) => <img {...props} className="rounded-lg" />}
439
290
  </gallery.ImageList>
440
-
441
- // Nested inside a Type.Repeater
442
- <paragraphGrid.Repeater name="paragraphs">
443
- {(item) => (
444
- <item.ImageList name="logos">
445
- {(props) => <img {...props} className="size-10 object-contain" />}
446
- </item.ImageList>
447
- )}
448
- </paragraphGrid.Repeater>
449
291
  ```
450
292
 
451
- `Repeater` is only for `Type.Repeater` arrays. Trying to use `Repeater` on an `ImageList` / `FileList` is a TypeScript error.
293
+ ### Settings — `block.useSetting`
452
294
 
453
- ### Reading settings — `block.useSetting`
454
-
455
- ```tsx
456
- function MyComponent() {
457
- const theme = myBlock.useSetting("theme");
458
- const compact = myBlock.useSetting("compact");
459
- // Use these values in your JSX for conditional rendering/styling
460
- }
461
- ```
295
+ Read settings inside the component with `myBlock.useSetting("theme")`.
462
296
 
463
297
  ### Detached rendering — `block.Detached`
464
298
 
465
- Renders content outside the block's DOM container. Useful for fixed/floating elements like sticky navbars or modals. Uses a render prop that provides `props` with `ref`, `onClick`, `onMouseEnter`, and `onMouseLeave` — spread these onto the root element.
299
+ Renders outside the block's DOM container for fixed/floating elements. Spread its `props` (`ref`, `onClick`, `onMouseEnter`, `onMouseLeave`) onto the root element.
466
300
 
467
301
  ```tsx
468
302
  <myBlock.Detached>
@@ -473,16 +307,3 @@ Renders content outside the block's DOM container. Useful for fixed/floating ele
473
307
  )}
474
308
  </myBlock.Detached>
475
309
  ```
476
-
477
- ## Rules and Conventions
478
-
479
- 1. **File = one block.** One `.tsx` file per block in `src/blocks/`. The `id` must match the filename (without `.tsx`).
480
- 2. **Named export as `block`.** Always: `export { myVar as block }`. Not a default export.
481
- 3. **Named function component.** Use `function MyComponent()`, not an arrow function. Reference it in `createBlock` before its declaration is fine (hoisting).
482
- 4. **All fields need defaults.** Every `Type.*` call requires a default value (images and files get automatic placeholders).
483
- 5. **Description is for the AI.** Write the `description` as guidance for an LLM — explain when to use this block, what kind of content it's for, and where it fits on a page.
484
- 6. **`toMarkdown` is required.** Every block must define how its content renders as markdown. Use the builder function `(c) => [...]` and reference content fields via `c.fieldName`.
485
- 7. **Settings = Enum and Boolean only.** Keep settings simple. Use `content` for everything the user edits inline.
486
- 8. **Repeater minItems >= 1.** You can't have an empty repeater — there's always at least one item.
487
- 9. **Import path is `"camox/createBlock"`.** Both `Type` and `createBlock` come from this import.
488
- 10. **Use Tailwind CSS for styling.** All example blocks use Tailwind utility classes. Follow the same patterns: `container mx-auto px-4` for centered content, responsive breakpoints, etc.
@@ -18,15 +18,13 @@ In the examples below, replace `{{CAMOX_CMD}}` with that command.
18
18
 
19
19
  ## Discover the current command surface
20
20
 
21
- The CLI evolves with the installed Camox version. Do not guess commands or flags from memory. Run `--help` at the root, command group, and subcommand before executing a write:
21
+ Before a write, verify the relevant subcommand's `--help` unless already checked for the installed version in this session:
22
22
 
23
23
  ```sh
24
- {{CAMOX_CMD}} --help
25
- {{CAMOX_CMD}} pages --help
26
24
  {{CAMOX_CMD}} pages create --help
27
25
  ```
28
26
 
29
- Treat the installed CLI's output as authoritative when it differs from an example in these references.
27
+ Use root or group help only to discover unfamiliar commands. Reuse verified help within the session; recheck if the installed version changes. Installed help overrides these examples—do not guess commands or flags.
30
28
 
31
29
  ## Draft-first workflow
32
30
 
@@ -36,6 +34,20 @@ By default, reads and writes operate on the draft in the current developer's iso
36
34
  - Omit `--production` unless the user explicitly asks to operate on the production Environment.
37
35
  - Publishing a draft and replicating an Environment are different operations. Read the Environment reference before using `env push` or `env pull`.
38
36
 
37
+ ## Verify drafts in a browser
38
+
39
+ CLI credentials and browser sessions are separate. A fresh browser shows published content until it signs in, even when CLI draft writes succeeded.
40
+
41
+ 1. Start the app's dev server and check `{{CAMOX_CMD}} preview --help`.
42
+ 2. Generate a handoff for the page you changed:
43
+ `{{CAMOX_CMD}} preview --url http://localhost:3000/about --json`
44
+ (Use the actual port; add `--cwd apps/site` in a monorepo.)
45
+ 3. Open the returned `url` in your verification browser, not a separate default browser.
46
+ 4. Wait for `[data-camox-preview="ready"]`. Its text identifies the project and developer environment; the server has loaded authenticated draft content. `[data-camox-preview="error"]` means verification failed, not that the write failed.
47
+ 5. Inspect the changed content. Do not publish merely to make it visible.
48
+
49
+ The URL is a single-use sign-in credential valid for three minutes. Do not share, commit, or include it in reports. Generate a new URL if expired or consumed. Only loopback HTTP(S) destinations are supported; production preview is intentionally unsupported. The SDK must support this handoff: if the ready marker never appears, check the installed SDK version rather than assuming the page is a draft. A project/backend/environment mismatch requires correcting `--cwd` or `--url` (or restarting a dev server running under another account).
50
+
39
51
  ## Write grounded content
40
52
 
41
53
  Before writing Content: