feat(web): put the one-line install in front of everyone

The CLI is the product, and the way you get it was nowhere on the site. You
had to already know the URL of a script served out of public/.

Two placements, one command:

- The homepage hero gets the loud version, directly under the lede and above
  the fold -- a bordered dark panel, the command at full size, Copy alongside.
- Every page carries a compact version in the rail, between the brand and the
  nav. Present on arrival, never competing with navigation.

Both come from renderInstallCommand() in one module. The homepage builds its
HTML as a string and the rest of the site is JSX, which is precisely the shape
that lets one copy of a command drift while the other stays right -- so there
is one definition and SiteShell renders it rather than restating it.

The command keeps its flags: `curl -fsSL`. Without -f, curl prints an HTTP
error body and still exits 0, so a 404 gets piped into sh; without -L the
install breaks the first time the URL redirects. This is the form install.sh
already documents in its own header.

Copy is one delegated listener on document for any [data-copy] button, mounted
site-wide in the layout. Delegation because the two placements arrive by
different rendering paths and a document listener does not care which; it also
means the next copy button needs the attribute and no wiring. It falls back to
a throwaway textarea + execCommand outside a secure context, where
navigator.clipboard is simply undefined, so the button never no-ops silently.

The contract tests pin the command, both placements, and that the clipboard
payload equals the visible text -- a Copy button that hands over something
other than what is on screen is worse than no button. They also read
public/install.sh and assert it is #!/bin/sh and documents this exact command,
so `| sh` cannot quietly become a lie.

apps/logicsrc-web: 13 new tests pass, 46 total. The ontology-api contract file
fails to resolve @logicsrc/validators, which it also does on a pristine
origin/master -- unbuilt workspace package, unrelated to this change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Anthony Ettinger 2026-07-30 19:54:57 +00:00
parent d28f20b9f4
commit f41abbd712
7 changed files with 349 additions and 0 deletions

View file

@ -0,0 +1,71 @@
"use client";
import { useEffect } from "react";
// One delegated listener for every `[data-copy]` button on the page.
//
// Delegation rather than a handler per button because the markup these target
// is server-rendered in two different ways -- the homepage arrives as an HTML
// string through dangerouslySetInnerHTML, the rest as JSX -- and a listener on
// `document` does not care which. It also means a new copy button anywhere on
// the site needs no wiring, just the attribute.
export function CopyButtons(): null {
useEffect(() => {
const flash = (button: HTMLButtonElement, message: string): void => {
const original = button.dataset.copyLabel ?? button.textContent ?? "Copy";
button.dataset.copyLabel = original;
button.textContent = message;
button.classList.add("is-copied");
window.setTimeout(() => {
button.textContent = original;
button.classList.remove("is-copied");
}, 1600);
};
// navigator.clipboard is undefined outside a secure context, which includes
// plain-http previews and older Safari. Falling back to a throwaway
// textarea keeps the button honest there instead of silently doing nothing.
const legacyCopy = (text: string): boolean => {
const field = document.createElement("textarea");
field.value = text;
field.setAttribute("readonly", "");
field.style.position = "fixed";
field.style.opacity = "0";
document.body.appendChild(field);
field.select();
let copied = false;
try {
copied = document.execCommand("copy");
} catch {
copied = false;
}
field.remove();
return copied;
};
const onClick = async (event: MouseEvent): Promise<void> => {
const target = event.target as HTMLElement | null;
const button = target?.closest<HTMLButtonElement>("button[data-copy]");
if (!button) return;
const text = button.dataset.copy ?? "";
if (!text) return;
try {
if (navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(text);
flash(button, "Copied");
return;
}
} catch {
// permission denied or a non-secure context -- fall through
}
flash(button, legacyCopy(text) ? "Copied" : "Press Ctrl+C");
};
document.addEventListener("click", onClick);
return () => document.removeEventListener("click", onClick);
}, []);
return null;
}

View file

@ -1,4 +1,5 @@
import type { ReactNode } from "react";
import { renderInstallCommand } from "@/lib/install-command";
// Mirrors the rail/nav from page-markup.ts so standalone routes (e.g. /blog)
// share the site chrome. Anchor links point at the homepage sections.
@ -40,6 +41,9 @@ export function SiteShell({
<small>Open coordination standards</small>
</div>
</a>
{/* Same markup the homepage uses, so the two can never drift apart.
Static content from a module constant -- nothing user-supplied. */}
<div dangerouslySetInnerHTML={{ __html: renderInstallCommand("rail") }} />
<nav aria-label="LogicSRC sections">
{NAV.map((item) => (
<a