@tastic/profile 0.14.1 → 0.16.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.js CHANGED
@@ -22,6 +22,7 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  MAX_PROFILE_NAME_LENGTH: () => MAX_PROFILE_NAME_LENGTH,
24
24
  MAX_TAG_LENGTH: () => MAX_TAG_LENGTH,
25
+ OptionPicker: () => OptionPicker,
25
26
  ProfileChip: () => ProfileChip,
26
27
  ProfileColorProvider: () => ProfileColorProvider,
27
28
  ProfilePicker: () => ProfilePicker,
@@ -32,34 +33,87 @@ __export(index_exports, {
32
33
  createProfileRecord: () => createProfileRecord,
33
34
  createProfileSelectionSlice: () => createProfileSelectionSlice,
34
35
  createSeatColorsSlice: () => createSeatColorsSlice,
36
+ getOptionPickerContentSize: () => getOptionPickerContentSize,
35
37
  getProfilePickerContentSize: () => getProfilePickerContentSize,
36
38
  isSharedProfileStoreAvailable: () => isSharedProfileStoreAvailable,
37
39
  isValidProfile: () => isValidProfile,
38
40
  isValidTag: () => isValidTag,
39
41
  loadSharedProfiles: () => loadSharedProfiles,
42
+ profileSeatColor: () => profileSeatColor,
40
43
  profilesActions: () => profilesActions,
41
44
  profilesReducer: () => profilesReducer,
42
45
  readSharedProfiles: () => readSharedProfiles,
43
46
  resolveInitialProfiles: () => resolveInitialProfiles,
44
47
  saveSharedProfiles: () => saveSharedProfiles,
48
+ subscribeSharedProfiles: () => subscribeSharedProfiles,
45
49
  useProfileColorPreview: () => useProfileColorPreview,
46
50
  useProfileDisplayColor: () => useProfileDisplayColor,
51
+ useProfileSwatchRenderer: () => useProfileSwatchRenderer,
47
52
  useSharedProfilesSync: () => useSharedProfilesSync
48
53
  });
49
54
  module.exports = __toCommonJS(index_exports);
50
55
 
56
+ // src/PickerMenu.tsx
57
+ var import_auto_paper2 = require("@rific/auto-paper");
58
+ var import_feedback_press = require("@rific/feedback-press");
59
+ var import_chrome = require("@tastic/hud/chrome");
60
+ var import_react2 = require("react");
61
+ var import_react_native3 = require("react-native");
62
+ var import_react_native_paper2 = require("react-native-paper");
63
+
51
64
  // src/IdentityChip.tsx
52
65
  var import_auto_paper = require("@rific/auto-paper");
53
- var import_react_native = require("react-native");
66
+ var import_react_native2 = require("react-native");
54
67
  var import_react_native_paper = require("react-native-paper");
68
+
69
+ // src/ProfileColorContext.tsx
70
+ var import_react = require("react");
55
71
  var import_jsx_runtime = require("react/jsx-runtime");
72
+ var ProfileColorContext = (0, import_react.createContext)({});
73
+ function ProfileColorProvider({ preview, renderSwatch, children }) {
74
+ const value = (0, import_react.useMemo)(() => ({ preview, renderSwatch }), [preview, renderSwatch]);
75
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ProfileColorContext.Provider, { value, children });
76
+ }
77
+ function useProfileColorPreview() {
78
+ return (0, import_react.useContext)(ProfileColorContext).preview;
79
+ }
80
+ function useProfileSwatchRenderer() {
81
+ return (0, import_react.useContext)(ProfileColorContext).renderSwatch;
82
+ }
83
+ function useProfileDisplayColor(hex) {
84
+ const preview = useProfileColorPreview();
85
+ return preview ? preview(hex) : hex;
86
+ }
87
+
88
+ // src/SwatchLayer.tsx
89
+ var import_react_native = require("react-native");
90
+ var import_jsx_runtime2 = require("react/jsx-runtime");
91
+ function SwatchLayer({ render, color, size }) {
92
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native.View, { style: [styles.layer, { borderRadius: size / 2, height: size, width: size }], children: render(color, size) });
93
+ }
94
+ var styles = import_react_native.StyleSheet.create({
95
+ layer: {
96
+ left: 0,
97
+ overflow: "hidden",
98
+ pointerEvents: "none",
99
+ position: "absolute",
100
+ top: 0
101
+ }
102
+ });
103
+
104
+ // src/IdentityChip.tsx
105
+ var import_jsx_runtime3 = require("react/jsx-runtime");
56
106
  var EMOJI_PATTERN = /\p{Extended_Pictographic}/u;
57
107
  function IdentityChip({ color, tag, icon, size, filled = true }) {
108
+ const renderSwatch = useProfileSwatchRenderer();
58
109
  const contrastColor = (0, import_auto_paper.getContrastColor)(color);
59
110
  const tagFontSize = size * (tag && EMOJI_PATTERN.test(tag) ? 0.6 : tag && Array.from(tag).length >= 3 ? 0.32 : 0.4);
60
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native.View, { style: [styles.chip, { borderRadius: size / 2, height: size, width: size }, filled && { backgroundColor: color }], children: tag ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native_paper.Text, { style: [styles.tag, { color: contrastColor, fontSize: tagFontSize }], numberOfLines: 1, adjustsFontSizeToFit: true, children: tag }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native_paper.Icon, { source: icon, size: size * 0.6, color: contrastColor }) });
111
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_react_native2.View, { style: [styles2.chip, { borderRadius: size / 2, height: size, width: size }, filled && !renderSwatch && { backgroundColor: color }], children: [
112
+ filled && renderSwatch && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SwatchLayer, { render: renderSwatch, color, size }),
113
+ tag ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native_paper.Text, { style: [styles2.tag, { color: contrastColor, fontSize: tagFontSize }], numberOfLines: 1, adjustsFontSizeToFit: true, children: tag }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native_paper.Icon, { source: icon, size: size * 0.6, color: contrastColor })
114
+ ] });
61
115
  }
62
- var styles = import_react_native.StyleSheet.create({
116
+ var styles2 = import_react_native2.StyleSheet.create({
63
117
  chip: {
64
118
  alignItems: "center",
65
119
  justifyContent: "center"
@@ -70,35 +124,7 @@ var styles = import_react_native.StyleSheet.create({
70
124
  }
71
125
  });
72
126
 
73
- // src/ProfileColorContext.tsx
74
- var import_react = require("react");
75
- var import_jsx_runtime2 = require("react/jsx-runtime");
76
- var ProfileColorContext = (0, import_react.createContext)(void 0);
77
- function ProfileColorProvider({ preview, children }) {
78
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ProfileColorContext.Provider, { value: preview, children });
79
- }
80
- function useProfileColorPreview() {
81
- return (0, import_react.useContext)(ProfileColorContext);
82
- }
83
- function useProfileDisplayColor(hex) {
84
- const preview = useProfileColorPreview();
85
- return preview ? preview(hex) : hex;
86
- }
87
-
88
- // src/ProfileChip.tsx
89
- var import_jsx_runtime3 = require("react/jsx-runtime");
90
- function ProfileChip({ profile, size = 22, filled = true }) {
91
- const color = useProfileDisplayColor(profile.color);
92
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(IdentityChip, { color, tag: profile.tag, icon: "account", size, filled });
93
- }
94
-
95
- // src/ProfilePicker.tsx
96
- var import_auto_paper2 = require("@rific/auto-paper");
97
- var import_feedback_press = require("@rific/feedback-press");
98
- var import_hud = require("@tastic/hud");
99
- var import_react2 = require("react");
100
- var import_react_native2 = require("react-native");
101
- var import_react_native_paper2 = require("react-native-paper");
127
+ // src/PickerMenu.tsx
102
128
  var import_jsx_runtime4 = require("react/jsx-runtime");
103
129
  var TRIGGER_HEIGHT = 28;
104
130
  var POPOVER_WIDTH = 220;
@@ -110,6 +136,8 @@ var CHIP_SIZE = 32;
110
136
  var CHIP_TRIGGER_PADDING = 6;
111
137
  var SWATCH_SIZE = 28;
112
138
  var SWATCH_MIN_GAP = 6;
139
+ var SWATCH_RING_WIDTH = 2;
140
+ var COLOR_ROW_SWATCH_SIZE = 22;
113
141
  var COLOR_ROW_CAPTIONED_HEIGHT = 48;
114
142
  var COLOR_LABEL_LINE = 18;
115
143
  var COLOR_CAPTION_LINE = 16;
@@ -126,59 +154,41 @@ function colorPageHeight(swatches) {
126
154
  const rows = Math.ceil(swatches.length / GRID_COLUMNS);
127
155
  return ROW_HEIGHT + GRID_TOP + rows * SWATCH_SIZE + (rows - 1) * GRID_GAP + GRID_BOTTOM;
128
156
  }
129
- function getProfilePickerContentSize(profileCount, hasManageRow = false, colorSection) {
130
- const rowCount = profileCount + 1 + (hasManageRow ? 1 : 0);
157
+ function pickerContentSize(rowCount, colorSection) {
131
158
  const list = rowCount * ROW_HEIGHT + (colorSection ? colorRowHeight(colorSection.caption) + COLOR_ROW_DIVIDER : 0);
132
159
  const content = colorSection ? Math.max(list, colorPageHeight(colorSection.swatches ?? import_auto_paper2.defaultColors)) : list;
133
160
  return { width: POPOVER_WIDTH, height: LIST_PADDING * 2 + content };
134
161
  }
135
- 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 }) {
136
- const id = `${idPrefix}-profile`;
137
- const colorPreview = useProfileColorPreview();
162
+ function PickerMenu({ id, host, color, dark, label, chip, colorSection, homeColor, contentSize, align: forcedAlign, alignOverride, rotation, triggerStyle, footer, children }) {
138
163
  const menuBg = dark ? "#000000" : "#FFFFFF";
139
164
  const fg = dark ? "#FFFFFF" : "#000000";
140
165
  const open = host.openId === id;
141
- const selectedProfile = profiles.find((p) => p.id === selectedId) ?? null;
142
- const sortedProfiles = (0, import_react2.useMemo)(() => [...profiles].sort((a, b) => a.name.localeCompare(b.name)), [profiles]);
143
- const { width: contentWidth, height: contentHeight } = getProfilePickerContentSize(profiles.length, !!onManage, colorSection);
144
- const auto = (0, import_hud.useAutoAlign)(open, contentWidth, contentHeight, rotation);
166
+ const auto = (0, import_chrome.useAutoAlign)(open, contentSize.width, contentSize.height, rotation);
145
167
  const placement = alignOverride ?? auto;
146
168
  const { maxHeight, measured, triggerRef, verticalAlign } = placement;
147
169
  const align = forcedAlign ?? placement.align;
148
- const handleSelectGuest = () => {
149
- onSelect(null);
150
- host.close();
151
- };
152
- const handleSelectExisting = (profile) => {
153
- if (profile.id === takenId) return;
154
- onSelect(profile);
155
- host.close();
156
- };
157
- const handleManage = () => {
158
- host.close();
159
- onManage?.();
160
- };
161
- const chipTrigger = !!colorSection;
170
+ const chipTrigger = !!chip;
162
171
  const [triggerWidth, setTriggerWidth] = (0, import_react2.useState)(0);
163
172
  const handleAnchorLayout = chipTrigger ? (e) => setTriggerWidth(e.nativeEvent.layout.width) : void 0;
164
173
  const [colorPage, setColorPage] = (0, import_react2.useState)(false);
174
+ const showColorPage = !!colorSection && colorPage;
165
175
  const handleTriggerPress = () => {
166
176
  setColorPage(false);
167
177
  host.toggle(id);
168
178
  };
169
- const triggerLabel = (selectedProfile?.name ?? guestLabel).toUpperCase();
170
- const manageFooter = !!onManage && !(colorSection && colorPage);
171
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native2.View, { style: [styles2.anchor, open && styles2.anchorOpen], onLayout: handleAnchorLayout, children: [
172
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_feedback_press.TouchableRipple, { onPress: handleTriggerPress, borderless: chipTrigger, style: [styles2.trigger, chipTrigger && styles2.chipTrigger, triggerStyle], accessibilityRole: "button", accessibilityLabel: triggerLabel, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native2.View, { ref: triggerRef, collapsable: false, style: chipTrigger ? styles2.chipTriggerInner : styles2.triggerInner, children: [
173
- chipTrigger && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(IdentityChip, { color, tag: selectedProfile?.tag, icon: selectedProfile ? "account" : nullIcon, size: CHIP_SIZE }),
174
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native_paper2.Text, { style: [styles2.triggerLabel, { color: chipTrigger ? fg : color }], numberOfLines: 1, children: triggerLabel }),
179
+ const triggerLabel = label.toUpperCase();
180
+ const showFooter = !!footer && !showColorPage;
181
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native3.View, { style: [styles3.anchor, open && styles3.anchorOpen], onLayout: handleAnchorLayout, children: [
182
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_feedback_press.TouchableRipple, { onPress: handleTriggerPress, borderless: chipTrigger, style: [styles3.trigger, chipTrigger && styles3.chipTrigger, triggerStyle], accessibilityRole: "button", accessibilityLabel: triggerLabel, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native3.View, { ref: triggerRef, collapsable: false, style: chipTrigger ? styles3.chipTriggerInner : styles3.triggerInner, children: [
183
+ chip && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(IdentityChip, { color, tag: chip.tag, icon: chip.icon, size: CHIP_SIZE }),
184
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native_paper2.Text, { style: [styles3.triggerLabel, { color: chipTrigger ? fg : color }], numberOfLines: 1, children: triggerLabel }),
175
185
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native_paper2.Icon, { source: "menu-down", size: 16, color: chipTrigger ? fg : color })
176
186
  ] }) }),
177
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_hud.PopoverBody, { visible: open && measured, align, verticalAlign, caretColor: menuBg, caretBorderColor: color, caretBorderWidth: MENU_BORDER_WIDTH, triggerSize: chipTrigger ? triggerWidth : TRIGGER_HEIGHT, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
178
- import_react_native2.View,
187
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_chrome.PopoverBody, { visible: open && measured, align, verticalAlign, caretColor: menuBg, caretBorderColor: color, caretBorderWidth: MENU_BORDER_WIDTH, triggerSize: chipTrigger ? triggerWidth : TRIGGER_HEIGHT, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
188
+ import_react_native3.View,
179
189
  {
180
190
  style: [
181
- styles2.menu,
191
+ styles3.menu,
182
192
  {
183
193
  backgroundColor: menuBg,
184
194
  borderColor: color,
@@ -187,78 +197,88 @@ function ProfilePicker({ idPrefix, host, profiles, selectedId, takenId, color, d
187
197
  }
188
198
  ],
189
199
  children: [
190
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_auto_paper2.FadingScrollView, { contentContainerStyle: [styles2.menuContent, manageFooter && styles2.menuContentAboveFooter], showsVerticalScrollIndicator: false, children: colorSection && colorPage ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ColorPage, { section: colorSection, profileColor: selectedProfile?.color, fg, onBack: () => setColorPage(false) }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
200
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_auto_paper2.FadingScrollView, { contentContainerStyle: [styles3.menuContent, showFooter && styles3.menuContentAboveFooter], showsVerticalScrollIndicator: false, children: colorSection && colorPage ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ColorPage, { section: colorSection, homeColor, fg, onBack: () => setColorPage(false) }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
191
201
  colorSection && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ColorRow, { section: colorSection, fg, dividerColor: color, onPress: () => setColorPage(true) }),
192
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_feedback_press.TouchableRipple, { onPress: handleSelectGuest, style: [styles2.row, selectedId === null && { backgroundColor: color }], children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native2.View, { style: styles2.rowInner, children: [
193
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native2.View, { style: styles2.guestChip, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native_paper2.Icon, { source: nullIcon, size: 13, color: selectedId === null ? (0, import_auto_paper2.getContrastColor)(color) : fg }) }),
194
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native_paper2.Text, { style: [styles2.rowLabel, { color: selectedId === null ? (0, import_auto_paper2.getContrastColor)(color) : fg }], children: nullLabel })
195
- ] }) }),
196
- sortedProfiles.map((profile) => {
197
- const taken = profile.id === takenId;
198
- const selected = selectedId === profile.id;
199
- const rowColor = colorPreview ? colorPreview(profile.color) : profile.color;
200
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_feedback_press.TouchableRipple, { disabled: taken, onPress: () => handleSelectExisting(profile), style: [styles2.row, selected && { backgroundColor: rowColor }], children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native2.View, { style: [styles2.rowInner, taken && styles2.rowDisabled], children: [
201
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ProfileChip, { profile, filled: !selected }),
202
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
203
- import_react_native_paper2.Text,
204
- {
205
- style: [
206
- styles2.rowLabel,
207
- {
208
- color: selected ? (0, import_auto_paper2.getContrastColor)(rowColor) : fg
209
- }
210
- ],
211
- numberOfLines: 1,
212
- children: profile.name
213
- }
214
- )
215
- ] }) }, profile.id);
216
- })
217
- ] }) }, colorSection && colorPage ? "color" : "list"),
218
- manageFooter && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native2.View, { style: styles2.menuFooter, children: [
219
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native2.View, { style: [styles2.divider, { backgroundColor: color }] }),
220
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_feedback_press.TouchableRipple, { onPress: handleManage, style: styles2.row, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native2.View, { style: styles2.manageRowInner, children: [
221
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native_paper2.Icon, { source: "account-cog-outline", size: 16, color }),
222
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native_paper2.Text, { style: [styles2.manageLabel, { color: fg }], children: "Manage" })
223
- ] }) })
202
+ children
203
+ ] }) }, showColorPage ? "color" : "list"),
204
+ showFooter && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native3.View, { style: styles3.menuFooter, children: [
205
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native3.View, { style: [styles3.divider, { backgroundColor: color }] }),
206
+ footer
224
207
  ] })
