@pie-players/pie-players-shared 0.3.65 → 0.3.67

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.
@@ -422,6 +422,12 @@ export type CatalogCardPayload = SignLanguageCardPayload | SpokenAudioCardPayloa
422
422
  export interface CatalogCard {
423
423
  catalog: string;
424
424
  language?: string;
425
+ /**
426
+ * Optional presentation policy carried by catalog-backed UI such as an audio
427
+ * transcript. Unknown values remain authored metadata for the capability that
428
+ * understands them; the generic catalog resolver does not reinterpret them.
429
+ */
430
+ visibility?: string;
425
431
  /**
426
432
  * The string form of the card's content, for catalog types a string can
427
433
  * express: SSML for `spoken`, plain text for `simplified-language`, and so
@@ -17,7 +17,7 @@
17
17
  *
18
18
  * The CSS text is passed in rather than imported here: this package builds with
19
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.
20
+ * import it with Vite's `?raw` and hand the text over.
21
21
  */
22
22
  export type ContentStylesResult = "installed" | "already-installed" | "opted-out" | "no-document";
23
23
  /**
@@ -26,8 +26,16 @@ export type ContentStylesResult = "installed" | "already-installed" | "opted-out
26
26
  */
27
27
  export declare function contentStylesOptedOut(): boolean;
28
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`.
29
+ * True when `components.css` is applied to the document, by any route.
30
+ *
31
+ * Two probes, because neither alone covers both deliveries. The computed
32
+ * sentinel on `<html>` is authoritative for a stylesheet applying to the whole
33
+ * document. A host that confines its copy to its player subtree — `@scope
34
+ * (.item-content) { … }`, the documented remedy for these rules reaching host
35
+ * chrome — puts the stylesheet's `:root` rule somewhere it can never match,
36
+ * since `<html>` is not a descendant of the scoping root; the property then
37
+ * reads empty while the stylesheet is present and working. So an empty read
38
+ * falls back to scanning the document's sheets for the sentinel.
31
39
  *
32
40
  * Only meaningful once the document's stylesheets have been applied — a host
33
41
  * that loads CSS via an async `<link>` reads as missing until it lands.
@@ -17,16 +17,50 @@
17
17
  *
18
18
  * The CSS text is passed in rather than imported here: this package builds with
19
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.
20
+ * import it with Vite's `?raw` and hand the text over.
21
21
  */
22
22
  /** Marks a `<style>` element this module owns, and keeps installs idempotent. */
23
23
  const MARKER_ATTRIBUTE = "data-pie-content-styles";
24
24
  /**
25
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.
26
+ * stylesheet arrived — our injection or a host import. Diagnostics only: it
27
+ * tells a host that opted out and then shipped nothing, and it spots a host copy
28
+ * sitting alongside ours.
28
29
  */
29
30
  const SENTINEL_PROPERTY = "--pie-content-styles";
31
+ // Svelte's dev-mode custom-element reset expands `all: unset` into individual
32
+ // declarations, including custom properties it has observed elsewhere in the
33
+ // bundle. That can produce `--pie-content-styles: unset` on a scoped selector;
34
+ // it is a reset, not evidence that a host loaded components.css.
35
+ const CSS_WIDE_RESET_VALUES = new Set([
36
+ "inherit",
37
+ "initial",
38
+ "revert",
39
+ "revert-layer",
40
+ "unset",
41
+ ]);
42
+ const declaresContentStylesSentinel = (rule) => {
43
+ const value = rule.style
44
+ ?.getPropertyValue(SENTINEL_PROPERTY)
45
+ .trim()
46
+ .toLowerCase();
47
+ if (value)
48
+ return !CSS_WIDE_RESET_VALUES.has(value);
49
+ // Grouping rules hold no declarations of their own, so the sentinel sits one
50
+ // or more levels down. A host that confines its copy — `@scope
51
+ // (.item-content) { … }`, `@layer pie-content { … }` — presents exactly one
52
+ // top-level rule with an empty `.style`, and a top-level-only scan reads that
53
+ // as "no copy here". That made both detection paths blind to the one host
54
+ // configuration this module most needs to recognise.
55
+ const nested = rule.cssRules;
56
+ if (!nested)
57
+ return false;
58
+ for (const child of Array.from(nested)) {
59
+ if (declaresContentStylesSentinel(child))
60
+ return true;
61
+ }
62
+ return false;
63
+ };
30
64
  /** `<html data-pie-content-styles="host">` opts a host out of installation. */
31
65
  const OPT_OUT_ATTRIBUTE = "data-pie-content-styles";
32
66
  const OPT_OUT_VALUE = "host";
@@ -41,8 +75,16 @@ export function contentStylesOptedOut() {
41
75
  return (document.documentElement.getAttribute(OPT_OUT_ATTRIBUTE) === OPT_OUT_VALUE);
42
76
  }
