From 4cee74e15da3ffbf710ed6fbee7ad3428ce4d3b7 Mon Sep 17 00:00:00 2001 From: Till JS Date: Mon, 30 Mar 2026 12:14:29 +0200 Subject: [PATCH] feat(shared-links): deep-link navigation to target app detail views ManaLinkBadge now resolves the correct URL for the linked record's detail view (e.g. /event/{id}, /contacts/{id}, /deck/{id}) instead of just linking to the app's root page. Uses an anchor tag by default for standard browser navigation, with onclick prop override for custom behavior. Supports all 12 apps with their specific routing patterns. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/shared-links/src/deep-links.ts | 80 +++++++++++++++++++ packages/shared-links/src/index.ts | 3 + .../shared-links/src/ui/ManaLinkBadge.svelte | 42 +++++++--- 3 files changed, 114 insertions(+), 11 deletions(-) create mode 100644 packages/shared-links/src/deep-links.ts diff --git a/packages/shared-links/src/deep-links.ts b/packages/shared-links/src/deep-links.ts new file mode 100644 index 000000000..d8241e031 --- /dev/null +++ b/packages/shared-links/src/deep-links.ts @@ -0,0 +1,80 @@ +/** + * ManaLink — Deep-Link Resolution + * + * Maps app + collection + recordId to a full URL that opens + * the record's detail view in the target app. + */ + +import { APP_URLS } from '@manacore/shared-branding'; +import type { AppIconId } from '@manacore/shared-branding'; + +/** Route pattern per app and collection. Use {id} as placeholder. */ +const DEEP_LINK_PATTERNS: Record> = { + todo: { + // Todo uses inline editing, no detail route — link to app root + tasks: '/', + projects: '/', + }, + calendar: { + events: '/event/{id}', + calendars: '/', + }, + contacts: { + contacts: '/contacts/{id}', + }, + chat: { + conversations: '/chat/{id}', + messages: '/chat/{id}', // Navigate to conversation, not individual message + }, + picture: { + images: '/app/board/{id}', + boards: '/app/board/{id}', + }, + storage: { + files: '/', + folders: '/files/{id}', + }, + presi: { + decks: '/deck/{id}', + slides: '/deck/{id}', // Navigate to the deck containing the slide + }, + context: { + documents: '/documents/{id}', + spaces: '/spaces/{id}', + }, + manadeck: { + decks: '/decks/{id}', + cards: '/decks/{id}', // Navigate to deck containing the card + }, + mukke: { + songs: '/', + playlists: '/playlists/{id}', + }, + clock: { + alarms: '/', + timers: '/', + }, + zitare: { + favorites: '/', + }, +}; + +/** + * Resolve a deep link URL for a cross-app record. + * + * @param app - App ID (e.g. 'todo', 'calendar') + * @param collection - Collection name (e.g. 'tasks', 'events') + * @param recordId - Record UUID + * @returns Full URL to the record's detail view, or app root as fallback + */ +export function resolveDeepLink(app: string, collection: string, recordId: string): string { + const isDev = typeof window !== 'undefined' && window.location.hostname === 'localhost'; + const urls = APP_URLS[app as AppIconId]; + if (!urls) return '#'; + + const baseUrl = isDev ? urls.dev : urls.prod; + const pattern = DEEP_LINK_PATTERNS[app]?.[collection] ?? '/'; + const path = pattern.replace('{id}', recordId); + + return `${baseUrl}${path}`; +} diff --git a/packages/shared-links/src/index.ts b/packages/shared-links/src/index.ts index 7cd6b12de..bf5a8dcb1 100644 --- a/packages/shared-links/src/index.ts +++ b/packages/shared-links/src/index.ts @@ -24,3 +24,6 @@ export { // Resolvers export { buildCachedData, isCacheStale } from './resolvers.js'; + +// Deep Links +export { resolveDeepLink } from './deep-links.js'; diff --git a/packages/shared-links/src/ui/ManaLinkBadge.svelte b/packages/shared-links/src/ui/ManaLinkBadge.svelte index 3661de05a..2de8744c8 100644 --- a/packages/shared-links/src/ui/ManaLinkBadge.svelte +++ b/packages/shared-links/src/ui/ManaLinkBadge.svelte @@ -2,6 +2,7 @@ import type { LocalManaLink } from '../types.js'; import { getManaApp } from '@manacore/shared-branding'; import type { AppIconId } from '@manacore/shared-branding'; + import { resolveDeepLink } from '../deep-links.js'; interface Props { link: LocalManaLink; @@ -17,20 +18,37 @@ ); let displayColor = $derived(link.cachedTarget?.color ?? targetApp?.color ?? '#6b7280'); let displayAppName = $derived(link.cachedTarget?.appName ?? targetApp?.name ?? link.targetApp); + let deepLinkUrl = $derived(resolveDeepLink(link.targetApp, link.targetCollection, link.targetId)); - + {#if onclick} + + {:else} + + + {displayTitle} + {#if link.cachedTarget?.subtitle} + {link.cachedTarget.subtitle} + {/if} + + {/if} {#if onRemove}