225
208
  ]
226
209
  }
227
210
  ) })
228
211
  ] });
229
212
  }
213
+ function IconRow({ icon, label, selected, color, fg, disabled = false, onPress }) {
214
+ const textColor = selected ? (0, import_auto_paper2.getContrastColor)(color) : fg;
215
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_feedback_press.TouchableRipple, { disabled, onPress, style: [pickerMenuStyles.row, selected && { backgroundColor: color }], children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native3.View, { style: [pickerMenuStyles.rowInner, disabled && pickerMenuStyles.rowDisabled], children: [
216
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native3.View, { style: styles3.iconSlot, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native_paper2.Icon, { source: icon, size: 13, color: textColor }) }),
217
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native_paper2.Text, { style: [pickerMenuStyles.rowLabel, { color: textColor }], numberOfLines: 1, children: label })
218
+ ] }) });
219
+ }
230
220
  function ColorRow({ section, fg, dividerColor, onPress }) {
231
221
  const { value, previewValue, label = "Color", caption } = section;
222
+ const renderSwatch = useProfileSwatchRenderer();
232
223
  const drawn = previewValue ? previewValue(value) : value;
233
224
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
234
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_feedback_press.TouchableRipple, { onPress, style: [styles2.row, { height: colorRowHeight(caption) }], accessibilityRole: "button", accessibilityLabel: caption ? `${label}, ${caption}` : label, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native2.View, { style: styles2.rowInner, children: [
235
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native2.View, { style: [styles2.colorRowSwatch, { backgroundColor: drawn }] }),
236
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native2.View, { style: styles2.colorRowText, children: [
237
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native_paper2.Text, { style: [styles2.colorRowLabel, { color: fg }], numberOfLines: 1, children: label }),
238
- caption ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native_paper2.Text, { style: [styles2.colorRowCaption, { color: fg }], numberOfLines: 1, children: caption }) : null
225
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_feedback_press.TouchableRipple, { onPress, style: [pickerMenuStyles.row, { height: colorRowHeight(caption) }], accessibilityRole: "button", accessibilityLabel: caption ? `${label}, ${caption}` : label, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native3.View, { style: pickerMenuStyles.rowInner, children: [
226
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native3.View, { style: [styles3.colorRowSwatch, !renderSwatch && { backgroundColor: drawn }], children: renderSwatch && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(SwatchLayer, { render: renderSwatch, color: drawn, size: COLOR_ROW_SWATCH_SIZE }) }),
227
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native3.View, { style: styles3.colorRowText, children: [
228
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native_paper2.Text, { style: [styles3.colorRowLabel, { color: fg }], numberOfLines: 1, children: label }),
229
+ caption ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native_paper2.Text, { style: [styles3.colorRowCaption, { color: fg }], numberOfLines: 1, children: caption }) : null
239
230
  ] }),
240
231
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native_paper2.Icon, { source: "chevron-right", size: 18, color: fg })
241
232
  ] }) }),
242
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native2.View, { style: [styles2.divider, { backgroundColor: dividerColor }] })
233
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native3.View, { style: [styles3.divider, { backgroundColor: dividerColor }] })
243
234
  ] });
244
235
  }
245
- function ColorPage({ section, profileColor, fg, onBack }) {
246
- const { value, onChange, previewValue, swatches = import_auto_paper2.defaultColors, label = "Color" } = section;
236
+ function ColorPage({ section, homeColor, fg, onBack }) {
237
+ const { value, onChange, previewValue, swatches = import_auto_paper2.defaultColors, label = "Color", takenValue, allowSwapTaken } = section;
238
+ const renderSwatch = useProfileSwatchRenderer();
247
239
  const displayFor = previewValue ?? ((hex) => hex);
248
240
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
249
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_feedback_press.TouchableRipple, { onPress: onBack, style: styles2.row, accessibilityRole: "button", accessibilityLabel: "Back", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native2.View, { style: styles2.rowInner, children: [
250
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native2.View, { style: styles2.guestChip, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native_paper2.Icon, { source: "chevron-left", size: 20, color: fg }) }),
251
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native_paper2.Text, { style: [styles2.colorRowLabel, { color: fg }], numberOfLines: 1, children: label })
241
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_feedback_press.TouchableRipple, { onPress: onBack, style: pickerMenuStyles.row, accessibilityRole: "button", accessibilityLabel: "Back", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native3.View, { style: pickerMenuStyles.rowInner, children: [
242
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native3.View, { style: styles3.iconSlot, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native_paper2.Icon, { source: "chevron-left", size: 20, color: fg }) }),
243
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native_paper2.Text, { style: [styles3.colorRowLabel, { color: fg }], numberOfLines: 1, children: label })
252
244
  ] }) }),
253
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native2.View, { style: styles2.swatches, children: swatches.map((swatch) => {
245
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native3.View, { style: styles3.swatches, children: swatches.map((swatch) => {
254
246
  const selected = swatch.value.toLowerCase() === value.toLowerCase();
255
- const isProfileColor = !!profileColor && swatch.value.toLowerCase() === profileColor.toLowerCase();
247
+ const isHomeColor = !!homeColor && swatch.value.toLowerCase() === homeColor.toLowerCase();
248
+ const taken = !selected && !!takenValue && swatch.value.toLowerCase() === takenValue.toLowerCase();
249
+ const swappable = taken && !!allowSwapTaken;
256
250
  const swatchDisplay = displayFor(swatch.value);
257
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_feedback_press.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__ */ (0, import_jsx_runtime4.jsx)(import_react_native_paper2.Icon, { source: "check", size: 16, color: (0, import_auto_paper2.getContrastColor)(swatchDisplay) }) : isProfileColor ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native2.View, { style: [styles2.profileDot, { backgroundColor: (0, import_auto_paper2.getContrastColor)(swatchDisplay) }] }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native2.View, {}) }, swatch.value);
251
+ const contrast = (0, import_auto_paper2.getContrastColor)(swatchDisplay);
252
+ const accessibilityLabel = swappable ? `${swatch.label}, swap` : taken ? `${swatch.label}, taken` : isHomeColor ? `${swatch.label}, profile color` : swatch.label;
253
+ const mark = selected ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native_paper2.Icon, { source: "check", size: 16, color: contrast }) : swappable ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native_paper2.Icon, { source: "swap-horizontal", size: 16, color: contrast }) : taken ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native_paper2.Icon, { source: "close", size: 16, color: contrast }) : isHomeColor ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native3.View, { style: [styles3.homeDot, { backgroundColor: contrast }] }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native3.View, {});
254
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_feedback_press.TouchableRipple, { disabled: taken && !swappable, onPress: () => onChange(swatch.value), borderless: true, style: [styles3.swatch, !renderSwatch && { backgroundColor: swatchDisplay }, selected && styles3.swatchSelected, taken && !swappable && styles3.swatchTaken], accessibilityRole: "button", accessibilityLabel, accessibilityState: { selected, disabled: taken && !swappable }, children: renderSwatch ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native3.View, { style: styles3.swatchFace, children: [
255
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(SwatchLayer, { render: renderSwatch, color: swatchDisplay, size: selected ? SWATCH_SIZE - 2 * SWATCH_RING_WIDTH : SWATCH_SIZE }),
256
+ mark
257
+ ] }) : mark }, swatch.value);
258
258
  }) })
