codexmate 0.0.34 → 0.0.37
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 +14 -5
- package/README.zh.md +14 -5
- package/cli.js +74 -61
- package/package.json +2 -1
- package/web-ui/app.js +32 -2
- package/web-ui/index.html +1 -1
- package/web-ui/logic.sessions.mjs +6 -5
- package/web-ui/modules/app.computed.dashboard.mjs +4 -0
- package/web-ui/modules/app.computed.session.mjs +147 -6
- package/web-ui/modules/app.methods.claude-config.mjs +4 -0
- package/web-ui/modules/app.methods.navigation.mjs +32 -16
- package/web-ui/modules/app.methods.session-browser.mjs +7 -0
- package/web-ui/modules/app.methods.session-trash.mjs +30 -0
- package/web-ui/modules/i18n.dict.mjs +5 -0
- package/web-ui/modules/sessions-filters-url.mjs +65 -12
- package/web-ui/modules/skills.methods.mjs +31 -0
- package/web-ui/partials/index/layout-header.html +20 -11
- package/web-ui/partials/index/panel-config-claude.html +5 -3
- package/web-ui/partials/index/panel-config-codex.html +1 -1
- package/web-ui/partials/index/panel-market.html +76 -149
- package/web-ui/partials/index/panel-sessions.html +2 -2
- package/web-ui/partials/index/panel-settings.html +4 -2
- package/web-ui/partials/index/panel-usage.html +111 -68
- package/web-ui/res/vue.runtime.global.prod.js +7 -0
- package/web-ui/res/web-ui-render.precompiled.js +7269 -0
- package/web-ui/session-helpers.mjs +15 -4
- package/web-ui/source-bundle.cjs +73 -1
- package/web-ui/styles/base-theme.css +10 -0
- package/web-ui/styles/layout-shell.css +65 -27
- package/web-ui/styles/navigation-panels.css +8 -0
- package/web-ui/styles/responsive.css +50 -9
- package/web-ui/styles/sessions-usage.css +501 -336
- package/web-ui/styles/skills-market.css +294 -0
- package/web-ui/styles/titles-cards.css +14 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { buildSessionListParams } from './logic.mjs';
|
|
1
|
+
import { buildSessionListParams, formatSessionTimelineTimestamp } from './logic.mjs';
|
|
2
2
|
|
|
3
3
|
function clearSessionTimelineRefs(vm) {
|
|
4
4
|
if (typeof vm.clearSessionTimelineRefs === 'function') {
|
|
@@ -90,7 +90,7 @@ export function switchMainTab(tab) {
|
|
|
90
90
|
emitSessionLoadDebug(this, 'switchMainTab:start', `from=${previousTab}\nto=${nextTab}`);
|
|
91
91
|
this.mainTab = nextTab;
|
|
92
92
|
|
|
93
|
-
if (leavingSessions) {
|
|
93
|
+
if (leavingSessions && this.preserveSessionRenderOnTabLeave !== true) {
|
|
94
94
|
const teardown = () => {
|
|
95
95
|
if (this.mainTab === 'sessions') return;
|
|
96
96
|
if (typeof this.finalizeSessionTabTeardown === 'function') {
|
|
@@ -156,7 +156,13 @@ export function switchMainTab(tab) {
|
|
|
156
156
|
if (nextTab !== 'orchestration' && typeof this.stopTaskOrchestrationPolling === 'function') {
|
|
157
157
|
this.stopTaskOrchestrationPolling();
|
|
158
158
|
}
|
|
159
|
-
if (
|
|
159
|
+
if (
|
|
160
|
+
nextTab === 'sessions'
|
|
161
|
+
&& (
|
|
162
|
+
!this.sessionListRenderEnabled
|
|
163
|
+
|| !this.sessionPreviewRenderEnabled
|
|
164
|
+
)
|
|
165
|
+
) {
|
|
160
166
|
this.prepareSessionTabRender();
|
|
161
167
|
}
|
|
162
168
|
const shouldLoadTrashListOnSettingsEnter = nextTab === 'settings'
|
|
@@ -259,7 +265,12 @@ export async function loadSessions(api, options = {}) {
|
|
|
259
265
|
clearSessionTimelineRefs(this);
|
|
260
266
|
} else {
|
|
261
267
|
loadSucceeded = true;
|
|
262
|
-
|
|
268
|
+
const rawSessions = Array.isArray(res.sessions) ? res.sessions : [];
|
|
269
|
+
this.sessionsList = rawSessions.filter(s => s && typeof s === 'object');
|
|
270
|
+
for (const session of this.sessionsList) {
|
|
271
|
+
const rawUpdatedAt = typeof session.updatedAt === 'string' ? session.updatedAt : '';
|
|
272
|
+
session.updatedAtLabel = formatSessionTimelineTimestamp(rawUpdatedAt);
|
|
273
|
+
}
|
|
263
274
|
emitSessionLoadDebug(this, 'loadSessions:response', `sessions=${this.sessionsList.length}`);
|
|
264
275
|
if (typeof this.primeSessionListRender === 'function') {
|
|
265
276
|
this.primeSessionListRender();
|
package/web-ui/source-bundle.cjs
CHANGED
|
@@ -7,6 +7,11 @@ const JS_IMPORT_RE = /(?:^|\n)\s*import\s+(?:[\s\S]*?\s+from\s+)?['"](\.[^'"]+)[
|
|
|
7
7
|
const JS_EXPORT_FROM_RE = /(?:^|\n)\s*export\s+\*\s+from\s+['"](\.[^'"]+)['"]\s*;?/g;
|
|
8
8
|
const JS_RELATIVE_IMPORT_STATEMENT_RE = /(^|\n)([ \t]*)import\s+([\s\S]*?)\s+from\s+['"](\.[^'"]+)['"]\s*;?[ \t]*/g;
|
|
9
9
|
const IDENTIFIER_RE = /^[A-Za-z_$][\w$]*$/;
|
|
10
|
+
const VOID_HTML_TAGS = new Set([
|
|
11
|
+
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
|
|
12
|
+
'link', 'meta', 'param', 'source', 'track', 'wbr'
|
|
13
|
+
]);
|
|
14
|
+
const PRECOMPILED_RENDER_PATH = path.join(__dirname, 'res', 'web-ui-render.precompiled.js');
|
|
10
15
|
|
|
11
16
|
function stripBom(content) {
|
|
12
17
|
return content.replace(/^\uFEFF/, '');
|
|
@@ -58,6 +63,69 @@ function bundleCssFile(filePath, stack = []) {
|
|
|
58
63
|
});
|
|
59
64
|
}
|
|
60
65
|
|
|
66
|
+
function extractElementInnerHtmlById(html, id) {
|
|
67
|
+
const escapedId = String(id || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
68
|
+
const startRe = new RegExp(`<([A-Za-z][\\w:-]*)(?=[^>]*\\bid=["']${escapedId}["'])[^>]*>`, 'i');
|
|
69
|
+
const startMatch = startRe.exec(html);
|
|
70
|
+
if (!startMatch) {
|
|
71
|
+
throw new Error(`Unable to find #${id} in bundled Web UI HTML`);
|
|
72
|
+
}
|
|
73
|
+
const tagName = startMatch[1].toLowerCase();
|
|
74
|
+
const openEnd = startMatch.index + startMatch[0].length;
|
|
75
|
+
const tagRe = new RegExp(`</?${tagName}\\b[^>]*>`, 'ig');
|
|
76
|
+
tagRe.lastIndex = openEnd;
|
|
77
|
+
let depth = 1;
|
|
78
|
+
let match = tagRe.exec(html);
|
|
79
|
+
while (match) {
|
|
80
|
+
const rawTag = match[0];
|
|
81
|
+
const isClosing = rawTag.startsWith('</');
|
|
82
|
+
const isSelfClosing = rawTag.endsWith('/>') || VOID_HTML_TAGS.has(tagName);
|
|
83
|
+
if (isClosing) {
|
|
84
|
+
depth -= 1;
|
|
85
|
+
} else if (!isSelfClosing) {
|
|
86
|
+
depth += 1;
|
|
87
|
+
}
|
|
88
|
+
if (depth === 0) {
|
|
89
|
+
return html.slice(openEnd, match.index);
|
|
90
|
+
}
|
|
91
|
+
match = tagRe.exec(html);
|
|
92
|
+
}
|
|
93
|
+
throw new Error(`Unable to find closing </${tagName}> for #${id} in bundled Web UI HTML`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function compileWebUiTemplateToRenderScript(htmlPath = path.join(__dirname, 'index.html')) {
|
|
97
|
+
let compile;
|
|
98
|
+
try {
|
|
99
|
+
({ compile } = require('@vue/compiler-dom'));
|
|
100
|
+
} catch (e) {
|
|
101
|
+
throw new Error(`Unable to compile Web UI template: @vue/compiler-dom is required (${e.message})`);
|
|
102
|
+
}
|
|
103
|
+
const html = bundleHtmlFile(htmlPath);
|
|
104
|
+
const appTemplate = extractElementInnerHtmlById(html, 'app');
|
|
105
|
+
const result = compile(appTemplate, { mode: 'function', prefixIdentifiers: true });
|
|
106
|
+
return [
|
|
107
|
+
'window.__CODEXMATE_WEB_UI_RENDER__ = (() => {',
|
|
108
|
+
result.code.trimEnd(),
|
|
109
|
+
'})();',
|
|
110
|
+
''
|
|
111
|
+
].join('\n');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function readPrecompiledWebUiRenderScript(entryPath = PRECOMPILED_RENDER_PATH) {
|
|
115
|
+
const resolvedPath = path.isAbsolute(entryPath)
|
|
116
|
+
? entryPath
|
|
117
|
+
: path.resolve(__dirname, entryPath);
|
|
118
|
+
try {
|
|
119
|
+
const source = readUtf8Text(resolvedPath).trimEnd();
|
|
120
|
+
return source ? `${source}\n` : '';
|
|
121
|
+
} catch (error) {
|
|
122
|
+
if (error && error.code === 'ENOENT') {
|
|
123
|
+
return '';
|
|
124
|
+
}
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
61
129
|
function resolveJavaScriptDependencies(filePath) {
|
|
62
130
|
const source = readUtf8Text(filePath);
|
|
63
131
|
const dependencies = [];
|
|
@@ -212,7 +280,8 @@ function readBundledWebUiScript(entryPath = path.join(__dirname, 'app.js')) {
|
|
|
212
280
|
}
|
|
213
281
|
|
|
214
282
|
function readExecutableBundledWebUiScript(entryPath = path.join(__dirname, 'app.js')) {
|
|
215
|
-
|
|
283
|
+
const renderScript = readPrecompiledWebUiRenderScript() || compileWebUiTemplateToRenderScript();
|
|
284
|
+
return `${renderScript}\n${bundleExecutableJavaScriptFile(entryPath, { preserveExports: false })}`;
|
|
216
285
|
}
|
|
217
286
|
|
|
218
287
|
function readExecutableBundledJavaScriptModule(entryPath) {
|
|
@@ -224,6 +293,9 @@ function readExecutableBundledJavaScriptModule(entryPath) {
|
|
|
224
293
|
|
|
225
294
|
module.exports = {
|
|
226
295
|
collectJavaScriptFiles,
|
|
296
|
+
compileWebUiTemplateToRenderScript,
|
|
297
|
+
extractElementInnerHtmlById,
|
|
298
|
+
readPrecompiledWebUiRenderScript,
|
|
227
299
|
readUtf8Text,
|
|
228
300
|
readBundledWebUiHtml,
|
|
229
301
|
readBundledWebUiCss,
|
|
@@ -26,6 +26,16 @@
|
|
|
26
26
|
--color-success: #4B8B6A;
|
|
27
27
|
--color-error: #C44536;
|
|
28
28
|
|
|
29
|
+
/* Delta 指示色 */
|
|
30
|
+
--color-delta-up: #4caf50;
|
|
31
|
+
--color-delta-down: #f44336;
|
|
32
|
+
|
|
33
|
+
/* 热力图色阶 */
|
|
34
|
+
--color-heatmap-1: rgba(200, 121, 99, 0.2);
|
|
35
|
+
--color-heatmap-2: rgba(200, 121, 99, 0.4);
|
|
36
|
+
--color-heatmap-3: rgba(200, 121, 99, 0.6);
|
|
37
|
+
--color-heatmap-4: rgba(200, 121, 99, 0.85);
|
|
38
|
+
|
|
29
39
|
--bg-warm-gradient:
|
|
30
40
|
radial-gradient(circle at 14% 8%, rgba(255, 219, 196, 0.5) 0%, rgba(255, 219, 196, 0) 32%),
|
|
31
41
|
radial-gradient(circle at 88% 0%, rgba(252, 239, 207, 0.58) 0%, rgba(252, 239, 207, 0) 30%),
|
|
@@ -415,6 +415,14 @@ body::after {
|
|
|
415
415
|
word-break: break-word;
|
|
416
416
|
}
|
|
417
417
|
|
|
418
|
+
.side-item-ghost {
|
|
419
|
+
opacity: 0;
|
|
420
|
+
pointer-events: none;
|
|
421
|
+
user-select: none;
|
|
422
|
+
cursor: default;
|
|
423
|
+
flex: 0 0 auto;
|
|
424
|
+
}
|
|
425
|
+
|
|
418
426
|
@media (min-width: 721px) {
|
|
419
427
|
body:not(.force-compact) #app > .top-tabs {
|
|
420
428
|
display: none;
|
|
@@ -425,58 +433,88 @@ body::after {
|
|
|
425
433
|
display: flex;
|
|
426
434
|
flex-direction: column;
|
|
427
435
|
align-items: flex-start;
|
|
428
|
-
gap: 10px;
|
|
429
436
|
margin-bottom: 0;
|
|
430
|
-
padding:
|
|
431
|
-
border-bottom:
|
|
437
|
+
padding: 16px 12px 20px;
|
|
438
|
+
border-bottom: 0.5px solid rgba(0, 0, 0, 0.06);
|
|
439
|
+
background: linear-gradient(180deg, rgba(255, 255, 255, 0.5) 0%, rgba(255, 255, 255, 0) 100%);
|
|
432
440
|
}
|
|
433
441
|
|
|
434
442
|
.brand-head {
|
|
435
443
|
display: flex;
|
|
436
444
|
align-items: center;
|
|
437
|
-
gap:
|
|
445
|
+
gap: 10px;
|
|
438
446
|
}
|
|
439
447
|
|
|
440
448
|
.brand-logo {
|
|
441
|
-
width:
|
|
442
|
-
height:
|
|
443
|
-
border-radius:
|
|
449
|
+
width: 28px;
|
|
450
|
+
height: 28px;
|
|
451
|
+
border-radius: 6px;
|
|
444
452
|
object-fit: cover;
|
|
445
453
|
flex-shrink: 0;
|
|
446
|
-
box-shadow:
|
|
447
|
-
|
|
448
|
-
|
|
454
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08), inset 0 1px 0 rgba(255, 255, 255, 0.5);
|
|
455
|
+
transition: width 0.24s var(--ease-smooth), height 0.24s var(--ease-smooth), box-shadow 0.24s var(--ease-smooth);
|
|
456
|
+
position: relative;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
.brand-logo::after {
|
|
460
|
+
content: "";
|
|
461
|
+
position: absolute;
|
|
462
|
+
inset: -4px;
|
|
463
|
+
background: radial-gradient(circle, rgba(0, 122, 255, 0.12), transparent 70%);
|
|
464
|
+
opacity: 0;
|
|
465
|
+
transition: opacity 0.3s var(--ease-smooth);
|
|
466
|
+
pointer-events: none;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
.brand-block:hover .brand-logo {
|
|
470
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12), inset 0 1px 0 rgba(255, 255, 255, 0.6);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
.brand-block:hover .brand-logo::after {
|
|
474
|
+
opacity: 1;
|
|
449
475
|
}
|
|
450
476
|
|
|
451
477
|
.brand-copy {
|
|
452
478
|
display: flex;
|
|
453
479
|
flex-direction: column;
|
|
454
|
-
gap:
|
|
480
|
+
gap: 0;
|
|
455
481
|
min-width: 0;
|
|
456
482
|
}
|
|
457
483
|
|
|
458
484
|
.brand-kicker {
|
|
459
|
-
font-size:
|
|
460
|
-
line-height: 1.
|
|
461
|
-
font-family:
|
|
462
|
-
color:
|
|
463
|
-
letter-spacing: -0.
|
|
464
|
-
font-weight:
|
|
485
|
+
font-size: 15px;
|
|
486
|
+
line-height: 1.2;
|
|
487
|
+
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", system-ui, sans-serif;
|
|
488
|
+
color: #1d1d1f;
|
|
489
|
+
letter-spacing: -0.01em;
|
|
490
|
+
font-weight: 500;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
@supports (-webkit-background-clip: text) or (background-clip: text) {
|
|
494
|
+
.brand-kicker {
|
|
495
|
+
background: linear-gradient(180deg, #1d1d1f 0%, #2d2d2f 100%);
|
|
496
|
+
-webkit-background-clip: text;
|
|
497
|
+
-webkit-text-fill-color: transparent;
|
|
498
|
+
background-clip: text;
|
|
499
|
+
}
|
|
465
500
|
}
|
|
466
501
|
|
|
467
502
|
.brand-version {
|
|
468
|
-
font-size:
|
|
469
|
-
font-weight:
|
|
470
|
-
color:
|
|
471
|
-
vertical-align:
|
|
472
|
-
|
|
503
|
+
font-size: 13px;
|
|
504
|
+
font-weight: 400;
|
|
505
|
+
color: #8e8e93;
|
|
506
|
+
vertical-align: baseline;
|
|
507
|
+
-webkit-text-fill-color: #8e8e93;
|
|
473
508
|
}
|
|
474
509
|
|
|
475
|
-
.brand-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
510
|
+
.brand-version-fade-enter-active,
|
|
511
|
+
.brand-version-fade-leave-active {
|
|
512
|
+
transition: opacity 0.2s var(--ease-smooth);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
.brand-version-fade-enter-from,
|
|
516
|
+
.brand-version-fade-leave-to {
|
|
517
|
+
opacity: 0;
|
|
480
518
|
}
|
|
481
519
|
|
|
482
520
|
.github-badge {
|
|
@@ -38,10 +38,18 @@
|
|
|
38
38
|
margin: 0 0 12px;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
.status-strip-placeholder {
|
|
42
|
+
visibility: hidden;
|
|
43
|
+
pointer-events: none;
|
|
44
|
+
user-select: none;
|
|
45
|
+
}
|
|
46
|
+
|
|
41
47
|
/* Give the status strip a bit more breathing room under the sticky header. */
|
|
42
48
|
.main-panel-topbar .status-strip {
|
|
43
49
|
margin-top: 10px;
|
|
44
50
|
margin-bottom: 16px;
|
|
51
|
+
min-height: 36px;
|
|
52
|
+
align-items: center;
|
|
45
53
|
}
|
|
46
54
|
|
|
47
55
|
.lang-toggle {
|
|
@@ -85,11 +85,21 @@ textarea:focus-visible {
|
|
|
85
85
|
:root {
|
|
86
86
|
--side-rail-width: min(188px, 46vw);
|
|
87
87
|
}
|
|
88
|
+
|
|
89
|
+
.container {
|
|
90
|
+
display: flex;
|
|
91
|
+
flex-direction: column;
|
|
92
|
+
height: 100vh;
|
|
93
|
+
min-height: 100vh;
|
|
94
|
+
overflow: hidden;
|
|
95
|
+
}
|
|
96
|
+
|
|
88
97
|
.app-shell {
|
|
89
98
|
grid-template-columns: 1fr;
|
|
90
|
-
|
|
99
|
+
flex: 1 1 auto;
|
|
100
|
+
min-height: 0;
|
|
91
101
|
height: auto;
|
|
92
|
-
overflow:
|
|
102
|
+
overflow: hidden;
|
|
93
103
|
}
|
|
94
104
|
|
|
95
105
|
.side-rail {
|
|
@@ -102,26 +112,34 @@ textarea:focus-visible {
|
|
|
102
112
|
|
|
103
113
|
.top-tabs {
|
|
104
114
|
display: flex !important;
|
|
115
|
+
flex: 0 0 auto;
|
|
105
116
|
flex-wrap: nowrap;
|
|
106
117
|
overflow-x: auto;
|
|
107
118
|
overflow-y: hidden;
|
|
108
119
|
padding-bottom: 2px;
|
|
109
120
|
margin-left: -2px;
|
|
110
121
|
margin-right: -2px;
|
|
122
|
+
background: rgba(255, 248, 241, 0.96);
|
|
123
|
+
position: relative;
|
|
124
|
+
z-index: 30;
|
|
111
125
|
}
|
|
112
126
|
|
|
113
127
|
.main-panel {
|
|
114
128
|
padding: 0 12px 16px;
|
|
115
|
-
height:
|
|
116
|
-
|
|
129
|
+
height: 100%;
|
|
130
|
+
min-height: 0;
|
|
131
|
+
overflow-y: auto;
|
|
132
|
+
-webkit-overflow-scrolling: touch;
|
|
117
133
|
}
|
|
118
134
|
|
|
119
135
|
.main-panel-topbar {
|
|
120
|
-
position:
|
|
136
|
+
position: sticky;
|
|
137
|
+
top: 0;
|
|
138
|
+
z-index: 20;
|
|
121
139
|
margin: 0 0 10px;
|
|
122
|
-
padding: 0;
|
|
123
|
-
background:
|
|
124
|
-
backdrop-filter:
|
|
140
|
+
padding: 0 0 8px;
|
|
141
|
+
background: rgba(255, 248, 241, 0.96);
|
|
142
|
+
backdrop-filter: blur(14px) saturate(130%);
|
|
125
143
|
}
|
|
126
144
|
|
|
127
145
|
.panel-header-refined {
|
|
@@ -155,10 +173,29 @@ textarea:focus-visible {
|
|
|
155
173
|
}
|
|
156
174
|
|
|
157
175
|
.status-strip {
|
|
158
|
-
grid-template-columns: 1fr;
|
|
159
176
|
gap: 12px;
|
|
160
177
|
}
|
|
161
178
|
|
|
179
|
+
.main-panel-topbar .status-strip {
|
|
180
|
+
display: flex;
|
|
181
|
+
flex-wrap: nowrap;
|
|
182
|
+
gap: 8px;
|
|
183
|
+
overflow-x: auto;
|
|
184
|
+
overflow-y: hidden;
|
|
185
|
+
padding-bottom: 2px;
|
|
186
|
+
scrollbar-width: none;
|
|
187
|
+
-webkit-overflow-scrolling: touch;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.main-panel-topbar .status-strip::-webkit-scrollbar {
|
|
191
|
+
display: none;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.main-panel-topbar .status-chip {
|
|
195
|
+
flex: 0 0 auto;
|
|
196
|
+
min-width: max-content;
|
|
197
|
+
}
|
|
198
|
+
|
|
162
199
|
.market-grid {
|
|
163
200
|
grid-template-columns: 1fr;
|
|
164
201
|
}
|
|
@@ -212,6 +249,10 @@ textarea:focus-visible {
|
|
|
212
249
|
min-width: 100%;
|
|
213
250
|
}
|
|
214
251
|
|
|
252
|
+
.main-panel-topbar .status-chip {
|
|
253
|
+
min-width: max-content;
|
|
254
|
+
}
|
|
255
|
+
|
|
215
256
|
.btn-add,
|
|
216
257
|
.btn-tool,
|
|
217
258
|
.card-action-btn,
|