@tastic/profile 0.9.0 → 0.11.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 +27 -2
- package/dist/index.d.mts +12 -3
- package/dist/index.d.ts +12 -3
- package/dist/index.js +121 -79
- package/dist/index.mjs +82 -43
- package/package.json +2 -2
- package/src/ProfileChip.tsx +5 -2
- package/src/ProfileColorContext.tsx +31 -0
- package/src/ProfilePicker.tsx +6 -2
- package/src/ProfilesManager.tsx +7 -2
- package/src/index.ts +1 -0
- package/src/redux/profileSelectionSlice.ts +21 -1
package/README.md
CHANGED
|
@@ -244,15 +244,40 @@ so a host app plugs the reducer(s) into whatever store it already has:
|
|
|
244
244
|
- **`createProfileSelectionSlice<TSeat>(namespace, defaultState)`** — a factory for the *other*
|
|
245
245
|
per-seat piece of state: which profile id each seat currently has selected (parallel to
|
|
246
246
|
`createProfileExtensionSlice` above, but keyed by seat instead of by profile id). Returns a
|
|
247
|
-
`{ actions: { select, clearProfile }, reducer }` pair over a plain
|
|
247
|
+
`{ actions: { select, clearProfile, pruneToRoster }, reducer }` pair over a plain
|
|
248
|
+
`Record<TSeat, string | null>`.
|
|
248
249
|
Unlike `createProfileExtensionSlice`, the seat set can't be derived from `TSeat` alone (types
|
|
249
250
|
don't exist at runtime), so callers pass their own `defaultState` — the same literal object (e.g.
|
|
250
251
|
`{ 1: null, 2: null }`) every app already declares today. `select({ seat, profileId })` sets one
|
|
251
252
|
seat's selection directly; `clearProfile(profileId)` walks every seat and nulls out any that
|
|
252
253
|
currently points at that id — call it when a profile is deleted so no seat is left referencing a
|
|
253
|
-
ghost id.
|
|
254
|
+
ghost id. `pruneToRoster(ids)` nulls every seat whose id is no longer in the given roster; dispatch
|
|
255
|
+
it after both `profilesActions.setAll` paths (the initial `resolveInitialProfiles` reconcile and
|
|
256
|
+
`onRemoteChange`), since a sibling app can delete a profile without this app's own delete ever
|
|
257
|
+
running, and it returns the same state object when nothing changes.
|
|
258
|
+
Always local, like the guest/CPU/override colors below, and never part of the shared
|
|
254
259
|
App Group roster, since who's selected on *this* device is naturally per-app, per-device state.
|
|
255
260
|
|
|
261
|
+
## Muted profile colors (`ProfileColorProvider`)
|
|
262
|
+
|
|
263
|
+
An app that draws profile colors through a transform (Solitaire blends them toward its muted
|
|
264
|
+
card-back palette) wraps its tree once:
|
|
265
|
+
|
|
266
|
+
```tsx
|
|
267
|
+
import { ProfileColorProvider } from '@tastic/profile'
|
|
268
|
+
|
|
269
|
+
const preview = useCallback((hex: string) => blendTowardCardBack(hex, dark), [dark])
|
|
270
|
+
<ProfileColorProvider preview={preview}>{children}</ProfileColorProvider>
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Every `ProfileChip` then draws `preview(profile.color)` (contrast computed against that),
|
|
274
|
+
`ProfilePicker`'s selected row fills with it, and `ProfilesManager`/`ProfilesScreen` use it for
|
|
275
|
+
their swatch previews unless an explicit `colorPreview` prop is passed. Components from other
|
|
276
|
+
packages that render a `ProfileChip` (e.g. `@tastic/hud`'s `AchievementUnlockList`) pick it up
|
|
277
|
+
for free. Only what's drawn changes: saved colors, `onCreate`/`onSave` values and selection are
|
|
278
|
+
untouched. `useProfileColorPreview()` returns the provider's function (or `undefined`), and
|
|
279
|
+
`useProfileDisplayColor(hex)` resolves one color. Without a provider nothing changes.
|
|
280
|
+
|
|
256
281
|
## Per-seat color persistence
|
|
257
282
|
|
|
258
283
|
A loadout/lobby screen's color for a given seat is never just "the selected profile's saved
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { ReactNode, RefObject, Ref } from 'react';
|
|
3
3
|
import { PopoverHost, PopoverRotation } from '@tastic/hud';
|
|
4
4
|
import { View } from 'react-native';
|
|
5
5
|
|
|
@@ -19,6 +19,14 @@ interface Props {
|
|
|
19
19
|
}
|
|
20
20
|
declare function ProfileChip({ profile, size, filled }: Props): react.JSX.Element;
|
|
21
21
|
|
|
22
|
+
interface ProfileColorProviderProps {
|
|
23
|
+
preview: (hex: string) => string;
|
|
24
|
+
children: ReactNode;
|
|
25
|
+
}
|
|
26
|
+
declare function ProfileColorProvider({ preview, children }: ProfileColorProviderProps): react.JSX.Element;
|
|
27
|
+
declare function useProfileColorPreview(): ((hex: string) => string) | undefined;
|
|
28
|
+
declare function useProfileDisplayColor(hex: string): string;
|
|
29
|
+
|
|
22
30
|
interface ProfilePickerAlignResult {
|
|
23
31
|
align: 'left' | 'right' | 'center';
|
|
24
32
|
verticalAlign: 'below' | 'above';
|
|
@@ -74,7 +82,7 @@ interface ProfilesManagerProps {
|
|
|
74
82
|
colorPreview?: (hex: string) => string;
|
|
75
83
|
useColorPickerAlign?: (open: boolean, contentWidth: number, contentHeight: number) => ProfilePickerAlignResult;
|
|
76
84
|
}
|
|
77
|
-
declare function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, headerLeft, ref, fg: fgOverride, fgMuted: fgMutedOverride, cardBg: cardBgOverride, titleVariant, colorPreview, useColorPickerAlign }: ProfilesManagerProps): react.JSX.Element;
|
|
85
|
+
declare function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, headerLeft, ref, fg: fgOverride, fgMuted: fgMutedOverride, cardBg: cardBgOverride, titleVariant, colorPreview: colorPreviewProp, useColorPickerAlign }: ProfilesManagerProps): react.JSX.Element;
|
|
78
86
|
|
|
79
87
|
type MD3TextVariant = 'displayLarge' | 'displayMedium' | 'displaySmall' | 'headlineLarge' | 'headlineMedium' | 'headlineSmall' | 'titleLarge' | 'titleMedium' | 'titleSmall' | 'labelLarge' | 'labelMedium' | 'labelSmall' | 'bodyLarge' | 'bodyMedium' | 'bodySmall';
|
|
80
88
|
interface ProfilesScreenProps {
|
|
@@ -133,6 +141,7 @@ interface SelectProfilePayload<TSeat> {
|
|
|
133
141
|
}
|
|
134
142
|
interface ProfileSelectionActions<TSeat> {
|
|
135
143
|
clearProfile: ActionCreator<string>;
|
|
144
|
+
pruneToRoster: ActionCreator<string[]>;
|
|
136
145
|
select: ActionCreator<SelectProfilePayload<TSeat>>;
|
|
137
146
|
}
|
|
138
147
|
type ProfileSelectionState<TSeat extends string | number> = Record<TSeat, string | null>;
|
|
@@ -207,4 +216,4 @@ declare const isSharedProfileStoreAvailable: boolean;
|
|
|
207
216
|
declare function loadSharedProfiles(groupId: string): Promise<Profile[]>;
|
|
208
217
|
declare function saveSharedProfiles(groupId: string, profiles: Profile[]): Promise<void>;
|
|
209
218
|
|
|
210
|
-
export { type CreateProfileInput, MAX_PROFILE_NAME_LENGTH, MAX_TAG_LENGTH, type Profile, ProfileChip, type ProfileEditPatch, type ProfileExtensionActions, type ProfileExtensionSlice, type ProfileExtensionState, ProfilePicker, type ProfilePickerAlignResult, type ProfilePickerContentSize, 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, getProfilePickerContentSize, isSharedProfileStoreAvailable, isValidProfile, isValidTag, loadSharedProfiles, profilesActions, profilesReducer, resolveInitialProfiles, saveSharedProfiles, useSharedProfilesSync };
|
|
219
|
+
export { type CreateProfileInput, MAX_PROFILE_NAME_LENGTH, MAX_TAG_LENGTH, type Profile, ProfileChip, ProfileColorProvider, type ProfileEditPatch, type ProfileExtensionActions, type ProfileExtensionSlice, type ProfileExtensionState, ProfilePicker, type ProfilePickerAlignResult, type ProfilePickerContentSize, 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, getProfilePickerContentSize, isSharedProfileStoreAvailable, isValidProfile, isValidTag, loadSharedProfiles, profilesActions, profilesReducer, resolveInitialProfiles, saveSharedProfiles, useProfileColorPreview, useProfileDisplayColor, useSharedProfilesSync };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { ReactNode, RefObject, Ref } from 'react';
|
|
3
3
|
import { PopoverHost, PopoverRotation } from '@tastic/hud';
|
|
4
4
|
import { View } from 'react-native';
|
|
5
5
|
|
|
@@ -19,6 +19,14 @@ interface Props {
|
|
|
19
19
|
}
|
|
20
20
|
declare function ProfileChip({ profile, size, filled }: Props): react.JSX.Element;
|
|
21
21
|
|
|
22
|
+
interface ProfileColorProviderProps {
|
|
23
|
+
preview: (hex: string) => string;
|
|
24
|
+
children: ReactNode;
|
|
25
|
+
}
|
|
26
|
+
declare function ProfileColorProvider({ preview, children }: ProfileColorProviderProps): react.JSX.Element;
|
|
27
|
+
declare function useProfileColorPreview(): ((hex: string) => string) | undefined;
|
|
28
|
+
declare function useProfileDisplayColor(hex: string): string;
|
|
29
|
+
|
|
22
30
|
interface ProfilePickerAlignResult {
|
|
23
31
|
align: 'left' | 'right' | 'center';
|
|
24
32
|
verticalAlign: 'below' | 'above';
|
|
@@ -74,7 +82,7 @@ interface ProfilesManagerProps {
|
|
|
74
82
|
colorPreview?: (hex: string) => string;
|
|
75
83
|
useColorPickerAlign?: (open: boolean, contentWidth: number, contentHeight: number) => ProfilePickerAlignResult;
|
|
76
84
|
}
|
|
77
|
-
declare function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, headerLeft, ref, fg: fgOverride, fgMuted: fgMutedOverride, cardBg: cardBgOverride, titleVariant, colorPreview, useColorPickerAlign }: ProfilesManagerProps): react.JSX.Element;
|
|
85
|
+
declare function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, headerLeft, ref, fg: fgOverride, fgMuted: fgMutedOverride, cardBg: cardBgOverride, titleVariant, colorPreview: colorPreviewProp, useColorPickerAlign }: ProfilesManagerProps): react.JSX.Element;
|
|
78
86
|
|
|
79
87
|
type MD3TextVariant = 'displayLarge' | 'displayMedium' | 'displaySmall' | 'headlineLarge' | 'headlineMedium' | 'headlineSmall' | 'titleLarge' | 'titleMedium' | 'titleSmall' | 'labelLarge' | 'labelMedium' | 'labelSmall' | 'bodyLarge' | 'bodyMedium' | 'bodySmall';
|
|
80
88
|
interface ProfilesScreenProps {
|
|
@@ -133,6 +141,7 @@ interface SelectProfilePayload<TSeat> {
|
|
|
133
141
|
}
|
|
134
142
|
interface ProfileSelectionActions<TSeat> {
|
|
135
143
|
clearProfile: ActionCreator<string>;
|
|
144
|
+
pruneToRoster: ActionCreator<string[]>;
|
|
136
145
|
select: ActionCreator<SelectProfilePayload<TSeat>>;
|
|
137
146
|
}
|
|
138
147
|
type ProfileSelectionState<TSeat extends string | number> = Record<TSeat, string | null>;
|
|
@@ -207,4 +216,4 @@ declare const isSharedProfileStoreAvailable: boolean;
|
|
|
207
216
|
declare function loadSharedProfiles(groupId: string): Promise<Profile[]>;
|
|
208
217
|
declare function saveSharedProfiles(groupId: string, profiles: Profile[]): Promise<void>;
|
|
209
218
|
|
|
210
|
-
export { type CreateProfileInput, MAX_PROFILE_NAME_LENGTH, MAX_TAG_LENGTH, type Profile, ProfileChip, type ProfileEditPatch, type ProfileExtensionActions, type ProfileExtensionSlice, type ProfileExtensionState, ProfilePicker, type ProfilePickerAlignResult, type ProfilePickerContentSize, 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, getProfilePickerContentSize, isSharedProfileStoreAvailable, isValidProfile, isValidTag, loadSharedProfiles, profilesActions, profilesReducer, resolveInitialProfiles, saveSharedProfiles, useSharedProfilesSync };
|
|
219
|
+
export { type CreateProfileInput, MAX_PROFILE_NAME_LENGTH, MAX_TAG_LENGTH, type Profile, ProfileChip, ProfileColorProvider, type ProfileEditPatch, type ProfileExtensionActions, type ProfileExtensionSlice, type ProfileExtensionState, ProfilePicker, type ProfilePickerAlignResult, type ProfilePickerContentSize, 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, getProfilePickerContentSize, isSharedProfileStoreAvailable, isValidProfile, isValidTag, loadSharedProfiles, profilesActions, profilesReducer, resolveInitialProfiles, saveSharedProfiles, useProfileColorPreview, useProfileDisplayColor, useSharedProfilesSync };
|
package/dist/index.js
CHANGED
|
@@ -23,6 +23,7 @@ __export(index_exports, {
|
|
|
23
23
|
MAX_PROFILE_NAME_LENGTH: () => MAX_PROFILE_NAME_LENGTH,
|
|
24
24
|
MAX_TAG_LENGTH: () => MAX_TAG_LENGTH,
|
|
25
25
|
ProfileChip: () => ProfileChip,
|
|
26
|
+
ProfileColorProvider: () => ProfileColorProvider,
|
|
26
27
|
ProfilePicker: () => ProfilePicker,
|
|
27
28
|
ProfilesManager: () => ProfilesManager,
|
|
28
29
|
ProfilesScreen: () => ProfilesScreen,
|
|
@@ -39,6 +40,8 @@ __export(index_exports, {
|
|
|
39
40
|
profilesReducer: () => profilesReducer,
|
|
40
41
|
resolveInitialProfiles: () => resolveInitialProfiles,
|
|
41
42
|
saveSharedProfiles: () => saveSharedProfiles,
|
|
43
|
+
useProfileColorPreview: () => useProfileColorPreview,
|
|
44
|
+
useProfileDisplayColor: () => useProfileDisplayColor,
|
|
42
45
|
useSharedProfilesSync: () => useSharedProfilesSync
|
|
43
46
|
});
|
|
44
47
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -47,12 +50,30 @@ module.exports = __toCommonJS(index_exports);
|
|
|
47
50
|
var import_auto_paper = require("@rific/auto-paper");
|
|
48
51
|
var import_react_native = require("react-native");
|
|
49
52
|
var import_react_native_paper = require("react-native-paper");
|
|
53
|
+
|
|
54
|
+
// src/ProfileColorContext.tsx
|
|
55
|
+
var import_react = require("react");
|
|
50
56
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
57
|
+
var ProfileColorContext = (0, import_react.createContext)(void 0);
|
|
58
|
+
function ProfileColorProvider({ preview, children }) {
|
|
59
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ProfileColorContext.Provider, { value: preview, children });
|
|
60
|
+
}
|
|
61
|
+
function useProfileColorPreview() {
|
|
62
|
+
return (0, import_react.useContext)(ProfileColorContext);
|
|
63
|
+
}
|
|
64
|
+
function useProfileDisplayColor(hex) {
|
|
65
|
+
const preview = useProfileColorPreview();
|
|
66
|
+
return preview ? preview(hex) : hex;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// src/ProfileChip.tsx
|
|
70
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
51
71
|
var EMOJI_PATTERN = /\p{Extended_Pictographic}/u;
|
|
52
72
|
function ProfileChip({ profile, size = 22, filled = true }) {
|
|
53
|
-
const
|
|
73
|
+
const color = useProfileDisplayColor(profile.color);
|
|
74
|
+
const contrastColor = (0, import_auto_paper.getContrastColor)(color);
|
|
54
75
|
const tagFontSize = size * (profile.tag && EMOJI_PATTERN.test(profile.tag) ? 0.6 : 0.4);
|
|
55
|
-
return /* @__PURE__ */ (0,
|
|
76
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native.View, { style: [styles.chip, { borderRadius: size / 2, height: size, width: size }, filled && { backgroundColor: color }], children: profile.tag ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native_paper.Text, { style: [styles.tag, { color: contrastColor, fontSize: tagFontSize }], numberOfLines: 1, adjustsFontSizeToFit: true, children: profile.tag }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native_paper.Icon, { source: "account", size: size * 0.6, color: contrastColor }) });
|
|
56
77
|
}
|
|
57
78
|
var styles = import_react_native.StyleSheet.create({
|
|
58
79
|
chip: {
|
|
@@ -69,10 +90,10 @@ var styles = import_react_native.StyleSheet.create({
|
|
|
69
90
|
var import_auto_paper2 = require("@rific/auto-paper");
|
|
70
91
|
var import_feedback_press = require("@rific/feedback-press");
|
|
71
92
|
var import_hud = require("@tastic/hud");
|
|
72
|
-
var
|
|
93
|
+
var import_react2 = require("react");
|
|
73
94
|
var import_react_native2 = require("react-native");
|
|
74
95
|
var import_react_native_paper2 = require("react-native-paper");
|
|
75
|
-
var
|
|
96
|
+
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
76
97
|
var TRIGGER_HEIGHT = 28;
|
|
77
98
|
var POPOVER_WIDTH = 220;
|
|
78
99
|
var ROW_HEIGHT = 36;
|
|
@@ -83,11 +104,12 @@ function getProfilePickerContentSize(profileCount, hasManageRow = false) {
|
|
|
83
104
|
}
|
|
84
105
|
function ProfilePicker({ idPrefix, host, profiles, selectedId, takenId, color, dark, align: forcedAlign, alignOverride, rotation = 0, guestLabel, nullLabel = "Player", nullIcon = "account-off-outline", onSelect, onManage }) {
|
|
85
106
|
const id = `${idPrefix}-profile`;
|
|
107
|
+
const colorPreview = useProfileColorPreview();
|
|
86
108
|
const menuBg = dark ? "#000000" : "#FFFFFF";
|
|
87
109
|
const fg = dark ? "#FFFFFF" : "#000000";
|
|
88
110
|
const open = host.openId === id;
|
|
89
111
|
const selectedProfile = profiles.find((p) => p.id === selectedId) ?? null;
|
|
90
|
-
const sortedProfiles = (0,
|
|
112
|
+
const sortedProfiles = (0, import_react2.useMemo)(() => [...profiles].sort((a, b) => a.name.localeCompare(b.name)), [profiles]);
|
|
91
113
|
const { width: contentWidth, height: contentHeight } = getProfilePickerContentSize(profiles.length, !!onManage);
|
|
92
114
|
const auto = (0, import_hud.useAutoAlign)(open, contentWidth, contentHeight, rotation);
|
|
93
115
|
const placement = alignOverride ?? auto;
|
|
@@ -106,12 +128,12 @@ function ProfilePicker({ idPrefix, host, profiles, selectedId, takenId, color, d
|
|
|
106
128
|
host.close();
|
|
107
129
|
onManage?.();
|
|
108
130
|
};
|
|
109
|
-
return /* @__PURE__ */ (0,
|
|
110
|
-
/* @__PURE__ */ (0,
|
|
111
|
-
/* @__PURE__ */ (0,
|
|
112
|
-
/* @__PURE__ */ (0,
|
|
131
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_react_native2.View, { style: [styles2.anchor, open && styles2.anchorOpen], children: [
|
|
132
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_feedback_press.TouchableRipple, { onPress: () => host.toggle(id), style: styles2.trigger, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_react_native2.View, { ref: triggerRef, collapsable: false, style: styles2.triggerInner, children: [
|
|
133
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native_paper2.Text, { style: [styles2.triggerLabel, { color }], numberOfLines: 1, children: (selectedProfile?.name ?? guestLabel).toUpperCase() }),
|
|
134
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native_paper2.Icon, { source: "menu-down", size: 16, color })
|
|
113
135
|
] }) }),
|
|
114
|
-
/* @__PURE__ */ (0,
|
|
136
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_hud.PopoverBody, { visible: open && measured, align, verticalAlign, caretColor: menuBg, caretBorderColor: color, caretBorderWidth: 2, triggerSize: TRIGGER_HEIGHT, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
115
137
|
import_react_native2.ScrollView,
|
|
116
138
|
{
|
|
117
139
|
style: [
|
|
@@ -126,22 +148,23 @@ function ProfilePicker({ idPrefix, host, profiles, selectedId, takenId, color, d
|
|
|
126
148
|
contentContainerStyle: styles2.menuContent,
|
|
127
149
|
showsVerticalScrollIndicator: false,
|
|
128
150
|
children: [
|
|
129
|
-
/* @__PURE__ */ (0,
|
|
130
|
-
/* @__PURE__ */ (0,
|
|
131
|
-
/* @__PURE__ */ (0,
|
|
151
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_feedback_press.TouchableRipple, { onPress: handleSelectGuest, style: [styles2.row, selectedId === null && { backgroundColor: color }], children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_react_native2.View, { style: styles2.rowInner, children: [
|
|
152
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native2.View, { style: styles2.guestChip, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native_paper2.Icon, { source: nullIcon, size: 13, color: selectedId === null ? (0, import_auto_paper2.getContrastColor)(color) : fg }) }),
|
|
153
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native_paper2.Text, { style: [styles2.rowLabel, { color: selectedId === null ? (0, import_auto_paper2.getContrastColor)(color) : fg }], children: nullLabel })
|
|
132
154
|
] }) }),
|
|
133
155
|
sortedProfiles.map((profile) => {
|
|
134
156
|
const taken = profile.id === takenId;
|
|
135
157
|
const selected = selectedId === profile.id;
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
/* @__PURE__ */ (0,
|
|
158
|
+
const rowColor = colorPreview ? colorPreview(profile.color) : profile.color;
|
|
159
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_feedback_press.TouchableRipple, { disabled: taken, onPress: () => handleSelectExisting(profile), style: [styles2.row, selected && { backgroundColor: rowColor }], children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_react_native2.View, { style: [styles2.rowInner, taken && styles2.rowDisabled], children: [
|
|
160
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ProfileChip, { profile, filled: !selected }),
|
|
161
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
139
162
|
import_react_native_paper2.Text,
|
|
140
163
|
{
|
|
141
164
|
style: [
|
|
142
165
|
styles2.rowLabel,
|
|
143
166
|
{
|
|
144
|
-
color: selected ? (0, import_auto_paper2.getContrastColor)(
|
|
167
|
+
color: selected ? (0, import_auto_paper2.getContrastColor)(rowColor) : fg
|
|
145
168
|
}
|
|
146
169
|
],
|
|
147
170
|
numberOfLines: 1,
|
|
@@ -150,11 +173,11 @@ function ProfilePicker({ idPrefix, host, profiles, selectedId, takenId, color, d
|
|
|
150
173
|
)
|
|
151
174
|
] }) }, profile.id);
|
|
152
175
|
}),
|
|
153
|
-
onManage && /* @__PURE__ */ (0,
|
|
154
|
-
/* @__PURE__ */ (0,
|
|
155
|
-
/* @__PURE__ */ (0,
|
|
156
|
-
/* @__PURE__ */ (0,
|
|
157
|
-
/* @__PURE__ */ (0,
|
|
176
|
+
onManage && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
|
|
177
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native2.View, { style: [styles2.divider, { backgroundColor: color }] }),
|
|
178
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_feedback_press.TouchableRipple, { onPress: handleManage, style: styles2.row, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_react_native2.View, { style: styles2.manageRowInner, children: [
|
|
179
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native_paper2.Icon, { source: "account-cog-outline", size: 16, color }),
|
|
180
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native_paper2.Text, { style: [styles2.manageLabel, { color }], children: "Manage" })
|
|
158
181
|
] }) })
|
|
159
182
|
] })
|
|
160
183
|
]
|
|
@@ -258,7 +281,7 @@ var import_feedback_press2 = require("@rific/feedback-press");
|
|
|
258
281
|
var import_focus_chain = require("@rific/focus-chain");
|
|
259
282
|
var import_toaster = require("@rific/toaster");
|
|
260
283
|
var import_hud2 = require("@tastic/hud");
|
|
261
|
-
var
|
|
284
|
+
var import_react3 = require("react");
|
|
262
285
|
var import_react_native3 = require("react-native");
|
|
263
286
|
var import_react_native_paper3 = require("react-native-paper");
|
|
264
287
|
var import_react_native_safe_area_context = require("react-native-safe-area-context");
|
|
@@ -296,13 +319,15 @@ function isValidProfile(value) {
|
|
|
296
319
|
}
|
|
297
320
|
|
|
298
321
|
// src/ProfilesManager.tsx
|
|
299
|
-
var
|
|
322
|
+
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
300
323
|
var CHIP_SIZE = 40;
|
|
301
324
|
function deriveTag(name) {
|
|
302
325
|
const trimmed = name.trim();
|
|
303
326
|
return trimmed ? trimmed[0].toUpperCase() : "";
|
|
304
327
|
}
|
|
305
|
-
function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, headerLeft, ref, fg: fgOverride, fgMuted: fgMutedOverride, cardBg: cardBgOverride, titleVariant = "headlineSmall", colorPreview, useColorPickerAlign }) {
|
|
328
|
+
function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, headerLeft, ref, fg: fgOverride, fgMuted: fgMutedOverride, cardBg: cardBgOverride, titleVariant = "headlineSmall", colorPreview: colorPreviewProp, useColorPickerAlign }) {
|
|
329
|
+
const contextColorPreview = useProfileColorPreview();
|
|
330
|
+
const colorPreview = colorPreviewProp ?? contextColorPreview;
|
|
306
331
|
const { dark, colors } = (0, import_auto_paper3.useAutoPaperTheme)();
|
|
307
332
|
const insets = (0, import_react_native_safe_area_context.useSafeAreaInsets)();
|
|
308
333
|
const { error: showErrorToast } = (0, import_toaster.useToast)();
|
|
@@ -313,13 +338,13 @@ function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, h
|
|
|
313
338
|
const cardBorder = dark ? "rgba(255,255,255,0.2)" : "rgba(0,0,0,0.2)";
|
|
314
339
|
const cancelBg = dark ? "#2A2A2A" : "#E0E0E0";
|
|
315
340
|
const cancelFg = dark ? "#FFFFFF" : "#000000";
|
|
316
|
-
const [editingId, setEditingId] = (0,
|
|
317
|
-
const [draftName, setDraftName] = (0,
|
|
318
|
-
const [draftColor, setDraftColor] = (0,
|
|
319
|
-
const [draftTag, setDraftTag] = (0,
|
|
320
|
-
const [tagManuallySet, setTagManuallySet] = (0,
|
|
321
|
-
const [confirmDeleteId, setConfirmDeleteId] = (0,
|
|
322
|
-
const commitEdit = (0,
|
|
341
|
+
const [editingId, setEditingId] = (0, import_react3.useState)(null);
|
|
342
|
+
const [draftName, setDraftName] = (0, import_react3.useState)("");
|
|
343
|
+
const [draftColor, setDraftColor] = (0, import_react3.useState)(defaultColor);
|
|
344
|
+
const [draftTag, setDraftTag] = (0, import_react3.useState)("");
|
|
345
|
+
const [tagManuallySet, setTagManuallySet] = (0, import_react3.useState)(false);
|
|
346
|
+
const [confirmDeleteId, setConfirmDeleteId] = (0, import_react3.useState)(null);
|
|
347
|
+
const commitEdit = (0, import_react3.useCallback)(() => {
|
|
323
348
|
const name = draftName.trim();
|
|
324
349
|
if (editingId && name) {
|
|
325
350
|
const tagValid = isValidTag(draftTag);
|
|
@@ -330,13 +355,13 @@ function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, h
|
|
|
330
355
|
}
|
|
331
356
|
setEditingId(null);
|
|
332
357
|
}, [editingId, draftName, draftColor, draftTag, onCreate, onSave, showErrorToast]);
|
|
333
|
-
const commitEditRef = (0,
|
|
334
|
-
(0,
|
|
358
|
+
const commitEditRef = (0, import_react3.useRef)(commitEdit);
|
|
359
|
+
(0, import_react3.useEffect)(() => {
|
|
335
360
|
commitEditRef.current = commitEdit;
|
|
336
361
|
});
|
|
337
|
-
(0,
|
|
338
|
-
(0,
|
|
339
|
-
const startEdit = (0,
|
|
362
|
+
(0, import_react3.useEffect)(() => () => commitEditRef.current(), []);
|
|
363
|
+
(0, import_react3.useImperativeHandle)(ref, () => ({ commitPendingEdit: () => commitEditRef.current() }), []);
|
|
364
|
+
const startEdit = (0, import_react3.useCallback)(
|
|
340
365
|
(target) => {
|
|
341
366
|
commitEdit();
|
|
342
367
|
if (target === "new") {
|
|
@@ -354,61 +379,61 @@ function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, h
|
|
|
354
379
|
},
|
|
355
380
|
[commitEdit, defaultColor]
|
|
356
381
|
);
|
|
357
|
-
const handleNameChange = (0,
|
|
382
|
+
const handleNameChange = (0, import_react3.useCallback)(
|
|
358
383
|
(name) => {
|
|
359
384
|
setDraftName(name);
|
|
360
385
|
if (!tagManuallySet) setDraftTag(deriveTag(name));
|
|
361
386
|
},
|
|
362
387
|
[tagManuallySet]
|
|
363
388
|
);
|
|
364
|
-
const handleTagChange = (0,
|
|
389
|
+
const handleTagChange = (0, import_react3.useCallback)((tag) => {
|
|
365
390
|
setDraftTag((previous) => clampTag(previous, tag));
|
|
366
391
|
setTagManuallySet(true);
|
|
367
392
|
}, []);
|
|
368
393
|
const confirmDeleteProfile = profiles.find((p) => p.id === confirmDeleteId) ?? null;
|
|
369
|
-
const handleConfirmDelete = (0,
|
|
394
|
+
const handleConfirmDelete = (0, import_react3.useCallback)(() => {
|
|
370
395
|
if (confirmDeleteId) {
|
|
371
396
|
onDelete(confirmDeleteId);
|
|
372
397
|
if (editingId === confirmDeleteId) setEditingId(null);
|
|
373
398
|
}
|
|
374
399
|
setConfirmDeleteId(null);
|
|
375
400
|
}, [confirmDeleteId, editingId, onDelete]);
|
|
376
|
-
const sortedProfiles = (0,
|
|
401
|
+
const sortedProfiles = (0, import_react3.useMemo)(() => [...profiles].sort((a, b) => a.name.localeCompare(b.name)), [profiles]);
|
|
377
402
|
const rows = [...sortedProfiles.map((profile) => ({ kind: "profile", profile })), { kind: "new" }];
|
|
378
|
-
return /* @__PURE__ */ (0,
|
|
379
|
-
/* @__PURE__ */ (0,
|
|
403
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
|
|
404
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native3.View, { style: [styles3.header, { paddingTop: 8 + insets.top, paddingLeft: 8 + insets.left }], children: [
|
|
380
405
|
headerLeft,
|
|
381
|
-
/* @__PURE__ */ (0,
|
|
406
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native_paper3.Text, { variant: titleVariant, style: [styles3.title, { color: fg }], children: "Profiles" })
|
|
382
407
|
] }),
|
|
383
|
-
/* @__PURE__ */ (0,
|
|
408
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native3.KeyboardAvoidingView, { behavior: import_react_native3.Platform.OS === "ios" ? "padding" : void 0, style: styles3.keyboardAvoiding, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native3.ScrollView, { style: styles3.scrollView, contentContainerStyle: [styles3.content, { paddingBottom: 32 + insets.bottom }], showsVerticalScrollIndicator: false, keyboardShouldPersistTaps: "handled", children: rows.map((row) => {
|
|
384
409
|
if (row.kind === "new") {
|
|
385
410
|
if (editingId !== "new") {
|
|
386
|
-
return /* @__PURE__ */ (0,
|
|
387
|
-
/* @__PURE__ */ (0,
|
|
388
|
-
/* @__PURE__ */ (0,
|
|
411
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_feedback_press2.TouchableRipple, { onPress: () => startEdit("new"), style: styles3.row, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native3.View, { style: styles3.rowInner, children: [
|
|
412
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native3.View, { style: [styles3.rowChip, styles3.newChip, { borderColor: fg }], children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native_paper3.Icon, { source: "plus", size: 20, color: fg }) }),
|
|
413
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native_paper3.Text, { variant: "bodyLarge", style: { color: fg }, children: "New Profile" })
|
|
389
414
|
] }) }, "new");
|
|
390
415
|
}
|
|
391
|
-
return /* @__PURE__ */ (0,
|
|
416
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(EditRow, { draftName, onNameChange: handleNameChange, draftColor, onColorChange: setDraftColor, draftTag, onTagChange: handleTagChange, onSubmit: commitEdit, autoFocus: true, fgMuted, host, dark, colorPreview, useColorPickerAlign }, "new");
|
|
392
417
|
}
|
|
393
418
|
const { profile } = row;
|
|
394
419
|
if (editingId === profile.id) {
|
|
395
|
-
return /* @__PURE__ */ (0,
|
|
420
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(EditRow, { draftName, onNameChange: handleNameChange, draftColor, onColorChange: setDraftColor, draftTag, onTagChange: handleTagChange, onSubmit: commitEdit, onDelete: () => setConfirmDeleteId(profile.id), fgMuted, host, dark, colorPreview, useColorPickerAlign }, profile.id);
|
|
396
421
|
}
|
|
397
|
-
return /* @__PURE__ */ (0,
|
|
398
|
-
/* @__PURE__ */ (0,
|
|
399
|
-
/* @__PURE__ */ (0,
|
|
422
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_feedback_press2.TouchableRipple, { onPress: () => startEdit(profile), style: styles3.row, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native3.View, { style: styles3.rowInner, children: [
|
|
423
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ProfileChip, { profile, size: CHIP_SIZE }),
|
|
424
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native_paper3.Text, { variant: "bodyLarge", style: [styles3.rowName, { color: fg }], numberOfLines: 1, children: profile.name })
|
|
400
425
|
] }) }, profile.id);
|
|
401
426
|
}) }) }),
|
|
402
|
-
confirmDeleteProfile && /* @__PURE__ */ (0,
|
|
403
|
-
/* @__PURE__ */ (0,
|
|
404
|
-
/* @__PURE__ */ (0,
|
|
405
|
-
/* @__PURE__ */ (0,
|
|
427
|
+
confirmDeleteProfile && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native_paper3.Portal, { children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native3.View, { style: styles3.overlay, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native3.View, { style: [styles3.overlayCard, { backgroundColor: cardBg, borderColor: cardBorder }], children: [
|
|
428
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native_paper3.Icon, { source: "alert-outline", size: 64, color: colors.danger }),
|
|
429
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native_paper3.Text, { variant: "headlineLarge", style: [styles3.overlayTitle, { color: colors.danger }], children: "Delete Profile?" }),
|
|
430
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native_paper3.Text, { variant: "bodyLarge", style: [styles3.overlayBody, { color: fg }], children: [
|
|
406
431
|
"This permanently deletes ",
|
|
407
432
|
confirmDeleteProfile.name,
|
|
408
433
|
"\u2019s profile. This cannot be undone."
|
|
409
434
|
] }),
|
|
410
|
-
/* @__PURE__ */ (0,
|
|
411
|
-
/* @__PURE__ */ (0,
|
|
435
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native_paper3.Button, { mode: "contained", onPress: () => setConfirmDeleteId(null), style: styles3.overlayButton, buttonColor: cancelBg, textColor: cancelFg, children: "Cancel" }),
|
|
436
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native_paper3.Button, { mode: "contained", onPress: handleConfirmDelete, style: styles3.overlayButton, buttonColor: colors.danger, textColor: colors.onDanger, children: "Delete" })
|
|
412
437
|
] }) }) })
|
|
413
438
|
] });
|
|
414
439
|
}
|
|
@@ -416,24 +441,24 @@ function AlignedColorPicker({ useAlign, ...props }) {
|
|
|
416
441
|
const { width: windowWidth } = (0, import_react_native3.useWindowDimensions)();
|
|
417
442
|
const { width, height } = (0, import_hud2.getInlineColorPickerContentSize)(import_auto_paper3.defaultColors.length, windowWidth);
|
|
418
443
|
const alignOverride = useAlign(props.host.openId === props.id, width, height);
|
|
419
|
-
return /* @__PURE__ */ (0,
|
|
444
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_hud2.InlineColorPicker, { ...props, alignOverride });
|
|
420
445
|
}
|
|
421
446
|
function EditRow({ draftName, onNameChange, draftColor, onColorChange, draftTag, onTagChange, onSubmit, onDelete, autoFocus, fgMuted, host, dark, colorPreview, useColorPickerAlign }) {
|
|
422
447
|
const register = (0, import_focus_chain.useFocusChain)();
|
|
423
448
|
const tag = register();
|
|
424
449
|
const name = register();
|
|
425
|
-
const tagFocusRef = (0,
|
|
426
|
-
(0,
|
|
450
|
+
const tagFocusRef = (0, import_react3.useRef)(tag.props.focus);
|
|
451
|
+
(0, import_react3.useEffect)(() => {
|
|
427
452
|
tagFocusRef.current = tag.props.focus;
|
|
428
453
|
});
|
|
429
|
-
(0,
|
|
454
|
+
(0, import_react3.useEffect)(() => {
|
|
430
455
|
if (host.openId === "color") tagFocusRef.current();
|
|
431
456
|
}, [host.openId]);
|
|
432
|
-
return /* @__PURE__ */ (0,
|
|
433
|
-
useColorPickerAlign ? /* @__PURE__ */ (0,
|
|
434
|
-
/* @__PURE__ */ (0,
|
|
435
|
-
/* @__PURE__ */ (0,
|
|
436
|
-
onDelete && /* @__PURE__ */ (0,
|
|
457
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native3.View, { style: styles3.editRow, children: [
|
|
458
|
+
useColorPickerAlign ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(AlignedColorPicker, { useAlign: useColorPickerAlign, id: "color", host, value: draftColor, onChange: onColorChange, previewValue: colorPreview, tag: draftTag, dark }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_hud2.InlineColorPicker, { id: "color", host, value: draftColor, onChange: onColorChange, previewValue: colorPreview, tag: draftTag, dark }),
|
|
459
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native_paper3.TextInput, { ref: tag.ref, ...tag.props, value: draftTag, onChangeText: onTagChange, accessibilityLabel: "Tag", autoCapitalize: "characters", selectTextOnFocus: true, returnKeyType: "next", style: styles3.hiddenTagInput, pointerEvents: "none" }),
|
|
460
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native_paper3.TextInput, { ref: name.ref, ...name.props, onSubmitEditing: onSubmit, mode: "outlined", dense: true, value: draftName, onChangeText: onNameChange, maxLength: MAX_PROFILE_NAME_LENGTH, placeholder: "Name", autoFocus, returnKeyType: "done", style: styles3.nameInput }),
|
|
461
|
+
onDelete && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_feedback_press2.TouchableRipple, { onPress: onDelete, style: styles3.iconButton, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native_paper3.Icon, { source: "trash-can-outline", size: 18, color: fgMuted }) })
|
|
437
462
|
] });
|
|
438
463
|
}
|
|
439
464
|
var styles3 = import_react_native3.StyleSheet.create({
|
|
@@ -532,19 +557,19 @@ var styles3 = import_react_native3.StyleSheet.create({
|
|
|
532
557
|
// src/ProfilesScreen.tsx
|
|
533
558
|
var import_auto_paper4 = require("@rific/auto-paper");
|
|
534
559
|
var import_feedback_press3 = require("@rific/feedback-press");
|
|
535
|
-
var
|
|
560
|
+
var import_react4 = require("react");
|
|
536
561
|
var import_react_native4 = require("react-native");
|
|
537
|
-
var
|
|
562
|
+
var import_jsx_runtime5 = require("react/jsx-runtime");
|
|
538
563
|
function ProfilesScreen({ profiles, defaultColor, onCreate, onSave, onDelete, onBack, fg: fgOverride, fgMuted, bg: bgOverride, cardBg, titleVariant, colorPreview, useColorPickerAlign }) {
|
|
539
564
|
const { dark } = (0, import_auto_paper4.useAutoPaperTheme)();
|
|
540
|
-
const managerRef = (0,
|
|
565
|
+
const managerRef = (0, import_react4.useRef)(null);
|
|
541
566
|
const fg = fgOverride ?? (dark ? "#FFFFFF" : "#000000");
|
|
542
567
|
const bg = bgOverride ?? (dark ? "#000000" : "#FFFFFF");
|
|
543
568
|
const handleBack = () => {
|
|
544
569
|
managerRef.current?.commitPendingEdit();
|
|
545
570
|
onBack();
|
|
546
571
|
};
|
|
547
|
-
return /* @__PURE__ */ (0,
|
|
572
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native4.View, { style: [styles4.container, { backgroundColor: bg }], children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(ProfilesManager, { ref: managerRef, profiles, defaultColor, onCreate, onSave, onDelete, fg, fgMuted, cardBg, titleVariant, colorPreview, useColorPickerAlign, headerLeft: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_feedback_press3.IconButton, { icon: "arrow-left", iconColor: fg, size: 24, onPress: handleBack, accessibilityLabel: "Back" }) }) });
|
|
548
573
|
}
|
|
549
574
|
var styles4 = import_react_native4.StyleSheet.create({
|
|
550
575
|
container: {
|
|
@@ -582,6 +607,7 @@ var REHYDRATE = "persist/REHYDRATE";
|
|
|
582
607
|
function createProfileSelectionSlice(namespace, defaultState, mountKey = "profileSelection") {
|
|
583
608
|
const select = createActionCreator(`profileSelection/${namespace}/select`);
|
|
584
609
|
const clearProfile = createActionCreator(`profileSelection/${namespace}/clearProfile`);
|
|
610
|
+
const pruneToRoster = createActionCreator(`profileSelection/${namespace}/pruneToRoster`);
|
|
585
611
|
function reducer(state = defaultState, action) {
|
|
586
612
|
if (action.type === REHYDRATE) {
|
|
587
613
|
const persisted = action.payload?.[mountKey];
|
|
@@ -595,9 +621,22 @@ function createProfileSelectionSlice(namespace, defaultState, mountKey = "profil
|
|
|
595
621
|
}
|
|
596
622
|
return next;
|
|
597
623
|
}
|
|
624
|
+
if (pruneToRoster.match(action)) {
|
|
625
|
+
const roster = new Set(action.payload);
|
|
626
|
+
const next = { ...state };
|
|
627
|
+
let changed = false;
|
|
628
|
+
for (const seat of Object.keys(state)) {
|
|
629
|
+
const profileId = state[seat];
|
|
630
|
+
if (profileId !== null && !roster.has(profileId)) {
|
|
631
|
+
next[seat] = null;
|
|
632
|
+
changed = true;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
return changed ? next : state;
|
|
636
|
+
}
|
|
598
637
|
return state;
|
|
599
638
|
}
|
|
600
|
-
return { actions: { clearProfile, select }, reducer };
|
|
639
|
+
return { actions: { clearProfile, pruneToRoster, select }, reducer };
|
|
601
640
|
}
|
|
602
641
|
|
|
603
642
|
// src/redux/profilesSlice.ts
|
|
@@ -690,15 +729,15 @@ function createSeatColorsSlice(namespace, defaultState, mountKey = "seatColors")
|
|
|
690
729
|
}
|
|
691
730
|
|
|
692
731
|
// src/redux/useSharedProfilesSync.ts
|
|
693
|
-
var
|
|
732
|
+
var import_react5 = require("react");
|
|
694
733
|
var import_react_native5 = require("react-native");
|
|
695
734
|
function useSharedProfilesSync({ groupId, onRemoteChange }) {
|
|
696
|
-
const pendingWriteRef = (0,
|
|
697
|
-
const onRemoteChangeRef = (0,
|
|
698
|
-
(0,
|
|
735
|
+
const pendingWriteRef = (0, import_react5.useRef)(false);
|
|
736
|
+
const onRemoteChangeRef = (0, import_react5.useRef)(onRemoteChange);
|
|
737
|
+
(0, import_react5.useEffect)(() => {
|
|
699
738
|
onRemoteChangeRef.current = onRemoteChange;
|
|
700
739
|
}, [onRemoteChange]);
|
|
701
|
-
(0,
|
|
740
|
+
(0, import_react5.useEffect)(() => {
|
|
702
741
|
if (!isSharedProfileStoreAvailable) return;
|
|
703
742
|
let cancelled = false;
|
|
704
743
|
const sub = import_react_native5.AppState.addEventListener("change", (next) => {
|
|
@@ -712,7 +751,7 @@ function useSharedProfilesSync({ groupId, onRemoteChange }) {
|
|
|
712
751
|
sub.remove();
|
|
713
752
|
};
|
|
714
753
|
}, [groupId]);
|
|
715
|
-
const syncToShared = (0,
|
|
754
|
+
const syncToShared = (0, import_react5.useCallback)(
|
|
716
755
|
(next) => {
|
|
717
756
|
if (!isSharedProfileStoreAvailable) return;
|
|
718
757
|
pendingWriteRef.current = true;
|
|
@@ -730,6 +769,7 @@ function useSharedProfilesSync({ groupId, onRemoteChange }) {
|
|
|
730
769
|
MAX_PROFILE_NAME_LENGTH,
|
|
731
770
|
MAX_TAG_LENGTH,
|
|
732
771
|
ProfileChip,
|
|
772
|
+
ProfileColorProvider,
|
|
733
773
|
ProfilePicker,
|
|
734
774
|
ProfilesManager,
|
|
735
775
|
ProfilesScreen,
|
|
@@ -746,5 +786,7 @@ function useSharedProfilesSync({ groupId, onRemoteChange }) {
|
|
|
746
786
|
profilesReducer,
|
|
747
787
|
resolveInitialProfiles,
|
|
748
788
|
saveSharedProfiles,
|
|
789
|
+
useProfileColorPreview,
|
|
790
|
+
useProfileDisplayColor,
|
|
749
791
|
useSharedProfilesSync
|
|
750
792
|
});
|
package/dist/index.mjs
CHANGED
|
@@ -9,12 +9,30 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
|
|
|
9
9
|
import { getContrastColor } from "@rific/auto-paper";
|
|
10
10
|
import { StyleSheet, View } from "react-native";
|
|
11
11
|
import { Icon, Text } from "react-native-paper";
|
|
12
|
+
|
|
13
|
+
// src/ProfileColorContext.tsx
|
|
14
|
+
import { createContext, useContext } from "react";
|
|
12
15
|
import { jsx } from "react/jsx-runtime";
|
|
16
|
+
var ProfileColorContext = createContext(void 0);
|
|
17
|
+
function ProfileColorProvider({ preview, children }) {
|
|
18
|
+
return /* @__PURE__ */ jsx(ProfileColorContext.Provider, { value: preview, children });
|
|
19
|
+
}
|
|
20
|
+
function useProfileColorPreview() {
|
|
21
|
+
return useContext(ProfileColorContext);
|
|
22
|
+
}
|
|
23
|
+
function useProfileDisplayColor(hex) {
|
|
24
|
+
const preview = useProfileColorPreview();
|
|
25
|
+
return preview ? preview(hex) : hex;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// src/ProfileChip.tsx
|
|
29
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
13
30
|
var EMOJI_PATTERN = /\p{Extended_Pictographic}/u;
|
|
14
31
|
function ProfileChip({ profile, size = 22, filled = true }) {
|
|
15
|
-
const
|
|
32
|
+
const color = useProfileDisplayColor(profile.color);
|
|
33
|
+
const contrastColor = getContrastColor(color);
|
|
16
34
|
const tagFontSize = size * (profile.tag && EMOJI_PATTERN.test(profile.tag) ? 0.6 : 0.4);
|
|
17
|
-
return /* @__PURE__ */
|
|
35
|
+
return /* @__PURE__ */ jsx2(View, { style: [styles.chip, { borderRadius: size / 2, height: size, width: size }, filled && { backgroundColor: color }], children: profile.tag ? /* @__PURE__ */ jsx2(Text, { style: [styles.tag, { color: contrastColor, fontSize: tagFontSize }], numberOfLines: 1, adjustsFontSizeToFit: true, children: profile.tag }) : /* @__PURE__ */ jsx2(Icon, { source: "account", size: size * 0.6, color: contrastColor }) });
|
|
18
36
|
}
|
|
19
37
|
var styles = StyleSheet.create({
|
|
20
38
|
chip: {
|
|
@@ -34,7 +52,7 @@ import { PopoverBody, useAutoAlign } from "@tastic/hud";
|
|
|
34
52
|
import { useMemo } from "react";
|
|
35
53
|
import { ScrollView, StyleSheet as StyleSheet2, View as View2 } from "react-native";
|
|
36
54
|
import { Icon as Icon2, Text as Text2 } from "react-native-paper";
|
|
37
|
-
import { Fragment, jsx as
|
|
55
|
+
import { Fragment, jsx as jsx3, jsxs } from "react/jsx-runtime";
|
|
38
56
|
var TRIGGER_HEIGHT = 28;
|
|
39
57
|
var POPOVER_WIDTH = 220;
|
|
40
58
|
var ROW_HEIGHT = 36;
|
|
@@ -45,6 +63,7 @@ function getProfilePickerContentSize(profileCount, hasManageRow = false) {
|
|
|
45
63
|
}
|
|
46
64
|
function ProfilePicker({ idPrefix, host, profiles, selectedId, takenId, color, dark, align: forcedAlign, alignOverride, rotation = 0, guestLabel, nullLabel = "Player", nullIcon = "account-off-outline", onSelect, onManage }) {
|
|
47
65
|
const id = `${idPrefix}-profile`;
|
|
66
|
+
const colorPreview = useProfileColorPreview();
|
|
48
67
|
const menuBg = dark ? "#000000" : "#FFFFFF";
|
|
49
68
|
const fg = dark ? "#FFFFFF" : "#000000";
|
|
50
69
|
const open = host.openId === id;
|
|
@@ -69,11 +88,11 @@ function ProfilePicker({ idPrefix, host, profiles, selectedId, takenId, color, d
|
|
|
69
88
|
onManage?.();
|
|
70
89
|
};
|
|
71
90
|
return /* @__PURE__ */ jsxs(View2, { style: [styles2.anchor, open && styles2.anchorOpen], children: [
|
|
72
|
-
/* @__PURE__ */
|
|
73
|
-
/* @__PURE__ */
|
|
74
|
-
/* @__PURE__ */
|
|
91
|
+
/* @__PURE__ */ jsx3(TouchableRipple, { onPress: () => host.toggle(id), style: styles2.trigger, children: /* @__PURE__ */ jsxs(View2, { ref: triggerRef, collapsable: false, style: styles2.triggerInner, children: [
|
|
92
|
+
/* @__PURE__ */ jsx3(Text2, { style: [styles2.triggerLabel, { color }], numberOfLines: 1, children: (selectedProfile?.name ?? guestLabel).toUpperCase() }),
|
|
93
|
+
/* @__PURE__ */ jsx3(Icon2, { source: "menu-down", size: 16, color })
|
|
75
94
|
] }) }),
|
|
76
|
-
/* @__PURE__ */
|
|
95
|
+
/* @__PURE__ */ jsx3(PopoverBody, { visible: open && measured, align, verticalAlign, caretColor: menuBg, caretBorderColor: color, caretBorderWidth: 2, triggerSize: TRIGGER_HEIGHT, children: /* @__PURE__ */ jsxs(
|
|
77
96
|
ScrollView,
|
|
78
97
|
{
|
|
79
98
|
style: [
|
|
@@ -88,22 +107,23 @@ function ProfilePicker({ idPrefix, host, profiles, selectedId, takenId, color, d
|
|
|
88
107
|
contentContainerStyle: styles2.menuContent,
|
|
89
108
|
showsVerticalScrollIndicator: false,
|
|
90
109
|
children: [
|
|
91
|
-
/* @__PURE__ */
|
|
92
|
-
/* @__PURE__ */
|
|
93
|
-
/* @__PURE__ */
|
|
110
|
+
/* @__PURE__ */ jsx3(TouchableRipple, { onPress: handleSelectGuest, style: [styles2.row, selectedId === null && { backgroundColor: color }], children: /* @__PURE__ */ jsxs(View2, { style: styles2.rowInner, children: [
|
|
111
|
+
/* @__PURE__ */ jsx3(View2, { style: styles2.guestChip, children: /* @__PURE__ */ jsx3(Icon2, { source: nullIcon, size: 13, color: selectedId === null ? getContrastColor2(color) : fg }) }),
|
|
112
|
+
/* @__PURE__ */ jsx3(Text2, { style: [styles2.rowLabel, { color: selectedId === null ? getContrastColor2(color) : fg }], children: nullLabel })
|
|
94
113
|
] }) }),
|
|
95
114
|
sortedProfiles.map((profile) => {
|
|
96
115
|
const taken = profile.id === takenId;
|
|
97
116
|
const selected = selectedId === profile.id;
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
/* @__PURE__ */
|
|
117
|
+
const rowColor = colorPreview ? colorPreview(profile.color) : profile.color;
|
|
118
|
+
return /* @__PURE__ */ jsx3(TouchableRipple, { disabled: taken, onPress: () => handleSelectExisting(profile), style: [styles2.row, selected && { backgroundColor: rowColor }], children: /* @__PURE__ */ jsxs(View2, { style: [styles2.rowInner, taken && styles2.rowDisabled], children: [
|
|
119
|
+
/* @__PURE__ */ jsx3(ProfileChip, { profile, filled: !selected }),
|
|
120
|
+
/* @__PURE__ */ jsx3(
|
|
101
121
|
Text2,
|
|
102
122
|
{
|
|
103
123
|
style: [
|
|
104
124
|
styles2.rowLabel,
|
|
105
125
|
{
|
|
106
|
-
color: selected ? getContrastColor2(
|
|
126
|
+
color: selected ? getContrastColor2(rowColor) : fg
|
|
107
127
|
}
|
|
108
128
|
],
|
|
109
129
|
numberOfLines: 1,
|
|
@@ -113,10 +133,10 @@ function ProfilePicker({ idPrefix, host, profiles, selectedId, takenId, color, d
|
|
|
113
133
|
] }) }, profile.id);
|
|
114
134
|
}),
|
|
115
135
|
onManage && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
116
|
-
/* @__PURE__ */
|
|
117
|
-
/* @__PURE__ */
|
|
118
|
-
/* @__PURE__ */
|
|
119
|
-
/* @__PURE__ */
|
|
136
|
+
/* @__PURE__ */ jsx3(View2, { style: [styles2.divider, { backgroundColor: color }] }),
|
|
137
|
+
/* @__PURE__ */ jsx3(TouchableRipple, { onPress: handleManage, style: styles2.row, children: /* @__PURE__ */ jsxs(View2, { style: styles2.manageRowInner, children: [
|
|
138
|
+
/* @__PURE__ */ jsx3(Icon2, { source: "account-cog-outline", size: 16, color }),
|
|
139
|
+
/* @__PURE__ */ jsx3(Text2, { style: [styles2.manageLabel, { color }], children: "Manage" })
|
|
120
140
|
] }) })
|
|
121
141
|
] })
|
|
122
142
|
]
|
|
@@ -258,13 +278,15 @@ function isValidProfile(value) {
|
|
|
258
278
|
}
|
|
259
279
|
|
|
260
280
|
// src/ProfilesManager.tsx
|
|
261
|
-
import { Fragment as Fragment2, jsx as
|
|
281
|
+
import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
262
282
|
var CHIP_SIZE = 40;
|
|
263
283
|
function deriveTag(name) {
|
|
264
284
|
const trimmed = name.trim();
|
|
265
285
|
return trimmed ? trimmed[0].toUpperCase() : "";
|
|
266
286
|
}
|
|
267
|
-
function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, headerLeft, ref, fg: fgOverride, fgMuted: fgMutedOverride, cardBg: cardBgOverride, titleVariant = "headlineSmall", colorPreview, useColorPickerAlign }) {
|
|
287
|
+
function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, headerLeft, ref, fg: fgOverride, fgMuted: fgMutedOverride, cardBg: cardBgOverride, titleVariant = "headlineSmall", colorPreview: colorPreviewProp, useColorPickerAlign }) {
|
|
288
|
+
const contextColorPreview = useProfileColorPreview();
|
|
289
|
+
const colorPreview = colorPreviewProp ?? contextColorPreview;
|
|
268
290
|
const { dark, colors } = useAutoPaperTheme();
|
|
269
291
|
const insets = useSafeAreaInsets();
|
|
270
292
|
const { error: showErrorToast } = useToast();
|
|
@@ -340,37 +362,37 @@ function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, h
|
|
|
340
362
|
return /* @__PURE__ */ jsxs2(Fragment2, { children: [
|
|
341
363
|
/* @__PURE__ */ jsxs2(View3, { style: [styles3.header, { paddingTop: 8 + insets.top, paddingLeft: 8 + insets.left }], children: [
|
|
342
364
|
headerLeft,
|
|
343
|
-
/* @__PURE__ */
|
|
365
|
+
/* @__PURE__ */ jsx4(Text3, { variant: titleVariant, style: [styles3.title, { color: fg }], children: "Profiles" })
|
|
344
366
|
] }),
|
|
345
|
-
/* @__PURE__ */
|
|
367
|
+
/* @__PURE__ */ jsx4(KeyboardAvoidingView, { behavior: Platform.OS === "ios" ? "padding" : void 0, style: styles3.keyboardAvoiding, children: /* @__PURE__ */ jsx4(ScrollView2, { style: styles3.scrollView, contentContainerStyle: [styles3.content, { paddingBottom: 32 + insets.bottom }], showsVerticalScrollIndicator: false, keyboardShouldPersistTaps: "handled", children: rows.map((row) => {
|
|
346
368
|
if (row.kind === "new") {
|
|
347
369
|
if (editingId !== "new") {
|
|
348
|
-
return /* @__PURE__ */
|
|
349
|
-
/* @__PURE__ */
|
|
350
|
-
/* @__PURE__ */
|
|
370
|
+
return /* @__PURE__ */ jsx4(TouchableRipple2, { onPress: () => startEdit("new"), style: styles3.row, children: /* @__PURE__ */ jsxs2(View3, { style: styles3.rowInner, children: [
|
|
371
|
+
/* @__PURE__ */ jsx4(View3, { style: [styles3.rowChip, styles3.newChip, { borderColor: fg }], children: /* @__PURE__ */ jsx4(Icon3, { source: "plus", size: 20, color: fg }) }),
|
|
372
|
+
/* @__PURE__ */ jsx4(Text3, { variant: "bodyLarge", style: { color: fg }, children: "New Profile" })
|
|
351
373
|
] }) }, "new");
|
|
352
374
|
}
|
|
353
|
-
return /* @__PURE__ */
|
|
375
|
+
return /* @__PURE__ */ jsx4(EditRow, { draftName, onNameChange: handleNameChange, draftColor, onColorChange: setDraftColor, draftTag, onTagChange: handleTagChange, onSubmit: commitEdit, autoFocus: true, fgMuted, host, dark, colorPreview, useColorPickerAlign }, "new");
|
|
354
376
|
}
|
|
355
377
|
const { profile } = row;
|
|
356
378
|
if (editingId === profile.id) {
|
|
357
|
-
return /* @__PURE__ */
|
|
379
|
+
return /* @__PURE__ */ jsx4(EditRow, { draftName, onNameChange: handleNameChange, draftColor, onColorChange: setDraftColor, draftTag, onTagChange: handleTagChange, onSubmit: commitEdit, onDelete: () => setConfirmDeleteId(profile.id), fgMuted, host, dark, colorPreview, useColorPickerAlign }, profile.id);
|
|
358
380
|
}
|
|
359
|
-
return /* @__PURE__ */
|
|
360
|
-
/* @__PURE__ */
|
|
361
|
-
/* @__PURE__ */
|
|
381
|
+
return /* @__PURE__ */ jsx4(TouchableRipple2, { onPress: () => startEdit(profile), style: styles3.row, children: /* @__PURE__ */ jsxs2(View3, { style: styles3.rowInner, children: [
|
|
382
|
+
/* @__PURE__ */ jsx4(ProfileChip, { profile, size: CHIP_SIZE }),
|
|
383
|
+
/* @__PURE__ */ jsx4(Text3, { variant: "bodyLarge", style: [styles3.rowName, { color: fg }], numberOfLines: 1, children: profile.name })
|
|
362
384
|
] }) }, profile.id);
|
|
363
385
|
}) }) }),
|
|
364
|
-
confirmDeleteProfile && /* @__PURE__ */
|
|
365
|
-
/* @__PURE__ */
|
|
366
|
-
/* @__PURE__ */
|
|
386
|
+
confirmDeleteProfile && /* @__PURE__ */ jsx4(Portal, { children: /* @__PURE__ */ jsx4(View3, { style: styles3.overlay, children: /* @__PURE__ */ jsxs2(View3, { style: [styles3.overlayCard, { backgroundColor: cardBg, borderColor: cardBorder }], children: [
|
|
387
|
+
/* @__PURE__ */ jsx4(Icon3, { source: "alert-outline", size: 64, color: colors.danger }),
|
|
388
|
+
/* @__PURE__ */ jsx4(Text3, { variant: "headlineLarge", style: [styles3.overlayTitle, { color: colors.danger }], children: "Delete Profile?" }),
|
|
367
389
|
/* @__PURE__ */ jsxs2(Text3, { variant: "bodyLarge", style: [styles3.overlayBody, { color: fg }], children: [
|
|
368
390
|
"This permanently deletes ",
|
|
369
391
|
confirmDeleteProfile.name,
|
|
370
392
|
"\u2019s profile. This cannot be undone."
|
|
371
393
|
] }),
|
|
372
|
-
/* @__PURE__ */
|
|
373
|
-
/* @__PURE__ */
|
|
394
|
+
/* @__PURE__ */ jsx4(Button, { mode: "contained", onPress: () => setConfirmDeleteId(null), style: styles3.overlayButton, buttonColor: cancelBg, textColor: cancelFg, children: "Cancel" }),
|
|
395
|
+
/* @__PURE__ */ jsx4(Button, { mode: "contained", onPress: handleConfirmDelete, style: styles3.overlayButton, buttonColor: colors.danger, textColor: colors.onDanger, children: "Delete" })
|
|
374
396
|
] }) }) })
|
|
375
397
|
] });
|
|
376
398
|
}
|
|
@@ -378,7 +400,7 @@ function AlignedColorPicker({ useAlign, ...props }) {
|
|
|
378
400
|
const { width: windowWidth } = useWindowDimensions();
|
|
379
401
|
const { width, height } = getInlineColorPickerContentSize(defaultColors.length, windowWidth);
|
|
380
402
|
const alignOverride = useAlign(props.host.openId === props.id, width, height);
|
|
381
|
-
return /* @__PURE__ */
|
|
403
|
+
return /* @__PURE__ */ jsx4(InlineColorPicker, { ...props, alignOverride });
|
|
382
404
|
}
|
|
383
405
|
function EditRow({ draftName, onNameChange, draftColor, onColorChange, draftTag, onTagChange, onSubmit, onDelete, autoFocus, fgMuted, host, dark, colorPreview, useColorPickerAlign }) {
|
|
384
406
|
const register = useFocusChain();
|
|
@@ -392,10 +414,10 @@ function EditRow({ draftName, onNameChange, draftColor, onColorChange, draftTag,
|
|
|
392
414
|
if (host.openId === "color") tagFocusRef.current();
|
|
393
415
|
}, [host.openId]);
|
|
394
416
|
return /* @__PURE__ */ jsxs2(View3, { style: styles3.editRow, children: [
|
|
395
|
-
useColorPickerAlign ? /* @__PURE__ */
|
|
396
|
-
/* @__PURE__ */
|
|
397
|
-
/* @__PURE__ */
|
|
398
|
-
onDelete && /* @__PURE__ */
|
|
417
|
+
useColorPickerAlign ? /* @__PURE__ */ jsx4(AlignedColorPicker, { useAlign: useColorPickerAlign, id: "color", host, value: draftColor, onChange: onColorChange, previewValue: colorPreview, tag: draftTag, dark }) : /* @__PURE__ */ jsx4(InlineColorPicker, { id: "color", host, value: draftColor, onChange: onColorChange, previewValue: colorPreview, tag: draftTag, dark }),
|
|
418
|
+
/* @__PURE__ */ jsx4(TextInput, { ref: tag.ref, ...tag.props, value: draftTag, onChangeText: onTagChange, accessibilityLabel: "Tag", autoCapitalize: "characters", selectTextOnFocus: true, returnKeyType: "next", style: styles3.hiddenTagInput, pointerEvents: "none" }),
|
|
419
|
+
/* @__PURE__ */ jsx4(TextInput, { ref: name.ref, ...name.props, onSubmitEditing: onSubmit, mode: "outlined", dense: true, value: draftName, onChangeText: onNameChange, maxLength: MAX_PROFILE_NAME_LENGTH, placeholder: "Name", autoFocus, returnKeyType: "done", style: styles3.nameInput }),
|
|
420
|
+
onDelete && /* @__PURE__ */ jsx4(TouchableRipple2, { onPress: onDelete, style: styles3.iconButton, children: /* @__PURE__ */ jsx4(Icon3, { source: "trash-can-outline", size: 18, color: fgMuted }) })
|
|
399
421
|
] });
|
|
400
422
|
}
|
|
401
423
|
var styles3 = StyleSheet3.create({
|
|
@@ -496,7 +518,7 @@ import { useAutoPaperTheme as useAutoPaperTheme2 } from "@rific/auto-paper";
|
|
|
496
518
|
import { IconButton } from "@rific/feedback-press";
|
|
497
519
|
import { useRef as useRef2 } from "react";
|
|
498
520
|
import { StyleSheet as StyleSheet4, View as View4 } from "react-native";
|
|
499
|
-
import { jsx as
|
|
521
|
+
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
500
522
|
function ProfilesScreen({ profiles, defaultColor, onCreate, onSave, onDelete, onBack, fg: fgOverride, fgMuted, bg: bgOverride, cardBg, titleVariant, colorPreview, useColorPickerAlign }) {
|
|
501
523
|
const { dark } = useAutoPaperTheme2();
|
|
502
524
|
const managerRef = useRef2(null);
|
|
@@ -506,7 +528,7 @@ function ProfilesScreen({ profiles, defaultColor, onCreate, onSave, onDelete, on
|
|
|
506
528
|
managerRef.current?.commitPendingEdit();
|
|
507
529
|
onBack();
|
|
508
530
|
};
|
|
509
|
-
return /* @__PURE__ */
|
|
531
|
+
return /* @__PURE__ */ jsx5(View4, { style: [styles4.container, { backgroundColor: bg }], children: /* @__PURE__ */ jsx5(ProfilesManager, { ref: managerRef, profiles, defaultColor, onCreate, onSave, onDelete, fg, fgMuted, cardBg, titleVariant, colorPreview, useColorPickerAlign, headerLeft: /* @__PURE__ */ jsx5(IconButton, { icon: "arrow-left", iconColor: fg, size: 24, onPress: handleBack, accessibilityLabel: "Back" }) }) });
|
|
510
532
|
}
|
|
511
533
|
var styles4 = StyleSheet4.create({
|
|
512
534
|
container: {
|
|
@@ -544,6 +566,7 @@ var REHYDRATE = "persist/REHYDRATE";
|
|
|
544
566
|
function createProfileSelectionSlice(namespace, defaultState, mountKey = "profileSelection") {
|
|
545
567
|
const select = createActionCreator(`profileSelection/${namespace}/select`);
|
|
546
568
|
const clearProfile = createActionCreator(`profileSelection/${namespace}/clearProfile`);
|
|
569
|
+
const pruneToRoster = createActionCreator(`profileSelection/${namespace}/pruneToRoster`);
|
|
547
570
|
function reducer(state = defaultState, action) {
|
|
548
571
|
if (action.type === REHYDRATE) {
|
|
549
572
|
const persisted = action.payload?.[mountKey];
|
|
@@ -557,9 +580,22 @@ function createProfileSelectionSlice(namespace, defaultState, mountKey = "profil
|
|
|
557
580
|
}
|
|
558
581
|
return next;
|
|
559
582
|
}
|
|
583
|
+
if (pruneToRoster.match(action)) {
|
|
584
|
+
const roster = new Set(action.payload);
|
|
585
|
+
const next = { ...state };
|
|
586
|
+
let changed = false;
|
|
587
|
+
for (const seat of Object.keys(state)) {
|
|
588
|
+
const profileId = state[seat];
|
|
589
|
+
if (profileId !== null && !roster.has(profileId)) {
|
|
590
|
+
next[seat] = null;
|
|
591
|
+
changed = true;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
return changed ? next : state;
|
|
595
|
+
}
|
|
560
596
|
return state;
|
|
561
597
|
}
|
|
562
|
-
return { actions: { clearProfile, select }, reducer };
|
|
598
|
+
return { actions: { clearProfile, pruneToRoster, select }, reducer };
|
|
563
599
|
}
|
|
564
600
|
|
|
565
601
|
// src/redux/profilesSlice.ts
|
|
@@ -691,6 +727,7 @@ export {
|
|
|
691
727
|
MAX_PROFILE_NAME_LENGTH,
|
|
692
728
|
MAX_TAG_LENGTH,
|
|
693
729
|
ProfileChip,
|
|
730
|
+
ProfileColorProvider,
|
|
694
731
|
ProfilePicker,
|
|
695
732
|
ProfilesManager,
|
|
696
733
|
ProfilesScreen,
|
|
@@ -707,5 +744,7 @@ export {
|
|
|
707
744
|
profilesReducer,
|
|
708
745
|
resolveInitialProfiles,
|
|
709
746
|
saveSharedProfiles,
|
|
747
|
+
useProfileColorPreview,
|
|
748
|
+
useProfileDisplayColor,
|
|
710
749
|
useSharedProfilesSync
|
|
711
750
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tastic/profile",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Player profile identity for local-multiplayer React Native games — a saved name/color/tag a player selects per seat, with a picker, an inline-editable roster screen, and a reusable identity badge, deliberately blind to any app-specific fields (control scheme, stats) an app layers on top",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react-native",
|
|
@@ -97,7 +97,7 @@
|
|
|
97
97
|
"@rific/focus-chain": ">=0.4.0",
|
|
98
98
|
"@rific/toaster": ">=0.4.0",
|
|
99
99
|
"@tastic/core": ">=0.1.0",
|
|
100
|
-
"@tastic/hud": ">=0.
|
|
100
|
+
"@tastic/hud": ">=0.12.0",
|
|
101
101
|
"expo-modules-core": ">=1.0.0",
|
|
102
102
|
"react": ">=19.0.0",
|
|
103
103
|
"react-native": ">=0.76.0",
|
package/src/ProfileChip.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import { getContrastColor } from '@rific/auto-paper'
|
|
|
2
2
|
import { StyleSheet, View } from 'react-native'
|
|
3
3
|
import { Icon, Text } from 'react-native-paper'
|
|
4
4
|
|
|
5
|
+
import { useProfileDisplayColor } from './ProfileColorContext'
|
|
5
6
|
import { Profile } from './types'
|
|
6
7
|
|
|
7
8
|
// Presence check, not isValidTag's own full-string emoji validation (profilesValidation.ts) — this
|
|
@@ -29,14 +30,16 @@ interface Props {
|
|
|
29
30
|
// badge for a list row (ProfilePicker's dropdown, ProfilesManager's roster) rather than an
|
|
30
31
|
// interactive picker trigger.
|
|
31
32
|
export function ProfileChip({ profile, size = 22, filled = true }: Props) {
|
|
32
|
-
|
|
33
|
+
// The drawn color, not necessarily the saved one — see ProfileColorProvider.
|
|
34
|
+
const color = useProfileDisplayColor(profile.color)
|
|
35
|
+
const contrastColor = getContrastColor(color)
|
|
33
36
|
// An emoji glyph reads visually smaller than a bold letter at the same fontSize (the system emoji
|
|
34
37
|
// font leaves more of its own em-box empty), so a plain-text ratio that looks right for "J" still
|
|
35
38
|
// looks small for "🎮" at the identical size — this bumps emoji up to the icon's own ratio instead,
|
|
36
39
|
// since a single emoji is closer in visual weight to an icon glyph than to a line of text.
|
|
37
40
|
const tagFontSize = size * (profile.tag && EMOJI_PATTERN.test(profile.tag) ? 0.6 : 0.4)
|
|
38
41
|
return (
|
|
39
|
-
<View style={[styles.chip, { borderRadius: size / 2, height: size, width: size }, filled && { backgroundColor:
|
|
42
|
+
<View style={[styles.chip, { borderRadius: size / 2, height: size, width: size }, filled && { backgroundColor: color }]}>
|
|
40
43
|
{profile.tag ? (
|
|
41
44
|
<Text style={[styles.tag, { color: contrastColor, fontSize: tagFontSize }]} numberOfLines={1} adjustsFontSizeToFit>
|
|
42
45
|
{profile.tag}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { createContext, type ReactNode, useContext } from 'react'
|
|
2
|
+
|
|
3
|
+
const ProfileColorContext = createContext<((hex: string) => string) | undefined>(undefined)
|
|
4
|
+
|
|
5
|
+
interface ProfileColorProviderProps {
|
|
6
|
+
// Maps a profile's saved color to the color actually drawn for it. Solitaire blends profile colors
|
|
7
|
+
// toward its card-back palette so they read muted, not as the raw swatch; a context rather than a
|
|
8
|
+
// module-level setting because that blend depends on light/dark mode.
|
|
9
|
+
preview: (hex: string) => string
|
|
10
|
+
children: ReactNode
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// App-wide display transform for profile colors. Every ProfileChip, ProfilePicker's selected-row
|
|
14
|
+
// fill and ProfilesManager's color picker previews read it, and so does anything outside this
|
|
15
|
+
// package that draws a ProfileChip (@tastic/hud's AchievementUnlockList). Without a provider,
|
|
16
|
+
// profile colors draw as saved.
|
|
17
|
+
export function ProfileColorProvider({ preview, children }: ProfileColorProviderProps) {
|
|
18
|
+
return <ProfileColorContext.Provider value={preview}>{children}</ProfileColorContext.Provider>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// The provider's preview, or undefined when there's none (so a caller can tell "no transform"
|
|
22
|
+
// apart from an identity one, e.g. to fall back to its own colorPreview prop).
|
|
23
|
+
export function useProfileColorPreview(): ((hex: string) => string) | undefined {
|
|
24
|
+
return useContext(ProfileColorContext)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Resolves one saved color to its drawn color.
|
|
28
|
+
export function useProfileDisplayColor(hex: string): string {
|
|
29
|
+
const preview = useProfileColorPreview()
|
|
30
|
+
return preview ? preview(hex) : hex
|
|
31
|
+
}
|
package/src/ProfilePicker.tsx
CHANGED
|
@@ -6,6 +6,7 @@ import { ScrollView, StyleSheet, View } from 'react-native'
|
|
|
6
6
|
import { Icon, Text } from 'react-native-paper'
|
|
7
7
|
|
|
8
8
|
import { ProfileChip } from './ProfileChip'
|
|
9
|
+
import { useProfileColorPreview } from './ProfileColorContext'
|
|
9
10
|
import { Profile } from './types'
|
|
10
11
|
|
|
11
12
|
const TRIGGER_HEIGHT = 28
|
|
@@ -121,6 +122,8 @@ interface ProfilePickerProps<P extends Profile> {
|
|
|
121
122
|
// its own persistence hook.
|
|
122
123
|
export function ProfilePicker<P extends Profile>({ idPrefix, host, profiles, selectedId, takenId, color, dark, align: forcedAlign, alignOverride, rotation = 0, guestLabel, nullLabel = 'Player', nullIcon = 'account-off-outline', onSelect, onManage }: ProfilePickerProps<P>) {
|
|
123
124
|
const id = `${idPrefix}-profile`
|
|
125
|
+
// A selected row fills with the profile's drawn color, the same one its ProfileChip would show.
|
|
126
|
+
const colorPreview = useProfileColorPreview()
|
|
124
127
|
const menuBg = dark ? '#000000' : '#FFFFFF'
|
|
125
128
|
const fg = dark ? '#FFFFFF' : '#000000'
|
|
126
129
|
|
|
@@ -201,8 +204,9 @@ export function ProfilePicker<P extends Profile>({ idPrefix, host, profiles, sel
|
|
|
201
204
|
{sortedProfiles.map((profile) => {
|
|
202
205
|
const taken = profile.id === takenId
|
|
203
206
|
const selected = selectedId === profile.id
|
|
207
|
+
const rowColor = colorPreview ? colorPreview(profile.color) : profile.color
|
|
204
208
|
return (
|
|
205
|
-
<TouchableRipple key={profile.id} disabled={taken} onPress={() => handleSelectExisting(profile)} style={[styles.row, selected && { backgroundColor:
|
|
209
|
+
<TouchableRipple key={profile.id} disabled={taken} onPress={() => handleSelectExisting(profile)} style={[styles.row, selected && { backgroundColor: rowColor }]}>
|
|
206
210
|
<View style={[styles.rowInner, taken && styles.rowDisabled]}>
|
|
207
211
|
{/* Unfilled on a selected row — its own background already fills with the
|
|
208
212
|
profile's color, so a second circle behind the tag would just be a redundant badge
|
|
@@ -212,7 +216,7 @@ export function ProfilePicker<P extends Profile>({ idPrefix, host, profiles, sel
|
|
|
212
216
|
style={[
|
|
213
217
|
styles.rowLabel,
|
|
214
218
|
{
|
|
215
|
-
color: selected ? getContrastColor(
|
|
219
|
+
color: selected ? getContrastColor(rowColor) : fg
|
|
216
220
|
}
|
|
217
221
|
]}
|
|
218
222
|
numberOfLines={1}
|
package/src/ProfilesManager.tsx
CHANGED
|
@@ -9,6 +9,7 @@ import { Button, Icon, Portal, Text, TextInput } from 'react-native-paper'
|
|
|
9
9
|
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
|
10
10
|
|
|
11
11
|
import { ProfileChip } from './ProfileChip'
|
|
12
|
+
import { useProfileColorPreview } from './ProfileColorContext'
|
|
12
13
|
import { type ProfilePickerAlignResult } from './ProfilePicker'
|
|
13
14
|
import { clampTag, isValidTag, MAX_PROFILE_NAME_LENGTH } from './profilesValidation'
|
|
14
15
|
import { Profile } from './types'
|
|
@@ -78,7 +79,7 @@ export interface ProfilesManagerProps {
|
|
|
78
79
|
titleVariant?: MD3TextVariant
|
|
79
80
|
// Transforms the color swatch grid's own preview per-swatch — forwarded straight to the internal
|
|
80
81
|
// InlineColorPicker's identical prop (see its own doc). Leaves draftColor/onCreate/onSave's actual
|
|
81
|
-
// color value untouched; only what's rendered changes.
|
|
82
|
+
// color value untouched; only what's rendered changes. Defaults to ProfileColorProvider's preview.
|
|
82
83
|
colorPreview?: (hex: string) => string
|
|
83
84
|
// Substitutes the color swatch popover's own placement, for a host that renders this screen inside a
|
|
84
85
|
// FAKE-rotated frame (a View turned +-90deg while the OS stays portrait-locked). @tastic/hud's
|
|
@@ -114,7 +115,11 @@ function deriveTag(name: string): string {
|
|
|
114
115
|
// really unmounts it on a given host's router — see ProfilesManagerHandle's own doc for the
|
|
115
116
|
// react-navigation-web case where it doesn't, and why a host there needs the `ref` escape hatch
|
|
116
117
|
// instead of relying on the unmount fallback alone.
|
|
117
|
-
export function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, headerLeft, ref, fg: fgOverride, fgMuted: fgMutedOverride, cardBg: cardBgOverride, titleVariant = 'headlineSmall', colorPreview, useColorPickerAlign }: ProfilesManagerProps) {
|
|
118
|
+
export function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, headerLeft, ref, fg: fgOverride, fgMuted: fgMutedOverride, cardBg: cardBgOverride, titleVariant = 'headlineSmall', colorPreview: colorPreviewProp, useColorPickerAlign }: ProfilesManagerProps) {
|
|
119
|
+
// An explicit prop wins; otherwise the app-wide ProfileColorProvider's, so the swatch previews
|
|
120
|
+
// match the ProfileChips drawn in the list.
|
|
121
|
+
const contextColorPreview = useProfileColorPreview()
|
|
122
|
+
const colorPreview = colorPreviewProp ?? contextColorPreview
|
|
118
123
|
const { dark, colors } = useAutoPaperTheme()
|
|
119
124
|
const insets = useSafeAreaInsets()
|
|
120
125
|
const { error: showErrorToast } = useToast()
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { ProfileChip } from './ProfileChip'
|
|
2
|
+
export { ProfileColorProvider, useProfileColorPreview, useProfileDisplayColor } from './ProfileColorContext'
|
|
2
3
|
export { getProfilePickerContentSize, ProfilePicker, type ProfilePickerAlignResult, type ProfilePickerContentSize } from './ProfilePicker'
|
|
3
4
|
export { type ProfileEditPatch, ProfilesManager, type ProfilesManagerHandle, type ProfilesManagerProps } from './ProfilesManager'
|
|
4
5
|
export { ProfilesScreen, type ProfilesScreenProps } from './ProfilesScreen'
|
|
@@ -14,6 +14,7 @@ export interface SelectProfilePayload<TSeat> {
|
|
|
14
14
|
|
|
15
15
|
export interface ProfileSelectionActions<TSeat> {
|
|
16
16
|
clearProfile: ActionCreator<string>
|
|
17
|
+
pruneToRoster: ActionCreator<string[]>
|
|
17
18
|
select: ActionCreator<SelectProfilePayload<TSeat>>
|
|
18
19
|
}
|
|
19
20
|
|
|
@@ -50,9 +51,15 @@ export interface ProfileSelectionSlice<TSeat extends string | number> {
|
|
|
50
51
|
// from inside its own reducer function — only worth overriding if some future consumer ever mounts
|
|
51
52
|
// this under a different key. Note `namespace` above is a different thing: it's the app's own name
|
|
52
53
|
// (e.g. 'airhockey', 'boxhockey'), used only to prefix action types, not the mount key.
|
|
54
|
+
//
|
|
55
|
+
// `pruneToRoster(ids)` nulls every seat whose profile id is no longer in the given roster. Call it
|
|
56
|
+
// after any profilesActions.setAll (initial reconcile, onRemoteChange), since a sibling app can
|
|
57
|
+
// delete a profile without this app's own deleteProfile ever running. It returns the same state
|
|
58
|
+
// object when nothing changes, so dispatching it on every foreground resume costs no re-render.
|
|
53
59
|
export function createProfileSelectionSlice<TSeat extends string | number>(namespace: string, defaultState: ProfileSelectionState<TSeat>, mountKey = 'profileSelection'): ProfileSelectionSlice<TSeat> {
|
|
54
60
|
const select = createActionCreator<SelectProfilePayload<TSeat>>(`profileSelection/${namespace}/select`)
|
|
55
61
|
const clearProfile = createActionCreator<string>(`profileSelection/${namespace}/clearProfile`)
|
|
62
|
+
const pruneToRoster = createActionCreator<string[]>(`profileSelection/${namespace}/pruneToRoster`)
|
|
56
63
|
|
|
57
64
|
function reducer(state: ProfileSelectionState<TSeat> = defaultState, action: { type: string; payload?: unknown }): ProfileSelectionState<TSeat> {
|
|
58
65
|
// redux-persist's own default stateReconciler (autoMergeLevel1) HARD-REPLACES this slice's
|
|
@@ -74,8 +81,21 @@ export function createProfileSelectionSlice<TSeat extends string | number>(names
|
|
|
74
81
|
}
|
|
75
82
|
return next
|
|
76
83
|
}
|
|
84
|
+
if (pruneToRoster.match(action)) {
|
|
85
|
+
const roster = new Set(action.payload)
|
|
86
|
+
const next = { ...state }
|
|
87
|
+
let changed = false
|
|
88
|
+
for (const seat of Object.keys(state) as TSeat[]) {
|
|
89
|
+
const profileId = state[seat]
|
|
90
|
+
if (profileId !== null && !roster.has(profileId)) {
|
|
91
|
+
next[seat] = null
|
|
92
|
+
changed = true
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return changed ? next : state
|
|
96
|
+
}
|
|
77
97
|
return state
|
|
78
98
|
}
|
|
79
99
|
|
|
80
|
-
return { actions: { clearProfile, select }, reducer }
|
|
100
|
+
return { actions: { clearProfile, pruneToRoster, select }, reducer }
|
|
81
101
|
}
|