Compare commits

...
65 Commits
Author SHA1 Message Date
ReaperOfVeriod 9167e04e77 Wire Tauri AppImage to native keyring
Client checks / checks (push) Canceled after 0s
Client dependency and security checks / audit (push) Canceled after 0s
2026-10-25 05:47:28 +01:00
ReaperOfVeriod 9854cc4c4f Ignore native build artifacts
Client checks / checks (push) Canceled after 0s
Client dependency and security checks / audit (push) Canceled after 0s
2026-10-25 05:43:49 +01:00
ReaperOfVeriod 761ff5305d Add Linux keyring storage boundary 2026-10-25 05:43:45 +01:00
ReaperOfVeriod d7dad3021f Complete bot command management controls
Client checks / checks (push) Canceled after 0s
Client dependency and security checks / audit (push) Canceled after 0s
2026-10-25 05:41:35 +01:00
ReaperOfVeriod a422cc70f0 Add bot command definition editor 2026-10-25 05:41:19 +01:00
ReaperOfVeriod f986cbb1e9 Add public bot command catalog 2026-10-25 05:39:09 +01:00
ReaperOfVeriod 3d2688c8be keep client reconnecting after outage 2026-10-25 05:38:28 +01:00
ReaperOfVeriod a387ea124a enforce client server compatibility 2026-10-25 05:38:06 +01:00
ReaperOfVeriod 02fcad6a8c add confirmed overlay deletion 2026-10-25 05:37:23 +01:00
ReaperOfVeriod 890c42e62f add overlay variable diagnostics 2026-10-25 05:36:50 +01:00
ReaperOfVeriod d095ddec03 confirm overlay draft discard 2026-10-25 05:36:16 +01:00
ReaperOfVeriod fa7ee4d87f make overlay draft recovery explicit 2026-10-25 05:35:19 +01:00
ReaperOfVeriod 000af8bfb1 connect to server on client startup 2026-10-25 05:34:55 +01:00
ReaperOfVeriod 950db9eb8c cache overlay drafts locally 2026-10-25 05:33:59 +01:00
ReaperOfVeriod 53756caea3 autosave overlay drafts after debounce 2026-10-25 05:33:41 +01:00
ReaperOfVeriod 87ceb8c87f consume server compatibility metadata 2026-10-25 05:32:10 +01:00
ReaperOfVeriod 953dfaaad0 preserve chat replies and threads 2026-10-25 05:30:51 +01:00
ReaperOfVeriod 46fb8e2cc3 add collaborator search controls 2026-10-25 05:28:00 +01:00
ReaperOfVeriod 8b8ba4b618 load collaboration favorites 2026-10-25 05:24:56 +01:00
ReaperOfVeriod 209f2ed7e0 add client issue and release scaffolding 2026-10-25 05:21:26 +01:00
ReaperOfVeriod 9d16dcebd5 run client quality gates in CI 2026-10-25 05:20:59 +01:00
ReaperOfVeriod 7e9f571df1 add backup controls to settings 2026-10-25 05:20:32 +01:00
ReaperOfVeriod 0fa40fc63d add overlay enabled controls 2026-10-25 05:15:14 +01:00
ReaperOfVeriod 5d6af050ad add overlay editor and preview 2026-10-25 05:13:42 +01:00
ReaperOfVeriod c1df6938b5 add bot secret manager view 2026-10-25 05:12:15 +01:00
ReaperOfVeriod 450503afc3 add bot command catalog view 2026-10-25 05:09:25 +01:00
ReaperOfVeriod 85cd8aee00 load shared chat state 2026-10-25 05:06:24 +01:00
ReaperOfVeriod ba84f9d778 show OAuth scope status 2026-10-25 05:05:30 +01:00
ReaperOfVeriod c256ce36e5 add collaboration invite controls 2026-10-25 05:04:11 +01:00
ReaperOfVeriod 1ba93cce8a add Guest Star collaboration panel 2026-10-25 05:02:51 +01:00
ReaperOfVeriod 0b2fb65af3 filter activity feed by category 2026-10-25 05:01:46 +01:00
ReaperOfVeriod 181ad46c29 add chat pin and delete actions 2026-10-25 05:01:32 +01:00
ReaperOfVeriod d20af42053 link chat users to moderation controls 2026-10-25 05:01:11 +01:00
ReaperOfVeriod ba3d3b2d80 render Twitch emote fragments in chat 2026-10-25 05:00:51 +01:00
ReaperOfVeriod 1b7c41ea07 add Twitch style emote autocomplete 2026-10-25 05:00:31 +01:00
ReaperOfVeriod 810589647b add Twitch chat reply controls 2026-10-25 04:59:33 +01:00
ReaperOfVeriod 7f0d3fbb6d render incoming Twitch chat 2026-10-25 04:59:08 +01:00
ReaperOfVeriod c6844fcc47 show server diagnostics in client 2026-10-25 04:55:01 +01:00
ReaperOfVeriod 36b99968b6 add Twitch identity disconnect controls 2026-10-25 04:54:15 +01:00
ReaperOfVeriod aa16fc29d3 add administrator notification bell 2026-10-25 04:53:31 +01:00
ReaperOfVeriod 0c684d4831 feat: add moderation controls 2026-10-25 04:49:52 +01:00
ReaperOfVeriod 2eb050294b feat: add streamer and bot chat sending 2026-10-25 04:48:17 +01:00
ReaperOfVeriod 93d34306bc feat: embed Twitch stream player 2026-10-25 04:47:28 +01:00
ReaperOfVeriod 625084cfa5 feat: add debounced Twitch category picker 2026-10-25 04:47:08 +01:00
ReaperOfVeriod c81748da2c feat: add stream information editor 2026-10-25 04:46:12 +01:00
ReaperOfVeriod f48618f94b feat: render activity history and live events 2026-10-25 04:44:43 +01:00
ReaperOfVeriod 5cb893af62 feat: connect client to realtime WebSocket 2026-10-25 04:42:25 +01:00
ReaperOfVeriod c32582a1cd feat: add client reconnect scheduling 2026-10-25 04:41:28 +01:00
ReaperOfVeriod 05d7947a29 feat: add Twitch account authorization controls 2026-10-25 04:37:08 +01:00
ReaperOfVeriod 8c8d632557 docs: describe client auth development behavior 2026-10-25 04:24:11 +01:00
ReaperOfVeriod 9ea2899ca7 feat: add client administrator authentication gate 2026-10-25 04:24:07 +01:00
ReaperOfVeriod e2076cb044 fix: preserve desktop dashboard panel layout 2026-10-25 04:06:52 +01:00
ReaperOfVeriod b5a6a61e2d style: restore dashboard grid rules 2026-10-25 04:06:10 +01:00
ReaperOfVeriod 71db966697 fix: keep top level dashboard navigation 2026-10-25 04:05:47 +01:00
ReaperOfVeriod 5d41473f47 style: adopt mewlody workspace shell 2026-10-25 04:05:27 +01:00
ReaperOfVeriod 35df7afe88 style: compact empty dashboard states 2026-10-25 04:04:56 +01:00
ReaperOfVeriod 512757ca4a style: remove redundant dashboard heading 2026-10-25 04:04:19 +01:00
ReaperOfVeriod 0003734bed style: compact dashboard heading 2026-10-25 04:04:06 +01:00
ReaperOfVeriod 694bfeabf3 style: follow albedo panel geometry 2026-10-25 04:03:46 +01:00
ReaperOfVeriod fa3915bc5d style: match albedo panel composition 2026-10-25 04:00:22 +01:00
ReaperOfVeriod 998b3d3072 fix: align controls with albedo style rules 2026-10-25 03:59:05 +01:00
ReaperOfVeriod 66fe3e4fea feat: establish albedo dashboard shell 2026-10-25 03:57:56 +01:00
ReaperOfVeriod 5298c6870b feat: add initial client shell
Client checks / checks (push) Canceled after 0s
Client dependency and security checks / audit (push) Canceled after 0s
2026-10-25 03:52:37 +01:00
ReaperOfVeriod 67a8a06268 chore: document branch workflow and security checks
Client checks / checks (push) Canceled after 0s
Client dependency and security checks / audit (push) Canceled after 0s
2026-10-25 03:48:58 +01:00
ReaperOfVeriod 51c4853d51 chore: add MIT license
Client checks / checks (push) Canceled after 0s
2026-10-25 03:45:43 +01:00
44 changed files with 13729 additions and 4 deletions
+17
View File
@@ -0,0 +1,17 @@
name: Bug report
description: Report a reproducible client defect.
title: 'bug: '
labels: [bug]
body:
- type: textarea
id: reproduction
attributes:
label: Reproduction and expected behavior
validations:
required: true
- type: input
id: version
attributes:
label: Client version or commit
validations:
required: true
+18
View File
@@ -0,0 +1,18 @@
name: Feature request
description: Propose a client-side change.
title: 'feature: '
labels: [enhancement]
body:
- type: textarea
id: behavior
attributes:
label: Desired behavior and affected states
validations:
required: true
- type: checkboxes
id: plan
attributes:
label: Planning
options:
- label: I checked PLAN.md and this does not silently change an existing decision.
required: true
+13
View File
@@ -9,4 +9,17 @@ jobs:
runs-on: debian-latest runs-on: debian-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: test -f README.md && test -f AGENTS.md && test -f .prettierrc.json - run: test -f README.md && test -f AGENTS.md && test -f .prettierrc.json
- run: npm ci
- run: npm run format:check
- run: npm run check
- run: npm run build
- uses: actions-rust-lang/setup-rust-toolchain@v1
- working-directory: native
run: cargo fmt -- --check
- working-directory: native
run: cargo test
+17
View File
@@ -0,0 +1,17 @@
name: Client release check
on:
workflow_dispatch:
push:
tags: ['v*.*.*']
jobs:
release-check:
runs-on: debian-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: test -s CHANGELOG.md && npm ci && npm run format:check && npm run check && npm run build
+16
View File
@@ -0,0 +1,16 @@
name: Client dependency and security checks
on:
push:
pull_request:
jobs:
audit:
runs-on: debian-latest
steps:
- uses: actions/checkout@v4
- name: Audit JavaScript dependencies when a lockfile exists
run: |
if test -f package-lock.json; then
npm audit --audit-level=high
fi
+2
View File
@@ -1,4 +1,6 @@
node_modules/ node_modules/
native/target/
src-tauri/target/
dist/ dist/
build/ build/
coverage/ coverage/
-1
View File
@@ -7,4 +7,3 @@ Read the parent `AGENTS.md`, `PLAN.md`, `TODO.md`, and `docs/wiki/README.md` bef
- Use Prettier for Svelte, JavaScript/TypeScript, JSON, YAML, and Markdown. - Use Prettier for Svelte, JavaScript/TypeScript, JSON, YAML, and Markdown.
- Never store passwords, tokens, or secrets in source, settings, fixtures, logs, or artifacts. - Never store passwords, tokens, or secrets in source, settings, fixtures, logs, or artifacts.
- Do not edit the parent `PLAN.md` without explicit user permission. - Do not edit the parent `PLAN.md` without explicit user permission.
+8
View File
@@ -0,0 +1,8 @@
# Changelog
Notable client changes are recorded here using the Keep a Changelog format.
Stable release entries require explicit user authorization.
## [Unreleased]
- Add administrator backup inventory and manual backup controls.
-1
View File
@@ -1,4 +1,3 @@
# Contributing # Contributing
Run formatting, linting, type checks, tests, and the AppImage build before submission. Use deterministic mocked server/API fixtures. Run formatting, linting, type checks, tests, and the AppImage build before submission. Use deterministic mocked server/API fixtures.
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 ReaperOfVeriod
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+12 -1
View File
@@ -2,9 +2,20 @@
This directory is the home of the standalone Linux client repository. This directory is the home of the standalone Linux client repository.
The current development shell connects to the server readiness endpoint and
provides an administrator login gate. The browser/dev client keeps its bearer
session in memory only. The `native/` crate defines the AppImage storage
boundary: `settings.json` contains only the server address and username, while
the session credential is stored in the Linux kernel keyring with no plaintext
fallback.
The client is a Svelte-based control surface distributed as an x86-64 AppImage. It communicates with `trixie-server` through the documented versioned HTTP and authenticated WebSocket interfaces. The client is a Svelte-based control surface distributed as an x86-64 AppImage. It communicates with `trixie-server` through the documented versioned HTTP and authenticated WebSocket interfaces.
Connection recovery follows the server policy: the first retry is immediate,
then retries wait 3, 10, and 15 seconds, followed by an indefinite 30-second
interval until the server reconnects. Manual connection checks do not create
overlapping retry loops.
Implementation has not started. See the parent workspace's `PLAN.md`, `TODO.md`, and `AGENTS.md` before beginning work. Implementation has not started. See the parent workspace's `PLAN.md`, `TODO.md`, and `AGENTS.md` before beginning work.
Consumes the versioned contracts from the sibling `trixie-api` repository. Consumes the versioned contracts from the sibling `trixie-api` repository.
-1
View File
@@ -1,4 +1,3 @@
# Security # Security
Report vulnerabilities privately through the hosting instance. Never include credentials, tokens, or real user data in issues or fixtures. Report vulnerabilities privately through the hosting instance. Never include credentials, tokens, or real user data in issues or fixtures.
+12
View File
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Trixie</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
+284
View File
@@ -0,0 +1,284 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "bitflags"
version = "2.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]]
name = "cfg-if"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
[[package]]
name = "cpufeatures"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
dependencies = [
"libc",
]
[[package]]
name = "crypto-common"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
]
[[package]]
name = "errno"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys",
]
[[package]]
name = "fastrand"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "getrandom"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
dependencies = [
"cfg-if",
"libc",
"r-efi",
]
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "libc"
version = "0.2.191"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a"
[[package]]
name = "linux-raw-sys"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]]
name = "memchr"
version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
[[package]]
name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "proc-macro2"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "rustix"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d"
dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys",
"windows-sys",
]
[[package]]
name = "serde"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.154"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "sha2"
version = "0.10.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "syn"
version = "3.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "tempfile"
version = "3.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
"fastrand",
"getrandom",
"once_cell",
"rustix",
"windows-sys",
]
[[package]]
name = "trixy-native"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
"sha2",
"tempfile",
]
[[package]]
name = "typenum"
version = "1.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
[[package]]
name = "unicode-ident"
version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "zmij"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
+14
View File
@@ -0,0 +1,14 @@
[package]
name = "trixy-native"
version = "0.1.0"
edition = "2021"
license = "MIT"
description = "Native Linux storage boundary for the Trixie client"
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sha2 = "0.10"
[dev-dependencies]
tempfile = "3"
+12
View File
@@ -0,0 +1,12 @@
# Native credential boundary
This crate is the storage boundary used by the future Linux AppImage shell.
`Settings` writes only `server_address` and `username` to the client
`settings.json`. Session credentials are stored under the Linux kernel
keyring service `trixie.server.session`, scoped by server address and
username. `load_session` returns `None` when the keyring is unavailable;
callers must return to credential entry and must not create a plaintext
fallback.
The crate uses `keyctl` with argument arrays, never a shell command. It does
not log or serialize session values.
+146
View File
@@ -0,0 +1,146 @@
//! Native persistence boundary for the Linux client.
//!
//! Settings are deliberately limited to non-secret connection metadata. The
//! session credential is stored as a `user` key in the Linux kernel keyring
//! through the `keyctl` utility. If keyring access fails, callers receive an
//! error and must ask for credentials again; no plaintext fallback is allowed.
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::{
fs, io,
path::{Path, PathBuf},
process::Command,
};
pub const KEYRING_SERVICE: &str = "trixie.server.session";
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Settings {
pub server_address: String,
pub username: String,
}
impl Settings {
pub fn load(path: impl AsRef<Path>) -> io::Result<Self> {
let bytes = fs::read(path)?;
serde_json::from_slice(&bytes)
.map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))
}
pub fn save(&self, path: impl AsRef<Path>) -> io::Result<()> {
let path = path.as_ref();
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
let bytes = serde_json::to_vec_pretty(self)
.map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
let temporary = path.with_extension("json.tmp");
fs::write(&temporary, bytes)?;
fs::rename(temporary, path)
}
}
pub fn default_settings_path() -> PathBuf {
let base = std::env::var_os("XDG_CONFIG_HOME")
.map(PathBuf::from)
.or_else(|| std::env::var_os("HOME").map(|home| PathBuf::from(home).join(".config")))
.unwrap_or_else(|| PathBuf::from("."));
base.join("trixie").join("settings.json")
}
fn key_description(settings: &Settings) -> String {
let mut digest = Sha256::new();
digest.update(settings.server_address.as_bytes());
digest.update([0]);
digest.update(settings.username.as_bytes());
let digest = digest.finalize();
format!("{KEYRING_SERVICE}:{}", hex_encode(&digest))
}
fn hex_encode(bytes: &[u8]) -> String {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut result = String::with_capacity(bytes.len() * 2);
for byte in bytes {
result.push(HEX[(byte >> 4) as usize] as char);
result.push(HEX[(byte & 0x0f) as usize] as char);
}
result
}
fn keyctl(args: &[&str]) -> io::Result<Vec<u8>> {
let output = Command::new("keyctl").args(args).output()?;
if !output.status.success() {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
"Linux kernel keyring access failed",
));
}
Ok(output.stdout)
}
pub fn store_session(settings: &Settings, session: &str) -> io::Result<()> {
if session.is_empty() {
return Err(io::Error::new(io::ErrorKind::InvalidInput, "empty session"));
}
let description = key_description(settings);
keyctl(&["add", "user", &description, session, "@s"]).map(|_| ())
}
pub fn load_session(settings: &Settings) -> io::Result<Option<String>> {
let description = key_description(settings);
let id = match keyctl(&["search", "@s", "user", &description]) {
Ok(value) => String::from_utf8_lossy(&value).trim().to_string(),
Err(_) => return Ok(None),
};
if id.is_empty() {
return Ok(None);
}
let value = keyctl(&["pipe", &id])?;
String::from_utf8(value)
.map(Some)
.map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))
}
pub fn clear_session(settings: &Settings) -> io::Result<()> {
let description = key_description(settings);
let id = String::from_utf8_lossy(&keyctl(&["search", "@s", "user", &description])?)
.trim()
.to_string();
if id.is_empty() {
return Ok(());
}
keyctl(&["unlink", &id, "@s"]).map(|_| ())
}
#[cfg(test)]
mod tests {
use super::*;
fn settings() -> Settings {
Settings {
server_address: "https://trixie.example".into(),
username: "admin".into(),
}
}
#[test]
fn settings_round_trip_contains_no_password_field() {
let directory = tempfile::tempdir().expect("temporary directory");
let path = directory.path().join("settings.json");
settings().save(&path).expect("save settings");
let text = fs::read_to_string(&path).expect("read settings");
assert!(!text.contains("password"));
assert_eq!(Settings::load(path).expect("load settings"), settings());
}
#[test]
fn key_description_is_stable_but_scoped_to_connection_and_user() {
let first = key_description(&settings());
assert_eq!(first, key_description(&settings()));
let mut different = settings();
different.username = "moderator".into();
assert_ne!(first, key_description(&different));
assert!(first.starts_with(KEYRING_SERVICE));
}
}
+1785
View File
File diff suppressed because it is too large Load Diff
+29
View File
@@ -0,0 +1,29 @@
{
"name": "trixy-linux-client",
"private": true,
"version": "0.1.0-dev",
"type": "module",
"scripts": {
"dev": "vite",
"tauri": "tauri",
"tauri:dev": "tauri dev",
"tauri:build": "tauri build",
"build": "vite build",
"check": "svelte-check --tsconfig ./tsconfig.json",
"format": "prettier --write .",
"format:check": "prettier --check ."
},
"dependencies": {
"@tauri-apps/api": "^2.0.0"
},
"devDependencies": {
"@tsconfig/svelte": "^5.0.0",
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"@tauri-apps/cli": "^2.0.0",
"prettier": "^3.5.0",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"typescript": "^5.7.0",
"vite": "^6.0.0"
}
}
+1
View File
@@ -0,0 +1 @@
target/
+4305
View File
File diff suppressed because it is too large Load Diff
+20
View File
@@ -0,0 +1,20 @@
[package]
name = "trixy-linux-client"
version = "0.1.0"
description = "Trixie Linux client"
authors = ["ReaperOfVeriod"]
license = "MIT"
edition = "2021"
[lib]
name = "trixy_linux_client_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2", features = [] }
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tauri = { version = "2", features = [] }
trixy-native = { path = "../native" }
+3
View File
@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
{}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
<rect width="128" height="128" rx="20" fill="#081018"/>
<path d="M64 18 104 64 64 110 24 64Z" fill="none" stroke="#00b9f2" stroke-width="8"/>
<path d="M64 36 88 64 64 92 40 64Z" fill="#182b3a" stroke="#00b9f2" stroke-width="4"/>
</svg>

