@pie-players/pie-section-player 0.2.13 → 0.2.14

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,6 +1,6 @@
1
1
  {
2
2
  "name": "@pie-players/pie-section-player",
3
- "version": "0.2.13",
3
+ "version": "0.2.14",
4
4
  "type": "module",
5
5
  "description": "Web component for rendering QTI 3.0 assessment sections with passages and items",
6
6
  "license": "MIT",
@@ -49,21 +49,22 @@
49
49
  "svelte": "^5.51.0"
50
50
  },
51
51
  "dependencies": {
52
- "@pie-players/pie-assessment-toolkit": "0.2.10",
53
- "@pie-players/pie-item-player": "0.1.1",
54
- "@pie-players/pie-context": "0.1.2",
55
- "@pie-players/pie-players-shared": "0.2.6",
56
- "@pie-players/pie-toolbars": "0.1.1",
57
- "@pie-players/pie-tool-answer-eliminator": "0.2.10",
58
- "@pie-players/pie-tool-calculator": "0.1.10",
59
- "@pie-players/pie-tool-theme": "0.1.10",
60
- "@pie-players/pie-tool-graph": "0.1.10",
61
- "@pie-players/pie-tool-line-reader": "0.1.10",
62
- "@pie-players/pie-tool-periodic-table": "0.1.10",
63
- "@pie-players/pie-tool-protractor": "0.1.10",
64
- "@pie-players/pie-tool-ruler": "0.1.10",
65
- "@pie-players/pie-tool-text-to-speech": "0.1.10",
66
- "@pie-players/tts-client-server": "0.2.5",
52
+ "@pie-players/pie-assessment-toolkit": "0.2.11",
53
+ "@pie-players/pie-item-player": "0.1.2",
54
+ "@pie-players/pie-context": "0.1.3",
55
+ "@pie-players/pie-players-shared": "0.2.7",
56
+ "@pie-players/pie-toolbars": "0.1.2",
57
+ "@pie-players/pie-tool-answer-eliminator": "0.2.11",
58
+ "@pie-players/pie-tool-annotation-toolbar": "0.1.11",
59
+ "@pie-players/pie-tool-calculator": "0.1.11",
60
+ "@pie-players/pie-tool-theme": "0.1.11",
61
+ "@pie-players/pie-tool-graph": "0.1.11",
62
+ "@pie-players/pie-tool-line-reader": "0.1.11",
63
+ "@pie-players/pie-tool-periodic-table": "0.1.11",
64
+ "@pie-players/pie-tool-protractor": "0.1.11",
65
+ "@pie-players/pie-tool-ruler": "0.1.11",
66
+ "@pie-players/pie-tool-text-to-speech": "0.1.11",
67
+ "@pie-players/tts-client-server": "0.2.6",
67
68
  "daisyui": "^5.5.18"
68
69
  },
69
70
  "devDependencies": {
@@ -13,8 +13,16 @@
13
13
  }}
14
14
  />
15
15
 
16
+ <script module lang="ts">
17
+ const crossShellSessionDedupe = new Map<
18
+ string,
19
+ { fingerprint: string; timestamp: number }
20
+ >();
21
+ </script>
22
+
16
23
  <script lang="ts">
17
24
  import {
25
+ PIE_INTERNAL_ITEM_SESSION_CHANGED_EVENT,
18
26
  PIE_ITEM_SESSION_CHANGED_EVENT,
19
27
  PIE_REGISTER_EVENT,
20
28
  PIE_UNREGISTER_EVENT,
@@ -23,11 +31,28 @@
23
31
  dispatchCrossBoundaryEvent,
24
32
  type AssessmentToolkitRegionScopeContext,
25
33
  type AssessmentToolkitShellContext,
34
+ type InternalItemSessionChangedDetail,
26
35
  type ItemSessionChangedDetail,
27
36
  type RuntimeRegistrationDetail,
28
37
  } from "@pie-players/pie-assessment-toolkit";
38
+ import { normalizeItemSessionChange } from "@pie-players/pie-players-shared";
29
39
  import { ContextProvider, ContextRoot } from "@pie-players/pie-context";
