mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-23 21:16:41 +02:00
feat(website): M3 — 5 more blocks, containers, upload, themes
Expands the builder from 3 M1 blocks to 8. Containers (columns) and
media blocks (image, gallery) are the structural additions; cta and faq
round out the content coverage.
packages/website-blocks:
- image, cta, faq, columns (container), gallery — each with Zod schema,
renderer (mode-aware for edit/preview/public), and fallback inspector.
- Block type extended with optional `children` + `renderChild` snippet
so containers render their children through the same chrome the
outer renderer provides (click-to-select, public-path tagging).
- themes/: 3 presets (classic light, modern dark, warm) with
`resolveTheme` + `themeCssVars` helpers. Public layout now emits
CSS vars via `style=` on the root; block components read
`var(--wb-primary)` / `var(--wb-bg)` / `var(--wb-fg)` / etc.
- Registry updated; new exports + `./themes` subpath export.
apps/mana/apps/web/src/lib/modules/website:
- upload.ts: multipart POST to mana-media with `app=website` scope,
returns { mediaId, url }. 25 MB cap, non-image rejection client-side.
- components/ImageInspector + GalleryInspector: app-side overrides
wired to upload. Registered via `CUSTOM_INSPECTORS` in BlockInspector
so block.type → app-side inspector, fallback to registry otherwise.
- components/SiteSettingsDialog: theme preset picker + color overrides
for primary/bg/fg + footer text. Mounted from a ⚙ button in the
editor's left pane.
- components/BlockRenderer: rebuilt around a byParent map + recursive
`renderBlock` snippet so container blocks can render their children
through the same click-to-select wrapper as top-level blocks.
- routes/s/[siteSlug]: rename `[[...path]]` → `[...path]` (SvelteKit
treats rest segments as optional automatically — double-bracket form
errored at sync time). +page.svelte renders snapshot trees
recursively so published pages match the editor.
apps/api: unchanged.
Validation:
- pnpm run validate:all: all 6 gates green
- pnpm run check (web): 0 errors, 0 warnings
- apps/api type-check: green
- website-blocks tsc: green
Plan: docs/plans/website-builder.md (M3 block shipped)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
25c3bb6cdf
commit
7a4f8894e1
36 changed files with 2899 additions and 40 deletions
108
packages/website-blocks/src/cta/Cta.svelte
Normal file
108
packages/website-blocks/src/cta/Cta.svelte
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
<script lang="ts">
|
||||
import type { BlockRenderProps } from '../types';
|
||||
import type { CtaProps } from './schema';
|
||||
|
||||
let { block, mode }: BlockRenderProps<CtaProps> = $props();
|
||||
|
||||
const isEdit = $derived(mode === 'edit');
|
||||
</script>
|
||||
|
||||
<section
|
||||
class="wb-cta"
|
||||
class:wb-cta--left={block.props.align === 'left'}
|
||||
class:wb-cta--center={block.props.align === 'center'}
|
||||
class:wb-cta--bg-subtle={block.props.background === 'subtle'}
|
||||
class:wb-cta--bg-primary={block.props.background === 'primary'}
|
||||
data-mode={mode}
|
||||
>
|
||||
<div class="wb-cta__inner">
|
||||
{#if block.props.title}
|
||||
<h2>{block.props.title}</h2>
|
||||
{:else if isEdit}
|
||||
<h2 class="wb-placeholder">Titel im Inspector setzen</h2>
|
||||
{/if}
|
||||
|
||||
{#if block.props.description}
|
||||
<p class="wb-cta__description">{block.props.description}</p>
|
||||
{/if}
|
||||
|
||||
<a
|
||||
class="wb-cta__button wb-cta__button--{block.props.variant}"
|
||||
href={block.props.buttonHref || '#'}
|
||||
>
|
||||
{block.props.buttonLabel || (isEdit ? 'Button-Label fehlt' : '')}
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.wb-cta {
|
||||
padding: 3rem 1.5rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.wb-cta--bg-subtle {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
.wb-cta--bg-primary {
|
||||
background: var(--wb-primary, rgba(99, 102, 241, 0.9));
|
||||
color: white;
|
||||
}
|
||||
.wb-cta__inner {
|
||||
max-width: 42rem;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
.wb-cta--center .wb-cta__inner {
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
.wb-cta--left .wb-cta__inner {
|
||||
align-items: flex-start;
|
||||
text-align: left;
|
||||
}
|
||||
.wb-cta h2 {
|
||||
margin: 0;
|
||||
font-size: 1.75rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.wb-cta__description {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
opacity: 0.8;
|
||||
}
|
||||
.wb-cta__button {
|
||||
display: inline-block;
|
||||
padding: 0.625rem 1.25rem;
|
||||
border-radius: 9999px;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
font-size: 0.9375rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
.wb-cta__button--primary {
|
||||
background: var(--wb-primary, rgba(99, 102, 241, 0.9));
|
||||
color: white;
|
||||
}
|
||||
.wb-cta__button--secondary {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: inherit;
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
.wb-cta__button--ghost {
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
border: 1px solid currentColor;
|
||||
}
|
||||
.wb-cta--bg-primary .wb-cta__button--primary {
|
||||
background: white;
|
||||
color: var(--wb-primary, rgb(99, 102, 241));
|
||||
}
|
||||
.wb-placeholder {
|
||||
opacity: 0.35;
|
||||
font-style: italic;
|
||||
}
|
||||
</style>
|
||||
122
packages/website-blocks/src/cta/CtaInspector.svelte
Normal file
122
packages/website-blocks/src/cta/CtaInspector.svelte
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
<script lang="ts">
|
||||
import type { BlockInspectorProps } from '../types';
|
||||
import type { CtaProps } from './schema';
|
||||
|
||||
let { block, onChange }: BlockInspectorProps<CtaProps> = $props();
|
||||
</script>
|
||||
|
||||
<div class="wb-inspector">
|
||||
<label class="wb-field">
|
||||
<span>Titel</span>
|
||||
<input
|
||||
type="text"
|
||||
value={block.props.title}
|
||||
oninput={(e) => onChange({ title: e.currentTarget.value })}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label class="wb-field">
|
||||
<span>Beschreibung</span>
|
||||
<textarea
|
||||
rows="3"
|
||||
value={block.props.description}
|
||||
oninput={(e) => onChange({ description: e.currentTarget.value })}
|
||||
></textarea>
|
||||
</label>
|
||||
|
||||
<div class="wb-row">
|
||||
<label class="wb-field">
|
||||
<span>Button-Label *</span>
|
||||
<input
|
||||
type="text"
|
||||
value={block.props.buttonLabel}
|
||||
oninput={(e) => onChange({ buttonLabel: e.currentTarget.value })}
|
||||
/>
|
||||
</label>
|
||||
<label class="wb-field">
|
||||
<span>Button-Link</span>
|
||||
<input
|
||||
type="text"
|
||||
value={block.props.buttonHref}
|
||||
oninput={(e) => onChange({ buttonHref: e.currentTarget.value })}
|
||||
placeholder="https://… oder /kontakt"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="wb-row">
|
||||
<label class="wb-field">
|
||||
<span>Variante</span>
|
||||
<select
|
||||
value={block.props.variant}
|
||||
onchange={(e) => onChange({ variant: e.currentTarget.value as CtaProps['variant'] })}
|
||||
>
|
||||
<option value="primary">Primär</option>
|
||||
<option value="secondary">Sekundär</option>
|
||||
<option value="ghost">Ghost</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label class="wb-field">
|
||||
<span>Ausrichtung</span>
|
||||
<select
|
||||
value={block.props.align}
|
||||
onchange={(e) => onChange({ align: e.currentTarget.value as CtaProps['align'] })}
|
||||
>
|
||||
<option value="center">Zentriert</option>
|
||||
<option value="left">Linksbündig</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label class="wb-field">
|
||||
<span>Hintergrund</span>
|
||||
<select
|
||||
value={block.props.background}
|
||||
onchange={(e) => onChange({ background: e.currentTarget.value as CtaProps['background'] })}
|
||||
>
|
||||
<option value="none">Kein</option>
|
||||
<option value="subtle">Dezent</option>
|
||||
<option value="primary">Primärfarbe</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.wb-inspector {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
.wb-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
.wb-field > span {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
opacity: 0.7;
|
||||
}
|
||||
.wb-field input,
|
||||
.wb-field select,
|
||||
.wb-field textarea {
|
||||
width: 100%;
|
||||
padding: 0.5rem 0.625rem;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
color: inherit;
|
||||
font-family: inherit;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
.wb-field textarea {
|
||||
resize: vertical;
|
||||
min-height: 4.5rem;
|
||||
}
|
||||
.wb-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
</style>
|
||||
19
packages/website-blocks/src/cta/index.ts
Normal file
19
packages/website-blocks/src/cta/index.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import type { BlockSpec } from '../types';
|
||||
import Cta from './Cta.svelte';
|
||||
import CtaInspector from './CtaInspector.svelte';
|
||||
import { CtaSchema, CTA_DEFAULTS, type CtaProps } from './schema';
|
||||
|
||||
export const ctaBlockSpec: BlockSpec<CtaProps> = {
|
||||
type: 'cta',
|
||||
label: 'Call-to-Action',
|
||||
icon: 'megaphone',
|
||||
category: 'content',
|
||||
schema: CtaSchema,
|
||||
schemaVersion: 1,
|
||||
defaults: CTA_DEFAULTS,
|
||||
Component: Cta,
|
||||
Inspector: CtaInspector,
|
||||
};
|
||||
|
||||
export type { CtaProps };
|
||||
export { CtaSchema, CTA_DEFAULTS };
|
||||
23
packages/website-blocks/src/cta/schema.ts
Normal file
23
packages/website-blocks/src/cta/schema.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
export const CtaSchema = z.object({
|
||||
title: z.string().max(160).default(''),
|
||||
description: z.string().max(480).default(''),
|
||||
buttonLabel: z.string().min(1).max(60).default('Los geht’s'),
|
||||
buttonHref: z.string().max(512).default('#'),
|
||||
variant: z.enum(['primary', 'secondary', 'ghost']).default('primary'),
|
||||
align: z.enum(['left', 'center']).default('center'),
|
||||
background: z.enum(['none', 'subtle', 'primary']).default('subtle'),
|
||||
});
|
||||
|
||||
export type CtaProps = z.infer<typeof CtaSchema>;
|
||||
|
||||
export const CTA_DEFAULTS: CtaProps = {
|
||||
title: 'Bereit loszulegen?',
|
||||
description: '',
|
||||
buttonLabel: 'Jetzt starten',
|
||||
buttonHref: '#',
|
||||
variant: 'primary',
|
||||
align: 'center',
|
||||
background: 'subtle',
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue