Commit Graph
203 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 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 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 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
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
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
phranck d3f8ff60f3 Chore: Change license to MIT
- Add LICENSE file (MIT)
- Update 141 Swift file headers: CC BY-NC-SA 4.0 → License: MIT
2026-02-06 00:21:51 +01:00
phranck 64940714a3 Refactor: Change darker/lighter to relative percentage scaling
Previously darker(by: 0.5) subtracted 50 absolute lightness points.
Now it scales relative to current position: 0.5 means 'move halfway
toward the extreme'. More intuitive for humans — 20% darker always
means 20% of the remaining range toward black.
2026-02-05 23:18:45 +01:00
phranck 45d08c447d Feat: Add ProgressView with 5 bar styles and SwiftUI-matching API
Determinate progress bar with block, blockFine, shade, bar, and dot
styles. 4 initializers matching SwiftUI signatures (value/total, label,
currentValueLabel, string title). Style selection via .progressBarStyle()
modifier. Colors: foregroundSecondary (filled), foregroundTertiary (empty),
accent (dot head). 26 tests in 3 suites.
2026-02-05 23:18:38 +01:00
phranck 7f3d6f67ba Refactor: eliminate BorderedView, consolidate into ContainerView
Remove BorderedView entirely — .border() now creates a ContainerView
without title or footer. This ensures consistent 1-char inner padding
across all bordered containers (Box, Card, Panel, Alert, Dialog).

- Delete BorderModifier.swift (BorderedView)
- Update .border() to create ContainerView with padding
- Add empty-content guard to ContainerView
- Fix StatusBar bordered padding (1-char inset)
- Update Box docs to reflect new padding behavior
- Update Renderable.swift docs
- Migrate BorderedView tests to .border() API (527 tests passing)
2026-02-05 22:23:23 +01:00
phranck 0bde1d9f4d Refactor: remove block/flat appearances, ascii border style, and update documentation
Remove the block and flat appearance systems entirely, keeping only the
four standard border-based appearances (line, rounded, doubleLine, heavy).
Remove BorderStyle.ascii preset. Clean up all stale references in doc
comments, DocC articles, and README.

- Remove Appearance.flat/.block and BlockPalette protocol
- Remove surface color tokens (surfaceBackground, surfaceHeaderBackground, elevatedBackground)
- Remove BorderStyle.block, .ascii, and related statics
- Remove flat/block rendering paths from all views
- Simplify BorderRenderer, BorderModifier, ContainerView, Menu, Button, StatusBar, AppHeader
- Fix BorderStyle doc examples (add missing right padding)
- Update DocC: PaletteReference, ThemingGuide, AppearanceAndColors, RenderCycle, TUIkit.md
- Update README palette references to SystemPalette
- Delete FlatThemePage from example app
- Remove related tests (526 tests / 84 suites passing)
2026-02-05 22:08:53 +01:00
phranck f4fd383a76 Fix: pad notification content lines to full Box width 2026-02-05 20:32:13 +01:00
phranck 88d3165c4b Feat: add fire-and-forget notification system with NotificationService
Centralized NotificationService with .notificationHost() modifier for
rendering stacked, auto-dismissing notifications in the top-right corner.
Notifications use a single style with theme border colors — severity
differentiation belongs to Alerts, not notifications.

- NotificationService with static accessor and environment key
- NotificationHostModifier with fade-in/out animation and vertical stacking
- NotificationTiming for opacity interpolation and word-wrap
- Box(lines:) convenience init with BufferView for pre-styled content
- LifecycleManager.resetAppearance(token:) for re-triggering animations
- 17 tests covering service, timing, word-wrap, and rendering
2026-02-05 18:05:29 +01:00
phranck fef1233fd4 Feat: add conditional Equatable conformance to TupleView via parameter packs 2026-02-05 16:15:12 +01:00
phranck 71f6e12a42 Docs: add Subtree Memoization section to RenderCycle.md
Document the EquatableView/RenderCache system in the DocC article:
- How .equatable() works (5-step cache lookup)
- Cache invalidation triggers (State change, environment change)
- When to use / not use .equatable() (decision guide with tables)
- Which types support Equatable conformance
- Debug logging via TUIKIT_DEBUG_RENDER=1
- Code example using FeatureBox from the example app