30
40
 
41
+ const PIE_INTERNAL_CONTENT_LOADED_EVENT = "pie-content-loaded";
42
+ const PIE_INTERNAL_ITEM_PLAYER_ERROR_EVENT = "pie-item-player-error";
43
+ type InternalContentLoadedDetail = {
44
+ itemId: string;
45
+ canonicalItemId?: string;
46
+ contentKind?: string;
47
+ detail?: unknown;
48
+ };
49
+ type InternalItemPlayerErrorDetail = {
50
+ itemId: string;
51
+ canonicalItemId?: string;
52
+ contentKind?: string;
53
+ error: unknown;
54
+ };
55
+
31
56
  let {
32
57
  itemId = "",
33
58
  canonicalItemId = "",
@@ -100,13 +125,64 @@
100
125
  function normalizeAndDispatchSession(event: Event): void {
101
126
  if (!host || !itemId) return;
102
127
  const detail = (event as CustomEvent).detail;
128
+ const internalPayload: InternalItemSessionChangedDetail = {
129
+ itemId,
130
+ session: detail,
131
+ };
132
+ // Internal runtime session wiring must always continue to keep item UI/state synchronized.
133
+ dispatchCrossBoundaryEvent(host, PIE_INTERNAL_ITEM_SESSION_CHANGED_EVENT, internalPayload);
134
+ // Keep public item stream response-focused; metadata-only belongs on canonical session-changed.
135
+ const normalized = normalizeItemSessionChange({
136
+ itemId,
137
+ sessionDetail: detail,
138
+ });
139
+ if (normalized.intent === "metadata-only" || !normalized.session) {
140
+ return;
141
+ }
103
142
  const payload: ItemSessionChangedDetail = {
104
143
  itemId,
105
144
  canonicalItemId: canonicalItemId || itemId,
106
- session: detail,
145
+ session: normalized.session,
107
146
  };
108
147
  dispatchCrossBoundaryEvent(host, PIE_ITEM_SESSION_CHANGED_EVENT, payload);
109
- dispatchCrossBoundaryEvent(host, "item-session-changed", payload);
148
+ }
149
+
150
+ function dispatchLoaded(detail: unknown): void {
151
+ if (!host || !itemId) return;
152
+ const payload: InternalContentLoadedDetail = {
153
+ itemId,
154
+ canonicalItemId: canonicalItemId || itemId,
155
+ contentKind,
156
+ detail,
157
+ };
158
+ dispatchCrossBoundaryEvent(host, PIE_INTERNAL_CONTENT_LOADED_EVENT, payload);
159
+ }
160
+
161
+ function dispatchPlayerError(error: unknown): void {
162
+ if (!host || !itemId) return;
163
+ const payload: InternalItemPlayerErrorDetail = {
164
+ itemId,
165
+ canonicalItemId: canonicalItemId || itemId,
166
+ contentKind,
167
+ error,
168
+ };
169
+ dispatchCrossBoundaryEvent(host, PIE_INTERNAL_ITEM_PLAYER_ERROR_EVENT, payload);
170
+ }
171
+
172
+ function createSessionEventFingerprint(detail: unknown): string {
173
+ const semanticDetail =
174
+ detail && typeof detail === "object"
175
+ ? { ...(detail as Record<string, unknown>) }
176
+ : detail;
177
+ if (semanticDetail && typeof semanticDetail === "object") {
178
+ delete (semanticDetail as Record<string, unknown>).timestamp;
179
+ delete (semanticDetail as Record<string, unknown>).sourceRuntimeId;
180
+ }
181
+ try {
182
+ return JSON.stringify(semanticDetail);
183
+ } catch {
184
+ return String(semanticDetail);
185
+ }
110
186
  }
111
187
 
112
188
  $effect(() => {
@@ -114,19 +190,50 @@
114
190
  dispatchRegistration(PIE_REGISTER_EVENT);
115
191
 
116
192
  const seenSessionEvents = new WeakSet<Event>();
193
+ let lastForwardedFingerprint = "";
194
+ const CROSS_SHELL_DEDUPE_WINDOW_MS = 500;
117
195
  const onSessionChanged = (event: Event) => {
196
+ // Keep framework surface minimal: normalize these descendant events and do not
197
+ // leak raw item-player session events to external listeners.
198
+ event.stopPropagation();
118
199
  // Some players emit `session-changed`, others emit `sessionchanged`.
119
- // Guard against duplicate forwarding when both fire for the same original event.
200
+ // Guard against duplicate forwarding when both fire for the same payload.
120
201
  if (seenSessionEvents.has(event)) return;
121
202
  seenSessionEvents.add(event);
203
+ const fingerprint = createSessionEventFingerprint((event as CustomEvent).detail);
204
+ if (fingerprint === lastForwardedFingerprint) return;
205
+ const dedupeKey = itemId || canonicalItemId || "__unknown-item__";
206
+ const now = Date.now();
207
+ const lastCrossShell = crossShellSessionDedupe.get(dedupeKey);
208
+ if (
209
+ lastCrossShell &&
210
+ lastCrossShell.fingerprint === fingerprint &&
211
+ now - lastCrossShell.timestamp < CROSS_SHELL_DEDUPE_WINDOW_MS
212
+ ) {
213
+ return;
214
+ }
215
+ crossShellSessionDedupe.set(dedupeKey, { fingerprint, timestamp: now });
216
+ lastForwardedFingerprint = fingerprint;
122
217
  normalizeAndDispatchSession(event);
123
218
  };
124
219
  host.addEventListener("sessionchanged", onSessionChanged);
125
220
  host.addEventListener("session-changed", onSessionChanged);
221
+ const onLoadComplete = (event: Event) => {
222
+ event.stopPropagation();
223
+ dispatchLoaded((event as CustomEvent).detail);
224
+ };
225
+ const onPlayerError = (event: Event) => {
226
+ event.stopPropagation();
227
+ dispatchPlayerError((event as CustomEvent).detail);
228
+ };
229
+ host.addEventListener("load-complete", onLoadComplete);
230
+ host.addEventListener("player-error", onPlayerError);
126
231
 
127
232
  return () => {
128
233
  host?.removeEventListener("sessionchanged", onSessionChanged);
129
234
  host?.removeEventListener("session-changed", onSessionChanged);
235
+ host?.removeEventListener("load-complete", onLoadComplete);
236
+ host?.removeEventListener("player-error", onPlayerError);
130
237
  dispatchRegistration(PIE_UNREGISTER_EVENT);
131
238
  };
132
239
  });
@@ -26,6 +26,21 @@
26
26
  } from "@pie-players/pie-assessment-toolkit";
