Compare commits
@@ -36,8 +36,15 @@ jobs:
|
|||||||
~/.cargo/bin
|
~/.cargo/bin
|
||||||
key: ${{ runner.os }}-${{ matrix.arch }}-cargo-leptos-${{ hashFiles('Cargo.lock') }}
|
key: ${{ runner.os }}-${{ matrix.arch }}-cargo-leptos-${{ hashFiles('Cargo.lock') }}
|
||||||
|
|
||||||
|
- name: Install cargo-binstall
|
||||||
|
run: |
|
||||||
|
command -v cargo-binstall || \
|
||||||
|
curl -L --proto '=https' --tlsv1.2 -sSf \
|
||||||
|
https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh \
|
||||||
|
| bash
|
||||||
|
|
||||||
- name: Install cargo-leptos
|
- name: Install cargo-leptos
|
||||||
run: command -v cargo-leptos || cargo install cargo-leptos --locked
|
run: command -v cargo-leptos || cargo binstall cargo-leptos --locked --no-confirm
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: cargo leptos build --release
|
run: cargo leptos build --release
|
||||||
@@ -58,7 +65,7 @@ jobs:
|
|||||||
- name: Create release
|
- name: Create release
|
||||||
run: |
|
run: |
|
||||||
curl -sX POST \
|
curl -sX POST \
|
||||||
-H "Authorization: token ${{ secrets.GITEA_TOKEN }}" \
|
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
"${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}/releases" \
|
"${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}/releases" \
|
||||||
-d "{\"tag_name\":\"${{ gitea.ref_name }}\",\"name\":\"${{ gitea.ref_name }}\"}" \
|
-d "{\"tag_name\":\"${{ gitea.ref_name }}\",\"name\":\"${{ gitea.ref_name }}\"}" \
|
||||||
@@ -67,24 +74,24 @@ jobs:
|
|||||||
- name: Upload assets
|
- name: Upload assets
|
||||||
run: |
|
run: |
|
||||||
RELEASE_ID=$(curl -s \
|
RELEASE_ID=$(curl -s \
|
||||||
-H "Authorization: token ${{ secrets.GITEA_TOKEN }}" \
|
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||||||
"${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}/releases/tags/${{ gitea.ref_name }}" \
|
"${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}/releases/tags/${{ gitea.ref_name }}" \
|
||||||
| jq -r '.id')
|
| jq -r '.id')
|
||||||
|
|
||||||
for FILE in "${{ env.TARBALL }}" "${{ env.TARBALL }}.sha256"; do
|
for FILE in "${{ env.TARBALL }}" "${{ env.TARBALL }}.sha256"; do
|
||||||
# Remove any existing asset with the same name so re-runs stay clean
|
# Remove any existing asset with the same name so re-runs stay clean
|
||||||
EXISTING=$(curl -s \
|
EXISTING=$(curl -s \
|
||||||
-H "Authorization: token ${{ secrets.GITEA_TOKEN }}" \
|
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||||||
"${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}/releases/${RELEASE_ID}/assets" \
|
"${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}/releases/${RELEASE_ID}/assets" \
|
||||||
| jq -r ".[] | select(.name == \"${FILE}\") | .id")
|
| jq -r ".[] | select(.name == \"${FILE}\") | .id")
|
||||||
for AID in $EXISTING; do
|
for AID in $EXISTING; do
|
||||||
curl -sX DELETE \
|
curl -sX DELETE \
|
||||||
-H "Authorization: token ${{ secrets.GITEA_TOKEN }}" \
|
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||||||
"${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}/releases/${RELEASE_ID}/assets/${AID}"
|
"${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}/releases/${RELEASE_ID}/assets/${AID}"
|
||||||
done
|
done
|
||||||
|
|
||||||
curl -sX POST \
|
curl -sX POST \
|
||||||
-H "Authorization: token ${{ secrets.GITEA_TOKEN }}" \
|
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||||||
-H "Content-Type: application/octet-stream" \
|
-H "Content-Type: application/octet-stream" \
|
||||||
"${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}/releases/${RELEASE_ID}/assets?name=${FILE}" \
|
"${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}/releases/${RELEASE_ID}/assets?name=${FILE}" \
|
||||||
--data-binary "@${FILE}" --fail-with-body
|
--data-binary "@${FILE}" --fail-with-body
|
||||||
@@ -125,6 +132,32 @@ jobs:
|
|||||||
sed -i "s/sha256sums_x86_64=('.*')/sha256sums_x86_64=('${SUM_X86}')/" aur/PKGBUILD
|
sed -i "s/sha256sums_x86_64=('.*')/sha256sums_x86_64=('${SUM_X86}')/" aur/PKGBUILD
|
||||||
sed -i "s/sha256sums_aarch64=('.*')/sha256sums_aarch64=('${SUM_AARCH}')/" aur/PKGBUILD
|
sed -i "s/sha256sums_aarch64=('.*')/sha256sums_aarch64=('${SUM_AARCH}')/" aur/PKGBUILD
|
||||||
|
|
||||||
|
# Also publish the built packages to this instance's Arch registry
|
||||||
|
# (docs.gitea.com/usage/packages/arch). The PKGBUILD only repacks the
|
||||||
|
# release tarballs, so CARCH can produce both architectures from this
|
||||||
|
# one host. Consumers: see the infrastructure README.
|
||||||
|
# Best-effort mirror to the instance Arch registry. The ephemeral
|
||||||
|
# GITHUB_TOKEN is not accepted as a package-write credential, so this
|
||||||
|
# uses a dedicated REGISTRY_TOKEN secret (a write:package token for bl);
|
||||||
|
# if it is unset the step is skipped, and continue-on-error keeps a
|
||||||
|
# registry hiccup from failing the release or the AUR push.
|
||||||
|
- name: Publish to the Arch package registry
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
if [ -z "${{ secrets.REGISTRY_TOKEN }}" ]; then
|
||||||
|
echo "::warning::REGISTRY_TOKEN not set — skipping Arch registry publish"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
cd aur
|
||||||
|
for carch in aarch64 x86_64; do
|
||||||
|
pkgfile=$(CARCH="$carch" makepkg --packagelist | tail -1)
|
||||||
|
CARCH="$carch" makepkg -f --nodeps --noconfirm --skipinteg
|
||||||
|
curl --fail-with-body --user "bl:${{ secrets.REGISTRY_TOKEN }}" \
|
||||||
|
--upload-file "$pkgfile" \
|
||||||
|
"${{ gitea.server_url }}/api/packages/${{ gitea.repository_owner }}/arch/uhhm"
|
||||||
|
done
|
||||||
|
|
||||||
- name: Push to AUR
|
- name: Push to AUR
|
||||||
env:
|
env:
|
||||||
AUR_SSH_KEY: ${{ secrets.AUR_SSH_KEY }}
|
AUR_SSH_KEY: ${{ secrets.AUR_SSH_KEY }}
|
||||||
|
|||||||
Generated
+1
-1
@@ -345,7 +345,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cnats"
|
name = "cnats"
|
||||||
version = "0.2.0"
|
version = "0.2.6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-nats",
|
"async-nats",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "cnats"
|
name = "cnats"
|
||||||
version = "0.2.0"
|
version = "0.2.6"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
|
|||||||
+5
-4
@@ -1,10 +1,11 @@
|
|||||||
# Maintainer: Bendik Aagaard Lynghaug <bendik.lynghaug@gmail.com>
|
# Maintainer: Bendik Aagaard Lynghaug <bendik.lynghaug@gmail.com>
|
||||||
pkgname=cnats
|
pkgname=cnats
|
||||||
pkgver=0.2.0
|
pkgver=0.2.6
|
||||||
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')
|
||||||
url="https://prosjekt.klingenbergbygg.no/bl/cnats"
|
options=('!strip')
|
||||||
|
url="https://project.uhhm.no/bl/cnats"
|
||||||
license=('MIT')
|
license=('MIT')
|
||||||
depends=('glibc' 'gcc-libs')
|
depends=('glibc' 'gcc-libs')
|
||||||
optdepends=(
|
optdepends=(
|
||||||
@@ -14,8 +15,8 @@ optdepends=(
|
|||||||
provides=('cnats')
|
provides=('cnats')
|
||||||
conflicts=('cnats-git' 'cnats-bin')
|
conflicts=('cnats-git' 'cnats-bin')
|
||||||
backup=('etc/cnats/env')
|
backup=('etc/cnats/env')
|
||||||
source_x86_64=("cnats-v${pkgver}-x86_64.tar.gz::https://prosjekt.klingenbergbygg.no/bl/cnats/releases/download/v${pkgver}/cnats-v${pkgver}-x86_64.tar.gz")
|
source_x86_64=("cnats-v${pkgver}-x86_64.tar.gz::https://project.uhhm.no/bl/cnats/releases/download/v${pkgver}/cnats-v${pkgver}-x86_64.tar.gz")
|
||||||
source_aarch64=("cnats-v${pkgver}-aarch64.tar.gz::https://prosjekt.klingenbergbygg.no/bl/cnats/releases/download/v${pkgver}/cnats-v${pkgver}-aarch64.tar.gz")
|
source_aarch64=("cnats-v${pkgver}-aarch64.tar.gz::https://project.uhhm.no/bl/cnats/releases/download/v${pkgver}/cnats-v${pkgver}-aarch64.tar.gz")
|
||||||
sha256sums_x86_64=('SKIP')
|
sha256sums_x86_64=('SKIP')
|
||||||
sha256sums_aarch64=('SKIP')
|
sha256sums_aarch64=('SKIP')
|
||||||
|
|
||||||
|
|||||||
+60
-13
@@ -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());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -472,16 +482,61 @@ fn CallPanel(room: Memo<String>, me: String) -> impl IntoView {
|
|||||||
|
|
||||||
view! {
|
view! {
|
||||||
<div class="call-panel">
|
<div class="call-panel">
|
||||||
<Show
|
{move || {
|
||||||
when=move || in_call.get()
|
if in_call.get() {
|
||||||
fallback=move || {
|
view! {
|
||||||
|
<CallActive
|
||||||
|
local_video_ref=local_video_ref
|
||||||
|
call_state=call_state.get_value()
|
||||||
|
on_leave=on_leave
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
.into_any()
|
||||||
|
} else {
|
||||||
view! {
|
view! {
|
||||||
<button class="call-join" on:click=on_join>
|
<button class="call-join" on:click=on_join>
|
||||||
"☎ join call"
|
"☎ join call"
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
|
.into_any()
|
||||||
}
|
}
|
||||||
>
|
}}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
.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"))]
|
||||||
|
{
|
||||||
|
let _ = (room, me);
|
||||||
|
view! {
|
||||||
|
<div class="call-panel">
|
||||||
|
<button class="call-join" disabled=true>
|
||||||
|
"☎ join call"
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
.into_any()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The in-call subtree (video grid + leave button), split out of
|
||||||
|
/// `CallPanel` so its `<For>`-over-peers view doesn't get inlined as a type
|
||||||
|
/// parameter of `CallPanel`'s own `if`/`else` branch - that inlining is what
|
||||||
|
/// was blowing the compiler's query recursion limit once mesh calling's
|
||||||
|
/// nested `Show`/`For` landed inside `ChatShell`'s own `Show`.
|
||||||
|
#[cfg(feature = "hydrate")]
|
||||||
|
#[component]
|
||||||
|
fn CallActive(
|
||||||
|
local_video_ref: NodeRef<leptos::html::Video>,
|
||||||
|
call_state: crate::webrtc::CallState,
|
||||||
|
on_leave: impl Fn(leptos::ev::MouseEvent) + 'static,
|
||||||
|
) -> impl IntoView {
|
||||||
|
view! {
|
||||||
<div class="call-active">
|
<div class="call-active">
|
||||||
<div class="video-grid">
|
<div class="video-grid">
|
||||||
<video
|
<video
|
||||||
@@ -492,7 +547,7 @@ fn CallPanel(room: Memo<String>, me: String) -> impl IntoView {
|
|||||||
playsinline=true
|
playsinline=true
|
||||||
></video>
|
></video>
|
||||||
<For
|
<For
|
||||||
each=move || call_state.get_value().peer_streams()
|
each=move || call_state.peer_streams()
|
||||||
key=|(id, _)| id.clone()
|
key=|(id, _)| id.clone()
|
||||||
children=move |(_id, stream)| {
|
children=move |(_id, stream)| {
|
||||||
view! { <PeerVideoTile stream=stream/> }
|
view! { <PeerVideoTile stream=stream/> }
|
||||||
@@ -503,14 +558,6 @@ fn CallPanel(room: Memo<String>, me: String) -> impl IntoView {
|
|||||||
"⏏ leave call"
|
"⏏ leave call"
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[cfg(not(feature = "hydrate"))]
|
|
||||||
{
|
|
||||||
let _ = (room, me);
|
|
||||||
view! { <div class="call-panel"></div> }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+18
-3
@@ -2,7 +2,7 @@
|
|||||||
//! into Postgres, so history survives restarts and includes messages
|
//! into Postgres, so history survives restarts and includes messages
|
||||||
//! published by any client on the bus (not just this app).
|
//! published by any client on the bus (not just this app).
|
||||||
|
|
||||||
use std::time::Duration;
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use async_nats::jetstream;
|
use async_nats::jetstream;
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
@@ -38,11 +38,26 @@ pub async fn init_schema(pool: &PgPool) -> anyhow::Result<()> {
|
|||||||
/// Runs forever; (re)creates the stream/consumer and retries on any failure,
|
/// Runs forever; (re)creates the stream/consumer and retries on any failure,
|
||||||
/// so a NATS or Postgres outage never takes the chat server down.
|
/// so a NATS or Postgres outage never takes the chat server down.
|
||||||
pub async fn run_consumer(nats: async_nats::Client, pool: PgPool) {
|
pub async fn run_consumer(nats: async_nats::Client, pool: PgPool) {
|
||||||
|
const MIN_BACKOFF: Duration = Duration::from_secs(5);
|
||||||
|
const MAX_BACKOFF: Duration = Duration::from_secs(60);
|
||||||
|
let mut backoff = MIN_BACKOFF;
|
||||||
loop {
|
loop {
|
||||||
|
let started = Instant::now();
|
||||||
if let Err(err) = consume(&nats, &pool).await {
|
if let Err(err) = consume(&nats, &pool).await {
|
||||||
tracing::error!("archive consumer failed: {err:#}; retrying in 5s");
|
// A failure after a long healthy run is a fresh incident, not an
|
||||||
|
// escalating one - reset the backoff so we retry promptly.
|
||||||
|
if started.elapsed() >= MAX_BACKOFF {
|
||||||
|
backoff = MIN_BACKOFF;
|
||||||
|
}
|
||||||
|
tracing::error!(
|
||||||
|
"archive consumer failed after {:?}: {err:#}; retrying in {}s",
|
||||||
|
started.elapsed(),
|
||||||
|
backoff.as_secs()
|
||||||
|
);
|
||||||
|
tokio::time::sleep(backoff).await;
|
||||||
|
// Cap the backoff so a persistent outage doesn't hammer NATS/PG.
|
||||||
|
backoff = (backoff * 2).min(MAX_BACKOFF);
|
||||||
}
|
}
|
||||||
tokio::time::sleep(Duration::from_secs(5)).await;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+35
-8
@@ -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 */
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user