Skip to content

Commit 0105b17

Browse files
committed
chore(ui): style improvements
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
1 parent 160d35f commit 0105b17

12 files changed

Lines changed: 300 additions & 115 deletions

File tree

core/http/react-ui/src/App.css

Lines changed: 67 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@
142142
box-shadow: var(--shadow-sidebar);
143143
transition: width var(--duration-normal) var(--ease-default),
144144
transform var(--duration-normal) var(--ease-default);
145+
will-change: transform;
145146
}
146147

147148
.sidebar-overlay {
@@ -244,6 +245,7 @@
244245
flex: 1;
245246
overflow: hidden;
246247
text-overflow: ellipsis;
248+
transition: opacity 150ms ease;
247249
}
248250

249251
.nav-external {
@@ -539,23 +541,23 @@
539541
}
540542

541543
.toast-success {
542-
background: rgba(20, 184, 166, 0.15);
543-
border: 1px solid rgba(20, 184, 166, 0.3);
544+
background: var(--color-success-light);
545+
border: 1px solid var(--color-success-border);
544546
color: var(--color-success);
545547
}
546548
.toast-error {
547-
background: rgba(239, 68, 68, 0.15);
548-
border: 1px solid rgba(239, 68, 68, 0.3);
549+
background: var(--color-error-light);
550+
border: 1px solid var(--color-error-border);
549551
color: var(--color-error);
550552
}
551553
.toast-warning {
552-
background: rgba(245, 158, 11, 0.15);
553-
border: 1px solid rgba(245, 158, 11, 0.3);
554+
background: var(--color-warning-light);
555+
border: 1px solid var(--color-warning-border);
554556
color: var(--color-warning);
555557
}
556558
.toast-info {
557-
background: rgba(56, 189, 248, 0.15);
558-
border: 1px solid rgba(56, 189, 248, 0.3);
559+
background: var(--color-info-light);
560+
border: 1px solid var(--color-info-border);
559561
color: var(--color-info);
560562
}
561563

@@ -747,12 +749,13 @@
747749
border: 1px solid var(--color-border-subtle);
748750
border-radius: var(--radius-lg);
749751
padding: var(--spacing-md);
750-
transition: border-color var(--duration-fast), box-shadow var(--duration-fast);
752+
transition: border-color var(--duration-fast), box-shadow var(--duration-fast), transform var(--duration-fast);
751753
}
752754

753755
.card:hover {
754756
border-color: var(--color-border-default);
755757
box-shadow: var(--shadow-sm);
758+
transform: translateY(-1px);
756759
}
757760

758761
.card-grid {
@@ -773,7 +776,7 @@
773776
font-weight: 500;
774777
cursor: pointer;
775778
border: none;
776-
transition: all var(--duration-fast) var(--ease-default);
779+
transition: background var(--duration-fast) var(--ease-default), color var(--duration-fast) var(--ease-default), border-color var(--duration-fast) var(--ease-default), box-shadow var(--duration-fast) var(--ease-default);
777780
text-decoration: none;
778781
}
779782

@@ -1410,6 +1413,37 @@
14101413
50% { opacity: 0.5; }
14111414
}
14121415

1416+
@keyframes messageSlideIn {
1417+
from { opacity: 0; transform: translateY(8px); }
1418+
to { opacity: 1; transform: translateY(0); }
1419+
}
1420+
1421+
@keyframes dropdownIn {
1422+
from { opacity: 0; transform: translateY(-4px); }
1423+
to { opacity: 1; transform: translateY(0); }
1424+
}
1425+
1426+
@keyframes completionGlow {
1427+
0% { box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.2); }
1428+
50% { box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.1); }
1429+
100% { box-shadow: 0 0 0 0 rgba(59, 130, 246, 0); }
1430+
}
1431+
1432+
/* Page route transitions */
1433+
.page-transition {
1434+
animation: fadeIn 200ms ease;
1435+
display: flex;
1436+
flex-direction: column;
1437+
flex: 1;
1438+
min-height: 0;
1439+
min-width: 0;
1440+
}
1441+
1442+
/* Completion glow on streaming finish */
1443+
.chat-message-new .chat-message-content {
1444+
animation: completionGlow 600ms ease-out;
1445+
}
1446+
14131447
/* Chat-specific styles */
14141448
.chat-layout {
14151449
display: flex;
@@ -1468,12 +1502,13 @@
14681502
cursor: pointer;
14691503
font-size: 0.8125rem;
14701504
color: var(--color-text-secondary);
1471-
transition: all var(--duration-fast);
1505+
transition: background var(--duration-fast), color var(--duration-fast), transform var(--duration-fast);
14721506
margin-bottom: 2px;
14731507
}
14741508

