@tastic/profile 0.3.7 → 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 CHANGED
@@ -35,6 +35,7 @@ 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;
@@ -51,8 +52,13 @@ interface ProfilesManagerProps {
51
52
  onDelete: (id: string) => void;
52
53
  headerLeft?: ReactNode;
53
54
  ref?: Ref<ProfilesManagerHandle>;
55
+ fg?: string;
56
+ fgMuted?: string;
57
+ cardBg?: string;
58
+ titleVariant?: MD3TextVariant;
59
+ colorPreview?: (hex: string) => string;
54
60
  }
55
- declare function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, headerLeft, ref }: 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;
56
62
 
57
63
  declare const MAX_PROFILE_NAME_LENGTH = 20;
58
64
  declare const MAX_TAG_LENGTH = 3;
package/dist/index.d.ts CHANGED
@@ -35,6 +35,7 @@ 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;
@@ -51,8 +52,13 @@ interface ProfilesManagerProps {
51
52
  onDelete: (id: string) => void;
52
53
  headerLeft?: ReactNode;
53
54
  ref?: Ref<ProfilesManagerHandle>;
55
+ fg?: string;
56
+ fgMuted?: string;
57
+ cardBg?: string;
58
+ titleVariant?: MD3TextVariant;
59
+ colorPreview?: (hex: string) => string;
54
60
  }
55
- declare function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, headerLeft, ref }: 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;
56
62
 
57
63
  declare const MAX_PROFILE_NAME_LENGTH = 20;
58
64
  declare const MAX_TAG_LENGTH = 3;
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, ref }) {
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";
@@ -363,7 +363,7 @@ function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, h
363
363
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
364
364
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_react_native3.View, { style: [styles3.header, { paddingTop: 8 + insets.top, paddingLeft: 8 + insets.left }], children: [
365
365
  headerLeft,
366
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native_paper3.Text, { variant: "displaySmall", style: [styles3.title, { color: fg }], children: "Profiles" })
366
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native_paper3.Text, { variant: titleVariant, style: [styles3.title, { color: fg }], children: "Profiles" })
367
367
  ] }),
368
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) => {
369
369
  if (row.kind === "new") {
@@ -373,11 +373,11 @@ function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, h
373
373
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native_paper3.Text, { variant: "bodyLarge", style: { color: fg }, children: "New Profile" })
374
374
  ] }) }, "new");
375
375
  }
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 }, "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");
377
377
  }
378
378
  const { profile } = row;
379
379
  if (editingId === 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 }, 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);
381
381
  }
382
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: [
383
383
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ProfileChip, { profile, size: CHIP_SIZE }),
@@ -397,7 +397,7 @@ function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, h
397
397
  ] }) }) })
398
398
  ] });
399
399
  }
400
- 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 }) {
401
401
  const register = (0, import_focus_chain.useFocusChain)();
402
402
  const tag = register();
403
403
  const name = register();
@@ -409,7 +409,7 @@ function EditRow({ draftName, onNameChange, draftColor, onColorChange, draftTag,
409
409
  if (host.openId === "color") tagFocusRef.current();
410
410
  }, [host.openId]);
411
411
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_react_native3.View, { style: styles3.editRow, children: [
412
- /* @__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 }),
413
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" }),
414
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 }),
415
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 }) })
@@ -432,6 +432,15 @@ var styles3 = import_react_native3.StyleSheet.create({
432
432
  gap: 4,
433
433
  paddingBottom: 8
434
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
+ },
435
444
  iconButton: {
436
445
  alignItems: "center",
437
446
  borderRadius: 8,
@@ -493,15 +502,6 @@ var styles3 = import_react_native3.StyleSheet.create({
493
502
  scrollView: {
494
503
  flex: 1
495
504
  },
496
- // 1x1 and fully transparent — there's nothing to see here on purpose (see EditRow's own comment
497
- // above the field this styles). position: 'absolute' pulls it out of editRow's flex flow
498
- // entirely, so it doesn't reserve a gap in the row the way a normal flex sibling would.
499
- hiddenTagInput: {
500
- height: 1,
501
- opacity: 0,
502
- position: "absolute",
503
- width: 1
504
- },
505
505
  title: {
506
506
  flexShrink: 1,
507
507
  fontWeight: "bold"
package/dist/index.mjs CHANGED
@@ -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, ref }) {
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";
@@ -335,7 +335,7 @@ function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, h
335
335
  return /* @__PURE__ */ jsxs2(Fragment2, { children: [
336
336
  /* @__PURE__ */ jsxs2(View3, { style: [styles3.header, { paddingTop: 8 + insets.top, paddingLeft: 8 + insets.left }], children: [
337
337
  headerLeft,
338
- /* @__PURE__ */ jsx3(Text3, { variant: "displaySmall", style: [styles3.title, { color: fg }], children: "Profiles" })
338
+ /* @__PURE__ */ jsx3(Text3, { variant: titleVariant, style: [styles3.title, { color: fg }], children: "Profiles" })
339
339
  ] }),
340
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) => {
341
341
  if (row.kind === "new") {
@@ -345,11 +345,11 @@ function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, h
345
345
  /* @__PURE__ */ jsx3(Text3, { variant: "bodyLarge", style: { color: fg }, children: "New Profile" })
346
346
  ] }) }, "new");
347
347
  }
348
- 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");
349
349
  }