259
259
  ] });
260
260
  }
261
- var styles2 = import_react_native2.StyleSheet.create({
261
+ var pickerMenuStyles = import_react_native3.StyleSheet.create({
262
+ row: {
263
+ borderRadius: 8,
264
+ height: ROW_HEIGHT
265
+ },
266
+ rowDisabled: {
267
+ opacity: 0.35
268
+ },
269
+ rowInner: {
270
+ alignItems: "center",
271
+ flexDirection: "row",
272
+ flex: 1,
273
+ gap: 8,
274
+ paddingHorizontal: ROW_INSET
275
+ },
276
+ rowLabel: {
277
+ flex: 1,
278
+ fontSize: 14
279
+ }
280
+ });
281
+ var styles3 = import_react_native3.StyleSheet.create({
262
282
  anchor: {
263
283
  position: "relative"
264
284
  },
@@ -294,45 +314,38 @@ var styles2 = import_react_native2.StyleSheet.create({
294
314
  },
295
315
  // The color in effect, in the same 22pt slot as every row's chip.
296
316
  colorRowSwatch: {
297
- borderRadius: 11,
298
- height: 22,
299
- width: 22
317
+ borderRadius: COLOR_ROW_SWATCH_SIZE / 2,
318
+ height: COLOR_ROW_SWATCH_SIZE,
319
+ width: COLOR_ROW_SWATCH_SIZE
300
320
  },
301
321
  colorRowText: {
302
322
  flex: 1
303
323
  },
304
- // Separates Manage (a navigation action) from the selectable rows above it - different kind of
305
- // action, not just another row in the same list. Tinted with the seat's own color (passed in as
306
- // backgroundColor) rather than a flat gray, matching the border/caret's own accent-tinted look.
324
+ // Separates the footer (a navigation action) and the Color row from the selectable rows - a
325
+ // different kind of action, not just another row in the same list. Tinted with the seat's own color
326
+ // (passed in as backgroundColor) rather than a flat gray, matching the border/caret's own
327
+ // accent-tinted look.
307
328
  divider: {
308
329
  height: 1,
309
330
  marginVertical: 4,
310
331
  opacity: 0.25
311
332
  },
312
- // Matches ProfileChip's own default size/shape exactly (see this row's own render-site comment)
313
- // - no backgroundColor, unlike ProfileChip's own default: this row has no identity color of its
314
- // own to fill it with, and the render site now colors the icon directly instead.
315
- guestChip: {
333
+ // The selected profile's own color (see ColorPage).
334
+ homeDot: {
335
+ borderRadius: 4,
336
+ height: 8,
337
+ width: 8
338
+ },
339
+ // Matches ProfileChip's own default size/shape exactly (see IconRow's own render-site comment) -
340
+ // no backgroundColor, unlike ProfileChip's own default: an icon row has no identity color of its
341
+ // own to fill it with, and the render site colors the icon directly instead.
342
+ iconSlot: {
316
343
  alignItems: "center",
317
344
  borderRadius: 11,
318
345
  height: 22,
319
346
  justifyContent: "center",
320
347
  width: 22
321
348
  },
322
- manageLabel: {
323
- fontSize: 13,
324
- fontWeight: "700",
325
- letterSpacing: 0.5
326
- },
327
- // Centered rather than left-aligned like a normal row - see this row's own render-site comment
328
- // for why (a footer action, not another selectable identity).
329
- manageRowInner: {
330
- alignItems: "center",
331
- flexDirection: "row",
332
- gap: 6,
333
- height: "100%",
334
- justifyContent: "center"
335
- },
336
349
  menu: {
337
350
  borderRadius: 12,
338
351
  borderWidth: MENU_BORDER_WIDTH,
@@ -348,7 +361,7 @@ var styles2 = import_react_native2.StyleSheet.create({
348
361
  menuContent: {
349
362
  padding: LIST_PADDING
350
363
  },
351
- // With Manage pinned below, its own padding takes the bottom edge (same total height as before).
364
+ // With a footer pinned below, its own padding takes the bottom edge (same total height as before).
352
365
  menuContentAboveFooter: {
353
366
  paddingBottom: 0
354
367
  },
@@ -356,30 +369,6 @@ var styles2 = import_react_native2.StyleSheet.create({
356
369
  paddingBottom: LIST_PADDING,
357
370
  paddingHorizontal: LIST_PADDING
358
371
  },
359
- // The selected profile's own color (see ColorSection).
360
- profileDot: {
361
- borderRadius: 4,
362
- height: 8,
363
- width: 8
364
- },
365
- row: {
366
- borderRadius: 8,
367
- height: ROW_HEIGHT
368
- },
369
- rowDisabled: {
370
- opacity: 0.35
371
- },
372
- rowInner: {
373
- alignItems: "center",
374
- flexDirection: "row",
375
- flex: 1,
376
- gap: 8,
377
- paddingHorizontal: ROW_INSET
378
- },
379
- rowLabel: {
380
- flex: 1,
381
- fontSize: 14
382
- },
383
372
  swatch: {
384
373
  alignItems: "center",
385
374
  borderRadius: SWATCH_SIZE / 2,
@@ -387,9 +376,22 @@ var styles2 = import_react_native2.StyleSheet.create({
387
376
  justifyContent: "center",
388
377
  width: SWATCH_SIZE
389
378
  },
379
+ // A drawn swatch (see ColorPage) and its mark: the whole circle inside the selected one's ring.
380
+ swatchFace: {
381
+ alignItems: "center",
382
+ bottom: 0,
383
+ justifyContent: "center",
384
+ left: 0,
385
+ position: "absolute",
386
+ right: 0,
387
+ top: 0
388
+ },
390
389
  swatchSelected: {
391
390
  borderColor: "#ffffff",
392
- borderWidth: 2
391
+ borderWidth: SWATCH_RING_WIDTH
392
+ },
393
+ swatchTaken: {
394
+ opacity: 0.35
393
395
  },
394
396
  swatches: {
395
397
  columnGap: GRID_GAP,
@@ -417,15 +419,114 @@ var styles2 = import_react_native2.StyleSheet.create({
417
419
  }
418
420
  });
419
421
 
420
- // src/ProfilesManager.tsx
422
+ // src/OptionPicker.tsx
423
+ var import_jsx_runtime5 = require("react/jsx-runtime");
424
+ function getOptionPickerContentSize(optionCount, colorSection) {
425
+ return pickerContentSize(optionCount, colorSection);
426
+ }
427
+ function OptionPicker({ id, host, options, value, onChange, color, dark, icon, rowIcon = icon, colorSection, align, alignOverride, rotation = 0, triggerStyle }) {
428
+ const fg = dark ? "#FFFFFF" : "#000000";
429
+ const selected = options.find((option) => option.value === value);
430
+ const handleSelect = (option) => {
431
+ onChange(option.value);
432
+ host.close();
433
+ };
434
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(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__ */ (0, import_jsx_runtime5.jsx)(IconRow, { icon: option.icon ?? rowIcon, label: option.label, selected: option.value === value, color, fg, onPress: () => handleSelect(option) }, option.value)) });
435
+ }
436
+
437
+ // src/ProfileChip.tsx
438
+ var import_jsx_runtime6 = require("react/jsx-runtime");
439
+ function ProfileChip({ profile, size = 22, filled = true }) {
440
+ const color = useProfileDisplayColor(profile.color);
441
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(IdentityChip, { color, tag: profile.tag, icon: "account", size, filled });
442
+ }
443
+
444
+ // src/ProfilePicker.tsx
421
445
  var import_auto_paper3 = require("@rific/auto-paper");
422
446
  var import_feedback_press2 = require("@rific/feedback-press");
423
- var import_focus_chain = require("@rific/focus-chain");
424
- var import_toaster = require("@rific/toaster");
425
- var import_hud2 = require("@tastic/hud");
426
447
  var import_react3 = require("react");
427
- var import_react_native3 = require("react-native");
448
+ var import_react_native4 = require("react-native");
428
449
  var import_react_native_paper3 = require("react-native-paper");
450
+ var import_jsx_runtime7 = require("react/jsx-runtime");
451
+ function getProfilePickerContentSize(profileCount, hasManageRow = false, colorSection) {
452
+ return pickerContentSize(profileCount + 1 + (hasManageRow ? 1 : 0), colorSection);
453
+ }
454
+ function ProfilePicker({ idPrefix, host, profiles, selectedId, takenId, takenIds, color, dark, align, alignOverride, rotation = 0, guestLabel, nullLabel = "Player", nullIcon = "account-off-outline", onSelect, onManage, colorSection, triggerStyle }) {
455
+ const id = `${idPrefix}-profile`;
456
+ const colorPreview = useProfileColorPreview();
457
+ const fg = dark ? "#FFFFFF" : "#000000";
458
+ const selectedProfile = profiles.find((p) => p.id === selectedId) ?? null;
459
+ const sortedProfiles = (0, import_react3.useMemo)(() => [...profiles].sort((a, b) => a.name.localeCompare(b.name)), [profiles]);
460
+ const contentSize = getProfilePickerContentSize(profiles.length, !!onManage, colorSection);
461
+ const isTaken = (profileId) => profileId === takenId || profileId !== selectedId && !!takenIds?.includes(profileId);
462
+ const handleSelectGuest = () => {
463
+ onSelect(null);
464
+ host.close();
465
+ };
466
+ const handleSelectExisting = (profile) => {
467
+ if (isTaken(profile.id)) return;
468
+ onSelect(profile);
469
+ host.close();
470
+ };
471
+ const handleManage = () => {
472
+ host.close();
473
+ onManage?.();
474
+ };
475
+ const manageFooter = onManage ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_feedback_press2.TouchableRipple, { onPress: handleManage, style: pickerMenuStyles.row, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_native4.View, { style: styles4.manageRowInner, children: [
476
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native_paper3.Icon, { source: "account-cog-outline", size: 16, color }),
477
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native_paper3.Text, { style: [styles4.manageLabel, { color: fg }], children: "Manage" })
478
+ ] }) }) : void 0;
479
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(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: [
480
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(IconRow, { icon: nullIcon, label: nullLabel, selected: selectedId === null, color, fg, onPress: handleSelectGuest }),
481
+ sortedProfiles.map((profile) => {
482
+ const taken = isTaken(profile.id);
483
+ const selected = selectedId === profile.id;
484
+ const rowColor = colorPreview ? colorPreview(profile.color) : profile.color;
485
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_feedback_press2.TouchableRipple, { disabled: taken, onPress: () => handleSelectExisting(profile), style: [pickerMenuStyles.row, selected && { backgroundColor: rowColor }], children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_native4.View, { style: [pickerMenuStyles.rowInner, taken && pickerMenuStyles.rowDisabled], children: [
486
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ProfileChip, { profile, filled: !selected }),
487
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
488
+ import_react_native_paper3.Text,
489
+ {
490
+ style: [
491
+ pickerMenuStyles.rowLabel,
492
+ {
493
+ color: selected ? (0, import_auto_paper3.getContrastColor)(rowColor) : fg
494
+ }
495
+ ],
496
+ numberOfLines: 1,
497
+ children: profile.name
498
+ }
499
+ )
500
+ ] }) }, profile.id);
501
+ })
502
+ ] });
503
+ }
504
+ var styles4 = import_react_native4.StyleSheet.create({
505
+ manageLabel: {
506
+ fontSize: 13,
507
+ fontWeight: "700",
508
+ letterSpacing: 0.5
509
+ },
510
+ // Centered rather than left-aligned like a normal row - see the footer's own comment for why (a
511
+ // footer action, not another selectable identity).
512
+ manageRowInner: {
513
+ alignItems: "center",
514
+ flexDirection: "row",
515
+ gap: 6,
516
+ height: "100%",
517
+ justifyContent: "center"
518
+ }
519
+ });
520
+
521
+ // src/ProfilesManager.tsx
522
+ var import_auto_paper4 = require("@rific/auto-paper");
523
+ var import_feedback_press3 = require("@rific/feedback-press");
524
+ var import_focus_chain = require("@rific/focus-chain");
525
+ var import_toaster = require("@rific/toaster");
526
+ var import_chrome2 = require("@tastic/hud/chrome");
527
+ var import_react4 = require("react");
528
+ var import_react_native5 = require("react-native");
529
+ var import_react_native_paper4 = require("react-native-paper");
429
530
  var import_react_native_safe_area_context = require("react-native-safe-area-context");
430
531
 
431
532
  // src/profilesValidation.ts
@@ -461,7 +562,7 @@ function isValidProfile(value) {
461
562
  }
462
563
 
463
564
  // src/ProfilesManager.tsx
464
- var import_jsx_runtime5 = require("react/jsx-runtime");
565
+ var import_jsx_runtime8 = require("react/jsx-runtime");
465
566
  var CHIP_SIZE2 = 40;
466
567
  function deriveTag(name) {
467
568
  const trimmed = name.trim();
@@ -470,19 +571,19 @@ function deriveTag(name) {
470
571
  function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, headerLeft, ref, fg: fgOverride, fgMuted: fgMutedOverride, titleVariant = "headlineSmall", colorPreview: colorPreviewProp, useColorPickerAlign }) {
471
572
  const contextColorPreview = useProfileColorPreview();
472
573
  const colorPreview = colorPreviewProp ?? contextColorPreview;
473
- const { dark } = (0, import_auto_paper3.useAutoPaperTheme)();
574
+ const { dark } = (0, import_auto_paper4.useAutoPaperTheme)();
474
575
  const insets = (0, import_react_native_safe_area_context.useSafeAreaInsets)();
475
576
  const { error: showErrorToast } = (0, import_toaster.useToast)();
476
- const host = (0, import_hud2.usePopoverHost)();
577
+ const host = (0, import_chrome2.usePopoverHost)();
477
578
  const fg = fgOverride ?? (dark ? "#FFFFFF" : "#000000");
478
579
  const fgMuted = fgMutedOverride ?? (dark ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.5)");
479
- const [editingId, setEditingId] = (0, import_react3.useState)(null);
480
- const [draftName, setDraftName] = (0, import_react3.useState)("");
481
- const [draftColor, setDraftColor] = (0, import_react3.useState)(defaultColor);
482
- const [draftTag, setDraftTag] = (0, import_react3.useState)("");
483
- const [tagManuallySet, setTagManuallySet] = (0, import_react3.useState)(false);
484
- const [confirmDeleteId, setConfirmDeleteId] = (0, import_react3.useState)(null);
485
- const commitEdit = (0, import_react3.useCallback)(() => {
580
+ const [editingId, setEditingId] = (0, import_react4.useState)(null);
581
+ const [draftName, setDraftName] = (0, import_react4.useState)("");
582
+ const [draftColor, setDraftColor] = (0, import_react4.useState)(defaultColor);
583
+ const [draftTag, setDraftTag] = (0, import_react4.useState)("");
584
+ const [tagManuallySet, setTagManuallySet] = (0, import_react4.useState)(false);
585
+ const [confirmDeleteId, setConfirmDeleteId] = (0, import_react4.useState)(null);
586
+ const commitEdit = (0, import_react4.useCallback)(() => {
486
587
  const name = draftName.trim();
487
588
  if (editingId && name) {
488
589
  const tagValid = isValidTag(draftTag);
@@ -493,13 +594,13 @@ function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, h
493
594
  }
494
595
  setEditingId(null);
495
596
  }, [editingId, draftName, draftColor, draftTag, onCreate, onSave, showErrorToast]);
496
- const commitEditRef = (0, import_react3.useRef)(commitEdit);
497
- (0, import_react3.useEffect)(() => {
597
+ const commitEditRef = (0, import_react4.useRef)(commitEdit);
598
+ (0, import_react4.useEffect)(() => {
498
599
  commitEditRef.current = commitEdit;
499
600
  });
500
- (0, import_react3.useEffect)(() => () => commitEditRef.current(), []);
501
- (0, import_react3.useImperativeHandle)(ref, () => ({ commitPendingEdit: () => commitEditRef.current() }), []);
502
- const startEdit = (0, import_react3.useCallback)(
601
+ (0, import_react4.useEffect)(() => () => commitEditRef.current(), []);
602
+ (0, import_react4.useImperativeHandle)(ref, () => ({ commitPendingEdit: () => commitEditRef.current() }), []);
603
+ const startEdit = (0, import_react4.useCallback)(
503
604
  (target) => {
504
605
  commitEdit();
505
606
  if (target === "new") {
@@ -517,79 +618,79 @@ function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, h
517
618
  },
518
619
  [commitEdit, defaultColor]
519
620
  );
520
- const handleNameChange = (0, import_react3.useCallback)(
621
+ const handleNameChange = (0, import_react4.useCallback)(
521
622
  (name) => {
522
623
  setDraftName(name);
523
624
  if (!tagManuallySet) setDraftTag(deriveTag(name));
524
625
  },
525
626
  [tagManuallySet]
526
627
  );
527
- const handleTagChange = (0, import_react3.useCallback)((tag) => {
628
+ const handleTagChange = (0, import_react4.useCallback)((tag) => {
528
629
  setDraftTag((previous) => clampTag(previous, tag));
529
630
  setTagManuallySet(true);
530
631
  }, []);
531
632
  const confirmDeleteProfile = profiles.find((p) => p.id === confirmDeleteId) ?? null;
532
- const handleConfirmDelete = (0, import_react3.useCallback)(() => {
633
+ const handleConfirmDelete = (0, import_react4.useCallback)(() => {
533
634
  if (confirmDeleteId) {
534
635
  onDelete(confirmDeleteId);
535
636
  if (editingId === confirmDeleteId) setEditingId(null);
536
637
  }
537
638
  setConfirmDeleteId(null);
538
639
  }, [confirmDeleteId, editingId, onDelete]);
539
- const sortedProfiles = (0, import_react3.useMemo)(() => [...profiles].sort((a, b) => a.name.localeCompare(b.name)), [profiles]);
640
+ const sortedProfiles = (0, import_react4.useMemo)(() => [...profiles].sort((a, b) => a.name.localeCompare(b.name)), [profiles]);
540
641
  const rows = [...sortedProfiles.map((profile) => ({ kind: "profile", profile })), { kind: "new" }];
541
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
542
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_react_native3.View, { style: [styles3.header, { paddingTop: 8 + insets.top, paddingLeft: 8 + insets.left }], children: [
642
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
643
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_react_native5.View, { style: [styles5.header, { paddingTop: 8 + insets.top, paddingLeft: 8 + insets.left }], children: [
543
644
  headerLeft,
544
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native_paper3.Text, { variant: titleVariant, style: [styles3.title, { color: fg }], children: "Profiles" })
645
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native_paper4.Text, { variant: titleVariant, style: [styles5.title, { color: fg }], children: "Profiles" })
545
646
  ] }),
546
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native3.KeyboardAvoidingView, { behavior: import_react_native3.Platform.OS === "ios" ? "padding" : void 0, style: styles3.keyboardAvoiding, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native3.ScrollView, { style: styles3.scrollView, contentContainerStyle: [styles3.content, { paddingBottom: 32 + insets.bottom }], showsVerticalScrollIndicator: false, keyboardShouldPersistTaps: "handled", children: rows.map((row) => {
647
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native5.KeyboardAvoidingView, { behavior: import_react_native5.Platform.OS === "ios" ? "padding" : void 0, style: styles5.keyboardAvoiding, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_auto_paper4.FadingScrollView, { style: styles5.scrollView, contentContainerStyle: [styles5.content, { paddingBottom: 32 + insets.bottom }], showsVerticalScrollIndicator: false, keyboardShouldPersistTaps: "handled", children: rows.map((row) => {
547
648
  if (row.kind === "new") {
548
649
  if (editingId !== "new") {
549
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_feedback_press2.TouchableRipple, { onPress: () => startEdit("new"), style: styles3.row, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_react_native3.View, { style: styles3.rowInner, children: [
550
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native3.View, { style: [styles3.rowChip, styles3.newChip, { borderColor: fg }], children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native_paper3.Icon, { source: "plus", size: 20, color: fg }) }),
551
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native_paper3.Text, { variant: "bodyLarge", style: { color: fg }, children: "New Profile" })
650
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_feedback_press3.TouchableRipple, { onPress: () => startEdit("new"), style: styles5.row, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_react_native5.View, { style: styles5.rowInner, children: [
651
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native5.View, { style: [styles5.rowChip, styles5.newChip, { borderColor: fg }], children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native_paper4.Icon, { source: "plus", size: 20, color: fg }) }),
652
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native_paper4.Text, { variant: "bodyLarge", style: { color: fg }, children: "New Profile" })
552
653
  ] }) }, "new");
553
654
  }
554
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(EditRow, { draftName, onNameChange: handleNameChange, draftColor, onColorChange: setDraftColor, draftTag, onTagChange: handleTagChange, onSubmit: commitEdit, autoFocus: true, fgMuted, host, dark, colorPreview, useColorPickerAlign }, "new");
655
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(EditRow, { draftName, onNameChange: handleNameChange, draftColor, onColorChange: setDraftColor, draftTag, onTagChange: handleTagChange, onSubmit: commitEdit, autoFocus: true, fgMuted, host, dark, colorPreview, useColorPickerAlign }, "new");
555
656
  }
556
657
  const { profile } = row;
557
658
  if (editingId === profile.id) {
558
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(EditRow, { draftName, onNameChange: handleNameChange, draftColor, onColorChange: setDraftColor, draftTag, onTagChange: handleTagChange, onSubmit: commitEdit, onDelete: () => setConfirmDeleteId(profile.id), fgMuted, host, dark, colorPreview, useColorPickerAlign }, profile.id);
659
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(EditRow, { draftName, onNameChange: handleNameChange, draftColor, onColorChange: setDraftColor, draftTag, onTagChange: handleTagChange, onSubmit: commitEdit, onDelete: () => setConfirmDeleteId(profile.id), fgMuted, host, dark, colorPreview, useColorPickerAlign }, profile.id);
559
660
  }
560
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_feedback_press2.TouchableRipple, { onPress: () => startEdit(profile), style: styles3.row, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_react_native3.View, { style: styles3.rowInner, children: [
561
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(ProfileChip, { profile, size: CHIP_SIZE2 }),
562
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native_paper3.Text, { variant: "bodyLarge", style: [styles3.rowName, { color: fg }], numberOfLines: 1, children: profile.name })
661
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_feedback_press3.TouchableRipple, { onPress: () => startEdit(profile), style: styles5.row, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_react_native5.View, { style: styles5.rowInner, children: [
662
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(ProfileChip, { profile, size: CHIP_SIZE2 }),
663
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native_paper4.Text, { variant: "bodyLarge", style: [styles5.rowName, { color: fg }], numberOfLines: 1, children: profile.name })
563
664
  ] }) }, profile.id);
564
665
  }) }) }),
565
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_hud2.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) })
666
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_chrome2.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) })
566
667
  ] });
