@tastic/profile 0.7.0 → 0.9.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 +61 -1
- package/dist/index.d.mts +23 -6
- package/dist/index.d.ts +23 -6
- package/dist/index.js +25 -12
- package/dist/index.mjs +27 -15
- package/package.json +5 -5
- package/src/ProfilePicker.tsx +67 -7
- package/src/ProfilesManager.tsx +35 -9
- package/src/ProfilesScreen.tsx +7 -3
- package/src/index.ts +1 -1
package/README.md
CHANGED
|
@@ -49,7 +49,11 @@ see its own doc comment for why that component is generic over your own `Profile
|
|
|
49
49
|
- **`ProfilePicker`** — a name trigger + selection popover: pick an existing profile, fall back to a
|
|
50
50
|
null/"guest" selection, or (if `onManage` is passed) navigate to a management screen. Selection
|
|
51
51
|
only — creating, renaming, and deleting all live in `ProfilesManager`. Generic over your own
|
|
52
|
-
`Profile` type (see above).
|
|
52
|
+
`Profile` type (see above). Placement props (all optional, see "Fake-rotated frames" below):
|
|
53
|
+
`align` (force a horizontal side), `alignOverride` (substitute the whole placement result), and
|
|
54
|
+
`rotation` (forwarded to the automatic measurement). Also exports
|
|
55
|
+
`getProfilePickerContentSize(profileCount, hasManageRow?)` — the popover's
|
|
56
|
+
`{ width, height }`, for sizing your own placement hook.
|
|
53
57
|
- **`ProfilesManager`** — the roster screen itself: create/rename/recolor/retag/delete, editing
|
|
54
58
|
inline in the row you tap, with a destructive-delete confirmation. No navigation of its own (no
|
|
55
59
|
router, no back button) — a host app renders this as a routed screen's body and supplies its own
|
|
@@ -98,6 +102,62 @@ see its own doc comment for why that component is generic over your own `Profile
|
|
|
98
102
|
{ "expo": { "ios": { "entitlements": { "com.apple.security.application-groups": ["group.com.yourteam.yourgames"] } } } }
|
|
99
103
|
```
|
|
100
104
|
|
|
105
|
+
### Fake-rotated frames: `ProfilePicker` placement
|
|
106
|
+
|
|
107
|
+
`ProfilePicker` positions its popover with `@tastic/hud`'s `useAutoAlign`, which measures the
|
|
108
|
+
trigger with `measureInWindow` and compares that rect against the raw window. Inside a *fake*
|
|
109
|
+
rotation (a `View` turned +-90deg while the OS stays portrait-locked) the rect is in true screen
|
|
110
|
+
space but the frame is not, so the hook measures along the wrong axes and can pick the wrong side —
|
|
111
|
+
flipping the list upward off-screen, or running it past the frame's bottom. Two optional props give
|
|
112
|
+
you the same seam `@tastic/hud`'s `InlineColorPicker` has:
|
|
113
|
+
|
|
114
|
+
| Prop | Type | Default | Effect |
|
|
115
|
+
| --- | --- | --- | --- |
|
|
116
|
+
| `align` | `'left' \| 'right' \| 'center'` | — | Forces the horizontal side; always beats the placement's own `align`. |
|
|
117
|
+
| `alignOverride` | `ProfilePickerAlignResult` (`{ align, verticalAlign, maxHeight, measured, triggerRef }`, the same shape as `useAutoAlign`'s return / `@tastic/hud`'s `AlignResult`) | — | Replaces the automatic placement wholesale: its `align`/`verticalAlign`/`maxHeight`/`measured` drive the popover, and its `triggerRef` is the ref attached to the measured trigger. |
|
|
118
|
+
| `rotation` | `0 \| 90 \| -90 \| 180` | `0` | Forwarded to the automatic `useAutoAlign` call (needs `@tastic/hud` >= 0.8.0). Ignored when `alignOverride` is given. |
|
|
119
|
+
|
|
120
|
+
With none of them set the picker behaves exactly as it always has. Note `rotation` is never read
|
|
121
|
+
from an ambient provider here (unlike `@tastic/hud`'s own pickers) — this package doesn't touch
|
|
122
|
+
`@tastic/core` at runtime — so pass it explicitly.
|
|
123
|
+
|
|
124
|
+
To supply your own placement, run a rotation-aware placement hook against the popover's real size,
|
|
125
|
+
using `getProfilePickerContentSize` so the two can't drift:
|
|
126
|
+
|
|
127
|
+
```tsx
|
|
128
|
+
const open = host.openId === `${idPrefix}-profile`
|
|
129
|
+
const { width, height } = getProfilePickerContentSize(profiles.length, !!onManage)
|
|
130
|
+
const placement = useMyRotatedAlign(open, width, height) // returns a ProfilePickerAlignResult
|
|
131
|
+
|
|
132
|
+
<ProfilePicker idPrefix={idPrefix} host={host} profiles={profiles} /* ... */ alignOverride={placement} />
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
`getProfilePickerContentSize(profileCount, hasManageRow = false)` returns `{ width, height }`: 220
|
|
136
|
+
wide, and `8 * 2 + (profileCount + 1 + (hasManageRow ? 1 : 0)) * 36` tall (the null/"guest" row, one
|
|
137
|
+
row per profile, and the Manage row when `onManage` is passed).
|
|
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
|
+
|
|
101
161
|
### Routed screens: `ProfilesScreen`
|
|
102
162
|
|
|
103
163
|
`ProfilesScreen` is the routed-screen shell every app in the fleet was hand-rolling on its own
|
package/dist/index.d.mts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
|
-
import { ReactNode, Ref } from 'react';
|
|
3
|
-
import { PopoverHost } from '@tastic/hud';
|
|
2
|
+
import { RefObject, ReactNode, Ref } from 'react';
|
|
3
|
+
import { PopoverHost, PopoverRotation } from '@tastic/hud';
|
|
4
|
+
import { View } from 'react-native';
|
|
4
5
|
|
|
5
6
|
interface Profile {
|
|
6
7
|
id: string;
|
|
@@ -18,6 +19,18 @@ interface Props {
|
|
|
18
19
|
}
|
|
19
20
|
declare function ProfileChip({ profile, size, filled }: Props): react.JSX.Element;
|
|
20
21
|
|
|
22
|
+
interface ProfilePickerAlignResult {
|
|
23
|
+
align: 'left' | 'right' | 'center';
|
|
24
|
+
verticalAlign: 'below' | 'above';
|
|
25
|
+
maxHeight: number;
|
|
26
|
+
measured: boolean;
|
|
27
|
+
triggerRef: RefObject<View | null>;
|
|
28
|
+
}
|
|
29
|
+
interface ProfilePickerContentSize {
|
|
30
|
+
width: number;
|
|
31
|
+
height: number;
|
|
32
|
+
}
|
|
33
|
+
declare function getProfilePickerContentSize(profileCount: number, hasManageRow?: boolean): ProfilePickerContentSize;
|
|
21
34
|
interface ProfilePickerProps<P extends Profile> {
|
|
22
35
|
idPrefix: string;
|
|
23
36
|
host: PopoverHost;
|
|
@@ -27,13 +40,15 @@ interface ProfilePickerProps<P extends Profile> {
|
|
|
27
40
|
color: string;
|
|
28
41
|
dark: boolean;
|
|
29
42
|
align?: 'left' | 'right' | 'center';
|
|
43
|
+
alignOverride?: ProfilePickerAlignResult;
|
|
44
|
+
rotation?: PopoverRotation;
|
|
30
45
|
guestLabel: string;
|
|
31
46
|
nullLabel?: string;
|
|
32
47
|
nullIcon?: string;
|
|
33
48
|
onSelect: (profile: P | null) => void;
|
|
34
49
|
onManage?: () => void;
|
|
35
50
|
}
|
|
36
|
-
declare function ProfilePicker<P extends Profile>({ idPrefix, host, profiles, selectedId, takenId, color, dark, align: alignOverride, guestLabel, nullLabel, nullIcon, onSelect, onManage }: ProfilePickerProps<P>): react.JSX.Element;
|
|
51
|
+
declare function ProfilePicker<P extends Profile>({ idPrefix, host, profiles, selectedId, takenId, color, dark, align: forcedAlign, alignOverride, rotation, guestLabel, nullLabel, nullIcon, onSelect, onManage }: ProfilePickerProps<P>): react.JSX.Element;
|
|
37
52
|
|
|
38
53
|
type MD3TextVariant$1 = 'displayLarge' | 'displayMedium' | 'displaySmall' | 'headlineLarge' | 'headlineMedium' | 'headlineSmall' | 'titleLarge' | 'titleMedium' | 'titleSmall' | 'labelLarge' | 'labelMedium' | 'labelSmall' | 'bodyLarge' | 'bodyMedium' | 'bodySmall';
|
|
39
54
|
interface ProfileEditPatch {
|
|
@@ -57,8 +72,9 @@ interface ProfilesManagerProps {
|
|
|
57
72
|
cardBg?: string;
|
|
58
73
|
titleVariant?: MD3TextVariant$1;
|
|
59
74
|
colorPreview?: (hex: string) => string;
|
|
75
|
+
useColorPickerAlign?: (open: boolean, contentWidth: number, contentHeight: number) => ProfilePickerAlignResult;
|
|
60
76
|
}
|
|
61
|
-
declare function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, headerLeft, ref, fg: fgOverride, fgMuted: fgMutedOverride, cardBg: cardBgOverride, titleVariant, colorPreview }: ProfilesManagerProps): react.JSX.Element;
|
|
77
|
+
declare function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, headerLeft, ref, fg: fgOverride, fgMuted: fgMutedOverride, cardBg: cardBgOverride, titleVariant, colorPreview, useColorPickerAlign }: ProfilesManagerProps): react.JSX.Element;
|
|
62
78
|
|
|
63
79
|
type MD3TextVariant = 'displayLarge' | 'displayMedium' | 'displaySmall' | 'headlineLarge' | 'headlineMedium' | 'headlineSmall' | 'titleLarge' | 'titleMedium' | 'titleSmall' | 'labelLarge' | 'labelMedium' | 'labelSmall' | 'bodyLarge' | 'bodyMedium' | 'bodySmall';
|
|
64
80
|
interface ProfilesScreenProps {
|
|
@@ -74,8 +90,9 @@ interface ProfilesScreenProps {
|
|
|
74
90
|
cardBg?: string;
|
|
75
91
|
titleVariant?: MD3TextVariant;
|
|
76
92
|
colorPreview?: (hex: string) => string;
|
|
93
|
+
useColorPickerAlign?: ProfilesManagerProps['useColorPickerAlign'];
|
|
77
94
|
}
|
|
78
|
-
declare function ProfilesScreen({ profiles, defaultColor, onCreate, onSave, onDelete, onBack, fg: fgOverride, fgMuted, bg: bgOverride, cardBg, titleVariant, colorPreview }: ProfilesScreenProps): react.JSX.Element;
|
|
95
|
+
declare function ProfilesScreen({ profiles, defaultColor, onCreate, onSave, onDelete, onBack, fg: fgOverride, fgMuted, bg: bgOverride, cardBg, titleVariant, colorPreview, useColorPickerAlign }: ProfilesScreenProps): react.JSX.Element;
|
|
79
96
|
|
|
80
97
|
declare const MAX_PROFILE_NAME_LENGTH = 20;
|
|
81
98
|
declare const MAX_TAG_LENGTH = 3;
|
|
@@ -190,4 +207,4 @@ declare const isSharedProfileStoreAvailable: boolean;
|
|
|
190
207
|
declare function loadSharedProfiles(groupId: string): Promise<Profile[]>;
|
|
191
208
|
declare function saveSharedProfiles(groupId: string, profiles: Profile[]): Promise<void>;
|
|
192
209
|
|
|
193
|
-
export { type CreateProfileInput, MAX_PROFILE_NAME_LENGTH, MAX_TAG_LENGTH, type Profile, ProfileChip, type ProfileEditPatch, type ProfileExtensionActions, type ProfileExtensionSlice, type ProfileExtensionState, ProfilePicker, type ProfileSelectionActions, type ProfileSelectionSlice, type ProfileSelectionState, ProfilesManager, type ProfilesManagerHandle, type ProfilesManagerProps, ProfilesScreen, type ProfilesScreenProps, type SeatColorsActions, type SeatColorsSlice, type SeatColorsState, type SelectProfilePayload, type SetLastGuestColorPayload, type SetProfileExtensionPayload, type SetProfileOverridePayload, type UpdateProfilePayload, type UseSharedProfilesSyncOptions, type UseSharedProfilesSyncResult, createProfileExtensionSlice, createProfileRecord, createProfileSelectionSlice, createSeatColorsSlice, isSharedProfileStoreAvailable, isValidProfile, isValidTag, loadSharedProfiles, profilesActions, profilesReducer, resolveInitialProfiles, saveSharedProfiles, useSharedProfilesSync };
|
|
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 };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
|
-
import { ReactNode, Ref } from 'react';
|
|
3
|
-
import { PopoverHost } from '@tastic/hud';
|
|
2
|
+
import { RefObject, ReactNode, Ref } from 'react';
|
|
3
|
+
import { PopoverHost, PopoverRotation } from '@tastic/hud';
|
|
4
|
+
import { View } from 'react-native';
|
|
4
5
|
|
|
5
6
|
interface Profile {
|
|
6
7
|
id: string;
|
|
@@ -18,6 +19,18 @@ interface Props {
|
|
|
18
19
|
}
|
|
19
20
|
declare function ProfileChip({ profile, size, filled }: Props): react.JSX.Element;
|
|
20
21
|
|
|
22
|
+
interface ProfilePickerAlignResult {
|
|
23
|
+
align: 'left' | 'right' | 'center';
|
|
24
|
+
verticalAlign: 'below' | 'above';
|
|
25
|
+
maxHeight: number;
|
|
26
|
+
measured: boolean;
|
|
27
|
+
triggerRef: RefObject<View | null>;
|
|
28
|
+
}
|
|
29
|
+
interface ProfilePickerContentSize {
|
|
30
|
+
width: number;
|
|
31
|
+
height: number;
|
|
32
|
+
}
|
|
33
|
+
declare function getProfilePickerContentSize(profileCount: number, hasManageRow?: boolean): ProfilePickerContentSize;
|
|
21
34
|
interface ProfilePickerProps<P extends Profile> {
|
|
22
35
|
idPrefix: string;
|
|
23
36
|
host: PopoverHost;
|
|
@@ -27,13 +40,15 @@ interface ProfilePickerProps<P extends Profile> {
|
|
|
27
40
|
color: string;
|
|
28
41
|
dark: boolean;
|
|
29
42
|
align?: 'left' | 'right' | 'center';
|
|
43
|
+
alignOverride?: ProfilePickerAlignResult;
|
|
44
|
+
rotation?: PopoverRotation;
|
|
30
45
|
guestLabel: string;
|
|
31
46
|
nullLabel?: string;
|
|
32
47
|
nullIcon?: string;
|
|
33
48
|
onSelect: (profile: P | null) => void;
|
|
34
49
|
onManage?: () => void;
|
|
35
50
|
}
|
|
36
|
-
declare function ProfilePicker<P extends Profile>({ idPrefix, host, profiles, selectedId, takenId, color, dark, align: alignOverride, guestLabel, nullLabel, nullIcon, onSelect, onManage }: ProfilePickerProps<P>): react.JSX.Element;
|
|
51
|
+
declare function ProfilePicker<P extends Profile>({ idPrefix, host, profiles, selectedId, takenId, color, dark, align: forcedAlign, alignOverride, rotation, guestLabel, nullLabel, nullIcon, onSelect, onManage }: ProfilePickerProps<P>): react.JSX.Element;
|
|
37
52
|
|
|
38
53
|
type MD3TextVariant$1 = 'displayLarge' | 'displayMedium' | 'displaySmall' | 'headlineLarge' | 'headlineMedium' | 'headlineSmall' | 'titleLarge' | 'titleMedium' | 'titleSmall' | 'labelLarge' | 'labelMedium' | 'labelSmall' | 'bodyLarge' | 'bodyMedium' | 'bodySmall';
|
|
39
54
|
interface ProfileEditPatch {
|
|
@@ -57,8 +72,9 @@ interface ProfilesManagerProps {
|
|
|
57
72
|
cardBg?: string;
|
|
58
73
|
titleVariant?: MD3TextVariant$1;
|
|
59
74
|
colorPreview?: (hex: string) => string;
|
|
75
|
+
useColorPickerAlign?: (open: boolean, contentWidth: number, contentHeight: number) => ProfilePickerAlignResult;
|
|
60
76
|
}
|
|
61
|
-
declare function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, headerLeft, ref, fg: fgOverride, fgMuted: fgMutedOverride, cardBg: cardBgOverride, titleVariant, colorPreview }: ProfilesManagerProps): react.JSX.Element;
|
|
77
|
+
declare function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, headerLeft, ref, fg: fgOverride, fgMuted: fgMutedOverride, cardBg: cardBgOverride, titleVariant, colorPreview, useColorPickerAlign }: ProfilesManagerProps): react.JSX.Element;
|
|
62
78
|
|
|
63
79
|
type MD3TextVariant = 'displayLarge' | 'displayMedium' | 'displaySmall' | 'headlineLarge' | 'headlineMedium' | 'headlineSmall' | 'titleLarge' | 'titleMedium' | 'titleSmall' | 'labelLarge' | 'labelMedium' | 'labelSmall' | 'bodyLarge' | 'bodyMedium' | 'bodySmall';
|
|
64
80
|
interface ProfilesScreenProps {
|
|
@@ -74,8 +90,9 @@ interface ProfilesScreenProps {
|
|
|
74
90
|
cardBg?: string;
|
|
75
91
|
titleVariant?: MD3TextVariant;
|
|
76
92
|
colorPreview?: (hex: string) => string;
|
|
93
|
+
useColorPickerAlign?: ProfilesManagerProps['useColorPickerAlign'];
|
|
77
94
|
}
|
|
78
|
-
declare function ProfilesScreen({ profiles, defaultColor, onCreate, onSave, onDelete, onBack, fg: fgOverride, fgMuted, bg: bgOverride, cardBg, titleVariant, colorPreview }: ProfilesScreenProps): react.JSX.Element;
|
|
95
|
+
declare function ProfilesScreen({ profiles, defaultColor, onCreate, onSave, onDelete, onBack, fg: fgOverride, fgMuted, bg: bgOverride, cardBg, titleVariant, colorPreview, useColorPickerAlign }: ProfilesScreenProps): react.JSX.Element;
|
|
79
96
|
|
|
80
97
|
declare const MAX_PROFILE_NAME_LENGTH = 20;
|
|
81
98
|
declare const MAX_TAG_LENGTH = 3;
|
|
@@ -190,4 +207,4 @@ declare const isSharedProfileStoreAvailable: boolean;
|
|
|
190
207
|
declare function loadSharedProfiles(groupId: string): Promise<Profile[]>;
|
|
191
208
|
declare function saveSharedProfiles(groupId: string, profiles: Profile[]): Promise<void>;
|
|
192
209
|
|
|
193
|
-
export { type CreateProfileInput, MAX_PROFILE_NAME_LENGTH, MAX_TAG_LENGTH, type Profile, ProfileChip, type ProfileEditPatch, type ProfileExtensionActions, type ProfileExtensionSlice, type ProfileExtensionState, ProfilePicker, type ProfileSelectionActions, type ProfileSelectionSlice, type ProfileSelectionState, ProfilesManager, type ProfilesManagerHandle, type ProfilesManagerProps, ProfilesScreen, type ProfilesScreenProps, type SeatColorsActions, type SeatColorsSlice, type SeatColorsState, type SelectProfilePayload, type SetLastGuestColorPayload, type SetProfileExtensionPayload, type SetProfileOverridePayload, type UpdateProfilePayload, type UseSharedProfilesSyncOptions, type UseSharedProfilesSyncResult, createProfileExtensionSlice, createProfileRecord, createProfileSelectionSlice, createSeatColorsSlice, isSharedProfileStoreAvailable, isValidProfile, isValidTag, loadSharedProfiles, profilesActions, profilesReducer, resolveInitialProfiles, saveSharedProfiles, useSharedProfilesSync };
|
|
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 };
|
package/dist/index.js
CHANGED
|
@@ -30,6 +30,7 @@ __export(index_exports, {
|
|
|
30
30
|
createProfileRecord: () => createProfileRecord,
|
|
31
31
|
createProfileSelectionSlice: () => createProfileSelectionSlice,
|
|
32
32
|
createSeatColorsSlice: () => createSeatColorsSlice,
|
|
33
|
+
getProfilePickerContentSize: () => getProfilePickerContentSize,
|
|
33
34
|
isSharedProfileStoreAvailable: () => isSharedProfileStoreAvailable,
|
|
34
35
|
isValidProfile: () => isValidProfile,
|
|
35
36
|
isValidTag: () => isValidTag,
|
|
@@ -76,17 +77,22 @@ var TRIGGER_HEIGHT = 28;
|
|
|
76
77
|
var POPOVER_WIDTH = 220;
|
|
77
78
|
var ROW_HEIGHT = 36;
|
|
78
79
|
var LIST_PADDING = 8;
|
|
79
|
-
function
|
|
80
|
+
function getProfilePickerContentSize(profileCount, hasManageRow = false) {
|
|
81
|
+
const rowCount = profileCount + 1 + (hasManageRow ? 1 : 0);
|
|
82
|
+
return { width: POPOVER_WIDTH, height: LIST_PADDING * 2 + rowCount * ROW_HEIGHT };
|
|
83
|
+
}
|
|
84
|
+
function ProfilePicker({ idPrefix, host, profiles, selectedId, takenId, color, dark, align: forcedAlign, alignOverride, rotation = 0, guestLabel, nullLabel = "Player", nullIcon = "account-off-outline", onSelect, onManage }) {
|
|
80
85
|
const id = `${idPrefix}-profile`;
|
|
81
86
|
const menuBg = dark ? "#000000" : "#FFFFFF";
|
|
82
87
|
const fg = dark ? "#FFFFFF" : "#000000";
|
|
83
88
|
const open = host.openId === id;
|
|
84
89
|
const selectedProfile = profiles.find((p) => p.id === selectedId) ?? null;
|
|
85
90
|
const sortedProfiles = (0, import_react.useMemo)(() => [...profiles].sort((a, b) => a.name.localeCompare(b.name)), [profiles]);
|
|
86
|
-
const
|
|
87
|
-
const
|
|
88
|
-
const
|
|
89
|
-
const
|
|
91
|
+
const { width: contentWidth, height: contentHeight } = getProfilePickerContentSize(profiles.length, !!onManage);
|
|
92
|
+
const auto = (0, import_hud.useAutoAlign)(open, contentWidth, contentHeight, rotation);
|
|
93
|
+
const placement = alignOverride ?? auto;
|
|
94
|
+
const { maxHeight, measured, triggerRef, verticalAlign } = placement;
|
|
95
|
+
const align = forcedAlign ?? placement.align;
|
|
90
96
|
const handleSelectGuest = () => {
|
|
91
97
|
onSelect(null);
|
|
92
98
|
host.close();
|
|
@@ -296,7 +302,7 @@ function deriveTag(name) {
|
|
|
296
302
|
const trimmed = name.trim();
|
|
297
303
|
return trimmed ? trimmed[0].toUpperCase() : "";
|
|
298
304
|
}
|
|
299
|
-
function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, headerLeft, ref, fg: fgOverride, fgMuted: fgMutedOverride, cardBg: cardBgOverride, titleVariant = "headlineSmall", colorPreview }) {
|
|
305
|
+
function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, headerLeft, ref, fg: fgOverride, fgMuted: fgMutedOverride, cardBg: cardBgOverride, titleVariant = "headlineSmall", colorPreview, useColorPickerAlign }) {
|
|
300
306
|
const { dark, colors } = (0, import_auto_paper3.useAutoPaperTheme)();
|
|
301
307
|
const insets = (0, import_react_native_safe_area_context.useSafeAreaInsets)();
|
|
302
308
|
const { error: showErrorToast } = (0, import_toaster.useToast)();
|
|
@@ -382,11 +388,11 @@ function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, h
|
|
|
382
388
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native_paper3.Text, { variant: "bodyLarge", style: { color: fg }, children: "New Profile" })
|
|
383
389
|
] }) }, "new");
|
|
384
390
|
}
|
|
385
|
-
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(EditRow, { draftName, onNameChange: handleNameChange, draftColor, onColorChange: setDraftColor, draftTag, onTagChange: handleTagChange, onSubmit: commitEdit, autoFocus: true, fgMuted, host, dark, colorPreview }, "new");
|
|
391
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(EditRow, { draftName, onNameChange: handleNameChange, draftColor, onColorChange: setDraftColor, draftTag, onTagChange: handleTagChange, onSubmit: commitEdit, autoFocus: true, fgMuted, host, dark, colorPreview, useColorPickerAlign }, "new");
|
|
386
392
|
}
|
|
387
393
|
const { profile } = row;
|
|
388
394
|
if (editingId === profile.id) {
|
|
389
|
-
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(EditRow, { draftName, onNameChange: handleNameChange, draftColor, onColorChange: setDraftColor, draftTag, onTagChange: handleTagChange, onSubmit: commitEdit, onDelete: () => setConfirmDeleteId(profile.id), fgMuted, host, dark, colorPreview }, profile.id);
|
|
395
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(EditRow, { draftName, onNameChange: handleNameChange, draftColor, onColorChange: setDraftColor, draftTag, onTagChange: handleTagChange, onSubmit: commitEdit, onDelete: () => setConfirmDeleteId(profile.id), fgMuted, host, dark, colorPreview, useColorPickerAlign }, profile.id);
|
|
390
396
|
}
|
|
391
397
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_feedback_press2.TouchableRipple, { onPress: () => startEdit(profile), style: styles3.row, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_react_native3.View, { style: styles3.rowInner, children: [
|
|
392
398
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ProfileChip, { profile, size: CHIP_SIZE }),
|
|
@@ -406,7 +412,13 @@ function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, h
|
|
|
406
412
|
] }) }) })
|
|
407
413
|
] });
|
|
408
414
|
}
|
|
409
|
-
function
|
|
415
|
+
function AlignedColorPicker({ useAlign, ...props }) {
|
|
416
|
+
const { width: windowWidth } = (0, import_react_native3.useWindowDimensions)();
|
|
417
|
+
const { width, height } = (0, import_hud2.getInlineColorPickerContentSize)(import_auto_paper3.defaultColors.length, windowWidth);
|
|
418
|
+
const alignOverride = useAlign(props.host.openId === props.id, width, height);
|
|
419
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_hud2.InlineColorPicker, { ...props, alignOverride });
|
|
420
|
+
}
|
|
421
|
+
function EditRow({ draftName, onNameChange, draftColor, onColorChange, draftTag, onTagChange, onSubmit, onDelete, autoFocus, fgMuted, host, dark, colorPreview, useColorPickerAlign }) {
|
|
410
422
|
const register = (0, import_focus_chain.useFocusChain)();
|
|
411
423
|
const tag = register();
|
|
412
424
|
const name = register();
|
|
@@ -418,7 +430,7 @@ function EditRow({ draftName, onNameChange, draftColor, onColorChange, draftTag,
|
|
|
418
430
|
if (host.openId === "color") tagFocusRef.current();
|
|
419
431
|
}, [host.openId]);
|
|
420
432
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_react_native3.View, { style: styles3.editRow, children: [
|
|
421
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_hud2.InlineColorPicker, { id: "color", host, value: draftColor, onChange: onColorChange, previewValue: colorPreview, tag: draftTag, dark }),
|
|
433
|
+
useColorPickerAlign ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(AlignedColorPicker, { useAlign: useColorPickerAlign, id: "color", host, value: draftColor, onChange: onColorChange, previewValue: colorPreview, tag: draftTag, dark }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_hud2.InlineColorPicker, { id: "color", host, value: draftColor, onChange: onColorChange, previewValue: colorPreview, tag: draftTag, dark }),
|
|
422
434
|
/* @__PURE__ */ (0, import_jsx_runtime3.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" }),
|
|
423
435
|
/* @__PURE__ */ (0, import_jsx_runtime3.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 }),
|
|
424
436
|
onDelete && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_feedback_press2.TouchableRipple, { onPress: onDelete, style: styles3.iconButton, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native_paper3.Icon, { source: "trash-can-outline", size: 18, color: fgMuted }) })
|
|
@@ -523,7 +535,7 @@ var import_feedback_press3 = require("@rific/feedback-press");
|
|
|
523
535
|
var import_react3 = require("react");
|
|
524
536
|
var import_react_native4 = require("react-native");
|
|
525
537
|
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
526
|
-
function ProfilesScreen({ profiles, defaultColor, onCreate, onSave, onDelete, onBack, fg: fgOverride, fgMuted, bg: bgOverride, cardBg, titleVariant, colorPreview }) {
|
|
538
|
+
function ProfilesScreen({ profiles, defaultColor, onCreate, onSave, onDelete, onBack, fg: fgOverride, fgMuted, bg: bgOverride, cardBg, titleVariant, colorPreview, useColorPickerAlign }) {
|
|
527
539
|
const { dark } = (0, import_auto_paper4.useAutoPaperTheme)();
|
|
528
540
|
const managerRef = (0, import_react3.useRef)(null);
|
|
529
541
|
const fg = fgOverride ?? (dark ? "#FFFFFF" : "#000000");
|
|
@@ -532,7 +544,7 @@ function ProfilesScreen({ profiles, defaultColor, onCreate, onSave, onDelete, on
|
|
|
532
544
|
managerRef.current?.commitPendingEdit();
|
|
533
545
|
onBack();
|
|
534
546
|
};
|
|
535
|
-
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.View, { style: [styles4.container, { backgroundColor: bg }], children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ProfilesManager, { ref: managerRef, profiles, defaultColor, onCreate, onSave, onDelete, fg, fgMuted, cardBg, titleVariant, colorPreview, headerLeft: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_feedback_press3.IconButton, { icon: "arrow-left", iconColor: fg, size: 24, onPress: handleBack, accessibilityLabel: "Back" }) }) });
|
|
547
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.View, { style: [styles4.container, { backgroundColor: bg }], children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ProfilesManager, { ref: managerRef, profiles, defaultColor, onCreate, onSave, onDelete, fg, fgMuted, cardBg, titleVariant, colorPreview, useColorPickerAlign, headerLeft: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_feedback_press3.IconButton, { icon: "arrow-left", iconColor: fg, size: 24, onPress: handleBack, accessibilityLabel: "Back" }) }) });
|
|
536
548
|
}
|
|
537
549
|
var styles4 = import_react_native4.StyleSheet.create({
|
|
538
550
|
container: {
|
|
@@ -725,6 +737,7 @@ function useSharedProfilesSync({ groupId, onRemoteChange }) {
|
|
|
725
737
|
createProfileRecord,
|
|
726
738
|
createProfileSelectionSlice,
|
|
727
739
|
createSeatColorsSlice,
|
|
740
|
+
getProfilePickerContentSize,
|
|
728
741
|
isSharedProfileStoreAvailable,
|
|
729
742
|
isValidProfile,
|
|
730
743
|
isValidTag,
|
package/dist/index.mjs
CHANGED
|
@@ -39,17 +39,22 @@ var TRIGGER_HEIGHT = 28;
|
|
|
39
39
|
var POPOVER_WIDTH = 220;
|
|
40
40
|
var ROW_HEIGHT = 36;
|
|
41
41
|
var LIST_PADDING = 8;
|
|
42
|
-
function
|
|
42
|
+
function getProfilePickerContentSize(profileCount, hasManageRow = false) {
|
|
43
|
+
const rowCount = profileCount + 1 + (hasManageRow ? 1 : 0);
|
|
44
|
+
return { width: POPOVER_WIDTH, height: LIST_PADDING * 2 + rowCount * ROW_HEIGHT };
|
|
45
|
+
}
|
|
46
|
+
function ProfilePicker({ idPrefix, host, profiles, selectedId, takenId, color, dark, align: forcedAlign, alignOverride, rotation = 0, guestLabel, nullLabel = "Player", nullIcon = "account-off-outline", onSelect, onManage }) {
|
|
43
47
|
const id = `${idPrefix}-profile`;
|
|
44
48
|
const menuBg = dark ? "#000000" : "#FFFFFF";
|
|
45
49
|
const fg = dark ? "#FFFFFF" : "#000000";
|
|
46
50
|
const open = host.openId === id;
|
|
47
51
|
const selectedProfile = profiles.find((p) => p.id === selectedId) ?? null;
|
|
48
52
|
const sortedProfiles = useMemo(() => [...profiles].sort((a, b) => a.name.localeCompare(b.name)), [profiles]);
|
|
49
|
-
const
|
|
50
|
-
const
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
+
const { width: contentWidth, height: contentHeight } = getProfilePickerContentSize(profiles.length, !!onManage);
|
|
54
|
+
const auto = useAutoAlign(open, contentWidth, contentHeight, rotation);
|
|
55
|
+
const placement = alignOverride ?? auto;
|
|
56
|
+
const { maxHeight, measured, triggerRef, verticalAlign } = placement;
|
|
57
|
+
const align = forcedAlign ?? placement.align;
|
|
53
58
|
const handleSelectGuest = () => {
|
|
54
59
|
onSelect(null);
|
|
55
60
|
host.close();
|
|
@@ -210,13 +215,13 @@ var styles2 = StyleSheet2.create({
|
|
|
210
215
|
});
|
|
211
216
|
|
|
212
217
|
// src/ProfilesManager.tsx
|
|
213
|
-
import { useAutoPaperTheme } from "@rific/auto-paper";
|
|
218
|
+
import { defaultColors, useAutoPaperTheme } from "@rific/auto-paper";
|
|
214
219
|
import { TouchableRipple as TouchableRipple2 } from "@rific/feedback-press";
|
|
215
220
|
import { useFocusChain } from "@rific/focus-chain";
|
|
216
221
|
import { useToast } from "@rific/toaster";
|
|
217
|
-
import { InlineColorPicker, usePopoverHost } from "@tastic/hud";
|
|
222
|
+
import { getInlineColorPickerContentSize, InlineColorPicker, usePopoverHost } from "@tastic/hud";
|
|
218
223
|
import { useCallback, useEffect, useImperativeHandle, useMemo as useMemo2, useRef, useState } from "react";
|
|
219
|
-
import { KeyboardAvoidingView, Platform, ScrollView as ScrollView2, StyleSheet as StyleSheet3, View as View3 } from "react-native";
|
|
224
|
+
import { KeyboardAvoidingView, Platform, ScrollView as ScrollView2, StyleSheet as StyleSheet3, useWindowDimensions, View as View3 } from "react-native";
|
|
220
225
|
import { Button, Icon as Icon3, Portal, Text as Text3, TextInput } from "react-native-paper";
|
|
221
226
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
222
227
|
|
|
@@ -259,7 +264,7 @@ function deriveTag(name) {
|
|
|
259
264
|
const trimmed = name.trim();
|
|
260
265
|
return trimmed ? trimmed[0].toUpperCase() : "";
|
|
261
266
|
}
|
|
262
|
-
function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, headerLeft, ref, fg: fgOverride, fgMuted: fgMutedOverride, cardBg: cardBgOverride, titleVariant = "headlineSmall", colorPreview }) {
|
|
267
|
+
function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, headerLeft, ref, fg: fgOverride, fgMuted: fgMutedOverride, cardBg: cardBgOverride, titleVariant = "headlineSmall", colorPreview, useColorPickerAlign }) {
|
|
263
268
|
const { dark, colors } = useAutoPaperTheme();
|
|
264
269
|
const insets = useSafeAreaInsets();
|
|
265
270
|
const { error: showErrorToast } = useToast();
|
|
@@ -345,11 +350,11 @@ function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, h
|
|
|
345
350
|
/* @__PURE__ */ jsx3(Text3, { variant: "bodyLarge", style: { color: fg }, children: "New Profile" })
|
|
346
351
|
] }) }, "new");
|
|
347
352
|
}
|
|
348
|
-
return /* @__PURE__ */ jsx3(EditRow, { draftName, onNameChange: handleNameChange, draftColor, onColorChange: setDraftColor, draftTag, onTagChange: handleTagChange, onSubmit: commitEdit, autoFocus: true, fgMuted, host, dark, colorPreview }, "new");
|
|
353
|
+
return /* @__PURE__ */ jsx3(EditRow, { draftName, onNameChange: handleNameChange, draftColor, onColorChange: setDraftColor, draftTag, onTagChange: handleTagChange, onSubmit: commitEdit, autoFocus: true, fgMuted, host, dark, colorPreview, useColorPickerAlign }, "new");
|
|
349
354
|
}
|
|
350
355
|
const { profile } = row;
|
|
351
356
|
if (editingId === profile.id) {
|
|
352
|
-
return /* @__PURE__ */ jsx3(EditRow, { draftName, onNameChange: handleNameChange, draftColor, onColorChange: setDraftColor, draftTag, onTagChange: handleTagChange, onSubmit: commitEdit, onDelete: () => setConfirmDeleteId(profile.id), fgMuted, host, dark, colorPreview }, profile.id);
|
|
357
|
+
return /* @__PURE__ */ jsx3(EditRow, { draftName, onNameChange: handleNameChange, draftColor, onColorChange: setDraftColor, draftTag, onTagChange: handleTagChange, onSubmit: commitEdit, onDelete: () => setConfirmDeleteId(profile.id), fgMuted, host, dark, colorPreview, useColorPickerAlign }, profile.id);
|
|
353
358
|
}
|
|
354
359
|
return /* @__PURE__ */ jsx3(TouchableRipple2, { onPress: () => startEdit(profile), style: styles3.row, children: /* @__PURE__ */ jsxs2(View3, { style: styles3.rowInner, children: [
|
|
355
360
|
/* @__PURE__ */ jsx3(ProfileChip, { profile, size: CHIP_SIZE }),
|
|
@@ -369,7 +374,13 @@ function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, h
|
|
|
369
374
|
] }) }) })
|
|
370
375
|
] });
|
|
371
376
|
}
|
|
372
|
-
function
|
|
377
|
+
function AlignedColorPicker({ useAlign, ...props }) {
|
|
378
|
+
const { width: windowWidth } = useWindowDimensions();
|
|
379
|
+
const { width, height } = getInlineColorPickerContentSize(defaultColors.length, windowWidth);
|
|
380
|
+
const alignOverride = useAlign(props.host.openId === props.id, width, height);
|
|
381
|
+
return /* @__PURE__ */ jsx3(InlineColorPicker, { ...props, alignOverride });
|
|
382
|
+
}
|
|
383
|
+
function EditRow({ draftName, onNameChange, draftColor, onColorChange, draftTag, onTagChange, onSubmit, onDelete, autoFocus, fgMuted, host, dark, colorPreview, useColorPickerAlign }) {
|
|
373
384
|
const register = useFocusChain();
|
|
374
385
|
const tag = register();
|
|
375
386
|
const name = register();
|
|
@@ -381,7 +392,7 @@ function EditRow({ draftName, onNameChange, draftColor, onColorChange, draftTag,
|
|
|
381
392
|
if (host.openId === "color") tagFocusRef.current();
|
|
382
393
|
}, [host.openId]);
|
|
383
394
|
return /* @__PURE__ */ jsxs2(View3, { style: styles3.editRow, children: [
|
|
384
|
-
/* @__PURE__ */ jsx3(InlineColorPicker, { id: "color", host, value: draftColor, onChange: onColorChange, previewValue: colorPreview, tag: draftTag, dark }),
|
|
395
|
+
useColorPickerAlign ? /* @__PURE__ */ jsx3(AlignedColorPicker, { useAlign: useColorPickerAlign, id: "color", host, value: draftColor, onChange: onColorChange, previewValue: colorPreview, tag: draftTag, dark }) : /* @__PURE__ */ jsx3(InlineColorPicker, { id: "color", host, value: draftColor, onChange: onColorChange, previewValue: colorPreview, tag: draftTag, dark }),
|
|
385
396
|
/* @__PURE__ */ jsx3(TextInput, { ref: tag.ref, ...tag.props, value: draftTag, onChangeText: onTagChange, accessibilityLabel: "Tag", autoCapitalize: "characters", selectTextOnFocus: true, returnKeyType: "next", style: styles3.hiddenTagInput, pointerEvents: "none" }),
|
|
386
397
|
/* @__PURE__ */ jsx3(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 }),
|
|
387
398
|
onDelete && /* @__PURE__ */ jsx3(TouchableRipple2, { onPress: onDelete, style: styles3.iconButton, children: /* @__PURE__ */ jsx3(Icon3, { source: "trash-can-outline", size: 18, color: fgMuted }) })
|
|
@@ -486,7 +497,7 @@ import { IconButton } from "@rific/feedback-press";
|
|
|
486
497
|
import { useRef as useRef2 } from "react";
|
|
487
498
|
import { StyleSheet as StyleSheet4, View as View4 } from "react-native";
|
|
488
499
|
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
489
|
-
function ProfilesScreen({ profiles, defaultColor, onCreate, onSave, onDelete, onBack, fg: fgOverride, fgMuted, bg: bgOverride, cardBg, titleVariant, colorPreview }) {
|
|
500
|
+
function ProfilesScreen({ profiles, defaultColor, onCreate, onSave, onDelete, onBack, fg: fgOverride, fgMuted, bg: bgOverride, cardBg, titleVariant, colorPreview, useColorPickerAlign }) {
|
|
490
501
|
const { dark } = useAutoPaperTheme2();
|
|
491
502
|
const managerRef = useRef2(null);
|
|
492
503
|
const fg = fgOverride ?? (dark ? "#FFFFFF" : "#000000");
|
|
@@ -495,7 +506,7 @@ function ProfilesScreen({ profiles, defaultColor, onCreate, onSave, onDelete, on
|
|
|
495
506
|
managerRef.current?.commitPendingEdit();
|
|
496
507
|
onBack();
|
|
497
508
|
};
|
|
498
|
-
return /* @__PURE__ */ jsx4(View4, { style: [styles4.container, { backgroundColor: bg }], children: /* @__PURE__ */ jsx4(ProfilesManager, { ref: managerRef, profiles, defaultColor, onCreate, onSave, onDelete, fg, fgMuted, cardBg, titleVariant, colorPreview, headerLeft: /* @__PURE__ */ jsx4(IconButton, { icon: "arrow-left", iconColor: fg, size: 24, onPress: handleBack, accessibilityLabel: "Back" }) }) });
|
|
509
|
+
return /* @__PURE__ */ jsx4(View4, { style: [styles4.container, { backgroundColor: bg }], children: /* @__PURE__ */ jsx4(ProfilesManager, { ref: managerRef, profiles, defaultColor, onCreate, onSave, onDelete, fg, fgMuted, cardBg, titleVariant, colorPreview, useColorPickerAlign, headerLeft: /* @__PURE__ */ jsx4(IconButton, { icon: "arrow-left", iconColor: fg, size: 24, onPress: handleBack, accessibilityLabel: "Back" }) }) });
|
|
499
510
|
}
|
|
500
511
|
var styles4 = StyleSheet4.create({
|
|
501
512
|
container: {
|
|
@@ -687,6 +698,7 @@ export {
|
|
|
687
698
|
createProfileRecord,
|
|
688
699
|
createProfileSelectionSlice,
|
|
689
700
|
createSeatColorsSlice,
|
|
701
|
+
getProfilePickerContentSize,
|
|
690
702
|
isSharedProfileStoreAvailable,
|
|
691
703
|
isValidProfile,
|
|
692
704
|
isValidTag,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tastic/profile",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.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",
|
|
@@ -69,9 +69,9 @@
|
|
|
69
69
|
"@rific/core": "^0.2.0",
|
|
70
70
|
"@rific/feedback-press": "^0.12.0",
|
|
71
71
|
"@rific/focus-chain": "^0.4.7",
|
|
72
|
-
"@rific/toaster": "^0.
|
|
73
|
-
"@tastic/core": "^0.
|
|
74
|
-
"@tastic/hud": "^0.
|
|
72
|
+
"@rific/toaster": "^0.7.0",
|
|
73
|
+
"@tastic/core": "^0.9.0",
|
|
74
|
+
"@tastic/hud": "^0.12.0",
|
|
75
75
|
"@testing-library/dom": "^10.4.1",
|
|
76
76
|
"@testing-library/react": "^16.3.2",
|
|
77
77
|
"@types/jest": "^30.0.0",
|
|
@@ -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.8.0",
|
|
101
101
|
"expo-modules-core": ">=1.0.0",
|
|
102
102
|
"react": ">=19.0.0",
|
|
103
103
|
"react-native": ">=0.76.0",
|
package/src/ProfilePicker.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { getContrastColor } from '@rific/auto-paper'
|
|
2
2
|
import { TouchableRipple } from '@rific/feedback-press'
|
|
3
|
-
import { PopoverBody, PopoverHost, useAutoAlign } from '@tastic/hud'
|
|
4
|
-
import { useMemo } from 'react'
|
|
3
|
+
import { PopoverBody, PopoverHost, PopoverRotation, useAutoAlign } from '@tastic/hud'
|
|
4
|
+
import { RefObject, useMemo } from 'react'
|
|
5
5
|
import { ScrollView, StyleSheet, View } from 'react-native'
|
|
6
6
|
import { Icon, Text } from 'react-native-paper'
|
|
7
7
|
|
|
@@ -13,6 +13,39 @@ const POPOVER_WIDTH = 220
|
|
|
13
13
|
const ROW_HEIGHT = 36
|
|
14
14
|
const LIST_PADDING = 8
|
|
15
15
|
|
|
16
|
+
// A complete placement decision for the popover: which way it opens, how tall it may grow, whether
|
|
17
|
+
// this open's measurement has landed yet, and the ref attached to the measured trigger.
|
|
18
|
+
// Structurally identical to @tastic/hud's own `AlignResult` (exported from hud >= 0.12.0, and the
|
|
19
|
+
// same shape `useAutoAlign` itself returns) — declared here rather than imported so this package's
|
|
20
|
+
// public types don't start requiring a newer @tastic/hud than its peer range already admits. A
|
|
21
|
+
// hud `AlignResult` (e.g. from `useZoneClampedAlign`) is assignable to this as-is.
|
|
22
|
+
export interface ProfilePickerAlignResult {
|
|
23
|
+
align: 'left' | 'right' | 'center'
|
|
24
|
+
verticalAlign: 'below' | 'above'
|
|
25
|
+
maxHeight: number
|
|
26
|
+
measured: boolean
|
|
27
|
+
triggerRef: RefObject<View | null>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ProfilePickerContentSize {
|
|
31
|
+
width: number
|
|
32
|
+
height: number
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// The popover's content footprint for a given roster size: always POPOVER_WIDTH wide, tall enough
|
|
36
|
+
// for the null/"guest" row + one row per profile + (when `hasManageRow`, i.e. when the picker is
|
|
37
|
+
// given an `onManage`) the trailing Manage row, plus the list's own padding. This is the size
|
|
38
|
+
// ProfilePicker hands to `useAutoAlign` — the ONE formula, used by the component itself (it leaves
|
|
39
|
+
// out the menu's 2pt border and the Manage divider, ~13pt in all, so treat it as a placement
|
|
40
|
+
// estimate, not a pixel-exact measurement) — exported
|
|
41
|
+
// so a host supplying its own placement through `alignOverride` can size that placement hook (its
|
|
42
|
+
// own useAutoAlign/useZoneClampedAlign call) to match the popover it is actually positioning,
|
|
43
|
+
// instead of copying these constants and drifting whenever they change here.
|
|
44
|
+
export function getProfilePickerContentSize(profileCount: number, hasManageRow = false): ProfilePickerContentSize {
|
|
45
|
+
const rowCount = profileCount + 1 + (hasManageRow ? 1 : 0)
|
|
46
|
+
return { width: POPOVER_WIDTH, height: LIST_PADDING * 2 + rowCount * ROW_HEIGHT }
|
|
47
|
+
}
|
|
48
|
+
|
|
16
49
|
interface ProfilePickerProps<P extends Profile> {
|
|
17
50
|
// Namespaces this trigger's id — pass something unique per instance whenever more than one of
|
|
18
51
|
// these shares a PopoverHost (e.g. one per seat).
|
|
@@ -31,7 +64,30 @@ interface ProfilePickerProps<P extends Profile> {
|
|
|
31
64
|
// trigger reads as one identity with whatever it's paired with.
|
|
32
65
|
color: string
|
|
33
66
|
dark: boolean
|
|
67
|
+
// Forces a specific horizontal alignment, beating whatever the placement (the automatic
|
|
68
|
+
// measurement, or `alignOverride` below) would have picked. Omit to let placement decide.
|
|
34
69
|
align?: 'left' | 'right' | 'center'
|
|
70
|
+
// Substitutes the popover's whole placement — horizontal/vertical alignment, max height, the
|
|
71
|
+
// `measured` gate and the trigger ref — for the automatic `useAutoAlign` one, for a host whose
|
|
72
|
+
// trigger sits somewhere that hook's plain window-relative measurement gets wrong. The motivating
|
|
73
|
+
// case is a FAKE-rotated frame (a View turned +-90deg while the OS stays portrait-locked):
|
|
74
|
+
// `measureInWindow` reports a rect in true screen space, `useAutoAlign` compares it against the
|
|
75
|
+
// raw portrait window, and so measures along the wrong axes — flipping the list upward off-screen
|
|
76
|
+
// or running it past the frame's bottom. The host runs its own rotation-aware placement hook
|
|
77
|
+
// (sized with `getProfilePickerContentSize`, passing `open` from its own `host.openId === ...`
|
|
78
|
+
// check) and hands the result in here; `align` above still wins over this result's own `align`.
|
|
79
|
+
// The result's `triggerRef` is the one attached to the measured trigger, so the hook that
|
|
80
|
+
// produced it is measuring the right element. Omit for the default automatic placement.
|
|
81
|
+
alignOverride?: ProfilePickerAlignResult
|
|
82
|
+
// Rotation of an ancestor frame turned by a fake landscape (or equivalent), forwarded to the
|
|
83
|
+
// automatic `useAutoAlign` measurement — the same seam @tastic/hud's InlineColorPicker/
|
|
84
|
+
// SectionedDropdown expose (needs @tastic/hud >= 0.8.0; older versions ignore it). Defaults to 0
|
|
85
|
+
// (measurement used as-is), which is exactly how this picker behaved before this prop existed.
|
|
86
|
+
// Deliberately NOT read from an ambient provider the way hud's own pickers do: this component
|
|
87
|
+
// never touches @tastic/core at runtime, and an ambient read would silently change placement for
|
|
88
|
+
// every existing caller already living under a rotation provider. Ignored when `alignOverride` is
|
|
89
|
+
// supplied (the override replaces the placement this feeds).
|
|
90
|
+
rotation?: PopoverRotation
|
|
35
91
|
// Trigger's own idle-state text (uppercased here regardless of casing passed in) — shown until a
|
|
36
92
|
// profile is selected.
|
|
37
93
|
guestLabel: string
|
|
@@ -63,7 +119,7 @@ interface ProfilePickerProps<P extends Profile> {
|
|
|
63
119
|
// it keeps this popover from needing its own destructive-delete confirmation. Fully prop-driven —
|
|
64
120
|
// no internal profile-storage access of its own, so the host app stays the one place that reads
|
|
65
121
|
// its own persistence hook.
|
|
66
|
-
export function ProfilePicker<P extends Profile>({ idPrefix, host, profiles, selectedId, takenId, color, dark, align: alignOverride, guestLabel, nullLabel = 'Player', nullIcon = 'account-off-outline', onSelect, onManage }: ProfilePickerProps<P>) {
|
|
122
|
+
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>) {
|
|
67
123
|
const id = `${idPrefix}-profile`
|
|
68
124
|
const menuBg = dark ? '#000000' : '#FFFFFF'
|
|
69
125
|
const fg = dark ? '#FFFFFF' : '#000000'
|
|
@@ -74,10 +130,14 @@ export function ProfilePicker<P extends Profile>({ idPrefix, host, profiles, sel
|
|
|
74
130
|
// most from staying scannable once there are more than a handful of saved profiles.
|
|
75
131
|
const sortedProfiles = useMemo(() => [...profiles].sort((a, b) => a.name.localeCompare(b.name)), [profiles])
|
|
76
132
|
// Null row + each saved profile + (if passed) the manage row.
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
133
|
+
const { width: contentWidth, height: contentHeight } = getProfilePickerContentSize(profiles.length, !!onManage)
|
|
134
|
+
// Always called, even when `alignOverride` is supplied and this result goes unused — hooks can't
|
|
135
|
+
// be conditional (see @tastic/hud's InlineColorPicker for the identical pattern). With an
|
|
136
|
+
// override its triggerRef is never attached, so this hook's own measurement is a no-op.
|
|
137
|
+
const auto = useAutoAlign(open, contentWidth, contentHeight, rotation)
|
|
138
|
+
const placement = alignOverride ?? auto
|
|
139
|
+
const { maxHeight, measured, triggerRef, verticalAlign } = placement
|
|
140
|
+
const align = forcedAlign ?? placement.align
|
|
81
141
|
|
|
82
142
|
const handleSelectGuest = () => {
|
|
83
143
|
onSelect(null)
|
package/src/ProfilesManager.tsx
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
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 { type ProfilePickerAlignResult } from './ProfilePicker'
|
|
12
13
|
import { clampTag, isValidTag, MAX_PROFILE_NAME_LENGTH } from './profilesValidation'
|
|
13
14
|
import { Profile } from './types'
|
|
14
15
|
|
|
@@ -79,6 +80,18 @@ export interface ProfilesManagerProps {
|
|
|
79
80
|
// InlineColorPicker's identical prop (see its own doc). Leaves draftColor/onCreate/onSave's actual
|
|
80
81
|
// color value untouched; only what's rendered changes.
|
|
81
82
|
colorPreview?: (hex: string) => string
|
|
83
|
+
// Substitutes the color swatch popover's own placement, for a host that renders this screen inside a
|
|
84
|
+
// FAKE-rotated frame (a View turned +-90deg while the OS stays portrait-locked). @tastic/hud's
|
|
85
|
+
// InlineColorPicker places its popover with useAutoAlign, which compares a screen-space
|
|
86
|
+
// measureInWindow rect against the raw portrait window and (through its ambient rotation) double-rotates
|
|
87
|
+
// an already-transformed rect - so under a fake rotation it lands off its trigger. A host that knows its
|
|
88
|
+
// frame passes its own placement HOOK here; it is called with the swatch popover's open state and content
|
|
89
|
+
// size, and its result is handed to InlineColorPicker as `alignOverride` (its `triggerRef` is what gets
|
|
90
|
+
// measured). It is a hook, not a plain function: give it a STABLE identity for the life of the mount (a
|
|
91
|
+
// module-level function) - switching it at runtime remounts the swatch. Omit for the ordinary case;
|
|
92
|
+
// behaviour is then identical to before. Needs @tastic/hud >= 0.12.0 to have any effect (older hud has no
|
|
93
|
+
// `alignOverride` and ignores it).
|
|
94
|
+
useColorPickerAlign?: (open: boolean, contentWidth: number, contentHeight: number) => ProfilePickerAlignResult
|
|
82
95
|
}
|
|
83
96
|
|
|
84
97
|
type Row = { kind: 'profile'; profile: Profile } | { kind: 'new' }
|
|
@@ -101,7 +114,7 @@ function deriveTag(name: string): string {
|
|
|
101
114
|
// really unmounts it on a given host's router — see ProfilesManagerHandle's own doc for the
|
|
102
115
|
// react-navigation-web case where it doesn't, and why a host there needs the `ref` escape hatch
|
|
103
116
|
// 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) {
|
|
117
|
+
export function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, headerLeft, ref, fg: fgOverride, fgMuted: fgMutedOverride, cardBg: cardBgOverride, titleVariant = 'headlineSmall', colorPreview, useColorPickerAlign }: ProfilesManagerProps) {
|
|
105
118
|
const { dark, colors } = useAutoPaperTheme()
|
|
106
119
|
const insets = useSafeAreaInsets()
|
|
107
120
|
const { error: showErrorToast } = useToast()
|
|
@@ -253,12 +266,12 @@ export function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDe
|
|
|
253
266
|
</TouchableRipple>
|
|
254
267
|
)
|
|
255
268
|
}
|
|
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} />
|
|
269
|
+
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
270
|
}
|
|
258
271
|
|
|
259
272
|
const { profile } = row
|
|
260
273
|
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} />
|
|
274
|
+
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
275
|
}
|
|
263
276
|
|
|
264
277
|
// No delete affordance at rest — tap the row to start editing, which is the one place
|
|
@@ -323,6 +336,19 @@ interface EditRowProps {
|
|
|
323
336
|
host: PopoverHost
|
|
324
337
|
dark: boolean
|
|
325
338
|
colorPreview?: (hex: string) => string
|
|
339
|
+
useColorPickerAlign?: ProfilesManagerProps['useColorPickerAlign']
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// InlineColorPicker with its placement supplied by a host hook (see ProfilesManagerProps.useColorPickerAlign).
|
|
343
|
+
// A component of its own - rather than a conditional hook call inside EditRow - so the hook is always called
|
|
344
|
+
// unconditionally here, and the plain path below stays byte-identical to what it was. Sized exactly as
|
|
345
|
+
// InlineColorPicker sizes itself (the default swatch list at the raw window width), so the host's placement and
|
|
346
|
+
// the popover it positions agree on its footprint.
|
|
347
|
+
function AlignedColorPicker({ useAlign, ...props }: ComponentProps<typeof InlineColorPicker> & { useAlign: NonNullable<ProfilesManagerProps['useColorPickerAlign']> }) {
|
|
348
|
+
const { width: windowWidth } = useWindowDimensions()
|
|
349
|
+
const { width, height } = getInlineColorPickerContentSize(defaultColors.length, windowWidth)
|
|
350
|
+
const alignOverride = useAlign(props.host.openId === props.id, width, height)
|
|
351
|
+
return <InlineColorPicker {...props} alignOverride={alignOverride} />
|
|
326
352
|
}
|
|
327
353
|
|
|
328
354
|
// One row, expanded: the color trigger (@tastic/hud's InlineColorPicker) — doubling as the tag's
|
|
@@ -337,7 +363,7 @@ interface EditRowProps {
|
|
|
337
363
|
// own — submitting the *name* field is what actually commits the row, so its registration's own
|
|
338
364
|
// onSubmitEditing (a no-op — it's last in the chain) is overridden with onSubmit below, matching
|
|
339
365
|
// 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) {
|
|
366
|
+
function EditRow({ draftName, onNameChange, draftColor, onColorChange, draftTag, onTagChange, onSubmit, onDelete, autoFocus, fgMuted, host, dark, colorPreview, useColorPickerAlign }: EditRowProps) {
|
|
341
367
|
const register = useFocusChain()
|
|
342
368
|
const tag = register()
|
|
343
369
|
const name = register()
|
|
@@ -364,7 +390,7 @@ function EditRow({ draftName, onNameChange, draftColor, onColorChange, draftTag,
|
|
|
364
390
|
|
|
365
391
|
return (
|
|
366
392
|
<View style={styles.editRow}>
|
|
367
|
-
<InlineColorPicker id='color' host={host} value={draftColor} onChange={onColorChange} previewValue={colorPreview} tag={draftTag} dark={dark} />
|
|
393
|
+
{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
394
|
{/* Invisible on purpose (styles.hiddenTagInput: zero footprint, position: 'absolute' so it
|
|
369
395
|
doesn't reserve space in the row's own flex layout) — the swatch above is the only place this
|
|
370
396
|
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,5 +1,5 @@
|
|
|
1
1
|
export { ProfileChip } from './ProfileChip'
|
|
2
|
-
export { ProfilePicker } from './ProfilePicker'
|
|
2
|
+
export { getProfilePickerContentSize, ProfilePicker, type ProfilePickerAlignResult, type ProfilePickerContentSize } from './ProfilePicker'
|
|
3
3
|
export { type ProfileEditPatch, ProfilesManager, type ProfilesManagerHandle, type ProfilesManagerProps } from './ProfilesManager'
|
|
4
4
|
export { ProfilesScreen, type ProfilesScreenProps } from './ProfilesScreen'
|
|
5
5
|
export { isValidProfile, isValidTag, MAX_PROFILE_NAME_LENGTH, MAX_TAG_LENGTH } from './profilesValidation'
|