twombly 0.4.2 → 0.4.4
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/cli.js +150 -47
- package/dist/ui/assets/Landing-CMSAED8E.js +111 -0
- package/dist/ui/assets/LandingStage-DlOW_YOx.js +1 -0
- package/dist/ui/assets/agentPrompt-Va8atsXW.js +1 -0
- package/dist/ui/assets/index-CJ7B-FU8.css +1 -0
- package/dist/ui/assets/index-DgaQcoTH.js +10 -0
- package/dist/ui/index.html +3 -3
- package/dist/ui/terms.html +5 -3
- package/package.json +1 -1
- package/dist/ui/assets/Landing-b9T9rJsm.js +0 -111
- package/dist/ui/assets/LandingStage-DuMwL_Pp.js +0 -1
- package/dist/ui/assets/agentPrompt-osLcSdHt.js +0 -1
- package/dist/ui/assets/index-CwpZWZuC.js +0 -10
- package/dist/ui/assets/index-DHQst-JO.css +0 -1
package/dist/cli.js
CHANGED
|
@@ -205,12 +205,69 @@ var FRAME_POSTER_PATH = "/__twombly/poster.png";
|
|
|
205
205
|
var FRAME_BRIDGE_JS = `(() => {
|
|
206
206
|
if (window.parent === window) return;
|
|
207
207
|
const post = message => { try { window.parent.postMessage({ twombly: true, ...message }, '*'); } catch {} };
|
|
208
|
+
const raf = requestAnimationFrame.bind(window), caf = cancelAnimationFrame.bind(window);
|
|
209
|
+
const held = new Map();
|
|
210
|
+
let resting = false, every = 1, nextHeld = -1, stopped = [], pumping = false, beat = 0;
|
|
211
|
+
// Callbacks wait here while the page rests or is paced, and the pump hands them out on the frames it keeps.
|
|
212
|
+
const pump = () => { if (!pumping && !resting && held.size) { pumping = true; raf(step); } };
|
|
213
|
+
const step = time => {
|
|
214
|
+
pumping = false;
|
|
215
|
+
if (resting) return;
|
|
216
|
+
if (every > 1 && ++beat % every) return pump();
|
|
217
|
+
const due = [...held.values()];
|
|
218
|
+
held.clear();
|
|
219
|
+
for (const callback of due) try { callback(time); } catch (error) { setTimeout(() => { throw error; }); }
|
|
220
|
+
};
|
|
221
|
+
window.requestAnimationFrame = callback => {
|
|
222
|
+
if (!resting && every === 1 && !held.size) return raf(callback);
|
|
223
|
+
held.set(nextHeld, callback);
|
|
224
|
+
pump();
|
|
225
|
+
return nextHeld--;
|
|
226
|
+
};
|
|
227
|
+
window.cancelAnimationFrame = id => { if (id < 0) held.delete(id); else caf(id); };
|
|
228
|
+
const rest = () => {
|
|
229
|
+
if (resting) return;
|
|
230
|
+
resting = true;
|
|
231
|
+
const animations = document.getAnimations ? document.getAnimations().filter(animation => animation.playState === 'running') : [];
|
|
232
|
+
const media = [...document.querySelectorAll('video, audio')].filter(element => !element.paused);
|
|
233
|
+
animations.forEach(animation => animation.pause());
|
|
234
|
+
media.forEach(element => element.pause());
|
|
235
|
+
stopped = [...animations, ...media];
|
|
236
|
+
};
|
|
237
|
+
const wake = pace => {
|
|
238
|
+
every = Math.max(1, Math.min(8, Math.round(pace) || 1));
|
|
239
|
+
if (!resting) return pump();
|
|
240
|
+
resting = false;
|
|
241
|
+
stopped.forEach(item => { try { const played = item.play(); played?.catch?.(() => {}); } catch {} });
|
|
242
|
+
stopped = [];
|
|
243
|
+
pump();
|
|
244
|
+
};
|
|
245
|
+
// The canvas's recorder asks each page how smoothly it's drawing, once a second.
|
|
246
|
+
let counting = false;
|
|
247
|
+
const count = () => {
|
|
248
|
+
if (counting) return;
|
|
249
|
+
counting = true;
|
|
250
|
+
let frames = 0, worst = 0, last = performance.now(), since = last;
|
|
251
|
+
const tick = now => {
|
|
252
|
+
frames++; worst = Math.max(worst, now - last); last = now;
|
|
253
|
+
if (now - since >= 1000) { post({ kind: 'fps', fps: Math.round(frames * 1000 / (now - since)), worst: Math.round(worst) }); frames = 0; worst = 0; since = now; }
|
|
254
|
+
raf(tick);
|
|
255
|
+
};
|
|
256
|
+
raf(tick);
|
|
257
|
+
};
|
|
258
|
+
addEventListener('message', event => {
|
|
259
|
+
if (event.source !== window.parent || !event.data?.twombly) return;
|
|
260
|
+
if (event.data.kind === 'rest') rest();
|
|
261
|
+
else if (event.data.kind === 'wake') wake(event.data.every);
|
|
262
|
+
else if (event.data.kind === 'count') count();
|
|
263
|
+
});
|
|
264
|
+
post({ kind: 'ready' });
|
|
208
265
|
let queued = null, scheduled = false;
|
|
209
266
|
addEventListener('pointermove', event => {
|
|
210
267
|
queued = { kind: 'pointer', x: event.clientX, y: event.clientY };
|
|
211
268
|
if (scheduled) return;
|
|
212
269
|
scheduled = true;
|
|
213
|
-
|
|
270
|
+
raf(() => { scheduled = false; post(queued); });
|
|
214
271
|
}, { capture: true, passive: true });
|
|
215
272
|
addEventListener('wheel', event => {
|
|
216
273
|
if (!event.ctrlKey && !event.metaKey) return;
|
|
@@ -273,7 +330,7 @@ function nextPosition(frames, newRow = false) {
|
|
|
273
330
|
}
|
|
274
331
|
|
|
275
332
|
// ../../skills/twombly/SKILL.md
|
|
276
|
-
var SKILL_default = "---\nname: twombly\ndescription: Lay out design explorations on a shared canvas. Use when asked to \"put this on Twombly\", \"put these on a canvas\", \"lay out these versions\", \"show me all the variations side by side\", or to share explorations with real-time cursors and comments. Works with any framework in any repo.\n---\n\n# Twombly\n\nTwombly shows design work as live frames on an infinite canvas. You build each version, turn it into a folder of static files, and add it. The canvas opens on `localhost`; `init` returns its address. Nothing is online until the person clicks Share. After that the local canvas and the link are one state: everything you add, update or move appears for everyone straight away.\n\nRun every command as `npx -y twombly@latest <command>`; this guide shortens that to `twombly <command>`. Output is JSON.\n\n## The rule that decides everything\n\n**A frame is a folder of static files that runs with no server and no network.** It can use anything in its own folder and nothing else. The browser enforces this: outside requests, form posts and server code all fail, locally and once shared.\n\nThe person's repo may have a database, an API, auth and environment keys. None of that goes in a frame. Your job is to adapt the design so it works without them:\n\n- **Data and APIs:** replace with realistic mock data, written inline or as JSON in the folder. For public CMS content, save a snapshot as JSON when you build.\n- **Auth and providers:** skip login, and mount the design with mock providers and a fake signed-in user.\n- **Environment values:** never include them. Build with none set. Don't copy `.env` files. If the build needs a value to compile, use an obviously fake one. **Never run or read a secret manager such as Infisical or Doppler for this, even if the project is set up for it.** If you can't get what you need without one, ask the person.\n- **Analytics and widgets:** remove tag managers, analytics, chat widgets and similar scripts, including any in page templates such as `app.html` or `_document`.\n- **Real customer data:** never copy it from a database, fixture or log. Invent names, emails and numbers. The gates can't detect this, so it's on you. Links are public.\n- **Fonts, images and video:** copy the files into the folder with relative or root paths. For a hosted font such as Google Fonts, download the files and use `@font-face`; Google only serves `woff2` to a browser user agent. If a font is licensed, tell the person it will be publicly hosted. Add up media sizes before downloading, take only what the design shows, and compress video.\n- **One screen of a routed app:** a router reads the page's address, which won't match the screen you want. Write a small entry file that mounts that screen with a memory or hash router, instead of trying to reach its route.\n\n## Limits\n\n100 MB per frame, 25 MB per file, 5000 files per frame, 500 frames per canvas. A URL can't be added, not even `localhost`: build it or `capture` it. Refused files: `.env`, credentials, keys, databases, source maps, server code, `node_modules` and project files such as `package.json`. Pop-ups, camera and microphone are off.\n\n## Steps\n\n1. **Start the canvas.** From the project root, or in a monorepo from the app's own folder so `.twombly/work` can find its packages: `twombly init --name \"Notification Explorations\" --no-open`. Pick a short, specific name. Run it once per canvas. The result's `local` field is the canvas's address. If `created` is false, the canvas already existed: see [Other agents and more canvases](#other-agents-and-more-canvases).\n2. **Open it beside the chat.** The person should see the canvas next to this conversation, in the app they're already using. Open the `local` address in that app's built-in browser: the Claude desktop app's browser pane, the ChatGPT app's in-app browser, or the built-in browser in Cursor, VS Code or any other editor. Only if the app has none, run `twombly open`, which uses their default browser.\n3. **Build each version** as its own static folder, keeping work under `.twombly/work/<task>/<version>` so the person's source stays clean. Pick a `<task>` no other agent would, because a shared folder gets overwritten. Use the first approach below that works. The canvas tells people an agent is working: `init` and the slow commands keep that showing for three minutes. Run `twombly working` only before a longer gap.\n4. **Add it:** `twombly add <folder> --title \"V1 Compact\" --width 1280 --height 800 --level build`, saying which level below it is. Use the design's real size, such as `--width 390 --height 844` for a phone. Use `--height auto` to fit a tall page.\n5. **Read the output.** If `ok` is false, fix every item in `problems` and run `add` again. Each one includes a `fix`. The frame isn't placed until it passes.\n6. **Look at it.** The result has a `screenshot` path. Open it and check the frame shows the design you meant, with the content in the right state. The gates can't tell if a page came out unstyled or on the wrong screen; a common miss is Tailwind classes that were never generated. Tailwind 4 skips gitignored folders, so source copied into `.twombly` needs an `@source`.\n7. **Test interactions, if they matter.** Open the `url` from `twombly preview <frame>` in your browser tool and use the page inside its iframe. Frame URLs refuse to open on their own. Tell the person what you didn't test.\n8. **Finish:** run `twombly done` so the background goes still, then tell the person the canvas is open. They click Share when they're ready. The first time, Google sign-in opens in their regular browser, and sharing continues on its own once they finish.\n\n## Choosing how to build a frame\n\nUse the first one that fits. Say so if you end up lower than the first, because the frame is less faithful. **If the app has server routes, auth or a database, skip straight to `component`**: a static export of that kind of app rarely works and isn't worth the attempt.\n\n1. `--level build`: **a real static build.** Vite: `vite build --base ./ --outDir .twombly/work/v1`. Next.js: `output: 'export'` in a temporary config (static export can't use server features). SvelteKit: `adapter-static` with a `fallback`, `ssr = false` in the root layout, and any server `load` swapped for mock data in a universal `+layout.ts`. Astro: `astro build`. Plain HTML: use the folder as is. To show one state of a bigger app, add a build entry that renders just that screen.\n2. `--level component`: **the component on its own.** Write a tiny entry file that imports the real component with mock props, and bundle it with Vite or esbuild into a folder. This is the usual answer when the app needs a backend. It keeps the real code and styles.\n3. `--level capture`: **freeze what's on screen.** With the app already running: `twombly capture http://localhost:3000/page --out .twombly/work/v1 --width 1280 --height 800`, then add that folder. It looks right but no longer responds, because scripts are removed.\n4. `--level image`: **a picture**, as a last resort. `twombly capture <url> --out <folder> --image`, or `twombly add shot.png --title \"V1\"`.\n\nSeveral versions can share one build: read the variant from `location.search` and pass `--entry \"index.html?variant=2\"`. Shared files are only uploaded once.\n\nAdd several frames in one command, which is faster, instead of writing a loop. Nothing is placed unless all of them pass. Each item takes `folder`, `title`, `entry`, `width`, `height`, `level`, `newRow`, `x` and `y`, and flags on the command apply to all:\n\n```sh\ntwombly add out --width 1280 --height 800 --level component --frames '[{\"title\":\"A\",\"entry\":\"index.html?variant=a\"},{\"title\":\"B\",\"entry\":\"index.html?variant=b\"}]'\n```\n\n### Example: the component level with Vite\n\nFor most projects, this is all it takes. It works the same for React, Svelte or Vue with that framework's Vite plugin. Keep it in your own folder under `.twombly/work`; here, `composer`.\n\n```js\n// .twombly/work/composer/vite.config.mjs\nimport { defineConfig } from 'vite';\nimport { resolve } from 'node:path';\nimport react from '@vitejs/plugin-react';\n\nconst root = resolve(import.meta.dirname, '../../..');\nexport default defineConfig({\n root: import.meta.dirname,\n base: './',\n plugins: [react()],\n resolve: { alias: { '@': root } }, // match the project's path aliases\n define: { 'process.env': {} },\n build: { outDir: 'out', emptyOutDir: true, sourcemap: false },\n});\n```\n\n```html\n<!-- .twombly/work/composer/index.html -->\n<!doctype html><html><body><div id=\"root\"></div><script type=\"module\" src=\"./main.jsx\"></script></body></html>\n```\n\n```jsx\n// .twombly/work/composer/main.jsx: the real component, the real styles, invented data.\nimport { createRoot } from 'react-dom/client';\nimport '@/styles/globals.css';\nimport { Composer } from '@/components/composer';\n\nconst variant = new URLSearchParams(location.search).get('variant');\ncreateRoot(document.getElementById('root')).render(<Composer variant={variant} onSend={() => {}} messages={MOCK_MESSAGES} />);\n```\n\nThen: `npx vite build --config .twombly/work/composer/vite.config.mjs` and `twombly add .twombly/work/composer/out --title \"A\" --entry \"index.html?variant=a\" --level component`. If the project uses Tailwind, add its Vite or PostCSS plugin to the config so the classes are generated.\n\n## Commands\n\n`twombly help` lists every command and flag. `<frame>` is a frame's id or its exact title. Frames fill in left to right; use `--new-row` to start a new group, for example one row per direction.\n\nUse `update` to change a version. Don't remove and re-add it, because that loses its comments. It keeps the frame's entry and size unless you change them. Each frame keeps its own copy of its files, so after rebuilding, update the set together: `twombly update out --frames '[{\"frame\":\"A\"},{\"frame\":\"B\"}]'`. Nothing changes unless all of them pass.\n\n## What the person is looking at\n\nWhen the person says \"this\" or \"these\" without naming a frame, run `twombly context`. It returns the frames they have open fullscreen, are using, or have selected, in that order, plus any comment they have open. `looking: false` means no canvas is open in a browser. If nothing is selected, ask which frame they mean.\n\n`twombly comments` returns every comment, with the frame it's on, where on the frame (in the frame's pixels), whether it's resolved, and `mine: true` for the person's own comments.\n\nEach frame in `context` and `list` has a `source`: the folder or file it was last added or updated from. Edit what built it, rebuild, then `update` the frame. Frames added before this existed show `null`.\n\n## Other agents and more canvases\n\nA folder has one default canvas, and every agent in that folder or below it lands on it. `init` on an existing canvas joins it and keeps its name. A folder can also hold any number of named canvases, each with its own address, frames and link.\n\n**Use a named canvas** when your work is a separate exploration, or the person asks for its own canvas: `twombly init --canvas \"Pricing\" --no-open`. Then pass `--canvas \"Pricing\"` on every command, because nothing remembers it between commands. `init --canvas` with a name that exists joins it, so agents can share one by agreeing on the name. `twombly canvases` lists what's here.\n\n**On a canvas with other agents:**\n\n- Only change your own frames, unless the person asks. Any agent's `update`, `move` and `remove` work on any frame, and `arrange` moves all of them.\n- Give frames titles no one else would use, such as \"Pricing: V1\", since commands find frames by title. A repeated title gets a warning, and then has to be addressed by id.\n- Add each set in one `--frames` command, with `--new-row` on its first frame, so it stays together.\n- If `update` says someone else changed a frame, run `twombly list` and don't retry without checking whose work you'd replace.\n- Run `twombly done` when you finish. The canvas keeps showing work until every agent that ran `init` has finished, or three minutes after the last activity.\n\n**Worktrees:** a canvas lives in the folder it was started in. A worktree outside the repo gets its own canvas, which is deleted with the worktree, so tell the person that before removing it. A worktree inside the repo joins the repo's default canvas if there is one.\n\n## Sharing\n\nThe person shares by clicking Share. Only run `twombly share` if they explicitly ask you to, because it publishes the canvas to a public link. If they ask you to take it down, run `twombly unshare`. That deletes the link and keeps the canvas on this machine, comments included.\n\nOnce it's shared, every `add`, `update`, `move` and `remove` goes live immediately. Check your work with `twombly check` before updating a canvas people are looking at.\n";
|
|
333
|
+
var SKILL_default = "---\nname: twombly\ndescription: Lay out design explorations on a shared canvas. Use when asked to \"put this on Twombly\", \"put these on a canvas\", \"lay out these versions\", \"show me all the variations side by side\", or to share explorations with real-time cursors and comments. Works with any framework in any repo.\n---\n\n# Twombly\n\nTwombly shows design work as live frames on an infinite canvas. You build each version, turn it into a folder of static files, and add it. The canvas opens on `localhost`; `init` returns its address. Nothing is online until the person clicks Share. After that the local canvas and the link are one state: everything you add, update or move appears for everyone straight away.\n\nRun every command as `npx -y twombly@latest <command>`; this guide shortens that to `twombly <command>`. Output is JSON.\n\n## The rule that decides everything\n\n**A frame is a folder of static files that runs with no server and no network.** It can use anything in its own folder and nothing else. The browser enforces this: outside requests, form posts and server code all fail, locally and once shared.\n\nThe person's repo may have a database, an API, auth and environment keys. None of that goes in a frame. Your job is to adapt the design so it works without them:\n\n- **Data and APIs:** replace with realistic mock data, written inline or as JSON in the folder. For public CMS content, save a snapshot as JSON when you build.\n- **Auth and providers:** skip login, and mount the design with mock providers and a fake signed-in user.\n- **Environment values:** never include them. Build with none set. Don't copy `.env` files. If the build needs a value to compile, use an obviously fake one. **Never run or read a secret manager such as Infisical or Doppler for this, even if the project is set up for it.** If you can't get what you need without one, ask the person.\n- **Analytics and widgets:** remove tag managers, analytics, chat widgets and similar scripts, including any in page templates such as `app.html` or `_document`.\n- **Real customer data:** never copy it from a database, fixture or log. Invent names, emails and numbers. The gates can't detect this, so it's on you. Links are public.\n- **Fonts, images and video:** copy the files into the folder with relative or root paths. For a hosted font such as Google Fonts, download the files and use `@font-face`; Google only serves `woff2` to a browser user agent. If a font is licensed, tell the person it will be publicly hosted. Add up media sizes before downloading, take only what the design shows, and compress video.\n- **One screen of a routed app:** a router reads the page's address, which won't match the screen you want. Write a small entry file that mounts that screen with a memory or hash router, instead of trying to reach its route.\n\n## Limits\n\n100 MB per frame, 25 MB per file, 5000 files per frame, 500 frames per canvas. A URL can't be added, not even `localhost`: build it or `capture` it. Refused files: `.env`, credentials, keys, databases, source maps, server code, `node_modules` and project files such as `package.json`. Pop-ups, camera and microphone are off.\n\n## Steps\n\n1. **Start the canvas.** From the project root, or in a monorepo from the app's own folder so `.twombly/work` can find its packages: `twombly init --name \"Notification Explorations\" --no-open`. Pick a short, specific name. Run it once per canvas. The result's `local` field is the canvas's address. If `created` is false, the canvas already existed: see [Other agents and more canvases](#other-agents-and-more-canvases).\n2. **Open it beside the chat.** The person should see the canvas next to this conversation, in the app they're already using. Open the `local` address in that app's built-in browser: the Claude desktop app's browser pane, the ChatGPT app's in-app browser, or the built-in browser in Cursor, VS Code or any other editor. Only if the app has none, run `twombly open`, which uses their default browser.\n3. **Build each version** as its own static folder, keeping work under `.twombly/work/<task>/<version>` so the person's source stays clean. Pick a `<task>` no other agent would, because a shared folder gets overwritten. Use the first approach below that works. The canvas tells people an agent is working: `init` and the slow commands keep that showing for three minutes. Run `twombly working` only before a longer gap.\n4. **Add it:** `twombly add <folder> --title \"V1 Compact\" --width 1280 --height 800 --level build`, saying which level below it is. Use the design's real size, such as `--width 390 --height 844` for a phone. Use `--height auto` to fit a tall page.\n5. **Read the output.** If `ok` is false, fix every item in `problems` and run `add` again. Each one includes a `fix`. The frame isn't placed until it passes.\n6. **Look at it.** The result has a `screenshot` path. Open it and check the frame shows the design you meant, with the content in the right state. The gates can't tell if a page came out unstyled or on the wrong screen; a common miss is Tailwind classes that were never generated. Tailwind 4 skips gitignored folders, so source copied into `.twombly` needs an `@source`.\n7. **Test interactions, if they matter.** Open the `url` from `twombly preview <frame>` in your browser tool and use the page inside its iframe. Frame URLs refuse to open on their own. Tell the person what you didn't test.\n8. **Finish:** run `twombly done` so the background goes still, then tell the person the canvas is open. They click Share when they're ready. The first time, Google sign-in opens in their regular browser, and sharing continues on its own once they finish.\n\n## Choosing how to build a frame\n\nUse the first one that fits. Say so if you end up lower than the first, because the frame is less faithful. **If the app has server routes, auth or a database, skip straight to `component`**: a static export of that kind of app rarely works and isn't worth the attempt.\n\n1. `--level build`: **a real static build.** Vite: `vite build --base ./ --outDir .twombly/work/v1`. Next.js: `output: 'export'` in a temporary config (static export can't use server features). SvelteKit: `adapter-static` with a `fallback`, `ssr = false` in the root layout, and any server `load` swapped for mock data in a universal `+layout.ts`. Astro: `astro build`. Plain HTML: use the folder as is. To show one state of a bigger app, add a build entry that renders just that screen.\n2. `--level component`: **the component on its own.** Write a tiny entry file that imports the real component with mock props, and bundle it with Vite or esbuild into a folder. This is the usual answer when the app needs a backend. It keeps the real code and styles.\n3. `--level capture`: **freeze what's on screen.** With the app already running: `twombly capture http://localhost:3000/page --out .twombly/work/v1 --width 1280 --height 800`, then add that folder. It looks right but no longer responds, because scripts are removed.\n4. `--level image`: **a picture**, as a last resort. `twombly capture <url> --out <folder> --image`, or `twombly add shot.png --title \"V1\"`.\n\nSeveral versions can share one build: read the variant from `location.search` and pass `--entry \"index.html?variant=2\"`. Shared files are only uploaded once.\n\nAdd several frames in one command, which is faster, instead of writing a loop. Nothing is placed unless all of them pass. Each item takes `folder`, `title`, `entry`, `width`, `height`, `level`, `newRow`, `x` and `y`, and flags on the command apply to all:\n\n```sh\ntwombly add out --width 1280 --height 800 --level component --frames '[{\"title\":\"A\",\"entry\":\"index.html?variant=a\"},{\"title\":\"B\",\"entry\":\"index.html?variant=b\"}]'\n```\n\n### Frames share the canvas\n\nMany frames run at once in one browser thread, so a heavy frame slows every other one. Build whatever the work calls for. These change how it runs, never how it looks:\n\n- **Rest when unseen.** Pause animation loops, video and timers when the page is hidden (`visibilitychange`) or off screen (`IntersectionObserver`), and resume where they left off.\n- **Per-pixel work belongs on the GPU.** Noise, shaders and simulations that compute every pixel belong in WebGL or WebGPU, not JavaScript loops with `putImageData`, when the result looks the same.\n- **Load lightly.** Generate heavy data on demand rather than shipping it in the first script.\n\n### Example: the component level with Vite\n\nFor most projects, this is all it takes. It works the same for React, Svelte or Vue with that framework's Vite plugin. Keep it in your own folder under `.twombly/work`; here, `composer`.\n\n```js\n// .twombly/work/composer/vite.config.mjs\nimport { defineConfig } from 'vite';\nimport { resolve } from 'node:path';\nimport react from '@vitejs/plugin-react';\n\nconst root = resolve(import.meta.dirname, '../../..');\nexport default defineConfig({\n root: import.meta.dirname,\n base: './',\n plugins: [react()],\n resolve: { alias: { '@': root } }, // match the project's path aliases\n define: { 'process.env': {} },\n build: { outDir: 'out', emptyOutDir: true, sourcemap: false },\n});\n```\n\n```html\n<!-- .twombly/work/composer/index.html -->\n<!doctype html><html><body><div id=\"root\"></div><script type=\"module\" src=\"./main.jsx\"></script></body></html>\n```\n\n```jsx\n// .twombly/work/composer/main.jsx: the real component, the real styles, invented data.\nimport { createRoot } from 'react-dom/client';\nimport '@/styles/globals.css';\nimport { Composer } from '@/components/composer';\n\nconst variant = new URLSearchParams(location.search).get('variant');\ncreateRoot(document.getElementById('root')).render(<Composer variant={variant} onSend={() => {}} messages={MOCK_MESSAGES} />);\n```\n\nThen: `npx vite build --config .twombly/work/composer/vite.config.mjs` and `twombly add .twombly/work/composer/out --title \"A\" --entry \"index.html?variant=a\" --level component`. If the project uses Tailwind, add its Vite or PostCSS plugin to the config so the classes are generated.\n\n## Commands\n\n`twombly help` lists every command and flag. `<frame>` is a frame's id or its exact title. Frames fill in left to right; use `--new-row` to start a new group, for example one row per direction.\n\nUse `update` to change a version. Don't remove and re-add it, because that loses its comments. It keeps the frame's entry and size unless you change them. Each frame keeps its own copy of its files, so after rebuilding, update the set together: `twombly update out --frames '[{\"frame\":\"A\"},{\"frame\":\"B\"}]'`. Nothing changes unless all of them pass.\n\n## What the person is looking at\n\nWhen the person says \"this\" or \"these\" without naming a frame, run `twombly context`. It returns the frames they have open fullscreen, are using, or have selected, in that order, plus any comment they have open. `looking: false` means no canvas is open in a browser. If nothing is selected, ask which frame they mean.\n\n`twombly comments` returns every comment, with the frame it's on, where on the frame (in the frame's pixels), whether it's resolved, and `mine: true` for the person's own comments.\n\nEach frame in `context` and `list` has a `source`: the folder or file it was last added or updated from. Edit what built it, rebuild, then `update` the frame. Frames added before this existed show `null`.\n\n## Other agents and more canvases\n\nA folder has one default canvas, and every agent in that folder or below it lands on it. `init` on an existing canvas joins it and keeps its name. A folder can also hold any number of named canvases, each with its own address, frames and link.\n\n**Use a named canvas** when your work is a separate exploration, or the person asks for its own canvas: `twombly init --canvas \"Pricing\" --no-open`. Then pass `--canvas \"Pricing\"` on every command, because nothing remembers it between commands. `init --canvas` with a name that exists joins it, so agents can share one by agreeing on the name. `twombly canvases` lists what's here.\n\n**On a canvas with other agents:**\n\n- Only change your own frames, unless the person asks. Any agent's `update`, `move` and `remove` work on any frame, and `arrange` moves all of them.\n- Give frames titles no one else would use, such as \"Pricing: V1\", since commands find frames by title. A repeated title gets a warning, and then has to be addressed by id.\n- Add each set in one `--frames` command, with `--new-row` on its first frame, so it stays together.\n- If `update` says someone else changed a frame, run `twombly list` and don't retry without checking whose work you'd replace.\n- Run `twombly done` when you finish. The canvas keeps showing work until every agent that ran `init` has finished, or three minutes after the last activity.\n\n**Worktrees:** a canvas lives in the folder it was started in. A worktree outside the repo gets its own canvas, which is deleted with the worktree, so tell the person that before removing it. A worktree inside the repo joins the repo's default canvas if there is one.\n\n## Sharing\n\nThe person shares by clicking Share. Only run `twombly share` if they explicitly ask you to, because it publishes the canvas to a public link. If they ask you to take it down, run `twombly unshare`. That deletes the link and keeps the canvas on this machine, comments included.\n\nOnce it's shared, every `add`, `update`, `move` and `remove` goes live immediately. Check your work with `twombly check` before updating a canvas people are looking at.\n";
|
|
277
334
|
|
|
278
335
|
// src/capture.ts
|
|
279
336
|
import { createHash } from "node:crypto";
|
|
@@ -605,7 +662,7 @@ var checkedAt = 0;
|
|
|
605
662
|
function interfaceFolders() {
|
|
606
663
|
if (OVERRIDE) return [OVERRIDE];
|
|
607
664
|
const folder = (released) => join5(STORE, String(released));
|
|
608
|
-
return [...saved.filter((released) => released >
|
|
665
|
+
return [...saved.filter((released) => released > 1793984913409).map(folder), BUNDLED, ...saved.filter((released) => released <= 1793984913409).map(folder)];
|
|
609
666
|
}
|
|
610
667
|
function unpack(body) {
|
|
611
668
|
const { payload, signature } = JSON.parse(body);
|
|
@@ -639,7 +696,7 @@ async function check() {
|
|
|
639
696
|
const release = unpack(body);
|
|
640
697
|
if (!release) return;
|
|
641
698
|
etag = response.headers.get("etag") || "";
|
|
642
|
-
if (release.released > Math.max(saved[0] ?? 0,
|
|
699
|
+
if (release.released > Math.max(saved[0] ?? 0, 1793984913409)) await install(release.released, release.files);
|
|
643
700
|
}
|
|
644
701
|
function startCheck() {
|
|
645
702
|
if (checking) return;
|
|
@@ -688,6 +745,25 @@ function inOrder() {
|
|
|
688
745
|
return next;
|
|
689
746
|
};
|
|
690
747
|
}
|
|
748
|
+
function atMost(size2) {
|
|
749
|
+
let active = 0;
|
|
750
|
+
const waiting = [];
|
|
751
|
+
return async (work) => {
|
|
752
|
+
if (active < size2) active++;
|
|
753
|
+
else await new Promise((go) => waiting.push(go));
|
|
754
|
+
try {
|
|
755
|
+
return await work();
|
|
756
|
+
} finally {
|
|
757
|
+
const next = waiting.shift();
|
|
758
|
+
if (next) next();
|
|
759
|
+
else active--;
|
|
760
|
+
}
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
async function settled(work) {
|
|
764
|
+
const failed = (await Promise.allSettled(work)).find((result) => result.status === "rejected");
|
|
765
|
+
if (failed) throw failed.reason;
|
|
766
|
+
}
|
|
691
767
|
var NAMED = "canvases";
|
|
692
768
|
function canvasSlug(name) {
|
|
693
769
|
const slug = name.normalize("NFKD").replace(/[̀-ͯ]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60);
|
|
@@ -856,7 +932,7 @@ async function flush() {
|
|
|
856
932
|
save();
|
|
857
933
|
if (!usageOn() || !Object.values(counts).some(Boolean)) return;
|
|
858
934
|
const canvases2 = Object.keys(await readRegistry()).length;
|
|
859
|
-
await post("/api/usage", { install: state.install, day, version: "0.4.
|
|
935
|
+
await post("/api/usage", { install: state.install, day, version: "0.4.4", platform: process.platform, counts: { ...counts, canvases: canvases2 } }).catch(() => {
|
|
860
936
|
});
|
|
861
937
|
}
|
|
862
938
|
async function startUsage() {
|
|
@@ -874,7 +950,7 @@ function reportError(error, source = "tool") {
|
|
|
874
950
|
source,
|
|
875
951
|
mode: "local",
|
|
876
952
|
install: state?.install,
|
|
877
|
-
version: "0.4.
|
|
953
|
+
version: "0.4.4",
|
|
878
954
|
type: String(error.type || "Error").slice(0, 80),
|
|
879
955
|
message: scrub(String(error.message), 500),
|
|
880
956
|
stack: scrub(String(error.stack || ""), 4e3)
|
|
@@ -944,6 +1020,19 @@ function syncSettings() {
|
|
|
944
1020
|
});
|
|
945
1021
|
}
|
|
946
1022
|
var STROKE_UPLOAD = 40;
|
|
1023
|
+
var FILES_AT_ONCE = 16;
|
|
1024
|
+
var FRAMES_PLACED_AT_ONCE = 4;
|
|
1025
|
+
var BUSY_WAIT_MS = Number(process.env.TWOMBLY_BUSY_WAIT_MS) || 1e4;
|
|
1026
|
+
async function patiently(work) {
|
|
1027
|
+
for (let tries = 1; ; tries++) {
|
|
1028
|
+
try {
|
|
1029
|
+
return await work();
|
|
1030
|
+
} catch (error) {
|
|
1031
|
+
if (error?.status !== 429 || tries >= 12) throw error;
|
|
1032
|
+
await new Promise((done) => setTimeout(done, BUSY_WAIT_MS));
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
947
1036
|
var THREAD_UPLOAD_CHARS = 5e5;
|
|
948
1037
|
var THREAD_ID = /^[a-z0-9]{8,32}$/;
|
|
949
1038
|
var finite = (value) => typeof value === "number" && Number.isFinite(value);
|
|
@@ -1267,33 +1356,41 @@ var Canvas = class {
|
|
|
1267
1356
|
}));
|
|
1268
1357
|
await this.saveNow();
|
|
1269
1358
|
}
|
|
1270
|
-
/**
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
const queue = files.filter((file2, index) => wanted.has(file2.hash) && files.findIndex((other) => other.hash === file2.hash) === index);
|
|
1282
|
-
const put = async (hash, bytes) => {
|
|
1283
|
-
await edge(`/api/canvases/${remoteId}/blobs/${hash}`, { method: "PUT", body: bytes });
|
|
1284
|
-
};
|
|
1285
|
-
let next = 0;
|
|
1286
|
-
await Promise.all(Array.from({ length: 6 }, async () => {
|
|
1287
|
-
while (next < queue.length) {
|
|
1288
|
-
const file2 = queue[next++];
|
|
1289
|
-
await put(file2.hash, await readFile6(file2.absolute));
|
|
1359
|
+
/**
|
|
1360
|
+
* Sends frames' files up, each file once and skipping what's already there, and places each frame in the room as soon as its files have arrived.
|
|
1361
|
+
* A canvas made a moment ago has nothing yet, so there's no need to ask.
|
|
1362
|
+
*/
|
|
1363
|
+
async uploadFrames(remoteId, frames, fresh = false, onFrame) {
|
|
1364
|
+
const prepared = await Promise.all(frames.map(async (frame) => {
|
|
1365
|
+
const { files } = await folderContent(frameDir(this.project, frame.id));
|
|
1366
|
+
let poster = null, posterBytes = null;
|
|
1367
|
+
if (frame.hasPoster) {
|
|
1368
|
+
posterBytes = await readFile6(posterFile(this.project, frame.id)).catch(() => null);
|
|
1369
|
+
if (posterBytes) poster = createHash3("sha256").update(posterBytes).digest("hex");
|
|
1290
1370
|
}
|
|
1371
|
+
const manifest = files.map(({ path, hash, size: size2 }) => ({ path, hash, size: size2 }));
|
|
1372
|
+
const missing = fresh ? [...files.map((file2) => file2.hash), ...poster ? [poster] : []] : (await edge(`/api/canvases/${remoteId}/uploads`, { method: "POST", body: JSON.stringify({ files: manifest, poster }) })).missing;
|
|
1373
|
+
return { frame, files, manifest, poster, posterBytes, missing: new Set(missing) };
|
|
1374
|
+
}));
|
|
1375
|
+
const fileTurn = atMost(FILES_AT_ONCE), placeTurn = atMost(FRAMES_PLACED_AT_ONCE);
|
|
1376
|
+
const sent = /* @__PURE__ */ new Map();
|
|
1377
|
+
const send = (hash, read) => {
|
|
1378
|
+
if (!sent.has(hash)) sent.set(hash, fileTurn(async () => {
|
|
1379
|
+
const body = await read();
|
|
1380
|
+
await patiently(() => edge(`/api/canvases/${remoteId}/blobs/${hash}`, { method: "PUT", body }));
|
|
1381
|
+
}));
|
|
1382
|
+
return sent.get(hash);
|
|
1383
|
+
};
|
|
1384
|
+
const arrivals = prepared.map(({ files, poster, posterBytes, missing }) => [
|
|
1385
|
+
...files.filter((file2) => missing.has(file2.hash)).map((file2) => send(file2.hash, () => readFile6(file2.absolute))),
|
|
1386
|
+
...poster && posterBytes && missing.has(poster) ? [send(poster, async () => posterBytes)] : []
|
|
1387
|
+
]);
|
|
1388
|
+
await settled(prepared.map(async ({ frame, manifest, poster }, index) => {
|
|
1389
|
+
await settled(arrivals[index]);
|
|
1390
|
+
const { id, title, x, y, w, h, z, entry, level: level2 } = frame;
|
|
1391
|
+
await placeTurn(() => patiently(() => edge(`/api/canvases/${remoteId}/frames`, { method: "POST", body: JSON.stringify({ id, title, x, y, w, h, z, entry, level: level2, files: manifest, poster }) })));
|
|
1392
|
+
onFrame?.();
|
|
1291
1393
|
}));
|
|
1292
|
-
if (poster && posterBytes && wanted.has(poster)) await put(poster, posterBytes);
|
|
1293
|
-
onFile?.();
|
|
1294
|
-
const { id, title, x, y, w, h, z, entry, level: level2 } = frame;
|
|
1295
|
-
const result = await edge(`/api/canvases/${remoteId}/frames`, { method: "POST", body: JSON.stringify({ id, title, x, y, w, h, z, entry, level: level2, files: manifest, poster }) });
|
|
1296
|
-
return result.frame;
|
|
1297
1394
|
}
|
|
1298
1395
|
async share() {
|
|
1299
1396
|
if (this.data.remote) return this.data.remote.url;
|
|
@@ -1302,16 +1399,13 @@ var Canvas = class {
|
|
|
1302
1399
|
const total = this.data.frames.length;
|
|
1303
1400
|
let done = 0;
|
|
1304
1401
|
this.event({ t: "share-progress", done, total });
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
});
|
|
1310
|
-
}
|
|
1311
|
-
const strokes = this.data.strokes ?? [];
|
|
1312
|
-
for (let i = 0; i < strokes.length; i += STROKE_UPLOAD) {
|
|
1313
|
-
await edge(`/api/canvases/${created.id}/strokes`, { method: "POST", body: JSON.stringify({ strokes: strokes.slice(i, i + STROKE_UPLOAD) }) });
|
|
1402
|
+
const strokes = async () => {
|
|
1403
|
+
const strokes2 = this.data.strokes ?? [];
|
|
1404
|
+
for (let i = 0; i < strokes2.length; i += STROKE_UPLOAD) {
|
|
1405
|
+
await edge(`/api/canvases/${created.id}/strokes`, { method: "POST", body: JSON.stringify({ strokes: strokes2.slice(i, i + STROKE_UPLOAD) }) });
|
|
1314
1406
|
}
|
|
1407
|
+
};
|
|
1408
|
+
const threads = async () => {
|
|
1315
1409
|
let batch = [], chars = 0;
|
|
1316
1410
|
const send = async () => {
|
|
1317
1411
|
if (batch.length) await edge(`/api/canvases/${created.id}/threads`, { method: "POST", body: JSON.stringify({ threads: batch }) });
|
|
@@ -1325,6 +1419,15 @@ var Canvas = class {
|
|
|
1325
1419
|
chars += size2;
|
|
1326
1420
|
}
|
|
1327
1421
|
await send();
|
|
1422
|
+
};
|
|
1423
|
+
try {
|
|
1424
|
+
await settled([
|
|
1425
|
+
this.uploadFrames(created.id, this.data.frames, true, () => {
|
|
1426
|
+
this.event({ t: "share-progress", done: ++done, total });
|
|
1427
|
+
}),
|
|
1428
|
+
strokes(),
|
|
1429
|
+
threads()
|
|
1430
|
+
]);
|
|
1328
1431
|
} catch (error) {
|
|
1329
1432
|
await edge(`/api/canvases/${created.id}`, { method: "DELETE" }).catch(() => {
|
|
1330
1433
|
});
|
|
@@ -1581,7 +1684,7 @@ async function place(canvas, input) {
|
|
|
1581
1684
|
updatedAt: Date.now(),
|
|
1582
1685
|
source: typeof item.source === "string" ? item.source : void 0
|
|
1583
1686
|
};
|
|
1584
|
-
if (canvas.data.remote) await canvas.
|
|
1687
|
+
if (canvas.data.remote) await canvas.uploadFrames(canvas.data.remote.id, [frame]);
|
|
1585
1688
|
canvas.data.frames.push(frame);
|
|
1586
1689
|
await canvas.saveNow();
|
|
1587
1690
|
await canvas.announce(frame);
|
|
@@ -1609,7 +1712,7 @@ async function revise(canvas, input) {
|
|
|
1609
1712
|
if (typeof item.source === "string") next.source = item.source;
|
|
1610
1713
|
next.content = await install2(canvas, item.id, item.folder, item.screenshot);
|
|
1611
1714
|
if (item.screenshot) next.hasPoster = true;
|
|
1612
|
-
if (canvas.data.remote) await canvas.
|
|
1715
|
+
if (canvas.data.remote) await canvas.uploadFrames(canvas.data.remote.id, [next]);
|
|
1613
1716
|
Object.assign(frame, next);
|
|
1614
1717
|
await canvas.saveNow();
|
|
1615
1718
|
await canvas.announce(frame);
|
|
@@ -1703,7 +1806,7 @@ async function servePreview(response, canvasId, frameId) {
|
|
|
1703
1806
|
async function route(request, response) {
|
|
1704
1807
|
const url = new URL(request.url || "/", DAEMON_ORIGIN);
|
|
1705
1808
|
const path = url.pathname, method = request.method || "GET";
|
|
1706
|
-
if (path === "/health") return reply(response, 200, { ok: true, build: "0.4.
|
|
1809
|
+
if (path === "/health") return reply(response, 200, { ok: true, build: "0.4.4+1793984913409", api: TOOL_API, pid: process.pid });
|
|
1707
1810
|
if (path.startsWith("/cli/") && method === "POST") return reply(response, 200, await command(path.slice(5), await readBody(request)));
|
|
1708
1811
|
if (path === "/auth/callback" && method === "GET") {
|
|
1709
1812
|
const code = url.searchParams.get("code"), state2 = url.searchParams.get("state");
|
|
@@ -2034,7 +2137,7 @@ async function runGates(folder, options) {
|
|
|
2034
2137
|
}
|
|
2035
2138
|
|
|
2036
2139
|
// src/cli.ts
|
|
2037
|
-
var HELP = `twombly ${"0.4.
|
|
2140
|
+
var HELP = `twombly ${"0.4.4"}: put design explorations on a shared canvas.
|
|
2038
2141
|
|
|
2039
2142
|
init [--name "Name"] [--no-open] create or join this folder's canvas
|
|
2040
2143
|
init --canvas "Name" [--no-open] create or join a separate named canvas in this folder
|
|
@@ -2136,12 +2239,12 @@ async function health() {
|
|
|
2136
2239
|
var builtAt = (build) => Number(build.split("+")[1]) || 0;
|
|
2137
2240
|
var sleep = (ms) => new Promise((done) => setTimeout(done, ms));
|
|
2138
2241
|
var IDLE_WAIT_MS = 6e4;
|
|
2139
|
-
var usable = (running) => !!running && (running.build === "0.4.
|
|
2242
|
+
var usable = (running) => !!running && (running.build === "0.4.4+1793984913409" || running.api === TOOL_API && builtAt(running.build) > builtAt("0.4.4+1793984913409"));
|
|
2140
2243
|
async function ensureDaemon() {
|
|
2141
2244
|
const running = await health();
|
|
2142
2245
|
if (usable(running)) return;
|
|
2143
2246
|
if (running) {
|
|
2144
|
-
if (builtAt(running.build) > builtAt("0.4.
|
|
2247
|
+
if (builtAt(running.build) > builtAt("0.4.4+1793984913409")) fail("A newer twombly is running on this machine. Run this with twombly@latest.");
|
|
2145
2248
|
let stopped = false;
|
|
2146
2249
|
for (const end = Date.now() + IDLE_WAIT_MS; !stopped && Date.now() < end; ) {
|
|
2147
2250
|
const answer = await fetch(`${DAEMON_ORIGIN}/cli/stop`, { method: "POST", body: '{"ifIdle":true}' }).then((response) => response.json()).catch(() => ({ stopped: true }));
|
|
@@ -2362,7 +2465,7 @@ async function main() {
|
|
|
2362
2465
|
return;
|
|
2363
2466
|
case "--version":
|
|
2364
2467
|
case "version":
|
|
2365
|
-
process.stdout.write("0.4.
|
|
2468
|
+
process.stdout.write("0.4.4\n");
|
|
2366
2469
|
return;
|
|
2367
2470
|
case "guide":
|
|
2368
2471
|
process.stdout.write(SKILL_default);
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/LandingStage-DlOW_YOx.js","assets/rolldown-runtime-WkwZ5CU-.js","assets/agentPrompt-Va8atsXW.js","assets/index-DgaQcoTH.js","assets/index-CJ7B-FU8.css","assets/LandingStage-BcivO6Eh.css"])))=>i.map(i=>d[i]);
|
|
2
|
+
import{n as e}from"./rolldown-runtime-WkwZ5CU-.js";import{Ct as t,D as n,E as r,N as i,Rt as a,S as o,T as s,i as c,j as l,o as u,p as d,s as f,t as p,u as m,w as h,wt as g,zt as _}from"./agentPrompt-Va8atsXW.js";import{n as v,r as y,t as b}from"./index-DgaQcoTH.js";var x=e(_(),1),S=i();function ee(){return(0,S.jsxs)(`span`,{className:`landing-marks`,"aria-hidden":`true`,children:[(0,S.jsx)(`svg`,{viewBox:`0 0 24 24`,fill:`currentColor`,children:(0,S.jsx)(`path`,{d:`m4.7144 15.9555 4.7174-2.6471.079-.2307-.079-.1275h-.2307l-.7893-.0486-2.6956-.0729-2.3375-.0971-2.2646-.1214-.5707-.1215-.5343-.7042.0546-.3522.4797-.3218.686.0608 1.5179.1032 2.2767.1578 1.6514.0972 2.4468.255h.3886l.0546-.1579-.1336-.0971-.1032-.0972L6.973 9.8356l-2.55-1.6879-1.3356-.9714-.7225-.4918-.3643-.4614-.1578-1.0078.6557-.7225.8803.0607.2246.0607.8925.686 1.9064 1.4754 2.4893 1.8336.3643.3035.1457-.1032.0182-.0728-.164-.2733-1.3539-2.4467-1.445-2.4893-.6435-1.032-.17-.6194c-.0607-.255-.1032-.4674-.1032-.7285L6.287.1335 6.6997 0l.9957.1336.419.3642.6192 1.4147 1.0018 2.2282 1.5543 3.0296.4553.8985.2429.8318.091.255h.1579v-.1457l.1275-1.706.2368-2.0947.2307-2.6957.0789-.7589.3764-.9107.7468-.4918.5828.2793.4797.686-.0668.4433-.2853 1.8517-.5586 2.9021-.3643 1.9429h.2125l.2429-.2429.9835-1.3053 1.6514-2.0643.7286-.8196.85-.9046.5464-.4311h1.0321l.759 1.1293-.34 1.1657-1.0625 1.3478-.8804 1.1414-1.2628 1.7-.7893 1.36.0729.1093.1882-.0183 2.8535-.607 1.5421-.2794 1.8396-.3157.8318.3886.091.3946-.3278.8075-1.967.4857-2.3072.4614-3.4364.8136-.0425.0304.0486.0607 1.5482.1457.6618.0364h1.621l3.0175.2247.7892.522.4736.6376-.079.4857-1.2142.6193-1.6393-.3886-3.825-.9107-1.3113-.3279h-.1822v.1093l1.0929 1.0686 2.0035 1.8092 2.5075 2.3314.1275.5768-.3218.4554-.34-.0486-2.2039-1.6575-.85-.7468-1.9246-1.621h-.1275v.17l.4432.6496 2.3436 3.5214.1214 1.0807-.17.3521-.6071.2125-.6679-.1214-1.3721-1.9246L14.38 17.959l-1.1414-1.9428-.1397.079-.674 7.2552-.3156.3703-.7286.2793-.6071-.4614-.3218-.7468.3218-1.4753.3886-1.9246.3157-1.53.2853-1.9004.17-.6314-.0121-.0425-.1397.0182-1.4328 1.9672-2.1796 2.9446-1.7243 1.8456-.4128.164-.7164-.3704.0667-.6618.4008-.5889 2.386-3.0357 1.4389-1.882.929-1.0868-.0062-.1579h-.0546l-6.3385 4.1164-1.1293.1457-.4857-.4554.0608-.7467.2307-.2429 1.9064-1.3114Z`})}),(0,S.jsx)(`svg`,{viewBox:`0 0 24 24`,fill:`currentColor`,children:(0,S.jsx)(`path`,{d:`M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z`})})]})}var C=`twombly:show-copied`,w=.41960295031576633,T=.15,E=.2765523188064277,D=.5783555327868779,O=-44*Math.PI/180,k={x:Math.sin(O),y:-Math.cos(O)},A=(e,t=0,n=1)=>Math.min(n,Math.max(t,e)),j=(e,t,n)=>({x:e.x+t.x*n,y:e.y+t.y*n}),M=e=>Math.hypot(e.x,e.y),N=e=>{let t=M(e);return t<.001?{x:1,y:0}:{x:e.x/t,y:e.y/t}},P=(e,t,n,r)=>({start:e,outgoing:t,incoming:n,end:r});function F(e,t){let n=1-t;return{x:n**3*e.start.x+3*n*n*t*e.outgoing.x+3*n*t*t*e.incoming.x+t**3*e.end.x,y:n**3*e.start.y+3*n*n*t*e.outgoing.y+3*n*t*t*e.incoming.y+t**3*e.end.y}}function te(e,t){let n=A(t),r=Math.min(e.curves.length-1,Math.floor(n*e.curves.length)),i=n===1?1:n*e.curves.length-r,a=e.curves[r],o=1-i,s={x:3*(o*o*(a.outgoing.x-a.start.x)+2*o*i*(a.incoming.x-a.outgoing.x)+i*i*(a.end.x-a.incoming.x))*e.curves.length,y:3*(o*o*(a.outgoing.y-a.start.y)+2*o*i*(a.incoming.y-a.outgoing.y)+i*i*(a.end.y-a.incoming.y))*e.curves.length},c=N(s),l=1-(1-A((n-.99)/.01))**3,u=N({x:c.x*(1-l)+k.x*l,y:c.y*(1-l)+k.y*l});return{point:F(a,i),derivative:s,heading:u}}function I(e,t){let n=e=>!t||e.x>=t.x+20&&e.x<t.x+t.width-20&&e.y>=t.y+20&&e.y<t.y+t.height-20,r=e[0].start,i,a=0,o=0,s=0,c=0,l=n(r);for(let t of e)for(let e=1;e<=24;e++){let u=F(t,e/24),d=u.x-r.x,f=u.y-r.y,p=Math.hypot(d,f);if(a+=p,l&&=n(u),p>.01){let e=Math.atan2(f,d);if(i!==void 0){let t=Math.atan2(Math.sin(e-i),Math.cos(e-i));o+=t*t,s+=Math.abs(t),c=Math.max(c,Math.abs(t))}i=e}r=u}return{length:a,turnEnergy:o,totalTurn:s,sharpestTurn:c,inBounds:l}}function L(e,t,n){let r={x:t.x-e.x,y:t.y-e.y},i=M(r),a=N(r),o=[];if(i<=10)o.push([P(e,j(e,r,1/3),j(e,r,2/3),t)]);else{let n=Math.min(i*w,i*.9,640),r=Math.min(i*T,i*.9,640),s=j(e,k,n),c=j(t,k,-r);o.push([P(e,s,c,t)]),o.push([P(e,j(e,k,n*.65),j(t,k,-r*.65),t)]);let l=k.x*a.y-k.y*a.x<0?-1:1,u={x:a.y*l,y:-a.x*l},d=j({x:(e.x+t.x)/2,y:(e.y+t.y)/2},k,n*.16),f=Math.min(i*E,520),p=Math.min(i*D,440);for(let n of[.55,.8,1.05])for(let r of[.65,1,1.35])for(let i of[1,-1]){let l=j(d,u,f*n*i);o.push([P(e,s,j(l,a,-p*r),l),P(l,j(l,a,p*r),c,t)])}}let s=A((-.08-k.x*a.x-k.y*a.y)/.92);return o.map(e=>{let t=I(e,n),r=Math.max(0,t.length/Math.max(1,i)-1),a=e.length>1,o=t.length+320*r+140*t.turnEnergy+180*t.sharpestTurn+18*t.totalTurn+(a?45:0)+90*s,c=A(.42*A(r/.55)+.38*A(t.totalTurn/(1.4*Math.PI))+.2*A(t.turnEnergy/1.25))*.12,l=A((a?.9:1)*.9*((a?.04:0)+.28*s+.22*A((t.length-180)/760)+.42+c),.12,2.2);return{curves:e,...t,score:o,response:l}})}function ne(e,t,n){let r=L(e,t,n),i=r.filter(e=>e.inBounds);return(i.length?i:r).reduce((e,t)=>t.score<e.score?t:e)}var R=(e=0)=>({value:e,velocity:0}),re=Math.atan2(k.y,k.x)*180/Math.PI,ie=1600;function z(e,t,n,r,i){let a=2*Math.PI/n,o=r*a,s=a*Math.sqrt(1-r*r),c=e.value-t,l=(e.velocity+o*c)/s,u=Math.cos(s*i),d=Math.sin(s*i),f=Math.exp(-o*i);e.value=t+f*(c*u+l*d),e.velocity=f*(-o*(c*u+l*d)+s*(-c*d+l*u))}function B(e,t){return e+Math.atan2(Math.sin((t-e)*Math.PI/180),Math.cos((t-e)*Math.PI/180))*180/Math.PI}var ae=class{position;velocity={x:0,y:0};kind=`idle`;target;path;pathScale=1;progress=R();horizontal;vertical;axis=R();baseRotation=R();lean=R();stretch=R();heading=0;constructor(e){this.position={...e},this.target={...e},this.horizontal=R(e.x),this.vertical=R(e.y)}moveTo(e,t,n=!1){if(!Number.isFinite(e.x)||!Number.isFinite(e.y))return;if(this.target={...e},n){this.jumpTo(e);return}this.pathScale=Math.max(.001,t.z);let r=Math.hypot(e.x-this.position.x,e.y-this.position.y)*this.pathScale;if(r<.01&&Math.hypot(this.velocity.x,this.velocity.y)*this.pathScale<12){this.jumpTo(e);return}if(this.horizontal={value:this.position.x,velocity:this.velocity.x},this.vertical={value:this.position.y,velocity:this.velocity.y},this.kind=r<=196?`scoot`:`curve`,this.kind===`scoot`){this.path=void 0;return}let i=e=>({x:e.x*this.pathScale,y:e.y*this.pathScale});this.path=ne(i(this.position),i(e),{x:-t.x,y:-t.y,width:t.width,height:t.height});let{derivative:a}=te(this.path,0),o=a.x**2+a.y**2,s=o?(this.velocity.x*a.x+this.velocity.y*a.y)*this.pathScale/o:0;this.progress={value:0,velocity:A(s,0,5/this.path.response)}}jumpTo(e){Object.assign(this.position,e),this.target={...e},this.velocity.x=this.velocity.y=0,this.horizontal=R(e.x),this.vertical=R(e.y),this.axis=R(),this.baseRotation=R(),this.lean=R(),this.stretch=R(),this.progress=R(),this.path=void 0,this.heading=0,this.kind=`idle`}translateBy(e,t){if(Number.isFinite(e)&&Number.isFinite(t)&&(e||t)&&(this.position.x+=e,this.position.y+=t,this.target.x+=e,this.target.y+=t,this.horizontal.value+=e,this.vertical.value+=t,this.path)){let n=n=>({x:n.x+e*this.pathScale,y:n.y+t*this.pathScale});this.path.curves=this.path.curves.map(e=>({start:n(e.start),outgoing:n(e.outgoing),incoming:n(e.incoming),end:n(e.end)}))}}advance(e,t=this.pathScale){if(!(e>0)||!Number.isFinite(e)||this.kind===`idle`&&this.deformationSettled())return!1;if(this.kind===`scoot`)z(this.horizontal,this.target.x,.24,.84,e),z(this.vertical,this.target.y,.24,.84,e),this.position.x=this.horizontal.value,this.position.y=this.vertical.value,this.velocity.x=this.horizontal.velocity,this.velocity.y=this.vertical.velocity;else if(this.kind===`curve`&&this.path){z(this.progress,1,this.path.response,.9,e);let{point:t,derivative:n,heading:r}=te(this.path,this.progress.value);this.position.x=t.x/this.pathScale,this.position.y=t.y/this.pathScale;let i=this.progress.value>=1?0:this.progress.velocity/this.pathScale;this.velocity.x=n.x*i,this.velocity.y=n.y*i,this.heading=B(this.heading,Math.atan2(r.y,r.x)*180/Math.PI-re)}let n=Math.hypot(this.velocity.x,this.velocity.y)*t,r=this.kind===`scoot`?A(n/ie):0,i=n>12?Math.atan2(this.velocity.y,this.velocity.x)*180/Math.PI:this.axis.value;z(this.axis,B(this.axis.value,i),.07,.82,e),z(this.baseRotation,this.kind===`curve`?this.heading:B(this.baseRotation.value,0),.09,.86,e),z(this.stretch,r,.095,.72,e);let a=n>12?A(this.velocity.x*t/n*76*r,-76,76):0;return z(this.lean,a,.055,.76,e),Math.hypot(this.target.x-this.position.x,this.target.y-this.position.y)*t<.05&&n<12&&(this.kind!==`curve`||Math.abs(this.progress.value-1)<.001)&&(Object.assign(this.position,this.target),this.velocity.x=this.velocity.y=0,this.kind=`idle`,this.path=void 0),this.kind===`idle`&&this.deformationSettled()&&(this.baseRotation=R(),this.lean=R(),this.stretch=R()),!0}deformationSettled(){let e=B(0,this.baseRotation.value);return Math.abs(e)<.01&&Math.abs(this.baseRotation.velocity)<.1&&Math.abs(this.lean.value)<.01&&Math.abs(this.lean.velocity)<.1&&Math.abs(this.stretch.value)<1e-4&&Math.abs(this.stretch.velocity)<.001}get bodyTransform(){let e=A(this.stretch.value,0,1);return`rotate(${this.baseRotation.value+this.lean.value}deg) rotate(${this.axis.value}deg) scale(${1+.38*e}, ${1-.18*e}) rotate(${-this.axis.value}deg)`}},oe={wide:{width:1280,height:720,columns:3},narrow:{width:640,height:760,columns:2}},V=440,H=340,U={top:84,right:64,bottom:64,left:64},W=[1,2,3,4,5,6],G=[{name:`Avery Kim`,color:`#3b82f6`},{name:`Mo Rivera`,color:`#10b981`},{name:`Jules Sato`,color:`#f59e0b`}],se=[[`AK`,`#3b82f6`,`Avery Kim`,`commented on Checkout redesign`,`2m`],[`MR`,`#10b981`,`Mo Rivera`,`approved your pull request`,`14m`],[`JS`,`#f59e0b`,`Jules Sato`,`shared Q4 roadmap with you`,`1h`],[`TN`,`#ef4444`,`Tess Novak`,`mentioned you in Launch plan`,`3h`],[`RO`,`#8b5cf6`,`Rae Okafor`,`invited you to Brand review`,`5h`],[`LB`,`#0ea5e9`,`Lee Brandt`,`resolved your comment`,`1d`],[`NH`,`#ec4899`,`Noor Haddad`,`added you to Onboarding`,`2d`],[`IC`,`#14b8a6`,`Ines Costa`,`replied to your comment`,`2d`],[`DV`,`#f97316`,`Dev Varma`,`shared Pricing page with you`,`3d`]],ce=`#e5484d`,le=5,ue=g(.45,.28),de=g(1.1,0),fe=g(.3,0),K=(e,t)=>({x:e%t*496,y:Math.floor(e/t)*412});function pe(e,t,n){let r=.5,i=e-.5,a=t-.5;return`M ${r+n} ${r} H ${i-n} A ${n} ${n} 0 0 1 ${i} ${r+n} V ${a-n} A ${n} ${n} 0 0 1 ${i-n} ${a} H ${r+n} A ${n} ${n} 0 0 1 ${r} ${a-n} V ${r+n} A ${n} ${n} 0 0 1 ${r+n} ${r} Z`}function me({version:e,rows:t=4}){return(0,S.jsxs)(`div`,{className:`ld-design`,"data-v":e,children:[(0,S.jsx)(`h1`,{children:`Notifications`}),se.slice(0,t).map(([e,t,n,r,i])=>(0,S.jsxs)(`div`,{className:`ld-n`,children:[(0,S.jsx)(`span`,{className:`ld-av`,style:{background:t},children:e}),(0,S.jsxs)(`div`,{children:[(0,S.jsx)(`div`,{className:`ld-t`,children:n}),(0,S.jsx)(`div`,{className:`ld-d`,children:r})]}),(0,S.jsx)(`span`,{className:`ld-w`,children:i})]},n))]})}function q({color:e,name:t,id:n}){return(0,S.jsxs)(`div`,{className:`cursor ld-cursor`,"data-cursor":n,style:{color:e},children:[(0,S.jsx)(`svg`,{width:`22`,height:`22`,viewBox:`0 0 24 24`,children:(0,S.jsx)(`path`,{d:c,fill:`currentColor`,stroke:`#fff`,strokeWidth:`1.5`,strokeLinejoin:`round`})}),t&&(0,S.jsx)(`span`,{className:`cursor-label`,style:{background:e},children:t})]})}function he({id:e,person:t,text:n}){let{name:r,color:i}=G[t];return(0,S.jsxs)(`div`,{className:`comment-pin ld-pin`,"data-pin":e,children:[(0,S.jsx)(l,{name:r,picture:null,color:i}),(0,S.jsxs)(`span`,{className:`comment-pin-preview`,children:[(0,S.jsxs)(`span`,{className:`comment-pin-head`,children:[(0,S.jsx)(`b`,{children:r}),` `,(0,S.jsx)(`span`,{className:`muted`,children:`now`})]}),(0,S.jsx)(`span`,{className:`comment-pin-text`,children:n})]})]})}function ge(e,t){let n=oe[t],r=t=>e.querySelector(t),i=r(`.ld-world`),a=r(`[data-zoom]`),o=r(`.ld-scene`),s=r(`.faces`),c=r(`.share-button`),l=r(`.share-sharing`),u=r(`.share-sharing i`),d=r(`.share-split`),f=r(`.ld-strokes path`),p=[...e.querySelectorAll(`.ld-frame`)],m=[...s.children],h={a:r(`[data-pin="a"]`),b:r(`[data-pin="b"]`)},_={width:n.width,height:n.height,x:0,y:0,z:1},v=new Set,b=0,x=0,S=!1,ee=e=>{b=requestAnimationFrame(ee);let t=Math.min(50,e-x);x=e;for(let e of v)e(t)||v.delete(e)},C=e=>{e!==S&&(S=e,cancelAnimationFrame(b),e&&(x=performance.now(),b=requestAnimationFrame(ee)))},w=e=>new Promise(t=>{let n=e;v.add(e=>(n-=e,n>0||(t(),!1)))}),T=(e,t)=>new Promise(n=>{let r=0;v.add(i=>(r+=i,r<e.ms?(t(e.at(r)),!0):(t(1),n(),!1)))}),E=(e,t)=>T({ms:e,at:t=>t/e,css:``},t),D={x:0,y:0,z:1},O=e=>{D=e,i.style.transform=`translate(${e.x}px, ${e.y}px) scale(${e.z})`,i.style.setProperty(`--inv`,String(1/e.z)),i.style.setProperty(`--zoom`,String(e.z)),a.textContent=`${Math.round(e.z*100)}%`},k=e=>{let t=W.slice(0,e).map((e,t)=>K(t,n.columns)),r=Math.max(...t.map(e=>e.x))+V,i=Math.max(...t.map(e=>e.y))+H,a={width:n.width-U.left-U.right,height:n.height-U.top-U.bottom},o=Math.min(1,a.width/r,a.height/i);return{z:o,x:U.left+(a.width-r*o)/2,y:U.top+(a.height-i*o)/2}},A=e=>{let t=D;return T(de,n=>O({x:t.x+(e.x-t.x)*n,y:t.y+(e.y-t.y)*n,z:t.z+(e.z-t.z)*n}))},j=(e,t,r,i=k(W.length))=>{let a=K(e-1,n.columns);return{x:i.x+(a.x+V*t)*i.z,y:i.y+(a.y+H*r)*i.z}},M=e=>{let t=r(`[data-cursor="${e}"]`),n=t.querySelector(`svg`),i=new ae({x:0,y:0}),a=()=>{t.style.transform=`translate3d(${i.position.x}px, ${i.position.y}px, 0)`,n.style.transform=i.bodyTransform};return v.add(e=>(i.advance(e/1e3,1),a(),!0)),{el:t,place(e){i.jumpTo(e),t.style.opacity=`1`,a()},hide(){t.style.opacity=`0`},move(e){return i.moveTo(e,_),new Promise(e=>v.add(()=>i.kind!==`idle`||(e(),!1)))}}},N=M(`you`),P=[M(`0`),M(`1`),M(`2`)],F=e=>T(ue,t=>{e.style.opacity=String(Math.min(1,t*1.6)),e.style.scale=String(.6+.4*t)}),te=async e=>{await E(90,t=>{e.style.scale=String(1-.05*t)}),await E(120,t=>{e.style.scale=String(.95+.05*t)})},I=e=>(s.style.display=``,e.style.display=``,F(e)),L=(e,t)=>{e.style.left=`${t.x}px`,e.style.top=`${t.y}px`},ne=()=>{let e=j(3,.5,.5),t=k(W.length),n=V*t.z*.56,r=H*t.z*.58;return Array.from({length:110},(t,i)=>{let a=i/109,o=-2.4+a*1.85*Math.PI*2,s=1+.06*Math.sin(a*9)+a*.05;return[e.x+Math.cos(o)*n*s+a*8,e.y+Math.sin(o)*r*s-a*6,.5]})},R=()=>{for(let e of p)e.style.opacity=`0`,e.style.scale=`0.6`;for(let e of m)e.style.opacity=`0`,e.style.scale=`0.6`,e.style.display=`none`;s.style.display=`none`;for(let e of Object.values(h))e.style.opacity=`0`,e.style.scale=`0.6`,e.classList.remove(`expanded`);for(let e of[N,...P])e.hide();c.style.display=``,l.style.display=`none`,d.style.display=`none`,f.setAttribute(`d`,``),o.style.opacity=`1`,s.style.opacity=`1`,O(k(1))},re=()=>{for(let e of[...p,...m,...Object.values(h)])e.style.opacity=`1`,e.style.scale=`1`;for(let e of m)e.style.display=``;s.style.display=``,L(h.a,j(2,.72,.34)),L(h.b,j(5,.3,.36)),h.b.classList.add(`expanded`),O(k(W.length)),f.setAttribute(`d`,y({points:ne(),size:le})),P[0].place(j(2,.34,.8)),P[1].place(j(5,.86,.7)),P[2].place(j(3,.92,.9))},ie=async()=>{let t=1500;for(;;){R(),await w(t),t=500;for(let e=0;e<p.length;e++)F(p[e]),e&&A(k(e+1)),await w(e?720:1e3);await w(700),N.place({x:n.width*.74,y:n.height+30});let r=e.getBoundingClientRect(),i=r.width/n.width;await N.move((e=>{let t=e.getBoundingClientRect();return{x:(t.left+t.width/2-r.left)/i,y:(t.top+t.height/2-r.top)/i}})(c)),await te(c),c.style.display=`none`,l.style.display=``,u.style.width=`6%`,await T(g(1,0),e=>{u.style.width=`${6+94*e}%`}),l.style.display=`none`,d.style.display=``,d.dataset.copied=``,N.move(j(1,.8,1.08)),await w(1e3),delete d.dataset.copied,P[0].place({x:-30,y:n.height*.42}),I(m[2]),await P[0].move(j(2,.72,.34)),P[1].place({x:n.width*.3,y:n.height+30}),I(m[1]),L(h.a,j(2,.72,.34)),F(h.a),P[0].move(j(2,.34,.8)),await P[1].move(j(5,.3,.36)),L(h.b,j(5,.3,.36)),await F(h.b),await w(250),h.b.classList.add(`expanded`),P[1].move(j(5,.86,.7));let a=ne();P[2].place({x:n.width+30,y:n.height*.3}),I(m[0]),await P[2].move({x:a[0][0],y:a[0][1]}),await E(1500,e=>{let t=Math.max(2,Math.ceil(e*a.length));f.setAttribute(`d`,y({points:a.slice(0,t),size:le}));let[n,r]=a[t-1];P[2].place({x:n,y:r})}),await w(400),P[2].move(j(3,.92,.9)),await w(1400),P[0].move(j(1,.5,.42)),await w(1100),P[1].move(j(4,.72,.62)),await w(2200),await E(420,e=>{o.style.opacity=String(1-e),s.style.opacity=String(1-e)}),await T(fe,()=>{})}};if(matchMedia(`(prefers-reduced-motion: reduce)`).matches)return R(),re(),()=>{};ie();let z=new IntersectionObserver(([e])=>C(e.isIntersecting));return z.observe(e),()=>{z.disconnect(),C(!1),v.clear()}}function _e(){let e=(0,x.useRef)(null),t=(0,x.useRef)(null),[i,a]=(0,x.useState)(0),c=i&&i<720?`narrow`:`wide`,p=oe[c];(0,x.useLayoutEffect)(()=>{let t=new ResizeObserver(([e])=>a(e.contentRect.width));return t.observe(e.current),()=>t.disconnect()},[]),(0,x.useEffect)(()=>{if(i)return ge(t.current,c)},[c,i>0]);let g=i/p.width,_=25*g,[v]=(0,x.useState)(()=>window.innerWidth*1.15/Math.min(1200,window.innerWidth-32)),y=(0,x.useMemo)(()=>W.map((e,t)=>K(t,p.columns)),[p.columns]);return(0,S.jsxs)(`div`,{ref:e,className:`ld-shell`,style:{height:i?p.height*g:void 0,borderRadius:_,"--ld-from":v},"aria-hidden":`true`,children:[i>0&&(0,S.jsx)(`svg`,{className:`ld-draw`,children:(0,S.jsx)(`path`,{d:pe(i,p.height*g,_-.5),pathLength:`1`})}),i>0&&(0,S.jsxs)(`div`,{ref:t,className:`ld-stage ld-${c}`,style:{width:p.width,height:p.height,transform:`scale(${g})`},children:[(0,S.jsxs)(`div`,{className:`ld-scene`,children:[(0,S.jsx)(`div`,{className:`world ld-world`,children:W.map((e,t)=>(0,S.jsxs)(`div`,{className:`frame ld-frame`,style:{left:y[t].x,top:y[t].y,width:V,height:H},children:[(0,S.jsx)(`div`,{className:`frame-title`,children:(0,S.jsxs)(`span`,{children:[`V`,e]})}),(0,S.jsx)(`div`,{className:`frame-surface`,children:(0,S.jsx)(me,{version:e})})]},e))}),(0,S.jsx)(`svg`,{className:`ld-strokes`,width:p.width,height:p.height,children:(0,S.jsx)(`path`,{fill:ce})}),(0,S.jsx)(he,{id:`a`,person:0,text:``}),(0,S.jsx)(he,{id:`b`,person:1,text:`Can we try this one darker?`})]}),(0,S.jsx)(`div`,{className:`chrome shell top-left ld-rise`,children:(0,S.jsx)(`span`,{className:`canvas-name`,children:`Notification Explorations`})}),(0,S.jsx)(`div`,{className:`chrome shell toolbar ld-rise`,children:(0,S.jsx)(`div`,{className:`toolbar-clip`,children:(0,S.jsxs)(`div`,{className:`toolbar-row`,children:[(0,S.jsx)(`span`,{className:`tool-slider`,style:{width:50}}),(0,S.jsx)(`span`,{className:`tool`,"aria-pressed":`true`,children:(0,S.jsx)(h,{size:18})}),(0,S.jsx)(`span`,{className:`tool`,children:(0,S.jsx)(d,{size:18})}),(0,S.jsx)(`span`,{className:`tool`,children:(0,S.jsx)(m,{size:18})}),(0,S.jsx)(`span`,{className:`shell-divider`}),(0,S.jsx)(`span`,{className:`tool`,children:(0,S.jsx)(s,{size:18})})]})})}),(0,S.jsxs)(`div`,{className:`chrome shell top-right ld-rise`,children:[(0,S.jsx)(`div`,{className:`faces`,children:[2,1,0].map(e=>(0,S.jsx)(`span`,{className:`face-button`,children:(0,S.jsx)(l,{name:G[e].name,picture:null,color:G[e].color})},e))}),(0,S.jsx)(`span`,{className:`primary-button share-button`,children:`Share`}),(0,S.jsxs)(`span`,{className:`primary-button share-sharing`,style:{display:`none`},children:[(0,S.jsx)(`i`,{}),(0,S.jsx)(`span`,{children:`Share`})]}),(0,S.jsxs)(`span`,{className:`share-split`,style:{display:`none`},children:[(0,S.jsxs)(`span`,{className:`primary-button`,children:[(0,S.jsxs)(`span`,{className:`ld-copied`,children:[(0,S.jsx)(u,{size:14}),`Copied`]}),(0,S.jsx)(`span`,{className:`ld-copy`,children:`Copy link`})]}),(0,S.jsx)(`span`,{className:`primary-button`,children:(0,S.jsx)(f,{size:14})})]})]}),(0,S.jsxs)(`div`,{className:`chrome shell zoom ld-rise`,children:[(0,S.jsx)(`span`,{className:`icon-button`,children:(0,S.jsx)(n,{})}),(0,S.jsx)(`span`,{className:`shell-divider`}),(0,S.jsx)(`span`,{className:`icon-button`,children:(0,S.jsx)(o,{})}),(0,S.jsx)(`span`,{className:`zoom-value`,"data-zoom":!0,children:`100%`}),(0,S.jsx)(`span`,{className:`icon-button`,children:(0,S.jsx)(r,{})})]}),(0,S.jsxs)(`div`,{className:`ld-cursors`,children:[(0,S.jsx)(q,{id:`0`,color:G[0].color,name:G[0].name}),(0,S.jsx)(q,{id:`1`,color:G[1].color,name:G[1].name}),(0,S.jsx)(q,{id:`2`,color:G[2].color,name:G[2].name}),(0,S.jsx)(q,{id:`you`,color:`#000`})]})]},c)]})}var J=[[0,6],[.4,13],[.75,19],[.88,20.2],[1,22]],ve=J[1][0],ye=e=>Math.min(1,Math.max(0,e)),be=e=>{let t=Math.max(1,J.findIndex(([t])=>t>=e)),[n,r]=J[t-1],[i,a]=J[t];return r+(e-n)/(i-n)*(a-r)},xe=e=>{if(e<J[0][1]||e>=J[J.length-1][1])return 1;let t=J.findIndex(([,t])=>t>=e);if(t<=0)return 0;let[n,r]=J[t-1],[i,a]=J[t];return n+(e-r)/(a-r)*(i-n)},Se=[[0,0],[.12,.5],[.28,1],[.58,.5],[.72,0],[.84,null]],Ce=e=>[...Se].reverse().find(([t])=>e>=t)[1],we=Array.from({length:8},(e,t)=>{let n=t*Math.PI/4,r=e=>`${(12+Math.cos(n)*e).toFixed(2)} ${(12+Math.sin(n)*e).toFixed(2)}`;return`M${r(8.2)}L${r(10.6)}`}).join(``);function Te({rays:e}){let t=(0,x.useId)(),n=e===null;return(0,S.jsxs)(`svg`,{className:`light-glyph`,viewBox:`0 0 24 24`,"aria-hidden":`true`,children:[(0,S.jsxs)(`mask`,{id:t,children:[(0,S.jsx)(`rect`,{x:`-4`,y:`-4`,width:`32`,height:`32`,fill:`#fff`}),(0,S.jsx)(`circle`,{className:`light-cut`,cx:`6.5`,cy:`17.5`,r:`8`,fill:`#000`,style:{transform:n?`none`:`translate(-9px, 9px)`}})]}),(0,S.jsxs)(`g`,{className:`light-turn`,style:{transform:`rotate(${n?180:0}deg)`},children:[(0,S.jsx)(`circle`,{className:`light-core`,cx:`12`,cy:`12`,r:`9`,fill:`currentColor`,mask:`url(#${t})`,style:{transform:`scale(${n?1:.56})`,opacity:n?1:.7+.3*e}}),(0,S.jsx)(`path`,{className:`light-rays`,d:we,stroke:`currentColor`,strokeWidth:`2`,strokeLinecap:`round`,style:{transform:n?`rotate(-90deg) scale(0.5)`:`scale(${.74+.26*e})`,opacity:n?0:.55+.45*e}})]})]})}function Ee({hour:e,onHour:t}){let[n,r]=(0,x.useState)(!1),[i,a]=(0,x.useState)(!1),[o,s]=(0,x.useState)(!1),c=(0,x.useRef)(null),l=n||i||o,u=xe(e),d=e=>{let n=c.current.getBoundingClientRect();t(be(ye((e.clientX-n.left)/n.width)))},f=e=>{e.currentTarget.setPointerCapture(e.pointerId),s(!0),d(e)},p=e=>{let n={ArrowLeft:-.05,ArrowDown:-.05,ArrowRight:.05,ArrowUp:.05}[e.key];n!==void 0&&(e.preventDefault(),t(be(ye(u+n))))},m=u>.85;return(0,S.jsx)(`div`,{className:`light-switch`,"data-open":l||void 0,"data-dragging":o||void 0,style:{"--x":u},onPointerEnter:e=>e.pointerType===`mouse`&&r(!0),onPointerLeave:e=>e.pointerType===`mouse`&&r(!1),onFocus:e=>e.target.matches(`:focus-visible`)&&a(!0),onBlur:()=>a(!1),children:(0,S.jsxs)(`div`,{className:`light-pill`,children:[(0,S.jsx)(`div`,{ref:c,className:`light-track`,role:`slider`,"aria-label":`Time of day`,"aria-valuemin":0,"aria-valuemax":100,"aria-valuenow":Math.round(u*100),"aria-valuetext":m?`Night`:u<ve?`Morning`:`Evening`,tabIndex:0,onPointerDown:f,onPointerMove:e=>o&&d(e),onPointerUp:()=>s(!1),onPointerCancel:()=>s(!1),onKeyDown:p,children:(0,S.jsx)(`span`,{className:`light-dot`})}),(0,S.jsx)(`button`,{type:`button`,className:`light-toggle`,"aria-label":m?`Switch to day`:`Switch to night`,onClick:()=>t(be(m?ve:1),!0),children:(0,S.jsx)(Te,{rays:Ce(u)})})]})})}var Y={ms:800,ease:[.45,0,.55,1]},De=t(...Y.ease),Oe=[[0,`#1f283c`,`#0f1421`,`#2a3755`,`#0c1220`],[4.5,`#1a2132`,`#0d111d`,`#25304c`,`#0a0f1b`],[5.5,`#555b7c`,`#2c3149`,`#8f91b4`,`#3a4252`],[6.5,`#efc4ae`,`#8a7a84`,`#ffe1c9`,`#a79a78`],[7.5,`#f8e2c8`,`#a89c94`,`#fff5e4`,`#b9c39a`],[10,`#fcfaf4`,`#adaca6`,`#ffffff`,`#c9d8bb`],[13,`#ffffff`,`#acadac`,`#ffffff`,`#d3e0c8`],[16,`#faf2e3`,`#aaa193`,`#fffbf1`,`#c7d0a7`],[18,`#e9bd95`,`#a07f62`,`#ffdcb2`,`#a89a62`],[19,`#da9570`,`#7a5a50`,`#ffb78c`,`#6d5f4b`],[19.75,`#84678a`,`#4a4262`,`#b78fa8`,`#3b3550`],[20.75,`#2f3756`,`#1a1f33`,`#3f4a70`,`#141a2c`],[24,`#1f283c`,`#0f1421`,`#2a3755`,`#0c1220`]],ke=e=>[1,3,5].map(t=>(parseInt(e.slice(t,t+2),16)/255)**2.2),Ae=e=>`#${e.map(e=>Math.round(e**(1/2.2)*255).toString(16).padStart(2,`0`)).join(``)}`,je=new URLSearchParams(location.search).get(`hour`),Me=()=>{if(je!==null)return Number(je);let e=new Date;return e.getHours()+e.getMinutes()/60};function Ne(){let[e,t]=(0,x.useState)(Me);return(0,x.useEffect)(()=>{let e=()=>t(Me()),n=window.setInterval(e,3e4);return document.addEventListener(`visibilitychange`,e),()=>{window.clearInterval(n),document.removeEventListener(`visibilitychange`,e)}},[]),e}function Pe(e){let{lit:t,shade:n}=Fe(e),[r,i,a]=t.map((e,t)=>n[t]+(e-n[t])*.6);return{lit:Ae(t),shade:Ae(n),dark:.2126*r+.7152*i+.0722*a<.18}}function Fe(e){let t=Math.max(1,Oe.findIndex(([t])=>t>=e)),[n,...r]=Oe[t-1],[i,...a]=Oe[t],o=(e-n)/(i-n),s=o*o*(3-2*o),[c,l,u,d]=r.map((e,t)=>{let n=ke(a[t]);return ke(e).map((e,t)=>e+(n[t]-e)*s)}),f=2*Math.PI*(e-12.5)/24,p=(9+36*Math.abs(Math.cos(f)))*Math.PI/180,m=Math.sin(f)*52*Math.PI/180;return{lit:c,shade:l,sky:u,leaf:d,sun:[-Math.cos(p)*Math.cos(m),Math.sin(p),Math.cos(p)*Math.sin(m)]}}var X={wind:1.1,gust:1,flutter:1.65,leafSize:1,shadow:.55,softness:1,sun:.61,skyLeaves:.29},Ie=`#version 300 es
|
|
3
|
+
in vec2 a;
|
|
4
|
+
void main() { gl_Position = vec4(a, 0., 1.); }`,Le=`#version 300 es
|
|
5
|
+
precision highp float;
|
|
6
|
+
out vec4 fragColor;
|
|
7
|
+
uniform vec2 uRes;
|
|
8
|
+
uniform float uTime;
|
|
9
|
+
uniform vec3 uSun, uLit, uShade, uSky, uLeaf;
|
|
10
|
+
uniform sampler2D uLeaves;
|
|
11
|
+
// Tuning, from LIGHT below.
|
|
12
|
+
uniform float uGust, uFlutter, uLeafSize, uShadow, uSoft, uSunStrength, uSkyLeaves;
|
|
13
|
+
|
|
14
|
+
// How far the corners melt. Lower is softer.
|
|
15
|
+
const float CORNER = 5.5;
|
|
16
|
+
// Half the height of the box, and how much of the screen its far wall fills.
|
|
17
|
+
const float HALF = 3.;
|
|
18
|
+
const float FAR_WALL = .5;
|
|
19
|
+
|
|
20
|
+
float dither(vec2 p) {
|
|
21
|
+
vec3 h = fract(p.xyx * .1031);
|
|
22
|
+
h += dot(h, h.yzx + 33.33);
|
|
23
|
+
return fract((h.x + h.y) * h.z);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
float band(float x, float a, float b, float soft) {
|
|
27
|
+
return smoothstep(a - soft, a + soft, x) * (1. - smoothstep(b - soft, b + soft, x));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Wind moving through the trees: gusts travel along the canopy, leaning the branches as they pass,
|
|
31
|
+
// stirring the leaves and opening small gaps that let more light through.
|
|
32
|
+
float foliage(vec2 zy) {
|
|
33
|
+
float t = uTime;
|
|
34
|
+
float gust = sin(t * .42 - zy.x * .3) + .6 * sin(t * .67 - zy.x * .5 + zy.y * .2 + 1.3);
|
|
35
|
+
float stir = (.12 + .2 * abs(gust)) * uFlutter;
|
|
36
|
+
vec2 lean = .6 * gust * uGust * vec2(1., .3);
|
|
37
|
+
vec2 flutter = stir * vec2(sin(t * 1.3 + zy.y * 1.7), cos(t * 1.1 + zy.x * 1.5));
|
|
38
|
+
float near = texture(uLeaves, (zy + lean + flutter) * .21 / uLeafSize + vec2(.17, .31)).r;
|
|
39
|
+
float far = texture(uLeaves, (zy + lean * .4 + flutter * .3) * .09 / uLeafSize + vec2(.61, .09)).r;
|
|
40
|
+
float gap = .03 * gust;
|
|
41
|
+
return max(smoothstep(.1 + gap, .42 + gap, near), .75 * smoothstep(.1, .4, far));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Sun reaching a point through a tall opening down the left side, then through the leaves.
|
|
45
|
+
// The further the light has travelled from the opening, the wider its edges.
|
|
46
|
+
float sunlight(vec3 p, float wall) {
|
|
47
|
+
vec3 hit = p + uSun * ((-wall - p.x) / uSun.x);
|
|
48
|
+
float soft = (.25 + .07 * length(hit - p)) * uSoft;
|
|
49
|
+
float open = band(hit.y, -HALF + .3, HALF - .3, soft) * band(hit.z, -30., 60., soft);
|
|
50
|
+
float bar = mod(hit.z, 4.) - 2.;
|
|
51
|
+
open *= 1. - min(1., .22 / soft) * exp(-bar * bar / (soft * soft));
|
|
52
|
+
vec3 leaf = p + uSun * ((-wall - 3. - p.x) / uSun.x);
|
|
53
|
+
return open * (1. - uShadow * foliage(leaf.zy));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
void main() {
|
|
57
|
+
float aspect = uRes.x / uRes.y;
|
|
58
|
+
vec2 p = (gl_FragCoord.xy - uRes * .5) / uRes.y;
|
|
59
|
+
|
|
60
|
+
// The far wall has the same shape as the screen, so the corners run straight to the screen's corners.
|
|
61
|
+
vec2 back = vec2(aspect, 1.) * .5 * FAR_WALL;
|
|
62
|
+
float wall = HALF * aspect;
|
|
63
|
+
float depth = HALF / back.y;
|
|
64
|
+
|
|
65
|
+
// How far past the far wall's outline this pixel is, and how much it belongs to each face.
|
|
66
|
+
vec2 reach = abs(p) / back;
|
|
67
|
+
vec3 weights = exp(CORNER * vec3(reach, 1.));
|
|
68
|
+
weights /= weights.x + weights.y + weights.z;
|
|
69
|
+
float besides = weights.x;
|
|
70
|
+
float above = weights.y;
|
|
71
|
+
float ahead = weights.z;
|
|
72
|
+
|
|
73
|
+
float leftFace = besides * (1. - smoothstep(-.1, .1, p.x / back.x));
|
|
74
|
+
float rightFace = besides - leftFace;
|
|
75
|
+
float floorFace = above * (1. - smoothstep(-.1, .1, p.y / back.y));
|
|
76
|
+
float ceilingFace = above - floorFace;
|
|
77
|
+
|
|
78
|
+
// Points on each face, for the sun.
|
|
79
|
+
float tWall = min(wall / max(abs(p.x), .01), depth);
|
|
80
|
+
float tFlat = min(HALF / max(abs(p.y), .01), depth);
|
|
81
|
+
vec3 onRight = vec3(wall, p.y * tWall, tWall);
|
|
82
|
+
vec3 onLeft = vec3(-wall, p.y * tWall, tWall);
|
|
83
|
+
vec3 onFloor = vec3(p.x * tFlat, -HALF, tFlat);
|
|
84
|
+
vec3 onBack = vec3(p.x * depth, p.y * depth, depth);
|
|
85
|
+
|
|
86
|
+
// Daylight fills the box from the left, thinning toward the right.
|
|
87
|
+
float x = p.x / (back.x * 2.);
|
|
88
|
+
float fill = 1. - .55 * smoothstep(-1.2, 1.5, x);
|
|
89
|
+
fill *= leftFace + floorFace + ahead * .9 + ceilingFace * .74 + rightFace * .8;
|
|
90
|
+
|
|
91
|
+
// The corners are only a slight gathering of shade where two faces meet.
|
|
92
|
+
float crease = 1. - max(besides, max(above, ahead));
|
|
93
|
+
fill *= 1. - crease * .3;
|
|
94
|
+
vec3 color = mix(uShade, uLit, fill);
|
|
95
|
+
|
|
96
|
+
// Sun.
|
|
97
|
+
vec3 s = uSun;
|
|
98
|
+
float sun = 0.;
|
|
99
|
+
if (floorFace > .02) sun += floorFace * sunlight(onFloor, wall) * min(1., s.y * 3.);
|
|
100
|
+
if (ahead > .02) sun += ahead * sunlight(onBack, wall) * min(1., max(-s.z, 0.) * 2.5);
|
|
101
|
+
if (rightFace > .02) sun += rightFace * sunlight(onRight, wall) * min(1., -s.x * 2.);
|
|
102
|
+
color += uLit * sun * uSunStrength;
|
|
103
|
+
|
|
104
|
+
// The left side is the light itself: bright sky with trees far out of focus.
|
|
105
|
+
color = mix(color, mix(uSky, uLeaf, foliage(onLeft.zy) * uSkyLeaves), leftFace);
|
|
106
|
+
|
|
107
|
+
color = pow(min(color, vec3(1.)), vec3(1. / 2.2));
|
|
108
|
+
// A single level of 8-bit noise: invisible, but it stops the gradients from banding.
|
|
109
|
+
color += (dither(gl_FragCoord.xy + 17.) - .5) / 255.;
|
|
110
|
+
fragColor = vec4(color, 1.);
|
|
111
|
+
}`;function Re(){let e=1024,t=document.createElement(`canvas`);t.width=t.height=e;let n=t.getContext(`2d`),r=606,i=()=>(r=r*16807%2147483647)/2147483647;n.fillStyle=`#000`,n.fillRect(0,0,e,e),n.fillStyle=n.strokeStyle=`#fff`,n.lineCap=`round`;for(let t=0;t<22;t++){let t=i()*e,r=i()*e,a=i()*Math.PI*2,o=260+i()*460;for(let e=0;e<o;e+=14){a+=(i()-.5)*.3;let s=t+Math.cos(a)*14,c=r+Math.sin(a)*14;if(n.lineWidth=Math.max(1.5,6*(1-e/o)),n.beginPath(),n.moveTo(t,r),n.lineTo(s,c),n.stroke(),t=s,r=c,!(i()>.42))for(let e of[-1,1]){if(i()>.8)continue;let o=a+e*(.5+i()*.7),s=34+i()*40;n.save(),n.translate(t+Math.cos(o)*s*.55,r+Math.sin(o)*s*.55),n.rotate(o),n.beginPath(),n.ellipse(0,0,s*.55,s*.2,0,0,Math.PI*2),n.fill(),n.restore()}}}return ze(t)}function ze(e){let t=document.createElement(`canvas`);t.width=t.height=128;let n=t.getContext(`2d`,{willReadFrequently:!0});n.drawImage(e,0,0,128,128);let r=n.getImageData(0,0,128,128),i=Float32Array.from({length:16384},(e,t)=>r.data[t*4]);for(let e=0;e<6;e++){let t=new Float32Array(16384);for(let n=0;n<128;n++)for(let r=0;r<128;r++){let a=0;for(let t=-13;t<=13;t++){let o=e%2?(n+t+128)%128*128+r:n*128+(r+t+128)%128;a+=i[o]}t[n*128+r]=a/27}i=t}return i.forEach((e,t)=>{r.data[t*4]=r.data[t*4+1]=r.data[t*4+2]=e}),n.putImageData(r,0,0),t}function Be({hour:e,glide:t}){let n=(0,x.useRef)(null),r=(0,x.useRef)(e),i=(0,x.useRef)(e),a=(0,x.useRef)(null),o=(0,x.useRef)(null);return(0,x.useEffect)(()=>{a.current=t?{from:i.current,at:performance.now()}:null,r.current=e,o.current?.()},[e]),(0,x.useEffect)(()=>{let e=n.current,t=e.getContext(`webgl2`,{antialias:!1,alpha:!1});if(!t)return;let s=t.createProgram();for(let[e,n]of[[t.VERTEX_SHADER,Ie],[t.FRAGMENT_SHADER,Le]]){let r=t.createShader(e);t.shaderSource(r,n),t.compileShader(r),t.attachShader(s,r)}t.bindAttribLocation(s,0,`a`),t.linkProgram(s),t.useProgram(s),t.bindBuffer(t.ARRAY_BUFFER,t.createBuffer()),t.bufferData(t.ARRAY_BUFFER,new Float32Array([-1,-1,3,-1,-1,3]),t.STATIC_DRAW),t.enableVertexAttribArray(0),t.vertexAttribPointer(0,2,t.FLOAT,!1,0,0),t.bindTexture(t.TEXTURE_2D,t.createTexture()),t.texImage2D(t.TEXTURE_2D,0,t.RGBA,t.RGBA,t.UNSIGNED_BYTE,Re()),t.texParameteri(t.TEXTURE_2D,t.TEXTURE_MIN_FILTER,t.LINEAR),t.texParameteri(t.TEXTURE_2D,t.TEXTURE_WRAP_S,t.REPEAT),t.texParameteri(t.TEXTURE_2D,t.TEXTURE_WRAP_T,t.REPEAT);let c=e=>t.getUniformLocation(s,e),l=(n,r)=>{let{clientWidth:i,clientHeight:a}=e;(e.width!==i||e.height!==a)&&(e.width=i,e.height=a,t.viewport(0,0,i,a));let o=Fe(r);t.uniform2f(c(`uRes`),i,a),t.uniform1f(c(`uTime`),n),t.uniform1f(c(`uGust`),X.gust),t.uniform1f(c(`uFlutter`),X.flutter),t.uniform1f(c(`uLeafSize`),X.leafSize),t.uniform1f(c(`uShadow`),X.shadow),t.uniform1f(c(`uSoft`),X.softness),t.uniform1f(c(`uSunStrength`),X.sun),t.uniform1f(c(`uSkyLeaves`),X.skyLeaves),t.uniform3fv(c(`uSun`),o.sun),t.uniform3fv(c(`uLit`),o.lit),t.uniform3fv(c(`uShade`),o.shade),t.uniform3fv(c(`uSky`),o.sky),t.uniform3fv(c(`uLeaf`),o.leaf),t.drawArrays(t.TRIANGLES,0,3)};if(matchMedia(`(prefers-reduced-motion: reduce)`).matches){o.current=()=>l(0,r.current);let t=new ResizeObserver(()=>l(0,r.current));return t.observe(e),()=>{o.current=null,t.disconnect()}}let u=performance.now(),d=u/1e3,f=requestAnimationFrame(function e(t){let n=a.current;if(n){let e=Math.min(1,Math.max(0,(t-n.at)/Y.ms));i.current=n.from+(r.current-n.from)*De(e),e===1&&(a.current=null)}else i.current+=(r.current-i.current)*(1-Math.exp((u-t)*.002));d+=(t-u)/1e3*X.wind,u=t,l(d,i.current),f=requestAnimationFrame(e)});return()=>cancelAnimationFrame(f)},[]),(0,S.jsx)(`canvas`,{ref:n,className:`window-light`,"aria-hidden":`true`})}var Ve=new URLSearchParams(location.search).get(`hero`)!==`1`,He=(0,x.lazy)(()=>b(()=>import(`./LandingStage-DlOW_YOx.js`),__vite__mapDeps([0,1,2,3,4,5]))),Ue=[[`A`,`canvas`,`for`,`your`],[`agent’s`,`explorations`]],We=g(.8,0),Ge=g(1.1,0),Ke=.1,qe=3,Je=.14,Z=8,Ye=500,Q=32,Xe=1500,Ze=(()=>{let e=crypto.getRandomValues(new Uint8Array(16)),t=Date.now();for(let n=5;n>=0;n--)e[n]=t%256,t=Math.floor(t/256);e[6]=e[6]&15|112,e[8]=e[8]&63|128;let n=Array.from(e,e=>e.toString(16).padStart(2,`0`)).join(``);return`${n.slice(0,8)}-${n.slice(8,12)}-${n.slice(12,16)}-${n.slice(16,20)}-${n.slice(20)}`})(),$=(e,t={})=>a(`/api/events`,{event:e,visit:Ze,...t});function Qe(){let e=(0,x.useRef)(null),t=(0,x.useRef)(null);return(0,x.useEffect)(()=>{if(matchMedia(`(prefers-reduced-motion: reduce)`).matches)return;let n=[0,0,0,0],r=[0,0,0,0],i=0,a=!1,o=()=>{let e=!0;r.forEach((t,i)=>{r[i]=t+(n[i]-t)*Je,Math.abs(n[i]-r[i])>.01&&(e=!1)}),e&&r.splice(0,4,...n);let[a,s,c,l]=r.map(e=>e.toFixed(2));t.current&&(t.current.style.transform=r.some(Boolean)?`perspective(${Ye}px) translate(${a}px, ${s}px) rotateX(${c}deg) rotateY(${l}deg)`:``),i=e?0:requestAnimationFrame(o)},s=()=>{i||=requestAnimationFrame(o)},c=(e,t)=>Math.max(-t,Math.min(t,e)),l=()=>{a&&(a=!1,n.fill(0),s())},u=t=>{if(t.pointerType!==`mouse`||!e.current)return;let r=e.current.getBoundingClientRect();if(t.clientX<=r.left-Q||t.clientX>=r.right+Q||t.clientY<=r.top-Q||t.clientY>=r.bottom+Q)return l();a=!0;let i=t.clientX-r.left-r.width/2,o=t.clientY-r.top-r.height/2;n[0]=c(i*Ke,qe),n[1]=c(o*Ke,qe),n[2]=c(-o/(r.height/2)*Z,Z),n[3]=c(i/(r.width/2)*Z,Z),s()};return window.addEventListener(`pointermove`,u),document.documentElement.addEventListener(`pointerleave`,l),()=>{cancelAnimationFrame(i),window.removeEventListener(`pointermove`,u),document.documentElement.removeEventListener(`pointerleave`,l)}},[]),[e,t]}function $e(){let[e,t]=(0,x.useState)(!1),n=(0,x.useRef)(0);(0,x.useEffect)(()=>()=>window.clearTimeout(n.current),[]);let r=()=>{t(!0),window.clearTimeout(n.current),n.current=window.setTimeout(()=>t(!1),Xe)};return[e,e=>{navigator.clipboard?.writeText(e).then(()=>{$(`prompt_copied`),r()}).catch(()=>{})},r]}function et(){let[e,t,n]=$e(),[r,i]=Qe();return(0,x.useEffect)(()=>(window.addEventListener(C,n),()=>window.removeEventListener(C,n)),[]),(0,S.jsx)(`span`,{ref:r,className:`landing-magnet`,children:(0,S.jsx)(`div`,{ref:i,className:`landing-cta`,children:(0,S.jsxs)(`button`,{type:`button`,className:`landing-button`,"data-copied":e||void 0,onClick:()=>t(p),children:[(0,S.jsxs)(`span`,{className:`landing-states`,children:[(0,S.jsxs)(`span`,{className:`landing-state`,children:[(0,S.jsx)(ee,{}),(0,S.jsx)(`span`,{className:`landing-label`,children:`Copy agent prompt`})]}),(0,S.jsxs)(`span`,{className:`landing-state`,"aria-hidden":`true`,children:[(0,S.jsx)(`svg`,{className:`landing-check`,viewBox:`0 0 18 18`,children:(0,S.jsx)(`path`,{d:`M3.5 9.5 7.25 13.25 14.5 5`,pathLength:`1`})}),(0,S.jsx)(`span`,{className:`landing-done`,children:`Copied`})]})]}),(0,S.jsx)(`span`,{className:`landing-sr`,"aria-live":`polite`,children:e?`Copied`:``})]})})})}function tt(){let e=0;return(0,S.jsx)(`h1`,{className:`landing-title`,children:Ue.map((t,n)=>(0,S.jsxs)(x.Fragment,{children:[n>0&&(0,S.jsx)(`br`,{}),t.map(t=>(0,S.jsxs)(x.Fragment,{children:[(0,S.jsx)(`span`,{className:`landing-word`,style:{"--delay":`${100+e++*80}ms`},children:t}),` `]},t))]},n))})}function nt(){(0,x.useEffect)(()=>{$(`landing_viewed`,{url:location.href,referrer:document.referrer})},[]);let e=Ne(),[t,n]=(0,x.useState)(null),[r,i]=(0,x.useState)(!1),a=(e,t=!1)=>{n(e),i(t)},o=t??e,{lit:s,shade:c,dark:l}=Pe(o),[u,d]=(0,x.useState)(!1),f=(0,x.useRef)(l);return(0,x.useEffect)(()=>{if(f.current===l)return;f.current=l,d(!0);let e=window.setTimeout(()=>d(!1),Y.ms);return()=>window.clearTimeout(e)},[l]),(0,x.useEffect)(()=>{document.body.style.background=s,document.body.style.colorScheme=l?`dark`:`light`},[s,l]),(0,S.jsxs)(`div`,{className:`landing`,"data-dark":l||void 0,"data-shifting":u||void 0,"data-glide":r||void 0,style:{"--lit":s,"--shade":c,"--shift-ms":`${Y.ms}ms`,"--shift-ease":`cubic-bezier(${Y.ease.join(`, `)})`,"--word-ease":We.css,"--word-ms":`${We.ms}ms`,"--rise-ease":Ge.css,"--rise-ms":`${Ge.ms}ms`},children:[(0,S.jsx)(Be,{hour:o,glide:r}),(0,S.jsxs)(`nav`,{className:`landing-nav`,"aria-label":`Main`,children:[(0,S.jsx)(`a`,{className:`wordmark`,href:`/`,children:(0,S.jsx)(v,{})}),(0,S.jsxs)(`div`,{className:`landing-nav-end`,children:[(0,S.jsx)(Ee,{hour:o,onHour:a}),(0,S.jsx)(`a`,{className:`landing-sign-in`,href:`/auth/google/start?next=/`,onClick:()=>$(`sign_in_clicked`),children:`Sign in`})]})]}),(0,S.jsxs)(`main`,{className:`landing-hero`,children:[(0,S.jsx)(tt,{}),(0,S.jsx)(`p`,{className:`landing-body`,children:`Then share, comment, and huddle with one link.`}),(0,S.jsx)(`div`,{className:`landing-actions`,children:(0,S.jsx)(et,{})}),Ve?(0,S.jsx)(`div`,{className:`landing-slot`,children:(0,S.jsx)(x.Suspense,{fallback:null,children:(0,S.jsx)(He,{dark:l})})}):(0,S.jsx)(_e,{})]}),(0,S.jsxs)(`footer`,{className:`landing-foot`,children:[(0,S.jsxs)(`span`,{children:[`© Materic, Inc. `,new Date().getFullYear()]}),(0,S.jsxs)(`nav`,{children:[(0,S.jsx)(`a`,{href:`/privacy`,children:`Privacy`}),(0,S.jsx)(`a`,{href:`/terms`,children:`Terms`})]})]})]})}export{nt as Landing,C as t};
|