サイドバープラグイン

サイドバープラグインは、DayFlow カレンダーにカレンダー管理用のサイドバーを追加します。これには、日付移動用のミニカレンダー、表示トグル付きのカレンダーリスト、およびカレンダーの完全な CRUD 操作(作成、名前変更、色変更、統合、削除、インポート/エクスポート)が含まれています。

インストール

パッケージをインストールします:

npm install @dayflow/plugin-sidebar
pnpm add @dayflow/plugin-sidebar
yarn add @dayflow/plugin-sidebar
bun add @dayflow/plugin-sidebar

使い方

import { useCalendarApp, DayFlowCalendar } from '@dayflow/react';
import { createSidebarPlugin } from '@dayflow/plugin-sidebar';

function MyCalendar() {
  const sidebarPlugin = createSidebarPlugin({
    width: 280,
    createCalendarMode: 'modal',
  });

  const calendar = useCalendarApp({
    views: [
      /* ビュー */
    ],
    plugins: [sidebarPlugin],
  });

  return <DayFlowCalendar calendar={calendar} />;
}
<template>
  <DayFlowCalendar :calendar="calendar" />
</template>

<script setup>
import { DayFlowCalendar, useCalendarApp } from '@dayflow/vue';
import { createSidebarPlugin } from '@dayflow/plugin-sidebar';

const sidebarPlugin = createSidebarPlugin({
  width: 280,
  createCalendarMode: 'modal',
});

const calendar = useCalendarApp({
  views: [
    /* ビュー */
  ],
  plugins: [sidebarPlugin],
});
</script>
import { Component } from '@angular/core';
import { DayFlowCalendarModule } from '@dayflow/angular';
import { createSidebarPlugin } from '@dayflow/plugin-sidebar';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [DayFlowCalendarModule],
  template: `<dayflow-calendar [calendar]="calendar"></dayflow-calendar>`
})
export class AppComponent {
  calendar = {
    views: [
      /* ビュー */
    ],
    plugins: [
      createSidebarPlugin({
        width: 280,
        createCalendarMode: 'modal',
      })
    ]
  };
}
<script>
  import { DayFlowCalendar, useCalendarApp } from '@dayflow/svelte';
  import { createSidebarPlugin } from '@dayflow/plugin-sidebar';

  const sidebarPlugin = createSidebarPlugin({
    width: 280,
    createCalendarMode: 'modal',
  });

  const calendar = useCalendarApp({
    views: [
      /* ビュー */
    ],
    plugins: [sidebarPlugin],
  });
</script>

<DayFlowCalendar {calendar} />

設定リファレンス

