@tastic/profile 0.17.0 → 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.d.mts CHANGED
@@ -31,8 +31,9 @@ interface OptionPickerOption<T extends string> {
31
31
  label: string;
32
32
  icon?: string;
33
33
  color?: string;
34
+ dividerBefore?: boolean;
34
35
  }
35
- declare function getOptionPickerContentSize(optionCount: number, colorSection?: Pick<ProfilePickerColorSection, 'swatches' | 'caption'>): ProfilePickerContentSize;
36
+ declare function getOptionPickerContentSize(options: number | readonly OptionPickerOption<string>[], colorSection?: Pick<ProfilePickerColorSection, 'swatches' | 'caption'>): ProfilePickerContentSize;
36
37
  interface OptionPickerProps<T extends string> {
37
38
  id: string;
38
39
  host: PopoverHost;
package/dist/index.d.ts CHANGED
@@ -31,8 +31,9 @@ interface OptionPickerOption<T extends string> {
31
31
  label: string;
32
32
  icon?: string;
33
33
  color?: string;
34
+ dividerBefore?: boolean;
34
35
  }
35
- declare function getOptionPickerContentSize(optionCount: number, colorSection?: Pick<ProfilePickerColorSection, 'swatches' | 'caption'>): ProfilePickerContentSize;
36
+ declare function getOptionPickerContentSize(options: number | readonly OptionPickerOption<string>[], colorSection?: Pick<ProfilePickerColorSection, 'swatches' | 'caption'>): ProfilePickerContentSize;
36
37
  interface OptionPickerProps<T extends string> {
37
38
  id: string;
38
39
  host: PopoverHost;
package/dist/index.js CHANGED
@@ -53,6 +53,9 @@ __export(index_exports, {
53
53
  });
54
54
  module.exports = __toCommonJS(index_exports);
55
55
 
56
+ // src/OptionPicker.tsx
57
+ var import_react3 = require("react");
58
+
56
59
  // src/PickerMenu.tsx
57
60
  var import_auto_paper2 = require("@rific/auto-paper");
58
61
  var import_feedback_press = require("@rific/feedback-press");
@@ -151,7 +154,7 @@ var COLOR_ROW_SWATCH_SIZE = 22;
151
154
  var COLOR_ROW_CAPTIONED_HEIGHT = 48;
152
155
  var COLOR_LABEL_LINE = 18;
153
156
  var COLOR_CAPTION_LINE = 16;
154
- var COLOR_ROW_DIVIDER = 9;
157
+ var DIVIDER_HEIGHT = 9;
155
158
  var GRID_TOP = 4;
156
159
  var GRID_BOTTOM = 8;
157
160
  var GRID_WIDTH = POPOVER_WIDTH - 2 * MENU_BORDER_WIDTH - 2 * LIST_PADDING - 2 * ROW_INSET;
@@ -164,8 +167,8 @@ function colorPageHeight(swatches) {
164
167
  const rows = Math.ceil(swatches.length / GRID_COLUMNS);
165
168
  return ROW_HEIGHT + GRID_TOP + rows * SWATCH_SIZE + (rows - 1) * GRID_GAP + GRID_BOTTOM;
166
169
  }
167
- function pickerContentSize(rowCount, colorSection) {
168
- const list = rowCount * ROW_HEIGHT + (colorSection ? colorRowHeight(colorSection.caption) + COLOR_ROW_DIVIDER : 0);
170
+ function pickerContentSize(rowCount, colorSection, dividerCount = 0) {
171
+ const list = rowCount * ROW_HEIGHT + dividerCount * DIVIDER_HEIGHT + (colorSection ? colorRowHeight(colorSection.caption) + DIVIDER_HEIGHT : 0);
169
172
  const content = colorSection ? Math.max(list, colorPageHeight(colorSection.swatches ?? import_auto_paper2.defaultColors)) : list;
170
173
  return { width: POPOVER_WIDTH, height: LIST_PADDING * 2 + content };
171
174
  }
@@ -216,7 +219,7 @@ function PickerMenu({ id, host, color, dark, label, chip, colorSection, homeColo
216
219
  children
217
220
  ] }) }, showColorPage ? "color" : "list"),
