@tastic/profile 0.8.0 → 0.9.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
@@ -136,6 +136,28 @@ const placement = useMyRotatedAlign(open, width, height) // returns a ProfilePic
136
136
  wide, and `8 * 2 + (profileCount + 1 + (hasManageRow ? 1 : 0)) * 36` tall (the null/"guest" row, one
137
137
  row per profile, and the Manage row when `onManage` is passed).
138
138
 
139
+
140
+ #### The Profiles screen's color swatch: `useColorPickerAlign`
141
+
142
+ `ProfilesManager` (and `ProfilesScreen`, which forwards it) renders `@tastic/hud`'s `InlineColorPicker`
143
+ for the swatch popover in an edit row, and owns the popover state internally — so the host cannot pass
144
+ `alignOverride` to it directly. Inside a fake-rotated frame that popover has the same misplacement (and
145
+ `InlineColorPicker` also feeds `useAutoAlign` its ambient rotation, double-rotating an already-transformed
146
+ rect). Pass a placement **hook** as `useColorPickerAlign`:
147
+
148
+ ```tsx
149
+ <ProfilesScreen useColorPickerAlign={useFrameAutoAlign} /* ...the usual props */ />
150
+ ```
151
+
152
+ It is called as `useColorPickerAlign(open, contentWidth, contentHeight)` — `open` is whether the swatch
153
+ popover is showing, and the size is what `InlineColorPicker` itself computes (`@tastic/hud`'s
154
+ `getInlineColorPickerContentSize` for the default swatch list at the raw window width) — and must return a
155
+ `ProfilePickerAlignResult`, which is handed to `InlineColorPicker` as its `alignOverride` (its `triggerRef`
156
+ is the ref that gets measured). It is a hook, not a plain function: give it a stable identity for the life of
157
+ the mount (a module-level function), since switching it at runtime remounts the swatch. Omit it and behaviour is
158
+ identical to before. **Needs `@tastic/hud` >= 0.12.0** to have any effect — older hud has no `alignOverride`
159
+ on `InlineColorPicker` and ignores it.
160
+
139
161
  ### Routed screens: `ProfilesScreen`
140
162
 
141
163
  `ProfilesScreen` is the routed-screen shell every app in the fleet was hand-rolling on its own
package/dist/index.d.mts CHANGED
@@ -72,8 +72,9 @@ interface ProfilesManagerProps {
72
72
  cardBg?: string;
73
73
  titleVariant?: MD3TextVariant$1;
74
74
  colorPreview?: (hex: string) => string;
75
+ useColorPickerAlign?: (open: boolean, contentWidth: number, contentHeight: number) => ProfilePickerAlignResult;
75
76
  }
76
- declare function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, headerLeft, ref, fg: fgOverride, fgMuted: fgMutedOverride, cardBg: cardBgOverride, titleVariant, colorPreview }: ProfilesManagerProps): react.JSX.Element;
77
+ declare function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, headerLeft, ref, fg: fgOverride, fgMuted: fgMutedOverride, cardBg: cardBgOverride, titleVariant, colorPreview, useColorPickerAlign }: ProfilesManagerProps): react.JSX.Element;
77
78
 
78
79
  type MD3TextVariant = 'displayLarge' | 'displayMedium' | 'displaySmall' | 'headlineLarge' | 'headlineMedium' | 'headlineSmall' | 'titleLarge' | 'titleMedium' | 'titleSmall' | 'labelLarge' | 'labelMedium' | 'labelSmall' | 'bodyLarge' | 'bodyMedium' | 'bodySmall';
79
80
  interface ProfilesScreenProps {
@@ -89,8 +90,9 @@ interface ProfilesScreenProps {
89
90
  cardBg?: string;
90
91
  titleVariant?: MD3TextVariant;
91
92
  colorPreview?: (hex: string) => string;
93
+ useColorPickerAlign?: ProfilesManagerProps['useColorPickerAlign'];
92
94
  }
93
- declare function ProfilesScreen({ profiles, defaultColor, onCreate, onSave, onDelete, onBack, fg: fgOverride, fgMuted, bg: bgOverride, cardBg, titleVariant, colorPreview }: ProfilesScreenProps): react.JSX.Element;
95
+ declare function ProfilesScreen({ profiles, defaultColor, onCreate, onSave, onDelete, onBack, fg: fgOverride, fgMuted, bg: bgOverride, cardBg, titleVariant, colorPreview, useColorPickerAlign }: ProfilesScreenProps): react.JSX.Element;
94
96
 
95
97
  declare const MAX_PROFILE_NAME_LENGTH = 20;
96
98
  declare const MAX_TAG_LENGTH = 3;
package/dist/index.d.ts CHANGED
@@ -72,8 +72,9 @@ interface ProfilesManagerProps {
72
72
  cardBg?: string;
73
73
  titleVariant?: MD3TextVariant$1;
74
74
  colorPreview?: (hex: string) => string;
75
+ useColorPickerAlign?: (open: boolean, contentWidth: number, contentHeight: number) => ProfilePickerAlignResult;
75
76
  }
76
- declare function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, headerLeft, ref, fg: fgOverride, fgMuted: fgMutedOverride, cardBg: cardBgOverride, titleVariant, colorPreview }: ProfilesManagerProps): react.JSX.Element;
77
+ declare function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, headerLeft, ref, fg: fgOverride, fgMuted: fgMutedOverride, cardBg: cardBgOverride, titleVariant, colorPreview, useColorPickerAlign }: ProfilesManagerProps): react.JSX.Element;
77
78
 
