camox 0.69.0 → 0.72.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/dist/core/createApp.d.ts +4 -103
- package/dist/core/createApp.js +3 -24
- package/dist/core/createBlock.d.ts +2 -2
- package/dist/core/createBlock.js +55 -192
- package/dist/core/createLayout.d.ts +6 -77
- package/dist/core/createLayout.js +12 -50
- package/dist/core/editing/createEditableBlock.d.ts +9 -75
- package/dist/core/editing/createEditableBlock.js +258 -640
- package/dist/core/hooks/useArtifactHost.js +56 -46
- package/dist/core/hooks/useIsEditable.js +2 -2
- package/dist/core/lib/artifact.d.ts +4 -0
- package/dist/core/lib/artifactData.js +8 -3
- package/dist/core/lib/artifactHost.js +40 -0
- package/dist/core/lib/artifactRuntime.js +20 -1
- package/dist/core/lib/contentType.d.ts +23 -38
- package/dist/core/lib/contentType.js +34 -62
- package/dist/core/lib/iconTypes.d.ts +0 -3
- package/dist/core/lib/icons.js +63 -51
- package/dist/core/lib/recordScope.js +1 -1
- package/dist/features/canvas/CanvasOverlays.js +94 -176
- package/dist/features/canvas/CanvasPageFrame.js +62 -68
- package/dist/features/canvas/CanvasPreviewPageSlot.js +1 -3
- package/dist/features/canvas/canvasOverlayGeometry.js +1 -7
- package/dist/features/canvas/sidebar/CuratedPageInspector.js +3 -21
- package/dist/features/canvas/sidebar/LayoutPageBlocks.js +1 -2
- package/dist/features/canvas/useCanvasCamera.js +1 -1
- package/dist/features/canvas/useCanvasFrame.js +1 -1
- package/dist/features/content/ContentCollection.js +2 -2
- package/dist/features/content/collection-form.js +6 -7
- package/dist/features/page/CuratedPageContent.js +15 -24
- package/dist/features/page/UnplacedBlockContent.js +1 -6
- package/dist/features/preview/CamoxPreview.js +30 -33
- package/dist/features/preview/EditablePageContent.js +131 -151
- package/dist/features/preview/components/BlockActionsPopover.js +43 -152
- package/dist/features/preview/components/CanvasPreview.js +2 -2
- package/dist/features/preview/components/Frame.js +3 -3
- package/dist/features/preview/components/IconFieldEditor.js +1 -1
- package/dist/features/preview/components/ItemFieldsEditor.js +1 -3
- package/dist/features/preview/components/Overlays.js +18 -84
- package/dist/features/preview/components/PageTree.js +57 -103
- package/dist/features/preview/components/PreviewPanel.js +8 -11
- package/dist/features/preview/components/RecordCombobox.js +65 -78
- package/dist/features/preview/components/ReferenceFieldEditor.js +1 -19
- package/dist/features/preview/components/ReferenceListFieldEditor.js +1 -19
- package/dist/features/preview/components/RemovedBlockPlaceholder.js +2 -2
- package/dist/features/preview/components/SelectionEditor.js +5 -4
- package/dist/features/preview/components/useRepeatableItemActions.js +18 -24
- package/dist/features/preview/components/useUpdateBlockPosition.js +1 -1
- package/dist/features/preview/overlayMessages.js +1 -4
- package/dist/features/preview/previewStore.js +3 -23
- package/dist/features/renderer/RendererApp.js +17 -141
- package/dist/features/renderer/definitionAnswers.js +1 -8
- package/dist/features/renderer/rendererHost.js +1 -31
- package/dist/features/renderer/studioConnection.js +1 -25
- package/dist/features/runtime/pageApp.js +8 -4
- package/dist/features/runtime/runtime.js +3 -3
- package/dist/features/studio/agentRunToast.js +71 -0
- package/dist/features/studio/components/ChatWidget.js +2 -0
- package/dist/features/studio/components/DraftBar.js +2 -3
- package/dist/features/studio/components/DraftsControl.js +150 -176
- package/dist/features/studio/components/ReviewDraftSheet.js +344 -548
- package/dist/features/studio/components/ThreadMessages.js +89 -22
- package/dist/features/studio/draftLabel.js +1 -1
- package/dist/features/studio/studioStore.js +5 -17
- package/dist/features/studio/viewSwitch.js +2 -4
- package/dist/features/vite/blockBoilerplate.js +1 -1
- package/dist/features/vite/definitionsSync.js +1 -8
- package/dist/lib/queries.js +0 -1
- package/dist/lib/studio-definitions.js +0 -2
- package/dist/lib/studio-reads.js +0 -5
- package/dist/lib/use-thread.js +196 -138
- package/dist/package.js +1 -1
- package/dist/studio-overlays.css +0 -29
- package/dist/studio.css +1 -1
- package/package.json +6 -4
- package/skills/camox/SKILL.md +1 -1
- package/skills/camox/references/artifacts.md +89 -5
- package/skills/camox/references/block-definitions.md +21 -25
- package/skills/camox/references/dom-integrations.md +1 -1
- package/skills/camox/references/layout-definitions.md +3 -12
- package/skills/camox/references/routed-layouts.md +2 -2
- package/skills/camox-expand/SKILL.md +5 -5
- package/skills/camoxify/SKILL.md +4 -4
- package/dist/features/canvas/canvasInsertionSeams.js +0 -44
- package/dist/features/preview/components/AddBlockSheet.js +0 -314
- package/dist/features/preview/components/BlockPeek.js +0 -66
- package/dist/features/preview/components/BlockThumbnail.js +0 -281
- package/dist/features/preview/components/blockGridNavigation.js +0 -15
- package/dist/features/preview/referencePickerFocus.js +0 -29
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "camox",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.72.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"bin": {
|
|
6
6
|
"camox": "./bin/camox.mjs"
|
|
@@ -148,9 +148,9 @@
|
|
|
148
148
|
"unhead": "^3.4.3",
|
|
149
149
|
"yaml": "^2.9.0",
|
|
150
150
|
"zod": "^4.6.7",
|
|
151
|
-
"@camox/
|
|
152
|
-
"@camox/
|
|
153
|
-
"@camox/cli": "0.
|
|
151
|
+
"@camox/ui": "0.72.0",
|
|
152
|
+
"@camox/api-contract": "0.72.0",
|
|
153
|
+
"@camox/cli": "0.72.0"
|
|
154
154
|
},
|
|
155
155
|
"devDependencies": {
|
|
156
156
|
"@earendil-works/pi-ai": "1.3.0",
|
|
@@ -162,6 +162,7 @@
|
|
|
162
162
|
"@types/react-dom": "^19.3.0",
|
|
163
163
|
"happy-dom": "^21.0.1",
|
|
164
164
|
"oxc-transform-react": "^0.152.0",
|
|
165
|
+
"playwright-core": "1.64.1",
|
|
165
166
|
"tailwindcss": "^4.3.3",
|
|
166
167
|
"tsrs": "0.20.1-ts7.1.0-beta",
|
|
167
168
|
"tsx": "^4.23.15",
|
|
@@ -203,6 +204,7 @@
|
|
|
203
204
|
"build": "vp pack && tailwindcss -i ./src/studio.css -o ./dist/studio.css --minify && cp ./src/features/preview/studio-overlays.css ./dist/studio-overlays.css",
|
|
204
205
|
"build:watch": "vp pack --watch",
|
|
205
206
|
"test": "tsx --test \"src/**/*.test.ts\" \"src/**/*.test.tsx\"",
|
|
207
|
+
"test:browser": "tsx --test --test-concurrency=1 \"src/**/*.browser-test.ts\"",
|
|
206
208
|
"test:inline": "tsx --test src/core/lib/inlineText.test.tsx src/core/components/lexical/inlineEditing.test.tsx",
|
|
207
209
|
"test:images": "tsx --test src/core/lib/imagePlaceholder.test.tsx src/core/createBlock.test.tsx",
|
|
208
210
|
"test:content": "tsx --test src/features/content/content.test.tsx src/features/content/delete-collection-item.test.tsx"
|
package/skills/camox/SKILL.md
CHANGED
|
@@ -45,6 +45,6 @@ Use the platform. Build tabs, dialogs, carousels, menus and disclosures from nat
|
|
|
45
45
|
|
|
46
46
|
## Content
|
|
47
47
|
|
|
48
|
-
Code definitions do not place blocks on
|
|
48
|
+
Code definitions do not place blocks on Pages, assign layouts, or write content. When the request includes placement, assignment, or content, make it as a content change once Camox has discovered the new definition. Singleton pages and derived routes are declared in code, never created as Pages.
|
|
49
49
|
|
|
50
50
|
A request can cross boundaries. Load an additional reference only once that need is established.
|
|
@@ -12,23 +12,23 @@ A definition that would fit one place on one site, or settings bolted on only to
|
|
|
12
12
|
|
|
13
13
|
```tsx
|
|
14
14
|
content: (field) => ({
|
|
15
|
-
heading: field.string({
|
|
15
|
+
heading: field.string({ title: "Heading" }),
|
|
16
16
|
visual: field.artifact({ size: "fill", title: "Diagram" }),
|
|
17
17
|
}),
|
|
18
18
|
```
|
|
19
19
|
|
|
20
20
|
- `size` (required): `"fill"` fills a container whose size the block controls, for example with an aspect ratio; `"content"` is for frames whose height follows their page, such as calculators. See [Size to the content](#size-to-the-content).
|
|
21
21
|
- `data` (optional): sibling content fields and settings shared with the artifact as **Artifact Data**, so editors can change what it shows without touching its source. Inside a repeater item, the siblings are the item's own fields and settings. The names are type-checked; artifacts, repeaters and references can't be listed. See [`camox.data`](#camoxdata).
|
|
22
|
-
-
|
|
22
|
+
- Agents write sources as content, never in the definition. An empty artifact renders nothing on the website and a placeholder in the studio preview.
|
|
23
23
|
- Artifacts work in block content and repeater items, so also in layout and synced blocks. Collection records can't hold them.
|
|
24
24
|
|
|
25
25
|
```tsx
|
|
26
26
|
content: (field) => ({
|
|
27
|
-
label: field.string({
|
|
27
|
+
label: field.string({ title: "Label" }),
|
|
28
28
|
gauge: field.artifact({ size: "fill", data: ["label", "tone"] }),
|
|
29
29
|
}),
|
|
30
30
|
settings: (setting) => ({
|
|
31
|
-
tone: setting.enum({
|
|
31
|
+
tone: setting.enum({ options: { calm: "Calm", bold: "Bold" } }),
|
|
32
32
|
}),
|
|
33
33
|
```
|
|
34
34
|
|
|
@@ -56,7 +56,8 @@ The source is content, not code: set it as the artifact field's value, like any
|
|
|
56
56
|
```
|
|
57
57
|
|
|
58
58
|
- **A meaningful `<title>`.** It names the frame for screen readers and is the artifact's Markdown Representation. Without one, the artifact is called "Untitled artifact", and `blocks create` and `blocks edit` return a warning.
|
|
59
|
-
- **
|
|
59
|
+
- **Motion that plays on its own goes through the Timeline** (`camox.timeline`, below): no CSS animations, CSS transitions, Web Animations or timers that move the artifact by themselves. CSS transitions stay fine for feedback to a visitor's hover or press, and so do libraries you can drive from `render(t)`.
|
|
60
|
+
- **Respect `prefers-reduced-motion`.** The Timeline does it for you (it shows its poster). For any other motion, such as a hover transition, use the native media query, in CSS (`@media (prefers-reduced-motion: reduce)`) and in scripts (`matchMedia`), by stopping or simplifying it.
|
|
60
61
|
- **Link fonts from Google Fonts**, with a `<link>` to the family's `css2` stylesheet in the source's own `<head>`, as the website's Document does. The sandbox gives the page an opaque origin, and Google Fonts sends the CORS headers that needs. A font file the website serves itself loads only when its server sends `Access-Control-Allow-Origin: *` for it.
|
|
61
62
|
|
|
62
63
|
## The `camox` runtime
|
|
@@ -133,6 +134,89 @@ window.camox.onTheme(draw);
|
|
|
133
134
|
- **Never use `prefers-color-scheme`** to pick light or dark. Inside the frame it follows the visitor's OS, not the website, which may be dark on a light OS or the reverse. Rely on the tokens.
|
|
134
135
|
- **Fonts.** The website's font stylesheets and preconnect links from its document definition are loaded before your source, so its fonts work from the first render. Use the website's font family names, or the tokens that hold them.
|
|
135
136
|
|
|
137
|
+
### Motion: `camox.timeline`
|
|
138
|
+
|
|
139
|
+
An artifact that moves on its own declares a Timeline: how long its animation lasts, whether it loops, its named beats (Marks), and a `render(t)` function that draws the artifact at any time `t`, in milliseconds, from `t` alone. Call it once, synchronously, from the source (never after an `await`):
|
|
140
|
+
|
|
141
|
+
```js
|
|
142
|
+
const meter = document.querySelector(".meter");
|
|
143
|
+
camox.timeline({
|
|
144
|
+
duration: 12000, // ms
|
|
145
|
+
loop: true, // false: plays once and holds its last frame
|
|
146
|
+
marks: { typed: 1200, payoff: 4800, settled: 5500 },
|
|
147
|
+
poster: "settled", // the Mark whose frame stands for the artifact
|
|
148
|
+
render(t) {
|
|
149
|
+
meter.style.width = camox.interpolate(t, [4800, 5500], [0, 64], { ease: "arrive" }) + "%";
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
camox.type(document.querySelector(".query"), "Revenue by region", { at: 200, cps: 18 });
|
|
153
|
+
camox.count(document.querySelector(".total"), {
|
|
154
|
+
from: 0,
|
|
155
|
+
to: 1280,
|
|
156
|
+
at: 4800,
|
|
157
|
+
duration: 700,
|
|
158
|
+
format: (value) => "$" + Math.round(value).toLocaleString("en-US"),
|
|
159
|
+
});
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
- **`render(t)` draws from `t` alone.** It's called inside `requestAnimationFrame`, synchronously, and must give the same frame for the same `t` whatever came before: never keep state between calls, never read the clock, and never read layout. Write only what changed.
|
|
163
|
+
- **On the website**, the runtime calls `render` on every animation frame with real time. A looping Timeline wraps at `duration`: `t = duration` is the next loop's start, so its last frame is `duration - 1`. Make the last frame lead back into the first, so the loop restarts without a jump. A Timeline that doesn't loop holds its last frame.
|
|
164
|
+
- **When it plays.** A looping Timeline plays from the moment the artifact loads. One that doesn't loop waits until its frame first comes into the viewport, plays once and holds its last frame, so visitors don't miss it. Both pause while the frame is out of the viewport and resume where they were when it comes back.
|
|
165
|
+
- **Its first frame is drawn at once.** `camox.timeline` draws `t = 0` (Tracks and `render`) before it returns, before the artifact is revealed, so it never shows unfinished. Build the elements `render` and the Tracks use before calling it; the `type` and `count` helpers, called right after, are drawn at once too.
|
|
166
|
+
- **Reduced motion shows the poster.** For visitors who prefer reduced motion, the Timeline shows its poster's frame and never plays: name as `poster` the Mark whose frame tells the story on its own. Without one, it shows the last frame of a Timeline that doesn't loop, or the first of one that loops.
|
|
167
|
+
- **`look` sees it exactly.** Camox's agent captures any Moment of the Timeline at once: a Mark, a Mark plus a delay, or a time. Place each Mark where its beat is visible, as its transition settles, not where the transition starts: the frame at a Mark shows the beat, and a Mark at the start of an entrance shows the frame before it, which reads as a beat that comes late.
|
|
168
|
+
- **`look` reports what escapes it**, beside its images: CSS animations, CSS transitions that nothing a visitor did started, Web Animations the source made and left playing (a paused one whose `currentTime` `render(t)` sets, as a seekable library does, is on the Timeline), timers still running, Tracks the runtime skipped, Tracks on the same element and property that overlap, and a loop cut, where a looping Timeline's last frame differs from its first (what Tracks show and what `render` wrote, on what's visible at either end). An element hidden at both ends can reset freely. Fix each finding rather than working around it.
|
|
169
|
+
- Marks are times from `0` to `duration`. `poster` names one of them: the frame for reduced motion.
|
|
170
|
+
- **Movement and fades are Tracks**, not `render` (below).
|
|
171
|
+
- An artifact that never calls `camox.timeline` behaves as before, with nothing to seek.
|
|
172
|
+
|
|
173
|
+
#### Tracks: movement and fades
|
|
174
|
+
|
|
175
|
+
Anything that moves or fades is a Track: the runtime plays Tracks as Web Animations on the compositor, so they stay smooth while the website's page is busy loading, where motion written from `render` stutters, most of all on phones. Declare them in `tracks`:
|
|
176
|
+
|
|
177
|
+
```js
|
|
178
|
+
const card = document.querySelector(".card");
|
|
179
|
+
camox.timeline({
|
|
180
|
+
duration: 8000,
|
|
181
|
+
loop: true,
|
|
182
|
+
marks: { shown: 1300, gone: 6000 },
|
|
183
|
+
tracks: [
|
|
184
|
+
// In, hold, out: Tracks of one element and property play as one sequence.
|
|
185
|
+
{
|
|
186
|
+
el: card,
|
|
187
|
+
at: 400,
|
|
188
|
+
duration: 900,
|
|
189
|
+
ease: "arrive",
|
|
190
|
+
keyframes: { transform: ["translateY(24px)", "none"], opacity: [0, 1] },
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
el: card,
|
|
194
|
+
at: 5400,
|
|
195
|
+
duration: 600,
|
|
196
|
+
ease: "travel",
|
|
197
|
+
keyframes: { transform: ["none", "translateY(-24px)"], opacity: [1, 0] },
|
|
198
|
+
},
|
|
199
|
+
],
|
|
200
|
+
});
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
- A Track has its element `el`, its start `at` and its `duration` in ms, an `ease` (`"linear"`, the default, `"travel"` or `"arrive"`, the same curves as `interpolate`'s), and its `keyframes`: for `transform` and `opacity` only, each `[from, to]`. Anything else (colors, sizes, text) goes in `render`.
|
|
204
|
+
- A Track must end by the Timeline's `duration`. Before its first Track, an element shows that Track's `from`; between Tracks it holds the last `to`; after its last Track it holds its `to`. Make the value at the loop's end match its start.
|
|
205
|
+
- Several Tracks on one element and property must not overlap in time: give them one after the other.
|
|
206
|
+
- A Track the runtime can't play (another property, an unknown `ease`, an end past `duration`) is skipped, so the rest of the artifact still plays, and `look` reports it.
|
|
207
|
+
- **Size-based keyframes:** `keyframes` can be a function of the artifact's size, `({ width, height }) => keyframes`, in CSS pixels. The runtime calls it again when the frame is resized, keeping the current time: `keyframes: ({ width }) => ({ transform: ["none", "translateX(" + width / 2 + "px)"] })`.
|
|
208
|
+
- Don't animate a Track's element and property anywhere else, in CSS or from `render`.
|
|
209
|
+
|
|
210
|
+
Helpers for `render`:
|
|
211
|
+
|
|
212
|
+
- `camox.interpolate(t, [inStart, inEnd], [outStart, outEnd], { ease })` maps `t` from one range to the other, clamped at both ends. `ease` is `"linear"` (the default), `"travel"` (`cubic-bezier(0.45, 0, 0.1, 1)`, for something moving from one place to another) or `"arrive"` (`cubic-bezier(0.25, 0.8, 0.25, 1)`, for something coming in and settling), or a function of the progress from `0` to `1`.
|
|
213
|
+
- `camox.random(seed)` gives a number from `0` to `1` for a number or a string, always the same for the same seed. Use it instead of `Math.random()` for anything `render` draws, such as `camox.random(index)` per particle.
|
|
214
|
+
|
|
215
|
+
Typed text and rolling numbers have their own helpers. Call them once each, after `camox.timeline` and outside `render`: the Timeline draws them at every `t`, before `render`, so they restart with each loop and land exactly where `look` seeks. They write only what changed, and add no style of their own: the element keeps the website's font, size and color.
|
|
216
|
+
|
|
217
|
+
- `camox.type(el, text, { at, cps })` types `text` into `el`, starting at `at` ms (default `0`), `cps` characters a second (default `20`). It replaces what `el` holds with the typed text and a caret in the text's own line, right after the last character, in the text's color, whatever `el`'s layout (even a flex row with a gap). The first character shows at `at`, then one more every `1000 / cps` ms, so the whole text is in at `at + (length - 1) * 1000 / cps`. The caret is solid while typing, until `at + length * 1000 / cps`, one character's time after the last, and blinks before and after. Never draw a caret of your own.
|
|
218
|
+
- `camox.count(el, { from, to, at, duration, ease, format })` rolls the number in `el` from `from` (default `0`) to `to`, over `duration` ms (default `1000`) from `at` (default `0`), eased with `ease` (`"arrive"` by default; the same names as `interpolate`). It shows `from` before `at` and holds `to` after. `format(value)` returns the text to show; without it, the number is shown with the decimals of `from` and `to`, grouped in the visitor's locale. Give `el` `font-variant-numeric: tabular-nums` so the digits don't jitter as they roll.
|
|
219
|
+
|
|
136
220
|
### `camox.onViewport(callback)`
|
|
137
221
|
|
|
138
222
|
Scroll-driven animations follow where the frame sits in the page's viewport. The first call subscribes; it returns a function that unsubscribes. The callback receives:
|
|
@@ -16,7 +16,7 @@ const myBlock = createBlock({
|
|
|
16
16
|
title: "My Block",
|
|
17
17
|
description: "A page introduction with an editable heading.",
|
|
18
18
|
content: (field) => ({
|
|
19
|
-
title: field.string({
|
|
19
|
+
title: field.string({ title: "Title" }),
|
|
20
20
|
}),
|
|
21
21
|
toReact: MyBlockComponent,
|
|
22
22
|
toTitle: ({ content }) => content.title,
|
|
@@ -45,7 +45,7 @@ For DOM listeners, scrolling, scripts, widgets, or JavaScript-driven animation,
|
|
|
45
45
|
| `description` | yes | Agent guidance on placement, expected content, and tone. |
|
|
46
46
|
| `content` | yes | `(field) => ({ ... })`: field names mapped to `field.*` builders for editable content. |
|
|
47
47
|
| `settings` | no | `(setting) => ({ ... })`: settings-panel options; only `setting.enum` and `setting.boolean`. |
|
|
48
|
-
| `layoutOnly` | no | If true, restricts placement to layouts
|
|
48
|
+
| `layoutOnly` | no | If true, restricts placement to layouts. |
|
|
49
49
|
| `synced` | no | Defaults to false. Shares content/settings across placements; see below. |
|
|
50
50
|
| `toReact` | yes | Named React function component. Receives `{ content, settings }` as props. |
|
|
51
51
|
| `toTitle` | yes | `({ content, settings }) => ...` one-line title; see below. |
|
|
@@ -58,7 +58,7 @@ For DOM listeners, scrolling, scripts, widgets, or JavaScript-driven animation,
|
|
|
58
58
|
|
|
59
59
|
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
60
|
|
|
61
|
-
- Editing any placement updates the shared content everywhere. New placements reuse
|
|
61
|
+
- Editing any placement updates the shared content everywhere. New placements reuse the shared content.
|
|
62
62
|
- Position, layout slot, and deletion remain per placement; deleting one placement leaves the others.
|
|
63
63
|
- Enabling syncing on an existing type applies the oldest placement's data to all placements. Disabling it leaves independently editable copies.
|
|
64
64
|
|
|
@@ -119,7 +119,9 @@ Only Boolean/Enum settings support these conditionals. A repeater's `settings` r
|
|
|
119
119
|
|
|
120
120
|
## Content Field Types
|
|
121
121
|
|
|
122
|
-
`content` and a repeater's `content` receive a `field` builder; `settings` receive a `setting` builder. Each builder only offers what its context accepts: `field` has no `enum` or `boolean`, and `setting` has nothing else.
|
|
122
|
+
`content` and a repeater's `content` receive a `field` builder; `settings` receive a `setting` builder. Each builder only offers what its context accepts: `field` has no `enum` or `boolean`, and `setting` has nothing else.
|
|
123
|
+
|
|
124
|
+
A definition declares a block's shape, never its content: the agent writes every field's content when it places the block. A field nobody has written reads as the empty value of its kind: `""` for strings, embeds, icons and artifacts, an empty link, `null` for an image, file or reference, no items for lists and repeaters, `false` for a boolean setting, and an enum setting's first option. So render well with any content, including empty optional fields: a short or long title, a missing subtitle, one repeatable item or many. An empty image paints Camox's placeholder with the stylesheet's `--camox-placeholder` and `--camox-placeholder-ink`; an empty file, image list or file list renders nothing.
|
|
123
125
|
|
|
124
126
|
### field.string
|
|
125
127
|
|
|
@@ -127,7 +129,6 @@ Inline-editable text supporting formatting and links while remaining a string. S
|
|
|
127
129
|
|
|
128
130
|
```tsx
|
|
129
131
|
field.string({
|
|
130
|
-
default: "Hello world",
|
|
131
132
|
title: "Heading", // Optional label
|
|
132
133
|
maxLength: 280, // Optional
|
|
133
134
|
minLength: 1, // Optional
|
|
@@ -143,34 +144,33 @@ Choose one Iconify collection in Vite config:
|
|
|
143
144
|
camox({ projectId: "01a15fff-627b-7fa1-938b-93ab659c1acc", icons: "lucide" });
|
|
144
145
|
```
|
|
145
146
|
|
|
146
|
-
Declare
|
|
147
|
+
Declare an icon field, then render inline SVG:
|
|
147
148
|
|
|
148
149
|
```tsx
|
|
149
150
|
// In content:
|
|
150
|
-
icon: field.icon({
|
|
151
|
+
icon: field.icon({ title: "Icon" })
|
|
151
152
|
|
|
152
153
|
// In the component (also supported as <item.Icon> inside repeaters):
|
|
153
154
|
<block.Icon name="icon" className="size-6 text-brand" />
|
|
154
155
|
<block.Icon name="icon" aria-label="Fast" />
|
|
155
156
|
```
|
|
156
157
|
|
|
157
|
-
Icons are decorative unless given `aria-label` or `aria-labelledby`. Clicking an icon in preview opens the searchable collection picker. Values
|
|
158
|
+
Icons are decorative unless given `aria-label` or `aria-labelledby`. Clicking an icon in preview opens the searchable collection picker. Values are namespaced strings such as `"lucide:zap"`; editors cannot select another collection or clear the icon.
|
|
158
159
|
|
|
159
160
|
Run `pnpm exec camox typegen` **before TypeScript checking in CI**, for example `camox typegen && tsc --noEmit`. This resolves Vite config without starting a server. 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.
|
|
160
161
|
|
|
161
|
-
Camox resolves selected SVGs from that same versioned snapshot and serves them through its API. Server rendering preloads selected
|
|
162
|
+
Camox resolves selected SVGs from that same versioned snapshot and serves them through its API. Server rendering preloads selected 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.
|
|
162
163
|
|
|
163
164
|
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.
|
|
164
165
|
|
|
165
166
|
### setting.boolean and setting.enum
|
|
166
167
|
|
|
167
|
-
Settings only: configuration choices are never block or repeater content.
|
|
168
|
+
Settings only: configuration choices are never block or repeater content. An unset enum reads as its first option, so list the most common or neutral one first. An unset boolean reads as `false`, so name it for what turning it on does (`showBackground`, not `hideBackground`).
|
|
168
169
|
|
|
169
170
|
```tsx
|
|
170
171
|
settings: (setting) => ({
|
|
171
|
-
showBackground: setting.boolean({
|
|
172
|
+
showBackground: setting.boolean({ title: "Show background" }),
|
|
172
173
|
alignment: setting.enum({
|
|
173
|
-
default: "left",
|
|
174
174
|
options: { left: "Left", center: "Center", right: "Right" },
|
|
175
175
|
title: "Alignment",
|
|
176
176
|
}),
|
|
@@ -182,10 +182,7 @@ settings: (setting) => ({
|
|
|
182
182
|
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.
|
|
183
183
|
|
|
184
184
|
```tsx
|
|
185
|
-
field.link({
|
|
186
|
-
default: { text: "Learn more", href: "/", newTab: false },
|
|
187
|
-
title: "CTA",
|
|
188
|
-
});
|
|
185
|
+
field.link({ title: "CTA" });
|
|
189
186
|
```
|
|
190
187
|
|
|
191
188
|
### Images and files
|
|
@@ -194,19 +191,18 @@ Files support MIME filtering. Render each type with its matching helper; asset l
|
|
|
194
191
|
|
|
195
192
|
```tsx
|
|
196
193
|
field.image({ title: "Cover photo" });
|
|
197
|
-
field.imageList({
|
|
194
|
+
field.imageList({ title: "Gallery images" });
|
|
198
195
|
field.file({ accept: ["application/pdf"], title: "PDF Document" });
|
|
199
|
-
field.fileList({ accept: ["application/pdf"],
|
|
196
|
+
field.fileList({ accept: ["application/pdf"], title: "Documents" });
|
|
200
197
|
```
|
|
201
198
|
|
|
202
199
|
### field.embed
|
|
203
200
|
|
|
204
|
-
A URL validated against a regex.
|
|
201
|
+
A URL validated against a regex. Content that doesn't match is rejected.
|
|
205
202
|
|
|
206
203
|
```tsx
|
|
207
204
|
field.embed({
|
|
208
205
|
pattern: "https:\\/\\/(www\\.)?(youtube\\.com|youtu\\.be)\\/.+",
|
|
209
|
-
default: "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
|
210
206
|
title: "YouTube URL",
|
|
211
207
|
});
|
|
212
208
|
```
|
|
@@ -222,16 +218,16 @@ field.artifact({ size: "fill", data: ["headline", "variant"] }); // shares sibli
|
|
|
222
218
|
|
|
223
219
|
### field.repeater
|
|
224
220
|
|
|
225
|
-
Structured items with their own content, optional settings, and required `toTitle` and `toMarkdown`. `minItems` must be at least 1
|
|
221
|
+
Structured items with their own content, optional settings, and required `toTitle` and `toMarkdown`. `minItems` must be at least 1: editors can't remove the last item. A block whose repeater was never written has no items, so render well with none. Nest repeaters in an item's `content` when needed.
|
|
226
222
|
|
|
227
223
|
```tsx
|
|
228
224
|
field.repeater({
|
|
229
225
|
content: (field) => ({
|
|
230
|
-
name: field.string({
|
|
231
|
-
description: field.string({
|
|
226
|
+
name: field.string({ title: "Name" }),
|
|
227
|
+
description: field.string({ title: "Description" }),
|
|
232
228
|
}),
|
|
233
229
|
settings: (setting) => ({
|
|
234
|
-
highlighted: setting.boolean({
|
|
230
|
+
highlighted: setting.boolean({ title: "Highlighted" }),
|
|
235
231
|
}),
|
|
236
232
|
minItems: 1,
|
|
237
233
|
maxItems: 10,
|
|
@@ -279,7 +275,7 @@ Place a repeater's token where its items' nodes go, such as an FAQ's questions:
|
|
|
279
275
|
|
|
280
276
|
```tsx
|
|
281
277
|
items: field.repeater({
|
|
282
|
-
content: (field) => ({ question: field.string({
|
|
278
|
+
content: (field) => ({ question: field.string({ title: "Question" }), answer: field.string({ title: "Answer" }) }),
|
|
283
279
|
minItems: 1,
|
|
284
280
|
maxItems: 20,
|
|
285
281
|
toTitle: ({ content }) => content.question,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
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
4
|
|
|
5
|
-
**Do not use component-global `window` or `document` to target a block's DOM.** Camox preview
|
|
5
|
+
**Do not use component-global `window` or `document` to target a block's DOM.** Camox preview 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
6
|
|
|
7
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
8
|
|
|
@@ -23,7 +23,6 @@ const myLayout = createLayout({
|
|
|
23
23
|
blocks: {
|
|
24
24
|
before: [navbarBlock],
|
|
25
25
|
after: [footerBlock],
|
|
26
|
-
initial: [() => import("../blocks/hero")],
|
|
27
26
|
},
|
|
28
27
|
toReact: MyLayoutComponent,
|
|
29
28
|
toTitle: ({ page, project }) => `${page.title} | ${project.name}`,
|
|
@@ -50,7 +49,7 @@ export { myLayout as layout };
|
|
|
50
49
|
| `id` | yes | Unique kebab-case identifier matching the filename without extension. |
|
|
51
50
|
| `title` | yes | CMS display name. |
|
|
52
51
|
| `description` | yes | Guidance for users and agents on which pages suit this layout. |
|
|
53
|
-
| `blocks` | yes | `before
|
|
52
|
+
| `blocks` | yes | `before` and `after` arrays; see layout slot rules below. |
|
|
54
53
|
| `toReact` | yes | Named function component receiving the context and `{ children }` as props. |
|
|
55
54
|
| `toTitle` | yes | `(context) => string`: the head title, `og:title`, and the Markdown front matter's `title`. |
|
|
56
55
|
| `toDescription` | yes | `(context) => string \| undefined`: the head description and `og:description`. May return `undefined`. |
|
|
@@ -68,20 +67,12 @@ Every `to*` option receives the same context object, and none receives another o
|
|
|
68
67
|
|
|
69
68
|
Singleton and derived layouts also receive `data` and `params`, and record pages `content`; see [Routed layouts](routed-layouts.md#metadata-and-markdown).
|
|
70
69
|
|
|
71
|
-
## Layout Slots — `before
|
|
70
|
+
## Layout Slots — `before` and `after`
|
|
72
71
|
|
|
73
72
|
- `before` and `after` require blocks with `layoutOnly: true`. Either array may be empty.
|
|
74
73
|
- Render groups with `<myLayout.BeforeBlocks />` and `<myLayout.AfterBlocks />`, not individual blocks. Array order controls rendering order; edit the config to add or reorder blocks.
|
|
75
74
|
- For identical navbar/footer content across layouts, also set `synced: true`. `layoutOnly` restricts a block to layout slots; it does not enable syncing. See [Synced blocks](block-definitions.md#synced-blocks-synced-optional) for sharing and existing-placement behavior.
|
|
76
|
-
|
|
77
|
-
## Initial Blocks — `blocks.initial` (optional)
|
|
78
|
-
|
|
79
|
-
`initial` seeds the homepage at project creation, using block defaults in array order. It does not populate existing Pages.
|
|
80
|
-
|
|
81
|
-
- List each block as an import of its module, `() => import("../blocks/hero")`, never a statically imported block. Layouts load on every page, so a static import would ship the block to every visitor.
|
|
82
|
-
- Accepts only blocks with `layoutOnly` omitted or `false`.
|
|
83
|
-
- At most one layout per app may define `initial`; multiple definitions cause a startup error.
|
|
84
|
-
- Without `initial`, Camox seeds an empty homepage using the first curated layout. Apps with only singleton/derived layouts do not seed a curated homepage.
|
|
75
|
+
- A slot's new blocks start with no content when the layout syncs. Fill them as a [content change](../SKILL.md#content) once Camox has discovered the layout.
|
|
85
76
|
|
|
86
77
|
## OG Image — `toImage` (optional)
|
|
87
78
|
|
|
@@ -53,7 +53,7 @@ function PokedexPage() {
|
|
|
53
53
|
- `loader` is optional. Return `{ kind: "data", data }`, synchronously or asynchronously. `layout.useData()` infers and returns the unwrapped `data`, not the envelope. It runs server-side for document requests and client navigation; the entire envelope travels through JSON to hydration. Keep data JSON-serializable (no functions, symbols, BigInt, or cyclic objects). Without a loader, `useData()` returns `undefined`. The loader also receives `locale`, the page's locale tag; Camox renders its data untranslated (see [Localization](localization.md#loaders)).
|
|
54
54
|
- 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).
|
|
55
55
|
- 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 behavior.
|
|
56
|
-
- No `children` slot
|
|
56
|
+
- No `children` slot: editors do not compose or create these pages.
|
|
57
57
|
- No curated page record is created. Never create, duplicate, move, or assign a singleton as a Page.
|
|
58
58
|
- Studio lists singletons as directly navigable pages with block editing, 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 pages appear in the sitemap only when their layout declares `routes` (see [Route discovery](#route-discovery)).
|
|
59
59
|
- 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.
|
|
@@ -65,7 +65,7 @@ function PokedexPage() {
|
|
|
65
65
|
|
|
66
66
|
## Derived routes
|
|
67
67
|
|
|
68
|
-
Use the same file-based form with `kind: "derived"`, for example `createLayout("pokemon.$name")`. Its required `loader: ({ params }) => ...` receives typed `params.name` and returns `{ kind: "data", data }`. Read its unwrapped data 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
|
|
68
|
+
Use the same file-based form with `kind: "derived"`, for example `createLayout("pokemon.$name")`. Its required `loader: ({ params }) => ...` receives typed `params.name` and returns `{ kind: "data", data }`. Read its unwrapped data 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`. Unlike singletons, curated URLs retain precedence over derived routes.
|
|
69
69
|
|
|
70
70
|
### Route discovery
|
|
71
71
|
|
|
@@ -51,11 +51,11 @@ Stop suggesting when the next idea would be filler; there is no target count. Th
|
|
|
51
51
|
|
|
52
52
|
Make only these kinds of change:
|
|
53
53
|
|
|
54
|
-
- **New Block Definitions**, following [Block Definitions](../camox/references/block-definitions.md), [Localized code](../camox/SKILL.md#localized-code) (every visible text in fields, internal links through `useLocalizedHref` without a locale prefix), and Camoxify's rules on [styling](../camoxify/SKILL.md#use-the-existing-styling-stack), [genericity
|
|
55
|
-
- **New Settings on existing Block Definitions
|
|
54
|
+
- **New Block Definitions**, following [Block Definitions](../camox/references/block-definitions.md), [Localized code](../camox/SKILL.md#localized-code) (every visible text in fields, internal links through `useLocalizedHref` without a locale prefix), and Camoxify's rules on [styling](../camoxify/SKILL.md#use-the-existing-styling-stack), [genericity](../camoxify/SKILL.md#keep-definitions-generic), [repeaters](../camoxify/SKILL.md#model-repetition-not-a-fixed-screenshot), [Settings](../camoxify/SKILL.md#use-settings-for-constrained-choices), and [formatted text](../camoxify/SKILL.md#keep-formatted-text-in-one-field).
|
|
55
|
+
- **New Settings on existing Block Definitions** whose unset value (`false`, or an enum's first option) leaves current rendering unchanged, since existing blocks read it. Existing content schemas stay exactly as they are. Before editing the block, capture it at desktop and mobile on a Page that places it, on the running dev server; step 6 compares against this capture.
|
|
56
56
|
- **Description edits.** A new block's `description` says when to choose it over its siblings. Edit the siblings' descriptions so each one names what sets it apart, and the Agent can choose between them.
|
|
57
57
|
|
|
58
|
-
New blocks live only in code, plus one review Page in the development draft: create it with the CLI (see [Managing Pages and Blocks](../camox-cli/references/cli-content.md)) and place each new block on it with
|
|
58
|
+
New blocks live only in code, plus one review Page in the development draft: create it with the CLI (see [Managing Pages and Blocks](../camox-cli/references/cli-content.md)) and place each new block on it with representative content. Leave every other Page, layouts, and publishing alone. This rules out new Layout-only Blocks: a Page can't hold them, and they only sync once a layout uses them. An existing Layout-only Block that gains a Setting is captured and checked on a Page whose layout uses it.
|
|
59
59
|
|
|
60
60
|
## 6. Verify
|
|
61
61
|
|
|
@@ -66,5 +66,5 @@ The run is complete when:
|
|
|
66
66
|
- every approved item is built, and every rejected one is deleted,
|
|
67
67
|
- the review Page is deleted once the operator has reviewed it,
|
|
68
68
|
- the project's `check` script passes, and so does `vp lint --type-aware --type-check`, since `vp check` skips type errors unless the project's Vite config sets `lint.options.typeCheck`,
|
|
69
|
-
- every new block renders correctly with its
|
|
70
|
-
- every existing block that gained a Setting renders the same as its capture with
|
|
69
|
+
- every new block renders correctly with its review content, and with its optional fields empty, at desktop and mobile on the review Page,
|
|
70
|
+
- every existing block that gained a Setting renders the same as its capture with the new Setting unset.
|
package/skills/camoxify/SKILL.md
CHANGED
|
@@ -47,9 +47,9 @@ Express layout, spacing, typography, colors, responsive behavior, and interactiv
|
|
|
47
47
|
|
|
48
48
|
## Keep definitions generic
|
|
49
49
|
|
|
50
|
-
Block names, schemas,
|
|
50
|
+
Block names, schemas, and rendering must work on other pages. Use `feature-grid`, not `acme-homepage-features`.
|
|
51
51
|
|
|
52
|
-
Apply reference-specific content **only to instances through the CLI**, never
|
|
52
|
+
Apply reference-specific content **only to instances through the CLI**, never in the definition or hardcoded in JSX.
|
|
53
53
|
|
|
54
54
|
Keep the result translatable, following [Localized code](../camox/SKILL.md#localized-code): every visible text in the shell, navigation and footer included, lives in blocks, and internal links go through `useLocalizedHref`, written without a locale prefix.
|
|
55
55
|
|
|
@@ -63,7 +63,7 @@ Choose bounds from real constraints, not the number of items visible in the refe
|
|
|
63
63
|
|
|
64
64
|
Before adding a `content` field, ask whether the editor is writing free-form content or choosing from known options. For configuration choices such as alignment, theme, size, or variant, use `settings` with `setting.enum`. For on/off choices such as showing a background or enabling autoplay, use `settings` with `setting.boolean`.
|
|
65
65
|
|
|
66
|
-
Do not model these choices as `content` fields using `field.string`, even if rendering code could interpret strings such as `"left"`, `"dark"`, or `"true"`. Editors should choose a valid option or toggle a setting, not type a magic string. Reserve `content`'s `field.string` for genuinely free-form text, such as headings and descriptions.
|
|
66
|
+
Do not model these choices as `content` fields using `field.string`, even if rendering code could interpret strings such as `"left"`, `"dark"`, or `"true"`. Editors should choose a valid option or toggle a setting, not type a magic string. Reserve `content`'s `field.string` for genuinely free-form text, such as headings and descriptions.
|
|
67
67
|
|
|
68
68
|
## Keep formatted text in one field
|
|
69
69
|
|
|
@@ -81,7 +81,7 @@ Use inline formatting and `textStyle`, including highlights, instead of fields s
|
|
|
81
81
|
|
|
82
82
|
Follow the [Camox CLI workflow](../camox-cli/SKILL.md) to assign layouts, place blocks, and populate content, including shared navigation/footer content. Don't publish unless asked.
|
|
83
83
|
|
|
84
|
-
Compare the rendered site with the reference at desktop and mobile sizes. Verify that fields remain editable and that longer text and different repeater counts still work. Fix the reusable implementation, not just the one captured arrangement.
|
|
84
|
+
Compare the rendered site with the reference at desktop and mobile sizes. Verify that fields remain editable and that longer text, empty optional fields, and different repeater counts still work. Fix the reusable implementation, not just the one captured arrangement.
|
|
85
85
|
|
|
86
86
|
In multi-page runs, give this full check to every page that introduced or changed a definition, and to each page family's representative page. Compare the remaining pages with their source at desktop size. The run is complete when every page in `camoxify-progress.md` is verified.
|
|
87
87
|
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
//#region src/features/canvas/canvasInsertionSeams.ts
|
|
2
|
-
/** Main block wrappers publish insertion permissions, including layout boundaries.
|
|
3
|
-
* Detached block targets and fields deliberately do not publish this metadata. */
|
|
4
|
-
function readBlockInsertion(element) {
|
|
5
|
-
const value = element.getAttribute("data-camox-block-insertion");
|
|
6
|
-
if (!value) return;
|
|
7
|
-
try {
|
|
8
|
-
const insertion = JSON.parse(value);
|
|
9
|
-
if (!insertion || typeof insertion.position !== "string" || typeof insertion.before !== "boolean" || typeof insertion.after !== "boolean" || insertion.afterPosition !== void 0 && insertion.afterPosition !== null && typeof insertion.afterPosition !== "string") return;
|
|
10
|
-
return insertion;
|
|
11
|
-
} catch {
|
|
12
|
-
return;
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
/** Walk boundaries, not blocks: the bottom of one block and the top of its
|
|
16
|
-
* successor describe ONE insertion seam, even when both permit insertion. */
|
|
17
|
-
function canvasInsertionSeams(targets) {
|
|
18
|
-
const blocks = targets.filter((target) => target.insertion && target.bounds.width > 0 && target.bounds.height > 0);
|
|
19
|
-
const seams = [];
|
|
20
|
-
for (let index = 0; index <= blocks.length; index++) {
|
|
21
|
-
const previous = blocks[index - 1];
|
|
22
|
-
const next = blocks[index];
|
|
23
|
-
const anchor = previous?.insertion?.after ? previous : next?.insertion?.before ? next : null;
|
|
24
|
-
if (!anchor?.insertion) continue;
|
|
25
|
-
const insertPosition = anchor === previous ? "after" : "before";
|
|
26
|
-
const { position, afterPosition } = anchor.insertion;
|
|
27
|
-
const y = previous && next ? (previous.bounds.y + previous.bounds.height + next.bounds.y) / 2 : anchor.bounds.y + (insertPosition === "after" ? anchor.bounds.height : 0);
|
|
28
|
-
seams.push({
|
|
29
|
-
key: `${previous?.element.getAttribute("data-camox-block-id") ?? "start"}:${next?.element.getAttribute("data-camox-block-id") ?? "end"}`,
|
|
30
|
-
x: anchor.bounds.x + anchor.bounds.width / 2,
|
|
31
|
-
y,
|
|
32
|
-
request: {
|
|
33
|
-
type: "CAMOX_ADD_BLOCK_REQUEST",
|
|
34
|
-
blockPosition: position,
|
|
35
|
-
insertPosition,
|
|
36
|
-
...afterPosition !== void 0 ? { afterPosition } : {}
|
|
37
|
-
}
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
return seams;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
//#endregion
|
|
44
|
-
export { canvasInsertionSeams, readBlockInsertion };
|