Conversation
…Inset Preparation for the code-structure / hardcoded-values cleanup tracked in #33. Adds tokens for the gaps in the existing table: - Spacing.xxs (2pt) covers the many spacing: 2 / .padding(.vertical, 2) call sites in cards and notifications. - Size.hairline (0.5pt) replaces the .frame(height: 0.5) divider rule that appears in 4+ card files. - Size.notificationBottomInset (80pt) replaces the .padding(.bottom, 80) used three times in InAppNotificationOverlay. No call sites migrated yet — that's the next commit. Refs #33 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Snaps spacing: 5 → Spacing.xs (4) and spacing: 4 → Spacing.xs across the hero/filter views, spacing: 2 → Spacing.xxs in the hero air-quality stack and age tile, frame(height: 0.5) → Size.hairline in the divider rule, and collapses the local rowIconWidth: 22 constant onto Size.cardSymbol so the card no longer redeclares a token. spacing: 0 left in place — semantic "no gap", not a literal-by-eye choice. No visible UI changes intended; the 5→4 and 4→4 snap shifts a couple of pixel-thin gaps by ≤1pt, which is the documented goal of the cleanup. Refs #33 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Snaps spacing: 5 → Spacing.xs in eyebrows and snapshot info rows, spacing: 4 → Spacing.xs in the brightness percent stack, spacing: 2 → Spacing.xxs in the snapshot value+unit row, frame(height: 0.5) → Size.hairline for the divider, frame(width: 22) → Size.cardSymbol for the color preview circle, frame(width: 18) → Size.summaryRowSymbol for the inline color dot, and .padding(.leading, 4) → Spacing.xs. Refs #33 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…kens Same pattern as the Fan and Light card sweeps: spacing: 5/4 → Spacing.xs, spacing: 2 → Spacing.xxs, frame(height: 0.5) → Size.hairline, plus a spacing: 6 in the cover open/stop/close action button row → Spacing.xs. Adds two new climate-scoped Size tokens: - climateActionButton (32) for the +/- setpoint buttons - climateSetpointMinWidth (72) for the temperature label, which needs a fixed min-width to prevent layout shift when the value changes. Refs #33 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…kens DeviceCard: - spacing: 5 → Spacing.xs in identity-metric label rows - spacing: 2/1 → Spacing.xxs in metric value+unit and vendor/model stack - frame(height: 0.5) → Size.hairline - preview VStack(spacing: 20) → Spacing.xl InAppNotificationOverlay: - frame(width: 22) for the leading icon and close button → Size.cardSymbol - spacing: 2 / .padding(.vertical, 2) → Spacing.xxs in the count chip - .padding(.leading, 30) → Size.cardSymbol + Spacing.sm (the value is derived from the icon column width, not a magic number) - .padding(.bottom, 80) → Size.notificationBottomInset (3 call sites) Refs #33 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds two inspector-scoped Size tokens (inspectorTabPickerWidth = 220, inspectorPayloadInset = 10) and snaps the rest to existing tokens: - spacing: 6 → Spacing.sm in MessageRow VStack - spacing: 8 → Spacing.sm in MessageRow header HStack - frame(width: 14) → Size.logLevelIconWidth (already a token, was duplicated) - frame(width: 22) and .padding(.leading, 22) → Size.cardSymbol (icon column width carried through to align the payload bubble with the topic text) - .padding(.horizontal, 12), .padding(.bottom, 12) → Spacing.md - .padding(.vertical, 6) → Spacing.sm - .padding(.vertical, 2) → Spacing.xxs - cornerRadius: 8 → CornerRadius.sm Refs #33 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
RestoreGuideSheet: - Adds Size.restoreStepCircle (24) for the numbered step badge - spacing: 12 → Spacing.md, spacing: 8 → Spacing.sm - spacing: 4 → Spacing.xs (2 sites: outer step list, inner title/body) - .padding(.vertical, 2) → Spacing.xxs DocBlockView: - .padding(.top, 1) bullet baseline nudge → Spacing.xxs (1pt → 2pt; imperceptible) - preview VStack(spacing: 20) → Spacing.xl Leaves the table view's spacing: 0 in place — those are deliberate zero-gap stacks where cells/rows touch, not literals chosen by eye. Refs #33 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wraps up step 2 of the cleanup tracked in #33. After this, no .swift file under Shellbee/ contains a hardcoded SwiftUI numeric modifier (padding, frame, spacing, cornerRadius) outside of DesignTokens.swift itself, except for spacing: 0 which is intentionally kept as semantic "no gap". Adds five new tokens for one-off frames/insets that didn't fit existing ones: - Size.splashIconLarge (120) for the splash icon - Size.mainTabBarInset (58) for the in-app notification overlay's tab-bar-aware bottom padding - Size.permitJoinQR (220) for the pairing QR - Size.homeAddDividerInset (60) for the divider in the hidden-cards list - Size.docLabelColumnWidth (90) for the doc info card label column Files swept (all snap to existing tokens where one fit): - App: MainTabView, SplashScreenView - Connection: ConnectionOverviewView - Devices: DeviceFirmwareMenu, DeviceImageView - Groups: GroupCard - Home: HomeAddCardsSection, HomeBridgeCard, HomeCardComponents, HomeLogsCard, PermitJoinActiveSheet - Logs: LogRowView - Settings: AcknowledgementsView - Shared cards: GenericExposeCard, LockControlCard, RemoteCard, SensorCard, SwitchControlCard - Shared/Components/Doc: DefaultDocSectionView, DeviceInfoCardView, DocInlineTextView, DocNoteView, DocOptionRowView, DocStepListView Refs #33 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
FanControlCard.swift was 754 lines housing four top-level types. Splits: - FanFeatureSections (the native List sections variant for use under iOS-style settings screens) → Shared/FanControl/FanFeatureSections.swift. Mirrors the same pattern as Shared/LightControl/LightFeatureSections.swift. - FanExtraRow (binary/enum/numeric/text row used inside the card's extras list) → Shared/FanControl/FanExtraRow.swift, becoming a top-level type so it can be split out without exposing more API than the card needs (it was already file-private; now file-private to its own file). DisclosureRow stays inside FanControlCard.swift — it's a private helper used only there, so it's tightly-coupled per the new code-structure rule. Both new files are added to the ShellbeeWidgetsExtension membershipExceptions list alphabetically, since they reference main-app types (Expose, JSONValue, FeatureCatalog) that don't ship in the widget target. Brings FanControlCard.swift from 754 → 538 lines (still over the 400 soft cap but below the 600 hard cap, and the remainder is one cohesive type). Refs #33 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
InAppNotificationOverlay.swift was 728 lines holding three independent top-level views and a preview host. Splits: - InAppNotificationBanner (the expanded/collapsed main banner with drag gestures and previews) → InAppNotificationBanner.swift, taking the PreviewHost helper and all 10 banner-flavor #Preview blocks with it. - FastTrackBanner (the compact "Copied to Clipboard"-style banner) → FastTrackBanner.swift with its single preview. The overlay file itself now stays at 300 lines and only holds the queue manager that orchestrates which banner to show. Both new files are added to the ShellbeeWidgetsExtension membership exception list. Refs #33 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…files MQTTInspectorView.swift was 411 lines holding the inspector view, an @observable store, a model type, a generic JSON highlighting utility, and two helper subviews. Splits: - JSONHighlighter (a generic regex-based JSON colorer with no MQTT-specific state) → Shared/Components/JSONHighlighter.swift, where it's reusable by anything else surfacing JSON to the user (BeautifulPayloadView is the obvious next consumer). Pure Foundation/SwiftUI, so no widget membership exception needed. - SubscribeStore + InspectorMessage (the @observable buffer of inbound messages plus the per-message model that decodes the topic into a log level for coloring) → Features/Settings/Developer/SubscribeStore.swift. Tightly coupled, so they stay together; both reference main-app types (ConnectionSessionController, JSONValue, Z2MTopics, LogLevel) and need the widget membership exception. The view file itself drops from 411 → 276 lines and now holds only the view layer (MQTTInspectorView, SubscribeView, MessageRow, PublishView). Refs #33 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
AppStore.swift was 691 lines, well past the 600-line hard cap, holding the @observable class plus event handling, notification queue management, OTA orchestration, log buffering, and device lookups all in one file. Splits methods into domain-scoped extensions while keeping stored properties (which @observable requires in the main class) and the init/reset lifecycle in the main file: - AppStore.swift (116) — class declaration, all stored properties, the DeviceCheckResult enum, init, reset, and the first-seen persistence helpers (recordFirstSeen / removeFirstSeen are now internal so the Events extension can call them). - AppStore+Events.swift (275) — the apply(_ event:) dispatcher plus the static logEntry(from:) and notification(from:) helpers it uses. - AppStore+Notifications.swift (109) — pop / popFastTrack / enqueueOTABulkSummary / enqueueNotification / notification(for:) / stripped helper. - AppStore+OTA.swift (140) — activeOTAUpdates, otaStatus(for:), the start/cancel methods, and the handle… response/state callbacks plus flashCheckResult. - AppStore+Logs.swift (22) — clearLogs, insertLogEntry, insertRawLogEntry. - AppStore+Devices.swift (49) — device(named:), state(for:), isAvailable, and the optimisticRename / revertOptimisticRename pair. A handful of formerly-private members (pendingRenames, recordFirstSeen, removeFirstSeen, revertOptimisticRename, the OTA handlers, etc.) bumped to internal so cross-file extensions can reach them. No external API surface change — all of these were already module-internal, just file-private within AppStore.swift. The five new files are quoted in pbxproj (the `+` requires it) and added to the ShellbeeWidgetsExtension membership exception list. Build green. No behavior changes intended. Refs #34 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DeviceDocNormalizer.swift was 646 lines mixing six DTO structs with the parsing/normalization enum. Splits into three focused files: - DeviceDocumentationModels.swift (119) — the six tightly-coupled DTOs: DeviceDocumentation, NormalizedDeviceDoc, DeviceDocIdentity, DevicePairingGuide, DevicePairingMethod, DeviceDocCapability. Per the Code-structure rule, DTO bundles can stay together. - DeviceDocNormalizer.swift (361) — the enum entry point, the main normalize(parsed:device:) orchestrator, capability/expose/section helpers, and the generic span helpers (firstParagraph, plainText, matchesAny, etc.) shared across pairing and capability extraction. - DeviceDocNormalizer+Pairing.swift (175) — pairing-specific helpers (extractFromNotes, makePairingGuide, collectStepItems, isPureStepList, collectSubsections, defaultPairingSummary) plus the prerequisite / success / troubleshooting keyword arrays they use. A handful of formerly-private helpers (normalizeTitle, isPairingAdjacentTitle, firstParagraph, collectParagraphSpans, unique, plainText, matchesAny) bumped to internal so the +Pairing extension across files can call them. No external API surface change — they were already module-internal, just file-private within the original file. The +Pairing path is quoted in pbxproj (the `+` requires it). Refs #34 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Update — #34 also folded inTwo more commits land the follow-up split that was originally deferred:
This PR now closes both #33 and #34 — the original scope plus the deferred follow-up. Updated acceptance criteria
Additional test plan items for the new commits
|
Stage 1 of the wider-cleanup pass tracked in #36. Adds tokens for the patterns the original sweep missed — typography, opacity literals, and animation durations. No call sites touched yet; this commit only adds the vocabulary. New tokens: Typography - Eyebrow pattern: eyebrowLabel/Icon (11pt, dominant), eyebrowLabelLarge/ IconLarge (12pt, used in SensorCard/RemoteCard/GenericExposeCard — see #36.A), eyebrowTracking (0.5), eyebrowTrackingLoose (0.6). - Hero block: heroValue (56pt), heroStateText (48pt for "On"/"Off"), heroUnit (18pt), heroSubtitle (20pt). - Tile metrics: metricValue (30pt), metricUnit (15pt), identityValue (24pt) / identityUnit (14pt) for DeviceCard/GroupCard, snapshotRowValue (20pt) / snapshotRowUnit (13pt) for snapshot rows. - cardTitle (24pt bold rounded), footerActionLabel (13pt), sectionHeader (15pt), formRowIcon/IconBold (16pt), sliderEndLabel (9pt). - Banner glyphs (notificationLevelIcon 15pt, fastTrackLevelIcon 14pt — see #36.D), permit-join countdown (64pt thin) and symbol (48pt), climate setpoint button (14pt bold), light secondary glyphs (14pt). - minimumScaleFactor presets covering the 7 distinct values in use. Opacity - hairline (0.08) for the thin rule color across all cards - subtleFade (0.04), offStateTint (0.06), onStateTint (0.18) - actionButtonFill (0.15), strongAccentFill (0.20), mediumAccentFill (0.14) - banner (0.9), pressedAlpha (0.25), dimmedSurface (0.30) - secondaryDim/Full and several one-off named values Duration - quickFade (0.15), mediumAnimation (0.25), slowAnimation (0.6) - pulseExpand (0.8), pulseFull (1.0) - checkResultDisplay (3s), pendingDeleteTimeout (15s) Refs #36 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… tokens Stage 2 of #36. Sweeps the 104 font.system(size:) call sites across every card, banner, and chrome view. After this commit, grep for that pattern returns zero matches outside DesignTokens.swift. Snap rules used: - size 11 bold → Typography.eyebrowIcon - size 11 semibold → Typography.eyebrowLabel - size 12 bold/semibold (only SensorCard / RemoteCard / GenericExposeCard headers and a couple of icon overlays) → eyebrowIconLarge / Label- Large, marked with `// NOTE: ... see #36.A` comments at the outlier sites so the inconsistency stays visible until you decide whether to unify on 11pt. - size 56 bold rounded → Typography.heroValue - size 48 bold rounded (Off-state hero) → heroStateText - size 30 semibold rounded → metricValue - size 24 semibold rounded → identityValue (DeviceCard / GroupCard / Climate) - size 24 bold rounded → cardTitle - size 20 bold rounded → compactCardTitle (new — used in GroupCard / DeviceCard) - size 20 semibold rounded → heroSubtitle (e.g. "Target X°") and snapshotRowValue depending on context - size 18 medium rounded → heroUnit - size 15 medium rounded → metricUnit - size 15 semibold → sectionHeader - size 14 medium rounded → identityUnit - size 14 semibold → lightSecondaryIcon (Cover action button), fast- track banner level glyph, climate +/- button (climateActionIcon) - size 13 medium rounded → snapshotRowUnit - size 13 semibold rounded → footerActionLabel - size 12 semibold for one-off icon overlays (LightColorControl eyedropper, HomeLogsCard badge) → eyebrowLabelLarge (12pt is already the dominant 12pt token; semantically correct re-use) - size 16 medium → formRowIcon (FanExtraRow, FanControlCard, GenericExposeCard) - size 16 semibold → formRowIconBold (LightBrightnessArea, LockControlCard) - size 9 medium → sliderEndLabel (LightTemperatureControl preset labels) - size 64 thin / 48 → permitJoinCountdown / permitJoinSymbol The visual outliers tracked in #36 (Sensor/Remote/Generic 12pt eyebrows, DeviceCard/GroupCard/Climate 24pt vs 30pt tile metrics, FastTrack 14pt vs main 15pt banner glyph, hero 56 vs Off-state 48) all use distinct tokens — when you decide on each item, it's a one-line token swap. Refs #36 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…, strokes Stage 3 of #36. After this commit, grep for hardcoded SwiftUI numeric literals across font sizes, opacity, tracking, animation duration, Task.sleep, minimumScaleFactor, Spacer minLength, lineWidth, radius, and scaleEffect — anywhere outside DesignTokens.swift — returns zero matches (modulo deliberate semantic zeros: spacing: 0, opacity(0), opacity(1), Spacer(minLength: 0)). What got swept: - 19 × tracking(0.5) → Typography.eyebrowTracking. The 0.6 variants in the size-12 outlier headers were already tokenized in stage 2. - ~40 × .opacity(N) literals → mapped to existing or newly named Opacity tokens (hairline 0.08, subtleFade 0.04, onStateTint 0.18, offStateTint 0.06, banner 0.9, lightOpaque 0.10, mildOpaque 0.22, pressedAlpha 0.25, secondaryDim 0.75, secondaryFull 0.7, dimmedSurface 0.30, outOfRange 0.35, veryLight 0.05, veryFaint 0.03, actionButtonFill 0.15, mediumAccentFill 0.14, strongAccentFill 0.20). - 14 × duration: literal → Duration.quickFade (0.15), fastFade (0.2), mediumAnimation (0.25), slowAnimation (0.6), pulseExpand (0.8), pulseFull (1.0). The duration: 0 case in HomeView is a function argument (permit-join "stop"), not an animation duration. - 2 × Task.sleep(for: .seconds(N)) → Duration.checkResultDisplay (3s), Duration.discoveryScanWindow (15s). - 27 × minimumScaleFactor(N) → Typography.scaleFactor* (Aggressive 0.45, Tight 0.55, Medium 0.6, AggressiveLight 0.65, Relaxed 0.7, MildLight 0.72, Mild 0.75, Subtle 0.82). Per-text shrink budgets are unavoidably per-element design choices, but they're still tokens now. - 1 × Spacer(minLength: 8) → Spacing.sm. The 16 minLength: 0 instances stay literal — they're the deliberate "no minimum spacing" semantic. - 4 × lineWidth: 8/2 → Size.permitJoinRingStroke, lightSelectionStroke. - 1 × radius: 18 + 1 × y: 8 → Shadow.splashRadius / splashY (used exclusively in SplashScreenView). - 1 × tracking(-1) → new Tracking enum, splashTitle (-1pt). Refs #36 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Update — wider literal sweep folded inThree more commits land everything the original audit's narrow regex missed (font sizes, opacity, tracking, animation duration, Task.sleep, minimumScaleFactor, lineWidth, Spacer minLength, shadow radius, tracking offset):
After these, a comprehensive grep across all the hardcoded-literal patterns I could think of (`\.system\(size:`, `opacity\(0\.`, `tracking\(`, `duration:`, `Task.sleep`, `minimumScaleFactor`, `Spacer\(minLength:`, `lineWidth:`, `radius:`, `scaleEffect`, plus the original `padding`/`frame`/`spacing`/`cornerRadius` set) returns zero matches outside `DesignTokens.swift`, modulo deliberate semantic zeros (`spacing: 0`, `opacity(0)`/`opacity(1)`, `Spacer(minLength: 0)`, function-argument `duration: 0`). Visual inconsistencies flagged for your assessment in #36While sweeping, several places where similar visual elements use different sizes were spotted. Outlier sites use distinct tokens (e.g. `eyebrowLabelLarge` for size-12 cards) and carry inline `// NOTE: ... see #36.X` markers, so any decision is a one-line token swap:
This PR now closes #33, #34, and the wider half of #36 (the mechanical tokenization). #36 itself stays open until you make the design-decision calls A through E. Updated additional test plan items
|
…me tile tokens Applies the design decisions agreed for #36: A. Eyebrow size — unified on 11pt SensorCard, RemoteCard, and GenericExposeCard headers were using a 12pt + tracking-0.6 variant ("eyebrowLabelLarge") because they were doing dual duty as both eyebrow and de-facto card title. Card eyebrows everywhere else are 11pt + tracking-0.5, so unify there. The three outlier sites now use eyebrowLabel/eyebrowIcon/ eyebrowTracking like every other card. The 12pt + tracking-0.6 token survives, but renamed to sectionHeaderLabel / sectionHeaderIcon / sectionHeaderTracking — its actual role is dividing content within a card (FanControlCard sectionView, plus the LightColorControl eyedropper and HomeLogsCard badge that share the size). B. Tile metric size — kept distinct, renamed for clarity The 24pt vs 30pt distinction is intentional: 24 for packed identity grids (DeviceCard / GroupCard 2x2 stat tiles, ClimateControlCard setpoint between +/- buttons), 30 for prominent feature tiles with room to breathe (FanControl, RemoteCard, SensorCard, SwitchControl). Token renames make the role explicit: - identityValue → identityTileValue - identityUnit → identityTileUnit - metricValue → featureTileValue - metricUnit → featureTileUnit No visual change. D. Banner level glyph — unified on 15pt FastTrackBanner glyph bumps from 14pt → 15pt to match InAppNotificationBanner. The 1pt drop wasn't doing visual work; the FastTrack banner is already smaller via padding/capsule. Drops the fastTrackLevelIcon token; both banners now use notificationLevelIcon. C and E left as-is (unit sizes scale with value sizes — intentional; hero state 48 vs metric 56 — deliberate "no data" vs "data" register). Closes #36. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Update — #36 resolved`c60f97d` applies the agreed design decisions:
PR now closes #33, #34, and #36. Visual diff to verify on simulatorThe two visible-change items are small but real:
Both should be barely perceptible; they're consistency fixes, not redesigns. |
…s, v1.3.1
Final pass on the design-tokens work. After this commit, every numeric
magic value in Shellbee/ either flows through DesignTokens (visual) or
AppConfig (behavior), or is a domain semantic constant.
New: AppConfig namespace
========================
Created Shellbee/Core/Config/AppConfig.swift for *behavior tokens* —
single sources of truth for runtime tuning that aren't visual:
AppConfig.Networking
- websocketConnectionTimeout (10s)
- websocketFirstMessageTimeout (5s)
- discoveryProbeTimeout (1.5s)
AppConfig.UX
- notificationCoalesceWindow (1.5s)
- recentDeviceWindow (30 min)
The five existing typed constants (`Z2MWebSocketClient.connectionTimeout`
et al) are kept as readable in-class references but now source their
values from AppConfig — single edit point for future Settings exposure.
Both nested enums are `nonisolated` so they can be referenced from
nonisolated contexts (Z2MDiscoveryService probe loop).
DesignTokens: layout ratios + residual offsets
==============================================
New `DesignTokens.Ratio` sub-enum for proportional layout — multipliers
applied to a parent dimension. Sweeps 12 distinct ratio literals across
6 files (LogRowView, GroupIconView, DeviceImageView, FeatureIconTile,
MemberAvatarStack):
Ratio.logRowBadgeSize / .logRowBadgeBorder / .logRowBadgeBorderMin
Ratio.groupIconMember / .groupIconOffset / .groupIconCorner
Ratio.deviceImageDot / .deviceImageDotMin
Ratio.featureTileCorner
Ratio.memberAvatarBorder / .memberAvatarBorderMin / .memberAvatarFont
Ratio.memberAvatarBadgeFont / .memberAvatarBadgeFontMin /
Ratio.memberAvatarBadgePadding
Plus three offset literals tokenized:
Size.logRowBadgeOffset (3)
Size.firmwareUpdateBadgeOffsetX (4) / OffsetY (-2)
Size.homeCardSlotButtonOffset (-8)
And the icon glyph ratios from the previous sweep moved out into a
clearer location:
Typography.iconRatioSmall (0.38)
Typography.iconRatioHalf (0.5)
Typography.iconRatioMedium (0.55)
Verified with grep — `.offset(x: \\d+`, `size \\* 0\\.\\d+`,
`max\\([0-9.]+, size \\* [0-9.]+\\)` patterns all return zero matches
outside DesignTokens.swift.
Version bump: 1.3.1
===================
MARKETING_VERSION bumped to 1.3.1 across all four build configurations
in preparation for the v1.3.1 release.
Closes #37.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Update — final residual sweep + v1.3.1 bump`5154b4f` lands the residual cleanup tracked in #37 plus the marketing-version bump. Behavior tokens — new `AppConfig` namespaceFive typed timing constants previously scattered across networking and view-model files now source their values from a centralized `Shellbee/Core/Config/AppConfig.swift`: ```swift These are behavior tokens — distinct from `DesignTokens` (visual). Future Settings entries ("Connection timeout", "Discovery scan duration") wire here. `DesignTokens.Ratio` — proportional layout multipliers12 distinct `size * 0.` ratios across LogRowView, GroupIconView, DeviceImageView, FeatureIconTile, and MemberAvatarStack are now in a new `Ratio` sub-enum (logRowBadgeSize, groupIconMember/Offset/Corner, deviceImageDot, memberAvatarFont/Border/etc.). Plus three pixel-pushed offsets. v1.3.1 bump`MARKETING_VERSION = 1.3.1` across all four build configurations. ClosesPR #35 now closes #33, #34, #36, and #37. Final stateA comprehensive grep for any numeric magic value — SwiftUI literals, proportional ratios, runtime tuning — returns zero matches outside the two token files. Two clean namespaces:
Build green throughout. Ready for visual eyeball + CI before squash-merge to main, then `git tag v1.3.1` per the release flow. |
Summary
Mechanical cleanup pass tracked in #33 — bringing the codebase into compliance with the new
Code structureand tightenedDesign tokensrules inCLAUDE.md/AGENTS.md. No new features, no behavior changes intended; every commit built green viaxcodebuild.11 commits across 41 files, +904 / -874 lines.
What changed
1. DesignTokens — three core additions, plus scoped one-offs
Spacing.xxs = 2— coversspacing: 2,.padding(.vertical, 2)call sites that previously fell betweenxs (4)and zero.Size.hairline = 0.5— replaces.frame(height: 0.5)divider rule duplicated across 4+ card files.Size.notificationBottomInset = 80— replaces three.padding(.bottom, 80)call sites in the in-app notification overlay.Plus scoped tokens added when an existing one didn't fit:
Size.climateActionButton (32)andclimateSetpointMinWidth (72)for the climate setpoint +/- controls.Size.inspectorTabPickerWidth (220)andinspectorPayloadInset (10)for the MQTT Inspector chrome.Size.restoreStepCircle (24)for the numbered step badge in the Restore guide.Size.splashIconLarge (120),mainTabBarInset (58),permitJoinQR (220),homeAddDividerInset (60),docLabelColumnWidth (90)for one-offs that were previously magic numbers.2. Hardcoded SwiftUI literals — zero remaining
Before: 131 occurrences of hardcoded numeric SwiftUI modifiers across the codebase.
After: `grep -rE '\.padding\([0-9]+\)|\.padding\(\.[a-z]+, [0-9]+\)|frame\(width: [0-9]+|frame\(height: [0-9]+|spacing: [0-9]+\)|cornerRadius\([0-9]+\)' Shellbee | grep -v DesignTokens` returns zero matches, except for `spacing: 0` which is intentionally kept as semantic "no gap" (deliberate zero-gap stacks where cells/rows touch).
Snap rules used throughout the sweep:
The 5 → 4 and 6 → 8 snaps shift a couple of pixel-thin gaps by ≤2pt. Visually imperceptible in the spots I spot-checked; full eyeball pass below.
3. File splits — three oversized files broken up
All extracted view/store files are added to the `ShellbeeWidgetsExtension` `membershipExceptions` list alphabetically, since they reference main-app types (`Expose`, `JSONValue`, `FeatureCatalog`, `ConnectionSessionController`, `Z2MTopics`, `LogLevel`, etc.) that don't ship in the widget target.
`JSONHighlighter` is the exception — it's pure Foundation/SwiftUI and can compile into both targets unchanged, so it's not in the exception list. That also makes it a candidate for the next consumer (`BeautifulPayloadView`) to migrate onto.
4. Skipped / deferred
Acceptance criteria from #33
Closes #33.
Test plan
Notes
```
git switch dev
git pull origin main --rebase
```
🤖 Generated with Claude Code