cynx-lib 1.3.80 → 1.3.81

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.
Files changed (41) hide show
  1. package/gen.js +77 -77
  2. package/package.json +1 -1
  3. package/ui/components/Drawer.jsx +208 -208
  4. package/ui/components/Layout.jsx +138 -138
  5. package/ui/components/LoginPage.jsx +405 -405
  6. package/ui/components/Pagination.jsx +145 -145
  7. package/ui/components/PrivacyBlur.jsx +603 -603
  8. package/ui/components/Sidebar.jsx +820 -820
  9. package/utils/index.js +20 -20
  10. package/utils/utils/duration.js +139 -139
  11. package/utils/utils/oidc.js +214 -214
  12. package/utils/utils/realtime.js +1634 -1634
  13. package/utils/utils/session.js +58 -58
  14. package/utils/utils/storage.js +393 -393
  15. package/utils/utils/storage_manager.cjs +55 -4
  16. package/utils/utils/storage_manager.js +64 -4
  17. package/utils/utils/ui.js +111 -111
  18. package/vue/components/Badge.vue +79 -79
  19. package/vue/components/Button.vue +209 -209
  20. package/vue/components/Checkbox.vue +103 -103
  21. package/vue/components/CircularProgress.vue +383 -383
  22. package/vue/components/Divider.vue +94 -94
  23. package/vue/components/Drawer.vue +243 -243
  24. package/vue/components/InfoBadge.vue +223 -223
  25. package/vue/components/Input.vue +341 -341
  26. package/vue/components/LinearProgress.vue +268 -268
  27. package/vue/components/Modal.vue +226 -226
  28. package/vue/components/Pagination.vue +199 -199
  29. package/vue/components/Select.vue +713 -713
  30. package/vue/components/Sidebar.vue +955 -955
  31. package/vue/components/Skeleton.vue +91 -91
  32. package/vue/components/Spinner.vue +263 -263
  33. package/vue/components/StatusTag.vue +89 -89
  34. package/vue/components/TabBar.vue +176 -176
  35. package/vue/components/Table.vue +388 -388
  36. package/vue/components/TimePicker.vue +399 -399
  37. package/vue/components/Toggle.vue +115 -115
  38. package/vue/components/XTable.vue +119 -119
  39. package/vue/composables/useAutoSkeletonRows.ts +71 -71
  40. package/vue/index.d.ts +67 -67
  41. package/vue/index.js +86 -86
