@tastic/profile 0.3.8 → 0.7.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 CHANGED
@@ -53,7 +53,17 @@ see its own doc comment for why that component is generic over your own `Profile
53
53
  - **`ProfilesManager`** — the roster screen itself: create/rename/recolor/retag/delete, editing
54
54
  inline in the row you tap, with a destructive-delete confirmation. No navigation of its own (no
55
55
  router, no back button) — a host app renders this as a routed screen's body and supplies its own
56
- persistence via `profiles`/`onCreate`/`onSave`/`onDelete`.
56
+ persistence via `profiles`/`onCreate`/`onSave`/`onDelete`. `onCreate`/`onSave` both take a
57
+ `ProfileEditPatch` (`{ name, color, tag }`) — exported from this package's own public API (it was
58
+ a private, unexported interface inside `ProfilesManager.tsx` until `ProfilesScreen` below needed
59
+ to reference it from its own props).
60
+ - **`ProfilesScreen`** — the routed-screen shell around `ProfilesManager`: the container `View`,
61
+ theme-derived background/foreground, a back button, and the commit-pending-edit-before-navigate
62
+ composition every app in the fleet used to hand-roll on its own around `ProfilesManager` (see
63
+ "Routed screens: `ProfilesScreen`" below). Same `ProfileEditPatch`-shaped
64
+ `profiles`/`defaultColor`/`onCreate`/`onSave`/`onDelete` as `ProfilesManager` itself, plus
65
+ `onBack`. Owns its own `ProfilesManagerHandle` ref internally — there's no `ref` prop here, since
66
+ nothing outside it needs one anymore.
57
67
  - **`loadSharedProfiles` / `saveSharedProfiles` / `isSharedProfileStoreAvailable`** — an *optional*
58
68
  cross-app roster, shared between multiple apps of yours via a native iOS App Group (same Apple
59
69
  Developer Team, same `"com.apple.security.application-groups"` entitlement value on every
@@ -67,7 +77,8 @@ see its own doc comment for why that component is generic over your own `Profile
67
77
  TypeScript type.
68
78
 
69
79
  ```ts
70
- // App-side sketch — see BoxHockey's or LightCycles' own useProfiles.tsx for a full example.
80
+ // Conceptual sketch of the pattern — see "Redux helpers for the shared roster" below for a
81
+ // ready-made version of it.
71
82
  const GROUP_ID = 'group.com.yourteam.yourgames'
72
83
  const base = isSharedProfileStoreAvailable ? await loadSharedProfiles(GROUP_ID) : localBaseFallback
73
84
  const profiles = base.map((p) => ({ ...p, controlScheme: localExtensions[p.id]?.controlScheme ?? DEFAULT_CONTROL_SCHEME }))
@@ -87,27 +98,163 @@ see its own doc comment for why that component is generic over your own `Profile
87
98
  { "expo": { "ios": { "entitlements": { "com.apple.security.application-groups": ["group.com.yourteam.yourgames"] } } } }
