@tastic/profile 0.3.6 → 0.3.7

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/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as react from 'react';
2
- import { ReactNode } from 'react';
2
+ import { ReactNode, Ref } from 'react';
3
3
  import { PopoverHost } from '@tastic/hud';
4
4
 
5
5
  interface Profile {
@@ -40,6 +40,9 @@ interface ProfileEditPatch {
40
40
  color: string;
41
41
  tag: string;
42
42
  }
43
+ interface ProfilesManagerHandle {
44
+ commitPendingEdit: () => void;
45
+ }
43
46
  interface ProfilesManagerProps {
44
47
  profiles: Profile[];
45
48
  defaultColor: string;
@@ -47,8 +50,9 @@ interface ProfilesManagerProps {
47
50
  onSave: (id: string, patch: ProfileEditPatch) => void;
48
51
  onDelete: (id: string) => void;
49
52
  headerLeft?: ReactNode;
53
+ ref?: Ref<ProfilesManagerHandle>;
50
54
  }
51
- declare function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, headerLeft }: ProfilesManagerProps): react.JSX.Element;
55
+ declare function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, headerLeft, ref }: ProfilesManagerProps): react.JSX.Element;
52
56
 
53
57
  declare const MAX_PROFILE_NAME_LENGTH = 20;
54
58
  declare const MAX_TAG_LENGTH = 3;
@@ -59,4 +63,4 @@ declare const isSharedProfileStoreAvailable: boolean;
59
63
  declare function loadSharedProfiles(groupId: string): Promise<Profile[]>;
60
64
  declare function saveSharedProfiles(groupId: string, profiles: Profile[]): Promise<void>;
61
65
 
62
- export { MAX_PROFILE_NAME_LENGTH, MAX_TAG_LENGTH, type Profile, ProfileChip, ProfilePicker, ProfilesManager, type ProfilesManagerProps, isSharedProfileStoreAvailable, isValidProfile, isValidTag, loadSharedProfiles, saveSharedProfiles };
66
+ export { MAX_PROFILE_NAME_LENGTH, MAX_TAG_LENGTH, type Profile, ProfileChip, ProfilePicker, ProfilesManager, type ProfilesManagerHandle, type ProfilesManagerProps, isSharedProfileStoreAvailable, isValidProfile, isValidTag, loadSharedProfiles, saveSharedProfiles };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as react from 'react';
2
- import { ReactNode } from 'react';
2
+ import { ReactNode, Ref } from 'react';
3
3
  import { PopoverHost } from '@tastic/hud';
4
4
 
5
5
  interface Profile {
@@ -40,6 +40,9 @@ interface ProfileEditPatch {
40
40
  color: string;
41
41
  tag: string;
42
42
  }
43
+ interface ProfilesManagerHandle {
44
+ commitPendingEdit: () => void;
45
+ }
43
46
  interface ProfilesManagerProps {
44
47
  profiles: Profile[];
45
48
  defaultColor: string;
@@ -47,8 +50,9 @@ interface ProfilesManagerProps {
47
50
  onSave: (id: string, patch: ProfileEditPatch) => void;
48
51
  onDelete: (id: string) => void;
49
52
  headerLeft?: ReactNode;
53
+ ref?: Ref<ProfilesManagerHandle>;
50
54
  }
51
- declare function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, headerLeft }: ProfilesManagerProps): react.JSX.Element;
55
+ declare function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, headerLeft, ref }: ProfilesManagerProps): react.JSX.Element;
52
56
 
53
57
  declare const MAX_PROFILE_NAME_LENGTH = 20;
54
58
  declare const MAX_TAG_LENGTH = 3;
@@ -59,4 +63,4 @@ declare const isSharedProfileStoreAvailable: boolean;
59
63
  declare function loadSharedProfiles(groupId: string): Promise<Profile[]>;
60
64
  declare function saveSharedProfiles(groupId: string, profiles: Profile[]): Promise<void>;
61
65
 
