@tastic/profile 0.14.0 → 0.14.1

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.js CHANGED
@@ -167,13 +167,14 @@ function ProfilePicker({ idPrefix, host, profiles, selectedId, takenId, color, d
167
167
  host.toggle(id);
168
168
  };
169
169
  const triggerLabel = (selectedProfile?.name ?? guestLabel).toUpperCase();
170
+ const manageFooter = !!onManage && !(colorSection && colorPage);
170
171
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native2.View, { style: [styles2.anchor, open && styles2.anchorOpen], onLayout: handleAnchorLayout, children: [
171
172
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_feedback_press.TouchableRipple, { onPress: handleTriggerPress, borderless: chipTrigger, style: [styles2.trigger, chipTrigger && styles2.chipTrigger, triggerStyle], accessibilityRole: "button", accessibilityLabel: triggerLabel, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native2.View, { ref: triggerRef, collapsable: false, style: chipTrigger ? styles2.chipTriggerInner : styles2.triggerInner, children: [
172
173
  chipTrigger && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(IdentityChip, { color, tag: selectedProfile?.tag, icon: selectedProfile ? "account" : nullIcon, size: CHIP_SIZE }),
173
174
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native_paper2.Text, { style: [styles2.triggerLabel, { color: chipTrigger ? fg : color }], numberOfLines: 1, children: triggerLabel }),
174
175
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native_paper2.Icon, { source: "menu-down", size: 16, color: chipTrigger ? fg : color })
175
176
  ] }) }),
