@pie-players/pie-tool-line-reader 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/tool-line-reader.js +3217 -0
- package/dist/tool-line-reader.js.map +1 -0
- package/index.ts +8 -0
- package/package.json +61 -0
- package/tool-line-reader.svelte +544 -0
package/index.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pie-tool-line-reader - PIE Assessment Tool
|
|
3
|
+
*
|
|
4
|
+
* This package exports a web component built from Svelte.
|
|
5
|
+
* Import the built version for CDN usage, or the .svelte source for Svelte projects.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Re-export any TypeScript types defined in the package
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pie-players/pie-tool-line-reader",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Reading guide overlay tool for PIE assessment player",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/pie-framework/pie-players.git"
|
|
9
|
+
},
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"access": "public"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"pie",
|
|
15
|
+
"assessment",
|
|
16
|
+
"tool",
|
|
17
|
+
"line-reader",
|
|
18
|
+
"reading",
|
|
19
|
+
"accessibility"
|
|
20
|
+
],
|
|
21
|
+
"svelte": "./tool-line-reader.svelte",
|
|
22
|
+
"main": "./dist/tool-line-reader.js",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"import": "./dist/tool-line-reader.js",
|
|
27
|
+
"svelte": "./tool-line-reader.svelte"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"dist",
|
|
32
|
+
"tool-line-reader.svelte",
|
|
33
|
+
"index.ts",
|
|
34
|
+
"package.json"
|
|
35
|
+
],
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"svelte": "^5.0.0"
|
|
38
|
+
},
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"unpkg": "./dist/tool-line-reader.js",
|
|
41
|
+
"jsdelivr": "./dist/tool-line-reader.js",
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@pie-players/pie-assessment-toolkit": "workspace:*",
|
|
44
|
+
"@pie-players/pie-players-shared": "workspace:*"
|
|
45
|
+
},
|
|
46
|
+
"types": "./dist/index.d.ts",
|
|
47
|
+
"scripts": {
|
|
48
|
+
"build": "vite build",
|
|
49
|
+
"dev": "vite build --watch",
|
|
50
|
+
"typecheck": "tsc --noEmit",
|
|
51
|
+
"lint": "biome check ."
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@biomejs/biome": "^2.3.10",
|
|
55
|
+
"@sveltejs/vite-plugin-svelte": "^6.1.4",
|
|
56
|
+
"svelte": "^5.16.1",
|
|
57
|
+
"typescript": "^5.7.0",
|
|
58
|
+
"vite": "^7.0.8",
|
|
59
|
+
"vite-plugin-dts": "^4.5.3"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
<svelte:options
|
|
2
|
+
customElement={{
|
|
3
|
+
tag: 'pie-tool-line-reader',
|
|
4
|
+
shadow: 'none',
|
|
5
|
+
props: {
|
|
6
|
+
visible: { type: 'Boolean', attribute: 'visible' },
|
|
7
|
+
toolId: { type: 'String', attribute: 'tool-id' },
|
|
8
|
+
coordinator: { type: 'Object' }
|
|
9
|
+
}
|
|
10
|
+
}}
|
|
11
|
+
/>
|
|
12
|
+
|
|
13
|
+
<script lang="ts">
|
|
14
|
+
|
|
15
|
+
import type { IToolCoordinator } from '@pie-players/pie-assessment-toolkit';
|
|
16
|
+
import { ZIndexLayer } from '@pie-players/pie-assessment-toolkit';
|
|
17
|
+
import ToolSettingsButton from '@pie-players/pie-players-shared/components/ToolSettingsButton.svelte';
|
|
18
|
+
import ToolSettingsPanel from '@pie-players/pie-players-shared/components/ToolSettingsPanel.svelte';
|
|
19
|
+
import { onMount } from 'svelte';
|
|
20
|
+
|
|
21
|
+
// Props
|
|
22
|
+
let { visible = false, toolId = 'lineReader', coordinator }: { visible?: boolean; toolId?: string; coordinator?: IToolCoordinator } = $props();
|
|
23
|
+
|
|
24
|
+
// Check if running in browser
|
|
25
|
+
const isBrowser = typeof window !== 'undefined';
|
|
26
|
+
|
|
27
|
+
// State
|
|
28
|
+
let containerEl = $state<HTMLDivElement | undefined>();
|
|
29
|
+
let settingsButtonEl = $state<HTMLButtonElement | undefined>();
|
|
30
|
+
let isDragging = $state(false);
|
|
31
|
+
let isResizing = $state(false);
|
|
32
|
+
let position = $state({
|
|
33
|
+
x: isBrowser ? window.innerWidth / 2 : 400,
|
|
34
|
+
y: isBrowser ? window.innerHeight / 2 : 300
|
|
35
|
+
});
|
|
36
|
+
let size = $state({ width: 600, height: 60 });
|
|
37
|
+
let dragStart = $state({ x: 0, y: 0 });
|
|
38
|
+
let resizeStart = $state({ width: 0, height: 0, mouseY: 0 });
|
|
39
|
+
let announceText = $state('');
|
|
40
|
+
let currentColor = $state('#ffff00'); // Yellow
|
|
41
|
+
let currentOpacity = $state(0.3);
|
|
42
|
+
let maskingMode = $state<'highlight' | 'obscure'>('highlight');
|
|
43
|
+
let settingsOpen = $state(false);
|
|
44
|
+
|
|
45
|
+
// Track registration state
|
|
46
|
+
let registered = $state(false);
|
|
47
|
+
|
|
48
|
+
// Available colors
|
|
49
|
+
const colors = [
|
|
50
|
+
{ name: 'Yellow', value: '#ffff00' },
|
|
51
|
+
{ name: 'Blue', value: '#00bfff' },
|
|
52
|
+
{ name: 'Pink', value: '#ff69b4' },
|
|
53
|
+
{ name: 'Green', value: '#00ff7f' },
|
|
54
|
+
{ name: 'Orange', value: '#ffa500' }
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
// Keyboard navigation constants
|
|
58
|
+
const MOVE_STEP = 10; // pixels
|
|
59
|
+
const RESIZE_STEP = 10; // pixels
|
|
60
|
+
|
|
61
|
+
function announce(message: string) {
|
|
62
|
+
announceText = message;
|
|
63
|
+
setTimeout(() => announceText = '', 1000);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function cycleColor() {
|
|
67
|
+
const currentIndex = colors.findIndex(c => c.value === currentColor);
|
|
68
|
+
const nextIndex = (currentIndex + 1) % colors.length;
|
|
69
|
+
currentColor = colors[nextIndex].value;
|
|
70
|
+
announce(`Color changed to ${colors[nextIndex].name}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function adjustOpacity(delta: number) {
|
|
74
|
+
currentOpacity = Math.max(0.1, Math.min(0.9, currentOpacity + delta));
|
|
75
|
+
announce(`Opacity ${Math.round(currentOpacity * 100)}%`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function toggleMaskingMode() {
|
|
79
|
+
maskingMode = maskingMode === 'highlight' ? 'obscure' : 'highlight';
|
|
80
|
+
announce(`Mode changed to ${maskingMode === 'highlight' ? 'highlight' : 'masking'}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function toggleSettings() {
|
|
84
|
+
settingsOpen = !settingsOpen;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function closeSettings() {
|
|
88
|
+
settingsOpen = false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function setColor(color: string) {
|
|
92
|
+
currentColor = color;
|
|
93
|
+
const colorName = colors.find(c => c.value === color)?.name || 'Unknown';
|
|
94
|
+
announce(`Color changed to ${colorName}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Pointer event handlers (better for web components)
|
|
98
|
+
function handlePointerDown(e: PointerEvent) {
|
|
99
|
+
const target = e.target as HTMLElement;
|
|
100
|
+
|
|
101
|
+
// Check if clicking the resize handle
|
|
102
|
+
if (target.closest('.resize-handle')) {
|
|
103
|
+
startResizing(e);
|
|
104
|
+
} else if (target.closest('.tool-settings-button') || target.closest('.tool-settings-panel')) {
|
|
105
|
+
// Don't start dragging when clicking settings
|
|
106
|
+
return;
|
|
107
|
+
} else {
|
|
108
|
+
startDragging(e);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function startDragging(e: PointerEvent) {
|
|
113
|
+
if (!containerEl) return;
|
|
114
|
+
|
|
115
|
+
// Capture pointer for isolated event handling
|
|
116
|
+
containerEl.setPointerCapture(e.pointerId);
|
|
117
|
+
isDragging = true;
|
|
118
|
+
dragStart = {
|
|
119
|
+
x: e.clientX - position.x,
|
|
120
|
+
y: e.clientY - position.y
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
coordinator?.bringToFront(containerEl);
|
|
124
|
+
|
|
125
|
+
// Add pointer move/up handlers to element (not window!)
|
|
126
|
+
containerEl.addEventListener('pointermove', handlePointerMove);
|
|
127
|
+
containerEl.addEventListener('pointerup', handlePointerUp);
|
|
128
|
+
|
|
129
|
+
e.preventDefault();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function startResizing(e: PointerEvent) {
|
|
133
|
+
if (!containerEl) return;
|
|
134
|
+
|
|
135
|
+
// Capture pointer for isolated event handling
|
|
136
|
+
containerEl.setPointerCapture(e.pointerId);
|
|
137
|
+
|
|
138
|
+
isResizing = true;
|
|
139
|
+
resizeStart = {
|
|
140
|
+
width: size.width,
|
|
141
|
+
height: size.height,
|
|
142
|
+
mouseY: e.clientY
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
coordinator?.bringToFront(containerEl);
|
|
146
|
+
|
|
147
|
+
// Add pointer move/up handlers to element (not window!)
|
|
148
|
+
containerEl.addEventListener('pointermove', handlePointerMove);
|
|
149
|
+
containerEl.addEventListener('pointerup', handlePointerUp);
|
|
150
|
+
|
|
151
|
+
e.preventDefault();
|
|
152
|
+
e.stopPropagation();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function handlePointerMove(e: PointerEvent) {
|
|
156
|
+
if (isDragging) {
|
|
157
|
+
position = {
|
|
158
|
+
x: e.clientX - dragStart.x,
|
|
159
|
+
y: e.clientY - dragStart.y
|
|
160
|
+
};
|
|
161
|
+
} else if (isResizing) {
|
|
162
|
+
// Vertical resize only
|
|
163
|
+
const deltaY = e.clientY - resizeStart.mouseY;
|
|
164
|
+
size.height = Math.max(20, Math.min(400, resizeStart.height + deltaY));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function handlePointerUp(e: PointerEvent) {
|
|
169
|
+
if (!containerEl) return;
|
|
170
|
+
|
|
171
|
+
// Release pointer capture
|
|
172
|
+
containerEl.releasePointerCapture(e.pointerId);
|
|
173
|
+
|
|
174
|
+
// Clean up event listeners
|
|
175
|
+
containerEl.removeEventListener('pointermove', handlePointerMove);
|
|
176
|
+
containerEl.removeEventListener('pointerup', handlePointerUp);
|
|
177
|
+
|
|
178
|
+
isDragging = false;
|
|
179
|
+
isResizing = false;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function handleKeyDown(e: KeyboardEvent) {
|
|
183
|
+
let handled = false;
|
|
184
|
+
|
|
185
|
+
switch (e.key) {
|
|
186
|
+
case 'ArrowUp':
|
|
187
|
+
position.y -= MOVE_STEP;
|
|
188
|
+
announce(`Moved up to ${Math.round(position.y)}`);
|
|
189
|
+
handled = true;
|
|
190
|
+
break;
|
|
191
|
+
case 'ArrowDown':
|
|
192
|
+
position.y += MOVE_STEP;
|
|
193
|
+
announce(`Moved down to ${Math.round(position.y)}`);
|
|
194
|
+
handled = true;
|
|
195
|
+
break;
|
|
196
|
+
case 'ArrowLeft':
|
|
197
|
+
position.x -= MOVE_STEP;
|
|
198
|
+
announce(`Moved left to ${Math.round(position.x)}`);
|
|
199
|
+
handled = true;
|
|
200
|
+
break;
|
|
201
|
+
case 'ArrowRight':
|
|
202
|
+
position.x += MOVE_STEP;
|
|
203
|
+
announce(`Moved right to ${Math.round(position.x)}`);
|
|
204
|
+
handled = true;
|
|
205
|
+
break;
|
|
206
|
+
case '+':
|
|
207
|
+
case '=':
|
|
208
|
+
size.height = Math.min(400, size.height + RESIZE_STEP);
|
|
209
|
+
announce(`Height ${size.height} pixels`);
|
|
210
|
+
handled = true;
|
|
211
|
+
break;
|
|
212
|
+
case '-':
|
|
213
|
+
case '_':
|
|
214
|
+
size.height = Math.max(20, size.height - RESIZE_STEP);
|
|
215
|
+
announce(`Height ${size.height} pixels`);
|
|
216
|
+
handled = true;
|
|
217
|
+
break;
|
|
218
|
+
case 'c':
|
|
219
|
+
case 'C':
|
|
220
|
+
cycleColor();
|
|
221
|
+
handled = true;
|
|
222
|
+
break;
|
|
223
|
+
case ']':
|
|
224
|
+
adjustOpacity(0.1);
|
|
225
|
+
handled = true;
|
|
226
|
+
break;
|
|
227
|
+
case '[':
|
|
228
|
+
adjustOpacity(-0.1);
|
|
229
|
+
handled = true;
|
|
230
|
+
break;
|
|
231
|
+
case 'm':
|
|
232
|
+
case 'M':
|
|
233
|
+
toggleMaskingMode();
|
|
234
|
+
handled = true;
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (handled) {
|
|
239
|
+
e.preventDefault();
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Register with coordinator when it becomes available
|
|
244
|
+
$effect(() => {
|
|
245
|
+
if (coordinator && toolId && !registered) {
|
|
246
|
+
coordinator.registerTool(toolId, 'Line Reader', undefined, ZIndexLayer.TOOL);
|
|
247
|
+
registered = true;
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
onMount(() => {
|
|
252
|
+
return () => {
|
|
253
|
+
if (coordinator && toolId) {
|
|
254
|
+
coordinator.unregisterTool(toolId);
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// Update element reference when container becomes available
|
|
260
|
+
$effect(() => {
|
|
261
|
+
if (coordinator && containerEl && toolId) {
|
|
262
|
+
coordinator.updateToolElement(toolId, containerEl);
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// Auto-focus when tool becomes visible
|
|
267
|
+
$effect(() => {
|
|
268
|
+
if (visible && containerEl) {
|
|
269
|
+
setTimeout(() => containerEl?.focus(), 100);
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// Computed background color with opacity
|
|
274
|
+
let backgroundColor = $derived(currentColor + Math.round(currentOpacity * 255).toString(16).padStart(2, '0'));
|
|
275
|
+
</script>
|
|
276
|
+
|
|
277
|
+
{#if visible}
|
|
278
|
+
<!-- Screen reader announcements -->
|
|
279
|
+
<div class="sr-only" role="status" aria-live="polite" aria-atomic="true">
|
|
280
|
+
{announceText}
|
|
281
|
+
</div>
|
|
282
|
+
|
|
283
|
+
<!-- Masking overlays (only in obscure mode) - 4 rectangles around the line reader window -->
|
|
284
|
+
{#if maskingMode === 'obscure'}
|
|
285
|
+
<!-- Top mask - from top of viewport to top of line reader -->
|
|
286
|
+
<div
|
|
287
|
+
class="line-reader-mask line-reader-mask-top"
|
|
288
|
+
style="height: {Math.max(0, position.y - size.height / 2)}px;"
|
|
289
|
+
aria-hidden="true"
|
|
290
|
+
></div>
|
|
291
|
+
<!-- Bottom mask - from bottom of line reader to bottom of viewport -->
|
|
292
|
+
<div
|
|
293
|
+
class="line-reader-mask line-reader-mask-bottom"
|
|
294
|
+
style="top: {position.y + size.height / 2}px;"
|
|
295
|
+
aria-hidden="true"
|
|
296
|
+
></div>
|
|
297
|
+
<!-- Left mask - left side of line reader window -->
|
|
298
|
+
<div
|
|
299
|
+
class="line-reader-mask line-reader-mask-left"
|
|
300
|
+
style="top: {position.y - size.height / 2}px; height: {size.height}px; width: {Math.max(0, position.x - size.width / 2)}px;"
|
|
301
|
+
aria-hidden="true"
|
|
302
|
+
></div>
|
|
303
|
+
<!-- Right mask - right side of line reader window -->
|
|
304
|
+
<div
|
|
305
|
+
class="line-reader-mask line-reader-mask-right"
|
|
306
|
+
style="top: {position.y - size.height / 2}px; height: {size.height}px; left: {position.x + size.width / 2}px;"
|
|
307
|
+
aria-hidden="true"
|
|
308
|
+
></div>
|
|
309
|
+
{/if}
|
|
310
|
+
|
|
311
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
312
|
+
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
313
|
+
<div
|
|
314
|
+
bind:this={containerEl}
|
|
315
|
+
class="line-reader-frame"
|
|
316
|
+
class:masking-mode={maskingMode === 'obscure'}
|
|
317
|
+
style="left: {position.x}px; top: {position.y}px; width: {size.width}px; height: {size.height}px;"
|
|
318
|
+
onpointerdown={handlePointerDown}
|
|
319
|
+
onkeydown={handleKeyDown}
|
|
320
|
+
role="application"
|
|
321
|
+
tabindex="0"
|
|
322
|
+
aria-label="Line Reader tool. Mode: {maskingMode === 'highlight' ? 'Highlight' : 'Masking'}. Use arrow keys to move, +/- to resize height, C to change color, [ and ] to adjust opacity, M to toggle mode. Current color: {colors.find(c => c.value === currentColor)?.name}, Opacity: {Math.round(currentOpacity * 100)}%"
|
|
323
|
+
aria-roledescription="Draggable and resizable reading guide overlay"
|
|
324
|
+
>
|
|
325
|
+
<div class="line-reader-container" style="background-color: {backgroundColor};">
|
|
326
|
+
<!-- Settings Button -->
|
|
327
|
+
<ToolSettingsButton
|
|
328
|
+
bind:buttonEl={settingsButtonEl}
|
|
329
|
+
onClick={toggleSettings}
|
|
330
|
+
ariaLabel="Line reader settings"
|
|
331
|
+
active={settingsOpen}
|
|
332
|
+
/>
|
|
333
|
+
</div>
|
|
334
|
+
|
|
335
|
+
<!-- Resize handle -->
|
|
336
|
+
<div
|
|
337
|
+
class="resize-handle resize-handle-bottom"
|
|
338
|
+
title="Drag to resize height"
|
|
339
|
+
role="button"
|
|
340
|
+
tabindex="-1"
|
|
341
|
+
aria-label="Resize handle - drag to adjust height"
|
|
342
|
+
>
|
|
343
|
+
<svg width="20" height="8" viewBox="0 0 20 8" aria-hidden="true">
|
|
344
|
+
<rect x="8" y="3" width="4" height="2" fill="#4CAF50" rx="1"/>
|
|
345
|
+
</svg>
|
|
346
|
+
</div>
|
|
347
|
+
</div>
|
|
348
|
+
|
|
349
|
+
<!-- Settings Panel - Rendered outside line-reader-frame to avoid height constraints -->
|
|
350
|
+
<ToolSettingsPanel
|
|
351
|
+
open={settingsOpen}
|
|
352
|
+
title="Line Reader Settings"
|
|
353
|
+
onClose={closeSettings}
|
|
354
|
+
anchorEl={settingsButtonEl}
|
|
355
|
+
>
|
|
356
|
+
<!-- Mode Selection - First, as it determines what other settings are relevant -->
|
|
357
|
+
<fieldset class="setting-group">
|
|
358
|
+
<legend>Mode</legend>
|
|
359
|
+
<label>
|
|
360
|
+
<input
|
|
361
|
+
type="radio"
|
|
362
|
+
name="mode"
|
|
363
|
+
value="highlight"
|
|
364
|
+
checked={maskingMode === 'highlight'}
|
|
365
|
+
onchange={() => { maskingMode = 'highlight'; announce('Mode changed to highlight'); }}
|
|
366
|
+
/>
|
|
367
|
+
<span>Highlight</span>
|
|
368
|
+
</label>
|
|
369
|
+
<label>
|
|
370
|
+
<input
|
|
371
|
+
type="radio"
|
|
372
|
+
name="mode"
|
|
373
|
+
value="obscure"
|
|
374
|
+
checked={maskingMode === 'obscure'}
|
|
375
|
+
onchange={() => { maskingMode = 'obscure'; announce('Mode changed to masking'); }}
|
|
376
|
+
/>
|
|
377
|
+
<span>Masking</span>
|
|
378
|
+
</label>
|
|
379
|
+
</fieldset>
|
|
380
|
+
|
|
381
|
+
<!-- Color Selection - Only shown in Highlight mode -->
|
|
382
|
+
{#if maskingMode === 'highlight'}
|
|
383
|
+
<fieldset class="setting-group">
|
|
384
|
+
<legend>Color</legend>
|
|
385
|
+
{#each colors as color}
|
|
386
|
+
<label>
|
|
387
|
+
<input
|
|
388
|
+
type="radio"
|
|
389
|
+
name="color"
|
|
390
|
+
value={color.value}
|
|
391
|
+
checked={currentColor === color.value}
|
|
392
|
+
onchange={() => setColor(color.value)}
|
|
393
|
+
/>
|
|
394
|
+
<div class="color-swatch" style="background-color: {color.value};"></div>
|
|
395
|
+
<span>{color.name}</span>
|
|
396
|
+
</label>
|
|
397
|
+
{/each}
|
|
398
|
+
</fieldset>
|
|
399
|
+
|
|
400
|
+
<!-- Opacity Slider - Only shown in Highlight mode -->
|
|
401
|
+
<div class="setting-group">
|
|
402
|
+
<div class="setting-label">
|
|
403
|
+
<span>Opacity</span>
|
|
404
|
+
<span class="setting-value" aria-live="polite">{Math.round(currentOpacity * 100)}%</span>
|
|
405
|
+
</div>
|
|
406
|
+
<input
|
|
407
|
+
type="range"
|
|
408
|
+
min="10"
|
|
409
|
+
max="90"
|
|
410
|
+
step="5"
|
|
411
|
+
value={currentOpacity * 100}
|
|
412
|
+
oninput={(e) => {
|
|
413
|
+
currentOpacity = Number(e.currentTarget.value) / 100;
|
|
414
|
+
announce(`Opacity ${Math.round(currentOpacity * 100)}%`);
|
|
415
|
+
}}
|
|
416
|
+
aria-label="Opacity"
|
|
417
|
+
aria-valuemin="10"
|
|
418
|
+
aria-valuemax="90"
|
|
419
|
+
aria-valuenow={Math.round(currentOpacity * 100)}
|
|
420
|
+
aria-valuetext="{Math.round(currentOpacity * 100)} percent"
|
|
421
|
+
/>
|
|
422
|
+
</div>
|
|
423
|
+
{/if}
|
|
424
|
+
</ToolSettingsPanel>
|
|
425
|
+
{/if}
|
|
426
|
+
|
|
427
|
+
<style>
|
|
428
|
+
.sr-only {
|
|
429
|
+
position: absolute;
|
|
430
|
+
width: 1px;
|
|
431
|
+
height: 1px;
|
|
432
|
+
padding: 0;
|
|
433
|
+
margin: -1px;
|
|
434
|
+
overflow: hidden;
|
|
435
|
+
clip: rect(0, 0, 0, 0);
|
|
436
|
+
white-space: nowrap;
|
|
437
|
+
border-width: 0;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
.line-reader-frame {
|
|
441
|
+
border: 2px solid rgba(76, 175, 80, 0.8);
|
|
442
|
+
cursor: move;
|
|
443
|
+
overflow: visible;
|
|
444
|
+
position: absolute;
|
|
445
|
+
transform: translate(-50%, -50%);
|
|
446
|
+
user-select: none;
|
|
447
|
+
pointer-events: auto;
|
|
448
|
+
touch-action: none;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
.line-reader-frame:focus {
|
|
452
|
+
outline: 3px solid #4A90E2;
|
|
453
|
+
outline-offset: 2px;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
.line-reader-frame:focus-visible {
|
|
457
|
+
outline: 3px solid #4A90E2;
|
|
458
|
+
outline-offset: 2px;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
.line-reader-container {
|
|
462
|
+
width: 100%;
|
|
463
|
+
height: 100%;
|
|
464
|
+
position: relative;
|
|
465
|
+
transition: background-color 0.2s ease;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
.resize-handle {
|
|
470
|
+
position: absolute;
|
|
471
|
+
cursor: ns-resize;
|
|
472
|
+
z-index: 10;
|
|
473
|
+
display: flex;
|
|
474
|
+
align-items: center;
|
|
475
|
+
justify-content: center;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
.resize-handle-bottom {
|
|
479
|
+
bottom: -10px;
|
|
480
|
+
left: 50%;
|
|
481
|
+
transform: translateX(-50%);
|
|
482
|
+
width: 40px;
|
|
483
|
+
height: 16px;
|
|
484
|
+
background-color: rgba(255, 255, 255, 0.9);
|
|
485
|
+
border-radius: 8px;
|
|
486
|
+
border: 2px solid #4CAF50;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
.resize-handle:hover {
|
|
490
|
+
background-color: rgba(76, 175, 80, 0.2);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
.resize-handle:active {
|
|
494
|
+
cursor: ns-resize;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
.line-reader-frame:active {
|
|
498
|
+
cursor: grabbing;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/* Masking overlays for obscure mode - 4 rectangles covering all areas except line reader window */
|
|
502
|
+
.line-reader-mask {
|
|
503
|
+
position: fixed;
|
|
504
|
+
background: rgba(0, 0, 0, 0.85);
|
|
505
|
+
z-index: 999;
|
|
506
|
+
pointer-events: none;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
.line-reader-mask-top {
|
|
510
|
+
top: 0;
|
|
511
|
+
left: 0;
|
|
512
|
+
right: 0;
|
|
513
|
+
/* Height set via inline style */
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
.line-reader-mask-bottom {
|
|
517
|
+
/* Top set via inline style */
|
|
518
|
+
left: 0;
|
|
519
|
+
right: 0;
|
|
520
|
+
bottom: 0;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
.line-reader-mask-left {
|
|
524
|
+
/* Top, height, and width set via inline style */
|
|
525
|
+
left: 0;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
.line-reader-mask-right {
|
|
529
|
+
/* Top, height, and left set via inline style */
|
|
530
|
+
right: 0;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/* In masking mode, change the window appearance */
|
|
534
|
+
.line-reader-frame.masking-mode {
|
|
535
|
+
border-color: rgba(76, 175, 80, 1);
|
|
536
|
+
box-shadow: 0 0 0 3px rgba(76, 175, 80, 0.8), 0 0 20px rgba(76, 175, 80, 0.4);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/* In masking mode, the window should be transparent to show content underneath */
|
|
540
|
+
.line-reader-frame.masking-mode .line-reader-container {
|
|
541
|
+
background-color: transparent !important;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
</style>
|