@tastic/profile 0.1.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/README.md +81 -0
- package/dist/index.d.mts +58 -0
- package/dist/index.d.ts +58 -0
- package/dist/index.js +515 -0
- package/dist/index.mjs +482 -0
- package/package.json +107 -0
- package/src/ProfileChip.tsx +60 -0
- package/src/ProfilePicker.tsx +275 -0
- package/src/ProfilesManager.tsx +417 -0
- package/src/index.ts +5 -0
- package/src/profilesValidation.ts +42 -0
- package/src/types.ts +16 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
MAX_PROFILE_NAME_LENGTH: () => MAX_PROFILE_NAME_LENGTH,
|
|
24
|
+
MAX_TAG_LENGTH: () => MAX_TAG_LENGTH,
|
|
25
|
+
ProfileChip: () => ProfileChip,
|
|
26
|
+
ProfilePicker: () => ProfilePicker,
|
|
27
|
+
ProfilesManager: () => ProfilesManager,
|
|
28
|
+
isValidProfile: () => isValidProfile,
|
|
29
|
+
isValidTag: () => isValidTag
|
|
30
|
+
});
|
|
31
|
+
module.exports = __toCommonJS(index_exports);
|
|
32
|
+
|
|
33
|
+
// src/ProfileChip.tsx
|
|
34
|
+
var import_auto_paper = require("@rific/auto-paper");
|
|
35
|
+
var import_react_native = require("react-native");
|
|
36
|
+
var import_react_native_paper = require("react-native-paper");
|
|
37
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
38
|
+
var EMOJI_PATTERN = /\p{Extended_Pictographic}/u;
|
|
39
|
+
function ProfileChip({ profile, size = 22, filled = true }) {
|
|
40
|
+
const contrastColor = (0, import_auto_paper.getContrastColor)(profile.color);
|
|
41
|
+
const tagFontSize = size * (profile.tag && EMOJI_PATTERN.test(profile.tag) ? 0.6 : 0.4);
|
|
42
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native.View, { style: [styles.chip, { borderRadius: size / 2, height: size, width: size }, filled && { backgroundColor: profile.color }], children: profile.tag ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native_paper.Text, { style: [styles.tag, { color: contrastColor, fontSize: tagFontSize }], numberOfLines: 1, adjustsFontSizeToFit: true, children: profile.tag }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native_paper.Icon, { source: "account", size: size * 0.6, color: contrastColor }) });
|
|
43
|
+
}
|
|
44
|
+
var styles = import_react_native.StyleSheet.create({
|
|
45
|
+
chip: {
|
|
46
|
+
alignItems: "center",
|
|
47
|
+
justifyContent: "center"
|
|
48
|
+
},
|
|
49
|
+
tag: {
|
|
50
|
+
fontWeight: "700",
|
|
51
|
+
paddingHorizontal: 2
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// src/ProfilePicker.tsx
|
|
56
|
+
var import_auto_paper2 = require("@rific/auto-paper");
|
|
57
|
+
var import_feedback_press = require("@rific/feedback-press");
|
|
58
|
+
var import_hud = require("@tastic/hud");
|
|
59
|
+
var import_react = require("react");
|
|
60
|
+
var import_react_native2 = require("react-native");
|
|
61
|
+
var import_react_native_paper2 = require("react-native-paper");
|
|
62
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
63
|
+
var TRIGGER_HEIGHT = 28;
|
|
64
|
+
var POPOVER_WIDTH = 220;
|
|
65
|
+
var ROW_HEIGHT = 36;
|
|
66
|
+
var LIST_PADDING = 8;
|
|
67
|
+
function ProfilePicker({ idPrefix, host, profiles, selectedId, takenId, color, dark, align: alignOverride, guestLabel, nullLabel = "Player", nullIcon = "account-off-outline", onSelect, onManage }) {
|
|
68
|
+
const id = `${idPrefix}-profile`;
|
|
69
|
+
const menuBg = dark ? "#000000" : "#FFFFFF";
|
|
70
|
+
const fg = dark ? "#FFFFFF" : "#000000";
|
|
71
|
+
const open = host.openId === id;
|
|
72
|
+
const selectedProfile = profiles.find((p) => p.id === selectedId) ?? null;
|
|
73
|
+
const sortedProfiles = (0, import_react.useMemo)(() => [...profiles].sort((a, b) => a.name.localeCompare(b.name)), [profiles]);
|
|
74
|
+
const rowCount = profiles.length + 1 + (onManage ? 1 : 0);
|
|
75
|
+
const contentHeight = LIST_PADDING * 2 + rowCount * ROW_HEIGHT;
|
|
76
|
+
const { align: autoAlign, maxHeight, measured, triggerRef, verticalAlign } = (0, import_hud.useAutoAlign)(open, POPOVER_WIDTH, contentHeight);
|
|
77
|
+
const align = alignOverride ?? autoAlign;
|
|
78
|
+
const handleSelectGuest = () => {
|
|
79
|
+
onSelect(null);
|
|
80
|
+
host.close();
|
|
81
|
+
};
|
|
82
|
+
const handleSelectExisting = (profile) => {
|
|
83
|
+
if (profile.id === takenId) return;
|
|
84
|
+
onSelect(profile);
|
|
85
|
+
host.close();
|
|
86
|
+
};
|
|
87
|
+
const handleManage = () => {
|
|
88
|
+
host.close();
|
|
89
|
+
onManage?.();
|
|
90
|
+
};
|
|
91
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react_native2.View, { style: [styles2.anchor, open && styles2.anchorOpen], children: [
|
|
92
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_feedback_press.TouchableRipple, { onPress: () => host.toggle(id), style: styles2.trigger, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react_native2.View, { ref: triggerRef, collapsable: false, style: styles2.triggerInner, children: [
|
|
93
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native_paper2.Text, { style: [styles2.triggerLabel, { color }], numberOfLines: 1, children: (selectedProfile?.name ?? guestLabel).toUpperCase() }),
|
|
94
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native_paper2.Icon, { source: "menu-down", size: 16, color })
|
|
95
|
+
] }) }),
|
|
96
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_hud.PopoverBody, { visible: open && measured, align, verticalAlign, caretColor: menuBg, caretBorderColor: color, caretBorderWidth: 2, triggerSize: TRIGGER_HEIGHT, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
97
|
+
import_react_native2.ScrollView,
|
|
98
|
+
{
|
|
99
|
+
style: [
|
|
100
|
+
styles2.menu,
|
|
101
|
+
{
|
|
102
|
+
backgroundColor: menuBg,
|
|
103
|
+
borderColor: color,
|
|
104
|
+
width: POPOVER_WIDTH,
|
|
105
|
+
maxHeight
|
|
106
|
+
}
|
|
107
|
+
],
|
|
108
|
+
contentContainerStyle: styles2.menuContent,
|
|
109
|
+
showsVerticalScrollIndicator: false,
|
|
110
|
+
children: [
|
|
111
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_feedback_press.TouchableRipple, { onPress: handleSelectGuest, style: [styles2.row, selectedId === null && { backgroundColor: color }], children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react_native2.View, { style: styles2.rowInner, children: [
|
|
112
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native2.View, { style: styles2.guestChip, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native_paper2.Icon, { source: nullIcon, size: 13, color: selectedId === null ? (0, import_auto_paper2.getContrastColor)(color) : fg }) }),
|
|
113
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native_paper2.Text, { style: [styles2.rowLabel, { color: selectedId === null ? (0, import_auto_paper2.getContrastColor)(color) : fg }], children: nullLabel })
|
|
114
|
+
] }) }),
|
|
115
|
+
sortedProfiles.map((profile) => {
|
|
116
|
+
const taken = profile.id === takenId;
|
|
117
|
+
const selected = selectedId === profile.id;
|
|
118
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_feedback_press.TouchableRipple, { disabled: taken, onPress: () => handleSelectExisting(profile), style: [styles2.row, selected && { backgroundColor: profile.color }], children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react_native2.View, { style: [styles2.rowInner, taken && styles2.rowDisabled], children: [
|
|
119
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ProfileChip, { profile, filled: !selected }),
|
|
120
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
121
|
+
import_react_native_paper2.Text,
|
|
122
|
+
{
|
|
123
|
+
style: [
|
|
124
|
+
styles2.rowLabel,
|
|
125
|
+
{
|
|
126
|
+
color: selected ? (0, import_auto_paper2.getContrastColor)(profile.color) : fg
|
|
127
|
+
}
|
|
128
|
+
],
|
|
129
|
+
numberOfLines: 1,
|
|
130
|
+
children: profile.name
|
|
131
|
+
}
|
|
132
|
+
)
|
|
133
|
+
] }) }, profile.id);
|
|
134
|
+
}),
|
|
135
|
+
onManage && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
|
|
136
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native2.View, { style: [styles2.divider, { backgroundColor: color }] }),
|
|
137
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_feedback_press.TouchableRipple, { onPress: handleManage, style: styles2.row, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react_native2.View, { style: styles2.manageRowInner, children: [
|
|
138
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native_paper2.Icon, { source: "account-cog-outline", size: 16, color }),
|
|
139
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native_paper2.Text, { style: [styles2.manageLabel, { color }], children: "Manage" })
|
|
140
|
+
] }) })
|
|
141
|
+
] })
|
|
142
|
+
]
|
|
143
|
+
}
|
|
144
|
+
) })
|
|
145
|
+
] });
|
|
146
|
+
}
|
|
147
|
+
var styles2 = import_react_native2.StyleSheet.create({
|
|
148
|
+
anchor: {
|
|
149
|
+
position: "relative"
|
|
150
|
+
},
|
|
151
|
+
// See InlineColorPicker's identical comment — React Native Web gives every position:'relative'
|
|
152
|
+
// view its own stacking context, so the elevation has to live on the anchor itself.
|
|
153
|
+
anchorOpen: {
|
|
154
|
+
zIndex: 100
|
|
155
|
+
},
|
|
156
|
+
// Separates Manage (a navigation action) from the selectable rows above it — different kind of
|
|
157
|
+
// action, not just another row in the same list. Tinted with the seat's own color (passed in as
|
|
158
|
+
// backgroundColor) rather than a flat gray, matching the border/caret's own accent-tinted look.
|
|
159
|
+
divider: {
|
|
160
|
+
height: 1,
|
|
161
|
+
marginVertical: 4,
|
|
162
|
+
opacity: 0.25
|
|
163
|
+
},
|
|
164
|
+
// Matches ProfileChip's own default size/shape exactly (see this row's own render-site comment)
|
|
165
|
+
// — no backgroundColor, unlike ProfileChip's own default: this row has no identity color of its
|
|
166
|
+
// own to fill it with, and the render site now colors the icon directly instead.
|
|
167
|
+
guestChip: {
|
|
168
|
+
alignItems: "center",
|
|
169
|
+
borderRadius: 11,
|
|
170
|
+
height: 22,
|
|
171
|
+
justifyContent: "center",
|
|
172
|
+
width: 22
|
|
173
|
+
},
|
|
174
|
+
manageLabel: {
|
|
175
|
+
fontSize: 13,
|
|
176
|
+
fontWeight: "700",
|
|
177
|
+
letterSpacing: 0.5
|
|
178
|
+
},
|
|
179
|
+
// Centered rather than left-aligned like a normal row — see this row's own render-site comment
|
|
180
|
+
// for why (a footer action, not another selectable identity).
|
|
181
|
+
manageRowInner: {
|
|
182
|
+
alignItems: "center",
|
|
183
|
+
flexDirection: "row",
|
|
184
|
+
gap: 6,
|
|
185
|
+
height: "100%",
|
|
186
|
+
justifyContent: "center"
|
|
187
|
+
},
|
|
188
|
+
menu: {
|
|
189
|
+
borderRadius: 12,
|
|
190
|
+
borderWidth: 2,
|
|
191
|
+
elevation: 8,
|
|
192
|
+
shadowColor: "#000000",
|
|
193
|
+
shadowOffset: { height: 2, width: 0 },
|
|
194
|
+
shadowOpacity: 0.3,
|
|
195
|
+
shadowRadius: 8
|
|
196
|
+
},
|
|
197
|
+
// Padded on every side (not just top/bottom) — a selected row's own full-color background (see
|
|
198
|
+
// `row`'s borderRadius) needs real margin from the menu's own straight edges to read as a rounded
|
|
199
|
+
// pill instead of looking clipped against them.
|
|
200
|
+
menuContent: {
|
|
201
|
+
padding: LIST_PADDING
|
|
202
|
+
},
|
|
203
|
+
row: {
|
|
204
|
+
borderRadius: 8,
|
|
205
|
+
height: ROW_HEIGHT
|
|
206
|
+
},
|
|
207
|
+
rowDisabled: {
|
|
208
|
+
opacity: 0.35
|
|
209
|
+
},
|
|
210
|
+
rowInner: {
|
|
211
|
+
alignItems: "center",
|
|
212
|
+
flexDirection: "row",
|
|
213
|
+
flex: 1,
|
|
214
|
+
gap: 8,
|
|
215
|
+
paddingHorizontal: 12
|
|
216
|
+
},
|
|
217
|
+
rowLabel: {
|
|
218
|
+
flex: 1,
|
|
219
|
+
fontSize: 14
|
|
220
|
+
},
|
|
221
|
+
trigger: {
|
|
222
|
+
alignSelf: "flex-start"
|
|
223
|
+
},
|
|
224
|
+
triggerInner: {
|
|
225
|
+
alignItems: "center",
|
|
226
|
+
flexDirection: "row",
|
|
227
|
+
gap: 2,
|
|
228
|
+
height: TRIGGER_HEIGHT
|
|
229
|
+
},
|
|
230
|
+
triggerLabel: {
|
|
231
|
+
fontSize: 13,
|
|
232
|
+
fontWeight: "700",
|
|
233
|
+
letterSpacing: 1
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// src/ProfilesManager.tsx
|
|
238
|
+
var import_auto_paper3 = require("@rific/auto-paper");
|
|
239
|
+
var import_feedback_press2 = require("@rific/feedback-press");
|
|
240
|
+
var import_focus_chain = require("@rific/focus-chain");
|
|
241
|
+
var import_toaster = require("@rific/toaster");
|
|
242
|
+
var import_hud2 = require("@tastic/hud");
|
|
243
|
+
var import_react2 = require("react");
|
|
244
|
+
var import_react_native3 = require("react-native");
|
|
245
|
+
var import_react_native_paper3 = require("react-native-paper");
|
|
246
|
+
var import_react_native_safe_area_context = require("react-native-safe-area-context");
|
|
247
|
+
|
|
248
|
+
// src/profilesValidation.ts
|
|
249
|
+
var MAX_PROFILE_NAME_LENGTH = 20;
|
|
250
|
+
var MAX_TAG_LENGTH = 3;
|
|
251
|
+
function isValidHexColor(value) {
|
|
252
|
+
return typeof value === "string" && /^#[0-9a-f]{6}$/i.test(value);
|
|
253
|
+
}
|
|
254
|
+
var VARIATION_SELECTOR = String.fromCodePoint(65039);
|
|
255
|
+
var ZERO_WIDTH_JOINER = String.fromCodePoint(8205);
|
|
256
|
+
var EMOJI_PATTERN2 = new RegExp(`^\\p{Extended_Pictographic}${VARIATION_SELECTOR}?(${ZERO_WIDTH_JOINER}\\p{Extended_Pictographic}${VARIATION_SELECTOR}?)*$`, "u");
|
|
257
|
+
function isValidTag(value) {
|
|
258
|
+
if (typeof value !== "string") return false;
|
|
259
|
+
if (value.length === 0) return true;
|
|
260
|
+
if (EMOJI_PATTERN2.test(value)) return true;
|
|
261
|
+
return !/\p{Extended_Pictographic}/u.test(value) && Array.from(value).length <= MAX_TAG_LENGTH;
|
|
262
|
+
}
|
|
263
|
+
function isValidProfile(value) {
|
|
264
|
+
if (!value || typeof value !== "object") return false;
|
|
265
|
+
const v = value;
|
|
266
|
+
return typeof v.id === "string" && v.id.length > 0 && typeof v.name === "string" && v.name.trim().length > 0 && v.name.length <= MAX_PROFILE_NAME_LENGTH && isValidHexColor(v.color) && isValidTag(v.tag) && typeof v.createdAt === "number" && typeof v.updatedAt === "number";
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// src/ProfilesManager.tsx
|
|
270
|
+
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
271
|
+
var CHIP_SIZE = 40;
|
|
272
|
+
function deriveTag(name) {
|
|
273
|
+
const trimmed = name.trim();
|
|
274
|
+
return trimmed ? trimmed[0].toUpperCase() : "";
|
|
275
|
+
}
|
|
276
|
+
function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, headerLeft }) {
|
|
277
|
+
const { dark, colors } = (0, import_auto_paper3.useAutoPaperTheme)();
|
|
278
|
+
const insets = (0, import_react_native_safe_area_context.useSafeAreaInsets)();
|
|
279
|
+
const { error: showErrorToast } = (0, import_toaster.useToast)();
|
|
280
|
+
const host = (0, import_hud2.usePopoverHost)();
|
|
281
|
+
const fg = dark ? "#FFFFFF" : "#000000";
|
|
282
|
+
const fgMuted = dark ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.5)";
|
|
283
|
+
const cardBg = dark ? "#111111" : "#F2F2F2";
|
|
284
|
+
const cardBorder = dark ? "rgba(255,255,255,0.2)" : "rgba(0,0,0,0.2)";
|
|
285
|
+
const cancelBg = dark ? "#2A2A2A" : "#E0E0E0";
|
|
286
|
+
const cancelFg = dark ? "#FFFFFF" : "#000000";
|
|
287
|
+
const [editingId, setEditingId] = (0, import_react2.useState)(null);
|
|
288
|
+
const [draftName, setDraftName] = (0, import_react2.useState)("");
|
|
289
|
+
const [draftColor, setDraftColor] = (0, import_react2.useState)(defaultColor);
|
|
290
|
+
const [draftTag, setDraftTag] = (0, import_react2.useState)("");
|
|
291
|
+
const [tagManuallySet, setTagManuallySet] = (0, import_react2.useState)(false);
|
|
292
|
+
const [confirmDeleteId, setConfirmDeleteId] = (0, import_react2.useState)(null);
|
|
293
|
+
const commitEdit = (0, import_react2.useCallback)(() => {
|
|
294
|
+
const name = draftName.trim();
|
|
295
|
+
if (editingId && name) {
|
|
296
|
+
const tagValid = isValidTag(draftTag);
|
|
297
|
+
if (draftTag && !tagValid) showErrorToast("Invalid tag", "Use up to 3 letters or a single emoji");
|
|
298
|
+
const tag = tagValid ? draftTag : "";
|
|
299
|
+
if (editingId === "new") onCreate({ color: draftColor, name, tag });
|
|
300
|
+
else onSave(editingId, { color: draftColor, name, tag });
|
|
301
|
+
}
|
|
302
|
+
setEditingId(null);
|
|
303
|
+
}, [editingId, draftName, draftColor, draftTag, onCreate, onSave, showErrorToast]);
|
|
304
|
+
const commitEditRef = (0, import_react2.useRef)(commitEdit);
|
|
305
|
+
(0, import_react2.useEffect)(() => {
|
|
306
|
+
commitEditRef.current = commitEdit;
|
|
307
|
+
});
|
|
308
|
+
(0, import_react2.useEffect)(() => () => commitEditRef.current(), []);
|
|
309
|
+
const startEdit = (0, import_react2.useCallback)(
|
|
310
|
+
(target) => {
|
|
311
|
+
commitEdit();
|
|
312
|
+
if (target === "new") {
|
|
313
|
+
setDraftName("");
|
|
314
|
+
setDraftColor(defaultColor);
|
|
315
|
+
setDraftTag("");
|
|
316
|
+
setTagManuallySet(false);
|
|
317
|
+
} else {
|
|
318
|
+
setDraftName(target.name);
|
|
319
|
+
setDraftColor(target.color);
|
|
320
|
+
setDraftTag(target.tag);
|
|
321
|
+
setTagManuallySet(target.tag !== "");
|
|
322
|
+
}
|
|
323
|
+
setEditingId(target === "new" ? "new" : target.id);
|
|
324
|
+
},
|
|
325
|
+
[commitEdit, defaultColor]
|
|
326
|
+
);
|
|
327
|
+
const handleNameChange = (0, import_react2.useCallback)(
|
|
328
|
+
(name) => {
|
|
329
|
+
setDraftName(name);
|
|
330
|
+
if (!tagManuallySet) setDraftTag(deriveTag(name));
|
|
331
|
+
},
|
|
332
|
+
[tagManuallySet]
|
|
333
|
+
);
|
|
334
|
+
const handleTagChange = (0, import_react2.useCallback)((tag) => {
|
|
335
|
+
setDraftTag(tag);
|
|
336
|
+
setTagManuallySet(true);
|
|
337
|
+
}, []);
|
|
338
|
+
const confirmDeleteProfile = profiles.find((p) => p.id === confirmDeleteId) ?? null;
|
|
339
|
+
const handleConfirmDelete = (0, import_react2.useCallback)(() => {
|
|
340
|
+
if (confirmDeleteId) {
|
|
341
|
+
onDelete(confirmDeleteId);
|
|
342
|
+
if (editingId === confirmDeleteId) setEditingId(null);
|
|
343
|
+
}
|
|
344
|
+
setConfirmDeleteId(null);
|
|
345
|
+
}, [confirmDeleteId, editingId, onDelete]);
|
|
346
|
+
const sortedProfiles = (0, import_react2.useMemo)(() => [...profiles].sort((a, b) => a.name.localeCompare(b.name)), [profiles]);
|
|
347
|
+
const rows = [...sortedProfiles.map((profile) => ({ kind: "profile", profile })), { kind: "new" }];
|
|
348
|
+
const tagInvalid = draftTag.length > 0 && !isValidTag(draftTag);
|
|
349
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
|
|
350
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_react_native3.View, { style: [styles3.header, { paddingTop: 8 + insets.top, paddingLeft: 8 + insets.left }], children: [
|
|
351
|
+
headerLeft,
|
|
352
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native_paper3.Text, { variant: "displaySmall", style: [styles3.title, { color: fg }], children: "Profiles" })
|
|
353
|
+
] }),
|
|
354
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native3.KeyboardAvoidingView, { behavior: import_react_native3.Platform.OS === "ios" ? "padding" : void 0, style: styles3.keyboardAvoiding, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native3.ScrollView, { style: styles3.scrollView, contentContainerStyle: [styles3.content, { paddingBottom: 32 + insets.bottom }], showsVerticalScrollIndicator: false, keyboardShouldPersistTaps: "handled", children: rows.map((row) => {
|
|
355
|
+
if (row.kind === "new") {
|
|
356
|
+
if (editingId !== "new") {
|
|
357
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_feedback_press2.TouchableRipple, { onPress: () => startEdit("new"), style: styles3.row, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_react_native3.View, { style: styles3.rowInner, children: [
|
|
358
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native3.View, { style: [styles3.rowChip, styles3.newChip, { borderColor: fg }], children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native_paper3.Icon, { source: "plus", size: 20, color: fg }) }),
|
|
359
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native_paper3.Text, { variant: "bodyLarge", style: { color: fg }, children: "New Profile" })
|
|
360
|
+
] }) }, "new");
|
|
361
|
+
}
|
|
362
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(EditRow, { draftName, onNameChange: handleNameChange, draftColor, onColorChange: setDraftColor, draftTag, onTagChange: handleTagChange, tagInvalid, onSubmit: commitEdit, autoFocus: true, fgMuted, host, dark }, "new");
|
|
363
|
+
}
|
|
364
|
+
const { profile } = row;
|
|
365
|
+
if (editingId === profile.id) {
|
|
366
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(EditRow, { draftName, onNameChange: handleNameChange, draftColor, onColorChange: setDraftColor, draftTag, onTagChange: handleTagChange, tagInvalid, onSubmit: commitEdit, onDelete: () => setConfirmDeleteId(profile.id), fgMuted, host, dark }, profile.id);
|
|
367
|
+
}
|
|
368
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_feedback_press2.TouchableRipple, { onPress: () => startEdit(profile), style: styles3.row, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_react_native3.View, { style: styles3.rowInner, children: [
|
|
369
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ProfileChip, { profile, size: CHIP_SIZE }),
|
|
370
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native_paper3.Text, { variant: "bodyLarge", style: [styles3.rowName, { color: fg }], numberOfLines: 1, children: profile.name })
|
|
371
|
+
] }) }, profile.id);
|
|
372
|
+
}) }) }),
|
|
373
|
+
confirmDeleteProfile && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native_paper3.Portal, { children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native3.View, { style: styles3.overlay, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_react_native3.View, { style: [styles3.overlayCard, { backgroundColor: cardBg, borderColor: cardBorder }], children: [
|
|
374
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native_paper3.Icon, { source: "alert-outline", size: 64, color: colors.danger }),
|
|
375
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native_paper3.Text, { variant: "headlineLarge", style: [styles3.overlayTitle, { color: colors.danger }], children: "Delete Profile?" }),
|
|
376
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_react_native_paper3.Text, { variant: "bodyLarge", style: [styles3.overlayBody, { color: fg }], children: [
|
|
377
|
+
"This permanently deletes ",
|
|
378
|
+
confirmDeleteProfile.name,
|
|
379
|
+
"\u2019s profile. This cannot be undone."
|
|
380
|
+
] }),
|
|
381
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native_paper3.Button, { mode: "contained", onPress: () => setConfirmDeleteId(null), style: styles3.overlayButton, buttonColor: cancelBg, textColor: cancelFg, children: "Cancel" }),
|
|
382
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native_paper3.Button, { mode: "contained", onPress: handleConfirmDelete, style: styles3.overlayButton, buttonColor: colors.danger, textColor: colors.onDanger, children: "Delete" })
|
|
383
|
+
] }) }) })
|
|
384
|
+
] });
|
|
385
|
+
}
|
|
386
|
+
function EditRow({ draftName, onNameChange, draftColor, onColorChange, draftTag, onTagChange, tagInvalid, onSubmit, onDelete, autoFocus, fgMuted, host, dark }) {
|
|
387
|
+
const register = (0, import_focus_chain.useFocusChain)();
|
|
388
|
+
const tag = register();
|
|
389
|
+
const name = register();
|
|
390
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_react_native3.View, { style: styles3.editRow, children: [
|
|
391
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_hud2.InlineColorPicker, { id: "color", host, value: draftColor, onChange: onColorChange, dark, size: CHIP_SIZE }),
|
|
392
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native_paper3.TextInput, { ref: tag.ref, ...tag.props, mode: "outlined", dense: true, value: draftTag, onChangeText: onTagChange, placeholder: "Tag", autoCapitalize: "characters", selectTextOnFocus: true, error: tagInvalid, returnKeyType: "next", style: styles3.tagInput, contentStyle: styles3.tagInputContent }),
|
|
393
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native_paper3.TextInput, { ref: name.ref, ...name.props, mode: "outlined", dense: true, value: draftName, onChangeText: onNameChange, maxLength: MAX_PROFILE_NAME_LENGTH, placeholder: "Name", autoFocus, returnKeyType: "done", style: styles3.nameInput, onSubmitEditing: onSubmit }),
|
|
394
|
+
onDelete && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_feedback_press2.TouchableRipple, { onPress: onDelete, style: styles3.iconButton, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native_paper3.Icon, { source: "trash-can-outline", size: 18, color: fgMuted }) })
|
|
395
|
+
] });
|
|
396
|
+
}
|
|
397
|
+
var styles3 = import_react_native3.StyleSheet.create({
|
|
398
|
+
content: {
|
|
399
|
+
gap: 4,
|
|
400
|
+
paddingHorizontal: 20
|
|
401
|
+
},
|
|
402
|
+
editRow: {
|
|
403
|
+
alignItems: "center",
|
|
404
|
+
flexDirection: "row",
|
|
405
|
+
gap: 8,
|
|
406
|
+
paddingVertical: 6
|
|
407
|
+
},
|
|
408
|
+
header: {
|
|
409
|
+
alignItems: "center",
|
|
410
|
+
flexDirection: "row",
|
|
411
|
+
gap: 4,
|
|
412
|
+
paddingBottom: 8
|
|
413
|
+
},
|
|
414
|
+
iconButton: {
|
|
415
|
+
alignItems: "center",
|
|
416
|
+
borderRadius: 8,
|
|
417
|
+
height: 36,
|
|
418
|
+
justifyContent: "center",
|
|
419
|
+
width: 36
|
|
420
|
+
},
|
|
421
|
+
keyboardAvoiding: {
|
|
422
|
+
flex: 1
|
|
423
|
+
},
|
|
424
|
+
nameInput: {
|
|
425
|
+
flex: 1
|
|
426
|
+
},
|
|
427
|
+
newChip: {
|
|
428
|
+
backgroundColor: "transparent",
|
|
429
|
+
borderStyle: "dashed",
|
|
430
|
+
borderWidth: 2
|
|
431
|
+
},
|
|
432
|
+
overlay: {
|
|
433
|
+
alignItems: "center",
|
|
434
|
+
backgroundColor: "rgba(0,0,0,0.72)",
|
|
435
|
+
bottom: 0,
|
|
436
|
+
justifyContent: "center",
|
|
437
|
+
left: 0,
|
|
438
|
+
position: "absolute",
|
|
439
|
+
right: 0,
|
|
440
|
+
top: 0
|
|
441
|
+
},
|
|
442
|
+
overlayBody: { textAlign: "center" },
|
|
443
|
+
overlayButton: { width: 160 },
|
|
444
|
+
overlayCard: {
|
|
445
|
+
alignItems: "center",
|
|
446
|
+
borderRadius: 20,
|
|
447
|
+
borderWidth: 1,
|
|
448
|
+
gap: 16,
|
|
449
|
+
maxWidth: 360,
|
|
450
|
+
padding: 32
|
|
451
|
+
},
|
|
452
|
+
overlayTitle: { fontWeight: "bold", marginBottom: -8 },
|
|
453
|
+
row: {
|
|
454
|
+
borderRadius: 8,
|
|
455
|
+
paddingVertical: 8
|
|
456
|
+
},
|
|
457
|
+
rowChip: {
|
|
458
|
+
alignItems: "center",
|
|
459
|
+
borderRadius: CHIP_SIZE / 2,
|
|
460
|
+
height: CHIP_SIZE,
|
|
461
|
+
justifyContent: "center",
|
|
462
|
+
width: CHIP_SIZE
|
|
463
|
+
},
|
|
464
|
+
rowInner: {
|
|
465
|
+
alignItems: "center",
|
|
466
|
+
flexDirection: "row",
|
|
467
|
+
gap: 12
|
|
468
|
+
},
|
|
469
|
+
rowName: {
|
|
470
|
+
flex: 1
|
|
471
|
+
},
|
|
472
|
+
scrollView: {
|
|
473
|
+
flex: 1
|
|
474
|
+
},
|
|
475
|
+
// Wide enough for MAX_TAG_LENGTH monospace characters (or one emoji) plus its own padding/border
|
|
476
|
+
// — matched to CHIP_SIZE (the same footprint every other circle on this screen uses), not a
|
|
477
|
+
// fraction of the row's width, since the name field next to it is what should actually get the
|
|
478
|
+
// row's remaining space.
|
|
479
|
+
tagInput: {
|
|
480
|
+
height: CHIP_SIZE,
|
|
481
|
+
width: CHIP_SIZE
|
|
482
|
+
},
|
|
483
|
+
tagInputContent: {
|
|
484
|
+
// react-native-paper's outlined TextInput sets its own native input's minWidth from the
|
|
485
|
+
// placeholder/label's measured layout width (sized for a real label, not this field's own
|
|
486
|
+
// 3-character placeholder) — left alone, that floor is wider than CHIP_SIZE, so the actual
|
|
487
|
+
// editable box silently overflows past this field's own visible outline and centers within
|
|
488
|
+
// that wider, partly invisible box instead. Zeroing it here lets the input size to CHIP_SIZE
|
|
489
|
+
// instead, which is what actually makes `textAlign: 'center'` below center against the
|
|
490
|
+
// visible box rather than an overflowing invisible one.
|
|
491
|
+
minWidth: 0,
|
|
492
|
+
// Same reasoning as minWidth above, for the horizontal direction: the outlined input's own
|
|
493
|
+
// default padding (16px a side) is sized for a normal-width field, and at CHIP_SIZE's 40px
|
|
494
|
+
// that leaves only 8px total for actual text — not enough to fit the "Tag" placeholder itself
|
|
495
|
+
// (3 characters), which is what was clipping it to just "T" once minWidth stopped letting the
|
|
496
|
+
// box silently overflow wider to compensate.
|
|
497
|
+
paddingHorizontal: 2,
|
|
498
|
+
textAlign: "center",
|
|
499
|
+
textAlignVertical: "center"
|
|
500
|
+
},
|
|
501
|
+
title: {
|
|
502
|
+
flexShrink: 1,
|
|
503
|
+
fontWeight: "bold"
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
507
|
+
0 && (module.exports = {
|
|
508
|
+
MAX_PROFILE_NAME_LENGTH,
|
|
509
|
+
MAX_TAG_LENGTH,
|
|
510
|
+
ProfileChip,
|
|
511
|
+
ProfilePicker,
|
|
512
|
+
ProfilesManager,
|
|
513
|
+
isValidProfile,
|
|
514
|
+
isValidTag
|
|
515
|
+
});
|