mirror of
https://github.com/ByteAtATime/raycast-linux.git
synced 2025-08-31 03:07:23 +00:00
refactor(actions): combine Footer into ActionBar
Previously, the two components did basically the same thing. This commit removes Footer in favor of ActionBar. It also extracts a Toast.svelte component, which is now used to render the toast and dropdown in ActionBar.
This commit is contained in:
parent
6ed4af5ce7
commit
ef510af02a
7 changed files with 145 additions and 125 deletions
BIN
src-tauri/SoulverWrapper/Vendor/SoulverCore-linux/lib_FoundationICU.so
vendored
Executable file
BIN
src-tauri/SoulverWrapper/Vendor/SoulverCore-linux/lib_FoundationICU.so
vendored
Executable file
Binary file not shown.
|
@ -68,6 +68,12 @@
|
|||
selectedIndex = findNextSelectableIndex(selectedIndex, 1);
|
||||
break;
|
||||
case 'Enter':
|
||||
if (
|
||||
event.target instanceof HTMLElement &&
|
||||
event.target.closest('[data-slot="dropdown-menu-content"]')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (selectedIndex !== -1 && isItemSelectable(items[selectedIndex])) {
|
||||
event.preventDefault();
|
||||
onenter(items[selectedIndex]);
|
||||
|
|
|
@ -167,28 +167,24 @@
|
|||
{/snippet}
|
||||
|
||||
{#snippet footer()}
|
||||
{#if toastToShow}
|
||||
<Footer toast={toastToShow} {onToastAction} />
|
||||
{:else}
|
||||
<ActionBar title={navigationTitle}>
|
||||
{#snippet primaryAction({ props })}
|
||||
{#if primaryActionObject}
|
||||
<NodeRenderer
|
||||
{...props}
|
||||
nodeId={primaryActionObject.id}
|
||||
{uiTree}
|
||||
onDispatch={handleDispatch}
|
||||
displayAs="button"
|
||||
/>
|
||||
{/if}
|
||||
{/snippet}
|
||||
{#snippet actions()}
|
||||
{#if showActionPanelDropdown && actionPanel}
|
||||
<NodeRenderer nodeId={actionPanel.id} {uiTree} onDispatch={handleDispatch} />
|
||||
{/if}
|
||||
{/snippet}
|
||||
</ActionBar>
|
||||
{/if}
|
||||
<ActionBar title={navigationTitle} toast={toastToShow} {onToastAction}>
|
||||
{#snippet primaryAction({ props })}
|
||||
{#if primaryActionObject}
|
||||
<NodeRenderer
|
||||
{...props}
|
||||
nodeId={primaryActionObject.id}
|
||||
{uiTree}
|
||||
onDispatch={handleDispatch}
|
||||
displayAs="button"
|
||||
/>
|
||||
{/if}
|
||||
{/snippet}
|
||||
{#snippet actions()}
|
||||
{#if showActionPanelDropdown && actionPanel}
|
||||
<NodeRenderer nodeId={actionPanel.id} {uiTree} onDispatch={handleDispatch} />
|
||||
{/if}
|
||||
{/snippet}
|
||||
</ActionBar>
|
||||
{/snippet}
|
||||
</MainLayout>
|
||||
{/if}
|
||||
|
|
|
@ -1,99 +0,0 @@
|
|||
<script lang="ts">
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
||||
import { Kbd } from '../ui/kbd';
|
||||
import { keyEventMatches, type KeyboardShortcut as KeyboardShortcutType } from '$lib/props';
|
||||
import type { Toast } from '$lib/ui.svelte';
|
||||
import KeyboardShortcut from '../KeyboardShortcut.svelte';
|
||||
|
||||
type Props = {
|
||||
toast: Toast;
|
||||
onToastAction: (toastId: number, actionType: 'primary' | 'secondary') => void;
|
||||
};
|
||||
let { toast, onToastAction }: Props = $props();
|
||||
|
||||
let dropdownOpen = $state(false);
|
||||
|
||||
const toastActions = $derived.by(() => {
|
||||
const availableActions: {
|
||||
type: 'primary' | 'secondary';
|
||||
title: string;
|
||||
shortcut?: KeyboardShortcutType;
|
||||
}[] = [];
|
||||
|
||||
if (toast?.primaryAction) {
|
||||
availableActions.push({ type: 'primary', ...toast.primaryAction });
|
||||
}
|
||||
if (toast?.secondaryAction) {
|
||||
availableActions.push({ type: 'secondary', ...toast.secondaryAction });
|
||||
}
|
||||
return availableActions;
|
||||
});
|
||||
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
if (e.key.toLowerCase() === 't' && (e.ctrlKey || e.metaKey)) {
|
||||
if (toastActions.length > 0) {
|
||||
e.preventDefault();
|
||||
dropdownOpen = !dropdownOpen;
|
||||
}
|
||||
} else if (toast?.primaryAction?.shortcut && keyEventMatches(e, toast.primaryAction.shortcut)) {
|
||||
handleActionSelect('primary');
|
||||
} else if (
|
||||
toast?.secondaryAction?.shortcut &&
|
||||
keyEventMatches(e, toast.secondaryAction.shortcut)
|
||||
) {
|
||||
handleActionSelect('secondary');
|
||||
}
|
||||
}
|
||||
|
||||
function handleActionSelect(actionType: 'primary' | 'secondary') {
|
||||
if (toast) {
|
||||
onToastAction(toast.id, actionType);
|
||||
}
|
||||
dropdownOpen = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
<footer class="bg-card flex h-10 shrink-0 items-center border-t px-2">
|
||||
<DropdownMenu.Root bind:open={dropdownOpen}>
|
||||
<DropdownMenu.Trigger>
|
||||
{#snippet child({ props })}
|
||||
<div {...props} class="flex items-center gap-2">
|
||||
<div class="flex size-4 items-center justify-center">
|
||||
{#if toast.style === 'ANIMATED'}
|
||||
<div
|
||||
class="size-4 animate-spin rounded-full border-2 border-white/30 border-t-white"
|
||||
></div>
|
||||
{:else if toast.style === 'SUCCESS'}
|
||||
<div class="shadow-glow size-2 rounded-full bg-green-500 shadow-green-500"></div>
|
||||
{:else if toast.style === 'FAILURE'}
|
||||
<div class="shadow-glow size-2 rounded-full bg-red-500 shadow-red-500"></div>
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
<span>{toast.title}</span>
|
||||
<span class="text-muted-foreground text-sm">{toast.message}</span>
|
||||
</div>
|
||||
{#if toast.primaryAction || toast.secondaryAction}
|
||||
<Kbd>Ctrl + T</Kbd>
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content side="top" align="start" class="w-60">
|
||||
<DropdownMenu.Label>Toast Actions</DropdownMenu.Label>
|
||||
<DropdownMenu.Separator />
|
||||
{#each toastActions as action (action.type)}
|
||||
<DropdownMenu.Item onselect={() => handleActionSelect(action.type)}>
|
||||
{action.title}
|
||||
{#if action.shortcut}
|
||||
<DropdownMenu.Shortcut>
|
||||
<KeyboardShortcut shortcut={action.shortcut} />
|
||||
</DropdownMenu.Shortcut>
|
||||
{/if}
|
||||
</DropdownMenu.Item>
|
||||
{/each}
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</footer>
|
|
@ -4,19 +4,26 @@
|
|||
import Icon from '$lib/components/Icon.svelte';
|
||||
import { Separator } from '$lib/components/ui/separator/index.js';
|
||||
import type { ButtonProps } from '$lib/components/ui/button';
|
||||
import type { Toast as ToastType } from '$lib/ui.svelte';
|
||||
import Toast from './Toast.svelte';
|
||||
|
||||
type Props = {
|
||||
title?: string | Snippet;
|
||||
icon?: ImageLike | null;
|
||||
primaryAction?: Snippet<[{ props: ButtonProps }]>;
|
||||
actions?: Snippet;
|
||||
|
||||
toast?: ToastType | null;
|
||||
onToastAction?: (toastId: number, actionType: 'primary' | 'secondary') => void;
|
||||
};
|
||||
|
||||
let { title, icon, primaryAction, actions }: Props = $props();
|
||||
let { title, icon, primaryAction, actions, toast = null, onToastAction }: Props = $props();
|
||||
</script>
|
||||
|
||||
<footer class="bg-card flex h-10 shrink-0 items-center border-t px-2">
|
||||
{#if title || icon}
|
||||
{#if toast}
|
||||
<Toast {toast} {onToastAction} />
|
||||
{:else if title || icon}
|
||||
<div class="flex min-w-0 items-center gap-2">
|
||||
{#if icon}
|
||||
<Icon {icon} class="size-5 shrink-0" />
|
||||
|
|
110
src/lib/components/nodes/shared/Toast.svelte
Normal file
110
src/lib/components/nodes/shared/Toast.svelte
Normal file
|
@ -0,0 +1,110 @@
|
|||
<script lang="ts">
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
||||
import type { Toast } from '$lib/ui.svelte';
|
||||
import type { KeyboardShortcut as KeyboardShortcutType } from '$lib/props';
|
||||
import KeyboardShortcut from '$lib/components/KeyboardShortcut.svelte';
|
||||
import { keyEventMatches } from '$lib/props/actions';
|
||||
import { focusManager } from '$lib/focus.svelte';
|
||||
|
||||
type Props = {
|
||||
toast: Toast;
|
||||
onToastAction?: (toastId: number, actionType: 'primary' | 'secondary') => void;
|
||||
};
|
||||
|
||||
let { toast, onToastAction }: Props = $props();
|
||||
|
||||
let open = $state(false);
|
||||
const scopeId = `toast-menu-${crypto.randomUUID()}`;
|
||||
|
||||
$effect(() => {
|
||||
if (open) {
|
||||
focusManager.requestFocus(scopeId);
|
||||
} else {
|
||||
focusManager.releaseFocus(scopeId);
|
||||
}
|
||||
});
|
||||
|
||||
const actions = $derived.by(() => {
|
||||
if (!toast) return [];
|
||||
|
||||
const availableActions: {
|
||||
type: 'primary' | 'secondary';
|
||||
title: string;
|
||||
shortcut?: KeyboardShortcutType;
|
||||
}[] = [];
|
||||
|
||||
if (toast.primaryAction) {
|
||||
availableActions.push({ type: 'primary', ...toast.primaryAction });
|
||||
}
|
||||
if (toast.secondaryAction) {
|
||||
availableActions.push({ type: 'secondary', ...toast.secondaryAction });
|
||||
}
|
||||
return availableActions;
|
||||
});
|
||||
|
||||
const handleKeydown = (e: KeyboardEvent) => {
|
||||
if (e.key.toLowerCase() === 't' && (e.ctrlKey || e.metaKey)) {
|
||||
if (actions.length > 0) {
|
||||
e.preventDefault();
|
||||
open = !open;
|
||||
}
|
||||
} else if (toast?.primaryAction?.shortcut && keyEventMatches(e, toast.primaryAction.shortcut)) {
|
||||
handleActionSelect('primary');
|
||||
} else if (
|
||||
toast?.secondaryAction?.shortcut &&
|
||||
keyEventMatches(e, toast.secondaryAction.shortcut)
|
||||
) {
|
||||
handleActionSelect('secondary');
|
||||
}
|
||||
};
|
||||
|
||||
const handleActionSelect = (actionType: 'primary' | 'secondary') => {
|
||||
if (onToastAction) {
|
||||
onToastAction(toast.id, actionType);
|
||||
}
|
||||
open = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
<DropdownMenu.Root bind:open>
|
||||
<DropdownMenu.Trigger>
|
||||
{#snippet child({ props })}
|
||||
<div {...props} class="flex items-baseline gap-2">
|
||||
<div class="flex size-4 items-center justify-center">
|
||||
{#if toast.style === 'ANIMATED'}
|
||||
<div
|
||||
class="size-4 animate-spin rounded-full border-2 border-white/30 border-t-white"
|
||||
></div>
|
||||
{:else if toast.style === 'SUCCESS'}
|
||||
<div class="shadow-glow size-2 rounded-full bg-green-500 shadow-green-500"></div>
|
||||
{:else if toast.style === 'FAILURE'}
|
||||
<div class="shadow-glow size-2 rounded-full bg-red-500 shadow-red-500"></div>
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
<span>{toast.title}</span>
|
||||
<span class="text-muted-foreground text-sm">{toast.message}</span>
|
||||
</div>
|
||||
{#if toast.primaryAction || toast.secondaryAction}
|
||||
<KeyboardShortcut shortcut={{ key: 't', modifiers: ['ctrl'] }} />
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content side="top" align="start" class="w-60">
|
||||
<DropdownMenu.Label>Toast Actions</DropdownMenu.Label>
|
||||
<DropdownMenu.Separator />
|
||||
{#each actions as action (action.type)}
|
||||
<DropdownMenu.Item onclick={() => handleActionSelect(action.type)}>
|
||||
{action.title}
|
||||
{#if action.shortcut}
|
||||
<DropdownMenu.Shortcut>
|
||||
<KeyboardShortcut shortcut={action.shortcut} />
|
||||
</DropdownMenu.Shortcut>
|
||||
{/if}
|
||||
</DropdownMenu.Item>
|
||||
{/each}
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
|
@ -9,8 +9,8 @@ export type Toast = {
|
|||
title: string;
|
||||
message?: string;
|
||||
style?: 'SUCCESS' | 'FAILURE' | 'ANIMATED';
|
||||
primaryAction?: { title: string; onAction: boolean; shortcut?: KeyboardShortcut };
|
||||
secondaryAction?: { title: string; onAction: boolean; shortcut?: KeyboardShortcut };
|
||||
primaryAction?: { title: string; onAction: () => void; shortcut?: KeyboardShortcut };
|
||||
secondaryAction?: { title: string; onAction: () => void; shortcut?: KeyboardShortcut };
|
||||
};
|
||||
|
||||
function createUiStore() {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue