@pie-players/pie-assessment-toolkit 0.2.0

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.
Files changed (194) hide show
  1. package/README.md +520 -0
  2. package/dist/attempt/TestSession.d.ts +90 -0
  3. package/dist/attempt/TestSession.d.ts.map +1 -0
  4. package/dist/attempt/TestSession.js +174 -0
  5. package/dist/attempt/TestSession.js.map +1 -0
  6. package/dist/core/TypedEventBus.d.ts +48 -0
  7. package/dist/core/TypedEventBus.d.ts.map +1 -0
  8. package/dist/core/TypedEventBus.js +63 -0
  9. package/dist/core/TypedEventBus.js.map +1 -0
  10. package/dist/index.d.ts +37 -0
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +44 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/item-loader.d.ts +45 -0
  15. package/dist/item-loader.d.ts.map +1 -0
  16. package/dist/item-loader.js +62 -0
  17. package/dist/item-loader.js.map +1 -0
  18. package/dist/player/AssessmentPlayer.d.ts +388 -0
  19. package/dist/player/AssessmentPlayer.d.ts.map +1 -0
  20. package/dist/player/AssessmentPlayer.js +965 -0
  21. package/dist/player/AssessmentPlayer.js.map +1 -0
  22. package/dist/player/index.d.ts +17 -0
  23. package/dist/player/index.d.ts.map +1 -0
  24. package/dist/player/index.js +15 -0
  25. package/dist/player/index.js.map +1 -0
  26. package/dist/player/navigation-types.d.ts +63 -0
  27. package/dist/player/navigation-types.d.ts.map +1 -0
  28. package/dist/player/navigation-types.js +7 -0
  29. package/dist/player/navigation-types.js.map +1 -0
  30. package/dist/player/qti-navigation.d.ts +29 -0
  31. package/dist/player/qti-navigation.d.ts.map +1 -0
  32. package/dist/player/qti-navigation.js +160 -0
  33. package/dist/player/qti-navigation.js.map +1 -0
  34. package/dist/reference-layout/index.d.ts +19 -0
  35. package/dist/reference-layout/index.d.ts.map +1 -0
  36. package/dist/reference-layout/index.js +20 -0
  37. package/dist/reference-layout/index.js.map +1 -0
  38. package/dist/services/AccessibilityCatalogResolver.d.ts +141 -0
  39. package/dist/services/AccessibilityCatalogResolver.d.ts.map +1 -0
  40. package/dist/services/AccessibilityCatalogResolver.js +249 -0
  41. package/dist/services/AccessibilityCatalogResolver.js.map +1 -0
  42. package/dist/services/AssessmentAuthoringService.d.ts +60 -0
  43. package/dist/services/AssessmentAuthoringService.d.ts.map +1 -0
  44. package/dist/services/AssessmentAuthoringService.js +183 -0
  45. package/dist/services/AssessmentAuthoringService.js.map +1 -0
  46. package/dist/services/ContextVariableStore.d.ts +114 -0
  47. package/dist/services/ContextVariableStore.d.ts.map +1 -0
  48. package/dist/services/ContextVariableStore.js +207 -0
  49. package/dist/services/ContextVariableStore.js.map +1 -0
  50. package/dist/services/ElementToolStateStore.d.ts +125 -0
  51. package/dist/services/ElementToolStateStore.d.ts.map +1 -0
  52. package/dist/services/ElementToolStateStore.js +200 -0
  53. package/dist/services/ElementToolStateStore.js.map +1 -0
  54. package/dist/services/HighlightCoordinator.d.ts +179 -0
  55. package/dist/services/HighlightCoordinator.d.ts.map +1 -0
  56. package/dist/services/HighlightCoordinator.js +446 -0
  57. package/dist/services/HighlightCoordinator.js.map +1 -0
  58. package/dist/services/I18nService.d.ts +121 -0
  59. package/dist/services/I18nService.d.ts.map +1 -0
  60. package/dist/services/I18nService.js +276 -0
  61. package/dist/services/I18nService.js.map +1 -0
  62. package/dist/services/PNPMapper.d.ts +71 -0
  63. package/dist/services/PNPMapper.d.ts.map +1 -0
  64. package/dist/services/PNPMapper.js +98 -0
  65. package/dist/services/PNPMapper.js.map +1 -0
  66. package/dist/services/PNPToolResolver.d.ts +127 -0
  67. package/dist/services/PNPToolResolver.d.ts.map +1 -0
  68. package/dist/services/PNPToolResolver.js +222 -0
  69. package/dist/services/PNPToolResolver.js.map +1 -0
  70. package/dist/services/SSMLExtractor.d.ts +61 -0
  71. package/dist/services/SSMLExtractor.d.ts.map +1 -0
  72. package/dist/services/SSMLExtractor.js +181 -0
  73. package/dist/services/SSMLExtractor.js.map +1 -0
  74. package/dist/services/TTSService.d.ts +162 -0
  75. package/dist/services/TTSService.d.ts.map +1 -0
  76. package/dist/services/TTSService.js +465 -0
  77. package/dist/services/TTSService.js.map +1 -0
  78. package/dist/services/ThemeProvider.d.ts +95 -0
  79. package/dist/services/ThemeProvider.d.ts.map +1 -0
  80. package/dist/services/ThemeProvider.js +299 -0
  81. package/dist/services/ThemeProvider.js.map +1 -0
  82. package/dist/services/ToolConfigResolver.d.ts +162 -0
  83. package/dist/services/ToolConfigResolver.d.ts.map +1 -0
  84. package/dist/services/ToolConfigResolver.js +230 -0
  85. package/dist/services/ToolConfigResolver.js.map +1 -0
  86. package/dist/services/ToolCoordinator.d.ts +137 -0
  87. package/dist/services/ToolCoordinator.d.ts.map +1 -0
  88. package/dist/services/ToolCoordinator.js +318 -0
  89. package/dist/services/ToolCoordinator.js.map +1 -0
  90. package/dist/services/ToolkitCoordinator.d.ts +188 -0
  91. package/dist/services/ToolkitCoordinator.d.ts.map +1 -0
  92. package/dist/services/ToolkitCoordinator.js +252 -0
  93. package/dist/services/ToolkitCoordinator.js.map +1 -0
  94. package/dist/services/interfaces.d.ts +391 -0
  95. package/dist/services/interfaces.d.ts.map +1 -0
  96. package/dist/services/interfaces.js +12 -0
  97. package/dist/services/interfaces.js.map +1 -0
  98. package/dist/services/tool-providers/DesmosToolProvider.d.ts +103 -0
  99. package/dist/services/tool-providers/DesmosToolProvider.d.ts.map +1 -0
  100. package/dist/services/tool-providers/DesmosToolProvider.js +127 -0
  101. package/dist/services/tool-providers/DesmosToolProvider.js.map +1 -0
  102. package/dist/services/tool-providers/IToolProvider.d.ts +137 -0
  103. package/dist/services/tool-providers/IToolProvider.d.ts.map +1 -0
  104. package/dist/services/tool-providers/IToolProvider.js +13 -0
  105. package/dist/services/tool-providers/IToolProvider.js.map +1 -0
  106. package/dist/services/tool-providers/TIToolProvider.d.ts +107 -0
  107. package/dist/services/tool-providers/TIToolProvider.d.ts.map +1 -0
  108. package/dist/services/tool-providers/TIToolProvider.js +123 -0
  109. package/dist/services/tool-providers/TIToolProvider.js.map +1 -0
  110. package/dist/services/tool-providers/TTSToolProvider.d.ts +143 -0
  111. package/dist/services/tool-providers/TTSToolProvider.d.ts.map +1 -0
  112. package/dist/services/tool-providers/TTSToolProvider.js +168 -0
  113. package/dist/services/tool-providers/TTSToolProvider.js.map +1 -0
  114. package/dist/services/tool-providers/ToolProviderRegistry.d.ts +165 -0
  115. package/dist/services/tool-providers/ToolProviderRegistry.d.ts.map +1 -0
  116. package/dist/services/tool-providers/ToolProviderRegistry.js +240 -0
  117. package/dist/services/tool-providers/ToolProviderRegistry.js.map +1 -0
  118. package/dist/services/tool-providers/index.d.ts +18 -0
  119. package/dist/services/tool-providers/index.d.ts.map +1 -0
  120. package/dist/services/tool-providers/index.js +15 -0
  121. package/dist/services/tool-providers/index.js.map +1 -0
  122. package/dist/services/tts/browser-provider.d.ts +24 -0
  123. package/dist/services/tts/browser-provider.d.ts.map +1 -0
  124. package/dist/services/tts/browser-provider.js +177 -0
  125. package/dist/services/tts/browser-provider.js.map +1 -0
  126. package/dist/services/tts/provider-interface.d.ts +131 -0
  127. package/dist/services/tts/provider-interface.d.ts.map +1 -0
  128. package/dist/services/tts/provider-interface.js +10 -0
  129. package/dist/services/tts/provider-interface.js.map +1 -0
  130. package/dist/tools/calculators/desmos-provider.d.ts +54 -0
  131. package/dist/tools/calculators/desmos-provider.d.ts.map +1 -0
  132. package/dist/tools/calculators/desmos-provider.js +384 -0
  133. package/dist/tools/calculators/desmos-provider.js.map +1 -0
  134. package/dist/tools/calculators/mathjs-provider.d.ts +50 -0
  135. package/dist/tools/calculators/mathjs-provider.d.ts.map +1 -0
  136. package/dist/tools/calculators/mathjs-provider.js +660 -0
  137. package/dist/tools/calculators/mathjs-provider.js.map +1 -0
  138. package/dist/tools/calculators/ti-provider.d.ts +122 -0
  139. package/dist/tools/calculators/ti-provider.d.ts.map +1 -0
  140. package/dist/tools/calculators/ti-provider.js +587 -0
  141. package/dist/tools/calculators/ti-provider.js.map +1 -0
  142. package/dist/tools/client.d.ts +17 -0
  143. package/dist/tools/client.d.ts.map +1 -0
  144. package/dist/tools/client.js +26 -0
  145. package/dist/tools/client.js.map +1 -0
  146. package/dist/tools/index.d.ts +8 -0
  147. package/dist/tools/index.d.ts.map +1 -0
  148. package/dist/tools/index.js +10 -0
  149. package/dist/tools/index.js.map +1 -0
  150. package/dist/tools/library-loader.d.ts +63 -0
  151. package/dist/tools/library-loader.d.ts.map +1 -0
  152. package/dist/tools/library-loader.js +292 -0
  153. package/dist/tools/library-loader.js.map +1 -0
  154. package/dist/tools/response-discovery.d.ts +72 -0
  155. package/dist/tools/response-discovery.d.ts.map +1 -0
  156. package/dist/tools/response-discovery.js +183 -0
  157. package/dist/tools/response-discovery.js.map +1 -0
  158. package/dist/tools/tool-coordinator.d.ts +76 -0
  159. package/dist/tools/tool-coordinator.d.ts.map +1 -0
  160. package/dist/tools/tool-coordinator.js +197 -0
  161. package/dist/tools/tool-coordinator.js.map +1 -0
  162. package/dist/tools/types.d.ts +427 -0
  163. package/dist/tools/types.d.ts.map +1 -0
  164. package/dist/tools/types.js +13 -0
  165. package/dist/tools/types.js.map +1 -0
  166. package/dist/tools/variant-resolver.d.ts +48 -0
  167. package/dist/tools/variant-resolver.d.ts.map +1 -0
  168. package/dist/tools/variant-resolver.js +214 -0
  169. package/dist/tools/variant-resolver.js.map +1 -0
  170. package/dist/types/events.d.ts +157 -0
  171. package/dist/types/events.d.ts.map +1 -0
  172. package/dist/types/events.js +11 -0
  173. package/dist/types/events.js.map +1 -0
  174. package/dist/utils/logger.d.ts +7 -0
  175. package/dist/utils/logger.d.ts.map +1 -0
  176. package/dist/utils/logger.js +11 -0
  177. package/dist/utils/logger.js.map +1 -0
  178. package/package.json +71 -0
  179. package/src/README.md +626 -0
  180. package/src/components/QuestionToolBar.svelte +406 -0
  181. package/src/player/AssessmentLayout.svelte +68 -0
  182. package/src/player/README.md +154 -0
  183. package/src/reference-layout/README.md +135 -0
  184. package/src/reference-layout/ReferenceLayout.svelte +209 -0
  185. package/src/reference-layout/components/AssessmentContent.svelte +259 -0
  186. package/src/reference-layout/components/AssessmentFooter.svelte +89 -0
  187. package/src/reference-layout/components/AssessmentHeader.svelte +250 -0
  188. package/src/reference-layout/components/AssessmentNavigation.svelte +134 -0
  189. package/src/reference-layout/components/AssessmentToolsBar.svelte +176 -0
  190. package/src/reference-layout/components/ItemPanel.svelte +200 -0
  191. package/src/reference-layout/components/NotesPanel.svelte +179 -0
  192. package/src/reference-layout/components/PassagePanel.svelte +76 -0
  193. package/src/tools/README.md +616 -0
  194. package/src/tools/calculators/README.md +395 -0
