@tastic/hud 0.22.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.
@@ -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.22.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",
@@ -88,7 +88,7 @@
88
88
  "@infinitetoken/jest-config": "^0.5.2",
89
89
  "@infinitetoken/tsconfig": "^0.6.0",
90
90
  "@rific/auto-paper": "^0.13.3",
91
- "@rific/core": "^0.3.1",
91
+ "@rific/core": "^0.4.0",
92
92
  "@rific/feedback-press": "^0.13.0",
93
93
  "@rific/updater": "^0.7.1",
94
94
  "@shopify/react-native-skia": "^2.14.0",
@@ -117,6 +117,7 @@
117
117
  },
118
118
  "peerDependencies": {
119
119
  "@rific/auto-paper": "^0.13.0",
120
+ "@rific/core": ">=0.4.0",
120
121
  "@rific/feedback-press": ">=0.10.2",
121
122
  "@rific/updater": ">=0.4.0",
122
123
  "@shopify/react-native-skia": ">=2.6.0",
@@ -1,4 +1,5 @@
1
1
  import { AutoAppearancePicker, Dialog, FadingScrollView, useAutoPaperTheme, useBlurModule, useThemeSettings } from '@rific/auto-paper'
2
+ import { useExpoKeepAwake, useKeepAwakeSettings } from '@rific/core'
2
3
  import { Button, SoundContext, TouchableRipple, useHapticSettings, useSoundSettings, useVibration } from '@rific/feedback-press'
3
4
  import { useUpdater } from '@rific/updater'
4
5
  import { rotateInsets, useIsTouchPrimaryDevice, useRotation, type ViewRotation } from '@tastic/core'
@@ -56,13 +57,7 @@ export interface BaseSettingsDialogProps {
56
57
  // @tastic/edge-guard's own swizzle is a no-op on every other platform.
57
58
  deferBottomEdgeGestures?: boolean
58
59
  onDeferBottomEdgeGestures?: (value: boolean) => void
59
- // Optional, and always passed as a pair - omit both for an app that doesn't keep the screen awake.
60
- // This dialog only shows and reports the toggle; the app itself keeps the screen on while it's set
61
- // (expo-keep-awake on its match screen), so this package needs no native dependency for it. Shown
62
- // on the same devices as Lock Orientation (see isTouchPrimary below): a desktop/laptop browser
63
- // stays awake on its own while someone is playing with its keyboard or mouse.
64
- keepAwake?: boolean
65
- onKeepAwakeChange?: (value: boolean) => void
60
+ // No Keep Awake props: that row reads @rific/core's Keep Awake setting itself (see showKeepAwake).
66
61
  // Explicit opt-outs for the rows that pull their own state straight from @rific/feedback-press,
67
62
  // @rific/auto-paper, and @rific/updater's own shared contexts/hooks rather than from anything the
68
63
  // caller passes in - unlike Lock Orientation/Edge Guard above, there's no "value" to omit for
@@ -97,7 +92,7 @@ export interface BaseSettingsDialogProps {
97
92
  // is the seam for whatever isn't shared: a game with its own board/difficulty settings (or a stats
98
93
  // backup flow, or anything else genuinely specific to it) renders those below Appearance, not by
99
94
  // forking this component.
100
- export function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, version, lockOrientation, onLockOrientationChange, deferBottomEdgeGestures, onDeferBottomEdgeGestures, keepAwake, onKeepAwakeChange, hideSound = false, hideHaptics = false, hideAppearance = false, hideUpdateCheck = false, onUpdateError, onShowHowToPlay, children }: BaseSettingsDialogProps) {
95
+ export function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, version, lockOrientation, onLockOrientationChange, deferBottomEdgeGestures, onDeferBottomEdgeGestures, hideSound = false, hideHaptics = false, hideAppearance = false, hideUpdateCheck = false, onUpdateError, onShowHowToPlay, children }: BaseSettingsDialogProps) {
101
96
  const { colors } = useAutoPaperTheme()
102
97
  const ambientRotation = useRotation()
103
98
  const rotation = rotationOverride ?? ambientRotation
@@ -113,7 +108,7 @@ export function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverr
113
108
  // Dialog's own 32pt padding is simply overhung.
114
109
  const insets = rotateInsets(useSafeAreaInsets(), rotation)
115
110
  const sheetWidth = Math.min(overlayStyles.card.maxWidth, (turned ? window.height : window.width) - 2 * Math.max(insets.left, insets.right, SHEET_GUTTER))
116
- // Also gates the Lock Orientation and Keep Awake rows below (alongside their own props) - true
111
+ // Also gates the Lock Orientation and Keep Awake rows below (alongside their own conditions) - true
117
112
  // unconditionally on native (see the hook's own doc), so this only actually excludes a
118
113
  // desktop/laptop browser, where a mouse-driven window has no physical orientation to lock
119
114
  // against; a resize there would just make the setting silently stop reflowing the layout instead
@@ -121,6 +116,8 @@ export function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverr
121
116
  const isTouchPrimary = useIsTouchPrimaryDevice()
122
117
  const { settings: hapticSettings, set: setHapticSettings } = useHapticSettings()
123
118
  const { settings: soundSettings, set: setSoundSettings } = useSoundSettings()
119
+ const { settings: keepAwakeSettings, set: setKeepAwakeSettings } = useKeepAwakeSettings()
120
+ const expoKeepAwake = useExpoKeepAwake()
124
121
  // Sound/Haptics toggle themselves: the ripple's automatic press feedback fires on onPressIn,
125
122
  // before onPress applies the toggle, so it reflects the OLD enabled value - backwards from what
126
123
  // a settings toggle should confirm (turning off would click/buzz, turning on would go silent).
@@ -160,7 +157,10 @@ export function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverr
160
157
 
161
158
  const showLockOrientation = isTouchPrimary && lockOrientation !== undefined && onLockOrientationChange !== undefined
162
159
  const showEdgeGuard = Platform.OS === 'ios' && deferBottomEdgeGestures !== undefined && onDeferBottomEdgeGestures !== undefined
163
- const showKeepAwake = isTouchPrimary && keepAwake !== undefined && onKeepAwakeChange !== undefined
160
+ // Only in an app that opted in by handing expo-keep-awake to @rific/core's KeepAwakeProvider:
161
+ // without the module the setting would do nothing. On the same devices as Lock Orientation, since a
162
+ // desktop/laptop browser stays awake on its own while someone plays with its keyboard or mouse.
163
+ const showKeepAwake = isTouchPrimary && expoKeepAwake !== undefined
164
164
  const showHaptics = Platform.OS !== 'web' && !hideHaptics
165
165
  const showSound = !hideSound
166
166
  const showAppearance = !hideAppearance
@@ -243,11 +243,11 @@ export function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverr
243
243
  </TouchableRipple>
244
244
  )}
