diff --git a/src/app.css b/src/app.css index 3914088..251dffa 100644 --- a/src/app.css +++ b/src/app.css @@ -3,6 +3,8 @@ @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/ATProtoCollectionsCard.svelte b/src/lib/cards/ATProtoCollectionsCard/ATProtoCollectionsCard.svelte index 54d9cae..dfd7a36 100644 --- a/src/lib/cards/ATProtoCollectionsCard/ATProtoCollectionsCard.svelte +++ b/src/lib/cards/ATProtoCollectionsCard/ATProtoCollectionsCard.svelte @@ -39,9 +39,23 @@ {collections.length} {/if} -
- {#each collections ?? [] as collection (collection)} - - {/each} -
+ {#if collections && collections.length > 0} +
+ {#each collections as collection (collection)} + + {/each} +
+ {:else if collections} +
+ No collections found. +
+ {:else} +
+ Loading collections... +
+ {/if} diff --git a/src/lib/cards/ATProtoCollectionsCard/index.ts b/src/lib/cards/ATProtoCollectionsCard/index.ts index 95a06a8..399da68 100644 --- a/src/lib/cards/ATProtoCollectionsCard/index.ts +++ b/src/lib/cards/ATProtoCollectionsCard/index.ts @@ -19,5 +19,10 @@ export const ATProtoCollectionsCardDefinition = { item.w = 4; item.mobileW = 8; }, - sidebarButtonText: 'Atmosphere Collections' + sidebarButtonText: 'Atmosphere Collections', + + name: 'ATProto Collections', + + groups: ['Social'], + icon: `` } as CardDefinition & { type: 'atprotocollections' }; diff --git a/src/lib/cards/BigSocialCard/index.ts b/src/lib/cards/BigSocialCard/index.ts index d0657bf..14821d1 100644 --- a/src/lib/cards/BigSocialCard/index.ts +++ b/src/lib/cards/BigSocialCard/index.ts @@ -51,7 +51,10 @@ export const BigSocialCardDefinition = { return item; }, urlHandlerPriority: 1, - canHaveLabel: true + canHaveLabel: true, + + groups: ['Social'], + icon: `` } as CardDefinition & { type: 'bigsocial' }; import { diff --git a/src/lib/cards/BlueskyMediaCard/index.ts b/src/lib/cards/BlueskyMediaCard/index.ts index b3411a2..86165e3 100644 --- a/src/lib/cards/BlueskyMediaCard/index.ts +++ b/src/lib/cards/BlueskyMediaCard/index.ts @@ -8,5 +8,10 @@ export const BlueskyMediaCardDefinition = { createNew: () => {}, creationModalComponent: CreateBlueskyMediaCardModal, sidebarButtonText: 'Bluesky Media', - canHaveLabel: true + canHaveLabel: true, + + groups: ['Media'], + + name: 'Video/Image from Bluesky', + icon: `` } as CardDefinition & { type: 'blueskyMedia' }; diff --git a/src/lib/cards/BlueskyPostCard/index.ts b/src/lib/cards/BlueskyPostCard/index.ts index aae6e61..6e1f85e 100644 --- a/src/lib/cards/BlueskyPostCard/index.ts +++ b/src/lib/cards/BlueskyPostCard/index.ts @@ -63,5 +63,8 @@ export const BlueskyPostCardDefinition = { return postsMap; }, minW: 4, - name: 'Bluesky Post' + name: 'Bluesky Post', + + groups: ['Social'], + icon: `` } as CardDefinition & { type: 'blueskyPost' }; diff --git a/src/lib/cards/ButtonCard/index.ts b/src/lib/cards/ButtonCard/index.ts index e5c0080..bc818f2 100644 --- a/src/lib/cards/ButtonCard/index.ts +++ b/src/lib/cards/ButtonCard/index.ts @@ -27,5 +27,9 @@ export const ButtonCardDefinition: CardDefinition = { minW: 2, minH: 1, maxW: 8, - maxH: 4 + maxH: 4, + + groups: ['Utilities'], + name: 'Button', + icon: `` }; diff --git a/src/lib/cards/ClockCard/ClockCard.svelte b/src/lib/cards/ClockCard/ClockCard.svelte new file mode 100644 index 0000000..5a79280 --- /dev/null +++ b/src/lib/cards/ClockCard/ClockCard.svelte @@ -0,0 +1,87 @@ + + +
+ +
+ + : + + : + +
+
+ {#if timezoneDisplay} +
+ {timezoneDisplay} +
+ {/if} +
diff --git a/src/lib/cards/ClockCard/ClockCardSettings.svelte b/src/lib/cards/ClockCard/ClockCardSettings.svelte new file mode 100644 index 0000000..0101adc --- /dev/null +++ b/src/lib/cards/ClockCard/ClockCardSettings.svelte @@ -0,0 +1,74 @@ + + +
+
+ +
+ + +
+
+
diff --git a/src/lib/cards/ClockCard/index.ts b/src/lib/cards/ClockCard/index.ts new file mode 100644 index 0000000..b02a42c --- /dev/null +++ b/src/lib/cards/ClockCard/index.ts @@ -0,0 +1,31 @@ +import type { CardDefinition } from '../types'; +import ClockCard from './ClockCard.svelte'; +import ClockCardSettings from './ClockCardSettings.svelte'; + +export type ClockCardData = { + timezone?: string; +}; + +export const ClockCardDefinition = { + type: 'clock', + contentComponent: ClockCard, + settingsComponent: ClockCardSettings, + + createNew: (card) => { + card.w = 4; + card.h = 2; + card.mobileW = 8; + card.mobileH = 3; + card.cardData = { + timezone: Intl.DateTimeFormat().resolvedOptions().timeZone + } as ClockCardData; + }, + + allowSetColor: true, + name: 'Clock', + minW: 4, + canHaveLabel: true, + groups: ['Utilities'], + keywords: ['time', 'timezone', 'watch'], + icon: `` +} as CardDefinition & { type: 'clock' }; diff --git a/src/lib/cards/CountdownCard/CountdownCard.svelte b/src/lib/cards/CountdownCard/CountdownCard.svelte new file mode 100644 index 0000000..248461d --- /dev/null +++ b/src/lib/cards/CountdownCard/CountdownCard.svelte @@ -0,0 +1,185 @@ + + +
+ {#if isEventPast && elapsedDiff !== null} + + +
+ {#if elapsedYears > 0} +
+ + {elapsedYears === 1 ? 'year' : 'years'} +
+ {/if} + {#if elapsedYears > 0 || elapsedDays > 0} +
+ + {elapsedDays === 1 ? 'day' : 'days'} +
+ {/if} +
+ + hrs +
+
+ + min +
+
+ + sec +
+
+
+ {:else if eventDiff !== null} + + +
+ {#if eventDays > 0} +
+ + {eventDays === 1 ? 'day' : 'days'} +
+ {/if} +
+ + hrs +
+
+ + min +
+
+ + sec +
+
+
+ {:else} +
Set a target date in settings
+ {/if} +
diff --git a/src/lib/cards/CountdownCard/CountdownCardSettings.svelte b/src/lib/cards/CountdownCard/CountdownCardSettings.svelte new file mode 100644 index 0000000..ecc5150 --- /dev/null +++ b/src/lib/cards/CountdownCard/CountdownCardSettings.svelte @@ -0,0 +1,44 @@ + + +
+
+ +
+ updateTargetDate(e.currentTarget.value, targetTimeValue)} + class="flex-1" + /> + updateTargetDate(targetDateValue, e.currentTarget.value)} + class="w-28" + /> +
+
+
diff --git a/src/lib/cards/CountdownCard/index.ts b/src/lib/cards/CountdownCard/index.ts new file mode 100644 index 0000000..21f0625 --- /dev/null +++ b/src/lib/cards/CountdownCard/index.ts @@ -0,0 +1,29 @@ +import type { CardDefinition } from '../types'; +import CountdownCard from './CountdownCard.svelte'; +import CountdownCardSettings from './CountdownCardSettings.svelte'; + +export type CountdownCardData = { + targetDate?: string; +}; + +export const CountdownCardDefinition = { + type: 'countdown', + contentComponent: CountdownCard, + settingsComponent: CountdownCardSettings, + + createNew: (card) => { + card.w = 4; + card.h = 2; + card.mobileW = 8; + card.mobileH = 3; + card.cardData = {} as CountdownCardData; + }, + + allowSetColor: true, + name: 'Countdown', + minW: 4, + canHaveLabel: true, + groups: ['Utilities'], + keywords: ['timer', 'event', 'date', 'countdown'], + icon: `` +} as CardDefinition & { type: 'countdown' }; diff --git a/src/lib/cards/DrawCard/index.ts b/src/lib/cards/DrawCard/index.ts index 29d99dd..1967745 100644 --- a/src/lib/cards/DrawCard/index.ts +++ b/src/lib/cards/DrawCard/index.ts @@ -23,5 +23,8 @@ export const DrawCardDefinition = { strokeWidth: 1, locked: true }; - } + }, + + groups: ['Visual'], + icon: `` } as CardDefinition & { type: 'draw' }; diff --git a/src/lib/cards/EmbedCard/index.ts b/src/lib/cards/EmbedCard/index.ts index 4bc8c95..1a2a6c8 100644 --- a/src/lib/cards/EmbedCard/index.ts +++ b/src/lib/cards/EmbedCard/index.ts @@ -19,5 +19,7 @@ export const EmbedCardDefinition = { // change: (item) => { // return item; // }, - name: 'Embed Card' + name: 'Embed', + groups: ['Media'], + icon: `` } as CardDefinition & { type: 'embed' }; diff --git a/src/lib/cards/EventCard/index.ts b/src/lib/cards/EventCard/index.ts index 5d03aca..368a565 100644 --- a/src/lib/cards/EventCard/index.ts +++ b/src/lib/cards/EventCard/index.ts @@ -112,5 +112,8 @@ export const EventCardDefinition = { urlHandlerPriority: 5, - name: 'Event Card' + name: 'Event', + + groups: ['Social'], + icon: `` } as CardDefinition & { type: 'event' }; diff --git a/src/lib/cards/FluidTextCard/index.ts b/src/lib/cards/FluidTextCard/index.ts index 666e89f..0bbca14 100644 --- a/src/lib/cards/FluidTextCard/index.ts +++ b/src/lib/cards/FluidTextCard/index.ts @@ -23,5 +23,9 @@ export const FluidTextCardDefinition = { sidebarButtonText: 'Fluid Text', defaultColor: 'transparent', allowSetColor: true, - minW: 2 + minW: 2, + + groups: ['Visual'], + name: 'Fluid Text', + icon: `` } as CardDefinition & { type: 'fluid-text' }; diff --git a/src/lib/cards/GIFCard/index.ts b/src/lib/cards/GIFCard/index.ts index 21e9cf1..a8d6b95 100644 --- a/src/lib/cards/GIFCard/index.ts +++ b/src/lib/cards/GIFCard/index.ts @@ -45,5 +45,8 @@ export const GifCardDefinition = { return null; }, urlHandlerPriority: 5, - name: 'GIF' + name: 'GIF', + + groups: ['Media'], + icon: `` } as CardDefinition & { type: 'gif' }; diff --git a/src/lib/cards/GameCards/DinoGameCard/index.ts b/src/lib/cards/GameCards/DinoGameCard/index.ts index e037b5c..339de0f 100644 --- a/src/lib/cards/GameCards/DinoGameCard/index.ts +++ b/src/lib/cards/GameCards/DinoGameCard/index.ts @@ -14,5 +14,9 @@ export const DinoGameCardDefinition = { card.mobileH = 6; card.cardData = {}; }, - canHaveLabel: true + canHaveLabel: true, + + groups: ['Games'], + name: 'Dino Game', + icon: `` } 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..7c14731 100644 --- a/src/lib/cards/GameCards/TetrisCard/index.ts +++ b/src/lib/cards/GameCards/TetrisCard/index.ts @@ -19,5 +19,10 @@ export const TetrisCardDefinition = { card.cardData = {}; }, maxH: 10, - canHaveLabel: true + canHaveLabel: true, + + groups: ['Games'], + + name: 'Tetris', + icon: `` } as CardDefinition & { type: 'tetris' }; diff --git a/src/lib/cards/GitHubProfileCard/CreateGitHubProfileCardModal.svelte b/src/lib/cards/GitHubProfileCard/CreateGitHubProfileCardModal.svelte new file mode 100644 index 0000000..b0d27d3 --- /dev/null +++ b/src/lib/cards/GitHubProfileCard/CreateGitHubProfileCardModal.svelte @@ -0,0 +1,70 @@ + + + +
{ + let input = item.cardData.href?.trim(); + if (!input) return; + + let username: string | undefined; + + // Try parsing as URL first + try { + const parsed = new URL(input); + if (/^(www\.)?github\.com$/.test(parsed.hostname)) { + const segments = parsed.pathname.split('/').filter(Boolean); + if ( + segments.length === 1 && + /^[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,37}[a-zA-Z0-9])?$/.test(segments[0]) + ) { + username = segments[0]; + } + } + } catch { + // Not a URL, try as plain username + if (/^[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,37}[a-zA-Z0-9])?$/.test(input)) { + username = input; + } + } + + if (!username) { + errorMessage = 'Please enter a valid GitHub username or profile URL'; + return; + } + + item.cardData.user = username; + item.cardData.href = `https://github.com/${username}`; + + item.w = 6; + item.mobileW = 8; + item.h = 3; + item.mobileH = 6; + + oncreate?.(); + }} + class="flex flex-col gap-2" + > + Enter a GitHub username or profile URL + + + {#if errorMessage} +

{errorMessage}

+ {/if} + +
+ + +
+
+
diff --git a/src/lib/cards/GitHubProfileCard/index.ts b/src/lib/cards/GitHubProfileCard/index.ts index af7ccc8..190400b 100644 --- a/src/lib/cards/GitHubProfileCard/index.ts +++ b/src/lib/cards/GitHubProfileCard/index.ts @@ -1,4 +1,5 @@ import type { CardDefinition } from '../types'; +import CreateGitHubProfileCardModal from './CreateGitHubProfileCardModal.svelte'; import type GithubContributionsGraph from './GithubContributionsGraph.svelte'; import GitHubProfileCard from './GitHubProfileCard.svelte'; import type { GitHubContributionsData } from './types'; @@ -8,6 +9,7 @@ export type GithubProfileLoadedData = Record { const githubData: Record = {}; @@ -50,7 +52,10 @@ export const GithubProfileCardDefitition = { return item; }, - name: 'Github Profile' + name: 'Github Profile', + + groups: ['Social'], + icon: `` } 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..8799f9b 100644 --- a/src/lib/cards/GuestbookCard/index.ts +++ b/src/lib/cards/GuestbookCard/index.ts @@ -60,5 +60,7 @@ export const GuestbookCardDefinition = { return results; }, - name: 'Guestbook' + name: 'Guestbook', + groups: ['Social'], + icon: `` } 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..ab5e796 100644 --- a/src/lib/cards/LatestBlueskyPostCard/index.ts +++ b/src/lib/cards/LatestBlueskyPostCard/index.ts @@ -18,5 +18,10 @@ export const LatestBlueskyPostCardDefinition = { return JSON.parse(JSON.stringify(authorFeed)); }, - minW: 4 + minW: 4, + + name: 'Latest Bluesky Post', + + groups: ['Social'], + icon: `` } 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 @@ + + + +
{ + if (!item.cardData.href.trim()) return; + + let link = validateLink(item.cardData.href); + if (!link) { + errorMessage = 'Invalid link'; + return; + } + + item.cardData.href = link; + item.cardData.domain = new URL(link).hostname; + item.cardData.hasFetched = false; + + oncreate?.(); + }} + class="flex flex-col gap-2" + > + Enter a link + + + {#if errorMessage} +

{errorMessage}

+ {/if} + +
+ + +
+
+
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..4835102 100644 --- a/src/lib/cards/LivestreamCard/index.ts +++ b/src/lib/cards/LivestreamCard/index.ts @@ -81,7 +81,9 @@ export const LivestreamCardDefitition = { urlHandlerPriority: 5, - name: 'stream.place Card' + name: 'Latest Livestream (stream.place)', + groups: ['Media'], + icon: `` } 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/PopfeedReviewsCard.svelte b/src/lib/cards/PopfeedReviews/PopfeedReviewsCard.svelte index 89c6cb9..32924b0 100644 --- a/src/lib/cards/PopfeedReviews/PopfeedReviewsCard.svelte +++ b/src/lib/cards/PopfeedReviews/PopfeedReviewsCard.svelte @@ -30,30 +30,44 @@
- {#each feed ?? [] as review (review.uri)} - {#if review.value.rating !== undefined && review.value.posterUrl} - - - - {/if} - {/each} + class="relative flex aspect-[2/3] h-full flex-col items-center justify-end overflow-hidden rounded-xl p-1" + > + + +
+ + +
+ + {/if} + {/each} + {:else if feed} +
+ No reviews yet. +
+ {:else} +
+ Loading reviews... +
+ {/if} diff --git a/src/lib/cards/PopfeedReviews/index.ts b/src/lib/cards/PopfeedReviews/index.ts index 6888e0f..eef90bc 100644 --- a/src/lib/cards/PopfeedReviews/index.ts +++ b/src/lib/cards/PopfeedReviews/index.ts @@ -18,5 +18,9 @@ export const PopfeedReviewsCardDefinition = { }, minH: 3, sidebarButtonText: 'Popfeed Reviews', - canHaveLabel: true + canHaveLabel: true, + + groups: ['Media'], + name: 'Movie and TV Reviews', + icon: `` } 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..1c09753 100644 --- a/src/lib/cards/SpotifyCard/index.ts +++ b/src/lib/cards/SpotifyCard/index.ts @@ -40,7 +40,10 @@ export const SpotifyCardDefinition = { name: 'Spotify Embed', canResize: true, minW: 4, - minH: 5 + minH: 5, + + groups: ['Media'], + icon: `` } as CardDefinition & { type: typeof cardType }; // Match Spotify album and playlist URLs diff --git a/src/lib/cards/StandardSiteDocumentListCard/StandardSiteDocumentListCard.svelte b/src/lib/cards/StandardSiteDocumentListCard/StandardSiteDocumentListCard.svelte index a1070d3..8cdcfe5 100644 --- a/src/lib/cards/StandardSiteDocumentListCard/StandardSiteDocumentListCard.svelte +++ b/src/lib/cards/StandardSiteDocumentListCard/StandardSiteDocumentListCard.svelte @@ -27,12 +27,38 @@
- {#each feed ?? [] as document (document.uri)} - - {/each} + {#if feed && feed.length > 0} + {#each feed as document (document.uri)} + + {/each} + {:else if feed} +
+ No blog posts found. + + Create some on Leaflet + or + Pckt + +
+ {:else} +
+ Loading blog posts... +
+ {/if}
diff --git a/src/lib/cards/StandardSiteDocumentListCard/index.ts b/src/lib/cards/StandardSiteDocumentListCard/index.ts index f0d7fc8..a18bac1 100644 --- a/src/lib/cards/StandardSiteDocumentListCard/index.ts +++ b/src/lib/cards/StandardSiteDocumentListCard/index.ts @@ -42,5 +42,10 @@ export const StandardSiteDocumentListCardDefinition = { return records; }, - sidebarButtonText: 'site.standard.document list' + sidebarButtonText: 'site.standard.document list', + + name: 'Blog Posts', + + groups: ['Content'], + icon: `` } 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..5520ba1 100644 --- a/src/lib/cards/StatusphereCard/index.ts +++ b/src/lib/cards/StatusphereCard/index.ts @@ -47,7 +47,11 @@ export const StatusphereCardDefinition = { item.cardData.label = item.cardData.title; } }, - canHaveLabel: true + canHaveLabel: true, + + name: 'Emoji', + groups: ['Media'], + icon: `` } as CardDefinition & { type: 'statusphere' }; export function emojiToNotoAnimatedWebp(emoji: string | undefined): string | undefined { diff --git a/src/lib/cards/TealFMPlaysCard/TealFMPlaysCard.svelte b/src/lib/cards/TealFMPlaysCard/TealFMPlaysCard.svelte index 4066169..610a352 100644 --- a/src/lib/cards/TealFMPlaysCard/TealFMPlaysCard.svelte +++ b/src/lib/cards/TealFMPlaysCard/TealFMPlaysCard.svelte @@ -85,13 +85,27 @@ {/snippet}
- {#each feed ?? [] as play (play.uri)} - {#if play.value.originUrl} - + {#if feed && feed.length > 0} + {#each feed as play (play.uri)} + {#if play.value.originUrl} + + {@render musicItem(play)} + + {:else} {@render musicItem(play)} - - {:else} - {@render musicItem(play)} - {/if} - {/each} + {/if} + {/each} + {:else if feed} +
+ No recent plays found. +
+ {:else} +
+ Loading plays... +
+ {/if}
diff --git a/src/lib/cards/TealFMPlaysCard/index.ts b/src/lib/cards/TealFMPlaysCard/index.ts index c18a511..79955e0 100644 --- a/src/lib/cards/TealFMPlaysCard/index.ts +++ b/src/lib/cards/TealFMPlaysCard/index.ts @@ -22,5 +22,10 @@ export const TealFMPlaysCardDefinition = { }, minW: 4, sidebarButtonText: 'teal.fm Plays', - canHaveLabel: true + canHaveLabel: true, + + name: 'Teal.fm Plays', + + groups: ['Media'], + icon: `` } 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..0cdc7c0 100644 --- a/src/lib/cards/TimerCard/index.ts +++ b/src/lib/cards/TimerCard/index.ts @@ -17,7 +17,6 @@ export const TimerCardDefinition = { type: 'timer', contentComponent: TimerCard, settingsComponent: TimerCardSettings, - sidebarButtonText: 'Timer', createNew: (card) => { card.w = 4; @@ -31,7 +30,20 @@ export const TimerCardDefinition = { }, allowSetColor: true, - name: 'Timer Card', minW: 4, - canHaveLabel: true + canHaveLabel: true, + + migrate: (item) => { + const data = item.cardData as TimerCardData; + if (data.mode === 'event') { + item.cardType = 'countdown'; + item.cardData = { targetDate: data.targetDate }; + } else { + item.cardType = 'clock'; + item.cardData = { timezone: data.timezone }; + } + if (data.label) { + item.cardData.label = data.label; + } + } } as CardDefinition & { type: 'timer' }; diff --git a/src/lib/cards/VCardCard/index.ts b/src/lib/cards/VCardCard/index.ts index 6632304..fdd2065 100644 --- a/src/lib/cards/VCardCard/index.ts +++ b/src/lib/cards/VCardCard/index.ts @@ -122,5 +122,7 @@ export const VCardCardDefinition = { sidebarButtonText: 'vCard', allowSetColor: true, - name: 'vCard Card' + name: 'vCard Card', + groups: ['Social'], + icon: `` } as CardDefinition & { type: 'vcard' }; diff --git a/src/lib/cards/VideoCard/index.ts b/src/lib/cards/VideoCard/index.ts index c969ec1..2138edb 100644 --- a/src/lib/cards/VideoCard/index.ts +++ b/src/lib/cards/VideoCard/index.ts @@ -59,5 +59,7 @@ export const VideoCardDefinition = { }, settingsComponent: VideoCardSettings, - name: 'Video Card' + name: 'Video', + groups: ['Media'], + icon: `` } as CardDefinition & { type: 'video' }; diff --git a/src/lib/cards/YoutubeVideoCard/CreateYoutubeCardModal.svelte b/src/lib/cards/YoutubeVideoCard/CreateYoutubeCardModal.svelte new file mode 100644 index 0000000..c677f8e --- /dev/null +++ b/src/lib/cards/YoutubeVideoCard/CreateYoutubeCardModal.svelte @@ -0,0 +1,52 @@ + + + +
{ + const url = item.cardData.href?.trim(); + if (!url) return; + + const id = matcher(url); + if (!id) { + errorMessage = 'Please enter a valid YouTube URL'; + return; + } + + item.cardData.youtubeId = id; + item.cardData.poster = `https://i.ytimg.com/vi/${id}/hqdefault.jpg`; + item.cardData.showInline = true; + + item.w = 4; + item.mobileW = 8; + item.h = 3; + item.mobileH = 5; + + oncreate?.(); + }} + class="flex flex-col gap-2" + > + Enter a YouTube URL + + + {#if errorMessage} +

{errorMessage}

+ {/if} + +
+ + +
+
+
diff --git a/src/lib/cards/YoutubeVideoCard/index.ts b/src/lib/cards/YoutubeVideoCard/index.ts index d8579cb..5bd3f01 100644 --- a/src/lib/cards/YoutubeVideoCard/index.ts +++ b/src/lib/cards/YoutubeVideoCard/index.ts @@ -1,4 +1,5 @@ import type { CardDefinition } from '../types'; +import CreateYoutubeCardModal from './CreateYoutubeCardModal.svelte'; import YoutubeCard from './YoutubeCard.svelte'; import YoutubeCardSettings from './YoutubeCardSettings.svelte'; @@ -6,6 +7,7 @@ export const YoutubeCardDefinition = { type: 'youtubeVideo', contentComponent: YoutubeCard, settingsComponent: YoutubeCardSettings, + creationModalComponent: CreateYoutubeCardModal, createNew: (card) => { card.cardType = 'youtubeVideo'; card.cardData = {}; @@ -51,7 +53,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/index.ts b/src/lib/cards/index.ts index cdf8e14..a95ce54 100644 --- a/src/lib/cards/index.ts +++ b/src/lib/cards/index.ts @@ -30,6 +30,8 @@ import { EventCardDefinition } from './EventCard'; import { VCardCardDefinition } from './VCardCard'; import { DrawCardDefinition } from './DrawCard'; import { TimerCardDefinition } from './TimerCard'; +import { ClockCardDefinition } from './ClockCard'; +import { CountdownCardDefinition } from './CountdownCard'; import { SpotifyCardDefinition } from './SpotifyCard'; import { ButtonCardDefinition } from './ButtonCard'; import { GuestbookCardDefinition } from './GuestbookCard'; @@ -69,6 +71,8 @@ export const AllCardDefinitions = [ VCardCardDefinition, DrawCardDefinition, TimerCardDefinition, + ClockCardDefinition, + CountdownCardDefinition, SpotifyCardDefinition // Model3DCardDefinition ] as const; 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..71d94a7 --- /dev/null +++ b/src/lib/components/card-command/CardCommand.svelte @@ -0,0 +1,192 @@ + + + + + + + + + Command Menu + + This is the command menu. Use the arrow keys to navigate and press ⌘K to open the search + bar. + + + { + searchValue = e.currentTarget.value; + }} + /> + + + + + No results found. + + + {#if urlMatchingCards.length > 0} + + + Add from link + + + {#each urlMatchingCards as cardDef (cardDef.type)} + { + selectUrl(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" + > + {#if cardDef.icon} +
+ {@html cardDef.icon} +
+ {/if} + {cardDef.name} +
+ {/each} +
+
+ + {/if} + + {#each CardDefGroups as group, index (group)} + {#if group && AllCardDefinitions.some((cardDef) => cardDef.groups?.includes(group))} + + + {group} + + + {#each AllCardDefinitions.filter( (cardDef) => cardDef.groups?.includes(group) ) as cardDef (cardDef.type)} + { + open = false; + searchValue = ''; + 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..a862e3a 100644 --- a/src/lib/website/EditBar.svelte +++ b/src/lib/website/EditBar.svelte @@ -2,13 +2,10 @@ import { dev } from '$app/environment'; import { user } from '$lib/atproto'; import type { WebsiteData } from '$lib/types'; - import { Button, Input, Navbar, Popover, Toggle, toast } from '@foxui/core'; + import { Button, Navbar, Toggle, toast } from '@foxui/core'; let { data, - linkValue = $bindable(), - newCard, - addLink, showingMobileView = $bindable(), isSaving = $bindable(), @@ -16,13 +13,9 @@ save, - handleImageInputChange, - handleVideoInputChange + showCardCommand }: { data: WebsiteData; - linkValue: string; - newCard: (type: string) => void; - addLink: (url: string) => void; showingMobileView: boolean; @@ -31,15 +24,9 @@ save: () => Promise; - handleImageInputChange: (evt: Event) => void; - handleVideoInputChange: (evt: Event) => void; + showCardCommand: () => void; } = $props(); - let linkPopoverOpen = $state(false); - - let imageInputRef: HTMLInputElement | undefined = $state(); - let videoInputRef: HTMLInputElement | undefined = $state(); - function getShareUrl() { const base = typeof window !== 'undefined' ? window.location.origin : ''; const pagePath = @@ -54,24 +41,6 @@ } - - - - {#if dev || (user.isLoggedIn && user.profile?.did === data.did)}
- - - - - - {#snippet child({ props })} - - {/snippet} - { - if (event.code === 'Enter') { - addLink(linkValue); - event.preventDefault(); - } - }} - placeholder="Enter link" - /> - - - - - - {#if dev} - - {/if} - -
+ + + + { + showCardCommand = true; + }} /> -- 2.51.2 From 363e250a1ab65936a40eec5497d6fafa70f1e065 Mon Sep 17 00:00:00 2001 From: Florian <45694132+flo-bit@users.noreply.github.com> Date: Sat, 31 Jan 2026 21:40:06 +0100 Subject: [PATCH 2/2] more layout stuff --- src/lib/cards/SectionCard/index.ts | 1 + src/lib/website/layout-mirror.ts | 65 ++++++++++++++++-------------- 2 files changed, 36 insertions(+), 30 deletions(-) diff --git a/src/lib/cards/SectionCard/index.ts b/src/lib/cards/SectionCard/index.ts index c3128df..2bb2097 100644 --- a/src/lib/cards/SectionCard/index.ts +++ b/src/lib/cards/SectionCard/index.ts @@ -24,6 +24,7 @@ export const SectionCardDefinition = { }, defaultColor: 'transparent', + minW: COLUMNS, maxH: 1, canResize: false, settingsComponent: SectionCardSettings, diff --git a/src/lib/website/layout-mirror.ts b/src/lib/website/layout-mirror.ts index 5a2c5c9..afd025b 100644 --- a/src/lib/website/layout-mirror.ts +++ b/src/lib/website/layout-mirror.ts @@ -1,6 +1,6 @@ import { COLUMNS } from '$lib'; import { CardDefinitionsByType } from '$lib/cards'; -import { clamp, fixAllCollisions } from '$lib/helper'; +import { clamp, findValidPosition, fixAllCollisions } from '$lib/helper'; import type { Item } from '$lib/types'; /** @@ -23,28 +23,17 @@ function snapEven(v: number): number { */ export function mirrorItemSize(item: Item, fromMobile: boolean): void { const def = CardDefinitionsByType[item.cardType]; - const minW = def?.minW ?? 2; - const maxW = def?.maxW ?? COLUMNS; - const minH = def?.minH ?? 2; - const maxH = def?.maxH ?? Infinity; if (fromMobile) { - const srcW = item.mobileW; - const srcH = item.mobileH; - // Full-width cards stay full-width - item.w = srcW >= COLUMNS ? COLUMNS : clamp(snapEven(srcW / 2), minW, maxW); - item.h = clamp(snapEven((srcH * item.w) / srcW), minH, maxH); + // Mobile → Desktop: halve both dimensions, then clamp to card def constraints + // (constraints are in desktop units) + item.w = clamp(snapEven(item.mobileW / 2), def?.minW ?? 2, def?.maxW ?? COLUMNS); + item.h = clamp(Math.round(item.mobileH / 2), def?.minH ?? 1, def?.maxH ?? Infinity); } else { - const srcW = item.w; - const srcH = item.h; - // Full-width cards stay full-width - if (srcW >= COLUMNS) { - item.mobileW = clamp(COLUMNS, minW, Math.min(maxW, COLUMNS)); - } else { - const scaleFactor = Math.min(2, COLUMNS / srcW); - item.mobileW = clamp(snapEven(srcW * scaleFactor), minW, Math.min(maxW, COLUMNS)); - } - item.mobileH = clamp(snapEven((srcH * item.mobileW) / srcW), minH, maxH); + // Desktop → Mobile: double both dimensions + // (don't apply card def constraints — they're in desktop units) + item.mobileW = Math.min(item.w * 2, COLUMNS); + item.mobileH = Math.max(item.h * 2, 2); } } @@ -54,20 +43,36 @@ export function mirrorItemSize(item: Item, fromMobile: boolean): void { * Mutates items in-place. */ export function mirrorLayout(items: Item[], fromMobile: boolean): void { + // Mirror sizes first for (const item of items) { mirrorItemSize(item, fromMobile); + } + + if (fromMobile) { + // Mobile → Desktop: reflow items to use the full grid width. + // Sort by mobile position so items are placed in reading order. + const sorted = items.toSorted( + (a, b) => a.mobileY - b.mobileY || a.mobileX - b.mobileX + ); - if (fromMobile) { - // Mobile → Desktop positions - item.x = clamp(Math.floor(item.mobileX / 2 / 2) * 2, 0, COLUMNS - item.w); - item.y = Math.max(0, Math.round(item.mobileY / 2)); - } else { - // Desktop → Mobile positions - item.mobileX = clamp(Math.floor((item.x * 2) / 2) * 2, 0, COLUMNS - item.mobileW); + // Place each item into the first available spot on the desktop grid + const placed: Item[] = []; + for (const item of sorted) { + item.x = 0; + item.y = 0; + findValidPosition(item, placed, false); + placed.push(item); + } + } else { + // Desktop → Mobile: proportional positions + for (const item of items) { + item.mobileX = clamp( + Math.floor((item.x * 2) / 2) * 2, + 0, + COLUMNS - item.mobileW + ); item.mobileY = Math.max(0, Math.round(item.y * 2)); } + fixAllCollisions(items, true); } - - // Resolve collisions on the target layout - fixAllCollisions(items, !fromMobile); }