62
- export { MAX_PROFILE_NAME_LENGTH, MAX_TAG_LENGTH, type Profile, ProfileChip, ProfilePicker, ProfilesManager, type ProfilesManagerProps, isSharedProfileStoreAvailable, isValidProfile, isValidTag, loadSharedProfiles, saveSharedProfiles };
66
+ export { MAX_PROFILE_NAME_LENGTH, MAX_TAG_LENGTH, type Profile, ProfileChip, ProfilePicker, ProfilesManager, type ProfilesManagerHandle, type ProfilesManagerProps, isSharedProfileStoreAvailable, isValidProfile, isValidTag, loadSharedProfiles, saveSharedProfiles };
package/dist/index.js CHANGED
@@ -287,7 +287,7 @@ function deriveTag(name) {
287
287
  const trimmed = name.trim();
288
288
  return trimmed ? trimmed[0].toUpperCase() : "";
289
289
  }
290
- function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, headerLeft }) {
290
+ function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, headerLeft, ref }) {
291
291
  const { dark, colors } = (0, import_auto_paper3.useAutoPaperTheme)();
292
292
  const insets = (0, import_react_native_safe_area_context.useSafeAreaInsets)();
293
293
  const { error: showErrorToast } = (0, import_toaster.useToast)();
@@ -320,6 +320,7 @@ function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, h
320
320
  commitEditRef.current = commitEdit;
321
321
  });
322
322
  (0, import_react2.useEffect)(() => () => commitEditRef.current(), []);
323
+ (0, import_react2.useImperativeHandle)(ref, () => ({ commitPendingEdit: () => commitEditRef.current() }), []);
323
324
  const startEdit = (0, import_react2.useCallback)(
324
325
  (target) => {
325
326
  commitEdit();
package/dist/index.mjs CHANGED
@@ -215,7 +215,7 @@ import { TouchableRipple as TouchableRipple2 } from "@rific/feedback-press";
215
215
  import { useFocusChain } from "@rific/focus-chain";
216
216
  import { useToast } from "@rific/toaster";
217
217
  import { InlineColorPicker, usePopoverHost } from "@tastic/hud";
218
- import { useCallback, useEffect, useMemo as useMemo2, useRef, useState } from "react";
218
+ import { useCallback, useEffect, useImperativeHandle, useMemo as useMemo2, useRef, useState } from "react";
219
219
  import { KeyboardAvoidingView, Platform, ScrollView as ScrollView2, StyleSheet as StyleSheet3, View as View3 } from "react-native";
220
220
  import { Button, Icon as Icon3, Portal, Text as Text3, TextInput } from "react-native-paper";
221
221
  import { useSafeAreaInsets } from "react-native-safe-area-context";
@@ -259,7 +259,7 @@ function deriveTag(name) {
259
259
  const trimmed = name.trim();
260
260
  return trimmed ? trimmed[0].toUpperCase() : "";
261
261
  }
262
- function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, headerLeft }) {
262
+ function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, headerLeft, ref }) {
263
263
  const { dark, colors } = useAutoPaperTheme();
264
264
  const insets = useSafeAreaInsets();
265
265
  const { error: showErrorToast } = useToast();
@@ -292,6 +292,7 @@ function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, h
292
292
  commitEditRef.current = commitEdit;
293
293
  });
294
294
  useEffect(() => () => commitEditRef.current(), []);
