You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A new Code structure section was added to CLAUDE.md / AGENTS.md (one-responsibility-per-file, soft 400 / hard 600 line caps, extract reusable components into Shared/, no dead code), and the Design tokens rule was tightened: any literal numeric value in SwiftUI must use a token unless the native styling carries the value implicitly.
This issue tracks bringing the existing codebase into compliance.
MQTTInspectorView.swift — MQTTInspectorView, SubscribeStore, InspectorMessage, JSONHighlighter. JSONHighlighter is a generic utility that doesn't belong in a view file.
InAppNotificationOverlay.swift — overlay + two ~200-line peer banner views.
Features/Home/HomeCardComponents.swift (7 types) — borderline; StatCellButtonStyle and HomeCardAlertList could split.
Features/Devices/DeviceListViewModel.swift (4 types) — worth a look.
Model files with multiple tightly-coupled DTOs (DeviceDoc.swift, BridgeSettings.swift, BridgeInfo.swift, Device.swift) are intentionally left alone — Swift convention permits grouping related value types.
Hardcoded numbers where a token should be used
131 instances total. Worst offenders:
File
Count
MQTTInspectorView.swift
11
LightControl/LightControlCard.swift
10
FanControl/FanControlCard.swift
10
InAppNotificationOverlay.swift
9
CoverControl/CoverControlCard.swift
7
ClimateControl/ClimateControlCard.swift
6
Backup/RestoreGuideSheet.swift
6
Components/Doc/DocBlockView.swift
5
Components/DeviceCard.swift
5
Two recurring patterns:
Small spacings the token table doesn't cover.spacing: 2, 4, 5, 6; .padding(.vertical, 2). xs = 4, sm = 8 — 5 and 6 have no token.
One-off frame sizes.frame(width: 22), frame(width: 14), frame(width: 220), .padding(.bottom, 80), frame(height: 0.5). The 0.5 divider height appears in ≥4 card files and clearly wants a Size.hairline token. FanControlCard even declares a local rowIconWidth: CGFloat = 22 that duplicates Size.cardSymbol = 22.
Plan
Sequenced so each step is a small, reviewable PR with low blast radius. Each step can land independently on dev and ship as part of v1.3.1.
Decide on 5 and 6: snap to xs (4) where it's a tight gap or sm (8) where it's a row spacing. Spot-check visually on LightControlCard and FanControlCard before committing — these values were likely chosen by eye, so collapsing them to existing tokens is the goal, not adding new ones.
Add Size.notificationBottomInset: CGFloat = 80 (used 3× in InAppNotificationOverlay).
Document any new token with a one-line comment if its purpose isn't obvious from the name.
No behavior changes in this step — tokens are added but not yet referenced.
Step 2 — Mechanical sweep: replace literals with tokens
Touch one file per commit so reviews stay scannable. Order by impact:
Shared/FanControl/FanControlCard.swift (10 literals; also delete the duplicate local rowIconWidth and use Size.cardSymbol).
Tail of remaining files (1–4 literals each), bundled into one final commit.
After each commit, run xcodebuild build for the simulator and visually diff the touched screen via mcp__XcodeBuildMCP__screenshot against dev to confirm nothing shifted.
Step 3 — Split FanControlCard.swift (754 → ~300)
Extract FanFeatureSections → new file Shellbee/Shared/FanControl/FanFeatureSections.swift (mirrors the existing Shared/LightControl/LightFeatureSections.swift pattern).
Keep PreviewHost adjacent to whichever file has the most preview surface — likely the banner itself.
Confirm the banners don't share private state with the overlay (a quick read suggests they don't — they take values via init).
Step 5 — Lift JSONHighlighter out of MQTTInspectorView.swift
Move JSONHighlighter → Shellbee/Shared/Components/JSONHighlighter.swift (it's reusable; BeautifulPayloadView would also benefit).
Consider whether InspectorMessage and SubscribeStore should also split. SubscribeStore is a substantial @Observable class (~35 lines) — split into Features/Settings/Developer/SubscribeStore.swift. InspectorMessage is small and tightly coupled — leave inline.
Lower priority. Split if the file grows further; otherwise leave as a deliberate "primitives bag" (it's under 200 lines).
Step 7 — Re-evaluate AppStore.swift and DeviceDocNormalizer.swift
These need design thought, not mechanical extraction. Defer to follow-up issues if the work is non-trivial:
AppStore.swift (691): consider splitting via extension AppStore files per domain (AppStore+Devices.swift, AppStore+Groups.swift, AppStore+Logs.swift, AppStore+OTA.swift). Read the file end-to-end first to confirm that's the right cut.
DeviceDocNormalizer.swift (646): 7 parsing types — likely a candidate for a Core/Parsing/Normalizer/ subfolder with one type per file. Confirm by reading; some of the 7 may be small inline helpers that should stay together.
If either turns out to need substantial refactoring, file a separate issue and target a later milestone.
Acceptance criteria
No file in Shellbee/ exceeds 600 lines (hard cap), unless explicitly justified in a comment at the top of the file.
grep for hardcoded SwiftUI numeric modifiers (.padding\(\d+\), frame(width:\s*\d+, spacing:\s*\d+\), cornerRadius(\d+)) returns zero matches outside DesignTokens.swift, ignoring 0 and tokens-passed-as-args.
No file has 3+ unrelated top-level types (DTO bundles excepted).
Fast CI green on the merge PR.
No visible UI regressions on Home, Devices list, Device detail (light/fan/cover/climate cards), Notifications overlay, MQTT inspector, Backup restore guide.
Notes
This is mechanical cleanup — no new features, no behavior changes. Each step should be reviewable in under 5 minutes.
The CLAUDE.md / AGENTS.md rules already shipped on dev. Until this issue is resolved, the codebase will fail its own conventions in the audited files.
Summary
A new
Code structuresection was added toCLAUDE.md/AGENTS.md(one-responsibility-per-file, soft 400 / hard 600 line caps, extract reusable components intoShared/, no dead code), and theDesign tokensrule was tightened: any literal numeric value in SwiftUI must use a token unless the native styling carries the value implicitly.This issue tracks bringing the existing codebase into compliance.
Findings
Files over the hard cap (600 lines)
Shellbee/Shared/FanControl/FanControlCard.swiftFanControlCard+FanFeatureSections+DisclosureRow+FanExtraRowShellbee/Features/Notifications/InAppNotificationOverlay.swiftInAppNotificationOverlay+InAppNotificationBanner(~200 lines) +FastTrackBanner(~200 lines) +PreviewHostShellbee/Core/Store/AppStore.swiftShellbee/Core/Parsing/DeviceDocNormalizer.swiftFiles over the soft cap (400–600 lines)
Shellbee/Shared/Components/Doc/DocumentationExperienceView.swiftShellbee/Shared/Components/DeviceCard.swiftShellbee/Features/Settings/Developer/MQTTInspectorView.swiftMultiple unrelated top-level types in one file
MQTTInspectorView.swift—MQTTInspectorView,SubscribeStore,InspectorMessage,JSONHighlighter.JSONHighlighteris a generic utility that doesn't belong in a view file.InAppNotificationOverlay.swift— overlay + two ~200-line peer banner views.Features/Home/HomeCardComponents.swift(7 types) — borderline;StatCellButtonStyleandHomeCardAlertListcould split.Features/Devices/DeviceListViewModel.swift(4 types) — worth a look.Model files with multiple tightly-coupled DTOs (
DeviceDoc.swift,BridgeSettings.swift,BridgeInfo.swift,Device.swift) are intentionally left alone — Swift convention permits grouping related value types.Hardcoded numbers where a token should be used
131 instances total. Worst offenders:
MQTTInspectorView.swiftLightControl/LightControlCard.swiftFanControl/FanControlCard.swiftInAppNotificationOverlay.swiftCoverControl/CoverControlCard.swiftClimateControl/ClimateControlCard.swiftBackup/RestoreGuideSheet.swiftComponents/Doc/DocBlockView.swiftComponents/DeviceCard.swiftTwo recurring patterns:
spacing: 2,4,5,6;.padding(.vertical, 2).xs = 4,sm = 8—5and6have no token.frame(width: 22),frame(width: 14),frame(width: 220),.padding(.bottom, 80),frame(height: 0.5). The0.5divider height appears in ≥4 card files and clearly wants aSize.hairlinetoken.FanControlCardeven declares a localrowIconWidth: CGFloat = 22that duplicatesSize.cardSymbol = 22.Plan
Sequenced so each step is a small, reviewable PR with low blast radius. Each step can land independently on
devand ship as part ofv1.3.1.Step 1 — Extend
DesignTokensto cover the gapsFile:
Shellbee/Shared/DesignTokens.swift.Additions:
Spacing.xxs: CGFloat = 2— for tight inline gaps (spacing: 2,.padding(.vertical, 2)).Size.hairline: CGFloat = 0.5— for divider heights / sub-pixel strokes.5and6: snap toxs(4) where it's a tight gap orsm(8) where it's a row spacing. Spot-check visually onLightControlCardandFanControlCardbefore committing — these values were likely chosen by eye, so collapsing them to existing tokens is the goal, not adding new ones.Size.notificationBottomInset: CGFloat = 80(used 3× inInAppNotificationOverlay).No behavior changes in this step — tokens are added but not yet referenced.
Step 2 — Mechanical sweep: replace literals with tokens
Touch one file per commit so reviews stay scannable. Order by impact:
Shared/FanControl/FanControlCard.swift(10 literals; also delete the duplicate localrowIconWidthand useSize.cardSymbol).Shared/LightControl/LightControlCard.swift(10).Shared/CoverControl/CoverControlCard.swift(7).Shared/ClimateControl/ClimateControlCard.swift(6).Shared/Components/DeviceCard.swift(5).Features/Notifications/InAppNotificationOverlay.swift(9).Features/Settings/Developer/MQTTInspectorView.swift(11).Features/Settings/Backup/RestoreGuideSheet.swift(6).Shared/Components/Doc/DocBlockView.swift(5).After each commit, run
xcodebuild buildfor the simulator and visually diff the touched screen viamcp__XcodeBuildMCP__screenshotagainstdevto confirm nothing shifted.Step 3 — Split
FanControlCard.swift(754 → ~300)FanFeatureSections→ new fileShellbee/Shared/FanControl/FanFeatureSections.swift(mirrors the existingShared/LightControl/LightFeatureSections.swiftpattern).FanExtraRow→Shellbee/Shared/FanControl/FanExtraRow.swift.DisclosureRowprivate insideFanFeatureSections.swiftif only used there; otherwise lift toShared/Components/.membershipExceptionslist — both new files need to be added there if they (transitively) referenceAppEnvironment.Step 4 — Split
InAppNotificationOverlay.swift(728 → ~300)InAppNotificationBanner→Shellbee/Features/Notifications/InAppNotificationBanner.swift.FastTrackBanner→Shellbee/Features/Notifications/FastTrackBanner.swift.PreviewHostadjacent to whichever file has the most preview surface — likely the banner itself.Step 5 — Lift
JSONHighlighterout ofMQTTInspectorView.swiftJSONHighlighter→Shellbee/Shared/Components/JSONHighlighter.swift(it's reusable;BeautifulPayloadViewwould also benefit).InspectorMessageandSubscribeStoreshould also split.SubscribeStoreis a substantial@Observableclass (~35 lines) — split intoFeatures/Settings/Developer/SubscribeStore.swift.InspectorMessageis small and tightly coupled — leave inline.Step 6 — Reduce
Features/Home/HomeCardComponents.swift(7 types)Lower priority. Split if the file grows further; otherwise leave as a deliberate "primitives bag" (it's under 200 lines).
Step 7 — Re-evaluate
AppStore.swiftandDeviceDocNormalizer.swiftThese need design thought, not mechanical extraction. Defer to follow-up issues if the work is non-trivial:
AppStore.swift(691): consider splitting viaextension AppStorefiles per domain (AppStore+Devices.swift,AppStore+Groups.swift,AppStore+Logs.swift,AppStore+OTA.swift). Read the file end-to-end first to confirm that's the right cut.DeviceDocNormalizer.swift(646): 7 parsing types — likely a candidate for aCore/Parsing/Normalizer/subfolder with one type per file. Confirm by reading; some of the 7 may be small inline helpers that should stay together.If either turns out to need substantial refactoring, file a separate issue and target a later milestone.
Acceptance criteria
Shellbee/exceeds 600 lines (hard cap), unless explicitly justified in a comment at the top of the file.grepfor hardcoded SwiftUI numeric modifiers (.padding\(\d+\),frame(width:\s*\d+,spacing:\s*\d+\),cornerRadius(\d+)) returns zero matches outsideDesignTokens.swift, ignoring0and tokens-passed-as-args.Notes
CLAUDE.md/AGENTS.mdrules already shipped ondev. Until this issue is resolved, the codebase will fail its own conventions in the audited files.