lavish-axi 0.1.32 → 0.1.34

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/README.md CHANGED
@@ -33,7 +33,7 @@ That loses the thing HTML is best at: interactivity.
33
33
 
34
34
  Lavish Editor opens agent-generated HTML files in a local browser, lets you pinpoint elements or selected text and send feedback to the agent to address.
35
35
 
36
- - **Local only** - Work with your local HTML artifacts with a local CLI. Zero cloud dependency.
36
+ - **Local-first** - Review local HTML artifacts with a local CLI and no cloud dependency in the core feedback loop; hosted sharing through third-party ht-ml.app is explicit and opt-in.
37
37
  - **Human-AI collaboration** - Annotate elements, selected text ranges, and send messages to the agent without leaving Lavish Editor.
38
38
  - **Battery included** - Lavish Editor teaches your agent good visualization for common use cases such as product or technical plans, design explorations and more out of the box.
39
39
 
@@ -88,7 +88,7 @@ npm install -g lavish-axi
88
88
  lavish-axi setup hooks
89
89
  ```
90
90
 
91
- This installs a `SessionStart` hook for **Claude Code**, **Codex**, and **OpenCode** that surfaces open sessions, visualization playbooks, and usage guidance at the start of each session.
91
+ This installs a `SessionStart` hook for **Claude Code**, **Codex**, **OpenCode**, and **GitHub Copilot CLI** that surfaces open sessions, visualization playbooks, and usage guidance at the start of each session.
92
92
  Unlike the skill, the hook also shows your live open sessions, so a fresh agent session can resume an in-flight review.
93
93
  **Restart your agent session after running this** so the new hook takes effect.
94
94
 
@@ -125,6 +125,7 @@ pnpm link
125
125
  ┌────────────────────────┐
126
126
  │ lavish-axi poll waits │
127
127
  │ and returns prompts │
128
+ │ or layout warnings │
128
129
  └────────────────────────┘
129
130
  ```
130
131
 
@@ -138,38 +139,48 @@ pnpm link
138
139
  - **Open-time layout gate** - The browser chrome masks each artifact until the real in-iframe layout audit reports no error-severity findings.
139
140
  Warning-only artifacts reveal normally; error findings notify the agent through the same `layout_warnings` poll path and keep the curtain up until a clean reload.
140
141
  The user can click **Show anyway**, and a bounded safety timeout reveals with a persistent layout-issues banner so review is never blocked indefinitely.
141
- - **Layout warnings** - After fonts load and layout settles, the injected SDK audits the real browser render for page horizontal overflow, element overflow, clipped text, and overlapping text.
142
- Intentional horizontal scrollers using `overflow-x: auto` or `scroll` are excluded.
143
- Fresh warnings are returned from `lavish-axi poll` as `layout_warnings` with `selector`, `kind`, `overflowPx`, `viewportWidth`, and `severity`, so agents can fix unreadable layouts before asking the human to review.
142
+ - **Layout warnings** - After fonts load and layout settles, the injected SDK audits the real browser render for page horizontal overflow, element overflow, clipped or visibly spilling text, and overlapping text.
143
+ Intentional horizontal scrollers using `overflow-x: auto` or `scroll` are excluded from horizontal checks, and `overflow-y: auto` or `scroll` is treated as intentional for vertical overflow.
144
+ Current findings are returned from `lavish-axi poll` as `layout_warnings` with `selector`, `kind`, `overflowPx`, `viewportWidth`, `severity`, and `persistent`.
145
+ Fresh error-severity findings should be fixed and rechecked before asking the human to review; repeated or warning-only findings can be surfaced to the human with a note when the cause is not obvious.
144
146
  - **Local assets** - Copy local images, CSS, fonts, and scripts next to the HTML artifact and reference them with relative paths from that directory; root-prefixed paths such as `/assets/logo.png` will not resolve through Lavish's artifact route.
