mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 20:21:09 +02:00
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:
parent
eaa1d7432b
commit
94d3277e2e
4 changed files with 65 additions and 109 deletions
|
|
@ -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>
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import type { Component, Snippet } from 'svelte';
|
import type { Component, Snippet } from 'svelte';
|
||||||
import ToastContainer from '$lib/components/ToastContainer.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 { onDestroy, setContext, tick } from 'svelte';
|
||||||
import { createReminderScheduler } from '@mana/shared-stores';
|
import { createReminderScheduler } from '@mana/shared-stores';
|
||||||
import { todoReminderSource } from '$lib/modules/todo/reminder-source';
|
import { todoReminderSource } from '$lib/modules/todo/reminder-source';
|
||||||
|
|
@ -251,6 +251,17 @@
|
||||||
let isBottomBarVisible = $state(false);
|
let isBottomBarVisible = $state(false);
|
||||||
let activeBar = $state<PillBarConfig | null>(null);
|
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() {
|
function closeAllBars() {
|
||||||
isTagStripVisible = false;
|
isTagStripVisible = false;
|
||||||
isQuickInputVisible = false;
|
isQuickInputVisible = false;
|
||||||
|
|
@ -1061,6 +1072,7 @@
|
||||||
creditsHref="/?app=credits"
|
creditsHref="/?app=credits"
|
||||||
themesHref="/?app=themes"
|
themesHref="/?app=themes"
|
||||||
helpHref="/?app=help"
|
helpHref="/?app=help"
|
||||||
|
onFeedback={() => (feedbackModalOpen = true)}
|
||||||
{spotlightActions}
|
{spotlightActions}
|
||||||
{contentSearcher}
|
{contentSearcher}
|
||||||
positioning="static"
|
positioning="static"
|
||||||
|
|
@ -1139,9 +1151,12 @@
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Global "Idee?" feedback pill — self-hides on /onboarding,
|
<!-- Quick-feedback modal — opened from the user-menu chip. -->
|
||||||
/feedback, /community, and for unauthenticated users. -->
|
<FeedbackQuickModal
|
||||||
<GlobalFeedbackPill />
|
open={feedbackModalOpen}
|
||||||
|
moduleContext={feedbackModuleContext}
|
||||||
|
onClose={() => (feedbackModalOpen = false)}
|
||||||
|
/>
|
||||||
</AuthGate>
|
</AuthGate>
|
||||||
|
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@
|
||||||
Heart,
|
Heart,
|
||||||
House,
|
House,
|
||||||
Key,
|
Key,
|
||||||
|
Lightbulb,
|
||||||
List,
|
List,
|
||||||
MagnifyingGlass,
|
MagnifyingGlass,
|
||||||
Microphone,
|
Microphone,
|
||||||
|
|
@ -135,6 +136,7 @@
|
||||||
scale: Scales,
|
scale: Scales,
|
||||||
robot: Robot,
|
robot: Robot,
|
||||||
key: Key,
|
key: Key,
|
||||||
|
lightbulb: Lightbulb,
|
||||||
shield: Shield,
|
shield: Shield,
|
||||||
gift: Gift,
|
gift: Gift,
|
||||||
'music-notes': MusicNotes,
|
'music-notes': MusicNotes,
|
||||||
|
|
@ -318,8 +320,13 @@
|
||||||
contentSearcher?: ContentSearcher;
|
contentSearcher?: ContentSearcher;
|
||||||
/** Accessible label for the nav element */
|
/** Accessible label for the nav element */
|
||||||
ariaLabel?: string;
|
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;
|
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. */
|
/** Themes page href (shown in user dropdown). Set to empty string to hide. */
|
||||||
themesHref?: string;
|
themesHref?: string;
|
||||||
/** Spiral page href (shown in user dropdown). Set to empty string to hide. */
|
/** Spiral page href (shown in user dropdown). Set to empty string to hide. */
|
||||||
|
|
@ -391,6 +398,7 @@
|
||||||
contentSearcher,
|
contentSearcher,
|
||||||
ariaLabel,
|
ariaLabel,
|
||||||
feedbackHref = '/feedback',
|
feedbackHref = '/feedback',
|
||||||
|
onFeedback,
|
||||||
themesHref,
|
themesHref,
|
||||||
spiralHref,
|
spiralHref,
|
||||||
creditsHref,
|
creditsHref,
|
||||||
|
|
@ -461,7 +469,14 @@
|
||||||
|
|
||||||
// Account links for UserMenuPanel
|
// Account links for UserMenuPanel
|
||||||
const accountLinks = $derived.by(() => {
|
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) {
|
if (userEmail && profileHref) {
|
||||||
links.push({
|
links.push({
|
||||||
id: 'profile',
|
id: 'profile',
|
||||||
|
|
@ -489,7 +504,14 @@
|
||||||
active: currentPath === creditsHref,
|
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({
|
links.push({
|
||||||
id: 'feedback',
|
id: 'feedback',
|
||||||
label: 'Feedback',
|
label: 'Feedback',
|
||||||
|
|
@ -533,6 +555,15 @@
|
||||||
// we don't duplicate it inside the opened bar.
|
// we don't duplicate it inside the opened bar.
|
||||||
const userMenuBarItems = $derived.by<PillDropdownItem[]>(() => {
|
const userMenuBarItems = $derived.by<PillDropdownItem[]>(() => {
|
||||||
const out: 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) {
|
if (onThemeModeChange) {
|
||||||
out.push(
|
out.push(
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
Gear,
|
Gear,
|
||||||
Globe,
|
Globe,
|
||||||
Heart,
|
Heart,
|
||||||
|
Lightbulb,
|
||||||
Moon,
|
Moon,
|
||||||
Palette,
|
Palette,
|
||||||
Question,
|
Question,
|
||||||
|
|
@ -36,6 +37,7 @@
|
||||||
sun: Sun,
|
sun: Sun,
|
||||||
palette: Palette,
|
palette: Palette,
|
||||||
robot: Robot,
|
robot: Robot,
|
||||||
|
lightbulb: Lightbulb,
|
||||||
logout: SignOut,
|
logout: SignOut,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -43,7 +45,8 @@
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
href: string;
|
href?: string;
|
||||||
|
onClick?: () => void;
|
||||||
active?: boolean;
|
active?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -182,7 +185,14 @@
|
||||||
<button
|
<button
|
||||||
class="chip"
|
class="chip"
|
||||||
class:active={link.active}
|
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}
|
title={link.label}
|
||||||
>
|
>
|
||||||
{#if icons[link.icon]}
|
{#if icons[link.icon]}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue