@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.mjs CHANGED
@@ -5,18 +5,67 @@ 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/chrome";
12
+ import { useState } from "react";
13
+ import { StyleSheet as StyleSheet3, View as View3 } 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
- import { StyleSheet, View } from "react-native";
18
+ import { StyleSheet as StyleSheet2, View as View2 } from "react-native";
11
19
  import { Icon, Text } from "react-native-paper";
20
+
21
+ // src/ProfileColorContext.tsx
22
+ import { createContext, useContext, useMemo } from "react";
12
23
  import { jsx } from "react/jsx-runtime";
24
+ var ProfileColorContext = createContext({});
25
+ function ProfileColorProvider({ preview, renderSwatch, children }) {
26
+ const value = useMemo(() => ({ preview, renderSwatch }), [preview, renderSwatch]);
27
+ return /* @__PURE__ */ jsx(ProfileColorContext.Provider, { value, children });
28
+ }
29
+ function useProfileColorPreview() {
30
+ return useContext(ProfileColorContext).preview;
31
+ }
32
+ function useProfileSwatchRenderer() {
33
+ return useContext(ProfileColorContext).renderSwatch;
34
+ }
35
+ function useProfileDisplayColor(hex) {
36
+ const preview = useProfileColorPreview();
37
+ return preview ? preview(hex) : hex;
38
+ }
39
+
40
+ // src/SwatchLayer.tsx
41
+ import { StyleSheet, View } from "react-native";
42
+ import { jsx as jsx2 } from "react/jsx-runtime";
43
+ function SwatchLayer({ render, color, size }) {
44
+ return /* @__PURE__ */ jsx2(View, { style: [styles.layer, { borderRadius: size / 2, height: size, width: size }], children: render(color, size) });
45
+ }
46
+ var styles = StyleSheet.create({
47
+ layer: {
48
+ left: 0,
49
+ overflow: "hidden",
50
+ pointerEvents: "none",
51
+ position: "absolute",
52
+ top: 0
53
+ }
54
+ });
55
+
56
+ // src/IdentityChip.tsx
57
+ import { jsx as jsx3, jsxs } from "react/jsx-runtime";
13
58
  var EMOJI_PATTERN = /\p{Extended_Pictographic}/u;
14
59
  function IdentityChip({ color, tag, icon, size, filled = true }) {
60
+ const renderSwatch = useProfileSwatchRenderer();
15
61
  const contrastColor = getContrastColor(color);
16
62
  const tagFontSize = size * (tag && EMOJI_PATTERN.test(tag) ? 0.6 : tag && Array.from(tag).length >= 3 ? 0.32 : 0.4);
17
- return /* @__PURE__ */ jsx(View, { style: [styles.chip, { borderRadius: size / 2, height: size, width: size }, filled && { backgroundColor: color }], children: tag ? /* @__PURE__ */ jsx(Text, { style: [styles.tag, { color: contrastColor, fontSize: tagFontSize }], numberOfLines: 1, adjustsFontSizeToFit: true, children: tag }) : /* @__PURE__ */ jsx(Icon, { source: icon, size: size * 0.6, color: contrastColor }) });
63
+ return /* @__PURE__ */ jsxs(View2, { style: [styles2.chip, { borderRadius: size / 2, height: size, width: size }, filled && !renderSwatch && { backgroundColor: color }], children: [
64
+ filled && renderSwatch && /* @__PURE__ */ jsx3(SwatchLayer, { render: renderSwatch, color, size }),
65
+ tag ? /* @__PURE__ */ jsx3(Text, { style: [styles2.tag, { color: contrastColor, fontSize: tagFontSize }], numberOfLines: 1, adjustsFontSizeToFit: true, children: tag }) : /* @__PURE__ */ jsx3(Icon, { source: icon, size: size * 0.6, color: contrastColor })
66
+ ] });
18
67
  }
