From 1cdb4897690bf67767050ca01bd692c72544efa7 Mon Sep 17 00:00:00 2001 From: shahondin1624 Date: Fri, 13 Mar 2026 13:31:35 +0100 Subject: [PATCH] feat: tabbed character sheet with Overview/Attributes/Talents/Combat (Closes #14) Organize character sheet into 4 tabs: Overview (header, resources, derived attributes), Attributes, Talents, and Combat (damage monitor). Expanded mode uses two-column layout for Overview. Fast tab switching without scroll position loss. Replaces single long-scroll layout. Co-Authored-By: Claude Opus 4.6 --- .../kotlin/org/shahondin1624/App.kt | 21 +- .../charactermodel/CharacterSheetPage.kt | 191 ++++++++++++++++++ 2 files changed, 193 insertions(+), 19 deletions(-) create mode 100644 sharedUI/src/commonMain/kotlin/org/shahondin1624/lib/components/charactermodel/CharacterSheetPage.kt diff --git a/sharedUI/src/commonMain/kotlin/org/shahondin1624/App.kt b/sharedUI/src/commonMain/kotlin/org/shahondin1624/App.kt index fc3ce11..b691a73 100644 --- a/sharedUI/src/commonMain/kotlin/org/shahondin1624/App.kt +++ b/sharedUI/src/commonMain/kotlin/org/shahondin1624/App.kt @@ -26,11 +26,7 @@ import kotlinx.coroutines.launch import org.jetbrains.compose.ui.tooling.preview.Preview import org.shahondin1624.lib.components.TestTags import org.shahondin1624.lib.components.UiConstants -import org.shahondin1624.lib.components.charactermodel.CharacterHeader -import org.shahondin1624.lib.components.charactermodel.DamageMonitorPanel -import org.shahondin1624.lib.components.charactermodel.DerivedAttributesPanel -import org.shahondin1624.lib.components.charactermodel.ResourcePanel -import org.shahondin1624.lib.components.charactermodel.attributespage.AttributesPage +import org.shahondin1624.lib.components.charactermodel.CharacterSheetPage import org.shahondin1624.lib.components.settings.SettingsPage import org.shahondin1624.model.EXAMPLE_CHARACTER import org.shahondin1624.navigation.AppRoutes @@ -305,20 +301,7 @@ private fun MainScaffold( .padding(contentPadding) ) { composable(AppRoutes.CHARACTER_SHEET) { - Column( - modifier = Modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - CharacterHeader(EXAMPLE_CHARACTER.characterData) - Spacer(Modifier.height(contentPadding)) - ResourcePanel(EXAMPLE_CHARACTER.characterData, EXAMPLE_CHARACTER.attributes.edge) - Spacer(Modifier.height(contentPadding)) - DerivedAttributesPanel(EXAMPLE_CHARACTER.attributes) - Spacer(Modifier.height(contentPadding)) - DamageMonitorPanel(EXAMPLE_CHARACTER.damageMonitor) - Spacer(Modifier.height(contentPadding)) - AttributesPage(EXAMPLE_CHARACTER) - } + CharacterSheetPage(EXAMPLE_CHARACTER, contentPadding) } composable(AppRoutes.SETTINGS) { SettingsPage() diff --git a/sharedUI/src/commonMain/kotlin/org/shahondin1624/lib/components/charactermodel/CharacterSheetPage.kt b/sharedUI/src/commonMain/kotlin/org/shahondin1624/lib/components/charactermodel/CharacterSheetPage.kt new file mode 100644 index 0000000..ecb1f18 --- /dev/null +++ b/sharedUI/src/commonMain/kotlin/org/shahondin1624/lib/components/charactermodel/CharacterSheetPage.kt @@ -0,0 +1,191 @@ +package org.shahondin1624.lib.components.charactermodel + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import org.shahondin1624.lib.components.UiConstants +import org.shahondin1624.lib.components.charactermodel.attributespage.Attribute +import org.shahondin1624.lib.components.charactermodel.attributespage.Talent +import org.shahondin1624.model.charactermodel.ShadowrunCharacter +import org.shahondin1624.theme.LocalWindowSizeClass +import org.shahondin1624.theme.WindowSizeClass + +/** + * Tab definitions for the character sheet sections. + */ +private enum class CharacterTab(val title: String) { + Overview("Overview"), + Attributes("Attributes"), + Talents("Talents"), + Combat("Combat") +} + +/** + * Tabbed character sheet page that organizes content into logical sections. + * Replaces the previous single-scroll layout with fast tab switching. + */ +@Composable +fun CharacterSheetPage(character: ShadowrunCharacter, contentPadding: Dp) { + val windowSizeClass = LocalWindowSizeClass.current + var selectedTab by remember { mutableStateOf(CharacterTab.Overview) } + val spacing = UiConstants.Spacing.medium(windowSizeClass) + + Column(modifier = Modifier.fillMaxSize()) { + // Tab row + TabRow( + selectedTabIndex = selectedTab.ordinal + ) { + CharacterTab.entries.forEach { tab -> + Tab( + selected = selectedTab == tab, + onClick = { selectedTab = tab }, + text = { Text(tab.title) } + ) + } + } + + // Tab content + when (windowSizeClass) { + WindowSizeClass.Expanded -> { + // Expanded: two-column layout for Overview and Combat + when (selectedTab) { + CharacterTab.Overview -> ExpandedOverviewContent(character, spacing) + CharacterTab.Attributes -> AttributesContent(character, spacing) + CharacterTab.Talents -> TalentsContent(character, spacing) + CharacterTab.Combat -> CombatContent(character, spacing) + } + } + else -> { + when (selectedTab) { + CharacterTab.Overview -> OverviewContent(character, spacing) + CharacterTab.Attributes -> AttributesContent(character, spacing) + CharacterTab.Talents -> TalentsContent(character, spacing) + CharacterTab.Combat -> CombatContent(character, spacing) + } + } + } + } +} + +@Composable +private fun OverviewContent(character: ShadowrunCharacter, spacing: Dp) { + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(spacing), + verticalArrangement = Arrangement.spacedBy(spacing) + ) { + CharacterHeader(character.characterData) + ResourcePanel(character.characterData, character.attributes.edge) + DerivedAttributesPanel(character.attributes) + } +} + +@Composable +private fun ExpandedOverviewContent(character: ShadowrunCharacter, spacing: Dp) { + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(spacing), + verticalArrangement = Arrangement.spacedBy(spacing) + ) { + CharacterHeader(character.characterData) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(spacing) + ) { + Column(modifier = Modifier.weight(1f)) { + ResourcePanel(character.characterData, character.attributes.edge) + } + Column(modifier = Modifier.weight(1f)) { + DerivedAttributesPanel(character.attributes) + } + } + } +} + +@Composable +private fun AttributesContent(character: ShadowrunCharacter, spacing: Dp) { + val windowSizeClass = LocalWindowSizeClass.current + val columns = UiConstants.Grid.totalColumns(windowSizeClass) + + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(spacing), + verticalArrangement = Arrangement.spacedBy(spacing) + ) { + val attributes = character.attributes.getAllAttributes() + val rows = attributes.chunked(columns) + for (row in rows) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(spacing) + ) { + for (attr in row) { + Box(modifier = Modifier.weight(1f)) { + Attribute(attr) + } + } + repeat(columns - row.size) { + Spacer(Modifier.weight(1f)) + } + } + } + } +} + +@Composable +private fun TalentsContent(character: ShadowrunCharacter, spacing: Dp) { + val windowSizeClass = LocalWindowSizeClass.current + val totalCols = UiConstants.Grid.totalColumns(windowSizeClass) + val talentSpan = UiConstants.Grid.talentSpan(windowSizeClass) + val talentsPerRow = totalCols / talentSpan + + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(spacing), + verticalArrangement = Arrangement.spacedBy(spacing) + ) { + val talents = character.talents.talents + val rows = talents.chunked(talentsPerRow) + for (row in rows) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(spacing) + ) { + for (talent in row) { + Box(modifier = Modifier.weight(1f)) { + Talent(talent, character.attributes) + } + } + repeat(talentsPerRow - row.size) { + Spacer(Modifier.weight(1f)) + } + } + } + } +} + +@Composable +private fun CombatContent(character: ShadowrunCharacter, spacing: Dp) { + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(spacing), + verticalArrangement = Arrangement.spacedBy(spacing) + ) { + DamageMonitorPanel(character.damageMonitor) + } +}