Skip to content

Commit a8aecbe

Browse files
committed
feat: release v1.0.8 with theme motion enhancements, API improvements, and documentation updates
1 parent 444df3c commit a8aecbe

8 files changed

Lines changed: 187 additions & 15 deletions

File tree

CHANGELOG.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,32 @@ This project follows [Semantic Versioning](https://iconical.dev/versioning).
1111

1212
---
1313

14+
## v1.0.8 – Theme Motion, Docs, and About Polish 🎬
15+
16+
**Released: February 22, 2026**
17+
18+
### 🎨 Theme & Appearance
19+
20+
- Added smooth circular theme/scheme reveal transitions (View Transitions API) with reduced-motion fallback.
21+
- Tuned reveal speed/easing for a slower, smoother expansion and reduced text flashing during transitions.
22+
23+
### 🔌 API & Shortcuts
24+
25+
- Improved `/api/v1/remote-upload` payload compatibility by accepting single URL shapes (`url` and string `urls`) in addition to array payloads.
26+
27+
### 📚 Docs
28+
29+
- Rewrote the Apple Shortcuts guide into a concise 0→hero flow.
30+
- Added clear BETA + coming-soon messaging for notes, bookmarks, snippets, recipes, and game list mentions.
31+
32+
### 🧭 About Page
33+
34+
- Added Product Hunt badge embed with light/dark theme variants.
35+
- Fixed `FeatureChip` text contrast in light mode.
36+
- Added a coming-soon note for **Profiles & meetings** in CE About.
37+
38+
---
39+
1440
## v1.0.7 – API Key Allowlist for Remote Uploads 🔐
1541

1642
**Released: February 22, 2026**

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "swush",
3-
"version": "1.0.7",
3+
"version": "1.0.8",
44
"private": true,
55
"description": "Swush; A secure, self-hosted file sharing app with privacy-first features.",
66
"author": {

src/app/(dash)/about/page.tsx

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,6 @@ export default function AboutPage() {
8484
<Badge className="gap-1">
8585
<IconUpload size={16} /> Upload Requests
8686
</Badge>
87-
<Badge className="gap-1">
88-
<IconCalendarEvent size={16} /> Meetings
89-
</Badge>
9087
<Badge className="gap-1">
9188
<IconSearch size={16} /> Fast Search
9289
</Badge>
@@ -99,6 +96,25 @@ export default function AboutPage() {
9996
Create, search, share (public or password‑protected), and ship
10097
beautiful public pages with QR + UTM baked in.
10198
</p>
99+
<a
100+
href="https://www.producthunt.com/products/swush/launches/swush?embed=true&utm_source=badge-featured&utm_medium=badge&utm_campaign=badge-swush"
101+
target="_blank"
102+
rel="noopener noreferrer"
103+
className="mt-2 inline-block"
104+
>
105+
<picture>
106+
<source
107+
media="(prefers-color-scheme: dark)"
108+
srcSet="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=1075424&theme=dark&t=1771759369691"
109+
/>
110+
<img
111+
alt="Swush - The self-hosted power tool that just gets things done | Product Hunt"
112+
width={250}
113+
height={54}
114+
src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=1075424&theme=light&t=1771759357743"
115+
/>
116+
</picture>
117+
</a>
102118
</div>
103119

104120
<TabsList className="mx-auto w-full max-w-3xl grid grid-cols-3 gap-2 mb-4">
@@ -215,6 +231,9 @@ function AboutSection() {
215231
<CardTitle className="flex items-center gap-2 text-lg">
216232
<IconCalendarEvent size={18} /> Profiles & meetings
217233
</CardTitle>
234+
<Badge variant="default">
235+
Note: Profiles & meetings are still coming soon... stay tuned.
236+
</Badge>
218237
<CardDescription>
219238
Public profiles, socials, and booking links.
220239
</CardDescription>
@@ -372,10 +391,8 @@ function HowToSection() {
372391
</ol>
373392
</CardContent>
374393
</Card>
375-
</section>
376394

377-
<section className="grid gap-4 md:grid-cols-2">
378-
<Card className="border-primary/20">
395+
<Card className="border-primary/20 col-span-2">
379396
<CardHeader>
380397
<CardTitle className="flex items-center gap-2 text-lg">
381398
<IconUpload size={18} /> Upload links & requests
@@ -622,7 +639,7 @@ function FeatureChip({
622639
<div
623640
className={cn(
624641
"inline-flex items-center gap-2 rounded-md border px-2.5 py-1.5",
625-
"border-primary/20 bg-primary/5 text-purple-100 dark:text-purple-200",
642+
"border-primary/20 bg-primary/5 text-primary",
626643
)}
627644
>
628645
<span className="opacity-90">{icon}</span>

src/app/api/v1/remote-upload/route.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,32 @@ export const POST = withApiError(async function POST(req: NextRequest) {
4848
const body = await req.json();
4949
let items: { url: string; name?: string }[] = [];
5050
if (Array.isArray(body.urls)) {
51-
items = body.urls.map((url: string) => ({ url }));
51+
items = body.urls
52+
.filter((url: unknown) => typeof url === "string" && url.trim())
53+
.map((url: string) => ({ url: url.trim() }));
54+
} else if (typeof body.urls === "string" && body.urls.trim()) {
55+
items = [{ url: body.urls.trim() }];
56+
} else if (typeof body.url === "string" && body.url.trim()) {
57+
items = [
58+
{
59+
url: body.url.trim(),
60+
name: typeof body.name === "string" ? body.name : undefined,
61+
},
62+
];
5263
} else if (Array.isArray(body.items)) {
5364
items = body.items.filter(
5465
(item: RemoteUploadJob) => typeof item.url === "string",
5566
);
67+
} else if (body.items && typeof body.items === "object") {
68+
const single = body.items as { url?: unknown; name?: unknown };
69+
if (typeof single.url === "string" && single.url.trim()) {
70+
items = [
71+
{
72+
url: single.url.trim(),
73+
name: typeof single.name === "string" ? single.name : undefined,
74+
},
75+
];
76+
}
5677
}
5778
if (!items.length)
5879
return NextResponse.json(

src/components/Common/ThemeButton.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {
3434
} from "@/components/ui/select";
3535
import { Label } from "../ui/label";
3636
import { useColorScheme } from "@/hooks/use-scheme";
37+
import { setThemeWithCurtain } from "@/lib/theme-transition";
3738

3839
export default function ThemeButton() {
3940
const { theme, setTheme } = useTheme();
@@ -65,7 +66,7 @@ export default function ThemeButton() {
6566
onValueChange={(v: string) => {
6667
const next = v as (typeof SCHEMES)[number];
6768
setPendingScheme(next);
68-
setScheme(next);
69+
setThemeWithCurtain(setScheme, next);
6970
}}
7071
>
7172
<SelectTrigger className="w-full">
@@ -106,7 +107,7 @@ export default function ThemeButton() {
106107
value={pendingMode}
107108
onValueChange={(v) => {
108109
setPendingMode(v);
109-
setTheme(v);
110+
setThemeWithCurtain(setTheme, v);
110111
}}
111112
>
112113
<SelectTrigger className="w-full">

src/components/Dashboard/Sidebar.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import { useTheme } from "next-themes";
4545
import { useUser } from "@/hooks/use-user";
4646
import { useUserPreferences } from "@/hooks/use-user-preferences";
4747
import { useColorScheme } from "@/hooks/use-scheme";
48+
import { setThemeWithCurtain } from "@/lib/theme-transition";
4849
import {
4950
Dialog,
5051
DialogContent,
@@ -412,7 +413,7 @@ export function SidebarWrapper({ children }: { children: React.ReactNode }) {
412413
onValueChange={(v: string) => {
413414
const next = v as (typeof SCHEMES)[number];
414415
setPendingScheme(next);
415-
setScheme(next);
416+
setThemeWithCurtain(setScheme, next);
416417
}}
417418
>
418419
<SelectTrigger className="w-full">
@@ -436,7 +437,7 @@ export function SidebarWrapper({ children }: { children: React.ReactNode }) {
436437
value={pendingMode}
437438
onValueChange={(v) => {
438439
setPendingMode(v);
439-
setTheme(v);
440+
setThemeWithCurtain(setTheme, v);
440441
}}
441442
>
442443
<SelectTrigger className="w-full">

src/lib/theme-transition.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
export function setThemeWithCurtain(
2+
setTheme: (theme: string) => void,
3+
nextTheme: string,
4+
): void;
5+
export function setThemeWithCurtain<T extends string>(
6+
setTheme: (theme: T | ((prev: T) => T)) => void,
7+
nextTheme: T,
8+
): void;
9+
export function setThemeWithCurtain(
10+
setTheme: (theme: unknown) => void,
11+
nextTheme: string,
12+
) {
13+
if (typeof document === "undefined" || typeof window === "undefined") {
14+
setTheme(nextTheme);
15+
return;
16+
}
17+
18+
const prefersReducedMotion = window.matchMedia(
19+
"(prefers-reduced-motion: reduce)",
20+
).matches;
21+
22+
const doc = document as Document & {
23+
startViewTransition?: (callback: () => void) => { finished: Promise<void> };
24+
};
25+
26+
if (prefersReducedMotion || typeof doc.startViewTransition !== "function") {
27+
setTheme(nextTheme);
28+
return;
29+
}
30+
31+
const active = document.activeElement as HTMLElement | null;
32+
const rect = active?.getBoundingClientRect?.();
33+
34+
const originX = rect ? rect.left + rect.width / 2 : window.innerWidth - 48;
35+
const originY = rect ? rect.top + rect.height / 2 : 48;
36+
37+
const maxX = Math.max(originX, window.innerWidth - originX);
38+
const maxY = Math.max(originY, window.innerHeight - originY);
39+
const radius = Math.hypot(maxX, maxY);
40+
41+
const root = document.documentElement;
42+
root.style.setProperty("--theme-transition-x", `${originX}px`);
43+
root.style.setProperty("--theme-transition-y", `${originY}px`);
44+
root.style.setProperty("--theme-transition-radius", `${radius}px`);
45+
root.classList.add("theme-curtain-transition");
46+
47+
const transition = doc.startViewTransition(() => {
48+
setTheme(nextTheme);
49+
});
50+
51+
void transition.finished.finally(() => {
52+
root.classList.remove("theme-curtain-transition");
53+
root.style.removeProperty("--theme-transition-x");
54+
root.style.removeProperty("--theme-transition-y");
55+
root.style.removeProperty("--theme-transition-radius");
56+
});
57+
}

src/styles/_base.css

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222

2323
@layer base {
2424
* {
25-
@apply border-border outline-ring/50;
25+
@apply border-border outline-ring/50 transition-colors duration-200;
2626
}
2727
body {
2828
@apply bg-background text-foreground;
@@ -33,6 +33,55 @@ html,
3333
body,
3434
#__next,
3535
.app-root {
36-
transition: background-color 0.25s ease, color 0.25s ease,
36+
transition:
37+
background-color 0.25s ease,
38+
color 0.25s ease,
3739
border-color 0.25s ease;
3840
}
41+
42+
html.theme-curtain-transition,
43+
html.theme-curtain-transition * {
44+
transition: none !important;
45+
}
46+
47+
html.theme-curtain-transition::view-transition-group(root),
48+
html.theme-curtain-transition::view-transition-image-pair(root) {
49+
animation: none;
50+
}
51+
52+
html.theme-curtain-transition::view-transition-old(root) {
53+
animation: none;
54+
}
55+
56+
html.theme-curtain-transition::view-transition-new(root) {
57+
clip-path: circle(
58+
0 at var(--theme-transition-x, 50%) var(--theme-transition-y, 50%)
59+
);
60+
will-change: clip-path;
61+
animation: theme-radial-reveal 1080ms cubic-bezier(0.22, 0.61, 0.36, 1) both;
62+
}
63+
64+
@keyframes theme-radial-reveal {
65+
to {
66+
clip-path: circle(
67+
var(--theme-transition-radius, 150vmax) at var(--theme-transition-x, 50%)
68+
var(--theme-transition-y, 50%)
69+
);
70+
}
71+
}
72+
73+
@media (prefers-reduced-motion: reduce) {
74+
html,
75+
body,
76+
#__next,
77+
.app-root,
78+
* {
79+
transition: none !important;
80+
}
81+
82+
html.theme-curtain-transition::view-transition-old(root),
83+
html.theme-curtain-transition::view-transition-new(root) {
84+
animation: none !important;
85+
clip-path: none !important;
86+
}
87+
}

0 commit comments

Comments
 (0)