npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@aime.ai/renderer-react

v1.6.0

Published

React component library for rendering AIME lesson packs with BlockSuite whiteboard support

Readme

@aime.ai/renderer-react

A composable React component library for rendering AIME lesson packs — structured educational content with slide-based presentation, a BlockSuite whiteboard, speaker notes, and full RTL / i18n support.


Table of Contents


Installation

pnpm add @aime.ai/renderer-react
# or
npm install @aime.ai/renderer-react

Import the stylesheet once at your app root:

import "@aime.ai/renderer-react/style.css";

Quick Start

import "@aime.ai/renderer-react/style.css";
import {
  LessonProvider,
  LessonWindow,
  SlideNavigation,
  PresentButton,
  ExportButton,
  CompleteLessonButton,
  SlideSidebar,
  SlideViewer,
  FloatingNavigation,
  NotesPanel,
  BlockSuiteCanvas,
} from "@aime.ai/renderer-react";
import type { LessonPackOutput } from "@aime.ai/renderer-react";

function LessonPage({ pack }: { pack: LessonPackOutput }) {
  return (
    <LessonProvider pack={pack}>
      <SlideNavigation>
        <PresentButton />
        <ExportButton onExport={() => downloadPdf(pack)} />
        <CompleteLessonButton onComplete={() => router.back()} />
      </SlideNavigation>

      <LessonWindow>
        <SlideSidebar onBack={() => router.back()} />
        <SlideViewer />
        <BlockSuiteCanvas />
      </LessonWindow>

      <FloatingNavigation />
      <NotesPanel />
    </LessonProvider>
  );
}

Core Concepts

Composability — Every component is optional. Omit BlockSuiteCanvas if you don't need whiteboard annotations. Omit NotesPanel for a minimal viewer. Compose only what you need.

Shared state via contextLessonProvider wraps two React contexts:

  • LessonPackContext — holds the current LessonPackOutput, the active slide index, and setters.
  • PresentationContext — holds UI state: presentation mode, sidebar collapse, whiteboard mode, zoom level, text direction, and i18n labels.

All built-in components read from and write to these contexts automatically.



Editing Feature

The renderer ships a full inline-editing system. When editing is enabled, every slide component swaps its static display for live-editable primitives — teachers or content creators can update text, markdown content, lists, and images without leaving the lesson view.


Enabling Edit Mode

Pass editable and editCallbacks to LessonProvider:

import {
  LessonProvider,
  LessonWindow,
  SlideNavigation,
  EditButton,
  EditView,
  SlideViewer,
} from "@aime.ai/renderer-react";
import type { EditCallbacks } from "@aime.ai/renderer-react";

const editCallbacks: EditCallbacks = {
  onSave: (updatedPack) => {
    // Persist to your API whenever any field changes
    api.patch(`/lesson-packs/${updatedPack.id}`, updatedPack);
  },
  onSelectImage: (resolve) => {
    // Open your own image picker, then call resolve() with the URL
    openMyImagePicker().then((url) => resolve(url));
  },
  onReorderSlides: (slides) => console.log("Slides reordered", slides),
  onDeleteSlide: (index) => console.log("Slide deleted at", index),
};

function LessonEditor({ pack }) {
  return (
    <LessonProvider pack={pack} editCallbacks={editCallbacks}>
      <SlideNavigation>
        <EditButton />
      </SlideNavigation>
      <LessonWindow>
        <EditView />     {/* Full-page editor — only renders when editable */}
        <SlideViewer />  {/* Presentation viewer — hides when EditView is active */}
      </LessonWindow>
    </LessonProvider>
  );
}

Note: editable defaults to false. It can also be toggled at runtime via useLessonPack().toggleEditable() or the built-in EditButton component.


EditView

A full-page slide management view that replaces the SlideViewer when editing is active. It renders null when editable is false, so it is safe to always include in the tree.

Features:

  • Horizontal thumbnail grid — every slide is rendered as a live mini-preview.
  • Drag-to-reorder — grab the grip handle on any card and drag it to a new position. Slide numbers are re-assigned automatically and onReorderSlides + onSave are called.
  • Delete — the trash icon on each card removes the slide; onDeleteSlide + onSave are called.
  • Inline editor — clicking a card opens a single-slide editing view where the active slide's primitive fields become editable. A breadcrumb header and Prev / Next arrows allow navigation between slides without returning to the grid.
<EditView />

No props — reads all state from LessonPackContext.


EditButton

A toggle button that switches editable on and off via useLessonPack().toggleEditable(). Place it inside SlideNavigation.

