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.
- package/bin/camox.mjs +9 -1
- package/dist/core/components/lexical/{GradientNode.js → HighlightNode.js} +34 -34
- package/dist/core/components/lexical/InlineLexicalEditor.js +14 -14
- package/dist/core/components/lexical/InlineStylesPlugin.js +25 -9
- package/dist/core/components/lexical/SelectionBroadcaster.js +7 -7
- package/dist/core/components/lexical/SidebarLexicalEditor.js +24 -24
- package/dist/core/components/lexical/TextLinkPopover.js +191 -208
- package/dist/core/components/lexical/editorConfig.js +2 -2
- package/dist/core/createApp.d.ts +31 -4
- package/dist/core/createApp.js +2 -1
- package/dist/core/createBlock.js +24 -10
- package/dist/core/createLayout.d.ts +16 -3
- package/dist/core/createLayout.js +15 -15
- package/dist/core/derivedRoutes.js +1 -1
- package/dist/core/dom.d.ts +17 -0
- package/dist/core/dom.js +22 -0
- package/dist/core/editing/createEditableBlock.d.ts +135 -1
- package/dist/core/editing/createEditableBlock.js +209 -74
- package/dist/core/lib/contentType.d.ts +5 -0
- package/dist/core/lib/contentType.js +11 -0
- package/dist/core/lib/fieldTypes.js +1 -0
- package/dist/core/lib/iconTypes.d.ts +13 -0
- package/dist/core/lib/icons.d.ts +6 -0
- package/dist/core/lib/icons.js +130 -0
- package/dist/core/lib/lexicalReact.js +4 -4
- package/dist/core/lib/lexicalState.js +8 -8
- package/dist/core/lib/modifierFormats.js +2 -2
- package/dist/core/lib/textLinks.js +5 -2
- package/dist/core/lib/textStyles.d.ts +3 -3
- package/dist/core/lib/textStyles.js +9 -14
- package/dist/core/pageDestinations.js +37 -0
- package/dist/features/content/components/AssetCard.js +104 -68
- package/dist/features/og/imageResponse.node.js +11 -9
- package/dist/features/preview/CamoxPreview.js +148 -146
- package/dist/features/preview/commentsEnabled.js +2 -2
- package/dist/features/preview/components/AddBlockSidebar.js +1 -1
- package/dist/features/preview/components/AssetLightbox.js +17 -6
- package/dist/features/preview/components/AttachedComments.js +224 -87
- package/dist/features/preview/components/BlockActionsPopover.js +1 -1
- package/dist/features/preview/components/CommentTargetQuote.js +22 -21
- package/dist/features/preview/components/CreatePageModal.js +4 -4
- package/dist/features/preview/components/DerivedLayoutDialog.js +36 -21
- package/dist/features/preview/components/DerivedLayoutSidebar.js +45 -26
- package/dist/features/preview/components/DerivedPageInfoSidebar.js +38 -26
- package/dist/features/preview/components/FieldOverlayStyles.js +29 -9
- package/dist/features/preview/components/FieldToolbar.js +12 -12
- package/dist/features/preview/components/IconFieldEditor.js +231 -0
- package/dist/features/preview/components/ItemFieldsEditor.js +21 -14
- package/dist/features/preview/components/LinkFieldEditor.js +212 -209
- package/dist/features/preview/components/OverlayTracker.js +1 -1
- package/dist/features/preview/components/PageEditorSidebar.js +1 -1
- package/dist/features/preview/components/PageInfoSidebar.js +28 -28
- package/dist/features/preview/components/PagePicker.js +46 -46
- package/dist/features/preview/components/PageTree.js +1 -1
- package/dist/features/preview/components/PreviewPanel.js +1 -1
- package/dist/features/preview/components/PreviewToolbar.js +224 -225
- package/dist/features/preview/components/PublicationRow.js +8 -4
- package/dist/features/preview/components/SendFeedbackDialog.js +83 -0
- package/dist/features/preview/components/useRepeatableItemActions.js +1 -1
- package/dist/features/preview/overlayHighlights.js +54 -0
- package/dist/features/preview/previewCommentsStore.js +54 -34
- package/dist/features/preview/previewSelection.js +28 -23
- package/dist/features/preview/publication.js +10 -0
- package/dist/features/preview/usePageComments.js +39 -0
- package/dist/features/provider/CoreCamoxProvider.js +38 -6
- package/dist/features/provider/LocalhostPreviewProvider.js +81 -45
- package/dist/features/provider/components/CommandPalette.js +35 -84
- package/dist/features/runtime/pageApp.js +32 -28
- package/dist/features/runtime/pageServer.js +4 -1
- package/dist/features/runtime/runtime.d.ts +2 -0
- package/dist/features/runtime/runtime.js +45 -21
- package/dist/features/studio/components/EnvironmentMenu.js +76 -63
- package/dist/features/studio/components/Navbar.js +53 -80
- package/dist/features/vite/auth.js +1 -50
- package/dist/features/vite/iconGeneration.js +16 -0
- package/dist/features/vite/layoutGeneration.js +7 -3
- package/dist/features/vite/vite.d.ts +4 -2
- package/dist/features/vite/vite.js +24 -3
- package/dist/hooks/use-page-destinations.js +53 -0
- package/dist/lib/auth.js +40 -20
- package/dist/lib/preview-handoff.js +13 -0
- package/dist/lib/queries.js +12 -1
- package/dist/lib/server-auth-cookie.js +21 -0
- package/dist/lib/use-project-room.js +5 -2
- package/dist/studio-overlays.css +19 -15
- package/dist/studio.css +1 -1
- package/package.json +9 -4
- package/skills/camox/SKILL.md +38 -9
- package/skills/camox/references/block-definitions.md +107 -286
- package/skills/camox/references/cli-common.md +16 -4
- package/skills/camox/references/cli-content.md +158 -0
- package/skills/camox/references/cli-environments.md +1 -1
- package/skills/camox/references/dom-integrations.md +93 -0
- package/skills/camox/references/field-styling.md +50 -0
- package/skills/camox/references/layout-definitions.md +33 -147
- package/skills/camox/references/routed-layouts.md +71 -0
- package/skills/camoxify/SKILL.md +66 -0
- package/skills/camox/references/cli-blocks.md +0 -78
- package/skills/camox/references/cli-pages.md +0 -53
|
@@ -1,30 +1,25 @@
|
|
|
1
1
|
# Creating Camox Block Definitions
|
|
2
2
|
|
|
3
|
-
A block
|
|
3
|
+
A block definition specifies a reusable section's schema and rendering, not an instance's content.
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
|
|
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",
|
|
18
|
-
title: "My Block",
|
|
19
|
-
description: "
|
|
15
|
+
id: "my-block",
|
|
16
|
+
title: "My Block",
|
|
17
|
+
description: "A page introduction with an editable heading.",
|
|
20
18
|
content: {
|
|
21
|
-
|
|
22
|
-
},
|
|
23
|
-
settings: {
|
|
24
|
-
/* ... */
|
|
25
|
-
}, // Optional config toggles
|
|
19
|
+
title: Type.String({ default: "Welcome" }),
|
|
20
|
+
},
|
|
26
21
|
component: MyBlockComponent,
|
|
27
|
-
toMarkdown: (c) => [`# ${c.title}
|
|
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
|
|
46
|
-
| `title` | yes |
|
|
47
|
-
| `description` | yes |
|
|
48
|
-
| `content` | yes |
|
|
49
|
-
| `settings` | no |
|
|
50
|
-
| `layoutOnly` | no | If
|
|
51
|
-
| `synced` | no |
|
|
52
|
-
| `component` | yes |
|
|
53
|
-
| `toMarkdown` | yes |
|
|
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
|
-
`
|
|
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
|
-
|
|
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
|
|
62
|
-
-
|
|
63
|
-
- Publishing a page or layout containing the block publishes
|
|
64
|
-
- Enabling syncing on an existing type
|
|
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
|
-
|
|
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
|
-
|
|
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) => [
|
|
70
|
+
toMarkdown: (c) => [`> ${c.quote}`, `— ${c.author}, ${c.company}`, c.photo];
|
|
80
71
|
```
|
|
81
72
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
- **String**: raw text value
|
|
85
|
-
- **Link**: `[text](href)`
|
|
86
|
-
- **Image**: ``
|
|
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
|
-
|
|
75
|
+
| Type | Markdown |
|
|
76
|
+
| -------------------- | ------------------------------------------------ |
|
|
77
|
+
| String | Raw text |
|
|
78
|
+
| Link | `[text](href)` |
|
|
79
|
+
| Image | `` |
|
|
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
|
-
|
|
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
|
|
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"`.
|
|
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
|
|
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",
|
|
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.
|
|
120
|
+
### Type.Icon
|
|
175
121
|
|
|
176
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
171
|
+
### Images and files
|
|
208
172
|
|
|
209
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
204
|
+
minItems: 1,
|
|
271
205
|
maxItems: 10,
|
|
272
206
|
title: "Features",
|
|
273
|
-
toMarkdown: (c) => [`### ${c.name}`, c.description],
|
|
207
|
+
toMarkdown: (c) => [`### ${c.name}`, c.description],
|
|
274
208
|
});
|
|
275
209
|
```
|
|
276
210
|
|
|
277
|
-
|
|
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
|
-
|
|
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
|
-
|
|
311
|
-
|
|
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
|
-
|
|
220
|
+
### Strings — `block.Field`
|
|
315
221
|
|
|
316
|
-
|
|
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
|
-
|
|
232
|
+
For `textStyle` and `linkStyle`, read [Field styling](field-styling.md).
|
|
353
233
|
|
|
354
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
401
|
-
<myBlock.Embed name="videoUrl">
|
|
402
|
-
{(_props, { url }) => <iframe src={transformUrl(url)} />}
|
|
403
|
-
</myBlock.Embed>
|
|
404
|
-
```
|
|
256
|
+
### Repeaters — `block.Repeater`
|
|
405
257
|
|
|
406
|
-
|
|
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
|
-
|
|
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
|
-
{(
|
|
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
|
-
###
|
|
283
|
+
### Asset lists — `block.ImageList` / `block.FileList`
|
|
432
284
|
|
|
433
|
-
|
|
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
|
-
|
|
293
|
+
### Settings — `block.useSetting`
|
|
452
294
|
|
|
453
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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:
|