350
350
  const { profile } = row;
351
351
  if (editingId === 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 }, 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);
353
353
  }
354
354
  return /* @__PURE__ */ jsx3(TouchableRipple2, { onPress: () => startEdit(profile), style: styles3.row, children: /* @__PURE__ */ jsxs2(View3, { style: styles3.rowInner, children: [
355
355
  /* @__PURE__ */ jsx3(ProfileChip, { profile, size: CHIP_SIZE }),
@@ -369,7 +369,7 @@ function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, h
369
369
  ] }) }) })
370
370
  ] });
371
371
  }
372
- 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 }) {
373
373
  const register = useFocusChain();
374
374
  const tag = register();
375
375
  const name = register();
@@ -381,7 +381,7 @@ function EditRow({ draftName, onNameChange, draftColor, onColorChange, draftTag,
381
381
  if (host.openId === "color") tagFocusRef.current();
382
382
  }, [host.openId]);
383
383
  return /* @__PURE__ */ jsxs2(View3, { style: styles3.editRow, children: [
384
- /* @__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 }),
385
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" }),
386
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 }),
387
387
  onDelete && /* @__PURE__ */ jsx3(TouchableRipple2, { onPress: onDelete, style: styles3.iconButton, children: /* @__PURE__ */ jsx3(Icon3, { source: "trash-can-outline", size: 18, color: fgMuted }) })
@@ -404,6 +404,15 @@ var styles3 = StyleSheet3.create({
404
404
  gap: 4,
405
405
  paddingBottom: 8
406
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
+ },
407
416
  iconButton: {
408
417
  alignItems: "center",
409
418
  borderRadius: 8,
@@ -465,15 +474,6 @@ var styles3 = StyleSheet3.create({
465
474
  scrollView: {
466
475
  flex: 1
467
476
  },
468
- // 1x1 and fully transparent — there's nothing to see here on purpose (see EditRow's own comment
469
- // above the field this styles). position: 'absolute' pulls it out of editRow's flex flow
470
- // entirely, so it doesn't reserve a gap in the row the way a normal flex sibling would.
471
- hiddenTagInput: {
472
- height: 1,
473
- opacity: 0,
474
- position: "absolute",
475
- width: 1
476
- },
477
477
  title: {
478
478
  flexShrink: 1,
479
479
  fontWeight: "bold"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tastic/profile",
3
- "version": "0.3.7",
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": "^0.1.8",
66
- "@infinitetoken/jest-config": "^0.2.1",
67
- "@infinitetoken/tsconfig": "^0.3.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.1.5",
73
- "@tastic/hud": "^0.1.6",
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",
@@ -16,6 +16,11 @@ 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
@@ -52,6 +57,25 @@ export interface ProfilesManagerProps {
52
57
  // React 19 accepts `ref` as a plain prop on a function component (no forwardRef needed) — see
53
58
  // ProfilesManagerHandle's own doc for why a host may need this.
54
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
55
79
  }
56
80
 
57
81
  type Row = { kind: 'profile'; profile: Profile } | { kind: 'new' }
@@ -74,16 +98,16 @@ function deriveTag(name: string): string {
74
98
  // really unmounts it on a given host's router — see ProfilesManagerHandle's own doc for the
75
99
  // react-navigation-web case where it doesn't, and why a host there needs the `ref` escape hatch
76
100
  // instead of relying on the unmount fallback alone.
77
- export function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, headerLeft, ref }: ProfilesManagerProps) {
101
+ export function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDelete, headerLeft, ref, fg: fgOverride, fgMuted: fgMutedOverride, cardBg: cardBgOverride, titleVariant = 'headlineSmall', colorPreview }: ProfilesManagerProps) {
78
102
  const { dark, colors } = useAutoPaperTheme()
79
103
  const insets = useSafeAreaInsets()
80
104
  const { error: showErrorToast } = useToast()
81
105
  // Only one row is ever in edit mode at a time (see commitEdit, called before any new row opens),
82
106
  // so every EditRow instance can safely share this one host instead of each needing its own.
83
107
  const host = usePopoverHost()
84
- const fg = dark ? '#FFFFFF' : '#000000'
85
- const fgMuted = dark ? 'rgba(255,255,255,0.5)' : 'rgba(0,0,0,0.5)'
86
- 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')
87
111
  const cardBorder = dark ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.2)'
88
112
  // A contained neutral, not an outlined one — reads as "the other button" next to Delete's solid
89
113
  // danger fill, rather than looking unfinished/secondary beside it.
@@ -198,7 +222,7 @@ export function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDe
198
222
  <>
199
223
  <View style={[styles.header, { paddingTop: 8 + insets.top, paddingLeft: 8 + insets.left }]}>
200
224
  {headerLeft}
201
- <Text variant='displaySmall' style={[styles.title, { color: fg }]}>
225
+ <Text variant={titleVariant} style={[styles.title, { color: fg }]}>
202
226
  Profiles
203
227
  </Text>
204
228
  </View>
@@ -226,12 +250,12 @@ export function ProfilesManager({ profiles, defaultColor, onCreate, onSave, onDe
226
250
  </TouchableRipple>
227
251
  )
228
252
  }
229
- 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} />
230
254
  }
231
255
 
232
256
  const { profile } = row
233
257
  if (editingId === profile.id) {
234
- 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} />
235
259
  }
236
260
 
237
261
  // No delete affordance at rest — tap the row to start editing, which is the one place
@@ -295,6 +319,7 @@ interface EditRowProps {
295
319
  fgMuted: string
296
320
  host: PopoverHost
297
321
  dark: boolean
322
+ colorPreview?: (hex: string) => string
298
323
  }
299
324
 
300
325
  // One row, expanded: the color trigger (@tastic/hud's InlineColorPicker) — doubling as the tag's
@@ -309,7 +334,7 @@ interface EditRowProps {
309
334
  // own — submitting the *name* field is what actually commits the row, so its registration's own
310
335
  // onSubmitEditing (a no-op — it's last in the chain) is overridden with onSubmit below, matching
311
336
  // how a normal multi-field form reads: fill fields in order, the last one finishes it.
312
- 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) {
313
338
  const register = useFocusChain()
314
339
  const tag = register()
315
340
  const name = register()
@@ -336,7 +361,7 @@ function EditRow({ draftName, onNameChange, draftColor, onColorChange, draftTag,
336
361
 
337
362
  return (
338
363
  <View style={styles.editRow}>
339
- <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} />
340
365
  {/* Invisible on purpose (styles.hiddenTagInput: zero footprint, position: 'absolute' so it
341
366
  doesn't reserve space in the row's own flex layout) — the swatch above is the only place this
342
367
  value is ever meant to be seen. pointerEvents='none' is what actually keeps it out of the
@@ -380,6 +405,15 @@ const styles = StyleSheet.create({
380
405
  gap: 4,
381
406
  paddingBottom: 8
382
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
+ },
383
417
  iconButton: {
384
418
  alignItems: 'center',
385
419
  borderRadius: 8,
@@ -441,15 +475,6 @@ const styles = StyleSheet.create({
441
475
  scrollView: {
442
476
  flex: 1
443
477
  },
444
- // 1x1 and fully transparent — there's nothing to see here on purpose (see EditRow's own comment
445
- // above the field this styles). position: 'absolute' pulls it out of editRow's flex flow
446
- // entirely, so it doesn't reserve a gap in the row the way a normal flex sibling would.
447
- hiddenTagInput: {
448
- height: 1,
449
- opacity: 0,
450
- position: 'absolute',
451
- width: 1
452
- },
453
478
  title: {
454
479
  flexShrink: 1,
455
480
  fontWeight: 'bold'