88
99
  ```
89
100
 
90
- ## Install (local dev via yalc)
101
+ ### Routed screens: `ProfilesScreen`
91
102
 
92
- Not published to the public npm registry yet.
103
+ `ProfilesScreen` is the routed-screen shell every app in the fleet was hand-rolling on its own
104
+ around `ProfilesManager`: a container `View`, theme-derived bg/fg, a `headerLeft` back button, and
105
+ the two-line `managerRef.current?.commitPendingEdit(); onBack()` composition every app's own
106
+ back-button handler used to write out by hand (see `ProfilesManager`'s own `commitPendingEdit` doc
107
+ above for why that flush matters on a router that doesn't genuinely unmount a popped screen). It
108
+ owns that ref itself — there's no `ref` prop on `ProfilesScreen`, since nothing outside it needs
109
+ one any more.
93
110
 
94
- ```bash
95
- cd react-native-game-profile
96
- npm run build
97
- yalc publish
111
+ Same `profiles`/`defaultColor`/`onCreate`/`onSave`/`onDelete` props as `ProfilesManager` — still
112
+ `ProfileEditPatch`-shaped, still deliberately not widened to your own richer `CreateProfileInput`;
113
+ compose any extra required fields (a key/control scheme, whatever's specific to your game) in your
114
+ own closure at the call site, exactly as you already would calling `ProfilesManager` directly —
115
+ plus `onBack`. `fg`/`fgMuted`/`bg`/`cardBg`/`titleVariant`/`colorPreview` are all independently
116
+ optional and forward straight through to the internal `ProfilesManager` (`bg` is new here —
117
+ `ProfilesManager` itself has no `bg`, since it doesn't own a container `View` of its own).
118
+
119
+ A whole routed screen, wired to your own store, is usually this small:
120
+
121
+ ```tsx
122
+ // app/profiles.tsx
123
+ import { defaultColors } from '@rific/auto-paper'
124
+ import { ProfilesScreen } from '@tastic/profile'
125
+ import { router } from 'expo-router'
126
+
127
+ import { useGameStats } from '@/hooks/useGameStats'
128
+ import { useProfiles } from '@/hooks/useProfiles'
129
+
130
+ export default function Profiles() {
131
+ const { profiles, createProfile, updateProfile, deleteProfile } = useProfiles()
132
+ // Only for wiring onDelete below — clears a deleted profile's own stats record so it doesn't
133
+ // stick around as an orphan once the profile itself is gone.
134
+ const { removeProfileStats } = useGameStats()
135
+
136
+ return (
137
+ <ProfilesScreen
138
+ profiles={profiles}
139
+ defaultColor={defaultColors[0].value}
140
+ onCreate={(patch) => createProfile(patch)}
141
+ onSave={(id, patch) => updateProfile(id, patch)}
142
+ onDelete={(id) => {
143
+ deleteProfile(id)
144
+ removeProfileStats(id)
145
+ }}
146
+ onBack={() => router.back()}
147
+ />
148
+ )
149
+ }
150
+ ```
151
+
152
+ `useProfiles`/`useGameStats` are your own app's hooks, not exports of this package — `profiles` is
153
+ whatever `Profile[]` (or your own extended shape) you already keep in Redux/context/state, and
154
+ `createProfile`/`updateProfile`/`deleteProfile` are however you already persist it. `onBack` can be
155
+ anything with no arguments — `router.back()`, a `safeBack()`-style helper that no-ops with no back
156
+ stack behind it, `navigation.goBack()` — `ProfilesScreen` only ever calls it after flushing any
157
+ in-progress inline edit first.
158
+
159
+ ### Redux helpers for the shared roster
160
+
161
+ The cross-app-roster sketch above is a pattern every `@tastic` game was hand-rolling on its own
162
+ (a roster reducer, a per-profile extension table, the initial-load decision, a foreground-resync
163
+ listener, a per-seat selection reducer). These five pieces are that logic extracted into reusable,
164
+ Redux-shaped building blocks — plain TypeScript, no dependency on `redux`/`react-redux` themselves,
165
+ so a host app plugs the reducer(s) into whatever store it already has:
166
+
167
+ - **`profilesActions` / `profilesReducer` / `createProfileRecord`** — the base roster as a reducer:
168
+ `add`/`update`/`remove`/`setAll` actions, plus `createProfileRecord(input)` to mint a complete
169
+ `Profile` (id + timestamps) ready to dispatch via `profilesActions.add`.
170
+ - **`createProfileExtensionSlice<TExtension>(namespace)`** — a factory for a host app's own
171
+ per-profile extension fields (a control scheme, a key scheme, ...): returns a
172
+ `{ actions: { set, remove }, reducer }` pair over a plain `Record<profileId, TExtension>`,
173
+ namespaced so more than one extension slice in the same app never collides. Not auto-cleared when
174
+ `profilesReducer` removes a profile — an app already dispatches its own cleanup action(s) on
175
+ delete, so this stays a self-contained reducer rather than new cross-slice wiring.
176
+ - **`resolveInitialProfiles(groupId, localFallback)`** — the one-time initial-load decision: shared
177
+ store unavailable → `localFallback` verbatim; shared store empty → seed it from `localFallback`;
178
+ otherwise the shared roster always wins over a possibly-stale local snapshot.
179
+ - **`useSharedProfilesSync({ groupId, onRemoteChange })`** — keeps a Redux-backed roster in sync for
180
+ the rest of the app's lifetime: call the returned `syncToShared(next)` right after a local roster
181
+ change to mirror it out, and `onRemoteChange` fires with a freshly-loaded roster whenever the app
182
+ returns to the foreground (skipped while a `syncToShared` write is still in flight, so a remote
183
+ refresh can't clobber a write that hasn't landed yet).
184
+ - **`createProfileSelectionSlice<TSeat>(namespace, defaultState)`** — a factory for the *other*
185
+ per-seat piece of state: which profile id each seat currently has selected (parallel to
186
+ `createProfileExtensionSlice` above, but keyed by seat instead of by profile id). Returns a
187
+ `{ actions: { select, clearProfile }, reducer }` pair over a plain `Record<TSeat, string | null>`.
188
+ Unlike `createProfileExtensionSlice`, the seat set can't be derived from `TSeat` alone (types
189
+ don't exist at runtime), so callers pass their own `defaultState` — the same literal object (e.g.
190
+ `{ 1: null, 2: null }`) every app already declares today. `select({ seat, profileId })` sets one
191
+ seat's selection directly; `clearProfile(profileId)` walks every seat and nulls out any that
192
+ currently points at that id — call it when a profile is deleted so no seat is left referencing a
193
+ ghost id. Always local, like the guest/CPU/override colors below — never part of the shared
194
+ App Group roster, since who's selected on *this* device is naturally per-app, per-device state.
98
195
 
99
- cd ../your-game
100
- yalc add @tastic/profile
101
- npm install
196
+ ## Per-seat color persistence
197
+
198
+ A loadout/lobby screen's color for a given seat is never just "the selected profile's saved
199
+ color" — a seat can be a guest (no profile selected), a CPU opponent, or a profile the player
200
+ manually recolored for a clash-swap without touching the profile itself. Every `@tastic` game
201
+ (AirHockey, BoxHockey, Pong, LightCycles, Snake) ends up needing the same three cooperating pieces
202
+ of state to cover all three cases. This is the established pattern every game should follow — it
203
+ was previously undocumented at the package level, which is exactly why two of the five apps
204
+ (AirHockey, Snake) didn't pick up the third piece below until after they'd already shipped without
205
+ it.
206
+
207
+ - **`lastGuestColor`** — a `Record<Seat, string>`: what a seat's color was the last time it was a
208
+ guest (no profile selected). Written only when a guest seat's color changes; a profile-selected
209
+ seat never reads or writes it.
210
+ - **`lastCpuColor`** — the same idea for the CPU slot, as a single remembered `string` rather than a
211
+ per-seat record (a CPU opponent only ever occupies one fixed seat in every shipped game so far).
212
+ - **`profileOverride`** — a `Record<Seat, string | null>`: a manual recolor or clash-swap landing on
213
+ a seat that currently has a profile selected. `null` means "no override — track the profile's own
214
+ saved color live." Cleared only when that seat's own profile selection genuinely changes, never by
215
+ merely leaving and returning to the screen.
216
+
217
+ None of these three are exports of this package today. `lastGuestColor`/`lastCpuColor` predate it
218
+ entirely, and `profileOverride` is still hand-rolled per app — typically alongside
219
+ `createProfileSelectionSlice`'s own selection state, in the same seat-scoped slice (see e.g.
220
+ BoxHockey's `src/redux/seatColorsSlice.ts`) or folded into the app's own `gameSlice.ts` when there's
221
+ no dedicated slice to put it in (see Snake's `src/redux/gameSlice.ts`). This section exists so a new
222
+ app's author reads the convention here first, rather than hand-rolling something that quietly can't
223
+ interop with the rest of the fleet.
224
+
225
+ The resolution order at a loadout/lobby screen, for one seat:
226
+
227
+ ```ts
228
+ const color = profile
229
+ ? (profileOverride[seat] ?? profile.color) // profile selected: override wins, else the profile's own color
230
+ : isCpu
231
+ ? lastCpuColor // CPU slot: its own remembered color
232
+ : lastGuestColor[seat] // guest seat: that seat's own remembered color
102
233
  ```
103
234
 
104
- Re-run `npm run build && yalc push` from this package after any change to propagate it to every
105
- linked consumer at once.
235
+ `lastGuestColor`/`lastCpuColor` are only ever consulted when no profile is selected for that seat at
236
+ all — a profile-selected seat's guest/CPU memory is neither read nor overwritten while it's active,
237
+ so switching a seat back to "guest" later still recalls whatever guest color it had before a profile
238
+ was ever selected there.
239
+
240
+ ## Install
241
+
242
+ ```bash
243
+ npm install @tastic/profile
244
+ ```
245
+
246
+ Published to the public npm registry via tag-based CI (OIDC trusted publishing — see
247
+ `npm run release:patch`/`:minor`/`:major`).
106
248
 
107
249
  ## Peer dependencies
108
250
 
109
251
  `react`, `react-native`, `react-native-paper`, `react-native-safe-area-context`, `@rific/auto-paper`
110
252
  (`getContrastColor`, `useAutoPaperTheme`), `@rific/feedback-press` (`TouchableRipple`),
111
253
  `@rific/focus-chain` (`useFocusChain`), `@rific/toaster` (`useToast`), `@tastic/hud`
112
- (`InlineColorPicker`, `PopoverBody`, `PopoverHost`, `useAutoAlign`, `usePopoverHost`) — none of these
113
- are bundled, so use whatever versions your app already has.
254
+ (`InlineColorPicker`, `PopoverBody`, `PopoverHost`, `useAutoAlign`, `usePopoverHost`), `@tastic/core`
255
+ (a peer of `@tastic/hud` itself) — none of these are bundled, so use whatever versions your app
256
+ already has.
257
+
258
+ `expo-modules-core` is an *optional* peer — only resolved by `loadSharedProfiles`/`saveSharedProfiles`
259
+ for the native App Group bridge described above; safe to omit entirely on an Android/web-only
260
+ consumer.
package/dist/index.d.mts CHANGED
@@ -35,7 +35,7 @@ interface ProfilePickerProps<P extends Profile> {
35
35
  }
36
36
  declare function ProfilePicker<P extends Profile>({ idPrefix, host, profiles, selectedId, takenId, color, dark, align: alignOverride, guestLabel, nullLabel, nullIcon, onSelect, onManage }: ProfilePickerProps<P>): react.JSX.Element;
37
37
 
38
- type MD3TextVariant = 'displayLarge' | 'displayMedium' | 'displaySmall' | 'headlineLarge' | 'headlineMedium' | 'headlineSmall' | 'titleLarge' | 'titleMedium' | 'titleSmall' | 'labelLarge' | 'labelMedium' | 'labelSmall' | 'bodyLarge' | 'bodyMedium' | 'bodySmall';
38
+ type MD3TextVariant$1 = 'displayLarge' | 'displayMedium' | 'displaySmall' | 'headlineLarge' | 'headlineMedium' | 'headlineSmall' | 'titleLarge' | 'titleMedium' | 'titleSmall' | 'labelLarge' | 'labelMedium' | 'labelSmall' | 'bodyLarge' | 'bodyMedium' | 'bodySmall';
39
39
  interface ProfileEditPatch {
40
40
  name: string;
41
41
  color: string;
@@ -55,18 +55,139 @@ interface ProfilesManagerProps {
55
55
  fg?: string;
56
56
  fgMuted?: string;
57
57
  cardBg?: string;
58
- titleVariant?: MD3TextVariant;
58
+ titleVariant?: MD3TextVariant$1;
59
59
  colorPreview?: (hex: string) => string;
60
60
  }
61
61
  declare function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, headerLeft, ref, fg: fgOverride, fgMuted: fgMutedOverride, cardBg: cardBgOverride, titleVariant, colorPreview }: ProfilesManagerProps): react.JSX.Element;
62
62
 
63
+ type MD3TextVariant = 'displayLarge' | 'displayMedium' | 'displaySmall' | 'headlineLarge' | 'headlineMedium' | 'headlineSmall' | 'titleLarge' | 'titleMedium' | 'titleSmall' | 'labelLarge' | 'labelMedium' | 'labelSmall' | 'bodyLarge' | 'bodyMedium' | 'bodySmall';
64
+ interface ProfilesScreenProps {
65
+ profiles: Profile[];
66
+ defaultColor: string;
67
+ onCreate: (patch: ProfileEditPatch) => void;
68
+ onSave: (id: string, patch: ProfileEditPatch) => void;
69
+ onDelete: (id: string) => void;
70
+ onBack: () => void;
71
+ fg?: string;
72
+ fgMuted?: string;
73
+ bg?: string;
74
+ cardBg?: string;
75
+ titleVariant?: MD3TextVariant;
76
+ colorPreview?: (hex: string) => string;
77
+ }
78
+ declare function ProfilesScreen({ profiles, defaultColor, onCreate, onSave, onDelete, onBack, fg: fgOverride, fgMuted, bg: bgOverride, cardBg, titleVariant, colorPreview }: ProfilesScreenProps): react.JSX.Element;
79
+
63
80
  declare const MAX_PROFILE_NAME_LENGTH = 20;
64
81
  declare const MAX_TAG_LENGTH = 3;
65
82
  declare function isValidTag(value: unknown): value is string;
66
83
  declare function isValidProfile(value: unknown): value is Profile;
67
84
 
85
+ interface ReduxAction<P> {
86
+ payload: P;
87
+ type: string;
88
+ }
89
+ type ActionCreator<P> = ((payload: P) => ReduxAction<P>) & {
90
+ type: string;
91
+ match: (action: {
92
+ type: string;
93
+ }) => action is ReduxAction<P>;
94
+ };
95
+
96
+ interface SetProfileExtensionPayload<TExtension> {
97
+ extension: TExtension;
98
+ id: string;
99
+ }
100
+ interface ProfileExtensionActions<TExtension> {
101
+ remove: ActionCreator<string>;
102
+ set: ActionCreator<SetProfileExtensionPayload<TExtension>>;
103
+ }
104
+ type ProfileExtensionState<TExtension> = Record<string, TExtension>;
105
+ interface ProfileExtensionSlice<TExtension> {
106
+ actions: ProfileExtensionActions<TExtension>;
107
+ reducer: (state: ProfileExtensionState<TExtension> | undefined, action: {
108
+ type: string;
109
+ }) => ProfileExtensionState<TExtension>;
110
+ }
111
+ declare function createProfileExtensionSlice<TExtension>(namespace: string): ProfileExtensionSlice<TExtension>;
112
+
113
+ interface SelectProfilePayload<TSeat> {
114
+ profileId: string | null;
115
+ seat: TSeat;
116
+ }
117
+ interface ProfileSelectionActions<TSeat> {
118
+ clearProfile: ActionCreator<string>;
119
+ select: ActionCreator<SelectProfilePayload<TSeat>>;
120
+ }
121
+ type ProfileSelectionState<TSeat extends string | number> = Record<TSeat, string | null>;
122
+ interface ProfileSelectionSlice<TSeat extends string | number> {
123
+ actions: ProfileSelectionActions<TSeat>;
124
+ reducer: (state: ProfileSelectionState<TSeat> | undefined, action: {
125
+ type: string;
126
+ payload?: unknown;
127
+ }) => ProfileSelectionState<TSeat>;
128
+ }
129
+ declare function createProfileSelectionSlice<TSeat extends string | number>(namespace: string, defaultState: ProfileSelectionState<TSeat>, mountKey?: string): ProfileSelectionSlice<TSeat>;
130
+
131
+ interface UpdateProfilePayload {
132
+ id: string;
133
+ patch: Partial<Pick<Profile, 'name' | 'color' | 'tag'>>;
134
+ }
135
+ declare const profilesActions: {
136
+ add: ActionCreator<Profile>;
137
+ remove: ActionCreator<string>;
138
+ setAll: ActionCreator<Profile[]>;
139
+ update: ActionCreator<UpdateProfilePayload>;
140
+ };
141
+ declare function profilesReducer(state: Profile[] | undefined, action: {
142
+ type: string;
143
+ }): Profile[];
144
+ interface CreateProfileInput {
145
+ name: string;
146
+ color: string;
147
+ tag: string;
148
+ }
149
+ declare function createProfileRecord(input: CreateProfileInput): Profile;
150
+
151
+ declare function resolveInitialProfiles(groupId: string, localFallback: Profile[]): Promise<Profile[]>;
152
+
153
+ interface SetLastGuestColorPayload<TSeat> {
154
+ color: string;
155
+ seat: TSeat;
156
+ }
157
+ interface SetProfileOverridePayload<TSeat> {
158
+ color: string | null;
159
+ seat: TSeat;
160
+ }
161
+ interface SeatColorsActions<TSeat> {
162
+ setLastCpuColor: ActionCreator<string>;
163
+ setLastGuestColor: ActionCreator<SetLastGuestColorPayload<TSeat>>;
164
+ setProfileOverride: ActionCreator<SetProfileOverridePayload<TSeat>>;
165
+ }
166
+ type SeatColorsState<TSeat extends string | number> = {
167
+ lastCpuColor: string;
168
+ lastGuestColor: Record<TSeat, string>;
169
+ profileOverride: Record<TSeat, string | null>;
170
+ };
171
+ interface SeatColorsSlice<TSeat extends string | number> {
172
+ actions: SeatColorsActions<TSeat>;
173
+ reducer: (state: SeatColorsState<TSeat> | undefined, action: {
174
+ type: string;
175
+ payload?: unknown;
176
+ }) => SeatColorsState<TSeat>;
177
+ }
178
+ declare function createSeatColorsSlice<TSeat extends string | number>(namespace: string, defaultState: SeatColorsState<TSeat>, mountKey?: string): SeatColorsSlice<TSeat>;
179
+
180
+ interface UseSharedProfilesSyncOptions {
181
+ groupId: string;
182
+ onRemoteChange: (profiles: Profile[]) => void;
183
+ }
184
+ interface UseSharedProfilesSyncResult {
185
+ syncToShared: (next: Profile[]) => void;
186
+ }
187
+ declare function useSharedProfilesSync({ groupId, onRemoteChange }: UseSharedProfilesSyncOptions): UseSharedProfilesSyncResult;
188
+
68
189
  declare const isSharedProfileStoreAvailable: boolean;
69
190
  declare function loadSharedProfiles(groupId: string): Promise<Profile[]>;
70
191
  declare function saveSharedProfiles(groupId: string, profiles: Profile[]): Promise<void>;
71
192
 
72
- export { MAX_PROFILE_NAME_LENGTH, MAX_TAG_LENGTH, type Profile, ProfileChip, ProfilePicker, ProfilesManager, type ProfilesManagerHandle, type ProfilesManagerProps, isSharedProfileStoreAvailable, isValidProfile, isValidTag, loadSharedProfiles, saveSharedProfiles };
193
+ export { type CreateProfileInput, MAX_PROFILE_NAME_LENGTH, MAX_TAG_LENGTH, type Profile, ProfileChip, type ProfileEditPatch, type ProfileExtensionActions, type ProfileExtensionSlice, type ProfileExtensionState, ProfilePicker, type ProfileSelectionActions, type ProfileSelectionSlice, type ProfileSelectionState, ProfilesManager, type ProfilesManagerHandle, type ProfilesManagerProps, ProfilesScreen, type ProfilesScreenProps, type SeatColorsActions, type SeatColorsSlice, type SeatColorsState, type SelectProfilePayload, type SetLastGuestColorPayload, type SetProfileExtensionPayload, type SetProfileOverridePayload, type UpdateProfilePayload, type UseSharedProfilesSyncOptions, type UseSharedProfilesSyncResult, createProfileExtensionSlice, createProfileRecord, createProfileSelectionSlice, createSeatColorsSlice, isSharedProfileStoreAvailable, isValidProfile, isValidTag, loadSharedProfiles, profilesActions, profilesReducer, resolveInitialProfiles, saveSharedProfiles, useSharedProfilesSync };
package/dist/index.d.ts CHANGED
@@ -35,7 +35,7 @@ interface ProfilePickerProps<P extends Profile> {
35
35
  }
36
36
  declare function ProfilePicker<P extends Profile>({ idPrefix, host, profiles, selectedId, takenId, color, dark, align: alignOverride, guestLabel, nullLabel, nullIcon, onSelect, onManage }: ProfilePickerProps<P>): react.JSX.Element;
37
37
 
38
- type MD3TextVariant = 'displayLarge' | 'displayMedium' | 'displaySmall' | 'headlineLarge' | 'headlineMedium' | 'headlineSmall' | 'titleLarge' | 'titleMedium' | 'titleSmall' | 'labelLarge' | 'labelMedium' | 'labelSmall' | 'bodyLarge' | 'bodyMedium' | 'bodySmall';
38
+ type MD3TextVariant$1 = 'displayLarge' | 'displayMedium' | 'displaySmall' | 'headlineLarge' | 'headlineMedium' | 'headlineSmall' | 'titleLarge' | 'titleMedium' | 'titleSmall' | 'labelLarge' | 'labelMedium' | 'labelSmall' | 'bodyLarge' | 'bodyMedium' | 'bodySmall';
39
39
  interface ProfileEditPatch {
40
40
  name: string;
41
41
  color: string;
@@ -55,18 +55,139 @@ interface ProfilesManagerProps {
55
55
  fg?: string;
56
56
  fgMuted?: string;
57
57
  cardBg?: string;
58
- titleVariant?: MD3TextVariant;
58
+ titleVariant?: MD3TextVariant$1;
59
59
  colorPreview?: (hex: string) => string;
60
60
  }
61
61
  declare function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, headerLeft, ref, fg: fgOverride, fgMuted: fgMutedOverride, cardBg: cardBgOverride, titleVariant, colorPreview }: ProfilesManagerProps): react.JSX.Element;
