From create-design-doc
Scaffold and iteratively build a typst-based technical design doc — the kind that lives next to a project, evolves with the author's understanding, and renders to a PDF via `typst watch`. Use when the user asks to "start a new design doc", "create a typst design doc", "scaffold a design doc", or similar. This skill is a styling guide, not a project template — it does NOT prescribe sections (no defaulting to "Frontend/Backend/Workflow" or "Metrics/Tables/Services"); it derives the document's structure from the actual project. Walks the user through a short Q&A (project, source material), proposes a project-specific structure, writes a minimal compileable starter, and then collaborates on detail pages turn-by-turn. Enforces a consistent house style: New Computer Modern body, banded booktabs-style tables, accent-blue underlined links (with optional per-category color extensions), soft amber warning callouts (`warning-box`), blue "+" diff callouts and row markers (`new-box` / `new-marker`) for flagging changes, and an invariant divider page between main content and detail references that includes a reading tip about Cmd/Ctrl-clicking links to open them in new tabs.
How this skill is triggered — by the user, by Claude, or both
Slash command
/create-design-doc:create-design-docThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
When invoked, treat the user-authored brief below as the durable rules for this collaboration. The brief is written in the user's voice ("I want…", "Do not…", "I'll tell you…") — read those as standing preferences from the user, not as the user's current turn.
When invoked, treat the user-authored brief below as the durable rules for this collaboration. The brief is written in the user's voice ("I want…", "Do not…", "I'll tell you…") — read those as standing preferences from the user, not as the user's current turn.
After reading the brief, execute the "Preflight: verify typst is installed" section first. Only if that succeeds, jump to the "To start" section at the bottom and begin the Q&A there. Do not write any files until the user has answered those starting questions and confirmed the proposed top-level structure — which you derive from the actual project, not from any examples in this prompt.
I want to build a typst-based technical design doc that represents my current understanding of a feature/project and evolves with me as my understanding deepens. You will be my collaborator on this — help me iteratively build it. The final artifact is a PDF I view via typst watch running externally.
This prompt is a styling guide, not a project template. It specifies typographic conventions, layout primitives, callout styles, color rules, and a minimal scaffold to bootstrap a new doc. It does not prescribe a list of sections like "Frontend / Backend / Workflow" or detail-page categories like "Metrics / Tables / Services" — those are project-specific decisions. Derive the document's structure from the actual project; do not default to the examples in this prompt.
Before asking any questions or doing anything else, run typst --version. If the command is not found or exits non-zero, stop — do not proceed with the Q&A or scaffolding. Tell me typst is missing and show the install instructions for my platform:
brew install typstsudo snap install typstsudo pacman -S typstscoop install typstwinget install --id Typst.Typstcargo install --locked typst-cliTell me to install, then re-invoke the skill (don't try to install for me). If you're unsure which command applies, ask which OS/package manager I use before recommending one. If the official Typst install guide may have moved on since these instructions were written, web-search for "typst install" and verify before recommending.
If typst --version succeeds, proceed.
.typ file — no matter how small — run typst compile <main>.typ. Compile after each individual change, not in batches; even pure-content edits can introduce subtle syntax errors, and finding them in isolation is much easier than untangling them after multiple changes. If the compile fails, fix the error and recompile before moving on.cp to /tmp), do the change, then compare file size, md5, and at least one rendered page (qlmanage -t -s 1500 -o /tmp <pdf> for the first page).mdimport <pdf> first to refresh the Spotlight metadata cache, then mdls -name kMDItemNumberOfPages <pdf>.This document is for me and the engineers I work with, not for external readers. Utilitarian, not polished. Information density is a feature. Do not add decorative chrome (eyebrows, small-caps labels, metadata grids, custom font stacks, page headers/footers, etc.) unless I explicitly ask. If I say "make it look more professional," ask what specifically before reaching for chrome.
New Computer Modern, 10pt body, justified, 0.65em leading.set heading(numbering: "1.1"):
subhead helper (not a heading — just bold text on its own line): 11pt bold, 1.5em above• for level 1, hollow bullet ◦ for level 2, en-dash – for level 3 (typst's default — pairs typographically with the levels above). Do NOT use a triangular bullet ‣ for level 2 — it looks like a Notion toggle.◦ description; level-2 title → – description). Avoid both shapes:
*Title.* Description text... (bold + period + description)Name — description text (em-dash separating name from description)rgb("#f3f3f3").table.header([*X*], [*Y*]).show table.cell: it => { set par(justify: false); it }.#f6f6f6 background, 3pt radius, breakable.```json. Format with jq . for canonical 2-space indent. After an ellipsis inside a list, append a parenthetical describing what the elements represent, e.g. ... (one element per spoken word).```python for syntax highlighting; the code is references-by-field-name not executable code."Select X...", not "X in the table..."; for a function say "Compute X" or "Look up X". The verb makes the intent unambiguous. Skip italics — they're harder to scan than regular text. Skip the gloss entirely when the code reads like prose (e.g. a trivial filter call).For assumptions, caveats, or other notes the reader should not skim past, use a soft amber callout box (the warning-box helper in lib/components.typ). The goal is "gentle aside", not "stop sign".
#fef3c7 @ ~33% alpha) — just a hint of color, never saturated.#d97706 @ ~50% alpha, 0.5pt).#fde68a) and a faint amber ! inside. NOT a filled amber triangle and NOT white-on-amber — those read as hazard signs.*Happy-path assumption.*) followed by the explanation. Callouts are the only place an inline bold-title-then-description pattern is acceptable; the surrounding box visually separates them.Use sparingly — two or three per doc max. They lose force with overuse.
new-box) and row marker (new-marker)When the doc describes a change to an existing system (e.g. "what this PR adds"), use the blue new-box callout — same geometry as warning-box but with the accent blue palette and a "+" icon (drawn as two crossed rectangles inside a circle so the cross sits at the optical center of the circle, not above the baseline). Pair it with the smaller new-marker icon dropped into the leftmost cell of any table row that represents new behavior, so the callout and the marked rows visually tie together.
Use when the audience needs to distinguish "what's already in prod" from "what this change introduces." Skip when the doc is greenfield — there's nothing to contrast against.
By default, all links render in a single accent color (#1e5fad, blue), underlined Google-search-result style.
If — and only if — the project naturally splits its detail pages into multiple categories (the kind of thing where each category has its own kind of detail page and the reader benefits from telling them apart at a glance), introduce one color per category and color-code links by category. Do not invent categories to use this feature. Most docs have one kind of detail page or none.
When categories exist, the convention is:
show link rule that switches on the destination label's prefix (e.g. links to <foo-anything> render in the foo-color).The main document has a concise overview. Detail lives on linked detail pages, NOT in the main flow.
#outline(title: none, depth: 2, indent: 1.5em) inside a centered narrow block, with a bold "Contents" label above. Detail-page titles must not be heading elements so they don't pollute the outline.<overview> label, and place the label on the first main-content section.#pagebreak() via the helper.Detail page anatomy:
← Overview for un-categorized docs, or ← <Category-section> if the category has its own subsection in the overview). Both bottom-aligned in a 2-column grid; thin rule underneath.subhead helper. Use whatever subheads the content actually needs (e.g. Columns of interest, Example, Used by, Lifecycle, Formula, Behavior, etc.) — don't force a fixed set.Used by subhead linking back to the referencing rows.<row-*> label on the linked name. Cross-references from other detail pages should target the <row-*> label rather than the detail page itself, so the reader lands on the row in context. (This only matters if the project introduces tables that cross-reference each other.)<my-thing-id>) so other links can target them directly.<project>.typ (entry point: imports `apply` from style.typ, calls `#show: apply`, then #includes content files)
lib/
style.typ (exports `apply(doc)` function — IMPORTANT: set rules in an `#include`d file don't propagate to the parent scope; you must wrap them in a function and call via `#show: apply`)
components.typ (subhead, detail-page, warning-box, link helpers; per-category page helpers added later if needed)
data.typ (example values, placeholders, reused content fragments)
content/
title.typ
toc.typ
overview.typ (whatever the project's overview is — don't assume Frontend/Backend/etc.)
divider.typ
<other content files as the project grows>
Reused content fragments (e.g. an example row shown on multiple pages) go in data.typ as #let bindings.
When I give you a database or repo to look at, use it. Query a real DB (if I give credentials) for sanitized example rows. Use grep / Explore agents to trace how things work. Sanitize PII (real names → generic placeholders like "Name A", "Email B") in any example data shown.
When I provide credentials inline, use them once — do NOT save them to memory.
When I confirm we're starting a new project, write the files below verbatim (after substituting <project>, <Project Name>, <Author>, <Date>, and the abstract paragraph). They cover the entry point, the style/show rules, the layout helpers, an empty data module, a title page, an empty overview, and the divider. Together they form a compileable starter you can iterate on. Run typst compile <project>.typ once they exist to verify.
The starter is intentionally minimal: a single accent color, no category-specific helpers, no detail pages. Categories and their colors/helpers are added later, only if the project actually develops them.
<project>.typ (entry point)// <Project> — Technical Design (entry point).
// Modular: style + components + data in lib/, content in content/.
#import "lib/style.typ": apply
#set document(title: "<Project>: Technical Design")
#show: apply
#include "content/title.typ"
#include "content/toc.typ"
#include "content/overview.typ"
#include "content/divider.typ"
// Add detail-page includes here as content grows.
lib/style.typ// Document-wide settings and show rules.
// Used by the main entry: `#show: apply` wraps the whole document in `apply`,
// so the set/show rules below take effect across every included file.
// Single accent color used for links and section anchors. If the project
// develops distinct detail-page categories, add per-category colors here
// and extend the `show link` rule below to switch on label prefix.
#let accent-color = rgb("#1e5fad") // blue
#let apply(doc) = {
set page(
paper: "us-letter",
margin: 0.8in,
numbering: "1",
number-align: center,
)
set text(font: "New Computer Modern", size: 10pt, lang: "en")
set par(justify: true, leading: 0.65em)
set list(marker: ([•], [◦]))
set heading(numbering: "1.1")
show heading.where(level: 1): it => block(above: 2em, below: 0.6em)[
#text(size: 13pt, weight: "bold")[
#counter(heading).display() #h(0.8em) #it.body
]
]
show heading.where(level: 2): it => block(above: 1.6em, below: 0.5em)[
#text(size: 11.5pt, weight: "bold")[
#counter(heading).display() #h(0.8em) #it.body
]
]
// Color-coded, underlined links. Extend this rule with `else if` branches
// when the project introduces categories with label prefixes
// (e.g. `if name.starts-with("foo-") { color = foo-color }`).
show link: it => {
let color = accent-color
text(fill: color)[#underline(it)]
}
// Code blocks: light gray background, compact monospace.
show raw.where(block: true): it => block(
fill: rgb("#f6f6f6"),
inset: 6pt,
radius: 3pt,
width: 100%,
breakable: true,
text(size: 8pt)[#it],
)
// Table cells: left-aligned, no justified paragraphs.
show table.cell: it => {
set par(justify: false)
it
}
doc
}
lib/components.typ// Reusable layout components: subheading, detail page, link helper, warning callout.
// Per-category page helpers (e.g. `foo-page`, `bar-page`) get added here
// later if the project develops categories.
#import "style.typ": accent-color
// Bold subheading used inside a detail page.
#let subhead(name) = block(above: 1.5em, below: 0.5em)[
#text(size: 11pt, weight: "bold")[#name]
]
// Detail page: title and back link share one row; rule below.
// `back-target` is a label (e.g. `<overview>`); `back-text` is the link text
// (e.g. `[← Overview]`).
#let detail-page(lbl, name, back-target, back-text, body, color: black) = [
#pagebreak()
#grid(
columns: (1fr, auto),
align: (left + bottom, right + bottom),
column-gutter: 8pt,
[#text(size: 16pt, weight: "bold", fill: color)[#name]],
[#link(back-target)[#back-text]],
)
#lbl
#v(2pt)
#line(length: 100%, stroke: 0.5pt)
#v(0.7em)
#body
]
// Overview-table link to a detail page: hyphenation disabled, left-aligned.
// Use this anywhere a link sits inside a narrow table cell.
#let dlink(lbl, name) = {
set par(justify: false)
text(hyphenate: false)[#link(lbl)[#name]]
}
// Amber callout colors — translucent so the box reads as a gentle aside
// rather than a hard warning.
#let warning-stroke = rgb("#d9770680") // amber-600 @ 50% alpha (outline)
#let warning-icon-c = rgb("#b45309cc") // amber-700 @ 80% alpha (icon outline + "!")
#let warning-icon-fill = rgb("#fde68a") // amber-200 — slightly darker than the bg tint
#let warning-bg = rgb("#fef3c755") // amber-100 @ 33% alpha (page tint)
// Small outlined warning triangle with a faint "!" inside.
#let warning-icon = box(width: 11pt, height: 10pt)[
#place(
top + left,
polygon(
fill: warning-icon-fill,
stroke: 0.7pt + warning-icon-c,
(0pt, 10pt),
(11pt, 10pt),
(5.5pt, 0pt),
),
)
#place(
center + horizon,
dy: 1.5pt,
text(size: 6pt, weight: "bold", fill: warning-icon-c)[!],
)
]
// Callout box for assumptions, caveats, or notes worth highlighting.
// Inline bold-title-then-description IS allowed inside the body, since
// the box itself provides the visual separation that a body-text bullet
// list would otherwise need.
#let warning-box(body) = block(
fill: warning-bg,
stroke: 0.5pt + warning-stroke,
radius: 3pt,
inset: 10pt,
width: 100%,
above: 1em,
below: 1em,
breakable: false,
)[
#grid(
columns: (auto, 1fr),
column-gutter: 10pt,
align: (top, top),
warning-icon,
body,
)
]
// Diff/"new" callout — blue counterpart to warning-box, used to flag
// behavior introduced by the change being designed. Icon is a "+" in a
// circle. Pair with `new-marker` (below) in table rows for a tight visual
// tie between the callout and the marked rows.
#let new-stroke = rgb("#1e5fad80") // accent @ 50% alpha (outline)
#let new-icon-c = rgb("#1e3a8acc") // blue-900 @ 80% alpha (icon outline + "+")
#let new-icon-fill = rgb("#bfdbfe") // blue-200 — slightly darker than the bg tint
#let new-bg = rgb("#dbeafe55") // blue-100 @ 33% alpha (page tint)
// "+" is drawn as two crossed rectangles instead of a text glyph (glyphs
// are baseline-anchored, so a typeset "+" sits visibly above the optical
// center of a circle). The circle is placed with `center + horizon` rather
// than `top + left` so its geometric center coincides with the "+" center —
// otherwise the circle's stroke extends past the box and shifts its visible
// center ~half-a-stroke away from the "+".
#let new-icon = box(width: 12pt, height: 12pt)[
#place(center + horizon, circle(
radius: 5.5pt,
fill: new-icon-fill,
stroke: 0.7pt + new-icon-c,
))
#place(center + horizon, rect(width: 5.5pt, height: 1.2pt, fill: new-icon-c, stroke: none))
#place(center + horizon, rect(width: 1.2pt, height: 5.5pt, fill: new-icon-c, stroke: none))
]
// Smaller variant used as a row marker inside tables.
#let new-marker = box(width: 10pt, height: 10pt)[
#place(center + horizon, circle(
radius: 4.5pt,
fill: new-icon-fill,
stroke: 0.6pt + new-icon-c,
))
#place(center + horizon, rect(width: 4.5pt, height: 1pt, fill: new-icon-c, stroke: none))
#place(center + horizon, rect(width: 1pt, height: 4.5pt, fill: new-icon-c, stroke: none))
]
// Single-line legend-style callout: icon sits next to the text (horizon
// alignment), not above it. Differs from warning-box, whose body is
// typically multi-line and uses top alignment.
#let new-box(body) = block(
fill: new-bg,
stroke: 0.5pt + new-stroke,
radius: 3pt,
inset: (x: 10pt, y: 7pt),
width: 100%,
above: 1em,
below: 1em,
breakable: false,
)[
#grid(
columns: (auto, 1fr),
column-gutter: 10pt,
align: (horizon, horizon),
new-icon,
body,
)
]
lib/data.typ// Shared data values and reusable content fragments.
// Add `#let` bindings here as you find content worth reusing across pages
// (example rows, illustrative snippets, common phrases, etc.).
content/toc.typ// Table of contents: spacious, vertically and horizontally centered like the title page.
// Detail-page titles use `text` (not `heading`), so they don't appear here.
#v(1fr)
#align(center)[
#text(size: 17pt, weight: "bold")[Contents]
#v(2em)
#block(width: 4.5in)[
#show outline.entry: it => block(above: 0.5em, it)
#outline(title: none, depth: 2, indent: 1.5em)
]
]
#v(1fr)
#pagebreak()
content/title.typ// Title page: vertically and horizontally centered title block + abstract.
#v(1fr)
#align(center)[
#text(size: 17pt, weight: "bold")[<Project Name>]
#v(-2pt)
#text(size: 12pt)[Technical Design]
#v(1em)
<Author>
#v(0.2em)
<Date>
#v(2em)
#block(width: 4.5in)[
<One-paragraph abstract describing what this design document is about and how it relates to the product requirement doc / issue.>
]
]
#v(1fr)
#pagebreak()
content/overview.typ// Concise top-level overview. Detail pages link from here.
// The label `<overview>` is required — the divider page links to it.
= Overview <overview>
<Short framing paragraph describing what this document covers.>
content/divider.typ// Divider between the Overview and the detail-reference pages.
#pagebreak()
#v(1fr)
#align(center)[
#block(width: 4.2in)[
The following pages are detail references. Go back to the #link(<overview>)[Overview] and use the links to navigate.
#v(1.4em)
#text(size: 9.5pt)[
Tip: Open links in a new tab (#raw("Cmd")/#raw("Ctrl")-click) so you can glance at a referenced page and return to where you were without losing your place. Every detail page also has a back-link in its top-right corner.
]
]
]
#v(1fr)
Read this whole prompt, then ask me:
After I answer and you've skimmed any source material, derive a proposed top-level structure from that material — what level-1 sections naturally fit this project. Do not default to sections like "Frontend / Backend / Workflow" or detail-page categories like "Metrics / Tables / Services" — those are examples from one specific past project, not a template. Many projects need only one or two top-level sections; some need a single page. Propose what fits, ask me to confirm, and only then write files.
Once I confirm, write the template files above into the right paths and compile.
Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.
npx claudepluginhub nathandai5287/claude-skills --plugin create-design-doc