Skip to content
cccskills
FreePublished on GitHub

journey

Self-contained HTML user-journey map (boxes/arrows with per-page modals) — preview between PRD and tech-design.

The English summary is being prepared. Showing the original description.

me2resh/apexyard538 stars(GitHub 538 + cccskills 0)

Run score Not tested

See how to install

Files included (4)

  • SKILL.md23.1 KB
  • fixtures/sample-checkout.yaml1.7 KB
  • tests/README.md2.1 KB
  • tests/render_smoke.py24.5 KB

SKILL.md (original)

Review the instructions the agent will receive before you install.

Writing: read docs/skills/shared/writing-rule.md before you write a durable artifact.

/journey — User-Journey HTML with Modal-Per-Page Design Preview

Generate a single self-contained HTML file at projects/<name>/journeys/<feature-slug>.html mapping the user journey as clickable boxes (each opening a modal with the page's content). Companion skill to /c4 (architecture preview) and /threat-model (security preview) — closes the gap on flow-level preview before any implementation.

When to use

TriggerUse /journey?
PRD approved, about to start tech designYes — visualises the flow the PRD describes, surfaces missing states
Stakeholder review of a new feature flowYes — single-file HTML, opens in any browser, shareable as an attachment
Quick "what does this look like as a flow?" sketchYes — conversational mode is 5 questions
Per-page visual design (high-fidelity mockups)No — /journey is flow-first; per-page mockups are designer territory
Architecture diagramsNo — use /c4 for L1/L2 architecture
Production deliverable HTMLNo — the artifact is a draft preview, not production code

Activated role

When /journey runs, activate the UX Designer role — they own user flows and information architecture. For PRD parsing in mode (a), the Product Manager is the supporting role (they wrote the PRD). See .claude/rules/role-triggers.md for the full activation protocol.

Path resolution

Read the registry path via portfolio_registry, the per-project docs dir via portfolio_projects_dir, and the ideas backlog via portfolio_ideas_backlog — all from .claude/hooks/_lib-portfolio-paths.sh. Source the helper at the top of any bash block that touches those paths:

source "$(git rev-parse --show-toplevel)/.claude/hooks/_lib-read-config.sh"
source "$(git rev-parse --show-toplevel)/.claude/hooks/_lib-portfolio-paths.sh"
projects_dir=$(portfolio_projects_dir)

Defaults match today's single-fork layout (./apexyard.projects.yaml, ./projects). Adopters in split-portfolio mode override the portfolio.{registry, projects_dir} keys in .claude/project-config.json. Don't hardcode literal projects/ paths in bash blocks — the helper resolves whichever mode the adopter is in. See docs/multi-project.md.

Write targets (see me2resh/apexyard#373 + #443): paths documented as projects/<name>/X in this skill are canonical adopter-facing forms — implement them in bash as "${projects_dir}/<name>/X". Never construct from "${PWD}/projects/...", "$(git rev-parse --show-toplevel)/projects/...", or a literal ./projects/... — those break in split-portfolio v2 mode where projects_dir resolves to a sibling repo.

REQUIRED per-block preamble (see #443): Claude executes each bash block as a separate shell invocation. The projects_dir assignment from the Path resolution section above does NOT carry into later blocks. Every bash block that writes to a projects/<name>/X path MUST start with this three-line preamble so it's self-contained:

source "$(git rev-parse --show-toplevel)/.claude/hooks/_lib-read-config.sh"
source "$(git rev-parse --show-toplevel)/.claude/hooks/_lib-portfolio-paths.sh"
projects_dir=$(portfolio_projects_dir)
# ... now write to "${projects_dir}/<name>/X"

The Path resolution section's example sources the helper once for documentation purposes; it does not absolve later blocks from sourcing it themselves. Treat each bash fence as a fresh process.

Usage

/journey                                        # conversational — asks for project, feature, pages, transitions
/journey checkout-v2                            # conversational, feature pre-named
/journey checkout-v2 --from-prd projects/example-app/prds/checkout.md
/journey checkout-v2 --from-yaml projects/example-app/journeys/checkout-v2.yaml
/journey checkout-v2 --update                   # regenerate HTML from existing YAML
/journey checkout-v2 --wireframe                # ask for inline HTML wireframe sketches in modals (v1, optional)

Argument shape:

FormBehaviour
No argsConversational mode; ask for the project + feature first
<feature-slug> onlyConversational mode for that feature (slug becomes the filename)
--from-prd <path>Parse pages + transitions from a PRD file
--from-yaml <path>Load structured journey data from a YAML file
--updateRe-render the HTML from <feature-slug>.yaml without re-asking
--wireframe(Default OFF) ask the agent to sketch HTML wireframes inline in each page's modal

--from-prd and --from-yaml are mutually exclusive. --update is mutually exclusive with both — it always reads from the existing YAML.

Output location

Files are written under the per-project docs dir (resolved via portfolio_projects_dir):

FilePathPurpose
YAML source-of-truth<projects_dir>/<name>/journeys/<feature-slug>.yamlEditable, diff-reviewable, regenerable input
HTML build artifact<projects_dir>/<name>/journeys/<feature-slug>.htmlSingle self-contained file — no external deps

Both are committed. The YAML so reviewers can diff flow changes across PRs; the HTML so reviewers don't need to re-render to see it.

Process

1. Resolve the target project

Same pattern as /feature, /bug, /c4:

  • If invoked from workspace/<name>/ → <name> is the project.
  • If invoked from the ops-fork root → ask which project this journey is for (list registered projects from apexyard.projects.yaml).
  • If exactly one project is registered → use it without asking.
  • If a <feature-slug> argument was passed and a journey already exists at <projects_dir>/<name>/journeys/<feature-slug>.yaml, infer the project from that path.

2. Resolve the feature slug

The slug becomes the filename (without extension). Prefer kebab-case, max 40 chars.

  • If <feature-slug> was passed as the first positional arg → use it.
  • If --from-prd <path> and the PRD has a clear title → derive a slug from the title.
  • Otherwise → ask: "What's the feature name? (used as the filename)".

3. Source the journey data

Three input modes — pick whichever the operator's flags imply, or ask if ambiguous.

Mode (a) — From a PRD

Triggered by --from-prd <path> (relative to the ops-fork root or absolute).

Read the PRD. Look for these structural cues to extract pages and transitions:

CueInferred
## User Flow / ## User Journey / ## Flow headingSection to parse for pages
Numbered steps under "Flow" / "Journey" / "Steps"Each step is a page candidate
- User clicks X → goes to Y patternsA transition from current page to Y
## Pages / ## Screens headingEach subheading is a page
## Acceptance Criteria with Given/When/ThenPre-conditions / post-conditions = transitions
Mermaid graph or flowchart blocksDirect edge list

If the PRD is too prose-heavy to parse mechanically, fall back to mode (b) but pre-fill the conversation with the PRD's title, problem statement, and any explicit flow notes.

Mode (b) — Conversational

Default mode when no --from-* flag is given. A 5-question micro-interview:

  1. Pages — "What pages / screens / states are in this flow? (one per line, name + 1-line description)"
  2. Entry point — "Which page does the user land on first?"
  3. Transitions — "What are the transitions? Format: <from> -> <to> on <trigger> (e.g. Login -> Dashboard on submit). One per line."
  4. Personas — "Are there multiple personas (e.g. user / admin / system)? Optional — leave blank for single-persona."
  5. Notes — "Any global notes about the flow? (e.g. 'all pages require auth except Login')"

Capture answers verbatim into the YAML structure below.

Mode (c) — From YAML

Triggered by --from-yaml <path> or --update (which uses the canonical path <projects_dir>/<name>/journeys/<feature-slug>.yaml).

Load the YAML as-is. Validate it has the required top-level keys; if not, surface what's missing and stop.

4. Validate the journey graph

Before writing files, sanity-check:

  • Every page is reachable. Walk transitions from the entry page; flag any unreachable page.
  • Every to: and from: references a page that exists. Typos here are the most common mistake.
  • Hard cap at 12 pages. A v1 journey with more than 12 pages is almost certainly two journeys; ask the operator to split.
  • Hard cap at 30 transitions. Same reasoning — too dense to render readably.

If validation fails, list the issues and ask the operator how to fix (the conversation is cheap; rendering an unreadable graph is expensive).

5. Persist the YAML

Write <projects_dir>/<name>/journeys/<feature-slug>.yaml with this shape:

version: 1
feature: <feature-slug>
project: <name>
title: <human-readable title>
description: <one-sentence description>
generated_at: <ISO-8601>
generated_by: /journey

# Optional persona dimension. When absent, single-persona flow.
personas: []  # or: [user, admin, system]

# Pages = nodes in the graph.
pages:
  - id: <kebab-case id>
    title: <Display Name>
    persona: <persona-id, optional>
    description: <one-line summary>
    contents:
      - <bullet describing what's on the page>
      - <another bullet>
    success_state: <what "success" looks like on this page, optional>
    error_state: <what error/edge/empty looks like, optional>
    image: <relative path or URL to an image, optional>
    wireframe_html: <inline HTML when --wireframe is on, optional>

# Transitions = directed edges.
transitions:
  - from: <page-id>
    to: <page-id>
    trigger: <user-readable description, e.g. "submits form">

# Entry point: the first page the user lands on.
entry: <page-id>

# Optional notes shown on the journey overview.
notes: |
  Multi-line notes about the flow, prerequisites, etc.

Write the file. If it already exists and --update was NOT passed, ask before overwriting (the YAML may have hand edits the operator wants to keep).

6. Render the HTML

Generate <projects_dir>/<name>/journeys/<feature-slug>.html — a single self-contained file with all CSS and JS inline. No <script src=…>, no <link rel="stylesheet" href=…>, no CDN references.

The HTML structure:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>{title} — User Journey</title>
  <style>/* inline CSS — see § "Inline CSS" below */</style>
</head>
<body>
  <header>
    <div class="meta">
      <div class="project">{project}</div>
      <h1>{title}</h1>
      <div class="description">{description}</div>
      <div class="disclaimer">DRAFT — preview before implementation. Not a production deliverable.</div>
      <div class="timestamp">Generated {generated_at}</div>
    </div>
  </header>

  <main>
    <section class="graph">
      <svg viewBox="0 0 {width} {height}" role="img" aria-label="User journey graph">
        <!-- Boxes (one <g> per page, with id="page-{page.id}") -->
        <!-- Arrows (one <path> per transition) -->
        <!-- Click handlers attach to each <g> via inline JS below -->
      </svg>
    </section>

    {modals}  <!-- One <div class="modal" id="modal-{page.id}"> per page -->
  </main>

  <footer>
    <div>Source: <code>{yaml_path}</code></div>
    <div>Regenerate: <code>/journey {feature-slug} --update</code></div>
  </footer>

  <script>/* inline JS — see § "Inline JS" below */</script>
</body>
</html>

Layout algorithm (v1: vertical flowchart)

Compute box positions before emitting SVG:

  1. Identify the entry page (entry: field).
  2. Topologically sort pages by BFS distance from entry. Pages at the same BFS depth are in the same row.
  3. Within a row, distribute boxes evenly across the canvas width.
  4. Box dimensions: width 220px, height 80px. Row spacing: 140px. Column spacing: 40px (between boxes in the same row).
  5. Cycles → break the cycle at the longest back-edge for layout purposes; the back-edge is still drawn as an arrow with a curved path.
  6. Total canvas: width = max(rowWidths), height = (numRows - 1) * rowSpacing + boxHeight + 80px (top + bottom padding).

For v1, this layout is intentionally simple. A page with 8 outgoing transitions will look messy — that's an acceptable v1 trade-off; complex graphs are a v2 concern.

Boxes

Each page renders as an SVG <g> with a <rect> and centered <text>:

<g class="page-box" id="page-{id}" data-page-id="{id}" tabindex="0" role="button" aria-label="Open details for {title}">
  <rect x="..." y="..." width="220" height="80" rx="8" />
  <text x="..." y="..." text-anchor="middle" dominant-baseline="middle">
    <tspan class="page-title" x="...">{title}</tspan>
    <tspan class="page-persona" x="..." dy="18">{persona}</tspan>  <!-- if persona set -->
  </text>
</g>

Persona styling: if the page has a persona field, give the <g> a data-persona="{persona}" attribute and use CSS to colour-code (one colour per distinct persona, picked deterministically from a small palette). Without persona, all boxes use the default neutral fill.

Arrows

Each transition renders as an SVG <path> with a <text> label centered along the path:

<defs>
  <marker id="arrowhead" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto">
    <path d="M0,0 L10,5 L0,10 z"/>
  </marker>
</defs>

<path class="transition-arrow"
      d="M{x1},{y1} C{cx1},{cy1} {cx2},{cy2} {x2},{y2}"
      marker-end="url(#arrowhead)" />
<text class="transition-label" x="..." y="...">{trigger}</text>

For straight downward arrows (parent row → next row), use a simple cubic curve with control points at the midpoint. For back-edges (cycles), use a wide curve that arcs to the side.

Modals

One modal per page, hidden by default:

<div class="modal" id="modal-{id}" role="dialog" aria-modal="true" aria-labelledby="modal-title-{id}" hidden>
  <div class="modal-backdrop" data-close-modal="{id}"></div>
  <div class="modal-content">
    <header>
      <h2 id="modal-title-{id}">{title}</h2>
      {persona-pill if persona set}
      <button class="modal-close" data-close-modal="{id}" aria-label="Close">×</button>
    </header>
    <section class="modal-description">{description}</section>
    <section class="modal-contents">
      <h3>Page contents</h3>
      <ul>
        {one <li> per contents bullet}
      </ul>
    </section>
    {modal-success-state section if set}
    {modal-error-state section if set}
    <section class="modal-transitions-in">
      <h3>Transitions in</h3>
      <ul>
        {one <li> per incoming transition: "from {other-page-title} on {trigger}"}
      </ul>
    </section>
    <section class="modal-transitions-out">
      <h3>Transitions out</h3>
      <ul>
        {one <li> per outgoing transition: "to {other-page-title} on {trigger}"}
      </ul>
    </section>
    {modal-image section if image set}
    {modal-wireframe section if wireframe_html set — inline HTML wrapped in a sandboxed wireframe container}
  </div>
</div>

Inline CSS (sketch — keep under ~100 lines)

* { box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
       margin: 0; color: #1f2937; background: #f9fafb; }
header { padding: 24px 32px; background: #fff; border-bottom: 1px solid #e5e7eb; }
header h1 { margin: 0 0 4px; font-size: 1.5rem; }
header .project { font-size: 0.85rem; color: #6b7280; text-transform: uppercase; letter-spacing: 0.04em; }
header .description { color: #4b5563; margin-top: 6px; }
header .disclaimer { display: inline-block; margin-top: 12px; padding: 4px 10px;
                     background: #fef3c7; color: #92400e; font-size: 0.8rem; border-radius: 4px; font-weight: 600; }
header .timestamp { font-size: 0.75rem; color: #9ca3af; margin-top: 8px; }
main { padding: 32px; }
section.graph { display: flex; justify-content: center; }
svg { max-width: 100%; height: auto; }
.page-box rect { fill: #fff; stroke: #6366f1; stroke-width: 2; cursor: pointer;
                 transition: fill 0.15s, stroke 0.15s; }
.page-box:hover rect, .page-box:focus rect { fill: #eef2ff; stroke: #4338ca; }
.page-box text { font-size: 14px; pointer-events: none; }
.page-title { font-weight: 600; }
.page-persona { font-size: 11px; fill: #6b7280; }
.transition-arrow { fill: none; stroke: #9ca3af; stroke-width: 1.5; }
.transition-label { font-size: 11px; fill: #4b5563; pointer-events: none; }
.modal[hidden] { display: none; }
.modal { position: fixed; inset: 0; z-index: 1000; }
.modal-backdrop { position: absolute; inset: 0; background: rgba(17, 24, 39, 0.5); }
.modal-content { position: relative; max-width: 640px; margin: 5vh auto; max-height: 90vh;
                 overflow-y: auto; background: #fff; border-radius: 8px; padding: 24px;
                 box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2); }
.modal-content header { display: flex; align-items: center; gap: 12px; padding: 0 0 16px;
                        border-bottom: 1px solid #e5e7eb; background: transparent; }
.modal-content header h2 { margin: 0; font-size: 1.25rem; flex: 1; }
.modal-close { background: none; border: 0; font-size: 1.5rem; cursor: pointer; color: #6b7280; }
.modal-close:hover { color: #1f2937; }
.modal-content section { margin-top: 16px; }
.modal-content h3 { font-size: 0.9rem; text-transform: uppercase; letter-spacing: 0.05em;
                    color: #6b7280; margin: 0 0 8px; }
.modal-content ul { padding-left: 20px; margin: 0; }
.persona-pill { display: inline-block; padding: 2px 8px; font-size: 0.75rem;
                background: #eef2ff; color: #4338ca; border-radius: 999px; }
footer { padding: 24px 32px; font-size: 0.8rem; color: #6b7280;
         border-top: 1px solid #e5e7eb; background: #fff; }
footer code { background: #f3f4f6; padding: 2px 6px; border-radius: 3px; font-size: 0.85em; }

Persona colour rotation (only when personas is non-empty): cycle through #6366f1, #10b981, #f59e0b, #ec4899, #8b5cf6 — apply via attribute selector .page-box[data-persona="X"] rect { stroke: ...; }.

Inline JS (sketch — keep under ~50 lines)

(function () {
  const pages = document.querySelectorAll('.page-box');
  const modals = document.querySelectorAll('.modal');

  function openModal(id) {
    const m = document.getElementById('modal-' + id);
    if (!m) return;
    m.hidden = false;
    document.body.style.overflow = 'hidden';
    const closeBtn = m.querySelector('.modal-close');
    if (closeBtn) closeBtn.focus();
  }

  function closeModal(id) {
    const m = document.getElementById('modal-' + id);
    if (!m) return;
    m.hidden = true;
    document.body.style.overflow = '';
    const trigger = document.getElementById('page-' + id);
    if (trigger) trigger.focus();
  }

  pages.forEach((p) => {
    const id = p.dataset.pageId;
    p.addEventListener('click', () => openModal(id));
    p.addEventListener('keydown', (e) => {
      if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); openModal(id); }
    });
  });

  document.querySelectorAll('[data-close-modal]').forEach((el) => {
    el.addEventListener('click', () => closeModal(el.dataset.closeModal));
  });

  document.addEventListener('keydown', (e) => {
    if (e.key === 'Escape') {
      modals.forEach((m) => { if (!m.hidden) closeModal(m.id.replace(/^modal-/, '')); });
    }
  });
})();

7. Confirm to the user

✓ <project>: Journey written

  YAML: <projects_dir>/<name>/journeys/<feature-slug>.yaml  (source of truth)
  HTML: <projects_dir>/<name>/journeys/<feature-slug>.html  (preview)

  Pages: <N>      Transitions: <M>      Personas: <P or "single">

Open the HTML directly in a browser. To edit, change the YAML and run:
  /journey <feature-slug> --update

If this was a --from-prd run, append a one-line tip:

  Source PRD: <path>

Rules

  1. Single self-contained HTML — no external CSS, JS, or font references. Everything inline. The whole point is "open the file, see the journey" — a fetch failure on a CDN should never break the artifact.
  2. YAML is the source of truth — never hand-edit the HTML. --update regenerates from YAML; the HTML is replaced entirely on each run.
  3. Hard cap at 12 pages, 30 transitions — bigger journeys are usually two journeys. Ask the operator to split.
  4. Modal-per-page is mandatory — even if the modal only has a one-line description. The interaction model is "click a box, see the detail"; without modals the artifact is just an unstyled flowchart.
  5. Don't fabricate page contents — in mode (b), if the operator didn't list contents for a page, the modal shows the description only. Don't invent bullets the operator didn't specify.
  6. --wireframe is opt-in (v1) — default to prose-only modals. Wireframe sketches are bounded by what the agent can produce in plain HTML; document this in the modal so reviewers don't mistake a sketch for a real design.
  7. Disclaimer banner is mandatory — every generated HTML carries the "DRAFT — preview before implementation" line. The artifact is a flow-mapping tool, not a design deliverable.
  8. No JS frameworks, no build step — vanilla DOM API only. The reader must be able to open the HTML in any browser without a server.

Out of scope (v1)

  • Swimlane layout — single-persona flowchart only. Multi-persona swimlanes are a v2 question (gated on the personas: field).
  • Live preview / hot-reload — --update is the only regen path.
  • Cross-project journeys — one project per invocation. Portfolio-spanning flows (e.g. shared auth across projects) are a v2 concern.
  • Real designtool integration (Figma API, Sketch export) — v1 takes paths or URLs only; deeper integration is v3.
  • A11y / responsive testing of the generated HTML — the journey is a preview artifact, not a production deliverable; basic keyboard + ARIA is provided but not exhaustively tested.
  • Image embedding via base64 — v1 references images by relative path or URL only. Base64 inflates the file size; if the operator wants a self-contained build, they reference images on the same disk and copy both files together.

Relationship to other skills

SkillRelationship
/write-specProduces the PRD that feeds /journey --from-prd. Sequential pair.
/c4Sibling preview-before-build skill. /c4 shows architecture; /journey shows user flow.
/threat-modelSibling skill. /threat-model shows attack surface; /journey shows user flow.
/feature / /bug / /taskTracker creation. /journey is sometimes attached to a feature ticket as a flow preview.

Part of ApexYard — multi-project SDLC framework for Claude Code · MIT.

Reviews

No reviews yet. Tell others what you thought.

Comments

No login needed. Share how it worked for you, ask a question, or note where it did not run.

No comments yet. Be the first to write one.

Comments are published right away. Do not include personal information such as email addresses or phone numbers. Up to 2 URLs.

0 / 1,000 characters

This form is protected by reCAPTCHA, and the Google Privacy Policy and Terms of Service apply.

More skills from this repository

WCAG 2.1 AA audit — perceivable, operable, understandable, robust criteria. Deep-dive for /launch-check accessibility.

The English summary is being prepared. Showing the original description.

me2resh/apexyard538Updated Oct 26, 2026

WCAG 2.2 AA audit — perceivable, operable, understandable, robust criteria. Deep-dive for /launch-check accessibility.

The English summary is being prepared. Showing the original description.

me2resh/apexyard538Updated Oct 26, 2026

agdr

Free

Browse / search / show / stats AgDRs across the portfolio — recalls "have we decided this before?".

The English summary is being prepared. Showing the original description.

me2resh/apexyard538Updated Oct 26, 2026

Analytics audit — SDK config, event naming, funnel completeness, dashboards. Deep-dive for /launch-check analytics.

The English summary is being prepared. Showing the original description.

me2resh/apexyard538Updated Oct 26, 2026

Record per-PR architecture-review approval for design-artifact PRs (required by the architecture gate). ONLY on an explicit per-PR architect "approved".

The English summary is being prepared. Showing the original description.

me2resh/apexyard538Updated Oct 26, 2026

Record per-PR design-review approval (UI merge gate). ONLY on an explicit per-PR designer "approved".

The English summary is being prepared. Showing the original description.

me2resh/apexyard538Updated Oct 26, 2026

See all skills by me2resh

Report a problem with this skill