Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 2 additions & 37 deletions .specify/memory/constitution.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,3 @@
<!--
SYNC IMPACT REPORT
==================
Version change: 1.0.0 → 1.1.0
Bump rationale: MINOR — new principle (V. Live Verification via Maestro) added
Modified principles: None renamed
Added sections: V. Live Verification via Maestro
Removed sections: None
Templates requiring updates:
- .specify/templates/plan-template.md ✅ Constitution Check guidance updated for Principle V
- .specify/templates/spec-template.md ✅ Independent Test guidance updated for Maestro
- .specify/templates/tasks-template.md ✅ Checkpoints and Polish phase updated for Maestro
Deferred TODOs: None
-->

# Cirquiz Constitution

## Core Principles
Expand Down Expand Up @@ -41,36 +26,16 @@ Deferred TODOs: None
- New features MUST NOT introduce obvious regressions to existing flows.
- N+1 data-fetching patterns are not allowed.

### V. Live Verification via Maestro

All P1 user journeys MUST be verified against the running app using the Maestro MCP before
a feature is considered done. Required steps:

- Launch the app with `mcp__maestro__launch_app` on a simulator or real device.
- Confirm expected UI state via `mcp__maestro__inspect_view_hierarchy` and/or
`mcp__maestro__take_screenshot`.
- Run scenario flows with `mcp__maestro__run_flow` or `mcp__maestro__run_flow_files`
where a repeatable flow exists.

Unit tests and static analysis alone do NOT satisfy verification for any UI-facing feature.
At least one Maestro-based check MUST be performed per P1 user story, and its result
(screenshot or passing flow output) MUST be documented in the feature's `specs/` directory
before merge.

**Rationale**: Cirquiz is a mobile app. Static tests can miss rendering issues, navigation
bugs, and platform-specific behavior. Maestro MCP provides direct inspection of the running
app, making acceptance verification concrete and reproducible.

## Governance

This constitution guides technical and implementation decisions for the Cirquiz monorepo.

- **Constitution Check**: Each feature plan (`plan.md`) MUST include a brief note on how
all five principles apply or are intentionally excepted.
all four principles apply or are intentionally excepted.
- **Exceptions**: Deviations from a principle MUST be noted in `plan.md` with a one-line
rationale.
- **Amendments**: Use semantic versioning — MAJOR for removals/redefinitions, MINOR for
additions, PATCH for wording. Update `LAST_AMENDED_DATE` on any change.
- This is a personal project; governance is lightweight by design. Common sense prevails.

**Version**: 1.1.0 | **Ratified**: 2026-03-07 | **Last Amended**: 2026-03-07
**Version**: 2.0.0 | **Ratified**: 2026-03-07 | **Last Amended**: 2026-03-11
5 changes: 1 addition & 4 deletions .specify/templates/plan-template.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,11 @@

*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*

[Gates determined based on constitution file. Must address all five principles:]
[Gates determined based on constitution file. Must address all four principles:]
[I. Code Quality — readability, no duplication, lint passes]
[II. Testing Standards — happy-path tests for P1 journeys, written alongside code]
[III. UX Consistency — navigation/error/loading patterns match existing screens]
[IV. Performance — <2s interactions, no regressions, no N+1 fetching]
[V. Live Verification via Maestro — each P1 story verified by launching the app and]
[ inspecting with Maestro MCP (inspect_view_hierarchy / take_screenshot / run_flow).]
[ Screenshot or passing flow output MUST be saved to specs/ before merge.]

## Project Structure

Expand Down
8 changes: 2 additions & 6 deletions .specify/templates/spec-template.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,7 @@

**Why this priority**: [Explain the value and why it has this priority level]

**Independent Test**: [Describe how this can be tested independently. For mobile UI stories,
include the Maestro MCP steps: launch app → inspect view hierarchy / take screenshot →
run flow. E.g., "Launch app, navigate to [screen], verify [element] visible via
inspect_view_hierarchy; run maestro flow flows/[story].yaml"]
**Independent Test**: [Describe how this can be tested independently.]

**Acceptance Scenarios**:

Expand All @@ -44,8 +41,7 @@ inspect_view_hierarchy; run maestro flow flows/[story].yaml"]

**Why this priority**: [Explain the value and why it has this priority level]

**Independent Test**: [Describe how this can be tested independently. For mobile UI stories,
include Maestro MCP steps: launch app → inspect / screenshot → run flow.]
**Independent Test**: [Describe how this can be tested independently.]

**Acceptance Scenarios**:

Expand Down
14 changes: 4 additions & 10 deletions .specify/templates/tasks-template.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,7 @@ Examples of foundational tasks (adjust based on your project):
- [ ] T016 [US1] Add validation and error handling
- [ ] T017 [US1] Add logging for user story 1 operations

**Checkpoint**: User Story 1 complete — verify live with Maestro MCP:
1. `mcp__maestro__launch_app` → navigate to the relevant screen
2. `mcp__maestro__inspect_view_hierarchy` or `mcp__maestro__take_screenshot`
3. Save screenshot/flow result to `specs/[###-feature-name]/verification/us1/`
**Checkpoint**: User Story 1 complete — verify independently per spec.md Independent Test.

---

Expand All @@ -120,8 +117,7 @@ Examples of foundational tasks (adjust based on your project):
- [ ] T022 [US2] Implement [endpoint/feature] in src/[location]/[file].py
- [ ] T023 [US2] Integrate with User Story 1 components (if needed)

**Checkpoint**: User Story 2 complete — verify live with Maestro MCP and save result to
`specs/[###-feature-name]/verification/us2/`
**Checkpoint**: User Story 2 complete — verify independently per spec.md Independent Test.

---

Expand All @@ -142,8 +138,7 @@ Examples of foundational tasks (adjust based on your project):
- [ ] T027 [US3] Implement [Service] in src/services/[service].py
- [ ] T028 [US3] Implement [endpoint/feature] in src/[location]/[file].py

**Checkpoint**: User Story 3 complete — verify live with Maestro MCP and save result to
`specs/[###-feature-name]/verification/us3/`
**Checkpoint**: User Story 3 complete — verify independently per spec.md Independent Test.

---

Expand All @@ -161,8 +156,7 @@ Examples of foundational tasks (adjust based on your project):
- [ ] TXXX [P] Additional unit tests (if requested) in tests/unit/
- [ ] TXXX Security hardening
- [ ] TXXX Run quickstart.md validation
- [ ] TXXX [P] Maestro live verification — run all P1 story flows, save screenshots/results
to `specs/[###-feature-name]/verification/` before merge
- [ ] TXXX Final validation — verify all P1 story acceptance scenarios pass

---

Expand Down
5 changes: 4 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ specs/ # Feature specs and plans
```

## Active Technologies
- TypeScript ~5.9 + React Native 0.83.2, Expo SDK ~55, Expo Router ~55, Zustand ^5, @react-native-async-storage/async-storage, react-native-reanimated, i18next + react-i18nex (002-provider-settings)
- AsyncStorage — `@cirquiz/settings` (new) and `@cirquiz/active_game` (existing) (002-provider-settings)

- **Runtime**: React Native 0.83.2, React 19.2.0
- **Framework**: Expo SDK ~55, Expo Router ~55
Expand Down Expand Up @@ -62,8 +64,9 @@ yarn workspace cirquiz typecheck
- Zustand for global state (stores in `apps/cirquiz/src/state/`)

## Recent Changes
- 002-provider-settings: Added TypeScript ~5.9 + React Native 0.83.2, Expo SDK ~55, Expo Router ~55, Zustand ^5, @react-native-async-storage/async-storage, react-native-reanimated, i18next + react-i18nex

- 001-pass-and-play-trivia: Pass-and-play local multiplayer trivia game with player setup, question flow, handoff screens, reveal, and standings

<!-- MANUAL ADDITIONS START -->
<!-- MANUAL ADDITIONS END -->
<!-- MANUAL ADDITIONS END -->
5 changes: 2 additions & 3 deletions apps/.TODO.txt
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
This file is purely for notes and tracking things I want to implement going forward.

- replace when going to the home screen
- Cache and prefetch the categories. Categories shouldn't be changing while the app is running. We can just hard code these.
- Add support for trivia API and a settings page to be able to change them.
- Along with this we should then support having multiple category selection because this api support that. also multiple difficulty
- Instead of refresshing the session for otdb when we run out of questions we should show some sort of UI to reset the session or allow changing the category selection
- On taking a long time to load for continue, if someone quits the game it should cancel the pending network request and navigaiton

OFFLINE SUPPORT
- Store the list of categories from otdb and return that
Expand Down
12 changes: 12 additions & 0 deletions apps/cirquiz/app/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import Animated, {
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { Button } from '../src/components/Button';
import { GradientScreen } from '../src/components/GradientScreen';
import { IconButton } from '../src/components/IconButton';
import { ShineButton } from '../src/components/ShineButton';
import { useGameStore } from '../src/state/gameStore';
import { colors, fontSize, spacing } from '../src/theme';
Expand Down Expand Up @@ -156,6 +157,13 @@ export default function HomeScreen() {
return (
<GradientScreen>
<View style={[styles.screen, { paddingTop: insets.top, paddingBottom: insets.bottom }]}>
<View style={styles.topBar}>
<IconButton
icon="settings-outline"
onPress={() => router.push('/settings')}
color={colors.text}
/>
</View>
<Animated.View style={[styles.hero, heroStyle]}>
<View style={styles.logoWrapper}>
<CirclequizLogo width={340} height={95} />
Expand Down Expand Up @@ -206,6 +214,10 @@ const styles = StyleSheet.create({
flex: 1,
flexDirection: 'column',
},
topBar: {
alignItems: 'flex-end',
paddingHorizontal: spacing.sm,
},
avatarRow: {
flexDirection: 'row',
flexWrap: 'wrap',
Expand Down
72 changes: 72 additions & 0 deletions apps/cirquiz/app/settings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { router } from 'expo-router';
import { useTranslation } from 'react-i18next';
import { StyleSheet, Text, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { GradientScreen } from '../src/components/GradientScreen';
import { IconButton } from '../src/components/IconButton';
import { SelectableRow } from '../src/components/SelectableRow';
import { useSettingsStore } from '../src/state/settingsStore';
import { colors, fontSize, fontWeight, spacing } from '../src/theme';

export default function SettingsScreen() {
const { t } = useTranslation();
const insets = useSafeAreaInsets();
const questionSource = useSettingsStore((s) => s.questionSource);
const setQuestionSource = useSettingsStore((s) => s.setQuestionSource);

return (
<GradientScreen>
<View style={[styles.screen, { paddingBottom: insets.bottom }]}>
<View style={[styles.header, { paddingTop: insets.top + 8 }]}>
<IconButton icon="arrow-back" onPress={() => router.back()} color={colors.text} />
<Text style={styles.headerTitle}>{t('settings.title')}</Text>
<View style={styles.headerSpacer} />
</View>

<View style={styles.content}>
<Text style={styles.sectionLabel}>{t('settings.questionSource')}</Text>
<SelectableRow
label={t('settings.otdb')}
active={questionSource === 'otdb'}
onPress={() => setQuestionSource('otdb')}
/>
<SelectableRow
label={t('settings.theTriviaApi')}
active={questionSource === 'the-trivia-api'}
onPress={() => setQuestionSource('the-trivia-api')}
/>
</View>
</View>
</GradientScreen>
);
}

const styles = StyleSheet.create({
screen: {
flex: 1,
},
header: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: spacing.lg,
paddingBottom: spacing.sm,
},
headerTitle: {
flex: 1,
textAlign: 'center',
fontSize: fontSize['2xl'],
fontWeight: fontWeight.bold,
color: colors.text,
},
headerSpacer: { width: 44 },
content: {
paddingHorizontal: spacing.xl,
paddingTop: spacing.lg,
},
sectionLabel: {
fontSize: fontSize.md,
fontWeight: fontWeight.semibold,
color: colors.textSecondary,
marginBottom: spacing.sm,
},
});
35 changes: 34 additions & 1 deletion apps/cirquiz/app/setup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ import { Button } from '../src/components/Button';
import { AVATAR_LIST, type AvatarKey } from '../src/avatars';
import { type Difficulty } from '../src/providers';
import { useCategoryLoader } from '../src/hooks/useCategoryLoader';
import { useToast } from '../src/hooks/useToast';
import { useGameStore } from '../src/state/gameStore';
import { useSettingsStore } from '../src/state/settingsStore';
import { colors, fontSize, fontWeight, radius, spacing } from '../src/theme';
import { IconButton } from '../src/components/IconButton';
import LightningSVG from '../assets/lightning.svg';
Expand Down Expand Up @@ -77,6 +79,32 @@ export default function SetupScreen() {
const [pickerIndex, setPickerIndex] = useState<number | null>(null);
const [stickyHeight, setStickyHeight] = useState(0);

const questionSource = useSettingsStore((s) => s.questionSource);
const isFirstRender = useRef(true);
const { showToast, ToastNode } = useToast();

// Refs let us read latest values in the effect without adding them as deps
const categoryRef = useRef(category);
categoryRef.current = category;
const quickPlayRef = useRef(quickPlay);
quickPlayRef.current = quickPlay;
const loadCategoriesRef = useRef(loadCategories);
loadCategoriesRef.current = loadCategories;

useEffect(() => {
if (isFirstRender.current) {
isFirstRender.current = false;
return;
}
if (categoryRef.current !== undefined) {
setCategory(undefined);
showToast(t('settings.categoryResetNotice'));
}
if (!quickPlayRef.current) {
loadCategoriesRef.current();
}
}, [questionSource, t, showToast]);

const usedAvatars = players.map((p) => p.avatar);

const addPlayer = () => {
Expand Down Expand Up @@ -175,7 +203,11 @@ export default function SetupScreen() {
<View style={[styles.header, { paddingTop: insets.top + 8 }]}>
<IconButton icon="arrow-back" onPress={() => router.back()} color={colors.text} />
<Text style={styles.headerTitle}>{t('setup.title', 'NEW GAME')}</Text>
<View style={styles.headerSpacer} />
<IconButton
icon="settings-outline"
onPress={() => router.push('/settings')}
color={colors.text}
/>
</View>

<KeyboardAwareScrollView
Expand Down Expand Up @@ -332,6 +364,7 @@ export default function SetupScreen() {
</View>
</Pressable>
</Modal>
{ToastNode}
</GradientScreen>
);
}
Expand Down
2 changes: 1 addition & 1 deletion apps/cirquiz/src/components/GradientScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ interface Props {
showBlobs?: boolean;
mode?: keyof typeof GRADIENT_COLORS;
}
const LIGHTEN_AMMOUNT = 0.06;
const LIGHTEN_AMMOUNT = 0.045;
const GRADIENT_COLORS = {
standard: [colors.primaryFaint, colors.white, colors.difficultyFaint] as const,
lighter: [
Expand Down
42 changes: 42 additions & 0 deletions apps/cirquiz/src/components/Toast.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import Animated from 'react-native-reanimated';
import { StyleSheet, Text, ViewStyle } from 'react-native';
import { colors, fontSize, radius, spacing } from '../theme';
import { useSafeAreaInsets } from 'react-native-safe-area-context';

export function Toast({
toastMessage,
toastStyle,
}: {
toastMessage: string;
toastStyle: ViewStyle;
}) {
const insets = useSafeAreaInsets();

return (
<Animated.View
style={[styles.toast, { marginBottom: insets.bottom }, toastStyle]}
pointerEvents="none"
>
<Text style={styles.toastText}>{toastMessage}</Text>
</Animated.View>
);
}

const styles = StyleSheet.create({
toast: {
position: 'absolute',
bottom: 0,
left: spacing.xl,
right: spacing.xl,
backgroundColor: 'rgba(0,0,0,0.85)',
borderRadius: radius.md,
paddingVertical: spacing.sm,
paddingHorizontal: spacing.lg,
alignItems: 'center',
},
toastText: {
color: colors.white,
fontSize: fontSize.base,
textAlign: 'center',
},
});
Loading
Loading