@pie-players/pie-assessment-toolkit 0.2.2 → 0.2.4

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 (100) hide show
  1. package/README.md +159 -1
  2. package/dist/index.d.ts +16 -2
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +9 -2
  5. package/dist/index.js.map +1 -1
  6. package/dist/player/AssessmentPlayer.d.ts +9 -0
  7. package/dist/player/AssessmentPlayer.d.ts.map +1 -1
  8. package/dist/player/AssessmentPlayer.js +13 -4
  9. package/dist/player/AssessmentPlayer.js.map +1 -1
  10. package/dist/services/AnnotationToolbarConfig.d.ts +134 -0
  11. package/dist/services/AnnotationToolbarConfig.d.ts.map +1 -0
  12. package/dist/services/AnnotationToolbarConfig.js +100 -0
  13. package/dist/services/AnnotationToolbarConfig.js.map +1 -0
  14. package/dist/services/HighlightCoordinator.d.ts +23 -10
  15. package/dist/services/HighlightCoordinator.d.ts.map +1 -1
  16. package/dist/services/HighlightCoordinator.js +87 -40
  17. package/dist/services/HighlightCoordinator.js.map +1 -1
  18. package/dist/services/PNPToolResolver.d.ts +171 -15
  19. package/dist/services/PNPToolResolver.d.ts.map +1 -1
  20. package/dist/services/PNPToolResolver.js +324 -32
  21. package/dist/services/PNPToolResolver.js.map +1 -1
  22. package/dist/services/RangeSerializer.d.ts +119 -0
  23. package/dist/services/RangeSerializer.d.ts.map +1 -0
  24. package/dist/services/RangeSerializer.js +199 -0
  25. package/dist/services/RangeSerializer.js.map +1 -0
  26. package/dist/services/TTSService.d.ts +4 -1
  27. package/dist/services/TTSService.d.ts.map +1 -1
  28. package/dist/services/TTSService.js +58 -3
  29. package/dist/services/TTSService.js.map +1 -1
  30. package/dist/services/ToolRegistry.d.ts +225 -0
  31. package/dist/services/ToolRegistry.d.ts.map +1 -0
  32. package/dist/services/ToolRegistry.js +248 -0
  33. package/dist/services/ToolRegistry.js.map +1 -0
  34. package/dist/services/ToolkitCoordinator.d.ts +22 -0
  35. package/dist/services/ToolkitCoordinator.d.ts.map +1 -1
  36. package/dist/services/ToolkitCoordinator.js +44 -0
  37. package/dist/services/ToolkitCoordinator.js.map +1 -1
  38. package/dist/services/createDefaultToolRegistry.d.ts +72 -0
  39. package/dist/services/createDefaultToolRegistry.d.ts.map +1 -0
  40. package/dist/services/createDefaultToolRegistry.js +147 -0
  41. package/dist/services/createDefaultToolRegistry.js.map +1 -0
  42. package/dist/services/interfaces.d.ts +8 -0
  43. package/dist/services/interfaces.d.ts.map +1 -1
  44. package/dist/services/interfaces.js.map +1 -1
  45. package/dist/services/pnp-provenance.d.ts +167 -0
  46. package/dist/services/pnp-provenance.d.ts.map +1 -0
  47. package/dist/services/pnp-provenance.js +230 -0
  48. package/dist/services/pnp-provenance.js.map +1 -0
  49. package/dist/services/pnp-standard-features.d.ts +240 -0
  50. package/dist/services/pnp-standard-features.d.ts.map +1 -0
  51. package/dist/services/pnp-standard-features.js +342 -0
  52. package/dist/services/pnp-standard-features.js.map +1 -0
  53. package/dist/services/tool-context.d.ts +150 -0
  54. package/dist/services/tool-context.d.ts.map +1 -0
  55. package/dist/services/tool-context.js +157 -0
  56. package/dist/services/tool-context.js.map +1 -0
  57. package/dist/services/tool-providers/ToolProviderRegistry.d.ts.map +1 -1
  58. package/dist/services/tool-providers/ToolProviderRegistry.js +7 -7
  59. package/dist/services/tool-providers/ToolProviderRegistry.js.map +1 -1
  60. package/dist/tools/registrations/accessibility-tools.d.ts +43 -0
  61. package/dist/tools/registrations/accessibility-tools.d.ts.map +1 -0
  62. package/dist/tools/registrations/accessibility-tools.js +230 -0
  63. package/dist/tools/registrations/accessibility-tools.js.map +1 -0
  64. package/dist/tools/registrations/calculator.d.ts +21 -0
  65. package/dist/tools/registrations/calculator.d.ts.map +1 -0
  66. package/dist/tools/registrations/calculator.js +98 -0
  67. package/dist/tools/registrations/calculator.js.map +1 -0
  68. package/dist/tools/registrations/interaction-tools.d.ts +28 -0
  69. package/dist/tools/registrations/interaction-tools.d.ts.map +1 -0
  70. package/dist/tools/registrations/interaction-tools.js +124 -0
  71. package/dist/tools/registrations/interaction-tools.js.map +1 -0
  72. package/dist/tools/registrations/measurement-tools.d.ts +25 -0
  73. package/dist/tools/registrations/measurement-tools.d.ts.map +1 -0
  74. package/dist/tools/registrations/measurement-tools.js +112 -0
  75. package/dist/tools/registrations/measurement-tools.js.map +1 -0
  76. package/dist/tools/registrations/subject-specific-tools.d.ts +28 -0
  77. package/dist/tools/registrations/subject-specific-tools.d.ts.map +1 -0
  78. package/dist/tools/registrations/subject-specific-tools.js +118 -0
  79. package/dist/tools/registrations/subject-specific-tools.js.map +1 -0
  80. package/dist/tools/registrations/tts.d.ts +20 -0
  81. package/dist/tools/registrations/tts.d.ts.map +1 -0
  82. package/dist/tools/registrations/tts.js +86 -0
  83. package/dist/tools/registrations/tts.js.map +1 -0
  84. package/dist/tools/tool-tag-map.d.ts +24 -0
  85. package/dist/tools/tool-tag-map.d.ts.map +1 -0
  86. package/dist/tools/tool-tag-map.js +38 -0
  87. package/dist/tools/tool-tag-map.js.map +1 -0
  88. package/package.json +15 -7
  89. package/src/README.md +25 -11
  90. package/src/components/PNPProfileTester.example.svelte +188 -0
  91. package/src/components/PNPProfileTester.svelte +397 -0
  92. package/src/components/PNPProvenanceViewer.svelte +260 -0
  93. package/src/components/QuestionToolBar.svelte +160 -83
  94. package/src/components/ToolButton.svelte +146 -0
  95. package/src/components/ToolButtonGroup.svelte +109 -0
  96. package/src/reference-layout/ReferenceLayout.svelte +5 -13
  97. package/dist/services/PNPMapper.d.ts +0 -71
  98. package/dist/services/PNPMapper.d.ts.map +0 -1
  99. package/dist/services/PNPMapper.js +0 -98
  100. package/dist/services/PNPMapper.js.map +0 -1
