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} /> <svelte:options runes={true} />
<script lang="ts"> <script lang="ts">
import { DropdownMenu } from 'bits-ui'
import { import {
type PaneConfig, type PaneConfig,
type PaneStackStore, type PaneStackStore,
@ -105,10 +106,39 @@
element={pane.action.element} element={pane.action.element}
elementStyle={!!pane.action.element} elementStyle={!!pane.action.element}
onclick={pane.action.handler} onclick={pane.action.handler}
disabled={pane.action.disabled}
> >
{pane.action.label} {pane.action.label}
</Button> </Button>
{/if} {/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} {/snippet}
</SidebarHeader> </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> </style>

View file

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

View file

@ -2,6 +2,7 @@ import type { Snippet, Component } from 'svelte'
import { import {
PaneStackStore, PaneStackStore,
type PaneConfig, type PaneConfig,
type OverflowMenuItem,
type ElementType type ElementType
} from '$lib/stores/paneStack.svelte' } from '$lib/stores/paneStack.svelte'
@ -128,30 +129,28 @@ class SidebarStore {
/** /**
* Update the action button for the current pane * 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( setAction(
onsave: (() => void) | undefined, handler: (() => void) | undefined,
saveLabel?: string, label?: string,
element?: ElementType element?: ElementType,
show: boolean = true
) { ) {
const currentPane = this.paneStack.currentPane const panes = this.paneStack.panes
if (currentPane) { const currentIndex = panes.length - 1
this.paneStack.updateCurrentProps({}) if (currentIndex >= 0 && panes[currentIndex]) {
// Update action on current pane panes[currentIndex] = {
const panes = this.paneStack.panes ...panes[currentIndex],
const currentIndex = panes.length - 1 action: show && label ? {
if (currentIndex >= 0 && panes[currentIndex]) { label,
panes[currentIndex] = { handler: handler ?? (() => {}),
...panes[currentIndex], element,
action: disabled: !handler
onsave ? } : undefined
{
label: saveLabel ?? 'Done',
handler: onsave,
element
}
: undefined
}
} }
} }
} }
@ -163,6 +162,27 @@ class SidebarStore {
this.setAction(undefined) 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 // Getters for reactive access
get isOpen() { get isOpen() {
return this.state.open return this.state.open