your personal website on atproto - mirror blento.app

Merge pull request #57 from flo-bit/theme-colors

add theme colors selection

authored by Florian and committed by GitHub 985b5df9 77a880cb

+274 -52
+9 -7
docs/Beta.md
··· 3 - site.standard 4 - move description to markdownDescription and set description as text only 5 6 - big button card 7 8 - card with big call to action button 9 10 - - link card allow changing favicon, og image (+ hide favicon) 11 - 12 - video card? 13 14 - allow setting base and accent color 15 16 - ask to fill with some default cards on page creation 17 18 - - share button (copy share link to blento, maybe post to bluesky?) 19 - 20 - - add icons to "change card to..." popover 21 - 22 - when adding images try to add them in a size that best fits aspect ratio 23 24 - onboarding 25 26 - - show alert when user tries to close window with unsaved changes
··· 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 - big button card 15 16 - card with big call to action button 17 18 - video card? 19 20 - allow setting base and accent color 21 22 - ask to fill with some default cards on page creation 23 24 - when adding images try to add them in a size that best fits aspect ratio 25 26 - onboarding 27 28 + - fix invalid handle thing
+12 -7
docs/CardIdeas.md
··· 3 ## media 4 5 - general video card 6 - - inline youtube video 7 - cartoons: aka https://www.opendoodles.com/ 8 - excalidraw (/svg card) 9 - latest blog post (e.g. leaflet) 10 - fake 3d image (with depth map) 11 - - fluid text effect (https://flo-bit.dev/projects/fluid-text-effect/) 12 - - gifs 13 - - little drawing app 14 - css voxel art 15 - 3d model 16 17 ## social accounts 18 19 - instagram card (showing follow button, follower count, latest posts) 20 - - github card (showing activity grid) 21 - bluesky account card (showing follow button, follower count, avatar, name, cover image) 22 - youtube channel card (showing channel name, latest videos, follow button?) 23 - bluesky posts workcloud ··· 40 - teal.fm 41 - [x] last played songs 42 - tangled.sh 43 - popfeed.social 44 - reading goal 45 - [x] latest ratings 46 - lists 47 - - smokesignal.events (https://pdsls.dev/at://did:plc:xbtmt2zjwlrfegqvch7fboei/events.smokesignal.calendar.event/3ltn2qrxf3626) 48 - - statusphere.xyz (https://googlefonts.github.io/noto-emoji-animation/, https://gist.github.com/sanjacob/a0ccdf6d88f15bf158d8895090722d14) 49 - goals.garden 50 - flashes.blue (https://pdsls.dev/at://did:plc:aytgljyikzbtgrnac2u4ccft/blue.flashes.actor.portfolio, https://app.flashes.blue/profile/j4ck.xyz) 51 - room: flo-bit.dev/room
··· 3 ## media 4 5 - general video card 6 + - [x] inline youtube video 7 - cartoons: aka https://www.opendoodles.com/ 8 - excalidraw (/svg card) 9 - latest blog post (e.g. leaflet) 10 - fake 3d image (with depth map) 11 + - [x] fluid text effect (https://flo-bit.dev/projects/fluid-text-effect/) 12 + - [x] gifs 13 + - [x] little drawing app 14 - css voxel art 15 - 3d model 16 + - spotify or apple music playlist 17 18 ## social accounts 19 20 - instagram card (showing follow button, follower count, latest posts) 21 + - [x] github card (showing activity grid) 22 - bluesky account card (showing follow button, follower count, avatar, name, cover image) 23 - youtube channel card (showing channel name, latest videos, follow button?) 24 - bluesky posts workcloud ··· 41 - teal.fm 42 - [x] last played songs 43 - tangled.sh 44 + - pinned repos 45 + - activity heatmap? 46 - popfeed.social 47 - reading goal 48 - [x] latest ratings 49 - lists 50 + - smokesignal.events 51 + - [x] specific event 52 + - all future events i'm hosting/attending 53 + - [x] statusphere.xyz (TODO: assing to specific record) 54 - goals.garden 55 - flashes.blue (https://pdsls.dev/at://did:plc:aytgljyikzbtgrnac2u4ccft/blue.flashes.actor.portfolio, https://app.flashes.blue/profile/j4ck.xyz) 56 - room: flo-bit.dev/room
+101
src/lib/components/select-theme/SelectTheme.svelte
···
··· 1 + <script lang="ts"> 2 + import { Paragraph } from '@foxui/core'; 3 + import { ColorSelect } from '@foxui/colors'; 4 + 5 + let accentColors = [ 6 + { class: 'text-red-500', label: 'red' }, 7 + { class: 'text-orange-500', label: 'orange' }, 8 + { class: 'text-amber-500', label: 'amber' }, 9 + { class: 'text-yellow-500', label: 'yellow' }, 10 + { class: 'text-lime-500', label: 'lime' }, 11 + { class: 'text-green-500', label: 'green' }, 12 + { class: 'text-emerald-500', label: 'emerald' }, 13 + { class: 'text-teal-500', label: 'teal' }, 14 + { class: 'text-cyan-500', label: 'cyan' }, 15 + { class: 'text-sky-500', label: 'sky' }, 16 + { class: 'text-blue-500', label: 'blue' }, 17 + { class: 'text-indigo-500', label: 'indigo' }, 18 + { class: 'text-violet-500', label: 'violet' }, 19 + { class: 'text-purple-500', label: 'purple' }, 20 + { class: 'text-fuchsia-500', label: 'fuchsia' }, 21 + { class: 'text-pink-500', label: 'pink' }, 22 + { class: 'text-rose-500', label: 'rose' } 23 + ]; 24 + 25 + let baseColors = [ 26 + { class: 'text-gray-500', label: 'gray' }, 27 + { class: 'text-stone-500', label: 'stone' }, 28 + { class: 'text-zinc-500', label: 'zinc' }, 29 + { class: 'text-neutral-500', label: 'neutral' }, 30 + { class: 'text-slate-500', label: 'slate' } 31 + ]; 32 + 33 + let { 34 + accentColor = $bindable('pink'), 35 + baseColor = $bindable('stone'), 36 + selectAccentColor = true, 37 + selectBaseColor = true, 38 + onchanged 39 + }: { 40 + accentColor?: string; 41 + baseColor?: string; 42 + selectAccentColor?: boolean; 43 + selectBaseColor?: boolean; 44 + onchanged?: (accentColor: string, baseColor: string) => void; 45 + } = $props(); 46 + 47 + let selectedAccentColor = $derived( 48 + accentColors.find((c) => c.label === accentColor) ?? accentColors[15] 49 + ); 50 + 51 + let selectedBaseColor = $derived(baseColors.find((c) => c.label === baseColor) ?? baseColors[1]); 52 + </script> 53 + 54 + {#if selectAccentColor} 55 + <Paragraph class="mb-2">Accent Color</Paragraph> 56 + <ColorSelect 57 + selected={selectedAccentColor} 58 + colors={accentColors} 59 + onselected={(color, previous) => { 60 + if (typeof previous === 'string' || typeof color === 'string') { 61 + return; 62 + } 63 + 64 + document.documentElement.classList.remove(previous.label.toLowerCase()); 65 + document.documentElement.classList.add(color.label.toLowerCase()); 66 + 67 + accentColor = color.label; 68 + 69 + window.dispatchEvent( 70 + new CustomEvent('theme-changed', { detail: { accentColor: color.label } }) 71 + ); 72 + 73 + onchanged?.(accentColor, baseColor); 74 + }} 75 + class="w-64" 76 + /> 77 + {/if} 78 + 79 + {#if selectBaseColor} 80 + <Paragraph class="mt-4 mb-2">Base Color</Paragraph> 81 + <ColorSelect 82 + selected={selectedBaseColor} 83 + colors={baseColors} 84 + onselected={(color, previous) => { 85 + if (typeof previous === 'string' || typeof color === 'string') { 86 + return; 87 + } 88 + 89 + document.documentElement.classList.remove(previous.label.toLowerCase()); 90 + document.documentElement.classList.add(color.label.toLowerCase()); 91 + 92 + baseColor = color.label; 93 + 94 + window.dispatchEvent( 95 + new CustomEvent('theme-changed', { detail: { baseColor: color.label } }) 96 + ); 97 + 98 + onchanged?.(accentColor, baseColor); 99 + }} 100 + /> 101 + {/if}
+43
src/lib/components/select-theme/SelectThemePopover.svelte
···
··· 1 + <script lang="ts"> 2 + import { buttonVariants, Popover, cn } from '@foxui/core'; 3 + import SelectTheme from './SelectTheme.svelte'; 4 + 5 + let { 6 + accentColor = $bindable('pink'), 7 + baseColor = $bindable('stone'), 8 + selectAccentColor = true, 9 + selectBaseColor = true, 10 + onchanged 11 + }: { 12 + accentColor?: string; 13 + baseColor?: string; 14 + selectAccentColor?: boolean; 15 + selectBaseColor?: boolean; 16 + onchanged?: (accentColor: string, baseColor: string) => void; 17 + } = $props(); 18 + </script> 19 + 20 + <Popover> 21 + {#snippet child({ props })} 22 + <button 23 + {...props} 24 + class={cn( 25 + buttonVariants({ variant: 'link', size: 'default' }), 26 + 'flex cursor-pointer items-center gap-0 -space-x-2 backdrop-blur-none' 27 + )} 28 + > 29 + {#if selectAccentColor} 30 + <div 31 + class=" from-accent-500 to-accent-600 border-accent-700 dark:border-accent-400 z-10 size-6 rounded-full border bg-linear-to-b" 32 + ></div> 33 + {/if} 34 + 35 + {#if selectBaseColor} 36 + <div 37 + class=" from-base-500 to-base-600 border-base-700 dark:border-base-400 size-6 rounded-full border bg-linear-to-b" 38 + ></div> 39 + {/if} 40 + </button> 41 + {/snippet} 42 + <SelectTheme bind:accentColor bind:baseColor {selectAccentColor} {selectBaseColor} {onchanged} /> 43 + </Popover>
+2
src/lib/components/select-theme/index.ts
···
··· 1 + export { default as SelectTheme } from './SelectTheme.svelte'; 2 + export { default as SelectThemePopover } from './SelectThemePopover.svelte';
+4
src/lib/types.ts
··· 51 52 // 'side' (default on desktop) or 'top' (always top like mobile view) 53 profilePosition?: 'side' | 'top'; 54 }; 55 }; 56 profile: AppBskyActorDefs.ProfileViewDetailed;
··· 51 52 // 'side' (default on desktop) or 'top' (always top like mobile view) 53 profilePosition?: 'side' | 'top'; 54 + 55 + // theme colors 56 + accentColor?: string; 57 + baseColor?: string; 58 }; 59 }; 60 profile: AppBskyActorDefs.ProfileViewDetailed;
+19 -4
src/lib/website/EditableProfile.svelte
··· 5 import MarkdownTextEditor from '$lib/components/MarkdownTextEditor.svelte'; 6 import { Avatar, Button } from '@foxui/core'; 7 import { getIsMobile } from './context'; 8 - import type { Editor } from '@tiptap/core'; 9 import MadeWithBlento from './MadeWithBlento.svelte'; 10 11 let { data = $bindable(), hideBlento = false }: { data: WebsiteData; hideBlento?: boolean } = 12 $props(); 13 14 let profilePosition = $derived(getProfilePosition(data)); 15 ··· 130 {/if} 131 </Button> 132 {/if} 133 </div> 134 135 <div 136 class={[ 137 - 'flex flex-col gap-4 pt-16 pb-8', 138 profilePosition === 'side' && '@5xl/wrapper:h-screen @5xl/wrapper:pt-24' 139 ]} 140 > ··· 215 /> 216 {/if} 217 </div> 218 - 219 - <div class={['h-10.5 w-1', profilePosition === 'side' && '@5xl/wrapper:hidden']}></div> 220 221 {#if !hideBlento} 222 <MadeWithBlento class="hidden {profilePosition === 'side' && '@5xl/wrapper:block'}" />
··· 5 import MarkdownTextEditor from '$lib/components/MarkdownTextEditor.svelte'; 6 import { Avatar, Button } from '@foxui/core'; 7 import { getIsMobile } from './context'; 8 import MadeWithBlento from './MadeWithBlento.svelte'; 9 + import { SelectThemePopover } from '$lib/components/select-theme'; 10 11 let { data = $bindable(), hideBlento = false }: { data: WebsiteData; hideBlento?: boolean } = 12 $props(); 13 + 14 + let accentColor = $derived(data.publication?.preferences?.accentColor ?? 'pink'); 15 + let baseColor = $derived(data.publication?.preferences?.baseColor ?? 'stone'); 16 + 17 + function updateTheme(newAccent: string, newBase: string) { 18 + data.publication.preferences ??= {}; 19 + data.publication.preferences.accentColor = newAccent; 20 + data.publication.preferences.baseColor = newBase; 21 + data = { ...data }; 22 + } 23 24 let profilePosition = $derived(getProfilePosition(data)); 25 ··· 140 {/if} 141 </Button> 142 {/if} 143 + 144 + <!-- Theme selection --> 145 + <SelectThemePopover 146 + {accentColor} 147 + {baseColor} 148 + onchanged={(newAccent, newBase) => updateTheme(newAccent, newBase)} 149 + /> 150 </div> 151 152 <div 153 class={[ 154 + 'flex flex-col gap-4 pt-16 pb-4', 155 profilePosition === 'side' && '@5xl/wrapper:h-screen @5xl/wrapper:pt-24' 156 ]} 157 > ··· 232 /> 233 {/if} 234 </div> 235 236 {#if !hideBlento} 237 <MadeWithBlento class="hidden {profilePosition === 'side' && '@5xl/wrapper:block'}" />
+48 -30
src/lib/website/EditableWebsite.svelte
··· 29 import BaseEditingCard from '../cards/BaseCard/BaseEditingCard.svelte'; 30 import Context from './Context.svelte'; 31 import Head from './Head.svelte'; 32 - import { compressImage } from '../helper'; 33 import Account from './Account.svelte'; 34 import EditBar from './EditBar.svelte'; 35 import SaveModal from './SaveModal.svelte'; 36 import FloatingEditButton from './FloatingEditButton.svelte'; ··· 45 46 // Check if floating login button will be visible (to hide MadeWithBlento) 47 const showLoginOnEditPage = $derived(!user.isInitializing && !user.isLoggedIn); 48 49 let imageDragOver = $state(false); 50 ··· 572 favicon={data.profile.avatar ?? null} 573 title={getName(data)} 574 image={'/' + data.handle + '/og.png'} 575 /> 576 577 <Account {data} /> ··· 631 ]} 632 > 633 {#if getHideProfileSection(data)} 634 - <Button 635 - size="icon" 636 - variant="ghost" 637 - onclick={() => { 638 - data.publication.preferences ??= {}; 639 - data.publication.preferences.hideProfileSection = false; 640 - data = { ...data }; 641 - }} 642 - class="pointer-events-auto absolute top-2 left-2 z-20" 643 - > 644 - <svg 645 - xmlns="http://www.w3.org/2000/svg" 646 - fill="none" 647 - viewBox="0 0 24 24" 648 - stroke-width="1.5" 649 - stroke="currentColor" 650 - class="size-6" 651 > 652 - <path 653 - stroke-linecap="round" 654 - stroke-linejoin="round" 655 - d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z" 656 - /> 657 - <path 658 - stroke-linecap="round" 659 - stroke-linejoin="round" 660 - d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" 661 - /> 662 - </svg> 663 - </Button> 664 {/if} 665 <div class="pointer-events-none"></div> 666 <!-- svelte-ignore a11y_no_static_element_interactions -->
··· 29 import BaseEditingCard from '../cards/BaseCard/BaseEditingCard.svelte'; 30 import Context from './Context.svelte'; 31 import Head from './Head.svelte'; 32 import Account from './Account.svelte'; 33 + import { SelectThemePopover } from '$lib/components/select-theme'; 34 import EditBar from './EditBar.svelte'; 35 import SaveModal from './SaveModal.svelte'; 36 import FloatingEditButton from './FloatingEditButton.svelte'; ··· 45 46 // Check if floating login button will be visible (to hide MadeWithBlento) 47 const showLoginOnEditPage = $derived(!user.isInitializing && !user.isLoggedIn); 48 + 49 + let accentColor = $derived(data.publication?.preferences?.accentColor ?? 'pink'); 50 + let baseColor = $derived(data.publication?.preferences?.baseColor ?? 'stone'); 51 + 52 + function updateTheme(newAccent: string, newBase: string) { 53 + data.publication.preferences ??= {}; 54 + data.publication.preferences.accentColor = newAccent; 55 + data.publication.preferences.baseColor = newBase; 56 + data = { ...data }; 57 + } 58 59 let imageDragOver = $state(false); 60 ··· 582 favicon={data.profile.avatar ?? null} 583 title={getName(data)} 584 image={'/' + data.handle + '/og.png'} 585 + accentColor={data.publication?.preferences?.accentColor} 586 + baseColor={data.publication?.preferences?.baseColor} 587 /> 588 589 <Account {data} /> ··· 643 ]} 644 > 645 {#if getHideProfileSection(data)} 646 + <div class="pointer-events-auto absolute top-2 left-2 z-20 flex gap-2"> 647 + <Button 648 + size="icon" 649 + variant="ghost" 650 + onclick={() => { 651 + data.publication.preferences ??= {}; 652 + data.publication.preferences.hideProfileSection = false; 653 + data = { ...data }; 654 + }} 655 > 656 + <svg 657 + xmlns="http://www.w3.org/2000/svg" 658 + fill="none" 659 + viewBox="0 0 24 24" 660 + stroke-width="1.5" 661 + stroke="currentColor" 662 + class="size-6" 663 + > 664 + <path 665 + stroke-linecap="round" 666 + stroke-linejoin="round" 667 + d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z" 668 + /> 669 + <path 670 + stroke-linecap="round" 671 + stroke-linejoin="round" 672 + d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" 673 + /> 674 + </svg> 675 + </Button> 676 + <SelectThemePopover 677 + {accentColor} 678 + {baseColor} 679 + onchanged={(newAccent, newBase) => updateTheme(newAccent, newBase)} 680 + /> 681 + </div> 682 {/if} 683 <div class="pointer-events-none"></div> 684 <!-- svelte-ignore a11y_no_static_element_interactions -->
+15 -3
src/lib/website/Head.svelte
··· 1 <script lang="ts"> 2 let { 3 favicon, 4 title, 5 image, 6 - description 7 - }: { favicon: string | null; title: string | null; image?: string; description?: string } = 8 - $props(); 9 </script> 10 11 <svelte:head> 12 {#if favicon}
··· 1 <script lang="ts"> 2 + import ThemeScript from './ThemeScript.svelte'; 3 + 4 let { 5 favicon, 6 title, 7 image, 8 + description, 9 + accentColor, 10 + baseColor 11 + }: { 12 + favicon: string | null; 13 + title: string | null; 14 + image?: string; 15 + description?: string; 16 + accentColor?: string; 17 + baseColor?: string; 18 + } = $props(); 19 </script> 20 + 21 + <ThemeScript {accentColor} {baseColor} /> 22 23 <svelte:head> 24 {#if favicon}
+1 -1
src/lib/website/Profile.svelte
··· 34 > 35 <div 36 class={[ 37 - 'flex flex-col gap-4 pt-16 pb-8', 38 profilePosition === 'side' && '@5xl/wrapper:h-screen @5xl/wrapper:pt-24' 39 ]} 40 >
··· 34 > 35 <div 36 class={[ 37 + 'flex flex-col gap-4 pt-16 pb-4', 38 profilePosition === 'side' && '@5xl/wrapper:h-screen @5xl/wrapper:pt-24' 39 ]} 40 >
+18
src/lib/website/ThemeScript.svelte
···
··· 1 + <script lang="ts"> 2 + let { 3 + accentColor = 'pink', 4 + baseColor = 'stone' 5 + }: { 6 + accentColor?: string; 7 + baseColor?: string; 8 + } = $props(); 9 + 10 + let script = $derived( 11 + `<script>(function(){document.documentElement.classList.add(${JSON.stringify(accentColor)},${JSON.stringify(baseColor)});})();<` + 12 + '/script>' 13 + ); 14 + </script> 15 + 16 + <svelte:head> 17 + {@html script} 18 + </svelte:head>
+2
src/lib/website/Website.svelte
··· 60 title={getName(data)} 61 image={'/' + data.handle + '/og.png'} 62 description={getDescription(data)} 63 /> 64 65 <Context {data}>
··· 60 title={getName(data)} 61 image={'/' + data.handle + '/og.png'} 62 description={getDescription(data)} 63 + accentColor={data.publication?.preferences?.accentColor} 64 + baseColor={data.publication?.preferences?.baseColor} 65 /> 66 67 <Context {data}>