your personal website on atproto - mirror blento.app

Merge pull request #130 from jycouet/feat/contrib-shapes

Github contrib

authored by Florian and committed by GitHub 90df684f 4feffd02

+103 -20
+41 -18
src/lib/cards/GitHubContributorsCard/GitHubContributorsCard.svelte
··· 13 13 let owner: string = $derived(item.cardData.owner ?? ''); 14 14 let repo: string = $derived(item.cardData.repo ?? ''); 15 15 let repoKey: string = $derived(owner && repo ? `${owner}/${repo}` : ''); 16 + let layout: 'grid' | 'cinema' = $derived(item.cardData.layout ?? 'grid'); 17 + let shape: 'square' | 'circle' = $derived(item.cardData.shape ?? 'square'); 16 18 17 19 let serverContributors: GitHubContributor[] = $derived.by(() => { 18 20 if (!repoKey) return []; ··· 58 60 59 61 const GAP = 6; 60 62 const MIN_SIZE = 16; 61 - const MAX_SIZE = 80; 63 + const MAX_SIZE = 120; 62 64 63 - function hexCapacity(size: number, availW: number, availH: number): number { 65 + function cinemaCapacity(size: number, availW: number, availH: number): number { 64 66 const colsWide = Math.floor((availW + GAP) / (size + GAP)); 65 67 if (colsWide < 1) return 0; 66 68 const colsNarrow = Math.max(1, colsWide - 1); 67 69 const maxRows = Math.floor((availH + GAP) / (size + GAP)); 68 70 let capacity = 0; 71 + // Pattern: narrow, wide, narrow, wide... (row 0 is narrow) 69 72 for (let r = 0; r < maxRows; r++) { 70 - capacity += r % 2 === 0 ? colsWide : colsNarrow; 73 + capacity += r % 2 === 0 ? colsNarrow : colsWide; 71 74 } 72 75 return capacity; 76 + } 77 + 78 + function gridCapacity(size: number, availW: number, availH: number): number { 79 + const cols = Math.floor((availW + GAP) / (size + GAP)); 80 + const rows = Math.floor((availH + GAP) / (size + GAP)); 81 + return cols * rows; 73 82 } 74 83 75 84 let computedSize = $derived.by(() => { ··· 77 86 78 87 let lo = MIN_SIZE; 79 88 let hi = MAX_SIZE; 89 + const capacityFn = layout === 'cinema' ? cinemaCapacity : gridCapacity; 80 90 81 91 while (lo <= hi) { 82 92 const mid = Math.floor((lo + hi) / 2); 83 - const availW = containerWidth - mid; 84 - const availH = containerHeight - mid; 93 + const availW = containerWidth - (layout === 'cinema' ? mid / 2 : 0); 94 + const availH = containerHeight - (layout === 'cinema' ? mid / 2 : 0); 85 95 if (availW <= 0 || availH <= 0) { 86 96 hi = mid - 1; 87 97 continue; 88 98 } 89 - if (hexCapacity(mid, availW, availH) >= totalItems) { 99 + if (capacityFn(mid, availW, availH) >= totalItems) { 90 100 lo = mid + 1; 91 101 } else { 92 102 hi = mid - 1; ··· 96 106 return Math.max(MIN_SIZE, hi); 97 107 }); 98 108 99 - let padding = $derived(computedSize / 2); 109 + let padding = $derived(layout === 'cinema' ? computedSize / 4 : 0); 100 110 101 111 let rows = $derived.by(() => { 102 - const availW = containerWidth - computedSize; 112 + const availW = containerWidth - (layout === 'cinema' ? computedSize / 4 : 0); 103 113 if (availW <= 0) return [] as GitHubContributor[][]; 114 + 104 115 const colsWide = Math.floor((availW + GAP) / (computedSize + GAP)); 105 - const colsNarrow = Math.max(1, colsWide - 1); 116 + const colsNarrow = layout === 'cinema' ? Math.max(1, colsWide - 1) : colsWide; 117 + 118 + // Calculate row sizes from bottom up, then reverse for incomplete row at top 119 + const rowSizes: number[] = []; 120 + let remaining = namedContributors.length; 121 + let rowNum = 0; 122 + while (remaining > 0) { 123 + const cols = layout === 'cinema' && rowNum % 2 === 0 ? colsNarrow : colsWide; 124 + rowSizes.push(Math.min(cols, remaining)); 125 + remaining -= cols; 126 + rowNum++; 127 + } 128 + rowSizes.reverse(); 106 129 130 + // Fill rows with contributors in order 107 131 const result: GitHubContributor[][] = []; 108 132 let idx = 0; 109 - let rowNum = 0; 110 - while (idx < namedContributors.length) { 111 - const cols = rowNum % 2 === 0 ? colsWide : colsNarrow; 112 - result.push(namedContributors.slice(idx, idx + cols)); 113 - idx += cols; 114 - rowNum++; 133 + for (const size of rowSizes) { 134 + result.push(namedContributors.slice(idx, idx + size)); 135 + idx += size; 115 136 } 116 137 return result; 117 138 }); ··· 119 140 let textSize = $derived( 120 141 computedSize < 24 ? 'text-[10px]' : computedSize < 40 ? 'text-xs' : 'text-sm' 121 142 ); 143 + 144 + let shapeClass = $derived(shape === 'circle' ? 'rounded-full' : 'rounded-lg'); 122 145 </script> 123 146 124 147 <div ··· 142 165 href="https://github.com/{contributor.username}" 143 166 target="_blank" 144 167 rel="noopener noreferrer" 145 - class="accent:ring-accent-500 block rounded-full ring-2 ring-white transition-transform hover:scale-110 dark:ring-neutral-900" 168 + class="accent:ring-accent-500 block {shapeClass} ring-2 ring-white transition-transform hover:scale-110 dark:ring-neutral-900" 146 169 > 147 170 {#if contributor.avatarUrl} 148 171 <img 149 172 src={contributor.avatarUrl} 150 173 alt={contributor.username} 151 - class="rounded-full object-cover" 174 + class="{shapeClass} object-cover" 152 175 style="width: {computedSize}px; height: {computedSize}px;" 153 176 /> 154 177 {:else} 155 178 <div 156 - class="bg-base-200 dark:bg-base-700 accent:bg-accent-400 flex items-center justify-center rounded-full" 179 + class="bg-base-200 dark:bg-base-700 accent:bg-accent-400 flex items-center justify-center {shapeClass}" 157 180 style="width: {computedSize}px; height: {computedSize}px;" 158 181 > 159 182 <span
+59
src/lib/cards/GitHubContributorsCard/GitHubContributorsCardSettings.svelte
··· 1 + <script lang="ts"> 2 + import type { SettingsComponentProps } from '../types'; 3 + import { Label } from '@foxui/core'; 4 + 5 + let { item = $bindable() }: SettingsComponentProps = $props(); 6 + 7 + const layoutOptions = [ 8 + { value: 'grid', label: 'Grid' }, 9 + { value: 'cinema', label: 'Cinema' } 10 + ]; 11 + 12 + const shapeOptions = [ 13 + { value: 'square', label: 'Square' }, 14 + { value: 'circle', label: 'Circle' } 15 + ]; 16 + 17 + let layout = $derived(item.cardData.layout ?? 'grid'); 18 + let shape = $derived(item.cardData.shape ?? 'square'); 19 + </script> 20 + 21 + <div class="flex flex-col gap-4"> 22 + <div class="flex flex-col gap-2"> 23 + <Label>Layout</Label> 24 + <div class="flex gap-2"> 25 + {#each layoutOptions as opt (opt.value)} 26 + <button 27 + class={[ 28 + 'flex-1 rounded-xl border px-3 py-2 text-sm transition-colors', 29 + layout === opt.value 30 + ? 'bg-accent-500 border-accent-500 text-white' 31 + : 'bg-base-100 dark:bg-base-800 border-base-300 dark:border-base-700 text-base-900 dark:text-base-100 hover:border-accent-400' 32 + ]} 33 + onclick={() => (item.cardData.layout = opt.value)} 34 + > 35 + {opt.label} 36 + </button> 37 + {/each} 38 + </div> 39 + </div> 40 + 41 + <div class="flex flex-col gap-2"> 42 + <Label>Shape</Label> 43 + <div class="flex gap-2"> 44 + {#each shapeOptions as opt (opt.value)} 45 + <button 46 + class={[ 47 + 'flex-1 rounded-xl border px-3 py-2 text-sm transition-colors', 48 + shape === opt.value 49 + ? 'bg-accent-500 border-accent-500 text-white' 50 + : 'bg-base-100 dark:bg-base-800 border-base-300 dark:border-base-700 text-base-900 dark:text-base-100 hover:border-accent-400' 51 + ]} 52 + onclick={() => (item.cardData.shape = opt.value)} 53 + > 54 + {opt.label} 55 + </button> 56 + {/each} 57 + </div> 58 + </div> 59 + </div>
+2
src/lib/cards/GitHubContributorsCard/index.ts
··· 1 1 import type { CardDefinition } from '../types'; 2 2 import GitHubContributorsCard from './GitHubContributorsCard.svelte'; 3 3 import CreateGitHubContributorsCardModal from './CreateGitHubContributorsCardModal.svelte'; 4 + import GitHubContributorsCardSettings from './GitHubContributorsCardSettings.svelte'; 4 5 5 6 export type GitHubContributor = { 6 7 username: string; ··· 15 16 type: 'githubContributors', 16 17 contentComponent: GitHubContributorsCard, 17 18 creationModalComponent: CreateGitHubContributorsCardModal, 19 + settingsComponent: GitHubContributorsCardSettings, 18 20 createNew: (card) => { 19 21 card.w = 4; 20 22 card.h = 2;
+1 -2
src/routes/(auth)/oauth/callback/+page.svelte
··· 12 12 goto('/' + getHandleOrDid(user.profile) + '/edit', {}); 13 13 } 14 14 15 - if(!user.isInitializing && !startedErrorTimer) { 15 + if (!user.isInitializing && !startedErrorTimer) { 16 16 startedErrorTimer = true; 17 17 18 18 setTimeout(() => { ··· 30 30 >There was an error signing you in, please go back to the 31 31 <a class="text-accent-600 dark:text-accent-400" href="/">homepage</a> 32 32 and try again. 33 - 34 33 </span> 35 34 </div> 36 35 {/if}