13 Commits

21 changed files with 1529 additions and 1097 deletions

View File

@@ -1,5 +1,44 @@
# Changelog
## 2026-04-07 - 2.12.1 - fix(ts_web)
handle slotted config section content visibility and field row borders correctly
- track whether the default slot has assigned content and hide the slot container when empty
- wrap rendered fields in a dedicated list so the last field row border is removed correctly when slot content is present
## 2026-04-07 - 2.12.0 - feat(elements)
standardize dashboard and detail views on dees tile and stats grid components
- replace custom stat cards with dees-statsgrid across status and network views and remove the obsolete sz-stat-card element
- migrate multiple detail and settings views to dees-tile headers and footers for consistent action placement and styling
- extend sz-config-section with footer links and actions plus improved collapsed tile behavior
## 2026-04-05 - 2.11.2 - fix(route-card)
align route card with source profile metadata and vpnOnly route configuration
- rename linked route metadata fields from security profile to source profile in rendering and feature detection
- simplify VPN display logic to use the boolean vpnOnly flag instead of the previous nested VPN configuration object
## 2026-04-04 - 2.11.1 - fix(route-card)
clarify VPN mode badge labels in route cards
- Renames VPN badge text from "VPN Only"/"VPN + Public" to "VPN Mandatory"/"VPN Voluntary" for clearer route mode descriptions.
## 2026-04-02 - 2.11.0 - feat(route-ui)
add VPN details and conditional card actions to route cards
- Extend route card data and rendering to display VPN access mode and allowed client tags.
- Add optional Edit and Delete action buttons that emit route-edit and route-delete events.
- Allow the route list view to control action visibility per route via a showActionsFilter callback.
- Include VPN as a visible route feature indicator in the card summary.
## 2026-04-02 - 2.10.0 - feat(docs)
document newly available catalog components and updated build configuration details
- Update component counts and add documentation for App Store, Routes, MTA/Email, and Configuration views
- Expand the README with new component tables and exported TypeScript types
- Refresh build notes to reference .smartconfig.json and the renamed config file
## 2026-04-02 - 2.9.1 - fix(build)
migrate build configuration to .smartconfig and update toolchain dependencies

View File

@@ -1,6 +1,6 @@
{
"name": "@serve.zone/catalog",
"version": "2.9.1",
"version": "2.12.1",
"private": false,
"description": "UI component catalog for serve.zone",
"main": "dist_ts_web/index.js",
@@ -14,7 +14,7 @@
"author": "Lossless GmbH",
"license": "MIT",
"dependencies": {
"@design.estate/dees-catalog": "^3.49.2",
"@design.estate/dees-catalog": "^3.67.1",
"@design.estate/dees-domtools": "^2.5.4",
"@design.estate/dees-element": "^2.2.4",
"@design.estate/dees-wcctools": "^3.8.0"
@@ -26,7 +26,7 @@
"@git.zone/tstest": "^3.6.3",
"@git.zone/tswatch": "^3.3.2",
"@push.rocks/projectinfo": "^5.1.0",
"@types/node": "^25.5.0"
"@types/node": "^25.5.2"
},
"files": [
"ts_web/**/*",

251
pnpm-lock.yaml generated
View File

@@ -9,8 +9,8 @@ importers:
.:
dependencies:
'@design.estate/dees-catalog':
specifier: ^3.49.2
version: 3.49.2(@tiptap/pm@2.27.2)
specifier: ^3.67.1
version: 3.67.1(@tiptap/pm@2.27.2)
'@design.estate/dees-domtools':
specifier: ^2.5.4
version: 2.5.4
@@ -40,8 +40,8 @@ importers:
specifier: ^5.1.0
version: 5.1.0
'@types/node':
specifier: ^25.5.0
version: 25.5.0
specifier: ^25.5.2
version: 25.5.2
packages:
@@ -256,8 +256,8 @@ packages:
'@configvault.io/interfaces@1.0.17':
resolution: {integrity: sha512-bEcCUR2VBDJsTin8HQh8Uw/mlYl2v8A3jMIaQ+MTB9Hrqd6CZL2dL7iJdWyFl/3EIX+LDxWFR+Oq7liIq7w+1Q==}
'@design.estate/dees-catalog@3.49.2':
resolution: {integrity: sha512-ChVf5IW/w1WSsfuI3BA1SX2QJFjZljnAvnyPDXnbzTXuOdTgs054p66JwlDca9KM8yBlndwibgAYJfD6/4sONw==}
'@design.estate/dees-catalog@3.67.1':
resolution: {integrity: sha512-8zaVNP70IbcB6pEmLoBxVA5WD0N5gQr12ylTdILtvds6rftKLCI1i2jx4RBztIy4FpZv0wIewJBtRvSUjK8Ysw==}
'@design.estate/dees-comms@1.0.30':
resolution: {integrity: sha512-KchMlklJfKAjQiJiR0xmofXtQ27VgZtBIxcMwPE9d+h3jJRv+lPZxzBQVOM0eyM0uS44S5vJMZ11IeV4uDXSHg==}
@@ -995,79 +995,79 @@ packages:
'@mongodb-js/saslprep@1.4.6':
resolution: {integrity: sha512-y+x3H1xBZd38n10NZF/rEBlvDOOMQ6LKUTHqr8R9VkJ+mmQOYtJFxIlkkK8fZrtOiL6VixbOBWMbZGBdal3Z1g==}
'@napi-rs/canvas-android-arm64@0.1.94':
resolution: {integrity: sha512-YQ6K83RWNMQOtgpk1aIML97QTE3zxPmVCHTi5eA8Nss4+B9JZi5J7LHQr7B5oD7VwSfWd++xsPdUiJ1+frqsMg==}
'@napi-rs/canvas-android-arm64@0.1.97':
resolution: {integrity: sha512-V1c/WVw+NzH8vk7ZK/O8/nyBSCQimU8sfMsB/9qeSvdkGKNU7+mxy/bIF0gTgeBFmHpj30S4E9WHMSrxXGQuVQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [android]
'@napi-rs/canvas-darwin-arm64@0.1.94':
resolution: {integrity: sha512-h1yl9XjqSrYZAbBUHCVLAhwd2knM8D8xt081Pv40KqNJXfeMmBrhG1SfroRymG2ak+pl42iQlWjFZ2Z8AWFdSw==}
'@napi-rs/canvas-darwin-arm64@0.1.97':
resolution: {integrity: sha512-ok+SCEF4YejcxuJ9Rm+WWunHHpf2HmiPxfz6z1a/NFQECGXtsY7A4B8XocK1LmT1D7P174MzwPF9Wy3AUAwEPw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@napi-rs/canvas-darwin-x64@0.1.94':
resolution: {integrity: sha512-rkr/lrafbU0IIHebst+sQJf1HjdHvTMN0GGqWvw5OfaVS0K/sVxhNHtxi8oCfaRSvRE62aJZjWTcdc2ue/o6yw==}
'@napi-rs/canvas-darwin-x64@0.1.97':
resolution: {integrity: sha512-PUP6e6/UGlclUvAQNnuXCcnkpdUou6VYZfQOQxExLp86epOylmiwLkqXIvpFmjoTEDmPmXrI+coL/9EFU1gKPA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.94':
resolution: {integrity: sha512-q95TDo32YkTKdi+Sp2yQ2Npm7pmfKEruNoJ3RUIw1KvQQ9EHKL3fii/iuU60tnzP0W+c8BKN7BFstNFcm2KXCQ==}
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.97':
resolution: {integrity: sha512-XyXH2L/cic8eTNtbrXCcvqHtMX/nEOxN18+7rMrAM2XtLYC/EB5s0wnO1FsLMWmK+04ZSLN9FBGipo7kpIkcOw==}
engines: {node: '>= 10'}
cpu: [arm]
os: [linux]
'@napi-rs/canvas-linux-arm64-gnu@0.1.94':
resolution: {integrity: sha512-Je5/gKVybWAoIGyDOcJF1zYgBTKWkPIkfOgvCzrQcl8h7DiDvRvEY70EapA+NicGe4X3DW9VsCT34KZJnerShA==}
'@napi-rs/canvas-linux-arm64-gnu@0.1.97':
resolution: {integrity: sha512-Kuq/M3djq0K8ktgz6nPlK7Ne5d4uWeDxPpyKWOjWDK2RIOhHVtLtyLiJw2fuldw7Vn4mhw05EZXCEr4Q76rs9w==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@napi-rs/canvas-linux-arm64-musl@0.1.94':
resolution: {integrity: sha512-9YleDDauDEZNsFnfz3HyZvp1LK1ECu8N2gDUg1wtL7uWLQv8dUbfVeFtp5HOdxht1o7LsWRmQeqeIbnD4EqE2A==}
'@napi-rs/canvas-linux-arm64-musl@0.1.97':
resolution: {integrity: sha512-kKmSkQVnWeqg7qdsiXvYxKhAFuHz3tkBjW/zyQv5YKUPhotpaVhpBGv5LqCngzyuRV85SXoe+OFj+Tv0a0QXkQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@napi-rs/canvas-linux-riscv64-gnu@0.1.94':
resolution: {integrity: sha512-lQUy9Xvz7ch8+0AXq8RkioLD41iQ6EqdKFu5uV40BxkBDijB2SCm1jna/BRhqitQRSjwAk2KlLUxTjHChyfNGg==}
'@napi-rs/canvas-linux-riscv64-gnu@0.1.97':
resolution: {integrity: sha512-Jc7I3A51jnEOIAXeLsN/M/+Z28LUeakcsXs07FLq9prXc0eYOtVwsDEv913Gr+06IRo34gJJVgT0TXvmz+N2VA==}
engines: {node: '>= 10'}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@napi-rs/canvas-linux-x64-gnu@0.1.94':
resolution: {integrity: sha512-0IYgyuUaugHdWxXRhDQUCMxTou8kAHHmpIBFtbmdRlciPlfK7AYQW5agvUU1PghPc5Ja3Zzp5qZfiiLu36vIWQ==}
'@napi-rs/canvas-linux-x64-gnu@0.1.97':
resolution: {integrity: sha512-iDUBe7AilfuBSRbSa8/IGX38Mf+iCSBqoVKLSQ5XaY2JLOaqz1TVyPFEyIck7wT6mRQhQt5sN6ogfjIDfi74tg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@napi-rs/canvas-linux-x64-musl@0.1.94':
resolution: {integrity: sha512-xuetfzzcflCIiBw2HJlOU4/+zTqhdxoe1BEcwdBsHAd/5wAQ4Pp+FGPi5g74gDvtcXQmTdEU3fLQvHc/j3wbxQ==}
'@napi-rs/canvas-linux-x64-musl@0.1.97':
resolution: {integrity: sha512-AKLFd/v0Z5fvgqBDqhvqtAdx+fHMJ5t9JcUNKq4FIZ5WH+iegGm8HPdj00NFlCSnm83Fp3Ln8I2f7uq1aIiWaA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
'@napi-rs/canvas-win32-arm64-msvc@0.1.94':
resolution: {integrity: sha512-2F3p8wci4Q4vjbENlQtSibqFWxBdpzYk1c8Jh1mqqLE92rBKElG018dBJ6C8Dp49vE350Hmy5LrfdLgFKMG8sg==}
'@napi-rs/canvas-win32-arm64-msvc@0.1.97':
resolution: {integrity: sha512-u883Yr6A6fO7Vpsy9YE4FVCIxzzo5sO+7pIUjjoDLjS3vQaNMkVzx5bdIpEL+ob+gU88WDK4VcxYMZ6nmnoX9A==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
'@napi-rs/canvas-win32-x64-msvc@0.1.94':
resolution: {integrity: sha512-hjwaIKMrQLoNiu3724octSGhDVKkBwJtMeQ3qUXOi+y60h2q6Sxq3+MM2za3V88+XQzzwn0DgG0Xo6v6gzV8kQ==}
'@napi-rs/canvas-win32-x64-msvc@0.1.97':
resolution: {integrity: sha512-sWtD2EE3fV0IzN+iiQUqr/Q1SwqWhs2O1FKItFlxtdDkikpEj5g7DKQpY3x55H/MAOnL8iomnlk3mcEeGiUMoQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
'@napi-rs/canvas@0.1.94':
resolution: {integrity: sha512-8jBkvqynXNdQPNZjLJxB/Rp9PdnnMSHFBLzPmMc615nlt/O6w0ergBbkEDEOr8EbjL8nRQDpEklPx4pzD7zrbg==}
'@napi-rs/canvas@0.1.97':
resolution: {integrity: sha512-8cFniXvrIEnVwuNSRCW9wirRZbHvrD3JVujdS2P5n5xiJZNZMOZcfOvJ1pb66c7jXMKHHglJEDVJGbm8XWFcXQ==}
engines: {node: '>= 10'}
'@napi-rs/wasm-runtime@1.0.7':
@@ -2069,11 +2069,11 @@ packages:
'@types/node@16.9.1':
resolution: {integrity: sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==}
'@types/node@22.19.11':
resolution: {integrity: sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==}
'@types/node@22.19.17':
resolution: {integrity: sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==}
'@types/node@25.5.0':
resolution: {integrity: sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==}
'@types/node@25.5.2':
resolution: {integrity: sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg==}
'@types/randomatic@3.1.5':
resolution: {integrity: sha512-VCwCTw6qh1pRRw+5rNTAwqPmf6A+hdrkdM7dBpZVmhl7g+em3ONXlYK/bWPVKqVGMWgP0d1bog8Vc/X6zRwRRQ==}
@@ -2175,9 +2175,6 @@ packages:
any-base@1.1.0:
resolution: {integrity: sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg==}
apexcharts@5.10.4:
resolution: {integrity: sha512-gt0VUqZ2+mr25ScbUcKZgJr96jKYm4vjOcxEWCEh/E5F4dWqhyo3dBhPRvNNnkKiWxkMd2cBwj3ZYH3rK39fkA==}
argparse@1.0.10:
resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
@@ -2504,6 +2501,9 @@ packages:
eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
echarts@5.6.0:
resolution: {integrity: sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==}
emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
@@ -2621,6 +2621,9 @@ packages:
resolution: {integrity: sha512-CGnyrvbhPlWYMngksqrSSUT1BAVP49dZocrHuK0SvtR0D5TMs5wP0o3j7jexDJW01KSadjBp1M/71o/KR3nD1w==}
engines: {node: '>=18'}
fancy-canvas@2.1.0:
resolution: {integrity: sha512-nifxXJ95JNLFR2NgRV4/MxVP45G9909wJTEKz5fg/TZS20JJZA6hfgRVh/bC9bwl2zBtBNcYPjiBE4njQHVBwQ==}
fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
@@ -2839,8 +2842,8 @@ packages:
humanize-ms@1.2.1:
resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==}
ibantools@4.5.1:
resolution: {integrity: sha512-DfKQpLlFq9yEUIEnFuCJzss3XavD7iHZTU5PyqXiAJ+rmaMp+NFP3hboumHKuK8nZjuOJg93WemTzcQ5b9jOZA==}
ibantools@4.5.2:
resolution: {integrity: sha512-is+8TgZcKS/AMv/z9nW1zz0bhjhoyjpA1p0nc3A6GkW/InOdcQiUZpkufADzh/aO/LY/TOD/P3oPWncNRn5QMA==}
iconv-lite@0.4.24:
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
@@ -2969,6 +2972,9 @@ packages:
resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==}
engines: {node: '>=0.10.0'}
lightweight-charts@5.1.0:
resolution: {integrity: sha512-jEAYR4ODYeyNZcWUigsoLTl52rbPmgXnvd5FLIv/ZoA/2sSDw63YKnef8n4yhzum7W926yHeFwlm7ididKb7YQ==}
lines-and-columns@1.2.4:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
@@ -3473,6 +3479,7 @@ packages:
resolution: {integrity: sha512-iiRSuRmLihoEJ4YGkoqSq3/r4MR0OmkMTYDda0Pq7DAWqJwMylTilXu46T16gfS3DUp3fhiVuz7NtRMbk3uBhw==}
engines: {node: '>=20.18.0'}
hasBin: true
bundledDependencies: []
pdfjs-dist@4.10.38:
resolution: {integrity: sha512-/Y3fcFrXEAsMjJXeL9J8+ZG9U01LbuWaYypvDW2ycW1jL269L3js3DVBjDJ0Up9Np1uqDXsDrRihHANhZOlwdQ==}
@@ -3539,8 +3546,8 @@ packages:
prosemirror-dropcursor@1.8.2:
resolution: {integrity: sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==}
prosemirror-gapcursor@1.4.0:
resolution: {integrity: sha512-z00qvurSdCEWUIulij/isHaqu4uLS8r/Fi61IbjdIPJEonQgggbJsLnstW7Lgdk4zQ68/yr6B6bf7sJXowIgdQ==}
prosemirror-gapcursor@1.4.1:
resolution: {integrity: sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==}
prosemirror-history@1.5.0:
resolution: {integrity: sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==}
@@ -3579,11 +3586,11 @@ packages:
prosemirror-state: ^1.4.2
prosemirror-view: ^1.33.8
prosemirror-transform@1.11.0:
resolution: {integrity: sha512-4I7Ce4KpygXb9bkiPS3hTEk4dSHorfRw8uI0pE8IhxlK2GXsqv5tIA7JUSxtSu7u8APVOTtbUBxTmnHIxVkIJw==}
prosemirror-transform@1.12.0:
resolution: {integrity: sha512-GxboyN4AMIsoHNtz5uf2r2Ru551i5hWeCMD6E2Ib4Eogqoub0NflniaBPVQ4MrGE5yZ8JV9tUHg9qcZTTrcN4w==}
prosemirror-view@1.41.6:
resolution: {integrity: sha512-mxpcDG4hNQa/CPtzxjdlir5bJFDlm0/x5nGBbStB2BWX+XOQ9M8ekEG+ojqB5BcVu2Rc80/jssCMZzSstJuSYg==}
prosemirror-view@1.41.8:
resolution: {integrity: sha512-TnKDdohEatgyZNGCDWIdccOHXhYloJwbwU+phw/a23KBvJIR9lWQWW7WHHK3vBdOLDNuF7TaX98GObUZOWkOnA==}
proto-list@1.2.4:
resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==}
@@ -3938,6 +3945,9 @@ packages:
tslib@1.14.1:
resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
tslib@2.3.0:
resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==}
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
@@ -4157,6 +4167,9 @@ packages:
zod@3.25.76:
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
zrender@5.6.1:
resolution: {integrity: sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==}
zwitch@2.0.4:
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
@@ -4196,7 +4209,7 @@ snapshots:
'@api.global/typedrequest-interfaces': 3.0.19
'@api.global/typedsocket': 4.1.2(@push.rocks/smartserve@2.0.3)
'@cloudflare/workers-types': 4.20260402.1
'@design.estate/dees-catalog': 3.49.2(@tiptap/pm@2.27.2)
'@design.estate/dees-catalog': 3.67.1(@tiptap/pm@2.27.2)
'@design.estate/dees-comms': 1.0.30
'@push.rocks/lik': 6.4.0
'@push.rocks/smartdelay': 3.0.5
@@ -4762,7 +4775,7 @@ snapshots:
dependencies:
'@api.global/typedrequest-interfaces': 3.0.19
'@design.estate/dees-catalog@3.49.2(@tiptap/pm@2.27.2)':
'@design.estate/dees-catalog@3.67.1(@tiptap/pm@2.27.2)':
dependencies:
'@design.estate/dees-domtools': 2.5.4
'@design.estate/dees-element': 2.2.4
@@ -4782,9 +4795,10 @@ snapshots:
'@tiptap/extension-underline': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))
'@tiptap/starter-kit': 2.27.2
'@tsclass/tsclass': 9.5.0
apexcharts: 5.10.4
echarts: 5.6.0
highlight.js: 11.11.1
ibantools: 4.5.1
ibantools: 4.5.2
lightweight-charts: 5.1.0
lucide: 0.577.0
monaco-editor: 0.55.1
pdfjs-dist: 4.10.38
@@ -5313,7 +5327,7 @@ snapshots:
'@inquirer/figures': 1.0.15
'@inquirer/type': 2.0.0
'@types/mute-stream': 0.0.4
'@types/node': 22.19.11
'@types/node': 22.19.17
'@types/wrap-ansi': 3.0.0
ansi-escapes: 4.3.2
cli-width: 4.1.0
@@ -5626,52 +5640,52 @@ snapshots:
dependencies:
sparse-bitfield: 3.0.3
'@napi-rs/canvas-android-arm64@0.1.94':
'@napi-rs/canvas-android-arm64@0.1.97':
optional: true
'@napi-rs/canvas-darwin-arm64@0.1.94':
'@napi-rs/canvas-darwin-arm64@0.1.97':
optional: true
'@napi-rs/canvas-darwin-x64@0.1.94':
'@napi-rs/canvas-darwin-x64@0.1.97':
optional: true
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.94':
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.97':
optional: true
'@napi-rs/canvas-linux-arm64-gnu@0.1.94':
'@napi-rs/canvas-linux-arm64-gnu@0.1.97':
optional: true
'@napi-rs/canvas-linux-arm64-musl@0.1.94':
'@napi-rs/canvas-linux-arm64-musl@0.1.97':
optional: true
'@napi-rs/canvas-linux-riscv64-gnu@0.1.94':
'@napi-rs/canvas-linux-riscv64-gnu@0.1.97':
optional: true
'@napi-rs/canvas-linux-x64-gnu@0.1.94':
'@napi-rs/canvas-linux-x64-gnu@0.1.97':
optional: true
'@napi-rs/canvas-linux-x64-musl@0.1.94':
'@napi-rs/canvas-linux-x64-musl@0.1.97':
optional: true
'@napi-rs/canvas-win32-arm64-msvc@0.1.94':
'@napi-rs/canvas-win32-arm64-msvc@0.1.97':
optional: true
'@napi-rs/canvas-win32-x64-msvc@0.1.94':
'@napi-rs/canvas-win32-x64-msvc@0.1.97':
optional: true
'@napi-rs/canvas@0.1.94':
'@napi-rs/canvas@0.1.97':
optionalDependencies:
'@napi-rs/canvas-android-arm64': 0.1.94
'@napi-rs/canvas-darwin-arm64': 0.1.94
'@napi-rs/canvas-darwin-x64': 0.1.94
'@napi-rs/canvas-linux-arm-gnueabihf': 0.1.94
'@napi-rs/canvas-linux-arm64-gnu': 0.1.94
'@napi-rs/canvas-linux-arm64-musl': 0.1.94
'@napi-rs/canvas-linux-riscv64-gnu': 0.1.94
'@napi-rs/canvas-linux-x64-gnu': 0.1.94
'@napi-rs/canvas-linux-x64-musl': 0.1.94
'@napi-rs/canvas-win32-arm64-msvc': 0.1.94
'@napi-rs/canvas-win32-x64-msvc': 0.1.94
'@napi-rs/canvas-android-arm64': 0.1.97
'@napi-rs/canvas-darwin-arm64': 0.1.97
'@napi-rs/canvas-darwin-x64': 0.1.97
'@napi-rs/canvas-linux-arm-gnueabihf': 0.1.97
'@napi-rs/canvas-linux-arm64-gnu': 0.1.97
'@napi-rs/canvas-linux-arm64-musl': 0.1.97
'@napi-rs/canvas-linux-riscv64-gnu': 0.1.97
'@napi-rs/canvas-linux-x64-gnu': 0.1.97
'@napi-rs/canvas-linux-x64-musl': 0.1.97
'@napi-rs/canvas-win32-arm64-msvc': 0.1.97
'@napi-rs/canvas-win32-x64-msvc': 0.1.97
optional: true
'@napi-rs/wasm-runtime@1.0.7':
@@ -7209,7 +7223,7 @@ snapshots:
prosemirror-collab: 1.3.1
prosemirror-commands: 1.7.1
prosemirror-dropcursor: 1.8.2
prosemirror-gapcursor: 1.4.0
prosemirror-gapcursor: 1.4.1
prosemirror-history: 1.5.0
prosemirror-inputrules: 1.5.1
prosemirror-keymap: 1.2.3
@@ -7220,9 +7234,9 @@ snapshots:
prosemirror-schema-list: 1.5.1
prosemirror-state: 1.4.4
prosemirror-tables: 1.8.5
prosemirror-trailing-node: 3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.6)
prosemirror-transform: 1.11.0
prosemirror-view: 1.41.6
prosemirror-trailing-node: 3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.8)
prosemirror-transform: 1.12.0
prosemirror-view: 1.41.8
'@tiptap/starter-kit@2.27.2':
dependencies:
@@ -7280,7 +7294,7 @@ snapshots:
'@types/clean-css@4.2.11':
dependencies:
'@types/node': 25.5.0
'@types/node': 25.5.2
source-map: 0.6.1
'@types/debug@4.1.12':
@@ -7290,7 +7304,7 @@ snapshots:
'@types/fs-extra@11.0.4':
dependencies:
'@types/jsonfile': 6.1.4
'@types/node': 25.5.0
'@types/node': 25.5.2
'@types/hast@3.0.4':
dependencies:
@@ -7310,7 +7324,7 @@ snapshots:
'@types/jsonfile@6.1.4':
dependencies:
'@types/node': 25.5.0
'@types/node': 25.5.2
'@types/linkify-it@5.0.0': {}
@@ -7333,19 +7347,19 @@ snapshots:
'@types/mute-stream@0.0.4':
dependencies:
'@types/node': 25.5.0
'@types/node': 25.5.2
'@types/node-forge@1.3.14':
dependencies:
'@types/node': 25.5.0
'@types/node': 25.5.2
'@types/node@16.9.1': {}
'@types/node@22.19.11':
'@types/node@22.19.17':
dependencies:
undici-types: 6.21.0
'@types/node@25.5.0':
'@types/node@25.5.2':
dependencies:
undici-types: 7.18.2
@@ -7359,11 +7373,11 @@ snapshots:
'@types/tar-stream@3.1.4':
dependencies:
'@types/node': 25.5.0
'@types/node': 25.5.2
'@types/through2@2.0.41':
dependencies:
'@types/node': 25.5.0
'@types/node': 25.5.2
'@types/trusted-types@2.0.7': {}
@@ -7393,11 +7407,11 @@ snapshots:
'@types/ws@8.18.1':
dependencies:
'@types/node': 25.5.0
'@types/node': 25.5.2
'@types/yauzl@2.10.3':
dependencies:
'@types/node': 25.5.0
'@types/node': 25.5.2
optional: true
'@ungap/structured-clone@1.3.0': {}
@@ -7440,8 +7454,6 @@ snapshots:
any-base@1.1.0: {}
apexcharts@5.10.4: {}
argparse@1.0.10:
dependencies:
sprintf-js: 1.0.3
@@ -7745,6 +7757,11 @@ snapshots:
eastasianwidth@0.2.0: {}
echarts@5.6.0:
dependencies:
tslib: 2.3.0
zrender: 5.6.1
emoji-regex@8.0.0: {}
emoji-regex@9.2.2: {}
@@ -7892,6 +7909,8 @@ snapshots:
fake-indexeddb@6.2.5: {}
fancy-canvas@2.1.0: {}
fast-deep-equal@3.1.3: {}
fast-fifo@1.3.2: {}
@@ -8167,7 +8186,7 @@ snapshots:
dependencies:
ms: 2.1.3
ibantools@4.5.1: {}
ibantools@4.5.2: {}
iconv-lite@0.4.24:
dependencies:
@@ -8304,6 +8323,10 @@ snapshots:
kind-of@6.0.3: {}
lightweight-charts@5.1.0:
dependencies:
fancy-canvas: 2.1.0
lines-and-columns@1.2.4: {}
linkify-it@5.0.0:
@@ -8970,7 +8993,7 @@ snapshots:
pdfjs-dist@4.10.38:
optionalDependencies:
'@napi-rs/canvas': 0.1.94
'@napi-rs/canvas': 0.1.97
peek-readable@4.1.0: {}
@@ -9006,7 +9029,7 @@ snapshots:
prosemirror-changeset@2.4.0:
dependencies:
prosemirror-transform: 1.11.0
prosemirror-transform: 1.12.0
prosemirror-collab@1.3.1:
dependencies:
@@ -9016,32 +9039,32 @@ snapshots:
dependencies:
prosemirror-model: 1.25.4
prosemirror-state: 1.4.4
prosemirror-transform: 1.11.0
prosemirror-transform: 1.12.0
prosemirror-dropcursor@1.8.2:
dependencies:
prosemirror-state: 1.4.4
prosemirror-transform: 1.11.0
prosemirror-view: 1.41.6
prosemirror-transform: 1.12.0
prosemirror-view: 1.41.8
prosemirror-gapcursor@1.4.0:
prosemirror-gapcursor@1.4.1:
dependencies:
prosemirror-keymap: 1.2.3
prosemirror-model: 1.25.4
prosemirror-state: 1.4.4
prosemirror-view: 1.41.6
prosemirror-view: 1.41.8
prosemirror-history@1.5.0:
dependencies:
prosemirror-state: 1.4.4
prosemirror-transform: 1.11.0
prosemirror-view: 1.41.6
prosemirror-transform: 1.12.0
prosemirror-view: 1.41.8
rope-sequence: 1.3.4
prosemirror-inputrules@1.5.1:
dependencies:
prosemirror-state: 1.4.4
prosemirror-transform: 1.11.0
prosemirror-transform: 1.12.0
prosemirror-keymap@1.2.3:
dependencies:
@@ -9073,39 +9096,39 @@ snapshots:
dependencies:
prosemirror-model: 1.25.4
prosemirror-state: 1.4.4
prosemirror-transform: 1.11.0
prosemirror-transform: 1.12.0
prosemirror-state@1.4.4:
dependencies:
prosemirror-model: 1.25.4
prosemirror-transform: 1.11.0
prosemirror-view: 1.41.6
prosemirror-transform: 1.12.0
prosemirror-view: 1.41.8
prosemirror-tables@1.8.5:
dependencies:
prosemirror-keymap: 1.2.3
prosemirror-model: 1.25.4
prosemirror-state: 1.4.4
prosemirror-transform: 1.11.0
prosemirror-view: 1.41.6
prosemirror-transform: 1.12.0
prosemirror-view: 1.41.8
prosemirror-trailing-node@3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.6):
prosemirror-trailing-node@3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.8):
dependencies:
'@remirror/core-constants': 3.0.0
escape-string-regexp: 4.0.0
prosemirror-model: 1.25.4
prosemirror-state: 1.4.4
prosemirror-view: 1.41.6
prosemirror-view: 1.41.8
prosemirror-transform@1.11.0:
prosemirror-transform@1.12.0:
dependencies:
prosemirror-model: 1.25.4
prosemirror-view@1.41.6:
prosemirror-view@1.41.8:
dependencies:
prosemirror-model: 1.25.4
prosemirror-state: 1.4.4
prosemirror-transform: 1.11.0
prosemirror-transform: 1.12.0
proto-list@1.2.4: {}
@@ -9591,6 +9614,8 @@ snapshots:
tslib@1.14.1: {}
tslib@2.3.0: {}
tslib@2.8.1: {}
tsx@4.21.0:
@@ -9786,4 +9811,8 @@ snapshots:
zod@3.25.76: {}
zrender@5.6.1:
dependencies:
tslib: 2.3.0
zwitch@2.0.4: {}

View File

@@ -2,7 +2,7 @@
## Project Structure
- `html/index.ts` - WccTools setup with sections for Pages and Elements
- `ts_web/elements/` - All web components (27 elements + 6 demo-view wrappers)
- `ts_web/elements/` - All web components (33 elements + 9 demo-view wrappers)
- `ts_web/elements/index.ts` - Barrel export for all element components
- `ts_web/pages/` - Page components
@@ -16,13 +16,18 @@
## Demo Groups
| Group | Elements |
|-------|----------|
| Dashboard | sz-dashboard-view, sz-stat-card, sz-resource-usage-card, sz-traffic-card, sz-quick-actions-card |
| Dashboard | sz-dashboard-view, sz-resource-usage-card, sz-traffic-card, sz-quick-actions-card |
| Dashboard Grids | sz-status-grid-cluster, sz-status-grid-services, sz-status-grid-network, sz-status-grid-infra |
| Platform | sz-platform-services-card, sz-platform-service-detail-view |
| Network | sz-network-proxy-view, sz-network-dns-view, sz-network-domains-view, sz-reverse-proxy-card, sz-dns-ssl-card, sz-certificates-card, sz-domain-detail-view |
| Routes | sz-route-list-view, sz-route-card |
| Services | sz-services-list-view, sz-services-backups-view, sz-service-detail-view, sz-service-create-view |
| App Store | sz-app-store-view |
| MTA / Email | sz-mta-list-view, sz-mta-detail-view |
| Configuration | sz-config-overview, sz-config-section |
| Auth & Settings | sz-login-view, sz-tokens-view, sz-settings-view, sz-registry-advertisement, sz-registry-external-view |
## Build
- `pnpm run build` - tsbuild tsfolders + tsbundle element --production
- `pnpm run build` - tsbuild tsfolders + tsbundle (reads from .smartconfig.json)
- `pnpm run watch` - starts wcctools dev server
- Config file: `.smartconfig.json` (renamed from npmextra.json)

View File

@@ -14,15 +14,19 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
## 🚀 What It Does
`@serve.zone/catalog` provides **30+ production-ready web components** covering every aspect of server management:
`@serve.zone/catalog` provides **34 production-ready web components** covering every aspect of server management:
- 📊 **Dashboard** — Real-time cluster overview, resource usage, traffic metrics, quick actions
- 🐳 **Services** — Docker container management, deployment, logs, live stats, backups, and an integrated IDE workspace
- 🛒 **App Store** — Browse and deploy pre-configured application templates (WordPress, Gitea, etc.)
- 🌐 **Network** — Reverse proxy configuration, DNS record management, domain & SSL certificate monitoring
- 🔀 **Routes** — SmartProxy route configuration, match criteria, TLS modes, security profiles, forwarding targets
- 📧 **MTA / Email** — Inbound and outbound email management, SMTP transaction logs, authentication results
- 📦 **Registries** — Container registry management (onebox + external registries like Docker Hub, GHCR, ECR)
- 🔑 **Auth** — Login view, API token management (global + CI tokens)
- ⚙️ **Settings** — Appearance, Cloudflare integration, SSL/TLS config, network settings, account management
- 🏗️ **Platform Services** — MongoDB, MinIO, ClickHouse, Redis, Caddy monitoring and control
- 📋 **Configuration** — Read-only overview of the running server configuration with collapsible sections
Every component supports **light and dark themes** out of the box and communicates via standard `CustomEvent` dispatching.
@@ -85,6 +89,12 @@ import '@serve.zone/catalog';
| `SzServiceCreateView` | `<sz-service-create-view>` | Service deployment form — image, ports, env vars, volumes, resource limits |
| `SzServicesBackupsView` | `<sz-services-backups-view>` | Backup schedule and backup history management |
### App Store
| Component | Tag | Description |
|-----------|-----|-------------|
| `SzAppStoreView` | `<sz-app-store-view>` | App marketplace for deploying pre-configured templates (WordPress, Gitea, etc.) with category filtering |
### Platform Services
| Component | Tag | Description |
@@ -104,6 +114,20 @@ import '@serve.zone/catalog';
| `SzDnsSslCard` | `<sz-dns-ssl-card>` | Cloudflare DNS and ACME config status |
| `SzCertificatesCard` | `<sz-certificates-card>` | Certificate status counts — valid, expiring, expired |
### Routes
| Component | Tag | Description |
|-----------|-----|-------------|
| `SzRouteListView` | `<sz-route-list-view>` | Route configuration list with type filtering (HTTPS, email, DNS, etc.) |
| `SzRouteCard` | `<sz-route-card>` | Single route card — match criteria, action type, TLS mode, targets, security profile |
### MTA / Email
| Component | Tag | Description |
|-----------|-----|-------------|
| `SzMtaListView` | `<sz-mta-list-view>` | Email management — inbound/outbound messages with status badges and filtering |
| `SzMtaDetailView` | `<sz-mta-detail-view>` | Email detail — SMTP transaction log, TLS info, SPF/DKIM/DMARC results, headers, body |
### Registries
| Component | Tag | Description |
@@ -119,6 +143,13 @@ import '@serve.zone/catalog';
| `SzTokensView` | `<sz-tokens-view>` | API token management — global and CI tokens with copy/regenerate/delete |
| `SzSettingsView` | `<sz-settings-view>` | Full settings panel — appearance, Cloudflare, SSL/TLS, network, account |
### Configuration
| Component | Tag | Description |
|-----------|-----|-------------|
| `SzConfigOverview` | `<sz-config-overview>` | Top-level configuration overview with informational banner |
| `SzConfigSection` | `<sz-config-section>` | Collapsible config section — icon, enabled/disabled badge, key-value fields, action buttons |
## 🏗️ Architecture
### Component Pattern
@@ -186,8 +217,20 @@ import type { IServiceDetail, IServiceStats, ILogEntry, IServiceBackup } from '@
// Network
import type { IDomainDetail, ICertificateDetail, IDnsRecord, ITrafficTarget } from '@serve.zone/catalog';
// Routes
import type { IRouteConfig, IRouteMatch, IRouteAction, IRouteTls, IRouteSecurity } from '@serve.zone/catalog';
// MTA / Email
import type { IEmail, IEmailDetail, ISmtpLogEntry, IConnectionInfo, IAuthenticationResults } from '@serve.zone/catalog';
// Configuration
import type { IConfigField, IConfigSectionAction } from '@serve.zone/catalog';
// Settings & Auth
import type { ISettings, IToken, IExternalRegistry } from '@serve.zone/catalog';
// App Store
import type { IAppTemplate } from '@serve.zone/catalog';
```
## 🛠️ Development
@@ -210,7 +253,7 @@ The **wcctools dev server** provides an interactive dashboard where every compon
## License and Legal Information
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file.
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [license](./license) file.
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
@@ -222,7 +265,7 @@ Use of these trademarks must comply with Task Venture Capital GmbH's Trademark G
### Company Information
Task Venture Capital GmbH
Task Venture Capital GmbH
Registered at District Court Bremen HRB 35230 HB, Germany
For any legal inquiries or further information, please contact us via email at hello@task.vc.

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@serve.zone/catalog',
version: '2.9.1',
version: '2.12.1',
description: 'UI component catalog for serve.zone'
}

View File

@@ -1,5 +1,4 @@
// Dashboard Cards
export * from './sz-stat-card.js';
export * from './sz-resource-usage-card.js';
export * from './sz-traffic-card.js';
export * from './sz-platform-services-card.js';

View File

@@ -24,6 +24,13 @@ export interface IConfigSectionAction {
detail?: any;
}
export interface IConfigSectionLink {
label: string;
href: string;
icon?: string;
external?: boolean;
}
declare global {
interface HTMLElementTagNameMap {
'sz-config-section': SzConfigSection;
@@ -46,6 +53,12 @@ export class SzConfigSection extends DeesElement {
{ key: 'Auto Renew', value: true, type: 'boolean' },
{ key: 'Renew Threshold', value: '30 days' },
] as IConfigField[]}
.links=${[
{ label: 'Docs', href: 'https://code.foss.global/serve.zone/smartproxy', icon: 'lucide:bookOpen', external: true },
] as IConfigSectionLink[]}
.actions=${[
{ label: 'Configure', icon: 'lucide:settings', event: 'configure' },
] as IConfigSectionAction[]}
></sz-config-section>
<sz-config-section
title="Email Server"
@@ -57,6 +70,13 @@ export class SzConfigSection extends DeesElement {
{ key: 'Hostname', value: null },
{ key: 'Domains', value: ['example.com', 'mail.example.com'], type: 'pills' },
] as IConfigField[]}
.links=${[
{ label: 'Docs', href: 'https://code.foss.global/serve.zone/smartmta', icon: 'lucide:bookOpen', external: true },
{ label: 'Source', href: 'https://code.foss.global/serve.zone/smartmta', icon: 'lucide:github', external: true },
] as IConfigSectionLink[]}
.actions=${[
{ label: 'Enable', icon: 'lucide:power', event: 'enable' },
] as IConfigSectionAction[]}
></sz-config-section>
<sz-config-section
title="DNS Server"
@@ -68,6 +88,9 @@ export class SzConfigSection extends DeesElement {
{ key: 'Port', value: 53 },
{ key: 'NS Domains', value: ['ns1.example.com', 'ns2.example.com'], type: 'pills' },
] as IConfigField[]}
.links=${[
{ label: 'Getting Started', href: 'https://docs.example.com/dns', icon: 'lucide:bookOpen', external: true },
] as IConfigSectionLink[]}
></sz-config-section>
`;
@@ -91,6 +114,9 @@ export class SzConfigSection extends DeesElement {
@property({ type: Array })
public accessor actions: IConfigSectionAction[] = [];
@property({ type: Array })
public accessor links: IConfigSectionLink[] = [];
@property({ type: Boolean })
public accessor collapsible: boolean = false;
@@ -100,6 +126,9 @@ export class SzConfigSection extends DeesElement {
@state()
accessor isCollapsed: boolean = false;
@state()
accessor hasSlottedContent: boolean = false;
public static styles = [
cssManager.defaultStyles,
css`
@@ -108,20 +137,25 @@ export class SzConfigSection extends DeesElement {
margin-bottom: 16px;
}
.section {
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
border-radius: 8px;
overflow: hidden;
dees-tile {
display: block;
}
:host([collapsed]) dees-tile::part(content) {
display: none;
}
:host([collapsed]) dees-tile::part(footer) {
display: none;
}
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 20px;
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
border-bottom: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
padding: 10px 16px;
gap: 12px;
width: 100%;
box-sizing: border-box;
cursor: default;
user-select: none;
}
@@ -131,30 +165,31 @@ export class SzConfigSection extends DeesElement {
}
:host([collapsible]) .section-header:hover {
background: ${cssManager.bdTheme('#ebebed', '#1c1c1f')};
background: var(--dees-color-hover);
}
.header-left {
display: flex;
align-items: center;
gap: 12px;
gap: 10px;
min-width: 0;
flex: 1;
}
.header-icon {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
background: ${cssManager.bdTheme('#e4e4e7', '#27272a')};
border-radius: 8px;
width: 28px;
height: 28px;
background: var(--dees-color-border-default);
border-radius: 6px;
flex-shrink: 0;
}
.header-icon dees-icon {
font-size: 18px;
color: ${cssManager.bdTheme('#52525b', '#a1a1aa')};
font-size: 14px;
color: var(--dees-color-text-muted);
}
.header-text {
@@ -162,15 +197,17 @@ export class SzConfigSection extends DeesElement {
}
.header-title {
font-size: 15px;
font-weight: 600;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
font-size: 13px;
font-weight: 500;
letter-spacing: -0.01em;
color: var(--dees-color-text-secondary);
line-height: 1.3;
}
.header-subtitle {
font-size: 12px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
font-size: 11px;
color: var(--dees-color-text-muted);
letter-spacing: -0.01em;
line-height: 1.3;
margin-top: 1px;
}
@@ -236,30 +273,60 @@ export class SzConfigSection extends DeesElement {
background: ${cssManager.bdTheme('#f59e0b', '#fbbf24')};
}
/* Action buttons */
.header-action {
display: inline-flex;
/* Footer action buttons — canonical dees-modal / dees-tile pattern */
.section-footer {
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
gap: 5px;
padding: 4px 12px;
border-radius: 6px;
gap: 0;
height: 36px;
width: 100%;
box-sizing: border-box;
}
.tile-button {
padding: 0 16px;
height: 100%;
text-align: center;
font-size: 12px;
font-weight: 500;
color: ${cssManager.bdTheme('#2563eb', '#60a5fa')};
cursor: pointer;
user-select: none;
transition: all 0.15s ease;
background: transparent;
border: none;
cursor: pointer;
transition: background 150ms ease;
border-left: 1px solid var(--dees-color-border-subtle);
color: var(--dees-color-text-muted);
white-space: nowrap;
display: flex;
align-items: center;
gap: 6px;
font-family: inherit;
text-decoration: none;
}
.header-action:hover {
background: ${cssManager.bdTheme('rgba(37,99,235,0.08)', 'rgba(96,165,250,0.1)')};
.tile-button:first-child {
border-left: none;
}
.header-action dees-icon {
font-size: 14px;
.tile-button:hover {
background: var(--dees-color-hover);
color: var(--dees-color-text-primary);
}
.tile-button.primary {
color: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8%)', 'hsl(213.1 93.9% 67.8%)')};
font-weight: 600;
}
.tile-button.primary:hover {
background: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8% / 0.08)', 'hsl(213.1 93.9% 67.8% / 0.08)')};
color: ${cssManager.bdTheme('hsl(217.2 91.2% 50%)', 'hsl(213.1 93.9% 75%)')};
}
.tile-button dees-icon {
font-size: 12px;
}
/* Chevron */
@@ -274,8 +341,8 @@ export class SzConfigSection extends DeesElement {
}
.chevron dees-icon {
font-size: 16px;
color: ${cssManager.bdTheme('#a1a1aa', '#52525b')};
font-size: 14px;
color: var(--dees-color-text-muted);
}
/* Content */
@@ -283,8 +350,9 @@ export class SzConfigSection extends DeesElement {
padding: 0;
}
.section-content.collapsed {
display: none;
.fields-list {
display: flex;
flex-direction: column;
}
/* Field rows */
@@ -297,7 +365,7 @@ export class SzConfigSection extends DeesElement {
gap: 16px;
}
.field-row:last-child {
.fields-list .field-row:last-child {
border-bottom: none;
}
@@ -412,9 +480,8 @@ export class SzConfigSection extends DeesElement {
border-top: 1px solid ${cssManager.bdTheme('#f4f4f5', '#1a1a1e')};
}
.slot-content:empty {
.slot-content.empty {
display: none;
border-top: none;
}
/* Badge type */
@@ -439,6 +506,18 @@ export class SzConfigSection extends DeesElement {
}
}
updated(changedProperties: Map<string, any>) {
super.updated(changedProperties);
if (changedProperties.has('isCollapsed')) {
this.toggleAttribute('collapsed', this.isCollapsed);
}
}
private onSlotChange(e: Event) {
const slot = e.target as HTMLSlotElement;
this.hasSlottedContent = slot.assignedNodes({ flatten: true }).length > 0;
}
public render(): TemplateResult {
const statusLabels: Record<string, string> = {
'enabled': 'Enabled',
@@ -448,8 +527,9 @@ export class SzConfigSection extends DeesElement {
};
return html`
<div class="section">
<dees-tile>
<div
slot="header"
class="section-header"
@click=${() => {
if (this.collapsible) {
@@ -475,8 +555,40 @@ export class SzConfigSection extends DeesElement {
${statusLabels[this.status] || this.status}
</span>
` : ''}
${this.collapsible ? html`
<span class="chevron ${this.isCollapsed ? 'collapsed' : ''}">
<dees-icon .icon=${'lucide:chevronDown'}></dees-icon>
</span>
` : ''}
</div>
</div>
<div class="section-content">
${this.fields.length > 0 ? html`
<div class="fields-list">
${this.fields.map(field => this.renderField(field))}
</div>
` : ''}
<div class="slot-content ${!this.hasSlottedContent ? 'empty' : ''}">
<slot @slotchange=${this.onSlotChange}></slot>
</div>
</div>
${this.links.length > 0 || this.actions.length > 0 ? html`
<div slot="footer" class="section-footer">
${this.links.map(link => html`
<a
class="tile-button"
href=${link.href}
target=${link.external ? '_blank' : '_self'}
rel=${link.external ? 'noopener noreferrer' : ''}
@click=${(e: Event) => e.stopPropagation()}
>
${link.icon ? html`<dees-icon .icon=${link.icon}></dees-icon>` : ''}
${link.label}
${link.external ? html`<dees-icon .icon=${'lucide:externalLink'}></dees-icon>` : ''}
</a>
`)}
${this.actions.map(action => html`
<button class="header-action" @click=${(e: Event) => {
<button class="tile-button primary" @click=${(e: Event) => {
e.stopPropagation();
this.dispatchEvent(new CustomEvent(action.event || 'action', {
detail: action.detail || { label: action.label },
@@ -488,20 +600,9 @@ export class SzConfigSection extends DeesElement {
${action.label}
</button>
`)}
${this.collapsible ? html`
<span class="chevron ${this.isCollapsed ? 'collapsed' : ''}">
<dees-icon .icon=${'lucide:chevronDown'}></dees-icon>
</span>
` : ''}
</div>
</div>
<div class="section-content ${this.isCollapsed ? 'collapsed' : ''}">
${this.fields.map(field => this.renderField(field))}
<div class="slot-content">
<slot></slot>
</div>
</div>
</div>
` : ''}
</dees-tile>
`;
}

View File

@@ -229,57 +229,87 @@ export class SzDomainDetailView extends DeesElement {
}
}
.section {
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
border-radius: 8px;
overflow: hidden;
}
.section.full-width {
dees-tile.full-width {
grid-column: 1 / -1;
}
.section-header {
height: 36px;
display: flex;
justify-content: space-between;
align-items: center;
padding: 14px 16px;
border-bottom: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
padding: 0 16px;
width: 100%;
box-sizing: border-box;
}
.section-title {
font-size: 14px;
font-weight: 600;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
flex: 1;
display: flex;
align-items: center;
gap: 8px;
font-weight: 500;
font-size: 13px;
letter-spacing: -0.01em;
color: var(--dees-color-text-secondary);
}
.section-title svg {
width: 16px;
height: 16px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
width: 14px;
height: 14px;
flex-shrink: 0;
color: var(--dees-color-text-secondary);
}
.section-action {
padding: 6px 10px;
background: transparent;
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
border-radius: 4px;
.section-footer {
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
gap: 0;
height: 36px;
width: 100%;
box-sizing: border-box;
}
.tile-button {
padding: 0 16px;
height: 100%;
text-align: center;
font-size: 12px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
font-weight: 500;
cursor: pointer;
transition: all 200ms ease;
user-select: none;
transition: all 0.15s ease;
background: transparent;
border: none;
border-left: 1px solid var(--dees-color-border-subtle);
color: var(--dees-color-text-muted);
white-space: nowrap;
display: flex;
align-items: center;
gap: 6px;
}
.section-action:hover {
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
.tile-button:first-child {
border-left: none;
}
.tile-button:hover {
background: var(--dees-color-hover);
color: var(--dees-color-text-primary);
}
.tile-button.primary {
color: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8%)', 'hsl(213.1 93.9% 67.8%)')};
font-weight: 600;
}
.tile-button.primary:hover {
background: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8% / 0.08)', 'hsl(213.1 93.9% 67.8% / 0.08)')};
color: ${cssManager.bdTheme('hsl(217.2 91.2% 50%)', 'hsl(213.1 93.9% 75%)')};
}
.section-content {
padding: 16px;
}
@@ -582,8 +612,8 @@ export class SzDomainDetailView extends DeesElement {
<div class="grid">
<!-- Certificate Section -->
<div class="section">
<div class="section-header">
<dees-tile>
<div slot="header" class="section-header">
<div class="section-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
@@ -591,9 +621,6 @@ export class SzDomainDetailView extends DeesElement {
</svg>
SSL Certificate
</div>
${this.certificate ? html`
<button class="section-action" @click=${() => this.handleRenewCertificate()}>Renew</button>
` : ''}
</div>
<div class="section-content">
${this.certificate ? html`
@@ -652,11 +679,16 @@ export class SzDomainDetailView extends DeesElement {
<div class="empty-state">No certificate configured</div>
`}
</div>
</div>
${this.certificate ? html`
<div slot="footer" class="section-footer">
<button class="tile-button primary" @click=${() => this.handleRenewCertificate()}>Renew</button>
</div>
` : ''}
</dees-tile>
<!-- Proxy Routes Section -->
<div class="section">
<div class="section-header">
<dees-tile>
<div slot="header" class="section-header">
<div class="section-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="16 3 21 3 21 8"></polyline>
@@ -679,11 +711,11 @@ export class SzDomainDetailView extends DeesElement {
<div class="empty-state">No proxy routes configured</div>
`}
</div>
</div>
</dees-tile>
<!-- DNS Records Section -->
<div class="section full-width">
<div class="section-header">
<dees-tile class="full-width">
<div slot="header" class="section-header">
<div class="section-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"></circle>
@@ -692,13 +724,6 @@ export class SzDomainDetailView extends DeesElement {
</svg>
DNS Records
</div>
<button class="section-action" @click=${() => this.handleAddDnsRecord()}>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 4px;">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
Add Record
</button>
</div>
<div class="section-content">
${this.dnsRecords.length > 0 ? html`
@@ -737,7 +762,16 @@ export class SzDomainDetailView extends DeesElement {
<div class="empty-state">No DNS records configured</div>
`}
</div>
</div>
<div slot="footer" class="section-footer">
<button class="tile-button primary" @click=${() => this.handleAddDnsRecord()}>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
Add Record
</button>
</div>
</dees-tile>
</div>
`;
}

View File

@@ -256,31 +256,89 @@ export class SzMtaDetailView extends DeesElement {
gap: 24px;
}
.card {
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
border-radius: 8px;
overflow: hidden;
.card-header {
height: 36px;
display: flex;
align-items: center;
padding: 0 16px;
width: 100%;
box-sizing: border-box;
}
.card-header {
.card-heading {
flex: 1;
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
border-bottom: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
align-items: baseline;
gap: 8px;
min-width: 0;
}
.card-title {
font-size: 16px;
font-weight: 600;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
font-weight: 500;
font-size: 13px;
letter-spacing: -0.01em;
color: var(--dees-color-text-secondary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.card-subtitle {
font-size: 13px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
margin-top: 2px;
font-size: 12px;
color: var(--dees-color-text-muted);
letter-spacing: -0.01em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.card-footer {
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
gap: 0;
height: 36px;
width: 100%;
box-sizing: border-box;
}
.tile-button {
padding: 0 16px;
height: 100%;
text-align: center;
font-size: 12px;
font-weight: 500;
cursor: pointer;
user-select: none;
transition: all 0.15s ease;
background: transparent;
border: none;
border-left: 1px solid var(--dees-color-border-subtle);
color: var(--dees-color-text-muted);
white-space: nowrap;
display: flex;
align-items: center;
gap: 6px;
}
.tile-button:first-child {
border-left: none;
}
.tile-button:hover {
background: var(--dees-color-hover);
color: var(--dees-color-text-primary);
}
.tile-button.primary {
color: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8%)', 'hsl(213.1 93.9% 67.8%)')};
font-weight: 600;
}
.tile-button.primary:hover {
background: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8% / 0.08)', 'hsl(213.1 93.9% 67.8% / 0.08)')};
color: ${cssManager.bdTheme('hsl(217.2 91.2% 50%)', 'hsl(213.1 93.9% 75%)')};
}
.card-content {
@@ -451,37 +509,13 @@ export class SzMtaDetailView extends DeesElement {
color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
}
/* Copy button */
.smtp-copy-button {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
border-radius: 6px;
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
background: transparent;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: all 150ms ease;
}
.smtp-copy-button:hover {
background: ${cssManager.bdTheme('#f4f4f5', '#27272a')};
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
border-color: ${cssManager.bdTheme('#d4d4d8', '#3f3f46')};
}
/* Header subtitle enhancements */
/* SMTP metadata banner — sits inside content, above the log */
.smtp-header-subtitle {
font-size: 13px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
margin-top: 2px;
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
padding: 10px 16px;
font-size: 12px;
color: var(--dees-color-text-muted);
border-bottom: 1px solid var(--dees-color-border-subtle);
font-family: monospace;
}
.smtp-direction-badge {
@@ -578,7 +612,7 @@ export class SzMtaDetailView extends DeesElement {
color: ${cssManager.bdTheme('#ca8a04', '#facc15')};
}
.rejection-card {
dees-tile.rejection-card::part(outer) {
border-color: ${cssManager.bdTheme('#fecaca', 'rgba(239, 68, 68, 0.3)')};
}
@@ -646,9 +680,11 @@ export class SzMtaDetailView extends DeesElement {
<div class="content">
<div class="main-content">
<!-- Email Metadata -->
<div class="card">
<div class="card-header">
<div class="card-title">Email Metadata</div>
<dees-tile>
<div slot="header" class="card-header">
<div class="card-heading">
<span class="card-title">Email Metadata</span>
</div>
</div>
<div class="card-content">
<div class="detail-list">
@@ -684,49 +720,53 @@ export class SzMtaDetailView extends DeesElement {
</div>
</div>
</div>
</div>
</dees-tile>
<!-- SMTP Transaction Log -->
<div class="card">
<div class="card-header">
<div>
<div class="card-title">SMTP Transaction Log</div>
<div class="smtp-header-subtitle">
<span class="smtp-direction-badge ${email.direction}">${email.direction}</span>
<span>${email.direction === 'outbound'
? `${email.connectionInfo.sourceHostname}${email.connectionInfo.destinationIp}:${email.connectionInfo.destinationPort}`
: `${email.connectionInfo.sourceIp}${email.connectionInfo.sourceHostname}:${email.connectionInfo.destinationPort}`
}</span>
</div>
<dees-tile>
<div slot="header" class="card-header">
<div class="card-heading">
<span class="card-title">SMTP Transaction Log</span>
<span class="smtp-direction-badge ${email.direction}">${email.direction}</span>
</div>
<button class="smtp-copy-button" @click=${() => this.copySmtpLog()}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
</div>
<div class="smtp-header-subtitle">
${email.direction === 'outbound'
? `${email.connectionInfo.sourceHostname}${email.connectionInfo.destinationIp}:${email.connectionInfo.destinationPort}`
: `${email.connectionInfo.sourceIp}${email.connectionInfo.sourceHostname}:${email.connectionInfo.destinationPort}`
}
</div>
${this.renderSmtpLog(email)}
<div slot="footer" class="card-footer">
<button class="tile-button" @click=${() => this.copySmtpLog()}>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
Copy Log
</button>
</div>
${this.renderSmtpLog(email)}
</div>
</dees-tile>
<!-- Email Body -->
<div class="card">
<div class="card-header">
<div>
<div class="card-title">Email Body (Escaped)</div>
<div class="card-subtitle">Raw content — HTML is not rendered</div>
<dees-tile>
<div slot="header" class="card-header">
<div class="card-heading">
<span class="card-title">Email Body (Escaped)</span>
<span class="card-subtitle">Raw content — HTML is not rendered</span>
</div>
</div>
<pre class="email-body-container">${email.body}</pre>
</div>
</dees-tile>
</div>
<div class="sidebar">
<!-- Connection Info -->
<div class="card">
<div class="card-header">
<div class="card-title">Connection Info</div>
<dees-tile>
<div slot="header" class="card-header">
<div class="card-heading">
<span class="card-title">Connection Info</span>
</div>
</div>
<div class="card-content">
<div class="detail-list">
@@ -772,12 +812,14 @@ export class SzMtaDetailView extends DeesElement {
` : ''}
</div>
</div>
</div>
</dees-tile>
<!-- Authentication Results -->
<div class="card">
<div class="card-header">
<div class="card-title">Authentication Results</div>
<dees-tile>
<div slot="header" class="card-header">
<div class="card-heading">
<span class="card-title">Authentication Results</span>
</div>
</div>
<div class="card-content">
<div class="auth-row">
@@ -802,13 +844,15 @@ export class SzMtaDetailView extends DeesElement {
<span class="auth-badge ${email.authenticationResults.dmarc}">${email.authenticationResults.dmarc}</span>
</div>
</div>
</div>
</dees-tile>
<!-- Rejection Details (conditional) -->
${email.status === 'rejected' || email.status === 'bounced' ? html`
<div class="card rejection-card">
<div class="card-header">
<div class="card-title">Rejection Details</div>
<dees-tile class="rejection-card">
<div slot="header" class="card-header">
<div class="card-heading">
<span class="card-title">Rejection Details</span>
</div>
</div>
<div class="card-content">
${email.rejectionReason ? html`
@@ -820,7 +864,7 @@ export class SzMtaDetailView extends DeesElement {
<div class="rejection-text">${email.bounceMessage}</div>
` : ''}
</div>
</div>
</dees-tile>
` : ''}
</div>
</div>

View File

@@ -7,8 +7,7 @@ import {
property,
type TemplateResult,
} from '@design.estate/dees-element';
import './sz-stat-card.js';
import type { IStatsTile } from '@design.estate/dees-catalog';
declare global {
interface HTMLElementTagNameMap {
@@ -91,19 +90,11 @@ export class SzNetworkDomainsView extends DeesElement {
border-color: ${cssManager.bdTheme('#d4d4d8', '#3f3f46')};
}
.stats-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
dees-statsgrid {
display: block;
margin-bottom: 24px;
}
@media (min-width: 768px) {
.stats-grid {
grid-template-columns: repeat(4, 1fr);
}
}
.table-container {
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
@@ -205,6 +196,42 @@ export class SzNetworkDomainsView extends DeesElement {
`,
];
private get tiles(): IStatsTile[] {
return [
{
id: 'total',
title: 'Total Domains',
value: this.stats.total,
type: 'number',
icon: 'lucide:globe',
},
{
id: 'valid',
title: 'Valid Certificates',
value: this.stats.valid,
type: 'number',
icon: 'lucide:shieldCheck',
color: '#22c55e',
},
{
id: 'expiring',
title: 'Expiring Soon',
value: this.stats.expiring,
type: 'number',
icon: 'lucide:shieldAlert',
color: this.stats.expiring > 0 ? '#f59e0b' : undefined,
},
{
id: 'expired',
title: 'Expired/Pending',
value: this.stats.expired,
type: 'number',
icon: 'lucide:circleOff',
color: this.stats.expired > 0 ? '#ef4444' : undefined,
},
];
}
public render(): TemplateResult {
return html`
<div class="header">
@@ -212,31 +239,10 @@ export class SzNetworkDomainsView extends DeesElement {
<button class="sync-button" @click=${() => this.handleSync()}>Sync Cloudflare</button>
</div>
<div class="stats-grid">
<sz-stat-card
label="Total Domains"
value="${this.stats.total}"
icon="server"
></sz-stat-card>
<sz-stat-card
label="Valid Certificates"
value="${this.stats.valid}"
icon="check"
variant="success"
></sz-stat-card>
<sz-stat-card
label="Expiring Soon"
value="${this.stats.expiring}"
icon="stop"
variant="${this.stats.expiring > 0 ? 'warning' : 'default'}"
></sz-stat-card>
<sz-stat-card
label="Expired/Pending"
value="${this.stats.expired}"
icon="stop"
variant="${this.stats.expired > 0 ? 'error' : 'default'}"
></sz-stat-card>
</div>
<dees-statsgrid
.tiles=${this.tiles}
.minTileWidth=${200}
></dees-statsgrid>
<div class="table-container">
<div class="table-header">

View File

@@ -7,8 +7,7 @@ import {
property,
type TemplateResult,
} from '@design.estate/dees-element';
import './sz-stat-card.js';
import type { IStatsTile } from '@design.estate/dees-catalog';
declare global {
interface HTMLElementTagNameMap {
@@ -113,42 +112,107 @@ export class SzNetworkProxyView extends DeesElement {
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
}
.stats-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
dees-statsgrid {
display: block;
margin-bottom: 24px;
}
@media (min-width: 768px) {
.stats-grid {
grid-template-columns: repeat(4, 1fr);
}
}
.section {
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
border-radius: 8px;
dees-tile {
display: block;
margin-bottom: 24px;
overflow: hidden;
}
.section-header {
padding: 16px;
border-bottom: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
height: 36px;
display: flex;
align-items: center;
padding: 0 16px;
width: 100%;
box-sizing: border-box;
}
.section-heading {
flex: 1;
display: flex;
align-items: baseline;
gap: 8px;
min-width: 0;
}
.section-title {
font-size: 16px;
font-weight: 600;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
font-weight: 500;
font-size: 13px;
letter-spacing: -0.01em;
color: var(--dees-color-text-secondary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.section-subtitle {
font-size: 13px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
margin-top: 2px;
font-size: 12px;
color: var(--dees-color-text-muted);
letter-spacing: -0.01em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.section-footer {
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
gap: 0;
height: 36px;
width: 100%;
box-sizing: border-box;
}
.tile-button {
padding: 0 16px;
height: 100%;
text-align: center;
font-size: 12px;
font-weight: 500;
cursor: pointer;
user-select: none;
transition: all 0.15s ease;
background: transparent;
border: none;
border-left: 1px solid var(--dees-color-border-subtle);
color: var(--dees-color-text-muted);
white-space: nowrap;
display: flex;
align-items: center;
gap: 6px;
}
.tile-button:first-child {
border-left: none;
}
.tile-button:hover {
background: var(--dees-color-hover);
color: var(--dees-color-text-primary);
}
.tile-button.primary {
color: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8%)', 'hsl(213.1 93.9% 67.8%)')};
font-weight: 600;
}
.tile-button.primary:hover {
background: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8% / 0.08)', 'hsl(213.1 93.9% 67.8% / 0.08)')};
color: ${cssManager.bdTheme('hsl(217.2 91.2% 50%)', 'hsl(213.1 93.9% 75%)')};
}
.tile-button.danger {
color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
}
.tile-button.danger:hover {
background: ${cssManager.bdTheme('#fee2e2', 'rgba(239, 68, 68, 0.1)')};
}
.table-header {
@@ -234,61 +298,6 @@ export class SzNetworkProxyView extends DeesElement {
color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
}
.logs-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
border-bottom: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
}
.logs-actions {
display: flex;
gap: 8px;
}
.stream-button {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
background: ${cssManager.bdTheme('#2563eb', '#3b82f6')};
border: none;
border-radius: 4px;
font-size: 13px;
font-weight: 500;
color: white;
cursor: pointer;
transition: all 200ms ease;
}
.stream-button:hover {
background: ${cssManager.bdTheme('#1d4ed8', '#2563eb')};
}
.stream-button.streaming {
background: ${cssManager.bdTheme('#dc2626', '#ef4444')};
}
.stream-button.streaming:hover {
background: ${cssManager.bdTheme('#b91c1c', '#dc2626')};
}
.clear-button {
padding: 6px 12px;
background: transparent;
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
border-radius: 4px;
font-size: 13px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
cursor: pointer;
transition: all 200ms ease;
}
.clear-button:hover {
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
}
.logs-container {
padding: 16px;
font-family: monospace;
@@ -336,41 +345,57 @@ export class SzNetworkProxyView extends DeesElement {
`,
];
private get tiles(): IStatsTile[] {
return [
{
id: 'proxy-status',
title: 'Proxy Status',
value: this.proxyStatus === 'running' ? 'Running' : 'Stopped',
type: 'text',
icon: 'lucide:server',
color: this.proxyStatus === 'running' ? '#22c55e' : '#ef4444',
},
{
id: 'routes',
title: 'Routes',
value: this.routeCount,
type: 'number',
icon: 'lucide:server',
},
{
id: 'certificates',
title: 'Certificates',
value: this.certificateCount,
type: 'number',
icon: 'lucide:check',
},
{
id: 'targets',
title: 'Targets',
value: this.targetCount,
type: 'number',
icon: 'lucide:server',
},
];
}
public render(): TemplateResult {
return html`
<div class="actions">
<button class="refresh-button" @click=${() => this.handleRefresh()}>Refresh</button>
</div>
<div class="stats-grid">
<sz-stat-card
label="Proxy Status"
value="${this.proxyStatus === 'running' ? 'Running' : 'Stopped'}"
icon="server"
variant="${this.proxyStatus === 'running' ? 'success' : 'error'}"
valueBadge
></sz-stat-card>
<sz-stat-card
label="Routes"
value="${this.routeCount}"
icon="server"
></sz-stat-card>
<sz-stat-card
label="Certificates"
value="${this.certificateCount}"
icon="check"
></sz-stat-card>
<sz-stat-card
label="Targets"
value="${this.targetCount}"
icon="server"
></sz-stat-card>
</div>
<dees-statsgrid
.tiles=${this.tiles}
.minTileWidth=${200}
></dees-statsgrid>
<div class="section">
<div class="section-header">
<div class="section-title">Traffic Targets</div>
<div class="section-subtitle">Services, registry, and platform services with their routing info</div>
<dees-tile>
<div slot="header" class="section-header">
<div class="section-heading">
<span class="section-title">Traffic Targets</span>
<span class="section-subtitle">Services, registry, and platform services with their routing info</span>
</div>
</div>
<div class="table-header">
<span>Type</span>
@@ -388,30 +413,13 @@ export class SzNetworkProxyView extends DeesElement {
<span><span class="status-badge ${target.status}">${target.status}</span></span>
</div>
`)}
</div>
</dees-tile>
<div class="section">
<div class="logs-header">
<div>
<div class="section-title">Access Logs</div>
<div class="section-subtitle">Real-time Caddy access logs</div>
</div>
<div class="logs-actions">
<button class="stream-button ${this.streaming ? 'streaming' : ''}" @click=${() => this.toggleStreaming()}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
${this.streaming
? html`<rect x="6" y="6" width="12" height="12" rx="1"/>`
: html`<polygon points="5,3 19,12 5,21"/>`
}
</svg>
${this.streaming ? 'Stop' : 'Stream'}
</button>
<button class="clear-button" @click=${() => this.handleClearLogs()}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="3,6 5,6 21,6"/><path d="M19,6v14a2,2,0,0,1-2,2H7a2,2,0,0,1-2-2V6m3,0V4a2,2,0,0,1,2-2h4a2,2,0,0,1,2,2v2"/>
</svg>
Clear logs
</button>
<dees-tile>
<div slot="header" class="section-header">
<div class="section-heading">
<span class="section-title">Access Logs</span>
<span class="section-subtitle">Real-time Caddy access logs</span>
</div>
</div>
<div class="logs-container">
@@ -428,7 +436,24 @@ export class SzNetworkProxyView extends DeesElement {
<div class="empty-logs">Click "Stream" to start live access log streaming</div>
`}
</div>
</div>
<div slot="footer" class="section-footer">
<button class="tile-button ${this.streaming ? 'danger' : 'primary'}" @click=${() => this.toggleStreaming()}>
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor">
${this.streaming
? html`<rect x="6" y="6" width="12" height="12" rx="1"/>`
: html`<polygon points="5,3 19,12 5,21"/>`
}
</svg>
${this.streaming ? 'Stop' : 'Stream'}
</button>
<button class="tile-button" @click=${() => this.handleClearLogs()}>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="3,6 5,6 21,6"/><path d="M19,6v14a2,2,0,0,1-2,2H7a2,2,0,0,1-2-2V6m3,0V4a2,2,0,0,1,2-2h4a2,2,0,0,1,2,2v2"/>
</svg>
Clear logs
</button>
</div>
</dees-tile>
`;
}

View File

@@ -225,39 +225,35 @@ export class SzPlatformServiceDetailView extends DeesElement {
}
}
.section {
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
border-radius: 8px;
overflow: hidden;
}
.section.full-width {
dees-tile.full-width {
grid-column: 1 / -1;
}
.section-header {
height: 36px;
display: flex;
justify-content: space-between;
align-items: center;
padding: 14px 16px;
border-bottom: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
padding: 0 16px;
width: 100%;
box-sizing: border-box;
}
.section-title {
font-size: 14px;
font-weight: 600;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
flex: 1;
display: flex;
align-items: center;
gap: 8px;
font-weight: 500;
font-size: 13px;
letter-spacing: -0.01em;
color: var(--dees-color-text-secondary);
}
.section-title svg {
width: 16px;
height: 16px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
width: 14px;
height: 14px;
flex-shrink: 0;
color: var(--dees-color-text-secondary);
}
.section-content {
@@ -452,8 +448,8 @@ export class SzPlatformServiceDetailView extends DeesElement {
<div class="grid">
<!-- Connection Info -->
<div class="section">
<div class="section-header">
<dees-tile>
<div slot="header" class="section-header">
<div class="section-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
@@ -516,11 +512,11 @@ export class SzPlatformServiceDetailView extends DeesElement {
</div>
` : ''}
</div>
</div>
</dees-tile>
<!-- Configuration -->
<div class="section">
<div class="section-header">
<dees-tile>
<div slot="header" class="section-header">
<div class="section-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="3"></circle>
@@ -537,12 +533,12 @@ export class SzPlatformServiceDetailView extends DeesElement {
</div>
`)}
</div>
</div>
</dees-tile>
<!-- Metrics -->
${this.service.metrics ? html`
<div class="section full-width">
<div class="section-header">
<dees-tile class="full-width">
<div slot="header" class="section-header">
<div class="section-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="20" x2="18" y2="10"></line>
@@ -583,7 +579,7 @@ export class SzPlatformServiceDetailView extends DeesElement {
` : ''}
</div>
</div>
</div>
</dees-tile>
` : ''}
<!-- Logs -->

View File

@@ -56,9 +56,9 @@ export interface IRouteSecurity {
}
export interface IRouteMetadata {
securityProfileRef?: string;
sourceProfileRef?: string;
networkTargetRef?: string;
securityProfileName?: string;
sourceProfileName?: string;
networkTargetName?: string;
lastResolvedAt?: number;
}
@@ -70,6 +70,8 @@ export interface IRouteConfig {
security?: IRouteSecurity;
headers?: { request?: Record<string, string>; response?: Record<string, string> };
metadata?: IRouteMetadata;
/** When true, only VPN clients whose TargetProfile matches this route get access */
vpnOnly?: boolean;
name?: string;
description?: string;
priority?: number;
@@ -136,8 +138,9 @@ export class SzRouteCard extends DeesElement {
rateLimit: { enabled: true, maxRequests: 100, window: 60 },
maxConnections: 1000,
},
vpnOnly: true,
metadata: {
securityProfileName: 'STANDARD',
sourceProfileName: 'STANDARD',
networkTargetName: 'LOSSLESS_INFRA',
},
} satisfies IRouteConfig}
@@ -150,6 +153,9 @@ export class SzRouteCard extends DeesElement {
@property({ type: Object })
public accessor route: IRouteConfig | null = null;
@property({ type: Boolean })
public accessor showActions: boolean = false;
public static styles = [
cssManager.defaultStyles,
css`
@@ -459,6 +465,83 @@ export class SzRouteCard extends DeesElement {
color: ${cssManager.bdTheme('#a1a1aa', '#52525b')};
font-size: 13px;
}
.section.vpn {
border-left-color: ${cssManager.bdTheme('#0891b2', '#06b6d4')};
}
.vpn-badge {
display: inline-flex;
align-items: center;
padding: 2px 8px;
border-radius: 9999px;
font-size: 11px;
font-weight: 600;
white-space: nowrap;
}
.vpn-badge.mandatory {
background: ${cssManager.bdTheme('#fff7ed', 'rgba(249, 115, 22, 0.2)')};
color: ${cssManager.bdTheme('#c2410c', '#fb923c')};
}
.vpn-badge.optional {
background: ${cssManager.bdTheme('#ecfdf5', 'rgba(16, 185, 129, 0.2)')};
color: ${cssManager.bdTheme('#047857', '#34d399')};
}
.vpn-tag {
display: inline-flex;
padding: 1px 6px;
border-radius: 3px;
font-size: 12px;
font-weight: 600;
font-family: monospace;
margin-right: 4px;
margin-bottom: 2px;
background: ${cssManager.bdTheme('#ecfeff', 'rgba(6, 182, 212, 0.15)')};
color: ${cssManager.bdTheme('#0e7490', '#22d3ee')};
}
.card-actions {
display: flex;
gap: 8px;
margin-top: 14px;
padding-top: 12px;
border-top: 1px solid ${cssManager.bdTheme('#f4f4f5', '#1a1a1a')};
justify-content: flex-end;
}
.action-btn {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 5px 12px;
border-radius: 6px;
font-size: 12px;
font-weight: 500;
cursor: pointer;
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
color: ${cssManager.bdTheme('#52525b', '#a1a1aa')};
transition: all 150ms ease;
}
.action-btn:hover {
border-color: ${cssManager.bdTheme('#d4d4d8', '#3f3f46')};
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
}
.action-btn.edit:hover {
border-color: ${cssManager.bdTheme('#93c5fd', 'rgba(59, 130, 246, 0.5)')};
color: ${cssManager.bdTheme('#2563eb', '#60a5fa')};
}
.action-btn.delete:hover {
border-color: ${cssManager.bdTheme('#fca5a5', 'rgba(239, 68, 68, 0.5)')};
color: ${cssManager.bdTheme('#dc2626', '#f87171')};
}
`,
];
@@ -652,11 +735,36 @@ export class SzRouteCard extends DeesElement {
`
: ''}
<!-- VPN Section -->
${this.renderVpn()}
<!-- Linked References Section -->
${this.renderLinked()}
<!-- Feature Icons Row -->
${this.renderFeatures()}
<!-- Action Buttons -->
${this.showActions ? html`
<div class="card-actions">
<button class="action-btn edit" @click=${(e: Event) => {
e.stopPropagation();
this.dispatchEvent(new CustomEvent('route-edit', {
detail: this.route,
bubbles: true,
composed: true,
}));
}}>Edit</button>
<button class="action-btn delete" @click=${(e: Event) => {
e.stopPropagation();
this.dispatchEvent(new CustomEvent('route-delete', {
detail: this.route,
bubbles: true,
composed: true,
}));
}}>Delete</button>
</div>
` : ''}
</div>
`;
}
@@ -669,10 +777,26 @@ export class SzRouteCard extends DeesElement {
)}`;
}
private renderVpn(): TemplateResult {
if (!this.route?.vpnOnly) return html``;
return html`
<div class="section vpn">
<div class="section-label">VPN Access</div>
<div class="field-row">
<span class="field-key">Mode</span>
<span class="field-value">
<span class="vpn-badge mandatory">VPN Only</span>
</span>
</div>
</div>
`;
}
private renderLinked(): TemplateResult {
const meta = this.route?.metadata;
if (!meta) return html``;
const hasProfile = !!meta.securityProfileName;
const hasProfile = !!meta.sourceProfileName;
const hasTarget = !!meta.networkTargetName;
if (!hasProfile && !hasTarget) return html``;
@@ -683,7 +807,7 @@ export class SzRouteCard extends DeesElement {
? html`
<div class="field-row">
<span class="field-key">Profile</span>
<span class="field-value"><span class="linked-name">${meta.securityProfileName}</span></span>
<span class="field-value"><span class="linked-name">${meta.sourceProfileName}</span></span>
</div>
`
: ''}
@@ -722,7 +846,10 @@ export class SzRouteCard extends DeesElement {
if (headers) {
features.push(html`<span class="feature"><span class="feature-icon">&#x2699;</span>Headers</span>`);
}
if (meta?.securityProfileName || meta?.networkTargetName) {
if (this.route?.vpnOnly) {
features.push(html`<span class="feature"><span class="feature-icon">&#x1f510;</span>VPN</span>`);
}
if (meta?.sourceProfileName || meta?.networkTargetName) {
features.push(html`<span class="feature"><span class="feature-icon">&#x1f517;</span>Linked</span>`);
}

View File

@@ -76,6 +76,9 @@ export class SzRouteListView extends DeesElement {
@property({ type: Array })
public accessor routes: IRouteConfig[] = [];
@property({ attribute: false })
public accessor showActionsFilter: ((route: IRouteConfig) => boolean) | null = null;
@state()
private accessor searchQuery: string = '';
@@ -299,6 +302,7 @@ export class SzRouteListView extends DeesElement {
(route) => html`
<sz-route-card
.route=${route}
.showActions=${this.showActionsFilter?.(route) ?? false}
@click=${() => this.handleRouteClick(route)}
></sz-route-card>
`

View File

@@ -11,8 +11,6 @@ import {
import type { IExecutionEnvironment } from '@design.estate/dees-catalog';
import './sz-stat-card.js';
declare global {
interface HTMLElementTagNameMap {
'sz-service-detail-view': SzServiceDetailView;
@@ -218,31 +216,97 @@ export class SzServiceDetailView extends DeesElement {
gap: 24px;
}
.card {
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
border-radius: 8px;
overflow: hidden;
.card-header {
height: 36px;
display: flex;
align-items: center;
padding: 0 8px 0 16px;
width: 100%;
box-sizing: border-box;
}
.card-header {
.card-heading {
flex: 1;
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
border-bottom: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
align-items: baseline;
gap: 8px;
min-width: 0;
}
.card-title {
font-size: 16px;
font-weight: 600;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
font-weight: 500;
font-size: 13px;
letter-spacing: -0.01em;
color: var(--dees-color-text-secondary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.card-subtitle {
font-size: 13px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
margin-top: 2px;
font-size: 12px;
color: var(--dees-color-text-muted);
letter-spacing: -0.01em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.card-footer {
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
gap: 0;
height: 36px;
width: 100%;
box-sizing: border-box;
}
.tile-button {
padding: 0 16px;
height: 100%;
text-align: center;
font-size: 12px;
font-weight: 500;
cursor: pointer;
user-select: none;
transition: all 0.15s ease;
background: transparent;
border: none;
border-left: 1px solid var(--dees-color-border-subtle);
color: var(--dees-color-text-muted);
white-space: nowrap;
display: flex;
align-items: center;
gap: 6px;
}
.tile-button:first-child {
border-left: none;
}
.tile-button:hover {
background: var(--dees-color-hover);
color: var(--dees-color-text-primary);
}
.tile-button.primary {
color: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8%)', 'hsl(213.1 93.9% 67.8%)')};
font-weight: 600;
}
.tile-button.primary:hover {
background: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8% / 0.08)', 'hsl(213.1 93.9% 67.8% / 0.08)')};
color: ${cssManager.bdTheme('hsl(217.2 91.2% 50%)', 'hsl(213.1 93.9% 75%)')};
}
.tile-button.danger {
color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
}
.tile-button.danger:hover {
background: ${cssManager.bdTheme('#fee2e2', 'rgba(239, 68, 68, 0.1)')};
}
.card-content {
@@ -518,12 +582,11 @@ export class SzServiceDetailView extends DeesElement {
<div class="content">
<div class="main-content">
<div class="card">
<div class="card-header">
<div>
<div class="card-title">Service Details</div>
<dees-tile>
<div slot="header" class="card-header">
<div class="card-heading">
<span class="card-title">Service Details</span>
</div>
<button class="action-button" style="width: auto; padding: 6px 12px;" @click=${() => this.handleEdit()}>Edit</button>
</div>
<div class="card-content">
<div class="detail-list">
@@ -557,7 +620,10 @@ export class SzServiceDetailView extends DeesElement {
</div>
</div>
</div>
</div>
<div slot="footer" class="card-footer">
<button class="tile-button primary" @click=${() => this.handleEdit()}>Edit</button>
</div>
</dees-tile>
<dees-chart-log
.label=${'Service Logs'}
@@ -573,9 +639,11 @@ export class SzServiceDetailView extends DeesElement {
</div>
<div class="sidebar">
<div class="card">
<div class="card-header">
<div class="card-title">Live stats</div>
<dees-tile>
<div slot="header" class="card-header">
<div class="card-heading">
<span class="card-title">Live stats</span>
</div>
</div>
<div class="card-content">
<div class="stats-grid">
@@ -598,13 +666,13 @@ export class SzServiceDetailView extends DeesElement {
</div>
</div>
</div>
</div>
</dees-tile>
<div class="card">
<div class="card-header">
<div>
<div class="card-title">Actions</div>
<div class="card-subtitle">Manage service state</div>
<dees-tile>
<div slot="header" class="card-header">
<div class="card-heading">
<span class="card-title">Actions</span>
<span class="card-subtitle">Manage service state</span>
</div>
</div>
<div class="card-content">
@@ -624,13 +692,13 @@ export class SzServiceDetailView extends DeesElement {
<button class="action-button danger" @click=${() => this.handleAction('delete')}>Delete Service</button>
</div>
</div>
</div>
</dees-tile>
<div class="card">
<div class="card-header">
<div>
<div class="card-title">Image Source</div>
<div class="card-subtitle">${this.service.registry === 'Docker Hub' ? 'External container registry' : 'Onebox registry'}</div>
<dees-tile>
<div slot="header" class="card-header">
<div class="card-heading">
<span class="card-title">Image Source</span>
<span class="card-subtitle">${this.service.registry === 'Docker Hub' ? 'External container registry' : 'Onebox registry'}</span>
</div>
</div>
<div class="card-content">
@@ -649,21 +717,14 @@ export class SzServiceDetailView extends DeesElement {
</div>
</div>
</div>
</div>
</dees-tile>
<div class="card">
<div class="card-header">
<div>
<div class="card-title">Backups</div>
<div class="card-subtitle">Create and manage service backups</div>
<dees-tile>
<div slot="header" class="card-header">
<div class="card-heading">
<span class="card-title">Backups</span>
<span class="card-subtitle">Create and manage service backups</span>
</div>
<button class="action-button" style="width: auto; padding: 6px 12px;" @click=${() => this.handleCreateBackup()}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 4px;">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
Create Backup
</button>
</div>
<div class="card-content">
<div class="backup-list">
@@ -698,7 +759,16 @@ export class SzServiceDetailView extends DeesElement {
`)}
</div>
</div>
</div>
<div slot="footer" class="card-footer">
<button class="tile-button primary" @click=${() => this.handleCreateBackup()}>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
Create Backup
</button>
</div>
</dees-tile>
</div>
</div>
`;

View File

@@ -65,71 +65,94 @@ export class SzServicesBackupsView extends DeesElement {
display: block;
}
.section {
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
border-radius: 8px;
dees-tile {
display: block;
margin-bottom: 24px;
overflow: hidden;
}
.section-header {
height: 36px;
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 16px;
border-bottom: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
align-items: center;
padding: 0 16px;
width: 100%;
box-sizing: border-box;
}
.section-info {
.section-heading {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
align-items: baseline;
gap: 8px;
min-width: 0;
}
.section-title {
font-size: 16px;
font-weight: 600;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
font-weight: 500;
font-size: 13px;
letter-spacing: -0.01em;
color: var(--dees-color-text-secondary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.section-subtitle {
font-size: 13px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
font-size: 12px;
color: var(--dees-color-text-muted);
letter-spacing: -0.01em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.header-actions {
.section-footer {
display: flex;
gap: 8px;
flex-direction: row;
justify-content: flex-end;
align-items: center;
gap: 0;
height: 36px;
width: 100%;
box-sizing: border-box;
}
.action-button {
display: inline-flex;
.tile-button {
padding: 0 16px;
height: 100%;
text-align: center;
font-size: 12px;
font-weight: 500;
cursor: pointer;
user-select: none;
transition: all 0.15s ease;
background: transparent;
border: none;
border-left: 1px solid var(--dees-color-border-subtle);
color: var(--dees-color-text-muted);
white-space: nowrap;
display: flex;
align-items: center;
gap: 6px;
padding: 8px 14px;
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
border-radius: 6px;
font-size: 13px;
font-weight: 500;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
cursor: pointer;
transition: all 200ms ease;
}
.action-button:hover {
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
.tile-button:first-child {
border-left: none;
}
.action-button.primary {
background: ${cssManager.bdTheme('#18181b', '#fafafa')};
color: ${cssManager.bdTheme('#fafafa', '#18181b')};
border: none;
.tile-button:hover {
background: var(--dees-color-hover);
color: var(--dees-color-text-primary);
}
.action-button.primary:hover {
opacity: 0.9;
.tile-button.primary {
color: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8%)', 'hsl(213.1 93.9% 67.8%)')};
font-weight: 600;
}
.tile-button.primary:hover {
background: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8% / 0.08)', 'hsl(213.1 93.9% 67.8% / 0.08)')};
color: ${cssManager.bdTheme('hsl(217.2 91.2% 50%)', 'hsl(213.1 93.9% 75%)')};
}
.table-header {
@@ -253,28 +276,11 @@ export class SzServicesBackupsView extends DeesElement {
public render(): TemplateResult {
return html`
<div class="section">
<div class="section-header">
<div class="section-info">
<div class="section-title">Backup Schedules</div>
<div class="section-subtitle">Configure automated backup schedules for your services</div>
</div>
<div class="header-actions">
<button class="action-button" @click=${() => this.handleImport()}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="17 8 12 3 7 8"/>
<line x1="12" y1="3" x2="12" y2="15"/>
</svg>
Import Backup
</button>
<button class="action-button primary" @click=${() => this.handleCreateSchedule()}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
Create Schedule
</button>
<dees-tile>
<div slot="header" class="section-header">
<div class="section-heading">
<span class="section-title">Backup Schedules</span>
<span class="section-subtitle">Configure automated backup schedules for your services</span>
</div>
</div>
<div class="table-header schedules-header">
@@ -318,13 +324,30 @@ export class SzServicesBackupsView extends DeesElement {
</span>
</div>
`)}
</div>
<div slot="footer" class="section-footer">
<button class="tile-button" @click=${() => this.handleImport()}>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="17 8 12 3 7 8"/>
<line x1="12" y1="3" x2="12" y2="15"/>
</svg>
Import Backup
</button>
<button class="tile-button primary" @click=${() => this.handleCreateSchedule()}>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
Create Schedule
</button>
</div>
</dees-tile>
<div class="section">
<div class="section-header">
<div class="section-info">
<div class="section-title">All Backups</div>
<div class="section-subtitle">Browse and manage all backups across services</div>
<dees-tile>
<div slot="header" class="section-header">
<div class="section-heading">
<span class="section-title">All Backups</span>
<span class="section-subtitle">Browse and manage all backups across services</span>
</div>
</div>
<div class="table-header backups-header">
@@ -358,7 +381,7 @@ export class SzServicesBackupsView extends DeesElement {
</span>
</div>
`)}
</div>
</dees-tile>
`;
}

View File

@@ -72,28 +72,98 @@ export class SzSettingsView extends DeesElement {
display: block;
}
.section {
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
border-radius: 8px;
padding: 20px;
dees-tile {
display: block;
margin-bottom: 24px;
}
.section-header {
margin-bottom: 16px;
height: 36px;
display: flex;
align-items: center;
padding: 0 16px;
width: 100%;
box-sizing: border-box;
}
.section-heading {
flex: 1;
display: flex;
align-items: baseline;
gap: 8px;
min-width: 0;
}
.section-title {
font-size: 16px;
font-weight: 600;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
font-weight: 500;
font-size: 13px;
letter-spacing: -0.01em;
color: var(--dees-color-text-secondary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.section-subtitle {
font-size: 13px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
margin-top: 2px;
font-size: 12px;
color: var(--dees-color-text-muted);
letter-spacing: -0.01em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.section-content {
padding: 20px;
}
.section-footer {
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
gap: 0;
height: 36px;
width: 100%;
box-sizing: border-box;
}
.tile-button {
padding: 0 16px;
height: 100%;
text-align: center;
font-size: 12px;
font-weight: 500;
cursor: pointer;
user-select: none;
transition: all 0.15s ease;
background: transparent;
border: none;
border-left: 1px solid var(--dees-color-border-subtle);
color: var(--dees-color-text-muted);
white-space: nowrap;
display: flex;
align-items: center;
gap: 6px;
}
.tile-button:first-child {
border-left: none;
}
.tile-button:hover {
background: var(--dees-color-hover);
color: var(--dees-color-text-primary);
}
.tile-button.primary {
color: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8%)', 'hsl(213.1 93.9% 67.8%)')};
font-weight: 600;
}
.tile-button.primary:hover {
background: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8% / 0.08)', 'hsl(213.1 93.9% 67.8% / 0.08)')};
color: ${cssManager.bdTheme('hsl(217.2 91.2% 50%)', 'hsl(213.1 93.9% 75%)')};
}
.form-group {
@@ -224,161 +294,151 @@ export class SzSettingsView extends DeesElement {
margin-bottom: 4px;
}
.actions {
display: flex;
justify-content: flex-end;
gap: 12px;
padding-top: 16px;
border-top: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
margin-top: 24px;
}
.button {
padding: 10px 20px;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 200ms ease;
}
.button.secondary {
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
}
.button.secondary:hover {
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
}
.button.primary {
background: ${cssManager.bdTheme('#18181b', '#fafafa')};
border: none;
color: ${cssManager.bdTheme('#fafafa', '#18181b')};
}
.button.primary:hover {
opacity: 0.9;
}
`,
];
public render(): TemplateResult {
return html`
<div class="section">
<div class="section-header">
<div class="section-title">Appearance</div>
<div class="section-subtitle">Customize the look and feel</div>
</div>
<div class="form-row">
<div class="form-label-group">
<span class="form-label">Dark Mode</span>
<span class="form-hint">Toggle dark mode on or off</span>
</div>
<div class="toggle-switch ${this.settings.darkMode ? 'active' : ''}" @click=${() => this.toggleDarkMode()}></div>
</div>
</div>
<div class="section">
<div class="section-header">
<div class="section-title">Cloudflare Integration</div>
<div class="section-subtitle">Configure Cloudflare API for DNS management</div>
</div>
<div class="input-group">
<div class="form-group">
<div class="field-label">API Token</div>
<input type="password" placeholder="Enter Cloudflare API token" .value=${this.settings.cloudflareToken} @input=${(e: Event) => this.updateSetting('cloudflareToken', (e.target as HTMLInputElement).value)}>
</div>
<div class="form-group">
<div class="field-label">Zone ID (Optional)</div>
<input type="text" placeholder="Default zone ID" .value=${this.settings.cloudflareZoneId} @input=${(e: Event) => this.updateSetting('cloudflareZoneId', (e.target as HTMLInputElement).value)}>
</div>
<div class="form-hint">Get your API token from the Cloudflare dashboard with DNS edit permissions.</div>
</div>
</div>
<div class="section">
<div class="section-header">
<div class="section-title">SSL/TLS Settings</div>
<div class="section-subtitle">Configure certificate management</div>
</div>
<div class="form-row">
<div class="form-label-group">
<span class="form-label">Auto-Renew Certificates</span>
<span class="form-hint">Automatically renew certificates before expiry</span>
</div>
<div class="toggle-switch ${this.settings.autoRenewCerts ? 'active' : ''}" @click=${() => this.toggleSetting('autoRenewCerts')}></div>
</div>
<div class="form-group" style="margin-top: 16px;">
<div class="field-label">Renewal Threshold (days)</div>
<input type="number" .value=${String(this.settings.renewalThreshold)} @input=${(e: Event) => this.updateSetting('renewalThreshold', parseInt((e.target as HTMLInputElement).value))}>
<div class="form-hint">Renew certificates when they have fewer than this many days remaining.</div>
</div>
<div class="form-group">
<div class="field-label">ACME Email</div>
<input type="email" placeholder="admin@example.com" .value=${this.settings.acmeEmail} @input=${(e: Event) => this.updateSetting('acmeEmail', (e.target as HTMLInputElement).value)}>
<div class="form-hint">Email address for Let's Encrypt notifications.</div>
</div>
</div>
<div class="section">
<div class="section-header">
<div class="section-title">Network Settings</div>
<div class="section-subtitle">Configure network and proxy settings</div>
</div>
<div class="input-row">
<div class="form-group">
<div class="field-label">HTTP Port</div>
<input type="number" .value=${String(this.settings.httpPort)} @input=${(e: Event) => this.updateSetting('httpPort', parseInt((e.target as HTMLInputElement).value))}>
</div>
<div class="form-group">
<div class="field-label">HTTPS Port</div>
<input type="number" .value=${String(this.settings.httpsPort)} @input=${(e: Event) => this.updateSetting('httpsPort', parseInt((e.target as HTMLInputElement).value))}>
<dees-tile>
<div slot="header" class="section-header">
<div class="section-heading">
<span class="section-title">Appearance</span>
<span class="section-subtitle">Customize the look and feel</span>
</div>
</div>
<div class="form-row">
<div class="form-label-group">
<span class="form-label">Force HTTPS</span>
<span class="form-hint">Redirect all HTTP traffic to HTTPS</span>
</div>
<div class="toggle-switch ${this.settings.forceHttps ? 'active' : ''}" @click=${() => this.toggleSetting('forceHttps')}></div>
</div>
</div>
<div class="section">
<div class="section-header">
<div class="section-title">Account</div>
<div class="section-subtitle">Manage your account settings</div>
</div>
<div class="form-group">
<div class="field-label">Current User</div>
<div style="font-size: 14px; color: ${cssManager.bdTheme('#18181b', '#fafafa')};">${this.currentUser || 'Unknown'}</div>
</div>
<div class="password-section">
<div class="password-title">Change Password</div>
<div class="password-fields">
<div>
<div class="field-label">Current Password</div>
<input type="password" id="currentPassword">
<div class="section-content">
<div class="form-row">
<div class="form-label-group">
<span class="form-label">Dark Mode</span>
<span class="form-hint">Toggle dark mode on or off</span>
</div>
<div>
<div class="field-label">New Password</div>
<input type="password" id="newPassword">
</div>
<div>
<div class="field-label">Confirm Password</div>
<input type="password" id="confirmPassword">
</div>
<button class="button secondary" style="width: fit-content;" @click=${() => this.handleChangePassword()}>Update Password</button>
<div class="toggle-switch ${this.settings.darkMode ? 'active' : ''}" @click=${() => this.toggleDarkMode()}></div>
</div>
</div>
</div>
</dees-tile>
<div class="actions">
<button class="button secondary" @click=${() => this.handleReset()}>Reset</button>
<button class="button primary" @click=${() => this.handleSave()}>Save Settings</button>
</div>
<dees-tile>
<div slot="header" class="section-header">
<div class="section-heading">
<span class="section-title">Cloudflare Integration</span>
<span class="section-subtitle">Configure Cloudflare API for DNS management</span>
</div>
</div>
<div class="section-content">
<div class="input-group">
<div class="form-group">
<div class="field-label">API Token</div>
<input type="password" placeholder="Enter Cloudflare API token" .value=${this.settings.cloudflareToken} @input=${(e: Event) => this.updateSetting('cloudflareToken', (e.target as HTMLInputElement).value)}>
</div>
<div class="form-group">
<div class="field-label">Zone ID (Optional)</div>
<input type="text" placeholder="Default zone ID" .value=${this.settings.cloudflareZoneId} @input=${(e: Event) => this.updateSetting('cloudflareZoneId', (e.target as HTMLInputElement).value)}>
</div>
<div class="form-hint">Get your API token from the Cloudflare dashboard with DNS edit permissions.</div>
</div>
</div>
</dees-tile>
<dees-tile>
<div slot="header" class="section-header">
<div class="section-heading">
<span class="section-title">SSL/TLS Settings</span>
<span class="section-subtitle">Configure certificate management</span>
</div>
</div>
<div class="section-content">
<div class="form-row">
<div class="form-label-group">
<span class="form-label">Auto-Renew Certificates</span>
<span class="form-hint">Automatically renew certificates before expiry</span>
</div>
<div class="toggle-switch ${this.settings.autoRenewCerts ? 'active' : ''}" @click=${() => this.toggleSetting('autoRenewCerts')}></div>
</div>
<div class="form-group" style="margin-top: 16px;">
<div class="field-label">Renewal Threshold (days)</div>
<input type="number" .value=${String(this.settings.renewalThreshold)} @input=${(e: Event) => this.updateSetting('renewalThreshold', parseInt((e.target as HTMLInputElement).value))}>
<div class="form-hint">Renew certificates when they have fewer than this many days remaining.</div>
</div>
<div class="form-group">
<div class="field-label">ACME Email</div>
<input type="email" placeholder="admin@example.com" .value=${this.settings.acmeEmail} @input=${(e: Event) => this.updateSetting('acmeEmail', (e.target as HTMLInputElement).value)}>
<div class="form-hint">Email address for Let's Encrypt notifications.</div>
</div>
</div>
</dees-tile>
<dees-tile>
<div slot="header" class="section-header">
<div class="section-heading">
<span class="section-title">Network Settings</span>
<span class="section-subtitle">Configure network and proxy settings</span>
</div>
</div>
<div class="section-content">
<div class="input-row">
<div class="form-group">
<div class="field-label">HTTP Port</div>
<input type="number" .value=${String(this.settings.httpPort)} @input=${(e: Event) => this.updateSetting('httpPort', parseInt((e.target as HTMLInputElement).value))}>
</div>
<div class="form-group">
<div class="field-label">HTTPS Port</div>
<input type="number" .value=${String(this.settings.httpsPort)} @input=${(e: Event) => this.updateSetting('httpsPort', parseInt((e.target as HTMLInputElement).value))}>
</div>
</div>
<div class="form-row">
<div class="form-label-group">
<span class="form-label">Force HTTPS</span>
<span class="form-hint">Redirect all HTTP traffic to HTTPS</span>
</div>
<div class="toggle-switch ${this.settings.forceHttps ? 'active' : ''}" @click=${() => this.toggleSetting('forceHttps')}></div>
</div>
</div>
</dees-tile>
<dees-tile>
<div slot="header" class="section-header">
<div class="section-heading">
<span class="section-title">Account</span>
<span class="section-subtitle">Manage your account settings</span>
</div>
</div>
<div class="section-content">
<div class="form-group">
<div class="field-label">Current User</div>
<div style="font-size: 14px; color: ${cssManager.bdTheme('#18181b', '#fafafa')};">${this.currentUser || 'Unknown'}</div>
</div>
<div class="password-section">
<div class="password-title">Change Password</div>
<div class="password-fields">
<div>
<div class="field-label">Current Password</div>
<input type="password" id="currentPassword">
</div>
<div>
<div class="field-label">New Password</div>
<input type="password" id="newPassword">
</div>
<div>
<div class="field-label">Confirm Password</div>
<input type="password" id="confirmPassword">
</div>
</div>
</div>
</div>
<div slot="footer" class="section-footer">
<button class="tile-button" @click=${() => this.handleChangePassword()}>Update Password</button>
</div>
</dees-tile>
<dees-tile>
<div class="section-content" style="padding: 12px 16px; text-align: center; color: var(--dees-color-text-muted); font-size: 12px;">
Save your changes or reset to defaults.
</div>
<div slot="footer" class="section-footer">
<button class="tile-button" @click=${() => this.handleReset()}>Reset</button>
<button class="tile-button primary" @click=${() => this.handleSave()}>Save Settings</button>
</div>
</dees-tile>
`;
}

View File

@@ -1,189 +0,0 @@
import {
DeesElement,
customElement,
html,
css,
cssManager,
property,
type TemplateResult,
} from '@design.estate/dees-element';
declare global {
interface HTMLElementTagNameMap {
'sz-stat-card': SzStatCard;
}
}
@customElement('sz-stat-card')
export class SzStatCard extends DeesElement {
public static demo = () => html`
<style>
.demo-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
padding: 24px;
max-width: 800px;
}
</style>
<div class="demo-grid">
<sz-stat-card
label="Total Services"
value="7"
icon="server"
></sz-stat-card>
<sz-stat-card
label="Running"
value="7"
icon="check"
variant="success"
></sz-stat-card>
<sz-stat-card
label="Stopped"
value="0"
icon="stop"
></sz-stat-card>
<sz-stat-card
label="Docker"
value="Running"
icon="container"
variant="success"
valueBadge
></sz-stat-card>
</div>
`;
public static demoGroups = ['Dashboard'];
@property({ type: String })
public accessor label: string = '';
@property({ type: String })
public accessor value: string = '';
@property({ type: String })
public accessor icon: string = '';
@property({ type: String })
public accessor variant: 'default' | 'success' | 'warning' | 'error' = 'default';
@property({ type: Boolean })
public accessor valueBadge: boolean = false;
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
height: 100%;
}
.card {
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
border-radius: 8px;
padding: 20px;
transition: all 200ms ease;
height: 100%;
box-sizing: border-box;
}
.card:hover {
border-color: ${cssManager.bdTheme('#d4d4d8', '#3f3f46')};
box-shadow: 0 4px 12px ${cssManager.bdTheme('rgba(0,0,0,0.05)', 'rgba(0,0,0,0.2)')};
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.label {
font-size: 14px;
font-weight: 500;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
.icon {
width: 20px;
height: 20px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
.value {
font-size: 28px;
font-weight: 700;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
line-height: 1.2;
}
.value.success {
color: ${cssManager.bdTheme('#16a34a', '#22c55e')};
}
.value.warning {
color: ${cssManager.bdTheme('#ca8a04', '#facc15')};
}
.value.error {
color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
}
.badge {
display: inline-flex;
align-items: center;
padding: 4px 12px;
border-radius: 9999px;
font-size: 14px;
font-weight: 500;
}
.badge.success {
background: ${cssManager.bdTheme('#dcfce7', 'rgba(34, 197, 94, 0.2)')};
color: ${cssManager.bdTheme('#16a34a', '#22c55e')};
}
.badge.warning {
background: ${cssManager.bdTheme('#fef9c3', 'rgba(250, 204, 21, 0.2)')};
color: ${cssManager.bdTheme('#ca8a04', '#facc15')};
}
.badge.error {
background: ${cssManager.bdTheme('#fee2e2', 'rgba(239, 68, 68, 0.2)')};
color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
}
.badge.default {
background: ${cssManager.bdTheme('#f4f4f5', '#27272a')};
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
`,
];
public render(): TemplateResult {
const valueClass = this.valueBadge ? `badge ${this.variant}` : `value ${this.variant}`;
return html`
<div class="card">
<div class="header">
<span class="label">${this.label}</span>
${this.renderIcon()}
</div>
<div class="${valueClass}">${this.value}</div>
</div>
`;
}
private renderIcon(): TemplateResult {
const icons: Record<string, TemplateResult> = {
server: html`<svg class="icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><rect x="2" y="2" width="20" height="8" rx="2" ry="2"></rect><rect x="2" y="14" width="20" height="8" rx="2" ry="2"></rect><line x1="6" y1="6" x2="6.01" y2="6"></line><line x1="6" y1="18" x2="6.01" y2="18"></line></svg>`,
check: html`<svg class="icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"></polyline></svg>`,
stop: html`<svg class="icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="10" y1="15" x2="10" y2="9"></line><line x1="14" y1="15" x2="14" y2="9"></line></svg>`,
container: html`<svg class="icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path></svg>`,
};
return icons[this.icon] || html``;
}
}

View File

@@ -7,8 +7,7 @@ import {
property,
type TemplateResult,
} from '@design.estate/dees-element';
import './sz-stat-card.js';
import type { IStatsTile } from '@design.estate/dees-catalog';
declare global {
interface HTMLElementTagNameMap {
@@ -54,54 +53,51 @@ export class SzStatusGridCluster extends DeesElement {
:host {
display: block;
}
.grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
align-items: stretch;
}
.grid > * {
height: 100%;
}
@media (min-width: 768px) {
.grid {
grid-template-columns: repeat(4, 1fr);
}
}
`,
];
private get tiles(): IStatsTile[] {
return [
{
id: 'total',
title: 'Total Services',
value: this.stats.totalServices,
type: 'number',
icon: 'lucide:server',
},
{
id: 'running',
title: 'Running',
value: this.stats.running,
type: 'number',
icon: 'lucide:check',
color: '#22c55e',
},
{
id: 'stopped',
title: 'Stopped',
value: this.stats.stopped,
type: 'number',
icon: 'lucide:circleStop',
color: this.stats.stopped > 0 ? '#f59e0b' : undefined,
},
{
id: 'docker',
title: 'Docker',
value: this.stats.dockerStatus === 'running' ? 'Running' : 'Stopped',
type: 'text',
icon: 'lucide:container',
color: this.stats.dockerStatus === 'running' ? '#22c55e' : '#ef4444',
},
];
}
public render(): TemplateResult {
return html`
<div class="grid">
<sz-stat-card
label="Total Services"
value="${this.stats.totalServices}"
icon="server"
></sz-stat-card>
<sz-stat-card
label="Running"
value="${this.stats.running}"
icon="check"
variant="success"
></sz-stat-card>
<sz-stat-card
label="Stopped"
value="${this.stats.stopped}"
icon="stop"
variant="${this.stats.stopped > 0 ? 'warning' : 'default'}"
></sz-stat-card>
<sz-stat-card
label="Docker"
value="${this.stats.dockerStatus === 'running' ? 'Running' : 'Stopped'}"
icon="container"
variant="${this.stats.dockerStatus === 'running' ? 'success' : 'error'}"
valueBadge
></sz-stat-card>
</div>
<dees-statsgrid
.tiles=${this.tiles}
.minTileWidth=${200}
></dees-statsgrid>
`;
}
}

View File

@@ -55,56 +55,94 @@ export class SzTokensView extends DeesElement {
display: block;
}
.section {
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
border-radius: 8px;
dees-tile {
display: block;
margin-bottom: 24px;
overflow: hidden;
}
.section-header {
height: 36px;
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 16px;
border-bottom: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
align-items: center;
padding: 0 16px;
width: 100%;
box-sizing: border-box;
}
.section-info {
.section-heading {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
align-items: baseline;
gap: 8px;
min-width: 0;
}
.section-title {
font-size: 16px;
font-weight: 600;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
font-weight: 500;
font-size: 13px;
letter-spacing: -0.01em;
color: var(--dees-color-text-secondary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.section-subtitle {
font-size: 13px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
font-size: 12px;
color: var(--dees-color-text-muted);
letter-spacing: -0.01em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.create-button {
display: inline-flex;
.section-footer {
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
gap: 0;
height: 36px;
width: 100%;
box-sizing: border-box;
}
.tile-button {
padding: 0 16px;
height: 100%;
text-align: center;
font-size: 12px;
font-weight: 500;
cursor: pointer;
user-select: none;
transition: all 0.15s ease;
background: transparent;
border: none;
border-left: 1px solid var(--dees-color-border-subtle);
color: var(--dees-color-text-muted);
white-space: nowrap;
display: flex;
align-items: center;
gap: 6px;
padding: 8px 14px;
background: ${cssManager.bdTheme('#18181b', '#fafafa')};
border: none;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
color: ${cssManager.bdTheme('#fafafa', '#18181b')};
cursor: pointer;
transition: all 200ms ease;
}
.create-button:hover {
opacity: 0.9;
.tile-button:first-child {
border-left: none;
}
.tile-button:hover {
background: var(--dees-color-hover);
color: var(--dees-color-text-primary);
}
.tile-button.primary {
color: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8%)', 'hsl(213.1 93.9% 67.8%)')};
font-weight: 600;
}
.tile-button.primary:hover {
background: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8% / 0.08)', 'hsl(213.1 93.9% 67.8% / 0.08)')};
color: ${cssManager.bdTheme('hsl(217.2 91.2% 50%)', 'hsl(213.1 93.9% 75%)')};
}
.token-list {
@@ -192,45 +230,18 @@ export class SzTokensView extends DeesElement {
.empty-text {
font-size: 14px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
margin-bottom: 16px;
}
.empty-button {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 14px;
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
border-radius: 6px;
font-size: 13px;
font-weight: 500;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
cursor: pointer;
transition: all 200ms ease;
}
.empty-button:hover {
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
}
`,
];
public render(): TemplateResult {
return html`
<div class="section">
<div class="section-header">
<div class="section-info">
<div class="section-title">Global Tokens</div>
<div class="section-subtitle">Tokens that can push images to multiple services</div>
<dees-tile>
<div slot="header" class="section-header">
<div class="section-heading">
<span class="section-title">Global Tokens</span>
<span class="section-subtitle">Tokens that can push images to multiple services</span>
</div>
<button class="create-button" @click=${() => this.handleCreate('global')}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
Create Token
</button>
</div>
${this.globalTokens.length > 0 ? html`
<div class="token-list">
@@ -239,25 +250,26 @@ export class SzTokensView extends DeesElement {
` : html`
<div class="empty-state">
<div class="empty-text">No global tokens created</div>
<button class="empty-button" @click=${() => this.handleCreate('global')}>Create Global Token</button>
</div>
`}
</div>
<div class="section">
<div class="section-header">
<div class="section-info">
<div class="section-title">CI Tokens (Service-specific)</div>
<div class="section-subtitle">Tokens tied to individual services for CI/CD pipelines</div>
</div>
<button class="create-button" @click=${() => this.handleCreate('ci')}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<div slot="footer" class="section-footer">
<button class="tile-button primary" @click=${() => this.handleCreate('global')}>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
Create Token
</button>
</div>
</dees-tile>
<dees-tile>
<div slot="header" class="section-header">
<div class="section-heading">
<span class="section-title">CI Tokens (Service-specific)</span>
<span class="section-subtitle">Tokens tied to individual services for CI/CD pipelines</span>
</div>
</div>
${this.ciTokens.length > 0 ? html`
<div class="token-list">
${this.ciTokens.map(token => this.renderToken(token))}
@@ -265,10 +277,18 @@ export class SzTokensView extends DeesElement {
` : html`
<div class="empty-state">
<div class="empty-text">No CI tokens created</div>
<button class="empty-button" @click=${() => this.handleCreate('ci')}>Create CI Token</button>
</div>
`}
</div>
<div slot="footer" class="section-footer">
<button class="tile-button primary" @click=${() => this.handleCreate('ci')}>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
Create Token
</button>
</div>
</dees-tile>
`;
}