@pie-players/pie-section-tools-toolbar 0.2.9 → 0.2.10

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/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@pie-players/pie-section-tools-toolbar",
3
- "version": "0.2.9",
3
+ "version": "0.2.10",
4
4
  "type": "module",
5
5
  "description": "Section-level tools toolbar for PIE assessment player",
6
6
  "repository": {
7
7
  "type": "git",
8
- "url": "git+https://github.com/pie-framework/pie-players.git"
8
+ "url": "git+https://github.com/pie-framework/pie-players.git",
9
+ "directory": "packages/section-tools-toolbar"
9
10
  },
10
11
  "publishConfig": {
11
12
  "access": "public"
@@ -40,16 +41,10 @@
40
41
  "unpkg": "./dist/section-tools-toolbar.js",
41
42
  "jsdelivr": "./dist/section-tools-toolbar.js",
42
43
  "dependencies": {
43
- "@pie-players/pie-assessment-toolkit": "0.2.7",
44
- "@pie-players/pie-calculator-mathjs": "0.1.3",
45
- "@pie-players/pie-players-shared": "0.2.4",
46
- "@pie-players/pie-tool-graph": "0.1.8",
47
- "@pie-players/pie-tool-line-reader": "0.1.8",
48
- "@pie-players/pie-tool-magnifier": "0.1.8",
49
- "@pie-players/pie-tool-periodic-table": "0.1.8",
50
- "@pie-players/pie-tool-protractor": "0.1.8",
51
- "@pie-players/pie-tool-ruler": "0.1.8",
52
- "@sveltejs/kit": "^2.52.0",
44
+ "@pie-players/pie-assessment-toolkit": "0.2.9",
45
+ "@pie-players/pie-context": "0.1.1",
46
+ "@pie-players/pie-default-tool-loaders": "0.1.1",
47
+ "@pie-players/pie-calculator-mathjs": "0.1.4",
53
48
  "daisyui": "^5.5.18"
54
49
  },
55
50
  "types": "./dist/index.d.ts",
@@ -66,5 +61,13 @@
66
61
  "typescript": "^5.7.0",
67
62
  "vite": "^7.0.8",
68
63
  "vite-plugin-dts": "^4.5.3"
69
- }
64
+ },
65
+ "homepage": "https://github.com/pie-framework/pie-players/tree/master/packages/section-tools-toolbar#readme",
66
+ "bugs": {
67
+ "url": "https://github.com/pie-framework/pie-players/issues"
68
+ },
69
+ "engines": {
70
+ "node": ">=18.0.0"
71
+ },
72
+ "sideEffects": true
70
73
  }
@@ -5,9 +5,7 @@
5
5
  props: {
6
6
  enabledTools: { type: 'String', attribute: 'enabled-tools' },
7
7
  position: { type: 'String', attribute: 'position' },
8
- // Services passed as JS properties (not attributes)
9
- toolCoordinator: { type: 'Object', reflect: false },
10
- toolProviderRegistry: { type: 'Object', reflect: false }
8
+ toolCoordinator: { type: 'Object', reflect: false }
11
9
  }
12
10
  }}
13
11
  />
@@ -28,67 +26,123 @@
28
26
  Similar to SchoolCity pattern - section-wide tools independent of item navigation.
29
27
  -->
30
28
  <script lang="ts">
29
+ import {
30
+ connectAssessmentToolkitRuntimeContext,
31
+ createDefaultToolRegistry,
32
+ normalizeToolList,
33
+ normalizeToolsConfig,
34
+ parseToolList,
35
+ resolveToolsForLevel,
36
+ createScopedToolId,
37
+ } from '@pie-players/pie-assessment-toolkit';
31
38
  import type {
39
+ AssessmentToolkitRuntimeContext,
32
40
  IToolCoordinator,
33
- ToolProviderRegistry,
41
+ ToolContext,
34
42
  } from '@pie-players/pie-assessment-toolkit';
43
+ import { registerSectionToolModuleLoaders } from '@pie-players/pie-default-tool-loaders';
35
44
  import { onDestroy, onMount } from 'svelte';
36
45
 
37
46
  const isBrowser = typeof window !== 'undefined';
47
+ const toolRegistry = createDefaultToolRegistry();
48
+ registerSectionToolModuleLoaders(toolRegistry);
38
49
 
39
50
  // Props
