feat(feedback): heart-half als globales Feedback-Icon + inline-Form in der Workbench

Drei Probleme adressiert:

1. **Icon-Vereinheitlichung**: alle Feedback-Affordances tragen jetzt
   das phosphor `heart-half`-Icon (statt vorher Lightbulb/Mix). Geändert
   in PillNav-Usermenü, ModuleShell-Header (FeedbackHook), Phosphor-Icon-
   Map. Eine Stelle, ein Icon — Wiedererkennung steigt.

2. **Inline statt Modal in Workbench-Cards**: AppPage.svelte rendert
   das Feedback-Formular jetzt im selben Slot wie die Hilfe-Seite —
   Klick auf das Heart-Half-Icon togglet den Inline-Panel statt einen
   Modal-Backdrop über die ganze Workbench zu legen. Hilfe und Feedback
   sind mutually-exclusive (eines geht zu, sobald das andere aufgeht).

3. **Form-Body extrahiert**: FeedbackForm.svelte enthält jetzt das
   Formular ohne jegliches Chrome. FeedbackQuickModal nutzt es im Modal-
   Mode (Standalone-Routen, PillNav), AppPage im Inline-Mode. Eine
   Quelle, beide Surfaces bleiben in sync.

ModuleShell schluckt zusätzlich `onFeedback`/`feedbackOpen`-Props: wenn
gesetzt, ruft die FeedbackHook-Komponente onClick statt das eigene Modal
zu öffnen — der Host (AppPage) übernimmt das Rendering.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-28 15:36:52 +02:00
parent f39e72340c
commit 15ab24bda8
8 changed files with 441 additions and 341 deletions

View file