@@ -0,0 +1,135 @@
1
+ # Reference Layout
2
+
3
+ A reference assessment layout implementation for PIE assessments.
4
+
5
+ ## Overview
6
+
7
+ This directory contains a complete layout implementation demonstrating a production-ready assessment interface. It provides:
8
+
9
+ - Three-column layout (passage | item | notes)
10
+ - Top bar with accommodations and assessment controls
11
+ - Navigation bar with question selector and numbered navigation
12
+ - Bottom bar with item-level tools
13
+ - Item-specific inline tools (answer eliminator, notes)
14
+
15
+ ## Architecture
16
+
17
+ This layout is **presentation-only** - it orchestrates visual layout but delegates all business logic to:
18
+ - `AssessmentPlayer` for navigation and state management
19
+ - Assessment Toolkit services (ToolCoordinator, TTSService, etc.)
20
+ - Existing tool components from `/lib/tags/tool-*`
21
+
22
+ ## Components
23
+
24
+ ### Main Component
25
+
26
+ - **ReferenceLayout.svelte** - Main layout orchestrator
27
+
28
+ ### Sub-Components
29
+
30
+ - **AssessmentHeader.svelte** - Top bar with accommodations and assessment actions
31
+ - **AssessmentNavigation.svelte** - Question selector and numbered navigation
32
+ - **AssessmentContent.svelte** - Three-column content container
33
+ - **PassagePanel.svelte** - Left column for passages (optional)
34
+ - **ItemPanel.svelte** - Middle column for questions/items
35
+ - **NotesPanel.svelte** - Right sidebar for notes (optional)
36
+ - **AssessmentToolsBar.svelte** - Bottom bar with item-level tools
37
+ - **AssessmentFooter.svelte** - Footer with navigation and actions
38
+
39
+ ## Usage
40
+
41
+ ### Direct Usage
42
+
43
+ ```svelte
44
+ <script>
45
+ import { AssessmentPlayer } from '$lib/assessment-toolkit/reference-implementation';
46
+ import { ReferenceLayout } from '$lib/assessment-toolkit/reference-layout';
47
+
48
+ const player = new AssessmentPlayer({
49
+ assessment,
50
+ organizationId,
51
+ bundleHost,
52
+ mode: 'gather',
53
+ role: 'student',
54
+ // ...
55
+ });
56
+ </script>
57
+
58
+ <ReferenceLayout {player} {assessment} />
59
+ ```
60
+
61
+ ### Via Reference Implementation
62
+
63
+ The reference implementation provides a wrapper that uses this layout:
64
+
65
+ ```svelte
66
+ <script>
67
+ import { ReferenceAssessmentLayout } from '$lib/assessment-toolkit/reference-implementation';
68
+ </script>
69
+
70
+ <ReferenceAssessmentLayout {assessment} {organizationId} />
71
+ ```
72
+
73
+ ## Key Features
74
+
75
+ ### Tool Placement
76
+
77
+ Tools are visually grouped into different areas but use the same interface:
78
+
79
+ 1. **Accommodations (Top Right)**
80
+ - Audio (TTS)
81
+ - Translation
82
+ - Contrast (Color Scheme)
83
+ - Fullscreen
84
+
85
+ 2. **Assessment Actions (Top Center)**
86
+ - Overall Instructions
87
+ - Resources
88
+ - Review Summary
89
+ - Finish Later
90
+ - Flag Question
91
+
92
+ 3. **Item-Level Tools (Bottom Bar)**
93
+ - Calculator
94
+ - Graph
95
+ - Periodic Table
96
+ - Protractor
97
+ - Line Reader
98
+ - Text Magnifier
99
+ - Ruler
100
+
101
+ 4. **Item-Specific (Inline)**
102
+ - Answer Eliminator (inline with choices)
103
+ - Notes (right sidebar)
104
+
105
+ ### Passage Support
106
+
107
+ - Automatically detects passages from items
108
+ - Loads passages on demand
109
+ - Renders passages in left column using `<pie-iife-player>`
110
+
111
+ ### Responsive Design
112
+
113
+ - Desktop: Full three-column layout
114
+ - Tablet: Hides notes column, two-column layout
115
+ - Mobile: Single column with tools in menu
116
+
117
+ ## Dependencies
118
+
119
+ This layout requires:
120
+
121
+ - `AssessmentPlayer` from `reference-implementation`
122
+ - Assessment Toolkit services (ToolCoordinator, TTSService, etc.)
123
+ - Existing tool components from `/lib/tags/tool-*`
124
+ - `<pie-iife-player>` for rendering items and passages
125
+
126
+ ## Status
127
+
128
+ 🚧 **Under development** - Implementation in progress. See [LAYOUT-PLAN.md](./LAYOUT-PLAN.md) for detailed implementation plan.
129
+
130
+ ## Related Documentation
131
+
132
+ - [LAYOUT-PLAN.md](./LAYOUT-PLAN.md) - Detailed implementation plan
133
+ - [../reference-implementation/README.md](../reference-implementation/README.md) - Reference implementation documentation
134
+ - [../../docs/tools-high-level-architecture.md](../../docs/tools-high-level-architecture.md) - Tools architecture overview
135
+
@@ -0,0 +1,209 @@
1
+ <script lang="ts">
2
+
3
+ import type { AssessmentEntity, ItemConfig, ItemEntity, PassageEntity, RubricBlock } from '@pie-players/pie-players-shared/types';
4
+ import ToolAnnotationToolbar from '@pie-players/pie-tool-annotation-toolbar';
5
+ import { onDestroy, onMount } from 'svelte';
6
+ import type { AssessmentPlayer } from '../player/AssessmentPlayer';
7
+ import AssessmentContent from './components/AssessmentContent.svelte';
8
+ import AssessmentFooter from './components/AssessmentFooter.svelte';
9
+ import AssessmentHeader from './components/AssessmentHeader.svelte';
10
+ import AssessmentNavigation from './components/AssessmentNavigation.svelte';
11
+ import AssessmentToolsBar from './components/AssessmentToolsBar.svelte';
12
+
13
+ let {
14
+ player,
15
+ assessment,
16
+ bundleHost,
17
+ organizationId,
18
+ userName
19
+ }: {
20
+ player: AssessmentPlayer;
21
+ assessment: AssessmentEntity;
22
+ bundleHost?: string;
23
+ organizationId?: string | null;
24
+ userName?: string;
25
+ } = $props();
26
+
27
+ // State from player
28
+ let currentItem = $state<ItemEntity | null>(null);
29
+ let currentConfig = $state<ItemConfig | null>(null);
30
+ let sessionState = $state<{ id: string; data: any[] }>({ id: '', data: [] });
31
+ let navigationState = $state({
32
+ currentIndex: 0,
33
+ totalItems: 0,
34
+ canNext: false,
35
+ canPrevious: false,
36
+ isLoading: true
37
+ });
38
+ let isLoadingItem = $state(true);
39
+ let passage = $state<PassageEntity | null>(null);
40
+ let rubricBlocks = $state<RubricBlock[]>([]);
41
+
42
+ // Picture Dictionary Modal state
43
+ let showPictureDictionaryModal = $state(false);
44
+ let pictureDictionaryWord = $state('');
45
+
46
+ // Annotation toolbar event handlers
47
+ function handleDictionaryLookup(detail: { text: string }) {
48
+ // TODO: Open dictionary modal
49
+ console.log('Dictionary lookup:', detail.text);
50
+ }
51
+
52
+ function handleTranslationRequest(detail: { text: string }) {
53
+ // TODO: Open translation modal
54
+ console.log('Translation request:', detail.text);
55
+ }
56
+
57
+ function handlePictureDictionaryLookup(detail: { text: string }) {
58
+ pictureDictionaryWord = detail.text;
59
+ showPictureDictionaryModal = true;
60
+ }
61
+
62
+ // Load passage when item changes
63
+ async function loadPassage(item: ItemEntity | null) {
64
+ if (!item?.passage) {
65
+ passage = null;
66
+ return;
67
+ }
68
+
69
+ // If passage is already a full entity, use it
70
+ if (typeof item.passage === 'object' && 'id' in item.passage) {
71
+ passage = item.passage as PassageEntity;
72
+ return;
73
+ }
74
+
75
+ // Otherwise load by ID
76
+ if (typeof item.passage === 'string') {
77
+ try {
78
+ const url = organizationId
79
+ ? `/api/passage/${item.passage}?organizationId=${encodeURIComponent(organizationId)}`
80
+ : `/api/passage/${item.passage}`;
81
+ const response = await fetch(url);
82
+ if (response.ok) {
83
+ passage = await response.json();
84
+ } else {
85
+ passage = null;
86
+ }
87
+ } catch (error) {
88
+ console.error('[ReferenceLayout] Error loading passage:', error);
89
+ passage = null;
90
+ }
91
+ }
92
+ }
93
+
94
+ // Subscribe to player state changes
95
+ let unsubNav: (() => void) | null = null;
96
+ let unsubItem: (() => void) | null = null;
97
+ let unsubLoading: (() => void) | null = null;
98
+ let unsubSession: (() => void) | null = null;
99
+
100
+ onMount(() => {
101
+ unsubNav = player.onNavigationChange((state) => {
102
+ navigationState = state;
103
+ rubricBlocks =
104
+ player
105
+ .getCurrentSectionRubricBlocks()
106
+ .filter((b) => b.view === 'candidate');
107
+ });
108
+
109
+ unsubItem = player.onItemChange((item) => {
110
+ currentItem = item;
111
+ currentConfig = item?.config as ItemConfig | null;
112
+ loadPassage(item);
113
+ });
114
+
115
+ unsubLoading = player.onLoadingChange((loading) => {
116
+ isLoadingItem = loading;
117
+ });
118
+
119
+ unsubSession = player.onSessionChange((session) => {
120
+ sessionState = session;
121
+ });
122
+ });
123
+
124
+ onDestroy(() => {
125
+ unsubNav?.();
126
+ unsubItem?.();
127
+ unsubLoading?.();
128
+ unsubSession?.();
129
+ });
130
+ </script>
131
+
132
+ <div class="reference-layout">
133
+ <!-- Header -->
134
+ <AssessmentHeader
135
+ {player}
136
+ {assessment}
137
+ currentQuestion={navigationState.currentIndex + 1}
138
+ totalQuestions={navigationState.totalItems}
139
+ {userName}
140
+ />
141
+
142
+ <!-- Navigation -->
143
+ <AssessmentNavigation
144
+ {player}
145
+ currentIndex={navigationState.currentIndex}
146
+ totalItems={navigationState.totalItems}
147
+ />
148
+
149
+ <!-- Main Content -->
150
+ <AssessmentContent
151
+ {player}
152
+ currentItem={currentItem}
153
+ config={currentConfig}
154
+ {passage}
155
+ {rubricBlocks}
156
+ session={sessionState}
157
+ {bundleHost}
158
+ {organizationId}
159
+ isLoading={isLoadingItem}
160
+ />
161
+
162
+ <!-- Tools Bar -->
163
+ <AssessmentToolsBar {player} />
164
+
165
+ <!-- Footer -->
166
+ <AssessmentFooter
167
+ {player}
168
+ currentQuestion={navigationState.currentIndex + 1}
169
+ totalQuestions={navigationState.totalItems}
170
+ canNext={navigationState.canNext}
171
+ canPrevious={navigationState.canPrevious}
172
+ />
173
+ </div>
174
+
175
+ <!-- Annotation Toolbar (floating, appears on text selection) -->
176
+ <!-- Outside layout container to avoid overflow: hidden affecting fixed positioning -->
177
+ <ToolAnnotationToolbar
178
+ enabled={true}
179
+ ttsService={player.getTTSService()}
180
+ highlightCoordinator={player.getHighlightCoordinator()}
181
+ ondictionarylookup={handleDictionaryLookup}
182
+ ontranslationrequest={handleTranslationRequest}
183
+ onpicturedictionarylookup={handlePictureDictionaryLookup}
184
+ />
185
+
186
+ <!-- Picture Dictionary Modal -->
187
+ <PictureDictionaryModal
188
+ bind:visible={showPictureDictionaryModal}
189
+ word={pictureDictionaryWord}
190
+ organizationId={organizationId || undefined}
191
+ />
192
+
193
+ <style>
194
+ .reference-layout {
195
+ display: grid;
196
+ grid-template-rows: auto auto 1fr auto auto;
197
+ grid-template-columns: 1fr;
198
+ height: 100%;
199
+ min-height: 0;
200
+ overflow: hidden;
201
+ background-color: var(--pie-background, #ffffff);
202
+ }
203
+
204
+ /* Ensure grid rows don't overflow */
205
+ :global(.reference-layout > *) {
206
+ min-height: 0;
207
+ }
208
+ </style>
209
+
@@ -0,0 +1,259 @@
1
+ <script lang="ts">
2
+
3
+ import type { ItemConfig, ItemEntity, PassageEntity, RubricBlock } from '@pie-players/pie-players-shared/types';
4
+ import { onDestroy, onMount } from 'svelte';
5
+ import type { AssessmentPlayer } from '../../player/AssessmentPlayer';
6
+ import ItemPanel from './ItemPanel.svelte';
7
+ import NotesPanel from './NotesPanel.svelte';
8
+ import PassagePanel from './PassagePanel.svelte';
9
+
10
+ let {
11
+ player,
12
+ currentItem,
13
+ config,
14
+ passage,
15
+ rubricBlocks = [],
16
+ session,
17
+ bundleHost,
18
+ organizationId,
19
+ isLoading = false
20
+ }: {
21
+ player: AssessmentPlayer;
22
+ currentItem: ItemEntity | null;
23
+ config: ItemConfig | null;
24
+ passage: PassageEntity | null;
25
+ rubricBlocks?: RubricBlock[];
26
+ session: { id: string; data: any[] };
27
+ bundleHost?: string;
28
+ organizationId?: string | null;
29
+ isLoading?: boolean;
30
+ } = $props();
31
+
32
+ let containerEl = $state<HTMLDivElement | null>(null);
33
+ let passagePanelEl = $state<HTMLDivElement | null>(null);
34
+ let itemPanelEl = $state<HTMLDivElement | null>(null);
35
+ let dividerEl = $state<HTMLDivElement | null>(null);
36
+ let notesDividerEl = $state<HTMLDivElement | null>(null);
37
+
38
+ // Resize state
39
+ let isDragging = $state(false);
40
+ let startX = $state(0);
41
+ let passageWidth = $state(300); // Initial width in pixels
42
+ let minPassageWidth = 200;
43
+ let maxPassageWidth = 800;
44
+
45
+ const hasPassage = $derived(
46
+ (passage !== null) || (rubricBlocks?.length ?? 0) > 0,
47
+ );
48
+
49
+ // Notes sidebar
50
+ let showNotes = $state(true);
51
+ const notesWidth = 320;
52
+
53
+ // Calculate grid columns dynamically
54
+ const gridColumns = $derived.by(() => {
55
+ const parts: string[] = [];
56
+
57
+ if (hasPassage) {
58
+ parts.push(`${passageWidth}px`, 'auto');
59
+ }
60
+
61
+ // Item column always present
62
+ parts.push('1fr');
63
+
64
+ // Notes column (optional)
65
+ if (showNotes) {
66
+ parts.push('auto', `${notesWidth}px`);
67
+ }
68
+
69
+ return parts.join(' ');
70
+ });
71
+
72
+ function handleDividerMouseDown(e: MouseEvent) {
73
+ if (!hasPassage) return;
74
+ isDragging = true;
75
+ startX = e.clientX;
76
+ document.body.style.cursor = 'col-resize';
77
+ document.body.style.userSelect = 'none';
78
+
79
+ window.addEventListener('mousemove', handleMouseMove);
80
+ window.addEventListener('mouseup', handleMouseUp);
81
+ }
82
+
83
+ function handleMouseMove(e: MouseEvent) {
84
+ if (!isDragging || !containerEl) return;
85
+
86
+ const deltaX = e.clientX - startX;
87
+ const containerRect = containerEl.getBoundingClientRect();
88
+ const newWidth = passageWidth + deltaX;
89
+ const constrainedWidth = Math.max(
90
+ minPassageWidth,
91
+ Math.min(maxPassageWidth, newWidth)
92
+ );
93
+
94
+ passageWidth = constrainedWidth;
95
+ startX = e.clientX;
96
+ }
97
+
98
+ function handleMouseUp() {
99
+ isDragging = false;
100
+ document.body.style.cursor = '';
101
+ document.body.style.userSelect = '';
102
+
103
+ window.removeEventListener('mousemove', handleMouseMove);
104
+ window.removeEventListener('mouseup', handleMouseUp);
105
+ }
106
+
107
+ function handleKeyDown(e: KeyboardEvent) {
108
+ if (!hasPassage) return;
109
+
110
+ const step = 10; // pixels per keypress
111
+ let deltaX = 0;
112
+
113
+ if (e.key === 'ArrowLeft') {
114
+ deltaX = -step;
115
+ } else if (e.key === 'ArrowRight') {
116
+ deltaX = step;
117
+ } else {
118
+ return; // Ignore other keys
119
+ }
120
+
121
+ e.preventDefault();
122
+ const newWidth = passageWidth + deltaX;
123
+ const constrainedWidth = Math.max(
124
+ minPassageWidth,
125
+ Math.min(maxPassageWidth, newWidth)
126
+ );
127
+ passageWidth = constrainedWidth;
128
+ }
129
+
130
+ onMount(() => {
131
+ // Persist width to localStorage
132
+ const savedWidth = localStorage.getItem('schoolcity-passage-width');
133
+ if (savedWidth) {
134
+ passageWidth = parseInt(savedWidth, 10);
135
+ }
136
+ });
137
+
138
+ onDestroy(() => {
139
+ // Save width to localStorage
140
+ localStorage.setItem('schoolcity-passage-width', String(passageWidth));
141
+ handleMouseUp(); // Clean up any active drag
142
+ });
143
+ </script>
144
+
145
+ <div class="assessment-content" bind:this={containerEl} style="grid-template-columns: {gridColumns}">
146
+ {#if hasPassage}
147
+ <!-- Passage Panel -->
148
+ <div class="passage-container" bind:this={passagePanelEl}>
149
+ <PassagePanel {passage} {rubricBlocks} {bundleHost} env={player.getEnv()} />
150
+ </div>
151
+
152
+ <!-- Resizable Divider -->
153
+ <div
154
+ class="divider"
155
+ class:dragging={isDragging}
156
+ bind:this={dividerEl}
157
+ onmousedown={handleDividerMouseDown}
158
+ onkeydown={handleKeyDown}
159
+ role="button"
160
+ aria-label="Resize passage panel"
161
+ tabindex="0"
162
+ >
163
+ <div class="divider-handle"></div>
164
+ </div>
165
+ {/if}
166
+
167
+ <!-- Item Panel -->
168
+ <div class="item-container" bind:this={itemPanelEl}>
169
+ <ItemPanel
170
+ {currentItem}
171
+ {config}
172
+ {session}
173
+ {bundleHost}
174
+ env={player.getEnv()}
175
+ toolCoordinator={player.getToolCoordinator()}
176
+ {isLoading}
177
+ />
178
+ </div>
179
+
180
+ {#if showNotes}
181
+ <!-- Notes Divider (visual only for now) -->
182
+ <div class="divider notes-divider" bind:this={notesDividerEl} aria-hidden="true">
183
+ <div class="divider-handle"></div>
184
+ </div>
185
+
186
+ <!-- Notes Panel -->
187
+ <div class="notes-container">
188
+ <NotesPanel
189
+ itemId={currentItem?.id || ''}
190
+ itemLabel={currentItem?.name || ''}
191
+ onClose={() => (showNotes = false)}
192
+ />
193
+ </div>
194
+ {/if}
195
+ </div>
196
+
197
+ <style>
198
+ .assessment-content {
199
+ display: grid;
200
+ gap: 0;
201
+ overflow: hidden;
202
+ position: relative;
203
+ min-height: 0;
204
+ height: 100%;
205
+ }
206
+
207
+ .passage-container,
208
+ .item-container,
209
+ .notes-container {
210
+ overflow-y: auto;
211
+ overflow-x: hidden;
212
+ background-color: var(--pie-background, #ffffff);
213
+ }
214
+
215
+ .divider {
216
+ position: relative;
217
+ width: 4px;
218
+ background-color: var(--pie-border, #e0e0e0);
219
+ cursor: col-resize;
220
+ display: flex;
221
+ align-items: center;
222
+ justify-content: center;
223
+ transition: background-color 0.2s;
224
+ }
225
+
226
+ .divider:hover,
227
+ .divider.dragging {
228
+ background-color: var(--pie-primary, #3f51b5);
229
+ }
230
+
231
+ .divider-handle {
232
+ width: 2px;
233
+ height: 40px;
234
+ background-color: var(--pie-text-secondary, #666);
235
+ border-radius: 1px;
236
+ }
237
+
238
+ .divider.dragging .divider-handle {
239
+ background-color: var(--pie-primary, #3f51b5);
240
+ }
241
+
242
+ .divider:focus {
243
+ outline: 2px solid var(--pie-primary, #3f51b5);
244
+ outline-offset: -2px;
245
+ }
246
+
247
+ .notes-divider {
248
+ cursor: default;
249
+ }
250
+
251
+ /* On narrow screens, hide notes by default */
252
+ @media (max-width: 1100px) {
253
+ .notes-container,
254
+ .notes-divider {
255
+ display: none;
256
+ }
257
+ }
258
+ </style>
259
+
@@ -0,0 +1,89 @@
1
+ <script lang="ts">
2
+ import type { AssessmentPlayer } from '../../player/AssessmentPlayer';
3
+
4
+ let {
5
+ player,
6
+ currentQuestion,
7
+ totalQuestions,
8
+ canNext,
9
+ canPrevious
10
+ }: {
11
+ player: AssessmentPlayer;
12
+ currentQuestion: number;
13
+ totalQuestions: number;
14
+ canNext: boolean;
15
+ canPrevious: boolean;
16
+ } = $props();
17
+
18
+ function handlePrevious() {
19
+ player.navigatePrevious();
20
+ }
21
+
22
+ function handleNext() {
23
+ player.navigateNext();
24
+ }
25
+ </script>
26
+
27
+ <footer class="assessment-footer">
28
+ <!-- Question Counter and Navigation -->
29
+ <div class="footer-content">
30
+ <span class="question-counter">Question {currentQuestion} of {totalQuestions}</span>
31
+ <div class="navigation-buttons">
32
+ <button class="nav-button" onclick={handlePrevious} disabled={!canPrevious}>
33
+ ← Back
34
+ </button>
35
+ <button class="nav-button" onclick={handleNext} disabled={!canNext}>
36
+ Next →
37
+ </button>
38
+ </div>
39
+ </div>
40
+ </footer>
41
+
42
+ <style>
43
+ .assessment-footer {
44
+ display: flex;
45
+ align-items: center;
46
+ justify-content: center;
47
+ padding: 0.75rem 1rem;
48
+ background-color: var(--pie-background, #ffffff);
49
+ border-top: 1px solid var(--pie-border, #e0e0e0);
50
+ min-height: 56px;
51
+ }
52
+
53
+ .footer-content {
54
+ display: flex;
55
+ align-items: center;
56
+ gap: 1rem;
57
+ }
58
+
59
+ .question-counter {
60
+ font-size: 0.875rem;
61
+ color: var(--pie-text, #000);
62
+ font-weight: 500;
63
+ }
64
+
65
+ .navigation-buttons {
66
+ display: flex;
67
+ gap: 0.5rem;
68
+ }
69
+
70
+ .nav-button {
71
+ padding: 0.5rem 1rem;
72
+ background-color: var(--pie-background, #ffffff);
73
+ border: 1px solid var(--pie-border, #e0e0e0);
74
+ border-radius: 4px;
75
+ cursor: pointer;
76
+ font-size: 0.875rem;
77
+ color: var(--pie-text, #000);
78
+ }
79
+
80
+ .nav-button:hover:not(:disabled) {
81
+ background-color: var(--pie-secondary-background, #f5f5f5);
82
+ }
83
+
84
+ .nav-button:disabled {
85
+ opacity: 0.5;
86
+ cursor: not-allowed;
87
+ }
88
+ </style>
89
+