@lotics/ui 3.4.0 → 3.5.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/ui",
3
- "version": "3.4.0",
3
+ "version": "3.5.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./tokens": "./src/tokens.ts",
@@ -81,6 +81,7 @@
81
81
  "./action_menu": "./src/action_menu.tsx",
82
82
  "./card_select_item": "./src/card_select_item.tsx",
83
83
  "./badge": "./src/badge.tsx",
84
+ "./callout": "./src/callout.tsx",
84
85
  "./divider": "./src/divider.tsx",
85
86
  "./spacer": "./src/spacer.tsx",
86
87
  "./index.css": "./src/index.css",
@@ -0,0 +1,75 @@
1
+ import { View, StyleSheet } from "react-native";
2
+ import { Text } from "./text";
3
+ import { Icon, type IconName } from "./icon";
4
+ import { colors } from "./colors";
5
+
6
+ export type CalloutTone = "info" | "success" | "warning" | "error" | "neutral";
7
+
8
+ interface CalloutProps {
9
+ /** Sets the accent (icon + tint + border) and the default icon. Default "info". */
10
+ tone?: CalloutTone;
11
+ /** Optional bold heading above the message. */
12
+ title?: string;
13
+ /** The message body — kept in the default text color for legibility on the tint. */
14
+ message: string;
15
+ /** Override the per-tone default icon (any kit icon), or `null` to drop it. */
16
+ icon?: IconName | null;
17
+ }
18
+
19
+ interface ToneStyle {
20
+ bg: string;
21
+ border: string;
22
+ icon: string;
23
+ glyph: IconName;
24
+ }
25
+
26
+ const TONES: Record<CalloutTone, ToneStyle> = {
27
+ info: { bg: colors.blue[50], border: colors.blue[200], icon: colors.blue[600], glyph: "info" },
28
+ success: { bg: colors.emerald[50], border: colors.emerald[200], icon: colors.emerald[600], glyph: "circle-check" },
29
+ warning: { bg: colors.amber[50], border: colors.amber[200], icon: colors.amber[600], glyph: "triangle-alert" },
30
+ error: { bg: colors.red[50], border: colors.red[200], icon: colors.red[600], glyph: "circle-alert" },
31
+ neutral: { bg: colors.zinc[50], border: colors.zinc[200], icon: colors.zinc[500], glyph: "info" },
32
+ };
33
+
34
+ /**
35
+ * An inline callout — a tinted, bordered box carrying a short status message
36
+ * (info / success / warning / error / neutral). Use it inline in a flow: form
37
+ * feedback, a heads-up, an actionable empty state. For a blocking, dismissible
38
+ * prompt use `Alert`; for a one-word status use `Badge`. The tone is carried by
39
+ * the icon + tint + border (never color alone — the icon and text stay legible),
40
+ * so it reads on a glance and meets contrast on the light tint.
41
+ */
42
+ export function Callout({ tone = "info", title, message, icon }: CalloutProps) {
43
+ const t = TONES[tone];
44
+ const glyph = icon === null ? null : (icon ?? t.glyph);
45
+ return (
46
+ <View
47
+ accessibilityRole={tone === "error" || tone === "warning" ? "alert" : undefined}
48
+ style={[styles.container, { backgroundColor: t.bg, borderColor: t.border }]}
49
+ >
50
+ {glyph ? <Icon name={glyph} size={18} color={t.icon} /> : null}
51
+ <View style={styles.body}>
52
+ {title ? (
53
+ <Text size="sm" weight="semibold">
54
+ {title}
55
+ </Text>
56
+ ) : null}
57
+ <Text size="sm">{message}</Text>
58
+ </View>
59
+ </View>
60
+ );
61
+ }
62
+
63
+ export type { CalloutProps };
64
+
65
+ const styles = StyleSheet.create({
66
+ container: {
67
+ flexDirection: "row",
68
+ alignItems: "flex-start",
69
+ gap: 10,
70
+ padding: 12,
71
+ borderRadius: 10,
72
+ borderWidth: 1,
73
+ },
74
+ body: { flex: 1, gap: 2, paddingTop: 1 },
75
+ });
@@ -1,4 +1,4 @@
1
- import { type ReactNode } from "react";
1
+ import { type ReactNode, useState } from "react";
2
2
  import { StyleSheet } from "react-native";
