fix: resolve all Svelte a11y warnings
- Associate labels with controls in CaptionSettingsPanel (9 warnings) - Add tabindex and keyboard handlers to dialog overlays (8 warnings) - Add <track> to video element for caption accessibility - Add keyboard support to resize handle separator Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -188,9 +188,23 @@
|
|||||||
<div
|
<div
|
||||||
class="resize-handle"
|
class="resize-handle"
|
||||||
class:active={isResizing}
|
class:active={isResizing}
|
||||||
role="separator"
|
role="slider"
|
||||||
aria-orientation="horizontal"
|
aria-orientation="horizontal"
|
||||||
|
aria-label="Resize timeline and clip list"
|
||||||
|
aria-valuenow={timelineHeight}
|
||||||
|
aria-valuemin={40}
|
||||||
|
aria-valuemax={500}
|
||||||
|
tabindex="0"
|
||||||
onmousedown={handleResizeStart}
|
onmousedown={handleResizeStart}
|
||||||
|
onkeydown={(e) => {
|
||||||
|
if (e.key === 'ArrowDown') {
|
||||||
|
e.preventDefault();
|
||||||
|
timelineHeight = Math.min(500, timelineHeight + 10);
|
||||||
|
} else if (e.key === 'ArrowUp') {
|
||||||
|
e.preventDefault();
|
||||||
|
timelineHeight = Math.max(40, timelineHeight - 10);
|
||||||
|
}
|
||||||
|
}}
|
||||||
></div>
|
></div>
|
||||||
<div class="cliplist-pane">
|
<div class="cliplist-pane">
|
||||||
<ClipList onExport={() => (showExportDialog = true)} />
|
<ClipList onExport={() => (showExportDialog = true)} />
|
||||||
|
|||||||
@@ -12,8 +12,25 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="overlay" role="presentation" onclick={onClose}>
|
<div
|
||||||
<div class="dialog" role="dialog" onclick={(e) => e.stopPropagation()}>
|
class="overlay"
|
||||||
|
role="presentation"
|
||||||
|
tabindex="-1"
|
||||||
|
onclick={onClose}
|
||||||
|
onkeydown={(e) => {
|
||||||
|
if (e.key === 'Escape' || e.key === 'Enter' || e.key === ' ') {
|
||||||
|
if (e.key !== 'Escape') e.preventDefault();
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="dialog"
|
||||||
|
role="dialog"
|
||||||
|
tabindex="-1"
|
||||||
|
onclick={(e) => e.stopPropagation()}
|
||||||
|
onkeydown={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
<img class="app-icon" src="/favicon.svg" alt="GUI Video Clipper icon" />
|
<img class="app-icon" src="/favicon.svg" alt="GUI Video Clipper icon" />
|
||||||
<h2>GUI Video Clipper</h2>
|
<h2>GUI Video Clipper</h2>
|
||||||
{#if version}
|
{#if version}
|
||||||
|
|||||||
@@ -45,8 +45,9 @@
|
|||||||
<div class="scroll-area">
|
<div class="scroll-area">
|
||||||
<!-- Font Family -->
|
<!-- Font Family -->
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<label>Font</label>
|
<label for="caption-font">Font</label>
|
||||||
<select
|
<select
|
||||||
|
id="caption-font"
|
||||||
value={settings.fontFamily}
|
value={settings.fontFamily}
|
||||||
onchange={(e) => update({ fontFamily: (e.target as HTMLSelectElement).value })}
|
onchange={(e) => update({ fontFamily: (e.target as HTMLSelectElement).value })}
|
||||||
>
|
>
|
||||||
@@ -61,19 +62,20 @@
|
|||||||
<label>
|
<label>
|
||||||
Font Size
|
Font Size
|
||||||
<span class="value">{settings.fontSize}px</span>
|
<span class="value">{settings.fontSize}px</span>
|
||||||
|
<input
|
||||||
|
type="range" min="12" max="36" step="1"
|
||||||
|
value={settings.fontSize}
|
||||||
|
oninput={(e) => update({ fontSize: parseInt((e.target as HTMLInputElement).value, 10) })}
|
||||||
|
/>
|
||||||
</label>
|
</label>
|
||||||
<input
|
|
||||||
type="range" min="12" max="36" step="1"
|
|
||||||
value={settings.fontSize}
|
|
||||||
oninput={(e) => update({ fontSize: parseInt((e.target as HTMLInputElement).value, 10) })}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Text Color + Bold -->
|
<!-- Text Color + Bold -->
|
||||||
<div class="control row">
|
<div class="control row">
|
||||||
<label>Text Color</label>
|
<label for="caption-text-color">Text Color</label>
|
||||||
<div class="row-controls">
|
<div class="row-controls">
|
||||||
<input
|
<input
|
||||||
|
id="caption-text-color"
|
||||||
type="color" value={settings.textColor}
|
type="color" value={settings.textColor}
|
||||||
oninput={(e) => update({ textColor: (e.target as HTMLInputElement).value })}
|
oninput={(e) => update({ textColor: (e.target as HTMLInputElement).value })}
|
||||||
/>
|
/>
|
||||||
@@ -87,10 +89,10 @@
|
|||||||
|
|
||||||
<!-- Dimmed Text Color -->
|
<!-- Dimmed Text Color -->
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<label>Dimmed Text</label>
|
<label for="caption-dimmed-auto">Dimmed Text</label>
|
||||||
<div class="radio-row">
|
<div class="radio-row">
|
||||||
<label>
|
<label>
|
||||||
<input type="radio" name="dimmed-mode" value="auto"
|
<input id="caption-dimmed-auto" type="radio" name="dimmed-mode" value="auto"
|
||||||
checked={settings.dimmedColorMode === 'auto'}
|
checked={settings.dimmedColorMode === 'auto'}
|
||||||
onchange={() => update({ dimmedColorMode: 'auto' })} />
|
onchange={() => update({ dimmedColorMode: 'auto' })} />
|
||||||
Auto
|
Auto
|
||||||
@@ -108,18 +110,19 @@
|
|||||||
<label>
|
<label>
|
||||||
Dim Opacity
|
Dim Opacity
|
||||||
<span class="value">{Math.round(settings.dimmedOpacity * 100)}%</span>
|
<span class="value">{Math.round(settings.dimmedOpacity * 100)}%</span>
|
||||||
|
<input
|
||||||
|
type="range" min="10" max="90" step="5"
|
||||||
|
value={Math.round(settings.dimmedOpacity * 100)}
|
||||||
|
oninput={(e) => update({ dimmedOpacity: parseInt((e.target as HTMLInputElement).value, 10) / 100 })}
|
||||||
|
/>
|
||||||
</label>
|
</label>
|
||||||
<input
|
|
||||||
type="range" min="10" max="90" step="5"
|
|
||||||
value={Math.round(settings.dimmedOpacity * 100)}
|
|
||||||
oninput={(e) => update({ dimmedOpacity: parseInt((e.target as HTMLInputElement).value, 10) / 100 })}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="control row">
|
<div class="control row">
|
||||||
<label>Dimmed Color</label>
|
<label for="caption-dimmed-color">Dimmed Color</label>
|
||||||
<div class="row-controls">
|
<div class="row-controls">
|
||||||
<input
|
<input
|
||||||
|
id="caption-dimmed-color"
|
||||||
type="color" value={settings.dimmedColor}
|
type="color" value={settings.dimmedColor}
|
||||||
oninput={(e) => update({ dimmedColor: (e.target as HTMLInputElement).value })}
|
oninput={(e) => update({ dimmedColor: (e.target as HTMLInputElement).value })}
|
||||||
/>
|
/>
|
||||||
@@ -165,12 +168,12 @@
|
|||||||
<label>
|
<label>
|
||||||
BG Opacity
|
BG Opacity
|
||||||
<span class="value">{Math.round(settings.backgroundOpacity * 100)}%</span>
|
<span class="value">{Math.round(settings.backgroundOpacity * 100)}%</span>
|
||||||
|
<input
|
||||||
|
type="range" min="0" max="100" step="5"
|
||||||
|
value={Math.round(settings.backgroundOpacity * 100)}
|
||||||
|
oninput={(e) => update({ backgroundOpacity: parseInt((e.target as HTMLInputElement).value, 10) / 100 })}
|
||||||
|
/>
|
||||||
</label>
|
</label>
|
||||||
<input
|
|
||||||
type="range" min="0" max="100" step="5"
|
|
||||||
value={Math.round(settings.backgroundOpacity * 100)}
|
|
||||||
oninput={(e) => update({ backgroundOpacity: parseInt((e.target as HTMLInputElement).value, 10) / 100 })}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@@ -195,21 +198,21 @@
|
|||||||
<label>
|
<label>
|
||||||
Shadow Depth
|
Shadow Depth
|
||||||
<span class="value">{settings.shadowDepth}px</span>
|
<span class="value">{settings.shadowDepth}px</span>
|
||||||
|
<input
|
||||||
|
type="range" min="1" max="5" step="1"
|
||||||
|
value={settings.shadowDepth}
|
||||||
|
oninput={(e) => update({ shadowDepth: parseInt((e.target as HTMLInputElement).value, 10) })}
|
||||||
|
/>
|
||||||
</label>
|
</label>
|
||||||
<input
|
|
||||||
type="range" min="1" max="5" step="1"
|
|
||||||
value={settings.shadowDepth}
|
|
||||||
oninput={(e) => update({ shadowDepth: parseInt((e.target as HTMLInputElement).value, 10) })}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Position -->
|
<!-- Position -->
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<label>Position</label>
|
<label for="caption-pos-bottom">Position</label>
|
||||||
<div class="radio-row">
|
<div class="radio-row">
|
||||||
<label>
|
<label>
|
||||||
<input type="radio" name="caption-position" value="bottom"
|
<input id="caption-pos-bottom" type="radio" name="caption-position" value="bottom"
|
||||||
checked={settings.position === 'bottom'}
|
checked={settings.position === 'bottom'}
|
||||||
onchange={() => update({ position: 'bottom' })} />
|
onchange={() => update({ position: 'bottom' })} />
|
||||||
Bottom
|
Bottom
|
||||||
|
|||||||
@@ -43,7 +43,17 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="clip-list" role="listbox" onclick={handleContainerClick}>
|
<div
|
||||||
|
class="clip-list"
|
||||||
|
role="listbox"
|
||||||
|
tabindex="0"
|
||||||
|
onclick={handleContainerClick}
|
||||||
|
onkeydown={(e) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
selectClip(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
{#if clips.length === 0}
|
{#if clips.length === 0}
|
||||||
<div class="empty">No clips yet — press I to mark in-point, O to mark out-point</div>
|
<div class="empty">No clips yet — press I to mark in-point, O to mark out-point</div>
|
||||||
{:else}
|
{:else}
|
||||||
|
|||||||
@@ -136,8 +136,25 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="overlay" role="presentation" onclick={onClose}>
|
<div
|
||||||
<div class="dialog" role="dialog" onclick={(e) => e.stopPropagation()}>
|
class="overlay"
|
||||||
|
role="presentation"
|
||||||
|
tabindex="-1"
|
||||||
|
onclick={onClose}
|
||||||
|
onkeydown={(e) => {
|
||||||
|
if (e.key === 'Escape' || e.key === 'Enter' || e.key === ' ') {
|
||||||
|
if (e.key !== 'Escape') e.preventDefault();
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="dialog"
|
||||||
|
role="dialog"
|
||||||
|
tabindex="-1"
|
||||||
|
onclick={(e) => e.stopPropagation()}
|
||||||
|
onkeydown={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
<h2>Export Clips</h2>
|
<h2>Export Clips</h2>
|
||||||
|
|
||||||
{#if !isExportReady}
|
{#if !isExportReady}
|
||||||
|
|||||||
@@ -57,8 +57,25 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="overlay" role="presentation" onclick={onClose}>
|
<div
|
||||||
<div class="panel" role="dialog" onclick={(e) => e.stopPropagation()}>
|
class="overlay"
|
||||||
|
role="presentation"
|
||||||
|
tabindex="-1"
|
||||||
|
onclick={onClose}
|
||||||
|
onkeydown={(e) => {
|
||||||
|
if (e.key === 'Escape' || e.key === 'Enter' || e.key === ' ') {
|
||||||
|
if (e.key !== 'Escape') e.preventDefault();
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="panel"
|
||||||
|
role="dialog"
|
||||||
|
tabindex="-1"
|
||||||
|
onclick={(e) => e.stopPropagation()}
|
||||||
|
onkeydown={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
<h2>Preferences</h2>
|
<h2>Preferences</h2>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
|
|||||||
@@ -194,7 +194,9 @@
|
|||||||
onloadeddata={handleLoadedData}
|
onloadeddata={handleLoadedData}
|
||||||
preload="metadata"
|
preload="metadata"
|
||||||
playsinline
|
playsinline
|
||||||
></video>
|
>
|
||||||
|
<track kind="captions" />
|
||||||
|
</video>
|
||||||
{#if activeCues.length > 0}
|
{#if activeCues.length > 0}
|
||||||
<div class="caption-overlay" style={captionPosition}>
|
<div class="caption-overlay" style={captionPosition}>
|
||||||
{#each activeCues as cue}
|
{#each activeCues as cue}
|
||||||
|
|||||||
Reference in New Issue
Block a user