Skip to content

Commit c9f9faa

Browse files
angusbezzinaclaude
andcommitted
design(web): §01 pastes an install that builds; the leads stop saying what follows them
The dependency snippet was half an install: the copy button handed over a .package line that does not build without the target's .product line, which is the one people forget. It carries both halves now, with the call broken across its arguments so a phone reads eight of eleven lines whole instead of one line that stops at "gpu-cli". The URL cannot be made to fit a 320 px column in any formatting, so the plate keeps its scroll area for that line and copy.ts says why, with the numbers. The prose around it was arguing with itself. The lead promised one taxonomy (AppKit, SwiftUI, iOS), the tail promised another (AppKit, SwiftUI, UIKit), and the note promised a third; the lead now names none, which settles it without picking a side. The paragraph listing what else the README covers is gone, because a table of contents is not copy and it pointed at the README a third time in four lines. Its slot carries the Xcode route, displaced from a 64-character caption that wrapped at every width. The note opens on what the snippet does not say and then states the working-directory gotcha, which is the thing that costs a reader a morning and the reason the README link has something to earn. Elsewhere the same fault, treated the same way: the hero standfirst loses the field list §03 shows verbatim and lands its payoff at word 39 rather than 57; §02's lead stops reciting the three terms printed underneath it; §05 stops making one promise three times, so the lead owns what arrives, the privacy line owns whose list it is, and the helper stays a field hint. The hero's primary now sends the reader one section down rather than off the site, and the repo-visibility branch moves onto the README link with it: an anchor into this same document has nothing to be unavailable about. The §01 grid override goes with the snippet that justified it. It floored the narrow track at 29 rem for a ClipboardSink line that has not been in §01 for two commits, and the floor was quietly stealing 27 px from the plate at 1600. Verified: build, 124 tests, responsive-check at 320/375/414/768/1280. Column and caption widths measured against dist at seven widths; the hero anchor clicked headlessly at 1280 and 375. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 18f71b7 commit c9f9faa

8 files changed

Lines changed: 85 additions & 51 deletions

File tree

