+2
-10
input.css
+2
-10
input.css
···
1
1
@import "tailwindcss";
2
2
3
-
.htmx-request.htmx-indicator {
4
-
display: inline;
5
-
}
6
-
.htmx-indicator {
7
-
display: none;
8
-
}
9
-
.htmx-request #submit-button {
10
-
opacity: 0.5;
11
-
pointer-events: none;
12
-
}
3
+
/* use to test light mode */
4
+
/* @custom-variant dark (&:where(.dark, .dark *)); */
+121
-2
main.tsx
+121
-2
main.tsx
···
59
59
const GOATCOUNTER_URL = Deno.env.get("GOATCOUNTER_URL");
60
60
61
61
let cssContentHash: string = "";
62
+
const staticJsFiles = new Map<string, string>();
62
63
63
64
bff({
64
65
appName: "Grain Social",
···
80
81
cssContentHash = Array.from(new Uint8Array(hashBuffer))
81
82
.map((b) => b.toString(16).padStart(2, "0"))
82
83
.join("");
84
+
for (const entry of Deno.readDirSync(join(Deno.cwd(), "static"))) {
85
+
if (entry.isFile && entry.name.endsWith(".js")) {
86
+
const fileContent = await Deno.readFile(
87
+
join(Deno.cwd(), "static", entry.name),
88
+
);
89
+
const hashBuffer = await crypto.subtle.digest("SHA-256", fileContent);
90
+
const hash = Array.from(new Uint8Array(hashBuffer))
91
+
.map((b) => b.toString(16).padStart(2, "0"))
92
+
.join("");
93
+
staticJsFiles.set(entry.name, hash);
94
+
}
95
+
}
83
96
},
84
97
onError: (err) => {
85
98
if (err instanceof UnauthorizedError) {
···
1059
1072
href="https://unpkg.com/@fortawesome/fontawesome-free@6.7.2/css/all.min.css"
1060
1073
preload
1061
1074
/>
1062
-
{scripts?.map((file) => <script key={file} src={`/static/${file}`} />)}
1075
+
{scripts?.map((file) => (
1076
+
<script
1077
+
key={file}
1078
+
src={`/static/${file}?${staticJsFiles.get(file)}`}
1079
+
/>
1080
+
))}
1063
1081
</head>
1064
1082
<body class="h-full w-full dark:bg-zinc-950 dark:text-white">
1065
1083
<Layout id="layout" class="border-zinc-200 dark:border-zinc-800">
···
1675
1693
)
1676
1694
: null}
1677
1695
</div>
1696
+
<div class="flex justify-end mb-2">
1697
+
<Button
1698
+
id="justified-button"
1699
+
variant="primary"
1700
+
class="flex justify-center w-full sm:w-fit bg-zinc-100 dark:bg-zinc-800 border-zinc-100 dark:border-zinc-800 data-[selected=false]:bg-transparent data-[selected=false]:border-transparent text-zinc-950 dark:text-zinc-50"
1701
+
_="on click call toggleLayout('justified')
1702
+
set @data-selected to 'true'
1703
+
set #masonry-button's @data-selected to 'false'"
1704
+
>
1705
+
<svg
1706
+
width="24"
1707
+
height="24"
1708
+
viewBox="0 0 24 24"
1709
+
xmlns="http://www.w3.org/2000/svg"
1710
+
>
1711
+
<rect x="2" y="2" width="8" height="6" fill="currentColor" rx="1" />
1712
+
<rect
1713
+
x="12"
1714
+
y="2"
1715
+
width="10"
1716
+
height="6"
1717
+
fill="currentColor"
1718
+
rx="1"
1719
+
/>
1720
+
<rect
1721
+
x="2"
1722
+
y="10"
1723
+
width="6"
1724
+
height="6"
1725
+
fill="currentColor"
1726
+
rx="1"
1727
+
/>
1728
+
<rect
1729
+
x="10"
1730
+
y="10"
1731
+
width="12"
1732
+
height="6"
1733
+
fill="currentColor"
1734
+
rx="1"
1735
+
/>
1736
+
<rect
1737
+
x="2"
1738
+
y="18"
1739
+
width="20"
1740
+
height="4"
1741
+
fill="currentColor"
1742
+
rx="1"
1743
+
/>
1744
+
</svg>
1745
+
</Button>
1746
+
<Button
1747
+
id="masonry-button"
1748
+
variant="primary"
1749
+
data-selected="false"
1750
+
class="flex justify-center w-full sm:w-fit bg-zinc-100 dark:bg-zinc-800 border-zinc-100 dark:border-zinc-800 data-[selected=false]:bg-transparent data-[selected=false]:border-transparent text-zinc-950 dark:text-zinc-50"
1751
+
_="on click call toggleLayout('masonry')
1752
+
set @data-selected to 'true'
1753
+
set #justified-button's @data-selected to 'false'"
1754
+
>
1755
+
<svg
1756
+
width="24"
1757
+
height="24"
1758
+
viewBox="0 0 24 24"
1759
+
xmlns="http://www.w3.org/2000/svg"
1760
+
>
1761
+
<rect x="2" y="2" width="8" height="8" fill="currentColor" rx="1" />
1762
+
<rect
1763
+
x="12"
1764
+
y="2"
1765
+
width="8"
1766
+
height="4"
1767
+
fill="currentColor"
1768
+
rx="1"
1769
+
/>
1770
+
<rect
1771
+
x="12"
1772
+
y="8"
1773
+
width="8"
1774
+
height="6"
1775
+
fill="currentColor"
1776
+
rx="1"
1777
+
/>
1778
+
<rect
1779
+
x="2"
1780
+
y="12"
1781
+
width="8"
1782
+
height="8"
1783
+
fill="currentColor"
1784
+
rx="1"
1785
+
/>
1786
+
<rect
1787
+
x="12"
1788
+
y="16"
1789
+
width="8"
1790
+
height="4"
1791
+
fill="currentColor"
1792
+
rx="1"
1793
+
/>
1794
+
</svg>
1795
+
</Button>
1796
+
</div>
1678
1797
<div
1679
1798
id="masonry-container"
1680
1799
class="h-0 overflow-hidden relative mx-auto w-full"
1681
-
_="on load or htmx:afterSettle call computeMasonry()"
1800
+
_="on load or htmx:afterSettle call computeLayout()"
1682
1801
>
1683
1802
{gallery.items?.filter(isPhotoView)?.length
1684
1803
? gallery?.items
+84
-2
static/masonry.js
+84
-2
static/masonry.js
···
1
1
// deno-lint-ignore-file
2
2
3
3
let masonryObserverInitialized = false;
4
+
let layoutMode = "justified";
5
+
6
+
function computeLayout() {
7
+
if (layoutMode === "masonry") {
8
+
computeMasonry();
9
+
} else {
10
+
computeJustified();
11
+
}
12
+
}
13
+
14
+
function toggleLayout(layout = "justified") {
15
+
layoutMode = layout;
16
+
computeLayout();
17
+
}
4
18
5
19
function computeMasonry() {
6
20
const container = document.getElementById("masonry-container");
···
52
66
container.style.height = `${Math.max(...columnHeights)}px`;
53
67
}
54
68
69
+
function computeJustified() {
70
+
const container = document.getElementById("masonry-container");
71
+
if (!container) return;
72
+
73
+
const spacing = 8;
74
+
const containerWidth = container.offsetWidth;
75
+
76
+
if (containerWidth === 0) {
77
+
requestAnimationFrame(computeJustified);
78
+
return;
79
+
}
80
+
81
+
const tiles = Array.from(container.querySelectorAll(".masonry-tile"));
82
+
let currentRow = [];
83
+
let rowAspectRatioSum = 0;
84
+
let yOffset = 0;
85
+
86
+
// Clear all styles before layout
87
+
tiles.forEach((tile) => {
88
+
Object.assign(tile.style, {
89
+
position: "absolute",
90
+
left: "0px",
91
+
top: "0px",
92
+
width: "auto",
93
+
height: "auto",
94
+
});
95
+
});
96
+
97
+
for (let i = 0; i < tiles.length; i++) {
98
+
const tile = tiles[i];
99
+
const imgW = parseFloat(tile.dataset.width);
100
+
const imgH = parseFloat(tile.dataset.height);
101
+
if (!imgW || !imgH) continue;
102
+
103
+
const aspectRatio = imgW / imgH;
104
+
currentRow.push({ tile, aspectRatio, imgW, imgH });
105
+
rowAspectRatioSum += aspectRatio;
106
+
107
+
// Estimate if row is "full" enough
108
+
const estimatedRowHeight =
109
+
(containerWidth - (currentRow.length - 1) * spacing) / rowAspectRatioSum;
110
+
111
+
// If height is reasonable or we're at the end, render the row
112
+
if (estimatedRowHeight < 300 || i === tiles.length - 1) {
113
+
let xOffset = 0;
114
+
115
+
for (const item of currentRow) {
116
+
const width = estimatedRowHeight * item.aspectRatio;
117
+
Object.assign(item.tile.style, {
118
+
position: "absolute",
119
+
top: `${yOffset}px`,
120
+
left: `${xOffset}px`,
121
+
width: `${width}px`,
122
+
height: `${estimatedRowHeight}px`,
123
+
});
124
+
xOffset += width + spacing;
125
+
}
126
+
127
+
yOffset += estimatedRowHeight + spacing;
128
+
currentRow = [];
129
+
rowAspectRatioSum = 0;
130
+
}
131
+
}
132
+
133
+
container.style.position = "relative";
134
+
container.style.height = `${yOffset}px`;
135
+
}
136
+
55
137
function observeMasonry() {
56
138
if (masonryObserverInitialized) return;
57
139
masonryObserverInitialized = true;
···
61
143
62
144
// Observe parent resize
63
145
if (typeof ResizeObserver !== "undefined") {
64
-
const resizeObserver = new ResizeObserver(() => computeMasonry());
146
+
const resizeObserver = new ResizeObserver(() => computeLayout());
65
147
if (container.parentElement) {
66
148
resizeObserver.observe(container.parentElement);
67
149
}
···
69
151
70
152
// Observe inner content changes (tiles being added/removed)
71
153
const mutationObserver = new MutationObserver(() => {
72
-
computeMasonry();
154
+
computeLayout();
73
155
});
74
156
75
157
mutationObserver.observe(container, {
+16
-10
static/styles.css
+16
-10
static/styles.css
···
414
414
.justify-center {
415
415
justify-content: center;
416
416
}
417
+
.justify-end {
418
+
justify-content: flex-end;
419
+
}
417
420
.gap-2 {
418
421
gap: calc(var(--spacing) * 2);
419
422
}
···
462
465
.border-b {
463
466
border-bottom-style: var(--tw-border-style);
464
467
border-bottom-width: 1px;
468
+
}
469
+
.border-zinc-100 {
470
+
border-color: var(--color-zinc-100);
465
471
}
466
472
.border-zinc-200 {
467
473
border-color: var(--color-zinc-200);
···
610
616
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
611
617
}
612
618
}
619
+
.data-\[selected\=false\]\:border-transparent {
620
+
&[data-selected="false"] {
621
+
border-color: transparent;
622
+
}
623
+
}
624
+
.data-\[selected\=false\]\:bg-transparent {
625
+
&[data-selected="false"] {
626
+
background-color: transparent;
627
+
}
628
+
}
613
629
.data-\[state\=pending\]\:opacity-50 {
614
630
&[data-state="pending"] {
615
631
opacity: 50%;
···
720
736
color: var(--color-zinc-500);
721
737
}
722
738
}
723
-
}
724
-
.htmx-request.htmx-indicator {
725
-
display: inline;
726
-
}
727
-
.htmx-indicator {
728
-
display: none;
729
-
}
730
-
.htmx-request #submit-button {
731
-
opacity: 0.5;
732
-
pointer-events: none;
733
739
}
734
740
@property --tw-space-y-reverse {
735
741
syntax: "*";