Also updates existing sections:
- Correct 'no subtree memoization' claim in 'What Is NOT Diffed'
- Add EquatableView to Renderable lists (Direct Rendering, View-Level Modifiers)
2026-02-05 14:49:58 +01:00
phranck 20547de056 Feat: apply .equatable() to static example app subtrees
Add Equatable conformance and .equatable() wrapper to extracted views:
- FeatureBox (MainMenuPage): title/subtitle comparison, 3 instances
- ContainerTypesRow (ContainersPage): property-free, always cache-hits
- SettingsAndAlignmentRow (ContainersPage): property-free, always cache-hits

These subtrees are purely palette-driven with no @State dependencies.
During Spinner/Pulse animation frames (25 FPS), the render cache skips
their entire subtree rendering — only the animated views re-render.
2026-02-05 14:47:31 +01:00
phranck 095c67e021 Refactor: decompose ContainersPage and MainMenuPage into smaller view structs
Extract static, state-free subtrees into standalone View structs to
prepare for .equatable() memoization in Phase 5:

- FeatureBox: extracted from MainMenuPage's private featureBox() method
- ContainerTypesRow: Card/Box/Panel examples from ContainersPage
- SettingsAndAlignmentRow: settings panel + alignment demos

ButtonsPage left as-is — nearly all sections depend on @State clickCount
through Button actions, making decomposition ineffective for memoization.
2026-02-05 14:46:12 +01:00
phranck 08daee3258 Feat: render cache debug tooling with per-frame stats and review fixes
Add RenderCache.Stats for cache performance tracking:
- Hit/miss/store/clear counters with hit rate calculation
- Per-frame delta via Stats.delta(since:) for meaningful frame-level logging
- TUIKIT_DEBUG_RENDER=1 env var enables stderr logging (zero cost when off)
- logDebug uses @autoclosure to avoid string allocation in production

Review fixes applied:
- F1: logFrameStats() now logs per-frame delta, not cumulative totals
- F2: clearAll() no longer reads entries.count unconditionally
- F3: Remove unnecessary Sendable from Stats (main-thread only type)
- F5: reset() now also clears stats and frame snapshot
- F7: Move EnvironmentSnapshot to RenderLoop.swift (private, where it belongs)
- F8: Build snapshot from EnvironmentValues, not from ThemeManager directly
2026-02-05 14:40:58 +01:00
phranck 773bb15f2b Feat: render performance phase 2 — cache invalidation fix + Equatable conformances
Phase 1: Add EnvironmentSnapshot comparison in RenderLoop to automatically
clear render cache on theme/appearance changes. Eliminates latent bug where
EquatableView-cached subtrees could serve stale content after palette changes.

Phase 2: Add Equatable conformances to enable subtree memoization:
- Types: TextStyle, Alignment, ContainerConfig, ContainerStyle
- Leaf: Text
- Containers: VStack, HStack, ZStack, Box, Panel, Card, Dialog,
  ContainerView, BorderedView (conditional where Content: Equatable)
- Modifiers: FlexibleFrameView, OverlayModifier, DimmedModifier
  (conditional where Content/Base/Overlay: Equatable)

Also fixes two pre-existing SwiftLint violations:
- State.swift: vertical whitespace before closing brace
- ContainerView.swift: line length (extract local variable)
2026-02-05 14:21:12 +01:00
phranck a705414c82 Feat: Add subtree memoization via EquatableView (render pipeline phase 5)
Equatable views can opt into render caching with .equatable(). On cache hit,
the entire subtree is skipped and the cached FrameBuffer returned directly.
Cache is cleared on every @State change; GC removes stale entries per frame.
2026-02-03 23:08:17 +01:00
phranck cb00b29d56 Refactor: Nest PalettePreset as SystemPalette.Preset 2026-02-03 22:31:33 +01:00
phranck ea44f1fd13 Refactor: Consolidate 6 palette structs into PalettePreset enum + SystemPalette 2026-02-03 22:27:59 +01:00
phranck df9cb27c88 Refactor: Apply extension separation rule to all 35 Swift source files — move ~135 functions from type bodies into access-level-prefixed extensions (public/internal/private) 2026-02-03 22:05:46 +01:00