--
2.51.2
From 5f5046525a985672cab8c29babcf8a4655034a0e Mon Sep 17 00:00:00 2001
From: Florian <45694132+flo-bit@users.noreply.github.com>
Date: Sat, 31 Jan 2026 14:16:21 +0100
Subject: [PATCH 05/21] add card command bar
---
docs/Beta.md | 42 --------
package.json | 1 +
pnpm-lock.yaml | 12 +++
src/app.css | 4 +
src/lib/cards/ATProtoCollectionsCard/index.ts | 6 +-
src/lib/cards/BigSocialCard/index.ts | 4 +-
src/lib/cards/BlueskyMediaCard/index.ts | 6 +-
src/lib/cards/BlueskyPostCard/index.ts | 4 +-
src/lib/cards/ButtonCard/index.ts | 5 +-
src/lib/cards/DrawCard/index.ts | 4 +-
src/lib/cards/EmbedCard/index.ts | 2 +-
src/lib/cards/EventCard/index.ts | 4 +-
src/lib/cards/FluidTextCard/index.ts | 5 +-
src/lib/cards/GIFCard/index.ts | 4 +-
src/lib/cards/GameCards/DinoGameCard/index.ts | 5 +-
src/lib/cards/GameCards/TetrisCard/index.ts | 6 +-
src/lib/cards/GitHubProfileCard/index.ts | 4 +-
src/lib/cards/GuestbookCard/index.ts | 3 +-
src/lib/cards/ImageCard/index.ts | 21 +++-
src/lib/cards/LatestBlueskyPostCard/index.ts | 6 +-
.../cards/LinkCard/CreateLinkCardModal.svelte | 44 ++++++++
src/lib/cards/LinkCard/index.ts | 24 ++++-
src/lib/cards/LivestreamCard/index.ts | 3 +-
src/lib/cards/MapCard/index.ts | 11 +-
src/lib/cards/PopfeedReviews/index.ts | 5 +-
src/lib/cards/SectionCard/index.ts | 17 ++-
src/lib/cards/SpotifyCard/index.ts | 4 +-
.../StandardSiteDocumentListCard/index.ts | 6 +-
src/lib/cards/StatusphereCard/index.ts | 5 +-
src/lib/cards/TealFMPlaysCard/index.ts | 6 +-
src/lib/cards/TextCard/index.ts | 17 ++-
src/lib/cards/TimerCard/index.ts | 3 +-
src/lib/cards/VCardCard/index.ts | 3 +-
src/lib/cards/YoutubeVideoCard/index.ts | 11 +-
src/lib/cards/types.ts | 6 ++
.../card-command/CardCommand.svelte | 102 ++++++++++++++++++
src/lib/website/EditBar.svelte | 9 +-
src/lib/website/EditableWebsite.svelte | 23 +++-
38 files changed, 371 insertions(+), 76 deletions(-)
delete mode 100644 docs/Beta.md
create mode 100644 src/lib/cards/LinkCard/CreateLinkCardModal.svelte
create mode 100644 src/lib/components/card-command/CardCommand.svelte
diff --git a/docs/Beta.md b/docs/Beta.md
deleted file mode 100644
index 6dc5a68..0000000
--- a/docs/Beta.md
+++ /dev/null
@@ -1,42 +0,0 @@
-# Todo for beta version
-
-- site.standard
- - move description to markdownDescription and set description as text only
-
-- allow editing on mobile
-
-- get automatic layout for mobile if only edited on desktop (and vice versa)
-
-- add cards in middle of current position (both mobile and desktop version)
-
-- show nsfw warnings
-
-- card with big call to action button "create your blento"
-
-- ask to fill with some default cards on page creation
-
-- when adding images try to add them in a size that best fits aspect ratio
-
-- onboarding?
-
-- switch sidebar to a quick list of available cards with search function
-
-- test
- - selfhosting
-
-- guestbook card
-
-- onboarding?
-
-- switch sidebar to a quick list of available cards with search function
-
-- test
- - selfhosting
-
-- guestbook card
-
-- analytics?
-
-- refresh recently updated blentos (move to top of list, update profiles every 24 hours)
-
-- server side oauth?
diff --git a/package.json b/package.json
index 70fde20..b7e00bf 100644
--- a/package.json
+++ b/package.json
@@ -85,6 +85,7 @@
"svelte-sonner": "^1.0.7",
"tailwind-merge": "^3.4.0",
"tailwind-variants": "^3.2.2",
+ "tailwindcss-animate": "^1.0.7",
"three": "^0.176.0",
"turndown": "^7.2.2",
"wrangler": "^4.60.0"
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 6434d0a..fefeeb3 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -146,6 +146,9 @@ importers:
tailwind-variants:
specifier: ^3.2.2
version: 3.2.2(tailwind-merge@3.4.0)(tailwindcss@4.1.18)
+ tailwindcss-animate:
+ specifier: ^1.0.7
+ version: 1.0.7(tailwindcss@4.1.18)
three:
specifier: ^0.176.0
version: 0.176.0
@@ -2800,6 +2803,11 @@ packages:
tailwind-merge:
optional: true
+ tailwindcss-animate@1.0.7:
+ resolution: {integrity: sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==, tarball: https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz}
+ peerDependencies:
+ tailwindcss: '>=3.0.0 || insiders'
+
tailwindcss@4.1.18:
resolution: {integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==}
@@ -5561,6 +5569,10 @@ snapshots:
optionalDependencies:
tailwind-merge: 3.4.0
+ tailwindcss-animate@1.0.7(tailwindcss@4.1.18):
+ dependencies:
+ tailwindcss: 4.1.18
+
tailwindcss@4.1.18: {}
tapable@2.3.0: {}
diff --git a/src/app.css b/src/app.css
index 3914088..a8f1c18 100644
--- a/src/app.css
+++ b/src/app.css
@@ -3,6 +3,10 @@
@plugin '@tailwindcss/forms';
@plugin '@tailwindcss/typography';
+
+@plugin "tailwindcss-animate";
+
+
@source '../node_modules/@foxui';
@custom-variant dark (&:where(.dark, .dark *):not(:where(.light, .light *)));
diff --git a/src/lib/cards/ATProtoCollectionsCard/index.ts b/src/lib/cards/ATProtoCollectionsCard/index.ts
index 95a06a8..3d50466 100644
--- a/src/lib/cards/ATProtoCollectionsCard/index.ts
+++ b/src/lib/cards/ATProtoCollectionsCard/index.ts
@@ -19,5 +19,9 @@ export const ATProtoCollectionsCardDefinition = {
item.w = 4;
item.mobileW = 8;
},
- sidebarButtonText: 'Atmosphere Collections'
+ sidebarButtonText: 'Atmosphere Collections',
+
+ name: 'ATProto Collections',
+
+ groups: ['Social']
} as CardDefinition & { type: 'atprotocollections' };
diff --git a/src/lib/cards/BigSocialCard/index.ts b/src/lib/cards/BigSocialCard/index.ts
index d0657bf..d46dbbc 100644
--- a/src/lib/cards/BigSocialCard/index.ts
+++ b/src/lib/cards/BigSocialCard/index.ts
@@ -51,7 +51,9 @@ export const BigSocialCardDefinition = {
return item;
},
urlHandlerPriority: 1,
- canHaveLabel: true
+ canHaveLabel: true,
+
+ groups: ['Social']
} as CardDefinition & { type: 'bigsocial' };
import {
diff --git a/src/lib/cards/BlueskyMediaCard/index.ts b/src/lib/cards/BlueskyMediaCard/index.ts
index b3411a2..722ab57 100644
--- a/src/lib/cards/BlueskyMediaCard/index.ts
+++ b/src/lib/cards/BlueskyMediaCard/index.ts
@@ -8,5 +8,9 @@ export const BlueskyMediaCardDefinition = {
createNew: () => {},
creationModalComponent: CreateBlueskyMediaCardModal,
sidebarButtonText: 'Bluesky Media',
- canHaveLabel: true
+ canHaveLabel: true,
+
+ groups: ['Media'],
+
+ name: 'Video/Image from Bluesky'
} as CardDefinition & { type: 'blueskyMedia' };
diff --git a/src/lib/cards/BlueskyPostCard/index.ts b/src/lib/cards/BlueskyPostCard/index.ts
index aae6e61..5194efb 100644
--- a/src/lib/cards/BlueskyPostCard/index.ts
+++ b/src/lib/cards/BlueskyPostCard/index.ts
@@ -63,5 +63,7 @@ export const BlueskyPostCardDefinition = {
return postsMap;
},
minW: 4,
- name: 'Bluesky Post'
+ name: 'Bluesky Post',
+
+ groups: ['Social']
} as CardDefinition & { type: 'blueskyPost' };
diff --git a/src/lib/cards/ButtonCard/index.ts b/src/lib/cards/ButtonCard/index.ts
index e5c0080..48317ae 100644
--- a/src/lib/cards/ButtonCard/index.ts
+++ b/src/lib/cards/ButtonCard/index.ts
@@ -27,5 +27,8 @@ export const ButtonCardDefinition: CardDefinition = {
minW: 2,
minH: 1,
maxW: 8,
- maxH: 4
+ maxH: 4,
+
+ groups: ['Utilities'],
+ name: 'Button'
};
diff --git a/src/lib/cards/DrawCard/index.ts b/src/lib/cards/DrawCard/index.ts
index 29d99dd..0b36c69 100644
--- a/src/lib/cards/DrawCard/index.ts
+++ b/src/lib/cards/DrawCard/index.ts
@@ -23,5 +23,7 @@ export const DrawCardDefinition = {
strokeWidth: 1,
locked: true
};
- }
+ },
+
+ groups: ['Visual']
} as CardDefinition & { type: 'draw' };
diff --git a/src/lib/cards/EmbedCard/index.ts b/src/lib/cards/EmbedCard/index.ts
index 4bc8c95..52cd098 100644
--- a/src/lib/cards/EmbedCard/index.ts
+++ b/src/lib/cards/EmbedCard/index.ts
@@ -19,5 +19,5 @@ export const EmbedCardDefinition = {
// change: (item) => {
// return item;
// },
- name: 'Embed Card'
+ name: 'Embed'
} as CardDefinition & { type: 'embed' };
diff --git a/src/lib/cards/EventCard/index.ts b/src/lib/cards/EventCard/index.ts
index 5d03aca..01a219d 100644
--- a/src/lib/cards/EventCard/index.ts
+++ b/src/lib/cards/EventCard/index.ts
@@ -112,5 +112,7 @@ export const EventCardDefinition = {
urlHandlerPriority: 5,
- name: 'Event Card'
+ name: 'Event',
+
+ groups: ['Social']
} as CardDefinition & { type: 'event' };
diff --git a/src/lib/cards/FluidTextCard/index.ts b/src/lib/cards/FluidTextCard/index.ts
index 666e89f..a55d2f6 100644
--- a/src/lib/cards/FluidTextCard/index.ts
+++ b/src/lib/cards/FluidTextCard/index.ts
@@ -23,5 +23,8 @@ export const FluidTextCardDefinition = {
sidebarButtonText: 'Fluid Text',
defaultColor: 'transparent',
allowSetColor: true,
- minW: 2
+ minW: 2,
+
+ groups: ['Visual'],
+ name: 'Fluid Text'
} as CardDefinition & { type: 'fluid-text' };
diff --git a/src/lib/cards/GIFCard/index.ts b/src/lib/cards/GIFCard/index.ts
index 21e9cf1..56ffd26 100644
--- a/src/lib/cards/GIFCard/index.ts
+++ b/src/lib/cards/GIFCard/index.ts
@@ -45,5 +45,7 @@ export const GifCardDefinition = {
return null;
},
urlHandlerPriority: 5,
- name: 'GIF'
+ name: 'GIF',
+
+ groups: ['Media']
} as CardDefinition & { type: 'gif' };
diff --git a/src/lib/cards/GameCards/DinoGameCard/index.ts b/src/lib/cards/GameCards/DinoGameCard/index.ts
index e037b5c..b2902b2 100644
--- a/src/lib/cards/GameCards/DinoGameCard/index.ts
+++ b/src/lib/cards/GameCards/DinoGameCard/index.ts
@@ -14,5 +14,8 @@ export const DinoGameCardDefinition = {
card.mobileH = 6;
card.cardData = {};
},
- canHaveLabel: true
+ canHaveLabel: true,
+
+ groups: ['Games'],
+ name: 'Dino Game'
} as CardDefinition & { type: 'dino-game' };
diff --git a/src/lib/cards/GameCards/TetrisCard/index.ts b/src/lib/cards/GameCards/TetrisCard/index.ts
index 030f890..e11df77 100644
--- a/src/lib/cards/GameCards/TetrisCard/index.ts
+++ b/src/lib/cards/GameCards/TetrisCard/index.ts
@@ -19,5 +19,9 @@ export const TetrisCardDefinition = {
card.cardData = {};
},
maxH: 10,
- canHaveLabel: true
+ canHaveLabel: true,
+
+ groups: ['Games'],
+
+ name: 'Tetris'
} as CardDefinition & { type: 'tetris' };
diff --git a/src/lib/cards/GitHubProfileCard/index.ts b/src/lib/cards/GitHubProfileCard/index.ts
index af7ccc8..2c9d9d1 100644
--- a/src/lib/cards/GitHubProfileCard/index.ts
+++ b/src/lib/cards/GitHubProfileCard/index.ts
@@ -50,7 +50,9 @@ export const GithubProfileCardDefitition = {
return item;
},
- name: 'Github Profile'
+ name: 'Github Profile',
+
+ groups: ['Social']
} as CardDefinition & { type: 'githubProfile' };
function getGitHubUsername(url: string | undefined): string | undefined {
diff --git a/src/lib/cards/GuestbookCard/index.ts b/src/lib/cards/GuestbookCard/index.ts
index 9e4fdfe..b3a7cd6 100644
--- a/src/lib/cards/GuestbookCard/index.ts
+++ b/src/lib/cards/GuestbookCard/index.ts
@@ -60,5 +60,6 @@ export const GuestbookCardDefinition = {
return results;
},
- name: 'Guestbook'
+ name: 'Guestbook',
+ groups: ['Social']
} as CardDefinition & { type: 'guestbook' };
diff --git a/src/lib/cards/ImageCard/index.ts b/src/lib/cards/ImageCard/index.ts
index 4dcec56..9e14944 100644
--- a/src/lib/cards/ImageCard/index.ts
+++ b/src/lib/cards/ImageCard/index.ts
@@ -42,7 +42,24 @@ export const ImageCardDefinition = {
},
urlHandlerPriority: 3,
- name: 'Image Card',
+ name: 'Image',
- canHaveLabel: true
+ canHaveLabel: true,
+
+ groups: ['Core'],
+
+ icon: `
`
} as CardDefinition & { type: 'image' };
diff --git a/src/lib/cards/LatestBlueskyPostCard/index.ts b/src/lib/cards/LatestBlueskyPostCard/index.ts
index dc07aa4..f547949 100644
--- a/src/lib/cards/LatestBlueskyPostCard/index.ts
+++ b/src/lib/cards/LatestBlueskyPostCard/index.ts
@@ -18,5 +18,9 @@ export const LatestBlueskyPostCardDefinition = {
return JSON.parse(JSON.stringify(authorFeed));
},
- minW: 4
+ minW: 4,
+
+ name: 'Latest Bluesky Post',
+
+ groups: ['Social']
} as CardDefinition & { type: 'latestPost' };
diff --git a/src/lib/cards/LinkCard/CreateLinkCardModal.svelte b/src/lib/cards/LinkCard/CreateLinkCardModal.svelte
new file mode 100644
index 0000000..1699229
--- /dev/null
+++ b/src/lib/cards/LinkCard/CreateLinkCardModal.svelte
@@ -0,0 +1,44 @@
+
+
+
+
+
diff --git a/src/lib/cards/LinkCard/index.ts b/src/lib/cards/LinkCard/index.ts
index bc65d59..1fa7ec3 100644
--- a/src/lib/cards/LinkCard/index.ts
+++ b/src/lib/cards/LinkCard/index.ts
@@ -1,5 +1,6 @@
import { checkAndUploadImage, validateLink } from '$lib/helper';
import type { CardDefinition } from '../types';
+import CreateLinkCardModal from './CreateLinkCardModal.svelte';
import EditingLinkCard from './EditingLinkCard.svelte';
import LinkCard from './LinkCard.svelte';
import LinkCardSettings from './LinkCardSettings.svelte';
@@ -13,7 +14,9 @@ export const LinkCardDefinition = {
},
settingsComponent: LinkCardSettings,
- name: 'Link Card',
+ creationModalComponent: CreateLinkCardModal,
+
+ name: 'Link',
canChange: (item) => Boolean(validateLink(item.cardData?.href)),
change: (item) => {
const href = validateLink(item.cardData?.href);
@@ -36,5 +39,22 @@ export const LinkCardDefinition = {
await checkAndUploadImage(item.cardData, 'favicon');
return item;
},
- urlHandlerPriority: 0
+ urlHandlerPriority: 0,
+
+ groups: ['Core'],
+
+ icon: `
`
} as CardDefinition & { type: 'link' };
diff --git a/src/lib/cards/LivestreamCard/index.ts b/src/lib/cards/LivestreamCard/index.ts
index 9fbd513..d20adfb 100644
--- a/src/lib/cards/LivestreamCard/index.ts
+++ b/src/lib/cards/LivestreamCard/index.ts
@@ -81,7 +81,8 @@ export const LivestreamCardDefitition = {
urlHandlerPriority: 5,
- name: 'stream.place Card'
+ name: 'Latest Livestream (stream.place)',
+ groups: ['Media']
} as CardDefinition & { type: 'latestLivestream' };
export const LivestreamEmbedCardDefitition = {
diff --git a/src/lib/cards/MapCard/index.ts b/src/lib/cards/MapCard/index.ts
index 87dd5bb..e0149a1 100644
--- a/src/lib/cards/MapCard/index.ts
+++ b/src/lib/cards/MapCard/index.ts
@@ -17,7 +17,16 @@ export const MapCardDefinition = {
creationModalComponent: CreateMapCardModal,
allowSetColor: false,
canHaveLabel: true,
- settingsComponent: MapCardSettings
+ settingsComponent: MapCardSettings,
+
+ groups: ['Core'],
+
+ name: 'Map',
+
+ icon: `
+`
} as CardDefinition & { type: 'mapLocation' };
export function getZoomLevel(type: string | undefined): number {
diff --git a/src/lib/cards/PopfeedReviews/index.ts b/src/lib/cards/PopfeedReviews/index.ts
index 6888e0f..ac58439 100644
--- a/src/lib/cards/PopfeedReviews/index.ts
+++ b/src/lib/cards/PopfeedReviews/index.ts
@@ -18,5 +18,8 @@ export const PopfeedReviewsCardDefinition = {
},
minH: 3,
sidebarButtonText: 'Popfeed Reviews',
- canHaveLabel: true
+ canHaveLabel: true,
+
+ groups: ['Media'],
+ name: 'Movie and TV Reviews'
} as CardDefinition & { type: 'recentPopfeedReviews' };
diff --git a/src/lib/cards/SectionCard/index.ts b/src/lib/cards/SectionCard/index.ts
index f8a2235..c3128df 100644
--- a/src/lib/cards/SectionCard/index.ts
+++ b/src/lib/cards/SectionCard/index.ts
@@ -26,7 +26,22 @@ export const SectionCardDefinition = {
defaultColor: 'transparent',
maxH: 1,
canResize: false,
- settingsComponent: SectionCardSettings
+ settingsComponent: SectionCardSettings,
+
+ name: 'Heading',
+ groups: ['Core'],
+
+ icon: `
`
} as CardDefinition & { type: 'section' };
export const textAlignClasses: Record
= {
diff --git a/src/lib/cards/SpotifyCard/index.ts b/src/lib/cards/SpotifyCard/index.ts
index 9db2828..873347e 100644
--- a/src/lib/cards/SpotifyCard/index.ts
+++ b/src/lib/cards/SpotifyCard/index.ts
@@ -40,7 +40,9 @@ export const SpotifyCardDefinition = {
name: 'Spotify Embed',
canResize: true,
minW: 4,
- minH: 5
+ minH: 5,
+
+ groups: ['Media']
} as CardDefinition & { type: typeof cardType };
// Match Spotify album and playlist URLs
diff --git a/src/lib/cards/StandardSiteDocumentListCard/index.ts b/src/lib/cards/StandardSiteDocumentListCard/index.ts
index f0d7fc8..191230f 100644
--- a/src/lib/cards/StandardSiteDocumentListCard/index.ts
+++ b/src/lib/cards/StandardSiteDocumentListCard/index.ts
@@ -42,5 +42,9 @@ export const StandardSiteDocumentListCardDefinition = {
return records;
},
- sidebarButtonText: 'site.standard.document list'
+ sidebarButtonText: 'site.standard.document list',
+
+ name: 'Blog Posts',
+
+ groups: ['Content']
} as CardDefinition & { type: 'site.standard.document list' };
diff --git a/src/lib/cards/StatusphereCard/index.ts b/src/lib/cards/StatusphereCard/index.ts
index 31aa6e1..2e80076 100644
--- a/src/lib/cards/StatusphereCard/index.ts
+++ b/src/lib/cards/StatusphereCard/index.ts
@@ -47,7 +47,10 @@ export const StatusphereCardDefinition = {
item.cardData.label = item.cardData.title;
}
},
- canHaveLabel: true
+ canHaveLabel: true,
+
+ name: 'Emoji',
+ groups: ['Media']
} as CardDefinition & { type: 'statusphere' };
export function emojiToNotoAnimatedWebp(emoji: string | undefined): string | undefined {
diff --git a/src/lib/cards/TealFMPlaysCard/index.ts b/src/lib/cards/TealFMPlaysCard/index.ts
index c18a511..e2d9d1f 100644
--- a/src/lib/cards/TealFMPlaysCard/index.ts
+++ b/src/lib/cards/TealFMPlaysCard/index.ts
@@ -22,5 +22,9 @@ export const TealFMPlaysCardDefinition = {
},
minW: 4,
sidebarButtonText: 'teal.fm Plays',
- canHaveLabel: true
+ canHaveLabel: true,
+
+ name: 'Teal.fm Plays',
+
+ groups: ['Media']
} as CardDefinition & { type: 'recentTealFMPlays' };
diff --git a/src/lib/cards/TextCard/index.ts b/src/lib/cards/TextCard/index.ts
index 4f8296a..ff7c4b9 100644
--- a/src/lib/cards/TextCard/index.ts
+++ b/src/lib/cards/TextCard/index.ts
@@ -14,7 +14,22 @@ export const TextCardDefinition = {
};
},
- settingsComponent: TextCardSettings
+ settingsComponent: TextCardSettings,
+
+ name: 'Text',
+
+ groups: ['Core'],
+
+ icon: ``
} as CardDefinition & { type: 'text' };
export const textAlignClasses: Record = {
diff --git a/src/lib/cards/TimerCard/index.ts b/src/lib/cards/TimerCard/index.ts
index 7bbe0b2..86726a3 100644
--- a/src/lib/cards/TimerCard/index.ts
+++ b/src/lib/cards/TimerCard/index.ts
@@ -33,5 +33,6 @@ export const TimerCardDefinition = {
allowSetColor: true,
name: 'Timer Card',
minW: 4,
- canHaveLabel: true
+ canHaveLabel: true,
+ groups: ['Utilities']
} as CardDefinition & { type: 'timer' };
diff --git a/src/lib/cards/VCardCard/index.ts b/src/lib/cards/VCardCard/index.ts
index 6632304..fc2b9ed 100644
--- a/src/lib/cards/VCardCard/index.ts
+++ b/src/lib/cards/VCardCard/index.ts
@@ -122,5 +122,6 @@ export const VCardCardDefinition = {
sidebarButtonText: 'vCard',
allowSetColor: true,
- name: 'vCard Card'
+ name: 'vCard Card',
+ groups: ['Social']
} as CardDefinition & { type: 'vcard' };
diff --git a/src/lib/cards/YoutubeVideoCard/index.ts b/src/lib/cards/YoutubeVideoCard/index.ts
index d8579cb..b2296c6 100644
--- a/src/lib/cards/YoutubeVideoCard/index.ts
+++ b/src/lib/cards/YoutubeVideoCard/index.ts
@@ -51,7 +51,16 @@ export const YoutubeCardDefinition = {
return item;
},
- name: 'Youtube Video'
+ name: 'Youtube Video',
+
+ groups: ['Media'],
+
+ icon: ``
} as CardDefinition & { type: 'youtubeVideo' };
// Thanks to eleventy-plugin-youtube-embed
diff --git a/src/lib/cards/types.ts b/src/lib/cards/types.ts
index 3143eec..0620740 100644
--- a/src/lib/cards/types.ts
+++ b/src/lib/cards/types.ts
@@ -73,4 +73,10 @@ export type CardDefinition = {
canHaveLabel?: boolean;
migrate?: (item: Item) => void;
+
+ groups?: string[];
+
+ keywords?: string[];
+
+ icon?: string;
};
diff --git a/src/lib/components/card-command/CardCommand.svelte b/src/lib/components/card-command/CardCommand.svelte
new file mode 100644
index 0000000..16fa762
--- /dev/null
+++ b/src/lib/components/card-command/CardCommand.svelte
@@ -0,0 +1,102 @@
+
+
+
+
+
+
+
+
+ Command Menu
+
+ This is the command menu. Use the arrow keys to navigate and press ⌘K to open the search
+ bar.
+
+
+
+
+
+
+ No results found.
+
+
+ {#each CardDefGroups as group, index}
+ {#if group && AllCardDefinitions.some((cardDef) => cardDef.groups?.includes(group))}
+
+
+ {group}
+
+
+ {#each AllCardDefinitions.filter( (cardDef) => cardDef.groups?.includes(group) ) as cardDef}
+ {
+ open = false;
+ onselect(cardDef);
+ }}
+ class="rounded-button data-selected:bg-accent-500/10 flex h-10 cursor-pointer items-center gap-2 rounded-xl px-3 py-2.5 text-sm outline-hidden select-none"
+ keywords={[group, cardDef.type, ...(cardDef.keywords || [])]}
+ >
+ {#if cardDef.icon}
+
+ {@html cardDef.icon}
+
+ {/if}
+ {cardDef.name}
+
+ {/each}
+
+
+ {#if index < CardDefGroups.length - 1}
+
+ {/if}
+ {/if}
+ {/each}
+
+
+
+
+
+
diff --git a/src/lib/website/EditBar.svelte b/src/lib/website/EditBar.svelte
index e07681b..e650629 100644
--- a/src/lib/website/EditBar.svelte
+++ b/src/lib/website/EditBar.svelte
@@ -17,7 +17,9 @@
save,
handleImageInputChange,
- handleVideoInputChange
+ handleVideoInputChange,
+
+ showCardCommand
}: {
data: WebsiteData;
linkValue: string;
@@ -33,6 +35,8 @@
handleImageInputChange: (evt: Event) => void;
handleVideoInputChange: (evt: Event) => void;
+
+ showCardCommand: () => void;
} = $props();
let linkPopoverOpen = $state(false);
@@ -59,6 +63,7 @@
accept="image/*"
onchange={handleImageInputChange}
class="hidden"
+ id="image-input"
multiple
bind:this={imageInputRef}
/>
@@ -220,7 +225,7 @@
{/if}
-
- {
showCardCommand = true;
+ }}
{selectedCard}
{isMobile}
{isCoarse}
--
2.51.2
From fc5edb07e849206db03b8a5398acdc4335bd31e5 Mon Sep 17 00:00:00 2001
From: Florian <45694132+flo-bit@users.noreply.github.com>
Date: Sat, 31 Jan 2026 20:09:13 +0100
Subject: [PATCH 07/21] improve editing
---
src/lib/cards/BaseCard/BaseEditingCard.svelte | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/lib/cards/BaseCard/BaseEditingCard.svelte b/src/lib/cards/BaseCard/BaseEditingCard.svelte
index 55dac42..aa77b30 100644
--- a/src/lib/cards/BaseCard/BaseEditingCard.svelte
+++ b/src/lib/cards/BaseCard/BaseEditingCard.svelte
@@ -194,12 +194,12 @@
]}
{...rest}
>
- {#if !item.cardData?.locked}
+ {#if isCoarse?.() ? !isSelected : !item.cardData?.locked}
{
if (isCoarse?.()) {
e.stopPropagation();
--
2.51.2
From 3b043fb734d59e6d03bd151811d2539a97624d4f Mon Sep 17 00:00:00 2001
From: polijn
Date: Sat, 31 Jan 2026 20:51:19 +0100
Subject: [PATCH 08/21] safari drag and drop fix
---
src/lib/website/EditableWebsite.svelte | 25 ++++++++++---------------
1 file changed, 10 insertions(+), 15 deletions(-)
diff --git a/src/lib/website/EditableWebsite.svelte b/src/lib/website/EditableWebsite.svelte
index 67d233c..31b0d46 100644
--- a/src/lib/website/EditableWebsite.svelte
+++ b/src/lib/website/EditableWebsite.svelte
@@ -809,21 +809,7 @@
}}
ondragend={async (e) => {
e.preventDefault();
- const cell = getDragXY(e);
- if (!cell) return;
-
- if (activeDragElement.item) {
- if (isMobile) {
- activeDragElement.item.mobileX = cell.x;
- activeDragElement.item.mobileY = cell.y;
- } else {
- activeDragElement.item.x = cell.x;
- activeDragElement.item.y = cell.y;
- }
-
- // Fix collisions and compact items after drag ends
- fixCollisions(items, activeDragElement.item, isMobile);
- }
+ // safari fix
activeDragElement.x = -1;
activeDragElement.y = -1;
activeDragElement.element = null;
@@ -863,6 +849,15 @@
activeDragElement.w = item.w;
activeDragElement.h = item.h;
activeDragElement.item = item;
+ // fix for div shadow during drag and drop
+ const transparent = document.createElement('div');
+ transparent.style.position = 'fixed';
+ transparent.style.top = '-1000px';
+ transparent.style.width = '1px';
+ transparent.style.height = '1px';
+ document.body.appendChild(transparent);
+ e.dataTransfer?.setDragImage(transparent, 0, 0);
+ requestAnimationFrame(() => transparent.remove());
// Store original positions of all items
activeDragElement.originalPositions = new Map();
--
2.51.2
From 84af212ccb126d2d2d20c8b06ae554a16cbb93ad Mon Sep 17 00:00:00 2001
From: polijn
Date: Sat, 31 Jan 2026 20:56:20 +0100
Subject: [PATCH 09/21] linter
---
wrangler.jsonc | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/wrangler.jsonc b/wrangler.jsonc
index 4eb5086..0ab7433 100644
--- a/wrangler.jsonc
+++ b/wrangler.jsonc
@@ -37,7 +37,7 @@
"PUBLIC_IS_SELFHOSTED": "true",
"PUBLIC_DOMAIN": "https://polijn.org",
"PUBLIC_GIPHY_API_TOKEN": "ltXijv1bkNPrEgnpJ0tIdLWXjnAeE7bL"
- },
+ }
/**
* Service Bindings (communicate between multiple Workers)
--
2.51.2
From f5a65c5918300b1993502d2409b305cf2ebe377a Mon Sep 17 00:00:00 2001
From: Florian <45694132+flo-bit@users.noreply.github.com>
Date: Sat, 31 Jan 2026 21:00:08 +0100
Subject: [PATCH 10/21] this is fine
---
src/lib/types.ts | 3 ++
src/lib/website/EditableWebsite.svelte | 35 +++++++++++-
src/lib/website/layout-mirror.ts | 73 ++++++++++++++++++++++++++
3 files changed, 110 insertions(+), 1 deletion(-)
create mode 100644 src/lib/website/layout-mirror.ts
diff --git a/src/lib/types.ts b/src/lib/types.ts
index 0cbc785..fe1166d 100644
--- a/src/lib/types.ts
+++ b/src/lib/types.ts
@@ -55,6 +55,9 @@ export type WebsiteData = {
// theme colors
accentColor?: string;
baseColor?: string;
+
+ // layout mirroring: 0/undefined=never edited, 1=desktop only, 2=mobile only, 3=both
+ editedOn?: number;
};
};
profile: AppBskyActorDefs.ProfileViewDetailed;
diff --git a/src/lib/website/EditableWebsite.svelte b/src/lib/website/EditableWebsite.svelte
index 25d242f..8b46e2d 100644
--- a/src/lib/website/EditableWebsite.svelte
+++ b/src/lib/website/EditableWebsite.svelte
@@ -39,6 +39,7 @@
import { launchConfetti } from '@foxui/visual';
import Controls from './Controls.svelte';
import CardCommand from '$lib/components/card-command/CardCommand.svelte';
+ import { shouldMirror, mirrorLayout } from './layout-mirror';
let {
data
@@ -122,6 +123,17 @@
setIsMobile(() => isMobile);
+ // svelte-ignore state_referenced_locally
+ let editedOn = $state(data.publication.preferences?.editedOn ?? 0);
+
+ function onLayoutChanged() {
+ // Set the bit for the current layout: desktop=1, mobile=2
+ editedOn = editedOn | (isMobile ? 2 : 1);
+ if (shouldMirror(editedOn)) {
+ mirrorLayout(items, isMobile);
+ }
+ }
+
const isCoarse = typeof window !== 'undefined' && window.matchMedia('(pointer: coarse)').matches;
setIsCoarse(() => isCoarse);
@@ -191,6 +203,8 @@
compactItems(items, false);
compactItems(items, true);
+ onLayoutChanged();
+
newItem = {};
await tick();
@@ -215,6 +229,10 @@
await checkAndUploadImage(data.publication, 'icon');
}
+ // Persist layout editing state
+ data.publication.preferences ??= {};
+ data.publication.preferences.editedOn = editedOn;
+
await savePage(data, items, publication);
publication = JSON.stringify(data.publication);
@@ -494,6 +512,7 @@
if (touchDragActive && activeDragElement.item) {
// Finalize position
fixCollisions(items, activeDragElement.item, isMobile);
+ onLayoutChanged();
activeDragElement.x = -1;
activeDragElement.y = -1;
@@ -645,6 +664,8 @@
compactItems(items, true);
}
+ onLayoutChanged();
+
await tick();
scrollToItem(item, isMobile, container);
@@ -780,6 +801,8 @@
compactItems(items, false);
compactItems(items, true);
+ onLayoutChanged();
+
await tick();
scrollToItem(item, isMobile, container);
@@ -1002,6 +1025,7 @@
// Fix collisions and compact items after drag ends
fixCollisions(items, activeDragElement.item, isMobile);
+ onLayoutChanged();
}
activeDragElement.x = -1;
activeDragElement.y = -1;
@@ -1024,6 +1048,7 @@
items = items.filter((it) => it !== item);
compactItems(items, false);
compactItems(items, true);
+ onLayoutChanged();
}}
onsetsize={(newW: number, newH: number) => {
if (isMobile) {
@@ -1035,6 +1060,7 @@
}
fixCollisions(items, item, isMobile);
+ onLayoutChanged();
}}
ondragstart={(e: DragEvent) => {
const target = e.currentTarget as HTMLDivElement;
@@ -1069,7 +1095,6 @@