feat(feedback): "Idee teilen" lebt jetzt im PillNav-Usermenü

Ersetzt den schwebenden "Idee?"-Pill durch einen Eintrag im rechten
Usermenü (Profil / Credits / Idee teilen / Logout). Ein Affordance an
einer Stelle statt zwei nebeneinander.

- PillNavigation: neuer onFeedback-Prop + Lightbulb-Icon. Wenn gesetzt,
  ersetzt der Eintrag den Legacy-/feedback-Link in accountLinks und
  taucht zusätzlich oben in den userMenuBarItems (barMode) auf.
- UserMenuPanel: AccountLink kennt jetzt onClick? als Alternative zu
  href? — Action-Chips schließen das Panel direkt nach dem Klick.
- (app)/+layout: GlobalFeedbackPill-Mount entfernt, FeedbackQuickModal
  wird state-gebunden gerendert (moduleContext aus Pfad/?app= abgeleitet
  wie bisher in der alten Pill).
- GlobalFeedbackPill.svelte gelöscht — niemand referenziert sie mehr.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-28 15:12:27 +02:00
parent eaa1d7432b
commit 94d3277e2e
4 changed files with 65 additions and 109 deletions

View file

@ -1,100 +0,0 @@
<!--
GlobalFeedbackPill — fallback feedback affordance for routes outside
ModuleShell (settings, profile, dashboards). Sits bottom-right, tucked
above the bottom-stack chrome.
Auto-detects module-context from the URL (e.g. `/todo` → `todo`,
`/?app=notes` → `notes`); otherwise leaves moduleContext undefined.
Hides itself on /onboarding and on /feedback + /community pages where
the affordance would be redundant.
-->
<script lang="ts">
import { page } from '$app/stores';
import { Lightbulb } from '@mana/shared-icons';
import { authStore } from '$lib/stores/auth.svelte';
import FeedbackQuickModal from './FeedbackQuickModal.svelte';
let open = $state(false);
let path = $derived($page.url.pathname);
let activeAppParam = $derived($page.url.searchParams.get('app'));
let hidden = $derived(
path.startsWith('/onboarding') ||
path.startsWith('/feedback') ||
path.startsWith('/community') ||
!authStore.user
);
let moduleContext = $derived.by(() => {
// Path-based detection: /todo, /notes, /picture, …
const seg = path.split('/').filter(Boolean)[0];
const fromPath = seg && !seg.startsWith('(') ? seg : null;
// Workbench `?app=` param wins (homepage scene mode).
return activeAppParam ?? fromPath ?? undefined;
});
function handleClick() {
open = true;
}
</script>
{#if !hidden}
<button
type="button"
class="pill"
onclick={handleClick}
title="Idee oder Feedback?"
aria-label="Feedback geben"
>
<Lightbulb size={18} weight="bold" />
<span class="label">Idee?</span>
</button>
<FeedbackQuickModal {open} {moduleContext} onClose={() => (open = false)} />
{/if}
<style>
.pill {
position: fixed;
right: 1rem;
bottom: calc(var(--bottom-chrome-height, 5rem) + 1rem);
z-index: 50;
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.5rem 0.875rem;
border: 1px solid hsl(var(--color-border));
background: hsl(var(--color-card));
color: hsl(var(--color-foreground));
border-radius: 999px;
font-size: 0.8125rem;
font-weight: 600;
cursor: pointer;
box-shadow:
0 6px 16px hsl(0 0% 0% / 0.12),
0 2px 6px hsl(0 0% 0% / 0.08);
transition:
transform 0.15s ease,
box-shadow 0.15s ease,
border-color 0.15s ease;
}
.pill:hover {
transform: translateY(-1px);
border-color: hsl(var(--color-primary) / 0.5);
box-shadow:
0 10px 22px hsl(0 0% 0% / 0.16),
0 3px 8px hsl(0 0% 0% / 0.1);
color: hsl(var(--color-primary));
}
@media (max-width: 480px) {
.pill .label {
display: none;
}
.pill {
padding: 0.5rem;
}
}
</style>

View file

@ -9,7 +9,7 @@
import { page } from '$app/stores';
import type { Component, Snippet } from 'svelte';
import ToastContainer from '$lib/components/ToastContainer.svelte';
import GlobalFeedbackPill from '$lib/components/feedback/GlobalFeedbackPill.svelte';
import FeedbackQuickModal from '$lib/components/feedback/FeedbackQuickModal.svelte';
import { onDestroy, setContext, tick } from 'svelte';
import { createReminderScheduler } from '@mana/shared-stores';
import { todoReminderSource } from '$lib/modules/todo/reminder-source';
@ -251,6 +251,17 @@
let isBottomBarVisible = $state(false);
let activeBar = $state<PillBarConfig | null>(null);
// Quick-feedback modal — opened from the user-menu chip ("Idee teilen").
// Replaces the older floating "Idee?" pill so feedback lives in the same
// affordance as Profile / Credits / Logout.
let feedbackModalOpen = $state(false);
let feedbackModuleContext = $derived.by(() => {
const path = $page.url.pathname;
const seg = path.split('/').filter(Boolean)[0];
const fromPath = seg && !seg.startsWith('(') ? seg : null;
return $page.url.searchParams.get('app') ?? fromPath ?? undefined;
});
function closeAllBars() {
isTagStripVisible = false;
isQuickInputVisible = false;
@ -1061,6 +1072,7 @@
creditsHref="/?app=credits"
themesHref="/?app=themes"
helpHref="/?app=help"
onFeedback={() => (feedbackModalOpen = true)}
{spotlightActions}
{contentSearcher}
positioning="static"
@ -1139,9 +1151,12 @@
/>
{/if}
<!-- Global "Idee?" feedback pill — self-hides on /onboarding,
/feedback, /community, and for unauthenticated users. -->
<GlobalFeedbackPill />
<!-- Quick-feedback modal — opened from the user-menu chip. -->
<FeedbackQuickModal
open={feedbackModalOpen}
moduleContext={feedbackModuleContext}
onClose={() => (feedbackModalOpen = false)}
/>
</AuthGate>
<ToastContainer />

View file

@ -52,6 +52,7 @@
Heart,
House,
Key,
Lightbulb,
List,
MagnifyingGlass,
Microphone,
@ -135,6 +136,7 @@
scale: Scales,
robot: Robot,
key: Key,
lightbulb: Lightbulb,
shield: Shield,
gift: Gift,
'music-notes': MusicNotes,
@ -318,8 +320,13 @@
contentSearcher?: ContentSearcher;
/** Accessible label for the nav element */
ariaLabel?: string;
/** Feedback page href (shown in user dropdown). Set to empty string to hide. */
/** Feedback page href (shown in user dropdown). Set to empty string to hide.
* Ignored when `onFeedback` is provided — the action takes precedence. */
feedbackHref?: string;
/** Called when the user picks "Idee teilen" in the user menu. When set,
* the Feedback chip opens the host's quick-feedback modal instead of
* navigating to feedbackHref. */
onFeedback?: () => void;
/** Themes page href (shown in user dropdown). Set to empty string to hide. */
themesHref?: string;
/** Spiral page href (shown in user dropdown). Set to empty string to hide. */
@ -391,6 +398,7 @@
contentSearcher,
ariaLabel,
feedbackHref = '/feedback',
onFeedback,
themesHref,
spiralHref,
creditsHref,
@ -461,7 +469,14 @@
// Account links for UserMenuPanel
const accountLinks = $derived.by(() => {
const links: { id: string; label: string; icon: string; href: string; active?: boolean }[] = [];
const links: {
id: string;
label: string;
icon: string;
href?: string;
onClick?: () => void;
active?: boolean;
}[] = [];
if (userEmail && profileHref) {
links.push({
id: 'profile',
@ -489,7 +504,14 @@
active: currentPath === creditsHref,
});
}
if (userEmail && feedbackHref) {
if (userEmail && onFeedback) {
links.push({
id: 'feedback',
label: 'Idee teilen',
icon: 'lightbulb',
onClick: onFeedback,
});
} else if (userEmail && feedbackHref) {
links.push({
id: 'feedback',
label: 'Feedback',
@ -533,6 +555,15 @@
// we don't duplicate it inside the opened bar.
const userMenuBarItems = $derived.by<PillDropdownItem[]>(() => {
const out: PillDropdownItem[] = [];
if (userEmail && onFeedback) {
out.push({
id: 'feedback',
label: 'Idee teilen',
icon: 'lightbulb',
onClick: () => onFeedback(),
});
out.push({ id: 'feedback-divider', label: '', divider: true });
}
if (onThemeModeChange) {
out.push(
{

View file

@ -8,6 +8,7 @@
Gear,
Globe,
Heart,
Lightbulb,
Moon,
Palette,
Question,
@ -36,6 +37,7 @@
sun: Sun,
palette: Palette,
robot: Robot,
lightbulb: Lightbulb,
logout: SignOut,
};
@ -43,7 +45,8 @@
id: string;
label: string;
icon: string;
href: string;
href?: string;
onClick?: () => void;
active?: boolean;
}
@ -182,7 +185,14 @@
<button
class="chip"
class:active={link.active}
onclick={() => navigateTo(link.href)}
onclick={() => {
if (link.onClick) {
link.onClick();
onClose();
} else if (link.href) {
navigateTo(link.href);
}
}}
title={link.label}
>
{#if icons[link.icon]}