@tastic/profile 0.16.1 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -57,7 +57,10 @@ see its own doc comment for why that component is generic over your own `Profile
57
57
  (`{ value, onChange, previewValue?, swatches?, label?, caption? }`) puts a Color row at the top of
58
58
  the popover that opens a page of swatches (with a back row), and turns the trigger into one
59
59
  button that leads with a chip in `color` with the selected profile's tag; `triggerStyle` styles
60
- that button. Also exports
60
+ that button. A `flexShrink` in `triggerStyle` lets the button give way in a row too narrow for
61
+ it: its label is cut off with an ellipsis while the chip and chevron keep their size, and it never
62
+ gets narrower than those. With room it lays out as before. `OptionPicker` takes the same
63
+ `triggerStyle`. Also exports
61
64
  `getProfilePickerContentSize(profileCount, hasManageRow?, colorSection?)` - the popover's
62
65
  `{ width, height }`, for sizing your own placement hook.
63
66
  - **`ProfilesManager`**: the roster screen itself: create/rename/recolor/retag/delete, editing
@@ -84,7 +87,10 @@ see its own doc comment for why that component is generic over your own `Profile
84
87
  in that app's own local extension table yet - e.g. it was created on a different app in the
85
88
  group). This split is deliberate, not a limitation to work around: it's this package's own
86
89
  base-fields-only `Profile` boundary (see above), just applied to storage instead of just to the
87
- TypeScript type.
90
+ TypeScript type. A read drops any stored entry that this version's `isValidProfile` rejects (a
91
+ profile saved by a newer app under a rule loosened since), and `saveSharedProfiles` writes those
92
+ entries back after your roster as they were, so an older app can't delete a newer app's profile
93
+ just by saving.
88
94
 
89
95
  ```ts
90
96
  // Conceptual sketch of the pattern - see "Redux helpers for the shared roster" below for a
@@ -174,14 +180,16 @@ rect). Pass a placement **hook** as `useColorPickerAlign`:
174
180
  <ProfilesScreen useColorPickerAlign={useFrameAutoAlign} /* ...the usual props */ />
175
181
  ```
176
182
 
177
- It is called as `useColorPickerAlign(open, contentWidth, contentHeight)` - `open` is whether the swatch
178
- popover is showing, and the size is what `InlineColorPicker` itself computes (`@tastic/hud`'s
179
- `getInlineColorPickerContentSize` for the default swatch list at the raw window width) - and must return a
183
+ It is called as `useColorPickerAlign(open, contentWidth, contentHeight, column)` - `open` is whether the swatch
184
+ popover is showing, the size is what `InlineColorPicker` itself computes (`@tastic/hud`'s
185
+ `getInlineColorPickerContentSize` for the default swatch list at the raw window width), and `column` is a ref
186
+ to the roster's scroll view, which clips the popover at its edges: measure it to keep the popover inside a
187
+ roster narrower than the frame (a column capped in a wide window) - and must return a
180
188
  `ProfilePickerAlignResult`, which is handed to `InlineColorPicker` as its `alignOverride` (its `triggerRef`
181
189
  is the ref that gets measured). It is a hook, not a plain function: give it a stable identity for the life of
182
190
  the mount (a module-level function), since switching it at runtime remounts the swatch. Omit it and behaviour is
183
- identical to before. **Needs `@tastic/hud` >= 0.12.0** to have any effect - older hud has no `alignOverride`
184
- on `InlineColorPicker` and ignores it.
191
+ identical to before, and without a hook the popover is kept inside that column for you (`InlineColorPicker`'s
192
+ `bounds`, `@tastic/hud` 0.25.0+).
185
193
 
186
194
  ### Routed screens: `ProfilesScreen`
187
195
 
@@ -310,23 +318,27 @@ untouched. `useProfileColorPreview()` returns the provider's function (or `undef
310
318
 
311
319
  `renderSwatch` (also optional, with or without `preview`) draws the fill of every round swatch this
312
320
  package draws, in place of the flat color: `ProfileChip`, the pickers' chip trigger, the Color row's
313
- swatch and the color page's swatches. It's called as `renderSwatch(color, size)` with the drawn
314
- color (after `preview`) and the circle's diameter, and its drawing is clipped to the circle, behind
315
- the tag, icon or check. The tag and check still take their contrast from `color`, so treat it as
316
- the primary color of whatever you draw. Hangman draws a pie of each color's triad:
321
+ swatch and the color page's swatches. It's called as `renderSwatch(color, size, place)` with the
322
+ drawn color (after `preview`), the circle's diameter and where the swatch is (`ProfileSwatchPlace`:
323
+ `'chip'`, `'colorRow'` or `'colorPage'`, 0.16.2+), and its drawing is clipped to the circle, behind
324
+ the tag, icon or check. Return `null` (or `undefined`) to keep a swatch's flat fill there. The tag and
325
+ check still take their contrast from `color`, so treat it as the primary color of whatever you draw.
326
+ Hangman draws a pie of each color's triad where colors are chosen, and keeps its chips flat so their
327
+ tags stay legible:
317
328
 
