@tastic/profile 0.14.0 → 0.15.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/dist/index.mjs CHANGED
@@ -5,6 +5,14 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
5
5
  throw Error('Dynamic require of "' + x + '" is not supported');
6
6
  });
7
7
 
8
+ // src/PickerMenu.tsx
9
+ import { defaultColors, FadingScrollView, getContrastColor as getContrastColor2 } from "@rific/auto-paper";
10
+ import { TouchableRipple } from "@rific/feedback-press";
11
+ import { PopoverBody, useAutoAlign } from "@tastic/hud";
12
+ import { useState } from "react";
13
+ import { StyleSheet as StyleSheet2, View as View2 } from "react-native";
14
+ import { Icon as Icon2, Text as Text2 } from "react-native-paper";
15
+
8
16
  // src/IdentityChip.tsx
9
17
  import { getContrastColor } from "@rific/auto-paper";
10
18
  import { StyleSheet, View } from "react-native";
@@ -27,36 +35,8 @@ var styles = StyleSheet.create({
27
35
  }
28
36
  });
29
37
 
30
- // src/ProfileColorContext.tsx
31
- import { createContext, useContext } from "react";
32
- import { jsx as jsx2 } from "react/jsx-runtime";
33
- var ProfileColorContext = createContext(void 0);
34
- function ProfileColorProvider({ preview, children }) {
35
- return /* @__PURE__ */ jsx2(ProfileColorContext.Provider, { value: preview, children });
36
- }
37
- function useProfileColorPreview() {
38
- return useContext(ProfileColorContext);
39
- }
40
- function useProfileDisplayColor(hex) {
41
- const preview = useProfileColorPreview();
42
- return preview ? preview(hex) : hex;
43
- }
44
-
45
- // src/ProfileChip.tsx
46
- import { jsx as jsx3 } from "react/jsx-runtime";
47
- function ProfileChip({ profile, size = 22, filled = true }) {
48
- const color = useProfileDisplayColor(profile.color);
49
- return /* @__PURE__ */ jsx3(IdentityChip, { color, tag: profile.tag, icon: "account", size, filled });
50
- }
51
-
52
- // src/ProfilePicker.tsx
53
- import { defaultColors, FadingScrollView, getContrastColor as getContrastColor2 } from "@rific/auto-paper";
54
- import { TouchableRipple } from "@rific/feedback-press";
55
- import { PopoverBody, useAutoAlign } from "@tastic/hud";
56
- import { useMemo, useState } from "react";
57
- import { StyleSheet as StyleSheet2, View as View2 } from "react-native";
58
- import { Icon as Icon2, Text as Text2 } from "react-native-paper";
59
- import { Fragment, jsx as jsx4, jsxs } from "react/jsx-runtime";
38
+ // src/PickerMenu.tsx
39
+ import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
60
40
  var TRIGGER_HEIGHT = 28;
61
41
  var POPOVER_WIDTH = 220;
62
42
  var ROW_HEIGHT = 36;
@@ -83,54 +63,37 @@ function colorPageHeight(swatches) {
83
63
  const rows = Math.ceil(swatches.length / GRID_COLUMNS);
84
64
  return ROW_HEIGHT + GRID_TOP + rows * SWATCH_SIZE + (rows - 1) * GRID_GAP + GRID_BOTTOM;
85
65
  }