14751509
.chat-list-item:hover {
14761510
background: var(--color-primary-light);
1511+
transform: translateX(2px);
14771512
}
14781513

14791514
.chat-list-item.active {
@@ -1570,7 +1605,7 @@
15701605
gap: var(--spacing-sm);
15711606
max-width: 80%;
15721607
min-width: 0;
1573-
animation: fadeIn 200ms ease;
1608+
animation: messageSlideIn 250ms ease-out;
15741609
}
15751610

15761611
.chat-message-user {
@@ -1740,7 +1775,7 @@
17401775
}
17411776
.chat-input-wrapper:focus-within {
17421777
border-color: var(--color-primary-border);
1743-
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.12), 0 0 12px rgba(99, 102, 241, 0.06);
1778+
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.12), 0 0 12px rgba(59, 130, 246, 0.06);
17441779
}
17451780

17461781
.chat-attach-btn {
@@ -1798,6 +1833,9 @@
17981833
opacity: 0.3;
17991834
cursor: not-allowed;
18001835
}
1836+
.chat-send-btn:active:not(:disabled) {
1837+
transform: scale(0.92);
1838+
}
18011839

18021840
.chat-stop-btn {
18031841
padding: var(--spacing-xs);
@@ -2128,13 +2166,13 @@
21282166
overflow-y: auto;
21292167
}
21302168
.chat-activity-thinking {
2131-
border-left-color: rgba(99, 102, 241, 0.3);
2169+
border-left-color: rgba(59, 130, 246, 0.3);
21322170
}
21332171
.chat-activity-tool-call {
2134-
border-left-color: rgba(139, 92, 246, 0.3);
2172+
border-left-color: rgba(245, 158, 11, 0.3);
21352173
}
21362174
.chat-activity-tool-result {
2137-
border-left-color: rgba(20, 184, 166, 0.3);
2175+
border-left-color: rgba(34, 197, 94, 0.3);
21382176
}
21392177

21402178
/* Context window progress bar */
@@ -2230,6 +2268,7 @@
22302268
border: 1px solid var(--color-border-subtle);
22312269
border-radius: var(--radius-md);
22322270
box-shadow: var(--shadow-lg);
2271+
animation: dropdownIn 120ms ease-out;
22332272
}
22342273
.chat-mcp-dropdown-loading,
22352274
.chat-mcp-dropdown-empty {
@@ -2305,15 +2344,15 @@
23052344
background: var(--color-text-tertiary);
23062345
}
23072346
.chat-client-mcp-status-connected {
2308-
background: #22c55e;
2347+
background: var(--color-success);
23092348
box-shadow: 0 0 4px rgba(34, 197, 94, 0.5);
23102349
}
23112350
.chat-client-mcp-status-connecting {
2312-
background: #f59e0b;
2351+
background: var(--color-warning);
23132352
animation: pulse 1s infinite;
23142353
}
23152354
.chat-client-mcp-status-error {
2316-
background: #ef4444;
2355+
background: var(--color-error);
23172356
}
23182357
.chat-client-mcp-status-disconnected {
23192358
background: var(--color-text-tertiary);
@@ -2394,6 +2433,7 @@
23942433
transform: translateX(100%);
23952434
transition: transform 250ms var(--ease-default);
23962435
box-shadow: var(--shadow-lg);
2436+
will-change: transform;
23972437
}
23982438
.chat-settings-drawer.open {
23992439
transform: translateX(0);
@@ -2488,7 +2528,7 @@
24882528

24892529
/* Max tokens/sec badge */
24902530
.chat-max-tps-badge {
2491-
background: rgba(99, 102, 241, 0.15);
2531+
background: rgba(59, 130, 246, 0.15);
24922532
color: var(--color-primary);
24932533
padding: 1px 6px;
24942534
border-radius: var(--radius-full);
@@ -2542,7 +2582,7 @@
25422582
align-items: center;
25432583
gap: 4px;
25442584
padding: 2px 6px;
2545-
background: rgba(99, 102, 241, 0.1);
2585+
background: rgba(59, 130, 246, 0.1);
25462586
border-radius: var(--radius-sm);
25472587
font-size: 0.7rem;
25482588
color: var(--color-text-secondary);
@@ -3097,6 +3137,7 @@
30973137
padding: var(--spacing-lg);
30983138
box-shadow: var(--shadow-lg);
30993139
animation: slideUp 150ms ease;
3140+
will-change: transform, opacity;
31003141
}
31013142
@keyframes slideUp {
31023143
from { opacity: 0; transform: translateY(8px); }
@@ -3249,7 +3290,7 @@
32493290
}
32503291
.home-input-container:focus-within {
32513292
border-color: var(--color-primary-border);
3252-
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.12), 0 0 12px rgba(99, 102, 241, 0.06);
3293+
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.12), 0 0 12px rgba(59, 130, 246, 0.06);
32533294
}
32543295
.home-textarea {
32553296
width: 100%;
@@ -3320,6 +3361,9 @@
33203361
opacity: 0.3;
33213362
cursor: not-allowed;
33223363
}
3364+
.home-send-btn:active:not(:disabled) {
3365+
transform: scale(0.92);
3366+
}
33233367

