@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 +22 -0
- package/dist/index.d.mts +4 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +13 -7
- package/dist/index.mjs +16 -10
- package/package.json +2 -2
- package/src/ProfilesManager.tsx +35 -9
- package/src/ProfilesScreen.tsx +7 -3
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
|
|
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
|
|
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.
|
|
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.
|
|
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",
|
package/src/ProfilesManager.tsx
CHANGED
|
@@ -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
|
package/src/ProfilesScreen.tsx
CHANGED
|
@@ -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
|
}
|