<SlideNavigation>
  <EditButton />
  <PresentButton />
</SlideNavigation>

No props.


EditCallbacks

All callbacks are optional. Pass only the ones you need.

interface EditCallbacks {
  /**
   * Called (debounced) whenever any slide field changes or slides are
   * reordered / deleted. Receives the full updated pack.
   */
  onSave?: (pack: LessonPackOutput) => void;

  /**
   * Invoked when the user clicks an image placeholder or "Change image".
   * Implement your own file picker and call `resolve(url)` with the chosen
   * URL, or `resolve(null)` to cancel.
   */
  onSelectImage?: (resolve: (url: string | null) => void) => void;

  /**
   * Called after a drag-to-reorder operation completes.
   * `slides` is the full new ordered array with updated `slide_number` values.
   */
  onReorderSlides?: (slides: AnySlide[]) => void;

  /**
   * Called after a slide is deleted.
   * `slideIndex` is the zero-based position of the deleted slide.
   */
  onDeleteSlide?: (slideIndex: number) => void;
}

useLessonPack — editing API

The following editing-specific members are available on useLessonPack():

const {
  editable,         // boolean — whether edit mode is active
  toggleEditable,   // () => void — toggle edit mode on/off
  editCallbacks,    // EditCallbacks — the callbacks passed to LessonProvider
  updateSlideField, // <K extends keyof AnySlide>(key: K, value: AnySlide[K]) => void
} = useLessonPack();

updateSlideField patches a single field on the currently active slide, merges it into the pack, and fires editCallbacks.onSave automatically.

// Update the title of the current slide
updateSlideField("title", "New Title");

// Update a slide-type-specific field (cast required for non-BaseSlide keys)
updateSlideField("content" as any, "# Updated content");

Primitive editing components

All primitive components read editable from context automatically — they render their non-interactive version when editable is false, so you can render them unconditionally.

EditableText

Inline plain-text editor. Renders as a contentEditable element. Commits on blur or Enter; reverts on Escape.

import { EditableText } from "@aime.ai/renderer-react";

<EditableText
  value={slide.title}
  onChange={(v) => updateSlideField("title", v)}
  className="font-black text-3xl text-zinc-900"
/>

| Prop | Type | Default | Description | |------|------|---------|-------------| | value | string | — | Current text value. | | onChange | (next: string) => void | — | Called on commit. | | className | string | "" | Applied to the element. | | as | "span" \| "p" \| "h1" \| "h2" \| "h3" \| "div" | "span" | Rendered tag. | | placeholder | string | "Click to edit…" | Shown when empty. | | multiline | boolean | false | Allow Shift+Enter for new lines. |


EditableMarkdown

Displays rendered markdown. In edit mode, clicking opens a raw-markdown <textarea> that auto-resizes. Commits on blur; reverts on Escape.

import { EditableMarkdown } from "@aime.ai/renderer-react";

<EditableMarkdown
  value={slide.content}
  onChange={(v) => updateSlideField("content" as any, v)}
  className="text-zinc-800 text-base leading-relaxed"
/>

| Prop | Type | Default | Description | |------|------|---------|-------------| | value | string | — | Raw markdown string. | | onChange | (next: string) => void | — | Called on commit. | | className | string | "" | Applied to both rendered and textarea views. |


EditableList

Edits a string[] array. Renders each item with an EditableText and shows Add / Remove controls. Supports a custom renderWrapper for per-item card styling.

import { EditableList } from "@aime.ai/renderer-react";

<EditableList
  items={slide.objectives}
  onChange={(v) => updateSlideField("objectives" as any, v)}
  placeholder="Add an objective…"
  renderWrapper={(child, i) => (
    <div key={i} className="flex gap-3 p-4 bg-white border rounded-xl">
      <span className="font-black text-blue-200 text-2xl">{i + 1}</span>
      <div className="flex-1">{child}</div>
    </div>
  )}
/>

| Prop | Type | Default | Description | |------|------|---------|-------------| | items | string[] | — | Current array of strings. | | onChange | (items: string[]) => void | — | Called with the entire updated array. | | itemClassName | string | "" | className passed to each item's EditableText. | | as | "span" \| "p" \| "div" | "span" | Tag for each item text. | | placeholder | string | "Click to edit…" | Placeholder for empty items. | | renderWrapper | (child: ReactNode, index: number) => ReactNode | — | Optional custom wrapper per item (e.g. icon + number badge). |


EditableImage

Displays an image with a hover overlay to change it in edit mode. When src is null in edit mode, renders an "Add image" placeholder button. Calls editCallbacks.onSelectImage to let the host app provide an image URL.

