mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-22 21:06: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
94
packages/website-blocks/src/columns/Columns.svelte
Normal file
94
packages/website-blocks/src/columns/Columns.svelte
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
<script lang="ts">
|
||||
import type { BlockRenderProps, Block } from '../types';
|
||||
import type { ColumnsProps } from './schema';
|
||||
import { columnSlotKeys } from './schema';
|
||||
|
||||
let { block, mode, children = [], renderChild }: BlockRenderProps<ColumnsProps> = $props();
|
||||
|
||||
const isEdit = $derived(mode === 'edit');
|
||||
const slots = $derived(columnSlotKeys(block.props.count));
|
||||
|
||||
function childrenForSlot(slot: string): Block[] {
|
||||
return (children ?? []).filter((c) => c.slotKey === slot);
|
||||
}
|
||||
</script>
|
||||
|
||||
<section
|
||||
class="wb-columns wb-columns--gap-{block.props.gap} wb-columns--align-{block.props.align}"
|
||||
class:wb-columns--2={block.props.count === 2}
|
||||
class:wb-columns--3={block.props.count === 3}
|
||||
class:wb-columns--stack={block.props.stackOnMobile}
|
||||
data-mode={mode}
|
||||
>
|
||||
{#each slots as slot, idx (slot)}
|
||||
{@const slotChildren = childrenForSlot(slot)}
|
||||
<div
|
||||
class="wb-column"
|
||||
data-slot={slot}
|
||||
data-slot-index={idx}
|
||||
class:wb-column--empty={slotChildren.length === 0}
|
||||
>
|
||||
{#if slotChildren.length === 0 && isEdit}
|
||||
<div class="wb-column__empty">Spalte {idx + 1}</div>
|
||||
{/if}
|
||||
{#each slotChildren as child (child.id)}
|
||||
{#if renderChild}
|
||||
{@render renderChild(child)}
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.wb-columns {
|
||||
display: grid;
|
||||
padding: 1.5rem;
|
||||
max-width: 72rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.wb-columns--2 {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
.wb-columns--3 {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
.wb-columns--gap-sm {
|
||||
gap: 0.75rem;
|
||||
}
|
||||
.wb-columns--gap-md {
|
||||
gap: 1.5rem;
|
||||
}
|
||||
.wb-columns--gap-lg {
|
||||
gap: 3rem;
|
||||
}
|
||||
.wb-columns--align-start {
|
||||
align-items: start;
|
||||
}
|
||||
.wb-columns--align-center {
|
||||
align-items: center;
|
||||
}
|
||||
.wb-columns--align-stretch {
|
||||
align-items: stretch;
|
||||
}
|
||||
.wb-column {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.wb-column__empty {
|
||||
padding: 1.5rem;
|
||||
text-align: center;
|
||||
border: 1px dashed rgba(127, 127, 127, 0.25);
|
||||
border-radius: 0.5rem;
|
||||
opacity: 0.4;
|
||||
font-size: 0.8125rem;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.wb-columns--stack {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
107
packages/website-blocks/src/columns/ColumnsInspector.svelte
Normal file
107
packages/website-blocks/src/columns/ColumnsInspector.svelte
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
<script lang="ts">
|
||||
import type { BlockInspectorProps } from '../types';
|
||||
import type { ColumnsProps } from './schema';
|
||||
|
||||
let { block, onChange }: BlockInspectorProps<ColumnsProps> = $props();
|
||||
</script>
|
||||
|
||||
<div class="wb-inspector">
|
||||
<label class="wb-field">
|
||||
<span>Spalten-Anzahl</span>
|
||||
<select
|
||||
value={String(block.props.count)}
|
||||
onchange={(e) => onChange({ count: Number(e.currentTarget.value) as ColumnsProps['count'] })}
|
||||
>
|
||||
<option value="2">2 Spalten</option>
|
||||
<option value="3">3 Spalten</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<div class="wb-row">
|
||||
<label class="wb-field">
|
||||
<span>Abstand</span>
|
||||
<select
|
||||
value={block.props.gap}
|
||||
onchange={(e) => onChange({ gap: e.currentTarget.value as ColumnsProps['gap'] })}
|
||||
>
|
||||
<option value="sm">Klein</option>
|
||||
<option value="md">Mittel</option>
|
||||
<option value="lg">Groß</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label class="wb-field">
|
||||
<span>Vertikale Ausrichtung</span>
|
||||
<select
|
||||
value={block.props.align}
|
||||
onchange={(e) => onChange({ align: e.currentTarget.value as ColumnsProps['align'] })}
|
||||
>
|
||||
<option value="start">Oben</option>
|
||||
<option value="center">Mittig</option>
|
||||
<option value="stretch">Gestreckt</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label class="wb-checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={block.props.stackOnMobile}
|
||||
onchange={(e) => onChange({ stackOnMobile: e.currentTarget.checked })}
|
||||
/>
|
||||
<span>Auf Mobile untereinander stapeln</span>
|
||||
</label>
|
||||
|
||||
<p class="wb-hint">
|
||||
Blöcke in eine Spalte hinzufügen: wähle oben die "+" Palette und füge Block ein — neue Blöcke
|
||||
werden der zuletzt aktiven Spalte zugewiesen. (In M4 kommt pro-Spalte-Insert-UI.)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.wb-inspector {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
.wb-field,
|
||||
.wb-checkbox {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
.wb-checkbox {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
.wb-field > span {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
opacity: 0.7;
|
||||
}
|
||||
.wb-field select {
|
||||
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-size: 0.875rem;
|
||||
}
|
||||
.wb-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
.wb-hint {
|
||||
margin: 0;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.6;
|
||||
line-height: 1.4;
|
||||
}
|
||||
</style>
|
||||
19
packages/website-blocks/src/columns/index.ts
Normal file
19
packages/website-blocks/src/columns/index.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import type { BlockSpec } from '../types';
|
||||
import Columns from './Columns.svelte';
|
||||
import ColumnsInspector from './ColumnsInspector.svelte';
|
||||
import { ColumnsSchema, COLUMNS_DEFAULTS, columnSlotKeys, type ColumnsProps } from './schema';
|
||||
|
||||
export const columnsBlockSpec: BlockSpec<ColumnsProps> = {
|
||||
type: 'columns',
|
||||
label: 'Spalten',
|
||||
icon: 'columns',
|
||||
category: 'layout',
|
||||
schema: ColumnsSchema,
|
||||
schemaVersion: 1,
|
||||
defaults: COLUMNS_DEFAULTS,
|
||||
Component: Columns,
|
||||
Inspector: ColumnsInspector,
|
||||
};
|
||||
|
||||
export type { ColumnsProps };
|
||||
export { ColumnsSchema, COLUMNS_DEFAULTS, columnSlotKeys };
|
||||
26
packages/website-blocks/src/columns/schema.ts
Normal file
26
packages/website-blocks/src/columns/schema.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
export const ColumnsSchema = z.object({
|
||||
count: z.union([z.literal(2), z.literal(3)]).default(2),
|
||||
gap: z.enum(['sm', 'md', 'lg']).default('md'),
|
||||
align: z.enum(['start', 'center', 'stretch']).default('stretch'),
|
||||
stackOnMobile: z.boolean().default(true),
|
||||
});
|
||||
|
||||
export type ColumnsProps = z.infer<typeof ColumnsSchema>;
|
||||
|
||||
export const COLUMNS_DEFAULTS: ColumnsProps = {
|
||||
count: 2,
|
||||
gap: 'md',
|
||||
align: 'stretch',
|
||||
stackOnMobile: true,
|
||||
};
|
||||
|
||||
/**
|
||||
* Slot keys that a `columns` container accepts. Children of this block
|
||||
* set `slotKey = 'col-0' | 'col-1' | 'col-2'` to be placed in that
|
||||
* column.
|
||||
*/
|
||||
export function columnSlotKeys(count: 2 | 3): readonly string[] {
|
||||
return count === 2 ? ['col-0', 'col-1'] : ['col-0', 'col-1', 'col-2'];
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue