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

vue3-mycalendar

v0.7.3

Published

A calendar and scheduling package for Vue 3

Readme

vue3-mycalendar

A powerful and customizable calendar component for Vue 3 with drag & drop support, custom forms, popups, and full theming capabilities.

npm version license

✨ Features

  • 📅 Weekly calendar view with hour grid
  • 🖱️ Drag & drop events between days and times
  • 📝 Customizable event forms via Scoped Slots
  • 🎨 Full theming with CSS variables
  • 🌙 Built-in Light & Dark mode
  • 📋 Event popups with todos & participants
  • 🔧 TypeScript support

Installation

npm install vue3-mycalendar

Quick Start

<template>
  <ScheduleForm 
    :schedules="schedules" 
    :additional-fields="additionalFields" 
    custom-class="my-calendar"
    :labels-and-settings="labelsAndSettings"
    :popup-fields="popupFields"
    @submit-event="handleNewEvent"
    @handle-delete="handleDeleteEvent"
    @update-event="handleUpdateEvent"
  />
</template>

<script setup lang="ts">
import { ref, computed } from 'vue';
import { ScheduleForm } from 'vue3-mycalendar';
import 'vue3-mycalendar/dist/style.css';

const schedules = ref([
  { 
    id: 1, 
    title: 'Meeting', 
    date: '2025-07-04', 
    start: '09:00', 
    end: '10:00', 
    color: '#e2Be33',
    teacher: 'Malika Heaney', 
    room: 'Room 1',
    todos: ['Prepare agenda', 'Print documents'],
    participants: ['Malika', 'Jonas']
  },
  { 
    id: 2, 
    title: 'Workshop', 
    date: '2025-07-04', 
    start: '13:00', 
    end: '15:00', 
    color: '#33C3FF',
    teacher: 'John Doe', 
    room: 'Room 2'
  },
]);

const additionalFields = ref([
  { id: 'title', label: 'Title', type: 'text', model: 'title' },
  { id: 'teacher', label: 'Teacher', type: 'text', model: 'teacher' },
  { 
    id: 'room', 
    label: 'Room', 
    type: 'select', 
    model: 'room', 
    options: [
      { id: 1, name: 'Room 1' }, 
      { id: 2, name: 'Room 2' }
    ] 
  },
]);

const popupFields = ref(['title', 'date', 'start', 'end', 'teacher', 'room']);

const labelsAndSettings = computed(() => ({
  startTimeLabel: 'Start Time',
  endTimeLabel: 'End Time',
  dateLabel: 'Date',
  submitButtonText: 'Add Event',
  calendarWeekLabel: 'Week',
  todosLabel: 'To-Do',
  participantsLabel: 'Participants',
}));

const handleNewEvent = (event) => {
  const newId = Math.max(...schedules.value.map(e => e.id), 0) + 1;
  schedules.value.push({ ...event, id: newId });
};

const handleDeleteEvent = (id) => {
  schedules.value = schedules.value.filter(event => event.id !== id);
};

const handleUpdateEvent = (updatedEvent) => {
  const index = schedules.value.findIndex(e => e.id === updatedEvent.id);
  if (index !== -1) {
    schedules.value[index] = updatedEvent;
  }
};
</script>

<style>
.my-calendar {
  --form-bg-color: #f8fafc;
  --button-bg-color: #3b82f6;
  --label-color: #334155;
}
</style>

🎨 Custom Form Slot

Replace the default form with your own design using the form slot. You get full access to the internal state and methods via Scoped Slot Props.

Slot Props

| Prop | Type | Description | |------|------|-------------| | newEvent | Ref<Partial<EventInfo>> | Reactive event object for v-model binding | | addEvent | (event?: Partial<EventInfo>) => void | Submit function to add the event | | additionalFields | Field[] | Configured additional fields array | | labels | LabelsAndSettings | All label texts and settings | | resetForm | () => void | Reset form to initial state |

Example: Custom Styled Form

<template>
  <ScheduleForm 
    :schedules="schedules" 
    :additional-fields="additionalFields"
    @submit-event="handleNewEvent"
  >
    <template #form="{ newEvent, addEvent, labels, resetForm }">
      <div class="custom-form">
        <h3>📅 Create New Event</h3>
        
        <div class="form-grid">
          <input 
            v-model="newEvent.title" 
            type="text" 
            placeholder="Event title..."
            class="input-field"
          />
          
          <input 
            v-model="newEvent.date" 
            type="date"
            class="input-field"
          />
          
          <div class="time-row">
            <input v-model="newEvent.start" type="time" class="input-field" />
            <span>to</span>
            <input v-model="newEvent.end" type="time" class="input-field" />
          </div>
          
          <select v-model="newEvent.room" class="input-field">
            <option value="">Select room...</option>
            <option value="Room 1">Room 1</option>
            <option value="Room 2">Room 2</option>
          </select>
          
          <input 
            v-model="newEvent.color" 
            type="color" 
            class="color-picker"
          />
        </div>
        
        <div class="form-actions">
          <button type="button" @click="resetForm" class="btn-secondary">
            Reset
          </button>
          <button type="button" @click="addEvent()" class="btn-primary">
            {{ labels.submitButtonText }}
          </button>
        </div>
      </div>
    </template>
  </ScheduleForm>
