@tastic/profile 0.15.0 → 0.16.1

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
@@ -8,23 +8,64 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
8
8
  // src/PickerMenu.tsx
9
9
  import { defaultColors, FadingScrollView, getContrastColor as getContrastColor2 } from "@rific/auto-paper";
10
10
  import { TouchableRipple } from "@rific/feedback-press";
11
- import { PopoverBody, useAutoAlign } from "@tastic/hud";
11
+ import { PopoverBody, useAutoAlign } from "@tastic/hud/chrome";
12
12
  import { useState } from "react";
13
- import { StyleSheet as StyleSheet2, View as View2 } from "react-native";
13
+ import { StyleSheet as StyleSheet3, View as View3 } from "react-native";
14
14
  import { Icon as Icon2, Text as Text2 } from "react-native-paper";
15
15
 
16
16
  // src/IdentityChip.tsx
17
17
  import { getContrastColor } from "@rific/auto-paper";
18
- import { StyleSheet, View } from "react-native";
18
+ import { StyleSheet as StyleSheet2, View as View2 } from "react-native";
19
19
  import { Icon, Text } from "react-native-paper";
20
+
21
+ // src/ProfileColorContext.tsx
22
+ import { createContext, useContext, useMemo } from "react";
20
23
  import { jsx } from "react/jsx-runtime";
24
+ var ProfileColorContext = createContext({});
25
+ function ProfileColorProvider({ preview, renderSwatch, children }) {
26
+ const value = useMemo(() => ({ preview, renderSwatch }), [preview, renderSwatch]);
27
+ return /* @__PURE__ */ jsx(ProfileColorContext.Provider, { value, children });
28
+ }
29
+ function useProfileColorPreview() {
30
+ return useContext(ProfileColorContext).preview;
31
+ }
32
+ function useProfileSwatchRenderer() {
33
+ return useContext(ProfileColorContext).renderSwatch;
34
+ }
35
+ function useProfileDisplayColor(hex) {
36
+ const preview = useProfileColorPreview();
37
+ return preview ? preview(hex) : hex;
38
+ }
39
+
40
+ // src/SwatchLayer.tsx
41
+ import { StyleSheet, View } from "react-native";
42
+ import { jsx as jsx2 } from "react/jsx-runtime";
43
+ function SwatchLayer({ render, color, size }) {
44
+ return /* @__PURE__ */ jsx2(View, { style: [styles.layer, { borderRadius: size / 2, height: size, width: size }], children: render(color, size) });
45
+ }
46
+ var styles = StyleSheet.create({
47
+ layer: {
48
+ left: 0,
49
+ overflow: "hidden",
50
+ pointerEvents: "none",
51
+ position: "absolute",
52
+ top: 0
53
+ }
54
+ });
55
+
56
+ // src/IdentityChip.tsx
57
+ import { jsx as jsx3, jsxs } from "react/jsx-runtime";
21
58
  var EMOJI_PATTERN = /\p{Extended_Pictographic}/u;
22
59
  function IdentityChip({ color, tag, icon, size, filled = true }) {
60
+ const renderSwatch = useProfileSwatchRenderer();
23
61
  const contrastColor = getContrastColor(color);
24
62
  const tagFontSize = size * (tag && EMOJI_PATTERN.test(tag) ? 0.6 : tag && Array.from(tag).length >= 3 ? 0.32 : 0.4);
25
- return /* @__PURE__ */ jsx(View, { style: [styles.chip, { borderRadius: size / 2, height: size, width: size }, filled && { backgroundColor: color }], children: tag ? /* @__PURE__ */ jsx(Text, { style: [styles.tag, { color: contrastColor, fontSize: tagFontSize }], numberOfLines: 1, adjustsFontSizeToFit: true, children: tag }) : /* @__PURE__ */ jsx(Icon, { source: icon, size: size * 0.6, color: contrastColor }) });
63
+ return /* @__PURE__ */ jsxs(View2, { style: [styles2.chip, { borderRadius: size / 2, height: size, width: size }, filled && !renderSwatch && { backgroundColor: color }], children: [
64
+ filled && renderSwatch && /* @__PURE__ */ jsx3(SwatchLayer, { render: renderSwatch, color, size }),
65
+ tag ? /* @__PURE__ */ jsx3(Text, { style: [styles2.tag, { color: contrastColor, fontSize: tagFontSize }], numberOfLines: 1, adjustsFontSizeToFit: true, children: tag }) : /* @__PURE__ */ jsx3(Icon, { source: icon, size: size * 0.6, color: contrastColor })
66
+ ] });
26
67
  }
