Sidebar

Sidebarは、Webサイトやアプリケーションの画面左側に表示されるコンポーネントです。
ヘッダーメニュー・ドロップダウンメニューなど、ナビゲーションに必要な要素をまとめて表示できます。

sidebar/standard is coming soon.

プロパティ

Sidebarは、以下のプロパティをサポートしています。

名前 デフォルト値 説明
headerMenu HeaderMenuProps ヘッダー部分に表示するメニュー情報(画像・タイトル・サブタイトルなど)です。
items MenuItems[] サイドバー内に表示するメニュー群です。
startContent Snippet メニュー項目の左側に表示するカスタム要素(アイコンなど)を指定できます。

インストールの手順

以下のコンポーネントのコードを、使いたいプロジェクトにコピー&ペーストします。
パスは実際のプロジェクトの構成にあわせて更新します。

modules/Sidebar.svelte
        <!--
@component
## 概要
- ヘッダーにドロップダウン付きの情報を表示し、カテゴリごとに整理されたメニューリストを描画するサイドバーコンポーネントです。
- 各メニューは1階層のサブメニュー(options)を持つことができ、押下時に任意のコールバックを発火させられます。

## 機能
- ヘッダーに画像・タイトル・サブタイトルを表示
- ヘッダーメニューのドロップダウン表示
- メニューカテゴリごとの区切り表示(Separator付き)
- メニューの階層化
- 選択状態のハイライト
- startContentによるアイコン表示
- 入力フィールドによる検索(※ロジックは未実装)

## Props
- class: 追加のクラスを指定できます
- headerMenu: ヘッダーに表示する画像・タイトル・メニュー情報を渡します
- items: メニューの設定(カテゴリや項目)を渡します
- startContent: 各メニュー項目の先頭に表示する要素を指定できます(例: アイコン)

## Usage
```svelte
<Sidebar headerMenu={headerMenu} items={menuItems}>
  {#snippet startContent()}
    <Icon />
  {/snippet}
</Sidebar>
```
-->

<script module lang="ts">
  import type { Snippet } from 'svelte';
  import type { ClassValue } from 'svelte/elements';

  /** メニュー内でセパレーターを挿入する位置のインデックス */
  const SEPARATOR_POSITION_INDEX = 2;

  export interface HeaderMenuProps {
    /** headerMenuに表示させる画像 */
    image: string;
    /** headerMenuに表示させるタイトル */
    title: string;
    /** headerMenuに表示させるサブタイトル */
    subTitle: string;
    /** DropdownMenuで表示する値 */
    menus: HeaderMenusProps[];
  }

  export interface HeaderMenusProps {
    /** メニューセクションのラベル */
    item?: string;
    /** ショートカットテキスト */
    shortCutText?: string;
  }

  export interface MenuItems {
    id: string;
    /** カテゴリー */
    category: string;
    /** サブメニューの設定 */
    entries?: MenuEntry[];
  }

  export interface MenuEntry {
    id: string;
    /** サブメニューの設定 */
    options?: MenuOptionProps[];
    text?: string;
  }

  export interface MenuOptionProps {
    id: string;
    /** メニュー内オプションのラベル */
    label: string;
    /** 押下時に発火させるコールバック関数 */
    onClick?: (items: MenuOptionProps) => void;
  }

  export interface SidebarProps {
    /** クラス */
    class?: ClassValue;
    /** ヘッダーの情報 */
    headerMenu: HeaderMenuProps;
    /** サイドバーの情報 */
    items: MenuItems[];
    /** itemの先頭にiconを表示させる */
    startContent?: Snippet;
  }
</script>