19
- var styles = StyleSheet.create({
68
+ var styles2 = StyleSheet2.create({
20
69
  chip: {
21
70
  alignItems: "center",
22
71
  justifyContent: "center"
@@ -27,36 +76,8 @@ var styles = StyleSheet.create({
27
76
  }
28
77
  });
29
78
 
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";
79
+ // src/PickerMenu.tsx
80
+ import { Fragment, jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
60
81
  var TRIGGER_HEIGHT = 28;
61
82
  var POPOVER_WIDTH = 220;
62
83
  var ROW_HEIGHT = 36;
@@ -67,6 +88,8 @@ var CHIP_SIZE = 32;
67
88
  var CHIP_TRIGGER_PADDING = 6;
68
89
  var SWATCH_SIZE = 28;
69
90
  var SWATCH_MIN_GAP = 6;
91
+ var SWATCH_RING_WIDTH = 2;
92
+ var COLOR_ROW_SWATCH_SIZE = 22;
70
93
  var COLOR_ROW_CAPTIONED_HEIGHT = 48;
71
94
  var COLOR_LABEL_LINE = 18;
72
95
  var COLOR_CAPTION_LINE = 16;
@@ -83,59 +106,41 @@ function colorPageHeight(swatches) {
83
106
  const rows = Math.ceil(swatches.length / GRID_COLUMNS);
84
107
  return ROW_HEIGHT + GRID_TOP + rows * SWATCH_SIZE + (rows - 1) * GRID_GAP + GRID_BOTTOM;
85
108
  }
86
- function getProfilePickerContentSize(profileCount, hasManageRow = false, colorSection) {
87
- const rowCount = profileCount + 1 + (hasManageRow ? 1 : 0);
109
+ function pickerContentSize(rowCount, colorSection) {
88
110
  const list = rowCount * ROW_HEIGHT + (colorSection ? colorRowHeight(colorSection.caption) + COLOR_ROW_DIVIDER : 0);
89
111
  const content = colorSection ? Math.max(list, colorPageHeight(colorSection.swatches ?? defaultColors)) : list;
90
112
  return { width: POPOVER_WIDTH, height: LIST_PADDING * 2 + content };
91
113
  }
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();
114
+ function PickerMenu({ id, host, color, dark, label, chip, colorSection, homeColor, contentSize, align: forcedAlign, alignOverride, rotation, triggerStyle, footer, children }) {
95
115
  const menuBg = dark ? "#000000" : "#FFFFFF";
96
116
  const fg = dark ? "#FFFFFF" : "#000000";
97
117
  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);
118
+ const auto = useAutoAlign(open, contentSize.width, contentSize.height, rotation);
102
119
  const placement = alignOverride ?? auto;
103
120
  const { maxHeight, measured, triggerRef, verticalAlign } = placement;
104
121
  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;
122
+ const chipTrigger = !!chip;
119
123
  const [triggerWidth, setTriggerWidth] = useState(0);
120
124
  const handleAnchorLayout = chipTrigger ? (e) => setTriggerWidth(e.nativeEvent.layout.width) : void 0;
121
125
  const [colorPage, setColorPage] = useState(false);
126
+ const showColorPage = !!colorSection && colorPage;
122
127
  const handleTriggerPress = () => {
123
128
  setColorPage(false);
124
129
  host.toggle(id);
125
130
  };
126
- const triggerLabel = (selectedProfile?.name ?? guestLabel).toUpperCase();
127
- const manageFooter = !!onManage && !(colorSection && colorPage);
128
- return /* @__PURE__ */ jsxs(View2, { style: [styles2.anchor, open && styles2.anchorOpen], onLayout: handleAnchorLayout, children: [
129
- /* @__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: [
130
- chipTrigger && /* @__PURE__ */ jsx4(IdentityChip, { color, tag: selectedProfile?.tag, icon: selectedProfile ? "account" : nullIcon, size: CHIP_SIZE }),
131
- /* @__PURE__ */ jsx4(Text2, { style: [styles2.triggerLabel, { color: chipTrigger ? fg : color }], numberOfLines: 1, children: triggerLabel }),
131
+ const triggerLabel = label.toUpperCase();
132
+ const showFooter = !!footer && !showColorPage;
133
+ return /* @__PURE__ */ jsxs2(View3, { style: [styles3.anchor, open && styles3.anchorOpen], onLayout: handleAnchorLayout, children: [
134
+ /* @__PURE__ */ jsx4(TouchableRipple, { onPress: handleTriggerPress, borderless: chipTrigger, style: [styles3.trigger, chipTrigger && styles3.chipTrigger, triggerStyle], accessibilityRole: "button", accessibilityLabel: triggerLabel, children: /* @__PURE__ */ jsxs2(View3, { ref: triggerRef, collapsable: false, style: chipTrigger ? styles3.chipTriggerInner : styles3.triggerInner, children: [
135
+ chip && /* @__PURE__ */ jsx4(IdentityChip, { color, tag: chip.tag, icon: chip.icon, size: CHIP_SIZE }),
136
+ /* @__PURE__ */ jsx4(Text2, { style: [styles3.triggerLabel, { color: chipTrigger ? fg : color }], numberOfLines: 1, children: triggerLabel }),
132
137
  /* @__PURE__ */ jsx4(Icon2, { source: "menu-down", size: 16, color: chipTrigger ? fg : color })
133
138
  ] }) }),
134
- /* @__PURE__ */ jsx4(PopoverBody, { visible: open && measured, align, verticalAlign, caretColor: menuBg, caretBorderColor: color, caretBorderWidth: MENU_BORDER_WIDTH, triggerSize: chipTrigger ? triggerWidth : TRIGGER_HEIGHT, children: /* @__PURE__ */ jsxs(
135
- View2,
139
+ /* @__PURE__ */ jsx4(PopoverBody, { visible: open && measured, align, verticalAlign, caretColor: menuBg, caretBorderColor: color, caretBorderWidth: MENU_BORDER_WIDTH, triggerSize: chipTrigger ? triggerWidth : TRIGGER_HEIGHT, children: /* @__PURE__ */ jsxs2(
140
+ View3,
136
141
  {
137
142
  style: [
138
- styles2.menu,
143
+ styles3.menu,
139
144
  {
140
145
  backgroundColor: menuBg,
141
146
  borderColor: color,
@@ -144,78 +149,88 @@ function ProfilePicker({ idPrefix, host, profiles, selectedId, takenId, color, d
144
149
  }
145
150
  ],
146
151
  children: [
147
- /* @__PURE__ */ jsx4(FadingScrollView, { contentContainerStyle: [styles2.menuContent, manageFooter && styles2.menuContentAboveFooter], showsVerticalScrollIndicator: false, children: colorSection && colorPage ? /* @__PURE__ */ jsx4(ColorPage, { section: colorSection, profileColor: selectedProfile?.color, fg, onBack: () => setColorPage(false) }) : /* @__PURE__ */ jsxs(Fragment, { children: [
152
+ /* @__PURE__ */ jsx4(FadingScrollView, { contentContainerStyle: [styles3.menuContent, showFooter && styles3.menuContentAboveFooter], showsVerticalScrollIndicator: false, children: colorSection && colorPage ? /* @__PURE__ */ jsx4(ColorPage, { section: colorSection, homeColor, fg, onBack: () => setColorPage(false) }) : /* @__PURE__ */ jsxs2(Fragment, { children: [
148
153
  colorSection && /* @__PURE__ */ jsx4(ColorRow, { section: colorSection, fg, dividerColor: color, onPress: () => setColorPage(true) }),
149
- /* @__PURE__ */ jsx4(TouchableRipple, { onPress: handleSelectGuest, style: [styles2.row, selectedId === null && { backgroundColor: color }], children: /* @__PURE__ */ jsxs(View2, { style: styles2.rowInner, children: [
150
- /* @__PURE__ */ jsx4(View2, { style: styles2.guestChip, children: /* @__PURE__ */ jsx4(Icon2, { source: nullIcon, size: 13, color: selectedId === null ? getContrastColor2(color) : fg }) }),
151
- /* @__PURE__ */ jsx4(Text2, { style: [styles2.rowLabel, { color: selectedId === null ? getContrastColor2(color) : fg }], children: nullLabel })
152
- ] }) }),
153
- sortedProfiles.map((profile) => {
154
- const taken = profile.id === takenId;
155
- const selected = selectedId === profile.id;
156
- const rowColor = colorPreview ? colorPreview(profile.color) : profile.color;
157
- 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: [
158
- /* @__PURE__ */ jsx4(ProfileChip, { profile, filled: !selected }),
159
- /* @__PURE__ */ jsx4(
160
- Text2,
161
- {
162
- style: [
163
- styles2.rowLabel,
164
- {
165
- color: selected ? getContrastColor2(rowColor) : fg
166
- }
167
- ],
168
- numberOfLines: 1,
169
- children: profile.name
170
- }
171
- )
172
- ] }) }, profile.id);
173
- })
174
- ] }) }, colorSection && colorPage ? "color" : "list"),
175
- manageFooter && /* @__PURE__ */ jsxs(View2, { style: styles2.menuFooter, children: [
176
- /* @__PURE__ */ jsx4(View2, { style: [styles2.divider, { backgroundColor: color }] }),
177
- /* @__PURE__ */ jsx4(TouchableRipple, { onPress: handleManage, style: styles2.row, children: /* @__PURE__ */ jsxs(View2, { style: styles2.manageRowInner, children: [
178
- /* @__PURE__ */ jsx4(Icon2, { source: "account-cog-outline", size: 16, color }),
179
- /* @__PURE__ */ jsx4(Text2, { style: [styles2.manageLabel, { color: fg }], children: "Manage" })
180
- ] }) })
154
+ children
155
+ ] }) }, showColorPage ? "color" : "list"),
156
+ showFooter && /* @__PURE__ */ jsxs2(View3, { style: styles3.menuFooter, children: [
157
+ /* @__PURE__ */ jsx4(View3, { style: [styles3.divider, { backgroundColor: color }] }),
158
+ footer
181
159
  ] })
182
160
  ]
183
161
  }
184
162
  ) })
185
163
  ] });
186
164
  }
165
+ function IconRow({ icon, label, selected, color, fg, disabled = false, onPress }) {
166
+ const textColor = selected ? getContrastColor2(color) : fg;
167
+ return /* @__PURE__ */ jsx4(TouchableRipple, { disabled, onPress, style: [pickerMenuStyles.row, selected && { backgroundColor: color }], children: /* @__PURE__ */ jsxs2(View3, { style: [pickerMenuStyles.rowInner, disabled && pickerMenuStyles.rowDisabled], children: [
168
+ /* @__PURE__ */ jsx4(View3, { style: styles3.iconSlot, children: /* @__PURE__ */ jsx4(Icon2, { source: icon, size: 13, color: textColor }) }),
169
+ /* @__PURE__ */ jsx4(Text2, { style: [pickerMenuStyles.rowLabel, { color: textColor }], numberOfLines: 1, children: label })
170
+ ] }) });
171
+ }
187
172
  function ColorRow({ section, fg, dividerColor, onPress }) {
188
173
  const { value, previewValue, label = "Color", caption } = section;
174
+ const renderSwatch = useProfileSwatchRenderer();
189
175
  const drawn = previewValue ? previewValue(value) : value;
190
- return /* @__PURE__ */ jsxs(Fragment, { children: [
191
- /* @__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: [
192
- /* @__PURE__ */ jsx4(View2, { style: [styles2.colorRowSwatch, { backgroundColor: drawn }] }),
193
- /* @__PURE__ */ jsxs(View2, { style: styles2.colorRowText, children: [
194
- /* @__PURE__ */ jsx4(Text2, { style: [styles2.colorRowLabel, { color: fg }], numberOfLines: 1, children: label }),
195
- caption ? /* @__PURE__ */ jsx4(Text2, { style: [styles2.colorRowCaption, { color: fg }], numberOfLines: 1, children: caption }) : null
176
+ return /* @__PURE__ */ jsxs2(Fragment, { children: [
177
+ /* @__PURE__ */ jsx4(TouchableRipple, { onPress, style: [pickerMenuStyles.row, { height: colorRowHeight(caption) }], accessibilityRole: "button", accessibilityLabel: caption ? `${label}, ${caption}` : label, children: /* @__PURE__ */ jsxs2(View3, { style: pickerMenuStyles.rowInner, children: [
178
+ /* @__PURE__ */ jsx4(View3, { style: [styles3.colorRowSwatch, !renderSwatch && { backgroundColor: drawn }], children: renderSwatch && /* @__PURE__ */ jsx4(SwatchLayer, { render: renderSwatch, color: drawn, size: COLOR_ROW_SWATCH_SIZE }) }),
179
+ /* @__PURE__ */ jsxs2(View3, { style: styles3.colorRowText, children: [
180
+ /* @__PURE__ */ jsx4(Text2, { style: [styles3.colorRowLabel, { color: fg }], numberOfLines: 1, children: label }),
181
+ caption ? /* @__PURE__ */ jsx4(Text2, { style: [styles3.colorRowCaption, { color: fg }], numberOfLines: 1, children: caption }) : null
196
182
  ] }),
197
183
  /* @__PURE__ */ jsx4(Icon2, { source: "chevron-right", size: 18, color: fg })
198
184
  ] }) }),
199
- /* @__PURE__ */ jsx4(View2, { style: [styles2.divider, { backgroundColor: dividerColor }] })
185
+ /* @__PURE__ */ jsx4(View3, { style: [styles3.divider, { backgroundColor: dividerColor }] })
200
186
  ] });
201
187
  }
202
- function ColorPage({ section, profileColor, fg, onBack }) {
203
- const { value, onChange, previewValue, swatches = defaultColors, label = "Color" } = section;
188
+ function ColorPage({ section, homeColor, fg, onBack }) {
189
+ const { value, onChange, previewValue, swatches = defaultColors, label = "Color", takenValue, allowSwapTaken } = section;
190
+ const renderSwatch = useProfileSwatchRenderer();
204
191
  const displayFor = previewValue ?? ((hex) => hex);
205
- return /* @__PURE__ */ jsxs(Fragment, { children: [
206
- /* @__PURE__ */ jsx4(TouchableRipple, { onPress: onBack, style: styles2.row, accessibilityRole: "button", accessibilityLabel: "Back", children: /* @__PURE__ */ jsxs(View2, { style: styles2.rowInner, children: [
207
- /* @__PURE__ */ jsx4(View2, { style: styles2.guestChip, children: /* @__PURE__ */ jsx4(Icon2, { source: "chevron-left", size: 20, color: fg }) }),
208
- /* @__PURE__ */ jsx4(Text2, { style: [styles2.colorRowLabel, { color: fg }], numberOfLines: 1, children: label })
192
+ return /* @__PURE__ */ jsxs2(Fragment, { children: [
193
+ /* @__PURE__ */ jsx4(TouchableRipple, { onPress: onBack, style: pickerMenuStyles.row, accessibilityRole: "button", accessibilityLabel: "Back", children: /* @__PURE__ */ jsxs2(View3, { style: pickerMenuStyles.rowInner, children: [
194
+ /* @__PURE__ */ jsx4(View3, { style: styles3.iconSlot, children: /* @__PURE__ */ jsx4(Icon2, { source: "chevron-left", size: 20, color: fg }) }),
195
+ /* @__PURE__ */ jsx4(Text2, { style: [styles3.colorRowLabel, { color: fg }], numberOfLines: 1, children: label })
209
196
  ] }) }),
210
- /* @__PURE__ */ jsx4(View2, { style: styles2.swatches, children: swatches.map((swatch) => {
197
+ /* @__PURE__ */ jsx4(View3, { style: styles3.swatches, children: swatches.map((swatch) => {
211
198
  const selected = swatch.value.toLowerCase() === value.toLowerCase();
212
- const isProfileColor = !!profileColor && swatch.value.toLowerCase() === profileColor.toLowerCase();
199
+ const isHomeColor = !!homeColor && swatch.value.toLowerCase() === homeColor.toLowerCase();
200
+ const taken = !selected && !!takenValue && swatch.value.toLowerCase() === takenValue.toLowerCase();
201
+ const swappable = taken && !!allowSwapTaken;
213
202
  const swatchDisplay = displayFor(swatch.value);
214
- 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);
203
+ const contrast = getContrastColor2(swatchDisplay);
204
+ const accessibilityLabel = swappable ? `${swatch.label}, swap` : taken ? `${swatch.label}, taken` : isHomeColor ? `${swatch.label}, profile color` : swatch.label;
205
+ const mark = selected ? /* @__PURE__ */ jsx4(Icon2, { source: "check", size: 16, color: contrast }) : swappable ? /* @__PURE__ */ jsx4(Icon2, { source: "swap-horizontal", size: 16, color: contrast }) : taken ? /* @__PURE__ */ jsx4(Icon2, { source: "close", size: 16, color: contrast }) : isHomeColor ? /* @__PURE__ */ jsx4(View3, { style: [styles3.homeDot, { backgroundColor: contrast }] }) : /* @__PURE__ */ jsx4(View3, {});
206
+ return /* @__PURE__ */ jsx4(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__ */ jsxs2(View3, { style: styles3.swatchFace, children: [
207
+ /* @__PURE__ */ jsx4(SwatchLayer, { render: renderSwatch, color: swatchDisplay, size: selected ? SWATCH_SIZE - 2 * SWATCH_RING_WIDTH : SWATCH_SIZE }),
208
+ mark
209
+ ] }) : mark }, swatch.value);
215
210
  }) })
216
211
  ] });
217
212
  }
218
- var styles2 = StyleSheet2.create({
213
+ var pickerMenuStyles = StyleSheet3.create({
214
+ row: {
215
+ borderRadius: 8,
216
+ height: ROW_HEIGHT
217
+ },
218
+ rowDisabled: {
219
+ opacity: 0.35
220
+ },
221
+ rowInner: {
222
+ alignItems: "center",
223
+ flexDirection: "row",
224
+ flex: 1,
225
+ gap: 8,
226
+ paddingHorizontal: ROW_INSET
227
+ },
228
+ rowLabel: {
229
+ flex: 1,
230
+ fontSize: 14
231
+ }
232
+ });
233
+ var styles3 = StyleSheet3.create({
219
234
  anchor: {
220
235
  position: "relative"
221
236
  },
@@ -251,45 +266,38 @@ var styles2 = StyleSheet2.create({
251
266
  },
252
267
  // The color in effect, in the same 22pt slot as every row's chip.
253
268
  colorRowSwatch: {
254
- borderRadius: 11,
255
- height: 22,
256
- width: 22
269
+ borderRadius: COLOR_ROW_SWATCH_SIZE / 2,
270
+ height: COLOR_ROW_SWATCH_SIZE,
271
+ width: COLOR_ROW_SWATCH_SIZE
257
272
  },
258
273
  colorRowText: {
259
274
  flex: 1
260
275
  },
261
- // Separates Manage (a navigation action) from the selectable rows above it - different kind of
262
- // action, not just another row in the same list. Tinted with the seat's own color (passed in as
263
- // backgroundColor) rather than a flat gray, matching the border/caret's own accent-tinted look.
276
+ // Separates the footer (a navigation action) and the Color row from the selectable rows - a
277
+ // different kind of action, not just another row in the same list. Tinted with the seat's own color
278
+ // (passed in as backgroundColor) rather than a flat gray, matching the border/caret's own
279
+ // accent-tinted look.
264
280
  divider: {
265
281
  height: 1,
266
282
  marginVertical: 4,
267
283
  opacity: 0.25
268
284
  },
269
- // Matches ProfileChip's own default size/shape exactly (see this row's own render-site comment)
270
- // - no backgroundColor, unlike ProfileChip's own default: this row has no identity color of its
271
- // own to fill it with, and the render site now colors the icon directly instead.
272
- guestChip: {
285
+ // The selected profile's own color (see ColorPage).
286
+ homeDot: {
287
+ borderRadius: 4,
288
+ height: 8,
289
+ width: 8
290
+ },
291
+ // Matches ProfileChip's own default size/shape exactly (see IconRow's own render-site comment) -
292
+ // no backgroundColor, unlike ProfileChip's own default: an icon row has no identity color of its
293
+ // own to fill it with, and the render site colors the icon directly instead.
294
+ iconSlot: {
273
295
  alignItems: "center",
274
296
  borderRadius: 11,
275
297
  height: 22,
276
298
  justifyContent: "center",
277
299
  width: 22
278
300
  },
279
- manageLabel: {
280
- fontSize: 13,
281
- fontWeight: "700",
282
- letterSpacing: 0.5
283
- },
284
- // Centered rather than left-aligned like a normal row - see this row's own render-site comment
285
- // for why (a footer action, not another selectable identity).
286
- manageRowInner: {
287
- alignItems: "center",
288
- flexDirection: "row",
289
- gap: 6,
290
- height: "100%",
291
- justifyContent: "center"
292
- },
293
301
  menu: {
294
302
  borderRadius: 12,
295
303
  borderWidth: MENU_BORDER_WIDTH,
@@ -305,7 +313,7 @@ var styles2 = StyleSheet2.create({
305
313
  menuContent: {
306
314
  padding: LIST_PADDING
307
315
  },
308
- // With Manage pinned below, its own padding takes the bottom edge (same total height as before).
316
+ // With a footer pinned below, its own padding takes the bottom edge (same total height as before).
309
317
  menuContentAboveFooter: {
310
318
  paddingBottom: 0
311
319
  },
@@ -313,30 +321,6 @@ var styles2 = StyleSheet2.create({
313
321
  paddingBottom: LIST_PADDING,
314
322
  paddingHorizontal: LIST_PADDING
315
323
  },
316
- // The selected profile's own color (see ColorSection).
317
- profileDot: {
318
- borderRadius: 4,
319
- height: 8,
320
- width: 8
321
- },
322
- row: {
323
- borderRadius: 8,
324
- height: ROW_HEIGHT
325
- },
326
- rowDisabled: {
327
- opacity: 0.35
328
- },
329
- rowInner: {
330
- alignItems: "center",
331
- flexDirection: "row",
332
- flex: 1,
333
- gap: 8,
334
- paddingHorizontal: ROW_INSET
335
- },
336
- rowLabel: {
337
- flex: 1,
338
- fontSize: 14
339
- },
340
324
  swatch: {
341
325
  alignItems: "center",
342
326
  borderRadius: SWATCH_SIZE / 2,
@@ -344,9 +328,22 @@ var styles2 = StyleSheet2.create({
344
328
  justifyContent: "center",
345
329
  width: SWATCH_SIZE
346
330
  },
331
+ // A drawn swatch (see ColorPage) and its mark: the whole circle inside the selected one's ring.
332
+ swatchFace: {
333
+ alignItems: "center",
334
+ bottom: 0,
335
+ justifyContent: "center",
336
+ left: 0,
337
+ position: "absolute",
338
+ right: 0,
339
+ top: 0
340
+ },
347
341
  swatchSelected: {
348
342
  borderColor: "#ffffff",
349
- borderWidth: 2
343
+ borderWidth: SWATCH_RING_WIDTH
344
+ },
345
+ swatchTaken: {
346
+ opacity: 0.35
350
347
  },
351
348
  swatches: {
352
349
  columnGap: GRID_GAP,
@@ -374,15 +371,114 @@ var styles2 = StyleSheet2.create({
374
371
  }
375
372
  });
376
373
 
377
- // src/ProfilesManager.tsx
378
- import { defaultColors as defaultColors2, useAutoPaperTheme } from "@rific/auto-paper";
374
+ // src/OptionPicker.tsx
375
+ import { jsx as jsx5 } from "react/jsx-runtime";
376
+ function getOptionPickerContentSize(optionCount, colorSection) {
377
+ return pickerContentSize(optionCount, colorSection);
378
+ }
379
+ function OptionPicker({ id, host, options, value, onChange, color, dark, icon, rowIcon = icon, colorSection, align, alignOverride, rotation = 0, triggerStyle }) {
380
+ const fg = dark ? "#FFFFFF" : "#000000";
381
+ const selected = options.find((option) => option.value === value);
382
+ const handleSelect = (option) => {
383
+ onChange(option.value);
384
+ host.close();
385
+ };
386
+ return /* @__PURE__ */ jsx5(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__ */ jsx5(IconRow, { icon: option.icon ?? rowIcon, label: option.label, selected: option.value === value, color, fg, onPress: () => handleSelect(option) }, option.value)) });
387
+ }
388
+
389
+ // src/ProfileChip.tsx
390
+ import { jsx as jsx6 } from "react/jsx-runtime";
391
+ function ProfileChip({ profile, size = 22, filled = true }) {
392
+ const color = useProfileDisplayColor(profile.color);
393
+ return /* @__PURE__ */ jsx6(IdentityChip, { color, tag: profile.tag, icon: "account", size, filled });
394
+ }
395
+
396
+ // src/ProfilePicker.tsx
397
+ import { getContrastColor as getContrastColor3 } from "@rific/auto-paper";
379
398
  import { TouchableRipple as TouchableRipple2 } from "@rific/feedback-press";
399
+ import { useMemo as useMemo2 } from "react";
400
+ import { StyleSheet as StyleSheet4, View as View4 } from "react-native";
401
+ import { Icon as Icon3, Text as Text3 } from "react-native-paper";
402
+ import { jsx as jsx7, jsxs as jsxs3 } from "react/jsx-runtime";
403
+ function getProfilePickerContentSize(profileCount, hasManageRow = false, colorSection) {
404
+ return pickerContentSize(profileCount + 1 + (hasManageRow ? 1 : 0), colorSection);
405
+ }
406
+ 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 }) {
407
+ const id = `${idPrefix}-profile`;
408
+ const colorPreview = useProfileColorPreview();
409
+ const fg = dark ? "#FFFFFF" : "#000000";
410
+ const selectedProfile = profiles.find((p) => p.id === selectedId) ?? null;
411
+ const sortedProfiles = useMemo2(() => [...profiles].sort((a, b) => a.name.localeCompare(b.name)), [profiles]);
412
+ const contentSize = getProfilePickerContentSize(profiles.length, !!onManage, colorSection);
413
+ const isTaken = (profileId) => profileId === takenId || profileId !== selectedId && !!takenIds?.includes(profileId);
414
+ const handleSelectGuest = () => {
415
+ onSelect(null);
416
+ host.close();
417
+ };
418
+ const handleSelectExisting = (profile) => {
419
+ if (isTaken(profile.id)) return;
420
+ onSelect(profile);
421
+ host.close();
422
+ };
423
+ const handleManage = () => {
424
+ host.close();
425
+ onManage?.();
426
+ };
427
+ const manageFooter = onManage ? /* @__PURE__ */ jsx7(TouchableRipple2, { onPress: handleManage, style: pickerMenuStyles.row, children: /* @__PURE__ */ jsxs3(View4, { style: styles4.manageRowInner, children: [
428
+ /* @__PURE__ */ jsx7(Icon3, { source: "account-cog-outline", size: 16, color }),
429
+ /* @__PURE__ */ jsx7(Text3, { style: [styles4.manageLabel, { color: fg }], children: "Manage" })
430
+ ] }) }) : void 0;
431
+ return /* @__PURE__ */ jsxs3(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: [
432
+ /* @__PURE__ */ jsx7(IconRow, { icon: nullIcon, label: nullLabel, selected: selectedId === null, color, fg, onPress: handleSelectGuest }),
433
+ sortedProfiles.map((profile) => {
434
+ const taken = isTaken(profile.id);
435
+ const selected = selectedId === profile.id;
436
+ const rowColor = colorPreview ? colorPreview(profile.color) : profile.color;
437
+ return /* @__PURE__ */ jsx7(TouchableRipple2, { disabled: taken, onPress: () => handleSelectExisting(profile), style: [pickerMenuStyles.row, selected && { backgroundColor: rowColor }], children: /* @__PURE__ */ jsxs3(View4, { style: [pickerMenuStyles.rowInner, taken && pickerMenuStyles.rowDisabled], children: [
438
+ /* @__PURE__ */ jsx7(ProfileChip, { profile, filled: !selected }),
439
+ /* @__PURE__ */ jsx7(
440
+ Text3,
441
+ {
442
+ style: [
443
+ pickerMenuStyles.rowLabel,
444
+ {
445
+ color: selected ? getContrastColor3(rowColor) : fg
446
+ }
447
+ ],
448
+ numberOfLines: 1,
449
+ children: profile.name
450
+ }
451
+ )
452
+ ] }) }, profile.id);
453
+ })
454
+ ] });
455
+ }
456
+ var styles4 = StyleSheet4.create({
457
+ manageLabel: {
458
+ fontSize: 13,
459
+ fontWeight: "700",
460
+ letterSpacing: 0.5
461
+ },
462
+ // Centered rather than left-aligned like a normal row - see the footer's own comment for why (a
463
+ // footer action, not another selectable identity).
464
+ manageRowInner: {
465
+ alignItems: "center",
466
+ flexDirection: "row",
467
+ gap: 6,
468
+ height: "100%",
469
+ justifyContent: "center"
470
+ }
471
+ });
472
+
473
+ // src/ProfilesManager.tsx
474
+ import { defaultColors as defaultColors2, FadingScrollView as FadingScrollView2, useAutoPaperTheme } from "@rific/auto-paper";
475
+ import { TouchableRipple as TouchableRipple3 } from "@rific/feedback-press";
380
476
  import { useFocusChain } from "@rific/focus-chain";
381
477
  import { useToast } from "@rific/toaster";
382
- import { ConfirmDialog, getInlineColorPickerContentSize, InlineColorPicker, usePopoverHost } from "@tastic/hud";
383
- import { useCallback, useEffect, useImperativeHandle, useMemo as useMemo2, useRef, useState as useState2 } from "react";
384
- import { KeyboardAvoidingView, Platform, ScrollView, StyleSheet as StyleSheet3, useWindowDimensions, View as View3 } from "react-native";
385
- import { Icon as Icon3, Text as Text3, TextInput } from "react-native-paper";
478
+ import { ConfirmDialog, getInlineColorPickerContentSize, InlineColorPicker, usePopoverHost } from "@tastic/hud/chrome";
479
+ import { useCallback, useEffect, useImperativeHandle, useMemo as useMemo3, useRef, useState as useState2 } from "react";
480
+ import { KeyboardAvoidingView, Platform, StyleSheet as StyleSheet5, useWindowDimensions, View as View5 } from "react-native";
481
+ import { Icon as Icon4, Text as Text4, TextInput } from "react-native-paper";
386
482
  import { useSafeAreaInsets } from "react-native-safe-area-context";
387
483
 
388
484
  // src/profilesValidation.ts
@@ -418,7 +514,7 @@ function isValidProfile(value) {
418
514
  }
419
515
 
420
516
  // src/ProfilesManager.tsx
421
- import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
517
+ import { Fragment as Fragment2, jsx as jsx8, jsxs as jsxs4 } from "react/jsx-runtime";
422
518
  var CHIP_SIZE2 = 40;
423
519
  function deriveTag(name) {
424
520
  const trimmed = name.trim();
@@ -493,40 +589,40 @@ function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, h
493
589
  }
494
590
  setConfirmDeleteId(null);
495
591
  }, [confirmDeleteId, editingId, onDelete]);
496
- const sortedProfiles = useMemo2(() => [...profiles].sort((a, b) => a.name.localeCompare(b.name)), [profiles]);
592
+ const sortedProfiles = useMemo3(() => [...profiles].sort((a, b) => a.name.localeCompare(b.name)), [profiles]);
497
593
  const rows = [...sortedProfiles.map((profile) => ({ kind: "profile", profile })), { kind: "new" }];
498
- return /* @__PURE__ */ jsxs2(Fragment2, { children: [
499
- /* @__PURE__ */ jsxs2(View3, { style: [styles3.header, { paddingTop: 8 + insets.top, paddingLeft: 8 + insets.left }], children: [
594
+ return /* @__PURE__ */ jsxs4(Fragment2, { children: [
595
+ /* @__PURE__ */ jsxs4(View5, { style: [styles5.header, { paddingTop: 8 + insets.top, paddingLeft: 8 + insets.left }], children: [
500
596
  headerLeft,
501
- /* @__PURE__ */ jsx5(Text3, { variant: titleVariant, style: [styles3.title, { color: fg }], children: "Profiles" })
597
+ /* @__PURE__ */ jsx8(Text4, { variant: titleVariant, style: [styles5.title, { color: fg }], children: "Profiles" })
502
598
  ] }),
503
- /* @__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) => {
599
+ /* @__PURE__ */ jsx8(KeyboardAvoidingView, { behavior: Platform.OS === "ios" ? "padding" : void 0, style: styles5.keyboardAvoiding, children: /* @__PURE__ */ jsx8(FadingScrollView2, { style: styles5.scrollView, contentContainerStyle: [styles5.content, { paddingBottom: 32 + insets.bottom }], showsVerticalScrollIndicator: false, keyboardShouldPersistTaps: "handled", children: rows.map((row) => {
504
600
  if (row.kind === "new") {
505
601
  if (editingId !== "new") {
506
- return /* @__PURE__ */ jsx5(TouchableRipple2, { onPress: () => startEdit("new"), style: styles3.row, children: /* @__PURE__ */ jsxs2(View3, { style: styles3.rowInner, children: [
507
- /* @__PURE__ */ jsx5(View3, { style: [styles3.rowChip, styles3.newChip, { borderColor: fg }], children: /* @__PURE__ */ jsx5(Icon3, { source: "plus", size: 20, color: fg }) }),
508
- /* @__PURE__ */ jsx5(Text3, { variant: "bodyLarge", style: { color: fg }, children: "New Profile" })
602
+ return /* @__PURE__ */ jsx8(TouchableRipple3, { onPress: () => startEdit("new"), style: styles5.row, children: /* @__PURE__ */ jsxs4(View5, { style: styles5.rowInner, children: [
603
+ /* @__PURE__ */ jsx8(View5, { style: [styles5.rowChip, styles5.newChip, { borderColor: fg }], children: /* @__PURE__ */ jsx8(Icon4, { source: "plus", size: 20, color: fg }) }),
604
+ /* @__PURE__ */ jsx8(Text4, { variant: "bodyLarge", style: { color: fg }, children: "New Profile" })
509
605
  ] }) }, "new");
510
606
  }
511
- return /* @__PURE__ */ jsx5(EditRow, { draftName, onNameChange: handleNameChange, draftColor, onColorChange: setDraftColor, draftTag, onTagChange: handleTagChange, onSubmit: commitEdit, autoFocus: true, fgMuted, host, dark, colorPreview, useColorPickerAlign }, "new");
607
+ return /* @__PURE__ */ jsx8(EditRow, { draftName, onNameChange: handleNameChange, draftColor, onColorChange: setDraftColor, draftTag, onTagChange: handleTagChange, onSubmit: commitEdit, autoFocus: true, fgMuted, host, dark, colorPreview, useColorPickerAlign }, "new");
512
608
  }
513
609
  const { profile } = row;
514
610
  if (editingId === profile.id) {
515
- 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);
611
+ return /* @__PURE__ */ jsx8(EditRow, { draftName, onNameChange: handleNameChange, draftColor, onColorChange: setDraftColor, draftTag, onTagChange: handleTagChange, onSubmit: commitEdit, onDelete: () => setConfirmDeleteId(profile.id), fgMuted, host, dark, colorPreview, useColorPickerAlign }, profile.id);
516
612
  }
517
- return /* @__PURE__ */ jsx5(TouchableRipple2, { onPress: () => startEdit(profile), style: styles3.row, children: /* @__PURE__ */ jsxs2(View3, { style: styles3.rowInner, children: [
518
- /* @__PURE__ */ jsx5(ProfileChip, { profile, size: CHIP_SIZE2 }),
519
- /* @__PURE__ */ jsx5(Text3, { variant: "bodyLarge", style: [styles3.rowName, { color: fg }], numberOfLines: 1, children: profile.name })
613
+ return /* @__PURE__ */ jsx8(TouchableRipple3, { onPress: () => startEdit(profile), style: styles5.row, children: /* @__PURE__ */ jsxs4(View5, { style: styles5.rowInner, children: [
614
+ /* @__PURE__ */ jsx8(ProfileChip, { profile, size: CHIP_SIZE2 }),
615
+ /* @__PURE__ */ jsx8(Text4, { variant: "bodyLarge", style: [styles5.rowName, { color: fg }], numberOfLines: 1, children: profile.name })
520
616
  ] }) }, profile.id);
521
617
  }) }) }),
522
- /* @__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) })
618
+ /* @__PURE__ */ jsx8(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) })
523
619
  ] });
524
620
  }
525
621
  function AlignedColorPicker({ useAlign, ...props }) {
526
622
  const { width: windowWidth } = useWindowDimensions();
527
623
  const { width, height } = getInlineColorPickerContentSize(defaultColors2.length, windowWidth);
528
624
  const alignOverride = useAlign(props.host.openId === props.id, width, height);
529
- return /* @__PURE__ */ jsx5(InlineColorPicker, { ...props, alignOverride });
625
+ return /* @__PURE__ */ jsx8(InlineColorPicker, { ...props, alignOverride });
530
626
  }
531
627
  function EditRow({ draftName, onNameChange, draftColor, onColorChange, draftTag, onTagChange, onSubmit, onDelete, autoFocus, fgMuted, host, dark, colorPreview, useColorPickerAlign }) {
532
628
  const register = useFocusChain();
@@ -539,14 +635,14 @@ function EditRow({ draftName, onNameChange, draftColor, onColorChange, draftTag,
539
635
  useEffect(() => {
540
636
  if (host.openId === "color") tagFocusRef.current();
541
637
  }, [host.openId]);
542
- return /* @__PURE__ */ jsxs2(View3, { style: styles3.editRow, children: [
543
- 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 }),
544
- /* @__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" }),
545
- /* @__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 }),
546
- onDelete && /* @__PURE__ */ jsx5(TouchableRipple2, { onPress: onDelete, style: styles3.iconButton, children: /* @__PURE__ */ jsx5(Icon3, { source: "trash-can-outline", size: 18, color: fgMuted }) })
638
+ return /* @__PURE__ */ jsxs4(View5, { style: styles5.editRow, children: [
639
+ useColorPickerAlign ? /* @__PURE__ */ jsx8(AlignedColorPicker, { useAlign: useColorPickerAlign, id: "color", host, value: draftColor, onChange: onColorChange, previewValue: colorPreview, tag: draftTag, dark }) : /* @__PURE__ */ jsx8(InlineColorPicker, { id: "color", host, value: draftColor, onChange: onColorChange, previewValue: colorPreview, tag: draftTag, dark }),
640
+ /* @__PURE__ */ jsx8(TextInput, { ref: tag.ref, ...tag.props, value: draftTag, onChangeText: onTagChange, accessibilityLabel: "Tag", autoCapitalize: "characters", selectTextOnFocus: true, returnKeyType: "next", style: styles5.hiddenTagInput, pointerEvents: "none" }),
641
+ /* @__PURE__ */ jsx8(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 }),
642
+ onDelete && /* @__PURE__ */ jsx8(TouchableRipple3, { onPress: onDelete, style: styles5.iconButton, children: /* @__PURE__ */ jsx8(Icon4, { source: "trash-can-outline", size: 18, color: fgMuted }) })
547
643
  ] });
548
644
  }
549
- var styles3 = StyleSheet3.create({
645
+ var styles5 = StyleSheet5.create({
550
646
  content: {
551
647
  gap: 4,
552
648
  paddingHorizontal: 20
@@ -628,8 +724,8 @@ var styles3 = StyleSheet3.create({
628
724
  import { useAutoPaperTheme as useAutoPaperTheme2 } from "@rific/auto-paper";
629
725
  import { IconButton } from "@rific/feedback-press";
630
726
  import { useRef as useRef2 } from "react";
631
- import { StyleSheet as StyleSheet4, View as View4 } from "react-native";
632
- import { jsx as jsx6 } from "react/jsx-runtime";
727
+ import { StyleSheet as StyleSheet6, View as View6 } from "react-native";
728
+ import { jsx as jsx9 } from "react/jsx-runtime";
633
729
  function ProfilesScreen({ profiles, defaultColor, onCreate, onSave, onDelete, onBack, fg: fgOverride, fgMuted, bg: bgOverride, titleVariant, colorPreview, useColorPickerAlign }) {
634
730
  const { dark } = useAutoPaperTheme2();
635
731
  const managerRef = useRef2(null);
@@ -639,9 +735,9 @@ function ProfilesScreen({ profiles, defaultColor, onCreate, onSave, onDelete, on
639
735
  managerRef.current?.commitPendingEdit();
640
736
  onBack();
641
737
  };
642
- 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" }) }) });
738
+ return /* @__PURE__ */ jsx9(View6, { style: [styles6.container, { backgroundColor: bg }], children: /* @__PURE__ */ jsx9(ProfilesManager, { ref: managerRef, profiles, defaultColor, onCreate, onSave, onDelete, fg, fgMuted, titleVariant, colorPreview, useColorPickerAlign, headerLeft: /* @__PURE__ */ jsx9(IconButton, { icon: "arrow-left", iconColor: fg, size: 24, onPress: handleBack, accessibilityLabel: "Back" }) }) });
643
739
  }
644
- var styles4 = StyleSheet4.create({
740
+ var styles6 = StyleSheet6.create({
645
741
  container: {
646
742
  flex: 1
647
743
  }
@@ -752,6 +848,7 @@ function createProfileRecord(input) {
752
848
  }
753
849
 
754
850
  // src/sharedProfileStore.ts
851
+ import { Platform as Platform2 } from "react-native";
755
852
  var native = null;
756
853
  try {
757
854
  const { requireOptionalNativeModule } = __require("expo-modules-core");
@@ -760,15 +857,42 @@ try {
760
857
  native = null;
761
858
  }
762
859
  var PROFILES_KEY = "sharedProfiles";
763
- var isSharedProfileStoreAvailable = native !== null;
764
- async function readSharedProfiles(groupId) {
765
- if (!native) return [];
766
- let json = null;
860
+ var webKey = (groupId, key) => `${groupId}:${key}`;
861
+ var createWebStore = () => {
862
+ if (Platform2.OS !== "web") return null;
767
863
  try {
768
- json = await native.getSharedJSON(groupId, PROFILES_KEY);
864
+ const storage = globalThis.localStorage;
865
+ if (!storage) return null;
866
+ const probe = webKey("@tastic/profile", "probe");
867
+ storage.setItem(probe, "1");
868
+ storage.removeItem(probe);
869
+ let lastWriteFailed = false;
870
+ return {
871
+ storage,
872
+ store: {
873
+ getSharedJSON: async (groupId, key) => {
874
+ if (lastWriteFailed) throw new Error("The last write to the shared roster failed");
875
+ return storage.getItem(webKey(groupId, key));
876
+ },
877
+ setSharedJSON: async (groupId, key, value) => {
878
+ try {
879
+ storage.setItem(webKey(groupId, key), value);
880
+ lastWriteFailed = false;
881
+ } catch (error) {
882
+ lastWriteFailed = true;
883
+ throw error;
884
+ }
885
+ }
886
+ }
887
+ };
769
888
  } catch {
770
889
  return null;
771
890
  }
891
+ };
892
+ var web = native ? null : createWebStore();
893
+ if (web) native = web.store;
894
+ var isSharedProfileStoreAvailable = native !== null;
895
+ var parseRoster = (json) => {
772
896
  if (!json) return [];
773
897
  try {
774
898
  const parsed = JSON.parse(json);
@@ -778,6 +902,16 @@ async function readSharedProfiles(groupId) {
778
902
  } catch {
779
903
  return null;
780
904
  }
905
+ };
906
+ async function readSharedProfiles(groupId) {
907
+ if (!native) return [];
908
+ let json = null;
909
+ try {
910
+ json = await native.getSharedJSON(groupId, PROFILES_KEY);
911
+ } catch {
912
+ return null;
913
+ }
914
+ return parseRoster(json);
781
915
  }
782
916
  async function loadSharedProfiles(groupId) {
783
917
  return await readSharedProfiles(groupId) ?? [];
@@ -789,6 +923,19 @@ async function saveSharedProfiles(groupId, profiles) {
789
923
  } catch {
790
924
  }
791
925
  }
926
+ function subscribeSharedProfiles(groupId, listener) {
927
+ if (!web || typeof window === "undefined" || typeof window.addEventListener !== "function") return () => {
928
+ };
929
+ const storageKey = webKey(groupId, PROFILES_KEY);
930
+ const storage = web.storage;
931
+ const onStorage = (event) => {
932
+ if (event.key !== storageKey || event.storageArea !== storage || !event.newValue) return;
933
+ const profiles = parseRoster(event.newValue);
934
+ if (profiles !== null) listener(profiles);
935
+ };
936
+ window.addEventListener("storage", onStorage);
937
+ return () => window.removeEventListener("storage", onStorage);
938
+ }
792
939
 
793
940
  // src/redux/resolveInitialProfiles.ts
794
941
  async function resolveInitialProfiles(groupId, localFallback) {
@@ -805,21 +952,52 @@ async function resolveInitialProfiles(groupId, localFallback) {
805
952
 
806
953
  // src/redux/seatColorsSlice.ts
807
954
  var REHYDRATE2 = "persist/REHYDRATE";
955
+ function sameColor(a, b) {
956
+ return a.toLowerCase() === b.toLowerCase();
957
+ }
958
+ function profileSeatColor(profile, profileColors) {
959
+ const pick = profileColors[profile.id];
960
+ return pick && sameColor(pick.profileColor, profile.color) ? pick.color : profile.color;
961
+ }
808
962
  function createSeatColorsSlice(namespace, defaultState, mountKey = "seatColors") {
809
963
  const setLastCpuColor = createActionCreator(`seatColors/${namespace}/setLastCpuColor`);
810
964
  const setLastGuestColor = createActionCreator(`seatColors/${namespace}/setLastGuestColor`);
811
- const setProfileOverride = createActionCreator(`seatColors/${namespace}/setProfileOverride`);
965
+ const setProfileColor = createActionCreator(`seatColors/${namespace}/setProfileColor`);
966
+ function withoutPick(state, id) {
967
+ if (!state.profileColors[id]) return state;
968
+ const profileColors = { ...state.profileColors };
969
+ delete profileColors[id];
970
+ return { ...state, profileColors };
971
+ }
812
972
  function reducer(state = defaultState, action) {
813
973
  if (action.type === REHYDRATE2) {
814
974
  const persisted = action.payload?.[mountKey];
815
- return { ...defaultState, ...state, ...persisted };
975
+ const next = { ...defaultState, ...state, ...persisted };
976
+ delete next.profileOverride;
977
+ return next;
816
978
  }
817
979
  if (setLastCpuColor.match(action)) return { ...state, lastCpuColor: action.payload };
818
980
  if (setLastGuestColor.match(action)) return { ...state, lastGuestColor: { ...state.lastGuestColor, [action.payload.seat]: action.payload.color } };
819
- if (setProfileOverride.match(action)) return { ...state, profileOverride: { ...state.profileOverride, [action.payload.seat]: action.payload.color } };
981
+ if (setProfileColor.match(action)) {
982
+ const { profile, color } = action.payload;
983
+ if (color === null || sameColor(color, profile.color)) return withoutPick(state, profile.id);
984
+ return { ...state, profileColors: { ...state.profileColors, [profile.id]: { color, profileColor: profile.color } } };
985
+ }
986
+ if (profilesActions.remove.match(action)) return withoutPick(state, action.payload);
987
+ if (profilesActions.update.match(action)) {
988
+ const { id, patch } = action.payload;
989
+ const pick = state.profileColors[id];
990
+ return pick && patch.color !== void 0 && !sameColor(patch.color, pick.profileColor) ? withoutPick(state, id) : state;
991
+ }
992
+ if (profilesActions.setAll.match(action)) {
993
+ return action.payload.reduce((next, profile) => {
994
+ const pick = next.profileColors[profile.id];
995
+ return pick && !sameColor(profile.color, pick.profileColor) ? withoutPick(next, profile.id) : next;
996
+ }, state);
997
+ }
820
998
  return state;
821
999
  }
822
- return { actions: { setLastCpuColor, setLastGuestColor, setProfileOverride }, reducer };
1000
+ return { actions: { setLastCpuColor, setLastGuestColor, setProfileColor }, reducer };
823
1001
  }
824
1002
 
825
1003
  // src/redux/useSharedProfilesSync.ts
@@ -840,9 +1018,13 @@ function useSharedProfilesSync({ groupId, onRemoteChange }) {
840
1018
  if (!cancelled && shared !== null) onRemoteChangeRef.current(shared);
841
1019
  });
842
1020
  });
1021
+ const unsubscribe = subscribeSharedProfiles(groupId, (shared) => {
1022
+ if (!cancelled && !pendingWriteRef.current) onRemoteChangeRef.current(shared);
1023
+ });
843
1024
  return () => {
844
1025
  cancelled = true;
845
1026
  sub.remove();
1027
+ unsubscribe();
846
1028
  };
847
1029
  }, [groupId]);
848
1030
  const syncToShared = useCallback2(
@@ -868,6 +1050,7 @@ function avoidSeatColorClash(p1Color, p2Color, defaults) {
868
1050
  export {
869
1051
  MAX_PROFILE_NAME_LENGTH,
870
1052
  MAX_TAG_LENGTH,
1053
+ OptionPicker,
871
1054
  ProfileChip,
872
1055
  ProfileColorProvider,
873
1056
  ProfilePicker,
@@ -878,17 +1061,21 @@ export {
878
1061
  createProfileRecord,
879
1062
  createProfileSelectionSlice,
880
1063
  createSeatColorsSlice,
1064
+ getOptionPickerContentSize,
881
1065
  getProfilePickerContentSize,
882
1066
  isSharedProfileStoreAvailable,
883
1067
  isValidProfile,
884
1068
  isValidTag,
885
1069
  loadSharedProfiles,
1070
+ profileSeatColor,
886
1071
  profilesActions,
887
1072
  profilesReducer,
888
1073
  readSharedProfiles,
889
1074
  resolveInitialProfiles,
890
1075
  saveSharedProfiles,
1076
+ subscribeSharedProfiles,
891
1077
  useProfileColorPreview,
892
1078
  useProfileDisplayColor,
1079
+ useProfileSwatchRenderer,
893
1080
  useSharedProfilesSync
894
1081
  };