318
329
  ```tsx
319
330
  import { getTriadicPalette } from '@rific/auto-paper'
320
331
  import { ProfileColorProvider, type ProfileSwatchRenderer } from '@tastic/profile'
321
332
 
322
- const renderSwatch: ProfileSwatchRenderer = (color, size) => <TriadPie colors={getTriadicPalette(color)} size={size} />
333
+ const renderSwatch: ProfileSwatchRenderer = (color, size, place) => (place === 'chip' ? null : <TriadPie colors={getTriadicPalette(color)} size={size} />)
323
334
  <ProfileColorProvider renderSwatch={renderSwatch}>{children}</ProfileColorProvider>
324
335
  ```
325
336
 
326
337
  (`TriadPie` is your own component, e.g. three `react-native-svg` arcs.) Give it a stable identity
327
- (module level, or `useCallback`). Rows that fill with a color (a selected picker row) stay flat, and
328
- so do `@tastic/hud`'s `InlineColorPicker` swatches in `ProfilesManager`'s edit row, which don't read
329
- this provider. `useProfileSwatchRenderer()` returns it (or `undefined`).
338
+ (module level, or `useCallback`). `ProfilesManager`'s edit row hands it to `@tastic/hud`'s
339
+ `InlineColorPicker` too: its trigger (the profile's color with its tag on it) as a `'chip'`, its grid
340
+ as the `'colorPage'`. Rows that fill with a color (a selected picker row) stay flat.
341
+ `useProfileSwatchRenderer()` returns it (or `undefined`).
330
342
 
331
343
  ## Per-seat color persistence
332
344
 
package/dist/index.d.mts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as react from 'react';
2
2
  import { RefObject, ReactNode, Ref } from 'react';
3
- import { PopoverHost, PopoverRotation } from '@tastic/hud/chrome';
4
- import { View, StyleProp, ViewStyle } from 'react-native';
3
+ import { ViewInstance, PopoverHost, PopoverRotation } from '@tastic/hud/chrome';
4
+ import { StyleProp, ViewStyle } from 'react-native';
5
5
  import { SeedColor } from '@rific/auto-paper';
6
6
 
7
7
  interface ProfilePickerColorSection {
@@ -19,7 +19,7 @@ interface ProfilePickerAlignResult {
19
19
  verticalAlign: 'below' | 'above';
20
20
  maxHeight: number;
21
21
  measured: boolean;
22
- triggerRef: RefObject<View | null>;
22
+ triggerRef: RefObject<ViewInstance | null>;
23
23
  }
24
24
  interface ProfilePickerContentSize {
25
25
  width: number;
@@ -67,7 +67,8 @@ interface Props {
67
67
  }
68
68
  declare function ProfileChip({ profile, size, filled }: Props): react.JSX.Element;
69
69
 
70
- type ProfileSwatchRenderer = (color: string, size: number) => ReactNode;
70
+ type ProfileSwatchPlace = 'chip' | 'colorRow' | 'colorPage';
71
+ type ProfileSwatchRenderer = (color: string, size: number, place: ProfileSwatchPlace) => ReactNode;
71
72
  interface ProfileColorProviderProps {
72
73
  preview?: (hex: string) => string;
73
74
  renderSwatch?: ProfileSwatchRenderer;
@@ -122,7 +123,7 @@ interface ProfilesManagerProps {
122
123
  fgMuted?: string;
123
124
  titleVariant?: MD3TextVariant$1;
124
125
  colorPreview?: (hex: string) => string;
125
- useColorPickerAlign?: (open: boolean, contentWidth: number, contentHeight: number) => ProfilePickerAlignResult;
126
+ useColorPickerAlign?: (open: boolean, contentWidth: number, contentHeight: number, column: RefObject<ViewInstance | null>) => ProfilePickerAlignResult;
126
127
  }
127
128
  declare function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, headerLeft, ref, fg: fgOverride, fgMuted: fgMutedOverride, titleVariant, colorPreview: colorPreviewProp, useColorPickerAlign }: ProfilesManagerProps): react.JSX.Element;
128
129
 
@@ -266,4 +267,4 @@ declare function loadSharedProfiles(groupId: string): Promise<Profile[]>;
266
267
  declare function saveSharedProfiles(groupId: string, profiles: Profile[]): Promise<void>;
267
268
  declare function subscribeSharedProfiles(groupId: string, listener: (profiles: Profile[]) => void): () => void;
268
269
 
269
- export { type CreateProfileInput, MAX_PROFILE_NAME_LENGTH, MAX_TAG_LENGTH, OptionPicker, type OptionPickerOption, type Profile, ProfileChip, type ProfileColorPick, ProfileColorProvider, type ProfileEditPatch, type ProfileExtensionActions, type ProfileExtensionSlice, type ProfileExtensionState, ProfilePicker, type ProfilePickerAlignResult, type ProfilePickerColorSection, type ProfilePickerContentSize, type ProfileSelectionActions, type ProfileSelectionSlice, type ProfileSelectionState, type ProfileSwatchRenderer, ProfilesManager, type ProfilesManagerHandle, type ProfilesManagerProps, ProfilesScreen, type ProfilesScreenProps, type SeatColorsActions, type SeatColorsSlice, type SeatColorsState, type SelectProfilePayload, type SetLastGuestColorPayload, type SetProfileColorPayload, type SetProfileExtensionPayload, type UpdateProfilePayload, type UseSharedProfilesSyncOptions, type UseSharedProfilesSyncResult, avoidSeatColorClash, createProfileExtensionSlice, createProfileRecord, createProfileSelectionSlice, createSeatColorsSlice, getOptionPickerContentSize, getProfilePickerContentSize, isSharedProfileStoreAvailable, isValidProfile, isValidTag, loadSharedProfiles, profileSeatColor, profilesActions, profilesReducer, readSharedProfiles, resolveInitialProfiles, saveSharedProfiles, subscribeSharedProfiles, useProfileColorPreview, useProfileDisplayColor, useProfileSwatchRenderer, useSharedProfilesSync };
270
+ export { type CreateProfileInput, MAX_PROFILE_NAME_LENGTH, MAX_TAG_LENGTH, OptionPicker, type OptionPickerOption, type Profile, ProfileChip, type ProfileColorPick, ProfileColorProvider, type ProfileEditPatch, type ProfileExtensionActions, type ProfileExtensionSlice, type ProfileExtensionState, ProfilePicker, type ProfilePickerAlignResult, type ProfilePickerColorSection, type ProfilePickerContentSize, type ProfileSelectionActions, type ProfileSelectionSlice, type ProfileSelectionState, type ProfileSwatchPlace, type ProfileSwatchRenderer, ProfilesManager, type ProfilesManagerHandle, type ProfilesManagerProps, ProfilesScreen, type ProfilesScreenProps, type SeatColorsActions, type SeatColorsSlice, type SeatColorsState, type SelectProfilePayload, type SetLastGuestColorPayload, type SetProfileColorPayload, type SetProfileExtensionPayload, type UpdateProfilePayload, type UseSharedProfilesSyncOptions, type UseSharedProfilesSyncResult, avoidSeatColorClash, createProfileExtensionSlice, createProfileRecord, createProfileSelectionSlice, createSeatColorsSlice, getOptionPickerContentSize, getProfilePickerContentSize, isSharedProfileStoreAvailable, isValidProfile, isValidTag, loadSharedProfiles, profileSeatColor, profilesActions, profilesReducer, readSharedProfiles, resolveInitialProfiles, saveSharedProfiles, subscribeSharedProfiles, useProfileColorPreview, useProfileDisplayColor, useProfileSwatchRenderer, useSharedProfilesSync };
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as react from 'react';
2
2
  import { RefObject, ReactNode, Ref } from 'react';
3
- import { PopoverHost, PopoverRotation } from '@tastic/hud/chrome';
4
- import { View, StyleProp, ViewStyle } from 'react-native';
3
+ import { ViewInstance, PopoverHost, PopoverRotation } from '@tastic/hud/chrome';
4
+ import { StyleProp, ViewStyle } from 'react-native';
5
5
  import { SeedColor } from '@rific/auto-paper';
6
6
 
7
7
  interface ProfilePickerColorSection {
@@ -19,7 +19,7 @@ interface ProfilePickerAlignResult {
19
19
  verticalAlign: 'below' | 'above';
20
20
  maxHeight: number;
21
21
  measured: boolean;
22
- triggerRef: RefObject<View | null>;
22
+ triggerRef: RefObject<ViewInstance | null>;
23
23
  }
24
24
  interface ProfilePickerContentSize {
25
25
  width: number;
@@ -67,7 +67,8 @@ interface Props {
67
67
  }
68
68
  declare function ProfileChip({ profile, size, filled }: Props): react.JSX.Element;
69
69
 
70
- type ProfileSwatchRenderer = (color: string, size: number) => ReactNode;
70
+ type ProfileSwatchPlace = 'chip' | 'colorRow' | 'colorPage';
71
+ type ProfileSwatchRenderer = (color: string, size: number, place: ProfileSwatchPlace) => ReactNode;
71
72
  interface ProfileColorProviderProps {
72
73
  preview?: (hex: string) => string;
73
74
  renderSwatch?: ProfileSwatchRenderer;
@@ -122,7 +123,7 @@ interface ProfilesManagerProps {
122
123
  fgMuted?: string;
123
124
  titleVariant?: MD3TextVariant$1;
124
125
  colorPreview?: (hex: string) => string;
125
- useColorPickerAlign?: (open: boolean, contentWidth: number, contentHeight: number) => ProfilePickerAlignResult;
126
+ useColorPickerAlign?: (open: boolean, contentWidth: number, contentHeight: number, column: RefObject<ViewInstance | null>) => ProfilePickerAlignResult;
126
127
  }
127
128
  declare function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, headerLeft, ref, fg: fgOverride, fgMuted: fgMutedOverride, titleVariant, colorPreview: colorPreviewProp, useColorPickerAlign }: ProfilesManagerProps): react.JSX.Element;
128
129
 
@@ -266,4 +267,4 @@ declare function loadSharedProfiles(groupId: string): Promise<Profile[]>;
266
267
  declare function saveSharedProfiles(groupId: string, profiles: Profile[]): Promise<void>;
267
268
  declare function subscribeSharedProfiles(groupId: string, listener: (profiles: Profile[]) => void): () => void;
268
269
 
269
- export { type CreateProfileInput, MAX_PROFILE_NAME_LENGTH, MAX_TAG_LENGTH, OptionPicker, type OptionPickerOption, type Profile, ProfileChip, type ProfileColorPick, ProfileColorProvider, type ProfileEditPatch, type ProfileExtensionActions, type ProfileExtensionSlice, type ProfileExtensionState, ProfilePicker, type ProfilePickerAlignResult, type ProfilePickerColorSection, type ProfilePickerContentSize, type ProfileSelectionActions, type ProfileSelectionSlice, type ProfileSelectionState, type ProfileSwatchRenderer, ProfilesManager, type ProfilesManagerHandle, type ProfilesManagerProps, ProfilesScreen, type ProfilesScreenProps, type SeatColorsActions, type SeatColorsSlice, type SeatColorsState, type SelectProfilePayload, type SetLastGuestColorPayload, type SetProfileColorPayload, type SetProfileExtensionPayload, type UpdateProfilePayload, type UseSharedProfilesSyncOptions, type UseSharedProfilesSyncResult, avoidSeatColorClash, createProfileExtensionSlice, createProfileRecord, createProfileSelectionSlice, createSeatColorsSlice, getOptionPickerContentSize, getProfilePickerContentSize, isSharedProfileStoreAvailable, isValidProfile, isValidTag, loadSharedProfiles, profileSeatColor, profilesActions, profilesReducer, readSharedProfiles, resolveInitialProfiles, saveSharedProfiles, subscribeSharedProfiles, useProfileColorPreview, useProfileDisplayColor, useProfileSwatchRenderer, useSharedProfilesSync };
270
+ export { type CreateProfileInput, MAX_PROFILE_NAME_LENGTH, MAX_TAG_LENGTH, OptionPicker, type OptionPickerOption, type Profile, ProfileChip, type ProfileColorPick, ProfileColorProvider, type ProfileEditPatch, type ProfileExtensionActions, type ProfileExtensionSlice, type ProfileExtensionState, ProfilePicker, type ProfilePickerAlignResult, type ProfilePickerColorSection, type ProfilePickerContentSize, type ProfileSelectionActions, type ProfileSelectionSlice, type ProfileSelectionState, type ProfileSwatchPlace, type ProfileSwatchRenderer, ProfilesManager, type ProfilesManagerHandle, type ProfilesManagerProps, ProfilesScreen, type ProfilesScreenProps, type SeatColorsActions, type SeatColorsSlice, type SeatColorsState, type SelectProfilePayload, type SetLastGuestColorPayload, type SetProfileColorPayload, type SetProfileExtensionPayload, type UpdateProfilePayload, type UseSharedProfilesSyncOptions, type UseSharedProfilesSyncResult, avoidSeatColorClash, createProfileExtensionSlice, createProfileRecord, createProfileSelectionSlice, createSeatColorsSlice, getOptionPickerContentSize, getProfilePickerContentSize, isSharedProfileStoreAvailable, isValidProfile, isValidTag, loadSharedProfiles, profileSeatColor, profilesActions, profilesReducer, readSharedProfiles, resolveInitialProfiles, saveSharedProfiles, subscribeSharedProfiles, useProfileColorPreview, useProfileDisplayColor, useProfileSwatchRenderer, useSharedProfilesSync };
package/dist/index.js CHANGED
@@ -88,8 +88,11 @@ function useProfileDisplayColor(hex) {
88
88
  // src/SwatchLayer.tsx
89
89
  var import_react_native = require("react-native");
90
90
  var import_jsx_runtime2 = require("react/jsx-runtime");
91
- function SwatchLayer({ render, color, size }) {
92
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native.View, { style: [styles.layer, { borderRadius: size / 2, height: size, width: size }], children: render(color, size) });
91
+ function drawSwatch(render, color, size, place) {
92
+ return render ? render(color, size, place) ?? null : null;
93
+ }
94
+ function SwatchLayer({ size, children }) {
95
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native.View, { style: [styles.layer, { borderRadius: size / 2, height: size, width: size }], children });
93
96
  }
94
97
  var styles = import_react_native.StyleSheet.create({
95
98
  layer: {
@@ -106,10 +109,11 @@ var import_jsx_runtime3 = require("react/jsx-runtime");
106
109
  var EMOJI_PATTERN = /\p{Extended_Pictographic}/u;
107
110
  function IdentityChip({ color, tag, icon, size, filled = true }) {
108
111
  const renderSwatch = useProfileSwatchRenderer();
112
+ const drawing = filled ? drawSwatch(renderSwatch, color, size, "chip") : null;
109
113
  const contrastColor = (0, import_auto_paper.getContrastColor)(color);
110
114
  const tagFontSize = size * (tag && EMOJI_PATTERN.test(tag) ? 0.6 : tag && Array.from(tag).length >= 3 ? 0.32 : 0.4);
111
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_react_native2.View, { style: [styles2.chip, { borderRadius: size / 2, height: size, width: size }, filled && !renderSwatch && { backgroundColor: color }], children: [
112
- filled && renderSwatch && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SwatchLayer, { render: renderSwatch, color, size }),
115
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_react_native2.View, { style: [styles2.chip, { borderRadius: size / 2, height: size, width: size }, filled && drawing === null && { backgroundColor: color }], children: [
116
+ drawing !== null && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SwatchLayer, { size, children: drawing }),
113
117
  tag ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native_paper.Text, { style: [styles2.tag, { color: contrastColor, fontSize: tagFontSize }], numberOfLines: 1, adjustsFontSizeToFit: true, children: tag }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native_paper.Icon, { source: icon, size: size * 0.6, color: contrastColor })
114
118
  ] });
115
119
  }
@@ -134,6 +138,12 @@ var MENU_BORDER_WIDTH = 2;
134
138
  var ROW_INSET = 12;
135
139
  var CHIP_SIZE = 32;
136
140
  var CHIP_TRIGGER_PADDING = 6;
141
+ var CHIP_TRIGGER_END_PADDING = CHIP_TRIGGER_PADDING + 8;
142
+ var CHIP_TRIGGER_GAP = 8;
143
+ var PLAIN_TRIGGER_GAP = 2;
144
+ var CHEVRON_SIZE = 16;
145
+ var CHIP_TRIGGER_FIXED_WIDTH = CHIP_TRIGGER_PADDING + CHIP_SIZE + 2 * CHIP_TRIGGER_GAP + CHEVRON_SIZE + CHIP_TRIGGER_END_PADDING;
146
+ var PLAIN_TRIGGER_FIXED_WIDTH = PLAIN_TRIGGER_GAP + CHEVRON_SIZE;
137
147
  var SWATCH_SIZE = 28;
138
148
  var SWATCH_MIN_GAP = 6;
139
149
  var SWATCH_RING_WIDTH = 2;
@@ -178,11 +188,15 @@ function PickerMenu({ id, host, color, dark, label, chip, colorSection, homeColo
178
188
  };
179
189
  const triggerLabel = label.toUpperCase();
180
190
  const showFooter = !!footer && !showColorPage;
181
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native3.View, { style: [styles3.anchor, open && styles3.anchorOpen], onLayout: handleAnchorLayout, children: [
182
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_feedback_press.TouchableRipple, { onPress: handleTriggerPress, borderless: chipTrigger, style: [styles3.trigger, chipTrigger && styles3.chipTrigger, triggerStyle], accessibilityRole: "button", accessibilityLabel: triggerLabel, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native3.View, { ref: triggerRef, collapsable: false, style: chipTrigger ? styles3.chipTriggerInner : styles3.triggerInner, children: [
191
+ const flatTriggerStyle = import_react_native3.StyleSheet.flatten(triggerStyle) ?? {};
192
+ const { flexShrink, ...pillStyle } = flatTriggerStyle;
193
+ const shrinks = typeof flexShrink === "number" && flexShrink > 0;
194
+ const anchorShrink = shrinks ? { flexShrink, minWidth: chipTrigger ? CHIP_TRIGGER_FIXED_WIDTH : PLAIN_TRIGGER_FIXED_WIDTH } : void 0;
195
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native3.View, { style: [styles3.anchor, anchorShrink, open && styles3.anchorOpen], onLayout: handleAnchorLayout, children: [
196
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_feedback_press.TouchableRipple, { onPress: handleTriggerPress, borderless: chipTrigger, style: [styles3.trigger, chipTrigger && styles3.chipTrigger, shrinks && styles3.triggerShrinks, shrinks ? pillStyle : triggerStyle], accessibilityRole: "button", accessibilityLabel: triggerLabel, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native3.View, { ref: triggerRef, collapsable: false, style: chipTrigger ? styles3.chipTriggerInner : styles3.triggerInner, children: [
183
197
  chip && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(IdentityChip, { color, tag: chip.tag, icon: chip.icon, size: CHIP_SIZE }),
184
198
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native_paper2.Text, { style: [styles3.triggerLabel, { color: chipTrigger ? fg : color }], numberOfLines: 1, children: triggerLabel }),
185
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native_paper2.Icon, { source: "menu-down", size: 16, color: chipTrigger ? fg : color })
199
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native_paper2.Icon, { source: "menu-down", size: CHEVRON_SIZE, color: chipTrigger ? fg : color })
186
200
  ] }) }),
187
201
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_chrome.PopoverBody, { visible: open && measured, align, verticalAlign, caretColor: menuBg, caretBorderColor: color, caretBorderWidth: MENU_BORDER_WIDTH, triggerSize: chipTrigger ? triggerWidth : TRIGGER_HEIGHT, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
188
202
  import_react_native3.View,
@@ -221,9 +235,10 @@ function ColorRow({ section, fg, dividerColor, onPress }) {
221
235
  const { value, previewValue, label = "Color", caption } = section;
222
236
  const renderSwatch = useProfileSwatchRenderer();
223
237
  const drawn = previewValue ? previewValue(value) : value;
238
+ const drawing = drawSwatch(renderSwatch, drawn, COLOR_ROW_SWATCH_SIZE, "colorRow");
224
239
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
225
240
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_feedback_press.TouchableRipple, { onPress, style: [pickerMenuStyles.row, { height: colorRowHeight(caption) }], accessibilityRole: "button", accessibilityLabel: caption ? `${label}, ${caption}` : label, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native3.View, { style: pickerMenuStyles.rowInner, children: [
226
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native3.View, { style: [styles3.colorRowSwatch, !renderSwatch && { backgroundColor: drawn }], children: renderSwatch && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(SwatchLayer, { render: renderSwatch, color: drawn, size: COLOR_ROW_SWATCH_SIZE }) }),
241
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native3.View, { style: [styles3.colorRowSwatch, drawing === null && { backgroundColor: drawn }], children: drawing !== null && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(SwatchLayer, { size: COLOR_ROW_SWATCH_SIZE, children: drawing }) }),
227
242
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native3.View, { style: styles3.colorRowText, children: [
228
243
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native_paper2.Text, { style: [styles3.colorRowLabel, { color: fg }], numberOfLines: 1, children: label }),
229
244
  caption ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native_paper2.Text, { style: [styles3.colorRowCaption, { color: fg }], numberOfLines: 1, children: caption }) : null
@@ -250,9 +265,11 @@ function ColorPage({ section, homeColor, fg, onBack }) {
250
265
  const swatchDisplay = displayFor(swatch.value);
251
266
  const contrast = (0, import_auto_paper2.getContrastColor)(swatchDisplay);
252
267
  const accessibilityLabel = swappable ? `${swatch.label}, swap` : taken ? `${swatch.label}, taken` : isHomeColor ? `${swatch.label}, profile color` : swatch.label;
268
+ const drawnSize = selected ? SWATCH_SIZE - 2 * SWATCH_RING_WIDTH : SWATCH_SIZE;
269
+ const drawing = drawSwatch(renderSwatch, swatchDisplay, drawnSize, "colorPage");
253
270
  const mark = selected ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native_paper2.Icon, { source: "check", size: 16, color: contrast }) : swappable ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native_paper2.Icon, { source: "swap-horizontal", size: 16, color: contrast }) : taken ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native_paper2.Icon, { source: "close", size: 16, color: contrast }) : isHomeColor ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native3.View, { style: [styles3.homeDot, { backgroundColor: contrast }] }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native3.View, {});
254
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_feedback_press.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__ */ (0, import_jsx_runtime4.jsxs)(import_react_native3.View, { style: styles3.swatchFace, children: [
255
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(SwatchLayer, { render: renderSwatch, color: swatchDisplay, size: selected ? SWATCH_SIZE - 2 * SWATCH_RING_WIDTH : SWATCH_SIZE }),
271
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_feedback_press.TouchableRipple, { disabled: taken && !swappable, onPress: () => onChange(swatch.value), borderless: true, style: [styles3.swatch, drawing === null && { backgroundColor: swatchDisplay }, selected && styles3.swatchSelected, taken && !swappable && styles3.swatchTaken], accessibilityRole: "button", accessibilityLabel, accessibilityState: { selected, disabled: taken && !swappable }, children: drawing !== null ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native3.View, { style: styles3.swatchFace, children: [
272
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(SwatchLayer, { size: drawnSize, children: drawing }),
256
273
  mark
257
274
  ] }) : mark }, swatch.value);
258
275
  }) })
@@ -297,10 +314,10 @@ var styles3 = import_react_native3.StyleSheet.create({
297
314
  chipTriggerInner: {
298
315
  alignItems: "center",
299
316
  flexDirection: "row",
300
- gap: 8,
317
+ gap: CHIP_TRIGGER_GAP,
301
318
  height: CHIP_SIZE + 2 * CHIP_TRIGGER_PADDING,
302
319
  paddingLeft: CHIP_TRIGGER_PADDING,
303
- paddingRight: CHIP_TRIGGER_PADDING + 8
320
+ paddingRight: CHIP_TRIGGER_END_PADDING
304
321
  },
305
322
  colorRowCaption: {
306
323
  fontSize: 12,
@@ -409,13 +426,22 @@ var styles3 = import_react_native3.StyleSheet.create({
409
426
  triggerInner: {
410
427
  alignItems: "center",
411
428
  flexDirection: "row",
412
- gap: 2,
429
+ gap: PLAIN_TRIGGER_GAP,
413
430
  height: TRIGGER_HEIGHT
414
431
  },
432
+ // Lets the label, alone in its row, give way to a trigger narrower than its name (see `shrinks`).
433
+ // With room it keeps its natural width, so this changes nothing there.
415
434
  triggerLabel: {
435
+ flexShrink: 1,
416
436
  fontSize: 13,
417
437
  fontWeight: "700",
418
- letterSpacing: 1
438
+ letterSpacing: 1,
439
+ minWidth: 0
440
+ },
441
+ // A shrinking trigger (see `shrinks`): no wider than its anchor. Its row is then laid out at that
442
+ // width, which is what cuts the label off.
443
+ triggerShrinks: {
444
+ maxWidth: "100%"
419
445
  }
420
446
  });
421
447
 
@@ -564,6 +590,7 @@ function isValidProfile(value) {
564
590
  // src/ProfilesManager.tsx
565
591
  var import_jsx_runtime8 = require("react/jsx-runtime");
566
592
  var CHIP_SIZE2 = 40;
593
+ var textInputRef = (ref) => ref;
567
594
  function deriveTag(name) {
568
595
  const trimmed = name.trim();
569
596
  return trimmed ? trimmed[0].toUpperCase() : "";
@@ -574,6 +601,8 @@ function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, h
574
601
  const { dark } = (0, import_auto_paper4.useAutoPaperTheme)();
575
602
  const insets = (0, import_react_native_safe_area_context.useSafeAreaInsets)();
576
603
  const { error: showErrorToast } = (0, import_toaster.useToast)();
604
+ const columnRef = (0, import_react4.useRef)(null);
605
+ const renderSwatch = useProfileSwatchRenderer();
577
606
  const host = (0, import_chrome2.usePopoverHost)();
578
607
  const fg = fgOverride ?? (dark ? "#FFFFFF" : "#000000");
579
608
  const fgMuted = fgMutedOverride ?? (dark ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.5)");
@@ -644,7 +673,7 @@ function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, h
644
673
  headerLeft,
645
674
  /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native_paper4.Text, { variant: titleVariant, style: [styles5.title, { color: fg }], children: "Profiles" })
646
675
  ] }),
647
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native5.KeyboardAvoidingView, { behavior: import_react_native5.Platform.OS === "ios" ? "padding" : void 0, style: styles5.keyboardAvoiding, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_auto_paper4.FadingScrollView, { style: styles5.scrollView, contentContainerStyle: [styles5.content, { paddingBottom: 32 + insets.bottom }], showsVerticalScrollIndicator: false, keyboardShouldPersistTaps: "handled", children: rows.map((row) => {
676
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native5.KeyboardAvoidingView, { behavior: import_react_native5.Platform.OS === "ios" ? "padding" : void 0, style: styles5.keyboardAvoiding, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native5.View, { ref: columnRef, collapsable: false, style: styles5.column, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_auto_paper4.FadingScrollView, { style: styles5.scrollView, contentContainerStyle: [styles5.content, { paddingBottom: 32 + insets.bottom }], showsVerticalScrollIndicator: false, keyboardShouldPersistTaps: "handled", children: rows.map((row) => {
648
677
  if (row.kind === "new") {
649
678
  if (editingId !== "new") {
650
679
  return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_feedback_press3.TouchableRipple, { onPress: () => startEdit("new"), style: styles5.row, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_react_native5.View, { style: styles5.rowInner, children: [
@@ -652,27 +681,28 @@ function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, h
652
681
  /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native_paper4.Text, { variant: "bodyLarge", style: { color: fg }, children: "New Profile" })
653
682
  ] }) }, "new");
654
683
  }
655
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(EditRow, { draftName, onNameChange: handleNameChange, draftColor, onColorChange: setDraftColor, draftTag, onTagChange: handleTagChange, onSubmit: commitEdit, autoFocus: true, fgMuted, host, dark, colorPreview, useColorPickerAlign }, "new");
684
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(EditRow, { draftName, onNameChange: handleNameChange, draftColor, onColorChange: setDraftColor, draftTag, onTagChange: handleTagChange, onSubmit: commitEdit, autoFocus: true, fgMuted, host, dark, colorPreview, useColorPickerAlign, column: columnRef, renderSwatch }, "new");
656
685
  }
657
686
  const { profile } = row;
658
687
  if (editingId === profile.id) {
659
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(EditRow, { draftName, onNameChange: handleNameChange, draftColor, onColorChange: setDraftColor, draftTag, onTagChange: handleTagChange, onSubmit: commitEdit, onDelete: () => setConfirmDeleteId(profile.id), fgMuted, host, dark, colorPreview, useColorPickerAlign }, profile.id);
688
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(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);
660
689
  }
661
690
  return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_feedback_press3.TouchableRipple, { onPress: () => startEdit(profile), style: styles5.row, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_react_native5.View, { style: styles5.rowInner, children: [
662
691
  /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(ProfileChip, { profile, size: CHIP_SIZE2 }),
663
692
  /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native_paper4.Text, { variant: "bodyLarge", style: [styles5.rowName, { color: fg }], numberOfLines: 1, children: profile.name })
664
693
  ] }) }, profile.id);
665
- }) }) }),
694
+ }) }) }) }),
666
695
  /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_chrome2.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) })
