@tastic/profile 0.11.0 → 0.12.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
@@ -148,12 +148,12 @@ var styles2 = StyleSheet2.create({
148
148
  anchor: {
149
149
  position: "relative"
150
150
  },
151
- // See InlineColorPicker's identical comment — React Native Web gives every position:'relative'
151
+ // See InlineColorPicker's identical comment - React Native Web gives every position:'relative'
152
152
  // view its own stacking context, so the elevation has to live on the anchor itself.
153
153
  anchorOpen: {
154
154
  zIndex: 100
155
155
  },
156
- // Separates Manage (a navigation action) from the selectable rows above it — different kind of
156
+ // Separates Manage (a navigation action) from the selectable rows above it - different kind of
157
157
  // action, not just another row in the same list. Tinted with the seat's own color (passed in as
158
158
  // backgroundColor) rather than a flat gray, matching the border/caret's own accent-tinted look.
159
159
  divider: {
@@ -162,7 +162,7 @@ var styles2 = StyleSheet2.create({
162
162
  opacity: 0.25
163
163
  },
164
164
  // Matches ProfileChip's own default size/shape exactly (see this row's own render-site comment)
165
- // — no backgroundColor, unlike ProfileChip's own default: this row has no identity color of its
165
+ // - no backgroundColor, unlike ProfileChip's own default: this row has no identity color of its
166
166
  // own to fill it with, and the render site now colors the icon directly instead.
167
167
  guestChip: {
168
168
  alignItems: "center",
@@ -176,7 +176,7 @@ var styles2 = StyleSheet2.create({
176
176
  fontWeight: "700",
177
177
  letterSpacing: 0.5
178
178
  },
179
- // Centered rather than left-aligned like a normal row — see this row's own render-site comment
179
+ // Centered rather than left-aligned like a normal row - see this row's own render-site comment
180
180
  // for why (a footer action, not another selectable identity).
181
181
  manageRowInner: {
182
182
  alignItems: "center",
@@ -194,7 +194,7 @@ var styles2 = StyleSheet2.create({
194
194
  shadowOpacity: 0.3,
195
195
  shadowRadius: 8
196
196
  },
197
- // Padded on every side (not just top/bottom) — a selected row's own full-color background (see
197
+ // Padded on every side (not just top/bottom) - a selected row's own full-color background (see
198
198
  // `row`'s borderRadius) needs real margin from the menu's own straight edges to read as a rounded
199
199
  // pill instead of looking clipped against them.
200
200
  menuContent: {
@@ -437,7 +437,7 @@ var styles3 = StyleSheet3.create({
437
437
  gap: 4,
438
438
  paddingBottom: 8
439
439
  },
440
- // 1x1 and fully transparent — there's nothing to see here on purpose (see EditRow's own comment
440
+ // 1x1 and fully transparent - there's nothing to see here on purpose (see EditRow's own comment
441
441
  // above the field this styles). position: 'absolute' pulls it out of editRow's flex flow
442
442
  // entirely, so it doesn't reserve a gap in the row the way a normal flex sibling would.
443
443
  hiddenTagInput: {
@@ -570,9 +570,27 @@ function createProfileSelectionSlice(namespace, defaultState, mountKey = "profil
570
570
  function reducer(state = defaultState, action) {
571
571
  if (action.type === REHYDRATE) {
572
572
  const persisted = action.payload?.[mountKey];
573
- return { ...defaultState, ...state, ...persisted };
573
+ const merged = { ...defaultState, ...state, ...persisted };
574
+ const seen = /* @__PURE__ */ new Set();
575
+ for (const seat of Object.keys(merged)) {
576
+ const profileId = merged[seat];
577
+ if (profileId === null) continue;
578
+ if (seen.has(profileId)) merged[seat] = null;
579
+ else seen.add(profileId);
580
+ }
581
+ return merged;
582
+ }
583
+ if (select.match(action)) {
584
+ const { profileId, seat } = action.payload;
585
+ const previous = state[seat] ?? null;
586
+ const next = { ...state, [seat]: profileId };
587
+ if (profileId !== null) {
588
+ for (const other of Object.keys(state)) {
589
+ if (String(other) !== String(seat) && state[other] === profileId) next[other] = previous === profileId ? null : previous;
590
+ }
591
+ }
592
+ return next;
574
593
  }
575
- if (select.match(action)) return { ...state, [action.payload.seat]: action.payload.profileId };
576
594
  if (clearProfile.match(action)) {
577
595
  const next = { ...state };
578
596
  for (const seat of Object.keys(state)) {
@@ -632,22 +650,27 @@ try {
632
650
  }
633
651
  var PROFILES_KEY = "sharedProfiles";
634
652
  var isSharedProfileStoreAvailable = native !== null;
635
- async function loadSharedProfiles(groupId) {
653
+ async function readSharedProfiles(groupId) {
636
654
  if (!native) return [];
637
655
  let json = null;
638
656
  try {
639
657
  json = await native.getSharedJSON(groupId, PROFILES_KEY);
640
658
  } catch {
641
- return [];
659
+ return null;
642
660
  }
643
661
  if (!json) return [];
644
662
  try {
645
663
  const parsed = JSON.parse(json);
646
- return Array.isArray(parsed) ? parsed.filter(isValidProfile) : [];
664
+ if (!Array.isArray(parsed)) return null;
665
+ const profiles = parsed.filter(isValidProfile);
666
+ return parsed.length > 0 && profiles.length === 0 ? null : profiles;
647
667
  } catch {
648
- return [];
668
+ return null;
649
669
  }
650
670
  }
671
+ async function loadSharedProfiles(groupId) {
672
+ return await readSharedProfiles(groupId) ?? [];
673
+ }
651
674
  async function saveSharedProfiles(groupId, profiles) {
652
675
  if (!native) return;
653
676
  try {
@@ -659,7 +682,8 @@ async function saveSharedProfiles(groupId, profiles) {
659
682
  // src/redux/resolveInitialProfiles.ts
660
683
  async function resolveInitialProfiles(groupId, localFallback) {
661
684
  if (!isSharedProfileStoreAvailable) return localFallback;
662
- const shared = await loadSharedProfiles(groupId);
685
+ const shared = await readSharedProfiles(groupId);
686
+ if (shared === null) return localFallback;
663
687
  if (shared.length === 0 && localFallback.length > 0) {
664
688
  saveSharedProfiles(groupId, localFallback).catch(() => {
665
689
  });
@@ -701,8 +725,8 @@ function useSharedProfilesSync({ groupId, onRemoteChange }) {
701
725
  let cancelled = false;
702
726
  const sub = AppState.addEventListener("change", (next) => {
703
727
  if (next !== "active" || pendingWriteRef.current) return;
704
- loadSharedProfiles(groupId).then((shared) => {
705
- if (!cancelled) onRemoteChangeRef.current(shared);
728
+ readSharedProfiles(groupId).then((shared) => {
729
+ if (!cancelled && shared !== null) onRemoteChangeRef.current(shared);
706
730
  });
707
731
  });
708
732
  return () => {
@@ -723,6 +747,13 @@ function useSharedProfilesSync({ groupId, onRemoteChange }) {
723
747
  );
724
748
  return { syncToShared };
725
749
  }
750
+
751
+ // src/seatColorClash.ts
752
+ function avoidSeatColorClash(p1Color, p2Color, defaults) {
753
+ if (p2Color.toLowerCase() !== p1Color.toLowerCase()) return p2Color;
754
+ const [p1Default, p2Default] = defaults;
755
+ return p2Color.toLowerCase() === p1Default.toLowerCase() ? p2Default : p1Default;
756
+ }
726
757
  export {
727
758
  MAX_PROFILE_NAME_LENGTH,
728
759
  MAX_TAG_LENGTH,
@@ -731,6 +762,7 @@ export {
731
762
  ProfilePicker,
732
763
  ProfilesManager,
733
764
  ProfilesScreen,
765
+ avoidSeatColorClash,
734
766
  createProfileExtensionSlice,
735
767
  createProfileRecord,
736
768
  createProfileSelectionSlice,
@@ -742,6 +774,7 @@ export {
742
774
  loadSharedProfiles,
743
775
  profilesActions,
744
776
  profilesReducer,
777
+ readSharedProfiles,
745
778
  resolveInitialProfiles,
746
779
  saveSharedProfiles,
747
780
  useProfileColorPreview,
@@ -1,9 +1,9 @@
1
1
  import ExpoModulesCore
2
2
 
3
3
  // Generic group/key -> JSON-string bridge onto a native iOS App Group's shared UserDefaults suite
4
- // — the mechanism that actually lets two separate apps (same Apple Developer Team, both declaring
4
+ // - the mechanism that actually lets two separate apps (same Apple Developer Team, both declaring
5
5
  // the same "com.apple.security.application-groups" entitlement) read and write one common blob of
6
- // data. Nothing here is @tastic/profile-specific — see src/sharedProfileStore.ts, the one caller,
6
+ // data. Nothing here is @tastic/profile-specific - see src/sharedProfileStore.ts, the one caller,
7
7
  // for the profile-roster-shaped API built on top of this. Android/web never register this module
8
8
  // at all (see expo-module.config.json's "apple"-only platform list), so a consuming app has to
9
9
  // treat "module not present" as a normal, expected case rather than an error.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tastic/profile",
3
- "version": "0.11.0",
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",
3
+ "version": "0.12.0",
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",
7
7
  "expo",
@@ -5,7 +5,7 @@ import { Icon, Text } from 'react-native-paper'
5
5
  import { useProfileDisplayColor } from './ProfileColorContext'
6
6
  import { Profile } from './types'
7
7
 
8
- // Presence check, not isValidTag's own full-string emoji validation (profilesValidation.ts) — this
8
+ // Presence check, not isValidTag's own full-string emoji validation (profilesValidation.ts) - this
9
9
  // only decides a font-size ratio, so it doesn't need that check's ZWJ/variation-selector precision,
10
10
  // just "does this tag contain a pictograph at all."
11
11
  const EMOJI_PATTERN = /\p{Extended_Pictographic}/u
@@ -14,7 +14,7 @@ interface Props {
14
14
  profile: Profile
15
15
  size?: number
16
16
  // False on a selected row, whose own background already fills with the profile's color (see
17
- // ProfilePicker's own selected-row handling) — a second circle behind the tag there would either
17
+ // ProfilePicker's own selected-row handling) - a second circle behind the tag there would either
18
18
  // double up decoratively or, filled some other flat color, look like a stray badge next to the
19
19
  // plain icon+text every other selected row in the app uses. Contrast is still computed against
20
20
  // the profile's own color either way (that's what's actually behind the glyph whether this chip
@@ -24,18 +24,18 @@ interface Props {
24
24
  filled?: boolean
25
25
  }
26
26
 
27
- // A profile's identity at a glance — its own color as a filled circle, with its tag (or a generic
27
+ // A profile's identity at a glance - its own color as a filled circle, with its tag (or a generic
28
28
  // fallback icon, for a profile with none set) in a contrast-safe color on top. The same combination
29
29
  // @tastic/hud's InlineColorPicker trigger shows for live color selection, just as a small static
30
30
  // badge for a list row (ProfilePicker's dropdown, ProfilesManager's roster) rather than an
31
31
  // interactive picker trigger.
32
32
  export function ProfileChip({ profile, size = 22, filled = true }: Props) {
33
- // The drawn color, not necessarily the saved one — see ProfileColorProvider.
33
+ // The drawn color, not necessarily the saved one - see ProfileColorProvider.
34
34
  const color = useProfileDisplayColor(profile.color)
35
35
  const contrastColor = getContrastColor(color)
36
36
  // An emoji glyph reads visually smaller than a bold letter at the same fontSize (the system emoji
37
37
  // font leaves more of its own em-box empty), so a plain-text ratio that looks right for "J" still
38
- // looks small for "🎮" at the identical size — this bumps emoji up to the icon's own ratio instead,
38
+ // looks small for "🎮" at the identical size - this bumps emoji up to the icon's own ratio instead,
39
39
  // since a single emoji is closer in visual weight to an icon glyph than to a line of text.
40
40
  const tagFontSize = size * (profile.tag && EMOJI_PATTERN.test(profile.tag) ? 0.6 : 0.4)
41
41
  return (
@@ -17,7 +17,7 @@ const LIST_PADDING = 8
17
17
  // A complete placement decision for the popover: which way it opens, how tall it may grow, whether
18
18
  // this open's measurement has landed yet, and the ref attached to the measured trigger.
19
19
  // Structurally identical to @tastic/hud's own `AlignResult` (exported from hud >= 0.12.0, and the
20
- // same shape `useAutoAlign` itself returns) — declared here rather than imported so this package's
20
+ // same shape `useAutoAlign` itself returns) - declared here rather than imported so this package's
21
21
  // public types don't start requiring a newer @tastic/hud than its peer range already admits. A
22
22
  // hud `AlignResult` (e.g. from `useZoneClampedAlign`) is assignable to this as-is.
23
23
  export interface ProfilePickerAlignResult {
@@ -36,9 +36,9 @@ export interface ProfilePickerContentSize {
36
36
  // The popover's content footprint for a given roster size: always POPOVER_WIDTH wide, tall enough
37
37
  // for the null/"guest" row + one row per profile + (when `hasManageRow`, i.e. when the picker is
38
38
  // given an `onManage`) the trailing Manage row, plus the list's own padding. This is the size
39
- // ProfilePicker hands to `useAutoAlign` — the ONE formula, used by the component itself (it leaves
39
+ // ProfilePicker hands to `useAutoAlign` - the ONE formula, used by the component itself (it leaves
40
40
  // out the menu's 2pt border and the Manage divider, ~13pt in all, so treat it as a placement
41
- // estimate, not a pixel-exact measurement) — exported
41
+ // estimate, not a pixel-exact measurement) - exported
42
42
  // so a host supplying its own placement through `alignOverride` can size that placement hook (its
43
43
  // own useAutoAlign/useZoneClampedAlign call) to match the popover it is actually positioning,
44
44
  // instead of copying these constants and drifting whenever they change here.
@@ -48,32 +48,32 @@ export function getProfilePickerContentSize(profileCount: number, hasManageRow =
48
48
  }
49
49
 
50
50
  interface ProfilePickerProps<P extends Profile> {
51
- // Namespaces this trigger's id — pass something unique per instance whenever more than one of
51
+ // Namespaces this trigger's id - pass something unique per instance whenever more than one of
52
52
  // these shares a PopoverHost (e.g. one per seat).
53
53
  idPrefix: string
54
- // The host this trigger's popover opens against — share one host across sibling pickers (a
54
+ // The host this trigger's popover opens against - share one host across sibling pickers (a
55
55
  // seat's color/tag/profile triggers) so opening one closes any other in the same group.
56
56
  host: PopoverHost
57
57
  profiles: P[]
58
58
  selectedId: string | null
59
- // Another picker's currently-selected profile id, if any — disabled in this list. Useful when a
59
+ // Another picker's currently-selected profile id, if any - disabled in this list. Useful when a
60
60
  // profile shouldn't be selectable in two places at once (e.g. two seats in a local-multiplayer
61
61
  // game, where double-selecting one profile would double-count its stats as both winner and
62
- // loser) — omit if that doesn't apply.
62
+ // loser) - omit if that doesn't apply.
63
63
  takenId?: string | null
64
- // Tint for the trigger's own label/icon — pass the seat/context's current live color so the
64
+ // Tint for the trigger's own label/icon - pass the seat/context's current live color so the
65
65
  // trigger reads as one identity with whatever it's paired with.
66
66
  color: string
67
67
  dark: boolean
68
68
  // Forces a specific horizontal alignment, beating whatever the placement (the automatic
69
69
  // measurement, or `alignOverride` below) would have picked. Omit to let placement decide.
70
70
  align?: 'left' | 'right' | 'center'
71
- // Substitutes the popover's whole placement — horizontal/vertical alignment, max height, the
72
- // `measured` gate and the trigger ref — for the automatic `useAutoAlign` one, for a host whose
71
+ // Substitutes the popover's whole placement - horizontal/vertical alignment, max height, the
72
+ // `measured` gate and the trigger ref - for the automatic `useAutoAlign` one, for a host whose
73
73
  // trigger sits somewhere that hook's plain window-relative measurement gets wrong. The motivating
74
74
  // case is a FAKE-rotated frame (a View turned +-90deg while the OS stays portrait-locked):
75
75
  // `measureInWindow` reports a rect in true screen space, `useAutoAlign` compares it against the
76
- // raw portrait window, and so measures along the wrong axes — flipping the list upward off-screen
76
+ // raw portrait window, and so measures along the wrong axes - flipping the list upward off-screen
77
77
  // or running it past the frame's bottom. The host runs its own rotation-aware placement hook
78
78
  // (sized with `getProfilePickerContentSize`, passing `open` from its own `host.openId === ...`
79
79
  // check) and hands the result in here; `align` above still wins over this result's own `align`.
@@ -81,7 +81,7 @@ interface ProfilePickerProps<P extends Profile> {
81
81
  // produced it is measuring the right element. Omit for the default automatic placement.
82
82
  alignOverride?: ProfilePickerAlignResult
83
83
  // Rotation of an ancestor frame turned by a fake landscape (or equivalent), forwarded to the
84
- // automatic `useAutoAlign` measurement — the same seam @tastic/hud's InlineColorPicker/
84
+ // automatic `useAutoAlign` measurement - the same seam @tastic/hud's InlineColorPicker/
85
85
  // SectionedDropdown expose (needs @tastic/hud >= 0.8.0; older versions ignore it). Defaults to 0
86
86
  // (measurement used as-is), which is exactly how this picker behaved before this prop existed.
87
87
  // Deliberately NOT read from an ambient provider the way hud's own pickers do: this component
@@ -89,35 +89,35 @@ interface ProfilePickerProps<P extends Profile> {
89
89
  // every existing caller already living under a rotation provider. Ignored when `alignOverride` is
90
90
  // supplied (the override replaces the placement this feeds).
91
91
  rotation?: PopoverRotation
92
- // Trigger's own idle-state text (uppercased here regardless of casing passed in) — shown until a
92
+ // Trigger's own idle-state text (uppercased here regardless of casing passed in) - shown until a
93
93
  // profile is selected.
94
94
  guestLabel: string
95
- // The null-selection row's own label/icon — defaults to a generic anonymous-player row, but
95
+ // The null-selection row's own label/icon - defaults to a generic anonymous-player row, but
96
96
  // overridable for a caller where null means something else (e.g. an "All Profiles" combined view
97
97
  // reusing this same component for its own picker).
98
98
  nullLabel?: string
99
99
  nullIcon?: string
100
100
  onSelect: (profile: P | null) => void
101
- // Renders a row for navigating to a dedicated create/rename/delete screen — omit to hide it
101
+ // Renders a row for navigating to a dedicated create/rename/delete screen - omit to hide it
102
102
  // entirely. This component never manages profiles itself (see ProfilesManager for that); this is
103
103
  // selection-only.
104
104
  onManage?: () => void
105
105
  }
106
106
 
107
107
  // Generic over P (an app's own Profile type, richer than this package's base Profile) so that a
108
- // caller's own onSelect — typed to accept its own richer profile shape — type-checks against the
108
+ // caller's own onSelect - typed to accept its own richer profile shape - type-checks against the
109
109
  // objects this component actually hands back, which are always exactly whatever came in through
110
110
  // `profiles` (see handleSelectExisting below). Without this, TypeScript would only ever let
111
111
  // onSelect declare the base Profile type, since that's all a non-generic prop signature could
112
- // promise it receives — even though at runtime it always receives the caller's own richer objects.
112
+ // promise it receives - even though at runtime it always receives the caller's own richer objects.
113
113
  // ProfileChip doesn't need this same treatment: it only ever takes a profile in, never hands one
114
114
  // back out, so plain structural covariance already lets a richer object through.
115
115
  //
116
116
  // A name trigger + selection popover: pick an existing profile, fall back to a null/"guest"
117
117
  // selection, or (if onManage is passed) navigate to manage the roster. Creating, renaming, and
118
- // deleting profiles all live outside this component (see ProfilesManager) — this is selection-only,
118
+ // deleting profiles all live outside this component (see ProfilesManager) - this is selection-only,
119
119
  // both because that keeps this component simple enough to drop into a HUD trigger row, and because
120
- // it keeps this popover from needing its own destructive-delete confirmation. Fully prop-driven —
120
+ // it keeps this popover from needing its own destructive-delete confirmation. Fully prop-driven -
121
121
  // no internal profile-storage access of its own, so the host app stays the one place that reads
122
122
  // its own persistence hook.
123
123
  export function ProfilePicker<P extends Profile>({ idPrefix, host, profiles, selectedId, takenId, color, dark, align: forcedAlign, alignOverride, rotation = 0, guestLabel, nullLabel = 'Player', nullIcon = 'account-off-outline', onSelect, onManage }: ProfilePickerProps<P>) {
@@ -129,12 +129,12 @@ export function ProfilePicker<P extends Profile>({ idPrefix, host, profiles, sel
129
129
 
130
130
  const open = host.openId === id
131
131
  const selectedProfile = profiles.find((p) => p.id === selectedId) ?? null
132
- // Alphabetical — this is the list that sees the most real use, so it's the one that benefits
132
+ // Alphabetical - this is the list that sees the most real use, so it's the one that benefits
133
133
  // most from staying scannable once there are more than a handful of saved profiles.
134
134
  const sortedProfiles = useMemo(() => [...profiles].sort((a, b) => a.name.localeCompare(b.name)), [profiles])
135
135
  // Null row + each saved profile + (if passed) the manage row.
136
136
  const { width: contentWidth, height: contentHeight } = getProfilePickerContentSize(profiles.length, !!onManage)
137
- // Always called, even when `alignOverride` is supplied and this result goes unused — hooks can't
137
+ // Always called, even when `alignOverride` is supplied and this result goes unused - hooks can't
138
138
  // be conditional (see @tastic/hud's InlineColorPicker for the identical pattern). With an
139
139
  // override its triggerRef is never attached, so this hook's own measurement is a no-op.
140
140
  const auto = useAutoAlign(open, contentWidth, contentHeight, rotation)
@@ -169,7 +169,7 @@ export function ProfilePicker<P extends Profile>({ idPrefix, host, profiles, sel
169
169
  </View>
170
170
  </TouchableRipple>
171
171
 
172
- {/* Gated on `measured`, not just `open` — see InlineColorPicker's identical fix. */}
172
+ {/* Gated on `measured`, not just `open` - see InlineColorPicker's identical fix. */}
173
173
  <PopoverBody visible={open && measured} align={align} verticalAlign={verticalAlign} caretColor={menuBg} caretBorderColor={color} caretBorderWidth={2} triggerSize={TRIGGER_HEIGHT}>
174
174
  <ScrollView
175
175
  style={[
@@ -185,12 +185,12 @@ export function ProfilePicker<P extends Profile>({ idPrefix, host, profiles, sel
185
185
  showsVerticalScrollIndicator={false}
186
186
  >
187
187
  {/* Selected row's own background fills entirely with its identity color (this seat's own
188
- accent for the null row, the profile's own saved color otherwise) — matching how a
188
+ accent for the null row, the profile's own saved color otherwise) - matching how a
189
189
  selected row reads elsewhere (see @tastic/hud's SectionedDropdown) rather than a checkmark
190
190
  bolted onto an otherwise-plain row. */}
191
191
  <TouchableRipple onPress={handleSelectGuest} style={[styles.row, selectedId === null && { backgroundColor: color }]}>
192
192
  <View style={styles.rowInner}>
193
- {/* Unfilled, matching a selected profile row's own ProfileChip treatment — no
193
+ {/* Unfilled, matching a selected profile row's own ProfileChip treatment - no
194
194
  background circle, but still the same fixed-size slot every row's icon/chip sits in
195
195
  (see ProfileChip's own doc), so this row's icon stays lined up with every other row
196
196
  regardless of selection. */}
@@ -208,7 +208,7 @@ export function ProfilePicker<P extends Profile>({ idPrefix, host, profiles, sel
208
208
  return (
209
209
  <TouchableRipple key={profile.id} disabled={taken} onPress={() => handleSelectExisting(profile)} style={[styles.row, selected && { backgroundColor: rowColor }]}>
210
210
  <View style={[styles.rowInner, taken && styles.rowDisabled]}>
211
- {/* Unfilled on a selected row — its own background already fills with the
211
+ {/* Unfilled on a selected row - its own background already fills with the
212
212
  profile's color, so a second circle behind the tag would just be a redundant badge
213
213
  where every other selected row in the app has plain icon+text instead. */}
214
214
  <ProfileChip profile={profile} filled={!selected} />
@@ -231,7 +231,7 @@ export function ProfilePicker<P extends Profile>({ idPrefix, host, profiles, sel
231
231
  {onManage && (
232
232
  <>
233
233
  <View style={[styles.divider, { backgroundColor: color }]} />
234
- {/* Tertiary — a navigation action, not another selectable identity in the list above
234
+ {/* Tertiary - a navigation action, not another selectable identity in the list above
235
235
  it, so it reads more like @tastic/hud's SectionedDropdown All/Clear footer button
236
236
  (centered, bold, accent-colored) than a normal row. */}
237
237
  <TouchableRipple onPress={handleManage} style={styles.row}>
@@ -252,12 +252,12 @@ const styles = StyleSheet.create({
252
252
  anchor: {
253
253
  position: 'relative'
254
254
  },
255
- // See InlineColorPicker's identical comment — React Native Web gives every position:'relative'
255
+ // See InlineColorPicker's identical comment - React Native Web gives every position:'relative'
256
256
  // view its own stacking context, so the elevation has to live on the anchor itself.
257
257
  anchorOpen: {
258
258
  zIndex: 100
259
259
  },
260
- // Separates Manage (a navigation action) from the selectable rows above it — different kind of
260
+ // Separates Manage (a navigation action) from the selectable rows above it - different kind of
261
261
  // action, not just another row in the same list. Tinted with the seat's own color (passed in as
262
262
  // backgroundColor) rather than a flat gray, matching the border/caret's own accent-tinted look.
263
263
  divider: {
@@ -266,7 +266,7 @@ const styles = StyleSheet.create({
266
266
  opacity: 0.25
267
267
  },
268
268
  // Matches ProfileChip's own default size/shape exactly (see this row's own render-site comment)
269
- // — no backgroundColor, unlike ProfileChip's own default: this row has no identity color of its
269
+ // - no backgroundColor, unlike ProfileChip's own default: this row has no identity color of its
270
270
  // own to fill it with, and the render site now colors the icon directly instead.
271
271
  guestChip: {
272
272
  alignItems: 'center',
@@ -280,7 +280,7 @@ const styles = StyleSheet.create({
280
280
  fontWeight: '700',
281
281
  letterSpacing: 0.5
282
282
  },
283
- // Centered rather than left-aligned like a normal row — see this row's own render-site comment
283
+ // Centered rather than left-aligned like a normal row - see this row's own render-site comment
284
284
  // for why (a footer action, not another selectable identity).
285
285
  manageRowInner: {
286
286
  alignItems: 'center',
@@ -298,7 +298,7 @@ const styles = StyleSheet.create({
298
298
  shadowOpacity: 0.3,
299
299
  shadowRadius: 8
300
300
  },
301
- // Padded on every side (not just top/bottom) — a selected row's own full-color background (see
301
+ // Padded on every side (not just top/bottom) - a selected row's own full-color background (see
302
302
  // `row`'s borderRadius) needs real margin from the menu's own straight edges to read as a rounded
303
303
  // pill instead of looking clipped against them.
304
304
  menuContent: {