プロパティデフォルト説明
widthnumber | string'240px'サイドバーの幅(例: 280 または '20rem'
miniWidthstring'50px'折りたたみ時のサイドバーの幅
initialCollapsedbooleanfalseサイドバーを折りたたんだ状態で開始するかどうか
createCalendarMode'inline' | 'modal''inline'カレンダー作成フォームの表示方法
colorPickerMode'blossom' | 'default''default'使用するカラーピッカーコンポーネント
onSubscribeCalendar(calendar, events) => Promise<void>undefined新しい購読が追加された後のコールバック
onLoadSubscription(calendar) => Promise<void>undefined既存の購読用のカスタムローダー
onReorder(calendars: CalendarType[]) => void | Promise<void>undefinedカレンダーの並べ替え後のコールバック
componentsOrder('calendarList' | 'miniCalendar')[]['calendarList', 'miniCalendar']サイドバーコンポーネントの表示順序
render(props: CalendarSidebarRenderProps) => TNodeundefinedサイドバー UI を完全にオーバーライドします
renderSidebarHeader(args: SidebarHeaderSlotArgs) => TNodeundefinedカスタムサイドバーヘッダーレンダラー
renderCalendarContextMenu(calendar, onClose) => TNodeundefinedカレンダー項目のカスタムコンテキストメニューレンダラー
renderCreateCalendarDialog(props) => TNodeundefinedカスタムカレンダー作成ダイアログレンダラー

プログラマティック API

カレンダーのレンダリング後、app.getPluginSidebarService を取得できます:

import { type SidebarService } from '@dayflow/plugin-sidebar';

const sidebar = app.getPlugin<SidebarService>('sidebar');

sidebar?.collapse(); // サイドバーを折りたたむ
sidebar?.expand(); // サイドバーを展開する
sidebar?.setCollapsed(true); // 状態を明示的に設定する
console.log(sidebar?.isCollapsed()); // 現在の状態を取得する

SidebarService

メソッド返り値説明
collapse()voidサイドバーを折りたたむ
expand()voidサイドバーを展開する
setCollapsed(collapsed: boolean)void折りたたみ状態を明示的に設定する
isCollapsed()boolean現在の折りたたみ状態を返す

キーボードショートカットへのバインド

window.addEventListener('keydown', e => {
  if (e.metaKey && e.key === '[') {
    const sidebar = app.getPlugin<SidebarService>('sidebar');
    sidebar?.setCollapsed(!sidebar.isCollapsed());
  }
});

機能

サイドバープラグインは、可視状態のトグル、カレンダーの配色表示、折りたたみボタンを備えた既製のレイアウトを提供します。また、カレンダーの作成とコンテキストメニューもサポートしており、ユーザーがカレンダーの追加、色の編集、削除を自由に行うことができます。

組み込み機能:

  • ミニカレンダー - 日付の素早いナビゲーション用のコンパクトな月表示
  • カレンダーリスト - 色分けされたチェックボックスで個々のカレンダーの表示を切替
  • カレンダー作成 - カスタム名と色で新しいカレンダーを追加
  • 名前変更 / 色変更 - カレンダーを右クリックして名前や色を変更
  • カレンダーの並べ替え - リスト内でカレンダーをドラッグ&ドロップして並べ替えることができます。onReorder コールバックを使用して、新しい順序をバックエンドに保存できます。
  • カレンダー統合 - あるカレンダーの全イベントを別のカレンダーに移動
  • カレンダー削除 - 確認ステップ付きでカレンダーを削除(事前に統合も可能)
  • インポート / エクスポート - .ics 文件のインポートまたはカレンダーを .ics にエクスポート
  • カレンダーを登録 - ICS URL でリモートの .ics フィードを登録でき、登録済みカレンダーにはサイドバーリストにバッジアイコンが表示されます

デモ

DayFlow のサイドバー体験を 2 つのデモで紹介します。標準 UI のまま使うパターンと、完全カスタムに置き換えるパターンを比較しながら確認してください。

見どころ

  • 表示の同期: 可視状態を切り替えるとカレンダー側もリアルタイムに反映
  • ドラッグ & 同期: ビューを切り替えてもカスタムサイドバーに表示する「次の予定」や KPI を同期可能
  • 折りたたみ: 集中したいときは折りたたんで表示領域を広げられる

カレンダーを登録

サイドバーにはリモート ICS カレンダーフィードを登録する機能が組み込まれています。サイドバーのカレンダーリストまたは空き領域を右クリックしてコンテキストメニューを開き、カレンダーを登録を選択し、公開されている .ics URL を入力するだけで利用できます。

DayFlow はファイルを取得してイベントを解析し、新しいカレンダーを自動的に作成します。登録済みカレンダーはサイドバーリストに小さなバッジアイコンが表示され、ローカルで作成したカレンダーと区別できます。

自動読み込みと重複排除

初期設定で subscription.url を持つカレンダーを提供すると、サイドバープラグインはマウント時に自動的に最新のイベントを取得します。

ローカルにキャッシュされたイベントと、新しく取得した購読データを組み合わせる際の二重表示を防ぐため、DayFlow はIDベースの重複排除を実装しています:

  • 購読から取得したイベントが、コアストア内の既存イベントと同じ id を持っている場合、購読側のバージョンが優先されます。
  • これにより、ページをリロードした後にイベントが「二重に」表示されることなく、常に最新の情報を UI に表示できます。

購読カレンダーの永続化

DayFlow は CalendarTypesubscription オブジェクトが存在するかどうかで購読カレンダーを識別します。

購読カレンダーのデフォルト動作:

  • 読み取り専用:タイトル編集、カレンダー変更、削除などの UI 操作が canMutateFromUI() を介して無効化されます。
  • ドラッグ不可:ドラッグ&ドロップ操作(移動、リサイズ)が無効化されます。
  • 備考の非表示:イベントに説明がない場合、詳細パネルの「備考」フィールドは非表示になり、UI が整理されます。

ユーザーに購読イベントの編集や移動を許可したい場合は、カレンダーオブジェクトに readOnly: false を設定することで、これらのデフォルトの保護を明示的に上書きできます:

const calendar = useCalendarApp({
  calendars: [
    {
      id: 'team-ics',
      name: 'チームカレンダー',
      subscription: {
        url: 'https://example.com/calendar.ics',
        status: 'ready',
      },
      // デフォルトの保護を上書き:
      readOnly: false, // UI 操作とドラッグ&ドロップの両方を有効化
      colors: {
        /* ... */
      },
    },
  ],
});

カレンダーの状態をバックエンドに保存する際は subscription フィールドを保持し、次回の読み込み時に復元してください。

注意: DayFlow は登録済みフィードを自動更新しません。イベントを最新の状態に保つには、アプリ側で定期的な取得を実装し、app.addEvent() / app.removeEvent() でイベントを更新してください。

完全なカスタマイズ

独自のサイドバーコンポーネントを使用することも可能です。createSidebarPluginrender にレンダリング関数を渡すか(React)、コンポーネントスロット/テンプレート(Vue、Angular、Svelte)を使用します。

import { useCalendarApp, DayFlowCalendar } from '@dayflow/react';
import {
  createSidebarPlugin,
  type CalendarSidebarRenderProps,
} from '@dayflow/plugin-sidebar';

const CustomSidebar = ({
  app,
  calendars,
  toggleCalendarVisibility,
  isCollapsed,
  setCollapsed,
}: CalendarSidebarRenderProps) => {
  if (isCollapsed) {
    return (
      <div className='p-2'>
        <button onClick={() => setCollapsed(false)}>→</button>
      </div>
    );
  }

  return (
    <aside className='flex h-full flex-col gap-4 p-4 bg-slate-50 border-r'>
      <header className='flex items-center justify-between'>
        <h3 className='font-semibold'>マイワークスペース</h3>
        <button onClick={() => setCollapsed(true)}>←</button>
      </header>

      <nav className='space-y-1'>
        {calendars.map(calendar => (
          <label
            key={calendar.id}
            className='flex items-center gap-2 cursor-pointer'
          >
            <input
              type='checkbox'
              checked={calendar.isVisible}
              onChange={e =>
                toggleCalendarVisibility(calendar.id, e.target.checked)
              }
            />
            <span
              className='w-3 h-3 rounded-full'
              style={{ backgroundColor: calendar.colors.lineColor }}
            />
            {calendar.name}
          </label>
        ))}
      </nav>

      <div className='mt-auto pt-4 border-t text-xs text-slate-500'>
        総イベント数:{app.getEvents().length}
      </div>
    </aside>
  );
};

function App() {
  const calendar = useCalendarApp({
    views: [/* ビュー */],
    plugins: [
      createSidebarPlugin({
        render: props => <CustomSidebar {...props} />,
      }),
    ],
  });

  return <DayFlowCalendar calendar={calendar} />;
}
<template>
  <DayFlowCalendar :calendar="calendar">
    <!-- スコープ付きスロット 'sidebar' を使用してオーバーライド -->
    <template #sidebar="args">
      <aside v-if="!args.isCollapsed" class="flex h-full flex-col gap-4 p-4 bg-slate-50 border-r">
        <header class="flex items-center justify-between">
          <h3 class="font-semibold">マイワークスペース</h3>
          <button @click="args.setCollapsed(true)">←</button>
        </header>

        <nav class="space-y-1">
          <label
            v-for="cal in args.calendars"
            :key="cal.id"
            class="flex items-center gap-2 cursor-pointer"
          >
            <input
              type="checkbox"
              :checked="cal.isVisible"
              @change="args.toggleCalendarVisibility(cal.id, !cal.isVisible)"
            />
            <span
              class="w-3 h-3 rounded-full"
              :style="{ backgroundColor: cal.colors.lineColor }"
            />
            {{ cal.name }}
          </label>
        </nav>

        <div class="mt-auto pt-4 border-t text-xs text-slate-500">
          総イベント数:{{ args.app.getEvents().length }}
        </div>
      </aside>
      <div v-else class="p-2">
        <button @click="args.setCollapsed(false)">→</button>
      </div>
    </template>
  </DayFlowCalendar>
</template>

<script setup>
import { DayFlowCalendar, useCalendarApp } from '@dayflow/vue';
import { createSidebarPlugin } from '@dayflow/plugin-sidebar';

const calendar = useCalendarApp({
  views: [/* ビュー */],
  plugins: [createSidebarPlugin()]
});
</script>
<!-- app.component.html -->
<dayflow-calendar
  [calendar]="calendar"
  [sidebar]="customSidebar"
>
</dayflow-calendar>

<ng-template #customSidebar let-args>
  <aside *ngIf="!args.isCollapsed; else collapsedView" class="flex h-full flex-col gap-4 p-4 bg-slate-50 border-r">
    <header class="flex items-center justify-between">
      <h3 class="font-semibold">マイワークスペース</h3>
      <button (click)="args.setCollapsed(true)">←</button>
    </header>

    <nav class="space-y-1">
      <label
        *ngFor="let cal of args.calendars"
        class="flex items-center gap-2 cursor-pointer"
      >
        <input
          type="checkbox"
          [checked]="cal.isVisible"
          (change)="args.toggleCalendarVisibility(cal.id, !cal.isVisible)"
        />
        <span
          class="w-3 h-3 rounded-full"
          [style.background-color]="cal.colors.lineColor"
        ></span>
        {{ cal.name }}
      </label>
    </nav>

    <div class="mt-auto pt-4 border-t text-xs text-slate-500">
      総イベント数:{{ args.app.getEvents().length }}
    </div>
  </aside>

  <ng-template #collapsedView>
    <div class="p-2">
      <button (click)="args.setCollapsed(false)">→</button>
    </div>
  </ng-template>
</ng-template>
<!-- App.svelte -->
<script lang="ts">
  import { DayFlowCalendar, useCalendarApp } from '@dayflow/svelte';
  import { createSidebarPlugin } from '@dayflow/plugin-sidebar';
  import CustomSidebar from './CustomSidebar.svelte';

  const calendar = useCalendarApp({
    views: [/* ビュー */],
    plugins: [createSidebarPlugin()]
  });
</script>

<DayFlowCalendar {calendar} sidebar={CustomSidebar} />

<!-- CustomSidebar.svelte -->
<script lang="ts">
  import type { CalendarSidebarRenderProps } from '@dayflow/core';

  let {
    app,
    calendars,
    isCollapsed,
    setCollapsed,
    toggleCalendarVisibility,
  } = $props<CalendarSidebarRenderProps>();
</script>

{#if isCollapsed}
  <div class="p-2">
    <button onclick={() => setCollapsed(false)}>→</button>
  </div>
{:else}
  <aside class="flex h-full flex-col gap-4 p-4 bg-slate-50 border-r">
    <header class="flex items-center justify-between">
      <h3 class="font-semibold">マイワークスペース</h3>
      <button onclick={() => setCollapsed(true)}>←</button>
    </header>

    <nav class="space-y-1">
      {#each calendars as cal (cal.id)}
        <label class="flex items-center gap-2 cursor-pointer">
          <input
            type="checkbox"
            checked={cal.isVisible}
            onchange={(e) => toggleCalendarVisibility(cal.id, e.currentTarget.checked)}
          />
          <span
            class="w-3 h-3 rounded-full"
            style:background-color={cal.colors.lineColor}
          ></span>
          {cal.name}
        </label>
      {/each}
    </nav>

    <div class="mt-auto pt-4 border-t text-xs text-slate-500">
      総イベント数:{app.getEvents().length}
    </div>
  </aside>
{/if}

カスタムコンテキストメニュー

カレンダー項目のデフォルトの右クリックメニューを置き換えることができます:

createSidebarPlugin({
  renderCalendarContextMenu: (calendar, onClose) => (
    <div className='bg-white shadow-lg border rounded p-2'>
      <button
        onClick={() => {
          console.log('Custom action');
          onClose();
        }}
      >
        Custom Action for {calendar.name}
      </button>
    </div>
  ),
});
<template>
  <DayFlowCalendar :calendar="calendar">
    <template #calendarContextMenu="{ calendar, onClose }">
      <div class="bg-white shadow-lg border rounded p-2">
        <button @click="onClose()">
          {{ calendar.name }} のカスタムアクション
        </button>
      </div>
    </template>
  </DayFlowCalendar>
</template>
<dayflow-calendar
  [calendar]="calendar"
  [calendarContextMenu]="contextMenu"
>
</dayflow-calendar>

<ng-template #contextMenu let-args>
  <div class="bg-white shadow-lg border rounded p-2">
    <button (click)="args.onClose()">
      {{ args.calendar.name }} のカスタムアクション
    </button>
  </div>
</ng-template>
<DayFlowCalendar {calendar} calendarContextMenu={CustomContextMenu} />

<!-- CustomContextMenu.svelte -->
<script lang="ts">
  import type { CalendarContextMenuSlotArgs } from '@dayflow/core';
  let { calendar, onClose } = $props<CalendarContextMenuSlotArgs>();
</script>

<div class="bg-white shadow-lg border rounded p-2">
  <button onclick={() => { console.log('Custom action'); onClose(); }}>
    Custom Action for {calendar.name}
  </button>
</div>

カスタムカレンダー作成ダイアログ

デフォルトのカレンダー作成ダイアログを置き換えることができます:

createSidebarPlugin({
  renderCreateCalendarDialog: ({ onCreate, onClose }) => (
    <MyCustomDialog onSave={onCreate} onCancel={onClose} />
  ),
});
<template>
  <DayFlowCalendar :calendar="calendar">
    <template #createCalendarDialog="{ onCreate, onClose }">
      <MyCustomDialog @save="onCreate" @cancel="onClose" />
    </template>
  </DayFlowCalendar>
</template>
<dayflow-calendar
  [calendar]="calendar"
  [createCalendarDialog]="customDialog"
>
</dayflow-calendar>

<ng-template #customDialog let-args>
  <my-custom-dialog
    (save)="args.onCreate($event)"
    (cancel)="args.onClose()"
  >
  </my-custom-dialog>
</ng-template>
<DayFlowCalendar {calendar} createCalendarDialog={CustomDialog} />

<!-- CustomDialog.svelte -->
<script lang="ts">
  import type { CreateCalendarDialogProps } from '@dayflow/core';
  let { onCreate, onClose } = $props<CreateCalendarDialogProps>();
</script>

<div class="dialog">
  <!-- カスタムダイアログマークアップ -->
  <button onclick={() => onCreate({ id: 'new', name: 'New' })}>作成</button>
  <button onclick={onClose}>キャンセル</button>
</div>

カスタムサイドバーヘッダー

デフォルトのサイドバーヘッダー(「カレンダー」というタイトルと折りたたみボタンがある領域)を置き換えることができます:

createSidebarPlugin({
  renderSidebarHeader: ({ isCollapsed, onCollapseToggle }) => (
    <div className='flex items-center justify-between p-4 border-b'>
      {!isCollapsed && <span className='font-bold text-lg'>My Calendars</span>}
      <button
        onClick={onCollapseToggle}
        className='p-1 hover:bg-slate-100 rounded'
      >
        {isCollapsed ? '→' : '←'}
      </button>
    </div>
  ),
});
<template>
  <DayFlowCalendar :calendar="calendar">
    <template #sidebarHeader="{ isCollapsed, onCollapseToggle }">
      <div class="flex items-center justify-between p-4 border-b">
        <span v-if="!isCollapsed" class="font-bold text-lg">マイカレンダー</span>
        <button
          @click="onCollapseToggle"
          class="p-1 hover:bg-slate-100 rounded"
        >
          {{ isCollapsed ? '→' : '←' }}
        </button>
      </div>
    </template>
  </DayFlowCalendar>
</template>
<dayflow-calendar
  [calendar]="calendar"
  [sidebarHeader]="headerTemplate"
>
</dayflow-calendar>

<ng-template #headerTemplate let-args>
  <div class="flex items-center justify-between p-4 border-b">
    <span *ngIf="!args.isCollapsed" class="font-bold text-lg">マイカレンダー</span>
    <button
      (click)="args.onCollapseToggle()"
      class="p-1 hover:bg-slate-100 rounded"
    >
      {{ args.isCollapsed ? '→' : '←' }}
    </button>
  </div>
</ng-template>
<DayFlowCalendar {calendar} sidebarHeader={CustomHeader} />

<!-- CustomHeader.svelte -->
<script lang="ts">
  import type { SidebarHeaderSlotArgs } from '@dayflow/core';
  let { isCollapsed, onCollapseToggle } = $props<SidebarHeaderSlotArgs>();
</script>

<div class="custom-header">
  {#if !isCollapsed}
    <span>マイカレンダー</span>
  {/if}
  <button onclick={onCollapseToggle}>
    {isCollapsed ? '→' : '←'}
  </button>
</div>

On this page