@tastic/hud 0.1.3 → 0.1.5
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 +6 -2
- package/dist/index.d.mts +4 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +22 -13
- package/dist/index.mjs +30 -21
- package/package.json +13 -15
- package/src/InlineColorPicker.tsx +61 -19
package/README.md
CHANGED
|
@@ -28,8 +28,12 @@ corner of the screen.
|
|
|
28
28
|
`maxHeight` for the caller to cap content and scroll when neither direction has enough room.
|
|
29
29
|
- **`SectionedDropdown`** — a popover holding any mix of single-select ("pick one", radio-style) and
|
|
30
30
|
multi-select ("pick some", checkbox-style, optional all/clear footer) sections, divided by rules.
|
|
31
|
-
- **`InlineColorPicker`** — swatch-grid color popover, auto-
|
|
32
|
-
|
|
31
|
+
- **`InlineColorPicker`** — swatch-grid color popover, columns auto-clamped to 4 or 5 (the default
|
|
32
|
+
`swatches` list is exactly 20 colors, so that's the only range that fills every row completely),
|
|
33
|
+
with an optional "taken" color (shown disabled, or swappable). The trigger itself is a `size`-
|
|
34
|
+
configurable circle showing either an `icon` or a short `tag` (e.g. a saved player identity's own
|
|
35
|
+
initial or emoji) — `tag` renders at a larger ratio automatically when it's an emoji, since an
|
|
36
|
+
emoji glyph reads visually smaller than a bold letter at the same font size.
|
|
33
37
|
- **`TriggerGauge`** — decorative ring of tick marks around a trigger, showing which option(s) are
|
|
34
38
|
active without opening the popover.
|
|
35
39
|
- **`ReadyButton`** — a per-player or standalone ready toggle.
|
package/dist/index.d.mts
CHANGED
|
@@ -23,10 +23,13 @@ interface Props$4 {
|
|
|
23
23
|
dark: boolean;
|
|
24
24
|
align?: 'left' | 'right' | 'center';
|
|
25
25
|
icon?: string;
|
|
26
|
+
tag?: string;
|
|
27
|
+
labelFontFamily?: string;
|
|
26
28
|
autoDismiss?: boolean;
|
|
27
29
|
columns?: number;
|
|
30
|
+
size?: number;
|
|
28
31
|
}
|
|
29
|
-
declare function InlineColorPicker({ id, host, value, onChange, swatches, takenValue, allowSwapTaken, dark, align: alignOverride, icon, autoDismiss, columns }: Props$4): react.JSX.Element;
|
|
32
|
+
declare function InlineColorPicker({ id, host, value, onChange, swatches, takenValue, allowSwapTaken, dark, align: alignOverride, icon, tag, labelFontFamily, autoDismiss, columns, size }: Props$4): react.JSX.Element;
|
|
30
33
|
|
|
31
34
|
declare function loadSkiaWeb(): Promise<void>;
|
|
32
35
|
|
package/dist/index.d.ts
CHANGED
|
@@ -23,10 +23,13 @@ interface Props$4 {
|
|
|
23
23
|
dark: boolean;
|
|
24
24
|
align?: 'left' | 'right' | 'center';
|
|
25
25
|
icon?: string;
|
|
26
|
+
tag?: string;
|
|
27
|
+
labelFontFamily?: string;
|
|
26
28
|
autoDismiss?: boolean;
|
|
27
29
|
columns?: number;
|
|
30
|
+
size?: number;
|
|
28
31
|
}
|
|
29
|
-
declare function InlineColorPicker({ id, host, value, onChange, swatches, takenValue, allowSwapTaken, dark, align: alignOverride, icon, autoDismiss, columns }: Props$4): react.JSX.Element;
|
|
32
|
+
declare function InlineColorPicker({ id, host, value, onChange, swatches, takenValue, allowSwapTaken, dark, align: alignOverride, icon, tag, labelFontFamily, autoDismiss, columns, size }: Props$4): react.JSX.Element;
|
|
30
33
|
|
|
31
34
|
declare function loadSkiaWeb(): Promise<void>;
|
|
32
35
|
|
package/dist/index.js
CHANGED
|
@@ -287,18 +287,19 @@ function useAutoAlign(open, contentWidth, contentHeight) {
|
|
|
287
287
|
|
|
288
288
|
// src/InlineColorPicker.tsx
|
|
289
289
|
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
290
|
-
var
|
|
290
|
+
var EMOJI_PATTERN = /\p{Extended_Pictographic}/u;
|
|
291
|
+
var DEFAULT_SIZE = 48;
|
|
291
292
|
var SWATCH_SIZE = 28;
|
|
292
293
|
var SWATCHES_PADDING = 8;
|
|
293
294
|
var SWATCHES_GAP = 6;
|
|
294
295
|
var SWATCHES_BORDER_WIDTH = 2;
|
|
295
|
-
var MIN_COLUMNS =
|
|
296
|
-
var MAX_COLUMNS =
|
|
296
|
+
var MIN_COLUMNS = 4;
|
|
297
|
+
var MAX_COLUMNS = 5;
|
|
297
298
|
var SCREEN_MARGIN = 40;
|
|
298
299
|
function widthForColumns(columns) {
|
|
299
300
|
return SWATCHES_BORDER_WIDTH * 2 + SWATCHES_PADDING * 2 + SWATCH_SIZE * columns + SWATCHES_GAP * (columns - 1);
|
|
300
301
|
}
|
|
301
|
-
function InlineColorPicker({ id, host, value, onChange, swatches = import_auto_paper.defaultColors, takenValue, allowSwapTaken, dark, align: alignOverride, icon = "palette", autoDismiss = true, columns }) {
|
|
302
|
+
function InlineColorPicker({ id, host, value, onChange, swatches = import_auto_paper.defaultColors, takenValue, allowSwapTaken, dark, align: alignOverride, icon = "palette", tag, labelFontFamily = MONO_FONT, autoDismiss = true, columns, size = DEFAULT_SIZE }) {
|
|
302
303
|
const menuBg = dark ? "#000000" : "#FFFFFF";
|
|
303
304
|
const { width: windowWidth } = (0, import_react_native4.useWindowDimensions)();
|
|
304
305
|
const autoColumns = (0, import_core.clamp)(Math.floor((windowWidth - 2 * SCREEN_MARGIN + SWATCHES_GAP) / (SWATCH_SIZE + SWATCHES_GAP)), MIN_COLUMNS, MAX_COLUMNS);
|
|
@@ -309,9 +310,11 @@ function InlineColorPicker({ id, host, value, onChange, swatches = import_auto_p
|
|
|
309
310
|
const open = host.openId === id;
|
|
310
311
|
const { align: autoAlign, maxHeight, measured, triggerRef, verticalAlign } = useAutoAlign(open, swatchesWidth, swatchesHeight);
|
|
311
312
|
const align = alignOverride ?? autoAlign;
|
|
313
|
+
const contrastColor = (0, import_auto_paper.getContrastColor)(value);
|
|
314
|
+
const tagFontSize = size * (tag && EMOJI_PATTERN.test(tag) ? 0.6 : 0.4);
|
|
312
315
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react_native4.View, { style: [styles2.anchor, open && styles2.anchorOpen], children: [
|
|
313
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_feedback_press.TouchableRipple, { onPress: () => host.toggle(id), borderless: true, style: [styles2.trigger, { backgroundColor: value }], children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native4.View, { ref: triggerRef, collapsable: false, style: styles2.triggerMeasure, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native_paper.
|
|
314
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(PopoverBody, { visible: open && measured, align, verticalAlign, caretColor: menuBg, caretBorderColor: value, caretBorderWidth: SWATCHES_BORDER_WIDTH, triggerSize:
|
|
316
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_feedback_press.TouchableRipple, { onPress: () => host.toggle(id), borderless: true, style: [styles2.trigger, { backgroundColor: value, borderRadius: size / 2, height: size, width: size }], children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native4.View, { ref: triggerRef, collapsable: false, style: styles2.triggerMeasure, children: tag ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native_paper.Text, { style: [styles2.tagLabel, { color: contrastColor, fontFamily: labelFontFamily, fontSize: tagFontSize }], numberOfLines: 1, adjustsFontSizeToFit: true, children: tag }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native_paper.Icon, { source: icon, size: size * 0.6, color: contrastColor }) }) }),
|
|
317
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(PopoverBody, { visible: open && measured, align, verticalAlign, caretColor: menuBg, caretBorderColor: value, caretBorderWidth: SWATCHES_BORDER_WIDTH, triggerSize: size, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native4.ScrollView, { style: [styles2.swatches, { backgroundColor: menuBg, borderColor: value, width: swatchesWidth, maxHeight }], contentContainerStyle: styles2.swatchesContent, showsVerticalScrollIndicator: false, children: swatches.map((swatch) => {
|
|
315
318
|
const selected = swatch.value.toLowerCase() === value.toLowerCase();
|
|
316
319
|
const taken = !selected && !!takenValue && swatch.value.toLowerCase() === takenValue.toLowerCase();
|
|
317
320
|
const swappable = taken && allowSwapTaken;
|
|
@@ -378,22 +381,28 @@ var styles2 = import_react_native4.StyleSheet.create({
|
|
|
378
381
|
gap: SWATCHES_GAP,
|
|
379
382
|
padding: SWATCHES_PADDING
|
|
380
383
|
},
|
|
384
|
+
// borderRadius/height/width come from the `size` prop instead (see render-site override) — no
|
|
385
|
+
// fixed default here, since this style object is shared by every caller regardless of size.
|
|
381
386
|
trigger: {
|
|
382
387
|
alignItems: "center",
|
|
383
388
|
alignSelf: "flex-start",
|
|
384
|
-
|
|
385
|
-
height: SIZE,
|
|
386
|
-
justifyContent: "center",
|
|
387
|
-
width: SIZE
|
|
389
|
+
justifyContent: "center"
|
|
388
390
|
},
|
|
389
|
-
// Wraps just the icon, inside the TouchableRipple, purely so useAutoAlign has a plain View
|
|
390
|
-
// attach its measurement ref to — TouchableRipple itself doesn't forward a ref to a
|
|
391
|
-
// native node.
|
|
391
|
+
// Wraps just the icon/tag, inside the TouchableRipple, purely so useAutoAlign has a plain View
|
|
392
|
+
// to attach its measurement ref to — TouchableRipple itself doesn't forward a ref to a
|
|
393
|
+
// measurable native node. Padded horizontally so a multi-character tag doesn't butt right up
|
|
394
|
+
// against the trigger's own circular edge (an icon alone doesn't need it, but this style is
|
|
395
|
+
// shared by both).
|
|
392
396
|
triggerMeasure: {
|
|
393
397
|
alignItems: "center",
|
|
394
398
|
height: "100%",
|
|
395
399
|
justifyContent: "center",
|
|
400
|
+
paddingHorizontal: 4,
|
|
396
401
|
width: "100%"
|
|
402
|
+
},
|
|
403
|
+
tagLabel: {
|
|
404
|
+
fontWeight: "700",
|
|
405
|
+
letterSpacing: 0.5
|
|
397
406
|
}
|
|
398
407
|
});
|
|
399
408
|
|
package/dist/index.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import { defaultColors, getContrastColor } from "@rific/auto-paper";
|
|
|
7
7
|
import { TouchableRipple } from "@rific/feedback-press";
|
|
8
8
|
import { clamp } from "@tastic/core";
|
|
9
9
|
import { ScrollView, StyleSheet as StyleSheet2, useWindowDimensions as useWindowDimensions2, View as View3 } from "react-native";
|
|
10
|
-
import { Icon } from "react-native-paper";
|
|
10
|
+
import { Icon, Text } from "react-native-paper";
|
|
11
11
|
|
|
12
12
|
// src/PopoverBody.tsx
|
|
13
13
|
import { StyleSheet, View } from "react-native";
|
|
@@ -115,18 +115,19 @@ function useAutoAlign(open, contentWidth, contentHeight) {
|
|
|
115
115
|
|
|
116
116
|
// src/InlineColorPicker.tsx
|
|
117
117
|
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
118
|
-
var
|
|
118
|
+
var EMOJI_PATTERN = /\p{Extended_Pictographic}/u;
|
|
119
|
+
var DEFAULT_SIZE = 48;
|
|
119
120
|
var SWATCH_SIZE = 28;
|
|
120
121
|
var SWATCHES_PADDING = 8;
|
|
121
122
|
var SWATCHES_GAP = 6;
|
|
122
123
|
var SWATCHES_BORDER_WIDTH = 2;
|
|
123
|
-
var MIN_COLUMNS =
|
|
124
|
-
var MAX_COLUMNS =
|
|
124
|
+
var MIN_COLUMNS = 4;
|
|
125
|
+
var MAX_COLUMNS = 5;
|
|
125
126
|
var SCREEN_MARGIN = 40;
|
|
126
127
|
function widthForColumns(columns) {
|
|
127
128
|
return SWATCHES_BORDER_WIDTH * 2 + SWATCHES_PADDING * 2 + SWATCH_SIZE * columns + SWATCHES_GAP * (columns - 1);
|
|
128
129
|
}
|
|
129
|
-
function InlineColorPicker({ id, host, value, onChange, swatches = defaultColors, takenValue, allowSwapTaken, dark, align: alignOverride, icon = "palette", autoDismiss = true, columns }) {
|
|
130
|
+
function InlineColorPicker({ id, host, value, onChange, swatches = defaultColors, takenValue, allowSwapTaken, dark, align: alignOverride, icon = "palette", tag, labelFontFamily = MONO_FONT, autoDismiss = true, columns, size = DEFAULT_SIZE }) {
|
|
130
131
|
const menuBg = dark ? "#000000" : "#FFFFFF";
|
|
131
132
|
const { width: windowWidth } = useWindowDimensions2();
|
|
132
133
|
const autoColumns = clamp(Math.floor((windowWidth - 2 * SCREEN_MARGIN + SWATCHES_GAP) / (SWATCH_SIZE + SWATCHES_GAP)), MIN_COLUMNS, MAX_COLUMNS);
|
|
@@ -137,9 +138,11 @@ function InlineColorPicker({ id, host, value, onChange, swatches = defaultColors
|
|
|
137
138
|
const open = host.openId === id;
|
|
138
139
|
const { align: autoAlign, maxHeight, measured, triggerRef, verticalAlign } = useAutoAlign(open, swatchesWidth, swatchesHeight);
|
|
139
140
|
const align = alignOverride ?? autoAlign;
|
|
141
|
+
const contrastColor = getContrastColor(value);
|
|
142
|
+
const tagFontSize = size * (tag && EMOJI_PATTERN.test(tag) ? 0.6 : 0.4);
|
|
140
143
|
return /* @__PURE__ */ jsxs2(View3, { style: [styles2.anchor, open && styles2.anchorOpen], children: [
|
|
141
|
-
/* @__PURE__ */ jsx2(TouchableRipple, { onPress: () => host.toggle(id), borderless: true, style: [styles2.trigger, { backgroundColor: value }], children: /* @__PURE__ */ jsx2(View3, { ref: triggerRef, collapsable: false, style: styles2.triggerMeasure, children: /* @__PURE__ */ jsx2(Icon, { source: icon, size:
|
|
142
|
-
/* @__PURE__ */ jsx2(PopoverBody, { visible: open && measured, align, verticalAlign, caretColor: menuBg, caretBorderColor: value, caretBorderWidth: SWATCHES_BORDER_WIDTH, triggerSize:
|
|
144
|
+
/* @__PURE__ */ jsx2(TouchableRipple, { onPress: () => host.toggle(id), borderless: true, style: [styles2.trigger, { backgroundColor: value, borderRadius: size / 2, height: size, width: size }], children: /* @__PURE__ */ jsx2(View3, { ref: triggerRef, collapsable: false, style: styles2.triggerMeasure, children: tag ? /* @__PURE__ */ jsx2(Text, { style: [styles2.tagLabel, { color: contrastColor, fontFamily: labelFontFamily, fontSize: tagFontSize }], numberOfLines: 1, adjustsFontSizeToFit: true, children: tag }) : /* @__PURE__ */ jsx2(Icon, { source: icon, size: size * 0.6, color: contrastColor }) }) }),
|
|
145
|
+
/* @__PURE__ */ jsx2(PopoverBody, { visible: open && measured, align, verticalAlign, caretColor: menuBg, caretBorderColor: value, caretBorderWidth: SWATCHES_BORDER_WIDTH, triggerSize: size, children: /* @__PURE__ */ jsx2(ScrollView, { style: [styles2.swatches, { backgroundColor: menuBg, borderColor: value, width: swatchesWidth, maxHeight }], contentContainerStyle: styles2.swatchesContent, showsVerticalScrollIndicator: false, children: swatches.map((swatch) => {
|
|
143
146
|
const selected = swatch.value.toLowerCase() === value.toLowerCase();
|
|
144
147
|
const taken = !selected && !!takenValue && swatch.value.toLowerCase() === takenValue.toLowerCase();
|
|
145
148
|
const swappable = taken && allowSwapTaken;
|
|
@@ -206,22 +209,28 @@ var styles2 = StyleSheet2.create({
|
|
|
206
209
|
gap: SWATCHES_GAP,
|
|
207
210
|
padding: SWATCHES_PADDING
|
|
208
211
|
},
|
|
212
|
+
// borderRadius/height/width come from the `size` prop instead (see render-site override) — no
|
|
213
|
+
// fixed default here, since this style object is shared by every caller regardless of size.
|
|
209
214
|
trigger: {
|
|
210
215
|
alignItems: "center",
|
|
211
216
|
alignSelf: "flex-start",
|
|
212
|
-
|
|
213
|
-
height: SIZE,
|
|
214
|
-
justifyContent: "center",
|
|
215
|
-
width: SIZE
|
|
217
|
+
justifyContent: "center"
|
|
216
218
|
},
|
|
217
|
-
// Wraps just the icon, inside the TouchableRipple, purely so useAutoAlign has a plain View
|
|
218
|
-
// attach its measurement ref to — TouchableRipple itself doesn't forward a ref to a
|
|
219
|
-
// native node.
|
|
219
|
+
// Wraps just the icon/tag, inside the TouchableRipple, purely so useAutoAlign has a plain View
|
|
220
|
+
// to attach its measurement ref to — TouchableRipple itself doesn't forward a ref to a
|
|
221
|
+
// measurable native node. Padded horizontally so a multi-character tag doesn't butt right up
|
|
222
|
+
// against the trigger's own circular edge (an icon alone doesn't need it, but this style is
|
|
223
|
+
// shared by both).
|
|
220
224
|
triggerMeasure: {
|
|
221
225
|
alignItems: "center",
|
|
222
226
|
height: "100%",
|
|
223
227
|
justifyContent: "center",
|
|
228
|
+
paddingHorizontal: 4,
|
|
224
229
|
width: "100%"
|
|
230
|
+
},
|
|
231
|
+
tagLabel: {
|
|
232
|
+
fontWeight: "700",
|
|
233
|
+
letterSpacing: 0.5
|
|
225
234
|
}
|
|
226
235
|
});
|
|
227
236
|
|
|
@@ -241,13 +250,13 @@ function PressAwayOverlay({ active, onPress, style }) {
|
|
|
241
250
|
// src/ReadyButton.tsx
|
|
242
251
|
import { TouchableRipple as TouchableRipple2 } from "@rific/feedback-press";
|
|
243
252
|
import { StyleSheet as StyleSheet4 } from "react-native";
|
|
244
|
-
import { Text } from "react-native-paper";
|
|
253
|
+
import { Text as Text2 } from "react-native-paper";
|
|
245
254
|
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
246
255
|
function ReadyButton({ color, ready, onToggleReady, style, labelFontFamily = MONO_FONT }) {
|
|
247
256
|
const readyButtonStyle = [styles3.readyButton, { borderColor: color }, ready && { backgroundColor: color }, style];
|
|
248
257
|
const labelTextStyle = { fontFamily: labelFontFamily, fontWeight: "bold" };
|
|
249
258
|
const labelColorStyle = { color: ready ? "#000000" : color };
|
|
250
|
-
return /* @__PURE__ */ jsx4(TouchableRipple2, { onPress: onToggleReady, borderless: true, style: readyButtonStyle, children: /* @__PURE__ */ jsx4(
|
|
259
|
+
return /* @__PURE__ */ jsx4(TouchableRipple2, { onPress: onToggleReady, borderless: true, style: readyButtonStyle, children: /* @__PURE__ */ jsx4(Text2, { variant: "labelLarge", style: [labelTextStyle, labelColorStyle], children: ready ? "READY \u2713" : "READY?" }) });
|
|
251
260
|
}
|
|
252
261
|
var styles3 = StyleSheet4.create({
|
|
253
262
|
readyButton: {
|
|
@@ -265,7 +274,7 @@ var styles3 = StyleSheet4.create({
|
|
|
265
274
|
import { getBlendedColor, getColorRoles } from "@rific/auto-paper";
|
|
266
275
|
import { IconButton, TouchableRipple as TouchableRipple3 } from "@rific/feedback-press";
|
|
267
276
|
import { ScrollView as ScrollView2, StyleSheet as StyleSheet5, View as View4 } from "react-native";
|
|
268
|
-
import { Icon as Icon2, Text as
|
|
277
|
+
import { Icon as Icon2, Text as Text3 } from "react-native-paper";
|
|
269
278
|
|
|
270
279
|
// src/TriggerGaugeHost.tsx
|
|
271
280
|
import { lazy, Suspense } from "react";
|
|
@@ -366,8 +375,8 @@ function SingleSection({
|
|
|
366
375
|
return /* @__PURE__ */ jsx6(TouchableRipple3, { onPress: () => onSelect(option.value), style: itemStyle, children: /* @__PURE__ */ jsxs3(View4, { style: styles4.itemRow, children: [
|
|
367
376
|
option.icon && /* @__PURE__ */ jsx6(Icon2, { source: option.icon, size: MENU_ITEM_ICON_SIZE, color: rowColor }),
|
|
368
377
|
/* @__PURE__ */ jsxs3(View4, { style: styles4.itemLabelColumn, children: [
|
|
369
|
-
/* @__PURE__ */ jsx6(
|
|
370
|
-
option.description && /* @__PURE__ */ jsx6(
|
|
378
|
+
/* @__PURE__ */ jsx6(Text3, { variant: "labelLarge", style: labelStyle, children: option.label }),
|
|
379
|
+
option.description && /* @__PURE__ */ jsx6(Text3, { variant: "labelSmall", style: descriptionStyle, children: option.description })
|
|
371
380
|
] })
|
|
372
381
|
] }) }, option.value);
|
|
373
382
|
}) });
|
|
@@ -391,10 +400,10 @@ function MultiSection({
|
|
|
391
400
|
const labelStyle = { fontFamily: labelFontFamily, color: rowColor, fontWeight: selected ? "bold" : "normal" };
|
|
392
401
|
return /* @__PURE__ */ jsx6(TouchableRipple3, { onPress: () => section.onChange(selected ? section.value.filter((v) => v !== option.value) : [...section.value, option.value]), style: itemStyle, children: /* @__PURE__ */ jsxs3(View4, { style: styles4.itemRow, children: [
|
|
393
402
|
option.icon && /* @__PURE__ */ jsx6(Icon2, { source: option.icon, size: MENU_ITEM_ICON_SIZE, color: rowColor }),
|
|
394
|
-
/* @__PURE__ */ jsx6(
|
|
403
|
+
/* @__PURE__ */ jsx6(Text3, { variant: "labelLarge", style: [styles4.itemLabelFlex, labelStyle], children: option.label })
|
|
395
404
|
] }) }, option.value);
|
|
396
405
|
}),
|
|
397
|
-
section.allClear && /* @__PURE__ */ jsx6(TouchableRipple3, { onPress: () => section.onChange(allSelected ? [] : section.options.map((o) => o.value)), style: styles4.item, children: /* @__PURE__ */ jsx6(
|
|
406
|
+
section.allClear && /* @__PURE__ */ jsx6(TouchableRipple3, { onPress: () => section.onChange(allSelected ? [] : section.options.map((o) => o.value)), style: styles4.item, children: /* @__PURE__ */ jsx6(Text3, { variant: "labelLarge", style: [styles4.allClearLabel, { fontFamily: labelFontFamily, color: accentColor }], children: allSelected ? allClearLabels.clear : allClearLabels.all }) })
|
|
398
407
|
] });
|
|
399
408
|
}
|
|
400
409
|
var styles4 = StyleSheet5.create({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tastic/hud",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Visual component kit for local-multiplayer React Native games: inline (non-modal) popovers, dropdowns, color pickers, gauges, ready buttons, and dialogs, built to stay scoped to one player's own zone on a shared screen",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react-native",
|
|
@@ -48,35 +48,34 @@
|
|
|
48
48
|
"scripts": {
|
|
49
49
|
"build": "tsup src/index.ts --format cjs,esm --dts --clean",
|
|
50
50
|
"build:watch": "tsup src/index.ts --format cjs,esm --dts --watch",
|
|
51
|
+
"ci": "npm run lint && npm test && npm run typecheck && npm run build",
|
|
51
52
|
"fix": "eslint --fix",
|
|
52
53
|
"lint": "eslint",
|
|
53
54
|
"prepublishOnly": "npm run build",
|
|
54
55
|
"release": "git push --follow-tags",
|
|
55
|
-
"release:major": "npm version major &&
|
|
56
|
-
"release:minor": "npm version minor &&
|
|
57
|
-
"release:patch": "npm version patch &&
|
|
56
|
+
"release:major": "npm version major && npm run release",
|
|
57
|
+
"release:minor": "npm version minor && npm run release",
|
|
58
|
+
"release:patch": "npm version patch && npm run release",
|
|
58
59
|
"test": "jest",
|
|
59
60
|
"test:watch": "jest --watchAll",
|
|
60
61
|
"typecheck": "tsc --noEmit",
|
|
61
|
-
"preversion": "npm run
|
|
62
|
+
"preversion": "npm run ci"
|
|
62
63
|
},
|
|
64
|
+
"prettier": "@infinitetoken/eslint-config/prettier",
|
|
63
65
|
"devDependencies": {
|
|
64
|
-
"@
|
|
65
|
-
"@
|
|
66
|
+
"@infinitetoken/eslint-config": "^0.1.4",
|
|
67
|
+
"@infinitetoken/tsconfig": "^0.1.1",
|
|
68
|
+
"@rific/auto-paper": "^0.10.1",
|
|
69
|
+
"@rific/feedback-press": "^0.10.7",
|
|
66
70
|
"@shopify/react-native-skia": "^2.6.2",
|
|
67
|
-
"@tastic/core": "^0.1.
|
|
71
|
+
"@tastic/core": "^0.1.3",
|
|
68
72
|
"@testing-library/dom": "^10.4.1",
|
|
69
73
|
"@testing-library/react": "^16.3.2",
|
|
70
74
|
"@types/jest": "^30.0.0",
|
|
71
75
|
"@types/react": "^19.0.0",
|
|
72
|
-
"@typescript-eslint/parser": "^8.59.3",
|
|
73
76
|
"eslint": "^9.39.4",
|
|
74
|
-
"eslint-config-prettier": "^10.1.8",
|
|
75
|
-
"eslint-plugin-package-json": "^1.0.0",
|
|
76
|
-
"eslint-plugin-prettier": "^5.5.5",
|
|
77
77
|
"eslint-plugin-react-hooks": "^7.1.1",
|
|
78
78
|
"eslint-plugin-react-native": "^5.0.0",
|
|
79
|
-
"eslint-plugin-simple-import-sort": "^13.0.0",
|
|
80
79
|
"jest": "^30.4.2",
|
|
81
80
|
"jest-environment-jsdom": "^30.4.1",
|
|
82
81
|
"prettier": "^3.8.3",
|
|
@@ -87,8 +86,7 @@
|
|
|
87
86
|
"react-native-reanimated": "^4.5.1",
|
|
88
87
|
"ts-jest": "^29.4.9",
|
|
89
88
|
"tsup": "^8.0.0",
|
|
90
|
-
"typescript": "^6.0.3"
|
|
91
|
-
"typescript-eslint": "^8.59.3"
|
|
89
|
+
"typescript": "^6.0.3"
|
|
92
90
|
},
|
|
93
91
|
"peerDependencies": {
|
|
94
92
|
"@rific/auto-paper": ">=0.9.0",
|
|
@@ -2,22 +2,30 @@ import { defaultColors, getContrastColor, SeedColor } from '@rific/auto-paper'
|
|
|
2
2
|
import { TouchableRipple } from '@rific/feedback-press'
|
|
3
3
|
import { clamp } from '@tastic/core'
|
|
4
4
|
import { ScrollView, StyleSheet, useWindowDimensions, View } from 'react-native'
|
|
5
|
-
import { Icon } from 'react-native-paper'
|
|
5
|
+
import { Icon, Text } from 'react-native-paper'
|
|
6
6
|
|
|
7
|
+
import { MONO_FONT } from './fonts'
|
|
7
8
|
import { PopoverBody } from './PopoverBody'
|
|
8
9
|
import { useAutoAlign } from './useAutoAlign'
|
|
9
10
|
import { PopoverHost } from './usePopoverHost'
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
// Presence check, not a full-string "is this exactly one emoji" validation — this only decides a
|
|
13
|
+
// font-size ratio for the trigger's own `tag`, so it doesn't need that precision, just "does this
|
|
14
|
+
// tag contain a pictograph at all."
|
|
15
|
+
const EMOJI_PATTERN = /\p{Extended_Pictographic}/u
|
|
16
|
+
|
|
17
|
+
const DEFAULT_SIZE = 48
|
|
12
18
|
const SWATCH_SIZE = 28
|
|
13
19
|
const SWATCHES_PADDING = 8
|
|
14
20
|
const SWATCHES_GAP = 6
|
|
15
21
|
const SWATCHES_BORDER_WIDTH = 2
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
|
|
20
|
-
|
|
22
|
+
// Clamped to exactly 4 or 5, not a wider 3-6 range — `defaultColors` (this component's own default
|
|
23
|
+
// `swatches`, and what every consumer actually ends up passing) is exactly 20 colors, so 4 or 5
|
|
24
|
+
// columns are the only counts that fill every row completely (4x5 or 5x4) instead of leaving a
|
|
25
|
+
// ragged, off-center last row. A caller passing a custom, non-20-length `swatches` list still gets
|
|
26
|
+
// a reasonable grid either way — just not guaranteed to be a full rectangle.
|
|
27
|
+
const MIN_COLUMNS = 4
|
|
28
|
+
const MAX_COLUMNS = 5
|
|
21
29
|
// How much screen width the grid leaves alone on either side when computing how many columns fit —
|
|
22
30
|
// this trigger can sit anywhere (including flush against a player's own screen edge in split-screen
|
|
23
31
|
// mode), so the budget is against the whole window, not this trigger's own local space.
|
|
@@ -48,20 +56,33 @@ interface Props {
|
|
|
48
56
|
// keeps it from overflowing the screen edge (see useAutoAlign).
|
|
49
57
|
align?: 'left' | 'right' | 'center'
|
|
50
58
|
// Trigger glyph — defaults to a plain palette. Pass a distinct icon per slot (e.g. a face for a
|
|
51
|
-
// human, a robot for CPU) to convey identity through the icon itself.
|
|
59
|
+
// human, a robot for CPU) to convey identity through the icon itself. Overridden by `tag` below
|
|
60
|
+
// when present.
|
|
52
61
|
icon?: string
|
|
62
|
+
// A short identity mark (an initial, an emoji, a couple of characters) shown as text on the
|
|
63
|
+
// trigger instead of `icon` — e.g. a saved player profile's own tag. Falls back to `icon` when
|
|
64
|
+
// empty/omitted, same as a seat with no such identity selected.
|
|
65
|
+
tag?: string
|
|
66
|
+
// Same convention as SectionedDropdown/ReadyButton's identical prop — defaults to this package's
|
|
67
|
+
// own MONO_FONT rather than inheriting whatever font the host app's theme happens to be using,
|
|
68
|
+
// so this trigger's tag reads consistently with the rest of this package's own chrome text.
|
|
69
|
+
labelFontFamily?: string
|
|
53
70
|
// Whether picking a swatch closes the popover. Defaults to true — a color pick is a single,
|
|
54
71
|
// complete choice the same way a single-select dropdown row is (see SectionedDropdown's identical
|
|
55
72
|
// prop), so press-away isn't the only way out, but doesn't have to be either.
|
|
56
73
|
autoDismiss?: boolean
|
|
57
74
|
// Overrides the auto-computed column count (see MIN_COLUMNS/MAX_COLUMNS/SCREEN_MARGIN above).
|
|
58
75
|
columns?: number
|
|
76
|
+
// Trigger circle's diameter — defaults to DEFAULT_SIZE. Overridable so a caller sharing a row
|
|
77
|
+
// with other fixed-size elements can line this trigger up with them instead of standing out at a
|
|
78
|
+
// mismatched size.
|
|
79
|
+
size?: number
|
|
59
80
|
}
|
|
60
81
|
|
|
61
82
|
// Renders inline rather than as a full-screen modal, scoped to its own panel — a modal color
|
|
62
83
|
// picker would block the whole screen for one player while another can't touch their own panel at
|
|
63
84
|
// the same time, which defeats the point of a split-screen lobby.
|
|
64
|
-
export function InlineColorPicker({ id, host, value, onChange, swatches = defaultColors, takenValue, allowSwapTaken, dark, align: alignOverride, icon = 'palette', autoDismiss = true, columns }: Props) {
|
|
85
|
+
export function InlineColorPicker({ id, host, value, onChange, swatches = defaultColors, takenValue, allowSwapTaken, dark, align: alignOverride, icon = 'palette', tag, labelFontFamily = MONO_FONT, autoDismiss = true, columns, size = DEFAULT_SIZE }: Props) {
|
|
65
86
|
const menuBg = dark ? '#000000' : '#FFFFFF'
|
|
66
87
|
const { width: windowWidth } = useWindowDimensions()
|
|
67
88
|
const autoColumns = clamp(Math.floor((windowWidth - 2 * SCREEN_MARGIN + SWATCHES_GAP) / (SWATCH_SIZE + SWATCHES_GAP)), MIN_COLUMNS, MAX_COLUMNS)
|
|
@@ -73,19 +94,34 @@ export function InlineColorPicker({ id, host, value, onChange, swatches = defaul
|
|
|
73
94
|
const open = host.openId === id
|
|
74
95
|
const { align: autoAlign, maxHeight, measured, triggerRef, verticalAlign } = useAutoAlign(open, swatchesWidth, swatchesHeight)
|
|
75
96
|
const align = alignOverride ?? autoAlign
|
|
97
|
+
const contrastColor = getContrastColor(value)
|
|
98
|
+
// An emoji glyph reads visually smaller than a bold letter at the same fontSize (the system emoji
|
|
99
|
+
// font leaves more of its own em-box empty), so a plain-text ratio that looks right for an initial
|
|
100
|
+
// still looks small for an emoji tag at the identical size — this bumps emoji up to the icon's own
|
|
101
|
+
// ratio instead, since a single emoji is closer in visual weight to an icon glyph than to text.
|
|
102
|
+
const tagFontSize = size * (tag && EMOJI_PATTERN.test(tag) ? 0.6 : 0.4)
|
|
76
103
|
|
|
77
104
|
return (
|
|
78
105
|
<View style={[styles.anchor, open && styles.anchorOpen]}>
|
|
79
|
-
<TouchableRipple onPress={() => host.toggle(id)} borderless style={[styles.trigger, { backgroundColor: value }]}>
|
|
106
|
+
<TouchableRipple onPress={() => host.toggle(id)} borderless style={[styles.trigger, { backgroundColor: value, borderRadius: size / 2, height: size, width: size }]}>
|
|
80
107
|
<View ref={triggerRef} collapsable={false} style={styles.triggerMeasure}>
|
|
81
|
-
|
|
108
|
+
{/* Sized as a fraction of the trigger's own diameter rather than a fixed pixel size, so
|
|
109
|
+
the glyph still reads as a deliberate part of the circle instead of shrinking toward its
|
|
110
|
+
center on a larger trigger. */}
|
|
111
|
+
{tag ? (
|
|
112
|
+
<Text style={[styles.tagLabel, { color: contrastColor, fontFamily: labelFontFamily, fontSize: tagFontSize }]} numberOfLines={1} adjustsFontSizeToFit>
|
|
113
|
+
{tag}
|
|
114
|
+
</Text>
|
|
115
|
+
) : (
|
|
116
|
+
<Icon source={icon} size={size * 0.6} color={contrastColor} />
|
|
117
|
+
)}
|
|
82
118
|
</View>
|
|
83
119
|
</TouchableRipple>
|
|
84
120
|
|
|
85
121
|
{/* Gated on `measured`, not just `open` — see useAutoAlign's own comment and
|
|
86
122
|
SectionedDropdown's identical fix: without it, the popover mounts for one frame at a stale or
|
|
87
123
|
guessed alignment and visibly jumps once this open's own measurement lands. */}
|
|
88
|
-
<PopoverBody visible={open && measured} align={align} verticalAlign={verticalAlign} caretColor={menuBg} caretBorderColor={value} caretBorderWidth={SWATCHES_BORDER_WIDTH} triggerSize={
|
|
124
|
+
<PopoverBody visible={open && measured} align={align} verticalAlign={verticalAlign} caretColor={menuBg} caretBorderColor={value} caretBorderWidth={SWATCHES_BORDER_WIDTH} triggerSize={size}>
|
|
89
125
|
{/* Border matches this trigger's own current color (not a neutral gray) — two triggers can
|
|
90
126
|
sit close together, so the popover needs a clear visual tie back to which one opened it,
|
|
91
127
|
not just its screen position. maxHeight (see useAutoAlign) only actually clamps on a screen
|
|
@@ -164,21 +200,27 @@ const styles = StyleSheet.create({
|
|
|
164
200
|
gap: SWATCHES_GAP,
|
|
165
201
|
padding: SWATCHES_PADDING
|
|
166
202
|
},
|
|
203
|
+
// borderRadius/height/width come from the `size` prop instead (see render-site override) — no
|
|
204
|
+
// fixed default here, since this style object is shared by every caller regardless of size.
|
|
167
205
|
trigger: {
|
|
168
206
|
alignItems: 'center',
|
|
169
207
|
alignSelf: 'flex-start',
|
|
170
|
-
|
|
171
|
-
height: SIZE,
|
|
172
|
-
justifyContent: 'center',
|
|
173
|
-
width: SIZE
|
|
208
|
+
justifyContent: 'center'
|
|
174
209
|
},
|
|
175
|
-
// Wraps just the icon, inside the TouchableRipple, purely so useAutoAlign has a plain View
|
|
176
|
-
// attach its measurement ref to — TouchableRipple itself doesn't forward a ref to a
|
|
177
|
-
// native node.
|
|
210
|
+
// Wraps just the icon/tag, inside the TouchableRipple, purely so useAutoAlign has a plain View
|
|
211
|
+
// to attach its measurement ref to — TouchableRipple itself doesn't forward a ref to a
|
|
212
|
+
// measurable native node. Padded horizontally so a multi-character tag doesn't butt right up
|
|
213
|
+
// against the trigger's own circular edge (an icon alone doesn't need it, but this style is
|
|
214
|
+
// shared by both).
|
|
178
215
|
triggerMeasure: {
|
|
179
216
|
alignItems: 'center',
|
|
180
217
|
height: '100%',
|
|
181
218
|
justifyContent: 'center',
|
|
219
|
+
paddingHorizontal: 4,
|
|
182
220
|
width: '100%'
|
|
221
|
+
},
|
|
222
|
+
tagLabel: {
|
|
223
|
+
fontWeight: '700',
|
|
224
|
+
letterSpacing: 0.5
|
|
183
225
|
}
|
|
184
226
|
})
|