ui-patterns について
Frappe アプリの画面を設計する開発者向けのスキルです。公式 Frappe アプリのパターンをもとに、レイアウト、状態、コンポーネント、レスポンシブ対応を扱います。
主な機能
- 01アプリシェルとナビゲーションのパターン
- 02リスト、カンバン、詳細ビュー
- 03フォームとアクティビティフィード
- 04空状態、読み込み、ステータスの規約
- 05レスポンシブデザインの指針
活用シーン
- 01新しい Frappe アプリの画面設計
- 02Frappe UI による CRUD 画面の構築
- 03CRM や Helpdesk の操作パターンの適用
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ lubusin/ frappe-skills --skill ui-patternsインストール後、Claude Code で/ui-patternsを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/app-shell-patterns.mdcomponent-patterns.mdmobile-patterns.md詳細
Frappe の UI パターン
公式 Frappe アプリから抽出した UI/UX パターンとデザインガイドラインです。
使用する場面
- 新しい Frappe アプリの UI を設計するとき
- Frappe UI で CRUD インターフェースを構築するとき
- リストビュー、詳細パネル、フォームを実装するとき
- CRM、Helpdesk、HRMS と一貫した UX を実現するとき
- コンポーネントのパターンやレイアウトを選ぶとき
必要な入力
- アプリの種類(CRM に似たもの、Helpdesk に似たもの、データ管理など)
- 主要なエンティティとその関係
- 主なユーザーワークフロー
参考アプリ
パターンを学ぶため、次の公式アプリを確認してください。
| App | Repo | Key Patterns |
|---|---|---|
| Frappe CRM | github.com/frappe/crm | リード/商談のパイプライン、カンバン、アクティビティフィード |
| Frappe Helpdesk | github.com/frappe/helpdesk | チケットキュー、SLA インジケーター、エージェントビュー |
| Frappe HRMS | github.com/frappe/hrms | 従業員のセルフサービス、承認、ダッシュボード |
| Frappe Insights | github.com/frappe/insights | クエリビルダー、可視化、ダッシュボード |
| Frappe Builder | github.com/frappe/builder | ドラッグ&ドロップのインターフェース、プロパティパネル |
手順
0) アプリシェルの構造
すべての Frappe アプリで、一貫したシェル構造を使用します。
┌─────────────────────────────────────────────────────────────┐
│ Header (App title, search, user menu) │
├──────────────┬──────────────────────────────────────────────┤
│ │ │
│ Sidebar │ Main Content │
│ │ │
│ - Nav items │ ┌─────────────────┬──────────────────────┐ │
│ - Filters │ │ List View │ Detail Panel │ │
│ - Actions │ │ │ │ │
│ │ │ │ │ │
│ │ └─────────────────┴──────────────────────┘ │
│ │ │
└──────────────┴──────────────────────────────────────────────┘
実装:
<template>
<div class="flex h-screen">
<Sidebar />
<div class="flex-1 flex">
<ListView
:class="selectedDoc ? 'w-1/2' : 'w-full'"
@select="selectDoc"
/>
<DetailPanel
v-if="selectedDoc"
:doc="selectedDoc"
class="w-1/2 border-l"
/>
</div>
</div>
</template>
1) サイドバーのパターン
標準的な構造:
<template>
<aside class="w-56 border-r bg-gray-50 flex flex-col">
<div class="p-4 border-b">
<h1 class="font-semibold">My App</h1>
</div>
<nav class="flex-1 p-2">
<SidebarLink
v-for="item in navItems"
:key="item.name"
:label="item.label"
:icon="item.icon"
:to="item.route"
:count="item.count"
/>
</nav>
<div v-if="filters.length" class="p-2 border-t">
<p class="text-xs text-gray-500 px-2 mb-1">Filters</p>
<SidebarLink
v-for="filter in filters"
:key="filter.name"
:label="filter.label"
:count="filter.count"
@click="applyFilter(filter)"
/>
</div>
<div class="p-2 border-t">
<UserMenu />
</div>
</aside>
</template>
CRM のナビゲーション項目の例:
- リード(件数バッジ付き)
- 商談(件数バッジ付き)
- 連絡先
- 組織
- アクティビティ
- 設定
2) リストビューのパターン
フィルター付きの標準的なリスト:
<template>
<div class="flex-1 flex flex-col">
<div class="flex items-center justify-between p-4 border-b">
<div class="flex items-center gap-2">
<Input
type="search"
placeholder="Search..."
v-model="searchQuery"
:debounce="300"
/>
<FilterDropdown :filters="availableFilters" v-model="activeFilters" />
</div>
<div class="flex items-center gap-2">
<ViewToggle v-model="viewMode" :options="['list', 'kanban', 'grid']" />
<Button variant="solid" @click="createNew">
<template #prefix><FeatherIcon name="plus" /></template>
New
</Button>
</div>
</div>
<ListView v-if="viewMode === 'list'" :data="items" @row-click="select" />
<KanbanView v-else-if="viewMode === 'kanban'" :data="items" :columns="stages" />
<GridView v-else :data="items" @card-click="select" />
</div>
</template>
リスト行の構造:
<template>
<div class="flex items-center p-3 hover:bg-gray-50 cursor-pointer border-b">
<Checkbox v-if="selectable" v-model="selected" class="mr-3" />
<Avatar :label="row.name" :image="row.image" class="mr-3" />
<div class="flex-1 min-w-0">
<p class="font-medium truncate">{{ row.title }}</p>
<p class="text-sm text-gray-500 truncate">{{ row.subtitle }}</p>
</div>
<Badge :variant="statusVariant">{{ row.status }}</Badge>
<span class="text-sm text-gray-500 ml-4">{{ timeAgo(row.modified) }}</span>
<Dropdown :options="rowActions" class="ml-2">
<Button variant="ghost" icon="more-horizontal" />
</Dropdown>
</div>
</template>
3) カンバンビューのパターン
使用例: CRM(商談)、Helpdesk(ステータス別のチケット)
<template>
<div class="flex overflow-x-auto p-4 gap-4">
<div
v-for="column in columns"
:key="column.name"
class="flex-shrink-0 w-72 bg-gray-100 rounded-lg"
>
<div class="p-3 font-medium flex items-center justify-between">
<span>{{ column.label }}</span>
<Badge>{{ column.items.length }}</Badge>
</div>
<div class="p-2 space-y-2 min-h-[200px]">
<KanbanCard
v-for="item in column.items"
:key="item.name"
:data="item"
@click="select(item)"
draggable
@dragend="handleDrop"
/>
</div>
<Button variant="ghost" class="w-full" @click="addTo(column)">
+ Add {{ column.singular }}
</Button>
</div>
</div>
</template>
カンバンカードの構造:
<template>
<div class="bg-white rounded-lg p-3 shadow-sm border cursor-pointer hover:shadow">
<p class="font-medium mb-1">{{ data.title }}</p>
<p class="text-sm text-gray-500 mb-2">{{ data.subtitle }}</p>
<div class="flex items-center justify-between">
<Avatar :label="data.assigned_to" size="sm" />
<span class="text-xs text-gray-400">{{ data.due_date }}</span>
</div>
</div>
</template>
4) 詳細パネル/サイドパネル
分割ビューのパターン(CRM/Helpdesk スタイル):
<template>
<aside class="w-[480px] border-l bg-white flex flex-col">
<div class="flex items-center justify-between p-4 border-b">
<h2 class="font-semibold">{{ doc.name }}</h2>
<Button variant="ghost" icon="x" @click="$emit('close')" />
</div>
<Tabs v-model="activeTab">
<Tab name="details" label="Details" />
<Tab name="activity" label="Activity" />
<Tab name="notes" label="Notes" />
</Tabs>
<div class="flex-1 overflow-auto p-4">
<DetailsTab v-if="activeTab === 'details'" :doc="doc" />
<ActivityFeed v-else-if="activeTab === 'activity'" :doctype="doctype" :name="doc.name" />
<NotesTab v-else :doctype="doctype" :name="doc.name" />
</div>
<div class="p-4 border-t flex justify-end gap-2">
<Button @click="edit">Edit</Button>
<Button variant="solid" @click="primaryAction">{{ primaryActionLabel }}</Button>
</div>
</aside>
</template>
5) フォームのパターン
標準的なフォームレイアウト:
<template>
<div class="max-w-2xl mx-auto p-6">
<div class="mb-6">
<h1 class="text-xl font-semibold">{{ isNew ? 'New' : 'Edit' }} {{ doctype }}</h1>
</div>
<FormSection title="Basic Information">
<div class="grid grid-cols-2 gap-4">
<FormControl label="Name" v-model="doc.name" :required="true" />
<FormControl label="Status" type="select" v-model="doc.status" :options="statusOptions" />
</div>
<FormControl label="Description" type="textarea" v-model="doc.description" class="mt-4" />
</FormSection>
<FormSection title="Details" collapsible>
</FormSection>
<div class="flex justify-end gap-2 mt-6 pt-4 border-t">
<Button @click="cancel">Cancel</Button>
<Button variant="solid" @click="save" :loading="saving">Save</Button>
</div>
</div>
</template>
FormSection コンポーネント:
<template>
<div class="mb-6">
<div
class="flex items-center justify-between mb-3 cursor-pointer"
@click="collapsible && (collapsed = !collapsed)"
>
<h3 class="font-medium text-gray-700">{{ title }}</h3>
<FeatherIcon v-if="collapsible" :name="collapsed ? 'chevron-down' : 'chevron-up'" />
</div>
<div v-show="!collapsed">
<slot />
</div>
</div>
</template>
6) アクティビティフィードのパターン
変更を追跡するため、すべてのアプリで使用します。
<template>
<div class="space-y-4">
<div class="flex gap-3">
<Avatar :label="$user.name" />
<div class="flex-1">
<Textarea v-model="newComment" placeholder="Add a comment..." rows="2" />
<Button class="mt-2" @click="addComment" :disabled="!newComment">Comment</Button>
</div>
</div>
<div v-for="item in activities" :key="item.name" class="flex gap-3">
<Avatar :label="item.owner" size="sm" />
<div class="flex-1">
<div class="flex items-baseline gap-2">
<span class="font-medium text-sm">{{ item.owner }}</span>
<span class="text-xs text-gray-400">{{ timeAgo(item.creation) }}</span>
</div>
<CommentContent v-if="item.type === 'comment'" :content="item.content" />
<StatusChange v-else-if="item.type === 'status'" :from="item.from" :to="item.to" />
<FieldChange v-else-if="item.type === 'change'" :field="item.field" :value="item.value" />
</div>
</div>
</div>
</template>
7) 空の状態
常に役立つ空の状態を表示してください。
<template>
<div class="flex flex-col items-center justify-center h-64 text-center">
<FeatherIcon name="inbox" class="w-12 h-12 text-gray-300 mb-4" />
<h3 class="font-medium text-gray-700 mb-1">{{ title }}</h3>
<p class="text-sm text-gray-500 mb-4">{{ description }}</p>
<Button v-if="action" variant="solid" @click="action.handler">
{{ action.label }}
</Button>
</div>
</template>
<EmptyState
title="No leads yet"
description="Create your first lead to get started"
:action="{ label: 'Create Lead', handler: createLead }"
/>
8) 読み込み状態
体感速度を高めるため、スケルトンローダーを使用します。
<template>
<div v-if="loading" class="space-y-2 p-4">
<div v-for="i in 5" :key="i" class="flex items-center gap-3 p-3">
<Skeleton class="w-10 h-10 rounded-full" />
<div class="flex-1">
<Skeleton class="h-4 w-1/3 mb-2" />
<Skeleton class="h-3 w-1/2" />
</div>
</div>
</div>
<ListView v-else :data="data" />
</template>
9) 色とステータスの規則
| ステータスの種類 | 色 | 用途 |
|---|---|---|
| 成功/アクティブ | 緑 (bg-green-100 text-green-700) | 完了、アクティブ、解決済み |
| 警告/保留中 | 黄色 (bg-yellow-100 text-yellow-700) | 保留中、進行中、期限間近 |
| エラー/ブロック | 赤 (bg-red-100 text-red-700) | 失敗、ブロック、期限超過 |
| 情報/デフォルト | 青 (bg-blue-100 text-blue-700) | 新規、オープン、情報 |
| ニュートラル | グレー (bg-gray-100 text-gray-700) | 下書き、キャンセル済み、クローズ済み |
Badge コンポーネントの使用方法:
<Badge variant="success">Active</Badge>
<Badge variant="warning">Pending</Badge>
<Badge variant="error">Overdue</Badge>
<Badge variant="info">New</Badge>
<Badge variant="subtle">Draft</Badge>
10) レスポンシブ対応のパターン
モバイルファーストで考慮する点:
<template>
<Sidebar v-if="!isMobile" />
<Drawer v-else v-model="sidebarOpen">
<Sidebar />
</Drawer>
<div :class="isMobile ? 'flex-col' : 'flex'">
<ListView v-show="!isMobile || !selectedDoc" />
<DetailPanel v-if="selectedDoc" :fullScreen="isMobile" />
</div>
</template>
コンポーネントのリファレンス
次の Frappe UI コンポーネントを一貫して使用してください。
| コンポーネント | 用途 |
|---|---|
<Button> | すべてのアクション。バリアント:solid、subtle、ghost |
<Input> | テキスト入力、検索フィールド |
<FormControl> | ラベルとバリデーション付きのフォームフィールド |
<Select> | ドロップダウン、ステータスセレクター |
<Checkbox> | 真偽値の入力、一括選択 |
<Avatar> | ユーザー画像、エンティティアイコン |
<Badge> | ステータスインジケーター、件数 |
<Dropdown> | アクションメニュー、コンテキストメニュー |
<Dialog> | モーダルでの確認、フォーム |
<Tabs> | コンテンツの整理 |
<Tooltip> | ヒント、切り詰められたテキストの表示 |
検証
- アプリシェルが標準的なレイアウト(サイドバー+メイン+任意の詳細パネル)に沿っている
- リストビューに検索、フィルター、ビュー切り替え、新規作成ボタンがある
- 詳細パネルにタブ(詳細、アクティビティ、メモ)がある
- 空の状態に役立つアクションが用意されている
- 読み込み状態ではスピナーではなくスケルトンを使用している
- ステータスの色が規則に沿っている
- フォームがセクション分けされ、一貫性がある
- モバイルでの使いやすさが考慮されている
障害モード/デバッグ
- 余白が不統一:TailwindCSS のスペーシングスケール(p-2、p-4、gap-2、gap-4)を使用します
- コンポーネントが不適切:Frappe UI のドキュメントを確認し、適切なコンポーネントと props を使用します
- レスポンシブ対応が機能しない:モバイルのブレークポイントでテストし、
sm:、md:、lg:のプレフィックスを使用します - 状態が不足:読み込み中、空、エラーの状態が処理されていることを確認します
エスカレーション
- コンポーネントの実装 →
frontend-development - バックエンド API の統合 →
api-development - エンタープライズワークフロー →
enterprise-patterns
参考資料
- references/app-shell-patterns.md — シェルレイアウトの詳細
- references/component-patterns.md — コンポーネントの使用ガイド
- references/mobile-patterns.md — レスポンシブデザイン
ガードレール
- まず公式アプリを調査する:UI を設計する前に、CRM、Helpdesk、または関連する公式アプリを確認し、パターンを参考にします
- Frappe UI コンポーネントを使用する:Frappe UI に相当するものがある場合は、カスタムコンポーネントを作成しないでください
- スペーシングの規約に従う:パディング/マージンは一貫性を保ちます(4px 単位)
- すべての状態に対応する:すべてのビューに読み込み中、空、エラーの状態を用意します
- ナビゲーションの一貫性を保つ:サイドバーの構成は公式アプリに合わせます
- レスポンシブにテストする:モバイルで問題なく利用できることを確認します
よくある間違い
| 間違い | 問題が起きる理由 | 修正方法 |
|---|---|---|
| カスタムのアプリシェルデザイン | ユーザーにとってなじみのない UX になる | CRM/Helpdesk のシェル構造をコピーする |
| 空の状態がない | データがないときにユーザーが戸惑う | アクション付きの EmptyState コンポーネントを追加する |
| スケルトンではなくスピナーを使う | 読み込み時の表示に違和感がある | 読み込み中は Skeleton コンポーネントを使用する |
| ステータスの色が不統一 | ユーザーが混乱する | 色の規約の表に従う |
| パンくずリストなしで深くネストする | ユーザーが迷う | パンくずナビゲーションを追加する |
| モーダルの多用 | ワークフローが中断される | 詳細ビューにはサイドパネルを優先する |
| キーボードナビゲーションがない | アクセシビリティの問題が生じる | 主要な操作で Tab/Enter が機能することを確認する |
コメント
Markdown 対応