@tastic/profile 0.12.0 → 0.14.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,17 +5,34 @@ 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/ProfileChip.tsx
8
+ // src/IdentityChip.tsx
9
9
  import { getContrastColor } from "@rific/auto-paper";
10
10
  import { StyleSheet, View } from "react-native";
11
11
  import { Icon, Text } from "react-native-paper";
12
+ import { jsx } from "react/jsx-runtime";
13
+ var EMOJI_PATTERN = /\p{Extended_Pictographic}/u;
14
+ function IdentityChip({ color, tag, icon, size, filled = true }) {
15
+ const contrastColor = getContrastColor(color);
16
+ 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 }) });
18
+ }
19
+ var styles = StyleSheet.create({
20
+ chip: {
21
+ alignItems: "center",
22
+ justifyContent: "center"
23
+ },
24
+ tag: {
25
+ fontWeight: "700",
26
+ paddingHorizontal: 2
27
+ }
28
+ });
12
29
 
13
30
  // src/ProfileColorContext.tsx
14
31
  import { createContext, useContext } from "react";
15
- import { jsx } from "react/jsx-runtime";
32
+ import { jsx as jsx2 } from "react/jsx-runtime";
16
33
  var ProfileColorContext = createContext(void 0);
17
34
  function ProfileColorProvider({ preview, children }) {
18
- return /* @__PURE__ */ jsx(ProfileColorContext.Provider, { value: preview, children });
35
+ return /* @__PURE__ */ jsx2(ProfileColorContext.Provider, { value: preview, children });
19
36
  }
