your personal website on atproto - mirror blento.app

add card command bar

Florian 5f504652 3cdc9f4b

+371 -76
-42
docs/Beta.md
··· 1 - # Todo for beta version 2 - 3 - - site.standard 4 - - move description to markdownDescription and set description as text only 5 - 6 - - allow editing on mobile 7 - 8 - - get automatic layout for mobile if only edited on desktop (and vice versa) 9 - 10 - - add cards in middle of current position (both mobile and desktop version) 11 - 12 - - show nsfw warnings 13 - 14 - - card with big call to action button "create your blento" 15 - 16 - - ask to fill with some default cards on page creation 17 - 18 - - when adding images try to add them in a size that best fits aspect ratio 19 - 20 - - onboarding? 21 - 22 - - switch sidebar to a quick list of available cards with search function 23 - 24 - - test 25 - - selfhosting 26 - 27 - - guestbook card 28 - 29 - - onboarding? 30 - 31 - - switch sidebar to a quick list of available cards with search function 32 - 33 - - test 34 - - selfhosting 35 - 36 - - guestbook card 37 - 38 - - analytics? 39 - 40 - - refresh recently updated blentos (move to top of list, update profiles every 24 hours) 41 - 42 - - server side oauth?
+1
package.json
··· 85 85 "svelte-sonner": "^1.0.7", 86 86 "tailwind-merge": "^3.4.0", 87 87 "tailwind-variants": "^3.2.2", 88 + "tailwindcss-animate": "^1.0.7", 88 89 "three": "^0.176.0", 89 90 "turndown": "^7.2.2", 90 91 "wrangler": "^4.60.0"
+12
pnpm-lock.yaml
··· 146 146 tailwind-variants: 147 147 specifier: ^3.2.2 148 148 version: 3.2.2(tailwind-merge@3.4.0)(tailwindcss@4.1.18) 149 + tailwindcss-animate: 150 + specifier: ^1.0.7 151 + version: 1.0.7(tailwindcss@4.1.18) 149 152 three: 150 153 specifier: ^0.176.0 151 154 version: 0.176.0 ··· 2799 2802 peerDependenciesMeta: 2800 2803 tailwind-merge: 2801 2804 optional: true 2805 + 2806 + tailwindcss-animate@1.0.7: 2807 + resolution: {integrity: sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==, tarball: https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz} 2808 + peerDependencies: 2809 + tailwindcss: '>=3.0.0 || insiders' 2802 2810 2803 2811 tailwindcss@4.1.18: 2804 2812 resolution: {integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==} ··· 5560 5568 tailwindcss: 4.1.18 5561 5569 optionalDependencies: 5562 5570 tailwind-merge: 3.4.0 5571 + 5572 + tailwindcss-animate@1.0.7(tailwindcss@4.1.18): 5573 + dependencies: 5574 + tailwindcss: 4.1.18 5563 5575 5564 5576 tailwindcss@4.1.18: {} 5565 5577
+4
src/app.css
··· 3 3 @plugin '@tailwindcss/forms'; 4 4 @plugin '@tailwindcss/typography'; 5 5 6 + 7 + @plugin "tailwindcss-animate"; 8 + 9 + 6 10 @source '../node_modules/@foxui'; 7 11 8 12 @custom-variant dark (&:where(.dark, .dark *):not(:where(.light, .light *)));
+5 -1
src/lib/cards/ATProtoCollectionsCard/index.ts
··· 19 19 item.w = 4; 20 20 item.mobileW = 8; 21 21 }, 22 - sidebarButtonText: 'Atmosphere Collections' 22 + sidebarButtonText: 'Atmosphere Collections', 23 + 24 + name: 'ATProto Collections', 25 + 26 + groups: ['Social'] 23 27 } as CardDefinition & { type: 'atprotocollections' };
+3 -1
src/lib/cards/BigSocialCard/index.ts
··· 51 51 return item; 52 52 }, 53 53 urlHandlerPriority: 1, 54 - canHaveLabel: true 54 + canHaveLabel: true, 55 + 56 + groups: ['Social'] 55 57 } as CardDefinition & { type: 'bigsocial' }; 56 58 57 59 import {
+5 -1
src/lib/cards/BlueskyMediaCard/index.ts
··· 8 8 createNew: () => {}, 9 9 creationModalComponent: CreateBlueskyMediaCardModal, 10 10 sidebarButtonText: 'Bluesky Media', 11 - canHaveLabel: true 11 + canHaveLabel: true, 12 + 13 + groups: ['Media'], 14 + 15 + name: 'Video/Image from Bluesky' 12 16 } as CardDefinition & { type: 'blueskyMedia' };
+3 -1
src/lib/cards/BlueskyPostCard/index.ts
··· 63 63 return postsMap; 64 64 }, 65 65 minW: 4, 66 - name: 'Bluesky Post' 66 + name: 'Bluesky Post', 67 + 68 + groups: ['Social'] 67 69 } as CardDefinition & { type: 'blueskyPost' };
+4 -1
src/lib/cards/ButtonCard/index.ts
··· 27 27 minW: 2, 28 28 minH: 1, 29 29 maxW: 8, 30 - maxH: 4 30 + maxH: 4, 31 + 32 + groups: ['Utilities'], 33 + name: 'Button' 31 34 };
+3 -1
src/lib/cards/DrawCard/index.ts
··· 23 23 strokeWidth: 1, 24 24 locked: true 25 25 }; 26 - } 26 + }, 27 + 28 + groups: ['Visual'] 27 29 } as CardDefinition & { type: 'draw' };
+1 -1
src/lib/cards/EmbedCard/index.ts
··· 19 19 // change: (item) => { 20 20 // return item; 21 21 // }, 22 - name: 'Embed Card' 22 + name: 'Embed' 23 23 } as CardDefinition & { type: 'embed' };
+3 -1
src/lib/cards/EventCard/index.ts
··· 112 112 113 113 urlHandlerPriority: 5, 114 114 115 - name: 'Event Card' 115 + name: 'Event', 116 + 117 + groups: ['Social'] 116 118 } as CardDefinition & { type: 'event' };
+4 -1
src/lib/cards/FluidTextCard/index.ts
··· 23 23 sidebarButtonText: 'Fluid Text', 24 24 defaultColor: 'transparent', 25 25 allowSetColor: true, 26 - minW: 2 26 + minW: 2, 27 + 28 + groups: ['Visual'], 29 + name: 'Fluid Text' 27 30 } as CardDefinition & { type: 'fluid-text' };
+3 -1
src/lib/cards/GIFCard/index.ts
··· 45 45 return null; 46 46 }, 47 47 urlHandlerPriority: 5, 48 - name: 'GIF' 48 + name: 'GIF', 49 + 50 + groups: ['Media'] 49 51 } as CardDefinition & { type: 'gif' };
+4 -1
src/lib/cards/GameCards/DinoGameCard/index.ts
··· 14 14 card.mobileH = 6; 15 15 card.cardData = {}; 16 16 }, 17 - canHaveLabel: true 17 + canHaveLabel: true, 18 + 19 + groups: ['Games'], 20 + name: 'Dino Game' 18 21 } as CardDefinition & { type: 'dino-game' };
+5 -1
src/lib/cards/GameCards/TetrisCard/index.ts
··· 19 19 card.cardData = {}; 20 20 }, 21 21 maxH: 10, 22 - canHaveLabel: true 22 + canHaveLabel: true, 23 + 24 + groups: ['Games'], 25 + 26 + name: 'Tetris' 23 27 } as CardDefinition & { type: 'tetris' };
+3 -1
src/lib/cards/GitHubProfileCard/index.ts
··· 50 50 51 51 return item; 52 52 }, 53 - name: 'Github Profile' 53 + name: 'Github Profile', 54 + 55 + groups: ['Social'] 54 56 } as CardDefinition & { type: 'githubProfile' }; 55 57 56 58 function getGitHubUsername(url: string | undefined): string | undefined {
+2 -1
src/lib/cards/GuestbookCard/index.ts
··· 60 60 61 61 return results; 62 62 }, 63 - name: 'Guestbook' 63 + name: 'Guestbook', 64 + groups: ['Social'] 64 65 } as CardDefinition & { type: 'guestbook' };
+19 -2
src/lib/cards/ImageCard/index.ts
··· 42 42 }, 43 43 urlHandlerPriority: 3, 44 44 45 - name: 'Image Card', 45 + name: 'Image', 46 + 47 + canHaveLabel: true, 48 + 49 + groups: ['Core'], 46 50 47 - canHaveLabel: true 51 + icon: `<svg 52 + xmlns="http://www.w3.org/2000/svg" 53 + fill="none" 54 + viewBox="0 0 24 24" 55 + stroke-width="2" 56 + stroke="currentColor" 57 + class="size-4" 58 + > 59 + <path 60 + stroke-linecap="round" 61 + stroke-linejoin="round" 62 + d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z" 63 + /> 64 + </svg>` 48 65 } as CardDefinition & { type: 'image' };
+5 -1
src/lib/cards/LatestBlueskyPostCard/index.ts
··· 18 18 19 19 return JSON.parse(JSON.stringify(authorFeed)); 20 20 }, 21 - minW: 4 21 + minW: 4, 22 + 23 + name: 'Latest Bluesky Post', 24 + 25 + groups: ['Social'] 22 26 } as CardDefinition & { type: 'latestPost' };
+44
src/lib/cards/LinkCard/CreateLinkCardModal.svelte
··· 1 + <script lang="ts"> 2 + import { Button, Input, Modal, Subheading } from '@foxui/core'; 3 + import type { CreationModalComponentProps } from '../types'; 4 + import { validateLink } from '$lib/helper'; 5 + 6 + let { item = $bindable(), oncreate, oncancel }: CreationModalComponentProps = $props(); 7 + 8 + let isFetchingLocation = $state(false); 9 + 10 + let errorMessage = $state(''); 11 + </script> 12 + 13 + <Modal open={true} closeButton={false}> 14 + <form 15 + onsubmit={() => { 16 + if (!item.cardData.href.trim()) return; 17 + 18 + let link = validateLink(item.cardData.href); 19 + if (!link) { 20 + errorMessage = 'Invalid link'; 21 + return; 22 + } 23 + 24 + item.cardData.href = link; 25 + item.cardData.domain = new URL(link).hostname; 26 + item.cardData.hasFetched = false; 27 + 28 + oncreate?.(); 29 + }} 30 + class="flex flex-col gap-2" 31 + > 32 + <Subheading>Enter a link</Subheading> 33 + <Input bind:value={item.cardData.href} class="mt-4" /> 34 + 35 + {#if errorMessage} 36 + <p class="mt-2 text-sm text-red-600">{errorMessage}</p> 37 + {/if} 38 + 39 + <div class="mt-4 flex justify-end gap-2"> 40 + <Button onclick={oncancel} variant="ghost">Cancel</Button> 41 + <Button type="submit" disabled={isFetchingLocation}>Create</Button> 42 + </div> 43 + </form> 44 + </Modal>
+22 -2
src/lib/cards/LinkCard/index.ts
··· 1 1 import { checkAndUploadImage, validateLink } from '$lib/helper'; 2 2 import type { CardDefinition } from '../types'; 3 + import CreateLinkCardModal from './CreateLinkCardModal.svelte'; 3 4 import EditingLinkCard from './EditingLinkCard.svelte'; 4 5 import LinkCard from './LinkCard.svelte'; 5 6 import LinkCardSettings from './LinkCardSettings.svelte'; ··· 13 14 }, 14 15 settingsComponent: LinkCardSettings, 15 16 16 - name: 'Link Card', 17 + creationModalComponent: CreateLinkCardModal, 18 + 19 + name: 'Link', 17 20 canChange: (item) => Boolean(validateLink(item.cardData?.href)), 18 21 change: (item) => { 19 22 const href = validateLink(item.cardData?.href); ··· 36 39 await checkAndUploadImage(item.cardData, 'favicon'); 37 40 return item; 38 41 }, 39 - urlHandlerPriority: 0 42 + urlHandlerPriority: 0, 43 + 44 + groups: ['Core'], 45 + 46 + icon: `<svg 47 + xmlns="http://www.w3.org/2000/svg" 48 + fill="none" 49 + viewBox="-2 -2 28 28" 50 + stroke-width="2" 51 + stroke="currentColor" 52 + class="size-4" 53 + > 54 + <path 55 + stroke-linecap="round" 56 + stroke-linejoin="round" 57 + d="M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 0 1.242 7.244" 58 + /> 59 + </svg>` 40 60 } as CardDefinition & { type: 'link' };
+2 -1
src/lib/cards/LivestreamCard/index.ts
··· 81 81 82 82 urlHandlerPriority: 5, 83 83 84 - name: 'stream.place Card' 84 + name: 'Latest Livestream (stream.place)', 85 + groups: ['Media'] 85 86 } as CardDefinition & { type: 'latestLivestream' }; 86 87 87 88 export const LivestreamEmbedCardDefitition = {
+10 -1
src/lib/cards/MapCard/index.ts
··· 17 17 creationModalComponent: CreateMapCardModal, 18 18 allowSetColor: false, 19 19 canHaveLabel: true, 20 - settingsComponent: MapCardSettings 20 + settingsComponent: MapCardSettings, 21 + 22 + groups: ['Core'], 23 + 24 + name: 'Map', 25 + 26 + icon: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="size-4"> 27 + <path stroke-linecap="round" stroke-linejoin="round" d="M9 6.75V15m6-6v8.25m.503 3.498 4.875-2.437c.381-.19.622-.58.622-1.006V4.82c0-.836-.88-1.38-1.628-1.006l-3.869 1.934c-.317.159-.69.159-1.006 0L9.503 3.252a1.125 1.125 0 0 0-1.006 0L3.622 5.689C3.24 5.88 3 6.27 3 6.695V19.18c0 .836.88 1.38 1.628 1.006l3.869-1.934c.317-.159.69-.159 1.006 0l4.994 2.497c.317.158.69.158 1.006 0Z" /> 28 + </svg> 29 + ` 21 30 } as CardDefinition & { type: 'mapLocation' }; 22 31 23 32 export function getZoomLevel(type: string | undefined): number {
+4 -1
src/lib/cards/PopfeedReviews/index.ts
··· 18 18 }, 19 19 minH: 3, 20 20 sidebarButtonText: 'Popfeed Reviews', 21 - canHaveLabel: true 21 + canHaveLabel: true, 22 + 23 + groups: ['Media'], 24 + name: 'Movie and TV Reviews' 22 25 } as CardDefinition & { type: 'recentPopfeedReviews' };
+16 -1
src/lib/cards/SectionCard/index.ts
··· 26 26 defaultColor: 'transparent', 27 27 maxH: 1, 28 28 canResize: false, 29 - settingsComponent: SectionCardSettings 29 + settingsComponent: SectionCardSettings, 30 + 31 + name: 'Heading', 32 + groups: ['Core'], 33 + 34 + icon: `<svg 35 + xmlns="http://www.w3.org/2000/svg" 36 + viewBox="0 0 24 24" 37 + fill="none" 38 + stroke="currentColor" 39 + stroke-width="2" 40 + stroke-linecap="round" 41 + stroke-linejoin="round" 42 + class="size-4" 43 + ><path d="M6 12h12" /><path d="M6 20V4" /><path d="M18 20V4" /></svg 44 + >` 30 45 } as CardDefinition & { type: 'section' }; 31 46 32 47 export const textAlignClasses: Record<string, string> = {
+3 -1
src/lib/cards/SpotifyCard/index.ts
··· 40 40 name: 'Spotify Embed', 41 41 canResize: true, 42 42 minW: 4, 43 - minH: 5 43 + minH: 5, 44 + 45 + groups: ['Media'] 44 46 } as CardDefinition & { type: typeof cardType }; 45 47 46 48 // Match Spotify album and playlist URLs
+5 -1
src/lib/cards/StandardSiteDocumentListCard/index.ts
··· 42 42 return records; 43 43 }, 44 44 45 - sidebarButtonText: 'site.standard.document list' 45 + sidebarButtonText: 'site.standard.document list', 46 + 47 + name: 'Blog Posts', 48 + 49 + groups: ['Content'] 46 50 } as CardDefinition & { type: 'site.standard.document list' };
+4 -1
src/lib/cards/StatusphereCard/index.ts
··· 47 47 item.cardData.label = item.cardData.title; 48 48 } 49 49 }, 50 - canHaveLabel: true 50 + canHaveLabel: true, 51 + 52 + name: 'Emoji', 53 + groups: ['Media'] 51 54 } as CardDefinition & { type: 'statusphere' }; 52 55 53 56 export function emojiToNotoAnimatedWebp(emoji: string | undefined): string | undefined {
+5 -1
src/lib/cards/TealFMPlaysCard/index.ts
··· 22 22 }, 23 23 minW: 4, 24 24 sidebarButtonText: 'teal.fm Plays', 25 - canHaveLabel: true 25 + canHaveLabel: true, 26 + 27 + name: 'Teal.fm Plays', 28 + 29 + groups: ['Media'] 26 30 } as CardDefinition & { type: 'recentTealFMPlays' };
+16 -1
src/lib/cards/TextCard/index.ts
··· 14 14 }; 15 15 }, 16 16 17 - settingsComponent: TextCardSettings 17 + settingsComponent: TextCardSettings, 18 + 19 + name: 'Text', 20 + 21 + groups: ['Core'], 22 + 23 + icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="size-4" 24 + ><path 25 + fill="none" 26 + stroke="currentColor" 27 + stroke-linecap="round" 28 + stroke-linejoin="round" 29 + stroke-width="2" 30 + d="m15 16l2.536-7.328a1.02 1.02 1 0 1 1.928 0L22 16m-6.303-2h5.606M2 16l4.039-9.69a.5.5 0 0 1 .923 0L11 16m-7.696-3h6.392" 31 + /></svg 32 + >` 18 33 } as CardDefinition & { type: 'text' }; 19 34 20 35 export const textAlignClasses: Record<string, string> = {
+2 -1
src/lib/cards/TimerCard/index.ts
··· 33 33 allowSetColor: true, 34 34 name: 'Timer Card', 35 35 minW: 4, 36 - canHaveLabel: true 36 + canHaveLabel: true, 37 + groups: ['Utilities'] 37 38 } as CardDefinition & { type: 'timer' };
+2 -1
src/lib/cards/VCardCard/index.ts
··· 122 122 123 123 sidebarButtonText: 'vCard', 124 124 allowSetColor: true, 125 - name: 'vCard Card' 125 + name: 'vCard Card', 126 + groups: ['Social'] 126 127 } as CardDefinition & { type: 'vcard' };
+10 -1
src/lib/cards/YoutubeVideoCard/index.ts
··· 51 51 52 52 return item; 53 53 }, 54 - name: 'Youtube Video' 54 + name: 'Youtube Video', 55 + 56 + groups: ['Media'], 57 + 58 + icon: `<svg xmlns="http://www.w3.org/2000/svg" class="h-3" viewBox="0 0 256 180" 59 + ><path 60 + fill="currentColor" 61 + d="M250.346 28.075A32.18 32.18 0 0 0 227.69 5.418C207.824 0 127.87 0 127.87 0S47.912.164 28.046 5.582A32.18 32.18 0 0 0 5.39 28.24c-6.009 35.298-8.34 89.084.165 122.97a32.18 32.18 0 0 0 22.656 22.657c19.866 5.418 99.822 5.418 99.822 5.418s79.955 0 99.82-5.418a32.18 32.18 0 0 0 22.657-22.657c6.338-35.348 8.291-89.1-.164-123.134" 62 + /><path fill="currentColor" class="invert" d="m102.421 128.06l66.328-38.418l-66.328-38.418z" /></svg 63 + >` 55 64 } as CardDefinition & { type: 'youtubeVideo' }; 56 65 57 66 // Thanks to eleventy-plugin-youtube-embed
+6
src/lib/cards/types.ts
··· 73 73 canHaveLabel?: boolean; 74 74 75 75 migrate?: (item: Item) => void; 76 + 77 + groups?: string[]; 78 + 79 + keywords?: string[]; 80 + 81 + icon?: string; 76 82 };
+102
src/lib/components/card-command/CardCommand.svelte
··· 1 + <script lang="ts"> 2 + import { AllCardDefinitions } from '$lib/cards'; 3 + import type { CardDefinition } from '$lib/cards/types'; 4 + import { Command, Dialog } from 'bits-ui'; 5 + 6 + const CardDefGroups = [ 7 + 'Core', 8 + ...Array.from( 9 + new Set( 10 + AllCardDefinitions.map((cardDef) => cardDef.groups) 11 + .flat() 12 + .filter((g) => g) 13 + ) 14 + ) 15 + .sort() 16 + .filter((g) => g !== 'Core') 17 + ]; 18 + 19 + let { 20 + open = $bindable(false), 21 + onselect 22 + }: { open: boolean; onselect: (cardDef: CardDefinition) => void } = $props(); 23 + 24 + function handleKeydown(e: KeyboardEvent) { 25 + if (e.key === 'k' && (e.metaKey || e.ctrlKey)) { 26 + e.preventDefault(); 27 + open = true; 28 + } 29 + } 30 + </script> 31 + 32 + <svelte:document onkeydown={handleKeydown} /> 33 + 34 + <Dialog.Root bind:open> 35 + <Dialog.Portal> 36 + <Dialog.Overlay 37 + class="data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80" 38 + /> 39 + <Dialog.Content 40 + class="data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-36 left-[50%] z-50 w-full max-w-[94%] translate-x-[-50%] outline-hidden sm:max-w-lg md:w-full" 41 + > 42 + <Dialog.Title class="sr-only">Command Menu</Dialog.Title> 43 + <Dialog.Description class="sr-only"> 44 + This is the command menu. Use the arrow keys to navigate and press ⌘K to open the search 45 + bar. 46 + </Dialog.Description> 47 + <Command.Root 48 + class="border-base-200 dark:border-base-800 mx-auto flex h-full w-full max-w-[90vw] flex-col overflow-hidden rounded-2xl border bg-white dark:bg-black" 49 + > 50 + <Command.Input 51 + class="focus-override placeholder:text-base-900/50 dark:placeholder:text-base-50/50 border-base-200 dark:border-base-800 bg-base-100 mx-1 mt-1 inline-flex truncate rounded-2xl rounded-tl-2xl px-4 text-sm transition-colors focus:ring-0 focus:outline-hidden dark:bg-black" 52 + placeholder="Search for a card..." 53 + /> 54 + <Command.List 55 + class="focus:outline-accent-500/50 max-h-[50vh] overflow-x-hidden overflow-y-auto rounded-br-2xl rounded-bl-2xl bg-white px-2 pb-2 focus:border-0 dark:bg-black" 56 + > 57 + <Command.Viewport> 58 + <Command.Empty 59 + class="text-base-900 dark:text-base-100 flex w-full items-center justify-center pt-8 pb-6 text-sm" 60 + > 61 + No results found. 62 + </Command.Empty> 63 + 64 + {#each CardDefGroups as group, index} 65 + {#if group && AllCardDefinitions.some((cardDef) => cardDef.groups?.includes(group))} 66 + <Command.Group> 67 + <Command.GroupHeading 68 + class="text-base-600 dark:text-base-400 px-3 pt-4 pb-2 text-xs" 69 + > 70 + {group} 71 + </Command.GroupHeading> 72 + <Command.GroupItems> 73 + {#each AllCardDefinitions.filter( (cardDef) => cardDef.groups?.includes(group) ) as cardDef} 74 + <Command.Item 75 + onSelect={() => { 76 + open = false; 77 + onselect(cardDef); 78 + }} 79 + 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" 80 + keywords={[group, cardDef.type, ...(cardDef.keywords || [])]} 81 + > 82 + {#if cardDef.icon} 83 + <div class="text-base-700 dark:text-base-300"> 84 + {@html cardDef.icon} 85 + </div> 86 + {/if} 87 + {cardDef.name} 88 + </Command.Item> 89 + {/each} 90 + </Command.GroupItems> 91 + </Command.Group> 92 + {#if index < CardDefGroups.length - 1} 93 + <Command.Separator class="bg-base-900/5 dark:bg-base-50/5 my-1 h-px w-full" /> 94 + {/if} 95 + {/if} 96 + {/each} 97 + </Command.Viewport> 98 + </Command.List> 99 + </Command.Root> 100 + </Dialog.Content> 101 + </Dialog.Portal> 102 + </Dialog.Root>
+7 -2
src/lib/website/EditBar.svelte
··· 17 17 save, 18 18 19 19 handleImageInputChange, 20 - handleVideoInputChange 20 + handleVideoInputChange, 21 + 22 + showCardCommand 21 23 }: { 22 24 data: WebsiteData; 23 25 linkValue: string; ··· 33 35 34 36 handleImageInputChange: (evt: Event) => void; 35 37 handleVideoInputChange: (evt: Event) => void; 38 + 39 + showCardCommand: () => void; 36 40 } = $props(); 37 41 38 42 let linkPopoverOpen = $state(false); ··· 59 63 accept="image/*" 60 64 onchange={handleImageInputChange} 61 65 class="hidden" 66 + id="image-input" 62 67 multiple 63 68 bind:this={imageInputRef} 64 69 /> ··· 220 225 </Button> 221 226 {/if} 222 227 223 - <Button size="iconLg" variant="ghost" class="backdrop-blur-none" popovertarget="mobile-menu"> 228 + <Button size="iconLg" variant="ghost" class="backdrop-blur-none" onclick={showCardCommand}> 224 229 <svg 225 230 xmlns="http://www.w3.org/2000/svg" 226 231 fill="none"
+22 -1
src/lib/website/EditableWebsite.svelte
··· 24 24 import EditingCard from '../cards/Card/EditingCard.svelte'; 25 25 import { AllCardDefinitions, CardDefinitionsByType } from '../cards'; 26 26 import { tick, type Component } from 'svelte'; 27 - import type { CreationModalComponentProps } from '../cards/types'; 27 + import type { CardDefinition, CreationModalComponentProps } from '../cards/types'; 28 28 import { dev } from '$app/environment'; 29 29 import { setIsMobile } from './context'; 30 30 import BaseEditingCard from '../cards/BaseCard/BaseEditingCard.svelte'; ··· 38 38 import { user } from '$lib/atproto'; 39 39 import { launchConfetti } from '@foxui/visual'; 40 40 import Controls from './Controls.svelte'; 41 + import CardCommand from '$lib/components/card-command/CardCommand.svelte'; 41 42 42 43 let { 43 44 data ··· 645 646 } 646 647 647 648 // $inspect(items); 649 + 650 + let showCardCommand = $state(true); 648 651 </script> 649 652 650 653 <svelte:body ··· 683 686 Editing on mobile is not supported yet. Please use a desktop browser. 684 687 </div> 685 688 {/if} 689 + 690 + <CardCommand 691 + bind:open={showCardCommand} 692 + onselect={(cardDef: CardDefinition) => { 693 + if (cardDef.type === 'image') { 694 + const input = document.getElementById('image-input') as HTMLInputElement; 695 + if (input) { 696 + input.click(); 697 + return; 698 + } 699 + } else { 700 + newCard(cardDef.type); 701 + } 702 + }} 703 + /> 686 704 687 705 <Controls bind:data /> 688 706 ··· 911 929 {save} 912 930 {handleImageInputChange} 913 931 {handleVideoInputChange} 932 + showCardCommand={() => { 933 + showCardCommand = true; 934 + }} 914 935 /> 915 936 916 937 <Toaster />