@tastic/profile 0.7.0 → 0.8.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 +39 -1
- package/dist/index.d.mts +19 -4
- package/dist/index.d.ts +19 -4
- package/dist/index.js +12 -5
- package/dist/index.mjs +11 -5
- package/package.json +4 -4
- package/src/ProfilePicker.tsx +67 -7
- 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,40 @@ 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
|
+
|
|
101
139
|
### Routed screens: `ProfilesScreen`
|
|
102
140
|
|
|
103
141
|
`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 {
|
|
@@ -190,4 +205,4 @@ declare const isSharedProfileStoreAvailable: boolean;
|
|
|
190
205
|
declare function loadSharedProfiles(groupId: string): Promise<Profile[]>;
|
|
191
206
|
declare function saveSharedProfiles(groupId: string, profiles: Profile[]): Promise<void>;
|
|
192
207
|
|
|
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 };
|
|
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 };
|
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 {
|
|
@@ -190,4 +205,4 @@ declare const isSharedProfileStoreAvailable: boolean;
|
|
|
190
205
|
declare function loadSharedProfiles(groupId: string): Promise<Profile[]>;
|
|
191
206
|
declare function saveSharedProfiles(groupId: string, profiles: Profile[]): Promise<void>;
|
|
192
207
|
|
|
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 };
|
|
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 };
|
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();
|
|
@@ -725,6 +731,7 @@ function useSharedProfilesSync({ groupId, onRemoteChange }) {
|
|
|
725
731
|
createProfileRecord,
|
|
726
732
|
createProfileSelectionSlice,
|
|
727
733
|
createSeatColorsSlice,
|
|
734
|
+
getProfilePickerContentSize,
|
|
728
735
|
isSharedProfileStoreAvailable,
|
|
729
736
|
isValidProfile,
|
|
730
737
|
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();
|
|
@@ -687,6 +692,7 @@ export {
|
|
|
687
692
|
createProfileRecord,
|
|
688
693
|
createProfileSelectionSlice,
|
|
689
694
|
createSeatColorsSlice,
|
|
695
|
+
getProfilePickerContentSize,
|
|
690
696
|
isSharedProfileStoreAvailable,
|
|
691
697
|
isValidProfile,
|
|
692
698
|
isValidTag,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tastic/profile",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.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.
|
|
72
|
+
"@rific/toaster": "^0.7.0",
|
|
73
73
|
"@tastic/core": "^0.8.0",
|
|
74
|
-
"@tastic/hud": "^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/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'
|