tangled
alpha
login
or
join now
flo-bit.dev
/
blento
your personal website on atproto - mirror
blento.app
20
fork
atom
overview
issues
pulls
pipelines
commit
Florian
3 weeks ago
1b9377e9
8053b848
+355
-222
22 changed files
expand all
collapse all
unified
split
package.json
pnpm-lock.yaml
src
app.html
lib
EditableWebsite.svelte
Profile.svelte
Website.svelte
cards
BaseCard
BaseCard.svelte
BaseEditingCard.svelte
BigSocialCard
BigSocialCard.svelte
CreateBigSocialCardModal.svelte
SidebarItemBigSocialCard.svelte
index.ts
LinkCard
EditingLinkCard.svelte
LinkCard.svelte
SpecialCards
UpdatedBlentos
UpdatedBlentosCard.svelte
TextCard
EditingTextCard.svelte
TextCard.svelte
TextCardSettings.svelte
index.ts
types.ts
utils
MarkdownTextEditor.svelte
components
ImageDropper.svelte
+1
package.json
···
68
"link-preview-js": "^4.0.0",
69
"marked": "^15.0.11",
70
"plyr": "^3.8.4",
0
71
"svelte-sonner": "^1.0.7",
72
"tailwind-merge": "^3.4.0",
73
"tailwind-variants": "^3.2.2",
···
68
"link-preview-js": "^4.0.0",
69
"marked": "^15.0.11",
70
"plyr": "^3.8.4",
71
+
"simple-icons": "^16.5.0",
72
"svelte-sonner": "^1.0.7",
73
"tailwind-merge": "^3.4.0",
74
"tailwind-variants": "^3.2.2",
+9
pnpm-lock.yaml
···
89
plyr:
90
specifier: ^3.8.4
91
version: 3.8.4
0
0
0
92
svelte-sonner:
93
specifier: ^1.0.7
94
version: 1.0.7(svelte@5.45.8)
···
2668
side-channel@1.1.0:
2669
resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==, tarball: https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz}
2670
engines: {node: '>= 0.4'}
0
0
0
0
2671
2672
simple-swizzle@0.2.4:
2673
resolution: {integrity: sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==, tarball: https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz}
···
5438
side-channel-list: 1.0.0
5439
side-channel-map: 1.0.1
5440
side-channel-weakmap: 1.0.2
0
0
5441
5442
simple-swizzle@0.2.4:
5443
dependencies:
···
89
plyr:
90
specifier: ^3.8.4
91
version: 3.8.4
92
+
simple-icons:
93
+
specifier: ^16.5.0
94
+
version: 16.5.0
95
svelte-sonner:
96
specifier: ^1.0.7
97
version: 1.0.7(svelte@5.45.8)
···
2671
side-channel@1.1.0:
2672
resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==, tarball: https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz}
2673
engines: {node: '>= 0.4'}
2674
+
2675
+
simple-icons@16.5.0:
2676
+
resolution: {integrity: sha512-72nn0oHADKx6Hknu7q6M0vfL8LiCUMKABOHane2+4xdqaFBSHfNNBjuZioihiqVQMz7IvVle4NKAM0IlXvl/9A==, tarball: https://registry.npmjs.org/simple-icons/-/simple-icons-16.5.0.tgz}
2677
+
engines: {node: '>=0.12.18'}
2678
2679
simple-swizzle@0.2.4:
2680
resolution: {integrity: sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==, tarball: https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz}
···
5445
side-channel-list: 1.0.0
5446
side-channel-map: 1.0.1
5447
side-channel-weakmap: 1.0.2
5448
+
5449
+
simple-icons@16.5.0: {}
5450
5451
simple-swizzle@0.2.4:
5452
dependencies:
+2
-2
src/app.html
···
1
<!doctype html>
2
-
<html lang="en" class="stone">
3
<head>
4
<meta charset="utf-8" />
5
<meta name="viewport" content="width=device-width, initial-scale=1" />
···
11
data-website-id="c55efa23-9abe-4a7e-b8fd-81b9fa7e8052"
12
></script>
13
</head>
14
-
<body data-sveltekit-preload-data="hover" class="bg-base-200/50 dark:bg-base-950">
15
<div style="display: contents">%sveltekit.body%</div>
16
</body>
17
</html>
···
1
<!doctype html>
2
+
<html lang="en" class="neutral">
3
<head>
4
<meta charset="utf-8" />
5
<meta name="viewport" content="width=device-width, initial-scale=1" />
···
11
data-website-id="c55efa23-9abe-4a7e-b8fd-81b9fa7e8052"
12
></script>
13
</head>
14
+
<body data-sveltekit-preload-data="hover" class="bg-base-50 dark:bg-base-900">
15
<div style="display: contents">%sveltekit.body%</div>
16
</body>
17
</html>
+56
-3
src/lib/EditableWebsite.svelte
···
28
import { setDidContext, setHandleContext } from './website/context';
29
import BaseEditingCard from './cards/BaseCard/BaseEditingCard.svelte';
30
import Settings from './Settings.svelte';
0
31
32
let {
33
handle,
···
234
}
235
</script>
236
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
237
{#if !dev}
238
<div
239
class="bg-base-200 dark:bg-base-800 fixed inset-0 z-50 inline-flex h-full w-full items-center justify-center p-4 text-center lg:hidden"
···
270
>
271
<Profile {handle} {did} {data} />
272
273
-
<div
274
-
class="mx-auto max-w-2xl @5xl/wrapper:grid @5xl/wrapper:max-w-7xl @5xl/wrapper:grid-cols-4"
275
-
>
276
<div></div>
277
<!-- svelte-ignore a11y_no_static_element_interactions -->
278
<div
···
28
import { setDidContext, setHandleContext } from './website/context';
29
import BaseEditingCard from './cards/BaseCard/BaseEditingCard.svelte';
30
import Settings from './Settings.svelte';
31
+
import ImageDropper from './components/ImageDropper.svelte';
32
33
let {
34
handle,
···
235
}
236
</script>
237
238
+
<svelte:body
239
+
onpaste={(event) => {
240
+
const target = event.target;
241
+
242
+
const active = document.activeElement;
243
+
const isEditable =
244
+
active instanceof HTMLInputElement ||
245
+
active instanceof HTMLTextAreaElement ||
246
+
active?.isContentEditable;
247
+
248
+
if (isEditable) {
249
+
// Let normal paste happen
250
+
return;
251
+
}
252
+
253
+
const text = event.clipboardData?.getData('text/plain');
254
+
255
+
if (!text) return;
256
+
257
+
try {
258
+
const url = new URL(text);
259
+
260
+
let item: Item = {
261
+
id: TID.nextStr(),
262
+
x: 0,
263
+
y: 0,
264
+
w: 2,
265
+
h: 2,
266
+
mobileH: 4,
267
+
mobileW: 4,
268
+
mobileX: 0,
269
+
mobileY: 0,
270
+
cardType: '',
271
+
cardData: {}
272
+
};
273
+
274
+
newItem.item = item;
275
+
276
+
for (const cardDef of AllCardDefinitions) {
277
+
if (cardDef.onUrlHandler?.(text, item)) {
278
+
item.cardType = cardDef.type;
279
+
saveNewItem();
280
+
}
281
+
}
282
+
283
+
newItem = {};
284
+
} catch (e) {
285
+
return;
286
+
}
287
+
}}
288
+
/>
289
+
290
+
<!-- <ImageDropper processImageFile={(file: File) => {}} /> -->
291
+
292
{#if !dev}
293
<div
294
class="bg-base-200 dark:bg-base-800 fixed inset-0 z-50 inline-flex h-full w-full items-center justify-center p-4 text-center lg:hidden"
···
325
>
326
<Profile {handle} {did} {data} />
327
328
+
<div class="mx-auto max-w-lg @5xl/wrapper:grid @5xl/wrapper:max-w-7xl @5xl/wrapper:grid-cols-4">
0
0
329
<div></div>
330
<!-- svelte-ignore a11y_no_static_element_interactions -->
331
<div
+2
-2
src/lib/Profile.svelte
···
29
30
<!-- lg:fixed lg:h-screen lg:w-1/4 lg:max-w-none lg:px-12 lg:pt-24 xl:w-1/3 -->
31
<div
32
-
class="mx-auto flex max-w-2xl flex-col justify-between px-8 @5xl/wrapper:fixed @5xl/wrapper:h-screen @5xl/wrapper:w-1/4 @5xl/wrapper:max-w-none @5xl/wrapper:px-12"
33
>
34
<div class="flex flex-col gap-4 pt-16 pb-8 @5xl/wrapper:h-screen @5xl/wrapper:pt-24">
35
{#if profileData?.avatar?.ref?.$link}
36
<img
37
-
class="size-32 rounded-full @5xl/wrapper:size-44"
38
src={'https://cdn.bsky.app/img/avatar/plain/' + did + '/' + profileData.avatar.ref.$link}
39
alt=""
40
/>
···
29
30
<!-- lg:fixed lg:h-screen lg:w-1/4 lg:max-w-none lg:px-12 lg:pt-24 xl:w-1/3 -->
31
<div
32
+
class="mx-auto flex max-w-lg flex-col justify-between px-8 @5xl/wrapper:fixed @5xl/wrapper:h-screen @5xl/wrapper:w-1/4 @5xl/wrapper:max-w-none @5xl/wrapper:px-12"
33
>
34
<div class="flex flex-col gap-4 pt-16 pb-8 @5xl/wrapper:h-screen @5xl/wrapper:pt-24">
35
{#if profileData?.avatar?.ref?.$link}
36
<img
37
+
class="size-32 rounded-full @5xl/wrapper:size-44 border border-base-400 dark:border-base-800"
38
src={'https://cdn.bsky.app/img/avatar/plain/' + did + '/' + profileData.avatar.ref.$link}
39
alt=""
40
/>
+1
-1
src/lib/Website.svelte
···
39
<div class="@container/wrapper relative w-full">
40
<Profile {handle} {did} {data} showEditButton={true} />
41
42
-
<div class="mx-auto max-w-2xl lg:grid lg:max-w-none lg:grid-cols-4">
43
<div></div>
44
<div
45
bind:this={container}
···
39
<div class="@container/wrapper relative w-full">
40
<Profile {handle} {did} {data} showEditButton={true} />
41
42
+
<div class="mx-auto max-w-lg lg:grid lg:max-w-none lg:grid-cols-4">
43
<div></div>
44
<div
45
bind:this={container}
+3
-1
src/lib/cards/BaseCard/BaseCard.svelte
···
7
import { getColor } from '..';
8
9
const colors = {
10
-
base: 'bg-base-50 dark:bg-base-900',
11
accent:
12
'bg-accent-400 dark:bg-accent-500 accent',
13
transparent: ''
···
27
isEditing = false,
28
controls,
29
showOutline,
0
30
...rest
31
}: BaseCardProps = $props();
32
···
43
color ? (colors[color] ?? colors.accent) : colors.base,
44
color !== 'accent' && item.color !== 'base' && item.color !== 'transparent' ? color : '',
45
showOutline ? 'outline-2' : '',
0
46
]}
47
style={`
48
--mx: ${item.mobileX};
···
7
import { getColor } from '..';
8
9
const colors = {
10
+
base: 'bg-base-200/50 dark:bg-base-950/50',
11
accent:
12
'bg-accent-400 dark:bg-accent-500 accent',
13
transparent: ''
···
27
isEditing = false,
28
controls,
29
showOutline,
30
+
class: className,
31
...rest
32
}: BaseCardProps = $props();
33
···
44
color ? (colors[color] ?? colors.accent) : colors.base,
45
color !== 'accent' && item.color !== 'base' && item.color !== 'transparent' ? color : '',
46
showOutline ? 'outline-2' : '',
47
+
className
48
]}
49
style={`
50
--mx: ${item.mobileX};
+6
-7
src/lib/cards/BaseCard/BaseEditingCard.svelte
···
106
let newW = resizeStartW + gridDeltaW;
107
let newH = resizeStartH + gridDeltaH;
108
109
-
console.log(item.mobileW, newW);
110
if (isMobile()) {
111
newW = Math.round(newW / 4) * 4;
112
} else {
113
newW = Math.round(newW / 2) * 2;
114
}
115
-
console.log(item.mobileW, newW);
116
117
// Clamp to min/max
118
-
newW = Math.max(minW, Math.min(maxW, newW));
119
-
newH = Math.max(minH, Math.min(maxH, newH));
120
121
// Only call onsetsize if size changed
122
const currentW = isMobile() ? (item.mobileW ?? item.w) : item.w;
···
137
if (!cardDef) return false;
138
139
if (isMobile()) {
140
-
w *= 2;
141
-
h *= 2;
142
}
143
144
return w >= minW && w <= maxW && h >= minH && h <= maxH;
···
155
let settingsPopoverOpen = $state(false);
156
</script>
157
158
-
<BaseCard {item} {...rest} isEditing={true} bind:ref showOutline={isResizing}>
159
{@render children?.()}
160
161
{#snippet controls()}
···
106
let newW = resizeStartW + gridDeltaW;
107
let newH = resizeStartH + gridDeltaH;
108
0
109
if (isMobile()) {
110
newW = Math.round(newW / 4) * 4;
111
} else {
112
newW = Math.round(newW / 2) * 2;
113
}
114
+
let mult = isMobile() ? 2 : 1;
115
116
// Clamp to min/max
117
+
newW = Math.max(minW * mult, Math.min(maxW, newW));
118
+
newH = Math.max(minH * mult, Math.min(maxH, newH));
119
120
// Only call onsetsize if size changed
121
const currentW = isMobile() ? (item.mobileW ?? item.w) : item.w;
···
136
if (!cardDef) return false;
137
138
if (isMobile()) {
139
+
140
+
return w >= minW && w*2 <= maxW && h >= minH && h*2 <= maxH;
141
}
142
143
return w >= minW && w <= maxW && h >= minH && h <= maxH;
···
154
let settingsPopoverOpen = $state(false);
155
</script>
156
157
+
<BaseCard {item} isEditing={true} bind:ref showOutline={isResizing} class="starting:scale-0 scale-100 starting:opacity-0 opacity-100" {...rest} >
158
{@render children?.()}
159
160
{#snippet controls()}
+9
-124
src/lib/cards/BigSocialCard/BigSocialCard.svelte
···
1
<script lang="ts">
0
2
import type { ContentComponentProps } from '../types';
3
4
let { item }: ContentComponentProps = $props();
5
6
const platform = $derived(item.cardData.platform as string);
0
0
7
</script>
8
9
<a
10
href={item.cardData.href}
11
target="_blank"
12
rel="noopener noreferrer"
13
-
class="flex h-full w-full items-center justify-center p-4"
0
0
0
14
>
15
-
<div class="flex aspect-square max-h-full max-w-full items-center justify-center">
16
-
{#if platform === 'instagram'}
17
-
<svg class="h-full w-full" viewBox="0 0 24 24" fill="currentColor">
18
-
<path
19
-
d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948 0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072 3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98-1.281-.059-1.69-.073-4.949-.073zm0 5.838c-3.403 0-6.162 2.759-6.162 6.162s2.759 6.163 6.162 6.163 6.162-2.759 6.162-6.163c0-3.403-2.759-6.162-6.162-6.162zm0 10.162c-2.209 0-4-1.79-4-4 0-2.209 1.791-4 4-4s4 1.791 4 4c0 2.21-1.791 4-4 4zm6.406-11.845c-.796 0-1.441.645-1.441 1.44s.645 1.44 1.441 1.44c.795 0 1.439-.645 1.439-1.44s-.644-1.44-1.439-1.44z"
20
-
/>
21
-
</svg>
22
-
{:else if platform === 'facebook'}
23
-
<svg class="h-full w-full" viewBox="0 0 24 24" fill="currentColor">
24
-
<path
25
-
d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"
26
-
/>
27
-
</svg>
28
-
{:else if platform === 'twitter' || platform === 'x'}
29
-
<svg class="h-full w-full" viewBox="0 0 24 24" fill="currentColor">
30
-
<path
31
-
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"
32
-
/>
33
-
</svg>
34
-
{:else if platform === 'youtube'}
35
-
<svg class="h-full w-full" viewBox="0 0 24 24" fill="currentColor">
36
-
<path
37
-
d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"
38
-
/>
39
-
</svg>
40
-
{:else if platform === 'tiktok'}
41
-
<svg class="h-full w-full" viewBox="0 0 24 24" fill="currentColor">
42
-
<path
43
-
d="M12.525.02c1.31-.02 2.61-.01 3.91-.02.08 1.53.63 3.09 1.75 4.17 1.12 1.11 2.7 1.62 4.24 1.79v4.03c-1.44-.05-2.89-.35-4.2-.97-.57-.26-1.1-.59-1.62-.93-.01 2.92.01 5.84-.02 8.75-.08 1.4-.54 2.79-1.35 3.94-1.31 1.92-3.58 3.17-5.91 3.21-1.43.08-2.86-.31-4.08-1.03-2.02-1.19-3.44-3.37-3.65-5.71-.02-.5-.03-1-.01-1.49.18-1.9 1.12-3.72 2.58-4.96 1.66-1.44 3.98-2.13 6.15-1.72.02 1.48-.04 2.96-.04 4.44-.99-.32-2.15-.23-3.02.37-.63.41-1.11 1.04-1.36 1.75-.21.51-.15 1.07-.14 1.61.24 1.64 1.82 3.02 3.5 2.87 1.12-.01 2.19-.66 2.77-1.61.19-.33.4-.67.41-1.06.1-1.79.06-3.57.07-5.36.01-4.03-.01-8.05.02-12.07z"
44
-
/>
45
-
</svg>
46
-
{:else if platform === 'linkedin'}
47
-
<svg class="h-full w-full" viewBox="0 0 24 24" fill="currentColor">
48
-
<path
49
-
d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"
50
-
/>
51
-
</svg>
52
-
{:else if platform === 'bluesky'}
53
-
<svg class="h-full w-full" viewBox="0 0 568 501" fill="currentColor">
54
-
<path
55
-
d="M123.121 33.6637C188.241 82.5526 258.281 181.681 284 234.873C309.719 181.681 379.759 82.5526 444.879 33.6637C491.866 -1.61183 568 -28.9064 568 57.9464C568 75.2916 558.055 203.659 552.222 224.501C531.947 296.954 458.067 315.434 392.347 304.249C507.222 323.8 536.444 388.56 473.333 453.32C353.473 576.312 301.061 422.461 287.631 383.039C285.169 374.014 284.017 369.587 284 371.839C283.983 369.587 282.831 374.014 280.369 383.039C266.939 422.461 214.527 576.312 94.6667 453.32C31.5556 388.56 60.7778 323.8 175.653 304.249C109.933 315.434 36.0535 296.954 15.7778 224.501C9.94525 203.659 0 75.2916 0 57.9464C0 -28.9064 76.1345 -1.61183 123.121 33.6637Z"
56
-
/>
57
-
</svg>
58
-
{:else if platform === 'threads'}
59
-
<svg class="h-full w-full" viewBox="0 0 24 24" fill="currentColor">
60
-
<path
61
-
d="M12.186 24h-.007c-3.581-.024-6.334-1.205-8.184-3.509C2.35 18.44 1.5 15.586 1.472 12.01v-.017c.03-3.579.879-6.43 2.525-8.482C5.845 1.205 8.6.024 12.18 0h.014c2.746.02 5.043.725 6.826 2.098 1.677 1.29 2.858 3.13 3.509 5.467l-2.04.569c-1.104-3.96-3.898-5.984-8.304-6.015-2.91.022-5.11.936-6.54 2.717C4.307 6.504 3.616 8.914 3.589 12c.027 3.086.718 5.496 2.057 7.164 1.43 1.783 3.631 2.698 6.54 2.717 2.623-.02 4.358-.631 5.8-2.045 1.647-1.613 1.618-3.593 1.09-4.798-.31-.71-.873-1.3-1.634-1.75-.192 1.352-.622 2.446-1.284 3.272-.886 1.102-2.14 1.704-3.73 1.79-1.202.065-2.361-.218-3.259-.801-1.063-.689-1.685-1.74-1.752-2.96-.065-1.182.408-2.256 1.33-3.022.88-.732 2.07-1.128 3.446-1.145.875-.01 1.71.097 2.482.32.019-.776-.042-1.49-.184-2.115H9.346v-2.06h6.043c.122.534.19 1.245.195 2.098-.38-.125-.77-.227-1.172-.305-.94-.182-1.935-.227-2.955-.127-1.307.13-2.344.483-3.086 1.051-.655.5-1.01 1.143-.999 1.808.01.663.376 1.23.999 1.549.549.28 1.254.406 2.046.369.958-.046 1.704-.324 2.218-.825.334-.326.56-.74.688-1.242-.29-.083-.61-.153-.96-.207-.942-.148-2.042-.105-3.06.121l-.524-1.993c1.32-.294 2.695-.35 3.884-.159.476.077.912.184 1.312.317a9.9 9.9 0 0 0-.078-1.136l2.043-.284c.085.61.116 1.328.09 2.138.922.499 1.631 1.18 2.104 2.027.593 1.06.854 2.39.712 3.848-.142 1.456-.726 2.78-1.684 3.823-1.724 1.877-4.123 2.835-7.134 2.849z"
62
-
/>
63
-
</svg>
64
-
{:else if platform === 'snapchat'}
65
-
<svg class="h-full w-full" viewBox="0 0 24 24" fill="currentColor">
66
-
<path
67
-
d="M12.017 0C5.396 0 .029 5.367.029 11.987c0 5.079 3.158 9.417 7.618 11.162-.105-.949-.199-2.403.041-3.439.219-.937 1.406-5.957 1.406-5.957s-.359-.72-.359-1.781c0-1.663.967-2.911 2.168-2.911 1.024 0 1.518.769 1.518 1.688 0 1.029-.653 2.567-.992 3.992-.285 1.193.6 2.165 1.775 2.165 2.128 0 3.768-2.245 3.768-5.487 0-2.861-2.063-4.869-5.008-4.869-3.41 0-5.409 2.562-5.409 5.199 0 1.033.394 2.143.889 2.741.099.12.112.225.085.345-.09.375-.293 1.199-.334 1.363-.053.225-.172.271-.401.165-1.495-.69-2.433-2.878-2.433-4.646 0-3.776 2.748-7.252 7.92-7.252 4.158 0 7.392 2.967 7.392 6.923 0 4.135-2.607 7.462-6.233 7.462-1.214 0-2.354-.629-2.758-1.379l-.749 2.848c-.269 1.045-1.004 2.352-1.498 3.146 1.123.345 2.306.535 3.55.535 6.607 0 11.985-5.365 11.985-11.987C23.97 5.39 18.592.026 11.985.026L12.017 0z"
68
-
/>
69
-
</svg>
70
-
{:else if platform === 'pinterest'}
71
-
<svg class="h-full w-full" viewBox="0 0 24 24" fill="currentColor">
72
-
<path
73
-
d="M12.017 0C5.396 0 .029 5.367.029 11.987c0 5.079 3.158 9.417 7.618 11.162-.105-.949-.199-2.403.041-3.439.219-.937 1.406-5.957 1.406-5.957s-.359-.72-.359-1.781c0-1.663.967-2.911 2.168-2.911 1.024 0 1.518.769 1.518 1.688 0 1.029-.653 2.567-.992 3.992-.285 1.193.6 2.165 1.775 2.165 2.128 0 3.768-2.245 3.768-5.487 0-2.861-2.063-4.869-5.008-4.869-3.41 0-5.409 2.562-5.409 5.199 0 1.033.394 2.143.889 2.741.099.12.112.225.085.345-.09.375-.293 1.199-.334 1.363-.053.225-.172.271-.401.165-1.495-.69-2.433-2.878-2.433-4.646 0-3.776 2.748-7.252 7.92-7.252 4.158 0 7.392 2.967 7.392 6.923 0 4.135-2.607 7.462-6.233 7.462-1.214 0-2.354-.629-2.758-1.379l-.749 2.848c-.269 1.045-1.004 2.352-1.498 3.146 1.123.345 2.306.535 3.55.535 6.607 0 11.985-5.365 11.985-11.987C23.97 5.39 18.592.026 11.985.026L12.017 0z"
74
-
/>
75
-
</svg>
76
-
{:else if platform === 'twitch'}
77
-
<svg class="h-full w-full" viewBox="0 0 24 24" fill="currentColor">
78
-
<path
79
-
d="M11.571 4.714h1.715v5.143H11.57zm4.715 0H18v5.143h-1.714zM6 0L1.714 4.286v15.428h5.143V24l4.286-4.286h3.428L22.286 12V0zm14.571 11.143l-3.428 3.428h-3.429l-3 3v-3H6.857V1.714h13.714Z"
80
-
/>
81
-
</svg>
82
-
{:else if platform === 'discord'}
83
-
<svg class="h-full w-full" viewBox="0 0 24 24" fill="currentColor">
84
-
<path
85
-
d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z"
86
-
/>
87
-
</svg>
88
-
{:else if platform === 'github'}
89
-
<svg class="h-full w-full" viewBox="0 0 24 24" fill="currentColor">
90
-
<path
91
-
d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"
92
-
/>
93
-
</svg>
94
-
{:else if platform === 'spotify'}
95
-
<svg class="h-full w-full" viewBox="0 0 24 24" fill="currentColor">
96
-
<path
97
-
d="M12 0C5.4 0 0 5.4 0 12s5.4 12 12 12 12-5.4 12-12S18.66 0 12 0zm5.521 17.34c-.24.359-.66.48-1.021.24-2.82-1.74-6.36-2.101-10.561-1.141-.418.122-.779-.179-.899-.539-.12-.421.18-.78.54-.9 4.56-1.021 8.52-.6 11.64 1.32.42.18.479.659.301 1.02zm1.44-3.3c-.301.42-.841.6-1.262.3-3.239-1.98-8.159-2.58-11.939-1.38-.479.12-1.02-.12-1.14-.6-.12-.48.12-1.021.6-1.141C9.6 9.9 15 10.561 18.72 12.84c.361.181.54.78.241 1.2zm.12-3.36C15.24 8.4 8.82 8.16 5.16 9.301c-.6.179-1.2-.181-1.38-.721-.18-.601.18-1.2.72-1.381 4.26-1.26 11.28-1.02 15.721 1.621.539.3.719 1.02.419 1.56-.299.421-1.02.599-1.559.3z"
98
-
/>
99
-
</svg>
100
-
{:else if platform === 'reddit'}
101
-
<svg class="h-full w-full" viewBox="0 0 24 24" fill="currentColor">
102
-
<path
103
-
d="M12 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0zm5.01 4.744c.688 0 1.25.561 1.25 1.249a1.25 1.25 0 0 1-2.498.056l-2.597-.547-.8 3.747c1.824.07 3.48.632 4.674 1.488.308-.309.73-.491 1.207-.491.968 0 1.754.786 1.754 1.754 0 .716-.435 1.333-1.01 1.614a3.111 3.111 0 0 1 .042.52c0 2.694-3.13 4.87-7.004 4.87-3.874 0-7.004-2.176-7.004-4.87 0-.183.015-.366.043-.534A1.748 1.748 0 0 1 4.028 12c0-.968.786-1.754 1.754-1.754.463 0 .898.196 1.207.49 1.207-.883 2.878-1.43 4.744-1.487l.885-4.182a.342.342 0 0 1 .14-.197.35.35 0 0 1 .238-.042l2.906.617a1.214 1.214 0 0 1 1.108-.701zM9.25 12C8.561 12 8 12.562 8 13.25c0 .687.561 1.248 1.25 1.248.687 0 1.248-.561 1.248-1.249 0-.688-.561-1.249-1.249-1.249zm5.5 0c-.687 0-1.248.561-1.248 1.25 0 .687.561 1.248 1.249 1.248.688 0 1.249-.561 1.249-1.249 0-.687-.562-1.249-1.25-1.249zm-5.466 3.99a.327.327 0 0 0-.231.094.33.33 0 0 0 0 .463c.842.842 2.484.913 2.961.913.477 0 2.105-.056 2.961-.913a.361.361 0 0 0 .029-.463.33.33 0 0 0-.464 0c-.547.533-1.684.73-2.512.73-.828 0-1.979-.196-2.512-.73a.326.326 0 0 0-.232-.095z"
104
-
/>
105
-
</svg>
106
-
{:else if platform === 'whatsapp'}
107
-
<svg class="h-full w-full" viewBox="0 0 24 24" fill="currentColor">
108
-
<path
109
-
d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413Z"
110
-
/>
111
-
</svg>
112
-
{:else if platform === 'telegram'}
113
-
<svg class="h-full w-full" viewBox="0 0 24 24" fill="currentColor">
114
-
<path
115
-
d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z"
116
-
/>
117
-
</svg>
118
-
{:else if platform === 'mastodon'}
119
-
<svg class="h-full w-full" viewBox="0 0 24 24" fill="currentColor">
120
-
<path
121
-
d="M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38.265-.061.527-.132.786-.213.585-.184 1.27-.39 1.774-.753a.057.057 0 0 0 .023-.043v-1.809a.052.052 0 0 0-.02-.041.053.053 0 0 0-.046-.01 20.282 20.282 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.593 5.593 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422.038-.008.077-.015.11-.024 2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545zm-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.67 1.977v6.218H4.822V8.102c0-1.31.337-2.35 1.011-3.12.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164.675.77 1.012 1.81 1.012 3.12z"
122
-
/>
123
-
</svg>
124
-
{:else}
125
-
<svg
126
-
class="h-full w-full"
127
-
xmlns="http://www.w3.org/2000/svg"
128
-
viewBox="0 0 24 24"
129
-
fill="currentColor"
130
-
>
131
-
<path
132
-
fill-rule="evenodd"
133
-
d="M19.902 4.098a3.75 3.75 0 0 0-5.304 0l-4.5 4.5a3.75 3.75 0 0 0 1.035 6.037.75.75 0 0 1-.646 1.353 5.25 5.25 0 0 1-1.449-8.45l4.5-4.5a5.25 5.25 0 1 1 7.424 7.424l-1.757 1.757a.75.75 0 1 1-1.06-1.06l1.757-1.757a3.75 3.75 0 0 0 0-5.304Zm-7.389 4.267a.75.75 0 0 1 1-.353 5.25 5.25 0 0 1 1.449 8.45l-4.5 4.5a5.25 5.25 0 1 1-7.424-7.424l1.757-1.757a.75.75 0 1 1 1.06 1.06l-1.757 1.757a3.75 3.75 0 1 0 5.304 5.304l4.5-4.5a3.75 3.75 0 0 0-1.035-6.037.75.75 0 0 1-.354-1Z"
134
-
clip-rule="evenodd"
135
-
/>
136
-
</svg>
137
-
{/if}
138
</div>
139
</a>
···
1
<script lang="ts">
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
+
9
+
$inspect(platformsData[platform].svg)
10
</script>
11
12
<a
13
href={item.cardData.href}
14
target="_blank"
15
rel="noopener noreferrer"
16
+
class="flex h-full w-full items-center justify-center p-10"
17
+
style={
18
+
`background-color: #${item.cardData.color}`
19
+
}
20
>
21
+
<div class="flex aspect-square max-h-full max-w-full items-center justify-center [&_svg]:size-full [&_svg]:max-w-60 [&_svg]:fill-white">
22
+
{@html platformsData[platform].svg}
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
23
</div>
24
</a>
+3
-54
src/lib/cards/BigSocialCard/CreateBigSocialCardModal.svelte
···
1
<script lang="ts">
2
import { Alert, Button, Input, Modal, Subheading } from '@foxui/core';
3
import type { CreationModalComponentProps } from '../types';
0
4
5
let { item = $bindable(), oncreate, oncancel }: CreationModalComponentProps = $props();
6
7
let errorMessage = $state('');
8
9
-
const platformPatterns: Record<string, RegExp> = {
10
-
instagram: /(?:instagram\.com|instagr\.am)/i,
11
-
facebook: /(?:facebook\.com|fb\.com|fb\.me)/i,
12
-
twitter: /(?:twitter\.com)/i,
13
-
x: /(?:x\.com)/i,
14
-
youtube: /(?:youtube\.com|youtu\.be)/i,
15
-
tiktok: /(?:tiktok\.com)/i,
16
-
linkedin: /(?:linkedin\.com)/i,
17
-
bluesky: /(?:bsky\.app|bsky\.social)/i,
18
-
threads: /(?:threads\.net)/i,
19
-
snapchat: /(?:snapchat\.com)/i,
20
-
pinterest: /(?:pinterest\.com|pin\.it)/i,
21
-
twitch: /(?:twitch\.tv)/i,
22
-
discord: /(?:discord\.gg|discord\.com)/i,
23
-
github: /(?:github\.com)/i,
24
-
spotify: /(?:spotify\.com|open\.spotify\.com)/i,
25
-
reddit: /(?:reddit\.com)/i,
26
-
whatsapp: /(?:whatsapp\.com|wa\.me)/i,
27
-
telegram: /(?:t\.me|telegram\.org)/i,
28
-
mastodon: /(?:mastodon\.social|mastodon\.online|mstdn\.social)/i
29
-
};
30
-
31
-
const platformColors: Record<string, string> = {
32
-
instagram: 'pink',
33
-
facebook: 'blue',
34
-
twitter: 'sky',
35
-
x: 'zinc',
36
-
youtube: 'red',
37
-
tiktok: 'zinc',
38
-
linkedin: 'blue',
39
-
bluesky: 'sky',
40
-
threads: 'zinc',
41
-
snapchat: 'yellow',
42
-
pinterest: 'red',
43
-
twitch: 'purple',
44
-
discord: 'indigo',
45
-
github: 'zinc',
46
-
spotify: 'green',
47
-
reddit: 'orange',
48
-
whatsapp: 'green',
49
-
telegram: 'sky',
50
-
mastodon: 'purple'
51
-
};
52
-
53
-
function detectPlatform(url: string): string | null {
54
-
for (const [platform, pattern] of Object.entries(platformPatterns)) {
55
-
if (pattern.test(url)) {
56
-
return platform;
57
-
}
58
-
}
59
-
return null;
60
-
}
61
-
62
function handleCreate() {
63
errorMessage = '';
64
···
76
}
77
78
item.cardData.platform = platform;
79
-
item.color = platformColors[platform] || 'accent';
0
80
oncreate();
81
}
82
</script>
···
1
<script lang="ts">
2
import { Alert, Button, Input, Modal, Subheading } from '@foxui/core';
3
import type { CreationModalComponentProps } from '../types';
4
+
import { detectPlatform, platformPatterns, platformsData } from '.';
5
6
let { item = $bindable(), oncreate, oncancel }: CreationModalComponentProps = $props();
7
8
let errorMessage = $state('');
9
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
10
function handleCreate() {
11
errorMessage = '';
12
···
24
}
25
26
item.cardData.platform = platform;
27
+
item.cardData.color = platformsData[platform].hex;
28
+
29
oncreate();
30
}
31
</script>
+1
-1
src/lib/cards/BigSocialCard/SidebarItemBigSocialCard.svelte
···
17
clip-rule="evenodd"
18
/>
19
</svg>
20
-
Big Social</Button
21
>
···
17
clip-rule="evenodd"
18
/>
19
</svg>
20
+
Big Social Icon</Button
21
>
+89
-6
src/lib/cards/BigSocialCard/index.ts
···
16
};
17
card.w = 2;
18
card.h = 2;
19
-
card.mobileW = 2;
20
-
card.mobileH = 2;
21
},
22
-
allowSetColor: true,
23
-
defaultColor: 'accent',
24
-
minW: 1,
25
-
minH: 1
0
0
0
0
0
0
0
0
0
0
26
} as CardDefinition & { type: 'bigsocial' };
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
16
};
17
card.w = 2;
18
card.h = 2;
19
+
card.mobileW = 4;
20
+
card.mobileH = 4;
21
},
22
+
allowSetColor: false,
23
+
defaultColor: 'transparent',
24
+
minW: 2,
25
+
minH: 2,
26
+
onUrlHandler: (url, item) => {
27
+
const platform = detectPlatform(url);
28
+
if (!platform) return null;
29
+
30
+
item.cardData.platform = platform;
31
+
item.cardData.color = platformsData[platform].hex;
32
+
item.cardData.href = url;
33
+
34
+
return item;
35
+
}
36
} as CardDefinition & { type: 'bigsocial' };
37
+
38
+
export const platformPatterns: Record<string, RegExp> = {
39
+
instagram: /(?:instagram\.com|instagr\.am)/i,
40
+
facebook: /(?:facebook\.com|fb\.com|fb\.me)/i,
41
+
twitter: /(?:twitter\.com)/i,
42
+
x: /(?:x\.com)/i,
43
+
youtube: /(?:youtube\.com|youtu\.be)/i,
44
+
tiktok: /(?:tiktok\.com)/i,
45
+
linkedin: /(?:linkedin\.com)/i,
46
+
bluesky: /(?:bsky\.app|bsky\.social)/i,
47
+
threads: /(?:threads\.net)/i,
48
+
snapchat: /(?:snapchat\.com)/i,
49
+
pinterest: /(?:pinterest\.com|pin\.it)/i,
50
+
twitch: /(?:twitch\.tv)/i,
51
+
discord: /(?:discord\.gg|discord\.com)/i,
52
+
github: /(?:github\.com)/i,
53
+
spotify: /(?:spotify\.com|open\.spotify\.com)/i,
54
+
reddit: /(?:reddit\.com)/i,
55
+
whatsapp: /(?:whatsapp\.com|wa\.me)/i,
56
+
telegram: /(?:t\.me|telegram\.org)/i,
57
+
mastodon: /(?:mastodon\.social|mastodon\.online|mstdn\.social)/i
58
+
};
59
+
60
+
import {
61
+
siInstagram,
62
+
siFacebook,
63
+
siX,
64
+
siYoutube,
65
+
siTiktok,
66
+
siBluesky,
67
+
siThreads,
68
+
siSnapchat,
69
+
siPinterest,
70
+
siTwitch,
71
+
siDiscord,
72
+
siGithub,
73
+
siSpotify,
74
+
siReddit,
75
+
siWhatsapp,
76
+
siTelegram,
77
+
siMastodon,
78
+
type SimpleIcon
79
+
} from 'simple-icons';
80
+
81
+
export const platformsData: Record<string, SimpleIcon> = {
82
+
instagram: siInstagram,
83
+
facebook: siFacebook,
84
+
twitter: siX,
85
+
x: siX,
86
+
youtube: siYoutube,
87
+
tiktok: siTiktok,
88
+
bluesky: siBluesky,
89
+
threads: siThreads,
90
+
snapchat: siSnapchat,
91
+
pinterest: siPinterest,
92
+
twitch: siTwitch,
93
+
discord: siDiscord,
94
+
github: siGithub,
95
+
spotify: siSpotify,
96
+
reddit: siReddit,
97
+
whatsapp: siWhatsapp,
98
+
telegram: siTelegram,
99
+
mastodon: siMastodon
100
+
};
101
+
102
+
export function detectPlatform(url: string): string | null {
103
+
for (const [platform, pattern] of Object.entries(platformPatterns)) {
104
+
if (pattern.test(url)) {
105
+
return platform;
106
+
}
107
+
}
108
+
return null;
109
+
}
+2
-1
src/lib/cards/LinkCard/EditingLinkCard.svelte
···
1
<script lang="ts">
0
2
import { getIsMobile } from '$lib/helper';
3
import type { ContentComponentProps } from '../types';
4
import PlainTextEditor from '../utils/PlainTextEditor.svelte';
···
57
</div>
58
</div>
59
60
-
{#if ((isMobile() && item.mobileH >= 8) || (!isMobile() && item.h >= 4)) && item.cardData.image}
61
<img class=" mb-2 max-h-32 w-full rounded-xl object-cover" src={item.cardData.image} alt="" />
62
{/if}
63
</div>
···
1
<script lang="ts">
2
+
import { browser } from '$app/environment';
3
import { getIsMobile } from '$lib/helper';
4
import type { ContentComponentProps } from '../types';
5
import PlainTextEditor from '../utils/PlainTextEditor.svelte';
···
58
</div>
59
</div>
60
61
+
{#if browser && ((isMobile() && item.mobileH >= 8) || (!isMobile() && item.h >= 4)) && item.cardData.image}
62
<img class=" mb-2 max-h-32 w-full rounded-xl object-cover" src={item.cardData.image} alt="" />
63
{/if}
64
</div>
+3
-2
src/lib/cards/LinkCard/LinkCard.svelte
···
1
<script lang="ts">
0
2
import { getIsMobile } from '$lib/helper';
3
import type { ContentComponentProps } from '../types';
4
···
56
</div>
57
</div>
58
59
-
{#if ((isMobile() && item.mobileH >= 8) || (!isMobile() && item.h >= 4)) && item.cardData.image}
60
-
<img class=" mb-2 max-h-32 w-full rounded-xl object-cover" src={item.cardData.image} alt="" />
61
{/if}
62
{#if item.cardData.href}
63
<a
···
1
<script lang="ts">
2
+
import { browser } from '$app/environment';
3
import { getIsMobile } from '$lib/helper';
4
import type { ContentComponentProps } from '../types';
5
···
57
</div>
58
</div>
59
60
+
{#if browser && ((isMobile() && item.mobileH >= 8) || (!isMobile() && item.h >= 4)) && item.cardData.image}
61
+
<img class="mb-2 max-h-32 w-full starting:opacity-0 opacity-100 transition-opacity duration-100 rounded-xl object-cover" src={item.cardData.image} alt="" />
62
{/if}
63
{#if item.cardData.href}
64
<a
+1
-1
src/lib/cards/SpecialCards/UpdatedBlentos/UpdatedBlentosCard.svelte
···
22
target="_blank"
23
>
24
<img src={profile.avatar} class="aspect-square size-28 rounded-full" alt="" />
25
-
<div class="line-clamp-1 text-lg font-bold">{profile.displayName || profile.handle}</div>
26
</a>
27
{/each}
28
</div>
···
22
target="_blank"
23
>
24
<img src={profile.avatar} class="aspect-square size-28 rounded-full" alt="" />
25
+
<div class="line-clamp-1 text-md font-bold text-center">{profile.displayName || profile.handle}</div>
26
</a>
27
{/each}
28
</div>
+18
-6
src/lib/cards/TextCard/EditingTextCard.svelte
···
1
<script lang="ts">
2
import type { Item } from '$lib/types';
3
-
import { textAlignClasses, verticalAlignClasses } from '.';
0
4
import type { ContentComponentProps } from '../types';
5
import MarkdownTextEditor from '../utils/MarkdownTextEditor.svelte';
0
6
7
let { item = $bindable<Item>() }: ContentComponentProps = $props();
0
0
0
0
8
</script>
9
0
0
10
<div
11
-
class={[
12
-
'prose dark:prose-invert prose-sm prose-a:no-underline prose-a:text-accent-600 dark:prose-a:text-accent-400 accent:prose-a:text-accent-950 accent:prose-a:underline accent:prose-p:text-base-900 hover:bg-base-500/20 prose-p:first:mt-0 prose-p:last:mb-0 h-full overflow-y-scroll rounded-md p-2 inline-flex w-full max-w-none',
13
textAlignClasses[item.cardData.textAlign as string],
14
-
verticalAlignClasses[item.cardData.verticalAlign as string]
15
-
]}
0
0
0
0
16
>
17
-
<MarkdownTextEditor bind:item />
18
</div>
···
1
<script lang="ts">
2
import type { Item } from '$lib/types';
3
+
import type { Editor } from '@tiptap/core';
4
+
import { textAlignClasses, textSizeClasses, verticalAlignClasses } from '.';
5
import type { ContentComponentProps } from '../types';
6
import MarkdownTextEditor from '../utils/MarkdownTextEditor.svelte';
7
+
import { cn } from '@foxui/core';
8
9
let { item = $bindable<Item>() }: ContentComponentProps = $props();
10
+
11
+
let editor: Editor | null = $state(null);
12
+
13
+
$inspect(textSizeClasses[item.cardData.textSize as number]);
14
</script>
15
16
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
17
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
18
<div
19
+
class={cn(
20
+
'prose dark:prose-invert prose-neutral prose-sm prose-a:no-underline prose-a:text-accent-600 dark:prose-a:text-accent-400 accent:prose-a:text-accent-950 accent:prose-a:underline accent:prose-p:text-base-900 hover:bg-base-700/5 accent:hover:bg-accent-300/20 prose-p:first:mt-0 prose-p:last:mb-0 inline-flex h-full w-full text-lg max-w-none overflow-y-scroll rounded-md p-2 transition-colors duration-150 cursor-text',
21
textAlignClasses[item.cardData.textAlign as string],
22
+
verticalAlignClasses[item.cardData.verticalAlign as string],
23
+
textSizeClasses[(item.cardData.textSize ?? 0) as number]
24
+
)}
25
+
onclick={() => {
26
+
editor?.commands.focus('end');
27
+
}}
28
>
29
+
<MarkdownTextEditor bind:item bind:editor />
30
</div>
+4
-3
src/lib/cards/TextCard/TextCard.svelte
···
1
<script lang="ts">
2
import { marked } from 'marked';
3
import type { ContentComponentProps } from '../types';
4
-
import { textAlignClasses, verticalAlignClasses } from '.';
5
6
let { item }: ContentComponentProps = $props();
7
···
12
13
<div
14
class={[
15
-
'prose dark:prose-invert prose-sm prose-a:no-underline prose-a:text-accent-600 dark:prose-a:text-accent-400 accent:prose-a:text-accent-950 accent:prose-a:underline accent:prose-p:text-base-900 prose-p:first:mt-0 prose-p:last:mb-0 inline-flex h-full w-full overflow-y-scroll rounded-md p-3 max-w-none',
16
textAlignClasses?.[item.cardData.textAlign as string],
17
-
verticalAlignClasses[item.cardData.verticalAlign as string]
0
18
]}
19
>
20
<span>{@html marked.parse(item.cardData.text ?? '', { renderer })}</span>
···
1
<script lang="ts">
2
import { marked } from 'marked';
3
import type { ContentComponentProps } from '../types';
4
+
import { textAlignClasses, textSizeClasses, verticalAlignClasses } from '.';
5
6
let { item }: ContentComponentProps = $props();
7
···
12
13
<div
14
class={[
15
+
'prose dark:prose-invert prose-neutral prose-sm prose-a:no-underline prose-a:text-accent-600 dark:prose-a:text-accent-400 accent:prose-a:text-accent-950 accent:prose-a:underline accent:prose-p:text-base-900 prose-p:first:mt-0 prose-p:last:mb-0 prose-headings:first:mt-0 prose-headings:last:mb-0 inline-flex h-full min-h-full w-full max-w-none overflow-y-scroll rounded-md p-3 text-lg',
16
textAlignClasses?.[item.cardData.textAlign as string],
17
+
verticalAlignClasses[item.cardData.verticalAlign as string],
18
+
textSizeClasses[(item.cardData.textSize ?? 0) as number]
19
]}
20
>
21
<span>{@html marked.parse(item.cardData.text ?? '', { renderer })}</span>
+50
-1
src/lib/cards/TextCard/TextCardSettings.svelte
···
1
<script lang="ts">
2
import type { Item } from '$lib/types';
3
import type { ContentComponentProps } from '../types';
4
-
import { ToggleGroup, ToggleGroupItem } from '@foxui/core';
5
6
let { item = $bindable<Item>() }: ContentComponentProps = $props();
7
···
119
></ToggleGroupItem
120
>
121
</ToggleGroup>
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
122
</div>
···
1
<script lang="ts">
2
import type { Item } from '$lib/types';
3
import type { ContentComponentProps } from '../types';
4
+
import { ToggleGroup, ToggleGroupItem, Button } from '@foxui/core';
5
6
let { item = $bindable<Item>() }: ContentComponentProps = $props();
7
···
119
></ToggleGroupItem
120
>
121
</ToggleGroup>
122
+
123
+
<div>
124
+
<Button
125
+
variant="ghost"
126
+
onclick={() => {
127
+
item.cardData.textSize = Math.max((item.cardData.textSize ?? 0) - 1, 0);
128
+
}}
129
+
disabled={(item.cardData.textSize ?? 0) < 1}
130
+
>
131
+
<svg
132
+
xmlns="http://www.w3.org/2000/svg"
133
+
width="24"
134
+
height="24"
135
+
viewBox="0 0 24 24"
136
+
fill="none"
137
+
stroke="currentColor"
138
+
stroke-width="2"
139
+
stroke-linecap="round"
140
+
stroke-linejoin="round"
141
+
class="lucide lucide-aarrow-down-icon lucide-a-arrow-down"
142
+
><path d="m14 12 4 4 4-4" /><path d="M18 16V7" /><path
143
+
d="m2 16 4.039-9.69a.5.5 0 0 1 .923 0L11 16"
144
+
/><path d="M3.304 13h6.392" /></svg
145
+
>
146
+
</Button>
147
+
<Button
148
+
variant="ghost"
149
+
onclick={() => {
150
+
item.cardData.textSize = Math.min((item.cardData.textSize ?? 0) + 1, 5);
151
+
}}
152
+
disabled={(item.cardData.textSize ?? 0) > 4}
153
+
>
154
+
<svg
155
+
xmlns="http://www.w3.org/2000/svg"
156
+
width="24"
157
+
height="24"
158
+
viewBox="0 0 24 24"
159
+
fill="none"
160
+
stroke="currentColor"
161
+
stroke-width="2"
162
+
stroke-linecap="round"
163
+
stroke-linejoin="round"
164
+
class="lucide lucide-aarrow-up-icon lucide-a-arrow-up"
165
+
><path d="m14 11 4-4 4 4" /><path d="M18 16V7" /><path
166
+
d="m2 16 4.039-9.69a.5.5 0 0 1 .923 0L11 16"
167
+
/><path d="M3.304 13h6.392" /></svg
168
+
>
169
+
</Button>
170
+
</div>
171
</div>
+10
-1
src/lib/cards/TextCard/index.ts
···
24
};
25
26
export const verticalAlignClasses: Record<string, string> = {
27
-
top: 'items-start',
28
center: 'items-center-safe',
29
bottom: 'items-end-safe'
30
};
31
0
0
0
0
0
0
0
0
0
···
24
};
25
26
export const verticalAlignClasses: Record<string, string> = {
27
+
top: 'items-stretch',
28
center: 'items-center-safe',
29
bottom: 'items-end-safe'
30
};
31
32
+
export const textSizeClasses = [
33
+
'text-lg',
34
+
'text-xl',
35
+
'text-2xl',
36
+
'text-3xl',
37
+
'text-4xl',
38
+
'text-5xl'
39
+
];
40
+
+2
src/lib/cards/types.ts
···
60
maxH?: number;
61
62
canResize?: boolean;
0
0
63
};
···
60
maxH?: number;
61
62
canResize?: boolean;
63
+
64
+
onUrlHandler?: (url: string, item: Item) => Item | null;
65
};
+17
-6
src/lib/cards/utils/MarkdownTextEditor.svelte
···
10
import TurndownService from 'turndown';
11
import { RichTextLink } from './extensions/RichTextLink';
12
import type { Item } from '$lib/types';
0
13
14
let element: HTMLElement | undefined = $state();
15
-
let editor: Editor | null = $state(null);
16
17
let loaded = $state(false);
18
19
let {
0
20
item = $bindable(),
21
placeholder = '',
22
defaultContent = ''
23
}: {
0
24
item: Item;
25
placeholder?: string;
26
defaultContent?: string;
···
50
51
// parse to json
52
json = generateJSON(html, [
53
-
StarterKit.configure(),
0
0
0
0
54
Image.configure(),
55
RichTextLink.configure({
56
openOnClick: false
···
61
}
62
63
let extensions: Extensions = [
64
-
StarterKit.configure(),
0
0
0
0
0
65
Image.configure(),
66
Link.configure({
67
openOnClick: false
···
92
93
editorProps: {
94
attributes: {
95
-
class: 'outline-none'
96
},
97
-
handleDOMEvents: { drop: () => true }
98
}
99
});
100
···
108
});
109
</script>
110
111
-
<div bind:this={element}></div>
112
113
<style>
114
:global(.tiptap p.is-editor-empty:first-child::before) {
···
10
import TurndownService from 'turndown';
11
import { RichTextLink } from './extensions/RichTextLink';
12
import type { Item } from '$lib/types';
13
+
import { textAlignClasses, verticalAlignClasses } from '../TextCard';
14
15
let element: HTMLElement | undefined = $state();
0
16
17
let loaded = $state(false);
18
19
let {
20
+
editor = $bindable(),
21
item = $bindable(),
22
placeholder = '',
23
defaultContent = ''
24
}: {
25
+
editor: Editor | null;
26
item: Item;
27
placeholder?: string;
28
defaultContent?: string;
···
52
53
// parse to json
54
json = generateJSON(html, [
55
+
StarterKit.configure({
56
+
heading: false,
57
+
bulletList: false,
58
+
codeBlock: false
59
+
}),
60
Image.configure(),
61
RichTextLink.configure({
62
openOnClick: false
···
67
}
68
69
let extensions: Extensions = [
70
+
StarterKit.configure({
71
+
heading: false,
72
+
bulletList: false,
73
+
codeBlock: false,
74
+
dropcursor: false
75
+
}),
76
Image.configure(),
77
Link.configure({
78
openOnClick: false
···
103
104
editorProps: {
105
attributes: {
106
+
class: 'outline-none w-full'
107
},
108
+
handleDOMEvents: { drop: () => false }
109
}
110
});
111
···
119
});
120
</script>
121
122
+
<div class="w-full" bind:this={element}></div>
123
124
<style>
125
:global(.tiptap p.is-editor-empty:first-child::before) {
+66
src/lib/components/ImageDropper.svelte
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
<script lang="ts">
2
+
import { Portal } from 'bits-ui';
3
+
4
+
let isDragOver = $state(false);
5
+
6
+
let {
7
+
processImageFile
8
+
}: {
9
+
processImageFile: (file: File) => Promise<void>;
10
+
} = $props();
11
+
12
+
function handleDragOver(event: DragEvent) {
13
+
event.preventDefault();
14
+
event.stopPropagation();
15
+
16
+
const dt = event.dataTransfer;
17
+
if (!dt) return;
18
+
19
+
let imageCount = 0;
20
+
if (dt.items) {
21
+
for (let i = 0; i < dt.items.length; i++) {
22
+
const item = dt.items[i];
23
+
if (item && item.kind === 'file' && item.type.startsWith('image/')) {
24
+
imageCount++;
25
+
}
26
+
}
27
+
} else if (dt.files) {
28
+
for (let i = 0; i < dt.files.length; i++) {
29
+
const file = dt.files[i];
30
+
if (file?.type.startsWith('image/')) {
31
+
imageCount++;
32
+
}
33
+
}
34
+
}
35
+
36
+
isDragOver = imageCount > 0;
37
+
}
38
+
function handleDragLeave(event: DragEvent) {
39
+
event.preventDefault();
40
+
event.stopPropagation();
41
+
isDragOver = false;
42
+
}
43
+
async function handleDrop(event: DragEvent) {
44
+
event.preventDefault();
45
+
event.stopPropagation();
46
+
isDragOver = false;
47
+
if (!event.dataTransfer?.files?.length) return;
48
+
for (const file of event.dataTransfer.files) {
49
+
if (file?.type.startsWith('image/')) {
50
+
await processImageFile(file);
51
+
}
52
+
}
53
+
}
54
+
</script>
55
+
56
+
<svelte:window ondragover={handleDragOver} ondragleave={handleDragLeave} ondrop={handleDrop} />
57
+
58
+
{#if isDragOver}
59
+
<Portal>
60
+
<div
61
+
class="bg-base-100/80 dark:bg-base-900/80 text-primary dark:text-base-100 pointer-events-none absolute inset-0 z-[1000] flex items-center justify-center text-4xl font-bold backdrop-blur-md"
62
+
>
63
+
Drop file to add it to your message
64
+
</div>
65
+
</Portal>
66
+
{/if}