React/Vite and a small Express API share TypeScript packages. This avoids network latency for live generation while keeping a server boundary for export libraries and future authenticated storage. npm scripts run both services and build one local production server. Dependencies are locked in package-lock.json; source packages are bundled into the production API so Node does not need to execute workspace TypeScript directly.
No credentials or external services are used. The local user is explicitly local-development. Repository filtering is convenience, not an access-control claim. A cloud deployment must replace the user provider, enforce authentication on server operations and implement Firestore rules scoped to authenticated user IDs.
ResourceGenerator<C, T> defines metadata, schema, defaults, UI controls, validation, content generation, layout and the validated public generation entry point. The registry provides category/keyword discovery and pinned-version lookup. Current routes and discovery categories derive from its metadata.
Worksheet definitions bind ResourceGenerator<C, T, Q> to typed editor rules through defineWorksheet. WorksheetConfig contains only common educational context and presentation fields; each generator owns its content shape and additional presentation settings. The document model accepts any question structure extending ResourceQuestion. Subtraction's arithmetic fields live in its own package.
Shared schemas and control groups are composable. Each definition explicitly opts into inherited teacher preferences, with optional inheritance from the current worksheet when starting another type. Target defaults and presets remain authoritative for settings it does not inherit. Content constraints are never transferred between types.
The common state layer carries generator ID and version, delegates edits, preparation, presets, summaries and resets to that definition, and persists the identity with drafts. History parsing and API generation/export resolve the registered schema and versions. No shared store, persistence reader or export route requires subtraction fields. A metadata-based settings editor supports new types automatically; the optional richer subtraction editor is isolated under apps/web/src/worksheets.
See Adding worksheet types for the registration workflow and inheritance rules. An executable multiplication fixture tests these boundaries without advertising an unfinished generator in the app.
History records carry generator, schema, renderer and layout versions, full configurations and seed. Reopen rejects unsupported versions; it never silently uses today's defaults or algorithm. Schema 2 adds optional layout-style and row-theme fields. Their absence identifies schema-1 configurations and preserves the original geometry. Reference-style resources without badge numbering use renderer/layout version 2; the educational algorithm remains generator version 1. Schema-1 resources are parsed without filling those fields, so historical configurations and SVG output remain unchanged. Future schema migrations must explicitly preserve originals; historical generator and renderer implementations can be retained under versioned registry entries when needed. Font assets and metrics are vendored. Changing them can require a renderer/layout version bump.
Schema 3 adds optional presentation.layout.questionNumberStyle (plain or badge). Absence retains the historical plain-number geometry; parsers never add this field to old records. Configurations using badges without independent row colours use layout/renderer version 3 in both classic and reference layouts. Explicit plain numbering retains the original layout/renderer versions. Shared badge geometry reserves space using the full question count and bold font metrics; semantic rectangle roles let preview and every export identify badges separately from row panels. Schema-1 and schema-2 output is frozen in compatibility fixtures.
Schema 4 adds optional presentation.appearance.colouredRows, independently of layout style. New defaults include it; both layouts render pastel panels when true and omit panels when false, without changing ink, content, seed or row geometry. Classic layout version 4 reserves padding around each full row and keeps answer boxes white. When the field is absent, the original interpretation of rowTheme, geometry and SVG remain unchanged for schemas 1–3. Only an explicit layout edit materialises the existing visible row-colour choice in the draft; parsing never migrates a saved record. API and history accept schemas 1–5.
Schema 5 adds optional presentation.layout.questionNumberSuffix with value ). New defaults include it and plain-number edits set it explicitly. The same formatting helper measures and renders 1) through 100) in both layouts and width validation. Missing suffixes preserve dotted numbering and exact SVG for saved schemas 1–4. Badge appearance, question content and seeds are unchanged.
The current studio uses schema/layout/renderer version 6 with presentation.layout.style: "beside". This keeps questions beside their number lines and rejects label collisions rather than falling back to stacked rows. Restored drafts and opened history entries adopt this style in the active configuration, preserving the seed, question settings and row-colour choice. Save writes the updated configuration as a revision. The old layout routines remain internal, but the studio no longer offers or opens an above-line layout. This change follows the owner’s instruction that legacy layout preservation is unnecessary.
- Educational context: country, education system, year group, age range and difficulty. Presets are explicit deterministic transformations; custom ages remain possible.
- Content: number-line range, intervals, question count and all subtraction constraints.
- Presentation: paper, typography, margins, spacing, headers, footers, ink, answer spaces and answer sheets.
The UI computes content and layout independently. Presentation edits never change the seed or generated questions. Regenerate replaces only the seed. Manual simple/advanced mode switching changes neither settings nor seed. Simple range shortcuts adjust dependent numeric bounds openly; advanced settings are preserved and validated individually.
The same Zod schema validates UI state, API requests and persisted configurations. Control metadata supplies labels, bounds, steps, sections and editors; numeric schema bounds are sourced from those definitions. Special educational presets remain explicit components. Future AI output must identify a versioned generator and conform to this exact schema, with teacher-visible corrections and no mathematics or diagram generation performed by AI.
Question generation enumerates every legal (start, amount, result) triple within the bounded primary-school domain, then samples with a seeded PRNG. Sampling without replacement gives a strict uniqueness guarantee. Insufficient capacity is reported before generation; there is no retry loop that can hang or silently loosen constraints. Content order is independent of rendering and viewport size.
Every tick is derived from start + index × interval. Every tick coordinate uses (value − start) / (end − start) × width. Labels are selected from those actual tick values. Intervals must divide the intended range and labels must align with ticks. Negative answers are excluded in this version.
All document geometry is millimetres; font settings are points converted explicitly. Paper dimensions are central. Each row is indivisible. The version-2 reference layout places the calculation, aligned answer box and number line side by side in a rounded panel. Rows cycle through a central pastel palette or use a monochrome print style. Multiples of five and endpoints have taller ticks; all numeric coordinates and labels remain derived from actual values. The older reference layout can stack lines on narrow paper. The current beside style validates the horizontal arrangement and never uses this fallback. Row height accounts for question type, ticks, label clearance and requested spacing. Headers wrap and reserve space, page capacity is computed, and pagination keeps question order intact. Question-number widths and student-name/date fields adapt to available space.
Label fitting uses advance widths measured from the same font binaries used everywhere. It reduces only label font size, down to 7 pt, and reports the adjustment. If adjacent labels still collide, generation is rejected with layout alternatives. It never changes requested tick or label intervals.
A semantic page has typed text, rule, answer and number-line elements. The SVG renderer is framework-independent and escapes user text. Browser previews use the same SVG string as export. PDFKit/SVG-to-PDFKit preserve vectors and embed the selected regular/bold font files. Resvg renders those SVGs using those same files, with system font loading disabled; Sharp encodes the requested formats, with print density metadata for PNG/JPEG; WebP resolution is expressed by its pixel dimensions. Images are derived directly from vector geometry, never screenshots. Missing font characters are rejected before export.
The browser loads vendored fonts via @font-face. Test PDFs have been rendered with Poppler and visually checked. Rasterisation naturally introduces anti-aliasing differences between PDF, SVG and image viewers; geometry and font assets remain shared.
UI components call a Zustand action layer, which calls repositories. They do not read/write browser storage themselves. History, templates, preferences and drafts have explicit contracts. The local adapter uses namespaced localStorage behind a storage interface; the initial data volume is small. Errors preserve stored data and show a recovery message instead of clearing it.
An initial generation, regenerate or export saves a history snapshot. A manual edit is a persistent draft until Save creates a new snapshot. Save does not overwrite a prior generation. Duplicate creates a fresh seed, and rename/favourite/delete are deliberate library operations. Navigating away preserves the draft. The library contains configuration/seed records only, never rendered SVG or thumbnails; thumbnails are reconstructed on demand.
History and templates are intentionally distinct. A future template stores a reusable configuration and produces new content with a fresh seed; it is not a historical generation.
Application tokens are central in styles/tokens.css. Worksheet presentation is independent from the application theme. Home, discovery, library and settings share the sidebar, buttons, dialogs, form controls and feedback. Advanced controls are generated from plugin metadata. Route-heavy screens load separately.
Tablet settings use a drawer; small screens start with it closed to preserve preview space. Fullscreen preview is immersive, with zoom/page controls and Escape. Both modes contain keyboard focus and make background controls inert. Native dialogs handle export/delete/rename focus. Navigation links keep accessible names when icon-only. The app includes visible focus styles, a skip link, actionable validation, reduced-motion handling and contrast checks.
- Firebase Auth, Firestore repositories/rules, Hosting and Cloud Functions.
- Template-management UI, resource sharing and multi-user access.
- AI-assisted interpretation and curriculum mapping.
- Further generator/editor plugins and non-worksheet document types.
- Dark theme, offline exports, bulk/background export jobs and tagged/accessible PDF semantics.
- A general historical renderer/algorithm execution system or automatic cross-version migration beyond the retained version-1 worksheet layout.
The interfaces establish seams for those capabilities; they are not advertised as implemented features.