40
51
  let {
41
- enabledTools = 'graph,periodicTable,protractor,lineReader,magnifier,ruler',
52
+ enabledTools = '',
42
53
  position = 'bottom',
43
- toolCoordinator,
44
- toolProviderRegistry
54
+ toolCoordinator = null as IToolCoordinator | null
45
55
  }: {
46
56
  enabledTools?: string;
47
57
  position?: 'top' | 'right' | 'bottom' | 'left' | 'none';
48
- toolCoordinator?: IToolCoordinator;
49
- toolProviderRegistry?: ToolProviderRegistry;
58
+ toolCoordinator?: IToolCoordinator | null;
50
59
  } = $props();
60
+ let toolbarRootElement = $state<HTMLElement | null>(null);
61
+ let runtimeContext = $state<AssessmentToolkitRuntimeContext | null>(null);
62
+ const effectiveToolCoordinator = $derived(
63
+ (toolCoordinator as IToolCoordinator | null) ||
64
+ (runtimeContext?.toolCoordinator as IToolCoordinator | undefined),
65
+ );
51
66
 
52
- // Parse enabled tools from comma-separated string
67
+ const effectiveSectionId = $derived(runtimeContext?.sectionId || 'default');
68
+ const requestedEnabledToolsList = $derived(parseToolList(enabledTools));
69
+ const effectiveToolsConfig = $derived.by(() => {
70
+ const coordinatorConfig = runtimeContext?.toolkitCoordinator?.config?.tools as any;
71
+ return normalizeToolsConfig(coordinatorConfig || {});
72
+ });
73
+ const placementTools = $derived.by(() =>
74
+ normalizeToolList(resolveToolsForLevel(effectiveToolsConfig, 'section'))
75
+ );
53
76
  let enabledToolsList = $derived(
54
- enabledTools
55
- .split(',')
56
- .map((t) => t.trim())
57
- .filter(Boolean)
77
+ requestedEnabledToolsList.length > 0
78
+ ? requestedEnabledToolsList
79
+ : placementTools
58
80
  );
59
81
  let hasEnabledTools = $derived(enabledToolsList.length > 0);
82
+ const sectionToolContext = $derived.by((): ToolContext => ({
83
+ level: 'section',
84
+ assessment: {} as any,
85
+ section: {} as any,
86
+ }));
60
87
 
61
88
  // Tool visibility state (reactive to coordinator changes)
62
89
  let showGraph = $state(false);
90
+ let showCalculator = $state(false);
63
91
  let showPeriodicTable = $state(false);
64
92
  let showProtractor = $state(false);
65
93
  let showLineReader = $state(false);
66
- let showMagnifier = $state(false);
67
94
  let showRuler = $state(false);
95
+ let toolActiveById = $state<Record<string, boolean>>({});
68
96
  let statusMessage = $state('');
97
+ type SectionButtonMeta = {
98
+ toolId: string;
99
+ label: string;
100
+ ariaLabel: string;
101
+ tooltip?: string;
102
+ icon: string;
103
+ onClick: () => void;
104
+ };
105
+
106
+ function toScopedToolId(toolId: string): string {
107
+ return createScopedToolId(toolId, 'section', effectiveSectionId);
108
+ }
69
109
 
70
110
  // Update visibility state from coordinator
71
111
  function updateToolVisibility() {
72
- if (!toolCoordinator) return;
73
- showGraph = toolCoordinator.isToolVisible('graph');
74
- showPeriodicTable = toolCoordinator.isToolVisible('periodicTable');
75
- showProtractor = toolCoordinator.isToolVisible('protractor');
76
- showLineReader = toolCoordinator.isToolVisible('lineReader');
77
- showMagnifier = toolCoordinator.isToolVisible('magnifier');
78
- showRuler = toolCoordinator.isToolVisible('ruler');
112
+ if (!effectiveToolCoordinator) return;
113
+ showCalculator = effectiveToolCoordinator.isToolVisible(toScopedToolId('calculator'));
114
+ showGraph = effectiveToolCoordinator.isToolVisible(toScopedToolId('graph'));
115
+ showPeriodicTable = effectiveToolCoordinator.isToolVisible(toScopedToolId('periodicTable'));
116
+ showProtractor = effectiveToolCoordinator.isToolVisible(toScopedToolId('protractor'));
117
+ showLineReader = effectiveToolCoordinator.isToolVisible(toScopedToolId('lineReader'));
118
+ showRuler = effectiveToolCoordinator.isToolVisible(toScopedToolId('ruler'));
119
+ toolActiveById = {
120
+ calculator: showCalculator,
121
+ graph: showGraph,
122
+ periodicTable: showPeriodicTable,
123
+ protractor: showProtractor,
124
+ lineReader: showLineReader,
125
+ ruler: showRuler
126
+ };
127
+ }
128
+
129
+ function isToolActive(toolId: string): boolean {
130
+ return toolActiveById[toolId] === true;
79
131
  }
