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

@asky-ai2/vue-timeline-editor

v0.0.6

Published

Interactive timeline editor for Vue 3 (drag, resize, zoom)

Downloads

407

Readme

Vue Timeline Editor

A lightweight, flexible, and interactive timeline editor for Vue 3.

Designed for building booking systems, schedulers, and resource timelines with full control over UI and behavior. Every interactive capability is an opt-in, tree-shakeable feature component — you only ship what you mount.


✨ Features

  • 📅 Horizontal, time-based grid with configurable range and zoom
  • 🧱 Multi-section / multi-row layout (tables, resources, zones)
  • 🖱 Drag frames to move them between times and rows (<Dnd/>)
  • ↔️ Resize frames from either edge (<Resize/>)
  • 🎯 Snap to grid, frame edges, and rows (<Snapping/>)
  • 📏 Static + active snap guide lines (<SnapGuideLines/>)
  • ➕ Click (or tap) an empty area to create a frame (<Sections rowClickable/>)
  • 🔗 Linked frames that move/resize together (<JoinRows/>)
  • ▶️ Playhead with drag, click-seek, and a built-in play loop (<Playhead/>)
  • ✋ Pan by click-drag (<PanScroll/>)
  • 🚫 Blocked time ranges on rows and sections (<BlockedAreas/>)
  • 🔍 Section filtering — show only specific sections at runtime
  • 🪝 Optimistic add/remove with server-confirm and auto-revert
  • 🎨 Fully customizable via slots and a single overridable theme stylesheet
  • ⚡ Sticky section headers, transform-based axis rendering

🚀 Installation

npm install @asky-ai2/vue-timeline-editor

Peer dependency: Vue ^3.3.

Import the base theme once in your app entry:

import '@asky-ai2/vue-timeline-editor/basic-theme.css';

⚡ Quick start

<script setup lang="ts">
import {
  Timeline, Sections, Dnd, Resize, Snapping, SnapGuideLines,
} from '@asky-ai2/vue-timeline-editor';
import '@asky-ai2/vue-timeline-editor/basic-theme.css';

const sections = [
  {
    title: 'Section 1',
    uuid: 'section1',
    rows: [
      {
        uuid: 'row1',
        title: 'Row 1',
        frames: [
          { uuid: 'frame1', title: 'Booking', start_ms: 0, end_ms: 2 * 60 * 60 * 1000 },
        ],
      },
    ],
  },
];
</script>

<template>
  <Timeline :initial-range="{ start_seconds: 0, end_seconds: 24 * 60 * 60 }">
    <Sections :sections="sections" />
    <Dnd />
    <Resize />
    <Snapping />
    <SnapGuideLines />
  </Timeline>
</template>

<Timeline> is the only required component. Everything inside it is optional — mount a feature component to enable that behavior, omit it to drop the code.


🧩 Components

<Timeline>

Root component. Provides the timeline context to every descendant feature.

| Prop | Type | Default | Description | |------|------|---------|-------------| | initialRange | { start_seconds, end_seconds } | 0 → 24h | Visible time window. | | timeAxisTimeFormat | string | 'HH:mm:ss' | Time-axis label format (e.g. 'hh:mm a'). |

| Event | Payload | Description | |-------|---------|-------------| | @init | TimelineInitInterface | Fires once mounted — exposes the imperative API. | | @scroll | Event | Editor scrolled. |

| Slot | Props | Description | |------|-------|-------------| | #frame | { uuid, title, startMs, endMs, meta, selected, ... } | Replaces a frame's inner content. | | #rowLabel | { uuid, title, sectionUuid } | Replaces a row's label in the row axis. |

Reactive slot content. Slot templates render in your component's scope, so they can close over your own reactive state:

<script setup>
const rowAccents = ref({});
</script>

<template>
  <Timeline>
    <template #rowLabel="{ uuid, title }">
      <span :style="{ color: rowAccents[uuid] }">{{ title }}</span>
    </template>
  </Timeline>
</template>

<Sections>

Initializes section/row/frame data and provides click-to-add on empty areas.

| Prop | Type | Default | Description | |------|------|---------|-------------| | sections | TimelineSectionInterface[] | — | The section/row/frame tree. | | filteredSections | (string \| number)[] \| null | null | Show only the listed section UUIDs. null shows all. Changes apply instantly without re-initializing frame data. | | rowClickable | boolean | false | Enable the empty-area new-frame suggestion globally. | | newFrameLabel | string | '+ Add' | Touch-mode label text. | | availableSlots | { uuid, available_slots: { start_ms, end_ms }[] }[] | — | Allowlist: dims/locks the editor except these slots. Clears to exit mode. | | selectedRowUuids | (string \| number)[] | — | Row UUIDs whose allowed slots render selected. | | frameHoldDuration | number | 600 | Press-and-hold time (ms) before @frame-hold fires. | | frameHoldThreshold | number | 8 | Movement tolerance (px) that cancels the hold. | | silentExternalSelection | boolean | false | When true, @frame-selected / @frame-deselected only fire for user-driven clicks. |

| Event | Payload | Description | |-------|---------|-------------| | @add-frame | { rowUuid, sectionUuid, start_ms, end_ms } | Empty area activated. | | @select-slot | { rowUuid, sectionUuid, start_ms, end_ms } | An allowlist slot was clicked. | | @frame-hold | (frame, event) | A frame was pressed and held. | | @frame-selected | (frame) | A frame became the primary selection. | | @frame-deselected | (frame) | A frame was deselected (always before @frame-selected). |

| Slot | Props | Description | |------|-------|-------------| | #label | { rowUuid, sectionUuid, start_ms, end_ms, length_ms } | Overrides the touch-mode suggestion label. |

Section filteringfilteredSections lets you show only a subset of sections without discarding frame data. The editor height, grid, snapping, and row suggestions all recalculate as if only the visible sections exist. Restoring null brings everything back instantly.

<Sections :sections="sections" :filtered-sections="['floor-1', 'floor-2']" />

<Dnd>

Drag frames to move them. Drags the whole selection together.

  • Prop: edgeSnap (boolean, default true).
  • Events: @dragStart, @dragEnd, @drop, @dragCancel, @dragBlocked.
  • Slot: #highlighter — replaces the drop-target indicator.

<Resize>

Resize a frame from either edge.

  • Events: @resizeStart, @resizeEnd, @resized, @resizeCancel, @resizeBlocked.

<Snapping>

Snaps drag/resize to the grid, frame edges, and rows, and protects against overlap. Each pipeline step is a toggle prop (all default true):

rows, frames, times, guides, overlapping.

<Snapping :overlapping="false" />  <!-- allow overlapping frames -->

<SnapGuideLines>

Renders grid lines and active snap guides.

  • Props: majorGrid (true), minorGrid (false), activeSnapGuides (true).
  • Slots: #majorGrid, #minorGrid, #activeSnapGuide.

<JoinRows>

Frames sharing a linkGroupUuid move and resize as one linked group. No props — mount alongside <Dnd/> / <Resize/>.


<PanScroll>

Click-and-drag panning (mouse only; touch scrolls natively). No props.


<Playhead>

A draggable position marker with a ruler handle, a full-height line, and an off-screen edge indicator.

| Prop | Type | Default | Description | |------|------|---------|-------------| | v-model | number | — | Playhead position in ms (two-way). | | v-model:playing | boolean | — | Playback running state (two-way). | | rate | number | 1 | Timeline-ms advanced per real-ms while playing. | | loop | boolean | false | Wrap to range start instead of pausing at end. | | updateInterval | number | 0 | Throttle reactive position updates. 0 = every frame, 1000 = once per second. The internal position still advances every frame; only the reactive v-model write is throttled. | | draggable | boolean | true | Allow dragging / ruler scrubbing. | | clickSeek | boolean | true | Allow clicking the ruler to jump. | | onTimelineMountedScrollToPlayHead | boolean | false | Scroll the playhead into view once after mount. |

| Event | Description | |-------|-------------| | @seek | Fired on seek (drag end or click). | | @dragStart / @dragEnd | Drag lifecycle. |

#handle slot — the slot receives secondsToTimeString as a prop so you can display the current time in the handle without importing a utility:

<Playhead v-model="posMs">
  <template #handle="{ currentMs, secondsToTimeString }">
    <div class="my-handle">
      {{ secondsToTimeString(currentMs / 1000) }}
    </div>
  </template>
</Playhead>

Out of range, the playhead hides and an edge arrow points toward it.


<BlockedAreas>

Renders visual overlays for blocked time windows on individual rows or entire sections. Blocked areas are purely visual — they do not prevent drag/resize by default.

<BlockedAreas
  :row-blockages="rowBlockages"
  :section-blockages="sectionBlockages"
/>

| Prop | Type | Description | |------|------|-------------| | rowBlockages | RowBlockage[] \| null | Row-level blockages. | | sectionBlockages | SectionBlockage[] \| null | Section-level blockages (covers all rows). |