667
696
  ] });
668
697
  }
669
- function AlignedColorPicker({ useAlign, ...props }) {
698
+ function AlignedColorPicker({ useAlign, column, ...props }) {
670
699
  const { width: windowWidth } = (0, import_react_native5.useWindowDimensions)();
671
700
  const { width, height } = (0, import_chrome2.getInlineColorPickerContentSize)(import_auto_paper4.defaultColors.length, windowWidth);
672
- const alignOverride = useAlign(props.host.openId === props.id, width, height);
701
+ const alignOverride = useAlign(props.host.openId === props.id, width, height, column);
673
702
  return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_chrome2.InlineColorPicker, { ...props, alignOverride });
674
703
  }
675
- function EditRow({ draftName, onNameChange, draftColor, onColorChange, draftTag, onTagChange, onSubmit, onDelete, autoFocus, fgMuted, host, dark, colorPreview, useColorPickerAlign }) {
704
+ 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]);
676
706
  const register = (0, import_focus_chain.useFocusChain)();
677
707
  const tag = register();
678
708
  const name = register();
@@ -684,13 +714,17 @@ function EditRow({ draftName, onNameChange, draftColor, onColorChange, draftTag,
684
714
  if (host.openId === "color") tagFocusRef.current();
685
715
  }, [host.openId]);
