Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions client/src/scss/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -108,13 +108,26 @@ body {
align-items: center;
height: 100vh;

transition: background-color 0.2s ease;

&.dragging {
background-color: rgba(50, 150, 255, 0.1);
outline: 3px dashed rgba(50, 150, 255, 0.6);
outline-offset: -10px;
}

.initial-hero-content {
text-align: center;
color: #fff;
pointer-events: none; // Allow drag events to bubble through
}
a {
color: #bbf;
text-decoration: underline;
pointer-events: all; // Re-enable pointer events for links
}
button {
pointer-events: all; // Re-enable pointer events for buttons
Comment on lines +122 to +130
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting 'pointer-events: none' on the content and then re-enabling it for specific elements is a fragile pattern. If any new interactive elements (inputs, selects, other buttons, etc.) are added to the initial-hero-content in the future, they will be non-interactive unless explicitly given 'pointer-events: all'.

Consider a more maintainable approach, such as handling drag events at the container level and using event.target checks, or using a more specific selector for elements that should not block drag events.

Suggested change
pointer-events: none; // Allow drag events to bubble through
}
a {
color: #bbf;
text-decoration: underline;
pointer-events: all; // Re-enable pointer events for links
}
button {
pointer-events: all; // Re-enable pointer events for buttons
}
a {
color: #bbf;
text-decoration: underline;
}
button {

Copilot uses AI. Check for mistakes.
}
}

Expand Down
101 changes: 94 additions & 7 deletions client/src/views/InitialView.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,84 @@ import ografLogoUrl from '../assets/ograf_logo_colour_draft.svg'
import { serviceWorkerHandler } from '../ServiceWorkerHandler'

export function InitialView({ onGraphicsFolder }) {
const [isDragging, setIsDragging] = React.useState(false)
const [error, setError] = React.useState(null)

const handleFolderSelect = React.useCallback(
async (dirHandle) => {
try {
fileHandler.dirHandle = dirHandle
onGraphicsFolder({
graphicsList: await fileHandler.listGraphics(),
graphicsFolderName: fileHandler.dirHandle.name,
})
} catch (err) {
console.error(err)
setError(err.message)
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message access pattern 'err.message' may not be safe for all error types. If err is null, undefined, or doesn't have a message property, this could fail. While there's a fallback in the handleDrop function (line 73), this catch block doesn't have one.

Consider using a more defensive pattern like 'err?.message || String(err)' to ensure an error message is always available.

Suggested change
setError(err.message)
setError(err?.message || String(err))

Copilot uses AI. Check for mistakes.
}
},
[onGraphicsFolder]
)

const handleDragOver = React.useCallback((e) => {
e.preventDefault()
e.stopPropagation()
setIsDragging(true)
}, [])

const handleDragEnter = React.useCallback((e) => {
e.preventDefault()
e.stopPropagation()
setIsDragging(true)
}, [])

const handleDragLeave = React.useCallback((e) => {
e.preventDefault()
e.stopPropagation()
setIsDragging(false)
}, [])
Comment on lines +40 to +44
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The handleDragLeave implementation will trigger when dragging over child elements within the drop zone, causing the dragging visual feedback to flicker. This happens because dragLeave fires when entering child elements like the heading, paragraphs, or button.

To fix this, you need to track if the drag actually left the container by checking the relatedTarget or by using a reference counter pattern. One common approach is to only reset isDragging to false when the related target is not a descendant of the drop zone container.

Copilot uses AI. Check for mistakes.

const handleDrop = React.useCallback(
async (e) => {
e.preventDefault()
e.stopPropagation()
setIsDragging(false)
setError(null)

try {
// Get the first item from the drop
const items = e.dataTransfer.items
if (items && items.length > 0) {
const item = items[0]

// Check if the browser supports getAsFileSystemHandle
if (item.getAsFileSystemHandle) {
const handle = await item.getAsFileSystemHandle()
if (handle.kind === 'directory') {
await handleFolderSelect(handle)
} else {
setError('Please drop a folder, not a file')
}
} else {
setError('Drag and drop folders is not supported in this browser. Please use the button instead.')
}
}
} catch (err) {
console.error(err)
setError(err.message || 'Failed to process dropped folder')
}
},
[handleFolderSelect]
)

return (
<div className="initial-hero">
<div
className={`initial-hero ${isDragging ? 'dragging' : ''}`}
onDragOver={handleDragOver}
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
Comment on lines +85 to +86
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The drag and drop zone lacks ARIA attributes to make it accessible to screen readers and assistive technologies. Users who rely on keyboard navigation or screen readers won't be aware that this area accepts folder drops.

Consider adding appropriate ARIA attributes such as 'role="region"' and 'aria-label' to describe the drop zone functionality. You might also want to add 'aria-live' to announce when the dragging state changes.

Suggested change
onDrop={handleDrop}
>
onDrop={handleDrop}
role="region"
aria-label="Graphics folder drop zone. Drag and drop a folder here or use the button below."
tabIndex={0}
>
<div
aria-live="polite"
style={{
position: 'absolute',
width: '1px',
height: '1px',
margin: '-1px',
border: 0,
padding: 0,
clip: 'rect(0 0 0 0)',
overflow: 'hidden',
whiteSpace: 'nowrap',
}}
>
{isDragging
? 'Folder detected over drop zone. Release to load graphics.'
: 'Drag and drop a graphics folder onto this region to load graphics.'}
</div>

Copilot uses AI. Check for mistakes.
<div className="initial-hero-content">
<div>
<h1>
Expand Down Expand Up @@ -38,19 +114,30 @@ export function InitialView({ onGraphicsFolder }) {
</i>
</p>

<p>Start the DevTool by selecting a folder that contains Ograf Graphics in any of its subfolders.</p>
<p>
Start the DevTool by selecting a folder that contains Ograf Graphics in any of its subfolders.
</p>
<p>
<strong>Drag and drop a folder here</strong> or click the button below:
</p>
{error && (
<div className="alert alert-danger" role="alert">
{error}
</div>
)}
<p>
<Button
onClick={() => {
setError(null)
fileHandler
.init()
.then(async () => {
onGraphicsFolder({
graphicsList: await fileHandler.listGraphics(),
graphicsFolderName: fileHandler.dirHandle.name,
})
await handleFolderSelect(fileHandler.dirHandle)
})
.catch((err) => {
console.error(err)
setError(err.message)
Comment on lines +137 to +139
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message access pattern 'err.message' may not be safe for all error types. If err is null, undefined, or doesn't have a message property, this could fail.

Consider using a more defensive pattern like 'err?.message || String(err)' to ensure an error message is always available, similar to the fallback used in the handleDrop function at line 73.

Copilot uses AI. Check for mistakes.
})
.catch(console.error)
}}
>
Select local folder
Expand Down
Loading