Admin Layout
Admin Layoutは、管理画面の各ページで必要となるタイトル、アクション、パンくずなどの基本構成を管理するコンポーネントです。
admin_layout/default is coming soon.
<script lang="ts">
import type { MenuItem } from '$lib/components/ui/atoms/Menu.svelte';
import Button from '$lib/components/ui/atoms/Button.svelte';
import Chip from '$lib/components/ui/atoms/Chip.svelte';
import Label from '$lib/components/ui/atoms/Label.svelte';
import Radio from '$lib/components/ui/atoms/Radio.svelte';
import Table from '$lib/components/ui/atoms/Table.svelte';
import TableData from '$lib/components/ui/atoms/TableData.svelte';
import TableHead from '$lib/components/ui/atoms/TableHead.svelte';
import Breadcrumb from '$lib/components/ui/modules/Breadcrumb.svelte';
import HeaderSnippet from '$lib/components/ui/modules/HeaderSnippet.svelte';
import Sidebar from '$lib/components/ui/modules/Sidebar.svelte';
import { ChevronLeft, Pen } from '@lucide/svelte';
const PROFILE_IMG_URL = '/images/sample.png';
// SP時のハンバーガーメニュー(Sidebarの代替)
const mobileMenus: MenuItem[] = [
{
id: 'mobile-menu1',
label: 'フォーム一覧',
},
{
id: 'mobile-menu2',
label: 'チーム管理',
},
{
id: 'mobile-menu3',
label: 'プランとお支払い',
},
];
const headerMenu = {
image: '/images/sample.png',
title: 'メニュー',
subTitle: 'サブタイトル',
menus: [
{ item: 'メニュー' },
{ item: 'メニュー' },
],
};
const menuItems = [
{
id: 'menu1',
category: '',
entries: [
{ id: '', text: 'フォーム一覧' },
{ id: 'menu1-2', text: 'チーム管理' },
{ id: 'menu1-3', text: 'プランとお支払い' },
],
},
];
const breadcrumbItems = [
{ label: 'フォーム一覧', link: '#' },
{ label: '採用エントリーフォーム', link: '#' },
];
const columnItems = [
{ label: 'No.', key: 'no', class: 'w-[55px]' },
{ label: 'ステータス', key: 'status', type: 'chip', class: 'w-[102px]' },
{ label: '名前', key: 'name', class: 'w-74' },
{ label: 'フリガナ', key: 'yomi', class: 'w-74' },
{ label: '希望職種', key: 'job', class: 'w-74' },
{ label: '回答日時', key: 'date', class: 'w-38' },
{ label: '', type: 'button', class: 'w-21' },
];
const entries = [
{ no: 20, status: '未対応', name: '佐藤 太郎', yomi: 'サトウ タロウ', job: 'エンジニア', date: '2025/09/08 10:30' },
{ no: 19, status: '未対応', name: '佐藤 太郎', yomi: 'サトウ タロウ', job: 'エンジニア', date: '2025/09/08 10:30' },
{ no: 18, status: '未対応', name: '佐藤 太郎', yomi: 'サトウ タロウ', job: 'エンジニア', date: '2025/09/08 10:30' },
{ no: 17, status: '未対応', name: '佐藤 太郎', yomi: 'サトウ タロウ', job: 'エンジニア', date: '2025/09/08 10:30' },
{ no: 16, status: '未対応', name: '佐藤 太郎', yomi: 'サトウ タロウ', job: 'エンジニア', date: '2025/09/08 10:30' },
{ no: 15, status: '対応済み', name: '佐藤 太郎', yomi: 'サトウ タロウ', job: 'エンジニア', date: '2025/09/08 10:30' },
{ no: 14, status: '未対応', name: '佐藤 太郎', yomi: 'サトウ タロウ', job: 'エンジニア', date: '2025/09/08 10:30' },
{ no: 13, status: '対応済み', name: '佐藤 太郎', yomi: 'サトウ タロウ', job: 'エンジニア', date: '2025/09/08 10:30' },
{ no: 12, status: '未対応', name: '佐藤 太郎', yomi: 'サトウ タロウ', job: 'エンジニア', date: '2025/09/08 10:30' },
{ no: 11, status: '対応済み', name: '佐藤 太郎', yomi: 'サトウ タロウ', job: 'エンジニア', date: '2025/09/08 10:30' },
{ no: 10, status: '対応済み', name: '佐藤 太郎', yomi: 'サトウ タロウ', job: 'エンジニア', date: '2025/09/08 10:30' },
{ no: 9, status: '対応済み', name: '佐藤 太郎', yomi: 'サトウ タロウ', job: 'エンジニア', date: '2025/09/08 10:30' },
{ no: 8, status: '対応済み', name: '佐藤 太郎', yomi: 'サトウ タロウ', job: 'エンジニア', date: '2025/09/08 10:30' },
{ no: 7, status: '対応済み', name: '佐藤 太郎', yomi: 'サトウ タロウ', job: 'エンジニア', date: '2025/09/08 10:30' },
{ no: 6, status: '対応済み', name: '佐藤 太郎', yomi: 'サトウ タロウ', job: 'エンジニア', date: '2025/09/08 10:30' },
{ no: 5, status: '対応済み', name: '佐藤 太郎', yomi: 'サトウ タロウ', job: 'エンジニア', date: '2025/09/08 10:30' },
{ no: 4, status: '対応済み', name: '佐藤 太郎', yomi: 'サトウ タロウ', job: 'エンジニア', date: '2025/09/08 10:30' },
{ no: 3, status: '対応済み', name: '佐藤 太郎', yomi: 'サトウ タロウ', job: 'エンジニア', date: '2025/09/08 10:30' },
{ no: 2, status: '対応済み', name: '佐藤 太郎', yomi: 'サトウ タロウ', job: 'エンジニア', date: '2025/09/08 10:30' },
{ no: 1, status: '対応済み', name: '佐藤 太郎', yomi: 'サトウ タロウ', job: 'エンジニア', date: '2025/09/08 10:30' },
];
const logoUrl = '#';
const statusTone: Record<string, 'filled' | 'outlined'> = {
'未対応': 'filled',
'対応済み': 'outlined',
};
const statusFilters = ['すべて', '未対応', '対応済み'];
let filterGroup = $state('すべて');
</script>
<div class="flex flex-col h-screen">
<HeaderSnippet profileImgUrl={PROFILE_IMG_URL} {mobileMenus}>
{#snippet startContent()}
<div class="absolute inset-x-0 w-fit mx-auto md:static md:mx-0">
<a class="flex items-center justify-center p-2 outline-offset-2 outline-ring focus-visible:rounded-md focus-visible:outline-2" href={logoUrl}>
<div class="w-28 h-[1.3125rem] bg-foreground transition mask-[url('/lp/images/logo.svg')] mask-center mask-contain mask-no-repeat">
<span class="sr-only">Rabee UI</span>
</div>
</a>
</div>
{/snippet}
</HeaderSnippet>
<div class="flex flex-1 min-h-0">
<!-- Sidebar: PCのみ表示 -->
<div class="shrink-0 w-64 overflow-hidden max-md:hidden">
<Sidebar class="w-full h-full" items={menuItems} {headerMenu} />
</div>
<main class="flex-1 px-4 py-6 overflow-y-auto md:p-12">
<div class="flex flex-col gap-2 mb-12">
<Breadcrumb items={breadcrumbItems} />
<div class="flex flex-col gap-2 lg:flex-row lg:items-center lg:justify-between">
<div class="flex items-center gap-3">
<Button variant="secondary" tone="ghost" isSquare size="small">
<ChevronLeft size="1rem" />
</Button>
<h1 class="font-semibold leading-tight text-2xl">採用エントリーフォーム</h1>
</div>
<Button variant="secondary" tone="solid" class="self-start lg:self-auto">
<Pen size="1rem" />
フォームを編集する
</Button>
</div>
</div>
<div class="flex flex-col mb-3 md:flex-row md:items-center md:justify-between">
<p class="font-bold text-xl/7 max-md:mb-4">回答一覧 <span class="font-normal text-sm/none text-subtle-foreground">({entries.length})</span></p>
<div class="flex items-center gap-4">
{#each statusFilters as filter}
<Label class="flex items-center gap-1.5 text-sm group cursor-pointer">
<Radio name="filter" value={filter} bind:group={filterGroup} /> {filter}
</Label>
{/each}
</div>
</div>
<div class="border border-border rounded-md overflow-x-scroll">
<Table class="min-w-2xl w-full">
<thead class="border-b border-border">
<tr class="transition-colors hover:bg-accent/40">
{#each columnItems as item}
<TableHead class={['text-nowrap', item.class]} size="small">
{item.label}
</TableHead>
{/each}
</tr>
</thead>
<tbody>
{#each entries as entry}
<tr class="transition-colors hover:bg-accent/40">
{#each columnItems as item}
<TableData size="small" class={['text-nowrap', item.class]}>
{#if item.type === 'chip' && item.key}
<Chip tone={statusTone[entry[item.key]] ?? 'outlined'} class="text-nowrap">
{entry[item.key]}
</Chip>
{:else if item.type === 'button'}
<Button size="small" variant="secondary" tone="solid" class="text-nowrap">
詳細
</Button>
{:else if item.key}
{entry[item.key]}
{/if}
</TableData>
{/each}
</tr>
{/each}
</tbody>
</Table>
</div>
</main>
</div>
</div>
インストールの手順
以下のコードを、使いたいプロジェクトにコピー&ペーストします。
パスは実際のプロジェクトの構成にあわせて更新します。
admin_layout/default is coming soon.
<script lang="ts">
import type { MenuItem } from '$lib/components/ui/atoms/Menu.svelte';
import Button from '$lib/components/ui/atoms/Button.svelte';
import Chip from '$lib/components/ui/atoms/Chip.svelte';
import Label from '$lib/components/ui/atoms/Label.svelte';
import Radio from '$lib/components/ui/atoms/Radio.svelte';
import Table from '$lib/components/ui/atoms/Table.svelte';
import TableData from '$lib/components/ui/atoms/TableData.svelte';
import TableHead from '$lib/components/ui/atoms/TableHead.svelte';
import Breadcrumb from '$lib/components/ui/modules/Breadcrumb.svelte';
import HeaderSnippet from '$lib/components/ui/modules/HeaderSnippet.svelte';
import Sidebar from '$lib/components/ui/modules/Sidebar.svelte';
import { ChevronLeft, Pen } from '@lucide/svelte';
const PROFILE_IMG_URL = '/images/sample.png';
// SP時のハンバーガーメニュー(Sidebarの代替)
const mobileMenus: MenuItem[] = [
{
id: 'mobile-menu1',
label: 'フォーム一覧',
},
{
id: 'mobile-menu2',
label: 'チーム管理',
},
{
id: 'mobile-menu3',
label: 'プランとお支払い',
},
];
const headerMenu = {
image: '/images/sample.png',
title: 'メニュー',
subTitle: 'サブタイトル',
menus: [
{ item: 'メニュー' },
{ item: 'メニュー' },
],
};
const menuItems = [
{
id: 'menu1',
category: '',
entries: [
{ id: '', text: 'フォーム一覧' },
{ id: 'menu1-2', text: 'チーム管理' },
{ id: 'menu1-3', text: 'プランとお支払い' },
],
},
];
const breadcrumbItems = [
{ label: 'フォーム一覧', link: '#' },
{ label: '採用エントリーフォーム', link: '#' },
];
const columnItems = [
{ label: 'No.', key: 'no', class: 'w-[55px]' },
{ label: 'ステータス', key: 'status', type: 'chip', class: 'w-[102px]' },
{ label: '名前', key: 'name', class: 'w-74' },
{ label: 'フリガナ', key: 'yomi', class: 'w-74' },
{ label: '希望職種', key: 'job', class: 'w-74' },
{ label: '回答日時', key: 'date', class: 'w-38' },
{ label: '', type: 'button', class: 'w-21' },
];
const entries = [
{ no: 20, status: '未対応', name: '佐藤 太郎', yomi: 'サトウ タロウ', job: 'エンジニア', date: '2025/09/08 10:30' },
{ no: 19, status: '未対応', name: '佐藤 太郎', yomi: 'サトウ タロウ', job: 'エンジニア', date: '2025/09/08 10:30' },
{ no: 18, status: '未対応', name: '佐藤 太郎', yomi: 'サトウ タロウ', job: 'エンジニア', date: '2025/09/08 10:30' },
{ no: 17, status: '未対応', name: '佐藤 太郎', yomi: 'サトウ タロウ', job: 'エンジニア', date: '2025/09/08 10:30' },
{ no: 16, status: '未対応', name: '佐藤 太郎', yomi: 'サトウ タロウ', job: 'エンジニア', date: '2025/09/08 10:30' },
{ no: 15, status: '対応済み', name: '佐藤 太郎', yomi: 'サトウ タロウ', job: 'エンジニア', date: '2025/09/08 10:30' },
{ no: 14, status: '未対応', name: '佐藤 太郎', yomi: 'サトウ タロウ', job: 'エンジニア', date: '2025/09/08 10:30' },
{ no: 13, status: '対応済み', name: '佐藤 太郎', yomi: 'サトウ タロウ', job: 'エンジニア', date: '2025/09/08 10:30' },
{ no: 12, status: '未対応', name: '佐藤 太郎', yomi: 'サトウ タロウ', job: 'エンジニア', date: '2025/09/08 10:30' },
{ no: 11, status: '対応済み', name: '佐藤 太郎', yomi: 'サトウ タロウ', job: 'エンジニア', date: '2025/09/08 10:30' },
{ no: 10, status: '対応済み', name: '佐藤 太郎', yomi: 'サトウ タロウ', job: 'エンジニア', date: '2025/09/08 10:30' },
{ no: 9, status: '対応済み', name: '佐藤 太郎', yomi: 'サトウ タロウ', job: 'エンジニア', date: '2025/09/08 10:30' },
{ no: 8, status: '対応済み', name: '佐藤 太郎', yomi: 'サトウ タロウ', job: 'エンジニア', date: '2025/09/08 10:30' },
{ no: 7, status: '対応済み', name: '佐藤 太郎', yomi: 'サトウ タロウ', job: 'エンジニア', date: '2025/09/08 10:30' },
{ no: 6, status: '対応済み', name: '佐藤 太郎', yomi: 'サトウ タロウ', job: 'エンジニア', date: '2025/09/08 10:30' },
{ no: 5, status: '対応済み', name: '佐藤 太郎', yomi: 'サトウ タロウ', job: 'エンジニア', date: '2025/09/08 10:30' },
{ no: 4, status: '対応済み', name: '佐藤 太郎', yomi: 'サトウ タロウ', job: 'エンジニア', date: '2025/09/08 10:30' },
{ no: 3, status: '対応済み', name: '佐藤 太郎', yomi: 'サトウ タロウ', job: 'エンジニア', date: '2025/09/08 10:30' },
{ no: 2, status: '対応済み', name: '佐藤 太郎', yomi: 'サトウ タロウ', job: 'エンジニア', date: '2025/09/08 10:30' },
{ no: 1, status: '対応済み', name: '佐藤 太郎', yomi: 'サトウ タロウ', job: 'エンジニア', date: '2025/09/08 10:30' },
];
const logoUrl = '#';
const statusTone: Record<string, 'filled' | 'outlined'> = {
'未対応': 'filled',
'対応済み': 'outlined',
};
const statusFilters = ['すべて', '未対応', '対応済み'];
let filterGroup = $state('すべて');
</script>
<div class="flex flex-col h-screen">
<HeaderSnippet profileImgUrl={PROFILE_IMG_URL} {mobileMenus}>
{#snippet startContent()}
<div class="absolute inset-x-0 w-fit mx-auto md:static md:mx-0">
<a class="flex items-center justify-center p-2 outline-offset-2 outline-ring focus-visible:rounded-md focus-visible:outline-2" href={logoUrl}>
<div class="w-28 h-[1.3125rem] bg-foreground transition mask-[url('/lp/images/logo.svg')] mask-center mask-contain mask-no-repeat">
<span class="sr-only">Rabee UI</span>
</div>
</a>
</div>
{/snippet}
</HeaderSnippet>
<div class="flex flex-1 min-h-0">
<!-- Sidebar: PCのみ表示 -->
<div class="shrink-0 w-64 overflow-hidden max-md:hidden">
<Sidebar class="w-full h-full" items={menuItems} {headerMenu} />
</div>
<main class="flex-1 px-4 py-6 overflow-y-auto md:p-12">
<div class="flex flex-col gap-2 mb-12">
<Breadcrumb items={breadcrumbItems} />
<div class="flex flex-col gap-2 lg:flex-row lg:items-center lg:justify-between">
<div class="flex items-center gap-3">
<Button variant="secondary" tone="ghost" isSquare size="small">
<ChevronLeft size="1rem" />
</Button>
<h1 class="font-semibold leading-tight text-2xl">採用エントリーフォーム</h1>
</div>
<Button variant="secondary" tone="solid" class="self-start lg:self-auto">
<Pen size="1rem" />
フォームを編集する
</Button>
</div>
</div>
<div class="flex flex-col mb-3 md:flex-row md:items-center md:justify-between">
<p class="font-bold text-xl/7 max-md:mb-4">回答一覧 <span class="font-normal text-sm/none text-subtle-foreground">({entries.length})</span></p>
<div class="flex items-center gap-4">
{#each statusFilters as filter}
<Label class="flex items-center gap-1.5 text-sm group cursor-pointer">
<Radio name="filter" value={filter} bind:group={filterGroup} /> {filter}
</Label>
{/each}
</div>
</div>
<div class="border border-border rounded-md overflow-x-scroll">
<Table class="min-w-2xl w-full">
<thead class="border-b border-border">
<tr class="transition-colors hover:bg-accent/40">
{#each columnItems as item}
<TableHead class={['text-nowrap', item.class]} size="small">
{item.label}
</TableHead>
{/each}
</tr>
</thead>
<tbody>
{#each entries as entry}
<tr class="transition-colors hover:bg-accent/40">
{#each columnItems as item}
<TableData size="small" class={['text-nowrap', item.class]}>
{#if item.type === 'chip' && item.key}
<Chip tone={statusTone[entry[item.key]] ?? 'outlined'} class="text-nowrap">
{entry[item.key]}
</Chip>
{:else if item.type === 'button'}
<Button size="small" variant="secondary" tone="solid" class="text-nowrap">
詳細
</Button>
{:else if item.key}
{entry[item.key]}
{/if}
</TableData>
{/each}
</tr>
{/each}
</tbody>
</Table>
</div>
</main>
</div>
</div>
依存コンポーネント
上記のサンプルを使うときは、以下のコンポーネントもダウンロードが必要です。