@tastic/hud 0.23.0 → 0.24.0

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/README.md CHANGED
@@ -668,6 +668,64 @@ type-checks hud's source and needs it installed. `AchievementUnlockList` isn't h
668
668
  Metro resolves both entries to the same source files, so an app that imports some components from here and
669
669
  others from `@tastic/hud` still bundles each module once.
670
670
 
671
+ ## Skia on web (`@tastic/hud/skia`)
672
+
673
+ On web, Skia draws through CanvasKit, a WASM file fetched at runtime. `@shopify/react-native-skia` builds
674
+ its `Skia` object from CanvasKit when the module is first evaluated, so a component that imports Skia can't
675
+ even be imported until CanvasKit has loaded. On native there is nothing to wait for.
676
+
677
+ Every app in the fleet loads Skia the same way, with a host pair: two small files per Skia component, side
678
+ by side, that Metro picks between by platform.
679
+
680
+ ```tsx
681
+ // BoardHost.tsx: native renders the board directly, on the first frame
682
+ import { Board, type BoardProps } from './Board'
683
+
684
+ export default function BoardHost(props: BoardProps) {
685
+ return <Board {...props} />
686
+ }
687
+ ```
688
+
689
+ ```tsx
690
+ // BoardHost.web.tsx: web imports the board only once CanvasKit has loaded
691
+ import { loadSkiaWeb } from '@tastic/hud/skia'
692
+ import { lazy, Suspense } from 'react'
693
+
694
+ import type { BoardProps } from './Board'
695
+
696
+ const LazyBoard = lazy(() => loadSkiaWeb().then(() => import('./Board').then((m) => ({ default: m.Board }))))
697
+
698
+ export default function BoardHost(props: BoardProps) {
699
+ return (
700
+ <Suspense fallback={null}>
701
+ <LazyBoard {...props} />
702
+ </Suspense>
703
+ )
704
+ }
705
+ ```
706
+
707
+ - Import the host, never `Board` itself, everywhere else in the app.
708
+ - Keep `lazy()` at module scope, as above, so the component is created once.
709
+ - Import from `@tastic/hud/skia` (also exported from `@tastic/hud`). It carries none of the main entry's
710
+ heavier peers, so it works in every app.
711
+ - A test that imports a native host imports Skia, which throws under Jest (no native bindings). Mock the
712
+ host, or don't import the screen that uses it.
713
+
714
+ `loadSkiaWeb()` fetches the app's `public/canvaskit.wasm` from `<baseUrl>/canvaskit.wasm`: under the Expo base
715
+ URL (`experiments.baseUrl`, e.g. `/snake/canvaskit.wasm`) when the app has one, and `/canvaskit.wasm` when it
716
+ doesn't. There's nothing to wire up: babel-preset-expo inlines `process.env.EXPO_BASE_URL` into hud's source
717
+ when Metro bundles it, the same as in the app's own code. It's idempotent, so every host can call it.
718
+
719
+ That rule is `locateSkiaWebFile(file: string): string`, also exported from `@tastic/hud/skia`, for code that
720
+ calls Skia's own `LoadSkiaWeb` or `WithSkiaWeb` (`opts={{ locateFile: locateSkiaWebFile }}`). Without it,
721
+ canvaskit looks for the file next to the running script or the page, which under a base URL is the wrong
722
+ place: from `/snake` (no trailing slash) it asks for `/canvaskit.wasm`, and a static-output app can end up
723
+ asking under `/_expo/static/js/web/`. `LoadSkiaWeb` also keeps its first call's result, a failed load
724
+ included, for the rest of the page's life.
725
+
726
+ `TriggerGaugeHost` uses the web half on both platforms: the main entry can't import Skia up front, since every
727
+ app that imports `@tastic/hud` would then load Skia, including in its tests.
728
+
671
729
  ## Install
672
730
 
673
731
  Published to the public npm registry as `@tastic/hud`. Everything described above, including the
@@ -710,5 +768,5 @@ resolve module".
710
768
  | `@rific/updater` | >=0.4.0, optional | main entry and `./chrome`: `BaseSettingsDialog`, `UpdateDialog` |
711
769
  | `@tastic/profile` | >=0.4.0, optional | main entry: `PlayerSetupPanel`, `AchievementUnlockList` (`ProfileChip`) |
712
770
  | `@tastic/split-screen` | >=0.5.0, optional | main entry: `useZoneClampedAlign` |
713
- | `@shopify/react-native-skia` | >=2.6.0, optional | `TriggerGaugeHost`'s gauge (lazy-loaded behind `SkiaGate`) and `./skia-gate` |
771
+ | `@shopify/react-native-skia` | >=2.6.0, optional | `TriggerGaugeHost`'s gauge (loaded when one mounts) and `./skia` |
714
772
  | `@tastic/achievements` | >=0.2.0, optional | `AchievementCatalogSection`, `AchievementUnlockList`, type-only (`AchievementCatalogRow`, first exported in 0.2.0; `AchievementTier`); `./chrome` needs it installed only to type-check |
@@ -0,0 +1,14 @@
1
+ // src/loadSkiaWeb.ts
2
+ function loadSkiaWeb() {
3
+ return Promise.resolve();
4
+ }
5
+
6
+ // src/locateSkiaWebFile.ts
7
+ function locateSkiaWebFile(file) {
8
+ return `${process.env.EXPO_BASE_URL ?? ""}/${file}`;
9
+ }
10
+
11
+ export {
12
+ loadSkiaWeb,
13
+ locateSkiaWebFile
14
+ };
package/dist/index.d.mts CHANGED
@@ -5,8 +5,8 @@ import { ReactNode } from 'react';
5
5
  import { AchievementTier } from '@tastic/achievements';
6
6
  import { Profile } from '@tastic/profile';
7
7
  import { ViewStyle, StyleProp } from 'react-native';
8
+ export { loadSkiaWeb, locateSkiaWebFile } from './skia/index.mjs';
8
9
  import { SeedColor } from '@rific/auto-paper';
9
- export { SkiaGate, SkiaGateProps } from './SkiaGate.mjs';
10
10
  import '@tastic/core';
11
11
 
12
12
  interface UnlockedAchievement {
@@ -125,8 +125,6 @@ interface Props$2<T extends string> {
125
125
  }
126
126
  declare function LabeledDropdown<T extends string>({ id, host, options, value, onChange, color, dark, align: forcedAlign, alignOverride }: Props$2<T>): react.JSX.Element;
127
127
 
128
- declare function loadSkiaWeb(): Promise<void>;
129
-
130
128
  declare function getColorPopoverId(idPrefix: string): string;
131
129
  declare function getProfilePopoverId(idPrefix: string): string;
132
130
  declare function getCpuDifficultyPopoverId(idPrefix: string): string;
@@ -210,4 +208,4 @@ declare function useQuitConfirmation(hasProgress: () => boolean, onConfirmedBack
210
208
 
211
209
  declare function useZoneClampedAlign(open: boolean, contentWidth: number, contentHeight: number, rotation?: PopoverRotation): ReturnType<typeof useAutoAlign>;
212
210
 
213
- export { AchievementUnlockList, type AchievementUnlockOwner, AlignResult, ControlSchemePicker, type ControlSchemePickerProps, type CornerStatusBadgeSeat, CornerStatusBadges, type CornerStatusBadgesProps, LABELED_DROPDOWN_POPOVER_WIDTH, LabeledDropdown, type LabeledDropdownOption, type MenuOption, type MenuSection, type MultiSelectSection, type OrientationMode, PlayerSetupPanel, type PlayerSetupPanelProps, PopoverHost, PopoverRotation, type QuitConfirmation, ReadyButton, SectionedDropdown, SharedActionBand, type SingleSelectSection, TriggerGaugeHost, type TriggerGaugeProps, type UnlockedAchievement, getColorPopoverId, getCpuDifficultyPopoverId, getLabeledDropdownContentHeight, getProfilePopoverId, loadSkiaWeb, useAutoAlign, useQuitConfirmation, useZoneClampedAlign };
211
+ export { AchievementUnlockList, type AchievementUnlockOwner, AlignResult, ControlSchemePicker, type ControlSchemePickerProps, type CornerStatusBadgeSeat, CornerStatusBadges, type CornerStatusBadgesProps, LABELED_DROPDOWN_POPOVER_WIDTH, LabeledDropdown, type LabeledDropdownOption, type MenuOption, type MenuSection, type MultiSelectSection, type OrientationMode, PlayerSetupPanel, type PlayerSetupPanelProps, PopoverHost, PopoverRotation, type QuitConfirmation, ReadyButton, SectionedDropdown, SharedActionBand, type SingleSelectSection, TriggerGaugeHost, type TriggerGaugeProps, type UnlockedAchievement, getColorPopoverId, getCpuDifficultyPopoverId, getLabeledDropdownContentHeight, getProfilePopoverId, useAutoAlign, useQuitConfirmation, useZoneClampedAlign };
package/dist/index.d.ts CHANGED
@@ -5,8 +5,8 @@ import { ReactNode } from 'react';
5
5
  import { AchievementTier } from '@tastic/achievements';
6
6
  import { Profile } from '@tastic/profile';
7
7
  import { ViewStyle, StyleProp } from 'react-native';
8
+ export { loadSkiaWeb, locateSkiaWebFile } from './skia/index.js';
8
9
  import { SeedColor } from '@rific/auto-paper';
9
- export { SkiaGate, SkiaGateProps } from './SkiaGate.js';
10
10
  import '@tastic/core';
11
11
 
12
12
  interface UnlockedAchievement {
@@ -125,8 +125,6 @@ interface Props$2<T extends string> {
125
125
  }
126
126
  declare function LabeledDropdown<T extends string>({ id, host, options, value, onChange, color, dark, align: forcedAlign, alignOverride }: Props$2<T>): react.JSX.Element;
127
127
 
128
- declare function loadSkiaWeb(): Promise<void>;
129
-
130
128
  declare function getColorPopoverId(idPrefix: string): string;
131
129
  declare function getProfilePopoverId(idPrefix: string): string;
132
130
  declare function getCpuDifficultyPopoverId(idPrefix: string): string;
@@ -210,4 +208,4 @@ declare function useQuitConfirmation(hasProgress: () => boolean, onConfirmedBack
210
208
 
211
209
  declare function useZoneClampedAlign(open: boolean, contentWidth: number, contentHeight: number, rotation?: PopoverRotation): ReturnType<typeof useAutoAlign>;
212
210
 
213
- export { AchievementUnlockList, type AchievementUnlockOwner, AlignResult, ControlSchemePicker, type ControlSchemePickerProps, type CornerStatusBadgeSeat, CornerStatusBadges, type CornerStatusBadgesProps, LABELED_DROPDOWN_POPOVER_WIDTH, LabeledDropdown, type LabeledDropdownOption, type MenuOption, type MenuSection, type MultiSelectSection, type OrientationMode, PlayerSetupPanel, type PlayerSetupPanelProps, PopoverHost, PopoverRotation, type QuitConfirmation, ReadyButton, SectionedDropdown, SharedActionBand, type SingleSelectSection, TriggerGaugeHost, type TriggerGaugeProps, type UnlockedAchievement, getColorPopoverId, getCpuDifficultyPopoverId, getLabeledDropdownContentHeight, getProfilePopoverId, loadSkiaWeb, useAutoAlign, useQuitConfirmation, useZoneClampedAlign };
211
+ export { AchievementUnlockList, type AchievementUnlockOwner, AlignResult, ControlSchemePicker, type ControlSchemePickerProps, type CornerStatusBadgeSeat, CornerStatusBadges, type CornerStatusBadgesProps, LABELED_DROPDOWN_POPOVER_WIDTH, LabeledDropdown, type LabeledDropdownOption, type MenuOption, type MenuSection, type MultiSelectSection, type OrientationMode, PlayerSetupPanel, type PlayerSetupPanelProps, PopoverHost, PopoverRotation, type QuitConfirmation, ReadyButton, SectionedDropdown, SharedActionBand, type SingleSelectSection, TriggerGaugeHost, type TriggerGaugeProps, type UnlockedAchievement, getColorPopoverId, getCpuDifficultyPopoverId, getLabeledDropdownContentHeight, getProfilePopoverId, useAutoAlign, useQuitConfirmation, useZoneClampedAlign };
package/dist/index.js CHANGED
@@ -38,22 +38,22 @@ __export(TriggerGauge_exports, {
38
38
  function TriggerGauge({ segments, litIndices, size, accentColor, mutedColor, dashWidth, startDeg = DEFAULT_START_DEG, endDeg = startDeg }) {
39
39
  const totalDurationMs = MOUNT_DURATION_MS + Math.max(0, segments - 1) * MOUNT_STAGGER_MS;
40
40
  const mountProgress = (0, import_react_native_reanimated.useSharedValue)(0);
41
- (0, import_react9.useLayoutEffect)(() => {
41
+ (0, import_react8.useLayoutEffect)(() => {
42
42
  mountProgress.value = (0, import_react_native_reanimated.withTiming)(1, { duration: totalDurationMs, easing: import_react_native_reanimated.Easing.out(import_react_native_reanimated.Easing.cubic) });
43
43
  }, []);
44
44
  const rawSweepDeg = ((endDeg - startDeg) % 360 + 360) % 360;
45
45
  const totalSweepDeg = rawSweepDeg === 0 ? 360 : rawSweepDeg;
46
46
  const step = segments > 0 ? totalSweepDeg / segments : 0;
47
- const centerDeg = (0, import_react9.useCallback)((i) => startDeg + (i + 0.5) * step, [startDeg, step]);
47
+ const centerDeg = (0, import_react8.useCallback)((i) => startDeg + (i + 0.5) * step, [startDeg, step]);
48
48
  const singleSelect = litIndices.length === 1;
49
49
  const angleDeg = (0, import_react_native_reanimated.useSharedValue)(centerDeg(litIndices[0] ?? 0));
50
50
  const litIndicesKey = [...litIndices].sort((a, b) => a - b).join(",");
51
- const [transitionFrom, setTransitionFrom] = (0, import_react9.useState)(litIndices);
51
+ const [transitionFrom, setTransitionFrom] = (0, import_react8.useState)(litIndices);
52
52
  const transitionFromKey = [...transitionFrom].sort((a, b) => a - b).join(",");
53
- const lastKeyRef = (0, import_react9.useRef)(litIndicesKey);
54
- const lastIndicesRef = (0, import_react9.useRef)(litIndices);
53
+ const lastKeyRef = (0, import_react8.useRef)(litIndicesKey);
54
+ const lastIndicesRef = (0, import_react8.useRef)(litIndices);
55
55
  const selectionProgress = (0, import_react_native_reanimated.useSharedValue)(1);
56
- (0, import_react9.useLayoutEffect)(() => {
56
+ (0, import_react8.useLayoutEffect)(() => {
57
57
  if (litIndicesKey !== lastKeyRef.current) {
58
58
  if (singleSelect && lastIndicesRef.current.length === 1) {
59
59
  angleDeg.value = (0, import_react_native_reanimated.withTiming)(centerDeg(litIndices[0]), { duration: SELECTION_TRANSITION_DURATION_MS, easing: import_react_native_reanimated.Easing.out(import_react_native_reanimated.Easing.cubic) });
@@ -130,21 +130,21 @@ function TriggerGauge({ segments, litIndices, size, accentColor, mutedColor, das
130
130
  height: canvasSize,
131
131
  width: canvasSize
132
132
  };
133
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_react_native17.View, { style: [styles14.container, gaugeSizeStyle], pointerEvents: "none", children: /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_react_native_skia.Canvas, { style: canvasSizeStyle, children: [
134
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_react_native_skia.Path, { path: unlitPath, style: "stroke", strokeWidth: UNLIT_THICKNESS, strokeCap: "round", color: mutedColor, opacity: 0.35 }),
135
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_react_native_skia.Path, { path: litPath, style: "stroke", strokeWidth: LIT_THICKNESS, strokeCap: "round", color: accentColor, opacity: 1 })
133
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_react_native17.View, { style: [styles14.container, gaugeSizeStyle], pointerEvents: "none", children: /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_react_native_skia.Canvas, { style: canvasSizeStyle, children: [
134
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_react_native_skia.Path, { path: unlitPath, style: "stroke", strokeWidth: UNLIT_THICKNESS, strokeCap: "round", color: mutedColor, opacity: 0.35 }),
135
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_react_native_skia.Path, { path: litPath, style: "stroke", strokeWidth: LIT_THICKNESS, strokeCap: "round", color: accentColor, opacity: 1 })
136
136
  ] }) });
137
137
  }
138
- var import_react_native_skia, import_core8, import_react9, import_react_native17, import_react_native_reanimated, import_jsx_runtime16, UNLIT_THICKNESS, LIT_THICKNESS, DASH_GAP_PX, ARC_GAP, DEFAULT_START_DEG, MIN_DASH_ANGLE_DEG, MOUNT_DURATION_MS, MOUNT_STAGGER_MS, SELECTION_TRANSITION_DURATION_MS, styles14;
138
+ var import_react_native_skia, import_core8, import_react8, import_react_native17, import_react_native_reanimated, import_jsx_runtime15, UNLIT_THICKNESS, LIT_THICKNESS, DASH_GAP_PX, ARC_GAP, DEFAULT_START_DEG, MIN_DASH_ANGLE_DEG, MOUNT_DURATION_MS, MOUNT_STAGGER_MS, SELECTION_TRANSITION_DURATION_MS, styles14;
139
139
  var init_TriggerGauge = __esm({
140
140
  "src/TriggerGauge.tsx"() {
141
141
  "use strict";
142
142
  import_react_native_skia = require("@shopify/react-native-skia");
143
143
  import_core8 = require("@tastic/core");
144
- import_react9 = require("react");
144
+ import_react8 = require("react");
145
145
  import_react_native17 = require("react-native");
146
146
  import_react_native_reanimated = require("react-native-reanimated");
147
- import_jsx_runtime16 = require("react/jsx-runtime");
147
+ import_jsx_runtime15 = require("react/jsx-runtime");
148
148
  UNLIT_THICKNESS = 3;
149
149
  LIT_THICKNESS = 7;
150
150
  DASH_GAP_PX = 9;
@@ -194,7 +194,6 @@ __export(index_exports, {
194
194
  RotatedFrame: () => RotatedFrame,
195
195
  SectionedDropdown: () => SectionedDropdown,
196
196
  SharedActionBand: () => SharedActionBand,
197
- SkiaGate: () => SkiaGate,
198
197
  StaggeredWord: () => StaggeredWord,
199
198
  StatRow: () => StatRow,
200
199
  StatSection: () => StatSection,
@@ -209,6 +208,7 @@ __export(index_exports, {
209
208
  getStaggeredWordDuration: () => getStaggeredWordDuration,
210
209
  isCompactDialogHeight: () => isCompactDialogHeight,
211
210
  loadSkiaWeb: () => loadSkiaWeb,
211
+ locateSkiaWebFile: () => locateSkiaWebFile,
212
212
  overlayActionStyles: () => overlayActionStyles,
213
213
  overlayStyles: () => overlayStyles,
214
214
  useAutoAlign: () => useAutoAlign,
@@ -1114,29 +1114,19 @@ var styles13 = import_react_native16.StyleSheet.create({
1114
1114
  }
1115
1115
  });
1116
1116
 
1117
- // src/SkiaGate.tsx
1118
- var import_react8 = require("react");
1119
- var import_jsx_runtime15 = require("react/jsx-runtime");
1120
- function SkiaGate({ getComponent, componentProps, fallback = null }) {
1121
- const [Component, setComponent] = (0, import_react8.useState)(null);
1122
- (0, import_react8.useEffect)(() => {
1123
- let cancelled = false;
1124
- getComponent().then((mod) => {
1125
- if (!cancelled) setComponent(() => mod.default);
1126
- });
1127
- return () => {
1128
- cancelled = true;
1129
- };
1130
- }, [getComponent]);
1131
- if (!Component) return fallback;
1132
- return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Component, { ...componentProps });
1117
+ // src/TriggerGaugeHost.tsx
1118
+ var import_react9 = require("react");
1119
+
1120
+ // src/loadSkiaWeb.ts
1121
+ function loadSkiaWeb() {
1122
+ return Promise.resolve();
1133
1123
  }
1134
1124
 
1135
1125
  // src/TriggerGaugeHost.tsx
1136
- var import_jsx_runtime17 = require("react/jsx-runtime");
1137
- var getTriggerGauge = () => Promise.resolve().then(() => (init_TriggerGauge(), TriggerGauge_exports)).then((mod) => ({ default: mod.TriggerGauge }));
1126
+ var import_jsx_runtime16 = require("react/jsx-runtime");
1127
+ var LazyTriggerGauge = (0, import_react9.lazy)(() => loadSkiaWeb().then(() => Promise.resolve().then(() => (init_TriggerGauge(), TriggerGauge_exports)).then((mod) => ({ default: mod.TriggerGauge }))));
1138
1128
  function TriggerGaugeHost(props) {
1139
- return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(SkiaGate, { getComponent: getTriggerGauge, componentProps: props });
1129
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_react9.Suspense, { fallback: null, children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(LazyTriggerGauge, { ...props }) });
1140
1130
  }
1141
1131
 
1142
1132
  // src/useAutoAlign.ts
@@ -1188,7 +1178,7 @@ function useAutoAlign(open, contentWidth, contentHeight, rotation = 0) {
1188
1178
  }
1189
1179
 
1190
1180
  // src/SectionedDropdown.tsx
1191
- var import_jsx_runtime18 = require("react/jsx-runtime");
1181
+ var import_jsx_runtime17 = require("react/jsx-runtime");
1192
1182
  var MENU_BORDER_WIDTH = 1;
1193
1183
  var TRIGGER_SIZE = 44;
1194
1184
  var MENU_MIN_WIDTH = 200;
@@ -1241,14 +1231,14 @@ function SectionedDropdown({ id, host, icon, accessibilityLabel, sections, accen
1241
1231
  const anchorStyle = [styles15.anchor, open && styles15.anchorOpen];
1242
1232
  const menuStyle = { backgroundColor: menuBg, borderColor: menuBorderColor, maxHeight };
1243
1233
  const dividerStyle = { backgroundColor: menuBorderColor };
1244
- return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(import_react_native19.View, { style: anchorStyle, children: [
1245
- /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(import_react_native19.View, { ref: triggerRef, collapsable: false, style: styles15.triggerBox, children: [
1246
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(TriggerGaugeHost, { segments: gaugeSegments, litIndices: gaugeLitIndices, size: TRIGGER_SIZE, accentColor, mutedColor }),
1247
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_feedback_press4.IconButton, { icon: triggerIcon, iconColor: triggerIconColor, size: triggerIconSize, accessibilityLabel, onPress: () => host.toggle(id) })
1234
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_react_native19.View, { style: anchorStyle, children: [
1235
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_react_native19.View, { ref: triggerRef, collapsable: false, style: styles15.triggerBox, children: [
1236
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(TriggerGaugeHost, { segments: gaugeSegments, litIndices: gaugeLitIndices, size: TRIGGER_SIZE, accentColor, mutedColor }),
1237
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_feedback_press4.IconButton, { icon: triggerIcon, iconColor: triggerIconColor, size: triggerIconSize, accessibilityLabel, onPress: () => host.toggle(id) })
1248
1238
  ] }),
1249
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(PopoverBody, { visible: open && measured, align, verticalAlign, caretColor: menuBg, caretBorderColor: menuBorderColor, caretBorderWidth: MENU_BORDER_WIDTH, triggerSize: TRIGGER_SIZE, children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native19.ScrollView, { style: [styles15.menu, menuStyle], contentContainerStyle: styles15.menuContent, showsVerticalScrollIndicator: false, children: sections.map((section, sectionIndex) => /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(import_react_native19.View, { children: [
1250
- sectionIndex > 0 && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native19.View, { style: [styles15.divider, dividerStyle] }),
1251
- section.kind === "single" ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
1239
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(PopoverBody, { visible: open && measured, align, verticalAlign, caretColor: menuBg, caretBorderColor: menuBorderColor, caretBorderWidth: MENU_BORDER_WIDTH, triggerSize: TRIGGER_SIZE, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_react_native19.ScrollView, { style: [styles15.menu, menuStyle], contentContainerStyle: styles15.menuContent, showsVerticalScrollIndicator: false, children: sections.map((section, sectionIndex) => /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_react_native19.View, { children: [
1240
+ sectionIndex > 0 && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_react_native19.View, { style: [styles15.divider, dividerStyle] }),
1241
+ section.kind === "single" ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
1252
1242
  SingleSection,
1253
1243
  {
1254
1244
  section,
@@ -1261,7 +1251,7 @@ function SectionedDropdown({ id, host, icon, accessibilityLabel, sections, accen
1261
1251
  if (autoDismiss) host.close();
1262
1252
  }
1263
1253
  }
1264
- ) : /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(MultiSection, { section, accentColor, onAccentColor: resolvedOnAccentColor, mutedColor, labelFontFamily, allClearLabels })
1254
+ ) : /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(MultiSection, { section, accentColor, onAccentColor: resolvedOnAccentColor, mutedColor, labelFontFamily, allClearLabels })
1265
1255
  ] }, section.id)) }) })
1266
1256
  ] });
1267
1257
  }
@@ -1273,18 +1263,18 @@ function SingleSection({
1273
1263
  labelFontFamily,
1274
1264
  onSelect
1275
1265
  }) {
1276
- return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_jsx_runtime18.Fragment, { children: section.options.map((option) => {
1266
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_jsx_runtime17.Fragment, { children: section.options.map((option) => {
1277
1267
  const selected = option.value === section.value;
1278
1268
  const taken = !selected && option.value === section.takenValue;
1279
1269
  const rowColor = selected ? onAccentColor : mutedColor;
1280
1270
  const itemStyle = [styles15.item, selected && { backgroundColor: accentColor }, taken && styles15.itemTaken];
1281
1271
  const labelStyle = { fontFamily: labelFontFamily, color: rowColor, fontWeight: selected ? "bold" : "normal" };
1282
1272
  const descriptionStyle = { fontFamily: labelFontFamily, color: rowColor };
1283
- return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_feedback_press4.TouchableRipple, { disabled: taken, onPress: () => onSelect(option.value), style: itemStyle, children: /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(import_react_native19.View, { style: styles15.itemRow, children: [
1284
- option.icon && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native_paper11.Icon, { source: option.icon, size: MENU_ITEM_ICON_SIZE, color: rowColor }),
1285
- /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(import_react_native19.View, { style: styles15.itemLabelColumn, children: [
1286
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native_paper11.Text, { variant: "labelLarge", style: labelStyle, children: option.label }),
1287
- option.description && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native_paper11.Text, { variant: "labelSmall", style: descriptionStyle, children: option.description })
1273
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_feedback_press4.TouchableRipple, { disabled: taken, onPress: () => onSelect(option.value), style: itemStyle, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_react_native19.View, { style: styles15.itemRow, children: [
1274
+ option.icon && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_react_native_paper11.Icon, { source: option.icon, size: MENU_ITEM_ICON_SIZE, color: rowColor }),
1275
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_react_native19.View, { style: styles15.itemLabelColumn, children: [
1276
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_react_native_paper11.Text, { variant: "labelLarge", style: labelStyle, children: option.label }),
1277
+ option.description && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_react_native_paper11.Text, { variant: "labelSmall", style: descriptionStyle, children: option.description })
1288
1278
  ] })
1289
1279
  ] }) }, option.value);
1290
1280
  }) });
@@ -1298,7 +1288,7 @@ function MultiSection({
1298
1288
  allClearLabels
1299
1289
  }) {
1300
1290
  const allSelected = section.value.length === section.options.length;
1301
- return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(import_jsx_runtime18.Fragment, { children: [
1291
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_jsx_runtime17.Fragment, { children: [
1302
1292
  section.options.map((option, i) => {
1303
1293
  const selected = section.value.includes(option.value);
1304
1294
  const rowColor = selected ? onAccentColor : mutedColor;
@@ -1306,12 +1296,12 @@ function MultiSection({
1306
1296
  const nextSelected = selected && i < section.options.length - 1 && section.value.includes(section.options[i + 1].value);
1307
1297
  const itemStyle = [styles15.item, selected && { backgroundColor: accentColor }, prevSelected && { borderTopLeftRadius: 0, borderTopRightRadius: 0 }, nextSelected && { borderBottomLeftRadius: 0, borderBottomRightRadius: 0 }];
1308
1298
  const labelStyle = { fontFamily: labelFontFamily, color: rowColor, fontWeight: selected ? "bold" : "normal" };
1309
- return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_feedback_press4.TouchableRipple, { onPress: () => section.onChange(selected ? section.value.filter((v) => v !== option.value) : [...section.value, option.value]), style: itemStyle, children: /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(import_react_native19.View, { style: styles15.itemRow, children: [
1310
- option.icon && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native_paper11.Icon, { source: option.icon, size: MENU_ITEM_ICON_SIZE, color: rowColor }),
1311
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native_paper11.Text, { variant: "labelLarge", style: [styles15.itemLabelFlex, labelStyle], children: option.label })
1299
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_feedback_press4.TouchableRipple, { onPress: () => section.onChange(selected ? section.value.filter((v) => v !== option.value) : [...section.value, option.value]), style: itemStyle, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_react_native19.View, { style: styles15.itemRow, children: [
1300
+ option.icon && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_react_native_paper11.Icon, { source: option.icon, size: MENU_ITEM_ICON_SIZE, color: rowColor }),
1301
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_react_native_paper11.Text, { variant: "labelLarge", style: [styles15.itemLabelFlex, labelStyle], children: option.label })
1312
1302
  ] }) }, option.value);
1313
1303
  }),
1314
- section.allClear && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_feedback_press4.TouchableRipple, { onPress: () => section.onChange(allSelected ? [] : section.options.map((o) => o.value)), style: styles15.item, children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native_paper11.Text, { variant: "labelLarge", style: [styles15.allClearLabel, { fontFamily: labelFontFamily, color: accentColor }], children: allSelected ? allClearLabels.clear : allClearLabels.all }) })
1304
+ section.allClear && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_feedback_press4.TouchableRipple, { onPress: () => section.onChange(allSelected ? [] : section.options.map((o) => o.value)), style: styles15.item, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_react_native_paper11.Text, { variant: "labelLarge", style: [styles15.allClearLabel, { fontFamily: labelFontFamily, color: accentColor }], children: allSelected ? allClearLabels.clear : allClearLabels.all }) })
1315
1305
  ] });
1316
1306
  }
1317
1307
  var styles15 = import_react_native19.StyleSheet.create({
@@ -1384,21 +1374,21 @@ var styles15 = import_react_native19.StyleSheet.create({
1384
1374
  });
1385
1375
 
1386
1376
  // src/ControlSchemePicker.tsx
1387
- var import_jsx_runtime19 = require("react/jsx-runtime");
1377
+ var import_jsx_runtime18 = require("react/jsx-runtime");
1388
1378
  function ControlSchemePicker({ id, host, value, onChange, options, takenValue, accentColor, mutedColor, onAccentColor, dark, align, alignOverride, rotation, labelFontFamily }) {
1389
1379
  const isTouchPrimary = (0, import_core10.useIsTouchPrimaryDevice)();
1390
1380
  if (import_react_native20.Platform.OS !== "web" || isTouchPrimary) return null;
1391
- return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(SectionedDropdown, { id, host, icon: "keyboard-outline", accessibilityLabel: "Control scheme", sections: [{ kind: "single", id: "controls", options, value, onChange, takenValue }], accentColor, mutedColor, onAccentColor, dark, align, alignOverride, rotation, labelFontFamily });
1381
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(SectionedDropdown, { id, host, icon: "keyboard-outline", accessibilityLabel: "Control scheme", sections: [{ kind: "single", id: "controls", options, value, onChange, takenValue }], accentColor, mutedColor, onAccentColor, dark, align, alignOverride, rotation, labelFontFamily });
1392
1382
  }
1393
1383
 
1394
1384
  // src/CornerActionButtons.tsx
1395
1385
  var import_feedback_press5 = require("@rific/feedback-press");
1396
1386
  var import_react_native21 = require("react-native");
1397
- var import_jsx_runtime20 = require("react/jsx-runtime");
1387
+ var import_jsx_runtime19 = require("react/jsx-runtime");
1398
1388
  function CornerActionButtons({ onBack, onSettings, fg, insets, backIcon = "arrow-left", backLabel = "Back", hitSlop }) {
1399
- return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(import_jsx_runtime20.Fragment, { children: [
1400
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_feedback_press5.IconButton, { icon: backIcon, iconColor: fg, size: 24, style: [styles16.back, { top: 8 + insets.top, left: 8 + insets.left }], hitSlop, onPress: onBack, accessibilityLabel: backLabel }),
1401
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_feedback_press5.IconButton, { icon: "cog", iconColor: fg, size: 24, style: [styles16.topRight, { top: 8 + insets.top, right: 8 + insets.right }], hitSlop, onPress: onSettings, accessibilityLabel: "Settings" })
1389
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(import_jsx_runtime19.Fragment, { children: [
1390
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_feedback_press5.IconButton, { icon: backIcon, iconColor: fg, size: 24, style: [styles16.back, { top: 8 + insets.top, left: 8 + insets.left }], hitSlop, onPress: onBack, accessibilityLabel: backLabel }),
1391
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_feedback_press5.IconButton, { icon: "cog", iconColor: fg, size: 24, style: [styles16.topRight, { top: 8 + insets.top, right: 8 + insets.right }], hitSlop, onPress: onSettings, accessibilityLabel: "Settings" })
1402
1392
  ] });
1403
1393
  }
1404
1394
  var styles16 = import_react_native21.StyleSheet.create({
@@ -1413,17 +1403,17 @@ var styles16 = import_react_native21.StyleSheet.create({
1413
1403
  // src/CornerStatusBadges.tsx
1414
1404
  var import_react_native22 = require("react-native");
1415
1405
  var import_react_native_paper12 = require("react-native-paper");
1416
- var import_jsx_runtime21 = require("react/jsx-runtime");
1406
+ var import_jsx_runtime20 = require("react/jsx-runtime");
1417
1407
  var BADGE_SIZE = 30;
1418
1408
  function StatusBadge({ icon }) {
1419
- return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_react_native22.View, { style: styles17.badge, children: icon && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_react_native_paper12.Icon, { source: icon, size: 16, color: "rgba(255,255,255,0.85)" }) });
1409
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_react_native22.View, { style: styles17.badge, children: icon && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_react_native_paper12.Icon, { source: icon, size: 16, color: "rgba(255,255,255,0.85)" }) });
1420
1410
  }
1421
1411
  function CornerStatusBadges({ orientationMode, p1OnRight, p1, p2 }) {
1422
1412
  const p1OnRightSide = orientationMode === "sideBySide" && p1OnRight;
1423
1413
  const p2OnLeftSide = orientationMode === "sideBySide" && !p1OnRight;
1424
- return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(import_jsx_runtime21.Fragment, { children: [
1425
- p1 && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_react_native22.View, { pointerEvents: "none", style: p1OnRightSide ? styles17.bottomRight : styles17.bottomLeft, children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(StatusBadge, { icon: p1.icon }) }),
1426
- p2 && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_react_native22.View, { pointerEvents: "none", style: p2OnLeftSide ? styles17.topLeft : styles17.topRight, children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(StatusBadge, { icon: p2.icon }) })
1414
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(import_jsx_runtime20.Fragment, { children: [
1415
+ p1 && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_react_native22.View, { pointerEvents: "none", style: p1OnRightSide ? styles17.bottomRight : styles17.bottomLeft, children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(StatusBadge, { icon: p1.icon }) }),
1416
+ p2 && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_react_native22.View, { pointerEvents: "none", style: p2OnLeftSide ? styles17.topLeft : styles17.topRight, children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(StatusBadge, { icon: p2.icon }) })
1427
1417
  ] });
1428
1418
  }
1429
1419
  var styles17 = import_react_native22.StyleSheet.create({
@@ -1450,7 +1440,7 @@ var import_feedback_press6 = require("@rific/feedback-press");
1450
1440
  var import_core11 = require("@tastic/core");
1451
1441
  var import_react_native23 = require("react-native");
1452
1442
  var import_react_native_paper13 = require("react-native-paper");
1453
- var import_jsx_runtime22 = require("react/jsx-runtime");
1443
+ var import_jsx_runtime21 = require("react/jsx-runtime");
1454
1444
  var EMOJI_PATTERN = /\p{Extended_Pictographic}/u;
1455
1445
  var DEFAULT_SIZE = 48;
1456
1446
  var SWATCH_SIZE = 28;
@@ -1486,14 +1476,14 @@ function InlineColorPicker({ id, host, value, onChange, previewValue, swatches =
1486
1476
  const displayValue = displayFor(value);
1487
1477
  const contrastColor = (0, import_auto_paper10.getContrastColor)(displayValue);
1488
1478
  const tagFontSize = size * (tag && EMOJI_PATTERN.test(tag) ? 0.6 : 0.4);
1489
- return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(import_react_native23.View, { style: [styles18.anchor, open && styles18.anchorOpen], children: [
1490
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_feedback_press6.TouchableRipple, { onPress: () => host.toggle(id), borderless: true, style: [styles18.trigger, { backgroundColor: displayValue, borderRadius: size / 2, height: size, width: size }], children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_react_native23.View, { ref: triggerRef, collapsable: false, style: styles18.triggerMeasure, children: tag ? /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_react_native_paper13.Text, { style: [styles18.tagLabel, { color: contrastColor, fontFamily: labelFontFamily, fontSize: tagFontSize }], numberOfLines: 1, adjustsFontSizeToFit: true, children: tag }) : /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_react_native_paper13.Icon, { source: icon, size: size * 0.6, color: contrastColor }) }) }),
1491
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(PopoverBody, { visible: open && measured, align, verticalAlign, caretColor: menuBg, caretBorderColor: displayValue, caretBorderWidth: SWATCHES_BORDER_WIDTH, triggerSize: size, children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_react_native23.ScrollView, { style: [styles18.swatches, { backgroundColor: menuBg, borderColor: displayValue, width: swatchesWidth, maxHeight }], contentContainerStyle: styles18.swatchesContent, showsVerticalScrollIndicator: false, children: swatches.map((swatch) => {
1479
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(import_react_native23.View, { style: [styles18.anchor, open && styles18.anchorOpen], children: [
1480
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_feedback_press6.TouchableRipple, { onPress: () => host.toggle(id), borderless: true, style: [styles18.trigger, { backgroundColor: displayValue, borderRadius: size / 2, height: size, width: size }], children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_react_native23.View, { ref: triggerRef, collapsable: false, style: styles18.triggerMeasure, children: tag ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_react_native_paper13.Text, { style: [styles18.tagLabel, { color: contrastColor, fontFamily: labelFontFamily, fontSize: tagFontSize }], numberOfLines: 1, adjustsFontSizeToFit: true, children: tag }) : /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_react_native_paper13.Icon, { source: icon, size: size * 0.6, color: contrastColor }) }) }),
1481
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(PopoverBody, { visible: open && measured, align, verticalAlign, caretColor: menuBg, caretBorderColor: displayValue, caretBorderWidth: SWATCHES_BORDER_WIDTH, triggerSize: size, children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_react_native23.ScrollView, { style: [styles18.swatches, { backgroundColor: menuBg, borderColor: displayValue, width: swatchesWidth, maxHeight }], contentContainerStyle: styles18.swatchesContent, showsVerticalScrollIndicator: false, children: swatches.map((swatch) => {
1492
1482
  const selected = swatch.value.toLowerCase() === value.toLowerCase();
1493
1483
  const taken = !selected && !!takenValue && swatch.value.toLowerCase() === takenValue.toLowerCase();
1494
1484
  const swappable = taken && allowSwapTaken;
1495
1485
  const swatchDisplay = displayFor(swatch.value);
1496
- return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
1486
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
1497
1487
  import_feedback_press6.TouchableRipple,
1498
1488
  {
1499
1489
  disabled: taken && !swappable,
@@ -1503,7 +1493,7 @@ function InlineColorPicker({ id, host, value, onChange, previewValue, swatches =
1503
1493
  },
1504
1494
  borderless: true,
1505
1495
  style: [styles18.swatch, { backgroundColor: swatchDisplay }, selected && styles18.swatchSelected, taken && !swappable && styles18.swatchTaken],
1506
- children: selected ? /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_react_native_paper13.Icon, { source: "check", size: 16, color: (0, import_auto_paper10.getContrastColor)(swatchDisplay) }) : swappable ? /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_react_native_paper13.Icon, { source: "swap-horizontal", size: 16, color: (0, import_auto_paper10.getContrastColor)(swatchDisplay) }) : taken ? /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_react_native_paper13.Icon, { source: "close", size: 16, color: (0, import_auto_paper10.getContrastColor)(swatchDisplay) }) : /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_react_native23.View, {})
1496
+ children: selected ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_react_native_paper13.Icon, { source: "check", size: 16, color: (0, import_auto_paper10.getContrastColor)(swatchDisplay) }) : swappable ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_react_native_paper13.Icon, { source: "swap-horizontal", size: 16, color: (0, import_auto_paper10.getContrastColor)(swatchDisplay) }) : taken ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_react_native_paper13.Icon, { source: "close", size: 16, color: (0, import_auto_paper10.getContrastColor)(swatchDisplay) }) : /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_react_native23.View, {})
1507
1497
  },
1508
1498
  swatch.value
1509
1499
  );
@@ -1586,7 +1576,7 @@ var import_auto_paper11 = require("@rific/auto-paper");
1586
1576
  var import_feedback_press7 = require("@rific/feedback-press");
1587
1577
  var import_react_native24 = require("react-native");
1588
1578
  var import_react_native_paper14 = require("react-native-paper");
1589
- var import_jsx_runtime23 = require("react/jsx-runtime");
1579
+ var import_jsx_runtime22 = require("react/jsx-runtime");
1590
1580
  var TRIGGER_HEIGHT = 28;
1591
1581
  var POPOVER_WIDTH = 180;
1592
1582
  var ROW_HEIGHT = 36;
@@ -1609,17 +1599,17 @@ function LabeledDropdown({ id, host, options, value, onChange, color, dark, alig
1609
1599
  onChange(option.value);
1610
1600
  host.close();
1611
1601
  };
1612
- return /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(import_react_native24.View, { style: [styles19.anchor, open && styles19.anchorOpen], children: [
1613
- /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_feedback_press7.TouchableRipple, { onPress: () => host.toggle(id), style: styles19.trigger, children: /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(import_react_native24.View, { ref: triggerRef, collapsable: false, style: styles19.triggerInner, children: [
1614
- /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_react_native_paper14.Text, { style: [styles19.triggerLabel, { color }], numberOfLines: 1, children: (selected?.label ?? "").toUpperCase() }),
1615
- /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_react_native_paper14.Icon, { source: "menu-down", size: 16, color })
1602
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(import_react_native24.View, { style: [styles19.anchor, open && styles19.anchorOpen], children: [
1603
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_feedback_press7.TouchableRipple, { onPress: () => host.toggle(id), style: styles19.trigger, children: /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(import_react_native24.View, { ref: triggerRef, collapsable: false, style: styles19.triggerInner, children: [
1604
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_react_native_paper14.Text, { style: [styles19.triggerLabel, { color }], numberOfLines: 1, children: (selected?.label ?? "").toUpperCase() }),
1605
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_react_native_paper14.Icon, { source: "menu-down", size: 16, color })
1616
1606
  ] }) }),
1617
- /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(PopoverBody, { visible: open && measured, align, verticalAlign, caretColor: menuBg, caretBorderColor: color, caretBorderWidth: 2, triggerSize: TRIGGER_HEIGHT, children: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_react_native24.ScrollView, { style: [styles19.menu, { backgroundColor: menuBg, borderColor: color, width: POPOVER_WIDTH, maxHeight }], contentContainerStyle: styles19.menuContent, showsVerticalScrollIndicator: false, children: options.map((option) => {
1607
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(PopoverBody, { visible: open && measured, align, verticalAlign, caretColor: menuBg, caretBorderColor: color, caretBorderWidth: 2, triggerSize: TRIGGER_HEIGHT, children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_react_native24.ScrollView, { style: [styles19.menu, { backgroundColor: menuBg, borderColor: color, width: POPOVER_WIDTH, maxHeight }], contentContainerStyle: styles19.menuContent, showsVerticalScrollIndicator: false, children: options.map((option) => {
1618
1608
  const isSelected = option.value === value;
1619
1609
  const onColor = (0, import_auto_paper11.getContrastColor)(color);
1620
- return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_feedback_press7.TouchableRipple, { onPress: () => handleSelect(option), style: [styles19.row, isSelected && { backgroundColor: color }], children: /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(import_react_native24.View, { style: styles19.rowInner, children: [
1621
- option.icon && /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_react_native_paper14.Icon, { source: option.icon, size: 18, color: isSelected ? onColor : fg }),
1622
- /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_react_native_paper14.Text, { style: [styles19.rowLabel, { color: isSelected ? onColor : fg }], numberOfLines: 1, children: option.label })
1610
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_feedback_press7.TouchableRipple, { onPress: () => handleSelect(option), style: [styles19.row, isSelected && { backgroundColor: color }], children: /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(import_react_native24.View, { style: styles19.rowInner, children: [
1611
+ option.icon && /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_react_native_paper14.Icon, { source: option.icon, size: 18, color: isSelected ? onColor : fg }),
1612
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_react_native_paper14.Text, { style: [styles19.rowLabel, { color: isSelected ? onColor : fg }], numberOfLines: 1, children: option.label })
1623
1613
  ] }) }, option.value);
1624
1614
  }) }) })
1625
1615
  ] });
@@ -1676,9 +1666,9 @@ var styles19 = import_react_native24.StyleSheet.create({
1676
1666
  }
1677
1667
  });
1678
1668
 
1679
- // src/loadSkiaWeb.ts
1680
- function loadSkiaWeb() {
1681
- return Promise.resolve();
1669
+ // src/locateSkiaWebFile.ts
1670
+ function locateSkiaWebFile(file) {
1671
+ return `${process.env.EXPO_BASE_URL ?? ""}/${file}`;
1682
1672
  }
1683
1673
 
1684
1674
  // src/PlayerSetupPanel.tsx
@@ -1690,12 +1680,12 @@ var import_react_native_paper16 = require("react-native-paper");
1690
1680
  var import_feedback_press8 = require("@rific/feedback-press");
1691
1681
  var import_react_native25 = require("react-native");
1692
1682
  var import_react_native_paper15 = require("react-native-paper");
1693
- var import_jsx_runtime24 = require("react/jsx-runtime");
1683
+ var import_jsx_runtime23 = require("react/jsx-runtime");
1694
1684
  function ReadyButton({ color, ready, onToggleReady, style, labelFontFamily = MONO_FONT }) {
1695
1685
  const readyButtonStyle = [styles20.readyButton, { borderColor: color }, ready && { backgroundColor: color }, style];
1696
1686
  const labelTextStyle = { fontFamily: labelFontFamily, fontWeight: "bold" };
1697
1687
  const labelColorStyle = { color: ready ? "#000000" : color };
1698
- return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_feedback_press8.TouchableRipple, { onPress: onToggleReady, borderless: true, style: readyButtonStyle, children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_react_native_paper15.Text, { variant: "labelLarge", style: [labelTextStyle, labelColorStyle], children: ready ? "READY \u2713" : "READY?" }) });
1688
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_feedback_press8.TouchableRipple, { onPress: onToggleReady, borderless: true, style: readyButtonStyle, children: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_react_native_paper15.Text, { variant: "labelLarge", style: [labelTextStyle, labelColorStyle], children: ready ? "READY \u2713" : "READY?" }) });
1699
1689
  }
1700
1690
  var styles20 = import_react_native25.StyleSheet.create({
1701
1691
  readyButton: {
@@ -1760,7 +1750,7 @@ function useZoneClampedAlign(open, contentWidth, contentHeight, rotation = 0) {
1760
1750
  }
1761
1751
 
1762
1752
  // src/PlayerSetupPanel.tsx
1763
- var import_jsx_runtime25 = require("react/jsx-runtime");
1753
+ var import_jsx_runtime24 = require("react/jsx-runtime");
1764
1754
  function getColorPopoverId(idPrefix) {
1765
1755
  return `${idPrefix}-color`;
1766
1756
  }
@@ -1783,13 +1773,13 @@ function PlayerSetupPanel({ idPrefix, host, label, labelFontFamily, color, onCol
1783
1773
  const seatPickerId = showProfilePicker ? getProfilePopoverId(idPrefix) : getCpuDifficultyPopoverId(idPrefix);
1784
1774
  const seatPickerSize = showProfilePicker ? (0, import_profile2.getProfilePickerContentSize)(profiles.length, !!onManageProfiles, colorSection) : (0, import_profile2.getOptionPickerContentSize)(cpuDifficultyOptions?.length ?? 0, colorSection);
1785
1775
  const seatPickerAlign = useZoneClampedAlign(seatButton && popover.openId === seatPickerId, seatPickerSize.width, seatPickerSize.height);
1786
- return /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(import_react_native27.View, { style: [styles21.panel, ownPopoverOpen && styles21.panelOpen], children: [
1787
- showProfilePicker ? /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(import_profile2.ProfilePicker, { idPrefix, host: popover, profiles, selectedId: selectedProfileId ?? null, takenId: takenProfileId, color, dark, guestLabel: guestLabel ?? "GUEST", onSelect: (profile) => onProfileSelect?.(profile), onManage: onManageProfiles, colorSection, alignOverride: seatPickerAlign }) : showCpuDifficulty ? /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(import_profile2.OptionPicker, { id: getCpuDifficultyPopoverId(idPrefix), host: popover, options: cpuDifficultyOptions, value: cpuDifficulty, onChange: onCpuDifficultyChange, color, dark, icon: cpuIcon, colorSection, alignOverride: seatPickerAlign }) : label && /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(import_react_native_paper16.Text, { variant: "labelSmall", style: { color, fontFamily: labelFontFamily }, children: label }),
1788
- (!seatButton || secondPicker) && /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(import_react_native27.View, { style: [styles21.pickerRow, pickerRowPopoverOpen && styles21.pickerRowOpen], children: [
1789
- !seatButton && /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(InlineColorPicker, { id: getColorPopoverId(idPrefix), host: popover, value: color, onChange: onColorChange, swatches, takenValue: takenColor, allowSwapTaken, alignOverride: colorAlignOverride, rotation: colorRotation, dark, tag: isHuman ? selectedProfile?.tag : void 0, icon: isHuman ? humanIcon : cpuIcon }),
1776
+ return /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(import_react_native27.View, { style: [styles21.panel, ownPopoverOpen && styles21.panelOpen], children: [
1777
+ showProfilePicker ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_profile2.ProfilePicker, { idPrefix, host: popover, profiles, selectedId: selectedProfileId ?? null, takenId: takenProfileId, color, dark, guestLabel: guestLabel ?? "GUEST", onSelect: (profile) => onProfileSelect?.(profile), onManage: onManageProfiles, colorSection, alignOverride: seatPickerAlign }) : showCpuDifficulty ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_profile2.OptionPicker, { id: getCpuDifficultyPopoverId(idPrefix), host: popover, options: cpuDifficultyOptions, value: cpuDifficulty, onChange: onCpuDifficultyChange, color, dark, icon: cpuIcon, colorSection, alignOverride: seatPickerAlign }) : label && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_react_native_paper16.Text, { variant: "labelSmall", style: { color, fontFamily: labelFontFamily }, children: label }),
1778
+ (!seatButton || secondPicker) && /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(import_react_native27.View, { style: [styles21.pickerRow, pickerRowPopoverOpen && styles21.pickerRowOpen], children: [
1779
+ !seatButton && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(InlineColorPicker, { id: getColorPopoverId(idPrefix), host: popover, value: color, onChange: onColorChange, swatches, takenValue: takenColor, allowSwapTaken, alignOverride: colorAlignOverride, rotation: colorRotation, dark, tag: isHuman ? selectedProfile?.tag : void 0, icon: isHuman ? humanIcon : cpuIcon }),
1790
1780
  secondPicker
1791
1781
  ] }),
1792
- isHuman && showReadyButton && onToggleReady && /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(ReadyButton, { color, ready: ready ?? false, onToggleReady })
1782
+ isHuman && showReadyButton && onToggleReady && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(ReadyButton, { color, ready: ready ?? false, onToggleReady })
1793
1783
  ] });
1794
1784
  }
1795
1785
  var styles21 = import_react_native27.StyleSheet.create({
@@ -1825,23 +1815,23 @@ var styles21 = import_react_native27.StyleSheet.create({
1825
1815
 
1826
1816
  // src/PressAwayOverlay.tsx
1827
1817
  var import_react_native28 = require("react-native");
1828
- var import_jsx_runtime26 = require("react/jsx-runtime");
1818
+ var import_jsx_runtime25 = require("react/jsx-runtime");
1829
1819
  function PressAwayOverlay({ active, onPress, style }) {
1830
1820
  if (!active) return null;
1831
- return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_react_native28.Pressable, { accessibilityLabel: "Dismiss", accessibilityRole: "button", onPress, style: [import_react_native28.StyleSheet.absoluteFill, style] });
1821
+ return /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(import_react_native28.Pressable, { accessibilityLabel: "Dismiss", accessibilityRole: "button", onPress, style: [import_react_native28.StyleSheet.absoluteFill, style] });
1832
1822
  }
1833
1823
 
1834
1824
  // src/SharedActionBand.tsx
1835
1825
  var import_feedback_press9 = require("@rific/feedback-press");
1836
1826
  var import_react_native29 = require("react-native");
1837
- var import_jsx_runtime27 = require("react/jsx-runtime");
1827
+ var import_jsx_runtime26 = require("react/jsx-runtime");
1838
1828
  function SharedActionBand({ onBack, onSettings, onRandomize, onReset, fg, popoverOpen, children }) {
1839
- return /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(import_react_native29.View, { style: [styles22.column, popoverOpen && styles22.columnOpen], children: [
1840
- /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(import_react_native29.View, { style: styles22.actionsRow, children: [
1841
- /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_feedback_press9.IconButton, { icon: "arrow-left", iconColor: fg, size: 20, onPress: onBack, accessibilityLabel: "Back" }),
1842
- onRandomize && /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_feedback_press9.IconButton, { icon: "dice-multiple", iconColor: fg, size: 18, onPress: onRandomize, accessibilityLabel: "Randomize match settings" }),
1843
- onReset && /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_feedback_press9.IconButton, { icon: "restore", iconColor: fg, size: 18, onPress: onReset, accessibilityLabel: "Reset match settings to defaults" }),
1844
- /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_feedback_press9.IconButton, { icon: "cog", iconColor: fg, size: 20, onPress: onSettings, accessibilityLabel: "Settings" })
1829
+ return /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(import_react_native29.View, { style: [styles22.column, popoverOpen && styles22.columnOpen], children: [
1830
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(import_react_native29.View, { style: styles22.actionsRow, children: [
1831
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_feedback_press9.IconButton, { icon: "arrow-left", iconColor: fg, size: 20, onPress: onBack, accessibilityLabel: "Back" }),
1832
+ onRandomize && /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_feedback_press9.IconButton, { icon: "dice-multiple", iconColor: fg, size: 18, onPress: onRandomize, accessibilityLabel: "Randomize match settings" }),
1833
+ onReset && /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_feedback_press9.IconButton, { icon: "restore", iconColor: fg, size: 18, onPress: onReset, accessibilityLabel: "Reset match settings to defaults" }),
1834
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_feedback_press9.IconButton, { icon: "cog", iconColor: fg, size: 20, onPress: onSettings, accessibilityLabel: "Settings" })
1845
1835
  ] }),
1846
1836
  children
1847
1837
  ] });
@@ -1865,7 +1855,7 @@ var styles22 = import_react_native29.StyleSheet.create({
1865
1855
  var import_react13 = require("react");
1866
1856
  var import_react_native30 = require("react-native");
1867
1857
  var import_react_native_reanimated2 = __toESM(require("react-native-reanimated"));
1868
- var import_jsx_runtime28 = require("react/jsx-runtime");
1858
+ var import_jsx_runtime27 = require("react/jsx-runtime");
1869
1859
  var STAGGER_MS = 45;
1870
1860
  var LETTER_DURATION_MS = 320;
1871
1861
  function getStaggeredWordDuration(letterCount) {
@@ -1881,10 +1871,10 @@ function AnimatedLetter({ char, index, color, reducedMotion, fontFamily, fontSiz
1881
1871
  opacity: progress.value,
1882
1872
  transform: [{ translateY: (1 - progress.value) * 12 }, { scale: 0.85 + progress.value * 0.15 }]
1883
1873
  }));
1884
- return /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_react_native_reanimated2.default.Text, { allowFontScaling: false, style: [{ color, fontFamily, fontSize, lineHeight, fontWeight, letterSpacing }, style], children: char });
1874
+ return /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_react_native_reanimated2.default.Text, { allowFontScaling: false, style: [{ color, fontFamily, fontSize, lineHeight, fontWeight, letterSpacing }, style], children: char });
1885
1875
  }
1886
1876
  function StaggeredWord({ word, color, fontFamily, fontSize, lineHeight, reducedMotion, startIndex = 0, fontWeight = "bold", letterSpacing, style, onLayout }) {
1887
- return /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_react_native30.View, { style: [styles23.row, style], onLayout, children: word.split("").map((char, i) => /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(AnimatedLetter, { char, index: startIndex + i, color, reducedMotion, fontFamily, fontSize, lineHeight, fontWeight, letterSpacing }, i)) });
1877
+ return /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_react_native30.View, { style: [styles23.row, style], onLayout, children: word.split("").map((char, i) => /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(AnimatedLetter, { char, index: startIndex + i, color, reducedMotion, fontFamily, fontSize, lineHeight, fontWeight, letterSpacing }, i)) });
1888
1878
  }
1889
1879
  var styles23 = import_react_native30.StyleSheet.create({
1890
1880
  row: {
@@ -1894,12 +1884,12 @@ var styles23 = import_react_native30.StyleSheet.create({
1894
1884
 
1895
1885
  // src/UpdateDialog.tsx
1896
1886
  var import_updater2 = require("@rific/updater");
1897
- var import_jsx_runtime29 = require("react/jsx-runtime");
1887
+ var import_jsx_runtime28 = require("react/jsx-runtime");
1898
1888
  function UpdateDialog({ rotation = 0, autoCheck = true, autoPrompt = true, onError }) {
1899
1889
  const { manifest, onConfirm, respond } = useUpdateConfirmBridge();
1900
1890
  (0, import_updater2.useUpdater)({ autoCheck, autoPrompt, onConfirm, onError });
1901
1891
  const date = manifest ? new Date(manifest.createdAt) : null;
1902
- return /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(ConfirmDialog, { visible: !!manifest, title: "Update Available", message: date ? `Released ${date.toLocaleDateString()} at ${date.toLocaleTimeString()}` : "", confirmLabel: "Restart", cancelLabel: "Later", icon: "update", rotation, onConfirm: () => respond(true), onCancel: () => respond(false) });
1892
+ return /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(ConfirmDialog, { visible: !!manifest, title: "Update Available", message: date ? `Released ${date.toLocaleDateString()} at ${date.toLocaleTimeString()}` : "", confirmLabel: "Restart", cancelLabel: "Later", icon: "update", rotation, onConfirm: () => respond(true), onCancel: () => respond(false) });
1903
1893
  }
1904
1894
 
1905
1895
  // src/useQuitConfirmation.ts
@@ -1950,7 +1940,6 @@ function useQuitConfirmation(hasProgress, onConfirmedBack) {
1950
1940
  RotatedFrame,
1951
1941
  SectionedDropdown,
1952
1942
  SharedActionBand,
1953
- SkiaGate,
1954
1943
  StaggeredWord,
1955
1944
  StatRow,
1956
1945
  StatSection,
@@ -1965,6 +1954,7 @@ function useQuitConfirmation(hasProgress, onConfirmedBack) {
1965
1954
  getStaggeredWordDuration,
1966
1955
  isCompactDialogHeight,
1967
1956
  loadSkiaWeb,
1957
+ locateSkiaWebFile,
1968
1958
  overlayActionStyles,
1969
1959
  overlayStyles,
1970
1960
  useAutoAlign,
package/dist/index.mjs CHANGED
@@ -1,6 +1,3 @@
1
- import {
2
- SkiaGate
3
- } from "./chunk-XEOFR236.mjs";
4
1
  import {
5
2
  AchievementCatalogSection,
6
3
  AchievementRow,
@@ -38,6 +35,10 @@ import {
38
35
  overlayStyles,
39
36
  useOverlayCardColors
40
37
  } from "./chunk-PUF6TABL.mjs";
38
+ import {
39
+ loadSkiaWeb,
40
+ locateSkiaWebFile
41
+ } from "./chunk-3TCJHMJP.mjs";
41
42
 
42
43
  // src/AchievementUnlockList.tsx
43
44
  import { getContrastColor, useAutoPaperTheme } from "@rific/auto-paper";
@@ -95,10 +96,11 @@ import { ScrollView, StyleSheet as StyleSheet2, View as View2 } from "react-nati
95
96
  import { Icon as Icon2, Text as Text2 } from "react-native-paper";
96
97
 
97
98
  // src/TriggerGaugeHost.tsx
99
+ import { lazy, Suspense } from "react";
98
100
  import { jsx as jsx2 } from "react/jsx-runtime";
99
- var getTriggerGauge = () => import("./TriggerGauge-GIHKGAZE.mjs").then((mod) => ({ default: mod.TriggerGauge }));
101
+ var LazyTriggerGauge = lazy(() => loadSkiaWeb().then(() => import("./TriggerGauge-GIHKGAZE.mjs").then((mod) => ({ default: mod.TriggerGauge }))));
100
102
  function TriggerGaugeHost(props) {
101
- return /* @__PURE__ */ jsx2(SkiaGate, { getComponent: getTriggerGauge, componentProps: props });
103
+ return /* @__PURE__ */ jsx2(Suspense, { fallback: null, children: /* @__PURE__ */ jsx2(LazyTriggerGauge, { ...props }) });
102
104
  }
103
105
 
104
106
  // src/SectionedDropdown.tsx
@@ -434,11 +436,6 @@ var styles4 = StyleSheet4.create({
434
436
  }
435
437
  });
436
438
 
437
- // src/loadSkiaWeb.ts
438
- function loadSkiaWeb() {
439
- return Promise.resolve();
440
- }
441
-
442
439
  // src/PlayerSetupPanel.tsx
443
440
  import { getOptionPickerContentSize, getProfilePickerContentSize, OptionPicker, ProfilePicker } from "@tastic/profile";
444
441
  import { StyleSheet as StyleSheet6, View as View5 } from "react-native";
@@ -647,7 +644,6 @@ export {
647
644
  RotatedFrame,
648
645
  SectionedDropdown,
649
646
  SharedActionBand,
650
- SkiaGate,
651
647
  StaggeredWord,
652
648
  StatRow,
653
649
  StatSection,
@@ -662,6 +658,7 @@ export {
662
658
  getStaggeredWordDuration,
663
659
  isCompactDialogHeight,
664
660
  loadSkiaWeb,
661
+ locateSkiaWebFile,
665
662
  overlayActionStyles,
666
663
  overlayStyles,
667
664
  useAutoAlign,
@@ -0,0 +1,5 @@
1
+ declare function loadSkiaWeb(): Promise<void>;
2
+
3
+ declare function locateSkiaWebFile(file: string): string;
4
+
5
+ export { loadSkiaWeb, locateSkiaWebFile };
@@ -0,0 +1,5 @@
1
+ declare function loadSkiaWeb(): Promise<void>;
2
+
3
+ declare function locateSkiaWebFile(file: string): string;
4
+
5
+ export { loadSkiaWeb, locateSkiaWebFile };
@@ -17,29 +17,25 @@ var __copyProps = (to, from, except, desc) => {
17
17
  };
18
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
19
 
20
- // src/SkiaGate.tsx
21
- var SkiaGate_exports = {};
22
- __export(SkiaGate_exports, {
23
- SkiaGate: () => SkiaGate
20
+ // src/skia/index.ts
21
+ var skia_exports = {};
22
+ __export(skia_exports, {
23
+ loadSkiaWeb: () => loadSkiaWeb,
24
+ locateSkiaWebFile: () => locateSkiaWebFile
24
25
  });
25
- module.exports = __toCommonJS(SkiaGate_exports);
26
- var import_react = require("react");
27
- var import_jsx_runtime = require("react/jsx-runtime");
28
- function SkiaGate({ getComponent, componentProps, fallback = null }) {
29
- const [Component, setComponent] = (0, import_react.useState)(null);
30
- (0, import_react.useEffect)(() => {
31
- let cancelled = false;
32
- getComponent().then((mod) => {
33
- if (!cancelled) setComponent(() => mod.default);
34
- });
35
- return () => {
36
- cancelled = true;
37
- };
38
- }, [getComponent]);
39
- if (!Component) return fallback;
40
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Component, { ...componentProps });
26
+ module.exports = __toCommonJS(skia_exports);
27
+
28
+ // src/loadSkiaWeb.ts
29
+ function loadSkiaWeb() {
30
+ return Promise.resolve();
31
+ }
32
+
33
+ // src/locateSkiaWebFile.ts
34
+ function locateSkiaWebFile(file) {
35
+ return `${process.env.EXPO_BASE_URL ?? ""}/${file}`;
41
36
  }
42
37
  // Annotate the CommonJS export names for ESM import in node:
43
38
  0 && (module.exports = {
44
- SkiaGate
39
+ loadSkiaWeb,
40
+ locateSkiaWebFile
45
41
  });
@@ -0,0 +1,8 @@
1
+ import {
2
+ loadSkiaWeb,
3
+ locateSkiaWebFile
4
+ } from "../chunk-3TCJHMJP.mjs";
5
+ export {
6
+ loadSkiaWeb,
7
+ locateSkiaWebFile
8
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tastic/hud",
3
- "version": "0.23.0",
3
+ "version": "0.24.0",
4
4
  "description": "Visual component kit for local-multiplayer React Native games: inline (non-modal) popovers, dropdowns, color pickers, gauges, ready buttons, and dialogs, built to stay scoped to one player's own zone on a shared screen",
5
5
  "keywords": [
6
6
  "react-native",
@@ -48,12 +48,12 @@
48
48
  "import": "./dist/guide/index.mjs",
49
49
  "require": "./dist/guide/index.js"
50
50
  },
51
- "./skia-gate": {
52
- "react-native": "./src/SkiaGate.tsx",
53
- "browser": "./src/SkiaGate.web.tsx",
54
- "types": "./dist/SkiaGate.d.ts",
55
- "import": "./dist/SkiaGate.mjs",
56
- "require": "./dist/SkiaGate.js"
51
+ "./skia": {
52
+ "react-native": "./src/skia/index.ts",
53
+ "browser": "./src/skia/index.ts",
54
+ "types": "./dist/skia/index.d.ts",
55
+ "import": "./dist/skia/index.mjs",
56
+ "require": "./dist/skia/index.js"
57
57
  }
58
58
  },
59
59
  "main": "dist/index.js",
@@ -1,18 +1,24 @@
1
- import { SkiaGate } from './SkiaGate'
2
- import { TriggerGaugeProps } from './TriggerGauge'
1
+ import { lazy, Suspense } from 'react'
3
2
 
4
- // TriggerGauge.tsx's `Skia` import binds to global.CanvasKit at module-evaluation time, which on web
5
- // only exists once CanvasKit's WASM fetch resolves - a plain `import { TriggerGauge } from
6
- // './TriggerGauge'` at the top of this file would already have evaluated that binding, against a
7
- // not-yet-loaded, undefined CanvasKit, before anything in a consuming app renders at all, since
8
- // bundlers evaluate the whole static import graph up front. This file therefore never imports
9
- // TriggerGauge itself except as a type (erased at compile time, carrying no runtime module
10
- // reference) - getTriggerGauge below, hoisted to module scope so its identity stays stable across
11
- // renders (see SkiaGate's own comment on why that matters), is what actually defers the real import
12
- // to runtime. No explicit loadSkiaWeb() call here on web: SkiaGate.web.tsx (WithSkiaWeb) already
13
- // gates its own mount behind CanvasKit being ready, and on native there's nothing to wait on at all.
14
- const getTriggerGauge = () => import('./TriggerGauge').then((mod) => ({ default: mod.TriggerGauge }))
3
+ import { loadSkiaWeb } from './loadSkiaWeb'
4
+ import type { TriggerGaugeProps } from './TriggerGauge'
5
+
6
+ // TriggerGauge.tsx imports @shopify/react-native-skia at its top level, and this file is reached from
7
+ // the main barrel, so a static import here would evaluate Skia the moment any app imports
8
+ // `@tastic/hud`. On web that binds Skia to a CanvasKit that hasn't loaded yet (its `Skia` object is
9
+ // built from global.CanvasKit at module-evaluation time), and in every app's Jest run it throws
10
+ // ("Native Skia Module failed to correctly install JSI Bindings!": no app mocks Skia). So the gauge
11
+ // module is only imported when a gauge mounts, behind loadSkiaWeb(), which loads CanvasKit on web and
12
+ // resolves at once on native. This is the web half of the fleet's Skia host pair (see the README's
13
+ // "Skia on web"), used on both platforms here: an app's own native host file can import its Skia
14
+ // component directly, but a package barrel can't. The lazy component is created once, at module
15
+ // scope, so its identity never changes between renders.
16
+ const LazyTriggerGauge = lazy(() => loadSkiaWeb().then(() => import('./TriggerGauge').then((mod) => ({ default: mod.TriggerGauge }))))
15
17
 
16
18
  export default function TriggerGaugeHost(props: TriggerGaugeProps) {
17
- return <SkiaGate getComponent={getTriggerGauge} componentProps={props} />
19
+ return (
20
+ <Suspense fallback={null}>
21
+ <LazyTriggerGauge {...props} />
22
+ </Suspense>
23
+ )
18
24
  }
@@ -1,5 +1,5 @@
1
1
  // The how-to-play entry point (`@tastic/hud/guide`) - a standalone build reachable without ever
2
- // touching the main barrel, the same reason `@tastic/hud/skia-gate` is. This file and everything it
2
+ // touching the main barrel, the same reason `@tastic/hud/skia` is. This file and everything it
3
3
  // imports must stay free of @tastic/core, @tastic/profile, @tastic/split-screen, @tastic/achievements,
4
4
  // @rific/updater and Skia: those are optional peers of this package (see package.json's
5
5
  // peerDependenciesMeta) precisely so an app with none of them can install @tastic/hud and use only
package/src/index.ts CHANGED
@@ -14,6 +14,7 @@ export { MONO_FONT } from './fonts'
14
14
  export { getInlineColorPickerContentSize, InlineColorPicker } from './InlineColorPicker'
15
15
  export { getLabeledDropdownContentHeight, LABELED_DROPDOWN_POPOVER_WIDTH, LabeledDropdown, type LabeledDropdownOption } from './LabeledDropdown'
16
16
  export { loadSkiaWeb } from './loadSkiaWeb'
17
+ export { locateSkiaWebFile } from './locateSkiaWebFile'
17
18
  export { overlayActionStyles, overlayStyles } from './overlayCard'
18
19
  export { OverlayBackdrop, type OverlayBackdropProps, OverlayCard, type OverlayCardProps, type OverlayCardThemeColors, useOverlayCardColors } from './OverlaySurface'
19
20
  export { getColorPopoverId, getCpuDifficultyPopoverId, getProfilePopoverId, PlayerSetupPanel, type PlayerSetupPanelProps } from './PlayerSetupPanel'
@@ -23,12 +24,11 @@ export { ReadyButton } from './ReadyButton'
23
24
  export { RotatedFrame, type RotatedFrameProps } from './RotatedFrame'
24
25
  export { type MenuOption, type MenuSection, type MultiSelectSection, SectionedDropdown, type SingleSelectSection } from './SectionedDropdown'
25
26
  export { SharedActionBand } from './SharedActionBand'
26
- export { SkiaGate, type SkiaGateProps } from './SkiaGate'
27
27
  export { getStaggeredWordDuration, StaggeredWord, type StaggeredWordProps } from './StaggeredWord'
28
28
  export { StatRow } from './StatRow'
29
29
  export { StatSection } from './StatSection'
30
30
  // TriggerGauge itself (the raw Skia-based component) is deliberately NOT exported here, even
31
- // though a native-only consumer could safely skip TriggerGaugeHost's SkiaGate indirection. A
31
+ // though a native-only consumer could safely skip TriggerGaugeHost's lazy indirection. A
32
32
  // value re-export forces the bundler to fold TriggerGauge.tsx's own `Skia` import into a chunk
33
33
  // that this entry point imports eagerly at the top level - since that chunk is then shared with
34
34
  // TriggerGaugeHost's own dynamic import too, it defeats TriggerGaugeHost's deferred loading for
@@ -1,11 +1,14 @@
1
1
  import { LoadSkiaWeb } from '@shopify/react-native-skia/src/web'
2
2
 
3
+ import { locateSkiaWebFile } from './locateSkiaWebFile'
4
+
3
5
  // Skia's web target renders through CanvasKit, a WASM build of Skia fetched at runtime - nothing
4
6
  // using @shopify/react-native-skia's React components can draw a single frame until this resolves.
5
- // Kept as a thin wrapper (rather than importing LoadSkiaWeb directly at call sites) purely so this
6
- // file's `.ts` counterpart, loadSkiaWeb.ts, gives native an identically-shaped async no-op - Metro's
7
+ // Kept as a thin wrapper (rather than importing LoadSkiaWeb directly at call sites) so this file's
8
+ // `.ts` counterpart, loadSkiaWeb.ts, gives native an identically-shaped async no-op - Metro's
7
9
  // platform-extension resolution picks whichever one matches the bundle target, so canvaskit-wasm's
8
- // `require('fs')' never even gets resolved into a native/iOS/Android bundle.
10
+ // `require('fs')` never even gets resolved into a native/iOS/Android bundle - and so the wasm is
11
+ // always fetched from the app's base URL (see locateSkiaWebFile.ts).
9
12
  export function loadSkiaWeb(): Promise<void> {
10
- return LoadSkiaWeb()
13
+ return LoadSkiaWeb({ locateFile: locateSkiaWebFile })
11
14
  }
@@ -0,0 +1,16 @@
1
+ // The `locateFile` for CanvasKit's WASM fetch: `<baseUrl>/<file>`, so `/snake/canvaskit.wasm` under
2
+ // an Expo base URL of `/snake` and `/canvaskit.wasm` with none. That is where an Expo web export
3
+ // serves the app's `public/canvaskit.wasm`. Without a `locateFile`, canvaskit-wasm 0.41 resolves the
4
+ // file next to the running script, or next to the page when no script is running: at `/snake` (no
5
+ // trailing slash) that is `/canvaskit.wasm`, and in a static-output app it can land under
6
+ // `/_expo/static/js/web/`. LoadSkiaWeb also keeps its first load's promise, a failed one included,
7
+ // for the life of the page, so whichever call loads CanvasKit first has to pass this.
8
+ //
9
+ // `process.env.EXPO_BASE_URL` is `experiments.baseUrl` with its trailing slashes stripped ('' when
10
+ // unset). babel-preset-expo inlines it as a string literal in every file Metro transforms,
11
+ // node_modules included, so this source picks up the consuming app's value with no wiring. It is
12
+ // read at call time, so a test (where it isn't inlined) can set it. No Skia import, so the barrel and
13
+ // `@tastic/hud/skia` can export it to native too.
14
+ export function locateSkiaWebFile(file: string): string {
15
+ return `${process.env.EXPO_BASE_URL ?? ''}/${file}`
16
+ }
@@ -0,0 +1,12 @@
1
+ // The Skia entry point (`@tastic/hud/skia`): the web CanvasKit loader every app's Skia host files use,
2
+ // and the wasm URL rule it applies, reachable without the main barrel. Several Skia apps lack the
3
+ // barrel's heavier peers (@tastic/profile, @tastic/split-screen), and importing `@tastic/hud` there
4
+ // fails to bundle, so this is the one import path every app's host files share.
5
+ //
6
+ // package.json points both "react-native" and "browser" at this file. That is fine because it has no
7
+ // .web twin itself: Metro doesn't platform-resolve an exports-map target, but it does resolve the
8
+ // relative imports below, so web bundles get loadSkiaWeb.web.ts and native ones the no-op. The
9
+ // boundary (only @shopify/react-native-skia's /src/web subpath, and only from the .web file) is
10
+ // enforced by src/__tests__/skiaImports.test.ts.
11
+ export { loadSkiaWeb } from '../loadSkiaWeb'
12
+ export { locateSkiaWebFile } from '../locateSkiaWebFile'
@@ -1,13 +0,0 @@
1
- import * as react from 'react';
2
- import { ComponentType, ReactNode } from 'react';
3
-
4
- interface SkiaGateProps<P extends object> {
5
- getComponent: () => Promise<{
6
- default: ComponentType<P>;
7
- }>;
8
- componentProps: P;
9
- fallback?: ReactNode;
10
- }
11
- declare function SkiaGate<P extends object>({ getComponent, componentProps, fallback }: SkiaGateProps<P>): string | number | bigint | boolean | Iterable<ReactNode> | Promise<string | number | bigint | boolean | react.ReactPortal | react.ReactElement<unknown, string | react.JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | react.JSX.Element | null;
12
-
13
- export { SkiaGate, type SkiaGateProps };
@@ -1,13 +0,0 @@
1
- import * as react from 'react';
2
- import { ComponentType, ReactNode } from 'react';
3
-
4
- interface SkiaGateProps<P extends object> {
5
- getComponent: () => Promise<{
6
- default: ComponentType<P>;
7
- }>;
8
- componentProps: P;
9
- fallback?: ReactNode;
10
- }
11
- declare function SkiaGate<P extends object>({ getComponent, componentProps, fallback }: SkiaGateProps<P>): string | number | bigint | boolean | Iterable<ReactNode> | Promise<string | number | bigint | boolean | react.ReactPortal | react.ReactElement<unknown, string | react.JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | react.JSX.Element | null;
12
-
13
- export { SkiaGate, type SkiaGateProps };
package/dist/SkiaGate.mjs DELETED
@@ -1,6 +0,0 @@
1
- import {
2
- SkiaGate
3
- } from "./chunk-XEOFR236.mjs";
4
- export {
5
- SkiaGate
6
- };
@@ -1,21 +0,0 @@
1
- // src/SkiaGate.tsx
2
- import { useEffect, useState } from "react";
3
- import { jsx } from "react/jsx-runtime";
4
- function SkiaGate({ getComponent, componentProps, fallback = null }) {
5
- const [Component, setComponent] = useState(null);
6
- useEffect(() => {
7
- let cancelled = false;
8
- getComponent().then((mod) => {
9
- if (!cancelled) setComponent(() => mod.default);
10
- });
11
- return () => {
12
- cancelled = true;
13
- };
14
- }, [getComponent]);
15
- if (!Component) return fallback;
16
- return /* @__PURE__ */ jsx(Component, { ...componentProps });
17
- }
18
-
19
- export {
20
- SkiaGate
21
- };
package/src/SkiaGate.tsx DELETED
@@ -1,45 +0,0 @@
1
- import type { ComponentType, ReactNode } from 'react'
2
- import { useEffect, useState } from 'react'
3
-
4
- export interface SkiaGateProps<P extends object> {
5
- // A dynamic import, e.g. `() => import('./Board')` - hoist to module scope at the call site
6
- // (never an inline closure written directly in JSX) so its identity stays stable across
7
- // renders; it's this component's own useEffect dependency below.
8
- getComponent: () => Promise<{ default: ComponentType<P> }>
9
- componentProps: P
10
- // Rendered while getComponent()'s promise is still pending. Defaults to null.
11
- fallback?: ReactNode
12
- }
13
-
14
- // Native Skia renders through JSI - no WASM/CanvasKit fetch to wait on - so this file just
15
- // lazily mounts whatever getComponent() resolves to, once it resolves. The .web.tsx sibling
16
- // (picked by Metro/webpack's own platform-extension resolution for web bundles) is a different
17
- // implementation entirely: a direct re-export of @shopify/react-native-skia's own WithSkiaWeb,
18
- // which gates the same mount behind LoadSkiaWeb() first - CanvasKit's WASM build has to actually
19
- // finish loading before anything that imports '@shopify/react-native-skia' can evaluate its own
20
- // top-level `Skia` binding without crashing (see loadSkiaWeb.web.ts's own comment).
21
- //
22
- // No React.lazy()/Suspense here, deliberately: lazy() can't be called inline during render
23
- // (react-hooks/static-components - a getComponent prop that isn't referentially stable would
24
- // give a new component identity, and therefore an unwanted remount, on every render that passes
25
- // a fresh one) and can't be cached in a ref either (react-hooks/refs forbids reading or writing
26
- // ref.current during render, only inside an effect). Awaiting the import directly and setting
27
- // state from the resolved module sidesteps both - setState only ever runs from the promise
28
- // callback, never synchronously in render or in the effect body itself.
29
- export function SkiaGate<P extends object>({ getComponent, componentProps, fallback = null }: SkiaGateProps<P>) {
30
- const [Component, setComponent] = useState<ComponentType<P> | null>(null)
31
-
32
- useEffect(() => {
33
- let cancelled = false
34
- getComponent().then((mod) => {
35
- if (!cancelled) setComponent(() => mod.default)
36
- })
37
- return () => {
38
- cancelled = true
39
- }
40
- }, [getComponent])
41
-
42
- if (!Component) return fallback
43
-
44
- return <Component {...componentProps} />
45
- }
@@ -1,9 +0,0 @@
1
- // Web needs CanvasKit's WASM loaded before any Skia <Canvas> evaluates - see loadSkiaWeb.web.ts's
2
- // own comment for why a plain useEffect-based loader can't fix this (it's a module-evaluation-
3
- // order problem, not a component-lifecycle one). Only this file (picked by Metro/webpack's
4
- // platform-extension resolution for web bundles) may import from
5
- // '@shopify/react-native-skia/src/web' - a static import of that subpath from a native-reachable
6
- // file pulls canvaskit-wasm's `require('fs')` into the native bundle and breaks iOS/Android
7
- // builds; this file vs. SkiaGate.tsx is what actually keeps it out, the same split
8
- // loadSkiaWeb.web.ts/loadSkiaWeb.ts already use.
9
- export { WithSkiaWeb as SkiaGate } from '@shopify/react-native-skia/src/web'