78
79
  type MD3TextVariant = 'displayLarge' | 'displayMedium' | 'displaySmall' | 'headlineLarge' | 'headlineMedium' | 'headlineSmall' | 'titleLarge' | 'titleMedium' | 'titleSmall' | 'labelLarge' | 'labelMedium' | 'labelSmall' | 'bodyLarge' | 'bodyMedium' | 'bodySmall';
79
80
  interface ProfilesScreenProps {
@@ -89,8 +90,9 @@ interface ProfilesScreenProps {
89
90
  cardBg?: string;
90
91
  titleVariant?: MD3TextVariant;
91
92
  colorPreview?: (hex: string) => string;
93
+ useColorPickerAlign?: ProfilesManagerProps['useColorPickerAlign'];
92
94
  }
93
- declare function ProfilesScreen({ profiles, defaultColor, onCreate, onSave, onDelete, onBack, fg: fgOverride, fgMuted, bg: bgOverride, cardBg, titleVariant, colorPreview }: ProfilesScreenProps): react.JSX.Element;
95
+ declare function ProfilesScreen({ profiles, defaultColor, onCreate, onSave, onDelete, onBack, fg: fgOverride, fgMuted, bg: bgOverride, cardBg, titleVariant, colorPreview, useColorPickerAlign }: ProfilesScreenProps): react.JSX.Element;
94
96
 
95
97
  declare const MAX_PROFILE_NAME_LENGTH = 20;
96
98
  declare const MAX_TAG_LENGTH = 3;
package/dist/index.js CHANGED
@@ -302,7 +302,7 @@ function deriveTag(name) {
302
302
  const trimmed = name.trim();
303
303
  return trimmed ? trimmed[0].toUpperCase() : "";
304
304
  }
305
- function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, headerLeft, ref, fg: fgOverride, fgMuted: fgMutedOverride, cardBg: cardBgOverride, titleVariant = "headlineSmall", colorPreview }) {
305
+ function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, headerLeft, ref, fg: fgOverride, fgMuted: fgMutedOverride, cardBg: cardBgOverride, titleVariant = "headlineSmall", colorPreview, useColorPickerAlign }) {
306
306
  const { dark, colors } = (0, import_auto_paper3.useAutoPaperTheme)();
307
307
  const insets = (0, import_react_native_safe_area_context.useSafeAreaInsets)();
308
308
  const { error: showErrorToast } = (0, import_toaster.useToast)();
@@ -388,11 +388,11 @@ function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, h
388
388
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native_paper3.Text, { variant: "bodyLarge", style: { color: fg }, children: "New Profile" })
389
389
  ] }) }, "new");
390
390
  }
391
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(EditRow, { draftName, onNameChange: handleNameChange, draftColor, onColorChange: setDraftColor, draftTag, onTagChange: handleTagChange, onSubmit: commitEdit, autoFocus: true, fgMuted, host, dark, colorPreview }, "new");
391
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(EditRow, { draftName, onNameChange: handleNameChange, draftColor, onColorChange: setDraftColor, draftTag, onTagChange: handleTagChange, onSubmit: commitEdit, autoFocus: true, fgMuted, host, dark, colorPreview, useColorPickerAlign }, "new");
392
392
  }
393
393
  const { profile } = row;
394
394
  if (editingId === profile.id) {
395
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(EditRow, { draftName, onNameChange: handleNameChange, draftColor, onColorChange: setDraftColor, draftTag, onTagChange: handleTagChange, onSubmit: commitEdit, onDelete: () => setConfirmDeleteId(profile.id), fgMuted, host, dark, colorPreview }, profile.id);
395
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(EditRow, { draftName, onNameChange: handleNameChange, draftColor, onColorChange: setDraftColor, draftTag, onTagChange: handleTagChange, onSubmit: commitEdit, onDelete: () => setConfirmDeleteId(profile.id), fgMuted, host, dark, colorPreview, useColorPickerAlign }, profile.id);
396
396
  }