43
77
  /**
44
- * True when `components.css` is applied to the document, by any route. Reads the
45
- * sentinel custom property the stylesheet declares on `:root`.
78
+ * True when `components.css` is applied to the document, by any route.
79
+ *
80
+ * Two probes, because neither alone covers both deliveries. The computed
81
+ * sentinel on `<html>` is authoritative for a stylesheet applying to the whole
82
+ * document. A host that confines its copy to its player subtree — `@scope
83
+ * (.item-content) { … }`, the documented remedy for these rules reaching host
84
+ * chrome — puts the stylesheet's `:root` rule somewhere it can never match,
85
+ * since `<html>` is not a descendant of the scoping root; the property then
86
+ * reads empty while the stylesheet is present and working. So an empty read
87
+ * falls back to scanning the document's sheets for the sentinel.
46
88
  *
47
89
  * Only meaningful once the document's stylesheets have been applied — a host
48
90
  * that loads CSS via an async `<link>` reads as missing until it lands.
@@ -53,7 +95,9 @@ export function contentStylesPresent() {
53
95
  const value = getComputedStyle(document.documentElement)
54
96
  .getPropertyValue(SENTINEL_PROPERTY)
55
97
  .trim();
56
- return value !== "";
98
+ if (value !== "")
99
+ return true;
100
+ return countContentStyleSheets({ excludeInstalled: false }) > 0;
57
101
  }
58
102
  /**
59
103
  * Installs `cssText` as a document-level stylesheet, once per document.
@@ -91,21 +135,22 @@ export function installContentStyles(cssText, source) {
91
135
  return "installed";
92
136
  }
93
137
  /**
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.
138
+ * Counts content stylesheets in the document, detected by the sentinel property
139
+ * rather than by URL, so a copy arriving as a `<link>`, a bundler-injected
140
+ * `<style>`, or anything else all count the same. `excludeInstalled` narrows the
141
+ * count to copies the host loaded itself.
98
142
  *
99
143
  * Cross-origin sheets throw on `cssRules` access and are skipped; a host copy
100
144
  * served from another origin therefore reads as absent. That only costs a
101
145
  * diagnostic, never correctness.
102
146
  */
103
- const countHostContentStyleSheets = () => {
147
+ const countContentStyleSheets = ({ excludeInstalled, }) => {
104
148
  // Walks the owning elements rather than document.styleSheets: the marker
105
149
  // attribute lives on the element, and CSSStyleSheet.ownerNode is not
106
150
  // universally implemented (happy-dom omits it), which would make our own
107
151
  // installed copy look like a host copy.
108
- const nodes = document.querySelectorAll(`style:not([${MARKER_ATTRIBUTE}]), link[rel~="stylesheet"]:not([${MARKER_ATTRIBUTE}])`);
152
+ const exclusion = excludeInstalled ? `:not([${MARKER_ATTRIBUTE}])` : "";
153
+ const nodes = document.querySelectorAll(`style${exclusion}, link[rel~="stylesheet"]${exclusion}`);
109
154
  let count = 0;
110
155
  for (const node of Array.from(nodes)) {
111
156
  let rules;
@@ -118,7 +163,7 @@ const countHostContentStyleSheets = () => {
118
163
  if (!rules)
119
164
  continue;
120
165
  for (const rule of Array.from(rules)) {
121
- if (rule.style?.getPropertyValue(SENTINEL_PROPERTY)) {
166
+ if (declaresContentStylesSentinel(rule)) {
122
167
  count += 1;
123
168
  break;
124
169
  }
@@ -126,6 +171,8 @@ const countHostContentStyleSheets = () => {
126
171
  }
127
172
  return count;
128
173
  };
174
+ /** Copies the host loaded itself, i.e. not the one this module installed. */
175
+ const countHostContentStyleSheets = () => countContentStyleSheets({ excludeInstalled: true });
129
176
  let auditWarningIssued = false;
130
177
  const pendingChecks = [];
131
178
  /**
@@ -2,6 +2,13 @@ import { FOCUSABLE_SELECTOR, isProgrammaticFocusTarget, } from "./first-focusabl
2
2
  function getFocusableElements(container) {
3
3
  return Array.from(container.querySelectorAll(FOCUSABLE_SELECTOR)).filter((el) => isProgrammaticFocusTarget(el));
4
4
  }
5
+ function getDeepActiveElement(root) {
6
+ let active = root.activeElement;
7
+ while (active?.shadowRoot?.activeElement) {
8
+ active = active.shadowRoot.activeElement;
9
+ }
10
+ return active;
11
+ }
5
12
  function focusInitialTarget(container, initialFocus) {
6
13
  try {
7
14
  if (initialFocus && container.contains(initialFocus)) {
@@ -27,7 +34,7 @@ function focusInitialTarget(container, initialFocus) {
27
34
  */
28
35
  export function createFocusTrap(container, options = {}) {
29
36
  const prev = typeof document !== "undefined"
30
- ? document.activeElement
37
+ ? getDeepActiveElement(document)
31
38
  : null;
32
39
  const wrap = options.wrap ?? true;
33
40
  const onKeydown = (event) => {
@@ -45,7 +52,9 @@ export function createFocusTrap(container, options = {}) {
45
52
  container.focus?.();
46
53
  return;
47
54
  }
48
- const current = document.activeElement;
55
+ const current = getDeepActiveElement(container.getRootNode() instanceof ShadowRoot
56
+ ? container.getRootNode()
57
+ : document);
49
58
  const currentIndex = focusable.indexOf(current || focusable[0]);
50
59
  if (event.shiftKey) {
51
60
  if (currentIndex <= 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pie-players/pie-players-shared",
3
- "version": "0.3.65",
3
+ "version": "0.3.67",
4
4
  "type": "module",
5
5
  "description": "Shared runtime + UI utilities for PIE players",
6
6
  "license": "MIT",