Skip to content

v1.3.1: code-structure + design-tokens cleanup#35

Merged
tashda merged 18 commits into
mainfrom
dev
Apr 29, 2026
Merged

v1.3.1: code-structure + design-tokens cleanup#35
tashda merged 18 commits into
mainfrom
dev

Conversation

@tashda

@tashda tashda commented Apr 28, 2026

Copy link
Copy Markdown
Owner

Summary

Mechanical cleanup pass tracked in #33 — bringing the codebase into compliance with the new Code structure and tightened Design tokens rules in CLAUDE.md / AGENTS.md. No new features, no behavior changes intended; every commit built green via xcodebuild.

11 commits across 41 files, +904 / -874 lines.

What changed

1. DesignTokens — three core additions, plus scoped one-offs

  • Spacing.xxs = 2 — covers spacing: 2, .padding(.vertical, 2) call sites that previously fell between xs (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) and climateSetpointMinWidth (72) for the climate setpoint +/- controls.
  • Size.inspectorTabPickerWidth (220) and inspectorPayloadInset (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:

  • `spacing: 5` → `Spacing.xs (4)` — closer to xs than sm
  • `spacing: 6` → `Spacing.sm (8)` — closer to sm than xs
  • `spacing: 4` → `Spacing.xs` (token alias)
  • `spacing: 2` / `.padding(.vertical, 2)` → `Spacing.xxs (2)` (new token)
  • `.frame(height: 0.5)` → `Size.hairline (0.5)` (new token)
  • `.frame(width: 22)` → `Size.cardSymbol` (existing token)
  • Unique frames/insets that don't fit existing tokens → new scoped tokens (see above)

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

File Before After Extracted
`Shellbee/Shared/FanControl/FanControlCard.swift` 754 538 `FanFeatureSections.swift`, `FanExtraRow.swift`
`Shellbee/Features/Notifications/InAppNotificationOverlay.swift` 728 300 `InAppNotificationBanner.swift`, `FastTrackBanner.swift`
`Shellbee/Features/Settings/Developer/MQTTInspectorView.swift` 411 276 `SubscribeStore.swift` (+ `InspectorMessage`), `Shared/Components/JSONHighlighter.swift`

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

  • `HomeCardComponents.swift` (152 lines, 7 small types) — well under any threshold and a deliberate "primitives bag." Left as-is per the issue plan.
  • `AppStore.swift` (691) and `DeviceDocNormalizer.swift` (646) — over the 600 hard cap but need design-level partition decisions (`@Observable` extension splits for AppStore; per-type-or-cluster decisions for the parser). Tracked as follow-up in Split oversized AppStore.swift and DeviceDocNormalizer.swift #34 with a concrete plan, on the Backlog milestone.

Acceptance criteria from #33

  • No hardcoded SwiftUI numeric modifiers outside `DesignTokens.swift` (modulo intentional `spacing: 0`).
  • No file has 3+ unrelated top-level types (DTO bundles excepted).
  • Every commit built green via `xcodebuild` (Debug, iPhone 17 Pro Max sim).
  • [⏭] No file in `Shellbee/` exceeds 600 lines — two exceptions (`AppStore`, `DeviceDocNormalizer`), tracked in Split oversized AppStore.swift and DeviceDocNormalizer.swift #34.

Closes #33.

Test plan

  • `xcodebuild -scheme Shellbee -destination 'platform=iOS Simulator,name=iPhone 17 Pro Max' build` — green
  • Visual regression eyeball pass through the touched screens. Specifically check that the 5 → 4 and 6 → 8 spacing snaps don't visibly shift any chrome:
    • Home — bridge card (statusLine spacing 6 → 8), reconnecting label
    • Devices list — DeviceCard identity metrics row, hairline divider
    • Device detail — Light card (eyebrow, brightness percent stack, color circle, snapshot info row), Fan card (eyebrow, hero air-quality stack, age tile), Cover card (eyebrow, position percent, action buttons, tilt row), Climate card (eyebrow, target setpoint row, +/- buttons, mode chips), Switch / Lock / Sensor / Remote cards, GenericExposeCard, GroupCard
    • Notifications — main banner header icon column + close button (frame 22 → `cardSymbol`), expanded body leading inset (30 → derived), bottom inset
    • Settings — Acknowledgements donate / repo rows (icon column 28 → `settingsIconFrame`), Backup → Restore guide step circles
    • Developer → MQTT Inspector — message row payload bubble, picker width
    • Splash, Permit Join QR sheet, Home → Add cards section
  • Run unit + UI tests on simulator
  • Tag `v1.3.1` from main after merge per CLAUDE.md release flow

Notes

  • This is mechanical cleanup. Every change is a 1:1 substitution of a literal for a token (with documented snap rules), or a top-level type extracted to its own file with no body changes. Reviewers can scan the diff for surprise.
  • The `spacing: 0` exemption is intentional and consistent — those are deliberate zero-gap stacks (table cells, divider columns, etc.), not eyeballed values.
  • After merge, follow the documented post-squash flow on `dev`:
    ```
    git switch dev
    git pull origin main --rebase
    ```

🤖 Generated with Claude Code

tashda and others added 13 commits April 28, 2026 23:11
…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>
@tashda tashda added this to the v1.3.1 milestone Apr 29, 2026
@tashda

tashda commented Apr 29, 2026

Copy link
Copy Markdown
Owner Author

Update — #34 also folded in

Two more commits land the follow-up split that was originally deferred:

  • `2088ebe` AppStore: 691 → 116 lines (split into 5 `extension AppStore` files: Events, OTA, Notifications, Devices, Logs).
  • `b34dc27` DeviceDocNormalizer: 646 → 361 lines (DTOs lifted to `DeviceDocumentationModels.swift`, pairing helpers extracted to `DeviceDocNormalizer+Pairing.swift`).

This PR now closes both #33 and #34 — the original scope plus the deferred follow-up.

Updated acceptance criteria

  • No file in `Shellbee/` exceeds 600 lines. ✅ Largest now 538 (`FanControlCard.swift`).
  • Zero hardcoded SwiftUI numeric modifiers outside `DesignTokens.swift` (modulo intentional `spacing: 0`).
  • No file has 3+ unrelated top-level types (DTO bundles excepted).
  • Every commit built green via `xcodebuild`.

Additional test plan items for the new commits

  • Seeder-driven smoke test of the apply-event paths, which moved to `AppStore+Events.swift`:
    • `docker-compose up` and connect — confirm `bridge/info`, `bridge/state`, `bridge/devices`, `bridge/groups` all populate
    • `POST /api/scenarios/device/join {fail:false}` — confirm join + interview log entries and the "Interviewing → Successful" notification banner
    • `POST /api/scenarios/ota/run` — confirm OTA state transitions through requested → updating → idle (handlers moved to `AppStore+OTA.swift`)
    • `POST /api/scenarios/device/leave` — confirm leave notification
    • `POST /api/scenarios/device/spam` — confirm log buffer cap (insert helpers moved to `AppStore+Logs.swift`)
  • Open a few device docs in the catalog browser — confirm pairing guides render unchanged (helpers moved to `DeviceDocNormalizer+Pairing.swift`)

tashda and others added 3 commits April 29, 2026 08:28
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>
@tashda

tashda commented Apr 29, 2026

Copy link
Copy Markdown
Owner Author

Update — wider literal sweep folded in

Three 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):

  • `db44792` DesignTokens: Typography / Opacity / Duration token expansion (~30 new tokens).
  • `eeed408` Sweep all 104 `font(.system(size:))` call sites across every card, banner, and chrome view.
  • `187efa7` Sweep tracking / opacity / durations / scale factors / strokes (~120 sites).

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 #36

While 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

  • Spot-check on simulator that the eyebrow text, hero metrics, and snapshot rows render the same as before. The font sizes, weights, and tracking values are all unchanged — call sites just reference the tokens. But snap rules (5→4, 6→8 spacing) from earlier still apply.
  • Permit Join sheet — the countdown ring stroke and ring backing now use `Size.permitJoinRingStroke`; check the ring still looks the same.
  • Light Color picker / Light Temperature presets — selection ring now uses `Size.lightSelectionStroke` (2pt), unchanged.
  • Splash screen — `splashRadius`/`splashY` and `Tracking.splashTitle` (-1) preserve the original glyph styling.

…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>
@tashda

tashda commented Apr 29, 2026

Copy link
Copy Markdown
Owner Author

Update — #36 resolved

`c60f97d` applies the agreed design decisions:

  • A: card eyebrows unified on 11pt. SensorCard, RemoteCard, and GenericExposeCard headers swap from the 12pt variant. The 12pt + tracking-0.6 token survives but renamed `sectionHeaderLabel` / `sectionHeaderIcon` / `sectionHeaderTracking` — its real role is in-card section dividers (FanControlCard `sectionView` and a couple of icon overlays at the same scale).
  • B: tile metric tokens renamed for clarity. `identityValue` → `identityTileValue` (24pt — packed 2×2 grids), `metricValue` → `featureTileValue` (30pt — prominent feature tiles). Same for the unit pairs. No visual change.
  • D: banner glyph unified on 15pt. FastTrackBanner now uses `notificationLevelIcon`. `fastTrackLevelIcon` token deleted.
  • C and E left as-is (unit-scales-with-value is the right pattern; hero 48pt off-state vs 56pt metric is a deliberate visual register).

PR now closes #33, #34, and #36.

Visual diff to verify on simulator

The two visible-change items are small but real:

  • Sensor / Remote / GenericExpose card headers: eyebrow text is now 1pt smaller and 0.1 less letter-spacing. Look at any sensor or remote tile in Devices list — the "SENSOR" / "REMOTE" label at the top.
  • FastTrack notification glyph (e.g. "Copied to Clipboard" toast): icon is now 1pt larger.

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>
@tashda

tashda commented Apr 29, 2026

Copy link
Copy Markdown
Owner Author

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` namespace

Five typed timing constants previously scattered across networking and view-model files now source their values from a centralized `Shellbee/Core/Config/AppConfig.swift`:

```swift
AppConfig.Networking.websocketConnectionTimeout // 10s
AppConfig.Networking.websocketFirstMessageTimeout // 5s
AppConfig.Networking.discoveryProbeTimeout // 1.5s
AppConfig.UX.notificationCoalesceWindow // 1.5s
AppConfig.UX.recentDeviceWindow // 30 min
```

These are behavior tokens — distinct from `DesignTokens` (visual). Future Settings entries ("Connection timeout", "Discovery scan duration") wire here.

`DesignTokens.Ratio` — proportional layout multipliers

12 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.

Closes

PR #35 now closes #33, #34, #36, and #37.

Final state

A comprehensive grep for any numeric magic value — SwiftUI literals, proportional ratios, runtime tuning — returns zero matches outside the two token files. Two clean namespaces:

  • `DesignTokens` (visual): Spacing / Size / CornerRadius / Typography / Opacity / Duration / Shadow / Ratio / Tracking / Threshold / Gradient
  • `AppConfig` (behavior): Networking / UX

Build green throughout. Ready for visual eyeball + CI before squash-merge to main, then `git tag v1.3.1` per the release flow.

@tashda tashda merged commit 6b3d361 into main Apr 29, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Code structure & design-tokens cleanup: oversized files and hardcoded SwiftUI values

1 participant