567
668
  }
568
669
  function AlignedColorPicker({ useAlign, ...props }) {
569
- const { width: windowWidth } = (0, import_react_native3.useWindowDimensions)();
570
- const { width, height } = (0, import_hud2.getInlineColorPickerContentSize)(import_auto_paper3.defaultColors.length, windowWidth);
670
+ const { width: windowWidth } = (0, import_react_native5.useWindowDimensions)();
671
+ const { width, height } = (0, import_chrome2.getInlineColorPickerContentSize)(import_auto_paper4.defaultColors.length, windowWidth);
571
672
  const alignOverride = useAlign(props.host.openId === props.id, width, height);
572
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_hud2.InlineColorPicker, { ...props, alignOverride });
673
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_chrome2.InlineColorPicker, { ...props, alignOverride });
573
674
  }
574
675
  function EditRow({ draftName, onNameChange, draftColor, onColorChange, draftTag, onTagChange, onSubmit, onDelete, autoFocus, fgMuted, host, dark, colorPreview, useColorPickerAlign }) {
575
676
  const register = (0, import_focus_chain.useFocusChain)();
576
677
  const tag = register();
577
678
  const name = register();
578
- const tagFocusRef = (0, import_react3.useRef)(tag.props.focus);
579
- (0, import_react3.useEffect)(() => {
679
+ const tagFocusRef = (0, import_react4.useRef)(tag.props.focus);
680
+ (0, import_react4.useEffect)(() => {
580
681
  tagFocusRef.current = tag.props.focus;
581
682
  });
582
- (0, import_react3.useEffect)(() => {
683
+ (0, import_react4.useEffect)(() => {
583
684
  if (host.openId === "color") tagFocusRef.current();
584
685
  }, [host.openId]);
585
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_react_native3.View, { style: styles3.editRow, children: [
586
- useColorPickerAlign ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(AlignedColorPicker, { useAlign: useColorPickerAlign, id: "color", host, value: draftColor, onChange: onColorChange, previewValue: colorPreview, tag: draftTag, dark }) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_hud2.InlineColorPicker, { id: "color", host, value: draftColor, onChange: onColorChange, previewValue: colorPreview, tag: draftTag, dark }),
587
- /* @__PURE__ */ (0, import_jsx_runtime5.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" }),
588
- /* @__PURE__ */ (0, import_jsx_runtime5.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 }),
589
- onDelete && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_feedback_press2.TouchableRipple, { onPress: onDelete, style: styles3.iconButton, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native_paper3.Icon, { source: "trash-can-outline", size: 18, color: fgMuted }) })
686
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_react_native5.View, { style: styles5.editRow, children: [
687
+ useColorPickerAlign ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(AlignedColorPicker, { useAlign: useColorPickerAlign, id: "color", host, value: draftColor, onChange: onColorChange, previewValue: colorPreview, tag: draftTag, dark }) : /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_chrome2.InlineColorPicker, { id: "color", host, value: draftColor, onChange: onColorChange, previewValue: colorPreview, tag: draftTag, dark }),
688
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native_paper4.TextInput, { ref: tag.ref, ...tag.props, value: draftTag, onChangeText: onTagChange, accessibilityLabel: "Tag", autoCapitalize: "characters", selectTextOnFocus: true, returnKeyType: "next", style: styles5.hiddenTagInput, pointerEvents: "none" }),
689
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native_paper4.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: styles5.nameInput }),
690
+ onDelete && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_feedback_press3.TouchableRipple, { onPress: onDelete, style: styles5.iconButton, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native_paper4.Icon, { source: "trash-can-outline", size: 18, color: fgMuted }) })
590
691
  ] });
