@tokenoftrust/storefront-runner 1.3.2 → 1.3.4-rc.1
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/apps/storefront/astro.config.mjs +50 -0
- package/apps/storefront/env.d.ts +24 -8
- package/apps/storefront/src/components/admin/{AdminFoxyCartSettingsPanel.astro → AdminCheckoutSettingsPanel.astro} +50 -51
- package/apps/storefront/src/components/admin/AdminSettingsTab.astro +2 -2
- package/apps/storefront/src/components/admin/AdminSubscriptionsTab.astro +613 -0
- package/apps/storefront/src/components/admin/orders/OrdersWorkspace.astro +3 -3
- package/apps/storefront/src/components/chrome/SiteHeader.astro +3 -4
- package/apps/storefront/src/components/commerce/{FoxySidecartStyles.astro → CartDrawerStyles.astro} +13 -14
- package/apps/storefront/src/components/commerce/CheckoutLoader.astro +21 -0
- package/apps/storefront/src/components/islands/VariantSelector.tsx +101 -20
- package/apps/storefront/src/config/adminTargetTenant.ts +2 -2
- package/apps/storefront/src/config/devTenantSeed.ts +65 -16
- package/apps/storefront/src/config/resolver.ts +48 -1
- package/apps/storefront/src/layouts/Layout.astro +14 -12
- package/apps/storefront/src/lib/admin/README.md +2 -2
- package/apps/storefront/src/lib/admin/checkoutStyleInput.ts +5 -5
- package/apps/storefront/src/lib/admin/orderClient.ts +20 -20
- package/apps/storefront/src/lib/admin/services/mock/{foxyCartSettings.ts → checkoutSettings.ts} +21 -22
- package/apps/storefront/src/lib/admin/services/mock/index.ts +3 -2
- package/apps/storefront/src/lib/admin/services/mock/settings.ts +1 -1
- package/apps/storefront/src/lib/admin/services/mock/subscriptions.ts +398 -0
- package/apps/storefront/src/lib/admin/services/orders.ts +21 -21
- package/apps/storefront/src/lib/admin/viewmodels/subscriptions.ts +106 -0
- package/apps/storefront/src/lib/auth/loginGate.ts +14 -3
- package/apps/storefront/src/lib/{foxyCommerce.ts → checkoutCommerce.ts} +20 -18
- package/apps/storefront/src/lib/commerce/{foxyCartSettings.ts → checkoutSettings.ts} +65 -66
- package/apps/storefront/src/lib/dev/activityIngestToken.ts +9 -0
- package/apps/storefront/src/lib/dev/activityStore.ts +12 -0
- package/apps/storefront/src/lib/dev/cliSignInCode.ts +23 -4
- package/apps/storefront/src/lib/dev/runtimeStore.ts +136 -7
- package/apps/storefront/src/lib/edgeCache.ts +7 -6
- package/apps/storefront/src/lib/health.ts +53 -0
- package/apps/storefront/src/lib/privacy/emailHint.ts +33 -0
- package/apps/storefront/src/lib/rawMarketingHtml.ts +1 -0
- package/apps/storefront/src/lib/sections.ts +19 -23
- package/apps/storefront/src/lib/securityHeaders.ts +1 -1
- package/apps/storefront/src/lib/storyblok/content-model.ts +3 -2
- package/apps/storefront/src/middleware/index.ts +29 -12
- package/apps/storefront/src/pages/[...slug].astro +5 -6
- package/apps/storefront/src/pages/admin.astro +10 -3
- package/apps/storefront/src/pages/api/admin/orders/[id].ts +4 -4
- package/apps/storefront/src/pages/api/admin/orders.ts +5 -5
- package/apps/storefront/src/pages/api/auth/magic-exchange.ts +14 -7
- package/apps/storefront/src/pages/api/checkout-style.css.ts +5 -5
- package/apps/storefront/src/pages/auth/login.astro +4 -1
- package/apps/storefront/src/pages/auth/magic.astro +263 -43
- package/apps/storefront/src/pages/capabilities.astro +1 -1
- package/apps/storefront/src/pages/cockpit.astro +2365 -0
- package/apps/storefront/src/pages/dashboard/[appDomain]/team.astro +19 -0
- package/apps/storefront/src/pages/index.astro +23 -29
- package/apps/storefront/src/pages/products/[handle].astro +50 -20
- package/package.json +1 -1
- package/packages/public-runtime/src/capabilities.ts +97 -9
- package/packages/public-runtime/src/{foxy-orders.ts → checkout-orders.ts} +28 -27
- package/packages/public-runtime/src/checkout-style-publish.ts +25 -25
- package/packages/public-runtime/src/checkout-style.ts +13 -13
- package/packages/public-runtime/src/{foxy.ts → checkout.ts} +111 -34
- package/packages/public-runtime/src/csp.ts +11 -11
- package/packages/public-runtime/src/customization-scripts.ts +28 -0
- package/packages/public-runtime/src/extension-contract.ts +73 -0
- package/packages/public-runtime/src/index.ts +4 -2
- package/packages/public-runtime/src/product.ts +57 -0
- package/packages/public-runtime/src/raw-html-policy.ts +35 -0
- package/packages/public-runtime/src/tenant.ts +14 -11
- package/scripts/dev/checkout-watch.mjs +28 -0
- package/scripts/dev/file-browser.mjs +5 -1
- package/apps/storefront/src/components/commerce/FoxyLoader.astro +0 -23
- package/apps/storefront/src/pages/api/dev/activity.ts +0 -124
- package/apps/storefront/src/pages/api/dev/cli-signin-code.ts +0 -73
- package/apps/storefront/src/pages/api/dev/diff.ts +0 -68
- package/apps/storefront/src/pages/api/dev/file.ts +0 -57
- package/apps/storefront/src/pages/api/dev/locate.ts +0 -125
- package/apps/storefront/src/pages/api/dev/publish.ts +0 -128
- package/apps/storefront/src/pages/api/dev/tree.ts +0 -70
- package/apps/storefront/src/pages/api/dev/write.ts +0 -116
|
@@ -0,0 +1,2365 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* /cockpit — the developer's first-run shell (what a granted developer lands on after
|
|
4
|
+
* sign-in). Renamed from /dev 2026-07-20; middleware 302-redirects old /dev links here.
|
|
5
|
+
*
|
|
6
|
+
* Deliberately NOT the operator/reconcile control plane, and — post U1 (strip-to-shell) —
|
|
7
|
+
* deliberately NOT the old marketing home either. This is the guided-cockpit's Step 1: a
|
|
8
|
+
* single dominant action (copy the one command that gets your store running) plus the
|
|
9
|
+
* developer's own tenant identity and an ⓘ explainer. The staged flow, live Monitor,
|
|
10
|
+
* in-browser file tree/diff/editor, "Stuck? Ask", and the "coming soon" shelf land on top
|
|
11
|
+
* of this shell in later chunks (U2/U3/U4, C1+) — this file is the frame they mount into.
|
|
12
|
+
*
|
|
13
|
+
* Brand: a STANDALONE document (bypasses Layout.astro) that ports the canonical /dev cockpit
|
|
14
|
+
* mockup's visual system — the tokenoftrust tenant theme (navy ink #0A2540 + trust teal
|
|
15
|
+
* #0E9F8E on cool-white #F7F9FC), Plus Jakarta Sans / Inter / JetBrains Mono, rounded cards —
|
|
16
|
+
* now with full dark-mode (system + manual toggle) and reduced-motion support. Same CSP as
|
|
17
|
+
* every storefront response (googleapis/gstatic allowlisted, so the shared typefaces load);
|
|
18
|
+
* inline style/script carry this request's nonce.
|
|
19
|
+
*
|
|
20
|
+
* The one command honours the shipped browserless-signin fastpath: a developer who reached
|
|
21
|
+
* this page from their magic link already has a session here, so the Copy button mints a
|
|
22
|
+
* single-use `tot login --code … && tot start` command (via /api/dev/cli-signin-code) and
|
|
23
|
+
* copies the whole thing — no browser sign-in in the terminal. Everyone else copies the
|
|
24
|
+
* generic `npm i -g @tokenoftrust/cli && tot start`.
|
|
25
|
+
*/
|
|
26
|
+
import { readViewerSession } from "@/lib/auth/route";
|
|
27
|
+
import { staticTenants } from "@/config/tenants";
|
|
28
|
+
import { resolveDevApiBase } from "@/lib/dev/apiBase";
|
|
29
|
+
import { normalizeEmailHint, redactEmailForHint } from "@/lib/privacy/emailHint";
|
|
30
|
+
|
|
31
|
+
export const prerender = false;
|
|
32
|
+
const cspNonce = Astro.locals.cspNonce;
|
|
33
|
+
const url = new URL(Astro.request.url);
|
|
34
|
+
const isCredentialRecovery = url.searchParams.get("recover") === "cli";
|
|
35
|
+
const legacyRecoveryEmail = (url.searchParams.get("email") ?? "").trim();
|
|
36
|
+
const recoveryEmailHint =
|
|
37
|
+
normalizeEmailHint(url.searchParams.get("email_hint")) ??
|
|
38
|
+
redactEmailForHint(legacyRecoveryEmail) ??
|
|
39
|
+
"";
|
|
40
|
+
if (isCredentialRecovery && url.searchParams.has("email")) {
|
|
41
|
+
const canonical = new URLSearchParams(url.searchParams);
|
|
42
|
+
canonical.delete("email");
|
|
43
|
+
if (recoveryEmailHint) canonical.set("email_hint", recoveryEmailHint);
|
|
44
|
+
return Astro.redirect(
|
|
45
|
+
`/cockpit${canonical.toString() ? `?${canonical.toString()}` : ""}`,
|
|
46
|
+
302,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
const cockpitNext = isCredentialRecovery ? "/cockpit?recover=cli" : "/cockpit";
|
|
50
|
+
const loginParams = new URLSearchParams({ next: cockpitNext });
|
|
51
|
+
if (recoveryEmailHint) loginParams.set("email_hint", recoveryEmailHint);
|
|
52
|
+
const loginHref = `/auth/login?${loginParams.toString()}`;
|
|
53
|
+
|
|
54
|
+
const session = await readViewerSession({ cookies: Astro.cookies });
|
|
55
|
+
const signedIn = !!session;
|
|
56
|
+
// The fastpath (browserless terminal sign-in) is gated behind the developer's own session +
|
|
57
|
+
// capability — the same gate /api/dev/cli-signin-code enforces. Mirror it here so a
|
|
58
|
+
// logged-out / no-capability visitor gets the generic command, never a broken mint.
|
|
59
|
+
const hasStoreAccess = signedIn && !!session?.capability && session.capability !== "none";
|
|
60
|
+
|
|
61
|
+
// Resolve the developer's tenant from their first membership (or ?tenant= for an
|
|
62
|
+
// ANONYMOUS preview only — never override a signed-in developer's own tenant with
|
|
63
|
+
// caller-supplied input; the mint/activity endpoints already derive tenant solely
|
|
64
|
+
// from the session/token, but letting the query param win here still meant a
|
|
65
|
+
// signed-in dev could be shown another vendor's branding + `tot checkout <theirs>`
|
|
66
|
+
// instructions, which is confusing/spoofable chrome even though nothing privileged
|
|
67
|
+
// leaked).
|
|
68
|
+
const wanted = (
|
|
69
|
+
session
|
|
70
|
+
? (session?.tenants?.[0]?.resource ?? session?.resource ?? "")
|
|
71
|
+
: (url.searchParams.get("tenant") ?? "")
|
|
72
|
+
).toLowerCase();
|
|
73
|
+
const tenant =
|
|
74
|
+
(wanted
|
|
75
|
+
? staticTenants.find((t) =>
|
|
76
|
+
[t.appDomain, t.tenant_id, t.canonicalDomain, ...t.domains].some(
|
|
77
|
+
(d) => d?.toLowerCase() === wanted,
|
|
78
|
+
),
|
|
79
|
+
)
|
|
80
|
+
: null) ??
|
|
81
|
+
staticTenants.find((t) => t.tenant_id === "tokenoftrust") ??
|
|
82
|
+
staticTenants[0];
|
|
83
|
+
|
|
84
|
+
const appDomain = tenant?.appDomain ?? "tokenoftrust.com";
|
|
85
|
+
// Signed in, but the account carries no developer capability on any store — an honest
|
|
86
|
+
// recovery state (not a 401): show what to do, not the fastpath.
|
|
87
|
+
const noStoreAccess = signedIn && !hasStoreAccess;
|
|
88
|
+
// The generic command shown (and copied) when we can't mint a personalised one.
|
|
89
|
+
// Pinned to the EXACT CLI version this build was cut alongside (`@tokenoftrust/cli@<version>`,
|
|
90
|
+
// astro.config Vite define) so the paste installs precisely the CLI this cockpit expects —
|
|
91
|
+
// never a floating dist-tag the terminal resolves. Used in the "prefer the steps" hint +
|
|
92
|
+
// (env-derived) the minted fastpath command.
|
|
93
|
+
const cliInstallSpec = __CLI_INSTALL_SPEC__;
|
|
94
|
+
|
|
95
|
+
// isLocalDevLoop === "the STOREFRONT app itself is running under `astro dev`" (a
|
|
96
|
+
// storefront engineer's local rig) — NOT the invited developer's dev loop (that's
|
|
97
|
+
// `tot dev`, the runner, which excludes this page entirely). It gates ONLY the
|
|
98
|
+
// local astro-dev cockpit — the Monitor/loopbar, in-browser Files editor, live
|
|
99
|
+
// preview + click-to-source, and the local stepper — all of which read the
|
|
100
|
+
// /__tot/* dev-plugins that exist ONLY under `astro dev`. import.meta.env.DEV is
|
|
101
|
+
// statically replaced with false in the production Cloudflare Worker build, so all
|
|
102
|
+
// of that is tree-shaken away there. Nothing that should reach a real developer on
|
|
103
|
+
// the deployed panel may hang off this flag — hosted product surface is either
|
|
104
|
+
// unconditional (see the "coming soon" shelf) or gated on `hostedActivity` below.
|
|
105
|
+
const isLocalDevLoop = import.meta.env.DEV;
|
|
106
|
+
|
|
107
|
+
// hostedActivity === the deployed hosted panel's activity-fed cockpit — the actual
|
|
108
|
+
// product surface the invited developer sees. It renders on the Worker (never under
|
|
109
|
+
// `astro dev`) for a signed-in developer with capability, and reflects what they're
|
|
110
|
+
// doing in their LOCAL dev loop via the activity bridge (POST /api/dev/activity →
|
|
111
|
+
// polled here). This is the "magic": the developer watches their own local rig work
|
|
112
|
+
// on our hosted surface. It is NOT a port of the local-only Monitor/Files/preview
|
|
113
|
+
// panels (those depend on /__tot/* endpoints that exist only under `astro dev`;
|
|
114
|
+
// bringing those hosted is cp-tree/cp-file/cp-preview, tracked separately).
|
|
115
|
+
const hostedActivity = !isLocalDevLoop && hasStoreAccess;
|
|
116
|
+
|
|
117
|
+
// cp-env (cloud-dev-cockpit Phase 0): the file-tree/viewer/diff + click-to-source
|
|
118
|
+
// cockpit scripts below (filesScript, c8Script) are ENVIRONMENT-AWARE — same client
|
|
119
|
+
// JS, different endpoint base. Local: the astro-dev-only /__tot/* plugins. Hosted: new
|
|
120
|
+
// /api/dev/* Astro API routes, landing incrementally (cp-tree/cp-file/cp-write/cp-diff/
|
|
121
|
+
// cp-locate — separate cards). Resolving it here, unconditionally, means those cards
|
|
122
|
+
// don't each have to invent the same env check; on isLocalDevLoop this is a no-op
|
|
123
|
+
// (resolves to the unchanged /__tot base). See decision architecture-environment-aware-cockpit.
|
|
124
|
+
const apiBase = resolveDevApiBase(isLocalDevLoop);
|
|
125
|
+
|
|
126
|
+
// Live loop Monitor (C1) client. Polls the dev server's /__tot/dev-loop-status
|
|
127
|
+
// and renders the loopbar: server health, watched file, and the save→reload
|
|
128
|
+
// timeline. Kept as a static string rendered via set:html (raw, un-escaped —
|
|
129
|
+
// the JS comparison operators must not be HTML-encoded) inside an is:inline
|
|
130
|
+
// nonce'd tag, gated on isLocalDevLoop so it's absent from the production build. Polling
|
|
131
|
+
// (not the WS custom event) keeps this a plain inline script with no Vite-module
|
|
132
|
+
// coupling, and it survives a full page reload — the server holds loopState, so
|
|
133
|
+
// the bar re-hydrates to the true state on every load. All dynamic values go
|
|
134
|
+
// through textContent, never innerHTML.
|
|
135
|
+
const monitorScript = `
|
|
136
|
+
(function () {
|
|
137
|
+
var bar = document.getElementById("loopbar");
|
|
138
|
+
if (!bar) return;
|
|
139
|
+
var elStatus = document.getElementById("lbStatus");
|
|
140
|
+
var elWatch = document.getElementById("lbWatch");
|
|
141
|
+
var elLast = document.getElementById("lbLast");
|
|
142
|
+
var elServer = document.getElementById("lbServer");
|
|
143
|
+
var elUrl = document.getElementById("lbUrl");
|
|
144
|
+
var elFile = document.getElementById("lbFile");
|
|
145
|
+
var elSave = document.getElementById("lbSave");
|
|
146
|
+
var elReload = document.getElementById("lbReload");
|
|
147
|
+
var head = document.getElementById("lbHead");
|
|
148
|
+
|
|
149
|
+
if (head) head.addEventListener("click", function () { bar.classList.toggle("open"); });
|
|
150
|
+
|
|
151
|
+
function rel(ts) {
|
|
152
|
+
var d = Math.max(0, Math.round((Date.now() - Number(ts)) / 1000));
|
|
153
|
+
return d < 2 ? "just now" : d < 60 ? d + "s ago" : Math.floor(d / 60) + "m ago";
|
|
154
|
+
}
|
|
155
|
+
function setTs(el, ts, emptyText) {
|
|
156
|
+
if (ts) {
|
|
157
|
+
el.setAttribute("data-ts", String(ts));
|
|
158
|
+
el.classList.remove("dim");
|
|
159
|
+
el.textContent = rel(ts);
|
|
160
|
+
} else {
|
|
161
|
+
el.removeAttribute("data-ts");
|
|
162
|
+
el.classList.add("dim");
|
|
163
|
+
el.textContent = emptyText;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
setInterval(function () {
|
|
167
|
+
[elSave, elReload].forEach(function (el) {
|
|
168
|
+
var ts = el.getAttribute("data-ts");
|
|
169
|
+
if (ts) el.textContent = rel(ts);
|
|
170
|
+
});
|
|
171
|
+
}, 1000);
|
|
172
|
+
|
|
173
|
+
function render(s) {
|
|
174
|
+
bar.classList.add("live");
|
|
175
|
+
var port = s.port || null;
|
|
176
|
+
var url = port ? ("localhost:" + port + (s.tenant ? "/" + s.tenant + "/" : "/")) : "—";
|
|
177
|
+
elServer.textContent = s.server ? ("running" + (port ? " · port " + port : "")) : "not running";
|
|
178
|
+
elServer.classList.toggle("dim", !s.server);
|
|
179
|
+
elUrl.textContent = url;
|
|
180
|
+
elUrl.classList.toggle("dim", !port);
|
|
181
|
+
elFile.textContent = s.watchedFile || "content/*.html";
|
|
182
|
+
elFile.classList.toggle("dim", !s.watchedFile);
|
|
183
|
+
elWatch.textContent = s.watchedFile || "your content";
|
|
184
|
+
setTs(elSave, s.lastSaveAt, "no saves yet");
|
|
185
|
+
setTs(elReload, s.lastReloadAt, "waiting for a save");
|
|
186
|
+
|
|
187
|
+
var saved = s.lastSaveAt || 0;
|
|
188
|
+
var reloaded = s.lastReloadAt || 0;
|
|
189
|
+
var state, status, last;
|
|
190
|
+
if (!saved) {
|
|
191
|
+
state = "running"; status = "Running"; last = "reloads on save";
|
|
192
|
+
} else if (reloaded >= saved) {
|
|
193
|
+
state = "running"; status = "Running"; last = "reloaded";
|
|
194
|
+
} else if (Date.now() - saved < 3000) {
|
|
195
|
+
state = "reloading"; status = "Reloading…"; last = "change detected";
|
|
196
|
+
} else {
|
|
197
|
+
state = "stale"; status = "Stale"; last = "save not reflected — restart?";
|
|
198
|
+
}
|
|
199
|
+
bar.setAttribute("data-state", state);
|
|
200
|
+
elStatus.textContent = status;
|
|
201
|
+
elLast.textContent = last;
|
|
202
|
+
// Broadcast the snapshot so other consumers (U2 stepper, U3 Ask) share this
|
|
203
|
+
// one poll instead of each opening their own. window.__totLoop holds the
|
|
204
|
+
// latest for listeners that attach late.
|
|
205
|
+
window.__totLoop = s;
|
|
206
|
+
window.dispatchEvent(new CustomEvent("tot:loop-state", { detail: s }));
|
|
207
|
+
}
|
|
208
|
+
function offline() {
|
|
209
|
+
bar.classList.add("live");
|
|
210
|
+
bar.setAttribute("data-state", "stale");
|
|
211
|
+
elStatus.textContent = "Offline";
|
|
212
|
+
elLast.textContent = "dev server not reachable";
|
|
213
|
+
elServer.textContent = "not reachable";
|
|
214
|
+
var off = { server: false, offline: true };
|
|
215
|
+
window.__totLoop = off;
|
|
216
|
+
window.dispatchEvent(new CustomEvent("tot:loop-state", { detail: off }));
|
|
217
|
+
}
|
|
218
|
+
function poll() {
|
|
219
|
+
fetch("/__tot/dev-loop-status", { cache: "no-store" })
|
|
220
|
+
.then(function (r) { return r.ok ? r.json() : Promise.reject(); })
|
|
221
|
+
.then(render)
|
|
222
|
+
.catch(offline);
|
|
223
|
+
}
|
|
224
|
+
poll();
|
|
225
|
+
setInterval(poll, 1000);
|
|
226
|
+
})();
|
|
227
|
+
`;
|
|
228
|
+
|
|
229
|
+
// Progressive stepper + staged flow (U2). Drives Run → Edit → What's next with
|
|
230
|
+
// progressive disclosure: only the current stage shows, completed stages collapse
|
|
231
|
+
// to a done-bar, and the flow AUTO-ADVANCES on C1's REAL loop signal (never a
|
|
232
|
+
// timer) — Step 1 completes when the local server is live, Step 2 when a real
|
|
233
|
+
// save→reload is observed since the edit step began. Consumes the shared
|
|
234
|
+
// tot:loop-state event from monitorScript (one poll for the whole page).
|
|
235
|
+
//
|
|
236
|
+
// Progress is persisted in sessionStorage. This is REQUIRED, not a nicety: saving
|
|
237
|
+
// a watched file triggers Vite's global full-reload, which reloads THIS /dev tab
|
|
238
|
+
// too — resetting in-memory state and re-baselining the very save that should
|
|
239
|
+
// advance Edit→Next. Persisting done-flags + current stage + the edit baseline
|
|
240
|
+
// (the lastSaveAt watermark captured when the edit step began, BEFORE the
|
|
241
|
+
// qualifying save) lets the flow survive that reload and credit the save.
|
|
242
|
+
//
|
|
243
|
+
// Dev-only, rendered via set:html inside an isLocalDevLoop-gated is:inline tag. All dynamic
|
|
244
|
+
// values via textContent.
|
|
245
|
+
const stepperScript = `
|
|
246
|
+
(function () {
|
|
247
|
+
var stepper = document.getElementById("stepper");
|
|
248
|
+
if (!stepper) return;
|
|
249
|
+
var stages = { run: document.getElementById("stage-run"), edit: document.getElementById("stage-edit"), next: document.getElementById("stage-next") };
|
|
250
|
+
var bars = { run: document.getElementById("bar-run"), edit: document.getElementById("bar-edit") };
|
|
251
|
+
var celRun = document.getElementById("cel-run");
|
|
252
|
+
var celEdit = document.getElementById("cel-edit");
|
|
253
|
+
var toNext = document.getElementById("toNext");
|
|
254
|
+
var detect = document.getElementById("detect");
|
|
255
|
+
var detectTxt = document.getElementById("detectTxt");
|
|
256
|
+
var detectEdit = document.getElementById("detect-edit");
|
|
257
|
+
var detectEditTxt = document.getElementById("detectEditTxt");
|
|
258
|
+
var barRunMeta = document.getElementById("bar-run-meta");
|
|
259
|
+
var barEditMeta = document.getElementById("bar-edit-meta");
|
|
260
|
+
var toast = document.getElementById("toast"), toastMsg = document.getElementById("toastMsg"), tT;
|
|
261
|
+
var reduce = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
262
|
+
|
|
263
|
+
var SS = window.sessionStorage;
|
|
264
|
+
function load(k, d) { try { var v = SS.getItem(k); return v === null ? d : JSON.parse(v); } catch (e) { return d; } }
|
|
265
|
+
function put(k, v) { try { SS.setItem(k, JSON.stringify(v)); } catch (e) {} }
|
|
266
|
+
|
|
267
|
+
function ping(m, once) {
|
|
268
|
+
if (once && load(once, false)) return;
|
|
269
|
+
if (once) put(once, true);
|
|
270
|
+
if (!toast || !toastMsg) return;
|
|
271
|
+
toastMsg.textContent = m; toast.classList.add("show");
|
|
272
|
+
clearTimeout(tT); tT = setTimeout(function () { toast.classList.remove("show"); }, 1900);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
var state = {
|
|
276
|
+
unlocked: load("tot.unlocked", { run: true, edit: false, next: false }),
|
|
277
|
+
done: load("tot.done", { run: false, edit: false }),
|
|
278
|
+
current: load("tot.current", "run"),
|
|
279
|
+
};
|
|
280
|
+
var editBase = load("tot.editBase", null);
|
|
281
|
+
function persist() { put("tot.unlocked", state.unlocked); put("tot.done", state.done); put("tot.current", state.current); }
|
|
282
|
+
|
|
283
|
+
function render() {
|
|
284
|
+
Array.prototype.forEach.call(stepper.querySelectorAll(".snode"), function (n) {
|
|
285
|
+
var s = n.getAttribute("data-go");
|
|
286
|
+
n.setAttribute("data-st", state.current === s ? "active" : state.done[s] ? "done" : state.unlocked[s] ? "done" : "locked");
|
|
287
|
+
});
|
|
288
|
+
Object.keys(stages).forEach(function (s) { if (stages[s]) stages[s].hidden = state.current !== s; });
|
|
289
|
+
if (bars.run) bars.run.hidden = !(state.done.run && state.current !== "run");
|
|
290
|
+
if (bars.edit) bars.edit.hidden = !(state.done.edit && state.current !== "edit");
|
|
291
|
+
// Contextual celebrate banners reflect achieved state (survive reloads).
|
|
292
|
+
if (celRun && state.done.run) celRun.hidden = false;
|
|
293
|
+
if (celEdit && state.done.edit) celEdit.hidden = false;
|
|
294
|
+
if (toNext) toNext.disabled = !state.unlocked.next;
|
|
295
|
+
}
|
|
296
|
+
function goTo(s) {
|
|
297
|
+
if (!state.unlocked[s]) return;
|
|
298
|
+
state.current = s; persist(); render();
|
|
299
|
+
window.scrollTo({ top: 0, behavior: reduce ? "auto" : "smooth" });
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function onSignal(s) {
|
|
303
|
+
if (!s || s.offline) return;
|
|
304
|
+
|
|
305
|
+
// Step 1 — local server is live.
|
|
306
|
+
if (s.server && !state.done.run) {
|
|
307
|
+
state.done.run = true; state.unlocked.edit = true;
|
|
308
|
+
if (editBase === null) { editBase = s.lastSaveAt || 0; put("tot.editBase", editBase); }
|
|
309
|
+
persist();
|
|
310
|
+
if (barRunMeta && s.port) barRunMeta.textContent = "localhost:" + s.port + (s.tenant ? "/" + s.tenant + "/" : "/");
|
|
311
|
+
if (detect) detect.setAttribute("data-listening", "false");
|
|
312
|
+
if (detectTxt) detectTxt.textContent = "Your store is live on this machine.";
|
|
313
|
+
if (state.current === "run") { goTo("edit"); } else { render(); }
|
|
314
|
+
ping("Your store is live", "tot.pinged.run");
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Step 2 — a NEW save (since the edit step began) has been reflected by a reload.
|
|
318
|
+
if (state.done.run && !state.done.edit && editBase !== null) {
|
|
319
|
+
var newSave = (s.lastSaveAt || 0) > editBase;
|
|
320
|
+
var reflected = (s.lastSaveAt || 0) > 0 && (s.lastReloadAt || 0) >= (s.lastSaveAt || 0);
|
|
321
|
+
if (newSave && !reflected && detectEditTxt) {
|
|
322
|
+
detectEditTxt.textContent = "Change detected — reloading your store…";
|
|
323
|
+
}
|
|
324
|
+
if (newSave && reflected) {
|
|
325
|
+
state.done.edit = true; state.unlocked.next = true; persist();
|
|
326
|
+
if (barEditMeta && s.watchedFile) barEditMeta.textContent = s.watchedFile;
|
|
327
|
+
if (detectEdit) detectEdit.setAttribute("data-listening", "false");
|
|
328
|
+
if (detectEditTxt) detectEditTxt.textContent = "Saved — your store reloaded. That's the loop.";
|
|
329
|
+
if (state.current === "edit") { goTo("next"); } else { render(); }
|
|
330
|
+
ping("Saved — your page reloaded", "tot.pinged.edit");
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
render();
|
|
336
|
+
Array.prototype.forEach.call(document.querySelectorAll("[data-go]"), function (el) {
|
|
337
|
+
el.addEventListener("click", function () { goTo(el.getAttribute("data-go")); });
|
|
338
|
+
});
|
|
339
|
+
window.addEventListener("tot:loop-state", function (e) { onSignal(e.detail); });
|
|
340
|
+
if (window.__totLoop) onSignal(window.__totLoop);
|
|
341
|
+
})();
|
|
342
|
+
`;
|
|
343
|
+
|
|
344
|
+
// "Stuck? Ask" recovery affordance (U3). A quiet FAB that opens a scoped Q&A for
|
|
345
|
+
// "why isn't the loop working" — NOT general AI editing. Static answers ship as
|
|
346
|
+
// markup; the two stateful answers (is it running? / save-vs-reload) update from
|
|
347
|
+
// the shared tot:loop-state signal + window.__totLoop (C1/U2 — no second poll).
|
|
348
|
+
// "Reconnect preview" reloads the page, re-establishing the HMR connection and
|
|
349
|
+
// re-reading loop state (recovers a stale client); a fully-down server is handled
|
|
350
|
+
// by the answer text (run tot start). Dev-only, set:html in an isLocalDevLoop-gated tag.
|
|
351
|
+
const askScript = `
|
|
352
|
+
(function () {
|
|
353
|
+
var fab = document.getElementById("askFab");
|
|
354
|
+
var panel = document.getElementById("askPanel");
|
|
355
|
+
if (!fab || !panel) return;
|
|
356
|
+
var ansRunning = document.getElementById("ans-running");
|
|
357
|
+
var ansChange = document.getElementById("ans-change");
|
|
358
|
+
var restart = document.getElementById("restartBtn");
|
|
359
|
+
|
|
360
|
+
fab.addEventListener("click", function (e) { e.stopPropagation(); panel.classList.toggle("open"); });
|
|
361
|
+
document.addEventListener("click", function (e) {
|
|
362
|
+
if (!panel.contains(e.target) && e.target !== fab && !fab.contains(e.target)) panel.classList.remove("open");
|
|
363
|
+
});
|
|
364
|
+
if (restart) restart.addEventListener("click", function () { location.reload(); });
|
|
365
|
+
|
|
366
|
+
function rel(ts) {
|
|
367
|
+
var d = Math.max(0, Math.round((Date.now() - Number(ts)) / 1000));
|
|
368
|
+
return d < 2 ? "just now" : d < 60 ? d + "s ago" : Math.floor(d / 60) + "m ago";
|
|
369
|
+
}
|
|
370
|
+
function update(s) {
|
|
371
|
+
if (!s) return;
|
|
372
|
+
if (ansRunning) {
|
|
373
|
+
ansRunning.textContent = (s.offline || !s.server)
|
|
374
|
+
? "Your dev server isn't reachable right now — run tot start in your terminal, then reload this page."
|
|
375
|
+
: "Yes — your server is running" + (s.port ? " on port " + s.port : "") + ". This page is connected to it.";
|
|
376
|
+
}
|
|
377
|
+
if (ansChange) {
|
|
378
|
+
if (!s.lastSaveAt) {
|
|
379
|
+
ansChange.textContent = "No saves seen yet. Edit a watched file (like content/home.html) and save — it'll show up here.";
|
|
380
|
+
} else if ((s.lastReloadAt || 0) >= s.lastSaveAt) {
|
|
381
|
+
ansChange.textContent = "Working: your last save (" + rel(s.lastSaveAt) + ") triggered a reload (" + rel(s.lastReloadAt) + "). If the page didn't visibly change, make sure you edited the file the page renders from.";
|
|
382
|
+
} else {
|
|
383
|
+
ansChange.textContent = "Your save (" + rel(s.lastSaveAt) + ") hasn't been reflected by a reload yet — the preview may be stale. Try Reconnect preview below.";
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
window.addEventListener("tot:loop-state", function (e) { update(e.detail); });
|
|
388
|
+
if (window.__totLoop) update(window.__totLoop);
|
|
389
|
+
})();
|
|
390
|
+
`;
|
|
391
|
+
|
|
392
|
+
// C5 · Files tab — in-browser file tree + LIVE EDITOR. Dev-only, rendered via
|
|
393
|
+
// set:html inside an isLocalDevLoop-gated is:inline nonce'd tag. Reads /__tot/tree +
|
|
394
|
+
// /__tot/file and writes /__tot/write (dev-plugins/dev-file-browser + dev-write).
|
|
395
|
+
// Writable files (content HTML + theme.json) become an editable textarea with
|
|
396
|
+
// ~600ms debounce autosave + Cmd/Ctrl+S; the save carries the C6 baseVersion so a
|
|
397
|
+
// concurrent $EDITOR/git change is caught (409) instead of clobbered. Dynamic
|
|
398
|
+
// values go through .value / textContent (never innerHTML) per the C1 decision.
|
|
399
|
+
const filesScript = `
|
|
400
|
+
(function () {
|
|
401
|
+
var apiBase = ${JSON.stringify(apiBase)};
|
|
402
|
+
var isDev = ${JSON.stringify(isLocalDevLoop)};
|
|
403
|
+
var offlineMsg = isDev ? "Could not reach the dev server." : "Not available on the hosted panel yet \\u2014 check back soon.";
|
|
404
|
+
var treeEl = document.getElementById("filesTree");
|
|
405
|
+
var viewEl = document.getElementById("filesView");
|
|
406
|
+
var pathEl = document.getElementById("filesPath");
|
|
407
|
+
var editbar = document.getElementById("filesEditbar");
|
|
408
|
+
var statusEl = document.getElementById("filesStatus");
|
|
409
|
+
var conflictEl = document.getElementById("filesConflict");
|
|
410
|
+
var reloadBtn = document.getElementById("filesReload");
|
|
411
|
+
var forceBtn = document.getElementById("filesForce");
|
|
412
|
+
var diffBtn = document.getElementById("filesDiffBtn");
|
|
413
|
+
var diffEl = document.getElementById("filesDiff");
|
|
414
|
+
var publishBtn = document.getElementById("filesPublishBtn");
|
|
415
|
+
if (!treeEl || !viewEl) return;
|
|
416
|
+
var selected = null;
|
|
417
|
+
var cur = null;
|
|
418
|
+
var saveTimer = null;
|
|
419
|
+
var diffOn = false;
|
|
420
|
+
var rowByPath = {}; // path -> tree row element, for programmatic open (C8 jump)
|
|
421
|
+
|
|
422
|
+
function setStatus(txt, cls) {
|
|
423
|
+
if (!statusEl) return;
|
|
424
|
+
statusEl.textContent = txt || "";
|
|
425
|
+
statusEl.className = "df-status" + (cls ? " " + cls : "");
|
|
426
|
+
}
|
|
427
|
+
function showConflict(show) { if (conflictEl) conflictEl.hidden = !show; }
|
|
428
|
+
function showBar(show) { if (editbar) editbar.hidden = !show; }
|
|
429
|
+
|
|
430
|
+
function openFile(path, row, afterLoad) {
|
|
431
|
+
if (selected) selected.classList.remove("sel");
|
|
432
|
+
selected = row; row.classList.add("sel");
|
|
433
|
+
if (pathEl) pathEl.textContent = path;
|
|
434
|
+
showConflict(false); showBar(true);
|
|
435
|
+
setDiffMode(false); if (diffBtn) diffBtn.hidden = true;
|
|
436
|
+
if (publishBtn) publishBtn.hidden = true;
|
|
437
|
+
viewEl.readOnly = true;
|
|
438
|
+
setStatus("Loading\\u2026");
|
|
439
|
+
fetch(apiBase + "/file?path=" + encodeURIComponent(path))
|
|
440
|
+
.then(function (r) { return r.json(); })
|
|
441
|
+
.then(function (d) {
|
|
442
|
+
if (!d || !d.ok) { viewEl.value = ""; cur = null; setStatus((d && d.message) || "Could not open that file.", "warn"); return; }
|
|
443
|
+
if (d.binary) { viewEl.value = "(binary file \\u2014 " + (d.size || 0) + " bytes)"; cur = null; setStatus("Binary \\u2014 not editable"); return; }
|
|
444
|
+
if (d.tooBig) { viewEl.value = "(file too large to show \\u2014 " + (d.size || 0) + " bytes)"; cur = null; setStatus("Too large \\u2014 not editable"); return; }
|
|
445
|
+
viewEl.value = d.content == null ? "" : d.content;
|
|
446
|
+
cur = { path: path, baseVersion: (typeof d.mtimeMs === "number" ? d.mtimeMs : null), writable: !!d.writable };
|
|
447
|
+
viewEl.readOnly = !cur.writable;
|
|
448
|
+
if (diffBtn) diffBtn.hidden = false;
|
|
449
|
+
if (publishBtn) publishBtn.hidden = !cur.writable;
|
|
450
|
+
setStatus(cur.writable ? "Editable \\u2014 edits save + reload your store" : "Read-only");
|
|
451
|
+
if (typeof afterLoad === "function") afterLoad();
|
|
452
|
+
})
|
|
453
|
+
.catch(function () { viewEl.value = ""; cur = null; setStatus(offlineMsg, "warn"); });
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Place the textarea caret at a 1-based line:col and scroll it into view. Used by
|
|
457
|
+
// the C8 click-to-source jump; a textarea has no CodeMirror EditorView, so we map
|
|
458
|
+
// (line,col) to a character offset + setSelectionRange, then center by scrollTop.
|
|
459
|
+
function placeCursor(line, col) {
|
|
460
|
+
var text = viewEl.value;
|
|
461
|
+
var lines = text.split("\\n");
|
|
462
|
+
var ln = Math.min(Math.max(1, line || 1), lines.length);
|
|
463
|
+
var offset = 0;
|
|
464
|
+
for (var i = 0; i < ln - 1; i++) offset += lines[i].length + 1; // +1 for the newline
|
|
465
|
+
offset += Math.min(Math.max(1, col || 1), (lines[ln - 1] || "").length + 1) - 1;
|
|
466
|
+
viewEl.focus();
|
|
467
|
+
try { viewEl.setSelectionRange(offset, offset); } catch (e) { /* not focusable yet */ }
|
|
468
|
+
// Center the target line: approximate line height from the rendered content.
|
|
469
|
+
var lh = viewEl.scrollHeight / Math.max(1, lines.length);
|
|
470
|
+
viewEl.scrollTop = Math.max(0, (ln - 1) * lh - viewEl.clientHeight / 2);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// C8 capstone entry point: open <path> and drop the caret at line:col. Invoked by
|
|
474
|
+
// the cockpit's origin-checked tot:locate listener when a click lands in the
|
|
475
|
+
// live-preview iframe. Finds the file's tree row (built by renderNodes); if the
|
|
476
|
+
// tree hasn't loaded yet, no-ops gracefully.
|
|
477
|
+
window.__totOpenFileAt = function (path, line, col) {
|
|
478
|
+
var row = rowByPath[path];
|
|
479
|
+
if (!row) return false;
|
|
480
|
+
if (typeof row.scrollIntoView === "function") row.scrollIntoView({ block: "nearest" });
|
|
481
|
+
openFile(path, row, function () { placeCursor(line, col); });
|
|
482
|
+
return true;
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
function doSave(force) {
|
|
486
|
+
if (!cur || !cur.writable) return;
|
|
487
|
+
setStatus("Saving\\u2026");
|
|
488
|
+
var body = { path: cur.path, content: viewEl.value };
|
|
489
|
+
if (!force && typeof cur.baseVersion === "number") body.baseVersion = cur.baseVersion;
|
|
490
|
+
fetch(apiBase + "/write", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) })
|
|
491
|
+
.then(function (r) { return r.json().then(function (j) { return { s: r.status, j: j }; }); })
|
|
492
|
+
.then(function (res) {
|
|
493
|
+
if (res.s === 200 && res.j && res.j.ok) { cur.baseVersion = res.j.mtimeMs; showConflict(false); setStatus("Saved \\u2713", "ok"); return; }
|
|
494
|
+
if (res.s === 409) { showConflict(true); setStatus("Changed on disk since you opened it", "warn"); return; }
|
|
495
|
+
if (res.s === 403) { setStatus("This file isn't editable.", "warn"); return; }
|
|
496
|
+
setStatus((res.j && res.j.message) || "Save failed", "warn");
|
|
497
|
+
})
|
|
498
|
+
.catch(function () { setStatus(isDev ? "Save failed \\u2014 dev server unreachable" : offlineMsg, "warn"); });
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
viewEl.addEventListener("input", function () {
|
|
502
|
+
if (!cur || !cur.writable) return;
|
|
503
|
+
setStatus("Editing\\u2026");
|
|
504
|
+
if (saveTimer) clearTimeout(saveTimer);
|
|
505
|
+
saveTimer = setTimeout(function () { doSave(false); }, 600);
|
|
506
|
+
});
|
|
507
|
+
viewEl.addEventListener("keydown", function (e) {
|
|
508
|
+
if ((e.metaKey || e.ctrlKey) && (e.key === "s" || e.key === "S")) {
|
|
509
|
+
e.preventDefault();
|
|
510
|
+
if (saveTimer) clearTimeout(saveTimer);
|
|
511
|
+
doSave(false);
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
if (reloadBtn) reloadBtn.addEventListener("click", function () { if (cur && selected) openFile(cur.path, selected); });
|
|
515
|
+
if (forceBtn) forceBtn.addEventListener("click", function () { showConflict(false); doSave(true); });
|
|
516
|
+
|
|
517
|
+
function setDiffMode(on) {
|
|
518
|
+
diffOn = on;
|
|
519
|
+
if (diffEl) diffEl.hidden = !on;
|
|
520
|
+
viewEl.hidden = on;
|
|
521
|
+
if (diffBtn) diffBtn.textContent = on ? "Edit" : "Diff";
|
|
522
|
+
}
|
|
523
|
+
function renderDiffText(t) { if (diffEl) diffEl.textContent = t; }
|
|
524
|
+
function lineDiff(before, after) {
|
|
525
|
+
var a = before.split("\\n"), b = after.split("\\n");
|
|
526
|
+
var m = a.length, n = b.length;
|
|
527
|
+
if (m * n > 4000000) return null;
|
|
528
|
+
var dp = []; for (var i = 0; i <= m; i++) { dp.push(new Array(n + 1).fill(0)); }
|
|
529
|
+
for (var i = m - 1; i >= 0; i--) for (var j = n - 1; j >= 0; j--) dp[i][j] = a[i] === b[j] ? dp[i + 1][j + 1] + 1 : (dp[i + 1][j] >= dp[i][j + 1] ? dp[i + 1][j] : dp[i][j + 1]);
|
|
530
|
+
var out = [], x = 0, y = 0;
|
|
531
|
+
while (x < m && y < n) {
|
|
532
|
+
if (a[x] === b[y]) { out.push([" ", a[x]]); x++; y++; }
|
|
533
|
+
else if (dp[x + 1][y] >= dp[x][y + 1]) { out.push(["-", a[x]]); x++; }
|
|
534
|
+
else { out.push(["+", b[y]]); y++; }
|
|
535
|
+
}
|
|
536
|
+
while (x < m) { out.push(["-", a[x]]); x++; }
|
|
537
|
+
while (y < n) { out.push(["+", b[y]]); y++; }
|
|
538
|
+
return out;
|
|
539
|
+
}
|
|
540
|
+
function renderDiff(before, after) {
|
|
541
|
+
if (!diffEl) return;
|
|
542
|
+
diffEl.textContent = "";
|
|
543
|
+
var rows = lineDiff(before, after);
|
|
544
|
+
if (!rows) { renderDiffText("(file too large to diff)"); return; }
|
|
545
|
+
var frag = document.createDocumentFragment();
|
|
546
|
+
rows.forEach(function (row) {
|
|
547
|
+
var ln = document.createElement("span");
|
|
548
|
+
ln.className = "dl " + (row[0] === "+" ? "add" : row[0] === "-" ? "del" : "ctx");
|
|
549
|
+
ln.textContent = row[0] + " " + row[1];
|
|
550
|
+
frag.appendChild(ln);
|
|
551
|
+
});
|
|
552
|
+
diffEl.appendChild(frag);
|
|
553
|
+
}
|
|
554
|
+
function doDiff() {
|
|
555
|
+
if (!cur) return;
|
|
556
|
+
if (diffOn) { setDiffMode(false); setStatus(cur.writable ? "Editable" : "Read-only"); return; }
|
|
557
|
+
setStatus("Loading diff\\u2026");
|
|
558
|
+
fetch(apiBase + "/diff?path=" + encodeURIComponent(cur.path))
|
|
559
|
+
.then(function (r) { return r.json(); })
|
|
560
|
+
.then(function (d) {
|
|
561
|
+
if (!d || !d.ok) { setStatus((d && d.message) || "Could not diff.", "warn"); return; }
|
|
562
|
+
if (!d.shown) { renderDiffText("(diff not shown \\u2014 " + (d.reason || "") + ")"); setDiffMode(true); setStatus(d.status); return; }
|
|
563
|
+
renderDiff(d.before || "", d.after || "");
|
|
564
|
+
setDiffMode(true);
|
|
565
|
+
setStatus(d.status === "unchanged" ? "No changes vs your last commit" : (d.status + " vs HEAD"));
|
|
566
|
+
})
|
|
567
|
+
.catch(function () { setStatus(offlineMsg, "warn"); });
|
|
568
|
+
}
|
|
569
|
+
if (diffBtn) diffBtn.addEventListener("click", doDiff);
|
|
570
|
+
|
|
571
|
+
// cp-publish: draft -> TEST channel (never live — see decision
|
|
572
|
+
// cp-publish-scope-correction). Confirm-before-publish per the card.
|
|
573
|
+
function doPublish() {
|
|
574
|
+
if (!cur || !cur.writable || !publishBtn) return;
|
|
575
|
+
if (!window.confirm("Publish your saved edit to the test channel? (Not your live site.)")) return;
|
|
576
|
+
var prevLabel = publishBtn.textContent;
|
|
577
|
+
publishBtn.disabled = true;
|
|
578
|
+
setStatus("Publishing\\u2026");
|
|
579
|
+
fetch(apiBase + "/publish", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ path: cur.path }) })
|
|
580
|
+
.then(function (r) { return r.json().then(function (j) { return { s: r.status, j: j }; }); })
|
|
581
|
+
.then(function (res) {
|
|
582
|
+
if (res.s === 200 && res.j && res.j.ok) { setStatus(res.j.applied ? "Published to test \\u2713" : "Published (not persisted \\u2014 no storage configured)", "ok"); return; }
|
|
583
|
+
if (res.s === 409 && res.j && res.j.status === "no_draft") { setStatus("No unpublished edit to publish.", "warn"); return; }
|
|
584
|
+
setStatus((res.j && res.j.message) || "Publish failed", "warn");
|
|
585
|
+
})
|
|
586
|
+
.catch(function () { setStatus(offlineMsg, "warn"); })
|
|
587
|
+
.then(function () { publishBtn.disabled = false; publishBtn.textContent = prevLabel; });
|
|
588
|
+
}
|
|
589
|
+
if (publishBtn) publishBtn.addEventListener("click", doPublish);
|
|
590
|
+
|
|
591
|
+
function renderNodes(nodes, parentUl, depth) {
|
|
592
|
+
nodes.forEach(function (n) {
|
|
593
|
+
var li = document.createElement("li");
|
|
594
|
+
li.className = "ft-" + n.type;
|
|
595
|
+
var row = document.createElement("div");
|
|
596
|
+
row.className = "ft-row";
|
|
597
|
+
row.style.paddingLeft = (depth * 12 + 10) + "px";
|
|
598
|
+
var icon = document.createElement("span");
|
|
599
|
+
icon.className = "ft-ic";
|
|
600
|
+
icon.textContent = n.type === "dir" ? "\\uD83D\\uDCC1" : "\\uD83D\\uDCC4";
|
|
601
|
+
var label = document.createElement("span");
|
|
602
|
+
label.className = "ft-name";
|
|
603
|
+
label.textContent = n.name;
|
|
604
|
+
row.appendChild(icon); row.appendChild(label); li.appendChild(row);
|
|
605
|
+
if (n.type === "dir") {
|
|
606
|
+
var childUl = document.createElement("ul");
|
|
607
|
+
childUl.className = "ft-children";
|
|
608
|
+
renderNodes(n.children || [], childUl, depth + 1);
|
|
609
|
+
li.appendChild(childUl);
|
|
610
|
+
} else {
|
|
611
|
+
row.tabIndex = 0;
|
|
612
|
+
rowByPath[n.path] = row; // for the C8 programmatic jump
|
|
613
|
+
row.addEventListener("click", function () { openFile(n.path, row); });
|
|
614
|
+
row.addEventListener("keydown", function (e) { if (e.key === "Enter") { e.preventDefault(); openFile(n.path, row); } });
|
|
615
|
+
}
|
|
616
|
+
parentUl.appendChild(li);
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function load() {
|
|
621
|
+
fetch(apiBase + "/tree")
|
|
622
|
+
.then(function (r) { return r.json(); })
|
|
623
|
+
.then(function (d) {
|
|
624
|
+
treeEl.textContent = "";
|
|
625
|
+
rowByPath = {}; // rebuild the path→row index for this fresh tree
|
|
626
|
+
if (!d || !d.ok) { treeEl.textContent = (d && d.message) || "Could not load your files."; return; }
|
|
627
|
+
var ul = document.createElement("ul");
|
|
628
|
+
ul.className = "ft-root";
|
|
629
|
+
renderNodes(d.tree || [], ul, 0);
|
|
630
|
+
treeEl.appendChild(ul);
|
|
631
|
+
})
|
|
632
|
+
.catch(function () { treeEl.textContent = offlineMsg; });
|
|
633
|
+
}
|
|
634
|
+
load();
|
|
635
|
+
})();
|
|
636
|
+
`;
|
|
637
|
+
|
|
638
|
+
// C8 · Click-to-source capstone — the cockpit side. Runs the live-preview iframe
|
|
639
|
+
// (mini + full-screen dialog) and LISTENS for the framed picker's tot:locate
|
|
640
|
+
// postMessage. The picker (ai-widget-client.js, auto-injected into the same-origin
|
|
641
|
+
// preview) resolves a click to {path,line,col} via /__tot/locate, then — because it
|
|
642
|
+
// is framed — postMessages the result here instead of showing its own toast. We
|
|
643
|
+
// verify event.origin === our origin, then hand off to the Files panel's
|
|
644
|
+
// window.__totOpenFileAt to open the source + drop the caret. Only content-region
|
|
645
|
+
// hits ever arrive (chrome/refused clicks stay silent in the frame), per decision
|
|
646
|
+
// click-to-source-anchors-at-content-region.
|
|
647
|
+
const c8Script = `
|
|
648
|
+
(function () {
|
|
649
|
+
var isDev = ${JSON.stringify(isLocalDevLoop)};
|
|
650
|
+
var hostedAppDomain = ${JSON.stringify(appDomain)};
|
|
651
|
+
var dlg = document.getElementById("previewDialog");
|
|
652
|
+
var expandBtn = document.getElementById("previewExpand");
|
|
653
|
+
var closeBtn = document.getElementById("previewClose");
|
|
654
|
+
var dlgFrame = document.getElementById("previewDialogFrame");
|
|
655
|
+
var miniFrame = document.getElementById("previewFrame");
|
|
656
|
+
var urlEl = document.getElementById("previewUrl");
|
|
657
|
+
|
|
658
|
+
if (miniFrame) {
|
|
659
|
+
if (isDev) {
|
|
660
|
+
// Point the preview at the DEV tenant (TOT_DEV_TENANT), read from the server
|
|
661
|
+
// via /__tot/dev-loop-status — NOT the SSR appDomain (workerd can't see
|
|
662
|
+
// TOT_DEV_*). Same-origin, so the auto-injected picker can postMessage back.
|
|
663
|
+
fetch("/__tot/dev-loop-status")
|
|
664
|
+
.then(function (r) { return r.json(); })
|
|
665
|
+
.then(function (s) {
|
|
666
|
+
var tenant = s && s.tenant;
|
|
667
|
+
if (!tenant) return;
|
|
668
|
+
var src = "/" + tenant + "/";
|
|
669
|
+
miniFrame.src = src;
|
|
670
|
+
if (urlEl) urlEl.textContent = "localhost preview · " + tenant;
|
|
671
|
+
})
|
|
672
|
+
.catch(function () { /* dev server unreachable — preview stays blank */ });
|
|
673
|
+
} else if (hostedAppDomain) {
|
|
674
|
+
// Hosted: the SSR-resolved tenant IS the signed-in developer's own store
|
|
675
|
+
// (cp-env's capability gate already scoped it) — same-origin, so a cp-write
|
|
676
|
+
// save + this reload shows the developer's own draft overlay via the
|
|
677
|
+
// middleware's tot_dev_draft cookie check.
|
|
678
|
+
miniFrame.src = "/" + hostedAppDomain + "/";
|
|
679
|
+
if (urlEl) urlEl.textContent = "your store · " + hostedAppDomain;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
if (expandBtn && dlg) {
|
|
684
|
+
expandBtn.addEventListener("click", function () {
|
|
685
|
+
if (dlgFrame && !dlgFrame.getAttribute("src") && miniFrame) dlgFrame.src = miniFrame.getAttribute("src") || miniFrame.src;
|
|
686
|
+
if (typeof dlg.showModal === "function") dlg.showModal(); else dlg.setAttribute("open", "");
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
if (closeBtn && dlg) closeBtn.addEventListener("click", function () { dlg.close ? dlg.close() : dlg.removeAttribute("open"); });
|
|
690
|
+
if (dlg) dlg.addEventListener("click", function (e) { if (e.target === dlg) { dlg.close ? dlg.close() : dlg.removeAttribute("open"); } });
|
|
691
|
+
|
|
692
|
+
// The one privileged inbound message: a source location from OUR OWN origin's
|
|
693
|
+
// framed preview. Reject any other origin, shape, or type outright.
|
|
694
|
+
window.addEventListener("message", function (e) {
|
|
695
|
+
if (e.origin !== window.location.origin) return;
|
|
696
|
+
var d = e.data;
|
|
697
|
+
if (!d || d.type !== "tot:locate" || typeof d.path !== "string") return;
|
|
698
|
+
if (typeof window.__totOpenFileAt === "function") {
|
|
699
|
+
var opened = window.__totOpenFileAt(d.path, d.line, d.col);
|
|
700
|
+
// If the preview was expanded full-screen, close it so the editor + caret are visible.
|
|
701
|
+
if (opened && dlg && dlg.open) { dlg.close ? dlg.close() : dlg.removeAttribute("open"); }
|
|
702
|
+
}
|
|
703
|
+
});
|
|
704
|
+
})();
|
|
705
|
+
`;
|
|
706
|
+
|
|
707
|
+
// Hosted /dev activity log (hosted-dev-activity-bridge). Polls the NEW
|
|
708
|
+
// GET /api/dev/activity — a translated, file-level feed of what the local dev
|
|
709
|
+
// loop reported — and renders it newest-first. Also drives the "your edit
|
|
710
|
+
// landed" celebration: the first poll primes a baseline (sessionStorage, same
|
|
711
|
+
// survive-a-reload rationale as the local stepper's editBase), and any LATER
|
|
712
|
+
// event past that baseline triggers the banner + a lightweight confetti burst.
|
|
713
|
+
// POLL (not SSE): Workers are stateless per request, so a redeploy mid-session
|
|
714
|
+
// needs no reconnect logic here. Rendered only when hostedActivity (never
|
|
715
|
+
// alongside the isLocalDevLoop-gated local scripts above).
|
|
716
|
+
const hostedActivityScript = `
|
|
717
|
+
(function () {
|
|
718
|
+
var root = document.getElementById("hostedActivity");
|
|
719
|
+
if (!root) return;
|
|
720
|
+
|
|
721
|
+
// ?reset — a TRUE reset for re-testing the onboarding loop: clear THIS browser's
|
|
722
|
+
// stepper progress AND the server-side KV (live-runtime record + activity buffer),
|
|
723
|
+
// then reload clean. Clearing the server too means a stopped local loop stops reading
|
|
724
|
+
// as "up" immediately instead of lingering until the runtime TTL/live-window expires.
|
|
725
|
+
try {
|
|
726
|
+
if (/[?&]reset\\b/.test(location.search)) {
|
|
727
|
+
["tot.hosted.steps", "tot.hosted.current", "tot.hosted.editBase"].forEach(function (k) {
|
|
728
|
+
try { window.sessionStorage.removeItem(k); } catch (e) {}
|
|
729
|
+
});
|
|
730
|
+
var reloaded = false;
|
|
731
|
+
var doneReset = function () { if (reloaded) return; reloaded = true; location.replace(location.pathname); };
|
|
732
|
+
setTimeout(doneReset, 2000); // fallback so a hung request still lands us fresh
|
|
733
|
+
try { fetch("/api/dev/activity", { method: "DELETE", cache: "no-store" }).then(doneReset, doneReset); }
|
|
734
|
+
catch (e) { doneReset(); }
|
|
735
|
+
return; // the post-clear reload re-runs this script against a fresh, empty state
|
|
736
|
+
}
|
|
737
|
+
} catch (e) {}
|
|
738
|
+
|
|
739
|
+
var tableEl = document.getElementById("haTable");
|
|
740
|
+
var bodyEl = document.getElementById("haBody");
|
|
741
|
+
var emptyEl = document.getElementById("haEmpty");
|
|
742
|
+
|
|
743
|
+
// Bridge strip — the rail's power source, fed by the SAME poll's \`runtime\` object
|
|
744
|
+
// (no second poller). \`runtime\` is { cliVersion, runnerVersion, url, aliveAt, live }
|
|
745
|
+
// reported by the developer's local \`tot\` CLI; absent until its data layer lands,
|
|
746
|
+
// so every read here is defensive (missing runtime -> the strip stays hidden).
|
|
747
|
+
var bridge = document.getElementById("bridge");
|
|
748
|
+
var bUrl = document.getElementById("bUrl");
|
|
749
|
+
var bGo = document.getElementById("bGo");
|
|
750
|
+
var bKicker = document.getElementById("bKicker");
|
|
751
|
+
var bHint = document.getElementById("bHint");
|
|
752
|
+
var bDiag = document.getElementById("bDiag");
|
|
753
|
+
var doneRunMeta = document.getElementById("doneRunMeta");
|
|
754
|
+
function updateBridge(rt) {
|
|
755
|
+
if (!bridge) return;
|
|
756
|
+
if (!rt || !rt.url) { bridge.hidden = true; return; }
|
|
757
|
+
var live = !!rt.live;
|
|
758
|
+
if (live) sawLive = true;
|
|
759
|
+
// On FIRST arrival, a stale runtime record (offline, never seen live this
|
|
760
|
+
// page-view — e.g. left over from a prior run, still within the heartbeat TTL)
|
|
761
|
+
// must NOT show — let Step 1 "Get your store running" do the work. Only surface
|
|
762
|
+
// the strip once it's live, or after it's gone quiet HAVING been live this page-view.
|
|
763
|
+
if (!live && !sawLive) { bridge.hidden = true; return; }
|
|
764
|
+
bridge.hidden = false;
|
|
765
|
+
bridge.setAttribute("data-live", live ? "1" : "0");
|
|
766
|
+
if (bUrl) { bUrl.textContent = rt.url; bUrl.href = rt.url; }
|
|
767
|
+
// No point offering "Open your store" to a server that stopped answering.
|
|
768
|
+
if (bGo) { bGo.href = rt.url; bGo.hidden = !live; }
|
|
769
|
+
if (bKicker) bKicker.textContent = live ? "Live on your machine" : "Went quiet";
|
|
770
|
+
if (bHint) bHint.hidden = live; // recovery hint only while offline
|
|
771
|
+
if (doneRunMeta) doneRunMeta.textContent = rt.url; // the collapsed Step-1 row's meta
|
|
772
|
+
if (bDiag) {
|
|
773
|
+
if (rt.cliVersion) {
|
|
774
|
+
bDiag.textContent = "tot CLI v" + rt.cliVersion + (rt.runnerVersion ? " \\u00b7 runner v" + rt.runnerVersion : "");
|
|
775
|
+
bDiag.hidden = false;
|
|
776
|
+
} else {
|
|
777
|
+
bDiag.hidden = true;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
updateEditHint(rt); // suggest the exact "open this file" command on Step 2
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// Item 6 — once the CLI reports the developer's $EDITOR, show the EXACT command to
|
|
784
|
+
// open the first file to edit (e.g. \`code content/home.html\`); otherwise a generic
|
|
785
|
+
// \`$EDITOR content/home.html\` plus a nudge to set $EDITOR.
|
|
786
|
+
var editOpenCmd = document.getElementById("editOpenCmd");
|
|
787
|
+
var editOpenHint = document.getElementById("editOpenHint");
|
|
788
|
+
function updateEditHint(rt) {
|
|
789
|
+
if (!editOpenCmd) return;
|
|
790
|
+
var editor = rt && typeof rt.editor === "string" ? rt.editor : "";
|
|
791
|
+
var cwd = rt && typeof rt.cwd === "string" ? rt.cwd : "";
|
|
792
|
+
// Prefer the FULL path (works from any directory) when the CLI reports the
|
|
793
|
+
// project dir; else a project-relative path with a "run it from there" note.
|
|
794
|
+
var file = cwd ? (cwd.replace(/\\/+$/, "") + "/content/home.html") : "content/home.html";
|
|
795
|
+
editOpenCmd.textContent = (editor || "$EDITOR") + " " + file;
|
|
796
|
+
if (editOpenHint) {
|
|
797
|
+
editOpenHint.textContent = editor
|
|
798
|
+
? (cwd
|
|
799
|
+
? "Run that anywhere to open the file — then change a line and save."
|
|
800
|
+
: "Run that from your project directory — then change a line and save.")
|
|
801
|
+
: "Tip: set $EDITOR (e.g. \`export EDITOR=\\"code\\"\`) and we'll show the exact open command here.";
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
var SS = window.sessionStorage;
|
|
806
|
+
function load(k, d) { try { var v = SS.getItem(k); return v === null ? d : JSON.parse(v); } catch (e) { return d; } }
|
|
807
|
+
function put(k, v) { try { SS.setItem(k, JSON.stringify(v)); } catch (e) {} }
|
|
808
|
+
|
|
809
|
+
var baseline = load("tot.hosted.editBase", null); // null = not yet primed by a first poll
|
|
810
|
+
var expanded = false; // activity list: last 3 by default, expand to all
|
|
811
|
+
// Have we seen the store go live during THIS PAGE VIEW? In-memory ONLY (not
|
|
812
|
+
// persisted): a fresh land or a reload starts false, so a stale/offline record can
|
|
813
|
+
// never greet the developer with "went quiet" — Step 1 owns that moment. The
|
|
814
|
+
// recovery ("went quiet") state shows only after we actually watched it live → drop.
|
|
815
|
+
var sawLive = false;
|
|
816
|
+
|
|
817
|
+
// Guided accordion. Each step opens with its content; when its activity is detected
|
|
818
|
+
// it animates CLOSED to a reopenable done-bar, and a single "Next Up" panel at the
|
|
819
|
+
// bottom expands to tease the following step with ONE CTA that opens it.
|
|
820
|
+
// steps = completion flags {run, edit}
|
|
821
|
+
// current = the OPEN step key, or null between steps (Next Up showing)
|
|
822
|
+
// Both persisted so a reload restores the exact place in the flow.
|
|
823
|
+
var steps = load("tot.hosted.steps", { run: false, edit: false });
|
|
824
|
+
// Live-driven flow. current is the EXPLICITLY-opened step ("run" when Step 1 was
|
|
825
|
+
// reopened, "edit"/"next" for those) or null — the default view, where Step 1's
|
|
826
|
+
// open/minimized state is driven by whether localhost is LIVE. NOT persisted: a
|
|
827
|
+
// refresh lands in this default view (progress in steps still persists).
|
|
828
|
+
var current = null;
|
|
829
|
+
var liveNow = false; // is localhost live right now? set each poll from runtime.live
|
|
830
|
+
var bodies = {
|
|
831
|
+
run: document.getElementById("stage-run"),
|
|
832
|
+
edit: document.getElementById("hosted-stage-edit"),
|
|
833
|
+
next: document.getElementById("hosted-stage-next"),
|
|
834
|
+
};
|
|
835
|
+
// Rail station elements — the reskin's DOM (applyView targets these).
|
|
836
|
+
var stEls = {
|
|
837
|
+
run: document.getElementById("st-run"),
|
|
838
|
+
edit: document.getElementById("st-edit"),
|
|
839
|
+
next: document.getElementById("st-next"),
|
|
840
|
+
};
|
|
841
|
+
var segRun = document.getElementById("seg-run");
|
|
842
|
+
var segEdit = document.getElementById("seg-edit");
|
|
843
|
+
var doneRunEl = document.getElementById("done-run");
|
|
844
|
+
var doneEditEl = document.getElementById("done-edit");
|
|
845
|
+
var doneEditMeta = document.getElementById("doneEditMeta");
|
|
846
|
+
var lockEditEl = document.getElementById("lock-edit");
|
|
847
|
+
var lockNextEl = document.getElementById("lock-next");
|
|
848
|
+
var teaseEditEl = document.getElementById("tease-edit");
|
|
849
|
+
var teaseNextEl = document.getElementById("tease-next");
|
|
850
|
+
var quietNoteEl = document.getElementById("quietNote");
|
|
851
|
+
|
|
852
|
+
// Persisted state mutations funnel through here so the sessionStorage write can
|
|
853
|
+
// never be forgotten at a call site (progress in steps + the edit baseline are
|
|
854
|
+
// the only things that survive a reload).
|
|
855
|
+
function completeStep(key) {
|
|
856
|
+
if (steps[key]) return false; // already done — no-op
|
|
857
|
+
steps[key] = true; put("tot.hosted.steps", steps);
|
|
858
|
+
return true; // "just completed" — caller may celebrate
|
|
859
|
+
}
|
|
860
|
+
function setBaseline(v) { baseline = v; put("tot.hosted.editBase", baseline); }
|
|
861
|
+
|
|
862
|
+
// ============================================================================
|
|
863
|
+
// STATE → VIEW → DOM. The flow's behaviour is split into three layers so the
|
|
864
|
+
// rules live in ONE readable place and every render goes through one path:
|
|
865
|
+
// 1. derive — computeView(): pure, reads state (steps/current/liveNow),
|
|
866
|
+
// returns a view model. EVERY visibility/emphasis rule is here.
|
|
867
|
+
// 2. apply — applyView(): the ONLY code that mutates the rail DOM (station
|
|
868
|
+
// statuses, segment lit/flow, step bodies, done rows, locked
|
|
869
|
+
// labels, teasers, quiet note, activity).
|
|
870
|
+
// 3. renderSteps(): apply(compute()). Call after ANY state change — never poke
|
|
871
|
+
// an element directly from a handler or the poll.
|
|
872
|
+
// The signal-rail reskin kept layer 1's RULES verbatim (only added derived
|
|
873
|
+
// fields) and rewrote applyView() + the markup/CSS it targets.
|
|
874
|
+
// ============================================================================
|
|
875
|
+
|
|
876
|
+
// --- derive ---------------------------------------------------------------
|
|
877
|
+
function unlocked(step) {
|
|
878
|
+
return step === "run" || (step === "edit" && steps.run) || (step === "next" && steps.edit);
|
|
879
|
+
}
|
|
880
|
+
// Step 1 is OPEN while you still need to get the store running (localhost not live) or
|
|
881
|
+
// when you explicitly reopened it; it MINIMIZES to a done row once localhost is live —
|
|
882
|
+
// in opposition to revealing the Step-2 teaser.
|
|
883
|
+
function step1Open() {
|
|
884
|
+
return current === "run" || (current === null && !(steps.run && liveNow));
|
|
885
|
+
}
|
|
886
|
+
// The complete view model — no DOM access, so the rules can be read top to bottom:
|
|
887
|
+
// open which step BODY is expanded {run, edit, next}
|
|
888
|
+
// seg which rail segment is LIT {run, edit}
|
|
889
|
+
// flow do lit segments flow downward (live) boolean
|
|
890
|
+
// stRun/Edit/Next each station's status "open" | "done" | "next" | "locked"
|
|
891
|
+
// teaseEdit/Next is that step's forward teaser shown (live-gated)
|
|
892
|
+
// doneRun/Edit is that step's collapsed done row shown
|
|
893
|
+
// lockEdit/Next is that step's locked label shown
|
|
894
|
+
// quietNote the Step-1 "went quiet" note (re-inflated after a drop)
|
|
895
|
+
// showActivity is "Your activity" visible (NEVER while Step 1 is open)
|
|
896
|
+
function computeView() {
|
|
897
|
+
var s1 = step1Open();
|
|
898
|
+
// Forward teasers show ONLY while localhost is live — a forward CTA only makes
|
|
899
|
+
// sense while the loop is alive. When the loop goes quiet Step 1 re-inflates to own
|
|
900
|
+
// that moment, so the teaser retracts. Either is also hidden while the step it
|
|
901
|
+
// teases is already open.
|
|
902
|
+
var teaseEdit = steps.run && !steps.edit && liveNow && current !== "edit";
|
|
903
|
+
var teaseNext = steps.edit && liveNow && current !== "next";
|
|
904
|
+
return {
|
|
905
|
+
open: { run: s1, edit: current === "edit", next: current === "next" },
|
|
906
|
+
seg: { run: steps.run, edit: steps.edit },
|
|
907
|
+
flow: liveNow,
|
|
908
|
+
stRun: s1 ? "open" : "done",
|
|
909
|
+
stEdit: steps.edit ? "done" : (current === "edit") ? "open" : teaseEdit ? "next" : "locked",
|
|
910
|
+
stNext: (current === "next") ? "open" : teaseNext ? "next" : "locked",
|
|
911
|
+
teaseEdit: teaseEdit,
|
|
912
|
+
teaseNext: teaseNext,
|
|
913
|
+
doneRun: !s1 && steps.run,
|
|
914
|
+
doneEdit: steps.edit && current !== "edit",
|
|
915
|
+
lockEdit: !steps.run,
|
|
916
|
+
lockNext: !steps.edit,
|
|
917
|
+
quietNote: s1 && steps.run && !liveNow && sawLive,
|
|
918
|
+
showActivity: !s1 && steps.run,
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// --- apply (targets the rail DOM; the rules live in computeView above) -----
|
|
923
|
+
function applyView(v) {
|
|
924
|
+
if (stEls.run) stEls.run.setAttribute("data-st", v.stRun);
|
|
925
|
+
if (stEls.edit) stEls.edit.setAttribute("data-st", v.stEdit);
|
|
926
|
+
if (stEls.next) stEls.next.setAttribute("data-st", v.stNext);
|
|
927
|
+
if (segRun) { segRun.setAttribute("data-lit", v.seg.run ? "1" : "0"); segRun.setAttribute("data-flow", v.flow ? "1" : "0"); }
|
|
928
|
+
if (segEdit) { segEdit.setAttribute("data-lit", v.seg.edit ? "1" : "0"); segEdit.setAttribute("data-flow", v.flow ? "1" : "0"); }
|
|
929
|
+
if (bodies.run) bodies.run.hidden = !v.open.run;
|
|
930
|
+
if (bodies.edit) bodies.edit.hidden = !v.open.edit;
|
|
931
|
+
if (bodies.next) bodies.next.hidden = !v.open.next;
|
|
932
|
+
if (doneRunEl) doneRunEl.hidden = !v.doneRun;
|
|
933
|
+
if (doneEditEl) doneEditEl.hidden = !v.doneEdit;
|
|
934
|
+
if (lockEditEl) lockEditEl.hidden = !v.lockEdit;
|
|
935
|
+
if (lockNextEl) lockNextEl.hidden = !v.lockNext;
|
|
936
|
+
if (teaseEditEl) teaseEditEl.hidden = !v.teaseEdit;
|
|
937
|
+
if (teaseNextEl) teaseNextEl.hidden = !v.teaseNext;
|
|
938
|
+
if (quietNoteEl) quietNoteEl.hidden = !v.quietNote;
|
|
939
|
+
if (root) root.hidden = !v.showActivity;
|
|
940
|
+
}
|
|
941
|
+
// Single render entry point — derive, then apply. (Named renderSteps so the
|
|
942
|
+
// existing call sites keep working; distinct from the activity render(events).)
|
|
943
|
+
function renderSteps() { applyView(computeView()); }
|
|
944
|
+
|
|
945
|
+
function expandStage(el) {
|
|
946
|
+
if (!el) return;
|
|
947
|
+
el.hidden = false;
|
|
948
|
+
el.classList.add("expanding");
|
|
949
|
+
setTimeout(function () { if (el && el.classList) el.classList.remove("expanding"); }, 420);
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
// Open a step (from a teaser CTA or a collapsed done row) — animates it in.
|
|
953
|
+
function goTo(step) {
|
|
954
|
+
if (!unlocked(step)) return;
|
|
955
|
+
current = step;
|
|
956
|
+
renderSteps();
|
|
957
|
+
expandStage(bodies[step]);
|
|
958
|
+
window.scrollTo({ top: 0, behavior: "smooth" });
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// Rail click wiring: done rows reopen their step; teaser CTAs open the teased step.
|
|
962
|
+
if (doneRunEl) doneRunEl.addEventListener("click", function () { goTo("run"); });
|
|
963
|
+
if (doneEditEl) doneEditEl.addEventListener("click", function () { goTo("edit"); });
|
|
964
|
+
var ctaEdit = document.getElementById("cta-edit");
|
|
965
|
+
if (ctaEdit) ctaEdit.addEventListener("click", function () { goTo("edit"); });
|
|
966
|
+
var ctaNext = document.getElementById("cta-next");
|
|
967
|
+
if (ctaNext) ctaNext.addEventListener("click", function () { goTo("next"); });
|
|
968
|
+
|
|
969
|
+
// Absolute clock time with seconds (e.g. "2:57:14 PM") — a fixed point, not a
|
|
970
|
+
// churning duration. Full date+time stays on the hover title (render()).
|
|
971
|
+
function rel(ts) {
|
|
972
|
+
try {
|
|
973
|
+
return new Date(Number(ts)).toLocaleTimeString([], { hour: "numeric", minute: "2-digit", second: "2-digit" });
|
|
974
|
+
} catch (x) {
|
|
975
|
+
return "";
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
// Toast — reuses the shared #toast element (also driven by the copy script).
|
|
980
|
+
var toastEl = document.getElementById("toast");
|
|
981
|
+
var toastMsgEl = document.getElementById("toastMsg");
|
|
982
|
+
var toastT;
|
|
983
|
+
function toast(msg) {
|
|
984
|
+
if (!toastEl || !toastMsgEl) return;
|
|
985
|
+
toastMsgEl.textContent = msg;
|
|
986
|
+
toastEl.classList.add("show");
|
|
987
|
+
clearTimeout(toastT);
|
|
988
|
+
toastT = setTimeout(function () { toastEl.classList.remove("show"); }, 2600);
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
// Celebration — replaces the full-screen confetti. Stamps the Step-2 node (spark
|
|
992
|
+
// burst + stamp, via the .celebrating class) and shows a toast. Reduced-motion
|
|
993
|
+
// disables the burst/stamp in CSS; the toast still shows.
|
|
994
|
+
function celebrate(file) {
|
|
995
|
+
var st = stEls.edit;
|
|
996
|
+
if (st) {
|
|
997
|
+
st.classList.remove("celebrating");
|
|
998
|
+
void st.offsetWidth; // restart the burst animation
|
|
999
|
+
st.classList.add("celebrating");
|
|
1000
|
+
setTimeout(function () { if (st && st.classList) st.classList.remove("celebrating"); }, 1100);
|
|
1001
|
+
}
|
|
1002
|
+
if (doneEditMeta && file) doneEditMeta.textContent = file;
|
|
1003
|
+
toast("Saved " + (file || "your file") + " \\u2014 your store reloaded. That's the loop.");
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
var toggleEl = document.getElementById("haToggle");
|
|
1007
|
+
var VISIBLE = 3; // show the latest 3 by default; expand for the rest
|
|
1008
|
+
if (toggleEl) {
|
|
1009
|
+
toggleEl.addEventListener("click", function () { expanded = !expanded; render(lastEvents); });
|
|
1010
|
+
}
|
|
1011
|
+
var lastEvents = [];
|
|
1012
|
+
|
|
1013
|
+
function render(events) {
|
|
1014
|
+
lastEvents = events;
|
|
1015
|
+
// Item 3 — drop the "up"/"running locally" pings; the live widget at the top
|
|
1016
|
+
// already conveys that. Only real FILE activity belongs in the table, and each
|
|
1017
|
+
// field is its own <td> so time / action / file never run together.
|
|
1018
|
+
var real = (events || []).filter(function (e) { return e && e.event !== "up"; });
|
|
1019
|
+
Array.prototype.forEach.call(bodyEl.querySelectorAll(".ha-row"), function (n) { n.remove(); });
|
|
1020
|
+
if (!real.length) {
|
|
1021
|
+
emptyEl.hidden = false;
|
|
1022
|
+
if (tableEl) tableEl.hidden = true;
|
|
1023
|
+
if (toggleEl) toggleEl.hidden = true;
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
emptyEl.hidden = true;
|
|
1027
|
+
if (tableEl) tableEl.hidden = false;
|
|
1028
|
+
var newestFirst = real.slice().reverse();
|
|
1029
|
+
var shown = expanded ? newestFirst : newestFirst.slice(0, VISIBLE);
|
|
1030
|
+
shown.forEach(function (e, i) {
|
|
1031
|
+
var tr = document.createElement("tr");
|
|
1032
|
+
tr.className = i === 0 ? "ha-row is-new" : "ha-row"; // accent the newest row
|
|
1033
|
+
var when = document.createElement("td");
|
|
1034
|
+
when.className = "ha-when";
|
|
1035
|
+
when.textContent = rel(e.at);
|
|
1036
|
+
try { when.title = new Date(e.at).toLocaleString(); } catch (x) {} // exact time on hover
|
|
1037
|
+
var desc = document.createElement("td");
|
|
1038
|
+
desc.className = "ha-desc";
|
|
1039
|
+
desc.textContent = e.description;
|
|
1040
|
+
var file = document.createElement("td");
|
|
1041
|
+
file.className = "ha-file";
|
|
1042
|
+
file.textContent = e.file || ""; // the concrete file name, e.g. content/home.html
|
|
1043
|
+
tr.appendChild(when); tr.appendChild(desc); tr.appendChild(file);
|
|
1044
|
+
bodyEl.appendChild(tr);
|
|
1045
|
+
});
|
|
1046
|
+
if (toggleEl) {
|
|
1047
|
+
if (real.length > VISIBLE) {
|
|
1048
|
+
toggleEl.hidden = false;
|
|
1049
|
+
toggleEl.textContent = expanded ? "Show fewer" : "Show all " + real.length;
|
|
1050
|
+
} else {
|
|
1051
|
+
toggleEl.hidden = true;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// Adaptive poll cadence + visibility pause (2026-07-15 KV-quota incident: a
|
|
1057
|
+
// single backgrounded tab, polling flat-out every 3s with no pause, burned
|
|
1058
|
+
// through the ENTIRE account-wide daily KV read quota by itself and took
|
|
1059
|
+
// down real customer traffic with 500s). Two rules:
|
|
1060
|
+
// 1. HIDDEN tab → don't poll at all. A backgrounded/minimized tab can't be
|
|
1061
|
+
// "watching" the loop; resume (with an immediate catch-up poll) the
|
|
1062
|
+
// instant it becomes visible again.
|
|
1063
|
+
// 2. VISIBLE but the loop is already fully celebrated (steps.run AND
|
|
1064
|
+
// steps.edit) → the high-stakes "is it working yet" moment has passed,
|
|
1065
|
+
// so back off to a slow cadence. Still visible AND still mid-loop →
|
|
1066
|
+
// keep the fast cadence (this is the one case where sub-second-feeling
|
|
1067
|
+
// responsiveness genuinely matters — a real developer staring at the
|
|
1068
|
+
// screen waiting to see their save land).
|
|
1069
|
+
var FAST_MS = 3000; // actively watching for the loop to prove itself
|
|
1070
|
+
var SLOW_MS = 20000; // loop already celebrated — just an occasional glance
|
|
1071
|
+
var pollTimer = null;
|
|
1072
|
+
|
|
1073
|
+
function nextDelay() {
|
|
1074
|
+
return (steps.run && steps.edit) ? SLOW_MS : FAST_MS;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
function scheduleNext() {
|
|
1078
|
+
if (pollTimer) { clearTimeout(pollTimer); pollTimer = null; }
|
|
1079
|
+
if (document.hidden) return; // stay fully paused; visibilitychange resumes us
|
|
1080
|
+
pollTimer = setTimeout(poll, nextDelay());
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
function poll() {
|
|
1084
|
+
fetch("/api/dev/activity", { cache: "no-store" })
|
|
1085
|
+
.then(function (r) { return r.ok ? r.json() : Promise.reject(); })
|
|
1086
|
+
.then(function (d) {
|
|
1087
|
+
var events = (d && d.events) || [];
|
|
1088
|
+
var rt = d && d.runtime;
|
|
1089
|
+
var isLive = !!(rt && rt.live);
|
|
1090
|
+
liveNow = isLive; // drives Step 1 minimize + the Step-2 teaser visibility below
|
|
1091
|
+
updateBridge(rt); // same poll drives the bridge strip
|
|
1092
|
+
render(events);
|
|
1093
|
+
var last = events.length ? events[events.length - 1] : null;
|
|
1094
|
+
var latest = last ? last.at : 0;
|
|
1095
|
+
var lastIsEdit = last && last.event !== "up"; // "up" = a server-running ping, not an edit
|
|
1096
|
+
// Step 1 "Get running" completes once localhost reports LIVE (or any activity
|
|
1097
|
+
// arrives). Once complete, renderSteps() minimizes Step 1 and reveals the Step-2 nudge.
|
|
1098
|
+
if (isLive || events.length) completeStep("run");
|
|
1099
|
+
if (baseline === null) {
|
|
1100
|
+
setBaseline(latest); // first poll primes the watermark — no edit inferred yet
|
|
1101
|
+
} else if (latest > baseline) {
|
|
1102
|
+
setBaseline(latest);
|
|
1103
|
+
// Step 2 completes only on a real FILE edit landing (not a running ping).
|
|
1104
|
+
// completeStep flips + persists once, returning true on that first poll only.
|
|
1105
|
+
if (lastIsEdit && completeStep("edit")) {
|
|
1106
|
+
celebrate(last && last.file);
|
|
1107
|
+
if (current === "edit") current = null; // minimize Step 2 → reveal Step-3 teaser
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
// ALWAYS re-render so a localhost drop hides the Step-2 nudge + reopens Step 1.
|
|
1111
|
+
renderSteps();
|
|
1112
|
+
})
|
|
1113
|
+
.catch(function () {})
|
|
1114
|
+
.then(scheduleNext); // reschedule (or not, if now hidden) regardless of outcome
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
document.addEventListener("visibilitychange", function () {
|
|
1118
|
+
if (document.hidden) {
|
|
1119
|
+
if (pollTimer) { clearTimeout(pollTimer); pollTimer = null; }
|
|
1120
|
+
} else {
|
|
1121
|
+
poll(); // catch up immediately; poll() reschedules itself at the end
|
|
1122
|
+
}
|
|
1123
|
+
});
|
|
1124
|
+
|
|
1125
|
+
// Item 7/10 — Step 3 "Submit for preview" opens the "coming soon" dialog (the
|
|
1126
|
+
// compliance-reviewed preview pipeline isn't wired into this panel yet).
|
|
1127
|
+
var submitBtn = document.getElementById("submitPreview");
|
|
1128
|
+
var comingSoon = document.getElementById("comingSoon");
|
|
1129
|
+
if (submitBtn && comingSoon) {
|
|
1130
|
+
submitBtn.addEventListener("click", function () {
|
|
1131
|
+
if (typeof comingSoon.showModal === "function") comingSoon.showModal();
|
|
1132
|
+
else comingSoon.setAttribute("open", "");
|
|
1133
|
+
});
|
|
1134
|
+
var csClose = document.getElementById("comingSoonClose");
|
|
1135
|
+
if (csClose) csClose.addEventListener("click", function () {
|
|
1136
|
+
if (typeof comingSoon.close === "function") comingSoon.close();
|
|
1137
|
+
comingSoon.removeAttribute("open");
|
|
1138
|
+
});
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
renderSteps();
|
|
1142
|
+
if (!document.hidden) poll(); // if the tab starts hidden, visibilitychange kicks it off later
|
|
1143
|
+
})();
|
|
1144
|
+
`;
|
|
1145
|
+
---
|
|
1146
|
+
|
|
1147
|
+
<html lang="en">
|
|
1148
|
+
<head>
|
|
1149
|
+
<meta charset="utf-8" />
|
|
1150
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
1151
|
+
<meta name="robots" content="noindex, nofollow" />
|
|
1152
|
+
<title>Get your store running — {appDomain}</title>
|
|
1153
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
1154
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
1155
|
+
<link
|
|
1156
|
+
rel="stylesheet"
|
|
1157
|
+
href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@500;600;700;800&family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap"
|
|
1158
|
+
/>
|
|
1159
|
+
<style nonce={cspNonce}>
|
|
1160
|
+
:root {
|
|
1161
|
+
--bg: #f7f9fc; --bg-2: #eef3f9; --surface: #ffffff;
|
|
1162
|
+
--ink: #0a2540; --ink-2: #33415b; --muted: #55637a; --faint: #8593a8;
|
|
1163
|
+
--accent: #0e9f8e; --accent-ink: #0b7a6d; --accent-wash: #e7f6f4; --accent-line: #b8e6df;
|
|
1164
|
+
--border: #e3e8ef; --border-2: #eef2f7;
|
|
1165
|
+
--warn: #c23b3b; --warn-wash: #fbecec; --warn-line: #f0cccc;
|
|
1166
|
+
--amber: #b7791f; --amber-wash: #fdf6e7; --amber-line: #f0dcae;
|
|
1167
|
+
|
|
1168
|
+
--term: #0a1a2f; --term-2: #071426; --term-fg: #dbe6f2; --term-dim: #5f7189;
|
|
1169
|
+
--term-prompt: #2dd4bf; --term-ok: #34d399; --term-line: #14263f;
|
|
1170
|
+
|
|
1171
|
+
--r-sm: 6px; --r-md: 10px; --r-lg: 16px; --r-xl: 22px; --r-full: 999px;
|
|
1172
|
+
--sh-sm: 0 1px 2px rgba(10,37,64,.06), 0 2px 6px -2px rgba(10,37,64,.08);
|
|
1173
|
+
--sh-md: 0 8px 28px -10px rgba(10,37,64,.16);
|
|
1174
|
+
--sh-lg: 0 28px 64px -26px rgba(10,37,64,.28);
|
|
1175
|
+
|
|
1176
|
+
/* Guided-flow motion tokens (Steps 1–3 choreography). One place to retune
|
|
1177
|
+
the whole open/collapse/reveal feel — a future reskin changes the motion
|
|
1178
|
+
here, not rule-by-rule. Theme-independent, so declared once. */
|
|
1179
|
+
--flow-ease: cubic-bezier(.2, .9, .3, 1);
|
|
1180
|
+
--flow-dur: .42s;
|
|
1181
|
+
--flow-rise: 8px; /* how far a newly-revealed block settles in from */
|
|
1182
|
+
|
|
1183
|
+
--display: "Plus Jakarta Sans", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
1184
|
+
--body: "Inter", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
1185
|
+
--mono: "JetBrains Mono", ui-monospace, "SF Mono", "Cascadia Code", Menlo, monospace;
|
|
1186
|
+
color-scheme: light;
|
|
1187
|
+
}
|
|
1188
|
+
@media (prefers-color-scheme: dark) {
|
|
1189
|
+
:root {
|
|
1190
|
+
--bg: #071320; --bg-2: #0a1a2b; --surface: #0e2134;
|
|
1191
|
+
--ink: #eaf1f8; --ink-2: #bdccdc; --muted: #90a3b7; --faint: #6c8098;
|
|
1192
|
+
--accent: #2fd4bf; --accent-ink: #7fe7da; --accent-wash: #0f2e30; --accent-line: #1e514b;
|
|
1193
|
+
--border: #1b3348; --border-2: #16293c;
|
|
1194
|
+
--warn: #f2857f; --warn-wash: #2a1414; --warn-line: #5a2626;
|
|
1195
|
+
--amber: #f0c56b; --amber-wash: #2a2110; --amber-line: #5a481f;
|
|
1196
|
+
color-scheme: dark;
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
:root[data-theme="dark"] {
|
|
1200
|
+
--bg: #071320; --bg-2: #0a1a2b; --surface: #0e2134;
|
|
1201
|
+
--ink: #eaf1f8; --ink-2: #bdccdc; --muted: #90a3b7; --faint: #6c8098;
|
|
1202
|
+
--accent: #2fd4bf; --accent-ink: #7fe7da; --accent-wash: #0f2e30; --accent-line: #1e514b;
|
|
1203
|
+
--border: #1b3348; --border-2: #16293c;
|
|
1204
|
+
--warn: #f2857f; --warn-wash: #2a1414; --warn-line: #5a2626;
|
|
1205
|
+
--amber: #f0c56b; --amber-wash: #2a2110; --amber-line: #5a481f;
|
|
1206
|
+
color-scheme: dark;
|
|
1207
|
+
}
|
|
1208
|
+
:root[data-theme="light"] {
|
|
1209
|
+
--bg: #f7f9fc; --bg-2: #eef3f9; --surface: #ffffff;
|
|
1210
|
+
--ink: #0a2540; --ink-2: #33415b; --muted: #55637a; --faint: #8593a8;
|
|
1211
|
+
--accent: #0e9f8e; --accent-ink: #0b7a6d; --accent-wash: #e7f6f4; --accent-line: #b8e6df;
|
|
1212
|
+
--border: #e3e8ef; --border-2: #eef2f7;
|
|
1213
|
+
--warn: #c23b3b; --warn-wash: #fbecec; --warn-line: #f0cccc;
|
|
1214
|
+
--amber: #b7791f; --amber-wash: #fdf6e7; --amber-line: #f0dcae;
|
|
1215
|
+
color-scheme: light;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
* { box-sizing: border-box; }
|
|
1219
|
+
html { -webkit-text-size-adjust: 100%; }
|
|
1220
|
+
body {
|
|
1221
|
+
margin: 0;
|
|
1222
|
+
background:
|
|
1223
|
+
radial-gradient(50rem 28rem at 100% -8%, var(--accent-wash) 0%, transparent 55%),
|
|
1224
|
+
var(--bg);
|
|
1225
|
+
background-repeat: no-repeat;
|
|
1226
|
+
color: var(--ink-2); font-family: var(--body); font-size: 16px; line-height: 1.6;
|
|
1227
|
+
letter-spacing: -.011em; -webkit-font-smoothing: antialiased;
|
|
1228
|
+
padding: 0 1.25rem 6rem;
|
|
1229
|
+
}
|
|
1230
|
+
a { color: var(--accent-ink); }
|
|
1231
|
+
a:focus-visible, button:focus-visible, summary:focus-visible {
|
|
1232
|
+
outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 3px;
|
|
1233
|
+
}
|
|
1234
|
+
.wrap { max-width: 58rem; margin: 0 auto; }
|
|
1235
|
+
|
|
1236
|
+
/* ---- top bar (tenant identity) ---- */
|
|
1237
|
+
.topbar {
|
|
1238
|
+
display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap;
|
|
1239
|
+
padding: 1.15rem 0 1rem; border-bottom: 1px solid var(--border); margin-bottom: 1.8rem;
|
|
1240
|
+
}
|
|
1241
|
+
.brand { display: flex; align-items: center; gap: .65rem; }
|
|
1242
|
+
.brand .mark {
|
|
1243
|
+
width: 30px; height: 30px; border-radius: 9px;
|
|
1244
|
+
background: linear-gradient(150deg, var(--accent), var(--accent-ink));
|
|
1245
|
+
display: grid; place-items: center; color: #fff; flex: none;
|
|
1246
|
+
box-shadow: 0 3px 10px -3px rgba(14,159,142,.6);
|
|
1247
|
+
}
|
|
1248
|
+
.brand .mark svg { width: 17px; height: 17px; }
|
|
1249
|
+
.brand .who { display: flex; flex-direction: column; line-height: 1.15; }
|
|
1250
|
+
.brand .dom { font-family: var(--display); font-weight: 700; color: var(--ink); font-size: .98rem; letter-spacing: -.01em; }
|
|
1251
|
+
.brand .sub { font-family: var(--mono); font-size: .7rem; color: var(--faint); text-transform: uppercase; letter-spacing: .12em; }
|
|
1252
|
+
.topbar .right { display: flex; align-items: center; gap: .9rem; font-family: var(--mono); font-size: .78rem; color: var(--muted); }
|
|
1253
|
+
.topbar .right .sess { display: inline-flex; align-items: center; gap: .45rem; }
|
|
1254
|
+
.topbar .right .sess::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--accent); box-shadow: 0 0 0 3px var(--accent-wash); }
|
|
1255
|
+
.themetog {
|
|
1256
|
+
font-family: var(--mono); font-size: .72rem; background: var(--surface); color: var(--muted);
|
|
1257
|
+
border: 1px solid var(--border); border-radius: var(--r-full); padding: .3rem .7rem; cursor: pointer;
|
|
1258
|
+
}
|
|
1259
|
+
.themetog:hover { color: var(--ink); border-color: var(--accent-line); }
|
|
1260
|
+
|
|
1261
|
+
/* ---- recovery notice (signed out / no store access) ---- */
|
|
1262
|
+
.notice {
|
|
1263
|
+
background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-lg);
|
|
1264
|
+
box-shadow: var(--sh-sm); padding: 1.15rem 1.4rem; margin: 0 0 1.6rem;
|
|
1265
|
+
display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: .7rem 1.4rem;
|
|
1266
|
+
}
|
|
1267
|
+
.notice p { margin: 0; color: var(--ink-2); font-size: .95rem; }
|
|
1268
|
+
.notice a.action {
|
|
1269
|
+
flex: none; display: inline-flex; align-items: center; gap: .4rem; background: var(--accent);
|
|
1270
|
+
color: #fff; text-decoration: none; font: 700 .92rem/1 var(--display); padding: .66rem 1.15rem;
|
|
1271
|
+
border-radius: var(--r-full); transition: background .18s ease, transform .12s ease;
|
|
1272
|
+
}
|
|
1273
|
+
.notice a.action:hover { background: var(--accent-ink); transform: translateY(-1px); }
|
|
1274
|
+
|
|
1275
|
+
/* ---- hosted activity ledger (hosted-dev-activity-bridge) — reskinned to the
|
|
1276
|
+
signal-rail "activity" look: a compact 3-col table (time | action | file)
|
|
1277
|
+
with the newest row accented (.is-new). ---- */
|
|
1278
|
+
.hosted-activity {
|
|
1279
|
+
margin-top: .4rem; background: var(--surface); border: 1px solid var(--border);
|
|
1280
|
+
border-radius: var(--r-lg); box-shadow: var(--sh-sm); padding: 1rem 1.2rem .9rem; margin-bottom: 1.6rem;
|
|
1281
|
+
}
|
|
1282
|
+
.ha-head { display: flex; align-items: baseline; gap: .6rem; flex-wrap: wrap; margin-bottom: .5rem; }
|
|
1283
|
+
.ha-head h2 { font-family: var(--display); font-size: .95rem; font-weight: 700; color: var(--ink); margin: 0; }
|
|
1284
|
+
.ha-sub { font-size: .78rem; color: var(--muted); }
|
|
1285
|
+
.ha-sub code { font-family: var(--mono); }
|
|
1286
|
+
/* Activity TABLE — three real columns (time | action | file) with row spacing,
|
|
1287
|
+
so the fields are always visually separated and never run together. */
|
|
1288
|
+
.ha-table { width: 100%; border-collapse: separate; border-spacing: 0 .4rem; margin: 0; }
|
|
1289
|
+
.ha-row > td {
|
|
1290
|
+
padding: .55rem .8rem; background: var(--bg-2); font-size: .85rem; color: var(--ink-2);
|
|
1291
|
+
vertical-align: baseline;
|
|
1292
|
+
}
|
|
1293
|
+
.ha-row > td:first-child { border-radius: var(--r-md) 0 0 var(--r-md); }
|
|
1294
|
+
.ha-row > td:last-child { border-radius: 0 var(--r-md) var(--r-md) 0; }
|
|
1295
|
+
.ha-row.is-new > td { background: var(--accent-wash); }
|
|
1296
|
+
td.ha-when { font-family: var(--mono); font-size: .74rem; color: var(--faint); width: 6.8rem; white-space: nowrap; font-variant-numeric: tabular-nums; }
|
|
1297
|
+
td.ha-desc { font-weight: 600; white-space: nowrap; width: 1%; padding-right: 1.1rem; }
|
|
1298
|
+
td.ha-file { font-family: var(--mono); font-size: .74rem; color: var(--muted); max-width: 0; width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1299
|
+
.ha-toggle {
|
|
1300
|
+
margin-top: .5rem; background: none; border: none; padding: .3rem .1rem; cursor: pointer;
|
|
1301
|
+
font: 600 .8rem var(--body); color: var(--accent-ink); text-decoration: underline;
|
|
1302
|
+
}
|
|
1303
|
+
.ha-toggle:hover { color: var(--accent); }
|
|
1304
|
+
/* Encouragement banner shown atop Step 2 when you arrive. */
|
|
1305
|
+
.ha-encourage {
|
|
1306
|
+
display: flex; align-items: center; gap: .6rem; margin: 0 0 1rem;
|
|
1307
|
+
padding: .7rem .9rem; border-radius: var(--r-md);
|
|
1308
|
+
background: var(--accent-wash); border: 1px solid var(--accent-line); color: var(--accent-ink);
|
|
1309
|
+
font-weight: 600; font-size: .92rem;
|
|
1310
|
+
}
|
|
1311
|
+
.ha-encourage .chk { flex: none; }
|
|
1312
|
+
.ha-empty { margin: .2rem 0 .3rem; font-size: .84rem; color: var(--muted); }
|
|
1313
|
+
.ha-empty code { font-family: var(--mono); }
|
|
1314
|
+
|
|
1315
|
+
/* ---- bridge strip (replaces the big live-dev-link card): the rail's power
|
|
1316
|
+
source, slimmed to a strip. Fed by the SAME activity poll's runtime. ---- */
|
|
1317
|
+
.bridge {
|
|
1318
|
+
display: flex; align-items: center; gap: .8rem; flex-wrap: wrap;
|
|
1319
|
+
border: 1px solid var(--accent-line); background: var(--surface);
|
|
1320
|
+
border-radius: var(--r-lg); padding: .7rem 1rem; margin-bottom: 1.4rem;
|
|
1321
|
+
box-shadow: var(--sh-sm);
|
|
1322
|
+
}
|
|
1323
|
+
.bridge[data-live="1"] { border-color: var(--accent); animation: bridgeGlow 2.6s ease-in-out infinite; }
|
|
1324
|
+
@keyframes bridgeGlow {
|
|
1325
|
+
0%, 100% { box-shadow: 0 0 0 1px var(--accent-line), 0 0 12px -4px rgba(14,159,142,.3); }
|
|
1326
|
+
50% { box-shadow: 0 0 0 2px var(--accent), 0 0 22px -2px rgba(14,159,142,.45); }
|
|
1327
|
+
}
|
|
1328
|
+
.bridge[data-live="0"] { border-color: var(--amber-line); background: var(--amber-wash); }
|
|
1329
|
+
.beacon { width: 10px; height: 10px; border-radius: 50%; flex: none; background: var(--accent); }
|
|
1330
|
+
.bridge[data-live="1"] .beacon { animation: beat 1.6s ease-in-out infinite; }
|
|
1331
|
+
.bridge[data-live="0"] .beacon { background: var(--amber); }
|
|
1332
|
+
.b-txt { display: flex; flex-direction: column; line-height: 1.25; min-width: 0; }
|
|
1333
|
+
.b-kicker { font-family: var(--mono); font-size: .66rem; text-transform: uppercase; letter-spacing: .13em; color: var(--accent-ink); }
|
|
1334
|
+
.bridge[data-live="0"] .b-kicker { color: var(--amber); }
|
|
1335
|
+
.b-url { font-family: var(--mono); font-size: .9rem; font-weight: 600; color: var(--accent-ink); text-decoration: none; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1336
|
+
.b-url:hover { text-decoration: underline; }
|
|
1337
|
+
.bridge[data-live="0"] .b-url { color: var(--muted); }
|
|
1338
|
+
.b-diag { margin-left: auto; font-family: var(--mono); font-size: .68rem; color: var(--faint); white-space: nowrap; }
|
|
1339
|
+
.b-go {
|
|
1340
|
+
flex: none; display: inline-flex; align-items: center; gap: .35rem;
|
|
1341
|
+
font: 700 .8rem/1 var(--display); background: var(--accent); color: #fff; text-decoration: none;
|
|
1342
|
+
padding: .5rem .95rem; border-radius: var(--r-full); transition: background .12s, transform .12s;
|
|
1343
|
+
}
|
|
1344
|
+
.b-go:hover { background: var(--accent-ink); transform: translateY(-1px); }
|
|
1345
|
+
.b-hint { flex-basis: 100%; margin: 0; font-size: .8rem; color: var(--amber); }
|
|
1346
|
+
.b-hint code { font-family: var(--mono); }
|
|
1347
|
+
@media (prefers-reduced-motion: reduce) { .bridge[data-live="1"], .beacon, .b-go { animation: none !important; transition: none; } }
|
|
1348
|
+
@media (max-width: 34rem) { .b-diag { display: none; } }
|
|
1349
|
+
|
|
1350
|
+
/* ---- the signal rail: breadcrumb + done-bars merged into stations on one
|
|
1351
|
+
spine. NO single .rail wrapper — each station is a top-level element and
|
|
1352
|
+
carries its own liveness flow via data-attributes on its .seg. ---- */
|
|
1353
|
+
.station { display: grid; grid-template-columns: 2.4rem 1fr; column-gap: 1rem; position: relative; }
|
|
1354
|
+
.station .nodecol { display: flex; flex-direction: column; align-items: center; }
|
|
1355
|
+
.node {
|
|
1356
|
+
width: 2.1rem; height: 2.1rem; border-radius: 50%; flex: none; z-index: 1;
|
|
1357
|
+
display: grid; place-items: center; position: relative;
|
|
1358
|
+
font-family: var(--mono); font-weight: 600; font-size: .85rem;
|
|
1359
|
+
background: var(--surface); border: 1.5px solid var(--border); color: var(--faint);
|
|
1360
|
+
transition: background .3s, border-color .3s, color .3s;
|
|
1361
|
+
}
|
|
1362
|
+
.node svg { width: 15px; height: 15px; display: none; }
|
|
1363
|
+
.station[data-st="done"] .node { background: var(--accent); border-color: var(--accent); color: #fff; }
|
|
1364
|
+
.station[data-st="done"] .node svg { display: block; }
|
|
1365
|
+
.station[data-st="done"] .node .num { display: none; }
|
|
1366
|
+
.station[data-st="open"] .node { border-color: var(--accent); color: var(--accent-ink); box-shadow: 0 0 0 4px var(--accent-wash); }
|
|
1367
|
+
.station[data-st="next"] .node { border-color: var(--accent-line); color: var(--accent-ink); background: var(--accent-wash); animation: nodeBreathe 1.8s ease-in-out infinite; }
|
|
1368
|
+
@keyframes nodeBreathe {
|
|
1369
|
+
0%, 100% { box-shadow: 0 0 0 3px var(--accent-wash); }
|
|
1370
|
+
50% { box-shadow: 0 0 0 8px var(--accent-wash); }
|
|
1371
|
+
}
|
|
1372
|
+
.station[data-st="locked"] .node { opacity: .55; }
|
|
1373
|
+
/* the segment of rail below each node — liveness flows down each lit segment */
|
|
1374
|
+
.seg { flex: 1; width: 2px; min-height: 1.2rem; background: var(--border); margin: 4px 0; border-radius: 1px; }
|
|
1375
|
+
#st-next .seg { visibility: hidden; } /* last station: no line below */
|
|
1376
|
+
.seg[data-lit="1"] { background: var(--accent-line); }
|
|
1377
|
+
.seg[data-lit="1"][data-flow="1"] {
|
|
1378
|
+
background: repeating-linear-gradient(180deg, var(--accent) 0 5px, var(--accent-line) 5px 11px);
|
|
1379
|
+
animation: flow 1.1s linear infinite;
|
|
1380
|
+
}
|
|
1381
|
+
@keyframes flow { to { background-position: 0 11px; } }
|
|
1382
|
+
.seg[data-lit="1"][data-flow="0"] {
|
|
1383
|
+
background: repeating-linear-gradient(180deg, var(--faint) 0 4px, transparent 4px 10px);
|
|
1384
|
+
opacity: .5;
|
|
1385
|
+
}
|
|
1386
|
+
.s-body { padding-bottom: 1.5rem; min-width: 0; }
|
|
1387
|
+
|
|
1388
|
+
/* spark burst on celebration — radiates from the node itself */
|
|
1389
|
+
.spark { position: absolute; left: 50%; top: 50%; width: 6px; height: 6px; border-radius: 2px; pointer-events: none; opacity: 0; }
|
|
1390
|
+
.station.celebrating .spark { animation: sparkOut .9s ease-out forwards; }
|
|
1391
|
+
@keyframes sparkOut {
|
|
1392
|
+
0% { opacity: 1; transform: translate(-50%, -50%); }
|
|
1393
|
+
100% { opacity: 0; transform: translate(calc(-50% + var(--dx) * 34px), calc(-50% + var(--dy) * 34px)) rotate(140deg); }
|
|
1394
|
+
}
|
|
1395
|
+
.station.celebrating .node { animation: stamp .5s cubic-bezier(.2,.9,.3,1.4); }
|
|
1396
|
+
@keyframes stamp { 0% { transform: scale(.7); } 60% { transform: scale(1.18); } 100% { transform: scale(1); } }
|
|
1397
|
+
|
|
1398
|
+
/* collapsed done row — lives IN sequence on the rail (replaces the done-bars) */
|
|
1399
|
+
.s-done {
|
|
1400
|
+
display: flex; align-items: baseline; gap: .6rem; width: 100%; text-align: left;
|
|
1401
|
+
background: none; border: none; border-radius: var(--r-sm); padding: .35rem .4rem .35rem 0; cursor: pointer;
|
|
1402
|
+
font-family: var(--body);
|
|
1403
|
+
}
|
|
1404
|
+
.s-done .lab { font-family: var(--display); font-weight: 600; color: var(--ink-2); font-size: .92rem; }
|
|
1405
|
+
.s-done .meta { font-family: var(--mono); font-size: .74rem; color: var(--faint); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1406
|
+
.s-done .reopen { margin-left: auto; flex: none; font-family: var(--mono); font-size: .7rem; color: var(--accent-ink); opacity: 0; transition: opacity .12s; }
|
|
1407
|
+
.s-done:hover .reopen, .s-done:focus-visible .reopen { opacity: 1; }
|
|
1408
|
+
.s-done:hover .lab { color: var(--ink); }
|
|
1409
|
+
|
|
1410
|
+
/* locked label */
|
|
1411
|
+
.s-locked { padding: .38rem 0; font-family: var(--display); font-weight: 600; font-size: .92rem; color: var(--faint); }
|
|
1412
|
+
.s-locked .sub { display: block; font: 400 .76rem/1.4 var(--body); color: var(--faint); }
|
|
1413
|
+
|
|
1414
|
+
/* next-up teaser — sits at its own station on the rail */
|
|
1415
|
+
.s-tease {
|
|
1416
|
+
background: var(--accent-wash); border: 1px solid var(--accent-line); border-radius: var(--r-lg);
|
|
1417
|
+
padding: 1rem 1.15rem; box-shadow: var(--sh-sm); animation: flowRise var(--flow-dur) var(--flow-ease);
|
|
1418
|
+
}
|
|
1419
|
+
.t-kick { font-family: var(--mono); font-size: .66rem; text-transform: uppercase; letter-spacing: .13em; color: var(--accent-ink); }
|
|
1420
|
+
.s-tease h3 { font-family: var(--display); font-size: 1.1rem; font-weight: 700; color: var(--ink); margin: .3rem 0 .25rem; letter-spacing: -.02em; }
|
|
1421
|
+
.s-tease p { font-size: .88rem; color: var(--ink-2); margin: 0 0 .9rem; max-width: 46ch; }
|
|
1422
|
+
.s-tease p code { font-family: var(--mono); font-size: .86em; background: var(--bg-2); border-radius: var(--r-sm); padding: .06rem .3rem; color: var(--ink); }
|
|
1423
|
+
.t-cta {
|
|
1424
|
+
display: inline-flex; align-items: center; gap: .5rem;
|
|
1425
|
+
font: 700 .88rem/1 var(--display); background: var(--accent); color: #fff; border: none;
|
|
1426
|
+
padding: .66rem 1.15rem; border-radius: var(--r-full); cursor: pointer;
|
|
1427
|
+
box-shadow: 0 8px 22px -10px rgba(14,159,142,.8); transition: background .12s, transform .12s;
|
|
1428
|
+
animation: ctaPulse 1.8s ease-in-out infinite;
|
|
1429
|
+
}
|
|
1430
|
+
.t-cta:hover { background: var(--accent-ink); transform: translateY(-1px); }
|
|
1431
|
+
|
|
1432
|
+
/* the Step-1 "went quiet" note, ported from the mockup (sits above the re-inflated Step 1) */
|
|
1433
|
+
.quiet-note { display: flex; gap: .5rem; align-items: baseline; margin: 0 0 1rem; padding: .6rem .8rem; border-radius: var(--r-md); background: var(--amber-wash); border: 1px solid var(--amber-line); color: var(--amber); font-size: .86rem; }
|
|
1434
|
+
.quiet-note code { font-family: var(--mono); }
|
|
1435
|
+
|
|
1436
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1437
|
+
.station[data-st="next"] .node, .seg[data-lit="1"][data-flow="1"],
|
|
1438
|
+
.t-cta, .s-tease, .station.celebrating .node, .station.celebrating .spark { animation: none !important; }
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
/* ---- Step 2 "lay it beside your editor" tip ---- */
|
|
1442
|
+
.tip-lay { margin: 1rem 0 0; font-size: .9rem; color: var(--muted); line-height: 1.5; }
|
|
1443
|
+
|
|
1444
|
+
/* ---- Guided accordion: a step body animates IN when opened (collapse is instant
|
|
1445
|
+
+ synchronous so the flow can't get stuck). Shared with isLocalDevLoop —
|
|
1446
|
+
left on its original timing so that variant is untouched. ---- */
|
|
1447
|
+
.stage.expanding { animation: stageExpand .42s ease; }
|
|
1448
|
+
@keyframes stageExpand {
|
|
1449
|
+
from { opacity: 0; transform: translateY(-8px); }
|
|
1450
|
+
to { opacity: 1; transform: none; }
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
/* Hosted guided-flow reveal (flowRise): the activity ledger settles into place
|
|
1454
|
+
with the same motion a step body uses to open — so moving between Steps 1–3
|
|
1455
|
+
reads as a single, deliberate transition. Scoped to the hosted id, so the
|
|
1456
|
+
shared isLocalDevLoop variant is left untouched. */
|
|
1457
|
+
#hostedActivity {
|
|
1458
|
+
animation: flowRise var(--flow-dur) var(--flow-ease);
|
|
1459
|
+
}
|
|
1460
|
+
@keyframes flowRise {
|
|
1461
|
+
from { opacity: 0; transform: translateY(calc(var(--flow-rise) * -1)); }
|
|
1462
|
+
to { opacity: 1; transform: none; }
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
/* CTA pulse — used by the rail teaser CTAs (.t-cta). */
|
|
1466
|
+
@keyframes ctaPulse {
|
|
1467
|
+
0%, 100% { box-shadow: 0 0 0 0 rgba(14,159,142,.5); }
|
|
1468
|
+
50% { box-shadow: 0 0 0 8px rgba(14,159,142,0); }
|
|
1469
|
+
}
|
|
1470
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1471
|
+
.stage.expanding, #hostedActivity { animation: none; }
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
/* ---- "coming soon" dialog (Step 3 "Submit for preview") ---- */
|
|
1475
|
+
.cs-dialog {
|
|
1476
|
+
border: 1px solid var(--border); border-radius: var(--r-xl); padding: 0;
|
|
1477
|
+
max-width: 30rem; width: calc(100% - 2rem);
|
|
1478
|
+
background: var(--surface); color: var(--ink);
|
|
1479
|
+
box-shadow: var(--sh-lg, 0 24px 60px -20px rgba(10,37,64,.5));
|
|
1480
|
+
}
|
|
1481
|
+
.cs-dialog::backdrop { background: rgba(10,37,64,.42); }
|
|
1482
|
+
.cs-body { padding: 1.8rem 1.7rem; text-align: center; }
|
|
1483
|
+
.cs-emoji { font-size: 2rem; display: block; margin-bottom: .5rem; }
|
|
1484
|
+
.cs-body h2 { font-family: var(--display); font-size: 1.15rem; font-weight: 700; margin: 0 0 .5rem; color: var(--ink); }
|
|
1485
|
+
.cs-body p { font-size: .92rem; color: var(--muted); line-height: 1.55; margin: 0 0 1.3rem; }
|
|
1486
|
+
|
|
1487
|
+
/* ---- Make an Edit (G2 — experimental, collapsed in-browser editor) ---- */
|
|
1488
|
+
.exp-badge {
|
|
1489
|
+
align-self: center; font-family: var(--mono); font-size: .6rem; font-weight: 500;
|
|
1490
|
+
text-transform: uppercase; letter-spacing: .12em; color: var(--amber);
|
|
1491
|
+
background: var(--amber-wash); border: 1px solid var(--amber-line);
|
|
1492
|
+
border-radius: var(--r-full); padding: .18rem .55rem;
|
|
1493
|
+
}
|
|
1494
|
+
.reveal-btn {
|
|
1495
|
+
margin-top: 1.1rem; display: inline-flex; align-items: center; gap: .5rem;
|
|
1496
|
+
font: 700 .9rem/1 var(--display); background: var(--accent); color: #fff; border: none;
|
|
1497
|
+
padding: .72rem 1.25rem; border-radius: var(--r-full); cursor: pointer;
|
|
1498
|
+
box-shadow: 0 8px 22px -10px rgba(14,159,142,.8); transition: background .12s, transform .12s;
|
|
1499
|
+
}
|
|
1500
|
+
.reveal-btn:hover { background: var(--accent-ink); transform: translateY(-1px); }
|
|
1501
|
+
.reveal-btn svg { width: 15px; height: 15px; }
|
|
1502
|
+
@media (prefers-reduced-motion: reduce) { .reveal-btn:hover { transform: none; } }
|
|
1503
|
+
|
|
1504
|
+
/* ---- stage card ---- */
|
|
1505
|
+
.stage {
|
|
1506
|
+
background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-xl);
|
|
1507
|
+
box-shadow: var(--sh-md); overflow: hidden; position: relative;
|
|
1508
|
+
}
|
|
1509
|
+
.stage::before { content: ""; position: absolute; inset: 0 0 auto 0; height: 3px; background: linear-gradient(90deg, var(--accent), var(--accent-line)); }
|
|
1510
|
+
.stage-body { padding: 1.6rem 1.6rem 1.7rem; }
|
|
1511
|
+
@media (max-width: 34rem) { .stage-body { padding: 1.2rem 1.1rem 1.3rem; } }
|
|
1512
|
+
.kicker {
|
|
1513
|
+
font-family: var(--mono); font-size: .72rem; font-weight: 500; text-transform: uppercase;
|
|
1514
|
+
letter-spacing: .15em; color: var(--accent-ink); display: inline-flex; align-items: center; gap: .5rem; margin: 0 0 .5rem;
|
|
1515
|
+
}
|
|
1516
|
+
.stage h1 {
|
|
1517
|
+
font-family: var(--display); font-weight: 800; color: var(--ink); letter-spacing: -.03em;
|
|
1518
|
+
font-size: clamp(1.5rem, 3.4vw, 1.95rem); margin: 0; text-wrap: balance;
|
|
1519
|
+
display: flex; align-items: center; gap: .55rem; flex-wrap: wrap;
|
|
1520
|
+
}
|
|
1521
|
+
.stage .goal { margin: .55rem 0 1.3rem; color: var(--muted); max-width: 48ch; }
|
|
1522
|
+
|
|
1523
|
+
/* info icon + popover */
|
|
1524
|
+
.info { position: relative; display: inline-flex; }
|
|
1525
|
+
.info-btn {
|
|
1526
|
+
width: 1.4rem; height: 1.4rem; border-radius: 50%; border: 1.5px solid var(--border);
|
|
1527
|
+
background: var(--surface); color: var(--muted); font-family: var(--display); font-weight: 700;
|
|
1528
|
+
font-size: .82rem; cursor: pointer; display: grid; place-items: center; line-height: 1;
|
|
1529
|
+
}
|
|
1530
|
+
.info-btn:hover { border-color: var(--accent-line); color: var(--accent-ink); }
|
|
1531
|
+
.info-pop {
|
|
1532
|
+
position: absolute; top: calc(100% + .5rem); left: 0; width: min(21rem, 78vw); z-index: 30;
|
|
1533
|
+
background: var(--ink); color: #e7eef6; font-size: .82rem; line-height: 1.55;
|
|
1534
|
+
border-radius: var(--r-md); padding: .85rem 1rem; box-shadow: var(--sh-lg);
|
|
1535
|
+
opacity: 0; visibility: hidden; transform: translateY(-4px); transition: opacity .16s, transform .16s, visibility .16s;
|
|
1536
|
+
}
|
|
1537
|
+
.info-pop code { font-family: var(--mono); font-size: .78rem; color: var(--term-prompt); }
|
|
1538
|
+
.info.open .info-pop { opacity: 1; visibility: visible; transform: translateY(0); }
|
|
1539
|
+
@media (prefers-color-scheme: dark) { .info-pop { background: #04101c; } }
|
|
1540
|
+
:root[data-theme="dark"] .info-pop { background: #04101c; }
|
|
1541
|
+
:root[data-theme="light"] .info-pop { background: var(--ink); }
|
|
1542
|
+
|
|
1543
|
+
/* ---- terminal ---- */
|
|
1544
|
+
.term {
|
|
1545
|
+
background: linear-gradient(180deg, var(--term) 0%, var(--term-2) 100%);
|
|
1546
|
+
border: 1px solid var(--term-line); border-radius: var(--r-lg); box-shadow: var(--sh-md);
|
|
1547
|
+
overflow: hidden; font-family: var(--mono); font-size: .82rem; line-height: 1.7;
|
|
1548
|
+
}
|
|
1549
|
+
.term-bar { display: flex; align-items: center; gap: .5rem; padding: .6rem .85rem; border-bottom: 1px solid var(--term-line); background: rgba(255,255,255,.02); }
|
|
1550
|
+
.term-bar .dots { display: flex; gap: .4rem; }
|
|
1551
|
+
.term-bar .dot { width: 11px; height: 11px; border-radius: 50%; }
|
|
1552
|
+
.dot.r { background: #ff5f56; } .dot.y { background: #ffbd2e; } .dot.g { background: #27c93f; }
|
|
1553
|
+
.term-bar .ttl { margin-left: .4rem; font-size: .72rem; color: var(--term-dim); }
|
|
1554
|
+
.term-body { padding: 1rem 1.05rem 1.1rem; color: var(--term-fg); white-space: pre-wrap; word-break: break-word; }
|
|
1555
|
+
/* block, not inline — Astro trims the whitespace text node between the spans, so the
|
|
1556
|
+
line break must be structural or the comment + command collapse onto one line. */
|
|
1557
|
+
.term-body .c { display: block; color: var(--term-dim); }
|
|
1558
|
+
.term-body .cmd { display: block; color: #fff; }
|
|
1559
|
+
|
|
1560
|
+
.cta {
|
|
1561
|
+
display: inline-flex; align-items: center; justify-content: center; gap: .55rem;
|
|
1562
|
+
font-family: var(--display); font-weight: 700; font-size: .95rem; background: var(--accent); color: #fff;
|
|
1563
|
+
border: none; padding: .82rem 1.3rem; border-radius: var(--r-full); cursor: pointer;
|
|
1564
|
+
box-shadow: 0 8px 22px -10px rgba(14,159,142,.8); transition: transform .12s, background .12s; width: 100%;
|
|
1565
|
+
}
|
|
1566
|
+
.cta:hover { transform: translateY(-1px); background: var(--accent-ink); }
|
|
1567
|
+
.cta:disabled { opacity: .7; cursor: default; transform: none; }
|
|
1568
|
+
.cta svg { width: 16px; height: 16px; }
|
|
1569
|
+
.cmd-actions { margin-top: .85rem; }
|
|
1570
|
+
.cmd-note { margin: .8rem 0 0; font: 500 .8rem/1.5 var(--mono); color: var(--muted); }
|
|
1571
|
+
.cmd-note em { color: var(--ink-2); font-style: normal; font-weight: 600; }
|
|
1572
|
+
.cmd-steps { margin: 1rem 0 0; font-size: .85rem; color: var(--muted); }
|
|
1573
|
+
.cmd-steps code { font-family: var(--mono); font-size: .86em; background: var(--bg-2); border-radius: var(--r-sm); padding: .08rem .34rem; color: var(--ink); }
|
|
1574
|
+
.cmd-steps a { color: var(--accent-ink); text-decoration: underline; text-underline-offset: 2px; }
|
|
1575
|
+
|
|
1576
|
+
/* ---- live loop status bar (the Monitor — C1) ---- */
|
|
1577
|
+
.loopbar {
|
|
1578
|
+
margin-top: 1.4rem; background: var(--surface); border: 1px solid var(--border);
|
|
1579
|
+
border-radius: var(--r-lg); box-shadow: var(--sh-sm); overflow: hidden;
|
|
1580
|
+
max-height: 0; opacity: 0; transition: max-height .4s ease, opacity .3s ease, margin .3s;
|
|
1581
|
+
}
|
|
1582
|
+
.loopbar.live { max-height: 40rem; opacity: 1; }
|
|
1583
|
+
.lb-head { display: flex; align-items: center; gap: .8rem; padding: .8rem 1.1rem; cursor: pointer; }
|
|
1584
|
+
.lb-head .pill {
|
|
1585
|
+
display: inline-flex; align-items: center; gap: .5rem; font-family: var(--mono); font-size: .74rem;
|
|
1586
|
+
padding: .32rem .7rem; border-radius: var(--r-full); border: 1px solid var(--accent-line);
|
|
1587
|
+
background: var(--accent-wash); color: var(--accent-ink); white-space: nowrap;
|
|
1588
|
+
}
|
|
1589
|
+
.lb-head .pill .beat { width: 8px; height: 8px; border-radius: 50%; background: var(--accent); }
|
|
1590
|
+
.loopbar[data-state="running"] .pill .beat { animation: beat 1.6s ease-in-out infinite; }
|
|
1591
|
+
.loopbar[data-state="reloading"] .pill .beat { animation: beat .5s ease-in-out infinite; }
|
|
1592
|
+
.loopbar[data-state="stale"] .pill { border-color: var(--amber-line); background: var(--amber-wash); color: var(--amber); }
|
|
1593
|
+
.loopbar[data-state="stale"] .pill .beat { background: var(--amber); }
|
|
1594
|
+
@keyframes beat { 0%,100% { box-shadow: 0 0 0 0 rgba(14,159,142,.5); } 50% { box-shadow: 0 0 0 5px rgba(14,159,142,0); } }
|
|
1595
|
+
.lb-head .lu { font-family: var(--mono); font-size: .76rem; color: var(--muted); }
|
|
1596
|
+
.lb-head .lu b { color: var(--ink); font-weight: 500; }
|
|
1597
|
+
.lb-head .chev { margin-left: auto; color: var(--faint); transition: transform .18s; }
|
|
1598
|
+
.loopbar.open .lb-head .chev { transform: rotate(90deg); }
|
|
1599
|
+
.lb-detail { display: none; padding: 0 1.1rem; }
|
|
1600
|
+
.loopbar.open .lb-detail { display: block; padding-bottom: .9rem; }
|
|
1601
|
+
.lb-row { display: grid; grid-template-columns: 1rem 8rem 1fr; gap: .6rem; align-items: baseline; padding: .5rem 0; border-top: 1px solid var(--border-2); }
|
|
1602
|
+
.lb-row .ic { color: var(--accent); font-size: .78rem; }
|
|
1603
|
+
.lb-row .k { font-size: .8rem; color: var(--muted); }
|
|
1604
|
+
.lb-row .v { font-family: var(--mono); font-size: .8rem; color: var(--ink); font-variant-numeric: tabular-nums; word-break: break-all; }
|
|
1605
|
+
.lb-row .v.dim { color: var(--faint); }
|
|
1606
|
+
@media (prefers-reduced-motion: reduce) { .loopbar .pill .beat { animation: none !important; } }
|
|
1607
|
+
|
|
1608
|
+
/* ---- stepper (U2) ---- */
|
|
1609
|
+
.stepper { display: flex; align-items: stretch; gap: 0; margin-bottom: 2rem; }
|
|
1610
|
+
.snode {
|
|
1611
|
+
flex: 1; display: flex; align-items: center; gap: .6rem; background: none; border: none;
|
|
1612
|
+
font-family: var(--body); text-align: left; cursor: default; padding: .2rem 0; position: relative; color: var(--faint);
|
|
1613
|
+
}
|
|
1614
|
+
.snode .dot {
|
|
1615
|
+
width: 2rem; height: 2rem; border-radius: 50%; flex: none; display: grid; place-items: center;
|
|
1616
|
+
font-family: var(--mono); font-weight: 600; font-size: .85rem;
|
|
1617
|
+
background: var(--surface); border: 1.5px solid var(--border); color: var(--faint);
|
|
1618
|
+
transition: background .25s ease, border-color .25s ease, color .25s ease;
|
|
1619
|
+
}
|
|
1620
|
+
.snode .lab { display: flex; flex-direction: column; line-height: 1.2; }
|
|
1621
|
+
.snode .lab .t { font-family: var(--display); font-weight: 600; font-size: .9rem; color: var(--muted); transition: color .25s; }
|
|
1622
|
+
.snode .lab .s { font-size: .72rem; color: var(--faint); }
|
|
1623
|
+
.snode .line { position: absolute; top: 1rem; left: 2.6rem; right: .4rem; height: 1.5px; background: var(--border); z-index: -1; }
|
|
1624
|
+
.snode:last-child .line { display: none; }
|
|
1625
|
+
.snode[data-st="active"] .dot { background: var(--accent); border-color: var(--accent); color: #fff; box-shadow: 0 0 0 4px var(--accent-wash); }
|
|
1626
|
+
.snode[data-st="active"] .lab .t { color: var(--ink); }
|
|
1627
|
+
.snode[data-st="done"] { cursor: pointer; }
|
|
1628
|
+
.snode[data-st="done"] .dot { background: var(--accent-wash); border-color: var(--accent-line); color: var(--accent-ink); }
|
|
1629
|
+
.snode[data-st="done"] .line { background: var(--accent-line); }
|
|
1630
|
+
.snode[data-st="done"] .lab .t { color: var(--ink-2); }
|
|
1631
|
+
.snode[data-st="done"]:hover .dot { border-color: var(--accent); }
|
|
1632
|
+
.snode[data-st="locked"] { opacity: .65; }
|
|
1633
|
+
|
|
1634
|
+
/* ---- collapsed step bar (U2) ---- */
|
|
1635
|
+
.done-bar {
|
|
1636
|
+
display: flex; align-items: center; gap: .7rem; width: 100%; text-align: left;
|
|
1637
|
+
background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-md);
|
|
1638
|
+
padding: .8rem 1rem; margin-bottom: 1rem; cursor: pointer; font-family: var(--body);
|
|
1639
|
+
transition: border-color .12s;
|
|
1640
|
+
}
|
|
1641
|
+
.done-bar:hover { border-color: var(--accent-line); }
|
|
1642
|
+
.done-bar .chk { width: 1.4rem; height: 1.4rem; border-radius: 50%; background: var(--accent-wash); border: 1px solid var(--accent-line); color: var(--accent-ink); display: grid; place-items: center; font-size: .8rem; flex: none; }
|
|
1643
|
+
.done-bar .lab { font-family: var(--display); font-weight: 600; color: var(--ink-2); font-size: .9rem; }
|
|
1644
|
+
.done-bar .meta { font-family: var(--mono); font-size: .75rem; color: var(--faint); }
|
|
1645
|
+
.done-bar .reopen { margin-left: auto; font-family: var(--mono); font-size: .72rem; color: var(--accent-ink); }
|
|
1646
|
+
|
|
1647
|
+
/* ---- waiting / detect panel (U2) ---- */
|
|
1648
|
+
.detect {
|
|
1649
|
+
margin-top: 1.1rem; border: 1px dashed var(--border); border-radius: var(--r-md);
|
|
1650
|
+
padding: .95rem 1.05rem; display: flex; align-items: center; gap: .8rem; background: var(--bg-2);
|
|
1651
|
+
}
|
|
1652
|
+
.detect .radar { width: 12px; height: 12px; border-radius: 50%; background: var(--faint); flex: none; position: relative; }
|
|
1653
|
+
.detect .radar::after { content: ""; position: absolute; inset: -5px; border-radius: 50%; border: 1.5px solid var(--faint); opacity: 0; }
|
|
1654
|
+
.detect[data-listening="true"] .radar { background: var(--accent); }
|
|
1655
|
+
.detect[data-listening="true"] .radar::after { border-color: var(--accent); animation: ping 1.6s ease-out infinite; }
|
|
1656
|
+
@keyframes ping { 0% { transform: scale(.6); opacity: .7; } 100% { transform: scale(1.8); opacity: 0; } }
|
|
1657
|
+
.detect .txt { font-size: .86rem; color: var(--muted); flex: 1; }
|
|
1658
|
+
.detect .txt b { color: var(--ink-2); font-weight: 600; }
|
|
1659
|
+
|
|
1660
|
+
/* ---- celebrate banner (U2) ---- */
|
|
1661
|
+
.celebrate {
|
|
1662
|
+
display: flex; align-items: center; gap: .7rem; background: var(--accent-wash);
|
|
1663
|
+
border: 1px solid var(--accent-line); color: var(--accent-ink); border-radius: var(--r-md);
|
|
1664
|
+
padding: .8rem 1rem; margin-bottom: 1.2rem; font-weight: 500; font-size: .92rem;
|
|
1665
|
+
}
|
|
1666
|
+
.celebrate .chk {
|
|
1667
|
+
width: 1.5rem; height: 1.5rem; border-radius: 50%; background: var(--accent); color: #fff; flex: none;
|
|
1668
|
+
display: grid; place-items: center; font-size: .85rem;
|
|
1669
|
+
}
|
|
1670
|
+
.pop-in { animation: popin .4s cubic-bezier(.2,.9,.3,1.3); }
|
|
1671
|
+
@keyframes popin { 0% { transform: scale(.9); opacity: 0; } 100% { transform: scale(1); opacity: 1; } }
|
|
1672
|
+
|
|
1673
|
+
/* ---- stage nav buttons (U2) ---- */
|
|
1674
|
+
.btnrow { display: flex; flex-wrap: wrap; gap: .6rem; margin-top: 1.3rem; align-items: center; }
|
|
1675
|
+
.btn {
|
|
1676
|
+
font-family: var(--display); font-weight: 600; font-size: .85rem; background: var(--surface); color: var(--ink-2);
|
|
1677
|
+
cursor: pointer; border: 1px solid var(--border); border-radius: var(--r-full); padding: .6rem 1rem;
|
|
1678
|
+
display: inline-flex; align-items: center; gap: .45rem; transition: border-color .12s, color .12s, background .12s;
|
|
1679
|
+
}
|
|
1680
|
+
.btn:hover { border-color: var(--accent-line); color: var(--ink); }
|
|
1681
|
+
.btn.ghost { background: transparent; }
|
|
1682
|
+
.btn:disabled { opacity: .5; cursor: not-allowed; }
|
|
1683
|
+
.btn svg { width: 15px; height: 15px; }
|
|
1684
|
+
.navspace { margin-left: auto; }
|
|
1685
|
+
|
|
1686
|
+
/* ---- future / roadmap note (U2) ---- */
|
|
1687
|
+
.future {
|
|
1688
|
+
margin-top: 1.2rem; border: 1px dashed var(--accent-line); background: var(--accent-wash);
|
|
1689
|
+
border-radius: var(--r-md); padding: .85rem 1rem; font-size: .84rem; color: var(--accent-ink);
|
|
1690
|
+
display: flex; gap: .6rem; align-items: flex-start;
|
|
1691
|
+
}
|
|
1692
|
+
.future b { font-weight: 700; }
|
|
1693
|
+
.future .tag { font-family: var(--mono); font-size: .64rem; text-transform: uppercase; letter-spacing: .1em; border: 1px solid var(--accent-line); border-radius: var(--r-full); padding: .1rem .45rem; flex: none; }
|
|
1694
|
+
|
|
1695
|
+
/* C8 · live-preview iframe — click an element here to jump to its source */
|
|
1696
|
+
.preview-embed { margin-top: 1.1rem; border: 1px solid var(--border); border-radius: var(--r-lg); overflow: hidden; background: var(--surface); }
|
|
1697
|
+
.pe-bar { display: flex; align-items: center; gap: .5rem; padding: .5rem .7rem; border-bottom: 1px solid var(--border-2); background: var(--bg-2); }
|
|
1698
|
+
.pe-bar .dots { display: flex; gap: .35rem; }
|
|
1699
|
+
.pe-bar .dot { width: 10px; height: 10px; border-radius: 50%; }
|
|
1700
|
+
.pe-url { flex: 1; font-family: var(--mono); font-size: .72rem; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1701
|
+
.pe-expand { font-family: var(--display); font-weight: 600; font-size: .74rem; color: var(--ink-2); background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-full); padding: .3rem .7rem; cursor: pointer; }
|
|
1702
|
+
.pe-expand:hover { border-color: var(--accent-line); color: var(--ink); }
|
|
1703
|
+
.pe-frame { width: 100%; height: 320px; border: 0; background: #fff; display: block; }
|
|
1704
|
+
.pe-hint { padding: .5rem .8rem; font-size: .78rem; color: var(--muted); border-top: 1px solid var(--border-2); background: var(--bg-2); }
|
|
1705
|
+
.pe-hint b { color: var(--ink-2); font-weight: 600; }
|
|
1706
|
+
dialog.pe-dialog { width: min(94vw, 1200px); height: min(90vh, 820px); max-width: none; max-height: none; padding: 0; border: 1px solid var(--border); border-radius: var(--r-lg); box-shadow: var(--sh-lg); background: var(--surface); }
|
|
1707
|
+
dialog.pe-dialog[open] { display: flex; flex-direction: column; }
|
|
1708
|
+
dialog.pe-dialog::backdrop { background: rgba(10, 37, 64, .55); }
|
|
1709
|
+
.ped-frame { flex: 1; width: 100%; border: 0; background: #fff; }
|
|
1710
|
+
|
|
1711
|
+
/* C3 · Files tab — in-browser file tree + read-only viewer */
|
|
1712
|
+
.devfiles { margin-top: 1.1rem; border: 1px solid var(--border); border-radius: var(--r-lg); overflow: hidden; background: var(--surface); }
|
|
1713
|
+
.df-head { display: flex; align-items: baseline; justify-content: space-between; gap: .75rem; padding: .55rem .85rem; border-bottom: 1px solid var(--border-2); background: var(--bg-2); }
|
|
1714
|
+
.df-title { font-weight: 700; font-size: .82rem; color: var(--ink); }
|
|
1715
|
+
.df-path { font-family: var(--mono); font-size: .72rem; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1716
|
+
.df-body { display: grid; grid-template-columns: minmax(160px, 240px) 1fr; min-height: 260px; max-height: 420px; }
|
|
1717
|
+
.df-tree { overflow: auto; border-right: 1px solid var(--border-2); padding: .35rem 0; font-size: .8rem; }
|
|
1718
|
+
.df-main { display: flex; flex-direction: column; min-width: 0; background: var(--term); }
|
|
1719
|
+
.df-editbar { flex: none; display: flex; gap: .7rem; align-items: center; min-height: 30px; padding: .35rem .8rem; border-bottom: 1px solid var(--term-line); background: var(--term-2); font-size: .72rem; color: var(--term-dim); }
|
|
1720
|
+
.df-status { font-family: var(--mono); }
|
|
1721
|
+
.df-status.ok { color: var(--term-ok); }
|
|
1722
|
+
.df-status.warn { color: #f6b0b0; }
|
|
1723
|
+
.df-conflict { display: inline-flex; gap: .4rem; align-items: center; color: #f6b0b0; }
|
|
1724
|
+
.df-mini { font-family: var(--mono); font-size: .7rem; padding: .1rem .45rem; border-radius: var(--r-sm); border: 1px solid var(--term-line); background: transparent; color: var(--term-fg); cursor: pointer; }
|
|
1725
|
+
.df-mini:hover { background: var(--term-line); }
|
|
1726
|
+
.df-view { flex: 1 1 auto; width: 100%; box-sizing: border-box; display: block; margin: 0; border: 0; resize: none; outline: none; overflow: auto; padding: .75rem .9rem; font-family: var(--mono); font-size: .76rem; line-height: 1.55; color: var(--term-fg); background: var(--term); white-space: pre; tab-size: 2; }
|
|
1727
|
+
.df-view[readonly] { opacity: .9; }
|
|
1728
|
+
.df-diffbtn { margin-left: auto; }
|
|
1729
|
+
.df-diff { flex: 1 1 auto; margin: 0; overflow: auto; padding: .5rem 0; font-family: var(--mono); font-size: .76rem; line-height: 1.5; background: var(--term); color: var(--term-fg); }
|
|
1730
|
+
.df-diff .dl { display: block; padding: 0 .9rem; white-space: pre; }
|
|
1731
|
+
.df-diff .add { background: rgba(52, 211, 153, .16); color: #bff2e0; }
|
|
1732
|
+
.df-diff .del { background: rgba(246, 176, 176, .15); color: #f6c2c2; }
|
|
1733
|
+
.df-diff .ctx { color: var(--term-dim); }
|
|
1734
|
+
.ft-root, .ft-children { list-style: none; margin: 0; padding: 0; }
|
|
1735
|
+
.ft-row { display: flex; align-items: center; gap: .4rem; padding: .18rem .5rem .18rem 0; cursor: default; border-radius: var(--r-sm); }
|
|
1736
|
+
.ft-file .ft-row { cursor: pointer; }
|
|
1737
|
+
.ft-file .ft-row:hover { background: var(--accent-wash); }
|
|
1738
|
+
.ft-row.sel { background: var(--accent-wash); color: var(--accent-ink); font-weight: 600; }
|
|
1739
|
+
.ft-row:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
|
|
1740
|
+
.ft-ic { font-size: .78rem; flex: none; opacity: .8; }
|
|
1741
|
+
.ft-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1742
|
+
.future-note { margin-top: .8rem; font-size: .8rem; color: var(--muted); display: flex; gap: .5rem; align-items: baseline; }
|
|
1743
|
+
.future-note .tag { font-family: var(--mono); font-size: .64rem; text-transform: uppercase; letter-spacing: .1em; border: 1px solid var(--accent-line); border-radius: var(--r-full); padding: .1rem .45rem; flex: none; color: var(--accent-ink); }
|
|
1744
|
+
@media (max-width: 560px) { .df-body { grid-template-columns: 1fr; } .df-view { max-height: 220px; } }
|
|
1745
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1746
|
+
.detect[data-listening="true"] .radar::after, .pop-in { animation: none !important; }
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
/* ---- Ask (quiet loop-recovery help, U3) ---- */
|
|
1750
|
+
.ask-fab {
|
|
1751
|
+
position: fixed; right: 1.4rem; bottom: 1.4rem; z-index: 50;
|
|
1752
|
+
display: inline-flex; align-items: center; gap: .5rem;
|
|
1753
|
+
background: var(--surface); color: var(--ink-2); border: 1px solid var(--border);
|
|
1754
|
+
border-radius: var(--r-full); padding: .6rem .95rem; cursor: pointer; box-shadow: var(--sh-md);
|
|
1755
|
+
font-family: var(--body); font-size: .85rem; transition: border-color .12s, color .12s;
|
|
1756
|
+
}
|
|
1757
|
+
.ask-fab:hover { border-color: var(--accent-line); color: var(--ink); }
|
|
1758
|
+
.ask-fab .u { text-decoration: underline; text-underline-offset: 3px; text-decoration-color: var(--accent-line); }
|
|
1759
|
+
.ask-fab svg { width: 16px; height: 16px; color: var(--accent); }
|
|
1760
|
+
.ask-panel {
|
|
1761
|
+
position: fixed; right: 1.4rem; bottom: 4.6rem; z-index: 51; width: min(23rem, 90vw);
|
|
1762
|
+
background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-lg);
|
|
1763
|
+
box-shadow: var(--sh-lg); overflow: hidden; transform-origin: bottom right;
|
|
1764
|
+
opacity: 0; visibility: hidden; transform: translateY(8px) scale(.98); transition: opacity .16s, transform .16s, visibility .16s;
|
|
1765
|
+
}
|
|
1766
|
+
.ask-panel.open { opacity: 1; visibility: visible; transform: translateY(0) scale(1); }
|
|
1767
|
+
.ask-head { padding: .95rem 1.1rem; border-bottom: 1px solid var(--border-2); }
|
|
1768
|
+
.ask-head strong { font-family: var(--display); color: var(--ink); font-size: .98rem; display: block; }
|
|
1769
|
+
.ask-head span { font-size: .8rem; color: var(--muted); }
|
|
1770
|
+
.qa { padding: .3rem .5rem .6rem; max-height: 60vh; overflow-y: auto; }
|
|
1771
|
+
.qa details { border-bottom: 1px solid var(--border-2); }
|
|
1772
|
+
.qa details:last-child { border-bottom: none; }
|
|
1773
|
+
.qa summary { list-style: none; cursor: pointer; padding: .7rem .6rem; display: flex; align-items: center; gap: .6rem; font-size: .88rem; color: var(--ink); }
|
|
1774
|
+
.qa summary::-webkit-details-marker { display: none; }
|
|
1775
|
+
.qa summary .q { font-family: var(--mono); color: var(--accent); font-size: .78rem; flex: none; }
|
|
1776
|
+
.qa summary .chev { margin-left: auto; color: var(--faint); transition: transform .18s; }
|
|
1777
|
+
.qa details[open] summary .chev { transform: rotate(90deg); }
|
|
1778
|
+
.qa .ans { padding: 0 .6rem .85rem 1.9rem; color: var(--muted); font-size: .84rem; }
|
|
1779
|
+
.qa .ans code { font-family: var(--mono); font-size: .8rem; color: var(--ink-2); background: var(--bg-2); padding: .05em .4em; border-radius: 5px; }
|
|
1780
|
+
.qa .ans b { color: var(--ink-2); font-weight: 600; }
|
|
1781
|
+
.ask-foot { padding: .7rem 1.1rem; border-top: 1px solid var(--border-2); font-family: var(--mono); font-size: .68rem; color: var(--faint); }
|
|
1782
|
+
@media (prefers-reduced-motion: reduce) { .ask-panel { transition: none; } }
|
|
1783
|
+
|
|
1784
|
+
/* ---- what's next : coming soon shelf (U4) ---- */
|
|
1785
|
+
.next { margin-top: 3.4rem; padding-top: 2rem; border-top: 1px dashed var(--border); }
|
|
1786
|
+
.next h3 { font-family: var(--display); font-weight: 700; color: var(--ink-2); font-size: 1.1rem; margin: 0 0 .3rem; letter-spacing: -.02em; }
|
|
1787
|
+
.next .sub { margin: 0 0 1.2rem; font-size: .88rem; color: var(--muted); max-width: 52ch; }
|
|
1788
|
+
.nextgrid { display: grid; gap: .7rem; }
|
|
1789
|
+
@media (min-width: 40rem) { .nextgrid { grid-template-columns: 1fr 1fr; } }
|
|
1790
|
+
@media (min-width: 60rem) { .nextgrid { grid-template-columns: repeat(4, 1fr); } }
|
|
1791
|
+
.nextcard {
|
|
1792
|
+
display: flex; flex-direction: column; gap: .3rem; background: transparent;
|
|
1793
|
+
border: 1px solid var(--border); border-radius: var(--r-md); padding: .9rem 1rem; opacity: .78;
|
|
1794
|
+
}
|
|
1795
|
+
.nextcard .path { font-family: var(--mono); font-size: .8rem; color: var(--muted); }
|
|
1796
|
+
.nextcard .d { font-size: .8rem; color: var(--faint); }
|
|
1797
|
+
.nextcard .soon { align-self: flex-start; margin-top: .3rem; font-family: var(--mono); font-size: .64rem; text-transform: uppercase; letter-spacing: .1em; color: var(--faint); border: 1px solid var(--border); border-radius: var(--r-full); padding: .1rem .5rem; }
|
|
1798
|
+
|
|
1799
|
+
/* ---- footer ---- */
|
|
1800
|
+
.foot {
|
|
1801
|
+
margin-top: 2.8rem; padding-top: 1.2rem; border-top: 1px solid var(--border);
|
|
1802
|
+
display: flex; flex-wrap: wrap; gap: 1rem 1.4rem; align-items: center;
|
|
1803
|
+
font-family: var(--mono); font-size: .78rem; color: var(--muted);
|
|
1804
|
+
}
|
|
1805
|
+
.foot .sp { margin-left: auto; }
|
|
1806
|
+
.foot a { color: var(--muted); text-decoration: none; }
|
|
1807
|
+
.foot a:hover { color: var(--accent-ink); }
|
|
1808
|
+
|
|
1809
|
+
/* ---- toast ---- */
|
|
1810
|
+
.toast {
|
|
1811
|
+
position: fixed; left: 50%; bottom: 1.6rem; transform: translateX(-50%) translateY(160%);
|
|
1812
|
+
/* Terminal palette (dark bg + light fg in BOTH themes) — a plain var(--ink)
|
|
1813
|
+
background went white-on-white in dark mode. */
|
|
1814
|
+
background: var(--term); color: var(--term-fg); border: 1px solid var(--term-line);
|
|
1815
|
+
font-family: var(--mono); font-size: .8rem;
|
|
1816
|
+
padding: .7rem 1.1rem; border-radius: var(--r-full); box-shadow: var(--sh-lg);
|
|
1817
|
+
transition: transform .28s cubic-bezier(.2,.9,.3,1.2); z-index: 60; pointer-events: none;
|
|
1818
|
+
display: inline-flex; align-items: center; gap: .5rem;
|
|
1819
|
+
}
|
|
1820
|
+
.toast.show { transform: translateX(-50%) translateY(0); }
|
|
1821
|
+
.toast .ok { color: var(--term-ok); }
|
|
1822
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1823
|
+
.cta, .toast, .notice a.action, .info-pop { transition: none; }
|
|
1824
|
+
}
|
|
1825
|
+
[hidden] { display: none !important; }
|
|
1826
|
+
</style>
|
|
1827
|
+
</head>
|
|
1828
|
+
<body>
|
|
1829
|
+
<div class="wrap">
|
|
1830
|
+
<header class="topbar">
|
|
1831
|
+
<div class="brand">
|
|
1832
|
+
<span class="mark" aria-hidden="true">
|
|
1833
|
+
<svg viewBox="0 0 24 24" fill="none"><path d="M12 2.5 20 6v6c0 4.6-3.2 7.9-8 9.5-4.8-1.6-8-4.9-8-9.5V6l8-3.5Z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/><path d="m8.5 12 2.4 2.4L15.8 9.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
1834
|
+
</span>
|
|
1835
|
+
<span class="who"><span class="dom">{appDomain}</span><span class="sub">Developer</span></span>
|
|
1836
|
+
</div>
|
|
1837
|
+
<div class="right">
|
|
1838
|
+
{session?.email ? <span class="sess">{session.email}</span> : null}
|
|
1839
|
+
<button class="themetog" id="themeBtn" type="button">◐ Theme</button>
|
|
1840
|
+
</div>
|
|
1841
|
+
</header>
|
|
1842
|
+
|
|
1843
|
+
{signedIn === false && (
|
|
1844
|
+
<div class="notice">
|
|
1845
|
+
<p>{isCredentialRecovery
|
|
1846
|
+
? recoveryEmailHint
|
|
1847
|
+
? <>Sign in with the invite email hinted as <strong>{recoveryEmailHint}</strong> to generate a fresh setup command for this machine.</>
|
|
1848
|
+
: <>Sign in with the same email that accepted the invite to generate a fresh setup command for this machine.</>
|
|
1849
|
+
: <>Sign in with the email your invite came to — then this page shows your store and your one command.</>}</p>
|
|
1850
|
+
<a class="action" href={loginHref}>Sign in</a>
|
|
1851
|
+
</div>
|
|
1852
|
+
)}
|
|
1853
|
+
|
|
1854
|
+
{noStoreAccess && (
|
|
1855
|
+
<div class="notice">
|
|
1856
|
+
<p>You're signed in{session?.email ? <>{" "}as <strong>{session.email}</strong></> : null}, but this account isn't linked to a store yet. Ask whoever sent your invite to grant developer access — then reload this page.</p>
|
|
1857
|
+
<a class="action" href="/auth/logout">Switch account</a>
|
|
1858
|
+
</div>
|
|
1859
|
+
)}
|
|
1860
|
+
|
|
1861
|
+
{/* Bridge strip (replaces the big live-dev-link card) — the rail's power source,
|
|
1862
|
+
fed by the activity poll's `runtime`. Hidden until the store has been seen
|
|
1863
|
+
live this page-view (sawLive); localhost is the developer's OWN machine, so
|
|
1864
|
+
clicking is a plain navigation. Pulses teal while live; turns amber with a
|
|
1865
|
+
recovery hint when the loop goes quiet (data-live="0"). */}
|
|
1866
|
+
{hostedActivity && (
|
|
1867
|
+
<section class="bridge" id="bridge" data-live="1" hidden>
|
|
1868
|
+
<span class="beacon" aria-hidden="true"></span>
|
|
1869
|
+
<div class="b-txt">
|
|
1870
|
+
<span class="b-kicker" id="bKicker">Live on your machine</span>
|
|
1871
|
+
<a class="b-url" id="bUrl" href="#" target="_blank" rel="noreferrer">http://localhost/</a>
|
|
1872
|
+
</div>
|
|
1873
|
+
<span class="b-diag" id="bDiag" hidden></span>
|
|
1874
|
+
<a class="b-go" id="bGo" href="#" target="_blank" rel="noreferrer">Open your store ↗</a>
|
|
1875
|
+
<p class="b-hint" id="bHint" hidden>Your store went quiet. Run <code>tot start</code> in your project to bring it back — Step 1 has the command.</p>
|
|
1876
|
+
</section>
|
|
1877
|
+
)}
|
|
1878
|
+
|
|
1879
|
+
{isLocalDevLoop && (
|
|
1880
|
+
<nav class="stepper" id="stepper" aria-label="Setup progress">
|
|
1881
|
+
<button class="snode" type="button" data-go="run" data-st="active">
|
|
1882
|
+
<span class="dot">1</span><span class="lab"><span class="t">Get running</span><span class="s">on your machine</span></span><span class="line"></span>
|
|
1883
|
+
</button>
|
|
1884
|
+
<button class="snode" type="button" data-go="edit" data-st="locked">
|
|
1885
|
+
<span class="dot">2</span><span class="lab"><span class="t">Make an edit</span><span class="s">see it reload</span></span><span class="line"></span>
|
|
1886
|
+
</button>
|
|
1887
|
+
<button class="snode" type="button" data-go="next" data-st="locked">
|
|
1888
|
+
<span class="dot">3</span><span class="lab"><span class="t">What's next</span><span class="s">coming soon</span></span>
|
|
1889
|
+
</button>
|
|
1890
|
+
</nav>
|
|
1891
|
+
)}
|
|
1892
|
+
|
|
1893
|
+
{isLocalDevLoop && (
|
|
1894
|
+
<button class="done-bar" id="bar-run" type="button" data-go="run" hidden>
|
|
1895
|
+
<span class="chk">✓</span><span class="lab">Store running</span><span class="meta" id="bar-run-meta">on your machine</span><span class="reopen">reopen</span>
|
|
1896
|
+
</button>
|
|
1897
|
+
)}
|
|
1898
|
+
{isLocalDevLoop && (
|
|
1899
|
+
<button class="done-bar" id="bar-edit" type="button" data-go="edit" hidden>
|
|
1900
|
+
<span class="chk">✓</span><span class="lab">First edit landed</span><span class="meta" id="bar-edit-meta">content/home.html</span><span class="reopen">reopen</span>
|
|
1901
|
+
</button>
|
|
1902
|
+
)}
|
|
1903
|
+
|
|
1904
|
+
{/* Station 1 — SHARED across all variants. #stage-run stays rendered exactly
|
|
1905
|
+
once with its inner content unchanged; class:list gives non-hosted variants
|
|
1906
|
+
neutral wrappers (.strun has no CSS), while hosted gets the rail station grid
|
|
1907
|
+
+ node column + collapsible done row. */}
|
|
1908
|
+
<section id="st-run" class:list={["strun", { station: hostedActivity }]} data-st="open">
|
|
1909
|
+
{hostedActivity && (
|
|
1910
|
+
<div class="nodecol">
|
|
1911
|
+
<span class="node" aria-hidden="true"><span class="num">1</span><svg viewBox="0 0 24 24" fill="none"><path d="m5 12.5 4.5 4.5L19 7.5" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"/></svg><span class="spark" style="--dx:1;--dy:-.4;background:#0e9f8e"></span><span class="spark" style="--dx:-1;--dy:-.7;background:#2fd4bf"></span><span class="spark" style="--dx:.6;--dy:1;background:#f0c56b"></span><span class="spark" style="--dx:-.7;--dy:.8;background:#0e9f8e"></span><span class="spark" style="--dx:0;--dy:-1.1;background:#2fd4bf"></span><span class="spark" style="--dx:1.1;--dy:.5;background:#b7791f"></span></span>
|
|
1912
|
+
<span class="seg" id="seg-run" data-lit="0" data-flow="0"></span>
|
|
1913
|
+
</div>
|
|
1914
|
+
)}
|
|
1915
|
+
<div class:list={{ "s-body": hostedActivity }}>
|
|
1916
|
+
{hostedActivity && (
|
|
1917
|
+
<button class="s-done" id="done-run" type="button" hidden>
|
|
1918
|
+
<span class="lab">Store running</span><span class="meta" id="doneRunMeta"></span><span class="reopen">Reopen ↓</span>
|
|
1919
|
+
</button>
|
|
1920
|
+
)}
|
|
1921
|
+
{hostedActivity && (
|
|
1922
|
+
<p class="quiet-note" id="quietNote" hidden><span aria-hidden="true">◍</span><span>Your store went quiet — it stopped answering from your machine. <code>tot start</code> brings it back.</span></p>
|
|
1923
|
+
)}
|
|
1924
|
+
<section class="stage" id="stage-run">
|
|
1925
|
+
<div class="stage-body">
|
|
1926
|
+
<span class="kicker">Step 1</span>
|
|
1927
|
+
<h1>Get your store running.
|
|
1928
|
+
<span class="info" id="info1">
|
|
1929
|
+
<button class="info-btn" type="button" aria-label="What does this command do?">i</button>
|
|
1930
|
+
<span class="info-pop" role="tooltip">Installs the <code>tot</code> command, signs your terminal in with a single-use code, checks out your store, runs it, and opens your browser. No Docker, no browser sign-in, nothing to provision. From then on it's just <code>tot start</code>.</span>
|
|
1931
|
+
</span>
|
|
1932
|
+
</h1>
|
|
1933
|
+
<p class="goal">{hasStoreAccess
|
|
1934
|
+
? isCredentialRecovery
|
|
1935
|
+
? <>Generate a fresh personal setup command, then paste it into your terminal and press Enter.</>
|
|
1936
|
+
: <>Generate your personal setup command, then paste it into your terminal and press Enter.</>
|
|
1937
|
+
: <>Open the invite link we emailed you to unlock your personal setup command.</>}</p>
|
|
1938
|
+
|
|
1939
|
+
<div class="term">
|
|
1940
|
+
<div class="term-bar"><span class="dots"><span class="dot r"></span><span class="dot y"></span><span class="dot g"></span></span><span class="ttl">your terminal</span></div>
|
|
1941
|
+
<div class="term-body"><span class="c"># your personal, single-use setup command</span>
|
|
1942
|
+
<span class="cmd is-placeholder" id="cli-cmd-text">{hasStoreAccess ? (isCredentialRecovery ? "→ click “Generate a fresh setup command” below" : "→ click “Generate my setup command” below") : "→ available once you’re signed in via your invite"}</span></div>
|
|
1943
|
+
</div>
|
|
1944
|
+
|
|
1945
|
+
{hasStoreAccess && (
|
|
1946
|
+
<div class="cmd-actions">
|
|
1947
|
+
<button class="cta" id="copyCmd" type="button" data-fastpath="1">
|
|
1948
|
+
<svg viewBox="0 0 24 24" fill="none"><path d="M12 5v14M5 12h14" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
|
|
1949
|
+
{isCredentialRecovery ? "Generate a fresh setup command" : "Generate my setup command"}
|
|
1950
|
+
</button>
|
|
1951
|
+
</div>
|
|
1952
|
+
)}
|
|
1953
|
+
<p class="cmd-note" id="cli-cmd-note">{hasStoreAccess
|
|
1954
|
+
? <>This mints a <em>single-use, short-lived</em> sign-in code, shows the full command here, and copies it to your clipboard — paste it and press Enter. It signs you in, runs your store, and opens your browser. Nothing to retype.</>
|
|
1955
|
+
: <>You'll need <em>Node.js and your invite</em>. Sign in through your invite link and this page hands you a one-paste setup command — no Docker, nothing to provision.</>}</p>
|
|
1956
|
+
|
|
1957
|
+
<p class="cmd-steps">Prefer the steps? <code>npm i -g {cliInstallSpec}</code>, then <code>tot checkout {appDomain}</code>, <code>cd {appDomain}</code>, <code>tot dev</code>. New to Node? Get it at <a href="https://nodejs.org">nodejs.org</a> — it includes <code>npm</code>.</p>
|
|
1958
|
+
|
|
1959
|
+
{isLocalDevLoop && (
|
|
1960
|
+
<div class="detect" id="detect" data-listening="true">
|
|
1961
|
+
<span class="radar" aria-hidden="true"></span>
|
|
1962
|
+
<span class="txt" id="detectTxt">Watching for your store to come online on this machine…</span>
|
|
1963
|
+
</div>
|
|
1964
|
+
)}
|
|
1965
|
+
</div>
|
|
1966
|
+
</section>
|
|
1967
|
+
</div>
|
|
1968
|
+
</section>
|
|
1969
|
+
|
|
1970
|
+
{/* Station 2 — hosted-only. Locked label → live-gated teaser → open step body →
|
|
1971
|
+
collapsed done row, all on the rail. */}
|
|
1972
|
+
{hostedActivity && (
|
|
1973
|
+
<section class="station" id="st-edit" data-st="locked">
|
|
1974
|
+
<div class="nodecol">
|
|
1975
|
+
<span class="node" aria-hidden="true"><span class="num">2</span><svg viewBox="0 0 24 24" fill="none"><path d="m5 12.5 4.5 4.5L19 7.5" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"/></svg><span class="spark" style="--dx:1;--dy:-.4;background:#0e9f8e"></span><span class="spark" style="--dx:-1;--dy:-.7;background:#2fd4bf"></span><span class="spark" style="--dx:.6;--dy:1;background:#f0c56b"></span><span class="spark" style="--dx:-.7;--dy:.8;background:#0e9f8e"></span><span class="spark" style="--dx:0;--dy:-1.1;background:#2fd4bf"></span><span class="spark" style="--dx:1.1;--dy:.5;background:#b7791f"></span></span>
|
|
1976
|
+
<span class="seg" id="seg-edit" data-lit="0" data-flow="0"></span>
|
|
1977
|
+
</div>
|
|
1978
|
+
<div class="s-body">
|
|
1979
|
+
<div class="s-locked" id="lock-edit">Change one line <span class="sub">unlocks when your store is running</span></div>
|
|
1980
|
+
<button class="s-done" id="done-edit" type="button" hidden>
|
|
1981
|
+
<span class="lab">First edit landed</span><span class="meta" id="doneEditMeta">content/home.html</span><span class="reopen">Reopen ↓</span>
|
|
1982
|
+
</button>
|
|
1983
|
+
<div class="s-tease" id="tease-edit" hidden>
|
|
1984
|
+
<span class="t-kick">Next up · Step 2</span>
|
|
1985
|
+
<h3>Change one line.</h3>
|
|
1986
|
+
<p>Open <code>content/home.html</code>, edit anything, save. Your store reloads itself and the save shows up here.</p>
|
|
1987
|
+
<button class="t-cta" id="cta-edit" type="button">Make your first edit →</button>
|
|
1988
|
+
</div>
|
|
1989
|
+
<section class="stage" id="hosted-stage-edit" hidden>
|
|
1990
|
+
<div class="stage-body">
|
|
1991
|
+
<p class="ha-encourage"><span class="chk">✓</span><span>You're running locally — nice. One more step: make an edit and watch it land here.</span></p>
|
|
1992
|
+
<span class="kicker">Step 2</span>
|
|
1993
|
+
<h1>Change one line.</h1>
|
|
1994
|
+
<p class="goal">Open <code>content/home.html</code> in your editor, change one line, and save. Your store reloads on its own, and the save shows up in <strong>Your activity</strong> below within a couple of seconds.</p>
|
|
1995
|
+
<div class="term">
|
|
1996
|
+
<div class="term-bar"><span class="dots"><span class="dot r"></span><span class="dot y"></span><span class="dot g"></span></span><span class="ttl">open the file</span></div>
|
|
1997
|
+
<div class="term-body"><span class="c"># open your homepage content to edit</span>
|
|
1998
|
+
<span class="cmd" id="editOpenCmd">$EDITOR content/home.html</span></div>
|
|
1999
|
+
</div>
|
|
2000
|
+
<p class="cmd-note" id="editOpenHint">Tip: set <code>$EDITOR</code> and we'll show the exact open command here.</p>
|
|
2001
|
+
<p class="tip-lay">Lay this cockpit <em>beside</em> your editor and your running store — hot reload is on, so every save reloads the store and lands in <strong>Your activity</strong> here, live.</p>
|
|
2002
|
+
</div>
|
|
2003
|
+
</section>
|
|
2004
|
+
</div>
|
|
2005
|
+
</section>
|
|
2006
|
+
)}
|
|
2007
|
+
|
|
2008
|
+
{/* Station 3 — hosted-only. Its .seg is the last on the rail (hidden via CSS). */}
|
|
2009
|
+
{hostedActivity && (
|
|
2010
|
+
<section class="station" id="st-next" data-st="locked">
|
|
2011
|
+
<div class="nodecol">
|
|
2012
|
+
<span class="node" aria-hidden="true"><span class="num">3</span><svg viewBox="0 0 24 24" fill="none"><path d="m5 12.5 4.5 4.5L19 7.5" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"/></svg></span>
|
|
2013
|
+
<span class="seg"></span>
|
|
2014
|
+
</div>
|
|
2015
|
+
<div class="s-body">
|
|
2016
|
+
<div class="s-locked" id="lock-next">Send it for review <span class="sub">unlocks after your first edit</span></div>
|
|
2017
|
+
<div class="s-tease" id="tease-next" hidden>
|
|
2018
|
+
<span class="t-kick">Next up · Step 3</span>
|
|
2019
|
+
<h3>Send your change for review.</h3>
|
|
2020
|
+
<p>Bundle what you've edited into a compliance-reviewed preview build.</p>
|
|
2021
|
+
<button class="t-cta" id="cta-next" type="button">Go to Step 3 →</button>
|
|
2022
|
+
</div>
|
|
2023
|
+
<section class="stage" id="hosted-stage-next" hidden>
|
|
2024
|
+
<div class="stage-body">
|
|
2025
|
+
<span class="kicker">Step 3</span>
|
|
2026
|
+
<h1>Submit your change for preview.</h1>
|
|
2027
|
+
<p class="goal">You've got the loop: edit locally, see it reflected here. When a change is ready, submit it for a preview build — it goes through the compliance review step before it can go live.</p>
|
|
2028
|
+
<div class="term">
|
|
2029
|
+
<div class="term-bar"><span class="dots"><span class="dot r"></span><span class="dot y"></span><span class="dot g"></span></span><span class="ttl">preview</span></div>
|
|
2030
|
+
<div class="term-body"><span class="c"># bundle your local changes into a reviewable preview</span>
|
|
2031
|
+
<span class="cmd">COMING SOON (tot submit)</span></div>
|
|
2032
|
+
</div>
|
|
2033
|
+
<div class="cmd-actions">
|
|
2034
|
+
<button class="cta" id="submitPreview" type="button">
|
|
2035
|
+
<svg viewBox="0 0 24 24" fill="none"><path d="M5 12h14M13 6l6 6-6 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
2036
|
+
Submit for preview
|
|
2037
|
+
</button>
|
|
2038
|
+
</div>
|
|
2039
|
+
<p class="cmd-note">This packages your changes and kicks off a compliance-reviewed preview build.</p>
|
|
2040
|
+
</div>
|
|
2041
|
+
</section>
|
|
2042
|
+
</div>
|
|
2043
|
+
</section>
|
|
2044
|
+
)}
|
|
2045
|
+
|
|
2046
|
+
{/* Activity ledger — positioned AFTER the stations. Keeps the ids the activity
|
|
2047
|
+
renderer targets (#haTable/#haBody/#haEmpty/#haToggle). Celebration is now the
|
|
2048
|
+
Step-2 node stamp + toast, so the old inline banner is gone. */}
|
|
2049
|
+
{hostedActivity && (
|
|
2050
|
+
<section class="hosted-activity" id="hostedActivity" hidden>
|
|
2051
|
+
<div class="ha-head">
|
|
2052
|
+
<h2>Your activity</h2>
|
|
2053
|
+
<span class="ha-sub">reported by <code>tot dev</code> on your machine</span>
|
|
2054
|
+
</div>
|
|
2055
|
+
<p class="ha-empty" id="haEmpty">No saves yet — that's Step 2. Edit a file locally and it appears here.</p>
|
|
2056
|
+
<table class="ha-table" id="haTable" hidden>
|
|
2057
|
+
<tbody id="haBody"></tbody>
|
|
2058
|
+
</table>
|
|
2059
|
+
<button class="ha-toggle" id="haToggle" type="button" hidden></button>
|
|
2060
|
+
</section>
|
|
2061
|
+
)}
|
|
2062
|
+
|
|
2063
|
+
{isLocalDevLoop && (
|
|
2064
|
+
<section class="stage" id="stage-edit" hidden>
|
|
2065
|
+
<div class="stage-body">
|
|
2066
|
+
<div class="celebrate pop-in" id="cel-run" hidden>
|
|
2067
|
+
<span class="chk">✓</span><span>You're live — your store is running on this machine. Now let's change something.</span>
|
|
2068
|
+
</div>
|
|
2069
|
+
<span class="kicker">Step 2</span>
|
|
2070
|
+
<h1>Make your first edit.</h1>
|
|
2071
|
+
<p class="goal">Open a content file in your editor, change one line, and save. Your store reloads on its own — watch the loop react below.</p>
|
|
2072
|
+
|
|
2073
|
+
<div class="term">
|
|
2074
|
+
<div class="term-bar"><span class="dots"><span class="dot r"></span><span class="dot y"></span><span class="dot g"></span></span><span class="ttl">your editor</span></div>
|
|
2075
|
+
<div class="term-body"><span class="c"># open your store's homepage source, edit the headline, save</span>
|
|
2076
|
+
<span class="cmd">content/home.html</span></div>
|
|
2077
|
+
</div>
|
|
2078
|
+
|
|
2079
|
+
<div class="detect" id="detect-edit" data-listening="true">
|
|
2080
|
+
<span class="radar" aria-hidden="true"></span>
|
|
2081
|
+
<span class="txt" id="detectEditTxt">Waiting for your first save… save a watched file and it shows up here instantly.</span>
|
|
2082
|
+
</div>
|
|
2083
|
+
|
|
2084
|
+
<div class="preview-embed" id="previewEmbed">
|
|
2085
|
+
<div class="pe-bar">
|
|
2086
|
+
<span class="dots"><span class="dot r"></span><span class="dot y"></span><span class="dot g"></span></span>
|
|
2087
|
+
<span class="pe-url" id="previewUrl">localhost preview</span>
|
|
2088
|
+
<button type="button" class="pe-expand" id="previewExpand">⤢ Full screen</button>
|
|
2089
|
+
</div>
|
|
2090
|
+
{/* src is set client-side from /__tot/dev-loop-status (the DEV tenant) —
|
|
2091
|
+
NOT the SSR appDomain, which runs in workerd and can't see TOT_DEV_TENANT. */}
|
|
2092
|
+
<iframe class="pe-frame" id="previewFrame" title="live preview" loading="lazy"></iframe>
|
|
2093
|
+
<p class="pe-hint">Your running store. Open the <b>✦</b> widget inside it, hit <b>◎ Locate</b>, then click any element — its source file opens below with the cursor on that line.</p>
|
|
2094
|
+
</div>
|
|
2095
|
+
|
|
2096
|
+
<div class="devfiles" id="devfiles">
|
|
2097
|
+
<div class="df-head">
|
|
2098
|
+
<span class="df-title">Your store's files</span>
|
|
2099
|
+
<span class="df-path" id="filesPath">select a file to view</span>
|
|
2100
|
+
</div>
|
|
2101
|
+
<div class="df-body">
|
|
2102
|
+
<div class="df-tree" id="filesTree">Loading your files…</div>
|
|
2103
|
+
<div class="df-main">
|
|
2104
|
+
<div class="df-editbar" id="filesEditbar" hidden>
|
|
2105
|
+
<span class="df-status" id="filesStatus"></span>
|
|
2106
|
+
<span class="df-conflict" id="filesConflict" hidden>Changed on disk.
|
|
2107
|
+
<button type="button" class="df-mini" id="filesReload">Reload</button>
|
|
2108
|
+
<button type="button" class="df-mini" id="filesForce">Overwrite</button>
|
|
2109
|
+
</span>
|
|
2110
|
+
<button type="button" class="df-mini df-diffbtn" id="filesDiffBtn" hidden>Diff</button>
|
|
2111
|
+
</div>
|
|
2112
|
+
<textarea class="df-view" id="filesView" readonly spellcheck="false" wrap="off">Pick a file on the left to view its source.</textarea>
|
|
2113
|
+
<pre class="df-diff" id="filesDiff" hidden></pre>
|
|
2114
|
+
</div>
|
|
2115
|
+
</div>
|
|
2116
|
+
</div>
|
|
2117
|
+
<p class="future-note">
|
|
2118
|
+
<span class="tag">Soon</span> Live editing is on. A visual diff and click-an-element-to-jump-to-source layer onto this panel next.
|
|
2119
|
+
</p>
|
|
2120
|
+
|
|
2121
|
+
<div class="btnrow">
|
|
2122
|
+
<button class="btn ghost" id="backToRun" type="button" data-go="run">← Back</button>
|
|
2123
|
+
<button class="btn ghost navspace" id="toNext" type="button" data-go="next" disabled>Next: what's next →</button>
|
|
2124
|
+
</div>
|
|
2125
|
+
</div>
|
|
2126
|
+
</section>
|
|
2127
|
+
)}
|
|
2128
|
+
|
|
2129
|
+
{isLocalDevLoop && (
|
|
2130
|
+
<section class="stage" id="stage-next" hidden>
|
|
2131
|
+
<div class="stage-body">
|
|
2132
|
+
<div class="celebrate pop-in" id="cel-edit" hidden>
|
|
2133
|
+
<span class="chk">✓</span><span>You changed a file and saw it on your site. That's the loop — you're in control.</span>
|
|
2134
|
+
</div>
|
|
2135
|
+
<span class="kicker">You're in control</span>
|
|
2136
|
+
<h1>You've got the loop.</h1>
|
|
2137
|
+
<p class="goal">Edit, save, see it — with no terminal round-trip. Previews, publish checks, AI assistance, and diagnostics open up from here soon.</p>
|
|
2138
|
+
<div class="btnrow">
|
|
2139
|
+
<button class="btn ghost" id="backToEdit" type="button" data-go="edit">← Back to editing</button>
|
|
2140
|
+
</div>
|
|
2141
|
+
</div>
|
|
2142
|
+
</section>
|
|
2143
|
+
)}
|
|
2144
|
+
|
|
2145
|
+
{isLocalDevLoop && (
|
|
2146
|
+
<div class="loopbar" id="loopbar" data-state="running" aria-live="polite">
|
|
2147
|
+
<div class="lb-head" id="lbHead">
|
|
2148
|
+
<span class="pill"><span class="beat"></span><span id="lbStatus">Connecting…</span></span>
|
|
2149
|
+
<span class="lu">watching <b id="lbWatch">your content</b> · <span id="lbLast">connecting…</span></span>
|
|
2150
|
+
<span class="chev">›</span>
|
|
2151
|
+
</div>
|
|
2152
|
+
<div class="lb-detail">
|
|
2153
|
+
<div class="lb-row"><span class="ic">✓</span><span class="k">Local server</span><span class="v dim" id="lbServer">—</span></div>
|
|
2154
|
+
<div class="lb-row"><span class="ic">↗</span><span class="k">Current URL</span><span class="v dim" id="lbUrl">—</span></div>
|
|
2155
|
+
<div class="lb-row"><span class="ic">✎</span><span class="k">Watched file</span><span class="v dim" id="lbFile">content/*.html</span></div>
|
|
2156
|
+
<div class="lb-row"><span class="ic">◷</span><span class="k">Last save seen</span><span class="v dim" id="lbSave" data-ts="">no saves yet</span></div>
|
|
2157
|
+
<div class="lb-row"><span class="ic">⟳</span><span class="k">Last reload sent</span><span class="v dim" id="lbReload" data-ts="">waiting for a save</span></div>
|
|
2158
|
+
<div class="lb-row"><span class="ic">≡</span><span class="k">Source</span><span class="v dim">your working tree — not published</span></div>
|
|
2159
|
+
</div>
|
|
2160
|
+
</div>
|
|
2161
|
+
)}
|
|
2162
|
+
|
|
2163
|
+
{/* Static "coming soon" cards — pure /dev rendering with no dependency on the
|
|
2164
|
+
isLocalDevLoop-only /__tot/* endpoints and no need for the activity bridge, so
|
|
2165
|
+
it renders unconditionally on the deployed hosted panel (and locally). Not
|
|
2166
|
+
gated on capability either — it's a teaser of what's next, and AI is one card
|
|
2167
|
+
among several, not a prerequisite. */}
|
|
2168
|
+
<section class="next">
|
|
2169
|
+
<h3>The rest of the workflow</h3>
|
|
2170
|
+
<p class="sub">Previews, publish checks, AI assistance, and diagnostics. These open up once you've got the loop — coming soon.</p>
|
|
2171
|
+
<div class="nextgrid">
|
|
2172
|
+
<div class="nextcard"><span class="path">Share a preview</span><span class="d">Put a branch or PR up at a shareable URL.</span><span class="soon">Coming soon</span></div>
|
|
2173
|
+
<div class="nextcard"><span class="path">Publish readiness</span><span class="d">What happens if you publish today — checks & blockers.</span><span class="soon">Coming soon</span></div>
|
|
2174
|
+
<div class="nextcard"><span class="path">Edit with AI</span><span class="d">Connect Claude, Cursor, or Codex to your store.</span><span class="soon">Coming soon</span></div>
|
|
2175
|
+
<div class="nextcard"><span class="path">Troubleshooting</span><span class="d">Doctor, logs, server health, and auth checks.</span><span class="soon">Coming soon</span></div>
|
|
2176
|
+
</div>
|
|
2177
|
+
</section>
|
|
2178
|
+
|
|
2179
|
+
<footer class="foot">
|
|
2180
|
+
<span>{session?.email ? <>Signed in as {session.email}</> : <>Not signed in</>}</span>
|
|
2181
|
+
<span class="sp"></span>
|
|
2182
|
+
{signedIn ? <a href="/auth/logout">Sign out</a> : null}
|
|
2183
|
+
</footer>
|
|
2184
|
+
</div>
|
|
2185
|
+
|
|
2186
|
+
<div class="toast" id="toast"><span class="ok">✓</span><span id="toastMsg">Copied</span></div>
|
|
2187
|
+
|
|
2188
|
+
{/* Item 7/10 — Step 3's "Submit for preview" CTA opens this "coming soon" dialog;
|
|
2189
|
+
the compliance-reviewed preview pipeline isn't wired into this panel yet. */}
|
|
2190
|
+
{hostedActivity && (
|
|
2191
|
+
<dialog class="cs-dialog" id="comingSoon" aria-label="Submit for preview — coming soon">
|
|
2192
|
+
<div class="cs-body">
|
|
2193
|
+
<span class="cs-emoji" aria-hidden="true">🚧</span>
|
|
2194
|
+
<h2>Preview submissions are coming soon</h2>
|
|
2195
|
+
<p>Submitting a change for a compliance-reviewed preview build isn't wired into this panel yet. For now, keep iterating locally — your saves land in <strong>Your activity</strong> above in real time.</p>
|
|
2196
|
+
<button type="button" class="cta" id="comingSoonClose">Got it</button>
|
|
2197
|
+
</div>
|
|
2198
|
+
</dialog>
|
|
2199
|
+
)}
|
|
2200
|
+
|
|
2201
|
+
{isLocalDevLoop && (
|
|
2202
|
+
<dialog class="pe-dialog" id="previewDialog" aria-label={`${appDomain} — full-screen preview`}>
|
|
2203
|
+
<div class="pe-bar">
|
|
2204
|
+
<span class="pe-url">{isLocalDevLoop ? "localhost preview" : "your store"} · {appDomain}</span>
|
|
2205
|
+
<button type="button" class="pe-expand" id="previewClose">✕ Close</button>
|
|
2206
|
+
</div>
|
|
2207
|
+
<iframe class="ped-frame" id="previewDialogFrame" title={`${appDomain} — full-screen preview`}></iframe>
|
|
2208
|
+
</dialog>
|
|
2209
|
+
)}
|
|
2210
|
+
|
|
2211
|
+
{isLocalDevLoop && (
|
|
2212
|
+
<button class="ask-fab" id="askFab" type="button">
|
|
2213
|
+
<svg viewBox="0 0 24 24" fill="none"><path d="M4 5.5A1.5 1.5 0 0 1 5.5 4h13A1.5 1.5 0 0 1 20 5.5v9a1.5 1.5 0 0 1-1.5 1.5H9l-4 3.5v-3.5H5.5A1.5 1.5 0 0 1 4 14.5v-9Z" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/></svg>
|
|
2214
|
+
<span class="u">Stuck?</span> Ask
|
|
2215
|
+
</button>
|
|
2216
|
+
)}
|
|
2217
|
+
{isLocalDevLoop && (
|
|
2218
|
+
<div class="ask-panel" id="askPanel" role="dialog" aria-label="Help with the local loop">
|
|
2219
|
+
<div class="ask-head"><strong>Ask about the loop</strong><span>Quick answers for "why isn't this working."</span></div>
|
|
2220
|
+
<div class="qa">
|
|
2221
|
+
<details>
|
|
2222
|
+
<summary><span class="q">?</span>Is my local site running?<span class="chev">›</span></summary>
|
|
2223
|
+
<div class="ans"><span id="ans-running">Checking…</span></div>
|
|
2224
|
+
</details>
|
|
2225
|
+
<details>
|
|
2226
|
+
<summary><span class="q">?</span>Why didn't my change show up?<span class="chev">›</span></summary>
|
|
2227
|
+
<div class="ans"><span id="ans-change">Save a watched file and I'll compare when it was seen vs when your store reloaded.</span></div>
|
|
2228
|
+
</details>
|
|
2229
|
+
<details>
|
|
2230
|
+
<summary><span class="q">?</span>Did the watcher see my save?<span class="chev">›</span></summary>
|
|
2231
|
+
<div class="ans"><span id="ans-watcher">The Monitor's <b>Last save seen</b> updates the instant a watched file changes. If it doesn't move after you save, the watcher missed it — usually after a rename or delete. Restart to re-scan.</span></div>
|
|
2232
|
+
</details>
|
|
2233
|
+
<details>
|
|
2234
|
+
<summary><span class="q">?</span>Am I editing the rendered source?<span class="chev">›</span></summary>
|
|
2235
|
+
<div class="ans">Yes — you're editing your <b>working tree</b>, the same file the page renders from. Nothing publishes; only your local preview sees it.</div>
|
|
2236
|
+
</details>
|
|
2237
|
+
<details>
|
|
2238
|
+
<summary><span class="q">↻</span>Restart the local preview.<span class="chev">›</span></summary>
|
|
2239
|
+
<div class="ans">Reconnects this page to the dev server and re-reads loop state — fixes a preview that's gone stale. If the server is fully down, run <code>tot start</code> in your terminal.
|
|
2240
|
+
<div style="margin-top:.6rem"><button class="btn primary" id="restartBtn" type="button" style="background:var(--accent);color:#fff;border-color:transparent">Reconnect preview</button></div>
|
|
2241
|
+
</div>
|
|
2242
|
+
</details>
|
|
2243
|
+
</div>
|
|
2244
|
+
<div class="ask-foot">Scoped to the local loop.</div>
|
|
2245
|
+
</div>
|
|
2246
|
+
)}
|
|
2247
|
+
|
|
2248
|
+
<script nonce={cspNonce} is:inline>
|
|
2249
|
+
// Theme toggle: flip data-theme on <html>, seeded from the OS preference on first click.
|
|
2250
|
+
(function () {
|
|
2251
|
+
var root = document.documentElement;
|
|
2252
|
+
var btn = document.getElementById("themeBtn");
|
|
2253
|
+
if (!btn) return;
|
|
2254
|
+
btn.addEventListener("click", function () {
|
|
2255
|
+
var cur = root.getAttribute("data-theme");
|
|
2256
|
+
var next = cur === "dark" ? "light" : cur === "light" ? "dark"
|
|
2257
|
+
: (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches ? "light" : "dark");
|
|
2258
|
+
root.setAttribute("data-theme", next);
|
|
2259
|
+
});
|
|
2260
|
+
})();
|
|
2261
|
+
</script>
|
|
2262
|
+
|
|
2263
|
+
<script nonce={cspNonce} is:inline>
|
|
2264
|
+
// Info popover: toggle on the ⓘ button, dismiss on outside click.
|
|
2265
|
+
(function () {
|
|
2266
|
+
var info1 = document.getElementById("info1");
|
|
2267
|
+
if (!info1) return;
|
|
2268
|
+
var openBtn = info1.querySelector(".info-btn");
|
|
2269
|
+
openBtn.addEventListener("click", function (e) { e.stopPropagation(); info1.classList.toggle("open"); });
|
|
2270
|
+
document.addEventListener("click", function (e) { if (!info1.contains(e.target)) info1.classList.remove("open"); });
|
|
2271
|
+
})();
|
|
2272
|
+
</script>
|
|
2273
|
+
|
|
2274
|
+
<script nonce={cspNonce} is:inline>
|
|
2275
|
+
// Copy the one command. Two behaviours:
|
|
2276
|
+
// · fastpath (signed-in dev): mint a single-use `tot login --code … && tot start` from
|
|
2277
|
+
// /api/dev/cli-signin-code, reveal it (textContent, never innerHTML — it carries a
|
|
2278
|
+
// live code), and copy the whole thing.
|
|
2279
|
+
// · otherwise: copy the generic install-and-run command directly.
|
|
2280
|
+
(function () {
|
|
2281
|
+
var btn = document.getElementById("copyCmd");
|
|
2282
|
+
var textEl = document.getElementById("cli-cmd-text");
|
|
2283
|
+
var note = document.getElementById("cli-cmd-note");
|
|
2284
|
+
var toast = document.getElementById("toast");
|
|
2285
|
+
var toastMsg = document.getElementById("toastMsg");
|
|
2286
|
+
if (!btn || !textEl) return;
|
|
2287
|
+
var generic = btn.getAttribute("data-generic") || "";
|
|
2288
|
+
var fastpath = btn.getAttribute("data-fastpath") === "1";
|
|
2289
|
+
var tT;
|
|
2290
|
+
function ping(m) {
|
|
2291
|
+
if (!toast) return;
|
|
2292
|
+
toastMsg.textContent = m; toast.classList.add("show");
|
|
2293
|
+
clearTimeout(tT); tT = setTimeout(function () { toast.classList.remove("show"); }, 1900);
|
|
2294
|
+
}
|
|
2295
|
+
function copy(text) {
|
|
2296
|
+
if (navigator.clipboard) return navigator.clipboard.writeText(text).then(function () { return true; }).catch(function () { return false; });
|
|
2297
|
+
return Promise.resolve(false);
|
|
2298
|
+
}
|
|
2299
|
+
function label(s) { btn.childNodes[btn.childNodes.length - 1].textContent = s; }
|
|
2300
|
+
|
|
2301
|
+
if (!fastpath) {
|
|
2302
|
+
btn.addEventListener("click", function () {
|
|
2303
|
+
copy(generic).then(function (ok) {
|
|
2304
|
+
label(ok ? " Copied — paste & press Enter" : " Copy the command");
|
|
2305
|
+
ping(ok ? "Copied the command" : "Select the command to copy");
|
|
2306
|
+
setTimeout(function () { label(" Copy the command"); }, 2600);
|
|
2307
|
+
});
|
|
2308
|
+
});
|
|
2309
|
+
return;
|
|
2310
|
+
}
|
|
2311
|
+
|
|
2312
|
+
var busy = false;
|
|
2313
|
+
btn.addEventListener("click", async function () {
|
|
2314
|
+
if (busy) return;
|
|
2315
|
+
busy = true;
|
|
2316
|
+
label(" …"); btn.disabled = true;
|
|
2317
|
+
var res, body;
|
|
2318
|
+
try {
|
|
2319
|
+
res = await fetch("/api/dev/cli-signin-code", {
|
|
2320
|
+
method: "POST",
|
|
2321
|
+
headers: { "content-type": "application/json" },
|
|
2322
|
+
body: "{}",
|
|
2323
|
+
});
|
|
2324
|
+
body = await res.json().catch(function () { return {}; });
|
|
2325
|
+
} catch (err) {
|
|
2326
|
+
if (note) note.textContent = "Couldn’t reach the sign-in service — try again in a moment.";
|
|
2327
|
+
label(" Copy the command"); btn.disabled = false; busy = false;
|
|
2328
|
+
return;
|
|
2329
|
+
}
|
|
2330
|
+
if (!res.ok || !body.command) {
|
|
2331
|
+
var byStatus = {
|
|
2332
|
+
401: "Your session expired — reload the page and sign in again.",
|
|
2333
|
+
403: "This account doesn’t have developer access to a store yet.",
|
|
2334
|
+
502: "The sign-in service is having a moment — try again.",
|
|
2335
|
+
503: "Terminal sign-in isn’t configured on this environment.",
|
|
2336
|
+
};
|
|
2337
|
+
if (note) note.textContent = byStatus[res.status] || "Couldn’t mint a sign-in command — try again.";
|
|
2338
|
+
label(" Copy the command"); btn.disabled = false; busy = false;
|
|
2339
|
+
return;
|
|
2340
|
+
}
|
|
2341
|
+
// Reveal the real command (contains the one-time code) as a pure text node, then copy it.
|
|
2342
|
+
textEl.textContent = body.command;
|
|
2343
|
+
var ok = await copy(body.command);
|
|
2344
|
+
label(ok ? " Copied ✓" : " Select & copy");
|
|
2345
|
+
ping(ok ? "Copied — paste & press Enter" : "Select the command above to copy");
|
|
2346
|
+
if (note) {
|
|
2347
|
+
note.textContent = ok
|
|
2348
|
+
? "Copied. Paste into your terminal and press Enter — it signs you in, runs your store, and opens your browser. Single-use, short-lived code."
|
|
2349
|
+
: "Select the command above and copy it, then paste into your terminal and press Enter. Single-use, short-lived code.";
|
|
2350
|
+
}
|
|
2351
|
+
btn.disabled = false;
|
|
2352
|
+
setTimeout(function () { label(" Copy the command"); }, 4000);
|
|
2353
|
+
busy = false;
|
|
2354
|
+
});
|
|
2355
|
+
})();
|
|
2356
|
+
</script>
|
|
2357
|
+
|
|
2358
|
+
{hostedActivity && <script nonce={cspNonce} is:inline set:html={hostedActivityScript} />}
|
|
2359
|
+
{isLocalDevLoop && <script nonce={cspNonce} is:inline set:html={stepperScript} />}
|
|
2360
|
+
{isLocalDevLoop && <script nonce={cspNonce} is:inline set:html={askScript} />}
|
|
2361
|
+
{isLocalDevLoop && <script nonce={cspNonce} is:inline set:html={monitorScript} />}
|
|
2362
|
+
{isLocalDevLoop && <script nonce={cspNonce} is:inline set:html={filesScript} />}
|
|
2363
|
+
{isLocalDevLoop && <script nonce={cspNonce} is:inline set:html={c8Script} />}
|
|
2364
|
+
</body>
|
|
2365
|
+
</html>
|