RoomCraft — v1.0.0 — 2026-03-19
作成日: 2026-03-19 アプローチ: モバイルファースト(min-width 方式) 対象: 一般ユーザー向けサービス + メーカー管理画面
| 名前 | 幅 | 対象デバイス | Tailwind プレフィックス |
|---|---|---|---|
| base | 375px+ | スマートフォン (標準) | (なし) |
| sm | 640px+ | 大型スマートフォン | sm: |
| md | 768px+ | タブレット | md: |
| lg | 1024px+ | デスクトップ | lg: |
| xl | 1280px+ | 大型デスクトップ | xl: |
| 2xl | 1536px+ | 超大型ディスプレイ | 2xl: |
import type { Config } from 'tailwindcss'
const config: Config = {
theme: {
extend: {
screens: {
// Tailwind デフォルトと同値(確認用)
'sm': '640px',
'md': '768px',
'lg': '1024px',
'xl': '1280px',
'2xl': '1536px',
},
},
},
}
| 要素 | 対応方法 |
|---|---|
| BottomSheet | padding-bottom: env(safe-area-inset-bottom) |
| 固定フッター | padding-bottom: env(safe-area-inset-bottom) |
| 固定ヘッダー | padding-top: env(safe-area-inset-top) |
| サイドメニュー(左) | padding-left: env(safe-area-inset-left) |
// tailwind.config.ts
plugins: [
plugin(function ({ addUtilities }) {
addUtilities({
'.safe-top': { paddingTop: 'env(safe-area-inset-top)' },
'.safe-bottom': { paddingBottom: 'env(safe-area-inset-bottom)' },
'.safe-left': { paddingLeft: 'env(safe-area-inset-left)' },
'.safe-right': { paddingRight: 'env(safe-area-inset-right)' },
})
}),
],
// 使用例
<footer className="fixed bottom-0 left-0 right-0 safe-bottom bg-white">
...
</footer>
最小サイズ: 44px × 44px(Apple HIG / WCAG 準拠)
// ボタン
<Button className="min-h-[44px] min-w-[44px]">...</Button>
// アイコンボタン
<button className="flex items-center justify-center w-11 h-11 rounded-full">
<Icon size={20} />
</button>
// リストアイテム
<li className="min-h-[44px] flex items-center px-4">...</li>
┌────────────────────────────┐
│ ヘッダー (fixed, safe-top) │ 高さ: 56px
├────────────────────────────┤
│ │
│ │
│ 3Dビューポート │ h-[calc(100dvh-56px-200px)]
│ (全画面) │
│ │
│ │
├────────────────────────────┤
│ BottomSheet (家具パネル) │ 高さ: 200px (折りたたみ時)
│ ドラッグで展開可能 │ 高さ: 60dvh (展開時)
└────────────────────────────┘ ← safe-bottom
// モバイル 3Dビューポート
<div className="
h-[calc(100dvh-56px-200px)]
md:hidden
relative
">
<RoomViewport />
<ViewportControlsFab className="absolute bottom-4 right-4" />
</div>
<BottomSheet>
<FurniturePanel />
</BottomSheet>
┌─────────────────┬──────────────────────┐
│ ヘッダー (固定) │ │ 高さ: 64px
├─────────────────┴──────────────────────┤
│ │
│ 3Dビューポート (70%) │ 右サイドパネル (30%)
│ ┌─────────────┤
│ │ 家具パネル │
│ │ (スクロール) │
│ └─────────────┤
└────────────────────────────────────────┘
// タブレット
<div className="
hidden md:flex lg:hidden
h-[calc(100dvh-64px)]
">
<div className="w-[70%] relative">
<RoomViewport />
</div>
<div className="w-[30%] overflow-y-auto border-l border-[#D4D0C8]">
<FurniturePanel />
</div>
</div>
┌──────────────────────────────────────────────────────┐
│ ヘッダー (固定) │ 高さ: 72px
├────────┬─────────────────────────────────┬────────────┤
│ │ │ │
│ 左 │ 3Dビューポート (中央) │ 右 │
│ ツール │ │ 家具 │
│ バー │ │ パネル │
│ (56px) │ │ (320px) │
│ │ │ │
└────────┴─────────────────────────────────┴────────────┘
// デスクトップ
<div className="
hidden lg:grid
grid-cols-[56px_1fr_320px]
h-[calc(100dvh-72px)]
">
<LeftToolbar />
<div className="relative">
<RoomViewport />
</div>
<div className="overflow-y-auto border-l border-[#D4D0C8]">
<FurniturePanel />
<RoomSettings />
</div>
</div>
import { useThree } from '@react-three/fiber'
import { useResizeObserver } from '@/hooks/useResizeObserver'
export function RoomViewportCanvas() {
const containerRef = useRef<HTMLDivElement>(null)
// コンテナサイズに追従(100% × 100%)
return (
<div ref={containerRef} className="w-full h-full">
<Canvas
style={{ width: '100%', height: '100%' }}
camera={{ position: [0, 5, 10], fov: 60 }}
// デバイスピクセル比に対応(Retina)
dpr={[1, 2]}
gl={{
antialias: true,
powerPreference: 'high-performance',
// モバイルではパフォーマンス優先
}}
>
...
</Canvas>
</div>
)
}
// モバイルではコントロールを最小化
<div className="
absolute
bottom-4 right-4 // モバイル: 右下
md:top-4 md:right-4 // タブレット: 右上
lg:bottom-6 lg:right-6 // デスクトップ: 右下(少し余白)
flex flex-col gap-2
bg-[rgba(26,29,26,0.7)] backdrop-blur-sm
rounded-xl p-2
">
<ViewportButton icon={<ZoomIn />} label="拡大" />
<ViewportButton icon={<ZoomOut />} label="縮小" />
<ViewportButton icon={<RotateCcw />} label="リセット" />
{/* グリッドはデスクトップのみ表示 */}
<div className="hidden lg:block">
<ViewportButton icon={<Grid />} label="グリッド" toggle />
</div>
</div>
// ページコンテナ
<div className="
w-full max-w-screen-xl
mx-auto
px-4 // base: 16px
sm:px-6 // sm: 24px
lg:px-8 // lg: 32px
">
// 商品一覧グリッド
<div className="
grid
grid-cols-2 // base: 2列
sm:grid-cols-2 // sm: 2列
md:grid-cols-3 // md: 3列
lg:grid-cols-4 // lg: 4列
xl:grid-cols-5 // xl: 5列
gap-3 // base: 12px
md:gap-4 // md: 16px
lg:gap-6 // lg: 24px
">
{products.map((p) => <FurnitureCard key={p.id} product={p} />)}
</div>
| ブレークポイント | 高さ |
|---|---|
| base (mobile) | 56px |
| md (tablet) | 64px |
| lg (desktop) | 72px |
<header className="
fixed top-0 left-0 right-0 z-50 safe-top
h-14 // 56px
md:h-16 // 64px
lg:h-18 // 72px (= h-[72px])
bg-white border-b border-[#D4D0C8]
">
// Playfair Display H1
<h1 className="
text-2xl // base: 24px
md:text-3xl // md: 30px
lg:text-4xl // lg: 36px
xl:text-[2.5rem] // xl: 40px (design-system.yml 定義値)
font-playfair font-bold
">
// 商品名 H3 (Montserrat)
<h3 className="
text-base // base: 16px
md:text-lg // md: 18px
lg:text-xl // lg: 20px
font-montserrat font-semibold
">
| デバイス | 機能 |
|---|---|
| モバイル (base ~ md) | 閲覧のみ: 売上サマリー・通知確認 |
| タブレット (md ~ lg) | 主要操作: 商品管理・注文確認 |
| デスクトップ (lg+) | フル機能: 統計・設定・一括操作 |
// 管理画面 モバイル制限バナー
export function MobileRestrictionBanner() {
return (
<div className="lg:hidden sticky top-0 z-40 bg-[#D4A843] text-white px-4 py-2 text-sm font-noto-sans-jp">
<p>デスクトップでフル機能をご利用いただけます</p>
</div>
)
}
// デスクトップ: 固定サイドバー / モバイル: BottomTab または DrawerMenu
<nav className="
// モバイル: 非表示
hidden
// デスクトップ: 固定サイドバー
lg:flex lg:flex-col lg:fixed lg:left-0 lg:top-0 lg:bottom-0
lg:w-64
lg:bg-[#FAFAF7] lg:border-r lg:border-[#D4D0C8]
">
// モバイルではカード表示にフォールバック
<div className="hidden md:block">
<DataTable data={products} columns={desktopColumns} />
</div>
<div className="md:hidden">
<ProductCardList data={products} />
</div>
import Image from 'next/image'
// 商品画像: レスポンシブsizes設定
<Image
src={product.image}
alt={product.name}
fill
sizes="
(max-width: 640px) 50vw,
(max-width: 1024px) 33vw,
20vw
"
className="object-cover"
/>
// デバイス性能に応じてLOD (Level of Detail) を変更
const isMobile = useMediaQuery('(max-width: 768px)')
<FurnitureModel
src={isMobile ? model.lowPolyUrl : model.highPolyUrl}
// モバイル: 低ポリゴン、デスクトップ: 高ポリゴン
/>
// 100vh ではなく 100dvh を使用(iOS Safari のダイナミックビューポート対応)
<div className="h-dvh">
<RoomViewport />
</div>
| 項目 | 実装 |
|---|---|
| フォーカスインジケーター | focus-visible:ring-2 focus-visible:ring-[#2C3E2D] 全サイズ共通 |
| スキップリンク | モバイル・デスクトップ共通で <a href="#main-content"> |
| 3Dビューポート | モバイルでもキーボード操作可 (矢印キー: 回転, +/-: ズーム) |
| タッチターゲット | 全ブレークポイントで 44px 以上維持 |
Phase 4 実装前確認:
tailwind.config.ts に全ブレークポイント設定済みh-dvh を h-screen の代わりに使用min-h-[44px] を持つsafe-bottom 適用済みsizes 属性設定済みh-dvh / min-h-dvh を使用Generated for RoomCraft - Phase 2: Design CCAGI SDK v3.14.4