diff --git a/src/landing.js b/src/landing.js index cad7804..cddc095 100644 --- a/src/landing.js +++ b/src/landing.js @@ -23,6 +23,7 @@ const LANDING_HTML = ` + ClawFix 0.11.2 โ€” OpenClaw Diagnostics & Repair @@ -49,12 +50,18 @@ const LANDING_HTML = ` --blue: #3b82f6; } + html { + color-scheme: dark; + scroll-behavior: smooth; + } + body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: var(--bg); color: var(--text); line-height: 1.6; min-height: 100vh; + overflow-x: hidden; } .container { @@ -63,6 +70,26 @@ const LANDING_HTML = ` padding: 0 24px; } + .skip-link { + position: fixed; + top: 12px; + left: 12px; + z-index: 100; + padding: 10px 14px; + border-radius: 8px; + background: var(--text); + color: var(--bg); + font-weight: 700; + text-decoration: none; + transform: translateY(-160%); + transition: transform 0.2s; + } + .skip-link:focus-visible { transform: translateY(0); } + :focus-visible { + outline: 3px solid var(--blue); + outline-offset: 3px; + } + /* Header */ header { padding: 20px 0; @@ -79,8 +106,22 @@ const LANDING_HTML = ` text-decoration: none; color: var(--text); } + .logo, + .nav-links a, + .nav-toggle { + min-height: 44px; + touch-action: manipulation; + -webkit-tap-highlight-color: rgba(239, 68, 68, 0.28); + } + .logo { display: inline-flex; align-items: center; } .logo span { color: var(--accent); } + .nav-links { + display: flex; + align-items: center; + } .nav-links a { + display: inline-flex; + align-items: center; color: var(--muted); text-decoration: none; font-size: 0.9rem; @@ -88,6 +129,24 @@ const LANDING_HTML = ` transition: color 0.2s; } .nav-links a:hover { color: var(--text); } + .nav-toggle { + display: none; + align-items: center; + justify-content: center; + border: 1px solid var(--border); + border-radius: 8px; + background: var(--surface); + color: var(--text); + padding: 8px 14px; + font: inherit; + font-weight: 700; + cursor: pointer; + } + #main-content:focus { + outline: 3px solid var(--blue); + outline-offset: -3px; + } + section[id] { scroll-margin-top: 24px; } /* Hero */ .hero { @@ -106,6 +165,7 @@ const LANDING_HTML = ` line-height: 1.1; margin-bottom: 16px; } + h1, h2, h3 { text-wrap: balance; } h1 .highlight { background: linear-gradient(135deg, var(--accent), #f97316); -webkit-background-clip: text; @@ -131,7 +191,6 @@ const LANDING_HTML = ` grid-template-columns: 20px minmax(0, 1fr) auto; align-items: center; gap: 12px; - cursor: pointer; transition: border-color 0.2s, box-shadow 0.2s; position: relative; text-align: left; @@ -159,11 +218,12 @@ const LANDING_HTML = ` border: none; color: var(--muted); padding: 8px 14px; - min-height: 40px; + min-height: 44px; border-radius: 6px; cursor: pointer; font-size: 0.85rem; - transition: all 0.2s; + touch-action: manipulation; + transition: background-color 0.2s, color 0.2s; } .copy-btn:hover { background: var(--accent); @@ -220,6 +280,87 @@ const LANDING_HTML = ` .command-hint a { color: var(--muted); } .command-hint a:hover { color: var(--text); } + .install-panel { + max-width: 860px; + margin: 0 auto; + padding: 24px; + border: 1px solid var(--border); + border-radius: 16px; + background: linear-gradient(180deg, rgba(255,255,255,0.035), rgba(20,20,20,0.76)); + text-align: left; + } + .install-heading { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 20px; + margin-bottom: 18px; + } + .install-heading h2 { font-size: 1.25rem; line-height: 1.3; } + .install-heading p { color: var(--muted); font-size: 0.9rem; max-width: 520px; } + .install-steps { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 12px; + } + .install-step { + min-width: 0; + padding: 14px; + border: 1px solid var(--border); + border-radius: 10px; + background: rgba(10,10,10,0.55); + } + .install-step-label { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 8px; + color: var(--text); + font-size: 0.82rem; + font-weight: 700; + } + .install-step-label span { + display: inline-flex; + width: 24px; + height: 24px; + align-items: center; + justify-content: center; + border-radius: 999px; + background: var(--accent-glow); + color: var(--accent); + } + .install-step .command-box, + .quick-command .command-box { + grid-template-columns: minmax(0, 1fr) auto; + padding: 10px; + margin: 0; + } + .install-step .command-box code, + .quick-command .command-box code { font-size: 0.78rem; } + .install-step .command-box code { white-space: normal; overflow-wrap: anywhere; } + .install-step .command-box code span { white-space: nowrap; } + .install-step .copy-btn { min-width: 44px; padding-inline: 8px; } + .install-step-note { margin-top: 8px; color: var(--muted); font-size: 0.75rem; } + .install-step-note a { color: var(--text); } + .quick-command { margin-top: 12px; } + .quick-command h3 { margin-bottom: 6px; font-size: 0.9rem; } + .quick-command > p { margin-bottom: 8px; color: var(--muted); font-size: 0.8rem; } + .privacy-summary { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 10px; + margin-top: 16px; + } + .privacy-summary p { + padding: 10px 12px; + border-left: 2px solid var(--green); + background: rgba(34,197,94,0.055); + color: var(--muted); + font-size: 0.78rem; + } + .privacy-summary strong { color: var(--text); } + .privacy-summary a { color: var(--text); } + .proof-row { display: flex; justify-content: center; @@ -449,15 +590,39 @@ const LANDING_HTML = ` margin-bottom: 12px; } - @media (max-width: 640px) { + @media (max-width: 720px) { h1 { font-size: 2rem; } .hero { padding: 40px 0 36px; } - .nav-optional { display: none; } - .nav-links a { margin-left: 16px; } + header .container { flex-wrap: wrap; } + .nav-toggle { display: inline-flex; } + .nav-links { + display: none; + width: 100%; + grid-template-columns: 1fr 1fr; + gap: 6px; + padding-top: 12px; + } + .nav-links.is-open { display: grid; } + .nav-links a { + justify-content: center; + margin-left: 0; + padding: 8px 10px; + border: 1px solid var(--border); + border-radius: 8px; + background: var(--surface); + } .status-row { gap: 8px; margin-bottom: 24px; } .status-pill { width: 100%; justify-content: center; } - .command-box { grid-template-columns: 14px minmax(0, 1fr) auto; gap: 8px; padding: 14px; } - .command-box code { font-size: 0.78rem; white-space: normal; overflow-wrap: normal; } + .install-panel { padding: 18px; } + .install-heading { display: block; } + .install-heading p { margin-top: 6px; } + .install-steps, + .privacy-summary { grid-template-columns: 1fr; } + .install-step .command-box, + .quick-command .command-box { grid-template-columns: minmax(0, 1fr) auto; } + .command-box { gap: 8px; } + .install-step .command-box code, + .quick-command .command-box code { font-size: 14px; white-space: normal; overflow-wrap: anywhere; } .command-box code span { white-space: nowrap; } .proof-row { align-items: stretch; flex-direction: column; } .proof-pill { border-radius: 8px; } @@ -470,30 +635,40 @@ const LANDING_HTML = ` .issues-grid { grid-template-columns: 1fr; } .sent-grid { grid-template-columns: 1fr !important; } } + + @media (prefers-reduced-motion: reduce) { + html { scroll-behavior: auto; } + *, *::before, *::after { + scroll-behavior: auto !important; + transition-duration: 0.01ms !important; + } + } +
-
-
+
-

