Presentations
Designing collaborative presentation software with slides, templates, and views.
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
- Style Inheritance - Template system
- ID-Based Storage - Slide ordering