397
397
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_feedback_press2.TouchableRipple, { onPress: () => startEdit(profile), style: styles3.row, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_react_native3.View, { style: styles3.rowInner, children: [
398
398
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ProfileChip, { profile, size: CHIP_SIZE }),
@@ -412,7 +412,13 @@ function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, h
412
412
  ] }) }) })
413
413
  ] });
414
414
  }
415
- function EditRow({ draftName, onNameChange, draftColor, onColorChange, draftTag, onTagChange, onSubmit, onDelete, autoFocus, fgMuted, host, dark, colorPreview }) {
415
+ function AlignedColorPicker({ useAlign, ...props }) {
416
+ const { width: windowWidth } = (0, import_react_native3.useWindowDimensions)();
417
+ const { width, height } = (0, import_hud2.getInlineColorPickerContentSize)(import_auto_paper3.defaultColors.length, windowWidth);
418
+ const alignOverride = useAlign(props.host.openId === props.id, width, height);
419
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_hud2.InlineColorPicker, { ...props, alignOverride });
420
+ }
421
+ function EditRow({ draftName, onNameChange, draftColor, onColorChange, draftTag, onTagChange, onSubmit, onDelete, autoFocus, fgMuted, host, dark, colorPreview, useColorPickerAlign }) {
416
422
  const register = (0, import_focus_chain.useFocusChain)();
417
423
  const tag = register();
418
424
  const name = register();
@@ -424,7 +430,7 @@ function EditRow({ draftName, onNameChange, draftColor, onColorChange, draftTag,
424
430
  if (host.openId === "color") tagFocusRef.current();
425
431
  }, [host.openId]);
426
432
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_react_native3.View, { style: styles3.editRow, children: [
427
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_hud2.InlineColorPicker, { id: "color", host, value: draftColor, onChange: onColorChange, previewValue: colorPreview, tag: draftTag, dark }),
433
+ useColorPickerAlign ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(AlignedColorPicker, { useAlign: useColorPickerAlign, id: "color", host, value: draftColor, onChange: onColorChange, previewValue: colorPreview, tag: draftTag, dark }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_hud2.InlineColorPicker, { id: "color", host, value: draftColor, onChange: onColorChange, previewValue: colorPreview, tag: draftTag, dark }),
428
434
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native_paper3.TextInput, { ref: tag.ref, ...tag.props, value: draftTag, onChangeText: onTagChange, accessibilityLabel: "Tag", autoCapitalize: "characters", selectTextOnFocus: true, returnKeyType: "next", style: styles3.hiddenTagInput, pointerEvents: "none" }),
429
435
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native_paper3.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: styles3.nameInput }),
430
436
  onDelete && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_feedback_press2.TouchableRipple, { onPress: onDelete, style: styles3.iconButton, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native_paper3.Icon, { source: "trash-can-outline", size: 18, color: fgMuted }) })
@@ -529,7 +535,7 @@ var import_feedback_press3 = require("@rific/feedback-press");
529
535
  var import_react3 = require("react");
530
536
  var import_react_native4 = require("react-native");
531
537
  var import_jsx_runtime4 = require("react/jsx-runtime");
532
- function ProfilesScreen({ profiles, defaultColor, onCreate, onSave, onDelete, onBack, fg: fgOverride, fgMuted, bg: bgOverride, cardBg, titleVariant, colorPreview }) {
538
+ function ProfilesScreen({ profiles, defaultColor, onCreate, onSave, onDelete, onBack, fg: fgOverride, fgMuted, bg: bgOverride, cardBg, titleVariant, colorPreview, useColorPickerAlign }) {
533
539
  const { dark } = (0, import_auto_paper4.useAutoPaperTheme)();
534
540
  const managerRef = (0, import_react3.useRef)(null);
535
541
  const fg = fgOverride ?? (dark ? "#FFFFFF" : "#000000");
@@ -538,7 +544,7 @@ function ProfilesScreen({ profiles, defaultColor, onCreate, onSave, onDelete, on
538
544
  managerRef.current?.commitPendingEdit();
539
545
  onBack();
540
546
  };
541
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.View, { style: [styles4.container, { backgroundColor: bg }], children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ProfilesManager, { ref: managerRef, profiles, defaultColor, onCreate, onSave, onDelete, fg, fgMuted, cardBg, titleVariant, colorPreview, headerLeft: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_feedback_press3.IconButton, { icon: "arrow-left", iconColor: fg, size: 24, onPress: handleBack, accessibilityLabel: "Back" }) }) });
547
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native4.View, { style: [styles4.container, { backgroundColor: bg }], children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ProfilesManager, { ref: managerRef, profiles, defaultColor, onCreate, onSave, onDelete, fg, fgMuted, cardBg, titleVariant, colorPreview, useColorPickerAlign, headerLeft: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_feedback_press3.IconButton, { icon: "arrow-left", iconColor: fg, size: 24, onPress: handleBack, accessibilityLabel: "Back" }) }) });
542
548
  }