27
27
  import { ContextProvider, ContextRoot } from "@pie-players/pie-context";
28
28
 
29
+ const PIE_INTERNAL_CONTENT_LOADED_EVENT = "pie-content-loaded";
30
+ const PIE_INTERNAL_ITEM_PLAYER_ERROR_EVENT = "pie-item-player-error";
31
+ type InternalContentLoadedDetail = {
32
+ itemId: string;
33
+ canonicalItemId?: string;
34
+ contentKind?: string;
35
+ detail?: unknown;
36
+ };
37
+ type InternalItemPlayerErrorDetail = {
38
+ itemId: string;
39
+ canonicalItemId?: string;
40
+ contentKind?: string;
41
+ error: unknown;
42
+ };
43
+
29
44
  let {
30
45
  itemId = "",
31
46
  canonicalItemId = "",
@@ -95,11 +110,45 @@
95
110
  dispatchCrossBoundaryEvent(host, eventName, detail);
96
111
  }
97
112
 
113
+ function dispatchLoaded(detail: unknown): void {
114
+ if (!host || !itemId) return;
115
+ const payload: InternalContentLoadedDetail = {
116
+ itemId,
117
+ canonicalItemId: canonicalItemId || itemId,
118
+ contentKind,
119
+ detail,
120
+ };
121
+ dispatchCrossBoundaryEvent(host, PIE_INTERNAL_CONTENT_LOADED_EVENT, payload);
122
+ }
123
+
124
+ function dispatchPlayerError(error: unknown): void {
125
+ if (!host || !itemId) return;
126
+ const payload: InternalItemPlayerErrorDetail = {
127
+ itemId,
128
+ canonicalItemId: canonicalItemId || itemId,
129
+ contentKind,
130
+ error,
131
+ };
132
+ dispatchCrossBoundaryEvent(host, PIE_INTERNAL_ITEM_PLAYER_ERROR_EVENT, payload);
133
+ }
134
+
98
135
  $effect(() => {
99
136
  if (!host) return;
100
137
  dispatchRegistration(PIE_REGISTER_EVENT);
138
+ const onLoadComplete = (event: Event) => {
139
+ event.stopPropagation();
140
+ dispatchLoaded((event as CustomEvent).detail);
141
+ };
142
+ const onPlayerError = (event: Event) => {
143
+ event.stopPropagation();
144
+ dispatchPlayerError((event as CustomEvent).detail);
145
+ };
146
+ host.addEventListener("load-complete", onLoadComplete);
147
+ host.addEventListener("player-error", onPlayerError);
101
148
 
102
149
  return () => {
150
+ host?.removeEventListener("load-complete", onLoadComplete);
151
+ host?.removeEventListener("player-error", onPlayerError);
103
152
  dispatchRegistration(PIE_UNREGISTER_EVENT);
104
153
  };
105
154
  });