80
132
 
81
133
  // Toggle tool visibility
82
- function toggleTool(toolId: string) {
83
- if (!toolCoordinator) return;
84
- toolCoordinator.toggleTool(toolId);
134
+ async function toggleTool(toolId: string) {
135
+ if (!effectiveToolCoordinator) return;
136
+ await toolRegistry.ensureToolModuleLoaded(toolId);
137
+ const scopedToolId = toScopedToolId(toolId);
138
+ effectiveToolCoordinator.toggleTool(scopedToolId);
85
139
  updateToolVisibility();
86
140
 
87
141
  // Get tool name for status message
88
- const tool = toolButtons.find(t => t.id === toolId);
142
+ const tool = visibleButtons.find((t) => t.toolId === toolId);
89
143
  if (tool) {
90
- const isVisible = toolCoordinator.isToolVisible(toolId);
91
- statusMessage = `${tool.label} ${isVisible ? 'opened' : 'closed'}`;
144
+ const isVisible = effectiveToolCoordinator.isToolVisible(scopedToolId);
145
+ statusMessage = `${tool.ariaLabel} ${isVisible ? 'opened' : 'closed'}`;
92
146
  }
93
147
  }
94
148
 
@@ -96,120 +150,76 @@
96
150
  let unsubscribe: (() => void) | null = null;
97
151
 
98
152
  onMount(() => {
99
- if (isBrowser && hasEnabledTools) {
100
- const toolModules: Record<string, string> = {
101
- calculator: '@pie-players/pie-tool-calculator',
102
- graph: '@pie-players/pie-tool-graph',
103
- periodicTable: '@pie-players/pie-tool-periodic-table',
104
- protractor: '@pie-players/pie-tool-protractor',
105
- lineReader: '@pie-players/pie-tool-line-reader',
106
- magnifier: '@pie-players/pie-tool-magnifier',
107
- ruler: '@pie-players/pie-tool-ruler'
108
- };
109
-
110
- Promise.all(
111
- enabledToolsList
112
- .map((toolId) => toolModules[toolId])
113
- .filter(Boolean)
114
- .map((moduleId) => import(moduleId))
115
- ).catch((err) => {
116
- console.error('[SectionToolsToolbar] Failed to load tool web components:', err);
117
- });
118
- }
153
+ updateToolVisibility();
154
+ });
155
+
156
+ $effect(() => {
157
+ if (!toolbarRootElement) return;
158
+ return connectAssessmentToolkitRuntimeContext(
159
+ toolbarRootElement,
160
+ (value: AssessmentToolkitRuntimeContext) => {
161
+ runtimeContext = value;
162
+ },
163
+ );
164
+ });
165
+
166
+ $effect(() => {
167
+ unsubscribe?.();
168
+ unsubscribe = null;
169
+ if (!effectiveToolCoordinator) return;
119
170
 
120
- if (toolCoordinator) {
171
+ updateToolVisibility();
172
+ unsubscribe = effectiveToolCoordinator.subscribe(() => {
121
173
  updateToolVisibility();
122
- unsubscribe = toolCoordinator.subscribe(() => {
123
- updateToolVisibility();
124
- });
125
- }
174
+ });
175
+
176
+ return () => {
177
+ unsubscribe?.();
178
+ unsubscribe = null;
179
+ };
126
180
  });
127
181
 
128
182
  onDestroy(() => {
129
183
  unsubscribe?.();
130
184
  });
131
185
 
132
- // Tool button definitions
133
- const toolButtons = $derived([
134
- {
135
- id: 'graph',
136
- ariaLabel: 'Graphing tool',
137
- visible: showGraph,
138
- enabled: enabledToolsList.includes('graph'),
139
- svg: '<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>'
140
- },
141
- {
142
- id: 'periodicTable',
143
- ariaLabel: 'Periodic table of elements',
144
- visible: showPeriodicTable,
145
- enabled: enabledToolsList.includes('periodicTable'),
146
- svg: '<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>'
147
- },
148
- {
149
- id: 'protractor',
150
- ariaLabel: 'Angle measurement tool',
151
- visible: showProtractor,
152
- enabled: enabledToolsList.includes('protractor'),
153
- svg: '<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>'
154
- },
155
- {
156
- id: 'lineReader',
157
- ariaLabel: 'Line reading guide',
158
- visible: showLineReader,
159
- enabled: enabledToolsList.includes('lineReader'),
160
- svg: '<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>'
161
- },
162
- {
163
- id: 'magnifier',
164
- ariaLabel: 'Text magnification tool',
165
- visible: showMagnifier,
166
- enabled: enabledToolsList.includes('magnifier'),
167
- svg: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M10.5 5.5c-1.813 0-3.438.969-4.344 2.5a4.937 4.937 0 0 0 0 5 4.974 4.974 0 0 0 4.344 2.5 4.96 4.96 0 0 0 4.313-2.5 4.937 4.937 0 0 0 0-5c-.908-1.531-2.533-2.5-4.313-2.5Zm0 11.5A6.495 6.495 0 0 1 4 10.5C4 6.937 6.906 4 10.5 4c3.563 0 6.5 2.938 6.5 6.5a6.597 6.597 0 0 1-1.406 4.063l4.156 4.187a.685.685 0 0 1 0 1.031.685.685 0 0 1-1.031 0l-4.188-4.156A6.548 6.548 0 0 1 10.5 17Zm-.75-3.75v-2h-2A.723.723 0 0 1 7 10.5a.74.74 0 0 1 .75-.75h2v-2A.74.74 0 0 1 10.5 7a.76.76 0 0 1 .75.75v2h2a.76.76 0 0 1 .696 1.039.741.741 0 0 1-.696.461h-2v2a.74.74 0 0 1-.75.75.723.723 0 0 1-.75-.75Z" fill="currentColor"/></svg>'
168
- },
169
- {
170
- id: 'ruler',
171
- ariaLabel: 'Measurement ruler',
172
- visible: showRuler,
173
- enabled: enabledToolsList.includes('ruler'),
174
- svg: '<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>'
175
- }
176
- ]);
186
+ const visibleButtons = $derived.by((): SectionButtonMeta[] => {
187
+ return enabledToolsList
188
+ .map((toolId: string) => toolRegistry.get(toolId))
189
+ .filter((tool): tool is NonNullable<ReturnType<typeof toolRegistry.get>> => Boolean(tool))
190
+ .map((tool) => {
191
+ const icon = typeof tool.icon === 'function' ? tool.icon(sectionToolContext) : tool.icon;
192
+ return {
193
+ toolId: tool.toolId,
194
+ label: tool.name,
195
+ ariaLabel: tool.name,
196
+ tooltip: tool.name,
197
+ icon,
198
+ onClick: () => {
199
+ void toggleTool(tool.toolId);
200
+ },
201
+ };
202
+ });
203
+ });
177
204
 
178
- // Tool element references for service binding
179
- let graphElement = $state<HTMLElement | null>(null);
180
- let periodicTableElement = $state<HTMLElement | null>(null);
181
- let protractorElement = $state<HTMLElement | null>(null);
182
- let lineReaderElement = $state<HTMLElement | null>(null);
183
- let magnifierElement = $state<HTMLElement | null>(null);
184
- let rulerElement = $state<HTMLElement | null>(null);
205
+ function resolveIconMarkup(icon: string): string {
206
+ if (icon.startsWith('<svg')) return icon;
207
+ const iconMap: Record<string, string> = {
208
+ calculator: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M7,2H17A2,2 0 0,1 19,4V20A2,2 0 0,1 17,22H7A2,2 0 0,1 5,20V4A2,2 0 0,1 7,2M7,4V8H17V4H7M7,10V12H9V10H7M11,10V12H13V10H11M15,10V12H17V10H15M7,14V16H9V14H7M11,14V16H13V14H11M15,14V16H17V14H15M7,18V20H9V18H7M11,18V20H13V18H11M15,18V20H17V18H15Z" fill="currentColor"/></svg>',
209
+ 'chart-bar': '<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>',
210
+ beaker: '<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>',
211
+ protractor: '<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>',
212
+ 'bars-3': '<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>',
213
+ ruler: '<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>',
214
+ };
215
+ return iconMap[icon] || '';
216
+ }
185
217
 
186
- // Bind coordinator to tool elements
187
- $effect(() => {
188
- if (toolCoordinator) {
189
- if (graphElement) {
190
- (graphElement as any).coordinator = toolCoordinator;
191
- }
192
- if (periodicTableElement) {
193
- (periodicTableElement as any).coordinator = toolCoordinator;
194
- }
195
- if (protractorElement) {
196
- (protractorElement as any).coordinator = toolCoordinator;
197
- }
198
- if (lineReaderElement) {
199
- (lineReaderElement as any).coordinator = toolCoordinator;
200
- }
201
- if (magnifierElement) {
202
- (magnifierElement as any).coordinator = toolCoordinator;
203
- }
204
- if (rulerElement) {
205
- (rulerElement as any).coordinator = toolCoordinator;
206
- }
207
- }
208
- });
209
218
  </script>
210
219
 
211
220
  {#if isBrowser && position !== 'none' && hasEnabledTools}
212
221
  <div
222
+ bind:this={toolbarRootElement}
213
223
  class="section-tools-toolbar section-tools-toolbar--{position}"
214
224
  class:section-tools-toolbar--top={position === 'top'}
215
225
  class:section-tools-toolbar--right={position === 'right'}
@@ -220,20 +230,18 @@
220
230
  aria-label="Assessment tools"
221
231
  >
222
232
  <div class="tools-buttons">
223
- {#each toolButtons as tool (tool.id)}
224
- {#if tool.enabled}
225
- <button
226
- type="button"
227
- class="tool-button"
228
- class:active={tool.visible}
229
- onclick={() => toggleTool(tool.id)}
230
- title={tool.ariaLabel}
231
- aria-label={tool.ariaLabel}
232
- aria-pressed={tool.visible}
233
- >
234
- {@html tool.svg}
235
- </button>
236
- {/if}
233
+ {#each visibleButtons as button (button.toolId)}
234
+ <button
235
+ type="button"
236
+ class="tool-button"
237
+ class:active={isToolActive(button.toolId)}
238
+ onclick={button.onClick}
239
+ title={button.tooltip || button.label}
240
+ aria-label={button.ariaLabel}
241
+ aria-pressed={isToolActive(button.toolId)}
242
+ >
243
+ {@html resolveIconMarkup(button.icon)}
244
+ </button>
237
245
  {/each}
238
246
  </div>
239
247
  </div>
@@ -243,49 +251,44 @@
243
251
 
244
252
  {#if enabledToolsList.includes('graph')}
245
253
  <pie-tool-graph
246
- bind:this={graphElement}
247
254
  visible={showGraph}
248
- tool-id="graph"
255
+ tool-id={toScopedToolId('graph')}
249
256
  ></pie-tool-graph>
250
257
  {/if}
251
258
 
259
+ {#if enabledToolsList.includes('calculator')}
260
+ <pie-tool-calculator
261
+ visible={showCalculator}
262
+ tool-id={toScopedToolId('calculator')}
263
+ calculator-type="scientific"
264
+ ></pie-tool-calculator>
265
+ {/if}
266
+
252
267
  {#if enabledToolsList.includes('periodicTable')}
253
268
  <pie-tool-periodic-table
254
- bind:this={periodicTableElement}
255
269
  visible={showPeriodicTable}
256
- tool-id="periodicTable"
270
+ tool-id={toScopedToolId('periodicTable')}
257
271
  ></pie-tool-periodic-table>
258
272
  {/if}
259
273
 
260
274
  {#if enabledToolsList.includes('protractor')}
261
275
  <pie-tool-protractor
262
- bind:this={protractorElement}
263
276
  visible={showProtractor}
264
- tool-id="protractor"
277
+ tool-id={toScopedToolId('protractor')}
265
278
  ></pie-tool-protractor>
266
279
  {/if}
267
280
 
268
281
  {#if enabledToolsList.includes('lineReader')}
269
282
  <pie-tool-line-reader
270
- bind:this={lineReaderElement}
271
283
  visible={showLineReader}
272
- tool-id="lineReader"
284
+ tool-id={toScopedToolId('lineReader')}
273
285
  ></pie-tool-line-reader>
274
286
  {/if}
275
287
 
276
- {#if enabledToolsList.includes('magnifier')}
277
- <pie-tool-magnifier
278
- bind:this={magnifierElement}
279
- visible={showMagnifier}
280
- tool-id="magnifier"
281
- ></pie-tool-magnifier>
282
- {/if}
283
-
284
288
  {#if enabledToolsList.includes('ruler')}
285
289
  <pie-tool-ruler
286
- bind:this={rulerElement}
287
290
  visible={showRuler}
288
- tool-id="ruler"
291
+ tool-id={toScopedToolId('ruler')}
289
292
  ></pie-tool-ruler>
290
293
  {/if}
291
294
 
@@ -374,7 +377,7 @@
374
377
  transition: all 0.15s ease;
375
378
  }
376
379
 
377
- .tool-button svg {
380
+ .tool-button :global(svg) {
378
381
  width: 100%;
379
382
  height: 100%;
380
383
  }