@tastic/hud 0.4.0 → 0.5.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/dist/index.d.mts CHANGED
@@ -13,8 +13,11 @@ interface Props$b {
13
13
  checkColor?: string;
14
14
  progress?: number;
15
15
  deviceMarker?: boolean;
16
+ fg?: string;
17
+ fgMuted?: string;
18
+ sectionBg?: string;
16
19
  }
17
- declare function AchievementRow({ icon, title, description, badgeColor, unlockedLabel, checkColor, progress, deviceMarker }: Props$b): react.JSX.Element;
20
+ declare function AchievementRow({ icon, title, description, badgeColor, unlockedLabel, checkColor, progress, deviceMarker, fg: fgOverride, fgMuted: fgMutedOverride, sectionBg: sectionBgOverride }: Props$b): react.JSX.Element;
18
21
 
19
22
  interface BaseSettingsDialogProps {
20
23
  visible: boolean;
@@ -34,9 +37,11 @@ interface BaseSettingsDialogProps {
34
37
  }
35
38
  declare function BaseSettingsDialog({ visible, onDismiss, rotation, version, lockOrientation, onLockOrientationChange, deferBottomEdgeGestures, onDeferBottomEdgeGestures, hideSound, hideHaptics, hideAppearance, hideUpdateCheck, onUpdateError, children }: BaseSettingsDialogProps): react.JSX.Element;
36
39
 
40
+ type MD3TextVariant = 'displayLarge' | 'displayMedium' | 'displaySmall' | 'headlineLarge' | 'headlineMedium' | 'headlineSmall' | 'titleLarge' | 'titleMedium' | 'titleSmall' | 'labelLarge' | 'labelMedium' | 'labelSmall' | 'bodyLarge' | 'bodyMedium' | 'bodySmall';
37
41
  interface Props$a {
38
42
  rotation?: number;
39
43
  title?: string;
44
+ titleVariant?: MD3TextVariant;
40
45
  onBack: () => void;
41
46
  insets: {
42
47
  top: number;
@@ -48,9 +53,13 @@ interface Props$a {
48
53
  resetLabel?: string;
49
54
  resetConfirmTitle?: string;
50
55
  resetConfirmBody?: string;
56
+ fg?: string;
57
+ bg?: string;
58
+ cardBg?: string;
59
+ accentColor?: string;
51
60
  children: ReactNode;
52
61
  }
53
- declare function BaseStatsScreen({ rotation, title, onBack, insets, onReset, resetLabel, resetConfirmTitle, resetConfirmBody, children }: Props$a): react.JSX.Element;
62
+ declare function BaseStatsScreen({ rotation, title, titleVariant, onBack, insets, onReset, resetLabel, resetConfirmTitle, resetConfirmBody, fg: fgOverride, bg: bgOverride, cardBg: cardBgOverride, accentColor: accentColorOverride, children }: Props$a): react.JSX.Element;
54
63
 
55
64
  interface Props$9 {
56
65
  onBack: () => void;
@@ -216,14 +225,18 @@ declare function SharedActionBand({ onBack, onSettings, onRandomize, onReset, fg
216
225
  interface Props$1 {
217
226
  label: string;
218
227
  value: string;
228
+ fg?: string;
229
+ fgMuted?: string;
219
230
  }
220
- declare function StatRow({ label, value }: Props$1): react.JSX.Element;
231
+ declare function StatRow({ label, value, fg: fgOverride, fgMuted: fgMutedOverride }: Props$1): react.JSX.Element;
221
232
 
222
233
  interface Props {
223
234
  label: string;
235
+ fg?: string;
236
+ sectionBg?: string;
224
237
  children: ReactNode;
225
238
  }
226
- declare function StatSection({ label, children }: Props): react.JSX.Element;
239
+ declare function StatSection({ label, fg: fgOverride, sectionBg: sectionBgOverride, children }: Props): react.JSX.Element;
227
240
 
228
241
  interface TriggerGaugeProps {
229
242
  segments: number;
package/dist/index.d.ts CHANGED
@@ -13,8 +13,11 @@ interface Props$b {
13
13
  checkColor?: string;
14
14
  progress?: number;
15
15
  deviceMarker?: boolean;
16
+ fg?: string;
17
+ fgMuted?: string;
18
+ sectionBg?: string;
16
19
  }
17
- declare function AchievementRow({ icon, title, description, badgeColor, unlockedLabel, checkColor, progress, deviceMarker }: Props$b): react.JSX.Element;
20
+ declare function AchievementRow({ icon, title, description, badgeColor, unlockedLabel, checkColor, progress, deviceMarker, fg: fgOverride, fgMuted: fgMutedOverride, sectionBg: sectionBgOverride }: Props$b): react.JSX.Element;
18
21
 
19
22
  interface BaseSettingsDialogProps {
20
23
  visible: boolean;
@@ -34,9 +37,11 @@ interface BaseSettingsDialogProps {
34
37
  }
35
38
  declare function BaseSettingsDialog({ visible, onDismiss, rotation, version, lockOrientation, onLockOrientationChange, deferBottomEdgeGestures, onDeferBottomEdgeGestures, hideSound, hideHaptics, hideAppearance, hideUpdateCheck, onUpdateError, children }: BaseSettingsDialogProps): react.JSX.Element;
36
39
 
40
+ type MD3TextVariant = 'displayLarge' | 'displayMedium' | 'displaySmall' | 'headlineLarge' | 'headlineMedium' | 'headlineSmall' | 'titleLarge' | 'titleMedium' | 'titleSmall' | 'labelLarge' | 'labelMedium' | 'labelSmall' | 'bodyLarge' | 'bodyMedium' | 'bodySmall';
37
41
  interface Props$a {
38
42
  rotation?: number;
39
43
  title?: string;
44
+ titleVariant?: MD3TextVariant;
40
45
  onBack: () => void;
41
46
  insets: {
42
47
  top: number;
@@ -48,9 +53,13 @@ interface Props$a {
48
53
  resetLabel?: string;
49
54
  resetConfirmTitle?: string;
50
55
  resetConfirmBody?: string;
56
+ fg?: string;
57
+ bg?: string;
58
+ cardBg?: string;
59
+ accentColor?: string;
51
60
  children: ReactNode;
52
61
  }
53
- declare function BaseStatsScreen({ rotation, title, onBack, insets, onReset, resetLabel, resetConfirmTitle, resetConfirmBody, children }: Props$a): react.JSX.Element;
62
+ declare function BaseStatsScreen({ rotation, title, titleVariant, onBack, insets, onReset, resetLabel, resetConfirmTitle, resetConfirmBody, fg: fgOverride, bg: bgOverride, cardBg: cardBgOverride, accentColor: accentColorOverride, children }: Props$a): react.JSX.Element;
54
63
 
55
64
  interface Props$9 {
56
65
  onBack: () => void;
@@ -216,14 +225,18 @@ declare function SharedActionBand({ onBack, onSettings, onRandomize, onReset, fg
216
225
  interface Props$1 {
217
226
  label: string;
218
227
  value: string;
228
+ fg?: string;
229
+ fgMuted?: string;
219
230
  }
220
- declare function StatRow({ label, value }: Props$1): react.JSX.Element;
231
+ declare function StatRow({ label, value, fg: fgOverride, fgMuted: fgMutedOverride }: Props$1): react.JSX.Element;
221
232
 
222
233
  interface Props {
223
234
  label: string;
235
+ fg?: string;
236
+ sectionBg?: string;
224
237
  children: ReactNode;
225
238
  }
226
- declare function StatSection({ label, children }: Props): react.JSX.Element;
239
+ declare function StatSection({ label, fg: fgOverride, sectionBg: sectionBgOverride, children }: Props): react.JSX.Element;
227
240
 
228
241
  interface TriggerGaugeProps {
229
242
  segments: number;
package/dist/index.js CHANGED
@@ -187,11 +187,11 @@ var import_react_native = require("react-native");
187
187
  var import_react_native_paper = require("react-native-paper");
188
188
  var import_jsx_runtime = require("react/jsx-runtime");
189
189
  var LOCKED_BADGE_COLOR = "rgba(128,128,128,0.3)";
190
- function AchievementRow({ icon, title, description, badgeColor, unlockedLabel, checkColor, progress, deviceMarker }) {
190
+ function AchievementRow({ icon, title, description, badgeColor, unlockedLabel, checkColor, progress, deviceMarker, fg: fgOverride, fgMuted: fgMutedOverride, sectionBg: sectionBgOverride }) {
191
191
  const { dark } = (0, import_auto_paper.useAutoPaperTheme)();
192
- const fg = dark ? "#FFFFFF" : "#000000";
193
- const fgMuted = dark ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.5)";
194
- const sectionBg = dark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.04)";
192
+ const fg = fgOverride ?? (dark ? "#FFFFFF" : "#000000");
193
+ const fgMuted = fgMutedOverride ?? (dark ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.5)");
194
+ const sectionBg = sectionBgOverride ?? (dark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.04)");
195
195
  const unlocked = unlockedLabel !== void 0;
196
196
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native.View, { style: [styles.row, { backgroundColor: sectionBg }], children: [
197
197
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native.View, { style: [styles.badge, { backgroundColor: badgeColor }], children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native_paper.Icon, { source: icon, size: 20, color: unlocked ? "#000000" : fgMuted }) }),
@@ -458,26 +458,27 @@ var import_react2 = require("react");
458
458
  var import_react_native3 = require("react-native");
459
459
  var import_react_native_paper3 = require("react-native-paper");
460
460
  var import_jsx_runtime3 = require("react/jsx-runtime");
461
- function BaseStatsScreen({ rotation = 0, title = "Achievements", onBack, insets, onReset, resetLabel = "Reset All Stats", resetConfirmTitle = "Reset Everything?", resetConfirmBody = "This permanently erases all stats and achievements. This cannot be undone.", children }) {
461
+ 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 }) {
462
462
  const { dark, colors } = (0, import_auto_paper3.useAutoPaperTheme)();
463
463
  const [confirmResetVisible, setConfirmResetVisible] = (0, import_react2.useState)(false);
464
464
  const showReset = !!onReset;
465
- const fg = dark ? "#FFFFFF" : "#000000";
466
- const bg = dark ? "#000000" : "#FFFFFF";
467
- const cardBg = dark ? "#111111" : "#F2F2F2";
465
+ const fg = fgOverride ?? (dark ? "#FFFFFF" : "#000000");
466
+ const bg = bgOverride ?? (dark ? "#000000" : "#FFFFFF");
467
+ const cardBg = cardBgOverride ?? (dark ? "#111111" : "#F2F2F2");
468
468
  const cardBorder = dark ? "rgba(255,255,255,0.2)" : "rgba(0,0,0,0.2)";
469
+ const accentColor = accentColorOverride ?? colors.secondary;
469
470
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_react_native3.View, { style: [styles3.container, { backgroundColor: bg }], children: [
470
471
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_react_native3.View, { style: [styles3.header, { paddingTop: 8 + insets.top, paddingLeft: 8 + insets.left }], children: [
471
472
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_feedback_press2.IconButton, { icon: "arrow-left", iconColor: fg, size: 24, onPress: onBack }),
472
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native_paper3.Text, { variant: "displaySmall", style: [styles3.title, { color: fg }], children: title })
473
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native_paper3.Text, { variant: titleVariant, style: [styles3.title, { color: fg }], children: title })
473
474
  ] }),
474
475
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_react_native3.ScrollView, { style: styles3.scrollView, contentContainerStyle: [styles3.content, { paddingBottom: 32 + insets.bottom }], showsVerticalScrollIndicator: false, children: [
475
476
  children,
476
- showReset && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_feedback_press2.Button, { mode: "outlined", onPress: () => setConfirmResetVisible(true), textColor: colors.secondary, style: styles3.resetButton, children: resetLabel })
477
+ showReset && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_feedback_press2.Button, { mode: "contained", onPress: () => setConfirmResetVisible(true), buttonColor: colors.secondary, textColor: colors.onSecondary, style: styles3.resetButton, children: resetLabel })
477
478
  ] }),
478
479
  confirmResetVisible && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native_paper3.Portal, { children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native3.View, { style: [styles3.overlay, rotation % 360 !== 0 && { transform: [{ rotate: `${rotation}deg` }] }], children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_react_native3.View, { style: [styles3.overlayCard, { backgroundColor: cardBg, borderColor: cardBorder }], children: [
479
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native_paper3.Icon, { source: "alert-outline", size: 64, color: colors.secondary }),
480
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native_paper3.Text, { variant: "headlineLarge", style: [styles3.overlayTitle, { color: colors.secondary }], children: resetConfirmTitle }),
480
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native_paper3.Icon, { source: "alert-outline", size: 64, color: accentColor }),
481
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native_paper3.Text, { variant: "headlineLarge", style: [styles3.overlayTitle, { color: accentColor }], children: resetConfirmTitle }),
481
482
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native_paper3.Text, { variant: "bodyLarge", style: [styles3.overlayBody, { color: fg }], children: resetConfirmBody }),
482
483
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_feedback_press2.Button, { mode: "contained", onPress: () => setConfirmResetVisible(false), style: styles3.overlayButton, children: "Cancel" }),
483
484
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
@@ -1172,10 +1173,10 @@ var import_auto_paper7 = require("@rific/auto-paper");
1172
1173
  var import_react_native15 = require("react-native");
1173
1174
  var import_react_native_paper8 = require("react-native-paper");
1174
1175
  var import_jsx_runtime14 = require("react/jsx-runtime");
1175
- function StatRow({ label, value }) {
1176
+ function StatRow({ label, value, fg: fgOverride, fgMuted: fgMutedOverride }) {
1176
1177
  const { dark } = (0, import_auto_paper7.useAutoPaperTheme)();
1177
- const fg = dark ? "#FFFFFF" : "#000000";
1178
- const fgMuted = dark ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.5)";
1178
+ const fg = fgOverride ?? (dark ? "#FFFFFF" : "#000000");
1179
+ const fgMuted = fgMutedOverride ?? (dark ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.5)");
1179
1180
  return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_react_native15.View, { style: styles12.row, children: [
1180
1181
  /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native_paper8.Text, { variant: "bodyMedium", style: { color: fgMuted }, children: label }),
1181
1182
  /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native_paper8.Text, { variant: "bodyMedium", style: [styles12.value, { color: fg }], children: value })
@@ -1197,17 +1198,22 @@ var import_auto_paper8 = require("@rific/auto-paper");
1197
1198
  var import_react_native16 = require("react-native");
1198
1199
  var import_react_native_paper9 = require("react-native-paper");
1199
1200
  var import_jsx_runtime15 = require("react/jsx-runtime");
1200
- function StatSection({ label, children }) {
1201
+ function StatSection({ label, fg: fgOverride, sectionBg: sectionBgOverride, children }) {
1201
1202
  const { dark } = (0, import_auto_paper8.useAutoPaperTheme)();
1202
- const fgMuted = dark ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.5)";
1203
- const sectionBg = dark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.04)";
1203
+ const fg = fgOverride ?? (dark ? "#FFFFFF" : "#000000");
1204
+ const sectionBg = sectionBgOverride ?? (dark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.04)");
1204
1205
  return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_react_native16.View, { style: [styles13.section, { backgroundColor: sectionBg }], children: [
1205
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_react_native_paper9.Text, { variant: "labelMedium", style: [styles13.label, { color: fgMuted }], children: label }),
1206
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_react_native_paper9.Text, { variant: "labelMedium", style: [styles13.label, { color: fg }], children: label }),
1206
1207
  children
1207
1208
  ] });
1208
1209
  }
1209
1210
  var styles13 = import_react_native16.StyleSheet.create({
1211
+ // Bold and full-contrast (fg), not the original fontless/fgMuted treatment — that read at the
1212
+ // same visual weight as StatRow's own row labels below it (both used the identical muted color;
1213
+ // letterSpacing/variant alone didn't separate them enough to read as "this is the group header"
1214
+ // at a glance). Matches AchievementRow's own title styling (bold + fg) below it in the same list.
1210
1215
  label: {
1216
+ fontWeight: "bold",
1211
1217
  letterSpacing: 2
1212
1218
  },
1213
1219
  section: {
package/dist/index.mjs CHANGED
@@ -4,11 +4,11 @@ import { StyleSheet, View } from "react-native";
4
4
  import { Icon, ProgressBar, Text } from "react-native-paper";
5
5
  import { jsx, jsxs } from "react/jsx-runtime";
6
6
  var LOCKED_BADGE_COLOR = "rgba(128,128,128,0.3)";
7
- function AchievementRow({ icon, title, description, badgeColor, unlockedLabel, checkColor, progress, deviceMarker }) {
7
+ function AchievementRow({ icon, title, description, badgeColor, unlockedLabel, checkColor, progress, deviceMarker, fg: fgOverride, fgMuted: fgMutedOverride, sectionBg: sectionBgOverride }) {
8
8
  const { dark } = useAutoPaperTheme();
9
- const fg = dark ? "#FFFFFF" : "#000000";
10
- const fgMuted = dark ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.5)";
11
- const sectionBg = dark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.04)";
9
+ const fg = fgOverride ?? (dark ? "#FFFFFF" : "#000000");
10
+ const fgMuted = fgMutedOverride ?? (dark ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.5)");
11
+ const sectionBg = sectionBgOverride ?? (dark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.04)");
12
12
  const unlocked = unlockedLabel !== void 0;
13
13
  return /* @__PURE__ */ jsxs(View, { style: [styles.row, { backgroundColor: sectionBg }], children: [
14
14
  /* @__PURE__ */ jsx(View, { style: [styles.badge, { backgroundColor: badgeColor }], children: /* @__PURE__ */ jsx(Icon, { source: icon, size: 20, color: unlocked ? "#000000" : fgMuted }) }),
@@ -275,26 +275,27 @@ import { useState as useState2 } from "react";
275
275
  import { ScrollView as ScrollView2, StyleSheet as StyleSheet3, View as View3 } from "react-native";
276
276
  import { Icon as Icon3, Portal as Portal2, Text as Text3 } from "react-native-paper";
277
277
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
278
- function BaseStatsScreen({ rotation = 0, title = "Achievements", onBack, insets, onReset, resetLabel = "Reset All Stats", resetConfirmTitle = "Reset Everything?", resetConfirmBody = "This permanently erases all stats and achievements. This cannot be undone.", children }) {
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
279
  const { dark, colors } = useAutoPaperTheme3();
280
280
  const [confirmResetVisible, setConfirmResetVisible] = useState2(false);
281
281
  const showReset = !!onReset;
282
- const fg = dark ? "#FFFFFF" : "#000000";
283
- const bg = dark ? "#000000" : "#FFFFFF";
284
- const cardBg = dark ? "#111111" : "#F2F2F2";
282
+ const fg = fgOverride ?? (dark ? "#FFFFFF" : "#000000");
283
+ const bg = bgOverride ?? (dark ? "#000000" : "#FFFFFF");
284
+ const cardBg = cardBgOverride ?? (dark ? "#111111" : "#F2F2F2");
285
285
  const cardBorder = dark ? "rgba(255,255,255,0.2)" : "rgba(0,0,0,0.2)";
286
+ const accentColor = accentColorOverride ?? colors.secondary;
286
287
  return /* @__PURE__ */ jsxs3(View3, { style: [styles3.container, { backgroundColor: bg }], children: [
287
288
  /* @__PURE__ */ jsxs3(View3, { style: [styles3.header, { paddingTop: 8 + insets.top, paddingLeft: 8 + insets.left }], children: [
288
289
  /* @__PURE__ */ jsx3(IconButton, { icon: "arrow-left", iconColor: fg, size: 24, onPress: onBack }),
289
- /* @__PURE__ */ jsx3(Text3, { variant: "displaySmall", style: [styles3.title, { color: fg }], children: title })
290
+ /* @__PURE__ */ jsx3(Text3, { variant: titleVariant, style: [styles3.title, { color: fg }], children: title })
290
291
  ] }),
291
292
  /* @__PURE__ */ jsxs3(ScrollView2, { style: styles3.scrollView, contentContainerStyle: [styles3.content, { paddingBottom: 32 + insets.bottom }], showsVerticalScrollIndicator: false, children: [
292
293
  children,
293
- showReset && /* @__PURE__ */ jsx3(Button2, { mode: "outlined", onPress: () => setConfirmResetVisible(true), textColor: colors.secondary, style: styles3.resetButton, children: resetLabel })
294
+ showReset && /* @__PURE__ */ jsx3(Button2, { mode: "contained", onPress: () => setConfirmResetVisible(true), buttonColor: colors.secondary, textColor: colors.onSecondary, style: styles3.resetButton, children: resetLabel })
294
295
  ] }),
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: [
296
- /* @__PURE__ */ jsx3(Icon3, { source: "alert-outline", size: 64, color: colors.secondary }),
297
- /* @__PURE__ */ jsx3(Text3, { variant: "headlineLarge", style: [styles3.overlayTitle, { color: colors.secondary }], children: resetConfirmTitle }),
297
+ /* @__PURE__ */ jsx3(Icon3, { source: "alert-outline", size: 64, color: accentColor }),
298
+ /* @__PURE__ */ jsx3(Text3, { variant: "headlineLarge", style: [styles3.overlayTitle, { color: accentColor }], children: resetConfirmTitle }),
298
299
  /* @__PURE__ */ jsx3(Text3, { variant: "bodyLarge", style: [styles3.overlayBody, { color: fg }], children: resetConfirmBody }),
299
300
  /* @__PURE__ */ jsx3(Button2, { mode: "contained", onPress: () => setConfirmResetVisible(false), style: styles3.overlayButton, children: "Cancel" }),
300
301
  /* @__PURE__ */ jsx3(
@@ -989,10 +990,10 @@ import { useAutoPaperTheme as useAutoPaperTheme4 } from "@rific/auto-paper";
989
990
  import { StyleSheet as StyleSheet12, View as View10 } from "react-native";
990
991
  import { Text as Text8 } from "react-native-paper";
991
992
  import { jsx as jsx13, jsxs as jsxs10 } from "react/jsx-runtime";
992
- function StatRow({ label, value }) {
993
+ function StatRow({ label, value, fg: fgOverride, fgMuted: fgMutedOverride }) {
993
994
  const { dark } = useAutoPaperTheme4();
994
- const fg = dark ? "#FFFFFF" : "#000000";
995
- const fgMuted = dark ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.5)";
995
+ const fg = fgOverride ?? (dark ? "#FFFFFF" : "#000000");
996
+ const fgMuted = fgMutedOverride ?? (dark ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.5)");
996
997
  return /* @__PURE__ */ jsxs10(View10, { style: styles11.row, children: [
997
998
  /* @__PURE__ */ jsx13(Text8, { variant: "bodyMedium", style: { color: fgMuted }, children: label }),
998
999
  /* @__PURE__ */ jsx13(Text8, { variant: "bodyMedium", style: [styles11.value, { color: fg }], children: value })
@@ -1014,17 +1015,22 @@ import { useAutoPaperTheme as useAutoPaperTheme5 } from "@rific/auto-paper";
1014
1015
  import { StyleSheet as StyleSheet13, View as View11 } from "react-native";
1015
1016
  import { Text as Text9 } from "react-native-paper";
1016
1017
  import { jsx as jsx14, jsxs as jsxs11 } from "react/jsx-runtime";
1017
- function StatSection({ label, children }) {
1018
+ function StatSection({ label, fg: fgOverride, sectionBg: sectionBgOverride, children }) {
1018
1019
  const { dark } = useAutoPaperTheme5();
1019
- const fgMuted = dark ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.5)";
1020
- const sectionBg = dark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.04)";
1020
+ const fg = fgOverride ?? (dark ? "#FFFFFF" : "#000000");
1021
+ const sectionBg = sectionBgOverride ?? (dark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.04)");
1021
1022
  return /* @__PURE__ */ jsxs11(View11, { style: [styles12.section, { backgroundColor: sectionBg }], children: [
1022
- /* @__PURE__ */ jsx14(Text9, { variant: "labelMedium", style: [styles12.label, { color: fgMuted }], children: label }),
1023
+ /* @__PURE__ */ jsx14(Text9, { variant: "labelMedium", style: [styles12.label, { color: fg }], children: label }),
1023
1024
  children
1024
1025
  ] });
1025
1026
  }
1026
1027
  var styles12 = StyleSheet13.create({
1028
+ // Bold and full-contrast (fg), not the original fontless/fgMuted treatment — that read at the
1029
+ // same visual weight as StatRow's own row labels below it (both used the identical muted color;
1030
+ // letterSpacing/variant alone didn't separate them enough to read as "this is the group header"
1031
+ // at a glance). Matches AchievementRow's own title styling (bold + fg) below it in the same list.
1027
1032
  label: {
1033
+ fontWeight: "bold",
1028
1034
  letterSpacing: 2
1029
1035
  },
1030
1036
  section: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tastic/hud",
3
- "version": "0.4.0",
3
+ "version": "0.5.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",
@@ -32,20 +32,28 @@ interface Props {
32
32
  // from a specific profile's own tab — optional since only an app with per-profile stat views has
33
33
  // this concept at all.
34
34
  deviceMarker?: boolean
35
+ // Independently optional overrides for this row's own text/background colors — each defaults to
36
+ // the same dark-mode-derived formula as StatRow/StatSection/BaseStatsScreen (see that component's
37
+ // own doc for why) when omitted, so every existing caller renders identically to before. A caller
38
+ // with its own app-wide chrome palette passes whichever of the three it needs.
39
+ fg?: string
40
+ fgMuted?: string
41
+ sectionBg?: string
35
42
  }
36
43
 
37
44
  // One row per achievement — a tier-colored (or locked-gray) badge, title/description, an optional
38
45
  // progress bar while locked, and either an unlocked check-mark+label or a lock icon. Extracted
39
46
  // verbatim from LightCycles' own achievements.tsx, where this exact row shape backed its entire
40
- // "ALL ACHIEVEMENTS" list. Derives fg/fgMuted/its own section background from useAutoPaperTheme(),
41
- // same as StatRow/StatSection/BaseStatsScreen see BaseStatsScreen's own doc for why. Has no
42
- // opinion on what an achievement actually IS (id, unlock predicate, catalog) that stays entirely
43
- // app-local; this is purely the presentational row.
44
- export function AchievementRow({ icon, title, description, badgeColor, unlockedLabel, checkColor, progress, deviceMarker }: Props) {
47
+ // "ALL ACHIEVEMENTS" list. fg/fgMuted/its own section background default to the same
48
+ // useAutoPaperTheme()-derived formula as StatRow/StatSection/BaseStatsScreen (see BaseStatsScreen's
49
+ // own doc for why), each independently overridable via the matching prop for a caller with its own
50
+ // chrome palette. Has no opinion on what an achievement actually IS (id, unlock predicate, catalog)
51
+ // that stays entirely app-local; this is purely the presentational row.
52
+ export function AchievementRow({ icon, title, description, badgeColor, unlockedLabel, checkColor, progress, deviceMarker, fg: fgOverride, fgMuted: fgMutedOverride, sectionBg: sectionBgOverride }: Props) {
45
53
  const { dark } = useAutoPaperTheme()
46
- const fg = dark ? '#FFFFFF' : '#000000'
47
- const fgMuted = dark ? 'rgba(255,255,255,0.5)' : 'rgba(0,0,0,0.5)'
48
- const sectionBg = dark ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.04)'
54
+ const fg = fgOverride ?? (dark ? '#FFFFFF' : '#000000')
55
+ const fgMuted = fgMutedOverride ?? (dark ? 'rgba(255,255,255,0.5)' : 'rgba(0,0,0,0.5)')
56
+ const sectionBg = sectionBgOverride ?? (dark ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.04)')
49
57
  const unlocked = unlockedLabel !== undefined
50
58
 
51
59
  return (
@@ -4,6 +4,11 @@ import { ReactNode, useState } from 'react'
4
4
  import { ScrollView, StyleSheet, View } from 'react-native'
5
5
  import { Icon, Portal, Text } from 'react-native-paper'
6
6
 
7
+ // react-native-paper's own MD3 Text variant names — mirrored locally rather than importing its
8
+ // internal VariantProp/MD3TypescaleKey (not exported from the package's public entry point) just
9
+ // to type one optional prop.
10
+ type MD3TextVariant = 'displayLarge' | 'displayMedium' | 'displaySmall' | 'headlineLarge' | 'headlineMedium' | 'headlineSmall' | 'titleLarge' | 'titleMedium' | 'titleSmall' | 'labelLarge' | 'labelMedium' | 'labelSmall' | 'bodyLarge' | 'bodyMedium' | 'bodySmall'
11
+
7
12
  interface Props {
8
13
  // Live physical-hold rotation (see @tastic/split-screen's getViewRotation) — this dialog renders
9
14
  // as a centered Portal modal, unaffected by whichever screen's own FakeLandscapeView wraps its
@@ -13,6 +18,13 @@ interface Props {
13
18
  // don't wrap it in a FakeLandscapeView at all and so never have a nonzero one to give.
14
19
  rotation?: number
15
20
  title?: string
21
+ // 'headlineSmall' (this screen's own default, below) — was originally 'displaySmall' (the full
22
+ // display tier's own smallest step), then 'headlineLarge', both still read as oversized for a
23
+ // header that's paired with a small 24px back-button glyph rather than standing alone the way a
24
+ // title screen's own wordmark does; headlineSmall sits close to that icon's own size instead of
25
+ // towering over it. Any react-native-paper MD3 Text variant, not just the headline tier, for a
26
+ // caller that wants something else entirely.
27
+ titleVariant?: MD3TextVariant
16
28
  onBack: () => void
17
29
  insets: { top: number; left: number; bottom: number; right: number }
18
30
  // Optional, and always passed as a pair — a caller with nothing resettable (or that deliberately
@@ -23,38 +35,60 @@ interface Props {
23
35
  resetLabel?: string
24
36
  resetConfirmTitle?: string
25
37
  resetConfirmBody?: string
38
+ // Independently optional overrides for this screen's own background/text colors — each defaults
39
+ // to the literal-black/white-by-appearance formula described below when omitted, so every
40
+ // existing caller (none of which pass these) renders identically to before. A caller with its own
41
+ // app-wide chrome palette (e.g. a tinted background rather than neutral black/white) passes
42
+ // whichever of the three it needs; cardBorder on the reset-confirm overlay stays fixed regardless,
43
+ // since its low alpha already reads fine against any cardBg.
44
+ fg?: string
45
+ bg?: string
46
+ cardBg?: string
47
+ // Overrides the confirm-overlay's alert icon/title, which otherwise default to auto-paper's
48
+ // colors.secondary. That default is tuned to read against auto-paper's OWN background role, not
49
+ // necessarily against a cardBg override above — a caller passing cardBg should generally pass a
50
+ // matching accentColor too, rather than risk colors.secondary landing close in hue/lightness to
51
+ // its own custom card background (this is what actually happened with a tinted felt cardBg in the
52
+ // app this was first overridden for). Every button on this screen (the reset trigger included)
53
+ // fills with colors.secondary/onSecondary as a self-contained, always-legible pair instead of
54
+ // relying on a text-only color read against the surrounding chrome, so none of them need this —
55
+ // only the bare icon/title do.
56
+ accentColor?: string
26
57
  children: ReactNode
27
58
  }
28
59
 
29
60
  // The stats/achievements screen shell every @tastic game shares — back button + title header,
30
61
  // scrollable content area, and an optional reset-everything action with its own confirm-before-
31
62
  // destroying overlay — extracted after the same header/scroll/reset-confirm shape was independently
32
- // built into LightCycles' own achievements.tsx. Derives its own colors from useAutoPaperTheme(),
33
- // same as this package's own BaseSettingsDialog, rather than taking fg/bg/cardBg/etc as props — a
34
- // stats screen is app-wide chrome, not a per-player surface, so there's no reason to make every
35
- // caller re-derive and thread through the identical dark-mode formula BaseSettingsDialog already
36
- // hardcodes internally. `children` is the seam for whatever isn't shared: this component has no
63
+ // built into LightCycles' own achievements.tsx. Colors default to the same literal-black/white-by-
64
+ // appearance formula BaseSettingsDialog hardcodes internally (same as this package's own
65
+ // BaseSettingsDialog) zero-config for every existing caller but fg/bg/cardBg are each
66
+ // independently overridable for a caller whose own app-wide chrome palette isn't that literal
67
+ // black/white convention. `children` is the seam for whatever isn't shared: this component has no
37
68
  // opinion on what a "stat" is or how achievements are catalogued — see StatRow/StatSection/
38
69
  // AchievementRow for the smaller presentational pieces built to go inside it.
39
- export function BaseStatsScreen({ rotation = 0, title = 'Achievements', onBack, insets, onReset, resetLabel = 'Reset All Stats', resetConfirmTitle = 'Reset Everything?', resetConfirmBody = 'This permanently erases all stats and achievements. This cannot be undone.', children }: Props) {
70
+ export 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 }: Props) {
40
71
  const { dark, colors } = useAutoPaperTheme()
41
72
  const [confirmResetVisible, setConfirmResetVisible] = useState(false)
42
73
  const showReset = !!onReset
43
74
 
44
- // High-contrast retro look: literal black/white by appearance, matching every other screen in
45
- // this ecosystem (title/loadout/settings all use this identical formula) rather than auto-
46
- // paper's own (slightly tinted) background role — same convention BaseSettingsDialog's own
47
- // fg/cardBg/cardBorder use internally.
48
- const fg = dark ? '#FFFFFF' : '#000000'
49
- const bg = dark ? '#000000' : '#FFFFFF'
50
- const cardBg = dark ? '#111111' : '#F2F2F2'
75
+ // High-contrast retro look by default: literal black/white by appearance, matching every other
76
+ // screen in this ecosystem (title/loadout/settings all use this identical formula) rather than
77
+ // auto-paper's own (slightly tinted) background role — same convention BaseSettingsDialog's own
78
+ // fg/cardBg/cardBorder use internally. Overridable per-instance via the props above for a caller
79
+ // whose own chrome isn't that literal black/white convention; cardBorder has no override (see the
80
+ // Props doc above for why).
81
+ const fg = fgOverride ?? (dark ? '#FFFFFF' : '#000000')
82
+ const bg = bgOverride ?? (dark ? '#000000' : '#FFFFFF')
83
+ const cardBg = cardBgOverride ?? (dark ? '#111111' : '#F2F2F2')
51
84
  const cardBorder = dark ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.2)'
85
+ const accentColor = accentColorOverride ?? colors.secondary
52
86
 
53
87
  return (
54
88
  <View style={[styles.container, { backgroundColor: bg }]}>
55
89
  <View style={[styles.header, { paddingTop: 8 + insets.top, paddingLeft: 8 + insets.left }]}>
56
90
  <IconButton icon='arrow-left' iconColor={fg} size={24} onPress={onBack} />
57
- <Text variant='displaySmall' style={[styles.title, { color: fg }]}>
91
+ <Text variant={titleVariant} style={[styles.title, { color: fg }]}>
58
92
  {title}
59
93
  </Text>
60
94
  </View>
@@ -63,7 +97,7 @@ export function BaseStatsScreen({ rotation = 0, title = 'Achievements', onBack,
63
97
  {children}
64
98
 
65
99
  {showReset && (
66
- <Button mode='outlined' onPress={() => setConfirmResetVisible(true)} textColor={colors.secondary} style={styles.resetButton}>
100
+ <Button mode='contained' onPress={() => setConfirmResetVisible(true)} buttonColor={colors.secondary} textColor={colors.onSecondary} style={styles.resetButton}>
67
101
  {resetLabel}
68
102
  </Button>
69
103
  )}
@@ -73,8 +107,8 @@ export function BaseStatsScreen({ rotation = 0, title = 'Achievements', onBack,
73
107
  <Portal>
74
108
  <View style={[styles.overlay, rotation % 360 !== 0 && { transform: [{ rotate: `${rotation}deg` }] }]}>
75
109
  <View style={[styles.overlayCard, { backgroundColor: cardBg, borderColor: cardBorder }]}>
76
- <Icon source='alert-outline' size={64} color={colors.secondary} />
77
- <Text variant='headlineLarge' style={[styles.overlayTitle, { color: colors.secondary }]}>
110
+ <Icon source='alert-outline' size={64} color={accentColor} />
111
+ <Text variant='headlineLarge' style={[styles.overlayTitle, { color: accentColor }]}>
78
112
  {resetConfirmTitle}
79
113
  </Text>
80
114
  <Text variant='bodyLarge' style={[styles.overlayBody, { color: fg }]}>
package/src/StatRow.tsx CHANGED
@@ -5,20 +5,25 @@ import { Text } from 'react-native-paper'
5
5
  interface Props {
6
6
  label: string
7
7
  value: string
8
+ // Independently optional overrides for this row's own text colors — default to the same
9
+ // useAutoPaperTheme()-derived formula as BaseStatsScreen (see that component's own doc for why)
10
+ // when omitted, so every existing caller renders identically to before.
11
+ fg?: string
12
+ fgMuted?: string
8
13
  }
9
14
 
10
15
  // A single labeled stat — label on the left (muted), value on the right (bold, full-contrast).
11
16
  // Extracted from LightCycles' own achievements.tsx, where this exact row shape backed every
12
- // section (Overall/Vs CPU/Two Player/Activity). Derives its own colors from useAutoPaperTheme(),
13
- // same as BaseStatsScreen see that component's own doc for why a stats-screen row doesn't take
14
- // fg/fgMuted as props. Deliberately just label+value text, not a color swatch or profile chip
15
- // an app needing a row with a leading visual builds its own on top of this same dark-mode formula
16
- // (see LightCycles' own ColorStatRow/ProfileRankingRow for an example), since what that visual
17
- // even is varies per game.
18
- export function StatRow({ label, value }: Props) {
17
+ // section (Overall/Vs CPU/Two Player/Activity). fg/fgMuted default to the same
18
+ // useAutoPaperTheme()-derived formula as BaseStatsScreen (see that component's own doc for why),
19
+ // each independently overridable via the matching prop for a caller with its own chrome palette.
20
+ // Deliberately just label+value text, not a color swatch or profile chip an app needing a row
21
+ // with a leading visual builds its own on top of this same dark-mode formula (see LightCycles' own
22
+ // ColorStatRow/ProfileRankingRow for an example), since what that visual even is varies per game.
23
+ export function StatRow({ label, value, fg: fgOverride, fgMuted: fgMutedOverride }: Props) {
19
24
  const { dark } = useAutoPaperTheme()
20
- const fg = dark ? '#FFFFFF' : '#000000'
21
- const fgMuted = dark ? 'rgba(255,255,255,0.5)' : 'rgba(0,0,0,0.5)'
25
+ const fg = fgOverride ?? (dark ? '#FFFFFF' : '#000000')
26
+ const fgMuted = fgMutedOverride ?? (dark ? 'rgba(255,255,255,0.5)' : 'rgba(0,0,0,0.5)')
22
27
 
23
28
  return (
24
29
  <View style={styles.row}>
@@ -5,22 +5,29 @@ import { Text } from 'react-native-paper'
5
5
 
6
6
  interface Props {
7
7
  label: string
8
+ // Independently optional overrides for this section's own label color/background — default to
9
+ // the same useAutoPaperTheme()-derived formula as StatRow/BaseStatsScreen (see BaseStatsScreen's
10
+ // own doc for why) when omitted, so every existing caller renders identically to before.
11
+ fg?: string
12
+ sectionBg?: string
8
13
  children: ReactNode
9
14
  }
10
15
 
11
16
  // A labeled, tinted-background grouping box — the "OVERALL"/"VS CPU"/"ACTIVITY"-style section
12
17
  // shape from LightCycles' own achievements.tsx, generalized to any content (typically a stack of
13
- // StatRow). Derives its own colors from useAutoPaperTheme(), same as StatRow/BaseStatsScreen — see
14
- // BaseStatsScreen's own doc for why. The label itself carries the section's own meaning (what it's
15
- // a section OF), so this component has no opinion on that beyond rendering the string it's given.
16
- export function StatSection({ label, children }: Props) {
18
+ // StatRow). fg/sectionBg default to the same useAutoPaperTheme()-derived formula as
19
+ // StatRow/BaseStatsScreen (see BaseStatsScreen's own doc for why), each independently overridable
20
+ // via the matching prop for a caller with its own chrome palette. The label itself carries the
21
+ // section's own meaning (what it's a section OF), so this component has no opinion on that beyond
22
+ // rendering the string it's given.
23
+ export function StatSection({ label, fg: fgOverride, sectionBg: sectionBgOverride, children }: Props) {
17
24
  const { dark } = useAutoPaperTheme()
18
- const fgMuted = dark ? 'rgba(255,255,255,0.5)' : 'rgba(0,0,0,0.5)'
19
- const sectionBg = dark ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.04)'
25
+ const fg = fgOverride ?? (dark ? '#FFFFFF' : '#000000')
26
+ const sectionBg = sectionBgOverride ?? (dark ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.04)')
20
27
 
21
28
  return (
22
29
  <View style={[styles.section, { backgroundColor: sectionBg }]}>
23
- <Text variant='labelMedium' style={[styles.label, { color: fgMuted }]}>
30
+ <Text variant='labelMedium' style={[styles.label, { color: fg }]}>
24
31
  {label}
25
32
  </Text>
26
33
  {children}
@@ -29,7 +36,12 @@ export function StatSection({ label, children }: Props) {
29
36
  }
30
37
 
31
38
  const styles = StyleSheet.create({
39
+ // Bold and full-contrast (fg), not the original fontless/fgMuted treatment — that read at the
40
+ // same visual weight as StatRow's own row labels below it (both used the identical muted color;
41
+ // letterSpacing/variant alone didn't separate them enough to read as "this is the group header"
42
+ // at a glance). Matches AchievementRow's own title styling (bold + fg) below it in the same list.
32
43
  label: {
44
+ fontWeight: 'bold',
33
45
  letterSpacing: 2
34
46
  },
35
47
  section: {