@pie-players/pie-assessment-toolkit 0.2.3 → 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.
- package/README.md +159 -1
- package/dist/index.d.ts +16 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -2
- package/dist/index.js.map +1 -1
- package/dist/player/AssessmentPlayer.d.ts +9 -0
- package/dist/player/AssessmentPlayer.d.ts.map +1 -1
- package/dist/player/AssessmentPlayer.js +13 -4
- package/dist/player/AssessmentPlayer.js.map +1 -1
- package/dist/services/AnnotationToolbarConfig.d.ts +134 -0
- package/dist/services/AnnotationToolbarConfig.d.ts.map +1 -0
- package/dist/services/AnnotationToolbarConfig.js +100 -0
- package/dist/services/AnnotationToolbarConfig.js.map +1 -0
- package/dist/services/HighlightCoordinator.d.ts +23 -10
- package/dist/services/HighlightCoordinator.d.ts.map +1 -1
- package/dist/services/HighlightCoordinator.js +87 -40
- package/dist/services/HighlightCoordinator.js.map +1 -1
- package/dist/services/PNPToolResolver.d.ts +171 -15
- package/dist/services/PNPToolResolver.d.ts.map +1 -1
- package/dist/services/PNPToolResolver.js +324 -32
- package/dist/services/PNPToolResolver.js.map +1 -1
- package/dist/services/RangeSerializer.d.ts +119 -0
- package/dist/services/RangeSerializer.d.ts.map +1 -0
- package/dist/services/RangeSerializer.js +199 -0
- package/dist/services/RangeSerializer.js.map +1 -0
- package/dist/services/TTSService.d.ts +4 -1
- package/dist/services/TTSService.d.ts.map +1 -1
- package/dist/services/TTSService.js +58 -3
- package/dist/services/TTSService.js.map +1 -1
- package/dist/services/ToolRegistry.d.ts +225 -0
- package/dist/services/ToolRegistry.d.ts.map +1 -0
- package/dist/services/ToolRegistry.js +248 -0
- package/dist/services/ToolRegistry.js.map +1 -0
- package/dist/services/ToolkitCoordinator.d.ts +22 -0
- package/dist/services/ToolkitCoordinator.d.ts.map +1 -1
- package/dist/services/ToolkitCoordinator.js +44 -0
- package/dist/services/ToolkitCoordinator.js.map +1 -1
- package/dist/services/createDefaultToolRegistry.d.ts +72 -0
- package/dist/services/createDefaultToolRegistry.d.ts.map +1 -0
- package/dist/services/createDefaultToolRegistry.js +147 -0
- package/dist/services/createDefaultToolRegistry.js.map +1 -0
- package/dist/services/interfaces.d.ts +8 -0
- package/dist/services/interfaces.d.ts.map +1 -1
- package/dist/services/interfaces.js.map +1 -1
- package/dist/services/pnp-provenance.d.ts +167 -0
- package/dist/services/pnp-provenance.d.ts.map +1 -0
- package/dist/services/pnp-provenance.js +230 -0
- package/dist/services/pnp-provenance.js.map +1 -0
- package/dist/services/pnp-standard-features.d.ts +240 -0
- package/dist/services/pnp-standard-features.d.ts.map +1 -0
- package/dist/services/pnp-standard-features.js +342 -0
- package/dist/services/pnp-standard-features.js.map +1 -0
- package/dist/services/tool-context.d.ts +150 -0
- package/dist/services/tool-context.d.ts.map +1 -0
- package/dist/services/tool-context.js +157 -0
- package/dist/services/tool-context.js.map +1 -0
- package/dist/services/tool-providers/ToolProviderRegistry.d.ts.map +1 -1
- package/dist/services/tool-providers/ToolProviderRegistry.js +7 -7
- package/dist/services/tool-providers/ToolProviderRegistry.js.map +1 -1
- package/dist/tools/registrations/accessibility-tools.d.ts +43 -0
- package/dist/tools/registrations/accessibility-tools.d.ts.map +1 -0
- package/dist/tools/registrations/accessibility-tools.js +230 -0
- package/dist/tools/registrations/accessibility-tools.js.map +1 -0
- package/dist/tools/registrations/calculator.d.ts +21 -0
- package/dist/tools/registrations/calculator.d.ts.map +1 -0
- package/dist/tools/registrations/calculator.js +98 -0
- package/dist/tools/registrations/calculator.js.map +1 -0
- package/dist/tools/registrations/interaction-tools.d.ts +28 -0
- package/dist/tools/registrations/interaction-tools.d.ts.map +1 -0
- package/dist/tools/registrations/interaction-tools.js +124 -0
- package/dist/tools/registrations/interaction-tools.js.map +1 -0
- package/dist/tools/registrations/measurement-tools.d.ts +25 -0
- package/dist/tools/registrations/measurement-tools.d.ts.map +1 -0
- package/dist/tools/registrations/measurement-tools.js +112 -0
- package/dist/tools/registrations/measurement-tools.js.map +1 -0
- package/dist/tools/registrations/subject-specific-tools.d.ts +28 -0
- package/dist/tools/registrations/subject-specific-tools.d.ts.map +1 -0
- package/dist/tools/registrations/subject-specific-tools.js +118 -0
- package/dist/tools/registrations/subject-specific-tools.js.map +1 -0
- package/dist/tools/registrations/tts.d.ts +20 -0
- package/dist/tools/registrations/tts.d.ts.map +1 -0
- package/dist/tools/registrations/tts.js +86 -0
- package/dist/tools/registrations/tts.js.map +1 -0
- package/dist/tools/tool-tag-map.d.ts +24 -0
- package/dist/tools/tool-tag-map.d.ts.map +1 -0
- package/dist/tools/tool-tag-map.js +38 -0
- package/dist/tools/tool-tag-map.js.map +1 -0
- package/package.json +15 -7
- package/src/README.md +25 -11
- package/src/components/PNPProfileTester.example.svelte +188 -0
- package/src/components/PNPProfileTester.svelte +397 -0
- package/src/components/PNPProvenanceViewer.svelte +260 -0
- package/src/components/QuestionToolBar.svelte +160 -83
- package/src/components/ToolButton.svelte +146 -0
- package/src/components/ToolButtonGroup.svelte +109 -0
- package/src/reference-layout/ReferenceLayout.svelte +5 -13
- package/dist/services/PNPMapper.d.ts +0 -71
- package/dist/services/PNPMapper.d.ts.map +0 -1
- package/dist/services/PNPMapper.js +0 -98
- package/dist/services/PNPMapper.js.map +0 -1
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Example usage of PNPProfileTester component
|
|
4
|
+
*
|
|
5
|
+
* This demonstrates how to use the PNP Profile Tester for development and testing.
|
|
6
|
+
* Shows integration with PNPToolResolver and PNPProvenanceViewer.
|
|
7
|
+
*/
|
|
8
|
+
import PNPProfileTester from './PNPProfileTester.svelte';
|
|
9
|
+
import PNPProvenanceViewer from './PNPProvenanceViewer.svelte';
|
|
10
|
+
import { PNPToolResolver } from '../services/PNPToolResolver';
|
|
11
|
+
import { createDefaultToolRegistry } from '../services/createDefaultToolRegistry';
|
|
12
|
+
import type { PersonalNeedsProfile } from '@pie-players/pie-players-shared/types';
|
|
13
|
+
import type { PNPResolutionProvenance } from '../services/pnp-provenance';
|
|
14
|
+
|
|
15
|
+
// Mock assessment for testing
|
|
16
|
+
const mockAssessment = {
|
|
17
|
+
id: 'test-assessment-1',
|
|
18
|
+
name: 'Example Assessment',
|
|
19
|
+
personalNeedsProfile: undefined as PersonalNeedsProfile | undefined,
|
|
20
|
+
settings: {
|
|
21
|
+
// Example district policy
|
|
22
|
+
districtPolicy: {
|
|
23
|
+
blockedTools: [], // No blocks in this example
|
|
24
|
+
requiredTools: [], // No requirements in this example
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Create resolver with default tool registry
|
|
30
|
+
const toolRegistry = createDefaultToolRegistry();
|
|
31
|
+
const resolver = new PNPToolResolver(toolRegistry, true);
|
|
32
|
+
|
|
33
|
+
// State
|
|
34
|
+
let currentProfile = $state<PersonalNeedsProfile | null>(null);
|
|
35
|
+
let resolutionResult = $state<{
|
|
36
|
+
toolIds: string[];
|
|
37
|
+
provenance: PNPResolutionProvenance;
|
|
38
|
+
} | null>(null);
|
|
39
|
+
let showProvenance = $state(true);
|
|
40
|
+
|
|
41
|
+
// Update resolution when profile changes
|
|
42
|
+
function handleProfileChange(profile: PersonalNeedsProfile | null) {
|
|
43
|
+
currentProfile = profile;
|
|
44
|
+
|
|
45
|
+
// Use the override method to test with this profile
|
|
46
|
+
const result = resolver.resolveWithOverride(mockAssessment as any, profile);
|
|
47
|
+
|
|
48
|
+
resolutionResult = {
|
|
49
|
+
toolIds: result.tools.filter((t) => t.enabled).map((t) => t.id),
|
|
50
|
+
provenance: result.provenance,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
</script>
|
|
54
|
+
|
|
55
|
+
<div class="container mx-auto p-4 max-w-6xl">
|
|
56
|
+
<div class="prose mb-6">
|
|
57
|
+
<h1>PNP Profile Tester - Example</h1>
|
|
58
|
+
<p>
|
|
59
|
+
This component allows developers and testers to experiment with different PNP
|
|
60
|
+
(Personal Needs Profile) configurations in real-time.
|
|
61
|
+
</p>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
|
65
|
+
<!-- Left: Profile Tester -->
|
|
66
|
+
<div>
|
|
67
|
+
<PNPProfileTester onProfileChange={handleProfileChange} />
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<!-- Right: Resolution Results -->
|
|
71
|
+
<div class="space-y-4">
|
|
72
|
+
<!-- Enabled Tools -->
|
|
73
|
+
<div class="card bg-base-100 shadow-sm">
|
|
74
|
+
<div class="card-body">
|
|
75
|
+
<h3 class="card-title text-sm">Resolved Tools</h3>
|
|
76
|
+
{#if resolutionResult && resolutionResult.toolIds.length > 0}
|
|
77
|
+
<div class="space-y-2">
|
|
78
|
+
{#each resolutionResult.toolIds as toolId}
|
|
79
|
+
<div class="badge badge-primary">{toolId}</div>
|
|
80
|
+
{/each}
|
|
81
|
+
</div>
|
|
82
|
+
{:else}
|
|
83
|
+
<p class="text-sm text-base-content/50">
|
|
84
|
+
No tools enabled. Select a profile or add features to see results.
|
|
85
|
+
</p>
|
|
86
|
+
{/if}
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<!-- Profile JSON -->
|
|
91
|
+
<div class="card bg-base-100 shadow-sm">
|
|
92
|
+
<div class="card-body">
|
|
93
|
+
<h3 class="card-title text-sm">Current Profile JSON</h3>
|
|
94
|
+
<div class="mockup-code">
|
|
95
|
+
<pre><code>{JSON.stringify(currentProfile, null, 2)}</code></pre>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<!-- Provenance Toggle -->
|
|
101
|
+
<div class="form-control">
|
|
102
|
+
<label class="label cursor-pointer">
|
|
103
|
+
<span class="label-text">Show Provenance</span>
|
|
104
|
+
<input
|
|
105
|
+
type="checkbox"
|
|
106
|
+
class="toggle toggle-primary"
|
|
107
|
+
bind:checked={showProvenance}
|
|
108
|
+
/>
|
|
109
|
+
</label>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<!-- Full-width Provenance Viewer -->
|
|
115
|
+
{#if showProvenance && resolutionResult}
|
|
116
|
+
<div class="mt-6">
|
|
117
|
+
<PNPProvenanceViewer provenance={resolutionResult.provenance} />
|
|
118
|
+
</div>
|
|
119
|
+
{/if}
|
|
120
|
+
|
|
121
|
+
<!-- Usage Examples -->
|
|
122
|
+
<div class="prose mt-8">
|
|
123
|
+
<h2>Integration Examples</h2>
|
|
124
|
+
|
|
125
|
+
<h3>Basic Usage</h3>
|
|
126
|
+
<pre><code>{`<script>
|
|
127
|
+
import { PNPProfileTester } from '@pie-players/pie-assessment-toolkit/components';
|
|
128
|
+
|
|
129
|
+
let currentProfile;
|
|
130
|
+
</script>
|
|
131
|
+
|
|
132
|
+
<PNPProfileTester
|
|
133
|
+
onProfileChange={(profile) => {
|
|
134
|
+
currentProfile = profile;
|
|
135
|
+
// Apply to your assessment player
|
|
136
|
+
}}
|
|
137
|
+
/>`}</code></pre>
|
|
138
|
+
|
|
139
|
+
<h3>With PNPToolResolver Override</h3>
|
|
140
|
+
<pre><code>{`<script>
|
|
141
|
+
import { PNPProfileTester } from '@pie-players/pie-assessment-toolkit/components';
|
|
142
|
+
import { PNPToolResolver, createDefaultToolRegistry } from '@pie-players/pie-assessment-toolkit';
|
|
143
|
+
|
|
144
|
+
const registry = createDefaultToolRegistry();
|
|
145
|
+
const resolver = new PNPToolResolver(registry);
|
|
146
|
+
|
|
147
|
+
function handleProfileChange(profile) {
|
|
148
|
+
// Test with override profile
|
|
149
|
+
const result = resolver.resolveWithOverride(assessment, profile);
|
|
150
|
+
|
|
151
|
+
// Use resolved tools
|
|
152
|
+
const toolIds = result.tools.filter(t => t.enabled).map(t => t.id);
|
|
153
|
+
|
|
154
|
+
// Show provenance
|
|
155
|
+
console.log(result.provenance);
|
|
156
|
+
}
|
|
157
|
+
</script>
|
|
158
|
+
|
|
159
|
+
<PNPProfileTester onProfileChange={handleProfileChange} />`}</code></pre>
|
|
160
|
+
|
|
161
|
+
<h3>Compact Mode in Sidebar</h3>
|
|
162
|
+
<pre><code>{`<PNPProfileTester compact={true} onProfileChange={handleProfileChange} />`}</code></pre>
|
|
163
|
+
|
|
164
|
+
<h3>Without Custom Features</h3>
|
|
165
|
+
<pre><code>{`<PNPProfileTester
|
|
166
|
+
allowCustomFeatures={false}
|
|
167
|
+
onProfileChange={handleProfileChange}
|
|
168
|
+
/>`}</code></pre>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
<style>
|
|
173
|
+
.prose {
|
|
174
|
+
max-width: 100%;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.prose code {
|
|
178
|
+
background-color: hsl(var(--b2));
|
|
179
|
+
padding: 0.125rem 0.375rem;
|
|
180
|
+
border-radius: 0.25rem;
|
|
181
|
+
font-size: 0.875em;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.prose pre code {
|
|
185
|
+
background-color: transparent;
|
|
186
|
+
padding: 0;
|
|
187
|
+
}
|
|
188
|
+
</style>
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// PNP Profile Tester Component
|
|
3
|
+
// Development and testing tool for overriding PNP profiles in real-time.
|
|
4
|
+
// See PNP_TESTING.md for usage documentation.
|
|
5
|
+
import type { PersonalNeedsProfile } from '@pie-players/pie-players-shared/types';
|
|
6
|
+
import {
|
|
7
|
+
EXAMPLE_PNP_CONFIGURATIONS,
|
|
8
|
+
QTI_STANDARD_ACCESS_FEATURES,
|
|
9
|
+
getFeatureCategory,
|
|
10
|
+
} from '../services/pnp-standard-features';
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
/** Callback when profile changes */
|
|
14
|
+
onProfileChange?: (profile: PersonalNeedsProfile | null) => void;
|
|
15
|
+
/** Initial profile (optional) */
|
|
16
|
+
initialProfile?: PersonalNeedsProfile | null;
|
|
17
|
+
/** Show compact view */
|
|
18
|
+
compact?: boolean;
|
|
19
|
+
/** Allow custom feature entry */
|
|
20
|
+
allowCustomFeatures?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let {
|
|
24
|
+
onProfileChange = () => {},
|
|
25
|
+
initialProfile = null,
|
|
26
|
+
compact = false,
|
|
27
|
+
allowCustomFeatures = true
|
|
28
|
+
}: Props = $props();
|
|
29
|
+
|
|
30
|
+
// Default profile with all tools enabled
|
|
31
|
+
const DEFAULT_ALL_TOOLS_PROFILE = [
|
|
32
|
+
'calculator',
|
|
33
|
+
'calculatorScientific',
|
|
34
|
+
'calculatorGraphing',
|
|
35
|
+
'graphingTool',
|
|
36
|
+
'periodicTable',
|
|
37
|
+
'protractor',
|
|
38
|
+
'ruler',
|
|
39
|
+
'readingGuide',
|
|
40
|
+
'magnification',
|
|
41
|
+
'screenMagnifier',
|
|
42
|
+
'textToSpeech',
|
|
43
|
+
'answerEliminator'
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
// Current state - initialize with default profile if no initialProfile provided
|
|
47
|
+
let activeFeatures = $state<Set<string>>(
|
|
48
|
+
new Set(initialProfile?.supports || DEFAULT_ALL_TOOLS_PROFILE),
|
|
49
|
+
);
|
|
50
|
+
let selectedPreset = $state<string | null>(initialProfile ? null : 'default');
|
|
51
|
+
let customFeature = $state('');
|
|
52
|
+
let showCategoryFilter = $state<string | null>(null);
|
|
53
|
+
let searchQuery = $state('');
|
|
54
|
+
let statusMessage = $state('');
|
|
55
|
+
|
|
56
|
+
// Computed profile
|
|
57
|
+
$effect(() => {
|
|
58
|
+
const profile: PersonalNeedsProfile = {
|
|
59
|
+
supports: Array.from(activeFeatures),
|
|
60
|
+
};
|
|
61
|
+
onProfileChange(activeFeatures.size > 0 ? profile : null);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Get all standard features as array
|
|
65
|
+
const allStandardFeatures = Object.entries(QTI_STANDARD_ACCESS_FEATURES).flatMap(
|
|
66
|
+
([category, features]) =>
|
|
67
|
+
Object.entries(features).map(([key, value]) => ({
|
|
68
|
+
id: value,
|
|
69
|
+
name: key,
|
|
70
|
+
category,
|
|
71
|
+
})),
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
// Filter features
|
|
75
|
+
let filteredFeatures = $derived.by(() => {
|
|
76
|
+
let filtered = allStandardFeatures;
|
|
77
|
+
|
|
78
|
+
if (showCategoryFilter) {
|
|
79
|
+
filtered = filtered.filter((f) => f.category === showCategoryFilter);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (searchQuery) {
|
|
83
|
+
const query = searchQuery.toLowerCase();
|
|
84
|
+
filtered = filtered.filter(
|
|
85
|
+
(f) =>
|
|
86
|
+
f.id.toLowerCase().includes(query) ||
|
|
87
|
+
f.name.toLowerCase().includes(query),
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return filtered;
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
function applyPreset(presetKey: string) {
|
|
95
|
+
const preset = EXAMPLE_PNP_CONFIGURATIONS[presetKey as keyof typeof EXAMPLE_PNP_CONFIGURATIONS];
|
|
96
|
+
if (!preset) return;
|
|
97
|
+
|
|
98
|
+
selectedPreset = presetKey;
|
|
99
|
+
// Create a new Set to trigger Svelte 5 reactivity
|
|
100
|
+
activeFeatures = new Set(preset.features);
|
|
101
|
+
statusMessage = `Profile changed to ${preset.label.replace('Example: ', '')}. ${preset.features.length} features enabled.`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function toggleFeature(featureId: string) {
|
|
105
|
+
// Create a new Set to trigger Svelte 5 reactivity
|
|
106
|
+
const newFeatures = new Set(activeFeatures);
|
|
107
|
+
const wasEnabled = newFeatures.has(featureId);
|
|
108
|
+
if (wasEnabled) {
|
|
109
|
+
newFeatures.delete(featureId);
|
|
110
|
+
} else {
|
|
111
|
+
newFeatures.add(featureId);
|
|
112
|
+
}
|
|
113
|
+
activeFeatures = newFeatures;
|
|
114
|
+
selectedPreset = null; // Clear preset when manually changing
|
|
115
|
+
statusMessage = `${featureId} ${wasEnabled ? 'disabled' : 'enabled'}. ${activeFeatures.size} features active.`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function addCustomFeature() {
|
|
119
|
+
if (customFeature.trim()) {
|
|
120
|
+
const newFeatures = new Set(activeFeatures);
|
|
121
|
+
const featureId = customFeature.trim();
|
|
122
|
+
newFeatures.add(featureId);
|
|
123
|
+
activeFeatures = newFeatures;
|
|
124
|
+
customFeature = '';
|
|
125
|
+
selectedPreset = null;
|
|
126
|
+
statusMessage = `Custom feature ${featureId} added. ${activeFeatures.size} features active.`;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function clearAll() {
|
|
131
|
+
activeFeatures = new Set();
|
|
132
|
+
selectedPreset = null;
|
|
133
|
+
statusMessage = 'All features cleared.';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function exportProfile() {
|
|
137
|
+
const profile: PersonalNeedsProfile = {
|
|
138
|
+
supports: Array.from(activeFeatures),
|
|
139
|
+
};
|
|
140
|
+
const json = JSON.stringify(profile, null, 2);
|
|
141
|
+
const blob = new Blob([json], { type: 'application/json' });
|
|
142
|
+
const url = URL.createObjectURL(blob);
|
|
143
|
+
const a = document.createElement('a');
|
|
144
|
+
a.href = url;
|
|
145
|
+
a.download = 'pnp-profile.json';
|
|
146
|
+
a.click();
|
|
147
|
+
URL.revokeObjectURL(url);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function importProfile(event: Event) {
|
|
151
|
+
const input = event.target as HTMLInputElement;
|
|
152
|
+
const file = input.files?.[0];
|
|
153
|
+
if (!file) return;
|
|
154
|
+
|
|
155
|
+
const reader = new FileReader();
|
|
156
|
+
reader.onload = (e) => {
|
|
157
|
+
try {
|
|
158
|
+
const profile = JSON.parse(e.target?.result as string) as PersonalNeedsProfile;
|
|
159
|
+
if (profile.supports && Array.isArray(profile.supports)) {
|
|
160
|
+
activeFeatures = new Set(profile.supports);
|
|
161
|
+
selectedPreset = null;
|
|
162
|
+
}
|
|
163
|
+
} catch (err) {
|
|
164
|
+
console.error('Failed to import PNP profile:', err);
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
reader.readAsText(file);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const categories = Object.keys(QTI_STANDARD_ACCESS_FEATURES);
|
|
171
|
+
</script>
|
|
172
|
+
|
|
173
|
+
<div class="pnp-profile-tester" class:compact>
|
|
174
|
+
<!-- Header -->
|
|
175
|
+
<div class="flex items-center gap-2 mb-3">
|
|
176
|
+
<h3 class="text-sm font-semibold">PNP Profile Tester</h3>
|
|
177
|
+
<div class="badge badge-neutral badge-sm">
|
|
178
|
+
{activeFeatures.size} feature{activeFeatures.size !== 1 ? 's' : ''}
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
<!-- Preset Selector and Search Row -->
|
|
183
|
+
<div class="grid grid-cols-1 {compact ? '' : 'md:grid-cols-2'} gap-2 mb-3">
|
|
184
|
+
<!-- Preset Selector -->
|
|
185
|
+
<div class="form-control">
|
|
186
|
+
<label for="preset-selector" class="label label-text text-xs font-semibold py-1">
|
|
187
|
+
Example Profiles
|
|
188
|
+
</label>
|
|
189
|
+
<select
|
|
190
|
+
id="preset-selector"
|
|
191
|
+
class="select select-sm select-bordered w-full"
|
|
192
|
+
value={selectedPreset || ''}
|
|
193
|
+
aria-label="Select an example accessibility profile"
|
|
194
|
+
onchange={(e) => {
|
|
195
|
+
const value = (e.target as HTMLSelectElement).value;
|
|
196
|
+
if (value === 'default') {
|
|
197
|
+
activeFeatures = new Set(DEFAULT_ALL_TOOLS_PROFILE);
|
|
198
|
+
selectedPreset = 'default';
|
|
199
|
+
statusMessage = `Default profile applied. ${activeFeatures.size} features enabled.`;
|
|
200
|
+
} else if (value === '') {
|
|
201
|
+
clearAll();
|
|
202
|
+
} else {
|
|
203
|
+
applyPreset(value);
|
|
204
|
+
}
|
|
205
|
+
}}
|
|
206
|
+
>
|
|
207
|
+
<option value="default">Default - All Tools</option>
|
|
208
|
+
<option value="">None (Clear All)</option>
|
|
209
|
+
<optgroup label="Example Profiles">
|
|
210
|
+
{#each Object.entries(EXAMPLE_PNP_CONFIGURATIONS) as [key, preset]}
|
|
211
|
+
<option value={key}>{preset.label.replace('Example: ', '')}</option>
|
|
212
|
+
{/each}
|
|
213
|
+
</optgroup>
|
|
214
|
+
</select>
|
|
215
|
+
</div>
|
|
216
|
+
|
|
217
|
+
<!-- Search -->
|
|
218
|
+
{#if !compact}
|
|
219
|
+
<div class="form-control">
|
|
220
|
+
<label for="feature-search" class="label label-text text-xs font-semibold py-1">
|
|
221
|
+
Search Features
|
|
222
|
+
</label>
|
|
223
|
+
<input
|
|
224
|
+
id="feature-search"
|
|
225
|
+
type="text"
|
|
226
|
+
placeholder="Search features..."
|
|
227
|
+
class="input input-bordered input-sm w-full"
|
|
228
|
+
bind:value={searchQuery}
|
|
229
|
+
aria-label="Search accessibility features by name or ID"
|
|
230
|
+
/>
|
|
231
|
+
</div>
|
|
232
|
+
{/if}
|
|
233
|
+
</div>
|
|
234
|
+
|
|
235
|
+
<!-- Category Filter -->
|
|
236
|
+
{#if !compact}
|
|
237
|
+
<div class="mb-3">
|
|
238
|
+
<div class="text-xs font-semibold mb-2">Filter by Category</div>
|
|
239
|
+
<div class="flex flex-wrap gap-1">
|
|
240
|
+
<button
|
|
241
|
+
class="btn btn-xs"
|
|
242
|
+
class:btn-primary={showCategoryFilter === null}
|
|
243
|
+
aria-pressed={showCategoryFilter === null}
|
|
244
|
+
onclick={() => (showCategoryFilter = null)}
|
|
245
|
+
>
|
|
246
|
+
All
|
|
247
|
+
</button>
|
|
248
|
+
{#each categories as category}
|
|
249
|
+
<button
|
|
250
|
+
class="btn btn-xs"
|
|
251
|
+
class:btn-primary={showCategoryFilter === category}
|
|
252
|
+
aria-pressed={showCategoryFilter === category}
|
|
253
|
+
onclick={() =>
|
|
254
|
+
(showCategoryFilter = showCategoryFilter === category ? null : category)}
|
|
255
|
+
>
|
|
256
|
+
{category}
|
|
257
|
+
</button>
|
|
258
|
+
{/each}
|
|
259
|
+
</div>
|
|
260
|
+
</div>
|
|
261
|
+
{/if}
|
|
262
|
+
|
|
263
|
+
<!-- Feature Toggles -->
|
|
264
|
+
<div class="border border-base-300 rounded-lg p-3 mb-3 bg-base-100">
|
|
265
|
+
<div class="text-xs font-semibold mb-2">
|
|
266
|
+
Standard Features
|
|
267
|
+
{#if filteredFeatures.length !== allStandardFeatures.length}
|
|
268
|
+
<span class="text-base-content/60 font-normal">
|
|
269
|
+
(showing {filteredFeatures.length} of {allStandardFeatures.length})
|
|
270
|
+
</span>
|
|
271
|
+
{/if}
|
|
272
|
+
</div>
|
|
273
|
+
<div class="max-h-80 overflow-y-auto space-y-0.5 pr-2">
|
|
274
|
+
{#each filteredFeatures as feature}
|
|
275
|
+
<label class="flex items-center gap-2 cursor-pointer hover:bg-base-200 p-2 rounded transition-colors">
|
|
276
|
+
<input
|
|
277
|
+
type="checkbox"
|
|
278
|
+
class="checkbox checkbox-sm"
|
|
279
|
+
checked={activeFeatures.has(feature.id)}
|
|
280
|
+
onchange={() => toggleFeature(feature.id)}
|
|
281
|
+
/>
|
|
282
|
+
<span class="text-sm flex-1">{feature.name}</span>
|
|
283
|
+
{#if !compact}
|
|
284
|
+
<span class="badge badge-xs badge-ghost">{feature.category}</span>
|
|
285
|
+
{/if}
|
|
286
|
+
</label>
|
|
287
|
+
{/each}
|
|
288
|
+
{#if filteredFeatures.length === 0}
|
|
289
|
+
<div class="text-center text-sm text-base-content/60 py-4">
|
|
290
|
+
No features match your search
|
|
291
|
+
</div>
|
|
292
|
+
{/if}
|
|
293
|
+
</div>
|
|
294
|
+
</div>
|
|
295
|
+
|
|
296
|
+
<!-- Custom Features -->
|
|
297
|
+
{#if allowCustomFeatures}
|
|
298
|
+
<div class="border border-base-300 rounded-lg p-3 mb-3 bg-base-100">
|
|
299
|
+
<div class="text-xs font-semibold mb-2">Custom Features</div>
|
|
300
|
+
<div class="flex gap-2 mb-2">
|
|
301
|
+
<label for="custom-feature-input" class="sr-only">Custom feature ID</label>
|
|
302
|
+
<input
|
|
303
|
+
id="custom-feature-input"
|
|
304
|
+
type="text"
|
|
305
|
+
placeholder="e.g., ext:custom-feature"
|
|
306
|
+
class="input input-bordered input-sm flex-1"
|
|
307
|
+
bind:value={customFeature}
|
|
308
|
+
aria-label="Enter custom feature ID"
|
|
309
|
+
onkeydown={(e) => e.key === 'Enter' && addCustomFeature()}
|
|
310
|
+
/>
|
|
311
|
+
<button class="btn btn-sm btn-primary" onclick={addCustomFeature}> Add </button>
|
|
312
|
+
</div>
|
|
313
|
+
|
|
314
|
+
{#if Array.from(activeFeatures).some((f) => !allStandardFeatures.find((sf) => sf.id === f))}
|
|
315
|
+
<div class="space-y-1 mt-2">
|
|
316
|
+
{#each Array.from(activeFeatures).filter((f) => !allStandardFeatures.find((sf) => sf.id === f)) as feature}
|
|
317
|
+
<div
|
|
318
|
+
class="flex items-center gap-2 text-sm bg-warning/10 p-2 rounded border border-warning/20"
|
|
319
|
+
>
|
|
320
|
+
<span class="flex-1 font-mono text-xs">{feature}</span>
|
|
321
|
+
<button
|
|
322
|
+
class="btn btn-xs btn-circle btn-ghost"
|
|
323
|
+
onclick={() => toggleFeature(feature)}
|
|
324
|
+
aria-label="Remove custom feature {feature}"
|
|
325
|
+
>
|
|
326
|
+
✕
|
|
327
|
+
</button>
|
|
328
|
+
</div>
|
|
329
|
+
{/each}
|
|
330
|
+
</div>
|
|
331
|
+
{:else}
|
|
332
|
+
<div class="text-xs text-base-content/60 mt-2">
|
|
333
|
+
Add non-standard QTI features for testing
|
|
334
|
+
</div>
|
|
335
|
+
{/if}
|
|
336
|
+
</div>
|
|
337
|
+
{/if}
|
|
338
|
+
|
|
339
|
+
<!-- Active Features Summary -->
|
|
340
|
+
{#if !compact && activeFeatures.size > 0}
|
|
341
|
+
<details class="mt-3">
|
|
342
|
+
<summary class="cursor-pointer text-xs font-semibold text-base-content/70">
|
|
343
|
+
Active Features ({activeFeatures.size})
|
|
344
|
+
</summary>
|
|
345
|
+
<div class="mt-2 p-2 bg-base-200 rounded text-xs">
|
|
346
|
+
<code class="whitespace-pre-wrap">
|
|
347
|
+
{JSON.stringify({ supports: Array.from(activeFeatures) }, null, 2)}
|
|
348
|
+
</code>
|
|
349
|
+
</div>
|
|
350
|
+
</details>
|
|
351
|
+
{/if}
|
|
352
|
+
|
|
353
|
+
<!-- Live region for status announcements -->
|
|
354
|
+
<div role="status" aria-live="polite" aria-atomic="true" class="sr-only">
|
|
355
|
+
{statusMessage}
|
|
356
|
+
</div>
|
|
357
|
+
</div>
|
|
358
|
+
|
|
359
|
+
<style>
|
|
360
|
+
.pnp-profile-tester {
|
|
361
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.pnp-profile-tester.compact {
|
|
365
|
+
font-size: 0.875rem;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/* Custom scrollbar for feature list */
|
|
369
|
+
.pnp-profile-tester .max-h-80::-webkit-scrollbar {
|
|
370
|
+
width: 8px;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
.pnp-profile-tester .max-h-80::-webkit-scrollbar-track {
|
|
374
|
+
background: transparent;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
.pnp-profile-tester .max-h-80::-webkit-scrollbar-thumb {
|
|
378
|
+
background: hsl(var(--bc) / 0.2);
|
|
379
|
+
border-radius: 4px;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
.pnp-profile-tester .max-h-80::-webkit-scrollbar-thumb:hover {
|
|
383
|
+
background: hsl(var(--bc) / 0.3);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/* Firefox scrollbar */
|
|
387
|
+
.pnp-profile-tester .max-h-80 {
|
|
388
|
+
scrollbar-width: thin;
|
|
389
|
+
scrollbar-color: hsl(var(--bc) / 0.2) transparent;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/* Improve form control spacing */
|
|
393
|
+
.pnp-profile-tester .form-control .label {
|
|
394
|
+
padding-top: 0.25rem;
|
|
395
|
+
padding-bottom: 0.25rem;
|
|
396
|
+
}
|
|
397
|
+
</style>
|