@tastic/profile 0.14.0 → 0.15.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 +59 -27
- package/dist/index.d.ts +59 -27
- package/dist/index.js +322 -223
- package/dist/index.mjs +279 -183
- package/package.json +8 -8
- package/src/OptionPicker.tsx +64 -0
- package/src/PickerMenu.tsx +481 -0
- package/src/ProfilePicker.tsx +54 -413
- package/src/ProfilesManager.tsx +7 -5
- package/src/index.ts +2 -1
- package/src/redux/seatColorsSlice.ts +79 -29
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { Profile } from '../types'
|
|
1
2
|
import { ActionCreator, createActionCreator } from './createActionCreator'
|
|
3
|
+
import { profilesActions } from './profilesSlice'
|
|
2
4
|
|
|
3
5
|
// redux-persist's own REHYDRATE action-type constant, inlined as a literal rather than taking a
|
|
4
6
|
// dependency on the package itself - this factory otherwise has no opinion on whether the
|
|
@@ -12,21 +14,32 @@ export interface SetLastGuestColorPayload<TSeat> {
|
|
|
12
14
|
seat: TSeat
|
|
13
15
|
}
|
|
14
16
|
|
|
15
|
-
|
|
17
|
+
// A color picked for a profile in this game: it holds while the profile keeps the color it had
|
|
18
|
+
// when the pick was made (see profileSeatColor).
|
|
19
|
+
export interface ProfileColorPick {
|
|
20
|
+
color: string
|
|
21
|
+
profileColor: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface SetProfileColorPayload {
|
|
25
|
+
// The profile the pick is for, with its own color as it is now.
|
|
26
|
+
profile: Pick<Profile, 'id' | 'color'>
|
|
27
|
+
// The pick. Null, or the profile's own color, drops it, so the seat follows the profile's color
|
|
28
|
+
// again.
|
|
16
29
|
color: string | null
|
|
17
|
-
seat: TSeat
|
|
18
30
|
}
|
|
19
31
|
|
|
20
32
|
export interface SeatColorsActions<TSeat> {
|
|
21
33
|
setLastCpuColor: ActionCreator<string>
|
|
22
34
|
setLastGuestColor: ActionCreator<SetLastGuestColorPayload<TSeat>>
|
|
23
|
-
|
|
35
|
+
setProfileColor: ActionCreator<SetProfileColorPayload>
|
|
24
36
|
}
|
|
25
37
|
|
|
26
38
|
export type SeatColorsState<TSeat extends string | number> = {
|
|
27
39
|
lastCpuColor: string
|
|
28
40
|
lastGuestColor: Record<TSeat, string>
|
|
29
|
-
|
|
41
|
+
// Each profile's pick in this game, by profile id. A profile not listed shows its own color.
|
|
42
|
+
profileColors: Record<string, ProfileColorPick>
|
|
30
43
|
}
|
|
31
44
|
|
|
32
45
|
export interface SeatColorsSlice<TSeat extends string | number> {
|
|
@@ -34,24 +47,33 @@ export interface SeatColorsSlice<TSeat extends string | number> {
|
|
|
34
47
|
reducer: (state: SeatColorsState<TSeat> | undefined, action: { type: string; payload?: unknown }) => SeatColorsState<TSeat>
|
|
35
48
|
}
|
|
36
49
|
|
|
50
|
+
function sameColor(a: string, b: string) {
|
|
51
|
+
return a.toLowerCase() === b.toLowerCase()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// The color a seat with `profile` selected shows: the profile's pick in this game, while the profile
|
|
55
|
+
// still has the color the pick was made against, else the profile's own color. So a new color for
|
|
56
|
+
// the profile itself (on any game's Profiles screen, this one or a sibling's through the shared
|
|
57
|
+
// roster) replaces an older pick, while a rename or a new tag keeps it.
|
|
58
|
+
export function profileSeatColor(profile: Pick<Profile, 'id' | 'color'>, profileColors: Record<string, ProfileColorPick>): string {
|
|
59
|
+
const pick = profileColors[profile.id]
|
|
60
|
+
return pick && sameColor(pick.profileColor, profile.color) ? pick.color : profile.color
|
|
61
|
+
}
|
|
62
|
+
|
|
37
63
|
// Which color a seat should show right now, remembered independently for three sources that must
|
|
38
|
-
// never fight over one value: a guest seat's own last pick, the CPU seat's own last pick, and
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
64
|
+
// never fight over one value: a guest seat's own last pick, the CPU seat's own last pick, and each
|
|
65
|
+
// profile's own pick in this game (`profileColors`, by profile id, read through profileSeatColor).
|
|
66
|
+
// A profile's pick follows the profile, not the seat: it is kept when the seat switches to another
|
|
67
|
+
// profile and back, when the profile moves to the other seat, and across relaunches, the same as
|
|
68
|
+
// Solitaire's per-profile table color (2026-10-16; until then it was a per-seat `profileOverride`
|
|
69
|
+
// that any profile change on the seat cleared). Picking the profile's own color drops the pick, and
|
|
70
|
+
// so do a new color for the profile itself and deleting it (this package's profilesActions, which
|
|
71
|
+
// this reducer also listens to). Five @tastic games (AirHockey, BoxHockey,
|
|
72
|
+
// LightCycles, Pong, Snake) use this factory; the first four each hand-rolled an identical
|
|
73
|
+
// dedicated seatColorsSlice.ts before it existed.
|
|
46
74
|
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
// than in a standalone slice of their own. There's no separate `seatColors` sub-state there for this
|
|
50
|
-
// factory's reducer to own without restructuring gameSlice's top-level shape to nest these three
|
|
51
|
-
// fields under their own key first - a real state-shape migration (and a redux-persist rehydration
|
|
52
|
-
// concern, see gameSlice's own REHYDRATE extraReducer), not a drop-in swap. This factory
|
|
53
|
-
// intentionally targets the standalone-slice shape the other four apps already have; Snake staying
|
|
54
|
-
// hand-rolled inside gameSlice is a deliberate scope call for this pass, not an oversight.
|
|
75
|
+
// A store saved before profileColors existed has a `profileOverride` key instead; REHYDRATE drops
|
|
76
|
+
// it, so every profile starts on its own color once.
|
|
55
77
|
//
|
|
56
78
|
// `namespace` prefixes every action type (e.g. 'seatColors/loadout/setLastGuestColor') so an app
|
|
57
79
|
// with more than one independent seat-colors concern never collides between them - same convention
|
|
@@ -71,26 +93,54 @@ export interface SeatColorsSlice<TSeat extends string | number> {
|
|
|
71
93
|
export function createSeatColorsSlice<TSeat extends string | number>(namespace: string, defaultState: SeatColorsState<TSeat>, mountKey = 'seatColors'): SeatColorsSlice<TSeat> {
|
|
72
94
|
const setLastCpuColor = createActionCreator<string>(`seatColors/${namespace}/setLastCpuColor`)
|
|
73
95
|
const setLastGuestColor = createActionCreator<SetLastGuestColorPayload<TSeat>>(`seatColors/${namespace}/setLastGuestColor`)
|
|
74
|
-
const
|
|
96
|
+
const setProfileColor = createActionCreator<SetProfileColorPayload>(`seatColors/${namespace}/setProfileColor`)
|
|
97
|
+
|
|
98
|
+
function withoutPick(state: SeatColorsState<TSeat>, id: string): SeatColorsState<TSeat> {
|
|
99
|
+
if (!state.profileColors[id]) return state
|
|
100
|
+
const profileColors = { ...state.profileColors }
|
|
101
|
+
delete profileColors[id]
|
|
102
|
+
return { ...state, profileColors }
|
|
103
|
+
}
|
|
75
104
|
|
|
76
105
|
function reducer(state: SeatColorsState<TSeat> = defaultState, action: { type: string; payload?: unknown }): SeatColorsState<TSeat> {
|
|
77
106
|
// redux-persist's own default stateReconciler (autoMergeLevel1) HARD-REPLACES this slice's
|
|
78
107
|
// entire persisted sub-state on rehydration rather than backfilling missing fields (confirmed
|
|
79
108
|
// against autoMergeLevel1's actual source: `newState[key] = inboundState[key]`, not a merge) -
|
|
80
|
-
// so a device with a blob persisted before
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
//
|
|
109
|
+
// so a device with a blob persisted before a field existed would otherwise rehydrate without
|
|
110
|
+
// it, and every consumer indexes into these fields with no fallback, throwing. Spreading
|
|
111
|
+
// `defaultState` first, then the incoming persisted value, backfills exactly that gap. A
|
|
112
|
+
// reducer that returns a new object here keeps autoMergeLevel1 from putting the saved blob back
|
|
113
|
+
// over it, which is what lets the old `profileOverride` key be dropped.
|
|
85
114
|
if (action.type === REHYDRATE) {
|
|
86
115
|
const persisted = (action.payload as Record<string, Partial<SeatColorsState<TSeat>>> | undefined)?.[mountKey]
|
|
87
|
-
|
|
116
|
+
const next: SeatColorsState<TSeat> & { profileOverride?: unknown } = { ...defaultState, ...state, ...persisted }
|
|
117
|
+
delete next.profileOverride
|
|
118
|
+
return next
|
|
88
119
|
}
|
|
89
120
|
if (setLastCpuColor.match(action)) return { ...state, lastCpuColor: action.payload }
|
|
90
121
|
if (setLastGuestColor.match(action)) return { ...state, lastGuestColor: { ...state.lastGuestColor, [action.payload.seat]: action.payload.color } as Record<TSeat, string> }
|
|
91
|
-
if (
|
|
122
|
+
if (setProfileColor.match(action)) {
|
|
123
|
+
const { profile, color } = action.payload
|
|
124
|
+
if (color === null || sameColor(color, profile.color)) return withoutPick(state, profile.id)
|
|
125
|
+
return { ...state, profileColors: { ...state.profileColors, [profile.id]: { color, profileColor: profile.color } } }
|
|
126
|
+
}
|
|
127
|
+
if (profilesActions.remove.match(action)) return withoutPick(state, action.payload)
|
|
128
|
+
// A new color for the profile itself replaces its pick: this game's Profiles screen (update), or
|
|
129
|
+
// a sibling's through the shared roster (setAll). A pick for a profile missing from the roster is
|
|
130
|
+
// left alone, like the selection's own pruneToRoster leaves stats alone.
|
|
131
|
+
if (profilesActions.update.match(action)) {
|
|
132
|
+
const { id, patch } = action.payload
|
|
133
|
+
const pick = state.profileColors[id]
|
|
134
|
+
return pick && patch.color !== undefined && !sameColor(patch.color, pick.profileColor) ? withoutPick(state, id) : state
|
|
135
|
+
}
|
|
136
|
+
if (profilesActions.setAll.match(action)) {
|
|
137
|
+
return action.payload.reduce((next, profile) => {
|
|
138
|
+
const pick = next.profileColors[profile.id]
|
|
139
|
+
return pick && !sameColor(profile.color, pick.profileColor) ? withoutPick(next, profile.id) : next
|
|
140
|
+
}, state)
|
|
141
|
+
}
|
|
92
142
|
return state
|
|
93
143
|
}
|
|
94
144
|
|
|
95
|
-
return { actions: { setLastCpuColor, setLastGuestColor,
|
|
145
|
+
return { actions: { setLastCpuColor, setLastGuestColor, setProfileColor }, reducer }
|
|
96
146
|
}
|