@tastic/hud 0.7.0 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,3 +1,7 @@
1
+ import {
2
+ SkiaGate
3
+ } from "./chunk-XEOFR236.mjs";
4
+
1
5
  // src/AchievementRow.tsx
2
6
  import { useAutoPaperTheme } from "@rific/auto-paper";
3
7
  import { StyleSheet, View } from "react-native";
@@ -70,7 +74,7 @@ var styles = StyleSheet.create({
70
74
  import { AutoAppearancePicker, Dialog, useAutoPaperTheme as useAutoPaperTheme2 } from "@rific/auto-paper";
71
75
  import { Button, SoundContext, TouchableRipple, useHapticSettings, useSoundSettings, useVibration } from "@rific/feedback-press";
72
76
  import { useUpdater } from "@rific/updater";
73
- import { useIsTouchPrimaryDevice } from "@tastic/core";
77
+ import { useIsTouchPrimaryDevice, useRotation } from "@tastic/core";
74
78
  import { useContext, useState } from "react";
75
79
  import { Platform, ScrollView, StyleSheet as StyleSheet2, View as View2 } from "react-native";
76
80
  import { Icon as Icon2, Portal, Text as Text2 } from "react-native-paper";
@@ -78,8 +82,10 @@ import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
78
82
  function SettingIcon({ source, color, containerColor }) {
79
83
  return /* @__PURE__ */ jsx2(View2, { style: [styles2.iconBadge, { backgroundColor: containerColor }], children: /* @__PURE__ */ jsx2(Icon2, { source, size: 18, color }) });
80
84
  }
81
- function BaseSettingsDialog({ visible, onDismiss, rotation = 0, version, lockOrientation, onLockOrientationChange, deferBottomEdgeGestures, onDeferBottomEdgeGestures, hideSound = false, hideHaptics = false, hideAppearance = false, hideUpdateCheck = false, onUpdateError, children }) {
85
+ function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, version, lockOrientation, onLockOrientationChange, deferBottomEdgeGestures, onDeferBottomEdgeGestures, hideSound = false, hideHaptics = false, hideAppearance = false, hideUpdateCheck = false, onUpdateError, children }) {
82
86
  const { dark, colors } = useAutoPaperTheme2();
87
+ const ambientRotation = useRotation();
88
+ const rotation = rotationOverride ?? ambientRotation;
83
89
  const isTouchPrimary = useIsTouchPrimaryDevice();
84
90
  const { settings: hapticSettings, set: setHapticSettings } = useHapticSettings();
85
91
  const { settings: soundSettings, set: setSoundSettings } = useSoundSettings();
@@ -269,136 +275,56 @@ var styles2 = StyleSheet2.create({
269
275
  });
270
276
 
271
277
  // src/BaseStatsScreen.tsx
272
- import { useAutoPaperTheme as useAutoPaperTheme3 } from "@rific/auto-paper";
273
- import { Button as Button2, IconButton } from "@rific/feedback-press";
278
+ import { useAutoPaperTheme as useAutoPaperTheme4 } from "@rific/auto-paper";
279
+ import { Button as Button3, IconButton } from "@rific/feedback-press";
280
+ import { useRotation as useRotation2 } from "@tastic/core";
274
281
  import { useState as useState2 } from "react";
275
- import { ScrollView as ScrollView2, StyleSheet as StyleSheet3, View as View3 } from "react-native";
276
- import { Icon as Icon3, Portal as Portal2, Text as Text3 } from "react-native-paper";
277
- import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
278
- function BaseStatsScreen({ rotation = 0, title = "Achievements", titleVariant = "headlineSmall", onBack, insets, onReset, resetLabel = "Reset All Stats", resetConfirmTitle = "Reset Everything?", resetConfirmBody = "This permanently erases all stats and achievements. This cannot be undone.", fg: fgOverride, bg: bgOverride, cardBg: cardBgOverride, accentColor: accentColorOverride, children }) {
279
- const { dark, colors } = useAutoPaperTheme3();
280
- const [confirmResetVisible, setConfirmResetVisible] = useState2(false);
281
- const showReset = !!onReset;
282
- const fg = fgOverride ?? (dark ? "#FFFFFF" : "#000000");
283
- const bg = bgOverride ?? (dark ? "#000000" : "#FFFFFF");
284
- const cardBg = cardBgOverride ?? (dark ? "#111111" : "#F2F2F2");
285
- const cardBorder = dark ? "rgba(255,255,255,0.2)" : "rgba(0,0,0,0.2)";
286
- const accentColor = accentColorOverride ?? colors.secondary;
287
- return /* @__PURE__ */ jsxs3(View3, { style: [styles3.container, { backgroundColor: bg }], children: [
288
- /* @__PURE__ */ jsxs3(View3, { style: [styles3.header, { paddingTop: 8 + insets.top, paddingLeft: 8 + insets.left }], children: [
289
- /* @__PURE__ */ jsx3(IconButton, { icon: "arrow-left", iconColor: fg, size: 24, onPress: onBack }),
290
- /* @__PURE__ */ jsx3(Text3, { variant: titleVariant, style: [styles3.title, { color: fg }], children: title })
291
- ] }),
292
- /* @__PURE__ */ jsxs3(ScrollView2, { style: styles3.scrollView, contentContainerStyle: [styles3.content, { paddingBottom: 32 + insets.bottom }], showsVerticalScrollIndicator: false, children: [
293
- children,
294
- showReset && /* @__PURE__ */ jsx3(Button2, { mode: "contained", onPress: () => setConfirmResetVisible(true), buttonColor: colors.secondary, textColor: colors.onSecondary, style: styles3.resetButton, children: resetLabel })
295
- ] }),
296
- confirmResetVisible && /* @__PURE__ */ jsx3(Portal2, { children: /* @__PURE__ */ jsx3(View3, { style: [styles3.overlay, rotation % 360 !== 0 && { transform: [{ rotate: `${rotation}deg` }] }], children: /* @__PURE__ */ jsxs3(View3, { style: [styles3.overlayCard, { backgroundColor: cardBg, borderColor: cardBorder }], children: [
297
- /* @__PURE__ */ jsx3(Icon3, { source: "alert-outline", size: 64, color: accentColor }),
298
- /* @__PURE__ */ jsx3(Text3, { variant: "headlineLarge", style: [styles3.overlayTitle, { color: accentColor }], children: resetConfirmTitle }),
299
- /* @__PURE__ */ jsx3(Text3, { variant: "bodyLarge", style: [styles3.overlayBody, { color: fg }], children: resetConfirmBody }),
300
- /* @__PURE__ */ jsx3(Button2, { mode: "contained", onPress: () => setConfirmResetVisible(false), style: styles3.overlayButton, children: "Cancel" }),
301
- /* @__PURE__ */ jsx3(
302
- Button2,
303
- {
304
- mode: "contained",
305
- onPress: () => {
306
- onReset?.();
307
- setConfirmResetVisible(false);
308
- },
309
- style: styles3.overlayButton,
310
- buttonColor: colors.secondary,
311
- textColor: colors.onSecondary,
312
- children: "Reset"
313
- }
314
- )
315
- ] }) }) })
316
- ] });
317
- }
318
- var styles3 = StyleSheet3.create({
319
- container: {
320
- flex: 1
321
- },
322
- content: {
323
- gap: 12,
324
- paddingHorizontal: 20
325
- },
326
- header: {
327
- alignItems: "center",
328
- flexDirection: "row",
329
- gap: 4,
330
- paddingBottom: 8
331
- },
332
- // Same overlay shape BaseSettingsDialog's own info overlay uses (backdrop + centered card),
333
- // rotated in place the same way rather than via Portal-root transform for the same reason — see
334
- // that component's own identical styles for the full rationale.
335
- overlay: {
336
- alignItems: "center",
337
- backgroundColor: "rgba(0,0,0,0.72)",
338
- bottom: 0,
339
- justifyContent: "center",
340
- left: 0,
341
- position: "absolute",
342
- right: 0,
343
- top: 0
344
- },
345
- overlayBody: { textAlign: "center" },
346
- overlayButton: { width: 160 },
347
- overlayCard: {
348
- alignItems: "center",
349
- borderRadius: 20,
350
- borderWidth: 1,
351
- gap: 16,
352
- maxWidth: 360,
353
- padding: 32
354
- },
355
- overlayTitle: { fontWeight: "bold", marginBottom: -8 },
356
- resetButton: {
357
- marginTop: 16
358
- },
359
- // Bounds the ScrollView to the space `container`'s flex:1 actually gives it — without this, a
360
- // ScrollView with only a contentContainerStyle isn't reliably height-constrained on native (it
361
- // can render at its full unclipped content height instead of the screen's), which leaves the
362
- // header's back button touch target unreliable underneath it.
363
- scrollView: {
364
- flex: 1
365
- },
366
- title: {
367
- flexShrink: 1,
368
- fontWeight: "bold"
369
- }
370
- });
282
+ import { ScrollView as ScrollView2, StyleSheet as StyleSheet4, View as View4 } from "react-native";
283
+ import { Text as Text4 } from "react-native-paper";
371
284
 
372
285
  // src/ConfirmDialog.tsx
373
- import { useAutoPaperTheme as useAutoPaperTheme4 } from "@rific/auto-paper";
374
- import { Button as Button3 } from "@rific/feedback-press";
375
- import { StyleSheet as StyleSheet4, View as View4 } from "react-native";
376
- import { Icon as Icon4, Portal as Portal3, Text as Text4 } from "react-native-paper";
377
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
286
+ import { useAutoPaperTheme as useAutoPaperTheme3 } from "@rific/auto-paper";
287
+ import { Button as Button2 } from "@rific/feedback-press";
288
+ import { StyleSheet as StyleSheet3, View as View3 } from "react-native";
289
+ import { Icon as Icon3, Portal as Portal2, Text as Text3 } from "react-native-paper";
290
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
378
291
  function ConfirmDialog({ visible, title, message, confirmLabel, cancelLabel, onConfirm, onCancel, rotation = 0, icon = "help-circle-outline", destructive = false }) {
379
- const { dark, colors } = useAutoPaperTheme4();
292
+ const { dark, colors } = useAutoPaperTheme3();
380
293
  if (!visible) return null;
381
294
  const fg = dark ? "#FFFFFF" : "#000000";
382
295
  const cardBg = dark ? "#111111" : "#F2F2F2";
383
296
  const cardBorder = dark ? "rgba(255,255,255,0.2)" : "rgba(0,0,0,0.2)";
384
297
  const accent = destructive ? colors.danger : colors.primary;
385
298
  const onAccent = destructive ? colors.onDanger : colors.onPrimary;
386
- return /* @__PURE__ */ jsx4(Portal3, { children: /* @__PURE__ */ jsx4(View4, { style: styles4.overlay, children: /* @__PURE__ */ jsxs4(View4, { style: [styles4.card, { backgroundColor: cardBg, borderColor: cardBorder }, rotation % 360 !== 0 && { transform: [{ rotate: `${rotation}deg` }] }], children: [
387
- /* @__PURE__ */ jsx4(Icon4, { source: icon, size: 64, color: accent }),
388
- /* @__PURE__ */ jsx4(Text4, { variant: "headlineLarge", style: [styles4.title, { color: accent }], children: title }),
389
- /* @__PURE__ */ jsx4(Text4, { variant: "bodyLarge", style: [styles4.body, { color: fg }], children: message }),
390
- /* @__PURE__ */ jsxs4(View4, { style: styles4.actions, children: [
391
- /* @__PURE__ */ jsx4(Button3, { mode: "outlined", onPress: onCancel, style: styles4.actionButton, children: cancelLabel }),
392
- /* @__PURE__ */ jsx4(Button3, { mode: "contained", onPress: onConfirm, style: styles4.actionButton, buttonColor: accent, textColor: onAccent, children: confirmLabel })
299
+ return /* @__PURE__ */ jsx3(Portal2, { children: /* @__PURE__ */ jsx3(View3, { style: styles3.overlay, children: /* @__PURE__ */ jsxs3(View3, { style: [styles3.card, { backgroundColor: cardBg, borderColor: cardBorder }, rotation % 360 !== 0 && { transform: [{ rotate: `${rotation}deg` }] }], children: [
300
+ /* @__PURE__ */ jsx3(Icon3, { source: icon, size: 64, color: accent }),
301
+ /* @__PURE__ */ jsx3(Text3, { variant: "headlineLarge", style: [styles3.title, { color: accent }], children: title }),
302
+ /* @__PURE__ */ jsx3(Text3, { variant: "bodyLarge", style: [styles3.body, { color: fg }], children: message }),
303
+ /* @__PURE__ */ jsxs3(View3, { style: styles3.actions, children: [
304
+ /* @__PURE__ */ jsx3(Button2, { mode: "outlined", onPress: onCancel, style: styles3.actionButton, children: cancelLabel }),
305
+ /* @__PURE__ */ jsx3(Button2, { mode: "contained", onPress: onConfirm, style: styles3.actionButton, buttonColor: accent, textColor: onAccent, children: confirmLabel })
393
306
  ] })
394
307
  ] }) }) });
395
308
  }
396
- var styles4 = StyleSheet4.create({
309
+ var styles3 = StyleSheet3.create({
310
+ // flexBasis (not flex: 1's own 0%) is each button's real floor: whenever both fit side by side,
311
+ // flexGrow still splits any leftover width evenly beyond that floor, same visual result as the
312
+ // old flex: 1 on a roomy screen - but a button can never be squeezed narrower than 128, only
313
+ // wrap to its own row instead (see actions' flexWrap below). react-native-paper's Button
314
+ // hardcodes numberOfLines={1} on its label with no override, so a button squeezed under its
315
+ // label's natural width silently ellipsizes ("Cancel" -> "Can...") instead of wrapping - assumed
316
+ // fixable by just giving the row more width, until confirmed live that these two-word labels
317
+ // (Cancel/Reset/New Game) can still lose the truncation fight even with the extra room card's own
318
+ // width fix below provides, on a narrow enough screen. flexBasis is the real fix: past a certain
319
+ // width there's no side-by-side arrangement that fits both buttons at their natural label size,
320
+ // so the layout has to give (stack) rather than the label (truncate).
397
321
  actionButton: {
398
- flex: 1
322
+ flexBasis: 128,
323
+ flexGrow: 1
399
324
  },
400
325
  actions: {
401
326
  flexDirection: "row",
327
+ flexWrap: "wrap",
402
328
  gap: 12,
403
329
  width: "100%"
404
330
  },
@@ -411,7 +337,12 @@ var styles4 = StyleSheet4.create({
411
337
  borderWidth: 1,
412
338
  gap: 16,
413
339
  maxWidth: 360,
414
- padding: 32
340
+ padding: 32,
341
+ // Claims its full allowance instead of just maxWidth alone, which only caps how wide the card
342
+ // is allowed to get - without an explicit width, a flex-centered child sizes to its own content
343
+ // instead of stretching, so the card (and therefore actions below) can end up narrower than the
344
+ // screen actually has room for, needlessly forcing the action row to wrap sooner than it has to.
345
+ width: "100%"
415
346
  },
416
347
  overlay: {
417
348
  alignItems: "center",
@@ -419,11 +350,88 @@ var styles4 = StyleSheet4.create({
419
350
  bottom: 0,
420
351
  justifyContent: "center",
421
352
  left: 0,
353
+ // Gives the card room to breathe from the screen edges now that it reliably stretches to fill
354
+ // its full width allowance (see card's own width comment) - without this, width: '100%' would
355
+ // run the card edge-to-edge on a screen narrower than maxWidth's own 360.
356
+ paddingHorizontal: 24,
422
357
  position: "absolute",
423
358
  right: 0,
424
359
  top: 0
425
360
  },
361
+ // textAlign is load-bearing, not decorative, once `title` wraps to more than one line - centered
362
+ // within card's own alignItems: 'center' only centers the Text's block as a whole; each wrapped
363
+ // line inside that block still defaults to left-aligned without this.
364
+ title: {
365
+ fontWeight: "bold",
366
+ textAlign: "center"
367
+ }
368
+ });
369
+
370
+ // src/BaseStatsScreen.tsx
371
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
372
+ function BaseStatsScreen({ rotation: rotationOverride, title = "Achievements", titleVariant = "headlineSmall", onBack, insets, onReset, resetLabel = "Reset All Stats", resetConfirmTitle = "Reset Everything?", resetConfirmBody = "This permanently erases all stats and achievements. This cannot be undone.", fg: fgOverride, bg: bgOverride, children }) {
373
+ const { dark, colors } = useAutoPaperTheme4();
374
+ const ambientRotation = useRotation2();
375
+ const rotation = rotationOverride ?? ambientRotation;
376
+ const [confirmResetVisible, setConfirmResetVisible] = useState2(false);
377
+ const showReset = !!onReset;
378
+ const fg = fgOverride ?? (dark ? "#FFFFFF" : "#000000");
379
+ const bg = bgOverride ?? (dark ? "#000000" : "#FFFFFF");
380
+ return /* @__PURE__ */ jsxs4(View4, { style: [styles4.container, { backgroundColor: bg }], children: [
381
+ /* @__PURE__ */ jsxs4(View4, { style: [styles4.header, { paddingTop: 8 + insets.top, paddingLeft: 8 + insets.left }], children: [
382
+ /* @__PURE__ */ jsx4(IconButton, { icon: "arrow-left", iconColor: fg, size: 24, onPress: onBack }),
383
+ /* @__PURE__ */ jsx4(Text4, { variant: titleVariant, style: [styles4.title, { color: fg }], children: title })
384
+ ] }),
385
+ /* @__PURE__ */ jsxs4(ScrollView2, { style: styles4.scrollView, contentContainerStyle: [styles4.content, { paddingBottom: 32 + insets.bottom }], showsVerticalScrollIndicator: false, children: [
386
+ children,
387
+ showReset && /* @__PURE__ */ jsx4(Button3, { mode: "contained", onPress: () => setConfirmResetVisible(true), buttonColor: colors.danger, textColor: colors.onDanger, style: styles4.resetButton, children: resetLabel })
388
+ ] }),
389
+ /* @__PURE__ */ jsx4(
390
+ ConfirmDialog,
391
+ {
392
+ visible: confirmResetVisible,
393
+ title: resetConfirmTitle,
394
+ message: resetConfirmBody,
395
+ confirmLabel: "Reset",
396
+ cancelLabel: "Cancel",
397
+ icon: "alert-outline",
398
+ destructive: true,
399
+ rotation,
400
+ onCancel: () => setConfirmResetVisible(false),
401
+ onConfirm: () => {
402
+ onReset?.();
403
+ setConfirmResetVisible(false);
404
+ }
405
+ }
406
+ )
407
+ ] });
408
+ }
409
+ var styles4 = StyleSheet4.create({
410
+ container: {
411
+ flex: 1
412
+ },
413
+ content: {
414
+ gap: 12,
415
+ paddingHorizontal: 20
416
+ },
417
+ header: {
418
+ alignItems: "center",
419
+ flexDirection: "row",
420
+ gap: 4,
421
+ paddingBottom: 8
422
+ },
423
+ resetButton: {
424
+ marginTop: 16
425
+ },
426
+ // Bounds the ScrollView to the space `container`'s flex:1 actually gives it — without this, a
427
+ // ScrollView with only a contentContainerStyle isn't reliably height-constrained on native (it
428
+ // can render at its full unclipped content height instead of the screen's), which leaves the
429
+ // header's back button touch target unreliable underneath it.
430
+ scrollView: {
431
+ flex: 1
432
+ },
426
433
  title: {
434
+ flexShrink: 1,
427
435
  fontWeight: "bold"
428
436
  }
429
437
  });
@@ -459,8 +467,9 @@ import { Platform as Platform3 } from "react-native";
459
467
  // src/SectionedDropdown.tsx
460
468
  import { getBlendedColor, getColorRoles } from "@rific/auto-paper";
461
469
  import { IconButton as IconButton2, TouchableRipple as TouchableRipple2 } from "@rific/feedback-press";
470
+ import { useRotation as useRotation3 } from "@tastic/core";
462
471
  import { ScrollView as ScrollView3, StyleSheet as StyleSheet7, View as View8 } from "react-native";
463
- import { Icon as Icon5, Text as Text5 } from "react-native-paper";
472
+ import { Icon as Icon4, Text as Text5 } from "react-native-paper";
464
473
 
465
474
  // src/fonts.ts
466
475
  import { Platform as Platform2 } from "react-native";
@@ -532,26 +541,27 @@ var styles6 = StyleSheet6.create({
532
541
  }
533
542
  });
534
543
 
535
- // src/TriggerGaugeHost.tsx
536
- import { lazy, Suspense } from "react";
537
-
538
- // src/loadSkiaWeb.ts
539
- function loadSkiaWeb() {
540
- return Promise.resolve();
541
- }
542
-
543
544
  // src/TriggerGaugeHost.tsx
544
545
  import { jsx as jsx7 } from "react/jsx-runtime";
545
- var LazyTriggerGauge = lazy(() => loadSkiaWeb().then(() => import("./TriggerGauge-2OJOOF6I.mjs").then((m) => ({ default: m.TriggerGauge }))));
546
+ var getTriggerGauge = () => import("./TriggerGauge-2OJOOF6I.mjs").then((mod) => ({ default: mod.TriggerGauge }));
546
547
  function TriggerGaugeHost(props) {
547
- return /* @__PURE__ */ jsx7(Suspense, { fallback: null, children: /* @__PURE__ */ jsx7(LazyTriggerGauge, { ...props }) });
548
+ return /* @__PURE__ */ jsx7(SkiaGate, { getComponent: getTriggerGauge, componentProps: props });
548
549
  }
549
550
 
550
551
  // src/useAutoAlign.ts
551
552
  import { useCallback, useEffect, useRef, useState as useState3 } from "react";
552
553
  import { useWindowDimensions as useWindowDimensions2 } from "react-native";
553
554
  var EDGE_MARGIN = 12;
554
- function useAutoAlign(open, contentWidth, contentHeight) {
555
+ function rotateRect(x, y, width, height, rotation, windowWidth, windowHeight) {
556
+ if (rotation === 0) return { x, y, width, height };
557
+ const cx = windowWidth / 2;
558
+ const cy = windowHeight / 2;
559
+ const dx = x + width / 2 - cx;
560
+ const dy = y + height / 2 - cy;
561
+ const [newDx, newDy, newWidth, newHeight] = rotation === 180 ? [-dx, -dy, width, height] : rotation === 90 ? [-dy, dx, height, width] : [dy, -dx, height, width];
562
+ return { x: cx + newDx - newWidth / 2, y: cy + newDy - newHeight / 2, width: newWidth, height: newHeight };
563
+ }
564
+ function useAutoAlign(open, contentWidth, contentHeight, rotation = 0) {
555
565
  const triggerRef = useRef(null);
556
566
  const { width: windowWidth, height: windowHeight } = useWindowDimensions2();
557
567
  const [align, setAlign] = useState3("center");
@@ -564,7 +574,8 @@ function useAutoAlign(open, contentWidth, contentHeight) {
564
574
  if (open) setMeasured(false);
565
575
  }
566
576
  const measure = useCallback(() => {
567
- triggerRef.current?.measureInWindow((x, y, triggerWidth, triggerHeight) => {
577
+ triggerRef.current?.measureInWindow((rawX, rawY, rawWidth, rawHeight) => {
578
+ const { x, y, width: triggerWidth, height: triggerHeight } = rotateRect(rawX, rawY, rawWidth, rawHeight, rotation, windowWidth, windowHeight);
568
579
  const centerX = x + triggerWidth / 2;
569
580
  const overflowsRight = centerX + contentWidth / 2 > windowWidth - EDGE_MARGIN;
570
581
  const overflowsLeft = centerX - contentWidth / 2 < EDGE_MARGIN;
@@ -578,7 +589,7 @@ function useAutoAlign(open, contentWidth, contentHeight) {
578
589
  setMaxHeight(Math.max(above ? roomAbove : roomBelow, 0));
579
590
  setMeasured(true);
580
591
  });
581
- }, [contentWidth, contentHeight, windowWidth, windowHeight]);
592
+ }, [contentWidth, contentHeight, windowWidth, windowHeight, rotation]);
582
593
  useEffect(() => {
583
594
  if (open) measure();
584
595
  }, [open, measure]);
@@ -597,7 +608,9 @@ var MENU_ITEM_HEIGHT = 40;
597
608
  var MENU_ITEM_HEIGHT_WITH_DESCRIPTION = 58;
598
609
  var MENU_DIVIDER_HEIGHT = 9;
599
610
  var MENU_VERTICAL_PADDING = 12;
600
- function SectionedDropdown({ id, host, icon, accessibilityLabel, sections, accentColor, mutedColor, onAccentColor, dark, align: alignOverride, autoDismiss = true, labelFontFamily = MONO_FONT, allClearLabels = { all: "All", clear: "Clear" } }) {
611
+ function SectionedDropdown({ id, host, icon, accessibilityLabel, sections, accentColor, mutedColor, onAccentColor, dark, align: alignOverride, rotation: rotationOverride, autoDismiss = true, labelFontFamily = MONO_FONT, allClearLabels = { all: "All", clear: "Clear" } }) {
612
+ const ambientRotation = useRotation3();
613
+ const rotation = rotationOverride ?? ambientRotation;
601
614
  const menuBg = dark ? "#000000" : "#FFFFFF";
602
615
  const menuBorderColor = getBlendedColor(mutedColor, menuBg, 0.5);
603
616
  const resolvedOnAccentColor = onAccentColor ?? getColorRoles(accentColor, menuBg).onColor;
@@ -609,7 +622,7 @@ function SectionedDropdown({ id, host, icon, accessibilityLabel, sections, accen
609
622
  const divider = sectionIndex > 0 ? MENU_DIVIDER_HEIGHT : 0;
610
623
  return sum + rows + footer + divider;
611
624
  }, 0);
612
- const { align: autoAlign, maxHeight, measured, triggerRef, verticalAlign } = useAutoAlign(open, MENU_MAX_WIDTH, estimatedHeight);
625
+ const { align: autoAlign, maxHeight, measured, triggerRef, verticalAlign } = useAutoAlign(open, MENU_MAX_WIDTH, estimatedHeight, rotation);
613
626
  const align = alignOverride ?? autoAlign;
614
627
  const onlySection = sections.length === 1 ? sections[0] : null;
615
628
  const selectedOption = onlySection?.kind === "single" ? onlySection.options.find((o) => o.value === onlySection.value) : void 0;
@@ -675,7 +688,7 @@ function SingleSection({
675
688
  const labelStyle = { fontFamily: labelFontFamily, color: rowColor, fontWeight: selected ? "bold" : "normal" };
676
689
  const descriptionStyle = { fontFamily: labelFontFamily, color: rowColor };
677
690
  return /* @__PURE__ */ jsx8(TouchableRipple2, { disabled: taken, onPress: () => onSelect(option.value), style: itemStyle, children: /* @__PURE__ */ jsxs7(View8, { style: styles7.itemRow, children: [
678
- option.icon && /* @__PURE__ */ jsx8(Icon5, { source: option.icon, size: MENU_ITEM_ICON_SIZE, color: rowColor }),
691
+ option.icon && /* @__PURE__ */ jsx8(Icon4, { source: option.icon, size: MENU_ITEM_ICON_SIZE, color: rowColor }),
679
692
  /* @__PURE__ */ jsxs7(View8, { style: styles7.itemLabelColumn, children: [
680
693
  /* @__PURE__ */ jsx8(Text5, { variant: "labelLarge", style: labelStyle, children: option.label }),
681
694
  option.description && /* @__PURE__ */ jsx8(Text5, { variant: "labelSmall", style: descriptionStyle, children: option.description })
@@ -701,7 +714,7 @@ function MultiSection({
701
714
  const itemStyle = [styles7.item, selected && { backgroundColor: accentColor }, prevSelected && { borderTopLeftRadius: 0, borderTopRightRadius: 0 }, nextSelected && { borderBottomLeftRadius: 0, borderBottomRightRadius: 0 }];
702
715
  const labelStyle = { fontFamily: labelFontFamily, color: rowColor, fontWeight: selected ? "bold" : "normal" };
703
716
  return /* @__PURE__ */ jsx8(TouchableRipple2, { onPress: () => section.onChange(selected ? section.value.filter((v) => v !== option.value) : [...section.value, option.value]), style: itemStyle, children: /* @__PURE__ */ jsxs7(View8, { style: styles7.itemRow, children: [
704
- option.icon && /* @__PURE__ */ jsx8(Icon5, { source: option.icon, size: MENU_ITEM_ICON_SIZE, color: rowColor }),
717
+ option.icon && /* @__PURE__ */ jsx8(Icon4, { source: option.icon, size: MENU_ITEM_ICON_SIZE, color: rowColor }),
705
718
  /* @__PURE__ */ jsx8(Text5, { variant: "labelLarge", style: [styles7.itemLabelFlex, labelStyle], children: option.label })
706
719
  ] }) }, option.value);
707
720
  }),
@@ -806,11 +819,11 @@ var styles8 = StyleSheet8.create({
806
819
 
807
820
  // src/CornerStatusBadges.tsx
808
821
  import { StyleSheet as StyleSheet9, View as View9 } from "react-native";
809
- import { Icon as Icon6 } from "react-native-paper";
822
+ import { Icon as Icon5 } from "react-native-paper";
810
823
  import { Fragment as Fragment5, jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
811
824
  var BADGE_SIZE = 30;
812
825
  function StatusBadge({ icon }) {
813
- return /* @__PURE__ */ jsx11(View9, { style: styles9.badge, children: icon && /* @__PURE__ */ jsx11(Icon6, { source: icon, size: 16, color: "rgba(255,255,255,0.85)" }) });
826
+ return /* @__PURE__ */ jsx11(View9, { style: styles9.badge, children: icon && /* @__PURE__ */ jsx11(Icon5, { source: icon, size: 16, color: "rgba(255,255,255,0.85)" }) });
814
827
  }
815
828
  function CornerStatusBadges({ orientationMode, p1OnRight, p1, p2 }) {
816
829
  const p1OnRightSide = orientationMode === "sideBySide" && p1OnRight;
@@ -841,9 +854,9 @@ var styles9 = StyleSheet9.create({
841
854
  // src/InlineColorPicker.tsx
842
855
  import { defaultColors, getContrastColor } from "@rific/auto-paper";
843
856
  import { TouchableRipple as TouchableRipple3 } from "@rific/feedback-press";
844
- import { clamp } from "@tastic/core";
857
+ import { clamp, useRotation as useRotation4 } from "@tastic/core";
845
858
  import { ScrollView as ScrollView4, StyleSheet as StyleSheet10, useWindowDimensions as useWindowDimensions3, View as View10 } from "react-native";
846
- import { Icon as Icon7, Text as Text6 } from "react-native-paper";
859
+ import { Icon as Icon6, Text as Text6 } from "react-native-paper";
847
860
  import { jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
848
861
  var EMOJI_PATTERN = /\p{Extended_Pictographic}/u;
849
862
  var DEFAULT_SIZE = 48;
@@ -857,7 +870,9 @@ var SCREEN_MARGIN = 40;
857
870
  function widthForColumns(columns) {
858
871
  return SWATCHES_BORDER_WIDTH * 2 + SWATCHES_PADDING * 2 + SWATCH_SIZE * columns + SWATCHES_GAP * (columns - 1);
859
872
  }
860
- function InlineColorPicker({ id, host, value, onChange, previewValue, swatches = defaultColors, takenValue, allowSwapTaken, dark, align: alignOverride, icon = "palette", tag, labelFontFamily = MONO_FONT, autoDismiss = true, columns, size = DEFAULT_SIZE }) {
873
+ function InlineColorPicker({ id, host, value, onChange, previewValue, swatches = defaultColors, takenValue, allowSwapTaken, dark, align: alignOverride, rotation: rotationOverride, icon = "palette", tag, labelFontFamily = MONO_FONT, autoDismiss = true, columns, size = DEFAULT_SIZE }) {
874
+ const ambientRotation = useRotation4();
875
+ const rotation = rotationOverride ?? ambientRotation;
861
876
  const menuBg = dark ? "#000000" : "#FFFFFF";
862
877
  const { width: windowWidth } = useWindowDimensions3();
863
878
  const autoColumns = clamp(Math.floor((windowWidth - 2 * SCREEN_MARGIN + SWATCHES_GAP) / (SWATCH_SIZE + SWATCHES_GAP)), MIN_COLUMNS, MAX_COLUMNS);
@@ -866,14 +881,14 @@ function InlineColorPicker({ id, host, value, onChange, previewValue, swatches =
866
881
  const swatchRows = Math.ceil(swatches.length / resolvedColumns);
867
882
  const swatchesHeight = SWATCHES_BORDER_WIDTH * 2 + SWATCHES_PADDING * 2 + SWATCH_SIZE * swatchRows + SWATCHES_GAP * (swatchRows - 1);
868
883
  const open = host.openId === id;
869
- const { align: autoAlign, maxHeight, measured, triggerRef, verticalAlign } = useAutoAlign(open, swatchesWidth, swatchesHeight);
884
+ const { align: autoAlign, maxHeight, measured, triggerRef, verticalAlign } = useAutoAlign(open, swatchesWidth, swatchesHeight, rotation);
870
885
  const align = alignOverride ?? autoAlign;
871
886
  const displayFor = previewValue ?? ((hex) => hex);
872
887
  const displayValue = displayFor(value);
873
888
  const contrastColor = getContrastColor(displayValue);
874
889
  const tagFontSize = size * (tag && EMOJI_PATTERN.test(tag) ? 0.6 : 0.4);
875
890
  return /* @__PURE__ */ jsxs10(View10, { style: [styles10.anchor, open && styles10.anchorOpen], children: [
876
- /* @__PURE__ */ jsx12(TouchableRipple3, { onPress: () => host.toggle(id), borderless: true, style: [styles10.trigger, { backgroundColor: displayValue, borderRadius: size / 2, height: size, width: size }], children: /* @__PURE__ */ jsx12(View10, { ref: triggerRef, collapsable: false, style: styles10.triggerMeasure, children: tag ? /* @__PURE__ */ jsx12(Text6, { style: [styles10.tagLabel, { color: contrastColor, fontFamily: labelFontFamily, fontSize: tagFontSize }], numberOfLines: 1, adjustsFontSizeToFit: true, children: tag }) : /* @__PURE__ */ jsx12(Icon7, { source: icon, size: size * 0.6, color: contrastColor }) }) }),
891
+ /* @__PURE__ */ jsx12(TouchableRipple3, { onPress: () => host.toggle(id), borderless: true, style: [styles10.trigger, { backgroundColor: displayValue, borderRadius: size / 2, height: size, width: size }], children: /* @__PURE__ */ jsx12(View10, { ref: triggerRef, collapsable: false, style: styles10.triggerMeasure, children: tag ? /* @__PURE__ */ jsx12(Text6, { style: [styles10.tagLabel, { color: contrastColor, fontFamily: labelFontFamily, fontSize: tagFontSize }], numberOfLines: 1, adjustsFontSizeToFit: true, children: tag }) : /* @__PURE__ */ jsx12(Icon6, { source: icon, size: size * 0.6, color: contrastColor }) }) }),
877
892
  /* @__PURE__ */ jsx12(PopoverBody, { visible: open && measured, align, verticalAlign, caretColor: menuBg, caretBorderColor: displayValue, caretBorderWidth: SWATCHES_BORDER_WIDTH, triggerSize: size, children: /* @__PURE__ */ jsx12(ScrollView4, { style: [styles10.swatches, { backgroundColor: menuBg, borderColor: displayValue, width: swatchesWidth, maxHeight }], contentContainerStyle: styles10.swatchesContent, showsVerticalScrollIndicator: false, children: swatches.map((swatch) => {
878
893
  const selected = swatch.value.toLowerCase() === value.toLowerCase();
879
894
  const taken = !selected && !!takenValue && swatch.value.toLowerCase() === takenValue.toLowerCase();
@@ -889,7 +904,7 @@ function InlineColorPicker({ id, host, value, onChange, previewValue, swatches =
889
904
  },
890
905
  borderless: true,
891
906
  style: [styles10.swatch, { backgroundColor: swatchDisplay }, selected && styles10.swatchSelected, taken && !swappable && styles10.swatchTaken],
892
- children: selected ? /* @__PURE__ */ jsx12(Icon7, { source: "check", size: 16, color: getContrastColor(swatchDisplay) }) : swappable ? /* @__PURE__ */ jsx12(Icon7, { source: "swap-horizontal", size: 16, color: getContrastColor(swatchDisplay) }) : taken ? /* @__PURE__ */ jsx12(Icon7, { source: "close", size: 16, color: getContrastColor(swatchDisplay) }) : /* @__PURE__ */ jsx12(View10, {})
907
+ children: selected ? /* @__PURE__ */ jsx12(Icon6, { source: "check", size: 16, color: getContrastColor(swatchDisplay) }) : swappable ? /* @__PURE__ */ jsx12(Icon6, { source: "swap-horizontal", size: 16, color: getContrastColor(swatchDisplay) }) : taken ? /* @__PURE__ */ jsx12(Icon6, { source: "close", size: 16, color: getContrastColor(swatchDisplay) }) : /* @__PURE__ */ jsx12(View10, {})
893
908
  },
894
909
  swatch.value
895
910
  );
@@ -971,7 +986,7 @@ var styles10 = StyleSheet10.create({
971
986
  import { getContrastColor as getContrastColor2 } from "@rific/auto-paper";
972
987
  import { TouchableRipple as TouchableRipple4 } from "@rific/feedback-press";
973
988
  import { ScrollView as ScrollView5, StyleSheet as StyleSheet11, View as View11 } from "react-native";
974
- import { Icon as Icon8, Text as Text7 } from "react-native-paper";
989
+ import { Icon as Icon7, Text as Text7 } from "react-native-paper";
975
990
  import { jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
976
991
  var TRIGGER_HEIGHT = 28;
977
992
  var POPOVER_WIDTH = 180;
@@ -998,13 +1013,13 @@ function LabeledDropdown({ id, host, options, value, onChange, color, dark, alig
998
1013
  return /* @__PURE__ */ jsxs11(View11, { style: [styles11.anchor, open && styles11.anchorOpen], children: [
999
1014
  /* @__PURE__ */ jsx13(TouchableRipple4, { onPress: () => host.toggle(id), style: styles11.trigger, children: /* @__PURE__ */ jsxs11(View11, { ref: triggerRef, collapsable: false, style: styles11.triggerInner, children: [
1000
1015
  /* @__PURE__ */ jsx13(Text7, { style: [styles11.triggerLabel, { color }], numberOfLines: 1, children: (selected?.label ?? "").toUpperCase() }),
1001
- /* @__PURE__ */ jsx13(Icon8, { source: "menu-down", size: 16, color })
1016
+ /* @__PURE__ */ jsx13(Icon7, { source: "menu-down", size: 16, color })
1002
1017
  ] }) }),
1003
1018
  /* @__PURE__ */ jsx13(PopoverBody, { visible: open && measured, align, verticalAlign, caretColor: menuBg, caretBorderColor: color, caretBorderWidth: 2, triggerSize: TRIGGER_HEIGHT, children: /* @__PURE__ */ jsx13(ScrollView5, { style: [styles11.menu, { backgroundColor: menuBg, borderColor: color, width: POPOVER_WIDTH, maxHeight }], contentContainerStyle: styles11.menuContent, showsVerticalScrollIndicator: false, children: options.map((option) => {
1004
1019
  const isSelected = option.value === value;
1005
1020
  const onColor = getContrastColor2(color);
1006
1021
  return /* @__PURE__ */ jsx13(TouchableRipple4, { onPress: () => handleSelect(option), style: [styles11.row, isSelected && { backgroundColor: color }], children: /* @__PURE__ */ jsxs11(View11, { style: styles11.rowInner, children: [
1007
- option.icon && /* @__PURE__ */ jsx13(Icon8, { source: option.icon, size: 18, color: isSelected ? onColor : fg }),
1022
+ option.icon && /* @__PURE__ */ jsx13(Icon7, { source: option.icon, size: 18, color: isSelected ? onColor : fg }),
1008
1023
  /* @__PURE__ */ jsx13(Text7, { style: [styles11.rowLabel, { color: isSelected ? onColor : fg }], numberOfLines: 1, children: option.label })
1009
1024
  ] }) }, option.value);
1010
1025
  }) }) })
@@ -1062,6 +1077,11 @@ var styles11 = StyleSheet11.create({
1062
1077
  }
1063
1078
  });
1064
1079
 
1080
+ // src/loadSkiaWeb.ts
1081
+ function loadSkiaWeb() {
1082
+ return Promise.resolve();
1083
+ }
1084
+
1065
1085
  // src/PressAwayOverlay.tsx
1066
1086
  import { Pressable, StyleSheet as StyleSheet12 } from "react-native";
1067
1087
  import { jsx as jsx14 } from "react/jsx-runtime";
@@ -1207,6 +1227,7 @@ export {
1207
1227
  ReadyButton,
1208
1228
  SectionedDropdown,
1209
1229
  SharedActionBand,
1230
+ SkiaGate,
1210
1231
  StatRow,
1211
1232
  StatSection,
1212
1233
  TriggerGaugeHost,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tastic/hud",
3
- "version": "0.7.0",
3
+ "version": "0.8.2",
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",
@@ -33,6 +33,13 @@
33
33
  "types": "./dist/index.d.ts",
34
34
  "import": "./dist/index.mjs",
35
35
  "require": "./dist/index.js"
36
+ },
37
+ "./skia-gate": {
38
+ "react-native": "./src/SkiaGate.tsx",
39
+ "browser": "./src/SkiaGate.web.tsx",
40
+ "types": "./dist/SkiaGate.d.ts",
41
+ "import": "./dist/SkiaGate.mjs",
42
+ "require": "./dist/SkiaGate.js"
36
43
  }
37
44
  },
38
45
  "main": "dist/index.js",
@@ -63,14 +70,14 @@
63
70
  },
64
71
  "prettier": "@infinitetoken/eslint-config/prettier",
65
72
  "devDependencies": {
66
- "@infinitetoken/eslint-config": "0.2.0",
67
- "@infinitetoken/jest-config": "^0.2.0",
73
+ "@infinitetoken/eslint-config": "^0.2.0",
74
+ "@infinitetoken/jest-config": "^0.3.0",
68
75
  "@infinitetoken/tsconfig": "^0.4.1",
69
76
  "@rific/auto-paper": "^0.10.3",
70
77
  "@rific/feedback-press": "^0.10.8",
71
- "@rific/updater": "^0.4.2",
78
+ "@rific/updater": "^0.6.0",
72
79
  "@shopify/react-native-skia": "^2.6.2",
73
- "@tastic/core": "^0.2.0",
80
+ "@tastic/core": "^0.3.0",
74
81
  "@testing-library/dom": "^10.4.1",
75
82
  "@testing-library/react": "^16.3.2",
76
83
  "@types/jest": "^30.0.0",
@@ -94,7 +101,7 @@
94
101
  "@rific/feedback-press": ">=0.10.0",
95
102
  "@rific/updater": ">=0.4.0",
96
103
  "@shopify/react-native-skia": ">=1.5.0",
97
- "@tastic/core": ">=0.1.0",
104
+ "@tastic/core": ">=0.3.0",
98
105
  "react": ">=19.0.0",
99
106
  "react-native": ">=0.76.0",
100
107
  "react-native-paper": ">=5.0.0",
@@ -1,7 +1,7 @@
1
1
  import { AutoAppearancePicker, Dialog, useAutoPaperTheme } from '@rific/auto-paper'
2
2
  import { Button, SoundContext, TouchableRipple, useHapticSettings, useSoundSettings, useVibration } from '@rific/feedback-press'
3
3
  import { useUpdater } from '@rific/updater'
4
- import { useIsTouchPrimaryDevice } from '@tastic/core'
4
+ import { useIsTouchPrimaryDevice, useRotation } from '@tastic/core'
5
5
  import { ReactNode, useContext, useState } from 'react'
6
6
  import { Platform, ScrollView, StyleSheet, View } from 'react-native'
7
7
  import { Icon, Portal, Text } from 'react-native-paper'
@@ -26,10 +26,11 @@ function SettingIcon({ source, color, containerColor }: SettingIconProps) {
26
26
  export interface BaseSettingsDialogProps {
27
27
  visible: boolean
28
28
  onDismiss: () => void
29
- // Live physical-hold rotation (see @tastic/split-screen's getViewRotation) — this is a centered,
30
- // app-wide modal with no per-player zone to match (unlike an in-game round-over dialog), so it
31
- // just rotates its own content in place; defaults to 0 for a caller that doesn't track one (a
32
- // game with no face-to-face two-player mode has nothing to stay consistent with anyway).
29
+ // Explicit override for the live physical-hold rotation (see @tastic/core's useRotation) — this is
30
+ // a centered, app-wide modal with no per-player zone to match (unlike an in-game round-over
31
+ // dialog), so it just rotates its own content in place. Defaults to a live ambient read via
32
+ // useRotation() when omitted; pass this only when a caller's own reading needs to differ from the
33
+ // ambient one (rare — most callers should simply omit it).
33
34
  rotation?: number
34
35
  // Each app's own OTA version (e.g. release.otaVersion) — not something this package can read
35
36
  // itself, since every consuming app tracks its own release.ts independently. Accepts a number too
@@ -76,8 +77,10 @@ export interface BaseSettingsDialogProps {
76
77
  // is the seam for whatever isn't shared: a game with its own board/difficulty settings (or a stats
77
78
  // backup flow, or anything else genuinely specific to it) renders those below Appearance, not by
78
79
  // forking this component.
79
- export function BaseSettingsDialog({ visible, onDismiss, rotation = 0, version, lockOrientation, onLockOrientationChange, deferBottomEdgeGestures, onDeferBottomEdgeGestures, hideSound = false, hideHaptics = false, hideAppearance = false, hideUpdateCheck = false, onUpdateError, children }: BaseSettingsDialogProps) {
80
+ export function BaseSettingsDialog({ visible, onDismiss, rotation: rotationOverride, version, lockOrientation, onLockOrientationChange, deferBottomEdgeGestures, onDeferBottomEdgeGestures, hideSound = false, hideHaptics = false, hideAppearance = false, hideUpdateCheck = false, onUpdateError, children }: BaseSettingsDialogProps) {
80
81
  const { dark, colors } = useAutoPaperTheme()
82
+ const ambientRotation = useRotation()
83
+ const rotation = rotationOverride ?? ambientRotation
81
84
  // Also gates the Lock Orientation row below (alongside showLockOrientation itself) — true
82
85
  // unconditionally on native (see the hook's own doc), so this only actually excludes a
83
86
  // desktop/laptop browser, where a mouse-driven window has no physical orientation to lock