@pie-players/pie-players-shared 0.3.60 → 0.3.61

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
@@ -8,6 +8,7 @@ export * from "./object/index.js";
8
8
  export * from "./pie/index.js";
9
9
  export type { AssessmentEntity, AssessmentItemRef, AssessmentSection, AdvancedItemConfig, ConfigEntity, ConfigResource, Env, ItemConfig, ItemEntity, ItemSession, OutcomeResponse, PassageEntity, PieController, PieContent, PieDefaultModel, PieItemElement, PieModel, QuestionEntity, RubricBlock, TestPart, } from "./types/index.js";
10
10
  export { editorPostFix } from "./types/index.js";
11
+ export * from "./ui/content-styles.js";
11
12
  export * from "./ui/focus-trap.js";
12
13
  export * from "./ui/first-focusable.js";
13
14
  export * from "./ui/debug-panel-persistence.js";
package/dist/index.js CHANGED
@@ -8,6 +8,7 @@ export * from "./object/index.js";
8
8
  // Barrel export for PIE runtime utilities
9
9
  export * from "./pie/index.js";
10
10
  export { editorPostFix } from "./types/index.js";
11
+ export * from "./ui/content-styles.js";
11
12
  export * from "./ui/focus-trap.js";
12
13
  export * from "./ui/first-focusable.js";
