Technical Specifications

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 have font-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 activeSlideId as a prop
  • Render an SVG with viewBox="0 0 640 360"
  • Use SlideSystem.show(activeSlideId) for all progressive reveal logic
  • Include data-element-id on all testable <g> groups
  • Use TSX component patterns from src/lib/deck/patterns/

The contract.ts must:

  • Export a contract object with a slides array
  • Each slide entry has id, visibleElements, and hiddenElements
  • 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