</template>

<style scoped>
.custom-form {
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  padding: 24px;
  border-radius: 16px;
  margin-bottom: 20px;
  color: white;
}

.custom-form h3 {
  margin: 0 0 20px 0;
  font-size: 1.25rem;
}

.form-grid {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.time-row {
  display: flex;
  align-items: center;
  gap: 12px;
}

.input-field {
  padding: 12px 16px;
  border: none;
  border-radius: 8px;
  font-size: 14px;
  background: rgba(255, 255, 255, 0.95);
}

.color-picker {
  width: 60px;
  height: 40px;
  border: none;
  border-radius: 8px;
  cursor: pointer;
}

.form-actions {
  display: flex;
  gap: 12px;
  margin-top: 20px;
}

.btn-secondary {
  padding: 12px 24px;
  background: rgba(255, 255, 255, 0.2);
  border: none;
  border-radius: 8px;
  color: white;
  cursor: pointer;
  transition: background 0.2s;
}

.btn-secondary:hover {
  background: rgba(255, 255, 255, 0.3);
}

.btn-primary {
  flex: 1;
  padding: 12px 24px;
  background: white;
  border: none;
  border-radius: 8px;
  color: #764ba2;
  font-weight: 600;
  cursor: pointer;
  transition: transform 0.2s;
}

.btn-primary:hover {
  transform: translateY(-2px);
}
</style>

Example: Minimal Form (Only Required Fields)

<template>
  <ScheduleForm :schedules="schedules" :additional-fields="[]">
    <template #form="{ newEvent, addEvent }">
      <div class="minimal-form">
        <input v-model="newEvent.title" placeholder="Title" />
        <input v-model="newEvent.date" type="date" />
        <input v-model="newEvent.start" type="time" />
        <input v-model="newEvent.end" type="time" />
        <button @click="addEvent()">Add</button>
      </div>
    </template>
  </ScheduleForm>
</template>

Example: No Form (Calendar Only)

<template>
  <ScheduleForm :schedules="schedules" :additional-fields="[]">
    <!-- Empty slot removes the form completely -->
    <template #form></template>
  </ScheduleForm>
</template>

🪟 Custom Popup Slot

Customize the event details popup with the popup-calendar slot.

<template>
  <ScheduleForm 
    :schedules="schedules"
    :additional-fields="additionalFields"
    :popup-visible="isPopupVisible"
    :popup-event="selectedEvent"
    @show-info="openPopup"
    @close-popup="closePopup"
  >
    <template #popup-calendar>
      <div v-if="isPopupVisible && selectedEvent" class="my-popup-overlay">
        <div class="my-popup">
          <h2>{{ selectedEvent.title }}</h2>
          <p>📅 {{ selectedEvent.date }}</p>
          <p>🕐 {{ selectedEvent.start }} - {{ selectedEvent.end }}</p>
          <p>📍 {{ selectedEvent.room }}</p>
          
          <button @click="closePopup">Close</button>
        </div>
      </div>
    </template>
  </ScheduleForm>
</template>

<script setup>
const isPopupVisible = ref(false);
const selectedEvent = ref(null);

const openPopup = (event) => {
  selectedEvent.value = event;
  isPopupVisible.value = true;
};

const closePopup = () => {
  isPopupVisible.value = false;
  selectedEvent.value = null;
};
</script>

📋 Props

Required Props

| Prop | Type | Description | |------|------|-------------| | schedules | EventInfo[] | Array of events to display on the calendar | | additionalFields | Field[] | Additional fields for the event form |

Optional Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | customClass | string | '' | Custom CSS class for styling | | customStyles | Record<string, any> | {} | Inline styles for the root element | | weekdays | string[] | ['Monday', ...] | Custom weekday names | | eventTitleColor | string | '#000' | Event title text color | | eventTitleSize | string | '16px' | Event title font size | | popupFields | string[] | [] | Fields to show in the default popup | | labelsAndSettings | LabelsAndSettings | See below | Labels and text customization | | placeholderSettings | object | {} | Placeholder texts for popup inputs | | popupVisible | boolean | false | Control popup visibility externally | | popupEvent | EventInfo \| null | null | Current event for the popup |

Labels and Settings

interface LabelsAndSettings {
  startTimeLabel?: string;      // Default: 'Start Time'
  endTimeLabel?: string;        // Default: 'End Time'
  dateLabel?: string;           // Default: 'Date'
  submitButtonText?: string;    // Default: 'Add Entry'
  calendarWeekLabel?: string;   // Default: 'CW'
  todosLabel?: string;          // Default: 'To-Do'
  participantsLabel?: string;   // Default: 'Teammates'
}

📡 Events

| Event | Payload | Description | |-------|---------|-------------| | submit-event | EventInfo | New event submitted via form | | update-event | EventInfo | Event updated (drag & drop or edit) | | handle-delete | number | Event ID to delete | | show-info | EventInfo | Event details requested | | close-popup | – | Popup closed | | update:todos | { todos: string[], eventId: number } | Todos changed | | update:participants | { participants: string[], eventId: number } | Participants changed |


🎭 Slots

| Slot | Scoped Props | Description | |------|--------------|-------------| | form | { newEvent, addEvent, additionalFields, labels, resetForm } | Custom event form | | popup-calendar | – | Custom popup for event details |


📊 Type Definitions

EventInfo

interface EventInfo {
  id: number;
  title: string;
  date: string;           // Format: 'YYYY-MM-DD'
  start: string;          // Format: 'HH:MM'
  end: string;            // Format: 'HH:MM'
  color: string;          // Hex color, e.g. '#3b82f6'
  info?: string;
  todos?: string[];
  participants?: string[];
  [key: string]: any;     // Additional custom fields
}

Field

interface Field {
  id: string;
  label: string;
  type: 'text' | 'select' | 'date' | 'time' | 'number';
  model: string;
  options?: { id: number | string; name: string }[];
}

🎨 Styling

CSS Variables

Override these variables via customClass:

<ScheduleForm custom-class="my-calendar" ... />
.my-calendar {
  /* Form */
  --form-bg-color: #ffffff;
  --form-padding: 1rem;
  --form-border-radius: 8px;
  --form-border: 1px solid #ccc;
  
  /* Labels */
  --label-color: #333333;
  --label-font-weight: bold;
  
  /* Inputs */
  --input-height: 40px;
  --input-padding: 0.5rem;
  --input-font-size: 1rem;
  --input-border: 1px solid #ccc;
  --input-border-radius: 6px;
  --input-bg-color: #ffffff;
  --input-color: #000000;
  
  /* Buttons */
  --button-bg-color: #007bff;
  --button-hover-bg-color: #0056b3;
  --button-color: #ffffff;
  --button-padding: 0.6rem 1rem;
  --button-border-radius: 6px;
  
  /* Calendar */
  --calendar-primary-color: #a4d8ff;
  --grid-border-color: #cccccc;
  --event-border-radius: 8px;
  
  /* Navigation */
  --arrow-button-bg: #007bff;
  --arrow-button-color: #ffffff;
  --current-week-color: #000000;
  
  /* Popup */
  --popup-bg: #ffffff;
  --popup-overlay-bg: rgba(0, 0, 0, 0.5);
  --popup-border-radius: 5px;
  
  /* Danger */
  --danger-bg: #e53e3e;
  --danger-text: #ffffff;
}

Dark Mode

Option A: Per Component

<ScheduleForm :custom-class="isDark ? 'my-calendar dark' : 'my-calendar'" ... />

Option B: Global (affects entire page)

// Enable dark mode
document.documentElement.classList.add('dark');

// Disable dark mode
document.documentElement.classList.remove('dark');

Theme Toggle Example

<script setup>
import { ref, computed, onMounted } from 'vue';

const isDark = ref(false);

const syncTheme = () => {
  document.documentElement.classList.toggle('dark', isDark.value);
};

const toggleTheme = () => {
  isDark.value = !isDark.value;
  syncTheme();
};

onMounted(() => {
  // Initialize from system preference
  isDark.value = window.matchMedia('(prefers-color-scheme: dark)').matches;
  syncTheme();
});

const calendarClass = computed(() => 
  isDark.value ? 'my-calendar dark' : 'my-calendar'
);
</script>

<template>
  <button @click="toggleTheme">
    {{ isDark ? '☀️ Light' : '🌙 Dark' }}
  </button>
  
  <ScheduleForm :custom-class="calendarClass" ... />
</template>

📸 Screenshots

Light Mode

Light Mode

Dark Mode

Dark Mode


🗺️ Roadmap

  • Quickly jump back to the current week
  • Visual indicator for the current day
  • Click on empty cell to create a new event
  • Full month overview with event indicators

🤝 Contributing

Contributions are welcome! Please open an issue or submit a pull request.


👨‍💻 About Me

I'm a motivated developer working as a full-stack engineer in the cloud department of a cybersecurity company. I'm dedicated to building secure and robust applications while continuously expanding my skills.


📄 License

MIT


💬 Support

If you have questions or need help, please open an issue.