mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 22:01:09 +02:00
feat(workbench): render scene header left of the first page
New SceneHeader component: big scene name (clamp 2.75rem–4.5rem responsive) plus a muted description underneath, or an italic "Beschreibung hinzufügen…" placeholder when empty. The whole block is a button — clicking it opens the existing scene edit dialog, now pulling double duty for both name and description. Wired through PageCarousel's new leading snippet from the previous commit, so the header scrolls with the track and stays anchored to the visual start of the carousel without needing a second scroll container. SceneRenameDialog grows a description textarea (maxlength 240, 3 rows, vertically resizable) and onSubmit now passes (name, description). The caller translates an empty description to null so the DB column reflects "no description set" rather than an empty string — keeps WorkbenchScene.description truthy checks honest. handleEditActiveScene resolves the currently-active scene and opens the dialog pre-filled; used by the SceneHeader click. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8f3ffefdf1
commit
db8e681120
3 changed files with 133 additions and 7 deletions
|
|
@ -0,0 +1,87 @@
|
|||
<!--
|
||||
SceneHeader — large title + description shown left of the first page
|
||||
in the workbench carousel. Part of the scroll track, so it slides away
|
||||
as the user moves right (intentional: it's an intro block, not chrome).
|
||||
Click opens the existing rename dialog for editing name + description.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { WorkbenchScene } from '$lib/types/workbench-scenes';
|
||||
|
||||
interface Props {
|
||||
scene: WorkbenchScene | null;
|
||||
onEdit: () => void;
|
||||
}
|
||||
|
||||
const { scene, onEdit }: Props = $props();
|
||||
</script>
|
||||
|
||||
{#if scene}
|
||||
<button
|
||||
type="button"
|
||||
class="scene-header"
|
||||
onclick={onEdit}
|
||||
aria-label="Szene bearbeiten"
|
||||
title="Klicken, um Name und Beschreibung zu bearbeiten"
|
||||
>
|
||||
<h1 class="scene-name">{scene.name}</h1>
|
||||
<p class="scene-desc" class:placeholder={!scene.description}>
|
||||
{scene.description || 'Beschreibung hinzufügen…'}
|
||||
</p>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.scene-header {
|
||||
width: 420px;
|
||||
max-width: 60vw;
|
||||
padding: 2rem 2.5rem 2rem 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
color: hsl(var(--color-foreground));
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
.scene-header:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
.scene-header:focus-visible {
|
||||
outline: 2px solid hsl(var(--color-ring));
|
||||
outline-offset: 0.5rem;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.scene-name {
|
||||
margin: 0;
|
||||
font-size: clamp(2.75rem, 5vw, 4.5rem);
|
||||
font-weight: 700;
|
||||
line-height: 1.05;
|
||||
letter-spacing: -0.02em;
|
||||
color: hsl(var(--color-foreground));
|
||||
}
|
||||
.scene-desc {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
line-height: 1.45;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
max-width: 32ch;
|
||||
}
|
||||
.scene-desc.placeholder {
|
||||
opacity: 0.55;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@media (max-width: 639px) {
|
||||
.scene-header {
|
||||
width: 280px;
|
||||
padding: 1.25rem 1.5rem 1.25rem 0;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.scene-desc {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -6,8 +6,9 @@
|
|||
show: boolean;
|
||||
title: string;
|
||||
initialName?: string;
|
||||
initialDescription?: string;
|
||||
confirmLabel?: string;
|
||||
onSubmit: (name: string) => void | Promise<void>;
|
||||
onSubmit: (name: string, description: string) => void | Promise<void>;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
|
|
@ -15,18 +16,21 @@
|
|||
show,
|
||||
title,
|
||||
initialName = '',
|
||||
initialDescription = '',
|
||||
confirmLabel = 'Speichern',
|
||||
onSubmit,
|
||||
onCancel,
|
||||
}: Props = $props();
|
||||
|
||||
let name = $state('');
|
||||
let description = $state('');
|
||||
let pending = $state(false);
|
||||
let inputEl = $state<HTMLInputElement | null>(null);
|
||||
|
||||
$effect(() => {
|
||||
if (show) {
|
||||
name = initialName;
|
||||
description = initialDescription;
|
||||
queueMicrotask(() => inputEl?.focus());
|
||||
}
|
||||
});
|
||||
|
|
@ -36,7 +40,7 @@
|
|||
if (pending || !name.trim()) return;
|
||||
pending = true;
|
||||
try {
|
||||
await onSubmit(name.trim());
|
||||
await onSubmit(name.trim(), description);
|
||||
} finally {
|
||||
pending = false;
|
||||
}
|
||||
|
|
@ -75,6 +79,16 @@
|
|||
required
|
||||
/>
|
||||
</label>
|
||||
<label class="srd-field">
|
||||
<span class="srd-label">Beschreibung</span>
|
||||
<textarea
|
||||
class="srd-input srd-textarea"
|
||||
maxlength="240"
|
||||
rows="3"
|
||||
placeholder="Wofür ist diese Szene gedacht?"
|
||||
bind:value={description}
|
||||
></textarea>
|
||||
</label>
|
||||
<div class="srd-actions">
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -153,6 +167,12 @@
|
|||
.srd-input:focus {
|
||||
border-color: hsl(var(--color-primary));
|
||||
}
|
||||
.srd-textarea {
|
||||
resize: vertical;
|
||||
min-height: 4rem;
|
||||
font-family: inherit;
|
||||
line-height: 1.45;
|
||||
}
|
||||
.srd-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import AppPagePicker from '$lib/components/workbench/AppPagePicker.svelte';
|
||||
import SceneAppBar from '$lib/components/workbench/SceneAppBar.svelte';
|
||||
import SceneRenameDialog from '$lib/components/workbench/scenes/SceneRenameDialog.svelte';
|
||||
import SceneHeader from '$lib/components/workbench/scenes/SceneHeader.svelte';
|
||||
import ConfirmDialog from '$lib/components/workbench/scenes/ConfirmDialog.svelte';
|
||||
import { PageCarousel, type CarouselPage } from '$lib/components/page-carousel';
|
||||
import { getApp, getAppByDragType, isAppAccessible } from '$lib/app-registry';
|
||||
|
|
@ -269,21 +270,35 @@
|
|||
}
|
||||
|
||||
// ── Scene CRUD dialogs ──────────────────────────────────
|
||||
type SceneDialogMode = { kind: 'rename'; id: string; name: string };
|
||||
type SceneDialogMode = {
|
||||
kind: 'rename';
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
};
|
||||
let sceneDialog = $state<SceneDialogMode | null>(null);
|
||||
let sceneToDelete = $state<{ id: string; name: string } | null>(null);
|
||||
|
||||
function handleRequestRename(id: string) {
|
||||
const scene = scenes.find((s) => s.id === id);
|
||||
if (!scene) return;
|
||||
sceneDialog = { kind: 'rename', id, name: scene.name };
|
||||
sceneDialog = {
|
||||
kind: 'rename',
|
||||
id,
|
||||
name: scene.name,
|
||||
description: scene.description ?? '',
|
||||
};
|
||||
}
|
||||
async function handleSubmitSceneDialog(name: string) {
|
||||
async function handleSubmitSceneDialog(name: string, description: string) {
|
||||
const mode = sceneDialog;
|
||||
if (!mode) return;
|
||||
await workbenchScenesStore.renameScene(mode.id, name);
|
||||
await workbenchScenesStore.renameScene(mode.id, name, description.trim() || null);
|
||||
sceneDialog = null;
|
||||
}
|
||||
function handleEditActiveScene() {
|
||||
const active = workbenchScenesStore.activeScene;
|
||||
if (active) handleRequestRename(active.id);
|
||||
}
|
||||
function handleDuplicateScene(id: string) {
|
||||
workbenchScenesStore.duplicateScene(id);
|
||||
}
|
||||
|
|
@ -336,6 +351,9 @@
|
|||
userTier={authStore.user?.tier}
|
||||
/>
|
||||
{/snippet}
|
||||
{#snippet leading()}
|
||||
<SceneHeader scene={workbenchScenesStore.activeScene} onEdit={handleEditActiveScene} />
|
||||
{/snippet}
|
||||
</PageCarousel>
|
||||
|
||||
<ContextMenu
|
||||
|
|
@ -348,8 +366,9 @@
|
|||
|
||||
<SceneRenameDialog
|
||||
show={sceneDialog !== null}
|
||||
title="Szene umbenennen"
|
||||
title="Szene bearbeiten"
|
||||
initialName={sceneDialog?.name ?? ''}
|
||||
initialDescription={sceneDialog?.description ?? ''}
|
||||
confirmLabel="Speichern"
|
||||
onSubmit={handleSubmitSceneDialog}
|
||||
onCancel={() => (sceneDialog = null)}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue