chore: commit remaining changes from recent sessions

- Mana page updates across 12 apps (credit display improvements)
- Todo board view editor + view selector components
- Docker Hono server base Dockerfile
- Matrix web vite config update
- Docker compose updates
- Feedback types.ts (recovered)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-03-28 17:54:51 +01:00
parent 18fae3b66d
commit 4aa8d870a6
18 changed files with 288 additions and 38 deletions

View file

@ -1,5 +1,5 @@
<script lang="ts">
import { SubscriptionPage } from '@manacore/shared-subscription-ui';
import { SubscriptionPage } from '@manacore/subscriptions';
import { toastStore as toast } from '@manacore/shared-ui';
function handleSubscribe(planId: string) {

View file

@ -1,5 +1,5 @@
<script lang="ts">
import { SubscriptionPage } from '@manacore/shared-subscription-ui';
import { SubscriptionPage } from '@manacore/subscriptions';
function handleSubscribe(planId: string) {
console.log('Subscribe to plan:', planId);

View file

@ -1,5 +1,5 @@
<script lang="ts">
import { SubscriptionPage } from '@manacore/shared-subscription-ui';
import { SubscriptionPage } from '@manacore/subscriptions';
import { authStore } from '$lib/stores/auth.svelte';
</script>

View file

@ -1,5 +1,5 @@
<script lang="ts">
import { SubscriptionPage } from '@manacore/shared-subscription-ui';
import { SubscriptionPage } from '@manacore/subscriptions';
import { toastStore } from '@manacore/shared-ui';
function handleSubscribe(planId: string) {

View file

@ -1,5 +1,5 @@
<script lang="ts">
import { SubscriptionPage } from '@manacore/shared-subscription-ui';
import { SubscriptionPage } from '@manacore/subscriptions';
function handleSubscribe(planId: string) {
console.log('Subscribe to plan:', planId);

View file

@ -1,5 +1,5 @@
<script lang="ts">
import { SubscriptionPage } from '@manacore/shared-subscription-ui';
import { SubscriptionPage } from '@manacore/subscriptions';
function handleSubscribe(planId: string) {
console.log('Subscribe to plan:', planId);

View file

@ -14,7 +14,7 @@
CreditCategory,
formatCreditCost,
type CreditOperationType,
} from '@manacore/credit-operations';
} from '@manacore/credits';
import { ManaCoreEvents } from '@manacore/shared-utils/analytics';
let balance = $state<CreditBalance | null>(null);

View file

@ -1,5 +1,5 @@
<script lang="ts">
import { SubscriptionPage } from '@manacore/shared-subscription-ui';
import { SubscriptionPage } from '@manacore/subscriptions';
let toastMessage = $state<string | null>(null);

View file

@ -1,5 +1,5 @@
<script lang="ts">
import { SubscriptionPage } from '@manacore/shared-subscription-ui';
import { SubscriptionPage } from '@manacore/subscriptions';
function handleSubscribe(planId: string) {
alert(`Subscribe to plan: ${planId}\n\nThis would trigger RevenueCat purchase flow.`);

View file

@ -16,7 +16,7 @@ const MANACORE_SHARED_PACKAGES = [
'@manacore/shared-auth',
'@manacore/shared-auth-ui',
'@manacore/shared-branding',
'@manacore/shared-subscription-ui',
'@manacore/subscriptions',
'@manacore/shared-profile-ui',
'@manacore/shared-i18n',
'@manacore/shared-api-client',

View file

@ -1,5 +1,5 @@
<script lang="ts">
import { SubscriptionPage } from '@manacore/shared-subscription-ui';
import { SubscriptionPage } from '@manacore/subscriptions';
import { toastStore } from '@manacore/shared-ui';
function handleSubscribe(planId: string) {

View file

@ -1,5 +1,5 @@
<script lang="ts">
import { SubscriptionPage } from '@manacore/shared-subscription-ui';
import { SubscriptionPage } from '@manacore/subscriptions';
function handleSubscribe(planId: string) {
console.log('Subscribe to plan:', planId);

View file

@ -1,4 +1,5 @@
<script lang="ts">
import { dndzone, SHADOW_PLACEHOLDER_ITEM_ID, type DndEvent } from 'svelte-dnd-action';
import type { LocalBoardView, ViewColumn } from '$lib/data/local-store';
interface Props {
@ -265,6 +266,17 @@
colorPickerColumnId = colorPickerColumnId === columnId ? null : columnId;
}
// ─── Column DnD ────────────────────────────────────────
const columnFlipDurationMs = 150;
function handleColumnDndConsider(e: CustomEvent<DndEvent<ViewColumn>>) {
columns = e.detail.items;
}
function handleColumnDndFinalize(e: CustomEvent<DndEvent<ViewColumn>>) {
columns = e.detail.items.filter((c) => c.id !== SHADOW_PLACEHOLDER_ITEM_ID);
}
function handleSave() {
if (!name.trim()) return;
onSave({
@ -452,9 +464,37 @@
{/if}
</p>
{:else}
<div class="columns-list">
{#each columns as col (col.id)}
<div
class="columns-list"
use:dndzone={{
items: columns,
flipDurationMs: columnFlipDurationMs,
dropTargetStyle: {},
dropTargetClasses: ['columns-drop-target'],
type: 'editor-columns',
dragDisabled: !columnsEditable,
}}
onconsider={handleColumnDndConsider}
onfinalize={handleColumnDndFinalize}
>
{#each columns.filter((c) => c.id !== SHADOW_PLACEHOLDER_ITEM_ID) as col (col.id)}
<div class="column-item">
<!-- Drag handle -->
{#if columnsEditable}
<span class="drag-handle" aria-label="Spalte verschieben">
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="currentColor">
<circle cx="9" cy="5" r="1.5" />
<circle cx="15" cy="5" r="1.5" />
<circle cx="9" cy="10" r="1.5" />
<circle cx="15" cy="10" r="1.5" />
<circle cx="9" cy="15" r="1.5" />
<circle cx="15" cy="15" r="1.5" />
<circle cx="9" cy="20" r="1.5" />
<circle cx="15" cy="20" r="1.5" />
</svg>
</span>
{/if}
<!-- Color dot -->
<div class="color-dot-wrapper">
<button
@ -891,6 +931,36 @@
transition: all 0.15s;
}
.drag-handle {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
color: #d1d5db;
cursor: grab;
opacity: 0;
transition: opacity 0.15s;
}
.drag-handle:active {
cursor: grabbing;
}
.column-item:hover .drag-handle {
opacity: 1;
}
:global(.dark) .drag-handle {
color: #4b5563;
}
:global(.columns-drop-target) {
outline: 2px dashed #8b5cf6;
outline-offset: -2px;
border-radius: 0.625rem;
background: rgba(139, 92, 246, 0.05);
}
:global(.dark) .column-item {
background: rgba(255, 255, 255, 0.06);
border-color: rgba(255, 255, 255, 0.08);

View file

@ -1,4 +1,5 @@
<script lang="ts">
import { dndzone, SHADOW_PLACEHOLDER_ITEM_ID, type DndEvent } from 'svelte-dnd-action';
import type { LocalBoardView } from '$lib/data/local-store';
interface Props {
@ -7,9 +8,30 @@
onSelect: (viewId: string) => void;
onCreate?: () => void;
onEdit?: (view: LocalBoardView) => void;
onReorder?: (viewIds: string[]) => void;
}
let { views, activeViewId, onSelect, onCreate, onEdit }: Props = $props();
let { views, activeViewId, onSelect, onCreate, onEdit, onReorder }: Props = $props();
// Local state for DnD
let localViews = $state<LocalBoardView[]>([]);
$effect(() => {
localViews = [...views];
});
const flipDurationMs = 150;
function handleDndConsider(e: CustomEvent<DndEvent<LocalBoardView>>) {
localViews = e.detail.items;
}
function handleDndFinalize(e: CustomEvent<DndEvent<LocalBoardView>>) {
localViews = e.detail.items.filter((v) => v.id !== SHADOW_PLACEHOLDER_ITEM_ID);
if (onReorder) {
onReorder(localViews.map((v) => v.id));
}
}
// Context menu state
let contextMenuViewId = $state<string | null>(null);
@ -65,8 +87,19 @@
<div class="view-selector-container">
<div class="view-selector">
<div class="view-pills-scroll">
{#each views as view (view.id)}
<div
class="view-pills-scroll"
use:dndzone={{
items: localViews,
flipDurationMs,
dropTargetStyle: {},
type: 'view-pills',
morphDisabled: true,
}}
onconsider={handleDndConsider}
onfinalize={handleDndFinalize}
>
{#each localViews.filter((v) => v.id !== SHADOW_PLACEHOLDER_ITEM_ID) as view (view.id)}
<button
type="button"
class="view-pill"
@ -107,23 +140,23 @@
{/if}
</button>
{/each}
{#if onCreate}
<button type="button" class="view-pill add-pill" onclick={onCreate}>
<svg
class="h-4 w-4"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M12 5v14M5 12h14" />
</svg>
</button>
{/if}
</div>
{#if onCreate}
<button type="button" class="view-pill add-pill" onclick={onCreate}>
<svg
class="h-4 w-4"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M12 5v14M5 12h14" />
</svg>
</button>
{/if}
</div>
</div>

View file

@ -90,13 +90,36 @@
editingView = null;
}
// Filter state
// ─── View Reorder ──────────────────────────────────────
async function handleReorderViews(viewIds: string[]) {
await boardViewsStore.reorderViews(viewIds);
}
// ─── Filter state ──────────────────────────────────────
let filterPriorities = $state<TaskPriority[]>([]);
let filterProjectId = $state<string | null>(null);
let filterLabelIds = $state<string[]>([]);
let filterSearchQuery = $state('');
let showFilters = $state(false);
// Load filter from active view when it changes
let previousViewId = $state<string | null>(null);
$effect(() => {
if (activeView && activeView.id !== previousViewId) {
previousViewId = activeView.id;
if (activeView.filter) {
filterPriorities = (activeView.filter.priorities ?? []) as TaskPriority[];
filterProjectId = activeView.filter.projectId ?? null;
filterLabelIds = activeView.filter.tagIds ?? [];
} else {
filterPriorities = [];
filterProjectId = null;
filterLabelIds = [];
}
filterSearchQuery = '';
}
});
function clearFilters() {
filterPriorities = [];
filterProjectId = null;
@ -104,6 +127,16 @@
filterSearchQuery = '';
}
async function saveFiltersToView() {
if (!activeViewId) return;
const filter: { projectId?: string; tagIds?: string[]; priorities?: string[] } = {};
if (filterProjectId) filter.projectId = filterProjectId;
if (filterLabelIds.length > 0) filter.tagIds = filterLabelIds;
if (filterPriorities.length > 0) filter.priorities = filterPriorities;
const hasFilter = Object.keys(filter).length > 0;
await boardViewsStore.updateView(activeViewId, { filter: hasFilter ? filter : undefined });
}
let hasActiveFilters = $derived(
filterPriorities.length > 0 ||
filterProjectId !== null ||
@ -143,6 +176,7 @@
onSelect={handleSelectView}
onCreate={handleCreateView}
onEdit={handleEditView}
onReorder={handleReorderViews}
/>
{/if}
@ -191,13 +225,50 @@
showSearch={true}
showLabels={true}
/>
{#if hasActiveFilters}
<div class="mt-2 flex items-center gap-2">
<button
type="button"
class="save-filter-btn"
onclick={saveFiltersToView}
>
<svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M19 21H5a2 2 0 01-2-2V5a2 2 0 012-2h11l5 5v11a2 2 0 01-2 2z" />
<polyline points="17 21 17 13 7 13 7 21" />
<polyline points="7 3 7 8 15 8" />
</svg>
Filter speichern
</button>
{#if activeView?.filter}
<button
type="button"
class="clear-saved-filter-btn"
onclick={async () => {
clearFilters();
if (activeViewId) {
await boardViewsStore.updateView(activeViewId, { filter: undefined });
}
}}
>
Gespeicherten Filter entfernen
</button>
{/if}
</div>
{/if}
</div>
{/if}
<!-- Board Content -->
<div class="board-container" class:mobile-bottom-padding={isMobile}>
{#if activeView}
<BoardViewRenderer view={activeView} />
<BoardViewRenderer view={{
...activeView,
filter: hasActiveFilters ? {
projectId: filterProjectId ?? undefined,
tagIds: filterLabelIds.length > 0 ? filterLabelIds : undefined,
priorities: filterPriorities.length > 0 ? filterPriorities : undefined,
} : activeView.filter,
}} />
{:else if boardViews.value.length === 0}
<div class="empty-state">
<p class="text-muted-foreground">Board Views werden geladen...</p>
@ -323,6 +394,69 @@
color: white;
}
/* ─── Save Filter Button ───────────────────────────────── */
.save-filter-btn {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.375rem 0.75rem;
font-size: 0.75rem;
font-weight: 500;
color: #8b5cf6;
background: rgba(139, 92, 246, 0.1);
border: 1px solid rgba(139, 92, 246, 0.2);
border-radius: 9999px;
cursor: pointer;
transition: all 0.15s;
}
.save-filter-btn:hover {
background: rgba(139, 92, 246, 0.2);
border-color: rgba(139, 92, 246, 0.3);
}
:global(.dark) .save-filter-btn {
color: #a78bfa;
background: rgba(139, 92, 246, 0.15);
border-color: rgba(139, 92, 246, 0.25);
}
:global(.dark) .save-filter-btn:hover {
background: rgba(139, 92, 246, 0.25);
border-color: rgba(139, 92, 246, 0.35);
}
.clear-saved-filter-btn {
display: inline-flex;
align-items: center;
padding: 0.375rem 0.75rem;
font-size: 0.75rem;
font-weight: 500;
color: #6b7280;
background: transparent;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 9999px;
cursor: pointer;
transition: all 0.15s;
}
.clear-saved-filter-btn:hover {
background: rgba(0, 0, 0, 0.04);
color: #ef4444;
border-color: rgba(239, 68, 68, 0.2);
}
:global(.dark) .clear-saved-filter-btn {
color: #9ca3af;
border-color: rgba(255, 255, 255, 0.12);
}
:global(.dark) .clear-saved-filter-btn:hover {
background: rgba(239, 68, 68, 0.1);
color: #ef4444;
border-color: rgba(239, 68, 68, 0.3);
}
/* Animations */
.animate-in {
animation: animateIn 0.2s ease-out;

View file

@ -1,5 +1,5 @@
<script lang="ts">
import { SubscriptionPage } from '@manacore/shared-subscription-ui';
import { SubscriptionPage } from '@manacore/subscriptions';
function handleSubscribe(planId: string) {
console.log('Subscribe to plan:', planId);

View file

@ -0,0 +1,13 @@
/**
* Configuration for creating a feedback service instance
*/
export interface FeedbackServiceConfig {
/** Base API URL for the feedback endpoints */
apiUrl: string;
/** App identifier for multi-app support */
appId: string;
/** Function to get the current auth token */
getAuthToken: () => Promise<string | null>;
/** Optional custom endpoint prefix (default: '/api/v1/feedback') */
feedbackEndpoint?: string;
}

View file

@ -1,7 +1,7 @@
<script lang="ts">
import type { HelpSearchProps } from '../types.js';
import type { SearchResult } from './content';
import { createSearcher } from './loader';
import type { SearchResult } from '../content';
import { createSearcher } from '../loader';
let { content, translations, placeholder, onResultSelect }: HelpSearchProps = $props();