Skip to content

Spec 12 Design Guidelines

github-actions[bot] edited this page Oct 14, 2026 · 1 revision

Section of Quizzivy — Frontend Portal & Data Model Specification · Previous · Next

12. Design guidelines

Deliberate. Do not "improve" them with trendy defaults.

As built in R1 (v0.7.0). The deck (docs/design/deck/) is the source.

  • Tokens. web/src/index.css carries the deck's light and dark sets. tokens.test.ts measures them rather than pinning values:

    • every themed token has a dark value;
    • the neutrals stay under 0.03 chroma and --primary stays charcoal;
    • each colour family stays in its hue band, with only info allowed to be blue;
    • text pairs reach 4.5:1, and the focus ring and input borders 3:1, in both themes;
    • @theme routes every utility colour through a variable.

    no-raw-colours.test.ts refuses a Tailwind palette class, or a literal hex, rgb()/rgba() or oklch() colour, in the .ts/.tsx files under src/ (except GoogleMark.tsx); the literal values live only in src/index.css.

  • Accessibility tokens beyond the deck (DG-30, DG-31), each measured in both themes:

    • --focus, the 2px :focus-visible ring on every control, 3:1 or more on --bg, --card, --sidebar and --muted;
    • --input, input borders, 3:1 or more;
    • --danger-solid / --danger-solid-fg, danger buttons, 4.5:1 or more.
  • Deck geometry that would change an existing primitive (control heights, --radius and the sm/md/lg radii, the badge and card shapes) applies only inside data-scale="deck", which rebuilt surfaces set on their root. The deck's type steps are global tokens, but the pre-R1 steps keep their values, so an old screen does not shift. Sizes that exist only in the deck have their geometry everywhere (button md, xl and icon-xl, the new avatar sizes, the toast).

  • Every console follows the theme. The teacher's TeacherLayout is a deck surface since R4, and the student and focus layouts since R3; nothing forces light. R5 makes the deck geometry the default and removes data-scale (T-R5.30).

The teacher workspace's page frame (DG-205, Decided: the design team's page-width design, docs/design/deck/page-width-proposal/, approved by the owner on 2026-10-12; the v2 deck drew 1320px on most screens and 1080, 860 and 720 on some, each centred).

  • One frame, up to 1600px. TeacherLayout puts every route in the same frame: the full content width up to --page-max (1600px), then centred, at every width, 2560 included. So every page, its loading, empty, error and not-found states included, starts at one left edge and has one width at each viewport and sidebar state. The one named exception is the import review, full-bleed with the sidebar collapsed (handle.width: "full"). The values are tokens in src/index.css; no other route or page sets a maximum, and page-frame.test.ts refuses a teacher page that centres itself in a narrower box.
  • Gutters by width: --page-gutter 14px below 768, 28px from 768 and 40px from 1920; top padding 16 / 24px.
  • Size the content, not the page. The page head (title, description, actions, the wizard's stepper) spans the frame, and cards, lists, tables and steppers fill it. Only text is capped: a field is at most --field-max (480px) wide, prose at most --prose-max (72ch). There are no form columns: New assignment's summary docks on the right at 300px beside a form that fills, and Settings' fields flow into at most three columns, each field at most 480px. Assignments shows Assigned to from 1240px of content, Submitted from 760 and To grade from 900; the Imports history adds Source from 1240. Inner reflows follow the content-width bands of §8 or a container query, never a new viewport breakpoint.

The AI assistant's panels (D4; DG-201). Built in T-D4.34 (the panel), T-D4.35 (the question editor's suggestions), T-D4.36 (the grading card) and T-D4.37 (Explain results). The server enforces what it can (§15); these are the rules the web draws.

  • The acceptance rule. What the assistant returns is a suggestion, never a record. A panel's primary button saves only the items the teacher kept, through the writes that already exist (the section and bank question writes, updateQuestion, duplicateTest, the bulk tag write), and features/ai/ itself calls nothing but /teacher/ai/*. "Use this" and "Use score and comment" fill a field and save nothing; the teacher's own Save does. On the grading card the filled score and comment are the answer's draft, unlike a score button, which saves at once: Save & next writes them, and so does moving to another answer, as it writes a typed comment; leaving the page with a filled score unsaved asks first, as it does for a typed comment. A question the assistant drafted carries the tag "AI draft" while the preference for tagging is on, until a teacher edits or approves it. No sample output is ever drawn.
  • The disabled-with-reason rule. An entry point is hidden only when the server has no assistant (PublicStatus.ai is false) or the caller lacks ai.use. For a teacher who is paused or out of credits it stays visible and disabled, with the reason as its tooltip and in the panel's footer: "AI is paused for your account.", "Your allowance is used up. Ask for more in Settings." or "The school's AI pool is used up this month." (AI_PAUSED, AI_CREDITS_EXHAUSTED with details.scope). The two features that send a student's answer text are not offered while the teacher's "Let AI read student answers" is off: in place of "Explain results with AI" the Questions card says in one line that it needs the switch, with a link to Settings › AI assistant, and the grading card says in one line that suggestions are off in Settings › AI assistant.
  • The question editor's suggestions (V-10), in the bank's editor and the builder's pane, which share the editor (DG-108). "Write with AI" sits in the Explanation's label row and "Suggest wrong options" beside Add option on a single or multiple choice question; each opens an inline panel under its field, not a drawer. Before asking, the editor checks the deck's two guards as toasts ("Write the question first", "Mark the correct answer first"). Both operations read the stored question, so the builder saves its pending edit first, and the bank, whose Save is explicit, asks for a save ("Save the question first") while what the assistant reads (the type, prompt, written options, accepted answers and sample answer) differs from what is stored; an empty option is not read and does not count. The explanation is written in the teacher's preferred language; "Shorter" keeps the language of the one shown and "In Vietnamese" / "In English" asks for the other. "Use this" replaces the field's text in the form it holds (rich content, or Markdown) and "Add" fills the first empty option that is not marked correct, or adds one while there are fewer than eight (DG-63); neither saves, so the builder's autosave or the bank's Save writes them. A panel that answered ends with the call's credit note. A failure, or an answer with no suggestion, is a line in the panel; the button is the way to try again.
  • The credit note. A panel's footer says what the call cost and what is left, from the answer's usage and in credits, never in money. The deck's costs are placeholders: the product shows no figure it has not metered.
  • Failure. A provider that did not answer, refused, or answered something that fails the schema is the warning callout "AI didn't respond. Try again in a minute." with "Try again" (AI_UNAVAILABLE, which asks for 30 seconds). A panel closes on Esc, keeps focus inside while open and returns it to the button that opened it (DG-36).

The student app and the engine, as built in R3 (v0.9.0) and D4. The source is Quizzivy Student.dc.html. What the deck does not draw, and where the product departs from it, is in docs/design/gaps.md (DG-80 to DG-89, DG-102 to DG-107, DG-119, DG-182 to DG-194).

  • One breakpoint for the shell, 768px. The student shell changes its chrome there and nowhere else (destinations in the top bar or bottom tabs), and so does the engine's: side-by-side panes or the drawer over the question, and the sheet as a card or a bottom sheet. A page keeps its state across it.
  • The engine's other thresholds are its own, read through one width source (useEngineWidthAtLeast): the window's width, or a preview frame's (§7.3). The sections rail shows from 900px on a question with no shared content and from 1180px on one with it, and the grid button with its sheet stands in for it everywhere else; the grid button's label takes its short form below 420px (and beside shared content from 768); the drawer is 88% of the width, at most 420px; the engine's 1024px sizes (engine-lg:, below) follow the same source. Crossing any of them remounts nothing and keeps what is typed.
  • One centred column per page: Home and Classes at most 960px, the test intro 720px, the result 820px, Settings 760px. No page has a side panel. In the engine the passage column is at most 640px and the question column 600px beside shared content, 720px alone.
  • Classes are cards in a grid that fills the row, with 280px as a card's least width. A card is not a link.
  • Touch targets. Below 1024px the student surfaces put a 44px floor on buttons, and so does the engine in a teacher's preview frame narrower than 1024px, whatever the window's width. A control the deck draws keeps the deck's size: the header's 36px ✕, the 32px flag toggle, the rail's 34px squares (never below 900px), the drawer's 30 × 84px handle and 34px close button, the dialogs' 42px buttons (46px for the one button of the "you left the test" alert). The engine's Previous and Next and the grid button are 44px, and the question sheet's squares 44px below 1024px and 42px from it. An option row is at least 52px high; a cloze card's option row is 48px.
  • Dialogs use the deck's frame: 440px (420px for the "you left the test" alert) or the width less 24px, no close button, the actions at the right; the alert has one button, as wide as the dialog. A dialog with a field sits 12% from the top below 768, so the keyboard does not cover it (DG-103).
  • The timer is a pill centred in the header's free space. Each digit sits in a cell one zero wide, because Be Vietnam Pro has no tabular figures and the pill would otherwise change width every second. Under five minutes it takes the danger tones.
  • Text in a test. The passage is 16px on a 1.75 line and cannot be selected, the prompt 17px, an option 15px. "Chữ lớn hơn khi làm bài" raises them to 18, 19 and 17px; the signed-in choice is an account preference mirrored in this browser; anonymous choices remain local.
  • Content keeps a light paper surface in dark mode (DG-35): images and rich tables in the engine and on the result.