Fix your OpenClaw
in one command

+

Diagnose OpenClaw in one command.
Review supported repairs.

- Deterministic diagnostics and guarded repairs. Optional AI analysis is used only when configured. Model output never becomes executable shell. + Run deterministic checks first. Optional AI can explain unmatched problems, while executable changes stay limited to reviewed repair definitions.

@@ -502,26 +677,65 @@ const LANDING_HTML = ` v0.11.2 ยท installer + agent v2 - New on main - Chat-first OpenTUI merged + Next release + Chat-first TUI ยท not in v0.11.2
-
- $ - curl -fsSL https://clawfix.dev/install -o install.sh - -
-

- Recommended Verify with shasum -a 256 install.sh against the published hash, then run bash install.sh. No global npm. -

-

- Prefer npm? npx clawfix@0.11.2 ยท Dry run: npx clawfix@0.11.2 --dry-run ยท macOS, Linux, WSL ยท Node.js 22+ -

-
- โœ“ GitHub OIDC publish - โœ“ npm attestation verified - โœ“ 21-file allowlisted package +
+
+
+

Recommended: verify, then install

+

Three explicit steps install under ~/.clawfix. No global npm, no remote shell pipe.

+
+

macOS, Linux, WSL ยท Node.js 22+

+
+ +
+
+

1 Download

+
+ curl -fsSL https://clawfix.dev/install -o install.sh + +
+
+
+

2 Verify SHA-256

+
+ shasum -a 256 install.sh + +
+

Compare with the published hash. On Linux, use sha256sum.

+
+
+

3 Install

+
+ bash install.sh + +
+
+
+ +
+

Quick one-command alternative

+

Run the portable npm CLI without the chat-first TUI. Use --dry-run to inspect collected data without sending it.

+
+ npx clawfix@0.11.2 + +
+
+ +
+

Local first. Deterministic checks and local commands do not upload.

+

You choose. Remote analysis asks [y/N] before upload unless you explicitly override it.

+

Inspect first. Recognized secrets are redacted; workspace document contents stay local. See exactly what may be sent.

+
+ +
+ โœ“ GitHub OIDC publish + โœ“ npm attestation verified + โœ“ 21-file allowlisted package +
@@ -559,9 +773,9 @@ const LANDING_HTML = `
-
Latest main update
-

