@pie-players/pie-tool-annotation-toolbar 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/index.ts ADDED
@@ -0,0 +1,8 @@
1
+ /**
2
+ * pie-tool-annotation-toolbar - 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,63 @@
1
+ {
2
+ "name": "@pie-players/pie-tool-annotation-toolbar",
3
+ "version": "0.1.1",
4
+ "type": "module",
5
+ "description": "Text annotation toolbar with highlighting and underlining 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
+ "annotation",
18
+ "toolbar",
19
+ "highlighting",
20
+ "accessibility"
21
+ ],
22
+ "svelte": "./tool-annotation-toolbar.svelte",
23
+ "main": "./dist/tool-annotation-toolbar.js",
24
+ "exports": {
25
+ ".": {
26
+ "types": "./dist/index.d.ts",
27
+ "import": "./dist/tool-annotation-toolbar.js",
28
+ "svelte": "./tool-annotation-toolbar.svelte"
29
+ }
30
+ },
31
+ "files": [
32
+ "dist",
33
+ "tool-annotation-toolbar.svelte",
34
+ "index.ts",
35
+ "package.json",
36
+ "README.md"
37
+ ],
38
+ "peerDependencies": {
39
+ "svelte": "^5.0.0"
40
+ },
41
+ "license": "MIT",
42
+ "unpkg": "./dist/tool-annotation-toolbar.js",
43
+ "jsdelivr": "./dist/tool-annotation-toolbar.js",
44
+ "dependencies": {
45
+ "@pie-players/pie-assessment-toolkit": "workspace:*",
46
+ "@pie-players/pie-players-shared": "workspace:*"
47
+ },
48
+ "types": "./dist/index.d.ts",
49
+ "scripts": {
50
+ "build": "vite build",
51
+ "dev": "vite build --watch",
52
+ "typecheck": "tsc --noEmit",
53
+ "lint": "biome check ."
54
+ },
55
+ "devDependencies": {
56
+ "@biomejs/biome": "^2.3.10",
57
+ "@sveltejs/vite-plugin-svelte": "^6.1.4",
58
+ "svelte": "^5.16.1",
59
+ "typescript": "^5.7.0",
60
+ "vite": "^7.0.8",
61
+ "vite-plugin-dts": "^4.5.3"
62
+ }
63
+ }
@@ -0,0 +1,129 @@
1
+ <svelte:options
2
+ customElement={{
3
+ tag: 'pie-tool-annotation-toolbar',
4
+ shadow: 'none',
5
+ props: {
6
+ enabled: { type: 'Boolean', attribute: 'enabled' },
7
+ highlightCoordinator: { type: 'Object', attribute: 'highlight-coordinator' }
8
+ }
9
+ }}
10
+ />
11
+
12
+ <script lang="ts">
13
+ import type { HighlightCoordinator, ITTSService } from '@pie-players/pie-assessment-toolkit';
14
+ import { BrowserTTSProvider, HighlightColor, TTSService } from '@pie-players/pie-assessment-toolkit';
15
+ import { onMount } from 'svelte';
16
+
17
+ interface Props {
18
+ enabled?: boolean;
19
+ highlightCoordinator?: HighlightCoordinator | null;
20
+ ttsService: ITTSService;
21
+ ondictionarylookup?: (detail: { text: string }) => void;
22
+ ontranslationrequest?: (detail: { text: string }) => void;
23
+ }
24
+
25
+ let { enabled = true, highlightCoordinator = null, ttsService, ondictionarylookup, ontranslationrequest }: Props = $props();
26
+
27
+ const isBrowser = typeof window !== 'undefined';
28
+
29
+ let state = $state({
30
+ visible: false,
31
+ text: '',
32
+ range: null as Range | null,
33
+ pos: { x: 0, y: 0 }
34
+ });
35
+
36
+ async function ensureTts() {
37
+ try {
38
+ const provider = new BrowserTTSProvider();
39
+ await ttsService.initialize(provider);
40
+ } catch {
41
+ // ignore
42
+ }
43
+ }
44
+
45
+ function hide() {
46
+ state.visible = false;
47
+ state.text = '';
48
+ state.range = null;
49
+ }
50
+
51
+ function showForSelection() {
52
+ if (!enabled || !isBrowser) return;
53
+ const sel = window.getSelection();
54
+ if (!sel || sel.rangeCount === 0) return hide();
55
+ const range = sel.getRangeAt(0);
56
+ const text = sel.toString().trim();
57
+ if (!text) return hide();
58
+
59
+ const rect = range.getBoundingClientRect();
60
+ state.visible = true;
61
+ state.text = text;
62
+ state.range = range.cloneRange();
63
+ state.pos = { x: rect.left + rect.width / 2, y: rect.top - 8 };
64
+ }
65
+
66
+ function addHighlight(color: HighlightColor) {
67
+ if (!state.range || !highlightCoordinator) return;
68
+ highlightCoordinator.addAnnotation(state.range, color);
69
+ hide();
70
+ }
71
+
72
+ function clearHighlights() {
73
+ highlightCoordinator?.clearAnnotations();
74
+ hide();
75
+ }
76
+
77
+ async function speak() {
78
+ if (!state.text) return;
79
+ await ensureTts();
80
+ await ttsService.speak(state.text);
81
+ }
82
+
83
+ onMount(() => {
84
+ const onMouseUp = () => showForSelection();
85
+ const onKeyUp = () => showForSelection();
86
+ const onScroll = () => hide();
87
+ document.addEventListener('mouseup', onMouseUp);
88
+ document.addEventListener('keyup', onKeyUp);
89
+ window.addEventListener('scroll', onScroll, true);
90
+ return () => {
91
+ document.removeEventListener('mouseup', onMouseUp);
92
+ document.removeEventListener('keyup', onKeyUp);
93
+ window.removeEventListener('scroll', onScroll, true);
94
+ };
95
+ });
96
+ </script>
97
+
98
+ {#if state.visible}
99
+ <div
100
+ class="annotation-toolbar fixed z-[4200] flex gap-1 bg-base-100 shadow rounded-box p-1"
101
+ style={`left:${state.pos.x}px; top:${state.pos.y}px; transform: translate(-50%, -100%);`}
102
+ role="toolbar"
103
+ aria-label="Annotation toolbar"
104
+ >
105
+ <button class="btn btn-xs" onclick={() => addHighlight(HighlightColor.YELLOW)} aria-label="Yellow highlight">Y</button>
106
+ <button class="btn btn-xs" onclick={() => addHighlight(HighlightColor.PINK)} aria-label="Pink highlight">P</button>
107
+ <button class="btn btn-xs" onclick={() => addHighlight(HighlightColor.BLUE)} aria-label="Blue highlight">B</button>
108
+ <button class="btn btn-xs" onclick={() => addHighlight(HighlightColor.GREEN)} aria-label="Green highlight">G</button>
109
+
110
+ <button class="btn btn-xs" onclick={speak} aria-label="Read selection">TTS</button>
111
+
112
+ <button
113
+ class="btn btn-xs"
114
+ onclick={() => ondictionarylookup?.({ text: state.text })}
115
+ aria-label="Dictionary lookup"
116
+ >
117
+ Dict
118
+ </button>
119
+ <button
120
+ class="btn btn-xs"
121
+ onclick={() => ontranslationrequest?.({ text: state.text })}
122
+ aria-label="Translation request"
123
+ >
124
+ Trans
125
+ </button>
126
+
127
+ <button class="btn btn-xs btn-ghost" onclick={clearHighlights} aria-label="Clear highlights">Clear</button>
128
+ </div>
129
+ {/if}