add overflow menu support to pane stack

This commit is contained in:
Justin Edmund 2025-12-03 18:20:09 -08:00
parent 100f506c44
commit db71e6dc80
3 changed files with 121 additions and 26 deletions

View file

@ -1,6 +1,7 @@
<svelte:options runes={true} />
<script lang="ts">
import { DropdownMenu } from 'bits-ui'
import {
type PaneConfig,
type PaneStackStore,
@ -105,10 +106,39 @@
element={pane.action.element}
elementStyle={!!pane.action.element}
onclick={pane.action.handler}
disabled={pane.action.disabled}
>
{pane.action.label}
</Button>
{/if}
{#if pane.overflowMenu && pane.overflowMenu.length > 0}
<DropdownMenu.Root>
<DropdownMenu.Trigger>
{#snippet child({ props })}
<Button
{...props}
variant="ghost"
size="small"
iconOnly
icon="ellipsis"
aria-label="More options"
/>
{/snippet}
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content class="overflow-menu" side="bottom" align="end" sideOffset={4}>
{#each pane.overflowMenu as item}
<DropdownMenu.Item
class="overflow-menu-item {item.variant === 'danger' ? 'danger' : ''}"
onSelect={item.handler}
>
{item.label}
</DropdownMenu.Item>
{/each}
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
{/if}
{/snippet}
</SidebarHeader>
@ -256,4 +286,38 @@
}
}
}
// Overflow menu styles
:global(.overflow-menu) {
background: var(--menu-bg, white);
border: 1px solid var(--border-color, #ddd);
border-radius: $card-corner;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
padding: $unit-half;
min-width: calc($unit * 20);
z-index: 200;
}
:global(.overflow-menu-item) {
padding: $unit $unit-2x;
border-radius: $item-corner-small;
cursor: pointer;
font-size: 14px;
color: var(--text-primary);
outline: none;
&:hover,
&:focus {
background: var(--button-bg-hover, #f5f5f5);
}
&.danger {
color: var(--danger, #dc3545);
&:hover,
&:focus {
background: var(--danger-bg, #fff5f5);
}
}
}
</style>

View file

@ -13,6 +13,19 @@ export type ElementType = 'wind' | 'fire' | 'water' | 'earth' | 'dark' | 'light'
/** Context key for pane stack */
const PANE_STACK_CONTEXT_KEY = Symbol('pane-stack')
export interface PaneAction {
label: string
handler: () => void
element?: ElementType
disabled?: boolean
}
export interface OverflowMenuItem {
label: string
handler: () => void
variant?: 'default' | 'danger'
}
export interface PaneConfig {
/** Unique identifier for this pane */
id: string
@ -25,11 +38,9 @@ export interface PaneConfig {
/** Optional callback when back is clicked (for root pane) */
onback?: () => void
/** Optional save/action button configuration */
action?: {
label: string
handler: () => void
element?: ElementType
}
action?: PaneAction
/** Optional overflow menu items */
overflowMenu?: OverflowMenuItem[]
/** Whether this pane's content should scroll */
scrollable?: boolean
}

View file

@ -2,6 +2,7 @@ import type { Snippet, Component } from 'svelte'
import {
PaneStackStore,
type PaneConfig,
type OverflowMenuItem,
type ElementType
} from '$lib/stores/paneStack.svelte'
@ -128,30 +129,28 @@ class SidebarStore {
/**
* Update the action button for the current pane
* @param handler - Click handler, or undefined to show disabled button
* @param label - Button label (defaults to 'Done')
* @param element - Element type for styling
* @param show - Whether to show the button at all (defaults to true if label provided)
*/
setAction(
onsave: (() => void) | undefined,
saveLabel?: string,
element?: ElementType
handler: (() => void) | undefined,
label?: string,
element?: ElementType,
show: boolean = true
) {
const currentPane = this.paneStack.currentPane
if (currentPane) {
this.paneStack.updateCurrentProps({})
// Update action on current pane
const panes = this.paneStack.panes
const currentIndex = panes.length - 1
if (currentIndex >= 0 && panes[currentIndex]) {
panes[currentIndex] = {
...panes[currentIndex],
action:
onsave ?
{
label: saveLabel ?? 'Done',
handler: onsave,
element
}
: undefined
}
const panes = this.paneStack.panes
const currentIndex = panes.length - 1
if (currentIndex >= 0 && panes[currentIndex]) {
panes[currentIndex] = {
...panes[currentIndex],
action: show && label ? {
label,
handler: handler ?? (() => {}),
element,
disabled: !handler
} : undefined
}
}
}
@ -163,6 +162,27 @@ class SidebarStore {
this.setAction(undefined)
}
/**
* Set the overflow menu items for the current pane
*/
setOverflowMenu(items: OverflowMenuItem[] | undefined) {
const panes = this.paneStack.panes
const currentIndex = panes.length - 1
if (currentIndex >= 0 && panes[currentIndex]) {
panes[currentIndex] = {
...panes[currentIndex],
overflowMenu: items
}
}
}
/**
* Clear the overflow menu for the current pane
*/
clearOverflowMenu() {
this.setOverflowMenu(undefined)
}
// Getters for reactive access
get isOpen() {
return this.state.open