add overflow menu support to pane stack
This commit is contained in:
parent
100f506c44
commit
db71e6dc80
3 changed files with 121 additions and 26 deletions
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue