app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

commit 594f847eb00ea87848b1311fffbdd8b73301abe8
parent ae2677b73b3c4d3f134027b35dfffe6862dc85ec
Author: triesap <tyson@radroots.org>
Date:   Thu, 13 Aug 2026 17:18:04 +0000

design: own the approved Studio shell foundation

- pin the approved light and dark shell palettes
- expose the Studio-derived application geometry grid
- add shell-specific text roles and interaction state
- verify exact palette and breakpoint invariants

Diffstat:
Aapp/design_system/src/commonMain/kotlin/org/harvestcircle/designsystem/shell/HarvestCircleShellInteraction.kt | 45+++++++++++++++++++++++++++++++++++++++++++++
Aapp/design_system/src/commonMain/kotlin/org/harvestcircle/designsystem/shell/HarvestCircleShellText.kt | 44++++++++++++++++++++++++++++++++++++++++++++
Aapp/design_system/src/commonMain/kotlin/org/harvestcircle/designsystem/shell/HarvestCircleShellVisuals.kt | 137+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/design_system/src/commonTest/kotlin/org/harvestcircle/designsystem/shell/HarvestCircleShellVisualsTest.kt | 31+++++++++++++++++++++++++++++++
4 files changed, 257 insertions(+), 0 deletions(-)

diff --git a/app/design_system/src/commonMain/kotlin/org/harvestcircle/designsystem/shell/HarvestCircleShellInteraction.kt b/app/design_system/src/commonMain/kotlin/org/harvestcircle/designsystem/shell/HarvestCircleShellInteraction.kt @@ -0,0 +1,45 @@ +package org.harvestcircle.designsystem.shell + +import androidx.compose.foundation.hoverable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsFocusedAsState +import androidx.compose.foundation.interaction.collectIsHoveredAsState +import androidx.compose.foundation.interaction.collectIsPressedAsState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier + +@Stable +public class HarvestCircleShellInteractionSources internal constructor( + public val activation: MutableInteractionSource, + public val hover: MutableInteractionSource, +) + +@Immutable +public data class HarvestCircleShellInteractions( + public val hovered: Boolean, + public val pressed: Boolean, + public val focused: Boolean, +) + +@Composable +public fun rememberHarvestCircleShellInteractionSources(key: Any? = Unit): HarvestCircleShellInteractionSources = + remember(key) { HarvestCircleShellInteractionSources(MutableInteractionSource(), MutableInteractionSource()) } + +@Composable +public fun HarvestCircleShellInteractionSources.collectHarvestCircleShellInteractions( + enabled: Boolean = true, +): HarvestCircleShellInteractions { + val hovered by hover.collectIsHoveredAsState() + val pressed by activation.collectIsPressedAsState() + val focused by activation.collectIsFocusedAsState() + return HarvestCircleShellInteractions(enabled && hovered, enabled && pressed, enabled && focused) +} + +public fun Modifier.harvestCircleShellHoverable( + sources: HarvestCircleShellInteractionSources, + enabled: Boolean = true, +): Modifier = hoverable(sources.hover, enabled) diff --git a/app/design_system/src/commonMain/kotlin/org/harvestcircle/designsystem/shell/HarvestCircleShellText.kt b/app/design_system/src/commonMain/kotlin/org/harvestcircle/designsystem/shell/HarvestCircleShellText.kt @@ -0,0 +1,44 @@ +package org.harvestcircle.designsystem.shell + +import androidx.compose.foundation.text.BasicText +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import org.harvestcircle.designsystem.theme.HarvestCircleTheme + +public enum class HarvestCircleShellTextRole { + PageTitle, + SectionTitle, + PaneTitle, + Body, + BodyStrong, + Small, + Label, + Code, +} + +@Composable +public fun HarvestCircleShellText( + text: String, + modifier: Modifier = Modifier, + role: HarvestCircleShellTextRole = HarvestCircleShellTextRole.Body, + color: Color = HarvestCircleShellPalette.contentPrimary, + maxLines: Int = Int.MAX_VALUE, + overflow: TextOverflow = TextOverflow.Clip, +) { + val typography = HarvestCircleTheme.foundation.typography + val base = + when (role) { + HarvestCircleShellTextRole.PageTitle -> typography.pageTitle + HarvestCircleShellTextRole.SectionTitle -> typography.sectionTitle + HarvestCircleShellTextRole.PaneTitle -> typography.bodyStrong.copy(fontWeight = FontWeight.SemiBold) + HarvestCircleShellTextRole.Body -> typography.body + HarvestCircleShellTextRole.BodyStrong -> typography.bodyStrong + HarvestCircleShellTextRole.Small -> typography.bodySmall + HarvestCircleShellTextRole.Label -> typography.label + HarvestCircleShellTextRole.Code -> typography.code + } + BasicText(text, modifier, base.copy(color = color), maxLines = maxLines, overflow = overflow) +} diff --git a/app/design_system/src/commonMain/kotlin/org/harvestcircle/designsystem/shell/HarvestCircleShellVisuals.kt b/app/design_system/src/commonMain/kotlin/org/harvestcircle/designsystem/shell/HarvestCircleShellVisuals.kt @@ -0,0 +1,137 @@ +package org.harvestcircle.designsystem.shell + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import org.harvestcircle.designsystem.theme.HarvestCircleTheme + +/** Approved Studio-derived semantic palette for HarvestCircle application chrome. */ +@Immutable +public data class HarvestCircleShellColors( + public val isDark: Boolean, + public val viewportCanvas: Color, + public val applicationFrame: Color, + public val sidebar: Color, + public val pane: Color, + public val raised: Color, + public val input: Color, + public val navigationHover: Color, + public val navigationPressed: Color, + public val navigationSelected: Color, + public val divider: Color, + public val border: Color, + public val contentPrimary: Color, + public val contentSecondary: Color, + public val contentMuted: Color, + public val contentDisabled: Color, + public val accent: Color, + public val accentHover: Color, + public val accentPressed: Color, + public val accentSubtle: Color, + public val onAccent: Color, + public val shadow: Color, +) + +private val HarvestCircleShellLightColors = + HarvestCircleShellColors( + isDark = false, + viewportCanvas = Color(0xFFD9D9D7), + applicationFrame = Color(0xFFFFFFFF), + sidebar = Color(0xFFF6F6F4), + pane = Color(0xFFFFFFFF), + raised = Color(0xFFFFFFFF), + input = Color(0xFFF3F3F1), + navigationHover = Color(0xFFF0F0ED), + navigationPressed = Color(0xFFE8E8E4), + navigationSelected = Color(0xFFECECEA), + divider = Color(0xFFE5E6E9), + border = Color(0xFFE5E6E9), + contentPrimary = Color(0xFF141517), + contentSecondary = Color(0xFF626560), + contentMuted = Color(0xFF858984), + contentDisabled = Color(0xFFA7AAA6), + accent = Color(0xFF155239), + accentHover = Color(0xFF1B6547), + accentPressed = Color(0xFF10432F), + accentSubtle = Color(0xFFE6EFEA), + onAccent = Color.White, + shadow = Color(0x2E000000), + ) + +private val HarvestCircleShellDarkColors = + HarvestCircleShellColors( + isDark = true, + viewportCanvas = Color(0xFF0E100F), + applicationFrame = Color(0xFF171917), + sidebar = Color(0xFF1B1D1B), + pane = Color(0xFF1E201E), + raised = Color(0xFF242624), + input = Color(0xFF222422), + navigationHover = Color(0xFF252825), + navigationPressed = Color(0xFF2D302D), + navigationSelected = Color(0xFF292C29), + divider = Color(0xFF30332F), + border = Color(0xFF343834), + contentPrimary = Color(0xFFF1F2EF), + contentSecondary = Color(0xFFB5BAB5), + contentMuted = Color(0xFF878D87), + contentDisabled = Color(0xFF666B66), + accent = Color(0xFF155239), + accentHover = Color(0xFF1D6849), + accentPressed = Color(0xFF10432F), + accentSubtle = Color(0xFF173126), + onAccent = Color.White, + shadow = Color(0x66000000), + ) + +public fun harvestCircleShellColors(isDark: Boolean): HarvestCircleShellColors = + if (isDark) HarvestCircleShellDarkColors else HarvestCircleShellLightColors + +public val HarvestCircleShellPalette: HarvestCircleShellColors + @Composable + @ReadOnlyComposable + get() = harvestCircleShellColors(HarvestCircleTheme.foundation.colors.isDark) + +/** Fixed application-shell geometry derived from the approved Studio frames. */ +public object HarvestCircleShellMetrics { + public val topBarHeight: Dp = 48.dp + public val localHeaderHeight: Dp = 40.dp + public val structuralDividerWidth: Dp = 1.dp + public val sidebarWidth: Dp = 232.dp + public val collapsedSidebarWidth: Dp = 72.dp + public val sidebarHorizontalInset: Dp = 16.dp + public val sidebarHeaderHeight: Dp = 48.dp + public val sidebarHeaderIconTarget: Dp = 28.dp + public val sidebarHeaderIconSize: Dp = 15.dp + public val sidebarQuickActionHeight: Dp = 32.dp + public val sidebarQuickActionBottomGap: Dp = 8.dp + public val sidebarSectionHeaderHeight: Dp = 28.dp + public val sidebarSectionGap: Dp = 14.dp + public val sidebarNavigationRowHeight: Dp = 32.dp + public val sidebarNavigationGap: Dp = 2.dp + public val sidebarNavigationHorizontalPadding: Dp = 8.dp + public val sidebarNavigationIconSize: Dp = 16.dp + public val sidebarNavigationIconGap: Dp = 10.dp + public val sidebarFooterOuterInset: Dp = 12.dp + public val sidebarFooterHeight: Dp = 36.dp + public val sidebarFooterIconSize: Dp = 18.dp + public val topBarHorizontalInset: Dp = 12.dp + public val topBarControlGap: Dp = 8.dp + public val topBarSquareControlSize: Dp = 32.dp + public val topBarIconSize: Dp = 16.dp + public val topBarSearchWidth: Dp = 280.dp + public val topBarSearchCompactWidth: Dp = 176.dp + public val localHeaderHorizontalInset: Dp = 12.dp + public val localHeaderActionSize: Dp = 28.dp + public val localHeaderGap: Dp = 8.dp + public val controlRadius: Dp = 10.dp + public val navigationRadius: Dp = 8.dp + public val contentPageHorizontalInset: Dp = 24.dp + public val contentPageVerticalInset: Dp = 24.dp + public val emptyStateWidth: Dp = 344.dp + public val compactBreakpoint: Dp = 976.dp + public val expandedBreakpoint: Dp = 1272.dp +} diff --git a/app/design_system/src/commonTest/kotlin/org/harvestcircle/designsystem/shell/HarvestCircleShellVisualsTest.kt b/app/design_system/src/commonTest/kotlin/org/harvestcircle/designsystem/shell/HarvestCircleShellVisualsTest.kt @@ -0,0 +1,31 @@ +package org.harvestcircle.designsystem.shell + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals + +class HarvestCircleShellVisualsTest { + @Test + fun palettePinsApprovedStudioValues() { + val light = harvestCircleShellColors(false) + val dark = harvestCircleShellColors(true) + assertEquals(Color(0xFF155239), light.accent) + assertEquals(Color(0xFFF6F6F4), light.sidebar) + assertEquals(Color(0xFFECECEA), light.navigationSelected) + assertEquals(Color(0xFF141517), light.contentPrimary) + assertEquals(Color(0xFF155239), dark.accent) + assertNotEquals(light.applicationFrame, dark.applicationFrame) + } + + @Test + fun geometryPinsApprovedStudioGrid() { + assertEquals(48.dp, HarvestCircleShellMetrics.topBarHeight) + assertEquals(40.dp, HarvestCircleShellMetrics.localHeaderHeight) + assertEquals(232.dp, HarvestCircleShellMetrics.sidebarWidth) + assertEquals(72.dp, HarvestCircleShellMetrics.collapsedSidebarWidth) + assertEquals(976.dp, HarvestCircleShellMetrics.compactBreakpoint) + assertEquals(1272.dp, HarvestCircleShellMetrics.expandedBreakpoint) + } +}