mirror of
https://github.com/lone-cloud/prism
synced 2026-06-04 04:04:44 -07:00
update to latest signal-cli, UI audit improvements"
This commit is contained in:
parent
88aeff8539
commit
c9cb3a7289
19 changed files with 411 additions and 190 deletions
20
.github/copilot-instructions.md
vendored
20
.github/copilot-instructions.md
vendored
|
|
@ -35,3 +35,23 @@ if time.Since(l.generatedAt) < l.ttl {
|
||||||
- Prefer composition over inheritance
|
- Prefer composition over inheritance
|
||||||
- Handle errors properly, don't ignore them
|
- Handle errors properly, don't ignore them
|
||||||
|
|
||||||
|
## Design Context
|
||||||
|
|
||||||
|
### Users
|
||||||
|
Self-hosters — technically capable people who run their own infrastructure. They visit this UI occasionally to configure integrations (Signal, Telegram, WebPush, Proton Mail), register apps, and verify things are wired up. Could be on any device, but desktop is most common. Setup is a one-time or rare task; the UI is not used daily.
|
||||||
|
|
||||||
|
### Brand Personality
|
||||||
|
Sharp. Minimal. No-nonsense. This is a tool for people who run servers, not a product trying to sell them something. The interface should feel like it respects their time — no filler, no decorative chrome, no marketing energy.
|
||||||
|
|
||||||
|
### Aesthetic Direction
|
||||||
|
Refined utilitarian. Light theme by default, dark by system preference (keep existing both-modes behavior). The palette should feel clean and precise, not sterile. The cyan accent (`#56c3de`) is a good starting point for the brand hue — it reads as calm and technical without being loud. Neutrals should be very subtly tinted toward it.
|
||||||
|
|
||||||
|
Anti-reference: PHPMyAdmin / boring CRUD admin panel. The goal is something that wouldn't look out of place as a polished open-source project UI — functional, cohesive, with quiet confidence.
|
||||||
|
|
||||||
|
### Design Principles
|
||||||
|
1. **Earn every element** — if it doesn't serve a function, remove it. No decorative chrome.
|
||||||
|
2. **Precision over personality** — tight spacing, clear hierarchy, no ambiguity about what's interactive.
|
||||||
|
3. **Trust through clarity** — statuses should be instantly readable; nothing should make the user wonder "did that work?"
|
||||||
|
4. **Quiet confidence** — not boring, not flashy. Typography and spacing do the work, not color.
|
||||||
|
5. **Polish the structure, don't replace it** — the existing `<details>`-card pattern and HTMX architecture should be respected. Improve the CSS layer, don't overhaul templates.
|
||||||
|
|
||||||
|
|
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -6,3 +6,5 @@ prism-*
|
||||||
tmp
|
tmp
|
||||||
.VSCodeCounter/
|
.VSCodeCounter/
|
||||||
.image-digests
|
.image-digests
|
||||||
|
.github/skills/
|
||||||
|
.impeccable.md
|
||||||
|
|
|
||||||
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
||||||
1.1.3
|
1.2.0
|
||||||
7
go.mod
7
go.mod
|
|
@ -9,6 +9,8 @@ require (
|
||||||
github.com/go-chi/httprate v0.15.0
|
github.com/go-chi/httprate v0.15.0
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/mymmrac/telego v1.8.0
|
github.com/mymmrac/telego v1.8.0
|
||||||
|
github.com/yeqown/go-qrcode/v2 v2.2.5
|
||||||
|
github.com/yeqown/go-qrcode/writer/standard v1.3.0
|
||||||
golang.org/x/crypto v0.50.0
|
golang.org/x/crypto v0.50.0
|
||||||
modernc.org/sqlite v1.48.2
|
modernc.org/sqlite v1.48.2
|
||||||
)
|
)
|
||||||
|
|
@ -23,20 +25,25 @@ require (
|
||||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/emersion/go-bcrypt v0.0.0-20170822072041-6e724a1baa63 // indirect
|
github.com/emersion/go-bcrypt v0.0.0-20170822072041-6e724a1baa63 // indirect
|
||||||
|
github.com/fogleman/gg v1.3.0 // indirect
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
|
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
|
||||||
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/grbit/go-json v0.11.0 // indirect
|
github.com/grbit/go-json v0.11.0 // indirect
|
||||||
github.com/klauspost/compress v1.18.4 // indirect
|
github.com/klauspost/compress v1.18.4 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||||
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
github.com/valyala/fasthttp v1.69.0 // indirect
|
github.com/valyala/fasthttp v1.69.0 // indirect
|
||||||
github.com/valyala/fastjson v1.6.10 // indirect
|
github.com/valyala/fastjson v1.6.10 // indirect
|
||||||
|
github.com/yeqown/reedsolomon v1.0.0 // indirect
|
||||||
github.com/zeebo/xxh3 v1.1.0 // indirect
|
github.com/zeebo/xxh3 v1.1.0 // indirect
|
||||||
golang.org/x/arch v0.24.0 // indirect
|
golang.org/x/arch v0.24.0 // indirect
|
||||||
|
golang.org/x/image v0.10.0 // indirect
|
||||||
golang.org/x/sys v0.43.0 // indirect
|
golang.org/x/sys v0.43.0 // indirect
|
||||||
modernc.org/libc v1.70.0 // indirect
|
modernc.org/libc v1.70.0 // indirect
|
||||||
modernc.org/mathutil v1.7.1 // indirect
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
|
|
|
||||||
15
go.sum
15
go.sum
|
|
@ -23,6 +23,8 @@ github.com/emersion/go-bcrypt v0.0.0-20170822072041-6e724a1baa63 h1:7aCSuwTBzg7B
|
||||||
github.com/emersion/go-bcrypt v0.0.0-20170822072041-6e724a1baa63/go.mod h1:eRwwJnuLVFtYTC+AI2JDJTMcuQUTYhBIK4I6bC5tpqw=
|
github.com/emersion/go-bcrypt v0.0.0-20170822072041-6e724a1baa63/go.mod h1:eRwwJnuLVFtYTC+AI2JDJTMcuQUTYhBIK4I6bC5tpqw=
|
||||||
github.com/emersion/hydroxide v0.2.31 h1:ofPKtEpD+AtE2oKJhcQKhUjubQS8+AHIjCuO3aeLBsM=
|
github.com/emersion/hydroxide v0.2.31 h1:ofPKtEpD+AtE2oKJhcQKhUjubQS8+AHIjCuO3aeLBsM=
|
||||||
github.com/emersion/hydroxide v0.2.31/go.mod h1:jhoMVyP0z2GACrmFkL0ppcWKt2LbC02auADSSsB4vH4=
|
github.com/emersion/hydroxide v0.2.31/go.mod h1:jhoMVyP0z2GACrmFkL0ppcWKt2LbC02auADSSsB4vH4=
|
||||||
|
github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8=
|
||||||
|
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
|
||||||
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
|
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
|
||||||
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
|
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
|
||||||
github.com/go-chi/httprate v0.15.0 h1:j54xcWV9KGmPf/X4H32/aTH+wBlrvxL7P+SdnRqxh5g=
|
github.com/go-chi/httprate v0.15.0 h1:j54xcWV9KGmPf/X4H32/aTH+wBlrvxL7P+SdnRqxh5g=
|
||||||
|
|
@ -30,6 +32,8 @@ github.com/go-chi/httprate v0.15.0/go.mod h1:rzGHhVrsBn3IMLYDOZQsSU4fJNWcjui4fWK
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||||
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
||||||
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||||
|
|
@ -51,6 +55,8 @@ github.com/mymmrac/telego v1.8.0 h1:EvIprWo9Cn0MHgumvvqNXPAXO1yJj3pu2cdCCeDxbow=
|
||||||
github.com/mymmrac/telego v1.8.0/go.mod h1:pdLV346EgVuq7Xrh3kMggeBiazeHhsdEoK0RTEOPXRM=
|
github.com/mymmrac/telego v1.8.0/go.mod h1:pdLV346EgVuq7Xrh3kMggeBiazeHhsdEoK0RTEOPXRM=
|
||||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
|
|
@ -75,6 +81,12 @@ github.com/valyala/fastjson v1.6.10 h1:/yjJg8jaVQdYR3arGxPE2X5z89xrlhS0eGXdv+ADT
|
||||||
github.com/valyala/fastjson v1.6.10/go.mod h1:e6FubmQouUNP73jtMLmcbxS6ydWIpOfhz34TSfO3JaE=
|
github.com/valyala/fastjson v1.6.10/go.mod h1:e6FubmQouUNP73jtMLmcbxS6ydWIpOfhz34TSfO3JaE=
|
||||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||||
|
github.com/yeqown/go-qrcode/v2 v2.2.5 h1:HCOe2bSjkhZyYoyyNaXNzh4DJZll6inVJQQw+8228Zk=
|
||||||
|
github.com/yeqown/go-qrcode/v2 v2.2.5/go.mod h1:uHpt9CM0V1HeXLz+Wg5MN50/sI/fQhfkZlOM+cOTHxw=
|
||||||
|
github.com/yeqown/go-qrcode/writer/standard v1.3.0 h1:chdyhEfRtUPgQtuPeaWVGQ/TQx4rE1PqeoW3U+53t34=
|
||||||
|
github.com/yeqown/go-qrcode/writer/standard v1.3.0/go.mod h1:O4MbzsotGCvy8upYPCR91j81dr5XLT7heuljcNXW+oQ=
|
||||||
|
github.com/yeqown/reedsolomon v1.0.0 h1:x1h/Ej/uJnNu8jaX7GLHBWmZKCAWjEJTetkqaabr4B0=
|
||||||
|
github.com/yeqown/reedsolomon v1.0.0/go.mod h1:P76zpcn2TCuL0ul1Fso373qHRc69LKwAw/Iy6g1WiiM=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
|
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
|
||||||
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
|
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
|
||||||
|
|
@ -92,6 +104,8 @@ golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v
|
||||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||||
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
||||||
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
||||||
|
golang.org/x/image v0.10.0 h1:gXjUUtwtx5yOE0VKWq1CH4IJAClq4UGgUA3i+rpON9M=
|
||||||
|
golang.org/x/image v0.10.0/go.mod h1:jtrku+n79PfroUbvDdeUWMAI+heR786BofxrbiSF+J0=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
|
@ -144,6 +158,7 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
|
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
|
|
||||||
306
public/index.css
306
public/index.css
|
|
@ -1,39 +1,28 @@
|
||||||
/* Variables */
|
/* Variables */
|
||||||
:root {
|
:root {
|
||||||
--bg-primary: #f5f5f5;
|
color-scheme: light dark;
|
||||||
--bg-secondary: #fdfdfd;
|
--bg-primary: light-dark(#f4f7f8, #141a1d);
|
||||||
--text-primary: #000;
|
--bg-secondary: light-dark(#fafcfd, #1e262a);
|
||||||
--text-secondary: #666;
|
--text-primary: light-dark(#0f1517, #dce9ed);
|
||||||
|
--text-secondary: light-dark(#566870, #8eacb5);
|
||||||
--text-on-color: #fff;
|
--text-on-color: #fff;
|
||||||
--border-color: rgba(0, 0, 0, 0.1);
|
--border-color: light-dark(rgba(0, 0, 0, 0.1), rgba(255, 255, 255, 0.1));
|
||||||
--accent: #56c3de;
|
--accent: light-dark(#0b7589, #7dd4e8);
|
||||||
--success: #06d6a0;
|
--accent-hover: light-dark(#086b7c, #5ec8e0);
|
||||||
--error: #ef476f;
|
--text-on-accent: light-dark(#fff, #0a2a33);
|
||||||
--spinner-track: #e0e0e0;
|
--success: #077a5d;
|
||||||
|
--success-hover: #065e48;
|
||||||
|
--error: #c01746;
|
||||||
|
--error-hover: #9e1239;
|
||||||
|
--spinner-track: light-dark(#dae5e9, #2e3e44);
|
||||||
|
--shadow-tooltip: light-dark(rgba(0, 0, 0, 0.25), rgba(0, 0, 0, 0.5));
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
:root[data-theme="light"] {
|
||||||
:root:not([data-theme="light"]) {
|
color-scheme: light;
|
||||||
--bg-primary: #1a1a1a;
|
|
||||||
--bg-secondary: #2d2d2d;
|
|
||||||
--text-primary: #e0e0e0;
|
|
||||||
--text-secondary: #a0a0a0;
|
|
||||||
--text-on-color: #fff;
|
|
||||||
--border-color: rgba(255, 255, 255, 0.1);
|
|
||||||
--accent: #56c3de;
|
|
||||||
--spinner-track: #4a4a4a;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:root[data-theme="dark"] {
|
:root[data-theme="dark"] {
|
||||||
--bg-primary: #1a1a1a;
|
color-scheme: dark;
|
||||||
--bg-secondary: #2d2d2d;
|
|
||||||
--text-primary: #e0e0e0;
|
|
||||||
--text-secondary: #a0a0a0;
|
|
||||||
--text-on-color: #fff;
|
|
||||||
--border-color: rgba(255, 255, 255, 0.1);
|
|
||||||
--accent: #56c3de;
|
|
||||||
--spinner-track: #4a4a4a;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Reset */
|
/* Reset */
|
||||||
|
|
@ -81,9 +70,8 @@ h6 {
|
||||||
|
|
||||||
/* Base */
|
/* Base */
|
||||||
body {
|
body {
|
||||||
font-family: system-ui;
|
font-family:
|
||||||
line-height: 1.5;
|
ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
max-width: 50rem;
|
max-width: 50rem;
|
||||||
margin: 1rem auto;
|
margin: 1rem auto;
|
||||||
padding: 0 1.25rem 1.25rem;
|
padding: 0 1.25rem 1.25rem;
|
||||||
|
|
@ -101,21 +89,23 @@ a {
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover {
|
a:hover {
|
||||||
opacity: 0.8;
|
color: var(--accent-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Components */
|
/* Components */
|
||||||
|
|
||||||
/* Cards */
|
/* Cards */
|
||||||
.card {
|
.card,
|
||||||
|
.integration-card {
|
||||||
background: var(--bg-secondary);
|
background: var(--bg-secondary);
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1.25rem;
|
||||||
box-shadow: 0 0.125rem 0.25rem var(--border-color);
|
box-shadow: 0 0.125rem 0.25rem var(--border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-header {
|
.card-header,
|
||||||
|
.integration-header {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 1.1em;
|
font-size: 1.1em;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -127,11 +117,13 @@ a:hover {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-header::-webkit-details-marker {
|
.card-header::-webkit-details-marker,
|
||||||
|
.integration-header::-webkit-details-marker {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-header::before {
|
.card-header::before,
|
||||||
|
.integration-header::before {
|
||||||
content: "▶";
|
content: "▶";
|
||||||
font-size: 0.7em;
|
font-size: 0.7em;
|
||||||
margin-right: 0.5rem;
|
margin-right: 0.5rem;
|
||||||
|
|
@ -139,14 +131,39 @@ a:hover {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
details[open] > .card-header::before {
|
details[open] > .card-header::before,
|
||||||
|
details[open] > .integration-header::before {
|
||||||
transform: rotate(90deg);
|
transform: rotate(90deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-content {
|
.card-content,
|
||||||
|
.integration-content {
|
||||||
padding: 0 1.25rem 1.25rem 1.25rem;
|
padding: 0 1.25rem 1.25rem 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Page identity */
|
||||||
|
.site-header {
|
||||||
|
padding-bottom: 1.25rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-wordmark {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header h2,
|
||||||
|
.integration-header h2 {
|
||||||
|
margin-bottom: 0;
|
||||||
|
font-size: 1.2em;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
/* Tooltips */
|
/* Tooltips */
|
||||||
.integration-status:has(.tooltip) {
|
.integration-status:has(.tooltip) {
|
||||||
cursor: help;
|
cursor: help;
|
||||||
|
|
@ -171,7 +188,7 @@ details[open] > .card-header::before {
|
||||||
transition: opacity 0.2s;
|
transition: opacity 0.2s;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.3);
|
box-shadow: 0 0.25rem 0.5rem var(--shadow-tooltip);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip::after {
|
.tooltip::after {
|
||||||
|
|
@ -184,7 +201,8 @@ details[open] > .card-header::before {
|
||||||
border-top-color: var(--bg-secondary);
|
border-top-color: var(--bg-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
:hover > .tooltip {
|
:hover > .tooltip,
|
||||||
|
:focus-within > .tooltip {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
@ -202,6 +220,10 @@ details[open] > .card-header::before {
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
transition: opacity 0.2s;
|
transition: opacity 0.2s;
|
||||||
|
min-width: 2.75rem;
|
||||||
|
min-height: 2.75rem;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-toggle:hover {
|
.theme-toggle:hover {
|
||||||
|
|
@ -212,8 +234,6 @@ details[open] > .card-header::before {
|
||||||
.app-list {
|
.app-list {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
max-height: 18.75rem;
|
|
||||||
overflow: visible;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-item {
|
.app-item {
|
||||||
|
|
@ -238,8 +258,14 @@ details[open] > .card-header::before {
|
||||||
font-size: 1.2em;
|
font-size: 1.2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.app-subscriptions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
.channel-badge {
|
.channel-badge {
|
||||||
padding: 0.15rem 0.5rem;
|
padding: 0.375rem 0.625rem;
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
font-size: 0.85em;
|
font-size: 0.85em;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
|
@ -251,15 +277,20 @@ details[open] > .card-header::before {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
min-width: 5rem;
|
min-width: 5rem;
|
||||||
|
min-height: 2.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge-active {
|
.badge-active {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: filter 0.2s ease;
|
transition: background-color 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge-active:hover {
|
.badge-active:hover {
|
||||||
filter: brightness(0.9);
|
background-color: var(--accent-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-webpush.badge-active:hover {
|
||||||
|
background-color: var(--success-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge-active.htmx-request {
|
.badge-active.htmx-request {
|
||||||
|
|
@ -271,11 +302,11 @@ details[open] > .card-header::before {
|
||||||
.badge-inactive {
|
.badge-inactive {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border: 0.0625rem dashed currentColor;
|
border: 0.0625rem dashed currentColor;
|
||||||
transition: filter 0.2s ease;
|
transition: opacity 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge-inactive:hover {
|
.badge-inactive:hover {
|
||||||
filter: brightness(1.2);
|
opacity: 0.75;
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge-inactive.htmx-request {
|
.badge-inactive.htmx-request {
|
||||||
|
|
@ -290,7 +321,7 @@ details[open] > .card-header::before {
|
||||||
|
|
||||||
.channel-signal.badge-active {
|
.channel-signal.badge-active {
|
||||||
background: var(--accent);
|
background: var(--accent);
|
||||||
color: var(--text-on-color);
|
color: var(--text-on-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.channel-signal.badge-inactive {
|
.channel-signal.badge-inactive {
|
||||||
|
|
@ -300,7 +331,7 @@ details[open] > .card-header::before {
|
||||||
|
|
||||||
.channel-signal.badge-subscribed {
|
.channel-signal.badge-subscribed {
|
||||||
background: var(--accent);
|
background: var(--accent);
|
||||||
color: var(--text-on-color);
|
color: var(--text-on-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.channel-webpush.badge-active {
|
.channel-webpush.badge-active {
|
||||||
|
|
@ -310,12 +341,12 @@ details[open] > .card-header::before {
|
||||||
|
|
||||||
.channel-webpush.badge-subscribed {
|
.channel-webpush.badge-subscribed {
|
||||||
background: var(--accent);
|
background: var(--accent);
|
||||||
color: var(--text-on-color);
|
color: var(--text-on-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.channel-telegram.badge-active {
|
.channel-telegram.badge-active {
|
||||||
background: var(--accent);
|
background: var(--accent);
|
||||||
color: var(--text-on-color);
|
color: var(--text-on-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.channel-telegram.badge-inactive {
|
.channel-telegram.badge-inactive {
|
||||||
|
|
@ -325,7 +356,7 @@ details[open] > .card-header::before {
|
||||||
|
|
||||||
.channel-telegram.badge-subscribed {
|
.channel-telegram.badge-subscribed {
|
||||||
background: var(--accent);
|
background: var(--accent);
|
||||||
color: var(--text-on-color);
|
color: var(--text-on-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-actions {
|
.app-actions {
|
||||||
|
|
@ -341,12 +372,12 @@ details[open] > .card-header::before {
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: filter 0.2s ease;
|
transition: background-color 0.2s ease;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-delete:hover {
|
.btn-delete:hover {
|
||||||
filter: brightness(0.85);
|
background-color: var(--error-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-delete-sub {
|
.btn-delete-sub {
|
||||||
|
|
@ -356,10 +387,13 @@ details[open] > .card-header::before {
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 0 0.25rem;
|
padding: 0.375rem 0.5rem;
|
||||||
|
min-width: 2.75rem;
|
||||||
|
min-height: 2.75rem;
|
||||||
margin-left: 0.25rem;
|
margin-left: 0.25rem;
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
transition: opacity 0.2s ease;
|
transition: opacity 0.2s ease;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-delete-sub:hover {
|
.btn-delete-sub:hover {
|
||||||
|
|
@ -390,54 +424,10 @@ details[open] > .card-header::before {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Integrations */
|
/* Integrations */
|
||||||
.integration-card {
|
|
||||||
background: var(--bg-secondary);
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
margin-bottom: 1.25rem;
|
|
||||||
box-shadow: 0 0.125rem 0.25rem var(--border-color);
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.integration-container {
|
.integration-container {
|
||||||
margin-bottom: 1.25rem;
|
margin-bottom: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.integration-header {
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 1.1em;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.75rem;
|
|
||||||
padding: 1.25rem;
|
|
||||||
cursor: pointer;
|
|
||||||
list-style: none;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.integration-name {
|
|
||||||
font-size: 1.2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.integration-header::-webkit-details-marker {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.integration-header::before {
|
|
||||||
content: "▶";
|
|
||||||
font-size: 0.7em;
|
|
||||||
margin-right: 0.5rem;
|
|
||||||
transition: transform 0.2s;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
details[open] > .integration-header::before {
|
|
||||||
transform: rotate(90deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.integration-content {
|
|
||||||
padding: 0 1.25rem 1.25rem 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.integration-status {
|
.integration-status {
|
||||||
padding: 0.25rem 0.625rem;
|
padding: 0.25rem 0.625rem;
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
|
|
@ -472,12 +462,14 @@ details[open] > .integration-header::before {
|
||||||
.channel-not-configured {
|
.channel-not-configured {
|
||||||
background: var(--error);
|
background: var(--error);
|
||||||
color: var(--text-on-color);
|
color: var(--text-on-color);
|
||||||
|
padding: 0.25rem 0.625rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Forms */
|
/* Forms */
|
||||||
.auth-form {
|
.auth-form {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
max-width: 400px;
|
max-width: 25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group {
|
.form-group {
|
||||||
|
|
@ -496,7 +488,7 @@ details[open] > .integration-header::before {
|
||||||
.form-group input[type="password"] {
|
.form-group input[type="password"] {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
border: 1px solid var(--border-color);
|
border: 0.0625rem solid var(--border-color);
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
background: var(--bg-secondary);
|
background: var(--bg-secondary);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
|
|
@ -523,8 +515,10 @@ details[open] > .integration-header::before {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
display: flex;
|
display: grid;
|
||||||
align-items: center;
|
place-items: center;
|
||||||
|
min-width: 2.75rem;
|
||||||
|
min-height: 2.75rem;
|
||||||
transition: color 0.2s;
|
transition: color 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -551,13 +545,19 @@ details[open] > .integration-header::before {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
transition: filter 0.2s ease;
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background-color: var(--accent-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary:hover,
|
|
||||||
.btn-secondary:hover,
|
|
||||||
.btn-danger:hover {
|
.btn-danger:hover {
|
||||||
filter: brightness(0.85);
|
background-color: var(--error-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary:disabled,
|
.btn-primary:disabled,
|
||||||
|
|
@ -569,7 +569,7 @@ details[open] > .integration-header::before {
|
||||||
|
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
background: var(--accent);
|
background: var(--accent);
|
||||||
color: var(--text-on-color);
|
color: var(--text-on-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-secondary {
|
.btn-secondary {
|
||||||
|
|
@ -607,7 +607,7 @@ details[open] > .integration-header::before {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
background: var(--accent);
|
background: var(--accent);
|
||||||
color: var(--text-on-color);
|
color: var(--text-on-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* QR Code */
|
/* QR Code */
|
||||||
|
|
@ -634,7 +634,7 @@ details[open] > .integration-header::before {
|
||||||
/* Toast Notifications */
|
/* Toast Notifications */
|
||||||
#toast-container {
|
#toast-container {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 1.5rem;
|
bottom: 4rem;
|
||||||
right: 1.5rem;
|
right: 1.5rem;
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -649,7 +649,7 @@ details[open] > .integration-header::before {
|
||||||
padding: 0.875rem 1.25rem;
|
padding: 0.875rem 1.25rem;
|
||||||
border-radius: 0.375rem;
|
border-radius: 0.375rem;
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 0.25rem 0.5rem rgba(0, 0, 0, 0.1),
|
0 0.25rem 0.5rem var(--shadow-tooltip),
|
||||||
0 0 0 0.0625rem var(--border-color);
|
0 0 0 0.0625rem var(--border-color);
|
||||||
min-width: 15rem;
|
min-width: 15rem;
|
||||||
max-width: 25rem;
|
max-width: 25rem;
|
||||||
|
|
@ -672,7 +672,7 @@ details[open] > .integration-header::before {
|
||||||
|
|
||||||
.toast.info {
|
.toast.info {
|
||||||
background: var(--accent);
|
background: var(--accent);
|
||||||
color: var(--text-on-color);
|
color: var(--text-on-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.toast.hiding {
|
.toast.hiding {
|
||||||
|
|
@ -700,3 +700,81 @@ details[open] > .integration-header::before {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Inline confirm widget */
|
||||||
|
.confirm-inline {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.375rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-message {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm {
|
||||||
|
padding: 0.25rem 0.625rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
min-height: 2.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
:focus-visible {
|
||||||
|
outline: 2px solid var(--accent);
|
||||||
|
outline-offset: 2px;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sr-only {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
padding: 0;
|
||||||
|
margin: -1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
white-space: nowrap;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skip-nav {
|
||||||
|
position: absolute;
|
||||||
|
top: -100%;
|
||||||
|
left: 1rem;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
color: var(--accent);
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
z-index: 10000;
|
||||||
|
transition: top 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skip-nav:focus {
|
||||||
|
top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#signal-qr-code:not([src]),
|
||||||
|
#signal-qr-code[src=""] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 30rem) {
|
||||||
|
.theme-toggle {
|
||||||
|
bottom: 0.75rem;
|
||||||
|
right: 0.75rem;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#toast-container {
|
||||||
|
right: 1rem;
|
||||||
|
left: 1rem;
|
||||||
|
bottom: 3.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast {
|
||||||
|
min-width: 0;
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,36 +4,43 @@
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>Prism</title>
|
<title>Prism</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<meta name="theme-color" content="#56C3DE">
|
<meta name="theme-color" content="#0b7589" media="(prefers-color-scheme: light)">
|
||||||
|
<meta name="theme-color" content="#7dd4e8" media="(prefers-color-scheme: dark)">
|
||||||
<link rel="icon" type="image/webp" href="/favicon.webp">
|
<link rel="icon" type="image/webp" href="/favicon.webp">
|
||||||
<link rel="manifest" href="/manifest.json">
|
<link rel="manifest" href="/manifest.json">
|
||||||
<link rel="stylesheet" href="/index.css?v={{.Version}}">
|
<link rel="stylesheet" href="/index.css?v={{.Version}}">
|
||||||
<script src="/htmx.min.js"></script>
|
<script src="/theme-init.js?v={{.Version}}"></script>
|
||||||
<script src="/theme.js?v={{.Version}}"></script>
|
<script src="/htmx.min.js" defer></script>
|
||||||
<script src="/toast.js?v={{.Version}}"></script>
|
<script src="/theme.js?v={{.Version}}" defer></script>
|
||||||
<script src="/integration.js?v={{.Version}}"></script>
|
<script src="/toast.js?v={{.Version}}" defer></script>
|
||||||
|
<script src="/integration.js?v={{.Version}}" defer></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<details class="card" open>
|
<a href="#main-content" class="skip-nav">Skip to content</a>
|
||||||
<summary class="card-header">
|
<main id="main-content">
|
||||||
<span class="integration-name">Registered Apps</span>
|
<h1 class="sr-only">Prism</h1>
|
||||||
</summary>
|
<details class="card" open>
|
||||||
<div id="apps-list" class="card-content"
|
<summary class="card-header">
|
||||||
hx-get="/fragment/apps"
|
<h2>Registered Apps</h2>
|
||||||
hx-trigger="load, reload">
|
</summary>
|
||||||
<div class="loading">
|
<div id="apps-list" class="card-content"
|
||||||
<div class="spinner"></div>
|
hx-get="/fragment/apps"
|
||||||
|
hx-trigger="load, reload">
|
||||||
|
<div class="loading" role="status">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
<span class="sr-only">Loading…</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<div id="integrations"
|
||||||
|
hx-get="/fragment/integrations"
|
||||||
|
hx-trigger="load, reload">
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</main>
|
||||||
|
|
||||||
<div id="integrations"
|
<div id="toast-container" role="status" aria-live="polite" aria-atomic="false"></div>
|
||||||
hx-get="/fragment/integrations"
|
|
||||||
hx-trigger="load, reload">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="toast-container"></div>
|
<button type="button" id="theme-toggle" class="theme-toggle" aria-label="Toggle theme"></button>
|
||||||
|
|
||||||
<button type="button" id="theme-toggle" class="theme-toggle"></button>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,36 @@
|
||||||
|
function showConfirm(btn, message, onConfirm) {
|
||||||
|
const wrapper = document.createElement('span');
|
||||||
|
wrapper.className = 'confirm-inline';
|
||||||
|
wrapper.setAttribute('role', 'group');
|
||||||
|
wrapper.setAttribute('aria-label', message);
|
||||||
|
|
||||||
|
const msg = document.createElement('span');
|
||||||
|
msg.className = 'confirm-message';
|
||||||
|
msg.textContent = message;
|
||||||
|
|
||||||
|
const yes = document.createElement('button');
|
||||||
|
yes.type = 'button';
|
||||||
|
yes.className = 'btn-danger btn-sm';
|
||||||
|
yes.textContent = 'Yes';
|
||||||
|
|
||||||
|
const cancel = document.createElement('button');
|
||||||
|
cancel.type = 'button';
|
||||||
|
cancel.className = 'btn-secondary btn-sm';
|
||||||
|
cancel.textContent = 'Cancel';
|
||||||
|
|
||||||
|
yes.addEventListener('click', () => {
|
||||||
|
wrapper.replaceWith(btn);
|
||||||
|
onConfirm();
|
||||||
|
});
|
||||||
|
cancel.addEventListener('click', () => {
|
||||||
|
delete btn.dataset.confirming;
|
||||||
|
wrapper.replaceWith(btn);
|
||||||
|
});
|
||||||
|
|
||||||
|
wrapper.append(msg, '\u00a0', yes, '\u00a0', cancel);
|
||||||
|
btn.replaceWith(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
document.addEventListener('submit', (e) => {
|
document.addEventListener('submit', (e) => {
|
||||||
const form = e.target;
|
const form = e.target;
|
||||||
|
|
@ -126,11 +159,38 @@ async function submitProtonAuth(e) {
|
||||||
|
|
||||||
let signalLinkingPoll = null;
|
let signalLinkingPoll = null;
|
||||||
|
|
||||||
|
document.addEventListener('htmx:beforeSwap', (e) => {
|
||||||
|
if (!signalLinkingPoll) return;
|
||||||
|
const t = e.detail.target;
|
||||||
|
if (t && (t.id === 'integrations' || t.id === 'signal-integration')) {
|
||||||
|
clearInterval(signalLinkingPoll);
|
||||||
|
signalLinkingPoll = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('htmx:confirm', (e) => {
|
||||||
|
if (!e.detail.question) return;
|
||||||
|
if (e.detail.elt.dataset.confirming) return;
|
||||||
|
e.preventDefault();
|
||||||
|
e.detail.elt.dataset.confirming = '1';
|
||||||
|
showConfirm(e.detail.elt, e.detail.question, () => {
|
||||||
|
delete e.detail.elt.dataset.confirming;
|
||||||
|
e.detail.issueRequest(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
async function linkSignal(btn) {
|
async function linkSignal(btn) {
|
||||||
|
if (signalLinkingPoll) {
|
||||||
|
clearInterval(signalLinkingPoll);
|
||||||
|
signalLinkingPoll = null;
|
||||||
|
}
|
||||||
const qrContainer = document.getElementById('signal-qr-container');
|
const qrContainer = document.getElementById('signal-qr-container');
|
||||||
const qrCode = document.getElementById('signal-qr-code');
|
const qrCode = document.getElementById('signal-qr-code');
|
||||||
const showQrError = (msg) => {
|
const showQrError = (msg) => {
|
||||||
qrContainer.innerHTML = `<p class="channel-not-configured">Error: ${msg}</p>`;
|
const p = document.createElement('p');
|
||||||
|
p.className = 'channel-not-configured';
|
||||||
|
p.textContent = `Error: ${msg}`;
|
||||||
|
qrContainer.replaceChildren(p);
|
||||||
qrContainer.style.display = 'block';
|
qrContainer.style.display = 'block';
|
||||||
btn.style.display = 'inline-block';
|
btn.style.display = 'inline-block';
|
||||||
};
|
};
|
||||||
|
|
@ -152,8 +212,7 @@ async function linkSignal(btn) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
const qrUrl = `https://api.qrserver.com/v1/create-qr-code/?size=400x400&data=${encodeURIComponent(data.qr_code)}`;
|
qrCode.src = data.qr_code;
|
||||||
qrCode.src = qrUrl;
|
|
||||||
qrContainer.style.display = 'block';
|
qrContainer.style.display = 'block';
|
||||||
|
|
||||||
let statusCheckInProgress = false;
|
let statusCheckInProgress = false;
|
||||||
|
|
@ -167,8 +226,10 @@ async function linkSignal(btn) {
|
||||||
|
|
||||||
if (statusData.linked) {
|
if (statusData.linked) {
|
||||||
clearInterval(signalLinkingPoll);
|
clearInterval(signalLinkingPoll);
|
||||||
qrContainer.innerHTML =
|
const linked = document.createElement('p');
|
||||||
'<p class="auth-status success">Linked! Refreshing...</p>';
|
linked.className = 'auth-status success';
|
||||||
|
linked.textContent = 'Linked! Refreshing...';
|
||||||
|
qrContainer.replaceChildren(linked);
|
||||||
showToast('Signal linked', 'success');
|
showToast('Signal linked', 'success');
|
||||||
setTimeout(() => reloadIntegrations(), 1000);
|
setTimeout(() => reloadIntegrations(), 1000);
|
||||||
}
|
}
|
||||||
|
|
@ -184,45 +245,37 @@ async function linkSignal(btn) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteTelegram(btn) {
|
async function deleteTelegram(btn) {
|
||||||
if (!confirm('Unlink Telegram integration?')) return;
|
showConfirm(btn, 'Unlink Telegram?', async () => {
|
||||||
|
try {
|
||||||
btn.disabled = true;
|
const response = await fetch('/api/v1/telegram/link', {
|
||||||
|
method: 'DELETE',
|
||||||
try {
|
});
|
||||||
const response = await fetch('/api/v1/telegram/link', { method: 'DELETE' });
|
if (response.ok) {
|
||||||
|
checkToast(response);
|
||||||
if (response.ok) {
|
reloadIntegrations();
|
||||||
checkToast(response);
|
} else {
|
||||||
reloadIntegrations();
|
const error = await response.json();
|
||||||
} else {
|
showToast(error.error || 'Failed to unlink Telegram', 'error');
|
||||||
const error = await response.json();
|
}
|
||||||
alert(`Error: ${error.error || 'Failed to unlink integration'}`);
|
} catch (err) {
|
||||||
btn.disabled = false;
|
showToast(err.message, 'error');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
});
|
||||||
alert(`Error: ${err.message}`);
|
|
||||||
btn.disabled = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteProton(btn) {
|
async function deleteProton(btn) {
|
||||||
if (!confirm('Unlink Proton Mail integration?')) return;
|
showConfirm(btn, 'Unlink Proton Mail?', async () => {
|
||||||
|
try {
|
||||||
btn.disabled = true;
|
const response = await fetch('/api/v1/proton/auth', { method: 'DELETE' });
|
||||||
|
if (response.ok) {
|
||||||
try {
|
checkToast(response);
|
||||||
const response = await fetch('/api/v1/proton/auth', { method: 'DELETE' });
|
reloadIntegrations();
|
||||||
|
} else {
|
||||||
if (response.ok) {
|
const error = await response.json();
|
||||||
checkToast(response);
|
showToast(error.error || 'Failed to unlink Proton', 'error');
|
||||||
reloadIntegrations();
|
}
|
||||||
} else {
|
} catch (err) {
|
||||||
const error = await response.json();
|
showToast(err.message, 'error');
|
||||||
alert(`Error: ${error.error || 'Failed to unlink integration'}`);
|
|
||||||
btn.disabled = false;
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
});
|
||||||
alert(`Error: ${err.message}`);
|
|
||||||
btn.disabled = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
5
public/theme-init.js
Normal file
5
public/theme-init.js
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
(() => {
|
||||||
|
var t = localStorage.getItem('theme');
|
||||||
|
if (t && t !== 'system')
|
||||||
|
document.documentElement.setAttribute('data-theme', t);
|
||||||
|
})();
|
||||||
|
|
@ -23,8 +23,14 @@ window.cycleTheme = () => {
|
||||||
function updateButtonText(theme) {
|
function updateButtonText(theme) {
|
||||||
const btn = document.getElementById('theme-toggle');
|
const btn = document.getElementById('theme-toggle');
|
||||||
if (btn) {
|
if (btn) {
|
||||||
|
const labels = {
|
||||||
|
system: 'Toggle theme (system)',
|
||||||
|
light: 'Toggle theme (light)',
|
||||||
|
dark: 'Toggle theme (dark)',
|
||||||
|
};
|
||||||
btn.textContent =
|
btn.textContent =
|
||||||
theme === 'system' ? '🌓' : theme === 'light' ? '☀️' : '🌙';
|
theme === 'system' ? '🌓' : theme === 'light' ? '☀️' : '🌙';
|
||||||
|
btn.setAttribute('aria-label', labels[theme] || 'Toggle theme');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,14 +12,14 @@
|
||||||
<div class="password-wrapper">
|
<div class="password-wrapper">
|
||||||
<input type="password" id="proton-password" name="password" placeholder="Your Proton password" required>
|
<input type="password" id="proton-password" name="password" placeholder="Your Proton password" required>
|
||||||
<button type="button" class="password-toggle" data-action="toggle-password" aria-label="Show password">
|
<button type="button" class="password-toggle" data-action="toggle-password" aria-label="Show password">
|
||||||
<svg class="eye-icon eye-show" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7z"/><circle cx="12" cy="12" r="3"/></svg>
|
<svg class="eye-icon eye-show" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7z"/><circle cx="12" cy="12" r="3"/></svg>
|
||||||
<svg class="eye-icon eye-hide" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-10-7-10-7a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 10 7 10 7a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/><line x1="1" y1="1" x2="23" y2="23"/></svg>
|
<svg class="eye-icon eye-hide" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-10-7-10-7a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 10 7 10 7a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/><line x1="1" y1="1" x2="23" y2="23"/></svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="proton-totp">2FA Code (optional):</label>
|
<label for="proton-totp">2FA Code (optional):</label>
|
||||||
<input type="text" id="proton-totp" name="totp" placeholder="123456" pattern="[0-9]{6}">
|
<input type="text" id="proton-totp" name="totp" placeholder="123456" pattern="[0-9]{6}" inputmode="numeric" autocomplete="one-time-code">
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn-primary">Link</button>
|
<button type="submit" class="btn-primary">Link</button>
|
||||||
<div id="proton-auth-status" class="auth-status"></div>
|
<div id="proton-auth-status" class="auth-status"></div>
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,16 @@
|
||||||
package signal
|
package signal
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"html/template"
|
"html/template"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
qrcode "github.com/yeqown/go-qrcode/v2"
|
||||||
|
"github.com/yeqown/go-qrcode/writer/standard"
|
||||||
|
|
||||||
"prism/service/util"
|
"prism/service/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -111,9 +116,27 @@ func (h *Handlers) HandleLinkDevice(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
qrc, err := qrcode.NewWith(qrCode, qrcode.WithErrorCorrectionLevel(qrcode.ErrorCorrectionMedium))
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Error("Failed to encode QR code", "error", err)
|
||||||
|
util.JSONError(w, "Failed to generate QR code", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
w2 := standard.NewWithWriter(nopWriteCloser{&buf},
|
||||||
|
standard.WithBuiltinImageEncoder(standard.PNG_FORMAT),
|
||||||
|
standard.WithQRWidth(10),
|
||||||
|
)
|
||||||
|
if err := qrc.Save(w2); err != nil {
|
||||||
|
h.logger.Error("Failed to render QR code", "error", err)
|
||||||
|
util.JSONError(w, "Failed to generate QR code", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dataURL := "data:image/png;base64," + base64.StdEncoding.EncodeToString(buf.Bytes())
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(map[string]string{
|
json.NewEncoder(w).Encode(map[string]string{
|
||||||
"qr_code": qrCode,
|
"qr_code": dataURL,
|
||||||
"status": "linking",
|
"status": "linking",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -145,3 +168,7 @@ func (h *Handlers) HandleLinkStatus(w http.ResponseWriter, r *http.Request) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type nopWriteCloser struct{ *bytes.Buffer }
|
||||||
|
|
||||||
|
func (nopWriteCloser) Close() error { return nil }
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@
|
||||||
<li>Scan the QR code below</li>
|
<li>Scan the QR code below</li>
|
||||||
</ol>
|
</ol>
|
||||||
<div class="qr-code-wrapper">
|
<div class="qr-code-wrapper">
|
||||||
<img id="signal-qr-code" alt="Signal QR Code">
|
<img id="signal-qr-code" alt="QR code — scan with Signal to link this device" width="400" height="400">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
{{if .NotConfigured}}
|
{{if .NotConfigured}}
|
||||||
<p><strong>Setup Telegram Bot:</strong></p>
|
<p><strong>Setup Telegram Bot:</strong></p>
|
||||||
<ol class="link-instructions">
|
<ol class="link-instructions">
|
||||||
<li>Message <a href="https://t.me/BotFather" target="_blank">@BotFather</a> on Telegram</li>
|
<li>Message <a href="https://t.me/BotFather" target="_blank" rel="noopener noreferrer">@BotFather</a> on Telegram</li>
|
||||||
<li>Send <code>/newbot</code> and follow the prompts</li>
|
<li>Send <code>/newbot</code> and follow the prompts</li>
|
||||||
<li>Copy the bot token from BotFather</li>
|
<li>Copy the bot token from BotFather</li>
|
||||||
<li>Message <a href="https://t.me/userinfobot" target="_blank">@userinfobot</a> to get your Chat ID</li>
|
<li>Message <a href="https://t.me/userinfobot" target="_blank" rel="noopener noreferrer">@userinfobot</a> to get your Chat ID</li>
|
||||||
<li>Enter both below:</li>
|
<li>Enter both below:</li>
|
||||||
</ol>
|
</ol>
|
||||||
<form class="auth-form" data-handler="telegram">
|
<form class="auth-form" data-handler="telegram">
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,17 @@
|
||||||
{{if .EnableSignal}}
|
{{if .EnableSignal}}
|
||||||
<div id="signal-integration" class="integration-container" hx-get="/fragment/signal" hx-trigger="load">
|
<div id="signal-integration" class="integration-container" hx-get="/fragment/signal" hx-trigger="load">
|
||||||
<div class="loading"><div class="spinner"></div></div>
|
<div class="loading" role="status"><div class="spinner"></div><span class="sr-only">Loading…</span></div>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{if .EnableTelegram}}
|
{{if .EnableTelegram}}
|
||||||
<div id="telegram-integration" class="integration-container" hx-get="/fragment/telegram" hx-trigger="load">
|
<div id="telegram-integration" class="integration-container" hx-get="/fragment/telegram" hx-trigger="load">
|
||||||
<div class="loading"><div class="spinner"></div></div>
|
<div class="loading" role="status"><div class="spinner"></div><span class="sr-only">Loading…</span></div>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{if .EnableProton}}
|
{{if .EnableProton}}
|
||||||
<div id="proton-integration" class="integration-container" hx-get="/fragment/proton" hx-trigger="load">
|
<div id="proton-integration" class="integration-container" hx-get="/fragment/proton" hx-trigger="load">
|
||||||
<div class="loading"><div class="spinner"></div></div>
|
<div class="loading" role="status"><div class="spinner"></div><span class="sr-only">Loading…</span></div>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,8 @@
|
||||||
hx-delete="/apps/{{$app.AppName}}/subscriptions/{{.ID}}"
|
hx-delete="/apps/{{$app.AppName}}/subscriptions/{{.ID}}"
|
||||||
hx-target="#apps-list"
|
hx-target="#apps-list"
|
||||||
hx-swap="innerHTML"
|
hx-swap="innerHTML"
|
||||||
hx-confirm="Delete this channel?">×</button>
|
hx-confirm="Delete this channel?"
|
||||||
|
aria-label="Remove {{$channel.Channel}} channel">×</button>
|
||||||
</span>
|
</span>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<details class="integration-card"{{if .Open}} open{{end}}>
|
<details class="integration-card"{{if .Open}} open{{end}}>
|
||||||
<summary class="integration-header">
|
<summary class="integration-header">
|
||||||
<span class="integration-name">{{.Name}}</span>
|
<h2>{{.Name}}</h2>
|
||||||
<span class="integration-status {{.StatusClass}}">
|
<span class="integration-status {{.StatusClass}}">
|
||||||
{{.StatusText}}
|
{{.StatusText}}
|
||||||
{{if .StatusTooltip}}<span class="tooltip">{{.StatusTooltip}}</span>{{end}}
|
{{if .StatusTooltip}}<span class="tooltip">{{.StatusTooltip}}</span>{{end}}
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
Loading…
Add table
Reference in a new issue