Skip to main content

Overview

A bounded undo/redo system with overflow management. Perfect for command pattern implementation and history tracking with a fixed-size buffer. When the timeline reaches capacity, oldest items move to an overflow queue that can be restored on undo.

Basic Usage

import { createTimeline } from '@vuetify/v0'

const timeline = createTimeline({ size: 10 })

// Register actions
timeline.register({ id: 'action-1', value: 'First action' })
timeline.register({ id: 'action-2', value: 'Second action' })
timeline.register({ id: 'action-3', value: 'Third action' })

console.log(timeline.size) // 3

// Undo last action
const undone = timeline.undo()
console.log(undone?.id) // 'action-3'
console.log(timeline.size) // 2

// Redo
const redone = timeline.redo()
console.log(redone?.id) // 'action-3'
console.log(timeline.size) // 3

Function Signature

function createTimeline<
  Z extends TimelineTicket = TimelineTicket,
  E extends TimelineContext<Z> = TimelineContext<Z>
>(options?: TimelineOptions): E

Parameters

options
TimelineOptions
Configuration options for the timeline
size
number
default:"10"
Maximum number of items in the timeline buffer. When exceeded, oldest items move to overflow.

Returns

TimelineContext
object
Timeline context with undo/redo capabilities
register
function
Register a new item, removing oldest if at capacity
const item = timeline.register({
  id: 'action-1',
  value: { type: 'insert', data: '...' }
})
undo
function
Remove the last registered item and store it for redo. Restores from overflow if available.
const undone = timeline.undo()
if (undone) {
  console.log('Undid:', undone.value)
}
redo
function
Restore the last undone item
const redone = timeline.redo()
if (redone) {
  console.log('Redid:', redone.value)
}
size
number
Current number of items in the timeline
values
function
Get all items in the timeline
const items = timeline.values()
clear
function
Remove all items from timeline and overflow

Overflow Management

When the timeline exceeds its size limit, the oldest item moves to an overflow queue:
const timeline = createTimeline({ size: 3 })

timeline.register({ id: '1', value: 'First' })
timeline.register({ id: '2', value: 'Second' })
timeline.register({ id: '3', value: 'Third' })

// Timeline: [1, 2, 3]

timeline.register({ id: '4', value: 'Fourth' })

// Timeline: [2, 3, 4]
// Overflow: [1]

// Undo restores from overflow
timeline.undo()

// Timeline: [1, 2, 3]
// Overflow: []

Undo/Redo Behavior

Basic Undo/Redo

const timeline = createTimeline({ size: 5 })

timeline.register({ id: 'a', value: 'Action A' })
timeline.register({ id: 'b', value: 'Action B' })
timeline.register({ id: 'c', value: 'Action C' })

console.log(timeline.values().map(t => t.id)) // ['a', 'b', 'c']

timeline.undo()
console.log(timeline.values().map(t => t.id)) // ['a', 'b']

timeline.redo()
console.log(timeline.values().map(t => t.id)) // ['a', 'b', 'c']

Redo Stack Clearing

Registering a new action clears the redo stack:
const timeline = createTimeline({ size: 5 })

timeline.register({ id: 'a', value: 'A' })
timeline.register({ id: 'b', value: 'B' })

timeline.undo()
console.log(timeline.values().map(t => t.id)) // ['a']

// Register new action - clears redo stack
timeline.register({ id: 'c', value: 'C' })
console.log(timeline.values().map(t => t.id)) // ['a', 'c']

// Can't redo 'b' anymore
const redone = timeline.redo()
console.log(redone) // undefined

Multiple Undos with Overflow

const timeline = createTimeline({ size: 3 })

// Register 6 items
for (let i = 0; i < 6; i++) {
  timeline.register({ id: `item${i}`, value: i })
}

// Timeline: [3, 4, 5], Overflow: [0, 1, 2]

timeline.undo()
// Timeline: [2, 3, 4], Overflow: [0, 1]

timeline.undo()
// Timeline: [1, 2, 3], Overflow: [0]

timeline.undo()
// Timeline: [0, 1, 2], Overflow: []

Command Pattern Example

interface Command {
  execute: () => void
  undo: () => void
}

interface CommandTicket extends TimelineTicket {
  value: Command
}

const timeline = createTimeline<CommandTicket>({ size: 20 })

function executeCommand(command: Command) {
  command.execute()
  timeline.register({ 
    id: crypto.randomUUID(),
    value: command 
  })
}

function undoLastCommand() {
  const item = timeline.undo()
  if (item) {
    item.value.undo()
  }
}

function redoLastCommand() {
  const item = timeline.redo()
  if (item) {
    item.value.execute()
  }
}

// Usage
const insertCommand = {
  execute: () => document.body.insertAdjacentHTML('beforeend', '<p>Hello</p>'),
  undo: () => document.body.lastElementChild?.remove(),
}

executeCommand(insertCommand)
undoLastCommand()
redoLastCommand()

Context Pattern

Use dependency injection for global timeline access:
import { createTimelineContext } from '@vuetify/v0'

export const [useHistory, provideHistory, history] = createTimelineContext({
  size: 50,
})

// Parent component
provideHistory()

// Child component
const history = useHistory()
history.register({ id: 'edit-1', value: editData })
history.undo()

TypeScript

Custom Ticket Types

interface EditorAction extends TimelineTicket {
  type: 'insert' | 'delete' | 'format'
  position: number
  content: string
}

const timeline = createTimeline<EditorAction>({ size: 100 })

timeline.register({
  id: 'edit-1',
  type: 'insert',
  position: 42,
  content: 'Hello',
  value: 'Hello',
})

Build docs developers (and LLMs) love