feat: add baoyu-diagram skill and baoyu-article-illustrator ink-notes style (v1.99.0)

- baoyu-diagram: new skill for generating publication-ready SVG diagrams (flowchart, structural, illustrative) with embedded styles and auto dark-mode
- baoyu-article-illustrator: add ink-notes style and mono-ink palette with presets for before/after, manifesto, and framework diagrams
This commit is contained in:
Jim Liu 宝玉
2026-04-11 20:54:32 -05:00
parent 31b2929d1c
commit b62ad26098
32 changed files with 6040 additions and 2 deletions
+360
View File
@@ -0,0 +1,360 @@
# Class Diagram (UML)
For showing static structure: what types exist, what they contain, and how they relate. The classic UML class diagram, adapted to baoyu-diagram's flat-rect design system.
## When to use
- "Draw the class diagram for X" / "show me the UML"
- "What are the types and how are they related"
- "Show the inheritance hierarchy"
- "Diagram the domain model / schema / type system"
- "Interface + implementations"
**When not to use:**
- If the user wants to see *behavior* (what happens when X calls Y), use sequence (`sequence.md`).
- If the user wants to see *data storage* with foreign keys and cardinalities only, that's an ER diagram — baoyu doesn't do ERDs in v1, suggest mermaid.
- If the user wants to see *runtime object instances* with field values, you're drawing an object diagram — use the class template with `th`-underlined instance names (`underline` class not in the template; use `text-decoration="underline"` inline on `<text>`) and skip the method compartment.
## Planning
Before writing SVG:
1. **List the classes.** 38 total. More than 8 and you should split into multiple diagrams grouped by package or subsystem.
2. **For each class, list attributes and methods.** Keep each line ≤30 chars. If a signature doesn't fit, abbreviate.
3. **Pick the visibility for each member.** `+` public, `` private, `#` protected, `~` package-private.
4. **List the relationships.** Who extends whom, who implements which interface, who has what.
5. **Pick colors by kind of class**, not by position. See "Color rule" below.
6. **Compute box widths** using the class-specific formula below.
7. **Lay out the classes** — parents / interfaces on top, children below. See "Layout" below.
## Class box template
Each class is a **3-compartment rect**: name (top), attributes (middle), methods (bottom). Horizontal divider lines separate the compartments.
### Geometry
| Element | Value |
|------------------------|-----------------------------------------------|
| Rect corner radius | `rx="6"` |
| Min width | 160 px |
| Name compartment height | 32 px (single line, centered) |
| Attribute/method row height | 18 px per line + 8 px top/bottom padding |
| Horizontal divider | `<line>` using `arr` class at 0.5 stroke |
### Width formula
The box width must fit the longest line across all three compartments:
```
longest = max(name_chars × 8,
max(attribute_line_chars) × 7,
max(method_line_chars) × 7)
width = longest + 24
```
- 8 px/char for 14px `th` (the class name)
- 7 px/char for 12px `ts` (attributes and methods — they use the subtitle class for compact line height)
Round up to the nearest 10. Minimum width is 160 regardless of label length.
### Height formula
```
name_h = 32
attr_h = 16 + (attribute_count × 18)
method_h = 16 + (method_count × 18)
total_h = name_h + attr_h + method_h
```
If the class has no attributes, show an empty attribute compartment with `h = 16`. Never collapse a compartment — readers expect all three.
### SVG template
```svg
<g class="c-teal">
<!-- outer rect spans all three compartments -->
<rect x="80" y="60" width="200" height="144" rx="6"/>
<!-- name compartment (top) -->
<text class="th" x="180" y="80" text-anchor="middle" dominant-baseline="central">User</text>
<!-- divider between name and attributes -->
<line class="arr" x1="80" y1="92" x2="280" y2="92" stroke-width="0.5"/>
<!-- attributes (12px, left-aligned) -->
<text class="ts" x="92" y="110">+ id: UUID</text>
<text class="ts" x="92" y="128">+ email: string</text>
<text class="ts" x="92" y="146"> passwordHash: string</text>
<!-- divider between attributes and methods -->
<line class="arr" x1="80" y1="158" x2="280" y2="158" stroke-width="0.5"/>
<!-- methods (12px, left-aligned) -->
<text class="ts" x="92" y="176">+ login(pwd): Token</text>
<text class="ts" x="92" y="194">+ logout(): void</text>
</g>
```
- Name is `th` 14px centered.
- Attributes and methods are `ts` 12px, **left-aligned at `rect_x + 12`**.
- Divider lines use `class="arr"` with inline `stroke-width="0.5"` (the default 1.5 is too heavy for an internal divider).
- All text inside the class inherits the ramp's text color from the `c-{ramp}` wrapper — no hardcoded fills.
### Stereotypes
For interfaces, abstract classes, and enums, prepend a stereotype line inside the name compartment:
```svg
<g class="c-purple">
<rect x="80" y="60" width="200" height="120" rx="6"/>
<text class="ts" x="180" y="78" text-anchor="middle" dominant-baseline="central">«interface»</text>
<text class="th" x="180" y="94" text-anchor="middle" dominant-baseline="central">Drawable</text>
<line class="arr" x1="80" y1="106" x2="280" y2="106" stroke-width="0.5"/>
...
</g>
```
The stereotype line is 12px `ts`, italic is encoded via the guillemets `«...»` characters — do not use inline `font-style="italic"` since baoyu's typography table doesn't include an italic class for `ts` outside poster captions.
When you add a stereotype, bump the name compartment height to 48 to fit both lines.
Common stereotypes:
- `«interface»` — interface type, no fields, method signatures only.
- `«abstract»` — abstract class with at least one abstract method.
- `«enumeration»` — enum with value constants in the attribute compartment.
- `«datatype»` — value type with no behavior (rare; usually just a plain class).
Abstract class names may optionally use the existing `th` weight — baoyu does not render italics for abstract, the stereotype carries the meaning.
## Relationship arrows
Six UML relationship styles. Each maps to a specific arrow rendering. All connectors use `class="arr"` (or `arr-alt` for dashed variants) so they inherit dark-mode stroke colors.
| Relationship | Line style | Arrowhead | From | Typical label |
|------------------|------------|-------------------|---------------------|-------------------------------------|
| Inheritance | solid | hollow triangle | child → parent | (none) |
| Implementation | dashed | hollow triangle | class → interface | (none) |
| Association | solid | open arrow `>` | from → to | multiplicity `1`, `0..*`, `1..*` |
| Aggregation | solid | hollow diamond | container → part | (none, diamond on container side) |
| Composition | solid | filled diamond | container → part | (none, diamond on container side) |
| Dependency | dashed | open arrow `>` | user → used | optional label like `«uses»` |
### Arrow head definitions (add to `<defs>`)
The template's default `<marker id="arrow">` renders an open arrow — that works for association and dependency. For inheritance/implementation/aggregation/composition you need four additional markers. Add these to `<defs>` **only if the diagram uses them**:
```svg
<defs>
<!-- existing open-arrow marker from the template -->
<marker id="arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M2 1L8 5L2 9" fill="none" stroke="context-stroke" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</marker>
<!-- hollow triangle for inheritance + implementation -->
<marker id="triangle-hollow" viewBox="0 0 12 12" refX="11" refY="6" markerWidth="10" markerHeight="10" orient="auto">
<path d="M1 1 L11 6 L1 11 Z" fill="none" stroke="context-stroke" stroke-width="1.2" stroke-linejoin="round"/>
</marker>
<!-- hollow diamond for aggregation -->
<marker id="diamond-hollow" viewBox="0 0 14 10" refX="1" refY="5" markerWidth="12" markerHeight="8" orient="auto">
<path d="M1 5 L7 1 L13 5 L7 9 Z" fill="none" stroke="context-stroke" stroke-width="1.2" stroke-linejoin="round"/>
</marker>
<!-- filled diamond for composition -->
<marker id="diamond-filled" viewBox="0 0 14 10" refX="1" refY="5" markerWidth="12" markerHeight="8" orient="auto">
<path d="M1 5 L7 1 L13 5 L7 9 Z" fill="context-stroke" stroke="context-stroke" stroke-width="1" stroke-linejoin="round"/>
</marker>
</defs>
```
**Why `fill="none"` instead of a hardcoded hex.** Hollow markers need to stay hollow in both light and dark mode, and any hardcoded hex color is frozen at author time — it can't respond to the template's `@media (prefers-color-scheme: dark)` block. `fill="none"` means the triangle/diamond is outlined only and the interior is transparent. At 1.2px stroke on a 10×10 marker, the 1.5px line passing behind the marker is invisible to the reader. For the filled composition diamond, `fill="context-stroke"` picks up whichever stroke color the attached line is using, so it inherits dark-mode correctly through the `class="arr"` rules.
### Using the markers
```svg
<!-- Inheritance: Circle extends Shape -->
<line x1="260" y1="200" x2="260" y2="150" class="arr" marker-end="url(#triangle-hollow)"/>
<!-- Implementation: Circle implements Drawable (dashed) -->
<line x1="260" y1="200" x2="460" y2="150" class="arr-alt" marker-end="url(#triangle-hollow)"/>
<!-- Association with multiplicity: User → Order (1 to many) -->
<line x1="180" y1="120" x2="380" y2="120" class="arr" marker-end="url(#arrow)"/>
<text class="ts" x="200" y="112">1</text>
<text class="ts" x="360" y="112">0..*</text>
<!-- Composition: Order composed of LineItems -->
<line x1="380" y1="140" x2="580" y2="140" class="arr" marker-end="url(#diamond-filled)"/>
```
**Pitfall**: the `marker-end` goes on the **parent** end for inheritance/implementation (the triangle points at the parent), and on the **container** end for aggregation/composition (the diamond sits on the container). Direction of `line`/`path` dictates which end is `start` vs `end` — always draw the arrow *toward* the marker.
## Color rule
**One ramp per kind of class**, not per position. This is the same exception as sequence diagrams — identity is a category, so using multiple ramps is fine, but each ramp must correspond to a stable *role*.
Default ramp assignments:
| Class kind | Ramp | Why |
|-------------------|-----------|---------------------------------------|
| Concrete class | `c-gray` | The default — most classes are here |
| Abstract class | `c-teal` | Signals "not directly instantiable" |
| Interface | `c-purple` | Signals "pure contract, no state" |
| Enum / datatype | `c-amber` | Signals "constants / value type" |
Cap at **3 ramps per diagram**. If you have concrete classes + interfaces + enums, use gray + purple + amber. If every class is concrete, all gray is fine and looks cleanest. Do not assign ramps by package or by author — readers won't learn the code.
**Legend implicit**: the ramps are anchored by the class kinds themselves. A reader who sees a purple `«interface» Drawable` box learns the convention immediately. No explicit legend needed unless the diagram uses >3 ramps (which shouldn't happen).
## Layout
Class diagrams are **2D grid** layouts, not linear flows. Parents live above children, interfaces sit on the side of their implementors, and association arrows run horizontally.
### Default: top-down inheritance tree
Parents / interfaces on top, children at the bottom:
```
[ «interface» Drawable ] [ Shape (abstract) ]
| |
| implements | extends
+---------+--------+---------+
| |
[ Circle ] [ Square ]
```
- Tier 1 (y=60): base types (interfaces, abstract base classes)
- Tier 2 (y=240): concrete subtypes
- Tier 3 (y=420): composed types / enums
Vertical gap between tiers: **40 px minimum**.
### 2-wide / 3-wide rows
For 46 classes per tier, pack horizontally with 20px gaps:
```
x=40 x=260 x=480 (width 180 each, gap 20)
```
For >6 classes, you're building a too-big diagram — split by package or subsystem into separate diagrams and link them with prose.
### Association arrows
Association arrows are **horizontal or L-bend**, running between classes in the same tier (or adjacent tiers). Do not let an association arrow cross an inheritance arrow — if they cross, reroute the weaker relationship (usually the association) with an L-bend.
### viewBox height
```
H = tier_count × 200 + 40
```
Where 200 px is the per-tier budget (class box height ~140 + 40 px vertical gap + 20 px margin). Round up after placing the last tier's actual box height — if a class in the last tier is extra-tall, bump H to fit.
Width stays at 680. If a tier of 3-wide boxes doesn't fit at minimum width, narrow the boxes to 180 each (total 540 + 40 gap = 580, centered at x=50).
## Worked example — Shape hierarchy
A small 4-class domain: a `Drawable` interface, an abstract `Shape` class, and two concrete subclasses `Circle` and `Square`.
**Plan:**
1. Classes: `Drawable` (interface), `Shape` (abstract), `Circle`, `Square` — 4 classes, 2 tiers.
2. Attributes/methods:
- `Drawable`: `+ draw(): void`
- `Shape`: `# color: Color`, `# position: Point`, `+ area(): number` (abstract)
- `Circle`: ` radius: number`, `+ area(): number` (override)
- `Square`: ` side: number`, `+ area(): number` (override)
3. Relationships: `Shape` implements `Drawable` (dashed + hollow triangle); `Circle` and `Square` extend `Shape` (solid + hollow triangle).
4. Colors: `Drawable` = `c-purple` (interface); `Shape` = `c-teal` (abstract); `Circle` / `Square` = `c-gray` (concrete). 3 ramps.
5. Widths:
- `Drawable`: name 8 chars, longest method "+ draw(): void" = 14 chars → `max(64, 98) + 24 = 122` → min 160
- `Shape`: name 5 chars, longest "+ area(): number" = 16 chars → `max(40, 112) + 24 = 136` → min 160
- `Circle`/`Square`: ~13 chars longest → min 160
6. Tier 1 (y=60): `Drawable` at x=130, `Shape` at x=390 (row of 2, both width 160, gap 100).
7. Tier 2 (y=260): `Circle` at x=130, `Square` at x=390 (row of 2, width 160, gap 100).
8. Arrows:
- `Shape → Drawable` (implements): horizontal dashed line from Shape's left edge (x=390, y=110) to Drawable's right edge (x=290, y=110).
- `Circle → Shape` (extends): L-bend from Circle's top (x=210, y=260) up to ymid=220, right to x=470, up to Shape's bottom (x=470, y=200).
- `Square → Shape` (extends): straight vertical from Square's top (x=470, y=260) to Shape's bottom (x=470, y=200).
**Coordinates (abbreviated; production code would fill in every `<text>`):**
```svg
<svg xmlns="http://www.w3.org/2000/svg" width="100%" viewBox="0 0 680 420" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', sans-serif">
<style>/* template block ... */</style>
<defs>
<marker id="arrow" .../>
<marker id="triangle-hollow" .../>
</defs>
<!-- Tier 1: Drawable (interface, purple) -->
<g class="c-purple">
<rect x="130" y="60" width="160" height="100" rx="6"/>
<text class="ts" x="210" y="76" text-anchor="middle" dominant-baseline="central">«interface»</text>
<text class="th" x="210" y="92" text-anchor="middle" dominant-baseline="central">Drawable</text>
<line class="arr" x1="130" y1="104" x2="290" y2="104" stroke-width="0.5"/>
<!-- empty attribute compartment -->
<line class="arr" x1="130" y1="120" x2="290" y2="120" stroke-width="0.5"/>
<text class="ts" x="142" y="138">+ draw(): void</text>
</g>
<!-- Tier 1: Shape (abstract, teal) -->
<g class="c-teal">
<rect x="390" y="60" width="160" height="140" rx="6"/>
<text class="ts" x="470" y="76" text-anchor="middle" dominant-baseline="central">«abstract»</text>
<text class="th" x="470" y="92" text-anchor="middle" dominant-baseline="central">Shape</text>
<line class="arr" x1="390" y1="104" x2="550" y2="104" stroke-width="0.5"/>
<text class="ts" x="402" y="122"># color: Color</text>
<text class="ts" x="402" y="140"># position: Point</text>
<line class="arr" x1="390" y1="152" x2="550" y2="152" stroke-width="0.5"/>
<text class="ts" x="402" y="170">+ area(): number</text>
</g>
<!-- Tier 2: Circle (concrete, gray) -->
<g class="c-gray">
<rect x="130" y="260" width="160" height="110" rx="6"/>
<text class="th" x="210" y="280" text-anchor="middle" dominant-baseline="central">Circle</text>
<line class="arr" x1="130" y1="292" x2="290" y2="292" stroke-width="0.5"/>
<text class="ts" x="142" y="310"> radius: number</text>
<line class="arr" x1="130" y1="322" x2="290" y2="322" stroke-width="0.5"/>
<text class="ts" x="142" y="340">+ area(): number</text>
</g>
<!-- Tier 2: Square (concrete, gray) -->
<g class="c-gray">
<rect x="390" y="260" width="160" height="110" rx="6"/>
<text class="th" x="470" y="280" text-anchor="middle" dominant-baseline="central">Square</text>
<line class="arr" x1="390" y1="292" x2="550" y2="292" stroke-width="0.5"/>
<text class="ts" x="402" y="310"> side: number</text>
<line class="arr" x1="390" y1="322" x2="550" y2="322" stroke-width="0.5"/>
<text class="ts" x="402" y="340">+ area(): number</text>
</g>
<!-- Shape implements Drawable (dashed, hollow triangle on Drawable side) -->
<line x1="390" y1="110" x2="290" y2="110" class="arr-alt" marker-end="url(#triangle-hollow)"/>
<!-- Circle extends Shape (solid L-bend) -->
<path d="M 210 260 L 210 230 L 470 230 L 470 200" class="arr" fill="none" marker-end="url(#triangle-hollow)"/>
<!-- Square extends Shape (solid straight) -->
<line x1="470" y1="260" x2="470" y2="200" class="arr" marker-end="url(#triangle-hollow)"/>
</svg>
```
viewBox H: bottom of last box is `260 + 110 = 370`, so `H = 370 + 20 = 390`. The example uses 420 for a 30px bottom margin, which is also fine.
## Checklist (class-specific)
On top of the standard `pitfalls.md` checks:
1. Every class has all **three compartments** (name / attrs / methods), even if one is empty.
2. The class **name is `th` centered**, attributes/methods are **`ts` left-aligned at `rect_x + 12`**.
3. Every attribute/method starts with a **visibility marker** (`+ # ~`).
4. Dividers use `class="arr"` with **inline `stroke-width="0.5"`**.
5. Inheritance and implementation arrows point **toward the parent/interface** (triangle on parent side).
6. Aggregation and composition diamonds sit on the **container side**, not the part side.
7. Associations carry **multiplicity labels** at both ends (`1`, `0..*`, `1..*`) when they aren't obvious.
8. Dashed arrows are `arr-alt`, solid arrows are `arr` — never mix.
9. Color ramp budget ≤3, one per class kind (concrete / abstract / interface / enum).
10. Association arrows **do not cross inheritance arrows** — if they do, re-route the association.