@tastic/hud 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -200,6 +200,25 @@ var styles = StyleSheet.create({
200
200
  }
201
201
  });
202
202
 
203
+ // src/CornerActionButtons.tsx
204
+ import { IconButton } from "@rific/feedback-press";
205
+ import { StyleSheet as StyleSheet2 } from "react-native";
206
+ import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
207
+ function CornerActionButtons({ onBack, onSettings, fg, insets }) {
208
+ return /* @__PURE__ */ jsxs2(Fragment2, { children: [
209
+ /* @__PURE__ */ jsx2(IconButton, { icon: "arrow-left", iconColor: fg, size: 24, style: [styles2.back, { top: 8 + insets.top, left: 8 + insets.left }], onPress: onBack, accessibilityLabel: "Back" }),
210
+ /* @__PURE__ */ jsx2(IconButton, { icon: "cog", iconColor: fg, size: 24, style: [styles2.topRight, { top: 8 + insets.top, right: 8 + insets.right }], onPress: onSettings, accessibilityLabel: "Settings" })
211
+ ] });
212
+ }
213
+ var styles2 = StyleSheet2.create({
214
+ back: {
215
+ position: "absolute"
216
+ },
217
+ topRight: {
218
+ position: "absolute"
219
+ }
220
+ });
221
+
203
222
  // src/fonts.ts
204
223
  import { Platform as Platform2 } from "react-native";
205
224
  var MONO_FONT = Platform2.select({ ios: "Menlo", android: "monospace", default: "monospace" });
