From 71aa3205e8222316a177f88798b3757554635fb5 Mon Sep 17 00:00:00 2001 From: Khoi Nguyen Date: Thu, 9 Jul 2026 15:32:28 +0700 Subject: [PATCH 1/4] ALFMOB-269: Source Badge styling from design tokens (semantic colors + spacing) --- .../Indicators/BadgeTabViewModifier.swift | 4 ++-- .../Indicators/BadgeViewModifier.swift | 23 +++++++++---------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/Alfie/AlfieKit/Sources/SharedUI/Components/Indicators/BadgeTabViewModifier.swift b/Alfie/AlfieKit/Sources/SharedUI/Components/Indicators/BadgeTabViewModifier.swift index ac3ad77b..0c830a7a 100644 --- a/Alfie/AlfieKit/Sources/SharedUI/Components/Indicators/BadgeTabViewModifier.swift +++ b/Alfie/AlfieKit/Sources/SharedUI/Components/Indicators/BadgeTabViewModifier.swift @@ -17,12 +17,12 @@ public struct BadgeTabViewModifier: ViewModifier { } private func setAppearance() { - UITabBarItem.appearance().badgeColor = Primitives.Colours.semanticError600.ui + UITabBarItem.appearance().badgeColor = Theme.surfaceBackgroundDestructive.ui let font = theme.font.body.small.uiFont let attributes: [NSAttributedString.Key: Any] = [ .font: font, - .foregroundColor: Primitives.Colours.neutrals0.ui, + .foregroundColor: Theme.contentContentInvertedPrimary.ui, ] UITabBarItem.appearance().setBadgeTextAttributes(attributes, for: .normal) UITabBarItem.appearance().setBadgeTextAttributes(attributes, for: .selected) diff --git a/Alfie/AlfieKit/Sources/SharedUI/Components/Indicators/BadgeViewModifier.swift b/Alfie/AlfieKit/Sources/SharedUI/Components/Indicators/BadgeViewModifier.swift index 468f2c11..4646e598 100644 --- a/Alfie/AlfieKit/Sources/SharedUI/Components/Indicators/BadgeViewModifier.swift +++ b/Alfie/AlfieKit/Sources/SharedUI/Components/Indicators/BadgeViewModifier.swift @@ -8,13 +8,12 @@ import SwiftUI /// If the value is set to higher than `maxValue`, the badge will show `maxValue+` public struct BadgeViewModifier: ViewModifier { private enum Constants { - static let badgeHeight = 16.0 - static let badgePadding = 4.0 - static let textPadding = 4.0 - static let indicatorHeight = 12.0 - static let indicatorWidth = 12.0 - static let capsuleOffsetXFactor = 3 - static let borderLineWidth = 1.0 + static let badgeHeight = Primitives.Spacing.spacing16 + static let textPadding = Primitives.Spacing.spacing4 + static let indicatorHeight = Primitives.Spacing.spacing12 + static let indicatorWidth = Primitives.Spacing.spacing12 + static let capsuleOffsetXFactor = 3 // layout multiplier, no design token + static let borderLineWidth = 1.0 // hairline, no design token (no spacing1) } private var helper: BadgeHelper @@ -34,13 +33,13 @@ public struct BadgeViewModifier: ViewModifier { let badgeLabelsOffsetX = CGFloat(helper.badgeLabel.count * Constants.capsuleOffsetXFactor) ZStack(alignment: .center) { RoundedRectangle(cornerRadius: Sizing.radiusRounded) - .fill(Primitives.Colours.semanticError600) + .fill(Theme.surfaceBackgroundDestructive) .frame(height: Constants.badgeHeight) RoundedRectangle(cornerRadius: Sizing.radiusRounded) - .stroke(Primitives.Colours.neutrals0, lineWidth: Constants.borderLineWidth) + .stroke(Theme.surfaceBackgroundPrimary, lineWidth: Constants.borderLineWidth) .frame(height: Constants.badgeHeight) Text.build(theme.font.body.small(helper.badgeLabel)) - .foregroundStyle(Primitives.Colours.neutrals0) + .foregroundStyle(Theme.contentContentInvertedPrimary) .padding(.all, Constants.textPadding) } .frame(height: Constants.badgeHeight) @@ -50,10 +49,10 @@ public struct BadgeViewModifier: ViewModifier { } else { ZStack(alignment: .center) { Circle() - .fill(Primitives.Colours.semanticError600) + .fill(Theme.surfaceBackgroundDestructive) .frame(width: Constants.indicatorWidth, height: Constants.indicatorHeight) RoundedRectangle(cornerRadius: Sizing.radiusRounded) - .stroke(Primitives.Colours.neutrals0, lineWidth: Constants.borderLineWidth) + .stroke(Theme.surfaceBackgroundPrimary, lineWidth: Constants.borderLineWidth) .frame(width: Constants.indicatorWidth, height: Constants.indicatorHeight) } .offset(x: (Constants.indicatorWidth / 2), y: -((Constants.indicatorHeight / 2))) From 97d809a8a4f476bdcdd2111ffd4aa1d895d45159 Mon Sep 17 00:00:00 2001 From: Khoi Nguyen Date: Thu, 9 Jul 2026 15:32:36 +0700 Subject: [PATCH 2/4] ALFMOB-269: Add plan & decision log for Badge token refactor --- .../_status.md | 31 +++++ .../ALFMOB-269-refactor-badge-tokens/grill.md | 23 ++++ .../phase-1-badge-tokens.md | 41 +++++++ .../ALFMOB-269-refactor-badge-tokens/plan.md | 106 ++++++++++++++++++ .../ALFMOB-269-refactor-badge-tokens/scope.md | 47 ++++++++ 5 files changed, 248 insertions(+) create mode 100644 Docs/Plans/ALFMOB-269-refactor-badge-tokens/_status.md create mode 100644 Docs/Plans/ALFMOB-269-refactor-badge-tokens/grill.md create mode 100644 Docs/Plans/ALFMOB-269-refactor-badge-tokens/phase-1-badge-tokens.md create mode 100644 Docs/Plans/ALFMOB-269-refactor-badge-tokens/plan.md create mode 100644 Docs/Plans/ALFMOB-269-refactor-badge-tokens/scope.md diff --git a/Docs/Plans/ALFMOB-269-refactor-badge-tokens/_status.md b/Docs/Plans/ALFMOB-269-refactor-badge-tokens/_status.md new file mode 100644 index 00000000..6f0dd945 --- /dev/null +++ b/Docs/Plans/ALFMOB-269-refactor-badge-tokens/_status.md @@ -0,0 +1,31 @@ +# ALFMOB-269 — Refactor Badge component to use design tokens + +- **Ticket:** [ALFMOB-269](https://mindera.atlassian.net/browse/ALFMOB-269) (Story) +- **Base:** main +- **Branch:** ALFMOB-269-refactor-badge-tokens +- **Type:** feat + +## Summary +Migrate `BadgeViewModifier` and `BadgeTabViewModifier` from hardcoded `Colors.*` / sizing / font +values to design-token-generated colors, typography, and spacing/shape tokens. Call sites must be +unchanged. + +## Acceptance Criteria +- [ ] Badge uses token values for all visual properties (color, text color, sizing, typography) +- [ ] Badge renders correctly with different count values (1, 9, 99, 999+) +- [ ] Both view modifier variants work correctly +- [ ] Existing call sites do not need changes + +## Phase checklist +- [x] 0. Load profile +- [x] 1. Resolve input (ticket fetched) +- [x] 2. Init (branch + status) +- [x] 3. Scout → scope.md +- [x] 4. Plan → plan.md +- [x] 4.5 Grill → hardened plan +- [x] GATE: approval ✓ approved 2026-07-09 +- [x] 5. Implement (ios-execute) ✓ verify PASSED, review APPROVED +- [ ] 6. Commit ← next +- [ ] 7. PR +- [ ] 8. Ticket transition +- [ ] 9. Report diff --git a/Docs/Plans/ALFMOB-269-refactor-badge-tokens/grill.md b/Docs/Plans/ALFMOB-269-refactor-badge-tokens/grill.md new file mode 100644 index 00000000..827709c3 --- /dev/null +++ b/Docs/Plans/ALFMOB-269-refactor-badge-tokens/grill.md @@ -0,0 +1,23 @@ +# Grill: Refactor Badge component to use design tokens +**Plan**: Docs/Plans/ALFMOB-269-refactor-badge-tokens/plan.md **Ticket**: ALFMOB-269 **Date**: 2026-07-09 **Branch**: ALFMOB-269-refactor-badge-tokens + +## Decisions +| # | Decision | Recommended | Chosen | Plan change | +|---|---|---|---|---| +| 1 | Color migration depth: elevate to semantic `Theme.*` vs keep raw `Primitives.*` (sizing-only) | Elevate to `Theme.*` | Elevate to `Theme.*` | Approach/File-Changes keep the `Theme.*` mapping; both files' colors migrate | +| 2 | Remove unused `badgePadding` constant | Remove | Remove | Phase-1 step 1 removes the orphan | + +## Answered by the codebase (not asked) +- **Border-stroke token** → `Theme.surfaceBackgroundPrimary` (=neutrals0). Folded into Decision 1; the stroke is the surface behind the badge. Both candidates resolve to the same value, no visual risk. +- **Extra `1`/`9` label tests** — redundant: `BadgeHelperTests.swift:65-81` already covers the `≤maxVal` branch (99→"99") and `>maxVal` branch (147/1000→"99+"). `1`/`9` exercise the identical false-branch. Dropped. +- **Sizing token targets** — `Sizing+Generated.swift` has no badge-specific tokens; `Sizing` is itself defined over `Primitives.Spacing`. So raw dimensions use `Primitives.Spacing.spacing{4,12,16}` (verified equal to the old literals). +- **"999+" in the ticket AC** — badge caps at 99 (`BadgeHelper.maxVal=99` → "99+"). Existing intended behavior; a token refactor must not change it. Recorded in Out of Scope. +- **Call-site safety (AC4)** — public API `badgeView`/`tabItemBadge` signatures untouched; the 12 call sites (scope.md) need no edits. +- **Precedent for elevation** — ALFMOB-271 ("Source ThemedButton styling from semantic design tokens") in recent git history establishes the semantic-layer pattern for component refactors. + +## Assumptions surfaced +- The ticket's premise ("currently references `Colors.*` and hardcoded sizing") is partly stale: colors/typography/radius are already token-based. The real gap is sizing + (by choice) semantic-layer elevation. +- No visual change is expected — every token substitution resolves to the current literal/primitive value. Correctness = builds, tests green, values identical, call sites untouched. + +## Still open (owner) +- None. All decisions closed; plan ready for the approval gate. diff --git a/Docs/Plans/ALFMOB-269-refactor-badge-tokens/phase-1-badge-tokens.md b/Docs/Plans/ALFMOB-269-refactor-badge-tokens/phase-1-badge-tokens.md new file mode 100644 index 00000000..42613ba3 --- /dev/null +++ b/Docs/Plans/ALFMOB-269-refactor-badge-tokens/phase-1-badge-tokens.md @@ -0,0 +1,41 @@ +## Phase 1: Badge token migration + +### Goal +Replace the remaining hardcoded sizing and raw-primitive color references in the two Badge view +modifiers with generated design tokens, with no visual change and no call-site change. + +### Acceptance criteria +- [ ] All `BadgeViewModifier.Constants` numeric style values are token-backed, except the two with + no token equivalent (`borderLineWidth`, `capsuleOffsetXFactor`), which stay literals with a + brief comment noting no token exists. +- [ ] Badge colors reference the semantic `Theme.*` layer (fill / text / stroke) in both modifiers. +- [ ] Dead `badgePadding` constant removed. +- [ ] `BadgeHelperTests` green; label logic unchanged (AC2). +- [ ] Public API `badgeView(...)` / `tabItemBadge(...)` signatures unchanged (AC4); no call site edited. + +### Steps +1. **Migrate sizing constants** (file: `SharedUI/Components/Indicators/BadgeViewModifier.swift:10`, + size: XS) — in `Constants`, set `badgeHeight = Primitives.Spacing.spacing16`, + `textPadding = Primitives.Spacing.spacing4`, `indicatorHeight`/`indicatorWidth = + Primitives.Spacing.spacing12`. Remove unused `badgePadding`. Leave `borderLineWidth = 1.0` and + `capsuleOffsetXFactor = 3` as literals (no token) with a one-line `// no design token` comment. + Why: closes the actual token gap. Test: build (values identical → no behavior change). +2. **Elevate colors to `Theme.*`** (file: `SharedUI/Components/Indicators/BadgeViewModifier.swift:37`, + size: XS) — fill `Theme.surfaceBackgroundDestructive`, text + `Theme.contentContentInvertedPrimary`, stroke `Theme.surfaceBackgroundPrimary` (both `.fill` + and `.stroke` sites in capsule + indicator branches). Why: semantic layer per design-system + intent. Test: build; values resolve identically. +3. **Elevate tab-badge colors** (file: `SharedUI/Components/Indicators/BadgeTabViewModifier.swift:20`, + size: XS) — `badgeColor = Theme.surfaceBackgroundDestructive.ui`; text attribute + `.foregroundColor = Theme.contentContentInvertedPrimary.ui`. Font already tokenized. Why: AC3 + parity across both variants. Test: build. + *(Grill dropped the optional AC2 label-test step — `BadgeHelperTests` already covers the + below-max and above-max branches; `1`/`9` are redundant.)* + +### Checkpoint +- [ ] `./Alfie/scripts/verify.sh` passes (build + unit; integration skippable — no BFF surface). +- [ ] All acceptance criteria above met. +- [ ] Manual: `BadgeDemoView` preview renders badge + indicator identically to `main`. + +### Depends on +none diff --git a/Docs/Plans/ALFMOB-269-refactor-badge-tokens/plan.md b/Docs/Plans/ALFMOB-269-refactor-badge-tokens/plan.md new file mode 100644 index 00000000..88e11b5e --- /dev/null +++ b/Docs/Plans/ALFMOB-269-refactor-badge-tokens/plan.md @@ -0,0 +1,106 @@ +--- +title: Refactor Badge component to use design tokens +ticket: ALFMOB-269 +status: completed +complexity: LOW +mode: auto +blockedBy: [] +blocks: [] +created: 2026-07-09 +--- + +## Overview +Finish migrating the SharedUI Badge overlay (`BadgeViewModifier`) and tab-item badge +(`BadgeTabViewModifier`) onto the generated design-token system. Colors, typography, and corner +radius are **already** token-based; the remaining gap is the hardcoded numeric sizing in +`BadgeViewModifier.Constants`. This plan also elevates the color references from raw `Primitives.*` +to the semantic `Theme.*` layer (identical values → zero visual change), the layer the design +system intends component code to consume. Public API (`badgeView`, `tabItemBadge`) is unchanged, +so no call site changes (AC #4). + +## Acceptance Criteria (feature-level) +- [ ] AC1 — Badge uses token values for all visual properties: background, text color, border, sizing, typography. +- [ ] AC2 — Badge renders correctly across count values (1, 9, 99, >99 → "99+"). *(label logic; already covered by `BadgeHelperTests`)* +- [ ] AC3 — Both variants (`BadgeViewModifier` capsule/indicator + `BadgeTabViewModifier` tab) work correctly. +- [ ] AC4 — Existing call sites do not need changes (public modifier API unchanged). + +## Approach +Single vertical slice — a styling-token substitution across two files in one module. No state, +navigation, networking, DI, or localization surface. Values map 1:1 so there is **no intended +visual change**; correctness = "compiles, existing tests green, call sites untouched, values +identical." + +**Token mapping** + +*Sizing* (`BadgeViewModifier.Constants`, currently raw literals) → `Primitives.Spacing.*` +(there are no badge-specific semantic `Sizing.*` tokens; `Sizing` itself is defined in terms of +`Primitives.Spacing`, so consuming spacing primitives for raw dimensions is consistent): +| Constant | Value | Token | +|---|---|---| +| `badgeHeight` | 16 | `Primitives.Spacing.spacing16` | +| `textPadding` | 4 | `Primitives.Spacing.spacing4` | +| `indicatorHeight` | 12 | `Primitives.Spacing.spacing12` | +| `indicatorWidth` | 12 | `Primitives.Spacing.spacing12` | +| `borderLineWidth` | 1 | **no token** (no `spacing1`) — keep literal `1.0` | +| `capsuleOffsetXFactor` | 3 | layout multiplier, not a design value — keep literal `3` | +| `badgePadding` | 4 | **unused dead constant** — remove (recommended) | + +*Colors* — elevate `Primitives.*` → semantic `Theme.*` (identical underlying values): +| Element | Current | Proposed `Theme.*` | Underlying | +|---|---|---|---| +| Badge/indicator fill | `Primitives.Colours.semanticError600` | `Theme.surfaceBackgroundDestructive` | semanticError600 | +| Badge text | `Primitives.Colours.neutrals0` | `Theme.contentContentInvertedPrimary` | neutrals0 | +| Border stroke | `Primitives.Colours.neutrals0` | `Theme.surfaceBackgroundPrimary` | neutrals0 | +| Tab badge color (UIKit) | `Primitives.Colours.semanticError600.ui` | `Theme.surfaceBackgroundDestructive.ui` | semanticError600 | +| Tab badge text (UIKit) | `Primitives.Colours.neutrals0.ui` | `Theme.contentContentInvertedPrimary.ui` | neutrals0 | + +*Typography / radius* — already token-based (`theme.font.body.small`, `Sizing.radiusRounded`); +no change. + +## Phases +Single phase — see `phase-1-badge-tokens.md`. + +## File Changes (Summary Table) +| File | Module | Type | Change | Owner | +|---|---|---|---|---| +| `Alfie/AlfieKit/Sources/SharedUI/Components/Indicators/BadgeViewModifier.swift` | SharedUI | edit | Sizing constants → `Primitives.Spacing.*`; colors → `Theme.*`; drop dead `badgePadding` | - | +| `Alfie/AlfieKit/Sources/SharedUI/Components/Indicators/BadgeTabViewModifier.swift` | SharedUI | edit | UIKit badge colors → `Theme.*.ui` | - | +| `Alfie/AlfieKit/Tests/SharedUITests/BadgeHelperTests.swift` | SharedUITests | edit (optional) | add explicit `1` and `9` label cases to trace AC2 enumeration | - | + +## Feature Flag +n/a — pure styling refactor of a shared component; no behavioral change, no rollout risk. + +## Testing Strategy +- **Unit**: existing `BadgeHelperTests` (label logic) must stay green. Optionally add `1`/`9` + label cases so AC2's enumeration is literally traceable (marginal value; label logic already + covered at boundaries). +- **Snapshot**: none — snapshot suite is disabled repo-wide with no committed refs (project + memory). Not introducing one for a zero-value-change substitution. +- **UI**: none needed — no new accessibility surface, public API unchanged. +- **Manual/visual**: `DebugMenu/UI/Demo/Indicators/BadgeDemoView.swift` (11 badge variants) via + SwiftUI preview / debug menu — confirm badge + indicator render identically before/after. +- **Gate**: `./Alfie/scripts/verify.sh` green (build + unit; `--skip-integration` acceptable — + no BFF surface touched). + +## Risks & Mitigations +| Risk | Likelihood | Mitigation | +|---|---|---| +| Semantic `Theme.*` token drifts from badge intent later (e.g. `surfaceBackgroundDestructive` remapped) and silently changes the badge | Low | Values identical today; badge already coupled to `semanticError*`. Documented mapping. Revisit only if a badge-specific token is added. | +| `spacing12`/`spacing16` differ subtly from the old literals | None | Verified equal: `spacing12`=12.0, `spacing16`=16.0, `spacing4`=4.0. | +| Removing `badgePadding` breaks a hidden reference | None | Grepped — zero references (dead constant). | + +## Out of Scope +- Changing the 99-cap behavior. AC lists "999+" but the badge intentionally shows "99+" for any + value >99 (`BadgeHelper.maxVal=99`). Token refactor must not alter this. +- Adding snapshot infrastructure. +- Touching `BadgeHelper.swift` (pure logic, no styling). +- `borderLineWidth`/`capsuleOffsetXFactor` (no matching tokens). + +## Decisions (grilled 2026-07-09 — see grill.md) +1. **Elevate to `Theme.*`.** ✓ Migrate colors to the semantic layer (matches ALFMOB-271 + ThemedButton precedent + project memory; zero visual change). +2. **Border-stroke → `Theme.surfaceBackgroundPrimary`.** ✓ The ring reads as the surface behind + the badge (resolves to neutrals0, identical to today). +3. **Remove dead `badgePadding`.** ✓ Orphan inside the enum being refactored; removal authorized. +4. **No extra label tests.** Value-below-max (99→"99") and above-max (147/1000→"99+") paths are + already covered by `BadgeHelperTests`; `1`/`9` hit the identical branch. Step 4 of phase-1 dropped. diff --git a/Docs/Plans/ALFMOB-269-refactor-badge-tokens/scope.md b/Docs/Plans/ALFMOB-269-refactor-badge-tokens/scope.md new file mode 100644 index 00000000..ddcc0c02 --- /dev/null +++ b/Docs/Plans/ALFMOB-269-refactor-badge-tokens/scope.md @@ -0,0 +1,47 @@ +# Scout Report: Badge component styling (ALFMOB-269) + +**Branch**: ALFMOB-269-refactor-badge-tokens **Agents**: 0 (direct read — single module, 3 small files) + +## Relevant Files +### Package: SharedUI (component under refactor) +- `Alfie/AlfieKit/Sources/SharedUI/Components/Indicators/BadgeViewModifier.swift` — capsule/indicator overlay badge; `Constants` enum holds hardcoded sizing; public `View.badgeView(...)`. +- `Alfie/AlfieKit/Sources/SharedUI/Components/Indicators/BadgeTabViewModifier.swift` — UITabBarItem badge appearance; public `View.tabItemBadge(...)`. +- `Alfie/AlfieKit/Sources/SharedUI/Components/Indicators/BadgeHelper.swift` — label/visibility logic (maxVal=99 → "99+"). No styling. Out of scope. + +### Generated tokens (READ-ONLY — never edit) +- `SharedUI/GeneratedTokens/Sizing+Generated.swift` — `radiusRounded=1000`, `radiusSoft=4`, `radiusStrong=16`, `iconsIconSmall=16`, icon/interactive sizes. +- `SharedUI/GeneratedTokens/Primitives+Generated.swift` — `Primitives.Spacing.spacing{0,2,4,8,12,14,16,...}`, `Primitives.Colours.*`. +- `SharedUI/GeneratedTokens/Theme+Generated.swift` — semantic layer (surface/content/button/link). Relevant: `surfaceBackgroundDestructive`(=semanticError600), `contentContentInvertedPrimary`(=neutrals0), `surfaceBackgroundPrimary`(=neutrals0). +- `SharedUI/GeneratedTokens/Typography+Generated.swift` — `theme.font.body.small` already used. + +### Call sites (public API — must NOT change; AC #4) +- `SharedUI/Components/Toolbar/ThemedToolbarButton.swift:69,74` — `.badgeView(badgeValue:)` +- `AppFeature/UI/TabBarItemView.swift:42` — `.badgeView(badgeValue:)` +- `DebugMenu/UI/Demo/Indicators/BadgeDemoView.swift` (×11) — demo/preview only +- `tabItemBadge(...)` — **no production call sites found** (only the modifier itself). Keep per AC #2 ("both variants work"), but note it's currently unreferenced. + +### Tests +- `Alfie/AlfieKit/Tests/SharedUITests/BadgeHelperTests.swift` — covers `BadgeHelper` label/visibility logic only. No styling/appearance tests exist. + +## Patterns Observed — IMPORTANT (ticket premise is partly stale) +The badges are **already partially token-migrated**, contrary to the ticket's "Current styling: directly references `Colors.*` and hardcoded sizing": +- **Colors**: already `Primitives.Colours.semanticError600` (bg) and `Primitives.Colours.neutrals0` (text + stroke). No legacy `Colors.*` references remain. +- **Typography**: already `theme.font.body.small` (SwiftUI) / `theme.font.body.small.uiFont` (UIKit tab). +- **Corner radius**: already `Sizing.radiusRounded`. + +**Actual remaining work = the hardcoded `Constants` sizing in `BadgeViewModifier`:** +| Constant | Value | Candidate token | +|---|---|---| +| `badgeHeight` | 16 | `Primitives.Spacing.spacing16` (or `Sizing.iconsIconSmall`) | +| `textPadding` | 4 | `Primitives.Spacing.spacing4` | +| `indicatorHeight` | 12 | `Primitives.Spacing.spacing12` | +| `indicatorWidth` | 12 | `Primitives.Spacing.spacing12` | +| `borderLineWidth` | 1 | no token (no spacing1) — keep literal | +| `capsuleOffsetXFactor` | 3 | layout multiplier, not a token — keep literal | +| `badgePadding` | 4 | **UNUSED dead constant** | + +## Unresolved Questions (for plan/grill) +1. **Semantic vs primitive colors**: elevate `Primitives.Colours.*` → semantic `Theme.*` (`surfaceBackgroundDestructive`, `contentContentInvertedPrimary`)? Memory note prefers the semantic layer for component refactors. Values are identical, so no visual change. +2. **Dead `badgePadding`**: remove while refactoring the `Constants` enum, or leave (pre-existing dead code)? +3. **No-token values** (`borderLineWidth=1`, `capsuleOffsetXFactor=3`): leave as literals — no matching token. +4. **Verification of AC #2** ("renders correctly with 1/9/99/999+"): no snapshot suite (disabled repo-wide per memory). How to assert appearance — SPM unit tests on token wiring, or rely on `BadgeHelper` label tests + build? From 681a2266a802ef44b320dd77ffdbc8d89d121c82 Mon Sep 17 00:00:00 2001 From: Khoi Nguyen Date: Thu, 9 Jul 2026 15:37:13 +0700 Subject: [PATCH 3/4] chore: remove plan after PR (ALFMOB-269) --- .../_status.md | 31 ----- .../ALFMOB-269-refactor-badge-tokens/grill.md | 23 ---- .../phase-1-badge-tokens.md | 41 ------- .../ALFMOB-269-refactor-badge-tokens/plan.md | 106 ------------------ .../ALFMOB-269-refactor-badge-tokens/scope.md | 47 -------- 5 files changed, 248 deletions(-) delete mode 100644 Docs/Plans/ALFMOB-269-refactor-badge-tokens/_status.md delete mode 100644 Docs/Plans/ALFMOB-269-refactor-badge-tokens/grill.md delete mode 100644 Docs/Plans/ALFMOB-269-refactor-badge-tokens/phase-1-badge-tokens.md delete mode 100644 Docs/Plans/ALFMOB-269-refactor-badge-tokens/plan.md delete mode 100644 Docs/Plans/ALFMOB-269-refactor-badge-tokens/scope.md diff --git a/Docs/Plans/ALFMOB-269-refactor-badge-tokens/_status.md b/Docs/Plans/ALFMOB-269-refactor-badge-tokens/_status.md deleted file mode 100644 index 6f0dd945..00000000 --- a/Docs/Plans/ALFMOB-269-refactor-badge-tokens/_status.md +++ /dev/null @@ -1,31 +0,0 @@ -# ALFMOB-269 — Refactor Badge component to use design tokens - -- **Ticket:** [ALFMOB-269](https://mindera.atlassian.net/browse/ALFMOB-269) (Story) -- **Base:** main -- **Branch:** ALFMOB-269-refactor-badge-tokens -- **Type:** feat - -## Summary -Migrate `BadgeViewModifier` and `BadgeTabViewModifier` from hardcoded `Colors.*` / sizing / font -values to design-token-generated colors, typography, and spacing/shape tokens. Call sites must be -unchanged. - -## Acceptance Criteria -- [ ] Badge uses token values for all visual properties (color, text color, sizing, typography) -- [ ] Badge renders correctly with different count values (1, 9, 99, 999+) -- [ ] Both view modifier variants work correctly -- [ ] Existing call sites do not need changes - -## Phase checklist -- [x] 0. Load profile -- [x] 1. Resolve input (ticket fetched) -- [x] 2. Init (branch + status) -- [x] 3. Scout → scope.md -- [x] 4. Plan → plan.md -- [x] 4.5 Grill → hardened plan -- [x] GATE: approval ✓ approved 2026-07-09 -- [x] 5. Implement (ios-execute) ✓ verify PASSED, review APPROVED -- [ ] 6. Commit ← next -- [ ] 7. PR -- [ ] 8. Ticket transition -- [ ] 9. Report diff --git a/Docs/Plans/ALFMOB-269-refactor-badge-tokens/grill.md b/Docs/Plans/ALFMOB-269-refactor-badge-tokens/grill.md deleted file mode 100644 index 827709c3..00000000 --- a/Docs/Plans/ALFMOB-269-refactor-badge-tokens/grill.md +++ /dev/null @@ -1,23 +0,0 @@ -# Grill: Refactor Badge component to use design tokens -**Plan**: Docs/Plans/ALFMOB-269-refactor-badge-tokens/plan.md **Ticket**: ALFMOB-269 **Date**: 2026-07-09 **Branch**: ALFMOB-269-refactor-badge-tokens - -## Decisions -| # | Decision | Recommended | Chosen | Plan change | -|---|---|---|---|---| -| 1 | Color migration depth: elevate to semantic `Theme.*` vs keep raw `Primitives.*` (sizing-only) | Elevate to `Theme.*` | Elevate to `Theme.*` | Approach/File-Changes keep the `Theme.*` mapping; both files' colors migrate | -| 2 | Remove unused `badgePadding` constant | Remove | Remove | Phase-1 step 1 removes the orphan | - -## Answered by the codebase (not asked) -- **Border-stroke token** → `Theme.surfaceBackgroundPrimary` (=neutrals0). Folded into Decision 1; the stroke is the surface behind the badge. Both candidates resolve to the same value, no visual risk. -- **Extra `1`/`9` label tests** — redundant: `BadgeHelperTests.swift:65-81` already covers the `≤maxVal` branch (99→"99") and `>maxVal` branch (147/1000→"99+"). `1`/`9` exercise the identical false-branch. Dropped. -- **Sizing token targets** — `Sizing+Generated.swift` has no badge-specific tokens; `Sizing` is itself defined over `Primitives.Spacing`. So raw dimensions use `Primitives.Spacing.spacing{4,12,16}` (verified equal to the old literals). -- **"999+" in the ticket AC** — badge caps at 99 (`BadgeHelper.maxVal=99` → "99+"). Existing intended behavior; a token refactor must not change it. Recorded in Out of Scope. -- **Call-site safety (AC4)** — public API `badgeView`/`tabItemBadge` signatures untouched; the 12 call sites (scope.md) need no edits. -- **Precedent for elevation** — ALFMOB-271 ("Source ThemedButton styling from semantic design tokens") in recent git history establishes the semantic-layer pattern for component refactors. - -## Assumptions surfaced -- The ticket's premise ("currently references `Colors.*` and hardcoded sizing") is partly stale: colors/typography/radius are already token-based. The real gap is sizing + (by choice) semantic-layer elevation. -- No visual change is expected — every token substitution resolves to the current literal/primitive value. Correctness = builds, tests green, values identical, call sites untouched. - -## Still open (owner) -- None. All decisions closed; plan ready for the approval gate. diff --git a/Docs/Plans/ALFMOB-269-refactor-badge-tokens/phase-1-badge-tokens.md b/Docs/Plans/ALFMOB-269-refactor-badge-tokens/phase-1-badge-tokens.md deleted file mode 100644 index 42613ba3..00000000 --- a/Docs/Plans/ALFMOB-269-refactor-badge-tokens/phase-1-badge-tokens.md +++ /dev/null @@ -1,41 +0,0 @@ -## Phase 1: Badge token migration - -### Goal -Replace the remaining hardcoded sizing and raw-primitive color references in the two Badge view -modifiers with generated design tokens, with no visual change and no call-site change. - -### Acceptance criteria -- [ ] All `BadgeViewModifier.Constants` numeric style values are token-backed, except the two with - no token equivalent (`borderLineWidth`, `capsuleOffsetXFactor`), which stay literals with a - brief comment noting no token exists. -- [ ] Badge colors reference the semantic `Theme.*` layer (fill / text / stroke) in both modifiers. -- [ ] Dead `badgePadding` constant removed. -- [ ] `BadgeHelperTests` green; label logic unchanged (AC2). -- [ ] Public API `badgeView(...)` / `tabItemBadge(...)` signatures unchanged (AC4); no call site edited. - -### Steps -1. **Migrate sizing constants** (file: `SharedUI/Components/Indicators/BadgeViewModifier.swift:10`, - size: XS) — in `Constants`, set `badgeHeight = Primitives.Spacing.spacing16`, - `textPadding = Primitives.Spacing.spacing4`, `indicatorHeight`/`indicatorWidth = - Primitives.Spacing.spacing12`. Remove unused `badgePadding`. Leave `borderLineWidth = 1.0` and - `capsuleOffsetXFactor = 3` as literals (no token) with a one-line `// no design token` comment. - Why: closes the actual token gap. Test: build (values identical → no behavior change). -2. **Elevate colors to `Theme.*`** (file: `SharedUI/Components/Indicators/BadgeViewModifier.swift:37`, - size: XS) — fill `Theme.surfaceBackgroundDestructive`, text - `Theme.contentContentInvertedPrimary`, stroke `Theme.surfaceBackgroundPrimary` (both `.fill` - and `.stroke` sites in capsule + indicator branches). Why: semantic layer per design-system - intent. Test: build; values resolve identically. -3. **Elevate tab-badge colors** (file: `SharedUI/Components/Indicators/BadgeTabViewModifier.swift:20`, - size: XS) — `badgeColor = Theme.surfaceBackgroundDestructive.ui`; text attribute - `.foregroundColor = Theme.contentContentInvertedPrimary.ui`. Font already tokenized. Why: AC3 - parity across both variants. Test: build. - *(Grill dropped the optional AC2 label-test step — `BadgeHelperTests` already covers the - below-max and above-max branches; `1`/`9` are redundant.)* - -### Checkpoint -- [ ] `./Alfie/scripts/verify.sh` passes (build + unit; integration skippable — no BFF surface). -- [ ] All acceptance criteria above met. -- [ ] Manual: `BadgeDemoView` preview renders badge + indicator identically to `main`. - -### Depends on -none diff --git a/Docs/Plans/ALFMOB-269-refactor-badge-tokens/plan.md b/Docs/Plans/ALFMOB-269-refactor-badge-tokens/plan.md deleted file mode 100644 index 88e11b5e..00000000 --- a/Docs/Plans/ALFMOB-269-refactor-badge-tokens/plan.md +++ /dev/null @@ -1,106 +0,0 @@ ---- -title: Refactor Badge component to use design tokens -ticket: ALFMOB-269 -status: completed -complexity: LOW -mode: auto -blockedBy: [] -blocks: [] -created: 2026-07-09 ---- - -## Overview -Finish migrating the SharedUI Badge overlay (`BadgeViewModifier`) and tab-item badge -(`BadgeTabViewModifier`) onto the generated design-token system. Colors, typography, and corner -radius are **already** token-based; the remaining gap is the hardcoded numeric sizing in -`BadgeViewModifier.Constants`. This plan also elevates the color references from raw `Primitives.*` -to the semantic `Theme.*` layer (identical values → zero visual change), the layer the design -system intends component code to consume. Public API (`badgeView`, `tabItemBadge`) is unchanged, -so no call site changes (AC #4). - -## Acceptance Criteria (feature-level) -- [ ] AC1 — Badge uses token values for all visual properties: background, text color, border, sizing, typography. -- [ ] AC2 — Badge renders correctly across count values (1, 9, 99, >99 → "99+"). *(label logic; already covered by `BadgeHelperTests`)* -- [ ] AC3 — Both variants (`BadgeViewModifier` capsule/indicator + `BadgeTabViewModifier` tab) work correctly. -- [ ] AC4 — Existing call sites do not need changes (public modifier API unchanged). - -## Approach -Single vertical slice — a styling-token substitution across two files in one module. No state, -navigation, networking, DI, or localization surface. Values map 1:1 so there is **no intended -visual change**; correctness = "compiles, existing tests green, call sites untouched, values -identical." - -**Token mapping** - -*Sizing* (`BadgeViewModifier.Constants`, currently raw literals) → `Primitives.Spacing.*` -(there are no badge-specific semantic `Sizing.*` tokens; `Sizing` itself is defined in terms of -`Primitives.Spacing`, so consuming spacing primitives for raw dimensions is consistent): -| Constant | Value | Token | -|---|---|---| -| `badgeHeight` | 16 | `Primitives.Spacing.spacing16` | -| `textPadding` | 4 | `Primitives.Spacing.spacing4` | -| `indicatorHeight` | 12 | `Primitives.Spacing.spacing12` | -| `indicatorWidth` | 12 | `Primitives.Spacing.spacing12` | -| `borderLineWidth` | 1 | **no token** (no `spacing1`) — keep literal `1.0` | -| `capsuleOffsetXFactor` | 3 | layout multiplier, not a design value — keep literal `3` | -| `badgePadding` | 4 | **unused dead constant** — remove (recommended) | - -*Colors* — elevate `Primitives.*` → semantic `Theme.*` (identical underlying values): -| Element | Current | Proposed `Theme.*` | Underlying | -|---|---|---|---| -| Badge/indicator fill | `Primitives.Colours.semanticError600` | `Theme.surfaceBackgroundDestructive` | semanticError600 | -| Badge text | `Primitives.Colours.neutrals0` | `Theme.contentContentInvertedPrimary` | neutrals0 | -| Border stroke | `Primitives.Colours.neutrals0` | `Theme.surfaceBackgroundPrimary` | neutrals0 | -| Tab badge color (UIKit) | `Primitives.Colours.semanticError600.ui` | `Theme.surfaceBackgroundDestructive.ui` | semanticError600 | -| Tab badge text (UIKit) | `Primitives.Colours.neutrals0.ui` | `Theme.contentContentInvertedPrimary.ui` | neutrals0 | - -*Typography / radius* — already token-based (`theme.font.body.small`, `Sizing.radiusRounded`); -no change. - -## Phases -Single phase — see `phase-1-badge-tokens.md`. - -## File Changes (Summary Table) -| File | Module | Type | Change | Owner | -|---|---|---|---|---| -| `Alfie/AlfieKit/Sources/SharedUI/Components/Indicators/BadgeViewModifier.swift` | SharedUI | edit | Sizing constants → `Primitives.Spacing.*`; colors → `Theme.*`; drop dead `badgePadding` | - | -| `Alfie/AlfieKit/Sources/SharedUI/Components/Indicators/BadgeTabViewModifier.swift` | SharedUI | edit | UIKit badge colors → `Theme.*.ui` | - | -| `Alfie/AlfieKit/Tests/SharedUITests/BadgeHelperTests.swift` | SharedUITests | edit (optional) | add explicit `1` and `9` label cases to trace AC2 enumeration | - | - -## Feature Flag -n/a — pure styling refactor of a shared component; no behavioral change, no rollout risk. - -## Testing Strategy -- **Unit**: existing `BadgeHelperTests` (label logic) must stay green. Optionally add `1`/`9` - label cases so AC2's enumeration is literally traceable (marginal value; label logic already - covered at boundaries). -- **Snapshot**: none — snapshot suite is disabled repo-wide with no committed refs (project - memory). Not introducing one for a zero-value-change substitution. -- **UI**: none needed — no new accessibility surface, public API unchanged. -- **Manual/visual**: `DebugMenu/UI/Demo/Indicators/BadgeDemoView.swift` (11 badge variants) via - SwiftUI preview / debug menu — confirm badge + indicator render identically before/after. -- **Gate**: `./Alfie/scripts/verify.sh` green (build + unit; `--skip-integration` acceptable — - no BFF surface touched). - -## Risks & Mitigations -| Risk | Likelihood | Mitigation | -|---|---|---| -| Semantic `Theme.*` token drifts from badge intent later (e.g. `surfaceBackgroundDestructive` remapped) and silently changes the badge | Low | Values identical today; badge already coupled to `semanticError*`. Documented mapping. Revisit only if a badge-specific token is added. | -| `spacing12`/`spacing16` differ subtly from the old literals | None | Verified equal: `spacing12`=12.0, `spacing16`=16.0, `spacing4`=4.0. | -| Removing `badgePadding` breaks a hidden reference | None | Grepped — zero references (dead constant). | - -## Out of Scope -- Changing the 99-cap behavior. AC lists "999+" but the badge intentionally shows "99+" for any - value >99 (`BadgeHelper.maxVal=99`). Token refactor must not alter this. -- Adding snapshot infrastructure. -- Touching `BadgeHelper.swift` (pure logic, no styling). -- `borderLineWidth`/`capsuleOffsetXFactor` (no matching tokens). - -## Decisions (grilled 2026-07-09 — see grill.md) -1. **Elevate to `Theme.*`.** ✓ Migrate colors to the semantic layer (matches ALFMOB-271 - ThemedButton precedent + project memory; zero visual change). -2. **Border-stroke → `Theme.surfaceBackgroundPrimary`.** ✓ The ring reads as the surface behind - the badge (resolves to neutrals0, identical to today). -3. **Remove dead `badgePadding`.** ✓ Orphan inside the enum being refactored; removal authorized. -4. **No extra label tests.** Value-below-max (99→"99") and above-max (147/1000→"99+") paths are - already covered by `BadgeHelperTests`; `1`/`9` hit the identical branch. Step 4 of phase-1 dropped. diff --git a/Docs/Plans/ALFMOB-269-refactor-badge-tokens/scope.md b/Docs/Plans/ALFMOB-269-refactor-badge-tokens/scope.md deleted file mode 100644 index ddcc0c02..00000000 --- a/Docs/Plans/ALFMOB-269-refactor-badge-tokens/scope.md +++ /dev/null @@ -1,47 +0,0 @@ -# Scout Report: Badge component styling (ALFMOB-269) - -**Branch**: ALFMOB-269-refactor-badge-tokens **Agents**: 0 (direct read — single module, 3 small files) - -## Relevant Files -### Package: SharedUI (component under refactor) -- `Alfie/AlfieKit/Sources/SharedUI/Components/Indicators/BadgeViewModifier.swift` — capsule/indicator overlay badge; `Constants` enum holds hardcoded sizing; public `View.badgeView(...)`. -- `Alfie/AlfieKit/Sources/SharedUI/Components/Indicators/BadgeTabViewModifier.swift` — UITabBarItem badge appearance; public `View.tabItemBadge(...)`. -- `Alfie/AlfieKit/Sources/SharedUI/Components/Indicators/BadgeHelper.swift` — label/visibility logic (maxVal=99 → "99+"). No styling. Out of scope. - -### Generated tokens (READ-ONLY — never edit) -- `SharedUI/GeneratedTokens/Sizing+Generated.swift` — `radiusRounded=1000`, `radiusSoft=4`, `radiusStrong=16`, `iconsIconSmall=16`, icon/interactive sizes. -- `SharedUI/GeneratedTokens/Primitives+Generated.swift` — `Primitives.Spacing.spacing{0,2,4,8,12,14,16,...}`, `Primitives.Colours.*`. -- `SharedUI/GeneratedTokens/Theme+Generated.swift` — semantic layer (surface/content/button/link). Relevant: `surfaceBackgroundDestructive`(=semanticError600), `contentContentInvertedPrimary`(=neutrals0), `surfaceBackgroundPrimary`(=neutrals0). -- `SharedUI/GeneratedTokens/Typography+Generated.swift` — `theme.font.body.small` already used. - -### Call sites (public API — must NOT change; AC #4) -- `SharedUI/Components/Toolbar/ThemedToolbarButton.swift:69,74` — `.badgeView(badgeValue:)` -- `AppFeature/UI/TabBarItemView.swift:42` — `.badgeView(badgeValue:)` -- `DebugMenu/UI/Demo/Indicators/BadgeDemoView.swift` (×11) — demo/preview only -- `tabItemBadge(...)` — **no production call sites found** (only the modifier itself). Keep per AC #2 ("both variants work"), but note it's currently unreferenced. - -### Tests -- `Alfie/AlfieKit/Tests/SharedUITests/BadgeHelperTests.swift` — covers `BadgeHelper` label/visibility logic only. No styling/appearance tests exist. - -## Patterns Observed — IMPORTANT (ticket premise is partly stale) -The badges are **already partially token-migrated**, contrary to the ticket's "Current styling: directly references `Colors.*` and hardcoded sizing": -- **Colors**: already `Primitives.Colours.semanticError600` (bg) and `Primitives.Colours.neutrals0` (text + stroke). No legacy `Colors.*` references remain. -- **Typography**: already `theme.font.body.small` (SwiftUI) / `theme.font.body.small.uiFont` (UIKit tab). -- **Corner radius**: already `Sizing.radiusRounded`. - -**Actual remaining work = the hardcoded `Constants` sizing in `BadgeViewModifier`:** -| Constant | Value | Candidate token | -|---|---|---| -| `badgeHeight` | 16 | `Primitives.Spacing.spacing16` (or `Sizing.iconsIconSmall`) | -| `textPadding` | 4 | `Primitives.Spacing.spacing4` | -| `indicatorHeight` | 12 | `Primitives.Spacing.spacing12` | -| `indicatorWidth` | 12 | `Primitives.Spacing.spacing12` | -| `borderLineWidth` | 1 | no token (no spacing1) — keep literal | -| `capsuleOffsetXFactor` | 3 | layout multiplier, not a token — keep literal | -| `badgePadding` | 4 | **UNUSED dead constant** | - -## Unresolved Questions (for plan/grill) -1. **Semantic vs primitive colors**: elevate `Primitives.Colours.*` → semantic `Theme.*` (`surfaceBackgroundDestructive`, `contentContentInvertedPrimary`)? Memory note prefers the semantic layer for component refactors. Values are identical, so no visual change. -2. **Dead `badgePadding`**: remove while refactoring the `Constants` enum, or leave (pre-existing dead code)? -3. **No-token values** (`borderLineWidth=1`, `capsuleOffsetXFactor=3`): leave as literals — no matching token. -4. **Verification of AC #2** ("renders correctly with 1/9/99/999+"): no snapshot suite (disabled repo-wide per memory). How to assert appearance — SPM unit tests on token wiring, or rely on `BadgeHelper` label tests + build? From 7dd65412d9c669fbb0f6d13729ee89b1dcd6c9e7 Mon Sep 17 00:00:00 2001 From: Khoi Nguyen Date: Thu, 9 Jul 2026 17:13:34 +0700 Subject: [PATCH 4/4] ALFMOB-269: Source badge border width from borderWeightDefault token --- .../SharedUI/Components/Indicators/BadgeViewModifier.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Alfie/AlfieKit/Sources/SharedUI/Components/Indicators/BadgeViewModifier.swift b/Alfie/AlfieKit/Sources/SharedUI/Components/Indicators/BadgeViewModifier.swift index 4646e598..6dc89443 100644 --- a/Alfie/AlfieKit/Sources/SharedUI/Components/Indicators/BadgeViewModifier.swift +++ b/Alfie/AlfieKit/Sources/SharedUI/Components/Indicators/BadgeViewModifier.swift @@ -13,7 +13,7 @@ public struct BadgeViewModifier: ViewModifier { static let indicatorHeight = Primitives.Spacing.spacing12 static let indicatorWidth = Primitives.Spacing.spacing12 static let capsuleOffsetXFactor = 3 // layout multiplier, no design token - static let borderLineWidth = 1.0 // hairline, no design token (no spacing1) + static let borderLineWidth = CGFloat(Primitives.Border.borderWeightDefault) } private var helper: BadgeHelper