591
692
  }
592
- var styles3 = import_react_native3.StyleSheet.create({
693
+ var styles5 = import_react_native5.StyleSheet.create({
593
694
  content: {
594
695
  gap: 4,
595
696
  paddingHorizontal: 20
@@ -668,23 +769,23 @@ var styles3 = import_react_native3.StyleSheet.create({
668
769
  });
669
770
 
670
771
  // src/ProfilesScreen.tsx
671
- var import_auto_paper4 = require("@rific/auto-paper");
672
- var import_feedback_press3 = require("@rific/feedback-press");
673
- var import_react4 = require("react");
674
- var import_react_native4 = require("react-native");
675
- var import_jsx_runtime6 = require("react/jsx-runtime");
772
+ var import_auto_paper5 = require("@rific/auto-paper");
773
+ var import_feedback_press4 = require("@rific/feedback-press");
774
+ var import_react5 = require("react");
775
+ var import_react_native6 = require("react-native");
776
+ var import_jsx_runtime9 = require("react/jsx-runtime");
676
777
  function ProfilesScreen({ profiles, defaultColor, onCreate, onSave, onDelete, onBack, fg: fgOverride, fgMuted, bg: bgOverride, titleVariant, colorPreview, useColorPickerAlign }) {
677
- const { dark } = (0, import_auto_paper4.useAutoPaperTheme)();
678
- const managerRef = (0, import_react4.useRef)(null);
778
+ const { dark } = (0, import_auto_paper5.useAutoPaperTheme)();
779
+ const managerRef = (0, import_react5.useRef)(null);
679
780
  const fg = fgOverride ?? (dark ? "#FFFFFF" : "#000000");
680
781
  const bg = bgOverride ?? (dark ? "#000000" : "#FFFFFF");
681
782
  const handleBack = () => {
682
783
  managerRef.current?.commitPendingEdit();
683
784
  onBack();
684
785
  };
685
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native4.View, { style: [styles4.container, { backgroundColor: bg }], children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ProfilesManager, { ref: managerRef, profiles, defaultColor, onCreate, onSave, onDelete, fg, fgMuted, titleVariant, colorPreview, useColorPickerAlign, headerLeft: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_feedback_press3.IconButton, { icon: "arrow-left", iconColor: fg, size: 24, onPress: handleBack, accessibilityLabel: "Back" }) }) });
786
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react_native6.View, { style: [styles6.container, { backgroundColor: bg }], children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(ProfilesManager, { ref: managerRef, profiles, defaultColor, onCreate, onSave, onDelete, fg, fgMuted, titleVariant, colorPreview, useColorPickerAlign, headerLeft: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_feedback_press4.IconButton, { icon: "arrow-left", iconColor: fg, size: 24, onPress: handleBack, accessibilityLabel: "Back" }) }) });
686
787
  }
