@tastic/profile 0.9.0 → 0.10.0

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