Chat-first TUI is the default session of the standalone binary.

-

The standalone binary opens directly into a conversation, keeps local commands offline, and asks before remote AI or guarded repairs. The npm package ships the portable CLI only โ€” npx clawfix runs the plain readline session.

+
Next release ยท not in v0.11.2
+

Chat-first TUI ships after v0.11.2.

+

The next standalone binary will open directly into a conversation, keep local commands offline, and ask before remote AI or guarded repairs. The npm package ships the portable CLI only โ€” npx clawfix runs the plain readline session.

  • โœ“Scrollable transcript, findings sidebar, and responsive terminal layouts.
  • โœ“Approval defaults to Cancel and reports success only after verified repairs.
  • @@ -868,14 +1082,56 @@ gateway-restart repair. Want me to? `; diff --git a/test/tooling-regressions.test.js b/test/tooling-regressions.test.js index 4ff8af3..bcd8712 100644 --- a/test/tooling-regressions.test.js +++ b/test/tooling-regressions.test.js @@ -1,6 +1,7 @@ import assert from 'node:assert/strict'; import { access, readFile, stat } from 'node:fs/promises'; import test from 'node:test'; +import vm from 'node:vm'; const read = path => readFile(new URL(`../${path}`, import.meta.url), 'utf8'); const json = async path => JSON.parse(await read(path)); @@ -77,7 +78,9 @@ test('landing page presents truthful evidence for the published 0.11.2 twenty-on assert.doesNotMatch(landing, /7-file allowlisted package/); assert.doesNotMatch(landing, /18-file allowlisted package/); assert.match(landing, /Evidence before repair/); - assert.match(landing, /Chat-first TUI is the default session of the standalone binary/); + assert.match(landing, /Diagnose OpenClaw in one command/); + assert.match(landing, /Review supported repairs/); + assert.match(landing, /Chat-first TUI ships after v0\.11\.2/); // The npm package ships the portable CLI only โ€” the site must not imply npx gives the TUI. assert.match(landing, /The npm package ships the portable CLI only/); assert.match(landing, /standalone binary on the GitHub release/); @@ -100,6 +103,149 @@ test('landing page presents truthful evidence for the published 0.11.2 twenty-on assert.doesNotMatch(landing, /npx clawfix<\/code>/); }); +test('landing install flow is safe, truthful, accessible, and mobile navigable', async () => { + const landing = await read('src/landing.js'); + + assert.match(landing, /Recommended: verify, then install/); + assert.match(landing, /id="cmd-install"/); + assert.match(landing, /id="cmd-verify"/); + assert.match(landing, /id="cmd-run"/); + assert.match(landing, /Quick one-command alternative/); + assert.match(landing, /not in v0\.11\.2/); + assert.match(landing, /class="privacy-summary"/); + assert.match(landing, /See exactly what may be sent/); + assert.doesNotMatch(landing, /Fix your OpenClaw
    in/); + + assert.match(landing, /class="skip-link" href="#main-content"/); + assert.match(landing, /
    /); + assert.match(landing, /class="nav-toggle" aria-expanded="false" aria-controls="site-nav"/); + assert.match(landing, /id="site-nav" aria-label="Primary navigation"/); + assert.match(landing, /@media \(prefers-reduced-motion: reduce\)/); + assert.match(landing, /color-scheme: dark/); + assert.match(landing, /meta name="theme-color"/); + assert.match(landing, /min-height: 44px/); + assert.match(landing, /data-copy-command="install"/); + assert.match(landing, /addEventListener\('click'/); + assert.doesNotMatch(landing, /onclick=/); + assert.match(landing, /\.install-step \.command-box code \{ white-space: normal; overflow-wrap: anywhere; \}/); + assert.match(landing, /\.install-step \.copy-btn \{ min-width: 44px; padding-inline: 8px; \}/); + assert.doesNotMatch(landing, /transition:\s*all/); + assert.doesNotMatch(landing, /\.nav-optional \{ display: none; \}/); +}); + +test('landing skip link focuses main with a visible indicator', async () => { + const landing = await read('src/landing.js'); + const focusRule = landing.match(/#main-content:focus\s*\{([^}]+)\}/)?.[1]; + assert.ok(focusRule, 'main content needs an explicit focus rule'); + assert.match(focusRule, /outline:\s*3px solid var\(--blue\)/); + assert.match(focusRule, /outline-offset:\s*-3px/); + assert.doesNotMatch(focusRule, /outline:\s*none/); + + const listeners = new Map(); + const main = { + focused: false, + focus() { + this.focused = true; + }, + }; + const skipLink = { + addEventListener(type, listener) { + listeners.set(`skip:${type}`, listener); + }, + }; + const navToggle = { + addEventListener() {}, + }; + const siteNav = { + classList: { toggle() {}, remove() {} }, + }; + const script = landing.match(/