Technical Rules for Deck Visuals
What this file covers: TSX component requirements, SVG viewport, data-element-id, animations, fixture testing, color format.
Deck Visual Requirements (Quick Reference)
| Rule | Requirement |
|---|---|
| Viewport | viewBox="0 0 640 360" (640x360 SVG) |
| Component | React TSX component in src/content/scm/.../lesson-N-sgi-deck/visual.tsx |
| Fonts | Prefer web-safe fonts (Arial, Georgia, Helvetica, Verdana, etc.) |
| Layout | TSX component patterns: TitleZone, ContentBox, CfuAnswerCard, etc. |
| Text | All text in proper SVG <text> elements or TSX component children |
| Reveal | SlideSystem.show(activeSlideId) returns a predicate for progressive reveal |
| Animations | <Animated segment="step-1" isVisible={s => show(s)} entrance="fade" /> |
| Fixtures | data-element-id on SVG <g> groups for visibility testing |
| Theme | Default to light theme; subtle gradients and colored backgrounds are allowed |
Progressive Reveal System
The show() Predicate
The show() function returned by SlideSystem.show(activeSlideId) is the core mechanism for progressive reveal. It takes a slide ID and returns true if that slide has been reached.
const show = SlideSystem.show(activeSlideId);
// Content appears on "step-1" and stays visible on all subsequent slides
{show("step-1") && <g data-element-id="step-1-content">...</g>}
Key property: show() is monotonic. Once show("step-1") returns true, it returns true for all subsequent slide IDs. Content accumulates and is never removed.
The <Animated> Component
Use <Animated> to add entrance animations when content first appears:
<Animated segment="step-1" isVisible={s => show(s)} entrance="fade">
<g data-element-id="step-1-content">
{/* Step 1 visual content */}
</g>
</Animated>
Entrance types: "fade", "slide-up", "slide-left", "scale"
CfuAnswerCard
CFU and Answer are separate slide IDs, not same-position overlays:
<CfuAnswerCard
showCfu={show("step-1-cfu")}
showAnswer={show("step-1-answer")}
question="Why did I [VERB] first?"
answer="Because [explanation]."
/>
"step-1-cfu": Reveals the CFU question (yellow card)"step-1-answer": Reveals the answer (green card)- Both are separate, non-overlapping cards
Fixture Testing with data-element-id
Every visual element that participates in progressive reveal must have a data-element-id attribute on its <g> wrapper. This enables fixture-based visibility testing.
Adding Element IDs
<g data-element-id="problem-setup-visual">
{/* Problem setup diagram */}
</g>
<g data-element-id="step-1-annotation">
{/* Step 1 annotation on the diagram */}
</g>
<g data-element-id="step-2-highlight">
{/* Step 2 highlighting */}
</g>
Contract and Fixture Structure
The contract.ts file defines which elements are visible and hidden for each slide ID:
export const contract = {
slides: [
{
id: "title",
visibleElements: ["title-content"],
hiddenElements: ["problem-setup-visual", "step-1-content", "step-1-annotation"],
},
{
id: "problem-setup",
visibleElements: ["title-content", "problem-setup-visual"],
hiddenElements: ["step-1-content", "step-1-annotation"],
},
{
id: "step-1",
visibleElements: ["title-content", "problem-setup-visual", "step-1-content"],
hiddenElements: ["step-1-annotation"],
},
// ... more slide IDs
],
};
Writing Fixture Tests (contract.test.ts)
import { contract } from "./contract";
describe("visual contract", () => {
for (const slide of contract.slides) {
describe(`slide: ${slide.id}`, () => {
for (const el of slide.visibleElements) {
it(`shows ${el}`, () => {
// Render visual with activeSlideId = slide.id
// Assert element with data-element-id={el} is visible
});
}
for (const el of slide.hiddenElements) {
it(`hides ${el}`, () => {
// Render visual with activeSlideId = slide.id
// Assert element with data-element-id={el} is not visible
});
}
});
}
});
Element ID Naming Convention
| Element Type | Pattern | Example |
|---|---|---|
| Title content | title-content |
Big Idea badge + statement |
| Problem setup | problem-setup-visual |
Initial diagram/graph |
| Step content | step-N-content |
Step N main content |
| Step annotation | step-N-annotation |
Annotation added at step N |
| Step highlight | step-N-highlight |
Highlighting at step N |
| Practice content | practice-N-content |
Practice problem N |
| CFU card | step-N-cfu-card |
CFU card for step N |
| Answer card | step-N-answer-card |
Answer card for step N |
Color Format (CRITICAL)
ALWAYS use 6-digit hex colors. NEVER use rgb(), rgba(), hsl(), or named colors.
| CORRECT | WRONG |
|---|---|
#ffffff |
white |
#1d1d1d |
rgb(29, 29, 29) |
#f59e0b |
rgba(245, 158, 11, 1) |
#000000 |
black |
Why? Consistency across the visual system and reliable rendering in all environments.
For shadows: Use a simple border or filter instead of box-shadow. Keep visual effects minimal.
SVG-Specific Requirements
For SVG visuals, additional rules apply:
Viewport
All deck visuals use a 640x360 SVG viewport:
<svg viewBox="0 0 640 360" width="640" height="360">
{/* All visual content */}
</svg>
Text in SVG
- ALL
<text>elements must havefont-family="Arial" - Use
font-weight="normal"for annotations (NOT bold)
Label Placement Rules (PREVENTS OVERLAPS)
The #1 cause of ugly SVG diagrams is labels overlapping with shapes or each other. Follow these rules to prevent overlaps:
| Scenario | text-anchor |
X Offset | Y Offset | Why It Works |
|---|---|---|---|---|
| Label RIGHT of point/shape | start |
+8px | 0 | Text grows rightward, away from element |
| Label LEFT of point/shape | end |
-8px | 0 | Text grows leftward, away from element |
| Label ABOVE element | middle |
0 | -10px | Text centered, positioned above |
| Label BELOW element | middle |
0 | +16px | Text centered, positioned below (accounts for text height) |
| Label INSIDE large shape (>60px) | middle |
centered | centered | Only when shape is large enough |
Quadrant Rules for Coordinate Graphs:
- Points in upper-right quadrant: Label BELOW-LEFT (
text-anchor="end", dy=+12) - Points in upper-left quadrant: Label BELOW-RIGHT (
text-anchor="start", dy=+12) - Points in lower-right quadrant: Label ABOVE-LEFT (
text-anchor="end", dy=-8) - Points in lower-left quadrant: Label ABOVE-RIGHT (
text-anchor="start", dy=-8) - Points near axes: Always place label AWAY from the axis
Example - Label to the RIGHT of a circle (text grows away):
<circle cx="100" cy="50" r="5" fill="#60a5fa" />
<text x="108" y="54" text-anchor="start" font-family="Arial" font-size="11"
>(4, 20)</text
>
Example - Label to the LEFT of a circle:
<circle cx="100" cy="50" r="5" fill="#60a5fa" />
<text x="92" y="54" text-anchor="end" font-family="Arial" font-size="11"
>(4, 20)</text
>
Example - Label BELOW a circle:
<circle cx="100" cy="50" r="5" fill="#60a5fa" />
<text x="100" y="70" text-anchor="middle" font-family="Arial" font-size="11"
>(4, 20)</text
>
See technical-specs/svg-workflow.md for coordinate graph SVG rules.
Available Layout Classes
These utility classes are available for convenience within TSX components. You may also use inline CSS flexbox or grid.
| Class | Purpose |
|---|---|
.row |
Horizontal flex container |
.col |
Vertical flex container |
.center |
Center content |
.items-center |
Align items center |
.gap-sm |
Small gap (8px) |
.gap-md |
Medium gap (16px) |
.fit |
Fit content width |
File Structure Requirements
Each deck visual must consist of these files:
src/content/scm/<grade>/<unit>/<section>/lesson-N-sgi-deck/
├── contract.ts ← Slide IDs + visible/hidden element mappings
├── visual.tsx ← React TSX component rendering 640x360 SVG
├── contract.test.ts ← Fixture tests for element visibility per slide ID
├── lesson-N-sgi-deck.kc.json ← KC pairing (slide IDs → knowledge components)
└── docs/
├── analysis.json ← Phase 1 output (for resume across conversations)
├── research.md ← Primitive research + layout plan
└── plan.md ← Slide-by-slide spec
The visual.tsx component must:
- Export a default React component that accepts
activeSlideIdas a prop - Render an SVG with
viewBox="0 0 640 360" - Use
SlideSystem.show(activeSlideId)for all progressive reveal logic - Include
data-element-idon all testable<g>groups - Use TSX component patterns from
src/lib/deck/patterns/
The contract.ts must:
- Export a
contractobject with aslidesarray - Each slide entry has
id,visibleElements, andhiddenElements - Cover every slide ID in the visual
The contract.test.ts must:
- Import and iterate over the contract
- Test that each slide ID shows/hides the correct elements