27
- var styles = StyleSheet.create({
68
+ var styles2 = StyleSheet2.create({
28
69
  chip: {
29
70
  alignItems: "center",
30
71
  justifyContent: "center"
@@ -36,7 +77,7 @@ var styles = StyleSheet.create({
36
77
  });
37
78
 
38
79
  // src/PickerMenu.tsx
39
- import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
80
+ import { Fragment, jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
40
81
  var TRIGGER_HEIGHT = 28;
41
82
  var POPOVER_WIDTH = 220;
42
83
  var ROW_HEIGHT = 36;
@@ -47,6 +88,8 @@ var CHIP_SIZE = 32;
47
88
  var CHIP_TRIGGER_PADDING = 6;
48
89
  var SWATCH_SIZE = 28;
49
90
  var SWATCH_MIN_GAP = 6;
91
+ var SWATCH_RING_WIDTH = 2;
92
+ var COLOR_ROW_SWATCH_SIZE = 22;
50
93
  var COLOR_ROW_CAPTIONED_HEIGHT = 48;
51
94
  var COLOR_LABEL_LINE = 18;
52
95
  var COLOR_CAPTION_LINE = 16;
@@ -87,17 +130,17 @@ function PickerMenu({ id, host, color, dark, label, chip, colorSection, homeColo
87
130
  };
88
131
  const triggerLabel = label.toUpperCase();
89
132
  const showFooter = !!footer && !showColorPage;
90
- return /* @__PURE__ */ jsxs(View2, { style: [styles2.anchor, open && styles2.anchorOpen], onLayout: handleAnchorLayout, children: [
91
- /* @__PURE__ */ jsx2(TouchableRipple, { onPress: handleTriggerPress, borderless: chipTrigger, style: [styles2.trigger, chipTrigger && styles2.chipTrigger, triggerStyle], accessibilityRole: "button", accessibilityLabel: triggerLabel, children: /* @__PURE__ */ jsxs(View2, { ref: triggerRef, collapsable: false, style: chipTrigger ? styles2.chipTriggerInner : styles2.triggerInner, children: [
92
- chip && /* @__PURE__ */ jsx2(IdentityChip, { color, tag: chip.tag, icon: chip.icon, size: CHIP_SIZE }),
93
- /* @__PURE__ */ jsx2(Text2, { style: [styles2.triggerLabel, { color: chipTrigger ? fg : color }], numberOfLines: 1, children: triggerLabel }),
94
- /* @__PURE__ */ jsx2(Icon2, { source: "menu-down", size: 16, color: chipTrigger ? fg : color })
133
+ return /* @__PURE__ */ jsxs2(View3, { style: [styles3.anchor, open && styles3.anchorOpen], onLayout: handleAnchorLayout, children: [
134
+ /* @__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: [
135
+ chip && /* @__PURE__ */ jsx4(IdentityChip, { color, tag: chip.tag, icon: chip.icon, size: CHIP_SIZE }),
136
+ /* @__PURE__ */ jsx4(Text2, { style: [styles3.triggerLabel, { color: chipTrigger ? fg : color }], numberOfLines: 1, children: triggerLabel }),
137
+ /* @__PURE__ */ jsx4(Icon2, { source: "menu-down", size: 16, color: chipTrigger ? fg : color })
95
138
  ] }) }),
96
- /* @__PURE__ */ jsx2(PopoverBody, { visible: open && measured, align, verticalAlign, caretColor: menuBg, caretBorderColor: color, caretBorderWidth: MENU_BORDER_WIDTH, triggerSize: chipTrigger ? triggerWidth : TRIGGER_HEIGHT, children: /* @__PURE__ */ jsxs(
97
- View2,
139
+ /* @__PURE__ */ jsx4(PopoverBody, { visible: open && measured, align, verticalAlign, caretColor: menuBg, caretBorderColor: color, caretBorderWidth: MENU_BORDER_WIDTH, triggerSize: chipTrigger ? triggerWidth : TRIGGER_HEIGHT, children: /* @__PURE__ */ jsxs2(
140
+ View3,
98
141
  {
99
142
  style: [
100
- styles2.menu,
143
+ styles3.menu,
101
144
  {
102
145
  backgroundColor: menuBg,
103
146
  borderColor: color,
@@ -106,12 +149,12 @@ function PickerMenu({ id, host, color, dark, label, chip, colorSection, homeColo
106
149
  }
107
150
  ],
108
151
  children: [
109
- /* @__PURE__ */ jsx2(FadingScrollView, { contentContainerStyle: [styles2.menuContent, showFooter && styles2.menuContentAboveFooter], showsVerticalScrollIndicator: false, children: colorSection && colorPage ? /* @__PURE__ */ jsx2(ColorPage, { section: colorSection, homeColor, fg, onBack: () => setColorPage(false) }) : /* @__PURE__ */ jsxs(Fragment, { children: [
110
- colorSection && /* @__PURE__ */ jsx2(ColorRow, { section: colorSection, fg, dividerColor: color, onPress: () => setColorPage(true) }),
152
+ /* @__PURE__ */ jsx4(FadingScrollView, { contentContainerStyle: [styles3.menuContent, showFooter && styles3.menuContentAboveFooter], showsVerticalScrollIndicator: false, children: colorSection && colorPage ? /* @__PURE__ */ jsx4(ColorPage, { section: colorSection, homeColor, fg, onBack: () => setColorPage(false) }) : /* @__PURE__ */ jsxs2(Fragment, { children: [
153
+ colorSection && /* @__PURE__ */ jsx4(ColorRow, { section: colorSection, fg, dividerColor: color, onPress: () => setColorPage(true) }),
111
154
  children
112
155
  ] }) }, showColorPage ? "color" : "list"),
113
- showFooter && /* @__PURE__ */ jsxs(View2, { style: styles2.menuFooter, children: [
114
- /* @__PURE__ */ jsx2(View2, { style: [styles2.divider, { backgroundColor: color }] }),
156
+ showFooter && /* @__PURE__ */ jsxs2(View3, { style: styles3.menuFooter, children: [
157
+ /* @__PURE__ */ jsx4(View3, { style: [styles3.divider, { backgroundColor: color }] }),
115
158
  footer
116
159
  ] })
117
160
  ]
@@ -119,37 +162,39 @@ function PickerMenu({ id, host, color, dark, label, chip, colorSection, homeColo
119
162
  ) })
120
163
  ] });
121
164
  }
122
- function IconRow({ icon, label, selected, color, fg, disabled = false, onPress }) {
165
+ function IconRow({ icon, iconColor, label, selected, color, fg, disabled = false, onPress }) {
123
166
  const textColor = selected ? getContrastColor2(color) : fg;
124
- return /* @__PURE__ */ jsx2(TouchableRipple, { disabled, onPress, style: [pickerMenuStyles.row, selected && { backgroundColor: color }], children: /* @__PURE__ */ jsxs(View2, { style: [pickerMenuStyles.rowInner, disabled && pickerMenuStyles.rowDisabled], children: [
125
- /* @__PURE__ */ jsx2(View2, { style: styles2.iconSlot, children: /* @__PURE__ */ jsx2(Icon2, { source: icon, size: 13, color: textColor }) }),
126
- /* @__PURE__ */ jsx2(Text2, { style: [pickerMenuStyles.rowLabel, { color: textColor }], numberOfLines: 1, children: label })
167
+ return /* @__PURE__ */ jsx4(TouchableRipple, { disabled, onPress, style: [pickerMenuStyles.row, selected && { backgroundColor: color }], children: /* @__PURE__ */ jsxs2(View3, { style: [pickerMenuStyles.rowInner, disabled && pickerMenuStyles.rowDisabled], children: [
168
+ /* @__PURE__ */ jsx4(View3, { style: styles3.iconSlot, children: /* @__PURE__ */ jsx4(Icon2, { source: icon, size: 13, color: selected ? textColor : iconColor ?? textColor }) }),
169
+ /* @__PURE__ */ jsx4(Text2, { style: [pickerMenuStyles.rowLabel, { color: textColor }], numberOfLines: 1, children: label })
127
170
  ] }) });
128
171
  }
129
172
  function ColorRow({ section, fg, dividerColor, onPress }) {
130
173
  const { value, previewValue, label = "Color", caption } = section;
174
+ const renderSwatch = useProfileSwatchRenderer();
131
175
  const drawn = previewValue ? previewValue(value) : value;
132
- return /* @__PURE__ */ jsxs(Fragment, { children: [
133
- /* @__PURE__ */ jsx2(TouchableRipple, { onPress, style: [pickerMenuStyles.row, { height: colorRowHeight(caption) }], accessibilityRole: "button", accessibilityLabel: caption ? `${label}, ${caption}` : label, children: /* @__PURE__ */ jsxs(View2, { style: pickerMenuStyles.rowInner, children: [
134
- /* @__PURE__ */ jsx2(View2, { style: [styles2.colorRowSwatch, { backgroundColor: drawn }] }),
135
- /* @__PURE__ */ jsxs(View2, { style: styles2.colorRowText, children: [
136
- /* @__PURE__ */ jsx2(Text2, { style: [styles2.colorRowLabel, { color: fg }], numberOfLines: 1, children: label }),
137
- caption ? /* @__PURE__ */ jsx2(Text2, { style: [styles2.colorRowCaption, { color: fg }], numberOfLines: 1, children: caption }) : null
176
+ return /* @__PURE__ */ jsxs2(Fragment, { children: [
177
+ /* @__PURE__ */ jsx4(TouchableRipple, { onPress, style: [pickerMenuStyles.row, { height: colorRowHeight(caption) }], accessibilityRole: "button", accessibilityLabel: caption ? `${label}, ${caption}` : label, children: /* @__PURE__ */ jsxs2(View3, { style: pickerMenuStyles.rowInner, children: [
178
+ /* @__PURE__ */ jsx4(View3, { style: [styles3.colorRowSwatch, !renderSwatch && { backgroundColor: drawn }], children: renderSwatch && /* @__PURE__ */ jsx4(SwatchLayer, { render: renderSwatch, color: drawn, size: COLOR_ROW_SWATCH_SIZE }) }),
179
+ /* @__PURE__ */ jsxs2(View3, { style: styles3.colorRowText, children: [
180
+ /* @__PURE__ */ jsx4(Text2, { style: [styles3.colorRowLabel, { color: fg }], numberOfLines: 1, children: label }),
181
+ caption ? /* @__PURE__ */ jsx4(Text2, { style: [styles3.colorRowCaption, { color: fg }], numberOfLines: 1, children: caption }) : null
138
182
  ] }),
139
- /* @__PURE__ */ jsx2(Icon2, { source: "chevron-right", size: 18, color: fg })
183
+ /* @__PURE__ */ jsx4(Icon2, { source: "chevron-right", size: 18, color: fg })
140
184
  ] }) }),
141
- /* @__PURE__ */ jsx2(View2, { style: [styles2.divider, { backgroundColor: dividerColor }] })
185
+ /* @__PURE__ */ jsx4(View3, { style: [styles3.divider, { backgroundColor: dividerColor }] })
142
186
  ] });
143
187
  }
144
188
  function ColorPage({ section, homeColor, fg, onBack }) {
145
189
  const { value, onChange, previewValue, swatches = defaultColors, label = "Color", takenValue, allowSwapTaken } = section;
190
+ const renderSwatch = useProfileSwatchRenderer();
146
191
  const displayFor = previewValue ?? ((hex) => hex);
147
- return /* @__PURE__ */ jsxs(Fragment, { children: [
148
- /* @__PURE__ */ jsx2(TouchableRipple, { onPress: onBack, style: pickerMenuStyles.row, accessibilityRole: "button", accessibilityLabel: "Back", children: /* @__PURE__ */ jsxs(View2, { style: pickerMenuStyles.rowInner, children: [
149
- /* @__PURE__ */ jsx2(View2, { style: styles2.iconSlot, children: /* @__PURE__ */ jsx2(Icon2, { source: "chevron-left", size: 20, color: fg }) }),
150
- /* @__PURE__ */ jsx2(Text2, { style: [styles2.colorRowLabel, { color: fg }], numberOfLines: 1, children: label })
192
+ return /* @__PURE__ */ jsxs2(Fragment, { children: [
193
+ /* @__PURE__ */ jsx4(TouchableRipple, { onPress: onBack, style: pickerMenuStyles.row, accessibilityRole: "button", accessibilityLabel: "Back", children: /* @__PURE__ */ jsxs2(View3, { style: pickerMenuStyles.rowInner, children: [
194
+ /* @__PURE__ */ jsx4(View3, { style: styles3.iconSlot, children: /* @__PURE__ */ jsx4(Icon2, { source: "chevron-left", size: 20, color: fg }) }),
195
+ /* @__PURE__ */ jsx4(Text2, { style: [styles3.colorRowLabel, { color: fg }], numberOfLines: 1, children: label })
151
196
  ] }) }),
152
- /* @__PURE__ */ jsx2(View2, { style: styles2.swatches, children: swatches.map((swatch) => {
197
+ /* @__PURE__ */ jsx4(View3, { style: styles3.swatches, children: swatches.map((swatch) => {
153
198
  const selected = swatch.value.toLowerCase() === value.toLowerCase();
154
199
  const isHomeColor = !!homeColor && swatch.value.toLowerCase() === homeColor.toLowerCase();
155
200
  const taken = !selected && !!takenValue && swatch.value.toLowerCase() === takenValue.toLowerCase();
@@ -157,11 +202,15 @@ function ColorPage({ section, homeColor, fg, onBack }) {
157
202
  const swatchDisplay = displayFor(swatch.value);
158
203
  const contrast = getContrastColor2(swatchDisplay);
159
204
  const accessibilityLabel = swappable ? `${swatch.label}, swap` : taken ? `${swatch.label}, taken` : isHomeColor ? `${swatch.label}, profile color` : swatch.label;
160
- return /* @__PURE__ */ jsx2(TouchableRipple, { disabled: taken && !swappable, onPress: () => onChange(swatch.value), borderless: true, style: [styles2.swatch, { backgroundColor: swatchDisplay }, selected && styles2.swatchSelected, taken && !swappable && styles2.swatchTaken], accessibilityRole: "button", accessibilityLabel, accessibilityState: { selected, disabled: taken && !swappable }, children: selected ? /* @__PURE__ */ jsx2(Icon2, { source: "check", size: 16, color: contrast }) : swappable ? /* @__PURE__ */ jsx2(Icon2, { source: "swap-horizontal", size: 16, color: contrast }) : taken ? /* @__PURE__ */ jsx2(Icon2, { source: "close", size: 16, color: contrast }) : isHomeColor ? /* @__PURE__ */ jsx2(View2, { style: [styles2.homeDot, { backgroundColor: contrast }] }) : /* @__PURE__ */ jsx2(View2, {}) }, swatch.value);
205
+ const mark = selected ? /* @__PURE__ */ jsx4(Icon2, { source: "check", size: 16, color: contrast }) : swappable ? /* @__PURE__ */ jsx4(Icon2, { source: "swap-horizontal", size: 16, color: contrast }) : taken ? /* @__PURE__ */ jsx4(Icon2, { source: "close", size: 16, color: contrast }) : isHomeColor ? /* @__PURE__ */ jsx4(View3, { style: [styles3.homeDot, { backgroundColor: contrast }] }) : /* @__PURE__ */ jsx4(View3, {});
206
+ return /* @__PURE__ */ jsx4(TouchableRipple, { disabled: taken && !swappable, onPress: () => onChange(swatch.value), borderless: true, style: [styles3.swatch, !renderSwatch && { backgroundColor: swatchDisplay }, selected && styles3.swatchSelected, taken && !swappable && styles3.swatchTaken], accessibilityRole: "button", accessibilityLabel, accessibilityState: { selected, disabled: taken && !swappable }, children: renderSwatch ? /* @__PURE__ */ jsxs2(View3, { style: styles3.swatchFace, children: [
207
+ /* @__PURE__ */ jsx4(SwatchLayer, { render: renderSwatch, color: swatchDisplay, size: selected ? SWATCH_SIZE - 2 * SWATCH_RING_WIDTH : SWATCH_SIZE }),
208
+ mark
209
+ ] }) : mark }, swatch.value);
161
210
  }) })
162
211
  ] });
163
212
  }
164
- var pickerMenuStyles = StyleSheet2.create({
213
+ var pickerMenuStyles = StyleSheet3.create({
165
214
  row: {
166
215
  borderRadius: 8,
167
216
  height: ROW_HEIGHT
@@ -181,7 +230,7 @@ var pickerMenuStyles = StyleSheet2.create({
181
230
  fontSize: 14
182
231
  }
183
232
  });
184
- var styles2 = StyleSheet2.create({
233
+ var styles3 = StyleSheet3.create({
185
234
  anchor: {
186
235
  position: "relative"
187
236
  },
@@ -217,9 +266,9 @@ var styles2 = StyleSheet2.create({
217
266
  },
218
267
  // The color in effect, in the same 22pt slot as every row's chip.
219
268
  colorRowSwatch: {
220
- borderRadius: 11,
221
- height: 22,
222
- width: 22
269
+ borderRadius: COLOR_ROW_SWATCH_SIZE / 2,
270
+ height: COLOR_ROW_SWATCH_SIZE,
271
+ width: COLOR_ROW_SWATCH_SIZE
223
272
  },
224
273
  colorRowText: {
225
274
  flex: 1
@@ -279,9 +328,19 @@ var styles2 = StyleSheet2.create({
279
328
  justifyContent: "center",
280
329
  width: SWATCH_SIZE
281
330
  },
331
+ // A drawn swatch (see ColorPage) and its mark: the whole circle inside the selected one's ring.
332
+ swatchFace: {
333
+ alignItems: "center",
334
+ bottom: 0,
335
+ justifyContent: "center",
336
+ left: 0,
337
+ position: "absolute",
338
+ right: 0,
339
+ top: 0
340
+ },
282
341
  swatchSelected: {
283
342
  borderColor: "#ffffff",
284
- borderWidth: 2
343
+ borderWidth: SWATCH_RING_WIDTH
285
344
  },
286
345
  swatchTaken: {
287
346
  opacity: 0.35
@@ -313,7 +372,7 @@ var styles2 = StyleSheet2.create({
313
372
  });
314
373
 
315
374
  // src/OptionPicker.tsx
316
- import { jsx as jsx3 } from "react/jsx-runtime";
375
+ import { jsx as jsx5 } from "react/jsx-runtime";
317
376
  function getOptionPickerContentSize(optionCount, colorSection) {
318
377
  return pickerContentSize(optionCount, colorSection);
319
378
  }
@@ -324,54 +383,40 @@ function OptionPicker({ id, host, options, value, onChange, color, dark, icon, r
324
383
  onChange(option.value);
325
384
  host.close();
326
385
  };
327
- return /* @__PURE__ */ jsx3(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__ */ jsx3(IconRow, { icon: option.icon ?? rowIcon, label: option.label, selected: option.value === value, color, fg, onPress: () => handleSelect(option) }, option.value)) });
328
- }
329
-
330
- // src/ProfileColorContext.tsx
331
- import { createContext, useContext } from "react";
332
- import { jsx as jsx4 } from "react/jsx-runtime";
333
- var ProfileColorContext = createContext(void 0);
334
- function ProfileColorProvider({ preview, children }) {
335
- return /* @__PURE__ */ jsx4(ProfileColorContext.Provider, { value: preview, children });
336
- }
337
- function useProfileColorPreview() {
338
- return useContext(ProfileColorContext);
339
- }
340
- function useProfileDisplayColor(hex) {
341
- const preview = useProfileColorPreview();
342
- return preview ? preview(hex) : hex;
386
+ 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)) });
343
387
  }
344
388
 
345
389
  // src/ProfileChip.tsx
346
- import { jsx as jsx5 } from "react/jsx-runtime";
390
+ import { jsx as jsx6 } from "react/jsx-runtime";
347
391
  function ProfileChip({ profile, size = 22, filled = true }) {
348
392
  const color = useProfileDisplayColor(profile.color);
349
- return /* @__PURE__ */ jsx5(IdentityChip, { color, tag: profile.tag, icon: "account", size, filled });
393
+ return /* @__PURE__ */ jsx6(IdentityChip, { color, tag: profile.tag, icon: "account", size, filled });
350
394
  }
351
395
 
352
396
  // src/ProfilePicker.tsx
353
397
  import { getContrastColor as getContrastColor3 } from "@rific/auto-paper";
354
398
  import { TouchableRipple as TouchableRipple2 } from "@rific/feedback-press";
355
- import { useMemo } from "react";
356
- import { StyleSheet as StyleSheet3, View as View3 } from "react-native";
399
+ import { useMemo as useMemo2 } from "react";
400
+ import { StyleSheet as StyleSheet4, View as View4 } from "react-native";
357
401
  import { Icon as Icon3, Text as Text3 } from "react-native-paper";
358
- import { jsx as jsx6, jsxs as jsxs2 } from "react/jsx-runtime";
402
+ import { jsx as jsx7, jsxs as jsxs3 } from "react/jsx-runtime";
359
403
  function getProfilePickerContentSize(profileCount, hasManageRow = false, colorSection) {
360
404
  return pickerContentSize(profileCount + 1 + (hasManageRow ? 1 : 0), colorSection);
361
405
  }
362
- function ProfilePicker({ idPrefix, host, profiles, selectedId, takenId, color, dark, align, alignOverride, rotation = 0, guestLabel, nullLabel = "Player", nullIcon = "account-off-outline", onSelect, onManage, colorSection, triggerStyle }) {
406
+ function ProfilePicker({ idPrefix, host, profiles, selectedId, takenId, takenIds, color, dark, align, alignOverride, rotation = 0, guestLabel, nullLabel = "Player", nullIcon = "account-off-outline", onSelect, onManage, colorSection, triggerStyle }) {
363
407
  const id = `${idPrefix}-profile`;
364
408
  const colorPreview = useProfileColorPreview();
365
409
  const fg = dark ? "#FFFFFF" : "#000000";
366
410
  const selectedProfile = profiles.find((p) => p.id === selectedId) ?? null;
367
- const sortedProfiles = useMemo(() => [...profiles].sort((a, b) => a.name.localeCompare(b.name)), [profiles]);
411
+ const sortedProfiles = useMemo2(() => [...profiles].sort((a, b) => a.name.localeCompare(b.name)), [profiles]);
368
412
  const contentSize = getProfilePickerContentSize(profiles.length, !!onManage, colorSection);
413
+ const isTaken = (profileId) => profileId === takenId || profileId !== selectedId && !!takenIds?.includes(profileId);
369
414
  const handleSelectGuest = () => {
370
415
  onSelect(null);
371
416
  host.close();
372
417
  };
373
418
  const handleSelectExisting = (profile) => {
374
- if (profile.id === takenId) return;
419
+ if (isTaken(profile.id)) return;
375
420
  onSelect(profile);
376
421
  host.close();
377
422
  };
@@ -379,19 +424,19 @@ function ProfilePicker({ idPrefix, host, profiles, selectedId, takenId, color, d
379
424
  host.close();
380
425
  onManage?.();
381
426
  };
382
- const manageFooter = onManage ? /* @__PURE__ */ jsx6(TouchableRipple2, { onPress: handleManage, style: pickerMenuStyles.row, children: /* @__PURE__ */ jsxs2(View3, { style: styles3.manageRowInner, children: [
383
- /* @__PURE__ */ jsx6(Icon3, { source: "account-cog-outline", size: 16, color }),
384
- /* @__PURE__ */ jsx6(Text3, { style: [styles3.manageLabel, { color: fg }], children: "Manage" })
427
+ const manageFooter = onManage ? /* @__PURE__ */ jsx7(TouchableRipple2, { onPress: handleManage, style: pickerMenuStyles.row, children: /* @__PURE__ */ jsxs3(View4, { style: styles4.manageRowInner, children: [
428
+ /* @__PURE__ */ jsx7(Icon3, { source: "account-cog-outline", size: 16, color }),
429
+ /* @__PURE__ */ jsx7(Text3, { style: [styles4.manageLabel, { color: fg }], children: "Manage" })
385
430
  ] }) }) : void 0;
386
- return /* @__PURE__ */ jsxs2(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: [
387
- /* @__PURE__ */ jsx6(IconRow, { icon: nullIcon, label: nullLabel, selected: selectedId === null, color, fg, onPress: handleSelectGuest }),
431
+ 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: [
432
+ /* @__PURE__ */ jsx7(IconRow, { icon: nullIcon, label: nullLabel, selected: selectedId === null, color, fg, onPress: handleSelectGuest }),
388
433
  sortedProfiles.map((profile) => {
389
- const taken = profile.id === takenId;
434
+ const taken = isTaken(profile.id);
390
435
  const selected = selectedId === profile.id;
391
436
  const rowColor = colorPreview ? colorPreview(profile.color) : profile.color;
392
- return /* @__PURE__ */ jsx6(TouchableRipple2, { disabled: taken, onPress: () => handleSelectExisting(profile), style: [pickerMenuStyles.row, selected && { backgroundColor: rowColor }], children: /* @__PURE__ */ jsxs2(View3, { style: [pickerMenuStyles.rowInner, taken && pickerMenuStyles.rowDisabled], children: [
393
- /* @__PURE__ */ jsx6(ProfileChip, { profile, filled: !selected }),
394
- /* @__PURE__ */ jsx6(
437
+ 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: [
438
+ /* @__PURE__ */ jsx7(ProfileChip, { profile, filled: !selected }),
439
+ /* @__PURE__ */ jsx7(
395
440
  Text3,
396
441
  {
397
442
  style: [
@@ -408,7 +453,7 @@ function ProfilePicker({ idPrefix, host, profiles, selectedId, takenId, color, d
408
453
  })
409
454
  ] });
410
455
  }
411
- var styles3 = StyleSheet3.create({
456
+ var styles4 = StyleSheet4.create({
412
457
  manageLabel: {
413
458
  fontSize: 13,
414
459
  fontWeight: "700",
@@ -430,9 +475,9 @@ import { defaultColors as defaultColors2, FadingScrollView as FadingScrollView2,
430
475
  import { TouchableRipple as TouchableRipple3 } from "@rific/feedback-press";
431
476
  import { useFocusChain } from "@rific/focus-chain";
432
477
  import { useToast } from "@rific/toaster";
433
- import { ConfirmDialog, getInlineColorPickerContentSize, InlineColorPicker, usePopoverHost } from "@tastic/hud";
434
- import { useCallback, useEffect, useImperativeHandle, useMemo as useMemo2, useRef, useState as useState2 } from "react";
435
- import { KeyboardAvoidingView, Platform, StyleSheet as StyleSheet4, useWindowDimensions, View as View4 } from "react-native";
478
+ import { ConfirmDialog, getInlineColorPickerContentSize, InlineColorPicker, usePopoverHost } from "@tastic/hud/chrome";
479
+ import { useCallback, useEffect, useImperativeHandle, useMemo as useMemo3, useRef, useState as useState2 } from "react";
480
+ import { KeyboardAvoidingView, Platform, StyleSheet as StyleSheet5, useWindowDimensions, View as View5 } from "react-native";
436
481
  import { Icon as Icon4, Text as Text4, TextInput } from "react-native-paper";
437
482
  import { useSafeAreaInsets } from "react-native-safe-area-context";
438
483
 
@@ -469,7 +514,7 @@ function isValidProfile(value) {
469
514
  }
470
515
 
471
516
  // src/ProfilesManager.tsx
472
- import { Fragment as Fragment2, jsx as jsx7, jsxs as jsxs3 } from "react/jsx-runtime";
517
+ import { Fragment as Fragment2, jsx as jsx8, jsxs as jsxs4 } from "react/jsx-runtime";
473
518
  var CHIP_SIZE2 = 40;
474
519
  function deriveTag(name) {
475
520
  const trimmed = name.trim();
@@ -544,40 +589,40 @@ function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, h
544
589
  }
545
590
  setConfirmDeleteId(null);
546
591
  }, [confirmDeleteId, editingId, onDelete]);
547
- const sortedProfiles = useMemo2(() => [...profiles].sort((a, b) => a.name.localeCompare(b.name)), [profiles]);
592
+ const sortedProfiles = useMemo3(() => [...profiles].sort((a, b) => a.name.localeCompare(b.name)), [profiles]);
548
593
  const rows = [...sortedProfiles.map((profile) => ({ kind: "profile", profile })), { kind: "new" }];
549
- return /* @__PURE__ */ jsxs3(Fragment2, { children: [
550
- /* @__PURE__ */ jsxs3(View4, { style: [styles4.header, { paddingTop: 8 + insets.top, paddingLeft: 8 + insets.left }], children: [
594
+ return /* @__PURE__ */ jsxs4(Fragment2, { children: [
595
+ /* @__PURE__ */ jsxs4(View5, { style: [styles5.header, { paddingTop: 8 + insets.top, paddingLeft: 8 + insets.left }], children: [
551
596
  headerLeft,
552
- /* @__PURE__ */ jsx7(Text4, { variant: titleVariant, style: [styles4.title, { color: fg }], children: "Profiles" })
597
+ /* @__PURE__ */ jsx8(Text4, { variant: titleVariant, style: [styles5.title, { color: fg }], children: "Profiles" })
553
598
  ] }),
554
- /* @__PURE__ */ jsx7(KeyboardAvoidingView, { behavior: Platform.OS === "ios" ? "padding" : void 0, style: styles4.keyboardAvoiding, children: /* @__PURE__ */ jsx7(FadingScrollView2, { style: styles4.scrollView, contentContainerStyle: [styles4.content, { paddingBottom: 32 + insets.bottom }], showsVerticalScrollIndicator: false, keyboardShouldPersistTaps: "handled", children: rows.map((row) => {
599
+ /* @__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) => {
555
600
  if (row.kind === "new") {
556
601
  if (editingId !== "new") {
557
- return /* @__PURE__ */ jsx7(TouchableRipple3, { onPress: () => startEdit("new"), style: styles4.row, children: /* @__PURE__ */ jsxs3(View4, { style: styles4.rowInner, children: [
558
- /* @__PURE__ */ jsx7(View4, { style: [styles4.rowChip, styles4.newChip, { borderColor: fg }], children: /* @__PURE__ */ jsx7(Icon4, { source: "plus", size: 20, color: fg }) }),
559
- /* @__PURE__ */ jsx7(Text4, { variant: "bodyLarge", style: { color: fg }, children: "New Profile" })
602
+ return /* @__PURE__ */ jsx8(TouchableRipple3, { onPress: () => startEdit("new"), style: styles5.row, children: /* @__PURE__ */ jsxs4(View5, { style: styles5.rowInner, children: [
603
+ /* @__PURE__ */ jsx8(View5, { style: [styles5.rowChip, styles5.newChip, { borderColor: fg }], children: /* @__PURE__ */ jsx8(Icon4, { source: "plus", size: 20, color: fg }) }),
604
+ /* @__PURE__ */ jsx8(Text4, { variant: "bodyLarge", style: { color: fg }, children: "New Profile" })
560
605
  ] }) }, "new");
561
606
  }
562
- return /* @__PURE__ */ jsx7(EditRow, { draftName, onNameChange: handleNameChange, draftColor, onColorChange: setDraftColor, draftTag, onTagChange: handleTagChange, onSubmit: commitEdit, autoFocus: true, fgMuted, host, dark, colorPreview, useColorPickerAlign }, "new");
607
+ return /* @__PURE__ */ jsx8(EditRow, { draftName, onNameChange: handleNameChange, draftColor, onColorChange: setDraftColor, draftTag, onTagChange: handleTagChange, onSubmit: commitEdit, autoFocus: true, fgMuted, host, dark, colorPreview, useColorPickerAlign }, "new");
563
608
  }
564
609
  const { profile } = row;
565
610
  if (editingId === profile.id) {
566
- return /* @__PURE__ */ jsx7(EditRow, { draftName, onNameChange: handleNameChange, draftColor, onColorChange: setDraftColor, draftTag, onTagChange: handleTagChange, onSubmit: commitEdit, onDelete: () => setConfirmDeleteId(profile.id), fgMuted, host, dark, colorPreview, useColorPickerAlign }, profile.id);
611
+ 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);
567
612
  }
568
- return /* @__PURE__ */ jsx7(TouchableRipple3, { onPress: () => startEdit(profile), style: styles4.row, children: /* @__PURE__ */ jsxs3(View4, { style: styles4.rowInner, children: [
569
- /* @__PURE__ */ jsx7(ProfileChip, { profile, size: CHIP_SIZE2 }),
570
- /* @__PURE__ */ jsx7(Text4, { variant: "bodyLarge", style: [styles4.rowName, { color: fg }], numberOfLines: 1, children: profile.name })
613
+ return /* @__PURE__ */ jsx8(TouchableRipple3, { onPress: () => startEdit(profile), style: styles5.row, children: /* @__PURE__ */ jsxs4(View5, { style: styles5.rowInner, children: [
614
+ /* @__PURE__ */ jsx8(ProfileChip, { profile, size: CHIP_SIZE2 }),
615
+ /* @__PURE__ */ jsx8(Text4, { variant: "bodyLarge", style: [styles5.rowName, { color: fg }], numberOfLines: 1, children: profile.name })
571
616
  ] }) }, profile.id);
572
617
  }) }) }),
573
- /* @__PURE__ */ jsx7(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) })
618
+ /* @__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) })
574
619
  ] });
575
620
  }
576
621
  function AlignedColorPicker({ useAlign, ...props }) {
577
622
  const { width: windowWidth } = useWindowDimensions();
578
623
  const { width, height } = getInlineColorPickerContentSize(defaultColors2.length, windowWidth);
579
624
  const alignOverride = useAlign(props.host.openId === props.id, width, height);
580
- return /* @__PURE__ */ jsx7(InlineColorPicker, { ...props, alignOverride });
625
+ return /* @__PURE__ */ jsx8(InlineColorPicker, { ...props, alignOverride });
581
626
  }
582
627
  function EditRow({ draftName, onNameChange, draftColor, onColorChange, draftTag, onTagChange, onSubmit, onDelete, autoFocus, fgMuted, host, dark, colorPreview, useColorPickerAlign }) {
583
628
  const register = useFocusChain();
@@ -590,14 +635,14 @@ function EditRow({ draftName, onNameChange, draftColor, onColorChange, draftTag,
590
635
  useEffect(() => {
591
636
  if (host.openId === "color") tagFocusRef.current();
592
637
  }, [host.openId]);
593
- return /* @__PURE__ */ jsxs3(View4, { style: styles4.editRow, children: [
594
- useColorPickerAlign ? /* @__PURE__ */ jsx7(AlignedColorPicker, { useAlign: useColorPickerAlign, id: "color", host, value: draftColor, onChange: onColorChange, previewValue: colorPreview, tag: draftTag, dark }) : /* @__PURE__ */ jsx7(InlineColorPicker, { id: "color", host, value: draftColor, onChange: onColorChange, previewValue: colorPreview, tag: draftTag, dark }),
595
- /* @__PURE__ */ jsx7(TextInput, { ref: tag.ref, ...tag.props, value: draftTag, onChangeText: onTagChange, accessibilityLabel: "Tag", autoCapitalize: "characters", selectTextOnFocus: true, returnKeyType: "next", style: styles4.hiddenTagInput, pointerEvents: "none" }),
596
- /* @__PURE__ */ jsx7(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: styles4.nameInput }),
597
- onDelete && /* @__PURE__ */ jsx7(TouchableRipple3, { onPress: onDelete, style: styles4.iconButton, children: /* @__PURE__ */ jsx7(Icon4, { source: "trash-can-outline", size: 18, color: fgMuted }) })
638
+ return /* @__PURE__ */ jsxs4(View5, { style: styles5.editRow, children: [
639
+ 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 }),
640
+ /* @__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" }),
641
+ /* @__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 }),
642
+ onDelete && /* @__PURE__ */ jsx8(TouchableRipple3, { onPress: onDelete, style: styles5.iconButton, children: /* @__PURE__ */ jsx8(Icon4, { source: "trash-can-outline", size: 18, color: fgMuted }) })
598
643
  ] });
599
644
  }
600
- var styles4 = StyleSheet4.create({
645
+ var styles5 = StyleSheet5.create({
601
646
  content: {
602
647
  gap: 4,
603
648
  paddingHorizontal: 20
@@ -679,8 +724,8 @@ var styles4 = StyleSheet4.create({
679
724
  import { useAutoPaperTheme as useAutoPaperTheme2 } from "@rific/auto-paper";
680
725
  import { IconButton } from "@rific/feedback-press";
681
726
  import { useRef as useRef2 } from "react";
682
- import { StyleSheet as StyleSheet5, View as View5 } from "react-native";
683
- import { jsx as jsx8 } from "react/jsx-runtime";
727
+ import { StyleSheet as StyleSheet6, View as View6 } from "react-native";
728
+ import { jsx as jsx9 } from "react/jsx-runtime";
684
729
  function ProfilesScreen({ profiles, defaultColor, onCreate, onSave, onDelete, onBack, fg: fgOverride, fgMuted, bg: bgOverride, titleVariant, colorPreview, useColorPickerAlign }) {
685
730
  const { dark } = useAutoPaperTheme2();
686
731
  const managerRef = useRef2(null);
@@ -690,9 +735,9 @@ function ProfilesScreen({ profiles, defaultColor, onCreate, onSave, onDelete, on
690
735
  managerRef.current?.commitPendingEdit();
691
736
  onBack();
692
737
  };
693
- return /* @__PURE__ */ jsx8(View5, { style: [styles5.container, { backgroundColor: bg }], children: /* @__PURE__ */ jsx8(ProfilesManager, { ref: managerRef, profiles, defaultColor, onCreate, onSave, onDelete, fg, fgMuted, titleVariant, colorPreview, useColorPickerAlign, headerLeft: /* @__PURE__ */ jsx8(IconButton, { icon: "arrow-left", iconColor: fg, size: 24, onPress: handleBack, accessibilityLabel: "Back" }) }) });
738
+ return /* @__PURE__ */ jsx9(View6, { style: [styles6.container, { backgroundColor: bg }], children: /* @__PURE__ */ jsx9(ProfilesManager, { ref: managerRef, profiles, defaultColor, onCreate, onSave, onDelete, fg, fgMuted, titleVariant, colorPreview, useColorPickerAlign, headerLeft: /* @__PURE__ */ jsx9(IconButton, { icon: "arrow-left", iconColor: fg, size: 24, onPress: handleBack, accessibilityLabel: "Back" }) }) });
694
739
  }
695
- var styles5 = StyleSheet5.create({
740
+ var styles6 = StyleSheet6.create({
696
741
  container: {
697
742
  flex: 1
698
743
  }
@@ -803,6 +848,7 @@ function createProfileRecord(input) {
803
848
  }
804
849
 
805
850
  // src/sharedProfileStore.ts
851
+ import { Platform as Platform2 } from "react-native";
806
852
  var native = null;
807
853
  try {
808
854
  const { requireOptionalNativeModule } = __require("expo-modules-core");
@@ -811,15 +857,42 @@ try {
811
857
  native = null;
812
858
  }
813
859
  var PROFILES_KEY = "sharedProfiles";
814
- var isSharedProfileStoreAvailable = native !== null;
815
- async function readSharedProfiles(groupId) {
816
- if (!native) return [];
817
- let json = null;
860
+ var webKey = (groupId, key) => `${groupId}:${key}`;
861
+ var createWebStore = () => {
862
+ if (Platform2.OS !== "web") return null;
818
863
  try {
819
- json = await native.getSharedJSON(groupId, PROFILES_KEY);
864
+ const storage = globalThis.localStorage;
865
+ if (!storage) return null;
866
+ const probe = webKey("@tastic/profile", "probe");
867
+ storage.setItem(probe, "1");
868
+ storage.removeItem(probe);
869
+ let lastWriteFailed = false;
870
+ return {
871
+ storage,
872
+ store: {
873
+ getSharedJSON: async (groupId, key) => {
874
+ if (lastWriteFailed) throw new Error("The last write to the shared roster failed");
875
+ return storage.getItem(webKey(groupId, key));
876
+ },
877
+ setSharedJSON: async (groupId, key, value) => {
878
+ try {
879
+ storage.setItem(webKey(groupId, key), value);
880
+ lastWriteFailed = false;
881
+ } catch (error) {
882
+ lastWriteFailed = true;
883
+ throw error;
884
+ }
885
+ }
886
+ }
887
+ };
820
888
  } catch {
821
889
  return null;
822
890
  }
891
+ };
892
+ var web = native ? null : createWebStore();
893
+ if (web) native = web.store;
894
+ var isSharedProfileStoreAvailable = native !== null;
895
+ var parseRoster = (json) => {
823
896
  if (!json) return [];
824
897
  try {
825
898
  const parsed = JSON.parse(json);
@@ -829,6 +902,16 @@ async function readSharedProfiles(groupId) {
829
902
  } catch {
830
903
  return null;
831
904
  }
905
+ };
906
+ async function readSharedProfiles(groupId) {
907
+ if (!native) return [];
908
+ let json = null;
909
+ try {
910
+ json = await native.getSharedJSON(groupId, PROFILES_KEY);
911
+ } catch {
912
+ return null;
913
+ }
914
+ return parseRoster(json);
832
915
  }
833
916
  async function loadSharedProfiles(groupId) {
834
917
  return await readSharedProfiles(groupId) ?? [];
@@ -840,6 +923,19 @@ async function saveSharedProfiles(groupId, profiles) {
840
923
  } catch {
841
924
  }
842
925
  }
926
+ function subscribeSharedProfiles(groupId, listener) {
927
+ if (!web || typeof window === "undefined" || typeof window.addEventListener !== "function") return () => {
928
+ };
929
+ const storageKey = webKey(groupId, PROFILES_KEY);
930
+ const storage = web.storage;
931
+ const onStorage = (event) => {
932
+ if (event.key !== storageKey || event.storageArea !== storage || !event.newValue) return;
933
+ const profiles = parseRoster(event.newValue);
934
+ if (profiles !== null) listener(profiles);
935
+ };
936
+ window.addEventListener("storage", onStorage);
937
+ return () => window.removeEventListener("storage", onStorage);
938
+ }
843
939
 
844
940
  // src/redux/resolveInitialProfiles.ts
845
941
  async function resolveInitialProfiles(groupId, localFallback) {
@@ -922,9 +1018,13 @@ function useSharedProfilesSync({ groupId, onRemoteChange }) {
922
1018
  if (!cancelled && shared !== null) onRemoteChangeRef.current(shared);
923
1019
  });
924
1020
  });
1021
+ const unsubscribe = subscribeSharedProfiles(groupId, (shared) => {
1022
+ if (!cancelled && !pendingWriteRef.current) onRemoteChangeRef.current(shared);
1023
+ });
925
1024
  return () => {
926
1025
  cancelled = true;
927
1026
  sub.remove();
1027
+ unsubscribe();
928
1028
  };
929
1029
  }, [groupId]);
930
1030
  const syncToShared = useCallback2(
@@ -973,7 +1073,9 @@ export {
973
1073
  readSharedProfiles,
974
1074
  resolveInitialProfiles,
975
1075
  saveSharedProfiles,
1076
+ subscribeSharedProfiles,
976
1077
  useProfileColorPreview,
977
1078
  useProfileDisplayColor,
1079
+ useProfileSwatchRenderer,
978
1080
  useSharedProfilesSync
979
1081
  };