本文へ移動
cccskills
無料GitHub で公開

openfoodjournal-ui

UI/UX design system and component patterns for OpenFoodJournal. Use when building new views, adding UI components, styling elements, presenting sheets, or modifying any view file. Contains the project's design tokens, component catalog, animation conventions, and layout patterns so new UI stays visually consistent with the existing app.

インストール方法を見る

含まれるファイル(1)

  • SKILL.md12.7 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

OpenFoodJournal UI/UX Design System

Living reference for every visual and interaction pattern in the app. Consult before creating or modifying any SwiftUI view. Update whenever a new pattern is established or an existing one changes.

Quick Reference

TokenValueWhere Used
Card corner radius20 ptMacroSummaryBar, glass cards
Button/chip radius12 ptRemaining calorie pill, macro chips
Badge radius8 ptStatus badges, small pills
List row insets(8, 16, 8, 16)All List rows
Section gap12–20 ptBetween major sections
Min hit target44 ptAll tappable elements
Glass availabilityiOS 26+ (no guards)Entire codebase

Color System

Macro Colors (Canonical)

MacroColorUsage
Calories.orangeCalorie countdown, energy metrics
Protein.blueProtein goals, progress rings
Carbs.greenCarbohydrate goals, progress
Fat.yellowFat goals, progress

Progress Status Colors

StatusColorThreshold
Met.green95–105% of goal
Near.yellow50–80% of goal
Slightly over.orange105–120% of goal
Way over.purple>120% of goal
Under.red<50% of goal

Opacity Conventions

ElementOpacity
Glass tint0.35
Background highlight0.12
Disabled state0.5
Separator lines.secondary.opacity(0.4)
Secondary text.foregroundStyle(.secondary) (system)
Tertiary text.foregroundStyle(.tertiary) (system)

Typography

RoleFontWeightSize
Display macro total.system(size: 32, weight: .bold, design: .rounded)bold32
Section headline.headlinedefault~17
Row title.body.fontWeight(.medium)~17
Row subtitle.captiondefault~12
Form label.subheadlinedefault~15
Numeric alignment.monospacedDigit()—inherited

Convention: All numeric displays use .system(design: .rounded). Precise alignment uses .monospacedDigit().

Glassmorphism (iOS 26+)

The app targets iOS 26+ exclusively — no #available guards needed.

Standard Patterns

// Card surface
.glassEffect(in: .rect(cornerRadius: 20))

// Circular element (macro rings)
.glassEffect(in: .circle)

// Tinted interactive element
.glassEffect(
    .regular.tint(color.opacity(0.35)),
    in: .circle
)

// Multiple glass elements in proximity
GlassEffectContainer(spacing: 20) {
    HStack { /* glass children */ }
}

// Buttons
.buttonStyle(.glass)           // Standard
.buttonStyle(.glassProminent)  // Primary action

Glass Rules

  1. Apply .glassEffect() after layout and appearance modifiers
  2. Wrap multiple glass siblings in GlassEffectContainer
  3. Use .interactive() only on tappable/focusable elements
  4. Use .glassEffectID(_:in:) + @Namespace for morphing transitions
  5. Prefer .glassEffect() over .background(.ultraThinMaterial) for new UI

See the swiftui-liquid-glass skill for the complete Liquid Glass API reference.

Navigation Architecture

Tab Structure

4-tab TabView at root with .tabBarMinimizeBehavior(.never):

TabViewIcon
JournalDailyLogViewbook.pages
Food BankFoodBankViewrefrigerator
HistoryHistoryViewchart.xyaxis.line
SettingsSettingsViewgearshape

Each tab wraps its content in NavigationStack.

Sheet Management (Enum-Driven)

Always use a single enum for all sheets within a page:

enum DailyLogSheet: Identifiable {
    case scan, manualEntry, editEntry(NutritionEntry)
    case foodBank, containers
    
    var id: String { /* unique per case */ }
}

@State private var presentedSheet: DailyLogSheet?

// In body:
.sheet(item: $presentedSheet) { sheet in
    switch sheet {
    case .scan: ScanCaptureView(logDate: selectedDate)
    case .manualEntry: ManualEntryView(defaultDate: selectedDate)
    // ...
    }
}

Never use multiple @State booleans for sheet presentation.

Sheet Configuration

Every sheet must include:

  • NavigationStack wrapper for internal nav
  • .navigationTitle() + .navigationBarTitleDisplayMode(.inline)
  • Cancel button: ToolbarItem(placement: .cancellationAction)
  • Save button: ToolbarItem(placement: .confirmationAction) with .fontWeight(.semibold) and .disabled() guard
  • @Environment(\.dismiss) private var dismiss

