From 739059218664fb5c52911cc4f353df4cc79623f9 Mon Sep 17 00:00:00 2001 From: AlexandreCamillo <43162926+AlexandreCamillo@users.noreply.github.com> Date: Fri, 29 May 2026 00:56:28 +0000 Subject: [PATCH 01/11] feat(landing): add DesignFeatureFlow section explaining the design-feature skill MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Six-phase pipeline (00–05) walking from framework discovery to visual+behavior QA, with a Markup-boost callout per phase showing where the platform turns a manual step into a hosted, addressable artifact (pin, version, diff). Includes a small SVG rail diagram above the cards and a 'Design loop' link in the topbar nav. No new dependencies — pure CSS + inline SVG built on the existing landing primitives (Section, Eyebrow). --- src/app/landing/page.tsx | 2 + .../landing/DesignFeatureFlow.module.css | 209 ++++++++++++++++++ src/components/landing/DesignFeatureFlow.tsx | 159 +++++++++++++ src/components/landing/Topbar.tsx | 1 + 4 files changed, 371 insertions(+) create mode 100644 src/components/landing/DesignFeatureFlow.module.css create mode 100644 src/components/landing/DesignFeatureFlow.tsx diff --git a/src/app/landing/page.tsx b/src/app/landing/page.tsx index 7ccd89ae..7aa23aec 100644 --- a/src/app/landing/page.tsx +++ b/src/app/landing/page.tsx @@ -2,6 +2,7 @@ import { CompareTable } from '@/components/landing/CompareTable'; import { Contributors } from '@/components/landing/Contributors'; +import { DesignFeatureFlow } from '@/components/landing/DesignFeatureFlow'; import { FAQ } from '@/components/landing/FAQ'; import { FeatureGrid } from '@/components/landing/FeatureGrid'; import { FixLoopSteps } from '@/components/landing/FixLoopSteps'; @@ -21,6 +22,7 @@ export default function LandingPage() { + diff --git a/src/components/landing/DesignFeatureFlow.module.css b/src/components/landing/DesignFeatureFlow.module.css new file mode 100644 index 00000000..f8c34547 --- /dev/null +++ b/src/components/landing/DesignFeatureFlow.module.css @@ -0,0 +1,209 @@ +.h2 { + font-family: var(--font-display); + font-size: clamp(28px, 3.4vw, 48px); + font-weight: 700; + letter-spacing: -0.025em; + line-height: 1.05; + color: var(--text-bright); + margin-top: 18px; +} +.lead { + font-size: 18px; + line-height: 1.55; + color: var(--text-dim); + max-width: 64ch; + margin-top: 16px; +} +.lead code { + font-family: var(--font-mono); + font-size: 0.92em; + padding: 1px 6px; + background: var(--surface-soft); + border: 1px solid var(--border-subtle); + border-radius: 4px; + color: var(--text); +} + +/* ─── Diagram rail ───────────────────────────────────────── */ +.rail { + display: block; + width: 100%; + height: 72px; + margin-top: 56px; + margin-bottom: -20px; + overflow: visible; +} + +.railText { + letter-spacing: 0.04em; + font-weight: 500; + dominant-baseline: middle; +} + +/* ─── Phase grid ─────────────────────────────────────────── */ +.grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 14px; + margin-top: 40px; + list-style: none; + padding: 0; + counter-reset: dff-phase; +} + +.card { + display: flex; + flex-direction: column; + gap: 12px; + padding: 22px; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius-md); + position: relative; + transition: + border-color 160ms ease, + transform 200ms ease; +} + +.card:hover { + border-color: var(--border-strong); + transform: translateY(-1px); +} + +.cardHead { + display: flex; + align-items: baseline; + gap: 12px; +} + +.ix { + font-family: var(--font-mono); + font-size: 10px; + letter-spacing: 0.1em; + color: var(--accent); + font-variant-numeric: tabular-nums; + /* tiny corner accent rail */ + position: relative; + padding-left: 0; +} + +.ix::before { + content: ""; + position: absolute; + left: -22px; + top: 50%; + width: 14px; + height: 1px; + background: var(--accent); + opacity: 0.4; + transform: translateY(-50%); +} + +.cardTitle { + font-family: var(--font-display); + font-size: 17px; + font-weight: 600; + color: var(--text-bright); + letter-spacing: -0.01em; + margin: 0; +} + +.cardBody { + font-size: 13px; + line-height: 1.55; + color: var(--text-dim); + margin: 0; +} + +.cardBody code, +.boostText code { + font-family: var(--font-mono); + font-size: 11px; + padding: 1px 5px; + background: var(--surface-soft); + border: 1px solid var(--border-subtle); + border-radius: 4px; + color: var(--text); + white-space: nowrap; +} + +/* ─── Markup boost row ───────────────────────────────────── */ +.boost { + display: flex; + align-items: flex-start; + gap: 10px; + padding: 12px 14px; + margin-top: auto; + background: linear-gradient( + 180deg, + color-mix(in oklch, var(--accent-soft) 70%, transparent), + color-mix(in oklch, var(--accent-soft) 40%, transparent) + ); + border: 1px solid color-mix(in oklch, var(--accent) 25%, var(--border)); + border-radius: var(--radius-sm, 6px); +} + +.boostLabel { + font-family: var(--font-mono); + font-size: 10px; + letter-spacing: 0.08em; + color: var(--accent-bright); + flex-shrink: 0; + padding-top: 1px; +} + +.boostText { + font-size: 12px; + line-height: 1.5; + color: var(--text); +} + +/* ─── Coda ───────────────────────────────────────────────── */ +.coda { + margin-top: 32px; + font-size: 14px; + line-height: 1.6; + color: var(--text-dim); + max-width: 68ch; + padding-left: 14px; + border-left: 1px solid var(--accent); + border-left-color: color-mix(in oklch, var(--accent) 40%, transparent); +} +.coda code { + font-family: var(--font-mono); + font-size: 12px; + padding: 1px 5px; + background: var(--surface-soft); + border: 1px solid var(--border-subtle); + border-radius: 4px; + color: var(--text); +} + +/* ─── Responsive ─────────────────────────────────────────── */ +@media (max-width: 1024px) { + .grid { + grid-template-columns: repeat(2, 1fr); + } + .rail { + display: none; + } +} + +@media (max-width: 640px) { + .grid { + grid-template-columns: 1fr; + gap: 10px; + } + .card { + padding: 18px; + } +} + +@media (prefers-reduced-motion: reduce) { + .card { + transition: none; + } + .card:hover { + transform: none; + } +} diff --git a/src/components/landing/DesignFeatureFlow.tsx b/src/components/landing/DesignFeatureFlow.tsx new file mode 100644 index 00000000..5778559d --- /dev/null +++ b/src/components/landing/DesignFeatureFlow.tsx @@ -0,0 +1,159 @@ +import type { ReactNode } from 'react'; +import styles from './DesignFeatureFlow.module.css'; +import { Eyebrow } from './primitives/Eyebrow'; +import { Section } from './primitives/Section'; + +type Phase = { + index: string; + title: string; + body: ReactNode; + markup: ReactNode; +}; + +const PHASES: Phase[] = [ + { + index: '00', + title: 'Discovery', + body: 'Detect framework + ecosystem, surface CLAUDE.md rules, pick a UI strategy.', + markup: 'Strategy persists per repo. Future features reuse it.', + }, + { + index: '01', + title: 'Design brainstorm', + body: 'One self-contained HTML mockup with a live tweaker for variants, density, accent.', + markup: 'Hosted URL. Reviewers drop DOM-anchored pins instead of pasting screenshots.', + }, + { + index: '02', + title: 'Promote', + body: 'Bake locked tweaker choices, strip scaffolding, become a Design System file.', + markup: 'Every iteration is a version. The promotion diff is the audit trail.', + }, + { + index: '03', + title: 'Tech brainstorm', + body: 'Architecture spec scoped to the DS file. No UI re-design at this layer.', + markup: ( + <> + Agent calls GET /api/agent/context/[id] — annotation + HTML + diff in one + request. + + ), + }, + { + index: '04', + title: 'Plan + execute', + body: 'TDD plan; subagents implement; DS edits are first-class plan tasks.', + markup: ( + <> + Patches land via PATCH /api/mockups/[id]/version-patch. Reviewer sees the diff + inline. + + ), + }, + { + index: '05', + title: 'Visual + behavior QA', + body: 'Live route vs DS reference. Chrome MCP drives the state matrix; deltas loop until parity.', + markup: 'Regressions reopen the loop on the same pin. Same surface, same protocol, no rebuild.', + }, +]; + +export function DesignFeatureFlow() { + return ( +
+ The design-feature skill +

Six phases. Hosted, pinned, patched.

+

+ design-feature orchestrates the whole lifecycle of a user-visible feature — + from "I want a pricing card" to a parity check against the Design System. Markup is the + substrate that turns every phase into a hosted, addressable artifact: a pin, a version, a + diff. +

+ + + +
    + {PHASES.map((p) => ( +
  1. +
    + +

    {p.title}

    +
    +

    {p.body}

    +
    + + {p.markup} +
    +
  2. + ))} +
+ +

+ Mockup hosted on the same surface as production. Pins anchor to the same DOM. The agent + reads context, patches HTML, and replies on the thread — every step in 0→5{' '} + leaves an addressable trail. +

+
+ ); +} + +function DiagramRail() { + return ( + + + + + + + + + + + {[100, 280, 460, 640, 820, 1000].map((cx, i) => ( + + + + 0{i} + + + ))} + {/* terminal arrow */} + + + ); +} diff --git a/src/components/landing/Topbar.tsx b/src/components/landing/Topbar.tsx index 307e0375..a1c68418 100644 --- a/src/components/landing/Topbar.tsx +++ b/src/components/landing/Topbar.tsx @@ -14,6 +14,7 @@ export function Topbar() { From ea813e8811cd54727f4b95c28f3e70c9b4fb1758 Mon Sep 17 00:00:00 2001 From: AlexandreCamillo <43162926+AlexandreCamillo@users.noreply.github.com> Date: Fri, 29 May 2026 02:22:38 +0000 Subject: [PATCH 02/11] fix(landing): reframe DesignFeatureFlow as a community-skill pairing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Drop the SVG pipeline rail (visual noise; the card grid already telegraphs the sequence). - Remove the per-card 'Markup ↗' label — the styled callout already reads as a highlighted note; the label was redundant. - Reposition the section as a third-party-skill pairing rather than a Markup-native flow. New lead leads with the value: lock UI before talking implementation. Adds a ghost-pill link to the skill repo (AlexandreCamillo/markup-design-toolkit). --- .../landing/DesignFeatureFlow.module.css | 65 +++++-------- src/components/landing/DesignFeatureFlow.tsx | 93 +++++-------------- 2 files changed, 48 insertions(+), 110 deletions(-) diff --git a/src/components/landing/DesignFeatureFlow.module.css b/src/components/landing/DesignFeatureFlow.module.css index f8c34547..99cd2231 100644 --- a/src/components/landing/DesignFeatureFlow.module.css +++ b/src/components/landing/DesignFeatureFlow.module.css @@ -24,20 +24,25 @@ color: var(--text); } -/* ─── Diagram rail ───────────────────────────────────────── */ -.rail { - display: block; - width: 100%; - height: 72px; - margin-top: 56px; - margin-bottom: -20px; - overflow: visible; +/* ─── CTA row (skill repo link) ──────────────────────────── */ +.ctaRow { + display: flex; + align-items: center; + gap: 16px; + flex-wrap: wrap; + margin-top: 24px; } -.railText { - letter-spacing: 0.04em; - font-weight: 500; - dominant-baseline: middle; +.repoLink { + /* PillButton.ghost already styles the chip; this just tightens the inline gap. */ + font-family: var(--font-mono); + letter-spacing: 0.02em; +} + +.ctaHint { + font-size: 13px; + color: var(--text-muted); + line-height: 1.5; } /* ─── Phase grid ─────────────────────────────────────────── */ @@ -48,7 +53,6 @@ margin-top: 40px; list-style: none; padding: 0; - counter-reset: dff-phase; } .card { @@ -82,9 +86,7 @@ letter-spacing: 0.1em; color: var(--accent); font-variant-numeric: tabular-nums; - /* tiny corner accent rail */ position: relative; - padding-left: 0; } .ix::before { @@ -116,7 +118,7 @@ } .cardBody code, -.boostText code { +.boost code { font-family: var(--font-mono); font-size: 11px; padding: 1px 5px; @@ -127,11 +129,11 @@ white-space: nowrap; } -/* ─── Markup boost row ───────────────────────────────────── */ +/* ─── Boost callout — what the skill adds to Markup ──────── */ .boost { - display: flex; - align-items: flex-start; - gap: 10px; + font-size: 12px; + line-height: 1.5; + color: var(--text); padding: 12px 14px; margin-top: auto; background: linear-gradient( @@ -140,22 +142,7 @@ color-mix(in oklch, var(--accent-soft) 40%, transparent) ); border: 1px solid color-mix(in oklch, var(--accent) 25%, var(--border)); - border-radius: var(--radius-sm, 6px); -} - -.boostLabel { - font-family: var(--font-mono); - font-size: 10px; - letter-spacing: 0.08em; - color: var(--accent-bright); - flex-shrink: 0; - padding-top: 1px; -} - -.boostText { - font-size: 12px; - line-height: 1.5; - color: var(--text); + border-radius: var(--radius-sm); } /* ─── Coda ───────────────────────────────────────────────── */ @@ -166,8 +153,7 @@ color: var(--text-dim); max-width: 68ch; padding-left: 14px; - border-left: 1px solid var(--accent); - border-left-color: color-mix(in oklch, var(--accent) 40%, transparent); + border-left: 1px solid color-mix(in oklch, var(--accent) 40%, transparent); } .coda code { font-family: var(--font-mono); @@ -184,9 +170,6 @@ .grid { grid-template-columns: repeat(2, 1fr); } - .rail { - display: none; - } } @media (max-width: 640px) { diff --git a/src/components/landing/DesignFeatureFlow.tsx b/src/components/landing/DesignFeatureFlow.tsx index 5778559d..0dd69490 100644 --- a/src/components/landing/DesignFeatureFlow.tsx +++ b/src/components/landing/DesignFeatureFlow.tsx @@ -1,8 +1,11 @@ import type { ReactNode } from 'react'; import styles from './DesignFeatureFlow.module.css'; import { Eyebrow } from './primitives/Eyebrow'; +import { PillLink } from './primitives/PillButton'; import { Section } from './primitives/Section'; +const SKILL_REPO_URL = 'https://github.com/AlexandreCamillo/markup-design-toolkit'; + type Phase = { index: string; title: string; @@ -62,16 +65,29 @@ const PHASES: Phase[] = [ export function DesignFeatureFlow() { return (
- The design-feature skill -

Six phases. Hosted, pinned, patched.

+ Pair Markup with a skill +

Lock the design before you discuss the build.

- design-feature orchestrates the whole lifecycle of a user-visible feature — - from "I want a pricing card" to a parity check against the Design System. Markup is the - substrate that turns every phase into a hosted, addressable artifact: a pin, a version, a - diff. + design-feature is a community skill that gets the most out of your AI agent and + Markup. It runs the agent through a UI/UX brainstorm, hands you a live tweaker to explore + every option it proposes, and only opens the implementation conversation once the design is + locked.

- +
+ + markup-design-toolkit + + + Drop the skill into your agent and run it on this repo. + +
    {PHASES.map((p) => ( @@ -83,12 +99,7 @@ export function DesignFeatureFlow() {

    {p.title}

    {p.body}

    -
    - - {p.markup} -
    +
    {p.markup}
    ))}
@@ -101,59 +112,3 @@ export function DesignFeatureFlow() {
); } - -function DiagramRail() { - return ( - - - - - - - - - - - {[100, 280, 460, 640, 820, 1000].map((cx, i) => ( - - - - 0{i} - - - ))} - {/* terminal arrow */} - - - ); -} From 244ecdbb74fb167cf8ffa38cdfaadc0a9816a859 Mon Sep 17 00:00:00 2001 From: AlexandreCamillo <43162926+AlexandreCamillo@users.noreply.github.com> Date: Fri, 29 May 2026 02:27:20 +0000 Subject: [PATCH 03/11] fix(landing): point design-feature skill link to alkg-cloud/design-skills markup-design-toolkit is deprecated. The skill family moved to github.com/alkg-cloud/design-skills. --- src/components/landing/DesignFeatureFlow.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/landing/DesignFeatureFlow.tsx b/src/components/landing/DesignFeatureFlow.tsx index 0dd69490..73a1dee1 100644 --- a/src/components/landing/DesignFeatureFlow.tsx +++ b/src/components/landing/DesignFeatureFlow.tsx @@ -4,7 +4,7 @@ import { Eyebrow } from './primitives/Eyebrow'; import { PillLink } from './primitives/PillButton'; import { Section } from './primitives/Section'; -const SKILL_REPO_URL = 'https://github.com/AlexandreCamillo/markup-design-toolkit'; +const SKILL_REPO_URL = 'https://github.com/alkg-cloud/design-skills'; type Phase = { index: string; @@ -82,7 +82,7 @@ export function DesignFeatureFlow() { rel="noopener noreferrer" className={styles.repoLink} > - markup-design-toolkit + alkg-cloud/design-skills Drop the skill into your agent and run it on this repo. From 1856dbbcc91fd19a6a68e8b10b19d322c56d8259 Mon Sep 17 00:00:00 2001 From: AlexandreCamillo <43162926+AlexandreCamillo@users.noreply.github.com> Date: Fri, 29 May 2026 03:00:29 +0000 Subject: [PATCH 04/11] feat(landing): trim sections, rebrand footer, user-facing skill copy - Remove FixLoopSteps (was '#how-it-works') and CompareTable; hide Pricing block until commercial framing is ready. Drop the topbar 'How it works' nav link and re-point the footer 'Agent API' link to '#design-loop'. - Rename topbar + hero CTAs from 'Spin it up' to 'Get started'. - Rewrite DesignFeatureFlow phase boosts to lead with 'With Markup,' and drop the API/code references in favor of outcome language. Rename phase 02 'Promote' to 'Lock the design' to match the new framing. - Footer copyright: Alexandre Camillo -> ALKG. - ThreeUp icons: swap glyphs for react-icons/vsc (VscPass, VscSymbolMethod, VscCode). - FAQ: drop 'How are pin coordinates persistent?' (already covered by ThreeUp 'iframe is the truth' card). - Quickstart: drop the agent-loop.sh curl card; the docker one is the whole quickstart now. Tighten the h2. - Replace em-dashes in user-visible copy across landing components. --- src/app/landing/page.tsx | 6 -- .../landing/CompareTable.module.css | 61 ---------------- src/components/landing/CompareTable.tsx | 49 ------------- src/components/landing/DesignFeatureFlow.tsx | 55 +++++++------- src/components/landing/FAQ.tsx | 8 +-- .../landing/FixLoopSteps.module.css | 71 ------------------- src/components/landing/FixLoopSteps.tsx | 68 ------------------ src/components/landing/Hero.tsx | 2 +- .../landing/InteractiveDemo/DemoStage.tsx | 6 +- .../landing/InteractiveDemo/seeds.ts | 4 +- src/components/landing/LandingFooter.tsx | 4 +- src/components/landing/Quickstart.tsx | 32 +-------- src/components/landing/ThreeUp.tsx | 22 +++--- src/components/landing/Topbar.tsx | 3 +- 14 files changed, 51 insertions(+), 340 deletions(-) delete mode 100644 src/components/landing/CompareTable.module.css delete mode 100644 src/components/landing/CompareTable.tsx delete mode 100644 src/components/landing/FixLoopSteps.module.css delete mode 100644 src/components/landing/FixLoopSteps.tsx diff --git a/src/app/landing/page.tsx b/src/app/landing/page.tsx index 7aa23aec..5b84ec23 100644 --- a/src/app/landing/page.tsx +++ b/src/app/landing/page.tsx @@ -1,15 +1,12 @@ 'use client'; -import { CompareTable } from '@/components/landing/CompareTable'; import { Contributors } from '@/components/landing/Contributors'; import { DesignFeatureFlow } from '@/components/landing/DesignFeatureFlow'; import { FAQ } from '@/components/landing/FAQ'; import { FeatureGrid } from '@/components/landing/FeatureGrid'; -import { FixLoopSteps } from '@/components/landing/FixLoopSteps'; import { Hero } from '@/components/landing/Hero'; import { DemoStage } from '@/components/landing/InteractiveDemo/DemoStage'; import { LandingFooter } from '@/components/landing/LandingFooter'; -import { Pricing } from '@/components/landing/Pricing'; import { Quickstart } from '@/components/landing/Quickstart'; import { ThreeUp } from '@/components/landing/ThreeUp'; import { Topbar } from '@/components/landing/Topbar'; @@ -21,12 +18,9 @@ export default function LandingPage() { - - - diff --git a/src/components/landing/CompareTable.module.css b/src/components/landing/CompareTable.module.css deleted file mode 100644 index 1c0e5b9a..00000000 --- a/src/components/landing/CompareTable.module.css +++ /dev/null @@ -1,61 +0,0 @@ -.h2 { - font-family: var(--font-display); - font-size: clamp(28px, 3.4vw, 48px); - font-weight: 700; - letter-spacing: -0.025em; - line-height: 1.05; - color: var(--text-bright); - margin-top: 18px; -} -.wrap { - margin-top: 44px; - border: 1px solid var(--border); - border-radius: var(--radius-lg); - overflow: hidden; -} -.table { - width: 100%; - border-collapse: collapse; -} -.table th, -.table td { - text-align: left; - padding: 14px 18px; - font-size: 13px; - border-bottom: 1px solid var(--border-subtle); - color: var(--text-dim); - font-weight: 400; -} -.table thead { - background: var(--surface-soft); -} -.table thead th { - color: var(--text-bright); - font-weight: 700; - font-size: 12px; - letter-spacing: 0.04em; -} -.table thead th.us { - color: var(--accent); -} -.table td.us { - color: var(--text-bright); - background: var(--accent-overlay-soft); -} -.table tbody th[scope="row"] { - color: var(--text); - font-weight: 600; -} -.table tr:last-child td, -.table tr:last-child th { - border-bottom: 0; -} - -@media (max-width: 768px) { - .wrap { - overflow-x: auto; - } - .table { - min-width: 560px; - } -} diff --git a/src/components/landing/CompareTable.tsx b/src/components/landing/CompareTable.tsx deleted file mode 100644 index a8b8a80e..00000000 --- a/src/components/landing/CompareTable.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import styles from './CompareTable.module.css'; -import { Eyebrow } from './primitives/Eyebrow'; -import { Section } from './primitives/Section'; - -const ROWS = [ - { cap: 'Live frontend in the loop', us: '✓', a: '✗ (static images)', b: '✗ (design surfaces)' }, - { cap: 'DOM-anchored pins', us: '✓', a: '✗', b: 'N/A' }, - { cap: 'Single-mount self-host', us: '✓ One docker', a: 'Vendor only', b: 'Vendor only' }, - { cap: 'First-class agent API', us: '✓ Native', a: '✗', b: 'Plugin-only' }, - { - cap: 'Fix round-trip size', - us: '5–15 KB unified diff', - a: 'Re-upload PNG or zip', - b: 'Out-of-band', - }, -]; - -export function CompareTable() { - return ( -
- Vs. the screenshot loop -

How Markup compares.

-
- - - - - - - - - - - {ROWS.map((r) => ( - - - - - - - ))} - -
Capability - Markup - Slack + screenshotsFigma comments
{r.cap}{r.us}{r.a}{r.b}
-
-
- ); -} diff --git a/src/components/landing/DesignFeatureFlow.tsx b/src/components/landing/DesignFeatureFlow.tsx index 73a1dee1..625feb87 100644 --- a/src/components/landing/DesignFeatureFlow.tsx +++ b/src/components/landing/DesignFeatureFlow.tsx @@ -1,4 +1,3 @@ -import type { ReactNode } from 'react'; import styles from './DesignFeatureFlow.module.css'; import { Eyebrow } from './primitives/Eyebrow'; import { PillLink } from './primitives/PillButton'; @@ -9,56 +8,52 @@ const SKILL_REPO_URL = 'https://github.com/alkg-cloud/design-skills'; type Phase = { index: string; title: string; - body: ReactNode; - markup: ReactNode; + body: string; + markup: string; }; const PHASES: Phase[] = [ { index: '00', title: 'Discovery', - body: 'Detect framework + ecosystem, surface CLAUDE.md rules, pick a UI strategy.', - markup: 'Strategy persists per repo. Future features reuse it.', + body: 'The agent scans your stack, your conventions, and your design rules before suggesting anything.', + markup: + 'With Markup, the chosen direction is saved to the project so future features start from the same baseline.', }, { index: '01', title: 'Design brainstorm', - body: 'One self-contained HTML mockup with a live tweaker for variants, density, accent.', - markup: 'Hosted URL. Reviewers drop DOM-anchored pins instead of pasting screenshots.', + body: 'The agent proposes several visual directions in a single live mockup. You pick variant, density, accent.', + markup: + 'With Markup, the mockup goes online with one click. Reviewers leave pins straight on the artwork instead of pasting screenshots into chat.', }, { index: '02', - title: 'Promote', - body: 'Bake locked tweaker choices, strip scaffolding, become a Design System file.', - markup: 'Every iteration is a version. The promotion diff is the audit trail.', + title: 'Lock the design', + body: 'When you approve, the chosen options are baked in. The mockup graduates to your Design System.', + markup: + 'With Markup, every round of iteration is a saved version. You can scrub back to any earlier proposal at any time.', }, { index: '03', title: 'Tech brainstorm', - body: 'Architecture spec scoped to the DS file. No UI re-design at this layer.', - markup: ( - <> - Agent calls GET /api/agent/context/[id] — annotation + HTML + diff in one - request. - - ), + body: 'The implementation conversation only opens after the design is locked. No UI re-design at this layer.', + markup: + 'With Markup, the agent already sees the final mockup and the full review thread before the technical discussion starts.', }, { index: '04', - title: 'Plan + execute', - body: 'TDD plan; subagents implement; DS edits are first-class plan tasks.', - markup: ( - <> - Patches land via PATCH /api/mockups/[id]/version-patch. Reviewer sees the diff - inline. - - ), + title: 'Plan + build', + body: 'A TDD plan splits the work into small tasks. Sub-agents implement them in parallel against the locked design.', + markup: + 'With Markup, the agent ships each change as a versioned patch. You see exactly what was changed and reply on the original thread.', }, { index: '05', title: 'Visual + behavior QA', - body: 'Live route vs DS reference. Chrome MCP drives the state matrix; deltas loop until parity.', - markup: 'Regressions reopen the loop on the same pin. Same surface, same protocol, no rebuild.', + body: 'A browser-driven check compares the live build against the Design System reference and loops on any drift.', + markup: + 'With Markup, any regression reopens the same review surface. You drop a new pin and the loop starts again, no rebuild needed.', }, ]; @@ -105,9 +100,9 @@ export function DesignFeatureFlow() {

- Mockup hosted on the same surface as production. Pins anchor to the same DOM. The agent - reads context, patches HTML, and replies on the thread — every step in 0→5{' '} - leaves an addressable trail. + The mockup lives on the same surface as production. Pins anchor to the same DOM. The agent + reads context, patches HTML, and replies on the thread, so every step from 0 to{' '} + 5 leaves an addressable trail.

); diff --git a/src/components/landing/FAQ.tsx b/src/components/landing/FAQ.tsx index d2da85ab..3a61e9a0 100644 --- a/src/components/landing/FAQ.tsx +++ b/src/components/landing/FAQ.tsx @@ -5,7 +5,7 @@ import { Section } from './primitives/Section'; const FAQS = [ { q: 'Does it work without an LLM?', - a: "Yes. The cookie-session reviewer UI is the same surface, with no required AI. The agent API is opt-in — issue a bearer token only if you want an automated client (or just don't issue any).", + a: "Yes. The cookie-session reviewer UI is the same surface, with no required AI. The agent API is opt-in. Issue a bearer token only if you want an automated client (or just don't issue any).", open: true, }, { @@ -14,11 +14,7 @@ const FAQS = [ }, { q: 'Can I use it with Claude Code / Cursor / Aider?', - a: 'Yes. The agent API is just an HTTP contract — bearer token in, annotation context out, unified diff back. Any dev assistant that can call curl can drive Markup. The docs show example loops.', - }, - { - q: 'How are pin coordinates persistent?', - a: 'Pins anchor to DOM nodes via text-anchor and element-anchor selectors that are resilient to layout drift. The server resolves them on render so the pin reflows naturally with viewport, zoom, fullscreen, and visual updates.', + a: 'Yes. The agent API is just an HTTP contract: bearer token in, annotation context out, unified diff back. Any dev assistant that can call curl can drive Markup. The docs show example loops.', }, ]; diff --git a/src/components/landing/FixLoopSteps.module.css b/src/components/landing/FixLoopSteps.module.css deleted file mode 100644 index e07690a4..00000000 --- a/src/components/landing/FixLoopSteps.module.css +++ /dev/null @@ -1,71 +0,0 @@ -.h2 { - font-family: var(--font-display); - font-size: clamp(28px, 3.4vw, 48px); - font-weight: 700; - letter-spacing: -0.025em; - line-height: 1.05; - color: var(--text-bright); - margin-top: 18px; -} -.lead { - font-size: 18px; - line-height: 1.55; - color: var(--text-dim); - max-width: 60ch; - margin-top: 16px; -} -.steps { - display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 12px; - margin-top: 40px; - list-style: none; - padding: 0; -} -.step { - padding: 22px; - background: var(--bg-card); - border: 1px solid var(--border); - border-radius: var(--radius-md); -} -.ix { - font-family: var(--font-mono); - font-size: 10px; - color: var(--accent); - letter-spacing: 0.08em; -} -.step h4 { - color: var(--text-bright); - margin-top: 6px; - margin-bottom: 8px; - font-size: 15px; - font-weight: 600; -} -.step p { - color: var(--text-dim); - font-size: 12px; - line-height: 1.5; -} -.step code { - font-size: 11px; - padding: 1px 4px; - background: var(--surface-soft); - border: 1px solid var(--border-subtle); - border-radius: 4px; -} - -@media (max-width: 768px) { - .steps { - grid-template-columns: 1fr 1fr; - gap: 10px; - margin-top: 28px; - } - .step { - padding: 18px; - } -} -@media (max-width: 480px) { - .steps { - grid-template-columns: 1fr; - } -} diff --git a/src/components/landing/FixLoopSteps.tsx b/src/components/landing/FixLoopSteps.tsx deleted file mode 100644 index 4106efb6..00000000 --- a/src/components/landing/FixLoopSteps.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import type { ReactNode } from 'react'; -import styles from './FixLoopSteps.module.css'; -import { Eyebrow } from './primitives/Eyebrow'; -import { Section } from './primitives/Section'; - -const STEPS: { title: string; body: ReactNode }[] = [ - { - title: 'Annotate', - body: ( - <> - Reviewer or agent posts a pin + thread to POST /api/mockups/[id]/annotations. - Body required; pins optional. - - ), - }, - { - title: 'Context', - body: ( - <> - GET /api/agent/context/[id] returns the annotation, the current inline HTML, - and a diff since creation — in one request. - - ), - }, - { - title: 'Patch', - body: ( - <> - PATCH /api/mockups/[id]/version-patch accepts standard unified diffs against a - base version. Atomic, append-only. - - ), - }, - { - title: 'Reply', - body: ( - <> - POST /api/threads/[id]/reply closes the loop. The reviewer sees the diff inline - and resolves. - - ), - }, -]; - -export function FixLoopSteps() { - return ( -
- The fix loop, end to end -

Four calls. Five to fifteen kilobytes.

-

- A reviewer drops a pin. The agent fetches one aggregated context payload, applies a patch, - and replies on the thread. That's the loop — same shape whether the agent is Claude Code, - Cursor, Aider, or your own CI script. -

-
    - {STEPS.map((s, i) => ( -
  1. - -

    {s.title}

    -

    {s.body}

    -
  2. - ))} -
-
- ); -} diff --git a/src/components/landing/Hero.tsx b/src/components/landing/Hero.tsx index da6b96a7..f3926180 100644 --- a/src/components/landing/Hero.tsx +++ b/src/components/landing/Hero.tsx @@ -23,7 +23,7 @@ export function Hero() {

- Spin it up docker + Get started docker Try the demo → diff --git a/src/components/landing/InteractiveDemo/DemoStage.tsx b/src/components/landing/InteractiveDemo/DemoStage.tsx index c399b254..1acef76e 100644 --- a/src/components/landing/InteractiveDemo/DemoStage.tsx +++ b/src/components/landing/InteractiveDemo/DemoStage.tsx @@ -48,14 +48,14 @@ export function DemoStage() { Try without signing up

Pin it yourself.

- A live Markup surface, running on your browser. Drop pins, write annotations, react, reply — - everything persists in localStorage so the next visitor finds a clean slate.{' '} + A live Markup surface, running on your browser. Drop pins, write annotations, react, reply. + Everything persists in localStorage so the next visitor finds a clean slate.{' '} Reset any time.

Demo mode - Lumen Coffee — Hero v3 + Lumen Coffee · Hero v3
localStorage only · no server
- © Alexandre Camillo · Elastic License 2.0 + © ALKG · Elastic License 2.0 Made for review loops that actually close.
diff --git a/src/components/landing/Quickstart.tsx b/src/components/landing/Quickstart.tsx index afd1c72f..f6f8d716 100644 --- a/src/components/landing/Quickstart.tsx +++ b/src/components/landing/Quickstart.tsx @@ -3,29 +3,20 @@ import { Eyebrow } from './primitives/Eyebrow'; import { Section } from './primitives/Section'; import styles from './Quickstart.module.css'; -const DOCKER_RAW = `# 1. Pull and start. The first request runs the setup wizard. +const DOCKER_RAW = `# Pull and start. The first request runs the setup wizard. docker run -d --name markup \\ -p 3000:3000 \\ -e AUTH_SECRET=$(openssl rand -hex 32) \\ -v $(pwd)/markup-data:/app/data \\ ghcr.io/alkg-cloud/markup:latest`; -const CURL_RAW = `# 2. Read context, apply patch, reply on thread. -curl -H "Authorization: Bearer $TOKEN" \\ - "http://localhost:3000/api/agent/context/$ANNOTATION_ID" -# → { annotation, thread, current_version, diff_since_creation } - -curl -X POST -H "Authorization: Bearer $TOKEN" \\ - -d "{ base_version_id, patches: { 'index.html': $DIFF } }" \\ - "http://localhost:3000/api/mockups/$ID/version-patch"`; - export function Quickstart() { return (
Quickstart -

Run it in 30 seconds. Open the agent API in 30 more.

+

Run it in 30 seconds.

- # 1. Pull and start. The first request runs the setup wizard. + # Pull and start. The first request runs the setup wizard. {'\n'} docker run -d --name markup \{'\n'} {' '} @@ -36,23 +27,6 @@ export function Quickstart() { -v $(pwd)/markup-data:/app/data \{'\n'} {' '}ghcr.io/alkg-cloud/markup:latest - - # 2. Read context, apply patch, reply on thread. - {'\n'} - curl -H "Authorization: Bearer $TOKEN" \{'\n'} - {' '} - "http://localhost:3000/api/agent/context/$ANNOTATION_ID" - {'\n'} - - # → {'{'} annotation, thread, current_version, diff_since_creation {'}'} - - {'\n\n'} - curl -X POST -H "Authorization: Bearer $TOKEN" \{'\n'} - {' '} - -d {`"{ base_version_id, patches: { 'index.html': $DIFF } }"`} \{'\n'} - {' '} - "http://localhost:3000/api/mockups/$ID/version-patch" -
); } diff --git a/src/components/landing/ThreeUp.tsx b/src/components/landing/ThreeUp.tsx index 4f8b2412..323b1e39 100644 --- a/src/components/landing/ThreeUp.tsx +++ b/src/components/landing/ThreeUp.tsx @@ -1,20 +1,22 @@ +import type { IconType } from 'react-icons'; +import { VscCode, VscPass, VscSymbolMethod } from 'react-icons/vsc'; import { Eyebrow } from './primitives/Eyebrow'; import { Section } from './primitives/Section'; import styles from './ThreeUp.module.css'; -const CARDS = [ +const CARDS: { Icon: IconType; title: string; body: string }[] = [ { - icon: '⊕', + Icon: VscPass, title: 'The iframe is the truth', - body: 'Pins anchor to DOM nodes. Reflow, zoom, fullscreen, viewport changes — the pin stays on the element it was dropped on. No screenshot drift.', + body: 'Pins anchor to DOM nodes. Reflow, zoom, fullscreen, viewport changes: the pin stays on the element it was dropped on. No screenshot drift.', }, { - icon: '▣', + Icon: VscSymbolMethod, title: 'Single-mount deploy', body: 'One docker container. SQLite + filesystem. No PostgreSQL, no Redis, no S3. Mount one volume, back up one tree. 256 MB is enough.', }, { - icon: '⟶', + Icon: VscCode, title: 'An API agents can use', body: 'Server-side DOM resolution, computed-style extraction, unified-diff versioning. Same routes power the browser UI and autonomous orchestrators.', }, @@ -31,13 +33,13 @@ export function ThreeUp() { without a human pasting context into chat.

- {CARDS.map((c) => ( -
+ {CARDS.map(({ Icon, title, body }) => ( +
-

{c.title}

-

{c.body}

+

{title}

+

{body}

))}
diff --git a/src/components/landing/Topbar.tsx b/src/components/landing/Topbar.tsx index a1c68418..a3e92a1b 100644 --- a/src/components/landing/Topbar.tsx +++ b/src/components/landing/Topbar.tsx @@ -13,7 +13,6 @@ export function Topbar() {
Demo - How it works Design loop Quickstart FAQ @@ -38,7 +37,7 @@ export function Topbar() { GitHub - Spin it up docker + Get started docker
From 05839b776ba43a5dcd9b00d62ec9c29e2478a67c Mon Sep 17 00:00:00 2001 From: AlexandreCamillo <43162926+AlexandreCamillo@users.noreply.github.com> Date: Fri, 29 May 2026 03:09:29 +0000 Subject: [PATCH 05/11] fix(landing): tighten design-loop boosts and drop inaccurate phrasing - Drop boost callouts from phases 00 (Discovery), 04 (Plan + build), and 05 (Visual + behavior QA). Those phases stand on their own; the boost row was adding nothing concrete. - Phase 01: replace 'mockup goes online with one click' (untrue: there is no click, the agent uploads automatically as part of the skill flow) with 'every iteration is uploaded for you'. --- src/components/landing/DesignFeatureFlow.tsx | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/components/landing/DesignFeatureFlow.tsx b/src/components/landing/DesignFeatureFlow.tsx index 625feb87..92fcd8e8 100644 --- a/src/components/landing/DesignFeatureFlow.tsx +++ b/src/components/landing/DesignFeatureFlow.tsx @@ -9,7 +9,7 @@ type Phase = { index: string; title: string; body: string; - markup: string; + markup?: string; }; const PHASES: Phase[] = [ @@ -17,15 +17,13 @@ const PHASES: Phase[] = [ index: '00', title: 'Discovery', body: 'The agent scans your stack, your conventions, and your design rules before suggesting anything.', - markup: - 'With Markup, the chosen direction is saved to the project so future features start from the same baseline.', }, { index: '01', title: 'Design brainstorm', body: 'The agent proposes several visual directions in a single live mockup. You pick variant, density, accent.', markup: - 'With Markup, the mockup goes online with one click. Reviewers leave pins straight on the artwork instead of pasting screenshots into chat.', + 'With Markup, every iteration is uploaded for you. Reviewers leave pins straight on the live mockup. No screenshots, no chat pasting.', }, { index: '02', @@ -45,15 +43,11 @@ const PHASES: Phase[] = [ index: '04', title: 'Plan + build', body: 'A TDD plan splits the work into small tasks. Sub-agents implement them in parallel against the locked design.', - markup: - 'With Markup, the agent ships each change as a versioned patch. You see exactly what was changed and reply on the original thread.', }, { index: '05', title: 'Visual + behavior QA', body: 'A browser-driven check compares the live build against the Design System reference and loops on any drift.', - markup: - 'With Markup, any regression reopens the same review surface. You drop a new pin and the loop starts again, no rebuild needed.', }, ]; @@ -94,7 +88,7 @@ export function DesignFeatureFlow() {

{p.title}

{p.body}

-
{p.markup}
+ {p.markup &&
{p.markup}
} ))} From a7f4e2c616196cd93610dafb8ad8a163645baacd Mon Sep 17 00:00:00 2001 From: AlexandreCamillo <43162926+AlexandreCamillo@users.noreply.github.com> Date: Fri, 29 May 2026 03:15:27 +0000 Subject: [PATCH 06/11] fix(landing): drop narrow section variant so all titles align FAQ, Quickstart, Contributors (and the hidden Pricing) used Section width="narrow" which capped them at 980px while the default sections sit at 1200px. That left section titles unevenly aligned across the page. Removing the narrow variant entirely (CSS + Section types + callers) collapses every section onto the same 1200px container. --- src/components/landing/Contributors.tsx | 1 - src/components/landing/FAQ.tsx | 2 +- src/components/landing/Pricing.tsx | 2 +- src/components/landing/Quickstart.tsx | 2 +- src/components/landing/primitives/Section.module.css | 3 --- src/components/landing/primitives/Section.tsx | 2 +- 6 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/components/landing/Contributors.tsx b/src/components/landing/Contributors.tsx index 861bc7e0..fe67c8c1 100644 --- a/src/components/landing/Contributors.tsx +++ b/src/components/landing/Contributors.tsx @@ -72,7 +72,6 @@ export function Contributors() { return (
+
Frequently asked

The questions reviewers always ask.

diff --git a/src/components/landing/Pricing.tsx b/src/components/landing/Pricing.tsx index f26e0a43..1f83f9b7 100644 --- a/src/components/landing/Pricing.tsx +++ b/src/components/landing/Pricing.tsx @@ -4,7 +4,7 @@ import { Section } from './primitives/Section'; export function Pricing() { return ( -
+
Licensing, plainly

Free to self-host. Free for your team.

diff --git a/src/components/landing/Quickstart.tsx b/src/components/landing/Quickstart.tsx index f6f8d716..cad56fe4 100644 --- a/src/components/landing/Quickstart.tsx +++ b/src/components/landing/Quickstart.tsx @@ -12,7 +12,7 @@ docker run -d --name markup \\ export function Quickstart() { return ( -
+
Quickstart

Run it in 30 seconds.

diff --git a/src/components/landing/primitives/Section.module.css b/src/components/landing/primitives/Section.module.css index 9bd89b75..96773aa7 100644 --- a/src/components/landing/primitives/Section.module.css +++ b/src/components/landing/primitives/Section.module.css @@ -3,9 +3,6 @@ margin: 0 auto; padding: 112px 32px; } -.narrow { - max-width: 980px; -} .wide { max-width: 1340px; } diff --git a/src/components/landing/primitives/Section.tsx b/src/components/landing/primitives/Section.tsx index 3ada34d1..d2e25797 100644 --- a/src/components/landing/primitives/Section.tsx +++ b/src/components/landing/primitives/Section.tsx @@ -5,7 +5,7 @@ import styles from './Section.module.css'; type Props = { children: ReactNode; id?: string; - width?: 'default' | 'narrow' | 'wide'; + width?: 'default' | 'wide'; className?: string; }; From 58d02240d489a21fb776029dbae3815cdfa2e216 Mon Sep 17 00:00:00 2001 From: AlexandreCamillo <43162926+AlexandreCamillo@users.noreply.github.com> Date: Thu, 28 May 2026 09:46:41 +0000 Subject: [PATCH 07/11] test(e2e): align landing demo specs with current selectors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The two failing landing.spec.ts tests still looked for a "Drop pin" button that has not existed since the ViewerShell migration. The demo's create-annotation flow now: click "New annotation" → click inside the iframe (title="Mockup") → fill the draft textarea ("Annotation body") → click "Send" → annotation card appears with data-pin-target. Drops the window.prompt mock (the old DraftCard used prompt, the new one is a real textarea). Reset test uses the same flow to seed a 4th annotation before exercising the two-click reset. --- tests/e2e/landing.spec.ts | 49 ++++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/tests/e2e/landing.spec.ts b/tests/e2e/landing.spec.ts index 2baedc8b..9390534b 100644 --- a/tests/e2e/landing.spec.ts +++ b/tests/e2e/landing.spec.ts @@ -9,35 +9,36 @@ test.describe('Landing page', () => { test('demo accepts a pin drop and persists across reload', async ({ page }) => { await page.goto('/landing#demo'); - await page.evaluate(() => { - window.prompt = () => 'Smoke test annotation'; - }); - const annotItems = page.locator('ul li[aria-label^="Annotation "]'); - const before = await annotItems.count(); - await page.getByRole('button', { name: /Drop pin/i }).click(); - const canvas = page.locator('[role="application"][aria-label*="Mockup canvas"]'); - await canvas.click({ position: { x: 200, y: 200 } }); - await expect(annotItems).toHaveCount(before + 1); + const annotCards = page.locator('[data-pin-target]'); + const before = await annotCards.count(); + + await page.getByRole('button', { name: /new annotation/i }).click(); + await page + .frameLocator('iframe[title="Mockup"]') + .locator('body') + .click({ position: { x: 200, y: 200 } }); + await page.getByRole('textbox', { name: /annotation body/i }).fill('Smoke test annotation'); + await page.getByRole('button', { name: /^send$/i }).click(); + + await expect(annotCards).toHaveCount(before + 1); await page.reload(); - await expect(annotItems).toHaveCount(before + 1); + await expect(annotCards).toHaveCount(before + 1); }); test('reset demo restores seeded state after confirmation', async ({ page }) => { await page.goto('/landing#demo'); - // window.prompt is mocked so the pin-drop flow doesn't block the test. - await page.evaluate(() => { - window.prompt = () => 'Extra annotation'; - }); - await expect(page.locator('ul li[aria-label^="Annotation "]')).toHaveCount(3); + const annotCards = page.locator('[data-pin-target]'); + await expect(annotCards).toHaveCount(3); - // Drop a 4th pin via the UI so reset has something to clear. This avoids - // the previous approach of editing localStorage + reloading (which caused - // a React hydration mismatch because the server-rendered tree used - // seeded state while the client read the populated localStorage). - await page.getByRole('button', { name: /Drop pin/i }).click(); - const canvas = page.locator('[role="application"][aria-label*="Mockup canvas"]'); - await canvas.click({ position: { x: 150, y: 150 } }); - await expect(page.locator('ul li[aria-label^="Annotation "]')).toHaveCount(4); + // Drop a 4th annotation via the draft flow so reset has something to clear. + await page.getByRole('button', { name: /new annotation/i }).click(); + await page + .frameLocator('iframe[title="Mockup"]') + .locator('body') + .click({ position: { x: 150, y: 150 } }); + await page.getByRole('textbox', { name: /annotation body/i }).fill('Extra annotation'); + await page.getByRole('button', { name: /^send$/i }).click(); + await expect(annotCards).toHaveCount(4); // Two-step confirm: first click arms the confirm, second click resets. // Match BOTH the rest-state ("Reset demo") and the armed-state @@ -46,7 +47,7 @@ test.describe('Landing page', () => { const reset = page.getByRole('button', { name: /Reset|Click again/i }); await reset.click(); await reset.click(); - await expect(page.locator('ul li[aria-label^="Annotation "]')).toHaveCount(3); + await expect(annotCards).toHaveCount(3); }); test('no horizontal scroll on mobile viewport', async ({ page }) => { From a239f91f92d5a6ce9c7c2cb3327cf455d5999fb7 Mon Sep 17 00:00:00 2001 From: AlexandreCamillo <43162926+AlexandreCamillo@users.noreply.github.com> Date: Fri, 29 May 2026 03:21:30 +0000 Subject: [PATCH 08/11] test(e2e): scope reset-demo locator to avoid toolbar zoom button The /Reset|Click again/i regex used to disambiguate the demo's reset button now also matches the CanvasToolbar's zoom-reset button (aria-label 'Current zoom 100% - click to reset'), which appeared after the ViewerShell migration. Narrow the regex to 'Reset demo' so only the intended button is targeted. --- tests/e2e/landing.spec.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/e2e/landing.spec.ts b/tests/e2e/landing.spec.ts index 9390534b..2c176fe6 100644 --- a/tests/e2e/landing.spec.ts +++ b/tests/e2e/landing.spec.ts @@ -41,10 +41,13 @@ test.describe('Landing page', () => { await expect(annotCards).toHaveCount(4); // Two-step confirm: first click arms the confirm, second click resets. - // Match BOTH the rest-state ("Reset demo") and the armed-state - // ("Click again to confirm") so the second click doesn't wait for the + // Match BOTH the rest-state ("↻ Reset demo") and the armed-state + // ("⚠ Click again to confirm") so the second click doesn't wait for the // 3s confirm window to expire before the locator resolves again. - const reset = page.getByRole('button', { name: /Reset|Click again/i }); + // The regex is scoped to "Reset demo" (not just "Reset") to avoid + // colliding with the toolbar's zoom-reset button aria-label + // ("Current zoom 100% — click to reset"). + const reset = page.getByRole('button', { name: /Reset demo|Click again/i }); await reset.click(); await reset.click(); await expect(annotCards).toHaveCount(3); From 3ce6d725998d6e57ec21e06b9997e72e4f46e6ae Mon Sep 17 00:00:00 2001 From: AlexandreCamillo <43162926+AlexandreCamillo@users.noreply.github.com> Date: Fri, 29 May 2026 03:32:02 +0000 Subject: [PATCH 09/11] test(landing): add render test for DesignFeatureFlow Covers eyebrow + headline + lead + repo CTA, all six phase cards in order, the boost-callout pattern (only on phases 01/02/03), and the closing coda paragraph. Restores the line coverage that was lost when FixLoopSteps and CompareTable were removed in the previous commits on this branch. --- .../landing/DesignFeatureFlow.render.test.tsx | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 tests/landing/DesignFeatureFlow.render.test.tsx diff --git a/tests/landing/DesignFeatureFlow.render.test.tsx b/tests/landing/DesignFeatureFlow.render.test.tsx new file mode 100644 index 00000000..1e3ea104 --- /dev/null +++ b/tests/landing/DesignFeatureFlow.render.test.tsx @@ -0,0 +1,54 @@ +// @vitest-environment jsdom +import { render } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; +import { DesignFeatureFlow } from '@/components/landing/DesignFeatureFlow'; + +describe('', () => { + it('renders the section with eyebrow, headline, lead, and repo CTA', () => { + const { container, getByText, getByRole } = render(); + + expect(container.querySelector('section#design-loop')).not.toBeNull(); + expect(getByText(/Pair Markup with a skill/)).toBeTruthy(); + expect(getByText(/Lock the design before you discuss the build\./)).toBeTruthy(); + + const repoLink = getByRole('link', { name: /alkg-cloud\/design-skills/i }); + expect(repoLink.getAttribute('href')).toBe('https://github.com/alkg-cloud/design-skills'); + expect(repoLink.getAttribute('target')).toBe('_blank'); + expect(repoLink.getAttribute('rel')).toContain('noopener'); + }); + + it('renders all six phase cards in order (00..05)', () => { + const { container } = render(); + const items = container.querySelectorAll('ol > li'); + expect(items.length).toBe(6); + const indices = Array.from(items).map( + (li) => li.querySelector('[aria-hidden="true"]')?.textContent, + ); + expect(indices).toEqual(['00', '01', '02', '03', '04', '05']); + }); + + it('renders boost callouts only for phases 01, 02, 03', () => { + const { container } = render(); + const items = container.querySelectorAll('ol > li'); + // boost is the last child in cards that have one; absent in others + const withBoost: string[] = []; + items.forEach((li) => { + const ix = li.querySelector('[aria-hidden="true"]')?.textContent; + // any direct child whose text starts with "With Markup," is the boost + const hasBoost = Array.from(li.children).some((c) => + (c.textContent ?? '').startsWith('With Markup'), + ); + if (hasBoost && ix) withBoost.push(ix); + }); + expect(withBoost).toEqual(['01', '02', '03']); + }); + + it('renders the closing coda paragraph', () => { + const { container } = render(); + // The coda has inline elements that split text nodes, so we + // assert against the concatenated text of the last

in the section. + const paragraphs = container.querySelectorAll('p'); + const codaText = paragraphs[paragraphs.length - 1]?.textContent; + expect(codaText).toContain('leaves an addressable trail'); + }); +}); From cf52fd40704817a6744ee925c415b14a5721e3b2 Mon Sep 17 00:00:00 2001 From: AlexandreCamillo <43162926+AlexandreCamillo@users.noreply.github.com> Date: Fri, 29 May 2026 03:34:33 +0000 Subject: [PATCH 10/11] test(landing): render tests for ThreeUp, Quickstart, and FAQ These components had no direct tests before; adding minimal render tests lifts branch + line coverage to recover the regression introduced by removing FixLoopSteps and CompareTable. The FAQ test also pins the removal of the 'pin coordinates' question so it can't drift back in unnoticed. --- tests/landing/FAQ.render.test.tsx | 38 ++++++++++++++++++++++++ tests/landing/Quickstart.render.test.tsx | 29 ++++++++++++++++++ tests/landing/ThreeUp.render.test.tsx | 26 ++++++++++++++++ 3 files changed, 93 insertions(+) create mode 100644 tests/landing/FAQ.render.test.tsx create mode 100644 tests/landing/Quickstart.render.test.tsx create mode 100644 tests/landing/ThreeUp.render.test.tsx diff --git a/tests/landing/FAQ.render.test.tsx b/tests/landing/FAQ.render.test.tsx new file mode 100644 index 00000000..94c5f0fb --- /dev/null +++ b/tests/landing/FAQ.render.test.tsx @@ -0,0 +1,38 @@ +// @vitest-environment jsdom +import { render } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; +import { FAQ } from '@/components/landing/FAQ'; + +describe('', () => { + it('renders the section with three FAQ entries, the first one open', () => { + const { container, getByText } = render(); + + expect(container.querySelector('section#faq')).not.toBeNull(); + expect(getByText(/Frequently asked/)).toBeTruthy(); + expect(getByText(/The questions reviewers always ask\./)).toBeTruthy(); + + const details = container.querySelectorAll('details'); + expect(details.length).toBe(3); + + const questions = Array.from(container.querySelectorAll('summary')).map( + (el) => el.textContent ?? '', + ); + expect(questions).toEqual([ + 'Does it work without an LLM?', + 'Why SQLite instead of Postgres?', + 'Can I use it with Claude Code / Cursor / Aider?', + ]); + + // First entry is the one marked `open` + expect((details[0] as HTMLDetailsElement).open).toBe(true); + expect((details[1] as HTMLDetailsElement).open).toBe(false); + expect((details[2] as HTMLDetailsElement).open).toBe(false); + + // The removed "pin coordinates" question must not appear + expect( + Array.from(container.querySelectorAll('summary')).some((s) => + (s.textContent ?? '').includes('pin coordinates'), + ), + ).toBe(false); + }); +}); diff --git a/tests/landing/Quickstart.render.test.tsx b/tests/landing/Quickstart.render.test.tsx new file mode 100644 index 00000000..7ec96525 --- /dev/null +++ b/tests/landing/Quickstart.render.test.tsx @@ -0,0 +1,29 @@ +// @vitest-environment jsdom +import { render } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; +import { Quickstart } from '@/components/landing/Quickstart'; + +describe('', () => { + it('renders the section with the docker code card and copy button', () => { + const { container, getByText, getByRole } = render(); + + expect(container.querySelector('section#quickstart')).not.toBeNull(); + expect(getByText(/Quickstart/)).toBeTruthy(); + expect(getByText(/Run it in 30 seconds\./)).toBeTruthy(); + + // Exactly one code card (the agent-loop curl card was removed) + const filenames = Array.from(container.querySelectorAll('section#quickstart span')) + .map((el) => el.textContent ?? '') + .filter((t) => t === '~/markup/start.sh'); + expect(filenames.length).toBe(1); + + // Copy button is rendered + expect(getByRole('button', { name: /Copy/i })).toBeTruthy(); + + // Docker command body is reachable in the rendered output + const preText = container.querySelector('pre')?.textContent ?? ''; + expect(preText).toContain('docker run -d'); + expect(preText).toContain('AUTH_SECRET'); + expect(preText).toContain('ghcr.io/alkg-cloud/markup'); + }); +}); diff --git a/tests/landing/ThreeUp.render.test.tsx b/tests/landing/ThreeUp.render.test.tsx new file mode 100644 index 00000000..355392ba --- /dev/null +++ b/tests/landing/ThreeUp.render.test.tsx @@ -0,0 +1,26 @@ +// @vitest-environment jsdom +import { render } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; +import { ThreeUp } from '@/components/landing/ThreeUp'; + +describe('', () => { + it('renders the section with eyebrow, h2, lead, and three cards', () => { + const { container, getByText } = render(); + + expect(container.querySelector('section#why')).not.toBeNull(); + expect(getByText(/Why Markup/)).toBeTruthy(); + expect(getByText(/Three reasons it's not just "Figma for code"\./)).toBeTruthy(); + + const cards = container.querySelectorAll('section#why > div > div'); + // The .grid container holds the three cards. + const cardArr = Array.from(container.querySelectorAll('section#why h3')); + expect(cardArr.map((h) => h.textContent)).toEqual([ + 'The iframe is the truth', + 'Single-mount deploy', + 'An API agents can use', + ]); + // sanity: each card has a body paragraph + an SVG icon + expect(cards.length).toBeGreaterThan(0); + expect(container.querySelectorAll('section#why svg').length).toBe(3); + }); +}); From a02df58e0d0d5225b0e13433404dc1a626ec2422 Mon Sep 17 00:00:00 2001 From: AlexandreCamillo <43162926+AlexandreCamillo@users.noreply.github.com> Date: Fri, 29 May 2026 03:48:02 +0000 Subject: [PATCH 11/11] test(lib): cover popover/position + upload/resolve-target MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These two pure helpers were sitting at 0% coverage and counted against the project ratchet. Adding unit tests against their full surface (every align path, every clamp branch, every regex outcome) lifts line, statement, branch, and function coverage above the recorded baseline. Required to unblock the coverage ratchet — landing files are excluded from the coverage scope by design, so the four landing-only test files added in earlier commits don't show up in the totals. --- tests/unit/lib/popover/position.test.ts | 116 +++++++++++++++++++ tests/unit/lib/upload/resolve-target.test.ts | 69 +++++++++++ 2 files changed, 185 insertions(+) create mode 100644 tests/unit/lib/popover/position.test.ts create mode 100644 tests/unit/lib/upload/resolve-target.test.ts diff --git a/tests/unit/lib/popover/position.test.ts b/tests/unit/lib/popover/position.test.ts new file mode 100644 index 00000000..60801903 --- /dev/null +++ b/tests/unit/lib/popover/position.test.ts @@ -0,0 +1,116 @@ +// @vitest-environment jsdom +import { beforeEach, describe, expect, it } from 'vitest'; +import { positionPopover } from '@/lib/popover/position'; + +type Rect = { top: number; left: number; width: number; height: number }; + +function makeAnchor(rect: Rect): HTMLElement { + const el = document.createElement('button'); + el.getBoundingClientRect = () => + ({ + top: rect.top, + left: rect.left, + width: rect.width, + height: rect.height, + bottom: rect.top + rect.height, + right: rect.left + rect.width, + x: rect.left, + y: rect.top, + toJSON: () => ({}), + }) as DOMRect; + return el; +} + +function makePopover(rect: Rect): HTMLElement { + const el = document.createElement('div'); + el.style.position = 'fixed'; + el.getBoundingClientRect = () => + ({ + top: rect.top, + left: rect.left, + width: rect.width, + height: rect.height, + bottom: rect.top + rect.height, + right: rect.left + rect.width, + x: rect.left, + y: rect.top, + toJSON: () => ({}), + }) as DOMRect; + return el; +} + +describe('positionPopover', () => { + beforeEach(() => { + Object.defineProperty(window, 'innerWidth', { value: 1000, configurable: true }); + Object.defineProperty(window, 'innerHeight', { value: 800, configurable: true }); + }); + + it('places below the trigger aligned to the left by default', () => { + const trigger = makeAnchor({ top: 100, left: 200, width: 80, height: 32 }); + const popover = makePopover({ top: 0, left: 0, width: 200, height: 120 }); + positionPopover(popover, trigger); + expect(popover.style.top).toBe('138px'); // 100 + 32 + 6 (default gap) + expect(popover.style.left).toBe('200px'); + }); + + it('aligns to the right when align="right"', () => { + const trigger = makeAnchor({ top: 100, left: 200, width: 80, height: 32 }); + const popover = makePopover({ top: 0, left: 0, width: 200, height: 120 }); + positionPopover(popover, trigger, 'right'); + // popover.right == trigger.right (280) → left = 280 - 200 = 80 + expect(popover.style.left).toBe('80px'); + }); + + it('aligns to the center when align="center"', () => { + const trigger = makeAnchor({ top: 100, left: 400, width: 100, height: 32 }); + const popover = makePopover({ top: 0, left: 0, width: 200, height: 120 }); + positionPopover(popover, trigger, 'center'); + // center of trigger = 450; popover.left = 450 - 100 = 350 + expect(popover.style.left).toBe('350px'); + }); + + it('flips above when no room below', () => { + const trigger = makeAnchor({ top: 700, left: 200, width: 80, height: 50 }); + const popover = makePopover({ top: 0, left: 0, width: 200, height: 200 }); + positionPopover(popover, trigger); + // below would be 756; window.innerHeight=800, 4 margin → 796 max + // 756 + 200 = 956 > 796 → flip + // flip top = 700 - 200 - 6 = 494 + expect(popover.style.top).toBe('494px'); + }); + + it('clamps horizontally against the right edge', () => { + const trigger = makeAnchor({ top: 100, left: 900, width: 80, height: 32 }); + const popover = makePopover({ top: 0, left: 0, width: 200, height: 120 }); + positionPopover(popover, trigger); + // left = 900; maxLeft = 1000 - 200 - 4 = 796 → clamped to 796 + expect(popover.style.left).toBe('796px'); + }); + + it('clamps horizontally against the left edge', () => { + const trigger = makeAnchor({ top: 100, left: -50, width: 80, height: 32 }); + const popover = makePopover({ top: 0, left: 0, width: 200, height: 120 }); + positionPopover(popover, trigger); + // left = -50 → margin 4 + expect(popover.style.left).toBe('4px'); + }); + + it('clamps vertical top so popover never starts above the viewport', () => { + // Flip happens, but the flipped position is still negative — must clamp to 4. + Object.defineProperty(window, 'innerHeight', { value: 200, configurable: true }); + const trigger = makeAnchor({ top: 150, left: 200, width: 80, height: 40 }); + const popover = makePopover({ top: 0, left: 0, width: 200, height: 300 }); + positionPopover(popover, trigger); + // below = 196; 196+300=496 > 196 (200-4) → flip + // flip top = 150 - 300 - 6 = -156 → clamp to 4 + expect(popover.style.top).toBe('4px'); + }); + + it('honors a custom gap value', () => { + const trigger = makeAnchor({ top: 100, left: 200, width: 80, height: 32 }); + const popover = makePopover({ top: 0, left: 0, width: 200, height: 120 }); + positionPopover(popover, trigger, 'left', 20); + // 100 + 32 + 20 = 152 + expect(popover.style.top).toBe('152px'); + }); +}); diff --git a/tests/unit/lib/upload/resolve-target.test.ts b/tests/unit/lib/upload/resolve-target.test.ts new file mode 100644 index 00000000..3f56b519 --- /dev/null +++ b/tests/unit/lib/upload/resolve-target.test.ts @@ -0,0 +1,69 @@ +import { describe, expect, it } from 'vitest'; +import { resolveTargetFromPath } from '@/lib/upload/resolve-target'; + +describe('resolveTargetFromPath', () => { + it('returns null for null or empty pathname', () => { + expect(resolveTargetFromPath(null)).toBeNull(); + expect(resolveTargetFromPath('')).toBeNull(); + }); + + it('returns Unsorted at the root path', () => { + expect(resolveTargetFromPath('/')).toEqual({ + projectId: null, + folderId: null, + projectLabel: 'Unsorted', + folderPath: [], + }); + }); + + it('returns null for unrelated paths', () => { + expect(resolveTargetFromPath('/settings/agents')).toBeNull(); + expect(resolveTargetFromPath('/login')).toBeNull(); + expect(resolveTargetFromPath('/mockups/abc')).toBeNull(); + }); + + it('resolves a project root path', () => { + expect(resolveTargetFromPath('/projects/lumen-coffee')).toEqual({ + projectId: null, + folderId: null, + projectLabel: 'lumen-coffee', + folderPath: [], + }); + }); + + it('resolves project + folder breadcrumb path', () => { + expect(resolveTargetFromPath('/projects/lumen-coffee/Hero/v3')).toEqual({ + projectId: null, + folderId: null, + projectLabel: 'lumen-coffee', + folderPath: ['Hero', 'v3'], + }); + }); + + it('decodes URL-encoded slug and segments', () => { + expect(resolveTargetFromPath('/projects/lumen%20coffee/Hero%20Section/v3')).toEqual({ + projectId: null, + folderId: null, + projectLabel: 'lumen coffee', + folderPath: ['Hero Section', 'v3'], + }); + }); + + it('drops empty segments from the folder path', () => { + expect(resolveTargetFromPath('/projects/lumen-coffee/Hero//')).toEqual({ + projectId: null, + folderId: null, + projectLabel: 'lumen-coffee', + folderPath: ['Hero'], + }); + }); + + it('handles trailing slash with no tail', () => { + expect(resolveTargetFromPath('/projects/lumen-coffee/')).toEqual({ + projectId: null, + folderId: null, + projectLabel: 'lumen-coffee', + folderPath: [], + }); + }); +});