summaryrefslogtreecommitdiff
path: root/src/web
diff options
context:
space:
mode:
authorPaul-Christian Volkmer2026-01-08 16:22:57 +0100
committerGitHub2026-01-08 15:22:57 +0000
commited4b068127530346345ed16b2e79b33bc5b03d57 (patch)
tree173d36e24bb04ea61a2a960dd765e1c745dd6f6b /src/web
parent7045318e87ecc853c000e9e11c955bc6298f2d56 (diff)
build: remove webjars and use custom build (#238)
Diffstat (limited to 'src/web')
-rw-r--r--src/web/charts.js195
-rw-r--r--src/web/main.js18
-rw-r--r--src/web/rspack.config.js33
-rw-r--r--src/web/style.css781
4 files changed, 1027 insertions, 0 deletions
diff --git a/src/web/charts.js b/src/web/charts.js
new file mode 100644
index 0000000..4826696
--- /dev/null
+++ b/src/web/charts.js
@@ -0,0 +1,195 @@
+import * as echarts from 'echarts/core';
+import { BarChart, PieChart } from 'echarts/charts';
+import { SVGRenderer } from 'echarts/renderers';
+import {
+ TitleComponent,
+ TooltipComponent,
+ DatasetComponent,
+ GridComponent
+} from 'echarts/components';
+
+echarts.use([
+ BarChart,
+ PieChart,
+ TitleComponent,
+ TooltipComponent,
+ DatasetComponent,
+ GridComponent,
+ SVGRenderer
+]);
+
+window.onload = () => {
+ drawPieChart('statistics/requeststates', 'piechart1', 'Statusverteilung aller Anfragen');
+ drawPieChart('statistics/requestpatientstates', 'piechart2', 'Statusverteilung nach Patient');
+ drawBarChart('statistics/requestslastmonth', 'barchart', 'Anfragen der letzten 30 Tage');
+
+ drawPieChart('statistics/requeststates?delete=true', 'piechartdel1', 'Statusverteilung aller Anfragen');
+ drawPieChart('statistics/requestpatientstates?delete=true', 'piechartdel2', 'Statusverteilung nach Patient');
+ drawBarChart('statistics/requestslastmonth?delete=true', 'barchartdel', 'Anfragen der letzten 30 Tage');
+
+ const eventSource = new EventSource('statistics/events');
+ eventSource.addEventListener('requeststates', event => {
+ drawPieChart('statistics/requeststates', 'piechart1', 'Statusverteilung aller Anfragen', JSON.parse(event.data));
+ });
+ eventSource.addEventListener('requestpatientstates', event => {
+ drawPieChart('statistics/requestpatientstates', 'piechart2', 'Statusverteilung nach Patient', JSON.parse(event.data));
+ });
+ eventSource.addEventListener('requestslastmonth', event => {
+ drawBarChart('statistics/requestslastmonth', 'barchart', 'Anfragen des letzten Monats', JSON.parse(event.data));
+ });
+
+ eventSource.addEventListener('deleterequeststates', event => {
+ drawPieChart('statistics/requeststates?delete=true', 'piechartdel1', 'Statusverteilung aller Anfragen', JSON.parse(event.data));
+ });
+ eventSource.addEventListener('deleterequestpatientstates', event => {
+ drawPieChart('statistics/requestpatientstates?delete=true', 'piechartdel2', 'Statusverteilung nach Patient', JSON.parse(event.data));
+ });
+ eventSource.addEventListener('deleterequestslastmonth', event => {
+ drawBarChart('statistics/requestslastmonth?delete=true', 'barchartdel', 'Anfragen des letzten Monats', JSON.parse(event.data));
+ });
+}
+
+const dateFormatOptions = { year: 'numeric', month: '2-digit', day: '2-digit' };
+const dateFormat = new Intl.DateTimeFormat('de-DE', dateFormatOptions);
+
+export function drawPieChart(url, elemId, title, data) {
+ if (data) {
+ update(elemId, data);
+ } else {
+ fetch(url)
+ .then(resp => resp.json())
+ .then(d => {
+ draw(elemId, title, d);
+ update(elemId, d);
+ });
+ }
+
+ function update(elemId, data) {
+ let chartDom = document.getElementById(elemId);
+ let chart = echarts.init(chartDom, null, {renderer: 'svg'});
+
+ let option = {
+ color: data.map(i => i.color),
+ animationDuration: 250,
+ animationDurationUpdate: 250,
+ series: [
+ {
+ type: 'pie',
+ radius: ['40%', '70%'],
+ avoidLabelOverlap: false,
+ label: {
+ show: false,
+ position: 'center'
+ },
+ labelLine: {
+ show: false
+ },
+ data: data
+ }
+ ]
+ };
+
+ option && chart.setOption(option);
+ }
+
+ function draw(elemId, title, data) {
+ let chartDom = document.getElementById(elemId);
+ let chart = echarts.init(chartDom, null, {renderer: 'svg'});
+ let option= {
+ title: {
+ text: title,
+ left: 'center'
+ },
+ tooltip: {
+ trigger: 'item'
+ },
+ color: data.map(i => i.color),
+ animationDuration: 250,
+ animationDurationUpdate: 250
+ };
+
+ option && chart.setOption(option);
+ }
+}
+
+export function drawBarChart(url, elemId, title, data) {
+ if (data) {
+ update(elemId, data);
+ } else {
+ fetch(url)
+ .then(resp => resp.json())
+ .then(data => {
+ draw(elemId, title, data);
+ update(elemId, data);
+ });
+ }
+
+ function update(elemId, data) {
+ let chartDom = document.getElementById(elemId);
+ let chart = echarts.init(chartDom, null, {renderer: 'svg'});
+
+ let option = {
+ series: [
+ {
+ name: 'UNKNOWN',
+ type: 'bar',
+ stack: 'total',
+ data: data.map(i => i.nameValues.unknown)
+ },
+ {
+ name: 'ERROR',
+ type: 'bar',
+ stack: 'total',
+ data: data.map(i => i.nameValues.error)
+ },
+ {
+ name: 'WARNING',
+ type: 'bar',
+ stack: 'total',
+ data: data.map(i => i.nameValues.warning)
+ },
+ {
+ name: 'SUCCESS',
+ type: 'bar',
+ stack: 'total',
+ data: data.map(i => i.nameValues.success)
+ },
+ {
+ name: 'DUPLICATION',
+ type: 'bar',
+ stack: 'total',
+ data: data.map(i => i.nameValues.duplication)
+ }
+ ]
+ };
+
+ option && chart.setOption(option);
+ }
+
+ function draw(elemId, title, data) {
+ let chartDom = document.getElementById(elemId);
+ let chart = echarts.init(chartDom, null, {renderer: 'svg'});
+ let option= {
+ title: {
+ text: title,
+ left: 'center'
+ },
+ xAxis: {
+ type: 'category',
+ data: data.map(i => dateFormat.format(Date.parse(i.date)))
+ },
+ yAxis: {
+ type: 'value',
+ minInterval: 1
+ },
+ tooltip: {
+ trigger: 'item'
+ },
+ color: ['slategray', 'red', 'darkorange', 'green', 'slategray'],
+ animationDuration: 250,
+ animationDurationUpdate: 250
+ };
+
+ option && chart.setOption(option);
+ }
+} \ No newline at end of file
diff --git a/src/web/main.js b/src/web/main.js
new file mode 100644
index 0000000..ca1ecc9
--- /dev/null
+++ b/src/web/main.js
@@ -0,0 +1,18 @@
+import * as styles from './style.css';
+
+import 'htmx.org';
+
+const dateTimeFormatOptions = { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: 'numeric', second: 'numeric' };
+const dateTimeFormat = new Intl.DateTimeFormat('de-DE', dateTimeFormatOptions);
+
+const formatTimeElements = () => {
+ Array.from(document.getElementsByTagName('time')).forEach((timeTag) => {
+ let date = Date.parse(timeTag.getAttribute('datetime'));
+ if (! isNaN(date)) {
+ timeTag.innerText = dateTimeFormat.format(date);
+ }
+ });
+};
+
+window.addEventListener('load', formatTimeElements);
+window.addEventListener('htmx:afterRequest', formatTimeElements); \ No newline at end of file
diff --git a/src/web/rspack.config.js b/src/web/rspack.config.js
new file mode 100644
index 0000000..aa92d31
--- /dev/null
+++ b/src/web/rspack.config.js
@@ -0,0 +1,33 @@
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+
+export default {
+ entry: {
+ main: './src/web/main.js',
+ charts: './src/web/charts.js'
+ },
+ output: {
+ path: path.resolve(__dirname, '../../src/main/resources/static'),
+ chunkFilename: '[id].js',
+ sourceMap: false,
+ library: {
+ type: "window"
+ }
+ },
+ module: {
+ rules: [
+ {
+ test: /\.css$/,
+ use: [{
+ loader: "postcss-loader"
+ }],
+ type: "css"
+ },
+ ]
+ },
+ experiments: {
+ css: true,
+ }
+} \ No newline at end of file
diff --git a/src/web/style.css b/src/web/style.css
new file mode 100644
index 0000000..3631750
--- /dev/null
+++ b/src/web/style.css
@@ -0,0 +1,781 @@
+:root {
+ --text: #333;
+ --table-border: rgba(16, 24, 40, .1);
+
+ --dark: brightness(.90);
+
+ --bg-blue: rgb(0, 74, 157);
+ --bg-blue-op: rgba(0, 74, 157, .35);
+
+ --bg-green: rgb(0, 128, 0);
+ --bg-green-op: rgba(0, 128, 0, .35);
+
+
+ --bg-yellow: rgb(255, 140, 0);
+ --bg-yellow-op: rgba(255, 140, 0, .35);
+
+
+ --bg-red: rgb(255, 0, 0);
+ --bg-red-op: rgba(255, 0, 0, .35);
+
+ --bg-gray: rgb(112, 128, 144);
+ --bg-gray-op: rgba(112, 128, 144, .35);
+}
+
+* {
+ font-family: sans-serif;
+ box-sizing: border-box;
+}
+
+html {
+ background:
+ linear-gradient(transparent 30rem, white 50rem),
+ linear-gradient(-135deg, transparent 20vw, #004d6e10 25vw, transparent 30vw),
+ linear-gradient(-135deg, transparent 30vw, #706f6f10 35vw, transparent 40vw),
+ linear-gradient(-135deg, transparent 40vw, #f59e0010 45vw, transparent 50vw);
+ overflow-y: scroll;
+}
+
+body {
+ margin: 0;
+ font-size: .8rem;
+ color: var(--text);
+ background-size: contain;
+}
+
+div.headline {
+ position: fixed;
+ display: block;
+ top: 0;
+ z-index: 1000;
+ width: 100%;
+ height: 5rem;
+ align-content: center;
+ background: white;
+ border-bottom: 1px solid var(--table-border);
+}
+
+nav {
+ display: flex;
+ margin: 0 auto;
+ line-height: 1.5rem;
+ max-width: 1140px;
+}
+
+nav a.nav-home {
+ margin: auto 0;
+
+ color: var(--text);
+ line-height: 1.5rem;
+ text-decoration: none;
+
+ font-size: 2rem;
+ font-weight: bold;
+}
+
+nav a.nav-home > img {
+ width: 1.5rem;
+ vertical-align: middle;
+}
+
+nav > ul {
+ display: block;
+ margin: 0 0 0 auto;
+ padding: 0;
+ width: max-content;
+}
+
+nav > ul > li {
+ display: inline-block;
+ padding: 0 1rem;
+}
+
+nav > ul > li.login {
+ margin: 0 0 0 1rem;
+ padding: 0 0 0 2rem;
+ border-left: 1px solid var(--table-border);
+ line-height: 3.5rem;
+}
+
+nav > ul > li.login a {
+ text-decoration: none;
+ text-transform: none;
+ padding: 1rem;
+}
+
+nav .login .user-name {
+ font-weight: bold;
+}
+
+nav > ul > li.login > span {
+ display: inline-block;
+ margin: 0 .5rem;
+}
+
+nav > ul > li.login .user-icon {
+ flex-direction: column;
+ display: inline flex;
+ vertical-align: middle;
+ inline-size: 4rem;
+}
+
+nav > ul > li.login .user-icon img {
+ margin: 0 0 -1em 0;
+ width: 80%;
+ align-self: center;
+}
+
+nav > ul > li.login .user-icon span {
+ padding: 0 .6em;
+ color: white;
+ font-size: .8rem;
+ font-weight: bold;
+ border-radius: 4px;
+ line-height: normal;
+ text-align: center;
+}
+
+nav > ul > li.login .user-icon span.guest {
+ background: darkslategray;
+}
+
+nav > ul > li.login .user-icon span.user {
+ background: darkgreen;
+}
+
+nav > ul > li.login .user-icon span.admin {
+ background: darkred;
+}
+
+nav li a {
+ color: var(--bg-blue);
+ text-transform: uppercase;
+ text-decoration: none;
+ font-weight: 700;
+}
+
+nav li a:hover {
+ text-decoration: underline;
+}
+
+a {
+ color: var(--bg-blue);
+}
+
+.breadcrumps {
+ margin: 0 auto;
+ max-width: 1140px;
+}
+
+.breadcrumps ul {
+ margin: 2px 0;
+ padding: .4rem 1rem;
+ list-style: none;
+ background: #eee;
+}
+
+.breadcrumps ul li {
+ display: inline;
+}
+
+.breadcrumps ul li + li:before {
+ padding: .4rem;
+ color: gray;
+ content: "/\00a0";
+}
+
+.breadcrumps ul li a {
+ color: var(--text);
+ text-decoration: none;
+}
+
+.centered {
+ text-align: center;
+}
+
+.container {
+ margin: auto;
+}
+
+main {
+ margin: 6rem auto 0;
+ min-height: calc(100dvh - 10rem);
+}
+
+main, .container {
+ max-width: 1140px;
+}
+
+footer {
+ width: 100%;
+ height: 4rem;
+ position: relative;
+ align-content: center;
+ display: flex;
+ bottom: 0;
+ padding: 1rem 0;
+ border-top: 1px solid var(--table-border);
+ background: var(--bg-blue);
+ color: white;
+}
+
+footer > .container > div {
+ margin: 0 auto;
+ max-width: 1140px;
+ display: inline-block;
+}
+
+footer > .container > div,
+footer > .container > div:after {
+ content: "-";
+ padding-left: 1rem;
+}
+
+footer > .container > div:last-child,
+footer > .container > div:last-child:after {
+ content: "";
+}
+
+footer svg {
+ height: 1.4rem;
+ color: white;
+ vertical-align: text-bottom;
+}
+
+section {
+ margin: 3rem 0;
+}
+
+form {
+ margin: 1rem 0;
+ padding: 1rem;
+
+ border: 1px solid lightgray;
+ border-radius: 3px;
+ background: #eee;
+
+ text-align: center;
+}
+
+form > h2 {
+ margin: 0;
+}
+
+form.samplecode-input > div {
+ padding: 0.6rem;
+ display: inline-block;
+
+ border: 1px solid lightgray;
+ border-radius: 3px;
+
+ background: white;
+}
+
+form.samplecode-input input {
+ padding: 0;
+
+ border: none;
+ outline: none;
+
+ text-align: left;
+ appearance: textfield;
+ font-size: 1.2rem;
+ font-weight: bold;
+}
+
+form.samplecode-input input:focus-visible {
+ background: lightgreen;
+}
+
+.login-form {
+ width: fit-content;
+ margin: 3rem auto;
+ padding: 2em 5rem;
+
+ border: 1px solid var(--table-border);
+ border-radius: .5rem;
+ background: white;
+}
+
+.login-form form {
+ width: 20rem;
+ margin: 0 auto;
+ display: grid;
+ grid-gap: .5rem;
+
+ border: none;
+ background: none;
+}
+
+.login-form img {
+ margin: 0 auto;
+ width: 4rem;
+ display: block;
+}
+
+.userrole-form {
+ display: inline-block;
+}
+
+.userrole-form form {
+ margin: 0;
+ padding: 0;
+
+ border: none;
+ border-radius: 0;
+ background: none;
+
+ text-align: inherit;
+}
+
+.login-form form *,
+.token-form form * {
+ padding: 0.5rem;
+ border: 1px solid var(--table-border);
+ border-radius: 3px;
+}
+
+.login-form form hr,
+.token-form form hr,
+.userrole-form form hr {
+ padding: 0;
+ width: 100%;
+}
+
+.login-form button,
+.login-form a.btn,
+.token-form button {
+ margin: 1rem 0;
+ background: var(--bg-blue);
+ color: white;
+ border: none;
+}
+
+.userrole-form form select {
+ padding: 0.5rem;
+ border: none;
+ border-radius: 3px;
+ line-height: 1.2rem;
+ font-size: 0.8rem;
+}
+
+.border {
+ padding: 1rem;
+ border: 1px solid var(--table-border);
+ border-radius: .5rem;
+ background: white;
+}
+
+table, .chart {
+ border: 1px solid var(--table-border);
+ padding: 1.5rem;
+
+ border-spacing: 0;
+ border-radius: .5rem;
+
+ background: white;
+}
+
+table {
+ min-width: 100%;
+ font-family: sans-serif;
+}
+
+table.config-table td:first-child {
+ width: 26rem;
+ min-width: fit-content;
+}
+
+table.config-table td > button:last-of-type {
+ float: right;
+}
+
+.border > table {
+ padding: 0;
+ border: none;
+ background: transparent;
+}
+
+.page-control {
+ border-radius: .5rem;
+ padding: 1rem 2rem;
+ text-align: center;
+
+ line-height: 1.75em;
+}
+
+.page-control a {
+ padding: 0 .25rem;
+ font-size: 1.75rem;
+ color: var(--bg-gray);
+ text-decoration: none;
+}
+
+.page-control a[href] {
+ color: var(--bg-blue);
+}
+
+.page-control span {
+ padding: 0 .5rem;
+ vertical-align: text-bottom;
+}
+
+#samples-table.max {
+ width: 100vw;
+ position: fixed;
+ padding: 1rem;
+ top: 0;
+ left: 0;
+ background: white;
+ min-height: 100vh;
+}
+
+table.samples {
+ max-width: 100%;
+ overflow-x: scroll;
+ display: block;
+}
+
+th, td {
+ padding: 0.4rem .2rem;
+
+ line-height: 2rem;
+
+ text-align: left;
+ white-space: nowrap;
+ vertical-align: top;
+}
+
+th {
+ border-bottom: 1px solid var(--bg-gray);
+}
+
+td {
+ border-bottom: 1px solid var(--bg-gray-op);
+}
+
+td, td > a {
+ font-family: monospace;
+}
+
+tr:last-of-type > td {
+ border-bottom: none;
+}
+
+td > small {
+ display: block;
+ text-align: center;
+}
+
+td.patient-id {
+ min-width: 20rem;
+ text-overflow: ellipsis;
+ overflow: hidden;
+}
+
+td.bg-blue, th.bg-blue,
+td.bg-green, th.bg-green,
+td.bg-yellow, th.bg-yellow,
+td.bg-red, th.bg-red,
+td.bg-gray, th.bg-gray
+{
+ width: 8rem;
+}
+
+td.bg-blue > small, th.bg-blue > small {
+ background: var(--bg-blue);
+ color: white;
+ border-radius: 0.4rem;
+}
+
+td.bg-green > small, th.bg-green > small {
+ background: var(--bg-green);
+ color: white;
+ border-radius: 0.4rem;
+}
+
+td.bg-yellow > small, th.bg-yellow > small {
+ background: var(--bg-yellow);
+ color: white;
+ border-radius: 0.4rem;
+}
+
+td.bg-red > small, th.bg-red > small {
+ background: var(--bg-red);
+ color: white;
+ border-radius: 0.4rem;
+}
+
+td.bg-gray > small, th.bg-gray > small {
+ background: var(--bg-gray);
+ color: white;
+ border-radius: 0.4rem;
+}
+
+.bg-path {
+ background: var(--bg-gray-op);
+}
+
+.bg-from {
+ background: var(--bg-red-op);
+}
+
+.bg-to {
+ background: var(--bg-green-op);
+}
+
+.bg-path, .bg-from, .bg-to {
+ padding: 0.25rem 0.5rem;
+ border-radius: 3px;
+
+ font-family: monospace;
+}
+
+td.bg-shaded, th.bg-shaded {
+ background: repeating-linear-gradient(140deg, white, #e5e5f5 4px, white 8px);
+}
+
+td.clipboard {
+ cursor: copy;
+}
+
+td.clipboard.clipped {
+ box-shadow: 0 0 1rem lightgreen inset;
+}
+
+.btn {
+ margin: 4px;
+ padding: 4px 8px;
+
+ line-height: 1.2rem;
+
+ border: 0 solid transparent;
+ border-radius: 3px;
+
+ text-decoration: none;
+ font-size: 0.8rem;
+ font-weight: normal;
+
+ cursor: pointer;
+}
+
+.btn:active,
+.btn:hover {
+ filter: drop-shadow(0px 1px 1px var(--bg-gray)) var(--dark);
+}
+
+.btn:active {
+ translate: 0 1px;
+}
+
+.btn.btn-red {
+ background: var(--bg-red);
+ color: white;
+}
+
+.btn.btn-yellow {
+ background: var(--bg-yellow);
+ color: white;
+}
+
+.btn.btn-green {
+ background: var(--bg-green);
+ color: white;
+}
+
+.btn.btn-blue {
+ background: var(--bg-blue);
+ color: white;
+}
+
+.btn.btn-delete:before {
+ content: '\1F5D1';
+ padding: .2rem;
+}
+
+button:disabled,
+.btn:disabled {
+ background: slategray !important;
+ color: lightgray;
+ filter: none;
+ cursor: default;
+}
+
+input.inline {
+ border: none;
+ font-size: 1.1rem;
+ outline: none;
+}
+
+input.inline:focus-visible {
+ background: lightgreen;
+}
+
+.monospace {
+ font-family: monospace;
+ color: #333333;
+ border-bottom: 1px dotted gray !important;
+}
+
+.help {
+ padding: 1rem;
+
+ border: 1px solid darkslategray;
+ border-radius: 3px;
+ background: slategray;
+ color: white;
+}
+
+.help.error {
+ border: 3px dashed red;
+ background: darkorange;
+}
+
+.help .help-header {
+ font-size: 1.2rem;
+ font-weight: bold;
+}
+
+.charts {
+ display: grid;
+ grid-gap: 1em;
+ grid-template:
+ "a b" 28rem
+ "c c" 28rem / 1fr 1fr;
+}
+
+.charts > .grid-left {
+ grid-area: a;
+}
+
+.charts > .grid-right {
+ grid-area: b;
+}
+
+.charts > .grid-full {
+ grid-area: c;
+}
+
+.connection-display {
+ display: grid;
+ grid-template-columns: 10rem 16rem 10rem;
+ place-items: center;
+ width: fit-content;
+ margin: 1em auto;
+}
+
+.connection-display > * {
+ text-align: center;
+ margin: auto 0;
+}
+
+.connection-display .connection {
+ display: block;
+ width: 100%;
+ height: 4px;
+ background: repeating-linear-gradient(to left, white, white 2px, transparent 2px, transparent 8px, white 8px) var(--bg-red);
+}
+
+.connection-display .connection.available {
+ background: var(--bg-green);
+}
+
+.notification {
+ margin: 1rem;
+ padding: .5rem;
+ border-radius: 3px;
+ text-align: center;
+}
+
+.notification.info {
+ color: var(--bg-blue);
+}
+
+.notification.success {
+ color: var(--bg-green);
+}
+
+.notification.notice {
+ color: var(--bg-yellow);
+}
+
+.notification.error {
+ color: var(--bg-red);
+}
+
+.tab {
+ padding: 1rem;
+ border: none;
+ border-radius: 3px 3px 0 0;
+ cursor: pointer;
+ transition: all 0.2s;
+
+ font-weight: bold;
+}
+
+.tab:hover,
+.tab.active {
+ background: var(--bg-gray);
+ color: white;
+}
+
+.tabcontent {
+ border: 2px solid var(--bg-gray);
+ border-radius: 0 .5rem .5rem .5rem;
+ display: none;
+ padding: 1rem;
+ background: white;
+}
+
+.tabcontent.active {
+ display: block;
+}
+
+a.reload {
+ display: none;
+ margin: 0;
+ vertical-align: top;
+ border-radius: 1.4rem;
+}
+
+a.reload::before {
+ content: "⟳";
+ font-size: 1.2rem;
+ vertical-align: top;
+}
+
+a.reload span {
+ display: none;
+}
+
+a.reload:hover span {
+ display: inline;
+}
+
+.new-token {
+ padding: 1rem;
+ background: var(--bg-green-op);
+}
+
+.new-token > pre {
+ margin: 0;
+ border: 1px solid var(--bg-green);
+ padding: .5rem;
+ width: max-content;
+ display: inline-block;
+}
+
+.no-token {
+ padding: 1rem;
+ background: var(--bg-red-op);
+}
+
+.issue-message {
+ font-family: monospace;
+ font-weight: bolder;
+}
+
+.issue-path {
+ font-family: monospace;
+ line-height: 1rem;
+}