@ -0,0 +1,344 @@
<!--
FeedbackForm — Pure feedback-submission form (no chrome).
Used by FeedbackQuickModal (modal-mode, fallback for non-workbench
contexts) and the inline panel inside AppPage (workbench cards). Keeps
the two render modes in sync — change the form once, both surfaces
pick it up.
-->
<script lang="ts">
import { PaperPlaneTilt } from '@mana/shared-icons';
import { FEEDBACK_CATEGORY_LABELS, type FeedbackCategory } from '@mana/feedback';
import { feedbackService } from '$lib/api/feedback';
interface Props {
moduleContext?: string;
defaultCategory?: FeedbackCategory;
onCancel?: () => void;
onSubmitted?: (displayName: string | null) => void;
}
let {
moduleContext,
defaultCategory: defaultCategoryProp,
onCancel,
onSubmitted,
}: Props = $props();
let defaultCategory = $derived<FeedbackCategory>(defaultCategoryProp ?? 'feature');
let text = $state('');
// svelte-ignore state_referenced_locally
let category = $state<FeedbackCategory>(defaultCategoryProp ?? 'feature');
let isPublic = $state(true);
let saving = $state(false);
let error = $state<string | null>(null);
let submittedDisplayName = $state<string | null>(null);
const MAX_LEN = 2000;
const SELECTABLE: FeedbackCategory[] = ['feature', 'improvement', 'bug', 'praise', 'question'];
export function reset() {
text = '';
category = defaultCategory;
isPublic = true;
error = null;
submittedDisplayName = null;
}
async function handleSubmit() {
const trimmed = text.trim();
if (!trimmed || saving) return;
saving = true;
error = null;
try {
const res = await feedbackService.createFeedback({
feedbackText: trimmed,
category,
isPublic,
moduleContext,
});
const name =
(res as { displayName?: string }).displayName ??
(res as { feedback?: { displayName?: string } }).feedback?.displayName ??
null;
submittedDisplayName = name;
onSubmitted?.(name);
} catch (err) {
console.error('[FeedbackForm] submit failed:', err);
error = err instanceof Error ? err.message : 'Senden fehlgeschlagen.';
} finally {
saving = false;
}
}
</script>
{#if submittedDisplayName}
<div class="success">
<p>
Dein Feedback ist eingegangen — sichtbar als <strong>{submittedDisplayName}</strong>.
</p>
<div class="reward-chip" aria-live="polite">
<span class="reward-amount">+5</span>
<span class="reward-label">Mana Credits</span>
</div>
{#if isPublic}
<p class="muted">Es taucht in der Community-Page auf, sobald wir es freigeben.</p>
{:else}
<p class="muted">Es bleibt privat und ist nur für dich + Admins sichtbar.</p>
{/if}
{#if onCancel}
<button class="btn-primary" onclick={() => onCancel()}>Schließen</button>
{/if}
</div>
{:else}
<div class="form">
{#if moduleContext}
<div class="context-badge">Modul: {moduleContext}</div>
{/if}
<label class="field">
<span class="label">Kategorie</span>
<select bind:value={category}>
{#each SELECTABLE as cat (cat)}
<option value={cat}>{FEEDBACK_CATEGORY_LABELS[cat]}</option>
{/each}
</select>
</label>
<label class="field">
<span class="label">Was ist los?</span>
<textarea
bind:value={text}
placeholder="Beschreib's so genau du willst…"
maxlength={MAX_LEN}
rows="5"
></textarea>
<span class="counter">{MAX_LEN - text.length} Zeichen übrig</span>
</label>
<label class="checkbox">
<input type="checkbox" bind:checked={isPublic} />
<span>
In Community-Feed öffentlich anzeigen (anonym als Eulen-Pseudonym).
{#if !isPublic}<small>— bleibt privat, nur für Admins sichtbar.</small>{/if}
</span>
</label>
{#if error}
<p class="error" role="alert">{error}</p>
{/if}
<div class="actions">
{#if onCancel}
<button class="btn-ghost" onclick={() => onCancel()} disabled={saving}>Abbrechen</button>
{/if}
<button
class="btn-primary"
onclick={handleSubmit}
disabled={saving || text.trim().length < 3}
>
<span>{saving ? 'Sende…' : 'Senden'}</span>
<PaperPlaneTilt size={16} weight="bold" />
</button>
</div>
</div>
{/if}
<style>
.form {
display: flex;
flex-direction: column;
gap: 0.875rem;
}
.success {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.success p {
margin: 0;
font-size: 0.9375rem;
}
.success .muted {
color: hsl(var(--color-muted-foreground));
font-size: 0.875rem;
}
.success .btn-primary {
align-self: flex-end;
margin-top: 0.5rem;
}
.reward-chip {
align-self: flex-start;
display: inline-flex;
align-items: baseline;
gap: 0.375rem;
padding: 0.4375rem 0.75rem;
border-radius: 999px;
background: linear-gradient(
135deg,
hsl(var(--color-primary) / 0.18),
hsl(var(--color-primary) / 0.08)
);
color: hsl(var(--color-primary));
border: 1px solid hsl(var(--color-primary) / 0.3);
font-weight: 600;
animation: reward-in 0.45s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.reward-amount {
font-size: 1rem;
font-variant-numeric: tabular-nums;
}
.reward-label {
font-size: 0.8125rem;
opacity: 0.85;
}
@keyframes reward-in {
from {
opacity: 0;
transform: translateY(-6px) scale(0.92);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.context-badge {
align-self: flex-start;
padding: 0.25rem 0.5rem;
border-radius: 999px;
background: hsl(var(--color-primary) / 0.12);
color: hsl(var(--color-primary));
font-size: 0.75rem;
font-weight: 600;
}
.field {
display: flex;
flex-direction: column;
gap: 0.375rem;
}
.label {
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
color: hsl(var(--color-muted-foreground));
}
.field select,
.field textarea {
width: 100%;
padding: 0.625rem 0.75rem;
border: 1px solid hsl(var(--color-border));
border-radius: 0.5rem;
background: hsl(var(--color-surface, var(--color-background)));
color: hsl(var(--color-foreground));
font: inherit;
}
.field textarea {
resize: vertical;
min-height: 6rem;
}
.field select:focus,
.field textarea:focus {
outline: none;
border-color: hsl(var(--color-primary));
box-shadow: 0 0 0 3px hsl(var(--color-primary) / 0.15);
}
.counter {
align-self: flex-end;
font-size: 0.6875rem;
color: hsl(var(--color-muted-foreground));
font-variant-numeric: tabular-nums;
}
.checkbox {
display: flex;
gap: 0.5rem;
font-size: 0.8125rem;
color: hsl(var(--color-muted-foreground));
line-height: 1.4;
}
.checkbox input {
margin-top: 0.2rem;
}
.checkbox small {
display: block;
font-size: 0.75rem;
}
.error {
margin: 0;
font-size: 0.8125rem;
color: hsl(var(--color-error, 0 84% 60%));
}
.actions {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
}
.btn-ghost {
padding: 0.5rem 0.875rem;
border: none;
background: transparent;
color: hsl(var(--color-muted-foreground));
font-size: 0.875rem;
border-radius: 0.5rem;
cursor: pointer;
}
.btn-ghost:hover:not(:disabled) {
background: hsl(var(--color-muted) / 0.4);
color: hsl(var(--color-foreground));
}
.btn-ghost:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-primary {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.5rem 0.875rem;
border: none;
background: hsl(var(--color-primary));
color: hsl(var(--color-primary-foreground, 0 0% 100%));
font-size: 0.875rem;
font-weight: 600;
border-radius: 0.5rem;
cursor: pointer;
transition:
transform 0.15s ease,
box-shadow 0.15s ease;
}
.btn-primary:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 4px 12px hsl(var(--color-primary) / 0.3);
}
.btn-primary:disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>

View file

@ -3,44 +3,64 @@
pre-filled with the calling module's id. Drops into ModuleShell's
window-actions row by default; can be placed anywhere by callers.
ModuleShell auto-injects this in its header (right next to the
window-controls). Modules opt out via `hideFeedback={true}`.
Two render modes:
- Standalone (`onClick` not provided): renders the heart-half button +
opens its own FeedbackQuickModal. Used outside the workbench
(e.g. /todo direct route, settings pages).
- Hosted (`onClick` provided): just the icon button — the host owns
the open-state and renders feedback inline (workbench AppPage).
ModuleShell auto-injects the standalone variant in its header. The
workbench's AppPage suppresses that and wires its own onFeedback to
flip into the inline feedback panel (like the Hilfe panel).
-->
<script lang="ts">
import { Lightbulb } from '@mana/shared-icons';
import { HeartHalf } from '@mana/shared-icons';
import { authStore } from '$lib/stores/auth.svelte';
import FeedbackQuickModal from './FeedbackQuickModal.svelte';
interface Props {
moduleId?: string;
size?: number;
/** When set, the button just calls this — host renders the form. */
onClick?: () => void;
/** When the host renders inline feedback, pass `true` to highlight
* the trigger like the help button does. */
active?: boolean;
}
let { moduleId, size = 22 }: Props = $props();
let { moduleId, size = 22, onClick, active = false }: Props = $props();
let open = $state(false);
function handleClick(e: MouseEvent) {
e.stopPropagation();
// Submit requires login. Guests would just see an auth-error toast,
// so silently skip the modal — global pill catches them elsewhere.
// so silently skip — global pill catches them elsewhere.
if (!authStore.user) return;
if (onClick) {
onClick();
} else {
open = true;
}
}
</script>
{#if authStore.user}
<button
type="button"
class="feedback-hook-btn"
class:active
onclick={handleClick}
title="Idee oder Feedback?"
aria-label="Feedback geben"
>
<Lightbulb {size} weight="bold" />
<HeartHalf {size} weight="bold" />
</button>
{#if !onClick}
<FeedbackQuickModal {open} moduleContext={moduleId} onClose={() => (open = false)} />
{/if}
{/if}
<style>
@ -62,4 +82,9 @@
background: hsl(var(--color-surface-hover, var(--color-muted)));
color: hsl(var(--color-primary));
}
.feedback-hook-btn.active {
background: hsl(var(--color-surface-hover, var(--color-muted)));
color: hsl(var(--color-primary));
}
</style>

View file

@ -1,76 +1,28 @@
<!--
FeedbackQuickModal — Lightweight feedback-submission modal opened from
any FeedbackHook button or the global floating pill.
Pre-fills moduleContext from the caller, exposes a category dropdown,
and posts via the shared feedbackService. Submit flips into a "Danke"
state that shows the public pseudonym so the user knows how their
post will appear in the community.
FeedbackQuickModal — Modal wrapper around FeedbackForm. Used outside
the workbench (e.g. PillNav user-menu, /todo direct route, settings
pages). Workbench cards render the same form inline instead.
-->
<script lang="ts">
import { X, PaperPlaneTilt } from '@mana/shared-icons';
import { FEEDBACK_CATEGORY_LABELS, type FeedbackCategory } from '@mana/feedback';
import { feedbackService } from '$lib/api/feedback';
import { X } from '@mana/shared-icons';
import type { FeedbackCategory } from '@mana/feedback';
import FeedbackForm from './FeedbackForm.svelte';
interface Props {
open: boolean;
onClose: () => void;
/** Module that owns this hook — pre-filled into the submission. */
moduleContext?: string;
/** Pre-selected category. Defaults to 'feature'. */
defaultCategory?: FeedbackCategory;
/** Optional headline override. */
title?: string;
}
let props: Props = $props();
let title = $derived(props.title ?? 'Idee oder Feedback?');
let defaultCategory = $derived<FeedbackCategory>(props.defaultCategory ?? 'feature');
let text = $state('');
// svelte-ignore state_referenced_locally
let category = $state<FeedbackCategory>(props.defaultCategory ?? 'feature');
let isPublic = $state(true);
let saving = $state(false);
let error = $state<string | null>(null);
let submittedDisplayName = $state<string | null>(null);
const MAX_LEN = 2000;
// Categories the user can pick — onboarding-wish/praise/other are
// possible too, but the inline form is geared at feature/bug/improvement.
const SELECTABLE: FeedbackCategory[] = ['feature', 'improvement', 'bug', 'praise', 'question'];
async function handleSubmit() {
const trimmed = text.trim();
if (!trimmed || saving) return;
saving = true;
error = null;
try {
const res = await feedbackService.createFeedback({
feedbackText: trimmed,
category,
isPublic,
moduleContext: props.moduleContext,
});
submittedDisplayName =
(res as { displayName?: string }).displayName ??
(res as { feedback?: { displayName?: string } }).feedback?.displayName ??
null;
} catch (err) {
console.error('[FeedbackQuickModal] submit failed:', err);
error = err instanceof Error ? err.message : 'Senden fehlgeschlagen.';
} finally {
saving = false;
}
}
let formRef = $state<{ reset: () => void } | null>(null);
function handleClose() {
text = '';
category = defaultCategory;
isPublic = true;
error = null;
submittedDisplayName = null;
formRef?.reset();
props.onClose();
}
@ -93,83 +45,21 @@
>
<div class="modal" role="document" onclick={(e) => e.stopPropagation()}>
<header class="modal-header">
<h2>{submittedDisplayName ? 'Danke!' : title}</h2>
<h2>{title}</h2>
<button class="close-btn" onclick={handleClose} aria-label="Schließen">
<X size={18} weight="bold" />
</button>
</header>
{#if submittedDisplayName}
<div class="success">
<p>
Dein Feedback ist eingegangen — sichtbar als
<strong>{submittedDisplayName}</strong>.
</p>
<div class="reward-chip" aria-live="polite">
<span class="reward-amount">+5</span>
<span class="reward-label">Mana Credits</span>
</div>
{#if isPublic}
<p class="muted">Es taucht in der Community-Page auf, sobald wir es freigeben.</p>
{:else}
<p class="muted">Es bleibt privat und ist nur für dich + Admins sichtbar.</p>
{/if}
<button class="btn-primary" onclick={handleClose}>Schließen</button>
</div>
{:else}
<div class="body">
{#if props.moduleContext}
<div class="context-badge">Modul: {props.moduleContext}</div>
{/if}
<label class="field">
<span class="label">Kategorie</span>
<select bind:value={category}>
{#each SELECTABLE as cat (cat)}
<option value={cat}>{FEEDBACK_CATEGORY_LABELS[cat]}</option>
{/each}
</select>
</label>
<label class="field">
<span class="label">Was ist los?</span>
<!-- svelte-ignore a11y_autofocus -->
<textarea
bind:value={text}
placeholder="Beschreib's so genau du willst…"
maxlength={MAX_LEN}
rows="5"
autofocus
></textarea>
<span class="counter">{MAX_LEN - text.length} Zeichen übrig</span>
</label>
<label class="checkbox">
<input type="checkbox" bind:checked={isPublic} />
<span>
In Community-Feed öffentlich anzeigen (anonym als Eulen-Pseudonym).
{#if !isPublic}<small>— bleibt privat, nur für Admins sichtbar.</small>{/if}
</span>
</label>
{#if error}
<p class="error" role="alert">{error}</p>
{/if}
<div class="actions">
<button class="btn-ghost" onclick={handleClose} disabled={saving}>Abbrechen</button>
<button
class="btn-primary"
onclick={handleSubmit}
disabled={saving || text.trim().length < 3}
>
<span>{saving ? 'Sende…' : 'Senden'}</span>
<PaperPlaneTilt size={16} weight="bold" />
</button>
<FeedbackForm
bind:this={formRef}
moduleContext={props.moduleContext}
defaultCategory={props.defaultCategory}
onCancel={handleClose}
/>
</div>
</div>
{/if}
</div>
</div>
{/if}
@ -237,200 +127,6 @@
.body {
padding: 0.5rem 1rem 1rem;
display: flex;
flex-direction: column;
gap: 0.875rem;
overflow-y: auto;
}
.success {
padding: 1rem 1rem 1.25rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.success p {
margin: 0;
font-size: 0.9375rem;
}
.success .muted {
color: hsl(var(--color-muted-foreground));
font-size: 0.875rem;
}
.success .btn-primary {
align-self: flex-end;
margin-top: 0.5rem;
}
.reward-chip {
align-self: flex-start;
display: inline-flex;
align-items: baseline;
gap: 0.375rem;
padding: 0.4375rem 0.75rem;
border-radius: 999px;
background: linear-gradient(
135deg,
hsl(var(--color-primary) / 0.18),
hsl(var(--color-primary) / 0.08)
);
color: hsl(var(--color-primary));
border: 1px solid hsl(var(--color-primary) / 0.3);
font-weight: 600;
animation: reward-in 0.45s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.reward-amount {
font-size: 1rem;
font-variant-numeric: tabular-nums;
}
.reward-label {
font-size: 0.8125rem;
opacity: 0.85;
}
@keyframes reward-in {
from {
opacity: 0;
transform: translateY(-6px) scale(0.92);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.context-badge {
align-self: flex-start;
padding: 0.25rem 0.5rem;
border-radius: 999px;
background: hsl(var(--color-primary) / 0.12);
color: hsl(var(--color-primary));
font-size: 0.75rem;
font-weight: 600;
}
.field {
display: flex;
flex-direction: column;
gap: 0.375rem;
}
.label {
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
color: hsl(var(--color-muted-foreground));
}
.field select,
.field textarea {
width: 100%;
padding: 0.625rem 0.75rem;
border: 1px solid hsl(var(--color-border));
border-radius: 0.5rem;
background: hsl(var(--color-surface, var(--color-background)));
color: hsl(var(--color-foreground));
font: inherit;
}
.field textarea {
resize: vertical;
min-height: 6rem;
}
.field select:focus,
.field textarea:focus {
outline: none;
border-color: hsl(var(--color-primary));
box-shadow: 0 0 0 3px hsl(var(--color-primary) / 0.15);
}
.counter {
align-self: flex-end;
font-size: 0.6875rem;
color: hsl(var(--color-muted-foreground));
font-variant-numeric: tabular-nums;
}
.checkbox {
display: flex;
gap: 0.5rem;
font-size: 0.8125rem;
color: hsl(var(--color-muted-foreground));
line-height: 1.4;
}
.checkbox input {
margin-top: 0.2rem;
}
.checkbox small {
display: block;
font-size: 0.75rem;
}
.error {
margin: 0;
font-size: 0.8125rem;
color: hsl(var(--color-error, 0 84% 60%));
}
.actions {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
}
.btn-ghost {
padding: 0.5rem 0.875rem;
border: none;
background: transparent;
color: hsl(var(--color-muted-foreground));
font-size: 0.875rem;
border-radius: 0.5rem;
cursor: pointer;
}
.btn-ghost:hover:not(:disabled) {
background: hsl(var(--color-muted) / 0.4);
color: hsl(var(--color-foreground));
}
.btn-ghost:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-primary {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.5rem 0.875rem;
border: none;
background: hsl(var(--color-primary));
color: hsl(var(--color-primary-foreground, 0 0% 100%));
font-size: 0.875rem;
font-weight: 600;
border-radius: 0.5rem;
cursor: pointer;
transition:
transform 0.15s ease,
box-shadow 0.15s ease;
}
.btn-primary:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 4px 12px hsl(var(--color-primary) / 0.3);
}
.btn-primary:disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>

View file

@ -67,7 +67,7 @@
helpOpen?: boolean;
onContextMenu?: (e: MouseEvent) => void;
// Inline feedback hook — renders a small Lightbulb button in the
// Inline feedback hook — renders a small heart-half button in the
// window-actions row. Submitted feedback is tagged with `moduleId`
// so the public community feed can group/filter by module.
/** Module identifier passed to the inline FeedbackHook. */
@ -75,6 +75,12 @@
/** Suppress the auto-injected FeedbackHook (e.g. on the
* /community-/feedback-pages where it's redundant). */
hideFeedback?: boolean;
/** When provided, the heart-half button calls this instead of
* opening its own modal. The host renders feedback inline (used
* by workbench AppPage to mirror the Hilfe-panel pattern). */
onFeedback?: () => void;
/** Highlights the heart-half trigger when the inline panel is open. */
feedbackOpen?: boolean;
// Snippets
header_left?: Snippet;
@ -106,6 +112,8 @@
onContextMenu,
moduleId,
hideFeedback = false,
onFeedback,
feedbackOpen = false,
header_left,
badge,
actions,
@ -205,7 +213,7 @@
{@render actions()}
{/if}
{#if !hideFeedback}
<FeedbackHook {moduleId} />
<FeedbackHook {moduleId} onClick={onFeedback} active={feedbackOpen} />
{/if}
{#if onHelp}
<button

View file

@ -7,6 +7,7 @@
import { X, CaretUp, CaretDown, ArrowLeft, SpinnerGap } from '@mana/shared-icons';
import { _ } from 'svelte-i18n';
import { ModuleShell } from '$lib/components/shell';
import FeedbackForm from '$lib/components/feedback/FeedbackForm.svelte';
import { getApp, getAppByDragType, canDrop, executeDrop } from '$lib/app-registry';
import type { Component } from 'svelte';
import { dropTarget } from '@mana/shared-ui/dnd';
@ -49,6 +50,19 @@
let helpOpen = $state(false);
let helpData = $derived(app?.help);
// ── Inline feedback ────────────────────────────────────
let feedbackOpen = $state(false);
function toggleFeedback() {
feedbackOpen = !feedbackOpen;
if (feedbackOpen) helpOpen = false;
}
function toggleHelp() {
helpOpen = !helpOpen;
if (helpOpen) feedbackOpen = false;
}
// ── Cross-module drop target ────────────────────────────
let acceptedDropTypes = $derived(app?.acceptsDropFrom ?? []);
@ -309,10 +323,17 @@
{onMoveLeft}
{onMoveRight}
{onContextMenu}
onHelp={helpData ? () => (helpOpen = !helpOpen) : undefined}
onHelp={helpData ? toggleHelp : undefined}
{helpOpen}
moduleId={appId}
onFeedback={toggleFeedback}
{feedbackOpen}
>
{#if helpOpen && helpData}
{#if feedbackOpen}
<div class="feedback-view">
<FeedbackForm moduleContext={appId} onCancel={() => (feedbackOpen = false)} />
</div>
{:else if helpOpen && helpData}
<div class="help-view">
<p class="help-desc">{helpData.description}</p>
@ -425,6 +446,12 @@
overflow-y: auto;
height: 100%;
}
.feedback-view {
padding: 1rem 1.125rem 1.5rem;
animation: helpFadeIn 0.2s ease-out;
overflow-y: auto;
height: 100%;
}
.help-desc {
font-size: 0.8125rem;
line-height: 1.65;

View file

@ -50,9 +50,9 @@
Globe,
GridFour,
Heart,
HeartHalf,
House,
Key,
Lightbulb,
List,
MagnifyingGlass,
Microphone,
@ -136,7 +136,7 @@
scale: Scales,
robot: Robot,
key: Key,
lightbulb: Lightbulb,
'heart-half': HeartHalf,
shield: Shield,
gift: Gift,
'music-notes': MusicNotes,
@ -508,7 +508,7 @@
links.push({
id: 'feedback',
label: 'Idee teilen',
icon: 'lightbulb',
icon: 'heart-half',
onClick: onFeedback,
});
} else if (userEmail && feedbackHref) {
@ -559,7 +559,7 @@
out.push({
id: 'feedback',
label: 'Idee teilen',
icon: 'lightbulb',
icon: 'heart-half',
onClick: () => onFeedback(),
});
out.push({ id: 'feedback-divider', label: '', divider: true });

View file

@ -8,7 +8,7 @@
Gear,
Globe,
Heart,
Lightbulb,
HeartHalf,
Moon,
Palette,
Question,
@ -37,7 +37,7 @@
sun: Sun,
palette: Palette,
robot: Robot,
lightbulb: Lightbulb,
'heart-half': HeartHalf,
logout: SignOut,
};

View file

@ -23,9 +23,9 @@ import {
Globe,
GridFour,
Heart,
HeartHalf,
House,
Key,
Lightbulb,
List,
MagnifyingGlass,
Microphone,
@ -110,7 +110,7 @@ export const phosphorIcons: Record<string, any> = {
scale: Scales,
robot: Robot,
key: Key,
lightbulb: Lightbulb,
'heart-half': HeartHalf,
shield: Shield,
gift: Gift,
'music-notes': MusicNotes,