176
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_hud.PopoverBody, { visible: open && measured, align, verticalAlign, caretColor: menuBg, caretBorderColor: color, caretBorderWidth: MENU_BORDER_WIDTH, triggerSize: chipTrigger ? triggerWidth : TRIGGER_HEIGHT, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
177
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_hud.PopoverBody, { visible: open && measured, align, verticalAlign, caretColor: menuBg, caretBorderColor: color, caretBorderWidth: MENU_BORDER_WIDTH, triggerSize: chipTrigger ? triggerWidth : TRIGGER_HEIGHT, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
177
178
  import_react_native2.View,
178
179
  {
179
180
  style: [
@@ -185,41 +186,43 @@ function ProfilePicker({ idPrefix, host, profiles, selectedId, takenId, color, d
185
186
  maxHeight
186
187
  }
187
188
  ],
188
- children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_auto_paper2.FadingScrollView, { contentContainerStyle: styles2.menuContent, showsVerticalScrollIndicator: false, children: colorSection && colorPage ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ColorPage, { section: colorSection, profileColor: selectedProfile?.color, fg, onBack: () => setColorPage(false) }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
189
- colorSection && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ColorRow, { section: colorSection, fg, dividerColor: color, onPress: () => setColorPage(true) }),
190
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_feedback_press.TouchableRipple, { onPress: handleSelectGuest, style: [styles2.row, selectedId === null && { backgroundColor: color }], children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native2.View, { style: styles2.rowInner, children: [
191
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native2.View, { style: styles2.guestChip, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native_paper2.Icon, { source: nullIcon, size: 13, color: selectedId === null ? (0, import_auto_paper2.getContrastColor)(color) : fg }) }),
192
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native_paper2.Text, { style: [styles2.rowLabel, { color: selectedId === null ? (0, import_auto_paper2.getContrastColor)(color) : fg }], children: nullLabel })
193
- ] }) }),
194
- sortedProfiles.map((profile) => {
195
- const taken = profile.id === takenId;
196
- const selected = selectedId === profile.id;
197
- const rowColor = colorPreview ? colorPreview(profile.color) : profile.color;
198
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_feedback_press.TouchableRipple, { disabled: taken, onPress: () => handleSelectExisting(profile), style: [styles2.row, selected && { backgroundColor: rowColor }], children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native2.View, { style: [styles2.rowInner, taken && styles2.rowDisabled], children: [
199
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ProfileChip, { profile, filled: !selected }),
200
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
201
- import_react_native_paper2.Text,
202
- {
203
- style: [
204
- styles2.rowLabel,
205
- {
206
- color: selected ? (0, import_auto_paper2.getContrastColor)(rowColor) : fg
207
- }
208
- ],
209
- numberOfLines: 1,
210
- children: profile.name
211
- }
212
- )
213
- ] }) }, profile.id);
214
- }),
215
- onManage && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
189
+ children: [
190
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_auto_paper2.FadingScrollView, { contentContainerStyle: [styles2.menuContent, manageFooter && styles2.menuContentAboveFooter], showsVerticalScrollIndicator: false, children: colorSection && colorPage ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ColorPage, { section: colorSection, profileColor: selectedProfile?.color, fg, onBack: () => setColorPage(false) }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
191
+ colorSection && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ColorRow, { section: colorSection, fg, dividerColor: color, onPress: () => setColorPage(true) }),
192
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_feedback_press.TouchableRipple, { onPress: handleSelectGuest, style: [styles2.row, selectedId === null && { backgroundColor: color }], children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native2.View, { style: styles2.rowInner, children: [
193
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native2.View, { style: styles2.guestChip, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native_paper2.Icon, { source: nullIcon, size: 13, color: selectedId === null ? (0, import_auto_paper2.getContrastColor)(color) : fg }) }),
194
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native_paper2.Text, { style: [styles2.rowLabel, { color: selectedId === null ? (0, import_auto_paper2.getContrastColor)(color) : fg }], children: nullLabel })
195
+ ] }) }),
196
+ sortedProfiles.map((profile) => {
197
+ const taken = profile.id === takenId;
198
+ const selected = selectedId === profile.id;
199
+ const rowColor = colorPreview ? colorPreview(profile.color) : profile.color;
200
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_feedback_press.TouchableRipple, { disabled: taken, onPress: () => handleSelectExisting(profile), style: [styles2.row, selected && { backgroundColor: rowColor }], children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native2.View, { style: [styles2.rowInner, taken && styles2.rowDisabled], children: [
201
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ProfileChip, { profile, filled: !selected }),
202
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
203
+ import_react_native_paper2.Text,
204
+ {
205
+ style: [
206
+ styles2.rowLabel,
207
+ {
208
+ color: selected ? (0, import_auto_paper2.getContrastColor)(rowColor) : fg
209
+ }
210
+ ],
211
+ numberOfLines: 1,
212
+ children: profile.name
213
+ }
214
+ )
215
+ ] }) }, profile.id);
216
+ })
217
+ ] }) }, colorSection && colorPage ? "color" : "list"),
218
+ manageFooter && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native2.View, { style: styles2.menuFooter, children: [
216
219
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native2.View, { style: [styles2.divider, { backgroundColor: color }] }),
217
220
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_feedback_press.TouchableRipple, { onPress: handleManage, style: styles2.row, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react_native2.View, { style: styles2.manageRowInner, children: [
218
221
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native_paper2.Icon, { source: "account-cog-outline", size: 16, color }),
219
222
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_native_paper2.Text, { style: [styles2.manageLabel, { color: fg }], children: "Manage" })
220
223
  ] }) })
221
224
  ] })
222
- ] }) }, colorSection && colorPage ? "color" : "list")
225
+ ]
223
226
  }
224
227
  ) })
225
228
  ] });
