From 04fcbd15c9f63f4102a9c47633fd5565ffc55fec Mon Sep 17 00:00:00 2001 From: Till JS Date: Thu, 2 Apr 2026 14:24:19 +0200 Subject: [PATCH] feat(shared-ui): add TagChip component and tag component tests Add compact inline TagChip for list items/cards (smaller than TagBadge). Set up vitest with jsdom for shared-ui package and add 44 tests covering TagChip, TagBadge, TagColorPicker, TagSelector, and constants. Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/manacore/apps/web/src/hooks.server.ts | 47 +- .../components/QuickEventPopover.svelte | 417 +++- .../apps/web/src/routes/(app)/+layout.svelte | 6 +- .../src/routes/(app)/calendar/+page.svelte | 15 +- packages/shared-ui/package.json | 11 +- packages/shared-ui/src/index.ts | 1 + packages/shared-ui/src/molecules/index.ts | 1 + .../src/molecules/tags/TagBadge.test.ts | 92 + .../src/molecules/tags/TagChip.svelte | 26 + .../src/molecules/tags/TagChip.test.ts | 32 + .../src/molecules/tags/TagColorPicker.test.ts | 76 + .../src/molecules/tags/TagSelector.test.ts | 147 ++ .../src/molecules/tags/constants.test.ts | 56 + .../shared-ui/src/molecules/tags/index.ts | 1 + packages/shared-ui/src/test/setup.ts | 5 + packages/shared-ui/tsconfig.json | 2 +- packages/shared-ui/vitest.config.ts | 18 + pnpm-lock.yaml | 2168 +++++++++-------- 18 files changed, 2017 insertions(+), 1104 deletions(-) create mode 100644 packages/shared-ui/src/molecules/tags/TagBadge.test.ts create mode 100644 packages/shared-ui/src/molecules/tags/TagChip.svelte create mode 100644 packages/shared-ui/src/molecules/tags/TagChip.test.ts create mode 100644 packages/shared-ui/src/molecules/tags/TagColorPicker.test.ts create mode 100644 packages/shared-ui/src/molecules/tags/TagSelector.test.ts create mode 100644 packages/shared-ui/src/molecules/tags/constants.test.ts create mode 100644 packages/shared-ui/src/test/setup.ts create mode 100644 packages/shared-ui/vitest.config.ts diff --git a/apps/manacore/apps/web/src/hooks.server.ts b/apps/manacore/apps/web/src/hooks.server.ts index 311882195..fdace0d5d 100644 --- a/apps/manacore/apps/web/src/hooks.server.ts +++ b/apps/manacore/apps/web/src/hooks.server.ts @@ -25,6 +25,30 @@ const PUBLIC_CONTACTS_API_URL_CLIENT = process.env.PUBLIC_CONTACTS_API_URL_CLIENT || process.env.PUBLIC_CONTACTS_API_URL || ''; const PUBLIC_GLITCHTIP_DSN = process.env.PUBLIC_GLITCHTIP_DSN || ''; +// Sync server URL (WebSocket) +const PUBLIC_SYNC_SERVER_URL_CLIENT = + process.env.PUBLIC_SYNC_SERVER_URL_CLIENT || process.env.PUBLIC_SYNC_SERVER_URL || ''; + +// Additional backend URLs +const PUBLIC_CHAT_API_URL_CLIENT = + process.env.PUBLIC_CHAT_API_URL_CLIENT || process.env.PUBLIC_CHAT_API_URL || ''; +const PUBLIC_STORAGE_API_URL_CLIENT = + process.env.PUBLIC_STORAGE_API_URL_CLIENT || process.env.PUBLIC_STORAGE_API_URL || ''; +const PUBLIC_CARDS_API_URL_CLIENT = + process.env.PUBLIC_CARDS_API_URL_CLIENT || process.env.PUBLIC_CARDS_API_URL || ''; +const PUBLIC_MUKKE_API_URL_CLIENT = + process.env.PUBLIC_MUKKE_API_URL_CLIENT || process.env.PUBLIC_MUKKE_API_URL || ''; +const PUBLIC_NUTRIPHI_API_URL_CLIENT = + process.env.PUBLIC_NUTRIPHI_API_URL_CLIENT || process.env.PUBLIC_NUTRIPHI_API_URL || ''; +const PUBLIC_ULOAD_SERVER_URL_CLIENT = + process.env.PUBLIC_ULOAD_SERVER_URL_CLIENT || process.env.PUBLIC_ULOAD_SERVER_URL || ''; +const PUBLIC_MEMORO_SERVER_URL_CLIENT = + process.env.PUBLIC_MEMORO_SERVER_URL_CLIENT || process.env.PUBLIC_MEMORO_SERVER_URL || ''; +const PUBLIC_MANA_MEDIA_URL_CLIENT = + process.env.PUBLIC_MANA_MEDIA_URL_CLIENT || process.env.PUBLIC_MANA_MEDIA_URL || ''; +const PUBLIC_MANA_LLM_URL_CLIENT = + process.env.PUBLIC_MANA_LLM_URL_CLIENT || process.env.PUBLIC_MANA_LLM_URL || ''; + // Map of app subdomains to internal paths const APP_SUBDOMAINS = new Set([ 'todo', @@ -72,6 +96,16 @@ window.__PUBLIC_TODO_API_URL__ = ${JSON.stringify(PUBLIC_TODO_API_URL_CLIENT)}; window.__PUBLIC_CALENDAR_API_URL__ = ${JSON.stringify(PUBLIC_CALENDAR_API_URL_CLIENT)}; window.__PUBLIC_CLOCK_API_URL__ = ${JSON.stringify(PUBLIC_CLOCK_API_URL_CLIENT)}; window.__PUBLIC_CONTACTS_API_URL__ = ${JSON.stringify(PUBLIC_CONTACTS_API_URL_CLIENT)}; +window.__PUBLIC_SYNC_SERVER_URL__ = ${JSON.stringify(PUBLIC_SYNC_SERVER_URL_CLIENT)}; +window.__PUBLIC_CHAT_API_URL__ = ${JSON.stringify(PUBLIC_CHAT_API_URL_CLIENT)}; +window.__PUBLIC_STORAGE_API_URL__ = ${JSON.stringify(PUBLIC_STORAGE_API_URL_CLIENT)}; +window.__PUBLIC_CARDS_API_URL__ = ${JSON.stringify(PUBLIC_CARDS_API_URL_CLIENT)}; +window.__PUBLIC_MUKKE_API_URL__ = ${JSON.stringify(PUBLIC_MUKKE_API_URL_CLIENT)}; +window.__PUBLIC_NUTRIPHI_API_URL__ = ${JSON.stringify(PUBLIC_NUTRIPHI_API_URL_CLIENT)}; +window.__PUBLIC_ULOAD_SERVER_URL__ = ${JSON.stringify(PUBLIC_ULOAD_SERVER_URL_CLIENT)}; +window.__PUBLIC_MEMORO_SERVER_URL__ = ${JSON.stringify(PUBLIC_MEMORO_SERVER_URL_CLIENT)}; +window.__PUBLIC_MANA_MEDIA_URL__ = ${JSON.stringify(PUBLIC_MANA_MEDIA_URL_CLIENT)}; +window.__PUBLIC_MANA_LLM_URL__ = ${JSON.stringify(PUBLIC_MANA_LLM_URL_CLIENT)}; window.__PUBLIC_GLITCHTIP_DSN__ = ${JSON.stringify(PUBLIC_GLITCHTIP_DSN)}; `; return injectUmamiAnalytics(html.replace('', `${envScript}`)); @@ -85,7 +119,18 @@ window.__PUBLIC_GLITCHTIP_DSN__ = ${JSON.stringify(PUBLIC_GLITCHTIP_DSN)}; PUBLIC_CALENDAR_API_URL_CLIENT, PUBLIC_CLOCK_API_URL_CLIENT, PUBLIC_CONTACTS_API_URL_CLIENT, - ], + PUBLIC_SYNC_SERVER_URL_CLIENT, + PUBLIC_CHAT_API_URL_CLIENT, + PUBLIC_STORAGE_API_URL_CLIENT, + PUBLIC_CARDS_API_URL_CLIENT, + PUBLIC_MUKKE_API_URL_CLIENT, + PUBLIC_NUTRIPHI_API_URL_CLIENT, + PUBLIC_ULOAD_SERVER_URL_CLIENT, + PUBLIC_MEMORO_SERVER_URL_CLIENT, + PUBLIC_MANA_MEDIA_URL_CLIENT, + PUBLIC_MANA_LLM_URL_CLIENT, + 'wss://sync.mana.how', + ].filter(Boolean), }); return response; diff --git a/apps/manacore/apps/web/src/lib/modules/calendar/components/QuickEventPopover.svelte b/apps/manacore/apps/web/src/lib/modules/calendar/components/QuickEventPopover.svelte index 7b9d2b219..4128ce030 100644 --- a/apps/manacore/apps/web/src/lib/modules/calendar/components/QuickEventPopover.svelte +++ b/apps/manacore/apps/web/src/lib/modules/calendar/components/QuickEventPopover.svelte @@ -2,9 +2,16 @@ import { getContext, onMount, tick } from 'svelte'; import { getDefaultCalendar, getCalendarColor } from '../queries'; import type { Calendar } from '../types'; - import { format } from 'date-fns'; + import { format, addMinutes } from 'date-fns'; import { de } from 'date-fns/locale'; - import { X } from '@manacore/shared-icons'; + import { + X, + Clock, + CalendarBlank, + MapPin, + ArrowsClockwise, + TextAlignLeft, + } from '@manacore/shared-icons'; interface Props { startTime: Date; @@ -17,48 +24,76 @@ endTime: string; isAllDay: boolean; location: string | null; + description: string | null; + recurrenceRule: string | null; }) => void; onClose: () => void; - onExpand?: () => void; } - let { startTime, endTime, position, onSave, onClose, onExpand }: Props = $props(); + let { startTime, endTime, position, onSave, onClose }: Props = $props(); const calendarsCtx: { readonly value: Calendar[] } = getContext('calendars'); let title = $state(''); let location = $state(''); + let description = $state(''); + let isAllDay = $state(false); + let recurrenceRule = $state(null); + let startDateStr = $state(format(startTime, 'yyyy-MM-dd')); + let startTimeStr = $state(format(startTime, 'HH:mm')); + let endDateStr = $state(format(endTime, 'yyyy-MM-dd')); + let endTimeStr = $state(format(endTime, 'HH:mm')); + let titleInput: HTMLInputElement; let popoverEl: HTMLDivElement; - - // Calculated popover position (adjusted to stay in viewport) let popoverPos = $state({ top: 0, left: 0 }); const defaultCalendar = $derived(getDefaultCalendar(calendarsCtx.value)); - const calendarColor = $derived(getCalendarColor(calendarsCtx.value, defaultCalendar?.id || '')); + let calendarId = $state(''); - const timeLabel = $derived( - `${format(startTime, 'EE d. MMM', { locale: de })} ${format(startTime, 'HH:mm')} – ${format(endTime, 'HH:mm')}` - ); + $effect(() => { + if (defaultCalendar && !calendarId) { + calendarId = defaultCalendar.id; + } + }); - function handleSubmit() { + const calendarColor = $derived(getCalendarColor(calendarsCtx.value, calendarId || '')); + + const RECURRENCE_OPTIONS = [ + { value: '', label: 'Keine Wiederholung' }, + { value: 'FREQ=DAILY', label: 'Täglich' }, + { value: 'FREQ=WEEKLY', label: 'Wöchentlich' }, + { value: 'FREQ=WEEKLY;INTERVAL=2', label: 'Alle 2 Wochen' }, + { value: 'FREQ=MONTHLY', label: 'Monatlich' }, + { value: 'FREQ=YEARLY', label: 'Jährlich' }, + ]; + + function handleSubmit(e: Event) { + e.preventDefault(); if (!title.trim()) return; + + const start = isAllDay + ? new Date(`${startDateStr}T00:00:00`) + : new Date(`${startDateStr}T${startTimeStr}`); + const end = isAllDay + ? new Date(`${endDateStr}T23:59:59`) + : new Date(`${endDateStr}T${endTimeStr}`); + onSave({ title: title.trim(), - calendarId: defaultCalendar?.id || '', - startTime: startTime.toISOString(), - endTime: endTime.toISOString(), - isAllDay: false, + calendarId, + startTime: start.toISOString(), + endTime: end.toISOString(), + isAllDay, location: location.trim() || null, + description: description.trim() || null, + recurrenceRule: recurrenceRule || null, }); } function handleKeydown(e: KeyboardEvent) { if (e.key === 'Escape') { onClose(); - } else if (e.key === 'Enter' && !e.shiftKey) { - e.preventDefault(); - handleSubmit(); } } @@ -70,9 +105,8 @@ const vh = window.innerHeight; let left = position.x + 12; - let top = position.y - rect.height / 2; + let top = position.y - 100; - // Keep in viewport if (left + rect.width > vw - 16) left = position.x - rect.width - 12; if (left < 16) left = 16; if (top < 16) top = 16; @@ -86,7 +120,7 @@ - +
@@ -101,43 +135,120 @@
-
- - - - -
- {timeLabel} +
+ +
+ Neuer Termin +
- - + +
+ + - -
- {#if onExpand} - + + {#if calendarsCtx.value.length > 1} +
+ {#each calendarsCtx.value as cal (cal.id)} + + {/each} +
{/if} -
- -
-
+ + +
+ + +
+