Files
classroomio/packages/storybook
20e34e29dd feat(exercise): add open-ended thumbs up/down question type (#780)
* feat(ui): add THUMBS question type renderer

- Add seven new renderer files under renderers/thumbs/:
  thumbs-correct.ts, take.svelte, edit.svelte, preview.svelte,
  thumbs-review-toggle.svelte, review.svelte, submission.svelte
- Register THUMBS in renderer-contract.ts with edit/take/preview/submission/review
- Export ThumbsEdit, ThumbsTake, ThumbsPreview, ThumbsSubmission,
  ThumbsReview from exercise-question/index.ts
- Add THUMBS_FIXTURE, four thumbs label keys, and Thumbs story
  in storybook exercise-question files
- Add THUMBS case to question-type-modes.svelte submission builder

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* feat(question-types): add THUMBS contracts, scoring, and seed

Introduce the THUMBS question type (id 14) with boolean answer data,
codec, auto-grading, Zod validation, DB seed/schema docs, and binary
normalize support shared with TRUE_FALSE. Dashboard wiring is deferred.

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* feat(dashboard): wire THUMBS question type into exercise UI

Connect THUMBS option sync in edit/view modes, labels, picker icon,
and locale strings so the existing contracts surface in the dashboard.

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* fix(ui): polish thumbs question UI and move premium upgrade icon

Widen thumbs Yes/No toggles, inherit selected icon color via custom class,
replace badge-based review correctness with solid fill states and bare
Check/X icons, add a success pill in preview, and place the premium
upgrade icon at the end of the question type select row.

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* fix(ui): align thumbs review colors with prototypes

Tune correct/incorrect selected states so review buttons use dark
emerald/red content colors on solid fills, matching the design comps.

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* fix(ui): address remaining PR review comments

Use a shared timestamp local in thumbs edit fallback options.
Move border-beam animation-delay after the animation shorthand.
Alias --spacing to --ui-spacing for tw-animate compatibility.

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* fix(db): sync question_type sequence before seeding new types

Avoid duplicate key errors when question_type_id_seq lags behind rows
inserted via restore or explicit ids (e.g. THUMBS on restored databases).

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* fix(exercise): resolve thumbs review label ICU crash

Remove {value} ICU placeholder from thumbs.review.correct_answer_label
since labels are resolved at map-build time without context. Append the
dynamic yes/no label in thumbs-review-toggle instead.

Also bind question points through the questionnaire store index to avoid
the Svelte non-reactive property binding warning in edit-mode.

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* docs(agent): add THUMBS guidance and sync question type docs

Add shared agent question-type guide helpers in @cio/question-types and
use them across the teacher prompt, add/update_questions tool schemas,
legacy ai-assistant schemas, and MCP exercise tools.

- Document when to use THUMBS (Yes/No sentiment) vs TRUE_FALSE (factual)
- Fix stale MCP question type ids and add THUMBS=14
- Exclude disabled MATCHING/HOTSPOT from enabled type lists

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* fix(thumbs): clarify auto-grading and add editable option labels

THUMBS is auto-graded like TRUE_FALSE; the difference is thumbs UI with
customizable Yes/No-style labels and the teacher selecting the correct
option. Render take/preview/review from question.options labels and add
label inputs in the thumbs edit renderer. Update agent and MCP docs to
stop implying THUMBS is manual or non-gradable.

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* chore(mcp): bump version to 0.0.10

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* fix(question-types): make THUMBS open-ended with no correct answer

THUMBS is a sentiment/poll question type, not an auto-graded binary like
TRUE_FALSE. Remove correct-answer UI, scoring, and validation; update agent
and MCP docs accordingly.

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* fix(dashboard): stop treating THUMBS as auto-graded in editor validation

Normalize thumbs questions on load/save, show an Open-ended badge instead
of Auto, and only enforce points/correct-answer rules on auto-gradable types.

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* revert(dashboard): remove unsolicited open-ended grading badge

THUMBS uses the existing Manual badge like other non-auto-gradable types.

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* fix(exercise): show question title validation errors in editor

Display mapped title errors beneath the question title input instead of
only highlighting the question card border. Pass the exercise entity to
mapZodErrorsToTranslations so messages resolve to validations.exercise.*
keys, and use a translated snackbar key for save validation failures.

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* fix(mcp): build workspace deps before MCP publish bundle

The MCP esbuild step imports @cio/question-types and @cio/utils dist
outputs. CI failed on a clean checkout because mcp:build only built the
MCP package itself. Build those dependencies first so 0.0.10 can publish.

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
2026-07-31 14:59:38 +01:00
..

@cio/storybook

Storybook for ClassroomIO UI components.

Development

To start the Storybook development server:

pnpm dev

Build

To build the Storybook:

pnpm build

Build Storybook

To build the Storybook (alternative command):

pnpm build-storybook

Upload

To upload the built Storybook to Cloudflare R2:

pnpm upload:storybook

Note: This requires the build to be completed first. Make sure you have the following environment variables set:

  • CLOUDFLARE_ACCESS_KEY
  • CLOUDFLARE_SECRET_ACCESS_KEY
  • CLOUDFLARE_ACCOUNT_ID

Publish

To build and upload the Storybook in one command:

pnpm storybook:publish

This will build the Storybook and then upload it to R2.

How to Create Storybook Stories

This guide documents the patterns and conventions for creating Storybook stories in the ClassroomIO UI component library.

Table of Contents

File Structure

Directory Organization

Stories are organized into two main categories:

  • Atoms (src/atoms/): Basic, indivisible UI components (Button, Input, Badge, etc.)
  • Molecules (src/molecules/): Composite components made of multiple atoms (Card, Field, ButtonGroup, etc.)

File Naming

  1. Create a directory matching the component name (kebab-case)
  2. Create a story file: {component-name}.stories.svelte
  3. Optionally create a fields file: fields.ts (for controls configuration)

Example:

src/molecules/hover-card/
  ├── hover-card.stories.svelte
  └── fields.ts

Basic Story Structure

Every story file follows this structure:

<script module>
  import { defineMeta } from '@storybook/addon-svelte-csf';

  // Component imports
  import * as ComponentName from '@cio/ui/base/component-name';

  // Other imports (icons, utilities, etc.)
  import IconName from '@lucide/svelte/icons/icon-name';

  // Fields import (if using controls)
  import { FIELDS } from './fields';

  // Story definition
  const { Story } = defineMeta({
    title: 'Category/ComponentName',
    component: ComponentName.Root, // or ComponentName for single components
    parameters: {
      layout: 'centered',
      controls: {
        include: FIELDS
      }
    },
    tags: ['autodocs']
  });

  // State variables (if needed)
  let someState = $state('');
</script>

<Story name="StoryName">
  {#snippet template()}
    <!-- Component usage -->
  {/snippet}
</Story>

Import Patterns

Component Imports

  • Namespace imports for multi-part components:

    import * as HoverCard from '@cio/ui/base/hover-card'; import * as Card from '@cio/ui/base/card'; import * as Field
    from '@cio/ui/base/field';
    
  • Named imports for single components:

    import {Button} from '@cio/ui/base/button'; import {Input} from '@cio/ui/base/input'; import {Avatar} from '@cio/ui/base/avatar';
    

Icon Imports

Use Lucide Svelte icons:

import CalendarDaysIcon from '@lucide/svelte/icons/calendar-days'; import UsersIcon from '@lucide/svelte/icons/users';
import BookOpenIcon from '@lucide/svelte/icons/book-open';

Story Definition

Title Format

  • Atoms: 'Atom/ComponentName' (e.g., 'Atom/Button')
  • Molecules: 'Molecules/ComponentName' (e.g., 'Molecules/HoverCard')

Component Reference

  • For multi-part components, reference the Root component:

    component: HoverCard.Root component: Card.Root
    
  • For single components, reference directly:

    component: Button component: Input
    

Parameters

Standard parameters configuration:

parameters: {
  layout: 'centered',
  controls: {
    include: FIELDS  // Only if using fields.ts
  }
}

Tags

Always include tags: ['autodocs'] for automatic documentation generation.

Template Snippets

Basic Template

<Story name="Default">
  {#snippet template()}
    <ComponentName.Root>
      <!-- Component content -->
    </ComponentName.Root>
  {/snippet}
</Story>

Template with Args (for controls)

For atoms that need to pass args to controls:

<Story name="Default">
  {#snippet template(args)}
    <Button {...args}>Button Text</Button>
  {/snippet}
</Story>

Multiple Stories

Create multiple stories to showcase different use cases:

<Story name="Default">
  {#snippet template()}
    <!-- Basic usage -->
  {/snippet}
</Story>

<Story name="With Avatar">
  {#snippet template()}
    <!-- Avatar variant -->
  {/snippet}
</Story>

<Story name="Custom Content">
  {#snippet template()}
    <!-- Custom content variant -->
  {/snippet}
</Story>

Fields File

Create a fields.ts file to control which props appear in Storybook controls:

export const FIELDS = ['class'] as string[];

Common fields:

  • 'class' - CSS classes
  • 'variant' - Component variants
  • 'size' - Component sizes
  • 'disabled' - Disabled state
  • Add more as needed based on component props

Component Categorization

Atoms

Basic, indivisible components:

  • Button, Input, Badge, Avatar, Label, Checkbox, Switch, etc.
  • Single-purpose components
  • Usually don't compose other components

Molecules

Composite components:

  • Card, Field, ButtonGroup, HoverCard, Empty, etc.
  • Made of multiple atoms or other components
  • More complex functionality

Guideline: If a component uses multiple other components together, it's likely a molecule.

CSS Class Conventions

UI Prefix

Use the ui: prefix for utility classes to scope them properly:

<!-- ✅ Correct -->
<div class="ui:flex ui:flex-col ui:gap-6">
<Button class="ui:w-full">

<!-- ❌ Incorrect -->
<div class="flex flex-col gap-6">
<Button class="w-full">

When to Use UI Prefix

  • Always use for utility classes (flex, grid, gap, padding, margin, etc.)
  • Don't use for component-specific classes or data attributes
  • Don't use for Tailwind classes that are part of component styling

Examples

<!-- Utility classes - use ui: prefix -->
<div class="ui:flex ui:items-center ui:gap-2">
<div class="ui:space-y-2">
<div class="ui:text-muted-foreground ui:text-sm">

<!-- Component classes - no prefix needed -->
<Card.Root class="w-full max-w-sm">
<HoverCard.Content class="w-80">

LMS Content Guidelines

Since ClassroomIO is an LMS platform, all story content should be LMS-related:

✅ Good Examples

  • Courses: "Introduction to Web Development", "Advanced React Patterns"
  • Users: "Sarah Johnson (Instructor)", "Alex Martinez (Student)"
  • Content: Course descriptions, assignment types, enrollment information
  • Actions: "Create Course", "Enroll Student", "Submit Assignment"
  • States: "No courses yet", "Student enrolled", "Assignment submitted"

❌ Avoid

  • Generic examples (Svelte, GitHub profiles, etc.)
  • Non-educational content
  • Examples that don't relate to learning/teaching

Content Patterns

Instructor Profiles:

<h4 class="text-sm font-semibold">Sarah Johnson</h4>
<p class="text-sm">Senior Full-Stack Developer & Instructor</p>
<span class="text-muted-foreground text-xs">Teaching since January 2020</span>

Course Information:

<h4 class="text-sm font-semibold">Advanced React Patterns</h4>
<p class="text-muted-foreground text-sm">
  Master advanced React concepts including hooks, context, performance optimization...
</p>

Student Information:

<h4 class="text-sm font-semibold">Alex Martinez</h4>
<p class="text-muted-foreground text-xs">5 courses enrolled</p>
<p class="text-muted-foreground text-xs">Active since March 2024</p>

Examples

Example 1: Simple Atom (Button)

<script module>
  import { defineMeta } from '@storybook/addon-svelte-csf';
  import { Button } from '@cio/ui/base/button';
  import { FIELDS } from './fields';

  const { Story } = defineMeta({
    title: 'Atom/Button',
    component: Button,
    parameters: {
      layout: 'centered',
      controls: {
        include: FIELDS
      }
    },
    tags: ['autodocs']
  });
</script>

<Story name="Default">
  {#snippet template(args)}
    <Button {...args}>Create Course</Button>
  {/snippet}
</Story>

Example 2: Multi-Part Molecule (HoverCard)

<script module>
  import { defineMeta } from '@storybook/addon-svelte-csf';
  import CalendarDaysIcon from '@lucide/svelte/icons/calendar-days';
  import * as HoverCard from '@cio/ui/base/hover-card';
  import * as Avatar from '@cio/ui/base/avatar';
  import { FIELDS } from './fields';

  const { Story } = defineMeta({
    title: 'Molecules/HoverCard',
    component: HoverCard.Root,
    parameters: {
      layout: 'centered',
      controls: {
        include: FIELDS
      }
    },
    tags: ['autodocs']
  });
</script>

<Story name="Instructor Profile">
  {#snippet template()}
    <HoverCard.Root>
      <HoverCard.Trigger
        href="/instructors/sarah-johnson"
        class="ui:rounded-sm ui:underline-offset-4 ui:hover:underline"
      >
        Sarah Johnson
      </HoverCard.Trigger>
      <HoverCard.Content class="w-80">
        <div class="flex justify-between space-x-4">
          <Avatar.Root>
            <Avatar.Image src="https://github.com/shadcn.png" />
            <Avatar.Fallback>SJ</Avatar.Fallback>
          </Avatar.Root>
          <div class="space-y-1">
            <h4 class="text-sm font-semibold">Sarah Johnson</h4>
            <p class="text-sm">Senior Full-Stack Developer & Instructor</p>
            <div class="flex items-center pt-2">
              <CalendarDaysIcon class="me-2 size-4 opacity-70" />
              <span class="text-muted-foreground text-xs">Teaching since January 2020</span>
            </div>
          </div>
        </div>
      </HoverCard.Content>
    </HoverCard.Root>
  {/snippet}
</Story>

Example 3: Complex Molecule with State (Field)

<script module>
  import { defineMeta } from '@storybook/addon-svelte-csf';
  import * as Field from '@cio/ui/base/field';
  import { Input } from '@cio/ui/base/input';
  import { FIELDS } from './fields';

  const { Story } = defineMeta({
    title: 'Molecules/Field',
    component: Field.Field,
    parameters: {
      layout: 'centered',
      controls: {
        include: FIELDS
      }
    },
    tags: ['autodocs']
  });

  let email = $state('');
</script>

<Story name="Course Enrollment">
  {#snippet template()}
    <div class="w-lg">
      <Field.Set>
        <Field.Legend>Enroll Student</Field.Legend>
        <Field.Description>Add a new student to your course</Field.Description>
        <Field.Group>
          <Field.Field>
            <Field.Label for="student-email">Student Email</Field.Label>
            <Input id="student-email" type="email" bind:value={email} placeholder="student@example.com" />
          </Field.Field>
        </Field.Group>
      </Field.Set>
    </div>
  {/snippet}
</Story>

Checklist

When creating a new story, ensure:

  • File is in the correct directory (atoms/ or molecules/)
  • File is named {component-name}.stories.svelte
  • Imports use @cio/ui/base/{component-name} pattern
  • Title follows 'Category/ComponentName' format
  • Component reference is correct (Root for multi-part, direct for single)
  • tags: ['autodocs'] is included
  • All utility classes use ui: prefix
  • Content is LMS-related (courses, students, instructors, etc.)
  • Multiple stories showcase different use cases
  • fields.ts is created if controls are needed
  • State variables use $state() for reactivity
  • Icons are imported from @lucide/svelte/icons/

Additional Notes

  • State Management: Use Svelte 5 runes ($state(), $derived()) for reactive state
  • Layout: Most stories use layout: 'centered' in parameters
  • Styling: Prefer Tailwind utility classes with ui: prefix
  • Accessibility: Include proper ARIA labels and semantic HTML
  • Documentation: Story names should be descriptive and reflect the use case

Resources