62
62
 
63
+ type MD3TextVariant = 'displayLarge' | 'displayMedium' | 'displaySmall' | 'headlineLarge' | 'headlineMedium' | 'headlineSmall' | 'titleLarge' | 'titleMedium' | 'titleSmall' | 'labelLarge' | 'labelMedium' | 'labelSmall' | 'bodyLarge' | 'bodyMedium' | 'bodySmall';
64
+ interface ProfilesScreenProps {
65
+ profiles: Profile[];
66
+ defaultColor: string;
67
+ onCreate: (patch: ProfileEditPatch) => void;
68
+ onSave: (id: string, patch: ProfileEditPatch) => void;
69
+ onDelete: (id: string) => void;
70
+ onBack: () => void;
71
+ fg?: string;
72
+ fgMuted?: string;
73
+ bg?: string;
74
+ cardBg?: string;
75
+ titleVariant?: MD3TextVariant;
76
+ colorPreview?: (hex: string) => string;
77
+ }
78
+ declare function ProfilesScreen({ profiles, defaultColor, onCreate, onSave, onDelete, onBack, fg: fgOverride, fgMuted, bg: bgOverride, cardBg, titleVariant, colorPreview }: ProfilesScreenProps): react.JSX.Element;
79
+
63
80
  declare const MAX_PROFILE_NAME_LENGTH = 20;
