Skip to content

Commit 0487083

Browse files
committed
Chore: Track .claude/plans, CLAUDE.md, openspec in repo
- Whitelist .claude/plans/, .claude/CLAUDE.md, .claude/openspec/ via gitignore negation patterns - Keep session-state.json, settings.local.json, memory.md, whats-next.md, .DS_Store ignored (per-developer / dynamic) - Relocate mouse-support plan from plans/open/ to .claude/plans/open/ to consolidate plan directory - Enables tuikit.dev dashboard to fetch plan metadata via GitHub Contents API (previously returned empty due to ignored .claude/)
1 parent 65750f4 commit 0487083

48 files changed

Lines changed: 10381 additions & 1 deletion

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/CLAUDE.md

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
## RULES
2+
3+
### Compatibility (non-negotiable)
4+
- **Swift 6.0 compatible**: `swift-tools-version: 6.0`. Never use features that require a newer compiler.
5+
- **Cross-platform**: must build and run without crashes/segfaults on both macOS and Linux. CI tests both (`macos-15` + `swift:6.0` container).
6+
- When in doubt, verify with the CI pipeline before merging.
7+
8+
### Pre-Push Verification (non-negotiable)
9+
- **Before pushing to GitHub**: ALWAYS run `./scripts/test-linux.sh` to verify build + tests pass on both macOS and Linux.
10+
- The script runs `swift build` and `swift test` natively on macOS, then repeats both inside a `swift:6.0` Docker container (same image as CI).
11+
- **Never push code that has not been verified on both platforms.**
12+
- Usage: `./scripts/test-linux.sh` (both), `./scripts/test-linux.sh linux` (Linux only), `./scripts/test-linux.sh shell` (interactive Linux shell)
13+
- Requires Docker Desktop to be running.
14+
15+
### Architecture (non-negotiable)
16+
17+
#### General Principles
18+
- No Singletons
19+
- **Before implementing ANYTHING NEW: Search the codebase** for similar patterns, reusable code, existing solutions
20+
- Consolidate and reuse before adding new functions or types
21+
- "Reinventing the wheel" is a code smell: investigate why it exists first
22+
23+
#### Code Reuse Checklist
24+
1. Does a similar feature exist? Use it or extend it
25+
2. Can I reuse a helper function/extension/modifier? Do it
26+
3. Does a pattern already exist? Follow it exactly
27+
4. Am I duplicating logic? Refactor into a shared utility
28+
5. **Never implement features in isolation**: maximize consistency and minimize maintenance burden
29+
30+
### Workflow
31+
- **NEVER merge PRs autonomously**: stop after creating, let user merge
32+
33+
### SwiftUI API Parity (non-negotiable)
34+
Public APIs MUST match SwiftUI signatures exactly unless terminal constraints require deviation (document why in comments).
35+
36+
| Aspect | Requirement |
37+
|--------|-------------|
38+
| Parameter names | Exact (`isPresented`, not `isVisible`) |
39+
| Parameter order | Exact (title, binding, actions, message) |
40+
| Parameter types | Match closely (ViewBuilder closures, not pre-built values) |
41+
| Trailing closures | `@ViewBuilder () -> T`, not `String` |
42+
43+
**Before implementing:** Look up exact SwiftUI signature first.
44+
**TUI-specific APIs:** OK to add, but keep separate from SwiftUI equivalents.
45+
46+
### View Architecture (non-negotiable)
47+
48+
#### Public API: Every control is a View with a real body
49+
50+
**The Rule:**
51+
- Every **public** control MUST be a `View` with a real `body: some View`
52+
- The `body` MUST return actual Views (not `Never`, not `fatalError()`)
53+
- All modifiers MUST propagate through the entire View hierarchy
54+
- Environment values MUST flow down automatically
55+
56+
**Why this matters:**
57+
```swift
58+
// This MUST work exactly like SwiftUI:
59+
List("Items", selection: $selection) {
60+
ForEach(items) { item in
61+
Text(item.name)
62+
}
63+
}
64+
.foregroundColor(.red) // MUST affect all Text inside!
65+
.disabled(true) // MUST disable the entire List!
66+
```
67+
68+
#### Renderable: When and where it is allowed
69+
70+
Terminal UI requires procedural buffer assembly (ANSI codes, Unicode borders,
71+
buffer overlays). `Renderable` is the mechanism for this. It is allowed in
72+
these cases:
73+
74+
| Layer | Example | Renderable? |
75+
|-------|---------|-------------|
76+
| **Leaf nodes** | `Text`, `Spacer`, `Divider` | Yes (terminal primitives) |
77+
| **Private `_*Core` views** | `_ButtonCore`, `_VStackCore` | Yes (procedural ANSI rendering) |
78+
| **Layout primitives** | `_VStackCore`, `_HStackCore` | Yes + `Layoutable` (two-pass layout) |
79+
| **Modifier infrastructure** | `ModifiedView`, `EnvironmentModifier` | Yes (context/buffer pipeline) |
80+
| **Public controls** | `Button`, `VStack`, `List` | **No** (must use `body: some View`) |
81+
82+
**The `_*Core` pattern:**
83+
```swift
84+
// Public View: real body, environment flows through
85+
public struct MyControl<Content: View>: View {
86+
let content: Content
87+
88+
public var body: some View {
89+
_MyControlCore(content: content)
90+
}
91+
}
92+
93+
// Private Core: Renderable for terminal-specific rendering
94+
private struct _MyControlCore<Content: View>: View, Renderable {
95+
let content: Content
96+
var body: Never { fatalError("_MyControlCore renders via Renderable") }
97+
98+
func renderToBuffer(context: RenderContext) -> FrameBuffer {
99+
// Read environment from context, render with ANSI codes
100+
}
101+
}
102+
```
103+
104+
**Preferred: Pure composition (Box.swift is the reference):**
105+
```swift
106+
public struct MyControl<Content: View>: View {
107+
let content: Content
108+
109+
public var body: some View {
110+
content
111+
.padding()
112+
.border()
113+
}
114+
}
115+
```
116+
117+
When possible, prefer composition over `_*Core`. Use `_*Core` + `Renderable`
118+
only when the rendering requires procedural buffer manipulation that cannot
119+
be expressed as View composition.
120+
121+
**WRONG Pattern (public control with Renderable):**
122+
```swift
123+
public struct MyControl: View {
124+
public var body: Never { fatalError() } // WRONG!
125+
}
126+
127+
extension MyControl: Renderable { // WRONG - public types must not be Renderable!
128+
func renderToBuffer() { ... }
129+
}
130+
```
131+
132+
**Before implementing ANY control:**
133+
1. Can it be composed from existing Views + modifiers? (preferred)
134+
2. If not, does the public View have a real `body` wrapping a private `_*Core`?
135+
3. Does `_*Core` read environment values from `RenderContext`?
136+
4. Test: `.foregroundColor()` on the control affects its content?
137+
5. Test: `.disabled()` on the control disables interactions?
138+
139+
### Interactive Views: Focus & State (non-negotiable)
140+
141+
All interactive views (Button, TextField, Toggle, Slider, etc.) that participate
142+
in the focus system MUST follow these rules:
143+
144+
#### FocusID generation
145+
- Default focusIDs MUST use `context.identity.path`, never user-facing data
146+
- Pattern: `"\(prefix)-\(context.identity.path)"` (e.g. `"button-\(context.identity.path)"`)
147+
- Never use label text, titles, or other user content for focusIDs (collision risk)
148+
149+
#### Focus registration
150+
- Use the shared `FocusRegistration` helper for all focus setup
151+
- Do NOT duplicate focus registration boilerplate in individual views
152+
- Registration, disabled-state check, and isFocused query are one operation
153+
154+
#### StateStorage property indices
155+
- Every `_*Core` view MUST document its property indices with named constants:
156+
```swift
157+
private enum StateIndex {
158+
static let focusID = 0
159+
static let handler = 1
160+
}
161+
```
162+
- Never use bare integer literals for `propertyIndex`
163+
164+
#### Disabled state
165+
- Disabled views MUST NOT register with the focus system
166+
- Check `isDisabled` BEFORE calling `focusManager.register()`
167+
- Disabled styling MUST be visually consistent across all interactive views
168+
169+
### SwiftUI API Design (non-negotiable)
170+
171+
#### Init signatures: Keep them minimal
172+
- Public inits MUST match SwiftUI parameter names and order
173+
- TUI-specific options (focusID, emptyPlaceholder, etc.) MUST be modifiers, not init params
174+
- Minimize init overloads; prefer `@ViewBuilder` label variants over String convenience inits
175+
176+
**Correct:**
177+
```swift
178+
List(selection: $selection) { content }
179+
.focusID("my-list")
180+
.listEmptyPlaceholder("No items")
181+
```
182+
183+
**Wrong:**
184+
```swift
185+
List(selection: $selection, focusID: "my-list", emptyPlaceholder: "No items") { content }
186+
```
187+
188+
#### Modifier-first principle
189+
TUI-specific behavior that SwiftUI handles via modifiers MUST also be modifiers:
190+
- Focus identity: `.focusID(_:)`
191+
- Placeholder text: `.listEmptyPlaceholder(_:)`
192+
- Visual customization: `.trackStyle(_:)`, `.buttonStyle(_:)`, etc.
193+
194+
### File Organization
195+
196+
- Source files SHOULD stay under 500 lines
197+
- If a file exceeds 500 lines, consider splitting: public API in one file, `_*Core` in another
198+
- One view per file (do not combine VStack + HStack + ZStack in one file)

0 commit comments

Comments
 (0)