@tastic/hud 0.20.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -1
- package/dist/chrome/index.d.mts +15 -3
- package/dist/chrome/index.d.ts +15 -3
- package/dist/chrome/index.js +55 -42
- package/dist/chrome/index.mjs +7 -3
- package/dist/{chunk-DK3HOZC5.mjs → chunk-N7A7W72S.mjs} +38 -29
- package/dist/index.d.mts +6 -4
- package/dist/index.d.ts +6 -4
- package/dist/index.js +140 -117
- package/dist/index.mjs +66 -53
- package/package.json +10 -10
- package/src/AchievementCatalogSection.tsx +7 -6
- package/src/AchievementRow.tsx +13 -13
- package/src/ActivityStatSection.tsx +10 -5
- package/src/BaseStatsScreen.tsx +7 -5
- package/src/PlayerSetupPanel.tsx +77 -56
- package/src/StatRow.tsx +10 -11
- package/src/StatSection.tsx +14 -14
- package/src/chrome/index.ts +1 -0
- package/src/index.ts +2 -1
- package/src/wellSurface.ts +30 -0
package/dist/index.mjs
CHANGED
|
@@ -17,9 +17,11 @@ import {
|
|
|
17
17
|
StatRow,
|
|
18
18
|
StatSection,
|
|
19
19
|
UpdateDialog,
|
|
20
|
+
WELL_SHADOW,
|
|
20
21
|
getStaggeredWordDuration,
|
|
21
|
-
useCompactDialog
|
|
22
|
-
|
|
22
|
+
useCompactDialog,
|
|
23
|
+
useWellColors
|
|
24
|
+
} from "./chunk-N7A7W72S.mjs";
|
|
23
25
|
import {
|
|
24
26
|
COMPACT_DIALOG_HEIGHT,
|
|
25
27
|
DialogHeader,
|
|
@@ -683,7 +685,7 @@ function loadSkiaWeb() {
|
|
|
683
685
|
}
|
|
684
686
|
|
|
685
687
|
// src/PlayerSetupPanel.tsx
|
|
686
|
-
import { ProfilePicker } from "@tastic/profile";
|
|
688
|
+
import { getOptionPickerContentSize, getProfilePickerContentSize, OptionPicker, ProfilePicker } from "@tastic/profile";
|
|
687
689
|
import { StyleSheet as StyleSheet8, View as View8 } from "react-native";
|
|
688
690
|
import { Text as Text6 } from "react-native-paper";
|
|
689
691
|
|
|
@@ -721,26 +723,73 @@ function usePopoverHost() {
|
|
|
721
723
|
return { openId, toggle, close };
|
|
722
724
|
}
|
|
723
725
|
|
|
726
|
+
// src/useZoneClampedAlign.ts
|
|
727
|
+
import { useZoneBounds } from "@tastic/split-screen";
|
|
728
|
+
import { useEffect as useEffect2, useState as useState3 } from "react";
|
|
729
|
+
import { useWindowDimensions as useWindowDimensions3 } from "react-native";
|
|
730
|
+
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
731
|
+
var EDGE_MARGIN2 = 12;
|
|
732
|
+
function useZoneClampedAlign(open, contentWidth, contentHeight, rotation = 0) {
|
|
733
|
+
const auto = useAutoAlign(open, contentWidth, contentHeight, rotation);
|
|
734
|
+
const zone = useZoneBounds();
|
|
735
|
+
const { height: windowHeight } = useWindowDimensions3();
|
|
736
|
+
const insets = useSafeAreaInsets();
|
|
737
|
+
const shouldMeasure = open && auto.measured && zone !== null;
|
|
738
|
+
const [ownBounds, setOwnBounds] = useState3(null);
|
|
739
|
+
const [prevShouldMeasure, setPrevShouldMeasure] = useState3(shouldMeasure);
|
|
740
|
+
if (shouldMeasure !== prevShouldMeasure) {
|
|
741
|
+
setPrevShouldMeasure(shouldMeasure);
|
|
742
|
+
setOwnBounds(null);
|
|
743
|
+
}
|
|
744
|
+
useEffect2(() => {
|
|
745
|
+
if (!shouldMeasure) return;
|
|
746
|
+
auto.triggerRef.current?.measureInWindow((_x, y, _width, height) => {
|
|
747
|
+
const belowShared = zone.zoneSide === "belowShared";
|
|
748
|
+
const awayRoom = belowShared ? windowHeight - insets.bottom - (y + height) - EDGE_MARGIN2 : y - insets.top - EDGE_MARGIN2;
|
|
749
|
+
const towardRoom = belowShared ? y - zone.sharedEdgeY - EDGE_MARGIN2 : zone.sharedEdgeY - (y + height) - EDGE_MARGIN2;
|
|
750
|
+
const useToward = contentHeight > awayRoom && towardRoom > awayRoom;
|
|
751
|
+
let awaySide = belowShared ? "below" : "above";
|
|
752
|
+
let towardSide = belowShared ? "above" : "below";
|
|
753
|
+
if (zone.rotated) {
|
|
754
|
+
awaySide = awaySide === "above" ? "below" : "above";
|
|
755
|
+
towardSide = towardSide === "above" ? "below" : "above";
|
|
756
|
+
}
|
|
757
|
+
setOwnBounds({ maxHeight: Math.max(useToward ? towardRoom : awayRoom, 0), verticalAlign: useToward ? towardSide : awaySide });
|
|
758
|
+
});
|
|
759
|
+
}, [shouldMeasure, zone, auto.triggerRef, contentHeight, windowHeight, insets.top, insets.bottom]);
|
|
760
|
+
if (!zone) return auto;
|
|
761
|
+
if (!ownBounds) return { ...auto, measured: false };
|
|
762
|
+
return { ...auto, maxHeight: ownBounds.maxHeight, measured: auto.measured, verticalAlign: ownBounds.verticalAlign };
|
|
763
|
+
}
|
|
764
|
+
|
|
724
765
|
// src/PlayerSetupPanel.tsx
|
|
725
766
|
import { jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
726
767
|
function getColorPopoverId(idPrefix) {
|
|
727
768
|
return `${idPrefix}-color`;
|
|
728
769
|
}
|
|
770
|
+
function getProfilePopoverId(idPrefix) {
|
|
771
|
+
return `${idPrefix}-profile`;
|
|
772
|
+
}
|
|
729
773
|
function getCpuDifficultyPopoverId(idPrefix) {
|
|
730
774
|
return `${idPrefix}-difficulty`;
|
|
731
775
|
}
|
|
732
|
-
function PlayerSetupPanel({ idPrefix, host, label, labelFontFamily, color, onColorChange, swatches, takenColor, allowSwapTaken, colorAlignOverride, colorRotation, isHuman, secondPicker, secondPickerId, ready, onToggleReady, dark, showReadyButton = true, profiles, selectedProfileId, takenProfileId, guestLabel, onProfileSelect, onManageProfiles, cpuDifficulty, cpuDifficultyOptions, onCpuDifficultyChange,
|
|
776
|
+
function PlayerSetupPanel({ idPrefix, host, label, labelFontFamily, color, onColorChange, swatches, takenColor, allowSwapTaken, colorCaption, showColor = true, colorAlignOverride, colorRotation, isHuman, secondPicker, secondPickerId, ready, onToggleReady, dark, showReadyButton = true, profiles, selectedProfileId, takenProfileId, guestLabel, onProfileSelect, onManageProfiles, cpuDifficulty, cpuDifficultyOptions, onCpuDifficultyChange, humanIcon = "face-man", cpuIcon = "robot" }) {
|
|
733
777
|
const ownHost = usePopoverHost();
|
|
734
778
|
const popover = host ?? ownHost;
|
|
735
779
|
const ownPopoverOpen = popover.openId?.startsWith(`${idPrefix}-`) ?? false;
|
|
736
780
|
const pickerRowPopoverOpen = popover.openId === getColorPopoverId(idPrefix) || secondPickerId !== void 0 && popover.openId === secondPickerId;
|
|
737
781
|
const showProfilePicker = isHuman && profiles !== void 0 && !!onProfileSelect;
|
|
738
782
|
const showCpuDifficulty = !isHuman && cpuDifficulty !== void 0 && cpuDifficultyOptions !== void 0 && !!onCpuDifficultyChange;
|
|
783
|
+
const seatButton = showProfilePicker || showCpuDifficulty;
|
|
739
784
|
const selectedProfile = profiles?.find((p) => p.id === selectedProfileId) ?? null;
|
|
785
|
+
const colorSection = showColor ? { value: color, onChange: onColorChange, swatches, takenValue: takenColor, allowSwapTaken, caption: showProfilePicker && selectedProfile ? colorCaption : void 0 } : void 0;
|
|
786
|
+
const seatPickerId = showProfilePicker ? getProfilePopoverId(idPrefix) : getCpuDifficultyPopoverId(idPrefix);
|
|
787
|
+
const seatPickerSize = showProfilePicker ? getProfilePickerContentSize(profiles.length, !!onManageProfiles, colorSection) : getOptionPickerContentSize(cpuDifficultyOptions?.length ?? 0, colorSection);
|
|
788
|
+
const seatPickerAlign = useZoneClampedAlign(seatButton && popover.openId === seatPickerId, seatPickerSize.width, seatPickerSize.height);
|
|
740
789
|
return /* @__PURE__ */ jsxs7(View8, { style: [styles8.panel, ownPopoverOpen && styles8.panelOpen], children: [
|
|
741
|
-
showProfilePicker ? /* @__PURE__ */ jsx10(ProfilePicker, { idPrefix, host: popover, profiles, selectedId: selectedProfileId ?? null, takenId: takenProfileId, color, dark, guestLabel: guestLabel ?? "GUEST", onSelect: (profile) => onProfileSelect?.(profile), onManage: onManageProfiles }) : showCpuDifficulty ? /* @__PURE__ */ jsx10(
|
|
742
|
-
/* @__PURE__ */ jsxs7(View8, { style: [styles8.pickerRow, pickerRowPopoverOpen && styles8.pickerRowOpen], children: [
|
|
743
|
-
/* @__PURE__ */ jsx10(InlineColorPicker, { id: getColorPopoverId(idPrefix), host: popover, value: color, onChange: onColorChange, swatches, takenValue: takenColor, allowSwapTaken, alignOverride: colorAlignOverride, rotation: colorRotation, dark, tag: isHuman ? selectedProfile?.tag : void 0, icon: isHuman ? humanIcon : cpuIcon }),
|
|
790
|
+
showProfilePicker ? /* @__PURE__ */ jsx10(ProfilePicker, { idPrefix, host: popover, profiles, selectedId: selectedProfileId ?? null, takenId: takenProfileId, color, dark, guestLabel: guestLabel ?? "GUEST", onSelect: (profile) => onProfileSelect?.(profile), onManage: onManageProfiles, colorSection, alignOverride: seatPickerAlign }) : showCpuDifficulty ? /* @__PURE__ */ jsx10(OptionPicker, { id: getCpuDifficultyPopoverId(idPrefix), host: popover, options: cpuDifficultyOptions, value: cpuDifficulty, onChange: onCpuDifficultyChange, color, dark, icon: cpuIcon, colorSection, alignOverride: seatPickerAlign }) : label && /* @__PURE__ */ jsx10(Text6, { variant: "labelSmall", style: { color, fontFamily: labelFontFamily }, children: label }),
|
|
791
|
+
(!seatButton || secondPicker) && /* @__PURE__ */ jsxs7(View8, { style: [styles8.pickerRow, pickerRowPopoverOpen && styles8.pickerRowOpen], children: [
|
|
792
|
+
!seatButton && /* @__PURE__ */ jsx10(InlineColorPicker, { id: getColorPopoverId(idPrefix), host: popover, value: color, onChange: onColorChange, swatches, takenValue: takenColor, allowSwapTaken, alignOverride: colorAlignOverride, rotation: colorRotation, dark, tag: isHuman ? selectedProfile?.tag : void 0, icon: isHuman ? humanIcon : cpuIcon }),
|
|
744
793
|
secondPicker
|
|
745
794
|
] }),
|
|
746
795
|
isHuman && showReadyButton && onToggleReady && /* @__PURE__ */ jsx10(ReadyButton, { color, ready: ready ?? false, onToggleReady })
|
|
@@ -757,9 +806,9 @@ var styles8 = StyleSheet8.create({
|
|
|
757
806
|
panelOpen: {
|
|
758
807
|
zIndex: 100
|
|
759
808
|
},
|
|
760
|
-
// Same gap as `panel`'s own vertical rhythm, reused horizontally - the color
|
|
761
|
-
// present) secondPicker sit side by side within this row instead of stacked in the panel's
|
|
762
|
-
// column.
|
|
809
|
+
// Same gap as `panel`'s own vertical rhythm, reused horizontally - the fallback color swatch and
|
|
810
|
+
// (when present) secondPicker sit side by side within this row instead of stacked in the panel's
|
|
811
|
+
// own column.
|
|
763
812
|
pickerRow: {
|
|
764
813
|
alignItems: "center",
|
|
765
814
|
flexDirection: "row",
|
|
@@ -816,15 +865,15 @@ var styles9 = StyleSheet10.create({
|
|
|
816
865
|
});
|
|
817
866
|
|
|
818
867
|
// src/useQuitConfirmation.ts
|
|
819
|
-
import { useCallback as useCallback3, useEffect as
|
|
868
|
+
import { useCallback as useCallback3, useEffect as useEffect3, useRef as useRef2, useState as useState4 } from "react";
|
|
820
869
|
function useQuitConfirmation(hasProgress, onConfirmedBack) {
|
|
821
|
-
const [confirmVisible, setConfirmVisible] =
|
|
870
|
+
const [confirmVisible, setConfirmVisible] = useState4(false);
|
|
822
871
|
const hasProgressRef = useRef2(hasProgress);
|
|
823
|
-
|
|
872
|
+
useEffect3(() => {
|
|
824
873
|
hasProgressRef.current = hasProgress;
|
|
825
874
|
});
|
|
826
875
|
const onConfirmedBackRef = useRef2(onConfirmedBack);
|
|
827
|
-
|
|
876
|
+
useEffect3(() => {
|
|
828
877
|
onConfirmedBackRef.current = onConfirmedBack;
|
|
829
878
|
});
|
|
830
879
|
const requestBack = useCallback3(() => {
|
|
@@ -834,45 +883,6 @@ function useQuitConfirmation(hasProgress, onConfirmedBack) {
|
|
|
834
883
|
const cancelBack = useCallback3(() => setConfirmVisible(false), []);
|
|
835
884
|
return { confirmVisible, requestBack, cancelBack };
|
|
836
885
|
}
|
|
837
|
-
|
|
838
|
-
// src/useZoneClampedAlign.ts
|
|
839
|
-
import { useZoneBounds } from "@tastic/split-screen";
|
|
840
|
-
import { useEffect as useEffect3, useState as useState4 } from "react";
|
|
841
|
-
import { useWindowDimensions as useWindowDimensions3 } from "react-native";
|
|
842
|
-
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
843
|
-
var EDGE_MARGIN2 = 12;
|
|
844
|
-
function useZoneClampedAlign(open, contentWidth, contentHeight, rotation = 0) {
|
|
845
|
-
const auto = useAutoAlign(open, contentWidth, contentHeight, rotation);
|
|
846
|
-
const zone = useZoneBounds();
|
|
847
|
-
const { height: windowHeight } = useWindowDimensions3();
|
|
848
|
-
const insets = useSafeAreaInsets();
|
|
849
|
-
const shouldMeasure = open && auto.measured && zone !== null;
|
|
850
|
-
const [ownBounds, setOwnBounds] = useState4(null);
|
|
851
|
-
const [prevShouldMeasure, setPrevShouldMeasure] = useState4(shouldMeasure);
|
|
852
|
-
if (shouldMeasure !== prevShouldMeasure) {
|
|
853
|
-
setPrevShouldMeasure(shouldMeasure);
|
|
854
|
-
setOwnBounds(null);
|
|
855
|
-
}
|
|
856
|
-
useEffect3(() => {
|
|
857
|
-
if (!shouldMeasure) return;
|
|
858
|
-
auto.triggerRef.current?.measureInWindow((_x, y, _width, height) => {
|
|
859
|
-
const belowShared = zone.zoneSide === "belowShared";
|
|
860
|
-
const awayRoom = belowShared ? windowHeight - insets.bottom - (y + height) - EDGE_MARGIN2 : y - insets.top - EDGE_MARGIN2;
|
|
861
|
-
const towardRoom = belowShared ? y - zone.sharedEdgeY - EDGE_MARGIN2 : zone.sharedEdgeY - (y + height) - EDGE_MARGIN2;
|
|
862
|
-
const useToward = contentHeight > awayRoom && towardRoom > awayRoom;
|
|
863
|
-
let awaySide = belowShared ? "below" : "above";
|
|
864
|
-
let towardSide = belowShared ? "above" : "below";
|
|
865
|
-
if (zone.rotated) {
|
|
866
|
-
awaySide = awaySide === "above" ? "below" : "above";
|
|
867
|
-
towardSide = towardSide === "above" ? "below" : "above";
|
|
868
|
-
}
|
|
869
|
-
setOwnBounds({ maxHeight: Math.max(useToward ? towardRoom : awayRoom, 0), verticalAlign: useToward ? towardSide : awaySide });
|
|
870
|
-
});
|
|
871
|
-
}, [shouldMeasure, zone, auto.triggerRef, contentHeight, windowHeight, insets.top, insets.bottom]);
|
|
872
|
-
if (!zone) return auto;
|
|
873
|
-
if (!ownBounds) return { ...auto, measured: false };
|
|
874
|
-
return { ...auto, maxHeight: ownBounds.maxHeight, measured: auto.measured, verticalAlign: ownBounds.verticalAlign };
|
|
875
|
-
}
|
|
876
886
|
export {
|
|
877
887
|
AchievementCatalogSection,
|
|
878
888
|
AchievementRow,
|
|
@@ -907,10 +917,12 @@ export {
|
|
|
907
917
|
StatSection,
|
|
908
918
|
TriggerGaugeHost,
|
|
909
919
|
UpdateDialog,
|
|
920
|
+
WELL_SHADOW,
|
|
910
921
|
getColorPopoverId,
|
|
911
922
|
getCpuDifficultyPopoverId,
|
|
912
923
|
getInlineColorPickerContentSize,
|
|
913
924
|
getLabeledDropdownContentHeight,
|
|
925
|
+
getProfilePopoverId,
|
|
914
926
|
getStaggeredWordDuration,
|
|
915
927
|
isCompactDialogHeight,
|
|
916
928
|
loadSkiaWeb,
|
|
@@ -921,5 +933,6 @@ export {
|
|
|
921
933
|
useOverlayCardColors,
|
|
922
934
|
usePopoverHost,
|
|
923
935
|
useQuitConfirmation,
|
|
936
|
+
useWellColors,
|
|
924
937
|
useZoneClampedAlign
|
|
925
938
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tastic/hud",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.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",
|
|
@@ -85,16 +85,16 @@
|
|
|
85
85
|
"prettier": "@infinitetoken/eslint-config/prettier",
|
|
86
86
|
"devDependencies": {
|
|
87
87
|
"@infinitetoken/eslint-config": "^0.2.0",
|
|
88
|
-
"@infinitetoken/jest-config": "^0.
|
|
89
|
-
"@infinitetoken/tsconfig": "^0.
|
|
90
|
-
"@rific/auto-paper": "^0.13.
|
|
88
|
+
"@infinitetoken/jest-config": "^0.5.2",
|
|
89
|
+
"@infinitetoken/tsconfig": "^0.6.0",
|
|
90
|
+
"@rific/auto-paper": "^0.13.3",
|
|
91
91
|
"@rific/core": "^0.3.1",
|
|
92
92
|
"@rific/feedback-press": "^0.13.0",
|
|
93
93
|
"@rific/updater": "^0.7.1",
|
|
94
|
-
"@shopify/react-native-skia": "^2.
|
|
94
|
+
"@shopify/react-native-skia": "^2.14.0",
|
|
95
95
|
"@tastic/achievements": "^0.6.0",
|
|
96
96
|
"@tastic/core": "^0.14.0",
|
|
97
|
-
"@tastic/profile": "^0.
|
|
97
|
+
"@tastic/profile": "^0.15.0",
|
|
98
98
|
"@tastic/split-screen": "^0.5.2",
|
|
99
99
|
"@testing-library/dom": "^10.4.1",
|
|
100
100
|
"@testing-library/react": "^16.3.2",
|
|
@@ -108,10 +108,10 @@
|
|
|
108
108
|
"prettier": "^3.8.3",
|
|
109
109
|
"react": "^19.0.0",
|
|
110
110
|
"react-dom": "^19.0.0",
|
|
111
|
-
"react-native": "^0.
|
|
111
|
+
"react-native": "^0.88.0",
|
|
112
112
|
"react-native-paper": "^5.15.2",
|
|
113
|
-
"react-native-reanimated": "^4.
|
|
114
|
-
"react-native-safe-area-context": "^5.10.
|
|
113
|
+
"react-native-reanimated": "^4.7.1",
|
|
114
|
+
"react-native-safe-area-context": "^5.10.1",
|
|
115
115
|
"tsup": "^8.0.0",
|
|
116
116
|
"typescript": "^6.0.3"
|
|
117
117
|
},
|
|
@@ -122,7 +122,7 @@
|
|
|
122
122
|
"@shopify/react-native-skia": ">=2.6.0",
|
|
123
123
|
"@tastic/achievements": ">=0.2.0",
|
|
124
124
|
"@tastic/core": ">=0.12.0",
|
|
125
|
-
"@tastic/profile": ">=0.
|
|
125
|
+
"@tastic/profile": ">=0.15.0",
|
|
126
126
|
"@tastic/split-screen": ">=0.5.0",
|
|
127
127
|
"react": ">=19.0.0",
|
|
128
128
|
"react-native": ">=0.76.0",
|
|
@@ -15,12 +15,13 @@ import { MONO_FONT } from './fonts'
|
|
|
15
15
|
interface Props {
|
|
16
16
|
rows: AchievementCatalogRow[]
|
|
17
17
|
label?: string
|
|
18
|
-
// Independently optional overrides for
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
18
|
+
// Independently optional overrides for the heading's and rows' colors. fg and sectionBg go to each
|
|
19
|
+
// <AchievementRow> (whose own defaults are the well's, see wellSurface.ts). fgMuted goes to the heading and
|
|
20
|
+
// the rows; omitted, the heading (drawn on the screen, not on a well) keeps its own screen-derived default
|
|
21
|
+
// and the rows theirs.
|
|
22
22
|
fg?: string
|
|
23
23
|
fgMuted?: string
|
|
24
|
+
sectionBg?: string
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
// The "ALL ACHIEVEMENTS" heading + one <AchievementRow> per row - extracted verbatim from every
|
|
@@ -42,7 +43,7 @@ interface Props {
|
|
|
42
43
|
// about 1.4:1 and 1.8:1 against the light screen's white, so a bar or check in either barely shows;
|
|
43
44
|
// mixed, they're about 5:1 and 6:1 (bronze goes from about 3:1 to 9:1). The badge keeps the true tier
|
|
44
45
|
// color: its glyph is black, which reads on all three.
|
|
45
|
-
export function AchievementCatalogSection({ rows, label = 'ALL ACHIEVEMENTS', fg, fgMuted: fgMutedOverride }: Props) {
|
|
46
|
+
export function AchievementCatalogSection({ rows, label = 'ALL ACHIEVEMENTS', fg, fgMuted: fgMutedOverride, sectionBg }: Props) {
|
|
46
47
|
const { dark } = useAutoPaperTheme()
|
|
47
48
|
const fgMuted = fgMutedOverride ?? (dark ? 'rgba(255,255,255,0.5)' : 'rgba(0,0,0,0.5)')
|
|
48
49
|
const tierAccent = (tierColor: string) => (dark ? tierColor : getBlendedColor('#000000', tierColor, 0.5))
|
|
@@ -53,7 +54,7 @@ export function AchievementCatalogSection({ rows, label = 'ALL ACHIEVEMENTS', fg
|
|
|
53
54
|
{label}
|
|
54
55
|
</Text>
|
|
55
56
|
{rows.map((row) => (
|
|
56
|
-
<AchievementRow key={row.id} icon={row.icon} title={row.title} description={row.description} badgeColor={row.unlockedAt !== undefined ? row.tierColor : LOCKED_BADGE_COLOR} checkColor={tierAccent(row.tierColor)} unlockedLabel={row.unlockedLabel} progress={row.progress} deviceMarker={row.deviceMarker} fg={fg} fgMuted={fgMutedOverride} />
|
|
57
|
+
<AchievementRow key={row.id} icon={row.icon} title={row.title} description={row.description} badgeColor={row.unlockedAt !== undefined ? row.tierColor : LOCKED_BADGE_COLOR} checkColor={tierAccent(row.tierColor)} unlockedLabel={row.unlockedLabel} progress={row.progress} deviceMarker={row.deviceMarker} fg={fg} fgMuted={fgMutedOverride} sectionBg={sectionBg} />
|
|
57
58
|
))}
|
|
58
59
|
</Fragment>
|
|
59
60
|
)
|
package/src/AchievementRow.tsx
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { useAutoPaperTheme } from '@rific/auto-paper'
|
|
2
1
|
import { StyleSheet, View } from 'react-native'
|
|
3
2
|
import { Icon, ProgressBar, Text } from 'react-native-paper'
|
|
4
3
|
|
|
4
|
+
import { useWellColors, WELL_SHADOW } from './wellSurface'
|
|
5
|
+
|
|
5
6
|
// A neutral gray for a locked badge's own background - exported so a caller computing its own
|
|
6
7
|
// badge color (unlocked ? tierColor : LOCKED_BADGE_COLOR, matching LightCycles' own achievements.tsx)
|
|
7
8
|
// doesn't need to invent or duplicate this exact tone.
|
|
@@ -32,10 +33,9 @@ interface Props {
|
|
|
32
33
|
// from a specific profile's own tab - optional since only an app with per-profile stat views has
|
|
33
34
|
// this concept at all.
|
|
34
35
|
deviceMarker?: boolean
|
|
35
|
-
// Independently optional overrides for this row's own text/background colors - each defaults to
|
|
36
|
-
// the
|
|
37
|
-
//
|
|
38
|
-
// with its own app-wide chrome palette passes whichever of the three it needs.
|
|
36
|
+
// Independently optional overrides for this row's own text/background colors - each defaults to the well's
|
|
37
|
+
// (wellSurface.ts: the theme's surface and its onSurface/onSurfaceVariant text), like StatSection/StatRow. A
|
|
38
|
+
// caller with its own app-wide chrome palette passes whichever of the three it needs.
|
|
39
39
|
fg?: string
|
|
40
40
|
fgMuted?: string
|
|
41
41
|
sectionBg?: string
|
|
@@ -44,16 +44,15 @@ interface Props {
|
|
|
44
44
|
// One row per achievement - a tier-colored (or locked-gray) badge, title/description, an optional
|
|
45
45
|
// progress bar while locked, and either an unlocked check-mark+label or a lock icon. Extracted
|
|
46
46
|
// verbatim from LightCycles' own achievements.tsx, where this exact row shape backed its entire
|
|
47
|
-
// "ALL ACHIEVEMENTS" list.
|
|
48
|
-
//
|
|
49
|
-
// own
|
|
50
|
-
// chrome palette. Has no opinion on what an achievement actually IS (id, unlock predicate, catalog)
|
|
47
|
+
// "ALL ACHIEVEMENTS" list. Each row is its own well (wellSurface.ts: the theme's surface on MD3's level-1 shadow);
|
|
48
|
+
// fg/fgMuted/its own background default to the well's colors, each independently overridable via the matching
|
|
49
|
+
// prop for a caller with its own chrome palette. Has no opinion on what an achievement actually IS (id, unlock predicate, catalog)
|
|
51
50
|
// - that stays entirely app-local; this is purely the presentational row.
|
|
52
51
|
export function AchievementRow({ icon, title, description, badgeColor, unlockedLabel, checkColor, progress, deviceMarker, fg: fgOverride, fgMuted: fgMutedOverride, sectionBg: sectionBgOverride }: Props) {
|
|
53
|
-
const
|
|
54
|
-
const fg = fgOverride ??
|
|
55
|
-
const fgMuted = fgMutedOverride ??
|
|
56
|
-
const sectionBg = sectionBgOverride ??
|
|
52
|
+
const well = useWellColors()
|
|
53
|
+
const fg = fgOverride ?? well.fg
|
|
54
|
+
const fgMuted = fgMutedOverride ?? well.fgMuted
|
|
55
|
+
const sectionBg = sectionBgOverride ?? well.bg
|
|
57
56
|
const unlocked = unlockedLabel !== undefined
|
|
58
57
|
|
|
59
58
|
return (
|
|
@@ -116,6 +115,7 @@ const styles = StyleSheet.create({
|
|
|
116
115
|
row: {
|
|
117
116
|
alignItems: 'center',
|
|
118
117
|
borderRadius: 12,
|
|
118
|
+
boxShadow: WELL_SHADOW,
|
|
119
119
|
flexDirection: 'row',
|
|
120
120
|
gap: 12,
|
|
121
121
|
padding: 12
|
|
@@ -9,6 +9,11 @@ interface ActivityStats {
|
|
|
9
9
|
|
|
10
10
|
interface Props {
|
|
11
11
|
stats: ActivityStats
|
|
12
|
+
// Optional overrides for the section's and its rows' colors, passed straight through to StatSection and each
|
|
13
|
+
// StatRow (whose own defaults are the well's, see wellSurface.ts) - for a caller with its own chrome palette.
|
|
14
|
+
fg?: string
|
|
15
|
+
fgMuted?: string
|
|
16
|
+
sectionBg?: string
|
|
12
17
|
}
|
|
13
18
|
|
|
14
19
|
// The "ACTIVITY" StatSection (Days Played / Day Streak / Best Day Streak) - extracted verbatim
|
|
@@ -18,12 +23,12 @@ interface Props {
|
|
|
18
23
|
// currentDayStreak/bestDayStreak) rather than importing DayStreakState itself, so this component
|
|
19
24
|
// has no dependency on @tastic/achievements at all: any caller's own stats/statsView object that
|
|
20
25
|
// happens to carry these three fields already satisfies it structurally.
|
|
21
|
-
export function ActivityStatSection({ stats }: Props) {
|
|
26
|
+
export function ActivityStatSection({ stats, fg, fgMuted, sectionBg }: Props) {
|
|
22
27
|
return (
|
|
23
|
-
<StatSection label='ACTIVITY'>
|
|
24
|
-
<StatRow label='Days Played' value={String(stats.distinctDaysPlayed)} />
|
|
25
|
-
<StatRow label='Day Streak' value={String(stats.currentDayStreak)} />
|
|
26
|
-
<StatRow label='Best Day Streak' value={String(stats.bestDayStreak)} />
|
|
28
|
+
<StatSection label='ACTIVITY' fg={fg} sectionBg={sectionBg}>
|
|
29
|
+
<StatRow label='Days Played' value={String(stats.distinctDaysPlayed)} fg={fg} fgMuted={fgMuted} />
|
|
30
|
+
<StatRow label='Day Streak' value={String(stats.currentDayStreak)} fg={fg} fgMuted={fgMuted} />
|
|
31
|
+
<StatRow label='Best Day Streak' value={String(stats.bestDayStreak)} fg={fg} fgMuted={fgMuted} />
|
|
27
32
|
</StatSection>
|
|
28
33
|
)
|
|
29
34
|
}
|
package/src/BaseStatsScreen.tsx
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { useAutoPaperTheme } from '@rific/auto-paper'
|
|
1
|
+
import { FadingScrollView, useAutoPaperTheme } from '@rific/auto-paper'
|
|
2
2
|
import { Button, IconButton } from '@rific/feedback-press'
|
|
3
3
|
import { useRotation, type ViewRotation } from '@tastic/core'
|
|
4
4
|
import { ReactNode, useState } from 'react'
|
|
5
|
-
import { type Insets,
|
|
5
|
+
import { type Insets, StyleSheet, View } from 'react-native'
|
|
6
6
|
import { Text } from 'react-native-paper'
|
|
7
7
|
|
|
8
8
|
import { ConfirmDialog } from './ConfirmDialog'
|
|
@@ -101,7 +101,9 @@ export function BaseStatsScreen({ rotation: rotationOverride, title = 'Achieveme
|
|
|
101
101
|
</Text>
|
|
102
102
|
</View>
|
|
103
103
|
|
|
104
|
-
|
|
104
|
+
{/* Fades out at an edge while there is more past it, the same cue as the Settings sheet's rows and the
|
|
105
|
+
Profiles screen's (Jay, 2026-10-16: every scrolling screen alike). */}
|
|
106
|
+
<FadingScrollView style={styles.scrollView} contentContainerStyle={[styles.content, { paddingBottom: 32 + insets.bottom }]} showsVerticalScrollIndicator={false}>
|
|
105
107
|
{children}
|
|
106
108
|
|
|
107
109
|
{showReset && (
|
|
@@ -109,7 +111,7 @@ export function BaseStatsScreen({ rotation: rotationOverride, title = 'Achieveme
|
|
|
109
111
|
{resetLabel}
|
|
110
112
|
</Button>
|
|
111
113
|
)}
|
|
112
|
-
</
|
|
114
|
+
</FadingScrollView>
|
|
113
115
|
|
|
114
116
|
<ConfirmDialog
|
|
115
117
|
visible={confirmResetVisible}
|
|
@@ -147,7 +149,7 @@ const styles = StyleSheet.create({
|
|
|
147
149
|
resetButton: {
|
|
148
150
|
marginTop: 16
|
|
149
151
|
},
|
|
150
|
-
// Bounds the
|
|
152
|
+
// Bounds the scroller (FadingScrollView puts it on its outer box) to the space `container`'s flex:1 actually gives it - without this, a
|
|
151
153
|
// ScrollView with only a contentContainerStyle isn't reliably height-constrained on native (it
|
|
152
154
|
// can render at its full unclipped content height instead of the screen's), which leaves the
|
|
153
155
|
// header's back button touch target unreliable underneath it.
|