mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-15 17:19:40 +02:00
refactor(mana/web): extract useItemContextMenu helper for ListView right-click menus
Nine files (8 ListViews + todo's TaskList) reimplemented the same context-menu state machinery character-for-character: a typed $state object with visible/x/y/<itemKey>, a handleItemContextMenu function that calls preventDefault and stuffs the click position in, and a close handler that resets the entity field. Extract `useItemContextMenu<T>()` in $lib/data/item-context-menu.svelte that returns a reactive handle with `.state` (visible/x/y/target), `.open(e, target)`, and `.close()`. Consumers derive their menu items from `ctxMenu.state.target` and pass `ctxMenu.close` directly to <ContextMenu onClose>. Per file: ~10 LOC of state declaration + handler removed; consumer items array switches from `ctxMenu.<entity>` to `ctxMenu.state.target`. Across the 9 files this is ~−90 LOC of pure boilerplate; helper itself is 50 LOC. Net small (~−40 LOC) but the boilerplate is gone and the shape is one helper away from being adjustable globally. Note: shared-ui already exports a `createContextMenuState` factory, but it's a plain default-value object — not a Svelte 5 reactive helper. This new wrapper composes with the existing `ContextMenuState<T>` type from shared-ui rather than replacing it. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0af9094096
commit
243c09d97c
10 changed files with 191 additions and 214 deletions
52
apps/mana/apps/web/src/lib/data/item-context-menu.svelte.ts
Normal file
52
apps/mana/apps/web/src/lib/data/item-context-menu.svelte.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
/**
|
||||
* useItemContextMenu — Svelte 5 reactive wrapper around `ContextMenuState`.
|
||||
*
|
||||
* Every workbench-style ListView reimplemented the same boilerplate:
|
||||
*
|
||||
* let ctxMenu = $state<{...}>({ visible: false, x: 0, y: 0, item: null });
|
||||
* function handleItemContextMenu(e: MouseEvent, item: T) {
|
||||
* e.preventDefault();
|
||||
* ctxMenu = { visible: true, x: e.clientX, y: e.clientY, item };
|
||||
* }
|
||||
* // ... and a close handler that resets the target
|
||||
*
|
||||
* This helper collapses that to:
|
||||
*
|
||||
* const ctxMenu = useItemContextMenu<LocalContact>();
|
||||
*
|
||||
* The consumer derives its menu items from `ctxMenu.state.target`, wires
|
||||
* `oncontextmenu={(e) => ctxMenu.open(e, item)}` on the row, and passes
|
||||
* `ctxMenu.close` to `<ContextMenu onClose>`.
|
||||
*/
|
||||
|
||||
import type { ContextMenuState } from '@mana/shared-ui';
|
||||
|
||||
export interface ItemContextMenuHandle<T> {
|
||||
readonly state: ContextMenuState<T>;
|
||||
/** Call from `oncontextmenu` to open the menu at the click position. */
|
||||
open: (e: MouseEvent, target: T) => void;
|
||||
/** Hide the menu and clear the target. Pass to `<ContextMenu onClose>`. */
|
||||
close: () => void;
|
||||
}
|
||||
|
||||
export function useItemContextMenu<T>(): ItemContextMenuHandle<T> {
|
||||
let state = $state<ContextMenuState<T>>({
|
||||
visible: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
target: null,
|
||||
});
|
||||
|
||||
return {
|
||||
get state() {
|
||||
return state;
|
||||
},
|
||||
open(e: MouseEvent, target: T) {
|
||||
e.preventDefault();
|
||||
state = { visible: true, x: e.clientX, y: e.clientY, target };
|
||||
},
|
||||
close() {
|
||||
state = { ...state, visible: false, target: null };
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@
|
|||
import type { TagDragData } from '@mana/shared-ui/dnd';
|
||||
import { useAllTags, getTagsByIds } from '@mana/shared-stores';
|
||||
import { addTagId } from '$lib/data/tag-mutations';
|
||||
import { useItemContextMenu } from '$lib/data/item-context-menu.svelte';
|
||||
|
||||
let { navigate, goBack, params }: ViewProps = $props();
|
||||
|
||||
|
|
@ -59,30 +60,18 @@
|
|||
|
||||
const dayNames = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'];
|
||||
|
||||
// Context menu
|
||||
let ctxMenu = $state<{ visible: boolean; x: number; y: number; event: CalendarEvent | null }>({
|
||||
visible: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
event: null,
|
||||
});
|
||||
|
||||
function handleItemContextMenu(e: MouseEvent, event: CalendarEvent) {
|
||||
e.preventDefault();
|
||||
ctxMenu = { visible: true, x: e.clientX, y: e.clientY, event };
|
||||
}
|
||||
const ctxMenu = useItemContextMenu<CalendarEvent>();
|
||||
|
||||
let ctxMenuItems = $derived<ContextMenuItem[]>(
|
||||
ctxMenu.event
|
||||
ctxMenu.state.target
|
||||
? [
|
||||
{
|
||||
id: 'open',
|
||||
label: 'Öffnen',
|
||||
icon: PencilSimple,
|
||||
action: () => {
|
||||
if (ctxMenu.event) {
|
||||
navigate('detail', { eventId: ctxMenu.event.id });
|
||||
}
|
||||
const target = ctxMenu.state.target;
|
||||
if (target) navigate('detail', { eventId: target.id });
|
||||
},
|
||||
},
|
||||
{ id: 'div', label: '', type: 'divider' as const },
|
||||
|
|
@ -92,9 +81,8 @@
|
|||
icon: Trash,
|
||||
variant: 'danger' as const,
|
||||
action: () => {
|
||||
if (ctxMenu.event) {
|
||||
eventsStore.deleteEvent(ctxMenu.event.id);
|
||||
}
|
||||
const target = ctxMenu.state.target;
|
||||
if (target) eventsStore.deleteEvent(target.id);
|
||||
},
|
||||
},
|
||||
]
|
||||
|
|
@ -177,7 +165,7 @@
|
|||
_siblingIds: todayEvents.map((e) => e.id),
|
||||
_siblingKey: 'eventId',
|
||||
})}
|
||||
oncontextmenu={(e) => handleItemContextMenu(e, event)}
|
||||
oncontextmenu={(e) => ctxMenu.open(e, event)}
|
||||
use:dragSource={{
|
||||
type: 'event',
|
||||
data: () => ({
|
||||
|
|
@ -227,11 +215,11 @@
|
|||
</div>
|
||||
|
||||
<ContextMenu
|
||||
visible={ctxMenu.visible}
|
||||
x={ctxMenu.x}
|
||||
y={ctxMenu.y}
|
||||
visible={ctxMenu.state.visible}
|
||||
x={ctxMenu.state.x}
|
||||
y={ctxMenu.state.y}
|
||||
items={ctxMenuItems}
|
||||
onClose={() => (ctxMenu = { ...ctxMenu, visible: false, event: null })}
|
||||
onClose={ctxMenu.close}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
import type { TagDragData } from '@mana/shared-ui/dnd';
|
||||
import { useAllTags, getTagsByIds } from '@mana/shared-stores';
|
||||
import { addTagId } from '$lib/data/tag-mutations';
|
||||
import { useItemContextMenu } from '$lib/data/item-context-menu.svelte';
|
||||
|
||||
let { navigate, goBack, params }: ViewProps = $props();
|
||||
|
||||
|
|
@ -61,40 +62,27 @@
|
|||
return (f + l).toUpperCase() || '?';
|
||||
}
|
||||
|
||||
// Context menu
|
||||
let ctxMenu = $state<{ visible: boolean; x: number; y: number; contact: LocalContact | null }>({
|
||||
visible: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
contact: null,
|
||||
});
|
||||
|
||||
function handleItemContextMenu(e: MouseEvent, contact: LocalContact) {
|
||||
e.preventDefault();
|
||||
ctxMenu = { visible: true, x: e.clientX, y: e.clientY, contact };
|
||||
}
|
||||
const ctxMenu = useItemContextMenu<LocalContact>();
|
||||
|
||||
let ctxMenuItems = $derived<ContextMenuItem[]>(
|
||||
ctxMenu.contact
|
||||
ctxMenu.state.target
|
||||
? [
|
||||
{
|
||||
id: 'open',
|
||||
label: 'Öffnen',
|
||||
icon: PencilSimple,
|
||||
action: () => {
|
||||
if (ctxMenu.contact) {
|
||||
navigate('detail', { contactId: ctxMenu.contact.id });
|
||||
}
|
||||
const target = ctxMenu.state.target;
|
||||
if (target) navigate('detail', { contactId: target.id });
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'favorite',
|
||||
label: ctxMenu.contact.isFavorite ? 'Favorit entfernen' : 'Als Favorit',
|
||||
label: ctxMenu.state.target.isFavorite ? 'Favorit entfernen' : 'Als Favorit',
|
||||
icon: Star,
|
||||
action: () => {
|
||||
if (ctxMenu.contact) {
|
||||
contactsStore.toggleFavorite(ctxMenu.contact.id);
|
||||
}
|
||||
const target = ctxMenu.state.target;
|
||||
if (target) contactsStore.toggleFavorite(target.id);
|
||||
},
|
||||
},
|
||||
{ id: 'div', label: '', type: 'divider' as const },
|
||||
|
|
@ -104,9 +92,8 @@
|
|||
icon: Trash,
|
||||
variant: 'danger' as const,
|
||||
action: () => {
|
||||
if (ctxMenu.contact) {
|
||||
contactsStore.deleteContact(ctxMenu.contact.id);
|
||||
}
|
||||
const target = ctxMenu.state.target;
|
||||
if (target) contactsStore.deleteContact(target.id);
|
||||
},
|
||||
},
|
||||
]
|
||||
|
|
@ -157,7 +144,7 @@
|
|||
_siblingIds: filtered().map((c) => c.id),
|
||||
_siblingKey: 'contactId',
|
||||
})}
|
||||
oncontextmenu={(e) => handleItemContextMenu(e, contact)}
|
||||
oncontextmenu={(e) => ctxMenu.open(e, contact)}
|
||||
use:dragSource={{
|
||||
type: 'contact',
|
||||
data: () => ({
|
||||
|
|
@ -203,11 +190,11 @@
|
|||
</div>
|
||||
|
||||
<ContextMenu
|
||||
visible={ctxMenu.visible}
|
||||
x={ctxMenu.x}
|
||||
y={ctxMenu.y}
|
||||
visible={ctxMenu.state.visible}
|
||||
x={ctxMenu.state.x}
|
||||
y={ctxMenu.state.y}
|
||||
items={ctxMenuItems}
|
||||
onClose={() => (ctxMenu = { ...ctxMenu, visible: false, contact: null })}
|
||||
onClose={ctxMenu.close}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
import { MOOD_COLORS, MOOD_LABELS, type Dream, type DreamMood, type SleepQuality } from './types';
|
||||
import type { ViewProps } from '$lib/app-registry';
|
||||
import { ContextMenu, type ContextMenuItem } from '@mana/shared-ui';
|
||||
import { useItemContextMenu } from '$lib/data/item-context-menu.svelte';
|
||||
import { PencilSimple, PushPin, Trash } from '@mana/shared-icons';
|
||||
import SymbolsView from './views/SymbolsView.svelte';
|
||||
|
||||
|
|
@ -132,36 +133,27 @@
|
|||
if (editingId === id) editingId = null;
|
||||
}
|
||||
|
||||
// Context menu
|
||||
let ctxMenu = $state<{ visible: boolean; x: number; y: number; dream: Dream | null }>({
|
||||
visible: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
dream: null,
|
||||
});
|
||||
|
||||
function handleItemContextMenu(e: MouseEvent, dream: Dream) {
|
||||
e.preventDefault();
|
||||
ctxMenu = { visible: true, x: e.clientX, y: e.clientY, dream };
|
||||
}
|
||||
const ctxMenu = useItemContextMenu<Dream>();
|
||||
|
||||
let ctxMenuItems = $derived<ContextMenuItem[]>(
|
||||
ctxMenu.dream
|
||||
ctxMenu.state.target
|
||||
? [
|
||||
{
|
||||
id: 'edit',
|
||||
label: 'Bearbeiten',
|
||||
icon: PencilSimple,
|
||||
action: () => {
|
||||
if (ctxMenu.dream) startEdit(ctxMenu.dream);
|
||||
const target = ctxMenu.state.target;
|
||||
if (target) startEdit(target);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'pin',
|
||||
label: ctxMenu.dream.isPinned ? 'Lösen' : 'Pinnen',
|
||||
label: ctxMenu.state.target.isPinned ? 'Lösen' : 'Pinnen',
|
||||
icon: PushPin,
|
||||
action: () => {
|
||||
if (ctxMenu.dream) dreamsStore.togglePin(ctxMenu.dream.id);
|
||||
const target = ctxMenu.state.target;
|
||||
if (target) dreamsStore.togglePin(target.id);
|
||||
},
|
||||
},
|
||||
{ id: 'div', label: '', type: 'divider' as const },
|
||||
|
|
@ -171,7 +163,8 @@
|
|||
icon: Trash,
|
||||
variant: 'danger' as const,
|
||||
action: () => {
|
||||
if (ctxMenu.dream) handleDelete(ctxMenu.dream.id);
|
||||
const target = ctxMenu.state.target;
|
||||
if (target) handleDelete(target.id);
|
||||
},
|
||||
},
|
||||
]
|
||||
|
|
@ -425,7 +418,7 @@
|
|||
startEdit(dream);
|
||||
}
|
||||
}}
|
||||
oncontextmenu={(e) => handleItemContextMenu(e, dream)}
|
||||
oncontextmenu={(e) => ctxMenu.open(e, dream)}
|
||||
>
|
||||
{#if dream.mood}
|
||||
<span class="mood-dot-row" style="background: {MOOD_COLORS[dream.mood]}"></span>
|
||||
|
|
@ -485,11 +478,11 @@
|
|||
{/if}
|
||||
|
||||
<ContextMenu
|
||||
visible={ctxMenu.visible}
|
||||
x={ctxMenu.x}
|
||||
y={ctxMenu.y}
|
||||
visible={ctxMenu.state.visible}
|
||||
x={ctxMenu.state.x}
|
||||
y={ctxMenu.state.y}
|
||||
items={ctxMenuItems}
|
||||
onClose={() => (ctxMenu = { ...ctxMenu, visible: false, dream: null })}
|
||||
onClose={ctxMenu.close}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
import type { Habit, HabitLog } from './types';
|
||||
import type { ViewProps } from '$lib/app-registry';
|
||||
import { ContextMenu, type ContextMenuItem } from '@mana/shared-ui';
|
||||
import { useItemContextMenu } from '$lib/data/item-context-menu.svelte';
|
||||
import { toastStore } from '@mana/shared-ui/toast';
|
||||
import { DynamicIcon } from '@mana/shared-ui/atoms';
|
||||
import { IconPicker } from '@mana/shared-ui/molecules';
|
||||
|
|
@ -95,38 +96,29 @@
|
|||
showIconPicker = false;
|
||||
}
|
||||
|
||||
// Context menu
|
||||
let ctxMenu = $state<{ visible: boolean; x: number; y: number; habit: Habit | null }>({
|
||||
visible: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
habit: null,
|
||||
});
|
||||
|
||||
function handleItemContextMenu(e: MouseEvent, habit: Habit) {
|
||||
e.preventDefault();
|
||||
ctxMenu = { visible: true, x: e.clientX, y: e.clientY, habit };
|
||||
}
|
||||
const ctxMenu = useItemContextMenu<Habit>();
|
||||
|
||||
let ctxMenuItems = $derived<ContextMenuItem[]>(
|
||||
ctxMenu.habit
|
||||
ctxMenu.state.target
|
||||
? [
|
||||
{
|
||||
id: 'log',
|
||||
label: 'Loggen',
|
||||
icon: Play,
|
||||
action: () => {
|
||||
if (ctxMenu.habit) handleTap(ctxMenu.habit.id);
|
||||
const target = ctxMenu.state.target;
|
||||
if (target) handleTap(target.id);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'archive',
|
||||
label: ctxMenu.habit.isArchived ? 'Aktivieren' : 'Archivieren',
|
||||
icon: ctxMenu.habit.isArchived ? Play : Pause,
|
||||
label: ctxMenu.state.target.isArchived ? 'Aktivieren' : 'Archivieren',
|
||||
icon: ctxMenu.state.target.isArchived ? Play : Pause,
|
||||
action: () => {
|
||||
if (ctxMenu.habit)
|
||||
habitsStore.updateHabit(ctxMenu.habit.id, {
|
||||
isArchived: !ctxMenu.habit.isArchived,
|
||||
const target = ctxMenu.state.target;
|
||||
if (target)
|
||||
habitsStore.updateHabit(target.id, {
|
||||
isArchived: !target.isArchived,
|
||||
});
|
||||
},
|
||||
},
|
||||
|
|
@ -137,7 +129,8 @@
|
|||
icon: Trash,
|
||||
variant: 'danger' as const,
|
||||
action: () => {
|
||||
if (ctxMenu.habit) habitsStore.deleteHabit(ctxMenu.habit.id);
|
||||
const target = ctxMenu.state.target;
|
||||
if (target) habitsStore.deleteHabit(target.id);
|
||||
},
|
||||
},
|
||||
]
|
||||
|
|
@ -175,7 +168,7 @@
|
|||
class:over-target={overTarget}
|
||||
class:pulse={animatingId === habit.id}
|
||||
onclick={() => handleTap(habit.id)}
|
||||
oncontextmenu={(e) => handleItemContextMenu(e, habit)}
|
||||
oncontextmenu={(e) => ctxMenu.open(e, habit)}
|
||||
>
|
||||
<span class="tally-icon">
|
||||
<DynamicIcon name={habit.icon} size={20} weight="bold" />
|
||||
|
|
@ -274,11 +267,11 @@
|
|||
{/if}
|
||||
|
||||
<ContextMenu
|
||||
visible={ctxMenu.visible}
|
||||
x={ctxMenu.x}
|
||||
y={ctxMenu.y}
|
||||
visible={ctxMenu.state.visible}
|
||||
x={ctxMenu.state.x}
|
||||
y={ctxMenu.state.y}
|
||||
items={ctxMenuItems}
|
||||
onClose={() => (ctxMenu = { ...ctxMenu, visible: false, habit: null })}
|
||||
onClose={ctxMenu.close}
|
||||
/>
|
||||
|
||||
{#if activeHabits.length === 0 && !showCreate}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
import type { LocalMood } from './types';
|
||||
import { moodsStore } from './stores/moods.svelte';
|
||||
import { ContextMenu, type ContextMenuItem } from '@mana/shared-ui';
|
||||
import { useItemContextMenu } from '$lib/data/item-context-menu.svelte';
|
||||
import { Trash, Power } from '@mana/shared-icons';
|
||||
|
||||
const moodsQuery = useLiveQueryWithDefault(async () => {
|
||||
|
|
@ -27,29 +28,18 @@
|
|||
return `background: linear-gradient(135deg, ${colors.join(', ')})`;
|
||||
}
|
||||
|
||||
// Context menu
|
||||
let ctxMenu = $state<{ visible: boolean; x: number; y: number; mood: LocalMood | null }>({
|
||||
visible: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
mood: null,
|
||||
});
|
||||
|
||||
function handleItemContextMenu(e: MouseEvent, mood: LocalMood) {
|
||||
e.preventDefault();
|
||||
ctxMenu = { visible: true, x: e.clientX, y: e.clientY, mood };
|
||||
}
|
||||
const ctxMenu = useItemContextMenu<LocalMood>();
|
||||
|
||||
let ctxMenuItems = $derived<ContextMenuItem[]>(
|
||||
ctxMenu.mood
|
||||
ctxMenu.state.target
|
||||
? [
|
||||
{
|
||||
id: 'activate',
|
||||
label: activeMoodId === ctxMenu.mood.id ? 'Deaktivieren' : 'Aktivieren',
|
||||
label: activeMoodId === ctxMenu.state.target.id ? 'Deaktivieren' : 'Aktivieren',
|
||||
icon: Power,
|
||||
action: () => {
|
||||
if (ctxMenu.mood)
|
||||
activeMoodId = activeMoodId === ctxMenu.mood.id ? null : ctxMenu.mood.id;
|
||||
const target = ctxMenu.state.target;
|
||||
if (target) activeMoodId = activeMoodId === target.id ? null : target.id;
|
||||
},
|
||||
},
|
||||
{ id: 'div', label: '', type: 'divider' as const },
|
||||
|
|
@ -59,9 +49,10 @@
|
|||
icon: Trash,
|
||||
variant: 'danger' as const,
|
||||
action: () => {
|
||||
if (ctxMenu.mood) {
|
||||
if (activeMoodId === ctxMenu.mood.id) activeMoodId = null;
|
||||
moodsStore.deleteMood(ctxMenu.mood.id);
|
||||
const target = ctxMenu.state.target;
|
||||
if (target) {
|
||||
if (activeMoodId === target.id) activeMoodId = null;
|
||||
moodsStore.deleteMood(target.id);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
@ -96,7 +87,7 @@
|
|||
{#snippet item(mood)}
|
||||
<button
|
||||
onclick={() => (activeMoodId = activeMoodId === mood.id ? null : mood.id)}
|
||||
oncontextmenu={(e) => handleItemContextMenu(e, mood)}
|
||||
oncontextmenu={(e) => ctxMenu.open(e, mood)}
|
||||
class="group flex flex-col items-center gap-1.5 rounded-lg p-2 transition-colors hover:bg-white/5
|
||||
{activeMoodId === mood.id ? 'ring-1 ring-white/30' : ''}"
|
||||
>
|
||||
|
|
@ -107,9 +98,9 @@
|
|||
</BaseListView>
|
||||
|
||||
<ContextMenu
|
||||
visible={ctxMenu.visible}
|
||||
x={ctxMenu.x}
|
||||
y={ctxMenu.y}
|
||||
visible={ctxMenu.state.visible}
|
||||
x={ctxMenu.state.x}
|
||||
y={ctxMenu.state.y}
|
||||
items={ctxMenuItems}
|
||||
onClose={() => (ctxMenu = { ...ctxMenu, visible: false, mood: null })}
|
||||
onClose={ctxMenu.close}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
import type { Note } from './types';
|
||||
import type { ViewProps } from '$lib/app-registry';
|
||||
import { ContextMenu, type ContextMenuItem } from '@mana/shared-ui';
|
||||
import { useItemContextMenu } from '$lib/data/item-context-menu.svelte';
|
||||
import { PencilSimple, Trash, PushPin } from '@mana/shared-icons';
|
||||
import VoiceCaptureBar from '$lib/components/voice/VoiceCaptureBar.svelte';
|
||||
|
||||
|
|
@ -78,36 +79,27 @@
|
|||
await notesStore.togglePin(id);
|
||||
}
|
||||
|
||||
// Context menu
|
||||
let ctxMenu = $state<{ visible: boolean; x: number; y: number; note: Note | null }>({
|
||||
visible: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
note: null,
|
||||
});
|
||||
|
||||
function handleItemContextMenu(e: MouseEvent, note: Note) {
|
||||
e.preventDefault();
|
||||
ctxMenu = { visible: true, x: e.clientX, y: e.clientY, note };
|
||||
}
|
||||
const ctxMenu = useItemContextMenu<Note>();
|
||||
|
||||
let ctxMenuItems = $derived<ContextMenuItem[]>(
|
||||
ctxMenu.note
|
||||
ctxMenu.state.target
|
||||
? [
|
||||
{
|
||||
id: 'edit',
|
||||
label: 'Bearbeiten',
|
||||
icon: PencilSimple,
|
||||
action: () => {
|
||||
if (ctxMenu.note) startEdit(ctxMenu.note);
|
||||
const target = ctxMenu.state.target;
|
||||
if (target) startEdit(target);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'pin',
|
||||
label: ctxMenu.note.isPinned ? 'Lösen' : 'Pinnen',
|
||||
label: ctxMenu.state.target.isPinned ? 'Lösen' : 'Pinnen',
|
||||
icon: PushPin,
|
||||
action: () => {
|
||||
if (ctxMenu.note) notesStore.togglePin(ctxMenu.note.id);
|
||||
const target = ctxMenu.state.target;
|
||||
if (target) notesStore.togglePin(target.id);
|
||||
},
|
||||
},
|
||||
{ id: 'div', label: '', type: 'divider' as const },
|
||||
|
|
@ -117,7 +109,8 @@
|
|||
icon: Trash,
|
||||
variant: 'danger' as const,
|
||||
action: () => {
|
||||
if (ctxMenu.note) handleDelete(ctxMenu.note.id);
|
||||
const target = ctxMenu.state.target;
|
||||
if (target) handleDelete(target.id);
|
||||
},
|
||||
},
|
||||
]
|
||||
|
|
@ -184,7 +177,7 @@
|
|||
<button
|
||||
class="note-item"
|
||||
onclick={() => startEdit(note)}
|
||||
oncontextmenu={(e) => handleItemContextMenu(e, note)}
|
||||
oncontextmenu={(e) => ctxMenu.open(e, note)}
|
||||
>
|
||||
{#if note.color}
|
||||
<span class="color-dot" style="background: {note.color}"></span>
|
||||
|
|
@ -213,11 +206,11 @@
|
|||
{/if}
|
||||
|
||||
<ContextMenu
|
||||
visible={ctxMenu.visible}
|
||||
x={ctxMenu.x}
|
||||
y={ctxMenu.y}
|
||||
visible={ctxMenu.state.visible}
|
||||
x={ctxMenu.state.x}
|
||||
y={ctxMenu.state.y}
|
||||
items={ctxMenuItems}
|
||||
onClose={() => (ctxMenu = { ...ctxMenu, visible: false, note: null })}
|
||||
onClose={ctxMenu.close}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
import type { TagDragData } from '@mana/shared-ui/dnd';
|
||||
import { useAllTags, getTagsByIds } from '@mana/shared-stores';
|
||||
import { addTagId } from '$lib/data/tag-mutations';
|
||||
import { useItemContextMenu } from '$lib/data/item-context-menu.svelte';
|
||||
|
||||
let { navigate, goBack, params }: ViewProps = $props();
|
||||
|
||||
|
|
@ -93,36 +94,27 @@
|
|||
navigate('detail', { placeId, _siblingIds: ids, _siblingKey: 'placeId' });
|
||||
}
|
||||
|
||||
// Context menu
|
||||
let ctxMenu = $state<{ visible: boolean; x: number; y: number; place: LocalPlace | null }>({
|
||||
visible: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
place: null,
|
||||
});
|
||||
|
||||
function handleItemContextMenu(e: MouseEvent, place: LocalPlace) {
|
||||
e.preventDefault();
|
||||
ctxMenu = { visible: true, x: e.clientX, y: e.clientY, place };
|
||||
}
|
||||
const ctxMenu = useItemContextMenu<LocalPlace>();
|
||||
|
||||
let ctxMenuItems = $derived<ContextMenuItem[]>(
|
||||
ctxMenu.place
|
||||
ctxMenu.state.target
|
||||
? [
|
||||
{
|
||||
id: 'open',
|
||||
label: 'Öffnen',
|
||||
icon: PencilSimple,
|
||||
action: () => {
|
||||
if (ctxMenu.place) openDetail(ctxMenu.place.id);
|
||||
const target = ctxMenu.state.target;
|
||||
if (target) openDetail(target.id);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'favorite',
|
||||
label: ctxMenu.place.isFavorite ? 'Favorit entfernen' : 'Als Favorit',
|
||||
label: ctxMenu.state.target.isFavorite ? 'Favorit entfernen' : 'Als Favorit',
|
||||
icon: Star,
|
||||
action: () => {
|
||||
if (ctxMenu.place) placesStore.toggleFavorite(ctxMenu.place.id);
|
||||
const target = ctxMenu.state.target;
|
||||
if (target) placesStore.toggleFavorite(target.id);
|
||||
},
|
||||
},
|
||||
{ id: 'div', label: '', type: 'divider' as const },
|
||||
|
|
@ -132,7 +124,8 @@
|
|||
icon: Trash,
|
||||
variant: 'danger' as const,
|
||||
action: () => {
|
||||
if (ctxMenu.place) placesStore.deletePlace(ctxMenu.place.id);
|
||||
const target = ctxMenu.state.target;
|
||||
if (target) placesStore.deletePlace(target.id);
|
||||
},
|
||||
},
|
||||
]
|
||||
|
|
@ -197,7 +190,7 @@
|
|||
<button
|
||||
class="place-item"
|
||||
onclick={() => openDetail(place.id)}
|
||||
oncontextmenu={(e) => handleItemContextMenu(e, place)}
|
||||
oncontextmenu={(e) => ctxMenu.open(e, place)}
|
||||
use:dropTarget={{
|
||||
accepts: ['tag'],
|
||||
onDrop: (payload) => handleTagDrop(place.id, payload.data as unknown as TagDragData),
|
||||
|
|
@ -244,11 +237,11 @@
|
|||
</div>
|
||||
|
||||
<ContextMenu
|
||||
visible={ctxMenu.visible}
|
||||
x={ctxMenu.x}
|
||||
y={ctxMenu.y}
|
||||
visible={ctxMenu.state.visible}
|
||||
x={ctxMenu.state.x}
|
||||
y={ctxMenu.state.y}
|
||||
items={ctxMenuItems}
|
||||
onClose={() => (ctxMenu = { ...ctxMenu, visible: false, place: null })}
|
||||
onClose={ctxMenu.close}
|
||||
/>
|
||||
|
||||
{#if filtered().length === 0 && !search}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
import type { TagDragData } from '@mana/shared-ui/dnd';
|
||||
import { useAllTags, getTagsByIds } from '@mana/shared-stores';
|
||||
import { addTagId } from '$lib/data/tag-mutations';
|
||||
import { useItemContextMenu } from '$lib/data/item-context-menu.svelte';
|
||||
import VoiceCaptureBar from '$lib/components/voice/VoiceCaptureBar.svelte';
|
||||
|
||||
let { navigate, goBack, params }: ViewProps = $props();
|
||||
|
|
@ -76,41 +77,27 @@
|
|||
await tasksStore.createFromVoice(blob, durationMs, 'de');
|
||||
}
|
||||
|
||||
// Context menu
|
||||
let ctxMenu = $state<{
|
||||
visible: boolean;
|
||||
x: number;
|
||||
y: number;
|
||||
task: import('./types').Task | null;
|
||||
}>({
|
||||
visible: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
task: null,
|
||||
});
|
||||
|
||||
function handleItemContextMenu(e: MouseEvent, task: import('./types').Task) {
|
||||
e.preventDefault();
|
||||
ctxMenu = { visible: true, x: e.clientX, y: e.clientY, task };
|
||||
}
|
||||
const ctxMenu = useItemContextMenu<import('./types').Task>();
|
||||
|
||||
let ctxMenuItems = $derived<ContextMenuItem[]>(
|
||||
ctxMenu.task
|
||||
ctxMenu.state.target
|
||||
? [
|
||||
{
|
||||
id: 'open',
|
||||
label: 'Öffnen',
|
||||
icon: PencilSimple,
|
||||
action: () => {
|
||||
if (ctxMenu.task) navigate('detail', { taskId: ctxMenu.task.id });
|
||||
const target = ctxMenu.state.target;
|
||||
if (target) navigate('detail', { taskId: target.id });
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'complete',
|
||||
label: ctxMenu.task.isCompleted ? 'Wieder öffnen' : 'Erledigen',
|
||||
icon: ctxMenu.task.isCompleted ? ArrowCounterClockwise : Check,
|
||||
label: ctxMenu.state.target.isCompleted ? 'Wieder öffnen' : 'Erledigen',
|
||||
icon: ctxMenu.state.target.isCompleted ? ArrowCounterClockwise : Check,
|
||||
action: () => {
|
||||
if (ctxMenu.task) tasksStore.toggleComplete(ctxMenu.task.id);
|
||||
const target = ctxMenu.state.target;
|
||||
if (target) tasksStore.toggleComplete(target.id);
|
||||
},
|
||||
},
|
||||
{ id: 'div', label: '', type: 'divider' as const },
|
||||
|
|
@ -120,7 +107,8 @@
|
|||
icon: Trash,
|
||||
variant: 'danger' as const,
|
||||
action: () => {
|
||||
if (ctxMenu.task) tasksStore.deleteTask(ctxMenu.task.id);
|
||||
const target = ctxMenu.state.target;
|
||||
if (target) tasksStore.deleteTask(target.id);
|
||||
},
|
||||
},
|
||||
]
|
||||
|
|
@ -186,7 +174,7 @@
|
|||
_siblingIds: filtered().map((t) => t.id),
|
||||
_siblingKey: 'taskId',
|
||||
})}
|
||||
oncontextmenu={(e) => handleItemContextMenu(e, task)}
|
||||
oncontextmenu={(e) => ctxMenu.open(e, task)}
|
||||
class="task-item"
|
||||
use:dragSource={{
|
||||
type: 'task',
|
||||
|
|
@ -239,11 +227,11 @@
|
|||
</div>
|
||||
|
||||
<ContextMenu
|
||||
visible={ctxMenu.visible}
|
||||
x={ctxMenu.x}
|
||||
y={ctxMenu.y}
|
||||
visible={ctxMenu.state.visible}
|
||||
x={ctxMenu.state.x}
|
||||
y={ctxMenu.state.y}
|
||||
items={ctxMenuItems}
|
||||
onClose={() => (ctxMenu = { ...ctxMenu, visible: false, task: null })}
|
||||
onClose={ctxMenu.close}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
import { dndzone, SOURCES, TRIGGERS } from 'svelte-dnd-action';
|
||||
import { flip } from 'svelte/animate';
|
||||
import { ContextMenu, type ContextMenuItem } from '@mana/shared-ui';
|
||||
import { useItemContextMenu } from '$lib/data/item-context-menu.svelte';
|
||||
import { PencilSimple, Check, ArrowCounterClockwise, Trash, Circle } from '@mana/shared-icons';
|
||||
|
||||
interface Props {
|
||||
|
|
@ -24,35 +25,31 @@
|
|||
items = [...tasks];
|
||||
});
|
||||
|
||||
// Context menu
|
||||
let ctxMenu = $state<{ visible: boolean; x: number; y: number; task: Task | null }>({
|
||||
visible: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
task: null,
|
||||
});
|
||||
const ctxMenu = useItemContextMenu<Task>();
|
||||
|
||||
function handleContextMenu(task: Task, e: MouseEvent) {
|
||||
ctxMenu = { visible: true, x: e.clientX, y: e.clientY, task };
|
||||
ctxMenu.open(e, task);
|
||||
}
|
||||
|
||||
let ctxMenuItems = $derived<ContextMenuItem[]>(
|
||||
ctxMenu.task
|
||||
ctxMenu.state.target
|
||||
? [
|
||||
{
|
||||
id: 'open',
|
||||
label: $_('todo.edit'),
|
||||
icon: PencilSimple,
|
||||
action: () => {
|
||||
if (ctxMenu.task) onOpenTask(ctxMenu.task);
|
||||
const target = ctxMenu.state.target;
|
||||
if (target) onOpenTask(target);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'complete',
|
||||
label: ctxMenu.task.isCompleted ? $_('todo.reopen') : $_('todo.markDone'),
|
||||
icon: ctxMenu.task.isCompleted ? ArrowCounterClockwise : Check,
|
||||
label: ctxMenu.state.target.isCompleted ? $_('todo.reopen') : $_('todo.markDone'),
|
||||
icon: ctxMenu.state.target.isCompleted ? ArrowCounterClockwise : Check,
|
||||
action: () => {
|
||||
if (ctxMenu.task) tasksStore.toggleComplete(ctxMenu.task.id);
|
||||
const target = ctxMenu.state.target;
|
||||
if (target) tasksStore.toggleComplete(target.id);
|
||||
},
|
||||
},
|
||||
{ id: 'div-priority', label: '', type: 'divider' as const },
|
||||
|
|
@ -68,7 +65,8 @@
|
|||
: $_('todo.priorityLow'),
|
||||
icon: Circle,
|
||||
action: () => {
|
||||
if (ctxMenu.task) tasksStore.updateTask(ctxMenu.task.id, { priority: p });
|
||||
const target = ctxMenu.state.target;
|
||||
if (target) tasksStore.updateTask(target.id, { priority: p });
|
||||
},
|
||||
})),
|
||||
{ id: 'div-delete', label: '', type: 'divider' as const },
|
||||
|
|
@ -78,7 +76,8 @@
|
|||
icon: Trash,
|
||||
variant: 'danger' as const,
|
||||
action: () => {
|
||||
if (ctxMenu.task) tasksStore.deleteTask(ctxMenu.task.id);
|
||||
const target = ctxMenu.state.target;
|
||||
if (target) tasksStore.deleteTask(target.id);
|
||||
},
|
||||
},
|
||||
]
|
||||
|
|
@ -143,9 +142,9 @@
|
|||
{/if}
|
||||
|
||||
<ContextMenu
|
||||
visible={ctxMenu.visible}
|
||||
x={ctxMenu.x}
|
||||
y={ctxMenu.y}
|
||||
visible={ctxMenu.state.visible}
|
||||
x={ctxMenu.state.x}
|
||||
y={ctxMenu.state.y}
|
||||
items={ctxMenuItems}
|
||||
onClose={() => (ctxMenu = { ...ctxMenu, visible: false, task: null })}
|
||||
onClose={ctxMenu.close}
|
||||
/>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue