@pie-players/pie-assessment-toolkit 0.3.2 → 0.3.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 (90) hide show
  1. package/README.md +5 -8
  2. package/dist/components/ItemToolBar.custom-element.js +539 -94
  3. package/dist/index.d.ts +1 -2
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +1 -1
  6. package/dist/index.js.map +1 -1
  7. package/dist/services/PNPToolResolver.d.ts.map +1 -1
  8. package/dist/services/PNPToolResolver.js +4 -1
  9. package/dist/services/PNPToolResolver.js.map +1 -1
  10. package/dist/services/ToolRegistry.d.ts +51 -0
  11. package/dist/services/ToolRegistry.d.ts.map +1 -1
  12. package/dist/services/ToolRegistry.js +60 -0
  13. package/dist/services/ToolRegistry.js.map +1 -1
  14. package/dist/services/ToolkitCoordinator.d.ts +12 -24
  15. package/dist/services/ToolkitCoordinator.d.ts.map +1 -1
  16. package/dist/services/ToolkitCoordinator.js +60 -99
  17. package/dist/services/ToolkitCoordinator.js.map +1 -1
  18. package/dist/services/defaultPersonalNeedsProfile.js +2 -2
  19. package/dist/services/defaultPersonalNeedsProfile.js.map +1 -1
  20. package/dist/services/interfaces.d.ts +8 -3
  21. package/dist/services/interfaces.d.ts.map +1 -1
  22. package/dist/services/interfaces.js.map +1 -1
  23. package/dist/services/pnp-standard-features.d.ts +1 -1
  24. package/dist/services/tool-providers/index.d.ts +0 -4
  25. package/dist/services/tool-providers/index.d.ts.map +1 -1
  26. package/dist/services/tool-providers/index.js +0 -2
  27. package/dist/services/tool-providers/index.js.map +1 -1
  28. package/dist/services/tools-config-normalizer.d.ts +40 -3
  29. package/dist/services/tools-config-normalizer.d.ts.map +1 -1
  30. package/dist/services/tools-config-normalizer.js +86 -11
  31. package/dist/services/tools-config-normalizer.js.map +1 -1
  32. package/dist/tools/client.d.ts +0 -2
  33. package/dist/tools/client.d.ts.map +1 -1
  34. package/dist/tools/client.js +0 -4
  35. package/dist/tools/client.js.map +1 -1
  36. package/dist/tools/default-tool-module-loaders.js +1 -1
  37. package/dist/tools/default-tool-module-loaders.js.map +1 -1
  38. package/dist/tools/library-loader.d.ts.map +1 -1
  39. package/dist/tools/library-loader.js +0 -35
  40. package/dist/tools/library-loader.js.map +1 -1
  41. package/dist/tools/registrations/accessibility-tools.d.ts.map +1 -1
  42. package/dist/tools/registrations/accessibility-tools.js +44 -41
  43. package/dist/tools/registrations/accessibility-tools.js.map +1 -1
  44. package/dist/tools/registrations/calculator.d.ts.map +1 -1
  45. package/dist/tools/registrations/calculator.js +50 -2
  46. package/dist/tools/registrations/calculator.js.map +1 -1
  47. package/dist/tools/registrations/interaction-tools.d.ts.map +1 -1
  48. package/dist/tools/registrations/interaction-tools.js +22 -28
  49. package/dist/tools/registrations/interaction-tools.js.map +1 -1
  50. package/dist/tools/registrations/measurement-tools.d.ts.map +1 -1
  51. package/dist/tools/registrations/measurement-tools.js +19 -27
  52. package/dist/tools/registrations/measurement-tools.js.map +1 -1
  53. package/dist/tools/registrations/subject-specific-tools.d.ts.map +1 -1
  54. package/dist/tools/registrations/subject-specific-tools.js +53 -37
  55. package/dist/tools/registrations/subject-specific-tools.js.map +1 -1
  56. package/dist/tools/registrations/toolbar-registration-helpers.d.ts +15 -0
  57. package/dist/tools/registrations/toolbar-registration-helpers.d.ts.map +1 -0
  58. package/dist/tools/registrations/toolbar-registration-helpers.js +22 -0
  59. package/dist/tools/registrations/toolbar-registration-helpers.js.map +1 -0
  60. package/dist/tools/registrations/tts.d.ts.map +1 -1
  61. package/dist/tools/registrations/tts.js +49 -0
  62. package/dist/tools/registrations/tts.js.map +1 -1
  63. package/dist/tools/types.d.ts +1 -1
  64. package/dist/tools/types.d.ts.map +1 -1
  65. package/dist/tools/types.js.map +1 -1
  66. package/package.json +8 -10
  67. package/dist/services/tool-providers/MathJsToolProvider.d.ts +0 -25
  68. package/dist/services/tool-providers/MathJsToolProvider.d.ts.map +0 -1
  69. package/dist/services/tool-providers/MathJsToolProvider.js +0 -53
  70. package/dist/services/tool-providers/MathJsToolProvider.js.map +0 -1
  71. package/dist/services/tool-providers/TIToolProvider.d.ts +0 -107
  72. package/dist/services/tool-providers/TIToolProvider.d.ts.map +0 -1
  73. package/dist/services/tool-providers/TIToolProvider.js +0 -123
  74. package/dist/services/tool-providers/TIToolProvider.js.map +0 -1
  75. package/dist/tools/calculators/mathjs-provider.d.ts +0 -50
  76. package/dist/tools/calculators/mathjs-provider.d.ts.map +0 -1
  77. package/dist/tools/calculators/mathjs-provider.js +0 -660
  78. package/dist/tools/calculators/mathjs-provider.js.map +0 -1
  79. package/dist/tools/calculators/ti-provider.d.ts +0 -122
  80. package/dist/tools/calculators/ti-provider.d.ts.map +0 -1
  81. package/dist/tools/calculators/ti-provider.js +0 -710
  82. package/dist/tools/calculators/ti-provider.js.map +0 -1
  83. package/src/README.md +0 -626
  84. package/src/components/ItemToolBar.svelte +0 -717
  85. package/src/components/PieAssessmentToolkit.svelte +0 -681
  86. package/src/components/SectionToolBar.svelte +0 -58
  87. package/src/components/ToolButton.svelte +0 -146
  88. package/src/components/ToolButtonGroup.svelte +0 -125
  89. package/src/tools/README.md +0 -617
  90. package/src/tools/calculators/README.md +0 -395
