@hncode/hncode 0.4.1 → 0.5.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 +3 -16
- package/README_CN.md +1 -15
- package/bin/hncode +22 -9
- package/package.json +3 -2
- package/src/agent.js +145 -33
- package/src/bootstrap.js +56 -0
- package/src/btw-panel.js +224 -0
- package/src/cache.js +3 -2
- package/src/cell-buffer.js +173 -0
- package/src/ci.js +4 -1
- package/src/clipboard.js +202 -43
- package/src/colors.js +175 -30
- package/src/config.js +376 -34
- package/src/context-bar.js +237 -0
- package/src/doctor.js +320 -0
- package/src/experiments.js +122 -0
- package/src/file-history.js +112 -0
- package/src/file-tree.js +255 -0
- package/src/flush-tick.js +56 -0
- package/src/fork.js +44 -0
- package/src/fx.js +148 -0
- package/src/index.js +123 -11
- package/src/keybindings.js +229 -0
- package/src/llm.js +355 -34
- package/src/lsp.js +401 -0
- package/src/notebook.js +232 -0
- package/src/output-styles.js +183 -0
- package/src/patch.js +248 -0
- package/src/permissions.js +130 -1
- package/src/plan.js +3 -2
- package/src/plugin.js +691 -43
- package/src/prompt-presets.js +1 -1
- package/src/registry-browser.js +317 -0
- package/src/remote.js +289 -0
- package/src/rollout.js +151 -0
- package/src/row-cache.js +143 -0
- package/src/schedule.js +294 -0
- package/src/session-approvals.js +86 -0
- package/src/session.js +178 -8
- package/src/shell-safety.js +473 -0
- package/src/side-thread.js +240 -0
- package/src/skills.js +27 -0
- package/src/stream-buffer.js +350 -0
- package/src/subagent-control.js +160 -0
- package/src/tasks-browser.js +54 -11
- package/src/teleport.js +260 -0
- package/src/term-caps.js +411 -0
- package/src/term.js +283 -8
- package/src/tools/agent-control.js +98 -0
- package/src/tools/agent-swarm.js +11 -2
- package/src/tools/agent.js +35 -1
- package/src/tools/bash.js +17 -3
- package/src/tools/diagnostics.js +175 -0
- package/src/tools/edit.js +25 -10
- package/src/tools/grep.js +62 -20
- package/src/tools/ignore.js +38 -14
- package/src/tools/index.js +10 -0
- package/src/tools/notebook.js +113 -0
- package/src/tools/read-media-file.js +10 -3
- package/src/tools/shell.js +134 -0
- package/src/tui.js +16682 -11665
- package/src/web-public/app.css +22 -9
- package/src/web-public/app.js +107 -54
- package/src/web-public/home.css +3 -3
- package/src/web-public/login.css +1 -1
package/src/web-public/app.css
CHANGED
|
@@ -24,9 +24,14 @@
|
|
|
24
24
|
--orange: #ff8c00;
|
|
25
25
|
--branch: #ffdb6e;
|
|
26
26
|
--shell: #bd93f9;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
/* Semantic colours. The TUI prints 16-COLOUR codes for these on the dark theme
|
|
28
|
+
(colors.js `C.red = ESC[91m` etc.), so it inherits whatever the user's terminal
|
|
29
|
+
defines; the web has no palette to inherit and must hardcode one. These are the
|
|
30
|
+
values the standard xterm/VSCode palette renders those codes as, so the two
|
|
31
|
+
front-ends agree instead of the web looking more saturated than the terminal. */
|
|
32
|
+
--red: #cd3131;
|
|
33
|
+
--green: #0dbc79;
|
|
34
|
+
--yellow: #e5e510;
|
|
30
35
|
|
|
31
36
|
--on-accent: #08090b;
|
|
32
37
|
|
|
@@ -127,18 +132,18 @@ button { font: inherit; color: inherit; }
|
|
|
127
132
|
white-space: nowrap;
|
|
128
133
|
}
|
|
129
134
|
.pill-solid { background: var(--teal); color: var(--on-accent); border-color: var(--teal); }
|
|
130
|
-
.pill-ok { color: var(--green); border-color:
|
|
131
|
-
.pill-warn { color: var(--yellow); border-color:
|
|
132
|
-
.pill-danger { color: var(--red); border-color:
|
|
133
|
-
.pill-info { color: var(--teal); border-color:
|
|
135
|
+
.pill-ok { color: var(--green); border-color: color-mix(in srgb, var(--green) 40%, transparent); }
|
|
136
|
+
.pill-warn { color: var(--yellow); border-color: color-mix(in srgb, var(--yellow) 40%, transparent); }
|
|
137
|
+
.pill-danger { color: var(--red); border-color: color-mix(in srgb, var(--red) 40%, transparent); }
|
|
138
|
+
.pill-info { color: var(--teal); border-color: color-mix(in srgb, var(--teal) 40%, transparent); }
|
|
134
139
|
.pill-soft { background: var(--s3); border-color: transparent; color: var(--fg1); }
|
|
135
140
|
|
|
136
141
|
.pill-sub { color: var(--fg2); }
|
|
137
142
|
|
|
138
143
|
/* Stop is the one destructive control in the composer, so it reads as a warning
|
|
139
144
|
rather than as another Send. */
|
|
140
|
-
.btn.is-stop { border-color:
|
|
141
|
-
.btn.is-stop:hover { border-color: var(--red); color: var(--red); background:
|
|
145
|
+
.btn.is-stop { border-color: color-mix(in srgb, var(--red) 50%, transparent); color: var(--red); }
|
|
146
|
+
.btn.is-stop:hover { border-color: var(--red); color: var(--red); background: color-mix(in srgb, var(--red) 10%, transparent); }
|
|
142
147
|
|
|
143
148
|
|
|
144
149
|
.dot {
|
|
@@ -931,7 +936,15 @@ button { font: inherit; color: inherit; }
|
|
|
931
936
|
border: 0;
|
|
932
937
|
padding: 0;
|
|
933
938
|
}
|
|
939
|
+
/* Plan / Focus / Swarm are MUTUALLY EXCLUSIVE modes, so at most one badge is shown —
|
|
940
|
+
and each carries its OWN accent, matching src/tui.js:
|
|
941
|
+
Plan -> cyan | Focus -> blue | Swarm -> spring (green-tinted cyan)
|
|
942
|
+
They used to collapse into one `--teal` badge, which made three different modes
|
|
943
|
+
look like the same state. */
|
|
934
944
|
.sl-badge { color: var(--teal); font-weight: 600; flex: none; }
|
|
945
|
+
.sl-badge-plan { color: var(--cyan); }
|
|
946
|
+
.sl-badge-focus { color: var(--blue); }
|
|
947
|
+
.sl-badge-swarm { color: var(--spring); }
|
|
935
948
|
.sl-model {
|
|
936
949
|
flex: none;
|
|
937
950
|
color: var(--fg);
|
package/src/web-public/app.js
CHANGED
|
@@ -321,7 +321,20 @@ function setScrollTop(v) {
|
|
|
321
321
|
// every real height, so two rows can never overlap. Only rows inside the
|
|
322
322
|
// window have DOM; the ranges above/below it are reserved by two padding divs,
|
|
323
323
|
// sized from measured heights (PLACEHOLDER_H until a row is first mounted).
|
|
324
|
-
|
|
324
|
+
//
|
|
325
|
+
// The overscan is measured in PIXELS, not rows. A row-count overscan is unusable
|
|
326
|
+
// here because row heights differ by an order of magnitude — a folded tool result is
|
|
327
|
+
// ~180px, a plain system line ~20px — so "8 rows" could be 160px of slack in one
|
|
328
|
+
// stretch of the transcript and 1400px in another. In the thin stretch a row was
|
|
329
|
+
// unmounted while it was still on screen (the reported "gone before it left the
|
|
330
|
+
// viewport"), because the padding divs then took its place and the content jumped.
|
|
331
|
+
// Keeping one extra viewport above and below makes the window independent of how
|
|
332
|
+
// tall the rows happen to be.
|
|
333
|
+
const WIN_OVERSCAN_PX = 800;
|
|
334
|
+
// The row-count floor that goes with it (see renderWindow): whichever reaches further
|
|
335
|
+
// wins. Estimates cannot be trusted, so this is what actually guarantees a row is not
|
|
336
|
+
// unmounted while it is still visible.
|
|
337
|
+
const WIN_OVERSCAN_ROWS = 12;
|
|
325
338
|
const NEAR_END_PX = 24;
|
|
326
339
|
// TanStack Virtual's rule for estimateSize: "estimate the LARGEST possible
|
|
327
340
|
// size (within comfort)". An underestimate makes the scroll range too short,
|
|
@@ -429,35 +442,66 @@ function renderWindow() {
|
|
|
429
442
|
return;
|
|
430
443
|
}
|
|
431
444
|
const viewH = stream.clientHeight || 608;
|
|
445
|
+
// Overscan is expressed in BOTH pixels and rows, and whichever reaches FURTHER
|
|
446
|
+
// wins. Row heights differ by an order of magnitude (a folded tool result ~180px,
|
|
447
|
+
// a plain line ~20px), so a row count alone left almost no margin in a thin
|
|
448
|
+
// stretch and unmounted rows that were still on screen. A pixel budget alone is not
|
|
449
|
+
// enough either: an UNMEASURED row is priced at PLACEHOLDER_H, so walking 800px of
|
|
450
|
+
// estimates can cover far less than 800px of real content and the last rows fall
|
|
451
|
+
// outside the window. The row floor is the guarantee; the pixel budget trims the
|
|
452
|
+
// overshoot on tall rows.
|
|
453
|
+
const over = Math.max(WIN_OVERSCAN_PX, viewH);
|
|
454
|
+
const MIN_MARGIN_ROWS = WIN_OVERSCAN_ROWS;
|
|
432
455
|
if (win.pinned) {
|
|
433
|
-
//
|
|
434
|
-
// rows are the newest, and a row-count window is exact regardless of how
|
|
435
|
-
// tall the measured rows are. PLACEHOLDER_H only prices the mounts into
|
|
436
|
-
// the two pads above/below the window; the pad math handles real heights.
|
|
437
|
-
const want = Math.ceil(viewH / PLACEHOLDER_H) + WIN_OVERSCAN;
|
|
456
|
+
// Pinned to the tail: grow upward until BOTH budgets are satisfied.
|
|
438
457
|
win.end = rowItems.length;
|
|
439
|
-
|
|
458
|
+
let used = 0;
|
|
459
|
+
let start = win.end;
|
|
460
|
+
while (start > 0 && (used < viewH + over || win.end - start < MIN_MARGIN_ROWS)) {
|
|
461
|
+
start--;
|
|
462
|
+
used += knownHeight(start);
|
|
463
|
+
}
|
|
464
|
+
win.start = start;
|
|
440
465
|
} else {
|
|
466
|
+
// Walk from the top accumulating heights until the scroll position is reached.
|
|
441
467
|
let acc = 0;
|
|
442
468
|
let first = rowItems.length - 1;
|
|
443
469
|
for (let i = 0; i < rowItems.length; i++) {
|
|
444
|
-
if (acc
|
|
470
|
+
if (acc + knownHeight(i) > stream.scrollTop) { first = i; break; }
|
|
445
471
|
acc += knownHeight(i);
|
|
446
472
|
}
|
|
447
|
-
|
|
448
|
-
let
|
|
449
|
-
let
|
|
450
|
-
while (
|
|
473
|
+
// Above the viewport: the pixel budget OR the row floor, whichever is further.
|
|
474
|
+
let start = first;
|
|
475
|
+
let above = 0;
|
|
476
|
+
while (start > 0 && (above < over || first - start < MIN_MARGIN_ROWS)) {
|
|
477
|
+
start--;
|
|
478
|
+
above += knownHeight(start);
|
|
479
|
+
}
|
|
480
|
+
// Below the viewport: same rule.
|
|
481
|
+
let end = first + 1;
|
|
482
|
+
let used = knownHeight(first);
|
|
483
|
+
while (end < rowItems.length && (used < viewH + over || end - first < MIN_MARGIN_ROWS)) {
|
|
451
484
|
used += knownHeight(end);
|
|
452
485
|
end++;
|
|
453
486
|
}
|
|
454
487
|
win.start = start;
|
|
455
|
-
win.end = Math.min(rowItems.length, end
|
|
488
|
+
win.end = Math.min(rowItems.length, end);
|
|
456
489
|
}
|
|
457
|
-
const
|
|
490
|
+
const sameRange = win.start === win._lastStart && win.end === win._lastEnd;
|
|
458
491
|
win._lastStart = win.start;
|
|
459
492
|
win._lastEnd = win.end;
|
|
460
|
-
|
|
493
|
+
// A height change does NOT need a different window — only different paddings. The
|
|
494
|
+
// pad sizes are recomputed below on every pass, so returning early here is safe.
|
|
495
|
+
// It used to be gated on `same` (start+end), which is why `_lastStart = -1` on
|
|
496
|
+
// every height change tore the whole window down and rebuilt it: each tool call
|
|
497
|
+
// changes a row's height, so the unmount/remount cycle re-created every visible
|
|
498
|
+
// row's DOM and the tool NAMES visibly flashed — not just the call that finished.
|
|
499
|
+
if (sameRange) {
|
|
500
|
+
win.topPad.style.height = heightAbove(win.start) + "px";
|
|
501
|
+
win.bottomPad.style.height = heightBelow(win.end) + "px";
|
|
502
|
+
if (win.pinned) setScrollTop(stream.scrollHeight);
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
461
505
|
for (const k of [...win.els.keys()]) {
|
|
462
506
|
if (k < win.start || k >= win.end) unmountRow(k);
|
|
463
507
|
}
|
|
@@ -886,14 +930,16 @@ function paintToolName(rec, name, pending, tick) {
|
|
|
886
930
|
|
|
887
931
|
if (!pending) {
|
|
888
932
|
unregisterSweep(rec);
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
933
|
+
// Leave the DOM alone when it already shows the plain name. `paintBody` rebuilds
|
|
934
|
+
// a row's children on every patch, so this node is normally FRESH (empty) and the
|
|
935
|
+
// branch below is what fills it; but when the name is unchanged the existing
|
|
936
|
+
// content is already correct, and rewriting it makes the name flicker on rows
|
|
937
|
+
// that finished long ago — the flash reported for "every tool name", not just the
|
|
938
|
+
// call that just ran.
|
|
939
|
+
if (rec.namePending === false && box.textContent === name) return;
|
|
940
|
+
box.replaceChildren(document.createTextNode(name));
|
|
941
|
+
rec.namePending = false;
|
|
942
|
+
rec.nameText = name;
|
|
897
943
|
return;
|
|
898
944
|
}
|
|
899
945
|
|
|
@@ -1053,31 +1099,38 @@ function paintResultRow(body, r) {
|
|
|
1053
1099
|
const text = String(r.text == null ? '' : r.text);
|
|
1054
1100
|
const diff = Array.isArray(r.diff) ? r.diff.filter((d) => d.type !== 'ctx') : [];
|
|
1055
1101
|
|
|
1056
|
-
if (diff.length) {
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
diff.forEach((d, i) => {
|
|
1102
|
+
if (diff.length) {
|
|
1103
|
+
// Same shape as the folded result below: show the first diff rows with the `↳`
|
|
1104
|
+
// on the first, and let the fold hint trail the block. The old form drew `↳` on
|
|
1105
|
+
// a line by itself with the hint on the NEXT line and no diff content at all.
|
|
1106
|
+
const rows = expandedRows.has(r.id) ? diff : diff.slice(0, DIFF_MAX_ROWS);
|
|
1107
|
+
const width = Math.max(1, ...rows.map((d) => String(d.no || 0).length));
|
|
1108
|
+
rows.forEach((d, i) => {
|
|
1064
1109
|
const no = String(d.no || 0).padStart(width);
|
|
1065
1110
|
const mark = d.type === 'add' ? '+' : '-';
|
|
1066
1111
|
const prefix = i === 0 ? tr('mark.result') + ' ' : ' ';
|
|
1067
1112
|
body.appendChild(el(d.type === 'add' ? 'diff-add' : 'diff-del',
|
|
1068
1113
|
prefix + no + ' ' + mark + ' ' + String(d.text) + '\n'));
|
|
1069
1114
|
});
|
|
1115
|
+
const hiddenDiff = diff.length - rows.length;
|
|
1116
|
+
if (hiddenDiff > 0) body.appendChild(moreButton(r.id, hiddenDiff));
|
|
1070
1117
|
const receipt = text.trim()
|
|
1071
1118
|
.split('\n').filter((ln) => !/^\[exit code:/.test(ln.trim())).join('\n')
|
|
1072
1119
|
.trim();
|
|
1073
1120
|
if (receipt) body.appendChild(el(failed ? 'diff-del' : 'res-more', receipt));
|
|
1074
|
-
body.appendChild(collapseButton(r.id));
|
|
1121
|
+
if (expandedRows.has(r.id) && diff.length > DIFF_MAX_ROWS) body.appendChild(collapseButton(r.id));
|
|
1075
1122
|
return;
|
|
1076
1123
|
}
|
|
1077
|
-
|
|
1078
|
-
//
|
|
1079
|
-
//
|
|
1080
|
-
//
|
|
1124
|
+
// A finished result is FOLDED (like the terminal's ctrl+o): a whole-file Read is
|
|
1125
|
+
// thousands of DOM nodes in ONE row, which stutters scrolling. The preview keeps
|
|
1126
|
+
// the FIRST lines visible with the `↳` marker on the first one, and the fold hint
|
|
1127
|
+
// rides the END of the block — the terminal's shape:
|
|
1128
|
+
// ↳ 1 first line of the output
|
|
1129
|
+
// 2 second line
|
|
1130
|
+
// … (N more lines, expand)
|
|
1131
|
+
// It used to render `↳` alone on one line and the hint on the NEXT, so a folded
|
|
1132
|
+
// row showed a dangling marker and no output at all, and the hint read as a
|
|
1133
|
+
// separate paragraph rather than as "there is more of THIS".
|
|
1081
1134
|
// `[exit code: N]` is bookkeeping for the model, never shown to the user.
|
|
1082
1135
|
const lines = text.replace(/\r\n/g, '\n').split('\n')
|
|
1083
1136
|
.filter((ln) => !/^\[exit code:/.test(ln.trim()));
|
|
@@ -1085,12 +1138,8 @@ if (diff.length) {
|
|
|
1085
1138
|
while (lines.length && lines[lines.length - 1].trim() === '') lines.pop();
|
|
1086
1139
|
if (!lines.length) return;
|
|
1087
1140
|
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
body.appendChild(expandButton(r.id, lines.length));
|
|
1091
|
-
return;
|
|
1092
|
-
}
|
|
1093
|
-
const shown = lines;
|
|
1141
|
+
const folded = !expandedRows.has(r.id);
|
|
1142
|
+
const shown = folded ? lines.slice(0, RESULT_MAX_LINES) : lines;
|
|
1094
1143
|
shown.forEach((ln, i) => {
|
|
1095
1144
|
const isErr = /^\[error:/.test(ln.trim());
|
|
1096
1145
|
const prefix = i === 0 ? tr('mark.result') + ' ' : ' ';
|
|
@@ -1098,19 +1147,10 @@ if (diff.length) {
|
|
|
1098
1147
|
});
|
|
1099
1148
|
const hidden = lines.length - shown.length;
|
|
1100
1149
|
if (hidden > 0) body.appendChild(moreButton(r.id, hidden));
|
|
1101
|
-
body.appendChild(collapseButton(r.id));
|
|
1150
|
+
if (expandedRows.has(r.id) && lines.length > RESULT_MAX_LINES) body.appendChild(collapseButton(r.id));
|
|
1102
1151
|
}
|
|
1103
1152
|
|
|
1104
1153
|
|
|
1105
|
-
function expandButton(id, n) {
|
|
1106
|
-
const b = document.createElement('button');
|
|
1107
|
-
b.type = 'button';
|
|
1108
|
-
b.className = 'msg-more';
|
|
1109
|
-
b.textContent = tr('tool.moreLines', { n: Number(n) || 0 });
|
|
1110
|
-
b.addEventListener('click', () => { expandedRows.add(id); repaintRow(id); });
|
|
1111
|
-
return b;
|
|
1112
|
-
}
|
|
1113
|
-
|
|
1114
1154
|
function moreButton(id, n) {
|
|
1115
1155
|
const b = document.createElement('button');
|
|
1116
1156
|
b.type = 'button';
|
|
@@ -1175,6 +1215,12 @@ function paintBgTaskRow(body, r) {
|
|
|
1175
1215
|
*/
|
|
1176
1216
|
function paintThinkingRow(body, r, text) {
|
|
1177
1217
|
const lines = text ? text.replace(/\r\n/g, '\n').split('\n') : [];
|
|
1218
|
+
// An EMPTY reasoning block renders NOTHING, matching `messageLines` in the
|
|
1219
|
+
// terminal. A model may emit a `think` event with no text, and a block finalised by
|
|
1220
|
+
// a tool call can be left empty; drawing a bare `●` (or a lone spinner) for those
|
|
1221
|
+
// reads as a rendering bug. The row stays in the model, so a block that later
|
|
1222
|
+
// receives text appears in full — only the paint is skipped.
|
|
1223
|
+
if (!lines.some((l) => l.trim() !== '')) return;
|
|
1178
1224
|
const cap = expandedRows.has(r.id) ? Infinity : THINK_PREVIEW;
|
|
1179
1225
|
|
|
1180
1226
|
if (r.pending) {
|
|
@@ -1202,6 +1248,7 @@ function paintThinkingRow(body, r, text) {
|
|
|
1202
1248
|
}
|
|
1203
1249
|
|
|
1204
1250
|
|
|
1251
|
+
|
|
1205
1252
|
const FENCE_RE = /^[ \t]*```+\s*(\S*)/;
|
|
1206
1253
|
const INLINE_RE = /(`[^`]+`|\*\*[^*]+\*\*|\*[^*\s][^*]*?\*|~~[^~]+~~|\[[^\]]+\]\([^)\s]+\)|(^|\s)https?:\/\/[^\s]+)/g;
|
|
1207
1254
|
|
|
@@ -1489,8 +1536,14 @@ function renderStatusLine(s) {
|
|
|
1489
1536
|
const o = s.options || {};
|
|
1490
1537
|
|
|
1491
1538
|
line.appendChild(popoverButton('sl-mode', modeLabel(s.mode), 'side.mode', () => modeItems(o)));
|
|
1492
|
-
|
|
1493
|
-
|
|
1539
|
+
// One badge, one accent — Plan/Focus/Swarm are mutually exclusive, so exactly one
|
|
1540
|
+
// of these is shown. The class carries the colour (see .sl-badge-* in app.css); the
|
|
1541
|
+
// three must NOT share it, or the statusline cannot say which mode is on.
|
|
1542
|
+
const badge = s.plan ? { text: 'Plan', cls: 'sl-badge-plan' }
|
|
1543
|
+
: s.focus ? { text: 'Focus', cls: 'sl-badge-focus' }
|
|
1544
|
+
: s.swarm ? { text: 'Swarm', cls: 'sl-badge-swarm' }
|
|
1545
|
+
: null;
|
|
1546
|
+
if (badge) line.appendChild(el('sl-badge ' + badge.cls, badge.text));
|
|
1494
1547
|
|
|
1495
1548
|
if (s.model) {
|
|
1496
1549
|
const label = s.model + (s.reasoning
|
package/src/web-public/home.css
CHANGED
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
|
|
20
20
|
--teal: #00b8db;
|
|
21
21
|
--cyan: #00d7ff;
|
|
22
|
-
--red: #
|
|
23
|
-
--green: #
|
|
22
|
+
--red: #cd3131;
|
|
23
|
+
--green: #0dbc79;
|
|
24
24
|
|
|
25
25
|
--mono: "JetBrains Mono", "Noto Sans SC", ui-monospace, SFMono-Regular,
|
|
26
26
|
"SF Mono", Menlo, Consolas, "Liberation Mono", "Courier New", monospace;
|
|
@@ -100,7 +100,7 @@ h1 .dim { color: var(--fg3); }
|
|
|
100
100
|
|
|
101
101
|
.err {
|
|
102
102
|
color: var(--red);
|
|
103
|
-
border: 1px solid
|
|
103
|
+
border: 1px solid color-mix(in srgb, var(--red) 40%, transparent);
|
|
104
104
|
border-radius: var(--radius);
|
|
105
105
|
padding: 9px 12px;
|
|
106
106
|
margin: 0 0 24px;
|
package/src/web-public/login.css
CHANGED