@@ -345,6 +348,14 @@ var styles2 = import_react_native2.StyleSheet.create({
345
348
  menuContent: {
346
349
  padding: LIST_PADDING
347
350
  },
351
+ // With Manage pinned below, its own padding takes the bottom edge (same total height as before).
352
+ menuContentAboveFooter: {
353
+ paddingBottom: 0
354
+ },
355
+ menuFooter: {
356
+ paddingBottom: LIST_PADDING,
357
+ paddingHorizontal: LIST_PADDING
358
+ },
348
359
  // The selected profile's own color (see ColorSection).
349
360
  profileDot: {
350
361
  borderRadius: 4,
package/dist/index.mjs CHANGED
@@ -124,13 +124,14 @@ function ProfilePicker({ idPrefix, host, profiles, selectedId, takenId, color, d
124
124
  host.toggle(id);
125
125
  };
126
126
  const triggerLabel = (selectedProfile?.name ?? guestLabel).toUpperCase();
127
+ const manageFooter = !!onManage && !(colorSection && colorPage);
127
128
  return /* @__PURE__ */ jsxs(View2, { style: [styles2.anchor, open && styles2.anchorOpen], onLayout: handleAnchorLayout, children: [
128
129
  /* @__PURE__ */ jsx4(TouchableRipple, { onPress: handleTriggerPress, borderless: chipTrigger, style: [styles2.trigger, chipTrigger && styles2.chipTrigger, triggerStyle], accessibilityRole: "button", accessibilityLabel: triggerLabel, children: /* @__PURE__ */ jsxs(View2, { ref: triggerRef, collapsable: false, style: chipTrigger ? styles2.chipTriggerInner : styles2.triggerInner, children: [
129
130
  chipTrigger && /* @__PURE__ */ jsx4(IdentityChip, { color, tag: selectedProfile?.tag, icon: selectedProfile ? "account" : nullIcon, size: CHIP_SIZE }),
130
131
  /* @__PURE__ */ jsx4(Text2, { style: [styles2.triggerLabel, { color: chipTrigger ? fg : color }], numberOfLines: 1, children: triggerLabel }),
131
132
  /* @__PURE__ */ jsx4(Icon2, { source: "menu-down", size: 16, color: chipTrigger ? fg : color })
132
133
  ] }) }),
133
- /* @__PURE__ */ jsx4(PopoverBody, { visible: open && measured, align, verticalAlign, caretColor: menuBg, caretBorderColor: color, caretBorderWidth: MENU_BORDER_WIDTH, triggerSize: chipTrigger ? triggerWidth : TRIGGER_HEIGHT, children: /* @__PURE__ */ jsx4(
134
+ /* @__PURE__ */ jsx4(PopoverBody, { visible: open && measured, align, verticalAlign, caretColor: menuBg, caretBorderColor: color, caretBorderWidth: MENU_BORDER_WIDTH, triggerSize: chipTrigger ? triggerWidth : TRIGGER_HEIGHT, children: /* @__PURE__ */ jsxs(
134
135
  View2,
135
136
  {
136
137
  style: [
@@ -142,41 +143,43 @@ function ProfilePicker({ idPrefix, host, profiles, selectedId, takenId, color, d
142
143
  maxHeight
143
144
  }
144
145
  ],
145
- children: /* @__PURE__ */ jsx4(FadingScrollView, { contentContainerStyle: styles2.menuContent, showsVerticalScrollIndicator: false, children: colorSection && colorPage ? /* @__PURE__ */ jsx4(ColorPage, { section: colorSection, profileColor: selectedProfile?.color, fg, onBack: () => setColorPage(false) }) : /* @__PURE__ */ jsxs(Fragment, { children: [
146
- colorSection && /* @__PURE__ */ jsx4(ColorRow, { section: colorSection, fg, dividerColor: color, onPress: () => setColorPage(true) }),
147
- /* @__PURE__ */ jsx4(TouchableRipple, { onPress: handleSelectGuest, style: [styles2.row, selectedId === null && { backgroundColor: color }], children: /* @__PURE__ */ jsxs(View2, { style: styles2.rowInner, children: [
148
- /* @__PURE__ */ jsx4(View2, { style: styles2.guestChip, children: /* @__PURE__ */ jsx4(Icon2, { source: nullIcon, size: 13, color: selectedId === null ? getContrastColor2(color) : fg }) }),
149
- /* @__PURE__ */ jsx4(Text2, { style: [styles2.rowLabel, { color: selectedId === null ? getContrastColor2(color) : fg }], children: nullLabel })
150
- ] }) }),
151
- sortedProfiles.map((profile) => {
152
- const taken = profile.id === takenId;
153
- const selected = selectedId === profile.id;
154
- const rowColor = colorPreview ? colorPreview(profile.color) : profile.color;
155
- return /* @__PURE__ */ jsx4(TouchableRipple, { disabled: taken, onPress: () => handleSelectExisting(profile), style: [styles2.row, selected && { backgroundColor: rowColor }], children: /* @__PURE__ */ jsxs(View2, { style: [styles2.rowInner, taken && styles2.rowDisabled], children: [
156
- /* @__PURE__ */ jsx4(ProfileChip, { profile, filled: !selected }),
157
- /* @__PURE__ */ jsx4(
158
- Text2,
159
- {
160
- style: [
161
- styles2.rowLabel,
162
- {
163
- color: selected ? getContrastColor2(rowColor) : fg
164
- }
165
- ],
166
- numberOfLines: 1,
167
- children: profile.name
168
- }
169
- )
170
- ] }) }, profile.id);
171
- }),
172
- onManage && /* @__PURE__ */ jsxs(Fragment, { children: [
146
+ children: [
147
+ /* @__PURE__ */ jsx4(FadingScrollView, { contentContainerStyle: [styles2.menuContent, manageFooter && styles2.menuContentAboveFooter], showsVerticalScrollIndicator: false, children: colorSection && colorPage ? /* @__PURE__ */ jsx4(ColorPage, { section: colorSection, profileColor: selectedProfile?.color, fg, onBack: () => setColorPage(false) }) : /* @__PURE__ */ jsxs(Fragment, { children: [
148
+ colorSection && /* @__PURE__ */ jsx4(ColorRow, { section: colorSection, fg, dividerColor: color, onPress: () => setColorPage(true) }),
149
+ /* @__PURE__ */ jsx4(TouchableRipple, { onPress: handleSelectGuest, style: [styles2.row, selectedId === null && { backgroundColor: color }], children: /* @__PURE__ */ jsxs(View2, { style: styles2.rowInner, children: [
150
+ /* @__PURE__ */ jsx4(View2, { style: styles2.guestChip, children: /* @__PURE__ */ jsx4(Icon2, { source: nullIcon, size: 13, color: selectedId === null ? getContrastColor2(color) : fg }) }),
151
+ /* @__PURE__ */ jsx4(Text2, { style: [styles2.rowLabel, { color: selectedId === null ? getContrastColor2(color) : fg }], children: nullLabel })
152
+ ] }) }),
153
+ sortedProfiles.map((profile) => {
154
+ const taken = profile.id === takenId;
155
+ const selected = selectedId === profile.id;
156
+ const rowColor = colorPreview ? colorPreview(profile.color) : profile.color;
157
+ return /* @__PURE__ */ jsx4(TouchableRipple, { disabled: taken, onPress: () => handleSelectExisting(profile), style: [styles2.row, selected && { backgroundColor: rowColor }], children: /* @__PURE__ */ jsxs(View2, { style: [styles2.rowInner, taken && styles2.rowDisabled], children: [
158
+ /* @__PURE__ */ jsx4(ProfileChip, { profile, filled: !selected }),
159
+ /* @__PURE__ */ jsx4(
160
+ Text2,
161
+ {
162
+ style: [
163
+ styles2.rowLabel,
164
+ {
165
+ color: selected ? getContrastColor2(rowColor) : fg
166
+ }
167
+ ],
168
+ numberOfLines: 1,
169
+ children: profile.name
170
+ }
171
+ )
172
+ ] }) }, profile.id);
173
+ })
174
+ ] }) }, colorSection && colorPage ? "color" : "list"),
175
+ manageFooter && /* @__PURE__ */ jsxs(View2, { style: styles2.menuFooter, children: [
173
176
  /* @__PURE__ */ jsx4(View2, { style: [styles2.divider, { backgroundColor: color }] }),
174
177
  /* @__PURE__ */ jsx4(TouchableRipple, { onPress: handleManage, style: styles2.row, children: /* @__PURE__ */ jsxs(View2, { style: styles2.manageRowInner, children: [
175
178
  /* @__PURE__ */ jsx4(Icon2, { source: "account-cog-outline", size: 16, color }),
176
179
  /* @__PURE__ */ jsx4(Text2, { style: [styles2.manageLabel, { color: fg }], children: "Manage" })
177
180
  ] }) })
178
181
  ] })
179
- ] }) }, colorSection && colorPage ? "color" : "list")
182
+ ]
180
183
  }
181
184
  ) })
182
185
  ] });
@@ -302,6 +305,14 @@ var styles2 = StyleSheet2.create({
302
305
  menuContent: {
303
306
  padding: LIST_PADDING
304
307
  },
308
+ // With Manage pinned below, its own padding takes the bottom edge (same total height as before).
309
+ menuContentAboveFooter: {
310
+ paddingBottom: 0
311
+ },
312
+ menuFooter: {
313
+ paddingBottom: LIST_PADDING,
314
+ paddingHorizontal: LIST_PADDING
315
+ },
305
316
  // The selected profile's own color (see ColorSection).
306
317
  profileDot: {
307
318
  borderRadius: 4,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tastic/profile",
3
- "version": "0.14.0",
3
+ "version": "0.14.1",
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",
@@ -245,6 +245,8 @@ export function ProfilePicker<P extends Profile>({ idPrefix, host, profiles, sel
245
245
  host.toggle(id)
246
246
  }
247
247
  const triggerLabel = (selectedProfile?.name ?? guestLabel).toUpperCase()
248
+ // Shown on the list page only (see the footer below).
249
+ const manageFooter = !!onManage && !(colorSection && colorPage)
248
250
 
249
251
  return (
250
252
  <View style={[styles.anchor, open && styles.anchorOpen]} onLayout={handleAnchorLayout}>
@@ -278,7 +280,7 @@ export function ProfilePicker<P extends Profile>({ idPrefix, host, profiles, sel
278
280
  ]}
279
281
  >
280
282
  {/* Keyed by page, so swapping pages starts the new one scrolled to its top. */}
281
- <FadingScrollView key={colorSection && colorPage ? 'color' : 'list'} contentContainerStyle={styles.menuContent} showsVerticalScrollIndicator={false}>
283
+ <FadingScrollView key={colorSection && colorPage ? 'color' : 'list'} contentContainerStyle={[styles.menuContent, manageFooter && styles.menuContentAboveFooter]} showsVerticalScrollIndicator={false}>
282
284
  {colorSection && colorPage ? (
283
285
  <ColorPage section={colorSection} profileColor={selectedProfile?.color} fg={fg} onBack={() => setColorPage(false)} />
284
286
  ) : (
@@ -327,25 +329,27 @@ export function ProfilePicker<P extends Profile>({ idPrefix, host, profiles, sel
327
329
  </TouchableRipple>
328
330
  )
329
331
  })}
330
-
331
- {onManage && (
332
- <>
333
- <View style={[styles.divider, { backgroundColor: color }]} />
334
- {/* Tertiary - a navigation action, not another selectable identity in the list above
335
- it, so it reads more like @tastic/hud's SectionedDropdown All/Clear footer button
336
- (centered, bold) than a normal row. The accent is on the icon only: a muted accent
337
- label on the menu's black read dim, almost disabled (2026-10-16). */}
338
- <TouchableRipple onPress={handleManage} style={styles.row}>
339
- <View style={styles.manageRowInner}>
340
- <Icon source='account-cog-outline' size={16} color={color} />
341
- <Text style={[styles.manageLabel, { color: fg }]}>Manage</Text>
342
- </View>
343
- </TouchableRipple>
344
- </>
345
- )}
346
332
  </>
347
333
  )}
348
334
  </FadingScrollView>
335
+ {/* Manage is pinned below the scrolling rows, the way a dialog's Done sits below its rows:
336
+ always reachable, with the rows fading out above its divider while there are more below
337
+ (2026-10-16; it used to be the list's last row and scrolled away with it). Tertiary - a
338
+ navigation action, not another selectable identity in the list above it, so it reads more
339
+ like @tastic/hud's SectionedDropdown All/Clear footer button (centered, bold) than a normal
340
+ row. The accent is on the icon only: a muted accent label on the menu's black read dim,
341
+ almost disabled. Not on the color page, which only has the back row and the swatches. */}
342
+ {manageFooter && (
343
+ <View style={styles.menuFooter}>
344
+ <View style={[styles.divider, { backgroundColor: color }]} />
345
+ <TouchableRipple onPress={handleManage} style={styles.row}>
346
+ <View style={styles.manageRowInner}>
347
+ <Icon source='account-cog-outline' size={16} color={color} />
348
+ <Text style={[styles.manageLabel, { color: fg }]}>Manage</Text>
349
+ </View>
350
+ </TouchableRipple>
351
+ </View>
352
+ )}
349
353
  </View>
350
354
  </PopoverBody>
351
355
  </View>
@@ -509,6 +513,14 @@ const styles = StyleSheet.create({
509
513
  menuContent: {
510
514
  padding: LIST_PADDING
511
515
  },
516
+ // With Manage pinned below, its own padding takes the bottom edge (same total height as before).
517
+ menuContentAboveFooter: {
518
+ paddingBottom: 0
519
+ },
520
+ menuFooter: {
521
+ paddingBottom: LIST_PADDING,
522
+ paddingHorizontal: LIST_PADDING
523
+ },
512
524
  // The selected profile's own color (see ColorSection).
513
525
  profileDot: {
514
526
  borderRadius: 4,