@@ -0,0 +1,260 @@
1
+ <script lang="ts">
2
+ import type { PNPResolutionProvenance } from '../services/pnp-provenance';
3
+
4
+ // PNP Provenance Viewer Component
5
+ // Visualizes how PNP resolution decisions were made.
6
+ // See PNP_TESTING.md for usage documentation.
7
+
8
+ interface Props {
9
+ provenance: PNPResolutionProvenance;
10
+ compact?: boolean;
11
+ }
12
+
13
+ let { provenance, compact = false }: Props = $props();
14
+
15
+ let features = $derived(Array.from(provenance.features.values()));
16
+ let enabledFeatures = $derived(features.filter((f) => f.finalState === 'enabled'));
17
+ let blockedFeatures = $derived(features.filter((f) => f.finalState === 'blocked'));
18
+
19
+ function getRuleBadgeClass(rule: string): string {
20
+ const classes: Record<string, string> = {
21
+ 'district-block': 'badge-error',
22
+ 'test-admin-override': 'badge-warning',
23
+ 'item-restriction': 'badge-info',
24
+ 'item-requirement': 'badge-success',
25
+ 'district-requirement': 'badge-success',
26
+ 'pnp-support': 'badge-primary',
27
+ 'system-default': 'badge-ghost'
28
+ };
29
+ return classes[rule] || 'badge-ghost';
30
+ }
31
+
32
+ function getActionIcon(action: string): string {
33
+ const icons: Record<string, string> = {
34
+ enable: '✓',
35
+ block: '✗',
36
+ skip: '○'
37
+ };
38
+ return icons[action] || '?';
39
+ }
40
+ </script>
41
+
42
+ <div class="pnp-provenance-viewer" class:compact>
43
+ <!-- Summary -->
44
+ <div class="card bg-base-100 shadow-sm mb-4">
45
+ <div class="card-body">
46
+ <h3 class="card-title text-sm">Resolution Summary</h3>
47
+ <div class="stats stats-vertical lg:stats-horizontal shadow">
48
+ <div class="stat">
49
+ <div class="stat-title">Total Features</div>
50
+ <div class="stat-value text-2xl">{provenance.summary.totalFeatures}</div>
51
+ </div>
52
+ <div class="stat">
53
+ <div class="stat-title">Enabled</div>
54
+ <div class="stat-value text-2xl text-success">{provenance.summary.enabled}</div>
55
+ </div>
56
+ <div class="stat">
57
+ <div class="stat-title">Blocked</div>
58
+ <div class="stat-value text-2xl text-error">{provenance.summary.blocked}</div>
59
+ </div>
60
+ </div>
61
+
62
+ <div class="text-xs text-base-content/70 mt-2">
63
+ Resolved at: {provenance.resolvedAt.toLocaleString()}
64
+ </div>
65
+ </div>
66
+ </div>
67
+
68
+ <!-- Configuration Sources -->
69
+ {#if !compact}
70
+ <div class="card bg-base-100 shadow-sm mb-4">
71
+ <div class="card-body">
72
+ <h3 class="card-title text-sm">Configuration Sources</h3>
73
+ <div class="space-y-2">
74
+ {#each Object.entries(provenance.sources) as [type, source]}
75
+ <div class="flex items-center gap-2">
76
+ <div class="badge badge-outline">{type}</div>
77
+ <div class="text-sm">
78
+ {source.name || source.id}
79
+ <span class="text-xs text-base-content/50">({source.id})</span>
80
+ </div>
81
+ </div>
82
+ {/each}
83
+ </div>
84
+ </div>
85
+ </div>
86
+ {/if}
87
+
88
+ <!-- Feature Resolution -->
89
+ <div class="space-y-4">
90
+ <!-- Enabled Features -->
91
+ {#if enabledFeatures.length > 0}
92
+ <div class="card bg-base-100 shadow-sm">
93
+ <div class="card-body">
94
+ <h3 class="card-title text-sm text-success">
95
+ Enabled Features ({enabledFeatures.length})
96
+ </h3>
97
+ <div class="space-y-3">
98
+ {#each enabledFeatures as feature}
99
+ <div class="border-l-4 border-success pl-3">
100
+ <div class="flex items-center gap-2 mb-1">
101
+ <code class="text-sm font-semibold">{feature.featureId}</code>
102
+ {#if feature.winningDecision}
103
+ <div class="badge {getRuleBadgeClass(feature.winningDecision.rule)} badge-sm">
104
+ {feature.winningDecision.rule}
105
+ </div>
106
+ {/if}
107
+ </div>
108
+
109
+ {#if !compact}
110
+ <div class="text-sm text-base-content/80 whitespace-pre-wrap">
111
+ {feature.explanation}
112
+ </div>
113
+ {:else if feature.winningDecision}
114
+ <div class="text-xs text-base-content/70">
115
+ {feature.winningDecision.reason}
116
+ </div>
117
+ {/if}
118
+
119
+ {#if !compact && feature.allDecisions.length > 1}
120
+ <details class="mt-2">
121
+ <summary class="text-xs cursor-pointer text-base-content/60 hover:text-base-content">
122
+ Show all decisions ({feature.allDecisions.length})
123
+ </summary>
124
+ <div class="ml-4 mt-2 space-y-1">
125
+ {#each feature.allDecisions as decision}
126
+ <div class="text-xs flex items-center gap-2">
127
+ <span class="opacity-50">{decision.step}.</span>
128
+ <span class="badge badge-xs {getRuleBadgeClass(decision.rule)}">
129
+ {decision.rule}
130
+ </span>
131
+ <span>{getActionIcon(decision.action)}</span>
132
+ <span class="opacity-70">{decision.reason}</span>
133
+ </div>
134
+ {/each}
135
+ </div>
136
+ </details>
137
+ {/if}
138
+ </div>
139
+ {/each}
140
+ </div>
141
+ </div>
142
+ </div>
143
+ {/if}
144
+
145
+ <!-- Blocked Features -->
146
+ {#if blockedFeatures.length > 0}
147
+ <div class="card bg-base-100 shadow-sm">
148
+ <div class="card-body">
149
+ <h3 class="card-title text-sm text-error">
150
+ Blocked Features ({blockedFeatures.length})
151
+ </h3>
152
+ <div class="space-y-3">
153
+ {#each blockedFeatures as feature}
154
+ <div class="border-l-4 border-error pl-3">
155
+ <div class="flex items-center gap-2 mb-1">
156
+ <code class="text-sm font-semibold">{feature.featureId}</code>
157
+ {#if feature.winningDecision}
158
+ <div class="badge {getRuleBadgeClass(feature.winningDecision.rule)} badge-sm">
159
+ {feature.winningDecision.rule}
160
+ </div>
161
+ {/if}
162
+ </div>
163
+
164
+ {#if !compact}
165
+ <div class="text-sm text-base-content/80 whitespace-pre-wrap">
166
+ {feature.explanation}
167
+ </div>
168
+ {:else if feature.winningDecision}
169
+ <div class="text-xs text-base-content/70">
170
+ {feature.winningDecision.reason}
171
+ </div>
172
+ {/if}
173
+
174
+ {#if !compact && feature.allDecisions.length > 1}
175
+ <details class="mt-2">
176
+ <summary class="text-xs cursor-pointer text-base-content/60 hover:text-base-content">
177
+ Show all decisions ({feature.allDecisions.length})
178
+ </summary>
179
+ <div class="ml-4 mt-2 space-y-1">
180
+ {#each feature.allDecisions as decision}
181
+ <div class="text-xs flex items-center gap-2">
182
+ <span class="opacity-50">{decision.step}.</span>
183
+ <span class="badge badge-xs {getRuleBadgeClass(decision.rule)}">
184
+ {decision.rule}
185
+ </span>
186
+ <span>{getActionIcon(decision.action)}</span>
187
+ <span class="opacity-70">{decision.reason}</span>
188
+ </div>
189
+ {/each}
190
+ </div>
191
+ </details>
192
+ {/if}
193
+ </div>
194
+ {/each}
195
+ </div>
196
+ </div>
197
+ </div>
198
+ {/if}
199
+ </div>
200
+
201
+ <!-- Decision Log (collapsed by default) -->
202
+ {#if !compact && provenance.decisionLog.length > 0}
203
+ <details class="mt-4">
204
+ <summary class="cursor-pointer text-sm font-semibold text-base-content/80 hover:text-base-content">
205
+ Complete Decision Log ({provenance.decisionLog.length} steps)
206
+ </summary>
207
+ <div class="card bg-base-100 shadow-sm mt-2">
208
+ <div class="card-body">
209
+ <div class="space-y-2">
210
+ {#each provenance.decisionLog as decision}
211
+ <div class="flex items-start gap-3 text-xs">
212
+ <div class="font-mono text-base-content/50 w-8">
213
+ {decision.step}.
214
+ </div>
215
+ <div class="flex-1">
216
+ <div class="flex items-center gap-2 mb-1">
217
+ <span class="badge {getRuleBadgeClass(decision.rule)} badge-sm">
218
+ {decision.rule}
219
+ </span>
220
+ <span class="font-semibold">{decision.featureId}</span>
221
+ <span class={decision.action === 'enable' ? 'text-success' : decision.action === 'block' ? 'text-error' : 'text-base-content/50'}>
222
+ {getActionIcon(decision.action)} {decision.action}
223
+ </span>
224
+ </div>
225
+ <div class="text-base-content/70">
226
+ {decision.reason}
227
+ </div>
228
+ <div class="text-base-content/50 mt-1">
229
+ Source: {decision.source.name || decision.source.type}
230
+ </div>
231
+ </div>
232
+ </div>
233
+ {/each}
234
+ </div>
235
+ </div>
236
+ </div>
237
+ </details>
238
+ {/if}
239
+ </div>
240
+
241
+ <style>
242
+ .pnp-provenance-viewer {
243
+ font-family: system-ui, -apple-system, sans-serif;
244
+ }
245
+
246
+ .pnp-provenance-viewer code {
247
+ background-color: hsl(var(--b2));
248
+ padding: 0.125rem 0.375rem;
249
+ border-radius: 0.25rem;
250
+ font-size: 0.875em;
251
+ }
252
+
253
+ .pnp-provenance-viewer.compact {
254
+ font-size: 0.875rem;
255
+ }
256
+
257
+ .pnp-provenance-viewer.compact .card-body {
258
+ padding: 1rem;
259
+ }
260
+ </style>
@@ -15,6 +15,11 @@
15
15
  highlightCoordinator: { type: 'Object', reflect: false },
16
16
  scopeElement: { type: 'Object', reflect: false },
17
17
  elementToolStateStore: { type: 'Object', reflect: false },
18
+ toolRegistry: { type: 'Object', reflect: false },
19
+ pnpResolver: { type: 'Object', reflect: false },
20
+ assessment: { type: 'Object', reflect: false },
21
+ itemRef: { type: 'Object', reflect: false },
22
+ item: { type: 'Object', reflect: false },
18
23
 
19
24
  // IDs for element-level state (passed as JS properties, not attributes)
20
25
  assessmentId: { type: 'String', reflect: false },
@@ -26,11 +31,10 @@
26
31
  <!--
27
32
  QuestionToolBar - Inline toolbar for question/passage headers
28
33
 
29
- Displays tool buttons (TTS, Answer Eliminator, etc.) in a compact,
30
- horizontal layout suitable for embedding in question headers.
34
+ Now uses ToolButtonGroup with registry-based two-pass visibility model.
31
35
 
32
- Similar to the SchoolCity pattern where tool buttons appear next to
33
- question titles rather than in a global sidebar.
36
+ If toolRegistry is provided, uses new architecture.
37
+ Otherwise, falls back to legacy hardcoded buttons.
34
38
  -->
35
39
  <script lang="ts">
36
40
  import type {
@@ -38,13 +42,15 @@
38
42
  ITTSService,
39
43
  IHighlightCoordinator,
40
44
  } from '../services/interfaces';
41
-
42
- // NOTE: Tool web components must be imported by parent components
43
- // that use QuestionToolBar (to avoid circular dependencies).
44
- // Parent components should:
45
- // import '@pie-players/pie-tool-answer-eliminator';
46
- // import '@pie-players/pie-tool-tts-inline';
47
- // import '@pie-players/pie-tool-calculator-inline';
45
+ import type { ToolRegistry } from '../services/ToolRegistry';
46
+ import type { PNPToolResolver } from '../services/PNPToolResolver';
47
+ import type {
48
+ AssessmentEntity,
49
+ AssessmentItemRef,
50
+ ItemEntity,
51
+ } from '@pie-players/pie-players-shared/types';
52
+ import type { ItemToolContext } from '../services/tool-context';
53
+ import ToolButtonGroup from './ToolButtonGroup.svelte';
48
54
 
49
55
  const isBrowser = typeof window !== 'undefined';
50
56
 
@@ -75,12 +81,17 @@
75
81
  let {
76
82
  itemId = '',
77
83
  catalogId = '',
78
- tools = 'calculator,tts,answerEliminator',
84
+ tools = 'calculator,textToSpeech,answerEliminator',
79
85
  ttsService,
80
86
  toolCoordinator,
81
87
  highlightCoordinator,
82
88
  scopeElement = null,
83
89
  elementToolStateStore,
90
+ toolRegistry = null,
91
+ pnpResolver = null,
92
+ assessment = null,
93
+ itemRef = null,
94
+ item = null,
84
95
  assessmentId = '',
85
96
  sectionId = '',
86
97
  class: className = '',
@@ -95,6 +106,11 @@
95
106
  highlightCoordinator?: IHighlightCoordinator;
96
107
  scopeElement?: HTMLElement | null;
97
108
  elementToolStateStore?: any;
109
+ toolRegistry?: ToolRegistry | null;
110
+ pnpResolver?: PNPToolResolver | null;
111
+ assessment?: AssessmentEntity | null;
112
+ itemRef?: AssessmentItemRef | null;
113
+ item?: ItemEntity | null;
98
114
  assessmentId?: string;
99
115
  sectionId?: string;
100
116
  class?: string;
@@ -109,7 +125,46 @@
109
125
  // Note: Using itemId as elementId since toolbar is scoped to one item/element
110
126
  });
111
127
 
112
- // Parse enabled tools
128
+ // Determine if using new registry-based architecture
129
+ const useRegistryArchitecture = $derived(
130
+ !!(toolRegistry && pnpResolver && assessment && itemRef && item),
131
+ );
132
+
133
+ // NEW ARCHITECTURE: Registry-based rendering
134
+ const allowedToolIds = $derived.by(() => {
135
+ if (!useRegistryArchitecture || !pnpResolver || !assessment || !itemRef) {
136
+ return [];
137
+ }
138
+ return pnpResolver.getAllowedToolIds(assessment, itemRef);
139
+ });
140
+
141
+ const toolContext = $derived.by((): ItemToolContext | null => {
142
+ if (!useRegistryArchitecture || !assessment || !itemRef || !item) {
143
+ return null;
144
+ }
145
+ return {
146
+ level: 'item',
147
+ assessment,
148
+ itemRef,
149
+ item,
150
+ };
151
+ });
152
+
153
+ // Handle tool click (activates tool)
154
+ function handleToolClick(toolId: string) {
155
+ console.log('[QuestionToolBar] Tool clicked:', toolId);
156
+
157
+ if (!toolCoordinator) {
158
+ console.warn('[QuestionToolBar] No toolCoordinator available');
159
+ return;
160
+ }
161
+
162
+ // Toggle tool visibility
163
+ const fullToolId = `${toolId}-${itemId}`;
164
+ toolCoordinator.toggleTool(fullToolId);
165
+ }
166
+
167
+ // LEGACY ARCHITECTURE: Fallback to old hardcoded rendering
113
168
  let enabledTools = $derived(
114
169
  tools
115
170
  .split(',')
@@ -121,7 +176,7 @@
121
176
  let hasCompatibleChoices = $state(false);
122
177
 
123
178
  $effect(() => {
124
- if (!isBrowser || !scopeElement) {
179
+ if (!isBrowser || !scopeElement || useRegistryArchitecture) {
125
180
  hasCompatibleChoices = false;
126
181
  return;
127
182
  }
@@ -130,10 +185,6 @@
130
185
  const checkChoices = () => {
131
186
  const hasChoices = hasCompatibleAnswerChoices(scopeElement);
132
187
  hasCompatibleChoices = hasChoices;
133
- console.debug('[QuestionToolBar] Checked choices:', {
134
- scopeElementTag: scopeElement.tagName,
135
- hasCompatibleChoices
136
- });
137
188
  };
138
189
 
139
190
  // Initial check
@@ -152,14 +203,14 @@
152
203
  return () => observer.disconnect();
153
204
  });
154
205
 
155
- // Dynamically load tool components based on enabled tools
206
+ // Dynamically load tool components based on enabled tools (LEGACY)
156
207
  $effect(() => {
157
- if (!isBrowser || toolsLoaded) return;
208
+ if (!isBrowser || toolsLoaded || useRegistryArchitecture) return;
158
209
 
159
210
  (async () => {
160
211
  const loadPromises = [];
161
212
 
162
- if (enabledTools.includes('tts')) {
213
+ if (enabledTools.includes('tts') || enabledTools.includes('textToSpeech')) {
163
214
  loadPromises.push(import('@pie-players/pie-tool-tts-inline'));
164
215
  }
165
216
  if (enabledTools.includes('answerEliminator')) {
@@ -177,22 +228,33 @@
177
228
  })();
178
229
  });
179
230
 
180
- // Should show each tool
181
- let showTTS = $derived(enabledTools.includes('tts') && ttsService && toolsLoaded);
231
+ // Should show each tool (LEGACY)
232
+ let showTTS = $derived(
233
+ !useRegistryArchitecture &&
234
+ (enabledTools.includes('tts') || enabledTools.includes('textToSpeech')) &&
235
+ ttsService &&
236
+ toolsLoaded
237
+ );
182
238
  let showAnswerEliminator = $derived(
239
+ !useRegistryArchitecture &&
183
240
  enabledTools.includes('answerEliminator') &&
184
241
  toolCoordinator &&
185
242
  hasCompatibleChoices &&
186
243
  toolsLoaded
187
244
  );
188
- let showCalculator = $derived(enabledTools.includes('calculator') && toolCoordinator && toolsLoaded);
245
+ let showCalculator = $derived(
246
+ !useRegistryArchitecture &&
247
+ enabledTools.includes('calculator') &&
248
+ toolCoordinator &&
249
+ toolsLoaded
250
+ );
189
251
 
190
252
  // Track answer eliminator visibility state
191
253
  let answerEliminatorVisible = $state(false);
192
254
 
193
255
  // Subscribe to tool coordinator to update button state
194
256
  $effect(() => {
195
- if (!isBrowser || !toolCoordinator) return;
257
+ if (!isBrowser || !toolCoordinator || useRegistryArchitecture) return;
196
258
 
197
259
  const unsubscribe = toolCoordinator.subscribe(() => {
198
260
  answerEliminatorVisible = toolCoordinator.isToolVisible(
@@ -272,63 +334,78 @@
272
334
 
273
335
  {#if isBrowser}
274
336
  <div class="question-toolbar {className} question-toolbar--{size}">
275
- <!-- Calculator Button (inline tool) -->
276
- {#if showCalculator}
277
- <pie-tool-calculator-inline
278
- bind:this={calculatorInlineElement}
279
- tool-id="calculator-inline-{itemId}"
280
- calculator-type="scientific"
281
- available-types="basic,scientific,graphing"
282
- size={size}
283
- ></pie-tool-calculator-inline>
284
- {/if}
285
-
286
- <!-- TTS Button (inline tool) -->
287
- {#if showTTS}
288
- <pie-tool-tts-inline
289
- bind:this={ttsToolElement}
290
- tool-id="tts-{itemId}"
291
- catalog-id={catalogId || itemId}
292
- language={language}
293
- size={size}
294
- ></pie-tool-tts-inline>
295
- {/if}
296
-
297
- <!-- Answer Eliminator Toggle Button -->
298
- {#if showAnswerEliminator}
299
- <button
300
- type="button"
301
- class="question-toolbar__button"
302
- class:question-toolbar__button--active={answerEliminatorVisible}
303
- onclick={toggleAnswerEliminator}
304
- aria-label="Answer Eliminator"
305
- aria-pressed={answerEliminatorVisible}
306
- title="Toggle answer elimination mode"
307
- >
308
- <!-- X in Circle Icon (matches SchoolCity) -->
309
- <svg
310
- xmlns="http://www.w3.org/2000/svg"
311
- viewBox="0 0 24 24"
312
- width="20"
313
- height="20"
314
- fill="currentColor"
315
- aria-hidden="true"
337
+ {#if useRegistryArchitecture && toolRegistry && toolContext}
338
+ <!-- NEW ARCHITECTURE: Registry-based rendering -->
339
+ <ToolButtonGroup
340
+ {toolRegistry}
341
+ {allowedToolIds}
342
+ context={toolContext}
343
+ onToolClick={handleToolClick}
344
+ orientation="horizontal"
345
+ compact={size === 'sm'}
346
+ />
347
+ {:else}
348
+ <!-- LEGACY ARCHITECTURE: Hardcoded tool buttons -->
349
+ <!-- This will be removed in Phase 5 after migration complete -->
350
+
351
+ <!-- Calculator Button (inline tool) -->
352
+ {#if showCalculator}
353
+ <pie-tool-calculator-inline
354
+ bind:this={calculatorInlineElement}
355
+ tool-id="calculator-inline-{itemId}"
356
+ calculator-type="scientific"
357
+ available-types="basic,scientific,graphing"
358
+ size={size}
359
+ ></pie-tool-calculator-inline>
360
+ {/if}
361
+
362
+ <!-- TTS Button (inline tool) -->
363
+ {#if showTTS}
364
+ <pie-tool-tts-inline
365
+ bind:this={ttsToolElement}
366
+ tool-id="tts-{itemId}"
367
+ catalog-id={catalogId || itemId}
368
+ language={language}
369
+ size={size}
370
+ ></pie-tool-tts-inline>
371
+ {/if}
372
+
373
+ <!-- Answer Eliminator Toggle Button -->
374
+ {#if showAnswerEliminator}
375
+ <button
376
+ type="button"
377
+ class="question-toolbar__button"
378
+ class:question-toolbar__button--active={answerEliminatorVisible}
379
+ onclick={toggleAnswerEliminator}
380
+ aria-label="Answer Eliminator"
381
+ aria-pressed={answerEliminatorVisible}
382
+ title="Toggle answer elimination mode"
316
383
  >
317
- <path
318
- d="M19,3H16.3H7.7H5A2,2 0 0,0 3,5V7.7V16.4V19A2,2 0 0,0 5,21H7.7H16.4H19A2,2 0 0,0 21,19V16.3V7.7V5A2,2 0 0,0 19,3M15.6,17L12,13.4L8.4,17L7,15.6L10.6,12L7,8.4L8.4,7L12,10.6L15.6,7L17,8.4L13.4,12L17,15.6L15.6,17Z"
319
- />
320
- </svg>
321
- </button>
322
-
323
- <!-- Answer Eliminator Tool Instance (hidden, manages state) -->
324
- <pie-tool-answer-eliminator
325
- bind:this={answerEliminatorElement}
326
- visible={answerEliminatorVisible}
327
- tool-id="answerEliminator-{itemId}"
328
- strategy="strikethrough"
329
- button-alignment="inline"
330
- coordinator={toolCoordinator}
331
- ></pie-tool-answer-eliminator>
384
+ <!-- X in Circle Icon (matches SchoolCity) -->
385
+ <svg
386
+ xmlns="http://www.w3.org/2000/svg"
387
+ viewBox="0 0 24 24"
388
+ width="20"
389
+ height="20"
390
+ fill="currentColor"
391
+ aria-hidden="true"
392
+ >
393
+ <path
394
+ d="M19,3H16.3H7.7H5A2,2 0 0,0 3,5V7.7V16.4V19A2,2 0 0,0 5,21H7.7H16.4H19A2,2 0 0,0 21,19V16.3V7.7V5A2,2 0 0,0 19,3M15.6,17L12,13.4L8.4,17L7,15.6L10.6,12L7,8.4L8.4,7L12,10.6L15.6,7L17,8.4L13.4,12L17,15.6L15.6,17Z"
395
+ />
396
+ </svg>
397
+ </button>
398
+
399
+ <!-- Answer Eliminator Tool Instance (hidden, manages state) -->
400
+ <pie-tool-answer-eliminator
401
+ bind:this={answerEliminatorElement}
402
+ visible={answerEliminatorVisible}
403
+ tool-id="answerEliminator-{itemId}"
404
+ strategy="strikethrough"
405
+ button-alignment="inline"
406
+ coordinator={toolCoordinator}
407
+ ></pie-tool-answer-eliminator>
408
+ {/if}
332
409
  {/if}
333
410
  </div>
334
411
  {/if}
@@ -356,8 +433,8 @@
356
433
  }
357
434
 
358
435
  .question-toolbar--sm .question-toolbar__button {
359
- width: 1.5rem;
360
- height: 1.5rem;
436
+ width: 2.75rem; /* 44px - WCAG 2.5.2 Level A minimum */
437
+ height: 2.75rem;
361
438
  }
362
439
 
363
440
  .question-toolbar--lg .question-toolbar__button {