33243368
/* Home quick links */
33253369
.home-quick-links {
@@ -3347,6 +3391,7 @@
33473391
.home-link-btn:hover {
33483392
border-color: var(--color-primary-border);
33493393
color: var(--color-primary);
3394+
transform: translateY(-1px);
33503395
}
33513396

33523397
/* Home loaded models */

core/http/react-ui/src/App.jsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ export default function App() {
2929
return () => window.removeEventListener('sidebar-collapse', handler)
3030
}, [])
3131

32+
// Scroll to top on route change
33+
useEffect(() => {
34+
window.scrollTo(0, 0)
35+
}, [location.pathname])
36+
3237
const layoutClasses = [
3338
'app-layout',
3439
isChatRoute ? 'app-layout-chat' : '',
@@ -51,7 +56,9 @@ export default function App() {
5156
<span className="mobile-title">LocalAI</span>
5257
</header>
5358
<div className="main-content-inner">
54-
<Outlet context={{ addToast }} />
59+
<div className="page-transition" key={location.pathname}>
60+
<Outlet context={{ addToast }} />
61+
</div>
5562
</div>
5663
{!isChatRoute && (
5764
<footer className="app-footer">

core/http/react-ui/src/components/ConfirmDialog.jsx

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,29 +10,68 @@ export default function ConfirmDialog({
1010
onConfirm,
1111
onCancel,
1212
}) {
13+
const dialogRef = useRef(null)
1314
const confirmRef = useRef(null)
1415

1516
useEffect(() => {
16-
if (open) {
17-
confirmRef.current?.focus()
18-
const handleKey = (e) => {
19-
if (e.key === 'Escape') onCancel?.()
17+
if (!open) return
18+
19+
confirmRef.current?.focus()
20+
21+
const dialog = dialogRef.current
22+
if (!dialog) return
23+
24+
const focusableSelector = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
25+
const getFocusable = () => dialog.querySelectorAll(focusableSelector)
26+
27+
const handleKeyDown = (e) => {
28+
if (e.key === 'Escape') {
29+
onCancel?.()
30+
return
31+
}
32+
if (e.key !== 'Tab') return
33+
const focusable = getFocusable()
34+
if (focusable.length === 0) return
35+
const first = focusable[0]
36+
const last = focusable[focusable.length - 1]
37+
if (e.shiftKey) {
38+
if (document.activeElement === first) {
39+
e.preventDefault()
40+
last.focus()
41+
}
42+
} else {
43+
if (document.activeElement === last) {
44+
e.preventDefault()
45+
first.focus()
46+
}
2047
}
21-
document.addEventListener('keydown', handleKey)
22-
return () => document.removeEventListener('keydown', handleKey)
2348
}
49+
50+
document.addEventListener('keydown', handleKeyDown)
51+
return () => document.removeEventListener('keydown', handleKeyDown)
2452
}, [open, onCancel])
2553

2654
if (!open) return null
2755

56+
const titleId = 'confirm-dialog-title'
57+
const bodyId = 'confirm-dialog-body'
58+
2859
return (
2960
<div className="confirm-dialog-backdrop" onClick={onCancel}>
30-
<div className="confirm-dialog" onClick={(e) => e.stopPropagation()}>
61+
<div
62+
ref={dialogRef}
63+
className="confirm-dialog"
64+
role="alertdialog"
65+
aria-modal="true"
66+
aria-labelledby={titleId}
67+
aria-describedby={message ? bodyId : undefined}
68+
onClick={(e) => e.stopPropagation()}
69+
>
3170
<div className="confirm-dialog-header">
3271
{danger && <i className="fas fa-exclamation-triangle confirm-dialog-danger-icon" />}
33-
<span className="confirm-dialog-title">{title}</span>
72+
<span id={titleId} className="confirm-dialog-title">{title}</span>
3473
</div>
35-
{message && <div className="confirm-dialog-body">{message}</div>}
74+
{message && <div id={bodyId} className="confirm-dialog-body">{message}</div>}
3675
<div className="confirm-dialog-actions">
3776
<button className="btn btn-secondary btn-sm" onClick={onCancel}>
3877
{cancelLabel}

0 commit comments

Comments
 (0)