@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 +2 -1
- package/src/callout.tsx +108 -0
- package/src/filter_pill.tsx +18 -5
- package/src/icon.tsx +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lotics/ui",
|
|
3
|
-
"version": "3.
|
|
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",
|
package/src/callout.tsx
ADDED
|
@@ -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
|
+
});
|
package/src/filter_pill.tsx
CHANGED
|
@@ -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
|
-
|
|
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={
|
|
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,
|