feat(toolkit): scaffold wiki-driven toolkit sections
This commit is contained in:
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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(
|
||||
<ToolkitScreen
|
||||
goHome={jest.fn()}
|
||||
navigate={jest.fn()}
|
||||
routeName={routeName}
|
||||
/>,
|
||||
);
|
||||
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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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 ||
|
||||
|
||||
+90
-19
@@ -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[];
|
||||
|
||||
@@ -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<{
|
||||
|
||||
+378
-21
@@ -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<Record<RouteName, string>> = {
|
||||
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<ToolkitRouteName, ToolkitSectionContent> = {
|
||||
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<ToolkitRouteName, string> = {
|
||||
toolkitAudio: '♪',
|
||||
toolkitCredits: '★',
|
||||
toolkitEndings: '!',
|
||||
toolkitFactions: '◇',
|
||||
toolkitFlags: '#',
|
||||
toolkitLore: '✦',
|
||||
toolkitOpeningSequence: '▶',
|
||||
toolkitMap: '⌖',
|
||||
toolkitOpening: '▶',
|
||||
toolkitPetitioners: '◉',
|
||||
toolkitReferences: '※',
|
||||
toolkitResources: '▦',
|
||||
toolkitRulings: '§',
|
||||
toolkitSeed: '◎',
|
||||
toolkitStory: '★',
|
||||
};
|
||||
|
||||
const socketStatusCopy: Record<ToolkitSocketStatus, string> = {
|
||||
@@ -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 (
|
||||
<View
|
||||
@@ -87,15 +311,72 @@ export function ToolkitScreen({
|
||||
<View style={styles.contentPanel}>
|
||||
<Text style={styles.eyebrow}>Bone Lord Bob</Text>
|
||||
<Text style={styles.title}>{activeRoute.title}</Text>
|
||||
<Text style={styles.description}>
|
||||
{toolkitDescriptions[routeName] ?? toolkitDescriptions.toolkit}
|
||||
</Text>
|
||||
<Text style={styles.readOnlyLabel}>Read-only planning panel</Text>
|
||||
<View style={styles.detailGrid}>
|
||||
<ToolkitDetail
|
||||
label="Purpose"
|
||||
testID="toolkit-purpose"
|
||||
value={toolkitContent.purpose}
|
||||
/>
|
||||
<ToolkitDetail
|
||||
label="Liable to change because"
|
||||
testID="toolkit-change-note"
|
||||
value={toolkitContent.liableToChange}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.sourcePanel} testID="toolkit-sources">
|
||||
<Text style={styles.detailLabel}>Source wiki pages</Text>
|
||||
<View style={styles.sourceRow}>
|
||||
{toolkitContent.sources.map(source => (
|
||||
<Text key={source} style={styles.sourcePill}>
|
||||
{source}
|
||||
</Text>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
<ToolkitDetail
|
||||
label="Anticipated future fields / entities"
|
||||
testID="toolkit-future-fields"
|
||||
value={toolkitContent.futureFields.join(', ')}
|
||||
/>
|
||||
<View
|
||||
style={styles.placeholderPanel}
|
||||
testID="toolkit-future-placeholder">
|
||||
<Text style={styles.placeholderTitle}>Future authoring area</Text>
|
||||
<Text style={styles.placeholderCopy}>
|
||||
Forms, tables, previews, and schema-backed validation will mount
|
||||
here after the JSON and Markdown content formats stabilize.
|
||||
</Text>
|
||||
</View>
|
||||
<ToolkitSocketIndicator status={socketStatus} />
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<View style={styles.detailPanel} testID={testID}>
|
||||
<Text style={styles.detailLabel}>{label}</Text>
|
||||
<Text style={styles.detailValue}>{value}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
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',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user