86
- function getProfilePickerContentSize(profileCount, hasManageRow = false, colorSection) {
87
- const rowCount = profileCount + 1 + (hasManageRow ? 1 : 0);
66
+ function pickerContentSize(rowCount, colorSection) {
88
67
  const list = rowCount * ROW_HEIGHT + (colorSection ? colorRowHeight(colorSection.caption) + COLOR_ROW_DIVIDER : 0);
89
68
  const content = colorSection ? Math.max(list, colorPageHeight(colorSection.swatches ?? defaultColors)) : list;
90
69
  return { width: POPOVER_WIDTH, height: LIST_PADDING * 2 + content };
91
70
  }
92
- function ProfilePicker({ idPrefix, host, profiles, selectedId, takenId, color, dark, align: forcedAlign, alignOverride, rotation = 0, guestLabel, nullLabel = "Player", nullIcon = "account-off-outline", onSelect, onManage, colorSection, triggerStyle }) {
93
- const id = `${idPrefix}-profile`;
94
- const colorPreview = useProfileColorPreview();
71
+ function PickerMenu({ id, host, color, dark, label, chip, colorSection, homeColor, contentSize, align: forcedAlign, alignOverride, rotation, triggerStyle, footer, children }) {
95
72
  const menuBg = dark ? "#000000" : "#FFFFFF";
96
73
  const fg = dark ? "#FFFFFF" : "#000000";
97
74
  const open = host.openId === id;
98
- const selectedProfile = profiles.find((p) => p.id === selectedId) ?? null;
99
- const sortedProfiles = useMemo(() => [...profiles].sort((a, b) => a.name.localeCompare(b.name)), [profiles]);
100
- const { width: contentWidth, height: contentHeight } = getProfilePickerContentSize(profiles.length, !!onManage, colorSection);
101
- const auto = useAutoAlign(open, contentWidth, contentHeight, rotation);
75
+ const auto = useAutoAlign(open, contentSize.width, contentSize.height, rotation);
102
76
  const placement = alignOverride ?? auto;
103
77
  const { maxHeight, measured, triggerRef, verticalAlign } = placement;
104
78
  const align = forcedAlign ?? placement.align;
105
- const handleSelectGuest = () => {
106
- onSelect(null);
107
- host.close();
108
- };
109
- const handleSelectExisting = (profile) => {
110
- if (profile.id === takenId) return;
111
- onSelect(profile);
112
- host.close();
113
- };
114
- const handleManage = () => {
115
- host.close();
116
- onManage?.();
117
- };
118
- const chipTrigger = !!colorSection;
79
+ const chipTrigger = !!chip;
119
80
  const [triggerWidth, setTriggerWidth] = useState(0);
120
81
  const handleAnchorLayout = chipTrigger ? (e) => setTriggerWidth(e.nativeEvent.layout.width) : void 0;
121
82
  const [colorPage, setColorPage] = useState(false);
83
+ const showColorPage = !!colorSection && colorPage;
122
84
  const handleTriggerPress = () => {
123
85
  setColorPage(false);
124
86
  host.toggle(id);
125
87
  };
126
- const triggerLabel = (selectedProfile?.name ?? guestLabel).toUpperCase();
88
+ const triggerLabel = label.toUpperCase();
89
+ const showFooter = !!footer && !showColorPage;
127
90
  return /* @__PURE__ */ jsxs(View2, { style: [styles2.anchor, open && styles2.anchorOpen], onLayout: handleAnchorLayout, children: [
128
- /* @__PURE__ */ jsx4(TouchableRipple, { onPress: handleTriggerPress, borderless: chipTrigger, style: [styles2.trigger, chipTrigger && styles2.chipTrigger, triggerStyle], accessibilityRole: "button", accessibilityLabel: triggerLabel, children: /* @__PURE__ */ jsxs(View2, { ref: triggerRef, collapsable: false, style: chipTrigger ? styles2.chipTriggerInner : styles2.triggerInner, children: [
129
- chipTrigger && /* @__PURE__ */ jsx4(IdentityChip, { color, tag: selectedProfile?.tag, icon: selectedProfile ? "account" : nullIcon, size: CHIP_SIZE }),
130
- /* @__PURE__ */ jsx4(Text2, { style: [styles2.triggerLabel, { color: chipTrigger ? fg : color }], numberOfLines: 1, children: triggerLabel }),
131
- /* @__PURE__ */ jsx4(Icon2, { source: "menu-down", size: 16, color: chipTrigger ? fg : color })
91
+ /* @__PURE__ */ jsx2(TouchableRipple, { onPress: handleTriggerPress, borderless: chipTrigger, style: [styles2.trigger, chipTrigger && styles2.chipTrigger, triggerStyle], accessibilityRole: "button", accessibilityLabel: triggerLabel, children: /* @__PURE__ */ jsxs(View2, { ref: triggerRef, collapsable: false, style: chipTrigger ? styles2.chipTriggerInner : styles2.triggerInner, children: [
92
+ chip && /* @__PURE__ */ jsx2(IdentityChip, { color, tag: chip.tag, icon: chip.icon, size: CHIP_SIZE }),
93
+ /* @__PURE__ */ jsx2(Text2, { style: [styles2.triggerLabel, { color: chipTrigger ? fg : color }], numberOfLines: 1, children: triggerLabel }),
94
+ /* @__PURE__ */ jsx2(Icon2, { source: "menu-down", size: 16, color: chipTrigger ? fg : color })
132
95
  ] }) }),
133
- /* @__PURE__ */ jsx4(PopoverBody, { visible: open && measured, align, verticalAlign, caretColor: menuBg, caretBorderColor: color, caretBorderWidth: MENU_BORDER_WIDTH, triggerSize: chipTrigger ? triggerWidth : TRIGGER_HEIGHT, children: /* @__PURE__ */ jsx4(
96
+ /* @__PURE__ */ jsx2(PopoverBody, { visible: open && measured, align, verticalAlign, caretColor: menuBg, caretBorderColor: color, caretBorderWidth: MENU_BORDER_WIDTH, triggerSize: chipTrigger ? triggerWidth : TRIGGER_HEIGHT, children: /* @__PURE__ */ jsxs(
134
97
  View2,
135
98
  {
136
99
  style: [
@@ -142,76 +105,82 @@ function ProfilePicker({ idPrefix, host, profiles, selectedId, takenId, color, d
142
105
  maxHeight
143
106
  }
144
107
  ],
145
- children: /* @__PURE__ */ jsx4(FadingScrollView, { contentContainerStyle: styles2.menuContent, showsVerticalScrollIndicator: false, children: colorSection && colorPage ? /* @__PURE__ */ jsx4(ColorPage, { section: colorSection, profileColor: selectedProfile?.color, fg, onBack: () => setColorPage(false) }) : /* @__PURE__ */ jsxs(Fragment, { children: [
146
- colorSection && /* @__PURE__ */ jsx4(ColorRow, { section: colorSection, fg, dividerColor: color, onPress: () => setColorPage(true) }),
147
- /* @__PURE__ */ jsx4(TouchableRipple, { onPress: handleSelectGuest, style: [styles2.row, selectedId === null && { backgroundColor: color }], children: /* @__PURE__ */ jsxs(View2, { style: styles2.rowInner, children: [
148
- /* @__PURE__ */ jsx4(View2, { style: styles2.guestChip, children: /* @__PURE__ */ jsx4(Icon2, { source: nullIcon, size: 13, color: selectedId === null ? getContrastColor2(color) : fg }) }),
149
- /* @__PURE__ */ jsx4(Text2, { style: [styles2.rowLabel, { color: selectedId === null ? getContrastColor2(color) : fg }], children: nullLabel })
150
- ] }) }),
151
- sortedProfiles.map((profile) => {
152
- const taken = profile.id === takenId;
153
- const selected = selectedId === profile.id;
154
- const rowColor = colorPreview ? colorPreview(profile.color) : profile.color;
155
- return /* @__PURE__ */ jsx4(TouchableRipple, { disabled: taken, onPress: () => handleSelectExisting(profile), style: [styles2.row, selected && { backgroundColor: rowColor }], children: /* @__PURE__ */ jsxs(View2, { style: [styles2.rowInner, taken && styles2.rowDisabled], children: [
156
- /* @__PURE__ */ jsx4(ProfileChip, { profile, filled: !selected }),
157
- /* @__PURE__ */ jsx4(
158
- Text2,
159
- {
160
- style: [
161
- styles2.rowLabel,
162
- {
163
- color: selected ? getContrastColor2(rowColor) : fg
164
- }
165
- ],
166
- numberOfLines: 1,
167
- children: profile.name
168
- }
169
- )
170
- ] }) }, profile.id);
171
- }),
172
- onManage && /* @__PURE__ */ jsxs(Fragment, { children: [
173
- /* @__PURE__ */ jsx4(View2, { style: [styles2.divider, { backgroundColor: color }] }),
174
- /* @__PURE__ */ jsx4(TouchableRipple, { onPress: handleManage, style: styles2.row, children: /* @__PURE__ */ jsxs(View2, { style: styles2.manageRowInner, children: [
175
- /* @__PURE__ */ jsx4(Icon2, { source: "account-cog-outline", size: 16, color }),
176
- /* @__PURE__ */ jsx4(Text2, { style: [styles2.manageLabel, { color: fg }], children: "Manage" })
177
- ] }) })
108
+ children: [
109
+ /* @__PURE__ */ jsx2(FadingScrollView, { contentContainerStyle: [styles2.menuContent, showFooter && styles2.menuContentAboveFooter], showsVerticalScrollIndicator: false, children: colorSection && colorPage ? /* @__PURE__ */ jsx2(ColorPage, { section: colorSection, homeColor, fg, onBack: () => setColorPage(false) }) : /* @__PURE__ */ jsxs(Fragment, { children: [
110
+ colorSection && /* @__PURE__ */ jsx2(ColorRow, { section: colorSection, fg, dividerColor: color, onPress: () => setColorPage(true) }),
111
+ children
112
+ ] }) }, showColorPage ? "color" : "list"),
113
+ showFooter && /* @__PURE__ */ jsxs(View2, { style: styles2.menuFooter, children: [
114
+ /* @__PURE__ */ jsx2(View2, { style: [styles2.divider, { backgroundColor: color }] }),
115
+ footer
178
116
  ] })
179
- ] }) }, colorSection && colorPage ? "color" : "list")
117
+ ]
180
118
  }
181
119
  ) })
182
120
  ] });
183
121
  }
122
+ function IconRow({ icon, label, selected, color, fg, disabled = false, onPress }) {
123
+ const textColor = selected ? getContrastColor2(color) : fg;
124
+ return /* @__PURE__ */ jsx2(TouchableRipple, { disabled, onPress, style: [pickerMenuStyles.row, selected && { backgroundColor: color }], children: /* @__PURE__ */ jsxs(View2, { style: [pickerMenuStyles.rowInner, disabled && pickerMenuStyles.rowDisabled], children: [
125
+ /* @__PURE__ */ jsx2(View2, { style: styles2.iconSlot, children: /* @__PURE__ */ jsx2(Icon2, { source: icon, size: 13, color: textColor }) }),
126
+ /* @__PURE__ */ jsx2(Text2, { style: [pickerMenuStyles.rowLabel, { color: textColor }], numberOfLines: 1, children: label })
127
+ ] }) });
128
+ }
184
129
  function ColorRow({ section, fg, dividerColor, onPress }) {
185
130
  const { value, previewValue, label = "Color", caption } = section;
186
131
  const drawn = previewValue ? previewValue(value) : value;
187
132
  return /* @__PURE__ */ jsxs(Fragment, { children: [
188
- /* @__PURE__ */ jsx4(TouchableRipple, { onPress, style: [styles2.row, { height: colorRowHeight(caption) }], accessibilityRole: "button", accessibilityLabel: caption ? `${label}, ${caption}` : label, children: /* @__PURE__ */ jsxs(View2, { style: styles2.rowInner, children: [
189
- /* @__PURE__ */ jsx4(View2, { style: [styles2.colorRowSwatch, { backgroundColor: drawn }] }),
133
+ /* @__PURE__ */ jsx2(TouchableRipple, { onPress, style: [pickerMenuStyles.row, { height: colorRowHeight(caption) }], accessibilityRole: "button", accessibilityLabel: caption ? `${label}, ${caption}` : label, children: /* @__PURE__ */ jsxs(View2, { style: pickerMenuStyles.rowInner, children: [
134
+ /* @__PURE__ */ jsx2(View2, { style: [styles2.colorRowSwatch, { backgroundColor: drawn }] }),
190
135
  /* @__PURE__ */ jsxs(View2, { style: styles2.colorRowText, children: [
191
- /* @__PURE__ */ jsx4(Text2, { style: [styles2.colorRowLabel, { color: fg }], numberOfLines: 1, children: label }),
192
- caption ? /* @__PURE__ */ jsx4(Text2, { style: [styles2.colorRowCaption, { color: fg }], numberOfLines: 1, children: caption }) : null
136
+ /* @__PURE__ */ jsx2(Text2, { style: [styles2.colorRowLabel, { color: fg }], numberOfLines: 1, children: label }),
137
+ caption ? /* @__PURE__ */ jsx2(Text2, { style: [styles2.colorRowCaption, { color: fg }], numberOfLines: 1, children: caption }) : null
193
138
  ] }),
194
- /* @__PURE__ */ jsx4(Icon2, { source: "chevron-right", size: 18, color: fg })
139
+ /* @__PURE__ */ jsx2(Icon2, { source: "chevron-right", size: 18, color: fg })
195
140
  ] }) }),
196
- /* @__PURE__ */ jsx4(View2, { style: [styles2.divider, { backgroundColor: dividerColor }] })
141
+ /* @__PURE__ */ jsx2(View2, { style: [styles2.divider, { backgroundColor: dividerColor }] })
197
142
  ] });
198
143
  }
199
- function ColorPage({ section, profileColor, fg, onBack }) {
200
- const { value, onChange, previewValue, swatches = defaultColors, label = "Color" } = section;
144
+ function ColorPage({ section, homeColor, fg, onBack }) {
145
+ const { value, onChange, previewValue, swatches = defaultColors, label = "Color", takenValue, allowSwapTaken } = section;
201
146
  const displayFor = previewValue ?? ((hex) => hex);
202
147
  return /* @__PURE__ */ jsxs(Fragment, { children: [
203
- /* @__PURE__ */ jsx4(TouchableRipple, { onPress: onBack, style: styles2.row, accessibilityRole: "button", accessibilityLabel: "Back", children: /* @__PURE__ */ jsxs(View2, { style: styles2.rowInner, children: [
204
- /* @__PURE__ */ jsx4(View2, { style: styles2.guestChip, children: /* @__PURE__ */ jsx4(Icon2, { source: "chevron-left", size: 20, color: fg }) }),
205
- /* @__PURE__ */ jsx4(Text2, { style: [styles2.colorRowLabel, { color: fg }], numberOfLines: 1, children: label })
148
+ /* @__PURE__ */ jsx2(TouchableRipple, { onPress: onBack, style: pickerMenuStyles.row, accessibilityRole: "button", accessibilityLabel: "Back", children: /* @__PURE__ */ jsxs(View2, { style: pickerMenuStyles.rowInner, children: [
149
+ /* @__PURE__ */ jsx2(View2, { style: styles2.iconSlot, children: /* @__PURE__ */ jsx2(Icon2, { source: "chevron-left", size: 20, color: fg }) }),
150
+ /* @__PURE__ */ jsx2(Text2, { style: [styles2.colorRowLabel, { color: fg }], numberOfLines: 1, children: label })
206
151
  ] }) }),
207
- /* @__PURE__ */ jsx4(View2, { style: styles2.swatches, children: swatches.map((swatch) => {
152
+ /* @__PURE__ */ jsx2(View2, { style: styles2.swatches, children: swatches.map((swatch) => {
208
153
  const selected = swatch.value.toLowerCase() === value.toLowerCase();
209
- const isProfileColor = !!profileColor && swatch.value.toLowerCase() === profileColor.toLowerCase();
154
+ const isHomeColor = !!homeColor && swatch.value.toLowerCase() === homeColor.toLowerCase();
155
+ const taken = !selected && !!takenValue && swatch.value.toLowerCase() === takenValue.toLowerCase();
156
+ const swappable = taken && !!allowSwapTaken;
210
157
  const swatchDisplay = displayFor(swatch.value);
211
- return /* @__PURE__ */ jsx4(TouchableRipple, { onPress: () => onChange(swatch.value), borderless: true, style: [styles2.swatch, { backgroundColor: swatchDisplay }, selected && styles2.swatchSelected], accessibilityRole: "button", accessibilityLabel: isProfileColor ? `${swatch.label}, profile color` : swatch.label, accessibilityState: { selected }, children: selected ? /* @__PURE__ */ jsx4(Icon2, { source: "check", size: 16, color: getContrastColor2(swatchDisplay) }) : isProfileColor ? /* @__PURE__ */ jsx4(View2, { style: [styles2.profileDot, { backgroundColor: getContrastColor2(swatchDisplay) }] }) : /* @__PURE__ */ jsx4(View2, {}) }, swatch.value);
158
+ const contrast = getContrastColor2(swatchDisplay);
159
+ const accessibilityLabel = swappable ? `${swatch.label}, swap` : taken ? `${swatch.label}, taken` : isHomeColor ? `${swatch.label}, profile color` : swatch.label;
160
+ return /* @__PURE__ */ jsx2(TouchableRipple, { disabled: taken && !swappable, onPress: () => onChange(swatch.value), borderless: true, style: [styles2.swatch, { backgroundColor: swatchDisplay }, selected && styles2.swatchSelected, taken && !swappable && styles2.swatchTaken], accessibilityRole: "button", accessibilityLabel, accessibilityState: { selected, disabled: taken && !swappable }, children: selected ? /* @__PURE__ */ jsx2(Icon2, { source: "check", size: 16, color: contrast }) : swappable ? /* @__PURE__ */ jsx2(Icon2, { source: "swap-horizontal", size: 16, color: contrast }) : taken ? /* @__PURE__ */ jsx2(Icon2, { source: "close", size: 16, color: contrast }) : isHomeColor ? /* @__PURE__ */ jsx2(View2, { style: [styles2.homeDot, { backgroundColor: contrast }] }) : /* @__PURE__ */ jsx2(View2, {}) }, swatch.value);
212
161
  }) })
213
162
  ] });
214
163
  }
164
+ var pickerMenuStyles = StyleSheet2.create({
165
+ row: {
166
+ borderRadius: 8,
167
+ height: ROW_HEIGHT
168
+ },
169
+ rowDisabled: {
170
+ opacity: 0.35
171
+ },
172
+ rowInner: {
173
+ alignItems: "center",
174
+ flexDirection: "row",
175
+ flex: 1,
176
+ gap: 8,
177
+ paddingHorizontal: ROW_INSET
178
+ },
179
+ rowLabel: {
180
+ flex: 1,
181
+ fontSize: 14
182
+ }
183
+ });
215
184
  var styles2 = StyleSheet2.create({
216
185
  anchor: {
217
186
  position: "relative"
@@ -255,38 +224,31 @@ var styles2 = StyleSheet2.create({
255
224
  colorRowText: {
256
225
  flex: 1
257
226
  },
258
- // Separates Manage (a navigation action) from the selectable rows above it - different kind of
259
- // action, not just another row in the same list. Tinted with the seat's own color (passed in as
260
- // backgroundColor) rather than a flat gray, matching the border/caret's own accent-tinted look.
227
+ // Separates the footer (a navigation action) and the Color row from the selectable rows - a
228
+ // different kind of action, not just another row in the same list. Tinted with the seat's own color
229
+ // (passed in as backgroundColor) rather than a flat gray, matching the border/caret's own
230
+ // accent-tinted look.
261
231
  divider: {
262
232
  height: 1,
263
233
  marginVertical: 4,
264
234
  opacity: 0.25
265
235
  },
266
- // Matches ProfileChip's own default size/shape exactly (see this row's own render-site comment)
267
- // - no backgroundColor, unlike ProfileChip's own default: this row has no identity color of its
268
- // own to fill it with, and the render site now colors the icon directly instead.
269
- guestChip: {
236
+ // The selected profile's own color (see ColorPage).
237
+ homeDot: {
238
+ borderRadius: 4,
239
+ height: 8,
240
+ width: 8
241
+ },
242
+ // Matches ProfileChip's own default size/shape exactly (see IconRow's own render-site comment) -
243
+ // no backgroundColor, unlike ProfileChip's own default: an icon row has no identity color of its
244
+ // own to fill it with, and the render site colors the icon directly instead.
245
+ iconSlot: {
270
246
  alignItems: "center",
271
247
  borderRadius: 11,
272
248
  height: 22,
273
249
  justifyContent: "center",
274
250
  width: 22
275
251
  },
276
- manageLabel: {
277
- fontSize: 13,
278
- fontWeight: "700",
279
- letterSpacing: 0.5
280
- },
281
- // Centered rather than left-aligned like a normal row - see this row's own render-site comment
282
- // for why (a footer action, not another selectable identity).
283
- manageRowInner: {
284
- alignItems: "center",
285
- flexDirection: "row",
286
- gap: 6,
287
- height: "100%",
288
- justifyContent: "center"
289
- },
290
252
  menu: {
291
253
  borderRadius: 12,
292
254
  borderWidth: MENU_BORDER_WIDTH,
@@ -302,29 +264,13 @@ var styles2 = StyleSheet2.create({
302
264
  menuContent: {
303
265
  padding: LIST_PADDING
304
266
  },
305
- // The selected profile's own color (see ColorSection).
306
- profileDot: {
307
- borderRadius: 4,
308
- height: 8,
309
- width: 8
267
+ // With a footer pinned below, its own padding takes the bottom edge (same total height as before).
268
+ menuContentAboveFooter: {
269
+ paddingBottom: 0
310
270
  },
311
- row: {
312
- borderRadius: 8,
313
- height: ROW_HEIGHT
314
- },
315
- rowDisabled: {
316
- opacity: 0.35
317
- },
318
- rowInner: {
319
- alignItems: "center",
320
- flexDirection: "row",
321
- flex: 1,
322
- gap: 8,
323
- paddingHorizontal: ROW_INSET
324
- },
325
- rowLabel: {
326
- flex: 1,
327
- fontSize: 14
271
+ menuFooter: {
272
+ paddingBottom: LIST_PADDING,
273
+ paddingHorizontal: LIST_PADDING
328
274
  },
329
275
  swatch: {
330
276
  alignItems: "center",
@@ -337,6 +283,9 @@ var styles2 = StyleSheet2.create({
337
283
  borderColor: "#ffffff",
338
284
  borderWidth: 2
339
285
  },
286
+ swatchTaken: {
287
+ opacity: 0.35
288
+ },
340
289
  swatches: {
341
290
  columnGap: GRID_GAP,
342
291
  flexDirection: "row",
@@ -363,15 +312,128 @@ var styles2 = StyleSheet2.create({
363
312
  }
364
313
  });
365
314
 
366
- // src/ProfilesManager.tsx
367
- import { defaultColors as defaultColors2, useAutoPaperTheme } from "@rific/auto-paper";
315
+ // src/OptionPicker.tsx
316
+ import { jsx as jsx3 } from "react/jsx-runtime";
317
+ function getOptionPickerContentSize(optionCount, colorSection) {
318
+ return pickerContentSize(optionCount, colorSection);
319
+ }
320
+ function OptionPicker({ id, host, options, value, onChange, color, dark, icon, rowIcon = icon, colorSection, align, alignOverride, rotation = 0, triggerStyle }) {
321
+ const fg = dark ? "#FFFFFF" : "#000000";
322
+ const selected = options.find((option) => option.value === value);
323
+ const handleSelect = (option) => {
324
+ onChange(option.value);
325
+ host.close();
326
+ };
327
+ return /* @__PURE__ */ jsx3(PickerMenu, { id, host, color, dark, label: selected?.label ?? "", chip: { icon }, colorSection, contentSize: getOptionPickerContentSize(options.length, colorSection), align, alignOverride, rotation, triggerStyle, children: options.map((option) => /* @__PURE__ */ jsx3(IconRow, { icon: option.icon ?? rowIcon, label: option.label, selected: option.value === value, color, fg, onPress: () => handleSelect(option) }, option.value)) });
328
+ }
329
+
330
+ // src/ProfileColorContext.tsx
331
+ import { createContext, useContext } from "react";
332
+ import { jsx as jsx4 } from "react/jsx-runtime";
333
+ var ProfileColorContext = createContext(void 0);
334
+ function ProfileColorProvider({ preview, children }) {
335
+ return /* @__PURE__ */ jsx4(ProfileColorContext.Provider, { value: preview, children });
336
+ }
337
+ function useProfileColorPreview() {
338
+ return useContext(ProfileColorContext);
339
+ }
340
+ function useProfileDisplayColor(hex) {
341
+ const preview = useProfileColorPreview();
342
+ return preview ? preview(hex) : hex;
343
+ }
344
+
345
+ // src/ProfileChip.tsx
346
+ import { jsx as jsx5 } from "react/jsx-runtime";
347
+ function ProfileChip({ profile, size = 22, filled = true }) {
348
+ const color = useProfileDisplayColor(profile.color);
349
+ return /* @__PURE__ */ jsx5(IdentityChip, { color, tag: profile.tag, icon: "account", size, filled });
350
+ }
351
+
352
+ // src/ProfilePicker.tsx
353
+ import { getContrastColor as getContrastColor3 } from "@rific/auto-paper";
368
354
  import { TouchableRipple as TouchableRipple2 } from "@rific/feedback-press";
355
+ import { useMemo } from "react";
356
+ import { StyleSheet as StyleSheet3, View as View3 } from "react-native";
357
+ import { Icon as Icon3, Text as Text3 } from "react-native-paper";
358
+ import { jsx as jsx6, jsxs as jsxs2 } from "react/jsx-runtime";
359
+ function getProfilePickerContentSize(profileCount, hasManageRow = false, colorSection) {
360
+ return pickerContentSize(profileCount + 1 + (hasManageRow ? 1 : 0), colorSection);
361
+ }
362
+ function ProfilePicker({ idPrefix, host, profiles, selectedId, takenId, color, dark, align, alignOverride, rotation = 0, guestLabel, nullLabel = "Player", nullIcon = "account-off-outline", onSelect, onManage, colorSection, triggerStyle }) {
363
+ const id = `${idPrefix}-profile`;
364
+ const colorPreview = useProfileColorPreview();
365
+ const fg = dark ? "#FFFFFF" : "#000000";
366
+ const selectedProfile = profiles.find((p) => p.id === selectedId) ?? null;
367
+ const sortedProfiles = useMemo(() => [...profiles].sort((a, b) => a.name.localeCompare(b.name)), [profiles]);
368
+ const contentSize = getProfilePickerContentSize(profiles.length, !!onManage, colorSection);
369
+ const handleSelectGuest = () => {
370
+ onSelect(null);
371
+ host.close();
372
+ };
373
+ const handleSelectExisting = (profile) => {
374
+ if (profile.id === takenId) return;
375
+ onSelect(profile);
376
+ host.close();
377
+ };
378
+ const handleManage = () => {
379
+ host.close();
380
+ onManage?.();
381
+ };
382
+ const manageFooter = onManage ? /* @__PURE__ */ jsx6(TouchableRipple2, { onPress: handleManage, style: pickerMenuStyles.row, children: /* @__PURE__ */ jsxs2(View3, { style: styles3.manageRowInner, children: [
383
+ /* @__PURE__ */ jsx6(Icon3, { source: "account-cog-outline", size: 16, color }),
384
+ /* @__PURE__ */ jsx6(Text3, { style: [styles3.manageLabel, { color: fg }], children: "Manage" })
385
+ ] }) }) : void 0;
386
+ return /* @__PURE__ */ jsxs2(PickerMenu, { id, host, color, dark, label: selectedProfile?.name ?? guestLabel, chip: colorSection ? { tag: selectedProfile?.tag, icon: selectedProfile ? "account" : nullIcon } : void 0, colorSection, homeColor: selectedProfile?.color, contentSize, align, alignOverride, rotation, triggerStyle, footer: manageFooter, children: [
387
+ /* @__PURE__ */ jsx6(IconRow, { icon: nullIcon, label: nullLabel, selected: selectedId === null, color, fg, onPress: handleSelectGuest }),
388
+ sortedProfiles.map((profile) => {
389
+ const taken = profile.id === takenId;
390
+ const selected = selectedId === profile.id;
391
+ const rowColor = colorPreview ? colorPreview(profile.color) : profile.color;
392
+ return /* @__PURE__ */ jsx6(TouchableRipple2, { disabled: taken, onPress: () => handleSelectExisting(profile), style: [pickerMenuStyles.row, selected && { backgroundColor: rowColor }], children: /* @__PURE__ */ jsxs2(View3, { style: [pickerMenuStyles.rowInner, taken && pickerMenuStyles.rowDisabled], children: [
393
+ /* @__PURE__ */ jsx6(ProfileChip, { profile, filled: !selected }),
394
+ /* @__PURE__ */ jsx6(
395
+ Text3,
396
+ {
397
+ style: [
398
+ pickerMenuStyles.rowLabel,
399
+ {
400
+ color: selected ? getContrastColor3(rowColor) : fg
401
+ }
402
+ ],
403
+ numberOfLines: 1,
404
+ children: profile.name
405
+ }
406
+ )
407
+ ] }) }, profile.id);
408
+ })
409
+ ] });
410
+ }
411
+ var styles3 = StyleSheet3.create({
412
+ manageLabel: {
413
+ fontSize: 13,
414
+ fontWeight: "700",
415
+ letterSpacing: 0.5
416
+ },
417
+ // Centered rather than left-aligned like a normal row - see the footer's own comment for why (a
418
+ // footer action, not another selectable identity).
419
+ manageRowInner: {
420
+ alignItems: "center",
421
+ flexDirection: "row",
422
+ gap: 6,
423
+ height: "100%",
424
+ justifyContent: "center"
425
+ }
426
+ });
427
+
428
+ // src/ProfilesManager.tsx
429
+ import { defaultColors as defaultColors2, FadingScrollView as FadingScrollView2, useAutoPaperTheme } from "@rific/auto-paper";
430
+ import { TouchableRipple as TouchableRipple3 } from "@rific/feedback-press";
369
431
  import { useFocusChain } from "@rific/focus-chain";
370
432
  import { useToast } from "@rific/toaster";
371
433
  import { ConfirmDialog, getInlineColorPickerContentSize, InlineColorPicker, usePopoverHost } from "@tastic/hud";
372
434
  import { useCallback, useEffect, useImperativeHandle, useMemo as useMemo2, useRef, useState as useState2 } from "react";
373
- import { KeyboardAvoidingView, Platform, ScrollView, StyleSheet as StyleSheet3, useWindowDimensions, View as View3 } from "react-native";
374
- import { Icon as Icon3, Text as Text3, TextInput } from "react-native-paper";
435
+ import { KeyboardAvoidingView, Platform, StyleSheet as StyleSheet4, useWindowDimensions, View as View4 } from "react-native";
436
+ import { Icon as Icon4, Text as Text4, TextInput } from "react-native-paper";
375
437
  import { useSafeAreaInsets } from "react-native-safe-area-context";
376
438
 
377
439
  // src/profilesValidation.ts
@@ -407,7 +469,7 @@ function isValidProfile(value) {
407
469
  }
408
470
 
409
471
  // src/ProfilesManager.tsx
410
- import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
472
+ import { Fragment as Fragment2, jsx as jsx7, jsxs as jsxs3 } from "react/jsx-runtime";
411
473
  var CHIP_SIZE2 = 40;
412
474
  function deriveTag(name) {
413
475
  const trimmed = name.trim();
@@ -484,38 +546,38 @@ function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, h
484
546
  }, [confirmDeleteId, editingId, onDelete]);
485
547
  const sortedProfiles = useMemo2(() => [...profiles].sort((a, b) => a.name.localeCompare(b.name)), [profiles]);
486
548
  const rows = [...sortedProfiles.map((profile) => ({ kind: "profile", profile })), { kind: "new" }];
487
- return /* @__PURE__ */ jsxs2(Fragment2, { children: [
488
- /* @__PURE__ */ jsxs2(View3, { style: [styles3.header, { paddingTop: 8 + insets.top, paddingLeft: 8 + insets.left }], children: [
549
+ return /* @__PURE__ */ jsxs3(Fragment2, { children: [
550
+ /* @__PURE__ */ jsxs3(View4, { style: [styles4.header, { paddingTop: 8 + insets.top, paddingLeft: 8 + insets.left }], children: [
489
551
  headerLeft,
490
- /* @__PURE__ */ jsx5(Text3, { variant: titleVariant, style: [styles3.title, { color: fg }], children: "Profiles" })
552
+ /* @__PURE__ */ jsx7(Text4, { variant: titleVariant, style: [styles4.title, { color: fg }], children: "Profiles" })
491
553
  ] }),
492
- /* @__PURE__ */ jsx5(KeyboardAvoidingView, { behavior: Platform.OS === "ios" ? "padding" : void 0, style: styles3.keyboardAvoiding, children: /* @__PURE__ */ jsx5(ScrollView, { style: styles3.scrollView, contentContainerStyle: [styles3.content, { paddingBottom: 32 + insets.bottom }], showsVerticalScrollIndicator: false, keyboardShouldPersistTaps: "handled", children: rows.map((row) => {
554
+ /* @__PURE__ */ jsx7(KeyboardAvoidingView, { behavior: Platform.OS === "ios" ? "padding" : void 0, style: styles4.keyboardAvoiding, children: /* @__PURE__ */ jsx7(FadingScrollView2, { style: styles4.scrollView, contentContainerStyle: [styles4.content, { paddingBottom: 32 + insets.bottom }], showsVerticalScrollIndicator: false, keyboardShouldPersistTaps: "handled", children: rows.map((row) => {
493
555
  if (row.kind === "new") {
494
556
  if (editingId !== "new") {
495
- return /* @__PURE__ */ jsx5(TouchableRipple2, { onPress: () => startEdit("new"), style: styles3.row, children: /* @__PURE__ */ jsxs2(View3, { style: styles3.rowInner, children: [
496
- /* @__PURE__ */ jsx5(View3, { style: [styles3.rowChip, styles3.newChip, { borderColor: fg }], children: /* @__PURE__ */ jsx5(Icon3, { source: "plus", size: 20, color: fg }) }),
497
- /* @__PURE__ */ jsx5(Text3, { variant: "bodyLarge", style: { color: fg }, children: "New Profile" })
557
+ return /* @__PURE__ */ jsx7(TouchableRipple3, { onPress: () => startEdit("new"), style: styles4.row, children: /* @__PURE__ */ jsxs3(View4, { style: styles4.rowInner, children: [
558
+ /* @__PURE__ */ jsx7(View4, { style: [styles4.rowChip, styles4.newChip, { borderColor: fg }], children: /* @__PURE__ */ jsx7(Icon4, { source: "plus", size: 20, color: fg }) }),
559
+ /* @__PURE__ */ jsx7(Text4, { variant: "bodyLarge", style: { color: fg }, children: "New Profile" })
498
560
  ] }) }, "new");
499
561
  }
500
- return /* @__PURE__ */ jsx5(EditRow, { draftName, onNameChange: handleNameChange, draftColor, onColorChange: setDraftColor, draftTag, onTagChange: handleTagChange, onSubmit: commitEdit, autoFocus: true, fgMuted, host, dark, colorPreview, useColorPickerAlign }, "new");
562
+ return /* @__PURE__ */ jsx7(EditRow, { draftName, onNameChange: handleNameChange, draftColor, onColorChange: setDraftColor, draftTag, onTagChange: handleTagChange, onSubmit: commitEdit, autoFocus: true, fgMuted, host, dark, colorPreview, useColorPickerAlign }, "new");
501
563
  }
502
564
  const { profile } = row;
503
565
  if (editingId === profile.id) {
504
- return /* @__PURE__ */ jsx5(EditRow, { draftName, onNameChange: handleNameChange, draftColor, onColorChange: setDraftColor, draftTag, onTagChange: handleTagChange, onSubmit: commitEdit, onDelete: () => setConfirmDeleteId(profile.id), fgMuted, host, dark, colorPreview, useColorPickerAlign }, profile.id);
566
+ return /* @__PURE__ */ jsx7(EditRow, { draftName, onNameChange: handleNameChange, draftColor, onColorChange: setDraftColor, draftTag, onTagChange: handleTagChange, onSubmit: commitEdit, onDelete: () => setConfirmDeleteId(profile.id), fgMuted, host, dark, colorPreview, useColorPickerAlign }, profile.id);
505
567
  }
506
- return /* @__PURE__ */ jsx5(TouchableRipple2, { onPress: () => startEdit(profile), style: styles3.row, children: /* @__PURE__ */ jsxs2(View3, { style: styles3.rowInner, children: [
507
- /* @__PURE__ */ jsx5(ProfileChip, { profile, size: CHIP_SIZE2 }),
508
- /* @__PURE__ */ jsx5(Text3, { variant: "bodyLarge", style: [styles3.rowName, { color: fg }], numberOfLines: 1, children: profile.name })
568
+ return /* @__PURE__ */ jsx7(TouchableRipple3, { onPress: () => startEdit(profile), style: styles4.row, children: /* @__PURE__ */ jsxs3(View4, { style: styles4.rowInner, children: [
569
+ /* @__PURE__ */ jsx7(ProfileChip, { profile, size: CHIP_SIZE2 }),
570
+ /* @__PURE__ */ jsx7(Text4, { variant: "bodyLarge", style: [styles4.rowName, { color: fg }], numberOfLines: 1, children: profile.name })
509
571
  ] }) }, profile.id);
510
572
  }) }) }),
511
- /* @__PURE__ */ jsx5(ConfirmDialog, { visible: confirmDeleteProfile !== null, title: "Delete Profile?", message: confirmDeleteProfile ? `This permanently deletes ${confirmDeleteProfile.name}\u2019s profile. This cannot be undone.` : "", confirmLabel: "Delete", cancelLabel: "Cancel", icon: "alert-outline", destructive: true, onConfirm: handleConfirmDelete, onCancel: () => setConfirmDeleteId(null) })
573
+ /* @__PURE__ */ jsx7(ConfirmDialog, { visible: confirmDeleteProfile !== null, title: "Delete Profile?", message: confirmDeleteProfile ? `This permanently deletes ${confirmDeleteProfile.name}\u2019s profile. This cannot be undone.` : "", confirmLabel: "Delete", cancelLabel: "Cancel", icon: "alert-outline", destructive: true, onConfirm: handleConfirmDelete, onCancel: () => setConfirmDeleteId(null) })
512
574
  ] });
513
575
  }
514
576
  function AlignedColorPicker({ useAlign, ...props }) {
515
577
  const { width: windowWidth } = useWindowDimensions();
516
578
  const { width, height } = getInlineColorPickerContentSize(defaultColors2.length, windowWidth);
517
579
  const alignOverride = useAlign(props.host.openId === props.id, width, height);
518
- return /* @__PURE__ */ jsx5(InlineColorPicker, { ...props, alignOverride });
580
+ return /* @__PURE__ */ jsx7(InlineColorPicker, { ...props, alignOverride });
519
581
  }
520
582
  function EditRow({ draftName, onNameChange, draftColor, onColorChange, draftTag, onTagChange, onSubmit, onDelete, autoFocus, fgMuted, host, dark, colorPreview, useColorPickerAlign }) {
521
583
  const register = useFocusChain();
@@ -528,14 +590,14 @@ function EditRow({ draftName, onNameChange, draftColor, onColorChange, draftTag,
528
590
  useEffect(() => {
529
591
  if (host.openId === "color") tagFocusRef.current();
530
592
  }, [host.openId]);
531
- return /* @__PURE__ */ jsxs2(View3, { style: styles3.editRow, children: [
532
- useColorPickerAlign ? /* @__PURE__ */ jsx5(AlignedColorPicker, { useAlign: useColorPickerAlign, id: "color", host, value: draftColor, onChange: onColorChange, previewValue: colorPreview, tag: draftTag, dark }) : /* @__PURE__ */ jsx5(InlineColorPicker, { id: "color", host, value: draftColor, onChange: onColorChange, previewValue: colorPreview, tag: draftTag, dark }),
533
- /* @__PURE__ */ jsx5(TextInput, { ref: tag.ref, ...tag.props, value: draftTag, onChangeText: onTagChange, accessibilityLabel: "Tag", autoCapitalize: "characters", selectTextOnFocus: true, returnKeyType: "next", style: styles3.hiddenTagInput, pointerEvents: "none" }),
534
- /* @__PURE__ */ jsx5(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 }),
535
- onDelete && /* @__PURE__ */ jsx5(TouchableRipple2, { onPress: onDelete, style: styles3.iconButton, children: /* @__PURE__ */ jsx5(Icon3, { source: "trash-can-outline", size: 18, color: fgMuted }) })
593
+ return /* @__PURE__ */ jsxs3(View4, { style: styles4.editRow, children: [
594
+ useColorPickerAlign ? /* @__PURE__ */ jsx7(AlignedColorPicker, { useAlign: useColorPickerAlign, id: "color", host, value: draftColor, onChange: onColorChange, previewValue: colorPreview, tag: draftTag, dark }) : /* @__PURE__ */ jsx7(InlineColorPicker, { id: "color", host, value: draftColor, onChange: onColorChange, previewValue: colorPreview, tag: draftTag, dark }),
595
+ /* @__PURE__ */ jsx7(TextInput, { ref: tag.ref, ...tag.props, value: draftTag, onChangeText: onTagChange, accessibilityLabel: "Tag", autoCapitalize: "characters", selectTextOnFocus: true, returnKeyType: "next", style: styles4.hiddenTagInput, pointerEvents: "none" }),
596
+ /* @__PURE__ */ jsx7(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: styles4.nameInput }),
597
+ onDelete && /* @__PURE__ */ jsx7(TouchableRipple3, { onPress: onDelete, style: styles4.iconButton, children: /* @__PURE__ */ jsx7(Icon4, { source: "trash-can-outline", size: 18, color: fgMuted }) })
536
598
  ] });
537
599
  }
538
- var styles3 = StyleSheet3.create({
600
+ var styles4 = StyleSheet4.create({
539
601
  content: {
540
602
  gap: 4,
541
603
  paddingHorizontal: 20
@@ -617,8 +679,8 @@ var styles3 = StyleSheet3.create({
617
679
  import { useAutoPaperTheme as useAutoPaperTheme2 } from "@rific/auto-paper";
618
680
  import { IconButton } from "@rific/feedback-press";
619
681
  import { useRef as useRef2 } from "react";
620
- import { StyleSheet as StyleSheet4, View as View4 } from "react-native";
621
- import { jsx as jsx6 } from "react/jsx-runtime";
682
+ import { StyleSheet as StyleSheet5, View as View5 } from "react-native";
683
+ import { jsx as jsx8 } from "react/jsx-runtime";
622
684
  function ProfilesScreen({ profiles, defaultColor, onCreate, onSave, onDelete, onBack, fg: fgOverride, fgMuted, bg: bgOverride, titleVariant, colorPreview, useColorPickerAlign }) {
623
685
  const { dark } = useAutoPaperTheme2();
624
686
  const managerRef = useRef2(null);
@@ -628,9 +690,9 @@ function ProfilesScreen({ profiles, defaultColor, onCreate, onSave, onDelete, on
628
690
  managerRef.current?.commitPendingEdit();
629
691
  onBack();
630
692
  };
631
- return /* @__PURE__ */ jsx6(View4, { style: [styles4.container, { backgroundColor: bg }], children: /* @__PURE__ */ jsx6(ProfilesManager, { ref: managerRef, profiles, defaultColor, onCreate, onSave, onDelete, fg, fgMuted, titleVariant, colorPreview, useColorPickerAlign, headerLeft: /* @__PURE__ */ jsx6(IconButton, { icon: "arrow-left", iconColor: fg, size: 24, onPress: handleBack, accessibilityLabel: "Back" }) }) });
693
+ return /* @__PURE__ */ jsx8(View5, { style: [styles5.container, { backgroundColor: bg }], children: /* @__PURE__ */ jsx8(ProfilesManager, { ref: managerRef, profiles, defaultColor, onCreate, onSave, onDelete, fg, fgMuted, titleVariant, colorPreview, useColorPickerAlign, headerLeft: /* @__PURE__ */ jsx8(IconButton, { icon: "arrow-left", iconColor: fg, size: 24, onPress: handleBack, accessibilityLabel: "Back" }) }) });
632
694
  }
633
- var styles4 = StyleSheet4.create({
695
+ var styles5 = StyleSheet5.create({
634
696
  container: {
635
697
  flex: 1
636
698
  }
@@ -794,21 +856,52 @@ async function resolveInitialProfiles(groupId, localFallback) {
794
856
 
795
857
  // src/redux/seatColorsSlice.ts
796
858
  var REHYDRATE2 = "persist/REHYDRATE";
859
+ function sameColor(a, b) {
860
+ return a.toLowerCase() === b.toLowerCase();
861
+ }
862
+ function profileSeatColor(profile, profileColors) {
863
+ const pick = profileColors[profile.id];
864
+ return pick && sameColor(pick.profileColor, profile.color) ? pick.color : profile.color;
865
+ }
797
866
  function createSeatColorsSlice(namespace, defaultState, mountKey = "seatColors") {
798
867
  const setLastCpuColor = createActionCreator(`seatColors/${namespace}/setLastCpuColor`);
799
868
  const setLastGuestColor = createActionCreator(`seatColors/${namespace}/setLastGuestColor`);
800
- const setProfileOverride = createActionCreator(`seatColors/${namespace}/setProfileOverride`);
869
+ const setProfileColor = createActionCreator(`seatColors/${namespace}/setProfileColor`);
870
+ function withoutPick(state, id) {
871
+ if (!state.profileColors[id]) return state;
872
+ const profileColors = { ...state.profileColors };
873
+ delete profileColors[id];
874
+ return { ...state, profileColors };
875
+ }
801
876
  function reducer(state = defaultState, action) {
802
877
  if (action.type === REHYDRATE2) {
803
878
  const persisted = action.payload?.[mountKey];
804
- return { ...defaultState, ...state, ...persisted };
879
+ const next = { ...defaultState, ...state, ...persisted };
880
+ delete next.profileOverride;
881
+ return next;
805
882
  }
806
883
  if (setLastCpuColor.match(action)) return { ...state, lastCpuColor: action.payload };
807
884
  if (setLastGuestColor.match(action)) return { ...state, lastGuestColor: { ...state.lastGuestColor, [action.payload.seat]: action.payload.color } };
808
- if (setProfileOverride.match(action)) return { ...state, profileOverride: { ...state.profileOverride, [action.payload.seat]: action.payload.color } };
885
+ if (setProfileColor.match(action)) {
886
+ const { profile, color } = action.payload;
887
+ if (color === null || sameColor(color, profile.color)) return withoutPick(state, profile.id);
888
+ return { ...state, profileColors: { ...state.profileColors, [profile.id]: { color, profileColor: profile.color } } };
889
+ }
890
+ if (profilesActions.remove.match(action)) return withoutPick(state, action.payload);
891
+ if (profilesActions.update.match(action)) {
892
+ const { id, patch } = action.payload;
893
+ const pick = state.profileColors[id];
894
+ return pick && patch.color !== void 0 && !sameColor(patch.color, pick.profileColor) ? withoutPick(state, id) : state;
895
+ }
896
+ if (profilesActions.setAll.match(action)) {
897
+ return action.payload.reduce((next, profile) => {
898
+ const pick = next.profileColors[profile.id];
899
+ return pick && !sameColor(profile.color, pick.profileColor) ? withoutPick(next, profile.id) : next;
900
+ }, state);
901
+ }
809
902
  return state;
810
903
  }
811
- return { actions: { setLastCpuColor, setLastGuestColor, setProfileOverride }, reducer };
904
+ return { actions: { setLastCpuColor, setLastGuestColor, setProfileColor }, reducer };
812
905
  }
813
906
 
814
907
  // src/redux/useSharedProfilesSync.ts
@@ -857,6 +950,7 @@ function avoidSeatColorClash(p1Color, p2Color, defaults) {
857
950
  export {
858
951
  MAX_PROFILE_NAME_LENGTH,
859
952
  MAX_TAG_LENGTH,
953
+ OptionPicker,
860
954
  ProfileChip,
861
955
  ProfileColorProvider,
862
956
  ProfilePicker,
@@ -867,11 +961,13 @@ export {
867
961
  createProfileRecord,
868
962
  createProfileSelectionSlice,
869
963
  createSeatColorsSlice,
964
+ getOptionPickerContentSize,
870
965
  getProfilePickerContentSize,
871
966
  isSharedProfileStoreAvailable,
872
967
  isValidProfile,
873
968
  isValidTag,
874
969
  loadSharedProfiles,
970
+ profileSeatColor,
875
971
  profilesActions,
876
972
  profilesReducer,
877
973
  readSharedProfiles,