@@ -1,717 +0,0 @@
1
- <svelte:options
2
- customElement={{
3
- tag: 'pie-item-toolbar',
4
- shadow: 'open',
5
- props: {
6
- level: { type: 'String', attribute: 'level' },
7
- scopeId: { type: 'String', attribute: 'scope-id' },
8
- itemId: { type: 'String', attribute: 'item-id' },
9
- sectionId: { type: 'String', attribute: 'section-id' },
10
- catalogId: { type: 'String', attribute: 'catalog-id' },
11
- tools: { type: 'String', attribute: 'tools' },
12
- contentKind: { type: 'String', attribute: 'content-kind' },
13
- position: { type: 'String', attribute: 'position' },
14
- size: { type: 'String', attribute: 'size' },
15
- language: { type: 'String', attribute: 'language' },
16
-
17
- // Local runtime contracts (passed as JS properties, not attributes)
18
- scopeElement: { type: 'Object', reflect: false },
19
- toolRegistry: { type: 'Object', reflect: false },
20
- pnpResolver: { type: 'Object', reflect: false },
21
- assessment: { type: 'Object', reflect: false },
22
- itemRef: { type: 'Object', reflect: false },
23
- item: { type: 'Object', reflect: false },
24
- hostButtons: { type: 'Object', reflect: false }
25
- }
26
- }}
27
- />
28
-
29
- <!--
30
- ItemToolBar - Inline toolbar for item/passage headers
31
-
32
- Button visibility is always registry-driven:
33
- - Pass 1: allowed tool IDs (PNP resolver when available, otherwise explicit tools prop)
34
- - Pass 2: tool-owned isVisibleInContext(context)
35
- -->
36
- <script lang="ts">
37
- import {
38
- type AssessmentToolkitShellContext,
39
- type AssessmentToolkitRuntimeContext
40
- } from '../context/assessment-toolkit-context.js';
41
- import {
42
- connectAssessmentToolkitRuntimeContext,
43
- connectAssessmentToolkitShellContext,
44
- } from '../context/runtime-context-consumer.js';
45
- import type { ToolRegistry, ToolToolbarRenderResult, ToolbarContext } from '../services/ToolRegistry.js';
46
- import {
47
- isExternalIconUrl,
48
- isInlineSvgIcon,
49
- isToolbarLinkItem,
50
- type ToolbarItem
51
- } from '../services/toolbar-items.js';
52
- import type { PNPToolResolver } from '../services/PNPToolResolver.js';
53
- import { createPackagedToolRegistry } from '../services/createDefaultToolRegistry.js';
54
- import { DEFAULT_TOOL_MODULE_LOADERS } from '../tools/default-tool-module-loaders.js';
55
- import {
56
- normalizeToolsConfig,
57
- parseToolList,
58
- resolveToolsForLevel,
59
- } from '../services/tools-config-normalizer.js';
60
- import { createScopedToolId, parseScopedToolId } from '../services/tool-instance-id.js';
61
- import type { AssessmentEntity, AssessmentItemRef, ItemEntity } from '@pie-players/pie-players-shared/types';
62
- import type { ElementToolContext, ItemToolContext, ToolLevel, ToolContext } from '../services/tool-context.js';
63
-
64
- const isBrowser = typeof window !== 'undefined';
65
- const fallbackToolRegistry = createPackagedToolRegistry({
66
- toolModuleLoaders: DEFAULT_TOOL_MODULE_LOADERS
67
- });
68
-
69
- let activeToolState = $state<Record<string, boolean>>({});
70
-
71
- // Props
72
- let {
73
- level = 'item' as ToolLevel,
74
- scopeId = '',
75
- itemId = '',
76
- sectionId = '',
77
- catalogId = '',
78
- tools = 'calculator,textToSpeech,answerEliminator',
79
- contentKind = 'assessment-item',
80
- position = 'bottom' as 'top' | 'right' | 'bottom' | 'left' | 'none',
81
- scopeElement = null,
82
- toolRegistry = null,
83
- pnpResolver = null,
84
- assessment = null,
85
- itemRef = null,
86
- item = null,
87
- hostButtons = [] as ToolbarItem[],
88
- class: className = '',
89
- size = 'md' as 'sm' | 'md' | 'lg',
90
- language = 'en-US'
91
- }: {
92
- level?: ToolLevel;
93
- scopeId?: string;
94
- itemId?: string;
95
- sectionId?: string;
96
- catalogId?: string;
97
- tools?: string;
98
- contentKind?: string;
99
- position?: 'top' | 'right' | 'bottom' | 'left' | 'none';
100
- scopeElement?: HTMLElement | null;
101
- toolRegistry?: ToolRegistry | null;
102
- pnpResolver?: PNPToolResolver | null;
103
- assessment?: AssessmentEntity | null;
104
- itemRef?: AssessmentItemRef | null;
105
- item?: ItemEntity | null;
106
- hostButtons?: ToolbarItem[];
107
- class?: string;
108
- size?: 'sm' | 'md' | 'lg';
109
- language?: string;
110
- } = $props();
111
-
112
- let toolbarRootElement = $state<HTMLDivElement | null>(null);
113
- let runtimeContext = $state<AssessmentToolkitRuntimeContext | null>(null);
114
- let shellContext = $state<AssessmentToolkitShellContext | null>(null);
115
- let moduleLoadVersion = $state(0);
116
-
117
- $effect(() => {
118
- if (!toolbarRootElement) return;
119
- return connectAssessmentToolkitRuntimeContext(toolbarRootElement, (value) => {
120
- runtimeContext = value;
121
- });
122
- });
123
-
124
- $effect(() => {
125
- if (!toolbarRootElement) return;
126
- return connectAssessmentToolkitShellContext(toolbarRootElement, (value) => {
127
- shellContext = value;
128
- });
129
- });
130
-
131
- const effectiveToolCoordinator = $derived(runtimeContext?.toolCoordinator);
132
- const effectiveTTSService = $derived(runtimeContext?.ttsService);
133
- const effectiveElementToolStateStore = $derived(runtimeContext?.elementToolStateStore);
134
- const effectiveAssessmentId = $derived(runtimeContext?.assessmentId ?? '');
135
- const effectiveItemId = $derived(itemId || shellContext?.itemId || '');
136
- const effectiveCanonicalItemId = $derived(shellContext?.canonicalItemId || effectiveItemId);
137
- const effectiveSectionId = $derived(sectionId || runtimeContext?.sectionId || '');
138
- const effectiveContentKind = $derived(contentKind || shellContext?.contentKind || (level === 'section' ? 'section' : 'assessment-item'));
139
- const effectiveLevel = $derived.by((): ToolLevel => {
140
- if (level && level !== 'item') return level;
141
- if (effectiveContentKind === 'rubric-block-stimulus') return 'passage';
142
- return 'item';
143
- });
144
- const effectiveScopeId = $derived.by(() => {
145
- if (scopeId) return scopeId;
146
- if (effectiveLevel === 'section') return effectiveSectionId || runtimeContext?.sectionId || 'default-section';
147
- if (effectiveLevel === 'assessment') return runtimeContext?.assessmentId || 'default-assessment';
148
- return effectiveCanonicalItemId || effectiveItemId || 'default-item';
149
- });
150
- const effectiveCatalogId = $derived(catalogId || effectiveScopeId);
151
- const effectiveItem = $derived((item as ItemEntity | null) || (shellContext?.item as ItemEntity | null) || null);
152
-
153
- // Effective registry for visibility and metadata ownership.
154
- const effectiveToolRegistry = $derived(toolRegistry || fallbackToolRegistry);
155
-
156
- const explicitTools = $derived(parseToolList(tools));
157
- const normalizedExplicitTools = $derived(
158
- effectiveToolRegistry.normalizeToolIds(explicitTools).filter(Boolean)
159
- );
160
- const effectiveToolsConfig = $derived.by(() => {
161
- const coordinatorConfig = runtimeContext?.toolkitCoordinator?.config?.tools as any;
162
- return normalizeToolsConfig(coordinatorConfig || {});
163
- });
164
- const placementLevel = $derived.by(() => {
165
- if (effectiveLevel === 'section') return 'section';
166
- if (effectiveLevel === 'passage' || effectiveContentKind === 'rubric-block-stimulus') return 'passage';
167
- return 'item';
168
- });
169
- const placementAllowedToolIds = $derived.by(() => {
170
- const resolved = resolveToolsForLevel(effectiveToolsConfig, placementLevel);
171
- return effectiveToolRegistry.normalizeToolIds(resolved).filter(Boolean);
172
- });
173
- const providerConfig = $derived(
174
- (effectiveToolsConfig?.providers || {}) as Record<
175
- string,
176
- { enabled?: boolean } | undefined
177
- >
178
- );
179
- const isProviderEnabled = (toolId: string): boolean =>
180
- providerConfig[toolId]?.enabled !== false;
181
-
182
- // Pass 1: determine allowed tools
183
- const allowedToolIds = $derived.by(() => {
184
- const configuredTools =
185
- normalizedExplicitTools.length > 0 ? normalizedExplicitTools : placementAllowedToolIds;
186
- const providerEnabledConfiguredTools = configuredTools.filter((toolId) =>
187
- isProviderEnabled(toolId)
188
- );
189
- if (pnpResolver && assessment && itemRef) {
190
- const allowedByPnp = pnpResolver.getAllowedToolIds(assessment, itemRef);
191
- if (providerEnabledConfiguredTools.length === 0) return allowedByPnp;
192
- return allowedByPnp.filter((toolId) =>
193
- providerEnabledConfiguredTools.includes(toolId)
194
- );
195
- }
196
- return providerEnabledConfiguredTools;
197
- });
198
-
199
- const contentReady = $derived.by(() => {
200
- if (effectiveLevel === 'section' || effectiveLevel === 'assessment') return true;
201
- const config = (effectiveItem as ItemEntity | null)?.config;
202
- return !!(effectiveItem && config && typeof config === 'object');
203
- });
204
-
205
- const toolContext = $derived.by((): ItemToolContext | null => {
206
- if (effectiveLevel === 'section') {
207
- return {
208
- level: 'section',
209
- assessment: (assessment || {}) as AssessmentEntity,
210
- section: {} as any
211
- } as ToolContext as ItemToolContext;
212
- }
213
- if (effectiveLevel === 'assessment') {
214
- return {
215
- level: 'assessment',
216
- assessment: (assessment || {}) as AssessmentEntity
217
- } as ToolContext as ItemToolContext;
218
- }
219
- if (!contentReady || !effectiveItem) {
220
- return null;
221
- }
222
- if (effectiveLevel === 'passage') {
223
- return {
224
- level: 'passage',
225
- assessment: (assessment || {}) as AssessmentEntity,
226
- itemRef: (itemRef || ({ id: effectiveCanonicalItemId } as AssessmentItemRef)) as AssessmentItemRef,
227
- passage: effectiveItem as any
228
- } as ToolContext as ItemToolContext;
229
- }
230
- return {
231
- level: 'item',
232
- assessment: (assessment || {}) as AssessmentEntity,
233
- itemRef: (itemRef || ({ id: effectiveCanonicalItemId } as AssessmentItemRef)) as AssessmentItemRef,
234
- item: effectiveItem as ItemEntity
235
- } as ToolContext as ItemToolContext;
236
- });
237
-
238
- const renderContext = $derived.by((): ToolContext => {
239
- if (toolContext) return toolContext;
240
- return {
241
- level: 'item',
242
- assessment: (assessment || {}) as AssessmentEntity,
243
- itemRef: (itemRef || ({ id: effectiveCanonicalItemId } as AssessmentItemRef)) as AssessmentItemRef,
244
- item: ((effectiveItem as ItemEntity | null) || ({ id: effectiveCanonicalItemId, config: {} } as ItemEntity)) as ItemEntity
245
- } as ToolContext;
246
- });
247
-
248
- const elementContexts = $derived.by((): ElementToolContext[] => {
249
- if (effectiveLevel !== 'item' && effectiveLevel !== 'passage') return [];
250
- if (!contentReady || !effectiveItem) return [];
251
- const modelsRaw = (effectiveItem as any)?.config?.models;
252
- const models = Array.isArray(modelsRaw)
253
- ? modelsRaw
254
- : modelsRaw && typeof modelsRaw === 'object'
255
- ? Object.values(modelsRaw)
256
- : [];
257
- return models
258
- .filter((model: any) => model && typeof model === 'object' && typeof model.id === 'string')
259
- .map((model: any) => ({
260
- level: 'element' as const,
261
- assessment: (assessment || {}) as AssessmentEntity,
262
- itemRef: (itemRef || ({ id: effectiveCanonicalItemId } as AssessmentItemRef)) as AssessmentItemRef,
263
- item: effectiveItem as ItemEntity,
264
- elementId: model.id as string
265
- }));
266
- });
267
-
268
- // Pass 2: tool-owned context filtering (item + element aggregation)
269
- const visibleToolIds = $derived.by(() => {
270
- const levelCompatibleToolIds = allowedToolIds.filter((toolId) => {
271
- const registration = effectiveToolRegistry.get(toolId);
272
- return registration ? registration.supportedLevels.includes(effectiveLevel) : false;
273
- });
274
- if (effectiveLevel === 'section') {
275
- // Section toolbars are orchestrator-driven. Tool-level relevance is often
276
- // item-content dependent. Keep pass-1 as the source of truth here.
277
- return allowedToolIds;
278
- }
279
- if (!contentReady) {
280
- // Stage 1: orchestrator-level allow-list only.
281
- return levelCompatibleToolIds;
282
- }
283
- if (!toolContext && elementContexts.length === 0) {
284
- return levelCompatibleToolIds;
285
- }
286
- const visible = new Set<string>();
287
- if (toolContext) {
288
- effectiveToolRegistry
289
- .filterVisibleInContext(levelCompatibleToolIds, toolContext)
290
- .forEach((tool) => visible.add(tool.toolId));
291
- }
292
-
293
- for (const context of elementContexts) {
294
- effectiveToolRegistry
295
- .filterVisibleInContext(levelCompatibleToolIds, context)
296
- .forEach((tool) => visible.add(tool.toolId));
297
- }
298
-
299
- return Array.from(visible);
300
- });
301
- const toolbarVisibleToolIds = $derived.by(() =>
302
- effectiveToolRegistry.filterToolIdsByActivation(visibleToolIds, 'toolbar-toggle')
303
- );
304
-
305
- // Dynamically load whatever tools are currently visible.
306
- // The registry owns module loader configuration by toolId.
307
- $effect(() => {
308
- if (!isBrowser) return;
309
- let cancelled = false;
310
- void effectiveToolRegistry
311
- .ensureToolModulesLoaded(toolbarVisibleToolIds)
312
- .then(() => {
313
- if (!cancelled) moduleLoadVersion += 1;
314
- })
315
- .catch((error: unknown) => {
316
- console.error('[ItemToolBar] Failed to load one or more tool modules:', error);
317
- });
318
- return () => {
319
- cancelled = true;
320
- };
321
- });
322
-
323
- function resolveScopeElement(): HTMLElement | null {
324
- if (scopeElement) return scopeElement;
325
- return shellContext?.scopeElement || null;
326
- }
327
-
328
- function resolveGlobalElementId(): string | null {
329
- if (!effectiveElementToolStateStore || !effectiveAssessmentId || !effectiveSectionId || !effectiveCanonicalItemId)
330
- return null;
331
- return effectiveElementToolStateStore.getGlobalElementId(
332
- effectiveAssessmentId,
333
- effectiveSectionId,
334
- effectiveCanonicalItemId,
335
- effectiveCanonicalItemId
336
- );
337
- }
338
-
339
- const toolbarContext = $derived.by((): ToolbarContext => {
340
- const toolkitCoordinator = runtimeContext?.toolkitCoordinator || null;
341
- const toInstanceToolId = (toolId: string): string => {
342
- // Accept either base tool IDs (e.g. "calculator") or already-scoped IDs.
343
- // This keeps toolbar contracts stable across mixed registration implementations.
344
- return parseScopedToolId(toolId)
345
- ? toolId
346
- : createScopedToolId(toolId, effectiveLevel, effectiveScopeId);
347
- };
348
- return {
349
- scope: {
350
- level: effectiveLevel,
351
- scopeId: effectiveScopeId,
352
- assessmentId: runtimeContext?.assessmentId,
353
- sectionId: effectiveSectionId,
354
- itemId: effectiveItemId,
355
- canonicalItemId: effectiveCanonicalItemId,
356
- contentKind: effectiveContentKind
357
- },
358
- itemId: effectiveScopeId,
359
- catalogId: effectiveCatalogId,
360
- language,
361
- ui: {
362
- size
363
- },
364
- getScopeElement: resolveScopeElement,
365
- getGlobalElementId: resolveGlobalElementId,
366
- toolCoordinator: effectiveToolCoordinator || null,
367
- toolkitCoordinator,
368
- ttsService: effectiveTTSService || null,
369
- elementToolStateStore: effectiveElementToolStateStore || null,
370
- toggleTool: (toolId: string) => {
371
- if (!effectiveToolCoordinator) return;
372
- const instanceToolId = toInstanceToolId(toolId);
373
- // Some tools only self-register after first visibility sync.
374
- // Seed a placeholder registration so first toggle always works.
375
- if (!effectiveToolCoordinator.getToolState(instanceToolId)) {
376
- effectiveToolCoordinator.registerTool(instanceToolId, toolId);
377
- }
378
- effectiveToolCoordinator.toggleTool(instanceToolId);
379
- },
380
- isToolVisible: (toolId: string) => {
381
- if (!effectiveToolCoordinator) return false;
382
- return effectiveToolCoordinator.isToolVisible(toInstanceToolId(toolId));
383
- },
384
- subscribeVisibility: effectiveToolCoordinator
385
- ? (listener: () => void) => effectiveToolCoordinator.subscribe(listener)
386
- : null,
387
- ensureTTSReady: toolkitCoordinator ? () => toolkitCoordinator.ensureTTSReady() : null
388
- };
389
- });
390
-
391
- let renderedTools = $derived.by((): ToolToolbarRenderResult[] => {
392
- if (!isBrowser) return [];
393
- moduleLoadVersion;
394
-
395
- const rendered: ToolToolbarRenderResult[] = [];
396
- for (const toolId of toolbarVisibleToolIds) {
397
- const result = effectiveToolRegistry.renderForToolbar(toolId, renderContext, toolbarContext);
398
- if (result) {
399
- rendered.push(result);
400
- }
401
- }
402
- return rendered;
403
- });
404
- const normalizedHostButtons = $derived.by((): ToolbarItem[] =>
405
- Array.isArray(hostButtons)
406
- ? hostButtons.filter((item): item is ToolbarItem => !!item && typeof item.id === 'string')
407
- : []
408
- );
409
- const nativeToolbarItems = $derived.by((): ToolbarItem[] =>
410
- renderedTools
411
- .filter((renderedTool) => !!renderedTool.button)
412
- .map((renderedTool) => {
413
- const button = renderedTool.button!;
414
- return {
415
- id: renderedTool.toolId,
416
- label: button.label,
417
- ariaLabel: button.ariaLabel || button.label,
418
- icon: button.icon,
419
- tooltip: button.tooltip || button.label,
420
- disabled: button.disabled,
421
- active: activeToolState[renderedTool.toolId] ?? button.active ?? false,
422
- onClick: () => {
423
- button.onClick();
424
- syncRenderedToolsState();
425
- },
426
- } satisfies ToolbarItem;
427
- })
428
- );
429
- const toolbarItems = $derived.by((): ToolbarItem[] => [
430
- ...nativeToolbarItems,
431
- ...normalizedHostButtons
432
- ]);
433
- const mountedElementsBeforeButtons = $derived.by(() =>
434
- renderedTools.flatMap((renderedTool) =>
435
- (renderedTool.elements || [])
436
- .filter((entry) => entry.mount === 'before-buttons')
437
- .map((entry) => entry.element)
438
- .filter((entry): entry is HTMLElement => Boolean(entry))
439
- )
440
- );
441
- const mountedElementsAfterButtons = $derived.by(() =>
442
- renderedTools.flatMap((renderedTool) =>
443
- (renderedTool.elements || [])
444
- .filter((entry) => entry.mount === 'after-buttons')
445
- .map((entry) => entry.element)
446
- .filter((entry): entry is HTMLElement => Boolean(entry))
447
- )
448
- );
449
-
450
- function isToolbarItemActive(item: ToolbarItem): boolean {
451
- if (item.id in activeToolState) {
452
- return activeToolState[item.id] === true;
453
- }
454
- return item.active === true;
455
- }
456
-
457
- function getFallbackIconSvg(iconName: string): string | null {
458
- const iconMap: Record<string, string> = {
459
- calculator:
460
- '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M7 2h10a2 2 0 0 1 2 2v16a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2Zm0 2v4h10V4H7Zm0 6v2h2v-2H7Zm4 0v2h2v-2h-2Zm4 0v2h2v-2h-2Zm-8 4v2h2v-2H7Zm4 0v2h2v-2h-2Zm4 0v2h2v-2h-2Zm-8 4v2h2v-2H7Zm4 0v2h2v-2h-2Zm4 0v2h2v-2h-2Z" fill="currentColor"/></svg>',
461
- 'volume-up':
462
- '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M14 3.23v2.06A7.002 7.002 0 0 1 19 12a7 7 0 0 1-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77Zm-2 17.75V3L7 8H3v8h4l5 5Zm4.5-9a4.5 4.5 0 0 0-2.5-4.03v8.05A4.5 4.5 0 0 0 16.5 12Z" fill="currentColor"/></svg>',
463
- swatch:
464
- '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 3a9 9 0 1 0 9 9c0-.55-.45-1-1-1h-2.5a1.5 1.5 0 0 1 0-3H20a1 1 0 0 0 1-1 8.99 8.99 0 0 0-9-4Zm-5.5 9A1.5 1.5 0 1 1 8 13.5 1.5 1.5 0 0 1 6.5 12Zm3-4A1.5 1.5 0 1 1 11 9.5 1.5 1.5 0 0 1 9.5 8Zm5 0A1.5 1.5 0 1 1 16 9.5 1.5 1.5 0 0 1 14.5 8Z" fill="currentColor"/></svg>',
465
- 'chart-bar':
466
- '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M4.75 5a.76.76 0 0 1 .75.75v11c0 .438.313.75.75.75h13a.76.76 0 0 1 .696 1.039.74.74 0 0 1-.696.461h-13C5 19 4 18 4 16.75v-11A.74.74 0 0 1 4.75 5ZM8 8.25a.74.74 0 0 1 .75-.75h6.5a.76.76 0 0 1 .696 1.039.74.74 0 0 1-.696.461h-6.5A.722.722 0 0 1 8 8.25Zm.75 2.25h4.5a.76.76 0 0 1 .696 1.039.74.74 0 0 1-.696.461h-4.5a.723.723 0 0 1-.75-.75.74.74 0 0 1 .75-.75Zm0 3h8.5a.76.76 0 0 1 .696 1.039.74.74 0 0 1-.696.461h-8.5a.723.723 0 0 1-.75-.75.74.74 0 0 1 .75-.75Z" fill="currentColor"/></svg>',
467
- beaker:
468
- '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M5 21c-.85 0-1.454-.38-1.813-1.137-.358-.759-.27-1.463.263-2.113L9 11V5H8a.968.968 0 0 1-.713-.287A.968.968 0 0 1 7 4c0-.283.096-.52.287-.712A.968.968 0 0 1 8 3h8c.283 0 .52.096.712.288.192.191.288.429.288.712s-.096.52-.288.713A.968.968 0 0 1 16 5h-1v6l5.55 6.75c.533.65.62 1.354.262 2.113C20.454 20.62 19.85 21 19 21H5Zm2-3h10l-3.4-4h-3.2L7 18Zm-2 1h14l-6-7.3V5h-2v6.7L5 19Z" fill="currentColor"/></svg>',
469
- protractor:
470
- '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="m6.75 21-.25-2.2 2.85-7.85a3.95 3.95 0 0 0 1.75.95l-2.75 7.55L6.75 21Zm10.5 0-1.6-1.55-2.75-7.55a3.948 3.948 0 0 0 1.75-.95l2.85 7.85-.25 2.2ZM12 11a2.893 2.893 0 0 1-2.125-.875A2.893 2.893 0 0 1 9 8c0-.65.188-1.23.563-1.737A2.935 2.935 0 0 1 11 5.2V3h2v2.2c.583.2 1.063.554 1.438 1.063C14.812 6.77 15 7.35 15 8c0 .833-.292 1.542-.875 2.125A2.893 2.893 0 0 1 12 11Zm0-2c.283 0 .52-.096.713-.287A.967.967 0 0 0 13 8a.967.967 0 0 0-.287-.713A.968.968 0 0 0 12 7a.968.968 0 0 0-.713.287A.967.967 0 0 0 11 8c0 .283.096.52.287.713.192.191.43.287.713.287Z" fill="currentColor"/></svg>',
471
- 'bars-3':
472
- '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M6.85 15c.517 0 .98-.15 1.388-.45.408-.3.695-.692.862-1.175l.375-1.15c.267-.8.2-1.537-.2-2.213C8.875 9.337 8.3 9 7.55 9H4.025l.475 3.925c.083.583.346 1.075.787 1.475.442.4.963.6 1.563.6Zm10.3 0c.6 0 1.12-.2 1.563-.6.441-.4.704-.892.787-1.475L19.975 9h-3.5c-.75 0-1.325.342-1.725 1.025-.4.683-.467 1.425-.2 2.225l.35 1.125c.167.483.454.875.862 1.175.409.3.871.45 1.388.45Zm-10.3 2c-1.1 0-2.063-.363-2.887-1.088a4.198 4.198 0 0 1-1.438-2.737L2 9H1V7h6.55c.733 0 1.404.18 2.013.537A3.906 3.906 0 0 1 11 9h2.025c.35-.617.83-1.104 1.438-1.463A3.892 3.892 0 0 1 16.474 7H23v2h-1l-.525 4.175a4.198 4.198 0 0 1-1.438 2.737A4.238 4.238 0 0 1 17.15 17c-.95 0-1.804-.27-2.562-.813A4.234 4.234 0 0 1 13 14.026l-.375-1.125a21.35 21.35 0 0 1-.1-.363 4.926 4.926 0 0 1-.1-.537h-.85c-.033.2-.067.363-.1.488a21.35 21.35 0 0 1-.1.362L11 14a4.3 4.3 0 0 1-1.588 2.175A4.258 4.258 0 0 1 6.85 17Z" fill="currentColor"/></svg>',
473
- ruler:
474
- '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="m8.8 10.95 2.15-2.175-1.4-1.425-1.1 1.1-1.4-1.4 1.075-1.1L7 4.825 4.825 7 8.8 10.95Zm8.2 8.225L19.175 17l-1.125-1.125-1.1 1.075-1.4-1.4 1.075-1.1-1.425-1.4-2.15 2.15L17 19.175ZM7.25 21H3v-4.25l4.375-4.375L2 7l5-5 5.4 5.4 3.775-3.8c.2-.2.425-.35.675-.45a2.068 2.068 0 0 1 1.55 0c.25.1.475.25.675.45L20.4 4.95c.2.2.35.425.45.675.1.25.15.508.15.775a1.975 1.975 0 0 1-.6 1.425l-3.775 3.8L22 17l-5 5-5.375-5.375L7.25 21ZM5 19h1.4l9.8-9.775L14.775 7.8 5 17.6V19Z" fill="currentColor"/></svg>'
475
- };
476
- return iconMap[iconName] || null;
477
- }
478
-
479
- function syncRenderedToolsState() {
480
- const nextActiveState: Record<string, boolean> = {};
481
- for (const renderedTool of renderedTools) {
482
- renderedTool.sync?.();
483
- if (renderedTool.button) {
484
- nextActiveState[renderedTool.toolId] = renderedTool.button.active ?? false;
485
- }
486
- }
487
- activeToolState = nextActiveState;
488
- }
489
-
490
- $effect(() => {
491
- const unsubs: Array<() => void> = [];
492
- syncRenderedToolsState();
493
- for (const renderedTool of renderedTools) {
494
- if (renderedTool.subscribeActive) {
495
- const unsubscribe = renderedTool.subscribeActive((active) => {
496
- activeToolState = {
497
- ...activeToolState,
498
- [renderedTool.toolId]: active
499
- };
500
- renderedTool.sync?.();
501
- });
502
- unsubs.push(unsubscribe);
503
- }
504
- }
505
-
506
- return () => {
507
- unsubs.forEach((unsub) => unsub());
508
- };
509
- });
510
-
511
- $effect(() => {
512
- if (!toolbarContext.subscribeVisibility) return;
513
- return toolbarContext.subscribeVisibility(() => {
514
- syncRenderedToolsState();
515
- });
516
- });
517
-
518
- function mountElement(node: HTMLSpanElement, element: HTMLElement | null) {
519
- let mountedElement: HTMLElement | null = null;
520
- const updateMountedElement = (nextElement: HTMLElement | null) => {
521
- if (mountedElement === nextElement) return;
522
- if (mountedElement && mountedElement.parentNode === node) {
523
- node.removeChild(mountedElement);
524
- }
525
- mountedElement = nextElement;
526
- if (mountedElement) {
527
- if (mountedElement.parentNode && mountedElement.parentNode !== node) {
528
- mountedElement.parentNode.removeChild(mountedElement);
529
- }
530
- node.appendChild(mountedElement);
531
- }
532
- };
533
- updateMountedElement(element);
534
- return {
535
- update(nextElement: HTMLElement | null) {
536
- updateMountedElement(nextElement);
537
- },
538
- destroy() {
539
- updateMountedElement(null);
540
- }
541
- };
542
- }
543
- </script>
544
-
545
- {#if isBrowser}
546
- <div
547
- class="item-toolbar {className} item-toolbar--{size}"
548
- class:item-toolbar--top={position === 'top'}
549
- class:item-toolbar--right={position === 'right'}
550
- class:item-toolbar--bottom={position === 'bottom'}
551
- class:item-toolbar--left={position === 'left'}
552
- data-content-kind={effectiveContentKind}
553
- data-level={effectiveLevel}
554
- bind:this={toolbarRootElement}
555
- >
556
- {#each mountedElementsBeforeButtons as mountedElement (`before-${mountedElement.tagName}-${mountedElement.getAttribute('tool-id') || ''}`)}
557
- <span class="item-toolbar__element-host" use:mountElement={mountedElement}></span>
558
- {/each}
559
-
560
- {#each toolbarItems as item (item.id)}
561
- {#if isToolbarLinkItem(item)}
562
- <a
563
- class="item-toolbar__button"
564
- class:item-toolbar__button--active={isToolbarItemActive(item)}
565
- href={item.disabled ? undefined : item.href}
566
- target={item.target}
567
- rel={item.rel}
568
- aria-label={item.ariaLabel || item.label}
569
- title={item.tooltip || item.label}
570
- aria-disabled={item.disabled ? 'true' : undefined}
571
- onclick={(event) => {
572
- if (item.disabled) {
573
- event.preventDefault();
574
- }
575
- }}
576
- >
577
- {#if item.icon}
578
- {#if isInlineSvgIcon(item.icon)}
579
- {@html item.icon}
580
- {:else if isExternalIconUrl(item.icon)}
581
- <img class="item-toolbar__icon-image" src={item.icon} alt="" />
582
- {:else}
583
- {@const fallbackIcon = getFallbackIconSvg(item.icon)}
584
- {#if fallbackIcon}
585
- {@html fallbackIcon}
586
- {:else}
587
- <i class={`icon icon-${item.icon}`} aria-hidden="true"></i>
588
- {/if}
589
- {/if}
590
- {/if}
591
- </a>
592
- {:else}
593
- <button
594
- type="button"
595
- class="item-toolbar__button"
596
- class:item-toolbar__button--active={isToolbarItemActive(item)}
597
- onclick={item.onClick}
598
- aria-label={item.ariaLabel || item.label}
599
- aria-pressed={isToolbarItemActive(item)}
600
- title={item.tooltip || item.label}
601
- disabled={item.disabled}
602
- >
603
- {#if item.icon}
604
- {#if isInlineSvgIcon(item.icon)}
605
- {@html item.icon}
606
- {:else if isExternalIconUrl(item.icon)}
607
- <img class="item-toolbar__icon-image" src={item.icon} alt="" />
608
- {:else}
609
- {@const fallbackIcon = getFallbackIconSvg(item.icon)}
610
- {#if fallbackIcon}
611
- {@html fallbackIcon}
612
- {:else}
613
- <i class={`icon icon-${item.icon}`} aria-hidden="true"></i>
614
- {/if}
615
- {/if}
616
- {/if}
617
- </button>
618
- {/if}
619
- {/each}
620
-
621
- {#each mountedElementsAfterButtons as mountedElement (`after-${mountedElement.tagName}-${mountedElement.getAttribute('tool-id') || ''}`)}
622
- <span class="item-toolbar__element-host" use:mountElement={mountedElement}></span>
623
- {/each}
624
- </div>
625
- {/if}
626
-
627
- <style>
628
- .item-toolbar {
629
- display: flex;
630
- align-items: center;
631
- gap: 0.5rem;
632
- }
633
-
634
- .item-toolbar--top,
635
- .item-toolbar--bottom {
636
- flex-direction: row;
637
- flex-wrap: wrap;
638
- }
639
-
640
- .item-toolbar--left,
641
- .item-toolbar--right {
642
- flex-direction: column;
643
- align-items: stretch;
644
- }
645
-
646
- .item-toolbar__button {
647
- display: flex;
648
- align-items: center;
649
- justify-content: center;
650
- width: 2rem;
651
- height: 2rem;
652
- padding: 0.25rem;
653
- border: 1px solid var(--pie-border, #ccc);
654
- border-radius: 0.25rem;
655
- background-color: var(--pie-background, white);
656
- color: var(--pie-text, #333);
657
- cursor: pointer;
658
- transition: all 0.15s ease;
659
- text-decoration: none;
660
- }
661
-
662
- .item-toolbar__element-host {
663
- display: contents;
664
- }
665
-
666
- .item-toolbar--sm .item-toolbar__button {
667
- width: 2.75rem;
668
- height: 2.75rem;
669
- }
670
-
671
- .item-toolbar--lg .item-toolbar__button {
672
- width: 2.5rem;
673
- height: 2.5rem;
674
- }
675
-
676
- .item-toolbar__button:hover:not(:disabled) {
677
- background-color: var(--pie-secondary-background, #f5f5f5);
678
- transform: translateY(-1px);
679
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
680
- }
681
-
682
- .item-toolbar__button:active:not(:disabled) {
683
- transform: translateY(0);
684
- box-shadow: none;
685
- }
686
-
687
- .item-toolbar__button--active {
688
- background-color: var(--pie-primary, #1976d2);
689
- color: white;
690
- border-color: var(--pie-primary, #1976d2);
691
- }
692
-
693
- .item-toolbar__button--active:hover:not(:disabled) {
694
- background-color: var(--pie-primary-dark, #1565c0);
695
- }
696
-
697
- .item-toolbar__button:focus-visible {
698
- outline: 2px solid var(--pie-primary, #1976d2);
699
- outline-offset: 2px;
700
- }
701
-
702
- .item-toolbar__button:disabled {
703
- opacity: 0.5;
704
- cursor: not-allowed;
705
- }
706
-
707
- .item-toolbar__button :global(svg) {
708
- width: 100%;
709
- height: 100%;
710
- }
711
-
712
- .item-toolbar__icon-image {
713
- width: 100%;
714
- height: 100%;
715
- object-fit: contain;
716
- }
717
- </style>