Commit Graph
536 Commits
Author SHA1 Message Date
phranck cfa8b31631 Refactor: Standardize UI card styling and remove em-dashes
- Unified card headers: gap-3, Icon size 20, text-accent color
- PlansCard: collapsible sections, animated expand/collapse
- update-plans-data.ts: export all plans (no limit)
- Replaced all em-dashes with colons or full sentences
- Content text standardized to text-lg across all cards
- FeatureCard, ArchHighlight descriptions adjusted
2026-02-07 00:24:19 +01:00
phranck 0d10b0255d Feat: Add Plans Dashboard Card with hourly data sync
- Standardize plan file structure (PLAN_TEMPLATE.md): Title → Preface → Context → Spec → Design → Implementation → Checklist
- Rewrite all 24 plan prefaces in natural, action-focused language with markdown support
- Add update-plans-data.ts script: extracts top 5 open + 5 done plans from plans/ directory
- Add PlansCard React component with markdown rendering (react-markdown)
- Integrate PlansCard into dashboard (below activity heatmap)
- Add usePlansCache hook with 5-minute localStorage cache
- Add update-plans-data.yml GitHub Actions workflow (hourly trigger)
- Install react-markdown dependency for markdown-in-JSON rendering
- Update npm scripts: prebuild and update:plans
2026-02-06 23:12:27 +01:00
phranck 8a470f6344 session: Planning phase complete — View Architecture Refactoring roadmap
Completed comprehensive planning for three-phase refactoring:

Phase 1: refactor/containerview
  - ContainerView: Extract _ContainerViewCore, enable modifiers
  - Effort: 2-3 hours, Risk: Low

Phase 2: refactor/shared-handlers
  - FocusableItemListHandler, SelectionStateManager, utilities
  - Effort: 4-5 hours, Risk: Moderate

Phase 3: feat/list-table-new
  - List refactoring, Table implementation (shared foundation)
  - Effort: 14-18 hours, Risk: Moderate

Total effort: ~20-25 hours
Outcome: Proper View architecture, maximum code reuse, no duplication

All analysis, plans, and documentation complete.
Ready to start implementation when needed.

See: plans/REFACTORING_ROADMAP.md for overview
See: plans/open/2026-02-06-imp-*.md for detailed plans
2026-02-06 22:03:02 +01:00
phranck 2f6220dd2d docs: Add comprehensive refactoring roadmap
Three phases with detailed plans, success criteria, and timelines:

Phase 1: ContainerView refactoring (2-3h, low-risk)
Phase 2: Shared handlers & helpers (4-5h, moderate)
Phase 3: List & Table implementation (14-18h, moderate)

Total effort: ~20-25 hours
Outcome: Proper View architecture, maximum code reuse

Three branches ready:
- refactor/containerview
- refactor/shared-handlers
- feat/list-table-new

Recommended: Execute in sequence, each with own PR.
2026-02-06 22:02:13 +01:00
phranck 005ff6f85f plans: Add three implementation plans for View Architecture refactoring
Phase 1: ContainerView Refactoring (Low-risk, ~2-3h)
- Extract _ContainerViewCore with Renderable
- Make ContainerView a simple View with body
- Enable modifier support

Phase 2: Shared Handlers & Helpers (Moderate, ~4-5h)
- FocusableItemListHandler (navigation logic)
- SelectionStateManager (selection tracking)
- ItemStateRenderer (styling utilities)
- renderFocusableContainer() (helper function)

Phase 3: List & Table Implementation (Moderate-High, ~14-18h)
- Refactor List to use new architecture
- Implement Table with same foundation
- Both follow Box.swift pattern
- Maximum code reuse

Each phase gets its own branch and PR.
2026-02-06 22:01:47 +01:00
phranck 465c850227 chore: Reframe todos — architecture alignment before implementation
Key items:
1. ContainerView refactor (pattern correctness)
2. List & Table architecture (shared components)
3. Verify modifiers work (environment propagation)

This ensures new components (List, Table) follow correct patterns from the start.
2026-02-06 21:59:59 +01:00
phranck 7a98d30321 plan: ContainerView refactoring to follow View pattern
ContainerView currently uses body: Never + direct Renderable (WRONG).
Should be refactored to:
- Public View with real body: some View
- Private _ContainerViewCore with Renderable
- Enables modifiers to work naturally
- Consistent with Box and SwiftUI patterns