3
3
  import { Text } from "./text";
4
4
  import { Icon } from "./icon";
@@ -23,8 +23,11 @@ export interface FilterPillProps {
23
23
  clearLabel?: string;
24
24
  /** The editor revealed on press — a premium primitive (`RangeSlider`,
25
25
  * `Counter`, `PickerMenu` multi) or any composed control. `FilterPill` owns
26
- * the pill + popover chrome; the consumer owns the value and applies it. */
27
- children: ReactNode;
26
+ * the pill + popover chrome; the consumer owns the value and applies it.
27
+ * Pass a render function to receive `close` — the one editor that closes on
28
+ * action is single-select (`<PickerMenu>` → close on pick); range/counter/
29
+ * multi stay open and dismiss on outside-press, so they pass a plain node. */
30
+ children: ReactNode | ((api: { close: () => void }) => ReactNode);
28
31
  side?: PopoverSide;
29
32
  align?: PopoverAlign;
30
33
  /** Optional controlled popover state — for an editor that closes on Save. */
@@ -71,8 +74,18 @@ export function FilterPill(props: FilterPillProps) {
71
74
  // custom footer — a valued, non-clearable pill ("Target: 20") keeps its chevron.
72
75
  const showClear = active && !!onClear && !footer;
73
76
 
77
+ // Own the open state so the editor can close itself via the render-prop
78
+ // `close`, while still honoring a controlled `open`/`onOpenChange` from the
79
+ // parent (the Save-footer VALUE-pill case).
80
+ const [internalOpen, setInternalOpen] = useState(false);
81
+ const isOpen = open ?? internalOpen;
82
+ const setOpen = (next: boolean) => {
83
+ if (open === undefined) setInternalOpen(next);
84
+ onOpenChange?.(next);
85
+ };
86
+
74
87
  return (
75
- <Popover side={side} align={align} open={open} onOpenChange={onOpenChange}>
88
+ <Popover side={side} align={align} open={isOpen} onOpenChange={setOpen}>
76
89
  <PopoverTrigger>
77
90
  <PillButton onDismiss={showClear ? onClear : undefined} dismissTooltip={clearLabel}>
78
91
  <Text size="sm" weight="medium" color={active ? "zinc-900" : "zinc-700"} numberOfLines={1}>
@@ -82,7 +95,7 @@ export function FilterPill(props: FilterPillProps) {
82
95
  </PillButton>
83
96
  </PopoverTrigger>
84
97
  <PopoverContent style={styles.body} disableBodyScroll>
85
- {children}
98
+ {typeof children === "function" ? children({ close: () => setOpen(false) }) : children}
86
99
  {footer ? (
87
100
  <PopoverFooter>{footer}</PopoverFooter>
88
101
  ) : showClear ? (
package/src/icon.tsx CHANGED
@@ -51,6 +51,7 @@ import ChevronsDownUp from "lucide-react-native/dist/esm/icons/chevrons-down-up"
51
51
  import ChevronsUpDown from "lucide-react-native/dist/esm/icons/chevrons-up-down";
52
52
  import CircleAlert from "lucide-react-native/dist/esm/icons/circle-alert";
53
53
  import CircleCheck from "lucide-react-native/dist/esm/icons/circle-check";
54
+ import TriangleAlert from "lucide-react-native/dist/esm/icons/triangle-alert";
54
55
  import Clock from "lucide-react-native/dist/esm/icons/clock";
55
56
  import Code from "lucide-react-native/dist/esm/icons/code";
56
57
  import CodeXml from "lucide-react-native/dist/esm/icons/code-xml";
@@ -242,6 +243,7 @@ const iconComponents = {
242
243
  "chevrons-up-down": ChevronsUpDown,
243
244
  "circle-alert": CircleAlert,
244
245
  "circle-check": CircleCheck,
246
+ "triangle-alert": TriangleAlert,
245
247
  clock: Clock,
246
248
  code: Code,
247
249
  "code-xml": CodeXml,