@tastic/profile 0.8.0 → 0.10.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 +42 -0
- package/dist/index.d.mts +14 -4
- package/dist/index.d.ts +14 -4
- package/dist/index.js +113 -79
- package/dist/index.mjs +77 -46
- package/package.json +3 -3
- package/src/ProfileChip.tsx +5 -2
- package/src/ProfileColorContext.tsx +31 -0
- package/src/ProfilePicker.tsx +6 -2
- package/src/ProfilesManager.tsx +41 -10
- package/src/ProfilesScreen.tsx +7 -3
- package/src/index.ts +1 -0
package/README.md
CHANGED
|
@@ -136,6 +136,28 @@ const placement = useMyRotatedAlign(open, width, height) // returns a ProfilePic
|
|
|
136
136
|
wide, and `8 * 2 + (profileCount + 1 + (hasManageRow ? 1 : 0)) * 36` tall (the null/"guest" row, one
|
|
137
137
|
row per profile, and the Manage row when `onManage` is passed).
|
|
138
138
|
|
|
139
|
+
|
|
140
|
+
#### The Profiles screen's color swatch: `useColorPickerAlign`
|
|
141
|
+
|
|
142
|
+
`ProfilesManager` (and `ProfilesScreen`, which forwards it) renders `@tastic/hud`'s `InlineColorPicker`
|
|
143
|
+
for the swatch popover in an edit row, and owns the popover state internally — so the host cannot pass
|
|
144
|
+
`alignOverride` to it directly. Inside a fake-rotated frame that popover has the same misplacement (and
|
|
145
|
+
`InlineColorPicker` also feeds `useAutoAlign` its ambient rotation, double-rotating an already-transformed
|
|
146
|
+
rect). Pass a placement **hook** as `useColorPickerAlign`:
|
|
147
|
+
|
|
148
|
+
```tsx
|
|
149
|
+
<ProfilesScreen useColorPickerAlign={useFrameAutoAlign} /* ...the usual props */ />
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
It is called as `useColorPickerAlign(open, contentWidth, contentHeight)` — `open` is whether the swatch
|
|
153
|
+
popover is showing, and the size is what `InlineColorPicker` itself computes (`@tastic/hud`'s
|
|
154
|
+
`getInlineColorPickerContentSize` for the default swatch list at the raw window width) — and must return a
|
|
155
|
+
`ProfilePickerAlignResult`, which is handed to `InlineColorPicker` as its `alignOverride` (its `triggerRef`
|
|
156
|
+
is the ref that gets measured). It is a hook, not a plain function: give it a stable identity for the life of
|
|
157
|
+
the mount (a module-level function), since switching it at runtime remounts the swatch. Omit it and behaviour is
|
|
158
|
+
identical to before. **Needs `@tastic/hud` >= 0.12.0** to have any effect — older hud has no `alignOverride`
|
|
159
|
+
on `InlineColorPicker` and ignores it.
|
|
160
|
+
|
|
139
161
|
### Routed screens: `ProfilesScreen`
|
|
140
162
|
|
|
141
163
|
`ProfilesScreen` is the routed-screen shell every app in the fleet was hand-rolling on its own
|
|
@@ -231,6 +253,26 @@ so a host app plugs the reducer(s) into whatever store it already has:
|
|
|
231
253
|
ghost id. Always local, like the guest/CPU/override colors below — never part of the shared
|
|
232
254
|
App Group roster, since who's selected on *this* device is naturally per-app, per-device state.
|
|
233
255
|
|
|
256
|
+
## Muted profile colors (`ProfileColorProvider`)
|
|
257
|
+
|
|
258
|
+
An app that draws profile colors through a transform (Solitaire blends them toward its muted
|
|
259
|
+
card-back palette) wraps its tree once:
|
|
260
|
+
|
|
261
|
+
```tsx
|
|
262
|
+
import { ProfileColorProvider } from '@tastic/profile'
|
|
263
|
+
|
|
264
|
+
const preview = useCallback((hex: string) => blendTowardCardBack(hex, dark), [dark])
|
|
265
|
+
<ProfileColorProvider preview={preview}>{children}</ProfileColorProvider>
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Every `ProfileChip` then draws `preview(profile.color)` (contrast computed against that),
|
|
269
|
+
`ProfilePicker`'s selected row fills with it, and `ProfilesManager`/`ProfilesScreen` use it for
|
|
270
|
+
their swatch previews unless an explicit `colorPreview` prop is passed. Components from other
|
|
271
|
+
packages that render a `ProfileChip` (e.g. `@tastic/hud`'s `AchievementUnlockList`) pick it up
|
|
272
|
+
for free. Only what's drawn changes: saved colors, `onCreate`/`onSave` values and selection are
|
|
273
|
+
untouched. `useProfileColorPreview()` returns the provider's function (or `undefined`), and
|
|
274
|
+
`useProfileDisplayColor(hex)` resolves one color. Without a provider nothing changes.
|
|
275
|
+
|
|
234
276
|
## Per-seat color persistence
|
|
235
277
|
|
|
236
278
|
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';
|
|
@@ -72,8 +80,9 @@ interface ProfilesManagerProps {
|
|
|
72
80
|
cardBg?: string;
|
|
73
81
|
titleVariant?: MD3TextVariant$1;
|
|
74
82
|
colorPreview?: (hex: string) => string;
|
|
83
|
+
useColorPickerAlign?: (open: boolean, contentWidth: number, contentHeight: number) => ProfilePickerAlignResult;
|
|
75
84
|
}
|
|
76
|
-
declare function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, headerLeft, ref, fg: fgOverride, fgMuted: fgMutedOverride, cardBg: cardBgOverride, titleVariant, colorPreview }: 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;
|
|
77
86
|
|
|
78
87
|
type MD3TextVariant = 'displayLarge' | 'displayMedium' | 'displaySmall' | 'headlineLarge' | 'headlineMedium' | 'headlineSmall' | 'titleLarge' | 'titleMedium' | 'titleSmall' | 'labelLarge' | 'labelMedium' | 'labelSmall' | 'bodyLarge' | 'bodyMedium' | 'bodySmall';
|
|
79
88
|
interface ProfilesScreenProps {
|
|
@@ -89,8 +98,9 @@ interface ProfilesScreenProps {
|
|
|
89
98
|
cardBg?: string;
|
|
90
99
|
titleVariant?: MD3TextVariant;
|
|
91
100
|
colorPreview?: (hex: string) => string;
|
|
101
|
+
useColorPickerAlign?: ProfilesManagerProps['useColorPickerAlign'];
|
|
92
102
|
}
|
|
93
|
-
declare function ProfilesScreen({ profiles, defaultColor, onCreate, onSave, onDelete, onBack, fg: fgOverride, fgMuted, bg: bgOverride, cardBg, titleVariant, colorPreview }: ProfilesScreenProps): react.JSX.Element;
|
|
103
|
+
declare function ProfilesScreen({ profiles, defaultColor, onCreate, onSave, onDelete, onBack, fg: fgOverride, fgMuted, bg: bgOverride, cardBg, titleVariant, colorPreview, useColorPickerAlign }: ProfilesScreenProps): react.JSX.Element;
|
|
94
104
|
|
|
95
105
|
declare const MAX_PROFILE_NAME_LENGTH = 20;
|
|
96
106
|
declare const MAX_TAG_LENGTH = 3;
|
|
@@ -205,4 +215,4 @@ declare const isSharedProfileStoreAvailable: boolean;
|
|
|
205
215
|
declare function loadSharedProfiles(groupId: string): Promise<Profile[]>;
|
|
206
216
|
declare function saveSharedProfiles(groupId: string, profiles: Profile[]): Promise<void>;
|
|
207
217
|
|
|
208
|
-
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 };
|
|
218
|
+
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';
|
|
@@ -72,8 +80,9 @@ interface ProfilesManagerProps {
|
|
|
72
80
|
cardBg?: string;
|
|
73
81
|
titleVariant?: MD3TextVariant$1;
|
|
74
82
|
colorPreview?: (hex: string) => string;
|
|
83
|
+
useColorPickerAlign?: (open: boolean, contentWidth: number, contentHeight: number) => ProfilePickerAlignResult;
|
|
75
84
|
}
|
|
76
|
-
declare function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, headerLeft, ref, fg: fgOverride, fgMuted: fgMutedOverride, cardBg: cardBgOverride, titleVariant, colorPreview }: 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;
|
|
77
86
|
|
|
78
87
|
type MD3TextVariant = 'displayLarge' | 'displayMedium' | 'displaySmall' | 'headlineLarge' | 'headlineMedium' | 'headlineSmall' | 'titleLarge' | 'titleMedium' | 'titleSmall' | 'labelLarge' | 'labelMedium' | 'labelSmall' | 'bodyLarge' | 'bodyMedium' | 'bodySmall';
|
|
79
88
|
interface ProfilesScreenProps {
|
|
@@ -89,8 +98,9 @@ interface ProfilesScreenProps {
|
|
|
89
98
|
cardBg?: string;
|
|
90
99
|
titleVariant?: MD3TextVariant;
|
|
91
100
|
colorPreview?: (hex: string) => string;
|
|
101
|
+
useColorPickerAlign?: ProfilesManagerProps['useColorPickerAlign'];
|
|
92
102
|
}
|
|
93
|
-
declare function ProfilesScreen({ profiles, defaultColor, onCreate, onSave, onDelete, onBack, fg: fgOverride, fgMuted, bg: bgOverride, cardBg, titleVariant, colorPreview }: ProfilesScreenProps): react.JSX.Element;
|
|
103
|
+
declare function ProfilesScreen({ profiles, defaultColor, onCreate, onSave, onDelete, onBack, fg: fgOverride, fgMuted, bg: bgOverride, cardBg, titleVariant, colorPreview, useColorPickerAlign }: ProfilesScreenProps): react.JSX.Element;
|
|
94
104
|
|
|
95
105
|
declare const MAX_PROFILE_NAME_LENGTH = 20;
|
|
96
106
|
declare const MAX_TAG_LENGTH = 3;
|
|
@@ -205,4 +215,4 @@ declare const isSharedProfileStoreAvailable: boolean;
|
|
|
205
215
|
declare function loadSharedProfiles(groupId: string): Promise<Profile[]>;
|
|
206
216
|
declare function saveSharedProfiles(groupId: string, profiles: Profile[]): Promise<void>;
|
|
207
217
|
|
|
208
|
-
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 };
|
|
218
|
+
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 }) {
|
|
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,80 +379,86 @@ 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
|
}
|
|
415
|
-
function
|
|
440
|
+
function AlignedColorPicker({ useAlign, ...props }) {
|
|
441
|
+
const { width: windowWidth } = (0, import_react_native3.useWindowDimensions)();
|
|
442
|
+
const { width, height } = (0, import_hud2.getInlineColorPickerContentSize)(import_auto_paper3.defaultColors.length, windowWidth);
|
|
443
|
+
const alignOverride = useAlign(props.host.openId === props.id, width, height);
|
|
444
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_hud2.InlineColorPicker, { ...props, alignOverride });
|
|
445
|
+
}
|
|
446
|
+
function EditRow({ draftName, onNameChange, draftColor, onColorChange, draftTag, onTagChange, onSubmit, onDelete, autoFocus, fgMuted, host, dark, colorPreview, useColorPickerAlign }) {
|
|
416
447
|
const register = (0, import_focus_chain.useFocusChain)();
|
|
417
448
|
const tag = register();
|
|
418
449
|
const name = register();
|
|
419
|
-
const tagFocusRef = (0,
|
|
420
|
-
(0,
|
|
450
|
+
const tagFocusRef = (0, import_react3.useRef)(tag.props.focus);
|
|
451
|
+
(0, import_react3.useEffect)(() => {
|
|
421
452
|
tagFocusRef.current = tag.props.focus;
|
|
422
453
|
});
|
|
423
|
-
(0,
|
|
454
|
+
(0, import_react3.useEffect)(() => {
|
|
424
455
|
if (host.openId === "color") tagFocusRef.current();
|
|
425
456
|
}, [host.openId]);
|
|
426
|
-
return /* @__PURE__ */ (0,
|
|
427
|
-
/* @__PURE__ */ (0,
|
|
428
|
-
/* @__PURE__ */ (0,
|
|
429
|
-
/* @__PURE__ */ (0,
|
|
430
|
-
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 }) })
|
|
431
462
|
] });
|
|
432
463
|
}
|
|
433
464
|
var styles3 = import_react_native3.StyleSheet.create({
|
|
@@ -526,19 +557,19 @@ var styles3 = import_react_native3.StyleSheet.create({
|
|
|
526
557
|
// src/ProfilesScreen.tsx
|
|
527
558
|
var import_auto_paper4 = require("@rific/auto-paper");
|
|
528
559
|
var import_feedback_press3 = require("@rific/feedback-press");
|
|
529
|
-
var
|
|
560
|
+
var import_react4 = require("react");
|
|
530
561
|
var import_react_native4 = require("react-native");
|
|
531
|
-
var
|
|
532
|
-
function ProfilesScreen({ profiles, defaultColor, onCreate, onSave, onDelete, onBack, fg: fgOverride, fgMuted, bg: bgOverride, cardBg, titleVariant, colorPreview }) {
|
|
562
|
+
var import_jsx_runtime5 = require("react/jsx-runtime");
|
|
563
|
+
function ProfilesScreen({ profiles, defaultColor, onCreate, onSave, onDelete, onBack, fg: fgOverride, fgMuted, bg: bgOverride, cardBg, titleVariant, colorPreview, useColorPickerAlign }) {
|
|
533
564
|
const { dark } = (0, import_auto_paper4.useAutoPaperTheme)();
|
|
534
|
-
const managerRef = (0,
|
|
565
|
+
const managerRef = (0, import_react4.useRef)(null);
|
|
535
566
|
const fg = fgOverride ?? (dark ? "#FFFFFF" : "#000000");
|
|
536
567
|
const bg = bgOverride ?? (dark ? "#000000" : "#FFFFFF");
|
|
537
568
|
const handleBack = () => {
|
|
538
569
|
managerRef.current?.commitPendingEdit();
|
|
539
570
|
onBack();
|
|
540
571
|
};
|
|
541
|
-
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" }) }) });
|
|
542
573
|
}
|
|
543
574
|
var styles4 = import_react_native4.StyleSheet.create({
|
|
544
575
|
container: {
|
|
@@ -684,15 +715,15 @@ function createSeatColorsSlice(namespace, defaultState, mountKey = "seatColors")
|
|
|
684
715
|
}
|
|
685
716
|
|
|
686
717
|
// src/redux/useSharedProfilesSync.ts
|
|
687
|
-
var
|
|
718
|
+
var import_react5 = require("react");
|
|
688
719
|
var import_react_native5 = require("react-native");
|
|
689
720
|
function useSharedProfilesSync({ groupId, onRemoteChange }) {
|
|
690
|
-
const pendingWriteRef = (0,
|
|
691
|
-
const onRemoteChangeRef = (0,
|
|
692
|
-
(0,
|
|
721
|
+
const pendingWriteRef = (0, import_react5.useRef)(false);
|
|
722
|
+
const onRemoteChangeRef = (0, import_react5.useRef)(onRemoteChange);
|
|
723
|
+
(0, import_react5.useEffect)(() => {
|
|
693
724
|
onRemoteChangeRef.current = onRemoteChange;
|
|
694
725
|
}, [onRemoteChange]);
|
|
695
|
-
(0,
|
|
726
|
+
(0, import_react5.useEffect)(() => {
|
|
696
727
|
if (!isSharedProfileStoreAvailable) return;
|
|
697
728
|
let cancelled = false;
|
|
698
729
|
const sub = import_react_native5.AppState.addEventListener("change", (next) => {
|
|
@@ -706,7 +737,7 @@ function useSharedProfilesSync({ groupId, onRemoteChange }) {
|
|
|
706
737
|
sub.remove();
|
|
707
738
|
};
|
|
708
739
|
}, [groupId]);
|
|
709
|
-
const syncToShared = (0,
|
|
740
|
+
const syncToShared = (0, import_react5.useCallback)(
|
|
710
741
|
(next) => {
|
|
711
742
|
if (!isSharedProfileStoreAvailable) return;
|
|
712
743
|
pendingWriteRef.current = true;
|
|
@@ -724,6 +755,7 @@ function useSharedProfilesSync({ groupId, onRemoteChange }) {
|
|
|
724
755
|
MAX_PROFILE_NAME_LENGTH,
|
|
725
756
|
MAX_TAG_LENGTH,
|
|
726
757
|
ProfileChip,
|
|
758
|
+
ProfileColorProvider,
|
|
727
759
|
ProfilePicker,
|
|
728
760
|
ProfilesManager,
|
|
729
761
|
ProfilesScreen,
|
|
@@ -740,5 +772,7 @@ function useSharedProfilesSync({ groupId, onRemoteChange }) {
|
|
|
740
772
|
profilesReducer,
|
|
741
773
|
resolveInitialProfiles,
|
|
742
774
|
saveSharedProfiles,
|
|
775
|
+
useProfileColorPreview,
|
|
776
|
+
useProfileDisplayColor,
|
|
743
777
|
useSharedProfilesSync
|
|
744
778
|
});
|
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
|
]
|
|
@@ -215,13 +235,13 @@ var styles2 = StyleSheet2.create({
|
|
|
215
235
|
});
|
|
216
236
|
|
|
217
237
|
// src/ProfilesManager.tsx
|
|
218
|
-
import { useAutoPaperTheme } from "@rific/auto-paper";
|
|
238
|
+
import { defaultColors, useAutoPaperTheme } from "@rific/auto-paper";
|
|
219
239
|
import { TouchableRipple as TouchableRipple2 } from "@rific/feedback-press";
|
|
220
240
|
import { useFocusChain } from "@rific/focus-chain";
|
|
221
241
|
import { useToast } from "@rific/toaster";
|
|
222
|
-
import { InlineColorPicker, usePopoverHost } from "@tastic/hud";
|
|
242
|
+
import { getInlineColorPickerContentSize, InlineColorPicker, usePopoverHost } from "@tastic/hud";
|
|
223
243
|
import { useCallback, useEffect, useImperativeHandle, useMemo as useMemo2, useRef, useState } from "react";
|
|
224
|
-
import { KeyboardAvoidingView, Platform, ScrollView as ScrollView2, StyleSheet as StyleSheet3, View as View3 } from "react-native";
|
|
244
|
+
import { KeyboardAvoidingView, Platform, ScrollView as ScrollView2, StyleSheet as StyleSheet3, useWindowDimensions, View as View3 } from "react-native";
|
|
225
245
|
import { Button, Icon as Icon3, Portal, Text as Text3, TextInput } from "react-native-paper";
|
|
226
246
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
227
247
|
|
|
@@ -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 }) {
|
|
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,41 +362,47 @@ 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
|
}
|
|
377
|
-
function
|
|
399
|
+
function AlignedColorPicker({ useAlign, ...props }) {
|
|
400
|
+
const { width: windowWidth } = useWindowDimensions();
|
|
401
|
+
const { width, height } = getInlineColorPickerContentSize(defaultColors.length, windowWidth);
|
|
402
|
+
const alignOverride = useAlign(props.host.openId === props.id, width, height);
|
|
403
|
+
return /* @__PURE__ */ jsx4(InlineColorPicker, { ...props, alignOverride });
|
|
404
|
+
}
|
|
405
|
+
function EditRow({ draftName, onNameChange, draftColor, onColorChange, draftTag, onTagChange, onSubmit, onDelete, autoFocus, fgMuted, host, dark, colorPreview, useColorPickerAlign }) {
|
|
378
406
|
const register = useFocusChain();
|
|
379
407
|
const tag = register();
|
|
380
408
|
const name = register();
|
|
@@ -386,10 +414,10 @@ function EditRow({ draftName, onNameChange, draftColor, onColorChange, draftTag,
|
|
|
386
414
|
if (host.openId === "color") tagFocusRef.current();
|
|
387
415
|
}, [host.openId]);
|
|
388
416
|
return /* @__PURE__ */ jsxs2(View3, { style: styles3.editRow, children: [
|
|
389
|
-
/* @__PURE__ */
|
|
390
|
-
/* @__PURE__ */
|
|
391
|
-
/* @__PURE__ */
|
|
392
|
-
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 }) })
|
|
393
421
|
] });
|
|
394
422
|
}
|
|
395
423
|
var styles3 = StyleSheet3.create({
|
|
@@ -490,8 +518,8 @@ import { useAutoPaperTheme as useAutoPaperTheme2 } from "@rific/auto-paper";
|
|
|
490
518
|
import { IconButton } from "@rific/feedback-press";
|
|
491
519
|
import { useRef as useRef2 } from "react";
|
|
492
520
|
import { StyleSheet as StyleSheet4, View as View4 } from "react-native";
|
|
493
|
-
import { jsx as
|
|
494
|
-
function ProfilesScreen({ profiles, defaultColor, onCreate, onSave, onDelete, onBack, fg: fgOverride, fgMuted, bg: bgOverride, cardBg, titleVariant, colorPreview }) {
|
|
521
|
+
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
522
|
+
function ProfilesScreen({ profiles, defaultColor, onCreate, onSave, onDelete, onBack, fg: fgOverride, fgMuted, bg: bgOverride, cardBg, titleVariant, colorPreview, useColorPickerAlign }) {
|
|
495
523
|
const { dark } = useAutoPaperTheme2();
|
|
496
524
|
const managerRef = useRef2(null);
|
|
497
525
|
const fg = fgOverride ?? (dark ? "#FFFFFF" : "#000000");
|
|
@@ -500,7 +528,7 @@ function ProfilesScreen({ profiles, defaultColor, onCreate, onSave, onDelete, on
|
|
|
500
528
|
managerRef.current?.commitPendingEdit();
|
|
501
529
|
onBack();
|
|
502
530
|
};
|
|
503
|
-
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" }) }) });
|
|
504
532
|
}
|
|
505
533
|
var styles4 = StyleSheet4.create({
|
|
506
534
|
container: {
|
|
@@ -685,6 +713,7 @@ export {
|
|
|
685
713
|
MAX_PROFILE_NAME_LENGTH,
|
|
686
714
|
MAX_TAG_LENGTH,
|
|
687
715
|
ProfileChip,
|
|
716
|
+
ProfileColorProvider,
|
|
688
717
|
ProfilePicker,
|
|
689
718
|
ProfilesManager,
|
|
690
719
|
ProfilesScreen,
|
|
@@ -701,5 +730,7 @@ export {
|
|
|
701
730
|
profilesReducer,
|
|
702
731
|
resolveInitialProfiles,
|
|
703
732
|
saveSharedProfiles,
|
|
733
|
+
useProfileColorPreview,
|
|
734
|
+
useProfileDisplayColor,
|
|
704
735
|
useSharedProfilesSync
|
|
705
736
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tastic/profile",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.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",
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
"@rific/feedback-press": "^0.12.0",
|
|
71
71
|
"@rific/focus-chain": "^0.4.7",
|
|
72
72
|
"@rific/toaster": "^0.7.0",
|
|
73
|
-
"@tastic/core": "^0.
|
|
73
|
+
"@tastic/core": "^0.9.0",
|
|
74
74
|
"@tastic/hud": "^0.12.0",
|
|
75
75
|
"@testing-library/dom": "^10.4.1",
|
|
76
76
|
"@testing-library/react": "^16.3.2",
|
|
@@ -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
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
import { useAutoPaperTheme } from '@rific/auto-paper'
|
|
1
|
+
import { defaultColors, useAutoPaperTheme } from '@rific/auto-paper'
|
|
2
2
|
import { TouchableRipple } from '@rific/feedback-press'
|
|
3
3
|
import { useFocusChain } from '@rific/focus-chain'
|
|
4
4
|
import { useToast } from '@rific/toaster'
|
|
5
|
-
import { InlineColorPicker, PopoverHost, usePopoverHost } from '@tastic/hud'
|
|
6
|
-
import { ReactNode, Ref, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'
|
|
7
|
-
import { KeyboardAvoidingView, Platform, ScrollView, StyleSheet, View } from 'react-native'
|
|
5
|
+
import { getInlineColorPickerContentSize, InlineColorPicker, PopoverHost, usePopoverHost } from '@tastic/hud'
|
|
6
|
+
import { ComponentProps, ReactNode, Ref, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'
|
|
7
|
+
import { KeyboardAvoidingView, Platform, ScrollView, StyleSheet, useWindowDimensions, View } from 'react-native'
|
|
8
8
|
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'
|
|
13
|
+
import { type ProfilePickerAlignResult } from './ProfilePicker'
|
|
12
14
|
import { clampTag, isValidTag, MAX_PROFILE_NAME_LENGTH } from './profilesValidation'
|
|
13
15
|
import { Profile } from './types'
|
|
14
16
|
|
|
@@ -77,8 +79,20 @@ export interface ProfilesManagerProps {
|
|
|
77
79
|
titleVariant?: MD3TextVariant
|
|
78
80
|
// Transforms the color swatch grid's own preview per-swatch — forwarded straight to the internal
|
|
79
81
|
// InlineColorPicker's identical prop (see its own doc). Leaves draftColor/onCreate/onSave's actual
|
|
80
|
-
// color value untouched; only what's rendered changes.
|
|
82
|
+
// color value untouched; only what's rendered changes. Defaults to ProfileColorProvider's preview.
|
|
81
83
|
colorPreview?: (hex: string) => string
|
|
84
|
+
// Substitutes the color swatch popover's own placement, for a host that renders this screen inside a
|
|
85
|
+
// FAKE-rotated frame (a View turned +-90deg while the OS stays portrait-locked). @tastic/hud's
|
|
86
|
+
// InlineColorPicker places its popover with useAutoAlign, which compares a screen-space
|
|
87
|
+
// measureInWindow rect against the raw portrait window and (through its ambient rotation) double-rotates
|
|
88
|
+
// an already-transformed rect - so under a fake rotation it lands off its trigger. A host that knows its
|
|
89
|
+
// frame passes its own placement HOOK here; it is called with the swatch popover's open state and content
|
|
90
|
+
// size, and its result is handed to InlineColorPicker as `alignOverride` (its `triggerRef` is what gets
|
|
91
|
+
// measured). It is a hook, not a plain function: give it a STABLE identity for the life of the mount (a
|
|
92
|
+
// module-level function) - switching it at runtime remounts the swatch. Omit for the ordinary case;
|
|
93
|
+
// behaviour is then identical to before. Needs @tastic/hud >= 0.12.0 to have any effect (older hud has no
|
|
94
|
+
// `alignOverride` and ignores it).
|
|
95
|
+
useColorPickerAlign?: (open: boolean, contentWidth: number, contentHeight: number) => ProfilePickerAlignResult
|
|
82
96
|
}
|
|
83
97
|
|
|
84
98
|
type Row = { kind: 'profile'; profile: Profile } | { kind: 'new' }
|
|
@@ -101,7 +115,11 @@ function deriveTag(name: string): string {
|
|
|
101
115
|
// really unmounts it on a given host's router — see ProfilesManagerHandle's own doc for the
|
|
102
116
|
// react-navigation-web case where it doesn't, and why a host there needs the `ref` escape hatch
|
|
103
117
|
// instead of relying on the unmount fallback alone.
|
|
104
|
-
export function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, headerLeft, ref, fg: fgOverride, fgMuted: fgMutedOverride, cardBg: cardBgOverride, titleVariant = 'headlineSmall', colorPreview }: 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
|
|
105
123
|
const { dark, colors } = useAutoPaperTheme()
|
|
106
124
|
const insets = useSafeAreaInsets()
|
|
107
125
|
const { error: showErrorToast } = useToast()
|
|
@@ -253,12 +271,12 @@ export function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDe
|
|
|
253
271
|
</TouchableRipple>
|
|
254
272
|
)
|
|
255
273
|
}
|
|
256
|
-
return <EditRow key='new' draftName={draftName} onNameChange={handleNameChange} draftColor={draftColor} onColorChange={setDraftColor} draftTag={draftTag} onTagChange={handleTagChange} onSubmit={commitEdit} autoFocus fgMuted={fgMuted} host={host} dark={dark} colorPreview={colorPreview} />
|
|
274
|
+
return <EditRow key='new' draftName={draftName} onNameChange={handleNameChange} draftColor={draftColor} onColorChange={setDraftColor} draftTag={draftTag} onTagChange={handleTagChange} onSubmit={commitEdit} autoFocus fgMuted={fgMuted} host={host} dark={dark} colorPreview={colorPreview} useColorPickerAlign={useColorPickerAlign} />
|
|
257
275
|
}
|
|
258
276
|
|
|
259
277
|
const { profile } = row
|
|
260
278
|
if (editingId === profile.id) {
|
|
261
|
-
return <EditRow key={profile.id} draftName={draftName} onNameChange={handleNameChange} draftColor={draftColor} onColorChange={setDraftColor} draftTag={draftTag} onTagChange={handleTagChange} onSubmit={commitEdit} onDelete={() => setConfirmDeleteId(profile.id)} fgMuted={fgMuted} host={host} dark={dark} colorPreview={colorPreview} />
|
|
279
|
+
return <EditRow key={profile.id} draftName={draftName} onNameChange={handleNameChange} draftColor={draftColor} onColorChange={setDraftColor} draftTag={draftTag} onTagChange={handleTagChange} onSubmit={commitEdit} onDelete={() => setConfirmDeleteId(profile.id)} fgMuted={fgMuted} host={host} dark={dark} colorPreview={colorPreview} useColorPickerAlign={useColorPickerAlign} />
|
|
262
280
|
}
|
|
263
281
|
|
|
264
282
|
// No delete affordance at rest — tap the row to start editing, which is the one place
|
|
@@ -323,6 +341,19 @@ interface EditRowProps {
|
|
|
323
341
|
host: PopoverHost
|
|
324
342
|
dark: boolean
|
|
325
343
|
colorPreview?: (hex: string) => string
|
|
344
|
+
useColorPickerAlign?: ProfilesManagerProps['useColorPickerAlign']
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// InlineColorPicker with its placement supplied by a host hook (see ProfilesManagerProps.useColorPickerAlign).
|
|
348
|
+
// A component of its own - rather than a conditional hook call inside EditRow - so the hook is always called
|
|
349
|
+
// unconditionally here, and the plain path below stays byte-identical to what it was. Sized exactly as
|
|
350
|
+
// InlineColorPicker sizes itself (the default swatch list at the raw window width), so the host's placement and
|
|
351
|
+
// the popover it positions agree on its footprint.
|
|
352
|
+
function AlignedColorPicker({ useAlign, ...props }: ComponentProps<typeof InlineColorPicker> & { useAlign: NonNullable<ProfilesManagerProps['useColorPickerAlign']> }) {
|
|
353
|
+
const { width: windowWidth } = useWindowDimensions()
|
|
354
|
+
const { width, height } = getInlineColorPickerContentSize(defaultColors.length, windowWidth)
|
|
355
|
+
const alignOverride = useAlign(props.host.openId === props.id, width, height)
|
|
356
|
+
return <InlineColorPicker {...props} alignOverride={alignOverride} />
|
|
326
357
|
}
|
|
327
358
|
|
|
328
359
|
// One row, expanded: the color trigger (@tastic/hud's InlineColorPicker) — doubling as the tag's
|
|
@@ -337,7 +368,7 @@ interface EditRowProps {
|
|
|
337
368
|
// own — submitting the *name* field is what actually commits the row, so its registration's own
|
|
338
369
|
// onSubmitEditing (a no-op — it's last in the chain) is overridden with onSubmit below, matching
|
|
339
370
|
// how a normal multi-field form reads: fill fields in order, the last one finishes it.
|
|
340
|
-
function EditRow({ draftName, onNameChange, draftColor, onColorChange, draftTag, onTagChange, onSubmit, onDelete, autoFocus, fgMuted, host, dark, colorPreview }: EditRowProps) {
|
|
371
|
+
function EditRow({ draftName, onNameChange, draftColor, onColorChange, draftTag, onTagChange, onSubmit, onDelete, autoFocus, fgMuted, host, dark, colorPreview, useColorPickerAlign }: EditRowProps) {
|
|
341
372
|
const register = useFocusChain()
|
|
342
373
|
const tag = register()
|
|
343
374
|
const name = register()
|
|
@@ -364,7 +395,7 @@ function EditRow({ draftName, onNameChange, draftColor, onColorChange, draftTag,
|
|
|
364
395
|
|
|
365
396
|
return (
|
|
366
397
|
<View style={styles.editRow}>
|
|
367
|
-
<InlineColorPicker id='color' host={host} value={draftColor} onChange={onColorChange} previewValue={colorPreview} tag={draftTag} dark={dark} />
|
|
398
|
+
{useColorPickerAlign ? <AlignedColorPicker useAlign={useColorPickerAlign} id='color' host={host} value={draftColor} onChange={onColorChange} previewValue={colorPreview} tag={draftTag} dark={dark} /> : <InlineColorPicker id='color' host={host} value={draftColor} onChange={onColorChange} previewValue={colorPreview} tag={draftTag} dark={dark} />}
|
|
368
399
|
{/* Invisible on purpose (styles.hiddenTagInput: zero footprint, position: 'absolute' so it
|
|
369
400
|
doesn't reserve space in the row's own flex layout) — the swatch above is the only place this
|
|
370
401
|
value is ever meant to be seen. pointerEvents='none' is what actually keeps it out of the
|
package/src/ProfilesScreen.tsx
CHANGED
|
@@ -3,7 +3,7 @@ import { IconButton } from '@rific/feedback-press'
|
|
|
3
3
|
import { useRef } from 'react'
|
|
4
4
|
import { StyleSheet, View } from 'react-native'
|
|
5
5
|
|
|
6
|
-
import { ProfileEditPatch, ProfilesManager, ProfilesManagerHandle } from './ProfilesManager'
|
|
6
|
+
import { ProfileEditPatch, ProfilesManager, ProfilesManagerHandle, ProfilesManagerProps } from './ProfilesManager'
|
|
7
7
|
import { Profile } from './types'
|
|
8
8
|
|
|
9
9
|
// react-native-paper's own MD3 Text variant names — mirrored locally rather than importing its
|
|
@@ -40,6 +40,10 @@ export interface ProfilesScreenProps {
|
|
|
40
40
|
cardBg?: string
|
|
41
41
|
titleVariant?: MD3TextVariant
|
|
42
42
|
colorPreview?: (hex: string) => string
|
|
43
|
+
// Forwarded straight through to the internal ProfilesManager - a placement hook for the color swatch popover, for
|
|
44
|
+
// a host inside a fake-rotated frame. See ProfilesManagerProps.useColorPickerAlign for what it is and its hud >= 0.12.0
|
|
45
|
+
// requirement.
|
|
46
|
+
useColorPickerAlign?: ProfilesManagerProps['useColorPickerAlign']
|
|
43
47
|
}
|
|
44
48
|
|
|
45
49
|
// The routed profiles-roster screen shell every app in this fleet hand-rolls around ProfilesManager
|
|
@@ -49,7 +53,7 @@ export interface ProfilesScreenProps {
|
|
|
49
53
|
// still doesn't call useProfiles()/useGameStats() (or any host-specific hook) itself — those two
|
|
50
54
|
// hooks' shapes are genuinely app-specific (different CreateProfileInput/GameStatsContextValue per
|
|
51
55
|
// app), so this stays props-in/callbacks-out, exactly like ProfilesManager itself.
|
|
52
|
-
export function ProfilesScreen({ profiles, defaultColor, onCreate, onSave, onDelete, onBack, fg: fgOverride, fgMuted, bg: bgOverride, cardBg, titleVariant, colorPreview }: ProfilesScreenProps) {
|
|
56
|
+
export function ProfilesScreen({ profiles, defaultColor, onCreate, onSave, onDelete, onBack, fg: fgOverride, fgMuted, bg: bgOverride, cardBg, titleVariant, colorPreview, useColorPickerAlign }: ProfilesScreenProps) {
|
|
53
57
|
const { dark } = useAutoPaperTheme()
|
|
54
58
|
// Owns the ref itself — nothing outside this component ever needs it, since this is now the one
|
|
55
59
|
// thing that both renders ProfilesManager AND owns the only navigation trigger next to it.
|
|
@@ -71,7 +75,7 @@ export function ProfilesScreen({ profiles, defaultColor, onCreate, onSave, onDel
|
|
|
71
75
|
|
|
72
76
|
return (
|
|
73
77
|
<View style={[styles.container, { backgroundColor: bg }]}>
|
|
74
|
-
<ProfilesManager ref={managerRef} profiles={profiles} defaultColor={defaultColor} onCreate={onCreate} onSave={onSave} onDelete={onDelete} fg={fg} fgMuted={fgMuted} cardBg={cardBg} titleVariant={titleVariant} colorPreview={colorPreview} headerLeft={<IconButton icon='arrow-left' iconColor={fg} size={24} onPress={handleBack} accessibilityLabel='Back' />} />
|
|
78
|
+
<ProfilesManager ref={managerRef} profiles={profiles} defaultColor={defaultColor} onCreate={onCreate} onSave={onSave} onDelete={onDelete} fg={fg} fgMuted={fgMuted} cardBg={cardBg} titleVariant={titleVariant} colorPreview={colorPreview} useColorPickerAlign={useColorPickerAlign} headerLeft={<IconButton icon='arrow-left' iconColor={fg} size={24} onPress={handleBack} accessibilityLabel='Back' />} />
|
|
75
79
|
</View>
|
|
76
80
|
)
|
|
77
81
|
}
|
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'
|