Launch Readiness Synopsis
Snapshot as of v1.13 (April 2026). See v1.14 milestone for cleanup and current component list.
A comprehensive record of all quality audit, WCAG 2.1 AA accessibility, and supporting sites changes shipped in the
feature/launch-readinessbranch.Branch:
feature/launch-readinessPR: #288Date: April 2026 Scope: 100+ files changed across quality, accessibility, performance, dashboard rethink, and Focus Mode
Table of Contents
- Overview
- Quality Audit — Feature 18
- Accessibility — WCAG 2.1 AA
- Supporting Sites Overhaul
- Infrastructure & Tooling
- Files Changed
- Metrics
Overview
This branch prepares Objectuve for public launch by addressing three workstreams:
graph LR
A[Feature 18: Quality Audit] --> D[Launch Ready]
B[WCAG 2.1 AA Accessibility] --> D
C[Supporting Sites Overhaul] --> D
A --> A1[9 views audited]
A --> A2[20 quality checks each]
B --> B1[Phase 3 baseline]
B --> B2[Phase 4 full audit]
C --> C1[Docs reorganized]
C --> C2[Cross-site links]
Roadmap Updates
Features 10, 16, and 17 were marked as shipped with all acceptance criteria checked off:
| Feature | Name | Criteria Checked |
|---|---|---|
| 10 | Push Notifications (FCM/APNs + Capacitor) | 8/8 |
| 16 | Screen Time Nudges | 7/7 |
| 17 | Quick-Add from Notification | 5/5 |
Quality Audit
Systematic audit of all 9 user-facing views against 20 quality checks.
Views Audited
graph TD
subgraph "All 9 Views Audited"
D[Dashboard - 848 lines]
G[Goal Detail - 1339 lines]
GL[Goals List - 536 lines]
C[Communities - 807 lines]
CD[Community Detail - 364 lines]
A[Achievements - 420 lines]
S[Settings/Profile - 354 lines]
F[Activity Feed - 123 lines]
P[Profile - in Settings]
end
Error States Added
Every view now shows a helpful error message with a recovery action when API queries fail.
Before: Silent failures or blank screens when Apollo queries error.
After: Friendly error panels with retry buttons.
+------------------------------------------+
| |
| (!) Could not load goals |
| |
| Something went wrong. Please check |
| your connection and try again. |
| |
| [ Retry ] |
| |
+------------------------------------------+Views with new error states:
Dashboard.vue— Combined error from USER_QUERY and GOALS_QUERYGoals.vue— Error from goals query with retryGoal.vue— Error when goal can't be loaded (replaces silent redirect)Communities.vue— Error from communities queryCommunity.vue— Error from community detail query + join failure toastsAchievements.vue— Error from user/badges query
Loading States Improved
Before (Goals.vue): Full-viewport spinner causing large layout shift when content arrived.
After (Goals.vue): Dimension-matched skeleton layout: header skeleton + 4 stat cards + 6 LoadingSkeletonCard instances in the same grid layout as real content.
+--[Skeleton Header]-------------------------+
| [====] [===========] |
+--------------------------------------------+
| +--------+ +--------+ +--------+ +--------+|
| | ==== | | ==== | | ==== | | ==== ||
| | ====== | | ====== | | ====== | | ====== ||
| +--------+ +--------+ +--------+ +--------+|
+--------------------------------------------+
| +------------------+ +------------------+ |
| | [LoadingSkeleton] | | [LoadingSkeleton]| |
| | Card matches | | Card matches | |
| | real layout | | real layout | |
| +------------------+ +------------------+ |
+--------------------------------------------+Dashboard.vue: Replaced single-row skeleton with 4-column momentum bar + goal card grid matching real content dimensions.
Empty States with Actionable Guidance
Achievements.vue (new): When filters produce zero results, shows contextual empty state:
+------------------------------------------+
| |
| No badges match your filters |
| |
| Try adjusting your category or |
| status filters to see more badges. |
| |
| [ Clear Filters ] |
| |
+------------------------------------------+Dark Mode Fixes
Replaced hardcoded colors with design tokens across multiple components:
| Component | Before | After |
|---|---|---|
| Goals stat cards | bg-white/80 | bg-card/80 |
| Goals stat icons | text-blue-500 | text-primary |
| Communities search | bg-white dark:bg-slate-800 | bg-background |
| Communities sort | bg-white dark:bg-slate-800 | bg-background |
| Communities filters | bg-white dark:bg-slate-800 | bg-background |
| UserFeedList | bg-white | bg-white dark:bg-slate-900 |
| Achievements badge count | text-slate-500 | text-muted-foreground |
| Goal.vue enhancement banner | Hardcoded #fffbeb | color-mix() adaptive |
| Goal.vue timeline empty state | rgba(255,255,255,0.5) | var(--ion-card-background) |
Tap Feedback
Added active:scale-95 or active:opacity-70 to all interactive elements across all views. Every button, card, and clickable element now provides immediate visual feedback on press.
Duplicate-Tap Guards
Added loading guards to prevent double-submission on mutation buttons:
| View | Mutation | Guard |
|---|---|---|
| Dashboard | UPDATE_GOAL_MUTATION | updatingGoal.value early return |
| Goal Detail | Habit check-in | habitCheckInLoading + disabled button |
| Goal Detail | Complete goal | isCompletingGoal ref + finally block |
| Goal Detail | Toggle privacy | isTogglingPrivacy ref + disabled buttons |
| Communities | Join/leave | isJoining ref keyed by community ID |
| Communities | Create community | isCreatingCommunity ref |
Bug Fix
Dashboard.vue: Target icon from lucide-vue-next was used in template but never imported — a pre-existing runtime bug producing a Vue component resolution warning. Fixed by adding the import.
Accessibility
Infrastructure
graph TD
subgraph "New A11y Infrastructure"
E[eslint-plugin-vuejs-accessibility]
CA[cypress-axe]
JA[jest-axe]
UC[useAccessibility.ts composable]
SL[Skip-to-main link]
AR[Aria-live announcer]
RM[prefers-reduced-motion CSS]
DOC[accessibility.md docs]
CT[Cypress a11y smoke test]
VT[Vitest a11y helper]
end
E --> |Lint-time| DEV[Developer Workflow]
CA --> |E2E| CI[CI Pipeline]
JA --> |Unit| CI
UC --> |Runtime| APP[Application]
SL --> APP
AR --> APP
RM --> APP
useAccessibility.ts Composable
Four reusable utilities:
// Focus trap for modals/dialogs
const { activate, deactivate } = useFocusTrap(containerRef)
// Screen reader announcements
const { announce } = useAnnounce()
announce('Badge unlocked: First Steps')
// Reactive reduced-motion preference
const { reducedMotion } = useReducedMotion()
// Programmatic skip to main content
const { skipToMain } = useSkipLink()Skip-to-Main Link
+--[Visible only on Tab focus]--------+
| Skip to main content |
+-------------------------------------+Added to App.vue. Visually hidden by default, becomes visible when focused via keyboard Tab. Links to #main-content (the ion-router-outlet).
Global prefers-reduced-motion
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}Plus PageTransition.vue skips enter/leave animations when reduced motion is preferred.
Minimum Tap Targets (44px)
WCAG 2.1 AA requires 44x44px minimum touch targets.
| Component | Before | After |
|---|---|---|
UiButton default | h-10 (40px) | h-11 (44px) |
UiButton sm | h-9 (36px) | h-10 (40px) |
UiButton lg | h-11 (44px) | h-12 (48px) |
UiButton icon | h-10 w-10 (40px) | h-11 w-11 (44px) |
UiInput | h-10 (40px) | h-11 (44px) |
| ActivityFeed refresh | No min size | min-w-[44px] min-h-[44px] |
| ActivityFeed tabs | Default | min-h-[44px] |
ARIA Tab Patterns
Added full tab widget semantics to 4 views:
<!-- Before -->
<div class="tabs">
<button @click="activeTab = 'profile'">Profile</button>
<button @click="activeTab = 'security'">Security</button>
</div>
<div v-show="activeTab === 'profile'">...</div>
<!-- After -->
<div role="tablist" aria-label="Settings sections">
<button role="tab" :aria-selected="activeTab === 'profile'"
:aria-controls="'panel-profile'">Profile</button>
<button role="tab" :aria-selected="activeTab === 'security'"
:aria-controls="'panel-security'">Security</button>
</div>
<div id="panel-profile" role="tabpanel" aria-labelledby="tab-profile"
v-show="activeTab === 'profile'">...</div>Applied to: Settings, ActivityFeed, Goals, Community Detail
Navigation Landmarks
+--[role="banner"]--[AppHeader]--------+
| [menu-button] [logo] |
+--------------------------------------+
| |
| [role="navigation" aria-label= |
| "Main menu"] -- SideMenu |
| |
| [#main-content] |
| [aria-live="polite"] |
| ... page content ... |
| |
+--------------------------------------+
| [role="navigation" aria-label= |
| "Main navigation"] -- BottomTabBar |
| [aria-current="page" on active tab] |
+--------------------------------------+Modal Accessibility
All 12 modals received:
aria-labelledbypointing to modal titlearia-labelon close/dismiss buttonsaria-hidden="true"on decorative icons- Focus traps on 5 custom div-based modals (via
useFocusTrap)
| Modal | Type | Focus Trap |
|---|---|---|
| AiCoachModal | IonModal | Native |
| AgreeToTermsModal | IonModal | Native |
| BadgeModal | Custom div | useFocusTrap |
| ChangelogModal | IonModal | Native |
| CommunitySuggestionModal | IonModal | Native |
| CommunityComparisonModal | Custom div | useFocusTrap |
| CommunityPreviewModal | Custom div | useFocusTrap |
| CreateCommunityModal | Custom div | useFocusTrap |
| MemberProfileModal | Custom div | useFocusTrap |
| ProfileCompletionModal | IonModal | Native |
| ReportContentModal | IonModal | Native |
| StreakDetailsModal | IonModal | Native |
Form Accessibility
All form fields now have properly associated labels:
<!-- Before -->
<label>First Name</label>
<UiInput v-model="form.firstName" />
<!-- After -->
<label for="settings-firstName">First Name</label>
<UiInput id="settings-firstName" v-model="form.firstName" />Files with form label fixes: Settings.vue (8 fields), GoalForm.vue (6 fields), GoalCreate.vue (6 fields), GoalEventForm.vue (3 fields), CreateCommunityModal.vue (6 fields), ProfileCompletionModal.vue (3 fields), MoodCheckIn.vue (2 fields), CommunityComparisonModal.vue (1 field), AiCoachModal.vue (1 field), SignIn.vue (2 fields), OnboardingWizard.vue (1 field), AchievementFilters.vue (1 field), CommunityFeed.vue (2 fields), CommunityGoals.vue (1 field), CommunityFeedCard.vue (1 field)
Radiogroup Patterns
MoodSelector: Full ARIA radiogroup with roving tabindex and keyboard navigation (arrow keys).
+--[role="radiogroup" aria-labelledby="mood-group-label"]--+
| |
| [role="radio" aria-checked="false" tabindex="-1"] |
| Terrible |
| |
| [role="radio" aria-checked="true" tabindex="0"] |
| Amazing <-- focused, selected |
| |
| [role="radio" aria-checked="false" tabindex="-1"] |
| Great |
| |
+----------------------------------------------------------+
Arrow keys move selection, Tab moves out of groupGoalForm type picker: role="radiogroup" with aria-checked on each goal type option.
Keyboard Support
Converted all clickable <div> elements to either:
- Semantic
<button>elements (preferred), or role="button"+tabindex="0"+@keyup.enterhandler
Components converted to <button>:
- GoalCard image container
- GoalEventCard content + action areas
- GoalListItem card
- AchievementCard
- ShowcaseSelector badge options
- AllyActivityFeed rows
- MilestoneManager toggle + name display
Components with role="button" added:
- SideMenu: logo, profile, nav items, CTA, version info
- AppHeader logo
- CommunityCard, GoalCard (ui/), NotificationsPanel items
- Dashboard encouraged-goal rows (converted to
<button>) - GettingStartedCard collapsed state
- CommunityMembers grid + leaderboard rows
- CommunityGoals cards
- GoalGridCard, ShowcasedAchievements, TodaysHabits
- CommunityListItem, EnhancedCommunityCard, CommunityInsightsPanel
- OnboardingTour backdrop, SmartNotifications cards
Dynamic Content (aria-live)
| Component | Attribute | Purpose |
|---|---|---|
| ActionAlert | role="alert" aria-live="assertive" | Goal needs action |
| ScreenTimeNudge | role="alert" aria-live="polite" | Break reminder |
| Error states (all views) | role="alert" aria-live="assertive" | Query failures |
| UserFeedList | aria-live="polite" | New feed items |
| Dashboard content | aria-live="polite" | State changes |
| Goals tab panels | aria-live="polite" | Filter/sort changes |
| Community tab panels | aria-live="polite" | Tab content changes |
| Achievements badge count | aria-live="polite" | Filter count updates |
| Goal.vue timeline | aria-live="polite" | New events after check-in |
| Goal.vue encouragements | aria-live="polite" | Count changes |
| Dashboard notifications | aria-live="polite" | Unread count badge |
| Loading containers | aria-busy="true" | While fetching |
| LoadingAnimation | role="status" | Spinner announcement |
| LoadingSkeletonCard | role="status" aria-busy="true" | Skeleton announcement |
| GoalDetailSkeleton | role="status" aria-busy="true" | Goal detail page skeleton announcement |
| AchievementsSkeleton | role="status" aria-busy="true" | Achievement Hall page skeleton announcement |
| PublicGoalSkeleton | role="status" aria-busy="true" | Public goal page skeleton announcement |
| PublicProfileSkeleton | role="status" aria-busy="true" | Public profile page skeleton announcement |
Image Alt Text
All <img> tags audited across 16+ files:
| Pattern | Alt Text |
|---|---|
| User avatars | :alt="'Profile photo for ' + userName" |
| Badge images | :alt="badge.name + ' badge'" |
| Check-in photos | alt="Check-in photo" |
| Community covers | :alt="community.name" |
| Goal cover images | alt="Goal cover image" |
| Decorative hero images | alt="" |
Decorative Icons
All decorative <ion-icon> and Lucide icon components marked with aria-hidden="true" across 60+ files — prevents screen readers from announcing purely visual elements like navigation arrows, status indicators, and ornamental icons.
Dashboard Rethink (11 → 6 sections)
The active user dashboard was restructured from 11 sections to 6:
| Before (11 sections) | After (6 sections) |
|---|---|
| MomentumBar | MomentumBar (now shown for ALL users, not just 3+) |
| Daily Quote | Moved to greeting subtitle |
| ActionHub | Merged into TodayActions |
| CheckInPromptCard | Merged into TodayActions |
| TodaysHabits | Merged into TodayActions |
| AiInsightCard | Merged into TodayActions (single AI prompt) |
| Your Goals (5 cap, with check-in) | Your Goals (all goals, view-only) |
| Community Support | Merged into SocialSection |
| Ally Activity Feed | Merged into SocialSection |
| WeeklyActivity (with stats grid) | WeeklyActivity (day circles only) |
| Showcased Achievements | Showcased Achievements (unchanged) |
New components: TodayActions.vue, SocialSection.vueSimplified: WeeklyActivity.vue (removed duplicate stats grid), GoalGridCard.vue (removed check-in button)
Focus Mode (Streamlined Dashboard)
Card-stack swipe interface for quick daily check-ins (~2-3 minutes):
| Card | Content | Action |
|---|---|---|
| Greeting | Streak + quote | Swipe |
| Mood | 6 mood options | Tap mood |
| Habits (1 per) | Name + streak + ring | Tap check-in |
| Urgent Goal | Name + days since | Add Progress / Dismiss |
| AI Insight | Coach message | Tap to open coach |
| Social Nudge | Encouragement | Nice! |
| Done | Confetti + stats | See Full Dashboard |
Three modes: Focus (always cards), Full (always dashboard), Auto (smart switch) Settings toggle: 3-button selector matching the theme toggle pattern Gesture: Ionic createGesture for horizontal swipe, CSS transitions New files: useFocusMode.ts, StreamlinedDashboard.vue, 7 card components
Supporting Sites
Docs Landing Page
Added "Supporting Sites" table to docs/index.md:
| Site | URL | Description |
|---|---|---|
| Component Library | storybook.objectuve.com | Storybook with 30+ stories |
| Design System | design.objectuve.com | Colors, typography, spacing |
| API Explorer | enkidu-api-docs.web.app | GraphQL Voyager |
| Marketing Site | www.objectuve.com | Landing page & policies |
| Production App | app.objectuve.com | Live app |
| Staging App | staging.app.objectuve.com | Pre-release testing |
VitePress Sidebar Reorganization
Before: Flat list with missing pages.
After: Organized into subsections with 15+ new entries:
Feature Guides
Core: Goals, Habits, Dashboard, Mood Logging, Progress Viz
Engagement: Achievements, Gamification Engine, Coach,
Check-in Prompts, Notifications, Push Notifications
Social: Communities, Social Features, Activity Feed,
Allies, User Profile
Admin: Admin Dashboard, Onboarding Timeline
Reference
+ Accessibility (NEW)
+ Color Philosophy
+ Logo Evolution & Transition
+ Legal & Policies
Operations
+ Observability
+ Alerting
+ Dashboards
+ Vendor AccountsInfrastructure
ESLint A11y Plugin
eslint-plugin-vuejs-accessibility (recommended config)
Custom rules:
label-has-for: allows nesting or id, recognizes UiInput/UiSelect/UiTextarea
form-control-has-label: recognizes custom + Ionic form componentsCypress A11y Smoke Test
New file: tests/e2e/specs/accessibility/a11y-smoke.cy.ts
Tests all 6 core views against WCAG 2.1 AA using cypress-axe:
- Dashboard, Goals, Communities, Achievements, Settings, Feed
- Verifies skip-to-main link exists and is focusable
- Verifies aria-live announcer region exists
Vitest A11y Helper
New file: tests/unit/helpers/a11y.ts
import { expectNoA11yViolations } from '@tests/unit/helpers/a11y'
it('has no a11y violations', async () => {
const wrapper = mount(MyComponent)
await expectNoA11yViolations(wrapper)
})New Dependencies
| Package | Version | Purpose |
|---|---|---|
eslint-plugin-vuejs-accessibility | latest | Lint-time a11y checks |
cypress-axe | latest | E2E a11y testing |
axe-core | latest | A11y engine (shared) |
jest-axe | latest | Vitest a11y assertions |
@types/jest-axe | latest | TypeScript types |
Files Changed
New Files (4)
| File | Purpose |
|---|---|
src/composables/useAccessibility.ts | Focus trap, announcer, reduced motion, skip link |
tests/e2e/specs/accessibility/a11y-smoke.cy.ts | Cypress a11y smoke test |
tests/unit/helpers/a11y.ts | Vitest a11y helper |
docs/development/accessibility.md | A11y standards document |
Modified Files by Category
Views (11): Dashboard, Goal, Goals, GoalCreate, Communities, Community, Achievements, ActivityFeed, Settings, SignIn, PublicGoal
Core Components (16): ActionAlert, BottomTabBar, GoalCard, GoalEventCard, GoalEventForm, GoalEventItem, GoalForm, GoalListItem, LoadingAnimation, LoadingSkeletonCard, MilestoneManager, MoodCheckIn, MoodSelector, OnboardingWizard, PageTransition, ReactionPicker, SideMenu, UserFeedList
Modal Components (12): AiCoachModal, AgreeToTermsModal, BadgeModal, ChangelogModal, CommunitySuggestionModal, CommunityComparisonModal, CommunityPreviewModal, CreateCommunityModal, MemberProfileModal, ProfileCompletionModal, ReportContentModal, StreakDetailsModal
UI Primitives (6): UiButton, UiInput, UiTooltip, AppHeader, CommunityCard, GoalCard (ui/), NotificationsPanel, ScreenTimeNudge
Community Components (8): AllyActivityFeed, CommunityInsightsPanel, CommunityListItem, EnhancedCommunityCard, OnboardingTour, SmartNotifications, CommunityFeed, CommunityFeedCard, CommunityGoals, CommunityMembers
Dashboard Components (5): GettingStartedCard, GoalGridCard, ShowcasedAchievements, StreakDetailsModal, TodaysHabits
Other (3): AdminUsersTable, TimelineEvent, PublicTimelineEvent
Docs & Config (5): roadmap.md, roadmap-visual.md, index.md, accessibility.md, .vitepress/config.ts, eslint.config.js, package.json
Metrics
| Metric | Before | After | Change |
|---|---|---|---|
| ESLint a11y errors | ~150 | 0 | -100% |
| ESLint warnings | 7 | 2 | -71% |
| Views with error states | 0 | 6 | +6 |
| Views with matched skeletons | 0 | 2+ | +2 |
| Components with aria-label | 7 | 60+ | +750% |
| Modals with aria-labelledby | 0 | 12 | +12 |
| Modals with focus traps | 0 | 5 | +5 |
| Images with proper alt text | ~2 | 16+ | +700% |
| Forms with label associations | ~5 | 40+ | +700% |
| Interactive divs with keyboard | ~5 | 30+ | +500% |
| Minimum tap target (UiButton) | 40px | 44px | WCAG AA |
| prefers-reduced-motion support | none | global | full |
| TypeScript | passing | passing | maintained |
| Vitest (444 tests) | passing | passing | maintained |
| Docs sidebar entries | ~30 | 45+ | +50% |
Last updated: 2026-07-09