@pie-players/pie-tool-theme 0.3.43 → 0.3.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -5,4 +5,3 @@
5
5
  * Import the built version for CDN usage, or the .svelte source for Svelte projects.
6
6
  */
7
7
  export {};
8
- //# sourceMappingURL=index.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pie-players/pie-tool-theme",
3
- "version": "0.3.43",
3
+ "version": "0.3.44",
4
4
  "type": "module",
5
5
  "description": "Theme tool for PIE assessment player - provides accessible themes and color schemes",
6
6
  "repository": {
@@ -21,28 +21,25 @@
21
21
  "wcag",
22
22
  "learnosity"
23
23
  ],
24
- "svelte": "./tool-color-scheme.svelte",
25
24
  "main": "./dist/tool-color-scheme.js",
26
25
  "exports": {
27
26
  ".": {
28
27
  "types": "./dist/index.d.ts",
29
- "import": "./dist/tool-color-scheme.js",
30
- "svelte": "./tool-color-scheme.svelte"
28
+ "import": "./dist/tool-color-scheme.js"
31
29
  }
32
30
  },
33
31
  "files": [
34
32
  "dist",
35
- "tool-color-scheme.svelte",
36
33
  "package.json"
37
34
  ],
38
35
  "license": "MIT",
39
36
  "unpkg": "./dist/tool-color-scheme.js",
40
37
  "jsdelivr": "./dist/tool-color-scheme.js",
41
38
  "dependencies": {
42
- "@pie-players/pie-assessment-toolkit": "0.3.43",
43
- "@pie-players/pie-context": "0.3.43",
44
- "@pie-players/pie-players-shared": "0.3.43",
45
- "@pie-players/pie-theme": "0.3.43"
39
+ "@pie-players/pie-assessment-toolkit": "0.3.44",
40
+ "@pie-players/pie-context": "0.3.44",
41
+ "@pie-players/pie-players-shared": "0.3.44",
42
+ "@pie-players/pie-theme": "0.3.44"
46
43
  },
47
44
  "types": "./dist/index.d.ts",
