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() {
+ {p.body}
+ 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 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 (
+ 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) => (
+
+
+ {p.title}
+ 0→5{' '}
+ leaves an addressable trail.
+
- 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.
{p.body}
-| Capability | -- Markup - | -Slack + screenshots | -Figma comments | -
|---|---|---|---|
| {r.cap} | -{r.us} | -{r.a} | -{r.b} | -
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.
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 (
- - 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. -
-{s.body}
-
- 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.
{c.body}
+{body}
{p.body}
- 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: [],
+ });
+ });
+});