64
81
  declare const MAX_TAG_LENGTH = 3;
65
82
  declare function isValidTag(value: unknown): value is string;
66
83
  declare function isValidProfile(value: unknown): value is Profile;
67
84
 
85
+ interface ReduxAction<P> {
86
+ payload: P;
87
+ type: string;
88
+ }
89
+ type ActionCreator<P> = ((payload: P) => ReduxAction<P>) & {
90
+ type: string;
91
+ match: (action: {
92
+ type: string;
93
+ }) => action is ReduxAction<P>;
94
+ };
95
+
96
+ interface SetProfileExtensionPayload<TExtension> {
97
+ extension: TExtension;
98
+ id: string;
99
+ }
100
+ interface ProfileExtensionActions<TExtension> {
101
+ remove: ActionCreator<string>;
102
+ set: ActionCreator<SetProfileExtensionPayload<TExtension>>;
103
+ }
104
+ type ProfileExtensionState<TExtension> = Record<string, TExtension>;
105
+ interface ProfileExtensionSlice<TExtension> {
106
+ actions: ProfileExtensionActions<TExtension>;
107
+ reducer: (state: ProfileExtensionState<TExtension> | undefined, action: {
108
+ type: string;
109
+ }) => ProfileExtensionState<TExtension>;
110
+ }
111
+ declare function createProfileExtensionSlice<TExtension>(namespace: string): ProfileExtensionSlice<TExtension>;
112
+
113
+ interface SelectProfilePayload<TSeat> {
114
+ profileId: string | null;
115
+ seat: TSeat;
116
+ }
117
+ interface ProfileSelectionActions<TSeat> {
118
+ clearProfile: ActionCreator<string>;
119
+ select: ActionCreator<SelectProfilePayload<TSeat>>;
120
+ }
121
+ type ProfileSelectionState<TSeat extends string | number> = Record<TSeat, string | null>;
122
+ interface ProfileSelectionSlice<TSeat extends string | number> {
123
+ actions: ProfileSelectionActions<TSeat>;
124
+ reducer: (state: ProfileSelectionState<TSeat> | undefined, action: {
125
+ type: string;
126
+ payload?: unknown;
127
+ }) => ProfileSelectionState<TSeat>;
128
+ }
129
+ declare function createProfileSelectionSlice<TSeat extends string | number>(namespace: string, defaultState: ProfileSelectionState<TSeat>, mountKey?: string): ProfileSelectionSlice<TSeat>;
130
+
131
+ interface UpdateProfilePayload {
132
+ id: string;
133
+ patch: Partial<Pick<Profile, 'name' | 'color' | 'tag'>>;
134
+ }
135
+ declare const profilesActions: {
136
+ add: ActionCreator<Profile>;
137
+ remove: ActionCreator<string>;
138
+ setAll: ActionCreator<Profile[]>;
139
+ update: ActionCreator<UpdateProfilePayload>;
140
+ };
141
+ declare function profilesReducer(state: Profile[] | undefined, action: {
142
+ type: string;
143
+ }): Profile[];
144
+ interface CreateProfileInput {
145
+ name: string;
146
+ color: string;
147
+ tag: string;
148
+ }
149
+ declare function createProfileRecord(input: CreateProfileInput): Profile;
150
+
151
+ declare function resolveInitialProfiles(groupId: string, localFallback: Profile[]): Promise<Profile[]>;
152
+
153
+ interface SetLastGuestColorPayload<TSeat> {
154
+ color: string;
155
+ seat: TSeat;
156
+ }
157
+ interface SetProfileOverridePayload<TSeat> {
158
+ color: string | null;
159
+ seat: TSeat;
160
+ }
161
+ interface SeatColorsActions<TSeat> {
162
+ setLastCpuColor: ActionCreator<string>;
163
+ setLastGuestColor: ActionCreator<SetLastGuestColorPayload<TSeat>>;
164
+ setProfileOverride: ActionCreator<SetProfileOverridePayload<TSeat>>;
165
+ }
166
+ type SeatColorsState<TSeat extends string | number> = {
167
+ lastCpuColor: string;
168
+ lastGuestColor: Record<TSeat, string>;
169
+ profileOverride: Record<TSeat, string | null>;
170
+ };
171
+ interface SeatColorsSlice<TSeat extends string | number> {
172
+ actions: SeatColorsActions<TSeat>;
173
+ reducer: (state: SeatColorsState<TSeat> | undefined, action: {
174
+ type: string;
175
+ payload?: unknown;
176
+ }) => SeatColorsState<TSeat>;
177
+ }
178
+ declare function createSeatColorsSlice<TSeat extends string | number>(namespace: string, defaultState: SeatColorsState<TSeat>, mountKey?: string): SeatColorsSlice<TSeat>;
179
+
180
+ interface UseSharedProfilesSyncOptions {
181
+ groupId: string;
182
+ onRemoteChange: (profiles: Profile[]) => void;
183
+ }
184
+ interface UseSharedProfilesSyncResult {
185
+ syncToShared: (next: Profile[]) => void;
186
+ }
187
+ declare function useSharedProfilesSync({ groupId, onRemoteChange }: UseSharedProfilesSyncOptions): UseSharedProfilesSyncResult;
188
+
68
189
  declare const isSharedProfileStoreAvailable: boolean;