@@ -23,9 +23,14 @@
23
23
 
24
24
  <script lang="ts">
25
25
  import "@pie-players/pie-assessment-toolkit/components/pie-assessment-toolkit-element";
26
+ import "@pie-players/pie-tool-annotation-toolbar";
26
27
  import {
27
28
  createDefaultPersonalNeedsProfile,
28
29
  } from "@pie-players/pie-assessment-toolkit";
30
+ import {
31
+ normalizeToolsConfig,
32
+ resolveToolsForLevel,
33
+ } from "@pie-players/pie-assessment-toolkit";
29
34
  import { createEventDispatcher } from "svelte";
30
35
  import { SectionController } from "../controllers/SectionController.js";
31
36
  import type { SectionCompositionModel } from "../controllers/types.js";
@@ -57,6 +62,7 @@
57
62
  } = $props();
58
63
 
59
64
  let toolkitElement = $state<any>(null);
65
+ let activeToolkitCoordinator = $state<any>(null);
60
66
  let lastCompositionVersion = $state(-1);
61
67
  type BaseSectionPlayerEvents = {
62
68
  "composition-changed": { composition: SectionCompositionModel };
@@ -124,9 +130,40 @@
124
130
  eventName: Exclude<keyof BaseSectionPlayerEvents, "composition-changed">,
125
131
  ): void {
126
132
  const detail = (event as CustomEvent).detail as Record<string, unknown>;
133
+ if (eventName === "toolkit-ready" && detail?.coordinator) {
134
+ activeToolkitCoordinator = detail.coordinator;
135
+ }
127
136
  emit(eventName, detail || ({} as Record<string, unknown>));
128
137
  }
129
138
 