218
221
  showFooter && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native3.View, { style: styles3.menuFooter, children: [
219
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native3.View, { style: [styles3.divider, { backgroundColor: color }] }),
222
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(MenuDivider, { color }),
220
223
  footer
221
224
  ] })
222
225
  ]
@@ -245,9 +248,12 @@ function ColorRow({ section, fg, dividerColor, onPress }) {
245
248
  ] }),
246
249
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native_paper2.Icon, { source: "chevron-right", size: 18, color: fg })
247
250
  ] }) }),
248
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native3.View, { style: [styles3.divider, { backgroundColor: dividerColor }] })
251
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(MenuDivider, { color: dividerColor })
249
252
  ] });
250
253
  }
254
+ function MenuDivider({ color }) {
255
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native3.View, { style: [styles3.divider, { backgroundColor: color }] });
256
+ }
251
257
  function ColorPage({ section, homeColor, fg, onBack }) {
252
258
  const { value, onChange, previewValue, swatches = import_auto_paper2.defaultColors, label = "Color", takenValue, allowSwapTaken } = section;
253
259
  const renderSwatch = useProfileSwatchRenderer();
@@ -339,7 +345,8 @@ var styles3 = import_react_native3.StyleSheet.create({
339
345
  flex: 1
340
346
  },
341
347
  // Separates the footer (a navigation action) and the Color row from the selectable rows - a
342
- // different kind of action, not just another row in the same list. Tinted with the seat's own color
348
+ // different kind of action, not just another row in the same list - and an OptionPicker's groups
349
+ // of options from each other (its `dividerBefore`). Tinted with the seat's own color
343
350
  // (passed in as backgroundColor) rather than a flat gray, matching the border/caret's own
344
351
  // accent-tinted look.
345
352
  divider: {
@@ -447,8 +454,12 @@ var styles3 = import_react_native3.StyleSheet.create({
447
454
 
448
455
  // src/OptionPicker.tsx
449
456
  var import_jsx_runtime5 = require("react/jsx-runtime");
450
- function getOptionPickerContentSize(optionCount, colorSection) {
451
- return pickerContentSize(optionCount, colorSection);
457
+ function hasDividerBefore(option, index) {
458
+ return index > 0 && !!option.dividerBefore;
459
+ }
460
+ function getOptionPickerContentSize(options, colorSection) {
461
+ if (typeof options === "number") return pickerContentSize(options, colorSection);
462
+ return pickerContentSize(options.length, colorSection, options.filter(hasDividerBefore).length);
452
463
  }
453
464
  function OptionPicker({ id, host, options, value, onChange, color, dark, icon, rowIcon = icon, colorSection, align, alignOverride, rotation = 0, triggerStyle }) {
454
465
  const fg = dark ? "#FFFFFF" : "#000000";
@@ -457,7 +468,10 @@ function OptionPicker({ id, host, options, value, onChange, color, dark, icon, r
457
468
  onChange(option.value);
458
469
  host.close();
459
470
  };
460
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(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__ */ (0, import_jsx_runtime5.jsx)(IconRow, { icon: option.icon ?? rowIcon, iconColor: option.color, label: option.label, selected: option.value === value, color, fg, onPress: () => handleSelect(option) }, option.value)) });
471
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(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__ */ (0, import_jsx_runtime5.jsxs)(import_react3.Fragment, { children: [
472
+ hasDividerBefore(option, index) && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(MenuDivider, { color }),
473
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(IconRow, { icon: option.icon ?? rowIcon, iconColor: option.color, label: option.label, selected: option.value === value, color, fg, onPress: () => handleSelect(option) })
474
+ ] }, option.value)) });
461
475
  }
462
476
 
463
477
  // src/ProfileChip.tsx
@@ -470,7 +484,7 @@ function ProfileChip({ profile, size = 22, filled = true }) {
470
484
  // src/ProfilePicker.tsx
471
485
  var import_auto_paper3 = require("@rific/auto-paper");
472
486
  var import_feedback_press2 = require("@rific/feedback-press");
473
- var import_react3 = require("react");
487
+ var import_react4 = require("react");
474
488
  var import_react_native4 = require("react-native");
475
489
  var import_react_native_paper3 = require("react-native-paper");
476
490
  var import_jsx_runtime7 = require("react/jsx-runtime");
@@ -482,7 +496,7 @@ function ProfilePicker({ idPrefix, host, profiles, selectedId, takenId, takenIds
482
496
  const colorPreview = useProfileColorPreview();
483
497
  const fg = dark ? "#FFFFFF" : "#000000";
484
498
  const selectedProfile = profiles.find((p) => p.id === selectedId) ?? null;
485
- const sortedProfiles = (0, import_react3.useMemo)(() => [...profiles].sort((a, b) => a.name.localeCompare(b.name)), [profiles]);
499
+ const sortedProfiles = (0, import_react4.useMemo)(() => [...profiles].sort((a, b) => a.name.localeCompare(b.name)), [profiles]);
486
500
  const contentSize = getProfilePickerContentSize(profiles.length, !!onManage, colorSection);
487
501
  const isTaken = (profileId) => profileId === takenId || profileId !== selectedId && !!takenIds?.includes(profileId);
488
502
  const handleSelectGuest = () => {
@@ -550,7 +564,7 @@ var import_feedback_press3 = require("@rific/feedback-press");
550
564
  var import_focus_chain = require("@rific/focus-chain");
551
565
  var import_toaster = require("@rific/toaster");
552
566
  var import_chrome2 = require("@tastic/hud/chrome");
553
- var import_react4 = require("react");
567
+ var import_react5 = require("react");
554
568
  var import_react_native5 = require("react-native");
555
569
  var import_react_native_paper4 = require("react-native-paper");
556
570
  var import_react_native_safe_area_context = require("react-native-safe-area-context");
@@ -601,18 +615,18 @@ function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, h
601
615
  const { dark } = (0, import_auto_paper4.useAutoPaperTheme)();
602
616
  const insets = (0, import_react_native_safe_area_context.useSafeAreaInsets)();
603
617
  const { error: showErrorToast } = (0, import_toaster.useToast)();
604
- const columnRef = (0, import_react4.useRef)(null);
618
+ const columnRef = (0, import_react5.useRef)(null);
605
619
  const renderSwatch = useProfileSwatchRenderer();
606
620
  const host = (0, import_chrome2.usePopoverHost)();
607
621
  const fg = fgOverride ?? (dark ? "#FFFFFF" : "#000000");
608
622
  const fgMuted = fgMutedOverride ?? (dark ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.5)");
609
- const [editingId, setEditingId] = (0, import_react4.useState)(null);
610
- const [draftName, setDraftName] = (0, import_react4.useState)("");
611
- const [draftColor, setDraftColor] = (0, import_react4.useState)(defaultColor);
612
- const [draftTag, setDraftTag] = (0, import_react4.useState)("");
613
- const [tagManuallySet, setTagManuallySet] = (0, import_react4.useState)(false);
614
- const [confirmDeleteId, setConfirmDeleteId] = (0, import_react4.useState)(null);
615
- const commitEdit = (0, import_react4.useCallback)(() => {
623
+ const [editingId, setEditingId] = (0, import_react5.useState)(null);
624
+ const [draftName, setDraftName] = (0, import_react5.useState)("");
625
+ const [draftColor, setDraftColor] = (0, import_react5.useState)(defaultColor);
626
+ const [draftTag, setDraftTag] = (0, import_react5.useState)("");
627
+ const [tagManuallySet, setTagManuallySet] = (0, import_react5.useState)(false);
628
+ const [confirmDeleteId, setConfirmDeleteId] = (0, import_react5.useState)(null);
629
+ const commitEdit = (0, import_react5.useCallback)(() => {
616
630
  const name = draftName.trim();
617
631
  if (editingId && name) {
618
632
  const tagValid = isValidTag(draftTag);
@@ -623,13 +637,13 @@ function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, h
623
637
  }
624
638
  setEditingId(null);
625
639
  }, [editingId, draftName, draftColor, draftTag, onCreate, onSave, showErrorToast]);
626
- const commitEditRef = (0, import_react4.useRef)(commitEdit);
627
- (0, import_react4.useEffect)(() => {
640
+ const commitEditRef = (0, import_react5.useRef)(commitEdit);
641
+ (0, import_react5.useEffect)(() => {
628
642
  commitEditRef.current = commitEdit;
629
643
  });
630
- (0, import_react4.useEffect)(() => () => commitEditRef.current(), []);
631
- (0, import_react4.useImperativeHandle)(ref, () => ({ commitPendingEdit: () => commitEditRef.current() }), []);
632
- const startEdit = (0, import_react4.useCallback)(
644
+ (0, import_react5.useEffect)(() => () => commitEditRef.current(), []);
645
+ (0, import_react5.useImperativeHandle)(ref, () => ({ commitPendingEdit: () => commitEditRef.current() }), []);
646
+ const startEdit = (0, import_react5.useCallback)(
633
647
  (target) => {
634
648
  commitEdit();
635
649
  if (target === "new") {
@@ -647,26 +661,26 @@ function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, h
647
661
  },
648
662
  [commitEdit, defaultColor]
649
663
  );
650
- const handleNameChange = (0, import_react4.useCallback)(
664
+ const handleNameChange = (0, import_react5.useCallback)(
651
665
  (name) => {
652
666
  setDraftName(name);
653
667
  if (!tagManuallySet) setDraftTag(deriveTag(name));
654
668
  },
655
669
  [tagManuallySet]
656
670
  );
657
- const handleTagChange = (0, import_react4.useCallback)((tag) => {
671
+ const handleTagChange = (0, import_react5.useCallback)((tag) => {
658
672
  setDraftTag((previous) => clampTag(previous, tag));
659
673
  setTagManuallySet(true);
660
674
  }, []);
661
675
  const confirmDeleteProfile = profiles.find((p) => p.id === confirmDeleteId) ?? null;
662
- const handleConfirmDelete = (0, import_react4.useCallback)(() => {
676
+ const handleConfirmDelete = (0, import_react5.useCallback)(() => {
663
677
  if (confirmDeleteId) {
664
678
  onDelete(confirmDeleteId);
665
679
  if (editingId === confirmDeleteId) setEditingId(null);
666
680
  }
667
681
  setConfirmDeleteId(null);
668
682
  }, [confirmDeleteId, editingId, onDelete]);
669
- const sortedProfiles = (0, import_react4.useMemo)(() => [...profiles].sort((a, b) => a.name.localeCompare(b.name)), [profiles]);
683
+ const sortedProfiles = (0, import_react5.useMemo)(() => [...profiles].sort((a, b) => a.name.localeCompare(b.name)), [profiles]);
670
684
  const rows = [...sortedProfiles.map((profile) => ({ kind: "profile", profile })), { kind: "new" }];
671
685
  return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
672
686
  /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_react_native5.View, { style: [styles5.header, { paddingTop: 8 + insets.top, paddingLeft: 8 + insets.left }], children: [
@@ -702,15 +716,15 @@ function AlignedColorPicker({ useAlign, column, ...props }) {
702
716
  return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_chrome2.InlineColorPicker, { ...props, alignOverride });
703
717
  }
704
718
  function EditRow({ draftName, onNameChange, draftColor, onColorChange, draftTag, onTagChange, onSubmit, onDelete, autoFocus, fgMuted, host, dark, colorPreview, useColorPickerAlign, column, renderSwatch }) {
705
- const pickerSwatch = (0, import_react4.useMemo)(() => renderSwatch ? (color, size, place) => renderSwatch(color, size, place === "trigger" ? "chip" : "colorPage") : void 0, [renderSwatch]);
719
+ const pickerSwatch = (0, import_react5.useMemo)(() => renderSwatch ? (color, size, place) => renderSwatch(color, size, place === "trigger" ? "chip" : "colorPage") : void 0, [renderSwatch]);
706
720
  const register = (0, import_focus_chain.useFocusChain)();
707
721
  const tag = register();
708
722
  const name = register();
709
- const tagFocusRef = (0, import_react4.useRef)(tag.props.focus);
710
- (0, import_react4.useEffect)(() => {
723
+ const tagFocusRef = (0, import_react5.useRef)(tag.props.focus);
724
+ (0, import_react5.useEffect)(() => {
711
725
  tagFocusRef.current = tag.props.focus;
712
726
  });
713
- (0, import_react4.useEffect)(() => {
727
+ (0, import_react5.useEffect)(() => {
714
728
  if (host.openId === "color") tagFocusRef.current();
715
729
  }, [host.openId]);
716
730
  return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_react_native5.View, { style: styles5.editRow, children: [
@@ -805,12 +819,12 @@ var styles5 = import_react_native5.StyleSheet.create({
805
819
  // src/ProfilesScreen.tsx
806
820
  var import_auto_paper5 = require("@rific/auto-paper");
807
821
  var import_feedback_press4 = require("@rific/feedback-press");
808
- var import_react5 = require("react");
822
+ var import_react6 = require("react");
809
823
  var import_react_native6 = require("react-native");
810
824
  var import_jsx_runtime9 = require("react/jsx-runtime");
811
825
  function ProfilesScreen({ profiles, defaultColor, onCreate, onSave, onDelete, onBack, fg: fgOverride, fgMuted, bg: bgOverride, titleVariant, colorPreview, useColorPickerAlign }) {
812
826
  const { dark } = (0, import_auto_paper5.useAutoPaperTheme)();
813
- const managerRef = (0, import_react5.useRef)(null);
827
+ const managerRef = (0, import_react6.useRef)(null);
814
828
  const fg = fgOverride ?? (dark ? "#FFFFFF" : "#000000");
815
829
  const bg = bgOverride ?? (dark ? "#000000" : "#FFFFFF");
816
830
  const handleBack = () => {
@@ -1102,15 +1116,15 @@ function createSeatColorsSlice(namespace, defaultState, mountKey = "seatColors")
1102
1116
  }
1103
1117
 
1104
1118
  // src/redux/useSharedProfilesSync.ts
1105
- var import_react6 = require("react");
1119
+ var import_react7 = require("react");
1106
1120
  var import_react_native8 = require("react-native");
1107
1121
  function useSharedProfilesSync({ groupId, onRemoteChange }) {
1108
- const pendingWriteRef = (0, import_react6.useRef)(false);
1109
- const onRemoteChangeRef = (0, import_react6.useRef)(onRemoteChange);
1110
- (0, import_react6.useEffect)(() => {
1122
+ const pendingWriteRef = (0, import_react7.useRef)(false);
1123
+ const onRemoteChangeRef = (0, import_react7.useRef)(onRemoteChange);
1124
+ (0, import_react7.useEffect)(() => {
1111
1125
  onRemoteChangeRef.current = onRemoteChange;
1112
1126
  }, [onRemoteChange]);
1113
- (0, import_react6.useEffect)(() => {
1127
+ (0, import_react7.useEffect)(() => {
1114
1128
  if (!isSharedProfileStoreAvailable) return;
1115
1129
  let cancelled = false;
1116
1130
  const sub = import_react_native8.AppState.addEventListener("change", (next) => {
@@ -1128,7 +1142,7 @@ function useSharedProfilesSync({ groupId, onRemoteChange }) {
1128
1142
  unsubscribe();
1129
1143
  };
1130
1144
  }, [groupId]);
1131
- const syncToShared = (0, import_react6.useCallback)(
1145
+ const syncToShared = (0, import_react7.useCallback)(
1132
1146
  (next) => {
1133
1147
  if (!isSharedProfileStoreAvailable) return;
1134
1148
  pendingWriteRef.current = true;
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";
@@ -103,7 +106,7 @@ var COLOR_ROW_SWATCH_SIZE = 22;
103
106
  var COLOR_ROW_CAPTIONED_HEIGHT = 48;
104
107
  var COLOR_LABEL_LINE = 18;
105
108
  var COLOR_CAPTION_LINE = 16;
106
- var COLOR_ROW_DIVIDER = 9;
109
+ var DIVIDER_HEIGHT = 9;
107
110
  var GRID_TOP = 4;
108
111
  var GRID_BOTTOM = 8;
109
112
  var GRID_WIDTH = POPOVER_WIDTH - 2 * MENU_BORDER_WIDTH - 2 * LIST_PADDING - 2 * ROW_INSET;
@@ -116,8 +119,8 @@ function colorPageHeight(swatches) {
116
119
  const rows = Math.ceil(swatches.length / GRID_COLUMNS);
117
120
  return ROW_HEIGHT + GRID_TOP + rows * SWATCH_SIZE + (rows - 1) * GRID_GAP + GRID_BOTTOM;
118
121
  }
119
- function pickerContentSize(rowCount, colorSection) {
120
- 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);
121
124
  const content = colorSection ? Math.max(list, colorPageHeight(colorSection.swatches ?? defaultColors)) : list;
122
125
  return { width: POPOVER_WIDTH, height: LIST_PADDING * 2 + content };
123
126
  }
@@ -168,7 +171,7 @@ function PickerMenu({ id, host, color, dark, label, chip, colorSection, homeColo
168
171
  children
169
172
  ] }) }, showColorPage ? "color" : "list"),
170
173
  showFooter && /* @__PURE__ */ jsxs2(View3, { style: styles3.menuFooter, children: [
171
- /* @__PURE__ */ jsx4(View3, { style: [styles3.divider, { backgroundColor: color }] }),
174
+ /* @__PURE__ */ jsx4(MenuDivider, { color }),
172
175
  footer
173
176
  ] })
174
177
  ]
@@ -197,9 +200,12 @@ function ColorRow({ section, fg, dividerColor, onPress }) {
197
200
  ] }),
198
201
  /* @__PURE__ */ jsx4(Icon2, { source: "chevron-right", size: 18, color: fg })
199
202
  ] }) }),
200
- /* @__PURE__ */ jsx4(View3, { style: [styles3.divider, { backgroundColor: dividerColor }] })
203
+ /* @__PURE__ */ jsx4(MenuDivider, { color: dividerColor })
201
204
  ] });
202
205
  }
206
+ function MenuDivider({ color }) {
207
+ return /* @__PURE__ */ jsx4(View3, { style: [styles3.divider, { backgroundColor: color }] });
208
+ }
203
209
  function ColorPage({ section, homeColor, fg, onBack }) {
204
210
  const { value, onChange, previewValue, swatches = defaultColors, label = "Color", takenValue, allowSwapTaken } = section;
205
211
  const renderSwatch = useProfileSwatchRenderer();
@@ -291,7 +297,8 @@ var styles3 = StyleSheet3.create({
291
297
  flex: 1
292
298
  },
293
299
  // Separates the footer (a navigation action) and the Color row from the selectable rows - a
294
- // 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
295
302
  // (passed in as backgroundColor) rather than a flat gray, matching the border/caret's own
296
303
  // accent-tinted look.
297
304
  divider: {
@@ -398,9 +405,13 @@ var styles3 = StyleSheet3.create({
398
405
  });
399
406
 
400
407
  // src/OptionPicker.tsx
401
- import { jsx as jsx5 } from "react/jsx-runtime";
402
- function getOptionPickerContentSize(optionCount, colorSection) {
403
- 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);
404
415
  }
405
416
  function OptionPicker({ id, host, options, value, onChange, color, dark, icon, rowIcon = icon, colorSection, align, alignOverride, rotation = 0, triggerStyle }) {
406
417
  const fg = dark ? "#FFFFFF" : "#000000";
@@ -409,7 +420,10 @@ function OptionPicker({ id, host, options, value, onChange, color, dark, icon, r
409
420
  onChange(option.value);
410
421
  host.close();
411
422
  };
412
- 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)) });
413
427
  }
414
428
 
415
429
  // src/ProfileChip.tsx
@@ -425,7 +439,7 @@ import { TouchableRipple as TouchableRipple2 } from "@rific/feedback-press";
425
439
  import { useMemo as useMemo2 } from "react";
426
440
  import { StyleSheet as StyleSheet4, View as View4 } from "react-native";
427
441
  import { Icon as Icon3, Text as Text3 } from "react-native-paper";
428
- import { jsx as jsx7, jsxs as jsxs3 } from "react/jsx-runtime";
442
+ import { jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
429
443
  function getProfilePickerContentSize(profileCount, hasManageRow = false, colorSection) {
430
444
  return pickerContentSize(profileCount + 1 + (hasManageRow ? 1 : 0), colorSection);
431
445
  }
@@ -450,17 +464,17 @@ function ProfilePicker({ idPrefix, host, profiles, selectedId, takenId, takenIds
450
464
  host.close();
451
465
  onManage?.();
452
466
  };
453
- 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: [
454
468
  /* @__PURE__ */ jsx7(Icon3, { source: "account-cog-outline", size: 16, color }),
455
469
  /* @__PURE__ */ jsx7(Text3, { style: [styles4.manageLabel, { color: fg }], children: "Manage" })
456
470
  ] }) }) : void 0;
457
- 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: [
458
472
  /* @__PURE__ */ jsx7(IconRow, { icon: nullIcon, label: nullLabel, selected: selectedId === null, color, fg, onPress: handleSelectGuest }),
459
473
  sortedProfiles.map((profile) => {
460
474
  const taken = isTaken(profile.id);
461
475
  const selected = selectedId === profile.id;
462
476
  const rowColor = colorPreview ? colorPreview(profile.color) : profile.color;
463
- 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: [
464
478
  /* @__PURE__ */ jsx7(ProfileChip, { profile, filled: !selected }),
465
479
  /* @__PURE__ */ jsx7(
466
480
  Text3,
@@ -540,7 +554,7 @@ function isValidProfile(value) {
540
554
  }
541
555
 
542
556
  // src/ProfilesManager.tsx
543
- 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";
544
558
  var CHIP_SIZE2 = 40;
545
559
  var textInputRef = (ref) => ref;
546
560
  function deriveTag(name) {
@@ -620,15 +634,15 @@ function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, h
620
634
  }, [confirmDeleteId, editingId, onDelete]);
621
635
  const sortedProfiles = useMemo3(() => [...profiles].sort((a, b) => a.name.localeCompare(b.name)), [profiles]);
622
636
  const rows = [...sortedProfiles.map((profile) => ({ kind: "profile", profile })), { kind: "new" }];
623
- return /* @__PURE__ */ jsxs4(Fragment2, { children: [
624
- /* @__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: [
625
639
  headerLeft,
626
640
  /* @__PURE__ */ jsx8(Text4, { variant: titleVariant, style: [styles5.title, { color: fg }], children: "Profiles" })
627
641
  ] }),
628
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) => {
629
643
  if (row.kind === "new") {
630
644
  if (editingId !== "new") {
631
- 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: [
632
646
  /* @__PURE__ */ jsx8(View5, { style: [styles5.rowChip, styles5.newChip, { borderColor: fg }], children: /* @__PURE__ */ jsx8(Icon4, { source: "plus", size: 20, color: fg }) }),
633
647
  /* @__PURE__ */ jsx8(Text4, { variant: "bodyLarge", style: { color: fg }, children: "New Profile" })
634
648
  ] }) }, "new");
@@ -639,7 +653,7 @@ function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, h
639
653
  if (editingId === profile.id) {
640
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);
641
655
  }
642
- 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: [
643
657
  /* @__PURE__ */ jsx8(ProfileChip, { profile, size: CHIP_SIZE2 }),
644
658
  /* @__PURE__ */ jsx8(Text4, { variant: "bodyLarge", style: [styles5.rowName, { color: fg }], numberOfLines: 1, children: profile.name })
645
659
  ] }) }, profile.id);
@@ -665,7 +679,7 @@ function EditRow({ draftName, onNameChange, draftColor, onColorChange, draftTag,
665
679
  useEffect(() => {
666
680
  if (host.openId === "color") tagFocusRef.current();
667
681
  }, [host.openId]);
668
- return /* @__PURE__ */ jsxs4(View5, { style: styles5.editRow, children: [
682
+ return /* @__PURE__ */ jsxs5(View5, { style: styles5.editRow, children: [
669
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 }),
670
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" }),
671
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 }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tastic/profile",
3
- "version": "0.17.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",
@@ -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> {
@@ -61,9 +72,12 @@ export function OptionPicker<T extends string>({ id, host, options, value, onCha
61
72
  }
62
73
 
63
74
  return (
64
- <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}>
65
- {options.map((option) => (
66
- <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>
67
81
  ))}
68
82
  </PickerMenu>
69
83
  )
@@ -49,8 +49,9 @@ const COLOR_ROW_SWATCH_SIZE = 22
49
49
  const COLOR_ROW_CAPTIONED_HEIGHT = 48
50
50
  const COLOR_LABEL_LINE = 18
51
51
  const COLOR_CAPTION_LINE = 16
52
- // The divider under the Color row: 1pt plus 4pt above and below, the same as the Manage divider.
53
- 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
54
55
  // The color page: a back row, then the grid with this much space above and below it.
55
56
  const GRID_TOP = 4
56
57
  const GRID_BOTTOM = 8
@@ -115,10 +116,11 @@ function colorPageHeight(swatches: SeedColor[]) {
115
116
  // pages, exactly: the list with the Color row and its divider on top, or the color page (a back row
116
117
  // and the swatches), so one placement fits whichever page is showing. It leaves out the menu's 2pt
117
118
  // border and the Manage divider, ~13pt in all, so treat it as a placement estimate, not a
118
- // pixel-exact measurement. getProfilePickerContentSize and getOptionPickerContentSize count the
119
- // rows for their own picker.
120
- export function pickerContentSize(rowCount: number, colorSection?: Pick<ProfilePickerColorSection, 'swatches' | 'caption'>): ProfilePickerContentSize {
121
- 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)
122
124
  const content = colorSection ? Math.max(list, colorPageHeight(colorSection.swatches ?? defaultColors)) : list
123
125
  return { width: POPOVER_WIDTH, height: LIST_PADDING * 2 + content }
124
126
  }
@@ -242,7 +244,7 @@ export function PickerMenu({ id, host, color, dark, label, chip, colorSection, h
242
244
  below. Not on the color page, which only has the back row and the swatches. */}
243
245
  {showFooter && (
244
246
  <View style={styles.menuFooter}>
245
- <View style={[styles.divider, { backgroundColor: color }]} />
247
+ <MenuDivider color={color} />
246
248
  {footer}
247
249
  </View>
248
250
  )}
@@ -315,11 +317,16 @@ function ColorRow({ section, fg, dividerColor, onPress }: { section: ProfilePick
315
317
  <Icon source='chevron-right' size={18} color={fg} />
316
318
  </View>
317
319
  </TouchableRipple>
318
- <View style={[styles.divider, { backgroundColor: dividerColor }]} />
320
+ <MenuDivider color={dividerColor} />
319
321
  </>
320
322
  )
321
323
  }
322
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
+
323
330
  // The color page: a back row to the list, then the swatches. `homeColor` is the selected profile's
324
331
  // own color: its swatch carries a dot, so a player who has picked something else can find their way
325
332
  // back to it (the profile's row shows that color, but matching it to one of two similar swatches by
@@ -443,7 +450,8 @@ const styles = StyleSheet.create({
443
450
  flex: 1
444
451
  },
445
452
  // Separates the footer (a navigation action) and the Color row from the selectable rows - a
446
- // 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
447
455
  // (passed in as backgroundColor) rather than a flat gray, matching the border/caret's own
448
456
  // accent-tinted look.
449
457
  divider: {