JavaScript-optional public web frontend for Bluesky
anartia.kelinci.net
sveltekit
atcute
bluesky
typescript
svelte
1<script lang="ts" module>
2 const DEFAULT_RATIO = { width: 16, height: 9 };
3</script>
4
5<script lang="ts">
6 import type { AppBskyEmbedImages } from '@atcute/bluesky';
7
8 import { trimRichText } from '$lib/utils/bluesky/richtext';
9
10 import ImageAlt from './components/image-alt.svelte';
11
12 interface Props {
13 embed: AppBskyEmbedImages.View;
14 borderless?: boolean;
15 standalone?: boolean;
16 blur?: boolean;
17 }
18
19 const { embed, borderless, standalone, blur }: Props = $props();
20
21 const images = $derived(embed.images);
22 const length = $derived(images.length);
23</script>
24
25<div class={['image-embed', !borderless && 'is-bordered', standalone && length === 1 && 'is-aligned']}>
26 {#if length === 4}
27 <div class="grid">
28 <div class="col">
29 <div class="item wide tl">
30 {@render Image(0)}
31 </div>
32 <div class="item wide bl">
33 {@render Image(2)}
34 </div>
35 </div>
36 <div class="col">
37 <div class="item wide tr">
38 {@render Image(1)}
39 </div>
40 <div class="item wide br">
41 {@render Image(3)}
42 </div>
43 </div>
44 </div>
45 {:else if length === 3}
46 <div class="grid">
47 <div class="col square">
48 <div class="item tl bl">
49 {@render Image(0)}
50 </div>
51 </div>
52 <div class="col square">
53 <div class="item tr">
54 {@render Image(1)}
55 </div>
56 <div class="item br">
57 {@render Image(2)}
58 </div>
59 </div>
60 </div>
61 {:else if length === 2}
62 <div class="grid">
63 <div class="col">
64 <div class="item square tl bl">
65 {@render Image(0)}
66 </div>
67 </div>
68 <div class="col">
69 <div class="item square tr br">
70 {@render Image(1)}
71 </div>
72 </div>
73 </div>
74 {:else if length === 1}
75 {@const ratio = standalone && (images[0].aspectRatio || DEFAULT_RATIO)}
76
77 <div
78 class={['single-item tl tr bl br', ratio && 'is-standalone', ratio === DEFAULT_RATIO && 'is-defaulted']}
79 style={ratio ? `aspect-ratio: ${ratio.width}/${ratio.height}` : ``}
80 >
81 {@render Image(0)}
82
83 {#if ratio}
84 <div class="placeholder"></div>
85 {/if}
86 </div>
87 {/if}
88</div>
89
90{#snippet Image(index: number)}
91 {@const image = images[index]}
92 {@const alt = trimRichText(image.alt)}
93
94 {#if standalone}
95 <a href={image.fullsize.replace('@jpeg', '@png')} target="_blank" rel="noopener" class="image-wrapper">
96 <img loading="lazy" src={image.thumb} {alt} class={`image` + (blur ? ` is-blurred` : ``)} />
97 </a>
98 {:else}
99 <div class="image-wrapper">
100 <img loading="lazy" src={image.thumb} {alt} class={`image` + (blur ? ` is-blurred` : ``)} />
101 </div>
102 {/if}
103
104 {#if standalone && alt}
105 <ImageAlt {alt} />
106 {/if}
107{/snippet}
108
109<style>
110 .is-aligned {
111 align-self: baseline;
112 max-width: 100%;
113 }
114
115 .grid {
116 display: flex;
117 gap: 2px;
118 }
119 .col {
120 display: flex;
121 flex: 1;
122 flex-direction: column;
123 gap: 2px;
124 }
125
126 .square {
127 aspect-ratio: 1;
128 }
129 .wide {
130 aspect-ratio: 1.5;
131 }
132
133 .item {
134 position: relative;
135 flex-grow: 1;
136 flex-shrink: 0;
137 overflow: hidden;
138 }
139
140 .is-bordered {
141 .tl,
142 .tr,
143 .bl,
144 .br {
145 border: 1px solid var(--divider-md);
146 }
147
148 .tl {
149 border-top-left-radius: 6px;
150 }
151 .tr {
152 border-top-right-radius: 6px;
153 }
154 .bl {
155 border-bottom-left-radius: 6px;
156 }
157 .br {
158 border-bottom-right-radius: 6px;
159 }
160 }
161
162 .single-item {
163 position: relative;
164 aspect-ratio: 16 / 9;
165 overflow: hidden;
166 }
167 .is-standalone {
168 min-width: 64px;
169 max-width: 100%;
170 min-height: 64px;
171 max-height: 320px;
172 }
173
174 .item,
175 .single-item {
176 &:has(.image-wrapper:focus-visible) {
177 outline: 2px solid var(--accent);
178 outline-offset: -1px;
179 }
180 }
181
182 .image-wrapper {
183 position: absolute;
184 inset: 0;
185 outline: none;
186 }
187 .image {
188 background: var(--bg-slate);
189 width: 100%;
190 height: 100%;
191 object-fit: cover;
192 font-size: 0px;
193 }
194 .single-item .image {
195 object-fit: contain;
196 }
197 .is-blurred {
198 scale: 125%;
199 filter: blur(24px);
200 }
201
202 .placeholder {
203 width: 100vw;
204 height: 100vh;
205 }
206</style>