コーディング規約
h1-core edited this page 2026-08-11 14:04:37 +09:00
This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

デザインシステムルール

このドキュメントは h-1-core のUIデザインに関するルールを定義します。 新規画面・プラグイン開発時は必ず参照してください。


テーマ配色ルール

基本テーマ

ColorScheme.fromSeed(seedColor: Colors.indigo, brightness: brightness)
  • シードカラー: Colors.indigo
  • Material 3: 有効(useMaterial3: true)
  • ダークモード: 完全対応

3層構造

壁紙(surfaceContainerLowest)
  └─ カード(surface)
      └─ 入力フォーム
層 Lightモード Darkモード 用途
壁紙 0xFFDCDCE0 0xFF2C2C2E scaffoldBackgroundColor
カード 0xFFFFFFFF 0xFF3E3E42 CardTheme.color
入力フォーム Colors.white 0xFF3E3E42 InputDecorationTheme.fillColor

カラーパレット

トークンカラー

static const wallpaperLight = Color(0xFFDCDCE0);
static const wallpaperDark = Color(0xFF2C2C2E);
static const cardLight = Color(0xFFFFFFFF);
static const cardDark = Color(0xFF3E3E42);
static const cardLostLight = Color(0xFFF0ECEA);
static const cardLostDark = Color(0xFF38373A);

アクセントカラー(クイックアクション)

static const accentMaster = Color(0xFFE65100);     // deepOrange 900 - マスター管理
static const accentSales = Color(0xFF1565C0);      // blue 800 - 売上
static const accentPurchase = Color(0xFF2E7D32);   // green 800 - 仕入
static const accentInventory = Color(0xFF6A1B9A);  // purple 900 - 在庫
static const accentReport = Color(0xFF37474F);     // blueGrey 800 - レポート
static const accentSettings = Color(0xFF00838F);   // teal 700 - 設定
static const accentDefault = Color(0xFF455A64);    // blueGrey 600 - デフォルト

タイムラインカラー

static const timelineBarLight = Color(0xFF1565C0);   // blue 800
static const timelineBarDark = Color(0xFF64B5F6);   // blue 300
static const timelineMarker = Color(0xFFD32F2F);    // red 700
static const timelineOverdueLight = Color(0xFFD32F2F); // red 700
static const timelineOverdueDark = Color(0xFFEF5350);  // red 400

カードの影

BoxShadow(
  color: cs.shadow.withValues(alpha: 0.12),
  blurRadius: 8,
  offset: const Offset(0, 2),
),
BoxShadow(
  color: cs.shadow.withValues(alpha: 0.06),
  blurRadius: 16,
  offset: const Offset(0, 4),
),

AppBarルール

必須ルール

全てのAppBarは ScreenAppBarTitle を使用すること

import 'package:h_1_core/constants/screen_ids.dart';
import 'package:h_1_core/widgets/screen_id_title.dart';

AppBar(
  title: const ScreenAppBarTitle(screenId: S.xxx, title: '画面タイトル'),
  // ...
)

AppBarTheme定義

appBarTheme: AppBarTheme(
  backgroundColor: scheme.primary,
  foregroundColor: _getContrastColor(scheme.primary),
  surfaceTintColor: Colors.transparent,
  iconTheme: IconThemeData(size: 20, color: _getContrastColor(scheme.primary)),
  titleTextStyle: TextStyle(
    color: _getContrastColor(scheme.primary),
    fontWeight: FontWeight.w600,
    fontSize: 16,
  ),
),

TabBarTheme定義

tabBarTheme: TabBarThemeData(
  labelColor: _getContrastColor(scheme.primary),
  unselectedLabelColor: _getContrastColor(scheme.primary).withValues(alpha: 0.7),
  indicatorColor: _getContrastColor(scheme.primary),
),

注意: TabBarThemeDataを使用すること(TabBarThemeはコンストラクタ)。アイコン色はlabelColorから自動的に継承されます。

