diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 17ab9dcd..7221bbce 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,16 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 + # Use the pnpm pinned in package.json "packageManager" (pnpm@11.2.2) via corepack. + # Enable corepack BEFORE setup-node so its `cache: pnpm` can find pnpm. corepack + # enable only creates the shim (fine under the runner's Node 20); pnpm itself only + # runs once Node 24 is on PATH, satisfying pnpm 11's engines.node >=22.13. + # We don't use pnpm/action-setup: its self-installer runs under Node 20 and pnpm 11 + # rejects it. corepack@latest is installed first for up-to-date signing keys. + - name: Enable pnpm (corepack) + run: | + npm install --global corepack@latest --force + corepack enable - uses: actions/setup-node@v4 with: node-version: 24 diff --git a/.gitignore b/.gitignore index 687c8f92..6cfd42f0 100644 --- a/.gitignore +++ b/.gitignore @@ -28,4 +28,6 @@ sveltekit-cloudflare-workers inlay -scripts/backups \ No newline at end of file +scripts/backups + +theming/ \ No newline at end of file diff --git a/CARD_SETTINGS_SIDEBAR.md b/CARD_SETTINGS_SIDEBAR.md new file mode 100644 index 00000000..cfa1c8e3 --- /dev/null +++ b/CARD_SETTINGS_SIDEBAR.md @@ -0,0 +1,140 @@ +# Card Settings → Sidebar migration + +Moving all card settings out of popups and into the left settings sidebar +(`src/lib/website/SettingsSidebar.svelte` — also hosts section + layout settings), and surfacing **everything** that can be +changed for a card — including options that today are only editable inline or in the +creation modal. + +While doing this we also upgrade the **design** of every settings panel to a consistent, +polished look using a shared set of reusable building blocks. + +## Decisions + +- **Text-bearing cards**: keep inline editing AND add the same fields to the sidebar (two entry points, one source of truth in `cardData`). +- **Source-only cards**: add an editable "source/URL" panel — high-traffic ones first (BigSocial, YouTube, Spotify, GitHub Profile, Bluesky Post), then the rest. +- **Build order**: Image card end-to-end first to establish the pattern, then generalize. + +## Architecture notes + +- The sidebar already renders `cardDef.settingsComponent` in a **Content** tab; the + **Design** tab handles background color + size. Settings components receive + `{ item, onclose }` (`SettingsComponentProps`). +- `EditBar.svelte` still has a redundant settings popover → remove once the sidebar covers everything. +- Settings components today are stylistically inconsistent (raw ` + +{#if layout === 'button'} + +{:else if layout === 'compact'} +
+ +
+ + {#if src && removable} + + {/if} +
+
+{:else} +
+ + + {#if src && removable} + + {/if} +
+{/if} diff --git a/src/lib/cards/_settings/SettingsLinkField.svelte b/src/lib/cards/_settings/SettingsLinkField.svelte new file mode 100644 index 00000000..b314cbd9 --- /dev/null +++ b/src/lib/cards/_settings/SettingsLinkField.svelte @@ -0,0 +1,76 @@ + + +
+ + + {#if enabled} +
+ + + + + {#if labelKey} + + + + {/if} +
+ {/if} +
diff --git a/src/lib/cards/_settings/SettingsSection.svelte b/src/lib/cards/_settings/SettingsSection.svelte new file mode 100644 index 00000000..29339dde --- /dev/null +++ b/src/lib/cards/_settings/SettingsSection.svelte @@ -0,0 +1,27 @@ + + +
+ {#if title} +
+

+ {title} +

+ {#if description} +

{description}

+ {/if} +
+ {/if} + {@render children()} +
diff --git a/src/lib/cards/_settings/SettingsSegmented.svelte b/src/lib/cards/_settings/SettingsSegmented.svelte new file mode 100644 index 00000000..345a2229 --- /dev/null +++ b/src/lib/cards/_settings/SettingsSegmented.svelte @@ -0,0 +1,34 @@ + + +
+ {#each options as option (option.value)} + {@const selected = value === option.value} + + {/each} +
diff --git a/src/lib/cards/_settings/SettingsTextSize.svelte b/src/lib/cards/_settings/SettingsTextSize.svelte new file mode 100644 index 00000000..337debb0 --- /dev/null +++ b/src/lib/cards/_settings/SettingsTextSize.svelte @@ -0,0 +1,99 @@ + + +
+ + +
+ {#each Array(steps) as _, i (i)} + + {/each} +
+ + +
diff --git a/src/lib/cards/_settings/SettingsToggle.svelte b/src/lib/cards/_settings/SettingsToggle.svelte new file mode 100644 index 00000000..ec6ecf1a --- /dev/null +++ b/src/lib/cards/_settings/SettingsToggle.svelte @@ -0,0 +1,40 @@ + + +
+ checked, + (value) => { + checked = value; + onCheckedChange?.(value); + } + } + {id} + variant="secondary" + class="mt-0.5 shrink-0" + /> +
+ + {#if description} +

{description}

+ {/if} +
+
diff --git a/src/lib/cards/_settings/SourceSettings.svelte b/src/lib/cards/_settings/SourceSettings.svelte new file mode 100644 index 00000000..8fad308c --- /dev/null +++ b/src/lib/cards/_settings/SourceSettings.svelte @@ -0,0 +1,60 @@ + + + + + { + if (event.code === 'Enter') { + event.preventDefault(); + apply(); + } + }} + /> + + diff --git a/src/lib/cards/_settings/index.ts b/src/lib/cards/_settings/index.ts new file mode 100644 index 00000000..67c42b24 --- /dev/null +++ b/src/lib/cards/_settings/index.ts @@ -0,0 +1,8 @@ +export { default as SettingsSection } from './SettingsSection.svelte'; +export { default as SettingsField } from './SettingsField.svelte'; +export { default as SettingsToggle } from './SettingsToggle.svelte'; +export { default as SettingsSegmented } from './SettingsSegmented.svelte'; +export { default as SettingsAlignToggle } from './SettingsAlignToggle.svelte'; +export { default as SettingsTextSize } from './SettingsTextSize.svelte'; +export { default as SettingsImagePicker } from './SettingsImagePicker.svelte'; +export { default as SettingsLinkField } from './SettingsLinkField.svelte'; diff --git a/src/lib/cards/helper.ts b/src/lib/cards/colors.ts similarity index 100% rename from src/lib/cards/helper.ts rename to src/lib/cards/colors.ts diff --git a/src/lib/cards/content/StandardSiteDocumentListCard/StandardSiteDocumentListCard.svelte b/src/lib/cards/content/StandardSiteDocumentListCard/StandardSiteDocumentListCard.svelte index 1d42cc40..0b3b1ec0 100644 --- a/src/lib/cards/content/StandardSiteDocumentListCard/StandardSiteDocumentListCard.svelte +++ b/src/lib/cards/content/StandardSiteDocumentListCard/StandardSiteDocumentListCard.svelte @@ -4,7 +4,7 @@ getCanEdit, getDidContext, getHandleContext - } from '$lib/website/context'; + } from '$lib/website/data/context'; import { onMount } from 'svelte'; import { CardDefinitionsByType } from '../..'; import type { ContentComponentProps } from '../../types'; diff --git a/src/lib/cards/core/ImageCard/ImageCard.svelte b/src/lib/cards/core/ImageCard/ImageCard.svelte index 1026835d..915b0ad1 100644 --- a/src/lib/cards/core/ImageCard/ImageCard.svelte +++ b/src/lib/cards/core/ImageCard/ImageCard.svelte @@ -1,8 +1,8 @@ - { - if (event.code === 'Enter') { - updateLink(); - event.preventDefault(); - } - }} - placeholder="Enter link" -/> - +
+ + + + + class="font-mono" + /> + {/if} - -
diff --git a/src/lib/cards/special/UpdatedBlentos/UpdatedBlentosCard.svelte b/src/lib/cards/special/UpdatedBlentos/UpdatedBlentosCard.svelte index 99d097fe..a3050228 100644 --- a/src/lib/cards/special/UpdatedBlentos/UpdatedBlentosCard.svelte +++ b/src/lib/cards/special/UpdatedBlentos/UpdatedBlentosCard.svelte @@ -1,6 +1,6 @@ -
-
- - { - if (event.code === 'Enter') { - event.preventDefault(); - confirmUrl(); - } - }} - /> -
+
+ + + + + + + + + { + if (event.code === 'Enter') { + event.preventDefault(); + normalizeUrl(); + } + }} + /> + +
diff --git a/src/lib/cards/utilities/ClockCard/ClockCardSettings.svelte b/src/lib/cards/utilities/ClockCard/ClockCardSettings.svelte index 0101adc9..75f39f81 100644 --- a/src/lib/cards/utilities/ClockCard/ClockCardSettings.svelte +++ b/src/lib/cards/utilities/ClockCard/ClockCardSettings.svelte @@ -1,10 +1,11 @@ -
-
- + +
- +
-
-
+ + diff --git a/src/lib/cards/utilities/CountdownCard/CountdownCardSettings.svelte b/src/lib/cards/utilities/CountdownCard/CountdownCardSettings.svelte index ecc5150c..bb8d8422 100644 --- a/src/lib/cards/utilities/CountdownCard/CountdownCardSettings.svelte +++ b/src/lib/cards/utilities/CountdownCard/CountdownCardSettings.svelte @@ -1,44 +1,37 @@ -
-
- -
- updateTargetDate(e.currentTarget.value, targetTimeValue)} - class="flex-1" - /> - updateTargetDate(targetDateValue, e.currentTarget.value)} - class="w-28" - /> -
-
-
+ + + updateTargetDate(e.currentTarget.value)} + /> + + diff --git a/src/lib/cards/visual/DrawCard/EditingDrawCard.svelte b/src/lib/cards/visual/DrawCard/EditingDrawCard.svelte index 454ac1c3..516b0a0d 100644 --- a/src/lib/cards/visual/DrawCard/EditingDrawCard.svelte +++ b/src/lib/cards/visual/DrawCard/EditingDrawCard.svelte @@ -137,6 +137,8 @@
- import { colorToHue, getHexCSSVar, getHexOfCardColor } from '../../helper'; + import { colorToHue, getHexCSSVar, getHexOfCardColor } from '../../colors'; import type { ContentComponentProps } from '../../types'; import { onMount, onDestroy, tick } from 'svelte'; let { item }: ContentComponentProps = $props(); diff --git a/src/lib/cards/visual/FluidTextCard/FluidTextCardSettings.svelte b/src/lib/cards/visual/FluidTextCard/FluidTextCardSettings.svelte index a43fb8c6..1a34aaaa 100644 --- a/src/lib/cards/visual/FluidTextCard/FluidTextCardSettings.svelte +++ b/src/lib/cards/visual/FluidTextCard/FluidTextCardSettings.svelte @@ -1,11 +1,10 @@ -
-
- - -
+
+ + + + + -
- - { - item.cardData.fontSize = e.currentTarget.value.toString(); - }} - class="bg-base-200 dark:bg-base-700 h-2 w-full cursor-pointer appearance-none rounded-lg" - /> -
+ + + { + item.cardData.fontSize = e.currentTarget.value.toString(); + }} + class="accent-accent-500 bg-base-200 dark:bg-base-700 h-2 w-full cursor-pointer appearance-none rounded-lg" + /> + +
diff --git a/src/lib/cards/visual/Model3DCard/Model3DCard.svelte b/src/lib/cards/visual/Model3DCard/Model3DCard.svelte index 07d2570d..6847268b 100644 --- a/src/lib/cards/visual/Model3DCard/Model3DCard.svelte +++ b/src/lib/cards/visual/Model3DCard/Model3DCard.svelte @@ -3,7 +3,7 @@ import { CineonToneMapping } from 'three'; import type { ContentComponentProps } from '../../types'; import Model3DScene from './Model3DScene.svelte'; - import { getDidContext } from '$lib/website/context'; + import { getDidContext } from '$lib/website/data/context'; import { getBlobURL } from '$lib/atproto'; import type { Did } from '@atcute/lexicons'; import { onMount } from 'svelte'; diff --git a/src/lib/components/DatePicker.svelte b/src/lib/components/DatePicker.svelte deleted file mode 100644 index b3a03326..00000000 --- a/src/lib/components/DatePicker.svelte +++ /dev/null @@ -1,244 +0,0 @@ - - - -
- - {#snippet children({ segments })} - {#each segments as segment, i (segment.part + i)} - {#if segment.part === 'literal'} - {segment.value} - {:else} - - {segment.value} - - {/if} - {/each} - {/snippet} - - - - - - - -
- - - - {#snippet children({ months, weekdays })} - - - - - - - -
- - -
- - - - - - -
- - {#each months as month (month.value.month)} - - - - {#each weekdays as weekday, i (i)} - - {weekday} - - {/each} - - - - - {#each month.weeks as week, weekIndex (weekIndex)} - - {#each week as day (day.toString())} - - - {#snippet children({ selected, disabled, day: dayText })} -
- {dayText} - {#if day.day === todayDay && day.month === todayMonth && day.year === todayYear} - - {/if} -
- {/snippet} -
-
- {/each} -
- {/each} -
-
- {/each} - {/snippet} -
-
-
diff --git a/src/lib/components/DateTimePicker.svelte b/src/lib/components/DateTimePicker.svelte deleted file mode 100644 index df75172b..00000000 --- a/src/lib/components/DateTimePicker.svelte +++ /dev/null @@ -1,73 +0,0 @@ - - -
- -
- -
-
diff --git a/src/lib/components/ImageDropper.svelte b/src/lib/components/ImageDropper.svelte deleted file mode 100644 index 343dc810..00000000 --- a/src/lib/components/ImageDropper.svelte +++ /dev/null @@ -1,66 +0,0 @@ - - - - -{#if isDragOver} - -
- Drop file to add it to your message -
-
-{/if} diff --git a/src/lib/components/MarkdownTextEditor.svelte b/src/lib/components/MarkdownTextEditor.svelte index 44822e4d..f71ad386 100644 --- a/src/lib/components/MarkdownTextEditor.svelte +++ b/src/lib/components/MarkdownTextEditor.svelte @@ -47,6 +47,28 @@ onupdate?.(markdown); }; + // Keep the editor in sync when the bound markdown changes from outside (e.g. edited + // in the settings sidebar) without disrupting the user while they type here. + // `lastIncoming` guards against a re-sync loop from markdown round-trip differences. + let lastIncoming: string | undefined; + $effect(() => { + const incoming = (contentDict[key] ?? defaultContent ?? '') as string; + if (!editor || editor.isFocused || incoming === lastIncoming) return; + + const turndownService = new TurndownService({ + headingStyle: 'atx', + bulletListMarker: '-' + }); + const currentMarkdown = turndownService.turndown(editor.getHTML()); + lastIncoming = incoming; + if (currentMarkdown === incoming) return; + + Promise.resolve(marked.parse(incoming)).then((html) => { + if (!editor || editor.isFocused) return; + editor.commands.setContent(html, { emitUpdate: false }); + }); + }); + onMount(async () => { if (!element || editor) return; diff --git a/src/lib/components/PlainTextEditor.svelte b/src/lib/components/PlainTextEditor.svelte index b3609d7b..5636d832 100644 --- a/src/lib/components/PlainTextEditor.svelte +++ b/src/lib/components/PlainTextEditor.svelte @@ -36,6 +36,15 @@ onupdate?.(text); }; + // Keep the editor in sync when the bound value changes from outside (e.g. edited + // in the settings sidebar) without disrupting the user while they type here. + $effect(() => { + const incoming = contentDict[key] ?? defaultContent ?? ''; + if (!editor || editor.isFocused) return; + if (editor.getText() === incoming) return; + editor.commands.setContent(incoming, { emitUpdate: false }); + }); + onMount(async () => { if (!element || editor) return; diff --git a/src/lib/website/Pronouns.svelte b/src/lib/components/Pronouns.svelte similarity index 98% rename from src/lib/website/Pronouns.svelte rename to src/lib/components/Pronouns.svelte index e5716404..0be30fd2 100644 --- a/src/lib/website/Pronouns.svelte +++ b/src/lib/components/Pronouns.svelte @@ -85,6 +85,7 @@ d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.832 19.82a4.5 4.5 0 0 1-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 0 1 1.13-1.897L16.863 4.487Zm0 0L19.5 7.125" /> + Edit pronouns {/if}
diff --git a/src/lib/components/TimePicker.svelte b/src/lib/components/TimePicker.svelte deleted file mode 100644 index e9b565fa..00000000 --- a/src/lib/components/TimePicker.svelte +++ /dev/null @@ -1,101 +0,0 @@ - - - -
- - {#snippet children({ segments })} - {#each segments as segment, i (segment.part + i)} - {#if segment.part === 'literal'} - {segment.value} - {:else} - - {segment.value} - - {/if} - {/each} - {/snippet} - - - - - -
-
diff --git a/src/lib/components/bluesky-post/BlueskyPost.svelte b/src/lib/components/bluesky-post/BlueskyPost.svelte index e3215ccf..baea2e2d 100644 --- a/src/lib/components/bluesky-post/BlueskyPost.svelte +++ b/src/lib/components/bluesky-post/BlueskyPost.svelte @@ -44,7 +44,7 @@ repostHref={postData?.href} likeHref={postData?.href} showBookmark={false} - logo={showLogo ? logo : undefined} + logo={(showLogo ? logo : undefined) as Snippet | undefined} {showAvatar} {compact} {...restProps} diff --git a/src/lib/components/card-command/CardCommand.svelte b/src/lib/components/card-command/CardCommand.svelte index 13187d13..821cbaf6 100644 --- a/src/lib/components/card-command/CardCommand.svelte +++ b/src/lib/components/card-command/CardCommand.svelte @@ -2,7 +2,7 @@ import { AllCardDefinitions } from '$lib/cards'; import type { CardDefinition } from '$lib/cards/types'; import { Command, Dialog } from 'bits-ui'; - import { isTyping } from '$lib/helper'; + import { isTyping } from '$lib/helpers/utils'; import { describeRepo, user } from '$lib/atproto'; let { diff --git a/src/lib/components/modals/MobileWarningModal.svelte b/src/lib/components/modals/MobileWarningModal.svelte new file mode 100644 index 00000000..8636b987 --- /dev/null +++ b/src/lib/components/modals/MobileWarningModal.svelte @@ -0,0 +1,30 @@ + + + +
+ + + +

Mobile Editing

+

+ Mobile editing is currently experimental. For the best experience, use a desktop browser. +

+ +
+
diff --git a/src/lib/components/post/Post.svelte b/src/lib/components/post/Post.svelte index bfdb3326..53c9196e 100644 --- a/src/lib/components/post/Post.svelte +++ b/src/lib/components/post/Post.svelte @@ -1,6 +1,6 @@
-
-
+
+
{#each sectionItems as item (item.id)}
+ import { Label } from '@foxui/core'; + import type { SectionSettingsProps } from '../types'; + + // items is bindable so the sidebar can bind uniformly across all section + // settings panels (only Hero actually mutates it). + let { section, items = $bindable(), onlayoutchange }: SectionSettingsProps = $props(); + + let d = $derived(section.sectionData); + let scrollMode = $derived((d.scrollMode as string) ?? 'scroll'); + + const modes: { value: string; label: string; hint: string }[] = [ + { value: 'scroll', label: 'Scroll', hint: 'Cards stay in one line and scroll sideways.' }, + { value: 'fit', label: 'Wrap', hint: 'Cards wrap onto multiple lines and stay centered.' } + ]; + + function update(key: string, value: any) { + section.sectionData = { ...d, [key]: value }; + onlayoutchange(); + } + + +
+ +
+ {#each modes as mode (mode.value)} + + {/each} +
+

+ {modes.find((m) => m.value === scrollMode)?.hint} +

+
diff --git a/src/lib/sections/RowSection/EditingRowSection.svelte b/src/lib/sections/ColumnsSection/EditingColumnsSection.svelte similarity index 89% rename from src/lib/sections/RowSection/EditingRowSection.svelte rename to src/lib/sections/ColumnsSection/EditingColumnsSection.svelte index 90b98d35..dd2c872c 100644 --- a/src/lib/sections/RowSection/EditingRowSection.svelte +++ b/src/lib/sections/ColumnsSection/EditingColumnsSection.svelte @@ -18,6 +18,8 @@ items.filter((i) => i.sectionId === section.id).toSorted((a, b) => a.x - b.x) ); + let fit = $derived((section.sectionData?.scrollMode as string) === 'fit'); + let containerRef: HTMLDivElement | undefined = $state(); let hovered = $state(false); @@ -54,10 +56,16 @@ bind:this={containerRef} class="@container/grid pointer-events-auto relative col-span-3 px-0 py-4" > - + -
-
+
+
{#each sectionItems as item (item.id)} {@const idx = items.indexOf(item)}
{ +export function defaultColumnsSectionData(): Record { return { scrollMode: 'scroll' // 'scroll' | 'fit' }; } -export const RowSectionDefinition: SectionDefinition = { - type: 'row', - contentComponent: RowSection, - editingContentComponent: EditingRowSection, - defaultSectionData: defaultRowSectionData, +export const ColumnsSectionDefinition: SectionDefinition = { + type: 'columns', + contentComponent: ColumnsSection, + editingContentComponent: EditingColumnsSection, + settingsComponent: ColumnsSectionSettings, + defaultSectionData: defaultColumnsSectionData, cardFilter: (def) => (def.minW ?? 2) <= 2 && (def.minH ?? 2) <= 2, allowRotate: true, addItem: (item, allItems) => { @@ -29,6 +31,6 @@ export const RowSectionDefinition: SectionDefinition = { }, deleteItem: (itemId, allItems) => allItems.filter((i) => i.id !== itemId), resizeItem: () => {}, - name: 'Row', + name: 'Columns', icon: `` }; diff --git a/src/lib/sections/GallerySection/EditingGallerySection.svelte b/src/lib/sections/GallerySection/EditingGallerySection.svelte index 740b16e7..54262503 100644 --- a/src/lib/sections/GallerySection/EditingGallerySection.svelte +++ b/src/lib/sections/GallerySection/EditingGallerySection.svelte @@ -1,8 +1,8 @@ + +
+ +
+ {#each columnOptions as n (n)} + + {/each} +
+

+ Columns on wide screens. Narrow screens always use two. +

+
diff --git a/src/lib/sections/GallerySection/index.ts b/src/lib/sections/GallerySection/index.ts index 0e539560..f44f128b 100644 --- a/src/lib/sections/GallerySection/index.ts +++ b/src/lib/sections/GallerySection/index.ts @@ -1,6 +1,7 @@ import type { SectionDefinition } from '../types'; import EditingGallerySection from './EditingGallerySection.svelte'; import GallerySection from './GallerySection.svelte'; +import GallerySectionSettings from './GallerySectionSettings.svelte'; export function defaultGallerySectionData(): Record { return { @@ -13,6 +14,7 @@ export const GallerySectionDefinition: SectionDefinition = { type: 'gallery', contentComponent: GallerySection, editingContentComponent: EditingGallerySection, + settingsComponent: GallerySectionSettings, defaultSectionData: defaultGallerySectionData, cardFilter: (def) => def.type === 'image' || def.type === 'gif', addItem: (item, allItems) => { diff --git a/src/lib/sections/GridSection/EditingGridSection.svelte b/src/lib/sections/GridSection/EditingGridSection.svelte index d38ad678..7a1a5aa2 100644 --- a/src/lib/sections/GridSection/EditingGridSection.svelte +++ b/src/lib/sections/GridSection/EditingGridSection.svelte @@ -85,6 +85,7 @@ onfiledrop={handleFileDrop} > - import { - Badge, - Button, - Checkbox, - cn, - Input, - Label, - Popover, - ToggleGroup, - ToggleGroupItem - } from '@foxui/core'; + import { Badge, cn } from '@foxui/core'; import PlainTextEditor from '$lib/components/PlainTextEditor.svelte'; - import { CardDefinitionsByType } from '$lib/cards'; import type { EditingSectionContentProps } from '../types'; import { DEFAULT_DECORATION_SLOTS, @@ -44,7 +33,6 @@ let containerRef: HTMLDivElement | undefined = $state(); let hovered = $state(false); - let settingsOpen = $state(false); $effect(() => { onrefchange(containerRef); @@ -119,185 +107,19 @@ delete next[slotId]; update('slotAssignments', next); } - - const toggleClasses = 'size-8 min-w-8 [&_svg]:size-3 cursor-pointer'; - - function confirmUrl() { - let href = (d.buttonHref as string)?.trim() || ''; - if (href && !/^https?:\/\//i.test(href) && !href.startsWith('#')) { - href = 'https://' + href; - } - update('buttonHref', href); - } - - let filledSlots = $derived( - DEFAULT_DECORATION_SLOTS.filter((slot) => assignments[slot.id]) - .map((slot) => ({ - slot, - item: getSlotItem(slot, assignments, sectionItems) - })) - .filter((s) => s.item) - );
- - - {#if hovered || isActive} -
- - {#snippet child({ props })} - - {/snippet} -
-
- -
- d.showBadge !== false, (val) => update('showBadge', val)} - id="hero-show-badge" - variant="secondary" - /> - -
-
- d.showSubtitle !== false, (val) => update('showSubtitle', val)} - id="hero-show-subtitle" - variant="secondary" - /> - -
-
- d.showButton !== false, (val) => update('showButton', val)} - id="hero-show-button" - variant="secondary" - /> - -
-
- -
- - update('buttonHref', (e.target as HTMLInputElement).value)} - placeholder="example.com" - class="text-sm" - onkeydown={(event) => { - if (event.code === 'Enter') { - event.preventDefault(); - confirmUrl(); - } - }} - /> -
- -
- - d.textAlign ?? 'center', - (value) => { - if (value) update('textAlign', value); - } - } - > - - - - - - - - - - -
- - {#if filledSlots.length > 0} -
- - {#each filledSlots as { slot, item: slotItem } (slot.id)} -
- - {slot.side === 'left' ? 'L' : 'R'}{DEFAULT_DECORATION_SLOTS.filter( - (s) => s.side === slot.side - ).indexOf(slot) + 1} - - - {CardDefinitionsByType[slotItem?.cardType ?? '']?.name ?? slotItem?.cardType} - - -
- {/each} -
- {/if} -
-
-
- {/if} +
{#each DEFAULT_DECORATION_SLOTS as slot (slot.id)} diff --git a/src/lib/sections/HeroSection/HeroSectionSettings.svelte b/src/lib/sections/HeroSection/HeroSectionSettings.svelte new file mode 100644 index 00000000..2d614230 --- /dev/null +++ b/src/lib/sections/HeroSection/HeroSectionSettings.svelte @@ -0,0 +1,168 @@ + + +
+

Show

+
+ d.showBadge !== false, (val) => update('showBadge', val)} + id="hero-show-badge" + variant="secondary" + /> + +
+
+ d.showSubtitle !== false, (val) => update('showSubtitle', val)} + id="hero-show-subtitle" + variant="secondary" + /> + +
+
+ d.showButton !== false, (val) => update('showButton', val)} + id="hero-show-button" + variant="secondary" + /> + +
+
+ +
+ + update('buttonHref', (e.target as HTMLInputElement).value)} + placeholder="example.com" + class="text-sm" + onkeydown={(event) => { + if (event.code === 'Enter') { + event.preventDefault(); + confirmUrl(); + } + }} + /> +
+ +
+ + d.textAlign ?? 'center', + (value) => { + if (value) update('textAlign', value); + } + } + > + + + + + + + + + + +
+ +{#if filledSlots.length > 0} +
+ + {#each filledSlots as { slot, item: slotItem } (slot.id)} +
+ + {slot.side === 'left' ? 'L' : 'R'}{DEFAULT_DECORATION_SLOTS.filter( + (s) => s.side === slot.side + ).indexOf(slot) + 1} + + + {CardDefinitionsByType[slotItem?.cardType ?? '']?.name ?? slotItem?.cardType} + + +
+ {/each} +
+{/if} diff --git a/src/lib/sections/HeroSection/index.ts b/src/lib/sections/HeroSection/index.ts index bf404289..3cc4bf91 100644 --- a/src/lib/sections/HeroSection/index.ts +++ b/src/lib/sections/HeroSection/index.ts @@ -2,6 +2,7 @@ import type { Item } from '$lib/types'; import type { SectionDefinition } from '../types'; import EditingHeroSection from './EditingHeroSection.svelte'; import HeroSection from './HeroSection.svelte'; +import HeroSectionSettings from './HeroSectionSettings.svelte'; import { DEFAULT_DECORATION_SLOTS, canFitInSlot, defaultHeroSectionData } from './shared'; export * from './shared'; @@ -10,6 +11,7 @@ export const HeroSectionDefinition: SectionDefinition = { type: 'hero', contentComponent: HeroSection, editingContentComponent: EditingHeroSection, + settingsComponent: HeroSectionSettings, defaultSectionData: defaultHeroSectionData, cardFilter: canFitInSlot, allowRotate: true, diff --git a/src/lib/sections/RowsSection/EditingRowsSection.svelte b/src/lib/sections/RowsSection/EditingRowsSection.svelte new file mode 100644 index 00000000..056ee322 --- /dev/null +++ b/src/lib/sections/RowsSection/EditingRowsSection.svelte @@ -0,0 +1,96 @@ + + +
+ + +
+ {#each sectionItems as item (item.id)} + {@const idx = items.indexOf(item)} +
+ deleteItem(item.id)} + showGridControls={false} + > + + +
+ {/each} + + +
+
diff --git a/src/lib/sections/RowsSection/RowsSection.svelte b/src/lib/sections/RowsSection/RowsSection.svelte new file mode 100644 index 00000000..fb487d63 --- /dev/null +++ b/src/lib/sections/RowsSection/RowsSection.svelte @@ -0,0 +1,24 @@ + + +
+
+ {#each sectionItems as item (item.id)} +
+ + + +
+ {/each} +
+
diff --git a/src/lib/sections/RowsSection/index.ts b/src/lib/sections/RowsSection/index.ts new file mode 100644 index 00000000..86502c13 --- /dev/null +++ b/src/lib/sections/RowsSection/index.ts @@ -0,0 +1,26 @@ +import type { SectionDefinition } from '../types'; +import EditingRowsSection from './EditingRowsSection.svelte'; +import RowsSection from './RowsSection.svelte'; + +export const RowsSectionDefinition: SectionDefinition = { + type: 'rows', + contentComponent: RowsSection, + editingContentComponent: EditingRowsSection, + addItem: (item, allItems) => { + const sectionItems = allItems.filter((i) => i.sectionId === item.sectionId); + // Full-width rows stacked vertically; ordered by y. + item.w = 8; + item.h = 2; + item.x = 0; + item.y = sectionItems.length; + item.mobileW = 8; + item.mobileH = 2; + item.mobileX = 0; + item.mobileY = sectionItems.length; + return [...allItems, item]; + }, + deleteItem: (itemId, allItems) => allItems.filter((i) => i.id !== itemId), + resizeItem: () => {}, + name: 'Rows', + icon: `` +}; diff --git a/src/lib/sections/RowsSection/shared.ts b/src/lib/sections/RowsSection/shared.ts new file mode 100644 index 00000000..1f1042fb --- /dev/null +++ b/src/lib/sections/RowsSection/shared.ts @@ -0,0 +1,12 @@ +import type { Item } from '$lib/types'; + +/** + * Per-row sizing for the Rows section. Media cards (which store a natural + * aspectRatio, e.g. images/gifs) keep their proportions full-width; everything + * else (headings, text, links, buttons, …) sizes to its own content height. + */ +export function rowItemStyle(item: Item): string { + const ar = item?.cardData?.aspectRatio as { width?: number; height?: number } | undefined; + if (ar?.width && ar?.height) return `aspect-ratio: ${ar.width} / ${ar.height};`; + return ''; +} diff --git a/src/lib/sections/SectionChrome.svelte b/src/lib/sections/SectionChrome.svelte index fabb54f7..02202ac3 100644 --- a/src/lib/sections/SectionChrome.svelte +++ b/src/lib/sections/SectionChrome.svelte @@ -1,13 +1,15 @@ @@ -34,5 +37,31 @@ {/if} {name}
+ +
{/if} diff --git a/src/lib/sections/SingleCardSection/EditingSingleCardSection.svelte b/src/lib/sections/SingleCardSection/EditingSingleCardSection.svelte new file mode 100644 index 00000000..90fd49c8 --- /dev/null +++ b/src/lib/sections/SingleCardSection/EditingSingleCardSection.svelte @@ -0,0 +1,97 @@ + + +
+ + +
+ {#if card} + {@const idx = items.indexOf(card)} +
+ + + +
+ {:else} + + {/if} +
+
diff --git a/src/lib/sections/SingleCardSection/SingleCardSection.svelte b/src/lib/sections/SingleCardSection/SingleCardSection.svelte new file mode 100644 index 00000000..9f02930b --- /dev/null +++ b/src/lib/sections/SingleCardSection/SingleCardSection.svelte @@ -0,0 +1,21 @@ + + +
+ {#if card} +
+ + + +
+ {/if} +
diff --git a/src/lib/sections/SingleCardSection/SingleCardSectionSettings.svelte b/src/lib/sections/SingleCardSection/SingleCardSectionSettings.svelte new file mode 100644 index 00000000..3f978eef --- /dev/null +++ b/src/lib/sections/SingleCardSection/SingleCardSectionSettings.svelte @@ -0,0 +1,47 @@ + + +
+ +
+ {#each options as opt (opt.value)} + + {/each} +
+

+ "Fit" uses the card's natural proportions (great for images). +

+
diff --git a/src/lib/sections/SingleCardSection/index.ts b/src/lib/sections/SingleCardSection/index.ts new file mode 100644 index 00000000..814fe6ff --- /dev/null +++ b/src/lib/sections/SingleCardSection/index.ts @@ -0,0 +1,32 @@ +import type { SectionDefinition } from '../types'; +import EditingSingleCardSection from './EditingSingleCardSection.svelte'; +import SingleCardSection from './SingleCardSection.svelte'; +import SingleCardSectionSettings from './SingleCardSectionSettings.svelte'; +import { defaultSingleCardSectionData } from './shared'; + +export * from './shared'; + +export const SingleCardSectionDefinition: SectionDefinition = { + type: 'single', + contentComponent: SingleCardSection, + editingContentComponent: EditingSingleCardSection, + settingsComponent: SingleCardSectionSettings, + defaultSectionData: defaultSingleCardSectionData, + addItem: (item, allItems) => { + // A single-card section holds exactly one full-width card; adding replaces it. + item.w = 8; + item.h = 6; + item.x = 0; + item.y = 0; + item.mobileW = 8; + item.mobileH = 6; + item.mobileX = 0; + item.mobileY = 0; + const others = allItems.filter((i) => i.sectionId !== item.sectionId); + return [...others, item]; + }, + deleteItem: (itemId, allItems) => allItems.filter((i) => i.id !== itemId), + resizeItem: () => {}, + name: 'Single card', + icon: `` +}; diff --git a/src/lib/sections/SingleCardSection/shared.ts b/src/lib/sections/SingleCardSection/shared.ts new file mode 100644 index 00000000..7a333625 --- /dev/null +++ b/src/lib/sections/SingleCardSection/shared.ts @@ -0,0 +1,21 @@ +import type { Item } from '$lib/types'; + +/** + * CSS aspect-ratio for a single-card section. When `aspect` is 'fit' the card's + * natural aspect ratio (from cardData.aspectRatio, e.g. images) is used, falling + * back to 16/9. + */ +export function singleCardAspectStyle(item: Item | undefined, aspect: string): string { + if (aspect && aspect !== 'fit') return `aspect-ratio: ${aspect};`; + // "Fit": media cards keep their natural proportions; content cards (headings, + // text, …) size to their own content height. + const ar = item?.cardData?.aspectRatio as { width?: number; height?: number } | undefined; + if (ar?.width && ar?.height) return `aspect-ratio: ${ar.width} / ${ar.height};`; + return ''; +} + +export function defaultSingleCardSectionData(): Record { + return { + aspect: 'fit' + }; +} diff --git a/src/lib/sections/TextSection/EditingTextSection.svelte b/src/lib/sections/TextSection/EditingTextSection.svelte index a9d567f3..0bc8d451 100644 --- a/src/lib/sections/TextSection/EditingTextSection.svelte +++ b/src/lib/sections/TextSection/EditingTextSection.svelte @@ -45,7 +45,7 @@ bind:this={containerRef} class="@container/grid pointer-events-auto relative col-span-3 px-4 py-10" > - +
import { marked } from 'marked'; - import { sanitize } from '$lib/sanitize'; + import { sanitize } from '$lib/helpers/sanitize'; import { cn } from '@foxui/core'; import type { SectionContentProps } from '../types'; import { textAlignClasses, textSizeClasses } from './shared'; diff --git a/src/lib/sections/TextSection/TextSectionSettings.svelte b/src/lib/sections/TextSection/TextSectionSettings.svelte new file mode 100644 index 00000000..5c01ba4e --- /dev/null +++ b/src/lib/sections/TextSection/TextSectionSettings.svelte @@ -0,0 +1,93 @@ + + +
+ + (d.textAlign as string) ?? 'left', + (value) => { + if (value) update('textAlign', value); + } + } + > + + + + + + + + + + +
+ +
+ +
+ {#each sizes as size (size.value)} + + {/each} +
+
diff --git a/src/lib/sections/TextSection/index.ts b/src/lib/sections/TextSection/index.ts index b2f079eb..8d1039d4 100644 --- a/src/lib/sections/TextSection/index.ts +++ b/src/lib/sections/TextSection/index.ts @@ -1,6 +1,7 @@ import type { SectionDefinition } from '../types'; import EditingTextSection from './EditingTextSection.svelte'; import TextSection from './TextSection.svelte'; +import TextSectionSettings from './TextSectionSettings.svelte'; import { defaultTextSectionData } from './shared'; export * from './shared'; @@ -9,6 +10,7 @@ export const TextSectionDefinition: SectionDefinition = { type: 'text', contentComponent: TextSection, editingContentComponent: EditingTextSection, + settingsComponent: TextSectionSettings, defaultSectionData: defaultTextSectionData, addItem: (_item, allItems) => allItems, deleteItem: (_itemId, allItems) => allItems, diff --git a/src/lib/sections/index.ts b/src/lib/sections/index.ts index 69befd54..a95de752 100644 --- a/src/lib/sections/index.ts +++ b/src/lib/sections/index.ts @@ -2,14 +2,18 @@ import type { SectionDefinition } from './types'; import { GridSectionDefinition } from './GridSection'; import { HeroSectionDefinition } from './HeroSection'; import { TextSectionDefinition } from './TextSection'; -import { RowSectionDefinition } from './RowSection'; +import { ColumnsSectionDefinition } from './ColumnsSection'; +import { RowsSectionDefinition } from './RowsSection'; +import { SingleCardSectionDefinition } from './SingleCardSection'; import { GallerySectionDefinition } from './GallerySection'; export const AllSectionDefinitions = [ GridSectionDefinition, HeroSectionDefinition, TextSectionDefinition, - RowSectionDefinition, + ColumnsSectionDefinition, + RowsSectionDefinition, + SingleCardSectionDefinition, GallerySectionDefinition ] as const; diff --git a/src/lib/sections/types.ts b/src/lib/sections/types.ts index 7a9cfdfc..3ce3f1e2 100644 --- a/src/lib/sections/types.ts +++ b/src/lib/sections/types.ts @@ -26,10 +26,25 @@ export type AddItemOptions = { extraData?: Record; }; +/** + * Props for a section's settings panel, rendered in the editor sidebar at the + * "section" level. Mirrors cards' SettingsComponentProps but operates on a + * section's `sectionData` (and `items` for sections that manage their own cards, + * e.g. Hero slots). + */ +export type SectionSettingsProps = { + section: SectionRecord; + items: Item[]; + onlayoutchange: () => void; + onclose: () => void; +}; + export type SectionDefinition = { type: string; contentComponent: Component; editingContentComponent: Component; + /** Optional settings panel shown in the editor sidebar's section level. */ + settingsComponent?: Component; defaultSectionData?: () => Record; cardFilter?: (cardDef: CardDefinition) => boolean; allowRotate?: boolean; diff --git a/src/lib/website/Account.svelte b/src/lib/website/Account.svelte deleted file mode 100644 index 615e57ee..00000000 --- a/src/lib/website/Account.svelte +++ /dev/null @@ -1,76 +0,0 @@ - - -{#if user.isLoggedIn && user.profile} -
- - - {#snippet child({ props })} - - {/snippet} - -
- {#if user.profile} - - {/if} - - -
-
- - - -
-{/if} diff --git a/src/lib/website/CustomDomainModal.svelte b/src/lib/website/CustomDomainModal.svelte deleted file mode 100644 index fb884265..00000000 --- a/src/lib/website/CustomDomainModal.svelte +++ /dev/null @@ -1,280 +0,0 @@ - - - - - - {#if step === 'current'} -

- Custom Domain -

- -
- {currentDomain} -
- -
- - - -
- {:else if step === 'input'} -

- Custom Domain -

- - - -
- - -
- {:else if step === 'instructions'} -

- Set up your domain -

- -

- Add a CNAME record for your domain pointing to: -

- -
- blento-proxy.fly.dev - -
- -
- - -
- {:else if step === 'verifying'} -

- Verifying... -

- -

- Checking DNS records and verifying your domain. -

- -
- - - - - Verifying... -
- {:else if step === 'removing'} -

- Removing... -

- -
- - - - - Removing domain... -
- {:else if step === 'success'} -

- Domain verified! -

- -

- Your custom domain {domain} has been set up successfully. -

- -
- -
- {:else if step === 'error'} -

- Verification failed -

- -

- {errorMessage} -

- {#if errorHint} -

- {errorHint} -

- {/if} - -
- - -
- {/if} -
diff --git a/src/lib/website/EditBar.svelte b/src/lib/website/EditBar.svelte deleted file mode 100644 index 95af1262..00000000 --- a/src/lib/website/EditBar.svelte +++ /dev/null @@ -1,457 +0,0 @@ - - - - -{#if dev || (user.isLoggedIn && user.profile?.did === data.did)} -
- {#if showEditControls} - -
- {#if cardDef?.allowSetColor !== false} - - {#snippet child({ props })} - - {/snippet} - { - if (typeof previous === 'string' || typeof color === 'string') { - return; - } - if (selectedCard) { - selectedCard.color = color.label; - } - }} - class="w-64" - /> - - {/if} - - - {#snippet child({ props })} - - {/snippet} -
- {#if canSetSize(2, 2)} - - {/if} - {#if canSetSize(4, 2)} - - {/if} - {#if canSetSize(2, 4)} - - {/if} - {#if canSetSize(4, 4)} - - {/if} -
-
- - {#if cardDef?.settingsComponent && selectedCard} - - {#snippet child({ props })} - - {/snippet} - { - settingsPopoverOpen = false; - }} - /> - - {/if} - - {#if allowRotate} - - - {/if} -
-
- - -
-
- {/if} - - -
- -
-
- - {#if hasUnsavedChanges} - - {:else} - - {/if} -
-
-
-{/if} diff --git a/src/lib/website/EditableProfile.svelte b/src/lib/website/EditableProfile.svelte deleted file mode 100644 index f5fa0c09..00000000 --- a/src/lib/website/EditableProfile.svelte +++ /dev/null @@ -1,157 +0,0 @@ - - -
-
- - - - - - - {#if data.publication} -
- -
- {/if} - - - - -
- {#if data.publication} - - {/if} -
- - {#if !hideBlento} -
-
diff --git a/src/lib/website/EmbeddedCard.svelte b/src/lib/website/EmbeddedCard.svelte deleted file mode 100644 index 343c8690..00000000 --- a/src/lib/website/EmbeddedCard.svelte +++ /dev/null @@ -1,177 +0,0 @@ - - - - - - - {@html themeScript} - - - - - - -
-
-
- - - -
-
-
-
- - diff --git a/src/lib/website/FloatingEditButton.svelte b/src/lib/website/FloatingEditButton.svelte deleted file mode 100644 index 6c7fbbd5..00000000 --- a/src/lib/website/FloatingEditButton.svelte +++ /dev/null @@ -1,94 +0,0 @@ - - -{#if isOwnPage && !isEditPage} - -{:else if showLoginOnEditPage} -
- -
-{:else if showLoginOnBlento} -
- -
-{:else if showEditBlentoButton} -
- -
-{/if} diff --git a/src/lib/website/SectionsModal.svelte b/src/lib/website/SectionsModal.svelte deleted file mode 100644 index c27be42e..00000000 --- a/src/lib/website/SectionsModal.svelte +++ /dev/null @@ -1,140 +0,0 @@ - - - -
-

Sections

- - {#if sections.length === 0} -

No sections yet.

- {/if} - -
- {#each sections.toSorted((a, b) => a.index - b.index) as section, i (section.id)} - {@const def = SectionDefinitionsByType[section.sectionType]} -
- {#if def?.icon} - - {@html def.icon} - - {/if} - -
- - {section.name || def?.name || section.sectionType} - -
- -
- - - {#if sections.length > 1} - - {/if} -
-
- {/each} -
- - -
-
diff --git a/src/lib/website/SectionsSidebar.svelte b/src/lib/website/SectionsSidebar.svelte deleted file mode 100644 index 1e356ffa..00000000 --- a/src/lib/website/SectionsSidebar.svelte +++ /dev/null @@ -1,234 +0,0 @@ - - - -
-
-

Layout

- -
- -
- - - -
- - - Sections - {#each sections.toSorted((a, b) => a.index - b.index) as section, i (section.id)} - {@const def = SectionDefinitionsByType[section.sectionType]} -
- - -
- - - {#if sections.length > 1} - - {/if} -
-
- {/each} -
- - -
- Add section - {#each AllSectionDefinitions as def (def.type)} - - {/each} -
-
diff --git a/src/lib/website/SettingsModal.svelte b/src/lib/website/SettingsModal.svelte deleted file mode 100644 index 5d3a44d2..00000000 --- a/src/lib/website/SettingsModal.svelte +++ /dev/null @@ -1,97 +0,0 @@ - - - - - -

Settings

- -
-

Layout Sync

-

- Control how desktop and mobile layouts stay in sync. -

- -
- {#each options as option (option.key)} - {@const isSelected = option.key === (selected === undefined ? 'automatic' : selected)} - - {/each} -
-
-
diff --git a/src/lib/website/context.ts b/src/lib/website/data/context.ts similarity index 72% rename from src/lib/website/context.ts rename to src/lib/website/data/context.ts index 47d4e50c..1dba075b 100644 --- a/src/lib/website/context.ts +++ b/src/lib/website/data/context.ts @@ -4,9 +4,13 @@ import { createContext } from 'svelte'; export const [getDidContext, setDidContext] = createContext(); export const [getHandleContext, setHandleContext] = createContext(); export const [getIsMobile, setIsMobile] = createContext<() => boolean>(); +export const [getIsRealMobile, setIsRealMobile] = createContext<() => boolean>(); export const [getCanEdit, setCanEdit] = createContext<() => boolean>(); export const [getAdditionalUserData, setAdditionalUserData] = createContext>(); export const [getIsCoarse, setIsCoarse] = createContext<() => boolean>(); export const [getSelectedCardId, setSelectedCardId] = createContext<() => string | null>(); export const [getSelectCard, setSelectCard] = createContext<(id: string | null) => void>(); +export const [getToggleCardSettings, setToggleCardSettings] = createContext<(id: string) => void>(); +export const [getOpenSectionSettings, setOpenSectionSettings] = + createContext<(id: string) => void>(); diff --git a/src/lib/website/file-processing.ts b/src/lib/website/data/file-processing.ts similarity index 97% rename from src/lib/website/file-processing.ts rename to src/lib/website/data/file-processing.ts index aecfa96b..73f0568f 100644 --- a/src/lib/website/file-processing.ts +++ b/src/lib/website/data/file-processing.ts @@ -1,4 +1,4 @@ -import { createEmptyCard } from '$lib/helper'; +import { createEmptyCard } from '$lib/helpers/items'; import type { Item } from '$lib/types'; export function getImageDimensions(src: string): Promise<{ width: number; height: number }> { diff --git a/src/lib/website/load.ts b/src/lib/website/data/load.ts similarity index 99% rename from src/lib/website/load.ts rename to src/lib/website/data/load.ts index 91febdee..8dd030ee 100644 --- a/src/lib/website/load.ts +++ b/src/lib/website/data/load.ts @@ -1,8 +1,8 @@ import { getDetailedProfile, listRecords, resolveHandle, parseUri, getRecord } from '$lib/atproto'; import { getCDNImageBlobUrl } from '$lib/atproto/methods'; import { CardDefinitionsByType } from '$lib/cards'; -import type { CacheService } from '$lib/cache'; -import { createEmptyCard } from '$lib/helper'; +import type { CacheService } from '$lib/helpers/cache'; +import { createEmptyCard } from '$lib/helpers/items'; import type { Item, PronounsRecord, SectionRecord, WebsiteData } from '$lib/types'; import { ensureSections } from '$lib/sections/migrate'; import { error } from '@sveltejs/kit'; diff --git a/src/lib/website/edit/EditTopBar.svelte b/src/lib/website/edit/EditTopBar.svelte new file mode 100644 index 00000000..c15d3197 --- /dev/null +++ b/src/lib/website/edit/EditTopBar.svelte @@ -0,0 +1,153 @@ + + +{#if dev || (user.isLoggedIn && user.profile?.did === data.did)} +
+
+ + {#snippet child({ props })} + + {/snippet} + +
+ +
+
+ + + {name} + + +
+
+ + + + +
+
+{/if} diff --git a/src/lib/website/edit/EditableProfile.svelte b/src/lib/website/edit/EditableProfile.svelte new file mode 100644 index 00000000..12884d87 --- /dev/null +++ b/src/lib/website/edit/EditableProfile.svelte @@ -0,0 +1,195 @@ + + +
+
+ +
+ + + +
+ + + + + {#if data.publication} +
+ +
+ {/if} + + + + +
+ {#if data.publication} + + {/if} +
+ + {#if !hideBlento} +
+
diff --git a/src/lib/website/EditableWebsite.svelte b/src/lib/website/edit/EditableWebsite.svelte similarity index 51% rename from src/lib/website/EditableWebsite.svelte rename to src/lib/website/edit/EditableWebsite.svelte index 69ee9631..37afeb49 100644 --- a/src/lib/website/EditableWebsite.svelte +++ b/src/lib/website/edit/EditableWebsite.svelte @@ -1,39 +1,42 @@ -
- +
+ {#snippet child({ props })} diff --git a/src/lib/website/edit/LayoutPanel.svelte b/src/lib/website/edit/LayoutPanel.svelte new file mode 100644 index 00000000..ae80e2e7 --- /dev/null +++ b/src/lib/website/edit/LayoutPanel.svelte @@ -0,0 +1,259 @@ + + +
+ +
+
+ + + + + + + Profile + +
+ {#if !hideProfile} +
+ + +
+ {/if} +
+ + + Sections + {#each sortedSections as section, i (section.id)} + {@const def = SectionDefinitionsByType[section.sectionType]} +
+ +
+ + +
+
+ {/each} + + + + {#snippet child({ props })} + + {/snippet} +
+ {#each AllSectionDefinitions as def (def.type)} + + {/each} +
+
+
diff --git a/src/lib/website/edit/MobileSelectionBar.svelte b/src/lib/website/edit/MobileSelectionBar.svelte new file mode 100644 index 00000000..a98b5ebe --- /dev/null +++ b/src/lib/website/edit/MobileSelectionBar.svelte @@ -0,0 +1,74 @@ + + +{#if visible && (dev || (user.isLoggedIn && user.profile?.did === data.did))} +
+ +
+ + +
+
+
+{/if} diff --git a/src/lib/website/edit/PageSwitcherBar.svelte b/src/lib/website/edit/PageSwitcherBar.svelte new file mode 100644 index 00000000..6f292ca6 --- /dev/null +++ b/src/lib/website/edit/PageSwitcherBar.svelte @@ -0,0 +1,37 @@ + + +{#if dev || (user.isLoggedIn && user.profile?.did === data.did)} +
+ + +
+{/if} diff --git a/src/lib/website/SaveModal.svelte b/src/lib/website/edit/SaveModal.svelte similarity index 100% rename from src/lib/website/SaveModal.svelte rename to src/lib/website/edit/SaveModal.svelte diff --git a/src/lib/website/edit/SectionPanel.svelte b/src/lib/website/edit/SectionPanel.svelte new file mode 100644 index 00000000..ec24c9c6 --- /dev/null +++ b/src/lib/website/edit/SectionPanel.svelte @@ -0,0 +1,122 @@ + + +
+ +
+ + rename((e.target as HTMLInputElement).value)} + placeholder={def?.name ?? section.sectionType} + class="border-base-200 dark:border-base-700 bg-base-50 dark:bg-base-900 text-base-900 dark:text-base-100 placeholder:text-base-400 focus:border-accent-500 w-full rounded-lg border px-3 py-1.5 text-sm outline-none" + /> +
+ + + {#if def?.settingsComponent} + + {/if} + + +
+

+ Cards ({sectionItems.length}) +

+ {#if sectionItems.length === 0} +

No cards in this section yet.

+ {:else} +
+ {#each sectionItems as item (item.id)} + {@const cardDef = CardDefinitionsByType[item.cardType]} + + {/each} +
+ {/if} +
+ + + {#if canDelete} +
+ +
+ {/if} +
diff --git a/src/lib/website/edit/SettingsSidebar.svelte b/src/lib/website/edit/SettingsSidebar.svelte new file mode 100644 index 00000000..f234b420 --- /dev/null +++ b/src/lib/website/edit/SettingsSidebar.svelte @@ -0,0 +1,414 @@ + + + diff --git a/src/lib/website/settings/SettingsOverlay.svelte b/src/lib/website/settings/SettingsOverlay.svelte index b92b0702..31e7b874 100644 --- a/src/lib/website/settings/SettingsOverlay.svelte +++ b/src/lib/website/settings/SettingsOverlay.svelte @@ -35,7 +35,7 @@ {#if settingsOverlayState.visible} -
+
diff --git a/src/lib/website/settings/sections/AccountSection.svelte b/src/lib/website/settings/sections/AccountSection.svelte index 4e69cc98..021dd310 100644 --- a/src/lib/website/settings/sections/AccountSection.svelte +++ b/src/lib/website/settings/sections/AccountSection.svelte @@ -93,7 +93,12 @@ -
diff --git a/src/lib/website/settings/sections/PageSection.svelte b/src/lib/website/settings/sections/PageSection.svelte index b858276a..b76134c9 100644 --- a/src/lib/website/settings/sections/PageSection.svelte +++ b/src/lib/website/settings/sections/PageSection.svelte @@ -1,6 +1,6 @@ diff --git a/src/routes/(legal)/+layout.svelte b/src/routes/(legal)/+layout.svelte index 91ba69a1..e6d16b30 100644 --- a/src/routes/(legal)/+layout.svelte +++ b/src/routes/(legal)/+layout.svelte @@ -1,5 +1,5 @@ diff --git a/src/routes/+error.svelte b/src/routes/+error.svelte index e7718b67..b980b69e 100644 --- a/src/routes/+error.svelte +++ b/src/routes/+error.svelte @@ -1,6 +1,6 @@
- import { refreshData } from '$lib/helper.js'; - import Website from '$lib/website/Website.svelte'; - import { onMount } from 'svelte'; + import Website from '$lib/website/view/Website.svelte'; let { data } = $props(); - - onMount(() => { - refreshData(data); - }); diff --git a/src/routes/[[actor=actor]]/(pages)/edit/+page.svelte b/src/routes/[[actor=actor]]/(pages)/edit/+page.svelte index e57bf319..5771973f 100644 --- a/src/routes/[[actor=actor]]/(pages)/edit/+page.svelte +++ b/src/routes/[[actor=actor]]/(pages)/edit/+page.svelte @@ -1,5 +1,5 @@ diff --git a/src/routes/[[actor=actor]]/(pages)/p/[[page]]/+page.svelte b/src/routes/[[actor=actor]]/(pages)/p/[[page]]/+page.svelte index 3419d88c..7a315e4f 100644 --- a/src/routes/[[actor=actor]]/(pages)/p/[[page]]/+page.svelte +++ b/src/routes/[[actor=actor]]/(pages)/p/[[page]]/+page.svelte @@ -1,13 +1,7 @@ diff --git a/src/routes/[[actor=actor]]/(pages)/p/[[page]]/edit/+page.svelte b/src/routes/[[actor=actor]]/(pages)/p/[[page]]/edit/+page.svelte index e57bf319..5771973f 100644 --- a/src/routes/[[actor=actor]]/(pages)/p/[[page]]/edit/+page.svelte +++ b/src/routes/[[actor=actor]]/(pages)/p/[[page]]/edit/+page.svelte @@ -1,5 +1,5 @@ diff --git a/src/routes/[[actor=actor]]/.well-known/site.standard.publication/+server.ts b/src/routes/[[actor=actor]]/.well-known/site.standard.publication/+server.ts index b1d75ece..e1baf25a 100644 --- a/src/routes/[[actor=actor]]/.well-known/site.standard.publication/+server.ts +++ b/src/routes/[[actor=actor]]/.well-known/site.standard.publication/+server.ts @@ -1,9 +1,9 @@ -import { loadData } from '$lib/website/load'; -import { createCache } from '$lib/cache'; +import { loadData } from '$lib/website/data/load'; +import { createCache } from '$lib/helpers/cache'; import { env } from '$env/dynamic/private'; import { error } from '@sveltejs/kit'; import { text } from '@sveltejs/kit'; -import { getActor } from '$lib/actor.js'; +import { getActor } from '$lib/helpers/actor.js'; export async function GET({ params, platform, request }) { const cache = createCache(platform); diff --git a/src/routes/[[actor=actor]]/api/reindex/+server.ts b/src/routes/[[actor=actor]]/api/reindex/+server.ts index 28a2b458..88ffa1ff 100644 --- a/src/routes/[[actor=actor]]/api/reindex/+server.ts +++ b/src/routes/[[actor=actor]]/api/reindex/+server.ts @@ -2,7 +2,7 @@ import { json } from '@sveltejs/kit'; import { contrail, ensureInit } from '$lib/contrail'; import { listRecords } from '$lib/atproto/methods'; import { collections } from '$lib/atproto/settings'; -import { getActor } from '$lib/actor'; +import { getActor } from '$lib/helpers/actor'; import type { Did } from '@atcute/lexicons'; import { dev } from '$app/environment'; diff --git a/src/routes/[[actor=actor]]/blog/+layout.server.ts b/src/routes/[[actor=actor]]/blog/+layout.server.ts index 5a88b2b4..9406d13a 100644 --- a/src/routes/[[actor=actor]]/blog/+layout.server.ts +++ b/src/routes/[[actor=actor]]/blog/+layout.server.ts @@ -1,6 +1,6 @@ import { getRecord } from '$lib/atproto/methods.js'; import type { Did } from '@atcute/lexicons'; -import { getActor } from '$lib/actor.js'; +import { getActor } from '$lib/helpers/actor.js'; export async function load({ params, platform, request }) { const did = await getActor({ request, paramActor: params.actor, platform }); diff --git a/src/routes/[[actor=actor]]/blog/+layout.svelte b/src/routes/[[actor=actor]]/blog/+layout.svelte index ea1c6fce..618ff8aa 100644 --- a/src/routes/[[actor=actor]]/blog/+layout.svelte +++ b/src/routes/[[actor=actor]]/blog/+layout.svelte @@ -1,5 +1,5 @@ diff --git a/src/routes/[[actor=actor]]/blog/+page.server.ts b/src/routes/[[actor=actor]]/blog/+page.server.ts index 838966b3..7adb2e65 100644 --- a/src/routes/[[actor=actor]]/blog/+page.server.ts +++ b/src/routes/[[actor=actor]]/blog/+page.server.ts @@ -1,8 +1,8 @@ import { error } from '@sveltejs/kit'; import { getBlentoOrBskyProfile, getRecord, listRecords, parseUri } from '$lib/atproto/methods.js'; -import { createCache, type CachedProfile } from '$lib/cache'; +import { createCache, type CachedProfile } from '$lib/helpers/cache'; import type { Did } from '@atcute/lexicons'; -import { getActor } from '$lib/actor.js'; +import { getActor } from '$lib/helpers/actor.js'; export async function load({ params, platform, request }) { const cache = createCache(platform); diff --git a/src/routes/[[actor=actor]]/blog/[rkey]/+page.server.ts b/src/routes/[[actor=actor]]/blog/[rkey]/+page.server.ts index 9c3e6340..015fc94a 100644 --- a/src/routes/[[actor=actor]]/blog/[rkey]/+page.server.ts +++ b/src/routes/[[actor=actor]]/blog/[rkey]/+page.server.ts @@ -1,8 +1,8 @@ import { error } from '@sveltejs/kit'; import { getBlentoOrBskyProfile, getRecord, parseUri } from '$lib/atproto/methods.js'; -import { createCache, type CachedProfile } from '$lib/cache'; +import { createCache, type CachedProfile } from '$lib/helpers/cache'; import type { Did } from '@atcute/lexicons'; -import { getActor } from '$lib/actor'; +import { getActor } from '$lib/helpers/actor'; export async function load({ params, platform, request }) { const { rkey } = params; diff --git a/src/routes/[[actor=actor]]/blog/[rkey]/+page.svelte b/src/routes/[[actor=actor]]/blog/[rkey]/+page.svelte index f73c50e0..ba054cc8 100644 --- a/src/routes/[[actor=actor]]/blog/[rkey]/+page.svelte +++ b/src/routes/[[actor=actor]]/blog/[rkey]/+page.svelte @@ -2,7 +2,7 @@ import { getCDNImageBlobUrl } from '$lib/atproto'; import { Avatar as FoxAvatar } from '@foxui/core'; import { marked } from 'marked'; - import { sanitize } from '$lib/sanitize'; + import { sanitize } from '$lib/helpers/sanitize'; import { all, createLowlight } from 'lowlight'; const lowlight = createLowlight(all); diff --git a/src/routes/[[actor=actor]]/card/[rkey]/+page.server.ts b/src/routes/[[actor=actor]]/card/[rkey]/+page.server.ts deleted file mode 100644 index 49190dd3..00000000 --- a/src/routes/[[actor=actor]]/card/[rkey]/+page.server.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { createCache } from '$lib/cache'; -import { getActor } from '$lib/actor'; -import { loadCardData } from '$lib/website/load'; -import { error } from '@sveltejs/kit'; -import { env } from '$env/dynamic/private'; - -export async function load({ params, platform, request }) { - const cache = createCache(platform); - const actor = await getActor({ request, paramActor: params.actor, platform }); - - if (!actor) { - throw error(404, 'Page not found'); - } - - return await loadCardData(actor, params.rkey, cache, env, platform); -} diff --git a/src/routes/[[actor=actor]]/card/[rkey]/+page.svelte b/src/routes/[[actor=actor]]/card/[rkey]/+page.svelte deleted file mode 100644 index adeea905..00000000 --- a/src/routes/[[actor=actor]]/card/[rkey]/+page.svelte +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/src/routes/[[actor=actor]]/card/[rkey]/type/[type]/+page.server.ts b/src/routes/[[actor=actor]]/card/[rkey]/type/[type]/+page.server.ts deleted file mode 100644 index 54c7d18d..00000000 --- a/src/routes/[[actor=actor]]/card/[rkey]/type/[type]/+page.server.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { createCache } from '$lib/cache'; -import { getActor } from '$lib/actor'; -import { loadCardTypeData } from '$lib/website/load'; -import { error } from '@sveltejs/kit'; -import { env } from '$env/dynamic/private'; - -function parseQueryParamValue(value: string): unknown { - const trimmed = value.trim(); - - if (trimmed === 'true') return true; - if (trimmed === 'false') return false; - if (trimmed === 'null') return null; - - if (/^-?(?:0|[1-9]\d*)(?:\.\d+)?$/.test(trimmed)) { - return Number(trimmed); - } - - if ( - (trimmed.startsWith('{') && trimmed.endsWith('}')) || - (trimmed.startsWith('[') && trimmed.endsWith(']')) - ) { - try { - return JSON.parse(trimmed); - } catch { - return value; - } - } - - return value; -} - -function getCardDataFromSearchParams(searchParams: URLSearchParams) { - const cardData: Record = {}; - const keys = new Set(searchParams.keys()); - - for (const key of keys) { - const values = searchParams.getAll(key).map(parseQueryParamValue); - cardData[key] = values.length === 1 ? values[0] : values; - } - - return cardData; -} - -export async function load({ params, platform, request, url }) { - const cache = createCache(platform); - const actor = await getActor({ request, paramActor: params.actor, platform }); - - if (!actor) { - throw error(404, 'Page not found'); - } - - return await loadCardTypeData( - actor, - params.type, - getCardDataFromSearchParams(url.searchParams), - cache, - env, - platform - ); -} diff --git a/src/routes/[[actor=actor]]/card/[rkey]/type/[type]/+page.svelte b/src/routes/[[actor=actor]]/card/[rkey]/type/[type]/+page.svelte deleted file mode 100644 index adeea905..00000000 --- a/src/routes/[[actor=actor]]/card/[rkey]/type/[type]/+page.svelte +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/src/routes/[[actor=actor]]/event/create/+page.svelte b/src/routes/[[actor=actor]]/event/create/+page.svelte index 30d96c77..2ca7e404 100644 --- a/src/routes/[[actor=actor]]/event/create/+page.svelte +++ b/src/routes/[[actor=actor]]/event/create/+page.svelte @@ -1,40 +1,25 @@ Create event · Blento -
- -
+ diff --git a/src/routes/[[actor=actor]]/event/r/[rkey]/+page.server.ts b/src/routes/[[actor=actor]]/event/r/[rkey]/+page.server.ts index 58d5ff28..e0744cd4 100644 --- a/src/routes/[[actor=actor]]/event/r/[rkey]/+page.server.ts +++ b/src/routes/[[actor=actor]]/event/r/[rkey]/+page.server.ts @@ -1,11 +1,82 @@ import { error } from '@sveltejs/kit'; -import { getActor } from '$lib/actor'; +import { getActor } from '$lib/helpers/actor'; +import { + fetchAtmoEvent, + fetchAtmoEventAttendees, + fetchAtmoProfile, + fetchAtmoViewerRsvp, + getHostProfile, + getProfileBlobUrl, + getRsvpStatus, + vodFromAtUri +} from '$lib/events/atmo-appview'; +import type { ActorIdentifier } from '@atcute/lexicons'; -export async function load({ params, request, platform }) { +export async function load({ params, request, platform, locals, url }) { if (!params.rkey) error(404, 'Event URL missing rkey'); - const actor = await getActor({ request, paramActor: params.actor, platform }); - if (!actor) error(404, 'Could not resolve actor'); + const did = await getActor({ + request, + paramActor: params.actor as ActorIdentifier | undefined, + platform + }); + if (!did) error(404, 'Could not resolve actor'); - return { actor, rkey: params.rkey }; + const event = await fetchAtmoEvent({ did, rkey: params.rkey }); + if (!event) error(404, 'Event not found'); + + const additional = event.flat.additionalData as Record | undefined; + const isAtmosphereconf = !!additional?.isAtmosphereconf; + const speakers = (additional?.speakers as Array<{ id: string; name: string }> | undefined) ?? []; + const vodAtUri = additional?.vodAtUri as string | undefined; + const vod = vodAtUri ? vodFromAtUri(vodAtUri) : null; + + const viewerDid = locals.did ?? null; + + const [attendees, viewerRsvpRecord, parentEvent, ...speakerProfiles] = await Promise.all([ + fetchAtmoEventAttendees(event.flat.uri), + viewerDid ? fetchAtmoViewerRsvp({ eventUri: event.flat.uri, actor: viewerDid }) : null, + isAtmosphereconf + ? fetchAtmoEvent({ + did: 'did:plc:lehcqqkwzcwvjvw66uthu5oq', + rkey: '3lte3c7x43l2e' + }) + .then((e) => e?.flat ?? null) + .catch(() => null) + : null, + ...speakers.map(async (s) => { + if (!s.id) return { id: undefined, name: s.name, avatar: undefined, handle: undefined }; + try { + const p = await fetchAtmoProfile(s.id); + return { + id: s.id, + name: s.name, + avatar: p?.value?.avatar ? getProfileBlobUrl(p.did, p.value.avatar) : undefined, + handle: p?.handle || s.id + }; + } catch { + return { id: s.id, name: s.name, avatar: undefined, handle: s.id }; + } + }) + ]); + + return { + ogImage: `https://atmo.rsvp/p/${params.actor || did}/e/${params.rkey}/og.png`, + eventData: event.flat, + actorDid: did, + rkey: params.rkey, + hostProfile: getHostProfile(did, event.raw.profiles) ?? null, + attendees, + viewerRsvpStatus: getRsvpStatus(viewerRsvpRecord?.value?.status), + viewerRsvpRkey: viewerRsvpRecord?.rkey ?? null, + parentEvent, + vod, + speakerProfiles: speakerProfiles as Array<{ + id?: string; + name: string; + avatar?: string; + handle?: string; + }>, + shareUrl: url.href + }; } diff --git a/src/routes/[[actor=actor]]/event/r/[rkey]/+page.svelte b/src/routes/[[actor=actor]]/event/r/[rkey]/+page.svelte index 789c1a15..77733f80 100644 --- a/src/routes/[[actor=actor]]/event/r/[rkey]/+page.svelte +++ b/src/routes/[[actor=actor]]/event/r/[rkey]/+page.svelte @@ -1,30 +1,23 @@ - Event · Blento - + {data.eventData.name} · Blento + - + - + -
- -
+ diff --git a/src/routes/[[actor=actor]]/og-new.png/+server.ts b/src/routes/[[actor=actor]]/og-new.png/+server.ts index c8bfaa5e..78bb3c3d 100644 --- a/src/routes/[[actor=actor]]/og-new.png/+server.ts +++ b/src/routes/[[actor=actor]]/og-new.png/+server.ts @@ -1,8 +1,8 @@ import { env } from '$env/dynamic/private'; import { env as publicEnv } from '$env/dynamic/public'; import { error, json } from '@sveltejs/kit'; -import { getActor } from '$lib/actor'; -import { createCache } from '$lib/cache'; +import { getActor } from '$lib/helpers/actor'; +import { createCache } from '$lib/helpers/cache'; export async function GET({ params, platform, request }) { const actor = await getActor({ diff --git a/src/routes/api/activate-domain/+server.ts b/src/routes/api/activate-domain/+server.ts index 25722d8a..b080a693 100644 --- a/src/routes/api/activate-domain/+server.ts +++ b/src/routes/api/activate-domain/+server.ts @@ -1,7 +1,7 @@ import { json } from '@sveltejs/kit'; import { isDid } from '@atcute/lexicons/syntax'; import { getRecord } from '$lib/atproto/methods'; -import { verifyDomainDns } from '$lib/dns'; +import { verifyDomainDns } from '$lib/helpers/dns'; import type { Did } from '@atcute/lexicons'; const EXPECTED_TARGET = 'blento-proxy.fly.dev'; diff --git a/src/routes/api/image-proxy/+server.ts b/src/routes/api/image-proxy/+server.ts index 44c02254..3bc7e392 100644 --- a/src/routes/api/image-proxy/+server.ts +++ b/src/routes/api/image-proxy/+server.ts @@ -1,5 +1,5 @@ import { error } from '@sveltejs/kit'; -import { parseSafeUrl } from '$lib/ssrf'; +import { parseSafeUrl } from '$lib/helpers/ssrf'; export async function GET({ url, locals }) { if (!locals.did) { diff --git a/src/routes/api/links/+server.ts b/src/routes/api/links/+server.ts index da65d4ea..f0cc50e2 100644 --- a/src/routes/api/links/+server.ts +++ b/src/routes/api/links/+server.ts @@ -1,6 +1,6 @@ import { json } from '@sveltejs/kit'; import { getLinkPreview } from 'link-preview-js'; -import { parseSafeUrl } from '$lib/ssrf'; +import { parseSafeUrl } from '$lib/helpers/ssrf'; export async function GET({ url, locals }) { if (!locals.did) { diff --git a/src/routes/api/verify-domain/+server.ts b/src/routes/api/verify-domain/+server.ts index e8358d74..79469b09 100644 --- a/src/routes/api/verify-domain/+server.ts +++ b/src/routes/api/verify-domain/+server.ts @@ -1,5 +1,5 @@ import { json } from '@sveltejs/kit'; -import { verifyDomainDns } from '$lib/dns'; +import { verifyDomainDns } from '$lib/helpers/dns'; const EXPECTED_TARGET = 'blento-proxy.fly.dev'; diff --git a/src/routes/test-cards/+layout.ts b/src/routes/test-cards/+layout.ts new file mode 100644 index 00000000..83addb7e --- /dev/null +++ b/src/routes/test-cards/+layout.ts @@ -0,0 +1,2 @@ +export const ssr = false; +export const prerender = false; diff --git a/src/routes/test-cards/+page.svelte b/src/routes/test-cards/+page.svelte new file mode 100644 index 00000000..e9eb86b4 --- /dev/null +++ b/src/routes/test-cards/+page.svelte @@ -0,0 +1,17 @@ + + + + Card test · view + + +{#await ready} +
Loading card data…
+{:then loaded} + +{/await} diff --git a/src/routes/test-cards/build-data.ts b/src/routes/test-cards/build-data.ts new file mode 100644 index 00000000..8275d5bb --- /dev/null +++ b/src/routes/test-cards/build-data.ts @@ -0,0 +1,346 @@ +import { COLUMNS } from '$lib'; +import { AllCardDefinitions, CardDefinitionsByType } from '$lib/cards'; +import { fixAllCollisions, compactItems } from '$lib/layout'; +import type { Item, SectionRecord, WebsiteData } from '$lib/types'; +import { createEmptyCard } from '$lib/helpers/items'; +import * as TID from '@atcute/tid'; +import type { Did } from '@atcute/lexicons'; +import type { AppBskyActorDefs } from '@atcute/bluesky'; + +// All cards point at this real PDS so any card that pulls data from the +// page's DID (atprotocollections, npmxLikes, margin, rpgActor, kichRecipe…) +// shows real data without hand-fed records. +const TEST_DID = 'did:plc:257wekqxg4hyapkq6k47igmp' as Did; +const TEST_HANDLE = 'flo-bit.dev'; + +// Categories rendered top-to-bottom. Cards without a group fall into "Other". +const CATEGORY_ORDER = [ + 'Core', + 'Sections', + 'Content', + 'Utilities', + 'Visual', + 'Games', + 'Media', + 'Social', + 'Other' +] as const; + +// Hand-picked test data for card types where the default is not enough. +// Anything not listed falls back to whatever `createNew` produces. +const CARD_DATA_PRESETS: Record) => void> = { + link: (d) => { + d.href = 'https://blento.app'; + d.domain = 'blento.app'; + d.label = 'Blento'; + d.title = 'Blento'; + d.description = 'Bluesky-powered bento grid websites.'; + d.hasFetched = true; + }, + image: (d) => { + d.image = 'https://picsum.photos/seed/blento/800/800'; + d.alt = 'Random sample image'; + }, + mapLocation: (d) => { + // Berlin + d.lat = '52.5200'; + d.lon = '13.4050'; + d.name = 'Berlin'; + d.type = 'city'; + d.zoom = 10; + }, + button: (d) => { + d.text = 'Click me'; + d.href = 'https://blento.app'; + }, + countdown: (d) => { + const year = new Date().getUTCFullYear() + 1; + d.targetDate = new Date(Date.UTC(year, 0, 1, 0, 0, 0)).toISOString(); + d.label = `New Year ${year}`; + }, + statusphere: (d) => { + d.mode = 'emoji'; + d.emoji = '👋'; + }, + 'fluid-text': (d) => { + d.text = 'FLUID'; + }, + githubProfile: (d) => { + d.user = 'flo-bit'; + d.href = 'https://github.com/flo-bit'; + }, + githubContributors: (d) => { + d.owner = 'sveltejs'; + d.repo = 'svelte'; + }, + bigsocial: (d) => { + d.platform = 'bluesky'; + d.href = 'https://bsky.app'; + }, + blueskyPost: (d) => { + d.uri = 'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3l6oveex3ii2l'; + d.href = 'https://bsky.app/profile/bsky.app/post/3l6oveex3ii2l'; + }, + blueskyProfile: (d) => { + d.handle = 'bsky.app'; + d.displayName = 'Bluesky'; + d.avatar = + 'https://cdn.bsky.app/img/avatar/plain/did:plc:z72i7hdynmk6r22z27h6tvur/bafkreih6cqcioehosbf2pikllesmegjxopcljpwcvwsskosygi6umm6kuq@jpeg'; + }, + blueskyFeed: (d) => { + d.handle = 'bsky.app'; + d.did = 'did:plc:z72i7hdynmk6r22z27h6tvur'; + }, + latestPost: (d) => { + d.label = 'Latest bluesky post'; + }, + friends: (d) => { + d.friends = ['did:plc:z72i7hdynmk6r22z27h6tvur']; + }, + blueskyMedia: (d) => { + d.image = { + fullsize: 'https://picsum.photos/seed/blueskymedia/800/800', + thumbnail: 'https://picsum.photos/seed/blueskymedia/400/400', + alt: 'Bluesky media placeholder' + }; + d.href = 'https://bsky.app/profile/flo-bit.dev/post/3mlf4zqebms2t'; + }, + 'grain-gallery': (d) => { + d.galleryUri = 'at://did:plc:z5efhtji4vfiutzqquvx3w7o/social.grain.gallery/3mkstfoquxv2m'; + d.href = 'https://grain.social/profile/did:plc:z5efhtji4vfiutzqquvx3w7o/gallery/3mkstfoquxv2m'; + }, + latestLivestream: (d) => { + d.href = 'https://stream.place/rtareview.stream'; + }, + livestreamEmbed: (d) => { + d.href = 'https://stream.place/rtareview.stream'; + d.embed = 'https://stream.place/embed/rtareview.stream'; + }, + producthunt: (d) => { + d.imageSrc = + 'https://api.producthunt.com/widgets/embed-image/v1/product_rating.svg?product_id=1097839&theme=light'; + d.linkHref = + 'https://www.producthunt.com/products/graphbit/reviews?utm_source=badge-product_rating&utm_medium=badge&utm_source=badge-graphbit'; + }, + kickstarter: (d) => { + d.src = 'https://www.kickstarter.com/projects/wraithmarked/hitchhikers/widget/card.html?v=2'; + d.widgetType = 'card'; + }, + event: (d) => { + d.uri = 'at://did:plc:257wekqxg4hyapkq6k47igmp/community.lexicon.calendar.event/3mlloxhxtmcxh'; + }, + 'plyr-fm': (d) => { + d.href = 'https://plyr.fm/embed/track/976'; + }, + 'plyr-fm-collection': (d) => { + // User gave a profile URL, not an album/playlist. Use the user's profile URL anyway — + // PlyrFM will likely render an error iframe, but the card itself shouldn't crash. + d.href = 'https://plyr.fm/u/eurocity.band'; + }, + recentTealFMPlays: (d) => { + d.handle = 'flo-bit.dev'; + }, + recentRockskyPlays: (d) => { + d.handle = 'flo-bit.dev'; + }, + vcard: (d) => { + d.vcard = [ + 'BEGIN:VCARD', + 'VERSION:3.0', + 'N:Tester;Card;;;', + 'FN:Card Tester', + 'ORG:Blento', + 'TITLE:Test Page', + 'URL:https://blento.app', + 'END:VCARD' + ].join('\n'); + }, + youtubeVideo: (d) => { + d.youtubeId = 'dQw4w9WgXcQ'; + d.href = 'https://www.youtube.com/watch?v=dQw4w9WgXcQ'; + d.poster = 'https://i.ytimg.com/vi/dQw4w9WgXcQ/hqdefault.jpg'; + d.showInline = false; + }, + 'spotify-list-embed': (d) => { + d.spotifyType = 'track'; + d.spotifyId = '4cOdK2wGLETKBW3PvgPWqT'; + d.href = 'https://open.spotify.com/track/4cOdK2wGLETKBW3PvgPWqT'; + }, + 'apple-music-embed': (d) => { + d.appleMusicStorefront = 'us'; + d.appleMusicType = 'album'; + d.appleMusicId = '1440857781'; + d.href = 'https://music.apple.com/us/album/1440857781'; + }, + 'soundcloud-embed': (d) => { + d.href = 'https://soundcloud.com/forss/flickermood'; + }, + gif: (d) => { + d.url = 'https://media.giphy.com/media/dzaUX7CAG0Ihi/giphy.mp4'; + d.alt = 'Test GIF'; + }, + lastfmRecentTracks: (d) => { + d.lastfmUsername = 'rj'; + }, + lastfmTopTracks: (d) => { + d.lastfmUsername = 'rj'; + d.period = '7day'; + }, + lastfmTopAlbums: (d) => { + d.lastfmUsername = 'rj'; + d.period = '7day'; + }, + lastfmProfile: (d) => { + d.lastfmUsername = 'rj'; + }, + listenbrainzRecentListens: (d) => { + d.username = 'rob'; + }, + listenbrainzTopArtists: (d) => { + d.username = 'rob'; + }, + listenbrainzTopAlbums: (d) => { + d.username = 'rob'; + }, + listenbrainzTopSongs: (d) => { + d.username = 'rob'; + }, + listenbrainzNowPlaying: (d) => { + d.username = 'rob'; + } +}; + +function buildHeadingCard(page: string, sectionId: string, title: string): Item { + const card = createEmptyCard(page, sectionId); + card.cardType = 'section'; + card.cardData = { + text: title, + verticalAlign: 'bottom', + textSize: 1 + }; + card.w = COLUMNS; + card.h = 1; + card.mobileW = COLUMNS; + card.mobileH = 1; + card.x = 0; + card.y = 0; + card.mobileX = 0; + card.mobileY = 0; + card.version = 2; + return card; +} + +export function buildTestData(): WebsiteData { + const page = 'blento.self'; + const sectionId = TID.now(); + + const section: SectionRecord = { + id: sectionId, + sectionType: 'grid', + page, + index: 0, + sectionData: {}, + version: 1 + }; + + // Bucket cards by category, preserving order from AllCardDefinitions. + const buckets = new Map(); + for (const def of AllCardDefinitions) { + if (def.type === 'section') continue; + const category = (def.groups?.[0] as string | undefined) ?? 'Other'; + const bucket = buckets.get(category) ?? []; + bucket.push(def); + buckets.set(category, bucket); + } + + const orderedCategories = [ + ...CATEGORY_ORDER.filter((c) => buckets.has(c)), + ...[...buckets.keys()].filter((c) => !(CATEGORY_ORDER as readonly string[]).includes(c)) + ]; + + const cards: Item[] = []; + + for (const category of orderedCategories) { + const defs = buckets.get(category); + if (!defs?.length) continue; + + cards.push(buildHeadingCard(page, sectionId, category)); + + for (const def of defs) { + const card = createEmptyCard(page, sectionId); + card.cardType = def.type; + def.createNew?.(card); + + const preset = CARD_DATA_PRESETS[def.type]; + if (preset) preset(card.cardData ?? (card.cardData = {})); + + const w = Math.max(card.w, def.minW ?? 2); + const h = Math.max(card.h, def.minH ?? 2); + card.w = Math.min(w, def.maxW ?? COLUMNS); + card.h = Math.min(h, def.maxH ?? 16); + card.mobileW = Math.max(card.mobileW, Math.min(card.w * 2, COLUMNS)); + card.mobileH = Math.max(card.mobileH, card.h); + + card.x = 0; + card.y = 0; + card.mobileX = 0; + card.mobileY = 0; + card.version = 2; + + cards.push(card); + } + } + + fixAllCollisions(cards, false); + fixAllCollisions(cards, true); + compactItems(cards, false); + compactItems(cards, true); + + const profile: AppBskyActorDefs.ProfileViewDetailed = { + did: TEST_DID, + handle: TEST_HANDLE as `${string}.${string}`, + displayName: 'Card Test Page', + description: 'Every card type, rendered against flo-bit.dev for live data.' + }; + + return { + page, + did: TEST_DID, + handle: TEST_HANDLE, + cards, + sections: [section], + publication: { + name: 'Card Test Page', + description: 'Every card type, rendered against flo-bit.dev for live data.' + }, + additionalData: {}, + profile, + updatedAt: Date.now(), + version: 1 + }; +} + +/** + * Populate `data.additionalData` by invoking each card type's `loadData` + * client-side. We can't run `loadDataServer` here (needs platform/env), so + * cards that only define server loaders will appear empty. + */ +export async function loadAllAdditionalData(data: WebsiteData): Promise { + const cardTypes = new Set(data.cards.map((c) => c.cardType)); + await Promise.all( + [...cardTypes].map(async (cardType) => { + const cardDef = CardDefinitionsByType[cardType]; + if (!cardDef?.loadData) return; + const items = data.cards.filter((c) => c.cardType === cardType); + try { + const result = await cardDef.loadData(items, { + did: data.did as Did, + handle: data.handle + }); + data.additionalData[cardType] = result; + } catch (err) { + console.error('test-cards: loadData failed for', cardType, err); + } + }) + ); +} diff --git a/src/routes/test-cards/edit/+page.svelte b/src/routes/test-cards/edit/+page.svelte new file mode 100644 index 00000000..50754c6e --- /dev/null +++ b/src/routes/test-cards/edit/+page.svelte @@ -0,0 +1,17 @@ + + + + Card test · edit + + +{#await ready} +
Loading card data…
+{:then loaded} + +{/await} diff --git a/todo-new-desing.md b/todo-new-desing.md new file mode 100644 index 00000000..f467c36b --- /dev/null +++ b/todo-new-desing.md @@ -0,0 +1,16 @@ +- better card adding modal thing (show previews) + +## onboarding + +- allow selecting what layout you wanna work in (both in onboarding and in settings), for the start: either basic bento (one grid section only, default to that for existing pages too) or "expert" layout (with multiple sections, section options, etc) + +## card settings + +- improve card settings for all cards, now that those are in the sidebar + +## sections + +- make sections use that card settings sidebar +- show section settings (add button under section for that) + +## themes diff --git a/vite.config.ts b/vite.config.ts index 9307d28e..4fff4e8d 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -5,6 +5,13 @@ import { sveltekitOG } from '@ethercorps/sveltekit-og/plugin'; export default defineConfig({ plugins: [sveltekit(), tailwindcss(), sveltekitOG()], + // Keep a single copy of these shared deps. @atmo-dev/events-ui and blento both + // pull in bits-ui / @internationalized/date / svelte; without deduping, bits-ui's + // `instanceof Time` checks can fail across copies → "dateValue.toDate is not a + // function" in the time picker. + resolve: { + dedupe: ['bits-ui', '@internationalized/date', 'svelte'] + }, server: { host: '127.0.0.1', port: 5179