After

Width:  |  Height:  |  Size: 305 B

+65
View File
@@ -0,0 +1,65 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use std::path::PathBuf;
use trixy_native::{clear_session, default_settings_path, load_session, store_session, Settings};
fn error_message(error: std::io::Error) -> String {
error.to_string()
}
#[tauri::command]
fn load_client_settings() -> Result<Settings, String> {
let path = default_settings_path();
if !path.exists() {
return Ok(Settings {
server_address: "http://127.0.0.1:8787".to_string(),
username: String::new(),
});
}
Settings::load(path).map_err(error_message)
}
#[tauri::command]
fn save_client_settings(settings: Settings) -> Result<(), String> {
if settings.server_address.trim().is_empty() || settings.username.trim().is_empty() {
return Err("Server address and username are required.".to_string());
}
Settings::save(&settings, default_settings_path()).map_err(error_message)
}
#[tauri::command]
fn load_server_session(settings: Settings) -> Result<Option<String>, String> {
load_session(&settings).map_err(error_message)
}
#[tauri::command]
fn store_server_session(settings: Settings, session: String) -> Result<(), String> {
store_session(&settings, &session).map_err(error_message)
}
#[tauri::command]
fn clear_server_session(settings: Settings) -> Result<(), String> {
clear_session(&settings).map_err(error_message)
}
#[tauri::command]
fn client_settings_path() -> String {
PathBuf::from(default_settings_path())
.to_string_lossy()
.into_owned()
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![
load_client_settings,
save_client_settings,
load_server_session,
store_server_session,
clear_server_session,
client_settings_path
])
.run(tauri::generate_context!())
.expect("error while running Trixie client");
}
+3
View File
@@ -0,0 +1,3 @@
fn main() {
trixy_linux_client_lib::run();
}
+35
View File
@@ -0,0 +1,35 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Trixie",
"version": "0.1.0",
"identifier": "nl.reaperofveriod.trixie",
"build": {
"beforeDevCommand": "npm run dev -- --host 127.0.0.1",
"devUrl": "http://127.0.0.1:5173",
"beforeBuildCommand": "npm run build",
"frontendDist": "../dist"
},
"app": {
"windows": [
{
"title": "Trixie",
"width": 1366,
"height": 768,
"minWidth": 1024,
"minHeight": 640,
"resizable": true
}
],
"security": {
"csp": "default-src 'self' http://127.0.0.1:8787 ws://127.0.0.1:8787 https://*.twitch.tv https://*.twitchcdn.net https://cdn.7tv.app; connect-src 'self' http: https: ws: wss:; img-src 'self' data: https:; frame-src https://player.twitch.tv; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'"
}
},
"bundle": {
"active": true,
"icon": ["icons/icon.png", "icons/icon.svg"],
"targets": ["appimage"],
"linux": {
"deb": { "depends": [] }
}
}
}
+1253
View File
File diff suppressed because it is too large Load Diff
+9
View File
@@ -0,0 +1,9 @@
import './styles.css';
import App from './App.svelte';
import { mount } from 'svelte';
const app = mount(App, {
target: document.getElementById('app')!,
});
export default app;
+963
View File
@@ -0,0 +1,963 @@
@font-face {
font-family: 'Rajdhani';
src: url('/fonts/Rajdhani-Regular.ttf') format('truetype');
font-weight: 400;
}
@font-face {
font-family: 'Rajdhani';
src: url('/fonts/Rajdhani-SemiBold.ttf') format('truetype');
font-weight: 600;
}
@font-face {
font-family: 'Rajdhani';
src: url('/fonts/Rajdhani-Bold.ttf') format('truetype');
font-weight: 700;
}
@font-face {
font-family: 'Inter';
src: url('/fonts/Inter-Variable.ttf') format('truetype');
}
@font-face {
font-family: 'JetBrains Mono';
src: url('/fonts/JetBrainsMono-Variable.ttf') format('truetype');
}
:root {
--ed-accent: #00b3ff;
--ed-accent2: #ff7f00;
--ed-bg: #06080c;
--ed-panel: #0d1117;
--ed-edge: #1d2630;
--ed-fg: #dde6ec;
--ed-dim: #7d8b96;
--ed-success: #35d073;
--ed-warn: #ffb400;
--ed-danger: #ff5436;
--ed-info: #00b3ff;
--ed-exo: #a3ff5e;
--font-display: 'Rajdhani', 'Inter', sans-serif;
--font-data: 'JetBrains Mono', monospace;
--font-body: 'Inter', sans-serif;
--panel-radius: 2px;
--ui-line: color-mix(in srgb, var(--ed-accent) 28%, var(--ed-edge));
--ui-line-soft: color-mix(in srgb, var(--ed-accent) 16%, var(--ed-edge));
--ui-line-focus: color-mix(in srgb, var(--ed-accent) 62%, var(--ed-edge));
--ui-surface: color-mix(in srgb, var(--ed-accent) 8%, var(--ed-bg));
--ui-surface-hi: color-mix(in srgb, var(--ed-accent) 14%, var(--ed-bg));
--ui-divider: color-mix(in srgb, var(--ed-fg) 7%, transparent);
}
* {
box-sizing: border-box;
}
html,
body {
margin: 0;
min-height: 100%;
background: var(--ed-bg);
color: var(--ed-fg);
font-family: var(--font-body);
font-size: 13px;
}
button,
input {
font: inherit;
color: inherit;
}
button,
input,
select,
textarea {
border: 1px solid var(--ui-line);
border-radius: var(--panel-radius);
outline: none;
background: var(--ed-bg);
color: var(--ed-fg);
font-family: var(--font-data);
font-size: 11px;
padding: 4px 6px;
color-scheme: dark;
}
button {
cursor: pointer;
}
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible {
border-color: var(--ui-line-focus);
}
button:hover:not(:disabled) {
border-color: var(--ui-line-focus);
background: var(--ui-surface-hi);
}
.app-shell {
min-height: 100vh;
background: var(--ed-panel);
}
.app-bar {
height: 42px;
display: flex;
align-items: center;
gap: 28px;
padding: 0 16px;
border-bottom: 1px solid var(--ui-divider);
background: var(--ed-bg);
}
.brand {
display: flex;
align-items: center;
gap: 9px;
min-width: 188px;
color: var(--ed-accent);
font: 600 15px var(--font-data);
letter-spacing: 0.22em;
}
.brand-mark {
color: var(--ed-accent);
font-size: 16px;
}
.top-nav {
display: flex;
align-self: stretch;
gap: 24px;
}
.nav-item {
position: relative;
padding: 0;
border: 0;
border-radius: 0;
color: var(--ed-dim);
background: transparent;
font: 10px var(--font-data);
letter-spacing: 0.16em;
text-transform: uppercase;
}
.nav-item:hover,
.nav-item.active {
color: var(--ed-fg);
background: transparent;
}
.nav-item.active::after {
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 2px;
background: var(--ed-accent);
content: '';
}
.app-actions {
display: flex;
align-items: center;
gap: 10px;
margin-left: auto;
}
.connection-state,
.badge {
padding: 3px 7px;
border: 1px solid var(--ed-warn);
color: var(--ed-warn);
font: 10px var(--font-data);
letter-spacing: 0.04em;
}
.connection-state.online {
border-color: var(--ed-success);
color: var(--ed-success);
}
.connection-notice {
color: var(--ed-dim);
font: 9px var(--font-data);
}
.icon-button,
.secondary {
border-color: transparent;
background: transparent;
color: var(--ed-dim);
font-size: 18px;
}
.notification-wrap {
position: relative;
}
.notification-button {
position: relative;
}
.notification-dot {
position: absolute;
top: 2px;
right: 2px;
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--ed-warn);
}
.notification-popover {
position: absolute;
z-index: 5;
top: 34px;
right: 0;
width: 320px;
max-height: 420px;
overflow: auto;
border: 1px solid var(--ui-line);
background: var(--ed-panel);
box-shadow: 0 8px 24px rgb(0 0 0 / 35%);
}
.notification-heading,
.notification-title {
display: flex;
align-items: center;
justify-content: space-between;
}
.notification-heading {
padding: 9px 10px;
border-bottom: 1px solid var(--ui-divider);
color: var(--ed-fg);
font: 600 11px var(--font-data);
letter-spacing: 0.12em;
}
.notification-item {
padding: 10px;
border-bottom: 1px solid var(--ui-divider);
opacity: 0.65;
}
.notification-item.unseen {
opacity: 1;
background: var(--ui-surface);
}
.notification-title {
color: var(--ed-fg);
font-weight: 600;
}
.notification-message {
margin: 6px 0 9px;
color: var(--ed-dim);
font-size: 11px;
line-height: 1.4;
}
.notification-empty {
padding: 18px 10px;
color: var(--ed-dim);
font: 11px var(--font-data);
}
.chat-message-list {
display: flex;
flex: 1;
flex-direction: column;
gap: 6px;
overflow: auto;
padding: 10px;
}
.chat-message {
display: flex;
gap: 7px;
font-size: 11px;
line-height: 1.35;
}
.chat-user {
border: 0;
padding: 0;
background: transparent;
color: var(--ed-accent);
font-family: var(--font-data);
font-weight: 600;
}
.chat-text {
color: var(--ed-fg);
overflow-wrap: anywhere;
}
.chat-emote {
width: 28px;
height: 28px;
margin: 0 2px;
vertical-align: middle;
object-fit: contain;
}
.chat-reply {
margin-left: auto;
padding: 2px 4px;
border-color: transparent;
color: var(--ed-dim);
font: 9px var(--font-data);
}
.chat-actions {
display: flex;
margin-left: auto;
gap: 3px;
opacity: 0;
}
.chat-message:hover .chat-actions,
.chat-message:focus-within .chat-actions {
opacity: 1;
}
.chat-reply.danger {
color: var(--ed-danger);
}
.message-pinned {
background: var(--ui-surface);
}
.pinned-messages {
padding: 7px 10px;
border-bottom: 1px solid var(--ui-divider);
background: var(--ui-surface);
}
.pinned-message {
display: flex;
gap: 7px;
margin-top: 5px;
overflow: hidden;
color: var(--ed-fg);
font-size: 11px;
text-overflow: ellipsis;
white-space: nowrap;
}
.chat-reply-context {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 6px;
padding: 4px 6px;
border-left: 2px solid var(--ed-accent);
color: var(--ed-accent);
font: 10px var(--font-data);
}
.chat-emote-picker {
display: flex;
max-height: 180px;
flex-direction: column;
overflow: auto;
margin-bottom: 6px;
border: 1px solid var(--ui-line);
background: var(--ed-bg);
}
.chat-emote-option {
display: grid;
grid-template-columns: 28px 1fr auto;
align-items: center;
gap: 8px;
width: 100%;
border: 0;
border-bottom: 1px solid var(--ui-divider);
text-align: left;
}
.chat-emote-option img {
width: 28px;
height: 28px;
object-fit: contain;
}
.workspace {
display: grid;
grid-template-columns: 214px 1fr;
min-height: calc(100vh - 42px);
border-top: 1px solid var(--ui-divider);
}
.side-panel {
border-right: 1px solid var(--ui-divider);
background: color-mix(in srgb, var(--ed-panel) 80%, var(--ed-bg));
}
.panel-header {
display: flex;
align-items: center;
justify-content: space-between;
min-height: 34px;
padding: 8px 12px;
border-bottom: 1px solid var(--ui-divider);
color: var(--ed-fg);
font: 600 13px var(--font-display);
letter-spacing: 0.16em;
text-transform: uppercase;
}
.panel-kicker,
.micro {
color: var(--ed-dim);
font: 10px var(--font-data);
letter-spacing: 0.08em;
}
.server-card,
.side-section {
padding: 12px 10px;
border-bottom: 1px solid var(--ui-divider);
}
.server-status {
display: flex;
align-items: center;
gap: 7px;
margin: 13px 0 18px;
font: 11px var(--font-data);
}
.status-dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--ed-danger);
}
.status-dot.good {
background: var(--ed-success);
}
label {
display: block;
margin-bottom: 5px;
}
.control {
width: 100%;
padding: 7px 8px;
font-size: 11px;
}
.control-button {
width: 100%;
margin-top: 8px;
}
.auth-card {
display: block;
}
.auth-card label {
margin-top: 9px;
}
.auth-error {
margin-top: 8px;
color: var(--ed-warn);
font: 10px/1.4 var(--font-data);
}
.auth-gate {
min-height: 300px;
}
.auth-gate-body {
display: grid;
justify-items: center;
align-content: start;
gap: 8px;
max-width: 360px;
margin: 0 auto;
padding: 48px 16px;
color: var(--ed-dim);
text-align: center;
}
.auth-gate-body strong {
color: var(--ed-fg);
font: 600 16px var(--font-display);
}
.credential-form {
justify-items: stretch;
text-align: left;
}
.credential-form strong {
text-align: center;
}
.primary {
padding: 7px 10px;
border-color: var(--ui-line-focus);
background: var(--ui-surface);
color: var(--ed-fg);
font-size: 11px;
font-weight: 600;
}
.primary:hover {
background: var(--ui-surface-hi);
}
.empty-row,
.mono {
margin-top: 8px;
color: var(--ed-dim);
font-size: 11px;
}
.oauth-actions {
display: grid;
gap: 6px;
margin-top: 9px;
}
.oauth-status-grid {
display: grid;
gap: 6px;
margin-top: 10px;
padding-top: 9px;
border-top: 1px solid var(--ui-divider);
}
.oauth-status-row {
display: flex;
align-items: center;
justify-content: space-between;
}
.oauth-status-row .good {
color: var(--ed-green);
}
.secondary-action {
width: 100%;
padding: 6px;
color: var(--ed-dim);
font-size: 10px;
}
.oauth-link-card {
display: grid;
gap: 6px;
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid var(--ui-divider);
}
.oauth-link {
width: 100%;
min-height: 72px;
resize: vertical;
overflow-wrap: anywhere;
color: var(--ed-dim);
font-size: 9px;
}
.oauth-hint {
color: var(--ed-dim);
font: 9px/1.4 var(--font-data);
}
.mono {
font-family: var(--font-data);
}
.side-footer {
margin-top: auto;
border-bottom: 0;
}
.bot-page {
min-height: 100%;
}
.bot-page-body {
padding: 18px;
}
.bot-command-list {
margin-top: 12px;
border-top: 1px solid var(--ui-divider);
}
.bot-command-row {
display: flex;
justify-content: space-between;
gap: 20px;
padding: 13px 4px;
border-bottom: 1px solid var(--ui-divider);
}
.bot-command-row strong {
display: block;
color: var(--ed-cyan);
font-family: var(--font-data);
}
.bot-command-description {
display: block;
margin-top: 4px;
color: var(--ed-dim);
}
.bot-command-meta {
display: flex;
gap: 10px;
align-items: flex-start;
color: var(--ed-dim);
font: 10px var(--font-data);
white-space: nowrap;
}
.bot-secret-section {
margin-top: 24px;
padding-top: 18px;
border-top: 1px solid var(--ui-divider);
}
.bot-secret-note {
margin-top: 5px;
color: var(--ed-dim);
font-size: 11px;
}
.secret-form {
display: grid;
grid-template-columns: 1fr 1fr 1fr auto;
gap: 7px;
margin-top: 12px;
}
.bot-secret-list {
margin-top: 12px;
border-top: 1px solid var(--ui-divider);
}
.bot-secret-row {
display: flex;
justify-content: space-between;
gap: 16px;
padding: 10px 4px;
border-bottom: 1px solid var(--ui-divider);
}
.bot-secret-row strong,
.bot-secret-row span {
display: block;
}
.overlay-page {
min-height: 100%;
}
.overlay-toolbar,
.overlay-controls {
display: flex;
gap: 7px;
}
.overlay-layout {
display: grid;
grid-template-columns: 220px 1fr;
min-height: calc(100vh - 100px);
}
.overlay-list {
border-right: 1px solid var(--ui-divider);
padding: 10px;
}
.overlay-list-item {
display: grid;
width: 100%;
gap: 4px;
padding: 10px;
color: var(--ed-dim);
text-align: left;
}
.overlay-list-item.active,
.overlay-list-item:hover {
color: var(--ed-cyan);
background: var(--ui-surface-hi);
}
.overlay-list-item span {
font: 9px var(--font-data);
}
.overlay-editor {
display: grid;
align-content: start;
gap: 10px;
padding: 14px;
}
.overlay-fields,
.overlay-tabs {
display: grid;
gap: 7px;
}
.overlay-code {
min-height: 110px;
resize: vertical;
color: var(--ed-dim);
background: var(--ui-surface-low);
font: 12px/1.4 var(--font-data);
}
.overlay-preview-label {
margin-top: 4px;
}
.overlay-preview {
width: 100%;
min-height: 180px;
border: 1px solid var(--ui-divider);
background: #101217;
}
.bot-secret-row span {
margin-top: 3px;
color: var(--ed-dim);
font-size: 11px;
}
.version-label {
display: block;
margin-top: 13px;
}
.content-area {
min-width: 0;
padding: 8px;
}
.dashboard-grid {
display: grid;
grid-template-columns: 1.3fr 1fr 1fr;
grid-template-rows: minmax(0, 1fr) 168px;
gap: 0;
min-height: calc(100vh - 96px);
border-top: 1px solid var(--ed-edge);
border-left: 1px solid var(--ed-edge);
}
.panel {
min-width: 0;
border: 0;
border-right: 1px solid var(--ed-edge);
border-bottom: 1px solid var(--ed-edge);
border-radius: 0;
box-shadow: none;
background: var(--ed-panel);
}
.preview-panel {
grid-column: span 2;
}
.chat-panel {
grid-row: span 2;
}
.preview-stage {
display: grid;
place-content: center;
min-height: 220px;
border-bottom: 1px solid var(--ui-divider);
background: color-mix(in srgb, var(--ed-accent) 3%, var(--ed-bg));
text-align: center;
}
.twitch-player {
display: block;
width: 100%;
min-height: 260px;
flex: 1;
border: 0;
background: #000;
}
.offline-mark {
color: var(--ed-fg);
font: 700 24px var(--font-display);
letter-spacing: 0.18em;
}
.offline-subtitle {
margin-top: 5px;
color: var(--ed-dim);
font: 10px var(--font-data);
letter-spacing: 0.1em;
}
.panel-footer {
display: flex;
justify-content: space-between;
padding: 9px 10px;
}
.panel-tabs {
display: flex;
gap: 14px;
padding: 9px 10px;
border-bottom: 1px solid var(--ui-divider);
}
.tab {
border: 0;
border-radius: 0;
background: transparent;
padding: 0 0 5px;
color: var(--ed-dim);
font: 10px var(--font-data);
}
.tab.active {
border-bottom: 2px solid var(--ed-accent);
color: var(--ed-accent);
}
.empty-state,
.chat-empty {
display: grid;
align-content: start;
justify-items: center;
gap: 6px;
min-height: 0;
padding: 18px 12px;
color: var(--ed-dim);
text-align: center;
}
.empty-state strong,
.chat-empty strong {
color: var(--ed-fg);
font: 600 14px var(--font-display);
}
.empty-state span,
.chat-empty span {
max-width: 260px;
line-height: 1.5;
}
.empty-glyph {
color: var(--ed-accent);
font-size: 28px;
}
.activity-list {
height: calc(100% - 70px);
overflow: auto;
}
.activity-row {
display: flex;
justify-content: space-between;
gap: 8px;
padding: 9px 12px;
border-bottom: 1px solid var(--ui-divider);
font: 10px var(--font-data);
}
.activity-type {
color: var(--ed-fg);
}
.activity-time {
color: var(--ed-dim);
}
.chat-composer {
display: grid;
gap: 6px;
padding: 8px;
border-top: 1px solid var(--ui-divider);
}
.chat-identity-toggle,
.chat-send-row {
display: flex;
gap: 5px;
}
.chat-identity {
flex: 1;
color: var(--ed-dim);
font-size: 9px;
}
.chat-identity.active {
border-color: var(--ui-line-focus);
background: var(--ui-surface-hi);
color: var(--ed-accent);
}
.chat-send-row .control {
min-width: 0;
}
.chat-composer .control {
min-width: 0;
}
.chat-composer .primary {
opacity: 0.5;
}
.quick-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 6px;
padding: 10px;
}
.quick-action {
min-height: 52px;
border: 1px solid var(--ui-line-soft);
background: var(--ui-surface);
color: var(--ed-dim);
font: 10px var(--font-data);
}
.stream-editor {
display: grid;
gap: 6px;
padding: 10px;
border-top: 1px solid var(--ui-divider);
}
.stream-editor label {
margin-top: 4px;
}
.category-options {
display: grid;
max-height: 180px;
overflow: auto;
border: 1px solid var(--ui-line);
background: var(--ed-bg);
}
.category-option {
display: flex;
justify-content: space-between;
gap: 8px;
border: 0;
border-bottom: 1px solid var(--ui-divider);
border-radius: 0;
text-align: left;
}
.quick-action:hover,
.quick-action.accent {
border-color: var(--ui-line-focus);
color: var(--ed-fg);
background: var(--ui-surface-hi);
}
@media (max-width: 760px) {
.workspace {
grid-template-columns: 170px 1fr;
}
.dashboard-grid {
grid-template-columns: 1fr 1fr;
grid-template-rows: auto auto auto;
}
.preview-panel {
grid-column: span 2;
}
.chat-panel {
grid-row: span 1;
grid-column: span 2;
}
}
@media (max-width: 640px) {
.top-nav {
display: none;
}
.workspace {
display: block;
}
.side-panel {
display: none;
}
.content-area {
padding: 10px;
}
.dashboard-grid {
display: block;
}
.panel {
margin-bottom: 10px;
}
.chat-panel {
min-height: 280px;
}
}
.collaboration-search-panel {
margin-top: 10px;
}
.collaborator-results {
display: grid;
gap: 4px;
margin: 8px 0 14px;
}
.collaborator-result {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 6px 8px;
border: 1px solid var(--ui-divider);
background: var(--ui-surface);
}
.collaborator-result .secondary {
min-width: 28px;
padding: 2px 6px;
}
.chat-thread-panel,
.thread-index-panel {
margin-top: 10px;
}
.thread-index-row,
.thread-reply {
display: flex;
justify-content: space-between;
gap: 12px;
width: 100%;
padding: 8px;
border: 0;
border-bottom: 1px solid var(--ui-divider);
background: transparent;
color: var(--ed-fg);
text-align: left;
}
.thread-index-row:hover {
background: var(--ui-surface-hi);
}
.thread-root {
display: flex;
gap: 8px;
padding: 10px;
border-bottom: 1px solid var(--ui-line-focus);
}
.thread-replies {
margin: 6px 0 10px 18px;
border-left: 2px solid var(--ui-line-focus);
}
.overlay-diagnostics-panel {
margin-top: 10px;
}
.overlay-danger-panel {
margin-top: 10px;
}
.danger {
border: 1px solid var(--ed-danger);
background: transparent;
color: var(--ed-danger);
padding: 7px 12px;
font: inherit;
}
.danger:hover {
background: color-mix(in srgb, var(--ed-danger) 16%, transparent);
}
.diagnostics-toggle {
border: 0;
background: transparent;
color: var(--ui-error, #ff6b7a);
font: inherit;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.diagnostics-details {
padding: 10px;
color: var(--ui-error, #ff6b7a);
font-family: var(--font-mono);
font-size: 0.8rem;
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+5
View File
@@ -0,0 +1,5 @@
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
export default {
preprocess: vitePreprocess(),
};
+16
View File
@@ -0,0 +1,16 @@
{
"extends": "@tsconfig/svelte/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler"
},
"include": ["src/**/*.ts", "src/**/*.svelte", "vite.config.ts"],
"exclude": ["dist", "node_modules"]
}
+6
View File
@@ -0,0 +1,6 @@
import { svelte } from '@sveltejs/vite-plugin-svelte';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [svelte()],
});