@sincpro/mobile 0.1.1 → 0.3.1
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 +831 -47
- package/dist/entrypoints/queue/printImage.subscriber.js +3 -3
- package/dist/entrypoints/ui/AppShell.d.ts +61 -0
- package/dist/entrypoints/ui/AppShell.js +72 -11
- package/dist/entrypoints/ui/common_provider.d.ts +6 -1
- package/dist/entrypoints/ui/common_provider.js +45 -1
- package/dist/entrypoints/ui/domain_switcher.js +5 -4
- package/dist/entrypoints/ui/theme.d.ts +8 -7
- package/dist/entrypoints/ui/theme.js +8 -7
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/infrastructure/ui/ToastHost.d.ts +6 -1
- package/dist/infrastructure/ui/ToastHost.js +18 -9
- package/dist/services/printer.service.d.ts +6 -0
- package/dist/services/printer.service.js +36 -3
- package/dist/ui/components/atoms/DebugBanner.js +4 -8
- package/dist/ui/components/atoms/PrinterIcon.d.ts +4 -0
- package/dist/ui/components/atoms/PrinterIcon.js +4 -0
- package/dist/ui/components/atoms/index.d.ts +1 -0
- package/dist/ui/components/atoms/index.js +1 -0
- package/dist/ui/components/molecules/BluetoothDeviceSelectorModal.js +16 -10
- package/dist/ui/components/molecules/DeadLetterErrorBlock.js +3 -2
- package/dist/ui/components/molecules/EventTimelineItem.js +31 -33
- package/dist/ui/components/molecules/GeoPermissionCard.d.ts +8 -0
- package/dist/ui/components/molecules/GeoPermissionCard.js +21 -0
- package/dist/ui/components/molecules/ProcessToast.d.ts +7 -4
- package/dist/ui/components/molecules/ProcessToast.js +45 -44
- package/dist/ui/components/molecules/ProcessToastProvider.js +1 -1
- package/dist/ui/components/molecules/TimeZoneSelector.d.ts +7 -0
- package/dist/ui/components/molecules/TimeZoneSelector.js +19 -0
- package/dist/ui/components/molecules/TimeZoneSelectorModal.d.ts +9 -0
- package/dist/ui/components/molecules/TimeZoneSelectorModal.js +61 -0
- package/dist/ui/components/molecules/index.d.ts +4 -0
- package/dist/ui/components/molecules/index.js +3 -0
- package/dist/ui/components/molecules/timezone.d.ts +4 -0
- package/dist/ui/components/molecules/timezone.js +1 -0
- package/dist/ui/components/organisms/BluetoothPrinterSelector.d.ts +3 -1
- package/dist/ui/components/organisms/BluetoothPrinterSelector.js +29 -9
- package/dist/ui/components/organisms/DatabaseInfoRow.js +1 -1
- package/dist/ui/components/organisms/DeadLetterQueueRow.js +3 -3
- package/dist/ui/components/organisms/EventRow.js +15 -15
- package/dist/ui/components/organisms/InjectableWebView.js +20 -10
- package/dist/ui/components/organisms/TableInfoInfoRow.js +1 -1
- package/dist/ui/screens/database/database.json_detail.js +3 -3
- package/dist/ui/screens/database/database.list.js +4 -5
- package/dist/ui/screens/database/database.table_rows.js +1 -2
- package/dist/ui/screens/dead_letter_queue/dead_letter_queue.list.js +4 -5
- package/dist/ui/screens/events/events.list.js +4 -5
- package/package.json +9 -5
- package/dist/ui/layouts/router_layouts.d.ts +0 -17
- package/dist/ui/layouts/router_layouts.js +0 -20
|
@@ -6,9 +6,9 @@ export class PrintImageSubscriber extends Subscriber {
|
|
|
6
6
|
requiresAuth = false;
|
|
7
7
|
listen = [PrintImageRequestedEvent];
|
|
8
8
|
async process(event) {
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
const printed = await printerService.printImageBase64(event.imageBase64);
|
|
10
|
+
if (!printed) {
|
|
11
|
+
throw new DomainValidationError("Print failed — printer not connected or timed out");
|
|
11
12
|
}
|
|
12
|
-
await printerService.printImageBase64(event.imageBase64);
|
|
13
13
|
}
|
|
14
14
|
}
|
|
@@ -1,15 +1,76 @@
|
|
|
1
1
|
import type { DomainModule } from "../../framework/domain_module";
|
|
2
|
+
import { type BrandingConfig } from "@sincpro/mobile-ui";
|
|
2
3
|
import { type ThemeTokens } from "@sincpro/mobile-ui/theme";
|
|
3
4
|
import { type ComponentType, type ReactNode } from "react";
|
|
4
5
|
type ProviderComponent = ComponentType<{
|
|
5
6
|
children: ReactNode;
|
|
6
7
|
}>;
|
|
8
|
+
export interface BrandFontWeights {
|
|
9
|
+
light?: string;
|
|
10
|
+
regular?: string;
|
|
11
|
+
medium?: string;
|
|
12
|
+
semiBold?: string;
|
|
13
|
+
bold?: string;
|
|
14
|
+
extraBold?: string;
|
|
15
|
+
black?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface BrandFontFamilies {
|
|
18
|
+
/** df — Display & headings. Brand character font. (Default: Inter) */
|
|
19
|
+
display?: BrandFontWeights;
|
|
20
|
+
/** bf — Body, labels, buttons. Interface text 13–15px. (Default: Inter) */
|
|
21
|
+
body?: BrandFontWeights;
|
|
22
|
+
/** cf — Captions, overlines, metadata. Uppercase + tracking. (Default: Fira Code) */
|
|
23
|
+
caption?: Pick<BrandFontWeights, "regular" | "medium">;
|
|
24
|
+
/** mono — Numeric data, SKU, amounts. Fixed-width. (Default: Fira Code) */
|
|
25
|
+
mono?: Pick<BrandFontWeights, "regular" | "medium">;
|
|
26
|
+
}
|
|
27
|
+
export interface BrandFontConfig {
|
|
28
|
+
/** Metro asset map: `{ "Satoshi-Bold": require("./assets/fonts/Satoshi-Bold.otf") }` */
|
|
29
|
+
files: Record<string, string | number>;
|
|
30
|
+
/** Semantic font family slots aligned with the brand manual (df / bf / cf / mono). */
|
|
31
|
+
families: BrandFontFamilies;
|
|
32
|
+
}
|
|
7
33
|
export interface AppShellConfig {
|
|
34
|
+
/**
|
|
35
|
+
* Domain modules to bootstrap. Each module registers its own repositories,
|
|
36
|
+
* migrations, subscribers, and cron workers. The built-in `baseModule` is
|
|
37
|
+
* always included automatically — do not pass it here.
|
|
38
|
+
*/
|
|
8
39
|
domains: DomainModule[];
|
|
40
|
+
/**
|
|
41
|
+
* Map of domain key → screen component. Domains without a matching entry
|
|
42
|
+
* here are bootstrapped (events, repos, crons) but have no UI — they run
|
|
43
|
+
* silently in the background. Must contain at least `ui[activeDomain]`.
|
|
44
|
+
*/
|
|
9
45
|
ui: Record<string, ComponentType>;
|
|
46
|
+
/**
|
|
47
|
+
* Key of the domain whose UI is shown on launch. Must match one of the
|
|
48
|
+
* keys in `domains` AND have a corresponding entry in `ui`.
|
|
49
|
+
*/
|
|
10
50
|
activeDomain: string;
|
|
51
|
+
/**
|
|
52
|
+
* Additional React context providers to wrap around the app. Providers are
|
|
53
|
+
* applied in array order, outermost last — i.e. the last element in the
|
|
54
|
+
* array becomes the outermost wrapper and can observe all others.
|
|
55
|
+
*
|
|
56
|
+
* Example: `[ConfirmationProvider, ProcessToastProvider]`
|
|
57
|
+
*/
|
|
11
58
|
providers?: ProviderComponent[];
|
|
59
|
+
/** Light theme tokens. Defaults to the DS base theme if omitted. */
|
|
12
60
|
theme?: ThemeTokens;
|
|
61
|
+
/** Dark theme tokens. When provided, the system color-scheme toggle activates it. */
|
|
62
|
+
darkTheme?: ThemeTokens;
|
|
63
|
+
branding?: BrandingConfig;
|
|
64
|
+
/**
|
|
65
|
+
* Brand typeface declared as semantic slots (df / bf / cf / mono).
|
|
66
|
+
* The app owns all font asset files; the framework wires them into DS roles.
|
|
67
|
+
* Omitting a slot keeps the DS default (Inter for body, Fira Code for mono).
|
|
68
|
+
*/
|
|
69
|
+
brandFont?: BrandFontConfig;
|
|
70
|
+
/** Component rendered while fonts + domain init complete. */
|
|
71
|
+
splashComponent?: ComponentType;
|
|
72
|
+
/** Minimum time (ms) the splash stays visible. Default 2000. */
|
|
73
|
+
splashDuration?: number;
|
|
13
74
|
}
|
|
14
75
|
export declare function createAppShell(config: AppShellConfig): ComponentType;
|
|
15
76
|
export {};
|
|
@@ -1,18 +1,61 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "nativewind/jsx-runtime";
|
|
2
|
+
import { DefaultTheme as NavDefaultTheme, NavigationContainer, } from "@react-navigation/native";
|
|
2
3
|
import { createApp } from "../../framework/createApp";
|
|
3
4
|
import { ToastHost } from "../../infrastructure/ui/ToastHost";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
5
|
+
import { setBranding } from "@sincpro/mobile-ui";
|
|
6
|
+
import { ToastProvider } from "@sincpro/mobile-ui/Feedback";
|
|
7
|
+
import { setActiveTheme, themeToVars, useTheme, } from "@sincpro/mobile-ui/theme";
|
|
8
|
+
import { configureFonts, useFonts } from "@sincpro/mobile-ui/theme/typography";
|
|
6
9
|
import * as SplashScreen from "expo-splash-screen";
|
|
7
10
|
import { vars } from "nativewind";
|
|
8
11
|
import { useEffect, useState } from "react";
|
|
9
12
|
import { View } from "react-native";
|
|
10
13
|
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
|
11
14
|
import { KeyboardProvider } from "react-native-keyboard-controller";
|
|
12
|
-
import { initialWindowMetrics, SafeAreaProvider } from "react-native-safe-area-context";
|
|
13
|
-
import { NativeRouter } from "react-router-native";
|
|
15
|
+
import { initialWindowMetrics, SafeAreaProvider, SafeAreaView, } from "react-native-safe-area-context";
|
|
14
16
|
import { CommonProvider } from "./common_provider";
|
|
15
17
|
import { ActiveDomainApp, DomainSwitcherProvider } from "./domain_switcher";
|
|
18
|
+
/** Maps the semantic families object to the low-level DS FontRole record. */
|
|
19
|
+
function familiesToRoles(families) {
|
|
20
|
+
const roles = {};
|
|
21
|
+
const { display, body, caption, mono } = families;
|
|
22
|
+
if (display) {
|
|
23
|
+
if (display.regular)
|
|
24
|
+
roles.titleRegular = display.regular;
|
|
25
|
+
if (display.medium)
|
|
26
|
+
roles.titleMedium = display.medium;
|
|
27
|
+
if (display.bold)
|
|
28
|
+
roles.title = display.bold;
|
|
29
|
+
if (display.black)
|
|
30
|
+
roles.titleBlack = display.black;
|
|
31
|
+
}
|
|
32
|
+
if (body) {
|
|
33
|
+
if (body.light)
|
|
34
|
+
roles.light = body.light;
|
|
35
|
+
if (body.regular)
|
|
36
|
+
roles.regular = body.regular;
|
|
37
|
+
if (body.medium)
|
|
38
|
+
roles.medium = body.medium;
|
|
39
|
+
if (body.semiBold)
|
|
40
|
+
roles.semiBold = body.semiBold;
|
|
41
|
+
if (body.extraBold)
|
|
42
|
+
roles.extraBold = body.extraBold;
|
|
43
|
+
}
|
|
44
|
+
if (caption) {
|
|
45
|
+
if (caption.regular)
|
|
46
|
+
roles.mono = caption.regular;
|
|
47
|
+
if (caption.medium)
|
|
48
|
+
roles.monoMedium = caption.medium;
|
|
49
|
+
}
|
|
50
|
+
if (mono) {
|
|
51
|
+
if (mono.regular)
|
|
52
|
+
roles.mono = mono.regular;
|
|
53
|
+
if (mono.medium)
|
|
54
|
+
roles.monoMedium = mono.medium;
|
|
55
|
+
}
|
|
56
|
+
return roles;
|
|
57
|
+
}
|
|
58
|
+
/** Wraps children in the provider list. reduceRight → first item = innermost wrapper. */
|
|
16
59
|
function withProviders(providers, children) {
|
|
17
60
|
return providers.reduceRight((acc, Provider) => _jsx(Provider, { children: acc }), children);
|
|
18
61
|
}
|
|
@@ -28,10 +71,23 @@ export function createAppShell(config) {
|
|
|
28
71
|
if (config.theme) {
|
|
29
72
|
setActiveTheme(config.theme);
|
|
30
73
|
}
|
|
31
|
-
|
|
74
|
+
if (config.branding) {
|
|
75
|
+
setBranding(config.branding);
|
|
76
|
+
}
|
|
77
|
+
// Wire brand font roles into the DS before first render.
|
|
78
|
+
if (config.brandFont?.families) {
|
|
79
|
+
configureFonts(familiesToRoles(config.brandFont.families));
|
|
80
|
+
}
|
|
32
81
|
return function AppShell() {
|
|
33
82
|
const [isReady, setIsReady] = useState(false);
|
|
34
|
-
const
|
|
83
|
+
const [minDurationMet, setMinDurationMet] = useState(false);
|
|
84
|
+
const [brandFontsLoaded] = useFonts(config.brandFont?.files ?? {});
|
|
85
|
+
const activeTheme = useTheme();
|
|
86
|
+
const themeStyle = vars(themeToVars(activeTheme));
|
|
87
|
+
const navTheme = {
|
|
88
|
+
...NavDefaultTheme,
|
|
89
|
+
colors: { ...NavDefaultTheme.colors, background: activeTheme.bg.page },
|
|
90
|
+
};
|
|
35
91
|
useEffect(() => {
|
|
36
92
|
let active = true;
|
|
37
93
|
createApp({ domains: config.domains }).then(() => {
|
|
@@ -46,13 +102,18 @@ export function createAppShell(config) {
|
|
|
46
102
|
SplashScreen.preventAutoHideAsync().catch(() => { });
|
|
47
103
|
}, []);
|
|
48
104
|
useEffect(() => {
|
|
49
|
-
|
|
105
|
+
const t = setTimeout(() => setMinDurationMet(true), config.splashDuration ?? 2000);
|
|
106
|
+
return () => clearTimeout(t);
|
|
107
|
+
}, []);
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
if (brandFontsLoaded && isReady && minDurationMet) {
|
|
50
110
|
SplashScreen.hideAsync().catch(() => { });
|
|
51
111
|
}
|
|
52
|
-
}, [
|
|
53
|
-
if (!isReady || !
|
|
54
|
-
|
|
112
|
+
}, [brandFontsLoaded, isReady, minDurationMet]);
|
|
113
|
+
if (!isReady || !brandFontsLoaded || !minDurationMet) {
|
|
114
|
+
const Splash = config.splashComponent;
|
|
115
|
+
return Splash ? (_jsx(Splash, {})) : (_jsx(View, { style: { flex: 1, backgroundColor: activeTheme.bg.page } }));
|
|
55
116
|
}
|
|
56
|
-
return (_jsx(GestureHandlerRootView, { style: { flex: 1 }, children: _jsx(View, { className: "flex-1", style: themeStyle, children: _jsx(SafeAreaProvider, { initialMetrics: initialWindowMetrics, children: _jsx(KeyboardProvider, { children:
|
|
117
|
+
return (_jsx(GestureHandlerRootView, { style: { flex: 1 }, children: _jsx(View, { className: "flex-1", style: themeStyle, children: _jsx(SafeAreaProvider, { initialMetrics: initialWindowMetrics, children: _jsx(SafeAreaView, { edges: ["top"], style: { flex: 1 }, children: _jsx(KeyboardProvider, { children: _jsx(CommonProvider, { darkTheme: config.darkTheme, lightTheme: config.theme, children: _jsxs(ToastProvider, { position: "top", children: [withProviders(providers, _jsx(DomainSwitcherProvider, { apps: apps, initialDomain: config.activeDomain, children: _jsx(NavigationContainer, { theme: navTheme, children: _jsx(ActiveDomainApp, {}) }) })), _jsx(ToastHost, {})] }) }) }) }) }) }) }));
|
|
57
118
|
};
|
|
58
119
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ThemeTokens } from "@sincpro/mobile-ui/theme";
|
|
1
2
|
import { ReactNode } from "react";
|
|
2
3
|
export interface TimezoneLocale {
|
|
3
4
|
timezone: string;
|
|
@@ -26,10 +27,14 @@ interface ICommonContext {
|
|
|
26
27
|
requestGeoPermission: () => Promise<boolean>;
|
|
27
28
|
timezone: string | null;
|
|
28
29
|
updateTimezone: (tz: TimezoneLocale) => Promise<void>;
|
|
30
|
+
colorScheme: "light" | "dark" | "system";
|
|
31
|
+
setColorScheme: (scheme: "light" | "dark" | "system") => Promise<void>;
|
|
29
32
|
}
|
|
30
33
|
interface CommonProviderProps {
|
|
31
34
|
children: ReactNode;
|
|
35
|
+
lightTheme?: ThemeTokens;
|
|
36
|
+
darkTheme?: ThemeTokens;
|
|
32
37
|
}
|
|
33
|
-
export declare function CommonProvider({ children }: CommonProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
38
|
+
export declare function CommonProvider({ children, lightTheme, darkTheme }: CommonProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
34
39
|
export declare function useCommon(): ICommonContext;
|
|
35
40
|
export {};
|
|
@@ -4,13 +4,16 @@ import { SettingsRepository } from "../../adapters/repositories/setting.reposito
|
|
|
4
4
|
import { QueueEndEvent, QueueStartEvent } from "../../domain/events";
|
|
5
5
|
import logger, { loggerUseCases } from "../../infrastructure/logger";
|
|
6
6
|
import { UIEventBus } from "../../infrastructure/ui/UIEventBus";
|
|
7
|
+
import { DEFAULT_DARK_THEME, DEFAULT_THEME, setActiveTheme } from "@sincpro/mobile-ui/theme";
|
|
7
8
|
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, } from "react";
|
|
9
|
+
import { Appearance, AppState } from "react-native";
|
|
8
10
|
const DEBUG_MODE_SETTING_KEY = "app.debug_mode";
|
|
9
11
|
const TIMEZONE_SETTING_KEY = "app.timezone";
|
|
10
12
|
const LOCALE_SETTING_KEY = "app.locale";
|
|
13
|
+
const COLOR_SCHEME_SETTING_KEY = "app.color_scheme";
|
|
11
14
|
const QUEUE_MIN_VISIBLE_MS = 1200;
|
|
12
15
|
const CommonContext = createContext(null);
|
|
13
|
-
export function CommonProvider({ children }) {
|
|
16
|
+
export function CommonProvider({ children, lightTheme, darkTheme }) {
|
|
14
17
|
const [debugMode, setDebugMode] = useState(false);
|
|
15
18
|
const [debugModeLoaded, setDebugModeLoaded] = useState(false);
|
|
16
19
|
const [currentActivity, setCurrentActivity] = useState(undefined);
|
|
@@ -21,6 +24,7 @@ export function CommonProvider({ children }) {
|
|
|
21
24
|
const [geoIsLoading, setGeoIsLoading] = useState(false);
|
|
22
25
|
const [geoError, setGeoError] = useState(null);
|
|
23
26
|
const [timezone, setTimezone] = useState(null);
|
|
27
|
+
const [colorScheme, setColorSchemeState] = useState("system");
|
|
24
28
|
const errorTimeoutRef = useRef(null);
|
|
25
29
|
const queueClearTimeoutRef = useRef(null);
|
|
26
30
|
useEffect(() => {
|
|
@@ -86,6 +90,42 @@ export function CommonProvider({ children }) {
|
|
|
86
90
|
logger.warn(error);
|
|
87
91
|
}
|
|
88
92
|
}, []);
|
|
93
|
+
const applyTheme = useCallback((scheme) => {
|
|
94
|
+
const resolved = scheme === "system" ? (Appearance.getColorScheme() ?? "light") : scheme;
|
|
95
|
+
setActiveTheme(resolved === "dark"
|
|
96
|
+
? (darkTheme ?? DEFAULT_DARK_THEME)
|
|
97
|
+
: (lightTheme ?? DEFAULT_THEME));
|
|
98
|
+
}, [lightTheme, darkTheme]);
|
|
99
|
+
const setColorScheme = useCallback(async (scheme) => {
|
|
100
|
+
await SettingsRepository.saveOneSetting(COLOR_SCHEME_SETTING_KEY, scheme);
|
|
101
|
+
setColorSchemeState(scheme);
|
|
102
|
+
applyTheme(scheme);
|
|
103
|
+
}, [applyTheme]);
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
checkGeoPermission();
|
|
106
|
+
const sub = AppState.addEventListener("change", (state) => {
|
|
107
|
+
if (state === "active")
|
|
108
|
+
checkGeoPermission();
|
|
109
|
+
});
|
|
110
|
+
return () => sub.remove();
|
|
111
|
+
}, [checkGeoPermission]);
|
|
112
|
+
// Load persisted color scheme on mount
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
async function loadColorScheme() {
|
|
115
|
+
const saved = await SettingsRepository.getSettingByName(COLOR_SCHEME_SETTING_KEY);
|
|
116
|
+
const scheme = saved === "light" || saved === "dark" || saved === "system" ? saved : "system";
|
|
117
|
+
setColorSchemeState(scheme);
|
|
118
|
+
applyTheme(scheme);
|
|
119
|
+
}
|
|
120
|
+
loadColorScheme();
|
|
121
|
+
}, [applyTheme]);
|
|
122
|
+
// Follow system changes when scheme is "system"
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
if (colorScheme !== "system")
|
|
125
|
+
return;
|
|
126
|
+
const sub = Appearance.addChangeListener(() => applyTheme("system"));
|
|
127
|
+
return () => sub.remove();
|
|
128
|
+
}, [colorScheme, applyTheme]);
|
|
89
129
|
useEffect(() => {
|
|
90
130
|
async function loadTimezone() {
|
|
91
131
|
try {
|
|
@@ -217,6 +257,8 @@ export function CommonProvider({ children }) {
|
|
|
217
257
|
requestGeoPermission,
|
|
218
258
|
timezone,
|
|
219
259
|
updateTimezone,
|
|
260
|
+
colorScheme,
|
|
261
|
+
setColorScheme,
|
|
220
262
|
}), [
|
|
221
263
|
debugMode,
|
|
222
264
|
debugModeLoaded,
|
|
@@ -232,6 +274,8 @@ export function CommonProvider({ children }) {
|
|
|
232
274
|
requestGeoPermission,
|
|
233
275
|
timezone,
|
|
234
276
|
updateTimezone,
|
|
277
|
+
colorScheme,
|
|
278
|
+
setColorScheme,
|
|
235
279
|
]);
|
|
236
280
|
return _jsx(CommonContext.Provider, { value: value, children: children });
|
|
237
281
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx } from "nativewind/jsx-runtime";
|
|
2
|
+
import { createNativeStackNavigator } from "@react-navigation/native-stack";
|
|
2
3
|
import { createContext, useContext, useState, } from "react";
|
|
4
|
+
const RootStack = createNativeStackNavigator();
|
|
3
5
|
const DomainSwitcherContext = createContext(null);
|
|
4
6
|
export function DomainSwitcherProvider(props) {
|
|
5
7
|
const [activeDomain, setActiveDomain] = useState(props.initialDomain);
|
|
@@ -14,9 +16,8 @@ export function useDomainSwitcher() {
|
|
|
14
16
|
}
|
|
15
17
|
export function ActiveDomainApp() {
|
|
16
18
|
const { activeDomain, apps } = useDomainSwitcher();
|
|
17
|
-
const
|
|
18
|
-
if (!
|
|
19
|
+
const currentApp = apps.find((entry) => entry.key === activeDomain);
|
|
20
|
+
if (!currentApp)
|
|
19
21
|
return null;
|
|
20
|
-
|
|
21
|
-
return _jsx(Component, {});
|
|
22
|
+
return (_jsx(RootStack.Navigator, { screenOptions: { headerShown: false }, children: _jsx(RootStack.Screen, { component: currentApp.component, name: currentApp.key }) }));
|
|
22
23
|
}
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { type DeepPartial, type ThemeTokens } from "@sincpro/mobile-ui/theme";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
* Build a fully-typed theme from a partial override object.
|
|
4
|
+
* Starts from the DS base theme and deep-merges your tokens on top.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
* theme: createTheme({ primary: "#FF6600", bg: { page: "#
|
|
8
|
-
*
|
|
6
|
+
* createAppShell({
|
|
7
|
+
* theme: createTheme({ primary: "#FF6600", bg: { page: "#F5F5F5" } }),
|
|
8
|
+
* darkTheme: createTheme({ bg: { page: "#0A0A0A" }, primary: "#F5F5F5" }),
|
|
9
|
+
* ...
|
|
9
10
|
* });
|
|
10
11
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
12
|
+
* The result is injected once at bootstrap (static, not swapped at runtime)
|
|
13
|
+
* and propagates as both NativeWind CSS vars (`className`) and inline style props.
|
|
13
14
|
*/
|
|
14
15
|
export declare function createTheme(overrides?: DeepPartial<ThemeTokens>): ThemeTokens;
|
|
15
16
|
export type { DeepPartial, ThemeTokens };
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { extendTheme } from "@sincpro/mobile-ui/theme";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
* Build a fully-typed theme from a partial override object.
|
|
4
|
+
* Starts from the DS base theme and deep-merges your tokens on top.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
* theme: createTheme({ primary: "#FF6600", bg: { page: "#
|
|
8
|
-
*
|
|
6
|
+
* createAppShell({
|
|
7
|
+
* theme: createTheme({ primary: "#FF6600", bg: { page: "#F5F5F5" } }),
|
|
8
|
+
* darkTheme: createTheme({ bg: { page: "#0A0A0A" }, primary: "#F5F5F5" }),
|
|
9
|
+
* ...
|
|
9
10
|
* });
|
|
10
11
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
12
|
+
* The result is injected once at bootstrap (static, not swapped at runtime)
|
|
13
|
+
* and propagates as both NativeWind CSS vars (`className`) and inline style props.
|
|
13
14
|
*/
|
|
14
15
|
export function createTheme(overrides) {
|
|
15
16
|
return extendTheme(overrides);
|
package/dist/index.d.ts
CHANGED
|
@@ -13,4 +13,3 @@ export { DomainModule } from "./framework/domain_module";
|
|
|
13
13
|
export { Kernel } from "./framework/kernel";
|
|
14
14
|
export { orchestrator } from "./framework/orchestrator";
|
|
15
15
|
export { installGlobalErrorHandler } from "./infrastructure/ui/errorHandler";
|
|
16
|
-
export { PlainLayout, TabNavigatorLayout } from "./ui/layouts/router_layouts";
|
package/dist/index.js
CHANGED
|
@@ -8,4 +8,3 @@ export { DomainModule } from "./framework/domain_module";
|
|
|
8
8
|
export { Kernel } from "./framework/kernel";
|
|
9
9
|
export { orchestrator } from "./framework/orchestrator";
|
|
10
10
|
export { installGlobalErrorHandler } from "./infrastructure/ui/errorHandler";
|
|
11
|
-
export { PlainLayout, TabNavigatorLayout } from "./ui/layouts/router_layouts";
|
|
@@ -1 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Bridges the framework's `UI_NOTIFICATION_EVENT` (emitted by services via UIEventBus)
|
|
3
|
+
* to the design-system Toast. Renders nothing; the toast stack is rendered by
|
|
4
|
+
* `ToastProvider` (mounted in AppShell). Must live inside `ToastProvider`.
|
|
5
|
+
*/
|
|
6
|
+
export declare function ToastHost(): null;
|
|
@@ -1,19 +1,28 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useToast } from "@sincpro/mobile-ui/Feedback";
|
|
2
2
|
import { useEffect } from "react";
|
|
3
|
-
import Toast from "react-native-toast-message";
|
|
4
3
|
import { UI_NOTIFICATION_EVENT } from "./events";
|
|
5
4
|
import { UIEventBus } from "./UIEventBus";
|
|
5
|
+
const VARIANT_BY_TYPE = {
|
|
6
|
+
success: "success",
|
|
7
|
+
error: "danger",
|
|
8
|
+
info: "info",
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Bridges the framework's `UI_NOTIFICATION_EVENT` (emitted by services via UIEventBus)
|
|
12
|
+
* to the design-system Toast. Renders nothing; the toast stack is rendered by
|
|
13
|
+
* `ToastProvider` (mounted in AppShell). Must live inside `ToastProvider`.
|
|
14
|
+
*/
|
|
6
15
|
export function ToastHost() {
|
|
16
|
+
const toast = useToast();
|
|
7
17
|
useEffect(() => {
|
|
8
18
|
const off = UIEventBus.on(UI_NOTIFICATION_EVENT, (payload) => {
|
|
9
19
|
const notification = payload;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
});
|
|
20
|
+
const variant = VARIANT_BY_TYPE[notification.type ?? "info"];
|
|
21
|
+
const message = notification.text2 ?? notification.text1 ?? "";
|
|
22
|
+
const title = notification.text2 ? notification.text1 : undefined;
|
|
23
|
+
toast.show({ message, title, variant });
|
|
15
24
|
});
|
|
16
25
|
return off;
|
|
17
|
-
}, []);
|
|
18
|
-
return
|
|
26
|
+
}, [toast]);
|
|
27
|
+
return null;
|
|
19
28
|
}
|
|
@@ -19,6 +19,12 @@ declare class PrinterService {
|
|
|
19
19
|
printFromView(viewRef: RefObject<View | null>): Promise<boolean>;
|
|
20
20
|
printHtml(html: string): Promise<boolean>;
|
|
21
21
|
private htmlToPdfBase64;
|
|
22
|
+
/**
|
|
23
|
+
* Drops the current BLE connection (even if cached as "connected") and
|
|
24
|
+
* reconnects to the last saved printer. Used as a one-shot recovery after
|
|
25
|
+
* a failed print caused by a stale connection.
|
|
26
|
+
*/
|
|
27
|
+
private forceReconnect;
|
|
22
28
|
private ensureConnected;
|
|
23
29
|
}
|
|
24
30
|
export declare const printerService: PrinterService;
|
|
@@ -133,9 +133,24 @@ class PrinterService {
|
|
|
133
133
|
await getPrinterDriver().printImageBase64(base64Data);
|
|
134
134
|
return true;
|
|
135
135
|
}
|
|
136
|
-
catch (
|
|
137
|
-
loggerUseCases.
|
|
138
|
-
|
|
136
|
+
catch (firstError) {
|
|
137
|
+
loggerUseCases.warn("Print failed — attempting grace reconnect", firstError);
|
|
138
|
+
const reconnected = await this.forceReconnect();
|
|
139
|
+
if (!reconnected)
|
|
140
|
+
return false; // connect() ya notificó al usuario
|
|
141
|
+
try {
|
|
142
|
+
await getPrinterDriver().printImageBase64(base64Data);
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
catch (retryError) {
|
|
146
|
+
loggerUseCases.error("Print retry after reconnect also failed", retryError);
|
|
147
|
+
UIEventBus.emit(UI_NOTIFICATION_EVENT, {
|
|
148
|
+
type: "error",
|
|
149
|
+
text1: "Error de impresión",
|
|
150
|
+
text2: "No se pudo imprimir incluso tras reconectar la impresora",
|
|
151
|
+
});
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
139
154
|
}
|
|
140
155
|
}
|
|
141
156
|
async printFromView(viewRef) {
|
|
@@ -202,6 +217,24 @@ class PrinterService {
|
|
|
202
217
|
return null;
|
|
203
218
|
}
|
|
204
219
|
}
|
|
220
|
+
/**
|
|
221
|
+
* Drops the current BLE connection (even if cached as "connected") and
|
|
222
|
+
* reconnects to the last saved printer. Used as a one-shot recovery after
|
|
223
|
+
* a failed print caused by a stale connection.
|
|
224
|
+
*/
|
|
225
|
+
async forceReconnect() {
|
|
226
|
+
try {
|
|
227
|
+
await getPrinterDriver().disconnect();
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
// Expected when the BLE link is already dead — ignore.
|
|
231
|
+
}
|
|
232
|
+
const printer = await this.getSelectedPrinter();
|
|
233
|
+
if (!printer)
|
|
234
|
+
return false;
|
|
235
|
+
loggerUseCases.info(`Grace reconnect: reconnecting to ${printer.name} (${printer.address})`);
|
|
236
|
+
return this.connect(printer.address, printer.name);
|
|
237
|
+
}
|
|
205
238
|
async ensureConnected() {
|
|
206
239
|
if (this.isConnected()) {
|
|
207
240
|
return true;
|
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx } from "nativewind/jsx-runtime";
|
|
2
2
|
import { useCommon } from "../../../entrypoints/ui/common_provider";
|
|
3
|
-
import {
|
|
4
|
-
import { View } from "react-native";
|
|
3
|
+
import { Feedback } from "@sincpro/mobile-ui/Feedback";
|
|
5
4
|
function DebugBanner() {
|
|
6
5
|
const { debugMode } = useCommon();
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
return null;
|
|
10
|
-
}
|
|
11
|
-
return (_jsx(View, { className: "bg-accent py-2 px-4", children: _jsx(Typography.Text, { className: "text-white font-bold text-center", semibold: true, children: "⚠️ Estás en modo Debug" }) }));
|
|
6
|
+
if (!debugMode) {
|
|
7
|
+
return null;
|
|
12
8
|
}
|
|
13
|
-
return
|
|
9
|
+
return _jsx(Feedback.Banner, { message: "Est\u00E1s en modo Debug", tone: "warning" });
|
|
14
10
|
}
|
|
15
11
|
export { DebugBanner };
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "nativewind/jsx-runtime";
|
|
2
|
+
import Svg, { Path, Rect } from "react-native-svg";
|
|
3
|
+
const PrinterIcon = ({ size = 24, color = "#000000" }) => (_jsxs(Svg, { fill: "none", height: size, viewBox: "0 0 24 24", width: size, children: [_jsx(Path, { d: "M6 9V2H18V9", stroke: color, strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: "2" }), _jsx(Rect, { height: "8", rx: "1", stroke: color, strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: "2", width: "16", x: "4", y: "9" }), _jsx(Path, { d: "M6 18H4C3.44772 18 3 17.5523 3 17V10C3 9.44772 3.44772 9 4 9H20C20.5523 9 21 9.44772 21 10V17C21 17.5523 20.5523 18 20 18H18", stroke: color, strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: "2" }), _jsx(Rect, { height: "6", stroke: color, strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: "2", width: "12", x: "6", y: "14" })] }));
|
|
4
|
+
export default PrinterIcon;
|
|
@@ -1,20 +1,24 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "nativewind/jsx-runtime";
|
|
2
2
|
import { bluetoothService } from "../../../services/bluetooth.service";
|
|
3
3
|
import { printerService } from "../../../services/printer.service";
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
4
|
+
import PrinterIcon from "../../../ui/components/atoms/PrinterIcon";
|
|
5
|
+
import Sheet from "@sincpro/mobile-ui/Dialog/Sheet";
|
|
6
|
+
import { Display } from "@sincpro/mobile-ui/Display";
|
|
7
|
+
import { Form } from "@sincpro/mobile-ui/Form";
|
|
8
|
+
import { useTheme } from "@sincpro/mobile-ui/theme";
|
|
8
9
|
import { Typography } from "@sincpro/mobile-ui/Typography";
|
|
9
10
|
import { useCallback, useEffect, useState } from "react";
|
|
10
|
-
import {
|
|
11
|
+
import { TouchableOpacity, View } from "react-native";
|
|
11
12
|
function DeviceRow({ device, onSelect, isSelected }) {
|
|
12
|
-
|
|
13
|
+
const theme = useTheme();
|
|
14
|
+
return (_jsxs(TouchableOpacity, { activeOpacity: 0.7, className: `flex-row items-center p-4 rounded-xl ${isSelected ? "bg-success-light" : "bg-bg-card"}`, onPress: onSelect, children: [_jsx(Display.Icon, { color: device.isPrinter ? theme.success : theme.text.secondary, customIcon: PrinterIcon, size: 24, type: "custom" }), _jsxs(View, { className: "ml-3 flex-1", children: [_jsx(Typography.Text, { semibold: true, children: device.name || "Dispositivo sin nombre" }), _jsx(Typography.Text, { className: "text-text-tertiary text-xs", children: device.address }), device.isPrinter && (_jsx(Typography.Text, { className: "text-success text-xs", children: "Impresora detectada" }))] }), isSelected && (_jsx(View, { className: "w-6 h-6 rounded-full bg-success items-center justify-center", children: _jsx(Display.Icon, { color: theme.text.inverse, name: "check", size: 16, type: "material" }) }))] }));
|
|
13
15
|
}
|
|
14
16
|
function EmptyState() {
|
|
15
|
-
|
|
17
|
+
const theme = useTheme();
|
|
18
|
+
return (_jsxs(View, { className: "p-8 items-center justify-center", style: { minHeight: 250 }, children: [_jsx(Display.Icon, { color: theme.text.tertiary, customIcon: PrinterIcon, size: 64, type: "custom" }), _jsx(Typography.Text, { className: "text-text-secondary text-center mt-4", children: "No hay dispositivos vinculados" }), _jsx(Typography.Text, { className: "text-text-tertiary text-center text-sm mt-2", children: "Vincula una impresora desde configuración de Bluetooth del sistema" })] }));
|
|
16
19
|
}
|
|
17
20
|
function BluetoothDeviceSelectorModal({ visible, onClose, onSelect, }) {
|
|
21
|
+
const theme = useTheme();
|
|
18
22
|
const [devices, setDevices] = useState([]);
|
|
19
23
|
const [loading, setLoading] = useState(false);
|
|
20
24
|
const [selectedDevice, setSelectedDevice] = useState(null);
|
|
@@ -55,7 +59,9 @@ function BluetoothDeviceSelectorModal({ visible, onClose, onSelect, }) {
|
|
|
55
59
|
}
|
|
56
60
|
const printers = devices.filter((d) => d.isPrinter);
|
|
57
61
|
const otherDevices = devices.filter((d) => !d.isPrinter);
|
|
58
|
-
|
|
59
|
-
|
|
62
|
+
function renderRow(item) {
|
|
63
|
+
return (_jsx(DeviceRow, { device: item, isSelected: selectedDevice?.address === item.address, onSelect: () => setSelectedDevice(item) }, item.address));
|
|
64
|
+
}
|
|
65
|
+
return (_jsxs(Sheet, { onClose: onClose, title: "Seleccionar Impresora", visible: visible, children: [_jsxs(View, { className: "flex-row items-center justify-between mb-3", children: [_jsx(Typography.Text, { className: "text-text-secondary text-xs", children: "Dispositivos Bluetooth vinculados" }), _jsx(TouchableOpacity, { accessibilityLabel: "Actualizar", disabled: loading, hitSlop: 8, onPress: loadDevices, children: _jsx(Display.Icon, { color: theme.icon.secondary, name: "refresh", size: 20, type: "material" }) })] }), devices.length === 0 && !loading ? (_jsx(EmptyState, {})) : (_jsxs(View, { className: "gap-1", children: [printers.length > 0 ? (_jsxs(Typography.Text, { className: "text-text-secondary px-1 py-1", semibold: true, children: ["Impresoras (", printers.length, ")"] })) : null, printers.map(renderRow), otherDevices.length > 0 ? (_jsxs(Typography.Text, { className: "text-text-secondary px-1 py-1 mt-1", semibold: true, children: ["Otros dispositivos (", otherDevices.length, ")"] })) : null, otherDevices.map(renderRow)] })), _jsxs(View, { className: "flex-row gap-2 mt-4", children: [_jsx(View, { className: "flex-1", children: _jsx(Form.Button, { onPress: onClose, title: "Cancelar", variant: "outline" }) }), selectedDevice ? (_jsx(View, { className: "flex-1", children: _jsx(Form.Button, { loading: connecting, onPress: handleConfirm, title: "Conectar", variant: "cta" }) })) : null] })] }));
|
|
60
66
|
}
|
|
61
67
|
export { BluetoothDeviceSelectorModal };
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "nativewind/jsx-runtime";
|
|
2
2
|
import { Display } from "@sincpro/mobile-ui/Display";
|
|
3
|
-
import {
|
|
3
|
+
import { useTheme } from "@sincpro/mobile-ui/theme";
|
|
4
4
|
import { cn } from "@sincpro/mobile-ui/theme/tw";
|
|
5
5
|
import { Typography } from "@sincpro/mobile-ui/Typography";
|
|
6
6
|
import { TouchableOpacity, View } from "react-native";
|
|
7
7
|
function DeadLetterErrorBlock({ errorMessage, expanded, onToggle, previewChars = 140, }) {
|
|
8
|
+
const theme = useTheme();
|
|
8
9
|
if (!errorMessage)
|
|
9
10
|
return null;
|
|
10
11
|
const preview = errorMessage.substring(0, previewChars);
|
|
11
12
|
const truncated = errorMessage.length > previewChars;
|
|
12
|
-
return (_jsxs(View, { className: cn("border-l-[3px] border-
|
|
13
|
+
return (_jsxs(View, { className: cn("border-l-[3px] border-danger bg-danger-light p-2.5 rounded-xl mb-3", expanded && "bg-danger-light"), children: [_jsxs(TouchableOpacity, { accessibilityRole: "button", activeOpacity: 0.75, className: "flex-row items-center mb-1", onPress: onToggle, children: [_jsx(Display.Icon, { color: theme.danger, name: "alert-circle", size: 16, type: "feather" }), _jsx(Typography.Text, { className: "text-danger ml-1.5", semibold: true, variant: "bodySmall", children: "Error" })] }), !expanded && (_jsxs(Typography.Text, { className: "text-danger", numberOfLines: 3, variant: "bodySmall", children: [preview, truncated ? "…" : ""] })), expanded && (_jsx(View, { className: "bg-bg-card rounded-lg p-2.5 border border-danger/30", children: _jsx(Typography.Text, { className: "text-danger text-xs leading-4", variant: "bodySmall", children: errorMessage }) })), truncated && !expanded && (_jsx(TouchableOpacity, { accessibilityRole: "button", activeOpacity: 0.7, className: "mt-1.5", onPress: onToggle, children: _jsx(Typography.Text, { className: "text-danger underline", semibold: true, variant: "bodySmall", children: "Ver error completo" }) }))] }));
|
|
13
14
|
}
|
|
14
15
|
export default DeadLetterErrorBlock;
|