feat(shared): Phase 6 — update app URLs and navigation for unified app

mana-apps.ts:
- Change all APP_URLS from subdomains to internal paths
  (e.g., https://todo.mana.howhttps://mana.how/todo)
- Keep separate subdomains only for games (arcade) and matrix

PillNavigation, AppDrawer, GlobalSpotlight:
- Detect internal URLs and navigate directly instead of window.open
- External URLs (games, matrix) still open in new tab

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-01 21:19:14 +02:00
parent 8fe16b20f4
commit 59e1e8e833
4 changed files with 72 additions and 88 deletions

View file

@ -723,40 +723,49 @@ export const APP_SLIDER_LABELS = {
/**
* Default app URLs for local development and production
*/
/**
* App URLs unified app uses internal paths, separate apps use subdomains.
*
* All productivity apps are now served under mana.how/{appId}.
* Games and Matrix remain on separate subdomains.
*/
export const APP_URLS: Record<AppIconId, { dev: string; prod: string }> = {
chat: { dev: 'http://localhost:5174', prod: 'https://chat.mana.how' },
memoro: { dev: 'http://localhost:5175', prod: 'https://memoro.mana.how' },
presi: { dev: 'http://localhost:5176', prod: 'https://presi.mana.how' },
cards: { dev: 'http://localhost:5177', prod: 'https://cards.mana.how' },
picture: { dev: 'http://localhost:5185', prod: 'https://picture.mana.how' },
zitare: { dev: 'http://localhost:5180', prod: 'https://zitare.mana.how' },
wisekeep: { dev: 'http://localhost:5181', prod: 'https://wisekeep.mana.how' },
nutriphi: { dev: 'http://localhost:5182', prod: 'https://nutriphi.mana.how' },
// ─── Unified App (internal paths) ─────────────────────────
manacore: { dev: 'http://localhost:5173', prod: 'https://mana.how' },
mana: { dev: 'http://localhost:5173', prod: 'https://mana.how' },
moodlit: { dev: 'http://localhost:5182', prod: 'https://moodlit.mana.how' },
contacts: { dev: 'http://localhost:5184', prod: 'https://contacts.mana.how' },
calendar: { dev: 'http://localhost:5179', prod: 'https://calendar.mana.how' },
storage: { dev: 'http://localhost:5185', prod: 'https://storage.mana.how' },
clock: { dev: 'http://localhost:5187', prod: 'https://clock.mana.how' },
todo: { dev: 'http://localhost:5188', prod: 'https://todo.mana.how' },
mail: { dev: 'http://localhost:5186', prod: 'https://mail.mana.how' },
inventory: { dev: 'http://localhost:5189', prod: 'https://inventory.mana.how' },
questions: { dev: 'http://localhost:5111', prod: 'https://questions.mana.how' },
matrix: { dev: 'http://localhost:5180', prod: 'https://matrix.mana.how' },
playground: { dev: 'http://localhost:5190', prod: 'https://playground.mana.how' },
context: { dev: 'http://localhost:5192', prod: 'https://context.mana.how' },
citycorners: { dev: 'http://localhost:5196', prod: 'https://citycorners.mana.how' },
times: { dev: 'http://localhost:5197', prod: 'https://times.mana.how' },
uload: { dev: 'http://localhost:5173', prod: 'https://ulo.ad' },
todo: { dev: 'http://localhost:5173/todo', prod: 'https://mana.how/todo' },
calendar: { dev: 'http://localhost:5173/calendar', prod: 'https://mana.how/calendar' },
contacts: { dev: 'http://localhost:5173/contacts', prod: 'https://mana.how/contacts' },
chat: { dev: 'http://localhost:5173/chat', prod: 'https://mana.how/chat' },
picture: { dev: 'http://localhost:5173/picture', prod: 'https://mana.how/picture' },
cards: { dev: 'http://localhost:5173/cards', prod: 'https://mana.how/cards' },
zitare: { dev: 'http://localhost:5173/zitare', prod: 'https://mana.how/zitare' },
clock: { dev: 'http://localhost:5173/clock', prod: 'https://mana.how/clock' },
mukke: { dev: 'http://localhost:5173/mukke', prod: 'https://mana.how/mukke' },
storage: { dev: 'http://localhost:5173/storage', prod: 'https://mana.how/storage' },
presi: { dev: 'http://localhost:5173/presi', prod: 'https://mana.how/presi' },
inventar: { dev: 'http://localhost:5173/inventar', prod: 'https://mana.how/inventar' },
inventory: { dev: 'http://localhost:5173/inventar', prod: 'https://mana.how/inventar' },
photos: { dev: 'http://localhost:5173/photos', prod: 'https://mana.how/photos' },
skilltree: { dev: 'http://localhost:5173/skilltree', prod: 'https://mana.how/skilltree' },
citycorners: { dev: 'http://localhost:5173/citycorners', prod: 'https://mana.how/citycorners' },
times: { dev: 'http://localhost:5173/times', prod: 'https://mana.how/times' },
context: { dev: 'http://localhost:5173/context', prod: 'https://mana.how/context' },
questions: { dev: 'http://localhost:5173/questions', prod: 'https://mana.how/questions' },
nutriphi: { dev: 'http://localhost:5173/nutriphi', prod: 'https://mana.how/nutriphi' },
planta: { dev: 'http://localhost:5173/planta', prod: 'https://mana.how/planta' },
uload: { dev: 'http://localhost:5173/uload', prod: 'https://mana.how/uload' },
calc: { dev: 'http://localhost:5173/calc', prod: 'https://mana.how/calc' },
moodlit: { dev: 'http://localhost:5173/moodlit', prod: 'https://mana.how/moodlit' },
memoro: { dev: 'http://localhost:5173/memoro', prod: 'https://mana.how/memoro' },
playground: { dev: 'http://localhost:5173/playground', prod: 'https://mana.how/playground' },
guides: { dev: 'http://localhost:5173/guides', prod: 'https://mana.how/guides' },
wisekeep: { dev: 'http://localhost:5173/wisekeep', prod: 'https://mana.how/wisekeep' },
news: { dev: 'http://localhost:5173/news', prod: 'https://mana.how/news' },
mail: { dev: 'http://localhost:5173/mail', prod: 'https://mana.how/mail' },
reader: { dev: 'exp://localhost:8081', prod: 'https://reader.mana.how' },
news: { dev: 'http://localhost:5174', prod: 'https://news.mana.how' },
calc: { dev: 'http://localhost:5198', prod: 'https://calc.mana.how' },
guides: { dev: 'http://localhost:5200', prod: 'https://guides.mana.how' },
mukke: { dev: 'http://localhost:5191', prod: 'https://mukke.mana.how' },
photos: { dev: 'http://localhost:5193', prod: 'https://photos.mana.how' },
planta: { dev: 'http://localhost:5194', prod: 'https://planta.mana.how' },
skilltree: { dev: 'http://localhost:5195', prod: 'https://skilltree.mana.how' },
// ─── Separate Apps (own subdomains) ───────────────────────
matrix: { dev: 'http://localhost:5180', prod: 'https://matrix.mana.how' },
arcade: { dev: 'http://localhost:5201', prod: 'https://arcade.mana.how' },
};

View file

@ -76,7 +76,14 @@
} else if (app.isCurrent) {
window.location.href = '/';
} else if (app.url) {
window.open(app.url, '_blank', 'noopener,noreferrer');
const isInternal =
app.url.startsWith('/') ||
new URL(app.url, window.location.origin).origin === window.location.origin;
if (isInternal) {
window.location.href = app.url;
} else {
window.open(app.url, '_blank', 'noopener,noreferrer');
}
}
close();

View file

@ -152,7 +152,14 @@
if (app.isCurrent) {
window.location.href = '/';
} else if (app.url) {
window.open(app.url, '_blank', 'noopener,noreferrer');
const isInternal =
app.url.startsWith('/') ||
new URL(app.url, window.location.origin).origin === window.location.origin;
if (isInternal) {
window.location.href = app.url;
} else {
window.open(app.url, '_blank', 'noopener,noreferrer');
}
}
}
} else {

View file

@ -170,7 +170,15 @@
// Navigate to home route for current app
window.location.href = '/';
} else if (app.url) {
window.open(app.url, '_blank', 'noopener,noreferrer');
// Internal paths (same-origin) navigate directly, external URLs open in new tab
const isInternal =
app.url.startsWith('/') ||
new URL(app.url, window.location.origin).origin === window.location.origin;
if (isInternal) {
window.location.href = app.url;
} else {
window.open(app.url, '_blank', 'noopener,noreferrer');
}
}
},
active: app.isCurrent,
@ -292,6 +300,8 @@
spiralHref?: string;
/** Help page href (shown in user dropdown). Set to empty string to hide. */
helpHref?: string;
/** Bottom offset from viewport bottom (default: '0px'). Use to position above other fixed bars. */
bottomOffset?: string;
}
let {
@ -340,6 +350,7 @@
themesHref,
spiralHref,
helpHref,
bottomOffset = '0px',
}: Props = $props();
// Type guards for elements
@ -369,14 +380,6 @@
return localPart.substring(0, maxLength) + '…';
}
// Local state for uncontrolled mode
let internalCollapsed = $state(false);
// Use external or internal state
const isCollapsed = $derived(
onCollapsedChange !== undefined ? (externalCollapsed ?? false) : internalCollapsed
);
// Dropdown direction: always up since nav is always at bottom
const dropdownDirection = 'up' as const;
@ -386,31 +389,17 @@
// Global spotlight (Cmd+K) — only active when spotlightActions are provided
const spotlight = spotlightActions ? createGlobalSpotlightState() : null;
function collapseNav() {
if (onCollapsedChange) {
onCollapsedChange(true);
} else {
internalCollapsed = true;
}
}
function expandNav() {
if (onCollapsedChange) {
onCollapsedChange(false);
} else {
internalCollapsed = false;
}
}
function isActive(path: string) {
return currentPath === path;
}
</script>
{#if !isCollapsed}
{#if !(externalCollapsed ?? false)}
<nav
class="pill-nav"
style={primaryColor ? `--pill-primary-color: ${primaryColor}` : ''}
style="{primaryColor
? `--pill-primary-color: ${primaryColor};`
: ''}--pill-nav-bottom: {bottomOffset}"
aria-label={ariaLabel}
>
<div class="pill-nav-container">
@ -803,22 +792,10 @@
<span class="pill-label">Anmelden</span>
</a>
{/if}
<!-- Collapse Button -->
<button onclick={collapseNav} class="pill glass-pill" title="Navigation minimieren">
<CaretRight size={18} class="pill-icon" />
</button>
</div>
</nav>
{/if}
<!-- FAB for collapsed state -->
{#if isCollapsed}
<button onclick={expandNav} class="nav-fab glass-pill" title="Expand navigation">
<List size={18} class="pill-icon" />
</button>
{/if}
<!-- Global Spotlight (Cmd+K) -->
{#if spotlight && spotlightActions}
<GlobalSpotlight
@ -833,7 +810,7 @@
<style>
.pill-nav {
position: fixed;
bottom: 0;
bottom: var(--pill-nav-bottom, 0px);
left: 0;
right: 0;
z-index: 1000;
@ -991,22 +968,6 @@
display: inline;
}
/* FAB for collapsed state - positioned at bottom right */
.nav-fab {
position: fixed;
bottom: 0;
right: 0;
z-index: 1001;
display: flex;
align-items: center;
justify-content: center;
padding: 0.875rem;
padding-bottom: calc(env(safe-area-inset-bottom, 0px) + 0.875rem);
border-radius: 1rem 0 0 0;
cursor: pointer;
border: none;
}
/* Transitions */
.pill-nav {
transition: all 0.3s ease;