@tastic/profile 0.16.2 → 0.18.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 +19 -10
- package/dist/index.d.mts +6 -5
- package/dist/index.d.ts +6 -5
- package/dist/index.js +117 -57
- package/dist/index.mjs +98 -38
- package/package.json +9 -8
- package/src/OptionPicker.tsx +26 -10
- package/src/PickerMenu.tsx +62 -22
- package/src/ProfileColorContext.tsx +2 -2
- package/src/ProfilePicker.tsx +2 -1
- package/src/ProfilesManager.tsx +77 -51
- package/src/sharedProfileStore.ts +35 -5
package/dist/index.mjs
CHANGED
|
@@ -5,6 +5,9 @@ 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/OptionPicker.tsx
|
|
9
|
+
import { Fragment as Fragment2 } from "react";
|
|
10
|
+
|
|
8
11
|
// src/PickerMenu.tsx
|
|
9
12
|
import { defaultColors, FadingScrollView, getContrastColor as getContrastColor2 } from "@rific/auto-paper";
|
|
10
13
|
import { TouchableRipple } from "@rific/feedback-press";
|
|
@@ -90,6 +93,12 @@ var MENU_BORDER_WIDTH = 2;
|
|
|
90
93
|
var ROW_INSET = 12;
|
|
91
94
|
var CHIP_SIZE = 32;
|
|
92
95
|
var CHIP_TRIGGER_PADDING = 6;
|
|
96
|
+
var CHIP_TRIGGER_END_PADDING = CHIP_TRIGGER_PADDING + 8;
|
|
97
|
+
var CHIP_TRIGGER_GAP = 8;
|
|
98
|
+
var PLAIN_TRIGGER_GAP = 2;
|
|
99
|
+
var CHEVRON_SIZE = 16;
|
|
100
|
+
var CHIP_TRIGGER_FIXED_WIDTH = CHIP_TRIGGER_PADDING + CHIP_SIZE + 2 * CHIP_TRIGGER_GAP + CHEVRON_SIZE + CHIP_TRIGGER_END_PADDING;
|
|
101
|
+
var PLAIN_TRIGGER_FIXED_WIDTH = PLAIN_TRIGGER_GAP + CHEVRON_SIZE;
|
|
93
102
|
var SWATCH_SIZE = 28;
|
|
94
103
|
var SWATCH_MIN_GAP = 6;
|
|
95
104
|
var SWATCH_RING_WIDTH = 2;
|
|
@@ -97,7 +106,7 @@ var COLOR_ROW_SWATCH_SIZE = 22;
|
|
|
97
106
|
var COLOR_ROW_CAPTIONED_HEIGHT = 48;
|
|
98
107
|
var COLOR_LABEL_LINE = 18;
|
|
99
108
|
var COLOR_CAPTION_LINE = 16;
|
|
100
|
-
var
|
|
109
|
+
var DIVIDER_HEIGHT = 9;
|
|
101
110
|
var GRID_TOP = 4;
|
|
102
111
|
var GRID_BOTTOM = 8;
|
|
103
112
|
var GRID_WIDTH = POPOVER_WIDTH - 2 * MENU_BORDER_WIDTH - 2 * LIST_PADDING - 2 * ROW_INSET;
|
|
@@ -110,8 +119,8 @@ function colorPageHeight(swatches) {
|
|
|
110
119
|
const rows = Math.ceil(swatches.length / GRID_COLUMNS);
|
|
111
120
|
return ROW_HEIGHT + GRID_TOP + rows * SWATCH_SIZE + (rows - 1) * GRID_GAP + GRID_BOTTOM;
|
|
112
121
|
}
|
|
113
|
-
function pickerContentSize(rowCount, colorSection) {
|
|
114
|
-
const list = rowCount * ROW_HEIGHT + (colorSection ? colorRowHeight(colorSection.caption) +
|
|
122
|
+
function pickerContentSize(rowCount, colorSection, dividerCount = 0) {
|
|
123
|
+
const list = rowCount * ROW_HEIGHT + dividerCount * DIVIDER_HEIGHT + (colorSection ? colorRowHeight(colorSection.caption) + DIVIDER_HEIGHT : 0);
|
|
115
124
|
const content = colorSection ? Math.max(list, colorPageHeight(colorSection.swatches ?? defaultColors)) : list;
|
|
116
125
|
return { width: POPOVER_WIDTH, height: LIST_PADDING * 2 + content };
|
|
117
126
|
}
|
|
@@ -134,11 +143,15 @@ function PickerMenu({ id, host, color, dark, label, chip, colorSection, homeColo
|
|
|
134
143
|
};
|
|
135
144
|
const triggerLabel = label.toUpperCase();
|
|
136
145
|
const showFooter = !!footer && !showColorPage;
|
|
137
|
-
|
|
138
|
-
|
|
146
|
+
const flatTriggerStyle = StyleSheet3.flatten(triggerStyle) ?? {};
|
|
147
|
+
const { flexShrink, ...pillStyle } = flatTriggerStyle;
|
|
148
|
+
const shrinks = typeof flexShrink === "number" && flexShrink > 0;
|
|
149
|
+
const anchorShrink = shrinks ? { flexShrink, minWidth: chipTrigger ? CHIP_TRIGGER_FIXED_WIDTH : PLAIN_TRIGGER_FIXED_WIDTH } : void 0;
|
|
150
|
+
return /* @__PURE__ */ jsxs2(View3, { style: [styles3.anchor, anchorShrink, open && styles3.anchorOpen], onLayout: handleAnchorLayout, children: [
|
|
151
|
+
/* @__PURE__ */ jsx4(TouchableRipple, { onPress: handleTriggerPress, borderless: chipTrigger, style: [styles3.trigger, chipTrigger && styles3.chipTrigger, shrinks && styles3.triggerShrinks, shrinks ? pillStyle : triggerStyle], accessibilityRole: "button", accessibilityLabel: triggerLabel, children: /* @__PURE__ */ jsxs2(View3, { ref: triggerRef, collapsable: false, style: chipTrigger ? styles3.chipTriggerInner : styles3.triggerInner, children: [
|
|
139
152
|
chip && /* @__PURE__ */ jsx4(IdentityChip, { color, tag: chip.tag, icon: chip.icon, size: CHIP_SIZE }),
|
|
140
153
|
/* @__PURE__ */ jsx4(Text2, { style: [styles3.triggerLabel, { color: chipTrigger ? fg : color }], numberOfLines: 1, children: triggerLabel }),
|
|
141
|
-
/* @__PURE__ */ jsx4(Icon2, { source: "menu-down", size:
|
|
154
|
+
/* @__PURE__ */ jsx4(Icon2, { source: "menu-down", size: CHEVRON_SIZE, color: chipTrigger ? fg : color })
|
|
142
155
|
] }) }),
|
|
143
156
|
/* @__PURE__ */ jsx4(PopoverBody, { visible: open && measured, align, verticalAlign, caretColor: menuBg, caretBorderColor: color, caretBorderWidth: MENU_BORDER_WIDTH, triggerSize: chipTrigger ? triggerWidth : TRIGGER_HEIGHT, children: /* @__PURE__ */ jsxs2(
|
|
144
157
|
View3,
|
|
@@ -158,7 +171,7 @@ function PickerMenu({ id, host, color, dark, label, chip, colorSection, homeColo
|
|
|
158
171
|
children
|
|
159
172
|
] }) }, showColorPage ? "color" : "list"),
|
|
160
173
|
showFooter && /* @__PURE__ */ jsxs2(View3, { style: styles3.menuFooter, children: [
|
|
161
|
-
/* @__PURE__ */ jsx4(
|
|
174
|
+
/* @__PURE__ */ jsx4(MenuDivider, { color }),
|
|
162
175
|
footer
|
|
163
176
|
] })
|
|
164
177
|
]
|
|
@@ -187,9 +200,12 @@ function ColorRow({ section, fg, dividerColor, onPress }) {
|
|
|
187
200
|
] }),
|
|
188
201
|
/* @__PURE__ */ jsx4(Icon2, { source: "chevron-right", size: 18, color: fg })
|
|
189
202
|
] }) }),
|
|
190
|
-
/* @__PURE__ */ jsx4(
|
|
203
|
+
/* @__PURE__ */ jsx4(MenuDivider, { color: dividerColor })
|
|
191
204
|
] });
|
|
192
205
|
}
|
|
206
|
+
function MenuDivider({ color }) {
|
|
207
|
+
return /* @__PURE__ */ jsx4(View3, { style: [styles3.divider, { backgroundColor: color }] });
|
|
208
|
+
}
|
|
193
209
|
function ColorPage({ section, homeColor, fg, onBack }) {
|
|
194
210
|
const { value, onChange, previewValue, swatches = defaultColors, label = "Color", takenValue, allowSwapTaken } = section;
|
|
195
211
|
const renderSwatch = useProfileSwatchRenderer();
|
|
@@ -256,10 +272,10 @@ var styles3 = StyleSheet3.create({
|
|
|
256
272
|
chipTriggerInner: {
|
|
257
273
|
alignItems: "center",
|
|
258
274
|
flexDirection: "row",
|
|
259
|
-
gap:
|
|
275
|
+
gap: CHIP_TRIGGER_GAP,
|
|
260
276
|
height: CHIP_SIZE + 2 * CHIP_TRIGGER_PADDING,
|
|
261
277
|
paddingLeft: CHIP_TRIGGER_PADDING,
|
|
262
|
-
paddingRight:
|
|
278
|
+
paddingRight: CHIP_TRIGGER_END_PADDING
|
|
263
279
|
},
|
|
264
280
|
colorRowCaption: {
|
|
265
281
|
fontSize: 12,
|
|
@@ -281,7 +297,8 @@ var styles3 = StyleSheet3.create({
|
|
|
281
297
|
flex: 1
|
|
282
298
|
},
|
|
283
299
|
// Separates the footer (a navigation action) and the Color row from the selectable rows - a
|
|
284
|
-
// different kind of action, not just another row in the same list
|
|
300
|
+
// different kind of action, not just another row in the same list - and an OptionPicker's groups
|
|
301
|
+
// of options from each other (its `dividerBefore`). Tinted with the seat's own color
|
|
285
302
|
// (passed in as backgroundColor) rather than a flat gray, matching the border/caret's own
|
|
286
303
|
// accent-tinted look.
|
|
287
304
|
divider: {
|
|
@@ -368,20 +385,33 @@ var styles3 = StyleSheet3.create({
|
|
|
368
385
|
triggerInner: {
|
|
369
386
|
alignItems: "center",
|
|
370
387
|
flexDirection: "row",
|
|
371
|
-
gap:
|
|
388
|
+
gap: PLAIN_TRIGGER_GAP,
|
|
372
389
|
height: TRIGGER_HEIGHT
|
|
373
390
|
},
|
|
391
|
+
// Lets the label, alone in its row, give way to a trigger narrower than its name (see `shrinks`).
|
|
392
|
+
// With room it keeps its natural width, so this changes nothing there.
|
|
374
393
|
triggerLabel: {
|
|
394
|
+
flexShrink: 1,
|
|
375
395
|
fontSize: 13,
|
|
376
396
|
fontWeight: "700",
|
|
377
|
-
letterSpacing: 1
|
|
397
|
+
letterSpacing: 1,
|
|
398
|
+
minWidth: 0
|
|
399
|
+
},
|
|
400
|
+
// A shrinking trigger (see `shrinks`): no wider than its anchor. Its row is then laid out at that
|
|
401
|
+
// width, which is what cuts the label off.
|
|
402
|
+
triggerShrinks: {
|
|
403
|
+
maxWidth: "100%"
|
|
378
404
|
}
|
|
379
405
|
});
|
|
380
406
|
|
|
381
407
|
// src/OptionPicker.tsx
|
|
382
|
-
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
383
|
-
function
|
|
384
|
-
return
|
|
408
|
+
import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
409
|
+
function hasDividerBefore(option, index) {
|
|
410
|
+
return index > 0 && !!option.dividerBefore;
|
|
411
|
+
}
|
|
412
|
+
function getOptionPickerContentSize(options, colorSection) {
|
|
413
|
+
if (typeof options === "number") return pickerContentSize(options, colorSection);
|
|
414
|
+
return pickerContentSize(options.length, colorSection, options.filter(hasDividerBefore).length);
|
|
385
415
|
}
|
|
386
416
|
function OptionPicker({ id, host, options, value, onChange, color, dark, icon, rowIcon = icon, colorSection, align, alignOverride, rotation = 0, triggerStyle }) {
|
|
387
417
|
const fg = dark ? "#FFFFFF" : "#000000";
|
|
@@ -390,7 +420,10 @@ function OptionPicker({ id, host, options, value, onChange, color, dark, icon, r
|
|
|
390
420
|
onChange(option.value);
|
|
391
421
|
host.close();
|
|
392
422
|
};
|
|
393
|
-
return /* @__PURE__ */ jsx5(PickerMenu, { id, host, color, dark, label: selected?.label ?? "", chip: { icon }, colorSection, contentSize: getOptionPickerContentSize(options
|
|
423
|
+
return /* @__PURE__ */ jsx5(PickerMenu, { id, host, color, dark, label: selected?.label ?? "", chip: { icon }, colorSection, contentSize: getOptionPickerContentSize(options, colorSection), align, alignOverride, rotation, triggerStyle, children: options.map((option, index) => /* @__PURE__ */ jsxs3(Fragment2, { children: [
|
|
424
|
+
hasDividerBefore(option, index) && /* @__PURE__ */ jsx5(MenuDivider, { color }),
|
|
425
|
+
/* @__PURE__ */ jsx5(IconRow, { icon: option.icon ?? rowIcon, iconColor: option.color, label: option.label, selected: option.value === value, color, fg, onPress: () => handleSelect(option) })
|
|
426
|
+
] }, option.value)) });
|
|
394
427
|
}
|
|
395
428
|
|
|
396
429
|
// src/ProfileChip.tsx
|
|
@@ -406,7 +439,7 @@ import { TouchableRipple as TouchableRipple2 } from "@rific/feedback-press";
|
|
|
406
439
|
import { useMemo as useMemo2 } from "react";
|
|
407
440
|
import { StyleSheet as StyleSheet4, View as View4 } from "react-native";
|
|
408
441
|
import { Icon as Icon3, Text as Text3 } from "react-native-paper";
|
|
409
|
-
import { jsx as jsx7, jsxs as
|
|
442
|
+
import { jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
410
443
|
function getProfilePickerContentSize(profileCount, hasManageRow = false, colorSection) {
|
|
411
444
|
return pickerContentSize(profileCount + 1 + (hasManageRow ? 1 : 0), colorSection);
|
|
412
445
|
}
|
|
@@ -431,17 +464,17 @@ function ProfilePicker({ idPrefix, host, profiles, selectedId, takenId, takenIds
|
|
|
431
464
|
host.close();
|
|
432
465
|
onManage?.();
|
|
433
466
|
};
|
|
434
|
-
const manageFooter = onManage ? /* @__PURE__ */ jsx7(TouchableRipple2, { onPress: handleManage, style: pickerMenuStyles.row, children: /* @__PURE__ */
|
|
467
|
+
const manageFooter = onManage ? /* @__PURE__ */ jsx7(TouchableRipple2, { onPress: handleManage, style: pickerMenuStyles.row, children: /* @__PURE__ */ jsxs4(View4, { style: styles4.manageRowInner, children: [
|
|
435
468
|
/* @__PURE__ */ jsx7(Icon3, { source: "account-cog-outline", size: 16, color }),
|
|
436
469
|
/* @__PURE__ */ jsx7(Text3, { style: [styles4.manageLabel, { color: fg }], children: "Manage" })
|
|
437
470
|
] }) }) : void 0;
|
|
438
|
-
return /* @__PURE__ */
|
|
471
|
+
return /* @__PURE__ */ jsxs4(PickerMenu, { id, host, color, dark, label: selectedProfile?.name ?? guestLabel, chip: colorSection ? { tag: selectedProfile?.tag, icon: selectedProfile ? "account" : nullIcon } : void 0, colorSection, homeColor: selectedProfile?.color, contentSize, align, alignOverride, rotation, triggerStyle, footer: manageFooter, children: [
|
|
439
472
|
/* @__PURE__ */ jsx7(IconRow, { icon: nullIcon, label: nullLabel, selected: selectedId === null, color, fg, onPress: handleSelectGuest }),
|
|
440
473
|
sortedProfiles.map((profile) => {
|
|
441
474
|
const taken = isTaken(profile.id);
|
|
442
475
|
const selected = selectedId === profile.id;
|
|
443
476
|
const rowColor = colorPreview ? colorPreview(profile.color) : profile.color;
|
|
444
|
-
return /* @__PURE__ */ jsx7(TouchableRipple2, { disabled: taken, onPress: () => handleSelectExisting(profile), style: [pickerMenuStyles.row, selected && { backgroundColor: rowColor }], children: /* @__PURE__ */
|
|
477
|
+
return /* @__PURE__ */ jsx7(TouchableRipple2, { disabled: taken, onPress: () => handleSelectExisting(profile), style: [pickerMenuStyles.row, selected && { backgroundColor: rowColor }], children: /* @__PURE__ */ jsxs4(View4, { style: [pickerMenuStyles.rowInner, taken && pickerMenuStyles.rowDisabled], children: [
|
|
445
478
|
/* @__PURE__ */ jsx7(ProfileChip, { profile, filled: !selected }),
|
|
446
479
|
/* @__PURE__ */ jsx7(
|
|
447
480
|
Text3,
|
|
@@ -521,8 +554,9 @@ function isValidProfile(value) {
|
|
|
521
554
|
}
|
|
522
555
|
|
|
523
556
|
// src/ProfilesManager.tsx
|
|
524
|
-
import { Fragment as
|
|
557
|
+
import { Fragment as Fragment3, jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
525
558
|
var CHIP_SIZE2 = 40;
|
|
559
|
+
var textInputRef = (ref) => ref;
|
|
526
560
|
function deriveTag(name) {
|
|
527
561
|
const trimmed = name.trim();
|
|
528
562
|
return trimmed ? trimmed[0].toUpperCase() : "";
|
|
@@ -533,6 +567,8 @@ function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, h
|
|
|
533
567
|
const { dark } = useAutoPaperTheme();
|
|
534
568
|
const insets = useSafeAreaInsets();
|
|
535
569
|
const { error: showErrorToast } = useToast();
|
|
570
|
+
const columnRef = useRef(null);
|
|
571
|
+
const renderSwatch = useProfileSwatchRenderer();
|
|
536
572
|
const host = usePopoverHost();
|
|
537
573
|
const fg = fgOverride ?? (dark ? "#FFFFFF" : "#000000");
|
|
538
574
|
const fgMuted = fgMutedOverride ?? (dark ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.5)");
|
|
@@ -598,40 +634,41 @@ function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, h
|
|
|
598
634
|
}, [confirmDeleteId, editingId, onDelete]);
|
|
599
635
|
const sortedProfiles = useMemo3(() => [...profiles].sort((a, b) => a.name.localeCompare(b.name)), [profiles]);
|
|
600
636
|
const rows = [...sortedProfiles.map((profile) => ({ kind: "profile", profile })), { kind: "new" }];
|
|
601
|
-
return /* @__PURE__ */
|
|
602
|
-
/* @__PURE__ */
|
|
637
|
+
return /* @__PURE__ */ jsxs5(Fragment3, { children: [
|
|
638
|
+
/* @__PURE__ */ jsxs5(View5, { style: [styles5.header, { paddingTop: 8 + insets.top, paddingLeft: 8 + insets.left }], children: [
|
|
603
639
|
headerLeft,
|
|
604
640
|
/* @__PURE__ */ jsx8(Text4, { variant: titleVariant, style: [styles5.title, { color: fg }], children: "Profiles" })
|
|
605
641
|
] }),
|
|
606
|
-
/* @__PURE__ */ jsx8(KeyboardAvoidingView, { behavior: Platform.OS === "ios" ? "padding" : void 0, style: styles5.keyboardAvoiding, children: /* @__PURE__ */ jsx8(FadingScrollView2, { style: styles5.scrollView, contentContainerStyle: [styles5.content, { paddingBottom: 32 + insets.bottom }], showsVerticalScrollIndicator: false, keyboardShouldPersistTaps: "handled", children: rows.map((row) => {
|
|
642
|
+
/* @__PURE__ */ jsx8(KeyboardAvoidingView, { behavior: Platform.OS === "ios" ? "padding" : void 0, style: styles5.keyboardAvoiding, children: /* @__PURE__ */ jsx8(View5, { ref: columnRef, collapsable: false, style: styles5.column, children: /* @__PURE__ */ jsx8(FadingScrollView2, { style: styles5.scrollView, contentContainerStyle: [styles5.content, { paddingBottom: 32 + insets.bottom }], showsVerticalScrollIndicator: false, keyboardShouldPersistTaps: "handled", children: rows.map((row) => {
|
|
607
643
|
if (row.kind === "new") {
|
|
608
644
|
if (editingId !== "new") {
|
|
609
|
-
return /* @__PURE__ */ jsx8(TouchableRipple3, { onPress: () => startEdit("new"), style: styles5.row, children: /* @__PURE__ */
|
|
645
|
+
return /* @__PURE__ */ jsx8(TouchableRipple3, { onPress: () => startEdit("new"), style: styles5.row, children: /* @__PURE__ */ jsxs5(View5, { style: styles5.rowInner, children: [
|
|
610
646
|
/* @__PURE__ */ jsx8(View5, { style: [styles5.rowChip, styles5.newChip, { borderColor: fg }], children: /* @__PURE__ */ jsx8(Icon4, { source: "plus", size: 20, color: fg }) }),
|
|
611
647
|
/* @__PURE__ */ jsx8(Text4, { variant: "bodyLarge", style: { color: fg }, children: "New Profile" })
|
|
612
648
|
] }) }, "new");
|
|
613
649
|
}
|
|
614
|
-
return /* @__PURE__ */ jsx8(EditRow, { draftName, onNameChange: handleNameChange, draftColor, onColorChange: setDraftColor, draftTag, onTagChange: handleTagChange, onSubmit: commitEdit, autoFocus: true, fgMuted, host, dark, colorPreview, useColorPickerAlign }, "new");
|
|
650
|
+
return /* @__PURE__ */ jsx8(EditRow, { draftName, onNameChange: handleNameChange, draftColor, onColorChange: setDraftColor, draftTag, onTagChange: handleTagChange, onSubmit: commitEdit, autoFocus: true, fgMuted, host, dark, colorPreview, useColorPickerAlign, column: columnRef, renderSwatch }, "new");
|
|
615
651
|
}
|
|
616
652
|
const { profile } = row;
|
|
617
653
|
if (editingId === profile.id) {
|
|
618
|
-
return /* @__PURE__ */ jsx8(EditRow, { draftName, onNameChange: handleNameChange, draftColor, onColorChange: setDraftColor, draftTag, onTagChange: handleTagChange, onSubmit: commitEdit, onDelete: () => setConfirmDeleteId(profile.id), fgMuted, host, dark, colorPreview, useColorPickerAlign }, profile.id);
|
|
654
|
+
return /* @__PURE__ */ jsx8(EditRow, { draftName, onNameChange: handleNameChange, draftColor, onColorChange: setDraftColor, draftTag, onTagChange: handleTagChange, onSubmit: commitEdit, onDelete: () => setConfirmDeleteId(profile.id), fgMuted, host, dark, colorPreview, useColorPickerAlign, column: columnRef, renderSwatch }, profile.id);
|
|
619
655
|
}
|
|
620
|
-
return /* @__PURE__ */ jsx8(TouchableRipple3, { onPress: () => startEdit(profile), style: styles5.row, children: /* @__PURE__ */
|
|
656
|
+
return /* @__PURE__ */ jsx8(TouchableRipple3, { onPress: () => startEdit(profile), style: styles5.row, children: /* @__PURE__ */ jsxs5(View5, { style: styles5.rowInner, children: [
|
|
621
657
|
/* @__PURE__ */ jsx8(ProfileChip, { profile, size: CHIP_SIZE2 }),
|
|
622
658
|
/* @__PURE__ */ jsx8(Text4, { variant: "bodyLarge", style: [styles5.rowName, { color: fg }], numberOfLines: 1, children: profile.name })
|
|
623
659
|
] }) }, profile.id);
|
|
624
|
-
}) }) }),
|
|
660
|
+
}) }) }) }),
|
|
625
661
|
/* @__PURE__ */ jsx8(ConfirmDialog, { visible: confirmDeleteProfile !== null, title: "Delete Profile?", message: confirmDeleteProfile ? `This permanently deletes ${confirmDeleteProfile.name}\u2019s profile. This cannot be undone.` : "", confirmLabel: "Delete", cancelLabel: "Cancel", icon: "alert-outline", destructive: true, onConfirm: handleConfirmDelete, onCancel: () => setConfirmDeleteId(null) })
|
|
626
662
|
] });
|
|
627
663
|
}
|
|
628
|
-
function AlignedColorPicker({ useAlign, ...props }) {
|
|
664
|
+
function AlignedColorPicker({ useAlign, column, ...props }) {
|
|
629
665
|
const { width: windowWidth } = useWindowDimensions();
|
|
630
666
|
const { width, height } = getInlineColorPickerContentSize(defaultColors2.length, windowWidth);
|
|
631
|
-
const alignOverride = useAlign(props.host.openId === props.id, width, height);
|
|
667
|
+
const alignOverride = useAlign(props.host.openId === props.id, width, height, column);
|
|
632
668
|
return /* @__PURE__ */ jsx8(InlineColorPicker, { ...props, alignOverride });
|
|
633
669
|
}
|
|
634
|
-
function EditRow({ draftName, onNameChange, draftColor, onColorChange, draftTag, onTagChange, onSubmit, onDelete, autoFocus, fgMuted, host, dark, colorPreview, useColorPickerAlign }) {
|
|
670
|
+
function EditRow({ draftName, onNameChange, draftColor, onColorChange, draftTag, onTagChange, onSubmit, onDelete, autoFocus, fgMuted, host, dark, colorPreview, useColorPickerAlign, column, renderSwatch }) {
|
|
671
|
+
const pickerSwatch = useMemo3(() => renderSwatch ? (color, size, place) => renderSwatch(color, size, place === "trigger" ? "chip" : "colorPage") : void 0, [renderSwatch]);
|
|
635
672
|
const register = useFocusChain();
|
|
636
673
|
const tag = register();
|
|
637
674
|
const name = register();
|
|
@@ -642,14 +679,18 @@ function EditRow({ draftName, onNameChange, draftColor, onColorChange, draftTag,
|
|
|
642
679
|
useEffect(() => {
|
|
643
680
|
if (host.openId === "color") tagFocusRef.current();
|
|
644
681
|
}, [host.openId]);
|
|
645
|
-
return /* @__PURE__ */
|
|
646
|
-
useColorPickerAlign ? /* @__PURE__ */ jsx8(AlignedColorPicker, { useAlign: useColorPickerAlign, id: "color", host, value: draftColor, onChange: onColorChange, previewValue: colorPreview, tag: draftTag, dark }) : /* @__PURE__ */ jsx8(InlineColorPicker, { id: "color", host, value: draftColor, onChange: onColorChange, previewValue: colorPreview, tag: draftTag, dark }),
|
|
647
|
-
/* @__PURE__ */ jsx8(TextInput, { ref: tag.ref, ...tag.props, value: draftTag, onChangeText: onTagChange, accessibilityLabel: "Tag", autoCapitalize: "characters", selectTextOnFocus: true, returnKeyType: "next", style: styles5.hiddenTagInput, pointerEvents: "none" }),
|
|
648
|
-
/* @__PURE__ */ jsx8(TextInput, { ref: name.ref, ...name.props, onSubmitEditing: onSubmit, mode: "outlined", dense: true, value: draftName, onChangeText: onNameChange, maxLength: MAX_PROFILE_NAME_LENGTH, placeholder: "Name", autoFocus, returnKeyType: "done", style: styles5.nameInput }),
|
|
682
|
+
return /* @__PURE__ */ jsxs5(View5, { style: styles5.editRow, children: [
|
|
683
|
+
useColorPickerAlign ? /* @__PURE__ */ jsx8(AlignedColorPicker, { useAlign: useColorPickerAlign, column, id: "color", host, value: draftColor, onChange: onColorChange, previewValue: colorPreview, tag: draftTag, dark, renderSwatch: pickerSwatch }) : /* @__PURE__ */ jsx8(InlineColorPicker, { id: "color", host, value: draftColor, onChange: onColorChange, previewValue: colorPreview, tag: draftTag, dark, renderSwatch: pickerSwatch, bounds: column }),
|
|
684
|
+
/* @__PURE__ */ jsx8(TextInput, { ref: textInputRef(tag.ref), ...tag.props, value: draftTag, onChangeText: onTagChange, accessibilityLabel: "Tag", autoCapitalize: "characters", selectTextOnFocus: true, returnKeyType: "next", style: styles5.hiddenTagInput, pointerEvents: "none" }),
|
|
685
|
+
/* @__PURE__ */ jsx8(TextInput, { ref: textInputRef(name.ref), ...name.props, onSubmitEditing: onSubmit, mode: "outlined", dense: true, value: draftName, onChangeText: onNameChange, maxLength: MAX_PROFILE_NAME_LENGTH, placeholder: "Name", autoFocus, returnKeyType: "done", style: styles5.nameInput }),
|
|
649
686
|
onDelete && /* @__PURE__ */ jsx8(TouchableRipple3, { onPress: onDelete, style: styles5.iconButton, children: /* @__PURE__ */ jsx8(Icon4, { source: "trash-can-outline", size: 18, color: fgMuted }) })
|
|
650
687
|
] });
|
|
651
688
|
}
|
|
652
689
|
var styles5 = StyleSheet5.create({
|
|
690
|
+
// The roster's column, around the scroll view (see columnRef).
|
|
691
|
+
column: {
|
|
692
|
+
flex: 1
|
|
693
|
+
},
|
|
653
694
|
content: {
|
|
654
695
|
gap: 4,
|
|
655
696
|
paddingHorizontal: 20
|
|
@@ -923,10 +964,29 @@ async function readSharedProfiles(groupId) {
|
|
|
923
964
|
async function loadSharedProfiles(groupId) {
|
|
924
965
|
return await readSharedProfiles(groupId) ?? [];
|
|
925
966
|
}
|
|
967
|
+
var hasId = (entry) => typeof entry === "object" && entry !== null && typeof entry.id === "string" && entry.id !== "";
|
|
968
|
+
async function unreadableEntries(groupId, profiles) {
|
|
969
|
+
let json;
|
|
970
|
+
try {
|
|
971
|
+
json = web ? web.storage.getItem(webKey(groupId, PROFILES_KEY)) : await native?.getSharedJSON(groupId, PROFILES_KEY);
|
|
972
|
+
} catch {
|
|
973
|
+
return [];
|
|
974
|
+
}
|
|
975
|
+
if (!json) return [];
|
|
976
|
+
try {
|
|
977
|
+
const parsed = JSON.parse(json);
|
|
978
|
+
if (!Array.isArray(parsed)) return [];
|
|
979
|
+
const saved = new Set(profiles.map((profile) => profile.id));
|
|
980
|
+
return parsed.filter((entry) => !isValidProfile(entry) && hasId(entry) && !saved.has(entry.id));
|
|
981
|
+
} catch {
|
|
982
|
+
return [];
|
|
983
|
+
}
|
|
984
|
+
}
|
|
926
985
|
async function saveSharedProfiles(groupId, profiles) {
|
|
927
986
|
if (!native) return;
|
|
928
987
|
try {
|
|
929
|
-
await
|
|
988
|
+
const kept = await unreadableEntries(groupId, profiles);
|
|
989
|
+
await native.setSharedJSON(groupId, PROFILES_KEY, JSON.stringify([...profiles, ...kept]));
|
|
930
990
|
} catch {
|
|
931
991
|
}
|
|
932
992
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tastic/profile",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.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",
|
|
@@ -62,16 +62,17 @@
|
|
|
62
62
|
},
|
|
63
63
|
"prettier": "@infinitetoken/eslint-config/prettier",
|
|
64
64
|
"devDependencies": {
|
|
65
|
-
"@infinitetoken/eslint-config": "0.2.
|
|
65
|
+
"@infinitetoken/eslint-config": "0.2.1",
|
|
66
66
|
"@infinitetoken/jest-config": "^0.5.2",
|
|
67
67
|
"@infinitetoken/tsconfig": "^0.6.0",
|
|
68
|
-
"@rific/auto-paper": "^0.13.
|
|
69
|
-
"@rific/core": "^0.
|
|
70
|
-
"@rific/feedback-press": "^0.13.
|
|
68
|
+
"@rific/auto-paper": "^0.13.4",
|
|
69
|
+
"@rific/core": "^0.5.0",
|
|
70
|
+
"@rific/feedback-press": "^0.13.1",
|
|
71
71
|
"@rific/focus-chain": "^0.4.8",
|
|
72
|
-
"@rific/
|
|
72
|
+
"@rific/gestures": "^0.1.3",
|
|
73
|
+
"@rific/toaster": "^0.10.1",
|
|
73
74
|
"@tastic/core": "^0.14.0",
|
|
74
|
-
"@tastic/hud": "^0.
|
|
75
|
+
"@tastic/hud": "^0.25.1",
|
|
75
76
|
"@testing-library/dom": "^10.4.1",
|
|
76
77
|
"@testing-library/react": "^16.3.2",
|
|
77
78
|
"@types/jest": "^30.0.0",
|
|
@@ -97,7 +98,7 @@
|
|
|
97
98
|
"@rific/focus-chain": ">=0.4.0",
|
|
98
99
|
"@rific/toaster": ">=0.4.0",
|
|
99
100
|
"@tastic/core": ">=0.1.0",
|
|
100
|
-
"@tastic/hud": ">=0.
|
|
101
|
+
"@tastic/hud": ">=0.25.1",
|
|
101
102
|
"expo-modules-core": ">=1.0.0",
|
|
102
103
|
"react": ">=19.0.0",
|
|
103
104
|
"react-native": ">=0.76.0",
|
package/src/OptionPicker.tsx
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { PopoverHost, PopoverRotation } from '@tastic/hud/chrome'
|
|
2
|
+
import { Fragment } from 'react'
|
|
2
3
|
import { type StyleProp, type ViewStyle } from 'react-native'
|
|
3
4
|
|
|
4
|
-
import { IconRow, pickerContentSize, PickerMenu, type ProfilePickerAlignResult, type ProfilePickerColorSection, type ProfilePickerContentSize } from './PickerMenu'
|
|
5
|
+
import { IconRow, MenuDivider, pickerContentSize, PickerMenu, type ProfilePickerAlignResult, type ProfilePickerColorSection, type ProfilePickerContentSize } from './PickerMenu'
|
|
5
6
|
|
|
6
7
|
export interface OptionPickerOption<T extends string> {
|
|
7
8
|
value: T
|
|
@@ -11,14 +12,24 @@ export interface OptionPickerOption<T extends string> {
|
|
|
11
12
|
// one; the selected row still fills with the picker's `color`, its icon contrasting as always.
|
|
12
13
|
// Without it the icon takes the row's text color.
|
|
13
14
|
color?: string
|
|
15
|
+
// A divider above this row, setting it and the rows after it apart from the ones before as a
|
|
16
|
+
// different kind of choice (a person after a CPU's levels). Ignored on the first option, which the
|
|
17
|
+
// Color row's own divider, or nothing, already sits above.
|
|
18
|
+
dividerBefore?: boolean
|
|
14
19
|
}
|
|
15
20
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
function hasDividerBefore(option: OptionPickerOption<string>, index: number): boolean {
|
|
22
|
+
return index > 0 && !!option.dividerBefore
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// The popover's content footprint: one row per option and its dividers, plus (with a
|
|
26
|
+
// `colorSection`) the Color row, or the color page if that's taller - the same formula as
|
|
27
|
+
// getProfilePickerContentSize, for a host that places this picker itself through `alignOverride` (a
|
|
28
|
+
// @tastic/split-screen zone's useZoneClampedAlign, for one). Given the options themselves it counts
|
|
29
|
+
// their dividers too; given a count, rows only.
|
|
30
|
+
export function getOptionPickerContentSize(options: number | readonly OptionPickerOption<string>[], colorSection?: Pick<ProfilePickerColorSection, 'swatches' | 'caption'>): ProfilePickerContentSize {
|
|
31
|
+
if (typeof options === 'number') return pickerContentSize(options, colorSection)
|
|
32
|
+
return pickerContentSize(options.length, colorSection, options.filter(hasDividerBefore).length)
|
|
22
33
|
}
|
|
23
34
|
|
|
24
35
|
interface OptionPickerProps<T extends string> {
|
|
@@ -41,6 +52,8 @@ interface OptionPickerProps<T extends string> {
|
|
|
41
52
|
align?: 'left' | 'right' | 'center'
|
|
42
53
|
alignOverride?: ProfilePickerAlignResult
|
|
43
54
|
rotation?: PopoverRotation
|
|
55
|
+
// The pill's own style (a background), as ProfilePicker's; a `flexShrink` lets it give way in a
|
|
56
|
+
// narrow row, cutting its label off.
|
|
44
57
|
triggerStyle?: StyleProp<ViewStyle>
|
|
45
58
|
}
|
|
46
59
|
|
|
@@ -59,9 +72,12 @@ export function OptionPicker<T extends string>({ id, host, options, value, onCha
|
|
|
59
72
|
}
|
|
60
73
|
|
|
61
74
|
return (
|
|
62
|
-
<PickerMenu id={id} host={host} color={color} dark={dark} label={selected?.label ?? ''} chip={{ icon }} colorSection={colorSection} contentSize={getOptionPickerContentSize(options
|
|
63
|
-
{options.map((option) => (
|
|
64
|
-
<
|
|
75
|
+
<PickerMenu id={id} host={host} color={color} dark={dark} label={selected?.label ?? ''} chip={{ icon }} colorSection={colorSection} contentSize={getOptionPickerContentSize(options, colorSection)} align={align} alignOverride={alignOverride} rotation={rotation} triggerStyle={triggerStyle}>
|
|
76
|
+
{options.map((option, index) => (
|
|
77
|
+
<Fragment key={option.value}>
|
|
78
|
+
{hasDividerBefore(option, index) && <MenuDivider color={color} />}
|
|
79
|
+
<IconRow icon={option.icon ?? rowIcon} iconColor={option.color} label={option.label} selected={option.value === value} color={color} fg={fg} onPress={() => handleSelect(option)} />
|
|
80
|
+
</Fragment>
|
|
65
81
|
))}
|
|
66
82
|
</PickerMenu>
|
|
67
83
|
)
|
package/src/PickerMenu.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { defaultColors, FadingScrollView, getContrastColor, type SeedColor } from '@rific/auto-paper'
|
|
2
2
|
import { TouchableRipple } from '@rific/feedback-press'
|
|
3
|
-
import type { PopoverHost, PopoverRotation } from '@tastic/hud/chrome'
|
|
3
|
+
import type { PopoverHost, PopoverRotation, ViewInstance } from '@tastic/hud/chrome'
|
|
4
4
|
import { PopoverBody, useAutoAlign } from '@tastic/hud/chrome'
|
|
5
5
|
import { ReactNode, RefObject, useState } from 'react'
|
|
6
6
|
import { type LayoutChangeEvent, type StyleProp, StyleSheet, View, type ViewStyle } from 'react-native'
|
|
@@ -25,6 +25,15 @@ const ROW_INSET = 12
|
|
|
25
25
|
// The chip trigger (see `colorSection`): a CHIP_SIZE circle inside a pill padded by CHIP_TRIGGER_PADDING.
|
|
26
26
|
const CHIP_SIZE = 32
|
|
27
27
|
const CHIP_TRIGGER_PADDING = 6
|
|
28
|
+
const CHIP_TRIGGER_END_PADDING = CHIP_TRIGGER_PADDING + 8
|
|
29
|
+
const CHIP_TRIGGER_GAP = 8
|
|
30
|
+
const PLAIN_TRIGGER_GAP = 2
|
|
31
|
+
const CHEVRON_SIZE = 16
|
|
32
|
+
// Everything in a trigger but its label: the chip, the chevron, the gaps and the padding (84pt for the
|
|
33
|
+
// chip pill, 18 for the plain trigger). A shrinking trigger (see `triggerStyle`) stops here, so its
|
|
34
|
+
// chip and chevron always fit inside it.
|
|
35
|
+
const CHIP_TRIGGER_FIXED_WIDTH = CHIP_TRIGGER_PADDING + CHIP_SIZE + 2 * CHIP_TRIGGER_GAP + CHEVRON_SIZE + CHIP_TRIGGER_END_PADDING
|
|
36
|
+
const PLAIN_TRIGGER_FIXED_WIDTH = PLAIN_TRIGGER_GAP + CHEVRON_SIZE
|
|
28
37
|
|
|
29
38
|
// The color row and page (see `colorSection`). Every height here is fixed (explicit line heights),
|
|
30
39
|
// so pickerContentSize can size them exactly. Swatches are the same 28pt circles as
|
|
@@ -40,8 +49,9 @@ const COLOR_ROW_SWATCH_SIZE = 22
|
|
|
40
49
|
const COLOR_ROW_CAPTIONED_HEIGHT = 48
|
|
41
50
|
const COLOR_LABEL_LINE = 18
|
|
42
51
|
const COLOR_CAPTION_LINE = 16
|
|
43
|
-
//
|
|
44
|
-
|
|
52
|
+
// Every divider in the menu (under the Color row, over Manage, and between an OptionPicker's groups
|
|
53
|
+
// of options): 1pt plus 4pt above and below.
|
|
54
|
+
const DIVIDER_HEIGHT = 9
|
|
45
55
|
// The color page: a back row, then the grid with this much space above and below it.
|
|
46
56
|
const GRID_TOP = 4
|
|
47
57
|
const GRID_BOTTOM = 8
|
|
@@ -74,16 +84,16 @@ export interface ProfilePickerColorSection {
|
|
|
74
84
|
|
|
75
85
|
// A complete placement decision for the popover: which way it opens, how tall it may grow, whether
|
|
76
86
|
// this open's measurement has landed yet, and the ref attached to the measured trigger.
|
|
77
|
-
// Structurally identical to @tastic/hud's own `AlignResult` (
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
//
|
|
87
|
+
// Structurally identical to @tastic/hud's own `AlignResult` (the same shape `useAutoAlign` itself
|
|
88
|
+
// returns), and a hud `AlignResult` (e.g. from `useZoneClampedAlign`) is assignable to this as-is.
|
|
89
|
+
// The ref holds hud's `ViewInstance`, a View's instance under React Native's legacy types and its
|
|
90
|
+
// strict ones alike (a bare `RefObject<View>` is a ref to the View function under the strict ones).
|
|
81
91
|
export interface ProfilePickerAlignResult {
|
|
82
92
|
align: 'left' | 'right' | 'center'
|
|
83
93
|
verticalAlign: 'below' | 'above'
|
|
84
94
|
maxHeight: number
|
|
85
95
|
measured: boolean
|
|
86
|
-
triggerRef: RefObject<
|
|
96
|
+
triggerRef: RefObject<ViewInstance | null>
|
|
87
97
|
}
|
|
88
98
|
|
|
89
99
|
export interface ProfilePickerContentSize {
|
|
@@ -106,10 +116,11 @@ function colorPageHeight(swatches: SeedColor[]) {
|
|
|
106
116
|
// pages, exactly: the list with the Color row and its divider on top, or the color page (a back row
|
|
107
117
|
// and the swatches), so one placement fits whichever page is showing. It leaves out the menu's 2pt
|
|
108
118
|
// border and the Manage divider, ~13pt in all, so treat it as a placement estimate, not a
|
|
109
|
-
// pixel-exact measurement.
|
|
110
|
-
//
|
|
111
|
-
|
|
112
|
-
|
|
119
|
+
// pixel-exact measurement. `dividerCount` is the dividers between the rows (an OptionPicker's
|
|
120
|
+
// `dividerBefore`). getProfilePickerContentSize and getOptionPickerContentSize count the rows for
|
|
121
|
+
// their own picker.
|
|
122
|
+
export function pickerContentSize(rowCount: number, colorSection?: Pick<ProfilePickerColorSection, 'swatches' | 'caption'>, dividerCount = 0): ProfilePickerContentSize {
|
|
123
|
+
const list = rowCount * ROW_HEIGHT + dividerCount * DIVIDER_HEIGHT + (colorSection ? colorRowHeight(colorSection.caption) + DIVIDER_HEIGHT : 0)
|
|
113
124
|
const content = colorSection ? Math.max(list, colorPageHeight(colorSection.swatches ?? defaultColors)) : list
|
|
114
125
|
return { width: POPOVER_WIDTH, height: LIST_PADDING * 2 + content }
|
|
115
126
|
}
|
|
@@ -134,6 +145,8 @@ interface PickerMenuProps {
|
|
|
134
145
|
align?: 'left' | 'right' | 'center'
|
|
135
146
|
alignOverride?: ProfilePickerAlignResult
|
|
136
147
|
rotation: PopoverRotation
|
|
148
|
+
// The pressable trigger's own style (a host's background behind the pill). A `flexShrink` in it lets
|
|
149
|
+
// the trigger give way in the host's row: see `shrinks` below.
|
|
137
150
|
triggerStyle?: StyleProp<ViewStyle>
|
|
138
151
|
// Pinned under the list page's rows, behind a divider (ProfilePicker's Manage row).
|
|
139
152
|
footer?: ReactNode
|
|
@@ -172,9 +185,21 @@ export function PickerMenu({ id, host, color, dark, label, chip, colorSection, h
|
|
|
172
185
|
// Shown on the list page only (see the footer below).
|
|
173
186
|
const showFooter = !!footer && !showColorPage
|
|
174
187
|
|
|
188
|
+
// A host's `flexShrink` is how far the trigger gives way in the host's row, so it goes on the anchor,
|
|
189
|
+
// which is the host's flex item (on the pressable it would only squeeze the pill's height), down to
|
|
190
|
+
// the trigger's fixed parts. The pressable is then capped at the anchor's width, so its label is cut
|
|
191
|
+
// off with an ellipsis and the chip and chevron keep their size. With room, the anchor and the pill
|
|
192
|
+
// are their natural width, as before. The pill's edges, and so the measured trigger, the caret and
|
|
193
|
+
// the popover's alignment, follow the shrunk width. Without a flexShrink nothing here changes.
|
|
194
|
+
const flatTriggerStyle: ViewStyle = StyleSheet.flatten(triggerStyle) ?? {}
|
|
195
|
+
const { flexShrink, ...pillStyle } = flatTriggerStyle
|
|
196
|
+
const shrinks = typeof flexShrink === 'number' && flexShrink > 0
|
|
197
|
+
const anchorShrink = shrinks ? { flexShrink, minWidth: chipTrigger ? CHIP_TRIGGER_FIXED_WIDTH : PLAIN_TRIGGER_FIXED_WIDTH } : undefined
|
|
198
|
+
|
|
175
199
|
return (
|
|
176
|
-
<View style={[styles.anchor, open && styles.anchorOpen]} onLayout={handleAnchorLayout}>
|
|
177
|
-
|
|
200
|
+
<View style={[styles.anchor, anchorShrink, open && styles.anchorOpen]} onLayout={handleAnchorLayout}>
|
|
201
|
+
{/* The cap goes before the host's style, so a host's own maxWidth still wins. */}
|
|
202
|
+
<TouchableRipple onPress={handleTriggerPress} borderless={chipTrigger} style={[styles.trigger, chipTrigger && styles.chipTrigger, shrinks && styles.triggerShrinks, shrinks ? pillStyle : triggerStyle]} accessibilityRole='button' accessibilityLabel={triggerLabel}>
|
|
178
203
|
<View ref={triggerRef} collapsable={false} style={chipTrigger ? styles.chipTriggerInner : styles.triggerInner}>
|
|
179
204
|
{chip && <IdentityChip color={color} tag={chip.tag} icon={chip.icon} size={CHIP_SIZE} />}
|
|
180
205
|
{/* The chip trigger's name and chevron are plain fg: the chip carries the color, and a muted
|
|
@@ -183,7 +208,7 @@ export function PickerMenu({ id, host, color, dark, label, chip, colorSection, h
|
|
|
183
208
|
<Text style={[styles.triggerLabel, { color: chipTrigger ? fg : color }]} numberOfLines={1}>
|
|
184
209
|
{triggerLabel}
|
|
185
210
|
</Text>
|
|
186
|
-
<Icon source='menu-down' size={
|
|
211
|
+
<Icon source='menu-down' size={CHEVRON_SIZE} color={chipTrigger ? fg : color} />
|
|
187
212
|
</View>
|
|
188
213
|
</TouchableRipple>
|
|
189
214
|
|
|
@@ -219,7 +244,7 @@ export function PickerMenu({ id, host, color, dark, label, chip, colorSection, h
|
|
|
219
244
|
below. Not on the color page, which only has the back row and the swatches. */}
|
|
220
245
|
{showFooter && (
|
|
221
246
|
<View style={styles.menuFooter}>
|
|
222
|
-
<
|
|
247
|
+
<MenuDivider color={color} />
|
|
223
248
|
{footer}
|
|
224
249
|
</View>
|
|
225
250
|
)}
|
|
@@ -292,11 +317,16 @@ function ColorRow({ section, fg, dividerColor, onPress }: { section: ProfilePick
|
|
|
292
317
|
<Icon source='chevron-right' size={18} color={fg} />
|
|
293
318
|
</View>
|
|
294
319
|
</TouchableRipple>
|
|
295
|
-
<
|
|
320
|
+
<MenuDivider color={dividerColor} />
|
|
296
321
|
</>
|
|
297
322
|
)
|
|
298
323
|
}
|
|
299
324
|
|
|
325
|
+
// A line across the list between kinds of rows (see `divider` below), in the seat's own color.
|
|
326
|
+
export function MenuDivider({ color }: { color: string }) {
|
|
327
|
+
return <View style={[styles.divider, { backgroundColor: color }]} />
|
|
328
|
+
}
|
|
329
|
+
|
|
300
330
|
// The color page: a back row to the list, then the swatches. `homeColor` is the selected profile's
|
|
301
331
|
// own color: its swatch carries a dot, so a player who has picked something else can find their way
|
|
302
332
|
// back to it (the profile's row shows that color, but matching it to one of two similar swatches by
|
|
@@ -395,10 +425,10 @@ const styles = StyleSheet.create({
|
|
|
395
425
|
chipTriggerInner: {
|
|
396
426
|
alignItems: 'center',
|
|
397
427
|
flexDirection: 'row',
|
|
398
|
-
gap:
|
|
428
|
+
gap: CHIP_TRIGGER_GAP,
|
|
399
429
|
height: CHIP_SIZE + 2 * CHIP_TRIGGER_PADDING,
|
|
400
430
|
paddingLeft: CHIP_TRIGGER_PADDING,
|
|
401
|
-
paddingRight:
|
|
431
|
+
paddingRight: CHIP_TRIGGER_END_PADDING
|
|
402
432
|
},
|
|
403
433
|
colorRowCaption: {
|
|
404
434
|
fontSize: 12,
|
|
@@ -420,7 +450,8 @@ const styles = StyleSheet.create({
|
|
|
420
450
|
flex: 1
|
|
421
451
|
},
|
|
422
452
|
// Separates the footer (a navigation action) and the Color row from the selectable rows - a
|
|
423
|
-
// different kind of action, not just another row in the same list
|
|
453
|
+
// different kind of action, not just another row in the same list - and an OptionPicker's groups
|
|
454
|
+
// of options from each other (its `dividerBefore`). Tinted with the seat's own color
|
|
424
455
|
// (passed in as backgroundColor) rather than a flat gray, matching the border/caret's own
|
|
425
456
|
// accent-tinted look.
|
|
426
457
|
divider: {
|
|
@@ -507,12 +538,21 @@ const styles = StyleSheet.create({
|
|
|
507
538
|
triggerInner: {
|
|
508
539
|
alignItems: 'center',
|
|
509
540
|
flexDirection: 'row',
|
|
510
|
-
gap:
|
|
541
|
+
gap: PLAIN_TRIGGER_GAP,
|
|
511
542
|
height: TRIGGER_HEIGHT
|
|
512
543
|
},
|
|
544
|
+
// Lets the label, alone in its row, give way to a trigger narrower than its name (see `shrinks`).
|
|
545
|
+
// With room it keeps its natural width, so this changes nothing there.
|
|
513
546
|
triggerLabel: {
|
|
547
|
+
flexShrink: 1,
|
|
514
548
|
fontSize: 13,
|
|
515
549
|
fontWeight: '700',
|
|
516
|
-
letterSpacing: 1
|
|
550
|
+
letterSpacing: 1,
|
|
551
|
+
minWidth: 0
|
|
552
|
+
},
|
|
553
|
+
// A shrinking trigger (see `shrinks`): no wider than its anchor. Its row is then laid out at that
|
|
554
|
+
// width, which is what cuts the label off.
|
|
555
|
+
triggerShrinks: {
|
|
556
|
+
maxWidth: '100%'
|
|
517
557
|
}
|
|
518
558
|
})
|
|
@@ -29,8 +29,8 @@ interface ProfileColorProviderProps {
|
|
|
29
29
|
// where it is (ProfileSwatchPlace) and kept flat where it returns null. Hangman draws a pie of each
|
|
30
30
|
// color's triad on the Color row and page only. The renderer is drawn behind the swatch's mark,
|
|
31
31
|
// clipped to the circle.
|
|
32
|
-
//
|
|
33
|
-
//
|
|
32
|
+
// ProfilesManager hands it to hud's InlineColorPicker too (its trigger as a 'chip', its grid as the
|
|
33
|
+
// 'colorPage'). Rows that fill with a color (a selected picker row) stay flat.
|
|
34
34
|
renderSwatch?: ProfileSwatchRenderer
|
|
35
35
|
children: ReactNode
|
|
36
36
|
}
|