686
716
  return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_react_native5.View, { style: styles5.editRow, children: [
687
- useColorPickerAlign ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(AlignedColorPicker, { useAlign: useColorPickerAlign, id: "color", host, value: draftColor, onChange: onColorChange, previewValue: colorPreview, tag: draftTag, dark }) : /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_chrome2.InlineColorPicker, { id: "color", host, value: draftColor, onChange: onColorChange, previewValue: colorPreview, tag: draftTag, dark }),
688
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native_paper4.TextInput, { ref: tag.ref, ...tag.props, value: draftTag, onChangeText: onTagChange, accessibilityLabel: "Tag", autoCapitalize: "characters", selectTextOnFocus: true, returnKeyType: "next", style: styles5.hiddenTagInput, pointerEvents: "none" }),
689
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native_paper4.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 }),
717
+ useColorPickerAlign ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(AlignedColorPicker, { useAlign: useColorPickerAlign, column, id: "color", host, value: draftColor, onChange: onColorChange, previewValue: colorPreview, tag: draftTag, dark, renderSwatch: pickerSwatch }) : /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_chrome2.InlineColorPicker, { id: "color", host, value: draftColor, onChange: onColorChange, previewValue: colorPreview, tag: draftTag, dark, renderSwatch: pickerSwatch, bounds: column }),
718
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native_paper4.TextInput, { ref: textInputRef(tag.ref), ...tag.props, value: draftTag, onChangeText: onTagChange, accessibilityLabel: "Tag", autoCapitalize: "characters", selectTextOnFocus: true, returnKeyType: "next", style: styles5.hiddenTagInput, pointerEvents: "none" }),
719
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native_paper4.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 }),
690
720
  onDelete && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_feedback_press3.TouchableRipple, { onPress: onDelete, style: styles5.iconButton, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native_paper4.Icon, { source: "trash-can-outline", size: 18, color: fgMuted }) })
