@lotics/ui 3.4.0 → 3.6.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.6.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,108 @@
1
+ import { type ReactNode } from "react";
2
+ import { View, StyleSheet } from "react-native";
3
+ import { Text } from "./text";
4
+ import { Icon, type IconName } from "./icon";
5
+ import { colors } from "./colors";
6
+
7
+ export type CalloutTone = "info" | "success" | "warning" | "error" | "neutral";
8
+
9
+ interface CalloutProps {
10
+ /** Sets the accent (icon + tint + border) and the default icon. Default "info". */
11
+ tone?: CalloutTone;
12
+ /** Override the per-tone default icon (any kit icon), or `null` to drop it. */
13
+ icon?: IconName | null;
14
+ /** Compose the body with `CalloutTitle`, `CalloutText`, and `CalloutActions`. */
15
+ children: ReactNode;
16
+ /** Test id on the container. */
17
+ testID?: string;
18
+ }
19
+
20
+ interface ToneStyle {
21
+ bg: string;
22
+ border: string;
23
+ icon: string;
24
+ glyph: IconName;
25
+ }
26
+
27
+ // Tint surface + SOFT hairline border + deep icon — matches the kit's toned-surface
28
+ // convention (Badge: bg-50 / border-100 / deep accent). A heavier 200 border + 600
29
+ // icon reads as a generic "library alert"; the soft 100 border lets the tint settle
30
+ // into a calm panel with the deep-700 icon carrying the tone. (Neutral keeps a 200
31
+ // border — zinc-100 is invisible on zinc-50.)
32
+ const TONES: Record<CalloutTone, ToneStyle> = {
33
+ info: { bg: colors.blue[50], border: colors.blue[100], icon: colors.blue[700], glyph: "info" },
34
+ success: { bg: colors.emerald[50], border: colors.emerald[100], icon: colors.emerald[700], glyph: "circle-check" },
35
+ warning: { bg: colors.amber[50], border: colors.amber[100], icon: colors.amber[700], glyph: "triangle-alert" },
36
+ error: { bg: colors.red[50], border: colors.red[100], icon: colors.red[700], glyph: "circle-alert" },
37
+ neutral: { bg: colors.zinc[50], border: colors.zinc[200], icon: colors.zinc[500], glyph: "info" },
38
+ };
39
+
40
+ /**
41
+ * An inline callout — a tinted, bordered box carrying a short status message
42
+ * (info / success / warning / error / neutral). Use it inline in a flow: form
43
+ * feedback, a heads-up, an actionable empty state. For a blocking, dismissible
44
+ * prompt use `Alert`; for a one-word status use `Badge`. The tone is carried by
45
+ * the icon + tint + border (never color alone — the icon and text stay legible),
46
+ * so it reads on a glance and meets contrast on the light tint.
47
+ *
48
+ * COMPOUND — compose the body from the sub-components (like `Card`); this lets a
49
+ * callout carry rich content and inline actions, not just a string message:
50
+ *
51
+ * ```tsx
52
+ * <Callout tone="warning">
53
+ * <CalloutTitle>Phí chưa nhập</CalloutTitle>
54
+ * <CalloutText>Nhập trước khi xuất chứng từ, hoặc đặt = 0.</CalloutText>
55
+ * <CalloutActions>
56
+ * <Button title="Đặt = 0" onPress={fill} />
57
+ * </CalloutActions>
58
+ * </Callout>
59
+ * ```
60
+ */
61
+ export function Callout({ tone = "info", icon, children, testID }: CalloutProps) {
62
+ const t = TONES[tone];
63
+ const glyph = icon === null ? null : (icon ?? t.glyph);
64
+ return (
65
+ <View
66
+ accessibilityRole={tone === "error" || tone === "warning" ? "alert" : undefined}
67
+ style={[styles.container, { backgroundColor: t.bg, borderColor: t.border }]}
68
+ testID={testID}
69
+ >
70
+ {glyph ? <Icon name={glyph} size={18} color={t.icon} /> : null}
71
+ <View style={styles.body}>{children}</View>
72
+ </View>
73
+ );
74
+ }
75
+
76
+ /** Bold heading at the top of a `Callout`. */
77
+ export function CalloutTitle({ children }: { children: ReactNode }) {
78
+ return (
79
+ <Text size="sm" weight="semibold">
80
+ {children}
81
+ </Text>
82
+ );
83
+ }
84
+
85
+ /** Message body of a `Callout` — default text color for legibility on the tint. */
86
+ export function CalloutText({ children }: { children: ReactNode }) {
87
+ return <Text size="sm">{children}</Text>;
88
+ }
89
+
90
+ /** A row of actions (e.g. buttons) at the foot of a `Callout`. */
91
+ export function CalloutActions({ children }: { children: ReactNode }) {
92
+ return <View style={styles.actions}>{children}</View>;
93
+ }
94
+
95
+ export type { CalloutProps };
96
+
97
+ const styles = StyleSheet.create({
98
+ container: {
99
+ flexDirection: "row",
100
+ alignItems: "flex-start",
101
+ gap: 10,
102
+ padding: 12,
103
+ borderRadius: 10,
104
+ borderWidth: 1,
105
+ },
106
+ body: { flex: 1, gap: 2, paddingTop: 1 },
107
+ actions: { flexDirection: "row", flexWrap: "wrap", gap: 8, marginTop: 6 },
108
+ });
@@ -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,