@aria-framework/theme 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/README.md +113 -0
- package/build.js +173 -0
- package/dist/ThemeContext.d.ts +21 -0
- package/dist/ThemeContext.js +50 -0
- package/dist/fonts.d.ts +15 -0
- package/dist/fonts.js +30 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +21 -0
- package/dist/tokens.d.ts +65 -0
- package/dist/tokens.js +68 -0
- package/dist/ui.d.ts +87 -0
- package/dist/ui.js +129 -0
- package/mobile/ThemeContext.tsx +59 -0
- package/mobile/fonts.ts +23 -0
- package/mobile/index.ts +5 -0
- package/mobile/tokens.ts +87 -0
- package/mobile/ui.tsx +190 -0
- package/package.json +47 -0
- package/tokens.json +36 -0
- package/tsconfig.json +17 -0
- package/types/rn-shims.d.ts +28 -0
- package/web/fonts/hanken-grotesk-wght.woff2 +0 -0
- package/web/fonts/ibm-plex-mono-400.woff2 +0 -0
- package/web/fonts/ibm-plex-mono-500.woff2 +0 -0
- package/web/fonts/ibm-plex-mono-600.woff2 +0 -0
- package/web/theme.css +85 -0
package/dist/ui.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MONO = exports.TAB_BAR_HEIGHT = void 0;
|
|
4
|
+
exports.useBottomPad = useBottomPad;
|
|
5
|
+
exports.useStyles = useStyles;
|
|
6
|
+
exports.Card = Card;
|
|
7
|
+
exports.Btn = Btn;
|
|
8
|
+
exports.Field = Field;
|
|
9
|
+
exports.Badge = Badge;
|
|
10
|
+
exports.Row = Row;
|
|
11
|
+
exports.Segmented = Segmented;
|
|
12
|
+
exports.Muted = Muted;
|
|
13
|
+
exports.Mono = Mono;
|
|
14
|
+
exports.Checkbox = Checkbox;
|
|
15
|
+
exports.Skeleton = Skeleton;
|
|
16
|
+
exports.Switch = Switch;
|
|
17
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
18
|
+
/** Small shared UI primitives (kept in one file to limit screen boilerplate).
|
|
19
|
+
* All are theme-aware via useTheme()/useStyles(). */
|
|
20
|
+
const react_1 = require("react");
|
|
21
|
+
const react_native_1 = require("react-native");
|
|
22
|
+
const react_native_safe_area_context_1 = require("react-native-safe-area-context");
|
|
23
|
+
const tokens_1 = require("./tokens");
|
|
24
|
+
const fonts_1 = require("./fonts");
|
|
25
|
+
const ThemeContext_1 = require("./ThemeContext");
|
|
26
|
+
/** Approx height of the floating bottom tab bar (excludes the device inset, which
|
|
27
|
+
* the bar adds on top via useSafeAreaInsets). */
|
|
28
|
+
exports.TAB_BAR_HEIGHT = 60;
|
|
29
|
+
/** Bottom padding for a scroll container so its last content clears the gesture
|
|
30
|
+
* bar — and, on the four tab screens, the floating tab bar too. Pass onTab=true
|
|
31
|
+
* for Home/Tickets/Visits/Customers. */
|
|
32
|
+
function useBottomPad(onTab = false) {
|
|
33
|
+
const insets = (0, react_native_safe_area_context_1.useSafeAreaInsets)();
|
|
34
|
+
return (onTab ? exports.TAB_BAR_HEIGHT : tokens_1.space.lg) + insets.bottom;
|
|
35
|
+
}
|
|
36
|
+
/** IBM Plex Mono for data (IDs, time, distances, GPS) — loaded in App.tsx. */
|
|
37
|
+
exports.MONO = fonts_1.FONT.mono;
|
|
38
|
+
function makeStyles(c) {
|
|
39
|
+
return react_native_1.StyleSheet.create({
|
|
40
|
+
card: { backgroundColor: c.surface, borderRadius: 12, borderWidth: 1, borderColor: c.lineStrong, padding: tokens_1.space.lg, marginBottom: tokens_1.space.md },
|
|
41
|
+
btn: { paddingVertical: 12, paddingHorizontal: 16, borderRadius: 10, borderWidth: 1, alignItems: "center", justifyContent: "center", minHeight: 46 },
|
|
42
|
+
btnText: { fontSize: 15, fontWeight: "600", fontFamily: (0, fonts_1.hanken)("600") },
|
|
43
|
+
label: { fontSize: 13, color: c.muted, marginBottom: 4, fontWeight: "600", fontFamily: (0, fonts_1.hanken)("600") },
|
|
44
|
+
input: { backgroundColor: c.surface2, borderWidth: 1, borderColor: c.lineStrong, borderRadius: 10, paddingHorizontal: 12, paddingVertical: 10, fontSize: 15, color: c.text, fontFamily: (0, fonts_1.hanken)("400") },
|
|
45
|
+
badge: { paddingHorizontal: 8, paddingVertical: 2, borderRadius: 999, borderWidth: 1, alignSelf: "flex-start" },
|
|
46
|
+
h1: { fontSize: 22, fontWeight: "700", color: c.text, fontFamily: (0, fonts_1.hanken)("700") },
|
|
47
|
+
title: { fontSize: 16, fontWeight: "700", color: c.text, fontFamily: (0, fonts_1.hanken)("700") },
|
|
48
|
+
cbox: { width: 24, height: 24, borderRadius: 7, borderWidth: 2, borderColor: c.lineStrong, alignItems: "center", justifyContent: "center", marginRight: tokens_1.space.sm },
|
|
49
|
+
swTrack: { width: 46, height: 28, borderRadius: 999, justifyContent: "center" },
|
|
50
|
+
swKnob: { position: "absolute", width: 22, height: 22, borderRadius: 11, backgroundColor: "#ffffff", shadowColor: "#000", shadowOpacity: 0.3, shadowRadius: 2, shadowOffset: { width: 0, height: 1 }, elevation: 2 },
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
/** Theme-aware shared stylesheet — `const styles = useStyles()`. */
|
|
54
|
+
function useStyles() {
|
|
55
|
+
const { colors } = (0, ThemeContext_1.useTheme)();
|
|
56
|
+
return (0, react_1.useMemo)(() => makeStyles(colors), [colors]);
|
|
57
|
+
}
|
|
58
|
+
function Card({ children, style, onLayout }) {
|
|
59
|
+
const styles = useStyles();
|
|
60
|
+
const { scheme } = (0, ThemeContext_1.useTheme)();
|
|
61
|
+
return (0, jsx_runtime_1.jsx)(react_native_1.View, { style: [styles.card, tokens_1.shadow[scheme], style], onLayout: onLayout, children: children });
|
|
62
|
+
}
|
|
63
|
+
function Btn({ title, onPress, loading, disabled, variant = "primary", }) {
|
|
64
|
+
const { colors } = (0, ThemeContext_1.useTheme)();
|
|
65
|
+
const styles = useStyles();
|
|
66
|
+
const isDisabled = disabled || loading;
|
|
67
|
+
const bg = variant === "primary" ? colors.primary : variant === "danger" ? colors.danger : variant === "signal" ? colors.signal : "transparent";
|
|
68
|
+
const fg = variant === "outline" ? colors.primary : "#ffffff";
|
|
69
|
+
return ((0, jsx_runtime_1.jsx)(react_native_1.Pressable, { onPress: onPress, disabled: isDisabled, style: ({ pressed }) => [
|
|
70
|
+
styles.btn,
|
|
71
|
+
{ backgroundColor: bg, borderColor: variant === "outline" ? colors.primary : bg, opacity: isDisabled ? 0.5 : pressed ? 0.85 : 1 },
|
|
72
|
+
], children: loading ? (0, jsx_runtime_1.jsx)(react_native_1.ActivityIndicator, { color: fg }) : (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [styles.btnText, { color: fg }], children: title }) }));
|
|
73
|
+
}
|
|
74
|
+
function Field({ label, value, onChangeText, placeholder, secureTextEntry, keyboardType, multiline, autoCapitalize, }) {
|
|
75
|
+
const { colors } = (0, ThemeContext_1.useTheme)();
|
|
76
|
+
const styles = useStyles();
|
|
77
|
+
return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: { marginBottom: tokens_1.space.md }, children: [label ? (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.label, children: label }) : null, (0, jsx_runtime_1.jsx)(react_native_1.TextInput, { style: [styles.input, multiline && { height: 96, textAlignVertical: "top" }], value: value, onChangeText: onChangeText, placeholder: placeholder, placeholderTextColor: colors.textFaint, secureTextEntry: secureTextEntry, keyboardType: keyboardType, multiline: multiline, autoCapitalize: autoCapitalize })] }));
|
|
78
|
+
}
|
|
79
|
+
function Badge({ label, color, filled }) {
|
|
80
|
+
const styles = useStyles();
|
|
81
|
+
return ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: [styles.badge, filled ? { backgroundColor: color, borderColor: color } : { backgroundColor: color + "22", borderColor: color }], children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: { color: filled ? "#ffffff" : color, fontSize: 12, fontWeight: "600" }, children: label }) }));
|
|
82
|
+
}
|
|
83
|
+
function Row({ children, style }) {
|
|
84
|
+
return (0, jsx_runtime_1.jsx)(react_native_1.View, { style: [{ flexDirection: "row", alignItems: "center", gap: tokens_1.space.sm }, style], children: children });
|
|
85
|
+
}
|
|
86
|
+
/** Compact in-screen segmented tab control (not a navigator) — pass the active
|
|
87
|
+
* key and the options; renders an evenly-split pill row. */
|
|
88
|
+
function Segmented({ options, value, onChange }) {
|
|
89
|
+
const { colors } = (0, ThemeContext_1.useTheme)();
|
|
90
|
+
return ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: { flexDirection: "row", backgroundColor: colors.surface2, borderWidth: 1, borderColor: colors.border, borderRadius: 10, padding: 3 }, children: options.map((o) => {
|
|
91
|
+
const on = o.key === value;
|
|
92
|
+
return ((0, jsx_runtime_1.jsx)(react_native_1.Pressable, { onPress: () => onChange(o.key), style: { flex: 1, paddingVertical: 8, paddingHorizontal: 2, borderRadius: 8, alignItems: "center", justifyContent: "center", backgroundColor: on ? colors.surface : "transparent", borderWidth: 1, borderColor: on ? colors.border : "transparent" }, children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { numberOfLines: 1, adjustsFontSizeToFit: true, minimumFontScale: 0.85, style: { color: on ? colors.primary : colors.textFaint, fontWeight: on ? "700" : "600", fontSize: 13, fontFamily: (0, fonts_1.hanken)(on ? "700" : "600") }, children: o.label }) }, o.key));
|
|
93
|
+
}) }));
|
|
94
|
+
}
|
|
95
|
+
function Muted({ children, style }) {
|
|
96
|
+
const { colors } = (0, ThemeContext_1.useTheme)();
|
|
97
|
+
return (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [{ color: colors.muted, fontSize: 13, fontFamily: fonts_1.FONT.r }, style], children: children });
|
|
98
|
+
}
|
|
99
|
+
/** Monospace data span (tracking IDs, time, distances, coordinates). */
|
|
100
|
+
function Mono({ children, style }) {
|
|
101
|
+
const { colors } = (0, ThemeContext_1.useTheme)();
|
|
102
|
+
return (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [{ fontFamily: exports.MONO, color: colors.text }, style], children: children });
|
|
103
|
+
}
|
|
104
|
+
/** Shared checkbox — replaces the hand-rolled 26px boxes duplicated across screens. */
|
|
105
|
+
function Checkbox({ checked, label, sublabel, onToggle }) {
|
|
106
|
+
const { colors } = (0, ThemeContext_1.useTheme)();
|
|
107
|
+
const styles = useStyles();
|
|
108
|
+
return ((0, jsx_runtime_1.jsxs)(react_native_1.Pressable, { onPress: onToggle, hitSlop: 8, style: { flexDirection: "row", alignItems: "center", paddingVertical: 8 }, children: [(0, jsx_runtime_1.jsx)(react_native_1.View, { style: [styles.cbox, checked && { backgroundColor: colors.primary, borderColor: colors.primary }], children: checked ? (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: { color: "#ffffff", fontSize: 15, fontWeight: "800" }, children: "\u2713" }) : null }), (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: { flexShrink: 1 }, children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: { color: colors.text, fontSize: 15 }, children: label }), sublabel ? (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: { color: colors.muted, fontSize: 12 }, children: sublabel }) : null] })] }));
|
|
109
|
+
}
|
|
110
|
+
/** Pulsing placeholder block for loading states (replaces blank spinners). */
|
|
111
|
+
function Skeleton({ height = 14, width = "100%", style }) {
|
|
112
|
+
const { colors } = (0, ThemeContext_1.useTheme)();
|
|
113
|
+
const op = (0, react_1.useRef)(new react_native_1.Animated.Value(0.4)).current;
|
|
114
|
+
(0, react_1.useEffect)(() => {
|
|
115
|
+
const loop = react_native_1.Animated.loop(react_native_1.Animated.sequence([
|
|
116
|
+
react_native_1.Animated.timing(op, { toValue: 0.85, duration: 700, useNativeDriver: true }),
|
|
117
|
+
react_native_1.Animated.timing(op, { toValue: 0.4, duration: 700, useNativeDriver: true }),
|
|
118
|
+
]));
|
|
119
|
+
loop.start();
|
|
120
|
+
return () => loop.stop();
|
|
121
|
+
}, [op]);
|
|
122
|
+
return (0, jsx_runtime_1.jsx)(react_native_1.Animated.View, { style: [{ height, width, borderRadius: 7, backgroundColor: colors.surface2, opacity: op }, style] });
|
|
123
|
+
}
|
|
124
|
+
/** iOS-style switch — for settings toggles (on/off state, not a form choice). */
|
|
125
|
+
function Switch({ value, onToggle }) {
|
|
126
|
+
const { colors } = (0, ThemeContext_1.useTheme)();
|
|
127
|
+
const styles = useStyles();
|
|
128
|
+
return ((0, jsx_runtime_1.jsx)(react_native_1.Pressable, { onPress: onToggle, hitSlop: 8, style: [styles.swTrack, { backgroundColor: value ? colors.primary : colors.lineStrong }], children: (0, jsx_runtime_1.jsx)(react_native_1.View, { style: [styles.swKnob, { left: value ? 21 : 3 }] }) }));
|
|
129
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme provider — resolves the active palette (Ops Light / Ops Dark) from the
|
|
3
|
+
* user's preference (light | dark | system) and the OS appearance, and persists
|
|
4
|
+
* the choice in AsyncStorage. Screens read `const { colors } = useTheme()`.
|
|
5
|
+
*/
|
|
6
|
+
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
|
|
7
|
+
import { Appearance } from "react-native";
|
|
8
|
+
import AsyncStorage from "@react-native-async-storage/async-storage";
|
|
9
|
+
import { palettes, type Palette } from "./tokens";
|
|
10
|
+
|
|
11
|
+
export type ThemePref = "light" | "dark" | "system";
|
|
12
|
+
type Scheme = "light" | "dark";
|
|
13
|
+
|
|
14
|
+
/** Default AsyncStorage key. Override per-app via <ThemeProvider storageKey="myapp.theme">
|
|
15
|
+
* so multiple field-ops-theme apps on a device don't share one preference. */
|
|
16
|
+
const DEFAULT_KEY = "field-ops.theme";
|
|
17
|
+
|
|
18
|
+
interface ThemeState {
|
|
19
|
+
colors: Palette;
|
|
20
|
+
scheme: Scheme; // resolved (system → actual OS scheme)
|
|
21
|
+
pref: ThemePref; // user's stored choice
|
|
22
|
+
setPref: (p: ThemePref) => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const Ctx = createContext<ThemeState | undefined>(undefined);
|
|
26
|
+
|
|
27
|
+
export function ThemeProvider({ children, storageKey = DEFAULT_KEY }: { children: React.ReactNode; storageKey?: string }) {
|
|
28
|
+
const [pref, setPrefState] = useState<ThemePref>("system");
|
|
29
|
+
const [sysScheme, setSysScheme] = useState<Scheme>(Appearance.getColorScheme() === "dark" ? "dark" : "light");
|
|
30
|
+
|
|
31
|
+
// OS appearance listener — independent of storageKey, so subscribe exactly once.
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
const sub = Appearance.addChangeListener(({ colorScheme }) => setSysScheme(colorScheme === "dark" ? "dark" : "light"));
|
|
34
|
+
return () => sub.remove();
|
|
35
|
+
}, []);
|
|
36
|
+
|
|
37
|
+
// Load the persisted preference (re-reads only if the storage key changes).
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
AsyncStorage.getItem(storageKey).then((v) => {
|
|
40
|
+
if (v === "light" || v === "dark" || v === "system") setPrefState(v);
|
|
41
|
+
}).catch(() => {});
|
|
42
|
+
}, [storageKey]);
|
|
43
|
+
|
|
44
|
+
const setPref = useCallback((p: ThemePref) => {
|
|
45
|
+
setPrefState(p);
|
|
46
|
+
AsyncStorage.setItem(storageKey, p).catch(() => {});
|
|
47
|
+
}, [storageKey]);
|
|
48
|
+
|
|
49
|
+
const scheme: Scheme = pref === "system" ? sysScheme : pref;
|
|
50
|
+
const value = useMemo<ThemeState>(() => ({ colors: palettes[scheme], scheme, pref, setPref }), [scheme, pref, setPref]);
|
|
51
|
+
|
|
52
|
+
return <Ctx.Provider value={value}>{children}</Ctx.Provider>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function useTheme(): ThemeState {
|
|
56
|
+
const v = useContext(Ctx);
|
|
57
|
+
if (!v) throw new Error("useTheme must be used within ThemeProvider");
|
|
58
|
+
return v;
|
|
59
|
+
}
|
package/mobile/fonts.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Font families — "Field Ops Console" uses Hanken Grotesk (UI) + IBM Plex Mono
|
|
3
|
+
* (data). @expo-google-fonts ships each weight as its own family name, so weighted
|
|
4
|
+
* text must pick the matching family via hanken() rather than relying on fontWeight.
|
|
5
|
+
*/
|
|
6
|
+
export const FONT = {
|
|
7
|
+
r: "HankenGrotesk_400Regular",
|
|
8
|
+
m: "HankenGrotesk_500Medium",
|
|
9
|
+
sb: "HankenGrotesk_600SemiBold",
|
|
10
|
+
b: "HankenGrotesk_700Bold",
|
|
11
|
+
xb: "HankenGrotesk_800ExtraBold",
|
|
12
|
+
monoR: "IBMPlexMono_400Regular",
|
|
13
|
+
mono: "IBMPlexMono_500Medium",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function hanken(weight?: string | number): string {
|
|
17
|
+
const w = String(weight ?? "400");
|
|
18
|
+
if (w === "800") return FONT.xb;
|
|
19
|
+
if (w === "700") return FONT.b;
|
|
20
|
+
if (w === "600") return FONT.sb;
|
|
21
|
+
if (w === "500") return FONT.m;
|
|
22
|
+
return FONT.r;
|
|
23
|
+
}
|
package/mobile/index.ts
ADDED
package/mobile/tokens.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// AUTO-GENERATED from tokens.json by build.js — DO NOT EDIT. Change tokens.json, run `node build.js`.
|
|
2
|
+
/**
|
|
3
|
+
* Field Ops Console — shared visual tokens (React Native side).
|
|
4
|
+
* `palettes.light|dark` are consumed by ThemeContext; `colors` is the light
|
|
5
|
+
* default kept for back-compat with screens that import it directly.
|
|
6
|
+
*/
|
|
7
|
+
export type Palette = {
|
|
8
|
+
bg: string; surface: string; surface2: string; border: string; lineStrong: string;
|
|
9
|
+
text: string; muted: string; textFaint: string;
|
|
10
|
+
primary: string; primaryText: string; brandDeep: string; brandBright: string;
|
|
11
|
+
danger: string; success: string; signal: string;
|
|
12
|
+
warnBg: string; warnText: string;
|
|
13
|
+
brandTint: string; signalTint: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const light: Palette = {
|
|
17
|
+
bg: "#f4f5f2",
|
|
18
|
+
surface: "#ffffff",
|
|
19
|
+
surface2: "#fafbf9",
|
|
20
|
+
border: "#dcdad2",
|
|
21
|
+
lineStrong: "#cbc8bc",
|
|
22
|
+
text: "#18222a",
|
|
23
|
+
muted: "#5c6770",
|
|
24
|
+
textFaint: "#929ba1",
|
|
25
|
+
primary: "#0e7a6e",
|
|
26
|
+
primaryText: "#ffffff",
|
|
27
|
+
brandDeep: "#0a5c53",
|
|
28
|
+
brandBright: "#15b8a6",
|
|
29
|
+
danger: "#dc2626",
|
|
30
|
+
success: "#0e9e6e",
|
|
31
|
+
signal: "#e2710b",
|
|
32
|
+
warnBg: "#fcefd9",
|
|
33
|
+
warnText: "#8a4b08",
|
|
34
|
+
brandTint: "rgba(14,122,110,0.10)",
|
|
35
|
+
signalTint: "rgba(226,113,11,0.12)",
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const dark: Palette = {
|
|
39
|
+
bg: "#0c1215",
|
|
40
|
+
surface: "#141c21",
|
|
41
|
+
surface2: "#192329",
|
|
42
|
+
border: "#243038",
|
|
43
|
+
lineStrong: "#3a4854",
|
|
44
|
+
text: "#e7edef",
|
|
45
|
+
muted: "#9ba7ae",
|
|
46
|
+
textFaint: "#69767d",
|
|
47
|
+
primary: "#16b5a3",
|
|
48
|
+
primaryText: "#08110f",
|
|
49
|
+
brandDeep: "#0e7a6e",
|
|
50
|
+
brandBright: "#2fe0cc",
|
|
51
|
+
danger: "#f05252",
|
|
52
|
+
success: "#22b583",
|
|
53
|
+
signal: "#f2871f",
|
|
54
|
+
warnBg: "#2a2114",
|
|
55
|
+
warnText: "#f2c879",
|
|
56
|
+
brandTint: "rgba(22,181,163,0.14)",
|
|
57
|
+
signalTint: "rgba(242,135,31,0.16)",
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const palettes = { light, dark };
|
|
61
|
+
|
|
62
|
+
/** Back-compat default (light). */
|
|
63
|
+
export const colors = light;
|
|
64
|
+
|
|
65
|
+
export const status: Record<string, string> = {
|
|
66
|
+
new: "#2563eb",
|
|
67
|
+
open: "#0e8fa8",
|
|
68
|
+
pending: "#d9870b",
|
|
69
|
+
resolved: "#0e9e6e",
|
|
70
|
+
closed: "#7a848b",
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const priority: Record<string, string> = {
|
|
74
|
+
low: "#9aa6ae",
|
|
75
|
+
medium: "#2563eb",
|
|
76
|
+
high: "#ea580c",
|
|
77
|
+
critical: "#dc2626",
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const space = { xs: 4, sm: 8, md: 12, lg: 16, xl: 24 };
|
|
81
|
+
|
|
82
|
+
/** Subtle card elevation per scheme — RN has no box-shadow string, so this mirrors
|
|
83
|
+
* the web `--shadow` token as style props (iOS shadow*; Android elevation). */
|
|
84
|
+
export const shadow = {
|
|
85
|
+
light: { shadowColor: "#101820", shadowOpacity: 0.08, shadowRadius: 8, shadowOffset: { width: 0, height: 2 }, elevation: 2 },
|
|
86
|
+
dark: { shadowColor: "#000000", shadowOpacity: 0.45, shadowRadius: 10, shadowOffset: { width: 0, height: 3 }, elevation: 3 },
|
|
87
|
+
} as const;
|
package/mobile/ui.tsx
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/** Small shared UI primitives (kept in one file to limit screen boilerplate).
|
|
2
|
+
* All are theme-aware via useTheme()/useStyles(). */
|
|
3
|
+
import React, { useEffect, useMemo, useRef } from "react";
|
|
4
|
+
import {
|
|
5
|
+
Text, View, TextInput, Pressable, ActivityIndicator, StyleSheet, ViewStyle, TextStyle, LayoutChangeEvent, Animated,
|
|
6
|
+
} from "react-native";
|
|
7
|
+
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
8
|
+
import { space, shadow, type Palette } from "./tokens";
|
|
9
|
+
import { FONT, hanken } from "./fonts";
|
|
10
|
+
import { useTheme } from "./ThemeContext";
|
|
11
|
+
|
|
12
|
+
/** Approx height of the floating bottom tab bar (excludes the device inset, which
|
|
13
|
+
* the bar adds on top via useSafeAreaInsets). */
|
|
14
|
+
export const TAB_BAR_HEIGHT = 60;
|
|
15
|
+
|
|
16
|
+
/** Bottom padding for a scroll container so its last content clears the gesture
|
|
17
|
+
* bar — and, on the four tab screens, the floating tab bar too. Pass onTab=true
|
|
18
|
+
* for Home/Tickets/Visits/Customers. */
|
|
19
|
+
export function useBottomPad(onTab = false): number {
|
|
20
|
+
const insets = useSafeAreaInsets();
|
|
21
|
+
return (onTab ? TAB_BAR_HEIGHT : space.lg) + insets.bottom;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** IBM Plex Mono for data (IDs, time, distances, GPS) — loaded in App.tsx. */
|
|
25
|
+
export const MONO = FONT.mono;
|
|
26
|
+
|
|
27
|
+
function makeStyles(c: Palette) {
|
|
28
|
+
return StyleSheet.create({
|
|
29
|
+
card: { backgroundColor: c.surface, borderRadius: 12, borderWidth: 1, borderColor: c.lineStrong, padding: space.lg, marginBottom: space.md },
|
|
30
|
+
btn: { paddingVertical: 12, paddingHorizontal: 16, borderRadius: 10, borderWidth: 1, alignItems: "center", justifyContent: "center", minHeight: 46 },
|
|
31
|
+
btnText: { fontSize: 15, fontWeight: "600", fontFamily: hanken("600") },
|
|
32
|
+
label: { fontSize: 13, color: c.muted, marginBottom: 4, fontWeight: "600", fontFamily: hanken("600") },
|
|
33
|
+
input: { backgroundColor: c.surface2, borderWidth: 1, borderColor: c.lineStrong, borderRadius: 10, paddingHorizontal: 12, paddingVertical: 10, fontSize: 15, color: c.text, fontFamily: hanken("400") },
|
|
34
|
+
badge: { paddingHorizontal: 8, paddingVertical: 2, borderRadius: 999, borderWidth: 1, alignSelf: "flex-start" },
|
|
35
|
+
h1: { fontSize: 22, fontWeight: "700", color: c.text, fontFamily: hanken("700") },
|
|
36
|
+
title: { fontSize: 16, fontWeight: "700", color: c.text, fontFamily: hanken("700") },
|
|
37
|
+
cbox: { width: 24, height: 24, borderRadius: 7, borderWidth: 2, borderColor: c.lineStrong, alignItems: "center", justifyContent: "center", marginRight: space.sm },
|
|
38
|
+
swTrack: { width: 46, height: 28, borderRadius: 999, justifyContent: "center" },
|
|
39
|
+
swKnob: { position: "absolute", width: 22, height: 22, borderRadius: 11, backgroundColor: "#ffffff", shadowColor: "#000", shadowOpacity: 0.3, shadowRadius: 2, shadowOffset: { width: 0, height: 1 }, elevation: 2 },
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type Styles = ReturnType<typeof makeStyles>;
|
|
44
|
+
|
|
45
|
+
/** Theme-aware shared stylesheet — `const styles = useStyles()`. */
|
|
46
|
+
export function useStyles(): Styles {
|
|
47
|
+
const { colors } = useTheme();
|
|
48
|
+
return useMemo(() => makeStyles(colors), [colors]);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function Card({ children, style, onLayout }: { children: React.ReactNode; style?: ViewStyle; onLayout?: (e: LayoutChangeEvent) => void }) {
|
|
52
|
+
const styles = useStyles();
|
|
53
|
+
const { scheme } = useTheme();
|
|
54
|
+
return <View style={[styles.card, shadow[scheme], style]} onLayout={onLayout}>{children}</View>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function Btn({
|
|
58
|
+
title, onPress, loading, disabled, variant = "primary",
|
|
59
|
+
}: { title: string; onPress: () => void; loading?: boolean; disabled?: boolean; variant?: "primary" | "outline" | "danger" | "signal" }) {
|
|
60
|
+
const { colors } = useTheme();
|
|
61
|
+
const styles = useStyles();
|
|
62
|
+
const isDisabled = disabled || loading;
|
|
63
|
+
const bg = variant === "primary" ? colors.primary : variant === "danger" ? colors.danger : variant === "signal" ? colors.signal : "transparent";
|
|
64
|
+
const fg = variant === "outline" ? colors.primary : "#ffffff";
|
|
65
|
+
return (
|
|
66
|
+
<Pressable
|
|
67
|
+
onPress={onPress}
|
|
68
|
+
disabled={isDisabled}
|
|
69
|
+
style={({ pressed }) => [
|
|
70
|
+
styles.btn,
|
|
71
|
+
{ backgroundColor: bg, borderColor: variant === "outline" ? colors.primary : bg, opacity: isDisabled ? 0.5 : pressed ? 0.85 : 1 },
|
|
72
|
+
]}
|
|
73
|
+
>
|
|
74
|
+
{loading ? <ActivityIndicator color={fg} /> : <Text style={[styles.btnText, { color: fg }]}>{title}</Text>}
|
|
75
|
+
</Pressable>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function Field({
|
|
80
|
+
label, value, onChangeText, placeholder, secureTextEntry, keyboardType, multiline, autoCapitalize,
|
|
81
|
+
}: {
|
|
82
|
+
label?: string; value: string; onChangeText: (t: string) => void; placeholder?: string;
|
|
83
|
+
secureTextEntry?: boolean; keyboardType?: "default" | "numeric" | "email-address"; multiline?: boolean;
|
|
84
|
+
autoCapitalize?: "none" | "sentences";
|
|
85
|
+
}) {
|
|
86
|
+
const { colors } = useTheme();
|
|
87
|
+
const styles = useStyles();
|
|
88
|
+
return (
|
|
89
|
+
<View style={{ marginBottom: space.md }}>
|
|
90
|
+
{label ? <Text style={styles.label}>{label}</Text> : null}
|
|
91
|
+
<TextInput
|
|
92
|
+
style={[styles.input, multiline && { height: 96, textAlignVertical: "top" }]}
|
|
93
|
+
value={value}
|
|
94
|
+
onChangeText={onChangeText}
|
|
95
|
+
placeholder={placeholder}
|
|
96
|
+
placeholderTextColor={colors.textFaint}
|
|
97
|
+
secureTextEntry={secureTextEntry}
|
|
98
|
+
keyboardType={keyboardType}
|
|
99
|
+
multiline={multiline}
|
|
100
|
+
autoCapitalize={autoCapitalize}
|
|
101
|
+
/>
|
|
102
|
+
</View>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function Badge({ label, color, filled }: { label: string; color: string; filled?: boolean }) {
|
|
107
|
+
const styles = useStyles();
|
|
108
|
+
return (
|
|
109
|
+
<View style={[styles.badge, filled ? { backgroundColor: color, borderColor: color } : { backgroundColor: color + "22", borderColor: color }]}>
|
|
110
|
+
<Text style={{ color: filled ? "#ffffff" : color, fontSize: 12, fontWeight: "600" }}>{label}</Text>
|
|
111
|
+
</View>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function Row({ children, style }: { children: React.ReactNode; style?: ViewStyle }) {
|
|
116
|
+
return <View style={[{ flexDirection: "row", alignItems: "center", gap: space.sm }, style]}>{children}</View>;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Compact in-screen segmented tab control (not a navigator) — pass the active
|
|
120
|
+
* key and the options; renders an evenly-split pill row. */
|
|
121
|
+
export function Segmented<T extends string>({ options, value, onChange }: { options: { key: T; label: string }[]; value: T; onChange: (k: T) => void }) {
|
|
122
|
+
const { colors } = useTheme();
|
|
123
|
+
return (
|
|
124
|
+
<View style={{ flexDirection: "row", backgroundColor: colors.surface2, borderWidth: 1, borderColor: colors.border, borderRadius: 10, padding: 3 }}>
|
|
125
|
+
{options.map((o) => {
|
|
126
|
+
const on = o.key === value;
|
|
127
|
+
return (
|
|
128
|
+
<Pressable key={o.key} onPress={() => onChange(o.key)}
|
|
129
|
+
style={{ flex: 1, paddingVertical: 8, paddingHorizontal: 2, borderRadius: 8, alignItems: "center", justifyContent: "center", backgroundColor: on ? colors.surface : "transparent", borderWidth: 1, borderColor: on ? colors.border : "transparent" }}>
|
|
130
|
+
<Text numberOfLines={1} adjustsFontSizeToFit minimumFontScale={0.85} style={{ color: on ? colors.primary : colors.textFaint, fontWeight: on ? "700" : "600", fontSize: 13, fontFamily: hanken(on ? "700" : "600") }}>{o.label}</Text>
|
|
131
|
+
</Pressable>
|
|
132
|
+
);
|
|
133
|
+
})}
|
|
134
|
+
</View>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function Muted({ children, style }: { children: React.ReactNode; style?: TextStyle }) {
|
|
139
|
+
const { colors } = useTheme();
|
|
140
|
+
return <Text style={[{ color: colors.muted, fontSize: 13, fontFamily: FONT.r }, style]}>{children}</Text>;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Monospace data span (tracking IDs, time, distances, coordinates). */
|
|
144
|
+
export function Mono({ children, style }: { children: React.ReactNode; style?: TextStyle }) {
|
|
145
|
+
const { colors } = useTheme();
|
|
146
|
+
return <Text style={[{ fontFamily: MONO, color: colors.text }, style]}>{children}</Text>;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Shared checkbox — replaces the hand-rolled 26px boxes duplicated across screens. */
|
|
150
|
+
export function Checkbox({ checked, label, sublabel, onToggle }: { checked: boolean; label: string; sublabel?: string; onToggle: () => void }) {
|
|
151
|
+
const { colors } = useTheme();
|
|
152
|
+
const styles = useStyles();
|
|
153
|
+
return (
|
|
154
|
+
<Pressable onPress={onToggle} hitSlop={8} style={{ flexDirection: "row", alignItems: "center", paddingVertical: 8 }}>
|
|
155
|
+
<View style={[styles.cbox, checked && { backgroundColor: colors.primary, borderColor: colors.primary }]}>
|
|
156
|
+
{checked ? <Text style={{ color: "#ffffff", fontSize: 15, fontWeight: "800" }}>✓</Text> : null}
|
|
157
|
+
</View>
|
|
158
|
+
<View style={{ flexShrink: 1 }}>
|
|
159
|
+
<Text style={{ color: colors.text, fontSize: 15 }}>{label}</Text>
|
|
160
|
+
{sublabel ? <Text style={{ color: colors.muted, fontSize: 12 }}>{sublabel}</Text> : null}
|
|
161
|
+
</View>
|
|
162
|
+
</Pressable>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Pulsing placeholder block for loading states (replaces blank spinners). */
|
|
167
|
+
export function Skeleton({ height = 14, width = "100%", style }: { height?: number; width?: ViewStyle["width"]; style?: ViewStyle }) {
|
|
168
|
+
const { colors } = useTheme();
|
|
169
|
+
const op = useRef(new Animated.Value(0.4)).current;
|
|
170
|
+
useEffect(() => {
|
|
171
|
+
const loop = Animated.loop(Animated.sequence([
|
|
172
|
+
Animated.timing(op, { toValue: 0.85, duration: 700, useNativeDriver: true }),
|
|
173
|
+
Animated.timing(op, { toValue: 0.4, duration: 700, useNativeDriver: true }),
|
|
174
|
+
]));
|
|
175
|
+
loop.start();
|
|
176
|
+
return () => loop.stop();
|
|
177
|
+
}, [op]);
|
|
178
|
+
return <Animated.View style={[{ height, width, borderRadius: 7, backgroundColor: colors.surface2, opacity: op }, style]} />;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** iOS-style switch — for settings toggles (on/off state, not a form choice). */
|
|
182
|
+
export function Switch({ value, onToggle }: { value: boolean; onToggle: () => void }) {
|
|
183
|
+
const { colors } = useTheme();
|
|
184
|
+
const styles = useStyles();
|
|
185
|
+
return (
|
|
186
|
+
<Pressable onPress={onToggle} hitSlop={8} style={[styles.swTrack, { backgroundColor: value ? colors.primary : colors.lineStrong }]}>
|
|
187
|
+
<View style={[styles.swKnob, { left: value ? 21 : 3 }]} />
|
|
188
|
+
</Pressable>
|
|
189
|
+
);
|
|
190
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aria-framework/theme",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Aria App Framework — theme module. Field Ops Console design system: tokens.json single source of record + a generator that emits a Bootstrap 5.3 web theme (theme.css + self-hosted fonts) and a React Native palette/provider/primitives. Light + dark.",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"private": false,
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"access": "public"
|
|
9
|
+
},
|
|
10
|
+
"main": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "node build.js && tsc",
|
|
14
|
+
"prepare": "npm run build"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"web",
|
|
18
|
+
"dist",
|
|
19
|
+
"mobile",
|
|
20
|
+
"tokens.json",
|
|
21
|
+
"build.js",
|
|
22
|
+
"tsconfig.json",
|
|
23
|
+
"types"
|
|
24
|
+
],
|
|
25
|
+
"exports": {
|
|
26
|
+
"./theme.css": "./web/theme.css",
|
|
27
|
+
"./mobile": { "types": "./dist/index.d.ts", "default": "./dist/index.js" },
|
|
28
|
+
"./tokens": { "types": "./dist/tokens.d.ts", "default": "./dist/tokens.js" },
|
|
29
|
+
"./fonts": { "types": "./dist/fonts.d.ts", "default": "./dist/fonts.js" },
|
|
30
|
+
"./ThemeContext": { "types": "./dist/ThemeContext.d.ts", "default": "./dist/ThemeContext.js" },
|
|
31
|
+
"./ui": { "types": "./dist/ui.d.ts", "default": "./dist/ui.js" }
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"typescript": "^5.6.0",
|
|
35
|
+
"@types/react": "^18.3.0"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"react": "*",
|
|
39
|
+
"react-native": "*",
|
|
40
|
+
"@react-native-async-storage/async-storage": "*"
|
|
41
|
+
},
|
|
42
|
+
"peerDependenciesMeta": {
|
|
43
|
+
"react": { "optional": true },
|
|
44
|
+
"react-native": { "optional": true },
|
|
45
|
+
"@react-native-async-storage/async-storage": { "optional": true }
|
|
46
|
+
}
|
|
47
|
+
}
|
package/tokens.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "SINGLE SOURCE OF RECORD for Field Ops Console theme tokens. Edit values HERE, then run `node build.js` to regenerate web/theme.css and mobile/tokens.ts. Do not hand-edit the generated files. Hex stored uppercase; build.js lowercases for the RN palette. --brand-rgb is derived from brand. Web '--line/--line-strong' map to RN 'border/lineStrong'; '--text-soft' maps to RN 'muted'; '--paper' to RN 'bg'; '--brand' to RN 'primary'.",
|
|
3
|
+
"palettes": {
|
|
4
|
+
"light": {
|
|
5
|
+
"brand": "#0E7A6E", "brandDeep": "#0A5C53", "brandBright": "#15B8A6",
|
|
6
|
+
"signal": "#E2710B",
|
|
7
|
+
"paper": "#F4F5F2", "surface": "#FFFFFF", "surface2": "#FAFBF9",
|
|
8
|
+
"line": "#DCDAD2", "lineStrong": "#CBC8BC",
|
|
9
|
+
"text": "#18222A", "textSoft": "#5C6770", "textFaint": "#929BA1",
|
|
10
|
+
"primaryText": "#FFFFFF",
|
|
11
|
+
"danger": "#DC2626", "success": "#0E9E6E",
|
|
12
|
+
"warnBg": "#FCEFD9", "warnText": "#8A4B08",
|
|
13
|
+
"brandTint": "rgba(14,122,110,0.10)", "signalTint": "rgba(226,113,11,0.12)",
|
|
14
|
+
"shadowWeb": "0 1px 2px rgba(16,24,28,.05), 0 4px 14px -6px rgba(16,24,28,.12)",
|
|
15
|
+
"shadowLgWeb": "0 18px 50px -18px rgba(16,24,28,.30)",
|
|
16
|
+
"shadowRn": { "shadowColor": "#101820", "shadowOpacity": 0.08, "shadowRadius": 8, "shadowOffset": { "width": 0, "height": 2 }, "elevation": 2 }
|
|
17
|
+
},
|
|
18
|
+
"dark": {
|
|
19
|
+
"brand": "#16B5A3", "brandDeep": "#0E7A6E", "brandBright": "#2FE0CC",
|
|
20
|
+
"signal": "#F2871F",
|
|
21
|
+
"paper": "#0C1215", "surface": "#141C21", "surface2": "#192329",
|
|
22
|
+
"line": "#243038", "lineStrong": "#3A4854",
|
|
23
|
+
"text": "#E7EDEF", "textSoft": "#9BA7AE", "textFaint": "#69767D",
|
|
24
|
+
"primaryText": "#08110F",
|
|
25
|
+
"danger": "#F05252", "success": "#22B583",
|
|
26
|
+
"warnBg": "#2A2114", "warnText": "#F2C879",
|
|
27
|
+
"brandTint": "rgba(22,181,163,0.14)", "signalTint": "rgba(242,135,31,0.16)",
|
|
28
|
+
"shadowWeb": "0 1px 2px rgba(0,0,0,.3), 0 8px 24px -10px rgba(0,0,0,.6)",
|
|
29
|
+
"shadowLgWeb": "0 24px 60px -18px rgba(0,0,0,.8)",
|
|
30
|
+
"shadowRn": { "shadowColor": "#000000", "shadowOpacity": 0.45, "shadowRadius": 10, "shadowOffset": { "width": 0, "height": 3 }, "elevation": 3 }
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"status": { "new": "#2563eb", "open": "#0e8fa8", "pending": "#d9870b", "resolved": "#0e9e6e", "closed": "#7a848b" },
|
|
34
|
+
"priority": { "low": "#9aa6ae", "medium": "#2563eb", "high": "#ea580c", "critical": "#dc2626" },
|
|
35
|
+
"space": { "xs": 4, "sm": 8, "md": 12, "lg": 16, "xl": 24 }
|
|
36
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2019",
|
|
4
|
+
"module": "CommonJS",
|
|
5
|
+
"moduleResolution": "node",
|
|
6
|
+
"jsx": "react-jsx",
|
|
7
|
+
"declaration": true,
|
|
8
|
+
"outDir": "dist",
|
|
9
|
+
"rootDir": "mobile",
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"strict": false,
|
|
13
|
+
"noImplicitAny": false,
|
|
14
|
+
"types": ["react"]
|
|
15
|
+
},
|
|
16
|
+
"include": ["mobile/**/*", "types/**/*"]
|
|
17
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Ambient module shims so the mobile sources compile to dist (.js + .d.ts) without
|
|
2
|
+
// installing all of React Native here. The consuming app provides the real
|
|
3
|
+
// react-native / async-storage / safe-area-context (peer deps); their types govern
|
|
4
|
+
// at the call site. Only @types/react is a real devDependency (for JSX / ReactNode).
|
|
5
|
+
//
|
|
6
|
+
// Exports are typed `any` — enough to emit; the public API (Palette, tokens, props)
|
|
7
|
+
// stays correctly typed from the package's own source. Add to the lists below if a
|
|
8
|
+
// mobile source imports a new symbol from these modules.
|
|
9
|
+
declare module "react-native" {
|
|
10
|
+
export type ViewStyle = any;
|
|
11
|
+
export type TextStyle = any;
|
|
12
|
+
export type LayoutChangeEvent = any;
|
|
13
|
+
export const Text: any;
|
|
14
|
+
export const View: any;
|
|
15
|
+
export const TextInput: any;
|
|
16
|
+
export const Pressable: any;
|
|
17
|
+
export const ActivityIndicator: any;
|
|
18
|
+
export const StyleSheet: any;
|
|
19
|
+
export const Animated: any;
|
|
20
|
+
export const Appearance: any;
|
|
21
|
+
}
|
|
22
|
+
declare module "@react-native-async-storage/async-storage" {
|
|
23
|
+
const AsyncStorage: any;
|
|
24
|
+
export default AsyncStorage;
|
|
25
|
+
}
|
|
26
|
+
declare module "react-native-safe-area-context" {
|
|
27
|
+
export const useSafeAreaInsets: any;
|
|
28
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|