@lotics/ui 9.0.0 → 11.0.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/AGENTS.md +41 -10
- package/examples/tpl_allocate.tsx +2 -2
- package/examples/tpl_attendance.tsx +1 -1
- package/examples/tpl_calendar.tsx +1 -1
- package/examples/tpl_dashboard.tsx +1 -1
- package/examples/tpl_documents.tsx +1 -1
- package/examples/tpl_item_list.tsx +1 -1
- package/examples/tpl_lookup.tsx +1 -1
- package/examples/tpl_pick.tsx +3 -3
- package/examples/tpl_pivot.tsx +1 -1
- package/examples/tpl_record.tsx +24 -30
- package/examples/tpl_report.tsx +1 -1
- package/examples/tpl_rollup.tsx +1 -1
- package/examples/tpl_shifts.tsx +1 -1
- package/examples/tpl_statements.tsx +1 -1
- package/examples/tpl_stock.tsx +1 -1
- package/examples/tpl_task_board.tsx +1 -1
- package/examples/tpl_tasks.tsx +1 -1
- package/examples/tpl_tower.tsx +1 -1
- package/package.json +2 -7
- package/src/button.tsx +5 -1
- package/src/button_colors.ts +4 -1
- package/src/locale.tsx +4 -0
- package/src/page_content.tsx +1 -1
- package/src/page_header.tsx +2 -4
- package/src/popover_nav.tsx +40 -0
- package/src/record_summary.tsx +3 -1
- package/src/ring_gauge.tsx +1 -1
- package/src/section_heading.tsx +70 -9
- package/src/section_stack.tsx +67 -0
- package/src/text.css +13 -2
- package/src/text.tsx +8 -2
- package/src/text_utils.ts +6 -1
- package/src/trend_footer.tsx +3 -1
- package/src/use_screen_size.ts +1 -1
- package/src/animation_horizontal_slide.tsx +0 -75
- package/src/form_time_picker.tsx +0 -22
- package/src/highlighted_text.tsx +0 -92
- package/src/menu_title.tsx +0 -15
- package/src/pager_view.tsx +0 -167
- package/src/popover_header.tsx +0 -38
package/src/locale.tsx
CHANGED
|
@@ -45,6 +45,8 @@ export interface LoticsLocale {
|
|
|
45
45
|
sectionHeading: { info: string };
|
|
46
46
|
/** `Chip`: the ✕ default name when no `dismissTooltip` is given. */
|
|
47
47
|
chip: { remove: string };
|
|
48
|
+
/** `TrendFooter`: the direction words of the trend sentence. */
|
|
49
|
+
trendFooter: { up: string; down: string };
|
|
48
50
|
/** `SuggestionChip`: the press target's default name and the ✕ tooltip. */
|
|
49
51
|
suggestionChip: { add: (label: string) => string; dismiss: string };
|
|
50
52
|
/** `Confidence`: the full level phrase ("High confidence" …). */
|
|
@@ -105,6 +107,7 @@ export const en: LoticsLocale = {
|
|
|
105
107
|
ledger: { rowDetails: (label) => `${label} details` },
|
|
106
108
|
sectionHeading: { info: "About this data" },
|
|
107
109
|
chip: { remove: "Remove" },
|
|
110
|
+
trendFooter: { up: "Up", down: "Down" },
|
|
108
111
|
suggestionChip: { add: (label) => `Add: ${label}`, dismiss: "Dismiss suggestion" },
|
|
109
112
|
confidence: { high: "High confidence", medium: "Medium confidence", low: "Low confidence" },
|
|
110
113
|
remainderMeter: {
|
|
@@ -181,6 +184,7 @@ export const vi: LoticsLocale = {
|
|
|
181
184
|
ledger: { rowDetails: (label) => `Chi tiết ${label}` },
|
|
182
185
|
sectionHeading: { info: "Giải thích dữ liệu" },
|
|
183
186
|
chip: { remove: "Xóa" },
|
|
187
|
+
trendFooter: { up: "Tăng", down: "Giảm" },
|
|
184
188
|
suggestionChip: { add: (label) => `Thêm: ${label}`, dismiss: "Bỏ gợi ý" },
|
|
185
189
|
confidence: { high: "Độ tin cậy cao", medium: "Độ tin cậy trung bình", low: "Độ tin cậy thấp" },
|
|
186
190
|
remainderMeter: {
|
package/src/page_content.tsx
CHANGED
package/src/page_header.tsx
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { View } from "react-native";
|
|
2
2
|
import { Text } from "@lotics/ui/text";
|
|
3
3
|
import { ReactNode } from "react";
|
|
4
|
-
import { useScreenSize } from "@lotics/ui/use_screen_size";
|
|
5
4
|
|
|
6
5
|
interface PageHeaderProps {
|
|
7
6
|
title: string;
|
|
@@ -12,7 +11,6 @@ interface PageHeaderProps {
|
|
|
12
11
|
|
|
13
12
|
export function PageHeader(props: PageHeaderProps) {
|
|
14
13
|
const { title, description, left, right } = props;
|
|
15
|
-
const screenSize = useScreenSize();
|
|
16
14
|
|
|
17
15
|
const hasNav = !!left || !!right;
|
|
18
16
|
|
|
@@ -24,12 +22,12 @@ export function PageHeader(props: PageHeaderProps) {
|
|
|
24
22
|
{left}
|
|
25
23
|
{right}
|
|
26
24
|
</View>
|
|
27
|
-
<Text size="
|
|
25
|
+
<Text size="xxl" weight="semibold">
|
|
28
26
|
{title}
|
|
29
27
|
</Text>
|
|
30
28
|
</>
|
|
31
29
|
) : (
|
|
32
|
-
<Text size="
|
|
30
|
+
<Text size="xxl" weight="semibold">
|
|
33
31
|
{title}
|
|
34
32
|
</Text>
|
|
35
33
|
)}
|
package/src/popover_nav.tsx
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { createContext, ReactNode, useContext } from "react";
|
|
2
|
+
import { IconButton } from "./icon_button";
|
|
3
|
+
import { View, StyleSheet } from "react-native";
|
|
4
|
+
import { Text } from "./text";
|
|
2
5
|
|
|
3
6
|
export interface PopoverNavContextValue {
|
|
4
7
|
currentRoute: string;
|
|
@@ -30,3 +33,40 @@ export function PopoverScreen(props: PopoverScreenProps) {
|
|
|
30
33
|
|
|
31
34
|
return <>{children}</>;
|
|
32
35
|
}
|
|
36
|
+
|
|
37
|
+
export interface PopoverNavHeaderProps {
|
|
38
|
+
title: string;
|
|
39
|
+
right?: ReactNode;
|
|
40
|
+
/** Accessible name for the back button. Default: "Back". Pass a translated string from the consumer. */
|
|
41
|
+
backLabel?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The routed-popover title row: a back chevron auto-appears while `canGoBack`
|
|
45
|
+
* (reads `usePopoverNav`), sm-medium title fills, `right` a trailing action.
|
|
46
|
+
* Pairs with `PopoverScreen`. Distinct from `Popover`'s own plain
|
|
47
|
+
* `PopoverHeader` children container. */
|
|
48
|
+
export function PopoverNavHeader(props: PopoverNavHeaderProps) {
|
|
49
|
+
const { title, right, backLabel = "Back" } = props;
|
|
50
|
+
const { goBack, canGoBack } = usePopoverNav();
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<View style={navHeaderStyles.container}>
|
|
54
|
+
{canGoBack && (
|
|
55
|
+
<IconButton icon="chevron-left" size="lg" color="secondary" accessibilityLabel={backLabel} onPress={goBack} />
|
|
56
|
+
)}
|
|
57
|
+
<Text size="sm" weight="medium" style={{ flex: 1 }}>
|
|
58
|
+
{title}
|
|
59
|
+
</Text>
|
|
60
|
+
{right}
|
|
61
|
+
</View>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const navHeaderStyles = StyleSheet.create({
|
|
66
|
+
container: {
|
|
67
|
+
paddingLeft: 8,
|
|
68
|
+
flexDirection: "row",
|
|
69
|
+
alignItems: "center",
|
|
70
|
+
gap: 8,
|
|
71
|
+
},
|
|
72
|
+
});
|
package/src/record_summary.tsx
CHANGED
|
@@ -47,7 +47,9 @@ export function RecordSummary(props: RecordSummaryProps) {
|
|
|
47
47
|
<View style={styles.identityRow}>
|
|
48
48
|
<View style={styles.identity}>
|
|
49
49
|
<View style={styles.titleRow}>
|
|
50
|
-
|
|
50
|
+
{/* xxl — the `#` of the heading ramp (# xxl → ## xl section →
|
|
51
|
+
### lg subsection); the identity always tops the outline */}
|
|
52
|
+
<Text size="xxl" weight="semibold" tabular level={level}>
|
|
51
53
|
{title}
|
|
52
54
|
</Text>
|
|
53
55
|
{status ?? null}
|
package/src/ring_gauge.tsx
CHANGED
package/src/section_heading.tsx
CHANGED
|
@@ -38,13 +38,72 @@ export interface SectionProps {
|
|
|
38
38
|
*/
|
|
39
39
|
export function Section(props: SectionProps) {
|
|
40
40
|
// gap 12 spaces the heading from its body. Space BETWEEN sections belongs to
|
|
41
|
-
// the page column
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
// section).
|
|
41
|
+
// the page column (`SectionStack`, 56 + hairline); a body of `Subsection`
|
|
42
|
+
// groups divides via a nested `SubsectionStack` (24 + hairline) —
|
|
43
|
+
// never a hairline directly under the heading (it belongs to its content).
|
|
45
44
|
return <View style={[{ gap: 12 }, props.style]}>{props.children}</View>;
|
|
46
45
|
}
|
|
47
46
|
|
|
47
|
+
export interface SubsectionProps {
|
|
48
|
+
children: React.ReactNode;
|
|
49
|
+
style?: StyleProp<ViewStyle>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* One named group INSIDE a `Section` — the level below `Section` on a long
|
|
54
|
+
* record surface (`###` in the heading ramp). Compose a `SubsectionHeading`
|
|
55
|
+
* then the group's rows; sibling subsections stack in a `SubsectionStack`
|
|
56
|
+
* (24 + hairline between groups — no margins, no hand-rolled dividers). A
|
|
57
|
+
* headingless `Subsection` is fine for the section's lead group.
|
|
58
|
+
*
|
|
59
|
+
* <Section>
|
|
60
|
+
* <SectionHeading><SectionHeadingTitle>Delivery</SectionHeadingTitle></SectionHeading>
|
|
61
|
+
* <SubsectionStack>
|
|
62
|
+
* <Subsection>{lead rows}</Subsection>
|
|
63
|
+
* <Subsection>
|
|
64
|
+
* <SubsectionHeading><SubsectionHeadingTitle>Carrier</SubsectionHeadingTitle></SubsectionHeading>
|
|
65
|
+
* {rows}
|
|
66
|
+
* </Subsection>
|
|
67
|
+
* </SubsectionStack>
|
|
68
|
+
* </Section>
|
|
69
|
+
*/
|
|
70
|
+
export function Subsection(props: SubsectionProps) {
|
|
71
|
+
return <View style={[{ gap: 12 }, props.style]}>{props.children}</View>;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface SubsectionHeadingProps {
|
|
75
|
+
children: React.ReactNode;
|
|
76
|
+
style?: StyleProp<ViewStyle>;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function SubsectionHeading(props: SubsectionHeadingProps) {
|
|
80
|
+
return (
|
|
81
|
+
<View style={[{ flexDirection: "row", alignItems: "center", gap: 10 }, props.style]}>
|
|
82
|
+
{props.children}
|
|
83
|
+
</View>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface SubsectionHeadingTitleProps {
|
|
88
|
+
children: React.ReactNode;
|
|
89
|
+
/** Heading rank. Defaults to 3 — one level under `SectionHeadingTitle`'s 2. */
|
|
90
|
+
level?: HeadingLevel;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** The subsection title — ALWAYS `###` (lg semibold), the fixed step under the
|
|
94
|
+
* xl section title and above the md/sm body. Grows to push siblings (a
|
|
95
|
+
* `SectionHeadingMeta`, a `Badge`, an action) to the right edge. */
|
|
96
|
+
export function SubsectionHeadingTitle(props: SubsectionHeadingTitleProps) {
|
|
97
|
+
const { children, level = 3 } = props;
|
|
98
|
+
return (
|
|
99
|
+
<View style={{ flex: 1 }}>
|
|
100
|
+
<Text level={level} size="lg" weight="semibold">
|
|
101
|
+
{children}
|
|
102
|
+
</Text>
|
|
103
|
+
</View>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
48
107
|
export interface SectionHeadingProps {
|
|
49
108
|
children: React.ReactNode;
|
|
50
109
|
style?: StyleProp<ViewStyle>;
|
|
@@ -73,16 +132,18 @@ export interface SectionHeadingTitleProps {
|
|
|
73
132
|
info?: string;
|
|
74
133
|
}
|
|
75
134
|
|
|
76
|
-
/** The title block —
|
|
77
|
-
*
|
|
78
|
-
*
|
|
135
|
+
/** The title block — ALWAYS `##` (xl semibold): the heading ramp is the fixed
|
|
136
|
+
* markdown ladder `#` xxl (`RecordSummary`/`PageHeader`) → `##` xl (section) →
|
|
137
|
+
* `###` lg (`SubsectionHeadingTitle`) over the sm body — no per-surface size
|
|
138
|
+
* knobs, one outline everywhere. Optional muted `description` line; grows to
|
|
139
|
+
* push any sibling actions to the right edge. */
|
|
79
140
|
export function SectionHeadingTitle(props: SectionHeadingTitleProps) {
|
|
80
141
|
const { children, description, icon, level = 2, weight = "semibold", info } = props;
|
|
81
142
|
return (
|
|
82
143
|
<View style={{ flex: 1, gap: 2 }}>
|
|
83
144
|
<View style={{ flexDirection: "row", alignItems: "center", gap: 6 }}>
|
|
84
|
-
{icon ? <Icon name={icon} size={
|
|
85
|
-
<Text level={level} weight={weight} size="
|
|
145
|
+
{icon ? <Icon name={icon} size={20} /> : null}
|
|
146
|
+
<Text level={level} weight={weight} size="xl">
|
|
86
147
|
{children}
|
|
87
148
|
</Text>
|
|
88
149
|
{info ? <InfoPopover text={info} accessibilityLabel={useLoticsLocale().sectionHeading.info} /> : null}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { Children, Fragment } from "react";
|
|
2
|
+
import { View, type StyleProp, type ViewStyle } from "react-native";
|
|
3
|
+
import { Divider } from "./divider";
|
|
4
|
+
|
|
5
|
+
interface StackProps {
|
|
6
|
+
children: React.ReactNode;
|
|
7
|
+
/** Hairline between blocks (default true). Turn off for a short surface
|
|
8
|
+
* where the beat alone is enough. */
|
|
9
|
+
divided?: boolean;
|
|
10
|
+
style?: StyleProp<ViewStyle>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Interleave a hairline between the non-null blocks of a gap column. */
|
|
14
|
+
function DividedStack({ children, divided = true, gap, style }: StackProps & { gap: number }) {
|
|
15
|
+
const blocks = Children.toArray(children); // drops null/undefined/false
|
|
16
|
+
return (
|
|
17
|
+
<View style={[{ gap }, style]}>
|
|
18
|
+
{blocks.map((block, i) => (
|
|
19
|
+
// Children.toArray keys are stable for conditional lists; the fragment
|
|
20
|
+
// pairs each block with the hairline that separates it from the last.
|
|
21
|
+
<Fragment key={`sec_${i}`}>
|
|
22
|
+
{divided && i > 0 ? <Divider /> : null}
|
|
23
|
+
{block}
|
|
24
|
+
</Fragment>
|
|
25
|
+
))}
|
|
26
|
+
</View>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type SectionStackProps = StackProps;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The content column of a flat record/form page — it OWNS the between-section
|
|
34
|
+
* law: a fixed 56px beat with a hairline `Divider` between top-level blocks
|
|
35
|
+
* (never under a heading; the hairline separates a block from the PREVIOUS
|
|
36
|
+
* one). Null/false children are skipped, so a conditional section never
|
|
37
|
+
* leaves a stray hairline. No knobs — every flat page shares one rhythm.
|
|
38
|
+
*
|
|
39
|
+
* <SectionStack>
|
|
40
|
+
* {headerBand}
|
|
41
|
+
* <Section>…</Section>
|
|
42
|
+
* {canTransfer ? <Section>…</Section> : null}
|
|
43
|
+
* </SectionStack>
|
|
44
|
+
*/
|
|
45
|
+
export function SectionStack(props: SectionStackProps) {
|
|
46
|
+
return <DividedStack {...props} gap={56} />;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type SubsectionStackProps = StackProps;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The divided stack of `Subsection` groups INSIDE a `Section` — the same
|
|
53
|
+
* hairline law one step tighter: a fixed 24px beat. Compose it as the
|
|
54
|
+
* section's body (after the `SectionHeading`); a section's groups divide
|
|
55
|
+
* without hand-rolled gaps or dividers.
|
|
56
|
+
*
|
|
57
|
+
* <Section>
|
|
58
|
+
* <SectionHeading><SectionHeadingTitle>Delivery</SectionHeadingTitle></SectionHeading>
|
|
59
|
+
* <SubsectionStack>
|
|
60
|
+
* <Subsection>{lead rows}</Subsection>
|
|
61
|
+
* {carrier ? <Subsection>…</Subsection> : null}
|
|
62
|
+
* </SubsectionStack>
|
|
63
|
+
* </Section>
|
|
64
|
+
*/
|
|
65
|
+
export function SubsectionStack(props: SubsectionStackProps) {
|
|
66
|
+
return <DividedStack {...props} gap={24} />;
|
|
67
|
+
}
|
package/src/text.css
CHANGED
|
@@ -31,12 +31,19 @@
|
|
|
31
31
|
line-height: 24px;
|
|
32
32
|
letter-spacing: -0.005em;
|
|
33
33
|
}
|
|
34
|
+
/* Display ramp — the heading ladder: xl = `##` (section), xxl = `#`
|
|
35
|
+
(page/record title), xxxl = hero numbers. */
|
|
34
36
|
[data-text-size="xl"] {
|
|
37
|
+
font-size: 22px;
|
|
38
|
+
line-height: 28px;
|
|
39
|
+
letter-spacing: -0.01em;
|
|
40
|
+
}
|
|
41
|
+
[data-text-size="xxl"] {
|
|
35
42
|
font-size: 28px;
|
|
36
43
|
line-height: 34px;
|
|
37
44
|
letter-spacing: -0.015em;
|
|
38
45
|
}
|
|
39
|
-
[data-text-size="
|
|
46
|
+
[data-text-size="xxxl"] {
|
|
40
47
|
font-size: 32px;
|
|
41
48
|
line-height: 38px;
|
|
42
49
|
letter-spacing: -0.02em;
|
|
@@ -61,10 +68,14 @@
|
|
|
61
68
|
line-height: 28px;
|
|
62
69
|
}
|
|
63
70
|
[data-text-size="xl"] {
|
|
71
|
+
font-size: 24px;
|
|
72
|
+
line-height: 32px;
|
|
73
|
+
}
|
|
74
|
+
[data-text-size="xxl"] {
|
|
64
75
|
font-size: 32px;
|
|
65
76
|
line-height: 44px;
|
|
66
77
|
}
|
|
67
|
-
[data-text-size="
|
|
78
|
+
[data-text-size="xxxl"] {
|
|
68
79
|
font-size: 48px;
|
|
69
80
|
line-height: 52px;
|
|
70
81
|
}
|
package/src/text.tsx
CHANGED
|
@@ -50,7 +50,7 @@ export interface TextProps {
|
|
|
50
50
|
|
|
51
51
|
export type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
|
|
52
52
|
|
|
53
|
-
export type TextSize = "xs" | "sm" | "md" | "lg" | "xl" | "xxl";
|
|
53
|
+
export type TextSize = "xs" | "sm" | "md" | "lg" | "xl" | "xxl" | "xxxl";
|
|
54
54
|
type TextAlign = "left" | "right" | "center";
|
|
55
55
|
type TextDecorationLine = "underline" | "lineThrough" | "underline lineThrough";
|
|
56
56
|
type TextWeight = "regular" | "medium" | "semibold";
|
|
@@ -146,11 +146,17 @@ const styles = StyleSheet.create({
|
|
|
146
146
|
fontSize: 18,
|
|
147
147
|
lineHeight: 30,
|
|
148
148
|
},
|
|
149
|
+
// Display ramp — the heading ladder: xl = `##` (section), xxl = `#`
|
|
150
|
+
// (page/record title), xxxl = hero numbers.
|
|
149
151
|
xl: {
|
|
152
|
+
fontSize: 22,
|
|
153
|
+
lineHeight: 28,
|
|
154
|
+
},
|
|
155
|
+
xxl: {
|
|
150
156
|
fontSize: 28,
|
|
151
157
|
lineHeight: 34,
|
|
152
158
|
},
|
|
153
|
-
|
|
159
|
+
xxxl: {
|
|
154
160
|
fontSize: 32,
|
|
155
161
|
lineHeight: 38,
|
|
156
162
|
},
|
package/src/text_utils.ts
CHANGED
|
@@ -10,7 +10,8 @@ export type TextColor =
|
|
|
10
10
|
| "success"
|
|
11
11
|
| "zinc-900"
|
|
12
12
|
| "zinc-700"
|
|
13
|
-
| "zinc-500"
|
|
13
|
+
| "zinc-500"
|
|
14
|
+
| "zinc-400";
|
|
14
15
|
|
|
15
16
|
export function getTextColor(color?: TextColor): string {
|
|
16
17
|
switch (color) {
|
|
@@ -24,6 +25,10 @@ export function getTextColor(color?: TextColor): string {
|
|
|
24
25
|
return colors.zinc["700"];
|
|
25
26
|
case "zinc-500":
|
|
26
27
|
return colors.zinc["500"];
|
|
28
|
+
// The disabled ink — light enough that a control reads inert at a glance
|
|
29
|
+
// (muted/600 still reads as body ink on a zinc-50 chip).
|
|
30
|
+
case "zinc-400":
|
|
31
|
+
return colors.zinc["400"];
|
|
27
32
|
case "inverted":
|
|
28
33
|
return colors.white;
|
|
29
34
|
// Valence set — the lightest status weight: a state WORD whose meaning is in
|
package/src/trend_footer.tsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { TrendingDown, TrendingUp } from "lucide-react";
|
|
2
|
+
import { useLoticsLocale } from "./locale";
|
|
2
3
|
import { View, StyleSheet } from "react-native";
|
|
3
4
|
import { Text } from "./text";
|
|
4
5
|
import { colors } from "./colors";
|
|
@@ -32,13 +33,14 @@ export function TrendFooter(props: TrendFooterProps) {
|
|
|
32
33
|
const flat = value === 0;
|
|
33
34
|
const isGood = flat ? null : (up && goodDirection === "up") || (!up && goodDirection === "down");
|
|
34
35
|
const color = isGood === null ? colors.zinc[600] : isGood ? colors.green[700] : colors.red[700];
|
|
36
|
+
const labels = useLoticsLocale().trendFooter;
|
|
35
37
|
const Icon = up ? TrendingUp : TrendingDown;
|
|
36
38
|
return (
|
|
37
39
|
<View style={styles.container}>
|
|
38
40
|
<View style={styles.row}>
|
|
39
41
|
<Icon size={16} color={color} />
|
|
40
42
|
<Text size="sm" weight="medium" style={{ color }}>
|
|
41
|
-
{up ?
|
|
43
|
+
{up ? labels.up : labels.down} {Math.abs(value)}% {periodLabel}
|
|
42
44
|
</Text>
|
|
43
45
|
</View>
|
|
44
46
|
{detail && (
|
package/src/use_screen_size.ts
CHANGED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import React, { useEffect, useRef } from "react";
|
|
2
|
-
import { Animated, StyleProp, ViewStyle } from "react-native";
|
|
3
|
-
|
|
4
|
-
interface AnimationHorizontalSlideProps {
|
|
5
|
-
width: number;
|
|
6
|
-
visible: boolean;
|
|
7
|
-
children?: React.ReactNode;
|
|
8
|
-
onSlideAnimationEnd?: () => void;
|
|
9
|
-
onCollapseAnimationEnd?: () => void;
|
|
10
|
-
style?: StyleProp<ViewStyle>;
|
|
11
|
-
contentContainerStyle?: StyleProp<ViewStyle>;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function AnimationHorizontalSlide(props: AnimationHorizontalSlideProps) {
|
|
15
|
-
const {
|
|
16
|
-
width: intrinsicWidth,
|
|
17
|
-
visible,
|
|
18
|
-
children,
|
|
19
|
-
onSlideAnimationEnd,
|
|
20
|
-
onCollapseAnimationEnd,
|
|
21
|
-
style,
|
|
22
|
-
contentContainerStyle,
|
|
23
|
-
} = props;
|
|
24
|
-
const widthAnim = useRef(new Animated.Value(visible ? intrinsicWidth : 0)).current;
|
|
25
|
-
const opacityAnim = useRef(new Animated.Value(visible ? 1 : 0)).current;
|
|
26
|
-
|
|
27
|
-
useEffect(() => {
|
|
28
|
-
Animated.parallel([
|
|
29
|
-
Animated.spring(widthAnim, {
|
|
30
|
-
toValue: visible ? intrinsicWidth : 0,
|
|
31
|
-
bounciness: 0,
|
|
32
|
-
useNativeDriver: false,
|
|
33
|
-
}),
|
|
34
|
-
Animated.spring(opacityAnim, {
|
|
35
|
-
toValue: visible ? 1 : 0,
|
|
36
|
-
bounciness: 0,
|
|
37
|
-
useNativeDriver: false,
|
|
38
|
-
}),
|
|
39
|
-
]).start((animation) => {
|
|
40
|
-
if (animation.finished) {
|
|
41
|
-
if (visible) {
|
|
42
|
-
onSlideAnimationEnd?.();
|
|
43
|
-
} else {
|
|
44
|
-
onCollapseAnimationEnd?.();
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
});
|
|
48
|
-
}, [
|
|
49
|
-
widthAnim,
|
|
50
|
-
opacityAnim,
|
|
51
|
-
visible,
|
|
52
|
-
intrinsicWidth,
|
|
53
|
-
onSlideAnimationEnd,
|
|
54
|
-
onCollapseAnimationEnd,
|
|
55
|
-
]);
|
|
56
|
-
|
|
57
|
-
return (
|
|
58
|
-
<Animated.View style={[{ width: widthAnim, opacity: opacityAnim }, style]}>
|
|
59
|
-
<Animated.View
|
|
60
|
-
style={[
|
|
61
|
-
{
|
|
62
|
-
display: visible ? "flex" : "none",
|
|
63
|
-
position: "absolute",
|
|
64
|
-
top: 0,
|
|
65
|
-
bottom: 0,
|
|
66
|
-
width: intrinsicWidth,
|
|
67
|
-
},
|
|
68
|
-
contentContainerStyle,
|
|
69
|
-
]}
|
|
70
|
-
>
|
|
71
|
-
{children}
|
|
72
|
-
</Animated.View>
|
|
73
|
-
</Animated.View>
|
|
74
|
-
);
|
|
75
|
-
}
|
package/src/form_time_picker.tsx
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { FormField, FormFieldProps } from "./form_field";
|
|
2
|
-
import { TimePicker, TimePickerProps } from "./time_picker";
|
|
3
|
-
|
|
4
|
-
export interface FormTimePickerProps
|
|
5
|
-
extends Omit<FormFieldProps, "style">,
|
|
6
|
-
TimePickerProps {}
|
|
7
|
-
|
|
8
|
-
export function FormTimePicker(props: FormTimePickerProps) {
|
|
9
|
-
const { label, description, optional, optionalLabel, error, ...timePickerProps } = props;
|
|
10
|
-
|
|
11
|
-
return (
|
|
12
|
-
<FormField
|
|
13
|
-
label={label}
|
|
14
|
-
description={description}
|
|
15
|
-
error={error}
|
|
16
|
-
optional={optional}
|
|
17
|
-
optionalLabel={optionalLabel}
|
|
18
|
-
>
|
|
19
|
-
<TimePicker {...timePickerProps} />
|
|
20
|
-
</FormField>
|
|
21
|
-
);
|
|
22
|
-
}
|
package/src/highlighted_text.tsx
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import { Text, TextProps } from "./text";
|
|
3
|
-
import { colors } from "./colors";
|
|
4
|
-
import { View, StyleSheet } from "react-native";
|
|
5
|
-
|
|
6
|
-
interface HighlightedTextProps extends Omit<TextProps, "children"> {
|
|
7
|
-
text: string;
|
|
8
|
-
searchTerm: string;
|
|
9
|
-
singleLine?: boolean;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function HighlightedText({
|
|
13
|
-
text,
|
|
14
|
-
searchTerm,
|
|
15
|
-
singleLine = false,
|
|
16
|
-
...textProps
|
|
17
|
-
}: HighlightedTextProps) {
|
|
18
|
-
const parts = highlightSearchTerm(text || "", searchTerm || "");
|
|
19
|
-
|
|
20
|
-
// Use numberOfLines prop for single line display instead of custom CSS
|
|
21
|
-
const numberOfLines = singleLine ? 1 : undefined;
|
|
22
|
-
|
|
23
|
-
return (
|
|
24
|
-
<Text {...textProps} numberOfLines={numberOfLines}>
|
|
25
|
-
{parts.map((part, index) => (
|
|
26
|
-
<Text key={index} style={part.isHighlighted ? styles.highlightedText : undefined}>
|
|
27
|
-
{part.text}
|
|
28
|
-
</Text>
|
|
29
|
-
))}
|
|
30
|
-
</Text>
|
|
31
|
-
);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
interface TextPart {
|
|
35
|
-
text: string;
|
|
36
|
-
isHighlighted: boolean;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function highlightSearchTerm(text: string, searchTerm: string): TextPart[] {
|
|
40
|
-
if (!searchTerm.trim()) {
|
|
41
|
-
return [{ text, isHighlighted: false }];
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const searchRegex = new RegExp(`(${escapeRegExp(searchTerm.trim())})`, "gi");
|
|
45
|
-
const parts: TextPart[] = [];
|
|
46
|
-
let lastIndex = 0;
|
|
47
|
-
|
|
48
|
-
text.replace(searchRegex, (match, ...args) => {
|
|
49
|
-
const index = args[args.length - 2] as number; // offset index from regex replace
|
|
50
|
-
|
|
51
|
-
// Add text before the match
|
|
52
|
-
if (index > lastIndex) {
|
|
53
|
-
parts.push({
|
|
54
|
-
text: text.slice(lastIndex, index),
|
|
55
|
-
isHighlighted: false,
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Add the highlighted match
|
|
60
|
-
parts.push({
|
|
61
|
-
text: match,
|
|
62
|
-
isHighlighted: true,
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
lastIndex = index + match.length;
|
|
66
|
-
return match;
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
// Add any remaining text after the last match
|
|
70
|
-
if (lastIndex < text.length) {
|
|
71
|
-
parts.push({
|
|
72
|
-
text: text.slice(lastIndex),
|
|
73
|
-
isHighlighted: false,
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return parts.length > 0 ? parts : [{ text, isHighlighted: false }];
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function escapeRegExp(string: string): string {
|
|
81
|
-
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const styles = StyleSheet.create({
|
|
85
|
-
highlightedText: {
|
|
86
|
-
backgroundColor: colors.yellow[100],
|
|
87
|
-
color: colors.yellow[900],
|
|
88
|
-
fontWeight: "bold" as const,
|
|
89
|
-
paddingHorizontal: 2,
|
|
90
|
-
borderRadius: 2,
|
|
91
|
-
},
|
|
92
|
-
});
|
package/src/menu_title.tsx
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { Text } from "./text";
|
|
2
|
-
|
|
3
|
-
export interface MenuTitleProps {
|
|
4
|
-
title: string;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export function MenuTitle(props: MenuTitleProps) {
|
|
8
|
-
const { title } = props;
|
|
9
|
-
|
|
10
|
-
return (
|
|
11
|
-
<Text size="lg" weight="semibold" style={{ paddingLeft: 8, lineHeight: 32, paddingBottom: 16 }}>
|
|
12
|
-
{title}
|
|
13
|
-
</Text>
|
|
14
|
-
);
|
|
15
|
-
}
|