139
+ const normalizedToolsConfig = $derived.by(() =>
140
+ normalizeToolsConfig((effectiveTools || {}) as any),
141
+ );
142
+ const annotationToolbarPlacementEnabled = $derived.by(() => {
143
+ const levels: Array<"section" | "item" | "passage"> = [
144
+ "section",
145
+ "item",
146
+ "passage",
147
+ ];
148
+ return levels.some((level) =>
149
+ resolveToolsForLevel(normalizedToolsConfig as any, level).includes(
150
+ "annotationToolbar",
151
+ ),
152
+ );
153
+ });
154
+ const annotationToolbarProviderEnabled = $derived.by(() =>
155
+ activeToolkitCoordinator?.isToolEnabled?.("annotationToolbar") ??
156
+ ((normalizedToolsConfig as any)?.providers?.annotationToolbar?.enabled !==
157
+ false),
158
+ );
159
+ const shouldRenderAnnotationToolbar = $derived(
160
+ Boolean(
161
+ activeToolkitCoordinator &&
162
+ annotationToolbarPlacementEnabled &&
163
+ annotationToolbarProviderEnabled,
164
+ ),
165
+ );
166
+
130
167
  $effect(() => {
131
168
  if (!toolkitElement) return;
132
169
  toolkitElement.createSectionController =
@@ -157,6 +194,13 @@
157
194
  onruntime-owned={(event: Event) => handleToolkitEvent(event, "runtime-owned")}
158
195
  onruntime-inherited={(event: Event) => handleToolkitEvent(event, "runtime-inherited")}
159
196
  >
197
+ {#if shouldRenderAnnotationToolbar}
198
+ <pie-tool-annotation-toolbar
199
+ enabled={true}
200
+ ttsService={activeToolkitCoordinator.ttsService}
201
+ highlightCoordinator={activeToolkitCoordinator.highlightCoordinator}
202
+ ></pie-tool-annotation-toolbar>
203
+ {/if}
160
204
  <slot></slot>
161
205
  </pie-assessment-toolkit>
162
206
 
@@ -19,7 +19,7 @@
19
19
  isolation: { attribute: "isolation", type: "String" },
20
20
  env: { type: "Object", reflect: false },
21
21
  iifeBundleHost: { attribute: "iife-bundle-host", type: "String" },
22
- showToolbar: { attribute: "show-toolbar", type: "Boolean" },
22
+ showToolbar: { attribute: "show-toolbar", type: "String" },
23
23
  toolbarPosition: { attribute: "toolbar-position", type: "String" },
24
24
  enabledTools: { attribute: "enabled-tools", type: "String" },
25
25
  itemToolbarTools: { attribute: "item-toolbar-tools", type: "String" },
@@ -29,6 +29,7 @@
29
29
  />
30
30
 
31
31
  <script lang="ts">
32
+ import { onMount } from "svelte";
32
33
  import "./section-player-base-element.js";
33
34
  import * as SectionItemCardModule from "./shared/SectionItemCard.svelte";
34
35
  import * as SectionPassageCardModule from "./shared/SectionPassageCard.svelte";
@@ -91,13 +92,36 @@
91
92
  isolation,
92
93
  env,
93
94
  iifeBundleHost,
94
- showToolbar = true,
95
+ showToolbar = "true" as boolean | string | null | undefined,
95
96
  toolbarPosition = "right",
96
97
  enabledTools = "",
97
98
  itemToolbarTools = "",
98
99
  passageToolbarTools = "",
99
100
  } = $props();
100
101
 
102
+ function resolveToolbarVisibility(value: boolean | string | null | undefined): boolean {
103
+ if (typeof value === "boolean") {
104
+ return value;
105
+ }
106
+ if (value === null || value === undefined) {
107
+ return true;
108
+ }
109
+ const normalizedValue = String(value).trim().toLowerCase();
110
+ if (normalizedValue === "") {
111
+ return true;
112
+ }
113
+ if (["false", "0", "off", "no"].includes(normalizedValue)) {
114
+ return false;
115
+ }
116
+ if (["true", "1", "on", "yes"].includes(normalizedValue)) {
117
+ return true;
118
+ }
119
+ return Boolean(normalizedValue);
120
+ }
121
+
122
+ const MANAGED_OUTER_SCROLL_CLASS = "pie-outer-scrollbars-managed";
123
+ const ACTIVE_OUTER_SCROLL_CLASS = "pie-outer-scrolling";
124
+
101
125
  let compositionModel = $state<SectionCompositionModel>(EMPTY_COMPOSITION);
102
126
  let leftPanelWidth = $state(50);
103
127
  let isDragging = $state(false);
@@ -109,7 +133,9 @@
109
133
  const passages = $derived(compositionModel.passages || []);
110
134
  const items = $derived(compositionModel.items || []);
111
135
  const hasPassages = $derived(passages.length > 0);
112
- const shouldRenderToolbar = $derived(showToolbar && toolbarPosition !== "none");
136
+ const shouldRenderToolbar = $derived(
137
+ resolveToolbarVisibility(showToolbar) && toolbarPosition !== "none",
138
+ );
113
139
  const toolbarBeforeContent = $derived(
114
140
  toolbarPosition === "top" || toolbarPosition === "left",
115
141
  );
@@ -237,6 +263,39 @@
237
263
  });
