@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/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 COLOR_ROW_DIVIDER = 9;
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) + COLOR_ROW_DIVIDER : 0);
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
- return /* @__PURE__ */ jsxs2(View3, { style: [styles3.anchor, open && styles3.anchorOpen], onLayout: handleAnchorLayout, children: [
138
- /* @__PURE__ */ jsx4(TouchableRipple, { onPress: handleTriggerPress, borderless: chipTrigger, style: [styles3.trigger, chipTrigger && styles3.chipTrigger, triggerStyle], accessibilityRole: "button", accessibilityLabel: triggerLabel, children: /* @__PURE__ */ jsxs2(View3, { ref: triggerRef, collapsable: false, style: chipTrigger ? styles3.chipTriggerInner : styles3.triggerInner, children: [
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: 16, color: chipTrigger ? fg : color })
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(View3, { style: [styles3.divider, { backgroundColor: color }] }),
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(View3, { style: [styles3.divider, { backgroundColor: dividerColor }] })
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: 8,
275
+ gap: CHIP_TRIGGER_GAP,
260
276
  height: CHIP_SIZE + 2 * CHIP_TRIGGER_PADDING,
261
277
  paddingLeft: CHIP_TRIGGER_PADDING,
262
- paddingRight: CHIP_TRIGGER_PADDING + 8
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. Tinted with the seat's own color
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: 2,
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 getOptionPickerContentSize(optionCount, colorSection) {
384
- return pickerContentSize(optionCount, colorSection);
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.length, colorSection), align, alignOverride, rotation, triggerStyle, children: options.map((option) => /* @__PURE__ */ jsx5(IconRow, { icon: option.icon ?? rowIcon, iconColor: option.color, label: option.label, selected: option.value === value, color, fg, onPress: () => handleSelect(option) }, option.value)) });
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 jsxs3 } from "react/jsx-runtime";
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__ */ jsxs3(View4, { style: styles4.manageRowInner, children: [
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__ */ jsxs3(PickerMenu, { id, host, color, dark, label: selectedProfile?.name ?? guestLabel, chip: colorSection ? { tag: selectedProfile?.tag, icon: selectedProfile ? "account" : nullIcon } : void 0, colorSection, homeColor: selectedProfile?.color, contentSize, align, alignOverride, rotation, triggerStyle, footer: manageFooter, children: [
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__ */ jsxs3(View4, { style: [pickerMenuStyles.rowInner, taken && pickerMenuStyles.rowDisabled], children: [
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 Fragment2, jsx as jsx8, jsxs as jsxs4 } from "react/jsx-runtime";
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__ */ jsxs4(Fragment2, { children: [
602
- /* @__PURE__ */ jsxs4(View5, { style: [styles5.header, { paddingTop: 8 + insets.top, paddingLeft: 8 + insets.left }], children: [
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__ */ jsxs4(View5, { style: styles5.rowInner, children: [
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__ */ jsxs4(View5, { style: styles5.rowInner, children: [
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__ */ jsxs4(View5, { style: styles5.editRow, children: [
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 native.setSharedJSON(groupId, PROFILES_KEY, JSON.stringify(profiles));
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.16.2",
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.0",
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.3",
69
- "@rific/core": "^0.4.0",
70
- "@rific/feedback-press": "^0.13.0",
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/toaster": "^0.8.3",
72
+ "@rific/gestures": "^0.1.3",
73
+ "@rific/toaster": "^0.10.1",
73
74
  "@tastic/core": "^0.14.0",
74
- "@tastic/hud": "^0.24.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.23.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",
@@ -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
- // The popover's content footprint: one row per option, plus (with a `colorSection`) the Color row,
17
- // or the color page if that's taller - the same formula as getProfilePickerContentSize, for a host
18
- // that places this picker itself through `alignOverride` (a @tastic/split-screen zone's
19
- // useZoneClampedAlign, for one).
20
- export function getOptionPickerContentSize(optionCount: number, colorSection?: Pick<ProfilePickerColorSection, 'swatches' | 'caption'>): ProfilePickerContentSize {
21
- return pickerContentSize(optionCount, colorSection)
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.length, colorSection)} align={align} alignOverride={alignOverride} rotation={rotation} triggerStyle={triggerStyle}>
63
- {options.map((option) => (
64
- <IconRow key={option.value} icon={option.icon ?? rowIcon} iconColor={option.color} label={option.label} selected={option.value === value} color={color} fg={fg} onPress={() => handleSelect(option)} />
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
  )
@@ -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
- // The divider under the Color row: 1pt plus 4pt above and below, the same as the Manage divider.
44
- const COLOR_ROW_DIVIDER = 9
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` (exported from hud >= 0.12.0, and the
78
- // same shape `useAutoAlign` itself returns) - declared here rather than imported so this package's
79
- // public types don't start requiring a newer @tastic/hud than its peer range already admits. A
80
- // hud `AlignResult` (e.g. from `useZoneClampedAlign`) is assignable to this as-is.
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<View | null>
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. getProfilePickerContentSize and getOptionPickerContentSize count the
110
- // rows for their own picker.
111
- export function pickerContentSize(rowCount: number, colorSection?: Pick<ProfilePickerColorSection, 'swatches' | 'caption'>): ProfilePickerContentSize {
112
- const list = rowCount * ROW_HEIGHT + (colorSection ? colorRowHeight(colorSection.caption) + COLOR_ROW_DIVIDER : 0)
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
- <TouchableRipple onPress={handleTriggerPress} borderless={chipTrigger} style={[styles.trigger, chipTrigger && styles.chipTrigger, triggerStyle]} accessibilityRole='button' accessibilityLabel={triggerLabel}>
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={16} color={chipTrigger ? fg : color} />
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
- <View style={[styles.divider, { backgroundColor: color }]} />
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
- <View style={[styles.divider, { backgroundColor: dividerColor }]} />
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: 8,
428
+ gap: CHIP_TRIGGER_GAP,
399
429
  height: CHIP_SIZE + 2 * CHIP_TRIGGER_PADDING,
400
430
  paddingLeft: CHIP_TRIGGER_PADDING,
401
- paddingRight: CHIP_TRIGGER_PADDING + 8
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. Tinted with the seat's own color
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: 2,
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
- // Rows that fill with a color (a selected picker row) stay flat, and so does hud's InlineColorPicker
33
- // (ProfilesManager's color trigger and swatches), which doesn't read this context.
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
  }