@tastic/profile 0.13.0 → 0.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -3
- package/dist/index.d.mts +15 -4
- package/dist/index.d.ts +15 -4
- package/dist/index.js +236 -90
- package/dist/index.mjs +246 -100
- package/package.json +2 -2
- package/src/IdentityChip.tsx +58 -0
- package/src/ProfileChip.tsx +2 -37
- package/src/ProfilePicker.tsx +315 -71
- package/src/ProfilesManager.tsx +7 -1
- package/src/index.ts +1 -1
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/
|
|
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__ */
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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,24 @@ function ProfilePicker({ idPrefix, host, profiles, selectedId, takenId, color, d
|
|
|
87
115
|
host.close();
|
|
88
116
|
onManage?.();
|
|
89
117
|
};
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
+
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 }),
|
|
132
|
+
/* @__PURE__ */ jsx4(Icon2, { source: "menu-down", size: 16, color: chipTrigger ? fg : color })
|
|
94
133
|
] }) }),
|
|
95
|
-
/* @__PURE__ */
|
|
96
|
-
|
|
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,
|
|
97
136
|
{
|
|
98
137
|
style: [
|
|
99
138
|
styles2.menu,
|
|
@@ -104,39 +143,40 @@ function ProfilePicker({ idPrefix, host, profiles, selectedId, takenId, color, d
|
|
|
104
143
|
maxHeight
|
|
105
144
|
}
|
|
106
145
|
],
|
|
107
|
-
contentContainerStyle: styles2.menuContent,
|
|
108
|
-
showsVerticalScrollIndicator: false,
|
|
109
146
|
children: [
|
|
110
|
-
/* @__PURE__ */
|
|
111
|
-
/* @__PURE__ */
|
|
112
|
-
/* @__PURE__ */
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
/* @__PURE__ */
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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: [
|
|
148
|
+
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" })
|
|
140
180
|
] }) })
|
|
141
181
|
] })
|
|
142
182
|
]
|
|
@@ -144,6 +184,37 @@ function ProfilePicker({ idPrefix, host, profiles, selectedId, takenId, color, d
|
|
|
144
184
|
) })
|
|
145
185
|
] });
|
|
146
186
|
}
|
|
187
|
+
function ColorRow({ section, fg, dividerColor, onPress }) {
|
|
188
|
+
const { value, previewValue, label = "Color", caption } = section;
|
|
189
|
+
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
|
|
196
|
+
] }),
|
|
197
|
+
/* @__PURE__ */ jsx4(Icon2, { source: "chevron-right", size: 18, color: fg })
|
|
198
|
+
] }) }),
|
|
199
|
+
/* @__PURE__ */ jsx4(View2, { style: [styles2.divider, { backgroundColor: dividerColor }] })
|
|
200
|
+
] });
|
|
201
|
+
}
|
|
202
|
+
function ColorPage({ section, profileColor, fg, onBack }) {
|
|
203
|
+
const { value, onChange, previewValue, swatches = defaultColors, label = "Color" } = section;
|
|
204
|
+
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 })
|
|
209
|
+
] }) }),
|
|
210
|
+
/* @__PURE__ */ jsx4(View2, { style: styles2.swatches, children: swatches.map((swatch) => {
|
|
211
|
+
const selected = swatch.value.toLowerCase() === value.toLowerCase();
|
|
212
|
+
const isProfileColor = !!profileColor && swatch.value.toLowerCase() === profileColor.toLowerCase();
|
|
213
|
+
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);
|
|
215
|
+
}) })
|
|
216
|
+
] });
|
|
217
|
+
}
|
|
147
218
|
var styles2 = StyleSheet2.create({
|
|
148
219
|
anchor: {
|
|
149
220
|
position: "relative"
|
|
@@ -153,6 +224,40 @@ var styles2 = StyleSheet2.create({
|
|
|
153
224
|
anchorOpen: {
|
|
154
225
|
zIndex: 100
|
|
155
226
|
},
|
|
227
|
+
// The pill, rounded at both ends. The host adds its own background through `triggerStyle`.
|
|
228
|
+
chipTrigger: {
|
|
229
|
+
borderRadius: (CHIP_SIZE + 2 * CHIP_TRIGGER_PADDING) / 2
|
|
230
|
+
},
|
|
231
|
+
// The padding lives on the measured view, not the pressable around it, so the placement measures
|
|
232
|
+
// the pill's real edge: measured from inside the padding, a popover opening toward a screen edge
|
|
233
|
+
// was sized as if the pill were 6pt further from it, and ran 2pt past that edge.
|
|
234
|
+
chipTriggerInner: {
|
|
235
|
+
alignItems: "center",
|
|
236
|
+
flexDirection: "row",
|
|
237
|
+
gap: 8,
|
|
238
|
+
height: CHIP_SIZE + 2 * CHIP_TRIGGER_PADDING,
|
|
239
|
+
paddingLeft: CHIP_TRIGGER_PADDING,
|
|
240
|
+
paddingRight: CHIP_TRIGGER_PADDING + 8
|
|
241
|
+
},
|
|
242
|
+
colorRowCaption: {
|
|
243
|
+
fontSize: 12,
|
|
244
|
+
lineHeight: COLOR_CAPTION_LINE,
|
|
245
|
+
opacity: 0.6
|
|
246
|
+
},
|
|
247
|
+
// A row's label, with a line height so the captioned Color row's height is exact.
|
|
248
|
+
colorRowLabel: {
|
|
249
|
+
fontSize: 14,
|
|
250
|
+
lineHeight: COLOR_LABEL_LINE
|
|
251
|
+
},
|
|
252
|
+
// The color in effect, in the same 22pt slot as every row's chip.
|
|
253
|
+
colorRowSwatch: {
|
|
254
|
+
borderRadius: 11,
|
|
255
|
+
height: 22,
|
|
256
|
+
width: 22
|
|
257
|
+
},
|
|
258
|
+
colorRowText: {
|
|
259
|
+
flex: 1
|
|
260
|
+
},
|
|
156
261
|
// Separates Manage (a navigation action) from the selectable rows above it - different kind of
|
|
157
262
|
// action, not just another row in the same list. Tinted with the seat's own color (passed in as
|
|
158
263
|
// backgroundColor) rather than a flat gray, matching the border/caret's own accent-tinted look.
|
|
@@ -187,7 +292,7 @@ var styles2 = StyleSheet2.create({
|
|
|
187
292
|
},
|
|
188
293
|
menu: {
|
|
189
294
|
borderRadius: 12,
|
|
190
|
-
borderWidth:
|
|
295
|
+
borderWidth: MENU_BORDER_WIDTH,
|
|
191
296
|
elevation: 8,
|
|
192
297
|
shadowColor: "#000000",
|
|
193
298
|
shadowOffset: { height: 2, width: 0 },
|
|
@@ -200,6 +305,20 @@ var styles2 = StyleSheet2.create({
|
|
|
200
305
|
menuContent: {
|
|
201
306
|
padding: LIST_PADDING
|
|
202
307
|
},
|
|
308
|
+
// With Manage pinned below, its own padding takes the bottom edge (same total height as before).
|
|
309
|
+
menuContentAboveFooter: {
|
|
310
|
+
paddingBottom: 0
|
|
311
|
+
},
|
|
312
|
+
menuFooter: {
|
|
313
|
+
paddingBottom: LIST_PADDING,
|
|
314
|
+
paddingHorizontal: LIST_PADDING
|
|
315
|
+
},
|
|
316
|
+
// The selected profile's own color (see ColorSection).
|
|
317
|
+
profileDot: {
|
|
318
|
+
borderRadius: 4,
|
|
319
|
+
height: 8,
|
|
320
|
+
width: 8
|
|
321
|
+
},
|
|
203
322
|
row: {
|
|
204
323
|
borderRadius: 8,
|
|
205
324
|
height: ROW_HEIGHT
|
|
@@ -212,12 +331,33 @@ var styles2 = StyleSheet2.create({
|
|
|
212
331
|
flexDirection: "row",
|
|
213
332
|
flex: 1,
|
|
214
333
|
gap: 8,
|
|
215
|
-
paddingHorizontal:
|
|
334
|
+
paddingHorizontal: ROW_INSET
|
|
216
335
|
},
|
|
217
336
|
rowLabel: {
|
|
218
337
|
flex: 1,
|
|
219
338
|
fontSize: 14
|
|
220
339
|
},
|
|
340
|
+
swatch: {
|
|
341
|
+
alignItems: "center",
|
|
342
|
+
borderRadius: SWATCH_SIZE / 2,
|
|
343
|
+
height: SWATCH_SIZE,
|
|
344
|
+
justifyContent: "center",
|
|
345
|
+
width: SWATCH_SIZE
|
|
346
|
+
},
|
|
347
|
+
swatchSelected: {
|
|
348
|
+
borderColor: "#ffffff",
|
|
349
|
+
borderWidth: 2
|
|
350
|
+
},
|
|
351
|
+
swatches: {
|
|
352
|
+
columnGap: GRID_GAP,
|
|
353
|
+
flexDirection: "row",
|
|
354
|
+
flexWrap: "wrap",
|
|
355
|
+
marginBottom: GRID_BOTTOM,
|
|
356
|
+
marginHorizontal: ROW_INSET,
|
|
357
|
+
marginTop: GRID_TOP,
|
|
358
|
+
rowGap: GRID_GAP,
|
|
359
|
+
width: GRID_WIDTH
|
|
360
|
+
},
|
|
221
361
|
trigger: {
|
|
222
362
|
alignSelf: "flex-start"
|
|
223
363
|
},
|
|
@@ -235,13 +375,13 @@ var styles2 = StyleSheet2.create({
|
|
|
235
375
|
});
|
|
236
376
|
|
|
237
377
|
// src/ProfilesManager.tsx
|
|
238
|
-
import { defaultColors, useAutoPaperTheme } from "@rific/auto-paper";
|
|
378
|
+
import { defaultColors as defaultColors2, useAutoPaperTheme } from "@rific/auto-paper";
|
|
239
379
|
import { TouchableRipple as TouchableRipple2 } from "@rific/feedback-press";
|
|
240
380
|
import { useFocusChain } from "@rific/focus-chain";
|
|
241
381
|
import { useToast } from "@rific/toaster";
|
|
242
382
|
import { ConfirmDialog, getInlineColorPickerContentSize, InlineColorPicker, usePopoverHost } from "@tastic/hud";
|
|
243
|
-
import { useCallback, useEffect, useImperativeHandle, useMemo as useMemo2, useRef, useState } from "react";
|
|
244
|
-
import { KeyboardAvoidingView, Platform, ScrollView
|
|
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";
|
|
245
385
|
import { Icon as Icon3, Text as Text3, TextInput } from "react-native-paper";
|
|
246
386
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
247
387
|
|
|
@@ -278,8 +418,8 @@ function isValidProfile(value) {
|
|
|
278
418
|
}
|
|
279
419
|
|
|
280
420
|
// src/ProfilesManager.tsx
|
|
281
|
-
import { Fragment as Fragment2, jsx as
|
|
282
|
-
var
|
|
421
|
+
import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
422
|
+
var CHIP_SIZE2 = 40;
|
|
283
423
|
function deriveTag(name) {
|
|
284
424
|
const trimmed = name.trim();
|
|
285
425
|
return trimmed ? trimmed[0].toUpperCase() : "";
|
|
@@ -293,12 +433,12 @@ function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, h
|
|
|
293
433
|
const host = usePopoverHost();
|
|
294
434
|
const fg = fgOverride ?? (dark ? "#FFFFFF" : "#000000");
|
|
295
435
|
const fgMuted = fgMutedOverride ?? (dark ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.5)");
|
|
296
|
-
const [editingId, setEditingId] =
|
|
297
|
-
const [draftName, setDraftName] =
|
|
298
|
-
const [draftColor, setDraftColor] =
|
|
299
|
-
const [draftTag, setDraftTag] =
|
|
300
|
-
const [tagManuallySet, setTagManuallySet] =
|
|
301
|
-
const [confirmDeleteId, setConfirmDeleteId] =
|
|
436
|
+
const [editingId, setEditingId] = useState2(null);
|
|
437
|
+
const [draftName, setDraftName] = useState2("");
|
|
438
|
+
const [draftColor, setDraftColor] = useState2(defaultColor);
|
|
439
|
+
const [draftTag, setDraftTag] = useState2("");
|
|
440
|
+
const [tagManuallySet, setTagManuallySet] = useState2(false);
|
|
441
|
+
const [confirmDeleteId, setConfirmDeleteId] = useState2(null);
|
|
302
442
|
const commitEdit = useCallback(() => {
|
|
303
443
|
const name = draftName.trim();
|
|
304
444
|
if (editingId && name) {
|
|
@@ -358,35 +498,35 @@ function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, h
|
|
|
358
498
|
return /* @__PURE__ */ jsxs2(Fragment2, { children: [
|
|
359
499
|
/* @__PURE__ */ jsxs2(View3, { style: [styles3.header, { paddingTop: 8 + insets.top, paddingLeft: 8 + insets.left }], children: [
|
|
360
500
|
headerLeft,
|
|
361
|
-
/* @__PURE__ */
|
|
501
|
+
/* @__PURE__ */ jsx5(Text3, { variant: titleVariant, style: [styles3.title, { color: fg }], children: "Profiles" })
|
|
362
502
|
] }),
|
|
363
|
-
/* @__PURE__ */
|
|
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) => {
|
|
364
504
|
if (row.kind === "new") {
|
|
365
505
|
if (editingId !== "new") {
|
|
366
|
-
return /* @__PURE__ */
|
|
367
|
-
/* @__PURE__ */
|
|
368
|
-
/* @__PURE__ */
|
|
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" })
|
|
369
509
|
] }) }, "new");
|
|
370
510
|
}
|
|
371
|
-
return /* @__PURE__ */
|
|
511
|
+
return /* @__PURE__ */ jsx5(EditRow, { draftName, onNameChange: handleNameChange, draftColor, onColorChange: setDraftColor, draftTag, onTagChange: handleTagChange, onSubmit: commitEdit, autoFocus: true, fgMuted, host, dark, colorPreview, useColorPickerAlign }, "new");
|
|
372
512
|
}
|
|
373
513
|
const { profile } = row;
|
|
374
514
|
if (editingId === profile.id) {
|
|
375
|
-
return /* @__PURE__ */
|
|
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);
|
|
376
516
|
}
|
|
377
|
-
return /* @__PURE__ */
|
|
378
|
-
/* @__PURE__ */
|
|
379
|
-
/* @__PURE__ */
|
|
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 })
|
|
380
520
|
] }) }, profile.id);
|
|
381
521
|
}) }) }),
|
|
382
|
-
/* @__PURE__ */
|
|
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) })
|
|
383
523
|
] });
|
|
384
524
|
}
|
|
385
525
|
function AlignedColorPicker({ useAlign, ...props }) {
|
|
386
526
|
const { width: windowWidth } = useWindowDimensions();
|
|
387
|
-
const { width, height } = getInlineColorPickerContentSize(
|
|
527
|
+
const { width, height } = getInlineColorPickerContentSize(defaultColors2.length, windowWidth);
|
|
388
528
|
const alignOverride = useAlign(props.host.openId === props.id, width, height);
|
|
389
|
-
return /* @__PURE__ */
|
|
529
|
+
return /* @__PURE__ */ jsx5(InlineColorPicker, { ...props, alignOverride });
|
|
390
530
|
}
|
|
391
531
|
function EditRow({ draftName, onNameChange, draftColor, onColorChange, draftTag, onTagChange, onSubmit, onDelete, autoFocus, fgMuted, host, dark, colorPreview, useColorPickerAlign }) {
|
|
392
532
|
const register = useFocusChain();
|
|
@@ -400,10 +540,10 @@ function EditRow({ draftName, onNameChange, draftColor, onColorChange, draftTag,
|
|
|
400
540
|
if (host.openId === "color") tagFocusRef.current();
|
|
401
541
|
}, [host.openId]);
|
|
402
542
|
return /* @__PURE__ */ jsxs2(View3, { style: styles3.editRow, children: [
|
|
403
|
-
useColorPickerAlign ? /* @__PURE__ */
|
|
404
|
-
/* @__PURE__ */
|
|
405
|
-
/* @__PURE__ */
|
|
406
|
-
onDelete && /* @__PURE__ */
|
|
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 }) })
|
|
407
547
|
] });
|
|
408
548
|
}
|
|
409
549
|
var styles3 = StyleSheet3.create({
|
|
@@ -411,11 +551,17 @@ var styles3 = StyleSheet3.create({
|
|
|
411
551
|
gap: 4,
|
|
412
552
|
paddingHorizontal: 20
|
|
413
553
|
},
|
|
554
|
+
// zIndex: the color popover lives inside this row, and a zIndex only orders siblings of one parent
|
|
555
|
+
// (React Native Web also gives every positioned view its own stacking context), so the popover's
|
|
556
|
+
// own zIndex never reached the rows after this one: they were drawn over it (a Kid row and the New
|
|
557
|
+
// Profile row across the swatches, 2026-10-16). Raising the row lifts the popover with it. There
|
|
558
|
+
// is only ever one edit row.
|
|
414
559
|
editRow: {
|
|
415
560
|
alignItems: "center",
|
|
416
561
|
flexDirection: "row",
|
|
417
562
|
gap: 8,
|
|
418
|
-
paddingVertical: 6
|
|
563
|
+
paddingVertical: 6,
|
|
564
|
+
zIndex: 1
|
|
419
565
|
},
|
|
420
566
|
header: {
|
|
421
567
|
alignItems: "center",
|
|
@@ -456,10 +602,10 @@ var styles3 = StyleSheet3.create({
|
|
|
456
602
|
},
|
|
457
603
|
rowChip: {
|
|
458
604
|
alignItems: "center",
|
|
459
|
-
borderRadius:
|
|
460
|
-
height:
|
|
605
|
+
borderRadius: CHIP_SIZE2 / 2,
|
|
606
|
+
height: CHIP_SIZE2,
|
|
461
607
|
justifyContent: "center",
|
|
462
|
-
width:
|
|
608
|
+
width: CHIP_SIZE2
|
|
463
609
|
},
|
|
464
610
|
rowInner: {
|
|
465
611
|
alignItems: "center",
|
|
@@ -483,7 +629,7 @@ import { useAutoPaperTheme as useAutoPaperTheme2 } from "@rific/auto-paper";
|
|
|
483
629
|
import { IconButton } from "@rific/feedback-press";
|
|
484
630
|
import { useRef as useRef2 } from "react";
|
|
485
631
|
import { StyleSheet as StyleSheet4, View as View4 } from "react-native";
|
|
486
|
-
import { jsx as
|
|
632
|
+
import { jsx as jsx6 } from "react/jsx-runtime";
|
|
487
633
|
function ProfilesScreen({ profiles, defaultColor, onCreate, onSave, onDelete, onBack, fg: fgOverride, fgMuted, bg: bgOverride, titleVariant, colorPreview, useColorPickerAlign }) {
|
|
488
634
|
const { dark } = useAutoPaperTheme2();
|
|
489
635
|
const managerRef = useRef2(null);
|
|
@@ -493,7 +639,7 @@ function ProfilesScreen({ profiles, defaultColor, onCreate, onSave, onDelete, on
|
|
|
493
639
|
managerRef.current?.commitPendingEdit();
|
|
494
640
|
onBack();
|
|
495
641
|
};
|
|
496
|
-
return /* @__PURE__ */
|
|
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" }) }) });
|
|
497
643
|
}
|
|
498
644
|
var styles4 = StyleSheet4.create({
|
|
499
645
|
container: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tastic/profile",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.1",
|
|
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",
|
|
@@ -92,7 +92,7 @@
|
|
|
92
92
|
"typescript": "^6.0.3"
|
|
93
93
|
},
|
|
94
94
|
"peerDependencies": {
|
|
95
|
-
"@rific/auto-paper": ">=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",
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { getContrastColor } from '@rific/auto-paper'
|
|
2
|
+
import { StyleSheet, View } from 'react-native'
|
|
3
|
+
import { Icon, Text } from 'react-native-paper'
|
|
4
|
+
|
|
5
|
+
// Presence check, not isValidTag's own full-string emoji validation (profilesValidation.ts) - this
|
|
6
|
+
// only decides a font-size ratio, so it doesn't need that check's ZWJ/variation-selector precision,
|
|
7
|
+
// just "does this tag contain a pictograph at all."
|
|
8
|
+
const EMOJI_PATTERN = /\p{Extended_Pictographic}/u
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
// The color drawn, exactly as given (no ProfileColorProvider transform: the caller has already
|
|
12
|
+
// decided what is drawn).
|
|
13
|
+
color: string
|
|
14
|
+
// Shown when non-empty, else `icon`.
|
|
15
|
+
tag?: string
|
|
16
|
+
icon: string
|
|
17
|
+
size: number
|
|
18
|
+
// See ProfileChip's `filled`.
|
|
19
|
+
filled?: boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// A filled circle with a tag (or an icon) in a contrast-safe color on top: the drawing behind
|
|
23
|
+
// ProfileChip (a profile's own colors) and ProfilePicker's chip trigger (the live color in effect,
|
|
24
|
+
// which may not be any profile's saved one).
|
|
25
|
+
export function IdentityChip({ color, tag, icon, size, filled = true }: Props) {
|
|
26
|
+
const contrastColor = getContrastColor(color)
|
|
27
|
+
// An emoji glyph reads visually smaller than a bold letter at the same fontSize (the system emoji
|
|
28
|
+
// font leaves more of its own em-box empty), so a plain-text ratio that looks right for "J" still
|
|
29
|
+
// looks small for "🎮" at the identical size - this bumps emoji up to the icon's own ratio instead,
|
|
30
|
+
// since a single emoji is closer in visual weight to an icon glyph than to a line of text.
|
|
31
|
+
//
|
|
32
|
+
// Three letters don't fit across the circle at 0.4 in a wide bold face (DAD in Solitaire's DejaVu
|
|
33
|
+
// Serif Bold is a whole chip wide), and the web build ignores adjustsFontSizeToFit, so it cut
|
|
34
|
+
// them off there ("D..."); 0.32 fits them, with adjustsFontSizeToFit still the backstop on native.
|
|
35
|
+
const tagFontSize = size * (tag && EMOJI_PATTERN.test(tag) ? 0.6 : tag && Array.from(tag).length >= 3 ? 0.32 : 0.4)
|
|
36
|
+
return (
|
|
37
|
+
<View style={[styles.chip, { borderRadius: size / 2, height: size, width: size }, filled && { backgroundColor: color }]}>
|
|
38
|
+
{tag ? (
|
|
39
|
+
<Text style={[styles.tag, { color: contrastColor, fontSize: tagFontSize }]} numberOfLines={1} adjustsFontSizeToFit>
|
|
40
|
+
{tag}
|
|
41
|
+
</Text>
|
|
42
|
+
) : (
|
|
43
|
+
<Icon source={icon} size={size * 0.6} color={contrastColor} />
|
|
44
|
+
)}
|
|
45
|
+
</View>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const styles = StyleSheet.create({
|
|
50
|
+
chip: {
|
|
51
|
+
alignItems: 'center',
|
|
52
|
+
justifyContent: 'center'
|
|
53
|
+
},
|
|
54
|
+
tag: {
|
|
55
|
+
fontWeight: '700',
|
|
56
|
+
paddingHorizontal: 2
|
|
57
|
+
}
|
|
58
|
+
})
|