295
+ useImperativeHandle(ref, () => ({ commitPendingEdit: () => commitEditRef.current() }), []);
295
296
  const startEdit = useCallback(
296
297
  (target) => {
297
298
  commitEdit();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tastic/profile",
3
- "version": "0.3.6",
3
+ "version": "0.3.7",
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",
@@ -3,7 +3,7 @@ import { TouchableRipple } from '@rific/feedback-press'
3
3
  import { useFocusChain } from '@rific/focus-chain'
4
4
  import { useToast } from '@rific/toaster'
5
5
  import { InlineColorPicker, PopoverHost, usePopoverHost } from '@tastic/hud'
6
- import { ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react'
6
+ import { ReactNode, Ref, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'
7
7
  import { KeyboardAvoidingView, Platform, ScrollView, StyleSheet, 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'
@@ -22,6 +22,18 @@ interface ProfileEditPatch {
22
22
  tag: string
23
23
  }
24
24
 
25
+ export interface ProfilesManagerHandle {
26
+ // Flushes whatever row is currently being edited — the same thing this component's own unmount
27
+ // effect does, but callable explicitly. Exists because "unmount" isn't a reliable "the user is
28
+ // navigating away" signal on every host: React Navigation's web renderer keeps a popped screen
29
+ // mounted-but-hidden instead of unmounting it, so a host relying only on the unmount fallback
30
+ // silently loses an in-progress edit on `router.back()` there. A host whose router doesn't
31
+ // genuinely unmount this component on navigation should hold a ref and call this right before
32
+ // navigating away (e.g. in its own back-button handler). Safe to call any time, including when
33
+ // nothing is being edited — a no-op then, same as the unmount effect already is.
34
+ commitPendingEdit: () => void
35
+ }
36
+
25
37
  export interface ProfilesManagerProps {
26
38
  profiles: Profile[]
27
39
  // The color a brand-new profile's draft starts on — this component doesn't have (and shouldn't
@@ -37,6 +49,9 @@ export interface ProfilesManagerProps {
37
49
  // rendering slot, not an onBack-style callback: this component doesn't know or care that it's a
38
50
  // back button, only that the host wants something shown there. Omit to hide the slot.
39
51
  headerLeft?: ReactNode
52
+ // React 19 accepts `ref` as a plain prop on a function component (no forwardRef needed) — see
53
+ // ProfilesManagerHandle's own doc for why a host may need this.
54
+ ref?: Ref<ProfilesManagerHandle>
40
55
  }
41
56
 
42
57
  type Row = { kind: 'profile'; profile: Profile } | { kind: 'new' }
@@ -55,7 +70,11 @@ function deriveTag(name: string): string {
55
70
  // would still assume *some* host owns navigation; this component doesn't even go that far), so any
56
71
  // consuming screen just renders it as its body inside its own routed wrapper (back button, native
57
72
  // push/pop transition, whatever persistence hook backs `profiles`/`onCreate`/`onSave`/`onDelete`).
58
- export function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, headerLeft }: ProfilesManagerProps) {
73
+ // Staying router-agnostic means this component can't know for itself whether "navigating away"
74
+ // really unmounts it on a given host's router — see ProfilesManagerHandle's own doc for the
75
+ // react-navigation-web case where it doesn't, and why a host there needs the `ref` escape hatch
76
+ // instead of relying on the unmount fallback alone.
77
+ export function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, headerLeft, ref }: ProfilesManagerProps) {
59
78
  const { dark, colors } = useAutoPaperTheme()
60
79
  const insets = useSafeAreaInsets()
61
80
  const { error: showErrorToast } = useToast()
@@ -116,6 +135,11 @@ export function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDe
116
135
  commitEditRef.current = commitEdit
117
136
  })
118
137
  useEffect(() => () => commitEditRef.current(), [])
138
+ // The explicit escape hatch for hosts whose "navigate away" doesn't genuinely unmount this
139
+ // component — see ProfilesManagerHandle's own doc. `[]` deps: the handle object itself never
140
+ // needs to change identity, since it always calls through the ref to whatever commitEdit is
141
+ // current at call time, the same indirection the unmount effect above already relies on.
142
+ useImperativeHandle(ref, () => ({ commitPendingEdit: () => commitEditRef.current() }), [])
119
143
 
120
144
  const startEdit = useCallback(
121
145
  (target: 'new' | Profile) => {
package/src/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { ProfileChip } from './ProfileChip'
2
2
  export { ProfilePicker } from './ProfilePicker'
3
- export { ProfilesManager, type ProfilesManagerProps } from './ProfilesManager'
3
+ export { ProfilesManager, type ProfilesManagerHandle, type ProfilesManagerProps } from './ProfilesManager'
4
4
  export { isValidProfile, isValidTag, MAX_PROFILE_NAME_LENGTH, MAX_TAG_LENGTH } from './profilesValidation'
5
5
  export { isSharedProfileStoreAvailable, loadSharedProfiles, saveSharedProfiles } from './sharedProfileStore'
6
6
  export type { Profile } from './types'