238
264
  });
239
265
 
266
+ onMount(() => {
267
+ let scrollTimeout: ReturnType<typeof setTimeout> | null = null;
268
+ const html = document.documentElement;
269
+ const body = document.body;
270
+
271
+ html.classList.add(MANAGED_OUTER_SCROLL_CLASS);
272
+ body.classList.add(MANAGED_OUTER_SCROLL_CLASS);
273
+
274
+ const showOuterScrollbars = () => {
275
+ html.classList.add(ACTIVE_OUTER_SCROLL_CLASS);
276
+ body.classList.add(ACTIVE_OUTER_SCROLL_CLASS);
277
+ if (scrollTimeout) {
278
+ clearTimeout(scrollTimeout);
279
+ }
280
+ scrollTimeout = setTimeout(() => {
281
+ html.classList.remove(ACTIVE_OUTER_SCROLL_CLASS);
282
+ body.classList.remove(ACTIVE_OUTER_SCROLL_CLASS);
283
+ }, 900);
284
+ };
285
+
286
+ window.addEventListener("scroll", showOuterScrollbars, { passive: true });
287
+ return () => {
288
+ window.removeEventListener("scroll", showOuterScrollbars);
289
+ html.classList.remove(ACTIVE_OUTER_SCROLL_CLASS);
290
+ body.classList.remove(ACTIVE_OUTER_SCROLL_CLASS);
291
+ html.classList.remove(MANAGED_OUTER_SCROLL_CLASS);
292
+ body.classList.remove(MANAGED_OUTER_SCROLL_CLASS);
293
+ if (scrollTimeout) {
294
+ clearTimeout(scrollTimeout);
295
+ }
296
+ };
297
+ });
298
+
240
299
  </script>
241
300
 
242
301
  <pie-section-player-base
@@ -258,7 +317,7 @@
258
317
  {/if}
259
318
 
260
319
  <div
261
- class={`pie-section-player-layout-body ${toolbarInline ? "pie-section-player-layout-body--inline" : ""}`}
320
+ class={`pie-section-player-layout-body ${shouldRenderToolbar && toolbarInline ? "pie-section-player-layout-body--inline" : ""}`}
262
321
  >
263
322
  <div
264
323
  class={`pie-section-player-split-content ${!hasPassages ? "pie-section-player-split-content--no-passages" : ""}`}
@@ -555,6 +614,47 @@
555
614
  padding: 1rem;
556
615
  }
557
616
 
