refactor(todo): rename Labels to Tags for consistency across apps

- Rename route /labels to /tags and /label/[id] to /tag/[id]
- Rename LabelSelector component to TagSelector
- Update all UI texts from "Labels" to "Tags"
- Update navigation items and references
- Align terminology with Calendar and Contacts apps

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Till-JS 2025-12-10 15:21:33 +01:00
parent 6b30a914f4
commit 9f13bba3d0
29 changed files with 1964 additions and 383 deletions

View file

@ -6,13 +6,13 @@
*/
/**
* Route definition with i18n label
* Route definition with label
*/
export interface AppRoute {
/** Route path (e.g., '/stopwatch') */
path: string;
/** i18n key for the label (e.g., 'nav.stopwatch') */
labelKey: string;
/** Display label for the route (e.g., 'Stoppuhr') */
label: string;
/** Optional icon name */
icon?: string;
/** If true, this route cannot be hidden (e.g., Settings, Home) */
@ -39,13 +39,14 @@ export const APP_ROUTES: Record<string, AppRouteConfig> = {
appId: 'clock',
defaultRoute: '/',
availableRoutes: [
{ path: '/', labelKey: 'nav.dashboard', icon: 'home' },
{ path: '/alarms', labelKey: 'nav.alarms', icon: 'alarm' },
{ path: '/timers', labelKey: 'nav.timers', icon: 'timer' },
{ path: '/stopwatch', labelKey: 'nav.stopwatch', icon: 'stopwatch' },
{ path: '/pomodoro', labelKey: 'nav.pomodoro', icon: 'target' },
{ path: '/world-clock', labelKey: 'nav.worldClock', icon: 'globe' },
{ path: '/life', labelKey: 'nav.lifeClock', icon: 'heart' },
{ path: '/', label: 'Dashboard', icon: 'home', alwaysVisible: true },
{ path: '/alarms', label: 'Wecker', icon: 'alarm' },
{ path: '/timers', label: 'Timer', icon: 'timer' },
{ path: '/stopwatch', label: 'Stoppuhr', icon: 'stopwatch' },
{ path: '/pomodoro', label: 'Pomodoro', icon: 'target' },
{ path: '/world-clock', label: 'Weltuhr', icon: 'globe' },
{ path: '/life', label: 'Lebenszeit', icon: 'heart' },
{ path: '/settings', label: 'Einstellungen', icon: 'settings', alwaysVisible: true },
],
},
@ -53,8 +54,11 @@ export const APP_ROUTES: Record<string, AppRouteConfig> = {
appId: 'calendar',
defaultRoute: '/',
availableRoutes: [
{ path: '/', labelKey: 'nav.month', icon: 'calendar' },
{ path: '/agenda', labelKey: 'nav.agenda', icon: 'list' },
{ path: '/', label: 'Kalender', icon: 'calendar', alwaysVisible: true },
{ path: '/agenda', label: 'Agenda', icon: 'list' },
{ path: '/tags', label: 'Tags', icon: 'tag' },
{ path: '/network', label: 'Netzwerk', icon: 'share' },
{ path: '/settings', label: 'Einstellungen', icon: 'settings', alwaysVisible: true },
],
},
@ -62,20 +66,14 @@ export const APP_ROUTES: Record<string, AppRouteConfig> = {
appId: 'contacts',
defaultRoute: '/',
availableRoutes: [
{ path: '/', labelKey: 'nav.contacts', icon: 'users' },
{ path: '/groups', labelKey: 'nav.groups', icon: 'folder' },
{ path: '/favorites', labelKey: 'nav.favorites', icon: 'star' },
],
},
mail: {
appId: 'mail',
defaultRoute: '/',
availableRoutes: [
{ path: '/', labelKey: 'nav.inbox', icon: 'inbox' },
{ path: '/sent', labelKey: 'nav.sent', icon: 'send' },
{ path: '/drafts', labelKey: 'nav.drafts', icon: 'file' },
{ path: '/starred', labelKey: 'nav.starred', icon: 'star' },
{ path: '/', label: 'Kontakte', icon: 'users', alwaysVisible: true },
{ path: '/favorites', label: 'Favoriten', icon: 'star' },
{ path: '/tags', label: 'Tags', icon: 'tag' },
{ path: '/archive', label: 'Archiv', icon: 'archive' },
{ path: '/duplicates', label: 'Duplikate', icon: 'copy' },
{ path: '/data', label: 'Import/Export', icon: 'download' },
{ path: '/network', label: 'Netzwerk', icon: 'share' },
{ path: '/settings', label: 'Einstellungen', icon: 'settings', alwaysVisible: true },
],
},
@ -83,21 +81,12 @@ export const APP_ROUTES: Record<string, AppRouteConfig> = {
appId: 'todo',
defaultRoute: '/',
availableRoutes: [
{ path: '/', labelKey: 'nav.all', icon: 'list' },
{ path: '/today', labelKey: 'nav.today', icon: 'calendar' },
{ path: '/upcoming', labelKey: 'nav.upcoming', icon: 'clock' },
{ path: '/completed', labelKey: 'nav.completed', icon: 'check' },
],
},
storage: {
appId: 'storage',
defaultRoute: '/',
availableRoutes: [
{ path: '/', labelKey: 'nav.home', icon: 'home' },
{ path: '/files', labelKey: 'nav.files', icon: 'folder' },
{ path: '/favorites', labelKey: 'nav.favorites', icon: 'star' },
{ path: '/shared', labelKey: 'nav.shared', icon: 'share' },
{ path: '/', label: 'Aufgaben', icon: 'list', alwaysVisible: true },
{ path: '/kanban', label: 'Kanban', icon: 'grid' },
{ path: '/labels', label: 'Labels', icon: 'tag' },
{ path: '/statistics', label: 'Statistiken', icon: 'chart' },
{ path: '/network', label: 'Netzwerk', icon: 'share' },
{ path: '/settings', label: 'Einstellungen', icon: 'settings', alwaysVisible: true },
],
},
@ -105,10 +94,13 @@ export const APP_ROUTES: Record<string, AppRouteConfig> = {
appId: 'chat',
defaultRoute: '/chat',
availableRoutes: [
{ path: '/chat', labelKey: 'nav.chat', icon: 'message' },
{ path: '/spaces', labelKey: 'nav.spaces', icon: 'folder' },
{ path: '/templates', labelKey: 'nav.templates', icon: 'file' },
{ path: '/documents', labelKey: 'nav.documents', icon: 'document' },
{ path: '/chat', label: 'Chat', icon: 'message', alwaysVisible: true },
{ path: '/templates', label: 'Vorlagen', icon: 'file' },
{ path: '/spaces', label: 'Spaces', icon: 'folder' },
{ path: '/documents', label: 'Dokumente', icon: 'document' },
{ path: '/archive', label: 'Archiv', icon: 'archive' },
{ path: '/feedback', label: 'Feedback', icon: 'chat' },
{ path: '/settings', label: 'Einstellungen', icon: 'settings', alwaysVisible: true },
],
},
@ -116,10 +108,14 @@ export const APP_ROUTES: Record<string, AppRouteConfig> = {
appId: 'picture',
defaultRoute: '/app/gallery',
availableRoutes: [
{ path: '/app/gallery', labelKey: 'nav.gallery', icon: 'image' },
{ path: '/app/generate', labelKey: 'nav.generate', icon: 'sparkle' },
{ path: '/app/board', labelKey: 'nav.board', icon: 'grid' },
{ path: '/app/explore', labelKey: 'nav.explore', icon: 'compass' },
{ path: '/app/gallery', label: 'Galerie', icon: 'image', alwaysVisible: true },
{ path: '/app/board', label: 'Moodboards', icon: 'grid' },
{ path: '/app/explore', label: 'Entdecken', icon: 'compass' },
{ path: '/app/generate', label: 'Generieren', icon: 'sparkle' },
{ path: '/app/upload', label: 'Upload', icon: 'upload' },
{ path: '/app/tags', label: 'Tags', icon: 'tag' },
{ path: '/app/archive', label: 'Archiv', icon: 'archive' },
{ path: '/app/settings', label: 'Einstellungen', icon: 'settings', alwaysVisible: true },
],
},
@ -127,9 +123,10 @@ export const APP_ROUTES: Record<string, AppRouteConfig> = {
appId: 'manadeck',
defaultRoute: '/decks',
availableRoutes: [
{ path: '/decks', labelKey: 'nav.decks', icon: 'layers' },
{ path: '/explore', labelKey: 'nav.explore', icon: 'compass' },
{ path: '/progress', labelKey: 'nav.progress', icon: 'trending' },
{ path: '/decks', label: 'Decks', icon: 'layers', alwaysVisible: true },
{ path: '/explore', label: 'Entdecken', icon: 'compass' },
{ path: '/progress', label: 'Fortschritt', icon: 'trending' },
{ path: '/settings', label: 'Einstellungen', icon: 'settings', alwaysVisible: true },
],
},
@ -137,24 +134,23 @@ export const APP_ROUTES: Record<string, AppRouteConfig> = {
appId: 'zitare',
defaultRoute: '/',
availableRoutes: [
{ path: '/', labelKey: 'nav.home', icon: 'home' },
{ path: '/quotes', labelKey: 'nav.quotes', icon: 'quote' },
{ path: '/favorites', labelKey: 'nav.favorites', icon: 'star' },
{ path: '/authors', labelKey: 'nav.authors', icon: 'users' },
{ path: '/lists', labelKey: 'nav.lists', icon: 'list' },
{ path: '/', label: 'Zitate', icon: 'quote', alwaysVisible: true },
{ path: '/search', label: 'Suche', icon: 'search' },
{ path: '/authors', label: 'Autoren', icon: 'users' },
{ path: '/favorites', label: 'Favoriten', icon: 'star' },
{ path: '/lists', label: 'Listen', icon: 'list' },
{ path: '/feedback', label: 'Feedback', icon: 'chat' },
{ path: '/settings', label: 'Einstellungen', icon: 'settings', alwaysVisible: true },
],
},
presi: {
appId: 'presi',
defaultRoute: '/',
availableRoutes: [{ path: '/', labelKey: 'nav.home', icon: 'home' }],
},
manacore: {
appId: 'manacore',
defaultRoute: '/',
availableRoutes: [{ path: '/', labelKey: 'nav.dashboard', icon: 'home' }],
availableRoutes: [
{ path: '/', label: 'Dashboard', icon: 'home', alwaysVisible: true },
{ path: '/settings', label: 'Einstellungen', icon: 'settings', alwaysVisible: true },
],
},
};

View file

@ -214,7 +214,7 @@
<div
class="space-y-4 {showNavigation ? 'pt-4 border-t border-[hsl(var(--border))]' : ''}"
>
<NavVisibilitySettings {userSettings} {appId} {t} />
<NavVisibilitySettings {userSettings} {appId} />
</div>
{/if}
@ -347,7 +347,7 @@
>
{#each availableRoutes as route}
<option value={route.path}>
{t(route.labelKey)}
{route.label}
</option>
{/each}
</select>

View file

@ -1,161 +1,74 @@
<script lang="ts">
import type { UserSettingsStore, AppRoute } from '@manacore/shared-theme';
import { getHideableRoutes, APP_ROUTES } from '@manacore/shared-theme';
import type { UserSettingsStore } from '@manacore/shared-theme';
import { getHideableRoutes } from '@manacore/shared-theme';
interface Props {
/** User settings store instance */
userSettings: UserSettingsStore;
/** Current app ID */
appId: string;
/** Translation function (optional, falls back to German) */
t?: (key: string) => string;
}
let { userSettings, appId, t = (key: string) => key }: Props = $props();
let { userSettings, appId }: Props = $props();
// Get all apps that have configurable routes
const configurableApps = $derived(
Object.entries(APP_ROUTES)
.filter(([, config]) => {
const hideableRoutes = config.availableRoutes.filter((r) => !r.alwaysVisible);
return hideableRoutes.length > 0;
})
.map(([id, config]) => ({
id,
label: getAppLabel(id),
routes: config.availableRoutes.filter((r) => !r.alwaysVisible),
}))
);
// Get hideable routes for the current app only
const hideableRoutes = $derived(getHideableRoutes(appId));
// Sort so current app is first
const sortedApps = $derived(
[...configurableApps].sort((a, b) => {
if (a.id === appId) return -1;
if (b.id === appId) return 1;
return a.label.localeCompare(b.label);
})
);
// Check if there are any routes to configure
const hasRoutes = $derived(hideableRoutes.length > 0);
function getAppLabel(id: string): string {
const labels: Record<string, string> = {
clock: 'Uhr',
calendar: 'Kalender',
contacts: 'Kontakte',
mail: 'Mail',
todo: 'Aufgaben',
storage: 'Speicher',
chat: 'Chat',
picture: 'Bilder',
manadeck: 'ManaDeck',
zitare: 'Zitare',
presi: 'Präsentation',
manacore: 'ManaCore',
};
return labels[id] || id;
}
function isRouteHidden(targetAppId: string, path: string): boolean {
const hidden = userSettings.getHiddenNavItemsForApp(targetAppId);
function isRouteHidden(path: string): boolean {
const hidden = userSettings.getHiddenNavItemsForApp(appId);
return hidden.includes(path);
}
async function handleToggle(targetAppId: string, path: string): Promise<void> {
await userSettings.toggleNavItemVisibility(targetAppId, path);
}
// Expanded state per app
let expandedApps = $state<Record<string, boolean>>({});
// Initialize with current app expanded
$effect(() => {
if (appId && expandedApps[appId] === undefined) {
expandedApps[appId] = true;
}
});
function toggleApp(id: string): void {
expandedApps[id] = !expandedApps[id];
async function handleToggle(path: string): Promise<void> {
await userSettings.toggleNavItemVisibility(appId, path);
}
</script>
<div class="space-y-4">
<div>
<h3 class="text-xs font-semibold text-[hsl(var(--muted-foreground))] uppercase tracking-wider">
Navigation anpassen
</h3>
<p class="text-sm text-[hsl(var(--muted-foreground))] mt-1">
Versteckte Seiten bleiben über die URL erreichbar
</p>
</div>
{#if hasRoutes}
<div class="space-y-4">
<div>
<h3
class="text-xs font-semibold text-[hsl(var(--muted-foreground))] uppercase tracking-wider"
>
Navigation anpassen
</h3>
<p class="text-sm text-[hsl(var(--muted-foreground))] mt-1">
Versteckte Seiten bleiben über die URL erreichbar
</p>
</div>
<div class="space-y-2">
{#each sortedApps as app (app.id)}
<div class="border border-[hsl(var(--border))] rounded-lg overflow-hidden">
<!-- App Header (collapsible) -->
<button
type="button"
class="w-full flex items-center justify-between px-4 py-3 bg-[hsl(var(--muted))] hover:bg-[hsl(var(--muted))]/80 transition-colors"
onclick={() => toggleApp(app.id)}
<div class="space-y-1">
{#each hideableRoutes as route (route.path)}
{@const hidden = isRouteHidden(route.path)}
<label
class="flex items-center justify-between py-2.5 px-3 rounded-lg hover:bg-[hsl(var(--muted))]/50 cursor-pointer transition-colors border border-transparent hover:border-[hsl(var(--border))]"
>
<span class="font-medium text-[hsl(var(--foreground))] flex items-center gap-2">
{app.label}
{#if app.id === appId}
<span
class="text-xs px-1.5 py-0.5 rounded bg-[hsl(var(--primary))] text-[hsl(var(--primary-foreground))]"
>
Aktuell
</span>
{/if}
</span>
<svg
class="w-5 h-5 text-[hsl(var(--muted-foreground))] transition-transform {expandedApps[
app.id
]
? 'rotate-180'
: ''}"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
<span
class="text-sm {hidden
? 'text-[hsl(var(--muted-foreground))] line-through'
: 'text-[hsl(var(--foreground))]'}"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"
></path>
</svg>
</button>
<!-- Routes List (collapsible) -->
{#if expandedApps[app.id]}
<div class="p-3 space-y-1 bg-[hsl(var(--background))]">
{#each app.routes as route (route.path)}
{@const hidden = isRouteHidden(app.id, route.path)}
<label
class="flex items-center justify-between py-2 px-3 rounded-lg hover:bg-[hsl(var(--muted))]/50 cursor-pointer transition-colors"
>
<span
class="text-sm {hidden
? 'text-[hsl(var(--muted-foreground))] line-through'
: 'text-[hsl(var(--foreground))]'}"
>
{t(route.labelKey)}
</span>
<button
type="button"
class="relative inline-flex h-5 w-9 items-center rounded-full transition-colors {!hidden
? 'bg-[hsl(var(--primary))]'
: 'bg-gray-200 dark:bg-gray-700'}"
onclick={() => handleToggle(app.id, route.path)}
aria-label={hidden ? 'Einblenden' : 'Ausblenden'}
>
<span
class="inline-block h-3.5 w-3.5 transform rounded-full bg-white transition-transform shadow-sm {!hidden
? 'translate-x-5'
: 'translate-x-0.5'}"
></span>
</button>
</label>
{/each}
</div>
{/if}
</div>
{/each}
{route.label}
</span>
<button
type="button"
class="relative inline-flex h-5 w-9 items-center rounded-full transition-colors {!hidden
? 'bg-[hsl(var(--primary))]'
: 'bg-gray-200 dark:bg-gray-700'}"
onclick={() => handleToggle(route.path)}
aria-label={hidden ? 'Einblenden' : 'Ausblenden'}
>
<span
class="inline-block h-3.5 w-3.5 transform rounded-full bg-white transition-transform shadow-sm {!hidden
? 'translate-x-5'
: 'translate-x-0.5'}"
></span>
</button>
</label>
{/each}
</div>
</div>
</div>
{/if}