Skip to content

Commit 3bafc2e

Browse files
committed
feat(ui): canvas fullscreen toggle + keyboard tab navigation
The canvas header gains a fullscreen toggle (expands the panel to cover the viewport; resize handle hidden while fullscreen). The artifact tab strip is now a proper ARIA tablist with roving tabindex and Left/Right arrow-key navigation. Assisted-by: Claude:claude-opus-4-8 [Claude Code] Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
1 parent 8991b94 commit 3bafc2e

2 files changed

Lines changed: 58 additions & 15 deletions

File tree

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5338,6 +5338,16 @@ select.input {
53385338
.canvas-resize-handle { display: none; }
53395339
.canvas-panel { width: 100% !important; max-width: none !important; }
53405340
}
5341+
.canvas-header-actions { display: inline-flex; gap: var(--spacing-xs); }
5342+
/* Fullscreen: the canvas covers the viewport. */
5343+
.canvas-panel--fullscreen {
5344+
position: fixed;
5345+
inset: 0;
5346+
width: 100% !important;
5347+
max-width: none !important;
5348+
z-index: 60;
5349+
border-left: none;
5350+
}
53415351
.canvas-panel-header {
53425352
display: flex;
53435353
align-items: center;

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

Lines changed: 48 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export default function CanvasPanel({ artifacts, selectedId, onSelect, onClose }
1616
const [width, setWidth] = useState(() => {
1717
try { const v = localStorage.getItem(WIDTH_KEY); return v ? Number(v) : null } catch { return null }
1818
})
19+
const [fullscreen, setFullscreen] = useState(false)
1920
const codeRef = useRef(null)
2021
const panelRef = useRef(null)
2122

@@ -149,33 +150,65 @@ export default function CanvasPanel({ artifacts, selectedId, onSelect, onClose }
149150
}
150151

151152
return (
152-
<div className="canvas-panel" ref={panelRef} style={width ? { width: `${width}px`, maxWidth: 'none' } : undefined}>
153-
<div
154-
className="canvas-resize-handle"
155-
onMouseDown={startResize}
156-
onDoubleClick={resetWidth}
157-
role="separator"
158-
aria-orientation="vertical"
159-
aria-label="Resize canvas (double-click to reset)"
160-
title="Drag to resize, double-click to reset"
161-
/>
153+
<div
154+
className={`canvas-panel${fullscreen ? ' canvas-panel--fullscreen' : ''}`}
155+
ref={panelRef}
156+
style={!fullscreen && width ? { width: `${width}px`, maxWidth: 'none' } : undefined}
157+
>
158+
{!fullscreen && (
159+
<div
160+
className="canvas-resize-handle"
161+
onMouseDown={startResize}
162+
onDoubleClick={resetWidth}
163+
role="separator"
164+
aria-orientation="vertical"
165+
aria-label="Resize canvas (double-click to reset)"
166+
title="Drag to resize, double-click to reset"
167+
/>
168+
)}
162169
<div className="canvas-panel-header">
163170
<span className="canvas-panel-title">{current.title || 'Artifact'}</span>
164-
<button className="btn btn-secondary btn-sm" onClick={onClose} title="Close canvas">
165-
<i className="fas fa-times" />
166-
</button>
171+
<div className="canvas-header-actions">
172+
<button
173+
className="btn btn-secondary btn-sm"
174+
onClick={() => setFullscreen(f => !f)}
175+
title={fullscreen ? 'Exit fullscreen' : 'Fullscreen'}
176+
aria-label={fullscreen ? 'Exit fullscreen' : 'Fullscreen'}
177+
>
178+
<i className={`fas ${fullscreen ? 'fa-compress' : 'fa-expand'}`} aria-hidden="true" />
179+
</button>
180+
<button className="btn btn-secondary btn-sm" onClick={onClose} title="Close canvas" aria-label="Close canvas">
181+
<i className="fas fa-times" aria-hidden="true" />
182+
</button>
183+
</div>
167184
</div>
168185

169186
{artifacts.length > 1 && (
170-
<div className="canvas-panel-tabs">
187+
<div
188+
className="canvas-panel-tabs"
189+
role="tablist"
190+
aria-label="Artifacts"
191+
onKeyDown={(e) => {
192+
if (e.key !== 'ArrowRight' && e.key !== 'ArrowLeft') return
193+
e.preventDefault()
194+
const idx = artifacts.findIndex(a => a.id === current.id)
195+
const n = e.key === 'ArrowRight'
196+
? (idx + 1) % artifacts.length
197+
: (idx - 1 + artifacts.length) % artifacts.length
198+
onSelect(artifacts[n].id)
199+
}}
200+
>
171201
{artifacts.map(a => (
172202
<button
173203
key={a.id}
204+
role="tab"
205+
aria-selected={a.id === current.id}
206+
tabIndex={a.id === current.id ? 0 : -1}
174207
className={`canvas-panel-tab${a.id === (current?.id) ? ' active' : ''}`}
175208
onClick={() => onSelect(a.id)}
176209
title={a.title}
177210
>
178-
<i className={`fas ${getArtifactIcon(a.type, a.language)}`} />
211+
<i className={`fas ${getArtifactIcon(a.type, a.language)}`} aria-hidden="true" />
179212
<span>{a.title}</span>
180213
</button>
181214
))}

0 commit comments

Comments
 (0)