interface RowBlockage {
  rowUuids: (string | number)[];   // rows to block — any order
  start_ms?: number | null;        // null / omit = full timeline width
  end_ms?: number | null;
  full_row?: boolean;              // explicit "all day" intent flag
  block_ulid: string;              // stable id for grouping / click handlers
  title?: string | null;
}

interface SectionBlockage {
  sectionUuid: string | number;
  start_ms?: number | null;
  end_ms?: number | null;
  full_row?: boolean;
  block_ulid: string;
  title?: string | null;
}

Sequential row merging — consecutive rowUuids in a single RowBlockage are merged into one visual block. Non-consecutive rows produce separate blocks. A single blockage with rows [1, 2, 3, 5] produces two blocks: one spanning rows 1–3, one for row 5.

Section vs row appearance — section blockages have no top/bottom border in the default theme (they cover all rows). The #blockage slot receives isSection so you can differentiate styling.

#blockage slot — a single slot applied to every rendered block:

<BlockedAreas :row-blockages="blockages">
  <template #blockage="{ title, isSection, rowUuids, block_ulid, blockage }">
    <div class="my-block" @click="openSidebar(block_ulid)">
      {{ title }}
    </div>
  </template>
</BlockedAreas>

Slot props: key, top, left, width, height, isSection, rowUuids, title, block_ulid, full_row, blockage (original object).


🗂 Data shapes

interface TimelineSectionInterface {
  uuid: string | number;
  title: string;
  rows: TimelineRowInterface[];
}

interface TimelineRowInterface {
  uuid: string | number;
  title: string;
  frames: TimelineFrameInterface[];
  new_frame_ms?: number;          // optional click-to-add length hint
}

interface TimelineFrameInterface {
  uuid: string | number;
  title: string | null;
  start_ms: number;               // absolute ms
  end_ms: number;
  linkGroupUuid?: string | number; // links frames for <JoinRows/>
  meta?: unknown;                  // arbitrary payload, passed to #frame slot
}

All times are absolute milliseconds. The visible window is set in seconds via initialRange.


🔌 Imperative API

@init hands back the live API:

import type { TimelineInitInterface } from '@asky-ai2/vue-timeline-editor';

const onInit = (api: TimelineInitInterface) => {
  // api.timeline  — data + frame CRUD
  // api.config    — reactive config (range, zoom, pixel-per-ms, …)
  // api.features  — mounted feature instances

  // Frame CRUD
  const handle = api.timeline.addFrame(
    { uuid: 'tmp', title: 'New', start_ms: 0, end_ms: 3600000, rowUuid: 'row1', sectionUuid: 'section1' },
    async (frame) => ({ uuid: await saveToServer(frame) }), // optional server sync
  );
  handle.promise
    .then(() => console.log('saved as', handle.uuid))
    .catch(() => console.log('reverted'));

  api.timeline.removeFrame('frame1', () => deleteOnServer('frame1'));
  api.timeline.updateFrame('frame1', { ...patch });
  api.timeline.syncFrame({ uuid: 'frame1', ...data });   // upsert
  api.timeline.syncFrames([...frames]);                   // bulk upsert

  // Section data (also callable async, after @init)
  api.timeline.initSections(sections);
  api.timeline.setSectionFilter(['floor-1']);  // reactive filter

  // Navigation
  api.timeline.scrollToViewPort('frame1', /* highlight */ true);

  // Pending / loading state
  api.timeline.setPending('frame1', true);
  api.timeline.setLoadingMode('immediate');

  // Selection (requires <Sections/>)
  api.features.data.frames.getSelectedFrames();

  // Playhead (requires <Playhead/>)
  api.features.data.playhead?.seek(3600000);
  api.features.data.playhead?.play();
  api.features.data.playhead?.pause();
};

addFrame / removeFrame are optimistic — the change applies immediately; if the sync/confirm callback rejects, it auto-reverts. No boilerplate rollback code needed.


🎨 Theming

All visuals come from basic-theme.css. Override any .vtd__* class in your own CSS to restyle. Structural rules are injected automatically from the package bundle; colors, borders, and typography live in basic-theme.css, which you import and may override freely.


🌳 Tree-shaking

Feature components are also published as individual entry points:

import { Timeline } from '@asky-ai2/vue-timeline-editor';
import Dnd          from '@asky-ai2/vue-timeline-editor/features/dnd';
import Snapping     from '@asky-ai2/vue-timeline-editor/features/snapping';
import Sections     from '@asky-ai2/vue-timeline-editor/features/sections';
import Resize       from '@asky-ai2/vue-timeline-editor/features/resize';
import Playhead     from '@asky-ai2/vue-timeline-editor/features/playhead';

👤 Author

Mohammudullah Khan[email protected]


📄 License

MIT