617
+ :global(html.pie-outer-scrollbars-managed),
618
+ :global(body.pie-outer-scrollbars-managed) {
619
+ scrollbar-width: auto;
620
+ scrollbar-color: transparent transparent;
621
+ }
622
+
623
+ :global(html.pie-outer-scrollbars-managed.pie-outer-scrolling),
624
+ :global(body.pie-outer-scrollbars-managed.pie-outer-scrolling) {
625
+ scrollbar-color: #c1c1c1 #f1f1f1;
626
+ }
627
+
628
+ :global(html.pie-outer-scrollbars-managed::-webkit-scrollbar),
629
+ :global(body.pie-outer-scrollbars-managed::-webkit-scrollbar) {
630
+ width: 0;
631
+ height: 0;
632
+ background: transparent;
633
+ }
634
+
635
+ :global(html.pie-outer-scrollbars-managed.pie-outer-scrolling::-webkit-scrollbar),
636
+ :global(body.pie-outer-scrollbars-managed.pie-outer-scrolling::-webkit-scrollbar) {
637
+ width: 8px;
638
+ height: 8px;
639
+ }
640
+
641
+ :global(html.pie-outer-scrollbars-managed.pie-outer-scrolling::-webkit-scrollbar-track),
642
+ :global(body.pie-outer-scrollbars-managed.pie-outer-scrolling::-webkit-scrollbar-track) {
643
+ background: #f1f1f1;
644
+ border-radius: 4px;
645
+ }
646
+
647
+ :global(html.pie-outer-scrollbars-managed.pie-outer-scrolling::-webkit-scrollbar-thumb),
648
+ :global(body.pie-outer-scrollbars-managed.pie-outer-scrolling::-webkit-scrollbar-thumb) {
649
+ background: #c1c1c1;
650
+ border-radius: 4px;
651
+ }
652
+
653
+ :global(html.pie-outer-scrollbars-managed.pie-outer-scrolling::-webkit-scrollbar-thumb:hover),
654
+ :global(body.pie-outer-scrollbars-managed.pie-outer-scrolling::-webkit-scrollbar-thumb:hover) {
655
+ background: #a1a1a1;
656
+ }
657
+
558
658
  @media (max-width: 1100px) {
559
659
  .pie-section-player-shell--left,
560
660
  .pie-section-player-shell--right {
@@ -19,7 +19,7 @@
19
19
  isolation: { attribute: "isolation", type: "String" },
20
20
  env: { type: "Object", reflect: false },
21
21
  iifeBundleHost: { attribute: "iife-bundle-host", type: "String" },
22
- showToolbar: { attribute: "show-toolbar", type: "Boolean" },
22
+ showToolbar: { attribute: "show-toolbar", type: "String" },
23
23
  toolbarPosition: { attribute: "toolbar-position", type: "String" },
24
24
  enabledTools: { attribute: "enabled-tools", type: "String" },
25
25
  itemToolbarTools: { attribute: "item-toolbar-tools", type: "String" },
@@ -89,13 +89,33 @@
89
89
  isolation,
90
90
  env,
91
91
  iifeBundleHost,
92
- showToolbar = true,
92
+ showToolbar = "true" as boolean | string | null | undefined,
93
93
  toolbarPosition = "right",
94
94
  enabledTools = "",
95
95
  itemToolbarTools = "",
96
96
  passageToolbarTools = "",
97
97
  } = $props();
98
98
 
99
+ function resolveToolbarVisibility(value: boolean | string | null | undefined): boolean {
100
+ if (typeof value === "boolean") {
101
+ return value;
102
+ }
103
+ if (value === null || value === undefined) {
104
+ return true;
105
+ }
106
+ const normalizedValue = String(value).trim().toLowerCase();
107
+ if (normalizedValue === "") {
108
+ return true;
109
+ }
110
+ if (["false", "0", "off", "no"].includes(normalizedValue)) {
111
+ return false;
112
+ }
113
+ if (["true", "1", "on", "yes"].includes(normalizedValue)) {
114
+ return true;
115
+ }
116
+ return Boolean(normalizedValue);
117
+ }
118
+
99
119
  let compositionModel = $state<SectionCompositionModel>(EMPTY_COMPOSITION);
100
120
  let elementsLoaded = $state(false);
101
121
  let lastPreloadSignature = $state("");
@@ -103,7 +123,9 @@
103
123
 
104
124
  const passages = $derived(compositionModel.passages || []);
105
125
  const items = $derived(compositionModel.items || []);
106
- const shouldRenderToolbar = $derived(showToolbar && toolbarPosition !== "none");
126
+ const shouldRenderToolbar = $derived(
127
+ resolveToolbarVisibility(showToolbar) && toolbarPosition !== "none",
128
+ );
107
129
  const toolbarBeforeContent = $derived(
108
130
  toolbarPosition === "top" || toolbarPosition === "left",
109
131
  );
@@ -205,7 +227,7 @@
205
227
  {/if}
206
228
 
207
229
  <div
208
- class={`pie-section-player-layout-body ${toolbarInline ? "pie-section-player-layout-body--inline" : ""}`}
230
+ class={`pie-section-player-layout-body ${shouldRenderToolbar && toolbarInline ? "pie-section-player-layout-body--inline" : ""}`}
209
231
  >
210
232
  <div class="pie-section-player-vertical-content">
211
233
  {#if !elementsLoaded}