Presentations

Designing collaborative presentation software with slides, templates, and views.

Looking for Prezillo’s exact format?

This page covers generic CRDT design patterns for presentation apps. For Prezillo’s complete type definitions, field tables, and data model, see the Prezillo Format Specification.

Document Structure

Document
├── slides: Map<slideId, SlideData>
├── slideOrder: Array<slideId>
├── containers: Map<containerId, Container>
├── masters: Map<masterId, MasterTemplate>
├── styles: Map<styleId, Style>
└── notes: Map<slideId, string | Y.Text>

Slides contain references to containers (text boxes, shapes, images). Masters define reusable layouts and styles.

Slide/Container Relationship

interface Slide {
  id: string
  masterId: string
  containerIds: string[]
  background?: Background
}

interface Container {
  id: string
  slideId: string
  type: 'text' | 'image' | 'shape'
  x: number; y: number
  width: number; height: number
  contentId?: string  // For text: Y.Text ID; for image: blob ID
  styleId?: string
  zIndex: number
}

When duplicating a slide, duplicate its containers too:

function duplicateSlide(slideId: string): string {
  const slide = slides.get(slideId)
  const newId = nanoid(8)
  const newContainerIds: string[] = []

  yDoc.transact(() => {
    for (const cid of slide.containerIds) {
      const container = containers.get(cid)
      const newCid = nanoid(8)
      containers.set(newCid, { ...container, id: newCid, slideId: newId })
      newContainerIds.push(newCid)
    }

    slides.set(newId, {
      ...slide, id: newId,
      containerIds: newContainerIds
    })

    const index = slideOrder.toArray().indexOf(slideId)
    slideOrder.insert(index + 1, [newId])
  })

  return newId
}

Style Inheritance

Resolve styles by cascading: master → slide → container:

function resolveStyle(container: Container): ResolvedStyle {
  const slide = slides.get(container.slideId)
  const master = masters.get(slide.masterId)

  let style = {}

  // 1. Master base style
  if (master?.styles?.[container.styleId]) {
    style = { ...master.styles[container.styleId] }
  }

  // 2. Slide overrides
  if (slide?.styleOverrides?.[container.styleId]) {
    style = { ...style, ...slide.styleOverrides[container.styleId] }
  }

  // 3. Container overrides
  if (container.styleOverrides) {
    style = { ...style, ...container.styleOverrides }
  }

  return style
}

Presentation Mode

Presentation state is local (not in CRDT), but can be shared via awareness:

// Broadcast current slide for "follow presenter" mode
awareness.setLocalStateField('presenting', {
  slideIndex: currentIndex,
  user: getCurrentUser()
})

// Follow mode: sync to presenter's slide
awareness.on('change', () => {
  if (followingClientId) {
    const state = awareness.getStates().get(followingClientId)
    if (state?.presenting) {
      goToSlide(state.presenting.slideIndex)
    }
  }
})

Common Mistakes

Slide content directly in slideOrder:

// WRONG: content in array
slideOrder.push([{ title: 'Intro', containers: [...] }])

// CORRECT: content separate
slides.set(id, { title: 'Intro', containerIds: [...] })
slideOrder.push([id])

Not cleaning up containers on slide delete:

function deleteSlide(slideId: string) {
  const slide = slides.get(slideId)
  yDoc.transact(() => {
    // Delete containers first
    for (const cid of slide.containerIds) {
      containers.delete(cid)
    }
    // Then slide
    const index = slideOrder.toArray().indexOf(slideId)
    if (index !== -1) slideOrder.delete(index, 1)
    slides.delete(slideId)
  })
}

See Also