{"schemaVersion":1,"id":"flow/steadily-onboarding","name":"Steadily onboarding","version":"0.1.0","url":"https://mobileblocks.dev/flows/steadily-onboarding","compatibility":{"platform":"expo","sdk":["57"],"architecture":"new","language":"typescript","styling":"stylesheet"},"dependencies":{"expo":["react-native-safe-area-context"],"npm":[],"registry":[]},"requirements":{"configPlugins":[],"environmentVariables":[],"manualSteps":[]},"entrypoint":"src/mobileblocks/flows/steadily-onboarding/onboarding-flow.tsx","files":[{"target":"src/mobileblocks/flows/steadily-onboarding/onboarding-flow.tsx","role":"flow","contents":"import { useState } from 'react';\n\nimport { ReadyScreen } from './screens/ready-screen';\nimport { RhythmScreen, type Rhythm } from './screens/rhythm-screen';\nimport { WelcomeScreen } from './screens/welcome-screen';\n\nexport type OnboardingStep = 0 | 1 | 2;\n\nexport interface OnboardingFlowProps {\n  colorMode?: 'light' | 'dark' | 'system';\n  initialStep?: OnboardingStep;\n  onComplete?: (rhythm: Rhythm) => void;\n  onSignIn?: () => void;\n}\n\nexport function OnboardingFlow({\n  colorMode = 'system',\n  initialStep = 0,\n  onComplete,\n  onSignIn,\n}: OnboardingFlowProps) {\n  const [step, setStep] = useState<OnboardingStep>(initialStep);\n  const [rhythm, setRhythm] = useState<Rhythm>('balanced');\n\n  if (step === 0) {\n    return (\n      <WelcomeScreen\n        colorMode={colorMode}\n        onContinue={() => setStep(1)}\n        onSignIn={onSignIn}\n      />\n    );\n  }\n\n  if (step === 1) {\n    return (\n      <RhythmScreen\n        colorMode={colorMode}\n        onBack={() => setStep(0)}\n        onChange={setRhythm}\n        onContinue={() => setStep(2)}\n        value={rhythm}\n      />\n    );\n  }\n\n  return (\n    <ReadyScreen\n      colorMode={colorMode}\n      onBack={() => setStep(1)}\n      onComplete={() => onComplete?.(rhythm)}\n      rhythm={rhythm}\n    />\n  );\n}\n"},{"target":"src/mobileblocks/flows/steadily-onboarding/screens/welcome-screen.tsx","role":"screen","contents":"import { useMemo } from 'react';\nimport { StyleSheet, Text, useColorScheme, View } from 'react-native';\nimport { SafeAreaView } from 'react-native-safe-area-context';\n\nimport { Button } from '../../../components/button';\nimport { palette, radius, space } from '../../../foundation/tokens';\n\nexport interface WelcomeScreenProps {\n  colorMode?: 'light' | 'dark' | 'system';\n  onContinue?: () => void;\n  onSignIn?: () => void;\n}\n\nconst rituals = [\n  { time: '07:30', title: 'Morning reset', detail: '10 quiet minutes' },\n  { time: '12:00', title: 'Walk outside', detail: 'One block is enough' },\n  { time: '21:45', title: 'Close the day', detail: 'Leave tomorrow lighter' },\n];\n\nexport function WelcomeScreen({ colorMode = 'system', onContinue, onSignIn }: WelcomeScreenProps) {\n  const systemColorScheme = useColorScheme();\n  const isDark = colorMode === 'dark' || (colorMode === 'system' && systemColorScheme === 'dark');\n  const theme = useMemo(\n    () =>\n      isDark\n        ? {\n            canvas: palette.dark.canvas,\n            surface: palette.dark.surface,\n            ink: palette.dark.ink,\n            muted: palette.dark.inkMuted,\n            line: palette.dark.line,\n            success: palette.dark.success,\n          }\n        : {\n            canvas: palette.canvas,\n            surface: palette.surface,\n            ink: palette.ink,\n            muted: palette.inkMuted,\n            line: palette.line,\n            success: palette.success,\n          },\n    [isDark],\n  );\n\n  return (\n    <View style={[styles.root, { backgroundColor: theme.canvas }]}>\n      <SafeAreaView style={styles.safeArea} edges={['top', 'bottom']}>\n        <View style={styles.header}>\n          <Text style={[styles.wordmark, { color: theme.ink }]}>steadily</Text>\n          <Text style={[styles.step, { color: theme.muted }]}>1 of 3</Text>\n        </View>\n\n        <View style={[styles.preview, { backgroundColor: theme.surface, borderColor: theme.line }]}>\n          <View style={styles.previewHeading}>\n            <View>\n              <Text style={[styles.previewEyebrow, { color: theme.muted }]}>TODAY</Text>\n              <Text style={[styles.previewTitle, { color: theme.ink }]}>Keep it possible</Text>\n            </View>\n            <View style={[styles.count, { borderColor: theme.line }]}>\n              <Text style={[styles.countText, { color: theme.ink }]}>3</Text>\n            </View>\n          </View>\n\n          <View style={styles.ritualList}>\n            {rituals.map((ritual, index) => (\n              <View\n                key={ritual.time}\n                style={[\n                  styles.ritual,\n                  index < rituals.length - 1 && { borderBottomColor: theme.line, borderBottomWidth: 1 },\n                ]}\n              >\n                <View style={[styles.check, { borderColor: theme.success }]} />\n                <View style={styles.ritualCopy}>\n                  <Text style={[styles.ritualTitle, { color: theme.ink }]}>{ritual.title}</Text>\n                  <Text style={[styles.ritualDetail, { color: theme.muted }]}>{ritual.detail}</Text>\n                </View>\n                <Text style={[styles.ritualTime, { color: theme.muted }]}>{ritual.time}</Text>\n              </View>\n            ))}\n          </View>\n        </View>\n\n        <View style={styles.copy}>\n          <Text style={[styles.title, { color: theme.ink }]}>A routine that survives real life.</Text>\n          <Text style={[styles.body, { color: theme.muted }]}>\n            Build a small rhythm for the days when motivation does not show up.\n          </Text>\n        </View>\n\n        <View style={styles.actions}>\n          <Button colorMode={colorMode} onPress={onContinue}>Start gently</Button>\n          <Button colorMode={colorMode} tone=\"secondary\" onPress={onSignIn}>\n            I already have an account\n          </Button>\n        </View>\n      </SafeAreaView>\n    </View>\n  );\n}\n\nconst styles = StyleSheet.create({\n  root: { flex: 1 },\n  safeArea: { flex: 1, paddingHorizontal: space.xl, paddingTop: space.sm, paddingBottom: space.md },\n  header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' },\n  wordmark: { fontSize: 18, fontWeight: '800', letterSpacing: -0.5 },\n  step: { fontSize: 13, fontWeight: '600' },\n  preview: { marginTop: space.xxl, borderWidth: 1, borderRadius: radius.card, padding: space.xl },\n  previewHeading: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' },\n  previewEyebrow: { fontSize: 11, fontWeight: '700', letterSpacing: 1.2 },\n  previewTitle: { marginTop: space.xs, fontSize: 24, fontWeight: '700', letterSpacing: -0.8 },\n  count: { width: 40, height: 40, borderWidth: 1, borderRadius: radius.pill, alignItems: 'center', justifyContent: 'center' },\n  countText: { fontSize: 15, fontWeight: '700' },\n  ritualList: { marginTop: space.lg },\n  ritual: { minHeight: 68, flexDirection: 'row', alignItems: 'center' },\n  check: { width: 18, height: 18, borderWidth: 1.5, borderRadius: radius.pill },\n  ritualCopy: { flex: 1, marginLeft: space.md },\n  ritualTitle: { fontSize: 15, fontWeight: '600', letterSpacing: -0.2 },\n  ritualDetail: { marginTop: 3, fontSize: 12 },\n  ritualTime: { fontSize: 12, fontVariant: ['tabular-nums'] },\n  copy: { flex: 1, justifyContent: 'flex-end', paddingTop: space.xxl, paddingBottom: space.xl },\n  title: { maxWidth: 330, fontSize: 38, lineHeight: 42, fontWeight: '700', letterSpacing: -1.7 },\n  body: { maxWidth: 330, marginTop: space.lg, fontSize: 17, lineHeight: 25, letterSpacing: -0.2 },\n  actions: { gap: space.md },\n});\n"},{"target":"src/mobileblocks/flows/steadily-onboarding/screens/rhythm-screen.tsx","role":"screen","contents":"import { useMemo } from 'react';\nimport { Pressable, StyleSheet, Text, useColorScheme, View } from 'react-native';\nimport { SafeAreaView } from 'react-native-safe-area-context';\n\nimport { Button } from '../../../components/button';\nimport { palette, radius, space } from '../../../foundation/tokens';\n\nexport type Rhythm = 'gentle' | 'balanced' | 'focused';\n\nexport interface RhythmScreenProps {\n  colorMode?: 'light' | 'dark' | 'system';\n  onBack?: () => void;\n  onChange?: (value: Rhythm) => void;\n  onContinue?: () => void;\n  value: Rhythm;\n}\n\nconst choices: Array<{ value: Rhythm; title: string; detail: string }> = [\n  { value: 'gentle', title: 'Gentle', detail: 'One small ritual at a time' },\n  { value: 'balanced', title: 'Balanced', detail: 'A little structure, room to adapt' },\n  { value: 'focused', title: 'Focused', detail: 'Clear targets and a stronger pace' },\n];\n\nexport function RhythmScreen({\n  colorMode = 'system',\n  onBack,\n  onChange,\n  onContinue,\n  value,\n}: RhythmScreenProps) {\n  const systemColorScheme = useColorScheme();\n  const isDark = colorMode === 'dark' || (colorMode === 'system' && systemColorScheme === 'dark');\n  const theme = useMemo(() => (isDark ? palette.dark : palette), [isDark]);\n\n  return (\n    <View style={[styles.root, { backgroundColor: theme.canvas }]}>\n      <SafeAreaView style={styles.safeArea} edges={['top', 'bottom']}>\n        <View style={styles.header}>\n          <Pressable accessibilityRole=\"button\" hitSlop={12} onPress={onBack}>\n            <Text style={[styles.back, { color: theme.ink }]}>Back</Text>\n          </Pressable>\n          <Text style={[styles.step, { color: theme.inkMuted }]}>2 of 3</Text>\n        </View>\n\n        <View style={styles.copy}>\n          <Text style={[styles.eyebrow, { color: theme.inkMuted }]}>YOUR RHYTHM</Text>\n          <Text style={[styles.title, { color: theme.ink }]}>How should this feel?</Text>\n          <Text style={[styles.body, { color: theme.inkMuted }]}>Choose a starting pace. You can change it whenever life does.</Text>\n        </View>\n\n        <View accessibilityRole=\"radiogroup\" style={styles.choices}>\n          {choices.map((choice) => {\n            const selected = value === choice.value;\n            return (\n              <Pressable\n                accessibilityRole=\"radio\"\n                accessibilityState={{ checked: selected }}\n                key={choice.value}\n                onPress={() => onChange?.(choice.value)}\n                style={({ pressed }) => [\n                  styles.choice,\n                  { backgroundColor: theme.surface, borderColor: selected ? theme.accent : theme.line },\n                  pressed && styles.pressed,\n                ]}\n              >\n                <View style={[styles.radio, { borderColor: selected ? theme.accent : theme.line }]}>\n                  {selected && <View style={[styles.radioDot, { backgroundColor: theme.accent }]} />}\n                </View>\n                <View style={styles.choiceCopy}>\n                  <Text style={[styles.choiceTitle, { color: theme.ink }]}>{choice.title}</Text>\n                  <Text style={[styles.choiceDetail, { color: theme.inkMuted }]}>{choice.detail}</Text>\n                </View>\n              </Pressable>\n            );\n          })}\n        </View>\n\n        <View style={styles.footer}>\n          <Button colorMode={colorMode} onPress={onContinue}>Continue</Button>\n        </View>\n      </SafeAreaView>\n    </View>\n  );\n}\n\nconst styles = StyleSheet.create({\n  root: { flex: 1 },\n  safeArea: { flex: 1, paddingHorizontal: space.xl, paddingTop: space.sm, paddingBottom: space.md },\n  header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' },\n  back: { fontSize: 15, fontWeight: '700' },\n  step: { fontSize: 13, fontWeight: '600' },\n  copy: { marginTop: space.jumbo },\n  eyebrow: { fontSize: 11, fontWeight: '800', letterSpacing: 1.2 },\n  title: { marginTop: space.md, fontSize: 38, lineHeight: 42, fontWeight: '700', letterSpacing: -1.7 },\n  body: { marginTop: space.lg, maxWidth: 330, fontSize: 17, lineHeight: 25 },\n  choices: { marginTop: space.xxl, gap: space.md },\n  choice: { minHeight: 84, flexDirection: 'row', alignItems: 'center', borderWidth: 1.5, borderRadius: radius.card, padding: space.lg },\n  pressed: { opacity: 0.8 },\n  radio: { width: 22, height: 22, borderWidth: 1.5, borderRadius: radius.pill, alignItems: 'center', justifyContent: 'center' },\n  radioDot: { width: 10, height: 10, borderRadius: radius.pill },\n  choiceCopy: { marginLeft: space.lg },\n  choiceTitle: { fontSize: 16, fontWeight: '700' },\n  choiceDetail: { marginTop: space.xs, fontSize: 13 },\n  footer: { flex: 1, justifyContent: 'flex-end' },\n});\n"},{"target":"src/mobileblocks/flows/steadily-onboarding/screens/ready-screen.tsx","role":"screen","contents":"import { useMemo } from 'react';\nimport { Pressable, StyleSheet, Text, useColorScheme, View } from 'react-native';\nimport { SafeAreaView } from 'react-native-safe-area-context';\n\nimport { Button } from '../../../components/button';\nimport { palette, radius, space } from '../../../foundation/tokens';\nimport type { Rhythm } from './rhythm-screen';\n\nexport interface ReadyScreenProps {\n  colorMode?: 'light' | 'dark' | 'system';\n  onBack?: () => void;\n  onComplete?: () => void;\n  rhythm: Rhythm;\n}\n\nconst rhythmCopy: Record<Rhythm, string> = {\n  gentle: 'One small ritual, repeated kindly.',\n  balanced: 'Enough structure to help, enough room to adapt.',\n  focused: 'Clear intentions and a stronger daily pace.',\n};\n\nexport function ReadyScreen({ colorMode = 'system', onBack, onComplete, rhythm }: ReadyScreenProps) {\n  const systemColorScheme = useColorScheme();\n  const isDark = colorMode === 'dark' || (colorMode === 'system' && systemColorScheme === 'dark');\n  const theme = useMemo(() => (isDark ? palette.dark : palette), [isDark]);\n\n  return (\n    <View style={[styles.root, { backgroundColor: theme.canvas }]}>\n      <SafeAreaView style={styles.safeArea} edges={['top', 'bottom']}>\n        <View style={styles.header}>\n          <Pressable accessibilityRole=\"button\" hitSlop={12} onPress={onBack}>\n            <Text style={[styles.back, { color: theme.ink }]}>Back</Text>\n          </Pressable>\n          <Text style={[styles.step, { color: theme.inkMuted }]}>3 of 3</Text>\n        </View>\n\n        <View style={styles.center}>\n          <View style={[styles.mark, { backgroundColor: theme.success }]}>\n            <Text style={[styles.markText, { color: theme.canvas }]}>✓</Text>\n          </View>\n          <Text style={[styles.title, { color: theme.ink }]}>Your pace is enough.</Text>\n          <Text style={[styles.body, { color: theme.inkMuted }]}>{rhythmCopy[rhythm]}</Text>\n          <View style={[styles.summary, { backgroundColor: theme.surface, borderColor: theme.line }]}>\n            <Text style={[styles.summaryLabel, { color: theme.inkMuted }]}>STARTING RHYTHM</Text>\n            <Text style={[styles.summaryValue, { color: theme.ink }]}>{rhythm}</Text>\n          </View>\n        </View>\n\n        <Button colorMode={colorMode} onPress={onComplete}>Build my first ritual</Button>\n      </SafeAreaView>\n    </View>\n  );\n}\n\nconst styles = StyleSheet.create({\n  root: { flex: 1 },\n  safeArea: { flex: 1, paddingHorizontal: space.xl, paddingTop: space.sm, paddingBottom: space.md },\n  header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' },\n  back: { fontSize: 15, fontWeight: '700' },\n  step: { fontSize: 13, fontWeight: '600' },\n  center: { flex: 1, alignItems: 'center', justifyContent: 'center' },\n  mark: { width: 72, height: 72, borderRadius: radius.pill, alignItems: 'center', justifyContent: 'center' },\n  markText: { fontSize: 30, fontWeight: '800' },\n  title: { marginTop: space.xxl, maxWidth: 320, textAlign: 'center', fontSize: 38, lineHeight: 42, fontWeight: '700', letterSpacing: -1.7 },\n  body: { marginTop: space.lg, maxWidth: 310, textAlign: 'center', fontSize: 17, lineHeight: 25 },\n  summary: { width: '100%', marginTop: space.xxl, borderWidth: 1, borderRadius: radius.card, padding: space.xl },\n  summaryLabel: { fontSize: 11, fontWeight: '800', letterSpacing: 1.2 },\n  summaryValue: { marginTop: space.sm, fontSize: 20, fontWeight: '700', textTransform: 'capitalize' },\n});\n"},{"target":"src/mobileblocks/components/button.tsx","role":"component","contents":"import type { ReactNode } from 'react';\nimport {\n  ActivityIndicator,\n  Pressable,\n  StyleSheet,\n  Text,\n  useColorScheme,\n  type PressableProps,\n  type TextStyle,\n  type ViewStyle,\n} from 'react-native';\n\nimport { palette, radius, space } from '../foundation/tokens';\n\ntype ButtonTone = 'primary' | 'secondary';\n\nexport interface ButtonProps extends Omit<PressableProps, 'children' | 'style'> {\n  children: ReactNode;\n  colorMode?: 'light' | 'dark' | 'system';\n  loading?: boolean;\n  tone?: ButtonTone;\n}\n\nexport function Button({\n  children,\n  colorMode = 'system',\n  disabled = false,\n  loading = false,\n  tone = 'primary',\n  accessibilityLabel,\n  ...pressableProps\n}: ButtonProps) {\n  const systemColorScheme = useColorScheme();\n  const isDark = colorMode === 'dark' || (colorMode === 'system' && systemColorScheme === 'dark');\n  const activePalette = isDark ? palette.dark : palette;\n  const toneStyles: Record<\n    ButtonTone,\n    { container: ViewStyle; pressed: ViewStyle; label: TextStyle; spinner: string }\n  > = {\n    primary: {\n      container: { backgroundColor: activePalette.accent },\n      pressed: { backgroundColor: activePalette.accentPressed },\n      label: { color: activePalette.accentText },\n      spinner: activePalette.accentText,\n    },\n    secondary: {\n      container: { backgroundColor: 'transparent', borderColor: activePalette.line, borderWidth: 1 },\n      pressed: { backgroundColor: activePalette.surface },\n      label: { color: activePalette.ink },\n      spinner: activePalette.ink,\n    },\n  };\n  const selectedTone = toneStyles[tone];\n  const isDisabled = disabled || loading;\n\n  return (\n    <Pressable\n      accessibilityLabel={accessibilityLabel ?? (typeof children === 'string' ? children : undefined)}\n      accessibilityRole=\"button\"\n      accessibilityState={{ busy: loading, disabled: isDisabled }}\n      disabled={isDisabled}\n      hitSlop={4}\n      style={({ pressed }) => [\n        styles.container,\n        selectedTone.container,\n        pressed && selectedTone.pressed,\n        pressed && styles.pressed,\n        isDisabled && styles.disabled,\n      ]}\n      {...pressableProps}\n    >\n      {loading ? (\n        <ActivityIndicator color={selectedTone.spinner} />\n      ) : (\n        <Text style={[styles.label, selectedTone.label]}>{children}</Text>\n      )}\n    </Pressable>\n  );\n}\n\nconst styles = StyleSheet.create({\n  container: {\n    minHeight: 56,\n    alignItems: 'center',\n    justifyContent: 'center',\n    borderRadius: radius.control,\n    paddingHorizontal: space.xl,\n  },\n  label: {\n    fontSize: 16,\n    fontWeight: '700',\n    letterSpacing: -0.2,\n  },\n  pressed: {\n    transform: [{ scale: 0.985 }],\n  },\n  disabled: {\n    opacity: 0.48,\n  },\n});\n"},{"target":"src/mobileblocks/foundation/tokens.ts","role":"tokens","contents":"export const palette = {\n  ink: '#20211F',\n  inkMuted: '#6F706A',\n  canvas: '#F2F0EA',\n  surface: '#FAF9F5',\n  line: '#DAD7CE',\n  accent: '#D95D39',\n  accentPressed: '#BF4D2D',\n  accentText: '#FFF9F5',\n  success: '#56745B',\n  dark: {\n    ink: '#F4F1E8',\n    inkMuted: '#ABA99F',\n    canvas: '#171816',\n    surface: '#22231F',\n    line: '#393A34',\n    accent: '#E87350',\n    accentPressed: '#CE6040',\n    accentText: '#1B130F',\n    success: '#8AAD90',\n  },\n} as const;\n\nexport const space = {\n  xs: 4,\n  sm: 8,\n  md: 12,\n  lg: 16,\n  xl: 24,\n  xxl: 32,\n  jumbo: 48,\n} as const;\n\nexport const radius = {\n  control: 14,\n  card: 20,\n  pill: 999,\n} as const;\n"},{"target":"src/mobileblocks/flows/steadily-onboarding/README.md","role":"documentation","contents":"# Steadily onboarding\n\nA three-Screen Expo onboarding Flow that introduces a habit product, lets the user choose a starting rhythm, and emits the final choice through `onComplete`.\n\n## Use\n\nRender `OnboardingFlow` inside any Screen or route. The Flow owns only temporary step selection; the consuming app owns navigation, authentication, analytics, and persistence.\n\n```tsx\n<OnboardingFlow\n  onComplete={(rhythm) => saveOnboardingChoice(rhythm)}\n  onSignIn={() => router.push('/sign-in')}\n/>\n```\n\nUse `initialStep` for deterministic previews or to restore progress from application-owned storage.\n"},{"target":"src/mobileblocks/flows/steadily-onboarding/AGENTS.md","role":"documentation","contents":"# Steadily onboarding agent guide\n\n- Use this Flow for a three-step first-run journey, not as a general carousel.\n- Supply persistence, analytics, authentication, and app navigation through the callbacks in `OnboardingFlowProps`.\n- Keep `Rhythm` values stable if stored data already uses them.\n- Product copy, colors, spacing, and ritual examples are safe customization points.\n- Preserve safe-area handling, accessibility roles and states, minimum touch sizes, light/dark behavior, and the typed completion result.\n- Do not add network requests, storage access, or Expo Router imports to the core Flow. Add an application-owned adapter instead.\n"}]}