245
245
 
246
- {/* See the keepAwake prop's own doc: the app does the keeping awake, this only toggles it. */}
246
+ {/* The app's own useKeepAwake('game') does the keeping awake; this only flips the shared setting. */}
247
247
  {showKeepAwake && (
248
- <TouchableRipple onPress={() => onKeepAwakeChange!(!keepAwake)} style={styles.toggleButton} accessibilityLabel={`Keep awake ${keepAwake ? 'on' : 'off'}`}>
248
+ <TouchableRipple onPress={() => setKeepAwakeSettings({ enabled: !keepAwakeSettings.enabled })} style={styles.toggleButton} accessibilityLabel={`Keep awake ${keepAwakeSettings.enabled ? 'on' : 'off'}`}>
249
249
  <View style={styles.toggleContent}>
250
- <SettingIcon source={keepAwake ? 'eye' : 'eye-closed'} color={keepAwake ? colors.tertiary : colors.onSurfaceVariant} containerColor={keepAwake ? colors.tertiaryContainer : colors.surfaceVariant} />
250
+ <SettingIcon source={keepAwakeSettings.enabled ? 'eye' : 'eye-closed'} color={keepAwakeSettings.enabled ? colors.tertiary : colors.onSurfaceVariant} containerColor={keepAwakeSettings.enabled ? colors.tertiaryContainer : colors.surfaceVariant} />
251
251
  <View style={styles.flexShrink}>
252
252
  <Text variant='bodyLarge' style={{ color: colors.onSurface }}>
253
253
  Keep Awake
@@ -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,8 +1,11 @@
1
1
  // The fleet-chrome entry point (`@tastic/hud/chrome`): the shared dialogs, stats screen pieces, corner
2
- // buttons and style tokens, reachable without the main barrel, for an app that has none of its heavy
3
- // peers. Hangman is the reason it exists: it has @tastic/core, @rific/updater and the Paper stack, but
4
- // no Skia, no @tastic/profile and no @tastic/split-screen. Everything reachable from this file must stay
5
- // free of those three and of the main barrel; @tastic/achievements may appear only as `import type`.
2
+ // buttons, popover primitives, color picker and style tokens, reachable without the main barrel, for an
3
+ // app that has none of its heavy peers. Hangman is the reason it exists: it has @tastic/core,
4
+ // @rific/updater and the Paper stack, but no Skia, no @tastic/profile and no @tastic/split-screen. The
5
+ // popover pieces (PopoverBody, PressAwayOverlay, usePopoverHost, useAutoAlign) and InlineColorPicker are
6
+ // here for @tastic/profile 0.16+, which imports them from this entry instead of the main barrel so its
7
+ // ProfilePicker and ProfilesScreen work in Hangman. Everything reachable from this file must stay free of
8
+ // those three and of the main barrel; @tastic/achievements may appear only as `import type`.
6
9
  // AchievementUnlockList is left out on purpose because it value-imports @tastic/profile's ProfileChip.
7
10
  // src/__tests__/chromeImports.test.ts walks the real import graph from here and enforces all of it.
8
11
  export { AchievementCatalogSection } from '../AchievementCatalogSection'
@@ -15,12 +18,17 @@ export { ContentGutter } from '../ContentGutter'
15
18
  export { CornerActionButtons } from '../CornerActionButtons'
16
19
  export { COMPACT_DIALOG_HEIGHT, DialogHeader, type DialogHeaderProps, isCompactDialogHeight } from '../DialogHeader'
17
20
  export { MONO_FONT } from '../fonts'
21
+ export { getInlineColorPickerContentSize, InlineColorPicker } from '../InlineColorPicker'
18
22
  export { overlayActionStyles, overlayStyles } from '../overlayCard'
19
23
  export { OverlayBackdrop, type OverlayBackdropProps, OverlayCard, type OverlayCardProps, type OverlayCardThemeColors, useOverlayCardColors } from '../OverlaySurface'
24
+ export { PopoverBody } from '../PopoverBody'
25
+ export { PressAwayOverlay } from '../PressAwayOverlay'
20
26
  export { RotatedFrame, type RotatedFrameProps } from '../RotatedFrame'
21
27
  export { getStaggeredWordDuration, StaggeredWord, type StaggeredWordProps } from '../StaggeredWord'
22
28
  export { StatRow } from '../StatRow'
23
29
  export { StatSection } from '../StatSection'
24
30
  export { UpdateDialog, type UpdateDialogProps } from '../UpdateDialog'
31
+ export { type AlignResult, type PopoverAlign, type PopoverRotation, type PopoverVerticalAlign, useAutoAlign } from '../useAutoAlign'
25
32
  export { useCompactDialog } from '../useCompactDialog'
33
+ export { type PopoverHost, usePopoverHost } from '../usePopoverHost'
26
34
  export { useWellColors, WELL_SHADOW, type WellColors } from '../wellSurface'
@@ -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'