Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4bea48959e | ||
|
|
34a3352e21 | ||
|
|
3cc5f0cc6a | ||
|
|
fd93339ad4 | ||
|
|
b1ecb0c46b | ||
|
|
4656f08802 | ||
|
|
a631e90ea2 | ||
|
|
10270b75ca | ||
|
|
7925866868 | ||
|
|
d5cfb663b9 |
+30
-26
@@ -30,8 +30,13 @@ jobs:
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: npm
|
||||
|
||||
# No npm / node_modules cache: the act_runner's internal cache server is
|
||||
# unreachable from job containers (`getCacheEntry failed: connect ETIMEDOUT
|
||||
# 172.17.0.2`), so every cache restore hangs ~5 min and then fails — pure
|
||||
# cost, zero benefit. `cache: npm` was removed from Setup Node above for the
|
||||
# same reason. Re-enable both (setup-node `cache: npm` + an actions/cache
|
||||
# node_modules step) once the runner's cache server is reachable from jobs.
|
||||
- name: Install dependencies
|
||||
# Harden against transient registry network failures (ECONNRESET etc.):
|
||||
# raise npm's built-in fetch retries/timeouts and retry `npm ci` up to
|
||||
@@ -52,37 +57,36 @@ jobs:
|
||||
sleep $((attempt * 15))
|
||||
done
|
||||
|
||||
# ── Critical gate — if this fails, nothing deploys ──────────────────
|
||||
# ── Quality gates run BEFORE the slow build so a format/lint/type/test
|
||||
# error fails in seconds instead of after the ~minutes-long build. All are
|
||||
# hard gates — any failure fails the job and blocks the deploy. The tree is
|
||||
# held clean (prettier formatted, eslint 0 errors, typecheck 0), so these
|
||||
# gate real regressions. NOTE: the lotus-build.sh upstream-merge path can
|
||||
# deploy without CI; a later normal push surfaces any introduced issue here
|
||||
# — fix forward (or briefly re-soften a gate) rather than deploy broken.
|
||||
# eslint gates on errors only (existing no-explicit-any warnings stay
|
||||
# informational — check:eslint has no --max-warnings).
|
||||
- name: Prettier
|
||||
run: npm run check:prettier
|
||||
|
||||
- name: ESLint
|
||||
run: npm run check:eslint
|
||||
|
||||
- name: TypeScript
|
||||
run: npm run typecheck
|
||||
|
||||
# Deterministic pure-logic tests on Node's built-in runner via tsx (no
|
||||
# vitest — Vite 8 is ahead of vitest's range). A failure blocks the deploy.
|
||||
- name: Unit tests
|
||||
run: npm test
|
||||
|
||||
# ── Critical gate — if this fails, nothing deploys. Produces dist/. ──
|
||||
- name: Build
|
||||
run: npm run build
|
||||
env:
|
||||
NODE_OPTIONS: '--max_old_space_size=4096'
|
||||
VITE_APP_VERSION: ${{ github.sha }}
|
||||
|
||||
# Unit tests are a hard gate too — deterministic pure-logic tests on Node's
|
||||
# built-in runner via tsx (no vitest — Vite 8 is ahead of vitest's range).
|
||||
# A failure blocks the deploy.
|
||||
- name: Unit tests
|
||||
run: npm test
|
||||
|
||||
# ── Quality gates (hard — a failure fails the job and blocks deploy) ──
|
||||
# The tree is held clean (typecheck 0, eslint 0 errors, prettier
|
||||
# formatted), so these gate real regressions instead of relying on local
|
||||
# runs. NOTE: an upstream-stable merge (the lotus-build.sh path) could
|
||||
# introduce upstream type/lint/format issues; that path deploys without
|
||||
# CI, but a subsequent normal push would surface the failure here — fix
|
||||
# forward (or briefly re-soften a gate) rather than let it deploy broken.
|
||||
# eslint gates on errors only (existing `no-explicit-any` warnings stay
|
||||
# informational — `check:eslint` has no --max-warnings).
|
||||
- name: TypeScript
|
||||
run: npm run typecheck
|
||||
|
||||
- name: ESLint
|
||||
run: npm run check:eslint
|
||||
|
||||
- name: Prettier
|
||||
run: npm run check:prettier
|
||||
|
||||
# ── Security (informational — findings shouldn't block a deploy) ─────
|
||||
- name: Audit (high/critical)
|
||||
run: npm audit --audit-level=high --omit=dev
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
name: Bug Report
|
||||
about: Report something that isn't working in Lotus Chat
|
||||
title: ''
|
||||
labels: bug
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what went wrong.
|
||||
|
||||
**Steps to reproduce**
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '...'
|
||||
3. See error
|
||||
|
||||
**Expected behavior**
|
||||
What you expected to happen instead.
|
||||
|
||||
**Client info**
|
||||
|
||||
- Lotus Chat version (Settings → Help & About):
|
||||
- Platform: Web / Desktop (Windows / macOS / Linux)
|
||||
- Browser + version (if web):
|
||||
|
||||
**Screenshots / logs**
|
||||
If applicable, add screenshots or the browser devtools console output.
|
||||
@@ -1,5 +1 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Features, Bug Reports, Questions
|
||||
url: https://github.com/cinnyapp/cinny/discussions/new/choose
|
||||
about: Our preferred starting point if you have any questions or suggestions about features or behavior.
|
||||
blank_issues_enabled: true
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
name: Feature Request
|
||||
about: Suggest an idea or improvement for Lotus Chat
|
||||
title: ''
|
||||
labels: enhancement
|
||||
---
|
||||
|
||||
**What would you like?**
|
||||
A clear and concise description of the feature or change.
|
||||
|
||||
**Why / use case**
|
||||
What problem does it solve, or what does it make better?
|
||||
|
||||
**Alternatives considered**
|
||||
Any workarounds or other approaches you've thought about.
|
||||
@@ -1,9 +0,0 @@
|
||||
---
|
||||
name: Pre-Discussed and Approved Topics
|
||||
about: |-
|
||||
Only for topics already discussed and approved in the GitHub Discussions section.
|
||||
---
|
||||
|
||||
**DO NOT OPEN A NEW ISSUE. PLEASE USE THE DISCUSSIONS SECTION.**
|
||||
|
||||
**I DIDN'T READ THE ABOVE LINE. PLEASE CLOSE THIS ISSUE.**
|
||||
+1
-3
@@ -1,3 +1 @@
|
||||
# These are commented until we enable lint and typecheck
|
||||
# npx tsc -p tsconfig.json --noEmit
|
||||
# npx lint-staged
|
||||
npx lint-staged
|
||||
|
||||
+13
-13
@@ -175,19 +175,19 @@ Decorative CSS-only overlays that activate automatically on holidays and events.
|
||||
|
||||
### Themes
|
||||
|
||||
| Theme | Window | Effect |
|
||||
| -------------------- | ------------- | -------------------------------------------------------------------------------------------------- |
|
||||
| 🎆 New Year | Dec 31–Jan 2 | Radial firework bursts in gold, red, cyan, purple; gold shimmer sweep |
|
||||
| 🏮 Lunar New Year | Jan 22–Feb 5 | Floating paper lanterns bobbing; silk texture; gold shimmer accent |
|
||||
| 💖 Valentine's Day | Feb 10–15 | ♥ hearts floating upward; soft pink ambient glow |
|
||||
| 🍀 St. Patrick's Day | Mar 15–18 | ☘ clovers drifting down; gold metallic shimmer top border |
|
||||
| 🃏 April Fool's | Apr 1 | Glitch overlay: RGB channel separation, hue-rotate spikes, scanline sweep, "SIGNAL LOST" watermark |
|
||||
| 🌱 Earth Day | Apr 20–23 | 🌿🍃 leaf emoji drift; sage green ambient tint; vine accent on left edge |
|
||||
| 🍂 Autumn | Sep 21–Oct 31 | Warm orange/amber leaf shapes rotating and falling |
|
||||
| 👾 Arcade Day | Sep 12 | CRT scanlines; blinking pixel corner decorations; "INSERT COIN" prompt |
|
||||
| 🚀 Deep Space Week | Oct 4–10 | Warp-speed star streaks radiating from screen centre; nebula purple/blue ambient |
|
||||
| 🎃 Halloween | Oct 15–Nov 1 | Purple and orange glowing particles; SVG spider web in top-left corner; dark purple tint |
|
||||
| ❄️ Christmas | Dec 10–Jan 2 | White dot snowfall in multiple layers at varied speeds |
|
||||
| Theme | Window | Effect |
|
||||
| -------------------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 🎆 New Year | Dec 31–Jan 2 | Radial firework bursts in gold, red, cyan, purple; gold shimmer sweep |
|
||||
| 🏮 Lunar New Year | Jan 22–Feb 5 | Floating paper lanterns bobbing; silk texture; gold shimmer accent |
|
||||
| 💖 Valentine's Day | Feb 10–15 | ♥ hearts floating upward; soft pink ambient glow |
|
||||
| 🍀 St. Patrick's Day | Mar 15–18 | ☘ clovers drifting down; gold metallic shimmer top border |
|
||||
| 🃏 April Fool's | Apr 1 | Glitch overlay: RGB channel separation, hue-rotate spikes, scanline sweep, "SIGNAL LOST" watermark |
|
||||
| 🌱 Earth Day | Apr 20–23 | 🌿🍃 leaf emoji drift; sage green ambient tint; vine accent on left edge |
|
||||
| 🍂 Autumn | Sep 21–Oct 31 | Warm orange/amber leaf shapes rotating and falling |
|
||||
| 👾 Arcade Day | Sep 12 | Synthwave CRT: neon perspective grid framing the timeline (faded through the chat column), broken horizon line, rolling scanlines, pixel sparkles, bottom-right "1UP / INSERT COIN" HUD |
|
||||
| 🚀 Deep Space Week | Oct 4–10 | Violet void with drifting magenta/cyan nebula clouds, two-depth parallax starfield (~60 twinkling stars + 6 hero gleams), slow galaxy spiral, occasional comet streaks |
|
||||
| 🎃 Halloween | Oct 15–Nov 1 | Purple and orange glowing particles; SVG spider web in top-left corner; dark purple tint |
|
||||
| ❄️ Christmas | Dec 10–Jan 2 | White dot snowfall in multiple layers at varied speeds |
|
||||
|
||||
### Implementation
|
||||
|
||||
|
||||
@@ -129,7 +129,16 @@ Lotus Chat has a desktop app for Windows, macOS, and Linux. It wraps the same we
|
||||
|
||||
### Download
|
||||
|
||||
Download the latest release from the [Releases page on code.lotusguild.org](https://code.lotusguild.org).
|
||||
Operating System | Download
|
||||
---|---
|
||||
Windows | [Get the installer (.exe)](https://code.lotusguild.org/LotusGuild/cinny-desktop/releases/download/latest/LotusChat-x86_64-setup.exe)
|
||||
Linux (AppImage, any distro) | [Get the AppImage](https://code.lotusguild.org/LotusGuild/cinny-desktop/releases/download/latest/LotusChat-x86_64.AppImage)
|
||||
Linux (Debian/Ubuntu) | [Get the .deb](https://code.lotusguild.org/LotusGuild/cinny-desktop/releases/download/latest/LotusChat-x86_64.deb)
|
||||
Linux (Arch/CachyOS/EndeavourOS) | [Get the .pkg.tar.zst](https://code.lotusguild.org/LotusGuild/cinny-desktop/releases/download/latest/LotusChat-x86_64.pkg.tar.zst) — install with `pacman -U LotusChat-x86_64.pkg.tar.zst`
|
||||
|
||||
All Linux builds need `webkit2gtk-4.1` and, for calls to work, GStreamer's `good`/`bad`/`ugly`/`libav` plugin sets (the pacman package pulls these in automatically; on the AppImage/.deb, install them via your package manager if joining a call shows "browser does not support WebRTC").
|
||||
|
||||
See the full [Releases page on code.lotusguild.org](https://code.lotusguild.org/LotusGuild/cinny-desktop/releases) for signatures and older builds.
|
||||
|
||||
### SmartScreen Warning (Windows)
|
||||
|
||||
|
||||
+3
-3
@@ -4,9 +4,9 @@
|
||||
"allowCustomHomeservers": true,
|
||||
"featuredCommunities": {
|
||||
"openAsDefault": false,
|
||||
"spaces": [],
|
||||
"rooms": [],
|
||||
"servers": []
|
||||
"spaces": ["!-1ZBnAH-JiCOV8MGSKN77zDGTuI3pgSdy8Unu_DrDyc", "#homelab:codestorm.net"],
|
||||
"rooms": ["#jellyfin:matrix.org"],
|
||||
"servers": ["matrixrooms.info"]
|
||||
},
|
||||
"hashRouter": {
|
||||
"enabled": false,
|
||||
|
||||
@@ -84,7 +84,15 @@ export function SeasonalPreview({ theme }: { theme: SeasonTheme }) {
|
||||
return (
|
||||
<div
|
||||
aria-hidden="true"
|
||||
style={{ position: 'absolute', inset: 0, overflow: 'hidden', pointerEvents: 'none' }}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
overflow: 'hidden',
|
||||
pointerEvents: 'none',
|
||||
// Size container so overlays can scale/hide fixed-size details (e.g.
|
||||
// Arcade's HUD text) with `cqw` instead of rendering clipped in a swatch.
|
||||
containerType: 'inline-size',
|
||||
}}
|
||||
>
|
||||
{buildOverlayContent(theme, true)}
|
||||
</div>
|
||||
|
||||
@@ -102,12 +102,12 @@ export const animSparkleTwinkle = keyframes({
|
||||
* Opacity + a hair of scale for a CRT bloom feel.
|
||||
*/
|
||||
export const animCoinBlink = keyframes({
|
||||
'0%': { opacity: '0.85', transform: 'translateX(-50%) scale(1)' },
|
||||
'6%': { opacity: '1', transform: 'translateX(-50%) scale(1.015)' },
|
||||
'12%': { opacity: '0.85', transform: 'translateX(-50%) scale(1)' },
|
||||
'49%': { opacity: '0.85', transform: 'translateX(-50%) scale(1)' },
|
||||
'50%': { opacity: '0', transform: 'translateX(-50%) scale(1)' },
|
||||
'100%': { opacity: '0', transform: 'translateX(-50%) scale(1)' },
|
||||
'0%': { opacity: '0.85', transform: 'scale(1)' },
|
||||
'6%': { opacity: '1', transform: 'scale(1.015)' },
|
||||
'12%': { opacity: '0.85', transform: 'scale(1)' },
|
||||
'49%': { opacity: '0.85', transform: 'scale(1)' },
|
||||
'50%': { opacity: '0', transform: 'scale(1)' },
|
||||
'100%': { opacity: '0', transform: 'scale(1)' },
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -46,6 +46,10 @@ const NEON_CYAN = 'oklch(0.80 0.15 200)';
|
||||
const GRID_PURPLE = 'oklch(0.45 0.18 300)';
|
||||
|
||||
// The receding grid as an inline SVG data-URI (CSP-safe, no external assets).
|
||||
// Strokes use vector-effect=non-scaling-stroke so a line is ~1px whether the
|
||||
// tile is stretched across a 2000px plane (preserveAspectRatio=none would
|
||||
// otherwise fatten the verticals ~4x) or squeezed into the 76px settings
|
||||
// swatch (where scaled strokes disappeared entirely).
|
||||
// It is a 1x2 vertical tile of horizontal rule lines + a single set of vertical
|
||||
// lines fanning toward a top-center vanishing point. The plane is then placed
|
||||
// under a CSS `perspective` rotateX so the lines genuinely recede. Scrolling the
|
||||
@@ -58,7 +62,7 @@ function gridDataUri(): string {
|
||||
rows.forEach((y) => {
|
||||
lines.push(
|
||||
`<line x1='0' y1='${y}' x2='600' y2='${y}' stroke='${GRID_PURPLE}' ` +
|
||||
`stroke-width='1.4' stroke-opacity='0.9'/>`,
|
||||
`stroke-width='1.2' stroke-opacity='0.9' vector-effect='non-scaling-stroke'/>`,
|
||||
);
|
||||
});
|
||||
// Vertical lines fanning out from the top-center vanishing point.
|
||||
@@ -67,7 +71,7 @@ function gridDataUri(): string {
|
||||
const botX = 300 + i * 95; // wide at the foreground
|
||||
lines.push(
|
||||
`<line x1='${topX}' y1='0' x2='${botX}' y2='600' stroke='${GRID_PURPLE}' ` +
|
||||
`stroke-width='1.4' stroke-opacity='0.8'/>`,
|
||||
`stroke-width='1.2' stroke-opacity='0.8' vector-effect='non-scaling-stroke'/>`,
|
||||
);
|
||||
}
|
||||
const svg =
|
||||
@@ -105,6 +109,13 @@ const RESTING_SPARKLES: ReadonlyArray<{
|
||||
|
||||
const GRID_URI = gridDataUri();
|
||||
|
||||
// HUD text size: 11px on any real viewport, 0px (invisible) inside anything
|
||||
// narrower than ~330px. `cqw` resolves against the nearest size container —
|
||||
// the settings swatch (`SeasonalPreview` sets container-type) — and falls back
|
||||
// to the viewport width when there is no container, i.e. the full-screen
|
||||
// overlay. clamp(0, 100cqw - 320px, 11px) → 76px swatch: 0px; 1440px app: 11px.
|
||||
const HUD_FONT_SIZE = 'clamp(0px, calc(100cqw - 320px), 11px)';
|
||||
|
||||
export function ArcadeOverlay({ reduced }: SeasonalOverlayProps) {
|
||||
// Deterministic sparkle field, computed ONCE. No per-frame state.
|
||||
const sparkles = useMemo<Sparkle[]>(() => {
|
||||
@@ -134,9 +145,9 @@ export function ArcadeOverlay({ reduced }: SeasonalOverlayProps) {
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
backgroundImage: [
|
||||
'radial-gradient(140% 80% at 50% -8%, oklch(0.65 0.25 350 / 0.16) 0%, transparent 55%)',
|
||||
'radial-gradient(120% 70% at 50% 112%, oklch(0.45 0.18 300 / 0.20) 0%, transparent 60%)',
|
||||
'linear-gradient(180deg, oklch(0.12 0.05 300 / 0.10) 0%, transparent 38%, oklch(0.10 0.06 310 / 0.16) 100%)',
|
||||
'radial-gradient(140% 80% at 50% -8%, oklch(0.65 0.25 350 / 0.12) 0%, transparent 55%)',
|
||||
'radial-gradient(120% 70% at 50% 112%, oklch(0.45 0.18 300 / 0.16) 0%, transparent 60%)',
|
||||
'linear-gradient(180deg, oklch(0.12 0.05 300 / 0.08) 0%, transparent 38%, oklch(0.10 0.06 310 / 0.12) 100%)',
|
||||
].join(','),
|
||||
contain: 'layout paint style',
|
||||
}}
|
||||
@@ -147,41 +158,59 @@ export function ArcadeOverlay({ reduced }: SeasonalOverlayProps) {
|
||||
a vanishing point at the top (the horizon). It lives in the lower
|
||||
half of the screen — the "floor". The inner plane scrolls upward by
|
||||
one tile via transform translateY, which reads as the grid flowing
|
||||
toward the viewer. Pure transform; never background-position. */}
|
||||
toward the viewer. Pure transform; never background-position.
|
||||
|
||||
Two masks are nested (multiple mask-images on one element union by
|
||||
default, and `mask-composite: intersect` isn't universal yet): the
|
||||
outer wrapper fades the lattice through the central column where the
|
||||
message timeline lives, so it frames the chat instead of striping
|
||||
the text; the inner box fades it in from the horizon. */}
|
||||
<div
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: '-25%',
|
||||
right: '-25%',
|
||||
bottom: 0,
|
||||
height: '62%',
|
||||
overflow: 'hidden',
|
||||
perspective: '280px',
|
||||
perspectiveOrigin: '50% 0%',
|
||||
maskImage: 'linear-gradient(180deg, transparent 0%, #000 26%, #000 100%)',
|
||||
WebkitMaskImage: 'linear-gradient(180deg, transparent 0%, #000 26%, #000 100%)',
|
||||
opacity: reduced ? 0.5 : 0.62,
|
||||
inset: 0,
|
||||
maskImage:
|
||||
'linear-gradient(90deg, #000 0%, #000 12%, rgba(0,0,0,0.3) 34%, rgba(0,0,0,0.3) 66%, #000 88%, #000 100%)',
|
||||
WebkitMaskImage:
|
||||
'linear-gradient(90deg, #000 0%, #000 12%, rgba(0,0,0,0.3) 34%, rgba(0,0,0,0.3) 66%, #000 88%, #000 100%)',
|
||||
opacity: reduced ? 0.4 : 0.46,
|
||||
contain: 'layout paint style',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
height: '200%',
|
||||
transformOrigin: 'top center',
|
||||
transform: 'rotateX(74deg)',
|
||||
backgroundImage: GRID_URI,
|
||||
backgroundRepeat: 'repeat-y',
|
||||
backgroundSize: '100% 50%',
|
||||
filter: 'drop-shadow(0 0 3px oklch(0.55 0.22 320 / 0.6))',
|
||||
willChange: reduced ? undefined : 'transform',
|
||||
animation: reduced ? 'none' : `${animGridScroll} 7s linear infinite`,
|
||||
left: '-25%',
|
||||
right: '-25%',
|
||||
bottom: 0,
|
||||
height: '62%',
|
||||
overflow: 'hidden',
|
||||
perspective: '280px',
|
||||
perspectiveOrigin: '50% 0%',
|
||||
maskImage: 'linear-gradient(180deg, transparent 0%, #000 26%, #000 100%)',
|
||||
WebkitMaskImage: 'linear-gradient(180deg, transparent 0%, #000 26%, #000 100%)',
|
||||
contain: 'layout paint style',
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
height: '200%',
|
||||
transformOrigin: 'top center',
|
||||
transform: 'rotateX(74deg)',
|
||||
backgroundImage: GRID_URI,
|
||||
backgroundRepeat: 'repeat-y',
|
||||
backgroundSize: '100% 50%',
|
||||
filter: 'drop-shadow(0 0 2px oklch(0.55 0.22 320 / 0.55))',
|
||||
willChange: reduced ? undefined : 'transform',
|
||||
animation: reduced ? 'none' : `${animGridScroll} 7s linear infinite`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 3. Horizon glow + neon horizon line. A soft synthwave sun-bloom sits
|
||||
@@ -197,7 +226,7 @@ export function ArcadeOverlay({ reduced }: SeasonalOverlayProps) {
|
||||
height: '34%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
backgroundImage:
|
||||
'radial-gradient(60% 100% at 50% 100%, oklch(0.70 0.22 350 / 0.22) 0%, oklch(0.65 0.18 330 / 0.10) 40%, transparent 72%)',
|
||||
'radial-gradient(60% 100% at 50% 100%, oklch(0.70 0.22 350 / 0.16) 0%, oklch(0.65 0.18 330 / 0.08) 40%, transparent 72%)',
|
||||
contain: 'layout paint style',
|
||||
}}
|
||||
/>
|
||||
@@ -209,8 +238,10 @@ export function ArcadeOverlay({ reduced }: SeasonalOverlayProps) {
|
||||
right: '12%',
|
||||
top: '38%',
|
||||
height: '1.5px',
|
||||
background: `linear-gradient(90deg, transparent 0%, ${NEON_CYAN} 25%, oklch(0.92 0.10 320 / 0.95) 50%, ${NEON_CYAN} 75%, transparent 100%)`,
|
||||
opacity: 0.55,
|
||||
// Bright at the flanks, dropped out through the centre column so the
|
||||
// rule frames the timeline rather than underlining a message.
|
||||
background: `linear-gradient(90deg, transparent 0%, ${NEON_CYAN} 14%, oklch(0.92 0.10 320 / 0.95) 22%, transparent 34%, transparent 66%, oklch(0.92 0.10 320 / 0.95) 78%, ${NEON_CYAN} 86%, transparent 100%)`,
|
||||
opacity: 0.4,
|
||||
filter: 'blur(0.4px) drop-shadow(0 0 4px oklch(0.78 0.16 200 / 0.7))',
|
||||
}}
|
||||
/>
|
||||
@@ -273,7 +304,7 @@ export function ArcadeOverlay({ reduced }: SeasonalOverlayProps) {
|
||||
inset: 0,
|
||||
overflow: 'hidden',
|
||||
mixBlendMode: 'multiply',
|
||||
opacity: 0.5,
|
||||
opacity: 0.32,
|
||||
contain: 'layout paint style',
|
||||
}}
|
||||
>
|
||||
@@ -285,7 +316,7 @@ export function ArcadeOverlay({ reduced }: SeasonalOverlayProps) {
|
||||
top: '-8px',
|
||||
bottom: '-8px',
|
||||
backgroundImage:
|
||||
'repeating-linear-gradient(0deg, oklch(0.10 0.04 300 / 0.55) 0px, oklch(0.10 0.04 300 / 0.55) 1px, transparent 1px, transparent 3px)',
|
||||
'repeating-linear-gradient(0deg, oklch(0.10 0.04 300 / 0.45) 0px, oklch(0.10 0.04 300 / 0.45) 1px, transparent 1px, transparent 4px)',
|
||||
willChange: reduced ? undefined : 'transform',
|
||||
animation: reduced ? 'none' : `${animScanRoll} 6s linear infinite`,
|
||||
}}
|
||||
@@ -309,51 +340,54 @@ export function ArcadeOverlay({ reduced }: SeasonalOverlayProps) {
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 7a. Glowing "INSERT COIN" attract-mode blip, low-opacity, bottom-center.
|
||||
Static scene shows it steady (no blink). */}
|
||||
{/* 7. Attract-mode HUD: a tiny SCORE readout over a glowing "INSERT COIN"
|
||||
blip, stacked bottom-right. That corner is the one spot that is
|
||||
clear in every layout (below the members list, or the empty right
|
||||
end of the read-receipt strip) — top-left collided with the space
|
||||
bar and bottom-centre sat on the composer. Static scene shows both
|
||||
steady (no blink). The font-size clamp collapses the text to nothing
|
||||
when the host is narrower than ~330px, so the 76px settings swatch
|
||||
never shows clipped glyphs. */}
|
||||
<div
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: '5%',
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
right: '14px',
|
||||
bottom: '8px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-end',
|
||||
gap: '3px',
|
||||
fontFamily: '"Courier New", monospace',
|
||||
fontSize: '12px',
|
||||
fontSize: HUD_FONT_SIZE,
|
||||
fontWeight: 700,
|
||||
letterSpacing: '0.32em',
|
||||
color: NEON_CYAN,
|
||||
textShadow: '0 0 6px oklch(0.80 0.15 200 / 0.9), 0 0 14px oklch(0.65 0.25 350 / 0.5)',
|
||||
lineHeight: 1,
|
||||
userSelect: 'none',
|
||||
whiteSpace: 'nowrap',
|
||||
opacity: reduced ? 0.6 : undefined,
|
||||
animation: reduced ? 'none' : `${animCoinBlink} 1.6s step-end infinite`,
|
||||
}}
|
||||
>
|
||||
INSERT COIN
|
||||
</div>
|
||||
|
||||
{/* 7b. Corner SCORE HUD glyph — a tiny pixel score that blips, top-left,
|
||||
very low opacity so it reads as ambient chrome, not UI. */}
|
||||
<div
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '2.5%',
|
||||
left: '2%',
|
||||
fontFamily: '"Courier New", monospace',
|
||||
fontSize: '10px',
|
||||
fontWeight: 700,
|
||||
letterSpacing: '0.18em',
|
||||
color: NEON_MAGENTA,
|
||||
textShadow: '0 0 6px oklch(0.65 0.25 350 / 0.8)',
|
||||
userSelect: 'none',
|
||||
whiteSpace: 'nowrap',
|
||||
opacity: reduced ? 0.5 : undefined,
|
||||
animation: reduced ? 'none' : `${animScoreBlip} 2.4s ease-in-out infinite`,
|
||||
}}
|
||||
>
|
||||
1UP 00<span style={{ color: NEON_CYAN }}>0000</span>
|
||||
<div
|
||||
style={{
|
||||
letterSpacing: '0.18em',
|
||||
color: NEON_MAGENTA,
|
||||
textShadow: '0 0 6px oklch(0.65 0.25 350 / 0.8)',
|
||||
opacity: reduced ? 0.5 : undefined,
|
||||
animation: reduced ? 'none' : `${animScoreBlip} 2.4s ease-in-out infinite`,
|
||||
}}
|
||||
>
|
||||
1UP 00<span style={{ color: NEON_CYAN }}>0000</span>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
letterSpacing: '0.32em',
|
||||
color: NEON_CYAN,
|
||||
textShadow: '0 0 6px oklch(0.80 0.15 200 / 0.9), 0 0 14px oklch(0.65 0.25 350 / 0.5)',
|
||||
opacity: reduced ? 0.6 : undefined,
|
||||
animation: reduced ? 'none' : `${animCoinBlink} 1.6s step-end infinite`,
|
||||
}}
|
||||
>
|
||||
INSERT COIN
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 8. CRT vignette + screen-glow. A radial darkening frames the corners,
|
||||
|
||||
@@ -97,8 +97,8 @@ function makeStars(count: number, seedBase: number): Star[] {
|
||||
|
||||
export function DeepSpaceOverlay({ reduced }: SeasonalOverlayProps) {
|
||||
// Two parallax depths. Far = dense + faint, Near = sparser + slightly larger.
|
||||
const farStars = useMemo<Star[]>(() => makeStars(16, 1000), []);
|
||||
const nearStars = useMemo<Star[]>(() => makeStars(12, 2000), []);
|
||||
const farStars = useMemo<Star[]>(() => makeStars(40, 1000), []);
|
||||
const nearStars = useMemo<Star[]>(() => makeStars(22, 2000), []);
|
||||
|
||||
const heroStars = useMemo<HeroStar[]>(
|
||||
() =>
|
||||
@@ -144,7 +144,7 @@ export function DeepSpaceOverlay({ reduced }: SeasonalOverlayProps) {
|
||||
position: 'absolute',
|
||||
inset: '-6%',
|
||||
contain: 'layout paint style',
|
||||
backgroundColor: 'oklch(0.2 0.12 300 / 0.16)',
|
||||
backgroundColor: 'oklch(0.2 0.12 300 / 0.12)',
|
||||
backgroundImage: [
|
||||
'radial-gradient(120% 90% at 50% -8%, oklch(0.28 0.13 295 / 0.2) 0%, transparent 60%)',
|
||||
'radial-gradient(100% 80% at 12% 18%, oklch(0.55 0.2 330 / 0.1) 0%, transparent 55%)',
|
||||
|
||||
@@ -66,6 +66,7 @@ import { useSpaceOptionally } from '../../hooks/useSpace';
|
||||
import {
|
||||
getRoomNotificationModeIcon,
|
||||
RoomNotificationMode,
|
||||
setRoomNotificationPreference,
|
||||
} from '../../hooks/useRoomsNotificationPreferences';
|
||||
import { RoomNotificationModeSwitcher } from '../../components/RoomNotificationSwitcher';
|
||||
import { getRoomCreatorsForRoomId, useRoomCreators } from '../../hooks/useRoomCreators';
|
||||
@@ -298,8 +299,6 @@ export function saveMuteTimers(timers: MuteTimerEntry[]): void {
|
||||
// Reverse a timed mute: restore the room's notification mode to Unset and drop
|
||||
// its persisted timer. Shared by the in-session timer and the boot-time restore.
|
||||
export async function unmuteRoom(mx: MatrixClient, roomId: string): Promise<void> {
|
||||
const { setRoomNotificationPreference } =
|
||||
await import('../../hooks/useRoomsNotificationPreferences');
|
||||
await setRoomNotificationPreference(
|
||||
mx,
|
||||
roomId,
|
||||
@@ -392,8 +391,6 @@ const RoomNavItemMenu = forwardRef<HTMLDivElement, RoomNavItemMenuProps>(
|
||||
|
||||
const handleMuteFor = useCallback(
|
||||
async (durationMs: number | null) => {
|
||||
const { setRoomNotificationPreference } =
|
||||
await import('../../hooks/useRoomsNotificationPreferences');
|
||||
const prevMode = notificationMode ?? RoomNotificationMode.Unset;
|
||||
await setRoomNotificationPreference(
|
||||
mx,
|
||||
|
||||
Reference in New Issue
Block a user