コントラスト比の自動計算

前景色は背景色に基づいて自動計算され、適切なコントラスト比が保証されます。

static Color _getContrastColor(Color backgroundColor) {
  final luminance = backgroundColor.computeLuminance();
  return luminance > 0.5 ? Colors.black : Colors.white;
}

highContrastフラグ

デザイナーが意図的に同系色を選択する可能性があるため、highContrast フラグで制御します。

// デフォルト: highContrast=true(輝度計算で自動選択)
AppTheme.light(inputStyle: 'raised', navbarStyle: 'primary')
AppTheme.dark(inputStyle: 'raised', navbarStyle: 'primary')

// 同系色を選択する場合: highContrast=false(Material 3デフォルト)
AppTheme.light(inputStyle: 'raised', navbarStyle: 'primary', highContrast: false)
AppTheme.dark(inputStyle: 'raised', navbarStyle: 'primary', highContrast: false)

ルール:

  • TabBarThemeはテーマ側で一元管理
  • 個別のTabBarで色をハードコードしない
  • AppBar内のTabBarは自動的にTabBarThemeを使用
  • highContrast=true(デフォルト): 前景色は背景色の輝度に基づいて自動選択(黒または白)
  • highContrast=false: Material 3のデフォルト配色(scheme.onPrimary)を使用
  • アクセシビリティ重視の場合は highContrast=true を推奨

画面ID割り当てルール

画面IDは lib/constants/screen_ids.dart の S クラスで管理。

プラグイン 画面ID 用途
配送管理 S.sh メイン画面
配送管理 S.sh1 追跡詳細
配送管理 S.sh2 送り状詳細
配送管理 S.sh3 送付先一覧
配送管理 S.sh4 バーコードスキャン

ルール:

  • メイン画面: S.{plugin_id}(例: S.sh)
  • サブ画面: S.{plugin_id}{1,2,3,...}(例: S.sh1, S.sh2)
  • 新規プラグイン追加時は必ず画面IDを追加すること

アクションボタン

actions: [
  IconButton(
    icon: const Icon(Icons.open_in_browser),
    onPressed: _openUrl,
    tooltip: 'ブラウザで開く',  // 必須
  ),
],

ルール:

  • アイコンは Icons.xxx を使用
  • tooltip は必須(ユーザー補助のため)
  • アクションは3つ以内に抑える(オーバーフロー対策)

TabBarとの併用

TabBarを持つメイン画面の場合:

AppBar(
  title: const ScreenAppBarTitle(screenId: S.sh, title: '配送管理'),
  bottom: TabBar(
    controller: _tabController,
    isScrollable: true,
    tabs: const [
      Tab(icon: Icon(Icons.local_shipping), text: '追跡一覧'),
      Tab(icon: Icon(Icons.print), text: '送り状'),
      // ...
    ],
  ),
),

ルール:

  • isScrollable: true を設定(タブが多い場合)
  • アイコン + テキストの組み合わせを推奨
  • タブ内の画面は個別のAppBarを持たない(メイン画面のTabBarに統合)
  • TabBarの色はハードコードしない - Material 3のテーマシステムに任せる
    • AppBar内のTabBarは自動的にAppBarの前景色(onPrimary)を使用
    • indicatorColor, labelColor, iconColor などの個別指定は禁止

文字色ルール

基本ルール

純白・純黒の直接指定は禁止

// ❌ 禁止
Text('Hello', style: TextStyle(color: Colors.black))
Text('Hello', style: TextStyle(color: Colors.white))

// ✅ 推奨
Text('Hello', style: TextStyle(color: Theme.of(context).colorScheme.onSurface))

textColorOn ユーティリティ

背景色に応じた適切な文字色を自動選択するユーティリティを使用すること。

