Merge pull request #1935 from AnishSarkar22/fix/scroll-windows
Some checks are pending
Build and Push Docker Images / build (./surfsense_backend, cu128, ./surfsense_backend/Dockerfile, backend, surfsense-backend, ubuntu-latest, linux/amd64, amd64, -cuda, production, true, cuda) (push) Blocked by required conditions
Build and Push Docker Images / build (./surfsense_web, cpu, ./surfsense_web/Dockerfile, web, surfsense-web, ubuntu-24.04-arm, linux/arm64, arm64, , runner, false, cpu) (push) Blocked by required conditions
Build and Push Docker Images / build (./surfsense_web, cpu, ./surfsense_web/Dockerfile, web, surfsense-web, ubuntu-latest, linux/amd64, amd64, , runner, false, cpu) (push) Blocked by required conditions
Build and Push Docker Images / image_version (push) Waiting to run
Build and Push Docker Images / build (./docker/sandbox, cpu, ./docker/sandbox/Dockerfile, sandbox, surfsense-sandbox, ubuntu-24.04-arm, linux/arm64, arm64, , false, cpu) (push) Blocked by required conditions
Build and Push Docker Images / build (./docker/sandbox, cpu, ./docker/sandbox/Dockerfile, sandbox, surfsense-sandbox, ubuntu-latest, linux/amd64, amd64, , false, cpu) (push) Blocked by required conditions
Build and Push Docker Images / build (./surfsense_backend, cpu, ./surfsense_backend/Dockerfile, backend, surfsense-backend, ubuntu-24.04-arm, linux/arm64, arm64, , production, false, cpu) (push) Blocked by required conditions
Build and Push Docker Images / build (./surfsense_backend, cpu, ./surfsense_backend/Dockerfile, backend, surfsense-backend, ubuntu-latest, linux/amd64, amd64, , production, false, cpu) (push) Blocked by required conditions
Build and Push Docker Images / build (./surfsense_backend, cu126, ./surfsense_backend/Dockerfile, backend, surfsense-backend, ubuntu-24.04-arm, linux/arm64, arm64, -cuda126, production, true, cuda126) (push) Blocked by required conditions
Build and Push Docker Images / build (./surfsense_backend, cu126, ./surfsense_backend/Dockerfile, backend, surfsense-backend, ubuntu-latest, linux/amd64, amd64, -cuda126, production, true, cuda126) (push) Blocked by required conditions
Build and Push Docker Images / build (./surfsense_backend, cu128, ./surfsense_backend/Dockerfile, backend, surfsense-backend, ubuntu-24.04-arm, linux/arm64, arm64, -cuda, production, true, cuda) (push) Blocked by required conditions
Build and Push Docker Images / verify_digests (push) Blocked by required conditions
Build and Push Docker Images / create_manifest (backend, surfsense-backend, , cpu) (push) Blocked by required conditions
Build and Push Docker Images / create_manifest (backend, surfsense-backend, -cuda, cuda) (push) Blocked by required conditions
Build and Push Docker Images / create_manifest (backend, surfsense-backend, -cuda126, cuda126) (push) Blocked by required conditions
Build and Push Docker Images / create_manifest (sandbox, surfsense-sandbox, , cpu) (push) Blocked by required conditions
Build and Push Docker Images / create_manifest (web, surfsense-web, , cpu) (push) Blocked by required conditions

feat(web,local): add wordmark, Windows overlay scrollbars, and marketing site polish
This commit is contained in:
Anish Sarkar 2026-09-28 20:17:37 +05:30 • committed by GitHub
commit 38407dad0b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
56 changed files with 995 additions and 201 deletions

View file

@ -20,7 +20,7 @@
--secondary: #f1f0ec;
--background: #f7f6f2;
--app-shell: #f3f2ee;
--font-serif: "Newsreader", serif;
--font-brand: "Fraunces", serif;
--foreground: #1e1e1e;
--destructive: #dc2626;
--notice: #3f74c8;
@ -70,7 +70,7 @@
--secondary: #222222;
--background: #141414;
--app-shell: #101010;
--font-serif: "Newsreader", serif;
--font-brand: "Fraunces", serif;
--foreground: #e8e3da;
--destructive: #ef4444;
--notice: #3f74c8;
@ -120,7 +120,7 @@
--color-secondary: var(--secondary);
--color-background: var(--background);
--color-app-shell: var(--app-shell);
--color-font-serif: var(--font-serif);
--color-font-brand: var(--font-brand);
--color-foreground: var(--foreground);
--color-destructive: var(--destructive);
--color-notice: var(--notice);

View file

@ -13,7 +13,7 @@
[![License](https://img.shields.io/badge/license-Apache--2.0-blue)](LICENSE)
[![Discord](https://img.shields.io/discord/1359368468260192417?label=Discord)](https://discord.gg/ejRNvftDp9)
<a href="https://www.surfsense.com/"><img width="128" height="128" alt="SurfSense، بديل NotebookLM مفتوح المصدر والمعزول عن الشبكة" src="surfsense_web/public/homepage/icon.png" /></a>
<a href="https://www.surfsense.com/"><img alt="SurfSense، بديل NotebookLM مفتوح المصدر والمعزول عن الشبكة" src="surfsense_web/public/homepage/banner.png" /></a>
<h1>SurfSense</h1>
@ -40,7 +40,7 @@
</div>
<p align="center">
<img src="surfsense_web/public/homepage/offline-studio.png" alt="تطبيق SurfSense لسطح المكتب مع Studio مفتوحًا وQwen محددًا، يحوّل المصادر المحلية إلى مخرجات" />
<img src="surfsense_web/public/homepage/desktop.png" alt="تطبيق SurfSense لسطح المكتب مع Studio مفتوحًا وQwen محددًا، يحوّل المصادر المحلية إلى مخرجات" />
</p>
SurfSense تطبيق سطح مكتب مجاني ومفتوح المصدر للمستندات الموجودة أصلًا. تُلقى في التطبيق، وتُطرح الأسئلة، وتأتي الإجابات مع ذكر المصدر، ثم تصير المستندات نفسها إحاطة أو عرض شرائح أو تقريرًا أو دليل دراسة أو بودكاست. كل ذلك يجري على الجهاز: الفهرس يقيم على القرص، والنموذج يُختار هناك، والتطبيق لا يرفع شيئًا. لا حساب يُنشأ.

View file

@ -13,7 +13,7 @@
[![License](https://img.shields.io/badge/license-Apache--2.0-blue)](LICENSE)
[![Discord](https://img.shields.io/discord/1359368468260192417?label=Discord)](https://discord.gg/ejRNvftDp9)
<a href="https://www.surfsense.com/"><img width="128" height="128" alt="SurfSense, die netzgetrennte Open-Source-Alternative zu NotebookLM" src="surfsense_web/public/homepage/icon.png" /></a>
<a href="https://www.surfsense.com/"><img alt="SurfSense, die netzgetrennte Open-Source-Alternative zu NotebookLM" src="surfsense_web/public/homepage/banner.png" /></a>
<h1>SurfSense</h1>
@ -40,7 +40,7 @@
</div>
<p align="center">
<img src="surfsense_web/public/homepage/offline-studio.png" alt="Die SurfSense-Desktop-App mit geöffnetem Studio und ausgewähltem Qwen, die lokale Quellen in Ergebnisse verwandelt" />
<img src="surfsense_web/public/homepage/desktop.png" alt="Die SurfSense-Desktop-App mit geöffnetem Studio und ausgewähltem Qwen, die lokale Quellen in Ergebnisse verwandelt" />
</p>
SurfSense ist eine kostenlose Open-Source-Desktop-App für die Dokumente, die du schon hast. Zieh sie hinein, stell Fragen und bekommst Antworten, die ihre Quelle nennen. Dieselben Dokumente werden daraus ein Briefing, eine Folienpräsentation, ein Bericht, ein Lernleitfaden oder ein Podcast. Alles läuft auf deinem eigenen Rechner: Der Index liegt auf deiner Festplatte, du wählst das Modell, und die App lädt nichts hoch. Ein Konto brauchst du nicht.

View file

@ -14,7 +14,7 @@
[![License](https://img.shields.io/badge/license-Apache--2.0-blue)](LICENSE)
[![Discord](https://img.shields.io/discord/1359368468260192417?label=Discord)](https://discord.gg/ejRNvftDp9)
<a href="https://www.surfsense.com/"><img width="128" height="128" alt="SurfSense, la alternativa de código abierto a NotebookLM, aislada de la red" src="surfsense_web/public/homepage/icon.png" /></a>
<a href="https://www.surfsense.com/"><img alt="SurfSense, la alternativa de código abierto a NotebookLM, aislada de la red" src="surfsense_web/public/homepage/banner.png" /></a>
<h1>SurfSense</h1>
@ -41,7 +41,7 @@
</div>
<p align="center">
<img src="surfsense_web/public/homepage/offline-studio.png" alt="La aplicación de escritorio SurfSense con Studio abierto y Qwen seleccionado, convirtiendo fuentes locales en artefactos" />
<img src="surfsense_web/public/homepage/desktop.png" alt="La aplicación de escritorio SurfSense con Studio abierto y Qwen seleccionado, convirtiendo fuentes locales en artefactos" />
</p>
SurfSense es una aplicación de escritorio gratuita y de código abierto para los documentos que ya tienes. Arrástralos a la aplicación, haz preguntas y obtén respuestas que citan sus fuentes; después convierte esos mismos documentos en un resumen ejecutivo, una presentación, un informe, una guía de estudio o un pódcast. Todo se ejecuta en tu propio equipo: el índice está en tu disco, tú eliges el modelo y la aplicación no sube nada. No hay ninguna cuenta que crear.

View file

@ -13,7 +13,7 @@
[![License](https://img.shields.io/badge/license-Apache--2.0-blue)](LICENSE)
[![Discord](https://img.shields.io/discord/1359368468260192417?label=Discord)](https://discord.gg/ejRNvftDp9)
<a href="https://www.surfsense.com/"><img width="128" height="128" alt="SurfSense, l'alternative open source à NotebookLM, isolée du réseau" src="surfsense_web/public/homepage/icon.png" /></a>
<a href="https://www.surfsense.com/"><img alt="SurfSense, l'alternative open source à NotebookLM, isolée du réseau" src="surfsense_web/public/homepage/banner.png" /></a>
<h1>SurfSense</h1>
@ -40,7 +40,7 @@
</div>
<p align="center">
<img src="surfsense_web/public/homepage/offline-studio.png" alt="L'application de bureau SurfSense avec Studio ouvert et Qwen sélectionné, qui transforme des sources locales en livrables" />
<img src="surfsense_web/public/homepage/desktop.png" alt="L'application de bureau SurfSense avec Studio ouvert et Qwen sélectionné, qui transforme des sources locales en livrables" />
</p>
SurfSense est une application de bureau gratuite et open source pour les documents que vous avez déjà. Déposez-les, posez des questions et obtenez des réponses qui citent leur source, puis transformez les mêmes documents en synthèse, présentation, rapport, guide d'étude ou podcast. Tout tourne sur votre propre machine : l'index est sur votre disque, vous choisissez le modèle, et l'application ne téléverse rien. Il n'y a pas de compte à créer.

View file

@ -13,7 +13,7 @@
[![License](https://img.shields.io/badge/license-Apache--2.0-blue)](LICENSE)
[![Discord](https://img.shields.io/discord/1359368468260192417?label=Discord)](https://discord.gg/ejRNvftDp9)
<a href="https://www.surfsense.com/"><img width="128" height="128" alt="SurfSense, एयर-गैप्ड, ओपन-सोर्स NotebookLM विकल्प" src="surfsense_web/public/homepage/icon.png" /></a>
<a href="https://www.surfsense.com/"><img alt="SurfSense, एयर-गैप्ड, ओपन-सोर्स NotebookLM विकल्प" src="surfsense_web/public/homepage/banner.png" /></a>
<h1>SurfSense</h1>
@ -40,7 +40,7 @@
</div>
<p align="center">
<img src="surfsense_web/public/homepage/offline-studio.png" alt="SurfSense डेस्कटॉप ऐप, जिसमें Studio खुला है और Qwen चुना गया है, लोकल स्रोतों को आर्टिफैक्ट में बदलते हुए" />
<img src="surfsense_web/public/homepage/desktop.png" alt="SurfSense डेस्कटॉप ऐप, जिसमें Studio खुला है और Qwen चुना गया है, लोकल स्रोतों को आर्टिफैक्ट में बदलते हुए" />
</p>
SurfSense एक मुफ़्त, ओपन-सोर्स डेस्कटॉप ऐप है, उन दस्तावेज़ों के लिए जो पहले से आपके पास हैं। उन्हें ऐप में डालें, सवाल पूछें और ऐसे जवाब पाएँ जो अपने स्रोत का हवाला देते हैं; फिर उन्हीं दस्तावेज़ों को ब्रीफ़िंग, स्लाइड डेक, रिपोर्ट, स्टडी गाइड या पॉडकास्ट में बदल दें। यह सब आपकी अपनी मशीन पर चलता है: इंडेक्स आपकी डिस्क पर रहता है, मॉडल आप चुनते हैं, और ऐप कुछ भी अपलोड नहीं करता। कोई अकाउंट बनाने की ज़रूरत नहीं।

View file

@ -13,7 +13,7 @@
[![License](https://img.shields.io/badge/license-Apache--2.0-blue)](LICENSE)
[![Discord](https://img.shields.io/discord/1359368468260192417?label=Discord)](https://discord.gg/ejRNvftDp9)
<a href="https://www.surfsense.com/"><img width="128" height="128" alt="SurfSense。ネットワークから切り離して使える、オープンソースの NotebookLM 代替" src="surfsense_web/public/homepage/icon.png" /></a>
<a href="https://www.surfsense.com/"><img alt="SurfSense。ネットワークから切り離して使える、オープンソースの NotebookLM 代替" src="surfsense_web/public/homepage/banner.png" /></a>
<h1>SurfSense</h1>
@ -40,7 +40,7 @@
</div>
<p align="center">
<img src="surfsense_web/public/homepage/offline-studio.png" alt="Studio を開き Qwen を選んだ SurfSense デスクトップアプリ。手元の資料から成果物を作っている" />
<img src="surfsense_web/public/homepage/desktop.png" alt="Studio を開き Qwen を選んだ SurfSense デスクトップアプリ。手元の資料から成果物を作っている" />
</p>
SurfSense は、すでに持っている文書のための、無料のオープンソース・デスクトップアプリです。ドロップして質問すると、出典を付けた答えが返ります。同じ文書から、ブリーフィング、スライド、レポート、学習ガイド、ポッドキャストも作れます。処理はすべて自分のマシンで行われます。索引はディスク上にあり、モデルは自分で選び、アプリは何もアップロードしません。アカウントも要りません。

View file

@ -13,7 +13,7 @@
[![License](https://img.shields.io/badge/license-Apache--2.0-blue)](LICENSE)
[![Discord](https://img.shields.io/discord/1359368468260192417?label=Discord)](https://discord.gg/ejRNvftDp9)
<a href="https://www.surfsense.com/"><img width="128" height="128" alt="SurfSense, 네트워크와 분리해 쓰는 오픈소스 NotebookLM 대안" src="surfsense_web/public/homepage/icon.png" /></a>
<a href="https://www.surfsense.com/"><img alt="SurfSense, 네트워크와 분리해 쓰는 오픈소스 NotebookLM 대안" src="surfsense_web/public/homepage/banner.png" /></a>
<h1>SurfSense</h1>
@ -40,7 +40,7 @@
</div>
<p align="center">
<img src="surfsense_web/public/homepage/offline-studio.png" alt="Studio를 열고 Qwen을 선택한 SurfSense 데스크톱 앱. 로컬 자료를 결과물로 바꾸고 있다" />
<img src="surfsense_web/public/homepage/desktop.png" alt="Studio를 열고 Qwen을 선택한 SurfSense 데스크톱 앱. 로컬 자료를 결과물로 바꾸고 있다" />
</p>
SurfSense는 이미 가진 문서를 위한 무료 오픈소스 데스크톱 앱입니다. 끌어다 놓고 질문하면, 출처를 밝히는 답이 돌아옵니다. 같은 문서로 브리핑, 슬라이드, 보고서, 학습 가이드, 팟캐스트도 만듭니다. 전부 내 컴퓨터에서 돌아갑니다. 색인은 디스크에 있고, 모델은 내가 고르며, 앱은 아무것도 업로드하지 않습니다. 만들 계정도 없습니다.

View file

@ -14,7 +14,7 @@
[![License](https://img.shields.io/badge/license-Apache--2.0-blue)](LICENSE)
[![Discord](https://img.shields.io/discord/1359368468260192417?label=Discord)](https://discord.gg/ejRNvftDp9)
<a href="https://www.surfsense.com/"><img width="128" height="128" alt="SurfSense, the air-gapped open-source NotebookLM alternative" src="surfsense_web/public/homepage/icon.png" /></a>
<a href="https://www.surfsense.com/"><img alt="SurfSense, the air-gapped open-source NotebookLM alternative" src="surfsense_web/public/homepage/banner.png" /></a>
<h1>SurfSense</h1>
@ -41,7 +41,7 @@
</div>
<p align="center">
<img src="surfsense_web/public/homepage/offline-studio.png" alt="SurfSense desktop app with Studio open and Qwen selected, turning local sources into artifacts" />
<img src="surfsense_web/public/homepage/desktop.png" alt="SurfSense desktop app with Studio open and Qwen selected, turning local sources into artifacts" />
</p>
SurfSense is a free, open-source desktop app for the documents you already have. Drop them in, ask questions and get answers that cite their sources, then turn the same documents into a briefing, a slide deck, a report, a study guide or a podcast. All of it runs on your own machine: the index sits on your disk, you pick the model, and the app uploads nothing. There is no account to create.

View file

@ -14,7 +14,7 @@
[![License](https://img.shields.io/badge/license-Apache--2.0-blue)](LICENSE)
[![Discord](https://img.shields.io/discord/1359368468260192417?label=Discord)](https://discord.gg/ejRNvftDp9)
<a href="https://www.surfsense.com/"><img width="128" height="128" alt="SurfSense, a alternativa open source ao NotebookLM, isolada da rede" src="surfsense_web/public/homepage/icon.png" /></a>
<a href="https://www.surfsense.com/"><img alt="SurfSense, a alternativa open source ao NotebookLM, isolada da rede" src="surfsense_web/public/homepage/banner.png" /></a>
<h1>SurfSense</h1>
@ -41,7 +41,7 @@
</div>
<p align="center">
<img src="surfsense_web/public/homepage/offline-studio.png" alt="O aplicativo desktop SurfSense com o Studio aberto e o Qwen selecionado, transformando fontes locais em artefatos" />
<img src="surfsense_web/public/homepage/desktop.png" alt="O aplicativo desktop SurfSense com o Studio aberto e o Qwen selecionado, transformando fontes locais em artefatos" />
</p>
O SurfSense é um aplicativo de desktop gratuito e open source para os documentos que você já tem. Arraste-os para dentro, faça perguntas e receba respostas que citam as fontes; depois transforme os mesmos documentos em um briefing, uma apresentação de slides, um relatório, um guia de estudo ou um podcast. Tudo roda na sua própria máquina: o índice fica no seu disco, você escolhe o modelo e o aplicativo não envia nada para fora. Não há conta para criar.

View file

@ -13,7 +13,7 @@
[![License](https://img.shields.io/badge/license-Apache--2.0-blue)](LICENSE)
[![Discord](https://img.shields.io/discord/1359368468260192417?label=Discord)](https://discord.gg/ejRNvftDp9)
<a href="https://www.surfsense.com/"><img width="128" height="128" alt="SurfSense, изолированная от сети альтернатива NotebookLM с открытым кодом" src="surfsense_web/public/homepage/icon.png" /></a>
<a href="https://www.surfsense.com/"><img alt="SurfSense, изолированная от сети альтернатива NotebookLM с открытым кодом" src="surfsense_web/public/homepage/banner.png" /></a>
<h1>SurfSense</h1>
@ -40,7 +40,7 @@
</div>
<p align="center">
<img src="surfsense_web/public/homepage/offline-studio.png" alt="Настольное приложение SurfSense со Studio открытым и выбранным Qwen, превращающее локальные источники в результаты" />
<img src="surfsense_web/public/homepage/desktop.png" alt="Настольное приложение SurfSense со Studio открытым и выбранным Qwen, превращающее локальные источники в результаты" />
</p>
SurfSense — бесплатное настольное приложение с открытым кодом для документов, которые уже есть. Перетащите их внутрь, задайте вопросы и получите ответы со ссылкой на источник, а затем превратите те же документы в брифинг, презентацию, отчёт, учебное руководство или подкаст. Всё это работает на вашем компьютере: индекс лежит на диске, модель выбираете вы, и приложение ничего не отправляет. Аккаунт создавать не нужно.

View file

@ -13,7 +13,7 @@
[![License](https://img.shields.io/badge/license-Apache--2.0-blue)](LICENSE)
[![Discord](https://img.shields.io/discord/1359368468260192417?label=Discord)](https://discord.gg/ejRNvftDp9)
<a href="https://www.surfsense.com/"><img width="128" height="128" alt="SurfSense,可物理隔离运行的开源 NotebookLM 替代品" src="surfsense_web/public/homepage/icon.png" /></a>
<a href="https://www.surfsense.com/"><img alt="SurfSense,可物理隔离运行的开源 NotebookLM 替代品" src="surfsense_web/public/homepage/banner.png" /></a>
<h1>SurfSense</h1>
@ -40,7 +40,7 @@
</div>
<p align="center">
<img src="surfsense_web/public/homepage/offline-studio.png" alt="SurfSense 桌面应用,已打开 Studio 并选中 Qwen,正在把本地来源变成成品" />
<img src="surfsense_web/public/homepage/desktop.png" alt="SurfSense 桌面应用,已打开 Studio 并选中 Qwen,正在把本地来源变成成品" />
</p>
SurfSense 是一款免费开源的桌面应用,专为你手头已有的文档而做。把文档拖进来提问,得到的答案都会标注出处;同一批文档还能直接变成简报、幻灯片、报告、学习指南或播客。这一切都在你自己的电脑上完成:索引存在你的硬盘里,模型由你来选,应用不上传任何东西。也不需要注册账号。

View file

@ -408,7 +408,7 @@ RBAC or connector feature from the hosted product, all of which are sunset.
"open web research" banner. A wide wordmark is still nicer if one
exists later; this ships without stretching the old asset.
- [x] **Studio visual.** Still of the desktop app with Studio open and Qwen
selected (`surfsense_web/public/homepage/offline-studio.png`) stands in
selected (`surfsense_web/public/homepage/desktop.png`) stands in
until a podcast demo GIF exists.
- [ ] **Verify the competitor claim on the day.** The draft says the local chat
apps do not produce artifacts. True as of the 17 Sep re-pull; it is a

View file

@ -55,7 +55,7 @@
</div>
<p align="center">
<img src="../../../../surfsense_web/public/homepage/offline-studio.png" alt="SurfSense desktop app with Studio open and Qwen selected, turning local sources into artifacts" />
<img src="../../../../surfsense_web/public/homepage/desktop.png" alt="SurfSense desktop app with Studio open and Qwen selected, turning local sources into artifacts" />
</p>
SurfSense is a free, open-source desktop app for the documents you already have. Drop them in, ask questions and get answers that cite their sources, then turn the same documents into a briefing, a slide deck, a report, a study guide or a podcast. All of it runs on your own machine: the index sits on your disk, you pick the model, and the app uploads nothing. There is no account to create.

View file

@ -630,6 +630,12 @@ function main(): void {
}
}
// Thin, auto-hiding scrollbars on every scroll container; must be set before whenReady().
app.commandLine.appendSwitch(
"enable-features",
"OverlayScrollbar,FluentScrollbar,FluentOverlayScrollbars"
)
// one app, one set of sidecars: a second instance would fight over the SQLite
// file and the port, so hand off to the primary window and quit
if (app.requestSingleInstanceLock()) {

View file

@ -23,8 +23,8 @@
"@assistant-ui/react": "^0.15.18",
"@assistant-ui/react-streamdown": "^0.3.13",
"@base-ui/react": "^1.8.0",
"@fontsource-variable/fraunces": "^5.3.0",
"@fontsource-variable/instrument-sans": "^5.3.0",
"@fontsource-variable/newsreader": "^5.3.0",
"@fontsource/ibm-plex-mono": "^5.3.0",
"@hugeicons/core-free-icons": "^4.3.0",
"@hugeicons/react": "^1.1.10",

View file

@ -20,10 +20,10 @@ importers:
'@base-ui/react':
specifier: ^1.8.0
version: 1.8.0(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
'@fontsource-variable/instrument-sans':
'@fontsource-variable/fraunces':
specifier: ^5.3.0
version: 5.3.0
'@fontsource-variable/newsreader':
'@fontsource-variable/instrument-sans':
specifier: ^5.3.0
version: 5.3.0
'@fontsource/ibm-plex-mono':
@ -553,12 +553,12 @@ packages:
'@floating-ui/utils@0.2.12':
resolution: {integrity: sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==}
'@fontsource-variable/fraunces@5.3.0':
resolution: {integrity: sha512-9BYGySn4AHEJdgp9Z28tQ3X+laJMEOITXkQarZXeloWQZDq5oOvXJ3kDA8c7MGIfpogIaZfjrQBqmda8POOCKA==}
'@fontsource-variable/instrument-sans@5.3.0':
resolution: {integrity: sha512-u4gKbDBTNFGkg997tfQn3eHOhHuquWUFTRT/rwzuKtrxX5P2ekfs2x+LgBPP4P32+cC+vUwF1Cr+IdRoPQbrGw==}
'@fontsource-variable/newsreader@5.3.0':
resolution: {integrity: sha512-rrzYi43qMpbzwuFtf9OkWH8sxAPVPcQQQEwXpPtwaKYeJ8yVg5aLs5kawmo1f2Q1t1M38TLmEKCkGVDsYwgdFw==}
'@fontsource/ibm-plex-mono@5.3.0':
resolution: {integrity: sha512-eTgnZjZEGk1QtD3ZstF+Vclo2HLAni8YMy34/DxllwZvyz1lR/1RF/xTiAquOBO7MvqBx8D2Ig2WCPMVfdZu7Q==}
@ -5529,9 +5529,9 @@ snapshots:
'@floating-ui/utils@0.2.12': {}
'@fontsource-variable/instrument-sans@5.3.0': {}
'@fontsource-variable/fraunces@5.3.0': {}
'@fontsource-variable/newsreader@5.3.0': {}
'@fontsource-variable/instrument-sans@5.3.0': {}
'@fontsource/ibm-plex-mono@5.3.0': {}

View file

@ -31,7 +31,7 @@ function AppIdentity({ version }: { version?: string }) {
}}
/>
<div className="flex flex-col">
<span className="font-heading text-lg font-medium">SurfSense</span>
<span className="brand-wordmark text-xl">SurfSense</span>
{version ? (
<div className="flex items-center gap-1">
<span className="text-sm text-muted-foreground tabular-nums">

View file

@ -35,7 +35,7 @@ describe("LeftSidebar", () => {
render(<LeftSidebar {...baseProps()} />)
const brand = screen.getByRole("heading", { name: "SurfSense" })
expect(brand.className).toContain("font-heading")
expect(brand.className).toContain("brand-wordmark")
expect(brand.className).toContain("select-none")
const newChat = screen.getByRole("button", { name: "New chat" })

View file

@ -82,7 +82,7 @@ export function LeftSidebar({
return (
<aside className="flex h-full min-w-0 flex-col border-r bg-background select-none">
<header className="space-y-6 px-3 py-3">
<h2 className="truncate px-1 font-heading text-lg font-medium text-foreground select-none">
<h2 className="truncate px-1 brand-wordmark text-xl text-foreground select-none">
SurfSense
</h2>
<div className="flex flex-col">

View file

@ -44,7 +44,9 @@ function OnboardingBrand() {
maskSize: "contain",
}}
/>
<span className="font-heading text-2xl font-medium">SurfSense</span>
<span className="brand-wordmark -translate-y-[0.08em] text-2xl">
SurfSense
</span>
</div>
)
}

View file

@ -2,7 +2,7 @@
@import "tw-animate-css";
@import "shadcn/tailwind.css";
@import "@fontsource-variable/instrument-sans";
@import "@fontsource-variable/newsreader";
@import "@fontsource-variable/fraunces/full.css";
@import "@fontsource/ibm-plex-mono/400.css";
@import "katex/dist/katex.min.css";
@ -15,8 +15,8 @@
@theme inline {
--font-heading: var(--font-sans);
--font-sans: "Instrument Sans Variable", sans-serif;
--font-serif: "Newsreader Variable", serif;
--font-mono: "IBM Plex Mono", monospace;
--font-brand: "Fraunces Variable", serif;
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
@ -159,6 +159,17 @@
--sidebar-ring: #d1cfc0;
}
/* The product name only. Headings stay on --font-heading so the name reads as
* the brand, not as one more title. */
@utility brand-wordmark {
font-family: var(--font-brand);
font-weight: 600;
font-optical-sizing: auto;
font-variation-settings:
"SOFT" 75,
"WONK" 1;
}
@layer base {
* {
@apply border-border outline-ring/50;

View file

@ -414,6 +414,87 @@
pointer-events: none;
}
.ss-home-features-tab-chevron {
display: none;
}
/* Mobile accordion. The list and the panel stack dissolve (`display: contents`)
* so triggers and panels become siblings, interleaved by their inline `order`.
* The trigger already shows the story heading, so the H2 is visually hidden
* rather than removed: the SEO brief still needs it in the markup. */
@media (max-width: 767px) {
.ss-home-features {
display: flex;
flex-direction: column;
gap: 1px;
background-color: var(--border);
}
.ss-home-features-tablist,
.ss-home-features-panels {
display: contents;
}
.ss-home-features-tab {
flex-direction: row;
align-items: center;
gap: 0.875rem;
width: 100%;
padding-block: 1rem;
border-top: 0;
border-left: 2px solid transparent;
background-color: var(--background);
white-space: normal;
}
.ss-home-features-tab[data-state="active"] {
border-left-color: var(--home-accent);
}
.ss-home-features-tab-title {
flex: 1;
}
.ss-home-features-tab-chevron {
display: block;
flex-shrink: 0;
color: var(--muted-foreground);
transition: transform 150ms ease-out;
}
.ss-home-features-tab[data-state="active"] .ss-home-features-tab-chevron {
transform: rotate(180deg);
}
.ss-home-features-panels > * {
margin-top: 0;
}
.ss-home-features-panels > [data-state="inactive"] {
display: none;
}
.ss-home-features .ss-home-statement {
/* The hidden H2 leaves the body's own mt-5 as the top gap. */
padding-block: 0 1.5rem;
}
.ss-home-features-heading {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
}
@media (prefers-reduced-motion: reduce) {
.ss-home-features-tab-chevron {
transition: none;
}
}
/* ---------------------------------------------------------------------------
* Type
*
@ -421,6 +502,16 @@
* shape with size rather than one tracking value everywhere.
* ------------------------------------------------------------------------ */
/* The product name only, matching the desktop app's brand-wordmark. */
.ss-home-wordmark {
font-family: var(--font-brand), serif;
font-weight: 600;
font-optical-sizing: auto;
font-variation-settings:
"SOFT" 75,
"WONK" 0;
}
.ss-home-display {
font-size: clamp(2.25rem, 6vw, 3.75rem);
line-height: 1.05;
@ -997,20 +1088,34 @@
background-color: var(--background);
}
/* Equal side columns keep the links on the bar's true center, whatever the
* wordmark or star count measure. */
.ss-home-nav-bar {
margin-inline: auto;
width: 100%;
max-width: var(--home-max);
display: flex;
display: grid;
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
height: 3.25rem;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding-inline: 1rem;
border-inline: 1px solid var(--border);
border-bottom: 1px solid var(--border);
}
/* Pinned to the last column: below md the links are hidden and would
* otherwise pull this group into the middle one. */
.ss-home-nav-actions {
grid-column: 3;
justify-self: end;
}
/* Keeps the wordmark's hover and focus box to its content, not the column. */
.ss-home-nav-bar > :first-child {
justify-self: start;
}
.ss-home-nav-link {
display: flex;
align-items: center;
@ -1160,6 +1265,101 @@
outline-offset: -2px;
}
/* ---------------------------------------------------------------------------
* Mobile menu motion
*
* How the drawer, its scrim and the toggle's icon move between closed and
* open. Everything here is a transition between two states rather than a
* keyframe, so a tap mid-animation reverses from where it is instead of
* snapping. The drawer and scrim stay mounted in `site-nav.tsx` for this.
* ------------------------------------------------------------------------ */
/* Drawer and scrim. Closing is quicker and travels less than opening: focus is
* already moving on. `visibility` flips at the end of a close, so the closed
* drawer takes no taps. */
.ss-home-nav-drawer,
.ss-home-nav-scrim {
transition:
opacity 150ms ease-out,
translate 150ms ease-out,
visibility 0s linear 150ms;
}
.ss-home-nav-drawer[data-state="closed"],
.ss-home-nav-scrim[data-state="closed"] {
opacity: 0;
visibility: hidden;
pointer-events: none;
}
.ss-home-nav-drawer[data-state="closed"] {
translate: 0 -0.25rem;
}
.ss-home-nav-drawer[data-state="open"],
.ss-home-nav-scrim[data-state="open"] {
transition:
opacity 220ms cubic-bezier(0.2, 0, 0, 1),
translate 220ms cubic-bezier(0.2, 0, 0, 1),
visibility 0s;
}
/* The toggle's bars morph between menu and close in two steps. Opening, the
* outer bars first slide to the middle, then turn into a cross; closing runs
* the same steps backwards. Each state names the transitions that lead *into*
* it, which is what gives the two directions their opposite order. */
.ss-home-nav-burger line {
transform-origin: 12px 12px;
transition:
rotate 140ms cubic-bezier(0.2, 0, 0, 1),
translate 140ms cubic-bezier(0.2, 0, 0, 1) 120ms,
opacity 0s linear 120ms;
}
.ss-home-nav-toggle[data-state="open"] .ss-home-nav-burger line {
transition:
translate 140ms cubic-bezier(0.2, 0, 0, 1),
rotate 140ms cubic-bezier(0.2, 0, 0, 1) 120ms,
opacity 0s linear 120ms;
}
/* Each outer bar turns about its own centre, so the slide lands it exactly on
* the middle bar's line. */
.ss-home-nav-burger line:nth-child(1) {
transform-origin: 12px 6px;
}
.ss-home-nav-burger line:nth-child(3) {
transform-origin: 12px 18px;
}
.ss-home-nav-toggle[data-state="open"] .ss-home-nav-burger line:nth-child(1) {
translate: 0 6px;
rotate: 45deg;
}
.ss-home-nav-toggle[data-state="open"] .ss-home-nav-burger line:nth-child(2) {
opacity: 0;
}
.ss-home-nav-toggle[data-state="open"] .ss-home-nav-burger line:nth-child(3) {
translate: 0 -6px;
rotate: -45deg;
}
/* Reduced motion: keep the state change, drop the movement. The drawer still
* fades, since opacity alone does not move anything. */
@media (prefers-reduced-motion: reduce) {
.ss-home-nav-burger line,
.ss-home-nav-toggle[data-state="open"] .ss-home-nav-burger line {
transition: none;
}
.ss-home-nav-drawer[data-state="closed"] {
translate: none;
}
}
/* ---------------------------------------------------------------------------
* Footer
*
@ -1269,14 +1469,19 @@
outline-offset: 4px;
}
/* A plus that becomes a minus. The rotation is decorative; the open state is
* already exposed by details/summary itself, so assistive technology and a
* reduced-motion visitor both still get the state. */
/* A plus that turns into a close cross. The rotation is decorative; the open
* state is already exposed by details/summary itself, so assistive technology
* and a reduced-motion visitor both still get the state. */
.ss-home-faq-marker {
position: relative;
flex: none;
width: 0.75rem;
height: 0.75rem;
transition: transform 150ms ease-out;
}
.ss-home-faq[open] .ss-home-faq-marker {
transform: rotate(45deg);
}
.ss-home-faq-marker::before,
@ -1286,17 +1491,12 @@
inset: 50% 0 auto;
height: 1px;
background: currentColor;
transition: transform 150ms ease-out;
}
.ss-home-faq-marker::after {
transform: rotate(90deg);
}
.ss-home-faq[open] .ss-home-faq-marker::after {
transform: rotate(0deg);
}
/* ---------------------------------------------------------------------------
* Links
* ------------------------------------------------------------------------ */

View file

@ -1,7 +1,17 @@
import { Fraunces } from "next/font/google";
import { getStarCount, STARS_HREF } from "@/components/site/github-stars";
import { siteSansFont } from "@/components/site/site-sans-font";
import { SiteShell } from "@/components/site/site-shell";
import "./home.css";
// Brand wordmark only; loaded here so the app routes never fetch it.
const fraunces = Fraunces({
subsets: ["latin"],
axes: ["SOFT", "WONK", "opsz"],
display: "swap",
variable: "--font-brand",
});
/**
* Layout for every marketing route.
*
@ -17,8 +27,12 @@ export default async function HomePageLayout({ children }: { children: React.Rea
const starCount = await getStarCount();
return (
<SiteShell starCount={starCount} starsHref={STARS_HREF}>
{children}
</SiteShell>
<div
className={`${fraunces.variable} ${siteSansFont.variable} ${siteSansFont.className} contents`}
>
<SiteShell starCount={starCount} starsHref={STARS_HREF}>
{children}
</SiteShell>
</div>
);
}

View file

@ -103,7 +103,7 @@ function InlineEmailField({
value={value}
onChange={(event) => onChange(event.target.value)}
required
className="w-64 rounded-(--radius) border border-border bg-background px-3 py-2.5 text-sm text-foreground outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50"
className="w-64 max-w-full rounded-(--radius) border border-border bg-background px-3 py-2.5 text-sm text-foreground outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50"
/>
</>
);
@ -231,7 +231,7 @@ export function TrialForm({
return (
<div className="mt-10 flex flex-col items-center gap-5">
<form onSubmit={handleSubmit} className="flex items-center gap-2">
<form onSubmit={handleSubmit} className="flex flex-wrap items-center justify-center gap-2">
<InlineEmailField id={`trial-email-${id}`} value={email} onChange={setEmail} />
<HomeButton type="submit" size="xl" disabled={busy} className="relative shrink-0">
<span className={busy ? "opacity-0" : ""}>{label}</span>

View file

@ -10,6 +10,7 @@ import {
HomeOnYourMachine,
HomePillars,
} from "@/components/homepage/home/home-sections";
import { HomeSmoothScroll } from "@/components/homepage/home/home-smooth-scroll";
import { JsonLd } from "@/components/seo/json-ld";
/**
@ -63,6 +64,7 @@ export default function HomePage() {
// from app/(home)/layout.tsx, which renders them for every site-design route.
<>
<JsonLd data={APPLICATION_SCHEMA} />
<HomeSmoothScroll />
<HomeHero />
<HomeLogos />
<HomeOnYourMachine />

View file

@ -1,6 +1,15 @@
import type { Metadata } from "next";
import Image from "next/image";
import Link from "next/link";
import { AmazonLogo } from "@/components/homepage/icons/amazon-logo";
import { GoogleMapsLogo } from "@/components/homepage/icons/google-maps-logo";
import { GoogleSearchLogo } from "@/components/homepage/icons/google-search-logo";
import { IndeedLogo } from "@/components/homepage/icons/indeed-logo";
import { InstagramLogo } from "@/components/homepage/icons/instagram-logo";
import { RedditLogo } from "@/components/homepage/icons/reddit-logo";
import { TiktokLogo } from "@/components/homepage/icons/tiktok-logo";
import { WalmartLogo } from "@/components/homepage/icons/walmart-logo";
import { WebCrawlLogo } from "@/components/homepage/icons/web-crawl-logo";
import { YoutubeLogo } from "@/components/homepage/icons/youtube-logo";
import { Badge } from "@/components/ui/badge";
import { ArrowUpRight01Icon } from "@/components/ui/icons";
@ -21,9 +30,10 @@ import { ArrowUpRight01Icon } from "@/components/ui/icons";
* `lib/connectors-marketing` — the plugin itself is not live yet, but the page
* explaining what it will do already is.
*
* Logos are the same brand SVGs the product's own connector picker uses (see
* `contracts/enums/connectorIcons.tsx`), read from `public/connectors/`, rather
* than a second icon set drawn just for this page.
* Logos are inline components from `components/homepage/icons/`, so they
* arrive in the HTML and paint with the page instead of loading one request
* each afterwards. They are the marketing site's own copies of the brand
* marks the product's connector picker reads from `public/connectors/`.
*
* Replaces the old `/connectors` index, which now redirects here from
* `next.config.ts`.
@ -58,16 +68,16 @@ export const metadata: Metadata = {
};
const PLUGINS = [
{ name: "Reddit", href: "/reddit", logo: "/connectors/reddit.svg" },
{ name: "YouTube", href: "/youtube", logo: "/connectors/youtube.svg" },
{ name: "Instagram", href: "/instagram", logo: "/connectors/instagram.svg" },
{ name: "TikTok", href: "/tiktok", logo: "/connectors/tiktok.svg" },
{ name: "Google Maps", href: "/google-maps", logo: "/connectors/google-maps.svg" },
{ name: "Google Search", href: "/google-search", logo: "/connectors/google-search.svg" },
{ name: "Indeed", href: "/indeed", logo: "/connectors/indeed.svg" },
{ name: "Amazon", href: "/amazon", logo: "/connectors/amazon.svg" },
{ name: "Walmart", href: "/walmart", logo: "/connectors/walmart.svg" },
{ name: "Web Crawl", href: "/web-crawl", logo: "/connectors/web.svg" },
{ name: "Reddit", href: "/reddit", Logo: RedditLogo },
{ name: "YouTube", href: "/youtube", Logo: YoutubeLogo },
{ name: "Instagram", href: "/instagram", Logo: InstagramLogo },
{ name: "TikTok", href: "/tiktok", Logo: TiktokLogo },
{ name: "Google Maps", href: "/google-maps", Logo: GoogleMapsLogo },
{ name: "Google Search", href: "/google-search", Logo: GoogleSearchLogo },
{ name: "Indeed", href: "/indeed", Logo: IndeedLogo },
{ name: "Amazon", href: "/amazon", Logo: AmazonLogo },
{ name: "Walmart", href: "/walmart", Logo: WalmartLogo },
{ name: "Web Crawl", href: "/web-crawl", Logo: WebCrawlLogo },
];
export default function PluginsPage() {
@ -100,13 +110,7 @@ export default function PluginsPage() {
{PLUGINS.map((plugin) => (
<li key={plugin.name}>
<Link href={plugin.href} className="ss-home-cell ss-home-cell-link">
<Image
src={plugin.logo}
alt=""
width={20}
height={20}
className="size-5 shrink-0"
/>
<plugin.Logo className="size-5 shrink-0" />
<span className="ss-home-h3">{plugin.name}</span>
<ArrowUpRight01Icon
aria-hidden="true"

View file

@ -1,6 +1,7 @@
import { DocsLayout } from "fumadocs-ui/layouts/docs";
import type { ReactNode } from "react";
import { baseOptions } from "@/app/layout.config";
import { siteSansFont } from "@/components/site/site-sans-font";
import { source } from "@/lib/source";
import { SidebarSeparator } from "./sidebar-separator";
@ -11,19 +12,27 @@ const gridTemplate = `"sidebar header toc"
const docsSurfaceClass =
"bg-main-panel [--color-fd-background:var(--main-panel)] [--color-fd-card:var(--main-panel)] [--color-fd-popover:var(--main-panel)] [--color-fd-muted:var(--main-panel)] [--color-fd-secondary:var(--main-panel)]";
// Set on body, not a wrapper, so the portalled search dialog follows it. Mounted
// with this layout, so it is dropped on navigating out of /docs.
// `:root body` outranks the root layout's Roboto class on body.
const docsFontStyle = `:root body{--font-sans:${siteSansFont.style.fontFamily};font-family:var(--font-sans)}`;
export default function Layout({ children }: { children: ReactNode }) {
return (
<DocsLayout
tree={source.pageTree}
{...baseOptions}
containerProps={{ style: { gridTemplate }, className: docsSurfaceClass }}
sidebar={{
components: {
Separator: SidebarSeparator,
},
}}
>
{children}
</DocsLayout>
<>
<style>{docsFontStyle}</style>
<DocsLayout
tree={source.pageTree}
{...baseOptions}
containerProps={{ style: { gridTemplate }, className: docsSurfaceClass }}
sidebar={{
components: {
Separator: SidebarSeparator,
},
}}
>
{children}
</DocsLayout>
</>
);
}

View file

@ -1,10 +1,14 @@
"use client";
import Link from "next/link";
import { useRef } from "react";
import { PROOF_POINTS, STORIES } from "@/components/homepage/home/home-content";
import { ArrowRightIcon, CheckIcon } from "@/components/ui/icons";
import { ArrowRightIcon, CheckIcon, ChevronDownIcon } from "@/components/ui/icons";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
// Matches the max-width of the accordion block in home.css.
const ACCORDION_QUERY = "(max-width: 767px)";
/**
* H2 #5, #6, #7 as a tabbed switcher rather than three stacked rows.
*
@ -14,24 +18,56 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
* stacked in one grid cell so the section's height is always the tallest
* panel's height: switching tabs never changes it, so nothing below the
* section moves.
*
* Below 768px the same tabs lay out as an accordion: the `order` pairs each
* panel with its trigger once CSS flattens the list. One component rather than
* a second accordion, so the three H2s appear once.
*/
export function HomeFeaturesTabs() {
const triggers = useRef(new Map<string, HTMLButtonElement>());
// Closing the panel above the tapped row pulls that row up, possibly off screen.
const keepOpenedRowInView = (value: string) => {
if (!window.matchMedia(ACCORDION_QUERY).matches) return;
requestAnimationFrame(() => triggers.current.get(value)?.scrollIntoView({ block: "nearest" }));
};
return (
<Tabs defaultValue={STORIES[0].key}>
<Tabs
defaultValue={STORIES[0].key}
onValueChange={keepOpenedRowInView}
className="ss-home-features"
>
<TabsList className="ss-home-features-tablist ss-home-grid ss-home-grid-3">
{STORIES.map((story, index) => (
<TabsTrigger key={story.key} value={story.key} className="ss-home-features-tab">
<TabsTrigger
key={story.key}
value={story.key}
ref={(node) => {
if (node) triggers.current.set(story.key, node);
else triggers.current.delete(story.key);
}}
style={{ order: index * 2 }}
className="ss-home-features-tab"
>
<span className="ss-home-features-tab-num">{String(index + 1).padStart(2, "0")}</span>
<span className="ss-home-features-tab-title">{story.heading}</span>
<ChevronDownIcon aria-hidden="true" className="ss-home-features-tab-chevron size-4" />
</TabsTrigger>
))}
</TabsList>
<div className="ss-home-features-panels">
{STORIES.map((story) => (
<TabsContent key={story.key} value={story.key} forceMount className="ss-home-split">
{STORIES.map((story, index) => (
<TabsContent
key={story.key}
value={story.key}
forceMount
style={{ order: index * 2 + 1 }}
className="ss-home-split"
>
<div className="ss-home-statement">
<h2 className="ss-home-h2">{story.heading}</h2>
<h2 className="ss-home-h2 ss-home-features-heading">{story.heading}</h2>
<div className="ss-home-body mt-5 flex flex-col gap-4">
{story.body.map((paragraph) => (
<p key={paragraph}>{paragraph}</p>

View file

@ -0,0 +1,20 @@
"use client";
import Lenis from "lenis";
import "lenis/dist/lenis.css";
import { useEffect } from "react";
/**
* Lenis smooth scrolling for the landing page only; mounting it here rather than
* in the (home) layout keeps the other marketing routes on native scroll.
*/
export function HomeSmoothScroll() {
useEffect(() => {
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
const lenis = new Lenis({ autoRaf: true, anchors: true });
return () => lenis.destroy();
}, []);
return null;
}

View file

@ -0,0 +1,21 @@
/** Amazon's full-colour mark. */
export function AmazonLogo(props: React.SVGProps<SVGSVGElement>) {
return (
<svg viewBox="0 0 48 48" aria-hidden="true" {...props}>
<g stroke="none" strokeWidth="1" fill="none" fillRule="evenodd">
<g transform="translate(-601.000000, -560.000000)">
<g transform="translate(601.000000, 560.000000)">
<path
d="M25.4026553,25.9595294 C24.660417,27.4418824 23.3876054,28.3962353 22.0103725,28.7181176 C21.8015298,28.7181176 21.4826213,28.8225882 21.1637129,28.8225882 C18.835399,28.8225882 17.458166,27.0211765 17.458166,24.3727059 C17.458166,20.9788235 19.4703937,19.392 22.0103725,18.6465882 C23.3876054,18.3303529 24.9793255,18.2230588 26.5682233,18.2230588 L26.5682233,19.4964706 C26.5682233,21.9331765 26.6726447,23.8390588 25.4026553,25.9595294 L25.4026553,25.9595294 Z M26.5682233,13.3524706 C25.1909904,13.4569412 23.5992703,13.5614118 22.0103725,13.7703529 C19.574815,14.0922353 17.1392576,14.5157647 15.1298521,15.4701176 C11.2098182,17.0597647 8.55977364,20.4508235 8.55977364,25.4287059 C8.55977364,31.6856471 12.5842289,34.8621176 17.6726531,34.8621176 C19.3659723,34.8621176 20.7432053,34.6475294 22.0103725,34.3341176 C24.0282445,33.696 25.7187415,32.5298824 27.7309692,30.4094118 C28.8965372,31.9990588 29.2182679,32.7444706 31.2276733,34.4385882 C31.7582467,34.6475294 32.28882,34.6475294 32.7093276,34.3341176 C33.9821392,33.2724706 36.208854,31.3637647 37.3715998,30.3049412 C37.9021732,29.8814118 37.7977518,29.2432941 37.4760212,28.7181176 C36.3132753,27.2329412 35.1448851,25.9595294 35.1448851,23.0992941 L35.1448851,13.5614118 C35.1448851,9.53505882 35.4666157,5.82494118 32.5004849,3.072 C30.0649275,0.849882353 26.2493149,0 23.2831841,0 L22.0103725,0 C16.6115064,0.313411765 10.8937319,2.64564706 9.61809814,9.32329412 C9.40643324,10.1731765 10.0442501,10.4894118 10.4675799,10.5938824 L16.3998415,11.3364706 C17.0348362,11.2291765 17.3537447,10.6983529 17.458166,10.1731765 C17.9859172,7.84094118 19.8937235,6.67482353 22.0103725,6.46023529 L22.4365245,6.46023529 C23.7093361,6.46023529 25.086569,6.99105882 25.8259851,8.05270588 C26.6726447,9.32329412 26.5682233,11.0202353 26.5682233,12.5054118 L26.5682233,13.3524706 L26.5682233,13.3524706 Z"
fill="#343B45"
/>
<path
d="M47.9943556,35.9463529 L47.9943556,35.9435294 C47.971778,35.4437647 47.8673567,35.0625882 47.658514,34.7463529 L47.6359364,34.7152941 L47.6105366,34.6842353 C47.3988717,34.4527059 47.1956734,34.3651765 46.9755419,34.2691765 C46.3179696,34.0150588 45.3612442,33.8795294 44.2097872,33.8767059 C43.382883,33.8767059 42.4713128,33.9557647 41.5540982,34.1562353 L41.551276,34.0941176 L40.6284171,34.4018824 L40.6114839,34.4103529 L40.0893771,34.5797647 L40.0893771,34.6023529 C39.47696,34.8564706 38.9209869,35.1727059 38.4045245,35.5482353 C38.0827939,35.7882353 37.8175072,36.1072941 37.8033962,36.5957647 C37.7949296,36.8611765 37.9303952,37.1661176 38.1533489,37.3468235 C38.3763025,37.5275294 38.6359448,37.5896471 38.8645429,37.5896471 C38.9181647,37.5896471 38.9689643,37.5868235 39.0141194,37.5783529 L39.0592746,37.5755294 L39.093141,37.5698824 C39.5446928,37.4738824 40.2022651,37.4089412 40.9727253,37.3016471 C41.6331198,37.2282353 42.3330251,37.1745882 42.9397978,37.1745882 C43.368772,37.1717647 43.7554132,37.2028235 44.0206999,37.2592941 C44.1533432,37.2875294 44.2521202,37.3214118 44.3057419,37.3496471 C44.3254973,37.3552941 44.3396083,37.3637647 44.3480749,37.3694118 C44.3593637,37.4061176 44.3762969,37.5021176 44.3734747,37.6348235 C44.3791191,38.1430588 44.164632,39.0861176 43.8683012,40.0065882 C43.5804369,40.9270588 43.2304843,41.8503529 42.999064,42.4630588 C42.94262,42.6042353 42.9059314,42.7595294 42.9059314,42.9289412 C42.900287,43.1745882 43.0018862,43.4738824 43.2163733,43.6715294 C43.425216,43.8691765 43.696147,43.9482353 43.9219229,43.9482353 L43.9332117,43.9482353 C44.2718756,43.9454118 44.5597398,43.8098824 44.8080933,43.6150588 C47.1505182,41.5087059 47.9661336,38.1430588 48,36.2484706 L47.9943556,35.9463529 Z M41.0489247,38.8658824 C40.8090378,38.8630588 40.5635065,38.9195294 40.3349084,39.0268235 C40.0780883,39.1284706 39.8156239,39.2470588 39.5672704,39.3515294 L39.2032068,39.504 L38.7290774,39.6931765 L38.7290774,39.6988235 C33.5785648,41.7882353 28.16841,43.0136471 23.1618295,43.1209412 C22.9783866,43.1265882 22.7921215,43.1265882 22.614323,43.1265882 C14.7403887,43.1322353 8.31706456,39.4785882 1.83729642,35.8785882 C1.61152053,35.76 1.37727804,35.6978824 1.15150215,35.6978824 C0.860815683,35.6978824 0.561662624,35.808 0.344353327,36.0112941 C0.12704403,36.2174118 -0.00277710907,36.5138824 4.50895989e-05,36.816 C-0.00277710907,37.2084706 0.208887791,37.5698824 0.505218651,37.8042353 C6.58705678,43.0870588 13.25309,47.9943529 22.2192152,48 C22.3941915,48 22.57199,47.9943529 22.7497885,47.9915294 C28.453452,47.8644706 34.902176,45.936 39.9087564,42.7905882 L39.9398006,42.7708235 C40.5945507,42.3783529 41.2493008,41.9322353 41.8673623,41.4381176 C42.2511813,41.1529412 42.516468,40.7068235 42.516468,40.2437647 C42.4995348,39.4221176 41.8024517,38.8658824 41.0489247,38.8658824 Z"
fill="#FF9A00"
/>
</g>
</g>
</g>
</svg>
);
}

View file

@ -0,0 +1,8 @@
/** Discord's official mark, in `currentColor`. Supplied directly; drawn on a 512 grid. */
export function DiscordLogo(props: React.SVGProps<SVGSVGElement>) {
return (
<svg viewBox="0 0 512 512" fill="currentColor" aria-hidden="true" {...props}>
<path d="M433.7 91a416.5 416.5 0 0 0-105.6-33.2c-4.6 8.2-9.9 19.3-13.5 28.1c-39.4-5.9-78.4-5.9-117.1 0c-3.7-8.8-9.1-19.9-13.7-28.1c-37.1 6.4-72.6 17.7-105.7 33.3c-66.8 101-85 199.5-75.9 296.6c44.3 33.1 87.3 53.2 129.6 66.4c10.4-14.4 19.7-29.6 27.7-45.7c-15.3-5.8-29.9-13-43.7-21.3c3.7-2.7 7.2-5.6 10.7-8.5c84.2 39.4 175.8 39.4 259 0c3.5 2.9 7.1 5.8 10.7 8.5c-13.9 8.3-28.5 15.5-43.8 21.3c8 16 17.3 31.3 27.7 45.7c42.3-13.2 85.3-33.3 129.6-66.4c10.8-112.5-18-210.1-76-296.7M170.9 328c-25.3 0-46-23.6-46-52.4s20.3-52.4 46-52.4s46.5 23.6 46 52.4c.1 28.8-20.2 52.4-46 52.4m170.2 0c-25.3 0-46-23.6-46-52.4s20.3-52.4 46-52.4s46.5 23.6 46 52.4c0 28.8-20.3 52.4-46 52.4" />
</svg>
);
}

View file

@ -0,0 +1,8 @@
/** GitHub's official mark, in `currentColor`. From Simple Icons (CC0), simple-icons@16.32.0. */
export function GithubLogo(props: React.SVGProps<SVGSVGElement>) {
return (
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" {...props}>
<path 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" />
</svg>
);
}

View file

@ -0,0 +1,27 @@
/** Google Maps' pin, the 2020 flat vector version (Wikimedia Commons). The 2026 icon ships only as embedded raster images, which are far too heavy to inline. */
export function GoogleMapsLogo(props: React.SVGProps<SVGSVGElement>) {
return (
<svg viewBox="0 0 92.3 132.3" aria-hidden="true" {...props}>
<path
fill="#1a73e8"
d="M60.2 2.2C55.8.8 51 0 46.1 0 32 0 19.3 6.4 10.8 16.5l21.8 18.3L60.2 2.2z"
/>
<path
fill="#ea4335"
d="M10.8 16.5C4.1 24.5 0 34.9 0 46.1c0 8.7 1.7 15.7 4.6 22l28-33.3-21.8-18.3z"
/>
<path
fill="#4285f4"
d="M46.2 28.5c9.8 0 17.7 7.9 17.7 17.7 0 4.3-1.6 8.3-4.2 11.4 0 0 13.9-16.6 27.5-32.7-5.6-10.8-15.3-19-27-22.7L32.6 34.8c3.3-3.8 8.1-6.3 13.6-6.3"
/>
<path
fill="#fbbc04"
d="M46.2 63.8c-9.8 0-17.7-7.9-17.7-17.7 0-4.3 1.5-8.3 4.1-11.3l-28 33.3c4.8 10.6 12.8 19.2 21 29.9l34.1-40.5c-3.3 3.9-8.1 6.3-13.5 6.3"
/>
<path
fill="#34a853"
d="M59.1 109.2c15.4-24.1 33.3-35 33.3-63 0-7.7-1.9-14.9-5.2-21.3L25.6 98c2.6 3.4 5.3 7.3 7.9 11.3 9.4 14.5 6.8 23.1 12.8 23.1s3.4-8.7 12.8-23.2"
/>
</svg>
);
}

View file

@ -0,0 +1,76 @@
import { useId } from "react";
/** Google's full-colour G. */
export function GoogleSearchLogo(props: React.SVGProps<SVGSVGElement>) {
const id = useId();
return (
<svg viewBox="0 0 24 24" aria-hidden="true" {...props}>
<defs>
<radialGradient
id={`${id}-b`}
cx="1.479"
cy="12.788"
fx="1.479"
fy="12.788"
r="9.655"
gradientTransform="matrix(.8032 0 0 1.0842 2.459 -.293)"
gradientUnits="userSpaceOnUse"
>
<stop offset=".368" stopColor="#ffcf09" />
<stop offset=".718" stopColor="#ffcf09" stopOpacity=".7" />
<stop offset="1" stopColor="#ffcf09" stopOpacity="0" />
</radialGradient>
<radialGradient
id={`${id}-c`}
cx="14.295"
cy="23.291"
fx="14.295"
fy="23.291"
r="11.878"
gradientTransform="matrix(1.3272 0 0 1.0073 -3.434 -.672)"
gradientUnits="userSpaceOnUse"
>
<stop offset=".383" stopColor="#34a853" />
<stop offset=".706" stopColor="#34a853" stopOpacity=".7" />
<stop offset="1" stopColor="#34a853" stopOpacity="0" />
</radialGradient>
<linearGradient
id={`${id}-d`}
x1="23.558"
y1="6.286"
x2="12.148"
y2="20.299"
gradientUnits="userSpaceOnUse"
>
<stop offset=".671" stopColor="#4285f4" />
<stop offset=".885" stopColor="#4285f4" stopOpacity="0" />
</linearGradient>
<clipPath id={`${id}-a`}>
<path
d="M22.36 10H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53h-.013l.013-.01c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09c.87-2.6 3.3-4.53 6.16-4.53 1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07 1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93v.01C3.99 20.53 7.7 23 12 23c2.97 0 5.46-.98 7.28-2.66 2.08-1.92 3.28-4.74 3.28-8.09 0-.78-.07-1.53-.2-2.25z"
fill="none"
/>
</clipPath>
</defs>
<path
d="M22.36 10H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53h-.013l.013-.01c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09c.87-2.6 3.3-4.53 6.16-4.53 1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07 1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93v.01C3.99 20.53 7.7 23 12 23c2.97 0 5.46-.98 7.28-2.66 2.08-1.92 3.28-4.74 3.28-8.09 0-.78-.07-1.53-.2-2.25z"
fill="#fc4c53"
/>
<g clipPath={`url(#${id}-a)`}>
<ellipse cx="3.646" cy="13.572" rx="7.755" ry="10.469" fill={`url(#${id}-b)`} />
<ellipse
cx="15.538"
cy="22.789"
rx="15.765"
ry="11.965"
transform="rotate(-7.12 15.539 22.789)"
fill={`url(#${id}-c)`}
/>
<path
fill={`url(#${id}-d)`}
d="M11.105 8.28l.491 5.596.623 3.747 7.362 6.848 8.607-15.897-17.083-.294z"
/>
</g>
</svg>
);
}

View file

@ -0,0 +1,8 @@
/** Indeed's mark, in its brand blue. */
export function IndeedLogo(props: React.SVGProps<SVGSVGElement>) {
return (
<svg viewBox="0 0 24 24" fill="#003A9B" aria-hidden="true" {...props}>
<path d="M11.566 21.5633v-8.762c.2553.0231.5009.0346.758.0346 1.2225 0 2.3739-.3206 3.3506-.8928v9.6182c0 .8219-.1957 1.4287-.5757 1.8338-.378.4033-.8808.6049-1.491.6049-.6007 0-1.0766-.2016-1.468-.6183-.3781-.4032-.5739-1.01-.5739-1.8184zM11.589.5659c2.5447-.8929 5.4424-.8449 7.6186.987.405.3687.8673.8334 1.0515 1.3806.2207.6913-.7695-.073-.9057-.167-.71-.4532-1.4182-.8334-2.2127-1.0946C12.8614.3873 8.8122 2.709 6.2945 6.315c-1.0516 1.5939-1.7367 3.2721-2.299 5.1174-.0614.2017-.1094.4647-.2207.6413-.1113.2036-.048-.5453-.048-.5702.0845-.7623.2438-1.4997.4414-2.237C5.3292 5.3375 7.897 2.0655 11.5891.5658zm4.9281 7.0587c0 1.6686-1.353 3.0224-3.0205 3.0224-1.6677 0-3.0186-1.3538-3.0186-3.0224 0-1.6687 1.351-3.0224 3.0186-3.0224 1.6676 0 3.0205 1.3518 3.0205 3.0224Z" />
</svg>
);
}

View file

@ -0,0 +1,44 @@
import { useId } from "react";
/** Instagram's full-colour mark. */
export function InstagramLogo(props: React.SVGProps<SVGSVGElement>) {
const id = useId();
return (
<svg viewBox="0 0 3364.7 3364.7" aria-hidden="true" {...props}>
<defs>
<radialGradient
id={`${id}-0`}
cx="217.76"
cy="3290.99"
r="4271.92"
gradientUnits="userSpaceOnUse"
>
<stop offset=".09" stopColor="#fa8f21" />
<stop offset=".78" stopColor="#d82d7e" />
</radialGradient>
<radialGradient
id={`${id}-1`}
cx="2330.61"
cy="3182.95"
r="3759.33"
gradientUnits="userSpaceOnUse"
>
<stop offset=".64" stopColor="#8c3aaa" stopOpacity="0" />
<stop offset="1" stopColor="#8c3aaa" />
</radialGradient>
</defs>
<path
d="M853.2,3352.8c-200.1-9.1-308.8-42.4-381.1-70.6-95.8-37.3-164.1-81.7-236-153.5S119.7,2988.6,82.6,2892.8c-28.2-72.3-61.5-181-70.6-381.1C2,2295.4,0,2230.5,0,1682.5s2.2-612.8,11.9-829.3C21,653.1,54.5,544.6,82.5,472.1,119.8,376.3,164.3,308,236,236c71.8-71.8,140.1-116.4,236-153.5C544.3,54.3,653,21,853.1,11.9,1069.5,2,1134.5,0,1682.3,0c548,0,612.8,2.2,829.3,11.9,200.1,9.1,308.6,42.6,381.1,70.6,95.8,37.1,164.1,81.7,236,153.5s116.2,140.2,153.5,236c28.2,72.3,61.5,181,70.6,381.1,9.9,216.5,11.9,281.3,11.9,829.3,0,547.8-2,612.8-11.9,829.3-9.1,200.1-42.6,308.8-70.6,381.1-37.3,95.8-81.7,164.1-153.5,235.9s-140.2,116.2-236,153.5c-72.3,28.2-181,61.5-381.1,70.6-216.3,9.9-281.3,11.9-829.3,11.9-547.8,0-612.8-1.9-829.1-11.9"
fill={`url(#${id}-0)`}
/>
<path
d="M853.2,3352.8c-200.1-9.1-308.8-42.4-381.1-70.6-95.8-37.3-164.1-81.7-236-153.5S119.7,2988.6,82.6,2892.8c-28.2-72.3-61.5-181-70.6-381.1C2,2295.4,0,2230.5,0,1682.5s2.2-612.8,11.9-829.3C21,653.1,54.5,544.6,82.5,472.1,119.8,376.3,164.3,308,236,236c71.8-71.8,140.1-116.4,236-153.5C544.3,54.3,653,21,853.1,11.9,1069.5,2,1134.5,0,1682.3,0c548,0,612.8,2.2,829.3,11.9,200.1,9.1,308.6,42.6,381.1,70.6,95.8,37.1,164.1,81.7,236,153.5s116.2,140.2,153.5,236c28.2,72.3,61.5,181,70.6,381.1,9.9,216.5,11.9,281.3,11.9,829.3,0,547.8-2,612.8-11.9,829.3-9.1,200.1-42.6,308.8-70.6,381.1-37.3,95.8-81.7,164.1-153.5,235.9s-140.2,116.2-236,153.5c-72.3,28.2-181,61.5-381.1,70.6-216.3,9.9-281.3,11.9-829.3,11.9-547.8,0-612.8-1.9-829.1-11.9"
fill={`url(#${id}-1)`}
/>
<path
d="M1269.25,1689.52c0-230.11,186.49-416.7,416.6-416.7s416.7,186.59,416.7,416.7-186.59,416.7-416.7,416.7-416.6-186.59-416.6-416.7m-225.26,0c0,354.5,287.36,641.86,641.86,641.86s641.86-287.36,641.86-641.86-287.36-641.86-641.86-641.86S1044,1335,1044,1689.52m1159.13-667.31a150,150,0,1,0,150.06-149.94h-0.06a150.07,150.07,0,0,0-150,149.94M1180.85,2707c-121.87-5.55-188.11-25.85-232.13-43-58.36-22.72-100-49.78-143.78-93.5s-70.88-85.32-93.5-143.68c-17.16-44-37.46-110.26-43-232.13-6.06-131.76-7.27-171.34-7.27-505.15s1.31-373.28,7.27-505.15c5.55-121.87,26-188,43-232.13,22.72-58.36,49.78-100,93.5-143.78s85.32-70.88,143.78-93.5c44-17.16,110.26-37.46,232.13-43,131.76-6.06,171.34-7.27,505-7.27S2059.13,666,2191,672c121.87,5.55,188,26,232.13,43,58.36,22.62,100,49.78,143.78,93.5s70.78,85.42,93.5,143.78c17.16,44,37.46,110.26,43,232.13,6.06,131.87,7.27,171.34,7.27,505.15s-1.21,373.28-7.27,505.15c-5.55,121.87-25.95,188.11-43,232.13-22.72,58.36-49.78,100-93.5,143.68s-85.42,70.78-143.78,93.5c-44,17.16-110.26,37.46-232.13,43-131.76,6.06-171.34,7.27-505.15,7.27s-373.28-1.21-505-7.27M1170.5,447.09c-133.07,6.06-224,27.16-303.41,58.06-82.19,31.91-151.86,74.72-221.43,144.18S533.39,788.47,501.48,870.76c-30.9,79.46-52,170.34-58.06,303.41-6.16,133.28-7.57,175.89-7.57,515.35s1.41,382.07,7.57,515.35c6.06,133.08,27.16,223.95,58.06,303.41,31.91,82.19,74.62,152,144.18,221.43s139.14,112.18,221.43,144.18c79.56,30.9,170.34,52,303.41,58.06,133.35,6.06,175.89,7.57,515.35,7.57s382.07-1.41,515.35-7.57c133.08-6.06,223.95-27.16,303.41-58.06,82.19-32,151.86-74.72,221.43-144.18s112.18-139.24,144.18-221.43c30.9-79.46,52.1-170.34,58.06-303.41,6.06-133.38,7.47-175.89,7.47-515.35s-1.41-382.07-7.47-515.35c-6.06-133.08-27.16-224-58.06-303.41-32-82.19-74.72-151.86-144.18-221.43S2586.8,537.06,2504.71,505.15c-79.56-30.9-170.44-52.1-303.41-58.06C2068,441,2025.41,439.52,1686,439.52s-382.1,1.41-515.45,7.57"
fill="#ffffff"
/>
</svg>
);
}

View file

@ -0,0 +1,17 @@
/** LinkedIn's full-colour mark: the white "in" on its blue square. Supplied directly. */
export function LinkedinLogo(props: React.SVGProps<SVGSVGElement>) {
return (
<svg viewBox="0 0 512 512" aria-hidden="true" {...props}>
<path
fill="#007ebb"
fillRule="evenodd"
d="M56.9 512h398.2c31.4 0 56.9-25.5 56.9-56.9V56.9C512 25.5 486.5 0 455.1 0H56.9C25.5 0 0 25.5 0 56.9v398.2C0 486.5 25.5 512 56.9 512"
/>
<path
fill="#fff"
fillRule="evenodd"
d="M440.9 440.9h-76V311.5c0-35.5-13.5-55.3-41.6-55.3c-30.5 0-46.5 20.6-46.5 55.3v129.4h-73.2V194.4h73.2v33.2s22-40.7 74.3-40.7s89.7 31.9 89.7 98v156zM116.3 162.1c-24.9 0-45.2-20.4-45.2-45.5s20.2-45.5 45.2-45.5s45.1 20.4 45.1 45.5s-20.2 45.5-45.1 45.5M78.5 440.9h76.4V194.4H78.5z"
/>
</svg>
);
}

View file

@ -0,0 +1,154 @@
import { useId } from "react";
/**
* Reddit's full-colour mark (the orange circle with Snoo), as supplied directly.
*
* Unlike the other brand logos it carries its own colours and gradients, so
* it ignores `currentColor`. Its gradient ids come from `useId`, because the
* plugins page draws it twice: once in the grid and once in the footer.
*/
export function RedditLogo(props: React.SVGProps<SVGSVGElement>) {
const id = useId();
return (
<svg viewBox="0 0 216 216" aria-hidden="true" {...props}>
<defs>
<radialGradient
id={`${id}-face`}
cx="169.75"
cy="92.19"
r="50.98"
fx="169.75"
fy="92.19"
gradientTransform="matrix(1 0 0 .87 0 11.64)"
gradientUnits="userSpaceOnUse"
>
<stop offset="0" stopColor="#feffff" />
<stop offset=".4" stopColor="#feffff" />
<stop offset=".51" stopColor="#f9fcfc" />
<stop offset=".62" stopColor="#edf3f5" />
<stop offset=".7" stopColor="#dee9ec" />
<stop offset=".72" stopColor="#d8e4e8" />
<stop offset=".76" stopColor="#ccd8df" />
<stop offset=".8" stopColor="#c8d5dd" />
<stop offset=".83" stopColor="#ccd6de" />
<stop offset=".85" stopColor="#d8dbe2" />
<stop offset=".88" stopColor="#ede3e9" />
<stop offset=".9" stopColor="#ffebef" />
</radialGradient>
<radialGradient
id={`${id}-ear-left`}
cx="47.31"
r="50.98"
fx="47.31"
href={`#${id}-face`}
/>
<radialGradient
id={`${id}-head`}
cx="109.61"
cy="85.59"
r="153.78"
fx="109.61"
fy="85.59"
gradientTransform="matrix(1 0 0 .7 0 25.56)"
href={`#${id}-face`}
/>
<radialGradient
id={`${id}-eye-left`}
cx="-6.01"
cy="64.68"
r="12.85"
fx="-6.01"
fy="64.68"
gradientTransform="matrix(1.07 0 0 1.55 81.08 27.26)"
gradientUnits="userSpaceOnUse"
>
<stop offset="0" stopColor="#f60" />
<stop offset=".5" stopColor="#ff4500" />
<stop offset=".7" stopColor="#fc4301" />
<stop offset=".82" stopColor="#f43f07" />
<stop offset=".92" stopColor="#e53812" />
<stop offset="1" stopColor="#d4301f" />
</radialGradient>
<radialGradient
id={`${id}-eye-right`}
cx="-73.55"
cy="64.68"
r="12.85"
fx="-73.55"
fy="64.68"
gradientTransform="matrix(-1.07 0 0 1.55 62.87 27.26)"
href={`#${id}-eye-left`}
/>
<radialGradient
id={`${id}-mouth`}
cx="107.93"
cy="166.96"
r="45.3"
fx="107.93"
fy="166.96"
gradientTransform="matrix(1 0 0 .66 0 57.4)"
gradientUnits="userSpaceOnUse"
>
<stop offset="0" stopColor="#172e35" />
<stop offset=".29" stopColor="#0e1c21" />
<stop offset=".73" stopColor="#030708" />
<stop offset="1" />
</radialGradient>
<radialGradient
id={`${id}-antenna-tip`}
cx="147.88"
cy="32.94"
r="39.77"
fx="147.88"
fy="32.94"
gradientTransform="matrix(1 0 0 .98 0 .54)"
href={`#${id}-face`}
/>
<radialGradient
id={`${id}-antenna`}
cx="131.31"
cy="73.08"
r="32.6"
fx="131.31"
fy="73.08"
gradientUnits="userSpaceOnUse"
>
<stop offset=".48" stopColor="#7a9299" />
<stop offset=".67" stopColor="#172e35" />
<stop offset=".75" />
<stop offset=".82" stopColor="#172e35" />
</radialGradient>
</defs>
<path
fill="#ff4500"
d="M108 0C48.35 0 0 48.35 0 108c0 29.82 12.09 56.82 31.63 76.37l-20.57 20.57C6.98 209.029.87 216 15.64 216H108c59.65 0 108-48.35 108-108S167.65 0 108 0"
/>
<circle cx="169.22" cy="106.98" r="25.22" fill={`url(#${id}-face)`} />
<circle cx="46.78" cy="106.98" r="25.22" fill={`url(#${id}-ear-left)`} />
<ellipse cx="108.06" cy="128.64" fill={`url(#${id}-head)`} rx="72" ry="54" />
<path
fill={`url(#${id}-eye-left)`}
d="M86.78 123.48c-.42 9.08-6.49 12.38-13.56 12.38s-12.46-4.93-12.04-14.01s6.49-15.02 13.56-15.02s12.46 7.58 12.04 16.66Z"
/>
<path
fill={`url(#${id}-eye-right)`}
d="M129.35 123.48c.42 9.08 6.49 12.38 13.56 12.38s12.46-4.93 12.04-14.01s-6.49-15.02-13.56-15.02s-12.46 7.58-12.04 16.66Z"
/>
<ellipse cx="79.63" cy="116.37" fill="#ffc49c" rx="2.8" ry="3.05" />
<ellipse cx="146.21" cy="116.37" fill="#ffc49c" rx="2.8" ry="3.05" />
<path
fill={`url(#${id}-mouth)`}
d="M108.06 142.92c-8.76 0-17.16.43-24.92 1.22c-1.33.13-2.17 1.51-1.65 2.74c4.35 10.39 14.61 17.69 26.57 17.69s22.23-7.3 26.57-17.69c.52-1.23-.33-2.61-1.65-2.74c-7.77-.79-16.16-1.22-24.92-1.22"
/>
<circle cx="147.49" cy="49.43" r="17.87" fill={`url(#${id}-antenna-tip)`} />
<path
fill={`url(#${id}-antenna)`}
d="M107.8 76.92c-2.14 0-3.87-.89-3.87-2.27c0-16.01 13.03-29.04 29.04-29.04c2.14 0 3.87 1.73 3.87 3.87s-1.73 3.87-3.87 3.87c-11.74 0-21.29 9.55-21.29 21.29c0 1.38-1.73 2.27-3.87 2.27Z"
/>
<path
fill="#842123"
d="M62.82 122.65c.39-8.56 6.08-14.16 12.69-14.16c6.26 0 11.1 6.39 11.28 14.33c.17-8.88-5.13-15.99-12.05-15.99s-13.14 6.05-13.56 15.2s4.97 13.83 12.04 13.83h.52c-6.44-.16-11.3-4.79-10.91-13.2Zm90.48 0c-.39-8.56-6.08-14.16-12.69-14.16c-6.26 0-11.1 6.39-11.28 14.33c-.17-8.88 5.13-15.99 12.05-15.99c7.07 0 13.14 6.05 13.56 15.2s-4.97 13.83-12.04 13.83h-.52c6.44-.16 11.3-4.79 10.91-13.2Z"
/>
</svg>
);
}

View file

@ -0,0 +1,27 @@
/** TikTok's full-colour mark. */
export function TiktokLogo(props: React.SVGProps<SVGSVGElement>) {
return (
<svg viewBox="0 0 64 64" aria-hidden="true" {...props}>
<path
fill="#070201"
fillRule="evenodd"
d="m60,12c0-4.42-3.58-8-8-8H12C7.58,4,4,7.58,4,12v40c0,4.42,3.58,8,8,8h40c4.42,0,8-3.58,8-8V12h0Z"
/>
<path
fill="#fe2c55"
fillRule="evenodd"
d="m47.64,27.65c0,.3-.13.58-.37.77-.23.19-.53.22-.83.21-1.86-.05-4.77-.69-6.14-2v12.54c0,5.78-4.68,10.46-10.46,10.46h-1.08c-2.42,0-4.72-.88-6.58-2.39,1.43.71,3,1.13,4.63,1.13h1.08c5.78,0,10.46-4.68,10.46-10.46v-12.54c1.37,1.3,4.28,1.95,6.14,2,.3,0,.6-.02.83-.21.23-.19.37-.47.37-.77,0-1.42,0-3.78,0-5.02.38.11.77.2,1.16.29.46.1.79.5.79.97,0,1.24,0,3.61,0,5.02Zm-19.58,4.43c-.19.19-.44.29-.71.29-2.76,0-5,2.24-5,5,0,2.17,1.39,4,3.33,4.69-.85-.9-1.38-2.1-1.38-3.43,0-2.76,2.24-5,5-5,.27,0,.52-.1.71-.29.19-.19.29-.44.29-.71v-3c0-.26-.11-.52-.29-.71s-.44-.29-.71-.29h-.54c-.14,0-.27.04-.41.04v2.7c0,.27-.11.52-.29.71Zm13.67-12.69c-.83-.75-1.53-1.74-2.03-3.09-.15-.39-.52-.67-.94-.67h-.74c.87,1.91,2.16,3.03,3.71,3.75Z"
/>
<path
fill="#25f4ee"
fillRule="evenodd"
d="m21.37,46.57c.25.25.55.45.82.67-1.01-.5-1.96-1.12-2.77-1.93-1.96-1.96-3.06-4.62-3.06-7.4v-.08c0-5.78,4.68-10.46,10.46-10.46h.54c.27,0,.52.11.71.29.19.19.29.44.29.71v.3c-5.58.22-10.05,4.78-10.05,10.42v.08c0,2.77,1.1,5.43,3.06,7.4Zm24.31-25.2h0c0-.47-.33-.88-.79-.97-1.12-.25-2.19-.55-3.17-1.01,1.11,1,2.47,1.57,3.95,1.98Zm-16.38,22.27c1.33,0,2.6-.53,3.54-1.46s1.46-2.21,1.46-3.54v-22c0-.55.45-1,1-1h2.72c-.09-.2-.19-.38-.27-.6-.15-.39-.52-.67-.94-.67h-3.46c-.55,0-1,.45-1,1v22c0,1.33-.53,2.6-1.46,3.54s-2.21,1.46-3.54,1.46c-.59,0-1.15-.12-1.67-.31.91.96,2.19,1.57,3.62,1.57Z"
/>
<path
fill="#fff"
fillRule="evenodd"
d="m27.9,48.37c5.78,0,10.46-4.68,10.46-10.46v-12.54c1.37,1.3,4.28,1.95,6.14,2,.3,0,.6-.02.83-.21.23-.19.37-.47.37-.77,0-1.42,0-3.78,0-5.02-1.48-.41-2.84-.98-3.95-1.98-1.54-.72-2.84-1.85-3.71-3.75h-2.72c-.55,0-1,.45-1,1v22c0,1.33-.53,2.6-1.46,3.54s-2.21,1.46-3.54,1.46c-1.43,0-2.71-.61-3.62-1.57-1.93-.69-3.33-2.52-3.33-4.69,0-2.76,2.24-5,5-5,.27,0,.52-.1.71-.29.19-.19.29-.44.29-.71v-2.7c-5.58.22-10.05,4.78-10.05,10.42v.08c0,2.77,1.1,5.43,3.06,7.4.25.25.55.45.82.67,1.43.71,3,1.13,4.63,1.13h1.08Z"
/>
</svg>
);
}

View file

@ -0,0 +1,15 @@
/** Walmart's spark. */
export function WalmartLogo(props: React.SVGProps<SVGSVGElement>) {
return (
<svg viewBox="0 0 48 48" fill="none" aria-hidden="true" {...props}>
<g fill="#ffc220">
<rect x="21.4" y="3.5" width="5.2" height="14" rx="2.6" transform="rotate(0 24 24)" />
<rect x="21.4" y="3.5" width="5.2" height="14" rx="2.6" transform="rotate(60 24 24)" />
<rect x="21.4" y="3.5" width="5.2" height="14" rx="2.6" transform="rotate(120 24 24)" />
<rect x="21.4" y="3.5" width="5.2" height="14" rx="2.6" transform="rotate(180 24 24)" />
<rect x="21.4" y="3.5" width="5.2" height="14" rx="2.6" transform="rotate(240 24 24)" />
<rect x="21.4" y="3.5" width="5.2" height="14" rx="2.6" transform="rotate(300 24 24)" />
</g>
</svg>
);
}

View file

@ -0,0 +1,18 @@
/** A globe, for the Web Crawl plugin. */
export function WebCrawlLogo(props: React.SVGProps<SVGSVGElement>) {
return (
<svg viewBox="0 0 48 48" aria-hidden="true" {...props}>
<g fill="none" strokeWidth="3">
<path fill="#8fbffa" d="M3 24a21 21 0 1 0 42 0a21 21 0 1 0-42 0" />
<path stroke="#2859c5" strokeLinejoin="round" d="M3 24a21 21 0 1 0 42 0a21 21 0 1 0-42 0" />
<path stroke="#2859c5" strokeLinejoin="round" d="M15 24a9 21 0 1 1 18 0a9 21 0 1 1-18 0" />
<path
stroke="#2859c5"
strokeLinecap="round"
strokeLinejoin="round"
d="M4.5 31h39m-39-14h39"
/>
</g>
</svg>
);
}

View file

@ -0,0 +1,8 @@
/** X's (formerly Twitter) official mark, in `currentColor`. From Simple Icons (CC0), simple-icons@16.32.0. */
export function XLogo(props: React.SVGProps<SVGSVGElement>) {
return (
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" {...props}>
<path d="M14.234 10.162 22.977 0h-2.072l-7.591 8.824L7.251 0H.258l9.168 13.343L.258 24H2.33l8.016-9.318L16.749 24h6.993zm-2.837 3.299-.929-1.329L3.076 1.56h3.182l5.965 8.532.929 1.329 7.754 11.09h-3.182z" />
</svg>
);
}

View file

@ -0,0 +1,12 @@
/** YouTube's full-colour mark. */
export function YoutubeLogo(props: React.SVGProps<SVGSVGElement>) {
return (
<svg viewBox="0 0 256 180" aria-hidden="true" {...props}>
<path
fill="#f00"
d="M250.346 28.075A32.18 32.18 0 0 0 227.69 5.418C207.824 0 127.87 0 127.87 0S47.912.164 28.046 5.582A32.18 32.18 0 0 0 5.39 28.24c-6.009 35.298-8.34 89.084.165 122.97a32.18 32.18 0 0 0 22.656 22.657c19.866 5.418 99.822 5.418 99.822 5.418s79.955 0 99.82-5.418a32.18 32.18 0 0 0 22.657-22.657c6.338-35.348 8.291-89.1-.164-123.134"
/>
<path fill="#fff" d="m102.421 128.06l66.328-38.418l-66.328-38.418z" />
</svg>
);
}

View file

@ -64,11 +64,18 @@ export function TopAnnouncementBar() {
`--home-max`, bordered on the sides. Filling *this* box with the
notice color (rather than the full-bleed strip around it) is what
keeps the banner reading as the same width as the nav. */}
<div className="relative mx-auto flex max-w-(--home-max) items-center justify-center gap-2 border-x border-border bg-(--notice,#3f74c8) px-4 py-2.5 text-center text-sm font-medium text-white">
<div className="relative mx-auto flex max-w-(--home-max) items-center justify-center gap-2 border-x border-border bg-(--notice,#3f74c8) px-10 py-2.5 sm:px-4 text-center text-sm font-medium text-white">
{/* On mobile the whole bar is the link, so the text stays one line
instead of wrapping around a separate "Read announcement". */}
<Link
href="/sunset"
aria-label="Read announcement"
className="absolute inset-0 sm:hidden"
/>
<span>SurfSense is moving to a local app.</span>
<Link
href="/sunset"
className="inline-flex items-center gap-1 underline underline-offset-2 hover:opacity-90"
className="hidden items-center sm:inline-flex gap-1 underline underline-offset-2 hover:opacity-90"
>
Read announcement
<LinkSquare02Icon className="size-3.5" />
@ -77,7 +84,7 @@ export function TopAnnouncementBar() {
type="button"
onClick={handleDismiss}
aria-label="Dismiss announcement"
className="absolute right-2 top-1/2 -translate-y-1/2 rounded-[2px] p-1 hover:opacity-80 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white"
className="absolute right-2 top-1/2 z-10 -translate-y-1/2 rounded-[2px] p-1 hover:opacity-80 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white"
>
<Cancel01Icon className="size-4" />
</button>

View file

@ -105,7 +105,7 @@ function PlanCell({ plan }: { plan: Plan }) {
key={action.label}
asChild
size="lg"
variant={action.primary ? "default" : "outline"}
variant={action.primary ? "default" : "secondary"}
>
{action.external ? (
<a href={action.href} target="_blank" rel="noreferrer noopener">

View file

@ -1,14 +1,8 @@
import {
IconBrandDiscord,
IconBrandGithub,
IconBrandLinkedin,
IconBrandReddit,
IconBrandTwitter,
} from "@tabler/icons-react";
import Image from "next/image";
import Link from "next/link";
import { FOOTER_COLUMNS, REPO_URL } from "@/components/site/site-content";
import { FOOTER_COLUMNS } from "@/components/site/site-content";
import { SiteFooterGlass } from "@/components/site/site-footer-glass";
import { SiteSocials } from "@/components/site/site-socials";
/**
* Site footer.
@ -28,18 +22,6 @@ import { SiteFooterGlass } from "@/components/site/site-footer-glass";
* it is held to the same width without the side borders.
*/
const SOCIALS = [
{ title: "GitHub", href: REPO_URL, icon: IconBrandGithub },
{ title: "Discord", href: "https://discord.gg/ejRNvftDp9", icon: IconBrandDiscord },
{ title: "Twitter", href: "https://x.com/mod_setter", icon: IconBrandTwitter },
{ title: "Reddit", href: "https://www.reddit.com/r/SurfSense/", icon: IconBrandReddit },
{
title: "LinkedIn",
href: "https://www.linkedin.com/company/surfsense/",
icon: IconBrandLinkedin,
},
];
const LINK_CLASS =
"text-sm text-[#e8e3da]/65 transition-colors duration-100 hover:text-[#e8e3da] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#d9aa90] focus-visible:rounded-xs";
@ -54,15 +36,9 @@ export function SiteFooter() {
<div className="ss-home-pad flex flex-col gap-12 py-16 lg:flex-row lg:justify-between lg:gap-16">
<div className="flex max-w-sm shrink-0 flex-col justify-between gap-12">
<div>
<Link href="/" className="inline-flex items-center gap-1.5">
<Image
src="/icon-128.svg"
alt=""
width={28}
height={28}
className="size-7 select-none invert"
/>
<span className="text-xl font-semibold tracking-tight text-[#e8e3da]">SurfSense</span>
<Link href="/" className="select-none inline-flex items-center gap-1.5">
<Image src="/icon-128.svg" alt="" width={28} height={28} className="size-6 invert" />
<span className="ss-home-wordmark text-xl text-[#e8e3da]">SurfSense</span>
</Link>
<p className="mt-4 text-lg font-medium leading-snug text-[#e8e3da]/90">
@ -73,24 +49,7 @@ export function SiteFooter() {
</div>
<div>
<ul className="flex list-none flex-wrap gap-4 p-0">
{SOCIALS.map((social) => {
const Icon = social.icon;
return (
<li key={social.title}>
<a
href={social.href}
target="_blank"
rel="noreferrer noopener"
aria-label={social.title}
className={`${LINK_CLASS} inline-flex`}
>
<Icon aria-hidden="true" className="size-5" />
</a>
</li>
);
})}
</ul>
<SiteSocials />
<p className="mt-4 text-xs text-[#e8e3da]/55">
&copy; SurfSense {new Date().getFullYear()}. Free and open source.

View file

@ -1,6 +1,6 @@
"use client";
import { IconChevronDown, IconMenu2, IconX } from "@tabler/icons-react";
import { IconChevronDown } from "@tabler/icons-react";
import Image from "next/image";
import Link from "next/link";
import { useEffect, useRef, useState } from "react";
@ -33,7 +33,7 @@ function Wordmark() {
return (
<Link
href="/"
className="flex shrink-0 items-center gap-1.5 rounded-[2px] px-1 py-1 transition-colors duration-100 hover:text-[color:var(--muted-foreground)] focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-[color:var(--ring)]"
className="select-none flex shrink-0 items-center gap-1.5 rounded-[2px] px-1 py-1 transition-colors duration-100 hover:text-[color:var(--muted-foreground)] focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-[color:var(--ring)]"
>
<Image
src="/icon-128.svg"
@ -41,9 +41,9 @@ function Wordmark() {
width={20}
height={20}
priority
className="size-5 select-none dark:invert"
className="size-6 dark:invert"
/>
<span className="text-[0.9375rem] font-semibold tracking-tight text-[color:var(--foreground)]">
<span className="ss-home-wordmark text-lg text-[color:var(--foreground)]">
SurfSense
</span>
</Link>
@ -139,7 +139,7 @@ export function SiteNav({ starCount, starsHref }: { starCount: number | null; st
</div>
</nav>
<div className="flex items-center gap-1">
<div className="ss-home-nav-actions flex items-center gap-1">
<SiteStars count={starCount} href={starsHref} />
<button
@ -150,63 +150,70 @@ export function SiteNav({ starCount, starsHref }: { starCount: number | null; st
}}
aria-label={menuOpen ? "Close menu" : "Open menu"}
aria-expanded={menuOpen}
data-state={menuOpen ? "open" : "closed"}
className="ss-home-nav-link ss-home-nav-toggle"
>
{menuOpen ? (
<IconX aria-hidden="true" className="size-4" />
) : (
<IconMenu2 aria-hidden="true" className="size-4" />
)}
{/* One icon whose bars morph between menu and close, rather than
two icons swapped in a single frame. */}
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
aria-hidden="true"
className="ss-home-nav-burger size-4"
>
<line x1="4" y1="6" x2="20" y2="6" />
<line x1="4" y1="12" x2="20" y2="12" />
<line x1="4" y1="18" x2="20" y2="18" />
</svg>
</button>
</div>
</div>
{menuOpen ? (
<>
{/* Tap-to-dismiss ground. It is decorative: Escape and the toggle
both already close the menu, so a keyboard user loses nothing by
it being unreachable. */}
<button
type="button"
aria-hidden="true"
tabIndex={-1}
className="ss-home-nav-scrim"
onClick={closeAll}
/>
{/* The scrim and drawer stay mounted so opening and closing can
animate; `inert` keeps the closed drawer out of the tab order and
the accessibility tree.
{/* No Sign in here: the bar's own button stays visible while the
drawer is open, so repeating it would show the same control
twice on one screen. */}
<div className="ss-home-nav-drawer">
<div className="flex flex-col gap-0.5 px-4 py-3">
{NAV_LINKS.map((link) => (
<Link
key={link.href}
href={link.href}
onClick={closeAll}
className="ss-home-nav-link"
>
{link.name}
</Link>
))}
The scrim is a tap-to-dismiss ground. It is decorative: Escape and
the toggle both already close the menu, so a keyboard user loses
nothing by it being unreachable. */}
<button
type="button"
aria-hidden="true"
tabIndex={-1}
data-state={menuOpen ? "open" : "closed"}
className="ss-home-nav-scrim"
onClick={closeAll}
/>
<div className="my-1.5 border-t border-[color:var(--border)]" />
<p className="ss-home-eyebrow px-2.5 pt-1 pb-1">Resources</p>
{/* No Sign in here: the bar's own button stays visible while the
drawer is open, so repeating it would show the same control twice
on one screen. */}
<div
data-state={menuOpen ? "open" : "closed"}
inert={!menuOpen}
data-lenis-prevent
className="ss-home-nav-drawer"
>
<div className="flex flex-col gap-0.5 px-4 py-3">
{NAV_LINKS.map((link) => (
<Link key={link.href} href={link.href} onClick={closeAll} className="ss-home-nav-link">
{link.name}
</Link>
))}
{NAV_RESOURCES.map((item) => (
<Link
key={item.href}
href={item.href}
onClick={closeAll}
className="ss-home-nav-link"
>
{item.name}
</Link>
))}
</div>
</div>
</>
) : null}
<div className="my-1.5 border-t border-[color:var(--border)]" />
<p className="ss-home-eyebrow px-2.5 pt-1 pb-1">Resources</p>
{NAV_RESOURCES.map((item) => (
<Link key={item.href} href={item.href} onClick={closeAll} className="ss-home-nav-link">
{item.name}
</Link>
))}
</div>
</div>
</header>
);
}

View file

@ -0,0 +1,10 @@
import { Instrument_Sans } from "next/font/google";
// Text font for the marketing routes and the docs; the app routes stay on Roboto.
// One loader call, so both trees share a single @font-face.
// Taking over --font-sans keeps `font-sans` utilities on it too.
export const siteSansFont = Instrument_Sans({
subsets: ["latin"],
display: "optional",
variable: "--font-sans",
});

View file

@ -0,0 +1,53 @@
import { DiscordLogo } from "@/components/homepage/icons/discord-logo";
import { GithubLogo } from "@/components/homepage/icons/github-logo";
import { LinkedinLogo } from "@/components/homepage/icons/linkedin-logo";
import { RedditLogo } from "@/components/homepage/icons/reddit-logo";
import { XLogo } from "@/components/homepage/icons/x-logo";
import { REPO_URL } from "@/components/site/site-content";
/**
* The footer's social links, drawn with each brand's official mark.
*
* Each mark is shown in its brand's official colour. GitHub and X are black
* marks, which would vanish on the dark footer, so they use the white version
* both brands publish for dark backgrounds. Reddit's and LinkedIn's marks are
* full-colour and bring their own.
*/
type Social = {
title: string;
href: string;
Logo: (props: React.SVGProps<SVGSVGElement>) => React.ReactNode;
color?: string;
};
const DARK_GROUND_MARK = "#ffffff";
const SOCIALS: Social[] = [
{ title: "GitHub", href: REPO_URL, Logo: GithubLogo, color: DARK_GROUND_MARK },
{ title: "Discord", href: "https://discord.gg/ejRNvftDp9", Logo: DiscordLogo, color: "#5865F2" },
{ title: "X", href: "https://x.com/mod_setter", Logo: XLogo, color: DARK_GROUND_MARK },
{ title: "Reddit", href: "https://www.reddit.com/r/SurfSense/", Logo: RedditLogo },
{ title: "LinkedIn", href: "https://www.linkedin.com/company/surfsense/", Logo: LinkedinLogo },
];
export function SiteSocials() {
return (
<ul className="flex list-none flex-wrap gap-4 p-0">
{SOCIALS.map(({ title, href, Logo, color }) => (
<li key={title}>
<a
href={href}
target="_blank"
rel="noreferrer noopener"
style={color ? { color } : undefined}
className="inline-flex transition-opacity duration-100 hover:opacity-80 focus-visible:rounded-xs focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#d9aa90]"
>
<Logo className="size-5" />
<span className="sr-only">{title}</span>
</a>
</li>
))}
</ul>
);
}

View file

@ -1,4 +1,4 @@
import { IconBrandGithub } from "@tabler/icons-react";
import { GithubLogo } from "@/components/homepage/icons/github-logo";
/**
* GitHub star count in the site navigation.
@ -39,7 +39,7 @@ export function SiteStars({ count, href }: { count: number | null; href: string
: `SurfSense on GitHub, ${count.toLocaleString("en-US")} stars`
}
>
<IconBrandGithub aria-hidden="true" className="size-4 shrink-0" />
<GithubLogo aria-hidden="true" className="size-4 shrink-0" />
<span aria-hidden="true" className="ss-home-nav-stars-count">
{count === null ? "" : compact(count)}
</span>

Binary file not shown.

After

Width:  |  Height:  |  Size: 927 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 395 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 135 KiB