import { EditableImage } from "@aime.ai/renderer-react";

<EditableImage
  src={slide.image_url}
  alt="Slide image"
  className="w-full h-48 rounded-2xl object-cover"
  onChanged={(url) => updateSlideField("image_url", url)}
/>

| Prop | Type | Default | Description | |------|------|---------|-------------| | src | string \| null | — | Image URL. null shows the Add placeholder in edit mode. | | alt | string | "" | Alt text. | | className | string | "" | Applied to the wrapper. | | onChanged | (url: string \| null) => void | — | Called with the new URL after the user picks an image. |

EditableImage requires editCallbacks.onSelectImage to be set on LessonProvider, otherwise the click does nothing.


Components

LessonProvider

Root provider and layout shell. Must wrap all other components from this library.

<LessonProvider
  pack={lessonPack}     // LessonPackOutput — pre-loads data into context
  dir="rtl"             // "ltr" | "rtl" — text direction (default: "ltr")
  labels={myLabels}     // LessonLabels — override button/label strings (i18n)
>
  {/* your composed UI */}
</LessonProvider>

| Prop | Type | Default | Description | |------|------|---------|-------------| | pack | LessonPackOutput | — | Lesson data to render. Can be omitted and loaded later via useLessonPack().setCurrentPack. | | dir | "ltr" \| "rtl" | "ltr" | Document text direction. Applied to the root <div>. | | labels | LessonLabels | English defaults | UI string overrides for i18n. |


LessonWindow

Flex row container for the main content area (sidebar + viewer). Place inside LessonProvider, after any top navigation.

<LessonWindow>
  <SlideSidebar onBack={() => router.back()} />
  <SlideViewer />
  <BlockSuiteCanvas />
</LessonWindow>

SlideNavigation

Top navigation bar. Accepts any combination of action buttons as children.

<SlideNavigation>
  <PresentButton />
  <ExportButton onExport={handleExport} />
  <CompleteLessonButton onComplete={handleComplete} />
</SlideNavigation>

PresentButton / ExitButton

Toggles presentation (full-screen) mode. Renders as Present when inactive and Exit when in presentation mode — labels are i18n-aware.

<PresentButton />

ExportButton

Calls a user-supplied handler when clicked. Label is i18n-aware.

<ExportButton onExport={() => generateAndDownloadPdf(pack)} />

| Prop | Type | Description | |------|------|-------------| | onExport | () => void | Required. Called when the button is pressed. |


CompleteLessonButton

Signals lesson completion. Label is i18n-aware.

<CompleteLessonButton onComplete={() => router.push("/dashboard")} />

| Prop | Type | Description | |------|------|-------------| | onComplete | () => void | Required. Called when the button is pressed. |


SlideSidebar

Collapsible slide-strip sidebar. Shows a SlideThumbnail for every slide in the pack. Collapses automatically in presentation mode.

<SlideSidebar onBack={() => router.back()} />

| Prop | Type | Description | |------|------|-------------| | onBack | () => void | Required. Called when the Back button is pressed. |


SlideViewer

Renders the currently active slide, scaled to fit the available space. Handles zoom via PresentationContext.

<SlideViewer />

No props — reads active slide and zoom from context.


SlideContainer

Lower-level wrapper used by SlideViewer. Use this if you need to render an individual slide without the full viewer infrastructure.

<SlideContainer slide={mySlide} scale={1} />

| Prop | Type | Description | |------|------|-------------| | slide | AnySlide | The slide data to render. | | scale | number | Transform scale applied to the 1280 × 720 fixed canvas. |


SlideThumbnail

Renders a single slide at thumbnail size. Used inside SlideSidebar; also useful for custom slide pickers.

<SlideThumbnail slide={slide} index={0} isActive={currentIndex === 0} />

| Prop | Type | Description | |------|------|-------------| | slide | AnySlide | Slide to render. | | index | number | Zero-based position in the pack. | | isActive | boolean | Whether this thumbnail is the active/selected slide. |


FloatingNavigation

Previous / Next arrow buttons that float over the viewer. Only visible in presentation mode.

<FloatingNavigation />

No props.


NotesPanel

Slide-out panel showing teacher_notes for the current slide. Toggled via PresentationContext.toggleNotesPanel().

<NotesPanel />

No props.


BlockSuiteCanvas

An edgeless BlockSuite whiteboard that overlays the slide content. Activates when isWhiteboardMode is true in PresentationContext.

<BlockSuiteCanvas
  initialData={savedData}
  onSave={(data) => persistCanvas(data)}