687
- var styles4 = import_react_native4.StyleSheet.create({
788
+ var styles6 = import_react_native6.StyleSheet.create({
688
789
  container: {
689
790
  flex: 1
690
791
  }
@@ -795,6 +896,7 @@ function createProfileRecord(input) {
795
896
  }
796
897
 
797
898
  // src/sharedProfileStore.ts
899
+ var import_react_native7 = require("react-native");
798
900
  var native = null;
799
901
  try {
800
902
  const { requireOptionalNativeModule } = require("expo-modules-core");
@@ -803,15 +905,42 @@ try {
803
905
  native = null;
804
906
  }
805
907
  var PROFILES_KEY = "sharedProfiles";
806
- var isSharedProfileStoreAvailable = native !== null;
807
- async function readSharedProfiles(groupId) {
808
- if (!native) return [];
809
- let json = null;
908
+ var webKey = (groupId, key) => `${groupId}:${key}`;
909
+ var createWebStore = () => {
910
+ if (import_react_native7.Platform.OS !== "web") return null;
810
911
  try {
811
- json = await native.getSharedJSON(groupId, PROFILES_KEY);
912
+ const storage = globalThis.localStorage;
913
+ if (!storage) return null;
914
+ const probe = webKey("@tastic/profile", "probe");
915
+ storage.setItem(probe, "1");
916
+ storage.removeItem(probe);
917
+ let lastWriteFailed = false;
918
+ return {
919
+ storage,
920
+ store: {
921
+ getSharedJSON: async (groupId, key) => {
922
+ if (lastWriteFailed) throw new Error("The last write to the shared roster failed");
923
+ return storage.getItem(webKey(groupId, key));
924
+ },
925
+ setSharedJSON: async (groupId, key, value) => {
926
+ try {
927
+ storage.setItem(webKey(groupId, key), value);
928
+ lastWriteFailed = false;
929
+ } catch (error) {
930
+ lastWriteFailed = true;
931
+ throw error;
932
+ }
933
+ }
934
+ }
935
+ };
812
936
  } catch {
813
937
  return null;
814
938
  }
939
+ };
940
+ var web = native ? null : createWebStore();
941
+ if (web) native = web.store;
942
+ var isSharedProfileStoreAvailable = native !== null;
943
+ var parseRoster = (json) => {
815
944
  if (!json) return [];
816
945
  try {
817
946
  const parsed = JSON.parse(json);
@@ -821,6 +950,16 @@ async function readSharedProfiles(groupId) {
821
950
  } catch {
822
951
  return null;
823
952
  }
953
+ };
954
+ async function readSharedProfiles(groupId) {
955
+ if (!native) return [];
956
+ let json = null;
957
+ try {
958
+ json = await native.getSharedJSON(groupId, PROFILES_KEY);
959
+ } catch {
960
+ return null;
961
+ }
962
+ return parseRoster(json);
824
963
  }
825
964
  async function loadSharedProfiles(groupId) {
826
965
  return await readSharedProfiles(groupId) ?? [];
@@ -832,6 +971,19 @@ async function saveSharedProfiles(groupId, profiles) {
832
971
  } catch {
833
972
  }
834
973
  }
974
+ function subscribeSharedProfiles(groupId, listener) {
975
+ if (!web || typeof window === "undefined" || typeof window.addEventListener !== "function") return () => {
976
+ };
977
+ const storageKey = webKey(groupId, PROFILES_KEY);
978
+ const storage = web.storage;
979
+ const onStorage = (event) => {
980
+ if (event.key !== storageKey || event.storageArea !== storage || !event.newValue) return;
981
+ const profiles = parseRoster(event.newValue);
982
+ if (profiles !== null) listener(profiles);
983
+ };
984
+ window.addEventListener("storage", onStorage);
985
+ return () => window.removeEventListener("storage", onStorage);
986
+ }
835
987
 
836
988
  // src/redux/resolveInitialProfiles.ts
837
989
  async function resolveInitialProfiles(groupId, localFallback) {
@@ -848,47 +1000,82 @@ async function resolveInitialProfiles(groupId, localFallback) {
848
1000
 
849
1001
  // src/redux/seatColorsSlice.ts
850
1002
  var REHYDRATE2 = "persist/REHYDRATE";
1003
+ function sameColor(a, b) {
1004
+ return a.toLowerCase() === b.toLowerCase();
1005
+ }
1006
+ function profileSeatColor(profile, profileColors) {
1007
+ const pick = profileColors[profile.id];
1008
+ return pick && sameColor(pick.profileColor, profile.color) ? pick.color : profile.color;
1009
+ }
851
1010
  function createSeatColorsSlice(namespace, defaultState, mountKey = "seatColors") {
852
1011
  const setLastCpuColor = createActionCreator(`seatColors/${namespace}/setLastCpuColor`);
853
1012
  const setLastGuestColor = createActionCreator(`seatColors/${namespace}/setLastGuestColor`);
854
- const setProfileOverride = createActionCreator(`seatColors/${namespace}/setProfileOverride`);
1013
+ const setProfileColor = createActionCreator(`seatColors/${namespace}/setProfileColor`);
1014
+ function withoutPick(state, id) {
1015
+ if (!state.profileColors[id]) return state;
1016
+ const profileColors = { ...state.profileColors };
1017
+ delete profileColors[id];
1018
+ return { ...state, profileColors };
1019
+ }
855
1020
  function reducer(state = defaultState, action) {
856
1021
  if (action.type === REHYDRATE2) {
857
1022
  const persisted = action.payload?.[mountKey];
858
- return { ...defaultState, ...state, ...persisted };
1023
+ const next = { ...defaultState, ...state, ...persisted };
1024
+ delete next.profileOverride;
1025
+ return next;
859
1026
  }
860
1027
  if (setLastCpuColor.match(action)) return { ...state, lastCpuColor: action.payload };
861
1028
  if (setLastGuestColor.match(action)) return { ...state, lastGuestColor: { ...state.lastGuestColor, [action.payload.seat]: action.payload.color } };
862
- if (setProfileOverride.match(action)) return { ...state, profileOverride: { ...state.profileOverride, [action.payload.seat]: action.payload.color } };
1029
+ if (setProfileColor.match(action)) {
1030
+ const { profile, color } = action.payload;
1031
+ if (color === null || sameColor(color, profile.color)) return withoutPick(state, profile.id);
1032
+ return { ...state, profileColors: { ...state.profileColors, [profile.id]: { color, profileColor: profile.color } } };
1033
+ }
1034
+ if (profilesActions.remove.match(action)) return withoutPick(state, action.payload);
1035
+ if (profilesActions.update.match(action)) {
1036
+ const { id, patch } = action.payload;
1037
+ const pick = state.profileColors[id];
1038
+ return pick && patch.color !== void 0 && !sameColor(patch.color, pick.profileColor) ? withoutPick(state, id) : state;
1039
+ }
1040
+ if (profilesActions.setAll.match(action)) {
1041
+ return action.payload.reduce((next, profile) => {
1042
+ const pick = next.profileColors[profile.id];
1043
+ return pick && !sameColor(profile.color, pick.profileColor) ? withoutPick(next, profile.id) : next;
1044
+ }, state);
1045
+ }
863
1046
  return state;
864
1047
  }
865
- return { actions: { setLastCpuColor, setLastGuestColor, setProfileOverride }, reducer };
1048
+ return { actions: { setLastCpuColor, setLastGuestColor, setProfileColor }, reducer };
866
1049
  }
867
1050
 
868
1051
  // src/redux/useSharedProfilesSync.ts
869
- var import_react5 = require("react");
870
- var import_react_native5 = require("react-native");
1052
+ var import_react6 = require("react");
1053
+ var import_react_native8 = require("react-native");
871
1054
  function useSharedProfilesSync({ groupId, onRemoteChange }) {
872
- const pendingWriteRef = (0, import_react5.useRef)(false);
873
- const onRemoteChangeRef = (0, import_react5.useRef)(onRemoteChange);
874
- (0, import_react5.useEffect)(() => {
1055
+ const pendingWriteRef = (0, import_react6.useRef)(false);
1056
+ const onRemoteChangeRef = (0, import_react6.useRef)(onRemoteChange);
1057
+ (0, import_react6.useEffect)(() => {
875
1058
  onRemoteChangeRef.current = onRemoteChange;
876
1059
  }, [onRemoteChange]);
877
- (0, import_react5.useEffect)(() => {
1060
+ (0, import_react6.useEffect)(() => {
878
1061
  if (!isSharedProfileStoreAvailable) return;
879
1062
  let cancelled = false;
880
- const sub = import_react_native5.AppState.addEventListener("change", (next) => {
1063
+ const sub = import_react_native8.AppState.addEventListener("change", (next) => {
881
1064
  if (next !== "active" || pendingWriteRef.current) return;
882
1065
  readSharedProfiles(groupId).then((shared) => {
883
1066
  if (!cancelled && shared !== null) onRemoteChangeRef.current(shared);
884
1067
  });
885
1068
  });
1069
+ const unsubscribe = subscribeSharedProfiles(groupId, (shared) => {
1070
+ if (!cancelled && !pendingWriteRef.current) onRemoteChangeRef.current(shared);
1071
+ });
886
1072
  return () => {
887
1073
  cancelled = true;
888
1074
  sub.remove();
1075
+ unsubscribe();
889
1076
  };
890
1077
  }, [groupId]);
891
- const syncToShared = (0, import_react5.useCallback)(
1078
+ const syncToShared = (0, import_react6.useCallback)(
892
1079
  (next) => {
893
1080
  if (!isSharedProfileStoreAvailable) return;
894
1081
  pendingWriteRef.current = true;
@@ -912,6 +1099,7 @@ function avoidSeatColorClash(p1Color, p2Color, defaults) {
912
1099
  0 && (module.exports = {
913
1100
  MAX_PROFILE_NAME_LENGTH,
914
1101
  MAX_TAG_LENGTH,
1102
+ OptionPicker,
915
1103
  ProfileChip,
916
1104
  ProfileColorProvider,
917
1105
  ProfilePicker,
@@ -922,17 +1110,21 @@ function avoidSeatColorClash(p1Color, p2Color, defaults) {
922
1110
  createProfileRecord,
923
1111
  createProfileSelectionSlice,
924
1112
  createSeatColorsSlice,
1113
+ getOptionPickerContentSize,
925
1114
  getProfilePickerContentSize,
926
1115
  isSharedProfileStoreAvailable,
927
1116
  isValidProfile,
928
1117
  isValidTag,
929
1118
  loadSharedProfiles,
1119
+ profileSeatColor,
930
1120
  profilesActions,
931
1121
  profilesReducer,
932
1122
  readSharedProfiles,
933
1123
  resolveInitialProfiles,
934
1124
  saveSharedProfiles,
1125
+ subscribeSharedProfiles,
935
1126
  useProfileColorPreview,
936
1127
  useProfileDisplayColor,
1128
+ useProfileSwatchRenderer,
937
1129
  useSharedProfilesSync
938
1130
  });