From 2687e9126e2aed74cc827e7dffb0c1eaec42790d Mon Sep 17 00:00:00 2001 From: Kyle Van Essen Date: Thu, 2 Apr 2026 15:08:34 -0400 Subject: [PATCH 01/13] Add sync-agents script to generate CLAUDE.md and Cursor rules from AGENTS.md AGENTS.md is the single source of truth for AI agent instructions. This script generates CLAUDE.md (for Claude Code) and .cursor/rules/*.mdc (for Cursor) so directory-scoped instructions work across all three tools. The pre-commit hook runs it automatically when any AGENTS.md file is staged. Made-with: Cursor --- .githooks/pre-commit | 11 ++++ AGENTS.md | 11 ++++ CLAUDE.md | 143 +++++++++++++++++++++++++++++++++++++++++++ sync-agents | 66 ++++++++++++++++++++ 4 files changed, 231 insertions(+) create mode 100644 CLAUDE.md create mode 100755 sync-agents diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 2ecf17c..a12a040 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -14,3 +14,14 @@ if [ -n "$staged" ]; then git diff --cached --name-only --diff-filter=ACMR -z -- '*.swift' \ | xargs -0 git add fi + +# Sync AGENTS.md → CLAUDE.md + .cursor/rules/*.mdc, then re-stage. +staged_agents=$(git diff --cached --name-only --diff-filter=ACMRD | grep 'AGENTS\.md$' || true) + +if [ -n "$staged_agents" ]; then + ./sync-agents + + git add -A -- 'CLAUDE.md' 2>/dev/null || true + git add -A -- '*/CLAUDE.md' 2>/dev/null || true + git add -A -- '.cursor/rules' 2>/dev/null || true +fi diff --git a/AGENTS.md b/AGENTS.md index 829d368..4f69607 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -54,6 +54,7 @@ Broadway is a SwiftUI iOS + Mac Catalyst application managed by **Tuist**. The X │ └── TypeIdentifierTests.swift ├── Plans/ # Archived implementation plans (see index below) ├── swiftformat # Run SwiftFormat (--lint to check only) +├── sync-agents # Generate CLAUDE.md + .cursor/rules from AGENTS.md ├── ide # Dev script (installs hooks, runs tuist generate) ├── LICENSE # Apache 2.0 ├── README.md # Project overview and setup instructions @@ -77,6 +78,16 @@ Broadway is a SwiftUI iOS + Mac Catalyst application managed by **Tuist**. The X - The `./ide` script configures `core.hooksPath` to `.githooks/`, which installs a pre-commit hook that lints staged `.swift` files. - CI runs `./swiftformat --lint` as a gate before build & test. +## Agent Instructions Sync + +`AGENTS.md` is the source of truth for AI agent instructions. Cursor and Codex read it natively, but Claude Code uses `CLAUDE.md` and Cursor uses `.cursor/rules/*.mdc` for directory-scoped rules. + +- Run `./sync-agents` to generate `CLAUDE.md` and `.cursor/rules/*.mdc` files from all `AGENTS.md` files in the repo. +- The root `AGENTS.md` produces only `CLAUDE.md` (Cursor reads `AGENTS.md` directly). +- Nested `AGENTS.md` files produce both a `CLAUDE.md` in the same directory and a glob-scoped `.mdc` rule under `.cursor/rules/`. +- Generated files contain a marker comment on the first line. The script uses this marker to clean up stale files on subsequent runs. +- Re-run `./sync-agents` after adding, editing, or removing any `AGENTS.md` file. + ## Targets | Target | Type | Bundle ID | Destinations | Min Deployment | diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..8d35483 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,143 @@ + + +# AGENTS.md — Repository Shape + +This document describes the structure and conventions of the Broadway repository for AI agents and contributors. + +## Overview + +Broadway is a SwiftUI iOS + Mac Catalyst application managed by **Tuist**. The Xcode project is not checked in — it is generated from the `Project.swift` manifest. + +## Directory Layout + +``` +/ +├── .githooks/pre-commit # Git pre-commit hook (SwiftFormat lint) +├── .mise.toml # mise tool versions (pins Tuist, SwiftFormat) +├── .swiftformat # SwiftFormat configuration +├── Tuist.swift # Tuist global configuration +├── Project.swift # Tuist project manifest (root level) +├── BroadwayCatalog/ +│ ├── Sources/ # Catalog app source code (Swift / SwiftUI) +│ │ ├── BroadwayApp.swift # @main entry point +│ │ └── ContentView.swift # Root view +│ ├── Resources/ # Bundled resources (assets, localization, etc.) +│ └── Tests/ # Catalog app unit tests (Swift Testing) +│ └── BroadwayCatalogTests.swift +├── BroadwayUI/ +│ ├── Sources/ # UI framework source code +│ │ └── BRootViewController.swift # Root container VC (context + trait propagation) +│ └── Tests/ # UI framework unit tests (Swift Testing) +│ └── BRootViewControllerTests.swift +├── BroadwayTestHost/ +│ └── Sources/ # Minimal app used as test host for unit tests +│ └── TestHostApp.swift # @main entry point (empty window) +├── BroadwayTesting/ +│ └── Sources/ # Test utilities framework (depends on BroadwayCore) +│ └── BroadwayTesting.swift # Module entry point +├── BroadwayCore/ +│ ├── Sources/ # Core framework source code +│ │ ├── AnyEquatable.swift # Type-erased Equatable wrapper +│ │ ├── BAccessibility.swift # Accessibility snapshot + Observer +│ │ ├── BContext.swift # Root environment container +│ │ ├── BContext+UITraits.swift # UITraitDefinition bridge (#if canImport(UIKit)) +│ │ ├── BStylesheets.swift # Lazy cached stylesheet resolver +│ │ ├── BThemes.swift # Type-keyed theme container +│ │ ├── BTraits.swift # Type-keyed trait container +│ │ ├── CopyOnWrite.swift # COW property wrapper +│ │ └── TypeIdentifier.swift # Lightweight type-keyed identifier +│ └── Tests/ # Core framework unit tests (Swift Testing) +│ ├── AnyEquatableTests.swift +│ ├── BAccessibilityTests.swift +│ ├── BContextTests.swift +│ ├── BThemesTests.swift +│ ├── BTraitsTests.swift +│ ├── CopyOnWriteTests.swift +│ └── TypeIdentifierTests.swift +├── Plans/ # Archived implementation plans (see index below) +├── swiftformat # Run SwiftFormat (--lint to check only) +├── sync-agents # Generate CLAUDE.md + .cursor/rules from AGENTS.md +├── ide # Dev script (installs hooks, runs tuist generate) +├── LICENSE # Apache 2.0 +├── README.md # Project overview and setup instructions +└── AGENTS.md # This file +``` + +## Build System + +- **Tuist 4+** is used to generate the Xcode project from `Project.swift`. +- Tuist and SwiftFormat are version-pinned via **mise** in `.mise.toml`. Run `mise install` to install them. +- Run `./ide` to generate the Xcode project (or `./ide -i` to run `mise exec -- tuist install` first). +- Run `mise exec -- tuist test` to execute all tests. +- Run `mise exec -- tuist test ` to test a specific target. The scheme name is the **framework name** (e.g., `BroadwayCore`), not the test target name (`BroadwayCoreTests`). +- The generated `.xcodeproj` and `Derived/` directory are git-ignored. + +## Formatting + +- **SwiftFormat** enforces consistent code style. Configuration lives in `.swiftformat`. +- Run `./swiftformat` to format all Swift files in-place. +- Run `./swiftformat --lint` to check without modifying (used in CI and pre-commit). +- The `./ide` script configures `core.hooksPath` to `.githooks/`, which installs a pre-commit hook that lints staged `.swift` files. +- CI runs `./swiftformat --lint` as a gate before build & test. + +## Agent Instructions Sync + +`AGENTS.md` is the source of truth for AI agent instructions. Cursor and Codex read it natively, but Claude Code uses `CLAUDE.md` and Cursor uses `.cursor/rules/*.mdc` for directory-scoped rules. + +- Run `./sync-agents` to generate `CLAUDE.md` and `.cursor/rules/*.mdc` files from all `AGENTS.md` files in the repo. +- The root `AGENTS.md` produces only `CLAUDE.md` (Cursor reads `AGENTS.md` directly). +- Nested `AGENTS.md` files produce both a `CLAUDE.md` in the same directory and a glob-scoped `.mdc` rule under `.cursor/rules/`. +- Generated files contain a marker comment on the first line. The script uses this marker to clean up stale files on subsequent runs. +- Re-run `./sync-agents` after adding, editing, or removing any `AGENTS.md` file. + +## Targets + +| Target | Type | Bundle ID | Destinations | Min Deployment | +|---|---|---|---|---| +| `BroadwayCatalog` | `.app` | `com.broadway.catalog` | iPhone, iPad, Mac Catalyst | iOS 26.0 | +| `BroadwayCatalogTests` | `.unitTests` | `com.broadway.catalog.tests` | iPhone, iPad, Mac Catalyst | iOS 26.0 | +| `BroadwayUI` | `.framework` | `com.broadway.ui` | iPhone, iPad, Mac Catalyst | iOS 26.0 | +| `BroadwayUITests` | `.unitTests` | `com.broadway.ui.tests` | iPhone, iPad, Mac Catalyst | iOS 26.0 | +| `BroadwayCore` | `.framework` | `com.broadway.core` | iPhone, iPad, Mac Catalyst | iOS 26.0 | +| `BroadwayCoreTests` | `.unitTests` | `com.broadway.core.tests` | iPhone, iPad, Mac Catalyst | iOS 26.0 | +| `BroadwayTestHost` | `.app` | `com.broadway.testhost` | iPhone, iPad, Mac Catalyst | iOS 26.0 | +| `BroadwayTesting` | `.framework` | `com.broadway.testing` | iPhone, iPad, Mac Catalyst | iOS 26.0 | + +### Dependency Graph + +``` +BroadwayCatalog (app) ──▶ BroadwayUI (framework) ──▶ BroadwayCore (framework) + ▲ +BroadwayTestHost (app) ──▶ BroadwayUI ──────────────────────┤ + │ +BroadwayTesting (framework) ────────────────────────────────┘ + +All framework test targets use BroadwayTestHost and depend on BroadwayTesting. +``` + +## Key Conventions + +- **SwiftUI** is the UI framework. Catalog app views live under `BroadwayCatalog/Sources/`. +- **BroadwayUI** is the reusable component library. All shared UI lives under `BroadwayUI/Sources/`. +- **BroadwayCore** provides foundational utilities and shared logic. Source lives under `BroadwayCore/Sources/`. +- **BroadwayTestHost** is a minimal app that serves as the test host for framework unit tests. Source lives under `BroadwayTestHost/Sources/`. +- **BroadwayTesting** provides shared test utilities. All test targets depend on it. Source lives under `BroadwayTesting/Sources/`. +- **Swift Testing** (`import Testing`) is used for unit tests, not XCTest. +- Source files use `/Sources/**` globs; test files use `/Tests/**`. +- Resources (asset catalogs, localization files, etc.) go in `BroadwayCatalog/Resources/`. +- The catalog app uses `@main` via `BroadwayApp.swift` as the app entry point. +- Info.plist is auto-generated by Tuist via `infoPlist: .extendingDefault(with:)`. + +## Plans + +Implementation plans are stored in the `Plans/` directory. When you develop a plan using Cursor's plan mode, copy the final plan into `Plans/` and add an entry to the index below. + +**Naming convention**: `--.md`, where `` is the next sequential number (zero-padded to 3 digits), `` is the date the plan was created, and `` is a short snake_case description. For example: `002-2026-03-15-dark_mode_support.md`. + +## Adding New Files + +- **New catalog app files**: Add `.swift` files to `BroadwayCatalog/Sources/`. +- **New UI components**: Add `.swift` files to `BroadwayUI/Sources/`. Mark public API as `public`. +- **New test files**: Add `.swift` files to the appropriate `Tests/` directory. +- **New resources**: Add to `BroadwayCatalog/Resources/`. They are bundled via the `Resources/**` glob. +- **New targets or dependencies**: Edit `Project.swift` at the repository root. diff --git a/sync-agents b/sync-agents new file mode 100755 index 0000000..8dbcec4 --- /dev/null +++ b/sync-agents @@ -0,0 +1,66 @@ +#!/bin/bash +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")" && pwd)" +MARKER="" + +# --- Clean previously generated files --- + +while IFS= read -r -d '' file; do + if head -n1 "$file" | grep -qF "Generated from AGENTS.md"; then + rm "$file" + echo "Removed $file" + fi +done < <(find "$REPO_ROOT" -name "CLAUDE.md" \ + -not -path "*/Derived/*" \ + -not -path "*/.build/*" \ + -not -path "*/.git/*" \ + -print0 2>/dev/null || true) + +if [ -d "$REPO_ROOT/.cursor/rules" ]; then + while IFS= read -r -d '' file; do + if head -n5 "$file" | grep -qF "AGENTS.md — do not edit directly"; then + rm "$file" + echo "Removed $file" + fi + done < <(find "$REPO_ROOT/.cursor/rules" -name "*.mdc" -print0 2>/dev/null || true) +fi + +# --- Generate from AGENTS.md files --- + +while IFS= read -r -d '' agents_file; do + dir="$(dirname "$agents_file")" + rel_dir="${dir#"$REPO_ROOT"}" + rel_dir="${rel_dir#/}" + + # CLAUDE.md — all AGENTS.md files including root + { + echo "$MARKER" + echo "" + cat "$agents_file" + } > "$dir/CLAUDE.md" + echo "Generated ${rel_dir:+$rel_dir/}CLAUDE.md" + + # .cursor/rules/*.mdc — nested only (Cursor reads root AGENTS.md natively) + if [ -n "$rel_dir" ]; then + mkdir -p "$REPO_ROOT/.cursor/rules" + mdc_name="${rel_dir//\//-}" + mdc_path="$REPO_ROOT/.cursor/rules/${mdc_name}.mdc" + + { + echo "---" + echo "description: \"Generated from ${rel_dir}/AGENTS.md — do not edit directly\"" + echo "globs: [\"${rel_dir}/**\"]" + echo "alwaysApply: false" + echo "---" + echo "" + cat "$agents_file" + } > "$mdc_path" + echo "Generated .cursor/rules/${mdc_name}.mdc" + fi +done < <(find "$REPO_ROOT" -name "AGENTS.md" \ + -not -path "*/Derived/*" \ + -not -path "*/.build/*" \ + -not -path "*/.git/*" \ + -not -path "*/.cursor/*" \ + -print0 2>/dev/null || true) From ccf33fa6ddc637ce3c7ef3b3376189c023c5c468 Mon Sep 17 00:00:00 2001 From: Kyle Van Essen Date: Thu, 2 Apr 2026 15:27:49 -0400 Subject: [PATCH 02/13] Add skills sync, incremental updates, and --git-add flag to sync-agents - Sync .agents/skills/ to .claude/skills/ so skills work across Codex, Cursor, and Claude Code from a single source directory. - Make sync-agents skip work when generated files are already up to date. - Add --git-add flag to stage generated files, keeping all sync logic self-contained. Simplify the pre-commit hook to a one-liner. - Add skeleton skill for custom container view controllers. Made-with: Cursor --- .../custom-container-view-controller/SKILL.md | 8 + .../custom-container-view-controller/SKILL.md | 8 + .githooks/pre-commit | 12 +- sync-agents | 137 +++++++++++++----- 4 files changed, 117 insertions(+), 48 deletions(-) create mode 100644 .agents/skills/custom-container-view-controller/SKILL.md create mode 100644 .claude/skills/custom-container-view-controller/SKILL.md diff --git a/.agents/skills/custom-container-view-controller/SKILL.md b/.agents/skills/custom-container-view-controller/SKILL.md new file mode 100644 index 0000000..d7eaf5b --- /dev/null +++ b/.agents/skills/custom-container-view-controller/SKILL.md @@ -0,0 +1,8 @@ +--- +name: custom-container-view-controller +description: Build UIKit custom container view controllers with correct child lifecycle management. Use when creating container view controllers, embedding child VCs, or managing view controller containment. +--- + +# Custom Container View Controller + + diff --git a/.claude/skills/custom-container-view-controller/SKILL.md b/.claude/skills/custom-container-view-controller/SKILL.md new file mode 100644 index 0000000..d7eaf5b --- /dev/null +++ b/.claude/skills/custom-container-view-controller/SKILL.md @@ -0,0 +1,8 @@ +--- +name: custom-container-view-controller +description: Build UIKit custom container view controllers with correct child lifecycle management. Use when creating container view controllers, embedding child VCs, or managing view controller containment. +--- + +# Custom Container View Controller + + diff --git a/.githooks/pre-commit b/.githooks/pre-commit index a12a040..19bdfcf 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -15,13 +15,5 @@ if [ -n "$staged" ]; then | xargs -0 git add fi -# Sync AGENTS.md → CLAUDE.md + .cursor/rules/*.mdc, then re-stage. -staged_agents=$(git diff --cached --name-only --diff-filter=ACMRD | grep 'AGENTS\.md$' || true) - -if [ -n "$staged_agents" ]; then - ./sync-agents - - git add -A -- 'CLAUDE.md' 2>/dev/null || true - git add -A -- '*/CLAUDE.md' 2>/dev/null || true - git add -A -- '.cursor/rules' 2>/dev/null || true -fi +# Sync AGENTS.md + .agents/skills → CLAUDE.md, .cursor/rules, .claude/skills. +./sync-agents --git-add diff --git a/sync-agents b/sync-agents index 8dbcec4..15342d9 100755 --- a/sync-agents +++ b/sync-agents @@ -4,27 +4,43 @@ set -euo pipefail REPO_ROOT="$(cd "$(dirname "$0")" && pwd)" MARKER="" -# --- Clean previously generated files --- +GIT_ADD=false +for arg in "$@"; do + case "$arg" in + --git-add) GIT_ADD=true ;; + esac +done -while IFS= read -r -d '' file; do - if head -n1 "$file" | grep -qF "Generated from AGENTS.md"; then - rm "$file" - echo "Removed $file" +changed=false +expected_files="$(mktemp)" +trap 'rm -f "$expected_files"' EXIT + +# --- Helper: write a file only when its content has changed --- + +write_if_changed() { + local target="$1" + local content="$2" + + if [ -f "$target" ] && [ "$(cat "$target")" = "$content" ]; then + return fi -done < <(find "$REPO_ROOT" -name "CLAUDE.md" \ - -not -path "*/Derived/*" \ - -not -path "*/.build/*" \ - -not -path "*/.git/*" \ - -print0 2>/dev/null || true) -if [ -d "$REPO_ROOT/.cursor/rules" ]; then - while IFS= read -r -d '' file; do - if head -n5 "$file" | grep -qF "AGENTS.md — do not edit directly"; then - rm "$file" - echo "Removed $file" - fi - done < <(find "$REPO_ROOT/.cursor/rules" -name "*.mdc" -print0 2>/dev/null || true) -fi + mkdir -p "$(dirname "$target")" + printf '%s\n' "$content" > "$target" + echo "Updated $target" + changed=true +} + +# --- Helper: remove a file if it exists --- + +remove_if_exists() { + local target="$1" + if [ -f "$target" ]; then + rm "$target" + echo "Removed $target" + changed=true + fi +} # --- Generate from AGENTS.md files --- @@ -33,34 +49,79 @@ while IFS= read -r -d '' agents_file; do rel_dir="${dir#"$REPO_ROOT"}" rel_dir="${rel_dir#/}" - # CLAUDE.md — all AGENTS.md files including root - { - echo "$MARKER" - echo "" - cat "$agents_file" - } > "$dir/CLAUDE.md" - echo "Generated ${rel_dir:+$rel_dir/}CLAUDE.md" + claude_path="$dir/CLAUDE.md" + content="$(printf '%s\n\n%s' "$MARKER" "$(cat "$agents_file")")" + write_if_changed "$claude_path" "$content" + echo "$claude_path" >> "$expected_files" - # .cursor/rules/*.mdc — nested only (Cursor reads root AGENTS.md natively) if [ -n "$rel_dir" ]; then - mkdir -p "$REPO_ROOT/.cursor/rules" mdc_name="${rel_dir//\//-}" mdc_path="$REPO_ROOT/.cursor/rules/${mdc_name}.mdc" - - { - echo "---" - echo "description: \"Generated from ${rel_dir}/AGENTS.md — do not edit directly\"" - echo "globs: [\"${rel_dir}/**\"]" - echo "alwaysApply: false" - echo "---" - echo "" - cat "$agents_file" - } > "$mdc_path" - echo "Generated .cursor/rules/${mdc_name}.mdc" + mdc_content="$(printf '%s\n%s\n%s\n%s\n%s\n\n%s' \ + "---" \ + "description: \"Generated from ${rel_dir}/AGENTS.md — do not edit directly\"" \ + "globs: [\"${rel_dir}/**\"]" \ + "alwaysApply: false" \ + "---" \ + "$(cat "$agents_file")")" + write_if_changed "$mdc_path" "$mdc_content" + echo "$mdc_path" >> "$expected_files" fi done < <(find "$REPO_ROOT" -name "AGENTS.md" \ -not -path "*/Derived/*" \ -not -path "*/.build/*" \ -not -path "*/.git/*" \ -not -path "*/.cursor/*" \ + -not -path "*/.claude/*" \ + -print0 2>/dev/null || true) + +# --- Clean stale CLAUDE.md files --- + +while IFS= read -r -d '' file; do + if ! grep -qxF "$file" "$expected_files"; then + if head -n1 "$file" | grep -qF "Generated from AGENTS.md"; then + remove_if_exists "$file" + fi + fi +done < <(find "$REPO_ROOT" -name "CLAUDE.md" \ + -not -path "*/Derived/*" \ + -not -path "*/.build/*" \ + -not -path "*/.git/*" \ -print0 2>/dev/null || true) + +# --- Clean stale .mdc files --- + +if [ -d "$REPO_ROOT/.cursor/rules" ]; then + while IFS= read -r -d '' file; do + if ! grep -qxF "$file" "$expected_files"; then + if head -n5 "$file" | grep -qF "AGENTS.md — do not edit directly"; then + remove_if_exists "$file" + fi + fi + done < <(find "$REPO_ROOT/.cursor/rules" -name "*.mdc" -print0 2>/dev/null || true) +fi + +# --- Sync .agents/skills → .claude/skills --- + +if [ -d "$REPO_ROOT/.agents/skills" ]; then + mkdir -p "$REPO_ROOT/.claude/skills" + if ! diff -rq "$REPO_ROOT/.agents/skills" "$REPO_ROOT/.claude/skills" > /dev/null 2>&1; then + rm -rf "$REPO_ROOT/.claude/skills" + cp -R "$REPO_ROOT/.agents/skills" "$REPO_ROOT/.claude/skills" + echo "Synced .agents/skills/ → .claude/skills/" + changed=true + fi +elif [ -d "$REPO_ROOT/.claude/skills" ]; then + rm -rf "$REPO_ROOT/.claude/skills" + echo "Removed .claude/skills/" + changed=true +fi + +# --- Stage generated files if --git-add was passed --- + +if [ "$GIT_ADD" = true ] && [ "$changed" = true ]; then + git add -A -- 'CLAUDE.md' 2>/dev/null || true + git add -A -- '*/CLAUDE.md' 2>/dev/null || true + git add -A -- '.cursor/rules' 2>/dev/null || true + git add -A -- '.claude/skills' 2>/dev/null || true +fi From 41ef94b811bd6fb2ac4802db2adb3b28daed949c Mon Sep 17 00:00:00 2001 From: Kyle Van Essen Date: Thu, 2 Apr 2026 15:35:28 -0400 Subject: [PATCH 03/13] =?UTF-8?q?Remove=20.cursor/rules=20sync=20=E2=80=94?= =?UTF-8?q?=20Cursor=20now=20reads=20nested=20AGENTS.md=20natively?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cursor added native support for nested AGENTS.md files in subdirectories, so the script no longer needs to generate .cursor/rules/*.mdc files. The sync-agents script now only handles CLAUDE.md and .claude/skills/. Made-with: Cursor --- .githooks/pre-commit | 2 +- AGENTS.md | 13 ++++++------- CLAUDE.md | 13 ++++++------- sync-agents | 29 +---------------------------- 4 files changed, 14 insertions(+), 43 deletions(-) diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 19bdfcf..ec5ea99 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -15,5 +15,5 @@ if [ -n "$staged" ]; then | xargs -0 git add fi -# Sync AGENTS.md + .agents/skills → CLAUDE.md, .cursor/rules, .claude/skills. +# Sync AGENTS.md + .agents/skills → CLAUDE.md + .claude/skills. ./sync-agents --git-add diff --git a/AGENTS.md b/AGENTS.md index 4f69607..27b5173 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -54,7 +54,7 @@ Broadway is a SwiftUI iOS + Mac Catalyst application managed by **Tuist**. The X │ └── TypeIdentifierTests.swift ├── Plans/ # Archived implementation plans (see index below) ├── swiftformat # Run SwiftFormat (--lint to check only) -├── sync-agents # Generate CLAUDE.md + .cursor/rules from AGENTS.md +├── sync-agents # Generate CLAUDE.md + .claude/skills from AGENTS.md ├── ide # Dev script (installs hooks, runs tuist generate) ├── LICENSE # Apache 2.0 ├── README.md # Project overview and setup instructions @@ -80,13 +80,12 @@ Broadway is a SwiftUI iOS + Mac Catalyst application managed by **Tuist**. The X ## Agent Instructions Sync -`AGENTS.md` is the source of truth for AI agent instructions. Cursor and Codex read it natively, but Claude Code uses `CLAUDE.md` and Cursor uses `.cursor/rules/*.mdc` for directory-scoped rules. +`AGENTS.md` is the source of truth for AI agent instructions. Cursor and Codex read nested `AGENTS.md` files natively. Claude Code uses `CLAUDE.md` for instructions and `.claude/skills/` for skills. -- Run `./sync-agents` to generate `CLAUDE.md` and `.cursor/rules/*.mdc` files from all `AGENTS.md` files in the repo. -- The root `AGENTS.md` produces only `CLAUDE.md` (Cursor reads `AGENTS.md` directly). -- Nested `AGENTS.md` files produce both a `CLAUDE.md` in the same directory and a glob-scoped `.mdc` rule under `.cursor/rules/`. -- Generated files contain a marker comment on the first line. The script uses this marker to clean up stale files on subsequent runs. -- Re-run `./sync-agents` after adding, editing, or removing any `AGENTS.md` file. +- Run `./sync-agents` to generate `CLAUDE.md` files from all `AGENTS.md` files and sync `.agents/skills/` to `.claude/skills/`. +- Generated `CLAUDE.md` files contain a marker comment on the first line. The script uses this marker to clean up stale files on subsequent runs. +- Skills live in `.agents/skills/` (read natively by Cursor and Codex) and are copied to `.claude/skills/` for Claude Code. +- The pre-commit hook runs `./sync-agents --git-add` automatically to keep generated files in sync. ## Targets diff --git a/CLAUDE.md b/CLAUDE.md index 8d35483..3a513af 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -56,7 +56,7 @@ Broadway is a SwiftUI iOS + Mac Catalyst application managed by **Tuist**. The X │ └── TypeIdentifierTests.swift ├── Plans/ # Archived implementation plans (see index below) ├── swiftformat # Run SwiftFormat (--lint to check only) -├── sync-agents # Generate CLAUDE.md + .cursor/rules from AGENTS.md +├── sync-agents # Generate CLAUDE.md + .claude/skills from AGENTS.md ├── ide # Dev script (installs hooks, runs tuist generate) ├── LICENSE # Apache 2.0 ├── README.md # Project overview and setup instructions @@ -82,13 +82,12 @@ Broadway is a SwiftUI iOS + Mac Catalyst application managed by **Tuist**. The X ## Agent Instructions Sync -`AGENTS.md` is the source of truth for AI agent instructions. Cursor and Codex read it natively, but Claude Code uses `CLAUDE.md` and Cursor uses `.cursor/rules/*.mdc` for directory-scoped rules. +`AGENTS.md` is the source of truth for AI agent instructions. Cursor and Codex read nested `AGENTS.md` files natively. Claude Code uses `CLAUDE.md` for instructions and `.claude/skills/` for skills. -- Run `./sync-agents` to generate `CLAUDE.md` and `.cursor/rules/*.mdc` files from all `AGENTS.md` files in the repo. -- The root `AGENTS.md` produces only `CLAUDE.md` (Cursor reads `AGENTS.md` directly). -- Nested `AGENTS.md` files produce both a `CLAUDE.md` in the same directory and a glob-scoped `.mdc` rule under `.cursor/rules/`. -- Generated files contain a marker comment on the first line. The script uses this marker to clean up stale files on subsequent runs. -- Re-run `./sync-agents` after adding, editing, or removing any `AGENTS.md` file. +- Run `./sync-agents` to generate `CLAUDE.md` files from all `AGENTS.md` files and sync `.agents/skills/` to `.claude/skills/`. +- Generated `CLAUDE.md` files contain a marker comment on the first line. The script uses this marker to clean up stale files on subsequent runs. +- Skills live in `.agents/skills/` (read natively by Cursor and Codex) and are copied to `.claude/skills/` for Claude Code. +- The pre-commit hook runs `./sync-agents --git-add` automatically to keep generated files in sync. ## Targets diff --git a/sync-agents b/sync-agents index 15342d9..1b2fafd 100755 --- a/sync-agents +++ b/sync-agents @@ -42,7 +42,7 @@ remove_if_exists() { fi } -# --- Generate from AGENTS.md files --- +# --- Generate CLAUDE.md from AGENTS.md files --- while IFS= read -r -d '' agents_file; do dir="$(dirname "$agents_file")" @@ -53,20 +53,6 @@ while IFS= read -r -d '' agents_file; do content="$(printf '%s\n\n%s' "$MARKER" "$(cat "$agents_file")")" write_if_changed "$claude_path" "$content" echo "$claude_path" >> "$expected_files" - - if [ -n "$rel_dir" ]; then - mdc_name="${rel_dir//\//-}" - mdc_path="$REPO_ROOT/.cursor/rules/${mdc_name}.mdc" - mdc_content="$(printf '%s\n%s\n%s\n%s\n%s\n\n%s' \ - "---" \ - "description: \"Generated from ${rel_dir}/AGENTS.md — do not edit directly\"" \ - "globs: [\"${rel_dir}/**\"]" \ - "alwaysApply: false" \ - "---" \ - "$(cat "$agents_file")")" - write_if_changed "$mdc_path" "$mdc_content" - echo "$mdc_path" >> "$expected_files" - fi done < <(find "$REPO_ROOT" -name "AGENTS.md" \ -not -path "*/Derived/*" \ -not -path "*/.build/*" \ @@ -89,18 +75,6 @@ done < <(find "$REPO_ROOT" -name "CLAUDE.md" \ -not -path "*/.git/*" \ -print0 2>/dev/null || true) -# --- Clean stale .mdc files --- - -if [ -d "$REPO_ROOT/.cursor/rules" ]; then - while IFS= read -r -d '' file; do - if ! grep -qxF "$file" "$expected_files"; then - if head -n5 "$file" | grep -qF "AGENTS.md — do not edit directly"; then - remove_if_exists "$file" - fi - fi - done < <(find "$REPO_ROOT/.cursor/rules" -name "*.mdc" -print0 2>/dev/null || true) -fi - # --- Sync .agents/skills → .claude/skills --- if [ -d "$REPO_ROOT/.agents/skills" ]; then @@ -122,6 +96,5 @@ fi if [ "$GIT_ADD" = true ] && [ "$changed" = true ]; then git add -A -- 'CLAUDE.md' 2>/dev/null || true git add -A -- '*/CLAUDE.md' 2>/dev/null || true - git add -A -- '.cursor/rules' 2>/dev/null || true git add -A -- '.claude/skills' 2>/dev/null || true fi From 2a35e428daf854919509ad8b11ba9284b2d0fc4b Mon Sep 17 00:00:00 2001 From: Kyle Van Essen Date: Thu, 2 Apr 2026 20:34:15 -0400 Subject: [PATCH 04/13] Address PR feedback: exact marker matching, targeted staging, Ruby convention - Use exact full-line match (grep -qxF) for the CLAUDE.md cleanup marker instead of substring matching, preventing accidental deletion of hand- written files that happen to contain the phrase. - Stage only files that actually changed instead of globbing all CLAUDE.md files in the repo, avoiding accidental commit contamination. - Add convention to AGENTS.md: shell scripts should stay short (~20 lines), use Ruby for longer scripts. Made-with: Cursor --- AGENTS.md | 1 + CLAUDE.md | 1 + sync-agents | 24 ++++++++++++------------ 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 27b5173..df842ee 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -114,6 +114,7 @@ All framework test targets use BroadwayTestHost and depend on BroadwayTesting. ## Key Conventions +- **Shell scripts** should be kept short (≤ ~20 lines). For anything longer, use **Ruby** for readability and portability. - **SwiftUI** is the UI framework. Catalog app views live under `BroadwayCatalog/Sources/`. - **BroadwayUI** is the reusable component library. All shared UI lives under `BroadwayUI/Sources/`. - **BroadwayCore** provides foundational utilities and shared logic. Source lives under `BroadwayCore/Sources/`. diff --git a/CLAUDE.md b/CLAUDE.md index 3a513af..de5f6f8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -116,6 +116,7 @@ All framework test targets use BroadwayTestHost and depend on BroadwayTesting. ## Key Conventions +- **Shell scripts** should be kept short (≤ ~20 lines). For anything longer, use **Ruby** for readability and portability. - **SwiftUI** is the UI framework. Catalog app views live under `BroadwayCatalog/Sources/`. - **BroadwayUI** is the reusable component library. All shared UI lives under `BroadwayUI/Sources/`. - **BroadwayCore** provides foundational utilities and shared logic. Source lives under `BroadwayCore/Sources/`. diff --git a/sync-agents b/sync-agents index 1b2fafd..d46db84 100755 --- a/sync-agents +++ b/sync-agents @@ -11,9 +11,9 @@ for arg in "$@"; do esac done -changed=false +changed_files="$(mktemp)" expected_files="$(mktemp)" -trap 'rm -f "$expected_files"' EXIT +trap 'rm -f "$changed_files" "$expected_files"' EXIT # --- Helper: write a file only when its content has changed --- @@ -28,17 +28,17 @@ write_if_changed() { mkdir -p "$(dirname "$target")" printf '%s\n' "$content" > "$target" echo "Updated $target" - changed=true + echo "$target" >> "$changed_files" } -# --- Helper: remove a file if it exists --- +# --- Helper: remove a file and track the change --- remove_if_exists() { local target="$1" if [ -f "$target" ]; then rm "$target" echo "Removed $target" - changed=true + echo "$target" >> "$changed_files" fi } @@ -65,7 +65,7 @@ done < <(find "$REPO_ROOT" -name "AGENTS.md" \ while IFS= read -r -d '' file; do if ! grep -qxF "$file" "$expected_files"; then - if head -n1 "$file" | grep -qF "Generated from AGENTS.md"; then + if head -n1 "$file" | grep -qxF "$MARKER"; then remove_if_exists "$file" fi fi @@ -83,18 +83,18 @@ if [ -d "$REPO_ROOT/.agents/skills" ]; then rm -rf "$REPO_ROOT/.claude/skills" cp -R "$REPO_ROOT/.agents/skills" "$REPO_ROOT/.claude/skills" echo "Synced .agents/skills/ → .claude/skills/" - changed=true + echo ".claude/skills" >> "$changed_files" fi elif [ -d "$REPO_ROOT/.claude/skills" ]; then rm -rf "$REPO_ROOT/.claude/skills" echo "Removed .claude/skills/" - changed=true + echo ".claude/skills" >> "$changed_files" fi # --- Stage generated files if --git-add was passed --- -if [ "$GIT_ADD" = true ] && [ "$changed" = true ]; then - git add -A -- 'CLAUDE.md' 2>/dev/null || true - git add -A -- '*/CLAUDE.md' 2>/dev/null || true - git add -A -- '.claude/skills' 2>/dev/null || true +if [ "$GIT_ADD" = true ] && [ -s "$changed_files" ]; then + while IFS= read -r file; do + git add -A -- "$file" 2>/dev/null || true + done < "$changed_files" fi From d58258a9e5b2a92d694ffea296d97c00ebacddc3 Mon Sep 17 00:00:00 2001 From: Kyle Van Essen Date: Thu, 2 Apr 2026 20:49:17 -0400 Subject: [PATCH 05/13] Simplify root AGENTS.md and README.md, add nested module instructions - Trim directory trees to show only directories and key files, remove individual source file listings that go stale as files are added. - Move module-specific context into nested AGENTS.md files for BroadwayCore, BroadwayUI, and BroadwayCatalog. - Replace tree-drawing characters with plain text across the codebase. Made-with: Cursor --- AGENTS.md | 135 ++++++++------------------------------ BroadwayCatalog/AGENTS.md | 13 ++++ BroadwayCatalog/CLAUDE.md | 15 +++++ BroadwayCore/AGENTS.md | 18 +++++ BroadwayCore/CLAUDE.md | 20 ++++++ BroadwayUI/AGENTS.md | 12 ++++ BroadwayUI/CLAUDE.md | 14 ++++ CLAUDE.md | 135 ++++++++------------------------------ README.md | 58 +++------------- 9 files changed, 154 insertions(+), 266 deletions(-) create mode 100644 BroadwayCatalog/AGENTS.md create mode 100644 BroadwayCatalog/CLAUDE.md create mode 100644 BroadwayCore/AGENTS.md create mode 100644 BroadwayCore/CLAUDE.md create mode 100644 BroadwayUI/AGENTS.md create mode 100644 BroadwayUI/CLAUDE.md diff --git a/AGENTS.md b/AGENTS.md index df842ee..199265e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,141 +1,60 @@ # AGENTS.md — Repository Shape -This document describes the structure and conventions of the Broadway repository for AI agents and contributors. - -## Overview - -Broadway is a SwiftUI iOS + Mac Catalyst application managed by **Tuist**. The Xcode project is not checked in — it is generated from the `Project.swift` manifest. +Broadway is a SwiftUI iOS + Mac Catalyst design system managed by **Tuist**. The Xcode project is not checked in — it is generated from `Project.swift`. ## Directory Layout ``` -/ -├── .githooks/pre-commit # Git pre-commit hook (SwiftFormat lint) -├── .mise.toml # mise tool versions (pins Tuist, SwiftFormat) -├── .swiftformat # SwiftFormat configuration -├── Tuist.swift # Tuist global configuration -├── Project.swift # Tuist project manifest (root level) -├── BroadwayCatalog/ -│ ├── Sources/ # Catalog app source code (Swift / SwiftUI) -│ │ ├── BroadwayApp.swift # @main entry point -│ │ └── ContentView.swift # Root view -│ ├── Resources/ # Bundled resources (assets, localization, etc.) -│ └── Tests/ # Catalog app unit tests (Swift Testing) -│ └── BroadwayCatalogTests.swift -├── BroadwayUI/ -│ ├── Sources/ # UI framework source code -│ │ └── BRootViewController.swift # Root container VC (context + trait propagation) -│ └── Tests/ # UI framework unit tests (Swift Testing) -│ └── BRootViewControllerTests.swift -├── BroadwayTestHost/ -│ └── Sources/ # Minimal app used as test host for unit tests -│ └── TestHostApp.swift # @main entry point (empty window) -├── BroadwayTesting/ -│ └── Sources/ # Test utilities framework (depends on BroadwayCore) -│ └── BroadwayTesting.swift # Module entry point -├── BroadwayCore/ -│ ├── Sources/ # Core framework source code -│ │ ├── AnyEquatable.swift # Type-erased Equatable wrapper -│ │ ├── BAccessibility.swift # Accessibility snapshot + Observer -│ │ ├── BContext.swift # Root environment container -│ │ ├── BContext+UITraits.swift # UITraitDefinition bridge (#if canImport(UIKit)) -│ │ ├── BStylesheets.swift # Lazy cached stylesheet resolver -│ │ ├── BThemes.swift # Type-keyed theme container -│ │ ├── BTraits.swift # Type-keyed trait container -│ │ ├── CopyOnWrite.swift # COW property wrapper -│ │ └── TypeIdentifier.swift # Lightweight type-keyed identifier -│ └── Tests/ # Core framework unit tests (Swift Testing) -│ ├── AnyEquatableTests.swift -│ ├── BAccessibilityTests.swift -│ ├── BContextTests.swift -│ ├── BThemesTests.swift -│ ├── BTraitsTests.swift -│ ├── CopyOnWriteTests.swift -│ └── TypeIdentifierTests.swift -├── Plans/ # Archived implementation plans (see index below) -├── swiftformat # Run SwiftFormat (--lint to check only) -├── sync-agents # Generate CLAUDE.md + .claude/skills from AGENTS.md -├── ide # Dev script (installs hooks, runs tuist generate) -├── LICENSE # Apache 2.0 -├── README.md # Project overview and setup instructions -└── AGENTS.md # This file +BroadwayCatalog/ # Catalog app (Sources/, Resources/, Tests/) +BroadwayUI/ # Reusable UI component framework (Sources/, Tests/) +BroadwayCore/ # Foundational utilities framework (Sources/, Tests/) +BroadwayTestHost/ # Minimal test host app (Sources/) +BroadwayTesting/ # Shared test utilities framework (Sources/) +Plans/ # Archived implementation plans +Project.swift # Tuist project manifest +Tuist.swift # Tuist global configuration +ide # Dev script (installs hooks, runs tuist generate) +swiftformat # Run SwiftFormat (--lint to check only) +sync-agents # Generate CLAUDE.md + .claude/skills from AGENTS.md ``` ## Build System -- **Tuist 4+** is used to generate the Xcode project from `Project.swift`. +- **Tuist 4+** generates the Xcode project from `Project.swift`. The `.xcodeproj` and `Derived/` are git-ignored. - Tuist and SwiftFormat are version-pinned via **mise** in `.mise.toml`. Run `mise install` to install them. -- Run `./ide` to generate the Xcode project (or `./ide -i` to run `mise exec -- tuist install` first). -- Run `mise exec -- tuist test` to execute all tests. -- Run `mise exec -- tuist test ` to test a specific target. The scheme name is the **framework name** (e.g., `BroadwayCore`), not the test target name (`BroadwayCoreTests`). -- The generated `.xcodeproj` and `Derived/` directory are git-ignored. +- Run `./ide` to generate the project (or `./ide -i` to run `mise exec -- tuist install` first). +- Run `mise exec -- tuist test` to execute all tests, or `mise exec -- tuist test ` for a specific target. ## Formatting -- **SwiftFormat** enforces consistent code style. Configuration lives in `.swiftformat`. -- Run `./swiftformat` to format all Swift files in-place. -- Run `./swiftformat --lint` to check without modifying (used in CI and pre-commit). -- The `./ide` script configures `core.hooksPath` to `.githooks/`, which installs a pre-commit hook that lints staged `.swift` files. -- CI runs `./swiftformat --lint` as a gate before build & test. +- **SwiftFormat** enforces code style via `.swiftformat`. Run `./swiftformat` to format, `./swiftformat --lint` to check. +- The pre-commit hook lints staged `.swift` files automatically. ## Agent Instructions Sync -`AGENTS.md` is the source of truth for AI agent instructions. Cursor and Codex read nested `AGENTS.md` files natively. Claude Code uses `CLAUDE.md` for instructions and `.claude/skills/` for skills. - -- Run `./sync-agents` to generate `CLAUDE.md` files from all `AGENTS.md` files and sync `.agents/skills/` to `.claude/skills/`. -- Generated `CLAUDE.md` files contain a marker comment on the first line. The script uses this marker to clean up stale files on subsequent runs. -- Skills live in `.agents/skills/` (read natively by Cursor and Codex) and are copied to `.claude/skills/` for Claude Code. -- The pre-commit hook runs `./sync-agents --git-add` automatically to keep generated files in sync. - -## Targets +`AGENTS.md` is the source of truth for AI agent instructions. Cursor and Codex read nested `AGENTS.md` natively; Claude Code uses `CLAUDE.md` and `.claude/skills/`. -| Target | Type | Bundle ID | Destinations | Min Deployment | -|---|---|---|---|---| -| `BroadwayCatalog` | `.app` | `com.broadway.catalog` | iPhone, iPad, Mac Catalyst | iOS 26.0 | -| `BroadwayCatalogTests` | `.unitTests` | `com.broadway.catalog.tests` | iPhone, iPad, Mac Catalyst | iOS 26.0 | -| `BroadwayUI` | `.framework` | `com.broadway.ui` | iPhone, iPad, Mac Catalyst | iOS 26.0 | -| `BroadwayUITests` | `.unitTests` | `com.broadway.ui.tests` | iPhone, iPad, Mac Catalyst | iOS 26.0 | -| `BroadwayCore` | `.framework` | `com.broadway.core` | iPhone, iPad, Mac Catalyst | iOS 26.0 | -| `BroadwayCoreTests` | `.unitTests` | `com.broadway.core.tests` | iPhone, iPad, Mac Catalyst | iOS 26.0 | -| `BroadwayTestHost` | `.app` | `com.broadway.testhost` | iPhone, iPad, Mac Catalyst | iOS 26.0 | -| `BroadwayTesting` | `.framework` | `com.broadway.testing` | iPhone, iPad, Mac Catalyst | iOS 26.0 | +- Run `./sync-agents` to generate `CLAUDE.md` files and sync `.agents/skills/` to `.claude/skills/`. +- The pre-commit hook runs `./sync-agents --git-add` automatically. -### Dependency Graph +## Dependency Graph ``` -BroadwayCatalog (app) ──▶ BroadwayUI (framework) ──▶ BroadwayCore (framework) - ▲ -BroadwayTestHost (app) ──▶ BroadwayUI ──────────────────────┤ - │ -BroadwayTesting (framework) ────────────────────────────────┘ +BroadwayCatalog (app) --> BroadwayUI (framework) --> BroadwayCore (framework) +BroadwayTestHost (app) --> BroadwayUI --> BroadwayCore +BroadwayTesting (framework) --> BroadwayCore All framework test targets use BroadwayTestHost and depend on BroadwayTesting. ``` ## Key Conventions -- **Shell scripts** should be kept short (≤ ~20 lines). For anything longer, use **Ruby** for readability and portability. -- **SwiftUI** is the UI framework. Catalog app views live under `BroadwayCatalog/Sources/`. -- **BroadwayUI** is the reusable component library. All shared UI lives under `BroadwayUI/Sources/`. -- **BroadwayCore** provides foundational utilities and shared logic. Source lives under `BroadwayCore/Sources/`. -- **BroadwayTestHost** is a minimal app that serves as the test host for framework unit tests. Source lives under `BroadwayTestHost/Sources/`. -- **BroadwayTesting** provides shared test utilities. All test targets depend on it. Source lives under `BroadwayTesting/Sources/`. +- **Shell scripts** should be kept short (≤ ~20 lines). For anything longer, use **Ruby**. - **Swift Testing** (`import Testing`) is used for unit tests, not XCTest. - Source files use `/Sources/**` globs; test files use `/Tests/**`. -- Resources (asset catalogs, localization files, etc.) go in `BroadwayCatalog/Resources/`. -- The catalog app uses `@main` via `BroadwayApp.swift` as the app entry point. - Info.plist is auto-generated by Tuist via `infoPlist: .extendingDefault(with:)`. +- New targets or dependencies: edit `Project.swift`. ## Plans -Implementation plans are stored in the `Plans/` directory. When you develop a plan using Cursor's plan mode, copy the final plan into `Plans/` and add an entry to the index below. - -**Naming convention**: `--.md`, where `` is the next sequential number (zero-padded to 3 digits), `` is the date the plan was created, and `` is a short snake_case description. For example: `002-2026-03-15-dark_mode_support.md`. - -## Adding New Files - -- **New catalog app files**: Add `.swift` files to `BroadwayCatalog/Sources/`. -- **New UI components**: Add `.swift` files to `BroadwayUI/Sources/`. Mark public API as `public`. -- **New test files**: Add `.swift` files to the appropriate `Tests/` directory. -- **New resources**: Add to `BroadwayCatalog/Resources/`. They are bundled via the `Resources/**` glob. -- **New targets or dependencies**: Edit `Project.swift` at the repository root. +Stored in `Plans/`. Naming: `--.md`. diff --git a/BroadwayCatalog/AGENTS.md b/BroadwayCatalog/AGENTS.md new file mode 100644 index 0000000..b46a724 --- /dev/null +++ b/BroadwayCatalog/AGENTS.md @@ -0,0 +1,13 @@ +# BroadwayCatalog + +The catalog app — a living showcase of BroadwayUI components. Depends on BroadwayUI. + +## Structure + +- `Sources/` — App views and logic. Entry point is `BroadwayApp.swift` (`@main`). +- `Resources/` — Asset catalogs, localization files, and other bundled resources. + +## Conventions + +- App-specific views go here, not in BroadwayUI. +- Resources are bundled via the `Resources/**` glob in `Project.swift`. diff --git a/BroadwayCatalog/CLAUDE.md b/BroadwayCatalog/CLAUDE.md new file mode 100644 index 0000000..25468a9 --- /dev/null +++ b/BroadwayCatalog/CLAUDE.md @@ -0,0 +1,15 @@ + + +# BroadwayCatalog + +The catalog app — a living showcase of BroadwayUI components. Depends on BroadwayUI. + +## Structure + +- `Sources/` — App views and logic. Entry point is `BroadwayApp.swift` (`@main`). +- `Resources/` — Asset catalogs, localization files, and other bundled resources. + +## Conventions + +- App-specific views go here, not in BroadwayUI. +- Resources are bundled via the `Resources/**` glob in `Project.swift`. diff --git a/BroadwayCore/AGENTS.md b/BroadwayCore/AGENTS.md new file mode 100644 index 0000000..0ff125c --- /dev/null +++ b/BroadwayCore/AGENTS.md @@ -0,0 +1,18 @@ +# BroadwayCore + +Foundational utilities and shared logic. All other frameworks depend on this module. + +## Key Types + +- **BContext** — Root environment container with type-keyed themes, traits, and stylesheets. +- **BThemes** / **BTraits** — Type-keyed containers for theme and trait values. +- **BStylesheets** — Lazy cached stylesheet resolver. +- **BAccessibility** — Accessibility snapshot and observer. +- **AnyEquatable** — Type-erased Equatable wrapper. +- **CopyOnWrite** — COW property wrapper. +- **TypeIdentifier** — Lightweight type-keyed identifier. + +## Conventions + +- Mark all public API as `public`. +- `BContext+UITraits.swift` bridges to `UITraitDefinition` via `#if canImport(UIKit)`. diff --git a/BroadwayCore/CLAUDE.md b/BroadwayCore/CLAUDE.md new file mode 100644 index 0000000..4a5275f --- /dev/null +++ b/BroadwayCore/CLAUDE.md @@ -0,0 +1,20 @@ + + +# BroadwayCore + +Foundational utilities and shared logic. All other frameworks depend on this module. + +## Key Types + +- **BContext** — Root environment container with type-keyed themes, traits, and stylesheets. +- **BThemes** / **BTraits** — Type-keyed containers for theme and trait values. +- **BStylesheets** — Lazy cached stylesheet resolver. +- **BAccessibility** — Accessibility snapshot and observer. +- **AnyEquatable** — Type-erased Equatable wrapper. +- **CopyOnWrite** — COW property wrapper. +- **TypeIdentifier** — Lightweight type-keyed identifier. + +## Conventions + +- Mark all public API as `public`. +- `BContext+UITraits.swift` bridges to `UITraitDefinition` via `#if canImport(UIKit)`. diff --git a/BroadwayUI/AGENTS.md b/BroadwayUI/AGENTS.md new file mode 100644 index 0000000..b604eb6 --- /dev/null +++ b/BroadwayUI/AGENTS.md @@ -0,0 +1,12 @@ +# BroadwayUI + +Reusable UI component library. Depends on BroadwayCore. + +## Key Types + +- **BRootViewController** — Root container view controller that propagates BContext and traits to its children. + +## Conventions + +- Mark all public API as `public`. +- UI components go in `Sources/`. This is the shared component library — app-specific views belong in BroadwayCatalog. diff --git a/BroadwayUI/CLAUDE.md b/BroadwayUI/CLAUDE.md new file mode 100644 index 0000000..1adb0ef --- /dev/null +++ b/BroadwayUI/CLAUDE.md @@ -0,0 +1,14 @@ + + +# BroadwayUI + +Reusable UI component library. Depends on BroadwayCore. + +## Key Types + +- **BRootViewController** — Root container view controller that propagates BContext and traits to its children. + +## Conventions + +- Mark all public API as `public`. +- UI components go in `Sources/`. This is the shared component library — app-specific views belong in BroadwayCatalog. diff --git a/CLAUDE.md b/CLAUDE.md index de5f6f8..6b38efd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,142 +2,61 @@ # AGENTS.md — Repository Shape -This document describes the structure and conventions of the Broadway repository for AI agents and contributors. - -## Overview - -Broadway is a SwiftUI iOS + Mac Catalyst application managed by **Tuist**. The Xcode project is not checked in — it is generated from the `Project.swift` manifest. +Broadway is a SwiftUI iOS + Mac Catalyst design system managed by **Tuist**. The Xcode project is not checked in — it is generated from `Project.swift`. ## Directory Layout ``` -/ -├── .githooks/pre-commit # Git pre-commit hook (SwiftFormat lint) -├── .mise.toml # mise tool versions (pins Tuist, SwiftFormat) -├── .swiftformat # SwiftFormat configuration -├── Tuist.swift # Tuist global configuration -├── Project.swift # Tuist project manifest (root level) -├── BroadwayCatalog/ -│ ├── Sources/ # Catalog app source code (Swift / SwiftUI) -│ │ ├── BroadwayApp.swift # @main entry point -│ │ └── ContentView.swift # Root view -│ ├── Resources/ # Bundled resources (assets, localization, etc.) -│ └── Tests/ # Catalog app unit tests (Swift Testing) -│ └── BroadwayCatalogTests.swift -├── BroadwayUI/ -│ ├── Sources/ # UI framework source code -│ │ └── BRootViewController.swift # Root container VC (context + trait propagation) -│ └── Tests/ # UI framework unit tests (Swift Testing) -│ └── BRootViewControllerTests.swift -├── BroadwayTestHost/ -│ └── Sources/ # Minimal app used as test host for unit tests -│ └── TestHostApp.swift # @main entry point (empty window) -├── BroadwayTesting/ -│ └── Sources/ # Test utilities framework (depends on BroadwayCore) -│ └── BroadwayTesting.swift # Module entry point -├── BroadwayCore/ -│ ├── Sources/ # Core framework source code -│ │ ├── AnyEquatable.swift # Type-erased Equatable wrapper -│ │ ├── BAccessibility.swift # Accessibility snapshot + Observer -│ │ ├── BContext.swift # Root environment container -│ │ ├── BContext+UITraits.swift # UITraitDefinition bridge (#if canImport(UIKit)) -│ │ ├── BStylesheets.swift # Lazy cached stylesheet resolver -│ │ ├── BThemes.swift # Type-keyed theme container -│ │ ├── BTraits.swift # Type-keyed trait container -│ │ ├── CopyOnWrite.swift # COW property wrapper -│ │ └── TypeIdentifier.swift # Lightweight type-keyed identifier -│ └── Tests/ # Core framework unit tests (Swift Testing) -│ ├── AnyEquatableTests.swift -│ ├── BAccessibilityTests.swift -│ ├── BContextTests.swift -│ ├── BThemesTests.swift -│ ├── BTraitsTests.swift -│ ├── CopyOnWriteTests.swift -│ └── TypeIdentifierTests.swift -├── Plans/ # Archived implementation plans (see index below) -├── swiftformat # Run SwiftFormat (--lint to check only) -├── sync-agents # Generate CLAUDE.md + .claude/skills from AGENTS.md -├── ide # Dev script (installs hooks, runs tuist generate) -├── LICENSE # Apache 2.0 -├── README.md # Project overview and setup instructions -└── AGENTS.md # This file +BroadwayCatalog/ # Catalog app (Sources/, Resources/, Tests/) +BroadwayUI/ # Reusable UI component framework (Sources/, Tests/) +BroadwayCore/ # Foundational utilities framework (Sources/, Tests/) +BroadwayTestHost/ # Minimal test host app (Sources/) +BroadwayTesting/ # Shared test utilities framework (Sources/) +Plans/ # Archived implementation plans +Project.swift # Tuist project manifest +Tuist.swift # Tuist global configuration +ide # Dev script (installs hooks, runs tuist generate) +swiftformat # Run SwiftFormat (--lint to check only) +sync-agents # Generate CLAUDE.md + .claude/skills from AGENTS.md ``` ## Build System -- **Tuist 4+** is used to generate the Xcode project from `Project.swift`. +- **Tuist 4+** generates the Xcode project from `Project.swift`. The `.xcodeproj` and `Derived/` are git-ignored. - Tuist and SwiftFormat are version-pinned via **mise** in `.mise.toml`. Run `mise install` to install them. -- Run `./ide` to generate the Xcode project (or `./ide -i` to run `mise exec -- tuist install` first). -- Run `mise exec -- tuist test` to execute all tests. -- Run `mise exec -- tuist test ` to test a specific target. The scheme name is the **framework name** (e.g., `BroadwayCore`), not the test target name (`BroadwayCoreTests`). -- The generated `.xcodeproj` and `Derived/` directory are git-ignored. +- Run `./ide` to generate the project (or `./ide -i` to run `mise exec -- tuist install` first). +- Run `mise exec -- tuist test` to execute all tests, or `mise exec -- tuist test ` for a specific target. ## Formatting -- **SwiftFormat** enforces consistent code style. Configuration lives in `.swiftformat`. -- Run `./swiftformat` to format all Swift files in-place. -- Run `./swiftformat --lint` to check without modifying (used in CI and pre-commit). -- The `./ide` script configures `core.hooksPath` to `.githooks/`, which installs a pre-commit hook that lints staged `.swift` files. -- CI runs `./swiftformat --lint` as a gate before build & test. +- **SwiftFormat** enforces code style via `.swiftformat`. Run `./swiftformat` to format, `./swiftformat --lint` to check. +- The pre-commit hook lints staged `.swift` files automatically. ## Agent Instructions Sync -`AGENTS.md` is the source of truth for AI agent instructions. Cursor and Codex read nested `AGENTS.md` files natively. Claude Code uses `CLAUDE.md` for instructions and `.claude/skills/` for skills. - -- Run `./sync-agents` to generate `CLAUDE.md` files from all `AGENTS.md` files and sync `.agents/skills/` to `.claude/skills/`. -- Generated `CLAUDE.md` files contain a marker comment on the first line. The script uses this marker to clean up stale files on subsequent runs. -- Skills live in `.agents/skills/` (read natively by Cursor and Codex) and are copied to `.claude/skills/` for Claude Code. -- The pre-commit hook runs `./sync-agents --git-add` automatically to keep generated files in sync. - -## Targets +`AGENTS.md` is the source of truth for AI agent instructions. Cursor and Codex read nested `AGENTS.md` natively; Claude Code uses `CLAUDE.md` and `.claude/skills/`. -| Target | Type | Bundle ID | Destinations | Min Deployment | -|---|---|---|---|---| -| `BroadwayCatalog` | `.app` | `com.broadway.catalog` | iPhone, iPad, Mac Catalyst | iOS 26.0 | -| `BroadwayCatalogTests` | `.unitTests` | `com.broadway.catalog.tests` | iPhone, iPad, Mac Catalyst | iOS 26.0 | -| `BroadwayUI` | `.framework` | `com.broadway.ui` | iPhone, iPad, Mac Catalyst | iOS 26.0 | -| `BroadwayUITests` | `.unitTests` | `com.broadway.ui.tests` | iPhone, iPad, Mac Catalyst | iOS 26.0 | -| `BroadwayCore` | `.framework` | `com.broadway.core` | iPhone, iPad, Mac Catalyst | iOS 26.0 | -| `BroadwayCoreTests` | `.unitTests` | `com.broadway.core.tests` | iPhone, iPad, Mac Catalyst | iOS 26.0 | -| `BroadwayTestHost` | `.app` | `com.broadway.testhost` | iPhone, iPad, Mac Catalyst | iOS 26.0 | -| `BroadwayTesting` | `.framework` | `com.broadway.testing` | iPhone, iPad, Mac Catalyst | iOS 26.0 | +- Run `./sync-agents` to generate `CLAUDE.md` files and sync `.agents/skills/` to `.claude/skills/`. +- The pre-commit hook runs `./sync-agents --git-add` automatically. -### Dependency Graph +## Dependency Graph ``` -BroadwayCatalog (app) ──▶ BroadwayUI (framework) ──▶ BroadwayCore (framework) - ▲ -BroadwayTestHost (app) ──▶ BroadwayUI ──────────────────────┤ - │ -BroadwayTesting (framework) ────────────────────────────────┘ +BroadwayCatalog (app) --> BroadwayUI (framework) --> BroadwayCore (framework) +BroadwayTestHost (app) --> BroadwayUI --> BroadwayCore +BroadwayTesting (framework) --> BroadwayCore All framework test targets use BroadwayTestHost and depend on BroadwayTesting. ``` ## Key Conventions -- **Shell scripts** should be kept short (≤ ~20 lines). For anything longer, use **Ruby** for readability and portability. -- **SwiftUI** is the UI framework. Catalog app views live under `BroadwayCatalog/Sources/`. -- **BroadwayUI** is the reusable component library. All shared UI lives under `BroadwayUI/Sources/`. -- **BroadwayCore** provides foundational utilities and shared logic. Source lives under `BroadwayCore/Sources/`. -- **BroadwayTestHost** is a minimal app that serves as the test host for framework unit tests. Source lives under `BroadwayTestHost/Sources/`. -- **BroadwayTesting** provides shared test utilities. All test targets depend on it. Source lives under `BroadwayTesting/Sources/`. +- **Shell scripts** should be kept short (≤ ~20 lines). For anything longer, use **Ruby**. - **Swift Testing** (`import Testing`) is used for unit tests, not XCTest. - Source files use `/Sources/**` globs; test files use `/Tests/**`. -- Resources (asset catalogs, localization files, etc.) go in `BroadwayCatalog/Resources/`. -- The catalog app uses `@main` via `BroadwayApp.swift` as the app entry point. - Info.plist is auto-generated by Tuist via `infoPlist: .extendingDefault(with:)`. +- New targets or dependencies: edit `Project.swift`. ## Plans -Implementation plans are stored in the `Plans/` directory. When you develop a plan using Cursor's plan mode, copy the final plan into `Plans/` and add an entry to the index below. - -**Naming convention**: `--.md`, where `` is the next sequential number (zero-padded to 3 digits), `` is the date the plan was created, and `` is a short snake_case description. For example: `002-2026-03-15-dark_mode_support.md`. - -## Adding New Files - -- **New catalog app files**: Add `.swift` files to `BroadwayCatalog/Sources/`. -- **New UI components**: Add `.swift` files to `BroadwayUI/Sources/`. Mark public API as `public`. -- **New test files**: Add `.swift` files to the appropriate `Tests/` directory. -- **New resources**: Add to `BroadwayCatalog/Resources/`. They are bundled via the `Resources/**` glob. -- **New targets or dependencies**: Edit `Project.swift` at the repository root. +Stored in `Plans/`. Naming: `--.md`. diff --git a/README.md b/README.md index 1c6d556..b3d2400 100644 --- a/README.md +++ b/README.md @@ -10,20 +10,6 @@ An open-source iOS and Mac Catalyst design system prototype. ## Getting Started -### Install mise - -```bash -curl https://mise.run | sh -``` - -Or via Homebrew: - -```bash -brew install mise -``` - -### Install Tools & Generate - ```bash mise install # Installs the pinned version of Tuist from .mise.toml ./ide # Generates the Xcode project @@ -41,44 +27,16 @@ Or open the generated project in Xcode and run tests with **Cmd+U**. ## Project Structure ``` -Broadway/ -├── .mise.toml # mise tool versions (pins Tuist) -├── Tuist.swift # Tuist configuration -├── Project.swift # Tuist project manifest -├── BroadwayCatalog/ -│ ├── Sources/ # Catalog app source files -│ │ ├── BroadwayApp.swift # @main app entry point -│ │ └── ContentView.swift # Root SwiftUI view -│ ├── Resources/ # Asset catalogs, etc. -│ └── Tests/ # Catalog app unit tests -│ └── BroadwayCatalogTests.swift -├── BroadwayUI/ -│ ├── Sources/ # UI framework source files -│ │ └── BroadwayUI.swift -│ └── Tests/ # UI framework unit tests -│ └── BroadwayUITests.swift -├── BroadwayCore/ -│ ├── Sources/ # Core framework source files -│ │ └── BroadwayCore.swift -│ └── Tests/ # Core framework unit tests -│ └── BroadwayCoreTests.swift -├── Plans/ # Archived implementation plans -├── ide # Dev script (generate project) -├── LICENSE # Apache 2.0 -└── README.md +BroadwayCatalog/ # Catalog app (Sources/, Resources/, Tests/) +BroadwayUI/ # Reusable UI component framework (Sources/, Tests/) +BroadwayCore/ # Foundational utilities framework (Sources/, Tests/) +BroadwayTestHost/ # Minimal test host app +BroadwayTesting/ # Shared test utilities framework +Plans/ # Archived implementation plans +Project.swift # Tuist project manifest +ide # Dev script (generate project) ``` -## Targets - -| Target | Product | Destinations | -|---|---|---| -| **BroadwayCatalog** | App | iOS, Mac Catalyst | -| **BroadwayCatalogTests** | Unit Tests | iOS, Mac Catalyst | -| **BroadwayUI** | Framework | iOS, Mac Catalyst | -| **BroadwayUITests** | Unit Tests | iOS, Mac Catalyst | -| **BroadwayCore** | Framework | iOS, Mac Catalyst | -| **BroadwayCoreTests** | Unit Tests | iOS, Mac Catalyst | - ## License This project is licensed under the Apache License 2.0. See [LICENSE](LICENSE) for details. From 5e38e38eff2df07dd5f2374d63a2fe396c51e1df Mon Sep 17 00:00:00 2001 From: Kyle Van Essen Date: Thu, 2 Apr 2026 20:55:10 -0400 Subject: [PATCH 06/13] Remove Plans directory and all references to it Made-with: Cursor --- AGENTS.md | 5 --- CLAUDE.md | 5 --- Plans/001-2026-02-27-github_ci_setup.md | 55 ------------------------- README.md | 1 - 4 files changed, 66 deletions(-) delete mode 100644 Plans/001-2026-02-27-github_ci_setup.md diff --git a/AGENTS.md b/AGENTS.md index 199265e..ff85a17 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,7 +10,6 @@ BroadwayUI/ # Reusable UI component framework (Sources/, Tests/) BroadwayCore/ # Foundational utilities framework (Sources/, Tests/) BroadwayTestHost/ # Minimal test host app (Sources/) BroadwayTesting/ # Shared test utilities framework (Sources/) -Plans/ # Archived implementation plans Project.swift # Tuist project manifest Tuist.swift # Tuist global configuration ide # Dev script (installs hooks, runs tuist generate) @@ -54,7 +53,3 @@ All framework test targets use BroadwayTestHost and depend on BroadwayTesting. - Source files use `/Sources/**` globs; test files use `/Tests/**`. - Info.plist is auto-generated by Tuist via `infoPlist: .extendingDefault(with:)`. - New targets or dependencies: edit `Project.swift`. - -## Plans - -Stored in `Plans/`. Naming: `--.md`. diff --git a/CLAUDE.md b/CLAUDE.md index 6b38efd..36d60fc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,7 +12,6 @@ BroadwayUI/ # Reusable UI component framework (Sources/, Tests/) BroadwayCore/ # Foundational utilities framework (Sources/, Tests/) BroadwayTestHost/ # Minimal test host app (Sources/) BroadwayTesting/ # Shared test utilities framework (Sources/) -Plans/ # Archived implementation plans Project.swift # Tuist project manifest Tuist.swift # Tuist global configuration ide # Dev script (installs hooks, runs tuist generate) @@ -56,7 +55,3 @@ All framework test targets use BroadwayTestHost and depend on BroadwayTesting. - Source files use `/Sources/**` globs; test files use `/Tests/**`. - Info.plist is auto-generated by Tuist via `infoPlist: .extendingDefault(with:)`. - New targets or dependencies: edit `Project.swift`. - -## Plans - -Stored in `Plans/`. Naming: `--.md`. diff --git a/Plans/001-2026-02-27-github_ci_setup.md b/Plans/001-2026-02-27-github_ci_setup.md deleted file mode 100644 index 9402ea6..0000000 --- a/Plans/001-2026-02-27-github_ci_setup.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -name: GitHub CI Setup -overview: Add a GitHub Actions workflow that builds and tests all targets on pull requests and pushes to main, using mise to install the pinned Tuist version on a macOS 26 runner. -todos: - - id: create-workflow - content: Create .github/workflows/ci.yml with the build + test workflow - status: completed -isProject: false ---- - -# GitHub CI Setup - -## Approach - -Create a single workflow file at `.github/workflows/ci.yml` that: - -1. Triggers on **pull requests** and **pushes to main** -2. Runs on `macos-26` (ARM, GA as of Feb 26 2026 -- includes Xcode 26.x with the iOS 26 SDK) -3. Uses `jdx/mise-action@v3` to install Tuist at the version pinned in `[.mise.toml](.mise.toml)` (currently 4.40.0) -4. Runs `tuist test` to build and execute all test targets (`BroadwayCatalogTests` + `BroadwayUITests`) - -## Workflow file - -`.github/workflows/ci.yml` -- roughly: - -```yaml -name: CI - -on: - push: - branches: [main] - pull_request: - branches: [main] - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - test: - name: Build & Test - runs-on: macos-26 - steps: - - uses: actions/checkout@v4 - - uses: jdx/mise-action@v3 - - run: tuist test -``` - -Key details: - -- `**concurrency**` cancels redundant in-flight runs for the same branch (saves CI minutes) -- `**jdx/mise-action@v3**` reads `.mise.toml`, installs + activates Tuist 4.40.0, and caches it across runs -- `**tuist test**` generates the project and runs all unit test targets in one command -- No need for `tuist generate` separately since `tuist test` handles it - diff --git a/README.md b/README.md index b3d2400..64f0747 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,6 @@ BroadwayUI/ # Reusable UI component framework (Sources/, Tests/) BroadwayCore/ # Foundational utilities framework (Sources/, Tests/) BroadwayTestHost/ # Minimal test host app BroadwayTesting/ # Shared test utilities framework -Plans/ # Archived implementation plans Project.swift # Tuist project manifest ide # Dev script (generate project) ``` From 4c6cb12d305de642c4186e4de325b27bf2205821 Mon Sep 17 00:00:00 2001 From: Kyle Van Essen Date: Sat, 4 Apr 2026 14:24:25 -0400 Subject: [PATCH 07/13] Rewrite sync-agents in Ruby, add --install and --update for external skills Rewrite the bash script in Ruby for readability and portability (per the project convention for scripts > 20 lines). All existing behavior is preserved: incremental sync, marker-based cleanup, and --git-add. New commands: - --install [name]: clone a skill from GitHub into .agents/skills/ and record it in .agents/external-skills.json - --update: re-fetch all external skills from their pinned refs Install the SwiftUI Pro skill from twostraws/swiftui-agent-skill as the first external skill. Made-with: Cursor --- .agents/external-skills.json | 7 + .agents/skills/swiftui-pro/SKILL.md | 108 ++++++ .agents/skills/swiftui-pro/agents/openai.yaml | 10 + .../swiftui-pro/assets/swiftui-pro-icon.png | Bin 0 -> 3117 bytes .../swiftui-pro/assets/swiftui-pro-icon.svg | 29 ++ .../swiftui-pro/references/accessibility.md | 13 + .agents/skills/swiftui-pro/references/api.md | 39 +++ .agents/skills/swiftui-pro/references/data.md | 43 +++ .../skills/swiftui-pro/references/design.md | 31 ++ .../skills/swiftui-pro/references/hygiene.md | 9 + .../swiftui-pro/references/navigation.md | 14 + .../swiftui-pro/references/performance.md | 46 +++ .../skills/swiftui-pro/references/swift.md | 56 +++ .../skills/swiftui-pro/references/views.md | 35 ++ .claude/skills/swiftui-pro/SKILL.md | 108 ++++++ .claude/skills/swiftui-pro/agents/openai.yaml | 10 + .../swiftui-pro/assets/swiftui-pro-icon.png | Bin 0 -> 3117 bytes .../swiftui-pro/assets/swiftui-pro-icon.svg | 29 ++ .../swiftui-pro/references/accessibility.md | 13 + .claude/skills/swiftui-pro/references/api.md | 39 +++ .claude/skills/swiftui-pro/references/data.md | 43 +++ .../skills/swiftui-pro/references/design.md | 31 ++ .../skills/swiftui-pro/references/hygiene.md | 9 + .../swiftui-pro/references/navigation.md | 14 + .../swiftui-pro/references/performance.md | 46 +++ .../skills/swiftui-pro/references/swift.md | 56 +++ .../skills/swiftui-pro/references/views.md | 35 ++ sync-agents | 319 ++++++++++++------ 28 files changed, 1092 insertions(+), 100 deletions(-) create mode 100644 .agents/external-skills.json create mode 100644 .agents/skills/swiftui-pro/SKILL.md create mode 100644 .agents/skills/swiftui-pro/agents/openai.yaml create mode 100644 .agents/skills/swiftui-pro/assets/swiftui-pro-icon.png create mode 100644 .agents/skills/swiftui-pro/assets/swiftui-pro-icon.svg create mode 100644 .agents/skills/swiftui-pro/references/accessibility.md create mode 100644 .agents/skills/swiftui-pro/references/api.md create mode 100644 .agents/skills/swiftui-pro/references/data.md create mode 100644 .agents/skills/swiftui-pro/references/design.md create mode 100644 .agents/skills/swiftui-pro/references/hygiene.md create mode 100644 .agents/skills/swiftui-pro/references/navigation.md create mode 100644 .agents/skills/swiftui-pro/references/performance.md create mode 100644 .agents/skills/swiftui-pro/references/swift.md create mode 100644 .agents/skills/swiftui-pro/references/views.md create mode 100644 .claude/skills/swiftui-pro/SKILL.md create mode 100644 .claude/skills/swiftui-pro/agents/openai.yaml create mode 100644 .claude/skills/swiftui-pro/assets/swiftui-pro-icon.png create mode 100644 .claude/skills/swiftui-pro/assets/swiftui-pro-icon.svg create mode 100644 .claude/skills/swiftui-pro/references/accessibility.md create mode 100644 .claude/skills/swiftui-pro/references/api.md create mode 100644 .claude/skills/swiftui-pro/references/data.md create mode 100644 .claude/skills/swiftui-pro/references/design.md create mode 100644 .claude/skills/swiftui-pro/references/hygiene.md create mode 100644 .claude/skills/swiftui-pro/references/navigation.md create mode 100644 .claude/skills/swiftui-pro/references/performance.md create mode 100644 .claude/skills/swiftui-pro/references/swift.md create mode 100644 .claude/skills/swiftui-pro/references/views.md diff --git a/.agents/external-skills.json b/.agents/external-skills.json new file mode 100644 index 0000000..f2649e8 --- /dev/null +++ b/.agents/external-skills.json @@ -0,0 +1,7 @@ +{ + "swiftui-pro": { + "repo": "twostraws/swiftui-agent-skill", + "path": "swiftui-pro", + "ref": "main" + } +} diff --git a/.agents/skills/swiftui-pro/SKILL.md b/.agents/skills/swiftui-pro/SKILL.md new file mode 100644 index 0000000..e8e699b --- /dev/null +++ b/.agents/skills/swiftui-pro/SKILL.md @@ -0,0 +1,108 @@ +--- +name: swiftui-pro +description: Comprehensively reviews SwiftUI code for best practices on modern APIs, maintainability, and performance. Use when reading, writing, or reviewing SwiftUI projects. +license: MIT +metadata: + author: Paul Hudson + version: "1.0" +--- + +Review Swift and SwiftUI code for correctness, modern API usage, and adherence to project conventions. Report only genuine problems - do not nitpick or invent issues. + +Review process: + +1. Check for deprecated API using `references/api.md`. +1. Check that views, modifiers, and animations have been written optimally using `references/views.md`. +1. Validate that data flow is configured correctly using `references/data.md`. +1. Ensure navigation is updated and performant using `references/navigation.md`. +1. Ensure the code uses designs that are accessible and compliant with Apple’s Human Interface Guidelines using `references/design.md`. +1. Validate accessibility compliance including Dynamic Type, VoiceOver, and Reduce Motion using `references/accessibility.md`. +1. Ensure the code is able to run efficiently using `references/performance.md`. +1. Quick validation of Swift code using `references/swift.md`. +1. Final code hygiene check using `references/hygiene.md`. + +If doing a partial review, load only the relevant reference files. + + +## Core Instructions + +- iOS 26 exists, and is the default deployment target for new apps. +- Target Swift 6.2 or later, using modern Swift concurrency. +- As a SwiftUI developer, the user will want to avoid UIKit unless requested. +- Do not introduce third-party frameworks without asking first. +- Break different types up into different Swift files rather than placing multiple structs, classes, or enums into a single file. +- Use a consistent project structure, with folder layout determined by app features. + + +## Output Format + +Organize findings by file. For each issue: + +1. State the file and relevant line(s). +2. Name the rule being violated (e.g., "Use `foregroundStyle()` instead of `foregroundColor()`"). +3. Show a brief before/after code fix. + +Skip files with no issues. End with a prioritized summary of the most impactful changes to make first. + +Example output: + +### ContentView.swift + +**Line 12: Use `foregroundStyle()` instead of `foregroundColor()`.** + +```swift +// Before +Text("Hello").foregroundColor(.red) + +// After +Text("Hello").foregroundStyle(.red) +``` + +**Line 24: Icon-only button is bad for VoiceOver - add a text label.** + +```swift +// Before +Button(action: addUser) { + Image(systemName: "plus") +} + +// After +Button("Add User", systemImage: "plus", action: addUser) +``` + +**Line 31: Avoid `Binding(get:set:)` in view body - use `@State` with `onChange()` instead.** + +```swift +// Before +TextField("Username", text: Binding( + get: { model.username }, + set: { model.username = $0; model.save() } +)) + +// After +TextField("Username", text: $model.username) + .onChange(of: model.username) { + model.save() + } +``` + +### Summary + +1. **Accessibility (high):** The add button on line 24 is invisible to VoiceOver. +2. **Deprecated API (medium):** `foregroundColor()` on line 12 should be `foregroundStyle()`. +3. **Data flow (medium):** The manual binding on line 31 is fragile and harder to maintain. + +End of example. + + +## References + +- `references/accessibility.md` - Dynamic Type, VoiceOver, Reduce Motion, and other accessibility requirements. +- `references/api.md` - updating code for modern API, and the deprecated code it replaces. +- `references/design.md` - guidance for building accessible apps that meet Apple’s Human Interface Guidelines. +- `references/hygiene.md` - making code compile cleanly and be maintainable in the long term. +- `references/navigation.md` - navigation using `NavigationStack`/`NavigationSplitView`, plus alerts, confirmation dialogs, and sheets. +- `references/performance.md` - optimizing SwiftUI code for maximum performance. +- `references/data.md` - data flow, shared state, and property wrappers. +- `references/swift.md` - tips on writing modern Swift code, including using Swift Concurrency effectively. +- `references/views.md` - view structure, composition, and animation. diff --git a/.agents/skills/swiftui-pro/agents/openai.yaml b/.agents/skills/swiftui-pro/agents/openai.yaml new file mode 100644 index 0000000..bace09e --- /dev/null +++ b/.agents/skills/swiftui-pro/agents/openai.yaml @@ -0,0 +1,10 @@ +interface: + display_name: "SwiftUI Pro" + short_description: "Reviews SwiftUI code for modern best practices." + icon_small: "./assets/swiftui-pro-icon.svg" + icon_large: "./assets/swiftui-pro-icon.png" + brand_color: "#006AFD" + default_prompt: "Use $swiftui-pro to review my project." + +policy: + allow_implicit_invocation: true \ No newline at end of file diff --git a/.agents/skills/swiftui-pro/assets/swiftui-pro-icon.png b/.agents/skills/swiftui-pro/assets/swiftui-pro-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..11db253c2b4093ae3bc2b96d530a0767f40ac33f GIT binary patch literal 3117 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 + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.agents/skills/swiftui-pro/references/accessibility.md b/.agents/skills/swiftui-pro/references/accessibility.md new file mode 100644 index 0000000..7cb96c1 --- /dev/null +++ b/.agents/skills/swiftui-pro/references/accessibility.md @@ -0,0 +1,13 @@ +# Accessibility + +- Respect the user’s accessibility settings for fonts, colors, animations, and more. +- Do not force specific font sizes. Prefer Dynamic Type (`.font(.body)`, `.font(.headline)`, etc.). +- If you *need* a custom font size, use `@ScaledMetric` when targeting iOS 18 and earlier. When targeting iOS 26 or later, `.font(.body.scaled(by:))` is also available to get font size adjustment. +- Flag instances where images have unclear or unhelpful VoiceOver readings, e.g. `Image(.newBanner2026)`. If they are decorative, suggest using `Image(decorative:)` or `accessibilityHidden()`, otherwise attach an `accessibilityLabel()`. +- If the user has “Reduce Motion” enabled, replace large, motion-based animations with opacity instead. +- If buttons have complex or frequently changing labels, recommend using `accessibilityInputLabels()` to provide better Voice Control commands. For example, if a button had a live-updating share price for Apple such as “AAPL $271.68”, adding an input label for “Apple” would be a big improvement. +- Buttons with image labels must always include text, even if the text is invisible: `Button("Label", systemImage: "plus", action: myAction)`. Flag icon-only buttons that lack a text label as being bad for VoiceOver. +- If color is an important differentiator in the user interface, make sure to respect the environment’s `.accessibilityDifferentiateWithoutColor` setting by showing some kind of variation beyond just color – icons, patterns, strokes, etc. +- The same is true of `Menu`: using `Menu("Options", systemImage: "ellipsis.circle") { }` is much better than just using an image. +- Never use `onTapGesture()` unless you specifically need tap location or tap count. All other tappable elements should be a `Button`. +- If `onTapGesture()` must be used, make sure to add `.accessibilityAddTraits(.isButton)` or similar so it can be read by VoiceOver correctly. diff --git a/.agents/skills/swiftui-pro/references/api.md b/.agents/skills/swiftui-pro/references/api.md new file mode 100644 index 0000000..bfd5199 --- /dev/null +++ b/.agents/skills/swiftui-pro/references/api.md @@ -0,0 +1,39 @@ +# Using modern SwiftUI API + +- Always use `foregroundStyle()` instead of `foregroundColor()`. +- Always use `clipShape(.rect(cornerRadius:))` instead of `cornerRadius()`. +- Always use the `Tab` API instead of `tabItem()`. +- Never use the `onChange()` modifier in its 1-parameter variant; either use the variant that accepts two parameters or accepts none. +- Do not use `GeometryReader` if a newer alternative works: `containerRelativeFrame()`, `visualEffect()`, or the `Layout` protocol. Flag `GeometryReader` usage and suggest the modern alternative. +- When designing haptic effects, prefer using `sensoryFeedback()` over older UIKit APIs such as `UIImpactFeedbackGenerator`. +- Use the `@Entry` macro to define custom `EnvironmentValues`, `FocusValues`, `Transaction`, and `ContainerValues` keys. This replaces the legacy pattern of manually creating a type conforming to (for example) `EnvironmentKey` with a `defaultValue`, then extending `EnvironmentValues` with a computed property. +- Strongly prefer `overlay(alignment:content:)` over the deprecated `overlay(_:alignment:)`. For example, use `.overlay { Text("Hello, world!") }` rather than `.overlay(Text("Hello, world!"))`. +- Never use `.navigationBarLeading` and `.navigationBarTrailing` for toolbar item placement; they are deprecated. The correct, modern placements are `.topBarLeading` and `.topBarTrailing`. +- Prefer to rely on automatic grammar agreement when dealing with English, French, German, Portuguese, Spanish, and Italian. For example, use `Text("^[\(people) person](inflect: true)")` to show a number of people. +- You can fill and stroke a shape with two chained modifiers; you do *not* need an overlay for the stroke. The overlay was required previously, but this is fixed in iOS 17 and later. +- When referencing images from an asset catalog, prefer the generated symbol asset API when the project is configured to use them: `Image(.avatar)` rather than `Image("avatar")`. +- When targeting iOS 26 and later, SwiftUI has a native `WebView` view type that replaces almost all uses of hand-wrapped `WKWebView` inside `UIViewRepresentable`. To use it, make sure to include `import WebKit`. +- `ForEach` over an `enumerated()` sequence should not convert to an array first. Use `ForEach(items.enumerated(), id: \.element.id)` directly. +- When hiding scroll indicators, use `.scrollIndicators(.hidden)` rather than `showsIndicators: false` in the initializer. +- Never use `Text` concatenation with `+`. + +For example, the usage of `+` here is bad and deprecated: + +```swift +Text("Hello").foregroundStyle(.red) ++ +Text("World").foregroundStyle(.blue) +``` + +Instead, use text interpolation like this: + +```swift +let red = Text("Hello").foregroundStyle(.red) +let blue = Text("World").foregroundStyle(.blue) +Text("\(red)\(blue)") +``` + + +## Using ObservableObject + +If using `ObservableObject` is absolutely required – for example if you are trying to create a debouncer using a Combine publisher – you should always make sure `import Combine` is added. This was previously provided through SwiftUI, but that is no longer the case. diff --git a/.agents/skills/swiftui-pro/references/data.md b/.agents/skills/swiftui-pro/references/data.md new file mode 100644 index 0000000..952571f --- /dev/null +++ b/.agents/skills/swiftui-pro/references/data.md @@ -0,0 +1,43 @@ +# Data flow, shared state, and property wrappers + +It is important that SwiftUI body code and logic code be kept separate in order to make code easier to read, write, and maintain. That usually means placing code into methods rather than inline in the `body` property, but often also means carving functionality out into separate `@Observable` classes. + +These rules help ensure code is efficient and works well in the long term. + + +## Shared state + +- `@Observable` classes must be marked `@MainActor` unless the project has Main Actor default actor isolation. Flag any `@Observable` class missing this annotation. +- All shared data should use `@Observable` classes with `@State` (for ownership) and `@Bindable` / `@Environment` (for passing). +- Strongly prefer not to use `ObservableObject`, `@Published`, `@StateObject`, `@ObservedObject`, or `@EnvironmentObject` unless they are unavoidable, or if they exist in legacy/integration contexts when changing architecture would be complicated. + + +## Local state + +- `@State` should be marked `private` and only owned by the view that created it. +- If a view stores a class instance that contains expensive-to-recompute data, e.g. `CIContext`, it can be stored using `@State` even though it is not an observable object. This effectively uses `@State` as a cache – storing something persistently, but not doing any change tracking on it since it's not an observable object. + + +## Bindings + +- Strongly prefer to avoid creating bindings using `Binding(get:set:)` in view body code. It is much cleaner and simpler to use a binding provided by `@State`, `@Binding` or similar, then use `onChange()` to trigger any effects. +- If the user needs to enter a number into a `TextField`, bind the `TextField` to a numeric value such as `Int` or `Double`, then use its `format` initializer like this: `TextField("Enter your score", value: $score, format: .number)`. Apply either `.keyboardType(.numberPad)` (for integers) or `.keyboardType(.decimalPad)` (for floating-point numbers) as appropriate. Using the modifier alone is *not* sufficient. + + +## Working with data + +- Prefer to make structs conform to `Identifiable` rather than using `id: \.someProperty` in SwiftUI code. +- Never attempt to use `@AppStorage` inside an `@Observable` class, even if marked `@ObservationIgnored` – it will *not* trigger view updates when a change happens. + + +## SwiftData + +- If you only need the number of items matching a query, consider `ModelContext.fetchCount()` with a fetch descriptor. This will *not* live update if the data changes unless something else triggers the update, such as `@Query`, so it should be used carefully. + +For more help with SwiftData, suggest the [SwiftData Pro agent skill](https://github.com/twostraws/swiftdata-agent-skill). + +## If the project uses SwiftData with CloudKit + +- Never use `@Attribute(.unique)`. +- Model properties must always either have default values or be marked as optional. +- All relationships must be marked optional. diff --git a/.agents/skills/swiftui-pro/references/design.md b/.agents/skills/swiftui-pro/references/design.md new file mode 100644 index 0000000..8e14b6e --- /dev/null +++ b/.agents/skills/swiftui-pro/references/design.md @@ -0,0 +1,31 @@ +# Design + +## Creating a uniform design in this app + +Prefer to place standard fonts, sizes, colors, stack spacing, padding, rounding, animation timings, and more into a shared enum of constants, so they can be used by all views. This allows the app’s design to feel uniform and consistent, and be adjusted easily. + + +## Requirements for flexible, accessible design + +- Never use `UIScreen.main.bounds` to read available space; prefer alternatives such as `containerRelativeFrame()`, or `visualEffect()` as appropriate, or (if there is no alternative) `GeometryReader`. +- Prefer to avoid fixed frames for views unless content can fit neatly inside; this can cause problems across different device sizes, different Dynamic Type settings, and more. Giving frames some flexibility is usually preferred. +- Apple’s minimum acceptable tap area for interactions on iOS is 44x44. Ensure this is strictly enforced. + + +## Standard system styling + +- Strongly prefer to use `ContentUnavailableView` when data is missing or empty, rather than designing something custom. +- When using `searchable()`, you can show empty results using `ContentUnavailableView.search` and it will include the search term they used automatically – there’s no need to use `ContentUnavailableView.search(text: searchText)` or similar. +- If you need an icon and some text placed horizontally side by side, prefer `Label` over `HStack`. +- Prefer system hierarchical styles (e.g. secondary/tertiary) over manual opacity when possible, so the system can adapt to the correct context automatically. +- When using `Form`, wrap controls such as `Slider` in `LabeledContent` so the title and control are laid out correctly. +- When using `RoundedRectangle`, the default rounding style is `.continuous` – there is no need to specify it explicitly. + + +## Ensuring designs work for everyone + +- Use `bold()` instead of `fontWeight(.bold)`, because using `bold()` allows the system to choose the correct weight for the current context. +- Only use `fontWeight()` for weights other than bold when there's an important reason - scattering around `fontWeight(.medium)` or `fontWeight(.semibold)` is counterproductive. +- Avoid hard-coded values for padding and stack spacing unless specifically requested. +- Avoid UIKit colors (`UIColor`) in SwiftUI code; use SwiftUI `Color` or asset catalog colors. +- The font size `.caption2` is extremely small, and is generally best avoided. Even the font size `.caption` is on the small side, and should be used carefully. diff --git a/.agents/skills/swiftui-pro/references/hygiene.md b/.agents/skills/swiftui-pro/references/hygiene.md new file mode 100644 index 0000000..80bc9c5 --- /dev/null +++ b/.agents/skills/swiftui-pro/references/hygiene.md @@ -0,0 +1,9 @@ +# Hygiene + +- If the project requires secrets such as API keys, never include them in the repository. +- Code comments and documentation comments should be present where the logic isn't self-evident. +- Unit tests should exist for core application logic. UI tests only where unit tests are not possible. +- `@AppStorage` must never be used to store usernames, passwords, or other sensitive data. Use the keychain for that. +- If SwiftLint is configured, it should return no warnings or errors. +- If the project uses Localizable.xcstrings, prefer to add user-facing strings using symbol keys (e.g. “helloWorld”) in the string catalog with `extractionState` set to "manual", accessing them via generated symbols such as `Text(.helloWorld)`. Offer to translate new keys into all languages supported by the project. +- If the Xcode MCP is configured, prefer its tools over generic alternatives. For example, `RenderPreview` is able to capture images of rendered SwiftUI previews for examination, and `DocumentationSearch` can search Apple’s documentation for latest usage instructions. diff --git a/.agents/skills/swiftui-pro/references/navigation.md b/.agents/skills/swiftui-pro/references/navigation.md new file mode 100644 index 0000000..44b4025 --- /dev/null +++ b/.agents/skills/swiftui-pro/references/navigation.md @@ -0,0 +1,14 @@ +# Navigation and presentation + +- Use `NavigationStack` or `NavigationSplitView` as appropriate; flag all use of the deprecated `NavigationView`. +- Strongly prefer to use `navigationDestination(for:)` to specify destinations; flag all use of the old `NavigationLink(destination:)` pattern where it should be replaced. +- Never mix `navigationDestination(for:)` and `NavigationLink(destination:)` in the same navigation hierarchy; it causes significant problems. +- `navigationDestination(for:)` must be registered once per data type; flag duplicates. + + +## Alerts, confirmation dialogs, and sheets + +- Always attach `confirmationDialog()` to the user interface that triggers the dialog. This allows Liquid Glass animations to move from the correct source. +- If an alert has only a single “OK” button that does nothing but dismiss the alert, it can be omitted entirely: `.alert("Dismiss Me", isPresented: $isShowingAlert) { }`. +- If a sheet is designed to present an optional piece of data, prefer `sheet(item:)` over `sheet(isPresented:)` so the optional is safely unwrapped. +- When using `sheet(item:)` with a view that accepts the item as its only initializer parameter, prefer `sheet(item: $someItem, content: SomeView.init)` over `sheet(item: $someItem) { someItem in SomeView(item: someItem) }`. diff --git a/.agents/skills/swiftui-pro/references/performance.md b/.agents/skills/swiftui-pro/references/performance.md new file mode 100644 index 0000000..72c5a03 --- /dev/null +++ b/.agents/skills/swiftui-pro/references/performance.md @@ -0,0 +1,46 @@ +# Performance + +- When toggling modifier values, prefer ternary expressions over if/else view branching to avoid `_ConditionalContent`, preserve structural identity, and avoid repeatedly recreating underlying platform views. +- Avoid `AnyView` unless absolutely required. Use `@ViewBuilder`, `Group`, or generics instead. +- If a `ScrollView` has an opaque, static, and solid background, prefer to use `scrollContentBackground(.visible)` to improve scroll-edge rendering efficiency. +- It is more efficient to break views up by making dedicated SwiftUI views rather than place them into computed properties or methods. Using `@ViewBuilder` on a property or method does not solve this; breaking views up is strongly preferred. +- Always ensure view initializers are kept as small and simple as possible, avoiding any non-trivial work. Flag any work that can be moved into a `task()` modifier to be run when the view is shown. +- Similarly, assume each view’s `body` property is called frequently – if logic such as sorting or filtering can be moved out of there easily, it should be. +- Avoid creating properties to store formatters such as `DateFormatter` unless they are required. A more natural approach is to use `Text` with a format, like this: `Text(Date.now, format: .dateTime.day().month().year())` or `Text(100, format: .currency(code: "USD"))`. +- Avoid expensive inline transforms in `List`/`ForEach` initializers (e.g. `items.filter { ... }`) when they are repeated often. +- Prefer deriving transformed data from the source-of-truth using `let`, or caching in `@State`. However, do not cache derived collections in `@State` unless you also own explicit invalidation logic to avoid stale UI. +- For large data sets in `ScrollView`, use `LazyVStack`/`LazyHStack`; flag eager stacks with many children. +- Prefer using `task()` over `onAppear()` when doing async work, because it will be cancelled automatically when the view disappears. +- Avoid storing escaping `@ViewBuilder` closures on views when possible; store built view results instead. + +Example: + +```swift +// Anti-pattern: stores an escaping closure on the view. +struct CardView: View { + let content: () -> Content + + var body: some View { + VStack(alignment: .leading) { + content() + } + .padding() + .background(.ultraThinMaterial) + .clipShape(.rect(cornerRadius: 8)) + } +} + +// Preferred: store the built view value; the synthesized init handles calling the builder. +struct CardView: View { + @ViewBuilder let content: Content + + var body: some View { + VStack(alignment: .leading) { + content + } + .padding() + .background(.ultraThinMaterial) + .clipShape(.rect(cornerRadius: 8)) + } +} +``` diff --git a/.agents/skills/swiftui-pro/references/swift.md b/.agents/skills/swiftui-pro/references/swift.md new file mode 100644 index 0000000..92f32b1 --- /dev/null +++ b/.agents/skills/swiftui-pro/references/swift.md @@ -0,0 +1,56 @@ +# Swift + +- Prefer Swift-native string methods over Foundation equivalents: use `replacing("a", with: "b")` not `replacingOccurrences(of: "a", with: "b")`. +- Prefer modern Foundation API: `URL.documentsDirectory` instead of `FileManager` directory lookups, `appending(path:)` to append strings to a URL. +- Never use C-style number formatting like `String(format: "%.2f", value)`. Use `Text(value, format: .number.precision(.fractionLength(2)))` or similar `FormatStyle` APIs. +- Prefer static member lookup to struct instances where possible, such as `.circle` rather than `Circle()`, and `.borderedProminent` rather than `BorderedProminentButtonStyle()`. +- Avoid force unwraps (`!`) and force `try` unless the failure is truly unrecoverable, and even then prefer using `fatalError()` with a clear description. If possible, use `if let`, `guard let`, nil-coalescing, or `try?`/`do-catch`. +- Filtering text based on user-input must be done using `localizedStandardContains()` as opposed to `contains()` or `localizedCaseInsensitiveContains()`. +- Strongly prefer `Double` over `CGFloat`, except when using optionals or `inout`; Swift is able to bridge the two freely except in those two cases. +- If you want to count array objects that match a predicate, always use `count(where:)` rather than `filter()` followed by `count`. +- Prefer `Date.now` over `Date()` for clarity. +- When `import SwiftUI` is already in a file, you do not need to add `import UIKit` or `import AppKit` to access things like `UIImage` or `NSImage` – they are imported automatically on the appropriate platform. +- When dealing with the names of people, strongly prefer to use `PersonNameComponents` with modern formatting over simple string interpolation such as `Text("\(firstName) \(lastName)")`. +- If a given type of data is repeatedly sorted using an identical closure, e.g. `books.sorted { $0.author < $1.author }`, prefer to make the type in question conform to `Comparable` so the sort order is centralized. +- Prefer to avoid manual date formatting strings if possible. If manual date formatting *is* used for user display, at least make sure to use “y” rather than “yyyy” for years, so the year value is correct in all localizations. If the purpose is data exchange with an API, this rule does not apply. +- When trying to convert a string to a date, prefer the modern `Date` initializer API such as `Date(myString, strategy: .iso8601)`. +- Flag instances where errors triggered by a user action are swallowed silently, e.g. using `print(error.localizedDescription)` rather than showing an alert or similar. +- Prefer `if let value {` shorthand over `if let value = value {`. +- Omit return for single expression functions. `if` and `switch` can be used as expressions when returning values and assigning to variables. + +For example, this kind of code: + +```swift +var tileColor: Color { + if isCorrect { + return .green + } else { + return .red + } +} +``` + +Should be written like this: + +```swift +var tileColor: Color { + if isCorrect { + .green + } else { + .red + } +} +``` + + +## Swift Concurrency + +- If an API offers both modern `async`/`await` equivalents and older closure-based variants, always prefer the `async`/`await` versions. +- Never use Grand Central Dispatch (`DispatchQueue.main.async()`, `DispatchQueue.global()`, etc.). Always use modern Swift concurrency (`async`/`await`, actors, `Task`). +- Never use `Task.sleep(nanoseconds:)`; use `Task.sleep(for:)` instead. +- Flag any mutable shared state that isn't protected by an actor or `@MainActor`, unless the project is configured to use MainActor default actor isolation. +- Assume strict concurrency rules are being applied; flag `@Sendable` violations and data races. +- When evaluating `MainActor.run()`, check whether the project has its default actor isolation set to Main Actor first, because `MainActor.run()` might not be needed. +- `Task.detached()` is often a bad idea. Check any usage extremely carefully. + +For more help with Swift concurrency, suggest the [Swift Concurrency Pro agent skill](https://github.com/twostraws/swift-concurrency-agent-skill). diff --git a/.agents/skills/swiftui-pro/references/views.md b/.agents/skills/swiftui-pro/references/views.md new file mode 100644 index 0000000..41ec1be --- /dev/null +++ b/.agents/skills/swiftui-pro/references/views.md @@ -0,0 +1,35 @@ +# SwiftUI Views + +- Strongly prefer to avoid breaking up view bodies using computed properties or methods that return `some View`, even if `@ViewBuilder` is used. Extract them into separate `View` structs instead, placing each into its own file. +- Flag `body` properties that are excessively long; they should be broken into extracted subviews. +- Button actions should be extracted from view bodies into separate methods, to avoid mixing layout and logic. +- Similarly, general business logic should not live inline in `task()`, `onAppear()` or elsewhere in `body`. +- Prefer to place view logic into view models or similar, so it can be tested. For more help with testing, suggest the [Swift Testing Pro agent skill](https://github.com/twostraws/swift-testing-agent-skill). +- Each type (struct, class, enum) should be in its own Swift file. Flag files containing multiple type definitions. +- Unless a full-screen editing experience is required, prefer using `TextField` with `axis: .vertical` to using `TextEditor`, because it allows placeholder text. If a specific minimum height is required for `TextField`, use something like `lineLimit(5...)`. +- If a button action can be provided directly as an `action` parameter, do so. For example: `Button("Label", systemImage: "plus", action: myAction)` is preferred over `Button("Label", systemImage: "plus") { action() }`. +- When rendering SwiftUI views to images, strongly prefer `ImageRenderer` over `UIGraphicsImageRenderer`. +- `#Preview` should be used for previews, not the legacy `PreviewProvider` protocol. +- When using `TabView(selection:)`, use a binding to a property that stores an enum rather than an integer or string. For example, `Tab("Home", systemImage: "house", value: .home)` is better than `Tab("Home", systemImage: "house", value: 0)`. +- Strongly prefer to avoid breaking up view bodies using computed properties or methods that return `some View`, even if `@ViewBuilder` is used. Extract them into separate `View` structs instead, placing each into its own file. (Yes, this is repeated, but it’s so important it needs to be mentioned twice.) + + +## Animating views + +- Strongly prefer to use the `@Animatable` macro over creating `animatableData` manually – the macro automatically adds conformance to the `Animatable` protocol and creates the correct `animatableData` property. If some properties should not or cannot be animated (e.g. Booleans, integers, etc), mark them `@AnimatableIgnored`. +- Never use `animation(_ animation: Animation?)`; always provide a value to watch, such as `.animation(.bouncy, value: score)`. +- Chaining animations must be done using a `completion` closure passed to `withAnimation()`, rather than trying to execute multiple `withAnimation()` calls using delays. + +For example: + +```swift +Button("Animate Me") { + withAnimation { + scale = 2 + } completion: { + withAnimation { + scale = 1 + } + } +} +``` diff --git a/.claude/skills/swiftui-pro/SKILL.md b/.claude/skills/swiftui-pro/SKILL.md new file mode 100644 index 0000000..e8e699b --- /dev/null +++ b/.claude/skills/swiftui-pro/SKILL.md @@ -0,0 +1,108 @@ +--- +name: swiftui-pro +description: Comprehensively reviews SwiftUI code for best practices on modern APIs, maintainability, and performance. Use when reading, writing, or reviewing SwiftUI projects. +license: MIT +metadata: + author: Paul Hudson + version: "1.0" +--- + +Review Swift and SwiftUI code for correctness, modern API usage, and adherence to project conventions. Report only genuine problems - do not nitpick or invent issues. + +Review process: + +1. Check for deprecated API using `references/api.md`. +1. Check that views, modifiers, and animations have been written optimally using `references/views.md`. +1. Validate that data flow is configured correctly using `references/data.md`. +1. Ensure navigation is updated and performant using `references/navigation.md`. +1. Ensure the code uses designs that are accessible and compliant with Apple’s Human Interface Guidelines using `references/design.md`. +1. Validate accessibility compliance including Dynamic Type, VoiceOver, and Reduce Motion using `references/accessibility.md`. +1. Ensure the code is able to run efficiently using `references/performance.md`. +1. Quick validation of Swift code using `references/swift.md`. +1. Final code hygiene check using `references/hygiene.md`. + +If doing a partial review, load only the relevant reference files. + + +## Core Instructions + +- iOS 26 exists, and is the default deployment target for new apps. +- Target Swift 6.2 or later, using modern Swift concurrency. +- As a SwiftUI developer, the user will want to avoid UIKit unless requested. +- Do not introduce third-party frameworks without asking first. +- Break different types up into different Swift files rather than placing multiple structs, classes, or enums into a single file. +- Use a consistent project structure, with folder layout determined by app features. + + +## Output Format + +Organize findings by file. For each issue: + +1. State the file and relevant line(s). +2. Name the rule being violated (e.g., "Use `foregroundStyle()` instead of `foregroundColor()`"). +3. Show a brief before/after code fix. + +Skip files with no issues. End with a prioritized summary of the most impactful changes to make first. + +Example output: + +### ContentView.swift + +**Line 12: Use `foregroundStyle()` instead of `foregroundColor()`.** + +```swift +// Before +Text("Hello").foregroundColor(.red) + +// After +Text("Hello").foregroundStyle(.red) +``` + +**Line 24: Icon-only button is bad for VoiceOver - add a text label.** + +```swift +// Before +Button(action: addUser) { + Image(systemName: "plus") +} + +// After +Button("Add User", systemImage: "plus", action: addUser) +``` + +**Line 31: Avoid `Binding(get:set:)` in view body - use `@State` with `onChange()` instead.** + +```swift +// Before +TextField("Username", text: Binding( + get: { model.username }, + set: { model.username = $0; model.save() } +)) + +// After +TextField("Username", text: $model.username) + .onChange(of: model.username) { + model.save() + } +``` + +### Summary + +1. **Accessibility (high):** The add button on line 24 is invisible to VoiceOver. +2. **Deprecated API (medium):** `foregroundColor()` on line 12 should be `foregroundStyle()`. +3. **Data flow (medium):** The manual binding on line 31 is fragile and harder to maintain. + +End of example. + + +## References + +- `references/accessibility.md` - Dynamic Type, VoiceOver, Reduce Motion, and other accessibility requirements. +- `references/api.md` - updating code for modern API, and the deprecated code it replaces. +- `references/design.md` - guidance for building accessible apps that meet Apple’s Human Interface Guidelines. +- `references/hygiene.md` - making code compile cleanly and be maintainable in the long term. +- `references/navigation.md` - navigation using `NavigationStack`/`NavigationSplitView`, plus alerts, confirmation dialogs, and sheets. +- `references/performance.md` - optimizing SwiftUI code for maximum performance. +- `references/data.md` - data flow, shared state, and property wrappers. +- `references/swift.md` - tips on writing modern Swift code, including using Swift Concurrency effectively. +- `references/views.md` - view structure, composition, and animation. diff --git a/.claude/skills/swiftui-pro/agents/openai.yaml b/.claude/skills/swiftui-pro/agents/openai.yaml new file mode 100644 index 0000000..bace09e --- /dev/null +++ b/.claude/skills/swiftui-pro/agents/openai.yaml @@ -0,0 +1,10 @@ +interface: + display_name: "SwiftUI Pro" + short_description: "Reviews SwiftUI code for modern best practices." + icon_small: "./assets/swiftui-pro-icon.svg" + icon_large: "./assets/swiftui-pro-icon.png" + brand_color: "#006AFD" + default_prompt: "Use $swiftui-pro to review my project." + +policy: + allow_implicit_invocation: true \ No newline at end of file diff --git a/.claude/skills/swiftui-pro/assets/swiftui-pro-icon.png b/.claude/skills/swiftui-pro/assets/swiftui-pro-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..11db253c2b4093ae3bc2b96d530a0767f40ac33f GIT binary patch literal 3117 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 + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.claude/skills/swiftui-pro/references/accessibility.md b/.claude/skills/swiftui-pro/references/accessibility.md new file mode 100644 index 0000000..7cb96c1 --- /dev/null +++ b/.claude/skills/swiftui-pro/references/accessibility.md @@ -0,0 +1,13 @@ +# Accessibility + +- Respect the user’s accessibility settings for fonts, colors, animations, and more. +- Do not force specific font sizes. Prefer Dynamic Type (`.font(.body)`, `.font(.headline)`, etc.). +- If you *need* a custom font size, use `@ScaledMetric` when targeting iOS 18 and earlier. When targeting iOS 26 or later, `.font(.body.scaled(by:))` is also available to get font size adjustment. +- Flag instances where images have unclear or unhelpful VoiceOver readings, e.g. `Image(.newBanner2026)`. If they are decorative, suggest using `Image(decorative:)` or `accessibilityHidden()`, otherwise attach an `accessibilityLabel()`. +- If the user has “Reduce Motion” enabled, replace large, motion-based animations with opacity instead. +- If buttons have complex or frequently changing labels, recommend using `accessibilityInputLabels()` to provide better Voice Control commands. For example, if a button had a live-updating share price for Apple such as “AAPL $271.68”, adding an input label for “Apple” would be a big improvement. +- Buttons with image labels must always include text, even if the text is invisible: `Button("Label", systemImage: "plus", action: myAction)`. Flag icon-only buttons that lack a text label as being bad for VoiceOver. +- If color is an important differentiator in the user interface, make sure to respect the environment’s `.accessibilityDifferentiateWithoutColor` setting by showing some kind of variation beyond just color – icons, patterns, strokes, etc. +- The same is true of `Menu`: using `Menu("Options", systemImage: "ellipsis.circle") { }` is much better than just using an image. +- Never use `onTapGesture()` unless you specifically need tap location or tap count. All other tappable elements should be a `Button`. +- If `onTapGesture()` must be used, make sure to add `.accessibilityAddTraits(.isButton)` or similar so it can be read by VoiceOver correctly. diff --git a/.claude/skills/swiftui-pro/references/api.md b/.claude/skills/swiftui-pro/references/api.md new file mode 100644 index 0000000..bfd5199 --- /dev/null +++ b/.claude/skills/swiftui-pro/references/api.md @@ -0,0 +1,39 @@ +# Using modern SwiftUI API + +- Always use `foregroundStyle()` instead of `foregroundColor()`. +- Always use `clipShape(.rect(cornerRadius:))` instead of `cornerRadius()`. +- Always use the `Tab` API instead of `tabItem()`. +- Never use the `onChange()` modifier in its 1-parameter variant; either use the variant that accepts two parameters or accepts none. +- Do not use `GeometryReader` if a newer alternative works: `containerRelativeFrame()`, `visualEffect()`, or the `Layout` protocol. Flag `GeometryReader` usage and suggest the modern alternative. +- When designing haptic effects, prefer using `sensoryFeedback()` over older UIKit APIs such as `UIImpactFeedbackGenerator`. +- Use the `@Entry` macro to define custom `EnvironmentValues`, `FocusValues`, `Transaction`, and `ContainerValues` keys. This replaces the legacy pattern of manually creating a type conforming to (for example) `EnvironmentKey` with a `defaultValue`, then extending `EnvironmentValues` with a computed property. +- Strongly prefer `overlay(alignment:content:)` over the deprecated `overlay(_:alignment:)`. For example, use `.overlay { Text("Hello, world!") }` rather than `.overlay(Text("Hello, world!"))`. +- Never use `.navigationBarLeading` and `.navigationBarTrailing` for toolbar item placement; they are deprecated. The correct, modern placements are `.topBarLeading` and `.topBarTrailing`. +- Prefer to rely on automatic grammar agreement when dealing with English, French, German, Portuguese, Spanish, and Italian. For example, use `Text("^[\(people) person](inflect: true)")` to show a number of people. +- You can fill and stroke a shape with two chained modifiers; you do *not* need an overlay for the stroke. The overlay was required previously, but this is fixed in iOS 17 and later. +- When referencing images from an asset catalog, prefer the generated symbol asset API when the project is configured to use them: `Image(.avatar)` rather than `Image("avatar")`. +- When targeting iOS 26 and later, SwiftUI has a native `WebView` view type that replaces almost all uses of hand-wrapped `WKWebView` inside `UIViewRepresentable`. To use it, make sure to include `import WebKit`. +- `ForEach` over an `enumerated()` sequence should not convert to an array first. Use `ForEach(items.enumerated(), id: \.element.id)` directly. +- When hiding scroll indicators, use `.scrollIndicators(.hidden)` rather than `showsIndicators: false` in the initializer. +- Never use `Text` concatenation with `+`. + +For example, the usage of `+` here is bad and deprecated: + +```swift +Text("Hello").foregroundStyle(.red) ++ +Text("World").foregroundStyle(.blue) +``` + +Instead, use text interpolation like this: + +```swift +let red = Text("Hello").foregroundStyle(.red) +let blue = Text("World").foregroundStyle(.blue) +Text("\(red)\(blue)") +``` + + +## Using ObservableObject + +If using `ObservableObject` is absolutely required – for example if you are trying to create a debouncer using a Combine publisher – you should always make sure `import Combine` is added. This was previously provided through SwiftUI, but that is no longer the case. diff --git a/.claude/skills/swiftui-pro/references/data.md b/.claude/skills/swiftui-pro/references/data.md new file mode 100644 index 0000000..952571f --- /dev/null +++ b/.claude/skills/swiftui-pro/references/data.md @@ -0,0 +1,43 @@ +# Data flow, shared state, and property wrappers + +It is important that SwiftUI body code and logic code be kept separate in order to make code easier to read, write, and maintain. That usually means placing code into methods rather than inline in the `body` property, but often also means carving functionality out into separate `@Observable` classes. + +These rules help ensure code is efficient and works well in the long term. + + +## Shared state + +- `@Observable` classes must be marked `@MainActor` unless the project has Main Actor default actor isolation. Flag any `@Observable` class missing this annotation. +- All shared data should use `@Observable` classes with `@State` (for ownership) and `@Bindable` / `@Environment` (for passing). +- Strongly prefer not to use `ObservableObject`, `@Published`, `@StateObject`, `@ObservedObject`, or `@EnvironmentObject` unless they are unavoidable, or if they exist in legacy/integration contexts when changing architecture would be complicated. + + +## Local state + +- `@State` should be marked `private` and only owned by the view that created it. +- If a view stores a class instance that contains expensive-to-recompute data, e.g. `CIContext`, it can be stored using `@State` even though it is not an observable object. This effectively uses `@State` as a cache – storing something persistently, but not doing any change tracking on it since it's not an observable object. + + +## Bindings + +- Strongly prefer to avoid creating bindings using `Binding(get:set:)` in view body code. It is much cleaner and simpler to use a binding provided by `@State`, `@Binding` or similar, then use `onChange()` to trigger any effects. +- If the user needs to enter a number into a `TextField`, bind the `TextField` to a numeric value such as `Int` or `Double`, then use its `format` initializer like this: `TextField("Enter your score", value: $score, format: .number)`. Apply either `.keyboardType(.numberPad)` (for integers) or `.keyboardType(.decimalPad)` (for floating-point numbers) as appropriate. Using the modifier alone is *not* sufficient. + + +## Working with data + +- Prefer to make structs conform to `Identifiable` rather than using `id: \.someProperty` in SwiftUI code. +- Never attempt to use `@AppStorage` inside an `@Observable` class, even if marked `@ObservationIgnored` – it will *not* trigger view updates when a change happens. + + +## SwiftData + +- If you only need the number of items matching a query, consider `ModelContext.fetchCount()` with a fetch descriptor. This will *not* live update if the data changes unless something else triggers the update, such as `@Query`, so it should be used carefully. + +For more help with SwiftData, suggest the [SwiftData Pro agent skill](https://github.com/twostraws/swiftdata-agent-skill). + +## If the project uses SwiftData with CloudKit + +- Never use `@Attribute(.unique)`. +- Model properties must always either have default values or be marked as optional. +- All relationships must be marked optional. diff --git a/.claude/skills/swiftui-pro/references/design.md b/.claude/skills/swiftui-pro/references/design.md new file mode 100644 index 0000000..8e14b6e --- /dev/null +++ b/.claude/skills/swiftui-pro/references/design.md @@ -0,0 +1,31 @@ +# Design + +## Creating a uniform design in this app + +Prefer to place standard fonts, sizes, colors, stack spacing, padding, rounding, animation timings, and more into a shared enum of constants, so they can be used by all views. This allows the app’s design to feel uniform and consistent, and be adjusted easily. + + +## Requirements for flexible, accessible design + +- Never use `UIScreen.main.bounds` to read available space; prefer alternatives such as `containerRelativeFrame()`, or `visualEffect()` as appropriate, or (if there is no alternative) `GeometryReader`. +- Prefer to avoid fixed frames for views unless content can fit neatly inside; this can cause problems across different device sizes, different Dynamic Type settings, and more. Giving frames some flexibility is usually preferred. +- Apple’s minimum acceptable tap area for interactions on iOS is 44x44. Ensure this is strictly enforced. + + +## Standard system styling + +- Strongly prefer to use `ContentUnavailableView` when data is missing or empty, rather than designing something custom. +- When using `searchable()`, you can show empty results using `ContentUnavailableView.search` and it will include the search term they used automatically – there’s no need to use `ContentUnavailableView.search(text: searchText)` or similar. +- If you need an icon and some text placed horizontally side by side, prefer `Label` over `HStack`. +- Prefer system hierarchical styles (e.g. secondary/tertiary) over manual opacity when possible, so the system can adapt to the correct context automatically. +- When using `Form`, wrap controls such as `Slider` in `LabeledContent` so the title and control are laid out correctly. +- When using `RoundedRectangle`, the default rounding style is `.continuous` – there is no need to specify it explicitly. + + +## Ensuring designs work for everyone + +- Use `bold()` instead of `fontWeight(.bold)`, because using `bold()` allows the system to choose the correct weight for the current context. +- Only use `fontWeight()` for weights other than bold when there's an important reason - scattering around `fontWeight(.medium)` or `fontWeight(.semibold)` is counterproductive. +- Avoid hard-coded values for padding and stack spacing unless specifically requested. +- Avoid UIKit colors (`UIColor`) in SwiftUI code; use SwiftUI `Color` or asset catalog colors. +- The font size `.caption2` is extremely small, and is generally best avoided. Even the font size `.caption` is on the small side, and should be used carefully. diff --git a/.claude/skills/swiftui-pro/references/hygiene.md b/.claude/skills/swiftui-pro/references/hygiene.md new file mode 100644 index 0000000..80bc9c5 --- /dev/null +++ b/.claude/skills/swiftui-pro/references/hygiene.md @@ -0,0 +1,9 @@ +# Hygiene + +- If the project requires secrets such as API keys, never include them in the repository. +- Code comments and documentation comments should be present where the logic isn't self-evident. +- Unit tests should exist for core application logic. UI tests only where unit tests are not possible. +- `@AppStorage` must never be used to store usernames, passwords, or other sensitive data. Use the keychain for that. +- If SwiftLint is configured, it should return no warnings or errors. +- If the project uses Localizable.xcstrings, prefer to add user-facing strings using symbol keys (e.g. “helloWorld”) in the string catalog with `extractionState` set to "manual", accessing them via generated symbols such as `Text(.helloWorld)`. Offer to translate new keys into all languages supported by the project. +- If the Xcode MCP is configured, prefer its tools over generic alternatives. For example, `RenderPreview` is able to capture images of rendered SwiftUI previews for examination, and `DocumentationSearch` can search Apple’s documentation for latest usage instructions. diff --git a/.claude/skills/swiftui-pro/references/navigation.md b/.claude/skills/swiftui-pro/references/navigation.md new file mode 100644 index 0000000..44b4025 --- /dev/null +++ b/.claude/skills/swiftui-pro/references/navigation.md @@ -0,0 +1,14 @@ +# Navigation and presentation + +- Use `NavigationStack` or `NavigationSplitView` as appropriate; flag all use of the deprecated `NavigationView`. +- Strongly prefer to use `navigationDestination(for:)` to specify destinations; flag all use of the old `NavigationLink(destination:)` pattern where it should be replaced. +- Never mix `navigationDestination(for:)` and `NavigationLink(destination:)` in the same navigation hierarchy; it causes significant problems. +- `navigationDestination(for:)` must be registered once per data type; flag duplicates. + + +## Alerts, confirmation dialogs, and sheets + +- Always attach `confirmationDialog()` to the user interface that triggers the dialog. This allows Liquid Glass animations to move from the correct source. +- If an alert has only a single “OK” button that does nothing but dismiss the alert, it can be omitted entirely: `.alert("Dismiss Me", isPresented: $isShowingAlert) { }`. +- If a sheet is designed to present an optional piece of data, prefer `sheet(item:)` over `sheet(isPresented:)` so the optional is safely unwrapped. +- When using `sheet(item:)` with a view that accepts the item as its only initializer parameter, prefer `sheet(item: $someItem, content: SomeView.init)` over `sheet(item: $someItem) { someItem in SomeView(item: someItem) }`. diff --git a/.claude/skills/swiftui-pro/references/performance.md b/.claude/skills/swiftui-pro/references/performance.md new file mode 100644 index 0000000..72c5a03 --- /dev/null +++ b/.claude/skills/swiftui-pro/references/performance.md @@ -0,0 +1,46 @@ +# Performance + +- When toggling modifier values, prefer ternary expressions over if/else view branching to avoid `_ConditionalContent`, preserve structural identity, and avoid repeatedly recreating underlying platform views. +- Avoid `AnyView` unless absolutely required. Use `@ViewBuilder`, `Group`, or generics instead. +- If a `ScrollView` has an opaque, static, and solid background, prefer to use `scrollContentBackground(.visible)` to improve scroll-edge rendering efficiency. +- It is more efficient to break views up by making dedicated SwiftUI views rather than place them into computed properties or methods. Using `@ViewBuilder` on a property or method does not solve this; breaking views up is strongly preferred. +- Always ensure view initializers are kept as small and simple as possible, avoiding any non-trivial work. Flag any work that can be moved into a `task()` modifier to be run when the view is shown. +- Similarly, assume each view’s `body` property is called frequently – if logic such as sorting or filtering can be moved out of there easily, it should be. +- Avoid creating properties to store formatters such as `DateFormatter` unless they are required. A more natural approach is to use `Text` with a format, like this: `Text(Date.now, format: .dateTime.day().month().year())` or `Text(100, format: .currency(code: "USD"))`. +- Avoid expensive inline transforms in `List`/`ForEach` initializers (e.g. `items.filter { ... }`) when they are repeated often. +- Prefer deriving transformed data from the source-of-truth using `let`, or caching in `@State`. However, do not cache derived collections in `@State` unless you also own explicit invalidation logic to avoid stale UI. +- For large data sets in `ScrollView`, use `LazyVStack`/`LazyHStack`; flag eager stacks with many children. +- Prefer using `task()` over `onAppear()` when doing async work, because it will be cancelled automatically when the view disappears. +- Avoid storing escaping `@ViewBuilder` closures on views when possible; store built view results instead. + +Example: + +```swift +// Anti-pattern: stores an escaping closure on the view. +struct CardView: View { + let content: () -> Content + + var body: some View { + VStack(alignment: .leading) { + content() + } + .padding() + .background(.ultraThinMaterial) + .clipShape(.rect(cornerRadius: 8)) + } +} + +// Preferred: store the built view value; the synthesized init handles calling the builder. +struct CardView: View { + @ViewBuilder let content: Content + + var body: some View { + VStack(alignment: .leading) { + content + } + .padding() + .background(.ultraThinMaterial) + .clipShape(.rect(cornerRadius: 8)) + } +} +``` diff --git a/.claude/skills/swiftui-pro/references/swift.md b/.claude/skills/swiftui-pro/references/swift.md new file mode 100644 index 0000000..92f32b1 --- /dev/null +++ b/.claude/skills/swiftui-pro/references/swift.md @@ -0,0 +1,56 @@ +# Swift + +- Prefer Swift-native string methods over Foundation equivalents: use `replacing("a", with: "b")` not `replacingOccurrences(of: "a", with: "b")`. +- Prefer modern Foundation API: `URL.documentsDirectory` instead of `FileManager` directory lookups, `appending(path:)` to append strings to a URL. +- Never use C-style number formatting like `String(format: "%.2f", value)`. Use `Text(value, format: .number.precision(.fractionLength(2)))` or similar `FormatStyle` APIs. +- Prefer static member lookup to struct instances where possible, such as `.circle` rather than `Circle()`, and `.borderedProminent` rather than `BorderedProminentButtonStyle()`. +- Avoid force unwraps (`!`) and force `try` unless the failure is truly unrecoverable, and even then prefer using `fatalError()` with a clear description. If possible, use `if let`, `guard let`, nil-coalescing, or `try?`/`do-catch`. +- Filtering text based on user-input must be done using `localizedStandardContains()` as opposed to `contains()` or `localizedCaseInsensitiveContains()`. +- Strongly prefer `Double` over `CGFloat`, except when using optionals or `inout`; Swift is able to bridge the two freely except in those two cases. +- If you want to count array objects that match a predicate, always use `count(where:)` rather than `filter()` followed by `count`. +- Prefer `Date.now` over `Date()` for clarity. +- When `import SwiftUI` is already in a file, you do not need to add `import UIKit` or `import AppKit` to access things like `UIImage` or `NSImage` – they are imported automatically on the appropriate platform. +- When dealing with the names of people, strongly prefer to use `PersonNameComponents` with modern formatting over simple string interpolation such as `Text("\(firstName) \(lastName)")`. +- If a given type of data is repeatedly sorted using an identical closure, e.g. `books.sorted { $0.author < $1.author }`, prefer to make the type in question conform to `Comparable` so the sort order is centralized. +- Prefer to avoid manual date formatting strings if possible. If manual date formatting *is* used for user display, at least make sure to use “y” rather than “yyyy” for years, so the year value is correct in all localizations. If the purpose is data exchange with an API, this rule does not apply. +- When trying to convert a string to a date, prefer the modern `Date` initializer API such as `Date(myString, strategy: .iso8601)`. +- Flag instances where errors triggered by a user action are swallowed silently, e.g. using `print(error.localizedDescription)` rather than showing an alert or similar. +- Prefer `if let value {` shorthand over `if let value = value {`. +- Omit return for single expression functions. `if` and `switch` can be used as expressions when returning values and assigning to variables. + +For example, this kind of code: + +```swift +var tileColor: Color { + if isCorrect { + return .green + } else { + return .red + } +} +``` + +Should be written like this: + +```swift +var tileColor: Color { + if isCorrect { + .green + } else { + .red + } +} +``` + + +## Swift Concurrency + +- If an API offers both modern `async`/`await` equivalents and older closure-based variants, always prefer the `async`/`await` versions. +- Never use Grand Central Dispatch (`DispatchQueue.main.async()`, `DispatchQueue.global()`, etc.). Always use modern Swift concurrency (`async`/`await`, actors, `Task`). +- Never use `Task.sleep(nanoseconds:)`; use `Task.sleep(for:)` instead. +- Flag any mutable shared state that isn't protected by an actor or `@MainActor`, unless the project is configured to use MainActor default actor isolation. +- Assume strict concurrency rules are being applied; flag `@Sendable` violations and data races. +- When evaluating `MainActor.run()`, check whether the project has its default actor isolation set to Main Actor first, because `MainActor.run()` might not be needed. +- `Task.detached()` is often a bad idea. Check any usage extremely carefully. + +For more help with Swift concurrency, suggest the [Swift Concurrency Pro agent skill](https://github.com/twostraws/swift-concurrency-agent-skill). diff --git a/.claude/skills/swiftui-pro/references/views.md b/.claude/skills/swiftui-pro/references/views.md new file mode 100644 index 0000000..41ec1be --- /dev/null +++ b/.claude/skills/swiftui-pro/references/views.md @@ -0,0 +1,35 @@ +# SwiftUI Views + +- Strongly prefer to avoid breaking up view bodies using computed properties or methods that return `some View`, even if `@ViewBuilder` is used. Extract them into separate `View` structs instead, placing each into its own file. +- Flag `body` properties that are excessively long; they should be broken into extracted subviews. +- Button actions should be extracted from view bodies into separate methods, to avoid mixing layout and logic. +- Similarly, general business logic should not live inline in `task()`, `onAppear()` or elsewhere in `body`. +- Prefer to place view logic into view models or similar, so it can be tested. For more help with testing, suggest the [Swift Testing Pro agent skill](https://github.com/twostraws/swift-testing-agent-skill). +- Each type (struct, class, enum) should be in its own Swift file. Flag files containing multiple type definitions. +- Unless a full-screen editing experience is required, prefer using `TextField` with `axis: .vertical` to using `TextEditor`, because it allows placeholder text. If a specific minimum height is required for `TextField`, use something like `lineLimit(5...)`. +- If a button action can be provided directly as an `action` parameter, do so. For example: `Button("Label", systemImage: "plus", action: myAction)` is preferred over `Button("Label", systemImage: "plus") { action() }`. +- When rendering SwiftUI views to images, strongly prefer `ImageRenderer` over `UIGraphicsImageRenderer`. +- `#Preview` should be used for previews, not the legacy `PreviewProvider` protocol. +- When using `TabView(selection:)`, use a binding to a property that stores an enum rather than an integer or string. For example, `Tab("Home", systemImage: "house", value: .home)` is better than `Tab("Home", systemImage: "house", value: 0)`. +- Strongly prefer to avoid breaking up view bodies using computed properties or methods that return `some View`, even if `@ViewBuilder` is used. Extract them into separate `View` structs instead, placing each into its own file. (Yes, this is repeated, but it’s so important it needs to be mentioned twice.) + + +## Animating views + +- Strongly prefer to use the `@Animatable` macro over creating `animatableData` manually – the macro automatically adds conformance to the `Animatable` protocol and creates the correct `animatableData` property. If some properties should not or cannot be animated (e.g. Booleans, integers, etc), mark them `@AnimatableIgnored`. +- Never use `animation(_ animation: Animation?)`; always provide a value to watch, such as `.animation(.bouncy, value: score)`. +- Chaining animations must be done using a `completion` closure passed to `withAnimation()`, rather than trying to execute multiple `withAnimation()` calls using delays. + +For example: + +```swift +Button("Animate Me") { + withAnimation { + scale = 2 + } completion: { + withAnimation { + scale = 1 + } + } +} +``` diff --git a/sync-agents b/sync-agents index d46db84..cf25fed 100755 --- a/sync-agents +++ b/sync-agents @@ -1,100 +1,219 @@ -#!/bin/bash -set -euo pipefail - -REPO_ROOT="$(cd "$(dirname "$0")" && pwd)" -MARKER="" - -GIT_ADD=false -for arg in "$@"; do - case "$arg" in - --git-add) GIT_ADD=true ;; - esac -done - -changed_files="$(mktemp)" -expected_files="$(mktemp)" -trap 'rm -f "$changed_files" "$expected_files"' EXIT - -# --- Helper: write a file only when its content has changed --- - -write_if_changed() { - local target="$1" - local content="$2" - - if [ -f "$target" ] && [ "$(cat "$target")" = "$content" ]; then - return - fi - - mkdir -p "$(dirname "$target")" - printf '%s\n' "$content" > "$target" - echo "Updated $target" - echo "$target" >> "$changed_files" -} - -# --- Helper: remove a file and track the change --- - -remove_if_exists() { - local target="$1" - if [ -f "$target" ]; then - rm "$target" - echo "Removed $target" - echo "$target" >> "$changed_files" - fi -} - -# --- Generate CLAUDE.md from AGENTS.md files --- - -while IFS= read -r -d '' agents_file; do - dir="$(dirname "$agents_file")" - rel_dir="${dir#"$REPO_ROOT"}" - rel_dir="${rel_dir#/}" - - claude_path="$dir/CLAUDE.md" - content="$(printf '%s\n\n%s' "$MARKER" "$(cat "$agents_file")")" - write_if_changed "$claude_path" "$content" - echo "$claude_path" >> "$expected_files" -done < <(find "$REPO_ROOT" -name "AGENTS.md" \ - -not -path "*/Derived/*" \ - -not -path "*/.build/*" \ - -not -path "*/.git/*" \ - -not -path "*/.cursor/*" \ - -not -path "*/.claude/*" \ - -print0 2>/dev/null || true) - -# --- Clean stale CLAUDE.md files --- - -while IFS= read -r -d '' file; do - if ! grep -qxF "$file" "$expected_files"; then - if head -n1 "$file" | grep -qxF "$MARKER"; then - remove_if_exists "$file" - fi - fi -done < <(find "$REPO_ROOT" -name "CLAUDE.md" \ - -not -path "*/Derived/*" \ - -not -path "*/.build/*" \ - -not -path "*/.git/*" \ - -print0 2>/dev/null || true) - -# --- Sync .agents/skills → .claude/skills --- - -if [ -d "$REPO_ROOT/.agents/skills" ]; then - mkdir -p "$REPO_ROOT/.claude/skills" - if ! diff -rq "$REPO_ROOT/.agents/skills" "$REPO_ROOT/.claude/skills" > /dev/null 2>&1; then - rm -rf "$REPO_ROOT/.claude/skills" - cp -R "$REPO_ROOT/.agents/skills" "$REPO_ROOT/.claude/skills" - echo "Synced .agents/skills/ → .claude/skills/" - echo ".claude/skills" >> "$changed_files" - fi -elif [ -d "$REPO_ROOT/.claude/skills" ]; then - rm -rf "$REPO_ROOT/.claude/skills" - echo "Removed .claude/skills/" - echo ".claude/skills" >> "$changed_files" -fi - -# --- Stage generated files if --git-add was passed --- - -if [ "$GIT_ADD" = true ] && [ -s "$changed_files" ]; then - while IFS= read -r file; do - git add -A -- "$file" 2>/dev/null || true - done < "$changed_files" -fi +#!/usr/bin/env ruby +# frozen_string_literal: true + +require "fileutils" +require "json" +require "tmpdir" +require "open3" + +REPO_ROOT = File.expand_path("..", __FILE__) +MARKER = "" +SKILLS_DIR = File.join(REPO_ROOT, ".agents", "skills") +CLAUDE_SKILLS_DIR = File.join(REPO_ROOT, ".claude", "skills") +MANIFEST_PATH = File.join(REPO_ROOT, ".agents", "external-skills.json") + +EXCLUDED_DIRS = %w[Derived .build .git .cursor .claude].freeze + +class SyncAgents + def initialize + @changed_files = [] + end + + def run(args) + if args.include?("--install") + idx = args.index("--install") + url = args[idx + 1] + name = args[idx + 2] + abort "Usage: sync-agents --install " unless url + install_skill(url, name) + elsif args.include?("--update") + update_skills + end + + sync_claude_md + sync_skills + + stage_files if args.include?("--git-add") && !@changed_files.empty? + end + + private + + # --- CLAUDE.md sync --- + + def sync_claude_md + expected = [] + + find_files("AGENTS.md", exclude: EXCLUDED_DIRS).each do |agents_file| + claude_path = File.join(File.dirname(agents_file), "CLAUDE.md") + content = "#{MARKER}\n\n#{File.read(agents_file)}" + write_if_changed(claude_path, content) + expected << claude_path + end + + find_files("CLAUDE.md", exclude: %w[Derived .build .git]).each do |claude_file| + next if expected.include?(claude_file) + first_line = File.open(claude_file, &:readline).chomp rescue next + remove_if_exists(claude_file) if first_line == MARKER + end + end + + # --- Skills sync --- + + def sync_skills + if Dir.exist?(SKILLS_DIR) + FileUtils.mkdir_p(CLAUDE_SKILLS_DIR) + unless dirs_equal?(SKILLS_DIR, CLAUDE_SKILLS_DIR) + FileUtils.rm_rf(CLAUDE_SKILLS_DIR) + FileUtils.cp_r(SKILLS_DIR, CLAUDE_SKILLS_DIR) + puts "Synced .agents/skills/ → .claude/skills/" + @changed_files << CLAUDE_SKILLS_DIR + end + elsif Dir.exist?(CLAUDE_SKILLS_DIR) + FileUtils.rm_rf(CLAUDE_SKILLS_DIR) + puts "Removed .claude/skills/" + @changed_files << CLAUDE_SKILLS_DIR + end + end + + # --- Install external skill --- + + def install_skill(url, name) + repo = parse_github_repo(url) + abort "Could not parse GitHub repo from: #{url}" unless repo + + Dir.mktmpdir do |tmpdir| + clone_repo(repo, "main", tmpdir) + + if name + skill_src = File.join(tmpdir, name) + unless Dir.exist?(skill_src) && File.exist?(File.join(skill_src, "SKILL.md")) + available = list_skills(tmpdir) + abort "Skill '#{name}' not found in #{repo}. Available: #{available.join(", ")}" + end + else + available = list_skills(tmpdir) + case available.length + when 0 then abort "No skills found in #{repo}" + when 1 then name = available.first + else abort "Multiple skills in #{repo}: #{available.join(", ")}\n" \ + "Specify one: ./sync-agents --install #{url} " + end + skill_src = File.join(tmpdir, name) + end + + dest = File.join(SKILLS_DIR, name) + FileUtils.rm_rf(dest) + FileUtils.mkdir_p(SKILLS_DIR) + FileUtils.cp_r(skill_src, dest) + puts "Installed #{name} from #{repo}" + + manifest = load_manifest + manifest[name] = { "repo" => repo, "path" => name, "ref" => "main" } + save_manifest(manifest) + end + end + + # --- Update all external skills --- + + def update_skills + manifest = load_manifest + if manifest.empty? + puts "No external skills to update." + return + end + + manifest.each do |name, entry| + repo = entry["repo"] + path = entry["path"] + ref = entry["ref"] || "main" + + Dir.mktmpdir do |tmpdir| + clone_repo(repo, ref, tmpdir) + + skill_src = File.join(tmpdir, path) + unless Dir.exist?(skill_src) + warn "Warning: '#{path}' not found in #{repo}@#{ref}, skipping #{name}" + next + end + + dest = File.join(SKILLS_DIR, name) + FileUtils.rm_rf(dest) + FileUtils.cp_r(skill_src, dest) + puts "Updated #{name} from #{repo}@#{ref}" + end + end + end + + # --- Helpers --- + + def write_if_changed(path, content) + return if File.exist?(path) && File.read(path) == content + FileUtils.mkdir_p(File.dirname(path)) + File.write(path, content) + puts "Updated #{path}" + @changed_files << path + end + + def remove_if_exists(path) + return unless File.exist?(path) + File.delete(path) + puts "Removed #{path}" + @changed_files << path + end + + def find_files(name, exclude: []) + Dir.glob(File.join(REPO_ROOT, "**", name)).reject do |f| + exclude.any? { |d| f.include?("/#{d}/") } + end + end + + def dirs_equal?(a, b) + return false unless Dir.exist?(a) && Dir.exist?(b) + a_files = Dir.glob(File.join(a, "**", "*")).map { |f| f.sub(a, "") }.sort + b_files = Dir.glob(File.join(b, "**", "*")).map { |f| f.sub(b, "") }.sort + return false unless a_files == b_files + a_files.each do |rel| + af = File.join(a, rel) + bf = File.join(b, rel) + next if File.directory?(af) + return false unless File.read(af) == File.read(bf) + end + true + end + + def stage_files + @changed_files.each do |file| + system("git", "add", "-A", "--", file, err: File::NULL) + end + end + + def parse_github_repo(url) + match = url.match(%r{github\.com[/:]([^/]+/[^/]+?)(?:\.git)?/?$}) + match && match[1] + end + + def clone_repo(repo, ref, dest) + url = "https://github.com/#{repo}.git" + _, stderr, status = Open3.capture3("git", "clone", "--depth", "1", "--branch", ref, url, dest) + abort "Failed to clone #{repo}@#{ref}: #{stderr}" unless status.success? + end + + def list_skills(dir) + Dir.children(dir) + .select { |name| File.exist?(File.join(dir, name, "SKILL.md")) } + .sort + end + + def load_manifest + return {} unless File.exist?(MANIFEST_PATH) + JSON.parse(File.read(MANIFEST_PATH)) + end + + def save_manifest(manifest) + FileUtils.mkdir_p(File.dirname(MANIFEST_PATH)) + File.write(MANIFEST_PATH, JSON.pretty_generate(manifest) + "\n") + puts "Updated #{MANIFEST_PATH}" + end +end + +SyncAgents.new.run(ARGV) From 8ab7531c4ca4146e73cd23af0ccbff50eee5c570 Mon Sep 17 00:00:00 2001 From: Kyle Van Essen Date: Sat, 4 Apr 2026 14:34:15 -0400 Subject: [PATCH 08/13] Gitignore external skills, split --install/--update/--add semantics - Rename --install to --add (adds a new skill from a GitHub URL) - New --install fetches all external skills from the manifest, skipping any already present (idempotent, like npm ci) - --update force re-fetches all external skills to latest - Gitignore external skills in .agents/skills/ via a generated .gitignore managed from the manifest; gitignore all of .claude/skills/ - Run ./sync-agents --install in ./ide so skills are fetched on setup Made-with: Cursor --- .agents/skills/.gitignore | 2 + .agents/skills/swiftui-pro/SKILL.md | 108 ------------------ .agents/skills/swiftui-pro/agents/openai.yaml | 10 -- .../swiftui-pro/assets/swiftui-pro-icon.png | Bin 3117 -> 0 bytes .../swiftui-pro/assets/swiftui-pro-icon.svg | 29 ----- .../swiftui-pro/references/accessibility.md | 13 --- .agents/skills/swiftui-pro/references/api.md | 39 ------- .agents/skills/swiftui-pro/references/data.md | 43 ------- .../skills/swiftui-pro/references/design.md | 31 ----- .../skills/swiftui-pro/references/hygiene.md | 9 -- .../swiftui-pro/references/navigation.md | 14 --- .../swiftui-pro/references/performance.md | 46 -------- .../skills/swiftui-pro/references/swift.md | 56 --------- .../skills/swiftui-pro/references/views.md | 35 ------ .../custom-container-view-controller/SKILL.md | 8 -- .claude/skills/swiftui-pro/SKILL.md | 108 ------------------ .claude/skills/swiftui-pro/agents/openai.yaml | 10 -- .../swiftui-pro/assets/swiftui-pro-icon.png | Bin 3117 -> 0 bytes .../swiftui-pro/assets/swiftui-pro-icon.svg | 29 ----- .../swiftui-pro/references/accessibility.md | 13 --- .claude/skills/swiftui-pro/references/api.md | 39 ------- .claude/skills/swiftui-pro/references/data.md | 43 ------- .../skills/swiftui-pro/references/design.md | 31 ----- .../skills/swiftui-pro/references/hygiene.md | 9 -- .../swiftui-pro/references/navigation.md | 14 --- .../swiftui-pro/references/performance.md | 46 -------- .../skills/swiftui-pro/references/swift.md | 56 --------- .../skills/swiftui-pro/references/views.md | 35 ------ .gitignore | 3 + ide | 3 + sync-agents | 72 ++++++++++-- 31 files changed, 71 insertions(+), 883 deletions(-) create mode 100644 .agents/skills/.gitignore delete mode 100644 .agents/skills/swiftui-pro/SKILL.md delete mode 100644 .agents/skills/swiftui-pro/agents/openai.yaml delete mode 100644 .agents/skills/swiftui-pro/assets/swiftui-pro-icon.png delete mode 100644 .agents/skills/swiftui-pro/assets/swiftui-pro-icon.svg delete mode 100644 .agents/skills/swiftui-pro/references/accessibility.md delete mode 100644 .agents/skills/swiftui-pro/references/api.md delete mode 100644 .agents/skills/swiftui-pro/references/data.md delete mode 100644 .agents/skills/swiftui-pro/references/design.md delete mode 100644 .agents/skills/swiftui-pro/references/hygiene.md delete mode 100644 .agents/skills/swiftui-pro/references/navigation.md delete mode 100644 .agents/skills/swiftui-pro/references/performance.md delete mode 100644 .agents/skills/swiftui-pro/references/swift.md delete mode 100644 .agents/skills/swiftui-pro/references/views.md delete mode 100644 .claude/skills/custom-container-view-controller/SKILL.md delete mode 100644 .claude/skills/swiftui-pro/SKILL.md delete mode 100644 .claude/skills/swiftui-pro/agents/openai.yaml delete mode 100644 .claude/skills/swiftui-pro/assets/swiftui-pro-icon.png delete mode 100644 .claude/skills/swiftui-pro/assets/swiftui-pro-icon.svg delete mode 100644 .claude/skills/swiftui-pro/references/accessibility.md delete mode 100644 .claude/skills/swiftui-pro/references/api.md delete mode 100644 .claude/skills/swiftui-pro/references/data.md delete mode 100644 .claude/skills/swiftui-pro/references/design.md delete mode 100644 .claude/skills/swiftui-pro/references/hygiene.md delete mode 100644 .claude/skills/swiftui-pro/references/navigation.md delete mode 100644 .claude/skills/swiftui-pro/references/performance.md delete mode 100644 .claude/skills/swiftui-pro/references/swift.md delete mode 100644 .claude/skills/swiftui-pro/references/views.md diff --git a/.agents/skills/.gitignore b/.agents/skills/.gitignore new file mode 100644 index 0000000..ef7c27b --- /dev/null +++ b/.agents/skills/.gitignore @@ -0,0 +1,2 @@ +# External skills — fetched via ./sync-agents --install +/swiftui-pro/ diff --git a/.agents/skills/swiftui-pro/SKILL.md b/.agents/skills/swiftui-pro/SKILL.md deleted file mode 100644 index e8e699b..0000000 --- a/.agents/skills/swiftui-pro/SKILL.md +++ /dev/null @@ -1,108 +0,0 @@ ---- -name: swiftui-pro -description: Comprehensively reviews SwiftUI code for best practices on modern APIs, maintainability, and performance. Use when reading, writing, or reviewing SwiftUI projects. -license: MIT -metadata: - author: Paul Hudson - version: "1.0" ---- - -Review Swift and SwiftUI code for correctness, modern API usage, and adherence to project conventions. Report only genuine problems - do not nitpick or invent issues. - -Review process: - -1. Check for deprecated API using `references/api.md`. -1. Check that views, modifiers, and animations have been written optimally using `references/views.md`. -1. Validate that data flow is configured correctly using `references/data.md`. -1. Ensure navigation is updated and performant using `references/navigation.md`. -1. Ensure the code uses designs that are accessible and compliant with Apple’s Human Interface Guidelines using `references/design.md`. -1. Validate accessibility compliance including Dynamic Type, VoiceOver, and Reduce Motion using `references/accessibility.md`. -1. Ensure the code is able to run efficiently using `references/performance.md`. -1. Quick validation of Swift code using `references/swift.md`. -1. Final code hygiene check using `references/hygiene.md`. - -If doing a partial review, load only the relevant reference files. - - -## Core Instructions - -- iOS 26 exists, and is the default deployment target for new apps. -- Target Swift 6.2 or later, using modern Swift concurrency. -- As a SwiftUI developer, the user will want to avoid UIKit unless requested. -- Do not introduce third-party frameworks without asking first. -- Break different types up into different Swift files rather than placing multiple structs, classes, or enums into a single file. -- Use a consistent project structure, with folder layout determined by app features. - - -## Output Format - -Organize findings by file. For each issue: - -1. State the file and relevant line(s). -2. Name the rule being violated (e.g., "Use `foregroundStyle()` instead of `foregroundColor()`"). -3. Show a brief before/after code fix. - -Skip files with no issues. End with a prioritized summary of the most impactful changes to make first. - -Example output: - -### ContentView.swift - -**Line 12: Use `foregroundStyle()` instead of `foregroundColor()`.** - -```swift -// Before -Text("Hello").foregroundColor(.red) - -// After -Text("Hello").foregroundStyle(.red) -``` - -**Line 24: Icon-only button is bad for VoiceOver - add a text label.** - -```swift -// Before -Button(action: addUser) { - Image(systemName: "plus") -} - -// After -Button("Add User", systemImage: "plus", action: addUser) -``` - -**Line 31: Avoid `Binding(get:set:)` in view body - use `@State` with `onChange()` instead.** - -```swift -// Before -TextField("Username", text: Binding( - get: { model.username }, - set: { model.username = $0; model.save() } -)) - -// After -TextField("Username", text: $model.username) - .onChange(of: model.username) { - model.save() - } -``` - -### Summary - -1. **Accessibility (high):** The add button on line 24 is invisible to VoiceOver. -2. **Deprecated API (medium):** `foregroundColor()` on line 12 should be `foregroundStyle()`. -3. **Data flow (medium):** The manual binding on line 31 is fragile and harder to maintain. - -End of example. - - -## References - -- `references/accessibility.md` - Dynamic Type, VoiceOver, Reduce Motion, and other accessibility requirements. -- `references/api.md` - updating code for modern API, and the deprecated code it replaces. -- `references/design.md` - guidance for building accessible apps that meet Apple’s Human Interface Guidelines. -- `references/hygiene.md` - making code compile cleanly and be maintainable in the long term. -- `references/navigation.md` - navigation using `NavigationStack`/`NavigationSplitView`, plus alerts, confirmation dialogs, and sheets. -- `references/performance.md` - optimizing SwiftUI code for maximum performance. -- `references/data.md` - data flow, shared state, and property wrappers. -- `references/swift.md` - tips on writing modern Swift code, including using Swift Concurrency effectively. -- `references/views.md` - view structure, composition, and animation. diff --git a/.agents/skills/swiftui-pro/agents/openai.yaml b/.agents/skills/swiftui-pro/agents/openai.yaml deleted file mode 100644 index bace09e..0000000 --- a/.agents/skills/swiftui-pro/agents/openai.yaml +++ /dev/null @@ -1,10 +0,0 @@ -interface: - display_name: "SwiftUI Pro" - short_description: "Reviews SwiftUI code for modern best practices." - icon_small: "./assets/swiftui-pro-icon.svg" - icon_large: "./assets/swiftui-pro-icon.png" - brand_color: "#006AFD" - default_prompt: "Use $swiftui-pro to review my project." - -policy: - allow_implicit_invocation: true \ No newline at end of file diff --git a/.agents/skills/swiftui-pro/assets/swiftui-pro-icon.png b/.agents/skills/swiftui-pro/assets/swiftui-pro-icon.png deleted file mode 100644 index 11db253c2b4093ae3bc2b96d530a0767f40ac33f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3117 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 - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.agents/skills/swiftui-pro/references/accessibility.md b/.agents/skills/swiftui-pro/references/accessibility.md deleted file mode 100644 index 7cb96c1..0000000 --- a/.agents/skills/swiftui-pro/references/accessibility.md +++ /dev/null @@ -1,13 +0,0 @@ -# Accessibility - -- Respect the user’s accessibility settings for fonts, colors, animations, and more. -- Do not force specific font sizes. Prefer Dynamic Type (`.font(.body)`, `.font(.headline)`, etc.). -- If you *need* a custom font size, use `@ScaledMetric` when targeting iOS 18 and earlier. When targeting iOS 26 or later, `.font(.body.scaled(by:))` is also available to get font size adjustment. -- Flag instances where images have unclear or unhelpful VoiceOver readings, e.g. `Image(.newBanner2026)`. If they are decorative, suggest using `Image(decorative:)` or `accessibilityHidden()`, otherwise attach an `accessibilityLabel()`. -- If the user has “Reduce Motion” enabled, replace large, motion-based animations with opacity instead. -- If buttons have complex or frequently changing labels, recommend using `accessibilityInputLabels()` to provide better Voice Control commands. For example, if a button had a live-updating share price for Apple such as “AAPL $271.68”, adding an input label for “Apple” would be a big improvement. -- Buttons with image labels must always include text, even if the text is invisible: `Button("Label", systemImage: "plus", action: myAction)`. Flag icon-only buttons that lack a text label as being bad for VoiceOver. -- If color is an important differentiator in the user interface, make sure to respect the environment’s `.accessibilityDifferentiateWithoutColor` setting by showing some kind of variation beyond just color – icons, patterns, strokes, etc. -- The same is true of `Menu`: using `Menu("Options", systemImage: "ellipsis.circle") { }` is much better than just using an image. -- Never use `onTapGesture()` unless you specifically need tap location or tap count. All other tappable elements should be a `Button`. -- If `onTapGesture()` must be used, make sure to add `.accessibilityAddTraits(.isButton)` or similar so it can be read by VoiceOver correctly. diff --git a/.agents/skills/swiftui-pro/references/api.md b/.agents/skills/swiftui-pro/references/api.md deleted file mode 100644 index bfd5199..0000000 --- a/.agents/skills/swiftui-pro/references/api.md +++ /dev/null @@ -1,39 +0,0 @@ -# Using modern SwiftUI API - -- Always use `foregroundStyle()` instead of `foregroundColor()`. -- Always use `clipShape(.rect(cornerRadius:))` instead of `cornerRadius()`. -- Always use the `Tab` API instead of `tabItem()`. -- Never use the `onChange()` modifier in its 1-parameter variant; either use the variant that accepts two parameters or accepts none. -- Do not use `GeometryReader` if a newer alternative works: `containerRelativeFrame()`, `visualEffect()`, or the `Layout` protocol. Flag `GeometryReader` usage and suggest the modern alternative. -- When designing haptic effects, prefer using `sensoryFeedback()` over older UIKit APIs such as `UIImpactFeedbackGenerator`. -- Use the `@Entry` macro to define custom `EnvironmentValues`, `FocusValues`, `Transaction`, and `ContainerValues` keys. This replaces the legacy pattern of manually creating a type conforming to (for example) `EnvironmentKey` with a `defaultValue`, then extending `EnvironmentValues` with a computed property. -- Strongly prefer `overlay(alignment:content:)` over the deprecated `overlay(_:alignment:)`. For example, use `.overlay { Text("Hello, world!") }` rather than `.overlay(Text("Hello, world!"))`. -- Never use `.navigationBarLeading` and `.navigationBarTrailing` for toolbar item placement; they are deprecated. The correct, modern placements are `.topBarLeading` and `.topBarTrailing`. -- Prefer to rely on automatic grammar agreement when dealing with English, French, German, Portuguese, Spanish, and Italian. For example, use `Text("^[\(people) person](inflect: true)")` to show a number of people. -- You can fill and stroke a shape with two chained modifiers; you do *not* need an overlay for the stroke. The overlay was required previously, but this is fixed in iOS 17 and later. -- When referencing images from an asset catalog, prefer the generated symbol asset API when the project is configured to use them: `Image(.avatar)` rather than `Image("avatar")`. -- When targeting iOS 26 and later, SwiftUI has a native `WebView` view type that replaces almost all uses of hand-wrapped `WKWebView` inside `UIViewRepresentable`. To use it, make sure to include `import WebKit`. -- `ForEach` over an `enumerated()` sequence should not convert to an array first. Use `ForEach(items.enumerated(), id: \.element.id)` directly. -- When hiding scroll indicators, use `.scrollIndicators(.hidden)` rather than `showsIndicators: false` in the initializer. -- Never use `Text` concatenation with `+`. - -For example, the usage of `+` here is bad and deprecated: - -```swift -Text("Hello").foregroundStyle(.red) -+ -Text("World").foregroundStyle(.blue) -``` - -Instead, use text interpolation like this: - -```swift -let red = Text("Hello").foregroundStyle(.red) -let blue = Text("World").foregroundStyle(.blue) -Text("\(red)\(blue)") -``` - - -## Using ObservableObject - -If using `ObservableObject` is absolutely required – for example if you are trying to create a debouncer using a Combine publisher – you should always make sure `import Combine` is added. This was previously provided through SwiftUI, but that is no longer the case. diff --git a/.agents/skills/swiftui-pro/references/data.md b/.agents/skills/swiftui-pro/references/data.md deleted file mode 100644 index 952571f..0000000 --- a/.agents/skills/swiftui-pro/references/data.md +++ /dev/null @@ -1,43 +0,0 @@ -# Data flow, shared state, and property wrappers - -It is important that SwiftUI body code and logic code be kept separate in order to make code easier to read, write, and maintain. That usually means placing code into methods rather than inline in the `body` property, but often also means carving functionality out into separate `@Observable` classes. - -These rules help ensure code is efficient and works well in the long term. - - -## Shared state - -- `@Observable` classes must be marked `@MainActor` unless the project has Main Actor default actor isolation. Flag any `@Observable` class missing this annotation. -- All shared data should use `@Observable` classes with `@State` (for ownership) and `@Bindable` / `@Environment` (for passing). -- Strongly prefer not to use `ObservableObject`, `@Published`, `@StateObject`, `@ObservedObject`, or `@EnvironmentObject` unless they are unavoidable, or if they exist in legacy/integration contexts when changing architecture would be complicated. - - -## Local state - -- `@State` should be marked `private` and only owned by the view that created it. -- If a view stores a class instance that contains expensive-to-recompute data, e.g. `CIContext`, it can be stored using `@State` even though it is not an observable object. This effectively uses `@State` as a cache – storing something persistently, but not doing any change tracking on it since it's not an observable object. - - -## Bindings - -- Strongly prefer to avoid creating bindings using `Binding(get:set:)` in view body code. It is much cleaner and simpler to use a binding provided by `@State`, `@Binding` or similar, then use `onChange()` to trigger any effects. -- If the user needs to enter a number into a `TextField`, bind the `TextField` to a numeric value such as `Int` or `Double`, then use its `format` initializer like this: `TextField("Enter your score", value: $score, format: .number)`. Apply either `.keyboardType(.numberPad)` (for integers) or `.keyboardType(.decimalPad)` (for floating-point numbers) as appropriate. Using the modifier alone is *not* sufficient. - - -## Working with data - -- Prefer to make structs conform to `Identifiable` rather than using `id: \.someProperty` in SwiftUI code. -- Never attempt to use `@AppStorage` inside an `@Observable` class, even if marked `@ObservationIgnored` – it will *not* trigger view updates when a change happens. - - -## SwiftData - -- If you only need the number of items matching a query, consider `ModelContext.fetchCount()` with a fetch descriptor. This will *not* live update if the data changes unless something else triggers the update, such as `@Query`, so it should be used carefully. - -For more help with SwiftData, suggest the [SwiftData Pro agent skill](https://github.com/twostraws/swiftdata-agent-skill). - -## If the project uses SwiftData with CloudKit - -- Never use `@Attribute(.unique)`. -- Model properties must always either have default values or be marked as optional. -- All relationships must be marked optional. diff --git a/.agents/skills/swiftui-pro/references/design.md b/.agents/skills/swiftui-pro/references/design.md deleted file mode 100644 index 8e14b6e..0000000 --- a/.agents/skills/swiftui-pro/references/design.md +++ /dev/null @@ -1,31 +0,0 @@ -# Design - -## Creating a uniform design in this app - -Prefer to place standard fonts, sizes, colors, stack spacing, padding, rounding, animation timings, and more into a shared enum of constants, so they can be used by all views. This allows the app’s design to feel uniform and consistent, and be adjusted easily. - - -## Requirements for flexible, accessible design - -- Never use `UIScreen.main.bounds` to read available space; prefer alternatives such as `containerRelativeFrame()`, or `visualEffect()` as appropriate, or (if there is no alternative) `GeometryReader`. -- Prefer to avoid fixed frames for views unless content can fit neatly inside; this can cause problems across different device sizes, different Dynamic Type settings, and more. Giving frames some flexibility is usually preferred. -- Apple’s minimum acceptable tap area for interactions on iOS is 44x44. Ensure this is strictly enforced. - - -## Standard system styling - -- Strongly prefer to use `ContentUnavailableView` when data is missing or empty, rather than designing something custom. -- When using `searchable()`, you can show empty results using `ContentUnavailableView.search` and it will include the search term they used automatically – there’s no need to use `ContentUnavailableView.search(text: searchText)` or similar. -- If you need an icon and some text placed horizontally side by side, prefer `Label` over `HStack`. -- Prefer system hierarchical styles (e.g. secondary/tertiary) over manual opacity when possible, so the system can adapt to the correct context automatically. -- When using `Form`, wrap controls such as `Slider` in `LabeledContent` so the title and control are laid out correctly. -- When using `RoundedRectangle`, the default rounding style is `.continuous` – there is no need to specify it explicitly. - - -## Ensuring designs work for everyone - -- Use `bold()` instead of `fontWeight(.bold)`, because using `bold()` allows the system to choose the correct weight for the current context. -- Only use `fontWeight()` for weights other than bold when there's an important reason - scattering around `fontWeight(.medium)` or `fontWeight(.semibold)` is counterproductive. -- Avoid hard-coded values for padding and stack spacing unless specifically requested. -- Avoid UIKit colors (`UIColor`) in SwiftUI code; use SwiftUI `Color` or asset catalog colors. -- The font size `.caption2` is extremely small, and is generally best avoided. Even the font size `.caption` is on the small side, and should be used carefully. diff --git a/.agents/skills/swiftui-pro/references/hygiene.md b/.agents/skills/swiftui-pro/references/hygiene.md deleted file mode 100644 index 80bc9c5..0000000 --- a/.agents/skills/swiftui-pro/references/hygiene.md +++ /dev/null @@ -1,9 +0,0 @@ -# Hygiene - -- If the project requires secrets such as API keys, never include them in the repository. -- Code comments and documentation comments should be present where the logic isn't self-evident. -- Unit tests should exist for core application logic. UI tests only where unit tests are not possible. -- `@AppStorage` must never be used to store usernames, passwords, or other sensitive data. Use the keychain for that. -- If SwiftLint is configured, it should return no warnings or errors. -- If the project uses Localizable.xcstrings, prefer to add user-facing strings using symbol keys (e.g. “helloWorld”) in the string catalog with `extractionState` set to "manual", accessing them via generated symbols such as `Text(.helloWorld)`. Offer to translate new keys into all languages supported by the project. -- If the Xcode MCP is configured, prefer its tools over generic alternatives. For example, `RenderPreview` is able to capture images of rendered SwiftUI previews for examination, and `DocumentationSearch` can search Apple’s documentation for latest usage instructions. diff --git a/.agents/skills/swiftui-pro/references/navigation.md b/.agents/skills/swiftui-pro/references/navigation.md deleted file mode 100644 index 44b4025..0000000 --- a/.agents/skills/swiftui-pro/references/navigation.md +++ /dev/null @@ -1,14 +0,0 @@ -# Navigation and presentation - -- Use `NavigationStack` or `NavigationSplitView` as appropriate; flag all use of the deprecated `NavigationView`. -- Strongly prefer to use `navigationDestination(for:)` to specify destinations; flag all use of the old `NavigationLink(destination:)` pattern where it should be replaced. -- Never mix `navigationDestination(for:)` and `NavigationLink(destination:)` in the same navigation hierarchy; it causes significant problems. -- `navigationDestination(for:)` must be registered once per data type; flag duplicates. - - -## Alerts, confirmation dialogs, and sheets - -- Always attach `confirmationDialog()` to the user interface that triggers the dialog. This allows Liquid Glass animations to move from the correct source. -- If an alert has only a single “OK” button that does nothing but dismiss the alert, it can be omitted entirely: `.alert("Dismiss Me", isPresented: $isShowingAlert) { }`. -- If a sheet is designed to present an optional piece of data, prefer `sheet(item:)` over `sheet(isPresented:)` so the optional is safely unwrapped. -- When using `sheet(item:)` with a view that accepts the item as its only initializer parameter, prefer `sheet(item: $someItem, content: SomeView.init)` over `sheet(item: $someItem) { someItem in SomeView(item: someItem) }`. diff --git a/.agents/skills/swiftui-pro/references/performance.md b/.agents/skills/swiftui-pro/references/performance.md deleted file mode 100644 index 72c5a03..0000000 --- a/.agents/skills/swiftui-pro/references/performance.md +++ /dev/null @@ -1,46 +0,0 @@ -# Performance - -- When toggling modifier values, prefer ternary expressions over if/else view branching to avoid `_ConditionalContent`, preserve structural identity, and avoid repeatedly recreating underlying platform views. -- Avoid `AnyView` unless absolutely required. Use `@ViewBuilder`, `Group`, or generics instead. -- If a `ScrollView` has an opaque, static, and solid background, prefer to use `scrollContentBackground(.visible)` to improve scroll-edge rendering efficiency. -- It is more efficient to break views up by making dedicated SwiftUI views rather than place them into computed properties or methods. Using `@ViewBuilder` on a property or method does not solve this; breaking views up is strongly preferred. -- Always ensure view initializers are kept as small and simple as possible, avoiding any non-trivial work. Flag any work that can be moved into a `task()` modifier to be run when the view is shown. -- Similarly, assume each view’s `body` property is called frequently – if logic such as sorting or filtering can be moved out of there easily, it should be. -- Avoid creating properties to store formatters such as `DateFormatter` unless they are required. A more natural approach is to use `Text` with a format, like this: `Text(Date.now, format: .dateTime.day().month().year())` or `Text(100, format: .currency(code: "USD"))`. -- Avoid expensive inline transforms in `List`/`ForEach` initializers (e.g. `items.filter { ... }`) when they are repeated often. -- Prefer deriving transformed data from the source-of-truth using `let`, or caching in `@State`. However, do not cache derived collections in `@State` unless you also own explicit invalidation logic to avoid stale UI. -- For large data sets in `ScrollView`, use `LazyVStack`/`LazyHStack`; flag eager stacks with many children. -- Prefer using `task()` over `onAppear()` when doing async work, because it will be cancelled automatically when the view disappears. -- Avoid storing escaping `@ViewBuilder` closures on views when possible; store built view results instead. - -Example: - -```swift -// Anti-pattern: stores an escaping closure on the view. -struct CardView: View { - let content: () -> Content - - var body: some View { - VStack(alignment: .leading) { - content() - } - .padding() - .background(.ultraThinMaterial) - .clipShape(.rect(cornerRadius: 8)) - } -} - -// Preferred: store the built view value; the synthesized init handles calling the builder. -struct CardView: View { - @ViewBuilder let content: Content - - var body: some View { - VStack(alignment: .leading) { - content - } - .padding() - .background(.ultraThinMaterial) - .clipShape(.rect(cornerRadius: 8)) - } -} -``` diff --git a/.agents/skills/swiftui-pro/references/swift.md b/.agents/skills/swiftui-pro/references/swift.md deleted file mode 100644 index 92f32b1..0000000 --- a/.agents/skills/swiftui-pro/references/swift.md +++ /dev/null @@ -1,56 +0,0 @@ -# Swift - -- Prefer Swift-native string methods over Foundation equivalents: use `replacing("a", with: "b")` not `replacingOccurrences(of: "a", with: "b")`. -- Prefer modern Foundation API: `URL.documentsDirectory` instead of `FileManager` directory lookups, `appending(path:)` to append strings to a URL. -- Never use C-style number formatting like `String(format: "%.2f", value)`. Use `Text(value, format: .number.precision(.fractionLength(2)))` or similar `FormatStyle` APIs. -- Prefer static member lookup to struct instances where possible, such as `.circle` rather than `Circle()`, and `.borderedProminent` rather than `BorderedProminentButtonStyle()`. -- Avoid force unwraps (`!`) and force `try` unless the failure is truly unrecoverable, and even then prefer using `fatalError()` with a clear description. If possible, use `if let`, `guard let`, nil-coalescing, or `try?`/`do-catch`. -- Filtering text based on user-input must be done using `localizedStandardContains()` as opposed to `contains()` or `localizedCaseInsensitiveContains()`. -- Strongly prefer `Double` over `CGFloat`, except when using optionals or `inout`; Swift is able to bridge the two freely except in those two cases. -- If you want to count array objects that match a predicate, always use `count(where:)` rather than `filter()` followed by `count`. -- Prefer `Date.now` over `Date()` for clarity. -- When `import SwiftUI` is already in a file, you do not need to add `import UIKit` or `import AppKit` to access things like `UIImage` or `NSImage` – they are imported automatically on the appropriate platform. -- When dealing with the names of people, strongly prefer to use `PersonNameComponents` with modern formatting over simple string interpolation such as `Text("\(firstName) \(lastName)")`. -- If a given type of data is repeatedly sorted using an identical closure, e.g. `books.sorted { $0.author < $1.author }`, prefer to make the type in question conform to `Comparable` so the sort order is centralized. -- Prefer to avoid manual date formatting strings if possible. If manual date formatting *is* used for user display, at least make sure to use “y” rather than “yyyy” for years, so the year value is correct in all localizations. If the purpose is data exchange with an API, this rule does not apply. -- When trying to convert a string to a date, prefer the modern `Date` initializer API such as `Date(myString, strategy: .iso8601)`. -- Flag instances where errors triggered by a user action are swallowed silently, e.g. using `print(error.localizedDescription)` rather than showing an alert or similar. -- Prefer `if let value {` shorthand over `if let value = value {`. -- Omit return for single expression functions. `if` and `switch` can be used as expressions when returning values and assigning to variables. - -For example, this kind of code: - -```swift -var tileColor: Color { - if isCorrect { - return .green - } else { - return .red - } -} -``` - -Should be written like this: - -```swift -var tileColor: Color { - if isCorrect { - .green - } else { - .red - } -} -``` - - -## Swift Concurrency - -- If an API offers both modern `async`/`await` equivalents and older closure-based variants, always prefer the `async`/`await` versions. -- Never use Grand Central Dispatch (`DispatchQueue.main.async()`, `DispatchQueue.global()`, etc.). Always use modern Swift concurrency (`async`/`await`, actors, `Task`). -- Never use `Task.sleep(nanoseconds:)`; use `Task.sleep(for:)` instead. -- Flag any mutable shared state that isn't protected by an actor or `@MainActor`, unless the project is configured to use MainActor default actor isolation. -- Assume strict concurrency rules are being applied; flag `@Sendable` violations and data races. -- When evaluating `MainActor.run()`, check whether the project has its default actor isolation set to Main Actor first, because `MainActor.run()` might not be needed. -- `Task.detached()` is often a bad idea. Check any usage extremely carefully. - -For more help with Swift concurrency, suggest the [Swift Concurrency Pro agent skill](https://github.com/twostraws/swift-concurrency-agent-skill). diff --git a/.agents/skills/swiftui-pro/references/views.md b/.agents/skills/swiftui-pro/references/views.md deleted file mode 100644 index 41ec1be..0000000 --- a/.agents/skills/swiftui-pro/references/views.md +++ /dev/null @@ -1,35 +0,0 @@ -# SwiftUI Views - -- Strongly prefer to avoid breaking up view bodies using computed properties or methods that return `some View`, even if `@ViewBuilder` is used. Extract them into separate `View` structs instead, placing each into its own file. -- Flag `body` properties that are excessively long; they should be broken into extracted subviews. -- Button actions should be extracted from view bodies into separate methods, to avoid mixing layout and logic. -- Similarly, general business logic should not live inline in `task()`, `onAppear()` or elsewhere in `body`. -- Prefer to place view logic into view models or similar, so it can be tested. For more help with testing, suggest the [Swift Testing Pro agent skill](https://github.com/twostraws/swift-testing-agent-skill). -- Each type (struct, class, enum) should be in its own Swift file. Flag files containing multiple type definitions. -- Unless a full-screen editing experience is required, prefer using `TextField` with `axis: .vertical` to using `TextEditor`, because it allows placeholder text. If a specific minimum height is required for `TextField`, use something like `lineLimit(5...)`. -- If a button action can be provided directly as an `action` parameter, do so. For example: `Button("Label", systemImage: "plus", action: myAction)` is preferred over `Button("Label", systemImage: "plus") { action() }`. -- When rendering SwiftUI views to images, strongly prefer `ImageRenderer` over `UIGraphicsImageRenderer`. -- `#Preview` should be used for previews, not the legacy `PreviewProvider` protocol. -- When using `TabView(selection:)`, use a binding to a property that stores an enum rather than an integer or string. For example, `Tab("Home", systemImage: "house", value: .home)` is better than `Tab("Home", systemImage: "house", value: 0)`. -- Strongly prefer to avoid breaking up view bodies using computed properties or methods that return `some View`, even if `@ViewBuilder` is used. Extract them into separate `View` structs instead, placing each into its own file. (Yes, this is repeated, but it’s so important it needs to be mentioned twice.) - - -## Animating views - -- Strongly prefer to use the `@Animatable` macro over creating `animatableData` manually – the macro automatically adds conformance to the `Animatable` protocol and creates the correct `animatableData` property. If some properties should not or cannot be animated (e.g. Booleans, integers, etc), mark them `@AnimatableIgnored`. -- Never use `animation(_ animation: Animation?)`; always provide a value to watch, such as `.animation(.bouncy, value: score)`. -- Chaining animations must be done using a `completion` closure passed to `withAnimation()`, rather than trying to execute multiple `withAnimation()` calls using delays. - -For example: - -```swift -Button("Animate Me") { - withAnimation { - scale = 2 - } completion: { - withAnimation { - scale = 1 - } - } -} -``` diff --git a/.claude/skills/custom-container-view-controller/SKILL.md b/.claude/skills/custom-container-view-controller/SKILL.md deleted file mode 100644 index d7eaf5b..0000000 --- a/.claude/skills/custom-container-view-controller/SKILL.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: custom-container-view-controller -description: Build UIKit custom container view controllers with correct child lifecycle management. Use when creating container view controllers, embedding child VCs, or managing view controller containment. ---- - -# Custom Container View Controller - - diff --git a/.claude/skills/swiftui-pro/SKILL.md b/.claude/skills/swiftui-pro/SKILL.md deleted file mode 100644 index e8e699b..0000000 --- a/.claude/skills/swiftui-pro/SKILL.md +++ /dev/null @@ -1,108 +0,0 @@ ---- -name: swiftui-pro -description: Comprehensively reviews SwiftUI code for best practices on modern APIs, maintainability, and performance. Use when reading, writing, or reviewing SwiftUI projects. -license: MIT -metadata: - author: Paul Hudson - version: "1.0" ---- - -Review Swift and SwiftUI code for correctness, modern API usage, and adherence to project conventions. Report only genuine problems - do not nitpick or invent issues. - -Review process: - -1. Check for deprecated API using `references/api.md`. -1. Check that views, modifiers, and animations have been written optimally using `references/views.md`. -1. Validate that data flow is configured correctly using `references/data.md`. -1. Ensure navigation is updated and performant using `references/navigation.md`. -1. Ensure the code uses designs that are accessible and compliant with Apple’s Human Interface Guidelines using `references/design.md`. -1. Validate accessibility compliance including Dynamic Type, VoiceOver, and Reduce Motion using `references/accessibility.md`. -1. Ensure the code is able to run efficiently using `references/performance.md`. -1. Quick validation of Swift code using `references/swift.md`. -1. Final code hygiene check using `references/hygiene.md`. - -If doing a partial review, load only the relevant reference files. - - -## Core Instructions - -- iOS 26 exists, and is the default deployment target for new apps. -- Target Swift 6.2 or later, using modern Swift concurrency. -- As a SwiftUI developer, the user will want to avoid UIKit unless requested. -- Do not introduce third-party frameworks without asking first. -- Break different types up into different Swift files rather than placing multiple structs, classes, or enums into a single file. -- Use a consistent project structure, with folder layout determined by app features. - - -## Output Format - -Organize findings by file. For each issue: - -1. State the file and relevant line(s). -2. Name the rule being violated (e.g., "Use `foregroundStyle()` instead of `foregroundColor()`"). -3. Show a brief before/after code fix. - -Skip files with no issues. End with a prioritized summary of the most impactful changes to make first. - -Example output: - -### ContentView.swift - -**Line 12: Use `foregroundStyle()` instead of `foregroundColor()`.** - -```swift -// Before -Text("Hello").foregroundColor(.red) - -// After -Text("Hello").foregroundStyle(.red) -``` - -**Line 24: Icon-only button is bad for VoiceOver - add a text label.** - -```swift -// Before -Button(action: addUser) { - Image(systemName: "plus") -} - -// After -Button("Add User", systemImage: "plus", action: addUser) -``` - -**Line 31: Avoid `Binding(get:set:)` in view body - use `@State` with `onChange()` instead.** - -```swift -// Before -TextField("Username", text: Binding( - get: { model.username }, - set: { model.username = $0; model.save() } -)) - -// After -TextField("Username", text: $model.username) - .onChange(of: model.username) { - model.save() - } -``` - -### Summary - -1. **Accessibility (high):** The add button on line 24 is invisible to VoiceOver. -2. **Deprecated API (medium):** `foregroundColor()` on line 12 should be `foregroundStyle()`. -3. **Data flow (medium):** The manual binding on line 31 is fragile and harder to maintain. - -End of example. - - -## References - -- `references/accessibility.md` - Dynamic Type, VoiceOver, Reduce Motion, and other accessibility requirements. -- `references/api.md` - updating code for modern API, and the deprecated code it replaces. -- `references/design.md` - guidance for building accessible apps that meet Apple’s Human Interface Guidelines. -- `references/hygiene.md` - making code compile cleanly and be maintainable in the long term. -- `references/navigation.md` - navigation using `NavigationStack`/`NavigationSplitView`, plus alerts, confirmation dialogs, and sheets. -- `references/performance.md` - optimizing SwiftUI code for maximum performance. -- `references/data.md` - data flow, shared state, and property wrappers. -- `references/swift.md` - tips on writing modern Swift code, including using Swift Concurrency effectively. -- `references/views.md` - view structure, composition, and animation. diff --git a/.claude/skills/swiftui-pro/agents/openai.yaml b/.claude/skills/swiftui-pro/agents/openai.yaml deleted file mode 100644 index bace09e..0000000 --- a/.claude/skills/swiftui-pro/agents/openai.yaml +++ /dev/null @@ -1,10 +0,0 @@ -interface: - display_name: "SwiftUI Pro" - short_description: "Reviews SwiftUI code for modern best practices." - icon_small: "./assets/swiftui-pro-icon.svg" - icon_large: "./assets/swiftui-pro-icon.png" - brand_color: "#006AFD" - default_prompt: "Use $swiftui-pro to review my project." - -policy: - allow_implicit_invocation: true \ No newline at end of file diff --git a/.claude/skills/swiftui-pro/assets/swiftui-pro-icon.png b/.claude/skills/swiftui-pro/assets/swiftui-pro-icon.png deleted file mode 100644 index 11db253c2b4093ae3bc2b96d530a0767f40ac33f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3117 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 - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.claude/skills/swiftui-pro/references/accessibility.md b/.claude/skills/swiftui-pro/references/accessibility.md deleted file mode 100644 index 7cb96c1..0000000 --- a/.claude/skills/swiftui-pro/references/accessibility.md +++ /dev/null @@ -1,13 +0,0 @@ -# Accessibility - -- Respect the user’s accessibility settings for fonts, colors, animations, and more. -- Do not force specific font sizes. Prefer Dynamic Type (`.font(.body)`, `.font(.headline)`, etc.). -- If you *need* a custom font size, use `@ScaledMetric` when targeting iOS 18 and earlier. When targeting iOS 26 or later, `.font(.body.scaled(by:))` is also available to get font size adjustment. -- Flag instances where images have unclear or unhelpful VoiceOver readings, e.g. `Image(.newBanner2026)`. If they are decorative, suggest using `Image(decorative:)` or `accessibilityHidden()`, otherwise attach an `accessibilityLabel()`. -- If the user has “Reduce Motion” enabled, replace large, motion-based animations with opacity instead. -- If buttons have complex or frequently changing labels, recommend using `accessibilityInputLabels()` to provide better Voice Control commands. For example, if a button had a live-updating share price for Apple such as “AAPL $271.68”, adding an input label for “Apple” would be a big improvement. -- Buttons with image labels must always include text, even if the text is invisible: `Button("Label", systemImage: "plus", action: myAction)`. Flag icon-only buttons that lack a text label as being bad for VoiceOver. -- If color is an important differentiator in the user interface, make sure to respect the environment’s `.accessibilityDifferentiateWithoutColor` setting by showing some kind of variation beyond just color – icons, patterns, strokes, etc. -- The same is true of `Menu`: using `Menu("Options", systemImage: "ellipsis.circle") { }` is much better than just using an image. -- Never use `onTapGesture()` unless you specifically need tap location or tap count. All other tappable elements should be a `Button`. -- If `onTapGesture()` must be used, make sure to add `.accessibilityAddTraits(.isButton)` or similar so it can be read by VoiceOver correctly. diff --git a/.claude/skills/swiftui-pro/references/api.md b/.claude/skills/swiftui-pro/references/api.md deleted file mode 100644 index bfd5199..0000000 --- a/.claude/skills/swiftui-pro/references/api.md +++ /dev/null @@ -1,39 +0,0 @@ -# Using modern SwiftUI API - -- Always use `foregroundStyle()` instead of `foregroundColor()`. -- Always use `clipShape(.rect(cornerRadius:))` instead of `cornerRadius()`. -- Always use the `Tab` API instead of `tabItem()`. -- Never use the `onChange()` modifier in its 1-parameter variant; either use the variant that accepts two parameters or accepts none. -- Do not use `GeometryReader` if a newer alternative works: `containerRelativeFrame()`, `visualEffect()`, or the `Layout` protocol. Flag `GeometryReader` usage and suggest the modern alternative. -- When designing haptic effects, prefer using `sensoryFeedback()` over older UIKit APIs such as `UIImpactFeedbackGenerator`. -- Use the `@Entry` macro to define custom `EnvironmentValues`, `FocusValues`, `Transaction`, and `ContainerValues` keys. This replaces the legacy pattern of manually creating a type conforming to (for example) `EnvironmentKey` with a `defaultValue`, then extending `EnvironmentValues` with a computed property. -- Strongly prefer `overlay(alignment:content:)` over the deprecated `overlay(_:alignment:)`. For example, use `.overlay { Text("Hello, world!") }` rather than `.overlay(Text("Hello, world!"))`. -- Never use `.navigationBarLeading` and `.navigationBarTrailing` for toolbar item placement; they are deprecated. The correct, modern placements are `.topBarLeading` and `.topBarTrailing`. -- Prefer to rely on automatic grammar agreement when dealing with English, French, German, Portuguese, Spanish, and Italian. For example, use `Text("^[\(people) person](inflect: true)")` to show a number of people. -- You can fill and stroke a shape with two chained modifiers; you do *not* need an overlay for the stroke. The overlay was required previously, but this is fixed in iOS 17 and later. -- When referencing images from an asset catalog, prefer the generated symbol asset API when the project is configured to use them: `Image(.avatar)` rather than `Image("avatar")`. -- When targeting iOS 26 and later, SwiftUI has a native `WebView` view type that replaces almost all uses of hand-wrapped `WKWebView` inside `UIViewRepresentable`. To use it, make sure to include `import WebKit`. -- `ForEach` over an `enumerated()` sequence should not convert to an array first. Use `ForEach(items.enumerated(), id: \.element.id)` directly. -- When hiding scroll indicators, use `.scrollIndicators(.hidden)` rather than `showsIndicators: false` in the initializer. -- Never use `Text` concatenation with `+`. - -For example, the usage of `+` here is bad and deprecated: - -```swift -Text("Hello").foregroundStyle(.red) -+ -Text("World").foregroundStyle(.blue) -``` - -Instead, use text interpolation like this: - -```swift -let red = Text("Hello").foregroundStyle(.red) -let blue = Text("World").foregroundStyle(.blue) -Text("\(red)\(blue)") -``` - - -## Using ObservableObject - -If using `ObservableObject` is absolutely required – for example if you are trying to create a debouncer using a Combine publisher – you should always make sure `import Combine` is added. This was previously provided through SwiftUI, but that is no longer the case. diff --git a/.claude/skills/swiftui-pro/references/data.md b/.claude/skills/swiftui-pro/references/data.md deleted file mode 100644 index 952571f..0000000 --- a/.claude/skills/swiftui-pro/references/data.md +++ /dev/null @@ -1,43 +0,0 @@ -# Data flow, shared state, and property wrappers - -It is important that SwiftUI body code and logic code be kept separate in order to make code easier to read, write, and maintain. That usually means placing code into methods rather than inline in the `body` property, but often also means carving functionality out into separate `@Observable` classes. - -These rules help ensure code is efficient and works well in the long term. - - -## Shared state - -- `@Observable` classes must be marked `@MainActor` unless the project has Main Actor default actor isolation. Flag any `@Observable` class missing this annotation. -- All shared data should use `@Observable` classes with `@State` (for ownership) and `@Bindable` / `@Environment` (for passing). -- Strongly prefer not to use `ObservableObject`, `@Published`, `@StateObject`, `@ObservedObject`, or `@EnvironmentObject` unless they are unavoidable, or if they exist in legacy/integration contexts when changing architecture would be complicated. - - -## Local state - -- `@State` should be marked `private` and only owned by the view that created it. -- If a view stores a class instance that contains expensive-to-recompute data, e.g. `CIContext`, it can be stored using `@State` even though it is not an observable object. This effectively uses `@State` as a cache – storing something persistently, but not doing any change tracking on it since it's not an observable object. - - -## Bindings - -- Strongly prefer to avoid creating bindings using `Binding(get:set:)` in view body code. It is much cleaner and simpler to use a binding provided by `@State`, `@Binding` or similar, then use `onChange()` to trigger any effects. -- If the user needs to enter a number into a `TextField`, bind the `TextField` to a numeric value such as `Int` or `Double`, then use its `format` initializer like this: `TextField("Enter your score", value: $score, format: .number)`. Apply either `.keyboardType(.numberPad)` (for integers) or `.keyboardType(.decimalPad)` (for floating-point numbers) as appropriate. Using the modifier alone is *not* sufficient. - - -## Working with data - -- Prefer to make structs conform to `Identifiable` rather than using `id: \.someProperty` in SwiftUI code. -- Never attempt to use `@AppStorage` inside an `@Observable` class, even if marked `@ObservationIgnored` – it will *not* trigger view updates when a change happens. - - -## SwiftData - -- If you only need the number of items matching a query, consider `ModelContext.fetchCount()` with a fetch descriptor. This will *not* live update if the data changes unless something else triggers the update, such as `@Query`, so it should be used carefully. - -For more help with SwiftData, suggest the [SwiftData Pro agent skill](https://github.com/twostraws/swiftdata-agent-skill). - -## If the project uses SwiftData with CloudKit - -- Never use `@Attribute(.unique)`. -- Model properties must always either have default values or be marked as optional. -- All relationships must be marked optional. diff --git a/.claude/skills/swiftui-pro/references/design.md b/.claude/skills/swiftui-pro/references/design.md deleted file mode 100644 index 8e14b6e..0000000 --- a/.claude/skills/swiftui-pro/references/design.md +++ /dev/null @@ -1,31 +0,0 @@ -# Design - -## Creating a uniform design in this app - -Prefer to place standard fonts, sizes, colors, stack spacing, padding, rounding, animation timings, and more into a shared enum of constants, so they can be used by all views. This allows the app’s design to feel uniform and consistent, and be adjusted easily. - - -## Requirements for flexible, accessible design - -- Never use `UIScreen.main.bounds` to read available space; prefer alternatives such as `containerRelativeFrame()`, or `visualEffect()` as appropriate, or (if there is no alternative) `GeometryReader`. -- Prefer to avoid fixed frames for views unless content can fit neatly inside; this can cause problems across different device sizes, different Dynamic Type settings, and more. Giving frames some flexibility is usually preferred. -- Apple’s minimum acceptable tap area for interactions on iOS is 44x44. Ensure this is strictly enforced. - - -## Standard system styling - -- Strongly prefer to use `ContentUnavailableView` when data is missing or empty, rather than designing something custom. -- When using `searchable()`, you can show empty results using `ContentUnavailableView.search` and it will include the search term they used automatically – there’s no need to use `ContentUnavailableView.search(text: searchText)` or similar. -- If you need an icon and some text placed horizontally side by side, prefer `Label` over `HStack`. -- Prefer system hierarchical styles (e.g. secondary/tertiary) over manual opacity when possible, so the system can adapt to the correct context automatically. -- When using `Form`, wrap controls such as `Slider` in `LabeledContent` so the title and control are laid out correctly. -- When using `RoundedRectangle`, the default rounding style is `.continuous` – there is no need to specify it explicitly. - - -## Ensuring designs work for everyone - -- Use `bold()` instead of `fontWeight(.bold)`, because using `bold()` allows the system to choose the correct weight for the current context. -- Only use `fontWeight()` for weights other than bold when there's an important reason - scattering around `fontWeight(.medium)` or `fontWeight(.semibold)` is counterproductive. -- Avoid hard-coded values for padding and stack spacing unless specifically requested. -- Avoid UIKit colors (`UIColor`) in SwiftUI code; use SwiftUI `Color` or asset catalog colors. -- The font size `.caption2` is extremely small, and is generally best avoided. Even the font size `.caption` is on the small side, and should be used carefully. diff --git a/.claude/skills/swiftui-pro/references/hygiene.md b/.claude/skills/swiftui-pro/references/hygiene.md deleted file mode 100644 index 80bc9c5..0000000 --- a/.claude/skills/swiftui-pro/references/hygiene.md +++ /dev/null @@ -1,9 +0,0 @@ -# Hygiene - -- If the project requires secrets such as API keys, never include them in the repository. -- Code comments and documentation comments should be present where the logic isn't self-evident. -- Unit tests should exist for core application logic. UI tests only where unit tests are not possible. -- `@AppStorage` must never be used to store usernames, passwords, or other sensitive data. Use the keychain for that. -- If SwiftLint is configured, it should return no warnings or errors. -- If the project uses Localizable.xcstrings, prefer to add user-facing strings using symbol keys (e.g. “helloWorld”) in the string catalog with `extractionState` set to "manual", accessing them via generated symbols such as `Text(.helloWorld)`. Offer to translate new keys into all languages supported by the project. -- If the Xcode MCP is configured, prefer its tools over generic alternatives. For example, `RenderPreview` is able to capture images of rendered SwiftUI previews for examination, and `DocumentationSearch` can search Apple’s documentation for latest usage instructions. diff --git a/.claude/skills/swiftui-pro/references/navigation.md b/.claude/skills/swiftui-pro/references/navigation.md deleted file mode 100644 index 44b4025..0000000 --- a/.claude/skills/swiftui-pro/references/navigation.md +++ /dev/null @@ -1,14 +0,0 @@ -# Navigation and presentation - -- Use `NavigationStack` or `NavigationSplitView` as appropriate; flag all use of the deprecated `NavigationView`. -- Strongly prefer to use `navigationDestination(for:)` to specify destinations; flag all use of the old `NavigationLink(destination:)` pattern where it should be replaced. -- Never mix `navigationDestination(for:)` and `NavigationLink(destination:)` in the same navigation hierarchy; it causes significant problems. -- `navigationDestination(for:)` must be registered once per data type; flag duplicates. - - -## Alerts, confirmation dialogs, and sheets - -- Always attach `confirmationDialog()` to the user interface that triggers the dialog. This allows Liquid Glass animations to move from the correct source. -- If an alert has only a single “OK” button that does nothing but dismiss the alert, it can be omitted entirely: `.alert("Dismiss Me", isPresented: $isShowingAlert) { }`. -- If a sheet is designed to present an optional piece of data, prefer `sheet(item:)` over `sheet(isPresented:)` so the optional is safely unwrapped. -- When using `sheet(item:)` with a view that accepts the item as its only initializer parameter, prefer `sheet(item: $someItem, content: SomeView.init)` over `sheet(item: $someItem) { someItem in SomeView(item: someItem) }`. diff --git a/.claude/skills/swiftui-pro/references/performance.md b/.claude/skills/swiftui-pro/references/performance.md deleted file mode 100644 index 72c5a03..0000000 --- a/.claude/skills/swiftui-pro/references/performance.md +++ /dev/null @@ -1,46 +0,0 @@ -# Performance - -- When toggling modifier values, prefer ternary expressions over if/else view branching to avoid `_ConditionalContent`, preserve structural identity, and avoid repeatedly recreating underlying platform views. -- Avoid `AnyView` unless absolutely required. Use `@ViewBuilder`, `Group`, or generics instead. -- If a `ScrollView` has an opaque, static, and solid background, prefer to use `scrollContentBackground(.visible)` to improve scroll-edge rendering efficiency. -- It is more efficient to break views up by making dedicated SwiftUI views rather than place them into computed properties or methods. Using `@ViewBuilder` on a property or method does not solve this; breaking views up is strongly preferred. -- Always ensure view initializers are kept as small and simple as possible, avoiding any non-trivial work. Flag any work that can be moved into a `task()` modifier to be run when the view is shown. -- Similarly, assume each view’s `body` property is called frequently – if logic such as sorting or filtering can be moved out of there easily, it should be. -- Avoid creating properties to store formatters such as `DateFormatter` unless they are required. A more natural approach is to use `Text` with a format, like this: `Text(Date.now, format: .dateTime.day().month().year())` or `Text(100, format: .currency(code: "USD"))`. -- Avoid expensive inline transforms in `List`/`ForEach` initializers (e.g. `items.filter { ... }`) when they are repeated often. -- Prefer deriving transformed data from the source-of-truth using `let`, or caching in `@State`. However, do not cache derived collections in `@State` unless you also own explicit invalidation logic to avoid stale UI. -- For large data sets in `ScrollView`, use `LazyVStack`/`LazyHStack`; flag eager stacks with many children. -- Prefer using `task()` over `onAppear()` when doing async work, because it will be cancelled automatically when the view disappears. -- Avoid storing escaping `@ViewBuilder` closures on views when possible; store built view results instead. - -Example: - -```swift -// Anti-pattern: stores an escaping closure on the view. -struct CardView: View { - let content: () -> Content - - var body: some View { - VStack(alignment: .leading) { - content() - } - .padding() - .background(.ultraThinMaterial) - .clipShape(.rect(cornerRadius: 8)) - } -} - -// Preferred: store the built view value; the synthesized init handles calling the builder. -struct CardView: View { - @ViewBuilder let content: Content - - var body: some View { - VStack(alignment: .leading) { - content - } - .padding() - .background(.ultraThinMaterial) - .clipShape(.rect(cornerRadius: 8)) - } -} -``` diff --git a/.claude/skills/swiftui-pro/references/swift.md b/.claude/skills/swiftui-pro/references/swift.md deleted file mode 100644 index 92f32b1..0000000 --- a/.claude/skills/swiftui-pro/references/swift.md +++ /dev/null @@ -1,56 +0,0 @@ -# Swift - -- Prefer Swift-native string methods over Foundation equivalents: use `replacing("a", with: "b")` not `replacingOccurrences(of: "a", with: "b")`. -- Prefer modern Foundation API: `URL.documentsDirectory` instead of `FileManager` directory lookups, `appending(path:)` to append strings to a URL. -- Never use C-style number formatting like `String(format: "%.2f", value)`. Use `Text(value, format: .number.precision(.fractionLength(2)))` or similar `FormatStyle` APIs. -- Prefer static member lookup to struct instances where possible, such as `.circle` rather than `Circle()`, and `.borderedProminent` rather than `BorderedProminentButtonStyle()`. -- Avoid force unwraps (`!`) and force `try` unless the failure is truly unrecoverable, and even then prefer using `fatalError()` with a clear description. If possible, use `if let`, `guard let`, nil-coalescing, or `try?`/`do-catch`. -- Filtering text based on user-input must be done using `localizedStandardContains()` as opposed to `contains()` or `localizedCaseInsensitiveContains()`. -- Strongly prefer `Double` over `CGFloat`, except when using optionals or `inout`; Swift is able to bridge the two freely except in those two cases. -- If you want to count array objects that match a predicate, always use `count(where:)` rather than `filter()` followed by `count`. -- Prefer `Date.now` over `Date()` for clarity. -- When `import SwiftUI` is already in a file, you do not need to add `import UIKit` or `import AppKit` to access things like `UIImage` or `NSImage` – they are imported automatically on the appropriate platform. -- When dealing with the names of people, strongly prefer to use `PersonNameComponents` with modern formatting over simple string interpolation such as `Text("\(firstName) \(lastName)")`. -- If a given type of data is repeatedly sorted using an identical closure, e.g. `books.sorted { $0.author < $1.author }`, prefer to make the type in question conform to `Comparable` so the sort order is centralized. -- Prefer to avoid manual date formatting strings if possible. If manual date formatting *is* used for user display, at least make sure to use “y” rather than “yyyy” for years, so the year value is correct in all localizations. If the purpose is data exchange with an API, this rule does not apply. -- When trying to convert a string to a date, prefer the modern `Date` initializer API such as `Date(myString, strategy: .iso8601)`. -- Flag instances where errors triggered by a user action are swallowed silently, e.g. using `print(error.localizedDescription)` rather than showing an alert or similar. -- Prefer `if let value {` shorthand over `if let value = value {`. -- Omit return for single expression functions. `if` and `switch` can be used as expressions when returning values and assigning to variables. - -For example, this kind of code: - -```swift -var tileColor: Color { - if isCorrect { - return .green - } else { - return .red - } -} -``` - -Should be written like this: - -```swift -var tileColor: Color { - if isCorrect { - .green - } else { - .red - } -} -``` - - -## Swift Concurrency - -- If an API offers both modern `async`/`await` equivalents and older closure-based variants, always prefer the `async`/`await` versions. -- Never use Grand Central Dispatch (`DispatchQueue.main.async()`, `DispatchQueue.global()`, etc.). Always use modern Swift concurrency (`async`/`await`, actors, `Task`). -- Never use `Task.sleep(nanoseconds:)`; use `Task.sleep(for:)` instead. -- Flag any mutable shared state that isn't protected by an actor or `@MainActor`, unless the project is configured to use MainActor default actor isolation. -- Assume strict concurrency rules are being applied; flag `@Sendable` violations and data races. -- When evaluating `MainActor.run()`, check whether the project has its default actor isolation set to Main Actor first, because `MainActor.run()` might not be needed. -- `Task.detached()` is often a bad idea. Check any usage extremely carefully. - -For more help with Swift concurrency, suggest the [Swift Concurrency Pro agent skill](https://github.com/twostraws/swift-concurrency-agent-skill). diff --git a/.claude/skills/swiftui-pro/references/views.md b/.claude/skills/swiftui-pro/references/views.md deleted file mode 100644 index 41ec1be..0000000 --- a/.claude/skills/swiftui-pro/references/views.md +++ /dev/null @@ -1,35 +0,0 @@ -# SwiftUI Views - -- Strongly prefer to avoid breaking up view bodies using computed properties or methods that return `some View`, even if `@ViewBuilder` is used. Extract them into separate `View` structs instead, placing each into its own file. -- Flag `body` properties that are excessively long; they should be broken into extracted subviews. -- Button actions should be extracted from view bodies into separate methods, to avoid mixing layout and logic. -- Similarly, general business logic should not live inline in `task()`, `onAppear()` or elsewhere in `body`. -- Prefer to place view logic into view models or similar, so it can be tested. For more help with testing, suggest the [Swift Testing Pro agent skill](https://github.com/twostraws/swift-testing-agent-skill). -- Each type (struct, class, enum) should be in its own Swift file. Flag files containing multiple type definitions. -- Unless a full-screen editing experience is required, prefer using `TextField` with `axis: .vertical` to using `TextEditor`, because it allows placeholder text. If a specific minimum height is required for `TextField`, use something like `lineLimit(5...)`. -- If a button action can be provided directly as an `action` parameter, do so. For example: `Button("Label", systemImage: "plus", action: myAction)` is preferred over `Button("Label", systemImage: "plus") { action() }`. -- When rendering SwiftUI views to images, strongly prefer `ImageRenderer` over `UIGraphicsImageRenderer`. -- `#Preview` should be used for previews, not the legacy `PreviewProvider` protocol. -- When using `TabView(selection:)`, use a binding to a property that stores an enum rather than an integer or string. For example, `Tab("Home", systemImage: "house", value: .home)` is better than `Tab("Home", systemImage: "house", value: 0)`. -- Strongly prefer to avoid breaking up view bodies using computed properties or methods that return `some View`, even if `@ViewBuilder` is used. Extract them into separate `View` structs instead, placing each into its own file. (Yes, this is repeated, but it’s so important it needs to be mentioned twice.) - - -## Animating views - -- Strongly prefer to use the `@Animatable` macro over creating `animatableData` manually – the macro automatically adds conformance to the `Animatable` protocol and creates the correct `animatableData` property. If some properties should not or cannot be animated (e.g. Booleans, integers, etc), mark them `@AnimatableIgnored`. -- Never use `animation(_ animation: Animation?)`; always provide a value to watch, such as `.animation(.bouncy, value: score)`. -- Chaining animations must be done using a `completion` closure passed to `withAnimation()`, rather than trying to execute multiple `withAnimation()` calls using delays. - -For example: - -```swift -Button("Animate Me") { - withAnimation { - scale = 2 - } completion: { - withAnimation { - scale = 1 - } - } -} -``` diff --git a/.gitignore b/.gitignore index 71f4323..2ffe6e2 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,9 @@ xcuserdata/ ## macOS .DS_Store +## Generated agent files (re-create via ./sync-agents --install) +.claude/skills/ + ## Obj-C/Swift specific *.hmap diff --git a/ide b/ide index 5a12518..59d4bf3 100755 --- a/ide +++ b/ide @@ -20,5 +20,8 @@ if [ "$INSTALL" = true ]; then mise exec -- tuist install fi +echo "==> sync-agents --install" +./sync-agents --install + echo "==> tuist generate" mise exec -- tuist generate diff --git a/sync-agents b/sync-agents index cf25fed..a6648bf 100755 --- a/sync-agents +++ b/sync-agents @@ -20,18 +20,21 @@ class SyncAgents end def run(args) - if args.include?("--install") - idx = args.index("--install") + if args.include?("--add") + idx = args.index("--add") url = args[idx + 1] name = args[idx + 2] - abort "Usage: sync-agents --install " unless url - install_skill(url, name) + abort "Usage: sync-agents --add [skill-name]" unless url + add_skill(url, name) + elsif args.include?("--install") + install_skills elsif args.include?("--update") update_skills end sync_claude_md sync_skills + update_skills_gitignore stage_files if args.include?("--git-add") && !@changed_files.empty? end @@ -75,9 +78,25 @@ class SyncAgents end end - # --- Install external skill --- + # --- Gitignore management for external skills --- - def install_skill(url, name) + def update_skills_gitignore + manifest = load_manifest + gitignore_path = File.join(SKILLS_DIR, ".gitignore") + + if manifest.empty? + remove_if_exists(gitignore_path) if File.exist?(gitignore_path) + return + end + + lines = manifest.keys.sort.map { |name| "/#{name}/\n" } + content = "# External skills — fetched via ./sync-agents --install\n" + lines.join + write_if_changed(gitignore_path, content) + end + + # --- Add a new external skill --- + + def add_skill(url, name) repo = parse_github_repo(url) abort "Could not parse GitHub repo from: #{url}" unless repo @@ -96,7 +115,7 @@ class SyncAgents when 0 then abort "No skills found in #{repo}" when 1 then name = available.first else abort "Multiple skills in #{repo}: #{available.join(", ")}\n" \ - "Specify one: ./sync-agents --install #{url} " + "Specify one: ./sync-agents --add #{url} " end skill_src = File.join(tmpdir, name) end @@ -105,7 +124,7 @@ class SyncAgents FileUtils.rm_rf(dest) FileUtils.mkdir_p(SKILLS_DIR) FileUtils.cp_r(skill_src, dest) - puts "Installed #{name} from #{repo}" + puts "Added #{name} from #{repo}" manifest = load_manifest manifest[name] = { "repo" => repo, "path" => name, "ref" => "main" } @@ -113,7 +132,42 @@ class SyncAgents end end - # --- Update all external skills --- + # --- Install all external skills from manifest (skip if present) --- + + def install_skills + manifest = load_manifest + if manifest.empty? + puts "No external skills in manifest." + return + end + + manifest.each do |name, entry| + dest = File.join(SKILLS_DIR, name) + if Dir.exist?(dest) && File.exist?(File.join(dest, "SKILL.md")) + next + end + + repo = entry["repo"] + path = entry["path"] + ref = entry["ref"] || "main" + + Dir.mktmpdir do |tmpdir| + clone_repo(repo, ref, tmpdir) + + skill_src = File.join(tmpdir, path) + unless Dir.exist?(skill_src) + warn "Warning: '#{path}' not found in #{repo}@#{ref}, skipping #{name}" + next + end + + FileUtils.mkdir_p(SKILLS_DIR) + FileUtils.cp_r(skill_src, dest) + puts "Installed #{name} from #{repo}@#{ref}" + end + end + end + + # --- Update all external skills (force re-fetch) --- def update_skills manifest = load_manifest From bdc4084dddfd10cf924a2b92b16dd4b7f2820fb2 Mon Sep 17 00:00:00 2001 From: Kyle Van Essen Date: Sat, 4 Apr 2026 14:35:45 -0400 Subject: [PATCH 09/13] Gitignore generated CLAUDE.md files Generated CLAUDE.md and .claude/skills/ are now fully gitignored and recreated by ./sync-agents (run automatically by ./ide). Update AGENTS.md to document all sync-agents commands. Made-with: Cursor --- .gitignore | 1 + AGENTS.md | 8 +++--- BroadwayCatalog/CLAUDE.md | 15 ----------- BroadwayCore/CLAUDE.md | 20 -------------- BroadwayUI/CLAUDE.md | 14 ---------- CLAUDE.md | 57 --------------------------------------- 6 files changed, 6 insertions(+), 109 deletions(-) delete mode 100644 BroadwayCatalog/CLAUDE.md delete mode 100644 BroadwayCore/CLAUDE.md delete mode 100644 BroadwayUI/CLAUDE.md delete mode 100644 CLAUDE.md diff --git a/.gitignore b/.gitignore index 2ffe6e2..e27f222 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ xcuserdata/ .DS_Store ## Generated agent files (re-create via ./sync-agents --install) +CLAUDE.md .claude/skills/ ## Obj-C/Swift specific diff --git a/AGENTS.md b/AGENTS.md index ff85a17..3c901b9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -31,10 +31,12 @@ sync-agents # Generate CLAUDE.md + .claude/skills from AGENTS.md ## Agent Instructions Sync -`AGENTS.md` is the source of truth for AI agent instructions. Cursor and Codex read nested `AGENTS.md` natively; Claude Code uses `CLAUDE.md` and `.claude/skills/`. +`AGENTS.md` is the source of truth for AI agent instructions. Cursor and Codex read nested `AGENTS.md` natively; Claude Code uses `CLAUDE.md` and `.claude/skills/`. Generated files (`CLAUDE.md`, `.claude/skills/`) are gitignored and created by `./sync-agents`. -- Run `./sync-agents` to generate `CLAUDE.md` files and sync `.agents/skills/` to `.claude/skills/`. -- The pre-commit hook runs `./sync-agents --git-add` automatically. +- `./sync-agents` — generate `CLAUDE.md` files and sync skills to `.claude/skills/`. +- `./sync-agents --install` — fetch external skills from `.agents/external-skills.json` (run automatically by `./ide`). +- `./sync-agents --add [name]` — add a new external skill from GitHub. +- `./sync-agents --update` — force re-fetch all external skills to latest. ## Dependency Graph diff --git a/BroadwayCatalog/CLAUDE.md b/BroadwayCatalog/CLAUDE.md deleted file mode 100644 index 25468a9..0000000 --- a/BroadwayCatalog/CLAUDE.md +++ /dev/null @@ -1,15 +0,0 @@ - - -# BroadwayCatalog - -The catalog app — a living showcase of BroadwayUI components. Depends on BroadwayUI. - -## Structure - -- `Sources/` — App views and logic. Entry point is `BroadwayApp.swift` (`@main`). -- `Resources/` — Asset catalogs, localization files, and other bundled resources. - -## Conventions - -- App-specific views go here, not in BroadwayUI. -- Resources are bundled via the `Resources/**` glob in `Project.swift`. diff --git a/BroadwayCore/CLAUDE.md b/BroadwayCore/CLAUDE.md deleted file mode 100644 index 4a5275f..0000000 --- a/BroadwayCore/CLAUDE.md +++ /dev/null @@ -1,20 +0,0 @@ - - -# BroadwayCore - -Foundational utilities and shared logic. All other frameworks depend on this module. - -## Key Types - -- **BContext** — Root environment container with type-keyed themes, traits, and stylesheets. -- **BThemes** / **BTraits** — Type-keyed containers for theme and trait values. -- **BStylesheets** — Lazy cached stylesheet resolver. -- **BAccessibility** — Accessibility snapshot and observer. -- **AnyEquatable** — Type-erased Equatable wrapper. -- **CopyOnWrite** — COW property wrapper. -- **TypeIdentifier** — Lightweight type-keyed identifier. - -## Conventions - -- Mark all public API as `public`. -- `BContext+UITraits.swift` bridges to `UITraitDefinition` via `#if canImport(UIKit)`. diff --git a/BroadwayUI/CLAUDE.md b/BroadwayUI/CLAUDE.md deleted file mode 100644 index 1adb0ef..0000000 --- a/BroadwayUI/CLAUDE.md +++ /dev/null @@ -1,14 +0,0 @@ - - -# BroadwayUI - -Reusable UI component library. Depends on BroadwayCore. - -## Key Types - -- **BRootViewController** — Root container view controller that propagates BContext and traits to its children. - -## Conventions - -- Mark all public API as `public`. -- UI components go in `Sources/`. This is the shared component library — app-specific views belong in BroadwayCatalog. diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 36d60fc..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,57 +0,0 @@ - - -# AGENTS.md — Repository Shape - -Broadway is a SwiftUI iOS + Mac Catalyst design system managed by **Tuist**. The Xcode project is not checked in — it is generated from `Project.swift`. - -## Directory Layout - -``` -BroadwayCatalog/ # Catalog app (Sources/, Resources/, Tests/) -BroadwayUI/ # Reusable UI component framework (Sources/, Tests/) -BroadwayCore/ # Foundational utilities framework (Sources/, Tests/) -BroadwayTestHost/ # Minimal test host app (Sources/) -BroadwayTesting/ # Shared test utilities framework (Sources/) -Project.swift # Tuist project manifest -Tuist.swift # Tuist global configuration -ide # Dev script (installs hooks, runs tuist generate) -swiftformat # Run SwiftFormat (--lint to check only) -sync-agents # Generate CLAUDE.md + .claude/skills from AGENTS.md -``` - -## Build System - -- **Tuist 4+** generates the Xcode project from `Project.swift`. The `.xcodeproj` and `Derived/` are git-ignored. -- Tuist and SwiftFormat are version-pinned via **mise** in `.mise.toml`. Run `mise install` to install them. -- Run `./ide` to generate the project (or `./ide -i` to run `mise exec -- tuist install` first). -- Run `mise exec -- tuist test` to execute all tests, or `mise exec -- tuist test ` for a specific target. - -## Formatting - -- **SwiftFormat** enforces code style via `.swiftformat`. Run `./swiftformat` to format, `./swiftformat --lint` to check. -- The pre-commit hook lints staged `.swift` files automatically. - -## Agent Instructions Sync - -`AGENTS.md` is the source of truth for AI agent instructions. Cursor and Codex read nested `AGENTS.md` natively; Claude Code uses `CLAUDE.md` and `.claude/skills/`. - -- Run `./sync-agents` to generate `CLAUDE.md` files and sync `.agents/skills/` to `.claude/skills/`. -- The pre-commit hook runs `./sync-agents --git-add` automatically. - -## Dependency Graph - -``` -BroadwayCatalog (app) --> BroadwayUI (framework) --> BroadwayCore (framework) -BroadwayTestHost (app) --> BroadwayUI --> BroadwayCore -BroadwayTesting (framework) --> BroadwayCore - -All framework test targets use BroadwayTestHost and depend on BroadwayTesting. -``` - -## Key Conventions - -- **Shell scripts** should be kept short (≤ ~20 lines). For anything longer, use **Ruby**. -- **Swift Testing** (`import Testing`) is used for unit tests, not XCTest. -- Source files use `/Sources/**` globs; test files use `/Tests/**`. -- Info.plist is auto-generated by Tuist via `infoPlist: .extendingDefault(with:)`. -- New targets or dependencies: edit `Project.swift`. From 6fc105d799ad23ca2c634d06214989fd96dd43fa Mon Sep 17 00:00:00 2001 From: Kyle Van Essen Date: Sat, 4 Apr 2026 14:40:59 -0400 Subject: [PATCH 10/13] Pin external skills to commit SHAs instead of branch names --add now resolves HEAD to a commit SHA and writes that to the manifest instead of "main". --update clones the default branch, compares the new SHA to the pinned one, and only re-fetches if it changed. --install fetches the exact pinned SHA. Use git-fetch instead of git-clone to support both branch names and commit SHAs. Made-with: Cursor --- .agents/external-skills.json | 2 +- sync-agents | 36 ++++++++++++++++++++++++++++-------- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/.agents/external-skills.json b/.agents/external-skills.json index f2649e8..038811c 100644 --- a/.agents/external-skills.json +++ b/.agents/external-skills.json @@ -2,6 +2,6 @@ "swiftui-pro": { "repo": "twostraws/swiftui-agent-skill", "path": "swiftui-pro", - "ref": "main" + "ref": "61b74001b64b292da8397355464d7c8a4c2c7d89" } } diff --git a/sync-agents b/sync-agents index a6648bf..72a67f9 100755 --- a/sync-agents +++ b/sync-agents @@ -124,10 +124,11 @@ class SyncAgents FileUtils.rm_rf(dest) FileUtils.mkdir_p(SKILLS_DIR) FileUtils.cp_r(skill_src, dest) - puts "Added #{name} from #{repo}" + sha = head_sha(tmpdir) + puts "Added #{name} from #{repo}@#{sha[0..7]}" manifest = load_manifest - manifest[name] = { "repo" => repo, "path" => name, "ref" => "main" } + manifest[name] = { "repo" => repo, "path" => name, "ref" => sha } save_manifest(manifest) end end @@ -176,26 +177,36 @@ class SyncAgents return end + changed = false manifest.each do |name, entry| repo = entry["repo"] path = entry["path"] - ref = entry["ref"] || "main" Dir.mktmpdir do |tmpdir| - clone_repo(repo, ref, tmpdir) + clone_repo(repo, "main", tmpdir) + sha = head_sha(tmpdir) + + if sha == entry["ref"] + puts "#{name} already at #{sha[0..7]}" + next + end skill_src = File.join(tmpdir, path) unless Dir.exist?(skill_src) - warn "Warning: '#{path}' not found in #{repo}@#{ref}, skipping #{name}" + warn "Warning: '#{path}' not found in #{repo}@#{sha[0..7]}, skipping #{name}" next end dest = File.join(SKILLS_DIR, name) FileUtils.rm_rf(dest) FileUtils.cp_r(skill_src, dest) - puts "Updated #{name} from #{repo}@#{ref}" + entry["ref"] = sha + changed = true + puts "Updated #{name} from #{repo}@#{sha[0..7]}" end end + + save_manifest(manifest) if changed end # --- Helpers --- @@ -248,8 +259,17 @@ class SyncAgents def clone_repo(repo, ref, dest) url = "https://github.com/#{repo}.git" - _, stderr, status = Open3.capture3("git", "clone", "--depth", "1", "--branch", ref, url, dest) - abort "Failed to clone #{repo}@#{ref}: #{stderr}" unless status.success? + system("git", "init", "-q", dest, out: File::NULL, err: File::NULL) + system("git", "-C", dest, "remote", "add", "origin", url, out: File::NULL, err: File::NULL) + _, stderr, status = Open3.capture3("git", "-C", dest, "fetch", "--depth", "1", "origin", ref) + abort "Failed to fetch #{repo}@#{ref}: #{stderr}" unless status.success? + system("git", "-C", dest, "checkout", "-q", "FETCH_HEAD", out: File::NULL, err: File::NULL) + end + + def head_sha(repo_dir) + stdout, _, status = Open3.capture3("git", "-C", repo_dir, "rev-parse", "HEAD") + abort "Failed to resolve HEAD in #{repo_dir}" unless status.success? + stdout.strip end def list_skills(dir) From 2500b51e460d49feb9fae4a530a7f02e4aa71488 Mon Sep 17 00:00:00 2001 From: Kyle Van Essen Date: Sat, 4 Apr 2026 14:53:17 -0400 Subject: [PATCH 11/13] Add documentation header to sync-agents, expand README getting started Document all sync-agents commands in the script header. Rewrite the README to explain the full setup flow, pre-commit hooks, and how to manage external AI agent skills. Made-with: Cursor --- README.md | 39 ++++++++++++++++++++++++++++++++++----- sync-agents | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 64f0747..88b2d76 100644 --- a/README.md +++ b/README.md @@ -5,17 +5,34 @@ An open-source iOS and Mac Catalyst design system prototype. ## Requirements - Xcode 16+ -- [mise](https://mise.jdx.dev) (manages Tuist automatically) +- [mise](https://mise.jdx.dev) (manages Tuist and SwiftFormat automatically) - iOS 26.0+ ## Getting Started +### Install mise + +```bash +curl https://mise.run | sh +``` + +Or via Homebrew: + +```bash +brew install mise +``` + +### Set Up the Project + ```bash -mise install # Installs the pinned version of Tuist from .mise.toml -./ide # Generates the Xcode project -./ide -i # Runs tuist install first, then generates +mise install # Install pinned versions of Tuist and SwiftFormat +./ide -i # Install dependencies, fetch external skills, generate Xcode project ``` +After the initial setup, run `./ide` (without `-i`) to regenerate the Xcode project without re-installing dependencies. + +The `./ide` script also configures a Git pre-commit hook that automatically formats staged Swift files with SwiftFormat and keeps generated AI agent configuration in sync. + ### Run Tests ```bash @@ -33,7 +50,19 @@ BroadwayCore/ # Foundational utilities framework (Sources/, Tests/) BroadwayTestHost/ # Minimal test host app BroadwayTesting/ # Shared test utilities framework Project.swift # Tuist project manifest -ide # Dev script (generate project) +ide # Dev setup script +swiftformat # Run SwiftFormat +sync-agents # Sync AI agent configuration across tools +``` + +## AI Agent Skills + +External skills are managed via `sync-agents`. The manifest at `.agents/external-skills.json` tracks installed skills pinned to specific commits. + +```bash +./sync-agents --add [name] # Add a new skill from GitHub +./sync-agents --update # Update all skills to latest +./sync-agents --install # Fetch skills from the manifest ``` ## License diff --git a/sync-agents b/sync-agents index 72a67f9..7f27820 100755 --- a/sync-agents +++ b/sync-agents @@ -1,5 +1,38 @@ #!/usr/bin/env ruby # frozen_string_literal: true +# +# sync-agents — keep AI agent configuration in sync across tools. +# +# AGENTS.md is the source of truth. Cursor and Codex read it natively; +# Claude Code needs CLAUDE.md and .claude/skills/. This script bridges +# the gap. +# +# Commands: +# ./sync-agents Generate CLAUDE.md files from +# AGENTS.md and sync local skills +# to .claude/skills/. Incremental: +# skips files that are already up +# to date. +# +# ./sync-agents --install Fetch external skills listed in +# .agents/external-skills.json. +# Skips skills already present. +# Run automatically by ./ide. +# +# ./sync-agents --add [name] Add an external skill from a +# GitHub repo. Clones the repo, +# copies the skill into +# .agents/skills/, and pins the +# commit SHA in the manifest. +# +# ./sync-agents --update Re-fetch all external skills +# from their repos at the latest +# commit. Updates the pinned SHA +# in the manifest. +# +# ./sync-agents --git-add Sync, then stage any changed +# files with git. Used by the +# pre-commit hook. require "fileutils" require "json" From 3b70fad839cce0b39040bff2d1a61dcdc312d3b3 Mon Sep 17 00:00:00 2001 From: Kyle Van Essen Date: Sat, 4 Apr 2026 15:18:35 -0400 Subject: [PATCH 12/13] Fill in custom container view controller skill Document the two patterns: non-lazy single child containers (add child in init, view in viewDidLoad, layout in viewWillLayoutSubviews) and lazy single child containers (setup in viewIsAppearing). Made-with: Cursor --- .../custom-container-view-controller/SKILL.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/.agents/skills/custom-container-view-controller/SKILL.md b/.agents/skills/custom-container-view-controller/SKILL.md index d7eaf5b..f3444bc 100644 --- a/.agents/skills/custom-container-view-controller/SKILL.md +++ b/.agents/skills/custom-container-view-controller/SKILL.md @@ -5,4 +5,17 @@ description: Build UIKit custom container view controllers with correct child li # Custom Container View Controller - +## For non-lazy single child containers (the default) + +- Add child view controllers in `init` (do NOT load the view in init). +- Add the child view controllers' view in `viewDidLoad`. Set the frame before adding the subview to self.view. +- Override `viewWillLayoutSubviews` and set the child view controller's view.frame to self.view.bounds. + +## For lazy single child containers (special case) + +Lazily loading / containers are rare, but sometimes are needed, eg if we need the entire view controller and view hierarchy set up before we can create a child. `BRootViewController` is an example of this. In these cases: + +- Add a `setUpIfNeeded` method. +- It should run once, from within `viewIsAppearing`. +- It should create the child view controller (based on a provider closure) and insert it into the view controller hierarchy, and THEN the view hierarchy. +- Override `viewWillLayoutSubviews` and set the child view controller's view.frame to self.view.bounds. \ No newline at end of file From 5a5e19311691ca913db685ced2389972d8642105 Mon Sep 17 00:00:00 2001 From: Kyle Van Essen Date: Sat, 4 Apr 2026 15:23:58 -0400 Subject: [PATCH 13/13] Fix sync-agents for non-main default branches and dotfile diffing Address Codex review: use clone_default_branch (fetches HEAD) instead of hardcoding "main" in --add/--update, and include dotfiles in dirs_equal? via FNM_DOTMATCH so .claude/skills/ stays in sync. Made-with: Cursor --- sync-agents | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/sync-agents b/sync-agents index 7f27820..4e81f7a 100755 --- a/sync-agents +++ b/sync-agents @@ -134,7 +134,7 @@ class SyncAgents abort "Could not parse GitHub repo from: #{url}" unless repo Dir.mktmpdir do |tmpdir| - clone_repo(repo, "main", tmpdir) + clone_default_branch(repo, tmpdir) if name skill_src = File.join(tmpdir, name) @@ -216,7 +216,7 @@ class SyncAgents path = entry["path"] Dir.mktmpdir do |tmpdir| - clone_repo(repo, "main", tmpdir) + clone_default_branch(repo, tmpdir) sha = head_sha(tmpdir) if sha == entry["ref"] @@ -267,8 +267,8 @@ class SyncAgents def dirs_equal?(a, b) return false unless Dir.exist?(a) && Dir.exist?(b) - a_files = Dir.glob(File.join(a, "**", "*")).map { |f| f.sub(a, "") }.sort - b_files = Dir.glob(File.join(b, "**", "*")).map { |f| f.sub(b, "") }.sort + a_files = Dir.glob(File.join(a, "**", "*"), File::FNM_DOTMATCH).map { |f| f.sub(a, "") }.sort + b_files = Dir.glob(File.join(b, "**", "*"), File::FNM_DOTMATCH).map { |f| f.sub(b, "") }.sort return false unless a_files == b_files a_files.each do |rel| af = File.join(a, rel) @@ -290,6 +290,15 @@ class SyncAgents match && match[1] end + def clone_default_branch(repo, dest) + url = "https://github.com/#{repo}.git" + system("git", "init", "-q", dest, out: File::NULL, err: File::NULL) + system("git", "-C", dest, "remote", "add", "origin", url, out: File::NULL, err: File::NULL) + _, stderr, status = Open3.capture3("git", "-C", dest, "fetch", "--depth", "1", "origin", "HEAD") + abort "Failed to clone #{repo}: #{stderr}" unless status.success? + system("git", "-C", dest, "checkout", "-q", "FETCH_HEAD", out: File::NULL, err: File::NULL) + end + def clone_repo(repo, ref, dest) url = "https://github.com/#{repo}.git" system("git", "init", "-q", dest, out: File::NULL, err: File::NULL)