Merge pull request #108 from findthelorax/fix/modal-accessibility

fix: modal accessibility
This commit is contained in:
Remy
2026-04-24 14:48:52 +02:00
committed by GitHub
8 changed files with 127 additions and 68 deletions
+18 -11
View File
@@ -1,4 +1,5 @@
import { useState } from 'react'
import modalStyles from './modal-interactive.module.css'
import { RotateCcw } from 'lucide-react'
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
@@ -68,7 +69,7 @@ export function EdgeModal({ open, onClose, onSubmit, onDelete, onClearWaypoints,
<div className="flex flex-col gap-1.5">
<Label className="text-xs text-muted-foreground">Link Type</Label>
<Select value={type} onValueChange={(v) => setType(v as EdgeType)}>
<SelectTrigger className="bg-[#21262d] border-[#30363d] text-sm h-8">
<SelectTrigger className={`bg-[#21262d] border-[#30363d] text-sm h-8 cursor-pointer ${modalStyles['modal-interactive']} ${modalStyles['modal-radius']}`} aria-label="Edge type selector">
<SelectValue>{EDGE_TYPE_LABELS[type]}</SelectValue>
</SelectTrigger>
<SelectContent className="bg-[#21262d] border-[#30363d]">
@@ -89,7 +90,7 @@ export function EdgeModal({ open, onClose, onSubmit, onDelete, onClearWaypoints,
value={vlanId}
onChange={(e) => setVlanId(e.target.value)}
placeholder="e.g. 20"
className="bg-[#21262d] border-[#30363d] font-mono text-sm h-8"
className={`bg-[#21262d] border-[#30363d] font-mono text-sm h-8 ${modalStyles['modal-radius']}`}
/>
</div>
)}
@@ -100,19 +101,21 @@ export function EdgeModal({ open, onClose, onSubmit, onDelete, onClearWaypoints,
value={label}
onChange={(e) => setLabel(e.target.value)}
placeholder="e.g. 1G, trunk..."
className="bg-[#21262d] border-[#30363d] text-sm h-8"
className={`bg-[#21262d] border-[#30363d] text-sm h-8 ${modalStyles['modal-radius']}`}
/>
</div>
<div className="flex flex-col gap-1.5">
<Label className="text-xs text-muted-foreground">Path Style</Label>
<div className="flex rounded-md overflow-hidden border border-[#30363d]">
<div className={`flex rounded-md overflow-hidden border border-[#30363d] ${modalStyles['modal-interactive']}`}>
{(['bezier', 'smooth'] as EdgePathStyle[]).map((style) => (
<button
key={style}
type="button"
onClick={() => setPathStyle(style)}
className="flex-1 py-1 text-xs capitalize transition-colors"
className="flex-1 py-1 text-xs capitalize transition-colors cursor-pointer"
tabIndex={0}
aria-label={`Path style ${style}`}
style={{
background: pathStyle === style ? '#00d4ff22' : '#21262d',
color: pathStyle === style ? '#00d4ff' : '#8b949e',
@@ -127,13 +130,15 @@ export function EdgeModal({ open, onClose, onSubmit, onDelete, onClearWaypoints,
<div className="flex flex-col gap-1.5">
<Label className="text-xs text-muted-foreground">Animation</Label>
<div className="flex rounded-md overflow-hidden border border-[#30363d]">
<div className={`flex rounded-md overflow-hidden border border-[#30363d] ${modalStyles['modal-interactive']}`}>
{(['none', 'basic', 'snake', 'flow'] as AnimMode[]).map((mode, i) => (
<button
key={mode}
type="button"
onClick={() => setAnimation(mode)}
className="flex-1 py-1 text-xs capitalize transition-colors"
className="flex-1 py-1 text-xs capitalize transition-colors cursor-pointer"
tabIndex={0}
aria-label={`Animation mode ${mode}`}
style={{
background: animation === mode ? '#00d4ff22' : '#21262d',
color: animation === mode ? '#00d4ff' : '#8b949e',
@@ -160,8 +165,10 @@ export function EdgeModal({ open, onClose, onSubmit, onDelete, onClearWaypoints,
)}
</div>
<label
className="relative flex items-center gap-2.5 px-2.5 h-8 rounded-md border cursor-pointer"
className={`relative flex items-center gap-2.5 px-2.5 h-8 rounded-md border cursor-pointer ${modalStyles['modal-interactive']}`}
style={{ borderColor: customColor ? effectiveColor : '#30363d', background: '#21262d' }}
tabIndex={0}
aria-label="Edge color picker"
>
<input
type="color"
@@ -189,13 +196,13 @@ export function EdgeModal({ open, onClose, onSubmit, onDelete, onClearWaypoints,
<div className="flex justify-between gap-2 pt-1">
{onDelete ? (
<Button type="button" variant="ghost" size="sm" className="text-[#f85149] hover:text-[#f85149] hover:bg-[#f85149]/10" onClick={handleDelete}>
<Button type="button" variant="ghost" size="sm" className="text-[#f85149] hover:text-[#f85149] hover:bg-[#f85149]/10 cursor-pointer" onClick={handleDelete}>
Delete
</Button>
) : <span />}
<div className="flex gap-2">
<Button type="button" variant="ghost" size="sm" onClick={onClose}>Cancel</Button>
<Button type="submit" size="sm" className="bg-[#00d4ff] text-[#0d1117] hover:bg-[#00d4ff]/90">
<Button type="button" variant="ghost" size="sm" className="cursor-pointer" onClick={onClose}>Cancel</Button>
<Button type="submit" size="sm" className="bg-[#00d4ff] text-[#0d1117] hover:bg-[#00d4ff]/90 cursor-pointer">
{onDelete ? 'Save' : 'Connect'}
</Button>
</div>
@@ -1,4 +1,5 @@
import { useState } from 'react'
import modalStyles from './modal-interactive.module.css'
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
@@ -129,7 +130,7 @@ export function GroupRectModal({ open, onClose, onSubmit, onDelete, initial, tit
value={form.label}
onChange={(e) => set('label', e.target.value)}
placeholder="Zone name…"
className="bg-[#21262d] border-[#30363d] text-sm h-8"
className={`bg-[#21262d] border-[#30363d] text-sm h-8 ${modalStyles['modal-radius']}`}
/>
</div>
@@ -137,7 +138,7 @@ export function GroupRectModal({ open, onClose, onSubmit, onDelete, initial, tit
<div className="flex flex-col gap-1.5">
<Label className="text-xs text-muted-foreground">Font</Label>
<Select value={form.font} onValueChange={(v: string | null) => set('font', v ?? 'inter')}>
<SelectTrigger className="bg-[#21262d] border-[#30363d] text-sm h-8">
<SelectTrigger className={`bg-[#21262d] border-[#30363d] text-sm h-8 cursor-pointer ${modalStyles['modal-interactive']} ${modalStyles['modal-radius']}`}>
<SelectValue />
</SelectTrigger>
<SelectContent className="bg-[#21262d] border-[#30363d]">
@@ -162,7 +163,7 @@ export function GroupRectModal({ open, onClose, onSubmit, onDelete, initial, tit
type="button"
title={value}
onClick={() => set('text_position', value)}
className="h-8 rounded text-base transition-colors"
className={`h-8 rounded text-base transition-colors cursor-pointer ${modalStyles['modal-interactive']}`}
style={{
background: isSelected ? '#00d4ff22' : '#21262d',
border: `1px solid ${isSelected ? '#00d4ff88' : '#30363d'}`,
@@ -187,7 +188,7 @@ export function GroupRectModal({ open, onClose, onSubmit, onDelete, initial, tit
key={value}
type="button"
onClick={() => set('label_position', value)}
className="flex items-center justify-center h-8 rounded text-xs transition-colors"
className={`flex items-center justify-center h-8 rounded text-xs transition-colors cursor-pointer ${modalStyles['modal-interactive']}`}
style={{
background: isSelected ? '#00d4ff22' : '#21262d',
border: `1px solid ${isSelected ? '#00d4ff88' : '#30363d'}`,
@@ -248,7 +249,7 @@ export function GroupRectModal({ open, onClose, onSubmit, onDelete, initial, tit
key={value}
type="button"
onClick={() => set('text_size', value)}
className="flex items-center justify-center h-8 rounded transition-colors"
className={`flex items-center justify-center h-8 rounded transition-colors cursor-pointer ${modalStyles['modal-interactive']}`}
style={{
background: isSelected ? '#00d4ff22' : '#21262d',
border: `1px solid ${isSelected ? '#00d4ff88' : '#30363d'}`,
@@ -275,7 +276,7 @@ export function GroupRectModal({ open, onClose, onSubmit, onDelete, initial, tit
type="button"
title={label}
onClick={() => set('border_style', value)}
className="flex flex-col items-center justify-center h-10 rounded text-xs gap-0.5 transition-colors"
className={`flex flex-col items-center justify-center h-10 rounded text-xs gap-0.5 transition-colors cursor-pointer ${modalStyles['modal-interactive']}`}
style={{
background: isSelected ? '#00d4ff22' : '#21262d',
border: `1px solid ${isSelected ? '#00d4ff88' : '#30363d'}`,
@@ -301,7 +302,7 @@ export function GroupRectModal({ open, onClose, onSubmit, onDelete, initial, tit
key={value}
type="button"
onClick={() => set('border_width', value)}
className="flex items-center justify-center h-8 rounded text-xs transition-colors"
className={`flex items-center justify-center h-8 rounded text-xs transition-colors cursor-pointer ${modalStyles['modal-interactive']}`}
style={{
background: isSelected ? '#00d4ff22' : '#21262d',
border: `1px solid ${isSelected ? '#00d4ff88' : '#30363d'}`,
@@ -319,7 +320,7 @@ export function GroupRectModal({ open, onClose, onSubmit, onDelete, initial, tit
<div className="flex flex-col gap-1.5">
<Label className="text-xs text-muted-foreground">Z-Order (1 = furthest back)</Label>
<Select value={String(form.z_order)} onValueChange={(v: string | null) => set('z_order', v !== null ? Number(v) : 1)}>
<SelectTrigger className="bg-[#21262d] border-[#30363d] text-sm h-8">
<SelectTrigger className={`bg-[#21262d] border-[#30363d] text-sm h-8 cursor-pointer ${modalStyles['modal-interactive']}`}>
<SelectValue />
</SelectTrigger>
<SelectContent className="bg-[#21262d] border-[#30363d]">
@@ -338,17 +339,17 @@ export function GroupRectModal({ open, onClose, onSubmit, onDelete, initial, tit
type="button"
variant="ghost"
size="sm"
className="text-[#f85149] hover:text-[#f85149] hover:bg-[#f85149]/10"
className="text-[#f85149] hover:text-[#f85149] hover:bg-[#f85149]/10 cursor-pointer"
onClick={() => { onDelete(); onClose() }}
>
Delete
</Button>
)}
<div className="flex gap-2 ml-auto">
<Button type="button" variant="ghost" size="sm" onClick={onClose}>
<Button type="button" variant="ghost" size="sm" className={`cursor-pointer ${modalStyles['modal-cancel-hover']}`} onClick={onClose}>
Cancel
</Button>
<Button type="submit" size="sm" className="bg-[#00d4ff] text-[#0d1117] hover:bg-[#00d4ff]/90">
<Button type="submit" size="sm" className="bg-[#00d4ff] text-[#0d1117] hover:bg-[#00d4ff]/90 cursor-pointer">
{title === 'Add Zone' ? 'Add' : 'Save'}
</Button>
</div>
+49 -26
View File
@@ -1,4 +1,5 @@
import { Fragment, createElement, useState } from 'react'
import modalStyles from './modal-interactive.module.css'
import { RotateCcw, ChevronDown } from 'lucide-react'
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
@@ -96,7 +97,7 @@ export function NodeModal({ open, onClose, onSubmit, initial, title = 'Add Node'
<div className="flex flex-col gap-1.5">
<Label className="text-xs text-muted-foreground">Type</Label>
<Select value={form.type} onValueChange={(v) => set('type', v as NodeType)}>
<SelectTrigger className="bg-[#21262d] border-[#30363d] text-sm h-8 w-full">
<SelectTrigger className={`bg-[#21262d] border-[#30363d] text-sm h-8 w-full cursor-pointer ${modalStyles['modal-interactive']} ${modalStyles['modal-radius']}`} aria-label="Node type selector">
<SelectValue>{NODE_TYPE_LABELS[(form.type ?? 'server') as NodeType]}</SelectValue>
</SelectTrigger>
<SelectContent className="bg-[#21262d] border-[#30363d]">
@@ -137,7 +138,8 @@ export function NodeModal({ open, onClose, onSubmit, initial, title = 'Add Node'
<button
type="button"
onClick={() => setIconPickerOpen((o) => !o)}
className="flex items-center justify-between gap-2 h-8 px-3 rounded-md bg-[#21262d] border border-[#30363d] text-sm hover:border-[#8b949e] transition-colors w-full"
className={`flex items-center justify-between gap-2 h-8 px-3 bg-[#21262d] border border-[#30363d] text-sm transition-colors w-full cursor-pointer ${modalStyles['modal-interactive']} ${modalStyles['modal-radius']}`}
aria-label="Icon picker trigger"
>
<span className="flex items-center gap-2 min-w-0">
{(() => {
@@ -160,7 +162,7 @@ export function NodeModal({ open, onClose, onSubmit, initial, title = 'Add Node'
value={iconSearch}
onChange={(e) => setIconSearch(e.target.value)}
placeholder="Search icons…"
className="bg-[#21262d] border-[#30363d] text-xs h-7"
className={`bg-[#21262d] border-[#30363d] text-xs h-7 ${modalStyles['modal-radius']}`}
autoFocus
/>
<div className="flex flex-col gap-2 max-h-52 overflow-y-auto">
@@ -182,7 +184,8 @@ export function NodeModal({ open, onClose, onSubmit, initial, title = 'Add Node'
type="button"
title={entry.label}
onClick={() => { set('custom_icon', isSelected ? undefined : entry.key); setIconPickerOpen(false) }}
className="flex items-center justify-center w-7 h-7 rounded transition-colors"
className={`flex items-center justify-center w-7 h-7 rounded transition-colors cursor-pointer ${modalStyles['modal-interactive']}`}
aria-label={`Select icon ${entry.label}`}
style={{
background: isSelected ? '#00d4ff22' : 'transparent',
border: isSelected ? '1px solid #00d4ff88' : '1px solid transparent',
@@ -210,7 +213,7 @@ export function NodeModal({ open, onClose, onSubmit, initial, title = 'Add Node'
value={form.label ?? ''}
onChange={(e) => { set('label', e.target.value); if (labelError) setLabelError(false) }}
placeholder="My Server"
className={`bg-[#21262d] text-sm h-8 ${labelError ? 'border-[#f85149] focus-visible:ring-[#f85149]' : 'border-[#30363d]'}`}
className={`bg-[#21262d] text-sm h-8 ${labelError ? 'border-[#f85149] focus-visible:ring-[#f85149]' : 'border-[#30363d]'} ${modalStyles['modal-radius']}`}
/>
{labelError && <p className="text-[11px] text-[#f85149]">Label is required</p>}
</div>
@@ -222,26 +225,27 @@ export function NodeModal({ open, onClose, onSubmit, initial, title = 'Add Node'
value={form.hostname ?? ''}
onChange={(e) => set('hostname', e.target.value)}
placeholder="server.lan"
className="bg-[#21262d] border-[#30363d] font-mono text-sm h-8"
className={`bg-[#21262d] border-[#30363d] font-mono text-sm h-8 ${modalStyles['modal-radius']}`}
/>
</div>
{/* IP */}
<div className="flex flex-col gap-1.5">
<Label className="text-xs text-muted-foreground">IP Address <span className="text-muted-foreground/50">(comma-separated)</span></Label>
<Label className="text-xs text-muted-foreground">IP Address</Label>
<Input
value={form.ip ?? ''}
onChange={(e) => set('ip', e.target.value)}
placeholder="192.168.1.x, 2001:db8::1"
className="bg-[#21262d] border-[#30363d] font-mono text-sm h-8"
className={`bg-[#21262d] border-[#30363d] font-mono text-sm h-8 ${modalStyles['modal-radius']}`}
/>
<span className="text-[10px] text-muted-foreground/50">comma-separated</span>
</div>
{/* Check method */}
<div className="flex flex-col gap-1.5">
<Label className="text-xs text-muted-foreground">Check Method</Label>
<Select value={form.check_method ?? 'ping'} onValueChange={(v) => set('check_method', v as CheckMethod)}>
<SelectTrigger className="bg-[#21262d] border-[#30363d] text-sm h-8">
<SelectTrigger className={`bg-[#21262d] border-[#30363d] text-sm h-8 cursor-pointer ${modalStyles['modal-interactive']} ${modalStyles['modal-radius']}`} aria-label="Check method selector">
<SelectValue>{CHECK_METHOD_LABELS[(form.check_method ?? 'ping') as CheckMethod]}</SelectValue>
</SelectTrigger>
<SelectContent className="bg-[#21262d] border-[#30363d]">
@@ -259,7 +263,7 @@ export function NodeModal({ open, onClose, onSubmit, initial, title = 'Add Node'
value={form.check_target ?? ''}
onChange={(e) => set('check_target', e.target.value)}
placeholder="http://..."
className="bg-[#21262d] border-[#30363d] font-mono text-sm h-8"
className={`bg-[#21262d] border-[#30363d] font-mono text-sm h-8 ${modalStyles['modal-radius']}`}
/>
</div>
@@ -271,7 +275,7 @@ export function NodeModal({ open, onClose, onSubmit, initial, title = 'Add Node'
value={form.parent_id ?? 'none'}
onValueChange={(v) => set('parent_id', v === 'none' ? undefined : v)}
>
<SelectTrigger className="bg-[#21262d] border-[#30363d] text-sm h-8">
<SelectTrigger className={`bg-[#21262d] border-[#30363d] text-sm h-8 cursor-pointer ${modalStyles['modal-interactive']} ${modalStyles['modal-radius']}`} aria-label="Parent container selector">
<SelectValue placeholder="None (standalone)">
{form.parent_id
? (filteredParentNodes.find((n) => n.id === form.parent_id)?.label ?? 'None (standalone)')
@@ -300,7 +304,9 @@ export function NodeModal({ open, onClose, onSubmit, initial, title = 'Add Node'
role="switch"
aria-checked={!!form.container_mode}
onClick={() => set('container_mode', !form.container_mode)}
className="relative inline-flex h-5 w-9 shrink-0 cursor-pointer rounded-full transition-colors focus:outline-none"
className={`relative inline-flex h-5 w-9 shrink-0 cursor-pointer rounded-full transition-colors focus:outline-none ${modalStyles['modal-interactive']}`}
tabIndex={0}
aria-label="Toggle container mode"
style={{ background: form.container_mode ? '#ff6e00' : '#30363d' }}
>
<span
@@ -333,9 +339,11 @@ export function NodeModal({ open, onClose, onSubmit, initial, title = 'Add Node'
return (
<div key={key} className="flex flex-col gap-1 items-center">
<label
className="relative w-full h-7 rounded-md border cursor-pointer overflow-hidden transition-all"
className={`relative w-full h-7 rounded-md border cursor-pointer overflow-hidden transition-all ${modalStyles['modal-interactive']}`}
style={{ borderColor: isCustom ? currentValue : '#30363d' }}
title={`${key.charAt(0).toUpperCase() + key.slice(1)}: ${currentValue}`}
tabIndex={0}
aria-label={`Color picker for ${key}`}
>
<input
type="color"
@@ -363,7 +371,7 @@ export function NodeModal({ open, onClose, onSubmit, initial, title = 'Add Node'
value={String(form.bottom_handles ?? 1)}
onValueChange={(v) => set('bottom_handles', parseInt(v ?? '1', 10))}
>
<SelectTrigger className="bg-[#21262d] border-[#30363d] text-sm h-8">
<SelectTrigger className={`bg-[#21262d] border-[#30363d] text-sm h-8 cursor-pointer ${modalStyles['modal-interactive']} ${modalStyles['modal-radius']}`} aria-label="Bottom connection points selector">
<SelectValue />
</SelectTrigger>
<SelectContent className="bg-[#21262d] border-[#30363d]">
@@ -383,22 +391,37 @@ export function NodeModal({ open, onClose, onSubmit, initial, title = 'Add Node'
value={form.notes ?? ''}
onChange={(e) => set('notes', e.target.value)}
placeholder="Optional notes"
className="bg-[#21262d] border-[#30363d] text-sm h-8"
className={`bg-[#21262d] border-[#30363d] text-sm h-8 ${modalStyles['modal-radius']}`}
/>
</div>
</div>
<div className="flex justify-end gap-2 pt-1">
<Button type="button" variant="ghost" size="sm" onClick={onClose}>
Cancel
</Button>
<Button
type="submit"
size="sm"
className="bg-[#00d4ff] text-[#0d1117] hover:bg-[#00d4ff]/90"
>
{title === 'Add Node' ? 'Add' : 'Save'}
</Button>
<div className="flex justify-between gap-2 pt-1">
{/* Show delete button only for edit mode (not add) */}
{title !== 'Add Node' ? (
<Button
type="button"
variant="ghost"
size="sm"
className="text-[#f85149] hover:text-[#f85149] hover:bg-[#f85149]/10 cursor-pointer"
onClick={() => { if (window.confirm('Delete this node?')) onSubmit({ ...form, _delete: true }); onClose(); }}
style={{ minWidth: 64 }}
>
Delete
</Button>
) : <span />}
<div className="flex gap-2">
<Button type="button" variant="ghost" size="sm" className={`cursor-pointer ${modalStyles['modal-cancel-hover']}`} onClick={onClose}>
Cancel
</Button>
<Button
type="submit"
size="sm"
className="bg-[#00d4ff] text-[#0d1117] hover:bg-[#00d4ff]/90 cursor-pointer"
>
{title === 'Add Node' ? 'Add' : 'Save'}
</Button>
</div>
</div>
</form>
</DialogContent>
@@ -0,0 +1,27 @@
/* SidebarItem pointer on hover */
.sidebar-pointer:hover {
cursor: pointer !important;
}
/* Consistent border radius for all modal input/select/button elements */
.modal-radius {
border-radius: 6px !important;
}
/* Pointer cursor for close X */
.modal-close-pointer:hover {
cursor: pointer !important;
}
/* Subtle hover background for cancel button */
.modal-cancel-hover:hover {
background: #21262d !important;
}
/* Shared hover/focus border effect for interactive modal elements */
.modal-interactive {
transition: border-color 0.15s;
}
.modal-interactive:hover,
.modal-interactive:focus {
border-color: #8b949e !important;
outline: none;
}
@@ -200,7 +200,7 @@ export function DetailPanel({ onEdit }: DetailPanelProps) {
<aside className="w-72 shrink-0 flex flex-col border-l border-border bg-[#161b22] overflow-y-auto">
<div className="flex items-center justify-between px-4 py-3 border-b border-border">
<span className="font-semibold text-sm text-foreground truncate">{data.label}</span>
<button aria-label="Close panel" onClick={() => setSelectedNode(null)} className="text-muted-foreground hover:text-foreground transition-colors">
<button aria-label="Close panel" onClick={() => setSelectedNode(null)} className="text-muted-foreground hover:text-foreground transition-colors cursor-pointer">
<X size={16} />
</button>
</div>
@@ -243,7 +243,7 @@ export function DetailPanel({ onEdit }: DetailPanelProps) {
<span className="text-xs text-muted-foreground">Properties{properties.length > 0 ? ` (${properties.length})` : ''}</span>
<button
onClick={() => { setAddingProp((v) => !v); setEditingPropIndex(null) }}
className="flex items-center gap-1 text-[10px] text-[#00d4ff] hover:text-[#00d4ff]/80 transition-colors"
className="flex items-center gap-1 text-[10px] text-[#00d4ff] hover:text-[#00d4ff]/80 transition-colors cursor-pointer"
>
<Plus size={10} /> Add
</button>
@@ -289,7 +289,7 @@ export function DetailPanel({ onEdit }: DetailPanelProps) {
<div className="px-4 py-3 border-t border-border">
<div className="flex items-center justify-between mb-2">
<span className="text-xs text-muted-foreground">Services{services.length > 0 ? ` (${services.length})` : ''}</span>
<button onClick={() => { setAddingForNode((v) => v === node.id ? null : node.id); setEditingFor(null) }} className="flex items-center gap-1 text-[10px] text-[#00d4ff] hover:text-[#00d4ff]/80 transition-colors">
<button onClick={() => { setAddingForNode((v) => v === node.id ? null : node.id); setEditingFor(null) }} className="flex items-center gap-1 text-[10px] text-[#00d4ff] hover:text-[#00d4ff]/80 transition-colors cursor-pointer">
<Plus size={10} /> Add
</button>
</div>
@@ -316,10 +316,10 @@ export function DetailPanel({ onEdit }: DetailPanelProps) {
)}
<div className="mt-auto flex gap-2 px-4 py-3 border-t border-border">
<Button size="sm" variant="secondary" className="flex-1 gap-1.5" onClick={() => onEdit(node.id)}>
<Button size="sm" variant="secondary" className="flex-1 gap-1.5 cursor-pointer" onClick={() => onEdit(node.id)}>
<Edit size={14} /> Edit
</Button>
<Button size="sm" variant="destructive" className="gap-1.5" aria-label="Delete node" onClick={handleDelete}>
<Button size="sm" variant="destructive" className="gap-1.5 cursor-pointer" aria-label="Delete node" onClick={handleDelete}>
<Trash2 size={14} />
</Button>
</div>
+3 -2
View File
@@ -7,6 +7,7 @@ import { useAuthStore } from '@/stores/authStore'
import { scanApi, settingsApi } from '@/api/client'
import { toast } from 'sonner'
import { useLatestRelease } from '@/hooks/useLatestRelease'
import { PendingDeviceModal, type PendingDevice } from '@/components/modals/PendingDeviceModal'
const STANDALONE = import.meta.env.VITE_STANDALONE === 'true'
@@ -593,7 +594,7 @@ function ScanHistoryPanel() {
<div className="text-[#8b949e] text-[10px] font-mono truncate">{r.ranges.join(', ')}</div>
)}
{r.error && (
<div className="text-[#f85149] text-[10px] mt-1 leading-tight break-words whitespace-pre-wrap">
<div className="text-[#f85149] text-[10px] mt-1 leading-tight wrap-break-word whitespace-pre-wrap">
{r.error}
</div>
)}
@@ -749,7 +750,7 @@ function SidebarItem({ icon: Icon, label, collapsed, active, badge, accent, onCl
const btn = (
<button
onClick={onClick}
className={`relative flex items-center gap-2 w-full px-2 py-1.5 rounded-md text-sm transition-colors ${
className={`relative flex items-center gap-2 w-full px-2 py-1.5 rounded-md text-sm transition-colors cursor-pointer ${
active
? 'bg-[#00d4ff]/10 text-[#00d4ff]'
: accent
+10 -10
View File
@@ -39,7 +39,7 @@ export function Toolbar({ onSave, onAutoLayout, onExport, onChangeStyle, onUndo,
<div className="flex-1" />
<Button
size="sm" variant="ghost"
className="gap-1.5 text-muted-foreground hover:text-foreground disabled:opacity-30"
className="gap-1.5 text-muted-foreground hover:text-foreground disabled:opacity-30 cursor-pointer hover:bg-[#21262d]"
onClick={onUndo}
disabled={past.length === 0}
title="Undo (Ctrl+Z)"
@@ -48,7 +48,7 @@ export function Toolbar({ onSave, onAutoLayout, onExport, onChangeStyle, onUndo,
</Button>
<Button
size="sm" variant="ghost"
className="gap-1.5 text-muted-foreground hover:text-foreground disabled:opacity-30"
className="gap-1.5 text-muted-foreground hover:text-foreground disabled:opacity-30 cursor-pointer hover:bg-[#21262d]"
onClick={onRedo}
disabled={future.length === 0}
title="Redo (Ctrl+Y)"
@@ -56,13 +56,13 @@ export function Toolbar({ onSave, onAutoLayout, onExport, onChangeStyle, onUndo,
<Redo2 size={14} />
</Button>
<div className="w-px h-4 bg-border mx-1" />
<Button size="sm" variant="ghost" className="gap-1.5 text-muted-foreground hover:text-foreground" onClick={onAutoLayout}>
<Button size="sm" variant="ghost" className="gap-1.5 text-muted-foreground hover:text-foreground cursor-pointer hover:bg-[#21262d]" onClick={onAutoLayout}>
<LayoutDashboard size={14} /> Auto Layout
</Button>
<Button size="sm" variant="ghost" className="gap-1.5 text-muted-foreground hover:text-foreground" onClick={onChangeStyle}>
<Button size="sm" variant="ghost" className="gap-1.5 text-muted-foreground hover:text-foreground cursor-pointer hover:bg-[#21262d]" onClick={onChangeStyle}>
<Palette size={14} /> Style
</Button>
<Button size="sm" variant="ghost" className="gap-1.5 text-muted-foreground hover:text-foreground" onClick={() => fileInputRef.current?.click()} title="Import from YAML">
<Button size="sm" variant="ghost" className="gap-1.5 text-muted-foreground hover:text-foreground cursor-pointer hover:bg-[#21262d]" onClick={() => fileInputRef.current?.click()} title="Import from YAML">
<Upload size={14} /> Import
</Button>
<input
@@ -72,21 +72,21 @@ export function Toolbar({ onSave, onAutoLayout, onExport, onChangeStyle, onUndo,
className="hidden"
onChange={handleFileChange}
/>
<Button size="sm" variant="ghost" className="gap-1.5 text-muted-foreground hover:text-foreground" onClick={onExportYaml} title="Export canvas as YAML">
<Button size="sm" variant="ghost" className="gap-1.5 text-muted-foreground hover:text-foreground cursor-pointer hover:bg-[#21262d]" onClick={onExportYaml} title="Export canvas as YAML">
<Download size={14} /> Export
</Button>
<Button size="sm" variant="ghost" className="gap-1.5 text-muted-foreground hover:text-foreground" onClick={onExport} title="Download canvas as PNG">
<Button size="sm" variant="ghost" className="gap-1.5 text-muted-foreground hover:text-foreground cursor-pointer hover:bg-[#21262d]" onClick={onExport} title="Download canvas as PNG">
<FileDown size={14} /> PNG
</Button>
<Button size="sm" variant="ghost" className="gap-1.5 text-muted-foreground hover:text-foreground" onClick={onExportMd} title="Copy inventory as Markdown table">
<Button size="sm" variant="ghost" className="gap-1.5 text-muted-foreground hover:text-foreground cursor-pointer hover:bg-[#21262d]" onClick={onExportMd} title="Copy inventory as Markdown table">
<Table2 size={14} /> MD
</Button>
<Button size="sm" variant="ghost" className="gap-1.5 text-muted-foreground hover:text-foreground" onClick={onShortcuts} title="Keyboard shortcuts (?)">
<Button size="sm" variant="ghost" className="gap-1.5 text-muted-foreground hover:text-foreground cursor-pointer hover:bg-[#21262d]" onClick={onShortcuts} title="Keyboard shortcuts (?)">
<HelpCircle size={14} />
</Button>
<Button
size="sm"
className="gap-1.5 relative"
className="gap-1.5 relative cursor-pointer border border-transparent hover:border-white"
style={{
background: hasUnsavedChanges ? '#00d4ff' : undefined,
color: hasUnsavedChanges ? '#0d1117' : undefined,
+3 -3
View File
@@ -1,3 +1,4 @@
import modalStyles from '../modals/modal-interactive.module.css'
import * as React from "react"
import { Dialog as DialogPrimitive } from "@base-ui/react/dialog"
@@ -63,13 +64,12 @@ function DialogContent({
render={
<Button
variant="ghost"
className="absolute top-2 right-2"
className={"absolute top-2 right-2 " + modalStyles['modal-close-pointer']}
size="icon-sm"
/>
}
>
<XIcon
/>
<XIcon />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}