lavish-axi 0.1.58 → 0.1.60
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 +32 -25
- package/dist/chrome-client.js +203 -38
- package/dist/chrome.css +23 -0
- package/dist/cli.mjs +954 -115
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/skills/lavish/SKILL.md +11 -65
package/README.md
CHANGED
|
@@ -53,7 +53,7 @@ npx skills add kunchenguid/lavish-axi --skill lavish
|
|
|
53
53
|
|
|
54
54
|
That is the entire setup - no npm install needed.
|
|
55
55
|
The skill teaches your agent to run Lavish through `npx -y lavish-axi`, so the CLI comes along on demand.
|
|
56
|
-
|
|
56
|
+
It stays a short stub and sends the agent to `npx -y lavish-axi --help`, `design`, and `playbook` for current instructions, so an installed copy cannot go stale against a newer CLI.
|
|
57
57
|
Its frontmatter also includes Hermes Agent metadata, so Hermes-compatible harnesses can categorize and surface it as a first-class productivity skill.
|
|
58
58
|
This installs the public `lavish` skill.
|
|
59
59
|
The repository also contains an internal `lavish-design` brand skill for maintainers; default `npx skills add ... --list` and skills.sh discovery hide it unless `INSTALL_INTERNAL_SKILLS=1` is set.
|
|
@@ -166,9 +166,9 @@ pnpm link
|
|
|
166
166
|
- **Self-paint warning** - `lavish-axi <html-file>`, `export`, and `share` run a render-free check for artifacts missing an explicit page background and return a one-line `self_paint_warning`.
|
|
167
167
|
The check fails open - any stylesheet link, `@import`, Tailwind runtime script, `color-scheme`, or `html`/`body`/`:root` background signal suppresses it - and it never blocks the open.
|
|
168
168
|
- **Open-time layout gate** - The browser chrome masks an artifact only while the real in-iframe audit waits for fonts and final geometry.
|
|
169
|
-
The first completed check reveals the artifact, whatever it found
|
|
170
|
-
The user can click **Show anyway**, and a bounded safety timeout fails open
|
|
171
|
-
If the review cannot load at all - the chrome's own script never runs, or the server does not answer the artifact's load request after several retries - the mask
|
|
169
|
+
The first completed client-side check reveals the artifact, whatever it found and even if reporting that check to the server fails; the gate never holds the review hostage waiting for a repair or a network round-trip.
|
|
170
|
+
The user can click **Show anyway**, and a bounded safety timeout fails open from every gate state.
|
|
171
|
+
If the review cannot load at all - the chrome's own script never runs, or the server does not answer the artifact's load request after several retries - the mask names the problem and offers **Check and reload** without removing the independent **Show anyway** escape.
|
|
172
172
|
A review already loaded in another browser tab is named the same way, with a **Take over here** button that moves it into the current tab, because Lavish loads an artifact in one tab at a time.
|
|
173
173
|
- **Layout issues inbox** - Detection is passive. After fonts and finite animations settle, the injected SDK confirms severe failures from direct rendered evidence such as materially escaped meaningful content or required controls, clipped text fragments, viewport reachability, or near-total semantic occlusion.
|
|
174
174
|
Explicit ellipsis and line clamp, standard visually hidden accessibility text, intentional scrollers or masks, parent overhang, generic element scroll geometry, decorative overlap, and uncertain motion do not produce findings by themselves.
|
|
@@ -184,11 +184,14 @@ pnpm link
|
|
|
184
184
|
- **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.
|
|
185
185
|
- **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.
|
|
186
186
|
`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.
|
|
187
|
-
Publishing sends the artifact to ht-ml.app's servers, public by default, or private and password-protected with `--password
|
|
187
|
+
Publishing sends the artifact to ht-ml.app's servers, public by default, or private and password-protected with `--private` (Lavish generates the password and returns it once, in the command output and in the browser publish dialog) or `--password <pw>` (one you supply, never echoed back). A generated password is a shared secret: give it to whoever should read the page, and expect it to appear in your agent's transcript, because the agent has to relay it. Lavish does not store it.
|
|
188
|
+
The response includes a secret `update_key` shown once. Keep it to republish the same URL later with `--site <site_id> --update-key <key>`, which replaces the HTML in place and leaves the password alone unless you pass `--private` (rotate to a new generated one) or `--password <pw>` (set one). Locking a page that was public is not instant - it can keep answering from ht-ml.app's CDN cache for minutes after the password is set - while a page that was already private has no such cached copy.
|
|
189
|
+
A page's password cannot be removed: ht-ml.app accepts a request to clear one and silently ignores it, so Lavish offers no way to make a private page public rather than telling you it did something the host did not do. Republish as a new page if you need a public URL.
|
|
190
|
+
ht-ml.app has no delete endpoint, so `--unpublish --site <site_id> --update-key <key>` replaces the page with a short placeholder and locks it behind a discarded random password rather than removing it: the URL still resolves and the host still holds what was published. The content swap is immediate, but the lock is not: a page that was public can keep serving the placeholder to uncredentialed visitors from ht-ml.app's CDN cache for minutes afterwards. The `update_key` plus `--private` republishes real content behind a new password you can share.
|
|
188
191
|
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.
|
|
189
192
|
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`.
|
|
190
193
|
Unresolved local assets or export notices such as author-set CSP meta tags and redacted file URLs are surfaced in command or browser output.
|
|
191
|
-
Use `--token` or `LAVISH_AXI_HTML_APP_TOKEN` for an optional bearer token; set `LAVISH_AXI_HTML_APP_API_URL` to override the ht-ml.app API base and point `share` at a backend you control (see [Self-hosting the share backend](docs/self-hosting-share.md) for the contract it must implement).
|
|
194
|
+
Use `--token` or `LAVISH_AXI_HTML_APP_TOKEN` for an optional bearer token when publishing a new page (a republish or `--unpublish` authorizes with the `update_key` instead and rejects `--token`); set `LAVISH_AXI_HTML_APP_API_URL` to override the ht-ml.app API base and point `share` at a backend you control (see [Self-hosting the share backend](docs/self-hosting-share.md) for the contract it must implement).
|
|
192
195
|
- **Live reload** - Lavish watches the HTML artifact file by default and preserves review context across reloads: the artifact iframe scroll position, an open annotation card's unsent text, and answers to `data-lavish-question` controls (application-owned form state is left alone). Unsent annotation text also survives a full reload of the review page itself. While a queued layout-issue batch is outstanding, closely spaced saves coalesce so one batch of fixes costs one refresh. 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">`.
|
|
193
196
|
If the element an unsent annotation was attached to is gone from the artifact for good, Lavish cannot reopen that card, so it writes your text into the conversation panel under **Unsent annotation** - selectable, never written over anything you have typed, and kept there across reloads; no note is ever dropped to make room for a newer one, and a note the browser refuses to store says so where it is shown.
|
|
194
197
|
- **Feedback controls** - Native controls (radios, checkboxes, inputs, selects, buttons, labels, disclosure summaries, contenteditable) are interactive automatically, so they do not need `data-lavish-action`.
|
|
@@ -237,27 +240,27 @@ pnpm link
|
|
|
237
240
|
- **Local-first state** - Session state stays under `~/.lavish-axi/` by default, or `LAVISH_AXI_STATE_DIR` when set.
|
|
238
241
|
- **Diagnostic viewports** - `LAVISH_AXI_DIAGNOSTIC_VIEWPORTS` sets which viewport classes the layout-issue inbox tracks (`mobile`, `compact`, `desktop`; comma-separated, default all). Warnings whose class leaves the set are marked obsolete with an explicit reason instead of silently reading as fixed.
|
|
239
242
|
- **Server port** - Set `LAVISH_AXI_PORT` to choose the server port; it defaults to `4387`.
|
|
240
|
-
- **Network binding** -
|
|
241
|
-
- **Allowed hosts** - To defend against DNS rebinding, the server rejects (`403`) any request whose `Host` header is missing or not one it answers to:
|
|
243
|
+
- **Network binding** - With Tailscale running, the review server automatically listens only on loopback (`127.0.0.1`) and this machine's Tailscale IPv4 address - never on `0.0.0.0` or every interface. The generated session link uses the machine's MagicDNS name, so it is the phone-ready URL to open from another device on the same tailnet. When Tailscale is absent or down, Lavish silently falls back to loopback-only and prints no phone URL. If Tailscale is running but MagicDNS is unavailable or its address cannot be bound after brief retries, Lavish visibly warns that phone access is unavailable and remains loopback-only. Binding beyond loopback exposes an unauthenticated server that can read and serve arbitrary local files to devices that can reach it, so the tailnet should be trusted. Any explicit `LAVISH_AXI_HOST` overrides automatic Tailscale binding; wildcard values are restricted to loopback, while a non-wildcard value selects that one concrete bind address. `LAVISH_AXI_LINK_HOST` controls the link host when automatic Tailscale binding is disabled.
|
|
244
|
+
- **Allowed hosts** - To defend against DNS rebinding, the server rejects (`403`) any request whose `Host` header is missing or not one it answers to: loopback names plus the concrete Tailscale IPv4 address and MagicDNS name when the Tailscale listener is successfully bound. If you configure a reverse proxy or another intentional hostname, list it in `LAVISH_AXI_ALLOWED_HOSTS` (whitespace-separated). Behind a reverse proxy, the forwarded `X-Forwarded-Host` is validated against the same list, so add the public hostname there and have the proxy send it together with `X-Forwarded-Proto`. Set `LAVISH_AXI_ALLOWED_HOSTS` to `*` to disable the check entirely, only when the server sits behind your own authentication or proxy. Mutating routes also reject a present foreign `Origin` or `Referer` (`403`); header-less CLI control requests remain allowed where supported.
|
|
242
245
|
- **Browser opening** - Set `LAVISH_AXI_NO_OPEN=1`, equivalent to `--no-open`, to create or resume a session without launching a browser window.
|
|
243
246
|
|
|
244
247
|
## CLI Reference
|
|
245
248
|
|
|
246
|
-
| Command | Description
|
|
247
|
-
| ------------------------------- |
|
|
248
|
-
| `lavish-axi` | Show current sessions and usage guidance.
|
|
249
|
-
| `lavish-axi update` | Check for or apply the latest npm release through the AXI SDK self-updater.
|
|
250
|
-
| `lavish-axi <html-file>` | Open or resume a Lavish Editor session, with the open-time layout gate enabled by default. Unresolved layout issues from earlier in the session are preserved. Refuses to reopen a session the user explicitly ended from the browser unless `--reopen` is passed.
|
|
251
|
-
| `lavish-axi poll <html-file>` | Long-poll until the user sends feedback or ends the session; detected layout issues wait in the user's Layout issues inbox and arrive only when queued. Leave no-timeout polls running, or re-run them if interrupted. Codex guidance keeps polls attached to the active turn. On `status: ended`, stop polling and do not reopen uninvited.
|
|
252
|
-
| `lavish-axi end <html-file>` | End a session as the agent; unlike a user-initiated end from the browser, this still allows a plain reopen later.
|
|
253
|
-
| `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.
|
|
254
|
-
| `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,
|
|
255
|
-
| `lavish-axi stop` | Shut down the background server.
|
|
256
|
-
| `lavish-axi playbook [id]` | List focused artifact guidance or show one playbook; agents must open each matching playbook before writing HTML.
|
|
257
|
-
| `lavish-axi design` | Show agent-facing design guidance, including optional CDN snippets and the whiteboard (Mermaid) opt-in snippet.
|
|
258
|
-
| `lavish-axi setup hooks` | Install or repair optional SessionStart hooks for Claude Code, Codex, OpenCode, and GitHub Copilot CLI; restart the agent session afterward.
|
|
259
|
-
| `lavish-axi setup plugin` | Register the installed package as an [Agent Plugin](https://agent-plugins.org) in VS Code, Cursor, and GitHub Copilot CLI; opt-in, idempotent, no marketplace involved. Reload each client afterward.
|
|
260
|
-
| `lavish-axi server` | Run the local Lavish Editor server.
|
|
249
|
+
| Command | Description |
|
|
250
|
+
| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
251
|
+
| `lavish-axi` | Show current sessions and usage guidance. |
|
|
252
|
+
| `lavish-axi update` | Check for or apply the latest npm release through the AXI SDK self-updater. |
|
|
253
|
+
| `lavish-axi <html-file>` | Open or resume a Lavish Editor session, with the open-time layout gate enabled by default. Unresolved layout issues from earlier in the session are preserved. Refuses to reopen a session the user explicitly ended from the browser unless `--reopen` is passed. |
|
|
254
|
+
| `lavish-axi poll <html-file>` | Long-poll until the user sends feedback or ends the session; detected layout issues wait in the user's Layout issues inbox and arrive only when queued. Leave no-timeout polls running, or re-run them if interrupted. Codex guidance keeps polls attached to the active turn. On `status: ended`, stop polling and do not reopen uninvited. |
|
|
255
|
+
| `lavish-axi end <html-file>` | End a session as the agent; unlike a user-initiated end from the browser, this still allows a plain reopen later. |
|
|
256
|
+
| `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. |
|
|
257
|
+
| `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, `--private` locks one behind a generated password, and the same command republishes or unpublishes an existing page with `--site`/`--update-key`. |
|
|
258
|
+
| `lavish-axi stop` | Shut down the background server. |
|
|
259
|
+
| `lavish-axi playbook [id]` | List focused artifact guidance or show one playbook; agents must open each matching playbook before writing HTML. |
|
|
260
|
+
| `lavish-axi design` | Show agent-facing design guidance, including optional CDN snippets and the whiteboard (Mermaid) opt-in snippet. |
|
|
261
|
+
| `lavish-axi setup hooks` | Install or repair optional SessionStart hooks for Claude Code, Codex, OpenCode, and GitHub Copilot CLI; restart the agent session afterward. |
|
|
262
|
+
| `lavish-axi setup plugin` | Register the installed package as an [Agent Plugin](https://agent-plugins.org) in VS Code, Cursor, and GitHub Copilot CLI; opt-in, idempotent, no marketplace involved. Reload each client afterward. |
|
|
263
|
+
| `lavish-axi server` | Run the local Lavish Editor server. |
|
|
261
264
|
|
|
262
265
|
Known playbook IDs: `diagram`, `table`, `comparison`, `plan`, `code`, `input`, `slides`.
|
|
263
266
|
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.
|
|
@@ -272,8 +275,12 @@ For flows, architecture, state, or sequence diagrams, open the diagram playbook
|
|
|
272
275
|
| `lavish-axi <html-file>` | `--reopen` | Reopen a session the user explicitly ended from the browser; without it, a plain open refuses and explains why instead of reopening uninvited. |
|
|
273
276
|
| `lavish-axi update` | `--check` | Report current vs latest npm version without installing an update. |
|
|
274
277
|
| `lavish-axi export` | `--out <path>` | Write the export to a specific path instead of `<name>.export.html` next to the source. |
|
|
275
|
-
| `lavish-axi share` | `--
|
|
276
|
-
| `lavish-axi share` | `--
|
|
278
|
+
| `lavish-axi share` | `--private` | Make the page private behind a password Lavish generates and returns once; hand it to viewers as a shared secret. |
|
|
279
|
+
| `lavish-axi share` | `--password <pw>` | Make the third-party ht-ml.app page private with a password you supply; viewers must supply the password. An empty or whitespace-only value (an unquoted `$PW` that is unset) is refused rather than publishing a public page. |
|
|
280
|
+
| `lavish-axi share` | `--site <site_id>` | Republish an existing page in place (with `--update-key`) instead of creating a new one; the URL does not change. |
|
|
281
|
+
| `lavish-axi share` | `--update-key <key>` | The secret returned when the page was published; required to republish or unpublish it. |
|
|
282
|
+
| `lavish-axi share` | `--unpublish` | Replace a published page with a locked placeholder (ht-ml.app cannot delete); takes `--site` and `--update-key` and no file. |
|
|
283
|
+
| `lavish-axi share` | `--token <t>` | Attach an optional bearer token (`LAVISH_AXI_HTML_APP_TOKEN`) when creating a page; never required, and rejected on a republish or `--unpublish`, where the `update_key` is the credential. |
|
|
277
284
|
| `lavish-axi poll` | `--agent-reply "..."` | Show the agent's reply in the existing browser chat, conclude delivered work, and return presence to waiting before polling again. |
|
|
278
285
|
| `lavish-axi poll` | `--timeout-ms <ms>` | Test/debug escape hatch only; agents should normally omit it and leave the long poll running. |
|
|
279
286
|
| `lavish-axi stop` | `--port <port>` | Shut down a server running on a non-default port. |
|
package/dist/chrome-client.js
CHANGED
|
@@ -133,8 +133,18 @@ const shareStatus = /** @type {HTMLDivElement} */ (document.getElementById("shar
|
|
|
133
133
|
const shareResult = /** @type {HTMLDivElement} */ (document.getElementById("shareResult"));
|
|
134
134
|
const shareUrlInput = /** @type {HTMLInputElement} */ (document.getElementById("shareUrl"));
|
|
135
135
|
const shareUpdateKeyInput = /** @type {HTMLInputElement} */ (document.getElementById("shareUpdateKey"));
|
|
136
|
+
const shareGenerateInput = /** @type {HTMLInputElement} */ (document.getElementById("shareGenerate"));
|
|
137
|
+
const sharePasswordResult = /** @type {HTMLLabelElement} */ (document.getElementById("sharePasswordResult"));
|
|
138
|
+
const shareUrlResult = /** @type {HTMLLabelElement} */ (document.getElementById("shareUrlResult"));
|
|
139
|
+
const shareUpdateKeyResult = /** @type {HTMLLabelElement} */ (document.getElementById("shareUpdateKeyResult"));
|
|
140
|
+
const shareUpdateKeyNote = /** @type {HTMLParagraphElement} */ (document.getElementById("shareUpdateKeyNote"));
|
|
141
|
+
const shareSiteIdResult = /** @type {HTMLLabelElement} */ (document.getElementById("shareSiteIdResult"));
|
|
142
|
+
const shareSiteIdInput = /** @type {HTMLInputElement} */ (document.getElementById("shareSiteId"));
|
|
143
|
+
const sharePasswordOutput = /** @type {HTMLInputElement} */ (document.getElementById("sharePasswordOut"));
|
|
144
|
+
const copySharePasswordButton = /** @type {HTMLButtonElement} */ (document.getElementById("copySharePassword"));
|
|
136
145
|
const copyShareUrlButton = /** @type {HTMLButtonElement} */ (document.getElementById("copyShareUrl"));
|
|
137
146
|
const copyUpdateKeyButton = /** @type {HTMLButtonElement} */ (document.getElementById("copyUpdateKey"));
|
|
147
|
+
const copyShareSiteIdButton = /** @type {HTMLButtonElement} */ (document.getElementById("copyShareSiteId"));
|
|
138
148
|
const endButton = /** @type {HTMLButtonElement} */ (document.getElementById("end"));
|
|
139
149
|
const copyPathButton = /** @type {HTMLButtonElement} */ (document.getElementById("copyPath"));
|
|
140
150
|
const copyHint = /** @type {HTMLSpanElement} */ (document.getElementById("copyHint"));
|
|
@@ -151,6 +161,8 @@ const layoutGateOverlay = /** @type {HTMLDivElement} */ (document.getElementById
|
|
|
151
161
|
const layoutGateTitle = /** @type {HTMLDivElement} */ (document.getElementById("layoutGateTitle"));
|
|
152
162
|
const layoutGateCopy = /** @type {HTMLParagraphElement} */ (document.getElementById("layoutGateCopy"));
|
|
153
163
|
const layoutGateAction = /** @type {HTMLButtonElement} */ (document.getElementById("layoutGateAction"));
|
|
164
|
+
const layoutGateBypass = /** @type {HTMLButtonElement} */ (document.getElementById("layoutGateBypass"));
|
|
165
|
+
const layoutGateEscape = /** @type {any} */ (window).__lavishLayoutGateEscape;
|
|
154
166
|
const warningsWrap = /** @type {HTMLDivElement} */ (document.getElementById("warningsWrap"));
|
|
155
167
|
const warningsButton = /** @type {HTMLButtonElement} */ (document.getElementById("warningsButton"));
|
|
156
168
|
const warningsCount = /** @type {HTMLSpanElement} */ (document.getElementById("warningsCount"));
|
|
@@ -185,7 +197,6 @@ let outdatedReloadInFlight = false;
|
|
|
185
197
|
let unrestorableDraftMiss = null;
|
|
186
198
|
let retiredDrafts = loadRetiredDrafts();
|
|
187
199
|
let layoutGateVisible = false;
|
|
188
|
-
let layoutGateArmed = false;
|
|
189
200
|
let layoutGateManuallyBypassed = !layoutGateEnabled;
|
|
190
201
|
let layoutGateFailureActive = false;
|
|
191
202
|
// A failure only the user can retire. The artifact-load card clears itself once a load succeeds;
|
|
@@ -1261,6 +1272,7 @@ function normalizeLayoutFindings(value) {
|
|
|
1261
1272
|
}
|
|
1262
1273
|
|
|
1263
1274
|
function clearLayoutGateTimer() {
|
|
1275
|
+
layoutGateEscape?.cancel?.();
|
|
1264
1276
|
if (layoutGateTimer) clearTimeout(layoutGateTimer);
|
|
1265
1277
|
layoutGateTimer = undefined;
|
|
1266
1278
|
}
|
|
@@ -1289,7 +1301,8 @@ function setLayoutGateActive(active) {
|
|
|
1289
1301
|
// its own: the artifact never loaded and retrying stopped helping. The overlay is reused because
|
|
1290
1302
|
// it already covers the empty artifact area; without this the user is left looking at either a
|
|
1291
1303
|
// spinner that never resolves or a blank frame, with nothing explaining it and nothing to click.
|
|
1292
|
-
// Bumping the cycle
|
|
1304
|
+
// Bumping the cycle invalidates the previous timer; setLayoutGateFailure immediately replaces it
|
|
1305
|
+
// with a fresh hold timer so the card cannot strand the visual gate.
|
|
1293
1306
|
function setLayoutGateFailure(title, copy, actionLabel = "Reload", onAction, { sticky = false } = {}) {
|
|
1294
1307
|
if (ended) return;
|
|
1295
1308
|
// A sticky card is the user's to retire, and that has to hold against being overwritten as
|
|
@@ -1299,8 +1312,9 @@ function setLayoutGateFailure(title, copy, actionLabel = "Reload", onAction, { s
|
|
|
1299
1312
|
layoutGateFailureActive = true;
|
|
1300
1313
|
layoutGateFailureSticky = sticky;
|
|
1301
1314
|
layoutGateCycle += 1;
|
|
1302
|
-
|
|
1303
|
-
|
|
1315
|
+
// Failure copy must not disable the visual gate's own recovery paths. Keep a fresh hold timer
|
|
1316
|
+
// over the card so a server replacement or any other failure cannot strand the artifact behind
|
|
1317
|
+
// a sticky message forever.
|
|
1304
1318
|
if (layoutGateTitle) layoutGateTitle.textContent = title;
|
|
1305
1319
|
if (layoutGateCopy) layoutGateCopy.textContent = copy;
|
|
1306
1320
|
if (layoutGateAction) {
|
|
@@ -1308,7 +1322,12 @@ function setLayoutGateFailure(title, copy, actionLabel = "Reload", onAction, { s
|
|
|
1308
1322
|
layoutGateAction.textContent = actionLabel;
|
|
1309
1323
|
layoutGateAction.onclick = onAction || (() => location.reload());
|
|
1310
1324
|
}
|
|
1325
|
+
if (layoutGateBypass) {
|
|
1326
|
+
layoutGateBypass.hidden = false;
|
|
1327
|
+
layoutGateBypass.onclick = () => forceRevealLayoutGate("manual");
|
|
1328
|
+
}
|
|
1311
1329
|
setLayoutGateActive(true);
|
|
1330
|
+
armLayoutGateTimer();
|
|
1312
1331
|
}
|
|
1313
1332
|
|
|
1314
1333
|
// Every failure card in this feature is raised in a state where the server may not be listening,
|
|
@@ -1362,35 +1381,31 @@ function clearLayoutGateFailure() {
|
|
|
1362
1381
|
layoutGateAction.textContent = "Show anyway";
|
|
1363
1382
|
layoutGateAction.onclick = () => forceRevealLayoutGate("manual");
|
|
1364
1383
|
}
|
|
1384
|
+
if (layoutGateBypass) layoutGateBypass.hidden = true;
|
|
1365
1385
|
revealLayoutGate();
|
|
1366
1386
|
}
|
|
1367
1387
|
|
|
1368
1388
|
function revealLayoutGate() {
|
|
1369
|
-
if (layoutGateFailureSticky) return;
|
|
1370
1389
|
clearLayoutGateTimer();
|
|
1371
|
-
|
|
1390
|
+
layoutGateEscape?.reveal?.();
|
|
1372
1391
|
setLayoutGateActive(false);
|
|
1373
1392
|
}
|
|
1374
1393
|
|
|
1375
1394
|
function forceRevealLayoutGate(reason) {
|
|
1376
|
-
if (
|
|
1377
|
-
if (reason === "manual")
|
|
1395
|
+
if (ended) return;
|
|
1396
|
+
if (reason === "manual") {
|
|
1397
|
+
layoutGateManuallyBypassed = true;
|
|
1398
|
+
layoutGateEscape?.manualReveal?.();
|
|
1399
|
+
}
|
|
1378
1400
|
revealLayoutGate();
|
|
1379
1401
|
}
|
|
1380
1402
|
|
|
1381
|
-
function
|
|
1382
|
-
clearLayoutGateFailure();
|
|
1383
|
-
// A sticky failure owns the overlay until the user acts on it, so a later load must not repaint
|
|
1384
|
-
// the checking card over the message it left there.
|
|
1385
|
-
if (layoutGateFailureSticky) return;
|
|
1386
|
-
if (!layoutGateEnabled || layoutGateManuallyBypassed || ended) return;
|
|
1387
|
-
|
|
1388
|
-
layoutGateCycle += 1;
|
|
1389
|
-
layoutGateArmed = true;
|
|
1390
|
-
setLayoutGateCard("checking");
|
|
1391
|
-
setLayoutGateActive(true);
|
|
1403
|
+
function armLayoutGateTimer() {
|
|
1392
1404
|
clearLayoutGateTimer();
|
|
1393
|
-
|
|
1405
|
+
if (layoutGateEscape?.arm) {
|
|
1406
|
+
layoutGateEscape.arm(layoutGateMaxHoldMs, () => forceRevealLayoutGate("timeout"));
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
1394
1409
|
const cycle = layoutGateCycle;
|
|
1395
1410
|
layoutGateTimer = setTimeout(() => {
|
|
1396
1411
|
if (cycle !== layoutGateCycle || !layoutGateVisible || ended) return;
|
|
@@ -1399,22 +1414,35 @@ function startLayoutGateCycle() {
|
|
|
1399
1414
|
layoutGateTimer?.unref?.();
|
|
1400
1415
|
}
|
|
1401
1416
|
|
|
1417
|
+
function startLayoutGateCycle() {
|
|
1418
|
+
clearLayoutGateFailure();
|
|
1419
|
+
if (!layoutGateEnabled || layoutGateManuallyBypassed || ended) return;
|
|
1420
|
+
|
|
1421
|
+
layoutGateCycle += 1;
|
|
1422
|
+
setLayoutGateActive(true);
|
|
1423
|
+
// A sticky failure owns the card copy, but never the reveal. Do not repaint it as a checking
|
|
1424
|
+
// card, and do arm a fresh timer for reloads that happen while the sticky card is present.
|
|
1425
|
+
if (!layoutGateFailureSticky) setLayoutGateCard("checking");
|
|
1426
|
+
armLayoutGateTimer();
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1402
1429
|
// The gate only waits for fonts and final geometry now. It never holds the artifact hostage
|
|
1403
1430
|
// pending an agent repair: findings are the user's to triage, so a completed pass always reveals
|
|
1404
1431
|
// and hands the result to the passive inbox.
|
|
1405
1432
|
function handleLayoutGatePass() {
|
|
1406
|
-
if (
|
|
1407
|
-
if (!layoutGateArmed && !layoutGateVisible) return;
|
|
1433
|
+
if (ended || !layoutGateVisible) return;
|
|
1408
1434
|
revealLayoutGate();
|
|
1409
1435
|
}
|
|
1410
1436
|
|
|
1411
1437
|
function initializeLayoutGate() {
|
|
1438
|
+
if (layoutGateEscape?.isManuallyBypassed?.()) layoutGateManuallyBypassed = true;
|
|
1412
1439
|
if (!layoutGateEnabled) {
|
|
1413
1440
|
setLayoutGateActive(false);
|
|
1414
1441
|
return;
|
|
1415
1442
|
}
|
|
1416
1443
|
|
|
1417
1444
|
if (layoutGateAction) layoutGateAction.onclick = () => forceRevealLayoutGate("manual");
|
|
1445
|
+
if (layoutGateBypass) layoutGateBypass.onclick = () => forceRevealLayoutGate("manual");
|
|
1418
1446
|
startLayoutGateCycle();
|
|
1419
1447
|
}
|
|
1420
1448
|
|
|
@@ -1801,6 +1829,7 @@ function markSessionEnded() {
|
|
|
1801
1829
|
layoutGateManuallyBypassed = true;
|
|
1802
1830
|
layoutGateFailureSticky = false;
|
|
1803
1831
|
revealLayoutGate();
|
|
1832
|
+
layoutGateEscape?.end?.();
|
|
1804
1833
|
postToFrame({ type: "lavish:setAnnotationMode", enabled: false });
|
|
1805
1834
|
endedOverlay.hidden = false;
|
|
1806
1835
|
}
|
|
@@ -1879,16 +1908,68 @@ async function exportArtifact() {
|
|
|
1879
1908
|
}
|
|
1880
1909
|
}
|
|
1881
1910
|
|
|
1911
|
+
// ONE owner for the whole result panel. Every path that renders an outcome - dialog open, a
|
|
1912
|
+
// successful publish, a retry after any failure, an indeterminate report, an incomplete 200 -
|
|
1913
|
+
// routes through here, because a row shown or hidden by one path and reset by another is how a
|
|
1914
|
+
// retry after a failed publish came to display "Published" with the once-only update_key still
|
|
1915
|
+
// hidden. Each row is derived from the value it would show, so nothing can be half-rendered.
|
|
1916
|
+
function renderShareResult({ url = "", siteId = "", password = "", updateKey = "" } = {}) {
|
|
1917
|
+
shareUrlInput.value = url;
|
|
1918
|
+
shareUrlResult.hidden = !url;
|
|
1919
|
+
// A self-hosted backend may not return one, and an empty box with a copy button is worse than
|
|
1920
|
+
// no row. Never derive it from the URL: that shape belongs to the backend, not Lavish.
|
|
1921
|
+
shareSiteIdInput.value = siteId;
|
|
1922
|
+
shareSiteIdResult.hidden = !siteId;
|
|
1923
|
+
sharePasswordOutput.value = password;
|
|
1924
|
+
sharePasswordResult.hidden = !password;
|
|
1925
|
+
shareUpdateKeyInput.value = updateKey;
|
|
1926
|
+
shareUpdateKeyResult.hidden = !updateKey;
|
|
1927
|
+
// The note's own copy tells the user to republish with `--site <site id> --update-key <key>`,
|
|
1928
|
+
// so it may only appear when BOTH halves of that credential are on screen. An update key with
|
|
1929
|
+
// no usable site id cannot update anything, and the status line says so instead.
|
|
1930
|
+
shareUpdateKeyNote.hidden = !(updateKey && siteId);
|
|
1931
|
+
shareResult.hidden = !(url || siteId || password || updateKey);
|
|
1932
|
+
// Returned so every sentence in the status line is derived from what was actually rendered.
|
|
1933
|
+
// Copy stamped from the request instead has promised rows the panel does not contain.
|
|
1934
|
+
return { url, siteId, password, updateKey };
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
// Wording shared with the CLI's next_step for the same condition, so the two surfaces cannot
|
|
1938
|
+
// drift into describing the same dead end differently.
|
|
1939
|
+
const NO_SITE_ID_WARNING =
|
|
1940
|
+
" The host did not return a site id Lavish can use, and --site is half the republish credential, so this page can NEVER be republished or unpublished even though its update key is in hand.";
|
|
1941
|
+
|
|
1942
|
+
// What the page is gated behind, said only in terms of what the panel can show. A password the
|
|
1943
|
+
// user typed is never echoed by the server, so pointing at a row that was not rendered is the
|
|
1944
|
+
// same confidently-wrong sentence this feature exists to avoid.
|
|
1945
|
+
function publishedVisibilityText(isPublic, rendered, publicText) {
|
|
1946
|
+
if (isPublic) return publicText;
|
|
1947
|
+
return rendered.password ? "behind the password below" : "behind the password you supplied";
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1882
1950
|
function openShareDialog() {
|
|
1883
1951
|
closeMenus();
|
|
1884
1952
|
shareDialog.hidden = false;
|
|
1885
1953
|
shareStatus.textContent = "";
|
|
1886
1954
|
shareStatus.classList.remove("error");
|
|
1887
|
-
|
|
1955
|
+
renderShareResult();
|
|
1956
|
+
shareGenerateInput.checked = false;
|
|
1888
1957
|
sharePasswordInput.value = "";
|
|
1958
|
+
syncSharePasswordInput();
|
|
1889
1959
|
sharePasswordInput.focus();
|
|
1890
1960
|
}
|
|
1891
1961
|
|
|
1962
|
+
// A generated password and a typed one are the same field to the server, so the checkbox owns
|
|
1963
|
+
// the input rather than the two racing to decide what gets published.
|
|
1964
|
+
function syncSharePasswordInput() {
|
|
1965
|
+
const generating = shareGenerateInput.checked;
|
|
1966
|
+
sharePasswordInput.disabled = generating;
|
|
1967
|
+
sharePasswordInput.placeholder = generating
|
|
1968
|
+
? "Lavish will generate one when you publish"
|
|
1969
|
+
: "Leave blank for a public page";
|
|
1970
|
+
if (generating) sharePasswordInput.value = "";
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1892
1973
|
function closeShareDialog() {
|
|
1893
1974
|
shareDialog.hidden = true;
|
|
1894
1975
|
}
|
|
@@ -1901,24 +1982,83 @@ async function copyToButton(value, button, label) {
|
|
|
1901
1982
|
}, 1200);
|
|
1902
1983
|
}
|
|
1903
1984
|
|
|
1985
|
+
function reportIndeterminatePublish(data) {
|
|
1986
|
+
const rendered = renderShareResult({ password: data.password || "" });
|
|
1987
|
+
shareStatus.classList.add("error");
|
|
1988
|
+
const reason = data.error ? data.error + " " : "";
|
|
1989
|
+
const visibility = publishedVisibilityText(data.public, rendered, "PUBLIC - anyone with the link could read it");
|
|
1990
|
+
shareStatus.textContent =
|
|
1991
|
+
reason +
|
|
1992
|
+
"ht-ml.app may or may not have published this page, so treat the outcome as unknown. If it did publish, the page is live " +
|
|
1993
|
+
visibility +
|
|
1994
|
+
", and its URL and update key were lost with the failed response, so it can never be republished or unpublished. Publishing again creates a SECOND page rather than replacing it." +
|
|
1995
|
+
(rendered.password ? " Copy the password now - it is shown once here and Lavish does not store it." : "");
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
// An incomplete 200 is NOT an unknown outcome: the host answered, so the page landed. Whatever
|
|
1999
|
+
// fields did arrive are rendered, because a url with no update_key names a live, public-by-default
|
|
2000
|
+
// page whose only write credential is gone - and saying "may or may not" there would throw away
|
|
2001
|
+
// the address Lavish is holding.
|
|
2002
|
+
function reportIncompletePublish(data) {
|
|
2003
|
+
const rendered = renderShareResult({
|
|
2004
|
+
url: data.url || "",
|
|
2005
|
+
siteId: data.site_id || "",
|
|
2006
|
+
password: data.password || "",
|
|
2007
|
+
updateKey: data.update_key || "",
|
|
2008
|
+
});
|
|
2009
|
+
shareStatus.classList.add("error");
|
|
2010
|
+
const visibility = publishedVisibilityText(data.public, rendered, "PUBLIC - anyone with the link can read it");
|
|
2011
|
+
const updateKeyNote = !rendered.updateKey
|
|
2012
|
+
? "No update key came back, and ht-ml.app issues one only once and has no delete, so this page can never be republished or unpublished. "
|
|
2013
|
+
: rendered.siteId
|
|
2014
|
+
? "Copy the update key below - it is issued once. "
|
|
2015
|
+
: "Copy the update key below - it is issued once, though" + NO_SITE_ID_WARNING.slice(1) + " ";
|
|
2016
|
+
shareStatus.textContent =
|
|
2017
|
+
"ht-ml.app accepted this publish, so the page IS live and " +
|
|
2018
|
+
visibility +
|
|
2019
|
+
", but its response was malformed and Lavish could not read the whole result back. " +
|
|
2020
|
+
(rendered.url ? "Its address is below. " : "The response carried no URL, so Lavish cannot show the address. ") +
|
|
2021
|
+
updateKeyNote +
|
|
2022
|
+
"Publishing again creates a SECOND page rather than replacing it." +
|
|
2023
|
+
(rendered.password ? " Copy the password now - it is shown once here and Lavish does not store it." : "");
|
|
2024
|
+
}
|
|
2025
|
+
|
|
1904
2026
|
async function publishShare(event) {
|
|
1905
2027
|
event.preventDefault();
|
|
1906
2028
|
sharePublishButton.disabled = true;
|
|
1907
2029
|
shareStatus.classList.remove("error");
|
|
1908
2030
|
shareStatus.textContent = "Publishing to ht-ml.app...";
|
|
1909
|
-
|
|
1910
|
-
const
|
|
1911
|
-
const
|
|
2031
|
+
renderShareResult();
|
|
2032
|
+
const generating = shareGenerateInput.checked;
|
|
2033
|
+
const password = generating ? "" : sharePasswordInput.value.trim();
|
|
2034
|
+
const passwordProtected = generating || Boolean(password);
|
|
1912
2035
|
try {
|
|
1913
2036
|
const response = await fetch("/api/" + key + "/share", {
|
|
1914
2037
|
method: "POST",
|
|
1915
2038
|
headers: { "content-type": "application/json" },
|
|
1916
|
-
body: JSON.stringify(password ? { password } : {}),
|
|
2039
|
+
body: JSON.stringify(generating ? { generate_password: true } : password ? { password } : {}),
|
|
1917
2040
|
});
|
|
1918
2041
|
const data = await response.json();
|
|
1919
|
-
if (!response.ok)
|
|
1920
|
-
|
|
1921
|
-
|
|
2042
|
+
if (!response.ok) {
|
|
2043
|
+
// Only a host rejection proves nothing was published. On anything else the page may already
|
|
2044
|
+
// be live, and a password minted for this request is the one thing that could still open it,
|
|
2045
|
+
// so it is shown here rather than dying with the failed response.
|
|
2046
|
+
if (data.outcome === "published-incomplete") {
|
|
2047
|
+
reportIncompletePublish(data);
|
|
2048
|
+
return;
|
|
2049
|
+
}
|
|
2050
|
+
if (data.outcome === "indeterminate") {
|
|
2051
|
+
reportIndeterminatePublish(data);
|
|
2052
|
+
return;
|
|
2053
|
+
}
|
|
2054
|
+
throw new Error(data.error || "publish failed");
|
|
2055
|
+
}
|
|
2056
|
+
const rendered = renderShareResult({
|
|
2057
|
+
url: data.url || "",
|
|
2058
|
+
siteId: data.site_id || "",
|
|
2059
|
+
password: data.password || "",
|
|
2060
|
+
updateKey: data.update_key || "",
|
|
2061
|
+
});
|
|
1922
2062
|
const unresolvedAssets = Array.isArray(data.unresolved_local_assets) ? data.unresolved_local_assets : [];
|
|
1923
2063
|
const notices = Array.isArray(data.notices) ? data.notices : [];
|
|
1924
2064
|
const warningCount = unresolvedAssets.length;
|
|
@@ -1932,10 +2072,16 @@ async function publishShare(event) {
|
|
|
1932
2072
|
: passwordProtected
|
|
1933
2073
|
? "Published. This page is PASSWORD-PROTECTED; viewers also need the password."
|
|
1934
2074
|
: "Published. Anyone with the link can view this page.";
|
|
1935
|
-
|
|
2075
|
+
if (rendered.updateKey && !rendered.siteId) shareStatus.textContent += NO_SITE_ID_WARNING;
|
|
2076
|
+
if (rendered.password) {
|
|
2077
|
+
shareStatus.textContent += " Copy the password now - it is shown once here and Lavish does not store it.";
|
|
2078
|
+
}
|
|
1936
2079
|
shareUrlInput.focus();
|
|
1937
2080
|
shareUrlInput.select();
|
|
1938
2081
|
} catch (error) {
|
|
2082
|
+
// Deliberately does NOT clear the panel. It is already cleared before the fetch, so the only
|
|
2083
|
+
// thing this could reach is a result the success path already rendered - and wiping that
|
|
2084
|
+
// destroys the once-issued update_key of a page that definitely published.
|
|
1939
2085
|
shareStatus.classList.add("error");
|
|
1940
2086
|
shareStatus.textContent = error instanceof Error ? error.message : String(error);
|
|
1941
2087
|
} finally {
|
|
@@ -2737,14 +2883,24 @@ window.addEventListener("message", (event) => {
|
|
|
2737
2883
|
|
|
2738
2884
|
const msg = event.data || {};
|
|
2739
2885
|
const messageToken = String(msg.artifact_load_token || "");
|
|
2740
|
-
if (messageToken !== artifactLoadToken)
|
|
2886
|
+
if (messageToken !== artifactLoadToken) {
|
|
2887
|
+
// A pass can be stamped by the load that just lost a token race. Ask the current artifact
|
|
2888
|
+
// document to run the audit again instead of consuming the only pass for this cycle.
|
|
2889
|
+
if (msg.type === "lavish:layoutDiagnostics") postToFrame({ type: "lavish:requestLayoutDiagnostics" });
|
|
2890
|
+
return;
|
|
2891
|
+
}
|
|
2741
2892
|
const messageSequence = ++artifactMessageSequence;
|
|
2742
2893
|
artifactSpokeToken = messageToken;
|
|
2743
2894
|
clearTimeout(artifactSilenceTimer);
|
|
2744
2895
|
if (msg.type === "lavish:layoutDiagnostics") {
|
|
2745
2896
|
const diagnosticSequence = ++layoutDiagnosticSequence;
|
|
2897
|
+
const complete = msg.complete !== false;
|
|
2898
|
+
// The gate is visual, so the client-side settled pass is the release signal. Reporting the
|
|
2899
|
+
// pass is deliberately fire-and-forget: a server restart or a diagnostics 4xx/5xx must not
|
|
2900
|
+
// hold a rendered artifact hostage to a network round-trip.
|
|
2901
|
+
if (complete) handleLayoutGatePass();
|
|
2746
2902
|
submitLayoutDiagnostics({
|
|
2747
|
-
complete
|
|
2903
|
+
complete,
|
|
2748
2904
|
targetPresenceComplete: msg.target_presence_complete === true,
|
|
2749
2905
|
artifactRevision: msg.artifact_revision,
|
|
2750
2906
|
artifactLoadToken: msg.artifact_load_token,
|
|
@@ -2759,9 +2915,14 @@ window.addEventListener("message", (event) => {
|
|
|
2759
2915
|
if (messageSequence === artifactMessageSequence) armArtifactAvailabilityProbe(messageToken);
|
|
2760
2916
|
return;
|
|
2761
2917
|
}
|
|
2762
|
-
if (msg.complete !== false) handleLayoutGatePass();
|
|
2763
2918
|
})
|
|
2764
|
-
.catch(() => {
|
|
2919
|
+
.catch(() => {
|
|
2920
|
+
// A failed report is still a completed client-side pass. Keep this fallback explicit so a
|
|
2921
|
+
// future change cannot accidentally make the network request the gate's release path.
|
|
2922
|
+
if (complete && messageToken === artifactLoadToken && diagnosticSequence === layoutDiagnosticSequence) {
|
|
2923
|
+
handleLayoutGatePass();
|
|
2924
|
+
}
|
|
2925
|
+
});
|
|
2765
2926
|
return;
|
|
2766
2927
|
}
|
|
2767
2928
|
// The artifact spoke, so it rendered and ran its SDK - there is nothing fatal to probe for.
|
|
@@ -3053,6 +3214,10 @@ shareDialog.addEventListener("click", (event) => {
|
|
|
3053
3214
|
});
|
|
3054
3215
|
copyShareUrlButton.onclick = () => copyToButton(shareUrlInput.value, copyShareUrlButton, "Copy URL");
|
|
3055
3216
|
copyUpdateKeyButton.onclick = () => copyToButton(shareUpdateKeyInput.value, copyUpdateKeyButton, "Copy key");
|
|
3217
|
+
copySharePasswordButton.onclick = () =>
|
|
3218
|
+
copyToButton(sharePasswordOutput.value, copySharePasswordButton, "Copy password");
|
|
3219
|
+
copyShareSiteIdButton.onclick = () => copyToButton(shareSiteIdInput.value, copyShareSiteIdButton, "Copy site ID");
|
|
3220
|
+
shareGenerateInput.onchange = syncSharePasswordInput;
|
|
3056
3221
|
endButton.onclick = () => {
|
|
3057
3222
|
closeMenus();
|
|
3058
3223
|
endSession();
|
|
@@ -3148,9 +3313,9 @@ setAgentPresence("waiting");
|
|
|
3148
3313
|
// to wait for - start read-only instead of looking live until a Send gets silently refused.
|
|
3149
3314
|
if (sessionData.initialEnded) markSessionEnded();
|
|
3150
3315
|
|
|
3151
|
-
// Reaching this line is the only proof that this file parsed and ran to completion. The
|
|
3152
|
-
//
|
|
3153
|
-
//
|
|
3316
|
+
// Reaching this line is the only proof that this file parsed and ran to completion. The inline
|
|
3317
|
+
// bootstrap already owns the gate's bounded escape if this script fails; retire only its separate
|
|
3318
|
+
// boot-failure timer now that the full client has taken over.
|
|
3154
3319
|
const chromeBootWindow = /** @type {Record<string, any>} */ (/** @type {unknown} */ (window));
|
|
3155
3320
|
chromeBootWindow.__lavishChromeReady = true;
|
|
3156
3321
|
chromeBootWindow.__lavishCancelChromeBootFailsafe?.();
|
package/dist/chrome.css
CHANGED
|
@@ -803,6 +803,23 @@ body.lavish {
|
|
|
803
803
|
.share-grid input::placeholder {
|
|
804
804
|
color: var(--fg-label);
|
|
805
805
|
}
|
|
806
|
+
.share-grid .share-check {
|
|
807
|
+
grid-template-columns: auto 1fr;
|
|
808
|
+
grid-auto-flow: column;
|
|
809
|
+
align-items: center;
|
|
810
|
+
gap: var(--space-6);
|
|
811
|
+
color: var(--fg-muted);
|
|
812
|
+
font-size: var(--text-sm);
|
|
813
|
+
font-weight: var(--w-regular);
|
|
814
|
+
letter-spacing: 0;
|
|
815
|
+
text-transform: none;
|
|
816
|
+
cursor: pointer;
|
|
817
|
+
}
|
|
818
|
+
.share-grid .share-check input {
|
|
819
|
+
width: auto;
|
|
820
|
+
padding: 0;
|
|
821
|
+
accent-color: var(--accent);
|
|
822
|
+
}
|
|
806
823
|
.share-status {
|
|
807
824
|
min-height: 20px;
|
|
808
825
|
margin-top: var(--space-8);
|
|
@@ -824,6 +841,9 @@ body.lavish {
|
|
|
824
841
|
.share-result[hidden] {
|
|
825
842
|
display: none;
|
|
826
843
|
}
|
|
844
|
+
.share-result label[hidden] {
|
|
845
|
+
display: none;
|
|
846
|
+
}
|
|
827
847
|
.share-copy-row {
|
|
828
848
|
display: flex;
|
|
829
849
|
gap: var(--space-6);
|
|
@@ -1328,6 +1348,9 @@ body.lavish {
|
|
|
1328
1348
|
.ended-action {
|
|
1329
1349
|
margin-top: var(--space-8);
|
|
1330
1350
|
}
|
|
1351
|
+
.layout-gate-bypass {
|
|
1352
|
+
margin-left: var(--space-4);
|
|
1353
|
+
}
|
|
1331
1354
|
iframe {
|
|
1332
1355
|
width: 100%;
|
|
1333
1356
|
height: 100%;
|