This should be done BEFORE List/Table implementation
so they follow the correct pattern from the start.

Impact: Card, Panel, Alert, Dialog use renderContainer() helper,
so they won't be directly affected. Low-risk refactoring.
2026-02-06 21:59:52 +01:00
phranck ff5019eb90 plan: Containerization via Composition (not inheritance)
SwiftUI/TUIKit pattern: Use composition helpers, not inheritance.

Current pattern (Card, Panel, Alert):
- renderContainer() helper function
- No inheritance from ContainerView
- Each component maintains own struct
- Shared logic in helper function

For List & Table:
- Create renderListWithFocus() or renderFocusableContainer()
- Extract FocusableItemListHandler for shared state
- Both use ContainerView via helper function
- Maximum code reuse without inheritance

This follows Swift conventions and matches existing TUIKit patterns.
2026-02-06 21:59:07 +01:00
phranck ac7b62b7f0 chore: Update todos — List & Table are now blocked pending architecture finalization
Both List and Table blocked until shared architecture is defined:
- FocusableItemListHandler base class
- SelectionStateManager
- Shared rendering utilities

This prevents redundancy and ensures consistency between components.
2026-02-06 21:53:13 +01:00
phranck 6c32c549c5 plan: List & Table shared architecture analysis
Before implementing either List or Table, analyze shared concerns:
- Focus management and navigation (identical logic)
- Selection state and binding
- Container rendering (border, title, padding)
- Item state rendering (focused/selected visuals)
- View modifiers and environment propagation

Extract reusable components:
- FocusableItemListHandler base class
- SelectionStateManager for consistent selection
- Shared rendering utilities

Do NOT implement List/Table yet without this architecture.
2026-02-06 21:52:55 +01:00
phranck bc925885d6 Refactor: List with title and body padding (simple approach)
- Add optional title parameter to List struct and init methods
- Title rendered above items with accent color
- Body items get horizontal padding (1 char left/right, 0 vertical)
- Focus indicators maintain full background width with padding
- Simplified ListPage: List is now self-contained with title parameter
- All 618 tests pass, 0 serious lint violations
- Maintains complete keyboard navigation and selection behavior
2026-02-06 21:34:39 +01:00
phranck 88bc3f1582 Fix: Remove dot from focused items, add dimmed background instead
- Focused items: dimmed background (foregroundSecondary, 15% opacity)
- Selected items: dimmed background (accent, 20% opacity) + accent text
- Unfocused items: plain text (no padding, no background)
- Dot indicator completely removed
- Selection and focus are visually distinct via background color
- 618 tests passing
2026-02-06 21:12:26 +01:00
phranck d2ce696d74 Fix: Correct List selection and focus rendering
- Selected items: dimmed background (20% opacity) + accent text (NO dot)
- Focused items: pulsing dot indicator (no background)
- Unfocused items: plain text with padding
- Selection and focus are now visually distinct
- 618 tests passing
2026-02-06 21:10:54 +01:00
phranck df5a6ea71c Feat: Implement List selection and improve rendering
- Selection now works: Enter/Space selects focused row (by index)
- Selected rows show with full-width background bar (accent color)
- Focused rows show with pulsing dot indicator
- Unfocused rows show with padding space for alignment
- ListPage content now uses theme foreground colors (not white)
- Selection binding updates when user presses Enter/Space
- 618 tests passing
2026-02-06 21:08:30 +01:00
phranck cba465caec Test: Add comprehensive test for List with multiple items
- Verify List renders 5+ items vertically
- Check all items are present in output
- Test buffer height matches item count
- All 618 tests passing
2026-02-06 21:05:03 +01:00
phranck a799911666 Fix: List now correctly renders all items from ViewBuilder content
- Changed from resolveChildInfos() to direct buffer rendering
- Extract rows from rendered content buffer instead of child infos
- Fixes issue where only first item was displayed
- Works with single items and multiple items
- 617 tests passing
2026-02-06 21:03:57 +01:00
phranck 924492f46b Feat: Integrate List component into example app menu
- Add .list case to DemoPage enum
- Add ListPage to menu with shortcut 9
- Shift Spinners to shortcut 0
- Update status bar menu range to include all shortcuts
- 617 tests passing
2026-02-06 21:02:14 +01:00
phranck 7ea5e51478 Feat: Add scrollable List component with keyboard navigation
- List<SelectionValue, Content> generic component with optional selection binding
- ListHandler (Focusable) manages focus, scroll offset, and keyboard navigation
- Keyboard: arrow keys, Page Up/Down, Home/End, Enter/Space for selection
- Auto-scroll: focused row always visible in viewport
- .tag() modifier for selection value association
- 30+ tests covering navigation, selection, scrolling, rendering
- ListPage example with static and dynamic content
- 617 total tests passing
2026-02-06 20:59:40 +01:00
phranck 45bf781b97 Chore: Move completed toggle plan to done 2026-02-06 20:17:41 +01:00
Frank Gregor 6473d5a0aa Chore: Update test count badge to 591 tests [skip ci] 2026-02-06 19:10:00 +00:00
phranck bee3f5fc8a Merge pull request #81 from phranck/feat/button-focus-indicator
Feat: Pulsing focus indicator for buttons
2026-02-06 20:08:09 +01:00
phranck d5b2d56e5c Fix: Reset RadioButtonGroup focusedIndex to selected item on focus loss
- isFocused now requires both focusedIndex match AND groupHasFocus
- Prevents focused items from appearing dimmed after group loses focus
- onFocusLost() resets focusedIndex to selected button for correct state
- Fixes visual state inconsistency when tabbing between groups
2026-02-06 20:04:44 +01:00
github-actions[bot] 1b6d2d7895 chore: update social cache [skip ci] 2026-02-06 18:40:20 +00:00
phranck 068d5f8a6b Refactor: Separate focus navigation from selection in RadioButtonGroup
Behavior Change:
- Arrow Keys: navigate focus (focusedIndex) only, DON'T change selection
- Enter/Space: select the currently focused option (set binding)
- Selection remains unchanged while navigating with arrows

Visual Distinction:
- Selected (●) with pulsing when group has focus
- Focused but not selected (●) dimmed at 50% opacity
- Other options (◯) tertiary color

Rendering:
- Both selected and focused show ● indicator
- Different colors distinguish them: pulsing accent vs dimmed accent

Tests:
- Updated all handler tests to verify focus ≠ selection
- Arrow key tests now check focusedIndex changes but selection unchanged
- All 591 tests passing

UX:
- User can browse options with arrows without committing
- Press Enter/Space when desired option has focus to select
- macOS/SwiftUI pattern: focus and selection are independent
2026-02-06 19:29:40 +01:00
phranck 16119f8e3d Fix: RadioButtonGroup consumes all arrow keys to prevent group switching
Problem:
- Horizontal group with Up press: group ignored Up, FocusManager navigated to previous group
- Vertical group with Left press: group ignored Left, FocusManager navigated to previous group
- User couldn't stay in group when pressing 'wrong' direction keys

Solution:
- All arrow keys always return true (consumed by group)
- Vertical: Up/Down navigate items, Left/Right do nothing
- Horizontal: Left/Right navigate items, Up/Down do nothing
- Never falls through to FocusManager for arrow key handling

Effect:
- User confined to current group (can't accidentally switch with arrow keys)
- Only Tab exits group to next focusable element
- Consistent UX: arrow keys always used for in-group navigation

Testing:
- All 591 tests passing
- No test changes needed (behavior was implicit)
2026-02-06 19:25:23 +01:00
phranck f1b29c17bf Feat: RadioButtonGroup wrapping navigation for seamless cycling
Behavior:
- Vertical groups: Up/Down wrap around (down at end → first, up at start → last)
- Horizontal groups: Left/Right wrap around (right at end → first, left at start → last)
- Always return true (event consumed) — never falls through to FocusManager
- Enables smooth cycling through options without leaving the group

Implementation:
- Modified handleKeyEvent to use ternary wrap logic
- All navigation keys now always consumed (return true)
- Tab still exits group to next focusable element

Testing:
- Updated 'boundaryNavigation' test to verify wrapping behavior
- Test now checks that Up at start wraps to last item
- All 591 tests passing

User Experience:
- Stay in group when cycling options with arrow keys
- Tab exits to next group (standard radio group UX)
2026-02-06 19:17:46 +01:00
phranck 86fb78d2b7 Fix: Make focusID persistent via StateStorage for stable focus state
Problem:
- focusID was generated fresh on every render: 'radio-group-<UUID>'
- Each render got different UUID → isFocused() always compared new IDs to old
- groupHasFocus was always false → pulsing never triggered

Solution:
- Store focusID in StateStorage (like focusedIndex and other persistent state)
- Generate from context.identity.path if not explicitly provided
- Reuse same focusID across all renders
- Now focusManager.isFocused(id:) comparison works correctly

Architecture:
- focusID now optional in RadioButtonGroup, derived during render
- Persistent in stateStorage key with propertyIndex: 1
- Handler uses persistedFocusID for all focus operations

Behavior:
- Selected indicator in focused group now pulses correctly
- Tab navigation between groups works
- Arrow keys navigate within group

Documentation:
- Updated focusID parameter doc
- Added comment explaining persistence requirement

Testing:
- All 591 tests passing
2026-02-06 19:11:00 +01:00
phranck 26bc1d7fcc Fix: Separate isFocused (radio button) from groupHasFocus (group)
Problem:
- isFocused was computed as: groupHasFocus && handler.focusedIndex == index
- This meant unfocused groups never showed accent color for focused radio buttons
- Pulse never triggered because groupHasFocus was false for unfocused groups

Solution:
- isFocused = handler.focusedIndex == index (which radio button has keyboard focus)
- groupHasFocus = focusManager.isFocused(id: focusID) (which group is active)
- These are independent — a radio can be focused even if group isn't

Behavior:
- Focused group: selected (●) pulses, focused ◯ shows accent, others tertiary
- Unfocused group: all static (selected=accent, focused=accent, others=tertiary)

Testing:
- All 591 tests passing
2026-02-06 19:03:10 +01:00
phranck 1661e17845 Fix: Selected indicator pulses when group has focus
Problem:
- Double condition: isFocused && groupHasFocus (but isFocused already included groupHasFocus)
- Pulse never triggered

Solution:
- Simplify: pulse when isSelected && groupHasFocus
- isFocused check only for unfocused/not-selected items

Behavior:
- Selected indicator (●) pulses when group has focus
- Focused but unselected indicator (◯) stays static accent
- Unfocused indicators stay tertiary

Testing:
- All 591 tests passing
2026-02-06 18:56:32 +01:00
phranck 7c27cec515 Fix: Only focused RadioButtonGroup's selected indicator pulses
Problem:
- All groups rendered as if they had focus, all selected indicators pulsed
- Pulsing was based on isFocused (radio button index match) not group focus

Solution:
- Check groupHasFocus: focusManager.isFocused(id: focusID)
- Pass groupHasFocus to render functions
- Only pulse when BOTH: isFocused (this radio) AND groupHasFocus (this group)

Behavior:
- Unfocused groups: all indicators static (selected=accent, focused=accent, other=tertiary)
- Focused group: selected indicator pulses if it was the focused radio button
- Pulsing only on the group that actually has keyboard focus

Documentation:
- Added groupHasFocus parameter with doc comment
- Render logic updated with precise condition

Testing:
- All 591 tests passing
2026-02-06 18:55:51 +01:00
phranck 96f9c4e865 Refactor: RadioButtonGroup color scheme — selected pulsing, focused static, other tertiary
Color State Matrix:

Selected (●):
- If group has focus: pulsing accent (35% → 100%)
- If unfocused: static accent

Focused by Arrow Keys (◯):
- Always static accent (not selected yet)

Unselected & Unfocused (◯):
- foregroundTertiary (dimmed)

Disabled:
- All indicators: foregroundTertiary

Visual Intent:
- Selected indicator pulsing draws attention when group is active
- Focused indicator (static accent) shows keyboard position
- Tertiary color for 'inactive' options

Documentation:
- Updated render logic with detailed color comments
- Clear state branching: selected → focused → unfocused

Testing:
- All 591 tests passing
2026-02-06 18:54:18 +01:00
phranck 5a11ec17fd Refactor: RadioButtonGroup focus/selection visual distinction with pulsing
Visual Behavior:
- **Selected Radio Button** (●): Filled circle, selected by Space/Enter
- **Focused Radio Button** (◯): Pulsing accent color, navigated by arrow keys
- **Unselected/Unfocused** (◯): Border color

Focus Indicators:
- When group has focus, focused radio button pulses with accent (35% → 100%)
- Allows visual distinction between 'which is selected' vs 'which has keyboard focus'
- macOS/SwiftUI pattern: focus ≠ selection

Text Rendering:
- Radio button labels use theme colors (foregroundSecondary default from Text)
- Consistent with app palette, not hardcoded

Documentation:
- Added doc comment explaining focus vs selection distinction
- Render logic documented with clear state branches

Testing:
- All 591 tests passing
- Focus pulsing behavior verified
2026-02-06 18:50:40 +01:00
phranck 2cb854c387 Fix: KeyEvent dispatch priority — focused element first, then FocusManager defaults
Problem:
- FocusManager.dispatchKeyEvent() processed Up/Down/Left/Right before asking focused element
- RadioButtonGroup (and other elements) couldn't handle arrow keys
- Only Tab worked, navigation keys were consumed by FocusManager

Solution:
- Reorder dispatch: first try currentFocused.handleKeyEvent()
- If element consumes event (returns true), stop
- Only if not handled, apply FocusManager defaults (Tab navigation, Up/Down between items)

Effect:
- RadioButtonGroup Up/Down/Left/Right navigation now works
- Tab still navigates between groups (FocusManager fallback)
- macOS/SwiftUI behavior: responder chain (element first, then system)
- All 591 tests passing

Documentation:
- Added inline comments explaining dispatch order and rationale
2026-02-06 18:22:38 +01:00
phranck 83bd03946f Refactor: RadioButtonGroup handler persistence via StateStorage for Tab navigation
Implementation:
- RadioButtonGroupHandler now persisted in StateStorage (like @State values)
- focusedIndex maintained across renders, enabling Tab navigation between groups
- Handler properties (selection, itemValues) sync'd each render with current values
- Properties marked var to allow syncing with current render state

Architecture:
- Follows SwiftUI/macOS pattern: persistent handler maintains internal state
- StateStorage.StateKey with context.identity ensures identity across renders
- stateStorage.markActive() enables garbage collection of inactive state
- Radio button behavior now fully consistent with Button/Toggle patterns

Documentation:
- Added doc comment to RadioButtonGroupHandler explaining persistence
- focusedIndex property documented as persisted value
- Inline comments explain StateStorage integration

Testing:
- All 591 tests passing
- RadioButtonGroup Tab navigation now functional
- Arrow key navigation persists focus position across renders
2026-02-06 18:18:00 +01:00
phranck 58ad43c6ea Fix: Auto-activate first section and auto-focus first element on register
- When first focusable component registers, auto-activate its section
- First element in active section auto-focuses (macOS/SwiftUI behavior)
- Enables Tab navigation between groups from the start
- All 591 tests passing
2026-02-06 18:14:26 +01:00
phranck 6fee5aa671 Refactor: RadioButtonGroup focus behavior and orientation-aware navigation
- Radio button indicator no longer pulses, stays static (selected=accent, unselected=border)
- Only focused item shows indicator, not all items
- Vertical orientation: Up/Down arrows navigate, Left/Right ignored
- Horizontal orientation: Left/Right arrows navigate, Up/Down ignored
- Enter/Space always select regardless of orientation
- Pass orientation to RadioButtonGroupHandler for navigation logic
- Updated all handler tests to include orientation parameter
- All 591 tests passing
2026-02-06 18:06:08 +01:00
phranck ce772ea6f3 Refactor: Checkbox style uses dot (●) instead of square
- Changed indicator from ◼ (U+25FC) to ● (large dot)
- More consistent with toggle indicator style
- Checkbox now shows [●] when on, [ ] when off
- Updated example page state summary
- All 591 tests passing
2026-02-06 18:03:01 +01:00
phranck b7de843009 Feat: Add RadioButtonPage to example app with menu integration
- RadioButtonPage shows vertical radio groups, horizontal radio groups, and disabled states
- Live state display for color, size, and layout choices
- Added to main menu as option 8
- Shortcut key '8' for quick navigation
- All 591 tests passing
2026-02-06 17:57:32 +01:00
phranck d28292b9eb Feat: RadioButtonGroup component with vertical/horizontal layout
- RadioButtonGroup<T> with generic Hashable selection
- RadioButtonItem builder with string or view labels (@ViewBuilder)
- Vertical (default) or horizontal layout with RadioButtonOrientation
- RadioButtonGroupHandler for arrow key navigation (up/down/left/right)
- Enter/Space to select, arrow keys to navigate
- Focus indicator with pulsing accent color
- Disabled state support
- 20 comprehensive tests (591 total passing)
- All patterns inherited from Button/Toggle (brackets pulse, etc.)
2026-02-06 17:53:45 +01:00
phranck 941d8b704d Refactor: Checkbox uses U+25FC (black small square)
- Changed indicator from ◾ to ◼ (U+25FC)
- Smaller, more compact checkbox appearance
- Updated example page state summary
- All 571 tests passing
2026-02-06 17:45:45 +01:00
phranck 6d1a4e876e Refactor: Checkbox uses U+25FE (black medium square) instead of U+25A3
- Changed indicator from ▣ to ◾ (U+25FE)
- Better visual weight and consistency
- Updated example page state summary
- All 571 tests passing
2026-02-06 17:44:19 +01:00
phranck 0713f3ead7 Refactor: Checkbox uses U+25A3 (square with horizontal fill) instead of dot
- Changed indicator from ● to ▣ (U+25A3)
- More visually distinctive for checkboxes
- Updated example page state summary
- All 571 tests passing
2026-02-06 17:43:21 +01:00
phranck 9a0634463b Refactor: Only brackets pulse, indicator content stays static
- Separate rendering for brackets ([, ]) and indicator content (●○, ●, space)
- Brackets pulse on focus (35% → 100% accent)
- Indicator content: accent when focused, foregroundSecondary when unfocused
- Matches exact Button bracket pulse behavior pattern
- All 571 tests passing
2026-02-06 17:42:20 +01:00
phranck 989861738c Refactor: Checkbox style uses dot (●) instead of x
- Checkbox toggle now shows [●] when on, [ ] when off
- More consistent with dot-based toggle style [●○]
- Updated example page to show new checkbox indicator
- All 571 tests passing
2026-02-06 17:41:17 +01:00
phranck 2a2e57a240 Refactor: Toggle brackets pulse on focus, remove dot prefix
- Toggle brackets [●○], [x] now pulse like Button brackets
- Subtle pulse: 35% → 100% accent interpolation on focus
- No more focus dot prefix (•) — brackets are the focus indicator
- Bold styling on focused toggle brackets
- Matches Button focus behavior pattern (PR #81)
- All 571 tests passing, 0 lint violations
2026-02-06 17:40:34 +01:00
github-actions[bot] 8cfef6e809 chore: update social cache [skip ci] 2026-02-06 16:38:26 +00:00
phranck 8f685cf26b cleanup: Finalize Toggle component and update status files
- Mark 2026-02-06-toggle.md as completed
- Move Toggle to Completed section in to-dos.md
- Update test count to 571 in whats-next.md
- Update next-steps priority (TextInput → Table)
2026-02-06 17:34:26 +01:00
phranck a0af77af15 Feat: Toggle component with toggle and checkbox styles
- Toggle struct with string initializer for SwiftUI API parity
- ToggleStyle enum: .toggle (slider ●○) and .checkbox ([x])
- ToggleHandler for Space/Enter keyboard events
- Focus indicator with pulsing accent dot (inherited from Button pattern)
- Disabled state with tertiary color
- .disabled() modifier for control
- Comprehensive tests (17 tests, 571 total passing)
- TogglePage example with both styles, disabled states, and live state demo
- Added to main menu with shortcut key 7
2026-02-06 17:34:00 +01:00
phranck 6a055ca126 Feat: Pulsing focus indicator for buttons and improved styling
- Button focus: brackets pulse in accent color when focused
- Plain buttons: use pulsing dot indicator (no brackets)
- Button styles: default is now dimmer, primary is accent+bold
- Disabled buttons: darker appearance (50% tertiary)
- Shift+Tab: parse CSI Z sequence for backward focus navigation
- PulseTimer: reset to bright on focus change
- Faster pulse: 100ms steps (was 150ms)
- Shared helper: BorderRenderer.focusIndicatorPrefix() for future use
2026-02-06 15:47:45 +01:00
github-actions[bot] d493205795 chore: update social cache [skip ci] 2026-02-06 14:42:02 +00:00