20
37
  function useProfileColorPreview() {
21
38
  return useContext(ProfileColorContext);
@@ -26,42 +43,53 @@ function useProfileDisplayColor(hex) {
26
43
  }
27
44
 
28
45
  // src/ProfileChip.tsx
29
- import { jsx as jsx2 } from "react/jsx-runtime";
30
- var EMOJI_PATTERN = /\p{Extended_Pictographic}/u;
46
+ import { jsx as jsx3 } from "react/jsx-runtime";
31
47
  function ProfileChip({ profile, size = 22, filled = true }) {
32
48
  const color = useProfileDisplayColor(profile.color);
33
- const contrastColor = getContrastColor(color);
34
- const tagFontSize = size * (profile.tag && EMOJI_PATTERN.test(profile.tag) ? 0.6 : 0.4);
35
- return /* @__PURE__ */ jsx2(View, { style: [styles.chip, { borderRadius: size / 2, height: size, width: size }, filled && { backgroundColor: color }], children: profile.tag ? /* @__PURE__ */ jsx2(Text, { style: [styles.tag, { color: contrastColor, fontSize: tagFontSize }], numberOfLines: 1, adjustsFontSizeToFit: true, children: profile.tag }) : /* @__PURE__ */ jsx2(Icon, { source: "account", size: size * 0.6, color: contrastColor }) });
49
+ return /* @__PURE__ */ jsx3(IdentityChip, { color, tag: profile.tag, icon: "account", size, filled });
36
50
  }
37
- var styles = StyleSheet.create({
38
- chip: {
39
- alignItems: "center",
40
- justifyContent: "center"
41
- },
42
- tag: {
43
- fontWeight: "700",
44
- paddingHorizontal: 2
45
- }
46
- });
47
51
 
48
52
  // src/ProfilePicker.tsx
49
- import { getContrastColor as getContrastColor2 } from "@rific/auto-paper";
53
+ import { defaultColors, FadingScrollView, getContrastColor as getContrastColor2 } from "@rific/auto-paper";
50
54
  import { TouchableRipple } from "@rific/feedback-press";
51
55
  import { PopoverBody, useAutoAlign } from "@tastic/hud";
52
- import { useMemo } from "react";
53
- import { ScrollView, StyleSheet as StyleSheet2, View as View2 } from "react-native";
56
+ import { useMemo, useState } from "react";
57
+ import { StyleSheet as StyleSheet2, View as View2 } from "react-native";
54
58
  import { Icon as Icon2, Text as Text2 } from "react-native-paper";
55
- import { Fragment, jsx as jsx3, jsxs } from "react/jsx-runtime";
59
+ import { Fragment, jsx as jsx4, jsxs } from "react/jsx-runtime";
56
60
  var TRIGGER_HEIGHT = 28;
57
61
  var POPOVER_WIDTH = 220;
58
62
  var ROW_HEIGHT = 36;
59
63
  var LIST_PADDING = 8;
60
- function getProfilePickerContentSize(profileCount, hasManageRow = false) {
64
+ var MENU_BORDER_WIDTH = 2;
65
+ var ROW_INSET = 12;
66
+ var CHIP_SIZE = 32;
67
+ var CHIP_TRIGGER_PADDING = 6;
68
+ var SWATCH_SIZE = 28;
69
+ var SWATCH_MIN_GAP = 6;
70
+ var COLOR_ROW_CAPTIONED_HEIGHT = 48;
71
+ var COLOR_LABEL_LINE = 18;
72
+ var COLOR_CAPTION_LINE = 16;
73
+ var COLOR_ROW_DIVIDER = 9;
74
+ var GRID_TOP = 4;
75
+ var GRID_BOTTOM = 8;
76
+ var GRID_WIDTH = POPOVER_WIDTH - 2 * MENU_BORDER_WIDTH - 2 * LIST_PADDING - 2 * ROW_INSET;
77
+ var GRID_COLUMNS = Math.floor((GRID_WIDTH + SWATCH_MIN_GAP) / (SWATCH_SIZE + SWATCH_MIN_GAP));
78
+ var GRID_GAP = (GRID_WIDTH - GRID_COLUMNS * SWATCH_SIZE) / (GRID_COLUMNS - 1);
79
+ function colorRowHeight(caption) {
80
+ return caption ? COLOR_ROW_CAPTIONED_HEIGHT : ROW_HEIGHT;
81
+ }
82
+ function colorPageHeight(swatches) {
83
+ const rows = Math.ceil(swatches.length / GRID_COLUMNS);
84
+ return ROW_HEIGHT + GRID_TOP + rows * SWATCH_SIZE + (rows - 1) * GRID_GAP + GRID_BOTTOM;
85
+ }
86
+ function getProfilePickerContentSize(profileCount, hasManageRow = false, colorSection) {
61
87
  const rowCount = profileCount + 1 + (hasManageRow ? 1 : 0);
62
- return { width: POPOVER_WIDTH, height: LIST_PADDING * 2 + rowCount * ROW_HEIGHT };
88
+ const list = rowCount * ROW_HEIGHT + (colorSection ? colorRowHeight(colorSection.caption) + COLOR_ROW_DIVIDER : 0);
89
+ const content = colorSection ? Math.max(list, colorPageHeight(colorSection.swatches ?? defaultColors)) : list;
90
+ return { width: POPOVER_WIDTH, height: LIST_PADDING * 2 + content };
63
91
  }
64
- function ProfilePicker({ idPrefix, host, profiles, selectedId, takenId, color, dark, align: forcedAlign, alignOverride, rotation = 0, guestLabel, nullLabel = "Player", nullIcon = "account-off-outline", onSelect, onManage }) {
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 }) {
65
93
  const id = `${idPrefix}-profile`;
66
94
  const colorPreview = useProfileColorPreview();
67
95
  const menuBg = dark ? "#000000" : "#FFFFFF";
@@ -69,7 +97,7 @@ function ProfilePicker({ idPrefix, host, profiles, selectedId, takenId, color, d
69
97
  const open = host.openId === id;
70
98
  const selectedProfile = profiles.find((p) => p.id === selectedId) ?? null;
71
99
  const sortedProfiles = useMemo(() => [...profiles].sort((a, b) => a.name.localeCompare(b.name)), [profiles]);
72
- const { width: contentWidth, height: contentHeight } = getProfilePickerContentSize(profiles.length, !!onManage);
100
+ const { width: contentWidth, height: contentHeight } = getProfilePickerContentSize(profiles.length, !!onManage, colorSection);
73
101
  const auto = useAutoAlign(open, contentWidth, contentHeight, rotation);
74
102
  const placement = alignOverride ?? auto;
75
103
  const { maxHeight, measured, triggerRef, verticalAlign } = placement;
@@ -87,13 +115,23 @@ function ProfilePicker({ idPrefix, host, profiles, selectedId, takenId, color, d
87
115
  host.close();
88
116
  onManage?.();
89
117
  };
90
- return /* @__PURE__ */ jsxs(View2, { style: [styles2.anchor, open && styles2.anchorOpen], children: [
91
- /* @__PURE__ */ jsx3(TouchableRipple, { onPress: () => host.toggle(id), style: styles2.trigger, children: /* @__PURE__ */ jsxs(View2, { ref: triggerRef, collapsable: false, style: styles2.triggerInner, children: [
92
- /* @__PURE__ */ jsx3(Text2, { style: [styles2.triggerLabel, { color }], numberOfLines: 1, children: (selectedProfile?.name ?? guestLabel).toUpperCase() }),
93
- /* @__PURE__ */ jsx3(Icon2, { source: "menu-down", size: 16, color })
118
+ const chipTrigger = !!colorSection;
119
+ const [triggerWidth, setTriggerWidth] = useState(0);
120
+ const handleAnchorLayout = chipTrigger ? (e) => setTriggerWidth(e.nativeEvent.layout.width) : void 0;
121
+ const [colorPage, setColorPage] = useState(false);
122
+ const handleTriggerPress = () => {
123
+ setColorPage(false);
124
+ host.toggle(id);
125
+ };
126
+ const triggerLabel = (selectedProfile?.name ?? guestLabel).toUpperCase();
127
+ return /* @__PURE__ */ jsxs(View2, { style: [styles2.anchor, open && styles2.anchorOpen], onLayout: handleAnchorLayout, children: [
128
+ /* @__PURE__ */ jsx4(TouchableRipple, { onPress: handleTriggerPress, borderless: chipTrigger, style: [styles2.trigger, chipTrigger && styles2.chipTrigger, triggerStyle], accessibilityRole: "button", accessibilityLabel: triggerLabel, children: /* @__PURE__ */ jsxs(View2, { ref: triggerRef, collapsable: false, style: chipTrigger ? styles2.chipTriggerInner : styles2.triggerInner, children: [
129
+ chipTrigger && /* @__PURE__ */ jsx4(IdentityChip, { color, tag: selectedProfile?.tag, icon: selectedProfile ? "account" : nullIcon, size: CHIP_SIZE }),
130
+ /* @__PURE__ */ jsx4(Text2, { style: [styles2.triggerLabel, { color: chipTrigger ? fg : color }], numberOfLines: 1, children: triggerLabel }),
131
+ /* @__PURE__ */ jsx4(Icon2, { source: "menu-down", size: 16, color: chipTrigger ? fg : color })
94
132
  ] }) }),
95
- /* @__PURE__ */ jsx3(PopoverBody, { visible: open && measured, align, verticalAlign, caretColor: menuBg, caretBorderColor: color, caretBorderWidth: 2, triggerSize: TRIGGER_HEIGHT, children: /* @__PURE__ */ jsxs(
96
- ScrollView,
133
+ /* @__PURE__ */ jsx4(PopoverBody, { visible: open && measured, align, verticalAlign, caretColor: menuBg, caretBorderColor: color, caretBorderWidth: MENU_BORDER_WIDTH, triggerSize: chipTrigger ? triggerWidth : TRIGGER_HEIGHT, children: /* @__PURE__ */ jsx4(
134
+ View2,
97
135
  {
98
136
  style: [
99
137
  styles2.menu,
@@ -104,20 +142,19 @@ function ProfilePicker({ idPrefix, host, profiles, selectedId, takenId, color, d
104
142
  maxHeight
105
143
  }
106
144
  ],
107
- contentContainerStyle: styles2.menuContent,
108
- showsVerticalScrollIndicator: false,
109
- children: [
110
- /* @__PURE__ */ jsx3(TouchableRipple, { onPress: handleSelectGuest, style: [styles2.row, selectedId === null && { backgroundColor: color }], children: /* @__PURE__ */ jsxs(View2, { style: styles2.rowInner, children: [
111
- /* @__PURE__ */ jsx3(View2, { style: styles2.guestChip, children: /* @__PURE__ */ jsx3(Icon2, { source: nullIcon, size: 13, color: selectedId === null ? getContrastColor2(color) : fg }) }),
112
- /* @__PURE__ */ jsx3(Text2, { style: [styles2.rowLabel, { color: selectedId === null ? getContrastColor2(color) : fg }], children: nullLabel })
145
+ children: /* @__PURE__ */ jsx4(FadingScrollView, { contentContainerStyle: styles2.menuContent, showsVerticalScrollIndicator: false, children: colorSection && colorPage ? /* @__PURE__ */ jsx4(ColorPage, { section: colorSection, profileColor: selectedProfile?.color, fg, onBack: () => setColorPage(false) }) : /* @__PURE__ */ jsxs(Fragment, { children: [
146
+ colorSection && /* @__PURE__ */ jsx4(ColorRow, { section: colorSection, fg, dividerColor: color, onPress: () => setColorPage(true) }),
147
+ /* @__PURE__ */ jsx4(TouchableRipple, { onPress: handleSelectGuest, style: [styles2.row, selectedId === null && { backgroundColor: color }], children: /* @__PURE__ */ jsxs(View2, { style: styles2.rowInner, children: [
148
+ /* @__PURE__ */ jsx4(View2, { style: styles2.guestChip, children: /* @__PURE__ */ jsx4(Icon2, { source: nullIcon, size: 13, color: selectedId === null ? getContrastColor2(color) : fg }) }),
149
+ /* @__PURE__ */ jsx4(Text2, { style: [styles2.rowLabel, { color: selectedId === null ? getContrastColor2(color) : fg }], children: nullLabel })
113
150
  ] }) }),
114
151
  sortedProfiles.map((profile) => {
115
152
  const taken = profile.id === takenId;
116
153
  const selected = selectedId === profile.id;
117
154
  const rowColor = colorPreview ? colorPreview(profile.color) : profile.color;
118
- return /* @__PURE__ */ jsx3(TouchableRipple, { disabled: taken, onPress: () => handleSelectExisting(profile), style: [styles2.row, selected && { backgroundColor: rowColor }], children: /* @__PURE__ */ jsxs(View2, { style: [styles2.rowInner, taken && styles2.rowDisabled], children: [
119
- /* @__PURE__ */ jsx3(ProfileChip, { profile, filled: !selected }),
120
- /* @__PURE__ */ jsx3(
155
+ return /* @__PURE__ */ jsx4(TouchableRipple, { disabled: taken, onPress: () => handleSelectExisting(profile), style: [styles2.row, selected && { backgroundColor: rowColor }], children: /* @__PURE__ */ jsxs(View2, { style: [styles2.rowInner, taken && styles2.rowDisabled], children: [
156
+ /* @__PURE__ */ jsx4(ProfileChip, { profile, filled: !selected }),
157
+ /* @__PURE__ */ jsx4(
121
158
  Text2,
122
159
  {
123
160
  style: [
@@ -133,17 +170,48 @@ function ProfilePicker({ idPrefix, host, profiles, selectedId, takenId, color, d
133
170
  ] }) }, profile.id);
134
171
  }),
135
172
  onManage && /* @__PURE__ */ jsxs(Fragment, { children: [
136
- /* @__PURE__ */ jsx3(View2, { style: [styles2.divider, { backgroundColor: color }] }),
137
- /* @__PURE__ */ jsx3(TouchableRipple, { onPress: handleManage, style: styles2.row, children: /* @__PURE__ */ jsxs(View2, { style: styles2.manageRowInner, children: [
138
- /* @__PURE__ */ jsx3(Icon2, { source: "account-cog-outline", size: 16, color }),
139
- /* @__PURE__ */ jsx3(Text2, { style: [styles2.manageLabel, { color }], children: "Manage" })
173
+ /* @__PURE__ */ jsx4(View2, { style: [styles2.divider, { backgroundColor: color }] }),
174
+ /* @__PURE__ */ jsx4(TouchableRipple, { onPress: handleManage, style: styles2.row, children: /* @__PURE__ */ jsxs(View2, { style: styles2.manageRowInner, children: [
175
+ /* @__PURE__ */ jsx4(Icon2, { source: "account-cog-outline", size: 16, color }),
176
+ /* @__PURE__ */ jsx4(Text2, { style: [styles2.manageLabel, { color: fg }], children: "Manage" })
140
177
  ] }) })
141
178
  ] })
142
- ]
179
+ ] }) }, colorSection && colorPage ? "color" : "list")
143
180
  }
144
181
  ) })
145
182
  ] });
146
183
  }
184
+ function ColorRow({ section, fg, dividerColor, onPress }) {
185
+ const { value, previewValue, label = "Color", caption } = section;
186
+ const drawn = previewValue ? previewValue(value) : value;
187
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
188
+ /* @__PURE__ */ jsx4(TouchableRipple, { onPress, style: [styles2.row, { height: colorRowHeight(caption) }], accessibilityRole: "button", accessibilityLabel: caption ? `${label}, ${caption}` : label, children: /* @__PURE__ */ jsxs(View2, { style: styles2.rowInner, children: [
189
+ /* @__PURE__ */ jsx4(View2, { style: [styles2.colorRowSwatch, { backgroundColor: drawn }] }),
190
+ /* @__PURE__ */ jsxs(View2, { style: styles2.colorRowText, children: [
191
+ /* @__PURE__ */ jsx4(Text2, { style: [styles2.colorRowLabel, { color: fg }], numberOfLines: 1, children: label }),
192
+ caption ? /* @__PURE__ */ jsx4(Text2, { style: [styles2.colorRowCaption, { color: fg }], numberOfLines: 1, children: caption }) : null
193
+ ] }),
194
+ /* @__PURE__ */ jsx4(Icon2, { source: "chevron-right", size: 18, color: fg })
195
+ ] }) }),
196
+ /* @__PURE__ */ jsx4(View2, { style: [styles2.divider, { backgroundColor: dividerColor }] })
197
+ ] });
198
+ }
199
+ function ColorPage({ section, profileColor, fg, onBack }) {
200
+ const { value, onChange, previewValue, swatches = defaultColors, label = "Color" } = section;
201
+ const displayFor = previewValue ?? ((hex) => hex);
202
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
203
+ /* @__PURE__ */ jsx4(TouchableRipple, { onPress: onBack, style: styles2.row, accessibilityRole: "button", accessibilityLabel: "Back", children: /* @__PURE__ */ jsxs(View2, { style: styles2.rowInner, children: [
204
+ /* @__PURE__ */ jsx4(View2, { style: styles2.guestChip, children: /* @__PURE__ */ jsx4(Icon2, { source: "chevron-left", size: 20, color: fg }) }),
205
+ /* @__PURE__ */ jsx4(Text2, { style: [styles2.colorRowLabel, { color: fg }], numberOfLines: 1, children: label })
206
+ ] }) }),
207
+ /* @__PURE__ */ jsx4(View2, { style: styles2.swatches, children: swatches.map((swatch) => {
208
+ const selected = swatch.value.toLowerCase() === value.toLowerCase();
209
+ const isProfileColor = !!profileColor && swatch.value.toLowerCase() === profileColor.toLowerCase();
210
+ const swatchDisplay = displayFor(swatch.value);
211
+ return /* @__PURE__ */ jsx4(TouchableRipple, { onPress: () => onChange(swatch.value), borderless: true, style: [styles2.swatch, { backgroundColor: swatchDisplay }, selected && styles2.swatchSelected], accessibilityRole: "button", accessibilityLabel: isProfileColor ? `${swatch.label}, profile color` : swatch.label, accessibilityState: { selected }, children: selected ? /* @__PURE__ */ jsx4(Icon2, { source: "check", size: 16, color: getContrastColor2(swatchDisplay) }) : isProfileColor ? /* @__PURE__ */ jsx4(View2, { style: [styles2.profileDot, { backgroundColor: getContrastColor2(swatchDisplay) }] }) : /* @__PURE__ */ jsx4(View2, {}) }, swatch.value);
212
+ }) })
213
+ ] });
214
+ }
147
215
  var styles2 = StyleSheet2.create({
148
216
  anchor: {
149
217
  position: "relative"
@@ -153,6 +221,40 @@ var styles2 = StyleSheet2.create({
153
221
  anchorOpen: {
154
222
  zIndex: 100
155
223
  },
224
+ // The pill, rounded at both ends. The host adds its own background through `triggerStyle`.
225
+ chipTrigger: {
226
+ borderRadius: (CHIP_SIZE + 2 * CHIP_TRIGGER_PADDING) / 2
227
+ },
228
+ // The padding lives on the measured view, not the pressable around it, so the placement measures
229
+ // the pill's real edge: measured from inside the padding, a popover opening toward a screen edge
230
+ // was sized as if the pill were 6pt further from it, and ran 2pt past that edge.
231
+ chipTriggerInner: {
232
+ alignItems: "center",
233
+ flexDirection: "row",
234
+ gap: 8,
235
+ height: CHIP_SIZE + 2 * CHIP_TRIGGER_PADDING,
236
+ paddingLeft: CHIP_TRIGGER_PADDING,
237
+ paddingRight: CHIP_TRIGGER_PADDING + 8
238
+ },
239
+ colorRowCaption: {
240
+ fontSize: 12,
241
+ lineHeight: COLOR_CAPTION_LINE,
242
+ opacity: 0.6
243
+ },
244
+ // A row's label, with a line height so the captioned Color row's height is exact.
245
+ colorRowLabel: {
246
+ fontSize: 14,
247
+ lineHeight: COLOR_LABEL_LINE
248
+ },
249
+ // The color in effect, in the same 22pt slot as every row's chip.
250
+ colorRowSwatch: {
251
+ borderRadius: 11,
252
+ height: 22,
253
+ width: 22
254
+ },
255
+ colorRowText: {
256
+ flex: 1
257
+ },
156
258
  // Separates Manage (a navigation action) from the selectable rows above it - different kind of
157
259
  // action, not just another row in the same list. Tinted with the seat's own color (passed in as
158
260
  // backgroundColor) rather than a flat gray, matching the border/caret's own accent-tinted look.
@@ -187,7 +289,7 @@ var styles2 = StyleSheet2.create({
187
289
  },
188
290
  menu: {
189
291
  borderRadius: 12,
190
- borderWidth: 2,
292
+ borderWidth: MENU_BORDER_WIDTH,
191
293
  elevation: 8,
192
294
  shadowColor: "#000000",
193
295
  shadowOffset: { height: 2, width: 0 },
@@ -200,6 +302,12 @@ var styles2 = StyleSheet2.create({
200
302
  menuContent: {
201
303
  padding: LIST_PADDING
202
304
  },
305
+ // The selected profile's own color (see ColorSection).
306
+ profileDot: {
307
+ borderRadius: 4,
308
+ height: 8,
309
+ width: 8
310
+ },
203
311
  row: {
204
312
  borderRadius: 8,
205
313
  height: ROW_HEIGHT
@@ -212,12 +320,33 @@ var styles2 = StyleSheet2.create({
212
320
  flexDirection: "row",
213
321
  flex: 1,
214
322
  gap: 8,
215
- paddingHorizontal: 12
323
+ paddingHorizontal: ROW_INSET
216
324
  },
217
325
  rowLabel: {
218
326
  flex: 1,
219
327
  fontSize: 14
220
328
  },
329
+ swatch: {
330
+ alignItems: "center",
331
+ borderRadius: SWATCH_SIZE / 2,
332
+ height: SWATCH_SIZE,
333
+ justifyContent: "center",
334
+ width: SWATCH_SIZE
335
+ },
336
+ swatchSelected: {
337
+ borderColor: "#ffffff",
338
+ borderWidth: 2
339
+ },
340
+ swatches: {
341
+ columnGap: GRID_GAP,
342
+ flexDirection: "row",
343
+ flexWrap: "wrap",
344
+ marginBottom: GRID_BOTTOM,
345
+ marginHorizontal: ROW_INSET,
346
+ marginTop: GRID_TOP,
347
+ rowGap: GRID_GAP,
348
+ width: GRID_WIDTH
349
+ },
221
350
  trigger: {
222
351
  alignSelf: "flex-start"
223
352
  },
@@ -235,14 +364,14 @@ var styles2 = StyleSheet2.create({
235
364
  });
236
365
 
237
366
  // src/ProfilesManager.tsx
238
- import { defaultColors, useAutoPaperTheme } from "@rific/auto-paper";
367
+ import { defaultColors as defaultColors2, useAutoPaperTheme } from "@rific/auto-paper";
239
368
  import { TouchableRipple as TouchableRipple2 } from "@rific/feedback-press";
240
369
  import { useFocusChain } from "@rific/focus-chain";
241
370
  import { useToast } from "@rific/toaster";
242
- import { getInlineColorPickerContentSize, InlineColorPicker, usePopoverHost } from "@tastic/hud";
243
- import { useCallback, useEffect, useImperativeHandle, useMemo as useMemo2, useRef, useState } from "react";
244
- import { KeyboardAvoidingView, Platform, ScrollView as ScrollView2, StyleSheet as StyleSheet3, useWindowDimensions, View as View3 } from "react-native";
245
- import { Button, Icon as Icon3, Portal, Text as Text3, TextInput } from "react-native-paper";
371
+ import { ConfirmDialog, getInlineColorPickerContentSize, InlineColorPicker, usePopoverHost } from "@tastic/hud";
372
+ import { useCallback, useEffect, useImperativeHandle, useMemo as useMemo2, useRef, useState as useState2 } from "react";
373
+ import { KeyboardAvoidingView, Platform, ScrollView, StyleSheet as StyleSheet3, useWindowDimensions, View as View3 } from "react-native";
374
+ import { Icon as Icon3, Text as Text3, TextInput } from "react-native-paper";
246
375
  import { useSafeAreaInsets } from "react-native-safe-area-context";
247
376
 
248
377
  // src/profilesValidation.ts
@@ -278,31 +407,27 @@ function isValidProfile(value) {
278
407
  }
279
408
 
280
409
  // src/ProfilesManager.tsx
281
- import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
282
- var CHIP_SIZE = 40;
410
+ import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
411
+ var CHIP_SIZE2 = 40;
283
412
  function deriveTag(name) {
284
413
  const trimmed = name.trim();
285
414
  return trimmed ? trimmed[0].toUpperCase() : "";
286
415
  }
287
- function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, headerLeft, ref, fg: fgOverride, fgMuted: fgMutedOverride, cardBg: cardBgOverride, titleVariant = "headlineSmall", colorPreview: colorPreviewProp, useColorPickerAlign }) {
416
+ function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, headerLeft, ref, fg: fgOverride, fgMuted: fgMutedOverride, titleVariant = "headlineSmall", colorPreview: colorPreviewProp, useColorPickerAlign }) {
288
417
  const contextColorPreview = useProfileColorPreview();
289
418
  const colorPreview = colorPreviewProp ?? contextColorPreview;
290
- const { dark, colors } = useAutoPaperTheme();
419
+ const { dark } = useAutoPaperTheme();
291
420
  const insets = useSafeAreaInsets();
292
421
  const { error: showErrorToast } = useToast();
293
422
  const host = usePopoverHost();
294
423
  const fg = fgOverride ?? (dark ? "#FFFFFF" : "#000000");
295
424
  const fgMuted = fgMutedOverride ?? (dark ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.5)");
296
- const cardBg = cardBgOverride ?? (dark ? "#111111" : "#F2F2F2");
297
- const cardBorder = dark ? "rgba(255,255,255,0.2)" : "rgba(0,0,0,0.2)";
298
- const cancelBg = dark ? "#2A2A2A" : "#E0E0E0";
299
- const cancelFg = dark ? "#FFFFFF" : "#000000";
300
- const [editingId, setEditingId] = useState(null);
301
- const [draftName, setDraftName] = useState("");
302
- const [draftColor, setDraftColor] = useState(defaultColor);
303
- const [draftTag, setDraftTag] = useState("");
304
- const [tagManuallySet, setTagManuallySet] = useState(false);
305
- const [confirmDeleteId, setConfirmDeleteId] = useState(null);
425
+ const [editingId, setEditingId] = useState2(null);
426
+ const [draftName, setDraftName] = useState2("");
427
+ const [draftColor, setDraftColor] = useState2(defaultColor);
428
+ const [draftTag, setDraftTag] = useState2("");
429
+ const [tagManuallySet, setTagManuallySet] = useState2(false);
430
+ const [confirmDeleteId, setConfirmDeleteId] = useState2(null);
306
431
  const commitEdit = useCallback(() => {
307
432
  const name = draftName.trim();
308
433
  if (editingId && name) {
@@ -362,45 +487,35 @@ function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, h
362
487
  return /* @__PURE__ */ jsxs2(Fragment2, { children: [
363
488
  /* @__PURE__ */ jsxs2(View3, { style: [styles3.header, { paddingTop: 8 + insets.top, paddingLeft: 8 + insets.left }], children: [
364
489
  headerLeft,
365
- /* @__PURE__ */ jsx4(Text3, { variant: titleVariant, style: [styles3.title, { color: fg }], children: "Profiles" })
490
+ /* @__PURE__ */ jsx5(Text3, { variant: titleVariant, style: [styles3.title, { color: fg }], children: "Profiles" })
366
491
  ] }),
367
- /* @__PURE__ */ jsx4(KeyboardAvoidingView, { behavior: Platform.OS === "ios" ? "padding" : void 0, style: styles3.keyboardAvoiding, children: /* @__PURE__ */ jsx4(ScrollView2, { style: styles3.scrollView, contentContainerStyle: [styles3.content, { paddingBottom: 32 + insets.bottom }], showsVerticalScrollIndicator: false, keyboardShouldPersistTaps: "handled", children: rows.map((row) => {
492
+ /* @__PURE__ */ jsx5(KeyboardAvoidingView, { behavior: Platform.OS === "ios" ? "padding" : void 0, style: styles3.keyboardAvoiding, children: /* @__PURE__ */ jsx5(ScrollView, { style: styles3.scrollView, contentContainerStyle: [styles3.content, { paddingBottom: 32 + insets.bottom }], showsVerticalScrollIndicator: false, keyboardShouldPersistTaps: "handled", children: rows.map((row) => {
368
493
  if (row.kind === "new") {
369
494
  if (editingId !== "new") {
370
- return /* @__PURE__ */ jsx4(TouchableRipple2, { onPress: () => startEdit("new"), style: styles3.row, children: /* @__PURE__ */ jsxs2(View3, { style: styles3.rowInner, children: [
371
- /* @__PURE__ */ jsx4(View3, { style: [styles3.rowChip, styles3.newChip, { borderColor: fg }], children: /* @__PURE__ */ jsx4(Icon3, { source: "plus", size: 20, color: fg }) }),
372
- /* @__PURE__ */ jsx4(Text3, { variant: "bodyLarge", style: { color: fg }, children: "New Profile" })
495
+ return /* @__PURE__ */ jsx5(TouchableRipple2, { onPress: () => startEdit("new"), style: styles3.row, children: /* @__PURE__ */ jsxs2(View3, { style: styles3.rowInner, children: [
496
+ /* @__PURE__ */ jsx5(View3, { style: [styles3.rowChip, styles3.newChip, { borderColor: fg }], children: /* @__PURE__ */ jsx5(Icon3, { source: "plus", size: 20, color: fg }) }),
497
+ /* @__PURE__ */ jsx5(Text3, { variant: "bodyLarge", style: { color: fg }, children: "New Profile" })
373
498
  ] }) }, "new");
374
499
  }
375
- return /* @__PURE__ */ jsx4(EditRow, { draftName, onNameChange: handleNameChange, draftColor, onColorChange: setDraftColor, draftTag, onTagChange: handleTagChange, onSubmit: commitEdit, autoFocus: true, fgMuted, host, dark, colorPreview, useColorPickerAlign }, "new");
500
+ return /* @__PURE__ */ jsx5(EditRow, { draftName, onNameChange: handleNameChange, draftColor, onColorChange: setDraftColor, draftTag, onTagChange: handleTagChange, onSubmit: commitEdit, autoFocus: true, fgMuted, host, dark, colorPreview, useColorPickerAlign }, "new");
376
501
  }
377
502
  const { profile } = row;
378
503
  if (editingId === profile.id) {
379
- return /* @__PURE__ */ jsx4(EditRow, { draftName, onNameChange: handleNameChange, draftColor, onColorChange: setDraftColor, draftTag, onTagChange: handleTagChange, onSubmit: commitEdit, onDelete: () => setConfirmDeleteId(profile.id), fgMuted, host, dark, colorPreview, useColorPickerAlign }, profile.id);
504
+ return /* @__PURE__ */ jsx5(EditRow, { draftName, onNameChange: handleNameChange, draftColor, onColorChange: setDraftColor, draftTag, onTagChange: handleTagChange, onSubmit: commitEdit, onDelete: () => setConfirmDeleteId(profile.id), fgMuted, host, dark, colorPreview, useColorPickerAlign }, profile.id);
380
505
  }
381
- return /* @__PURE__ */ jsx4(TouchableRipple2, { onPress: () => startEdit(profile), style: styles3.row, children: /* @__PURE__ */ jsxs2(View3, { style: styles3.rowInner, children: [
382
- /* @__PURE__ */ jsx4(ProfileChip, { profile, size: CHIP_SIZE }),
383
- /* @__PURE__ */ jsx4(Text3, { variant: "bodyLarge", style: [styles3.rowName, { color: fg }], numberOfLines: 1, children: profile.name })
506
+ return /* @__PURE__ */ jsx5(TouchableRipple2, { onPress: () => startEdit(profile), style: styles3.row, children: /* @__PURE__ */ jsxs2(View3, { style: styles3.rowInner, children: [
507
+ /* @__PURE__ */ jsx5(ProfileChip, { profile, size: CHIP_SIZE2 }),
508
+ /* @__PURE__ */ jsx5(Text3, { variant: "bodyLarge", style: [styles3.rowName, { color: fg }], numberOfLines: 1, children: profile.name })
384
509
  ] }) }, profile.id);
385
510
  }) }) }),
386
- confirmDeleteProfile && /* @__PURE__ */ jsx4(Portal, { children: /* @__PURE__ */ jsx4(View3, { style: styles3.overlay, children: /* @__PURE__ */ jsxs2(View3, { style: [styles3.overlayCard, { backgroundColor: cardBg, borderColor: cardBorder }], children: [
387
- /* @__PURE__ */ jsx4(Icon3, { source: "alert-outline", size: 64, color: colors.danger }),
388
- /* @__PURE__ */ jsx4(Text3, { variant: "headlineLarge", style: [styles3.overlayTitle, { color: colors.danger }], children: "Delete Profile?" }),
389
- /* @__PURE__ */ jsxs2(Text3, { variant: "bodyLarge", style: [styles3.overlayBody, { color: fg }], children: [
390
- "This permanently deletes ",
391
- confirmDeleteProfile.name,
392
- "\u2019s profile. This cannot be undone."
393
- ] }),
394
- /* @__PURE__ */ jsx4(Button, { mode: "contained", onPress: () => setConfirmDeleteId(null), style: styles3.overlayButton, buttonColor: cancelBg, textColor: cancelFg, children: "Cancel" }),
395
- /* @__PURE__ */ jsx4(Button, { mode: "contained", onPress: handleConfirmDelete, style: styles3.overlayButton, buttonColor: colors.danger, textColor: colors.onDanger, children: "Delete" })
396
- ] }) }) })
511
+ /* @__PURE__ */ jsx5(ConfirmDialog, { visible: confirmDeleteProfile !== null, title: "Delete Profile?", message: confirmDeleteProfile ? `This permanently deletes ${confirmDeleteProfile.name}\u2019s profile. This cannot be undone.` : "", confirmLabel: "Delete", cancelLabel: "Cancel", icon: "alert-outline", destructive: true, onConfirm: handleConfirmDelete, onCancel: () => setConfirmDeleteId(null) })
397
512
  ] });
398
513
  }
399
514
  function AlignedColorPicker({ useAlign, ...props }) {
400
515
  const { width: windowWidth } = useWindowDimensions();
401
- const { width, height } = getInlineColorPickerContentSize(defaultColors.length, windowWidth);
516
+ const { width, height } = getInlineColorPickerContentSize(defaultColors2.length, windowWidth);
402
517
  const alignOverride = useAlign(props.host.openId === props.id, width, height);
403
- return /* @__PURE__ */ jsx4(InlineColorPicker, { ...props, alignOverride });
518
+ return /* @__PURE__ */ jsx5(InlineColorPicker, { ...props, alignOverride });
404
519
  }
405
520
  function EditRow({ draftName, onNameChange, draftColor, onColorChange, draftTag, onTagChange, onSubmit, onDelete, autoFocus, fgMuted, host, dark, colorPreview, useColorPickerAlign }) {
406
521
  const register = useFocusChain();
@@ -414,10 +529,10 @@ function EditRow({ draftName, onNameChange, draftColor, onColorChange, draftTag,
414
529
  if (host.openId === "color") tagFocusRef.current();
415
530
  }, [host.openId]);
416
531
  return /* @__PURE__ */ jsxs2(View3, { style: styles3.editRow, children: [
417
- useColorPickerAlign ? /* @__PURE__ */ jsx4(AlignedColorPicker, { useAlign: useColorPickerAlign, id: "color", host, value: draftColor, onChange: onColorChange, previewValue: colorPreview, tag: draftTag, dark }) : /* @__PURE__ */ jsx4(InlineColorPicker, { id: "color", host, value: draftColor, onChange: onColorChange, previewValue: colorPreview, tag: draftTag, dark }),
418
- /* @__PURE__ */ jsx4(TextInput, { ref: tag.ref, ...tag.props, value: draftTag, onChangeText: onTagChange, accessibilityLabel: "Tag", autoCapitalize: "characters", selectTextOnFocus: true, returnKeyType: "next", style: styles3.hiddenTagInput, pointerEvents: "none" }),
419
- /* @__PURE__ */ jsx4(TextInput, { ref: name.ref, ...name.props, onSubmitEditing: onSubmit, mode: "outlined", dense: true, value: draftName, onChangeText: onNameChange, maxLength: MAX_PROFILE_NAME_LENGTH, placeholder: "Name", autoFocus, returnKeyType: "done", style: styles3.nameInput }),
420
- onDelete && /* @__PURE__ */ jsx4(TouchableRipple2, { onPress: onDelete, style: styles3.iconButton, children: /* @__PURE__ */ jsx4(Icon3, { source: "trash-can-outline", size: 18, color: fgMuted }) })
532
+ useColorPickerAlign ? /* @__PURE__ */ jsx5(AlignedColorPicker, { useAlign: useColorPickerAlign, id: "color", host, value: draftColor, onChange: onColorChange, previewValue: colorPreview, tag: draftTag, dark }) : /* @__PURE__ */ jsx5(InlineColorPicker, { id: "color", host, value: draftColor, onChange: onColorChange, previewValue: colorPreview, tag: draftTag, dark }),
533
+ /* @__PURE__ */ jsx5(TextInput, { ref: tag.ref, ...tag.props, value: draftTag, onChangeText: onTagChange, accessibilityLabel: "Tag", autoCapitalize: "characters", selectTextOnFocus: true, returnKeyType: "next", style: styles3.hiddenTagInput, pointerEvents: "none" }),
534
+ /* @__PURE__ */ jsx5(TextInput, { ref: name.ref, ...name.props, onSubmitEditing: onSubmit, mode: "outlined", dense: true, value: draftName, onChangeText: onNameChange, maxLength: MAX_PROFILE_NAME_LENGTH, placeholder: "Name", autoFocus, returnKeyType: "done", style: styles3.nameInput }),
535
+ onDelete && /* @__PURE__ */ jsx5(TouchableRipple2, { onPress: onDelete, style: styles3.iconButton, children: /* @__PURE__ */ jsx5(Icon3, { source: "trash-can-outline", size: 18, color: fgMuted }) })
421
536
  ] });
422
537
  }
423
538
  var styles3 = StyleSheet3.create({
@@ -425,11 +540,17 @@ var styles3 = StyleSheet3.create({
425
540
  gap: 4,
426
541
  paddingHorizontal: 20
427
542
  },
543
+ // zIndex: the color popover lives inside this row, and a zIndex only orders siblings of one parent
544
+ // (React Native Web also gives every positioned view its own stacking context), so the popover's
545
+ // own zIndex never reached the rows after this one: they were drawn over it (a Kid row and the New
546
+ // Profile row across the swatches, 2026-10-16). Raising the row lifts the popover with it. There
547
+ // is only ever one edit row.
428
548
  editRow: {
429
549
  alignItems: "center",
430
550
  flexDirection: "row",
431
551
  gap: 8,
432
- paddingVertical: 6
552
+ paddingVertical: 6,
553
+ zIndex: 1
433
554
  },
434
555
  header: {
435
556
  alignItems: "center",
@@ -464,37 +585,16 @@ var styles3 = StyleSheet3.create({
464
585
  borderStyle: "dashed",
465
586
  borderWidth: 2
466
587
  },
467
- overlay: {
468
- alignItems: "center",
469
- backgroundColor: "rgba(0,0,0,0.72)",
470
- bottom: 0,
471
- justifyContent: "center",
472
- left: 0,
473
- position: "absolute",
474
- right: 0,
475
- top: 0
476
- },
477
- overlayBody: { textAlign: "center" },
478
- overlayButton: { width: 160 },
479
- overlayCard: {
480
- alignItems: "center",
481
- borderRadius: 20,
482
- borderWidth: 1,
483
- gap: 16,
484
- maxWidth: 360,
485
- padding: 32
486
- },
487
- overlayTitle: { fontWeight: "bold", marginBottom: -8 },
488
588
  row: {
489
589
  borderRadius: 8,
490
590
  paddingVertical: 8
491
591
  },
492
592
  rowChip: {
493
593
  alignItems: "center",
494
- borderRadius: CHIP_SIZE / 2,
495
- height: CHIP_SIZE,
594
+ borderRadius: CHIP_SIZE2 / 2,
595
+ height: CHIP_SIZE2,
496
596
  justifyContent: "center",
497
- width: CHIP_SIZE
597
+ width: CHIP_SIZE2
498
598
  },
499
599
  rowInner: {
500
600
  alignItems: "center",
@@ -518,8 +618,8 @@ import { useAutoPaperTheme as useAutoPaperTheme2 } from "@rific/auto-paper";
518
618
  import { IconButton } from "@rific/feedback-press";
519
619
  import { useRef as useRef2 } from "react";
520
620
  import { StyleSheet as StyleSheet4, View as View4 } from "react-native";
521
- import { jsx as jsx5 } from "react/jsx-runtime";
522
- function ProfilesScreen({ profiles, defaultColor, onCreate, onSave, onDelete, onBack, fg: fgOverride, fgMuted, bg: bgOverride, cardBg, titleVariant, colorPreview, useColorPickerAlign }) {
621
+ import { jsx as jsx6 } from "react/jsx-runtime";
622
+ function ProfilesScreen({ profiles, defaultColor, onCreate, onSave, onDelete, onBack, fg: fgOverride, fgMuted, bg: bgOverride, titleVariant, colorPreview, useColorPickerAlign }) {
523
623
  const { dark } = useAutoPaperTheme2();
524
624
  const managerRef = useRef2(null);
525
625
  const fg = fgOverride ?? (dark ? "#FFFFFF" : "#000000");
@@ -528,7 +628,7 @@ function ProfilesScreen({ profiles, defaultColor, onCreate, onSave, onDelete, on
528
628
  managerRef.current?.commitPendingEdit();
529
629
  onBack();
530
630
  };
531
- return /* @__PURE__ */ jsx5(View4, { style: [styles4.container, { backgroundColor: bg }], children: /* @__PURE__ */ jsx5(ProfilesManager, { ref: managerRef, profiles, defaultColor, onCreate, onSave, onDelete, fg, fgMuted, cardBg, titleVariant, colorPreview, useColorPickerAlign, headerLeft: /* @__PURE__ */ jsx5(IconButton, { icon: "arrow-left", iconColor: fg, size: 24, onPress: handleBack, accessibilityLabel: "Back" }) }) });
631
+ return /* @__PURE__ */ jsx6(View4, { style: [styles4.container, { backgroundColor: bg }], children: /* @__PURE__ */ jsx6(ProfilesManager, { ref: managerRef, profiles, defaultColor, onCreate, onSave, onDelete, fg, fgMuted, titleVariant, colorPreview, useColorPickerAlign, headerLeft: /* @__PURE__ */ jsx6(IconButton, { icon: "arrow-left", iconColor: fg, size: 24, onPress: handleBack, accessibilityLabel: "Back" }) }) });
532
632
  }
533
633
  var styles4 = StyleSheet4.create({
534
634
  container: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tastic/profile",
3
- "version": "0.12.0",
3
+ "version": "0.14.0",
4
4
  "description": "Player profile identity for local-multiplayer React Native games: a saved name/color/tag a player selects per seat, with a picker, an inline-editable roster screen, and a reusable identity badge, deliberately blind to any app-specific fields (control scheme, stats) an app layers on top",
5
5
  "keywords": [
6
6
  "react-native",
@@ -65,13 +65,13 @@
65
65
  "@infinitetoken/eslint-config": "0.2.0",
66
66
  "@infinitetoken/jest-config": "^0.4.0",
67
67
  "@infinitetoken/tsconfig": "^0.5.0",
68
- "@rific/auto-paper": "^0.12.0",
69
- "@rific/core": "^0.2.0",
70
- "@rific/feedback-press": "^0.12.0",
71
- "@rific/focus-chain": "^0.4.7",
72
- "@rific/toaster": "^0.7.0",
73
- "@tastic/core": "^0.9.0",
74
- "@tastic/hud": "^0.12.0",
68
+ "@rific/auto-paper": "^0.13.2",
69
+ "@rific/core": "^0.3.1",
70
+ "@rific/feedback-press": "^0.13.0",
71
+ "@rific/focus-chain": "^0.4.8",
72
+ "@rific/toaster": "^0.8.0",
73
+ "@tastic/core": "^0.14.0",
74
+ "@tastic/hud": "^0.19.0",
75
75
  "@testing-library/dom": "^10.4.1",
76
76
  "@testing-library/react": "^16.3.2",
77
77
  "@types/jest": "^30.0.0",
@@ -85,14 +85,14 @@
85
85
  "prettier": "^3.8.3",
86
86
  "react": "^19.0.0",
87
87
  "react-dom": "^19.0.0",
88
- "react-native": "^0.85.3",
88
+ "react-native": "^0.86.3",
89
89
  "react-native-paper": "^5.15.2",
90
90
  "react-native-safe-area-context": "^5.0.0",
91
91
  "tsup": "^8.0.0",
92
92
  "typescript": "^6.0.3"
93
93
  },
94
94
  "peerDependencies": {
95
- "@rific/auto-paper": ">=0.9.0",
95
+ "@rific/auto-paper": ">=0.13.0",
96
96
  "@rific/feedback-press": ">=0.10.0",
97
97
  "@rific/focus-chain": ">=0.4.0",
98
98
  "@rific/toaster": ">=0.4.0",