The rules below carry over from v0.43, restated in the deck's tokens, except that the shadow and radius limits now follow the deck. Typography, motion, the front door, dark mode and the lime accent are new in R1.

  • Neutral by default. The deck's teal-grey scale (--bg, --card, --muted, --border, --fg, --muted-fg) and 1px borders. Deck surfaces use the deck's shadows (shadow-card at rest); floating surfaces go up to shadow-lg. The old consoles keep shadcn's shadows until their rebuild.

  • Primary action: the deck's charcoal --primary with --primary-fg. Not blue, not purple, not indigo. The lime --accent-c (with its soft and ink tones) marks progress, counts and current states, never a primary button.

  • Semantic color only where it carries meaning: green = correct/success, red = incorrect/error/destructive, amber = warning/time-low. Never decorative.

  • Forbidden: decorative gradients, glassmorphism/backdrop blur, pulsing rings, glow effects, radii beyond the deck's (the old consoles keep rounded-md controls and rounded-lg cards), emoji in UI chrome.

  • Typography: Be Vietnam Pro, self-hosted (latin, latin-ext and vietnamese subsets, 400–700, upright and italic; the CSP allows no font host). Every character the UI draws is in the loaded font faces; a symbol the font lacks is an icon (KeyGlyph for ⌘, ↵, ↑ and ↓), or a character it has, such as the en dash for a range. Content is shown in NFC: the server composes the text a person types before it stores it, the rich content editor composes what it writes, and a view composes what it draws, because a row saved before the server did can still be decomposed. The deck's scale: 3xs 10.5, 2xs 11, caption 11.5, xs 12, meta 12.5, sm 13, ui 13.5, base 14, body 14.5, md 15, title 16, lg 17, stat-sm 18, xl 20, stat 22, h1 24, h1-student 26, kpi-sm 28, kpi 30, display 34px. Phone text inputs stay at 16px to avoid input zoom; student touch targets follow the R3 rule above; seek tracks have a 44px hit area. leading-relaxed in the test view.

  • Icons: lucide-react, 16px dense / 20px nav, consistent stroke, aria-hidden unless standalone.

  • Density: admin tables dense (~40px rows). Student pages are the centred columns of the R3 rules above. The teacher's pages follow the deck's tables and cards inside the content-width rules of §8. The student test view stays spacious: one question at a time (a cloze's run is one screen), beside its shared content from 768px when it has some.

  • Action hierarchy: resume is primary; opening an assignment's intro is secondary. Deadline pills turn amber only within 24 hours. The 320px test footer has previous as an icon, the grid button that opens the question sheet, and Next or Finish. In the Submit dialog the two buttons stay in reach while a long list of unanswered questions scrolls. Submission confirmation offers the submitted paper directly.

  • Explicit states: single- and multiple-choice instructions identify selection behaviour without revealing the key. The save line says saved, saving, offline or failed; exhausted audio allowances explain continued playback is recorded. Login exposes class joining and a reversible password visibility toggle. Signed-in join screens identify the current account and provide a way back.

  • Motion. The deck's keyframes, and what the old consoles still carry:

    • 150ms control feedback, and the settings panels' 150ms ease-out settings-enter entrance.
    • A 2px hover lift on cards, on pointer devices (ours; the deck changes the border instead).
    • The splash's .35s fade and its bar.
    • qz-breath, qz-indet and qz-shimmer: the splash, and skeletons on deck surfaces. Elsewhere the skeleton keeps shadcn's animate-pulse; spinners use Tailwind's animate-spin (qz-spin is defined and not yet used).
    • qz-pulse, for the live dot.
    • qz-marquee, only when a title overflows, paused on hover and focus.
    • The vendored primitives' enter and exit (dialog, popover, select, dropdown menu), standing in for the deck's popover entrance.
    • The test builder outline's drag-and-drop reorder slide (dnd-kit); the deck draws a drop line instead.

    Under prefers-reduced-motion the marquee, live dot, breath, shimmer and indeterminate bar each have a static state, not a shorter one: an ellipsis, a solid dot, a still mark, flat --muted, a fixed 40% bar. Everything else is cut to 0.01ms by the global reduced-motion block. Exam inputs remain stationary. The rebuilt consoles (R3–R5) replace the old consoles' motion with the deck's.

  • Audio player: monochrome. A filled --primary play button, a thin --secondary track with a --primary fill. The engine's recording card (§11.3) is the one exception, as the deck draws it: its 6px track on --muted fills with the lime --accent-c, which marks progress. No waveform visualisation, no equaliser animation, no other colored accents.

  • Front door: sign-in, forgot password, join and change password share the brand frame, with the brand panel from 900px, except join, which stays one column. On join, the class name is large and there is one primary button. This is the first thing a new student sees — it should look calm and legitimate, not like a marketing page.

  • Dark mode: in scope from R1.

    • The preference is light, dark or system, stored in localStorage['quizzivy.theme'].
    • web/public/boot.js applies it before paint, since the CSP allows no inline script.
    • Components read colour tokens rather than hard-coding colours. The exceptions: the brand art, which shows its on-dark files in dark because an image cannot read a token; GoogleMark, in Google's own colours; and the vendored primitives in components/ui/, which keep their dark: variants.
    • Content surfaces keep a light --paper surface in dark mode (DG-35). The tokens exist, and each surface adopts them when it is rebuilt.
  • Empty states: one short sentence + one primary action. No illustrations.

  • Vietnamese first. Design for longer Vietnamese strings; avoid fixed-width labels.

  • Integrity UI follows the deck (D8, decided 2026-09-26). On the student side since R3: the red timer under five minutes, the "Bạn vừa rời trang làm bài" dialog with its eye-off tile, "Giáo viên đã được báo" past the allowance under flag, and the danger toast for a blocked copy or paste (§10.2). The words stay plain: no sentence a student reads says "violation" or "cheating", and the product never concludes that a student cheated. The teacher's roster follows in R4; until then it keeps the calm rule: plain text, no alarm iconography.


Section of Quizzivy — Frontend Portal & Data Model Specification · Previous · Next


Generated from docs/quizzivy-spec-v0.3.md. Edit that file in the repository; changes made on this page are overwritten.

Home

Start here

Product

Implementation plan

Redesign (Phase R)

Setup and operations

Design

Team

Code READMEs

Clone this wiki locally