691
721
  ] });
692
722
  }
693
723
  var styles5 = import_react_native5.StyleSheet.create({
724
+ // The roster's column, around the scroll view (see columnRef).
725
+ column: {
726
+ flex: 1
727
+ },
694
728
  content: {
695
729
  gap: 4,
696
730
  paddingHorizontal: 20
@@ -964,10 +998,29 @@ async function readSharedProfiles(groupId) {
964
998
  async function loadSharedProfiles(groupId) {
965
999
  return await readSharedProfiles(groupId) ?? [];
966
1000
  }
1001
+ var hasId = (entry) => typeof entry === "object" && entry !== null && typeof entry.id === "string" && entry.id !== "";
1002
+ async function unreadableEntries(groupId, profiles) {
1003
+ let json;
1004
+ try {
1005
+ json = web ? web.storage.getItem(webKey(groupId, PROFILES_KEY)) : await native?.getSharedJSON(groupId, PROFILES_KEY);
1006
+ } catch {
1007
+ return [];
1008
+ }
1009
+ if (!json) return [];
1010
+ try {
1011
+ const parsed = JSON.parse(json);
1012
+ if (!Array.isArray(parsed)) return [];
1013
+ const saved = new Set(profiles.map((profile) => profile.id));
1014
+ return parsed.filter((entry) => !isValidProfile(entry) && hasId(entry) && !saved.has(entry.id));
1015
+ } catch {
1016
+ return [];
1017
+ }
1018
+ }
967
1019
  async function saveSharedProfiles(groupId, profiles) {
968
1020
  if (!native) return;
969
1021
  try {
970
- await native.setSharedJSON(groupId, PROFILES_KEY, JSON.stringify(profiles));
1022
+ const kept = await unreadableEntries(groupId, profiles);
1023
+ await native.setSharedJSON(groupId, PROFILES_KEY, JSON.stringify([...profiles, ...kept]));
971
1024
  } catch {
972
1025
  }
973
1026
  }