your personal website on atproto - mirror blento.app

Merge pull request #25 from flo-bit/card-label

add card label, add google maps link, small fixes

authored by Florian and committed by GitHub 725395dc a9b6899b

+157 -99
+1 -1
docs/Selfhosting.md
··· 31 6. some cards need their own additional env keys, if you have these cards in your profile, create your keys and add them to your cloudflare worker 32 33 - github profile: GITHUB_TOKEN 34 - - map: PUBLIC_MAPBOX_TOKEN
··· 31 6. some cards need their own additional env keys, if you have these cards in your profile, create your keys and add them to your cloudflare worker 32 33 - github profile: GITHUB_TOKEN 34 + - map: PUBLIC_MAPBOX_TOKEN
+8
src/lib/cards/BaseCard/BaseCard.svelte
··· 68 ]} 69 > 70 {@render children?.()} 71 </div> 72 {@render controls?.()} 73 </div>
··· 68 ]} 69 > 70 {@render children?.()} 71 + 72 + {#if !isEditing && item.cardData.label} 73 + <div 74 + class="text-base-900 dark:text-base-50 bg-base-200/50 dark:bg-base-900/50 absolute top-2 left-2 z-30 max-w-[calc(100%-1rem)] rounded-xl p-1 px-2 text-base font-semibold backdrop-blur-md" 75 + > 76 + {item.cardData.label} 77 + </div> 78 + {/if} 79 </div> 80 {@render controls?.()} 81 </div>
+19 -4
src/lib/cards/BaseCard/BaseEditingCard.svelte
··· 3 import type { HTMLAttributes } from 'svelte/elements'; 4 import BaseCard from './BaseCard.svelte'; 5 import type { Item } from '$lib/types'; 6 - import { Button, Label, Popover } from '@foxui/core'; 7 import { ColorSelect } from '@foxui/colors'; 8 import { AllCardDefinitions, CardDefinitionsByType, getColor } from '..'; 9 import { COLUMNS } from '$lib'; 10 import { getCanEdit, getIsMobile } from '$lib/website/context'; 11 12 let colorsChoices = [ 13 { class: 'text-base-500', label: 'base' }, ··· 151 let settingsPopoverOpen = $state(false); 152 let changePopoverOpen = $state(false); 153 154 - const changeOptions = $derived( 155 - AllCardDefinitions.filter((def) => def.canChange?.(item)) 156 - ); 157 158 function applyChange(def: (typeof AllCardDefinitions)[number]) { 159 const updated = def.change ? def.change(item) : item; ··· 179 > 180 <div class="absolute inset-0 cursor-grab"></div> 181 {@render children?.()} 182 183 {#snippet controls()} 184 <!-- class="bg-base-100 border-base-200 dark:bg-base-800 dark:border-base-700 absolute -top-3 -left-3 hidden cursor-pointer items-center justify-center rounded-full border p-2 shadow-lg group-focus-within:inline-flex group-hover/card:inline-flex" -->
··· 3 import type { HTMLAttributes } from 'svelte/elements'; 4 import BaseCard from './BaseCard.svelte'; 5 import type { Item } from '$lib/types'; 6 + import { Button, cn, Label, Popover } from '@foxui/core'; 7 import { ColorSelect } from '@foxui/colors'; 8 import { AllCardDefinitions, CardDefinitionsByType, getColor } from '..'; 9 import { COLUMNS } from '$lib'; 10 import { getCanEdit, getIsMobile } from '$lib/website/context'; 11 + import PlainTextEditor from '../utils/PlainTextEditor.svelte'; 12 13 let colorsChoices = [ 14 { class: 'text-base-500', label: 'base' }, ··· 152 let settingsPopoverOpen = $state(false); 153 let changePopoverOpen = $state(false); 154 155 + const changeOptions = $derived(AllCardDefinitions.filter((def) => def.canChange?.(item))); 156 157 function applyChange(def: (typeof AllCardDefinitions)[number]) { 158 const updated = def.change ? def.change(item) : item; ··· 178 > 179 <div class="absolute inset-0 cursor-grab"></div> 180 {@render children?.()} 181 + 182 + {#if cardDef.canHaveLabel} 183 + <div 184 + class={cn( 185 + 'bg-base-200/30 dark:bg-base-900/30 absolute top-2 left-2 z-100 w-fit max-w-[calc(100%-1rem)] rounded-xl p-1 px-2 backdrop-blur-md', 186 + !item.cardData.label && 'hidden group-hover/card:block' 187 + )} 188 + > 189 + <PlainTextEditor 190 + class="text-base-900 dark:text-base-50 w-fit text-base font-semibold" 191 + key="label" 192 + bind:item 193 + placeholder="Label" 194 + /> 195 + </div> 196 + {/if} 197 198 {#snippet controls()} 199 <!-- class="bg-base-100 border-base-200 dark:bg-base-800 dark:border-base-700 absolute -top-3 -left-3 hidden cursor-pointer items-center justify-center rounded-full border p-2 shadow-lg group-focus-within:inline-flex group-hover/card:inline-flex" -->
+10 -6
src/lib/cards/BigSocialCard/BigSocialCard.svelte
··· 2 import { platformsData } from '.'; 3 import type { ContentComponentProps } from '../types'; 4 5 - let { item }: ContentComponentProps = $props(); 6 7 const platform = $derived(item.cardData.platform as string); 8 </script> 9 10 - <a 11 - href={item.cardData.href} 12 - target="_blank" 13 - rel="noopener noreferrer" 14 class="flex h-full w-full items-center justify-center p-10" 15 style={`background-color: #${item.cardData.color}`} 16 > ··· 19 > 20 {@html platformsData[platform].svg} 21 </div> 22 - </a>
··· 2 import { platformsData } from '.'; 3 import type { ContentComponentProps } from '../types'; 4 5 + let { item, isEditing }: ContentComponentProps = $props(); 6 7 const platform = $derived(item.cardData.platform as string); 8 </script> 9 10 + <div 11 class="flex h-full w-full items-center justify-center p-10" 12 style={`background-color: #${item.cardData.color}`} 13 > ··· 16 > 17 {@html platformsData[platform].svg} 18 </div> 19 + </div> 20 + 21 + {#if !isEditing} 22 + <a href={item.cardData.href} target="_blank" rel="noopener noreferrer"> 23 + <div class="absolute inset-0 z-50"></div> 24 + <span class="sr-only">open {platformsData[platform].title}</span> 25 + </a> 26 + {/if}
+2 -1
src/lib/cards/BigSocialCard/index.ts
··· 50 51 return item; 52 }, 53 - urlHandlerPriority: 1 54 } as CardDefinition & { type: 'bigsocial' }; 55 56 import {
··· 50 51 return item; 52 }, 53 + urlHandlerPriority: 1, 54 + canHaveLabel: true 55 } as CardDefinition & { type: 'bigsocial' }; 56 57 import {
+2 -1
src/lib/cards/BlueskyMediaCard/index.ts
··· 9 createNew: () => {}, 10 creationModalComponent: CreateBlueskyMediaCardModal, 11 sidebarButtonText: 'Bluesky Media', 12 - sidebarComponent: SidebarItemBlueskyMediaCard 13 } as CardDefinition & { type: 'blueskyMedia' };
··· 9 createNew: () => {}, 10 creationModalComponent: CreateBlueskyMediaCardModal, 11 sidebarButtonText: 'Bluesky Media', 12 + sidebarComponent: SidebarItemBlueskyMediaCard, 13 + canHaveLabel: true 14 } as CardDefinition & { type: 'blueskyMedia' };
+1 -1
src/lib/cards/Card/Card.svelte
··· 7 8 {#if CardDefinitionsByType[item.cardType]} 9 {@const cardDef = CardDefinitionsByType[item.cardType]} 10 - <cardDef.contentComponent {item} {...rest} /> 11 {:else} 12 <div class="m-4">Unsupported card type: {item.cardType}</div> 13 {/if}
··· 7 8 {#if CardDefinitionsByType[item.cardType]} 9 {@const cardDef = CardDefinitionsByType[item.cardType]} 10 + <cardDef.contentComponent isEditing={false} {item} {...rest} /> 11 {:else} 12 <div class="m-4">Unsupported card type: {item.cardType}</div> 13 {/if}
+2 -2
src/lib/cards/Card/EditingCard.svelte
··· 8 {#if CardDefinitionsByType[item.cardType]} 9 {@const cardDef = CardDefinitionsByType[item.cardType]} 10 {#if cardDef.editingContentComponent} 11 - <cardDef.editingContentComponent bind:item /> 12 {:else} 13 - <cardDef.contentComponent bind:item /> 14 {/if} 15 {:else} 16 <div class="m-4">Unsupported card type: {item.cardType}</div>
··· 8 {#if CardDefinitionsByType[item.cardType]} 9 {@const cardDef = CardDefinitionsByType[item.cardType]} 10 {#if cardDef.editingContentComponent} 11 + <cardDef.editingContentComponent bind:item isEditing /> 12 {:else} 13 + <cardDef.contentComponent bind:item isEditing /> 14 {/if} 15 {:else} 16 <div class="m-4">Unsupported card type: {item.cardType}</div>
+4 -4
src/lib/cards/EmbedCard/index.ts
··· 14 card.mobileW = 8; 15 }, 16 17 - canChange: (item) => Boolean(item.cardData.href && !item.cardData.href.startsWith('mailto:')), 18 19 - change: (item) => { 20 - return item; 21 - }, 22 name: 'Embed Card' 23 } as CardDefinition & { type: 'embed' };
··· 14 card.mobileW = 8; 15 }, 16 17 + // canChange: (item) => Boolean(item.cardData.href), 18 19 + // change: (item) => { 20 + // return item; 21 + // }, 22 name: 'Embed Card' 23 } as CardDefinition & { type: 'embed' };
+18 -21
src/lib/cards/GIFCard/GifCardSettings.svelte
··· 19 } 20 </script> 21 22 - <div class="flex flex-col gap-3"> 23 - <div> 24 - <Label class="mb-1 text-xs">Change GIF</Label> 25 - <Button variant="secondary" class="w-full justify-start" onclick={() => (isSearchOpen = true)}> 26 - <svg 27 - xmlns="http://www.w3.org/2000/svg" 28 - fill="none" 29 - viewBox="0 0 24 24" 30 - stroke-width="1.5" 31 - stroke="currentColor" 32 - class="mr-2 size-4" 33 - > 34 - <path 35 - stroke-linecap="round" 36 - stroke-linejoin="round" 37 - d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" 38 - /> 39 - </svg> 40 - Search GIPHY 41 - </Button> 42 - </div> 43 </div> 44 45 <GiphySearchModal
··· 19 } 20 </script> 21 22 + <div class="flex flex-col gap-2"> 23 + <Button variant="secondary" class="w-full justify-start" onclick={() => (isSearchOpen = true)}> 24 + <svg 25 + xmlns="http://www.w3.org/2000/svg" 26 + fill="none" 27 + viewBox="0 0 24 24" 28 + stroke-width="1.5" 29 + stroke="currentColor" 30 + class="mr-2 size-4" 31 + > 32 + <path 33 + stroke-linecap="round" 34 + stroke-linejoin="round" 35 + d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" 36 + /> 37 + </svg> 38 + Change GIF 39 + </Button> 40 </div> 41 42 <GiphySearchModal
+1
src/lib/cards/GIFCard/index.ts
··· 26 allowSetColor: false, 27 minW: 1, 28 minH: 1, 29 onUrlHandler: (url, item) => { 30 // Match Giphy page URLs: https://giphy.com/gifs/name-ID or https://giphy.com/gifs/ID 31 const pageMatch = url.match(/giphy\.com\/gifs\/(?:.*-)?([a-zA-Z0-9]+)(?:\?|$)/);
··· 26 allowSetColor: false, 27 minW: 1, 28 minH: 1, 29 + canHaveLabel: true, 30 onUrlHandler: (url, item) => { 31 // Match Giphy page URLs: https://giphy.com/gifs/name-ID or https://giphy.com/gifs/ID 32 const pageMatch = url.match(/giphy\.com\/gifs\/(?:.*-)?([a-zA-Z0-9]+)(?:\?|$)/);
+4 -4
src/lib/cards/ImageCard/ImageCard.svelte
··· 3 import { getImageBlobUrl } from '$lib/atproto'; 4 import type { ContentComponentProps } from '../types'; 5 6 - let { item = $bindable() }: ContentComponentProps = $props(); 7 8 const did = getDidContext(); 9 ··· 21 <img 22 class={[ 23 'absolute inset-0 h-full w-full object-cover opacity-100 transition-transform duration-300 ease-in-out', 24 - item.cardData.href ? 'group-hover:scale-102' : '' 25 ]} 26 src={getSrc()} 27 alt="" 28 /> 29 {/key} 30 - {#if item.cardData.href} 31 <a 32 href={item.cardData.href} 33 - class="absolute inset-0 h-full w-full" 34 target="_blank" 35 rel="noopener noreferrer" 36 >
··· 3 import { getImageBlobUrl } from '$lib/atproto'; 4 import type { ContentComponentProps } from '../types'; 5 6 + let { item = $bindable(), isEditing }: ContentComponentProps = $props(); 7 8 const did = getDidContext(); 9 ··· 21 <img 22 class={[ 23 'absolute inset-0 h-full w-full object-cover opacity-100 transition-transform duration-300 ease-in-out', 24 + item.cardData.href ? 'group-hover/card:scale-101' : '' 25 ]} 26 src={getSrc()} 27 alt="" 28 /> 29 {/key} 30 + {#if item.cardData.href && !isEditing} 31 <a 32 href={item.cardData.href} 33 + class="absolute inset-0 z-50 h-full w-full" 34 target="_blank" 35 rel="noopener noreferrer" 36 >
+3 -1
src/lib/cards/ImageCard/index.ts
··· 36 change: (item) => { 37 return item; 38 }, 39 - name: 'Image Card' 40 } as CardDefinition & { type: 'image' };
··· 36 change: (item) => { 37 return item; 38 }, 39 + name: 'Image Card', 40 + 41 + canHaveLabel: true 42 } as CardDefinition & { type: 'image' };
+2 -2
src/lib/cards/MapCard/CreateMapCardModal.svelte
··· 28 item.cardData.lon = data.lon; 29 item.cardData.name = data.display_name?.split(',')[0] || search; 30 item.cardData.type = data.class || 'city'; 31 - item.cardData.zoom = Math.max(getZoomLevel(data.class), getZoomLevel(data.type)); 32 } else { 33 throw new Error('response not ok'); 34 } ··· 56 <Alert type="error" title="Failed to create map card"><span>{errorMessage}</span></Alert> 57 {/if} 58 59 - <p class="text-xs mt-2"> 60 Geocoding by <a 61 href="https://nominatim.openstreetmap.org/" 62 class="text-accent-800 dark:text-accent-300"
··· 28 item.cardData.lon = data.lon; 29 item.cardData.name = data.display_name?.split(',')[0] || search; 30 item.cardData.type = data.class || 'city'; 31 + item.cardData.zoom = Math.max(getZoomLevel(data.class), getZoomLevel(data.type)); 32 } else { 33 throw new Error('response not ok'); 34 } ··· 56 <Alert type="error" title="Failed to create map card"><span>{errorMessage}</span></Alert> 57 {/if} 58 59 + <p class="mt-2 text-xs"> 60 Geocoding by <a 61 href="https://nominatim.openstreetmap.org/" 62 class="text-accent-800 dark:text-accent-300"
+3 -10
src/lib/cards/MapCard/Map.svelte
··· 13 let mapContainer: HTMLElement | undefined = $state(); 14 let map: mapboxgl.Map | undefined = $state(); 15 16 - // Update light preset when changed in settings 17 - $effect(() => { 18 - const preset = item.cardData.lightPreset; 19 - if (map && preset) { 20 - map.setConfigProperty('basemap', 'lightPreset', preset); 21 - } 22 - }); 23 - 24 onMount(() => { 25 if (!mapContainer || !env.PUBLIC_MAPBOX_TOKEN) { 26 console.log('no map container or no mapbox token'); 27 } 28 29 try { ··· 151 map.setCenter([lon, lat]); 152 } 153 }); 154 - resizeObserver.observe(mapContainer); 155 156 return () => { 157 resizeObserver.disconnect(); ··· 165 }); 166 </script> 167 168 - <div bind:this={mapContainer} class="absolute inset-0 isolate z-50 h-full w-full"></div>
··· 13 let mapContainer: HTMLElement | undefined = $state(); 14 let map: mapboxgl.Map | undefined = $state(); 15 16 onMount(() => { 17 if (!mapContainer || !env.PUBLIC_MAPBOX_TOKEN) { 18 console.log('no map container or no mapbox token'); 19 + return; 20 } 21 22 try { ··· 144 map.setCenter([lon, lat]); 145 } 146 }); 147 + if (mapContainer) resizeObserver.observe(mapContainer); 148 149 return () => { 150 resizeObserver.disconnect(); ··· 158 }); 159 </script> 160 161 + <div bind:this={mapContainer} class="absolute inset-0 isolate h-full w-full"></div>
+14 -2
src/lib/cards/MapCard/MapCard.svelte
··· 1 <script lang="ts"> 2 - import type { Item } from '$lib/types'; 3 import Map from './Map.svelte'; 4 5 - let { item = $bindable() }: { item: Item } = $props(); 6 </script> 7 8 <Map bind:item />
··· 1 <script lang="ts"> 2 + import type { ContentComponentProps } from '../types'; 3 import Map from './Map.svelte'; 4 5 + let { item = $bindable(), isEditing }: ContentComponentProps = $props(); 6 </script> 7 8 <Map bind:item /> 9 + 10 + {#if item.cardData.linkToGoogleMaps && !isEditing} 11 + <a 12 + target="_blank" 13 + rel="noopener noreferrer" 14 + href={'http://maps.google.com/maps?q=' + 15 + encodeURIComponent(item.cardData.lat + ',' + item.cardData.lon)} 16 + > 17 + <div class="absolute inset-0 z-100"></div> 18 + <span class="sr-only">open map</span> 19 + </a> 20 + {/if}
+24
src/lib/cards/MapCard/MapCardSettings.svelte
···
··· 1 + <script lang="ts"> 2 + import type { Item } from '$lib/types'; 3 + import { Checkbox, Label } from '@foxui/core'; 4 + 5 + let { item }: { item: Item; onclose: () => void } = $props(); 6 + </script> 7 + 8 + <div class="flex items-center space-x-2"> 9 + <Checkbox 10 + bind:checked={ 11 + () => Boolean(item.cardData.linkToGoogleMaps), (val) => (item.cardData.linkToGoogleMaps = val) 12 + } 13 + id="show-inline" 14 + aria-labelledby="show-inline-label" 15 + variant="secondary" 16 + /> 17 + <Label 18 + id="show-inline-label" 19 + for="show-inline" 20 + class="text-sm leading-none font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 21 + > 22 + Link to google maps 23 + </Label> 24 + </div>
+4 -1
src/lib/cards/MapCard/index.ts
··· 1 import type { CardDefinition } from '../types'; 2 import CreateMapCardModal from './CreateMapCardModal.svelte'; 3 import MapCard from './MapCard.svelte'; 4 import SidebarItemMapCard from './SidebarItemMapCard.svelte'; 5 6 export const MapCardDefinition = { ··· 16 17 sidebarComponent: SidebarItemMapCard, 18 creationModalComponent: CreateMapCardModal, 19 - allowSetColor: false 20 } as CardDefinition & { type: 'mapLocation' }; 21 22 export function getZoomLevel(type: string | undefined): number {
··· 1 import type { CardDefinition } from '../types'; 2 import CreateMapCardModal from './CreateMapCardModal.svelte'; 3 import MapCard from './MapCard.svelte'; 4 + import MapCardSettings from './MapCardSettings.svelte'; 5 import SidebarItemMapCard from './SidebarItemMapCard.svelte'; 6 7 export const MapCardDefinition = { ··· 17 18 sidebarComponent: SidebarItemMapCard, 19 creationModalComponent: CreateMapCardModal, 20 + allowSetColor: false, 21 + canHaveLabel: true, 22 + settingsComponent: MapCardSettings 23 } as CardDefinition & { type: 'mapLocation' }; 24 25 export function getZoomLevel(type: string | undefined): number {
+2 -1
src/lib/cards/PopfeedReviews/index.ts
··· 17 return data; 18 }, 19 minH: 3, 20 - sidebarButtonText: 'Popfeed Reviews' 21 } as CardDefinition & { type: 'recentPopfeedReviews' };
··· 17 return data; 18 }, 19 minH: 3, 20 + sidebarButtonText: 'Popfeed Reviews', 21 + canHaveLabel: true 22 } as CardDefinition & { type: 'recentPopfeedReviews' };
+2 -1
src/lib/cards/SectionCard/EditingSectionCard.svelte
··· 8 </script> 9 10 <div 11 - class={["line-clamp-1 inline-flex h-full w-full rounded-md p-1 px-2 font-semibold", 12 textAlignClasses[item.cardData.textAlign as string], 13 verticalAlignClasses[item.cardData.verticalAlign ?? ('center' as string)], 14 textSizeClasses[(item.cardData.textSize ?? 1) as number]
··· 8 </script> 9 10 <div 11 + class={[ 12 + 'line-clamp-1 inline-flex h-full w-full rounded-md p-1 px-2 font-semibold', 13 textAlignClasses[item.cardData.textAlign as string], 14 verticalAlignClasses[item.cardData.verticalAlign ?? ('center' as string)], 15 textSizeClasses[(item.cardData.textSize ?? 1) as number]
+1 -3
src/lib/cards/SectionCard/index.ts
··· 29 settingsComponent: SectionCardSettings 30 } as CardDefinition & { type: 'section' }; 31 32 - 33 - 34 export const textAlignClasses: Record<string, string> = { 35 left: '', 36 center: 'text-center justify-center', ··· 43 bottom: 'items-end-safe' 44 }; 45 46 - export const textSizeClasses = ['text-lg', 'text-2xl', 'text-4xl', 'text-6xl'];
··· 29 settingsComponent: SectionCardSettings 30 } as CardDefinition & { type: 'section' }; 31 32 export const textAlignClasses: Record<string, string> = { 33 left: '', 34 center: 'text-center justify-center', ··· 41 bottom: 'items-end-safe' 42 }; 43 44 + export const textSizeClasses = ['text-lg', 'text-2xl', 'text-4xl', 'text-5xl'];
-16
src/lib/cards/StatusphereCard/EditStatusphereCard.svelte
··· 5 import { CardDefinitionsByType } from '..'; 6 import { PopoverEmojiPicker } from '@foxui/social'; 7 import { emojiToNotoAnimatedWebp } from '.'; 8 - import PlainTextEditor from '../utils/PlainTextEditor.svelte'; 9 - import { cn } from '@foxui/core'; 10 11 let { item }: { item: Item } = $props(); 12 ··· 67 </button> 68 {/snippet} 69 </PopoverEmojiPicker> 70 - 71 - <div 72 - class={cn( 73 - 'bg-base-200/30 dark:bg-base-900/30 absolute top-2 right-2 left-2 z-30 rounded-lg p-1 backdrop-blur-md', 74 - !item.cardData.title && 'hidden group-hover/card:block' 75 - )} 76 - > 77 - <PlainTextEditor 78 - class="text-base-900 dark:text-base-50 text-md line-clamp-1 font-bold" 79 - key="title" 80 - bind:item 81 - placeholder="I'm feeling..." 82 - /> 83 - </div> 84 </div>
··· 5 import { CardDefinitionsByType } from '..'; 6 import { PopoverEmojiPicker } from '@foxui/social'; 7 import { emojiToNotoAnimatedWebp } from '.'; 8 9 let { item }: { item: Item } = $props(); 10 ··· 65 </button> 66 {/snippet} 67 </PopoverEmojiPicker> 68 </div>
-8
src/lib/cards/StatusphereCard/StatusphereCard.svelte
··· 22 {:else} 23 No status yet 24 {/if} 25 - 26 - {#if item.cardData.title} 27 - <div 28 - class="text-base-900 dark:text-base-50 text-md bg-base-200/30 dark:bg-base-900/30 absolute top-2 right-2 left-2 z-30 line-clamp-1 rounded-lg p-1 font-bold backdrop-blur-md" 29 - > 30 - {item.cardData.title} 31 - </div> 32 - {/if} 33 </div>
··· 22 {:else} 23 No status yet 24 {/if} 25 </div>
+8 -1
src/lib/cards/StatusphereCard/index.ts
··· 40 } 41 42 return item; 43 - } 44 } as CardDefinition & { type: 'statusphere' }; 45 46 export function emojiToNotoAnimatedWebp(emoji: string | undefined): string | undefined {
··· 40 } 41 42 return item; 43 + }, 44 + 45 + migrate: (item) => { 46 + if (item.cardData.title && !item.cardData.label) { 47 + item.cardData.label = item.cardData.title; 48 + } 49 + }, 50 + canHaveLabel: true 51 } as CardDefinition & { type: 'statusphere' }; 52 53 export function emojiToNotoAnimatedWebp(emoji: string | undefined): string | undefined {
+2 -1
src/lib/cards/TealFMPlaysCard/index.ts
··· 21 return data; 22 }, 23 minW: 4, 24 - sidebarButtonText: 'teal.fm Plays' 25 } as CardDefinition & { type: 'recentTealFMPlays' };
··· 21 return data; 22 }, 23 minW: 4, 24 + sidebarButtonText: 'teal.fm Plays', 25 + canHaveLabel: true 26 } as CardDefinition & { type: 'recentTealFMPlays' };
+1 -1
src/lib/cards/helper.ts
··· 15 } 16 17 export function getHexOfCardColor(item: Item) { 18 - let color = 19 !item.color || item.color === 'transparent' || item.color === 'base' ? 'accent' : item.color; 20 21 return convertCSSToHex(getCSSVar(`--color-${color}-500`));
··· 15 } 16 17 export function getHexOfCardColor(item: Item) { 18 + const color = 19 !item.color || item.color === 'transparent' || item.color === 'base' ? 'accent' : item.color; 20 21 return convertCSSToHex(getCSSVar(`--color-${color}-500`));
+5
src/lib/cards/types.ts
··· 19 20 export type ContentComponentProps = { 21 item: Item; 22 }; 23 24 export type CardDefinition = { ··· 69 change?: (item: Item) => Item; 70 71 name?: string; 72 };
··· 19 20 export type ContentComponentProps = { 21 item: Item; 22 + isEditing?: boolean; 23 }; 24 25 export type CardDefinition = { ··· 70 change?: (item: Item) => Item; 71 72 name?: string; 73 + 74 + canHaveLabel?: boolean; 75 + 76 + migrate?: (item: Item) => void; 77 };
+2 -5
src/lib/cards/utils/PlainTextEditor.svelte
··· 74 75 <style> 76 :global(.tiptap p.is-editor-empty:first-child::before) { 77 - color: var(--color-base-800); 78 content: attr(data-placeholder); 79 - opacity: 50%; 80 float: left; 81 height: 0; 82 pointer-events: none; 83 - } 84 - :global(.dark .tiptap p.is-editor-empty:first-child::before) { 85 - color: var(--color-base-200); 86 } 87 </style>
··· 74 75 <style> 76 :global(.tiptap p.is-editor-empty:first-child::before) { 77 + color: var(--color-base-500); 78 content: attr(data-placeholder); 79 + opacity: 100%; 80 float: left; 81 height: 0; 82 pointer-events: none; 83 } 84 </style>
+12 -1
src/lib/website/load.ts
··· 163 return data; 164 } 165 166 function checkData(data: WebsiteData): WebsiteData { 167 data = migrateData(data); 168 ··· 182 } 183 184 function migrateData(data: WebsiteData): WebsiteData { 185 - return migrateFromV1ToV2(migrateFromV0ToV1(data)); 186 }
··· 163 return data; 164 } 165 166 + function migrateCards(data: WebsiteData): WebsiteData { 167 + for (const card of data.cards) { 168 + const cardDef = CardDefinitionsByType[card.cardType]; 169 + 170 + if (!cardDef?.migrate) continue; 171 + 172 + cardDef.migrate(card); 173 + } 174 + return data; 175 + } 176 + 177 function checkData(data: WebsiteData): WebsiteData { 178 data = migrateData(data); 179 ··· 193 } 194 195 function migrateData(data: WebsiteData): WebsiteData { 196 + return migrateCards(migrateFromV1ToV2(migrateFromV0ToV1(data))); 197 }