6 Commits

Author SHA1 Message Date
bl 1f47337c40 chore: Release cnats version 0.2.5
Release / build (x86_64, ubuntu-latest) (push) Successful in 8m17s
Release / build (aarch64, aarch64) (push) Successful in 14m46s
Release / update-aur (push) Successful in 15s
Release / docker (push) Successful in 19m58s
2026-08-16 14:42:39 +02:00
bl e2caee536f Light theme: green-tinted paper print following prefers-color-scheme
Palette override in a light media query; scanlines, vignette, card
shadow and signal glows moved to tokens/color-mix so both prints share
one set of rules. color-scheme on :root brings UA scrollbars and form
controls along.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 14:40:49 +02:00
bl 080b9c46d6 chore: Release cnats version 0.2.4
Release / build (x86_64, ubuntu-latest) (push) Successful in 7m14s
Release / docker (push) Successful in 4m32s
Release / build (aarch64, aarch64) (push) Successful in 14m2s
Release / update-aur (push) Successful in 10s
2026-07-30 15:06:52 +02:00
bl 3807294333 Fix mic feedback in solo calls: force .muted on the local preview tile
Every video tile is built client-side via document.createElement, never
parsed from HTML - browsers only seed the live .muted property from the
muted attribute for parser-inserted elements, so the attribute alone
never actually silenced the local preview. Result: your own mic played
back through your own speakers even with no other peers in the call.
Now sets el.set_muted(true) directly whenever the preview's srcObject
is (re)assigned.
2026-07-30 15:06:14 +02:00
bl 9c89cfbe9c chore: Release cnats version 0.2.3
Release / build (x86_64, ubuntu-latest) (push) Successful in 7m5s
Release / docker (push) Successful in 3m54s
Release / build (aarch64, aarch64) (push) Successful in 13m37s
Release / update-aur (push) Successful in 11s
2026-07-29 10:50:09 +02:00
bl 9326f93576 Fix hydration mismatch in CallPanel that crashed room switching
The SSR branch rendered an empty call-panel div while the hydrate
branch expected a child button node. On any full load/refresh of the
lobby room, the mismatched hydration cursor hit tachys's
unreachable!() panic path, trapping the wasm instance and killing all
reactivity (routing, room switching, SSE) for the rest of the page
load, while the already-rendered SSR HTML stayed visually intact.

SSR now renders the same default join-button markup hydrate expects.
2026-07-29 10:50:02 +02:00
5 changed files with 61 additions and 12 deletions
Generated
+1 -1
View File
@@ -345,7 +345,7 @@ dependencies = [
[[package]] [[package]]
name = "cnats" name = "cnats"
version = "0.2.2" version = "0.2.5"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-nats", "async-nats",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "cnats" name = "cnats"
version = "0.2.2" version = "0.2.5"
edition = "2021" edition = "2021"
[lib] [lib]
+1 -1
View File
@@ -1,6 +1,6 @@
# Maintainer: Bendik Aagaard Lynghaug <bendik.lynghaug@gmail.com> # Maintainer: Bendik Aagaard Lynghaug <bendik.lynghaug@gmail.com>
pkgname=cnats pkgname=cnats
pkgver=0.2.2 pkgver=0.2.5
pkgrel=1 pkgrel=1
pkgdesc="Web chat over NATS subjects with Kanidm SSO (Leptos SSR)" pkgdesc="Web chat over NATS subjects with Kanidm SSO (Leptos SSR)"
arch=('x86_64' 'aarch64') arch=('x86_64' 'aarch64')
+23 -1
View File
@@ -450,9 +450,19 @@ fn CallPanel(room: Memo<String>, me: String) -> impl IntoView {
// Mirror the local MediaStream into the preview <video> element - // Mirror the local MediaStream into the preview <video> element -
// `srcObject` has no HTML attribute form, has to be set via JS. // `srcObject` has no HTML attribute form, has to be set via JS.
//
// Also force `.muted` via the JS property here, not just the
// `muted` attribute on the element below: every video tile in this
// app is built client-side via `document.createElement` (never
// parsed from HTML), and browsers only seed the live `.muted`
// property from the `muted` *attribute* for parser-inserted
// elements. Without this, the local preview plays back the user's
// own mic through their speakers - audible as feedback in a
// solo call.
Effect::new(move |_| { Effect::new(move |_| {
let stream = call_state.get_value().local_stream().get(); let stream = call_state.get_value().local_stream().get();
if let Some(el) = local_video_ref.get() { if let Some(el) = local_video_ref.get() {
el.set_muted(true);
el.set_src_object(stream.as_ref()); el.set_src_object(stream.as_ref());
} }
}); });
@@ -495,10 +505,22 @@ fn CallPanel(room: Memo<String>, me: String) -> impl IntoView {
} }
.into_any() .into_any()
} }
// Must mirror the hydrate branch's default (not-in-call) markup exactly -
// hydration reconciles this SSR output against what the hydrate branch
// above expects to find, and an empty div here (vs. the button hydrate
// wants) is a hydration mismatch that panics and traps the whole wasm
// instance, killing all reactivity on the page.
#[cfg(not(feature = "hydrate"))] #[cfg(not(feature = "hydrate"))]
{ {
let _ = (room, me); let _ = (room, me);
view! { <div class="call-panel"></div> }.into_any() view! {
<div class="call-panel">
<button class="call-join" disabled=true>
"☎ join call"
</button>
</div>
}
.into_any()
} }
} }
+35 -8
View File
@@ -1,8 +1,10 @@
/* ── cnats · message-bus console ───────────────────────────────────────── /* ── cnats · message-bus console ─────────────────────────────────────────
dark phosphor terminal: deep green-black ground, mint signal, amber id. phosphor terminal in two prints: dark (green-black ground, mint signal)
and light (green-tinted paper, forest signal), following the OS scheme.
type: Archivo (UI voice) + IBM Plex Mono (wire voice). */ type: Archivo (UI voice) + IBM Plex Mono (wire voice). */
:root { :root {
color-scheme: light dark;
--ink-0: #060a09; --ink-0: #060a09;
--ink-1: #0b1210; --ink-1: #0b1210;
--ink-2: #101a17; --ink-2: #101a17;
@@ -16,10 +18,34 @@
--signal-dim: #2a8f6c; --signal-dim: #2a8f6c;
--amber: #ffb454; --amber: #ffb454;
--alarm: #ff6b6b; --alarm: #ff6b6b;
--scanline: rgba(255, 255, 255, 0.015);
--vignette: rgba(0, 0, 0, 0.45);
--card-shadow: rgba(0, 0, 0, 0.55);
--mono: "IBM Plex Mono", ui-monospace, monospace; --mono: "IBM Plex Mono", ui-monospace, monospace;
--sans: "Archivo", system-ui, sans-serif; --sans: "Archivo", system-ui, sans-serif;
} }
@media (prefers-color-scheme: light) {
:root {
--ink-0: #f3f6f4;
--ink-1: #eaf0ec;
--ink-2: #dfe8e2;
--ink-3: #d2ded6;
--line: #c3d2c9;
--line-hot: #a3bcae;
--text: #14211c;
--text-dim: #46584f;
--text-faint: #74887e;
--signal: #0b7a52;
--signal-dim: #2f8a66;
--amber: #a85f00;
--alarm: #c73f3f;
--scanline: rgba(6, 10, 9, 0.02);
--vignette: rgba(6, 10, 9, 0.06);
--card-shadow: rgba(20, 33, 28, 0.18);
}
}
* { margin: 0; padding: 0; box-sizing: border-box; } * { margin: 0; padding: 0; box-sizing: border-box; }
html, body { height: 100%; } html, body { height: 100%; }
@@ -39,8 +65,8 @@ body::before {
pointer-events: none; pointer-events: none;
z-index: 999; z-index: 999;
background: background:
repeating-linear-gradient(0deg, rgba(255, 255, 255, 0.015) 0 1px, transparent 1px 3px), repeating-linear-gradient(0deg, var(--scanline) 0 1px, transparent 1px 3px),
radial-gradient(ellipse 120% 90% at 50% 40%, transparent 55%, rgba(0, 0, 0, 0.45)); radial-gradient(ellipse 120% 90% at 50% 40%, transparent 55%, var(--vignette));
} }
::selection { background: var(--signal); color: var(--ink-0); } ::selection { background: var(--signal); color: var(--ink-0); }
@@ -53,7 +79,7 @@ body::before {
place-items: center; place-items: center;
padding: 2rem; padding: 2rem;
background: background:
radial-gradient(ellipse 60% 45% at 50% 0%, rgba(78, 240, 177, 0.07), transparent 70%), radial-gradient(ellipse 60% 45% at 50% 0%, color-mix(in srgb, var(--signal) 7%, transparent), transparent 70%),
linear-gradient(var(--ink-0), var(--ink-1)); linear-gradient(var(--ink-0), var(--ink-1));
} }
@@ -69,7 +95,7 @@ body::before {
background: linear-gradient(160deg, var(--ink-2), var(--ink-1) 60%); background: linear-gradient(160deg, var(--ink-2), var(--ink-1) 60%);
padding: 3rem 2.75rem 2.5rem; padding: 3rem 2.75rem 2.5rem;
position: relative; position: relative;
box-shadow: 0 40px 80px rgba(0, 0, 0, 0.55); box-shadow: 0 40px 80px var(--card-shadow);
animation: rise 0.5s cubic-bezier(0.2, 0.9, 0.3, 1) both; animation: rise 0.5s cubic-bezier(0.2, 0.9, 0.3, 1) both;
} }
@@ -129,7 +155,8 @@ body::before {
.gate-btn:hover { .gate-btn:hover {
background: transparent; background: transparent;
color: var(--signal); color: var(--signal);
box-shadow: 0 0 24px rgba(78, 240, 177, 0.25), inset 0 0 12px rgba(78, 240, 177, 0.08); box-shadow: 0 0 24px color-mix(in srgb, var(--signal) 25%, transparent),
inset 0 0 12px color-mix(in srgb, var(--signal) 8%, transparent);
} }
.gate-btn-glyph { font-size: 1.05rem; } .gate-btn-glyph { font-size: 1.05rem; }
@@ -458,7 +485,7 @@ body::before {
.composer:focus-within { .composer:focus-within {
border-color: var(--signal-dim); border-color: var(--signal-dim);
box-shadow: 0 0 0 1px var(--signal-dim), 0 0 30px rgba(78, 240, 177, 0.08); box-shadow: 0 0 0 1px var(--signal-dim), 0 0 30px color-mix(in srgb, var(--signal) 8%, transparent);
} }
.composer-prompt { .composer-prompt {
@@ -551,7 +578,7 @@ body::before {
transition: background 120ms; transition: background 120ms;
} }
.call-leave:hover { background: rgba(255, 90, 90, 0.12); } .call-leave:hover { background: color-mix(in srgb, var(--alarm) 12%, transparent); }
/* motion */ /* motion */