.beads/interactions.jsonl

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,14 @@
2323
{"id":"int-5fc8bfc7c0dfa68822c658816e6b8e94","kind":"field_change","created_at":"2026-08-20T17:09:16.762437Z","actor":"Angus Bezzina","issue_id":"annotkit-dmu","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Bare Lucide Sun/Moon at strokeWidth 1.5, in its own flow row above the masthead (not pinned: the centred dateline reaches the right edge below 48rem and would collide). 44x44 target, aria-label names the destination theme, svg aria-hidden+focusable=false. Gate 30's justification is now weaker and the record says so rather than rewriting the old argument. Fold budget re-verified: CTAs at 709px of 800."}}
2424
{"id":"int-a1b30624ca7c46e6192b68275c02f6e2","kind":"field_change","created_at":"2026-08-20T17:09:17.244552Z","actor":"Angus Bezzina","issue_id":"annotkit-bqh","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Seven hairline frames drifting and cross-fading behind the page: the app's marquee at rest, which is what makes it survive gates 29/45 where a blob field would not. CSS only, aria-hidden, pointer-events none, overflow hidden, transform+opacity only, near-prime 43-97s cycles so no two frames rephase. Reduced motion holds them still at rest opacity rather than removing them. Measured: 7,136px2 of stroke, 0.62% of a viewport before alpha, drawn at 6-9%. Gates 29 and 45 changed from plain 'no' to justifications."}}
2525
{"id":"int-0fe7247da53b0f235927a1f1fc435201","kind":"field_change","created_at":"2026-08-20T17:39:53.415873Z","actor":"Angus Bezzina","issue_id":"annotkit-6y6","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"All six changes landed. Theme toggle moved into the masthead link row (maintainer rejected the under-wordmark row on render). Every icon on Lucide via src/components/Icon.tsx, one stroke weight, two sizes; copy button is Copy/Check/X. Backdrop retuned from ~0.5px/s to 3-7px/s with alpha up to 10-12%. Figure frames on --color-rule-2. Subfoot link hovers to GPU CLI green (verbatim rgb(24,220,106) in dark, same hue re-derived for AA in light). Machine text in prose wrapped in <code> via a backtick convention + src/markup.tsx. Gates: 116 tests, typecheck, 30/30 contrast, responsive 5/5."}}
26+
{"id":"int-1ce89e891ef1fd6042b3dd135791c9b5","kind":"field_change","created_at":"2026-08-21T02:46:17.651388Z","actor":"Angus Bezzina","issue_id":"annotkit-0i4.1","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Closed"}}
27+
{"id":"int-0fa4dfa4944cf5f443082b27f17b5c2a","kind":"field_change","created_at":"2026-08-21T02:46:17.959175Z","actor":"Angus Bezzina","issue_id":"annotkit-0i4.2","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Closed"}}
28+
{"id":"int-e2350190abe978a0e5cf6955056d34f2","kind":"field_change","created_at":"2026-08-21T02:46:18.267322Z","actor":"Angus Bezzina","issue_id":"annotkit-0i4.3","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Closed"}}
29+
{"id":"int-7ea76a13a8b7b702bbd41021087a4da1","kind":"field_change","created_at":"2026-08-21T02:46:18.564292Z","actor":"Angus Bezzina","issue_id":"annotkit-0i4.4","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Closed"}}
30+
{"id":"int-785a5fceca3f8adc29eb1194ee9bb21a","kind":"field_change","created_at":"2026-08-21T02:46:18.870442Z","actor":"Angus Bezzina","issue_id":"annotkit-0i4.6","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Closed"}}
31+
{"id":"int-ee60bb7c650ae7c44b5718b6a742a8e6","kind":"field_change","created_at":"2026-08-21T02:46:19.19445Z","actor":"Angus Bezzina","issue_id":"annotkit-0i4.7","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Closed"}}
32+
{"id":"int-16c2126cb34a681d1cf03862f2185cec","kind":"field_change","created_at":"2026-08-21T02:46:19.535431Z","actor":"Angus Bezzina","issue_id":"annotkit-0i4.8","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Closed"}}
33+
{"id":"int-153dd31ab5d214096170cae4f91816eb","kind":"field_change","created_at":"2026-08-21T02:46:19.786287Z","actor":"Angus Bezzina","issue_id":"annotkit-0i4.9","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Closed"}}
34+
{"id":"int-c66f1419329a991eb86760fed37ef61c","kind":"field_change","created_at":"2026-08-21T02:46:20.05137Z","actor":"Angus Bezzina","issue_id":"annotkit-0i4.10","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Closed"}}
35+
{"id":"int-6f4b0709bef645bef1fa00d3f3dc82d5","kind":"field_change","created_at":"2026-08-21T02:46:27.044421Z","actor":"Angus Bezzina","issue_id":"annotkit-0i4.5","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Measured, not a defect. The backdrop is viewport-anchored, so its frames sit behind running text on every section at every width, by design and at 10-12% accent on a 1px stroke; there is no §01-scoped fix and the finding's coordinates came from an artificial 4000px-tall viewport. See notes."}}
36+
{"id":"int-ab2eaeffeb29c6c884d50256e60012f4","kind":"field_change","created_at":"2026-08-21T02:46:39.863426Z","actor":"Angus Bezzina","issue_id":"annotkit-0i4","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"All ten children resolved: nine implemented in web/, one (0i4.5, backdrop) closed as measured-not-a-defect. Gates green: npm run build, npm test (124), npm run responsive (320-1280 PASS). Not committed — conservative profile."}}

web/src/analytics.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export type AnalyticsEvent =
2323
| { name: "landing_view" }
2424
| { name: "cta_github_clicked"; props: { placement: "masthead" | "hero" | "install" | "footer" } }
2525
| { name: "cta_updates_clicked"; props: { placement: "masthead" | "hero" } }
26+
| { name: "cta_install_clicked"; props: { placement: "hero" } }
2627
| { name: "signup_submitted" }
2728
| { name: "signup_succeeded"; props: { status: "subscribed" | "already_subscribed" } }
2829
| { name: "signup_failed"; props: { kind: SignupFailureKind } };

web/src/copy.ts

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -43,38 +43,68 @@ export const masthead = {
4343
export const hero = {
4444
display: "Point at the view. Hand the agent the map.",
4545
standfirst:
46-
"AnnotKit is a Swift package that mounts a floating toolbar in the dev build of your macOS or iOS app. Click a control, or draw a frame around a whole card, then type a note. It writes an agent-readable annotation: a stable selector, an element path, the element’s role and text, and your words. Your coding agent stops guessing.",
47-
ctaPrimary: "Read the README",
48-
ctaSecondary: "Get release notes",
46+
"AnnotKit mounts a floating toolbar in your macOS or iOS dev build. Click a control, or draw a frame around a card, then type a note. The note carries a selector that locates that control in code. Your coding agent stops guessing.",
47+
ctaPrimary: "Install it",
48+
ctaSecondary: "Read the README",
4949
} as const;
5050

5151
export const install = {
5252
number: "§01",
5353
title: "Install",
54-
lead: "Add the package, mount the toolbar in your dev build. The README walks through AppKit, SwiftUI and iOS.",
54+
lead: "Add the package, then mount the toolbar once your dev build has a window.",
55+
/**
56+
* Both halves of the dependency, not just the first. The `.package` line
57+
* alone is what people copy and it does not build on its own: the product
58+
* has to reach the app target too, and that is the line that gets forgotten.
59+
*
60+
* The call is broken across its arguments to cut what the plate has to
61+
* scroll on a phone: 121 px at 375 rather than 214, and eight of its eleven
62+
* lines read whole there, where the old single line read as far as
63+
* `gpu-cli` and stopped. The URL cannot be made to fit at a phone width in
64+
* any formatting: the quoted string alone measures 289 px at --text-sm and
65+
* the 320 px column leaves 246 for it. So the plate keeps its scroll area
66+
* for that line, which is what the scroll area is for.
67+
*/
5568
package: {
56-
caption: "Add the dependency, or File ▸ Add Package Dependencies… in Xcode",
69+
caption: "Package.swift",
5770
language: "swift",
58-
code: `.package(url: "https://github.com/gpu-cli/annotkit", from: "0.8.0")`,
71+
code: `dependencies: [
72+
.package(
73+
url: "https://github.com/gpu-cli/annotkit",
74+
from: "0.8.0"
75+
)
76+
],
77+
targets: [
78+
.executableTarget(name: "MyApp", dependencies: [
79+
.product(name: "AnnotKit", package: "annotkit")
80+
])
81+
]`,
5982
},
6083
mount: {
61-
caption: "Mount it once the app has a window",
84+
caption: "Mount the toolbar",
6285
language: "swift",
6386
code: `import AnnotKit
6487
6588
#if DEBUG
6689
Annotation.install() // floating toolbar; click a view, type a note
6790
#endif`,
6891
},
69-
note: "`install()` mounts the floating toolbar and writes notes to `ANNOTKIT_NOTES.md` in the working directory. SwiftUI on iOS attaches it with `.installAnnotation()` instead.",
70-
more: "Where to call it from AppKit, SwiftUI and UIKit, how to point the notes file at your repo, other sinks, and world context: all in the README.",
92+
/** Beside the first plate: the route most app projects actually take. */
93+
xcode:
94+
"An Xcode project does it through File ▸ Add Package Dependencies… instead: paste the URL there, then add the AnnotKit library to your app target.",
95+
/**
96+
* Beside the second. It opens on what the snippet does NOT say rather than
97+
* restating it, and the working-directory line is the one thing a reader
98+
* loses a morning to if the page leaves it out (README §3).
99+
*/
100+
note: "SwiftUI on iOS attaches the toolbar to the root view with `.installAnnotation()` instead. Either way the notes land in `ANNOTKIT_NOTES.md` in the working directory, which is usually not your repo when Xcode launches the app. Set that in the scheme, or hand `install()` a sink with a path of its own.",
71101
readme: "Setup guide in the README",
72102
} as const;
73103

74104
export const annotate = {
75105
number: "§02",
76106
title: "Annotate",
77-
lead: "A click means this exact spot: AnnotKit descends to the deepest actionable control. A drawn frame means this whole thing: the largest element the frame surrounds wins. One rule, both platforms. The selector anchors itself to the nearest `accessibilityIdentifier`, so it round-trips back to code.",
107+
lead: "One rule, both platforms: what you point at is what the note binds to.",
78108
points: [
79109
{
80110
term: "Click",
@@ -86,7 +116,7 @@ export const annotate = {
86116
},
87117
{
88118
term: "Anchor",
89-
body: "An unidentified target is anchored to its nearest seeded identifier (`#Settings.Profile >> @Save`), so the selector points an agent at the right component’s code.",
119+
body: "An unidentified target is anchored to the nearest `accessibilityIdentifier` above it (`#Settings.Profile >> @Save`), so the selector points an agent at the right component’s code.",
90120
},
91121
],
92122
} as const;
@@ -102,7 +132,7 @@ export const agentNotes = {
102132
* whole sample off the page.
103133
*/
104134
sample: {
105-
caption: "The block `AnnotationFormatter` writes, one per note",
135+
caption: "`AnnotationFormatter` output",
106136
language: "markdown",
107137
code: `## [3f9c1a] Settings - #Settings.Profile.Save
108138
**Timestamp**: 2026-08-19T09:14:02Z
@@ -147,7 +177,7 @@ export const mcpBridge = {
147177
export const signup = {
148178
number: "§05",
149179
title: "One email when it’s worth reading.",
150-
lead: "The GPU CLI list: releases and design notes for AnnotKit, for the CLI that runs cloud GPUs from your terminal, and for whatever ships next. No digest, no drip campaign.",
180+
lead: "Releases and design notes for AnnotKit. No digest, no drip campaign.",
151181
label: "Email address",
152182
placeholder: "you@example.com",
153183
submit: "Get release notes",

web/src/sections/Hero.tsx

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,29 +35,34 @@ export function Hero() {
3535
<p className="hero__standfirst reveal" style={{ "--i": 1 } as React.CSSProperties}>
3636
{hero.standfirst}
3737
</p>
38+
{/* The primary sends the reader one section down rather than off the
39+
* site: §01 is the next thing on the page and it already ends with
40+
* the README link. The repo-visibility branch moved with the README
41+
* onto the secondary, which is where it was always aimed — an anchor
42+
* into this same document has nothing to be unavailable about. */}
3843
<div className="hero__ctas reveal" style={{ "--i": 2 } as React.CSSProperties}>
44+
<a
45+
className="link link--lead"
46+
href="#install"
47+
onClick={() => track({ name: "cta_install_clicked", props: { placement: "hero" } })}
48+
>
49+
{hero.ctaPrimary}
50+
<Icon as={ArrowDown} />
51+
</a>
3952
{site.repoIsPublic ? (
4053
<a
4154
className="link link--lead"
4255
href={site.repo}
4356
onClick={() => track({ name: "cta_github_clicked", props: { placement: "hero" } })}
4457
>
45-
{hero.ctaPrimary}
58+
{hero.ctaSecondary}
4659
<Icon as={ArrowUpRight} />
4760
</a>
4861
) : (
4962
<span className="link link--lead" aria-disabled="true">
50-
{hero.ctaPrimary}
63+
{hero.ctaSecondary}
5164
</span>
5265
)}
53-
<a
54-
className="link link--lead"
55-
href="#updates"
56-
onClick={() => track({ name: "cta_updates_clicked", props: { placement: "hero" } })}
57-
>
58-
{hero.ctaSecondary}
59-
<Icon as={ArrowDown} />
60-
</a>
6166
</div>
6267
</div>
6368
<div className="hero__figure reveal" style={{ "--i": 3 } as React.CSSProperties}>

web/src/sections/Install.tsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,14 @@ import { withCode } from "../markup";
88
import { track } from "../analytics";
99

1010
/**
11-
* §01 — two snippets: the dependency line and the mount. Everything the
12-
* README covers beyond that (AppKit vs SwiftUI vs UIKit call sites, the
13-
* working-directory gotcha, sinks, world context) is one link away rather
14-
* than repeated here; the page sells the loop, the README installs it.
11+
* §01 — two snippets, and one paragraph beside each: the Xcode route beside
12+
* the dependency, the notes path beside the mount. The paragraph that used to
13+
* sit here listing what else the README covers was a table of contents, not
14+
* copy, and it pointed at the README a third time in four lines.
15+
*
16+
* Everything past these two moves (call sites per framework, sinks, seeded
17+
* identifiers, world context) is one link away; the page sells the loop, the
18+
* README installs it.
1519
*/
1620
export function Install() {
1721
return (
@@ -28,8 +32,8 @@ export function Install() {
2832
<CodeBlock {...install.mount} />
2933
</div>
3034
<div className="stack">
35+
<p>{withCode(install.xcode)}</p>
3136
<p>{withCode(install.note)}</p>
32-
<p>{withCode(install.more)}</p>
3337
<a
3438
className="link link--lead"
3539
href={site.setup}

web/src/styles/base.css

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -776,26 +776,6 @@ pre {
776776
}
777777
}
778778

779-
/* §01's narrow column carries a snippet that cannot wrap and cannot shrink:
780-
* `Annotation.install(sink: ClipboardSink(format: .json))` needs 447 px at
781-
* --text-sm, and a 5/12 share stops covering that above about 1400 px.
782-
*
783-
* The reason is counter-intuitive enough to write down: the shell's content
784-
* box gets NARROWER as the viewport widens past `--page-max`, because the max
785-
* width stops growing at 78 rem while `--page-gutter` keeps climbing to 5 rem
786-
* and eats the difference. So the plate fits at 1280 and overflows at 1600.
787-
*
788-
* Fixed by giving the track a floor rather than by re-tuning the ratio — the
789-
* constraint is a fixed width, so a fixed width is the honest way to state
790-
* it. The wide column still clears its own longest line (562 px) with room
791-
* to spare, and `minmax(0, …)` on that track keeps gate 34 safe if it ever
792-
* does not. Scoped to where the problem starts; below this the 7:5 fits. */
793-
@media (min-width: 85rem) {
794-
.section--install .section__body--7-5 {
795-
grid-template-columns: minmax(0, 7fr) minmax(29rem, 5fr);
796-
}
797-
}
798-
799779
.stack {
800780
display: grid;
801781
gap: var(--space-md);

web/tests/highlight.test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,12 @@ describe("the highlighter", () => {
4949
expect(colour(html, ".install\\(\\) ")).toBe("var(--color-ink)");
5050
expect(html).toMatch(/color:var\(--color-muted\)">\/\/ floating/);
5151

52+
// The dependency snippet is both halves of the install now, so both are
53+
// pinned: the URL on the package line and the product on the target line.
5254
const dep = render(hl, install.package);
5355
expect(colour(dep, '"https://github.com/gpu-cli/annotkit"')).toBe("var(--color-syntax-string)");
54-
expect(colour(dep, "\\.package\\(url: ")).toBe("var(--color-ink)");
56+
expect(colour(dep, '"AnnotKit"')).toBe("var(--color-syntax-string)");
57+
expect(colour(dep, " \\.product\\(name: ")).toBe("var(--color-ink)");
5558
});
5659

5760
it("sets the note block's heading and labels as labels", () => {

web/tests/markup.dom.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,9 @@ describe("withCode", () => {
3636
const { container } = render(<p>{withCode(install.note)}</p>);
3737

3838
expect([...container.querySelectorAll("code")].map((mark) => mark.textContent)).toEqual([
39-
"install()",
40-
"ANNOTKIT_NOTES.md",
4139
".installAnnotation()",
40+
"ANNOTKIT_NOTES.md",
41+
"install()",
4242
]);
4343
});
4444

0 commit comments

Comments
 (0)