/>

| Prop | Type | Description | |------|------|-------------| | initialData | Uint8Array | Optional. Restores a previously saved Yjs snapshot. When provided, user annotations are re-applied on top of the slides. | | onSave | (data: Uint8Array) => void | Optional. Called (debounced 500 ms) whenever the canvas changes. Serialize to your backend or localStorage. Convert to number[] for JSON storage. |

See Canvas Persistence for a full example.


Contexts & Hooks

useLessonPack

Access and mutate the lesson pack state from any descendant of LessonProvider.

import { useLessonPack } from "@aime.ai/renderer-react";

function MyComponent() {
  const {
    currentPack,          // LessonPackOutput | null
    setCurrentPack,       // (pack: LessonPackOutput | null) => void
    currentSlideIndex,    // number
    setCurrentSlideIndex, // (index: number) => void
    currentSlide,         // AnySlide | null
  } = useLessonPack();
}

Use useLessonPackOptional() if the component might render outside a provider — returns null instead of throwing.


usePresentation

Access and mutate presentation UI state.

import { usePresentation } from "@aime.ai/renderer-react";

function MyComponent() {
  const {
    isPresentationMode,      // boolean
    isSidebarCollapsed,      // boolean
    isNotesPanelVisible,     // boolean
    isWhiteboardMode,        // boolean
    zoom,                    // number (50–200, default 90)
    dir,                     // "ltr" | "rtl"
    labels,                  // Required<LessonLabels>

    // Setters
    setIsPresentationMode,
    setIsSidebarCollapsed,
    setIsNotesPanelVisible,
    setIsWhiteboardMode,
    setZoom,
    setDir,

    // Convenience toggles
    togglePresentationMode,
    toggleSidebar,
    toggleNotesPanel,
    toggleWhiteboardMode,
    zoomIn,    // +10, max 200
    zoomOut,   // -10, min 50
    resetZoom, // back to 100
  } = usePresentation();
}

Use usePresentationOptional() if the component might render outside a provider — returns undefined instead of throwing.


i18n / Labels

Override any built-in button label by passing a labels prop to LessonProvider. Only specify the strings you want to change — others fall back to English defaults.

<LessonProvider
  pack={pack}
  dir="rtl"
  labels={{
    present: "عرض",
    exit: "خروج",
    exportLesson: "تصدير الدرس",
    completeLesson: "إتمام الدرس",
    back: "رجوع",
  }}
>
  {/* ... */}
</LessonProvider>

LessonLabels interface

interface LessonLabels {
  present?: string;         // default: "Present"
  exit?: string;            // default: "Exit"
  exportLesson?: string;    // default: "Export Lesson"
  completeLesson?: string;  // default: "Complete Lesson"
  back?: string;            // default: "Back"
}

Canvas Persistence

BlockSuiteCanvas exposes a raw Yjs binary API so you can store annotations wherever you like.

import { useState, useCallback } from "react";
import {
  LessonProvider,
  LessonWindow,
  SlideViewer,
  BlockSuiteCanvas,
} from "@aime.ai/renderer-react";
import type { LessonPackOutput } from "@aime.ai/renderer-react";

function LessonPage({ pack }: { pack: LessonPackOutput }) {
  // Restore from JSON field on the lesson pack
  const initialData = pack.canvasData
    ? new Uint8Array(pack.canvasData)
    : undefined;

  const handleSave = useCallback(
    (data: Uint8Array) => {
      // Convert to number[] for JSON serialisation, then PATCH your API
      saveLessonCanvas(pack.id, Array.from(data));
    },
    [pack.id],
  );

  return (
    <LessonProvider pack={pack}>
      <LessonWindow>
        <SlideViewer />
        <BlockSuiteCanvas initialData={initialData} onSave={handleSave} />
      </LessonWindow>
    </LessonProvider>
  );
}

LessonPackOutput includes a canvasData?: number[] field as a convenience slot — populate it from your API response and the component handles the rest.

If you need to serialize a doc manually (e.g. for an export flow), use the serializeDoc utility:

import { createSlideDoc, serializeDoc } from "@aime.ai/renderer-react";

const doc = createSlideDoc(pack.slides);
const bytes = serializeDoc(doc); // Uint8Array

Types Reference

LessonPackOutput

interface LessonPackOutput {
  id: string;
  lesson_intent_id: string;
  version: number;
  is_active: boolean;
  status: LessonPackStatus;          // "completed" | "generating" | "starting" | "failed"
  lesson_type: LessonType;
  title: string;
  subject: string;
  grade_level: string;
  total_duration_minutes: number;
  ai_rationale: string;
  file_url: string | null;
  created_at: string;
  updated_at: string;
  slides: AnySlide[];
  resources: string[];
  canvasData?: number[];             // Persisted Yjs state for the whiteboard
}