543
549
  var styles4 = import_react_native4.StyleSheet.create({
544
550
  container: {
package/dist/index.mjs CHANGED
@@ -215,13 +215,13 @@ var styles2 = StyleSheet2.create({
215
215
  });
216
216
 
217
217
  // src/ProfilesManager.tsx
218
- import { useAutoPaperTheme } from "@rific/auto-paper";
218
+ import { defaultColors, useAutoPaperTheme } from "@rific/auto-paper";
219
219
  import { TouchableRipple as TouchableRipple2 } from "@rific/feedback-press";
220
220
  import { useFocusChain } from "@rific/focus-chain";
221
221
  import { useToast } from "@rific/toaster";
222
- import { InlineColorPicker, usePopoverHost } from "@tastic/hud";
222
+ import { getInlineColorPickerContentSize, InlineColorPicker, usePopoverHost } from "@tastic/hud";
223
223
  import { useCallback, useEffect, useImperativeHandle, useMemo as useMemo2, useRef, useState } from "react";
224
- import { KeyboardAvoidingView, Platform, ScrollView as ScrollView2, StyleSheet as StyleSheet3, View as View3 } from "react-native";
224
+ import { KeyboardAvoidingView, Platform, ScrollView as ScrollView2, StyleSheet as StyleSheet3, useWindowDimensions, View as View3 } from "react-native";
225
225
  import { Button, Icon as Icon3, Portal, Text as Text3, TextInput } from "react-native-paper";
226
226
  import { useSafeAreaInsets } from "react-native-safe-area-context";
227
227
 
@@ -264,7 +264,7 @@ function deriveTag(name) {
264
264
  const trimmed = name.trim();
265
265
  return trimmed ? trimmed[0].toUpperCase() : "";
266
266
  }
267
- function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, headerLeft, ref, fg: fgOverride, fgMuted: fgMutedOverride, cardBg: cardBgOverride, titleVariant = "headlineSmall", colorPreview }) {
267
+ function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, headerLeft, ref, fg: fgOverride, fgMuted: fgMutedOverride, cardBg: cardBgOverride, titleVariant = "headlineSmall", colorPreview, useColorPickerAlign }) {
268
268
  const { dark, colors } = useAutoPaperTheme();
269
269
  const insets = useSafeAreaInsets();
270
270
  const { error: showErrorToast } = useToast();
@@ -350,11 +350,11 @@ function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, h
350
350
  /* @__PURE__ */ jsx3(Text3, { variant: "bodyLarge", style: { color: fg }, children: "New Profile" })
351
351
  ] }) }, "new");
352
352
  }
353
- return /* @__PURE__ */ jsx3(EditRow, { draftName, onNameChange: handleNameChange, draftColor, onColorChange: setDraftColor, draftTag, onTagChange: handleTagChange, onSubmit: commitEdit, autoFocus: true, fgMuted, host, dark, colorPreview }, "new");
353
+ return /* @__PURE__ */ jsx3(EditRow, { draftName, onNameChange: handleNameChange, draftColor, onColorChange: setDraftColor, draftTag, onTagChange: handleTagChange, onSubmit: commitEdit, autoFocus: true, fgMuted, host, dark, colorPreview, useColorPickerAlign }, "new");
354
354
  }
355
355
  const { profile } = row;
356
356
  if (editingId === profile.id) {
357
- return /* @__PURE__ */ jsx3(EditRow, { draftName, onNameChange: handleNameChange, draftColor, onColorChange: setDraftColor, draftTag, onTagChange: handleTagChange, onSubmit: commitEdit, onDelete: () => setConfirmDeleteId(profile.id), fgMuted, host, dark, colorPreview }, profile.id);
357
+ return /* @__PURE__ */ jsx3(EditRow, { draftName, onNameChange: handleNameChange, draftColor, onColorChange: setDraftColor, draftTag, onTagChange: handleTagChange, onSubmit: commitEdit, onDelete: () => setConfirmDeleteId(profile.id), fgMuted, host, dark, colorPreview, useColorPickerAlign }, profile.id);
358
358
  }
359
359
  return /* @__PURE__ */ jsx3(TouchableRipple2, { onPress: () => startEdit(profile), style: styles3.row, children: /* @__PURE__ */ jsxs2(View3, { style: styles3.rowInner, children: [
360
360
  /* @__PURE__ */ jsx3(ProfileChip, { profile, size: CHIP_SIZE }),
@@ -374,7 +374,13 @@ function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, h
374
374
  ] }) }) })
