From af1efca3eab7789cea5c5222a0ab27ddbbab9a4a Mon Sep 17 00:00:00 2001 From: Florian <45694132+flo-bit@users.noreply.github.com> Date: Thu, 16 Apr 2026 02:57:40 +0200 Subject: [PATCH] add hero section card --- src/lib/cards/_base/BaseCard/BaseCard.svelte | 6 +- src/lib/cards/index.ts | 4 +- .../cards/sections/HeroCard/Decoration.svelte | 84 ++++++ .../sections/HeroCard/EditingHeroCard.svelte | 118 ++++++++ .../cards/sections/HeroCard/HeroCard.svelte | 80 ++++++ .../sections/HeroCard/HeroCardSettings.svelte | 270 ++++++++++++++++++ src/lib/cards/sections/HeroCard/index.ts | 98 +++++++ src/lib/cards/types.ts | 3 + .../card-command/CardCommand.svelte | 2 + src/lib/website/EditBar.svelte | 10 +- src/lib/website/EditableWebsite.svelte | 5 +- src/lib/website/SaveModal.svelte | 11 +- 12 files changed, 684 insertions(+), 7 deletions(-) create mode 100644 src/lib/cards/sections/HeroCard/Decoration.svelte create mode 100644 src/lib/cards/sections/HeroCard/EditingHeroCard.svelte create mode 100644 src/lib/cards/sections/HeroCard/HeroCard.svelte create mode 100644 src/lib/cards/sections/HeroCard/HeroCardSettings.svelte create mode 100644 src/lib/cards/sections/HeroCard/index.ts diff --git a/src/lib/cards/_base/BaseCard/BaseCard.svelte b/src/lib/cards/_base/BaseCard/BaseCard.svelte index a0f2597..74aac40 100644 --- a/src/lib/cards/_base/BaseCard/BaseCard.svelte +++ b/src/lib/cards/_base/BaseCard/BaseCard.svelte @@ -4,7 +4,7 @@ import type { WithElementRef } from 'bits-ui'; import type { Snippet } from 'svelte'; import type { HTMLAttributes } from 'svelte/elements'; - import { getColor } from '../..'; + import { CardDefinitionsByType, getColor } from '../..'; const colors = { base: 'bg-base-200/50 dark:bg-base-950/50', @@ -35,6 +35,7 @@ }: BaseCardProps = $props(); let color = $derived(getColor(item)); + let noOverflow = $derived(CardDefinitionsByType[item.cardType]?.noOverflow ?? false);
+ import { getDidContext } from '$lib/website/context'; + import { getImage } from '$lib/helper'; + import type { HeroDecoration } from '.'; + + let { + decoration, + isEditing = false, + onclick + }: { + decoration: HeroDecoration; + isEditing?: boolean; + onclick?: () => void; + } = $props(); + + const did = getDidContext(); + const src = $derived(getImage(decoration as Record, did, 'image')); + const filled = $derived(Boolean(src)); + + const sideStyle = $derived( + decoration.side === 'left' + ? `left: 0; transform: translate(-65%, -50%) rotate(${decoration.rotation ?? 0}deg);` + : `right: 0; transform: translate(65%, -50%) rotate(${decoration.rotation ?? 0}deg);` + ); + + +{#snippet contents()} + + {#if decoration.title || decoration.subtitle} +
+ {#if decoration.title} + + {decoration.title} + + {/if} + {#if decoration.subtitle} + + {decoration.subtitle} + + {/if} +
+ {/if} +{/snippet} + +{#if filled && isEditing} + +{:else if filled} +
+ {@render contents()} +
+{:else if isEditing} + +{/if} diff --git a/src/lib/cards/sections/HeroCard/EditingHeroCard.svelte b/src/lib/cards/sections/HeroCard/EditingHeroCard.svelte new file mode 100644 index 0000000..8355250 --- /dev/null +++ b/src/lib/cards/sections/HeroCard/EditingHeroCard.svelte @@ -0,0 +1,118 @@ + + + + +
+ {#each decorations as decoration (decoration.id)} + openImagePicker(decoration.id)} /> + {/each} + +
+ {#if item.cardData.showBadge !== false} + + + + {/if} + + + + {#if item.cardData.showSubtitle !== false} + + {/if} + + {#if item.cardData.showButton !== false} +
+ +
+ {/if} +
+
diff --git a/src/lib/cards/sections/HeroCard/HeroCard.svelte b/src/lib/cards/sections/HeroCard/HeroCard.svelte new file mode 100644 index 0000000..7008db2 --- /dev/null +++ b/src/lib/cards/sections/HeroCard/HeroCard.svelte @@ -0,0 +1,80 @@ + + +
+ {#each decorations as decoration (decoration.id)} + + {/each} + +
+ {#if item.cardData.showBadge !== false && item.cardData.badge} + + {item.cardData.badge} + + {/if} + +

+ {item.cardData.title || 'My cool website'} +

+ + {#if item.cardData.showSubtitle !== false && item.cardData.subtitle} +

+ {item.cardData.subtitle} +

+ {/if} + + {#if item.cardData.showButton !== false && item.cardData.buttonText} + {#if item.cardData.buttonHref === '#login'} + + {:else} + + {item.cardData.buttonText} + + {/if} + {/if} +
+
diff --git a/src/lib/cards/sections/HeroCard/HeroCardSettings.svelte b/src/lib/cards/sections/HeroCard/HeroCardSettings.svelte new file mode 100644 index 0000000..1ccdc24 --- /dev/null +++ b/src/lib/cards/sections/HeroCard/HeroCardSettings.svelte @@ -0,0 +1,270 @@ + + +
+
+ +
+ item.cardData.showBadge !== false, (val) => (item.cardData.showBadge = val) + } + id="hero-show-badge" + aria-labelledby="hero-show-badge-label" + variant="secondary" + /> + +
+
+ item.cardData.showSubtitle !== false, (val) => (item.cardData.showSubtitle = val) + } + id="hero-show-subtitle" + aria-labelledby="hero-show-subtitle-label" + variant="secondary" + /> + +
+
+ item.cardData.showButton !== false, (val) => (item.cardData.showButton = val) + } + id="hero-show-button" + aria-labelledby="hero-show-button-label" + variant="secondary" + /> + +
+
+ +
+ + { + if (event.code === 'Enter') { + event.preventDefault(); + confirmUrl(); + } + }} + /> +
+ +
+ + { + return item.cardData.textAlign ?? 'center'; + }, + (value) => { + if (!value) return; + item.cardData.textAlign = value; + } + } + > + + + + + + + + + + + + + + + + +
+ +
+ + { + return item.cardData.verticalAlign ?? 'center'; + }, + (value) => { + if (!value) return; + item.cardData.verticalAlign = value; + } + } + > + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + {#if filledSlots.length === 0} +

+ Hover the hero card and click an empty slot to add a side image. Edit titles & subtitles + here once filled. +

+ {/if} + + {#each filledSlots as decoration (decoration.id)} +
+ , did, 'image')} + alt="" + class="size-12 shrink-0 rounded-lg object-cover" + /> +
+ + updateSlot(decoration.id, { title: (e.target as HTMLInputElement).value })} + class="text-xs" + /> + + updateSlot(decoration.id, { subtitle: (e.target as HTMLInputElement).value })} + class="text-xs" + /> + +
+
+ {/each} +
+
diff --git a/src/lib/cards/sections/HeroCard/index.ts b/src/lib/cards/sections/HeroCard/index.ts new file mode 100644 index 0000000..161212d --- /dev/null +++ b/src/lib/cards/sections/HeroCard/index.ts @@ -0,0 +1,98 @@ +import { COLUMNS } from '$lib'; +import { checkAndUploadImage } from '$lib/helper'; +import type { CardDefinition } from '../../types'; +import EditingHeroCard from './EditingHeroCard.svelte'; +import HeroCard from './HeroCard.svelte'; +import HeroCardSettings from './HeroCardSettings.svelte'; + +export type HeroDecoration = { + id: string; + side: 'left' | 'right'; + top: number; + rotation: number; + image?: any; + title?: string; + subtitle?: string; +}; + +export const DEFAULT_DECORATION_SLOTS: HeroDecoration[] = [ + { id: 'slot-l-0', side: 'left', top: 8, rotation: -10 }, + { id: 'slot-l-1', side: 'left', top: 32, rotation: -4 }, + { id: 'slot-l-2', side: 'left', top: 58, rotation: -12 }, + { id: 'slot-l-3', side: 'left', top: 80, rotation: -6 }, + { id: 'slot-r-0', side: 'right', top: 10, rotation: 8 }, + { id: 'slot-r-1', side: 'right', top: 34, rotation: 12 }, + { id: 'slot-r-2', side: 'right', top: 60, rotation: 5 }, + { id: 'slot-r-3', side: 'right', top: 82, rotation: 11 } +]; + +export function getHeroDecorations(cardData: Record): HeroDecoration[] { + const stored = (cardData.decorations as HeroDecoration[]) ?? []; + return DEFAULT_DECORATION_SLOTS.map((slot) => { + const found = stored.find((d) => d.id === slot.id); + return found ? { ...slot, ...found } : slot; + }); +} + +export const HeroCardDefinition = { + type: 'hero', + contentComponent: HeroCard, + editingContentComponent: EditingHeroCard, + settingsComponent: HeroCardSettings, + upload: async (item) => { + const decorations = (item.cardData.decorations as HeroDecoration[]) ?? []; + for (const decoration of decorations) { + await checkAndUploadImage(decoration as Record, 'image'); + } + return item; + }, + createNew: (card) => { + card.cardType = 'hero'; + card.cardData = { + title: 'My cool website', + subtitle: 'A little something about me, what I do, and why you should stick around.', + badge: 'Welcome', + buttonText: 'Say hi', + buttonHref: '', + showBadge: true, + showSubtitle: true, + showButton: true, + textAlign: 'center', + verticalAlign: 'center', + decorations: [] + }; + + card.w = COLUMNS; + card.h = 7; + card.mobileW = COLUMNS; + card.mobileH = 10; + }, + + defaultColor: 'transparent', + allowSetColor: true, + canHaveLabel: false, + noOverflow: true, + + minW: COLUMNS, + minH: 4, + maxW: COLUMNS, + maxH: 16, + + name: 'Hero', + keywords: ['hero', 'header', 'landing', 'banner', 'intro', 'title', 'cta'], + groups: ['Sections'], + + icon: `` +} as CardDefinition & { type: 'hero' }; + +export const heroAlignClasses: Record = { + left: 'text-left items-start', + center: 'text-center items-center', + right: 'text-right items-end' +}; + +export const heroVerticalAlignClasses: Record = { + top: 'justify-start', + center: 'justify-center', + bottom: 'justify-end' +}; diff --git a/src/lib/cards/types.ts b/src/lib/cards/types.ts index e5e9c83..06b1a2a 100644 --- a/src/lib/cards/types.ts +++ b/src/lib/cards/types.ts @@ -75,6 +75,9 @@ export type CardDefinition = { canResize?: boolean; + // if true, content can render outside the card's bounds (no overflow:hidden on the inner wrapper) + noOverflow?: boolean; + canAdd?: (context: { collections: string[] }) => boolean; onUrlHandler?: (url: string, item: Item) => Item | null; diff --git a/src/lib/components/card-command/CardCommand.svelte b/src/lib/components/card-command/CardCommand.svelte index 1ba0e5f..28b6984 100644 --- a/src/lib/components/card-command/CardCommand.svelte +++ b/src/lib/components/card-command/CardCommand.svelte @@ -34,6 +34,8 @@ AllCardDefinitions.filter((d) => !d.canAdd || d.canAdd({ collections })) ); + $inspect(filteredCardDefs, 'filteredCardDefs'); + let cardDefGroups = $derived([ 'Core', ...Array.from( diff --git a/src/lib/website/EditBar.svelte b/src/lib/website/EditBar.svelte index b36933c..293a085 100644 --- a/src/lib/website/EditBar.svelte +++ b/src/lib/website/EditBar.svelte @@ -1,5 +1,6 @@