add SettingsNav and SettingsRow components

This commit is contained in:
Justin Edmund 2025-12-14 01:23:47 -08:00
parent 8e57cdc2a5
commit c785d1d0ab
2 changed files with 199 additions and 0 deletions

View file

@ -0,0 +1,132 @@
<svelte:options runes={true} />
<script lang="ts">
import { RadioGroup as RadioGroupPrimitive } from 'bits-ui'
export type ElementType = 'wind' | 'fire' | 'water' | 'earth' | 'dark' | 'light'
interface NavItem {
value: string
label: string
}
interface Props {
value?: string
onValueChange?: (value: string) => void
element?: ElementType
items: NavItem[]
}
let { value = $bindable(), onValueChange, element = 'water', items }: Props = $props()
// Track previous value to only fire callback on actual changes
let previousValue = $state<string | undefined>(undefined)
$effect(() => {
if (onValueChange && value !== undefined) {
if (previousValue !== undefined && value !== previousValue) {
onValueChange(value)
}
previousValue = value
}
})
</script>
<nav class="settings-nav element-{element}">
<RadioGroupPrimitive.Root bind:value class="nav-list">
{#each items as item}
<RadioGroupPrimitive.Item value={item.value} class="nav-item">
{item.label}
</RadioGroupPrimitive.Item>
{/each}
</RadioGroupPrimitive.Root>
</nav>
<style lang="scss">
@use '$src/themes/spacing' as spacing;
@use '$src/themes/colors' as colors;
@use '$src/themes/typography' as typography;
@use '$src/themes/layout' as layout;
@use '$src/themes/effects' as effects;
.settings-nav {
display: flex;
flex-direction: column;
width: 160px;
flex-shrink: 0;
}
:global(.nav-list) {
display: flex;
flex-direction: column;
gap: spacing.$unit-half;
}
:global(.nav-item) {
display: flex;
align-items: center;
padding: spacing.$unit spacing.$unit-2x;
border: none;
border-radius: layout.$item-corner;
background: transparent;
color: var(--text-secondary);
font-size: typography.$font-regular;
font-weight: 500;
cursor: pointer;
text-align: left;
@include effects.smooth-transition(effects.$duration-quick, background-color, color);
&:hover:not([data-state='checked']) {
background: var(--button-contained-bg-hover);
color: var(--text-primary);
}
&:focus-visible {
outline: 2px solid var(--focus-ring);
outline-offset: 2px;
}
}
// Element-specific styles for checked state
.element-wind {
:global(.nav-item[data-state='checked']) {
background: colors.$wind-bg-10;
color: white;
}
}
.element-fire {
:global(.nav-item[data-state='checked']) {
background: colors.$fire-bg-10;
color: white;
}
}
.element-water {
:global(.nav-item[data-state='checked']) {
background: colors.$water-bg-10;
color: white;
}
}
.element-earth {
:global(.nav-item[data-state='checked']) {
background: colors.$earth-bg-10;
color: white;
}
}
.element-dark {
:global(.nav-item[data-state='checked']) {
background: colors.$dark-bg-10;
color: white;
}
}
.element-light {
:global(.nav-item[data-state='checked']) {
background: colors.$light-bg-10;
color: white;
}
}
</style>

View file

@ -0,0 +1,67 @@
<svelte:options runes={true} />
<script lang="ts">
import type { Snippet } from 'svelte'
interface Props {
title: string
subtitle?: string
control: Snippet
}
let { title, subtitle, control }: Props = $props()
</script>
<div class="settings-row">
<div class="text">
<span class="title">{title}</span>
{#if subtitle}
<p class="subtitle">{subtitle}</p>
{/if}
</div>
<div class="control">
{@render control()}
</div>
</div>
<style lang="scss">
@use '$src/themes/spacing' as spacing;
@use '$src/themes/typography' as typography;
.settings-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: spacing.$unit-2x;
.text {
display: flex;
flex-direction: column;
gap: spacing.$unit-half;
.title {
font-size: typography.$font-regular;
color: var(--text-primary);
}
.subtitle {
margin: 0;
font-size: typography.$font-small;
color: var(--text-secondary);
}
}
.control {
flex-shrink: 0;
width: 160px;
display: flex;
justify-content: flex-end;
// Make select triggers and fieldsets fill the control width
:global([data-select-trigger]),
:global(.fieldset) {
width: 100%;
}
}
}
</style>