Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"vite": "^7.3.0"
},
"dependencies": {
"@treyorr/voca-svelte": "^0.3.0"
"@treyorr/voca-svelte": "^0.3.0",
"@lucide/svelte": "^0.574.0"
}
}
128 changes: 127 additions & 1 deletion apps/web/src/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,27 @@

:root {
--pulse: 1px;
color-scheme: light;
}

:root[data-resolved-theme="dark"] {
--color-black: #ffffff;
--color-white: #000000;
--color-voca-bg: #000000;
--color-voca-fg: #ffffff;
--color-voca-border: #ffffff;
color-scheme: dark;
}

@media (prefers-color-scheme: dark) {
:root:not([data-resolved-theme]) {
--color-black: #ffffff;
--color-white: #000000;
--color-voca-bg: #000000;
--color-voca-fg: #ffffff;
--color-voca-border: #ffffff;
color-scheme: dark;
}
}

* {
Expand All @@ -21,6 +42,111 @@ body {
color: var(--color-voca-fg);
margin: 0;
padding: 0;
transition: background-color 200ms ease-out, color 200ms ease-out;
}

a,
button,
input,
nav,
main,
pre,
table,
thead,
tbody,
tr,
td,
th,
div {
transition: background-color 200ms ease-out, color 200ms ease-out, border-color 200ms ease-out,
box-shadow 200ms ease-out;
}

.theme-toggle {
position: fixed;
top: 0.75rem;
right: 0.75rem;
display: inline-flex;
gap: 0;
border: 1px solid var(--color-voca-border);
background: var(--color-voca-bg);
z-index: 120;
}

.theme-toggle button {
border: 0;
border-right: 1px solid var(--color-voca-border);
background: var(--color-voca-bg);
color: var(--color-voca-fg);
width: 2rem;
height: 2rem;
padding: 0;
font-family: var(--font-mono);
font-size: 0.75rem;
line-height: 1;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
}

.theme-toggle button:last-child {
border-right: 0;
}

.theme-toggle button.active {
background: var(--color-voca-fg);
color: var(--color-voca-bg);
}

.theme-toggle button:focus-visible {
outline: 2px solid var(--color-voca-border);
outline-offset: -2px;
}

.theme-icon {
width: 1rem;
height: 1rem;
}

:root[data-resolved-theme="dark"] .border-black {
border-color: var(--color-voca-border) !important;
}

:root[data-resolved-theme="dark"] .border-t-black {
border-top-color: var(--color-voca-border) !important;
}

:root[data-resolved-theme="dark"] .border-b-black {
border-bottom-color: var(--color-voca-border) !important;
}

:root[data-resolved-theme="dark"] .border-r-black {
border-right-color: var(--color-voca-border) !important;
}

:root[data-resolved-theme="dark"] .bg-white {
background-color: var(--color-voca-bg) !important;
}

:root[data-resolved-theme="dark"] .text-black {
color: var(--color-voca-fg) !important;
}

:root[data-resolved-theme="dark"] .bg-black {
background-color: var(--color-voca-fg) !important;
}

:root[data-resolved-theme="dark"] .text-white {
color: var(--color-voca-bg) !important;
}

:root[data-resolved-theme="dark"] .hover\:bg-black:hover {
background-color: var(--color-voca-fg) !important;
}

:root[data-resolved-theme="dark"] .hover\:text-white:hover {
color: var(--color-voca-bg) !important;
}

.brutalist-box {
Expand Down Expand Up @@ -91,4 +217,4 @@ body {

.brutalist-input::placeholder {
color: #888;
}
}
16 changes: 15 additions & 1 deletion apps/web/src/app.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,25 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<script>
(() => {
try {
const stored = localStorage.getItem("voca-theme");
const theme = stored === "light" || stored === "dark" ? stored : "system";
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
const resolved = theme === "system" ? (prefersDark ? "dark" : "light") : theme;
document.documentElement.dataset.theme = theme;
document.documentElement.dataset.resolvedTheme = resolved;
} catch {
// Ignore storage/matchMedia errors and fall back to CSS defaults.
}
})();
</script>
%sveltekit.head%
</head>

<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>

</html>
</html>
36 changes: 36 additions & 0 deletions apps/web/src/lib/theme.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
export type ThemePreference = "light" | "dark" | "system";

const THEME_KEY = "voca-theme";

const isStoredTheme = (value: string | null): value is "light" | "dark" =>
value === "light" || value === "dark";

export const getThemePreference = (): ThemePreference => {
const stored = localStorage.getItem(THEME_KEY);
return isStoredTheme(stored) ? stored : "system";
};

export const applyTheme = (preference: ThemePreference) => {
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
const resolvedTheme = preference === "system" ? (prefersDark ? "dark" : "light") : preference;

const root = document.documentElement;
root.dataset.theme = preference;
root.dataset.resolvedTheme = resolvedTheme;
};

export const setThemePreference = (preference: ThemePreference) => {
if (preference === "system") {
localStorage.removeItem(THEME_KEY);
} else {
localStorage.setItem(THEME_KEY, preference);
}

applyTheme(preference);
};

export const getThemeFromDom = (): ThemePreference | null => {
const current = document.documentElement.dataset.theme;
if (current === "light" || current === "dark" || current === "system") return current;
return null;
};
16 changes: 16 additions & 0 deletions apps/web/src/routes/+layout.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,22 @@
<script lang="ts">
import { onMount } from "svelte";
import { applyTheme, getThemePreference } from "$lib/theme";

import "../app.css";

let { children } = $props();

onMount(() => {
applyTheme(getThemePreference());

const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
const handlePreferenceChange = () => {
if (getThemePreference() === "system") applyTheme("system");
};

mediaQuery.addEventListener("change", handlePreferenceChange);
return () => mediaQuery.removeEventListener("change", handlePreferenceChange);
});
</script>

{@render children()}
43 changes: 43 additions & 0 deletions apps/web/src/routes/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,15 +1,28 @@
<script lang="ts">
import { goto } from "$app/navigation";
import { getThemeFromDom, getThemePreference, setThemePreference, type ThemePreference } from "$lib/theme";
import { Monitor, Moon, Sun } from "@lucide/svelte";
import { VocaClient, validatePassword } from "@treyorr/voca-svelte";
import { onMount } from "svelte";

let isCreating = $state(false);
let error = $state<string | null>(null);
let password = $state("");
let showPasswordInput = $state(false);
let themePreference = $state<ThemePreference>("system");

const serverUrl = import.meta.env.DEV ? "http://localhost:3001" : undefined;
const apiKey = import.meta.env.VITE_VOCA_API_KEY || "";

const setTheme = (preference: ThemePreference) => {
themePreference = preference;
setThemePreference(preference);
};

onMount(() => {
themePreference = getThemeFromDom() ?? getThemePreference();
});

async function createRoom() {
const trimmedPassword = password.trim();

Expand Down Expand Up @@ -49,6 +62,36 @@
<title>voca.vc</title>
</svelte:head>

<div class="theme-toggle" role="group" aria-label="Color theme">
<button
type="button"
class:active={themePreference === "light"}
aria-pressed={themePreference === "light"}
aria-label="Use light theme"
onclick={() => setTheme("light")}
>
<Sun class="theme-icon" strokeWidth={1.8} />
</button>
<button
type="button"
class:active={themePreference === "dark"}
aria-pressed={themePreference === "dark"}
aria-label="Use dark theme"
onclick={() => setTheme("dark")}
>
<Moon class="theme-icon" strokeWidth={1.8} />
</button>
<button
type="button"
class:active={themePreference === "system"}
aria-pressed={themePreference === "system"}
aria-label="Use system theme"
onclick={() => setTheme("system")}
>
<Monitor class="theme-icon" strokeWidth={1.8} />
</button>
</div>

<main class="min-h-screen flex flex-col items-center justify-center p-8">
<div class="brutalist-box max-w-lg w-full text-center">
<h1 class="text-4xl font-bold mb-4">voca.vc</h1>
Expand Down
3 changes: 3 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion services/signaling/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.