13
14
  export * from "./ui/debug-panel-persistence.js";
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAC;AACpC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,oBAAoB,CAAC;AACnC,cAAc,sBAAsB,CAAC;AACrC,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,mBAAmB,CAAC;AAClC,0CAA0C;AAC1C,cAAc,gBAAgB,CAAC;AAuB/B,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,cAAc,oBAAoB,CAAC;AACnC,cAAc,yBAAyB,CAAC;AACxC,cAAc,iCAAiC,CAAC;AAChD,cAAc,sBAAsB,CAAC","sourcesContent":["export * from \"./config/profile.js\";\nexport * from \"./instrumentation/index.js\";\nexport * from \"./loader-config.js\";\nexport * from \"./player-strategy.js\";\nexport * from \"./security/index.js\";\nexport * from \"./loaders/index.js\";\nexport * from \"./object/index.js\";\n// Barrel export for PIE runtime utilities\nexport * from \"./pie/index.js\";\nexport type {\n\tAssessmentEntity,\n\tAssessmentItemRef,\n\tAssessmentSection,\n\tAdvancedItemConfig,\n\tConfigEntity,\n\tConfigResource,\n\tEnv,\n\tItemConfig,\n\tItemEntity,\n\tItemSession,\n\tOutcomeResponse,\n\tPassageEntity,\n\tPieController,\n\tPieContent,\n\tPieDefaultModel,\n\tPieItemElement,\n\tPieModel,\n\tQuestionEntity,\n\tRubricBlock,\n\tTestPart,\n} from \"./types/index.js\";\nexport { editorPostFix } from \"./types/index.js\";\nexport * from \"./ui/focus-trap.js\";\nexport * from \"./ui/first-focusable.js\";\nexport * from \"./ui/debug-panel-persistence.js\";\nexport * from \"./ui/safe-storage.js\";\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAC;AACpC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,oBAAoB,CAAC;AACnC,cAAc,sBAAsB,CAAC;AACrC,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,mBAAmB,CAAC;AAClC,0CAA0C;AAC1C,cAAc,gBAAgB,CAAC;AAuB/B,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,cAAc,wBAAwB,CAAC;AACvC,cAAc,oBAAoB,CAAC;AACnC,cAAc,yBAAyB,CAAC;AACxC,cAAc,iCAAiC,CAAC;AAChD,cAAc,sBAAsB,CAAC","sourcesContent":["export * from \"./config/profile.js\";\nexport * from \"./instrumentation/index.js\";\nexport * from \"./loader-config.js\";\nexport * from \"./player-strategy.js\";\nexport * from \"./security/index.js\";\nexport * from \"./loaders/index.js\";\nexport * from \"./object/index.js\";\n// Barrel export for PIE runtime utilities\nexport * from \"./pie/index.js\";\nexport type {\n\tAssessmentEntity,\n\tAssessmentItemRef,\n\tAssessmentSection,\n\tAdvancedItemConfig,\n\tConfigEntity,\n\tConfigResource,\n\tEnv,\n\tItemConfig,\n\tItemEntity,\n\tItemSession,\n\tOutcomeResponse,\n\tPassageEntity,\n\tPieController,\n\tPieContent,\n\tPieDefaultModel,\n\tPieItemElement,\n\tPieModel,\n\tQuestionEntity,\n\tRubricBlock,\n\tTestPart,\n} from \"./types/index.js\";\nexport { editorPostFix } from \"./types/index.js\";\nexport * from \"./ui/content-styles.js\";\nexport * from \"./ui/focus-trap.js\";\nexport * from \"./ui/first-focusable.js\";\nexport * from \"./ui/debug-panel-persistence.js\";\nexport * from \"./ui/safe-storage.js\";\n"]}
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Installs the shared PIE content stylesheet
3
+ * (`@pie-players/pie-theme/components.css`) into the host document.
4
+ *
5
+ * Authored assessment content depends on classes that belong to no single
6
+ * component: passage markup (`.numbered-paragraph`, `.p-number`,
7
+ * `div.passage-title`), the legacy `kds-*` families, the `@media print`
8
+ * `.noprint` rules, and the answer-eliminator classes. Players render into
9
+ * light DOM, so those rules have to exist as a document-level stylesheet —
10
+ * there is no shadow root to scope them to.
11
+ *
12
+ * Hosts used to be required to import that stylesheet themselves. Nothing
13
+ * enforced it and nothing failed loudly when they didn't: the item rendered,
14
+ * the passage was simply unstyled, and it surfaced as a visual bug reported by
15
+ * hand days later. Players install the stylesheet themselves instead, which
16
+ * keeps the host contract at "import the player".
17
+ *
18
+ * The CSS text is passed in rather than imported here: this package builds with
19
+ * plain `tsc`, so it cannot inline a stylesheet. Bundler-built player packages
20
+ * import it with Vite's `?inline` and hand the text over.
21
+ */
22
+ export type ContentStylesResult = "installed" | "already-installed" | "opted-out" | "no-document";
23
+ /**
24
+ * True when the host declared `<html data-pie-content-styles="host">`, i.e. it
25
+ * takes ownership of loading the stylesheet.
26
+ */
27
+ export declare function contentStylesOptedOut(): boolean;
28
+ /**
29
+ * True when `components.css` is applied to the document, by any route. Reads the
30
+ * sentinel custom property the stylesheet declares on `:root`.
31
+ *
32
+ * Only meaningful once the document's stylesheets have been applied — a host
33
+ * that loads CSS via an async `<link>` reads as missing until it lands.
34
+ */
35
+ export declare function contentStylesPresent(): boolean;
36
+ /**
37
+ * Installs `cssText` as a document-level stylesheet, once per document.
38
+ *
39
+ * The stylesheet is **prepended** to `<head>` and deliberately left out of a
40
+ * cascade layer. Prepending reproduces the placement hosts were told to use by
41
+ * hand — first in the entry, before app CSS — so an app rule and a content rule
42
+ * of equal specificity resolve in the host's favour, exactly as before.
43
+ *
44
+ * A cascade layer looks tempting here and is the wrong tool: unlayered author
45
+ * declarations beat *all* layered ones regardless of specificity, so a host
46
+ * reset as broad as `p { margin: 0 }` would silently outrank
47
+ * `.numbered-paragraph { margin-left: 36px }`. That trades a visible missing
48
+ * stylesheet for a subtler override bug, so ordinary specificity wins instead.
49
+ *
50
+ * @param cssText Contents of `@pie-players/pie-theme/components.css`.
51
+ * @param source Package installing the styles, for diagnostics.
52
+ */
53
+ export declare function installContentStyles(cssText: string, source: string): ContentStylesResult;
54
+ /**
55
+ * Reports, once per page, the two ways content styling can go wrong. Neither is
56
+ * detectable synchronously — a host copy may still be in flight as an async
57
+ * `<link>` — so the check is deferred.
58
+ *
59
+ * - **Missing**: the host opted out and then loaded nothing, so authored content
60
+ * renders unstyled. This is the failure the old host-import contract produced
61
+ * silently.
62
+ * - **Duplicated**: the host still imports `components.css` itself *and* the
63
+ * player installed a copy. Rendering is correct while the two agree, but the
64
+ * host's copy loads later and therefore wins ties at equal specificity — so a
65
+ * host copy pinned to an older `@pie-players/pie-theme` silently overrides the
66
+ * player's newer rules. Harmless today, a confusing override tomorrow.
67
+ */
68
+ export declare function auditContentStyles(source: string): void;
69
+ /**
70
+ * Test-only: clears the once-per-page warning latch and cancels any pending
71
+ * deferred check, so a timer scheduled by one test cannot warn during the next.
72
+ */
73
+ export declare function resetContentStylesWarningForTesting(): void;
@@ -0,0 +1,205 @@
1
+ /**
2
+ * Installs the shared PIE content stylesheet
3
+ * (`@pie-players/pie-theme/components.css`) into the host document.
4
+ *
5
+ * Authored assessment content depends on classes that belong to no single
6
+ * component: passage markup (`.numbered-paragraph`, `.p-number`,
7
+ * `div.passage-title`), the legacy `kds-*` families, the `@media print`
8
+ * `.noprint` rules, and the answer-eliminator classes. Players render into
9
+ * light DOM, so those rules have to exist as a document-level stylesheet —
10
+ * there is no shadow root to scope them to.
11
+ *
12
+ * Hosts used to be required to import that stylesheet themselves. Nothing
13
+ * enforced it and nothing failed loudly when they didn't: the item rendered,
14
+ * the passage was simply unstyled, and it surfaced as a visual bug reported by
15
+ * hand days later. Players install the stylesheet themselves instead, which
16
+ * keeps the host contract at "import the player".
17
+ *
18
+ * The CSS text is passed in rather than imported here: this package builds with
19
+ * plain `tsc`, so it cannot inline a stylesheet. Bundler-built player packages
20
+ * import it with Vite's `?inline` and hand the text over.
21
+ */
22
+ /** Marks a `<style>` element this module owns, and keeps installs idempotent. */
23
+ const MARKER_ATTRIBUTE = "data-pie-content-styles";
24
+ /**
25
+ * Declared by `components.css` itself, so it is observable no matter how the
26
+ * stylesheet arrived — our injection or a host import. Used only to tell a host
27
+ * that opted out but then shipped nothing.
28
+ */
29
+ const SENTINEL_PROPERTY = "--pie-content-styles";
30
+ /** `<html data-pie-content-styles="host">` opts a host out of installation. */
31
+ const OPT_OUT_ATTRIBUTE = "data-pie-content-styles";
32
+ const OPT_OUT_VALUE = "host";
33
+ const isBrowser = () => typeof document !== "undefined" && !!document.head;
34
+ /**
35
+ * True when the host declared `<html data-pie-content-styles="host">`, i.e. it
36
+ * takes ownership of loading the stylesheet.
37
+ */
38
+ export function contentStylesOptedOut() {
39
+ if (!isBrowser())
40
+ return false;
41
+ return (document.documentElement.getAttribute(OPT_OUT_ATTRIBUTE) === OPT_OUT_VALUE);
42
+ }
43
+ /**
44
+ * True when `components.css` is applied to the document, by any route. Reads the
45
+ * sentinel custom property the stylesheet declares on `:root`.
46
+ *
47
+ * Only meaningful once the document's stylesheets have been applied — a host
48
+ * that loads CSS via an async `<link>` reads as missing until it lands.
49
+ */
50
+ export function contentStylesPresent() {
51
+ if (!isBrowser())
52
+ return false;
53
+ const value = getComputedStyle(document.documentElement)
54
+ .getPropertyValue(SENTINEL_PROPERTY)
55
+ .trim();
56
+ return value !== "";
57
+ }
58
+ /**
59
+ * Installs `cssText` as a document-level stylesheet, once per document.
60
+ *
61
+ * The stylesheet is **prepended** to `<head>` and deliberately left out of a
62
+ * cascade layer. Prepending reproduces the placement hosts were told to use by
63
+ * hand — first in the entry, before app CSS — so an app rule and a content rule
64
+ * of equal specificity resolve in the host's favour, exactly as before.
65
+ *
66
+ * A cascade layer looks tempting here and is the wrong tool: unlayered author
67
+ * declarations beat *all* layered ones regardless of specificity, so a host
68
+ * reset as broad as `p { margin: 0 }` would silently outrank
69
+ * `.numbered-paragraph { margin-left: 36px }`. That trades a visible missing
70
+ * stylesheet for a subtler override bug, so ordinary specificity wins instead.
71
+ *
72
+ * @param cssText Contents of `@pie-players/pie-theme/components.css`.
73
+ * @param source Package installing the styles, for diagnostics.
74
+ */
75
+ export function installContentStyles(cssText, source) {
76
+ if (!isBrowser())
77
+ return "no-document";
78
+ if (contentStylesOptedOut())
79
+ return "opted-out";
80
+ if (document.querySelector(`style[${MARKER_ATTRIBUTE}]`)) {
81
+ // Another player package, or a second copy of this one, already installed
82
+ // the same stylesheet. Duplicating it would be harmless but pointless.
83
+ return "already-installed";
84
+ }
85
+ if (!cssText)
86
+ return "no-document";
87
+ const style = document.createElement("style");
88
+ style.setAttribute(MARKER_ATTRIBUTE, source);
89
+ style.textContent = cssText;
90
+ document.head.prepend(style);
91
+ return "installed";
92
+ }
93
+ /**
94
+ * Counts content stylesheets in the document that this module did not install —
95
+ * i.e. copies the host loaded itself. Detected by the sentinel property rather
96
+ * than by URL, so a copy arriving as a `<link>`, a bundler-injected `<style>`, or
97
+ * anything else all count the same.
98
+ *
99
+ * Cross-origin sheets throw on `cssRules` access and are skipped; a host copy
100
+ * served from another origin therefore reads as absent. That only costs a
101
+ * diagnostic, never correctness.
102
+ */
103
+ const countHostContentStyleSheets = () => {
104
+ // Walks the owning elements rather than document.styleSheets: the marker
105
+ // attribute lives on the element, and CSSStyleSheet.ownerNode is not
106
+ // universally implemented (happy-dom omits it), which would make our own
107
+ // installed copy look like a host copy.
108
+ const nodes = document.querySelectorAll(`style:not([${MARKER_ATTRIBUTE}]), link[rel~="stylesheet"]:not([${MARKER_ATTRIBUTE}])`);
109
+ let count = 0;
110
+ for (const node of Array.from(nodes)) {
111
+ let rules;
112
+ try {
113
+ rules = node.sheet?.cssRules;
114
+ }
115
+ catch {
116
+ continue;
117
+ }
118
+ if (!rules)
119
+ continue;
120
+ for (const rule of Array.from(rules)) {
121
+ if (rule.style?.getPropertyValue(SENTINEL_PROPERTY)) {
122
+ count += 1;
123
+ break;
124
+ }
125
+ }
126
+ }
127
+ return count;
128
+ };
129
+ let auditWarningIssued = false;
130
+ const pendingChecks = [];
131
+ /**
132
+ * Reports, once per page, the two ways content styling can go wrong. Neither is
133
+ * detectable synchronously — a host copy may still be in flight as an async
134
+ * `<link>` — so the check is deferred.
135
+ *
136
+ * - **Missing**: the host opted out and then loaded nothing, so authored content
137
+ * renders unstyled. This is the failure the old host-import contract produced
138
+ * silently.
139
+ * - **Duplicated**: the host still imports `components.css` itself *and* the
140
+ * player installed a copy. Rendering is correct while the two agree, but the
141
+ * host's copy loads later and therefore wins ties at equal specificity — so a
142
+ * host copy pinned to an older `@pie-players/pie-theme` silently overrides the
143
+ * player's newer rules. Harmless today, a confusing override tomorrow.
144
+ */
145
+ export function auditContentStyles(source) {
146
+ if (!isBrowser() || auditWarningIssued)
147
+ return;
148
+ const check = () => {
149
+ if (auditWarningIssued)
150
+ return;
151
+ if (contentStylesOptedOut()) {
152
+ if (contentStylesPresent())
153
+ return;
154
+ auditWarningIssued = true;
155
+ console.warn(`[${source}] No PIE content stylesheet found. This document sets ` +
156
+ `${OPT_OUT_ATTRIBUTE}="${OPT_OUT_VALUE}", so ${source} did not install ` +
157
+ `one. Authored content that relies on shared classes ` +
158
+ `(.numbered-paragraph, .p-number, div.passage-title, the kds-* ` +
159
+ `families, answer-eliminator styles) will render unstyled. Either ` +
160
+ `import "@pie-players/pie-theme/components.css" in the host app, or ` +
161
+ `drop the ${OPT_OUT_ATTRIBUTE} attribute to let the player install it.`);
162
+ return;
163
+ }
164
+ if (countHostContentStyleSheets() > 0) {
165
+ auditWarningIssued = true;
166
+ console.warn(`[${source}] The PIE content stylesheet is loaded twice: ${source} ` +
167
+ `installs it, and this host also imports ` +
168
+ `"@pie-players/pie-theme/components.css". Rendering is unaffected ` +
169
+ `while both copies match, but the host copy loads later and wins ` +
170
+ `ties at equal specificity, so a copy pinned to an older ` +
171
+ `@pie-players/pie-theme will silently override newer player styles. ` +
172
+ `Remove the host import, or set ` +
173
+ `${OPT_OUT_ATTRIBUTE}="${OPT_OUT_VALUE}" on <html> to own the ` +
174
+ `stylesheet deliberately.`);
175
+ }
176
+ };
177
+ // A host stylesheet can land well after the module graph evaluates, and a
178
+ // bundler-injected <link> is not necessarily parsed by the time `load` fires.
179
+ // This is advisory output, so it is checked at a few widening points rather
180
+ // than raced: whichever one first sees a settled document wins, and the latch
181
+ // keeps the rest quiet. Missing the window costs a diagnostic, not
182
+ // correctness.
183
+ if (typeof requestAnimationFrame !== "function") {
184
+ check();
185
+ return;
186
+ }
187
+ requestAnimationFrame(() => requestAnimationFrame(check));
188
+ if (typeof window !== "undefined" && document.readyState !== "complete") {
189
+ window.addEventListener("load", () => check(), { once: true });
190
+ }
191
+ if (typeof setTimeout === "function") {
192
+ pendingChecks.push(setTimeout(check, 1000));
193
+ }
194
+ }
195
+ /**
196
+ * Test-only: clears the once-per-page warning latch and cancels any pending
197
+ * deferred check, so a timer scheduled by one test cannot warn during the next.
198
+ */
199
+ export function resetContentStylesWarningForTesting() {
200
+ auditWarningIssued = false;
201
+ for (const handle of pendingChecks)
202
+ clearTimeout(handle);
203
+ pendingChecks.length = 0;
204
+ }
205
+ //# sourceMappingURL=content-styles.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content-styles.js","sourceRoot":"","sources":["../../src/ui/content-styles.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Installs the shared PIE content stylesheet\n * (`@pie-players/pie-theme/components.css`) into the host document.\n *\n * Authored assessment content depends on classes that belong to no single\n * component: passage markup (`.numbered-paragraph`, `.p-number`,\n * `div.passage-title`), the legacy `kds-*` families, the `@media print`\n * `.noprint` rules, and the answer-eliminator classes. Players render into\n * light DOM, so those rules have to exist as a document-level stylesheet —\n * there is no shadow root to scope them to.\n *\n * Hosts used to be required to import that stylesheet themselves. Nothing\n * enforced it and nothing failed loudly when they didn't: the item rendered,\n * the passage was simply unstyled, and it surfaced as a visual bug reported by\n * hand days later. Players install the stylesheet themselves instead, which\n * keeps the host contract at \"import the player\".\n *\n * The CSS text is passed in rather than imported here: this package builds with\n * plain `tsc`, so it cannot inline a stylesheet. Bundler-built player packages\n * import it with Vite's `?inline` and hand the text over.\n */\n\n/** Marks a `<style>` element this module owns, and keeps installs idempotent. */\nconst MARKER_ATTRIBUTE = \"data-pie-content-styles\";\n\n/**\n * Declared by `components.css` itself, so it is observable no matter how the\n * stylesheet arrived — our injection or a host import. Used only to tell a host\n * that opted out but then shipped nothing.\n */\nconst SENTINEL_PROPERTY = \"--pie-content-styles\";\n\n/** `<html data-pie-content-styles=\"host\">` opts a host out of installation. */\nconst OPT_OUT_ATTRIBUTE = \"data-pie-content-styles\";\nconst OPT_OUT_VALUE = \"host\";\n\nexport type ContentStylesResult =\n\t| \"installed\"\n\t| \"already-installed\"\n\t| \"opted-out\"\n\t| \"no-document\";\n\nconst isBrowser = (): boolean =>\n\ttypeof document !== \"undefined\" && !!document.head;\n\n/**\n * True when the host declared `<html data-pie-content-styles=\"host\">`, i.e. it\n * takes ownership of loading the stylesheet.\n */\nexport function contentStylesOptedOut(): boolean {\n\tif (!isBrowser()) return false;\n\treturn (\n\t\tdocument.documentElement.getAttribute(OPT_OUT_ATTRIBUTE) === OPT_OUT_VALUE\n\t);\n}\n\n/**\n * True when `components.css` is applied to the document, by any route. Reads the\n * sentinel custom property the stylesheet declares on `:root`.\n *\n * Only meaningful once the document's stylesheets have been applied — a host\n * that loads CSS via an async `<link>` reads as missing until it lands.\n */\nexport function contentStylesPresent(): boolean {\n\tif (!isBrowser()) return false;\n\tconst value = getComputedStyle(document.documentElement)\n\t\t.getPropertyValue(SENTINEL_PROPERTY)\n\t\t.trim();\n\treturn value !== \"\";\n}\n\n/**\n * Installs `cssText` as a document-level stylesheet, once per document.\n *\n * The stylesheet is **prepended** to `<head>` and deliberately left out of a\n * cascade layer. Prepending reproduces the placement hosts were told to use by\n * hand — first in the entry, before app CSS — so an app rule and a content rule\n * of equal specificity resolve in the host's favour, exactly as before.\n *\n * A cascade layer looks tempting here and is the wrong tool: unlayered author\n * declarations beat *all* layered ones regardless of specificity, so a host\n * reset as broad as `p { margin: 0 }` would silently outrank\n * `.numbered-paragraph { margin-left: 36px }`. That trades a visible missing\n * stylesheet for a subtler override bug, so ordinary specificity wins instead.\n *\n * @param cssText Contents of `@pie-players/pie-theme/components.css`.\n * @param source Package installing the styles, for diagnostics.\n */\nexport function installContentStyles(\n\tcssText: string,\n\tsource: string,\n): ContentStylesResult {\n\tif (!isBrowser()) return \"no-document\";\n\tif (contentStylesOptedOut()) return \"opted-out\";\n\tif (document.querySelector(`style[${MARKER_ATTRIBUTE}]`)) {\n\t\t// Another player package, or a second copy of this one, already installed\n\t\t// the same stylesheet. Duplicating it would be harmless but pointless.\n\t\treturn \"already-installed\";\n\t}\n\tif (!cssText) return \"no-document\";\n\n\tconst style = document.createElement(\"style\");\n\tstyle.setAttribute(MARKER_ATTRIBUTE, source);\n\tstyle.textContent = cssText;\n\tdocument.head.prepend(style);\n\treturn \"installed\";\n}\n\n/**\n * Counts content stylesheets in the document that this module did not install —\n * i.e. copies the host loaded itself. Detected by the sentinel property rather\n * than by URL, so a copy arriving as a `<link>`, a bundler-injected `<style>`, or\n * anything else all count the same.\n *\n * Cross-origin sheets throw on `cssRules` access and are skipped; a host copy\n * served from another origin therefore reads as absent. That only costs a\n * diagnostic, never correctness.\n */\nconst countHostContentStyleSheets = (): number => {\n\t// Walks the owning elements rather than document.styleSheets: the marker\n\t// attribute lives on the element, and CSSStyleSheet.ownerNode is not\n\t// universally implemented (happy-dom omits it), which would make our own\n\t// installed copy look like a host copy.\n\tconst nodes = document.querySelectorAll<HTMLStyleElement | HTMLLinkElement>(\n\t\t`style:not([${MARKER_ATTRIBUTE}]), link[rel~=\"stylesheet\"]:not([${MARKER_ATTRIBUTE}])`,\n\t);\n\tlet count = 0;\n\tfor (const node of Array.from(nodes)) {\n\t\tlet rules: CSSRuleList | undefined;\n\t\ttry {\n\t\t\trules = node.sheet?.cssRules;\n\t\t} catch {\n\t\t\tcontinue;\n\t\t}\n\t\tif (!rules) continue;\n\t\tfor (const rule of Array.from(rules)) {\n\t\t\tif ((rule as CSSStyleRule).style?.getPropertyValue(SENTINEL_PROPERTY)) {\n\t\t\t\tcount += 1;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\treturn count;\n};\n\nlet auditWarningIssued = false;\nconst pendingChecks: ReturnType<typeof setTimeout>[] = [];\n\n/**\n * Reports, once per page, the two ways content styling can go wrong. Neither is\n * detectable synchronously — a host copy may still be in flight as an async\n * `<link>` — so the check is deferred.\n *\n * - **Missing**: the host opted out and then loaded nothing, so authored content\n * renders unstyled. This is the failure the old host-import contract produced\n * silently.\n * - **Duplicated**: the host still imports `components.css` itself *and* the\n * player installed a copy. Rendering is correct while the two agree, but the\n * host's copy loads later and therefore wins ties at equal specificity — so a\n * host copy pinned to an older `@pie-players/pie-theme` silently overrides the\n * player's newer rules. Harmless today, a confusing override tomorrow.\n */\nexport function auditContentStyles(source: string): void {\n\tif (!isBrowser() || auditWarningIssued) return;\n\n\tconst check = () => {\n\t\tif (auditWarningIssued) return;\n\n\t\tif (contentStylesOptedOut()) {\n\t\t\tif (contentStylesPresent()) return;\n\t\t\tauditWarningIssued = true;\n\t\t\tconsole.warn(\n\t\t\t\t`[${source}] No PIE content stylesheet found. This document sets ` +\n\t\t\t\t\t`${OPT_OUT_ATTRIBUTE}=\"${OPT_OUT_VALUE}\", so ${source} did not install ` +\n\t\t\t\t\t`one. Authored content that relies on shared classes ` +\n\t\t\t\t\t`(.numbered-paragraph, .p-number, div.passage-title, the kds-* ` +\n\t\t\t\t\t`families, answer-eliminator styles) will render unstyled. Either ` +\n\t\t\t\t\t`import \"@pie-players/pie-theme/components.css\" in the host app, or ` +\n\t\t\t\t\t`drop the ${OPT_OUT_ATTRIBUTE} attribute to let the player install it.`,\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\n\t\tif (countHostContentStyleSheets() > 0) {\n\t\t\tauditWarningIssued = true;\n\t\t\tconsole.warn(\n\t\t\t\t`[${source}] The PIE content stylesheet is loaded twice: ${source} ` +\n\t\t\t\t\t`installs it, and this host also imports ` +\n\t\t\t\t\t`\"@pie-players/pie-theme/components.css\". Rendering is unaffected ` +\n\t\t\t\t\t`while both copies match, but the host copy loads later and wins ` +\n\t\t\t\t\t`ties at equal specificity, so a copy pinned to an older ` +\n\t\t\t\t\t`@pie-players/pie-theme will silently override newer player styles. ` +\n\t\t\t\t\t`Remove the host import, or set ` +\n\t\t\t\t\t`${OPT_OUT_ATTRIBUTE}=\"${OPT_OUT_VALUE}\" on <html> to own the ` +\n\t\t\t\t\t`stylesheet deliberately.`,\n\t\t\t);\n\t\t}\n\t};\n\n\t// A host stylesheet can land well after the module graph evaluates, and a\n\t// bundler-injected <link> is not necessarily parsed by the time `load` fires.\n\t// This is advisory output, so it is checked at a few widening points rather\n\t// than raced: whichever one first sees a settled document wins, and the latch\n\t// keeps the rest quiet. Missing the window costs a diagnostic, not\n\t// correctness.\n\tif (typeof requestAnimationFrame !== \"function\") {\n\t\tcheck();\n\t\treturn;\n\t}\n\trequestAnimationFrame(() => requestAnimationFrame(check));\n\tif (typeof window !== \"undefined\" && document.readyState !== \"complete\") {\n\t\twindow.addEventListener(\"load\", () => check(), { once: true });\n\t}\n\tif (typeof setTimeout === \"function\") {\n\t\tpendingChecks.push(setTimeout(check, 1000));\n\t}\n}\n\n/**\n * Test-only: clears the once-per-page warning latch and cancels any pending\n * deferred check, so a timer scheduled by one test cannot warn during the next.\n */\nexport function resetContentStylesWarningForTesting(): void {\n\tauditWarningIssued = false;\n\tfor (const handle of pendingChecks) clearTimeout(handle);\n\tpendingChecks.length = 0;\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pie-players/pie-players-shared",
3
- "version": "0.3.60",
3
+ "version": "0.3.61",
4
4
  "type": "module",
5
5
  "description": "Shared runtime + UI utilities for PIE players",
6
6
  "license": "MIT",
@@ -64,6 +64,10 @@
64
64
  "./ui/zoom-compensation": {
65
65
  "types": "./dist/ui/zoom-compensation.d.ts",
66
66
  "import": "./dist/ui/zoom-compensation.js"
67
+ },
68
+ "./ui/content-styles": {
69
+ "types": "./dist/ui/content-styles.d.ts",
70
+ "import": "./dist/ui/content-styles.js"
67
71
  }
68
72
  },
69
73
  "files": [
@@ -72,7 +76,7 @@
72
76
  "dependencies": {
73
77
  "@pie-lib/math-rendering-module": "5.1.0",
74
78
  "dompurify": "^3.4.0",
75
- "semver": "^7.7.4"
79
+ "semver": "^7.8.5"
76
80
  },
77
81
  "scripts": {
78
82
  "build": "rm -rf dist tsconfig.tsbuildinfo && tsc -p tsconfig.json && mkdir -p dist/components && cp -R src/components/vendor dist/components/vendor",