@vendoai/vendo 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/capability-misses.js +2 -3
- package/dist/cli/cloud/client.d.ts +2 -5
- package/dist/cli/cloud/client.js +4 -4
- package/dist/cli/cloud/device-login.d.ts +11 -0
- package/dist/cli/cloud/device-login.js +52 -1
- package/dist/cli/dep-versions.d.ts +1 -1
- package/dist/cli/dep-versions.js +1 -1
- package/dist/cli/doctor-codes.d.ts +7 -0
- package/dist/cli/doctor-codes.js +7 -0
- package/dist/cli/doctor-live.js +11 -1
- package/dist/cli/doctor.js +122 -11
- package/dist/cli/extract/apply.js +1 -1
- package/dist/cli/extract/delegate.js +9 -1
- package/dist/cli/extract/extraction.d.ts +4 -0
- package/dist/cli/extract/extraction.js +29 -2
- package/dist/cli/extract/harness.d.ts +12 -0
- package/dist/cli/extract/harness.js +3 -0
- package/dist/cli/extract/stages.js +5 -1
- package/dist/cli/framework.d.ts +17 -2
- package/dist/cli/framework.js +32 -5
- package/dist/cli/init-scaffolds.d.ts +32 -1
- package/dist/cli/init-scaffolds.js +153 -9
- package/dist/cli/init.d.ts +14 -4
- package/dist/cli/init.js +212 -43
- package/dist/cli/playground/app/fake-client.js +11 -5
- package/dist/cli/playground/app/main.js +23 -13
- package/dist/cli/playground/app/scenario-mount.js +4 -1
- package/dist/cli/playground/app/theme-editor.js +27 -27
- package/dist/cli/playground/bundle.gen.d.ts +1 -1
- package/dist/cli/playground/bundle.gen.js +1 -1
- package/dist/cli/playground/embed-bundle.gen.d.ts +1 -1
- package/dist/cli/playground/embed-bundle.gen.js +1 -1
- package/dist/cli/shared.d.ts +1 -1
- package/dist/cli/shared.js +1 -1
- package/dist/cli/sync.d.ts +1 -1
- package/dist/cli/sync.js +1 -1
- package/dist/cloud-apps.js +2 -1
- package/dist/cloud-key-fetch.d.ts +16 -0
- package/dist/cloud-key-fetch.js +40 -0
- package/dist/cloud-tools.js +2 -1
- package/dist/connections.js +2 -2
- package/dist/dev-creds/model-edge.d.ts +21 -0
- package/dist/dev-creds/model-edge.js +24 -0
- package/dist/hosted-store.js +2 -1
- package/dist/mastra.js +71 -2
- package/dist/react.d.ts +1 -0
- package/dist/react.js +7 -0
- package/dist/refine.js +2 -1
- package/dist/sandbox.js +2 -2
- package/dist/server.d.ts +1 -1
- package/dist/server.js +56 -22
- package/dist/wire/apps.js +30 -1
- package/dist/wire/shared.d.ts +2 -2
- package/dist/wire/shared.js +1 -1
- package/package.json +20 -11
package/dist/cli/init.js
CHANGED
|
@@ -2,7 +2,7 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { mkdir, mkdtemp, readFile, rm } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
5
|
-
import { mergeOverrides, vendoSync
|
|
5
|
+
import { mergeOverrides, vendoSync } from "@vendoai/actions/sync";
|
|
6
6
|
import { createInterface } from "node:readline/promises";
|
|
7
7
|
import { stdin, stdout } from "node:process";
|
|
8
8
|
import { scrubErrorDetail } from "@vendoai/telemetry";
|
|
@@ -15,7 +15,7 @@ import { ENV_KEY_VARS, resolveDevCredential, describeDevCredential } from "../de
|
|
|
15
15
|
import { detectFramework, detectVendoWiring } from "./framework.js";
|
|
16
16
|
import { resolveScaffoldAuth } from "./init-auth.js";
|
|
17
17
|
import { ensureProviderDeps } from "./provider-deps.js";
|
|
18
|
-
import { expressServerSource, registrySource, routeSource, serverActionsModuleSource, VENDO_ENV_EXAMPLE, wiringServerActions, } from "./init-scaffolds.js";
|
|
18
|
+
import { customServerSource, expressServerSource, registrySource, routeSource, serverActionsModuleSource, VENDO_ENV_EXAMPLE, vendoRootWrapperSource, wiringServerActions, } from "./init-scaffolds.js";
|
|
19
19
|
import { createPrettyOutput, plainSelect, usePrettyOutput } from "./pretty.js";
|
|
20
20
|
import { contrastingText } from "./theme/color.js";
|
|
21
21
|
import { applyThemeDraft, extractTheme as extractThemeSlots, validateSlotValue, } from "./theme/extract-theme.js";
|
|
@@ -24,17 +24,24 @@ import { cloudProjectProps, consoleOutput, envLocalValueSync, errorClass, exists
|
|
|
24
24
|
* `vendo init` (install-dx v1, re-derived 2026-07-18): one command, zero
|
|
25
25
|
* questions on the happy path, no ceremony.
|
|
26
26
|
*
|
|
27
|
-
* scan → wire (the
|
|
28
|
-
* catch-all handler wired to
|
|
29
|
-
*
|
|
30
|
-
*
|
|
27
|
+
* scan → wire (the surface files — empty vendo/registry.tsx, the client
|
|
28
|
+
* mount vendo/vendo-root.tsx, the catch-all handler wired to the registry,
|
|
29
|
+
* and the ONE bounded layout edit that mounts <VendoRoot> + <VendoOverlay />
|
|
30
|
+
* so a by-the-book install is actually visible (0.4.1 E2E cert B3); a
|
|
31
|
+
* detected auth preset gets one consent-style confirm in interactive runs,
|
|
32
|
+
* --yes/non-interactive accept it silently — plus package.json hooks)
|
|
31
33
|
* → key (env stated, else the cloud starter offer) → done summary (files
|
|
32
|
-
* changed,
|
|
34
|
+
* changed, any remaining paste, next steps).
|
|
33
35
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
36
|
+
* The layout edit is the one place init touches user-authored code: it is
|
|
37
|
+
* idempotent (skipped when <VendoRoot> or <VendoOverlay> is already mounted),
|
|
38
|
+
* bounded (wrap the single JSX `{children}` + one import line), and degrades
|
|
39
|
+
* to the printed paste when the layout can't be edited safely.
|
|
40
|
+
*
|
|
41
|
+
* Removed by design: the interview, per-diff y/N approvals, the lib/ai.ts
|
|
42
|
+
* scaffold (createVendo's `model` is optional now), remix offers, the
|
|
43
|
+
* encryption-key step, the refine offer, and the finale ceremony (doctor owns
|
|
44
|
+
* verification and the live turn).
|
|
38
45
|
*/
|
|
39
46
|
const DEFAULT_RADIUS = { small: "4px", large: "12px" };
|
|
40
47
|
const BRIEF_PLACEHOLDER = `${BRIEF_TEMPLATE}\n`;
|
|
@@ -309,27 +316,59 @@ async function setupSkillSource() {
|
|
|
309
316
|
return null;
|
|
310
317
|
}
|
|
311
318
|
}
|
|
312
|
-
/** The
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
319
|
+
/** The remaining manual wiring lines, derived from how far the auto-wire got.
|
|
320
|
+
A layout wired this run (or one that already mounts a surface) leaves
|
|
321
|
+
nothing to paste; an uneditable layout gets the wrapper paste (the wrapper
|
|
322
|
+
owns the registry/theme imports — pasting them into a Server Component
|
|
323
|
+
layout is the RSC-serialization crash the wrapper exists to avoid); a
|
|
324
|
+
<VendoRoot>-without-surface host gets the one overlay line; Express keeps
|
|
325
|
+
its printed wiring lines. */
|
|
326
|
+
async function manualWiringLines(root, layout, withRegistry) {
|
|
327
|
+
if (layout.kind === "express") {
|
|
318
328
|
const wrap = withRegistry
|
|
319
|
-
? `<VendoRoot components={registry} theme={theme}
|
|
320
|
-
: `<VendoRoot theme={theme}
|
|
329
|
+
? `<VendoRoot components={registry} theme={theme}>…<VendoOverlay /></VendoRoot>`
|
|
330
|
+
: `<VendoRoot theme={theme}>…<VendoOverlay /></VendoRoot>`;
|
|
321
331
|
return [
|
|
322
332
|
`app.use("/api/vendo", mountVendo()); // in your server`,
|
|
323
|
-
`${wrap} // around your client root (see vendo/server for the imports)`,
|
|
333
|
+
`${wrap} // around your client root (see vendo/server for the imports; <VendoOverlay /> is the visible launcher + panel)`,
|
|
334
|
+
];
|
|
335
|
+
}
|
|
336
|
+
if (layout.kind === "custom") {
|
|
337
|
+
const wrap = withRegistry
|
|
338
|
+
? `<VendoRoot components={registry} theme={theme}>…<VendoOverlay /></VendoRoot>`
|
|
339
|
+
: `<VendoRoot theme={theme}>…<VendoOverlay /></VendoRoot>`;
|
|
340
|
+
return [
|
|
341
|
+
`Route your runtime's requests through the generated module — Cloudflare Workers: export default { fetch: (request, env) => handleVendoRequest(request, env) };`,
|
|
342
|
+
`${wrap} // around your client root (see vendo/server for the imports; <VendoOverlay /> is the visible launcher + panel)`,
|
|
343
|
+
`Set VENDO_BASE_URL to the deployed origin (credential forwarding fails closed without it).`,
|
|
344
|
+
];
|
|
345
|
+
}
|
|
346
|
+
if (layout.kind === "wired" || layout.kind === "already")
|
|
347
|
+
return [];
|
|
348
|
+
if (layout.kind === "overlay-missing") {
|
|
349
|
+
return [
|
|
350
|
+
`In ${layout.layoutPath}: nothing renders yet — <VendoRoot> is a context provider. Mount the visible surface inside it:`,
|
|
351
|
+
` import { VendoOverlay } from "@vendoai/vendo/react";`,
|
|
352
|
+
` … then add: <VendoOverlay /> (the launcher pill + panel users open)`,
|
|
324
353
|
];
|
|
325
354
|
}
|
|
326
355
|
const app = await appDirectory(root);
|
|
356
|
+
const layoutRel = relative(root, join(app, "layout.tsx"));
|
|
357
|
+
if (withRegistry) {
|
|
358
|
+
const wrapperSpecifier = relative(app, join(dirname(app), "vendo", "vendo-root")).split(sep).join("/");
|
|
359
|
+
return [
|
|
360
|
+
`In ${layoutRel}:`,
|
|
361
|
+
` import { VendoRoot } from ${JSON.stringify(wrapperSpecifier)};`,
|
|
362
|
+
` … then wrap: <VendoRoot>{children}</VendoRoot>`,
|
|
363
|
+
` (${join("vendo", "vendo-root.tsx")} mounts <VendoOverlay />, the visible launcher + panel)`,
|
|
364
|
+
];
|
|
365
|
+
}
|
|
366
|
+
// No registry consumer (a hand-wired route that ignores it): the direct
|
|
367
|
+
// provider + overlay paste — theme.json is serializable, so it may cross
|
|
368
|
+
// the Server Component boundary; the registry may not.
|
|
327
369
|
const specifier = await themeImportSpecifier(root, app);
|
|
328
|
-
const layout = relative(root, join(app, "layout.tsx"));
|
|
329
|
-
const registrySpecifier = relative(app, join(dirname(app), "vendo", "registry")).split(sep).join("/");
|
|
330
370
|
const importLines = [
|
|
331
|
-
`import { VendoRoot } from "@vendoai/vendo/react";`,
|
|
332
|
-
...(withRegistry ? [`import { registry } from ${JSON.stringify(registrySpecifier)};`] : []),
|
|
371
|
+
`import { VendoOverlay, VendoRoot } from "@vendoai/vendo/react";`,
|
|
333
372
|
...(specifier === null
|
|
334
373
|
? []
|
|
335
374
|
: [
|
|
@@ -337,12 +376,8 @@ async function vendoRootPasteLines(root, framework, withRegistry) {
|
|
|
337
376
|
`import type { VendoTheme } from "@vendoai/vendo";`,
|
|
338
377
|
]),
|
|
339
378
|
];
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
...(specifier === null ? [] : ["theme={theme as VendoTheme}"]),
|
|
343
|
-
];
|
|
344
|
-
const wrap = `<VendoRoot${props.length === 0 ? "" : ` ${props.join(" ")}`}>{children}</VendoRoot>`;
|
|
345
|
-
return [`In ${layout}:`, ...importLines.map((line) => ` ${line}`), ` … then wrap: ${wrap}`];
|
|
379
|
+
const wrap = `<VendoRoot${specifier === null ? "" : " theme={theme as VendoTheme}"}>{children}<VendoOverlay /></VendoRoot>`;
|
|
380
|
+
return [`In ${layoutRel}:`, ...importLines.map((line) => ` ${line}`), ` … then wrap: ${wrap}`];
|
|
346
381
|
}
|
|
347
382
|
/** The repo-specific agent tail (agent-install-dx): a non-interactive
|
|
348
383
|
scaffold run is agent-driven, so the run ends with plain deterministic
|
|
@@ -374,11 +409,17 @@ async function agentTailLines(args) {
|
|
|
374
409
|
if (args.framework === "express") {
|
|
375
410
|
// No exact entry file exists to name on Express — point at the printed
|
|
376
411
|
// wiring lines instead of guessing a path.
|
|
377
|
-
lines.push("edit your server and client entries — paste the mountVendo() and <VendoRoot
|
|
412
|
+
lines.push("edit your server and client entries — paste the mountVendo() and <VendoRoot>/<VendoOverlay /> lines above (without a mounted surface, users see nothing)");
|
|
378
413
|
}
|
|
379
|
-
else {
|
|
414
|
+
else if (args.layout.kind === "wired") {
|
|
415
|
+
lines.push(`surface: ${args.layout.layoutPath} wired this run — <VendoRoot> wraps the app and mounts <VendoOverlay /> (the visible launcher + panel) via ${join("vendo", "vendo-root.tsx")}; review the diff`);
|
|
416
|
+
}
|
|
417
|
+
else if (args.layout.kind === "overlay-missing") {
|
|
418
|
+
lines.push(`edit ${args.layout.layoutPath} — add <VendoOverlay /> inside your <VendoRoot> (see the lines above; <VendoRoot> alone renders NOTHING visible)`);
|
|
419
|
+
}
|
|
420
|
+
else if (args.layout.kind === "manual") {
|
|
380
421
|
const layout = relative(args.root, join(await appDirectory(args.root), "layout.tsx"));
|
|
381
|
-
lines.push(`edit ${layout} — wrap the app in the <VendoRoot> lines above`);
|
|
422
|
+
lines.push(`edit ${layout} — wrap the app in the <VendoRoot> lines above (it mounts <VendoOverlay />, the visible surface; without it users see nothing)`);
|
|
382
423
|
}
|
|
383
424
|
if (await readOptional(join(args.root, ".vendo", "brief.md")) === BRIEF_PLACEHOLDER) {
|
|
384
425
|
lines.push(`edit ${join(".vendo", "brief.md")} — replace the placeholder with what this product does and for whom`);
|
|
@@ -420,16 +461,98 @@ export function starViaGh(spawnStar, timeoutMs = 5_000) {
|
|
|
420
461
|
child.on("exit", (code) => settle(code === 0));
|
|
421
462
|
});
|
|
422
463
|
}
|
|
464
|
+
/** Where the wrapped `{children}` insert lands: the SINGLE JSX-rendered
|
|
465
|
+
`{children}` (between a `>` and a `<`). Requiring the JSX position keeps
|
|
466
|
+
the layout's own `({ children })` parameter destructure out of the count. */
|
|
467
|
+
const LAYOUT_CHILDREN = /(>)(\s*\{\s*children\s*\}\s*)(<)/g;
|
|
468
|
+
/** End index for the wrapper import: after the last top-level import
|
|
469
|
+
statement (lazy up to its module-specifier string literal, so multi-line
|
|
470
|
+
destructures work), else after a leading directive ("use client"), else 0. */
|
|
471
|
+
function importInsertionIndex(source) {
|
|
472
|
+
const heads = [...source.matchAll(/^import\b/gm)];
|
|
473
|
+
if (heads.length === 0) {
|
|
474
|
+
const directive = /^(?:\s*(?:'[^']*'|"[^"]*");?[^\S\n]*\n)+/.exec(source);
|
|
475
|
+
return directive === null ? 0 : directive[0].length;
|
|
476
|
+
}
|
|
477
|
+
const last = heads[heads.length - 1].index;
|
|
478
|
+
const statement = /import\s[\s\S]*?["'][^"']*["'][^\S\n]*;?/.exec(source.slice(last));
|
|
479
|
+
const end = last + (statement === null ? 0 : statement[0].length);
|
|
480
|
+
const newline = source.indexOf("\n", end);
|
|
481
|
+
return newline === -1 ? source.length : newline + 1;
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* The one bounded edit init makes to user-authored code (visible-surface fix,
|
|
485
|
+
* 0.4.1 E2E cert B3): mount the generated client wrapper around the layout's
|
|
486
|
+
* `{children}` and import it. Null — leave the file untouched, fall back to
|
|
487
|
+
* the printed paste — whenever a Vendo mount already exists (idempotence) or
|
|
488
|
+
* the file doesn't have exactly one JSX `{children}` to wrap (ambiguity).
|
|
489
|
+
* Exported for direct unit tests.
|
|
490
|
+
*/
|
|
491
|
+
export function wireNextLayout(source, wrapperSpecifier) {
|
|
492
|
+
if (source.includes("VendoRoot") || source.includes("VendoOverlay"))
|
|
493
|
+
return null;
|
|
494
|
+
const matches = [...source.matchAll(LAYOUT_CHILDREN)];
|
|
495
|
+
if (matches.length !== 1)
|
|
496
|
+
return null;
|
|
497
|
+
const match = matches[0];
|
|
498
|
+
const open = match.index + match[1].length;
|
|
499
|
+
const close = match.index + match[0].length - match[3].length;
|
|
500
|
+
let next = `${source.slice(0, open)}<VendoRoot>${source.slice(open, close)}</VendoRoot>${source.slice(close)}`;
|
|
501
|
+
const at = importInsertionIndex(next);
|
|
502
|
+
const importLine = `import { VendoRoot } from ${JSON.stringify(wrapperSpecifier)};\n`;
|
|
503
|
+
next = `${next.slice(0, at)}${importLine}${next.slice(at)}`;
|
|
504
|
+
return next;
|
|
505
|
+
}
|
|
423
506
|
async function buildPlan(options, confirmAuth, selectAuth) {
|
|
424
507
|
const root = resolve(options.targetDir);
|
|
425
|
-
|
|
508
|
+
// "unknown" detection lands on the runtime-neutral custom scaffold — the
|
|
509
|
+
// safe default that exists now (guessing the Next layout into a Worker
|
|
510
|
+
// host was the field failure; the non-interactive guard still demands an
|
|
511
|
+
// explicit --framework so agents never inherit a guess silently).
|
|
512
|
+
const detected = options.framework ?? await detectFramework(root);
|
|
513
|
+
const framework = detected === "unknown" ? "custom" : detected;
|
|
426
514
|
const changes = [];
|
|
427
515
|
let authAdvice = null;
|
|
428
516
|
let authWired = null;
|
|
429
517
|
let compositionPath = null;
|
|
430
518
|
let registryPath = null;
|
|
431
519
|
let withRegistry = false;
|
|
432
|
-
|
|
520
|
+
let layout = { kind: "manual" };
|
|
521
|
+
if (framework === "custom") {
|
|
522
|
+
layout = { kind: "custom" };
|
|
523
|
+
const wiring = await detectVendoWiring(root);
|
|
524
|
+
if (!wiring.server || !wiring.client) {
|
|
525
|
+
const typescript = await exists(join(root, "tsconfig.json"));
|
|
526
|
+
const server = join(root, "vendo", typescript ? "server.ts" : "server.mjs");
|
|
527
|
+
const registryFile = join(root, "vendo", typescript ? "registry.tsx" : "registry.mjs");
|
|
528
|
+
const registryBefore = await readOptional(registryFile);
|
|
529
|
+
const serverBefore = await readOptional(server);
|
|
530
|
+
// Same ownership rules as the Express branch: init composes only when
|
|
531
|
+
// it CREATES the composition, and the registry regenerates only for a
|
|
532
|
+
// consumer that uses it.
|
|
533
|
+
const scaffolding = serverBefore === null && !wiring.server;
|
|
534
|
+
const registryPlanned = registryBefore === null
|
|
535
|
+
&& (scaffolding || serverBefore?.includes("./registry") === true);
|
|
536
|
+
if (registryPlanned) {
|
|
537
|
+
const path = relative(root, registryFile);
|
|
538
|
+
const registryAfter = registrySource(typescript ? "tsx" : "mjs");
|
|
539
|
+
changes.push({ absolute: registryFile, path, before: null, after: registryAfter, diff: diff(path, null, registryAfter) });
|
|
540
|
+
registryPath = path;
|
|
541
|
+
}
|
|
542
|
+
if (scaffolding) {
|
|
543
|
+
const path = relative(root, server);
|
|
544
|
+
const auth = await resolveScaffoldAuth(root, path, options.auth, confirmAuth, selectAuth);
|
|
545
|
+
const serverAfter = customServerSource(typescript, auth.wired);
|
|
546
|
+
changes.push({ absolute: server, path, before: null, after: serverAfter, diff: diff(path, null, serverAfter) });
|
|
547
|
+
authAdvice = auth.advice;
|
|
548
|
+
authWired = auth.wired;
|
|
549
|
+
compositionPath = path;
|
|
550
|
+
}
|
|
551
|
+
withRegistry = registryBefore !== null || registryPlanned;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
else if (framework === "express") {
|
|
555
|
+
layout = { kind: "express" };
|
|
433
556
|
const wiring = await detectVendoWiring(root);
|
|
434
557
|
if (!wiring.server || !wiring.client) {
|
|
435
558
|
const typescript = await exists(join(root, "tsconfig.json"));
|
|
@@ -522,6 +645,45 @@ async function buildPlan(options, confirmAuth, selectAuth) {
|
|
|
522
645
|
changes.push({ absolute: route, path, before: routeBefore, after: wiredRoute, diff: diff(path, routeBefore, wiredRoute) });
|
|
523
646
|
}
|
|
524
647
|
}
|
|
648
|
+
// Visible surface (0.4.1 E2E cert B3): the client mount wrapper next to
|
|
649
|
+
// the registry — the "use client" boundary that owns the registry + theme
|
|
650
|
+
// imports (passing the registry from the Server Component layout into the
|
|
651
|
+
// client provider fails RSC serialization) and mounts <VendoOverlay /> —
|
|
652
|
+
// plus the ONE bounded layout edit that wraps `{children}` in it. Both
|
|
653
|
+
// idempotent: an existing wrapper is never clobbered, a layout already
|
|
654
|
+
// mounting <VendoRoot> or <VendoOverlay> is never re-edited, and an
|
|
655
|
+
// uneditable layout degrades to the printed paste.
|
|
656
|
+
const wrapperFile = join(dirname(app), "vendo", "vendo-root.tsx");
|
|
657
|
+
const wrapperBefore = await readOptional(wrapperFile);
|
|
658
|
+
const layoutFile = join(app, "layout.tsx");
|
|
659
|
+
const layoutBefore = await readOptional(layoutFile);
|
|
660
|
+
// The generated wrapper doesn't count as a host mount: its overlay is
|
|
661
|
+
// only real once a layout mounts the wrapper itself.
|
|
662
|
+
const mounts = await detectVendoWiring(root, { exclude: [wrapperFile] });
|
|
663
|
+
if (mounts.client || mounts.surface) {
|
|
664
|
+
// A mounted <VendoRoot> next to an existing wrapper IS the surface —
|
|
665
|
+
// the wrapper renders <VendoOverlay /> (the re-run after an auto-wire).
|
|
666
|
+
layout = mounts.surface || wrapperBefore !== null
|
|
667
|
+
? { kind: "already" }
|
|
668
|
+
: { kind: "overlay-missing", layoutPath: relative(root, layoutFile) };
|
|
669
|
+
}
|
|
670
|
+
else if (withRegistry) {
|
|
671
|
+
// The wrapper consumes ./registry, so it exists only alongside one —
|
|
672
|
+
// a hand-wired host that ignores the registry keeps the manual paste.
|
|
673
|
+
if (wrapperBefore === null) {
|
|
674
|
+
const path = relative(root, wrapperFile);
|
|
675
|
+
const themeSpecifier = await themeImportSpecifier(root, dirname(wrapperFile));
|
|
676
|
+
const wrapperAfter = vendoRootWrapperSource({ themeSpecifier });
|
|
677
|
+
changes.push({ absolute: wrapperFile, path, before: null, after: wrapperAfter, diff: diff(path, null, wrapperAfter) });
|
|
678
|
+
}
|
|
679
|
+
const wrapperSpecifier = relative(app, join(dirname(app), "vendo", "vendo-root")).split(sep).join("/");
|
|
680
|
+
const layoutAfter = layoutBefore === null ? null : wireNextLayout(layoutBefore, wrapperSpecifier);
|
|
681
|
+
if (layoutAfter !== null) {
|
|
682
|
+
const path = relative(root, layoutFile);
|
|
683
|
+
changes.push({ absolute: layoutFile, path, before: layoutBefore, after: layoutAfter, diff: diff(path, layoutBefore, layoutAfter) });
|
|
684
|
+
layout = { kind: "wired", layoutPath: path };
|
|
685
|
+
}
|
|
686
|
+
}
|
|
525
687
|
}
|
|
526
688
|
const packageJson = join(root, "package.json");
|
|
527
689
|
const packageBefore = await readOptional(packageJson);
|
|
@@ -561,7 +723,7 @@ async function buildPlan(options, confirmAuth, selectAuth) {
|
|
|
561
723
|
".vendo/theme.json",
|
|
562
724
|
".vendo/data/.gitignore",
|
|
563
725
|
];
|
|
564
|
-
const manualSteps = await
|
|
726
|
+
const manualSteps = await manualWiringLines(root, layout, withRegistry);
|
|
565
727
|
return {
|
|
566
728
|
changes,
|
|
567
729
|
manualSteps,
|
|
@@ -569,6 +731,7 @@ async function buildPlan(options, confirmAuth, selectAuth) {
|
|
|
569
731
|
authWired,
|
|
570
732
|
compositionPath,
|
|
571
733
|
registryPath,
|
|
734
|
+
layout,
|
|
572
735
|
plan: {
|
|
573
736
|
framework,
|
|
574
737
|
root,
|
|
@@ -658,7 +821,7 @@ export async function runInit(options) {
|
|
|
658
821
|
if (options.framework === undefined && (options.yes === true || !interactive)
|
|
659
822
|
&& await detectFramework(root) === "unknown") {
|
|
660
823
|
output.error("Framework not detected (no next or express dependency in package.json) and this run cannot ask. " +
|
|
661
|
-
"Pass --framework.
|
|
824
|
+
"Pass --framework. Examples: vendo init --yes --framework next · --framework custom (any Web-standard runtime: Cloudflare Workers, Bun, Hono, ...)");
|
|
662
825
|
return 1;
|
|
663
826
|
}
|
|
664
827
|
// (No stdin-TTY guard on these defaults, unlike the star ask's: an unshown
|
|
@@ -671,7 +834,7 @@ export async function runInit(options) {
|
|
|
671
834
|
? undefined
|
|
672
835
|
: (options.selectAuth ?? (pretty === null ? plainSelect : pretty.select));
|
|
673
836
|
const detectStarted = Date.now();
|
|
674
|
-
const { plan, changes, manualSteps, authAdvice, authWired, compositionPath, registryPath } = await buildPlan(options, confirmAuth, selectAuth);
|
|
837
|
+
const { plan, changes, manualSteps, authAdvice, authWired, compositionPath, registryPath, layout } = await buildPlan(options, confirmAuth, selectAuth);
|
|
675
838
|
const detectMs = Date.now() - detectStarted;
|
|
676
839
|
let telemetry = telemetryFor(options, output, root);
|
|
677
840
|
await telemetry.track("init_started", { framework: plan.framework });
|
|
@@ -950,10 +1113,16 @@ export async function runInit(options) {
|
|
|
950
1113
|
if (aiVersion !== null && Number.parseInt(aiVersion, 10) >= 7) {
|
|
951
1114
|
output.error(`warning: installed ai@${aiVersion} is unsupported — Vendo supports ai@6; downgrade (npm install ai@^6 @ai-sdk/anthropic@^3 @ai-sdk/react@^3) or track github.com/runvendo/vendo/issues/478`);
|
|
952
1115
|
}
|
|
953
|
-
// Done —
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
output.log(
|
|
1116
|
+
// Done — whatever paste remains (none when the layout auto-wire landed),
|
|
1117
|
+
// then their own dev server.
|
|
1118
|
+
if (layout.kind === "wired") {
|
|
1119
|
+
output.log(`\nMounted <VendoRoot> + <VendoOverlay /> (the visible launcher + panel) in ${layout.layoutPath} — review the diff before committing.`);
|
|
1120
|
+
}
|
|
1121
|
+
if (manualSteps.length > 0) {
|
|
1122
|
+
output.log("\nLast steps are yours:");
|
|
1123
|
+
for (const line of manualSteps)
|
|
1124
|
+
output.log(` ${line}`);
|
|
1125
|
+
}
|
|
957
1126
|
output.log("\nThen start your dev server — the agent is live in your app.");
|
|
958
1127
|
output.log("Verify everything: `npx vendo doctor` (it can start the server and run a live turn).");
|
|
959
1128
|
// Agent tail (agent-install-dx): the --yes-or-non-TTY path is agent-driven
|
|
@@ -962,7 +1131,7 @@ export async function runInit(options) {
|
|
|
962
1131
|
// never reaches here (its read-only JSON plan returned above).
|
|
963
1132
|
if (options.yes === true || !interactive) {
|
|
964
1133
|
output.log("\nAgent tail:");
|
|
965
|
-
const tail = await agentTailLines({ root, framework: plan.framework, registryPath, compositionPath, authWired, cloudKeyMissing: credential.rung === "none" });
|
|
1134
|
+
const tail = await agentTailLines({ root, framework: plan.framework, registryPath, compositionPath, authWired, layout, cloudKeyMissing: credential.rung === "none" });
|
|
966
1135
|
for (const line of tail)
|
|
967
1136
|
output.log(` ${line}`);
|
|
968
1137
|
}
|
|
@@ -72,12 +72,18 @@ export function createFakeClient(fixtures) {
|
|
|
72
72
|
},
|
|
73
73
|
connections: {
|
|
74
74
|
list: async () => [...state.connections],
|
|
75
|
-
//
|
|
76
|
-
//
|
|
75
|
+
// A realistic host catalog: the toolkits the scenarios act through
|
|
76
|
+
// (slack, github) plus the breadth a real Composio-brokered host
|
|
77
|
+
// exposes, so the connect tray demos search + scroll + real brand
|
|
78
|
+
// marks (every slug below has a real logo on logos.composio.dev).
|
|
77
79
|
catalog: async () => [
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
80
|
+
"slack", "github", "gmail", "googlecalendar", "googledrive",
|
|
81
|
+
"googlesheets", "notion", "linear", "jira", "confluence", "asana",
|
|
82
|
+
"trello", "clickup", "monday", "todoist", "hubspot", "salesforce",
|
|
83
|
+
"stripe", "shopify", "intercom", "zendesk", "airtable", "dropbox",
|
|
84
|
+
"figma", "discord", "zoom", "outlook", "calendly", "mailchimp",
|
|
85
|
+
"typeform",
|
|
86
|
+
].map((toolkit) => ({ toolkit, connector: "composio" })),
|
|
81
87
|
initiate: async ({ toolkit, connector }) => ({
|
|
82
88
|
id: `conn_${toolkit}_new`,
|
|
83
89
|
connector: connector ?? "composio",
|
|
@@ -6,27 +6,34 @@ import { ScenarioMount } from "./scenario-mount.js";
|
|
|
6
6
|
import { scenarios } from "./scenarios.js";
|
|
7
7
|
import { ThemeEditor, useGoogleFont } from "./theme-editor.js";
|
|
8
8
|
import { decodeThemeParam, encodeThemeParam } from "./theme-state.js";
|
|
9
|
+
/* Vendo brand shell (brand kit): Porcelain #FAFAF8, Ink #17171A, Ultramarine
|
|
10
|
+
#4338CA, Onest (its @font-face rides in via the chrome stylesheet). The
|
|
11
|
+
STAGE column deliberately has no fixed background — App paints it with the
|
|
12
|
+
active theme's colors.background so every chrome surface sits on its own
|
|
13
|
+
canvas edge-to-edge instead of printing an abrupt theme-colored rectangle
|
|
14
|
+
on the shell. */
|
|
9
15
|
const SHELL_CSS = `
|
|
10
16
|
:root { color-scheme: light; }
|
|
11
17
|
* { box-sizing: border-box; }
|
|
12
|
-
body { margin: 0; font-family: Inter, system-ui, -apple-system, sans-serif; background: #
|
|
18
|
+
body { margin: 0; font-family: Onest, Inter, system-ui, -apple-system, sans-serif; background: #fafaf8; color: #17171a; }
|
|
13
19
|
.pg-shell { display: grid; grid-template-columns: 250px minmax(0, 1fr); min-height: 100vh; }
|
|
14
|
-
.pg-nav { border-right: 1px solid
|
|
20
|
+
.pg-nav { border-right: 1px solid rgba(23, 23, 26, 0.08); background: #fafaf8; padding: 18px 14px; position: sticky; top: 0; height: 100vh; overflow-y: auto; }
|
|
15
21
|
.pg-brand { font-size: 15px; font-weight: 700; letter-spacing: -0.01em; margin: 4px 6px 2px; }
|
|
16
|
-
.pg-brand
|
|
17
|
-
.pg-
|
|
18
|
-
.pg-
|
|
19
|
-
.pg-link
|
|
20
|
-
.pg-link
|
|
21
|
-
.pg-
|
|
22
|
+
.pg-brand-mark { color: #4338ca; }
|
|
23
|
+
.pg-brand small { display: block; font-weight: 500; font-size: 11px; color: #6f6f78; margin-top: 3px; }
|
|
24
|
+
.pg-group { font-size: 10.5px; font-weight: 650; text-transform: uppercase; letter-spacing: 0.08em; color: #85858f; margin: 18px 6px 6px; }
|
|
25
|
+
.pg-link { display: block; padding: 7px 10px; border-radius: 8px; font-size: 13px; color: #3a3a41; text-decoration: none; transition: background .12s, color .12s; }
|
|
26
|
+
.pg-link:hover { background: rgba(67, 56, 202, 0.07); color: #17171a; }
|
|
27
|
+
.pg-link[aria-current="page"] { background: #17171a; color: #fafaf8; }
|
|
28
|
+
.pg-main { padding: 26px 30px 60px; min-width: 0; transition: background .2s, color .2s; }
|
|
22
29
|
@media (max-width: 640px) {
|
|
23
30
|
.pg-shell { grid-template-columns: 1fr; }
|
|
24
|
-
.pg-nav { position: static; height: auto; border-right: none; border-bottom: 1px solid
|
|
31
|
+
.pg-nav { position: static; height: auto; border-right: none; border-bottom: 1px solid rgba(23, 23, 26, 0.08); }
|
|
25
32
|
.pg-main { padding: 18px 16px 80px; }
|
|
26
33
|
}
|
|
27
34
|
.pg-head { max-width: 860px; margin-bottom: 20px; }
|
|
28
35
|
.pg-head h1 { font-size: 19px; letter-spacing: -0.01em; margin: 0 0 6px; }
|
|
29
|
-
.pg-head p { font-size: 13.5px;
|
|
36
|
+
.pg-head p { font-size: 13.5px; opacity: 0.72; line-height: 1.55; margin: 0; }
|
|
30
37
|
.pg-stage { max-width: 860px; }
|
|
31
38
|
.pg-embed { padding: 0; }
|
|
32
39
|
`;
|
|
@@ -95,12 +102,15 @@ function App() {
|
|
|
95
102
|
const embed = new URLSearchParams(window.location.search).has("embed");
|
|
96
103
|
const [theme, setTheme] = useThemeState(embed);
|
|
97
104
|
if (embed) {
|
|
98
|
-
return (
|
|
105
|
+
return (
|
|
106
|
+
// Same host-canvas rule as the stage: the embed page (the phone iframe's
|
|
107
|
+
// whole viewport) wears the theme background so the surface blends.
|
|
108
|
+
_jsx("div", { className: "pg-embed", style: { background: theme.colors.background, minHeight: "100vh" }, children: _jsx(ScenarioMount, { scenario: scenario, theme: theme }, scenario.id) }));
|
|
99
109
|
}
|
|
100
110
|
const groups = [...new Set(scenarios.map((entry) => entry.group))];
|
|
101
|
-
return (_jsxs("div", { className: "pg-shell", children: [_jsxs("nav", { className: "pg-nav", "aria-label": "Scenarios", children: [_jsxs("div", { className: "pg-brand", children: ["
|
|
111
|
+
return (_jsxs("div", { className: "pg-shell", children: [_jsxs("nav", { className: "pg-nav", "aria-label": "Scenarios", children: [_jsxs("div", { className: "pg-brand", children: [_jsx("span", { className: "pg-brand-mark", children: "vendo" }), " playground", _jsx("small", { children: "every surface \u00B7 scripted data \u00B7 no model key" })] }), groups.map((group) => (_jsxs("div", { children: [_jsx("div", { className: "pg-group", children: group }), scenarios
|
|
102
112
|
.filter((entry) => entry.group === group)
|
|
103
|
-
.map((entry) => (_jsx("a", { className: "pg-link", href: `#${entry.id}`, "aria-current": entry.id === scenario.id ? "page" : undefined, children: entry.title }, entry.id)))] }, group)))] }), _jsxs("main", { className: "pg-main", children: [_jsxs("header", { className: "pg-head", children: [_jsx("h1", { children: scenario.title }), _jsx("p", { children: scenario.description })] }), _jsx("div", { className: "pg-stage", children: _jsx(ScenarioMount, { scenario: scenario, theme: theme }, scenario.id) })] }), _jsx(ThemeEditor, { theme: theme, onChange: setTheme })] }));
|
|
113
|
+
.map((entry) => (_jsx("a", { className: "pg-link", href: `#${entry.id}`, "aria-current": entry.id === scenario.id ? "page" : undefined, children: entry.title }, entry.id)))] }, group)))] }), _jsxs("main", { className: "pg-main", style: { background: theme.colors.background, color: theme.colors.text }, children: [_jsxs("header", { className: "pg-head", children: [_jsx("h1", { children: scenario.title }), _jsx("p", { children: scenario.description })] }), _jsx("div", { className: "pg-stage", children: _jsx(ScenarioMount, { scenario: scenario, theme: theme }, scenario.id) })] }), _jsx(ThemeEditor, { theme: theme, onChange: setTheme })] }));
|
|
104
114
|
}
|
|
105
115
|
const style = document.createElement("style");
|
|
106
116
|
style.textContent = SHELL_CSS;
|
|
@@ -44,5 +44,8 @@ export function ScenarioMount({ scenario, theme, root }) {
|
|
|
44
44
|
const client = useMemo(() => createFakeClient((scenario.fixtures ?? playgroundFixtures)()), [scenario]);
|
|
45
45
|
const transport = useMemo(() => (scenario.script ? new ScriptedTransport(scenario.script, { speed: scenario.speed ?? 1 }) : undefined), [scenario]);
|
|
46
46
|
useAutoSend(scenario, root);
|
|
47
|
-
|
|
47
|
+
// No explicit `connectors` prop: the surfaces resolve the auto catalog from
|
|
48
|
+
// the fake client (fake-client.ts), so the tray demos search + scroll over
|
|
49
|
+
// a realistic host's toolkit breadth.
|
|
50
|
+
return (_jsx(VendoProvider, { client: client, transport: transport, theme: theme, tools: playgroundToolMeta, children: scenario.render() }));
|
|
48
51
|
}
|
|
@@ -5,51 +5,51 @@ const EDITOR_CSS = `
|
|
|
5
5
|
/* Above the chrome's own overlay/takeover layers (they top out at
|
|
6
6
|
2147483100) — the editor is harness tooling and must stay reachable
|
|
7
7
|
while the overlay scenario is open. */
|
|
8
|
-
.te-card, .te-pill { position: fixed; top: 16px; right: 16px; z-index: 2147483200; font-family: Inter, system-ui, sans-serif; }
|
|
9
|
-
.te-card { width: 302px; max-height: calc(100vh - 32px); display: flex; flex-direction: column; background: #
|
|
8
|
+
.te-card, .te-pill { position: fixed; top: 16px; right: 16px; z-index: 2147483200; font-family: Onest, Inter, system-ui, sans-serif; }
|
|
9
|
+
.te-card { width: 302px; max-height: calc(100vh - 32px); display: flex; flex-direction: column; background: #fafaf8; border: 1px solid rgba(23, 23, 26, 0.1); border-radius: 14px; box-shadow: 0 18px 44px rgba(23, 23, 26, 0.18); transform-origin: top right; transition: transform 320ms cubic-bezier(0.2, 0.8, 0.2, 1), opacity 320ms cubic-bezier(0.2, 0.8, 0.2, 1); }
|
|
10
10
|
.te-card[data-closed="true"] { transform: scale(0.6) translateY(-8px); opacity: 0; pointer-events: none; }
|
|
11
|
-
.te-pill { display: inline-flex; align-items: center; gap: 7px; background: #
|
|
11
|
+
.te-pill { display: inline-flex; align-items: center; gap: 7px; background: #17171a; color: #fafaf8; border: none; border-radius: 999px; font: 600 12.5px Onest, Inter, sans-serif; padding: 9px 15px; cursor: pointer; box-shadow: 0 10px 26px rgba(23, 23, 26, 0.25); opacity: 0; pointer-events: none; transition: opacity 320ms cubic-bezier(0.2, 0.8, 0.2, 1); }
|
|
12
12
|
.te-pill[data-shown="true"] { opacity: 1; pointer-events: auto; }
|
|
13
|
-
.te-dot { width: 13px; height: 13px; border-radius: 50%; background: conic-gradient(#
|
|
13
|
+
.te-dot { width: 13px; height: 13px; border-radius: 50%; background: conic-gradient(#4338ca, #818cf8, #22d3ee, #4338ca); border: 1px solid rgba(255, 255, 255, 0.4); }
|
|
14
14
|
.te-grab { display: none; }
|
|
15
|
-
.te-head { display: flex; align-items: center; justify-content: space-between; padding: 12px 14px 10px; border-bottom: 1px solid
|
|
16
|
-
.te-head b { font-size: 12.5px; color: #
|
|
17
|
-
.te-head small { display: block; font-weight: 500; font-size: 10.5px; color: #
|
|
18
|
-
.te-x { border: none; background: none; color: #
|
|
19
|
-
.te-x:hover { background:
|
|
20
|
-
.te-body { overflow-y: auto; padding: 10px 14px 12px; font-size: 12.5px; color: #
|
|
15
|
+
.te-head { display: flex; align-items: center; justify-content: space-between; padding: 12px 14px 10px; border-bottom: 1px solid rgba(23, 23, 26, 0.1); flex-shrink: 0; }
|
|
16
|
+
.te-head b { font-size: 12.5px; color: #17171a; }
|
|
17
|
+
.te-head small { display: block; font-weight: 500; font-size: 10.5px; color: #6f6f78; margin-top: 2px; }
|
|
18
|
+
.te-x { border: none; background: none; color: #6f6f78; font-size: 14px; cursor: pointer; padding: 2px 6px; border-radius: 6px; line-height: 1; }
|
|
19
|
+
.te-x:hover { background: rgba(67, 56, 202, 0.08); color: #17171a; }
|
|
20
|
+
.te-body { overflow-y: auto; padding: 10px 14px 12px; font-size: 12.5px; color: #17171a; }
|
|
21
21
|
.te-sec { margin-bottom: 14px; }
|
|
22
|
-
.te-label { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: #
|
|
22
|
+
.te-label { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: #85858f; margin-bottom: 7px; }
|
|
23
23
|
.te-presets { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
24
|
-
.te-chip { display: inline-flex; align-items: center; gap: 6px; border: 1px solid
|
|
24
|
+
.te-chip { display: inline-flex; align-items: center; gap: 6px; border: 1px solid rgba(23, 23, 26, 0.1); background: #ffffff; border-radius: 999px; padding: 4px 10px 4px 5px; font: 600 11.5px Onest, Inter, sans-serif; cursor: pointer; color: #17171a; }
|
|
25
25
|
.te-chip .te-swatch { width: 16px; height: 16px; border-radius: 50%; border: 1px solid rgba(0, 0, 0, 0.12); }
|
|
26
|
-
.te-chip[aria-pressed="true"] { border-color: #
|
|
26
|
+
.te-chip[aria-pressed="true"] { border-color: #4338ca; box-shadow: inset 0 0 0 1px #4338ca; }
|
|
27
27
|
.te-crow { display: flex; align-items: center; gap: 8px; margin-bottom: 5px; }
|
|
28
|
-
.te-crow input[type="color"] { width: 24px; height: 24px; border: 1px solid
|
|
28
|
+
.te-crow input[type="color"] { width: 24px; height: 24px; border: 1px solid rgba(23, 23, 26, 0.1); border-radius: 7px; padding: 1px; background: #ffffff; cursor: pointer; flex-shrink: 0; }
|
|
29
29
|
.te-crow span { flex: 1; font-weight: 550; font-size: 11.5px; }
|
|
30
|
-
.te-crow code { font: 500 10.5px ui-monospace, monospace; color: #
|
|
31
|
-
.te-body select, .te-body input[type="text"] { width: 100%; font: 500 12px Inter, sans-serif; color: #
|
|
30
|
+
.te-crow code { font: 500 10.5px ui-monospace, monospace; color: #6f6f78; }
|
|
31
|
+
.te-body select, .te-body input[type="text"] { width: 100%; font: 500 12px Onest, Inter, sans-serif; color: #17171a; background: #ffffff; border: 1px solid rgba(23, 23, 26, 0.1); border-radius: 8px; padding: 6px 8px; }
|
|
32
32
|
.te-body input[type="text"] { margin-top: 6px; }
|
|
33
33
|
.te-srow { display: flex; align-items: center; gap: 8px; margin-top: 6px; }
|
|
34
34
|
.te-srow label { font-size: 11.5px; font-weight: 550; width: 62px; flex-shrink: 0; }
|
|
35
|
-
.te-srow input[type="range"] { flex: 1; accent-color: #
|
|
36
|
-
.te-srow output { font: 500 10.5px ui-monospace, monospace; color: #
|
|
35
|
+
.te-srow input[type="range"] { flex: 1; accent-color: #4338ca; min-width: 0; }
|
|
36
|
+
.te-srow output { font: 500 10.5px ui-monospace, monospace; color: #6f6f78; width: 34px; text-align: right; flex-shrink: 0; }
|
|
37
37
|
.te-segs { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
|
|
38
|
-
.te-seg { display: flex; background: #
|
|
39
|
-
.te-seg button { flex: 1; border: none; background: none; font: 600 11px Inter, sans-serif; color: #
|
|
40
|
-
.te-seg button[aria-pressed="true"] { background: #
|
|
41
|
-
.te-foot { display: flex; gap: 8px; padding: 10px 14px; border-top: 1px solid
|
|
42
|
-
.te-foot button { flex: 1; font: 600 11.5px Inter, sans-serif; border-radius: 8px; padding: 7px 0; cursor: pointer; }
|
|
43
|
-
.te-copy { background: #
|
|
44
|
-
.te-link { background: #
|
|
38
|
+
.te-seg { display: flex; background: #ffffff; border: 1px solid rgba(23, 23, 26, 0.1); border-radius: 9px; padding: 2px; gap: 2px; }
|
|
39
|
+
.te-seg button { flex: 1; border: none; background: none; font: 600 11px Onest, Inter, sans-serif; color: #6f6f78; padding: 5px 0; border-radius: 7px; cursor: pointer; }
|
|
40
|
+
.te-seg button[aria-pressed="true"] { background: #17171a; color: #fafaf8; }
|
|
41
|
+
.te-foot { display: flex; gap: 8px; padding: 10px 14px; border-top: 1px solid rgba(23, 23, 26, 0.1); flex-shrink: 0; }
|
|
42
|
+
.te-foot button { flex: 1; font: 600 11.5px Onest, Inter, sans-serif; border-radius: 8px; padding: 7px 0; cursor: pointer; }
|
|
43
|
+
.te-copy { background: #17171a; color: #fafaf8; border: 1px solid #17171a; }
|
|
44
|
+
.te-link { background: #ffffff; color: #17171a; border: 1px solid rgba(23, 23, 26, 0.1); }
|
|
45
45
|
@media (prefers-reduced-motion: reduce) {
|
|
46
46
|
.te-card, .te-pill { transition: none; }
|
|
47
47
|
}
|
|
48
48
|
@media (max-width: 640px) {
|
|
49
|
-
.te-card { top: auto; right: 0; left: 0; bottom: 0; width: auto; max-height: 72vh; border-radius: 18px 18px 0 0; border-left: none; border-right: none; border-bottom: none; padding-bottom: env(safe-area-inset-bottom, 0px); transform-origin: bottom center; box-shadow: 0 -16px 44px rgba(
|
|
49
|
+
.te-card { top: auto; right: 0; left: 0; bottom: 0; width: auto; max-height: 72vh; border-radius: 18px 18px 0 0; border-left: none; border-right: none; border-bottom: none; padding-bottom: env(safe-area-inset-bottom, 0px); transform-origin: bottom center; box-shadow: 0 -16px 44px rgba(23, 23, 26, 0.3); }
|
|
50
50
|
.te-card[data-closed="true"] { transform: translateY(105%); opacity: 1; }
|
|
51
51
|
.te-pill { top: auto; bottom: calc(env(safe-area-inset-bottom, 0px) + 16px); }
|
|
52
|
-
.te-grab { display: block; width: 40px; height: 4px; border-radius: 2px; background:
|
|
52
|
+
.te-grab { display: block; width: 40px; height: 4px; border-radius: 2px; background: rgba(23, 23, 26, 0.18); margin: 8px auto 0; flex-shrink: 0; }
|
|
53
53
|
.te-x { padding: 6px 10px; }
|
|
54
54
|
}
|
|
55
55
|
`;
|