48
45
  "scripts": {
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG"}
@@ -1,560 +0,0 @@
1
- <svelte:options
2
- customElement={{
3
- tag: 'pie-tool-theme',
4
- shadow: 'open',
5
- props: {
6
- visible: { type: 'Boolean', attribute: 'visible' },
7
- toolId: { type: 'String', attribute: 'tool-id' },
8
- schemes: { type: 'String', attribute: 'schemes' },
9
- schemeCatalog: { type: 'Object', reflect: false }
10
- }
11
- }}
12
- />
13
-
14
- <!-- ToolColorScheme - WCAG Color Scheme Selection Tool
15
-
16
- Provides accessible color scheme options for users who need
17
- high contrast or alternative color combinations.
18
-
19
- Addresses WCAG 2.1 Level AA criteria:
20
- - 1.4.1 Use of Color
21
- - 1.4.3 Contrast (Minimum)
22
- - 1.4.11 Non-text Contrast
23
- -->
24
-
25
- <script lang="ts">
26
- const browser = typeof window !== "undefined";
27
-
28
- import {
29
- connectToolRuntimeContext,
30
- ZIndexLayer,
31
- } from '@pie-players/pie-assessment-toolkit';
32
- import {
33
- listPieColorSchemes,
34
- type PieColorSchemeDefinition,
35
- } from '@pie-players/pie-theme';
36
- import type {
37
- AssessmentToolkitRuntimeContext,
38
- ToolCoordinatorApi,
39
- } from '@pie-players/pie-assessment-toolkit';
40
- import { createFocusTrap, safeLocalStorageGet, safeLocalStorageSet } from '@pie-players/pie-players-shared';
41
- import { onMount } from 'svelte';
42
-
43
- let {
44
- visible = false,
45
- toolId = 'theme',
46
- schemes = '',
47
- schemeCatalog = null
48
- }: {
49
- visible?: boolean;
50
- toolId?: string;
51
- schemes?: string;
52
- schemeCatalog?: unknown;
53
- } = $props();
54
-
55
- let containerEl = $state<HTMLDivElement | undefined>();
56
- let runtimeContext = $state<AssessmentToolkitRuntimeContext | null>(null);
57
- const coordinator = $derived(
58
- runtimeContext?.toolCoordinator as ToolCoordinatorApi | undefined,
59
- );
60
-
61
- // Track registration state
62
- let registered = $state(false);
63
-
64
- $effect(() => {
65
- if (!containerEl) return;
66
- return connectToolRuntimeContext(containerEl, (value: AssessmentToolkitRuntimeContext) => {
67
- runtimeContext = value;
68
- });
69
- });
70
-
71
- type ToolSchemePreview = {
72
- bg: string;
73
- text: string;
74
- primary: string;
75
- };
76
-
77
- type ToolColorSchemeOption = {
78
- id: string;
79
- name: string;
80
- description: string;
81
- preview: ToolSchemePreview;
82
- };
83
- const FALLBACK_PREVIEW: ToolSchemePreview = {
84
- bg: '#ffffff',
85
- text: '#000000',
86
- primary: '#3f51b5'
87
- };
88
-
89
- function previewFromVariables(
90
- variables: Record<string, string> | undefined,
91
- fallback: ToolSchemePreview
92
- ): ToolSchemePreview {
93
- return {
94
- bg: variables?.['--pie-background'] || variables?.['--pie-secondary-background'] || fallback.bg,
95
- text: variables?.['--pie-text'] || fallback.text,
96
- primary: variables?.['--pie-primary'] || fallback.primary
97
- };
98
- }
99
-
100
- function normalizeSchemeCatalogEntry(entry: unknown): ToolColorSchemeOption | null {
101
- if (!entry || typeof entry !== 'object') return null;
102
- const value = entry as Record<string, unknown>;
103
- const id = typeof value.id === 'string' ? value.id.trim() : '';
104
- if (!id) return null;
105
- const name = typeof value.name === 'string' && value.name.trim() ? value.name.trim() : id;
106
- const description = typeof value.description === 'string' ? value.description : '';
107
- const previewValue = value.preview;
108
- const preview = (
109
- previewValue &&
110
- typeof previewValue === 'object' &&
111
- typeof (previewValue as Record<string, unknown>).bg === 'string' &&
112
- typeof (previewValue as Record<string, unknown>).text === 'string' &&
113
- typeof (previewValue as Record<string, unknown>).primary === 'string'
114
- )
115
- ? {
116
- bg: (previewValue as Record<string, string>).bg,
117
- text: (previewValue as Record<string, string>).text,
118
- primary: (previewValue as Record<string, string>).primary
119
- }
120
- : previewFromVariables(
121
- typeof value.variables === 'object' && value.variables
122
- ? (value.variables as Record<string, string>)
123
- : undefined,
124
- FALLBACK_PREVIEW
125
- );
126
- return { id, name, description, preview };
127
- }
128
-
129
- function parseSchemesFromAttribute(raw: string): ToolColorSchemeOption[] {
130
- if (!raw?.trim()) return [];
131
- try {
132
- const parsed = JSON.parse(raw) as unknown;
133
- return Array.isArray(parsed)
134
- ? parsed
135
- .map((entry) => normalizeSchemeCatalogEntry(entry))
136
- .filter((entry): entry is ToolColorSchemeOption => Boolean(entry))
137
- : [];
138
- } catch {
139
- return [];
140
- }
141
- }
142
-
143
- function resolveThemeHost(): HTMLElement | null {
144
- const localHost = containerEl?.closest('pie-theme') as HTMLElement | null;
145
- if (localHost) return localHost;
146
- const scopedDocumentHost = document.querySelector('pie-theme[scope="document"]') as HTMLElement | null;
147
- if (scopedDocumentHost) return scopedDocumentHost;
148
- return document.querySelector('pie-theme') as HTMLElement | null;
149
- }
150
-
151
- function resolveThemeSchemes(): ToolColorSchemeOption[] {
152
- const themeSchemes = listPieColorSchemes();
153
- return themeSchemes.map((scheme: PieColorSchemeDefinition) => {
154
- return {
155
- id: scheme.id,
156
- name: scheme.name || scheme.id,
157
- description: scheme.description || '',
158
- preview: scheme.preview || previewFromVariables(scheme.variables, FALLBACK_PREVIEW)
159
- };
160
- });
161
- }
162
-
163
- const availableSchemes = $derived.by(() => {
164
- if (Array.isArray(schemeCatalog) && schemeCatalog.length > 0) {
165
- const normalized = schemeCatalog
166
- .map((entry) => normalizeSchemeCatalogEntry(entry))
167
- .filter((entry): entry is ToolColorSchemeOption => Boolean(entry));
168
- if (normalized.length > 0) return normalized;
169
- }
170
- const parsedFromAttribute = parseSchemesFromAttribute(schemes);
171
- if (parsedFromAttribute.length > 0) return parsedFromAttribute;
172
- const themeSchemes = resolveThemeSchemes();
173
- if (themeSchemes.length > 0) return themeSchemes;
174
- return [{ id: 'default', name: 'Default', description: '', preview: FALLBACK_PREVIEW }];
175
- });
176
-
177
- // Current color scheme
178
- let currentScheme = $state('default');
179
-
180
- // Dropdown state
181
- let dropdownOpen = $state(false);
182
-
183
- // Focus trap cleanup function (plain variable, not reactive)
184
- let cleanupFocusTrap: (() => void) | null = null;
185
-
186
- // Apply color scheme to document
187
- function applyColorScheme(schemeId: string) {
188
- if (!browser) return;
189
-
190
- const themeHost = resolveThemeHost();
191
- if (themeHost) {
192
- themeHost.setAttribute('scheme', schemeId || 'default');
193
- } else {
194
- const root = document.documentElement;
195
- if (schemeId === 'default') {
196
- root.removeAttribute('data-color-scheme');
197
- } else {
198
- root.setAttribute('data-color-scheme', schemeId);
199
- }
200
- }
201
-
202
- // Save to localStorage safely
203
- safeLocalStorageSet('pie-color-scheme', schemeId);
204
- }
205
-
206
- // Select scheme and close the tool
207
- // Automatically closes the modal after selection for better UX
208
- function selectScheme(schemeId: string) {
209
- currentScheme = schemeId;
210
- applyColorScheme(schemeId);
211
- // Close the entire tool modal and deselect toolbar button
212
- coordinator?.hideTool(toolId);
213
- }
214
-
215
- // Toggle dropdown
216
- function toggleDropdown() {
217
- dropdownOpen = !dropdownOpen;
218
- }
219
-
220
- // Get current scheme object
221
- let currentSchemeObj = $derived(
222
- availableSchemes.find(s => s.id === currentScheme) ||
223
- availableSchemes[0] ||
224
- { id: 'default', name: 'Default', description: '', preview: FALLBACK_PREVIEW }
225
- );
226
-
227
- $effect(() => {
228
- if (!availableSchemes.find((scheme) => scheme.id === currentScheme)) {
229
- currentScheme = availableSchemes[0]?.id || 'default';
230
- }
231
- });
232
-
233
- // Handle escape key
234
- function handleKeyDown(e: KeyboardEvent) {
235
- if (e.key === 'Escape') {
236
- if (dropdownOpen) {
237
- // Close dropdown first
238
- dropdownOpen = false;
239
- }
240
- } else if (dropdownOpen && (e.key === 'ArrowDown' || e.key === 'ArrowUp')) {
241
- // Handle arrow key navigation in dropdown
242
- e.preventDefault();
243
- const options = containerEl?.querySelectorAll('.pie-tool-color-scheme__option:not([disabled])') as NodeListOf<HTMLButtonElement>;
244
- if (!options || options.length === 0) return;
245
-
246
- const currentIndex = Array.from(options).findIndex(opt => opt === document.activeElement);
247
- let nextIndex: number;
248
-
249
- if (e.key === 'ArrowDown') {
250
- nextIndex = currentIndex === -1 ? 0 : (currentIndex + 1) % options.length;
251
- } else {
252
- nextIndex = currentIndex === -1 ? options.length - 1 : (currentIndex - 1 + options.length) % options.length;
253
- }
254
-
255
- options[nextIndex]?.focus();
256
- }
257
- }
258
-
259
- // Register with coordinator when it becomes available
260
- $effect(() => {
261
- if (coordinator && toolId && !registered) {
262
- coordinator.registerTool(toolId, 'Theme', undefined, ZIndexLayer.MODAL);
263
- registered = true;
264
- }
265
- });
266
-
267
- // Update element reference when container becomes available
268
- $effect(() => {
269
- if (coordinator && containerEl && toolId) {
270
- coordinator.updateToolElement(toolId, containerEl);
271
- }
272
- });
273
-
274
- // Handle focus trap cleanup when visibility changes
275
- $effect(() => {
276
- if (!visible && cleanupFocusTrap) {
277
- cleanupFocusTrap();
278
- cleanupFocusTrap = null;
279
- } else if (visible && containerEl && !cleanupFocusTrap) {
280
- cleanupFocusTrap = createFocusTrap(containerEl);
281
- }
282
- });
283
-
284
- onMount(() => {
285
- // Load saved scheme from localStorage safely
286
- if (browser) {
287
- const themeHost = resolveThemeHost();
288
- const hostScheme = themeHost?.getAttribute('scheme');
289
- const saved = safeLocalStorageGet('pie-color-scheme') ?? hostScheme ?? 'default';
290
- currentScheme = saved;
291
- if (saved) {
292
- requestAnimationFrame(() => {
293
- applyColorScheme(saved);
294
- });
295
- }
296
- }
297
-
298
- // Click outside handler
299
- function handleClickOutside(e: MouseEvent) {
300
- if (!dropdownOpen) return;
301
- const clickPath = e.composedPath();
302
- if (!containerEl || !clickPath.includes(containerEl)) {
303
- dropdownOpen = false;
304
- }
305
- }
306
-
307
- document.addEventListener('click', handleClickOutside);
308
-
309
- return () => {
310
- document.removeEventListener('click', handleClickOutside);
311
- if (coordinator && toolId) {
312
- coordinator.unregisterTool(toolId);
313
- }
314
- };
315
- });
316
- </script>
317
-
318
- {#if visible}
319
- <div
320
- bind:this={containerEl}
321
- class="pie-tool-color-scheme"
322
- role="dialog"
323
- tabindex="-1"
324
- aria-label="Theme selector"
325
- onkeydown={handleKeyDown}
326
- >
327
- <div class="pie-tool-color-scheme__content">
328
- <p class="pie-tool-color-scheme__description">
329
- Select a theme to improve readability and reduce eye strain.
330
- </p>
331
-
332
- <button
333
- type="button"
334
- class="pie-tool-color-scheme__dropdown-trigger"
335
- aria-label="Select theme"
336
- aria-expanded={dropdownOpen}
337
- onclick={toggleDropdown}
338
- >
339
- <div class="pie-tool-color-scheme__current">
340
- <div class="pie-tool-color-scheme__preview">
341
- <div class="pie-tool-color-scheme__preview-bg" style="background-color: {currentSchemeObj.preview.bg}">
342
- <div class="pie-tool-color-scheme__preview-text" style="color: {currentSchemeObj.preview.text}">A</div>
343
- <div class="pie-tool-color-scheme__preview-primary" style="background-color: {currentSchemeObj.preview.primary}"></div>
344
- </div>
345
- </div>
346
- <div class="pie-tool-color-scheme__current-info">
347
- <div class="pie-tool-color-scheme__current-name">{currentSchemeObj.name}</div>
348
- <div class="pie-tool-color-scheme__current-description">{currentSchemeObj.description}</div>
349
- </div>
350
- <svg class="pie-tool-color-scheme__dropdown-arrow" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
351
- <path d="M7,10L12,15L17,10H7Z"/>
352
- </svg>
353
- </div>
354
- </button>
355
-
356
- {#if dropdownOpen}
357
- <div class="pie-tool-color-scheme__dropdown" role="menu">
358
- {#each availableSchemes as scheme (scheme.id)}
359
- <button
360
- type="button"
361
- class="pie-tool-color-scheme__option"
362
- class:pie-tool-color-scheme__option--active={currentScheme === scheme.id}
363
- role="menuitem"
364
- aria-label={scheme.name}
365
- aria-current={currentScheme === scheme.id}
366
- onclick={() => selectScheme(scheme.id)}
367
- >
368
- <div class="pie-tool-color-scheme__preview">
369
- <div class="pie-tool-color-scheme__preview-bg" style="background-color: {scheme.preview.bg}">
370
- <div class="pie-tool-color-scheme__preview-text" style="color: {scheme.preview.text}">A</div>
371
- <div class="pie-tool-color-scheme__preview-primary" style="background-color: {scheme.preview.primary}"></div>
372
- </div>
373
- </div>
374
- <div class="pie-tool-color-scheme__info">
375
- <div class="pie-tool-color-scheme__name">{scheme.name}</div>
376
- <div class="pie-tool-color-scheme__description">{scheme.description}</div>
377
- </div>
378
- {#if currentScheme === scheme.id}
379
- <svg xmlns="http://www.w3.org/2000/svg" class="pie-tool-color-scheme__check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
380
- <polyline points="20 6 9 17 4 12"/>
381
- </svg>
382
- {/if}
383
- </button>
384
- {/each}
385
- </div>
386
- {/if}
387
- </div>
388
- </div>
389
- {/if}
390
-
391
- <style>
392
- .pie-tool-color-scheme {
393
- position: relative;
394
- width: 100%;
395
- height: 100%;
396
- min-height: 0;
397
- background-color: var(--pie-background, white);
398
- display: flex;
399
- flex-direction: column;
400
- }
401
-
402
- .pie-tool-color-scheme__content {
403
- padding: 1rem;
404
- position: relative;
405
- display: flex;
406
- flex-direction: column;
407
- gap: 1rem;
408
- min-height: 0;
409
- overflow: auto;
410
- }
411
-
412
- .pie-tool-color-scheme__description {
413
- margin: 0 0 1rem 0;
414
- color: var(--pie-text, black);
415
- opacity: 0.7;
416
- font-size: 0.875rem;
417
- }
418
-
419
- .pie-tool-color-scheme__dropdown-trigger {
420
- width: 100%;
421
- padding: 0.75rem;
422
- border: 2px solid var(--pie-border, #ccc);
423
- border-radius: 0.5rem;
424
- background-color: var(--pie-background, white);
425
- color: var(--pie-text, black);
426
- cursor: pointer;
427
- transition: all 0.15s ease;
428
- }
429
-
430
- .pie-tool-color-scheme__dropdown-trigger:hover {
431
- background-color: var(--pie-secondary-background, #f0f0f0);
432
- }
433
-
434
- .pie-tool-color-scheme__current {
435
- display: flex;
436
- align-items: center;
437
- gap: 0.75rem;
438
- }
439
-
440
- .pie-tool-color-scheme__current-info {
441
- flex: 1;
442
- text-align: left;
443
- }
444
-
445
- .pie-tool-color-scheme__current-name {
446
- font-size: 0.875rem;
447
- font-weight: 600;
448
- color: var(--pie-text, black);
449
- }
450
-
451
- .pie-tool-color-scheme__current-description {
452
- font-size: 0.75rem;
453
- color: var(--pie-text, black);
454
- opacity: 0.6;
455
- }
456
-
457
- .pie-tool-color-scheme__dropdown-arrow {
458
- width: 1.5rem;
459
- height: 1.5rem;
460
- flex-shrink: 0;
461
- }
462
-
463
- .pie-tool-color-scheme__dropdown {
464
- position: relative;
465
- width: 100%;
466
- max-height: 24rem;
467
- background-color: var(--pie-background, white);
468
- border: 2px solid var(--pie-border, #ccc);
469
- border-radius: 0.5rem;
470
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
471
- overflow-y: auto;
472
- overflow-x: hidden;
473
- margin-top: 0.5rem;
474
- }
475
-
476
- .pie-tool-color-scheme__option {
477
- display: flex;
478
- align-items: center;
479
- gap: 0.75rem;
480
- width: 100%;
481
- padding: 0.75rem 1rem;
482
- border: none;
483
- background-color: transparent;
484
- color: var(--pie-text, black);
485
- cursor: pointer;
486
- text-align: left;
487
- transition: background-color 0.15s ease;
488
- }
489
-
490
- .pie-tool-color-scheme__option:hover {
491
- background-color: var(--pie-secondary-background, rgba(0, 0, 0, 0.05));
492
- }
493
-
494
- .pie-tool-color-scheme__option--active {
495
- background-color: var(--pie-primary-light, rgba(63, 81, 181, 0.1));
496
- }
497
-
498
- .pie-tool-color-scheme__preview {
499
- flex-shrink: 0;
500
- width: 2.5rem;
501
- height: 2.5rem;
502
- border: 2px solid var(--pie-border, #ccc);
503
- border-radius: 0.25rem;
504
- overflow: hidden;
505
- }
506
-
507
- .pie-tool-color-scheme__preview-bg {
508
- width: 100%;
509
- height: 100%;
510
- position: relative;
511
- display: flex;
512
- align-items: center;
513
- justify-content: center;
514
- }
515
-
516
- .pie-tool-color-scheme__preview-text {
517
- font-weight: 700;
518
- font-size: 1.25rem;
519
- }
520
-
521
- .pie-tool-color-scheme__preview-primary {
522
- position: absolute;
523
- bottom: 0;
524
- right: 0;
525
- width: 0.75rem;
526
- height: 0.75rem;
527
- border-radius: 50%;
528
- }
529
-
530
- .pie-tool-color-scheme__info {
531
- flex: 1;
532
- }
533
-
534
- .pie-tool-color-scheme__name {
535
- font-size: 0.875rem;
536
- font-weight: 600;
537
- color: var(--pie-text, black);
538
- line-height: 1.25;
539
- }
540
-
541
- .pie-tool-color-scheme__description {
542
- font-size: 0.75rem;
543
- color: var(--pie-text, black);
544
- opacity: 0.6;
545
- }
546
-
547
- .pie-tool-color-scheme__check {
548
- flex-shrink: 0;
549
- width: 1rem;
550
- height: 1rem;
551
- color: var(--pie-primary, #3f51b5);
552
- }
553
-
554
- /* Keyboard focus styling */
555
- .pie-tool-color-scheme__dropdown-trigger:focus-visible,
556
- .pie-tool-color-scheme__option:focus-visible {
557
- outline: 2px solid var(--pie-primary, #3f51b5);
558
- outline-offset: 2px;
559
- }
560
- </style>