ui: update Input, Select, Button, Sidebar components

This commit is contained in:
Justin Edmund 2025-11-30 20:05:42 -08:00
parent 5df563198b
commit f815ca4f30
5 changed files with 108 additions and 86 deletions

View file

@ -8,7 +8,15 @@
interface Props { interface Props {
/** Button variant style */ /** Button variant style */
variant?: 'primary' | 'secondary' | 'ghost' | 'text' | 'destructive' | 'notice' | 'subtle' | undefined variant?:
| 'primary'
| 'secondary'
| 'ghost'
| 'text'
| 'destructive'
| 'notice'
| 'subtle'
| undefined
/** Button size */ /** Button size */
size?: 'small' | 'medium' | 'large' | 'icon' | undefined size?: 'small' | 'medium' | 'large' | 'icon' | undefined
/** Whether button is contained */ /** Whether button is contained */
@ -119,10 +127,10 @@
{@render leftAccessory()} {@render leftAccessory()}
</span> </span>
{:else if hasLeftIcon && !iconOnly && icon} {:else if hasLeftIcon && !iconOnly && icon}
<span class="accessory"> <span class="accessory">
<Icon name={icon} size={iconSizes[size]} /> <Icon name={icon} size={iconSizes[size]} />
</span> </span>
{/if} {/if}
{#if children && !iconOnly} {#if children && !iconOnly}
<span class="text"> <span class="text">
@ -137,10 +145,10 @@
{@render rightAccessory()} {@render rightAccessory()}
</span> </span>
{:else if hasRightIcon && !iconOnly && icon} {:else if hasRightIcon && !iconOnly && icon}
<span class="accessory"> <span class="accessory">
<Icon name={icon} size={iconSizes[size]} /> <Icon name={icon} size={iconSizes[size]} />
</span> </span>
{/if} {/if}
</ButtonPrimitive.Root> </ButtonPrimitive.Root>
<style lang="scss"> <style lang="scss">
@ -305,7 +313,7 @@
// Sizes // Sizes
:global([data-button-root].small) { :global([data-button-root].small) {
padding: $unit $unit-2x; padding: $unit calc($unit * 1.5);
font-size: $font-small; font-size: $font-small;
min-height: calc($unit * 3.5); min-height: calc($unit * 3.5);
} }
@ -417,6 +425,7 @@
&:hover:not(:disabled) { &:hover:not(:disabled) {
background: var(--wind-bg-hover); background: var(--wind-bg-hover);
color: white;
} }
} }
@ -426,6 +435,7 @@
&:hover:not(:disabled) { &:hover:not(:disabled) {
background: var(--fire-bg-hover); background: var(--fire-bg-hover);
color: white;
} }
} }
@ -435,6 +445,7 @@
&:hover:not(:disabled) { &:hover:not(:disabled) {
background: var(--water-bg-hover); background: var(--water-bg-hover);
color: white;
} }
} }
@ -444,6 +455,7 @@
&:hover:not(:disabled) { &:hover:not(:disabled) {
background: var(--earth-bg-hover); background: var(--earth-bg-hover);
color: white;
} }
} }
@ -453,6 +465,7 @@
&:hover:not(:disabled) { &:hover:not(:disabled) {
background: var(--dark-bg-hover); background: var(--dark-bg-hover);
color: white;
} }
} }
@ -462,6 +475,7 @@
&:hover:not(:disabled) { &:hover:not(:disabled) {
background: var(--light-bg-hover); background: var(--light-bg-hover);
color: white;
} }
} }
@ -472,6 +486,7 @@
&:hover:not(:disabled) { &:hover:not(:disabled) {
background: var(--wind-bg-hover); background: var(--wind-bg-hover);
color: var(--wind-text-contrast);
} }
} }
@ -481,6 +496,7 @@
&:hover:not(:disabled) { &:hover:not(:disabled) {
background: var(--fire-bg-hover); background: var(--fire-bg-hover);
color: var(--fire-text-contrast);
} }
} }
@ -490,6 +506,7 @@
&:hover:not(:disabled) { &:hover:not(:disabled) {
background: var(--water-bg-hover); background: var(--water-bg-hover);
color: var(--water-text-contrast);
} }
} }
@ -499,6 +516,7 @@
&:hover:not(:disabled) { &:hover:not(:disabled) {
background: var(--earth-bg-hover); background: var(--earth-bg-hover);
color: var(--earth-text-contrast);
} }
} }
@ -508,6 +526,7 @@
&:hover:not(:disabled) { &:hover:not(:disabled) {
background: var(--dark-bg-hover); background: var(--dark-bg-hover);
color: var(--dark-text-contrast);
} }
} }
@ -517,6 +536,7 @@
&:hover:not(:disabled) { &:hover:not(:disabled) {
background: var(--light-bg-hover); background: var(--light-bg-hover);
color: var(--light-text-contrast);
} }
} }
</style> </style>