Component Catalog

Shared Components (Views/Shared/)

ComponentPurposeSize/Shape
MacroRingViewCircular progress for one macro56×56 pt, circle
MacroSummaryBar3-column macro cards + calorie headlineFull width, glass card
RadialMenuButtonFloating "+" FAB with radial menuCircular, bottom-aligned
MicronutrientSummaryViewProgress bars for all microsFull width section
NutrientBreakdownViewDonut chart + per-food barsNavigationDestination
NutritionDetailViewPeriod picker + glass macro card + glass vitamin/mineral sectionsScrollView on JournalCalorieBackground
ServingMappingSectionReusable Form section for unit mapsForm section
CursorEndModifierUITextField cursor fixApplied at app root

Row Components

ComponentContextKey Elements
EntryRowViewList row in DailyLogName + brand + calories + macro chips + swipe
SavedFoodRowViewList row in FoodBankSource icon + name + serving + calories
MealSectionViewSection wrapper in ListHeader with meal icon + calorie total

Inline Component Pattern

Extract reusable card builders as private @ViewBuilder functions:

@ViewBuilder
private func macroCard(_ macro: NutrientKind.MacroType, value: Double, 
                       goal: Double, color: Color) -> some View {
    Button { selectedMacro = macro } label: {
        VStack(spacing: 4) { /* content */ }
    }
    .buttonStyle(.plain)
}

Layout Patterns

List (Preferred for Swipeable Content)

List {
    MealSectionView(...)  // Returns Section{}
}
.listStyle(.plain)
.scrollContentBackground(.hidden)

Gotcha: .swipeActions is silently ignored outside List. Never use ScrollView + LazyVStack for rows that need swipe actions.

LazyVGrid (2-Column Cards)

LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 10) {
    macroCard(...)  // 4 cards in 2×2
}

List Row Customization

.listRowSeparator(.hidden)
.listRowBackground(Color.clear)
.listRowInsets(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))

Empty States

ContentUnavailableView {
    Label("No Saved Foods", systemImage: "refrigerator")
} description: {
    Text("Save a food from a scan first...")
}

Animation Conventions

ContextAnimationValue
Menu open/close.spring(duration: 0.4, bounce: 0.3)RadialMenuButton
Micro-interaction.spring(duration: 0.2)Option highlight
Disclosure toggle.spring(duration: 0.3)Section expand
Progress rings.easeInOutValue transitions
View transitions.opacity.combined(with: .move(edge: .top))Expanding sections
Glass morphing.glassEffectTransition(.matchedGeometry)RadialMenu items
Haptic feedback.sensoryFeedback(.impact(flexibility: .soft))Menu open/close
Sheet chain delayasyncAfter(deadline: .now() + 0.15)Before next sheet

Convention: Use withAnimation(.spring(...)) for user-initiated actions. Use .animation(.easeInOut, value:) for data-driven transitions.

Form/Input Patterns

Numeric Text Fields

// Always use text-backed fields to avoid cursor-jump artifacts
@State private var quantityText: String
let quantity = Double(quantityText) ?? 0

TextField("Weight", text: $quantityText)
    .keyboardType(.decimalPad)

Focus Chain

fileprivate enum FormField: Hashable {
    case name, calories, protein, carbs, fat
    case micronutrient(String)
}
@FocusState private var focusedField: FormField?

// Chain fields:
TextField("Calories", text: $caloriesText)
    .focused($focusedField, equals: .calories)
    .submitLabel(.next)
    .onSubmit { focusedField = .protein }

Picker Styles

StyleUse Case
.pickerStyle(.menu)Compact inline (meal type in cards)
.pickerStyle(.segmented)3–4 options (period picker)
Default wheelIn-form selection

History Micronutrient Pins

Long-press a History micronutrient chip to open a Pin / Unpin menu. Pins persist in UserDefaults (history.pinnedMicronutrientIDs) via PinnedMicronutrientSettings and appear in a dedicated row above the full chip scroller. Macros are not pinnable. Pinnable chips use Menu with primaryAction (tap selects, hold opens the menu) rather than Button + contextMenu, which does not present inside a horizontal ScrollView with interactive Liquid Glass.

Confirmation Dialogs

.confirmationDialog(
    "Delete \(food.name)?",
    isPresented: $showDeleteConfirm,
    titleVisibility: .visible
) {
    Button("Delete", role: .destructive) { /* action */ }
}

State Management Patterns

Service Injection

// App creates @Observable services, injects via .environment()
@Environment(NutritionStore.self) private var store
@Environment(UserGoals.self) private var goals

SwiftData Bindings

