mirror of
https://github.com/phranck/TUIkit.git
synced 2026-05-21 09:50:35 +00:00
Refactor: Fix FocusID collision bugs and standardize disabled-state handling
- Fix FocusID generation in 6 interactive views (Button, TextField, SecureField, Toggle, Slider, Stepper)
* Changed from user-data-based IDs ("button-Label") to nil (auto-generated from context.identity.path)
* Eliminates collision risk when multiple views have same label
* Example: two "Save" buttons now get unique IDs ("button-1.0.0", "button-1.0.1") instead of both "button-Save"
- Audit findings on disabled-state handling:
* All 9 interactive views already use consistent pattern: canBeFocused: !isDisabled
* Disabled color styling already standardized across Button, Toggle, Slider, Stepper, RadioButton
* List and Table lack explicit visual disabled styling (minor - functionality already correct)
- List API already migrated to modifiers:
* .focusID(), .listEmptyPlaceholder(), .listFooterSeparator() all exist
* Init parameters already removed (SwiftUI-compliant)
Changes:
- Sources/TUIkit/Views/Button.swift: focusID: String? init defaults to nil
- Sources/TUIkit/Views/TextField.swift: focusID init defaults to nil
- Sources/TUIkit/Views/SecureField.swift: focusID init defaults to nil
- Sources/TUIkit/Views/Toggle.swift: focusID init defaults to nil
- Sources/TUIkit/Views/Slider.swift: focusID init defaults to nil
- Sources/TUIkit/Views/Stepper.swift: focusID init defaults to nil (3 inits)
- Tests/TUIkitTests/ButtonTests.swift: Update test expectations for nil focusID default
Moved:
- .claude/plans/open/2026-02-10-layout-system-refactor.md → .claude/plans/done/
- Added completion notes: Phases 1-4 complete, 1034+ tests passing
Test Results: 1037/1037 tests passing ✅
This commit is contained in:
@@ -154,7 +154,10 @@ public struct Button: View {
|
||||
let focusedStyle: ButtonStyle
|
||||
|
||||
/// The unique focus identifier.
|
||||
var focusID: String
|
||||
///
|
||||
/// If `nil`, automatically generated from the view's identity path.
|
||||
/// Use the `.focusID()` modifier to override.
|
||||
var focusID: String?
|
||||
|
||||
/// Whether the button is disabled.
|
||||
var isDisabled: Bool
|
||||
@@ -178,8 +181,8 @@ public struct Button: View {
|
||||
self.action = action
|
||||
self.role = nil
|
||||
self.style = style
|
||||
// Use label as default focusID for stability across render cycles
|
||||
self.focusID = "button-\(label)"
|
||||
// Auto-generated focusID from view identity (collision-free)
|
||||
self.focusID = nil
|
||||
self.isDisabled = isDisabled
|
||||
|
||||
// Default focused style: bold version of the normal style
|
||||
@@ -213,7 +216,8 @@ public struct Button: View {
|
||||
self.label = label
|
||||
self.action = action
|
||||
self.role = role
|
||||
self.focusID = "button-\(label)"
|
||||
// Auto-generated focusID from view identity (collision-free)
|
||||
self.focusID = nil
|
||||
self.isDisabled = false
|
||||
|
||||
// Style based on role
|
||||
@@ -260,7 +264,7 @@ private struct _ButtonCore: View, Renderable {
|
||||
let role: ButtonRole?
|
||||
let style: ButtonStyle
|
||||
let focusedStyle: ButtonStyle
|
||||
let focusID: String
|
||||
let focusID: String?
|
||||
let isDisabled: Bool
|
||||
|
||||
var body: Never {
|
||||
|
||||
@@ -103,7 +103,8 @@ extension SecureField {
|
||||
self.title = title
|
||||
self.text = text
|
||||
self.prompt = nil
|
||||
self.focusID = "securefield-\(title)"
|
||||
// Auto-generated focusID from view identity (collision-free)
|
||||
self.focusID = nil
|
||||
self.isDisabled = false
|
||||
self.onSubmitAction = nil
|
||||
}
|
||||
@@ -119,7 +120,8 @@ extension SecureField {
|
||||
self.title = title
|
||||
self.text = text
|
||||
self.prompt = prompt
|
||||
self.focusID = "securefield-\(title)"
|
||||
// Auto-generated focusID from view identity (collision-free)
|
||||
self.focusID = nil
|
||||
self.isDisabled = false
|
||||
self.onSubmitAction = nil
|
||||
}
|
||||
|
||||
@@ -162,7 +162,8 @@ extension Slider where Label == Text, ValueLabel == EmptyView {
|
||||
self.label = Text(String(title))
|
||||
self.valueLabel = nil
|
||||
self.trackStyle = .block
|
||||
self.focusID = "slider-\(title)"
|
||||
// Auto-generated focusID from view identity (collision-free)
|
||||
self.focusID = nil
|
||||
self.isDisabled = false
|
||||
self.onEditingChanged = onEditingChanged
|
||||
}
|
||||
|
||||
@@ -119,7 +119,8 @@ extension Stepper where Label == Text {
|
||||
self.label = Text(String(title))
|
||||
self.onIncrement = nil
|
||||
self.onDecrement = nil
|
||||
self.focusID = "stepper-\(title)"
|
||||
// Auto-generated focusID from view identity (collision-free)
|
||||
self.focusID = nil
|
||||
self.isDisabled = false
|
||||
self.onEditingChanged = onEditingChanged
|
||||
}
|
||||
@@ -145,7 +146,8 @@ extension Stepper where Label == Text {
|
||||
self.label = Text(String(title))
|
||||
self.onIncrement = nil
|
||||
self.onDecrement = nil
|
||||
self.focusID = "stepper-\(title)"
|
||||
// Auto-generated focusID from view identity (collision-free)
|
||||
self.focusID = nil
|
||||
self.isDisabled = false
|
||||
self.onEditingChanged = onEditingChanged
|
||||
}
|
||||
@@ -174,7 +176,8 @@ extension Stepper where Label == Text {
|
||||
self.label = Text(String(title))
|
||||
self.onIncrement = onIncrement
|
||||
self.onDecrement = onDecrement
|
||||
self.focusID = "stepper-\(title)"
|
||||
// Auto-generated focusID from view identity (collision-free)
|
||||
self.focusID = nil
|
||||
self.isDisabled = false
|
||||
self.onEditingChanged = onEditingChanged
|
||||
}
|
||||
|
||||
@@ -111,7 +111,8 @@ extension TextField where Label == Text {
|
||||
self.label = Text(title)
|
||||
self.text = text
|
||||
self.prompt = nil
|
||||
self.focusID = "textfield-\(title)"
|
||||
// Auto-generated focusID from view identity (collision-free)
|
||||
self.focusID = nil
|
||||
self.isDisabled = false
|
||||
self.onSubmitAction = nil
|
||||
}
|
||||
@@ -127,7 +128,8 @@ extension TextField where Label == Text {
|
||||
self.label = Text(title)
|
||||
self.text = text
|
||||
self.prompt = prompt
|
||||
self.focusID = "textfield-\(title)"
|
||||
// Auto-generated focusID from view identity (collision-free)
|
||||
self.focusID = nil
|
||||
self.isDisabled = false
|
||||
self.onSubmitAction = nil
|
||||
}
|
||||
|
||||
@@ -181,7 +181,8 @@ extension Toggle where Label == Text {
|
||||
) {
|
||||
self.isOn = isOn
|
||||
self.label = Text(String(title))
|
||||
self.focusID = "toggle-\(title)"
|
||||
// Auto-generated focusID from view identity (collision-free)
|
||||
self.focusID = nil
|
||||
self.isDisabled = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,14 +51,15 @@ struct ButtonTests {
|
||||
#expect(enabledButton.isDisabled == false)
|
||||
}
|
||||
|
||||
@Test("Button generates unique focus ID by default")
|
||||
@Test("Button focusID defaults to nil (auto-generated during rendering)")
|
||||
func buttonGeneratesUniqueID() {
|
||||
let button1 = Button("One") {}
|
||||
let button2 = Button("Two") {}
|
||||
|
||||
#expect(button1.focusID != button2.focusID)
|
||||
// UUID format check
|
||||
#expect(button1.focusID.contains("-"))
|
||||
// FocusID is now nil by default, allowing auto-generation from context.identity.path
|
||||
// during rendering via FocusRegistration.persistFocusID()
|
||||
#expect(button1.focusID == nil)
|
||||
#expect(button2.focusID == nil)
|
||||
}
|
||||
|
||||
@Test("Default button renders as single-line bracket style")
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
# TUIkit: Tasks
|
||||
|
||||
## In Progress
|
||||
|
||||
- [ ] **Two-Pass Layout Focus Fix**: Verify TextField focus works correctly after isMeasuring flag fix
|
||||
|
||||
## Open
|
||||
|
||||
### Components
|
||||
|
||||
#### High
|
||||
|
||||
(none)
|
||||
|
||||
#### High
|
||||
|
||||
- [ ] **Image View**: ASCII art rendering with full 24-bit color support (plan complete)
|
||||
- [ ] **Bundle Resource Loading**: SPM resource integration for Image support (plan complete, deferred)
|
||||
|
||||
#### Medium
|
||||
|
||||
- [ ] **DisclosureGroup**: Expandable/collapsible sections
|
||||
|
||||
#### Low
|
||||
|
||||
(none)
|
||||
|
||||
### Performance
|
||||
|
||||
- [ ] **`View._printChanges()` Equivalent**: Debug mechanism that logs why body was re-evaluated
|
||||
|
||||
### Infrastructure
|
||||
|
||||
- [ ] **Example App Redesign**: Feature catalog → multiple small example apps
|
||||
|
||||
### Testing & Docs
|
||||
|
||||
- [ ] **Mobile/Tablet Docs**: Test DocC on mobile devices (landing page done)
|
||||
- [ ] **Code Examples**: Counter, Todo List, Form, Table/List
|
||||
|
||||
## Completed
|
||||
|
||||
### 2026-02-11
|
||||
|
||||
- [x] **Two-Pass Layout Focus Bug**: Added isMeasuring flag to prevent double focus registration during measure/render passes
|
||||
|
||||
### 2026-02-10
|
||||
|
||||
- [x] **Two-Pass Layout (Phases 1-4)**: ProposedSize/ViewSize/Layoutable, HStack/VStack refactor, TextField/SecureField/Slider flexible
|
||||
- [x] **TextField Clipboard & Undo**: Ctrl+A/C/X/V/Z support, clipboard via pbcopy/pbpaste (macOS) and xclip/xsel (Linux), 50-state undo stack
|
||||
- [x] **NavigationSplitView**: Two/three-column layouts, visibility control, focus sections, styles, 39 tests
|
||||
|
||||
### 2026-02-09
|
||||
|
||||
- [x] **TextField Selection**: Shift+Arrow selection, highlight rendering, delete/replace selected text, 41 new tests
|
||||
- [x] **tuikit CLI Enhancements**: git init option, SQLiteData (pointfreeco), Linux Swift install, Xcode open prompt
|
||||
- [x] **Landing Page Update**: CLI features (git, SQLiteData), syntax highlighter fix, foregroundStyle API update
|
||||
|
||||
### 2026-02-08
|
||||
|
||||
- [x] **Section Integration (Phase 2c3)**: List uses SelectableListRow, Section flattening, selectableIndices
|
||||
- [x] **Test Cleanup**: Removed 5 flaky/tautological tests (732 → 727)
|
||||
- [x] **README Update**: Added missing features (List, Table, Section, Toggle, ProgressView, Spinner, ListStyle, Badge)
|
||||
- [x] **SelectionDisabled Modifier**: Environment key + modifier for disabling row selection
|
||||
- [x] **ListRowSeparator Stub**: API-compatible stub with warning (not supported in TUI)
|
||||
|
||||
### 2026-02-09 (earlier)
|
||||
|
||||
- [x] **Badge Modifier (Phase 2a)**: Int/Text/StringProtocol overloads, List integration, 20+ tests
|
||||
- [x] **ListStyle System (Phase 2b)**: PlainListStyle + InsetGroupedListStyle, alternating rows, environment keys
|
||||
- [x] **SelectableListRow Foundation (Phase 2c1)**: ListRowType enum, type-safe row classification, FrameBuffer Sendable
|
||||
- [x] **ItemListHandler Skip Logic (Phase 2c2)**: selectableIndices, focus navigation over non-selectable rows
|
||||
|
||||
### 2026-02-08
|
||||
|
||||
- [x] **Section View**: SwiftUI-conformant Section with header/content/footer, SectionRowExtractor, 14 tests
|
||||
- [x] **ButtonRole + Alert**: Horizontal buttons, cancel/destructive roles, ESC dismiss, arrow navigation
|
||||
- [x] **Xcode Project Template**: TUIkit App.xctemplate with install script, landing page one-liner
|
||||
- [x] **xcode-templates Skill**: Global skill for creating Xcode project templates
|
||||
- [x] **List & Table PR**: Merged PR #86 with focus bar, F-keys, StatusBar defaults, SwiftLint fixes
|
||||
|
||||
### 2026-02-07
|
||||
|
||||
- [x] **List & Table Components**: ItemListHandler + List + Table with selection, keyboard navigation, scrolling
|
||||
- [x] **Deep Code Review**: Force-unwrap fix, doc comments, 6 new tests, StatusBarTests split (4 files), SwiftLint 0 warnings
|
||||
- [x] **Swift 6 Concurrency Complete**: Phases 1-7; TerminalProtocol, ActionHandler, AppRunner cleanup
|
||||
- [x] **List/Table Shared Architecture**: Analysis complete, ItemListHandler pattern defined
|
||||
- [x] **Em-dash Removal**: Replaced all em-dashes with colons/sentences across 73 files
|
||||
|
||||
### 2026-02-06
|
||||
|
||||
- [x] **Toggle / Checkbox**: Boolean toggle with Space/Enter, slider + checkbox styles, focus indicator, disabled state, 17 tests
|
||||
- [x] **Dashboard Cache + Auto-Refresh**: localStorage cache (5 min TTL), auto-refresh timer, Framer Motion list animations
|
||||
- [x] **License Change**: CC BY-NC-SA 4.0 → MIT, 141 Swift files + LICENSE file
|
||||
- [x] **Mobile Responsive**: SiteNav hamburger, StatCards vertical, heatmap hidden, CommitList compact
|
||||
|
||||
### 2026-02-05
|
||||
|
||||
- [x] **ProgressView**: 5 bar styles, SwiftUI API parity
|
||||
- [x] **Remove Block/Flat Appearances**: BorderedView consolidated into ContainerView
|
||||
- [x] **Notification System**: Fire-and-forget NotificationService, fade-in/out animation, word-wrap
|
||||
- [x] **Render Performance Phase 2**: Cache invalidation fix, Equatable on 15 types/views
|
||||
- [x] **TupleView Equatable**: Conditional Equatable via parameter packs
|
||||
|
||||
### 2026-02-03
|
||||
|
||||
- [x] **Subtree Memoization**: EquatableView + RenderCache, opt-in via `.equatable()`
|
||||
- [x] **Palette Consolidation**: 6 Palette-Structs → SystemPalette.Preset enum
|
||||
- [x] **AppHeader**: Framework-managed Header Bar, `.appHeader {}` Modifier
|
||||
- [x] **Focus Sections**: `.focusSection()`, section-aware FocusManager
|
||||
|
||||
### 2026-02-02
|
||||
|
||||
- [x] **Render-Pipeline Phase 1-4**: Line-Diffing, Output Buffering, Caching
|
||||
- [x] **Spinner View**: dots/line/bouncing Styles, auto-animating
|
||||
- [x] **Structural Identity for @State**: ViewIdentity, StateStorage
|
||||
|
||||
## Notes
|
||||
|
||||
- DocC: `swift-docc-plugin`, GitHub Pages with `theme-settings.json` workaround
|
||||
- Landing Page: Astro + React + Tailwind 4, CI-deployed, tuikit.layered.work
|
||||
- Xcode Template: `~/Library/Developer/Xcode/Templates/Project Templates/macOS/Application/`
|
||||
|
||||
---
|
||||
|
||||
**Last Updated:** 2026-02-11
|
||||
Reference in New Issue
Block a user