View file

@ -112,16 +112,16 @@
</div> </div>
{:else} {:else}
<input <input
bind:value bind:value
class={inputClasses} class={inputClasses}
{type} {type}
{placeholder} {placeholder}
{disabled} {disabled}
{readonly} {readonly}
{required} {required}
maxlength={maxLength} maxlength={maxLength}
{...restProps} {...restProps}
/> />
{/if} {/if}
{#if error} {#if error}
@ -161,16 +161,16 @@
</div> </div>
{:else} {:else}
<input <input
bind:value bind:value
class={inputClasses} class={inputClasses}
{type} {type}
{placeholder} {placeholder}
{disabled} {disabled}
{readonly} {readonly}
{required} {required}
maxlength={maxLength} maxlength={maxLength}
{...restProps} {...restProps}
/> />
{/if} {/if}
<style lang="scss"> <style lang="scss">
@ -235,7 +235,7 @@
@include smooth-transition($duration-quick, background-color); @include smooth-transition($duration-quick, background-color);
&:not(.wrapper) { &:not(.wrapper) {
padding: calc($unit * 1.5) $unit-2x; padding: calc($unit * 1.25) $unit-2x;
} }
&.fullHeight { &.fullHeight {

View file

@ -289,6 +289,7 @@
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
font-weight: $normal;
} }
.image { .image {

View file

@ -2,6 +2,7 @@
<script lang="ts"> <script lang="ts">
import SidebarHeader from './SidebarHeader.svelte' import SidebarHeader from './SidebarHeader.svelte'
import Button from './Button.svelte'
import { SIDEBAR_WIDTH } from '$lib/stores/sidebar.svelte' import { SIDEBAR_WIDTH } from '$lib/stores/sidebar.svelte'
import type { Snippet } from 'svelte' import type { Snippet } from 'svelte'
@ -14,23 +15,59 @@
onClose?: () => void onClose?: () => void
/** Callback when close is requested (lowercase, deprecated - use onClose) */ /** Callback when close is requested (lowercase, deprecated - use onClose) */
onclose?: () => void onclose?: () => void
/** Callback when back is requested (shows arrow instead of X) */
onback?: () => void
/** Callback when save/done is requested */
onsave?: () => void
/** Label for the save button */
saveLabel?: string
/** Element for styling the save button */
element?: 'wind' | 'fire' | 'water' | 'earth' | 'dark' | 'light'
/** Content to render in the sidebar */ /** Content to render in the sidebar */
children?: Snippet children?: Snippet
/** Optional header actions */
headerActions?: Snippet
/** Whether the sidebar content should scroll. Default true. */ /** Whether the sidebar content should scroll. Default true. */
scrollable?: boolean scrollable?: boolean
} }
const { open = false, title, onClose, onclose, children, headerActions, scrollable = true }: Props = $props() const { open = false, title, onClose, onclose, onback, onsave, saveLabel = 'Done', element, children, scrollable = true }: Props = $props()
// Support both onClose (camelCase) and onclose (lowercase) for backward compatibility // Support both onClose (camelCase) and onclose (lowercase) for backward compatibility
const handleClose = $derived(onClose ?? onclose) const handleClose = $derived(onClose ?? onclose)
</script> </script>
{#snippet leftAccessory()}
{#if onback}
<Button
variant="ghost"
size="small"
iconOnly
icon="arrow-left"
onclick={onback}
aria-label="Go back"
/>
{:else if handleClose}
<Button
variant="ghost"
size="small"
iconOnly
icon="close"
onclick={handleClose}
aria-label="Close sidebar"
/>
{/if}
{/snippet}
{#snippet rightAccessory()}
{#if onsave}
<Button variant="ghost" size="small" {element} elementStyle={!!element} onclick={onsave}>
{saveLabel}
</Button>
{/if}
{/snippet}
<aside class="sidebar" class:open style:--sidebar-width={SIDEBAR_WIDTH}> <aside class="sidebar" class:open style:--sidebar-width={SIDEBAR_WIDTH}>
{#if title} {#if title}
<SidebarHeader {title} onclose={handleClose} actions={headerActions} /> <SidebarHeader {title} {leftAccessory} {rightAccessory} />
{/if} {/if}
<div class="sidebar-content" class:scrollable> <div class="sidebar-content" class:scrollable>

View file

@ -1,36 +1,32 @@
<svelte:options runes={true} /> <svelte:options runes={true} />
<script lang="ts"> <script lang="ts">
import Button from './Button.svelte'
import closeIcon from '$src/assets/icons/close.svg?raw'
import type { Snippet } from 'svelte' import type { Snippet } from 'svelte'
interface Props { interface Props {
/** Title for the sidebar header */ /** Title for the sidebar header */
title: string title: string
/** Callback when close is requested */ /** Left accessory content (e.g., close/back button) */
onclose?: () => void leftAccessory?: Snippet
/** Optional additional actions to render in the header */ /** Right accessory content (e.g., save/edit button) */
actions?: Snippet rightAccessory?: Snippet
} }
const { title, onclose, actions }: Props = $props() const { title, leftAccessory, rightAccessory }: Props = $props()
</script> </script>
<div class="sidebar-header"> <div class="sidebar-header">
<div class="header-left"> <div class="header-left">
{#if actions} {#if leftAccessory}
{@render actions()} {@render leftAccessory()}
{/if} {/if}
</div> </div>
<h2 class="sidebar-title">{title}</h2> <h2 class="sidebar-title">{title}</h2>
<div class="header-right"> <div class="header-right">
{#if onclose} {#if rightAccessory}
<button onclick={onclose} class="close-button" aria-label="Close sidebar"> {@render rightAccessory()}
{@html closeIcon}
</button>
{/if} {/if}
</div> </div>
</div> </div>
@ -42,9 +38,9 @@
@use '$src/themes/layout' as *; @use '$src/themes/layout' as *;
.sidebar-header { .sidebar-header {
display: flex; display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center; align-items: center;
justify-content: space-between;
padding: $unit-2x; padding: $unit-2x;
border-bottom: 1px solid var(--border-primary); border-bottom: 1px solid var(--border-primary);
flex-shrink: 0; flex-shrink: 0;
@ -57,11 +53,8 @@
.header-left, .header-left,
.header-right { .header-right {
width: 32px; // Same width as close button for balance
flex-shrink: 0;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center;
} }
.header-left { .header-left {
@ -78,39 +71,10 @@
font-weight: $medium; font-weight: $medium;
color: var(--text-primary); color: var(--text-primary);
text-align: center; text-align: center;
flex: 1; white-space: nowrap;
} overflow: hidden;
text-overflow: ellipsis;
.close-button { padding: 0 $unit;
padding: $unit;
background: transparent;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
color: var(--text-secondary);
transition:
background-color 0.2s,
color 0.2s;
width: 32px;
height: 32px;
:global(svg) {
width: 14px;
height: 14px;
fill: currentColor;
}
&:hover {
background-color: var(--button-bg);
color: var(--text-primary);
}
&:active {
transform: translateY(1px);
}
} }
} }
</style> </style>