@@ -224,6 +224,24 @@ class StorageManager {
224
224
  invalidateStorageCache() {
225
225
  this.activeStorageCache = { data: null, ts: 0 };
226
226
  }
227
+ async _hasInspectionColumn() {
228
+ if (this._hasInspectionCol !== void 0) return this._hasInspectionCol;
229
+ if (!this.query) {
230
+ this._hasInspectionCol = false;
231
+ return false;
232
+ }
233
+ try {
234
+ const res = await this.query(
235
+ `SELECT 1 FROM information_schema.columns WHERE table_name = $1 AND column_name = 'inspection_id' LIMIT 1`,
236
+ [this.filesTable]
237
+ );
238
+ this._hasInspectionCol = res.rows && res.rows.length > 0;
239
+ return this._hasInspectionCol;
240
+ } catch {
241
+ this._hasInspectionCol = false;
242
+ return false;
243
+ }
244
+ }
227
245
  async getCachedFileRecord(fileId) {
228
246
  if (!fileId) return null;
229
247
  const now = Date.now();
@@ -918,7 +936,7 @@ class StorageManager {
918
936
  this._streamHttp(file, req, res, isInline, redirectCount);
919
937
  }
920
938
  _streamHttp(file, req, res, isInline, redirectCount = 0, knownProvider = null) {
921
- let targetUrl = file.cloud_url;
939
+ let targetUrl = file.cloud_url || file.url || file.view_url;
922
940
  if (!targetUrl) {
923
941
  if (file.storage_id && file.cloud_public_id) {
924
942
  return this._streamFromS3(file, req, res, isInline);
@@ -935,6 +953,7 @@ class StorageManager {
935
953
  if (req?.headers && req.headers["range"]) {
936
954
  requestHeaders["Range"] = req.headers["range"];
937
955
  }
956
+ requestHeaders["Accept-Encoding"] = "identity";
938
957
  client.get(targetUrl, { agent, headers: requestHeaders }, (cloudRes) => {
939
958
  if (cloudRes.statusCode >= 300 && cloudRes.statusCode < 400 && cloudRes.headers.location) {
940
959
  const redirectUrl = new URL(cloudRes.headers.location, targetUrl).toString();
@@ -948,7 +967,36 @@ class StorageManager {
948
967
  if (cloudRes.statusCode >= 400) {
949
968
  return res.status(cloudRes.statusCode).send("Error loading file from storage");
950
969
  }
951
- const mimeType = cloudRes.headers["content-type"] || file.mime_type || "application/octet-stream";
970
+ const rawHeaderType = cloudRes.headers["content-type"];
971
+ const ext = (file.original_name || "").split(".").pop()?.toLowerCase();
972
+ const EXT_MIMES = {
973
+ svg: "image/svg+xml",
974
+ png: "image/png",
975
+ jpg: "image/jpeg",
976
+ jpeg: "image/jpeg",
977
+ webp: "image/webp",
978
+ gif: "image/gif",
979
+ ico: "image/x-icon",
980
+ bmp: "image/bmp",
981
+ avif: "image/avif",
982
+ pdf: "application/pdf",
983
+ json: "application/json",
984
+ txt: "text/plain",
985
+ csv: "text/csv",
986
+ xml: "application/xml",
987
+ zip: "application/zip",
988
+ tar: "application/x-tar",
989
+ gz: "application/gzip",
990
+ mp4: "video/mp4",
991
+ webm: "video/webm",
992
+ mp3: "audio/mpeg",
993
+ wav: "audio/wav"
994
+ };
995
+ const inferredMime = ext ? EXT_MIMES[ext] : null;
996
+ let mimeType = rawHeaderType;
997
+ if (!mimeType || mimeType === "application/octet-stream") {
998
+ mimeType = file.mime_type && file.mime_type !== "application/octet-stream" ? file.mime_type : inferredMime || rawHeaderType || "application/octet-stream";
999
+ }
952
1000
  const filename = file.original_name || "file";
953
1001
  const safeFilename = filename.replace(/["\r\n]/g, "_");
954
1002
  const encodedFilename = encodeURIComponent(filename);
@@ -961,6 +1009,9 @@ class StorageManager {
961
1009
  `${disposition}; filename="${safeFilename}"; filename*=UTF-8''${encodedFilename}`
962
1010
  );
963
1011
  res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
1012
+ if (cloudRes.headers["content-encoding"]) {
1013
+ res.setHeader("Content-Encoding", cloudRes.headers["content-encoding"]);
1014
+ }
964
1015
  if (cloudRes.headers["content-range"]) {
965
1016
  res.setHeader("Content-Range", cloudRes.headers["content-range"]);
966
1017
  res.status(206);
@@ -1020,7 +1071,7 @@ class StorageManager {
1020
1071
  const cloudPublicId = rf.cloudPublicId || rf.cloud_public_id || fileId;
1021
1072
  const storageId = rf.storageId || rf.storage_id || null;
1022
1073
  if (this.query) {
1023
- const hasInspectionCol = this.filesTable === "storage_files";
1074
+ const hasInspectionCol = await this._hasInspectionColumn();
1024
1075
  const insertSql = hasInspectionCol ? `INSERT INTO ${this.filesTable} (
1025
1076
  id, entity_type, entity_id, inspection_id, storage_id,
1026
1077
  original_name, file_type, mime_type, file_size, cloud_url, cloud_public_id, created_at, updated_at
@@ -1077,7 +1128,7 @@ class StorageManager {
1077
1128
  const fileId = (0, import_uid.genUid)();
1078
1129
  const isInspection = entityType === "dot_inspection" || entityType === "inspection";
1079
1130
  const inspectionId = isInspection ? entityId : null;
1080
- const hasInspectionCol = this.filesTable === "storage_files";
1131
+ const hasInspectionCol = await this._hasInspectionColumn();
1081
1132
  const insertSql = hasInspectionCol ? `INSERT INTO ${this.filesTable} (
1082
1133
  id, entity_type, entity_id, inspection_id, storage_id,
1083
1134
  original_name, file_type, mime_type, file_size, cloud_url, cloud_public_id
@@ -229,6 +229,25 @@ export class StorageManager {
229
229
  this.activeStorageCache = { data: null, ts: 0 };
230
230
  }
231
231
 
232
+ async _hasInspectionColumn() {
233
+ if (this._hasInspectionCol !== undefined) return this._hasInspectionCol;
234
+ if (!this.query) {
235
+ this._hasInspectionCol = false;
236
+ return false;
237
+ }
238
+ try {
239
+ const res = await this.query(
240
+ `SELECT 1 FROM information_schema.columns WHERE table_name = $1 AND column_name = 'inspection_id' LIMIT 1`,
241
+ [this.filesTable]
242
+ );
243
+ this._hasInspectionCol = res.rows && res.rows.length > 0;
244
+ return this._hasInspectionCol;
245
+ } catch {
246
+ this._hasInspectionCol = false;
247
+ return false;
248
+ }
249
+ }
250
+
232
251
  async getCachedFileRecord(fileId) {
233
252
  if (!fileId) return null;
234
253
  const now = Date.now();
@@ -1017,7 +1036,7 @@ export class StorageManager {
1017
1036
  }
1018
1037
 
1019
1038
  _streamHttp(file, req, res, isInline, redirectCount = 0, knownProvider = null) {
1020
- let targetUrl = file.cloud_url;
1039
+ let targetUrl = file.cloud_url || file.url || file.view_url;
1021
1040
  if (!targetUrl) {
1022
1041
  if (file.storage_id && file.cloud_public_id) {
1023
1042
  return this._streamFromS3(file, req, res, isInline);
@@ -1037,6 +1056,9 @@ export class StorageManager {
1037
1056
  if (req?.headers && req.headers['range']) {
1038
1057
  requestHeaders['Range'] = req.headers['range'];
1039
1058
  }
1059
+ // Prevent upstream providers (Cloudinary, Vercel, S3) from sending compressed gzip/br streams
1060
+ // which would otherwise be piped raw to clients without correct decompression.
1061
+ requestHeaders['Accept-Encoding'] = 'identity';
1040
1062
 
1041
1063
  client.get(targetUrl, { agent, headers: requestHeaders }, (cloudRes) => {
1042
1064
  if (cloudRes.statusCode >= 300 && cloudRes.statusCode < 400 && cloudRes.headers.location) {
@@ -1054,7 +1076,41 @@ export class StorageManager {
1054
1076
  return res.status(cloudRes.statusCode).send('Error loading file from storage');
1055
1077
  }
1056
1078
 
1057
- const mimeType = cloudRes.headers['content-type'] || file.mime_type || 'application/octet-stream';
1079
+ const rawHeaderType = cloudRes.headers['content-type'];
1080
+ const ext = (file.original_name || '').split('.').pop()?.toLowerCase();
1081
+ const EXT_MIMES = {
1082
+ svg: 'image/svg+xml',
1083
+ png: 'image/png',
1084
+ jpg: 'image/jpeg',
1085
+ jpeg: 'image/jpeg',
1086
+ webp: 'image/webp',
1087
+ gif: 'image/gif',
1088
+ ico: 'image/x-icon',
1089
+ bmp: 'image/bmp',
1090
+ avif: 'image/avif',
1091
+ pdf: 'application/pdf',
1092
+ json: 'application/json',
1093
+ txt: 'text/plain',
1094
+ csv: 'text/csv',
1095
+ xml: 'application/xml',
1096
+ zip: 'application/zip',
1097
+ tar: 'application/x-tar',
1098
+ gz: 'application/gzip',
1099
+ mp4: 'video/mp4',
1100
+ webm: 'video/webm',
1101
+ mp3: 'audio/mpeg',
1102
+ wav: 'audio/wav',
1103
+ };
1104
+ const inferredMime = ext ? EXT_MIMES[ext] : null;
1105
+
1106
+ let mimeType = rawHeaderType;
1107
+ // If remote returned generic octet-stream, prioritize known DB mime_type or file extension
1108
+ if (!mimeType || mimeType === 'application/octet-stream') {
1109
+ mimeType = (file.mime_type && file.mime_type !== 'application/octet-stream')
1110
+ ? file.mime_type
1111
+ : inferredMime || rawHeaderType || 'application/octet-stream';
1112
+ }
1113
+
1058
1114
  const filename = file.original_name || 'file';
1059
1115
  const safeFilename = filename.replace(/["\r\n]/g, '_');
1060
1116
  const encodedFilename = encodeURIComponent(filename);
@@ -1069,6 +1125,10 @@ export class StorageManager {
1069
1125
  );
1070
1126
  res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
1071
1127
 
1128
+ if (cloudRes.headers['content-encoding']) {
1129
+ res.setHeader('Content-Encoding', cloudRes.headers['content-encoding']);
1130
+ }
1131
+
1072
1132
  if (cloudRes.headers['content-range']) {
1073
1133
  res.setHeader('Content-Range', cloudRes.headers['content-range']);
1074
1134
  res.status(206);
@@ -1140,7 +1200,7 @@ export class StorageManager {
1140
1200
  const storageId = rf.storageId || rf.storage_id || null;
1141
1201
 
1142
1202
  if (this.query) {
1143
- const hasInspectionCol = this.filesTable === 'storage_files';
1203
+ const hasInspectionCol = await this._hasInspectionColumn();
1144
1204
  const insertSql = hasInspectionCol
1145
1205
  ? `INSERT INTO ${this.filesTable} (
1146
1206
  id, entity_type, entity_id, inspection_id, storage_id,
@@ -1209,7 +1269,7 @@ export class StorageManager {
1209
1269
  const fileId = genUid();
1210
1270
  const isInspection = entityType === 'dot_inspection' || entityType === 'inspection';
1211
1271
  const inspectionId = isInspection ? entityId : null;
1212
- const hasInspectionCol = this.filesTable === 'storage_files';
1272
+ const hasInspectionCol = await this._hasInspectionColumn();
1213
1273
 
1214
1274
  const insertSql = hasInspectionCol
1215
1275
  ? `INSERT INTO ${this.filesTable} (
package/utils/utils/ui.js CHANGED
@@ -1,111 +1,111 @@
1
- /**
2
- * cynx/utils — UI and Formatting Helpers (ui.js)
3
- *
4
- * Universal classname merger (cn), clipboard helper, and friendly date/byte formatters.
5
- */
6
-
7
- /**
8
- * Universal class names combiner / merger (zero-dependency clsx replacement).
9
- * Handles strings, arrays, objects, booleans, and null/undefined values.
10
- *
11
- * @param {...any} inputs
12
- * @returns {string} Combined class string
13
- */
14
- export function cn(...inputs) {
15
- const classes = [];
16
-
17
- for (let i = 0; i < inputs.length; i++) {
18
- const arg = inputs[i];
19
- if (!arg) continue;
20
-
21
- const argType = typeof arg;
22
- if (argType === 'string' || argType === 'number') {
23
- classes.push(arg);
24
- } else if (Array.isArray(arg)) {
25
- if (arg.length) {
26
- const inner = cn(...arg);
27
- if (inner) classes.push(inner);
28
- }
29
- } else if (argType === 'object') {
30
- for (const key in arg) {
31
- if (Object.prototype.hasOwnProperty.call(arg, key) && arg[key]) {
32
- classes.push(key);
33
- }
34
- }
35
- }
36
- }
37
-
38
- return classes.join(' ');
39
- }
40
-
41
- /**
42
- * Safely copy text to clipboard with modern Clipboard API and legacy fallback.
43
- *
44
- * @param {string} text
45
- * @returns {Promise<boolean>}
46
- */
47
- export async function copyToClipboard(text) {
48
- if (typeof navigator !== 'undefined' && navigator.clipboard?.writeText) {
49
- try {
50
- await navigator.clipboard.writeText(text);
51
- return true;
52
- } catch {}
53
- }
54
-
55
- if (typeof document !== 'undefined') {
56
- try {
57
- const el = document.createElement('textarea');
58
- el.value = text;
59
- el.setAttribute('readonly', '');
60
- el.style.position = 'absolute';
61
- el.style.left = '-9999px';
62
- document.body.appendChild(el);
63
- el.select();
64
- const successful = document.execCommand('copy');
65
- document.body.removeChild(el);
66
- return successful;
67
- } catch {}
68
- }
69
-
70
- return false;
71
- }
72
-
73
- /**
74
- * Format relative time (e.g. "5s ago", "12m ago", "3h ago", "2d ago").
75
- *
76
- * @param {Date|string|number} d
77
- * @returns {string}
78
- */
79
- export function fmtAgo(d) {
80
- if (!d) return '—';
81
- const s = Math.floor((Date.now() - new Date(d).getTime()) / 1000);
82
- if (isNaN(s)) return '—';
83
- if (s < 60) return `${Math.max(0, s)}s ago`;
84
- if (s < 3600) return `${Math.floor(s / 60)}m ago`;
85
- if (s < 86400) return `${Math.floor(s / 3600)}h ago`;
86
- return `${Math.floor(s / 86400)}d ago`;
87
- }
88
-
89
- /**
90
- * Format localized date or return fallback dash.
91
- *
92
- * @param {Date|string|number} d
93
- * @param {string} [locale='ru-RU']
94
- * @returns {string}
95
- */
96
- export function fmtDate(d, locale = 'ru-RU') {
97
- if (!d) return '—';
98
- try {
99
- const date = new Date(d);
100
- return isNaN(date.getTime()) ? '—' : date.toLocaleString(locale);
101
- } catch {
102
- return '—';
103
- }
104
- }
105
-
106
- export default {
107
- cn,
108
- copyToClipboard,
109
- fmtAgo,
110
- fmtDate,
111
- };
1
+ /**
2
+ * cynx/utils — UI and Formatting Helpers (ui.js)
3
+ *
4
+ * Universal classname merger (cn), clipboard helper, and friendly date/byte formatters.
5
+ */
6
+
7
+ /**
8
+ * Universal class names combiner / merger (zero-dependency clsx replacement).
9
+ * Handles strings, arrays, objects, booleans, and null/undefined values.
10
+ *
11
+ * @param {...any} inputs
12
+ * @returns {string} Combined class string
13
+ */
14
+ export function cn(...inputs) {
15
+ const classes = [];
16
+
17
+ for (let i = 0; i < inputs.length; i++) {
18
+ const arg = inputs[i];
19
+ if (!arg) continue;
20
+
21
+ const argType = typeof arg;
22
+ if (argType === 'string' || argType === 'number') {
23
+ classes.push(arg);
24
+ } else if (Array.isArray(arg)) {
25
+ if (arg.length) {
26
+ const inner = cn(...arg);
27
+ if (inner) classes.push(inner);
28
+ }
29
+ } else if (argType === 'object') {
30
+ for (const key in arg) {
31
+ if (Object.prototype.hasOwnProperty.call(arg, key) && arg[key]) {
32
+ classes.push(key);
33
+ }
34
+ }
35
+ }
36
+ }
37
+
38
+ return classes.join(' ');
39
+ }
40
+
41
+ /**
42
+ * Safely copy text to clipboard with modern Clipboard API and legacy fallback.
43
+ *
44
+ * @param {string} text
45
+ * @returns {Promise<boolean>}
46
+ */
47
+ export async function copyToClipboard(text) {
48
+ if (typeof navigator !== 'undefined' && navigator.clipboard?.writeText) {
49
+ try {
50
+ await navigator.clipboard.writeText(text);
51
+ return true;
52
+ } catch {}
53
+ }
54
+
55
+ if (typeof document !== 'undefined') {
56
+ try {
57
+ const el = document.createElement('textarea');
58
+ el.value = text;
59
+ el.setAttribute('readonly', '');
60
+ el.style.position = 'absolute';
61
+ el.style.left = '-9999px';
62
+ document.body.appendChild(el);
63
+ el.select();
64
+ const successful = document.execCommand('copy');
65
+ document.body.removeChild(el);
66
+ return successful;
67
+ } catch {}
68
+ }
69
+
70
+ return false;
71
+ }
72
+
73
+ /**
74
+ * Format relative time (e.g. "5s ago", "12m ago", "3h ago", "2d ago").
75
+ *
76
+ * @param {Date|string|number} d
77
+ * @returns {string}
78
+ */
79
+ export function fmtAgo(d) {
80
+ if (!d) return '—';
81
+ const s = Math.floor((Date.now() - new Date(d).getTime()) / 1000);
82
+ if (isNaN(s)) return '—';
83
+ if (s < 60) return `${Math.max(0, s)}s ago`;
84
+ if (s < 3600) return `${Math.floor(s / 60)}m ago`;
85
+ if (s < 86400) return `${Math.floor(s / 3600)}h ago`;
86
+ return `${Math.floor(s / 86400)}d ago`;
87
+ }
88
+
89
+ /**
90
+ * Format localized date or return fallback dash.
91
+ *
92
+ * @param {Date|string|number} d
93
+ * @param {string} [locale='ru-RU']
94
+ * @returns {string}
95
+ */
96
+ export function fmtDate(d, locale = 'ru-RU') {
97
+ if (!d) return '—';
98
+ try {
99
+ const date = new Date(d);
100
+ return isNaN(date.getTime()) ? '—' : date.toLocaleString(locale);
101
+ } catch {
102
+ return '—';
103
+ }
104
+ }
105
+
106
+ export default {
107
+ cn,
108
+ copyToClipboard,
109
+ fmtAgo,
110
+ fmtDate,
111
+ };
@@ -1,79 +1,79 @@
1
- <script setup lang="ts">
2
- import { computed } from "vue";
3
-
4
- const VARIANTS: Record<string, any> = {
5
- ok: { bg: "var(--success-bg, rgba(39,174,96,.1))", color: "var(--success, #16a34a)", border: "var(--sd, rgba(39,174,96,.2))", dot: "var(--success, #16a34a)" },
6
- success: { bg: "var(--success-bg, rgba(39,174,96,.1))", color: "var(--success, #16a34a)", border: "var(--sd, rgba(39,174,96,.2))", dot: "var(--success, #16a34a)" },
7
- err: { bg: "var(--danger-bg, rgba(246,71,71,.1))", color: "var(--danger, #ef4444)", border: "var(--dd, rgba(246,71,71,.2))", dot: "var(--danger, #ef4444)" },
8
- danger: { bg: "var(--danger-bg, rgba(246,71,71,.1))", color: "var(--danger, #ef4444)", border: "var(--dd, rgba(246,71,71,.2))", dot: "var(--danger, #ef4444)" },
9
- error: { bg: "var(--danger-bg, rgba(246,71,71,.1))", color: "var(--danger, #ef4444)", border: "var(--dd, rgba(246,71,71,.2))", dot: "var(--danger, #ef4444)" },
10
- warn: { bg: "var(--accent-bg, rgba(249,158,44,.1))", color: "var(--accent, #f59e0b)", border: "var(--accent-border, rgba(249,158,44,.25))", dot: "var(--accent, #f59e0b)" },
11
- warning: { bg: "var(--accent-bg, rgba(249,158,44,.1))", color: "var(--accent, #f59e0b)", border: "var(--accent-border, rgba(249,158,44,.25))", dot: "var(--accent, #f59e0b)" },
12
- blue: { bg: "var(--blue-bg, rgba(39,130,228,.1))", color: "var(--blue, #2563eb)", border: "var(--infd, rgba(39,130,228,.2))", dot: "var(--blue, #2563eb)" },
13
- info: { bg: "var(--blue-bg, rgba(39,130,228,.1))", color: "var(--blue, #2563eb)", border: "var(--infd, rgba(39,130,228,.2))", dot: "var(--blue, #2563eb)" },
14
- teal: { bg: "rgba(18,103,93,.1)", color: "var(--accent, #12675D)", border: "rgba(18,103,93,.25)", dot: "var(--accent, #12675D)" },
15
- primary: { bg: "rgba(18,103,93,.1)", color: "var(--accent, #12675D)", border: "rgba(18,103,93,.25)", dot: "var(--accent, #12675D)" },
16
- dim: { bg: "var(--grey-1, #f5f5f8)", color: "var(--grey-4, #64748b)", border: "var(--border, #e2e8f0)", dot: "var(--grey-4, #64748b)" },
17
- neutral: { bg: "var(--grey-1, #f5f5f8)", color: "var(--grey-4, #64748b)", border: "var(--border, #e2e8f0)", dot: "var(--grey-4, #64748b)" },
18
- secondary: { bg: "var(--grey-1, #f5f5f8)", color: "var(--grey-4, #64748b)", border: "var(--border, #e2e8f0)", dot: "var(--grey-4, #64748b)" },
19
- ghost: { bg: "transparent", color: "var(--t2, #64748b)", border: "var(--bd, #e2e8f0)", dot: "var(--t3, #94a3b8)" },
20
- };
21
-
22
- const SIZE_MAP: Record<string, any> = {
23
- xs: { padding: "1.5px 6px", plainPadding: "1px 0", fontSize: "9.5px", dotSize: 5 },
24
- sm: { padding: "2px 8px", plainPadding: "2px 0", fontSize: "11px", dotSize: 6 },
25
- md: { padding: "3px 9px", plainPadding: "2px 0", fontSize: "12px", dotSize: 6.5 },
26
- lg: { padding: "4px 12px", plainPadding: "3px 0", fontSize: "13px", dotSize: 8 },
27
- };
28
-
29
- const props = withDefaults(
30
- defineProps<{
31
- variant?: string;
32
- size?: "xs" | "sm" | "md" | "lg";
33
- dot?: boolean;
34
- pill?: boolean;
35
- plain?: boolean;
36
- minimal?: boolean;
37
- }>(),
38
- {
39
- variant: "dim",
40
- size: "sm",
41
- dot: false,
42
- pill: true,
43
- plain: false,
44
- minimal: false,
45
- }
46
- );
47
-
48
- const v = computed(() => VARIANTS[props.variant] || VARIANTS.dim);
49
- const s = computed(() => SIZE_MAP[props.size] || SIZE_MAP.sm);
50
- const isPlain = computed(() => props.plain || props.minimal);
51
- </script>
52
-
53
- <template>
54
- <span
55
- :class="[
56
- 'inline-flex items-center gap-1.5 select-none',
57
- isPlain ? 'font-semibold' : 'font-bold leading-none',
58
- isPlain ? '' : (pill ? 'rounded-full' : 'rounded-md')
59
- ]"
60
- :style="{
61
- background: isPlain ? 'transparent' : v.bg,
62
- color: v.color,
63
- border: isPlain ? 'none' : `1px solid ${v.border}`,
64
- padding: isPlain ? s.plainPadding : s.padding,
65
- fontSize: s.fontSize,
66
- }"
67
- >
68
- <span
69
- v-if="dot"
70
- class="rounded-full flex-shrink-0"
71
- :style="{
72
- width: `${s.dotSize}px`,
73
- height: `${s.dotSize}px`,
74
- background: v.dot,
75
- }"
76
- />
77
- <slot />
78
- </span>
79
- </template>
1
+ <script setup lang="ts">
2
+ import { computed } from "vue";
3
+
4
+ const VARIANTS: Record<string, any> = {
5
+ ok: { bg: "var(--success-bg, rgba(39,174,96,.1))", color: "var(--success, #16a34a)", border: "var(--sd, rgba(39,174,96,.2))", dot: "var(--success, #16a34a)" },
6
+ success: { bg: "var(--success-bg, rgba(39,174,96,.1))", color: "var(--success, #16a34a)", border: "var(--sd, rgba(39,174,96,.2))", dot: "var(--success, #16a34a)" },
7
+ err: { bg: "var(--danger-bg, rgba(246,71,71,.1))", color: "var(--danger, #ef4444)", border: "var(--dd, rgba(246,71,71,.2))", dot: "var(--danger, #ef4444)" },
8
+ danger: { bg: "var(--danger-bg, rgba(246,71,71,.1))", color: "var(--danger, #ef4444)", border: "var(--dd, rgba(246,71,71,.2))", dot: "var(--danger, #ef4444)" },
9
+ error: { bg: "var(--danger-bg, rgba(246,71,71,.1))", color: "var(--danger, #ef4444)", border: "var(--dd, rgba(246,71,71,.2))", dot: "var(--danger, #ef4444)" },
10
+ warn: { bg: "var(--accent-bg, rgba(249,158,44,.1))", color: "var(--accent, #f59e0b)", border: "var(--accent-border, rgba(249,158,44,.25))", dot: "var(--accent, #f59e0b)" },
11
+ warning: { bg: "var(--accent-bg, rgba(249,158,44,.1))", color: "var(--accent, #f59e0b)", border: "var(--accent-border, rgba(249,158,44,.25))", dot: "var(--accent, #f59e0b)" },
12
+ blue: { bg: "var(--blue-bg, rgba(39,130,228,.1))", color: "var(--blue, #2563eb)", border: "var(--infd, rgba(39,130,228,.2))", dot: "var(--blue, #2563eb)" },
13
+ info: { bg: "var(--blue-bg, rgba(39,130,228,.1))", color: "var(--blue, #2563eb)", border: "var(--infd, rgba(39,130,228,.2))", dot: "var(--blue, #2563eb)" },
14
+ teal: { bg: "rgba(18,103,93,.1)", color: "var(--accent, #12675D)", border: "rgba(18,103,93,.25)", dot: "var(--accent, #12675D)" },
15
+ primary: { bg: "rgba(18,103,93,.1)", color: "var(--accent, #12675D)", border: "rgba(18,103,93,.25)", dot: "var(--accent, #12675D)" },
16
+ dim: { bg: "var(--grey-1, #f5f5f8)", color: "var(--grey-4, #64748b)", border: "var(--border, #e2e8f0)", dot: "var(--grey-4, #64748b)" },
17
+ neutral: { bg: "var(--grey-1, #f5f5f8)", color: "var(--grey-4, #64748b)", border: "var(--border, #e2e8f0)", dot: "var(--grey-4, #64748b)" },
18
+ secondary: { bg: "var(--grey-1, #f5f5f8)", color: "var(--grey-4, #64748b)", border: "var(--border, #e2e8f0)", dot: "var(--grey-4, #64748b)" },
19
+ ghost: { bg: "transparent", color: "var(--t2, #64748b)", border: "var(--bd, #e2e8f0)", dot: "var(--t3, #94a3b8)" },
20
+ };
21
+
22
+ const SIZE_MAP: Record<string, any> = {
23
+ xs: { padding: "1.5px 6px", plainPadding: "1px 0", fontSize: "9.5px", dotSize: 5 },
24
+ sm: { padding: "2px 8px", plainPadding: "2px 0", fontSize: "11px", dotSize: 6 },
25
+ md: { padding: "3px 9px", plainPadding: "2px 0", fontSize: "12px", dotSize: 6.5 },
26
+ lg: { padding: "4px 12px", plainPadding: "3px 0", fontSize: "13px", dotSize: 8 },
27
+ };
28
+
29
+ const props = withDefaults(
30
+ defineProps<{
31
+ variant?: string;
32
+ size?: "xs" | "sm" | "md" | "lg";
33
+ dot?: boolean;
34
+ pill?: boolean;
35
+ plain?: boolean;
36
+ minimal?: boolean;
37
+ }>(),
38
+ {
39
+ variant: "dim",
40
+ size: "sm",
41
+ dot: false,
42
+ pill: true,
43
+ plain: false,
44
+ minimal: false,
45
+ }
46
+ );
47
+
48
+ const v = computed(() => VARIANTS[props.variant] || VARIANTS.dim);
49
+ const s = computed(() => SIZE_MAP[props.size] || SIZE_MAP.sm);
50
+ const isPlain = computed(() => props.plain || props.minimal);
51
+ </script>
52
+
53
+ <template>
54
+ <span
55
+ :class="[
56
+ 'inline-flex items-center gap-1.5 select-none',
57
+ isPlain ? 'font-semibold' : 'font-bold leading-none',
58
+ isPlain ? '' : (pill ? 'rounded-full' : 'rounded-md')
59
+ ]"
60
+ :style="{
61
+ background: isPlain ? 'transparent' : v.bg,
62
+ color: v.color,
63
+ border: isPlain ? 'none' : `1px solid ${v.border}`,
64
+ padding: isPlain ? s.plainPadding : s.padding,
65
+ fontSize: s.fontSize,
66
+ }"
67
+ >
68
+ <span
69
+ v-if="dot"
70
+ class="rounded-full flex-shrink-0"
71
+ :style="{
72
+ width: `${s.dotSize}px`,
73
+ height: `${s.dotSize}px`,
74
+ background: v.dot,
75
+ }"
76
+ />
77
+ <slot />
78
+ </span>
79
+ </template>