69
190
  declare function loadSharedProfiles(groupId: string): Promise<Profile[]>;
70
191
  declare function saveSharedProfiles(groupId: string, profiles: Profile[]): Promise<void>;
71
192
 
72
- export { MAX_PROFILE_NAME_LENGTH, MAX_TAG_LENGTH, type Profile, ProfileChip, ProfilePicker, ProfilesManager, type ProfilesManagerHandle, type ProfilesManagerProps, isSharedProfileStoreAvailable, isValidProfile, isValidTag, loadSharedProfiles, saveSharedProfiles };
193
+ export { type CreateProfileInput, MAX_PROFILE_NAME_LENGTH, MAX_TAG_LENGTH, type Profile, ProfileChip, type ProfileEditPatch, type ProfileExtensionActions, type ProfileExtensionSlice, type ProfileExtensionState, ProfilePicker, type ProfileSelectionActions, type ProfileSelectionSlice, type ProfileSelectionState, ProfilesManager, type ProfilesManagerHandle, type ProfilesManagerProps, ProfilesScreen, type ProfilesScreenProps, type SeatColorsActions, type SeatColorsSlice, type SeatColorsState, type SelectProfilePayload, type SetLastGuestColorPayload, type SetProfileExtensionPayload, type SetProfileOverridePayload, type UpdateProfilePayload, type UseSharedProfilesSyncOptions, type UseSharedProfilesSyncResult, createProfileExtensionSlice, createProfileRecord, createProfileSelectionSlice, createSeatColorsSlice, isSharedProfileStoreAvailable, isValidProfile, isValidTag, loadSharedProfiles, profilesActions, profilesReducer, resolveInitialProfiles, saveSharedProfiles, useSharedProfilesSync };