147
+ - **Export and sharing** - `lavish-axi export` writes `<name>.export.html` by inlining local assets only, stripping the annotation SDK, and leaving remote CDN/font references as links that still need network access.
148
+ `lavish-axi share` publishes the same local-inlined HTML to [ht-ml.app](https://ht-ml.app), a third-party hosting service not part of Lavish.
149
+ Publishing sends the artifact to ht-ml.app's servers, public by default, or private and password-protected with `--password`; the response includes a secret `update_key` shown once for later management.
150
+ Bundling never fetches remote URLs, Lavish itself does not set a CSP, local reads stay confined and size-capped, and absolute `file://` paths outside safe inlined asset references are redacted before output.
151
+ Per-asset and per-bundle inline caps default to 10 MB and 25 MB, overridable with `LAVISH_AXI_EXPORT_MAX_ASSET_BYTES` and `LAVISH_AXI_EXPORT_MAX_BUNDLE_BYTES`.
152
+ Unresolved local assets or export notices such as author-set CSP meta tags and redacted file URLs are surfaced in command or browser output.
153
+ Use `--token` or `LAVISH_AXI_HTML_APP_TOKEN` for an optional bearer token; set `LAVISH_AXI_HTML_APP_API_URL` only when overriding the ht-ml.app API base.
145
154
  - **Live reload** - Lavish watches the HTML artifact file by default and preserves the artifact iframe scroll position across reloads. To also reload on sibling asset changes, add `data-lavish-live-reload-root` to the root element or `<meta name="lavish-live-reload" content="root">`.
146
155
  - **Feedback controls** - Native controls (radios, checkboxes, inputs, selects, buttons, labels, disclosure summaries, contenteditable) are interactive automatically, so they do not need `data-lavish-action`.
147
156
  For reversible choices, let option clicks update local state, then queue exactly one final answer from a per-question submit or Queue answer button with `window.lavish.queuePrompt()`.
148
157
  Mark only custom (non-native) clickable elements with `data-lavish-action` so Lavish does not annotate them, and use `data-lavish-question` or `queueKey` when pre-send updates for the same question should replace each other.
149
- The browser chrome keeps editing actions in the overflow menu (copy path, reload artifact, copy DOM snapshot, end session) and can submit queued prompts with **Send & end session**, which delivers the prompts before ending the session.
158
+ The browser chrome keeps editing actions in the overflow menu (copy path, reload artifact, copy DOM snapshot, export standalone HTML, publish link, end session) and can submit queued prompts with **Send & end session**, which delivers the prompts before ending the session.
150
159
  - **Keyboard shortcuts** - In the chrome composer, Enter sends queued prompts and Shift+Enter inserts a newline.
151
160
  In the annotation card, Enter queues the annotation, Shift+Enter inserts a newline, and Ctrl+Enter (Cmd+Enter on macOS) queues it and sends all queued prompts immediately.
152
161
  - **Agent presence** - The browser shows when no agent is listening, keeps queued feedback and fresh layout warnings for the next successful `lavish-axi poll` send even across reloads, and only blocks human sends while the agent is working on delivered feedback. The no-timeout poll writes an immediate stderr banner and periodic stderr heartbeats while stdout stays reserved for the final response; if the poll is interrupted or times out, re-run it because queued feedback is never lost.
153
162
  - **Precise targets** - Text annotations include selected text plus range anchors, so agents are not limited to whole-element selectors.
154
163
  - **Server cleanup** - The detached server stops after the last session ends when nothing is connected, or after `LAVISH_AXI_IDLE_TIMEOUT_MS` (default 30 minutes) with no browser or poll connections.
155
164
  Set `LAVISH_AXI_IDLE_TIMEOUT_MS=0` or `off` to disable idle self-shutdown.
156
- - **Local-first state** - Session state stays under `.lavish-axi/` in the workspace.
165
+ - **Local-first state** - Session state stays under `~/.lavish-axi/` by default, or `LAVISH_AXI_STATE_DIR` when set.
157
166
  - **Network binding** - The server binds to loopback (`127.0.0.1`) by default. Set `LAVISH_AXI_HOST` to bind elsewhere; a wildcard (`0.0.0.0` or `::`) binds every interface. Binding beyond loopback exposes an unauthenticated server that can read and serve arbitrary local files to anything that can reach it, so only do so on a trusted network. Set `LAVISH_AXI_LINK_HOST` to control the hostname written into generated session links (defaults to the bind address, or loopback when bound to a wildcard).
158
167
 
159
168
  ## CLI Reference
160
169
 
161
- | Command | Description |
162
- | ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
163
- | `lavish-axi` | Show current sessions and usage guidance. |
164
- | `lavish-axi update` | Check for or apply the latest npm release through the AXI SDK self-updater. |
165
- | `lavish-axi <html-file>` | Open or resume a Lavish Editor session, with the open-time layout gate enabled by default. |
166
- | `lavish-axi poll <html-file>` | Long-poll until the user sends feedback, ends the session, or the browser reports fresh `layout_warnings`; leave no-timeout polls running, or re-run them if interrupted. |
167
- | `lavish-axi end <html-file>` | End a session. |
168
- | `lavish-axi stop` | Shut down the background server. |
169
- | `lavish-axi playbook [id]` | List focused artifact guidance or show one playbook; agents must open each matching playbook before writing HTML. |
170
- | `lavish-axi design` | Show the Tailwind + DaisyUI CDN fallback, content-to-playbook router, Mermaid diagram tooling, `luxury` default theme, DaisyUI `@apply` warning, and layout safety snippet. |
171
- | `lavish-axi setup hooks` | Install or repair optional SessionStart hooks for Claude Code, Codex, and OpenCode; restart the agent session afterward. |
172
- | `lavish-axi server` | Run the local Lavish Editor server. |
170
+ | Command | Description |
171
+ | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
172
+ | `lavish-axi` | Show current sessions and usage guidance. |
173
+ | `lavish-axi update` | Check for or apply the latest npm release through the AXI SDK self-updater. |
174
+ | `lavish-axi <html-file>` | Open or resume a Lavish Editor session, with the open-time layout gate enabled by default. |
175
+ | `lavish-axi poll <html-file>` | Long-poll until the user sends feedback, ends the session, or the browser reports fresh `layout_warnings`; leave no-timeout polls running, or re-run them if interrupted. |
176
+ | `lavish-axi end <html-file>` | End a session. |
177
+ | `lavish-axi export <html-file>` | Write a portable copy of the artifact: one HTML file with its local assets inlined, so it opens with no server and no sibling files. Remote CDN/font references are left as links. |
178
+ | `lavish-axi share <html-file>` | Publish the artifact (local assets inlined) to [ht-ml.app](https://ht-ml.app), a third-party host not part of Lavish, and print a visitable URL plus a secret update key; shares are public by default, and `--password` makes viewers enter the password before viewing. |
179
+ | `lavish-axi stop` | Shut down the background server. |
180
+ | `lavish-axi playbook [id]` | List focused artifact guidance or show one playbook; agents must open each matching playbook before writing HTML. |
181
+ | `lavish-axi design` | Show the Tailwind + DaisyUI CDN fallback, content-to-playbook router, Mermaid diagram tooling, `luxury` default theme, DaisyUI `@apply` warning, and layout safety snippet. |
182
+ | `lavish-axi setup hooks` | Install or repair optional SessionStart hooks for Claude Code, Codex, OpenCode, and GitHub Copilot CLI; restart the agent session afterward. |
183
+ | `lavish-axi server` | Run the local Lavish Editor server. |
173
184
 
174
185
  Known playbook IDs: `diagram`, `table`, `comparison`, `plan`, `code`, `input`, `slides`.
175
186
  One artifact often combines several playbooks, such as a plan that includes a comparison and a diagram, so agents must match against each `use_when` trigger and open every matching playbook before writing HTML.
@@ -182,6 +193,9 @@ For flows, architecture, state, or sequence diagrams, open the diagram playbook
182
193
  | `lavish-axi <html-file>` | `--no-open` | Ensure the server/session exists without opening another browser window. |
183
194
  | `lavish-axi <html-file>` | `--no-gate` | Skip the open-time layout curtain for this browser open. |
184
195
  | `lavish-axi update` | `--check` | Report current vs latest npm version without installing an update. |
196
+ | `lavish-axi export` | `--out <path>` | Write the export to a specific path instead of `<name>.export.html` next to the source. |
197
+ | `lavish-axi share` | `--password <pw>` | Make the third-party ht-ml.app page private; viewers must supply the password. |
198
+ | `lavish-axi share` | `--token <t>` | Attach an optional bearer token (`LAVISH_AXI_HTML_APP_TOKEN`); never required to publish. |
185
199
  | `lavish-axi poll` | `--agent-reply "..."` | Show the agent's reply in the existing browser chat before polling again. |
186
200
  | `lavish-axi poll` | `--timeout-ms <ms>` | Test/debug escape hatch only; agents should normally omit it and leave the long poll running. |
187
201
  | `lavish-axi stop` | `--port <port>` | Shut down a server running on a non-default port. |
@@ -24,6 +24,20 @@ const moreButton = /** @type {HTMLButtonElement} */ (document.getElementById("mo
24
24
  const moreMenu = /** @type {HTMLDivElement} */ (document.getElementById("moreMenu"));
25
25
  const reloadArtifactButton = /** @type {HTMLButtonElement} */ (document.getElementById("reloadArtifact"));
26
26
  const copySnapshotButton = /** @type {HTMLButtonElement} */ (document.getElementById("copySnapshot"));
27
+ const exportArtifactButton = /** @type {HTMLButtonElement} */ (document.getElementById("exportArtifact"));
28
+ const shareArtifactButton = /** @type {HTMLButtonElement} */ (document.getElementById("shareArtifact"));
29
+ const shareDialog = /** @type {HTMLDivElement} */ (document.getElementById("shareDialog"));
30
+ const shareForm = /** @type {HTMLFormElement} */ (document.getElementById("shareForm"));
31
+ const shareCloseButton = /** @type {HTMLButtonElement} */ (document.getElementById("shareClose"));
32
+ const shareCancelButton = /** @type {HTMLButtonElement} */ (document.getElementById("shareCancel"));
33
+ const sharePublishButton = /** @type {HTMLButtonElement} */ (document.getElementById("sharePublish"));
34
+ const sharePasswordInput = /** @type {HTMLInputElement} */ (document.getElementById("sharePassword"));
35
+ const shareStatus = /** @type {HTMLDivElement} */ (document.getElementById("shareStatus"));
36
+ const shareResult = /** @type {HTMLDivElement} */ (document.getElementById("shareResult"));
37
+ const shareUrlInput = /** @type {HTMLInputElement} */ (document.getElementById("shareUrl"));
38
+ const shareUpdateKeyInput = /** @type {HTMLInputElement} */ (document.getElementById("shareUpdateKey"));
39
+ const copyShareUrlButton = /** @type {HTMLButtonElement} */ (document.getElementById("copyShareUrl"));
40
+ const copyUpdateKeyButton = /** @type {HTMLButtonElement} */ (document.getElementById("copyUpdateKey"));
27
41
  const endButton = /** @type {HTMLButtonElement} */ (document.getElementById("end"));
28
42
  const copyPathButton = /** @type {HTMLButtonElement} */ (document.getElementById("copyPath"));
29
43
  const copyHint = /** @type {HTMLSpanElement} */ (document.getElementById("copyHint"));
@@ -109,7 +123,7 @@ function render() {
109
123
  escapeHtml(prompt.prompt) +
110
124
  '</span><button class="pill-close" type="button" aria-label="Remove queued prompt" data-index="' +
111
125
  index +
112
- '">×</button></div><div class="pill-tooltip">' +
126
+ '"><svg width="10" height="10" viewBox="0 0 10 10" fill="none" aria-hidden="true" focusable="false"><path d="M1 1L9 9M9 1L1 9" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg></button></div><div class="pill-tooltip">' +
113
127
  (prompt.selector
114
128
  ? '<div class="tooltip-label">Target</div><div class="pill-tooltip-target">' +
115
129
  escapeHtml(prompt.selector) +
@@ -489,6 +503,128 @@ function copyDomSnapshot() {
489
503
  requestSnapshot("copy");
490
504
  }
491
505
 
506
+ function exportFileName() {
507
+ const base = (filePath.split(/[\\/]/).pop() || "artifact.html").replace(/\.html?$/i, "");
508
+ return (base || "artifact") + ".export.html";
509
+ }
510
+
511
+ function setExportLabel(text) {
512
+ const label = exportArtifactButton.querySelector("span");
513
+ if (label) label.textContent = text;
514
+ }
515
+
516
+ function unresolvedAssetText(count) {
517
+ return count === 1 ? "1 unresolved asset" : `${count} unresolved assets`;
518
+ }
519
+
520
+ function noticeText(count) {
521
+ return count === 1 ? "1 notice" : `${count} notices`;
522
+ }
523
+
524
+ function exportWarningText(unresolvedCount, noticeCount) {
525
+ if (unresolvedCount > 0 && noticeCount > 0) {
526
+ return `${unresolvedAssetText(unresolvedCount)} and ${noticeText(noticeCount)}`;
527
+ }
528
+ if (unresolvedCount > 0) return unresolvedAssetText(unresolvedCount);
529
+ return noticeText(noticeCount);
530
+ }
531
+
532
+ async function exportArtifact() {
533
+ // The bundle inlines local assets server-side, so it can take a moment - keep the menu open
534
+ // and narrate progress in place instead of closing it and leaving the user with no feedback.
535
+ exportArtifactButton.disabled = true;
536
+ setExportLabel("Exporting...");
537
+ try {
538
+ const response = await fetch("/api/" + key + "/export");
539
+ if (!response.ok) throw new Error("export failed");
540
+ const warningCount = Number(response.headers.get("x-lavish-export-warning-count") || "0");
541
+ const noticeCount = Number(response.headers.get("x-lavish-export-notice-count") || "0");
542
+ const blob = await response.blob();
543
+ const url = URL.createObjectURL(blob);
544
+ const link = document.createElement("a");
545
+ link.href = url;
546
+ link.download = exportFileName();
547
+ document.body.appendChild(link);
548
+ link.click();
549
+ link.remove();
550
+ setTimeout(() => URL.revokeObjectURL(url), 2000);
551
+ if (warningCount > 0 || noticeCount > 0) {
552
+ setExportLabel(`Exported with ${exportWarningText(warningCount, noticeCount)}`);
553
+ } else {
554
+ setExportLabel("Export standalone HTML");
555
+ closeMenus();
556
+ }
557
+ } catch {
558
+ setExportLabel("Export failed - retry");
559
+ } finally {
560
+ exportArtifactButton.disabled = false;
561
+ }
562
+ }
563
+
564
+ function openShareDialog() {
565
+ closeMenus();
566
+ shareDialog.hidden = false;
567
+ shareStatus.textContent = "";
568
+ shareStatus.classList.remove("error");
569
+ shareResult.hidden = true;
570
+ sharePasswordInput.value = "";
571
+ sharePasswordInput.focus();
572
+ }
573
+
574
+ function closeShareDialog() {
575
+ shareDialog.hidden = true;
576
+ }
577
+
578
+ async function copyToButton(value, button, label) {
579
+ await copyText(value);
580
+ button.textContent = "Copied";
581
+ setTimeout(() => {
582
+ button.textContent = label;
583
+ }, 1200);
584
+ }
585
+
586
+ async function publishShare(event) {
587
+ event.preventDefault();
588
+ sharePublishButton.disabled = true;
589
+ shareStatus.classList.remove("error");
590
+ shareStatus.textContent = "Publishing to ht-ml.app...";
591
+ shareResult.hidden = true;
592
+ const password = sharePasswordInput.value.trim();
593
+ const passwordProtected = Boolean(password);
594
+ try {
595
+ const response = await fetch("/api/" + key + "/share", {
596
+ method: "POST",
597
+ headers: { "content-type": "application/json" },
598
+ body: JSON.stringify(password ? { password } : {}),
599
+ });
600
+ const data = await response.json();
601
+ if (!response.ok) throw new Error(data.error || "publish failed");
602
+ shareUrlInput.value = data.url || "";
603
+ shareUpdateKeyInput.value = data.update_key || "";
604
+ const unresolvedAssets = Array.isArray(data.unresolved_local_assets) ? data.unresolved_local_assets : [];
605
+ const notices = Array.isArray(data.notices) ? data.notices : [];
606
+ const warningCount = unresolvedAssets.length;
607
+ const noticeCount = notices.length;
608
+ const noticeSummary = noticeCount ? noticeText(noticeCount) : "";
609
+ shareStatus.textContent =
610
+ warningCount > 0
611
+ ? `Published with ${warningCount === 1 ? "1 unresolved local asset" : `${warningCount} unresolved local assets`}${noticeSummary ? ` and ${noticeSummary}` : ""}.${passwordProtected ? " This page is PASSWORD-PROTECTED; viewers also need the password." : ""}`
612
+ : noticeCount > 0
613
+ ? `Published with ${noticeSummary}.${passwordProtected ? " This page is PASSWORD-PROTECTED; viewers also need the password." : ""}`
614
+ : passwordProtected
615
+ ? "Published. This page is PASSWORD-PROTECTED; viewers also need the password."
616
+ : "Published. Anyone with the link can view this page.";
617
+ shareResult.hidden = false;
618
+ shareUrlInput.focus();
619
+ shareUrlInput.select();
620
+ } catch (error) {
621
+ shareStatus.classList.add("error");
622
+ shareStatus.textContent = error instanceof Error ? error.message : String(error);
623
+ } finally {
624
+ sharePublishButton.disabled = false;
625
+ }
626
+ }
627
+
492
628
  function resetFrame() {
493
629
  startLayoutGateCycle();
494
630
  // The iframe is sandboxed, so reload by resetting the iframe URL from chrome.
@@ -575,6 +711,16 @@ chatInput.addEventListener("input", hideSendHint);
575
711
  copyPathButton.onclick = copyFilePath;
576
712
  reloadArtifactButton.onclick = reloadArtifact;
577
713
  copySnapshotButton.onclick = copyDomSnapshot;
714
+ exportArtifactButton.onclick = exportArtifact;
715
+ shareArtifactButton.onclick = openShareDialog;
716
+ shareCloseButton.onclick = closeShareDialog;
717
+ shareCancelButton.onclick = closeShareDialog;
718
+ shareForm.addEventListener("submit", publishShare);
719
+ shareDialog.addEventListener("click", (event) => {
720
+ if (event.target === shareDialog) closeShareDialog();
721
+ });
722
+ copyShareUrlButton.onclick = () => copyToButton(shareUrlInput.value, copyShareUrlButton, "Copy URL");
723
+ copyUpdateKeyButton.onclick = () => copyToButton(shareUpdateKeyInput.value, copyUpdateKeyButton, "Copy key");
578
724
  endButton.onclick = () => {
579
725
  closeMenus();
580
726
  endSession();
@@ -585,7 +731,13 @@ document.addEventListener("mousedown", (event) => {
585
731
  if (!sendMenu.hidden && !sendActions.contains(target)) setMenuOpen(sendCaret, sendMenu, false);
586
732
  });
587
733
  document.addEventListener("keydown", (event) => {
588
- if (event.key === "Escape") closeMenus();
734
+ if (event.key === "Escape") {
735
+ if (!shareDialog.hidden) {
736
+ closeShareDialog();
737
+ } else {
738
+ closeMenus();
739
+ }
740
+ }
589
741
  });
590
742
  frame.addEventListener("load", () => {
591
743
  postToFrame({ type: "lavish:setAnnotationMode", enabled: annotation && !ended });
package/dist/chrome.css CHANGED
@@ -424,6 +424,193 @@ body.lavish {
424
424
  width: 208px;
425
425
  border-radius: 11px;
426
426
  }
427
+ .share-overlay {
428
+ position: fixed;
429
+ inset: 0;
430
+ z-index: 80;
431
+ display: grid;
432
+ place-items: center;
433
+ padding: var(--space-16);
434
+ background: rgba(15, 17, 21, 0.72);
435
+ }
436
+ .share-overlay[hidden] {
437
+ display: none;
438
+ }
439
+ .share-card {
440
+ width: min(520px, 100%);
441
+ max-height: min(760px, calc(100vh - 32px));
442
+ overflow: auto;
443
+ border: 1px solid var(--border-strong);
444
+ border-radius: var(--radius-xl);
445
+ background: var(--bg-panel);
446
+ color: var(--fg);
447
+ box-shadow: var(--shadow-floating);
448
+ padding: var(--space-12);
449
+ }
450
+ .share-head {
451
+ display: flex;
452
+ align-items: flex-start;
453
+ justify-content: space-between;
454
+ gap: var(--space-8);
455
+ margin-bottom: var(--space-8);
456
+ }
457
+ .share-kicker {
458
+ color: var(--fg-label);
459
+ font-size: var(--text-xs);
460
+ font-weight: var(--w-bold);
461
+ letter-spacing: var(--track-label);
462
+ text-transform: uppercase;
463
+ }
464
+ .share-link {
465
+ color: inherit;
466
+ text-decoration: underline;
467
+ text-decoration-color: var(--border-strong);
468
+ text-underline-offset: 2px;
469
+ }
470
+ .share-link:hover {
471
+ color: var(--accent-hover);
472
+ text-decoration-color: currentColor;
473
+ }
474
+ .share-head h2 {
475
+ margin: var(--space-2) 0 0;
476
+ font-family: var(--font-serif);
477
+ font-size: var(--text-2xl);
478
+ font-style: italic;
479
+ font-weight: var(--w-regular);
480
+ line-height: var(--lh-2xl);
481
+ }
482
+ .share-close {
483
+ display: flex;
484
+ align-items: center;
485
+ justify-content: center;
486
+ width: 30px;
487
+ height: 30px;
488
+ border: 1px solid var(--border);
489
+ border-radius: var(--radius-pill);
490
+ background: transparent;
491
+ color: var(--fg-muted);
492
+ cursor: pointer;
493
+ font: inherit;
494
+ padding: 0;
495
+ }
496
+ .share-close svg {
497
+ display: block;
498
+ }
499
+ .share-close:hover {
500
+ color: var(--fg);
501
+ border-color: var(--border-strong);
502
+ }
503
+ .share-copy,
504
+ .share-note {
505
+ margin: 0 0 var(--space-8);
506
+ color: var(--fg-muted);
507
+ }
508
+ .share-note {
509
+ color: var(--fg-faint);
510
+ font-size: var(--text-sm);
511
+ }
512
+ .share-grid {
513
+ display: grid;
514
+ gap: var(--space-8);
515
+ margin-top: var(--space-10);
516
+ }
517
+ .share-grid label,
518
+ .share-result label {
519
+ display: grid;
520
+ gap: var(--space-4);
521
+ min-width: 0;
522
+ color: var(--fg-muted);
523
+ font-size: var(--text-xs);
524
+ font-weight: var(--w-bold);
525
+ letter-spacing: var(--track-label);
526
+ text-transform: uppercase;
527
+ }
528
+ .share-grid input,
529
+ .share-result input {
530
+ min-width: 0;
531
+ width: 100%;
532
+ border: 1px solid var(--border);
533
+ border-radius: var(--radius-lg);
534
+ background: var(--bg);
535
+ color: var(--fg);
536
+ font: inherit;
537
+ font-size: var(--text-base);
538
+ font-weight: var(--w-regular);
539
+ letter-spacing: 0;
540
+ padding: 10px;
541
+ text-transform: none;
542
+ }
543
+ .share-grid input::placeholder {
544
+ color: var(--fg-label);
545
+ }
546
+ .share-status {
547
+ min-height: 20px;
548
+ margin-top: var(--space-8);
549
+ color: var(--fg-muted);
550
+ font-size: var(--text-sm);
551
+ }
552
+ .share-status.error {
553
+ color: var(--danger);
554
+ }
555
+ .share-result {
556
+ display: grid;
557
+ gap: var(--space-8);
558
+ margin-top: var(--space-8);
559
+ border: 1px solid var(--border-subtle);
560
+ border-radius: var(--radius-lg);
561
+ background: var(--bg-elevated);
562
+ padding: var(--space-8);
563
+ }
564
+ .share-result[hidden] {
565
+ display: none;
566
+ }
567
+ .share-copy-row {
568
+ display: flex;
569
+ gap: var(--space-6);
570
+ min-width: 0;
571
+ }
572
+ .share-copy-row input {
573
+ font-family: var(--font-mono);
574
+ font-size: var(--text-xs);
575
+ }
576
+ .share-copy-btn {
577
+ flex: none;
578
+ border: 1px solid var(--border);
579
+ border-radius: var(--radius-md);
580
+ background: transparent;
581
+ color: var(--fg-muted);
582
+ font: inherit;
583
+ font-size: var(--text-xs);
584
+ font-weight: var(--w-bold);
585
+ padding: 0 10px;
586
+ cursor: pointer;
587
+ white-space: nowrap;
588
+ }
589
+ .share-copy-btn:hover {
590
+ color: var(--fg);
591
+ border-color: var(--border-strong);
592
+ }
593
+ .share-actions {
594
+ display: flex;
595
+ justify-content: flex-end;
596
+ gap: var(--space-8);
597
+ margin-top: var(--space-12);
598
+ }
599
+ .share-cancel {
600
+ border: 1px solid var(--border);
601
+ border-radius: var(--radius-md);
602
+ background: transparent;
603
+ color: var(--fg-muted);
604
+ font-family: inherit;
605
+ font-size: var(--text-sm);
606
+ font-weight: var(--w-bold);
607
+ padding: 9px 14px;
608
+ cursor: pointer;
609
+ }
610
+ .share-cancel:hover {
611
+ color: var(--fg);
612
+ border-color: var(--border-strong);
613
+ }
427
614
  .layout {
428
615
  height: calc(100vh - var(--bar-h));
429
616
  min-height: 0;
@@ -565,6 +752,9 @@ body.lavish {
565
752
  text-overflow: ellipsis;
566
753
  }
567
754
  .pill-close {
755
+ display: flex;
756
+ align-items: center;
757
+ justify-content: center;
568
758
  width: 18px;
569
759
  height: 18px;
570
760
  border: 0;
@@ -572,10 +762,11 @@ body.lavish {
572
762
  padding: 0;
573
763
  background: var(--border);
574
764
  color: var(--fg-muted);
575
- line-height: 18px;
576
- font-size: 14px;
577
765
  cursor: pointer;
578
766
  }
767
+ .pill-close svg {
768
+ display: block;
769
+ }
579
770
  .pill-tooltip {
580
771
  display: none;
581
772
  position: absolute;
@@ -739,4 +930,11 @@ body.layout-gate-active iframe#artifact {
739
930
  .pill-preview {
740
931
  max-width: min(220px, 70vw);
741
932
  }
933
+ .share-overlay {
934
+ align-items: start;
935
+ padding: var(--space-8);
936
+ }
937
+ .share-copy-row {
938
+ flex-direction: column;
939
+ }
742
940
  }