@Bindable var entry: NutritionEntry  // Two-way binding to @Model
TextField("Name", text: $entry.name)  // Auto-persists

Derived State

// Compute from primary sources — never duplicate as @State
private var filteredFoods: [SavedFood] {
    searchText.isEmpty ? allFoods : allFoods.filter { ... }
}

Persistence

// UserGoals uses AppStorage with @ObservationIgnored to avoid conflicts
@ObservationIgnored @AppStorage("goals.calories")
var dailyCalories: Double = 2000

SF Symbols Reference

ActionSymbol
Scancamera.fill
Manual entrypencil
Food Bankrefrigerator
Containersscalemass
Historychart.xyaxis.line
Settingsgearshape
Deletetrash
Addplus / plus.circle
Breakfastsunrise
Lunchsun.max
Dinnermoon.stars
Snackleaf
Estimate modewand.and.sparkles
Label modebarcode.viewfinder

Checklist for New Views

Before merging any new view:

  • Uses glass effects (not .background(.ultraThinMaterial))
  • Card corners use 20 pt radius
  • Colors match macro color table above
  • Sheets use enum-driven presentation
  • Sheets have Cancel/Save toolbar + dismiss
  • List rows use standard insets (8, 16, 8, 16)
  • Animations use spring for user actions, easeInOut for data
  • Numeric inputs use text-backed fields
  • Tappable elements have 44+ pt hit targets
  • Empty states use ContentUnavailableView
  • Progress indicators use the status color thresholds

UI Roadmap

Food Bank Improvements

  • Remove source icons (barcode/fork/pencil) from SavedFoodRowView — they add visual noise without value
  • Shift calorie count from right edge to where source icons were (left-center area)
  • Add macro chips (P/C/F) to the right of each food row — same MacroChip pattern as EntryRowView
  • Add "Last Used" sort option and make it the default — newly added foods count as "last used" even before being logged, so recently saved foods surface to the top for better glanceability
  • Search should match brand field in addition to name

Keyboard UX

  • Every keyboard popup must have a "Done" button to dismiss — use .toolbar { ToolbarItemGroup(placement: .keyboard) { Spacer(); Button("Done") { focusedField = nil } } }

Calendar Strip → Continuous Scrollable Calendar

  • Replace fixed Sun–Sat WeeklyCalendarStrip with a continuously scrollable calendar
  • Interaction design modeled after iOS Calendar monthly view: vertical continuous scroll with smooth momentum snapping to month boundaries
  • Sticky month/year header that transitions smoothly as user scrolls between months
  • Days show the same progress ring colors as current day cells
  • Tapping a day selects it and scrolls the journal to that date

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

同じリポジトリのスキル

概要と使いどころ

Evaluates code against Apple's App Store Review Guidelines. Use this skill when reviewing iOS, macOS, tvOS, watchOS, or visionOS app code (Swift, Objective-C, React Native, or Expo) to identify potential App Store rejection issues before submission. Triggers on tasks involving app review preparation, compliance checking, or App Store submission readiness.

日本語の概要は準備中です。原文の説明を表示しています。

kvn8888/OpenFoodJournal52026年10月10日 更新

Create a new App Store Connect app record via browser automation. Use when there is no public API for app creation and you need an agent to drive the New App form.

日本語の概要は準備中です。原文の説明を表示しています。

kvn8888/OpenFoodJournal52026年10月10日 更新

Run an offline ASO audit on canonical App Store metadata under `./metadata` and surface keyword gaps using Astro MCP. Use after pulling metadata with `asc metadata pull`.

日本語の概要は準備中です。原文の説明を表示しています。

kvn8888/OpenFoodJournal52026年10月10日 更新

Track build processing, find latest builds, and clean up old builds with asc. Use when managing build retention or waiting on processing.

日本語の概要は準備中です。原文の説明を表示しています。

kvn8888/OpenFoodJournal52026年10月10日 更新

Guidance for using asc cli in this repo (flags, output formats, pagination, auth, and discovery). Use when asked to run or design asc commands or interact with App Store Connect via the CLI.

日本語の概要は準備中です。原文の説明を表示しています。

kvn8888/OpenFoodJournal52026年10月10日 更新

Triage TestFlight crashes, beta feedback, and performance diagnostics using asc. Use when the user asks about TF crashes, TestFlight crash reports, beta tester feedback, app hangs, disk writes, launch diagnostics, or wants a crash summary for a build or app.

日本語の概要は準備中です。原文の説明を表示しています。

kvn8888/OpenFoodJournal52026年10月10日 更新

kvn8888 のスキルをすべて見る

このスキルの問題を報告する