@tastic/profile 0.3.6 → 0.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +13 -3
- package/dist/index.d.ts +13 -3
- package/dist/index.js +19 -18
- package/dist/index.mjs +20 -19
- package/package.json +6 -6
- package/src/ProfilesManager.tsx +68 -19
- package/src/index.ts +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
|
-
import { ReactNode } from 'react';
|
|
2
|
+
import { ReactNode, Ref } from 'react';
|
|
3
3
|
import { PopoverHost } from '@tastic/hud';
|
|
4
4
|
|
|
5
5
|
interface Profile {
|
|
@@ -35,11 +35,15 @@ interface ProfilePickerProps<P extends Profile> {
|
|
|
35
35
|
}
|
|
36
36
|
declare function ProfilePicker<P extends Profile>({ idPrefix, host, profiles, selectedId, takenId, color, dark, align: alignOverride, guestLabel, nullLabel, nullIcon, onSelect, onManage }: ProfilePickerProps<P>): react.JSX.Element;
|
|
37
37
|
|
|
38
|
+
type MD3TextVariant = 'displayLarge' | 'displayMedium' | 'displaySmall' | 'headlineLarge' | 'headlineMedium' | 'headlineSmall' | 'titleLarge' | 'titleMedium' | 'titleSmall' | 'labelLarge' | 'labelMedium' | 'labelSmall' | 'bodyLarge' | 'bodyMedium' | 'bodySmall';
|
|
38
39
|
interface ProfileEditPatch {
|
|
39
40
|
name: string;
|
|
40
41
|
color: string;
|
|
41
42
|
tag: string;
|
|
42
43
|
}
|
|
44
|
+
interface ProfilesManagerHandle {
|
|
45
|
+
commitPendingEdit: () => void;
|
|
46
|
+
}
|
|
43
47
|
interface ProfilesManagerProps {
|
|
44
48
|
profiles: Profile[];
|
|
45
49
|
defaultColor: string;
|
|
@@ -47,8 +51,14 @@ interface ProfilesManagerProps {
|
|
|
47
51
|
onSave: (id: string, patch: ProfileEditPatch) => void;
|
|
48
52
|
onDelete: (id: string) => void;
|
|
49
53
|
headerLeft?: ReactNode;
|
|
54
|
+
ref?: Ref<ProfilesManagerHandle>;
|
|
55
|
+
fg?: string;
|
|
56
|
+
fgMuted?: string;
|
|
57
|
+
cardBg?: string;
|
|
58
|
+
titleVariant?: MD3TextVariant;
|
|
59
|
+
colorPreview?: (hex: string) => string;
|
|
50
60
|
}
|
|
51
|
-
declare function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, headerLeft }: ProfilesManagerProps): react.JSX.Element;
|
|
61
|
+
declare function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, headerLeft, ref, fg: fgOverride, fgMuted: fgMutedOverride, cardBg: cardBgOverride, titleVariant, colorPreview }: ProfilesManagerProps): react.JSX.Element;
|
|
52
62
|
|
|
53
63
|
declare const MAX_PROFILE_NAME_LENGTH = 20;
|
|
54
64
|
declare const MAX_TAG_LENGTH = 3;
|
|
@@ -59,4 +69,4 @@ declare const isSharedProfileStoreAvailable: boolean;
|
|
|
59
69
|
declare function loadSharedProfiles(groupId: string): Promise<Profile[]>;
|
|
60
70
|
declare function saveSharedProfiles(groupId: string, profiles: Profile[]): Promise<void>;
|
|
61
71
|
|
|
62
|
-
export { MAX_PROFILE_NAME_LENGTH, MAX_TAG_LENGTH, type Profile, ProfileChip, ProfilePicker, ProfilesManager, type ProfilesManagerProps, isSharedProfileStoreAvailable, isValidProfile, isValidTag, loadSharedProfiles, saveSharedProfiles };
|
|
72
|
+
export { MAX_PROFILE_NAME_LENGTH, MAX_TAG_LENGTH, type Profile, ProfileChip, ProfilePicker, ProfilesManager, type ProfilesManagerHandle, type ProfilesManagerProps, isSharedProfileStoreAvailable, isValidProfile, isValidTag, loadSharedProfiles, saveSharedProfiles };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
|
-
import { ReactNode } from 'react';
|
|
2
|
+
import { ReactNode, Ref } from 'react';
|
|
3
3
|
import { PopoverHost } from '@tastic/hud';
|
|
4
4
|
|
|
5
5
|
interface Profile {
|
|
@@ -35,11 +35,15 @@ interface ProfilePickerProps<P extends Profile> {
|
|
|
35
35
|
}
|
|
36
36
|
declare function ProfilePicker<P extends Profile>({ idPrefix, host, profiles, selectedId, takenId, color, dark, align: alignOverride, guestLabel, nullLabel, nullIcon, onSelect, onManage }: ProfilePickerProps<P>): react.JSX.Element;
|
|
37
37
|
|
|
38
|
+
type MD3TextVariant = 'displayLarge' | 'displayMedium' | 'displaySmall' | 'headlineLarge' | 'headlineMedium' | 'headlineSmall' | 'titleLarge' | 'titleMedium' | 'titleSmall' | 'labelLarge' | 'labelMedium' | 'labelSmall' | 'bodyLarge' | 'bodyMedium' | 'bodySmall';
|
|
38
39
|
interface ProfileEditPatch {
|
|
39
40
|
name: string;
|
|
40
41
|
color: string;
|
|
41
42
|
tag: string;
|
|
42
43
|
}
|
|
44
|
+
interface ProfilesManagerHandle {
|
|
45
|
+
commitPendingEdit: () => void;
|
|
46
|
+
}
|
|
43
47
|
interface ProfilesManagerProps {
|
|
44
48
|
profiles: Profile[];
|
|
45
49
|
defaultColor: string;
|
|
@@ -47,8 +51,14 @@ interface ProfilesManagerProps {
|
|
|
47
51
|
onSave: (id: string, patch: ProfileEditPatch) => void;
|
|
48
52
|
onDelete: (id: string) => void;
|
|
49
53
|
headerLeft?: ReactNode;
|
|
54
|
+
ref?: Ref<ProfilesManagerHandle>;
|
|
55
|
+
fg?: string;
|
|
56
|
+
fgMuted?: string;
|
|
57
|
+
cardBg?: string;
|
|
58
|
+
titleVariant?: MD3TextVariant;
|
|
59
|
+
colorPreview?: (hex: string) => string;
|
|
50
60
|
}
|
|
51
|
-
declare function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, headerLeft }: ProfilesManagerProps): react.JSX.Element;
|
|
61
|
+
declare function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, headerLeft, ref, fg: fgOverride, fgMuted: fgMutedOverride, cardBg: cardBgOverride, titleVariant, colorPreview }: ProfilesManagerProps): react.JSX.Element;
|
|
52
62
|
|
|
53
63
|
declare const MAX_PROFILE_NAME_LENGTH = 20;
|
|
54
64
|
declare const MAX_TAG_LENGTH = 3;
|
|
@@ -59,4 +69,4 @@ declare const isSharedProfileStoreAvailable: boolean;
|
|
|
59
69
|
declare function loadSharedProfiles(groupId: string): Promise<Profile[]>;
|
|
60
70
|
declare function saveSharedProfiles(groupId: string, profiles: Profile[]): Promise<void>;
|
|
61
71
|
|
|
62
|
-
export { MAX_PROFILE_NAME_LENGTH, MAX_TAG_LENGTH, type Profile, ProfileChip, ProfilePicker, ProfilesManager, type ProfilesManagerProps, isSharedProfileStoreAvailable, isValidProfile, isValidTag, loadSharedProfiles, saveSharedProfiles };
|
|
72
|
+
export { MAX_PROFILE_NAME_LENGTH, MAX_TAG_LENGTH, type Profile, ProfileChip, ProfilePicker, ProfilesManager, type ProfilesManagerHandle, type ProfilesManagerProps, isSharedProfileStoreAvailable, isValidProfile, isValidTag, loadSharedProfiles, saveSharedProfiles };
|
package/dist/index.js
CHANGED
|
@@ -287,14 +287,14 @@ function deriveTag(name) {
|
|
|
287
287
|
const trimmed = name.trim();
|
|
288
288
|
return trimmed ? trimmed[0].toUpperCase() : "";
|
|
289
289
|
}
|
|
290
|
-
function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, headerLeft }) {
|
|
290
|
+
function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, headerLeft, ref, fg: fgOverride, fgMuted: fgMutedOverride, cardBg: cardBgOverride, titleVariant = "headlineSmall", colorPreview }) {
|
|
291
291
|
const { dark, colors } = (0, import_auto_paper3.useAutoPaperTheme)();
|
|
292
292
|
const insets = (0, import_react_native_safe_area_context.useSafeAreaInsets)();
|
|
293
293
|
const { error: showErrorToast } = (0, import_toaster.useToast)();
|
|
294
294
|
const host = (0, import_hud2.usePopoverHost)();
|
|
295
|
-
const fg = dark ? "#FFFFFF" : "#000000";
|
|
296
|
-
const fgMuted = dark ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.5)";
|
|
297
|
-
const cardBg = dark ? "#111111" : "#F2F2F2";
|
|
295
|
+
const fg = fgOverride ?? (dark ? "#FFFFFF" : "#000000");
|
|
296
|
+
const fgMuted = fgMutedOverride ?? (dark ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.5)");
|
|
297
|
+
const cardBg = cardBgOverride ?? (dark ? "#111111" : "#F2F2F2");
|
|
298
298
|
const cardBorder = dark ? "rgba(255,255,255,0.2)" : "rgba(0,0,0,0.2)";
|
|
299
299
|
const cancelBg = dark ? "#2A2A2A" : "#E0E0E0";
|
|
300
300
|
const cancelFg = dark ? "#FFFFFF" : "#000000";
|
|
@@ -320,6 +320,7 @@ function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, h
|
|
|
320
320
|
commitEditRef.current = commitEdit;
|
|
321
321
|
});
|
|
322
322
|
(0, import_react2.useEffect)(() => () => commitEditRef.current(), []);
|
|
323
|
+
(0, import_react2.useImperativeHandle)(ref, () => ({ commitPendingEdit: () => commitEditRef.current() }), []);
|
|
323
324
|
const startEdit = (0, import_react2.useCallback)(
|
|
324
325
|
(target) => {
|
|
325
326
|
commitEdit();
|
|
@@ -362,7 +363,7 @@ function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, h
|
|
|
362
363
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
|
|
363
364
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_react_native3.View, { style: [styles3.header, { paddingTop: 8 + insets.top, paddingLeft: 8 + insets.left }], children: [
|
|
364
365
|
headerLeft,
|
|
365
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native_paper3.Text, { variant:
|
|
366
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native_paper3.Text, { variant: titleVariant, style: [styles3.title, { color: fg }], children: "Profiles" })
|
|
366
367
|
] }),
|
|
367
368
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native3.KeyboardAvoidingView, { behavior: import_react_native3.Platform.OS === "ios" ? "padding" : void 0, style: styles3.keyboardAvoiding, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native3.ScrollView, { style: styles3.scrollView, contentContainerStyle: [styles3.content, { paddingBottom: 32 + insets.bottom }], showsVerticalScrollIndicator: false, keyboardShouldPersistTaps: "handled", children: rows.map((row) => {
|
|
368
369
|
if (row.kind === "new") {
|
|
@@ -372,11 +373,11 @@ function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, h
|
|
|
372
373
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native_paper3.Text, { variant: "bodyLarge", style: { color: fg }, children: "New Profile" })
|
|
373
374
|
] }) }, "new");
|
|
374
375
|
}
|
|
375
|
-
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(EditRow, { draftName, onNameChange: handleNameChange, draftColor, onColorChange: setDraftColor, draftTag, onTagChange: handleTagChange, onSubmit: commitEdit, autoFocus: true, fgMuted, host, dark }, "new");
|
|
376
|
+
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");
|
|
376
377
|
}
|
|
377
378
|
const { profile } = row;
|
|
378
379
|
if (editingId === profile.id) {
|
|
379
|
-
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 }, profile.id);
|
|
380
|
+
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);
|
|
380
381
|
}
|
|
381
382
|
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: [
|
|
382
383
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ProfileChip, { profile, size: CHIP_SIZE }),
|
|
@@ -396,7 +397,7 @@ function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, h
|
|
|
396
397
|
] }) }) })
|
|
397
398
|
] });
|
|
398
399
|
}
|
|
399
|
-
function EditRow({ draftName, onNameChange, draftColor, onColorChange, draftTag, onTagChange, onSubmit, onDelete, autoFocus, fgMuted, host, dark }) {
|
|
400
|
+
function EditRow({ draftName, onNameChange, draftColor, onColorChange, draftTag, onTagChange, onSubmit, onDelete, autoFocus, fgMuted, host, dark, colorPreview }) {
|
|
400
401
|
const register = (0, import_focus_chain.useFocusChain)();
|
|
401
402
|
const tag = register();
|
|
402
403
|
const name = register();
|
|
@@ -408,7 +409,7 @@ function EditRow({ draftName, onNameChange, draftColor, onColorChange, draftTag,
|
|
|
408
409
|
if (host.openId === "color") tagFocusRef.current();
|
|
409
410
|
}, [host.openId]);
|
|
410
411
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_react_native3.View, { style: styles3.editRow, children: [
|
|
411
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_hud2.InlineColorPicker, { id: "color", host, value: draftColor, onChange: onColorChange, tag: draftTag, dark }),
|
|
412
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_hud2.InlineColorPicker, { id: "color", host, value: draftColor, onChange: onColorChange, previewValue: colorPreview, tag: draftTag, dark }),
|
|
412
413
|
/* @__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" }),
|
|
413
414
|
/* @__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 }),
|
|
414
415
|
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 }) })
|
|
@@ -431,6 +432,15 @@ var styles3 = import_react_native3.StyleSheet.create({
|
|
|
431
432
|
gap: 4,
|
|
432
433
|
paddingBottom: 8
|
|
433
434
|
},
|
|
435
|
+
// 1x1 and fully transparent — there's nothing to see here on purpose (see EditRow's own comment
|
|
436
|
+
// above the field this styles). position: 'absolute' pulls it out of editRow's flex flow
|
|
437
|
+
// entirely, so it doesn't reserve a gap in the row the way a normal flex sibling would.
|
|
438
|
+
hiddenTagInput: {
|
|
439
|
+
height: 1,
|
|
440
|
+
opacity: 0,
|
|
441
|
+
position: "absolute",
|
|
442
|
+
width: 1
|
|
443
|
+
},
|
|
434
444
|
iconButton: {
|
|
435
445
|
alignItems: "center",
|
|
436
446
|
borderRadius: 8,
|
|
@@ -492,15 +502,6 @@ var styles3 = import_react_native3.StyleSheet.create({
|
|
|
492
502
|
scrollView: {
|
|
493
503
|
flex: 1
|
|
494
504
|
},
|
|
495
|
-
// 1x1 and fully transparent — there's nothing to see here on purpose (see EditRow's own comment
|
|
496
|
-
// above the field this styles). position: 'absolute' pulls it out of editRow's flex flow
|
|
497
|
-
// entirely, so it doesn't reserve a gap in the row the way a normal flex sibling would.
|
|
498
|
-
hiddenTagInput: {
|
|
499
|
-
height: 1,
|
|
500
|
-
opacity: 0,
|
|
501
|
-
position: "absolute",
|
|
502
|
-
width: 1
|
|
503
|
-
},
|
|
504
505
|
title: {
|
|
505
506
|
flexShrink: 1,
|
|
506
507
|
fontWeight: "bold"
|
package/dist/index.mjs
CHANGED
|
@@ -215,7 +215,7 @@ import { TouchableRipple as TouchableRipple2 } from "@rific/feedback-press";
|
|
|
215
215
|
import { useFocusChain } from "@rific/focus-chain";
|
|
216
216
|
import { useToast } from "@rific/toaster";
|
|
217
217
|
import { InlineColorPicker, usePopoverHost } from "@tastic/hud";
|
|
218
|
-
import { useCallback, useEffect, useMemo as useMemo2, useRef, useState } from "react";
|
|
218
|
+
import { useCallback, useEffect, useImperativeHandle, useMemo as useMemo2, useRef, useState } from "react";
|
|
219
219
|
import { KeyboardAvoidingView, Platform, ScrollView as ScrollView2, StyleSheet as StyleSheet3, View as View3 } from "react-native";
|
|
220
220
|
import { Button, Icon as Icon3, Portal, Text as Text3, TextInput } from "react-native-paper";
|
|
221
221
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
@@ -259,14 +259,14 @@ function deriveTag(name) {
|
|
|
259
259
|
const trimmed = name.trim();
|
|
260
260
|
return trimmed ? trimmed[0].toUpperCase() : "";
|
|
261
261
|
}
|
|
262
|
-
function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, headerLeft }) {
|
|
262
|
+
function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, headerLeft, ref, fg: fgOverride, fgMuted: fgMutedOverride, cardBg: cardBgOverride, titleVariant = "headlineSmall", colorPreview }) {
|
|
263
263
|
const { dark, colors } = useAutoPaperTheme();
|
|
264
264
|
const insets = useSafeAreaInsets();
|
|
265
265
|
const { error: showErrorToast } = useToast();
|
|
266
266
|
const host = usePopoverHost();
|
|
267
|
-
const fg = dark ? "#FFFFFF" : "#000000";
|
|
268
|
-
const fgMuted = dark ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.5)";
|
|
269
|
-
const cardBg = dark ? "#111111" : "#F2F2F2";
|
|
267
|
+
const fg = fgOverride ?? (dark ? "#FFFFFF" : "#000000");
|
|
268
|
+
const fgMuted = fgMutedOverride ?? (dark ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.5)");
|
|
269
|
+
const cardBg = cardBgOverride ?? (dark ? "#111111" : "#F2F2F2");
|
|
270
270
|
const cardBorder = dark ? "rgba(255,255,255,0.2)" : "rgba(0,0,0,0.2)";
|
|
271
271
|
const cancelBg = dark ? "#2A2A2A" : "#E0E0E0";
|
|
272
272
|
const cancelFg = dark ? "#FFFFFF" : "#000000";
|
|
@@ -292,6 +292,7 @@ function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, h
|
|
|
292
292
|
commitEditRef.current = commitEdit;
|
|
293
293
|
});
|
|
294
294
|
useEffect(() => () => commitEditRef.current(), []);
|
|
295
|
+
useImperativeHandle(ref, () => ({ commitPendingEdit: () => commitEditRef.current() }), []);
|
|
295
296
|
const startEdit = useCallback(
|
|
296
297
|
(target) => {
|
|
297
298
|
commitEdit();
|
|
@@ -334,7 +335,7 @@ function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, h
|
|
|
334
335
|
return /* @__PURE__ */ jsxs2(Fragment2, { children: [
|
|
335
336
|
/* @__PURE__ */ jsxs2(View3, { style: [styles3.header, { paddingTop: 8 + insets.top, paddingLeft: 8 + insets.left }], children: [
|
|
336
337
|
headerLeft,
|
|
337
|
-
/* @__PURE__ */ jsx3(Text3, { variant:
|
|
338
|
+
/* @__PURE__ */ jsx3(Text3, { variant: titleVariant, style: [styles3.title, { color: fg }], children: "Profiles" })
|
|
338
339
|
] }),
|
|
339
340
|
/* @__PURE__ */ jsx3(KeyboardAvoidingView, { behavior: Platform.OS === "ios" ? "padding" : void 0, style: styles3.keyboardAvoiding, children: /* @__PURE__ */ jsx3(ScrollView2, { style: styles3.scrollView, contentContainerStyle: [styles3.content, { paddingBottom: 32 + insets.bottom }], showsVerticalScrollIndicator: false, keyboardShouldPersistTaps: "handled", children: rows.map((row) => {
|
|
340
341
|
if (row.kind === "new") {
|
|
@@ -344,11 +345,11 @@ function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, h
|
|
|
344
345
|
/* @__PURE__ */ jsx3(Text3, { variant: "bodyLarge", style: { color: fg }, children: "New Profile" })
|
|
345
346
|
] }) }, "new");
|
|
346
347
|
}
|
|
347
|
-
return /* @__PURE__ */ jsx3(EditRow, { draftName, onNameChange: handleNameChange, draftColor, onColorChange: setDraftColor, draftTag, onTagChange: handleTagChange, onSubmit: commitEdit, autoFocus: true, fgMuted, host, dark }, "new");
|
|
348
|
+
return /* @__PURE__ */ jsx3(EditRow, { draftName, onNameChange: handleNameChange, draftColor, onColorChange: setDraftColor, draftTag, onTagChange: handleTagChange, onSubmit: commitEdit, autoFocus: true, fgMuted, host, dark, colorPreview }, "new");
|
|
348
349
|
}
|
|
349
350
|
const { profile } = row;
|
|
350
351
|
if (editingId === profile.id) {
|
|
351
|
-
return /* @__PURE__ */ jsx3(EditRow, { draftName, onNameChange: handleNameChange, draftColor, onColorChange: setDraftColor, draftTag, onTagChange: handleTagChange, onSubmit: commitEdit, onDelete: () => setConfirmDeleteId(profile.id), fgMuted, host, dark }, profile.id);
|
|
352
|
+
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);
|
|
352
353
|
}
|
|
353
354
|
return /* @__PURE__ */ jsx3(TouchableRipple2, { onPress: () => startEdit(profile), style: styles3.row, children: /* @__PURE__ */ jsxs2(View3, { style: styles3.rowInner, children: [
|
|
354
355
|
/* @__PURE__ */ jsx3(ProfileChip, { profile, size: CHIP_SIZE }),
|
|
@@ -368,7 +369,7 @@ function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, h
|
|
|
368
369
|
] }) }) })
|
|
369
370
|
] });
|
|
370
371
|
}
|
|
371
|
-
function EditRow({ draftName, onNameChange, draftColor, onColorChange, draftTag, onTagChange, onSubmit, onDelete, autoFocus, fgMuted, host, dark }) {
|
|
372
|
+
function EditRow({ draftName, onNameChange, draftColor, onColorChange, draftTag, onTagChange, onSubmit, onDelete, autoFocus, fgMuted, host, dark, colorPreview }) {
|
|
372
373
|
const register = useFocusChain();
|
|
373
374
|
const tag = register();
|
|
374
375
|
const name = register();
|
|
@@ -380,7 +381,7 @@ function EditRow({ draftName, onNameChange, draftColor, onColorChange, draftTag,
|
|
|
380
381
|
if (host.openId === "color") tagFocusRef.current();
|
|
381
382
|
}, [host.openId]);
|
|
382
383
|
return /* @__PURE__ */ jsxs2(View3, { style: styles3.editRow, children: [
|
|
383
|
-
/* @__PURE__ */ jsx3(InlineColorPicker, { id: "color", host, value: draftColor, onChange: onColorChange, tag: draftTag, dark }),
|
|
384
|
+
/* @__PURE__ */ jsx3(InlineColorPicker, { id: "color", host, value: draftColor, onChange: onColorChange, previewValue: colorPreview, tag: draftTag, dark }),
|
|
384
385
|
/* @__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" }),
|
|
385
386
|
/* @__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 }),
|
|
386
387
|
onDelete && /* @__PURE__ */ jsx3(TouchableRipple2, { onPress: onDelete, style: styles3.iconButton, children: /* @__PURE__ */ jsx3(Icon3, { source: "trash-can-outline", size: 18, color: fgMuted }) })
|
|
@@ -403,6 +404,15 @@ var styles3 = StyleSheet3.create({
|
|
|
403
404
|
gap: 4,
|
|
404
405
|
paddingBottom: 8
|
|
405
406
|
},
|
|
407
|
+
// 1x1 and fully transparent — there's nothing to see here on purpose (see EditRow's own comment
|
|
408
|
+
// above the field this styles). position: 'absolute' pulls it out of editRow's flex flow
|
|
409
|
+
// entirely, so it doesn't reserve a gap in the row the way a normal flex sibling would.
|
|
410
|
+
hiddenTagInput: {
|
|
411
|
+
height: 1,
|
|
412
|
+
opacity: 0,
|
|
413
|
+
position: "absolute",
|
|
414
|
+
width: 1
|
|
415
|
+
},
|
|
406
416
|
iconButton: {
|
|
407
417
|
alignItems: "center",
|
|
408
418
|
borderRadius: 8,
|
|
@@ -464,15 +474,6 @@ var styles3 = StyleSheet3.create({
|
|
|
464
474
|
scrollView: {
|
|
465
475
|
flex: 1
|
|
466
476
|
},
|
|
467
|
-
// 1x1 and fully transparent — there's nothing to see here on purpose (see EditRow's own comment
|
|
468
|
-
// above the field this styles). position: 'absolute' pulls it out of editRow's flex flow
|
|
469
|
-
// entirely, so it doesn't reserve a gap in the row the way a normal flex sibling would.
|
|
470
|
-
hiddenTagInput: {
|
|
471
|
-
height: 1,
|
|
472
|
-
opacity: 0,
|
|
473
|
-
position: "absolute",
|
|
474
|
-
width: 1
|
|
475
|
-
},
|
|
476
477
|
title: {
|
|
477
478
|
flexShrink: 1,
|
|
478
479
|
fontWeight: "bold"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tastic/profile",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.8",
|
|
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",
|
|
@@ -62,15 +62,15 @@
|
|
|
62
62
|
},
|
|
63
63
|
"prettier": "@infinitetoken/eslint-config/prettier",
|
|
64
64
|
"devDependencies": {
|
|
65
|
-
"@infinitetoken/eslint-config": "
|
|
66
|
-
"@infinitetoken/jest-config": "^0.2.
|
|
67
|
-
"@infinitetoken/tsconfig": "^0.
|
|
65
|
+
"@infinitetoken/eslint-config": "0.2.0",
|
|
66
|
+
"@infinitetoken/jest-config": "^0.2.4",
|
|
67
|
+
"@infinitetoken/tsconfig": "^0.4.1",
|
|
68
68
|
"@rific/auto-paper": "^0.10.3",
|
|
69
69
|
"@rific/feedback-press": "^0.10.8",
|
|
70
70
|
"@rific/focus-chain": "^0.4.7",
|
|
71
71
|
"@rific/toaster": "^0.6.3",
|
|
72
|
-
"@tastic/core": "^0.
|
|
73
|
-
"@tastic/hud": "^0.
|
|
72
|
+
"@tastic/core": "^0.2.0",
|
|
73
|
+
"@tastic/hud": "^0.6.0",
|
|
74
74
|
"@testing-library/dom": "^10.4.1",
|
|
75
75
|
"@testing-library/react": "^16.3.2",
|
|
76
76
|
"@types/jest": "^30.0.0",
|
package/src/ProfilesManager.tsx
CHANGED
|
@@ -3,7 +3,7 @@ import { TouchableRipple } from '@rific/feedback-press'
|
|
|
3
3
|
import { useFocusChain } from '@rific/focus-chain'
|
|
4
4
|
import { useToast } from '@rific/toaster'
|
|
5
5
|
import { InlineColorPicker, PopoverHost, usePopoverHost } from '@tastic/hud'
|
|
6
|
-
import { ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
6
|
+
import { ReactNode, Ref, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'
|
|
7
7
|
import { KeyboardAvoidingView, Platform, ScrollView, StyleSheet, 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'
|
|
@@ -16,12 +16,29 @@ import { Profile } from './types'
|
|
|
16
16
|
// size for every circle on this screen, rather than each picking its own.
|
|
17
17
|
const CHIP_SIZE = 40
|
|
18
18
|
|
|
19
|
+
// react-native-paper's own MD3 Text variant names — mirrored locally rather than importing its
|
|
20
|
+
// internal VariantProp/MD3TypescaleKey (not exported from the package's public entry point), same
|
|
21
|
+
// as @tastic/hud's BaseStatsScreen does for its own identical titleVariant prop.
|
|
22
|
+
type MD3TextVariant = 'displayLarge' | 'displayMedium' | 'displaySmall' | 'headlineLarge' | 'headlineMedium' | 'headlineSmall' | 'titleLarge' | 'titleMedium' | 'titleSmall' | 'labelLarge' | 'labelMedium' | 'labelSmall' | 'bodyLarge' | 'bodyMedium' | 'bodySmall'
|
|
23
|
+
|
|
19
24
|
interface ProfileEditPatch {
|
|
20
25
|
name: string
|
|
21
26
|
color: string
|
|
22
27
|
tag: string
|
|
23
28
|
}
|
|
24
29
|
|
|
30
|
+
export interface ProfilesManagerHandle {
|
|
31
|
+
// Flushes whatever row is currently being edited — the same thing this component's own unmount
|
|
32
|
+
// effect does, but callable explicitly. Exists because "unmount" isn't a reliable "the user is
|
|
33
|
+
// navigating away" signal on every host: React Navigation's web renderer keeps a popped screen
|
|
34
|
+
// mounted-but-hidden instead of unmounting it, so a host relying only on the unmount fallback
|
|
35
|
+
// silently loses an in-progress edit on `router.back()` there. A host whose router doesn't
|
|
36
|
+
// genuinely unmount this component on navigation should hold a ref and call this right before
|
|
37
|
+
// navigating away (e.g. in its own back-button handler). Safe to call any time, including when
|
|
38
|
+
// nothing is being edited — a no-op then, same as the unmount effect already is.
|
|
39
|
+
commitPendingEdit: () => void
|
|
40
|
+
}
|
|
41
|
+
|
|
25
42
|
export interface ProfilesManagerProps {
|
|
26
43
|
profiles: Profile[]
|
|
27
44
|
// The color a brand-new profile's draft starts on — this component doesn't have (and shouldn't
|
|
@@ -37,6 +54,28 @@ export interface ProfilesManagerProps {
|
|
|
37
54
|
// rendering slot, not an onBack-style callback: this component doesn't know or care that it's a
|
|
38
55
|
// back button, only that the host wants something shown there. Omit to hide the slot.
|
|
39
56
|
headerLeft?: ReactNode
|
|
57
|
+
// React 19 accepts `ref` as a plain prop on a function component (no forwardRef needed) — see
|
|
58
|
+
// ProfilesManagerHandle's own doc for why a host may need this.
|
|
59
|
+
ref?: Ref<ProfilesManagerHandle>
|
|
60
|
+
// Independently optional overrides for this screen's text/card colors — each defaults to the same
|
|
61
|
+
// literal-black/white-by-appearance formula as before when omitted, so every existing caller
|
|
62
|
+
// (none of which pass these) renders identically to before. Same convention as @tastic/hud's
|
|
63
|
+
// BaseStatsScreen fg/cardBg overrides, for a caller with its own app-wide chrome palette instead
|
|
64
|
+
// of that literal black/white convention. cardBorder/cancelBg/cancelFg have no override, matching
|
|
65
|
+
// BaseStatsScreen's identical reasoning for cardBorder: their low-alpha/neutral values already
|
|
66
|
+
// read fine against any cardBg, and (for Cancel specifically) are deliberately neutral rather than
|
|
67
|
+
// theme-derived so a host's own live gameplay/seat color can't land there by coincidence.
|
|
68
|
+
fg?: string
|
|
69
|
+
fgMuted?: string
|
|
70
|
+
cardBg?: string
|
|
71
|
+
// 'headlineSmall' (this screen's own default, below), matching BaseStatsScreen's own settled
|
|
72
|
+
// default and the reasoning behind it — this title sits next to a small headerLeft back-button
|
|
73
|
+
// glyph the same way, and the previous hardcoded 'displaySmall' read as oversized there too.
|
|
74
|
+
titleVariant?: MD3TextVariant
|
|
75
|
+
// Transforms the color swatch grid's own preview per-swatch — forwarded straight to the internal
|
|
76
|
+
// InlineColorPicker's identical prop (see its own doc). Leaves draftColor/onCreate/onSave's actual
|
|
77
|
+
// color value untouched; only what's rendered changes.
|
|
78
|
+
colorPreview?: (hex: string) => string
|
|
40
79
|
}
|
|
41
80
|
|
|
42
81
|
type Row = { kind: 'profile'; profile: Profile } | { kind: 'new' }
|
|
@@ -55,16 +94,20 @@ function deriveTag(name: string): string {
|
|
|
55
94
|
// would still assume *some* host owns navigation; this component doesn't even go that far), so any
|
|
56
95
|
// consuming screen just renders it as its body inside its own routed wrapper (back button, native
|
|
57
96
|
// push/pop transition, whatever persistence hook backs `profiles`/`onCreate`/`onSave`/`onDelete`).
|
|
58
|
-
|
|
97
|
+
// Staying router-agnostic means this component can't know for itself whether "navigating away"
|
|
98
|
+
// really unmounts it on a given host's router — see ProfilesManagerHandle's own doc for the
|
|
99
|
+
// react-navigation-web case where it doesn't, and why a host there needs the `ref` escape hatch
|
|
100
|
+
// instead of relying on the unmount fallback alone.
|
|
101
|
+
export function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, headerLeft, ref, fg: fgOverride, fgMuted: fgMutedOverride, cardBg: cardBgOverride, titleVariant = 'headlineSmall', colorPreview }: ProfilesManagerProps) {
|
|
59
102
|
const { dark, colors } = useAutoPaperTheme()
|
|
60
103
|
const insets = useSafeAreaInsets()
|
|
61
104
|
const { error: showErrorToast } = useToast()
|
|
62
105
|
// Only one row is ever in edit mode at a time (see commitEdit, called before any new row opens),
|
|
63
106
|
// so every EditRow instance can safely share this one host instead of each needing its own.
|
|
64
107
|
const host = usePopoverHost()
|
|
65
|
-
const fg = dark ? '#FFFFFF' : '#000000'
|
|
66
|
-
const fgMuted = dark ? 'rgba(255,255,255,0.5)' : 'rgba(0,0,0,0.5)'
|
|
67
|
-
const cardBg = dark ? '#111111' : '#F2F2F2'
|
|
108
|
+
const fg = fgOverride ?? (dark ? '#FFFFFF' : '#000000')
|
|
109
|
+
const fgMuted = fgMutedOverride ?? (dark ? 'rgba(255,255,255,0.5)' : 'rgba(0,0,0,0.5)')
|
|
110
|
+
const cardBg = cardBgOverride ?? (dark ? '#111111' : '#F2F2F2')
|
|
68
111
|
const cardBorder = dark ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.2)'
|
|
69
112
|
// A contained neutral, not an outlined one — reads as "the other button" next to Delete's solid
|
|
70
113
|
// danger fill, rather than looking unfinished/secondary beside it.
|
|
@@ -116,6 +159,11 @@ export function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDe
|
|
|
116
159
|
commitEditRef.current = commitEdit
|
|
117
160
|
})
|
|
118
161
|
useEffect(() => () => commitEditRef.current(), [])
|
|
162
|
+
// The explicit escape hatch for hosts whose "navigate away" doesn't genuinely unmount this
|
|
163
|
+
// component — see ProfilesManagerHandle's own doc. `[]` deps: the handle object itself never
|
|
164
|
+
// needs to change identity, since it always calls through the ref to whatever commitEdit is
|
|
165
|
+
// current at call time, the same indirection the unmount effect above already relies on.
|
|
166
|
+
useImperativeHandle(ref, () => ({ commitPendingEdit: () => commitEditRef.current() }), [])
|
|
119
167
|
|
|
120
168
|
const startEdit = useCallback(
|
|
121
169
|
(target: 'new' | Profile) => {
|
|
@@ -174,7 +222,7 @@ export function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDe
|
|
|
174
222
|
<>
|
|
175
223
|
<View style={[styles.header, { paddingTop: 8 + insets.top, paddingLeft: 8 + insets.left }]}>
|
|
176
224
|
{headerLeft}
|
|
177
|
-
<Text variant=
|
|
225
|
+
<Text variant={titleVariant} style={[styles.title, { color: fg }]}>
|
|
178
226
|
Profiles
|
|
179
227
|
</Text>
|
|
180
228
|
</View>
|
|
@@ -202,12 +250,12 @@ export function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDe
|
|
|
202
250
|
</TouchableRipple>
|
|
203
251
|
)
|
|
204
252
|
}
|
|
205
|
-
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} />
|
|
253
|
+
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} />
|
|
206
254
|
}
|
|
207
255
|
|
|
208
256
|
const { profile } = row
|
|
209
257
|
if (editingId === profile.id) {
|
|
210
|
-
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} />
|
|
258
|
+
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} />
|
|
211
259
|
}
|
|
212
260
|
|
|
213
261
|
// No delete affordance at rest — tap the row to start editing, which is the one place
|
|
@@ -271,6 +319,7 @@ interface EditRowProps {
|
|
|
271
319
|
fgMuted: string
|
|
272
320
|
host: PopoverHost
|
|
273
321
|
dark: boolean
|
|
322
|
+
colorPreview?: (hex: string) => string
|
|
274
323
|
}
|
|
275
324
|
|
|
276
325
|
// One row, expanded: the color trigger (@tastic/hud's InlineColorPicker) — doubling as the tag's
|
|
@@ -285,7 +334,7 @@ interface EditRowProps {
|
|
|
285
334
|
// own — submitting the *name* field is what actually commits the row, so its registration's own
|
|
286
335
|
// onSubmitEditing (a no-op — it's last in the chain) is overridden with onSubmit below, matching
|
|
287
336
|
// how a normal multi-field form reads: fill fields in order, the last one finishes it.
|
|
288
|
-
function EditRow({ draftName, onNameChange, draftColor, onColorChange, draftTag, onTagChange, onSubmit, onDelete, autoFocus, fgMuted, host, dark }: EditRowProps) {
|
|
337
|
+
function EditRow({ draftName, onNameChange, draftColor, onColorChange, draftTag, onTagChange, onSubmit, onDelete, autoFocus, fgMuted, host, dark, colorPreview }: EditRowProps) {
|
|
289
338
|
const register = useFocusChain()
|
|
290
339
|
const tag = register()
|
|
291
340
|
const name = register()
|
|
@@ -312,7 +361,7 @@ function EditRow({ draftName, onNameChange, draftColor, onColorChange, draftTag,
|
|
|
312
361
|
|
|
313
362
|
return (
|
|
314
363
|
<View style={styles.editRow}>
|
|
315
|
-
<InlineColorPicker id='color' host={host} value={draftColor} onChange={onColorChange} tag={draftTag} dark={dark} />
|
|
364
|
+
<InlineColorPicker id='color' host={host} value={draftColor} onChange={onColorChange} previewValue={colorPreview} tag={draftTag} dark={dark} />
|
|
316
365
|
{/* Invisible on purpose (styles.hiddenTagInput: zero footprint, position: 'absolute' so it
|
|
317
366
|
doesn't reserve space in the row's own flex layout) — the swatch above is the only place this
|
|
318
367
|
value is ever meant to be seen. pointerEvents='none' is what actually keeps it out of the
|
|
@@ -356,6 +405,15 @@ const styles = StyleSheet.create({
|
|
|
356
405
|
gap: 4,
|
|
357
406
|
paddingBottom: 8
|
|
358
407
|
},
|
|
408
|
+
// 1x1 and fully transparent — there's nothing to see here on purpose (see EditRow's own comment
|
|
409
|
+
// above the field this styles). position: 'absolute' pulls it out of editRow's flex flow
|
|
410
|
+
// entirely, so it doesn't reserve a gap in the row the way a normal flex sibling would.
|
|
411
|
+
hiddenTagInput: {
|
|
412
|
+
height: 1,
|
|
413
|
+
opacity: 0,
|
|
414
|
+
position: 'absolute',
|
|
415
|
+
width: 1
|
|
416
|
+
},
|
|
359
417
|
iconButton: {
|
|
360
418
|
alignItems: 'center',
|
|
361
419
|
borderRadius: 8,
|
|
@@ -417,15 +475,6 @@ const styles = StyleSheet.create({
|
|
|
417
475
|
scrollView: {
|
|
418
476
|
flex: 1
|
|
419
477
|
},
|
|
420
|
-
// 1x1 and fully transparent — there's nothing to see here on purpose (see EditRow's own comment
|
|
421
|
-
// above the field this styles). position: 'absolute' pulls it out of editRow's flex flow
|
|
422
|
-
// entirely, so it doesn't reserve a gap in the row the way a normal flex sibling would.
|
|
423
|
-
hiddenTagInput: {
|
|
424
|
-
height: 1,
|
|
425
|
-
opacity: 0,
|
|
426
|
-
position: 'absolute',
|
|
427
|
-
width: 1
|
|
428
|
-
},
|
|
429
478
|
title: {
|
|
430
479
|
flexShrink: 1,
|
|
431
480
|
fontWeight: 'bold'
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { ProfileChip } from './ProfileChip'
|
|
2
2
|
export { ProfilePicker } from './ProfilePicker'
|
|
3
|
-
export { ProfilesManager, type ProfilesManagerProps } from './ProfilesManager'
|
|
3
|
+
export { ProfilesManager, type ProfilesManagerHandle, type ProfilesManagerProps } from './ProfilesManager'
|
|
4
4
|
export { isValidProfile, isValidTag, MAX_PROFILE_NAME_LENGTH, MAX_TAG_LENGTH } from './profilesValidation'
|
|
5
5
|
export { isSharedProfileStoreAvailable, loadSharedProfiles, saveSharedProfiles } from './sharedProfileStore'
|
|
6
6
|
export type { Profile } from './types'
|