web_plsql 0.17.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -4
- package/package.json +21 -8
- package/src/admin/client/charts.ts +299 -0
- package/src/admin/client/main.js +3 -0
- package/src/admin/client/tailwind.css +41 -0
- package/src/admin/favicon.svg +3 -0
- package/src/admin/index.html +315 -0
- package/src/admin/js/api.ts +95 -0
- package/src/admin/js/app.ts +306 -0
- package/src/admin/js/eslint.config.js +74 -0
- package/src/admin/js/schemas.ts +153 -0
- package/src/admin/js/templates/config.ts +146 -0
- package/src/admin/js/templates/errorRow.ts +18 -0
- package/src/admin/js/templates/index.ts +3 -0
- package/src/admin/js/templates/poolCard.ts +61 -0
- package/src/admin/js/tsconfig.json +24 -0
- package/src/admin/js/types.ts +223 -0
- package/src/admin/js/ui/theme.ts +93 -0
- package/src/admin/js/ui/views.ts +164 -0
- package/src/admin/js/util/format.ts +27 -0
- package/src/admin/lib/assets/main-zpdhQ1gD.css +1 -0
- package/src/admin/lib/chart.bundle.js +139 -0
- package/src/admin/style.css +1321 -0
- package/src/bin/load-test.js +202 -0
- package/src/handler/handlerAdmin.js +198 -0
- package/src/handler/plsql/cgi.js +1 -1
- package/src/handler/plsql/errorPage.js +37 -12
- package/src/handler/plsql/handlerPlSql.js +32 -6
- package/src/handler/plsql/parsePage.js +15 -11
- package/src/handler/plsql/procedure.js +81 -33
- package/src/handler/plsql/procedureNamed.js +18 -52
- package/src/handler/plsql/procedureSanitize.js +96 -49
- package/src/handler/plsql/procedureVariable.js +2 -2
- package/src/handler/plsql/request.js +11 -6
- package/src/handler/plsql/sendResponse.js +43 -11
- package/src/handler/plsql/stream.js +1 -1
- package/src/handler/plsql/upload.js +1 -1
- package/src/server/config.js +1 -0
- package/src/server/server.js +108 -2
- package/src/types.js +7 -1
- package/src/util/cache.js +123 -0
- package/src/util/file.js +5 -9
- package/src/util/jsonLogger.js +47 -0
- package/src/util/shutdown.js +6 -2
- package/src/util/trace.js +5 -5
- package/src/version.js +1 -1
- package/types/handler/handlerAdmin.d.ts +9 -0
- package/types/handler/plsql/errorPage.d.ts +1 -0
- package/types/handler/plsql/handlerPlSql.d.ts +6 -1
- package/types/handler/plsql/procedure.d.ts +3 -1
- package/types/handler/plsql/procedureNamed.d.ts +2 -5
- package/types/handler/plsql/procedureSanitize.d.ts +2 -5
- package/types/handler/plsql/procedureVariable.d.ts +1 -1
- package/types/handler/plsql/request.d.ts +3 -1
- package/types/handler/plsql/sendResponse.d.ts +1 -1
- package/types/server/server.d.ts +22 -0
- package/types/types.d.ts +19 -1
- package/types/util/cache.d.ts +69 -0
- package/types/util/jsonLogger.d.ts +45 -0
- package/types/handler/plsql/stream.d.ts +0 -2
package/README.md
CHANGED
|
@@ -31,10 +31,29 @@ Please visit the [node-oracledb](https://node-oracledb.readthedocs.io/en/latest/
|
|
|
31
31
|
* Start server using `node examples/config-native.js` after having set the WEB_PLSQL_ORACLE_SERVER environment variable to the database where you just installed the sample schema.
|
|
32
32
|
* Invoke a browser and open the page `http://localhost/sample`.
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
# Admin Console
|
|
35
|
+
|
|
36
|
+
`web_plsql` includes a built-in Admin Console for real-time monitoring and management of your gateway.
|
|
37
|
+
|
|
38
|
+
## Features
|
|
39
|
+
- **Dashboard**: Real-time charts showing requests per second and error rates.
|
|
40
|
+
- **Pool Monitoring**: Visual representation of database connection pool usage.
|
|
41
|
+
- **Cache Management**: View and manage the metadata cache.
|
|
42
|
+
- **Access & Error Logs**: Interactive viewers for the server logs.
|
|
43
|
+
- **System Info**: Overview of the server environment and configuration.
|
|
44
|
+
|
|
45
|
+
## Configuration
|
|
46
|
+
The admin console is enabled by default in the `startServer` API. You can configure it via the `admin` property in the main configuration object.
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
/**
|
|
50
|
+
* @typedef {object} configAdminType
|
|
51
|
+
* @property {boolean} [enabled=true] - Whether the admin console is enabled.
|
|
52
|
+
* @property {string} [route='/admin'] - The route path for the admin console.
|
|
53
|
+
*/
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Access the console at `http://localhost:<port>/admin` (e.g., `http://localhost:8080/admin`).
|
|
38
57
|
|
|
39
58
|
# Configuration
|
|
40
59
|
|
|
@@ -93,6 +112,7 @@ The `startServer` api uses the following configuration object:
|
|
|
93
112
|
/**
|
|
94
113
|
* @typedef {object} configType
|
|
95
114
|
* @property {number} port - The server port number.
|
|
115
|
+
* @property {configAdminType} [admin] - The admin console configuration.
|
|
96
116
|
* @property {configStaticType[]} routeStatic - The static routes.
|
|
97
117
|
* @property {configPlSqlType[]} routePlSql - The PL/SQL routes.
|
|
98
118
|
* @property {number} [uploadFileSizeLimit] - Maximum size of each uploaded file in bytes or no limit if omitted.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "web_plsql",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"author": "Dieter Oberkofler <dieter.oberkofler@gmail.com>",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "The Express Middleware for Oracle PL/SQL",
|
|
@@ -43,12 +43,17 @@
|
|
|
43
43
|
"types": "./types/index.d.ts",
|
|
44
44
|
"type": "module",
|
|
45
45
|
"scripts": {
|
|
46
|
-
"
|
|
47
|
-
"
|
|
46
|
+
"build": "npm run build:admin",
|
|
47
|
+
"build:admin": "vite build",
|
|
48
|
+
"lint": "prettier --check --experimental-cli . && eslint . && tsc --noEmit && eslint src/admin --config src/admin/js/eslint.config.js",
|
|
49
|
+
"test": "vitest run",
|
|
50
|
+
"test:coverage": "vitest run --coverage",
|
|
51
|
+
"stylelint": "stylelint \"src/admin/**/*.css\"",
|
|
48
52
|
"types": "rm -rf types && tsc --noEmit false --declaration true --declarationDir types --emitDeclarationOnly true --allowJs true --target es2020 --module es2020 --moduleResolution node --allowSyntheticDefaultImports true src/*.js",
|
|
49
53
|
"clean": "shx rm -f *.tgz && shx rm -f *.log",
|
|
50
|
-
"ci": "npm run clean && npm run version && npm run lint && npm run test && npm run types",
|
|
51
54
|
"version": "node update-version.js",
|
|
55
|
+
"prepack": "npm run ci",
|
|
56
|
+
"ci": "npm run clean && npm run version && npm run types && npm run build && npm run lint && npm run stylelint && npm run test:coverage",
|
|
52
57
|
"create-package": "shx rm -f *.tgz && npm pack",
|
|
53
58
|
"image-build": "docker build --no-cache --progress=plain --tag=web_plsql .",
|
|
54
59
|
"image-save": "docker save web_plsql | gzip > web_plsql.tar.gz"
|
|
@@ -57,6 +62,7 @@
|
|
|
57
62
|
"oracledb": "6.10.0"
|
|
58
63
|
},
|
|
59
64
|
"dependencies": {
|
|
65
|
+
"chart.js": "^4.5.1",
|
|
60
66
|
"compression": "1.8.1",
|
|
61
67
|
"cookie-parser": "1.4.7",
|
|
62
68
|
"debug": "4.4.3",
|
|
@@ -65,24 +71,31 @@
|
|
|
65
71
|
"morgan": "1.10.1",
|
|
66
72
|
"multer": "2.0.2",
|
|
67
73
|
"rotating-file-stream": "3.2.8",
|
|
68
|
-
"zod": "4.3.6"
|
|
74
|
+
"zod": "^4.3.6"
|
|
69
75
|
},
|
|
70
76
|
"devDependencies": {
|
|
77
|
+
"@tailwindcss/vite": "^4.1.18",
|
|
71
78
|
"@types/compression": "1.8.1",
|
|
72
79
|
"@types/cookie-parser": "1.4.10",
|
|
73
80
|
"@types/debug": "4.1.12",
|
|
74
81
|
"@types/morgan": "1.9.10",
|
|
75
82
|
"@types/multer": "2.0.0",
|
|
76
|
-
"@types/node": "25.2.
|
|
83
|
+
"@types/node": "25.2.1",
|
|
77
84
|
"@types/oracledb": "6.10.1",
|
|
78
85
|
"@types/supertest": "6.0.3",
|
|
86
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
79
87
|
"eslint": "9.39.2",
|
|
80
|
-
"eslint-plugin-jsdoc": "62.5.
|
|
88
|
+
"eslint-plugin-jsdoc": "62.5.3",
|
|
81
89
|
"oracledb": "6.10.0",
|
|
82
90
|
"prettier": "3.8.1",
|
|
83
91
|
"shx": "0.4.0",
|
|
92
|
+
"stylelint": "^17.1.1",
|
|
93
|
+
"stylelint-config-standard": "^40.0.0",
|
|
84
94
|
"supertest": "7.2.2",
|
|
95
|
+
"tailwindcss": "^4.1.18",
|
|
85
96
|
"typescript": "5.9.3",
|
|
86
|
-
"typescript-eslint": "8.54.0"
|
|
97
|
+
"typescript-eslint": "8.54.0",
|
|
98
|
+
"vite": "^7.3.1",
|
|
99
|
+
"vitest": "^4.0.18"
|
|
87
100
|
}
|
|
88
101
|
}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import {Chart, registerables} from 'chart.js';
|
|
2
|
+
import type {State, ChartInstance} from '../js/types.js';
|
|
3
|
+
import type {StatusResponse} from '../js/schemas.js';
|
|
4
|
+
|
|
5
|
+
// Register Chart.js components
|
|
6
|
+
Chart.register(...registerables);
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Cache pie chart instances.
|
|
10
|
+
*/
|
|
11
|
+
const cachePieInstances = new Map<HTMLCanvasElement, Chart>();
|
|
12
|
+
|
|
13
|
+
type PoolInfo = StatusResponse['pools'][number];
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Chart colors based on theme mode.
|
|
17
|
+
*/
|
|
18
|
+
type ChartColors = {
|
|
19
|
+
gridColor: string;
|
|
20
|
+
textColor: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Get chart colors for the current theme.
|
|
25
|
+
* @param isDark - Whether dark mode is active.
|
|
26
|
+
* @returns Chart color configuration.
|
|
27
|
+
*/
|
|
28
|
+
function getChartColors(isDark: boolean): ChartColors {
|
|
29
|
+
return {
|
|
30
|
+
gridColor: isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)',
|
|
31
|
+
textColor: isDark ? '#94a3b8' : '#475569',
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Initialize charts.
|
|
37
|
+
* @param state - Application state.
|
|
38
|
+
*/
|
|
39
|
+
export function initCharts(state: State): void {
|
|
40
|
+
const isDark = document.body.classList.contains('dark');
|
|
41
|
+
const colors = getChartColors(isDark);
|
|
42
|
+
|
|
43
|
+
const trafficEl = document.getElementById('traffic-chart') as HTMLCanvasElement | null;
|
|
44
|
+
if (trafficEl) {
|
|
45
|
+
const chartData = {
|
|
46
|
+
labels: [],
|
|
47
|
+
datasets: [
|
|
48
|
+
{
|
|
49
|
+
label: 'Requests/s',
|
|
50
|
+
data: [],
|
|
51
|
+
borderColor: '#3b82f6',
|
|
52
|
+
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
|
53
|
+
fill: true,
|
|
54
|
+
tension: 0.4,
|
|
55
|
+
yAxisID: 'y',
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
label: 'Avg Response Time (ms)',
|
|
59
|
+
data: [],
|
|
60
|
+
borderColor: '#10b981',
|
|
61
|
+
backgroundColor: 'rgba(16, 185, 129, 0.1)',
|
|
62
|
+
fill: true,
|
|
63
|
+
tension: 0.4,
|
|
64
|
+
yAxisID: 'y1',
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const chartOptions = {
|
|
70
|
+
responsive: true,
|
|
71
|
+
maintainAspectRatio: false,
|
|
72
|
+
scales: {
|
|
73
|
+
x: {
|
|
74
|
+
grid: {color: colors.gridColor},
|
|
75
|
+
ticks: {
|
|
76
|
+
color: colors.textColor,
|
|
77
|
+
maxRotation: 0,
|
|
78
|
+
autoSkip: true,
|
|
79
|
+
maxTicksLimit: 10,
|
|
80
|
+
},
|
|
81
|
+
title: {
|
|
82
|
+
display: true,
|
|
83
|
+
text: 'Time',
|
|
84
|
+
color: colors.textColor,
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
y: {
|
|
88
|
+
type: 'linear' as const,
|
|
89
|
+
display: true,
|
|
90
|
+
position: 'left' as const,
|
|
91
|
+
grid: {color: colors.gridColor},
|
|
92
|
+
ticks: {color: colors.textColor},
|
|
93
|
+
beginAtZero: true,
|
|
94
|
+
title: {
|
|
95
|
+
display: true,
|
|
96
|
+
text: 'Requests/s',
|
|
97
|
+
color: colors.textColor,
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
y1: {
|
|
101
|
+
type: 'linear' as const,
|
|
102
|
+
display: true,
|
|
103
|
+
position: 'right' as const,
|
|
104
|
+
grid: {drawOnChartArea: false, color: colors.gridColor},
|
|
105
|
+
ticks: {color: '#10b981'},
|
|
106
|
+
beginAtZero: true,
|
|
107
|
+
title: {
|
|
108
|
+
display: true,
|
|
109
|
+
text: 'Avg Response Time (ms)',
|
|
110
|
+
color: '#10b981',
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
plugins: {
|
|
115
|
+
legend: {
|
|
116
|
+
labels: {color: colors.textColor},
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const chart = new Chart(trafficEl, {
|
|
122
|
+
type: 'line',
|
|
123
|
+
data: chartData,
|
|
124
|
+
options: chartOptions,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
state.charts.traffic = {
|
|
128
|
+
data: chartData,
|
|
129
|
+
options: chartOptions,
|
|
130
|
+
update: () => chart.update(),
|
|
131
|
+
} as ChartInstance;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const poolEl = document.getElementById('pool-chart') as HTMLCanvasElement | null;
|
|
135
|
+
if (poolEl) {
|
|
136
|
+
const chartData = {
|
|
137
|
+
labels: [],
|
|
138
|
+
datasets: [],
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const chartOptions = {
|
|
142
|
+
responsive: true,
|
|
143
|
+
maintainAspectRatio: false,
|
|
144
|
+
scales: {
|
|
145
|
+
x: {
|
|
146
|
+
grid: {color: colors.gridColor},
|
|
147
|
+
ticks: {
|
|
148
|
+
color: colors.textColor,
|
|
149
|
+
maxRotation: 0,
|
|
150
|
+
autoSkip: true,
|
|
151
|
+
maxTicksLimit: 10,
|
|
152
|
+
},
|
|
153
|
+
title: {
|
|
154
|
+
display: true,
|
|
155
|
+
text: 'Time',
|
|
156
|
+
color: colors.textColor,
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
y: {
|
|
160
|
+
grid: {color: colors.gridColor},
|
|
161
|
+
ticks: {color: colors.textColor},
|
|
162
|
+
beginAtZero: true,
|
|
163
|
+
title: {
|
|
164
|
+
display: true,
|
|
165
|
+
text: 'Connections',
|
|
166
|
+
color: colors.textColor,
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
plugins: {
|
|
171
|
+
legend: {
|
|
172
|
+
labels: {color: colors.textColor},
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const chart = new Chart(poolEl, {
|
|
178
|
+
type: 'line',
|
|
179
|
+
data: chartData,
|
|
180
|
+
options: chartOptions,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
state.charts.pool = {
|
|
184
|
+
data: chartData,
|
|
185
|
+
options: chartOptions,
|
|
186
|
+
update: () => chart.update(),
|
|
187
|
+
} as ChartInstance;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Update charts with new data.
|
|
193
|
+
* @param state - Application state.
|
|
194
|
+
* @param timeLabel - Time label for the data point.
|
|
195
|
+
* @param reqPerSec - Requests per second.
|
|
196
|
+
* @param avgResponseTime - Average response time in ms.
|
|
197
|
+
* @param pools - Pool information.
|
|
198
|
+
*/
|
|
199
|
+
export function updateCharts(state: State, timeLabel: string, reqPerSec: number, avgResponseTime: number, pools: PoolInfo[]): void {
|
|
200
|
+
const maxPoints = state.maxHistoryPoints;
|
|
201
|
+
|
|
202
|
+
state.history.labels.push(timeLabel);
|
|
203
|
+
state.history.requests.push(reqPerSec);
|
|
204
|
+
state.history.avgResponseTimes.push(avgResponseTime);
|
|
205
|
+
|
|
206
|
+
if (state.history.labels.length > maxPoints) {
|
|
207
|
+
state.history.labels.shift();
|
|
208
|
+
state.history.requests.shift();
|
|
209
|
+
state.history.avgResponseTimes.shift();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const trafficChart = state.charts.traffic;
|
|
213
|
+
if (trafficChart?.data.datasets[0] && trafficChart.data.datasets[1]) {
|
|
214
|
+
trafficChart.data.labels = state.history.labels;
|
|
215
|
+
trafficChart.data.datasets[0].data = state.history.requests;
|
|
216
|
+
trafficChart.data.datasets[1].data = state.history.avgResponseTimes;
|
|
217
|
+
trafficChart.update();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const poolChart = state.charts.pool;
|
|
221
|
+
if (poolChart) {
|
|
222
|
+
poolChart.data.labels = state.history.labels;
|
|
223
|
+
pools.forEach((p, i) => {
|
|
224
|
+
const poolName = p.name;
|
|
225
|
+
const history = state.history;
|
|
226
|
+
let usage = history.poolUsage[poolName];
|
|
227
|
+
if (!usage) {
|
|
228
|
+
usage = [];
|
|
229
|
+
history.poolUsage[poolName] = usage;
|
|
230
|
+
const colors = ['#10b981', '#f59e0b', '#3b82f6', '#8b5cf6', '#ec4899'];
|
|
231
|
+
const color = colors[i % colors.length];
|
|
232
|
+
if (color) {
|
|
233
|
+
poolChart.data.datasets.push({
|
|
234
|
+
label: poolName,
|
|
235
|
+
data: usage,
|
|
236
|
+
borderColor: color,
|
|
237
|
+
backgroundColor: undefined,
|
|
238
|
+
fill: undefined,
|
|
239
|
+
tension: 0.4,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
usage.push(p.connectionsInUse);
|
|
244
|
+
if (usage.length > maxPoints) {
|
|
245
|
+
usage.shift();
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
poolChart.update();
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Create or update a pie chart for cache statistics.
|
|
254
|
+
* @param canvas - The canvas element.
|
|
255
|
+
*/
|
|
256
|
+
export function renderCachePie(canvas: HTMLCanvasElement): void {
|
|
257
|
+
const hits = parseInt(canvas.dataset.hits ?? '0');
|
|
258
|
+
const misses = parseInt(canvas.dataset.misses ?? '0');
|
|
259
|
+
|
|
260
|
+
const existingChart = cachePieInstances.get(canvas);
|
|
261
|
+
if (existingChart?.data.datasets[0]) {
|
|
262
|
+
existingChart.data.datasets[0].data = [hits, misses];
|
|
263
|
+
existingChart.update();
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const chart = new Chart(canvas, {
|
|
268
|
+
type: 'pie',
|
|
269
|
+
data: {
|
|
270
|
+
labels: ['Hits', 'Misses'],
|
|
271
|
+
datasets: [
|
|
272
|
+
{
|
|
273
|
+
data: [hits, misses],
|
|
274
|
+
backgroundColor: ['#10b981', '#ef4444'],
|
|
275
|
+
borderColor: 'transparent',
|
|
276
|
+
},
|
|
277
|
+
],
|
|
278
|
+
},
|
|
279
|
+
options: {
|
|
280
|
+
responsive: true,
|
|
281
|
+
maintainAspectRatio: true,
|
|
282
|
+
plugins: {
|
|
283
|
+
legend: {display: false},
|
|
284
|
+
tooltip: {
|
|
285
|
+
callbacks: {
|
|
286
|
+
label: (context) => {
|
|
287
|
+
const value = context.raw as number;
|
|
288
|
+
const total = hits + misses;
|
|
289
|
+
const pct = total > 0 ? ((value / total) * 100).toFixed(1) : '0';
|
|
290
|
+
return `${context.label}: ${value} (${pct}%)`;
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
cachePieInstances.set(canvas, chart);
|
|
299
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
@import 'tailwindcss';
|
|
2
|
+
|
|
3
|
+
@theme {
|
|
4
|
+
--color-primary: #3b82f6;
|
|
5
|
+
--color-success: #10b981;
|
|
6
|
+
--color-warning: #f59e0b;
|
|
7
|
+
--color-danger: #ef4444;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
body {
|
|
11
|
+
font-family:
|
|
12
|
+
'Inter',
|
|
13
|
+
-apple-system,
|
|
14
|
+
BlinkMacSystemFont,
|
|
15
|
+
'Segoe UI',
|
|
16
|
+
Roboto,
|
|
17
|
+
Helvetica,
|
|
18
|
+
Arial,
|
|
19
|
+
sans-serif;
|
|
20
|
+
-webkit-font-smoothing: antialiased;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
body.dark {
|
|
24
|
+
--bg-main: #0f172a;
|
|
25
|
+
--bg-side: #1e293b;
|
|
26
|
+
--bg-card: #1e293b;
|
|
27
|
+
--bg-hover: #334155;
|
|
28
|
+
--text-main: #94a3b8;
|
|
29
|
+
--text-bright: #f8fafc;
|
|
30
|
+
--border: #334155;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
body:not(.dark) {
|
|
34
|
+
--bg-main: #f8fafc;
|
|
35
|
+
--bg-side: #ffffff;
|
|
36
|
+
--bg-card: #ffffff;
|
|
37
|
+
--bg-hover: #f1f5f9;
|
|
38
|
+
--text-main: #475569;
|
|
39
|
+
--text-bright: #0f172a;
|
|
40
|
+
--border: #e2e8f0;
|
|
41
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#3b82f6">
|
|
2
|
+
<path d="M12 2C6.48 2 2 4.02 2 6.5s4.48 4.5 10 4.5 10-2.02 10-4.5S17.52 2 12 2zm0 13c-5.52 0-10-2.02-10-4.5V14c0 2.48 4.48 4.5 10 4.5s10-2.02 10-4.5v-3.5c0 2.48-4.48 4.5-10 4.5zm0 4.5c-5.52 0-10-2.02-10-4.5V19c0 2.48 4.48 4.5 10 4.5s10-2.02 10-4.5v-2.5c0 2.48-4.48 4.5-10 4.5z"/>
|
|
3
|
+
</svg>
|