app

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

commit 38cef450cffa4bdc9b4ebd0bc6604ceb204c3d31
parent 68a6f5812b347ec7af9872d4afe3587516206650
Author: triesap <tyson@radroots.org>
Date:   Thu, 13 Aug 2026 15:35:39 +0000

design: add the HarvestCircle component catalog

- port the complete approved desktop component showcase
- expose appearance contrast density motion input and text-scale controls
- keep visible focus enabled throughout the catalog
- replace source-product copy with owned HarvestCircle terminology

Diffstat:
Atools/design_catalog/src/commonMain/kotlin/org/harvestcircle/designcatalog/CatalogApp.kt | 563+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atools/design_catalog/src/desktopMain/kotlin/org/harvestcircle/designcatalog/Main.kt | 21+++++++++++++++++++++
2 files changed, 584 insertions(+), 0 deletions(-)

diff --git a/tools/design_catalog/src/commonMain/kotlin/org/harvestcircle/designcatalog/CatalogApp.kt b/tools/design_catalog/src/commonMain/kotlin/org/harvestcircle/designcatalog/CatalogApp.kt @@ -0,0 +1,563 @@ +@file:OptIn(androidx.compose.foundation.layout.ExperimentalLayoutApi::class) + +package org.harvestcircle.designcatalog + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import org.harvestcircle.designsystem.component.HarvestCircleButtonVariant +import org.harvestcircle.designsystem.component.HarvestCircleContentTone +import org.harvestcircle.designsystem.component.HarvestCircleControlSize +import org.harvestcircle.designsystem.component.HarvestCircleFocusRing +import org.harvestcircle.designsystem.component.HarvestCircleTextRole +import org.harvestcircle.designsystem.component.action.HarvestCircleButton +import org.harvestcircle.designsystem.component.action.HarvestCircleIconButton +import org.harvestcircle.designsystem.component.container.HarvestCircleCard +import org.harvestcircle.designsystem.component.container.HarvestCircleCardPadding +import org.harvestcircle.designsystem.component.container.HarvestCircleCardVariant +import org.harvestcircle.designsystem.component.container.HarvestCircleDialog +import org.harvestcircle.designsystem.component.container.HarvestCircleGroupBox +import org.harvestcircle.designsystem.component.feedback.HarvestCircleBanner +import org.harvestcircle.designsystem.component.feedback.HarvestCircleBannerTone +import org.harvestcircle.designsystem.component.feedback.HarvestCircleProgressIndicator +import org.harvestcircle.designsystem.component.feedback.HarvestCircleProgressKind +import org.harvestcircle.designsystem.component.feedback.HarvestCircleTooltip +import org.harvestcircle.designsystem.component.input.HarvestCircleSearchField +import org.harvestcircle.designsystem.component.input.HarvestCircleTextField +import org.harvestcircle.designsystem.component.menu.HarvestCircleMenuOption +import org.harvestcircle.designsystem.component.menu.HarvestCirclePopupButton +import org.harvestcircle.designsystem.component.navigation.HarvestCircleNavigationItem +import org.harvestcircle.designsystem.component.navigation.HarvestCircleTab +import org.harvestcircle.designsystem.component.navigation.HarvestCircleTabRow +import org.harvestcircle.designsystem.component.selection.HarvestCircleCheckbox +import org.harvestcircle.designsystem.component.selection.HarvestCircleRadioButton +import org.harvestcircle.designsystem.component.selection.HarvestCircleSwitch +import org.harvestcircle.designsystem.component.utility.HarvestCircleHorizontalDivider +import org.harvestcircle.designsystem.icon.HarvestCircleIcons +import org.harvestcircle.designsystem.layout.HarvestCircleSidebar +import org.harvestcircle.designsystem.layout.HarvestCircleSidebarSectionHeader +import org.harvestcircle.designsystem.layout.HarvestCircleToolbar +import org.harvestcircle.designsystem.primitive.HarvestCircleAppSurface +import org.harvestcircle.designsystem.primitive.HarvestCircleText +import org.harvestcircle.designsystem.theme.HarvestCircleContrast +import org.harvestcircle.designsystem.theme.HarvestCircleDensity +import org.harvestcircle.designsystem.theme.HarvestCircleDesignTheme +import org.harvestcircle.designsystem.theme.HarvestCircleInputMode +import org.harvestcircle.designsystem.theme.HarvestCircleMotionMode +import org.harvestcircle.designsystem.theme.HarvestCircleTextScale +import org.harvestcircle.designsystem.theme.HarvestCircleTheme +import org.harvestcircle.designsystem.theme.HarvestCircleThemeConfig +import org.harvestcircle.designsystem.theme.HarvestCircleThemeMode + +private enum class CatalogPage { + Overview, + Controls, + Inputs, + Navigation, +} + +@Composable +public fun HarvestCircleCatalogApp() { + var mode by remember { mutableStateOf(HarvestCircleThemeMode.System) } + var contrast by remember { mutableStateOf(HarvestCircleContrast.Standard) } + var density by remember { mutableStateOf(HarvestCircleDensity.Comfortable) } + var motion by remember { mutableStateOf(HarvestCircleMotionMode.Full) } + var inputMode by remember { mutableStateOf(HarvestCircleInputMode.Pointer) } + var textScale by remember { mutableStateOf(HarvestCircleTextScale.Standard) } + val focusRing = HarvestCircleFocusRing.WhenFocused + var page by remember { mutableStateOf(CatalogPage.Overview) } + + HarvestCircleDesignTheme( + config = + HarvestCircleThemeConfig( + mode = mode, + contrast = contrast, + density = density, + motion = motion, + inputMode = inputMode, + textScale = textScale, + ), + ) { + HarvestCircleAppSurface { + Column(Modifier.fillMaxSize()) { + HarvestCircleToolbar { + HarvestCircleText( + text = "HarvestCircle", + modifier = Modifier.weight(1f), + role = HarvestCircleTextRole.SubsectionTitle, + ) + + HarvestCirclePopupButton( + selectedValue = mode, + options = HarvestCircleThemeMode.entries.map { HarvestCircleMenuOption(it, it.name) }, + onValueChange = { mode = it }, + size = HarvestCircleControlSize.Small, + focusRing = focusRing, + ) + HarvestCirclePopupButton( + selectedValue = contrast, + options = HarvestCircleContrast.entries.map { HarvestCircleMenuOption(it, it.name) }, + onValueChange = { contrast = it }, + size = HarvestCircleControlSize.Small, + focusRing = focusRing, + ) + HarvestCirclePopupButton( + selectedValue = density, + options = HarvestCircleDensity.entries.map { HarvestCircleMenuOption(it, it.name) }, + onValueChange = { density = it }, + size = HarvestCircleControlSize.Small, + focusRing = focusRing, + ) + HarvestCirclePopupButton( + selectedValue = motion, + options = HarvestCircleMotionMode.entries.map { HarvestCircleMenuOption(it, it.name) }, + onValueChange = { motion = it }, + size = HarvestCircleControlSize.Small, + focusRing = focusRing, + ) + HarvestCirclePopupButton( + selectedValue = inputMode, + options = HarvestCircleInputMode.entries.map { HarvestCircleMenuOption(it, it.name) }, + onValueChange = { inputMode = it }, + size = HarvestCircleControlSize.Small, + focusRing = focusRing, + ) + HarvestCirclePopupButton( + selectedValue = textScale, + options = HarvestCircleTextScale.entries.map { HarvestCircleMenuOption(it, it.name) }, + onValueChange = { textScale = it }, + size = HarvestCircleControlSize.Small, + focusRing = focusRing, + ) + } + + Row(Modifier.fillMaxSize()) { + CatalogSidebar( + selected = page, + onSelected = { page = it }, + focusRing = focusRing, + ) + CatalogContent( + page = page, + focusRing = focusRing, + modifier = Modifier.weight(1f), + ) + } + } + } + } +} + +@Composable +private fun CatalogSidebar( + selected: CatalogPage, + onSelected: (CatalogPage) -> Unit, + focusRing: HarvestCircleFocusRing, +) { + HarvestCircleSidebar(modifier = Modifier.width(220.dp)) { + HarvestCircleSidebarSectionHeader("Library") + CatalogPage.entries.forEach { page -> + HarvestCircleNavigationItem( + selected = selected == page, + onClick = { onSelected(page) }, + label = page.name, + icon = + when (page) { + CatalogPage.Overview -> HarvestCircleIcons.Info + CatalogPage.Controls -> HarvestCircleIcons.Add + CatalogPage.Inputs -> HarvestCircleIcons.Search + CatalogPage.Navigation -> HarvestCircleIcons.ChevronRight + }, + focusRing = focusRing, + ) + } + } +} + +@Composable +private fun CatalogContent( + page: CatalogPage, + focusRing: HarvestCircleFocusRing, + modifier: Modifier = Modifier, +) { + LazyColumn( + modifier = modifier.fillMaxHeight(), + contentPadding = + PaddingValues( + horizontal = HarvestCircleTheme.shell.layout.pageInset, + vertical = HarvestCircleTheme.foundation.spacing.xl, + ), + verticalArrangement = Arrangement.spacedBy(HarvestCircleTheme.shell.layout.sectionGap), + ) { + item { + HarvestCircleText( + text = "Canonical macOS component system", + role = HarvestCircleTextRole.PageTitle, + ) + Spacer(Modifier.height(HarvestCircleTheme.foundation.spacing.sm)) + HarvestCircleText( + text = + "When focus rings are enabled, press blank canvas space to clear the active " + + "focus and ring.", + role = HarvestCircleTextRole.BodySmall, + tone = HarvestCircleContentTone.Secondary, + ) + Spacer(Modifier.height(HarvestCircleTheme.foundation.spacing.sm)) + HarvestCircleText( + text = + "Foundation-rendered controls using the pre-Liquid-Glass AppKit visual grammar: " + + "compact metrics, quiet borders, restrained gradients, and no Material ripple.", + role = HarvestCircleTextRole.Body, + tone = HarvestCircleContentTone.Secondary, + ) + } + + when (page) { + CatalogPage.Overview -> { + item { PaletteSection() } + item { TypographySection() } + item { FeedbackSection() } + } + + CatalogPage.Controls -> { + item { ButtonSection(focusRing) } + item { SelectionSection(focusRing) } + item { ContainerSection(focusRing) } + } + + CatalogPage.Inputs -> { + item { InputSection(focusRing) } + } + + CatalogPage.Navigation -> { + item { NavigationSection(focusRing) } + } + } + } +} + +@Composable +private fun CatalogSection( + title: String, + content: @Composable () -> Unit, +) { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(HarvestCircleTheme.foundation.spacing.md), + ) { + HarvestCircleText( + text = title, + role = HarvestCircleTextRole.SectionTitle, + ) + content() + } +} + +@Composable +private fun PaletteSection() { + CatalogSection("Semantic surfaces") { + FlowRow( + horizontalArrangement = Arrangement.spacedBy(HarvestCircleTheme.foundation.spacing.md), + verticalArrangement = Arrangement.spacedBy(HarvestCircleTheme.foundation.spacing.md), + ) { + val colors = HarvestCircleTheme.foundation.colors + listOf( + "Canvas" to colors.surface.canvas, + "Base" to colors.surface.base, + "Raised" to colors.surface.raised, + "Sunken" to colors.surface.sunken, + "Selection" to colors.surface.selected, + "Accent" to colors.action.primary.rest, + ).forEach { (name, color) -> ColorSwatch(name, color) } + } + } +} + +@Composable +private fun ColorSwatch( + name: String, + color: Color, +) { + Column( + modifier = Modifier.width(112.dp), + verticalArrangement = Arrangement.spacedBy(HarvestCircleTheme.foundation.spacing.xs), + ) { + Box( + modifier = + Modifier + .size(width = 112.dp, height = 52.dp) + .background(color, HarvestCircleTheme.foundation.shapes.card), + ) + HarvestCircleText( + text = name, + role = HarvestCircleTextRole.LabelSmall, + tone = HarvestCircleContentTone.Secondary, + ) + } +} + +@Composable +private fun TypographySection() { + CatalogSection("HarvestCircle typography") { + HarvestCircleGroupBox { + Column(verticalArrangement = Arrangement.spacedBy(HarvestCircleTheme.foundation.spacing.sm)) { + HarvestCircleText("Page title", role = HarvestCircleTextRole.PageTitle) + HarvestCircleText("Section title", role = HarvestCircleTextRole.SectionTitle) + HarvestCircleText("Subsection title", role = HarvestCircleTextRole.SubsectionTitle) + HarvestCircleText("Body text uses the owned Inter family at compact desktop sizes.") + HarvestCircleText("Secondary explanatory text", tone = HarvestCircleContentTone.Secondary) + HarvestCircleText("val project = HarvestCircle()", role = HarvestCircleTextRole.Code) + } + } + } +} + +@Composable +private fun ButtonSection(focusRing: HarvestCircleFocusRing) { + CatalogSection("Push and toolbar buttons") { + HarvestCircleGroupBox { + FlowRow( + horizontalArrangement = Arrangement.spacedBy(HarvestCircleTheme.foundation.spacing.md), + verticalArrangement = Arrangement.spacedBy(HarvestCircleTheme.foundation.spacing.md), + ) { + HarvestCircleButton(onClick = {}, focusRing = focusRing) { + HarvestCircleText("Default", role = HarvestCircleTextRole.Label, tone = HarvestCircleContentTone.Inherit) + } + HarvestCircleButton(onClick = {}, variant = HarvestCircleButtonVariant.Secondary, focusRing = focusRing) { + HarvestCircleText("Secondary", role = HarvestCircleTextRole.Label, tone = HarvestCircleContentTone.Inherit) + } + HarvestCircleButton(onClick = {}, variant = HarvestCircleButtonVariant.Ghost, focusRing = focusRing) { + HarvestCircleText("Borderless", role = HarvestCircleTextRole.Label, tone = HarvestCircleContentTone.Inherit) + } + HarvestCircleButton(onClick = {}, variant = HarvestCircleButtonVariant.Destructive, focusRing = focusRing) { + HarvestCircleText("Delete", role = HarvestCircleTextRole.Label, tone = HarvestCircleContentTone.Inherit) + } + HarvestCircleButton(onClick = {}, enabled = false, focusRing = focusRing) { + HarvestCircleText("Disabled", role = HarvestCircleTextRole.Label, tone = HarvestCircleContentTone.Inherit) + } + HarvestCircleButton(onClick = {}, loading = true, focusRing = focusRing) { + HarvestCircleText("Working", role = HarvestCircleTextRole.Label, tone = HarvestCircleContentTone.Inherit) + } + HarvestCircleTooltip("Add item") { + HarvestCircleIconButton( + onClick = {}, + icon = HarvestCircleIcons.Add, + label = "Add item", + focusRing = focusRing, + ) + } + } + } + } +} + +@Composable +private fun SelectionSection(focusRing: HarvestCircleFocusRing) { + var checked by remember { mutableStateOf(true) } + var radio by remember { mutableIntStateOf(0) } + var switched by remember { mutableStateOf(true) } + + CatalogSection("Selection controls") { + HarvestCircleGroupBox { + Column(verticalArrangement = Arrangement.spacedBy(HarvestCircleTheme.foundation.spacing.sm)) { + HarvestCircleCheckbox( + checked = checked, + onCheckedChange = { checked = it }, + label = "Enable automatic saves", + focusRing = focusRing, + ) + HarvestCircleRadioButton( + selected = radio == 0, + onClick = { radio = 0 }, + label = "Open in current window", + focusRing = focusRing, + ) + HarvestCircleRadioButton( + selected = radio == 1, + onClick = { radio = 1 }, + label = "Open in new window", + focusRing = focusRing, + ) + HarvestCircleSwitch( + checked = switched, + onCheckedChange = { switched = it }, + label = "Show inspector", + focusRing = focusRing, + ) + } + } + } +} + +@Composable +private fun InputSection(focusRing: HarvestCircleFocusRing) { + var projectName by remember { mutableStateOf("HarvestCircle") } + var search by remember { mutableStateOf("") } + + CatalogSection("Text and search fields") { + HarvestCircleGroupBox(title = "Project") { + Column(verticalArrangement = Arrangement.spacedBy(HarvestCircleTheme.foundation.spacing.lg)) { + HarvestCircleTextField( + value = projectName, + onValueChange = { projectName = it }, + modifier = Modifier.fillMaxWidth(), + label = "Name", + supportingText = "Labels remain outside the bezel; there are no floating labels.", + focusRing = focusRing, + ) + HarvestCircleSearchField( + value = search, + onValueChange = { search = it }, + placeholder = "Search", + clearLabel = "Clear search", + modifier = Modifier.fillMaxWidth(), + focusRing = focusRing, + ) + HarvestCircleTextField( + value = "Invalid value", + onValueChange = {}, + modifier = Modifier.fillMaxWidth(), + label = "Validation", + errorMessage = "Enter a valid value.", + focusRing = focusRing, + ) + } + } + } +} + +@Composable +private fun FeedbackSection() { + CatalogSection("Feedback") { + Column(verticalArrangement = Arrangement.spacedBy(HarvestCircleTheme.foundation.spacing.md)) { + HarvestCircleBanner( + title = "Classic macOS chrome", + message = "The component layer is independent of Material visual anatomy.", + ) + HarvestCircleBanner( + message = "A recoverable warning uses semantic feedback colors.", + tone = HarvestCircleBannerTone.Warning, + ) + Row( + horizontalArrangement = Arrangement.spacedBy(HarvestCircleTheme.foundation.spacing.lg), + verticalAlignment = Alignment.CenterVertically, + ) { + HarvestCircleProgressIndicator() + HarvestCircleProgressIndicator( + modifier = Modifier.width(180.dp), + kind = HarvestCircleProgressKind.Linear, + ) + } + } + } +} + +@Composable +private fun ContainerSection(focusRing: HarvestCircleFocusRing) { + var showDialog by remember { mutableStateOf(false) } + + CatalogSection("Panels and dialogs") { + FlowRow( + horizontalArrangement = Arrangement.spacedBy(HarvestCircleTheme.foundation.spacing.md), + verticalArrangement = Arrangement.spacedBy(HarvestCircleTheme.foundation.spacing.md), + ) { + HarvestCircleCard( + modifier = Modifier.width(220.dp), + variant = HarvestCircleCardVariant.Outlined, + padding = HarvestCircleCardPadding.Default, + ) { + Column(verticalArrangement = Arrangement.spacedBy(HarvestCircleTheme.foundation.spacing.sm)) { + HarvestCircleText("Group box", role = HarvestCircleTextRole.SubsectionTitle) + HarvestCircleText( + "Flat surfaces use borders before shadows.", + tone = HarvestCircleContentTone.Secondary, + ) + } + } + HarvestCircleButton( + onClick = { showDialog = true }, + variant = HarvestCircleButtonVariant.Secondary, + focusRing = focusRing, + ) { + HarvestCircleText("Show dialog", role = HarvestCircleTextRole.Label, tone = HarvestCircleContentTone.Inherit) + } + } + } + + if (showDialog) { + HarvestCircleDialog( + onDismissRequest = { showDialog = false }, + title = "Delete project?", + message = "This action cannot be undone.", + confirmLabel = "Delete", + onConfirm = { showDialog = false }, + dismissLabel = "Cancel", + onDismiss = { showDialog = false }, + destructive = true, + focusRing = focusRing, + ) + } +} + +@Composable +private fun NavigationSection(focusRing: HarvestCircleFocusRing) { + var selectedTab by remember { mutableIntStateOf(0) } + var format by remember { mutableStateOf("PNG") } + + CatalogSection("Segmented controls and popup buttons") { + HarvestCircleGroupBox { + Column(verticalArrangement = Arrangement.spacedBy(HarvestCircleTheme.foundation.spacing.lg)) { + HarvestCircleTabRow { + listOf("General", "Editor", "Advanced").forEachIndexed { index, label -> + HarvestCircleTab( + selected = selectedTab == index, + onClick = { selectedTab = index }, + label = label, + focusRing = focusRing, + ) + } + } + + HarvestCirclePopupButton( + selectedValue = format, + options = + listOf( + HarvestCircleMenuOption("PNG", "PNG Image"), + HarvestCircleMenuOption("JPG", "JPEG Image"), + HarvestCircleMenuOption("SVG", "SVG Vector"), + ), + onValueChange = { format = it }, + focusRing = focusRing, + ) + } + } + HarvestCircleHorizontalDivider() + HarvestCircleText( + text = "Selected tab: $selectedTab ยท Format: $format", + tone = HarvestCircleContentTone.Secondary, + ) + } +} diff --git a/tools/design_catalog/src/desktopMain/kotlin/org/harvestcircle/designcatalog/Main.kt b/tools/design_catalog/src/desktopMain/kotlin/org/harvestcircle/designcatalog/Main.kt @@ -0,0 +1,21 @@ +package org.harvestcircle.designcatalog + +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.application +import androidx.compose.ui.window.rememberWindowState + +public fun main(): Unit = + application { + Window( + onCloseRequest = ::exitApplication, + title = "HarvestCircle UI Catalog", + state = + rememberWindowState( + width = 1180.dp, + height = 820.dp, + ), + ) { + HarvestCircleCatalogApp() + } + }