camox 0.68.0 → 0.71.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 (34) hide show
  1. package/dist/core/createBlock.js +14 -7
  2. package/dist/core/hooks/useArtifactHost.js +56 -46
  3. package/dist/core/lib/artifact.d.ts +4 -0
  4. package/dist/core/lib/artifactHost.js +40 -0
  5. package/dist/core/lib/artifactRuntime.js +20 -1
  6. package/dist/features/canvas/canvasFrameDocument.js +25 -2
  7. package/dist/features/canvas/useCanvasFrame.js +6 -2
  8. package/dist/features/preview/components/previewStyles.js +11 -2
  9. package/dist/features/provider/AuthenticatedCamoxProvider.js +33 -28
  10. package/dist/features/renderer/RendererApp.js +202 -154
  11. package/dist/features/renderer/RendererFrames.js +87 -55
  12. package/dist/features/renderer/definitionAnswers.js +1 -0
  13. package/dist/features/renderer/rendererHost.js +4 -0
  14. package/dist/features/renderer/studioConnection.js +9 -1
  15. package/dist/features/runtime/pageApp.js +7 -3
  16. package/dist/features/runtime/pageNavigation.js +93 -17
  17. package/dist/features/runtime/rendererClient.js +13 -6
  18. package/dist/features/studio/agentRunToast.js +71 -0
  19. package/dist/features/studio/components/ChatWidget.js +2 -0
  20. package/dist/features/studio/components/DraftBar.js +2 -3
  21. package/dist/features/studio/components/DraftsControl.js +181 -198
  22. package/dist/features/studio/components/LocalePicker.js +22 -4
  23. package/dist/features/studio/components/ReviewDraftSheet.js +345 -568
  24. package/dist/features/studio/draftLabel.js +1 -1
  25. package/dist/features/studio/studioStore.js +5 -17
  26. package/dist/features/studio/viewSwitch.js +62 -0
  27. package/dist/lib/auth.js +5 -1
  28. package/dist/lib/draft-scope.js +28 -15
  29. package/dist/lib/studio-reads.js +46 -13
  30. package/dist/lib/view-switch.js +64 -0
  31. package/dist/package.js +1 -1
  32. package/dist/studio.css +1 -1
  33. package/package.json +6 -4
  34. package/skills/camox/references/artifacts.md +85 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "camox",
3
- "version": "0.68.0",
3
+ "version": "0.71.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/api-contract": "0.68.0",
152
- "@camox/cli": "0.68.0",
153
- "@camox/ui": "0.68.0"
151
+ "@camox/api-contract": "0.71.0",
152
+ "@camox/cli": "0.71.0",
153
+ "@camox/ui": "0.71.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"
@@ -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
- - **Respect `prefers-reduced-motion`** with the native media query, in CSS (`@media (prefers-reduced-motion: reduce)`) and in scripts (`matchMedia`), by stopping or simplifying motion.
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: