From 255e5a6b244ca99ff0122341a632d01c3ecd92fa Mon Sep 17 00:00:00 2001 From: Jacob Mathison Date: Fri, 12 Jun 2026 07:13:41 -0500 Subject: [PATCH] feat(toolkit): scaffold wiki-driven toolkit sections --- __tests__/navigationRoutes.test.ts | 40 ++- __tests__/screenScaffold.test.tsx | 64 ++++- src/navigation/AppNavigator.tsx | 13 +- src/navigation/routes.ts | 109 ++++++-- src/screens/PageScreen.tsx | 14 +- src/screens/ToolkitScreen.tsx | 399 +++++++++++++++++++++++++++-- 6 files changed, 574 insertions(+), 65 deletions(-) diff --git a/__tests__/navigationRoutes.test.ts b/__tests__/navigationRoutes.test.ts index 72755e7..a39ca95 100644 --- a/__tests__/navigationRoutes.test.ts +++ b/__tests__/navigationRoutes.test.ts @@ -4,6 +4,7 @@ import { requiredPageRoutes, toolkitRoutes, routeDefinitions, + normalizeRouteName, } from '../src/navigation/routes'; describe('routeDefinitions', () => { @@ -19,11 +20,19 @@ describe('routeDefinitions', () => { 'policiesCookies', 'credits', 'toolkit', - 'toolkitOpeningSequence', - 'toolkitCredits', - 'toolkitAudio', - 'toolkitLore', + 'toolkitOpening', + 'toolkitPetitioners', + 'toolkitRulings', + 'toolkitSeed', 'toolkitResources', + 'toolkitMap', + 'toolkitFactions', + 'toolkitFlags', + 'toolkitStory', + 'toolkitLore', + 'toolkitAudio', + 'toolkitEndings', + 'toolkitReferences', ]); for (const routeName of requiredPageRoutes) { @@ -50,11 +59,26 @@ describe('routeDefinitions', () => { ]); expect(toolkitRoutes).toEqual([ - 'toolkitOpeningSequence', - 'toolkitCredits', - 'toolkitAudio', - 'toolkitLore', + 'toolkitOpening', + 'toolkitPetitioners', + 'toolkitRulings', + 'toolkitSeed', 'toolkitResources', + 'toolkitMap', + 'toolkitFactions', + 'toolkitFlags', + 'toolkitStory', + 'toolkitLore', + 'toolkitAudio', + 'toolkitEndings', + 'toolkitReferences', ]); }); + + it('normalizes stale persisted toolkit routes', () => { + expect(normalizeRouteName('toolkitOpeningSequence')).toBe('toolkit'); + expect(normalizeRouteName('toolkitCredits')).toBe('toolkit'); + expect(normalizeRouteName('not-a-route')).toBe('home'); + expect(normalizeRouteName('toolkitPetitioners')).toBe('toolkitPetitioners'); + }); }); diff --git a/__tests__/screenScaffold.test.tsx b/__tests__/screenScaffold.test.tsx index bf33ded..54b5421 100644 --- a/__tests__/screenScaffold.test.tsx +++ b/__tests__/screenScaffold.test.tsx @@ -240,11 +240,21 @@ describe('screen scaffold', () => { const text = getRenderedText(renderer); expect(text).toContain('Toolkit'); - expect(text).toContain('Opening Sequence'); - expect(text).toContain('Credits'); - expect(text).toContain('Audio'); - expect(text).toContain('Lore'); + expect(text).toContain('Opening'); + expect(text).toContain('Petitioners'); + expect(text).toContain('Rulings'); + expect(text).toContain('Seed'); expect(text).toContain('Resources'); + expect(text).toContain('Map'); + expect(text).toContain('Factions'); + expect(text).toContain('Flags'); + expect(text).toContain('Story'); + expect(text).toContain('Lore'); + expect(text).toContain('Audio'); + expect(text).toContain('Endings'); + expect(text).toContain('References'); + expect(text).toContain('Read-only planning panel'); + expect(text).toContain('Future authoring area'); const homeButton = renderer.root.findByProps({ testID: 'toolkit-home' }); @@ -256,6 +266,48 @@ describe('screen scaffold', () => { expect(renderer.root.findByProps({ testID: 'toolkit-socket-indicator' })).toBeTruthy(); }); + it('renders purpose panels for every toolkit section without forms', () => { + const expectedPurposeText = { + toolkitAudio: 'Preview and catalog sound identity', + toolkitEndings: 'Tune collapse states', + toolkitFactions: 'Tune faction trust', + toolkitFlags: 'Debug delayed consequences', + toolkitLore: 'Reference changing canon', + toolkitMap: 'Support settlement and world-map iteration', + toolkitOpening: 'Preview and sequence the first-run premise', + toolkitPetitioners: 'Inspect and eventually author throne-room cases', + toolkitReferences: 'Keep design inspirations', + toolkitResources: 'Tune the core Bones, Minions, and Milk economy', + toolkitRulings: 'Inspect and eventually tune choice outcomes', + toolkitSeed: 'Debug why a day produced specific petitioners', + toolkitStory: 'Organize Act I-IV pressure', + } as const; + + for (const routeName of toolkitRoutes) { + const renderer = render( + , + ); + const text = getRenderedText(renderer); + + expect(text).toContain(expectedPurposeText[routeName]); + expect(renderer.root.findByProps({ testID: 'toolkit-purpose' })).toBeTruthy(); + expect( + renderer.root.findByProps({ testID: 'toolkit-change-note' }), + ).toBeTruthy(); + expect(renderer.root.findByProps({ testID: 'toolkit-sources' })).toBeTruthy(); + expect( + renderer.root.findByProps({ testID: 'toolkit-future-fields' }), + ).toBeTruthy(); + expect( + renderer.root.findByProps({ testID: 'toolkit-future-placeholder' }), + ).toBeTruthy(); + } + }); + it('renders the toolkit websocket connection indicator', () => { act(() => { useToolkitSocketStore.getState().setConnected('test-socket'); @@ -300,10 +352,10 @@ describe('screen scaffold', () => { expect(optionGroup.props.style).toMatchObject({ flexDirection: 'row' }); expect(homeSlot.props.style).toMatchObject({ marginLeft: 'auto' }); expect( - renderer.root.findByProps({ testID: 'toolkit-toolkitOpeningSequence-icon' }), + renderer.root.findByProps({ testID: 'toolkit-toolkitOpening-icon' }), ).toBeTruthy(); expect( - renderer.root.findByProps({ testID: 'toolkit-toolkitOpeningSequence-label' }), + renderer.root.findByProps({ testID: 'toolkit-toolkitOpening-label' }), ).toBeTruthy(); }); diff --git a/src/navigation/AppNavigator.tsx b/src/navigation/AppNavigator.tsx index ae9aca5..1c8b07a 100644 --- a/src/navigation/AppNavigator.tsx +++ b/src/navigation/AppNavigator.tsx @@ -8,7 +8,7 @@ import { PoliciesScreen } from '../screens/PoliciesScreen'; import { SettingsScreen } from '../screens/SettingsScreen'; import { ToolkitScreen } from '../screens/ToolkitScreen'; import { useNavigationStore } from '../state'; -import { toolkitRoutes, type RouteName } from './routes'; +import { normalizeRouteName, toolkitRoutes, type RouteName } from './routes'; const fadeOutDurationMs = process.env.NODE_ENV === 'test' ? 0 : 140; const fadeInDurationMs = process.env.NODE_ENV === 'test' ? 0 : 180; @@ -19,8 +19,9 @@ export function AppNavigator() { const cancelPendingRoute = useNavigationStore(state => state.cancelPendingRoute); const completeNavigation = useNavigationStore(state => state.completeNavigation); const pendingRouteName = useNavigationStore(state => state.pendingRouteName); - const routeName = useNavigationStore(state => state.currentRouteName); + const rawRouteName = useNavigationStore(state => state.currentRouteName); const setPendingRoute = useNavigationStore(state => state.setPendingRoute); + const routeName = normalizeRouteName(rawRouteName); const opacity = useRef(new Animated.Value(1)).current; const isTransitioning = useRef(false); @@ -29,9 +30,15 @@ export function AppNavigator() { return; } - completeNavigation(pendingRouteName); + completeNavigation(normalizeRouteName(pendingRouteName)); }, [completeNavigation, pendingRouteName]); + useEffect(() => { + if (routeName !== rawRouteName) { + completeNavigation(routeName); + } + }, [completeNavigation, rawRouteName, routeName]); + const navigate = useCallback((nextRouteName: RouteName) => { if ( nextRouteName === routeName || diff --git a/src/navigation/routes.ts b/src/navigation/routes.ts index 1196d18..c8c6964 100644 --- a/src/navigation/routes.ts +++ b/src/navigation/routes.ts @@ -43,25 +43,57 @@ export const routeDefinitions = { label: 'Toolkit', title: 'Toolkit', }, - toolkitOpeningSequence: { - label: 'Opening Sequence', + toolkitOpening: { + label: 'Opening', title: 'Opening Sequence', }, - toolkitCredits: { - label: 'Credits', - title: 'Toolkit Credits', + toolkitPetitioners: { + label: 'Petitioners', + title: 'Petitioners', }, - toolkitAudio: { - label: 'Audio', - title: 'Audio', + toolkitRulings: { + label: 'Rulings', + title: 'Rulings', + }, + toolkitSeed: { + label: 'Seed', + title: 'Daily Seed', + }, + toolkitResources: { + label: 'Resources', + title: 'Resources', + }, + toolkitMap: { + label: 'Map', + title: 'World Map', + }, + toolkitFactions: { + label: 'Factions', + title: 'Factions', + }, + toolkitFlags: { + label: 'Flags', + title: 'Flags & History', + }, + toolkitStory: { + label: 'Story', + title: 'Story Arcs', }, toolkitLore: { label: 'Lore', title: 'Lore', }, - toolkitResources: { - label: 'Resources', - title: 'Resources', + toolkitAudio: { + label: 'Audio', + title: 'Audio', + }, + toolkitEndings: { + label: 'Endings', + title: 'Failure & Endings', + }, + toolkitReferences: { + label: 'References', + title: 'References', }, } as const; @@ -84,12 +116,43 @@ export const policyRoutes = [ ] as const satisfies readonly RouteName[]; export const toolkitRoutes = [ + 'toolkitOpening', + 'toolkitPetitioners', + 'toolkitRulings', + 'toolkitSeed', + 'toolkitResources', + 'toolkitMap', + 'toolkitFactions', + 'toolkitFlags', + 'toolkitStory', + 'toolkitLore', + 'toolkitAudio', + 'toolkitEndings', + 'toolkitReferences', +] as const satisfies readonly RouteName[]; + +export type ToolkitRouteName = (typeof toolkitRoutes)[number]; + +const legacyToolkitRoutes = [ 'toolkitOpeningSequence', 'toolkitCredits', - 'toolkitAudio', - 'toolkitLore', - 'toolkitResources', -] as const satisfies readonly RouteName[]; +] as const; + +export function normalizeRouteName(routeName: unknown): RouteName { + if (typeof routeName !== 'string') { + return 'home'; + } + + if (routeName in routeDefinitions) { + return routeName as RouteName; + } + + if ((legacyToolkitRoutes as readonly string[]).includes(routeName)) { + return 'toolkit'; + } + + return 'home'; +} export const requiredPageRoutes = [ 'home', @@ -102,9 +165,17 @@ export const requiredPageRoutes = [ 'policiesCookies', 'credits', 'toolkit', - 'toolkitOpeningSequence', - 'toolkitCredits', - 'toolkitAudio', - 'toolkitLore', + 'toolkitOpening', + 'toolkitPetitioners', + 'toolkitRulings', + 'toolkitSeed', 'toolkitResources', + 'toolkitMap', + 'toolkitFactions', + 'toolkitFlags', + 'toolkitStory', + 'toolkitLore', + 'toolkitAudio', + 'toolkitEndings', + 'toolkitReferences', ] as const satisfies readonly RouteName[]; diff --git a/src/screens/PageScreen.tsx b/src/screens/PageScreen.tsx index a743edf..05e85fe 100644 --- a/src/screens/PageScreen.tsx +++ b/src/screens/PageScreen.tsx @@ -2,17 +2,15 @@ import { StyleSheet, Text, View } from 'react-native'; import { BackHomeButton } from '../components/BackHomeButton'; import { ScreenLayout } from '../components/ScreenLayout'; -import { routeDefinitions, type RouteName } from '../navigation/routes'; +import { + routeDefinitions, + type RouteName, + type ToolkitRouteName, +} from '../navigation/routes'; type PageRouteName = Exclude< RouteName, - | 'home' - | 'policies' - | 'settings' - | 'toolkit' - | 'toolkitOpeningSequence' - | 'toolkitCredits' - | 'toolkitAudio' + 'home' | 'policies' | 'settings' | 'toolkit' | ToolkitRouteName >; type PageScreenProps = Readonly<{ diff --git a/src/screens/ToolkitScreen.tsx b/src/screens/ToolkitScreen.tsx index ae47920..240a05f 100644 --- a/src/screens/ToolkitScreen.tsx +++ b/src/screens/ToolkitScreen.tsx @@ -5,32 +5,255 @@ import { routeDefinitions, toolkitRoutes, type RouteName, + type ToolkitRouteName, } from '../navigation/routes'; import { useToolkitSocketStore, type ToolkitSocketStatus } from '../state'; -type ToolkitRouteName = (typeof toolkitRoutes)[number]; - type ToolkitScreenProps = Readonly<{ goHome: () => void; navigate: (routeName: RouteName) => void; routeName: RouteName; }>; -const toolkitDescriptions: Partial> = { - toolkit: 'Select a toolkit area to preview game support assets.', - toolkitAudio: 'Audio tools and checks will live here.', - toolkitCredits: 'Toolkit-specific credit review will live here.', - toolkitLore: 'Lore reference material will live here.', - toolkitOpeningSequence: 'Opening sequence preview controls will live here.', - toolkitResources: 'Production and gameplay resources will live here.', +type ToolkitSectionContent = Readonly<{ + futureFields: string[]; + liableToChange: string; + purpose: string; + sources: string[]; +}>; + +const toolkitOverview: ToolkitSectionContent = { + futureFields: [ + 'section purpose map', + 'wiki source references', + 'future forms', + 'tables', + 'previews', + ], + liableToChange: + 'The Toolkit should follow whichever gameplay loops prove most volatile during early design.', + purpose: + 'Select a toolkit area to inspect the systems most likely to change while the throne room loop stabilizes.', + sources: ['Home', 'Gameplay', 'Technical Design Specs'], +}; + +const toolkitSections: Record = { + toolkitAudio: { + futureFields: [ + 'throne room theme', + 'world map theme', + 'petitioner stings', + 'BLB voice direction', + 'comedy SFX rules', + ], + liableToChange: + 'Audio identity will shift as petitioner types, faction tone, and readable decision pacing evolve.', + purpose: + 'Preview and catalog sound identity tied to throne room, map, petitioner, BLB voice, and comedy contexts.', + sources: ['Music', 'Gameplay', 'References'], + }, + toolkitEndings: { + futureFields: [ + 'resource collapse', + 'stability collapse', + 'settlement collapse count', + 'faction coup', + 'ending summaries', + ], + liableToChange: + 'Failure thresholds and ending categories will change as the run length and kingdom identity model settle.', + purpose: + 'Tune collapse states and outcome summaries so failure and victory reflect accumulated rulings.', + sources: ['Story', 'Technical Design Specs'], + }, + toolkitFactions: { + futureFields: [ + 'faction ID', + 'trust', + 'power', + 'agenda', + 'likes', + 'dislikes', + 'coup risks', + ], + liableToChange: + 'Faction trust and power are expected to move as petitioner pressure and map consequence rules mature.', + purpose: + 'Tune faction trust, power, agenda pressure, and the events attached to faction relationships.', + sources: ['Lore', 'Gameplay', 'Technical Design Specs'], + }, + toolkitFlags: { + futureFields: [ + 'active flags', + 'seen petitioners', + 'rulings', + 'daily summaries', + 'unlocked events', + 'chain triggers', + ], + liableToChange: + 'Delayed consequences and recurring petitioner chains depend on flags that will expand quickly.', + purpose: + 'Debug delayed consequences, recurring petitioner chains, run history, and unlocked future events.', + sources: ['Gameplay', 'Story', 'Technical Design Specs'], + }, + toolkitLore: { + futureFields: [ + 'settlements', + 'factions', + 'BLB origin variants', + 'BLB limits', + 'canon rules', + 'joke rules', + ], + liableToChange: + 'Canon needs room to absorb absurd jokes while still preserving consequence and internal logic.', + purpose: + 'Reference changing canon, world concepts, BLB rules, faction needs, and settlement identity.', + sources: ['Lore', 'Story', 'References'], + }, + toolkitMap: { + futureFields: [ + 'settlements', + 'owner', + 'faction influence', + 'production', + 'stability', + 'need', + 'status effects', + ], + liableToChange: + 'The map is the consequence layer, so settlement state will change as ruling effects become visible.', + purpose: + 'Support settlement and world-map iteration as throne room choices ripple into territory state.', + sources: ['Gameplay', 'Lore', 'Technical Design Specs'], + }, + toolkitOpening: { + futureFields: [ + 'opening premise', + 'first day setup', + 'initial resources', + 'first petitioners', + 'first map action', + ], + liableToChange: + 'The first-run sequence will change as the tutorial learns how much of the loop to teach up front.', + purpose: + 'Preview and sequence the first-run premise, Act I intro beats, and early tutorial framing.', + sources: ['Story', 'Gameplay', 'Technical Design Specs'], + }, + toolkitPetitioners: { + futureFields: [ + 'petitioner ID', + 'name', + 'type', + 'faction', + 'settlement', + 'tags', + 'day range', + 'weight', + 'flags', + ], + liableToChange: + 'Petitioners are the primary content loop and will change as cases, factions, and map pressure evolve.', + purpose: + 'Inspect and eventually author throne-room cases, the main variable content loop of each day.', + sources: ['Gameplay', 'Story', 'Technical Design Specs'], + }, + toolkitReferences: { + futureFields: [ + 'game references', + 'visual pillars', + 'tone references', + 'BLB asset links', + 'repo links', + 'build notes', + ], + liableToChange: + 'Reference material will grow as art, audio, design, and implementation sources become concrete.', + purpose: + 'Keep design inspirations, source links, asset references, and build notes close to implementation.', + sources: ['References', 'Home'], + }, + toolkitResources: { + futureFields: [ + 'current totals', + 'daily production', + 'upkeep', + 'conversion rules', + 'thresholds', + 'warnings', + 'collapse triggers', + ], + liableToChange: + 'The Bones, Minions, and Milk triangle is the core economy and will need repeated tuning.', + purpose: + 'Tune the core Bones, Minions, and Milk economy plus production, upkeep, and collapse rules.', + sources: ['Gameplay', 'Technical Design Specs'], + }, + toolkitRulings: { + futureFields: [ + 'label', + 'tone', + 'costs', + 'resource effects', + 'settlement effects', + 'faction effects', + 'future weights', + 'result text', + ], + liableToChange: + 'Ruling effects need to be tuned separately from flavor text as consequence balance changes.', + purpose: + 'Inspect and eventually tune choice outcomes independently from petitioner presentation.', + sources: ['Gameplay', 'Technical Design Specs'], + }, + toolkitSeed: { + futureFields: [ + 'eligible pool', + 'filters', + 'weights', + 'shortage modifiers', + 'trust modifiers', + 'selected IDs', + 'day history', + ], + liableToChange: + 'Daily selection rules will change as shortages, unstable settlements, and faction pressure matter more.', + purpose: + 'Debug why a day produced specific petitioners from deterministic seed and world-state weighting.', + sources: ['Gameplay', 'Technical Design Specs'], + }, + toolkitStory: { + futureFields: [ + 'act definitions', + 'recurring petitioner arcs', + 'campaign pressure', + 'kingdom signals', + 'endgame hooks', + ], + liableToChange: + 'Story pressure should remain sandbox-first, so arcs will shift around emergent kingdom state.', + purpose: + 'Organize Act I-IV pressure, recurring petitioner arcs, and long-term kingdom identity.', + sources: ['Story', 'Lore', 'Gameplay'], + }, }; const toolkitIcons: Record = { toolkitAudio: '♪', - toolkitCredits: '★', + toolkitEndings: '!', + toolkitFactions: '◇', + toolkitFlags: '#', toolkitLore: '✦', - toolkitOpeningSequence: '▶', + toolkitMap: '⌖', + toolkitOpening: '▶', + toolkitPetitioners: '◉', + toolkitReferences: '※', toolkitResources: '▦', + toolkitRulings: '§', + toolkitSeed: '◎', + toolkitStory: '★', }; const socketStatusCopy: Record = { @@ -47,6 +270,7 @@ export function ToolkitScreen({ const safeAreaInsets = useSafeAreaInsets(); const activeRoute = routeDefinitions[routeName]; const socketStatus = useToolkitSocketStore(state => state.status); + const toolkitContent = getToolkitContent(routeName); return ( Bone Lord Bob {activeRoute.title} - - {toolkitDescriptions[routeName] ?? toolkitDescriptions.toolkit} - + Read-only planning panel + + + + + + Source wiki pages + + {toolkitContent.sources.map(source => ( + + {source} + + ))} + + + + + Future authoring area + + Forms, tables, previews, and schema-backed validation will mount + here after the JSON and Markdown content formats stabilize. + + ); } +function getToolkitContent(routeName: RouteName) { + if ((toolkitRoutes as readonly RouteName[]).includes(routeName)) { + return toolkitSections[routeName as ToolkitRouteName]; + } + + return toolkitOverview; +} + +type ToolkitDetailProps = Readonly<{ + label: string; + testID: string; + value: string; +}>; + +function ToolkitDetail({ label, testID, value }: ToolkitDetailProps) { + return ( + + {label} + {value} + + ); +} + type ToolkitSocketIndicatorProps = Readonly<{ status: ToolkitSocketStatus; }>; @@ -187,11 +468,31 @@ const styles = StyleSheet.create({ padding: 14, position: 'relative', }, - description: { + detailGrid: { + flexDirection: 'row', + gap: 10, + marginBottom: 10, + }, + detailLabel: { + color: '#d6b25e', + fontSize: 10, + fontWeight: '800', + letterSpacing: 0, + marginBottom: 5, + textTransform: 'uppercase', + }, + detailPanel: { + backgroundColor: '#131518', + borderColor: '#2d2f32', + borderRadius: 6, + borderWidth: 1, + flex: 1, + padding: 10, + }, + detailValue: { color: '#c9c1b3', - fontSize: 14, - lineHeight: 20, - maxWidth: 420, + fontSize: 12, + lineHeight: 17, }, eyebrow: { color: '#d6b25e', @@ -209,6 +510,60 @@ const styles = StyleSheet.create({ lineHeight: 34, marginBottom: 12, }, + placeholderCopy: { + color: '#c9c1b3', + fontSize: 12, + lineHeight: 17, + }, + placeholderPanel: { + backgroundColor: '#24272b', + borderColor: '#4d4432', + borderRadius: 6, + borderStyle: 'dashed', + borderWidth: 1, + marginTop: 10, + padding: 10, + }, + placeholderTitle: { + color: '#f6f1e7', + fontSize: 12, + fontWeight: '800', + letterSpacing: 0, + marginBottom: 4, + }, + readOnlyLabel: { + color: '#80796c', + fontSize: 11, + fontWeight: '800', + letterSpacing: 0, + marginBottom: 10, + textTransform: 'uppercase', + }, + sourcePanel: { + backgroundColor: '#131518', + borderColor: '#2d2f32', + borderRadius: 6, + borderWidth: 1, + marginBottom: 10, + padding: 10, + }, + sourcePill: { + backgroundColor: '#2a261d', + borderColor: '#4d4432', + borderRadius: 4, + borderWidth: 1, + color: '#f5d98d', + fontSize: 11, + fontWeight: '800', + letterSpacing: 0, + paddingHorizontal: 7, + paddingVertical: 4, + }, + sourceRow: { + flexDirection: 'row', + flexWrap: 'wrap', + gap: 6, + }, socketDot: { borderRadius: 5, height: 10, @@ -245,7 +600,7 @@ const styles = StyleSheet.create({ borderWidth: 1, justifyContent: 'center', minHeight: 34, - paddingHorizontal: 12, + paddingHorizontal: 8, paddingVertical: 6, }, toolbarButtonActive: { @@ -255,7 +610,7 @@ const styles = StyleSheet.create({ toolbarButtonContent: { alignItems: 'center', flexDirection: 'row', - gap: 5, + gap: 4, justifyContent: 'center', }, toolbarButtonIcon: { @@ -272,7 +627,7 @@ const styles = StyleSheet.create({ }, toolbarButtonLabel: { color: '#f6f1e7', - fontSize: 12, + fontSize: 10, fontWeight: '800', letterSpacing: 0, textAlign: 'center', @@ -285,6 +640,8 @@ const styles = StyleSheet.create({ }, toolbarGroup: { flexDirection: 'row', + flexShrink: 1, + flexWrap: 'wrap', }, });