375
375
  ] });
376
376
  }
377
- function EditRow({ draftName, onNameChange, draftColor, onColorChange, draftTag, onTagChange, onSubmit, onDelete, autoFocus, fgMuted, host, dark, colorPreview }) {
377
+ function AlignedColorPicker({ useAlign, ...props }) {
378
+ const { width: windowWidth } = useWindowDimensions();
379
+ const { width, height } = getInlineColorPickerContentSize(defaultColors.length, windowWidth);
380
+ const alignOverride = useAlign(props.host.openId === props.id, width, height);
381
+ return /* @__PURE__ */ jsx3(InlineColorPicker, { ...props, alignOverride });
382
+ }
383
+ function EditRow({ draftName, onNameChange, draftColor, onColorChange, draftTag, onTagChange, onSubmit, onDelete, autoFocus, fgMuted, host, dark, colorPreview, useColorPickerAlign }) {
378
384
  const register = useFocusChain();
379
385
  const tag = register();
380
386
  const name = register();
@@ -386,7 +392,7 @@ function EditRow({ draftName, onNameChange, draftColor, onColorChange, draftTag,
386
392
  if (host.openId === "color") tagFocusRef.current();
387
393
  }, [host.openId]);
388
394
  return /* @__PURE__ */ jsxs2(View3, { style: styles3.editRow, children: [
389
- /* @__PURE__ */ jsx3(InlineColorPicker, { id: "color", host, value: draftColor, onChange: onColorChange, previewValue: colorPreview, tag: draftTag, dark }),
395
+ useColorPickerAlign ? /* @__PURE__ */ jsx3(AlignedColorPicker, { useAlign: useColorPickerAlign, id: "color", host, value: draftColor, onChange: onColorChange, previewValue: colorPreview, tag: draftTag, dark }) : /* @__PURE__ */ jsx3(InlineColorPicker, { id: "color", host, value: draftColor, onChange: onColorChange, previewValue: colorPreview, tag: draftTag, dark }),
390
396
  /* @__PURE__ */ jsx3(TextInput, { ref: tag.ref, ...tag.props, value: draftTag, onChangeText: onTagChange, accessibilityLabel: "Tag", autoCapitalize: "characters", selectTextOnFocus: true, returnKeyType: "next", style: styles3.hiddenTagInput, pointerEvents: "none" }),
391
397
  /* @__PURE__ */ jsx3(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: styles3.nameInput }),
392
398
  onDelete && /* @__PURE__ */ jsx3(TouchableRipple2, { onPress: onDelete, style: styles3.iconButton, children: /* @__PURE__ */ jsx3(Icon3, { source: "trash-can-outline", size: 18, color: fgMuted }) })
@@ -491,7 +497,7 @@ import { IconButton } from "@rific/feedback-press";
491
497
  import { useRef as useRef2 } from "react";
492
498
  import { StyleSheet as StyleSheet4, View as View4 } from "react-native";
493
499
  import { jsx as jsx4 } from "react/jsx-runtime";
494
- function ProfilesScreen({ profiles, defaultColor, onCreate, onSave, onDelete, onBack, fg: fgOverride, fgMuted, bg: bgOverride, cardBg, titleVariant, colorPreview }) {
500
+ function ProfilesScreen({ profiles, defaultColor, onCreate, onSave, onDelete, onBack, fg: fgOverride, fgMuted, bg: bgOverride, cardBg, titleVariant, colorPreview, useColorPickerAlign }) {
495
501
  const { dark } = useAutoPaperTheme2();
496
502
  const managerRef = useRef2(null);
497
503
  const fg = fgOverride ?? (dark ? "#FFFFFF" : "#000000");
@@ -500,7 +506,7 @@ function ProfilesScreen({ profiles, defaultColor, onCreate, onSave, onDelete, on
500
506
  managerRef.current?.commitPendingEdit();
501
507
  onBack();
502
508
  };
503
- return /* @__PURE__ */ jsx4(View4, { style: [styles4.container, { backgroundColor: bg }], children: /* @__PURE__ */ jsx4(ProfilesManager, { ref: managerRef, profiles, defaultColor, onCreate, onSave, onDelete, fg, fgMuted, cardBg, titleVariant, colorPreview, headerLeft: /* @__PURE__ */ jsx4(IconButton, { icon: "arrow-left", iconColor: fg, size: 24, onPress: handleBack, accessibilityLabel: "Back" }) }) });
509
+ return /* @__PURE__ */ jsx4(View4, { style: [styles4.container, { backgroundColor: bg }], children: /* @__PURE__ */ jsx4(ProfilesManager, { ref: managerRef, profiles, defaultColor, onCreate, onSave, onDelete, fg, fgMuted, cardBg, titleVariant, colorPreview, useColorPickerAlign, headerLeft: /* @__PURE__ */ jsx4(IconButton, { icon: "arrow-left", iconColor: fg, size: 24, onPress: handleBack, accessibilityLabel: "Back" }) }) });
504
510
  }
505
511
  var styles4 = StyleSheet4.create({
506
512
  container: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tastic/profile",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Player profile identity for local-multiplayer React Native games — a saved name/color/tag a player selects per seat, with a picker, an inline-editable roster screen, and a reusable identity badge, deliberately blind to any app-specific fields (control scheme, stats) an app layers on top",
5
5
  "keywords": [
6
6
  "react-native",
@@ -70,7 +70,7 @@
70
70
  "@rific/feedback-press": "^0.12.0",
71
71
  "@rific/focus-chain": "^0.4.7",
72
72
  "@rific/toaster": "^0.7.0",
73
- "@tastic/core": "^0.8.0",
73
+ "@tastic/core": "^0.9.0",
74
74
  "@tastic/hud": "^0.12.0",
75
75
  "@testing-library/dom": "^10.4.1",
76
76
  "@testing-library/react": "^16.3.2",
@@ -1,14 +1,15 @@
1
- import { useAutoPaperTheme } from '@rific/auto-paper'
1
+ import { defaultColors, useAutoPaperTheme } from '@rific/auto-paper'
2
2
  import { TouchableRipple } from '@rific/feedback-press'
3
3
  import { useFocusChain } from '@rific/focus-chain'
4
4
  import { useToast } from '@rific/toaster'
5
- import { InlineColorPicker, PopoverHost, usePopoverHost } from '@tastic/hud'
6
- import { ReactNode, Ref, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'
7
- import { KeyboardAvoidingView, Platform, ScrollView, StyleSheet, View } from 'react-native'
5
+ import { getInlineColorPickerContentSize, InlineColorPicker, PopoverHost, usePopoverHost } from '@tastic/hud'
6
+ import { ComponentProps, ReactNode, Ref, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'
7
+ import { KeyboardAvoidingView, Platform, ScrollView, StyleSheet, useWindowDimensions, View } from 'react-native'
8
8
  import { Button, Icon, Portal, Text, TextInput } from 'react-native-paper'
9
9
  import { useSafeAreaInsets } from 'react-native-safe-area-context'
10
10
 
11
11
  import { ProfileChip } from './ProfileChip'
12
+ import { type ProfilePickerAlignResult } from './ProfilePicker'
12
13
  import { clampTag, isValidTag, MAX_PROFILE_NAME_LENGTH } from './profilesValidation'
13
14
  import { Profile } from './types'
14
15
 
@@ -79,6 +80,18 @@ export interface ProfilesManagerProps {
79
80
  // InlineColorPicker's identical prop (see its own doc). Leaves draftColor/onCreate/onSave's actual
80
81
  // color value untouched; only what's rendered changes.
81
82
  colorPreview?: (hex: string) => string
83
+ // Substitutes the color swatch popover's own placement, for a host that renders this screen inside a
84
+ // FAKE-rotated frame (a View turned +-90deg while the OS stays portrait-locked). @tastic/hud's
85
+ // InlineColorPicker places its popover with useAutoAlign, which compares a screen-space
86
+ // measureInWindow rect against the raw portrait window and (through its ambient rotation) double-rotates
87
+ // an already-transformed rect - so under a fake rotation it lands off its trigger. A host that knows its
88
+ // frame passes its own placement HOOK here; it is called with the swatch popover's open state and content
89
+ // size, and its result is handed to InlineColorPicker as `alignOverride` (its `triggerRef` is what gets
90
+ // measured). It is a hook, not a plain function: give it a STABLE identity for the life of the mount (a
91
+ // module-level function) - switching it at runtime remounts the swatch. Omit for the ordinary case;
92
+ // behaviour is then identical to before. Needs @tastic/hud >= 0.12.0 to have any effect (older hud has no
93
+ // `alignOverride` and ignores it).
94
+ useColorPickerAlign?: (open: boolean, contentWidth: number, contentHeight: number) => ProfilePickerAlignResult
82
95
  }
83
96
 
84
97
  type Row = { kind: 'profile'; profile: Profile } | { kind: 'new' }
@@ -101,7 +114,7 @@ function deriveTag(name: string): string {
101
114
  // really unmounts it on a given host's router — see ProfilesManagerHandle's own doc for the
102
115
  // react-navigation-web case where it doesn't, and why a host there needs the `ref` escape hatch
103
116
  // instead of relying on the unmount fallback alone.
104
- export function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, headerLeft, ref, fg: fgOverride, fgMuted: fgMutedOverride, cardBg: cardBgOverride, titleVariant = 'headlineSmall', colorPreview }: ProfilesManagerProps) {
117
+ export function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, headerLeft, ref, fg: fgOverride, fgMuted: fgMutedOverride, cardBg: cardBgOverride, titleVariant = 'headlineSmall', colorPreview, useColorPickerAlign }: ProfilesManagerProps) {
105
118
  const { dark, colors } = useAutoPaperTheme()
106
119
  const insets = useSafeAreaInsets()
107
120
  const { error: showErrorToast } = useToast()
@@ -253,12 +266,12 @@ export function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDe
253
266
  </TouchableRipple>
254
267
  )
255
268
  }
256
- return <EditRow key='new' draftName={draftName} onNameChange={handleNameChange} draftColor={draftColor} onColorChange={setDraftColor} draftTag={draftTag} onTagChange={handleTagChange} onSubmit={commitEdit} autoFocus fgMuted={fgMuted} host={host} dark={dark} colorPreview={colorPreview} />
269
+ return <EditRow key='new' draftName={draftName} onNameChange={handleNameChange} draftColor={draftColor} onColorChange={setDraftColor} draftTag={draftTag} onTagChange={handleTagChange} onSubmit={commitEdit} autoFocus fgMuted={fgMuted} host={host} dark={dark} colorPreview={colorPreview} useColorPickerAlign={useColorPickerAlign} />
257
270
  }
258
271
 
259
272
  const { profile } = row
260
273
  if (editingId === profile.id) {
261
- return <EditRow key={profile.id} draftName={draftName} onNameChange={handleNameChange} draftColor={draftColor} onColorChange={setDraftColor} draftTag={draftTag} onTagChange={handleTagChange} onSubmit={commitEdit} onDelete={() => setConfirmDeleteId(profile.id)} fgMuted={fgMuted} host={host} dark={dark} colorPreview={colorPreview} />
274
+ return <EditRow key={profile.id} draftName={draftName} onNameChange={handleNameChange} draftColor={draftColor} onColorChange={setDraftColor} draftTag={draftTag} onTagChange={handleTagChange} onSubmit={commitEdit} onDelete={() => setConfirmDeleteId(profile.id)} fgMuted={fgMuted} host={host} dark={dark} colorPreview={colorPreview} useColorPickerAlign={useColorPickerAlign} />
262
275
  }
263
276
 
264
277
  // No delete affordance at rest — tap the row to start editing, which is the one place
@@ -323,6 +336,19 @@ interface EditRowProps {
323
336
  host: PopoverHost
324
337
  dark: boolean
325
338
  colorPreview?: (hex: string) => string
339
+ useColorPickerAlign?: ProfilesManagerProps['useColorPickerAlign']
340
+ }
341
+
342
+ // InlineColorPicker with its placement supplied by a host hook (see ProfilesManagerProps.useColorPickerAlign).
343
+ // A component of its own - rather than a conditional hook call inside EditRow - so the hook is always called
344
+ // unconditionally here, and the plain path below stays byte-identical to what it was. Sized exactly as
345
+ // InlineColorPicker sizes itself (the default swatch list at the raw window width), so the host's placement and
346
+ // the popover it positions agree on its footprint.
347
+ function AlignedColorPicker({ useAlign, ...props }: ComponentProps<typeof InlineColorPicker> & { useAlign: NonNullable<ProfilesManagerProps['useColorPickerAlign']> }) {
348
+ const { width: windowWidth } = useWindowDimensions()
349
+ const { width, height } = getInlineColorPickerContentSize(defaultColors.length, windowWidth)
350
+ const alignOverride = useAlign(props.host.openId === props.id, width, height)
351
+ return <InlineColorPicker {...props} alignOverride={alignOverride} />
326
352
  }
327
353
 
328
354
  // One row, expanded: the color trigger (@tastic/hud's InlineColorPicker) — doubling as the tag's
@@ -337,7 +363,7 @@ interface EditRowProps {
337
363
  // own — submitting the *name* field is what actually commits the row, so its registration's own
338
364
  // onSubmitEditing (a no-op — it's last in the chain) is overridden with onSubmit below, matching
339
365
  // how a normal multi-field form reads: fill fields in order, the last one finishes it.
340
- function EditRow({ draftName, onNameChange, draftColor, onColorChange, draftTag, onTagChange, onSubmit, onDelete, autoFocus, fgMuted, host, dark, colorPreview }: EditRowProps) {
366
+ function EditRow({ draftName, onNameChange, draftColor, onColorChange, draftTag, onTagChange, onSubmit, onDelete, autoFocus, fgMuted, host, dark, colorPreview, useColorPickerAlign }: EditRowProps) {
341
367
  const register = useFocusChain()
342
368
  const tag = register()
343
369
  const name = register()
@@ -364,7 +390,7 @@ function EditRow({ draftName, onNameChange, draftColor, onColorChange, draftTag,
364
390
 
365
391
  return (
366
392
  <View style={styles.editRow}>
367
- <InlineColorPicker id='color' host={host} value={draftColor} onChange={onColorChange} previewValue={colorPreview} tag={draftTag} dark={dark} />
393
+ {useColorPickerAlign ? <AlignedColorPicker useAlign={useColorPickerAlign} id='color' host={host} value={draftColor} onChange={onColorChange} previewValue={colorPreview} tag={draftTag} dark={dark} /> : <InlineColorPicker id='color' host={host} value={draftColor} onChange={onColorChange} previewValue={colorPreview} tag={draftTag} dark={dark} />}
368
394
  {/* Invisible on purpose (styles.hiddenTagInput: zero footprint, position: 'absolute' so it
369
395
  doesn't reserve space in the row's own flex layout) — the swatch above is the only place this
370
396
  value is ever meant to be seen. pointerEvents='none' is what actually keeps it out of the
@@ -3,7 +3,7 @@ import { IconButton } from '@rific/feedback-press'
3
3
  import { useRef } from 'react'
4
4
  import { StyleSheet, View } from 'react-native'
5
5
 
6
- import { ProfileEditPatch, ProfilesManager, ProfilesManagerHandle } from './ProfilesManager'
6
+ import { ProfileEditPatch, ProfilesManager, ProfilesManagerHandle, ProfilesManagerProps } from './ProfilesManager'
7
7
  import { Profile } from './types'
8
8
 
9
9
  // react-native-paper's own MD3 Text variant names — mirrored locally rather than importing its
@@ -40,6 +40,10 @@ export interface ProfilesScreenProps {
40
40
  cardBg?: string
41
41
  titleVariant?: MD3TextVariant
42
42
  colorPreview?: (hex: string) => string
43
+ // Forwarded straight through to the internal ProfilesManager - a placement hook for the color swatch popover, for
44
+ // a host inside a fake-rotated frame. See ProfilesManagerProps.useColorPickerAlign for what it is and its hud >= 0.12.0
45
+ // requirement.
46
+ useColorPickerAlign?: ProfilesManagerProps['useColorPickerAlign']
43
47
  }
44
48
 
45
49
  // The routed profiles-roster screen shell every app in this fleet hand-rolls around ProfilesManager
@@ -49,7 +53,7 @@ export interface ProfilesScreenProps {
49
53
  // still doesn't call useProfiles()/useGameStats() (or any host-specific hook) itself — those two
50
54
  // hooks' shapes are genuinely app-specific (different CreateProfileInput/GameStatsContextValue per
51
55
  // app), so this stays props-in/callbacks-out, exactly like ProfilesManager itself.
52
- export function ProfilesScreen({ profiles, defaultColor, onCreate, onSave, onDelete, onBack, fg: fgOverride, fgMuted, bg: bgOverride, cardBg, titleVariant, colorPreview }: ProfilesScreenProps) {
56
+ export function ProfilesScreen({ profiles, defaultColor, onCreate, onSave, onDelete, onBack, fg: fgOverride, fgMuted, bg: bgOverride, cardBg, titleVariant, colorPreview, useColorPickerAlign }: ProfilesScreenProps) {
53
57
  const { dark } = useAutoPaperTheme()
54
58
  // Owns the ref itself — nothing outside this component ever needs it, since this is now the one
55
59
  // thing that both renders ProfilesManager AND owns the only navigation trigger next to it.
@@ -71,7 +75,7 @@ export function ProfilesScreen({ profiles, defaultColor, onCreate, onSave, onDel
71
75
 
72
76
  return (
73
77
  <View style={[styles.container, { backgroundColor: bg }]}>
74
- <ProfilesManager ref={managerRef} profiles={profiles} defaultColor={defaultColor} onCreate={onCreate} onSave={onSave} onDelete={onDelete} fg={fg} fgMuted={fgMuted} cardBg={cardBg} titleVariant={titleVariant} colorPreview={colorPreview} headerLeft={<IconButton icon='arrow-left' iconColor={fg} size={24} onPress={handleBack} accessibilityLabel='Back' />} />
78
+ <ProfilesManager ref={managerRef} profiles={profiles} defaultColor={defaultColor} onCreate={onCreate} onSave={onSave} onDelete={onDelete} fg={fg} fgMuted={fgMuted} cardBg={cardBg} titleVariant={titleVariant} colorPreview={colorPreview} useColorPickerAlign={useColorPickerAlign} headerLeft={<IconButton icon='arrow-left' iconColor={fg} size={24} onPress={handleBack} accessibilityLabel='Back' />} />
75
79
  </View>
76
80
  )
77
81
  }