// 実装: lib/utils/theme_utils.dart
Color textColorOn(Color background) {
  // 知覚輝度ベース(NTSC加重平均)で判定。
  // 中輝度・高彩度の色では WCAG 輝度比(computeLuminance)が
  // 人間の視覚と乖離するため、加重平均による知覚的な明るさで判定する。
  // 例: 緑色(0xFF388E3C) は輝度比では黒文字が有利だが、
  //      知覚輝度では白文字の方が読みやすい。
  final brightness = 0.299 * background.r + 0.587 * background.g + 0.114 * background.b;
  return brightness > 0.55 ? _nearBlack : _nearWhite;
}

許可される純白・純黒

以下の場合のみ直接指定が許可される:

  1. 入力フォーム内部: ユーザー入力フィールド
  2. ヘッダー: AppBar、BottomAppBar
  3. コントラスト要件: アクセシビリティのために必要な場合

入力フォームルール

入力スタイル

2種類の入力スタイルが存在:

スタイル Lightモード Darkモード 用途
raised Colors.white 0xFF3E3E42 デフォルト
flat Colors.white cardDark フラットデザイン

設定方法

AppTheme.light(inputStyle: 'raised')
AppTheme.dark(inputStyle: 'raised')

InputDecorationTheme

const radius = BorderRadius.all(Radius.circular(12));
const pad = EdgeInsetsDirectional.fromSTEB(12, 16, 12, 12);

InputDecorationTheme(
  filled: true,
  fillColor: isDark ? const Color(0xFF3E3E42) : Colors.white,
  contentPadding: pad,
  border: OutlineInputBorder(
    borderRadius: radius,
    borderSide: BorderSide(color: isDark ? const Color(0xFF555559) : const Color(0xFFE0E0E3)),
  ),
  enabledBorder: OutlineInputBorder(
    borderRadius: radius,
    borderSide: BorderSide(color: isDark ? const Color(0xFF555559) : const Color(0xFFE0E0E3)),
  ),
  focusedBorder: OutlineInputBorder(
    borderRadius: radius,
    borderSide: const BorderSide(color: Color(0xFF6366F1), width: 2),
  ),
),

ナビゲーションバー

ナビゲーションスタイル

3種類のナビゲーションスタイルが存在:

スタイル 説明
primary プライマリカラー
black 純黒
dark_grey ダークグレー(デフォルト)

設定方法

AppTheme.light(navbarStyle: 'primary')
AppTheme.dark(navbarStyle: 'primary')

NavigationBarTheme

navigationBarTheme: NavigationBarThemeData(
  backgroundColor: navBarColor,
  indicatorColor: scheme.primary.withValues(alpha: 0.2),
),

フォント

static const fontFamily = 'IPAexGothic';

日本語フォントとして IPAexGothic を使用。


禁止事項

色の直接指定

// ❌ 禁止
Container(color: Colors.red)
Text('Hello', style: TextStyle(color: Colors.black))

// ✅ 推奨
Container(color: Theme.of(context).colorScheme.error)
Text('Hello', style: TextStyle(color: Theme.of(context).colorScheme.onSurface))

ハードコードされたサイズ

// ❌ 禁止
SizedBox(height: 16)

// ✅ 推奨
SizedBox(height: 8.0)  // 4の倍数(Material Designグリッド)

相対パスimport

// ❌ 禁止
import '../widgets/my_widget.dart';

// ✅ 推奨
import 'package:h_1_core/widgets/my_widget.dart';

参考ファイル

  • lib/utils/app_theme.dart - テーマ定義
  • lib/constants/screen_ids.dart - 画面ID定義
  • lib/widgets/screen_id_title.dart - ScreenAppBarTitle実装
  • .ai/context.md - プロジェクト全体のコンテキスト

チェックリスト

新規画面・プラグイン開発時のチェックリスト:

  • ScreenAppBarTitle を使用している
  • 画面IDを screen_ids.dart に追加した
  • AppBarのアクションボタンに tooltip を設定した
  • 色は Theme.of(context).colorScheme を使用している
  • 純白・純黒の直接指定をしていない
  • インポートは絶対パスを使用している
  • ダークモードで確認した