@tastic/profile 0.8.0 → 0.10.0

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