LessonType enum

enum LessonType {
  INTRODUCTION = "INTRODUCTION",
  PRACTICE     = "PRACTICE",
  DEEP_DIVE    = "DEEP_DIVE",
  REVIEW       = "REVIEW",
  ASSESSMENT   = "ASSESSMENT",
  CONSOLIDATION = "CONSOLIDATION",
}

Slide types

Every slide extends BaseSlide and has a discriminating slide_type field:

| slide_type | Type | |---|---| | "TITLE" | TitleSlide | | "LEARNING_OBJECTIVES" | LearningObjectivesSlide | | "HOOK" | HookSlide | | "PRIOR_KNOWLEDGE" | PriorKnowledgeSlide | | "TEACH" | TeachSlide | | "WORKED_EXAMPLE" | WorkedExampleSlide | | "GUIDED_PRACTICE" | GuidedPracticeSlide | | "STUDENT_TASK" | StudentTaskSlide | | "WARM_UP" | WarmUpSlide | | "DISCUSSION" | DiscussionSlide | | "WORKED_SOLUTION" | WorkedSolutionSlide | | "MISCONCEPTION" | MisconceptionSlide | | "SELF_ASSESSMENT" | SelfAssessmentSlide | | "CONCEPT_MAP" | ConceptMapSlide | | "SYNTHESIS_TASK" | SynthesisTaskSlide | | "SUMMARY" | SummarySlide | | "EXIT_TICKET" | ExitTicketSlide | | "ASSESSMENT_INSTRUCTIONS" | AssessmentInstructionsSlide | | "ASSESSMENT_QUESTION" | AssessmentQuestionSlide | | "MARK_SCHEME" | MarkSchemeSlide |

The union type AnySlide covers all of the above. Use a switch on slide_type for exhaustive handling.

BaseSlide

interface BaseSlide {
  slide_number: number;
  title: string;
  teacher_notes: string;
  duration_minutes: number;
  teacher_only: boolean;
  micro_objective_ids: number[];
  sen_support: string | null;
  image_url: string | null;
}

Peer Dependencies

The following packages must be installed alongside this library:

{
  "@blocksuite/block-std": "0.19.5",
  "@blocksuite/blocks": "0.19.5",
  "@blocksuite/icons": "2.1.75",
  "@blocksuite/presets": "0.19.5",
  "@blocksuite/store": "0.19.5",
  "lit": "^3.0.0",
  "react": "^18.0.0 || ^19.0.0",
  "react-dom": "^18.0.0 || ^19.0.0",
  "yjs": "^13.0.0"
}

Install them all at once:

pnpm add @blocksuite/[email protected] @blocksuite/[email protected] @blocksuite/[email protected] @blocksuite/[email protected] @blocksuite/[email protected] lit yjs

Currently, two official plugins are available:

React Compiler

The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see this documentation.

Expanding the ESLint configuration

If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:

export default defineConfig([
  globalIgnores(["dist"]),
  {
    files: ["**/*.{ts,tsx}"],
    extends: [
      // Other configs...

      // Remove tseslint.configs.recommended and replace with this
      tseslint.configs.recommendedTypeChecked,
      // Alternatively, use this for stricter rules
      tseslint.configs.strictTypeChecked,
      // Optionally, add this for stylistic rules
      tseslint.configs.stylisticTypeChecked,

      // Other configs...
    ],
    languageOptions: {
      parserOptions: {
        project: ["./tsconfig.node.json", "./tsconfig.app.json"],
        tsconfigRootDir: import.meta.dirname,
      },
      // other options...
    },
  },
]);

You can also install eslint-plugin-react-x and eslint-plugin-react-dom for React-specific lint rules:

// eslint.config.js
import reactX from "eslint-plugin-react-x";
import reactDom from "eslint-plugin-react-dom";

export default defineConfig([
  globalIgnores(["dist"]),
  {
    files: ["**/*.{ts,tsx}"],
    extends: [
      // Other configs...
      // Enable lint rules for React
      reactX.configs["recommended-typescript"],
      // Enable lint rules for React DOM
      reactDom.configs.recommended,
    ],
    languageOptions: {
      parserOptions: {
        project: ["./tsconfig.node.json", "./tsconfig.app.json"],
        tsconfigRootDir: import.meta.dirname,
      },
      // other options...
    },
  },
]);