<script lang="ts">
  import Input from '$lib/components/ui/atoms/Input.svelte';
  import Separator from '$lib/components/ui/atoms/Separator.svelte';
  import Popover from '$lib/components/ui/modules/Popover.svelte';
  import { ChevronRight, ChevronsUpDown, Circle, UserRound } from '@lucide/svelte';
  import { slide } from 'svelte/transition';

  let { class: className, headerMenu, items, startContent }: SidebarProps = $props();

  let isMenuOpenMap = $state<Record<string, boolean>>({});

  let current = $state('');

  /**
   * メニュー押下時の処理
   */
  function onClick(item: MenuEntry) {
    current = item.id;
  }

  /**
   * サブメニュー押下時の処理
   */
  function onClickOption(callback: ((option: MenuOptionProps) => void) | undefined, option: MenuOptionProps) {
    current = option.id;
    callback?.(option);
  }

  /**
   * 開閉フラグの切り替え処理
   */
  function toggleOpen(id: string) {
    isMenuOpenMap = {
      ...isMenuOpenMap,
      [id]: !isMenuOpenMap[id],
    };
  }
</script>

<div class={[className, 'flex flex-col h-full min-h-screen pb-2 bg-base-surface-subarea-default']}>
  <!-- Header -->
  <div class="p-2">
    <Popover offset={{ x: 8 }} placement="right" hideArrow>
      <div class="w-64 px-2 py-1">
        <div class="flex items-center gap-2 py-2">
          {#if headerMenu.image}
            <img class="size-8 rounded-md" src={headerMenu.image} alt={`${headerMenu.title}のアイコン`} />
          {:else}
            <UserRound class="rounded-md" size="2rem" />
          {/if}
          <div class="text-left">
            <div class="font-bold text-base-foreground-default text-sm">{headerMenu.title}</div>
            <div class="text-base-foreground-subarea-default text-xs">{headerMenu.subTitle}</div>
          </div>
        </div>

        <Separator class="my-1" />

        <div class="flex flex-col">
          {#each headerMenu.menus as menu, i}
            <div class="flex items-center justify-between w-full px-2 py-1.5 rounded-md text-left text-base-foreground-subarea-default text-sm cursor-pointer hover:bg-base-container-subarea-accent/90">
              <div class="flex items-center gap-2 text-base-foreground-default text-sm/[150%]">
                <Circle size="1rem" />
                {menu.item}
              </div>
              {#if menu.shortCutText}
                <span class="text-base-foreground-subarea-default text-xs">{menu.shortCutText}</span>
              {/if}
            </div>

            {#if i === SEPARATOR_POSITION_INDEX}
              <Separator class="my-1" />
            {/if}
          {/each}
        </div>
      </div>
      {#snippet triggerContent(toggle)}
        <button class="flex items-center justify-between gap-2 p-2 rounded-md cursor-pointer hover:bg-base-container-subarea-accent/90" onclick={toggle}>
          <img class="size-8 rounded-md" src={headerMenu.image} alt={`${headerMenu.title}のアイコン`} />
          <div class="w-40 text-left">
            <div class="font-semibold text-base-foreground-default text-sm/[100%] mb-1">{headerMenu.title}</div>
            <div class="font-normal text-base-foreground-subarea-default text-xs/[100%]">{headerMenu.subTitle}</div>
          </div>
          <ChevronsUpDown class="text-base-foreground-subarea-default" size="1rem" />
        </button>
      {/snippet}
    </Popover>

    <div class="py-2">
      <Input inputClass="!min-h-auto !h-7.25" placeholder="検索" />
    </div>
  </div>
  <!-- Menu -->
  <div class="flex flex-col overflow-y-scroll">
    {#each items as item, index}
      {#if index !== 0}
        <Separator class="my-2" />
      {/if}
      <div class="px-2">
        {#if item.category}
          <div class="h-8 p-2 text-base-foreground-subarea-default text-xs/[100%]">
            {item.category}
          </div>
        {/if}

        {#each item.entries || [] as entry}
          <button class={['flex items-center justify-between w-full p-2 rounded-md text-left text-base-foreground-subarea-default text-sm/[100%] cursor-pointer hover:bg-base-container-subarea-accent/90', current === entry.id && 'bg-primary/10 rounded-md hover:bg-primary/20']} onclick={!entry.options ? () => onClick(entry) : () => toggleOpen(entry.id)}>
            {#if startContent}
              <div class="contents pointer-events-auto">{@render startContent()}</div>
            {/if}

            {entry.text}

            {#if entry.options}
              <ChevronRight class={['text-base-foreground-subarea-default transition duration-250 my-auto', isMenuOpenMap[entry.id] && 'rotate-90']} size="1rem" />
            {/if}
          </button>

          {#if entry.options && isMenuOpenMap[entry.id]}
            <div class="flex w-full py-0.5 pl-4 overflow-hidden translate-3d" transition:slide={{ duration: 250 }}>
              <div class="w-0.25 min-h-full bg-base-stroke-default mr-2"></div>
              <div class="flex flex-col w-full">
                {#each entry.options as option}
                  <button class={['flex items-center justify-between w-full h-8 px-2 py-1.5 rounded-md text-left text-base-foreground-subarea-default text-sm/[100%] transition-colors cursor-pointer hover:bg-base-container-subarea-accent/90', current === option.id && 'bg-primary/10 rounded-md hover:bg-primary/20']} onclick={() => onClickOption(option.onClick, option)}>
                    {option.label}
                  </button>
                {/each}
              </div>
            </div>
          {/if}
        {/each}
      </div>
    {/each}
  </div>
</div>

      

依存コンポーネント

Sidebarを使うときは、以下のコンポーネントもダウンロードが必要です。

使い方

sidebar/standard is coming soon.


Sidebar Dropdown

SidebarDropdownは、Webサイトやアプリケーションのサイドバー内に表示されるメニューコンポーネントです。
ヘッダー・フッター・ドロップダウン形式のメニューを持ち、ナビゲーションやアカウント切り替えなどの主要な要素を表示します。

sidebar/dropdown is coming soon.

プロパティ

SidebarDropdownは、以下のプロパティをサポートしています。

名前 デフォルト値 説明
headerMenu HeaderMenuProps ヘッダー部分に表示するメニュー情報(画像・タイトル・サブタイトルなど)です。
footerMenu FooterMenuProps フッター部分に表示するメニュー情報(画像・タイトル・サブタイトルなど)です。
menus SidebarMenu[] サイドバー内に表示するドロップダウン形式のメニュー群です。
startContent Snippet メニュー項目の左側に表示するカスタム要素(アイコンなど)を指定できます。

インストールの手順

以下のコンポーネントのコードを、使いたいプロジェクトにコピー&ペーストします。
パスは実際のプロジェクトの構成にあわせて更新します。

modules/SidebarDropdown.svelte
        <!--
@component
## 概要
- サイドバーの中にヘッダーメニュー・フッターメニュー・ドロップダウンメニューを持つ UI コンポーネントです。
- 各セクションに画像、タイトル、サブタイトルを設定でき、ドロップダウンメニューはショートカットキーやセパレーターなど柔軟に構成可能です。

## 機能
- ヘッダー・フッターにメニューを表示し、クリックでドロップダウンを展開
- サブメニュー(階層構造)に対応
- 現在選択中のメニューの表示・切り替え
- ショートカットキー表示、項目ごとのセパレーター表示
- メニュー表示位置の自動調整(ウィンドウサイズに追従)

## Props
- class: 追加のクラス名を付与できます
- headerMenu: ヘッダー部分に表示するメニュー情報
- footerMenu: フッター部分に表示するメニュー情報
- menus: サイドバーに表示されるドロップダウン形式のメニュー群
- startContent: メニュー項目左側に任意の要素を描画できます

## Usage
```svelte
<SidebarDropdown menus={menus} {headerMenu} {footerMenu}>
  {#snippet startContent()}
    <Icon />
  {/snippet}
</SidebarDropdown>
```
-->

<script module lang="ts">
  import type { Snippet } from 'svelte';
  import type { ClassValue } from 'svelte/elements';

  /** メニュー内でセパレーターを挿入する位置のインデックス */
  export const SEPARATOR_POSITION_INDEX = 2;

  export interface DropdownItem {
    label: string;
    shortCutText?: string;
    subMenus?: DropdownSection[];
  }

  export interface DropdownSection {
    label?: string;
    items: DropdownItem[];
  }

  export interface HeaderMenuProps {
    /** headerMenuに表示させる画像 */
    image: string;
    /** headerMenuに表示させるタイトル */
    title: string;
    /** headerMenuに表示させるサブタイトル */
    subTitle: string;
    /** DropdownMenuで表示する値 */
    menus: HeaderMenusProps[];
  }

  export interface HeaderMenusProps {
    /** メニューセクションのラベル */
    item?: string;
    /** ショートカットテキスト */
    shortCutText?: string;
  }

  export interface FooterMenuProps {
    /** footerMenuに表示させる画像 */
    image: string;
    /** footerMenuに表示させるタイトル */
    title: string;
    /** footerMenuに表示させるサブタイトル */
    subTitle: string;
    /** DropdownMenuで表示する値 */
    menus: FooterMenusProps[];
  }

  export interface FooterMenusProps {
    /** メニューセクションのラベル */
    item?: string;
    /** ショートカットテキスト */
    shortCutText?: string;
  }

  export interface SidebarMenu {
    label: string;
    items: DropdownSection[];
  }

  export interface SidebarDropdownProps {
    /** クラス */
    class?: ClassValue;
    /** ヘッダーの情報 */
    headerMenu: HeaderMenuProps;
    /** フッターの情報 */
    footerMenu: FooterMenuProps;
    /** メニューの情報 */
    menus: SidebarMenu[];
    /** itemの先頭にiconを表示させる */
    startContent?: Snippet;
  }
</script>

<script lang="ts">
  import Separator from '$lib/components/ui/atoms/Separator.svelte';
  import Popover from '$lib/components/ui/modules/Popover.svelte';
  import { ChevronsUpDown, Circle, Ellipsis, UserRound } from '@lucide/svelte';

  let { class: className, headerMenu, footerMenu, menus, startContent }: SidebarDropdownProps = $props();
</script>

<div class={[className, 'flex flex-col justify-between h-full min-h-screen bg-base-surface-subarea-default']}>
  <div>
    <!-- Header -->
    <div class="p-2">
      <Popover offset={{ x: 8 }} placement="right" hideArrow>
        <div class="w-64 px-2 py-1">
          <div class="flex items-center gap-2 py-2">
            {#if headerMenu.image}
              <img class="size-8 rounded-md" src={headerMenu.image} alt={`${headerMenu.title}のアイコン`} />
            {:else}
              <UserRound class="rounded-md" size="2rem" />
            {/if}
            <div class="text-left">
              <div class="font-bold text-base-foreground-default text-sm">{headerMenu.title}</div>
              <div class="text-base-foreground-subarea-default text-xs">{headerMenu.subTitle}</div>
            </div>
          </div>

          <Separator class="my-1" />

          <div class="flex flex-col">
            {#each headerMenu.menus as menu, i}
              <div class="flex items-center justify-between w-full px-2 py-1.5 rounded-md text-left text-base-foreground-subarea-default text-sm cursor-pointer hover:bg-base-container-subarea-accent/90">
                <div class="flex items-center gap-2 text-base-foreground-default text-sm/[150%]">
                  <Circle size="1rem" />
                  {menu.item}
                </div>
                {#if menu.shortCutText}
                  <span class="text-base-foreground-subarea-default text-xs">{menu.shortCutText}</span>
                {/if}
              </div>

              {#if i === SEPARATOR_POSITION_INDEX}
                <Separator class="my-1" />
              {/if}
            {/each}
          </div>
        </div>
        {#snippet triggerContent(toggle)}
          <button class="flex items-center justify-between gap-2 p-2 rounded-md cursor-pointer hover:bg-base-container-subarea-accent/90" onclick={toggle}>
            <img class="size-8 rounded-md" src={headerMenu.image} alt={`${headerMenu.title}のアイコン`} />
            <div class="w-40 text-left">
              <div class="font-semibold text-base-foreground-default text-sm/[100%] mb-1">{headerMenu.title}</div>
              <div class="font-normal text-base-foreground-subarea-default text-xs/[100%]">{headerMenu.subTitle}</div>
            </div>
            <ChevronsUpDown class="text-base-foreground-subarea-default" size="1rem" />
          </button>
        {/snippet}
      </Popover>
    </div>
    <!-- Dropdown -->
    <div class="p-2">
      <div class="flex flex-col overflow-y-scroll">
        {#each menus as menu, index}
          <Popover offset={{ x: 8 }} placement="right" hideArrow>
            <div class="w-60 px-2 py-1">
              <div class="flex flex-col">
                {#each menu.items as group, groupIndex (group)}
                  {#if group.label}
                    <div class="px-2 py-1.5 font-semibold">
                      {group.label}
                    </div>
                  {/if}

                  {#each group.items as item}
                    <div class="flex items-center justify-between w-full px-2 py-1.5 rounded-md text-left text-base-foreground-subarea-default text-sm cursor-pointer hover:bg-base-container-subarea-accent/90">
                      <div class="text-base-foreground-default text-sm/[150%]">
                        {item.label}
                      </div>
                      {#if item.shortCutText}
                        <span class="text-base-foreground-subarea-default text-xs">{item.shortCutText}</span>
                      {/if}
                    </div>
                  {/each}

                  {#if groupIndex !== menu.items.length - 1}
                    <Separator class="my-1" />
                  {/if}
                {/each}
              </div>
            </div>

            {#snippet triggerContent(toggle)}
              <button class="flex items-center justify-between w-full p-2 rounded-md text-left text-base-foreground-subarea-default text-sm/[100%] transition-colors cursor-pointer hover:bg-base-container-subarea-accent/90" onclick={toggle}>
                {#if startContent}
                  <div class="contents pointer-events-auto">{@render startContent()}</div>
                {/if}
                {menu.label}
                <Ellipsis class="text-base-foreground-subarea-default" size="1rem" />
              </button>
            {/snippet}
          </Popover>
        {/each}
      </div>
    </div>
  </div>
  <!-- Footer -->
  <div class="p-2">
    <Popover align="end" offset={{ x: 8 }} placement="right" hideArrow>
      <div class="w-64 px-2 py-1">
        <div class="flex items-center gap-2 py-2">
          {#if footerMenu.image}
            <img class="size-8 rounded-md" src={footerMenu.image} alt={`${footerMenu.title}のアイコン`} />
          {:else}
            <UserRound class="rounded-md" size="2rem" />
          {/if}
          <div class="text-left">
            <div class="font-bold text-base-foreground-default text-sm">{footerMenu.title}</div>
            <div class="text-base-foreground-subarea-default text-xs">{footerMenu.subTitle}</div>
          </div>
        </div>

        <Separator class="my-1" />

        <div class="flex flex-col">
          {#each footerMenu.menus as menu, i}
            <div class="flex items-center justify-between w-full px-2 py-1.5 rounded-md text-left text-base-foreground-subarea-default text-sm cursor-pointer hover:bg-base-container-subarea-accent/90">
              <div class="flex items-center gap-2 text-base-foreground-default text-sm/[150%]">
                <Circle size="1rem" />
                {menu.item}
              </div>
              {#if menu.shortCutText}
                <span class="text-base-foreground-subarea-default text-xs">{menu.shortCutText}</span>
              {/if}
            </div>

            {#if i === SEPARATOR_POSITION_INDEX}
              <Separator class="my-1" />
            {/if}
          {/each}
        </div>
      </div>
      {#snippet triggerContent(toggle)}
        <button class="flex items-center justify-between gap-2 p-2 rounded-md cursor-pointer hover:bg-base-container-subarea-accent/90" onclick={toggle}>
          <img class="size-8 rounded-md" src={footerMenu.image} alt={`${footerMenu.title}のアイコン`} />
          <div class="w-40 text-left">
            <div class="font-semibold text-base-foreground-default text-sm/[100%] mb-1">{footerMenu.title}</div>
            <div class="font-normal text-base-foreground-subarea-default text-xs/[100%]">{footerMenu.subTitle}</div>
          </div>
          <ChevronsUpDown class="text-base-foreground-subarea-default" size="1rem" />
        </button>
      {/snippet}
    </Popover>
  </div>
</div>

      

依存コンポーネント

SidebarDropdownを使うときは、以下のコンポーネントもダウンロードが必要です。

使い方

sidebar/dropdown is coming soon.