-
-
-
-
- Alpha
-
+ <>
+ {/* light mode background */}
+
+
+
-
-
- A social knowledge network for researchers
-
-
- Follow your peers’ research trails. Surface and discover new
- connections. Built on ATProto so you own your data.
-
-
+ {/* dark mode background */}
+
+
+
+ >
+ );
+}
-
-
+function Content() {
+ return (
+
+
+
+
+
+ Alpha
+
- }
- >
- Log in
-
-
+
+
+ A social knowledge network for researchers
+
+
+ Follow your peers’ research trails. Surface and discover new
+ connections. Built on ATProto so you own your data.
+
+
+ Follow your peers’ research trails. Surface and discover new
+ connections. Built on ATProto so you own your data.
+
-
-
+
+
+
+
A social knowledge network for researchers
+
+ {/* light mode subtitle */}
+
+ Follow your peers’ research trails. Surface and discover new
+ connections. Built on ATProto so you own your data.
+
+
+ {/* dark mode subtitle */}
Follow your peers’ research trails. Surface and discover new
connections. Built on ATProto so you own your data.
{process.env.VERCEL_ENV !== 'production' && (
-
+
@@ -101,52 +128,52 @@ export default function Home() {
spacing={{ base: 'xl' }}
mt={{ base: '1rem', xs: '5rem' }}
>
-
+
-
+
Curate your research trails.
{' '}
-
+
Collect interesting links, add notes, and organize them into
shareable collections. Build trails others can explore and
extend.
-
+
-
+
Connect with peers.
{' '}
-
+
See what your peers are sharing and find new collaborators
with shared interests. Experience research rabbit holes,
together.
-
+
-
+
Own your data.
{' '}
-
+
Built on ATProto, new apps will come to you. No more
rebuilding your social graph and data when apps pivot and
shut down.
-
+
-
+
See the bigger picture.
{' '}
-
+
Find relevant research based on your network. Get the extra
context that matters before you dive into a long read.
@@ -154,130 +181,133 @@ export default function Home() {
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- }
- >
- Follow our blog for updates
-
-
-
- Made by
-
-
-
-
-
-
-
- with support from
-
- Open Philanthropy
- {' '}
- and{' '}
-
- Astera
-
-
-
-
-
-
+
-
+ >
+ );
+}
+
+function Footer() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+ >
+ Follow our blog for updates
+
+
+
+
+ Made by
+
+
+ {/* light logo */}
+
+ {/* dark logo */}
+
+
+
+
+
+ with support from
+
+ Open Philanthropy
+ {' '}
+ and{' '}
+
+ Astera
+
+
+
+
+
+
);
}
--
2.51.2
From 19eddf72cfc4becdfe159dc1116a17515161681a Mon Sep 17 00:00:00 2001
From: Pouria Delfanazari
Date: Thu, 6 Nov 2025 10:33:35 -0800
Subject: [PATCH 06/18] fix: semble header theme color
---
.../sembleContainer/SembleHeaderBackground.tsx | 10 ++++------
1 file changed, 4 insertions(+), 6 deletions(-)
diff --git a/src/webapp/features/semble/containers/sembleContainer/SembleHeaderBackground.tsx b/src/webapp/features/semble/containers/sembleContainer/SembleHeaderBackground.tsx
index 084b886f..6fc190b5 100644
--- a/src/webapp/features/semble/containers/sembleContainer/SembleHeaderBackground.tsx
+++ b/src/webapp/features/semble/containers/sembleContainer/SembleHeaderBackground.tsx
@@ -1,23 +1,21 @@
-'use client';
-
-import { useColorScheme } from '@mantine/hooks';
import BG from '@/assets/semble-header-bg.webp';
import DarkBG from '@/assets/semble-header-bg-dark.webp';
import { Box, Image } from '@mantine/core';
export default function SembleHeaderBackground() {
- const colorScheme = useColorScheme();
-
return (
+
+
{/* White gradient overlay */}
Date: Thu, 6 Nov 2025 10:33:44 -0800
Subject: [PATCH 07/18] feat: enable auto theme switch
---
src/webapp/app/layout.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/webapp/app/layout.tsx b/src/webapp/app/layout.tsx
index b3d11418..8fd0e5af 100644
--- a/src/webapp/app/layout.tsx
+++ b/src/webapp/app/layout.tsx
@@ -26,7 +26,7 @@ export default function RootLayout({
{...mantineHtmlProps}
>
-
+ {children}
--
2.51.2
From b7b3e79e8ef6e5916256513438690aadd1c3bcc9 Mon Sep 17 00:00:00 2001
From: Pouria Delfanazari
Date: Thu, 6 Nov 2025 10:40:56 -0800
Subject: [PATCH 08/18] fix: note card author theme colour
---
.../features/notes/components/noteCard/NoteCard.tsx | 11 +++++++----
1 file changed, 7 insertions(+), 4 deletions(-)
diff --git a/src/webapp/features/notes/components/noteCard/NoteCard.tsx b/src/webapp/features/notes/components/noteCard/NoteCard.tsx
index 15250d89..d15d47b4 100644
--- a/src/webapp/features/notes/components/noteCard/NoteCard.tsx
+++ b/src/webapp/features/notes/components/noteCard/NoteCard.tsx
@@ -30,18 +30,21 @@ export default function NoteCard(props: Props) {
size={'sm'}
/>
-
+
{props.author.name}
- {' · '}
- {relativeCreateDate}
+
+ {' · '}
+
+
+ {relativeCreateDate}{' '}
+
--
2.51.2
From 0eee78dd5622e4ebcc549cf141784a7fd8dfa259 Mon Sep 17 00:00:00 2001
From: Pouria Delfanazari
Date: Thu, 6 Nov 2025 10:41:07 -0800
Subject: [PATCH 09/18] fix: collection card author theme colour
---
.../collections/components/collectionCard/CollectionCard.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/webapp/features/collections/components/collectionCard/CollectionCard.tsx b/src/webapp/features/collections/components/collectionCard/CollectionCard.tsx
index d02a1b7d..4a910723 100644
--- a/src/webapp/features/collections/components/collectionCard/CollectionCard.tsx
+++ b/src/webapp/features/collections/components/collectionCard/CollectionCard.tsx
@@ -58,7 +58,7 @@ export default function CollectionCard(props: Props) {
size={'sm'}
/>
-
+
{collection.author.name}
--
2.51.2
From b30e1f88c66930844b924f3b50da8a9b45899e5c Mon Sep 17 00:00:00 2001
From: Pouria Delfanazari
Date: Thu, 6 Nov 2025 10:41:20 -0800
Subject: [PATCH 10/18] fix: semble aside added recently author theme colour
---
.../features/semble/containers/sembleAside/SembleAside.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/webapp/features/semble/containers/sembleAside/SembleAside.tsx b/src/webapp/features/semble/containers/sembleAside/SembleAside.tsx
index 0ab11676..c458ae9b 100644
--- a/src/webapp/features/semble/containers/sembleAside/SembleAside.tsx
+++ b/src/webapp/features/semble/containers/sembleAside/SembleAside.tsx
@@ -45,7 +45,7 @@ export default async function SembleAside(props: Props) {
{lib.user.name}
- {lib.user.handle}
+ @{lib.user.handle}
--
2.51.2
From a0540c160f6f92095b96ad31ac2a14dabefc1fec Mon Sep 17 00:00:00 2001
From: Pouria Delfanazari
Date: Thu, 6 Nov 2025 10:43:47 -0800
Subject: [PATCH 11/18] fix: collection container author theme colour
---
.../containers/collectionContainer/CollectionContainer.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/webapp/features/collections/containers/collectionContainer/CollectionContainer.tsx b/src/webapp/features/collections/containers/collectionContainer/CollectionContainer.tsx
index 3ac486ce..326ee702 100644
--- a/src/webapp/features/collections/containers/collectionContainer/CollectionContainer.tsx
+++ b/src/webapp/features/collections/containers/collectionContainer/CollectionContainer.tsx
@@ -68,7 +68,7 @@ export default function CollectionContainer(props: Props) {
-
+
By
--
2.51.2
From af42a5ae19dc8225dde331342a1bd5c3284fde85 Mon Sep 17 00:00:00 2001
From: Pouria Delfanazari
Date: Thu, 6 Nov 2025 10:46:32 -0800
Subject: [PATCH 12/18] feat: move theme toggle above log out
---
.../components/profileMenu/ProfileMenu.tsx | 20 +++++++++----------
1 file changed, 10 insertions(+), 10 deletions(-)
diff --git a/src/webapp/features/profile/components/profileMenu/ProfileMenu.tsx b/src/webapp/features/profile/components/profileMenu/ProfileMenu.tsx
index 3dd00316..c35f071f 100644
--- a/src/webapp/features/profile/components/profileMenu/ProfileMenu.tsx
+++ b/src/webapp/features/profile/components/profileMenu/ProfileMenu.tsx
@@ -104,16 +104,6 @@ export default function ProfileMenu() {
Submit an issue
- }
- onClick={handleLogout}
- >
- Log out
-
-
-
-
+
+
+ }
+ onClick={handleLogout}
+ >
+ Log out
+
+
Date: Thu, 6 Nov 2025 14:25:14 -0500
Subject: [PATCH 13/18] feat: add trailing slash only for bare root URLs
without paths or parameters
Co-authored-by: aider (anthropic/claude-sonnet-4-20250514)
---
src/modules/cards/domain/value-objects/URL.ts | 17 +++++++++++++++--
1 file changed, 15 insertions(+), 2 deletions(-)
diff --git a/src/modules/cards/domain/value-objects/URL.ts b/src/modules/cards/domain/value-objects/URL.ts
index 2307012b..772ee098 100644
--- a/src/modules/cards/domain/value-objects/URL.ts
+++ b/src/modules/cards/domain/value-objects/URL.ts
@@ -30,8 +30,21 @@ export class URL extends ValueObject {
try {
// Validate URL format using the global URL constructor
- new globalThis.URL(trimmedUrl);
- return ok(new URL({ value: trimmedUrl }));
+ const parsedUrl = new globalThis.URL(trimmedUrl);
+
+ // Add trailing slash only to truly bare root URLs
+ // (no path, no query parameters, no fragments)
+ let normalizedUrl = trimmedUrl;
+ if (
+ (parsedUrl.pathname === '' || parsedUrl.pathname === '/') &&
+ parsedUrl.search === '' &&
+ parsedUrl.hash === '' &&
+ !trimmedUrl.endsWith('/')
+ ) {
+ normalizedUrl = trimmedUrl + '/';
+ }
+
+ return ok(new URL({ value: normalizedUrl }));
} catch (error) {
return err(new InvalidURLError('Invalid URL format'));
}
--
2.51.2
From d33f1536f57d9445fc5d40af0fedc32e2ba8e370 Mon Sep 17 00:00:00 2001
From: Wesley Finck
Date: Thu, 6 Nov 2025 14:27:08 -0500
Subject: [PATCH 14/18] test: add comprehensive unit tests for URL value object
Co-authored-by: aider (anthropic/claude-sonnet-4-20250514)
---
.../tests/domain/value-objects/URL.test.ts | 367 ++++++++++++++++++
1 file changed, 367 insertions(+)
create mode 100644 src/modules/cards/tests/domain/value-objects/URL.test.ts
diff --git a/src/modules/cards/tests/domain/value-objects/URL.test.ts b/src/modules/cards/tests/domain/value-objects/URL.test.ts
new file mode 100644
index 00000000..3f0d926d
--- /dev/null
+++ b/src/modules/cards/tests/domain/value-objects/URL.test.ts
@@ -0,0 +1,367 @@
+import { URL, InvalidURLError } from '../../../domain/value-objects/URL';
+
+describe('URL Value Object', () => {
+ describe('create', () => {
+ describe('valid URLs', () => {
+ it('should create URL with valid http URL', () => {
+ const result = URL.create('http://example.com');
+
+ expect(result.isOk()).toBe(true);
+ expect(result.unwrap().value).toBe('http://example.com/');
+ });
+
+ it('should create URL with valid https URL', () => {
+ const result = URL.create('https://example.com');
+
+ expect(result.isOk()).toBe(true);
+ expect(result.unwrap().value).toBe('https://example.com/');
+ });
+
+ it('should create URL with subdomain', () => {
+ const result = URL.create('https://www.example.com');
+
+ expect(result.isOk()).toBe(true);
+ expect(result.unwrap().value).toBe('https://www.example.com/');
+ });
+
+ it('should create URL with port', () => {
+ const result = URL.create('https://example.com:8080');
+
+ expect(result.isOk()).toBe(true);
+ expect(result.unwrap().value).toBe('https://example.com:8080/');
+ });
+ });
+
+ describe('trailing slash normalization', () => {
+ describe('should add trailing slash to bare root domains', () => {
+ it('should add trailing slash to http://example.com', () => {
+ const result = URL.create('http://example.com');
+
+ expect(result.isOk()).toBe(true);
+ expect(result.unwrap().value).toBe('http://example.com/');
+ });
+
+ it('should add trailing slash to https://example.com', () => {
+ const result = URL.create('https://example.com');
+
+ expect(result.isOk()).toBe(true);
+ expect(result.unwrap().value).toBe('https://example.com/');
+ });
+
+ it('should add trailing slash to https://www.example.com', () => {
+ const result = URL.create('https://www.example.com');
+
+ expect(result.isOk()).toBe(true);
+ expect(result.unwrap().value).toBe('https://www.example.com/');
+ });
+
+ it('should add trailing slash to https://example.com:8080', () => {
+ const result = URL.create('https://example.com:8080');
+
+ expect(result.isOk()).toBe(true);
+ expect(result.unwrap().value).toBe('https://example.com:8080/');
+ });
+
+ it('should add trailing slash to https://sub.domain.example.com', () => {
+ const result = URL.create('https://sub.domain.example.com');
+
+ expect(result.isOk()).toBe(true);
+ expect(result.unwrap().value).toBe('https://sub.domain.example.com/');
+ });
+ });
+
+ describe('should NOT add trailing slash when already present', () => {
+ it('should not add trailing slash to https://example.com/', () => {
+ const result = URL.create('https://example.com/');
+
+ expect(result.isOk()).toBe(true);
+ expect(result.unwrap().value).toBe('https://example.com/');
+ });
+
+ it('should not add trailing slash to https://www.example.com/', () => {
+ const result = URL.create('https://www.example.com/');
+
+ expect(result.isOk()).toBe(true);
+ expect(result.unwrap().value).toBe('https://www.example.com/');
+ });
+
+ it('should not add trailing slash to https://example.com:8080/', () => {
+ const result = URL.create('https://example.com:8080/');
+
+ expect(result.isOk()).toBe(true);
+ expect(result.unwrap().value).toBe('https://example.com:8080/');
+ });
+ });
+
+ describe('should NOT add trailing slash to URLs with paths', () => {
+ it('should not add trailing slash to https://example.com/path', () => {
+ const result = URL.create('https://example.com/path');
+
+ expect(result.isOk()).toBe(true);
+ expect(result.unwrap().value).toBe('https://example.com/path');
+ });
+
+ it('should not add trailing slash to https://example.com/path/subpath', () => {
+ const result = URL.create('https://example.com/path/subpath');
+
+ expect(result.isOk()).toBe(true);
+ expect(result.unwrap().value).toBe(
+ 'https://example.com/path/subpath',
+ );
+ });
+
+ it('should not add trailing slash to https://example.com/path/', () => {
+ const result = URL.create('https://example.com/path/');
+
+ expect(result.isOk()).toBe(true);
+ expect(result.unwrap().value).toBe('https://example.com/path/');
+ });
+
+ it('should not add trailing slash to https://example.com/path.html', () => {
+ const result = URL.create('https://example.com/path.html');
+
+ expect(result.isOk()).toBe(true);
+ expect(result.unwrap().value).toBe('https://example.com/path.html');
+ });
+
+ it('should not add trailing slash to https://example.com/api/v1/users', () => {
+ const result = URL.create('https://example.com/api/v1/users');
+
+ expect(result.isOk()).toBe(true);
+ expect(result.unwrap().value).toBe(
+ 'https://example.com/api/v1/users',
+ );
+ });
+ });
+
+ describe('should NOT add trailing slash to URLs with query parameters', () => {
+ it('should not add trailing slash to https://example.com?param=value', () => {
+ const result = URL.create('https://example.com?param=value');
+
+ expect(result.isOk()).toBe(true);
+ expect(result.unwrap().value).toBe('https://example.com?param=value');
+ });
+
+ it('should not add trailing slash to https://example.com?param1=value1¶m2=value2', () => {
+ const result = URL.create(
+ 'https://example.com?param1=value1¶m2=value2',
+ );
+
+ expect(result.isOk()).toBe(true);
+ expect(result.unwrap().value).toBe(
+ 'https://example.com?param1=value1¶m2=value2',
+ );
+ });
+
+ it('should not add trailing slash to https://example.com/?param=value', () => {
+ const result = URL.create('https://example.com/?param=value');
+
+ expect(result.isOk()).toBe(true);
+ expect(result.unwrap().value).toBe(
+ 'https://example.com/?param=value',
+ );
+ });
+
+ it('should not add trailing slash to https://example.com/path?param=value', () => {
+ const result = URL.create('https://example.com/path?param=value');
+
+ expect(result.isOk()).toBe(true);
+ expect(result.unwrap().value).toBe(
+ 'https://example.com/path?param=value',
+ );
+ });
+ });
+
+ describe('should NOT add trailing slash to URLs with fragments', () => {
+ it('should not add trailing slash to https://example.com#section', () => {
+ const result = URL.create('https://example.com#section');
+
+ expect(result.isOk()).toBe(true);
+ expect(result.unwrap().value).toBe('https://example.com#section');
+ });
+
+ it('should not add trailing slash to https://example.com/#section', () => {
+ const result = URL.create('https://example.com/#section');
+
+ expect(result.isOk()).toBe(true);
+ expect(result.unwrap().value).toBe('https://example.com/#section');
+ });
+
+ it('should not add trailing slash to https://example.com/path#section', () => {
+ const result = URL.create('https://example.com/path#section');
+
+ expect(result.isOk()).toBe(true);
+ expect(result.unwrap().value).toBe(
+ 'https://example.com/path#section',
+ );
+ });
+
+ it('should not add trailing slash to https://example.com?param=value#section', () => {
+ const result = URL.create('https://example.com?param=value#section');
+
+ expect(result.isOk()).toBe(true);
+ expect(result.unwrap().value).toBe(
+ 'https://example.com?param=value#section',
+ );
+ });
+ });
+
+ describe('should NOT add trailing slash to URLs with query parameters AND fragments', () => {
+ it('should not add trailing slash to https://example.com?param=value#section', () => {
+ const result = URL.create('https://example.com?param=value#section');
+
+ expect(result.isOk()).toBe(true);
+ expect(result.unwrap().value).toBe(
+ 'https://example.com?param=value#section',
+ );
+ });
+
+ it('should not add trailing slash to https://example.com/path?param=value#section', () => {
+ const result = URL.create(
+ 'https://example.com/path?param=value#section',
+ );
+
+ expect(result.isOk()).toBe(true);
+ expect(result.unwrap().value).toBe(
+ 'https://example.com/path?param=value#section',
+ );
+ });
+ });
+ });
+
+ describe('edge cases', () => {
+ it('should handle URLs with IP addresses', () => {
+ const result = URL.create('http://192.168.1.1');
+
+ expect(result.isOk()).toBe(true);
+ expect(result.unwrap().value).toBe('http://192.168.1.1/');
+ });
+
+ it('should handle URLs with IP addresses and ports', () => {
+ const result = URL.create('http://192.168.1.1:8080');
+
+ expect(result.isOk()).toBe(true);
+ expect(result.unwrap().value).toBe('http://192.168.1.1:8080/');
+ });
+
+ it('should handle localhost', () => {
+ const result = URL.create('http://localhost');
+
+ expect(result.isOk()).toBe(true);
+ expect(result.unwrap().value).toBe('http://localhost/');
+ });
+
+ it('should handle localhost with port', () => {
+ const result = URL.create('http://localhost:3000');
+
+ expect(result.isOk()).toBe(true);
+ expect(result.unwrap().value).toBe('http://localhost:3000/');
+ });
+
+ it('should trim whitespace before processing', () => {
+ const result = URL.create(' https://example.com ');
+
+ expect(result.isOk()).toBe(true);
+ expect(result.unwrap().value).toBe('https://example.com/');
+ });
+
+ it('should handle URLs with authentication', () => {
+ const result = URL.create('https://user:pass@example.com');
+
+ expect(result.isOk()).toBe(true);
+ expect(result.unwrap().value).toBe('https://user:pass@example.com/');
+ });
+ it('should handle URLs with multiple slashes', () => {
+ const result = URL.create('https://example.com//');
+
+ expect(result.isOk()).toBe(true);
+ expect(result.unwrap().value).toBe('https://example.com//');
+ });
+ });
+
+ describe('invalid URLs', () => {
+ it('should fail for empty string', () => {
+ const result = URL.create('');
+
+ if (!result.isErr()) {
+ throw new Error('Expected result to be an error');
+ }
+ expect(result.isErr()).toBe(true);
+ expect(result.error).toBeInstanceOf(InvalidURLError);
+ expect(result.error.message).toBe('URL cannot be empty');
+ });
+
+ it('should fail for whitespace only', () => {
+ const result = URL.create(' ');
+
+ if (!result.isErr()) {
+ throw new Error('Expected result to be an error');
+ }
+ expect(result.isErr()).toBe(true);
+ expect(result.error).toBeInstanceOf(InvalidURLError);
+ expect(result.error.message).toBe('URL cannot be empty');
+ });
+
+ it('should fail for invalid URL format', () => {
+ const result = URL.create('not-a-url');
+
+ if (!result.isErr()) {
+ throw new Error('Expected result to be an error');
+ }
+ expect(result.isErr()).toBe(true);
+ expect(result.error).toBeInstanceOf(InvalidURLError);
+ expect(result.error.message).toBe('Invalid URL format');
+ });
+
+ it('should fail for URL without protocol', () => {
+ const result = URL.create('example.com');
+
+ if (!result.isErr()) {
+ throw new Error('Expected result to be an error');
+ }
+ expect(result.isErr()).toBe(true);
+ expect(result.error).toBeInstanceOf(InvalidURLError);
+ expect(result.error.message).toBe('Invalid URL format');
+ });
+
+ it('should fail for malformed URL', () => {
+ const result = URL.create('https://');
+
+ if (!result.isErr()) {
+ throw new Error('Expected result to be an error');
+ }
+ expect(result.isErr()).toBe(true);
+ expect(result.error).toBeInstanceOf(InvalidURLError);
+ expect(result.error.message).toBe('Invalid URL format');
+ });
+ });
+ });
+
+ describe('toString', () => {
+ it('should return the URL value as string', () => {
+ const url = URL.create('https://example.com/path').unwrap();
+
+ expect(url.toString()).toBe('https://example.com/path');
+ });
+
+ it('should return normalized URL with trailing slash for bare domains', () => {
+ const url = URL.create('https://example.com').unwrap();
+
+ expect(url.toString()).toBe('https://example.com/');
+ });
+ });
+
+ describe('value getter', () => {
+ it('should return the URL value', () => {
+ const url = URL.create('https://example.com/path').unwrap();
+
+ expect(url.value).toBe('https://example.com/path');
+ });
+
+ it('should return normalized URL with trailing slash for bare domains', () => {
+ const url = URL.create('https://example.com').unwrap();
+
+ expect(url.value).toBe('https://example.com/');
+ });
+ });
+});
--
2.51.2
From 2472e104435a759bef650005f0eb16b600673a34 Mon Sep 17 00:00:00 2001
From: Wesley Finck
Date: Thu, 6 Nov 2025 14:34:28 -0500
Subject: [PATCH 15/18] formatting
---
src/modules/cards/domain/value-objects/URL.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/modules/cards/domain/value-objects/URL.ts b/src/modules/cards/domain/value-objects/URL.ts
index 772ee098..689a58fd 100644
--- a/src/modules/cards/domain/value-objects/URL.ts
+++ b/src/modules/cards/domain/value-objects/URL.ts
@@ -31,7 +31,7 @@ export class URL extends ValueObject {
try {
// Validate URL format using the global URL constructor
const parsedUrl = new globalThis.URL(trimmedUrl);
-
+
// Add trailing slash only to truly bare root URLs
// (no path, no query parameters, no fragments)
let normalizedUrl = trimmedUrl;
@@ -43,7 +43,7 @@ export class URL extends ValueObject {
) {
normalizedUrl = trimmedUrl + '/';
}
-
+
return ok(new URL({ value: normalizedUrl }));
} catch (error) {
return err(new InvalidURLError('Invalid URL format'));
--
2.51.2
From f274f44a3f2da0f76135e267ae42ce82460a0729 Mon Sep 17 00:00:00 2001
From: Pouria Delfanazari
Date: Thu, 6 Nov 2025 11:42:10 -0800
Subject: [PATCH 16/18] fix: log in button theme colour
---
src/webapp/app/page.tsx | 2 +-
src/webapp/features/auth/components/loginForm/LoginForm.tsx | 4 ++--
src/webapp/features/auth/components/signUpForm/SignUpForm.tsx | 2 +-
.../semble/containers/sembleContainer/SembleContainer.tsx | 2 +-
4 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/src/webapp/app/page.tsx b/src/webapp/app/page.tsx
index 5d01945a..601da013 100644
--- a/src/webapp/app/page.tsx
+++ b/src/webapp/app/page.tsx
@@ -114,7 +114,7 @@ function Content() {
component={Link}
href="/login"
size="lg"
- color="dark"
+ color="var(--mantine-color-dark-filled)"
rightSection={}
>
Log in
diff --git a/src/webapp/features/auth/components/loginForm/LoginForm.tsx b/src/webapp/features/auth/components/loginForm/LoginForm.tsx
index 37795e7f..3cddc73c 100644
--- a/src/webapp/features/auth/components/loginForm/LoginForm.tsx
+++ b/src/webapp/features/auth/components/loginForm/LoginForm.tsx
@@ -177,7 +177,7 @@ export default function LoginForm() {
}
loading={isLoading}
@@ -226,7 +226,7 @@ export default function LoginForm() {
}
loading={isLoading}
diff --git a/src/webapp/features/auth/components/signUpForm/SignUpForm.tsx b/src/webapp/features/auth/components/signUpForm/SignUpForm.tsx
index b272bba3..c18c0053 100644
--- a/src/webapp/features/auth/components/signUpForm/SignUpForm.tsx
+++ b/src/webapp/features/auth/components/signUpForm/SignUpForm.tsx
@@ -9,7 +9,7 @@ export default function SignUpForm() {
href="https://bsky.app/"
target="_blank"
size="lg"
- color="dark"
+ color="var(--mantine-color-dark-filled)"
leftSection={}
>
Sign up on Bluesky
diff --git a/src/webapp/features/semble/containers/sembleContainer/SembleContainer.tsx b/src/webapp/features/semble/containers/sembleContainer/SembleContainer.tsx
index 8a2750b6..50766530 100644
--- a/src/webapp/features/semble/containers/sembleContainer/SembleContainer.tsx
+++ b/src/webapp/features/semble/containers/sembleContainer/SembleContainer.tsx
@@ -9,7 +9,7 @@ interface Props {
url: string;
}
-export default async function SembleContainer(props: Props) {
+export default function SembleContainer(props: Props) {
return (
--
2.51.2
From b477b15b86167c9973c8c4fc5495a1a224ac61d3 Mon Sep 17 00:00:00 2001
From: Pouria Delfanazari
Date: Thu, 6 Nov 2025 11:50:55 -0800
Subject: [PATCH 17/18] fix: theme toggle icon size
---
.../features/profile/components/profileMenu/ProfileMenu.tsx | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/webapp/features/profile/components/profileMenu/ProfileMenu.tsx b/src/webapp/features/profile/components/profileMenu/ProfileMenu.tsx
index c35f071f..519232e1 100644
--- a/src/webapp/features/profile/components/profileMenu/ProfileMenu.tsx
+++ b/src/webapp/features/profile/components/profileMenu/ProfileMenu.tsx
@@ -108,11 +108,11 @@ export default function ProfileMenu() {
color="gray"
leftSection={
colorScheme === 'auto' ? (
-
+
) : computedColorScheme === 'dark' ? (
-
+
) : (
-
+
)
}
closeMenuOnClick={false}
--
2.51.2
From c7e443e710845b3c51f0e9a21607e81d3b90d671 Mon Sep 17 00:00:00 2001
From: Wesley Finck
Date: Thu, 6 Nov 2025 14:45:39 -0500
Subject: [PATCH 18/18] refactor: use normalized URL value across use cases
Co-authored-by: aider (anthropic/claude-sonnet-4-20250514)
---
.../useCases/commands/AddUrlToLibraryUseCase.ts | 4 ++--
.../queries/GetCollectionsForUrlUseCase.ts | 2 +-
.../useCases/queries/GetLibrariesForUrlUseCase.ts | 15 +++++++++------
.../useCases/queries/GetNoteCardsForUrlUseCase.ts | 15 +++++++++------
.../useCases/queries/GetUrlMetadataUseCase.ts | 2 +-
5 files changed, 22 insertions(+), 16 deletions(-)
diff --git a/src/modules/cards/application/useCases/commands/AddUrlToLibraryUseCase.ts b/src/modules/cards/application/useCases/commands/AddUrlToLibraryUseCase.ts
index 54cab9e3..8620cde4 100644
--- a/src/modules/cards/application/useCases/commands/AddUrlToLibraryUseCase.ts
+++ b/src/modules/cards/application/useCases/commands/AddUrlToLibraryUseCase.ts
@@ -107,7 +107,7 @@ export class AddUrlToLibraryUseCase extends BaseUseCase<
// Create URL card
const urlCardInput: IUrlCardInput = {
type: CardTypeEnum.URL,
- url: request.url,
+ url: url.value,
metadata: metadataResult.value,
};
@@ -192,7 +192,7 @@ export class AddUrlToLibraryUseCase extends BaseUseCase<
type: CardTypeEnum.NOTE,
text: request.note,
parentCardId: urlCard.cardId.getStringValue(),
- url: request.url,
+ url: url.value,
};
const noteCardResult = CardFactory.create({
diff --git a/src/modules/cards/application/useCases/queries/GetCollectionsForUrlUseCase.ts b/src/modules/cards/application/useCases/queries/GetCollectionsForUrlUseCase.ts
index 58a048e3..20c2f26f 100644
--- a/src/modules/cards/application/useCases/queries/GetCollectionsForUrlUseCase.ts
+++ b/src/modules/cards/application/useCases/queries/GetCollectionsForUrlUseCase.ts
@@ -60,7 +60,7 @@ export class GetCollectionsForUrlUseCase
try {
// Execute query to get collections containing cards with this URL (raw data with authorId)
const result = await this.collectionQueryRepo.getCollectionsWithUrl(
- query.url,
+ urlResult.value.value,
{
page,
limit,
diff --git a/src/modules/cards/application/useCases/queries/GetLibrariesForUrlUseCase.ts b/src/modules/cards/application/useCases/queries/GetLibrariesForUrlUseCase.ts
index 25b39cfe..e26fcbd0 100644
--- a/src/modules/cards/application/useCases/queries/GetLibrariesForUrlUseCase.ts
+++ b/src/modules/cards/application/useCases/queries/GetLibrariesForUrlUseCase.ts
@@ -68,12 +68,15 @@ export class GetLibrariesForUrlUseCase
try {
// Execute query to get libraries with full card data
- const result = await this.cardQueryRepo.getLibrariesForUrl(query.url, {
- page,
- limit,
- sortBy,
- sortOrder,
- });
+ const result = await this.cardQueryRepo.getLibrariesForUrl(
+ urlResult.value.value,
+ {
+ page,
+ limit,
+ sortBy,
+ sortOrder,
+ },
+ );
// Enrich with user profiles
const uniqueUserIds = Array.from(
diff --git a/src/modules/cards/application/useCases/queries/GetNoteCardsForUrlUseCase.ts b/src/modules/cards/application/useCases/queries/GetNoteCardsForUrlUseCase.ts
index 5ec5548d..4b9c7d2c 100644
--- a/src/modules/cards/application/useCases/queries/GetNoteCardsForUrlUseCase.ts
+++ b/src/modules/cards/application/useCases/queries/GetNoteCardsForUrlUseCase.ts
@@ -58,12 +58,15 @@ export class GetNoteCardsForUrlUseCase
try {
// Execute query to get note cards for the URL (raw data with authorId)
- const result = await this.cardQueryRepo.getNoteCardsForUrl(query.url, {
- page,
- limit,
- sortBy,
- sortOrder,
- });
+ const result = await this.cardQueryRepo.getNoteCardsForUrl(
+ urlResult.value.value,
+ {
+ page,
+ limit,
+ sortBy,
+ sortOrder,
+ },
+ );
// Enrich with author profiles
const uniqueAuthorIds = Array.from(
diff --git a/src/modules/cards/application/useCases/queries/GetUrlMetadataUseCase.ts b/src/modules/cards/application/useCases/queries/GetUrlMetadataUseCase.ts
index da750c15..cb8cc129 100644
--- a/src/modules/cards/application/useCases/queries/GetUrlMetadataUseCase.ts
+++ b/src/modules/cards/application/useCases/queries/GetUrlMetadataUseCase.ts
@@ -62,7 +62,7 @@ export class GetUrlMetadataUseCase
return ok({
metadata: {
- url: metadata.url,
+ url: url.value,
title: metadata.title,
description: metadata.description,
author: metadata.author,