mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 20:01:09 +02:00
refactor(shell): unify card + route chrome into ModuleShell + RoutePage
Replaces the old PageShell (workbench-only) with a single ModuleShell that serves both carousel cards (variant=card, width-sized, window actions) and sub-routes (variant=fill, fills main area, optional back button). RoutePage wraps ModuleShell with auto-metadata lookup from the app-registry so every (app)/*/+page.svelte can stay a three-liner. Drops the dead onMinimize prop-drilling that was declared on PageShell but never rendered — TodoPage/ContactPage callers cleaned up too. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4c2fbece56
commit
13b785b33f
11 changed files with 245 additions and 101 deletions
|
|
@ -1,8 +1,8 @@
|
|||
/**
|
||||
* Per-module help content — description, features, tips.
|
||||
*
|
||||
* Rendered inline in the page-body when the user clicks the help (?)
|
||||
* icon in the PageShell header. Keyed by appId.
|
||||
* Rendered inline in the shell body when the user clicks the help (?)
|
||||
* icon in the ModuleShell header. Keyed by appId.
|
||||
*/
|
||||
|
||||
export interface ModuleHelp {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
export { default as PageShell } from './PageShell.svelte';
|
||||
export { default as PageCarousel } from './PageCarousel.svelte';
|
||||
|
||||
export type { CarouselPage } from './types';
|
||||
|
|
|
|||
|
|
@ -1,7 +1,23 @@
|
|||
<!--
|
||||
PageShell — Shared card wrapper for pages in a carousel.
|
||||
Provides: header, five preset widths, maximized mode.
|
||||
Used by workbench (AppPage) and todo (TodoPage).
|
||||
ModuleShell — Canonical card shell for every Mana module surface.
|
||||
|
||||
Replaces the old PageShell + AppPage split. A single component serves
|
||||
both rendering paths:
|
||||
|
||||
variant="card" — width-sized, sits in a PageCarousel next to siblings.
|
||||
Window actions (close / move / resize / maximize)
|
||||
belong here. This is what the homepage and the
|
||||
legacy per-module carousels (e.g. /todo) use.
|
||||
|
||||
variant="fill" — fills the main content area. Sub-routes
|
||||
(/picture, /picture/generate, /library, …) use
|
||||
this. Back-button replaces the close button when
|
||||
a backHref is provided. No carousel window
|
||||
actions.
|
||||
|
||||
Visual chrome (paper-texture, soft border, rounded corners, shadow,
|
||||
header bar) is identical across both variants so the homepage and
|
||||
sub-routes read as the same system.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { _ } from 'svelte-i18n';
|
||||
|
|
@ -13,52 +29,74 @@
|
|||
CaretLeft,
|
||||
CaretRight,
|
||||
ArrowsOutLineHorizontal,
|
||||
ArrowLeft,
|
||||
Question,
|
||||
} from '@mana/shared-icons';
|
||||
import type { Snippet, Component } from 'svelte';
|
||||
import { PAGE_WIDTH_PRESETS, nearestPresetIndex } from './width-presets';
|
||||
import { PAGE_WIDTH_PRESETS, nearestPresetIndex } from '../page-carousel/width-presets';
|
||||
|
||||
interface Props {
|
||||
widthPx: number;
|
||||
// Layout mode
|
||||
variant?: 'card' | 'fill';
|
||||
/** Required for variant="card" unless maximized. */
|
||||
widthPx?: number;
|
||||
maximized?: boolean;
|
||||
onClose: () => void;
|
||||
onMinimize?: () => void;
|
||||
onMaximize?: () => void;
|
||||
onResize?: (widthPx: number) => void;
|
||||
onMoveLeft?: () => void;
|
||||
onMoveRight?: () => void;
|
||||
onHelp?: () => void;
|
||||
helpOpen?: boolean;
|
||||
// Default header
|
||||
|
||||
// Header content
|
||||
title?: string;
|
||||
titleHref?: string;
|
||||
color?: string;
|
||||
icon?: Component;
|
||||
|
||||
// Card-mode actions (carousel window chrome)
|
||||
onClose?: () => void;
|
||||
onMaximize?: () => void;
|
||||
onResize?: (widthPx: number) => void;
|
||||
onMoveLeft?: () => void;
|
||||
onMoveRight?: () => void;
|
||||
|
||||
// Fill-mode actions (route navigation)
|
||||
/** If provided, renders a back arrow in the header that navigates to this URL. */
|
||||
backHref?: string;
|
||||
/** Custom back handler. Takes precedence over backHref — use for history.back() or custom logic. */
|
||||
onBack?: () => void;
|
||||
|
||||
// Shared
|
||||
onHelp?: () => void;
|
||||
helpOpen?: boolean;
|
||||
onContextMenu?: (e: MouseEvent) => void;
|
||||
// Snippet overrides
|
||||
|
||||
// Snippets
|
||||
header_left?: Snippet;
|
||||
badge?: Snippet;
|
||||
/** Renders to the right of the title, before the window actions.
|
||||
* Use for view-specific controls (e.g. credit badge on /picture/generate). */
|
||||
actions?: Snippet;
|
||||
toolbar?: Snippet;
|
||||
children: Snippet;
|
||||
}
|
||||
|
||||
let {
|
||||
variant = 'card',
|
||||
widthPx,
|
||||
maximized = false,
|
||||
title = '',
|
||||
titleHref,
|
||||
color = '#6B7280',
|
||||
icon: IconComponent,
|
||||
onClose,
|
||||
onMaximize,
|
||||
onResize,
|
||||
onMoveLeft,
|
||||
onMoveRight,
|
||||
backHref,
|
||||
onBack,
|
||||
onHelp,
|
||||
helpOpen = false,
|
||||
onContextMenu,
|
||||
title = '',
|
||||
titleHref,
|
||||
color = '#6B7280',
|
||||
icon: IconComponent,
|
||||
header_left,
|
||||
badge,
|
||||
actions,
|
||||
toolbar,
|
||||
children,
|
||||
}: Props = $props();
|
||||
|
|
@ -79,7 +117,7 @@
|
|||
let widthMenuOpen = $state(false);
|
||||
let widthBtnEl = $state<HTMLButtonElement | null>(null);
|
||||
|
||||
const activePresetIdx = $derived(nearestPresetIndex(widthPx));
|
||||
const activePresetIdx = $derived(typeof widthPx === 'number' ? nearestPresetIndex(widthPx) : 0);
|
||||
|
||||
function selectWidth(px: number) {
|
||||
widthMenuOpen = false;
|
||||
|
|
@ -93,12 +131,33 @@
|
|||
widthBtnEl?.focus();
|
||||
}
|
||||
}
|
||||
|
||||
// ── Derived layout style ────────────────────────────────
|
||||
// Card: width is whatever the carousel drag-resizes it to.
|
||||
// Fill: width is 100% of the main container; layout's max-w-7xl caps it.
|
||||
const widthStyle = $derived(
|
||||
variant === 'card' ? `width: ${maximized ? '100%' : `${widthPx ?? 480}px`};` : 'width: 100%;'
|
||||
);
|
||||
|
||||
const showCardActions = $derived(variant === 'card');
|
||||
const showBackButton = $derived(variant === 'fill' && (backHref || onBack));
|
||||
</script>
|
||||
|
||||
<div class="page-shell" class:maximized style="width: {maximized ? '100%' : `${widthPx}px`};">
|
||||
<!-- Header with window actions -->
|
||||
<div class="page-header" oncontextmenu={onContextMenu} role="banner">
|
||||
<div class="module-shell" class:maximized class:fill={variant === 'fill'} style={widthStyle}>
|
||||
<!-- Header with window actions / back button -->
|
||||
<div class="shell-header" oncontextmenu={onContextMenu} role="banner">
|
||||
<div class="header-left">
|
||||
{#if showBackButton}
|
||||
{#if onBack}
|
||||
<button class="back-btn" onclick={onBack} title="Zurück">
|
||||
<ArrowLeft size={16} weight="bold" />
|
||||
</button>
|
||||
{:else if backHref}
|
||||
<a class="back-btn" href={backHref} title="Zurück">
|
||||
<ArrowLeft size={16} weight="bold" />
|
||||
</a>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if header_left}
|
||||
{@render header_left()}
|
||||
{:else}
|
||||
|
|
@ -111,7 +170,7 @@
|
|||
{/if}
|
||||
{#if titleHref}
|
||||
<a
|
||||
class="page-title page-title-link"
|
||||
class="shell-title shell-title-link"
|
||||
href={titleHref}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
|
|
@ -121,14 +180,18 @@
|
|||
{title}
|
||||
</a>
|
||||
{:else}
|
||||
<span class="page-title">{title}</span>
|
||||
<span class="shell-title">{title}</span>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if badge}
|
||||
{@render badge()}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="window-actions">
|
||||
{#if actions}
|
||||
{@render actions()}
|
||||
{/if}
|
||||
{#if onHelp}
|
||||
<button
|
||||
class="window-btn"
|
||||
|
|
@ -142,7 +205,7 @@
|
|||
<Question size={22} weight="bold" />
|
||||
</button>
|
||||
{/if}
|
||||
{#if onMoveLeft}
|
||||
{#if showCardActions && onMoveLeft}
|
||||
<button
|
||||
class="window-btn"
|
||||
onclick={(e) => {
|
||||
|
|
@ -154,7 +217,7 @@
|
|||
<CaretLeft size={24} weight="bold" />
|
||||
</button>
|
||||
{/if}
|
||||
{#if onMoveRight}
|
||||
{#if showCardActions && onMoveRight}
|
||||
<button
|
||||
class="window-btn"
|
||||
onclick={(e) => {
|
||||
|
|
@ -166,7 +229,7 @@
|
|||
<CaretRight size={24} weight="bold" />
|
||||
</button>
|
||||
{/if}
|
||||
{#if onResize && !maximized}
|
||||
{#if showCardActions && onResize && !maximized}
|
||||
<div class="width-picker-wrapper">
|
||||
<button
|
||||
bind:this={widthBtnEl}
|
||||
|
|
@ -222,16 +285,18 @@
|
|||
/>{/if}
|
||||
</button>
|
||||
{/if}
|
||||
<button
|
||||
class="window-btn window-btn-close"
|
||||
onclick={(e) => {
|
||||
e.stopPropagation();
|
||||
onClose();
|
||||
}}
|
||||
title={$_('common.close')}
|
||||
>
|
||||
<X size={24} weight="bold" />
|
||||
</button>
|
||||
{#if showCardActions && onClose}
|
||||
<button
|
||||
class="window-btn window-btn-close"
|
||||
onclick={(e) => {
|
||||
e.stopPropagation();
|
||||
onClose();
|
||||
}}
|
||||
title={$_('common.close')}
|
||||
>
|
||||
<X size={24} weight="bold" />
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -241,32 +306,21 @@
|
|||
{/if}
|
||||
|
||||
<!-- Body -->
|
||||
<div class="page-body">
|
||||
<div class="shell-body">
|
||||
{@render children()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* P5: theme-token migration. The workbench paper card now follows the
|
||||
active theme variant, incl. a per-theme paper-grain texture applied
|
||||
via background-blend-mode (more robust than a ::before overlay +
|
||||
mix-blend-mode + opacity combo, which had invisibility issues in
|
||||
dark mode and stacking-context quirks). CSS vars come from
|
||||
applyThemeToDocument() in @mana/shared-theme — swap one line in
|
||||
THEME_DEFINITIONS to change the texture for a whole theme. */
|
||||
.page-shell {
|
||||
/* Paper card: active-theme-aware texture applied via background-blend-mode.
|
||||
CSS vars come from applyThemeToDocument() in @mana/shared-theme.
|
||||
See git log of PageShell.svelte for the blend-mode rationale — the
|
||||
::before overlay pattern failed in dark mode due to stacking-context
|
||||
quirks. */
|
||||
.module-shell {
|
||||
flex: 0 0 auto;
|
||||
/* Default page height fills the viewport between the workbench
|
||||
top padding and the bottom chrome (pill nav + tag strip +
|
||||
bottom bar). Two CSS vars cascade from the layout's <main>:
|
||||
- --bottom-chrome-height reacts to pill-nav collapse, tag
|
||||
strip visibility and bottom-bar mount state
|
||||
- --workbench-reserved-y collapses the py-* wrapper padding
|
||||
plus a small buffer into a single "non-chrome vertical"
|
||||
number so this calc doesn't have to mirror DOM padding
|
||||
`dvh` accounts for mobile Safari's retractable address bar.
|
||||
An inline `height: {px}px` style from the resize-drag prop
|
||||
overrides this value (same specificity rule as before). */
|
||||
/* Height calc uses layout-supplied CSS vars (--bottom-chrome-height,
|
||||
--workbench-reserved-y) from routes/(app)/+layout.svelte's <main>. */
|
||||
height: calc(100dvh - var(--bottom-chrome-height, 80px) - var(--workbench-reserved-y, 2.5rem));
|
||||
min-height: 320px;
|
||||
max-width: calc(100vw - 2rem);
|
||||
|
|
@ -275,9 +329,6 @@
|
|||
background-size: var(--paper-size, 240px 240px);
|
||||
background-repeat: repeat;
|
||||
background-blend-mode: var(--paper-blend-mode, multiply);
|
||||
/* Soft black border — 12% in light mode, bumped to 28% in dark
|
||||
mode (see :global(.dark) override below) where a low-alpha
|
||||
black would otherwise vanish into the background. */
|
||||
border: 2px solid hsl(0 0% 0% / 0.12);
|
||||
border-radius: 1.25rem;
|
||||
box-shadow:
|
||||
|
|
@ -289,20 +340,23 @@
|
|||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
/* Fill variant: no fade-in-from-side (that was a carousel affordance),
|
||||
and max-width drops since the layout already caps at max-w-7xl. */
|
||||
.module-shell.fill {
|
||||
animation: fadeIn 0.2s ease-out;
|
||||
max-width: none;
|
||||
}
|
||||
/* Dark-mode border needs more alpha to stay visible against the
|
||||
dark card background. */
|
||||
:global(.dark) .page-shell {
|
||||
:global(.dark) .module-shell {
|
||||
border-color: hsl(0 0% 0% / 0.28);
|
||||
}
|
||||
|
||||
/* A11y: users who asked for reduced transparency/contrast drop the
|
||||
texture entirely — solid card only. */
|
||||
@media (prefers-contrast: more) {
|
||||
.page-shell {
|
||||
.module-shell {
|
||||
background-image: none;
|
||||
}
|
||||
}
|
||||
.page-shell.maximized {
|
||||
.module-shell.maximized {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 95;
|
||||
|
|
@ -335,9 +389,23 @@
|
|||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
/* Fill variant enters from below, not from the side. */
|
||||
.module-shell.fill {
|
||||
animation-name: fadeInUp;
|
||||
}
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.page-header {
|
||||
.shell-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
|
@ -351,6 +419,25 @@
|
|||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.back-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
text-decoration: none;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.back-btn:hover {
|
||||
background: hsl(var(--color-surface-hover, var(--color-muted)));
|
||||
color: hsl(var(--color-foreground));
|
||||
}
|
||||
.header-icon {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
|
|
@ -363,7 +450,7 @@
|
|||
border-radius: 9999px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.page-title {
|
||||
.shell-title {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--color-foreground));
|
||||
|
|
@ -373,12 +460,12 @@
|
|||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
a.page-title-link {
|
||||
a.shell-title-link {
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
a.page-title-link:hover {
|
||||
a.shell-title-link:hover {
|
||||
color: hsl(var(--color-primary));
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
|
@ -403,36 +490,35 @@
|
|||
transition: all 0.15s;
|
||||
}
|
||||
.window-btn:hover {
|
||||
background: hsl(var(--color-surface-hover));
|
||||
background: hsl(var(--color-surface-hover, var(--color-muted)));
|
||||
color: hsl(var(--color-foreground));
|
||||
}
|
||||
.window-btn-close:hover {
|
||||
background: hsl(var(--color-error) / 0.15);
|
||||
color: hsl(var(--color-error));
|
||||
}
|
||||
.window-btn-active {
|
||||
background: hsl(var(--color-primary) / 0.12);
|
||||
color: hsl(var(--color-primary));
|
||||
}
|
||||
|
||||
/* Body */
|
||||
.page-body {
|
||||
.shell-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
min-height: 200px;
|
||||
}
|
||||
.maximized .page-header {
|
||||
.maximized .shell-header {
|
||||
max-width: 48rem;
|
||||
margin-inline: auto;
|
||||
width: 100%;
|
||||
}
|
||||
.maximized .page-body {
|
||||
.maximized .shell-body {
|
||||
max-width: 48rem;
|
||||
margin-inline: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.window-btn-active {
|
||||
background: hsl(var(--color-primary) / 0.12);
|
||||
color: hsl(var(--color-primary));
|
||||
}
|
||||
|
||||
/* Width picker */
|
||||
.width-picker-wrapper {
|
||||
position: relative;
|
||||
59
apps/mana/apps/web/src/lib/components/shell/RoutePage.svelte
Normal file
59
apps/mana/apps/web/src/lib/components/shell/RoutePage.svelte
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
<!--
|
||||
RoutePage — auto-metadata wrapper around ModuleShell for sub-route pages.
|
||||
|
||||
Looks up title / icon / color from the app-registry by appId so every
|
||||
(app)/{appId}/+page.svelte can stay three lines:
|
||||
|
||||
<RoutePage appId="library">
|
||||
<ListView />
|
||||
</RoutePage>
|
||||
|
||||
For custom titles (sub-views like /picture/generate), custom actions,
|
||||
or a back button, the caller can override or use ModuleShell directly.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
import { getApp } from '$lib/app-registry';
|
||||
import { ModuleShell } from './index';
|
||||
|
||||
interface Props {
|
||||
/** App descriptor id from the registry (matches apps.ts `id:`). */
|
||||
appId: string;
|
||||
/** Override the registry title (useful for sub-views). */
|
||||
title?: string;
|
||||
/** Back button target. When set, header shows a back arrow instead of nothing. */
|
||||
backHref?: string;
|
||||
onBack?: () => void;
|
||||
/** Right-side header slot for view-specific controls. */
|
||||
actions?: Snippet;
|
||||
/** Toolbar slot rendered below the header, above the body. */
|
||||
toolbar?: Snippet;
|
||||
children: Snippet;
|
||||
}
|
||||
|
||||
let {
|
||||
appId,
|
||||
title: titleOverride,
|
||||
backHref,
|
||||
onBack,
|
||||
actions,
|
||||
toolbar,
|
||||
children,
|
||||
}: Props = $props();
|
||||
|
||||
const app = $derived(getApp(appId));
|
||||
const resolvedTitle = $derived(titleOverride ?? app?.name ?? appId);
|
||||
</script>
|
||||
|
||||
<ModuleShell
|
||||
variant="fill"
|
||||
title={resolvedTitle}
|
||||
icon={app?.icon}
|
||||
color={app?.color}
|
||||
{backHref}
|
||||
{onBack}
|
||||
{actions}
|
||||
{toolbar}
|
||||
>
|
||||
{@render children()}
|
||||
</ModuleShell>
|
||||
2
apps/mana/apps/web/src/lib/components/shell/index.ts
Normal file
2
apps/mana/apps/web/src/lib/components/shell/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export { default as ModuleShell } from './ModuleShell.svelte';
|
||||
export { default as RoutePage } from './RoutePage.svelte';
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
<script lang="ts">
|
||||
import { X, CaretUp, CaretDown, ArrowLeft, SpinnerGap } from '@mana/shared-icons';
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { PageShell } from '$lib/components/page-carousel';
|
||||
import { ModuleShell } from '$lib/components/shell';
|
||||
import { getApp, getAppByDragType, canDrop, executeDrop } from '$lib/app-registry';
|
||||
import type { Component } from 'svelte';
|
||||
import { dropTarget } from '@mana/shared-ui/dnd';
|
||||
|
|
@ -295,8 +295,8 @@
|
|||
canDrop: (p) => canDrop(p.type, appId),
|
||||
}}
|
||||
>
|
||||
<!-- Base: PageShell with list view (always visible) -->
|
||||
<PageShell
|
||||
<!-- Base: ModuleShell with list view (always visible) -->
|
||||
<ModuleShell
|
||||
{widthPx}
|
||||
{maximized}
|
||||
title={appName}
|
||||
|
|
@ -351,7 +351,7 @@
|
|||
<SpinnerGap size={24} class="spinner" />
|
||||
</div>
|
||||
{/if}
|
||||
</PageShell>
|
||||
</ModuleShell>
|
||||
|
||||
<!-- Overlay: Detail view floating above -->
|
||||
{#if overlay?.component || closing}
|
||||
|
|
@ -409,12 +409,12 @@
|
|||
.app-page-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
:global(.app-page-wrapper.mana-drop-target-hover) :global(.page-shell) {
|
||||
:global(.app-page-wrapper.mana-drop-target-hover) :global(.module-shell) {
|
||||
outline: 2px solid hsl(var(--color-primary) / 0.5);
|
||||
outline-offset: -2px;
|
||||
box-shadow: 0 0 20px rgba(139, 92, 246, 0.15);
|
||||
}
|
||||
:global(.app-page-wrapper.mana-drop-target-success) :global(.page-shell) {
|
||||
:global(.app-page-wrapper.mana-drop-target-success) :global(.module-shell) {
|
||||
outline: 2px solid hsl(var(--color-success) / 0.5);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<!--
|
||||
AI Missions app — workbench card.
|
||||
Renders inside AppPage, which provides the PageShell + window chrome.
|
||||
Renders inside AppPage, which provides the ModuleShell + window chrome.
|
||||
Master-detail inline (list ↔ create ↔ detail) in a single panel.
|
||||
-->
|
||||
<script lang="ts">
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@
|
|||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
/* Innen-Padding als Single-Source-of-Truth. In Workbench-Karten */
|
||||
/* hat PageShell's `.page-body` null padding — ohne das hier würde */
|
||||
/* hat ModuleShell's `.shell-body` null padding — ohne das hier würde */
|
||||
/* der QuickAdd-Input direkt am Card-Rand kleben. Im Route-Kontext */
|
||||
/* liegt dieses Padding innerhalb des (app)-Layout-Wrappers und */
|
||||
/* ergibt insgesamt ein ruhig gespaciedes Bild. */
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<!--
|
||||
ContactPage — A single page in the contacts carousel.
|
||||
Shows a filtered/sorted contact list inside a PageShell.
|
||||
Shows a filtered/sorted contact list inside a ModuleShell.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { isToday, differenceInDays, startOfDay, setYear } from 'date-fns';
|
||||
|
|
@ -19,7 +19,7 @@
|
|||
MapPin,
|
||||
Clock,
|
||||
} from '@mana/shared-icons';
|
||||
import { PageShell } from '$lib/components/page-carousel';
|
||||
import { ModuleShell } from '$lib/components/shell';
|
||||
import type { Contact } from '../../types';
|
||||
import { SELF_CONTACT_ID } from '../../collections';
|
||||
import {
|
||||
|
|
@ -211,14 +211,13 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<PageShell
|
||||
<ModuleShell
|
||||
{widthPx}
|
||||
{maximized}
|
||||
title={meta.title}
|
||||
color={meta.color}
|
||||
icon={meta.icon}
|
||||
{onClose}
|
||||
{onMinimize}
|
||||
{onMaximize}
|
||||
{onResize}
|
||||
>
|
||||
|
|
@ -252,7 +251,7 @@
|
|||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</PageShell>
|
||||
</ModuleShell>
|
||||
|
||||
{#snippet profileCard(contact: Contact)}
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
Leaf,
|
||||
Heart,
|
||||
} from '@mana/shared-icons';
|
||||
import { PageShell } from '$lib/components/page-carousel';
|
||||
import { ModuleShell } from '$lib/components/shell';
|
||||
|
||||
interface Props {
|
||||
pageId: string;
|
||||
|
|
@ -223,13 +223,12 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<PageShell
|
||||
<ModuleShell
|
||||
{widthPx}
|
||||
{maximized}
|
||||
color={displayColor}
|
||||
icon={IconComponent}
|
||||
{onClose}
|
||||
{onMinimize}
|
||||
{onMaximize}
|
||||
{onResize}
|
||||
>
|
||||
|
|
@ -310,7 +309,7 @@
|
|||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</PageShell>
|
||||
</ModuleShell>
|
||||
|
||||
<style>
|
||||
.header-icon {
|
||||
|
|
|
|||
|
|
@ -1001,7 +1001,7 @@
|
|||
|
||||
<!-- Main content.
|
||||
Publish layout offsets as CSS variables so descendants (esp.
|
||||
PageShell in the carousel) can compute their available
|
||||
ModuleShell in the carousel) can compute their available
|
||||
height against viewport + bottom chrome without prop
|
||||
drilling. `--workbench-top-offset` must match the vertical
|
||||
padding on the inner max-w-7xl wrapper below. -->
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue