Skip to content

Commit fb659f9

Browse files
committed
feat(sample): reorganize gallery sidebar with SystemIcon and disclosure groups
1 parent 322db0f commit fb659f9

4 files changed

Lines changed: 193 additions & 146 deletions

File tree

macosui/src/commonMain/kotlin/io/github/kdroidfilter/nucleus/ui/apple/macos/components/Sidebar.kt

Lines changed: 14 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ import androidx.compose.ui.graphics.Brush
3939
import androidx.compose.ui.graphics.CompositingStrategy
4040
import androidx.compose.ui.graphics.Color
4141
import androidx.compose.ui.graphics.graphicsLayer
42-
import androidx.compose.ui.graphics.vector.ImageVector
4342
import androidx.compose.ui.semantics.Role
4443
import androidx.compose.ui.text.style.TextOverflow
4544
import androidx.compose.ui.unit.Dp
@@ -153,40 +152,29 @@ private fun <T> sidebarSpring() = spring<T>(
153152
*
154153
* @param label Display text for the item.
155154
* @param onClick Callback invoked when this item is clicked.
156-
* @param icon Optional Lucide-style [ImageVector] rendered before the label.
157-
* @param group Optional group header text. When any item has a non-null group,
158-
* items are rendered in groups with a header label above each group.
159-
* @param id Unique identifier used for active-item matching. Defaults to [label].
160-
*/
161-
/**
162-
* Data for a single sidebar navigation item.
163-
*
164-
* [onClick] is intentionally excluded from [equals]/[hashCode] to keep
165-
* [SidebarItem] stable for Compose's skipping mechanism (lambdas don't
166-
* implement structural equality).
167-
*
168-
* @param label Display text for the item.
169-
* @param onClick Callback invoked when this item is clicked.
170-
* @param icon Optional Lucide-style [ImageVector] rendered before the label.
155+
* @param icon Optional [SystemIcon] rendered before the label. Supports SF Symbols on Apple platforms.
171156
* @param group Optional group header text. When any item has a non-null group,
172157
* items are rendered in groups with a header label above each group.
173158
* @param id Unique identifier used for active-item matching. Defaults to [label].
174159
* @param children Child items displayed in a collapsible disclosure section.
175160
* When non-empty, a disclosure chevron is shown that toggles visibility of children.
161+
* @param initiallyExpanded Whether the disclosure section starts expanded. Defaults to `true`.
176162
*/
177163
class SidebarItem(
178164
val label: String,
179165
val onClick: () -> Unit,
180-
val icon: ImageVector? = null,
166+
val icon: SystemIcon? = null,
181167
val group: String? = null,
182168
val id: String = label,
183169
val children: List<SidebarItem> = emptyList(),
170+
val initiallyExpanded: Boolean = true,
184171
) {
185172
override fun equals(other: Any?): Boolean {
186173
if (this === other) return true
187174
if (other !is SidebarItem) return false
188175
return id == other.id && label == other.label && icon == other.icon
189176
&& group == other.group && children == other.children
177+
&& initiallyExpanded == other.initiallyExpanded
190178
}
191179

192180
override fun hashCode(): Int {
@@ -195,6 +183,7 @@ class SidebarItem(
195183
result = 31 * result + (group?.hashCode() ?: 0)
196184
result = 31 * result + id.hashCode()
197185
result = 31 * result + children.hashCode()
186+
result = 31 * result + initiallyExpanded.hashCode()
198187
return result
199188
}
200189
}
@@ -680,7 +669,7 @@ private fun SidebarItemWithVisibility(
680669
label = item.label,
681670
onClick = item.onClick,
682671
active = activeItem == item.id,
683-
icon = item.icon?.let { SystemIcon(it) },
672+
icon = item.icon,
684673
isCollapsed = collapsed,
685674
controlSize = controlSize,
686675
sidebarMetrics = sidebarMetrics,
@@ -732,7 +721,7 @@ private fun DisclosureItem(
732721
itemColors: SidebarItemColors,
733722
indentLevel: Int = 0,
734723
) {
735-
var expanded by remember { mutableStateOf(true) }
724+
var expanded by remember { mutableStateOf(item.initiallyExpanded) }
736725
val chevronRotation by animateFloatAsState(
737726
targetValue = if (expanded) 90f else 0f,
738727
animationSpec = sidebarSpring(),
@@ -783,13 +772,16 @@ private fun DisclosureItem(
783772
)
784773
}
785774

786-
// Item content
775+
// Item content — clicking the row also toggles disclosure
787776
Box(modifier = Modifier.weight(1f)) {
788777
SidebarItemRow(
789778
label = item.label,
790-
onClick = item.onClick,
779+
onClick = {
780+
expanded = !expanded
781+
item.onClick()
782+
},
791783
active = activeItem == item.id,
792-
icon = item.icon?.let { SystemIcon(it) },
784+
icon = item.icon,
793785
isCollapsed = collapsed,
794786
controlSize = controlSize,
795787
sidebarMetrics = sidebarMetrics,

sample/src/commonMain/kotlin/io/github/kdroidfilter/nucleus/ui/apple/macos/sample/App.kt

Lines changed: 131 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ import androidx.compose.runtime.setValue
3434
import androidx.compose.ui.Alignment
3535
import androidx.compose.ui.Modifier
3636
import androidx.compose.ui.draw.clip
37-
import androidx.compose.ui.graphics.vector.ImageVector
3837
import androidx.compose.ui.platform.LocalUriHandler
3938
import androidx.compose.ui.text.font.FontWeight
4039
import androidx.compose.ui.unit.dp
@@ -58,8 +57,18 @@ import com.composables.icons.lucide.MessageCircle
5857
import com.composables.icons.lucide.MessageSquare
5958
import com.composables.icons.lucide.MousePointerClick
6059
import com.composables.icons.lucide.PanelTopOpen
61-
import com.composables.icons.lucide.Scan
60+
import com.composables.icons.lucide.ArrowUpDown
61+
import com.composables.icons.lucide.Box
62+
import com.composables.icons.lucide.ChevronDown
63+
import com.composables.icons.lucide.Component
64+
import com.composables.icons.lucide.Navigation
65+
import com.composables.icons.lucide.Paintbrush
66+
import com.composables.icons.lucide.Palette
67+
import com.composables.icons.lucide.RectangleHorizontal
6268
import com.composables.icons.lucide.Search
69+
import com.composables.icons.lucide.ShieldAlert
70+
import com.composables.icons.lucide.SquareMousePointer
71+
import com.composables.icons.lucide.Star
6372
import com.composables.icons.lucide.Ruler
6473
import com.composables.icons.lucide.SlidersHorizontal
6574
import com.composables.icons.lucide.SquareCheck
@@ -95,11 +104,11 @@ import io.github.kdroidfilter.nucleus.ui.apple.macos.components.VerticalScrollba
95104
import io.github.kdroidfilter.nucleus.ui.apple.macos.components.rememberScrollbarState
96105
import io.github.kdroidfilter.nucleus.ui.apple.macos.components.rememberToastState
97106
import io.github.kdroidfilter.nucleus.ui.apple.macos.icons.Icon
98-
import io.github.kdroidfilter.nucleus.ui.apple.macos.icons.LucideHome
107+
import io.github.kdroidfilter.nucleus.ui.apple.macos.icons.Icons
99108
import io.github.kdroidfilter.nucleus.ui.apple.macos.icons.LucideMoon
100109
import io.github.kdroidfilter.nucleus.ui.apple.macos.icons.LucideSettings
101110
import io.github.kdroidfilter.nucleus.ui.apple.macos.icons.LucideSun
102-
import io.github.kdroidfilter.nucleus.ui.apple.macos.icons.RadixPanelLeft
111+
import io.github.kdroidfilter.nucleus.ui.apple.macos.icons.SystemIcon
103112
import io.github.kdroidfilter.nucleus.ui.apple.macos.sample.pages.AccordionPage
104113
import io.github.kdroidfilter.nucleus.ui.apple.macos.sample.pages.AddressBarPage
105114
import io.github.kdroidfilter.nucleus.ui.apple.macos.sample.pages.AlertPage
@@ -158,59 +167,126 @@ import io.github.kdroidfilter.nucleus.ui.apple.macos.theme.vibrant
158167
internal enum class ThemeMode { System, Light, Dark }
159168

160169
// Navigation data
161-
internal data class SidebarEntryDef(val id: String, val label: String, val group: String, val icon: ImageVector)
170+
internal data class SidebarEntryDef(val id: String, val label: String, val group: String, val icon: SystemIcon)
162171

163172
internal val sidebarEntryDefs = listOf(
164-
SidebarEntryDef("home", "Home", "GENERAL", LucideHome),
165-
SidebarEntryDef("getting-started", "Getting Started", "GENERAL", Lucide.LayoutList),
166-
SidebarEntryDef("license", "License", "GENERAL", Lucide.Tag),
167-
SidebarEntryDef("button", "Button", "FORM CONTROLS", Lucide.MousePointerClick),
168-
SidebarEntryDef("iconbutton", "Icon Button", "FORM CONTROLS", Lucide.CircleDot),
169-
SidebarEntryDef("input", "Input", "FORM CONTROLS", Lucide.TextCursorInput),
170-
SidebarEntryDef("textarea", "Textarea", "FORM CONTROLS", Lucide.TextAlignStart),
171-
SidebarEntryDef("checkbox", "Checkbox", "FORM CONTROLS", Lucide.SquareCheck),
172-
SidebarEntryDef("radiobutton", "Radio Button", "FORM CONTROLS", Lucide.CircleDot),
173-
SidebarEntryDef("switch", "Switch", "FORM CONTROLS", Lucide.ToggleLeft),
174-
SidebarEntryDef("combobox", "Combo Box", "FORM CONTROLS", Lucide.ChevronsUpDown),
175-
SidebarEntryDef("multiselect", "Multi Select", "FORM CONTROLS", Lucide.ListChecks),
176-
SidebarEntryDef("searchinput", "Search Input", "FORM CONTROLS", Lucide.Search),
177-
SidebarEntryDef("slider", "Slider", "FORM CONTROLS", Lucide.SlidersHorizontal),
178-
SidebarEntryDef("circularslider", "Circular Slider", "FORM CONTROLS", Lucide.Loader),
179-
SidebarEntryDef("stepper", "Stepper", "FORM CONTROLS", Lucide.ChevronsUpDown),
180-
SidebarEntryDef("popupbutton", "Pop-up Button", "FORM CONTROLS", Lucide.ChevronsUpDown),
181-
SidebarEntryDef("datepicker", "Date Picker", "FORM CONTROLS", Lucide.Calendar),
182-
SidebarEntryDef("colorwell", "Color Wells & Pickers", "FORM CONTROLS", Lucide.Scan),
183-
SidebarEntryDef("scrollbar", "Scrollbar", "DATA DISPLAY", Lucide.GripVertical),
184-
SidebarEntryDef("groupbox", "Group Box", "DATA DISPLAY", Lucide.SquareDashed),
185-
SidebarEntryDef("groupedlist", "Grouped List", "DATA DISPLAY", Lucide.ListChecks),
186-
SidebarEntryDef("form", "Form", "DATA DISPLAY", Lucide.LayoutList),
187-
SidebarEntryDef("badge", "Badge", "DATA DISPLAY", Lucide.Tag),
188-
SidebarEntryDef("avatar", "Avatar", "DATA DISPLAY", Lucide.CircleUser),
189-
SidebarEntryDef("surface", "Surface", "DATA DISPLAY", Lucide.Layers),
190-
SidebarEntryDef("card", "Card", "DATA DISPLAY", Lucide.CreditCard),
191-
SidebarEntryDef("table", "Table", "DATA DISPLAY", Lucide.Table),
192-
SidebarEntryDef("pagecontrol", "Page Control", "DATA DISPLAY", Lucide.CircleDot),
193-
SidebarEntryDef("progress", "Progress", "DATA DISPLAY", Lucide.Loader),
194-
SidebarEntryDef("skeleton", "Skeleton", "DATA DISPLAY", Lucide.Scan),
195-
SidebarEntryDef("alert", "Alert", "FEEDBACK", Lucide.TriangleAlert),
196-
SidebarEntryDef("toast", "Toast", "FEEDBACK", Lucide.Bell),
197-
SidebarEntryDef("dialog", "Dialog", "OVERLAYS", Lucide.MessageSquare),
198-
SidebarEntryDef("tooltip", "Tooltip", "OVERLAYS", Lucide.MessageCircle),
199-
SidebarEntryDef("popover", "Popover", "OVERLAYS", Lucide.PanelTopOpen),
200-
SidebarEntryDef("dropdown", "Dropdown Menu", "OVERLAYS", Lucide.Menu),
201-
SidebarEntryDef("contextmenu", "Context Menu", "OVERLAYS", Lucide.Ellipsis),
202-
SidebarEntryDef("tabs", "Tabs", "NAVIGATION", Lucide.Columns3),
203-
SidebarEntryDef("accordion", "Accordion", "NAVIGATION", Lucide.ChevronsUpDown),
204-
SidebarEntryDef("sidebar", "Sidebar", "NAVIGATION", RadixPanelLeft),
205-
SidebarEntryDef("segmentedcontrol", "Segmented Control", "FORM CONTROLS", Lucide.Columns3),
206-
SidebarEntryDef("titlebar", "Title Bar", "NAVIGATION", Lucide.PanelTopOpen),
207-
SidebarEntryDef("addressbar", "Address Bar", "NAVIGATION", Lucide.Search),
208-
SidebarEntryDef("scaffold", "Scaffold", "NAVIGATION", RadixPanelLeft),
209-
SidebarEntryDef("icons", "Icons", "THEME", Lucide.Scan),
210-
SidebarEntryDef("material", "Material", "THEME", Lucide.Scan),
211-
SidebarEntryDef("controlsize", "Control Size", "THEME", Lucide.SlidersHorizontal),
173+
// General
174+
SidebarEntryDef("home", "Home", "GENERAL", Icons.Home),
175+
SidebarEntryDef("getting-started", "Getting Started", "GENERAL", SystemIcon("list.bullet.rectangle", Lucide.LayoutList)),
176+
SidebarEntryDef("license", "License", "GENERAL", Icons.Tag),
177+
178+
// Controls — Buttons
179+
SidebarEntryDef("button", "Button", "CONTROLS", SystemIcon("cursorarrow.click", Lucide.MousePointerClick)),
180+
SidebarEntryDef("iconbutton", "Icon Button", "CONTROLS", SystemIcon("square.and.arrow.up.on.square", Lucide.SquareMousePointer)),
181+
182+
// Controls — Text input
183+
SidebarEntryDef("input", "Input", "CONTROLS", SystemIcon("character.cursor.ibeam", Lucide.TextCursorInput)),
184+
SidebarEntryDef("textarea", "Textarea", "CONTROLS", SystemIcon("text.alignleft", Lucide.TextAlignStart)),
185+
SidebarEntryDef("searchinput", "Search Input", "CONTROLS", Icons.Search),
186+
187+
// Controls — Selection
188+
SidebarEntryDef("checkbox", "Checkbox", "CONTROLS", SystemIcon("checkmark.square", Lucide.SquareCheck)),
189+
SidebarEntryDef("radiobutton", "Radio Button", "CONTROLS", SystemIcon("circle.inset.filled", Lucide.CircleDot)),
190+
SidebarEntryDef("switch", "Switch", "CONTROLS", SystemIcon("switch.2", Lucide.ToggleLeft)),
191+
SidebarEntryDef("segmentedcontrol", "Segmented Control", "CONTROLS", SystemIcon("rectangle.split.3x1", Lucide.Columns3)),
192+
SidebarEntryDef("combobox", "Combo Box", "CONTROLS", Icons.ChevronsUpDown),
193+
SidebarEntryDef("popupbutton", "Pop-up Button", "CONTROLS", Icons.ChevronDown),
194+
SidebarEntryDef("multiselect", "Multi Select", "CONTROLS", SystemIcon("checklist", Lucide.ListChecks)),
195+
196+
// Controls — Value input
197+
SidebarEntryDef("slider", "Slider", "CONTROLS", SystemIcon("slider.horizontal.3", Lucide.SlidersHorizontal)),
198+
SidebarEntryDef("circularslider", "Circular Slider", "CONTROLS", SystemIcon("dial.min", Lucide.Loader)),
199+
SidebarEntryDef("stepper", "Stepper", "CONTROLS", SystemIcon("plusminus", Lucide.ArrowUpDown)),
200+
SidebarEntryDef("datepicker", "Date Picker", "CONTROLS", Icons.Calendar),
201+
SidebarEntryDef("colorwell", "Color Wells & Pickers", "CONTROLS", SystemIcon("paintpalette", Lucide.Palette)),
202+
203+
// Layout & Containers
204+
SidebarEntryDef("form", "Form", "LAYOUT", SystemIcon("list.bullet.rectangle", Lucide.LayoutList)),
205+
SidebarEntryDef("groupbox", "Group Box", "LAYOUT", SystemIcon("rectangle.dashed", Lucide.SquareDashed)),
206+
SidebarEntryDef("card", "Card", "LAYOUT", SystemIcon("rectangle.on.rectangle", Lucide.CreditCard)),
207+
SidebarEntryDef("surface", "Surface", "LAYOUT", SystemIcon("square.stack", Lucide.Layers)),
208+
SidebarEntryDef("accordion", "Accordion", "LAYOUT", Icons.ChevronsUpDown),
209+
SidebarEntryDef("scaffold", "Scaffold", "LAYOUT", Icons.PanelLeft),
210+
211+
// Collections
212+
SidebarEntryDef("groupedlist", "Grouped List", "COLLECTIONS", SystemIcon("checklist", Lucide.ListChecks)),
213+
SidebarEntryDef("table", "Table", "COLLECTIONS", SystemIcon("tablecells", Lucide.Table)),
214+
215+
// Navigation
216+
SidebarEntryDef("tabs", "Tabs", "NAVIGATION", SystemIcon("rectangle.split.3x1", Lucide.Columns3)),
217+
SidebarEntryDef("sidebar", "Sidebar", "NAVIGATION", Icons.PanelLeft),
218+
SidebarEntryDef("titlebar", "Title Bar", "NAVIGATION", SystemIcon("macwindow", Lucide.PanelTopOpen)),
219+
SidebarEntryDef("addressbar", "Address Bar", "NAVIGATION", Icons.Search),
220+
SidebarEntryDef("pagecontrol", "Page Control", "NAVIGATION", SystemIcon("circle.inset.filled", Lucide.CircleDot)),
221+
SidebarEntryDef("scrollbar", "Scrollbar", "NAVIGATION", SystemIcon("arrow.up.and.down", Lucide.GripVertical)),
222+
223+
// Status & Feedback
224+
SidebarEntryDef("badge", "Badge", "STATUS & FEEDBACK", Icons.Tag),
225+
SidebarEntryDef("avatar", "Avatar", "STATUS & FEEDBACK", SystemIcon("person.circle", Lucide.CircleUser)),
226+
SidebarEntryDef("progress", "Progress", "STATUS & FEEDBACK", SystemIcon("progress.indicator", Lucide.Loader)),
227+
SidebarEntryDef("skeleton", "Skeleton", "STATUS & FEEDBACK", SystemIcon("rectangle.fill", Lucide.RectangleHorizontal)),
228+
SidebarEntryDef("alert", "Alert", "STATUS & FEEDBACK", Icons.TriangleAlert),
229+
SidebarEntryDef("toast", "Toast", "STATUS & FEEDBACK", SystemIcon("bell", Lucide.Bell)),
230+
231+
// Overlays
232+
SidebarEntryDef("dialog", "Dialog", "OVERLAYS", SystemIcon("bubble.left.and.bubble.right", Lucide.MessageSquare)),
233+
SidebarEntryDef("popover", "Popover", "OVERLAYS", SystemIcon("macwindow", Lucide.PanelTopOpen)),
234+
SidebarEntryDef("tooltip", "Tooltip", "OVERLAYS", SystemIcon("text.bubble", Lucide.MessageCircle)),
235+
SidebarEntryDef("dropdown", "Dropdown Menu", "OVERLAYS", SystemIcon("line.3.horizontal", Lucide.Menu)),
236+
SidebarEntryDef("contextmenu", "Context Menu", "OVERLAYS", Icons.Ellipsis),
237+
238+
// Theme & Design
239+
SidebarEntryDef("icons", "Icons", "THEME", Icons.Star),
240+
SidebarEntryDef("material", "Material", "THEME", SystemIcon("paintpalette", Lucide.Palette)),
241+
SidebarEntryDef("controlsize", "Control Size", "THEME", SystemIcon("slider.horizontal.3", Lucide.SlidersHorizontal)),
212242
)
213243

244+
private fun buildSidebarItems(onNavigate: (String) -> Unit): List<SidebarItem> {
245+
fun entry(id: String) = sidebarEntryDefs.first { it.id == id }
246+
fun leaf(id: String) = entry(id).let { SidebarItem(it.label, onClick = { onNavigate(it.id) }, icon = it.icon, id = it.id) }
247+
248+
fun group(label: String, icon: SystemIcon, ids: List<String>) = SidebarItem(
249+
label = label,
250+
onClick = {},
251+
icon = icon,
252+
children = ids.map { leaf(it) },
253+
initiallyExpanded = false,
254+
)
255+
256+
return listOf(
257+
leaf("home"),
258+
leaf("getting-started"),
259+
leaf("license"),
260+
261+
group("Controls", SystemIcon("slider.horizontal.below.rectangle", Lucide.Component), listOf(
262+
"button", "iconbutton",
263+
"input", "textarea", "searchinput",
264+
"checkbox", "radiobutton", "switch", "segmentedcontrol",
265+
"combobox", "popupbutton", "multiselect",
266+
"slider", "circularslider", "stepper",
267+
"datepicker", "colorwell",
268+
)),
269+
group("Layout", SystemIcon("rectangle.3.group", Lucide.Box), listOf(
270+
"form", "groupbox", "card", "surface", "accordion", "scaffold",
271+
)),
272+
group("Collections", Icons.Folder, listOf(
273+
"groupedlist", "table",
274+
)),
275+
group("Navigation", SystemIcon("arrow.triangle.turn.up.right.diamond", Lucide.Navigation), listOf(
276+
"tabs", "sidebar", "titlebar", "addressbar", "pagecontrol", "scrollbar",
277+
)),
278+
group("Status & Feedback", SystemIcon("bell.badge", Lucide.ShieldAlert), listOf(
279+
"badge", "avatar", "progress", "skeleton", "alert", "toast",
280+
)),
281+
group("Overlays", SystemIcon("bubble.left.and.bubble.right", Lucide.MessageSquare), listOf(
282+
"dialog", "popover", "tooltip", "dropdown", "contextmenu",
283+
)),
284+
group("Theme", SystemIcon("paintbrush", Lucide.Paintbrush), listOf(
285+
"icons", "material", "controlsize",
286+
)),
287+
)
288+
}
289+
214290
@Composable
215291
fun App() {
216292
var themeMode by remember { mutableStateOf(ThemeMode.System) }
@@ -272,14 +348,8 @@ fun App() {
272348
// Navigation helpers
273349
val currentPageLabel = sidebarEntryDefs.firstOrNull { it.id == nav.currentPageId }?.label ?: ""
274350

275-
val sidebarItems = sidebarEntryDefs.map { def ->
276-
SidebarItem(
277-
label = def.label,
278-
onClick = { nav.navigateTo(def.id) },
279-
icon = def.icon,
280-
group = def.group,
281-
id = def.id,
282-
)
351+
val sidebarItems = remember {
352+
buildSidebarItems { id -> nav.navigateTo(id) }
283353
}
284354

285355
BoxWithConstraints(modifier = Modifier.fillMaxSize()) {

0 commit comments

Comments
 (0)