@@ -208,34 +227,34 @@ var MONO_FONT = Platform2.select({ ios: "Menlo", android: "monospace", default:
208
227
  import { defaultColors, getContrastColor } from "@rific/auto-paper";
209
228
  import { TouchableRipple as TouchableRipple2 } from "@rific/feedback-press";
210
229
  import { clamp } from "@tastic/core";
211
- import { ScrollView as ScrollView2, StyleSheet as StyleSheet3, useWindowDimensions as useWindowDimensions2, View as View4 } from "react-native";
230
+ import { ScrollView as ScrollView2, StyleSheet as StyleSheet4, useWindowDimensions as useWindowDimensions2, View as View4 } from "react-native";
212
231
  import { Icon as Icon2, Text as Text2 } from "react-native-paper";
213
232
 
214
233
  // src/PopoverBody.tsx
215
- import { StyleSheet as StyleSheet2, View as View2 } from "react-native";
216
- import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
234
+ import { StyleSheet as StyleSheet3, View as View2 } from "react-native";
235
+ import { Fragment as Fragment3, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
217
236
  var CARET_SIZE = 7;
218
237
  var CARET_RING_OFFSET = 2;
219
238
  var CARET_DIP = 1;
220
239
  function PopoverBody({ visible, children, align = "center", verticalAlign = "below", caretColor, caretBorderColor, caretBorderWidth = 1, triggerSize = 0 }) {
221
240
  if (!visible) return null;
222
241
  const above = verticalAlign === "above";
223
- const alignStyle = align === "left" ? styles2.contentLeft : align === "right" ? styles2.contentRight : styles2.contentCenter;
224
- const verticalStyle = above ? styles2.contentAbove : styles2.contentBelow;
242
+ const alignStyle = align === "left" ? styles3.contentLeft : align === "right" ? styles3.contentRight : styles3.contentCenter;
243
+ const verticalStyle = above ? styles3.contentAbove : styles3.contentBelow;
225
244
  const ringSize = CARET_SIZE + caretBorderWidth;
226
245
  const caretOffset = (halfFootprint) => align === "right" ? { right: triggerSize / 2 - halfFootprint } : { left: triggerSize / 2 - halfFootprint };
227
246
  const caretFill = above ? "borderTopColor" : "borderBottomColor";
228
247
  const caretWidthProp = above ? "borderTopWidth" : "borderBottomWidth";
229
248
  const caretEdge = (size) => above ? { bottom: -size + CARET_DIP } : { top: -size + CARET_DIP };
230
- return /* @__PURE__ */ jsxs2(View2, { style: [styles2.content, alignStyle, verticalStyle], children: [
249
+ return /* @__PURE__ */ jsxs3(View2, { style: [styles3.content, alignStyle, verticalStyle], children: [
231
250
  children,
232
- caretColor && /* @__PURE__ */ jsxs2(Fragment2, { children: [
233
- /* @__PURE__ */ jsx2(View2, { style: [styles2.caret, caretOffset(ringSize), caretEdge(ringSize), { [caretFill]: caretBorderColor ?? caretColor, [caretWidthProp]: ringSize, borderLeftWidth: ringSize, borderRightWidth: ringSize }] }),
234
- /* @__PURE__ */ jsx2(View2, { style: [styles2.caret, caretOffset(CARET_SIZE), caretEdge(CARET_SIZE - CARET_RING_OFFSET), { [caretFill]: caretColor, [caretWidthProp]: CARET_SIZE, borderLeftWidth: CARET_SIZE, borderRightWidth: CARET_SIZE }] })
251
+ caretColor && /* @__PURE__ */ jsxs3(Fragment3, { children: [
252
+ /* @__PURE__ */ jsx3(View2, { style: [styles3.caret, caretOffset(ringSize), caretEdge(ringSize), { [caretFill]: caretBorderColor ?? caretColor, [caretWidthProp]: ringSize, borderLeftWidth: ringSize, borderRightWidth: ringSize }] }),
253
+ /* @__PURE__ */ jsx3(View2, { style: [styles3.caret, caretOffset(CARET_SIZE), caretEdge(CARET_SIZE - CARET_RING_OFFSET), { [caretFill]: caretColor, [caretWidthProp]: CARET_SIZE, borderLeftWidth: CARET_SIZE, borderRightWidth: CARET_SIZE }] })
235
254
  ] })
236
255
  ] });
237
256
  }
238
- var styles2 = StyleSheet2.create({
257
+ var styles3 = StyleSheet3.create({
239
258
  caret: {
240
259
  borderLeftColor: "transparent",
241
260
  borderRightColor: "transparent",
@@ -316,7 +335,7 @@ function useAutoAlign(open, contentWidth, contentHeight) {
316
335
  }
317
336
 
318
337
  // src/InlineColorPicker.tsx
319
- import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
338
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
320
339
  var EMOJI_PATTERN = /\p{Extended_Pictographic}/u;
321
340
  var DEFAULT_SIZE = 48;
322
341
  var SWATCH_SIZE = 28;
@@ -342,13 +361,13 @@ function InlineColorPicker({ id, host, value, onChange, swatches = defaultColors
342
361
  const align = alignOverride ?? autoAlign;
343
362
  const contrastColor = getContrastColor(value);
344
363
  const tagFontSize = size * (tag && EMOJI_PATTERN.test(tag) ? 0.6 : 0.4);
345
- return /* @__PURE__ */ jsxs3(View4, { style: [styles3.anchor, open && styles3.anchorOpen], children: [
346
- /* @__PURE__ */ jsx3(TouchableRipple2, { onPress: () => host.toggle(id), borderless: true, style: [styles3.trigger, { backgroundColor: value, borderRadius: size / 2, height: size, width: size }], children: /* @__PURE__ */ jsx3(View4, { ref: triggerRef, collapsable: false, style: styles3.triggerMeasure, children: tag ? /* @__PURE__ */ jsx3(Text2, { style: [styles3.tagLabel, { color: contrastColor, fontFamily: labelFontFamily, fontSize: tagFontSize }], numberOfLines: 1, adjustsFontSizeToFit: true, children: tag }) : /* @__PURE__ */ jsx3(Icon2, { source: icon, size: size * 0.6, color: contrastColor }) }) }),
347
- /* @__PURE__ */ jsx3(PopoverBody, { visible: open && measured, align, verticalAlign, caretColor: menuBg, caretBorderColor: value, caretBorderWidth: SWATCHES_BORDER_WIDTH, triggerSize: size, children: /* @__PURE__ */ jsx3(ScrollView2, { style: [styles3.swatches, { backgroundColor: menuBg, borderColor: value, width: swatchesWidth, maxHeight }], contentContainerStyle: styles3.swatchesContent, showsVerticalScrollIndicator: false, children: swatches.map((swatch) => {
364
+ return /* @__PURE__ */ jsxs4(View4, { style: [styles4.anchor, open && styles4.anchorOpen], children: [
365
+ /* @__PURE__ */ jsx4(TouchableRipple2, { onPress: () => host.toggle(id), borderless: true, style: [styles4.trigger, { backgroundColor: value, borderRadius: size / 2, height: size, width: size }], children: /* @__PURE__ */ jsx4(View4, { ref: triggerRef, collapsable: false, style: styles4.triggerMeasure, children: tag ? /* @__PURE__ */ jsx4(Text2, { style: [styles4.tagLabel, { color: contrastColor, fontFamily: labelFontFamily, fontSize: tagFontSize }], numberOfLines: 1, adjustsFontSizeToFit: true, children: tag }) : /* @__PURE__ */ jsx4(Icon2, { source: icon, size: size * 0.6, color: contrastColor }) }) }),
366
+ /* @__PURE__ */ jsx4(PopoverBody, { visible: open && measured, align, verticalAlign, caretColor: menuBg, caretBorderColor: value, caretBorderWidth: SWATCHES_BORDER_WIDTH, triggerSize: size, children: /* @__PURE__ */ jsx4(ScrollView2, { style: [styles4.swatches, { backgroundColor: menuBg, borderColor: value, width: swatchesWidth, maxHeight }], contentContainerStyle: styles4.swatchesContent, showsVerticalScrollIndicator: false, children: swatches.map((swatch) => {
348
367
  const selected = swatch.value.toLowerCase() === value.toLowerCase();
349
368
  const taken = !selected && !!takenValue && swatch.value.toLowerCase() === takenValue.toLowerCase();
350
369
  const swappable = taken && allowSwapTaken;
351
- return /* @__PURE__ */ jsx3(
370
+ return /* @__PURE__ */ jsx4(
352
371
  TouchableRipple2,
353
372
  {
354
373
  disabled: taken && !swappable,
@@ -357,15 +376,15 @@ function InlineColorPicker({ id, host, value, onChange, swatches = defaultColors
357
376
  if (autoDismiss) host.close();
358
377
  },
359
378
  borderless: true,
360
- style: [styles3.swatch, { backgroundColor: swatch.value }, selected && styles3.swatchSelected, taken && !swappable && styles3.swatchTaken],
361
- children: selected ? /* @__PURE__ */ jsx3(Icon2, { source: "check", size: 16, color: getContrastColor(swatch.value) }) : swappable ? /* @__PURE__ */ jsx3(Icon2, { source: "swap-horizontal", size: 16, color: getContrastColor(swatch.value) }) : taken ? /* @__PURE__ */ jsx3(Icon2, { source: "close", size: 16, color: getContrastColor(swatch.value) }) : /* @__PURE__ */ jsx3(View4, {})
379
+ style: [styles4.swatch, { backgroundColor: swatch.value }, selected && styles4.swatchSelected, taken && !swappable && styles4.swatchTaken],
380
+ children: selected ? /* @__PURE__ */ jsx4(Icon2, { source: "check", size: 16, color: getContrastColor(swatch.value) }) : swappable ? /* @__PURE__ */ jsx4(Icon2, { source: "swap-horizontal", size: 16, color: getContrastColor(swatch.value) }) : taken ? /* @__PURE__ */ jsx4(Icon2, { source: "close", size: 16, color: getContrastColor(swatch.value) }) : /* @__PURE__ */ jsx4(View4, {})
362
381
  },
363
382
  swatch.value
364
383
  );
365
384
  }) }) })
366
385
  ] });
367
386
  }
368
- var styles3 = StyleSheet3.create({
387
+ var styles4 = StyleSheet4.create({
369
388
  anchor: {
370
389
  position: "relative"
371
390
  },
@@ -436,31 +455,126 @@ var styles3 = StyleSheet3.create({
436
455
  }
437
456
  });
438
457
 
458
+ // src/LabeledDropdown.tsx
459
+ import { getContrastColor as getContrastColor2 } from "@rific/auto-paper";
460
+ import { TouchableRipple as TouchableRipple3 } from "@rific/feedback-press";
461
+ import { ScrollView as ScrollView3, StyleSheet as StyleSheet5, View as View5 } from "react-native";
462
+ import { Icon as Icon3, Text as Text3 } from "react-native-paper";
463
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
464
+ var TRIGGER_HEIGHT = 28;
465
+ var POPOVER_WIDTH = 180;
466
+ var ROW_HEIGHT = 36;
467
+ var LIST_PADDING = 8;
468
+ var LABELED_DROPDOWN_POPOVER_WIDTH = POPOVER_WIDTH;
469
+ function getLabeledDropdownContentHeight(optionCount) {
470
+ return LIST_PADDING * 2 + optionCount * ROW_HEIGHT;
471
+ }
472
+ function LabeledDropdown({ id, host, options, value, onChange, color, dark, align: forcedAlign, alignOverride }) {
473
+ const menuBg = dark ? "#000000" : "#FFFFFF";
474
+ const fg = dark ? "#FFFFFF" : "#000000";
475
+ const open = host.openId === id;
476
+ const selected = options.find((o) => o.value === value) ?? null;
477
+ const contentHeight = LIST_PADDING * 2 + options.length * ROW_HEIGHT;
478
+ const auto = useAutoAlign(open, POPOVER_WIDTH, contentHeight);
479
+ const placement = alignOverride ?? auto;
480
+ const { maxHeight, measured, triggerRef, verticalAlign } = placement;
481
+ const align = forcedAlign ?? placement.align;
482
+ const handleSelect = (option) => {
483
+ onChange(option.value);
484
+ host.close();
485
+ };
486
+ return /* @__PURE__ */ jsxs5(View5, { style: [styles5.anchor, open && styles5.anchorOpen], children: [
487
+ /* @__PURE__ */ jsx5(TouchableRipple3, { onPress: () => host.toggle(id), style: styles5.trigger, children: /* @__PURE__ */ jsxs5(View5, { ref: triggerRef, collapsable: false, style: styles5.triggerInner, children: [
488
+ /* @__PURE__ */ jsx5(Text3, { style: [styles5.triggerLabel, { color }], numberOfLines: 1, children: (selected?.label ?? "").toUpperCase() }),
489
+ /* @__PURE__ */ jsx5(Icon3, { source: "menu-down", size: 16, color })
490
+ ] }) }),
491
+ /* @__PURE__ */ jsx5(PopoverBody, { visible: open && measured, align, verticalAlign, caretColor: menuBg, caretBorderColor: color, caretBorderWidth: 2, triggerSize: TRIGGER_HEIGHT, children: /* @__PURE__ */ jsx5(ScrollView3, { style: [styles5.menu, { backgroundColor: menuBg, borderColor: color, width: POPOVER_WIDTH, maxHeight }], contentContainerStyle: styles5.menuContent, showsVerticalScrollIndicator: false, children: options.map((option) => {
492
+ const isSelected = option.value === value;
493
+ const onColor = getContrastColor2(color);
494
+ return /* @__PURE__ */ jsx5(TouchableRipple3, { onPress: () => handleSelect(option), style: [styles5.row, isSelected && { backgroundColor: color }], children: /* @__PURE__ */ jsxs5(View5, { style: styles5.rowInner, children: [
495
+ option.icon && /* @__PURE__ */ jsx5(Icon3, { source: option.icon, size: 18, color: isSelected ? onColor : fg }),
496
+ /* @__PURE__ */ jsx5(Text3, { style: [styles5.rowLabel, { color: isSelected ? onColor : fg }], numberOfLines: 1, children: option.label })
497
+ ] }) }, option.value);
498
+ }) }) })
499
+ ] });
500
+ }
501
+ var styles5 = StyleSheet5.create({
502
+ anchor: {
503
+ position: "relative"
504
+ },
505
+ // React Native Web gives every position:'relative' view its own stacking context, so the
506
+ // elevation has to live on the anchor itself — see PopoverBody's own stacking-context notes.
507
+ anchorOpen: {
508
+ zIndex: 100
509
+ },
510
+ menu: {
511
+ borderRadius: 12,
512
+ borderWidth: 2,
513
+ elevation: 8,
514
+ shadowColor: "#000000",
515
+ shadowOffset: { height: 2, width: 0 },
516
+ shadowOpacity: 0.3,
517
+ shadowRadius: 8
518
+ },
519
+ menuContent: {
520
+ padding: LIST_PADDING
521
+ },
522
+ row: {
523
+ borderRadius: 8,
524
+ height: ROW_HEIGHT
525
+ },
526
+ rowInner: {
527
+ alignItems: "center",
528
+ flexDirection: "row",
529
+ flex: 1,
530
+ gap: 8,
531
+ paddingHorizontal: 12
532
+ },
533
+ rowLabel: {
534
+ flex: 1,
535
+ fontSize: 14
536
+ },
537
+ trigger: {
538
+ alignSelf: "flex-start"
539
+ },
540
+ triggerInner: {
541
+ alignItems: "center",
542
+ flexDirection: "row",
543
+ gap: 2,
544
+ height: TRIGGER_HEIGHT
545
+ },
546
+ triggerLabel: {
547
+ fontSize: 13,
548
+ fontWeight: "700",
549
+ letterSpacing: 1
550
+ }
551
+ });
552
+
439
553
  // src/loadSkiaWeb.ts
440
554
  function loadSkiaWeb() {
441
555
  return Promise.resolve();
442
556
  }
443
557
 
444
558
  // src/PressAwayOverlay.tsx
445
- import { Pressable, StyleSheet as StyleSheet4 } from "react-native";
446
- import { jsx as jsx4 } from "react/jsx-runtime";
559
+ import { Pressable, StyleSheet as StyleSheet6 } from "react-native";
560
+ import { jsx as jsx6 } from "react/jsx-runtime";
447
561
  function PressAwayOverlay({ active, onPress, style }) {
448
562
  if (!active) return null;
449
- return /* @__PURE__ */ jsx4(Pressable, { accessibilityLabel: "Dismiss", accessibilityRole: "button", onPress, style: [StyleSheet4.absoluteFill, style] });
563
+ return /* @__PURE__ */ jsx6(Pressable, { accessibilityLabel: "Dismiss", accessibilityRole: "button", onPress, style: [StyleSheet6.absoluteFill, style] });
450
564
  }
451
565
 
452
566
  // src/ReadyButton.tsx
453
- import { TouchableRipple as TouchableRipple3 } from "@rific/feedback-press";
454
- import { StyleSheet as StyleSheet5 } from "react-native";
455
- import { Text as Text3 } from "react-native-paper";
456
- import { jsx as jsx5 } from "react/jsx-runtime";
567
+ import { TouchableRipple as TouchableRipple4 } from "@rific/feedback-press";
568
+ import { StyleSheet as StyleSheet7 } from "react-native";
569
+ import { Text as Text4 } from "react-native-paper";
570
+ import { jsx as jsx7 } from "react/jsx-runtime";
457
571
  function ReadyButton({ color, ready, onToggleReady, style, labelFontFamily = MONO_FONT }) {
458
- const readyButtonStyle = [styles4.readyButton, { borderColor: color }, ready && { backgroundColor: color }, style];
572
+ const readyButtonStyle = [styles6.readyButton, { borderColor: color }, ready && { backgroundColor: color }, style];
459
573
  const labelTextStyle = { fontFamily: labelFontFamily, fontWeight: "bold" };
460
574
  const labelColorStyle = { color: ready ? "#000000" : color };
461
- return /* @__PURE__ */ jsx5(TouchableRipple3, { onPress: onToggleReady, borderless: true, style: readyButtonStyle, children: /* @__PURE__ */ jsx5(Text3, { variant: "labelLarge", style: [labelTextStyle, labelColorStyle], children: ready ? "READY \u2713" : "READY?" }) });
575
+ return /* @__PURE__ */ jsx7(TouchableRipple4, { onPress: onToggleReady, borderless: true, style: readyButtonStyle, children: /* @__PURE__ */ jsx7(Text4, { variant: "labelLarge", style: [labelTextStyle, labelColorStyle], children: ready ? "READY \u2713" : "READY?" }) });
462
576
  }
463
- var styles4 = StyleSheet5.create({
577
+ var styles6 = StyleSheet7.create({
464
578
  readyButton: {
465
579
  alignItems: "center",
466
580
  borderRadius: 12,
@@ -474,20 +588,20 @@ var styles4 = StyleSheet5.create({
474
588
 
475
589
  // src/SectionedDropdown.tsx
476
590
  import { getBlendedColor, getColorRoles } from "@rific/auto-paper";
477
- import { IconButton, TouchableRipple as TouchableRipple4 } from "@rific/feedback-press";
478
- import { ScrollView as ScrollView3, StyleSheet as StyleSheet6, View as View5 } from "react-native";
479
- import { Icon as Icon3, Text as Text4 } from "react-native-paper";
591
+ import { IconButton as IconButton2, TouchableRipple as TouchableRipple5 } from "@rific/feedback-press";
592
+ import { ScrollView as ScrollView4, StyleSheet as StyleSheet8, View as View6 } from "react-native";
593
+ import { Icon as Icon4, Text as Text5 } from "react-native-paper";
480
594
 
481
595
  // src/TriggerGaugeHost.tsx
482
596
  import { lazy, Suspense } from "react";
483
- import { jsx as jsx6 } from "react/jsx-runtime";
597
+ import { jsx as jsx8 } from "react/jsx-runtime";
484
598
  var LazyTriggerGauge = lazy(() => loadSkiaWeb().then(() => import("./TriggerGauge-2OJOOF6I.mjs").then((m) => ({ default: m.TriggerGauge }))));
485
599
  function TriggerGaugeHost(props) {
486
- return /* @__PURE__ */ jsx6(Suspense, { fallback: null, children: /* @__PURE__ */ jsx6(LazyTriggerGauge, { ...props }) });
600
+ return /* @__PURE__ */ jsx8(Suspense, { fallback: null, children: /* @__PURE__ */ jsx8(LazyTriggerGauge, { ...props }) });
487
601
  }
488
602
 
489
603
  // src/SectionedDropdown.tsx
490
- import { Fragment as Fragment3, jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
604
+ import { Fragment as Fragment4, jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
491
605
  var MENU_BORDER_WIDTH = 1;
492
606
  var TRIGGER_SIZE = 44;
493
607
  var MENU_MIN_WIDTH = 200;
@@ -533,17 +647,17 @@ function SectionedDropdown({ id, host, icon, accessibilityLabel, sections, accen
533
647
  }
534
648
  gaugeOffset += section.options.length;
535
649
  }
536
- const anchorStyle = [styles5.anchor, open && styles5.anchorOpen];
650
+ const anchorStyle = [styles7.anchor, open && styles7.anchorOpen];
537
651
  const menuStyle = { backgroundColor: menuBg, borderColor: menuBorderColor, maxHeight };
538
652
  const dividerStyle = { backgroundColor: menuBorderColor };
539
- return /* @__PURE__ */ jsxs4(View5, { style: anchorStyle, children: [
540
- /* @__PURE__ */ jsxs4(View5, { ref: triggerRef, collapsable: false, style: styles5.triggerBox, children: [
541
- /* @__PURE__ */ jsx7(TriggerGaugeHost, { segments: gaugeSegments, litIndices: gaugeLitIndices, size: TRIGGER_SIZE, accentColor, mutedColor }),
542
- /* @__PURE__ */ jsx7(IconButton, { icon: triggerIcon, iconColor: triggerIconColor, size: triggerIconSize, accessibilityLabel, onPress: () => host.toggle(id) })
653
+ return /* @__PURE__ */ jsxs6(View6, { style: anchorStyle, children: [
654
+ /* @__PURE__ */ jsxs6(View6, { ref: triggerRef, collapsable: false, style: styles7.triggerBox, children: [
655
+ /* @__PURE__ */ jsx9(TriggerGaugeHost, { segments: gaugeSegments, litIndices: gaugeLitIndices, size: TRIGGER_SIZE, accentColor, mutedColor }),
656
+ /* @__PURE__ */ jsx9(IconButton2, { icon: triggerIcon, iconColor: triggerIconColor, size: triggerIconSize, accessibilityLabel, onPress: () => host.toggle(id) })
543
657
  ] }),
544
- /* @__PURE__ */ jsx7(PopoverBody, { visible: open && measured, align, verticalAlign, caretColor: menuBg, caretBorderColor: menuBorderColor, caretBorderWidth: MENU_BORDER_WIDTH, triggerSize: TRIGGER_SIZE, children: /* @__PURE__ */ jsx7(ScrollView3, { style: [styles5.menu, menuStyle], contentContainerStyle: styles5.menuContent, showsVerticalScrollIndicator: false, children: sections.map((section, sectionIndex) => /* @__PURE__ */ jsxs4(View5, { children: [
545
- sectionIndex > 0 && /* @__PURE__ */ jsx7(View5, { style: [styles5.divider, dividerStyle] }),
546
- section.kind === "single" ? /* @__PURE__ */ jsx7(
658
+ /* @__PURE__ */ jsx9(PopoverBody, { visible: open && measured, align, verticalAlign, caretColor: menuBg, caretBorderColor: menuBorderColor, caretBorderWidth: MENU_BORDER_WIDTH, triggerSize: TRIGGER_SIZE, children: /* @__PURE__ */ jsx9(ScrollView4, { style: [styles7.menu, menuStyle], contentContainerStyle: styles7.menuContent, showsVerticalScrollIndicator: false, children: sections.map((section, sectionIndex) => /* @__PURE__ */ jsxs6(View6, { children: [
659
+ sectionIndex > 0 && /* @__PURE__ */ jsx9(View6, { style: [styles7.divider, dividerStyle] }),
660
+ section.kind === "single" ? /* @__PURE__ */ jsx9(
547
661
  SingleSection,
548
662
  {
549
663
  section,
@@ -556,7 +670,7 @@ function SectionedDropdown({ id, host, icon, accessibilityLabel, sections, accen
556
670
  if (autoDismiss) host.close();
557
671
  }
558
672
  }
559
- ) : /* @__PURE__ */ jsx7(MultiSection, { section, accentColor, onAccentColor: resolvedOnAccentColor, mutedColor, labelFontFamily, allClearLabels })
673
+ ) : /* @__PURE__ */ jsx9(MultiSection, { section, accentColor, onAccentColor: resolvedOnAccentColor, mutedColor, labelFontFamily, allClearLabels })
560
674
  ] }, section.id)) }) })
561
675
  ] });
562
676
  }
@@ -568,17 +682,17 @@ function SingleSection({
568
682
  labelFontFamily,
569
683
  onSelect
570
684
  }) {
571
- return /* @__PURE__ */ jsx7(Fragment3, { children: section.options.map((option) => {
685
+ return /* @__PURE__ */ jsx9(Fragment4, { children: section.options.map((option) => {
572
686
  const selected = option.value === section.value;
573
687
  const rowColor = selected ? onAccentColor : mutedColor;
574
- const itemStyle = [styles5.item, selected && { backgroundColor: accentColor }];
688
+ const itemStyle = [styles7.item, selected && { backgroundColor: accentColor }];
575
689
  const labelStyle = { fontFamily: labelFontFamily, color: rowColor, fontWeight: selected ? "bold" : "normal" };
576
690
  const descriptionStyle = { fontFamily: labelFontFamily, color: rowColor };
577
- return /* @__PURE__ */ jsx7(TouchableRipple4, { onPress: () => onSelect(option.value), style: itemStyle, children: /* @__PURE__ */ jsxs4(View5, { style: styles5.itemRow, children: [
578
- option.icon && /* @__PURE__ */ jsx7(Icon3, { source: option.icon, size: MENU_ITEM_ICON_SIZE, color: rowColor }),
579
- /* @__PURE__ */ jsxs4(View5, { style: styles5.itemLabelColumn, children: [
580
- /* @__PURE__ */ jsx7(Text4, { variant: "labelLarge", style: labelStyle, children: option.label }),
581
- option.description && /* @__PURE__ */ jsx7(Text4, { variant: "labelSmall", style: descriptionStyle, children: option.description })
691
+ return /* @__PURE__ */ jsx9(TouchableRipple5, { onPress: () => onSelect(option.value), style: itemStyle, children: /* @__PURE__ */ jsxs6(View6, { style: styles7.itemRow, children: [
692
+ option.icon && /* @__PURE__ */ jsx9(Icon4, { source: option.icon, size: MENU_ITEM_ICON_SIZE, color: rowColor }),
693
+ /* @__PURE__ */ jsxs6(View6, { style: styles7.itemLabelColumn, children: [
694
+ /* @__PURE__ */ jsx9(Text5, { variant: "labelLarge", style: labelStyle, children: option.label }),
695
+ option.description && /* @__PURE__ */ jsx9(Text5, { variant: "labelSmall", style: descriptionStyle, children: option.description })
582
696
  ] })
583
697
  ] }) }, option.value);
584
698
  }) });
@@ -592,23 +706,23 @@ function MultiSection({
592
706
  allClearLabels
593
707
  }) {
594
708
  const allSelected = section.value.length === section.options.length;
595
- return /* @__PURE__ */ jsxs4(Fragment3, { children: [
709
+ return /* @__PURE__ */ jsxs6(Fragment4, { children: [
596
710
  section.options.map((option, i) => {
597
711
  const selected = section.value.includes(option.value);
598
712
  const rowColor = selected ? onAccentColor : mutedColor;
599
713
  const prevSelected = selected && i > 0 && section.value.includes(section.options[i - 1].value);
600
714
  const nextSelected = selected && i < section.options.length - 1 && section.value.includes(section.options[i + 1].value);
601
- const itemStyle = [styles5.item, selected && { backgroundColor: accentColor }, prevSelected && { borderTopLeftRadius: 0, borderTopRightRadius: 0 }, nextSelected && { borderBottomLeftRadius: 0, borderBottomRightRadius: 0 }];
715
+ const itemStyle = [styles7.item, selected && { backgroundColor: accentColor }, prevSelected && { borderTopLeftRadius: 0, borderTopRightRadius: 0 }, nextSelected && { borderBottomLeftRadius: 0, borderBottomRightRadius: 0 }];
602
716
  const labelStyle = { fontFamily: labelFontFamily, color: rowColor, fontWeight: selected ? "bold" : "normal" };
603
- return /* @__PURE__ */ jsx7(TouchableRipple4, { onPress: () => section.onChange(selected ? section.value.filter((v) => v !== option.value) : [...section.value, option.value]), style: itemStyle, children: /* @__PURE__ */ jsxs4(View5, { style: styles5.itemRow, children: [
604
- option.icon && /* @__PURE__ */ jsx7(Icon3, { source: option.icon, size: MENU_ITEM_ICON_SIZE, color: rowColor }),
605
- /* @__PURE__ */ jsx7(Text4, { variant: "labelLarge", style: [styles5.itemLabelFlex, labelStyle], children: option.label })
717
+ return /* @__PURE__ */ jsx9(TouchableRipple5, { onPress: () => section.onChange(selected ? section.value.filter((v) => v !== option.value) : [...section.value, option.value]), style: itemStyle, children: /* @__PURE__ */ jsxs6(View6, { style: styles7.itemRow, children: [
718
+ option.icon && /* @__PURE__ */ jsx9(Icon4, { source: option.icon, size: MENU_ITEM_ICON_SIZE, color: rowColor }),
719
+ /* @__PURE__ */ jsx9(Text5, { variant: "labelLarge", style: [styles7.itemLabelFlex, labelStyle], children: option.label })
606
720
  ] }) }, option.value);
607
721
  }),
608
- section.allClear && /* @__PURE__ */ jsx7(TouchableRipple4, { onPress: () => section.onChange(allSelected ? [] : section.options.map((o) => o.value)), style: styles5.item, children: /* @__PURE__ */ jsx7(Text4, { variant: "labelLarge", style: [styles5.allClearLabel, { fontFamily: labelFontFamily, color: accentColor }], children: allSelected ? allClearLabels.clear : allClearLabels.all }) })
722
+ section.allClear && /* @__PURE__ */ jsx9(TouchableRipple5, { onPress: () => section.onChange(allSelected ? [] : section.options.map((o) => o.value)), style: styles7.item, children: /* @__PURE__ */ jsx9(Text5, { variant: "labelLarge", style: [styles7.allClearLabel, { fontFamily: labelFontFamily, color: accentColor }], children: allSelected ? allClearLabels.clear : allClearLabels.all }) })
609
723
  ] });
610
724
  }
611
- var styles5 = StyleSheet6.create({
725
+ var styles7 = StyleSheet8.create({
612
726
  allClearLabel: {
613
727
  fontWeight: "bold",
614
728
  textAlign: "center"
@@ -625,7 +739,7 @@ var styles5 = StyleSheet6.create({
625
739
  zIndex: 100
626
740
  },
627
741
  divider: {
628
- height: StyleSheet6.hairlineWidth,
742
+ height: StyleSheet8.hairlineWidth,
629
743
  marginVertical: 4
630
744
  },
631
745
  item: {
@@ -672,6 +786,36 @@ var styles5 = StyleSheet6.create({
672
786
  }
673
787
  });
674
788
 
789
+ // src/SharedActionBand.tsx
790
+ import { IconButton as IconButton3 } from "@rific/feedback-press";
791
+ import { StyleSheet as StyleSheet9, View as View7 } from "react-native";
792
+ import { jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
793
+ function SharedActionBand({ onBack, onSettings, onRandomize, onReset, fg, popoverOpen, children }) {
794
+ return /* @__PURE__ */ jsxs7(View7, { style: [styles8.column, popoverOpen && styles8.columnOpen], children: [
795
+ /* @__PURE__ */ jsxs7(View7, { style: styles8.actionsRow, children: [
796
+ /* @__PURE__ */ jsx10(IconButton3, { icon: "arrow-left", iconColor: fg, size: 20, onPress: onBack, accessibilityLabel: "Back" }),
797
+ onRandomize && /* @__PURE__ */ jsx10(IconButton3, { icon: "dice-multiple", iconColor: fg, size: 18, onPress: onRandomize, accessibilityLabel: "Randomize match settings" }),
798
+ onReset && /* @__PURE__ */ jsx10(IconButton3, { icon: "restore", iconColor: fg, size: 18, onPress: onReset, accessibilityLabel: "Reset match settings to defaults" }),
799
+ /* @__PURE__ */ jsx10(IconButton3, { icon: "cog", iconColor: fg, size: 20, onPress: onSettings, accessibilityLabel: "Settings" })
800
+ ] }),
801
+ children
802
+ ] });
803
+ }
804
+ var styles8 = StyleSheet9.create({
805
+ actionsRow: {
806
+ alignItems: "center",
807
+ flexDirection: "row",
808
+ gap: 16
809
+ },
810
+ column: {
811
+ alignItems: "center",
812
+ gap: 8
813
+ },
814
+ columnOpen: {
815
+ zIndex: 100
816
+ }
817
+ });
818
+
675
819
  // src/usePopoverHost.ts
676
820
  import { useCallback as useCallback2, useState as useState3 } from "react";
677
821
  function usePopoverHost() {
@@ -684,13 +828,18 @@ function usePopoverHost() {
684
828
  }
685
829
  export {
686
830
  BaseSettingsDialog,
831
+ CornerActionButtons,
687
832
  InlineColorPicker,
833
+ LABELED_DROPDOWN_POPOVER_WIDTH,
834
+ LabeledDropdown,
688
835
  MONO_FONT,
689
836
  PopoverBody,
690
837
  PressAwayOverlay,
691
838
  ReadyButton,
692
839
  SectionedDropdown,
840
+ SharedActionBand,
693
841
  TriggerGaugeHost,
842
+ getLabeledDropdownContentHeight,
694
843
  loadSkiaWeb,
695
844
  useAutoAlign,
696
845
  usePopoverHost
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tastic/hud",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
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",
@@ -0,0 +1,39 @@
1
+ import { IconButton } from '@rific/feedback-press'
2
+ import { StyleSheet } from 'react-native'
3
+
4
+ interface Props {
5
+ onBack: () => void
6
+ onSettings: () => void
7
+ // Full-contrast foreground — same convention as every other component keying off a `dark` prop
8
+ // (BaseSettingsDialog, SectionedDropdown, etc.), just resolved by the caller since this component
9
+ // never reads theme state itself.
10
+ fg: string
11
+ // Safe-area insets already remapped onto the screen's real visual edges (see
12
+ // @tastic/split-screen's rotateInsets) — this component just adds the fixed 8px margin on top,
13
+ // it doesn't know anything about physical device orientation itself.
14
+ insets: { top: number; left: number; right: number }
15
+ }
16
+
17
+ // Back (top-left) + Settings (top-right) icon buttons, fixed at the screen's rotated corners — the
18
+ // default reachability answer for a two-player screen: correct any time both players share one
19
+ // screen but only one of them (or neither, in vs-CPU) is on the far side of a rotated zone. Not the
20
+ // right answer for a screen where the top corners themselves fall *inside* one player's own rotated
21
+ // zone (two-player face-to-face) — see SharedActionBand for that case instead, which callers select
22
+ // between based on their own game-mode/orientation state (this component has no opinion on which).
23
+ export function CornerActionButtons({ onBack, onSettings, fg, insets }: Props) {
24
+ return (
25
+ <>
26
+ <IconButton icon='arrow-left' iconColor={fg} size={24} style={[styles.back, { top: 8 + insets.top, left: 8 + insets.left }]} onPress={onBack} accessibilityLabel='Back' />
27
+ <IconButton icon='cog' iconColor={fg} size={24} style={[styles.topRight, { top: 8 + insets.top, right: 8 + insets.right }]} onPress={onSettings} accessibilityLabel='Settings' />
28
+ </>
29
+ )
30
+ }
31
+
32
+ const styles = StyleSheet.create({
33
+ back: {
34
+ position: 'absolute'
35
+ },
36
+ topRight: {
37
+ position: 'absolute'
38
+ }
39
+ })