@gaozh1024/rn-kit 0.3.4 → 0.4.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/dist/index.mjs CHANGED
@@ -250,6 +250,11 @@ import { useMemo as useMemo2 } from "react";
250
250
  function getThemeColors(theme, isDark) {
251
251
  return {
252
252
  primary: theme.colors.primary?.[500] || "#f38b32",
253
+ success: theme.colors.success?.[500] || "#22c55e",
254
+ warning: theme.colors.warning?.[500] || "#f59e0b",
255
+ error: theme.colors.error?.[500] || "#ef4444",
256
+ info: theme.colors.info?.[500] || theme.colors.secondary?.[500] || "#3b82f6",
257
+ muted: isDark ? "#9ca3af" : "#6b7280",
253
258
  primarySurface: isDark ? theme.colors.primary?.[900] || "#7c2d12" : theme.colors.primary?.[50] || "#fff7ed",
254
259
  background: theme.colors.background?.[500] || (isDark ? "#0a0a0a" : "#ffffff"),
255
260
  card: theme.colors.card?.[500] || (isDark ? "#1f2937" : "#ffffff"),
@@ -346,6 +351,189 @@ function useAsyncState() {
346
351
 
347
352
  // src/core/api/create-api.ts
348
353
  import { ZodError } from "zod";
354
+
355
+ // src/core/logger/utils.ts
356
+ var LOG_LEVEL_WEIGHT = {
357
+ debug: 10,
358
+ info: 20,
359
+ warn: 30,
360
+ error: 40
361
+ };
362
+ function shouldLog(currentLevel, nextLevel) {
363
+ return LOG_LEVEL_WEIGHT[nextLevel] >= LOG_LEVEL_WEIGHT[currentLevel];
364
+ }
365
+ function createLogEntry(input) {
366
+ return {
367
+ id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
368
+ level: input.level,
369
+ message: input.message,
370
+ data: input.data,
371
+ namespace: input.namespace,
372
+ timestamp: Date.now(),
373
+ source: input.source ?? "logger"
374
+ };
375
+ }
376
+ function formatLogTime(timestamp) {
377
+ const date = new Date(timestamp);
378
+ const hh = String(date.getHours()).padStart(2, "0");
379
+ const mm = String(date.getMinutes()).padStart(2, "0");
380
+ const ss = String(date.getSeconds()).padStart(2, "0");
381
+ return `${hh}:${mm}:${ss}`;
382
+ }
383
+ function stringifyLogData(data) {
384
+ if (data === void 0) return "";
385
+ if (typeof data === "string") return data;
386
+ try {
387
+ return JSON.stringify(data, null, 2);
388
+ } catch {
389
+ return String(data);
390
+ }
391
+ }
392
+ function normalizeConsoleArgs(args) {
393
+ if (args.length === 0) {
394
+ return { message: "", data: void 0 };
395
+ }
396
+ const [first, ...rest] = args;
397
+ if (typeof first === "string") {
398
+ return {
399
+ message: first,
400
+ data: rest.length === 0 ? void 0 : rest.length === 1 ? rest[0] : rest
401
+ };
402
+ }
403
+ return {
404
+ message: stringifyLogData(first),
405
+ data: args.length === 1 ? first : args
406
+ };
407
+ }
408
+ function serializeLogEntries(entries) {
409
+ return JSON.stringify(
410
+ entries.map((entry) => ({
411
+ id: entry.id,
412
+ level: entry.level,
413
+ message: entry.message,
414
+ namespace: entry.namespace,
415
+ timestamp: entry.timestamp,
416
+ source: entry.source,
417
+ data: entry.data
418
+ })),
419
+ null,
420
+ 2
421
+ );
422
+ }
423
+
424
+ // src/core/logger/transports.ts
425
+ var ANSI_COLORS = {
426
+ debug: "\x1B[90m",
427
+ info: "\x1B[36m",
428
+ warn: "\x1B[33m",
429
+ error: "\x1B[31m",
430
+ reset: "\x1B[0m"
431
+ };
432
+ function supportsAnsiColors() {
433
+ return typeof process !== "undefined" && Boolean(process.stdout?.isTTY);
434
+ }
435
+ function resolveConsoleMethod(consoleRef, entry) {
436
+ const target = consoleRef ?? console;
437
+ if (entry.level === "debug" && typeof target.debug === "function") return target.debug.bind(target);
438
+ if (entry.level === "info" && typeof target.info === "function") return target.info.bind(target);
439
+ if (entry.level === "warn" && typeof target.warn === "function") return target.warn.bind(target);
440
+ if (entry.level === "error" && typeof target.error === "function") return target.error.bind(target);
441
+ return target.log.bind(target);
442
+ }
443
+ function createConsoleTransport(options = {}) {
444
+ const useAnsiColors = options.useAnsiColors ?? supportsAnsiColors();
445
+ return (entry) => {
446
+ if (entry.source === "console") return;
447
+ const method = resolveConsoleMethod(options.consoleRef, entry);
448
+ const time = formatLogTime(entry.timestamp);
449
+ const basePrefix = `[${time}] [${entry.level.toUpperCase()}]${entry.namespace ? ` [${entry.namespace}]` : ""}`;
450
+ const prefix = useAnsiColors ? `${ANSI_COLORS[entry.level]}${basePrefix}${ANSI_COLORS.reset}` : basePrefix;
451
+ if (entry.data === void 0) {
452
+ method(`${prefix} ${entry.message}`);
453
+ return;
454
+ }
455
+ method(`${prefix} ${entry.message}`, entry.data);
456
+ };
457
+ }
458
+
459
+ // src/core/logger/registry.ts
460
+ var globalLogger = null;
461
+ function setGlobalLogger(logger) {
462
+ globalLogger = logger;
463
+ }
464
+ function getGlobalLogger() {
465
+ return globalLogger;
466
+ }
467
+
468
+ // src/core/api/observability.ts
469
+ function defaultSanitize(value, sanitize, stage, field) {
470
+ if (!sanitize) return value;
471
+ return sanitize(value, { stage, field });
472
+ }
473
+ function resolveMessage(event) {
474
+ if (event.stage === "request") {
475
+ return `API Request ${event.method} ${event.path}`;
476
+ }
477
+ if (event.stage === "response") {
478
+ return `API Response ${event.method} ${event.path} ${event.statusCode ?? "-"}`;
479
+ }
480
+ return `API Error ${event.method} ${event.path}`;
481
+ }
482
+ function resolveLevel(error) {
483
+ if (!error) return "info";
484
+ if (error.code === "VALIDATION" || error.code === "BUSINESS") return "warn";
485
+ return "error";
486
+ }
487
+ function createApiLoggerTransport(config = {}) {
488
+ return (event) => {
489
+ const logger = config.logger ?? getGlobalLogger();
490
+ if (!logger) return;
491
+ const data = {
492
+ endpointName: event.endpointName,
493
+ method: event.method,
494
+ path: event.path,
495
+ url: event.url,
496
+ statusCode: event.statusCode,
497
+ durationMs: event.durationMs,
498
+ input: config.includeInput || event.stage !== "request" ? defaultSanitize(event.input, config.sanitize, event.stage, "input") : void 0,
499
+ responseData: config.includeResponseData ? defaultSanitize(event.responseData, config.sanitize, event.stage, "responseData") : void 0,
500
+ error: event.error ? defaultSanitize(
501
+ {
502
+ code: event.error.code,
503
+ message: event.error.message,
504
+ statusCode: event.error.statusCode,
505
+ retryable: event.error.retryable
506
+ },
507
+ config.sanitize,
508
+ event.stage,
509
+ "error"
510
+ ) : void 0
511
+ };
512
+ const namespace = config.namespace ?? "api";
513
+ const message = resolveMessage(event);
514
+ if (event.stage === "request") {
515
+ logger.debug(message, data, namespace);
516
+ return;
517
+ }
518
+ if (event.stage === "response") {
519
+ logger.info(message, data, namespace);
520
+ return;
521
+ }
522
+ logger[resolveLevel(event.error)](message, data, namespace);
523
+ };
524
+ }
525
+ function resolveApiObservability(config) {
526
+ const enabled = config?.enabled ?? isDevelopment();
527
+ if (!enabled) {
528
+ return { enabled, transports: [] };
529
+ }
530
+ return {
531
+ enabled,
532
+ transports: [createApiLoggerTransport(config), ...config?.transports ?? []]
533
+ };
534
+ }
535
+
536
+ // src/core/api/create-api.ts
349
537
  function parseZodError(error) {
350
538
  const first = error.errors[0];
351
539
  return {
@@ -398,6 +586,7 @@ async function notifyError(error, context, endpoint, config) {
398
586
  function createAPI(config) {
399
587
  const endpoints = {};
400
588
  const fetcher = config.fetcher ?? fetch;
589
+ const observability = resolveApiObservability(config.observability);
401
590
  for (const [name, ep] of Object.entries(config.endpoints)) {
402
591
  endpoints[name] = async (input) => {
403
592
  const context = {
@@ -413,11 +602,43 @@ function createAPI(config) {
413
602
  }
414
603
  }
415
604
  const url = config.baseURL + ep.path;
605
+ const startAt = Date.now();
606
+ if (observability.enabled) {
607
+ await Promise.all(
608
+ observability.transports.map(
609
+ (transport) => transport({
610
+ stage: "request",
611
+ endpointName: name,
612
+ path: ep.path,
613
+ method: ep.method,
614
+ url,
615
+ input
616
+ })
617
+ )
618
+ );
619
+ }
416
620
  let response;
417
621
  try {
418
622
  response = await fetcher(url, { method: ep.method });
419
623
  } catch (error) {
420
- throw await notifyError(parseNetworkError(error), context, ep, config);
624
+ const enhanced = await notifyError(parseNetworkError(error), context, ep, config);
625
+ if (observability.enabled) {
626
+ await Promise.all(
627
+ observability.transports.map(
628
+ (transport) => transport({
629
+ stage: "error",
630
+ endpointName: name,
631
+ path: ep.path,
632
+ method: ep.method,
633
+ url,
634
+ input,
635
+ durationMs: Date.now() - startAt,
636
+ error: enhanced
637
+ })
638
+ )
639
+ );
640
+ }
641
+ throw enhanced;
421
642
  }
422
643
  context.response = response;
423
644
  let data = void 0;
@@ -432,18 +653,115 @@ function createAPI(config) {
432
653
  }
433
654
  context.responseData = data;
434
655
  if (!response.ok) {
435
- throw await notifyError(parseHttpError(response, data), context, ep, config);
656
+ const enhanced = await notifyError(parseHttpError(response, data), context, ep, config);
657
+ if (observability.enabled) {
658
+ await Promise.all(
659
+ observability.transports.map(
660
+ (transport) => transport({
661
+ stage: "error",
662
+ endpointName: name,
663
+ path: ep.path,
664
+ method: ep.method,
665
+ url,
666
+ input,
667
+ response,
668
+ responseData: data,
669
+ statusCode: response.status,
670
+ durationMs: Date.now() - startAt,
671
+ error: enhanced
672
+ })
673
+ )
674
+ );
675
+ }
676
+ throw enhanced;
436
677
  }
437
678
  const businessError = ep.parseBusinessError?.(data, response) ?? config.parseBusinessError?.(data, response);
438
679
  if (businessError) {
439
- throw await notifyError(businessError, context, ep, config);
680
+ const enhanced = await notifyError(businessError, context, ep, config);
681
+ if (observability.enabled) {
682
+ await Promise.all(
683
+ observability.transports.map(
684
+ (transport) => transport({
685
+ stage: "error",
686
+ endpointName: name,
687
+ path: ep.path,
688
+ method: ep.method,
689
+ url,
690
+ input,
691
+ response,
692
+ responseData: data,
693
+ statusCode: response.status,
694
+ durationMs: Date.now() - startAt,
695
+ error: enhanced
696
+ })
697
+ )
698
+ );
699
+ }
700
+ throw enhanced;
440
701
  }
441
702
  try {
442
703
  if (ep.output) {
443
- return ep.output.parse(data);
704
+ const parsed = ep.output.parse(data);
705
+ if (observability.enabled) {
706
+ await Promise.all(
707
+ observability.transports.map(
708
+ (transport) => transport({
709
+ stage: "response",
710
+ endpointName: name,
711
+ path: ep.path,
712
+ method: ep.method,
713
+ url,
714
+ input,
715
+ response,
716
+ responseData: parsed,
717
+ statusCode: response.status,
718
+ durationMs: Date.now() - startAt
719
+ })
720
+ )
721
+ );
722
+ }
723
+ return parsed;
444
724
  }
445
725
  } catch (error) {
446
- throw await notifyError(parseUnknownError(error), context, ep, config);
726
+ const enhanced = await notifyError(parseUnknownError(error), context, ep, config);
727
+ if (observability.enabled) {
728
+ await Promise.all(
729
+ observability.transports.map(
730
+ (transport) => transport({
731
+ stage: "error",
732
+ endpointName: name,
733
+ path: ep.path,
734
+ method: ep.method,
735
+ url,
736
+ input,
737
+ response,
738
+ responseData: data,
739
+ statusCode: response.status,
740
+ durationMs: Date.now() - startAt,
741
+ error: enhanced
742
+ })
743
+ )
744
+ );
745
+ }
746
+ throw enhanced;
747
+ }
748
+ if (observability.enabled) {
749
+ await Promise.all(
750
+ observability.transports.map(
751
+ (transport) => transport({
752
+ stage: "response",
753
+ endpointName: name,
754
+ path: ep.path,
755
+ method: ep.method,
756
+ url,
757
+ input,
758
+ response,
759
+ responseData: data,
760
+ statusCode: response.status,
761
+ durationMs: Date.now() - startAt
762
+ })
763
+ )
764
+ );
447
765
  }
448
766
  return data;
449
767
  };
@@ -1037,7 +1355,7 @@ function AppView({
1037
1355
  }
1038
1356
 
1039
1357
  // src/ui/primitives/AppScrollView.tsx
1040
- import { ScrollView } from "react-native";
1358
+ import { Keyboard, ScrollView, TouchableWithoutFeedback } from "react-native";
1041
1359
  import { jsx as jsx3 } from "nativewind/jsx-runtime";
1042
1360
  function AppScrollView({
1043
1361
  flex,
@@ -1049,6 +1367,7 @@ function AppScrollView({
1049
1367
  surface,
1050
1368
  rounded,
1051
1369
  className,
1370
+ dismissKeyboardOnPressOutside = false,
1052
1371
  children,
1053
1372
  style,
1054
1373
  ...props
@@ -1056,7 +1375,7 @@ function AppScrollView({
1056
1375
  const { theme, isDark } = useOptionalTheme();
1057
1376
  const resolvedBgColor = resolveSurfaceColor(surface, theme, isDark) ?? resolveNamedColor(bg, theme, isDark);
1058
1377
  const shouldUseClassBg = !!bg && !resolvedBgColor;
1059
- return /* @__PURE__ */ jsx3(
1378
+ const content = /* @__PURE__ */ jsx3(
1060
1379
  ScrollView,
1061
1380
  {
1062
1381
  className: cn(
@@ -1069,11 +1388,16 @@ function AppScrollView({
1069
1388
  rounded && `rounded-${rounded}`,
1070
1389
  className
1071
1390
  ),
1072
- style: [resolvedBgColor ? { backgroundColor: resolvedBgColor } : void 0, style],
1073
1391
  ...props,
1392
+ style: [resolvedBgColor ? { backgroundColor: resolvedBgColor } : void 0, style],
1393
+ keyboardShouldPersistTaps: dismissKeyboardOnPressOutside ? props.keyboardShouldPersistTaps ?? "handled" : props.keyboardShouldPersistTaps,
1074
1394
  children
1075
1395
  }
1076
1396
  );
1397
+ if (!dismissKeyboardOnPressOutside) {
1398
+ return content;
1399
+ }
1400
+ return /* @__PURE__ */ jsx3(TouchableWithoutFeedback, { onPress: Keyboard.dismiss, accessible: false, children: content });
1077
1401
  }
1078
1402
 
1079
1403
  // src/ui/primitives/AppText.tsx
@@ -1156,8 +1480,19 @@ function AppPressable({
1156
1480
  );
1157
1481
  }
1158
1482
 
1159
- // src/ui/layout/Row.tsx
1483
+ // src/ui/primitives/KeyboardDismissView.tsx
1484
+ import { Keyboard as Keyboard2, TouchableWithoutFeedback as TouchableWithoutFeedback2 } from "react-native";
1160
1485
  import { jsx as jsx6 } from "nativewind/jsx-runtime";
1486
+ function KeyboardDismissView({ enabled = true, children, ...props }) {
1487
+ const content = /* @__PURE__ */ jsx6(AppView, { ...props, children });
1488
+ if (!enabled) {
1489
+ return content;
1490
+ }
1491
+ return /* @__PURE__ */ jsx6(TouchableWithoutFeedback2, { onPress: Keyboard2.dismiss, accessible: false, children: content });
1492
+ }
1493
+
1494
+ // src/ui/layout/Row.tsx
1495
+ import { jsx as jsx7 } from "nativewind/jsx-runtime";
1161
1496
  var justifyMap = {
1162
1497
  start: "justify-start",
1163
1498
  center: "justify-center",
@@ -1172,11 +1507,11 @@ var itemsMap = {
1172
1507
  stretch: "items-stretch"
1173
1508
  };
1174
1509
  function Row({ justify = "start", items = "center", className, ...props }) {
1175
- return /* @__PURE__ */ jsx6(AppView, { row: true, className: cn(justifyMap[justify], itemsMap[items], className), ...props });
1510
+ return /* @__PURE__ */ jsx7(AppView, { row: true, className: cn(justifyMap[justify], itemsMap[items], className), ...props });
1176
1511
  }
1177
1512
 
1178
1513
  // src/ui/layout/Col.tsx
1179
- import { jsx as jsx7 } from "nativewind/jsx-runtime";
1514
+ import { jsx as jsx8 } from "nativewind/jsx-runtime";
1180
1515
  var justifyMap2 = {
1181
1516
  start: "justify-start",
1182
1517
  center: "justify-center",
@@ -1191,19 +1526,19 @@ var itemsMap2 = {
1191
1526
  stretch: "items-stretch"
1192
1527
  };
1193
1528
  function Col({ justify = "start", items = "stretch", className, ...props }) {
1194
- return /* @__PURE__ */ jsx7(AppView, { className: cn(justifyMap2[justify], itemsMap2[items], className), ...props });
1529
+ return /* @__PURE__ */ jsx8(AppView, { className: cn(justifyMap2[justify], itemsMap2[items], className), ...props });
1195
1530
  }
1196
1531
 
1197
1532
  // src/ui/layout/Center.tsx
1198
- import { jsx as jsx8 } from "nativewind/jsx-runtime";
1533
+ import { jsx as jsx9 } from "nativewind/jsx-runtime";
1199
1534
  function Center({ flex = true, ...props }) {
1200
- return /* @__PURE__ */ jsx8(AppView, { center: true, flex, ...props });
1535
+ return /* @__PURE__ */ jsx9(AppView, { center: true, flex, ...props });
1201
1536
  }
1202
1537
 
1203
1538
  // src/ui/layout/SafeScreen.tsx
1204
- import { View as View2, StyleSheet as StyleSheet2 } from "react-native";
1539
+ import { Keyboard as Keyboard3, TouchableWithoutFeedback as TouchableWithoutFeedback3, View as View2, StyleSheet as StyleSheet2 } from "react-native";
1205
1540
  import { useSafeAreaInsets } from "react-native-safe-area-context";
1206
- import { jsx as jsx9 } from "nativewind/jsx-runtime";
1541
+ import { jsx as jsx10 } from "nativewind/jsx-runtime";
1207
1542
  function SafeScreen({
1208
1543
  top = true,
1209
1544
  bottom = true,
@@ -1212,6 +1547,7 @@ function SafeScreen({
1212
1547
  bg,
1213
1548
  surface,
1214
1549
  flex = true,
1550
+ dismissKeyboardOnPressOutside = false,
1215
1551
  className,
1216
1552
  children,
1217
1553
  style,
@@ -1221,7 +1557,7 @@ function SafeScreen({
1221
1557
  const { theme, isDark } = useOptionalTheme();
1222
1558
  const resolvedBgColor = resolveSurfaceColor(surface, theme, isDark) ?? resolveNamedColor(bg, theme, isDark);
1223
1559
  const shouldUseClassBg = !!bg && !resolvedBgColor;
1224
- return /* @__PURE__ */ jsx9(
1560
+ const content = /* @__PURE__ */ jsx10(
1225
1561
  View2,
1226
1562
  {
1227
1563
  className: cn(flex && "flex-1", shouldUseClassBg && `bg-${bg}`, className),
@@ -1240,6 +1576,10 @@ function SafeScreen({
1240
1576
  children
1241
1577
  }
1242
1578
  );
1579
+ if (!dismissKeyboardOnPressOutside) {
1580
+ return content;
1581
+ }
1582
+ return /* @__PURE__ */ jsx10(TouchableWithoutFeedback3, { onPress: Keyboard3.dismiss, accessible: false, children: content });
1243
1583
  }
1244
1584
  var styles = StyleSheet2.create({
1245
1585
  flex: {
@@ -1251,19 +1591,20 @@ function AppScreen({
1251
1591
  className,
1252
1592
  ...props
1253
1593
  }) {
1254
- return /* @__PURE__ */ jsx9(SafeScreen, { flex: true, surface: "background", ...props, className, children });
1594
+ return /* @__PURE__ */ jsx10(SafeScreen, { flex: true, surface: "background", ...props, className, children });
1255
1595
  }
1256
1596
  function SafeBottom({
1257
1597
  children,
1258
1598
  className,
1259
1599
  ...props
1260
1600
  }) {
1261
- return /* @__PURE__ */ jsx9(SafeScreen, { bottom: true, left: true, right: true, flex: false, ...props, className, children });
1601
+ return /* @__PURE__ */ jsx10(SafeScreen, { bottom: true, left: true, right: true, flex: false, ...props, className, children });
1262
1602
  }
1263
1603
 
1264
1604
  // src/ui/actions/AppButton.tsx
1265
- import { ActivityIndicator } from "react-native";
1266
- import { jsx as jsx10 } from "nativewind/jsx-runtime";
1605
+ import { useCallback as useCallback9 } from "react";
1606
+ import { ActivityIndicator, Keyboard as Keyboard4 } from "react-native";
1607
+ import { jsx as jsx11 } from "nativewind/jsx-runtime";
1267
1608
  function AppButton({
1268
1609
  variant = "solid",
1269
1610
  size = "md",
@@ -1271,6 +1612,7 @@ function AppButton({
1271
1612
  loading,
1272
1613
  disabled,
1273
1614
  onPress,
1615
+ dismissKeyboardOnPress = true,
1274
1616
  children,
1275
1617
  className
1276
1618
  }) {
@@ -1291,11 +1633,17 @@ function AppButton({
1291
1633
  const ghostBackgroundColor = isDark ? "rgba(255,255,255,0.04)" : "transparent";
1292
1634
  const loadingColor = variant === "solid" ? "white" : buttonColors[color];
1293
1635
  const textColor = variant === "solid" ? "#ffffff" : variant === "ghost" ? ghostTextColor : buttonColors[color];
1636
+ const handlePress = useCallback9(() => {
1637
+ if (dismissKeyboardOnPress) {
1638
+ Keyboard4.dismiss();
1639
+ }
1640
+ onPress?.();
1641
+ }, [dismissKeyboardOnPress, onPress]);
1294
1642
  const buttonStyle = variant === "solid" ? { backgroundColor: buttonColors[color] } : variant === "outline" ? { borderWidth: 0.5, borderColor: buttonColors[color], backgroundColor: "transparent" } : { backgroundColor: ghostBackgroundColor };
1295
- return /* @__PURE__ */ jsx10(
1643
+ return /* @__PURE__ */ jsx11(
1296
1644
  AppPressable,
1297
1645
  {
1298
- onPress,
1646
+ onPress: onPress ? handlePress : void 0,
1299
1647
  disabled: isDisabled,
1300
1648
  className: cn(
1301
1649
  "flex-row items-center justify-center rounded-lg",
@@ -1304,30 +1652,61 @@ function AppButton({
1304
1652
  className
1305
1653
  ),
1306
1654
  style: buttonStyle,
1307
- children: loading ? /* @__PURE__ */ jsx10(ActivityIndicator, { size: "small", color: loadingColor }) : /* @__PURE__ */ jsx10(AppText, { weight: "semibold", style: { color: textColor }, children })
1655
+ children: loading ? /* @__PURE__ */ jsx11(ActivityIndicator, { size: "small", color: loadingColor }) : /* @__PURE__ */ jsx11(AppText, { weight: "semibold", style: { color: textColor }, children })
1308
1656
  }
1309
1657
  );
1310
1658
  }
1311
1659
 
1312
1660
  // src/ui/feedback/Toast.tsx
1313
- import { jsx as jsx11 } from "nativewind/jsx-runtime";
1314
- var typeStyles = {
1315
- success: "bg-green-500",
1316
- error: "bg-red-500",
1317
- warning: "bg-yellow-500",
1318
- info: "bg-blue-500"
1319
- };
1320
- function Toast({ message, type = "info", visible = true }) {
1661
+ import { jsx as jsx12 } from "nativewind/jsx-runtime";
1662
+ function Toast({ message, type = "info", visible = true, testID }) {
1663
+ const { theme } = useOptionalTheme();
1321
1664
  if (!visible) return null;
1322
- return /* @__PURE__ */ jsx11(AppView, { className: cn("px-4 py-3 rounded-lg", typeStyles[type]), children: /* @__PURE__ */ jsx11(AppText, { color: "white", children: message }) });
1665
+ const palette = {
1666
+ success: {
1667
+ backgroundColor: theme.colors.success?.[500] || "#22c55e",
1668
+ textColor: "#ffffff"
1669
+ },
1670
+ error: {
1671
+ backgroundColor: theme.colors.error?.[500] || "#ef4444",
1672
+ textColor: "#ffffff"
1673
+ },
1674
+ warning: {
1675
+ backgroundColor: theme.colors.warning?.[500] || "#f59e0b",
1676
+ textColor: "#111827"
1677
+ },
1678
+ info: {
1679
+ backgroundColor: theme.colors.info?.[500] || theme.colors.primary?.[500] || "#3b82f6",
1680
+ textColor: "#ffffff"
1681
+ }
1682
+ };
1683
+ const currentPalette = palette[type];
1684
+ return /* @__PURE__ */ jsx12(
1685
+ AppView,
1686
+ {
1687
+ testID,
1688
+ className: "px-4 py-3 rounded-lg",
1689
+ style: { backgroundColor: currentPalette.backgroundColor },
1690
+ children: /* @__PURE__ */ jsx12(AppText, { style: { color: currentPalette.textColor }, children: message })
1691
+ }
1692
+ );
1323
1693
  }
1324
1694
 
1325
1695
  // src/ui/feedback/Alert.tsx
1326
- import { useCallback as useCallback9 } from "react";
1327
- import { Modal, TouchableOpacity, StyleSheet as StyleSheet3 } from "react-native";
1328
- import { jsx as jsx12, jsxs } from "nativewind/jsx-runtime";
1696
+ import { useCallback as useCallback10, useEffect as useEffect5, useRef as useRef6 } from "react";
1697
+ import { Modal, TouchableOpacity, StyleSheet as StyleSheet3, Animated } from "react-native";
1698
+ import { jsx as jsx13, jsxs } from "nativewind/jsx-runtime";
1699
+ function createAnimatedValue(value) {
1700
+ const AnimatedValue = Animated.Value;
1701
+ try {
1702
+ return new AnimatedValue(value);
1703
+ } catch {
1704
+ return AnimatedValue(value);
1705
+ }
1706
+ }
1329
1707
  function Alert({ visible, title, message, buttons, onClose }) {
1330
1708
  const { theme, isDark } = useTheme();
1709
+ const progress = useRef6(createAnimatedValue(0)).current;
1331
1710
  const modalBgColor = isDark ? "#1f2937" : "#ffffff";
1332
1711
  const textColor = isDark ? "#ffffff" : "#1f2937";
1333
1712
  const messageColor = isDark ? "#9ca3af" : "#6b7280";
@@ -1335,7 +1714,19 @@ function Alert({ visible, title, message, buttons, onClose }) {
1335
1714
  const cancelButtonBg = isDark ? "#374151" : "#f3f4f6";
1336
1715
  const cancelButtonText = isDark ? "#ffffff" : "#374151";
1337
1716
  const destructiveColor = theme.colors.error?.[500] || "#ef4444";
1338
- const handleButtonPress = useCallback9(
1717
+ useEffect5(() => {
1718
+ if (!visible) {
1719
+ progress.setValue(0);
1720
+ return;
1721
+ }
1722
+ progress.setValue(0);
1723
+ Animated.timing(progress, {
1724
+ toValue: 1,
1725
+ duration: 220,
1726
+ useNativeDriver: true
1727
+ }).start();
1728
+ }, [progress, visible]);
1729
+ const handleButtonPress = useCallback10(
1339
1730
  (button) => (e) => {
1340
1731
  e.stopPropagation();
1341
1732
  button.onPress?.();
@@ -1372,104 +1763,137 @@ function Alert({ visible, title, message, buttons, onClose }) {
1372
1763
  }
1373
1764
  return "#ffffff";
1374
1765
  };
1375
- return /* @__PURE__ */ jsx12(
1766
+ return /* @__PURE__ */ jsx13(
1376
1767
  Modal,
1377
1768
  {
1378
1769
  visible,
1379
1770
  transparent: true,
1380
- animationType: "fade",
1771
+ animationType: "none",
1381
1772
  onRequestClose: onClose,
1382
1773
  statusBarTranslucent: true,
1383
- children: /* @__PURE__ */ jsx12(AppView, { className: "flex-1", style: { backgroundColor: "rgba(0,0,0,0.5)" }, center: true, children: /* @__PURE__ */ jsxs(
1384
- AppView,
1385
- {
1386
- className: "rounded-xl mx-8 min-w-[280px]",
1387
- style: { backgroundColor: modalBgColor },
1388
- children: [
1389
- /* @__PURE__ */ jsxs(AppView, { className: "px-6 py-5", children: [
1390
- /* @__PURE__ */ jsx12(
1391
- AppText,
1392
- {
1393
- size: "lg",
1394
- weight: "semibold",
1395
- className: "text-center mb-2",
1396
- style: { color: textColor },
1397
- children: title
1398
- }
1399
- ),
1400
- message && /* @__PURE__ */ jsx12(AppText, { size: "sm", style: { color: messageColor }, className: "text-center leading-5", children: message })
1401
- ] }),
1402
- /* @__PURE__ */ jsx12(AppView, { className: "border-t", style: { borderTopColor: borderColor }, children: buttons.length === 1 ? (
1403
- // 单个按钮
1404
- /* @__PURE__ */ jsx12(
1405
- TouchableOpacity,
1406
- {
1407
- onPress: handleButtonPress(buttons[0]),
1408
- className: "py-3 rounded-b-xl",
1409
- style: [styles2.singleButton, getButtonStyle(buttons[0])],
1410
- children: /* @__PURE__ */ jsx12(
1411
- AppText,
1412
- {
1413
- weight: "medium",
1414
- className: "text-center",
1415
- style: { color: getButtonTextColor(buttons[0]) },
1416
- children: buttons[0].text
1417
- }
1418
- )
1419
- }
1420
- )
1421
- ) : buttons.length === 2 ? (
1422
- // 两个按钮横向排列
1423
- /* @__PURE__ */ jsx12(AppView, { row: true, style: styles2.twoButtonContainer, children: buttons.map((button, index) => /* @__PURE__ */ jsx12(
1424
- TouchableOpacity,
1425
- {
1426
- onPress: handleButtonPress(button),
1427
- className: cn(
1428
- "py-3 flex-1",
1429
- index === 0 && "rounded-bl-xl",
1430
- index === 1 && "rounded-br-xl"
1431
- ),
1432
- style: [
1433
- styles2.twoButton,
1434
- index > 0 && { borderLeftColor: borderColor },
1435
- getButtonStyle(button)
1436
- ],
1437
- children: /* @__PURE__ */ jsx12(
1438
- AppText,
1439
- {
1440
- weight: "medium",
1441
- className: "text-center",
1442
- style: { color: getButtonTextColor(button) },
1443
- children: button.text
1444
- }
1445
- )
1446
- },
1447
- index
1448
- )) })
1449
- ) : (
1450
- // 多个按钮纵向排列
1451
- /* @__PURE__ */ jsx12(AppView, { className: "gap-2 pb-4 px-4", children: buttons.map((button, index) => /* @__PURE__ */ jsx12(
1452
- TouchableOpacity,
1453
- {
1454
- onPress: handleButtonPress(button),
1455
- className: "py-3 rounded-lg",
1456
- style: getButtonStyle(button),
1457
- children: /* @__PURE__ */ jsx12(
1458
- AppText,
1459
- {
1460
- weight: "medium",
1461
- className: "text-center",
1462
- style: { color: getButtonTextColor(button) },
1463
- children: button.text
1464
- }
1465
- )
1466
- },
1467
- index
1468
- )) })
1469
- ) })
1470
- ]
1471
- }
1472
- ) })
1774
+ children: /* @__PURE__ */ jsxs(AppView, { className: "flex-1", center: true, children: [
1775
+ /* @__PURE__ */ jsx13(
1776
+ Animated.View,
1777
+ {
1778
+ style: [
1779
+ StyleSheet3.absoluteFillObject,
1780
+ {
1781
+ backgroundColor: "rgba(0,0,0,0.5)",
1782
+ opacity: progress
1783
+ }
1784
+ ]
1785
+ }
1786
+ ),
1787
+ /* @__PURE__ */ jsxs(
1788
+ Animated.View,
1789
+ {
1790
+ className: "rounded-xl mx-8 min-w-[280px]",
1791
+ style: [
1792
+ {
1793
+ backgroundColor: modalBgColor,
1794
+ opacity: progress,
1795
+ transform: [
1796
+ {
1797
+ translateY: progress.interpolate({
1798
+ inputRange: [0, 1],
1799
+ outputRange: [16, 0]
1800
+ })
1801
+ },
1802
+ {
1803
+ scale: progress.interpolate({
1804
+ inputRange: [0, 1],
1805
+ outputRange: [0.96, 1]
1806
+ })
1807
+ }
1808
+ ]
1809
+ }
1810
+ ],
1811
+ children: [
1812
+ /* @__PURE__ */ jsxs(AppView, { className: "px-6 py-5", children: [
1813
+ /* @__PURE__ */ jsx13(
1814
+ AppText,
1815
+ {
1816
+ size: "lg",
1817
+ weight: "semibold",
1818
+ className: "text-center mb-2",
1819
+ style: { color: textColor },
1820
+ children: title
1821
+ }
1822
+ ),
1823
+ message && /* @__PURE__ */ jsx13(AppText, { size: "sm", style: { color: messageColor }, className: "text-center leading-5", children: message })
1824
+ ] }),
1825
+ /* @__PURE__ */ jsx13(AppView, { className: "border-t", style: { borderTopColor: borderColor }, children: buttons.length === 1 ? (
1826
+ // 单个按钮
1827
+ /* @__PURE__ */ jsx13(
1828
+ TouchableOpacity,
1829
+ {
1830
+ onPress: handleButtonPress(buttons[0]),
1831
+ className: "py-3 rounded-b-xl",
1832
+ style: [styles2.singleButton, getButtonStyle(buttons[0])],
1833
+ children: /* @__PURE__ */ jsx13(
1834
+ AppText,
1835
+ {
1836
+ weight: "medium",
1837
+ className: "text-center",
1838
+ style: { color: getButtonTextColor(buttons[0]) },
1839
+ children: buttons[0].text
1840
+ }
1841
+ )
1842
+ }
1843
+ )
1844
+ ) : buttons.length === 2 ? (
1845
+ // 两个按钮横向排列
1846
+ /* @__PURE__ */ jsx13(AppView, { row: true, style: styles2.twoButtonContainer, children: buttons.map((button, index) => /* @__PURE__ */ jsx13(
1847
+ TouchableOpacity,
1848
+ {
1849
+ onPress: handleButtonPress(button),
1850
+ className: cn(
1851
+ "py-3 flex-1",
1852
+ index === 0 && "rounded-bl-xl",
1853
+ index === 1 && "rounded-br-xl"
1854
+ ),
1855
+ style: [
1856
+ styles2.twoButton,
1857
+ index > 0 && { borderLeftColor: borderColor },
1858
+ getButtonStyle(button)
1859
+ ],
1860
+ children: /* @__PURE__ */ jsx13(
1861
+ AppText,
1862
+ {
1863
+ weight: "medium",
1864
+ className: "text-center",
1865
+ style: { color: getButtonTextColor(button) },
1866
+ children: button.text
1867
+ }
1868
+ )
1869
+ },
1870
+ index
1871
+ )) })
1872
+ ) : (
1873
+ // 多个按钮纵向排列
1874
+ /* @__PURE__ */ jsx13(AppView, { className: "gap-2 pb-4 px-4", children: buttons.map((button, index) => /* @__PURE__ */ jsx13(
1875
+ TouchableOpacity,
1876
+ {
1877
+ onPress: handleButtonPress(button),
1878
+ className: "py-3 rounded-lg",
1879
+ style: getButtonStyle(button),
1880
+ children: /* @__PURE__ */ jsx13(
1881
+ AppText,
1882
+ {
1883
+ weight: "medium",
1884
+ className: "text-center",
1885
+ style: { color: getButtonTextColor(button) },
1886
+ children: button.text
1887
+ }
1888
+ )
1889
+ },
1890
+ index
1891
+ )) })
1892
+ ) })
1893
+ ]
1894
+ }
1895
+ )
1896
+ ] })
1473
1897
  }
1474
1898
  );
1475
1899
  }
@@ -1488,19 +1912,8 @@ var styles2 = StyleSheet3.create({
1488
1912
  });
1489
1913
 
1490
1914
  // src/ui/feedback/Loading.tsx
1491
- import { ActivityIndicator as ActivityIndicator2 } from "react-native";
1492
- import { jsx as jsx13, jsxs as jsxs2 } from "nativewind/jsx-runtime";
1493
- function Loading({ text, color, overlay = false, visible = true, testID }) {
1494
- if (!visible) return null;
1495
- const content = /* @__PURE__ */ jsxs2(AppView, { center: true, gap: 3, testID, children: [
1496
- /* @__PURE__ */ jsx13(ActivityIndicator2, { size: "large", color }),
1497
- text && /* @__PURE__ */ jsx13(AppText, { style: color ? { color } : void 0, children: text })
1498
- ] });
1499
- if (overlay) {
1500
- return /* @__PURE__ */ jsx13(AppView, { center: true, flex: true, className: "absolute inset-0 bg-black/30", testID, children: content });
1501
- }
1502
- return content;
1503
- }
1915
+ import { ActivityIndicator as ActivityIndicator4 } from "react-native";
1916
+ import { useEffect as useEffect6, useState as useState12 } from "react";
1504
1917
 
1505
1918
  // src/ui/display/Progress.tsx
1506
1919
  import { jsx as jsx14 } from "nativewind/jsx-runtime";
@@ -1694,13 +2107,13 @@ var FileIcons = {
1694
2107
  };
1695
2108
 
1696
2109
  // src/ui/display/AppImage.tsx
1697
- import { useState as useState10, useCallback as useCallback10 } from "react";
2110
+ import { useState as useState10, useCallback as useCallback11 } from "react";
1698
2111
  import {
1699
2112
  Image,
1700
- ActivityIndicator as ActivityIndicator3,
2113
+ ActivityIndicator as ActivityIndicator2,
1701
2114
  View as View4
1702
2115
  } from "react-native";
1703
- import { jsx as jsx17, jsxs as jsxs3 } from "nativewind/jsx-runtime";
2116
+ import { jsx as jsx17, jsxs as jsxs2 } from "nativewind/jsx-runtime";
1704
2117
  var radiusMap = {
1705
2118
  none: 0,
1706
2119
  sm: 2,
@@ -1738,11 +2151,11 @@ function AppImage({
1738
2151
  const [hasError, setHasError] = useState10(false);
1739
2152
  const { theme } = useTheme();
1740
2153
  const resolvedRadius = resolveRadius(borderRadius);
1741
- const handleLoad = useCallback10(() => {
2154
+ const handleLoad = useCallback11(() => {
1742
2155
  setIsLoading(false);
1743
2156
  onLoad?.();
1744
2157
  }, [onLoad]);
1745
- const handleError = useCallback10(
2158
+ const handleError = useCallback11(
1746
2159
  (error) => {
1747
2160
  setIsLoading(false);
1748
2161
  setHasError(true);
@@ -1783,7 +2196,7 @@ function AppImage({
1783
2196
  center: true,
1784
2197
  className: "absolute inset-0 bg-gray-100",
1785
2198
  style: { borderRadius: resolvedRadius },
1786
- children: /* @__PURE__ */ jsx17(ActivityIndicator3, { color: theme.colors.primary?.[500] })
2199
+ children: /* @__PURE__ */ jsx17(ActivityIndicator2, { color: theme.colors.primary?.[500] })
1787
2200
  }
1788
2201
  );
1789
2202
  }
@@ -1823,7 +2236,7 @@ function AppImage({
1823
2236
  };
1824
2237
  const isNumberWidth = typeof width === "number";
1825
2238
  const isNumberHeight = typeof height === "number";
1826
- const content = /* @__PURE__ */ jsxs3(
2239
+ const content = /* @__PURE__ */ jsxs2(
1827
2240
  View4,
1828
2241
  {
1829
2242
  className: cn("overflow-hidden", className),
@@ -1856,22 +2269,31 @@ function AppImage({
1856
2269
  }
1857
2270
 
1858
2271
  // src/ui/display/AppList.tsx
1859
- import { useState as useState11, useCallback as useCallback11, useMemo as useMemo3 } from "react";
2272
+ import React4, { useState as useState11, useCallback as useCallback12, useMemo as useMemo3 } from "react";
1860
2273
  import {
1861
2274
  FlatList,
1862
2275
  RefreshControl,
1863
- ActivityIndicator as ActivityIndicator4,
2276
+ ActivityIndicator as ActivityIndicator3,
1864
2277
  StyleSheet as StyleSheet4
1865
2278
  } from "react-native";
1866
- import { Fragment, jsx as jsx18, jsxs as jsxs4 } from "nativewind/jsx-runtime";
2279
+ import { Fragment, jsx as jsx18, jsxs as jsxs3 } from "nativewind/jsx-runtime";
2280
+ function renderListSlot(slot) {
2281
+ if (!slot) return null;
2282
+ if (React4.isValidElement(slot)) return slot;
2283
+ if (typeof slot === "function") {
2284
+ const SlotComponent = slot;
2285
+ return /* @__PURE__ */ jsx18(SlotComponent, {});
2286
+ }
2287
+ return null;
2288
+ }
1867
2289
  function SkeletonItem2({ render }) {
1868
2290
  const colors = useThemeColors();
1869
2291
  if (render) {
1870
2292
  return render();
1871
2293
  }
1872
- return /* @__PURE__ */ jsx18(AppView, { p: 4, gap: 3, testID: "skeleton", children: /* @__PURE__ */ jsxs4(AppView, { row: true, gap: 3, children: [
2294
+ return /* @__PURE__ */ jsx18(AppView, { p: 4, gap: 3, testID: "skeleton", children: /* @__PURE__ */ jsxs3(AppView, { row: true, gap: 3, children: [
1873
2295
  /* @__PURE__ */ jsx18(AppView, { className: "w-16 h-16 rounded-lg", style: { backgroundColor: colors.divider } }),
1874
- /* @__PURE__ */ jsxs4(AppView, { flex: true, gap: 2, children: [
2296
+ /* @__PURE__ */ jsxs3(AppView, { flex: true, gap: 2, children: [
1875
2297
  /* @__PURE__ */ jsx18(AppView, { className: "h-4 w-3/4 rounded", style: { backgroundColor: colors.divider } }),
1876
2298
  /* @__PURE__ */ jsx18(AppView, { className: "h-3 w-1/2 rounded", style: { backgroundColor: colors.divider } })
1877
2299
  ] })
@@ -1883,7 +2305,7 @@ function EmptyState({
1883
2305
  icon
1884
2306
  }) {
1885
2307
  const colors = useThemeColors();
1886
- return /* @__PURE__ */ jsxs4(Center, { py: 20, children: [
2308
+ return /* @__PURE__ */ jsxs3(Center, { py: 20, children: [
1887
2309
  /* @__PURE__ */ jsx18(Icon, { name: icon || "inbox", size: 64, color: colors.textMuted }),
1888
2310
  /* @__PURE__ */ jsx18(AppText, { size: "lg", weight: "medium", className: "mt-4", style: { color: colors.text }, children: title || "\u6682\u65E0\u6570\u636E" }),
1889
2311
  description && /* @__PURE__ */ jsx18(AppText, { size: "sm", className: "mt-2", style: { color: colors.textMuted }, children: description })
@@ -1897,7 +2319,7 @@ function ErrorState({
1897
2319
  retryText
1898
2320
  }) {
1899
2321
  const colors = useThemeColors();
1900
- return /* @__PURE__ */ jsxs4(Center, { py: 20, children: [
2322
+ return /* @__PURE__ */ jsxs3(Center, { py: 20, children: [
1901
2323
  /* @__PURE__ */ jsx18(Icon, { name: "error-outline", size: 64, color: "error-300" }),
1902
2324
  /* @__PURE__ */ jsx18(AppText, { size: "lg", weight: "medium", color: "error-500", className: "mt-4", children: errorTitle || "\u52A0\u8F7D\u5931\u8D25" }),
1903
2325
  /* @__PURE__ */ jsx18(AppText, { size: "sm", style: { color: colors.textMuted }, className: "mt-2 text-center px-8", children: error.message || errorDescription || "\u8BF7\u68C0\u67E5\u7F51\u7EDC\u540E\u91CD\u8BD5" }),
@@ -1917,7 +2339,7 @@ function ErrorState({
1917
2339
  }
1918
2340
  function LoadMoreFooter({ loading }) {
1919
2341
  if (!loading) return null;
1920
- return /* @__PURE__ */ jsx18(Center, { py: 4, children: /* @__PURE__ */ jsx18(ActivityIndicator4, { size: "small" }) });
2342
+ return /* @__PURE__ */ jsx18(Center, { py: 4, children: /* @__PURE__ */ jsx18(ActivityIndicator3, { size: "small" }) });
1921
2343
  }
1922
2344
  function Divider({ style }) {
1923
2345
  const colors = useThemeColors();
@@ -1964,7 +2386,7 @@ function AppList({
1964
2386
  }) {
1965
2387
  const { theme } = useTheme();
1966
2388
  const [isLoadingMore, setIsLoadingMore] = useState11(false);
1967
- const handleEndReached = useCallback11(async () => {
2389
+ const handleEndReached = useCallback12(async () => {
1968
2390
  if (isLoadingMore || !hasMore || !onEndReached) return;
1969
2391
  setIsLoadingMore(true);
1970
2392
  try {
@@ -1973,16 +2395,16 @@ function AppList({
1973
2395
  setIsLoadingMore(false);
1974
2396
  }
1975
2397
  }, [isLoadingMore, hasMore, onEndReached]);
1976
- const defaultKeyExtractor = useCallback11(
2398
+ const defaultKeyExtractor = useCallback12(
1977
2399
  (item, index) => {
1978
2400
  if (keyExtractor) return keyExtractor(item, index);
1979
2401
  return `item-${index}`;
1980
2402
  },
1981
2403
  [keyExtractor]
1982
2404
  );
1983
- const wrappedRenderItem = useCallback11(
2405
+ const wrappedRenderItem = useCallback12(
1984
2406
  (info) => {
1985
- return /* @__PURE__ */ jsxs4(Fragment, { children: [
2407
+ return /* @__PURE__ */ jsxs3(Fragment, { children: [
1986
2408
  divider && info.index > 0 && /* @__PURE__ */ jsx18(Divider, { style: dividerStyle }),
1987
2409
  renderItem(info)
1988
2410
  ] });
@@ -1993,10 +2415,14 @@ function AppList({
1993
2415
  () => new Array(skeletonCount).fill(null).map((_, i) => ({ _skeletonId: i })),
1994
2416
  [skeletonCount]
1995
2417
  );
1996
- const skeletonRenderItem = useCallback11(
2418
+ const skeletonRenderItem = useCallback12(
1997
2419
  () => /* @__PURE__ */ jsx18(SkeletonItem2, { render: skeletonRender }),
1998
2420
  [skeletonRender]
1999
2421
  );
2422
+ const flatListKey = useMemo3(() => {
2423
+ if (horizontal) return "app-list-horizontal";
2424
+ return `app-list-columns-${numColumns ?? 1}`;
2425
+ }, [horizontal, numColumns]);
2000
2426
  if (loading && data.length === 0) {
2001
2427
  return /* @__PURE__ */ jsx18(
2002
2428
  FlatList,
@@ -2008,7 +2434,8 @@ function AppList({
2008
2434
  style,
2009
2435
  showsVerticalScrollIndicator,
2010
2436
  showsHorizontalScrollIndicator
2011
- }
2437
+ },
2438
+ `${flatListKey}-skeleton`
2012
2439
  );
2013
2440
  }
2014
2441
  if (error && data.length === 0) {
@@ -2024,15 +2451,19 @@ function AppList({
2024
2451
  ) });
2025
2452
  }
2026
2453
  const ListEmptyComponent = useMemo3(() => {
2027
- if (EmptyComponent) return /* @__PURE__ */ jsx18(EmptyComponent, {});
2454
+ if (EmptyComponent) return renderListSlot(EmptyComponent);
2028
2455
  return /* @__PURE__ */ jsx18(EmptyState, { title: emptyTitle, description: emptyDescription, icon: emptyIcon });
2029
2456
  }, [EmptyComponent, emptyTitle, emptyDescription, emptyIcon]);
2030
2457
  const FooterComponent = useMemo3(() => {
2031
- return /* @__PURE__ */ jsxs4(Fragment, { children: [
2458
+ return /* @__PURE__ */ jsxs3(Fragment, { children: [
2032
2459
  /* @__PURE__ */ jsx18(LoadMoreFooter, { loading: isLoadingMore }),
2033
- ListFooterComponent
2460
+ renderListSlot(ListFooterComponent)
2034
2461
  ] });
2035
2462
  }, [isLoadingMore, ListFooterComponent]);
2463
+ const HeaderComponent = useMemo3(
2464
+ () => renderListSlot(ListHeaderComponent),
2465
+ [ListHeaderComponent]
2466
+ );
2036
2467
  return /* @__PURE__ */ jsx18(
2037
2468
  FlatList,
2038
2469
  {
@@ -2051,7 +2482,7 @@ function AppList({
2051
2482
  onEndReached: onEndReached ? handleEndReached : void 0,
2052
2483
  onEndReachedThreshold,
2053
2484
  ListEmptyComponent,
2054
- ListHeaderComponent,
2485
+ ListHeaderComponent: HeaderComponent,
2055
2486
  ListFooterComponent: FooterComponent,
2056
2487
  contentContainerStyle,
2057
2488
  style,
@@ -2064,7 +2495,8 @@ function AppList({
2064
2495
  maxToRenderPerBatch: 10,
2065
2496
  windowSize: 10,
2066
2497
  initialNumToRender: 10
2067
- }
2498
+ },
2499
+ flatListKey
2068
2500
  );
2069
2501
  }
2070
2502
  var styles3 = StyleSheet4.create({
@@ -2076,7 +2508,7 @@ var styles3 = StyleSheet4.create({
2076
2508
  // src/ui/display/PageDrawer.tsx
2077
2509
  import React5 from "react";
2078
2510
  import { BackHandler, Modal as Modal2, PanResponder, StyleSheet as StyleSheet5 } from "react-native";
2079
- import { jsx as jsx19, jsxs as jsxs5 } from "nativewind/jsx-runtime";
2511
+ import { jsx as jsx19, jsxs as jsxs4 } from "nativewind/jsx-runtime";
2080
2512
  function PageDrawer({
2081
2513
  visible,
2082
2514
  onClose,
@@ -2096,7 +2528,6 @@ function PageDrawer({
2096
2528
  }) {
2097
2529
  const colors = useThemeColors();
2098
2530
  const [translateX, setTranslateX] = React5.useState(0);
2099
- if (!visible) return null;
2100
2531
  const handleClose = React5.useCallback(() => {
2101
2532
  setTranslateX(0);
2102
2533
  onClose?.();
@@ -2140,7 +2571,8 @@ function PageDrawer({
2140
2571
  }),
2141
2572
  [handleClose, placement, swipeEnabled, swipeThreshold, width]
2142
2573
  );
2143
- const drawerContent = /* @__PURE__ */ jsxs5(
2574
+ if (!visible) return null;
2575
+ const drawerContent = /* @__PURE__ */ jsxs4(
2144
2576
  AppView,
2145
2577
  {
2146
2578
  testID: contentTestID,
@@ -2159,7 +2591,7 @@ function PageDrawer({
2159
2591
  }
2160
2592
  ],
2161
2593
  children: [
2162
- (header || title || showCloseButton) && /* @__PURE__ */ jsxs5(
2594
+ (header || title || showCloseButton) && /* @__PURE__ */ jsxs4(
2163
2595
  AppView,
2164
2596
  {
2165
2597
  row: true,
@@ -2187,7 +2619,7 @@ function PageDrawer({
2187
2619
  ]
2188
2620
  }
2189
2621
  );
2190
- return /* @__PURE__ */ jsx19(Modal2, { visible: true, transparent: true, animationType: "fade", onRequestClose: handleClose, children: /* @__PURE__ */ jsxs5(
2622
+ return /* @__PURE__ */ jsx19(Modal2, { visible: true, transparent: true, animationType: "fade", onRequestClose: handleClose, children: /* @__PURE__ */ jsxs4(
2191
2623
  AppView,
2192
2624
  {
2193
2625
  testID,
@@ -2236,14 +2668,57 @@ function GradientView({
2236
2668
  return /* @__PURE__ */ jsx20(LinearGradient, { colors: [...colors], start, end, style, ...props, children });
2237
2669
  }
2238
2670
 
2671
+ // src/ui/feedback/Loading.tsx
2672
+ import { jsx as jsx21, jsxs as jsxs5 } from "nativewind/jsx-runtime";
2673
+ var LOADING_CLOSE_DELAY = 3e4;
2674
+ function Loading({
2675
+ text,
2676
+ color,
2677
+ overlay = false,
2678
+ visible = true,
2679
+ testID,
2680
+ onClose
2681
+ }) {
2682
+ const [showCloseButton, setShowCloseButton] = useState12(false);
2683
+ useEffect6(() => {
2684
+ if (!visible) {
2685
+ setShowCloseButton(false);
2686
+ return;
2687
+ }
2688
+ setShowCloseButton(false);
2689
+ const timer = setTimeout(() => {
2690
+ setShowCloseButton(true);
2691
+ }, LOADING_CLOSE_DELAY);
2692
+ return () => clearTimeout(timer);
2693
+ }, [visible]);
2694
+ if (!visible) return null;
2695
+ const content = /* @__PURE__ */ jsxs5(AppView, { center: true, gap: 3, testID, children: [
2696
+ /* @__PURE__ */ jsx21(ActivityIndicator4, { size: "large", color }),
2697
+ text && /* @__PURE__ */ jsx21(AppText, { style: color ? { color } : void 0, children: text }),
2698
+ showCloseButton && onClose && /* @__PURE__ */ jsx21(
2699
+ AppPressable,
2700
+ {
2701
+ testID: testID ? `${testID}-close` : "loading-close",
2702
+ className: "mt-1 p-1",
2703
+ onPress: onClose,
2704
+ children: /* @__PURE__ */ jsx21(Icon, { name: "close", size: "md", color: color || "white" })
2705
+ }
2706
+ )
2707
+ ] });
2708
+ if (overlay) {
2709
+ return /* @__PURE__ */ jsx21(AppView, { center: true, flex: true, className: "absolute inset-0 bg-black/30", testID, children: content });
2710
+ }
2711
+ return content;
2712
+ }
2713
+
2239
2714
  // src/ui/form/AppInput.tsx
2240
- import { forwardRef, useState as useState12 } from "react";
2715
+ import { forwardRef, useState as useState13 } from "react";
2241
2716
  import { TextInput, View as View5, StyleSheet as StyleSheet6 } from "react-native";
2242
- import { jsx as jsx21, jsxs as jsxs6 } from "nativewind/jsx-runtime";
2717
+ import { jsx as jsx22, jsxs as jsxs6 } from "nativewind/jsx-runtime";
2243
2718
  var AppInput = forwardRef(
2244
2719
  ({ label, error, disabled = false, leftIcon, rightIcon, className, style, ...props }, ref) => {
2245
2720
  const colors = useThemeColors();
2246
- const [isFocused, setIsFocused] = useState12(false);
2721
+ const [isFocused, setIsFocused] = useState13(false);
2247
2722
  const errorColor = "#ef4444";
2248
2723
  const getBorderColor = () => {
2249
2724
  if (error) return errorColor;
@@ -2251,7 +2726,7 @@ var AppInput = forwardRef(
2251
2726
  return colors.border;
2252
2727
  };
2253
2728
  return /* @__PURE__ */ jsxs6(AppView, { className: cn("flex-col gap-1", className), children: [
2254
- label && /* @__PURE__ */ jsx21(AppText, { size: "sm", weight: "medium", style: { color: colors.textSecondary }, children: label }),
2729
+ label && /* @__PURE__ */ jsx22(AppText, { size: "sm", weight: "medium", style: { color: colors.textSecondary }, children: label }),
2255
2730
  /* @__PURE__ */ jsxs6(
2256
2731
  AppView,
2257
2732
  {
@@ -2267,8 +2742,8 @@ var AppInput = forwardRef(
2267
2742
  }
2268
2743
  ],
2269
2744
  children: [
2270
- leftIcon && /* @__PURE__ */ jsx21(View5, { style: styles5.icon, children: leftIcon }),
2271
- /* @__PURE__ */ jsx21(
2745
+ leftIcon && /* @__PURE__ */ jsx22(View5, { style: styles5.icon, children: leftIcon }),
2746
+ /* @__PURE__ */ jsx22(
2272
2747
  TextInput,
2273
2748
  {
2274
2749
  ref,
@@ -2287,11 +2762,11 @@ var AppInput = forwardRef(
2287
2762
  ...props
2288
2763
  }
2289
2764
  ),
2290
- rightIcon && /* @__PURE__ */ jsx21(View5, { style: styles5.icon, children: rightIcon })
2765
+ rightIcon && /* @__PURE__ */ jsx22(View5, { style: styles5.icon, children: rightIcon })
2291
2766
  ]
2292
2767
  }
2293
2768
  ),
2294
- error && /* @__PURE__ */ jsx21(AppText, { size: "xs", style: { color: errorColor }, children: error })
2769
+ error && /* @__PURE__ */ jsx22(AppText, { size: "xs", style: { color: errorColor }, children: error })
2295
2770
  ] });
2296
2771
  }
2297
2772
  );
@@ -2313,9 +2788,9 @@ var styles5 = StyleSheet6.create({
2313
2788
  });
2314
2789
 
2315
2790
  // src/ui/form/Checkbox.tsx
2316
- import { useState as useState13 } from "react";
2791
+ import { useState as useState14 } from "react";
2317
2792
  import { TouchableOpacity as TouchableOpacity2, StyleSheet as StyleSheet7 } from "react-native";
2318
- import { jsx as jsx22, jsxs as jsxs7 } from "nativewind/jsx-runtime";
2793
+ import { jsx as jsx23, jsxs as jsxs7 } from "nativewind/jsx-runtime";
2319
2794
  function Checkbox({
2320
2795
  checked,
2321
2796
  defaultChecked,
@@ -2326,7 +2801,7 @@ function Checkbox({
2326
2801
  testID
2327
2802
  }) {
2328
2803
  const colors = useThemeColors();
2329
- const [internalChecked, setInternalChecked] = useState13(defaultChecked || false);
2804
+ const [internalChecked, setInternalChecked] = useState14(defaultChecked || false);
2330
2805
  const isChecked = checked !== void 0 ? checked : internalChecked;
2331
2806
  const toggle = () => {
2332
2807
  if (disabled) return;
@@ -2347,7 +2822,7 @@ function Checkbox({
2347
2822
  testID,
2348
2823
  activeOpacity: 0.7,
2349
2824
  children: [
2350
- /* @__PURE__ */ jsx22(
2825
+ /* @__PURE__ */ jsx23(
2351
2826
  AppView,
2352
2827
  {
2353
2828
  className: cn(
@@ -2361,10 +2836,10 @@ function Checkbox({
2361
2836
  borderColor: isChecked ? colors.primary : colors.border
2362
2837
  }
2363
2838
  ],
2364
- children: isChecked && /* @__PURE__ */ jsx22(AppView, { testID: `${testID}-icon`, children: /* @__PURE__ */ jsx22(Icon, { name: "check", size: "sm", color: "white" }) })
2839
+ children: isChecked && /* @__PURE__ */ jsx23(AppView, { pointerEvents: "none", style: styles6.iconContainer, testID: `${testID}-icon`, children: /* @__PURE__ */ jsx23(Icon, { name: "check", size: 14, color: "white", style: styles6.icon }) })
2365
2840
  }
2366
2841
  ),
2367
- children && /* @__PURE__ */ jsx22(AppText, { size: "sm", style: { color: colors.text }, children })
2842
+ children && /* @__PURE__ */ jsx23(AppText, { size: "sm", style: { color: colors.text }, children })
2368
2843
  ]
2369
2844
  }
2370
2845
  );
@@ -2372,6 +2847,16 @@ function Checkbox({
2372
2847
  var styles6 = StyleSheet7.create({
2373
2848
  checkbox: {
2374
2849
  borderWidth: 0.5
2850
+ },
2851
+ iconContainer: {
2852
+ ...StyleSheet7.absoluteFillObject,
2853
+ alignItems: "center",
2854
+ justifyContent: "center"
2855
+ },
2856
+ icon: {
2857
+ lineHeight: 14,
2858
+ includeFontPadding: false,
2859
+ textAlignVertical: "center"
2375
2860
  }
2376
2861
  });
2377
2862
 
@@ -2387,7 +2872,7 @@ var isGroupOptionDisabled = (groupDisabled, optionDisabled) => {
2387
2872
  };
2388
2873
 
2389
2874
  // src/ui/form/CheckboxGroup.tsx
2390
- import { jsx as jsx23 } from "nativewind/jsx-runtime";
2875
+ import { jsx as jsx24 } from "nativewind/jsx-runtime";
2391
2876
  function CheckboxGroup({
2392
2877
  value = [],
2393
2878
  onChange,
@@ -2400,7 +2885,7 @@ function CheckboxGroup({
2400
2885
  onChange(toggleGroupValue(value, optionValue, checked));
2401
2886
  };
2402
2887
  const isRow = direction === "row";
2403
- return /* @__PURE__ */ jsx23(AppView, { row: isRow, flex: isRow, gap: 4, children: options.map((option) => /* @__PURE__ */ jsx23(
2888
+ return /* @__PURE__ */ jsx24(AppView, { row: isRow, flex: isRow, gap: 4, children: options.map((option) => /* @__PURE__ */ jsx24(
2404
2889
  Checkbox,
2405
2890
  {
2406
2891
  checked: value.includes(option.value),
@@ -2413,9 +2898,9 @@ function CheckboxGroup({
2413
2898
  }
2414
2899
 
2415
2900
  // src/ui/form/Radio.tsx
2416
- import { useState as useState14 } from "react";
2901
+ import { useState as useState15 } from "react";
2417
2902
  import { TouchableOpacity as TouchableOpacity3, StyleSheet as StyleSheet8 } from "react-native";
2418
- import { jsx as jsx24, jsxs as jsxs8 } from "nativewind/jsx-runtime";
2903
+ import { jsx as jsx25, jsxs as jsxs8 } from "nativewind/jsx-runtime";
2419
2904
  function Radio({
2420
2905
  checked,
2421
2906
  defaultChecked,
@@ -2426,7 +2911,7 @@ function Radio({
2426
2911
  testID
2427
2912
  }) {
2428
2913
  const colors = useThemeColors();
2429
- const [internalChecked, setInternalChecked] = useState14(defaultChecked || false);
2914
+ const [internalChecked, setInternalChecked] = useState15(defaultChecked || false);
2430
2915
  const isChecked = checked !== void 0 ? checked : internalChecked;
2431
2916
  const toggle = () => {
2432
2917
  if (disabled) return;
@@ -2447,7 +2932,7 @@ function Radio({
2447
2932
  testID,
2448
2933
  activeOpacity: 0.7,
2449
2934
  children: [
2450
- /* @__PURE__ */ jsx24(
2935
+ /* @__PURE__ */ jsx25(
2451
2936
  AppView,
2452
2937
  {
2453
2938
  className: cn("w-5 h-5 rounded-full items-center justify-center", isChecked && "border-2"),
@@ -2459,7 +2944,7 @@ function Radio({
2459
2944
  borderWidth: isChecked ? 0.5 : 0.5
2460
2945
  }
2461
2946
  ],
2462
- children: isChecked && /* @__PURE__ */ jsx24(
2947
+ children: isChecked && /* @__PURE__ */ jsx25(
2463
2948
  AppView,
2464
2949
  {
2465
2950
  className: "rounded-full",
@@ -2468,7 +2953,7 @@ function Radio({
2468
2953
  )
2469
2954
  }
2470
2955
  ),
2471
- children && /* @__PURE__ */ jsx24(AppText, { size: "sm", style: { color: colors.text }, children })
2956
+ children && /* @__PURE__ */ jsx25(AppText, { size: "sm", style: { color: colors.text }, children })
2472
2957
  ]
2473
2958
  }
2474
2959
  );
@@ -2484,7 +2969,7 @@ var styles7 = StyleSheet8.create({
2484
2969
  });
2485
2970
 
2486
2971
  // src/ui/form/RadioGroup.tsx
2487
- import { jsx as jsx25 } from "nativewind/jsx-runtime";
2972
+ import { jsx as jsx26 } from "nativewind/jsx-runtime";
2488
2973
  function RadioGroup({
2489
2974
  value,
2490
2975
  onChange,
@@ -2493,7 +2978,7 @@ function RadioGroup({
2493
2978
  disabled = false
2494
2979
  }) {
2495
2980
  const isRow = direction === "row";
2496
- return /* @__PURE__ */ jsx25(AppView, { row: isRow, flex: isRow, gap: 4, children: options.map((option) => /* @__PURE__ */ jsx25(
2981
+ return /* @__PURE__ */ jsx26(AppView, { row: isRow, flex: isRow, gap: 4, children: options.map((option) => /* @__PURE__ */ jsx26(
2497
2982
  Radio,
2498
2983
  {
2499
2984
  checked: value === option.value,
@@ -2506,9 +2991,17 @@ function RadioGroup({
2506
2991
  }
2507
2992
 
2508
2993
  // src/ui/form/Switch.tsx
2509
- import { useState as useState15 } from "react";
2510
- import { TouchableOpacity as TouchableOpacity4, StyleSheet as StyleSheet9 } from "react-native";
2511
- import { jsx as jsx26 } from "nativewind/jsx-runtime";
2994
+ import { useEffect as useEffect7, useRef as useRef7, useState as useState16 } from "react";
2995
+ import { Animated as Animated2, StyleSheet as StyleSheet9, TouchableOpacity as TouchableOpacity4 } from "react-native";
2996
+ import { jsx as jsx27 } from "nativewind/jsx-runtime";
2997
+ function createAnimatedValue2(value) {
2998
+ const AnimatedValue = Animated2.Value;
2999
+ try {
3000
+ return new AnimatedValue(value);
3001
+ } catch {
3002
+ return AnimatedValue(value);
3003
+ }
3004
+ }
2512
3005
  function Switch({
2513
3006
  checked,
2514
3007
  defaultChecked,
@@ -2520,35 +3013,79 @@ function Switch({
2520
3013
  style
2521
3014
  }) {
2522
3015
  const colors = useThemeColors();
2523
- const [internalChecked, setInternalChecked] = useState15(defaultChecked || false);
3016
+ const [internalChecked, setInternalChecked] = useState16(defaultChecked || false);
3017
+ const [isInteractionLocked, setIsInteractionLocked] = useState16(false);
3018
+ const isFirstRender = useRef7(true);
3019
+ const unlockTimerRef = useRef7(null);
2524
3020
  const isChecked = checked !== void 0 ? checked : internalChecked;
3021
+ const sizes = {
3022
+ sm: { width: 36, height: 20, thumb: 16, padding: 2 },
3023
+ md: { width: 48, height: 26, thumb: 22, padding: 2 },
3024
+ lg: { width: 60, height: 32, thumb: 28, padding: 2 }
3025
+ };
3026
+ const config = sizes[size];
3027
+ const maxTranslateX = config.width - config.thumb - config.padding * 2;
3028
+ const thumbTranslateX = useRef7(createAnimatedValue2(isChecked ? maxTranslateX : 0)).current;
3029
+ const clearUnlockTimer = () => {
3030
+ if (!unlockTimerRef.current) return;
3031
+ clearTimeout(unlockTimerRef.current);
3032
+ unlockTimerRef.current = null;
3033
+ };
3034
+ const animateThumb = (nextChecked, shouldUnlock = true) => {
3035
+ Animated2.timing(thumbTranslateX, {
3036
+ toValue: nextChecked ? maxTranslateX : 0,
3037
+ duration: 180,
3038
+ useNativeDriver: true
3039
+ }).start((result) => {
3040
+ if (result?.finished ?? true) {
3041
+ thumbTranslateX.setValue(nextChecked ? maxTranslateX : 0);
3042
+ }
3043
+ if (shouldUnlock) {
3044
+ clearUnlockTimer();
3045
+ setIsInteractionLocked(false);
3046
+ }
3047
+ });
3048
+ };
3049
+ useEffect7(() => {
3050
+ if (isFirstRender.current) {
3051
+ thumbTranslateX.setValue(isChecked ? maxTranslateX : 0);
3052
+ isFirstRender.current = false;
3053
+ return;
3054
+ }
3055
+ animateThumb(isChecked);
3056
+ }, [isChecked, maxTranslateX, thumbTranslateX]);
3057
+ useEffect7(() => {
3058
+ return () => {
3059
+ clearUnlockTimer();
3060
+ };
3061
+ }, []);
2525
3062
  const toggle = () => {
2526
- if (disabled) return;
3063
+ if (disabled || isInteractionLocked) return;
2527
3064
  const newChecked = !isChecked;
3065
+ setIsInteractionLocked(true);
2528
3066
  if (checked === void 0) {
2529
3067
  setInternalChecked(newChecked);
3068
+ } else {
3069
+ animateThumb(newChecked, false);
3070
+ unlockTimerRef.current = setTimeout(() => {
3071
+ unlockTimerRef.current = null;
3072
+ setIsInteractionLocked(false);
3073
+ }, 220);
2530
3074
  }
2531
3075
  onChange?.(newChecked);
2532
3076
  };
2533
- const uncheckedTrackColor = colors.divider;
2534
- const checkedTrackColor = colors.primary;
2535
- const disabledOpacity = 0.4;
2536
- const sizes = {
2537
- sm: { width: 36, height: 20, thumb: 16, padding: 2 },
2538
- md: { width: 48, height: 26, thumb: 22, padding: 2 },
2539
- lg: { width: 60, height: 32, thumb: 28, padding: 2 }
2540
- };
2541
- const config = sizes[size];
2542
- const thumbPosition = isChecked ? config.width - config.thumb - config.padding : config.padding;
2543
- return /* @__PURE__ */ jsx26(
3077
+ const trackBackgroundColor = disabled ? isChecked ? colors.primarySurface : colors.divider : isChecked ? colors.primary : colors.divider;
3078
+ const trackBorderColor = disabled ? isChecked ? colors.primarySurface : colors.border : isChecked ? colors.primary : colors.border;
3079
+ const thumbBackgroundColor = disabled ? colors.card : colors.textInverse;
3080
+ return /* @__PURE__ */ jsx27(
2544
3081
  TouchableOpacity4,
2545
3082
  {
2546
3083
  onPress: toggle,
2547
- disabled,
3084
+ disabled: disabled || isInteractionLocked,
2548
3085
  className: cn(className),
2549
3086
  testID,
2550
- activeOpacity: disabled ? 1 : 0.8,
2551
- children: /* @__PURE__ */ jsx26(
3087
+ activeOpacity: disabled || isInteractionLocked ? 1 : 0.8,
3088
+ children: /* @__PURE__ */ jsx27(
2552
3089
  AppView,
2553
3090
  {
2554
3091
  className: "rounded-full",
@@ -2557,22 +3094,22 @@ function Switch({
2557
3094
  {
2558
3095
  width: config.width,
2559
3096
  height: config.height,
2560
- backgroundColor: isChecked ? checkedTrackColor : uncheckedTrackColor,
2561
- opacity: disabled ? disabledOpacity : 1
3097
+ backgroundColor: trackBackgroundColor,
3098
+ borderColor: trackBorderColor
2562
3099
  },
2563
3100
  style
2564
3101
  ],
2565
- children: /* @__PURE__ */ jsx26(
2566
- AppView,
3102
+ children: /* @__PURE__ */ jsx27(
3103
+ Animated2.View,
2567
3104
  {
2568
- className: "rounded-full",
2569
3105
  style: [
2570
3106
  styles8.thumb,
2571
3107
  {
2572
3108
  width: config.thumb,
2573
3109
  height: config.thumb,
2574
- backgroundColor: colors.textInverse,
2575
- transform: [{ translateX: thumbPosition }],
3110
+ borderRadius: config.thumb / 2,
3111
+ backgroundColor: thumbBackgroundColor,
3112
+ transform: [{ translateX: thumbTranslateX }],
2576
3113
  shadowColor: "#000000",
2577
3114
  shadowOffset: { width: 0, height: 1 },
2578
3115
  shadowOpacity: 0.25,
@@ -2589,7 +3126,8 @@ function Switch({
2589
3126
  var styles8 = StyleSheet9.create({
2590
3127
  track: {
2591
3128
  justifyContent: "center",
2592
- padding: 2
3129
+ padding: 2,
3130
+ borderWidth: 0.5
2593
3131
  },
2594
3132
  thumb: {
2595
3133
  elevation: 2,
@@ -2601,7 +3139,7 @@ var styles8 = StyleSheet9.create({
2601
3139
  });
2602
3140
 
2603
3141
  // src/ui/form/Slider.tsx
2604
- import { useState as useState16, useCallback as useCallback12, useRef as useRef6 } from "react";
3142
+ import { useCallback as useCallback13, useEffect as useEffect8, useMemo as useMemo5, useRef as useRef8, useState as useState17 } from "react";
2605
3143
  import {
2606
3144
  View as View6,
2607
3145
  PanResponder as PanResponder2,
@@ -2634,7 +3172,7 @@ function useFormThemeColors() {
2634
3172
  }
2635
3173
 
2636
3174
  // src/ui/form/Slider.tsx
2637
- import { jsx as jsx27, jsxs as jsxs9 } from "nativewind/jsx-runtime";
3175
+ import { jsx as jsx28, jsxs as jsxs9 } from "nativewind/jsx-runtime";
2638
3176
  function Slider({
2639
3177
  value,
2640
3178
  defaultValue = 0,
@@ -2648,53 +3186,82 @@ function Slider({
2648
3186
  className
2649
3187
  }) {
2650
3188
  const colors = useFormThemeColors();
2651
- const [internalValue, setInternalValue] = useState16(defaultValue);
2652
- const [trackWidth, setTrackWidth] = useState16(0);
2653
- const [isDragging, setIsDragging] = useState16(false);
3189
+ const [internalValue, setInternalValue] = useState17(defaultValue);
3190
+ const [isDragging, setIsDragging] = useState17(false);
3191
+ const trackWidthRef = useRef8(0);
3192
+ const currentValueRef = useRef8(value ?? defaultValue);
3193
+ const dragStartValueRef = useRef8(value ?? defaultValue);
2654
3194
  const currentValue = value !== void 0 ? value : internalValue;
3195
+ const range = max - min;
2655
3196
  const disabledOpacity = 0.4;
2656
- const progress = (currentValue - min) / (max - min) * 100;
2657
- const getValueFromPosition = useCallback12(
3197
+ const progress = range <= 0 ? 0 : (currentValue - min) / range * 100;
3198
+ useEffect8(() => {
3199
+ currentValueRef.current = currentValue;
3200
+ }, [currentValue]);
3201
+ const clampValue = useCallback13(
3202
+ (nextValue) => {
3203
+ if (!Number.isFinite(nextValue)) return currentValueRef.current;
3204
+ return Math.min(max, Math.max(min, nextValue));
3205
+ },
3206
+ [max, min]
3207
+ );
3208
+ const valueToPosition = useCallback13(
3209
+ (nextValue) => {
3210
+ if (trackWidthRef.current <= 0 || range <= 0) return 0;
3211
+ return (clampValue(nextValue) - min) / range * trackWidthRef.current;
3212
+ },
3213
+ [clampValue, min, range]
3214
+ );
3215
+ const getValueFromPosition = useCallback13(
2658
3216
  (position) => {
2659
- const percentage = Math.max(0, Math.min(1, position / trackWidth));
2660
- const rawValue = min + percentage * (max - min);
2661
- const steppedValue = Math.round(rawValue / step) * step;
2662
- return Math.min(max, Math.max(min, steppedValue));
3217
+ if (trackWidthRef.current <= 0 || range <= 0 || !Number.isFinite(position)) {
3218
+ return clampValue(currentValueRef.current);
3219
+ }
3220
+ const percentage = Math.max(0, Math.min(1, position / trackWidthRef.current));
3221
+ const rawValue = min + percentage * range;
3222
+ const steppedValue = step > 0 ? Math.round((rawValue - min) / step) * step + min : rawValue;
3223
+ return clampValue(steppedValue);
2663
3224
  },
2664
- [trackWidth, min, max, step]
3225
+ [clampValue, min, range, step]
2665
3226
  );
2666
- const setValue = useCallback12(
3227
+ const setValue = useCallback13(
2667
3228
  (newValue) => {
2668
- const clampedValue = Math.min(max, Math.max(min, newValue));
3229
+ const clampedValue = clampValue(newValue);
2669
3230
  if (value === void 0) {
2670
3231
  setInternalValue(clampedValue);
2671
3232
  }
3233
+ currentValueRef.current = clampedValue;
2672
3234
  onChange?.(clampedValue);
2673
3235
  },
2674
- [value, min, max, onChange]
3236
+ [clampValue, onChange, value]
2675
3237
  );
2676
- const panResponder = useRef6(
2677
- PanResponder2.create({
3238
+ const panResponder = useMemo5(
3239
+ () => PanResponder2.create({
2678
3240
  onStartShouldSetPanResponder: () => !disabled,
2679
3241
  onMoveShouldSetPanResponder: () => !disabled,
2680
3242
  onPanResponderGrant: () => {
3243
+ dragStartValueRef.current = currentValueRef.current;
2681
3244
  setIsDragging(true);
2682
3245
  },
2683
3246
  onPanResponderMove: (_, gestureState) => {
2684
- const position = progress / 100 * trackWidth + gestureState.dx;
3247
+ const position = valueToPosition(dragStartValueRef.current) + gestureState.dx;
2685
3248
  const newValue = getValueFromPosition(position);
2686
3249
  setValue(newValue);
2687
3250
  },
2688
3251
  onPanResponderRelease: (_, gestureState) => {
2689
- const position = progress / 100 * trackWidth + gestureState.dx;
3252
+ const position = valueToPosition(dragStartValueRef.current) + gestureState.dx;
2690
3253
  const newValue = getValueFromPosition(position);
2691
3254
  setValue(newValue);
2692
3255
  setIsDragging(false);
2693
3256
  onChangeEnd?.(newValue);
3257
+ },
3258
+ onPanResponderTerminate: () => {
3259
+ setIsDragging(false);
2694
3260
  }
2695
- })
2696
- ).current;
2697
- const handleTrackPress = useCallback12(
3261
+ }),
3262
+ [disabled, getValueFromPosition, onChangeEnd, setValue, valueToPosition]
3263
+ );
3264
+ const handleTrackPress = useCallback13(
2698
3265
  (event) => {
2699
3266
  if (disabled) return;
2700
3267
  const { locationX } = event.nativeEvent;
@@ -2704,8 +3271,9 @@ function Slider({
2704
3271
  },
2705
3272
  [disabled, getValueFromPosition, setValue, onChangeEnd]
2706
3273
  );
2707
- const onLayout = useCallback12((event) => {
2708
- setTrackWidth(event.nativeEvent.layout.width);
3274
+ const onLayout = useCallback13((event) => {
3275
+ const width = event.nativeEvent.layout.width;
3276
+ trackWidthRef.current = width;
2709
3277
  }, []);
2710
3278
  return /* @__PURE__ */ jsxs9(AppView, { className: cn("py-2", className), children: [
2711
3279
  showTooltip && isDragging && /* @__PURE__ */ jsxs9(
@@ -2721,8 +3289,8 @@ function Slider({
2721
3289
  }
2722
3290
  ],
2723
3291
  children: [
2724
- /* @__PURE__ */ jsx27(AppText, { size: "xs", style: { color: colors.text }, children: Math.round(currentValue) }),
2725
- /* @__PURE__ */ jsx27(
3292
+ /* @__PURE__ */ jsx28(AppText, { size: "xs", style: { color: colors.text }, children: Math.round(currentValue) }),
3293
+ /* @__PURE__ */ jsx28(
2726
3294
  AppView,
2727
3295
  {
2728
3296
  style: [
@@ -2747,7 +3315,7 @@ function Slider({
2747
3315
  ],
2748
3316
  onTouchEnd: handleTrackPress,
2749
3317
  children: [
2750
- /* @__PURE__ */ jsx27(
3318
+ /* @__PURE__ */ jsx28(
2751
3319
  AppView,
2752
3320
  {
2753
3321
  className: "rounded-full",
@@ -2760,7 +3328,7 @@ function Slider({
2760
3328
  ]
2761
3329
  }
2762
3330
  ),
2763
- /* @__PURE__ */ jsx27(
3331
+ /* @__PURE__ */ jsx28(
2764
3332
  AppView,
2765
3333
  {
2766
3334
  className: "absolute rounded-full items-center justify-center",
@@ -2777,7 +3345,7 @@ function Slider({
2777
3345
  }
2778
3346
  ],
2779
3347
  ...panResponder.panHandlers,
2780
- children: /* @__PURE__ */ jsx27(
3348
+ children: /* @__PURE__ */ jsx28(
2781
3349
  AppView,
2782
3350
  {
2783
3351
  className: "rounded-full",
@@ -2837,16 +3405,215 @@ var styles9 = StyleSheet10.create({
2837
3405
  });
2838
3406
 
2839
3407
  // src/ui/form/Select.tsx
2840
- import { useState as useState17, useCallback as useCallback13, useMemo as useMemo5 } from "react";
3408
+ import { useState as useState19, useCallback as useCallback14, useMemo as useMemo7 } from "react";
2841
3409
  import {
2842
- Modal as Modal3,
2843
3410
  View as View7,
2844
3411
  TouchableOpacity as TouchableOpacity5,
2845
3412
  FlatList as FlatList2,
2846
3413
  TextInput as TextInput2,
3414
+ StyleSheet as StyleSheet12
3415
+ } from "react-native";
3416
+
3417
+ // src/ui/form/BottomSheetModal.tsx
3418
+ import { useEffect as useEffect9, useMemo as useMemo6, useRef as useRef9, useState as useState18 } from "react";
3419
+ import {
3420
+ Animated as Animated3,
3421
+ Modal as Modal3,
3422
+ PanResponder as PanResponder3,
2847
3423
  StyleSheet as StyleSheet11
2848
3424
  } from "react-native";
2849
- import { Fragment as Fragment2, jsx as jsx28, jsxs as jsxs10 } from "nativewind/jsx-runtime";
3425
+ import { jsx as jsx29, jsxs as jsxs10 } from "nativewind/jsx-runtime";
3426
+ var SHEET_OPEN_DURATION = 220;
3427
+ var SHEET_CLOSE_DURATION = 180;
3428
+ var OVERLAY_OPEN_DURATION = 180;
3429
+ var OVERLAY_CLOSE_DURATION = 160;
3430
+ var SHEET_INITIAL_OFFSET = 24;
3431
+ var SHEET_CLOSED_OFFSET = 240;
3432
+ var SHEET_DRAG_CLOSE_THRESHOLD = 72;
3433
+ var SHEET_DRAG_VELOCITY_THRESHOLD = 1;
3434
+ function createAnimatedValue3(value) {
3435
+ const AnimatedValue = Animated3.Value;
3436
+ try {
3437
+ return new AnimatedValue(value);
3438
+ } catch {
3439
+ return AnimatedValue(value);
3440
+ }
3441
+ }
3442
+ function startAnimations(animations, onComplete) {
3443
+ if (animations.length === 0) {
3444
+ onComplete?.();
3445
+ return;
3446
+ }
3447
+ let completed = 0;
3448
+ animations.forEach((animation) => {
3449
+ animation.start(() => {
3450
+ completed += 1;
3451
+ if (completed >= animations.length) {
3452
+ onComplete?.();
3453
+ }
3454
+ });
3455
+ });
3456
+ }
3457
+ function BottomSheetModal({
3458
+ visible,
3459
+ onRequestClose,
3460
+ overlayColor,
3461
+ surfaceColor,
3462
+ children,
3463
+ closeOnBackdropPress = false,
3464
+ maxHeight = "70%",
3465
+ showHandle = true,
3466
+ contentClassName,
3467
+ contentStyle,
3468
+ swipeToClose = true,
3469
+ backdropTestID = "bottom-sheet-backdrop",
3470
+ handleTestID = "bottom-sheet-handle"
3471
+ }) {
3472
+ const [renderVisible, setRenderVisible] = useState18(visible);
3473
+ const overlayOpacity = useRef9(createAnimatedValue3(0)).current;
3474
+ const sheetTranslateY = useRef9(createAnimatedValue3(SHEET_INITIAL_OFFSET)).current;
3475
+ const isDraggingRef = useRef9(false);
3476
+ const handlePanResponder = useMemo6(
3477
+ () => PanResponder3.create({
3478
+ onMoveShouldSetPanResponder: (_event, gestureState) => {
3479
+ if (!visible || !swipeToClose) return false;
3480
+ const isVertical = Math.abs(gestureState.dy) > Math.abs(gestureState.dx);
3481
+ return isVertical && gestureState.dy > 6;
3482
+ },
3483
+ onPanResponderGrant: () => {
3484
+ isDraggingRef.current = true;
3485
+ },
3486
+ onPanResponderMove: (_event, gestureState) => {
3487
+ if (!visible || !swipeToClose) return;
3488
+ sheetTranslateY.setValue(Math.max(0, Math.min(SHEET_CLOSED_OFFSET, gestureState.dy)));
3489
+ },
3490
+ onPanResponderRelease: (_event, gestureState) => {
3491
+ isDraggingRef.current = false;
3492
+ if (!visible || !swipeToClose) {
3493
+ sheetTranslateY.setValue(0);
3494
+ return;
3495
+ }
3496
+ const shouldClose = gestureState.dy >= SHEET_DRAG_CLOSE_THRESHOLD || gestureState.vy >= SHEET_DRAG_VELOCITY_THRESHOLD;
3497
+ if (shouldClose) {
3498
+ onRequestClose();
3499
+ return;
3500
+ }
3501
+ Animated3.timing(sheetTranslateY, {
3502
+ toValue: 0,
3503
+ duration: SHEET_OPEN_DURATION,
3504
+ useNativeDriver: true
3505
+ }).start();
3506
+ },
3507
+ onPanResponderTerminate: () => {
3508
+ isDraggingRef.current = false;
3509
+ Animated3.timing(sheetTranslateY, {
3510
+ toValue: 0,
3511
+ duration: SHEET_OPEN_DURATION,
3512
+ useNativeDriver: true
3513
+ }).start();
3514
+ }
3515
+ }),
3516
+ [onRequestClose, sheetTranslateY, swipeToClose, visible]
3517
+ );
3518
+ useEffect9(() => {
3519
+ if (visible) {
3520
+ setRenderVisible(true);
3521
+ overlayOpacity.setValue(0);
3522
+ sheetTranslateY.setValue(SHEET_INITIAL_OFFSET);
3523
+ startAnimations([
3524
+ Animated3.timing(overlayOpacity, {
3525
+ toValue: 1,
3526
+ duration: OVERLAY_OPEN_DURATION,
3527
+ useNativeDriver: true
3528
+ }),
3529
+ Animated3.timing(sheetTranslateY, {
3530
+ toValue: 0,
3531
+ duration: SHEET_OPEN_DURATION,
3532
+ useNativeDriver: true
3533
+ })
3534
+ ]);
3535
+ return;
3536
+ }
3537
+ if (!renderVisible) return;
3538
+ isDraggingRef.current = false;
3539
+ startAnimations(
3540
+ [
3541
+ Animated3.timing(overlayOpacity, {
3542
+ toValue: 0,
3543
+ duration: OVERLAY_CLOSE_DURATION,
3544
+ useNativeDriver: true
3545
+ }),
3546
+ Animated3.timing(sheetTranslateY, {
3547
+ toValue: SHEET_CLOSED_OFFSET,
3548
+ duration: SHEET_CLOSE_DURATION,
3549
+ useNativeDriver: true
3550
+ })
3551
+ ],
3552
+ () => {
3553
+ setRenderVisible(false);
3554
+ }
3555
+ );
3556
+ }, [overlayOpacity, renderVisible, sheetTranslateY, visible]);
3557
+ return /* @__PURE__ */ jsx29(Modal3, { visible: renderVisible, transparent: true, animationType: "none", onRequestClose, children: /* @__PURE__ */ jsxs10(AppView, { flex: true, justify: "end", children: [
3558
+ /* @__PURE__ */ jsx29(
3559
+ Animated3.View,
3560
+ {
3561
+ pointerEvents: "none",
3562
+ style: [StyleSheet11.absoluteFillObject, { backgroundColor: overlayColor, opacity: overlayOpacity }]
3563
+ }
3564
+ ),
3565
+ closeOnBackdropPress && /* @__PURE__ */ jsx29(AppPressable, { testID: backdropTestID, className: "flex-1", onPress: onRequestClose }),
3566
+ /* @__PURE__ */ jsxs10(
3567
+ Animated3.View,
3568
+ {
3569
+ className: contentClassName,
3570
+ style: [
3571
+ styles10.sheet,
3572
+ {
3573
+ backgroundColor: surfaceColor,
3574
+ maxHeight,
3575
+ transform: [{ translateY: sheetTranslateY }]
3576
+ },
3577
+ contentStyle
3578
+ ],
3579
+ children: [
3580
+ showHandle && /* @__PURE__ */ jsx29(
3581
+ AppView,
3582
+ {
3583
+ testID: handleTestID,
3584
+ center: true,
3585
+ className: "pt-2 pb-1",
3586
+ ...swipeToClose ? handlePanResponder.panHandlers : void 0,
3587
+ children: /* @__PURE__ */ jsx29(AppView, { style: styles10.handle })
3588
+ }
3589
+ ),
3590
+ children
3591
+ ]
3592
+ }
3593
+ )
3594
+ ] }) });
3595
+ }
3596
+ var styles10 = StyleSheet11.create({
3597
+ handle: {
3598
+ width: 36,
3599
+ height: 4,
3600
+ borderRadius: 999,
3601
+ backgroundColor: "rgba(156,163,175,0.7)"
3602
+ },
3603
+ sheet: {
3604
+ borderTopLeftRadius: 24,
3605
+ borderTopRightRadius: 24,
3606
+ overflow: "hidden",
3607
+ shadowColor: "#000000",
3608
+ shadowOffset: { width: 0, height: -4 },
3609
+ shadowOpacity: 0.12,
3610
+ shadowRadius: 16,
3611
+ elevation: 12
3612
+ }
3613
+ });
3614
+
3615
+ // src/ui/form/Select.tsx
3616
+ import { Fragment as Fragment2, jsx as jsx30, jsxs as jsxs11 } from "nativewind/jsx-runtime";
2850
3617
  function formatSelectedCountText(template, count) {
2851
3618
  return template.replace("{{count}}", String(count));
2852
3619
  }
@@ -2869,24 +3636,24 @@ function Select({
2869
3636
  className
2870
3637
  }) {
2871
3638
  const colors = useFormThemeColors();
2872
- const [visible, setVisible] = useState17(false);
2873
- const [searchKeyword, setSearchKeyword] = useState17("");
2874
- const selectedValues = useMemo5(() => {
3639
+ const [visible, setVisible] = useState19(false);
3640
+ const [searchKeyword, setSearchKeyword] = useState19("");
3641
+ const selectedValues = useMemo7(() => {
2875
3642
  if (multiple) {
2876
3643
  return Array.isArray(value) ? value : [];
2877
3644
  }
2878
3645
  return value ? [value] : [];
2879
3646
  }, [value, multiple]);
2880
- const displayText = useMemo5(() => {
3647
+ const displayText = useMemo7(() => {
2881
3648
  if (selectedValues.length === 0) return placeholder;
2882
3649
  const selectedLabels = options.filter((opt) => selectedValues.includes(opt.value)).map((opt) => opt.label);
2883
3650
  return selectedLabels.join(", ") || placeholder;
2884
3651
  }, [selectedValues, options, placeholder]);
2885
- const filteredOptions = useMemo5(() => {
3652
+ const filteredOptions = useMemo7(() => {
2886
3653
  if (!searchable || !searchKeyword) return options;
2887
3654
  return options.filter((opt) => opt.label.toLowerCase().includes(searchKeyword.toLowerCase()));
2888
3655
  }, [options, searchable, searchKeyword]);
2889
- const handleSelect = useCallback13(
3656
+ const handleSelect = useCallback14(
2890
3657
  (optionValue) => {
2891
3658
  if (multiple) {
2892
3659
  const currentValues = Array.isArray(value) ? value : [];
@@ -2897,26 +3664,26 @@ function Select({
2897
3664
  setVisible(false);
2898
3665
  }
2899
3666
  },
2900
- [multiple, value, onChange]
3667
+ [multiple, onChange, value]
2901
3668
  );
2902
- const handleClear = useCallback13(
3669
+ const handleClear = useCallback14(
2903
3670
  (e) => {
2904
3671
  e.stopPropagation();
2905
3672
  onChange?.(multiple ? [] : "");
2906
3673
  },
2907
3674
  [multiple, onChange]
2908
3675
  );
2909
- const handleSearch = useCallback13(
3676
+ const handleSearch = useCallback14(
2910
3677
  (text) => {
2911
3678
  setSearchKeyword(text);
2912
3679
  onSearch?.(text);
2913
3680
  },
2914
3681
  [onSearch]
2915
3682
  );
2916
- const renderOption = useCallback13(
3683
+ const renderOption = useCallback14(
2917
3684
  ({ item }) => {
2918
3685
  const isSelected = selectedValues.includes(item.value);
2919
- return /* @__PURE__ */ jsxs10(
3686
+ return /* @__PURE__ */ jsxs11(
2920
3687
  AppPressable,
2921
3688
  {
2922
3689
  className: cn(
@@ -2924,22 +3691,22 @@ function Select({
2924
3691
  isSelected && "bg-primary-50"
2925
3692
  ),
2926
3693
  style: [
2927
- styles10.optionItem,
3694
+ styles11.optionItem,
2928
3695
  { borderBottomColor: colors.divider },
2929
3696
  isSelected && { backgroundColor: colors.primarySurface }
2930
3697
  ],
2931
3698
  onPress: () => handleSelect(item.value),
2932
3699
  children: [
2933
- /* @__PURE__ */ jsx28(AppText, { style: { color: isSelected ? colors.primary : colors.text }, children: item.label }),
2934
- isSelected && /* @__PURE__ */ jsx28(Icon, { name: "check", size: "sm", color: "primary-500" })
3700
+ /* @__PURE__ */ jsx30(AppText, { style: { color: isSelected ? colors.primary : colors.text }, children: item.label }),
3701
+ isSelected && /* @__PURE__ */ jsx30(Icon, { name: "check", size: "sm", color: "primary-500" })
2935
3702
  ]
2936
3703
  }
2937
3704
  );
2938
3705
  },
2939
3706
  [selectedValues, handleSelect, colors]
2940
3707
  );
2941
- return /* @__PURE__ */ jsxs10(Fragment2, { children: [
2942
- /* @__PURE__ */ jsxs10(
3708
+ return /* @__PURE__ */ jsxs11(Fragment2, { children: [
3709
+ /* @__PURE__ */ jsxs11(
2943
3710
  AppPressable,
2944
3711
  {
2945
3712
  className: cn(
@@ -2947,11 +3714,11 @@ function Select({
2947
3714
  disabled ? "opacity-60" : "",
2948
3715
  className
2949
3716
  ),
2950
- style: [styles10.trigger, { backgroundColor: colors.surface, borderColor: colors.border }],
3717
+ style: [styles11.trigger, { backgroundColor: colors.surface, borderColor: colors.border }],
2951
3718
  disabled,
2952
3719
  onPress: () => setVisible(true),
2953
3720
  children: [
2954
- /* @__PURE__ */ jsx28(
3721
+ /* @__PURE__ */ jsx30(
2955
3722
  AppText,
2956
3723
  {
2957
3724
  className: "flex-1",
@@ -2960,111 +3727,106 @@ function Select({
2960
3727
  children: displayText
2961
3728
  }
2962
3729
  ),
2963
- /* @__PURE__ */ jsxs10(View7, { className: "flex-row items-center", children: [
2964
- clearable && selectedValues.length > 0 && !disabled && /* @__PURE__ */ jsx28(TouchableOpacity5, { onPress: handleClear, className: "mr-2 p-1", children: /* @__PURE__ */ jsx28(Icon, { name: "close", size: "sm", color: colors.icon }) }),
2965
- /* @__PURE__ */ jsx28(Icon, { name: "keyboard-arrow-down", size: "md", color: colors.icon })
3730
+ /* @__PURE__ */ jsxs11(View7, { className: "flex-row items-center", children: [
3731
+ clearable && selectedValues.length > 0 && !disabled && /* @__PURE__ */ jsx30(TouchableOpacity5, { onPress: handleClear, className: "mr-2 p-1", children: /* @__PURE__ */ jsx30(Icon, { name: "close", size: "sm", color: colors.icon }) }),
3732
+ /* @__PURE__ */ jsx30(Icon, { name: "keyboard-arrow-down", size: "md", color: colors.icon })
2966
3733
  ] })
2967
3734
  ]
2968
3735
  }
2969
3736
  ),
2970
- /* @__PURE__ */ jsx28(
2971
- Modal3,
3737
+ /* @__PURE__ */ jsx30(
3738
+ BottomSheetModal,
2972
3739
  {
2973
3740
  visible,
2974
- transparent: true,
2975
- animationType: "slide",
2976
3741
  onRequestClose: () => setVisible(false),
2977
- children: /* @__PURE__ */ jsx28(AppView, { className: "flex-1", style: { backgroundColor: colors.overlay }, justify: "end", children: /* @__PURE__ */ jsxs10(
2978
- AppView,
2979
- {
2980
- className: "rounded-t-2xl max-h-[70%]",
2981
- style: { backgroundColor: colors.surface },
2982
- children: [
2983
- /* @__PURE__ */ jsxs10(
2984
- AppView,
2985
- {
2986
- row: true,
2987
- between: true,
2988
- items: "center",
2989
- className: "px-4 py-3",
2990
- style: [styles10.header, { borderBottomColor: colors.divider }],
2991
- children: [
2992
- /* @__PURE__ */ jsx28(AppText, { className: "text-lg font-semibold", style: { color: colors.text }, children: multiple ? multipleSelectTitle : singleSelectTitle }),
2993
- /* @__PURE__ */ jsx28(TouchableOpacity5, { onPress: () => setVisible(false), children: /* @__PURE__ */ jsx28(Icon, { name: "close", size: "md", color: colors.icon }) })
2994
- ]
2995
- }
2996
- ),
2997
- searchable && /* @__PURE__ */ jsx28(
2998
- AppView,
2999
- {
3000
- className: "px-4 py-3",
3001
- style: [styles10.searchBox, { borderBottomColor: colors.divider }],
3002
- children: /* @__PURE__ */ jsxs10(
3003
- AppView,
3004
- {
3005
- row: true,
3006
- items: "center",
3007
- className: "px-3 py-2 rounded-lg",
3008
- style: { backgroundColor: colors.surfaceMuted },
3009
- children: [
3010
- /* @__PURE__ */ jsx28(View7, { style: { marginRight: 8 }, children: /* @__PURE__ */ jsx28(Icon, { name: "search", size: "sm", color: colors.icon }) }),
3011
- /* @__PURE__ */ jsx28(
3012
- TextInput2,
3013
- {
3014
- className: "flex-1 text-base",
3015
- style: { color: colors.text },
3016
- placeholder: searchPlaceholder,
3017
- placeholderTextColor: colors.textMuted,
3018
- value: searchKeyword,
3019
- onChangeText: handleSearch,
3020
- autoFocus: true
3021
- }
3022
- ),
3023
- searchKeyword.length > 0 && /* @__PURE__ */ jsx28(TouchableOpacity5, { onPress: () => setSearchKeyword(""), children: /* @__PURE__ */ jsx28(Icon, { name: "close", size: "sm", color: colors.icon }) })
3024
- ]
3025
- }
3026
- )
3027
- }
3028
- ),
3029
- /* @__PURE__ */ jsx28(
3030
- FlatList2,
3031
- {
3032
- data: filteredOptions,
3033
- keyExtractor: (item) => item.value,
3034
- renderItem: renderOption,
3035
- ListEmptyComponent: /* @__PURE__ */ jsx28(AppView, { center: true, className: "py-8", children: /* @__PURE__ */ jsx28(AppText, { style: { color: colors.textMuted }, children: emptyText }) })
3036
- }
3037
- ),
3038
- multiple && /* @__PURE__ */ jsxs10(
3742
+ overlayColor: colors.overlay,
3743
+ surfaceColor: colors.surface,
3744
+ closeOnBackdropPress: true,
3745
+ contentClassName: "max-h-[70%]",
3746
+ children: /* @__PURE__ */ jsxs11(Fragment2, { children: [
3747
+ /* @__PURE__ */ jsxs11(
3748
+ AppView,
3749
+ {
3750
+ row: true,
3751
+ between: true,
3752
+ items: "center",
3753
+ className: "px-4 py-3",
3754
+ style: [styles11.header, { borderBottomColor: colors.divider }],
3755
+ children: [
3756
+ /* @__PURE__ */ jsx30(AppText, { className: "text-lg font-semibold", style: { color: colors.text }, children: multiple ? multipleSelectTitle : singleSelectTitle }),
3757
+ /* @__PURE__ */ jsx30(TouchableOpacity5, { onPress: () => setVisible(false), children: /* @__PURE__ */ jsx30(Icon, { name: "close", size: "md", color: colors.icon }) })
3758
+ ]
3759
+ }
3760
+ ),
3761
+ searchable && /* @__PURE__ */ jsx30(
3762
+ AppView,
3763
+ {
3764
+ className: "px-4 py-3",
3765
+ style: [styles11.searchBox, { borderBottomColor: colors.divider }],
3766
+ children: /* @__PURE__ */ jsxs11(
3039
3767
  AppView,
3040
3768
  {
3041
3769
  row: true,
3042
- between: true,
3043
3770
  items: "center",
3044
- className: "px-4 py-3",
3045
- style: [styles10.footer, { borderTopColor: colors.divider }],
3771
+ className: "px-3 py-2 rounded-lg",
3772
+ style: { backgroundColor: colors.surfaceMuted },
3046
3773
  children: [
3047
- /* @__PURE__ */ jsx28(AppText, { style: { color: colors.textMuted }, children: formatSelectedCountText(selectedCountText, selectedValues.length) }),
3048
- /* @__PURE__ */ jsx28(
3049
- TouchableOpacity5,
3774
+ /* @__PURE__ */ jsx30(View7, { style: { marginRight: 8 }, children: /* @__PURE__ */ jsx30(Icon, { name: "search", size: "sm", color: colors.icon }) }),
3775
+ /* @__PURE__ */ jsx30(
3776
+ TextInput2,
3050
3777
  {
3051
- className: "px-4 py-2 rounded-lg",
3052
- style: { backgroundColor: colors.primary },
3053
- onPress: () => setVisible(false),
3054
- children: /* @__PURE__ */ jsx28(AppText, { className: "font-medium", style: { color: colors.textInverse }, children: confirmText })
3778
+ className: "flex-1 text-base",
3779
+ style: { color: colors.text },
3780
+ placeholder: searchPlaceholder,
3781
+ placeholderTextColor: colors.textMuted,
3782
+ value: searchKeyword,
3783
+ onChangeText: handleSearch,
3784
+ autoFocus: true
3055
3785
  }
3056
- )
3786
+ ),
3787
+ searchKeyword.length > 0 && /* @__PURE__ */ jsx30(TouchableOpacity5, { onPress: () => setSearchKeyword(""), children: /* @__PURE__ */ jsx30(Icon, { name: "close", size: "sm", color: colors.icon }) })
3057
3788
  ]
3058
3789
  }
3059
3790
  )
3060
- ]
3061
- }
3062
- ) })
3791
+ }
3792
+ ),
3793
+ /* @__PURE__ */ jsx30(
3794
+ FlatList2,
3795
+ {
3796
+ data: filteredOptions,
3797
+ keyExtractor: (item, index) => `${item.value}-${index}`,
3798
+ renderItem: renderOption,
3799
+ ListEmptyComponent: /* @__PURE__ */ jsx30(AppView, { center: true, className: "py-8", children: /* @__PURE__ */ jsx30(AppText, { style: { color: colors.textMuted }, children: emptyText }) })
3800
+ }
3801
+ ),
3802
+ multiple && /* @__PURE__ */ jsxs11(
3803
+ AppView,
3804
+ {
3805
+ row: true,
3806
+ between: true,
3807
+ items: "center",
3808
+ className: "px-4 py-3",
3809
+ style: [styles11.footer, { borderTopColor: colors.divider }],
3810
+ children: [
3811
+ /* @__PURE__ */ jsx30(AppText, { style: { color: colors.textMuted }, children: formatSelectedCountText(selectedCountText, selectedValues.length) }),
3812
+ /* @__PURE__ */ jsx30(
3813
+ TouchableOpacity5,
3814
+ {
3815
+ className: "px-4 py-2 rounded-lg",
3816
+ style: { backgroundColor: colors.primary },
3817
+ onPress: () => setVisible(false),
3818
+ children: /* @__PURE__ */ jsx30(AppText, { className: "font-medium", style: { color: colors.textInverse }, children: confirmText })
3819
+ }
3820
+ )
3821
+ ]
3822
+ }
3823
+ )
3824
+ ] })
3063
3825
  }
3064
3826
  )
3065
3827
  ] });
3066
3828
  }
3067
- var styles10 = StyleSheet11.create({
3829
+ var styles11 = StyleSheet12.create({
3068
3830
  trigger: {
3069
3831
  borderWidth: 0.5
3070
3832
  },
@@ -3082,126 +3844,306 @@ var styles10 = StyleSheet11.create({
3082
3844
  }
3083
3845
  });
3084
3846
 
3085
- // src/ui/form/DatePicker.tsx
3086
- import { useState as useState18, useCallback as useCallback14, useMemo as useMemo6 } from "react";
3087
- import { Modal as Modal4, TouchableOpacity as TouchableOpacity6, StyleSheet as StyleSheet12 } from "react-native";
3088
- import { Fragment as Fragment3, jsx as jsx29, jsxs as jsxs11 } from "nativewind/jsx-runtime";
3089
- function PickerColumn({
3090
- title,
3091
- values,
3847
+ // src/ui/form/Picker.tsx
3848
+ import { useCallback as useCallback15, useEffect as useEffect10, useMemo as useMemo8, useRef as useRef10, useState as useState20 } from "react";
3849
+ import {
3850
+ ScrollView as ScrollView2,
3851
+ StyleSheet as StyleSheet13,
3852
+ TouchableOpacity as TouchableOpacity6
3853
+ } from "react-native";
3854
+ import { Fragment as Fragment3, jsx as jsx31, jsxs as jsxs12 } from "nativewind/jsx-runtime";
3855
+ function findFirstEnabledValue(column) {
3856
+ return column.options.find((option) => !option.disabled)?.value;
3857
+ }
3858
+ function normalizeValues(columns, values) {
3859
+ return columns.map((column, index) => {
3860
+ const candidate = values?.[index];
3861
+ const matched = column.options.find((option) => option.value === candidate && !option.disabled);
3862
+ return matched?.value ?? findFirstEnabledValue(column) ?? column.options[0]?.value ?? "";
3863
+ });
3864
+ }
3865
+ function WheelPickerColumn({
3866
+ colors,
3867
+ column,
3868
+ onChange,
3869
+ rowHeight,
3092
3870
  selectedValue,
3093
- onSelect,
3094
- isDisabled,
3095
- formatLabel = (value) => String(value),
3096
3871
  showDivider = false,
3097
- colors
3872
+ visibleRows
3098
3873
  }) {
3099
- return /* @__PURE__ */ jsxs11(
3874
+ const scrollRef = useRef10(null);
3875
+ const paddingRows = Math.floor(visibleRows / 2);
3876
+ const selectedIndex = Math.max(
3877
+ 0,
3878
+ column.options.findIndex((option) => option.value === selectedValue)
3879
+ );
3880
+ const scrollToIndex = useCallback15(
3881
+ (index, animated) => {
3882
+ scrollRef.current?.scrollTo?.({ y: index * rowHeight, animated });
3883
+ },
3884
+ [rowHeight]
3885
+ );
3886
+ const selectNearestEnabled = useCallback15(
3887
+ (targetIndex) => {
3888
+ if (column.options.length === 0) return;
3889
+ const maxIndex = column.options.length - 1;
3890
+ const clampedIndex = Math.max(0, Math.min(maxIndex, targetIndex));
3891
+ const exactOption = column.options[clampedIndex];
3892
+ if (exactOption && !exactOption.disabled) {
3893
+ onChange(exactOption.value);
3894
+ scrollToIndex(clampedIndex, true);
3895
+ return;
3896
+ }
3897
+ for (let distance = 1; distance <= maxIndex; distance += 1) {
3898
+ const prevIndex = clampedIndex - distance;
3899
+ if (prevIndex >= 0) {
3900
+ const prevOption = column.options[prevIndex];
3901
+ if (prevOption && !prevOption.disabled) {
3902
+ onChange(prevOption.value);
3903
+ scrollToIndex(prevIndex, true);
3904
+ return;
3905
+ }
3906
+ }
3907
+ const nextIndex = clampedIndex + distance;
3908
+ if (nextIndex <= maxIndex) {
3909
+ const nextOption = column.options[nextIndex];
3910
+ if (nextOption && !nextOption.disabled) {
3911
+ onChange(nextOption.value);
3912
+ scrollToIndex(nextIndex, true);
3913
+ return;
3914
+ }
3915
+ }
3916
+ }
3917
+ },
3918
+ [column.options, onChange, scrollToIndex]
3919
+ );
3920
+ const handleScrollEnd = useCallback15(
3921
+ (event) => {
3922
+ const offsetY = event.nativeEvent.contentOffset?.y ?? 0;
3923
+ selectNearestEnabled(Math.round(offsetY / rowHeight));
3924
+ },
3925
+ [rowHeight, selectNearestEnabled]
3926
+ );
3927
+ useEffect10(() => {
3928
+ scrollToIndex(selectedIndex, false);
3929
+ }, [scrollToIndex, selectedIndex]);
3930
+ return /* @__PURE__ */ jsxs12(
3100
3931
  AppView,
3101
3932
  {
3102
3933
  flex: true,
3103
3934
  style: [
3104
- showDivider && styles11.column,
3935
+ showDivider ? styles12.columnDivider : void 0,
3105
3936
  showDivider ? { borderRightColor: colors.divider } : void 0
3106
3937
  ],
3107
3938
  children: [
3108
- /* @__PURE__ */ jsx29(AppView, { center: true, className: "py-2", style: { backgroundColor: colors.headerSurface }, children: /* @__PURE__ */ jsx29(AppText, { className: "text-sm font-medium", style: { color: colors.textMuted }, children: title }) }),
3109
- /* @__PURE__ */ jsx29(AppView, { className: "flex-1", children: values.map((value) => {
3110
- const selected = selectedValue === value;
3111
- const disabled = isDisabled(value);
3112
- return /* @__PURE__ */ jsx29(
3113
- TouchableOpacity6,
3114
- {
3115
- className: cn("py-2 items-center", selected && "bg-primary-50"),
3116
- style: selected ? { backgroundColor: colors.primarySurface } : void 0,
3117
- disabled,
3118
- onPress: () => onSelect(value),
3119
- children: /* @__PURE__ */ jsx29(
3120
- AppText,
3939
+ column.title && /* @__PURE__ */ jsx31(AppView, { center: true, className: "py-2", style: { backgroundColor: colors.headerSurface }, children: /* @__PURE__ */ jsx31(AppText, { className: "text-sm font-medium", style: { color: colors.textMuted }, children: column.title }) }),
3940
+ /* @__PURE__ */ jsxs12(
3941
+ AppView,
3942
+ {
3943
+ style: [
3944
+ styles12.wheelViewport,
3945
+ {
3946
+ height: rowHeight * visibleRows,
3947
+ backgroundColor: colors.surface
3948
+ }
3949
+ ],
3950
+ children: [
3951
+ /* @__PURE__ */ jsx31(
3952
+ ScrollView2,
3121
3953
  {
3122
- className: cn(selected ? "font-semibold" : void 0, disabled && "opacity-30"),
3123
- style: {
3124
- color: selected ? colors.primary : colors.textSecondary
3125
- },
3126
- children: formatLabel(value)
3954
+ ref: scrollRef,
3955
+ showsVerticalScrollIndicator: false,
3956
+ snapToInterval: rowHeight,
3957
+ decelerationRate: "fast",
3958
+ onMomentumScrollEnd: handleScrollEnd,
3959
+ onScrollEndDrag: handleScrollEnd,
3960
+ contentContainerStyle: { paddingVertical: rowHeight * paddingRows },
3961
+ children: column.options.map((option, index) => {
3962
+ const selected = option.value === selectedValue;
3963
+ return /* @__PURE__ */ jsx31(
3964
+ TouchableOpacity6,
3965
+ {
3966
+ disabled: option.disabled,
3967
+ onPress: () => {
3968
+ if (option.disabled) return;
3969
+ onChange(option.value);
3970
+ scrollToIndex(index, true);
3971
+ },
3972
+ style: [
3973
+ styles12.optionButton,
3974
+ {
3975
+ height: rowHeight,
3976
+ opacity: option.disabled ? 0.35 : selected ? 1 : 0.72
3977
+ }
3978
+ ],
3979
+ children: /* @__PURE__ */ jsx31(
3980
+ AppText,
3981
+ {
3982
+ className: cn(selected ? "font-semibold" : void 0),
3983
+ style: { color: selected ? colors.primary : colors.text },
3984
+ children: option.label
3985
+ }
3986
+ )
3987
+ },
3988
+ `${column.key}-${String(option.value)}-${index}`
3989
+ );
3990
+ })
3991
+ }
3992
+ ),
3993
+ /* @__PURE__ */ jsx31(
3994
+ AppView,
3995
+ {
3996
+ pointerEvents: "none",
3997
+ style: [
3998
+ styles12.fadeMask,
3999
+ {
4000
+ top: 0,
4001
+ height: rowHeight * paddingRows
4002
+ }
4003
+ ],
4004
+ children: [0.92, 0.72, 0.48, 0.24].map((opacity) => /* @__PURE__ */ jsx31(
4005
+ AppView,
4006
+ {
4007
+ flex: true,
4008
+ style: { backgroundColor: colors.surface, opacity }
4009
+ },
4010
+ `top-${opacity}`
4011
+ ))
4012
+ }
4013
+ ),
4014
+ /* @__PURE__ */ jsx31(
4015
+ AppView,
4016
+ {
4017
+ pointerEvents: "none",
4018
+ style: [
4019
+ styles12.fadeMask,
4020
+ {
4021
+ bottom: 0,
4022
+ height: rowHeight * paddingRows
4023
+ }
4024
+ ],
4025
+ children: [0.24, 0.48, 0.72, 0.92].map((opacity) => /* @__PURE__ */ jsx31(
4026
+ AppView,
4027
+ {
4028
+ flex: true,
4029
+ style: { backgroundColor: colors.surface, opacity }
4030
+ },
4031
+ `bottom-${opacity}`
4032
+ ))
4033
+ }
4034
+ ),
4035
+ /* @__PURE__ */ jsx31(
4036
+ AppView,
4037
+ {
4038
+ pointerEvents: "none",
4039
+ style: [
4040
+ styles12.selectionOverlay,
4041
+ {
4042
+ top: rowHeight * paddingRows,
4043
+ height: rowHeight,
4044
+ borderColor: colors.divider,
4045
+ backgroundColor: colors.primarySurface
4046
+ }
4047
+ ]
3127
4048
  }
3128
4049
  )
3129
- },
3130
- value
3131
- );
3132
- }) })
4050
+ ]
4051
+ }
4052
+ )
3133
4053
  ]
3134
4054
  }
3135
4055
  );
3136
4056
  }
3137
- function DatePicker({
4057
+ function Picker({
3138
4058
  value,
3139
4059
  onChange,
3140
- placeholder = "\u8BF7\u9009\u62E9\u65E5\u671F",
4060
+ columns,
4061
+ placeholder = "\u8BF7\u9009\u62E9",
3141
4062
  disabled = false,
3142
- format = "yyyy-MM-dd",
3143
- minDate,
3144
- maxDate,
3145
4063
  className,
4064
+ pickerTitle = "\u8BF7\u9009\u62E9",
3146
4065
  cancelText = "\u53D6\u6D88",
3147
4066
  confirmText = "\u786E\u5B9A",
3148
- pickerTitle = "\u9009\u62E9\u65E5\u671F",
3149
- pickerDateFormat = "yyyy\u5E74MM\u6708dd\u65E5",
3150
- yearLabel = "\u5E74",
3151
- monthLabel = "\u6708",
3152
- dayLabel = "\u65E5",
3153
- todayText = "\u4ECA\u5929",
3154
- minDateText = "\u6700\u65E9",
3155
- maxDateText = "\u6700\u665A"
4067
+ triggerIconName = "keyboard-arrow-down",
4068
+ renderDisplayText,
4069
+ renderFooter,
4070
+ tempValue,
4071
+ defaultTempValue,
4072
+ onTempChange,
4073
+ onOpen,
4074
+ rowHeight = 40,
4075
+ visibleRows = 5
3156
4076
  }) {
3157
4077
  const colors = useFormThemeColors();
3158
- const [visible, setVisible] = useState18(false);
3159
- const [tempDate, setTempDate] = useState18(value || /* @__PURE__ */ new Date());
3160
- const displayText = useMemo6(() => {
3161
- return value ? formatDate(value, format) : placeholder;
3162
- }, [value, format, placeholder]);
3163
- const handleConfirm = useCallback14(() => {
3164
- onChange?.(tempDate);
3165
- setVisible(false);
3166
- }, [tempDate, onChange]);
3167
- const years = useMemo6(() => {
3168
- const currentYear = (/* @__PURE__ */ new Date()).getFullYear();
3169
- const arr = [];
3170
- for (let i = currentYear - 50; i <= currentYear + 50; i++) {
3171
- arr.push(i);
4078
+ const [visible, setVisible] = useState20(false);
4079
+ const [internalTempValues, setInternalTempValues] = useState20(
4080
+ normalizeValues(columns, defaultTempValue ?? value)
4081
+ );
4082
+ const isControlledTemp = tempValue !== void 0;
4083
+ const tempValues = useMemo8(
4084
+ () => isControlledTemp ? normalizeValues(columns, tempValue) : internalTempValues,
4085
+ [columns, internalTempValues, isControlledTemp, tempValue]
4086
+ );
4087
+ useEffect10(() => {
4088
+ if (!isControlledTemp) {
4089
+ setInternalTempValues((previous) => normalizeValues(columns, previous));
3172
4090
  }
3173
- return arr;
3174
- }, []);
3175
- const months = useMemo6(() => {
3176
- return Array.from({ length: 12 }, (_, i) => i + 1);
3177
- }, []);
3178
- const days = useMemo6(() => {
3179
- const year = tempDate.getFullYear();
3180
- const month = tempDate.getMonth();
3181
- const daysInMonth = new Date(year, month + 1, 0).getDate();
3182
- return Array.from({ length: daysInMonth }, (_, i) => i + 1);
3183
- }, [tempDate]);
3184
- const isDateDisabled = useCallback14(
3185
- (year, month, day) => {
3186
- const date = new Date(year, month - 1, day);
3187
- if (minDate && date < minDate) return true;
3188
- if (maxDate && date > maxDate) return true;
3189
- return false;
4091
+ }, [columns, isControlledTemp]);
4092
+ const selectedOptions = useMemo8(
4093
+ () => columns.map(
4094
+ (column, index) => column.options.find((option) => option.value === value?.[index] && !option.disabled)
4095
+ ),
4096
+ [columns, value]
4097
+ );
4098
+ const displayText = useMemo8(() => {
4099
+ if (!value || value.length === 0) return placeholder;
4100
+ if (renderDisplayText) return renderDisplayText(selectedOptions);
4101
+ const labels = selectedOptions.map((option) => option?.label).filter(Boolean);
4102
+ return labels.length > 0 ? labels.join(" / ") : placeholder;
4103
+ }, [placeholder, renderDisplayText, selectedOptions, value]);
4104
+ const setTempValues = useCallback15(
4105
+ (nextValues) => {
4106
+ const normalized = normalizeValues(columns, nextValues);
4107
+ if (isControlledTemp) {
4108
+ onTempChange?.(normalized);
4109
+ return;
4110
+ }
4111
+ setInternalTempValues(normalized);
4112
+ onTempChange?.(normalized);
3190
4113
  },
3191
- [minDate, maxDate]
4114
+ [columns, isControlledTemp, onTempChange]
3192
4115
  );
3193
- const updateTempDate = useCallback14(
3194
- (year, month, day) => {
3195
- const newDate = new Date(tempDate);
3196
- if (year !== void 0) newDate.setFullYear(year);
3197
- if (month !== void 0) newDate.setMonth(month - 1);
3198
- if (day !== void 0) newDate.setDate(day);
3199
- setTempDate(newDate);
4116
+ const updateColumnValue = useCallback15(
4117
+ (columnIndex, nextValue) => {
4118
+ const nextValues = [...tempValues];
4119
+ nextValues[columnIndex] = nextValue;
4120
+ setTempValues(nextValues);
3200
4121
  },
3201
- [tempDate]
4122
+ [setTempValues, tempValues]
3202
4123
  );
3203
- return /* @__PURE__ */ jsxs11(Fragment3, { children: [
3204
- /* @__PURE__ */ jsxs11(
4124
+ const openModal = useCallback15(() => {
4125
+ const normalized = normalizeValues(columns, tempValue ?? value ?? defaultTempValue);
4126
+ if (!isControlledTemp) {
4127
+ setInternalTempValues(normalized);
4128
+ }
4129
+ onTempChange?.(normalized);
4130
+ onOpen?.();
4131
+ setVisible(true);
4132
+ }, [
4133
+ columns,
4134
+ defaultTempValue,
4135
+ isControlledTemp,
4136
+ onOpen,
4137
+ onTempChange,
4138
+ tempValue,
4139
+ value
4140
+ ]);
4141
+ const handleConfirm = useCallback15(() => {
4142
+ onChange?.(tempValues);
4143
+ setVisible(false);
4144
+ }, [onChange, tempValues]);
4145
+ return /* @__PURE__ */ jsxs12(Fragment3, { children: [
4146
+ /* @__PURE__ */ jsxs12(
3205
4147
  AppPressable,
3206
4148
  {
3207
4149
  className: cn(
@@ -3209,147 +4151,252 @@ function DatePicker({
3209
4151
  disabled ? "opacity-60" : "",
3210
4152
  className
3211
4153
  ),
3212
- style: [styles11.trigger, { backgroundColor: colors.surface, borderColor: colors.border }],
4154
+ style: [styles12.trigger, { backgroundColor: colors.surface, borderColor: colors.border }],
3213
4155
  disabled,
3214
- onPress: () => {
3215
- setTempDate(value || /* @__PURE__ */ new Date());
3216
- setVisible(true);
3217
- },
4156
+ onPress: openModal,
3218
4157
  children: [
3219
- /* @__PURE__ */ jsx29(
4158
+ /* @__PURE__ */ jsx31(
3220
4159
  AppText,
3221
4160
  {
3222
4161
  className: "flex-1",
3223
- style: { color: value ? colors.text : colors.textMuted },
4162
+ style: { color: value && value.length > 0 ? colors.text : colors.textMuted },
3224
4163
  numberOfLines: 1,
3225
4164
  children: displayText
3226
4165
  }
3227
4166
  ),
3228
- /* @__PURE__ */ jsx29(Icon, { name: "calendar-today", size: "md", color: colors.icon })
4167
+ /* @__PURE__ */ jsx31(Icon, { name: triggerIconName, size: "md", color: colors.icon })
3229
4168
  ]
3230
4169
  }
3231
4170
  ),
3232
- /* @__PURE__ */ jsx29(
3233
- Modal4,
4171
+ /* @__PURE__ */ jsx31(
4172
+ BottomSheetModal,
3234
4173
  {
3235
4174
  visible,
3236
- transparent: true,
3237
- animationType: "slide",
3238
4175
  onRequestClose: () => setVisible(false),
3239
- children: /* @__PURE__ */ jsx29(AppView, { className: "flex-1", style: { backgroundColor: colors.overlay }, justify: "end", children: /* @__PURE__ */ jsxs11(AppView, { className: "rounded-t-2xl", style: { backgroundColor: colors.surface }, children: [
3240
- /* @__PURE__ */ jsxs11(
4176
+ overlayColor: colors.overlay,
4177
+ surfaceColor: colors.surface,
4178
+ closeOnBackdropPress: true,
4179
+ children: /* @__PURE__ */ jsxs12(Fragment3, { children: [
4180
+ /* @__PURE__ */ jsxs12(
3241
4181
  AppView,
3242
4182
  {
3243
4183
  row: true,
3244
4184
  between: true,
3245
4185
  items: "center",
3246
4186
  className: "px-4 py-3",
3247
- style: [styles11.header, { borderBottomColor: colors.divider }],
4187
+ style: [styles12.header, { borderBottomColor: colors.divider }],
3248
4188
  children: [
3249
- /* @__PURE__ */ jsx29(TouchableOpacity6, { onPress: () => setVisible(false), children: /* @__PURE__ */ jsx29(AppText, { style: { color: colors.textMuted }, children: cancelText }) }),
3250
- /* @__PURE__ */ jsx29(AppText, { className: "text-lg font-semibold", style: { color: colors.text }, children: pickerTitle }),
3251
- /* @__PURE__ */ jsx29(TouchableOpacity6, { onPress: handleConfirm, children: /* @__PURE__ */ jsx29(AppText, { style: { color: colors.primary }, className: "font-medium", children: confirmText }) })
4189
+ /* @__PURE__ */ jsx31(TouchableOpacity6, { onPress: () => setVisible(false), children: /* @__PURE__ */ jsx31(AppText, { style: { color: colors.textMuted }, children: cancelText }) }),
4190
+ /* @__PURE__ */ jsx31(AppText, { className: "text-lg font-semibold", style: { color: colors.text }, children: pickerTitle }),
4191
+ /* @__PURE__ */ jsx31(TouchableOpacity6, { onPress: handleConfirm, children: /* @__PURE__ */ jsx31(AppText, { className: "font-medium", style: { color: colors.primary }, children: confirmText }) })
3252
4192
  ]
3253
4193
  }
3254
4194
  ),
3255
- /* @__PURE__ */ jsx29(AppView, { center: true, className: "py-4", style: { backgroundColor: colors.headerSurface }, children: /* @__PURE__ */ jsx29(AppText, { className: "text-2xl font-semibold", style: { color: colors.text }, children: formatDate(tempDate, pickerDateFormat) }) }),
3256
- /* @__PURE__ */ jsxs11(AppView, { row: true, className: "h-48", children: [
3257
- /* @__PURE__ */ jsx29(
3258
- PickerColumn,
3259
- {
3260
- title: yearLabel,
3261
- values: years,
3262
- selectedValue: tempDate.getFullYear(),
3263
- onSelect: (year) => updateTempDate(year),
3264
- isDisabled: (year) => isDateDisabled(year, tempDate.getMonth() + 1, tempDate.getDate()),
3265
- colors,
3266
- showDivider: true
3267
- }
3268
- ),
3269
- /* @__PURE__ */ jsx29(
3270
- PickerColumn,
3271
- {
3272
- title: monthLabel,
3273
- values: months,
3274
- selectedValue: tempDate.getMonth() + 1,
3275
- onSelect: (month) => updateTempDate(void 0, month),
3276
- isDisabled: (month) => isDateDisabled(tempDate.getFullYear(), month, tempDate.getDate()),
3277
- formatLabel: (month) => `${month}\u6708`,
3278
- colors,
3279
- showDivider: true
3280
- }
3281
- ),
3282
- /* @__PURE__ */ jsx29(
3283
- PickerColumn,
3284
- {
3285
- title: dayLabel,
3286
- values: days,
3287
- selectedValue: tempDate.getDate(),
3288
- onSelect: (day) => updateTempDate(void 0, void 0, day),
3289
- isDisabled: (day) => isDateDisabled(tempDate.getFullYear(), tempDate.getMonth() + 1, day),
3290
- colors
3291
- }
3292
- )
3293
- ] }),
3294
- /* @__PURE__ */ jsxs11(
4195
+ /* @__PURE__ */ jsx31(AppView, { row: true, children: columns.map((column, index) => /* @__PURE__ */ jsx31(
4196
+ WheelPickerColumn,
4197
+ {
4198
+ colors,
4199
+ column,
4200
+ onChange: (nextValue) => updateColumnValue(index, nextValue),
4201
+ rowHeight,
4202
+ selectedValue: tempValues[index],
4203
+ showDivider: index < columns.length - 1,
4204
+ visibleRows
4205
+ },
4206
+ column.key
4207
+ )) }),
4208
+ renderFooter && /* @__PURE__ */ jsx31(
3295
4209
  AppView,
3296
4210
  {
3297
- row: true,
3298
- className: "px-4 py-3 gap-2",
3299
- style: [styles11.footer, { borderTopColor: colors.divider }],
3300
- children: [
3301
- /* @__PURE__ */ jsx29(
3302
- TouchableOpacity6,
3303
- {
3304
- className: "flex-1 py-2 items-center rounded-lg",
3305
- style: { backgroundColor: colors.surfaceMuted },
3306
- onPress: () => setTempDate(/* @__PURE__ */ new Date()),
3307
- children: /* @__PURE__ */ jsx29(AppText, { style: { color: colors.text }, children: todayText })
3308
- }
3309
- ),
3310
- minDate && /* @__PURE__ */ jsx29(
3311
- TouchableOpacity6,
3312
- {
3313
- className: "flex-1 py-2 items-center rounded-lg",
3314
- style: { backgroundColor: colors.surfaceMuted },
3315
- onPress: () => setTempDate(minDate),
3316
- children: /* @__PURE__ */ jsx29(AppText, { style: { color: colors.text }, children: minDateText })
3317
- }
3318
- ),
3319
- maxDate && /* @__PURE__ */ jsx29(
3320
- TouchableOpacity6,
3321
- {
3322
- className: "flex-1 py-2 items-center rounded-lg",
3323
- style: { backgroundColor: colors.surfaceMuted },
3324
- onPress: () => setTempDate(maxDate),
3325
- children: /* @__PURE__ */ jsx29(AppText, { style: { color: colors.text }, children: maxDateText })
3326
- }
3327
- )
3328
- ]
4211
+ className: "px-4 py-3",
4212
+ style: [styles12.footer, { borderTopColor: colors.divider }],
4213
+ children: renderFooter({ close: () => setVisible(false), setTempValues, tempValues })
3329
4214
  }
3330
4215
  )
3331
- ] }) })
4216
+ ] })
3332
4217
  }
3333
4218
  )
3334
4219
  ] });
3335
4220
  }
3336
- var styles11 = StyleSheet12.create({
4221
+ var styles12 = StyleSheet13.create({
3337
4222
  trigger: {
3338
4223
  borderWidth: 0.5
3339
4224
  },
3340
4225
  header: {
3341
4226
  borderBottomWidth: 0.5
3342
4227
  },
3343
- column: {
3344
- borderRightWidth: 0.5
4228
+ footer: {
4229
+ borderTopWidth: 0.5
4230
+ },
4231
+ columnDivider: {
4232
+ borderRightWidth: 0.5
4233
+ },
4234
+ wheelViewport: {
4235
+ position: "relative",
4236
+ overflow: "hidden"
4237
+ },
4238
+ fadeMask: {
4239
+ position: "absolute",
4240
+ left: 0,
4241
+ right: 0
3345
4242
  },
3346
- footer: {
3347
- borderTopWidth: 0.5
4243
+ selectionOverlay: {
4244
+ position: "absolute",
4245
+ left: 8,
4246
+ right: 8,
4247
+ borderRadius: 12,
4248
+ borderWidth: 0.5
4249
+ },
4250
+ optionButton: {
4251
+ alignItems: "center",
4252
+ justifyContent: "center"
3348
4253
  }
3349
4254
  });
3350
4255
 
4256
+ // src/ui/form/DatePicker.tsx
4257
+ import { useCallback as useCallback16, useEffect as useEffect11, useMemo as useMemo9, useState as useState21 } from "react";
4258
+ import { TouchableOpacity as TouchableOpacity7 } from "react-native";
4259
+ import { jsx as jsx32 } from "nativewind/jsx-runtime";
4260
+ function createSafeDate(year, month, day) {
4261
+ const daysInMonth = new Date(year, month, 0).getDate();
4262
+ return new Date(year, month - 1, Math.min(day, daysInMonth));
4263
+ }
4264
+ function getDateValues(date) {
4265
+ return [date.getFullYear(), date.getMonth() + 1, date.getDate()];
4266
+ }
4267
+ function DatePicker({
4268
+ value,
4269
+ onChange,
4270
+ placeholder = "\u8BF7\u9009\u62E9\u65E5\u671F",
4271
+ disabled = false,
4272
+ format = "yyyy-MM-dd",
4273
+ minDate,
4274
+ maxDate,
4275
+ className,
4276
+ cancelText = "\u53D6\u6D88",
4277
+ confirmText = "\u786E\u5B9A",
4278
+ pickerTitle = "\u9009\u62E9\u65E5\u671F",
4279
+ pickerDateFormat: _pickerDateFormat = "yyyy\u5E74MM\u6708dd\u65E5",
4280
+ yearLabel = "\u5E74",
4281
+ monthLabel = "\u6708",
4282
+ dayLabel = "\u65E5",
4283
+ todayText = "\u4ECA\u5929",
4284
+ minDateText = "\u6700\u65E9",
4285
+ maxDateText = "\u6700\u665A"
4286
+ }) {
4287
+ const colors = useFormThemeColors();
4288
+ const [tempDate, setTempDate] = useState21(value || /* @__PURE__ */ new Date());
4289
+ useEffect11(() => {
4290
+ if (value) setTempDate(value);
4291
+ }, [value]);
4292
+ const years = useMemo9(() => {
4293
+ const baseYear = value?.getFullYear() ?? (/* @__PURE__ */ new Date()).getFullYear();
4294
+ const startYear = minDate?.getFullYear() ?? baseYear - 50;
4295
+ const endYear = maxDate?.getFullYear() ?? baseYear + 50;
4296
+ return Array.from({ length: endYear - startYear + 1 }, (_, index) => startYear + index);
4297
+ }, [maxDate, minDate, value]);
4298
+ const months = useMemo9(() => Array.from({ length: 12 }, (_, index) => index + 1), []);
4299
+ const days = useMemo9(() => {
4300
+ const daysInMonth = new Date(tempDate.getFullYear(), tempDate.getMonth() + 1, 0).getDate();
4301
+ return Array.from({ length: daysInMonth }, (_, index) => index + 1);
4302
+ }, [tempDate]);
4303
+ const isDateDisabled = useCallback16(
4304
+ (year, month, day) => {
4305
+ const date = createSafeDate(year, month, day);
4306
+ if (minDate && date < minDate) return true;
4307
+ if (maxDate && date > maxDate) return true;
4308
+ return false;
4309
+ },
4310
+ [maxDate, minDate]
4311
+ );
4312
+ const columns = useMemo9(
4313
+ () => [
4314
+ {
4315
+ key: "year",
4316
+ title: yearLabel,
4317
+ options: years.map((year) => ({
4318
+ label: String(year),
4319
+ value: year,
4320
+ disabled: isDateDisabled(year, tempDate.getMonth() + 1, tempDate.getDate())
4321
+ }))
4322
+ },
4323
+ {
4324
+ key: "month",
4325
+ title: monthLabel,
4326
+ options: months.map((month) => ({
4327
+ label: `${month}\u6708`,
4328
+ value: month,
4329
+ disabled: isDateDisabled(tempDate.getFullYear(), month, tempDate.getDate())
4330
+ }))
4331
+ },
4332
+ {
4333
+ key: "day",
4334
+ title: dayLabel,
4335
+ options: days.map((day) => ({
4336
+ label: `${day}\u65E5`,
4337
+ value: day,
4338
+ disabled: isDateDisabled(tempDate.getFullYear(), tempDate.getMonth() + 1, day)
4339
+ }))
4340
+ }
4341
+ ],
4342
+ [dayLabel, days, isDateDisabled, monthLabel, months, tempDate, yearLabel, years]
4343
+ );
4344
+ const handleTempChange = useCallback16((nextValues) => {
4345
+ const [nextYear, nextMonth, nextDay] = nextValues;
4346
+ if (typeof nextYear !== "number" || typeof nextMonth !== "number" || typeof nextDay !== "number") {
4347
+ return;
4348
+ }
4349
+ setTempDate(createSafeDate(nextYear, nextMonth, nextDay));
4350
+ }, []);
4351
+ const handleChange = useCallback16(
4352
+ (nextValues) => {
4353
+ const [nextYear, nextMonth, nextDay] = nextValues;
4354
+ if (typeof nextYear !== "number" || typeof nextMonth !== "number" || typeof nextDay !== "number") {
4355
+ return;
4356
+ }
4357
+ onChange?.(createSafeDate(nextYear, nextMonth, nextDay));
4358
+ },
4359
+ [onChange]
4360
+ );
4361
+ const quickActions = useMemo9(() => {
4362
+ const actions = [{ label: todayText, date: /* @__PURE__ */ new Date() }];
4363
+ if (minDate) actions.push({ label: minDateText, date: minDate });
4364
+ if (maxDate) actions.push({ label: maxDateText, date: maxDate });
4365
+ return actions;
4366
+ }, [maxDate, maxDateText, minDate, minDateText, todayText]);
4367
+ return /* @__PURE__ */ jsx32(
4368
+ Picker,
4369
+ {
4370
+ value: value ? getDateValues(value) : void 0,
4371
+ tempValue: getDateValues(tempDate),
4372
+ onTempChange: handleTempChange,
4373
+ onChange: handleChange,
4374
+ onOpen: () => setTempDate(value || /* @__PURE__ */ new Date()),
4375
+ columns,
4376
+ placeholder,
4377
+ disabled,
4378
+ className,
4379
+ pickerTitle,
4380
+ cancelText,
4381
+ confirmText,
4382
+ triggerIconName: "calendar-today",
4383
+ renderDisplayText: () => value ? formatDate(value, format) : placeholder,
4384
+ renderFooter: () => /* @__PURE__ */ jsx32(AppView, { row: true, className: "gap-2", children: quickActions.map((action) => /* @__PURE__ */ jsx32(
4385
+ TouchableOpacity7,
4386
+ {
4387
+ className: "flex-1 py-2 items-center rounded-lg",
4388
+ style: { backgroundColor: colors.surfaceMuted },
4389
+ onPress: () => setTempDate(action.date),
4390
+ children: /* @__PURE__ */ jsx32(AppText, { style: { color: colors.text }, children: action.label })
4391
+ },
4392
+ action.label
4393
+ )) })
4394
+ }
4395
+ );
4396
+ }
4397
+
3351
4398
  // src/ui/form/FormItem.tsx
3352
- import { jsx as jsx30, jsxs as jsxs12 } from "nativewind/jsx-runtime";
4399
+ import { jsx as jsx33, jsxs as jsxs13 } from "nativewind/jsx-runtime";
3353
4400
  function FormItem({
3354
4401
  name: _name,
3355
4402
  label,
@@ -3361,19 +4408,19 @@ function FormItem({
3361
4408
  labelClassName
3362
4409
  }) {
3363
4410
  const colors = useThemeColors();
3364
- return /* @__PURE__ */ jsxs12(AppView, { className: cn("mb-4", className), children: [
3365
- label && /* @__PURE__ */ jsxs12(AppView, { row: true, items: "center", gap: 1, className: cn("mb-2", labelClassName), children: [
3366
- /* @__PURE__ */ jsx30(AppText, { size: "sm", weight: "medium", style: { color: colors.textSecondary }, children: label }),
3367
- required && /* @__PURE__ */ jsx30(AppText, { color: "error-500", children: "*" })
4411
+ return /* @__PURE__ */ jsxs13(AppView, { className: cn("mb-4", className), children: [
4412
+ label && /* @__PURE__ */ jsxs13(AppView, { row: true, items: "center", gap: 1, className: cn("mb-2", labelClassName), children: [
4413
+ /* @__PURE__ */ jsx33(AppText, { size: "sm", weight: "medium", style: { color: colors.textSecondary }, children: label }),
4414
+ required && /* @__PURE__ */ jsx33(AppText, { color: "error-500", children: "*" })
3368
4415
  ] }),
3369
4416
  children,
3370
- error && /* @__PURE__ */ jsx30(AppText, { size: "sm", color: "error-500", className: "mt-1", children: error }),
3371
- help && !error && /* @__PURE__ */ jsx30(AppText, { size: "sm", className: "mt-1", style: { color: colors.textMuted }, children: help })
4417
+ error && /* @__PURE__ */ jsx33(AppText, { size: "sm", color: "error-500", className: "mt-1", children: error }),
4418
+ help && !error && /* @__PURE__ */ jsx33(AppText, { size: "sm", className: "mt-1", style: { color: colors.textMuted }, children: help })
3372
4419
  ] });
3373
4420
  }
3374
4421
 
3375
4422
  // src/ui/form/useForm.ts
3376
- import { useState as useState19, useCallback as useCallback15, useMemo as useMemo7 } from "react";
4423
+ import { useState as useState22, useCallback as useCallback17, useMemo as useMemo10 } from "react";
3377
4424
  var getIssuePath = (issue) => issue.path.map(String).join(".");
3378
4425
  var getFieldError = (issues, name) => {
3379
4426
  const exactIssue = issues.find((issue) => getIssuePath(issue) === name);
@@ -3389,16 +4436,16 @@ var buildFormErrors = (issues) => {
3389
4436
  }, {});
3390
4437
  };
3391
4438
  function useForm({ schema, defaultValues }) {
3392
- const [values, setValues] = useState19(defaultValues);
3393
- const [errors, setErrors] = useState19({});
3394
- const [isSubmitting, setIsSubmitting] = useState19(false);
3395
- const isDirty = useMemo7(() => {
4439
+ const [values, setValues] = useState22(defaultValues);
4440
+ const [errors, setErrors] = useState22({});
4441
+ const [isSubmitting, setIsSubmitting] = useState22(false);
4442
+ const isDirty = useMemo10(() => {
3396
4443
  return JSON.stringify(values) !== JSON.stringify(defaultValues);
3397
4444
  }, [values, defaultValues]);
3398
- const isValid = useMemo7(() => {
4445
+ const isValid = useMemo10(() => {
3399
4446
  return Object.keys(errors).length === 0;
3400
4447
  }, [errors]);
3401
- const clearFieldError = useCallback15((name) => {
4448
+ const clearFieldError = useCallback17((name) => {
3402
4449
  setErrors((prev) => {
3403
4450
  if (!(name in prev)) return prev;
3404
4451
  const next = { ...prev };
@@ -3406,20 +4453,20 @@ function useForm({ schema, defaultValues }) {
3406
4453
  return next;
3407
4454
  });
3408
4455
  }, []);
3409
- const setValue = useCallback15(
4456
+ const setValue = useCallback17(
3410
4457
  (name, value) => {
3411
4458
  setValues((prev) => ({ ...prev, [name]: value }));
3412
4459
  clearFieldError(name);
3413
4460
  },
3414
4461
  [clearFieldError]
3415
4462
  );
3416
- const getValue = useCallback15(
4463
+ const getValue = useCallback17(
3417
4464
  (name) => {
3418
4465
  return values[name];
3419
4466
  },
3420
4467
  [values]
3421
4468
  );
3422
- const validateField = useCallback15(
4469
+ const validateField = useCallback17(
3423
4470
  async (name) => {
3424
4471
  const fieldName = name;
3425
4472
  const result = await schema.safeParseAsync(values);
@@ -3440,7 +4487,7 @@ function useForm({ schema, defaultValues }) {
3440
4487
  },
3441
4488
  [schema, values, clearFieldError]
3442
4489
  );
3443
- const validate = useCallback15(async () => {
4490
+ const validate = useCallback17(async () => {
3444
4491
  const result = await schema.safeParseAsync(values);
3445
4492
  if (result.success) {
3446
4493
  setErrors({});
@@ -3449,12 +4496,12 @@ function useForm({ schema, defaultValues }) {
3449
4496
  setErrors(buildFormErrors(result.error.issues));
3450
4497
  return false;
3451
4498
  }, [schema, values]);
3452
- const reset = useCallback15(() => {
4499
+ const reset = useCallback17(() => {
3453
4500
  setValues(defaultValues);
3454
4501
  setErrors({});
3455
4502
  setIsSubmitting(false);
3456
4503
  }, [defaultValues]);
3457
- const handleSubmit = useCallback15(
4504
+ const handleSubmit = useCallback17(
3458
4505
  async (onSubmit) => {
3459
4506
  const valid = await validate();
3460
4507
  if (!valid) return;
@@ -3483,38 +4530,38 @@ function useForm({ schema, defaultValues }) {
3483
4530
  }
3484
4531
 
3485
4532
  // src/ui/hooks/useToggle.ts
3486
- import { useCallback as useCallback16, useState as useState20 } from "react";
4533
+ import { useCallback as useCallback18, useState as useState23 } from "react";
3487
4534
  function useToggle(defaultValue = false) {
3488
- const [value, setValue] = useState20(defaultValue);
3489
- const toggle = useCallback16(() => {
4535
+ const [value, setValue] = useState23(defaultValue);
4536
+ const toggle = useCallback18(() => {
3490
4537
  setValue((v) => !v);
3491
4538
  }, []);
3492
- const set = useCallback16((newValue) => {
4539
+ const set = useCallback18((newValue) => {
3493
4540
  setValue(newValue);
3494
4541
  }, []);
3495
- const setTrue = useCallback16(() => {
4542
+ const setTrue = useCallback18(() => {
3496
4543
  setValue(true);
3497
4544
  }, []);
3498
- const setFalse = useCallback16(() => {
4545
+ const setFalse = useCallback18(() => {
3499
4546
  setValue(false);
3500
4547
  }, []);
3501
4548
  return [value, { toggle, set, setTrue, setFalse }];
3502
4549
  }
3503
4550
 
3504
4551
  // src/ui/hooks/usePageDrawer.ts
3505
- import { useCallback as useCallback17, useState as useState21 } from "react";
4552
+ import { useCallback as useCallback19, useState as useState24 } from "react";
3506
4553
  function usePageDrawer(defaultVisible = false) {
3507
- const [visible, setVisibleState] = useState21(defaultVisible);
3508
- const open = useCallback17(() => {
4554
+ const [visible, setVisibleState] = useState24(defaultVisible);
4555
+ const open = useCallback19(() => {
3509
4556
  setVisibleState(true);
3510
4557
  }, []);
3511
- const close = useCallback17(() => {
4558
+ const close = useCallback19(() => {
3512
4559
  setVisibleState(false);
3513
4560
  }, []);
3514
- const toggle = useCallback17(() => {
4561
+ const toggle = useCallback19(() => {
3515
4562
  setVisibleState((current) => !current);
3516
4563
  }, []);
3517
- const setVisible = useCallback17((nextVisible) => {
4564
+ const setVisible = useCallback19((nextVisible) => {
3518
4565
  setVisibleState(nextVisible);
3519
4566
  }, []);
3520
4567
  return {
@@ -3527,10 +4574,10 @@ function usePageDrawer(defaultVisible = false) {
3527
4574
  }
3528
4575
 
3529
4576
  // src/ui/hooks/useDebounce.ts
3530
- import { useEffect as useEffect5, useState as useState22 } from "react";
4577
+ import { useEffect as useEffect12, useState as useState25 } from "react";
3531
4578
  function useDebounce(value, delay = 500) {
3532
- const [debouncedValue, setDebouncedValue] = useState22(value);
3533
- useEffect5(() => {
4579
+ const [debouncedValue, setDebouncedValue] = useState25(value);
4580
+ useEffect12(() => {
3534
4581
  const timer = setTimeout(() => {
3535
4582
  setDebouncedValue(value);
3536
4583
  }, delay);
@@ -3542,11 +4589,11 @@ function useDebounce(value, delay = 500) {
3542
4589
  }
3543
4590
 
3544
4591
  // src/ui/hooks/useThrottle.ts
3545
- import { useEffect as useEffect6, useRef as useRef7, useState as useState23 } from "react";
4592
+ import { useEffect as useEffect13, useRef as useRef11, useState as useState26 } from "react";
3546
4593
  function useThrottle(value, delay = 200) {
3547
- const [throttledValue, setThrottledValue] = useState23(value);
3548
- const lastUpdatedRef = useRef7(Date.now());
3549
- useEffect6(() => {
4594
+ const [throttledValue, setThrottledValue] = useState26(value);
4595
+ const lastUpdatedRef = useRef11(Date.now());
4596
+ useEffect13(() => {
3550
4597
  const now = Date.now();
3551
4598
  const timeElapsed = now - lastUpdatedRef.current;
3552
4599
  if (timeElapsed >= delay) {
@@ -3567,12 +4614,12 @@ function useThrottle(value, delay = 200) {
3567
4614
  }
3568
4615
 
3569
4616
  // src/ui/hooks/useKeyboard.ts
3570
- import { useEffect as useEffect7, useState as useState24, useCallback as useCallback18 } from "react";
3571
- import { Keyboard, Platform } from "react-native";
4617
+ import { useEffect as useEffect14, useState as useState27, useCallback as useCallback20 } from "react";
4618
+ import { Keyboard as Keyboard5, Platform } from "react-native";
3572
4619
  function useKeyboard() {
3573
- const [visible, setVisible] = useState24(false);
3574
- const [height, setHeight] = useState24(0);
3575
- useEffect7(() => {
4620
+ const [visible, setVisible] = useState27(false);
4621
+ const [height, setHeight] = useState27(0);
4622
+ useEffect14(() => {
3576
4623
  const handleKeyboardWillShow = (event) => {
3577
4624
  setVisible(true);
3578
4625
  setHeight(event.endCoordinates.height);
@@ -3589,11 +4636,11 @@ function useKeyboard() {
3589
4636
  setVisible(false);
3590
4637
  setHeight(0);
3591
4638
  };
3592
- const willShowSub = Keyboard.addListener(
4639
+ const willShowSub = Keyboard5.addListener(
3593
4640
  Platform.OS === "ios" ? "keyboardWillShow" : "keyboardDidShow",
3594
4641
  Platform.OS === "ios" ? handleKeyboardWillShow : handleKeyboardDidShow
3595
4642
  );
3596
- const willHideSub = Keyboard.addListener(
4643
+ const willHideSub = Keyboard5.addListener(
3597
4644
  Platform.OS === "ios" ? "keyboardWillHide" : "keyboardDidHide",
3598
4645
  Platform.OS === "ios" ? handleKeyboardWillHide : handleKeyboardDidHide
3599
4646
  );
@@ -3602,17 +4649,17 @@ function useKeyboard() {
3602
4649
  willHideSub.remove();
3603
4650
  };
3604
4651
  }, []);
3605
- const dismiss = useCallback18(() => {
3606
- Keyboard.dismiss();
4652
+ const dismiss = useCallback20(() => {
4653
+ Keyboard5.dismiss();
3607
4654
  }, []);
3608
4655
  return { visible, height, dismiss };
3609
4656
  }
3610
4657
 
3611
4658
  // src/ui/hooks/useDimensions.ts
3612
- import { useEffect as useEffect8, useState as useState25 } from "react";
4659
+ import { useEffect as useEffect15, useState as useState28 } from "react";
3613
4660
  import { Dimensions } from "react-native";
3614
4661
  function useDimensions() {
3615
- const [dimensions, setDimensions] = useState25(() => {
4662
+ const [dimensions, setDimensions] = useState28(() => {
3616
4663
  const window = Dimensions.get("window");
3617
4664
  return {
3618
4665
  width: window.width,
@@ -3621,7 +4668,7 @@ function useDimensions() {
3621
4668
  fontScale: window.fontScale
3622
4669
  };
3623
4670
  });
3624
- useEffect8(() => {
4671
+ useEffect15(() => {
3625
4672
  const handleChange = ({ window }) => {
3626
4673
  setDimensions({
3627
4674
  width: window.width,
@@ -3639,15 +4686,15 @@ function useDimensions() {
3639
4686
  }
3640
4687
 
3641
4688
  // src/ui/hooks/useOrientation.ts
3642
- import { useEffect as useEffect9, useState as useState26 } from "react";
4689
+ import { useEffect as useEffect16, useState as useState29 } from "react";
3643
4690
  import { Dimensions as Dimensions2 } from "react-native";
3644
4691
  function useOrientation() {
3645
4692
  const getOrientation = () => {
3646
4693
  const { width, height } = Dimensions2.get("window");
3647
4694
  return width > height ? "landscape" : "portrait";
3648
4695
  };
3649
- const [orientation, setOrientation] = useState26(getOrientation);
3650
- useEffect9(() => {
4696
+ const [orientation, setOrientation] = useState29(getOrientation);
4697
+ useEffect16(() => {
3651
4698
  const handleChange = ({ window }) => {
3652
4699
  const newOrientation = window.width > window.height ? "landscape" : "portrait";
3653
4700
  setOrientation(newOrientation);
@@ -3693,7 +4740,7 @@ function createNavigationTheme(pantherTheme, isDark) {
3693
4740
  }
3694
4741
 
3695
4742
  // src/navigation/provider.tsx
3696
- import { jsx as jsx31 } from "nativewind/jsx-runtime";
4743
+ import { jsx as jsx34 } from "nativewind/jsx-runtime";
3697
4744
  function NavigationProvider({
3698
4745
  children,
3699
4746
  linking,
@@ -3708,7 +4755,7 @@ function NavigationProvider({
3708
4755
  () => customTheme || createNavigationTheme(theme, isDark),
3709
4756
  [customTheme, theme, isDark]
3710
4757
  );
3711
- return /* @__PURE__ */ jsx31(
4758
+ return /* @__PURE__ */ jsx34(
3712
4759
  NavigationContainer,
3713
4760
  {
3714
4761
  theme: navigationTheme,
@@ -3726,14 +4773,14 @@ function NavigationProvider({
3726
4773
  import { createStackNavigator, TransitionPresets } from "@react-navigation/stack";
3727
4774
 
3728
4775
  // src/navigation/navigators/StackNavigator.tsx
3729
- import { jsx as jsx32 } from "nativewind/jsx-runtime";
4776
+ import { jsx as jsx35 } from "nativewind/jsx-runtime";
3730
4777
  var NativeStack = createStackNavigator();
3731
4778
  var defaultScreenOptions = {
3732
4779
  headerShown: false,
3733
4780
  ...TransitionPresets.SlideFromRightIOS
3734
4781
  };
3735
4782
  function StackNavigator({ initialRouteName, screenOptions, children }) {
3736
- return /* @__PURE__ */ jsx32(
4783
+ return /* @__PURE__ */ jsx35(
3737
4784
  NativeStack.Navigator,
3738
4785
  {
3739
4786
  initialRouteName,
@@ -3745,7 +4792,7 @@ function StackNavigator({ initialRouteName, screenOptions, children }) {
3745
4792
  StackNavigator.Screen = NativeStack.Screen;
3746
4793
  StackNavigator.Group = NativeStack.Group;
3747
4794
  function createStackScreens(routes) {
3748
- return routes.map((route) => /* @__PURE__ */ jsx32(
4795
+ return routes.map((route) => /* @__PURE__ */ jsx35(
3749
4796
  StackNavigator.Screen,
3750
4797
  {
3751
4798
  name: route.name,
@@ -3762,9 +4809,9 @@ import React7 from "react";
3762
4809
  import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
3763
4810
 
3764
4811
  // src/navigation/components/BottomTabBar.tsx
3765
- import { View as View8, TouchableOpacity as TouchableOpacity7, StyleSheet as StyleSheet13 } from "react-native";
4812
+ import { View as View8, TouchableOpacity as TouchableOpacity8, StyleSheet as StyleSheet14 } from "react-native";
3766
4813
  import { useSafeAreaInsets as useSafeAreaInsets2 } from "react-native-safe-area-context";
3767
- import { jsx as jsx33, jsxs as jsxs13 } from "nativewind/jsx-runtime";
4814
+ import { jsx as jsx36, jsxs as jsxs14 } from "nativewind/jsx-runtime";
3768
4815
  var DEFAULT_TAB_BAR_HEIGHT = 65;
3769
4816
  function BottomTabBar({
3770
4817
  state,
@@ -3786,11 +4833,11 @@ function BottomTabBar({
3786
4833
  const inactiveColor = inactiveTintColor || colors.textMuted;
3787
4834
  const backgroundColor = style?.backgroundColor || colors.card;
3788
4835
  const borderTopColor = colors.divider;
3789
- return /* @__PURE__ */ jsx33(
4836
+ return /* @__PURE__ */ jsx36(
3790
4837
  View8,
3791
4838
  {
3792
4839
  style: [
3793
- styles12.container,
4840
+ styles13.container,
3794
4841
  { borderTopColor },
3795
4842
  { backgroundColor, height: height + insets.bottom, paddingBottom: insets.bottom },
3796
4843
  style
@@ -3821,8 +4868,8 @@ function BottomTabBar({
3821
4868
  size: 24
3822
4869
  }) : null;
3823
4870
  const badge = options.tabBarBadge;
3824
- return /* @__PURE__ */ jsxs13(
3825
- TouchableOpacity7,
4871
+ return /* @__PURE__ */ jsxs14(
4872
+ TouchableOpacity8,
3826
4873
  {
3827
4874
  accessibilityRole: "button",
3828
4875
  accessibilityState: isFocused ? { selected: true } : {},
@@ -3831,21 +4878,21 @@ function BottomTabBar({
3831
4878
  onPress,
3832
4879
  onLongPress,
3833
4880
  style: [
3834
- styles12.tab,
4881
+ styles13.tab,
3835
4882
  {
3836
4883
  backgroundColor: isFocused ? activeBackgroundColor : inactiveBackgroundColor
3837
4884
  }
3838
4885
  ],
3839
4886
  children: [
3840
- /* @__PURE__ */ jsxs13(View8, { style: [styles12.iconContainer, iconStyle], children: [
4887
+ /* @__PURE__ */ jsxs14(View8, { style: [styles13.iconContainer, iconStyle], children: [
3841
4888
  iconName,
3842
- badge != null && /* @__PURE__ */ jsx33(View8, { style: [styles12.badge, { backgroundColor: activeColor }], children: /* @__PURE__ */ jsx33(AppText, { style: styles12.badgeText, children: typeof badge === "number" && badge > 99 ? "99+" : badge }) })
4889
+ badge != null && /* @__PURE__ */ jsx36(View8, { style: [styles13.badge, { backgroundColor: activeColor }], children: /* @__PURE__ */ jsx36(AppText, { style: styles13.badgeText, children: typeof badge === "number" && badge > 99 ? "99+" : badge }) })
3843
4890
  ] }),
3844
- showLabel && /* @__PURE__ */ jsx33(
4891
+ showLabel && /* @__PURE__ */ jsx36(
3845
4892
  AppText,
3846
4893
  {
3847
4894
  style: [
3848
- styles12.label,
4895
+ styles13.label,
3849
4896
  { color: isFocused ? activeColor : inactiveColor },
3850
4897
  labelStyle
3851
4898
  ],
@@ -3861,7 +4908,7 @@ function BottomTabBar({
3861
4908
  }
3862
4909
  );
3863
4910
  }
3864
- var styles12 = StyleSheet13.create({
4911
+ var styles13 = StyleSheet14.create({
3865
4912
  container: {
3866
4913
  flexDirection: "row",
3867
4914
  borderTopWidth: 0.5,
@@ -3904,7 +4951,7 @@ var styles12 = StyleSheet13.create({
3904
4951
  });
3905
4952
 
3906
4953
  // src/navigation/navigators/TabNavigator.tsx
3907
- import { jsx as jsx34 } from "nativewind/jsx-runtime";
4954
+ import { jsx as jsx37 } from "nativewind/jsx-runtime";
3908
4955
  var NativeTab = createBottomTabNavigator();
3909
4956
  var defaultScreenOptions2 = {
3910
4957
  headerShown: false,
@@ -3952,7 +4999,7 @@ function TabNavigator({
3952
4999
  }, [tabBarOptions, screenOptions]);
3953
5000
  const resolvedTabBar = React7.useMemo(() => {
3954
5001
  if (tabBar) return tabBar;
3955
- return (props) => /* @__PURE__ */ jsx34(
5002
+ return (props) => /* @__PURE__ */ jsx37(
3956
5003
  BottomTabBar,
3957
5004
  {
3958
5005
  ...props,
@@ -3979,7 +5026,7 @@ function TabNavigator({
3979
5026
  tabBarOptions?.style,
3980
5027
  tabBarOptions?.height
3981
5028
  ]);
3982
- return /* @__PURE__ */ jsx34(
5029
+ return /* @__PURE__ */ jsx37(
3983
5030
  NativeTab.Navigator,
3984
5031
  {
3985
5032
  initialRouteName,
@@ -3991,7 +5038,7 @@ function TabNavigator({
3991
5038
  }
3992
5039
  TabNavigator.Screen = NativeTab.Screen;
3993
5040
  function createTabScreens(routes) {
3994
- return routes.map((route) => /* @__PURE__ */ jsx34(
5041
+ return routes.map((route) => /* @__PURE__ */ jsx37(
3995
5042
  TabNavigator.Screen,
3996
5043
  {
3997
5044
  name: route.name,
@@ -4006,7 +5053,7 @@ function createTabScreens(routes) {
4006
5053
  // src/navigation/navigators/DrawerNavigator.tsx
4007
5054
  import React8 from "react";
4008
5055
  import { createDrawerNavigator } from "@react-navigation/drawer";
4009
- import { jsx as jsx35 } from "nativewind/jsx-runtime";
5056
+ import { jsx as jsx38 } from "nativewind/jsx-runtime";
4010
5057
  var NativeDrawer = createDrawerNavigator();
4011
5058
  function DrawerNavigator({
4012
5059
  initialRouteName,
@@ -4036,7 +5083,7 @@ function DrawerNavigator({
4036
5083
  ...screenOptions
4037
5084
  };
4038
5085
  }, [screenOptions, drawerOptions, navigationTheme, theme]);
4039
- return /* @__PURE__ */ jsx35(
5086
+ return /* @__PURE__ */ jsx38(
4040
5087
  NativeDrawer.Navigator,
4041
5088
  {
4042
5089
  initialRouteName,
@@ -4048,7 +5095,7 @@ function DrawerNavigator({
4048
5095
  }
4049
5096
  DrawerNavigator.Screen = NativeDrawer.Screen;
4050
5097
  function createDrawerScreens(routes) {
4051
- return routes.map((route) => /* @__PURE__ */ jsx35(
5098
+ return routes.map((route) => /* @__PURE__ */ jsx38(
4052
5099
  DrawerNavigator.Screen,
4053
5100
  {
4054
5101
  name: route.name,
@@ -4061,7 +5108,7 @@ function createDrawerScreens(routes) {
4061
5108
  }
4062
5109
 
4063
5110
  // src/navigation/components/AppHeader.tsx
4064
- import { StyleSheet as StyleSheet17 } from "react-native";
5111
+ import { StyleSheet as StyleSheet18 } from "react-native";
4065
5112
  import { useSafeAreaInsets as useSafeAreaInsets3 } from "react-native-safe-area-context";
4066
5113
 
4067
5114
  // src/overlay/AppProvider.tsx
@@ -4070,7 +5117,7 @@ import { SafeAreaProvider } from "react-native-safe-area-context";
4070
5117
  // src/overlay/AppStatusBar.tsx
4071
5118
  import { StatusBar } from "react-native";
4072
5119
  import { useIsFocused } from "@react-navigation/native";
4073
- import { jsx as jsx36 } from "nativewind/jsx-runtime";
5120
+ import { jsx as jsx39 } from "nativewind/jsx-runtime";
4074
5121
  function AppStatusBar({
4075
5122
  barStyle = "auto",
4076
5123
  backgroundColor,
@@ -4080,7 +5127,7 @@ function AppStatusBar({
4080
5127
  const { theme, isDark } = useTheme();
4081
5128
  const resolvedBarStyle = barStyle === "auto" ? isDark ? "light-content" : "dark-content" : barStyle;
4082
5129
  const resolvedBackgroundColor = backgroundColor ?? (translucent ? "transparent" : theme.colors.background?.[500] || "#ffffff");
4083
- return /* @__PURE__ */ jsx36(
5130
+ return /* @__PURE__ */ jsx39(
4084
5131
  StatusBar,
4085
5132
  {
4086
5133
  barStyle: resolvedBarStyle,
@@ -4093,11 +5140,11 @@ function AppStatusBar({
4093
5140
  function AppFocusedStatusBar(props) {
4094
5141
  const isFocused = useIsFocused();
4095
5142
  if (!isFocused) return null;
4096
- return /* @__PURE__ */ jsx36(AppStatusBar, { ...props });
5143
+ return /* @__PURE__ */ jsx39(AppStatusBar, { ...props });
4097
5144
  }
4098
5145
 
4099
5146
  // src/overlay/loading/provider.tsx
4100
- import { useState as useState27, useCallback as useCallback19 } from "react";
5147
+ import { useState as useState31, useCallback as useCallback21, useEffect as useEffect18, useRef as useRef12 } from "react";
4101
5148
 
4102
5149
  // src/overlay/loading/context.ts
4103
5150
  import { createContext as createContext2, useContext as useContext2 } from "react";
@@ -4109,15 +5156,42 @@ function useLoadingContext() {
4109
5156
  }
4110
5157
 
4111
5158
  // src/overlay/loading/component.tsx
4112
- import { View as View9, Modal as Modal5, ActivityIndicator as ActivityIndicator5, StyleSheet as StyleSheet14 } from "react-native";
4113
- import { jsx as jsx37, jsxs as jsxs14 } from "nativewind/jsx-runtime";
4114
- function LoadingModal({ visible, text }) {
4115
- return /* @__PURE__ */ jsx37(Modal5, { transparent: true, visible, animationType: "fade", children: /* @__PURE__ */ jsx37(View9, { style: styles13.overlay, children: /* @__PURE__ */ jsxs14(View9, { style: [styles13.loadingBox, { backgroundColor: "rgba(0,0,0,0.8)" }], children: [
4116
- /* @__PURE__ */ jsx37(ActivityIndicator5, { size: "large", color: "#fff" }),
4117
- text && /* @__PURE__ */ jsx37(AppText, { className: "text-white mt-3 text-sm", children: text })
5159
+ import { useEffect as useEffect17, useState as useState30 } from "react";
5160
+ import { View as View9, Modal as Modal4, ActivityIndicator as ActivityIndicator5, StyleSheet as StyleSheet15 } from "react-native";
5161
+ import { jsx as jsx40, jsxs as jsxs15 } from "nativewind/jsx-runtime";
5162
+ var LOADING_CLOSE_DELAY2 = 3e4;
5163
+ function LoadingModal({
5164
+ visible,
5165
+ text,
5166
+ onRequestClose
5167
+ }) {
5168
+ const [showCloseButton, setShowCloseButton] = useState30(false);
5169
+ useEffect17(() => {
5170
+ if (!visible) {
5171
+ setShowCloseButton(false);
5172
+ return;
5173
+ }
5174
+ setShowCloseButton(false);
5175
+ const timer = setTimeout(() => {
5176
+ setShowCloseButton(true);
5177
+ }, LOADING_CLOSE_DELAY2);
5178
+ return () => clearTimeout(timer);
5179
+ }, [visible]);
5180
+ return /* @__PURE__ */ jsx40(Modal4, { transparent: true, visible, animationType: "fade", children: /* @__PURE__ */ jsx40(View9, { style: styles14.overlay, children: /* @__PURE__ */ jsxs15(View9, { style: [styles14.loadingBox, { backgroundColor: "rgba(0,0,0,0.8)" }], children: [
5181
+ /* @__PURE__ */ jsx40(ActivityIndicator5, { size: "large", color: "#fff" }),
5182
+ text && /* @__PURE__ */ jsx40(AppText, { className: "text-white mt-3 text-sm", children: text }),
5183
+ showCloseButton && onRequestClose && /* @__PURE__ */ jsx40(
5184
+ AppPressable,
5185
+ {
5186
+ testID: "loading-close",
5187
+ className: "mt-3 p-1",
5188
+ onPress: onRequestClose,
5189
+ children: /* @__PURE__ */ jsx40(Icon, { name: "close", size: "md", color: "#ffffff" })
5190
+ }
5191
+ )
4118
5192
  ] }) }) });
4119
5193
  }
4120
- var styles13 = StyleSheet14.create({
5194
+ var styles14 = StyleSheet15.create({
4121
5195
  overlay: {
4122
5196
  flex: 1,
4123
5197
  backgroundColor: "rgba(0,0,0,0.5)",
@@ -4133,24 +5207,53 @@ var styles13 = StyleSheet14.create({
4133
5207
  });
4134
5208
 
4135
5209
  // src/overlay/loading/provider.tsx
4136
- import { jsx as jsx38, jsxs as jsxs15 } from "nativewind/jsx-runtime";
5210
+ import { jsx as jsx41, jsxs as jsxs16 } from "nativewind/jsx-runtime";
5211
+ var MIN_VISIBLE_DURATION = 500;
4137
5212
  function LoadingProvider({ children }) {
4138
- const [state, setState] = useState27({ visible: false });
4139
- const show = useCallback19((text) => {
4140
- setState({ visible: true, text });
5213
+ const [state, setState] = useState31({ visible: false });
5214
+ const shownAtRef = useRef12(0);
5215
+ const pendingCountRef = useRef12(0);
5216
+ const hideTimerRef = useRef12(null);
5217
+ const clearHideTimer = useCallback21(() => {
5218
+ if (!hideTimerRef.current) return;
5219
+ clearTimeout(hideTimerRef.current);
5220
+ hideTimerRef.current = null;
4141
5221
  }, []);
4142
- const hide = useCallback19(() => {
4143
- setState({ visible: false });
4144
- }, []);
4145
- return /* @__PURE__ */ jsxs15(LoadingContext.Provider, { value: { show, hide }, children: [
5222
+ const show = useCallback21((text) => {
5223
+ pendingCountRef.current += 1;
5224
+ clearHideTimer();
5225
+ setState((previous) => {
5226
+ if (!previous.visible) {
5227
+ shownAtRef.current = Date.now();
5228
+ }
5229
+ return { visible: true, text };
5230
+ });
5231
+ }, [clearHideTimer]);
5232
+ const hide = useCallback21(() => {
5233
+ pendingCountRef.current = Math.max(0, pendingCountRef.current - 1);
5234
+ if (pendingCountRef.current > 0) return;
5235
+ const elapsed = Date.now() - shownAtRef.current;
5236
+ const remaining = Math.max(0, MIN_VISIBLE_DURATION - elapsed);
5237
+ clearHideTimer();
5238
+ if (remaining === 0) {
5239
+ setState({ visible: false });
5240
+ return;
5241
+ }
5242
+ hideTimerRef.current = setTimeout(() => {
5243
+ hideTimerRef.current = null;
5244
+ setState({ visible: false });
5245
+ }, remaining);
5246
+ }, [clearHideTimer]);
5247
+ useEffect18(() => () => clearHideTimer(), [clearHideTimer]);
5248
+ return /* @__PURE__ */ jsxs16(LoadingContext.Provider, { value: { show, hide }, children: [
4146
5249
  children,
4147
- /* @__PURE__ */ jsx38(LoadingModal, { ...state })
5250
+ /* @__PURE__ */ jsx41(LoadingModal, { ...state, onRequestClose: hide })
4148
5251
  ] });
4149
5252
  }
4150
5253
 
4151
5254
  // src/overlay/toast/provider.tsx
4152
- import { useState as useState28, useCallback as useCallback20, useRef as useRef9 } from "react";
4153
- import { View as View10, StyleSheet as StyleSheet15 } from "react-native";
5255
+ import { useState as useState32, useCallback as useCallback22, useRef as useRef14 } from "react";
5256
+ import { View as View10, StyleSheet as StyleSheet16 } from "react-native";
4154
5257
 
4155
5258
  // src/overlay/toast/context.ts
4156
5259
  import { createContext as createContext3, useContext as useContext3 } from "react";
@@ -4162,26 +5265,54 @@ function useToastContext() {
4162
5265
  }
4163
5266
 
4164
5267
  // src/overlay/toast/component.tsx
4165
- import { useRef as useRef8, useEffect as useEffect10 } from "react";
4166
- import { Animated } from "react-native";
4167
- import { jsx as jsx39 } from "nativewind/jsx-runtime";
5268
+ import { useRef as useRef13, useEffect as useEffect19 } from "react";
5269
+ import { Animated as Animated4 } from "react-native";
5270
+ import { jsx as jsx42 } from "nativewind/jsx-runtime";
5271
+ function createAnimatedValue4(value) {
5272
+ const AnimatedValue = Animated4.Value;
5273
+ try {
5274
+ return new AnimatedValue(value);
5275
+ } catch {
5276
+ return AnimatedValue(value);
5277
+ }
5278
+ }
4168
5279
  function ToastItemView({ message, type, onHide }) {
4169
- const fadeAnim = useRef8(new Animated.Value(0)).current;
4170
- useEffect10(() => {
4171
- Animated.sequence([
4172
- Animated.timing(fadeAnim, { toValue: 1, duration: 200, useNativeDriver: true }),
4173
- Animated.delay(2500),
4174
- Animated.timing(fadeAnim, { toValue: 0, duration: 200, useNativeDriver: true })
5280
+ const fadeAnim = useRef13(createAnimatedValue4(0)).current;
5281
+ const { theme } = useOptionalTheme();
5282
+ useEffect19(() => {
5283
+ Animated4.sequence([
5284
+ Animated4.timing(fadeAnim, { toValue: 1, duration: 200, useNativeDriver: true }),
5285
+ Animated4.delay(2500),
5286
+ Animated4.timing(fadeAnim, { toValue: 0, duration: 200, useNativeDriver: true })
4175
5287
  ]).start(onHide);
4176
5288
  }, []);
4177
- const bgColors = {
4178
- success: "bg-success-500",
4179
- error: "bg-error-500",
4180
- warning: "bg-warning-500",
4181
- info: "bg-primary-500"
5289
+ const palette = {
5290
+ success: {
5291
+ backgroundColor: theme.colors.success?.[500] || "#22c55e",
5292
+ textColor: "#ffffff"
5293
+ },
5294
+ error: {
5295
+ backgroundColor: theme.colors.error?.[500] || "#ef4444",
5296
+ textColor: "#ffffff"
5297
+ },
5298
+ warning: {
5299
+ backgroundColor: theme.colors.warning?.[500] || "#f59e0b",
5300
+ textColor: "#111827"
5301
+ },
5302
+ info: {
5303
+ backgroundColor: theme.colors.info?.[500] || theme.colors.primary?.[500] || "#3b82f6",
5304
+ textColor: "#ffffff"
5305
+ }
4182
5306
  };
4183
- return /* @__PURE__ */ jsx39(
4184
- Animated.View,
5307
+ const currentPalette = palette[type];
5308
+ const bgStyles = {
5309
+ success: "bg-green-500",
5310
+ error: "bg-red-500",
5311
+ warning: "bg-yellow-500",
5312
+ info: "bg-blue-500"
5313
+ };
5314
+ return /* @__PURE__ */ jsx42(
5315
+ Animated4.View,
4185
5316
  {
4186
5317
  style: {
4187
5318
  opacity: fadeAnim,
@@ -4194,17 +5325,25 @@ function ToastItemView({ message, type, onHide }) {
4194
5325
  }
4195
5326
  ]
4196
5327
  },
4197
- children: /* @__PURE__ */ jsx39(AppView, { className: `${bgColors[type]} px-4 py-3 rounded-lg mb-2 mx-4 shadow-lg`, children: /* @__PURE__ */ jsx39(AppText, { className: "text-white text-center", children: message }) })
5328
+ children: /* @__PURE__ */ jsx42(
5329
+ AppView,
5330
+ {
5331
+ testID: `toast-item-${type}`,
5332
+ className: `${bgStyles[type]} px-4 py-3 rounded-lg mb-2 mx-4 shadow-lg`,
5333
+ style: { backgroundColor: currentPalette.backgroundColor },
5334
+ children: /* @__PURE__ */ jsx42(AppText, { className: "text-center", style: { color: currentPalette.textColor }, children: message })
5335
+ }
5336
+ )
4198
5337
  }
4199
5338
  );
4200
5339
  }
4201
5340
 
4202
5341
  // src/overlay/toast/provider.tsx
4203
- import { jsx as jsx40, jsxs as jsxs16 } from "nativewind/jsx-runtime";
5342
+ import { jsx as jsx43, jsxs as jsxs17 } from "nativewind/jsx-runtime";
4204
5343
  function ToastProvider({ children }) {
4205
- const [toasts, setToasts] = useState28([]);
4206
- const timersRef = useRef9(/* @__PURE__ */ new Map());
4207
- const remove = useCallback20((id) => {
5344
+ const [toasts, setToasts] = useState32([]);
5345
+ const timersRef = useRef14(/* @__PURE__ */ new Map());
5346
+ const remove = useCallback22((id) => {
4208
5347
  setToasts((prev) => prev.filter((t) => t.id !== id));
4209
5348
  const timer = timersRef.current.get(id);
4210
5349
  if (timer) {
@@ -4212,7 +5351,7 @@ function ToastProvider({ children }) {
4212
5351
  timersRef.current.delete(id);
4213
5352
  }
4214
5353
  }, []);
4215
- const show = useCallback20(
5354
+ const show = useCallback22(
4216
5355
  (message, type = "info", duration = 3e3) => {
4217
5356
  const id = Math.random().toString(36).substring(7);
4218
5357
  const toast = { id, message, type, duration };
@@ -4222,28 +5361,28 @@ function ToastProvider({ children }) {
4222
5361
  },
4223
5362
  [remove]
4224
5363
  );
4225
- const success = useCallback20(
5364
+ const success = useCallback22(
4226
5365
  (message, duration) => show(message, "success", duration),
4227
5366
  [show]
4228
5367
  );
4229
- const error = useCallback20(
5368
+ const error = useCallback22(
4230
5369
  (message, duration) => show(message, "error", duration),
4231
5370
  [show]
4232
5371
  );
4233
- const info = useCallback20(
5372
+ const info = useCallback22(
4234
5373
  (message, duration) => show(message, "info", duration),
4235
5374
  [show]
4236
5375
  );
4237
- const warning = useCallback20(
5376
+ const warning = useCallback22(
4238
5377
  (message, duration) => show(message, "warning", duration),
4239
5378
  [show]
4240
5379
  );
4241
- return /* @__PURE__ */ jsxs16(ToastContext.Provider, { value: { show, success, error, info, warning }, children: [
5380
+ return /* @__PURE__ */ jsxs17(ToastContext.Provider, { value: { show, success, error, info, warning }, children: [
4242
5381
  children,
4243
- /* @__PURE__ */ jsx40(View10, { style: styles14.toastContainer, pointerEvents: "none", children: toasts.map((toast) => /* @__PURE__ */ jsx40(ToastItemView, { ...toast, onHide: () => remove(toast.id) }, toast.id)) })
5382
+ /* @__PURE__ */ jsx43(View10, { style: styles15.toastContainer, pointerEvents: "none", children: toasts.map((toast) => /* @__PURE__ */ jsx43(ToastItemView, { ...toast, onHide: () => remove(toast.id) }, toast.id)) })
4244
5383
  ] });
4245
5384
  }
4246
- var styles14 = StyleSheet15.create({
5385
+ var styles15 = StyleSheet16.create({
4247
5386
  toastContainer: {
4248
5387
  position: "absolute",
4249
5388
  top: 60,
@@ -4254,7 +5393,7 @@ var styles14 = StyleSheet15.create({
4254
5393
  });
4255
5394
 
4256
5395
  // src/overlay/alert/provider.tsx
4257
- import { useState as useState29, useCallback as useCallback21 } from "react";
5396
+ import { useState as useState33, useCallback as useCallback23 } from "react";
4258
5397
 
4259
5398
  // src/overlay/alert/context.ts
4260
5399
  import { createContext as createContext4, useContext as useContext4 } from "react";
@@ -4266,8 +5405,17 @@ function useAlertContext() {
4266
5405
  }
4267
5406
 
4268
5407
  // src/overlay/alert/component.tsx
4269
- import { View as View11, Modal as Modal6, StyleSheet as StyleSheet16 } from "react-native";
4270
- import { jsx as jsx41, jsxs as jsxs17 } from "nativewind/jsx-runtime";
5408
+ import { useEffect as useEffect20, useRef as useRef15 } from "react";
5409
+ import { View as View11, Modal as Modal5, StyleSheet as StyleSheet17, Animated as Animated5 } from "react-native";
5410
+ import { jsx as jsx44, jsxs as jsxs18 } from "nativewind/jsx-runtime";
5411
+ function createAnimatedValue5(value) {
5412
+ const AnimatedValue = Animated5.Value;
5413
+ try {
5414
+ return new AnimatedValue(value);
5415
+ } catch {
5416
+ return AnimatedValue(value);
5417
+ }
5418
+ }
4271
5419
  function AlertModal({
4272
5420
  visible,
4273
5421
  title,
@@ -4278,23 +5426,67 @@ function AlertModal({
4278
5426
  onConfirm,
4279
5427
  onCancel
4280
5428
  }) {
5429
+ const progress = useRef15(createAnimatedValue5(0)).current;
5430
+ useEffect20(() => {
5431
+ if (!visible) {
5432
+ progress.setValue(0);
5433
+ return;
5434
+ }
5435
+ progress.setValue(0);
5436
+ Animated5.timing(progress, {
5437
+ toValue: 1,
5438
+ duration: 220,
5439
+ useNativeDriver: true
5440
+ }).start();
5441
+ }, [progress, visible]);
4281
5442
  if (!visible) return null;
4282
- return /* @__PURE__ */ jsx41(Modal6, { transparent: true, visible: true, animationType: "fade", children: /* @__PURE__ */ jsx41(View11, { style: styles15.overlay, children: /* @__PURE__ */ jsxs17(View11, { style: styles15.alertBox, children: [
4283
- title && /* @__PURE__ */ jsx41(AppText, { className: "text-lg font-semibold text-center mb-2", children: title }),
4284
- message && /* @__PURE__ */ jsx41(AppText, { className: "text-gray-600 text-center mb-4", children: message }),
4285
- /* @__PURE__ */ jsxs17(AppView, { row: true, gap: 3, className: "mt-2", children: [
4286
- showCancel && /* @__PURE__ */ jsx41(AppPressable, { onPress: onCancel, className: "flex-1 py-3 bg-gray-100 rounded-lg", children: /* @__PURE__ */ jsx41(AppText, { className: "text-center text-gray-700", children: cancelText || "\u53D6\u6D88" }) }),
4287
- /* @__PURE__ */ jsx41(AppPressable, { onPress: onConfirm, className: "flex-1 py-3 bg-primary-500 rounded-lg", children: /* @__PURE__ */ jsx41(AppText, { className: "text-center text-white", children: confirmText || "\u786E\u5B9A" }) })
4288
- ] })
4289
- ] }) }) });
5443
+ return /* @__PURE__ */ jsx44(Modal5, { transparent: true, visible: true, animationType: "none", children: /* @__PURE__ */ jsxs18(View11, { style: styles16.container, children: [
5444
+ /* @__PURE__ */ jsx44(Animated5.View, { style: [styles16.overlay, { opacity: progress }] }),
5445
+ /* @__PURE__ */ jsxs18(
5446
+ Animated5.View,
5447
+ {
5448
+ style: [
5449
+ styles16.alertBox,
5450
+ {
5451
+ opacity: progress,
5452
+ transform: [
5453
+ {
5454
+ translateY: progress.interpolate({
5455
+ inputRange: [0, 1],
5456
+ outputRange: [16, 0]
5457
+ })
5458
+ },
5459
+ {
5460
+ scale: progress.interpolate({
5461
+ inputRange: [0, 1],
5462
+ outputRange: [0.96, 1]
5463
+ })
5464
+ }
5465
+ ]
5466
+ }
5467
+ ],
5468
+ children: [
5469
+ title && /* @__PURE__ */ jsx44(AppText, { className: "text-lg font-semibold text-center mb-2", children: title }),
5470
+ message && /* @__PURE__ */ jsx44(AppText, { className: "text-gray-600 text-center mb-4", children: message }),
5471
+ /* @__PURE__ */ jsxs18(AppView, { row: true, gap: 3, className: "mt-2", children: [
5472
+ showCancel && /* @__PURE__ */ jsx44(AppPressable, { onPress: onCancel, className: "flex-1 py-3 bg-gray-100 rounded-lg", children: /* @__PURE__ */ jsx44(AppText, { className: "text-center text-gray-700", children: cancelText || "\u53D6\u6D88" }) }),
5473
+ /* @__PURE__ */ jsx44(AppPressable, { onPress: onConfirm, className: "flex-1 py-3 bg-primary-500 rounded-lg", children: /* @__PURE__ */ jsx44(AppText, { className: "text-center text-white", children: confirmText || "\u786E\u5B9A" }) })
5474
+ ] })
5475
+ ]
5476
+ }
5477
+ )
5478
+ ] }) });
4290
5479
  }
4291
- var styles15 = StyleSheet16.create({
4292
- overlay: {
5480
+ var styles16 = StyleSheet17.create({
5481
+ container: {
4293
5482
  flex: 1,
4294
- backgroundColor: "rgba(0,0,0,0.5)",
4295
5483
  justifyContent: "center",
4296
5484
  alignItems: "center"
4297
5485
  },
5486
+ overlay: {
5487
+ ...StyleSheet17.absoluteFillObject,
5488
+ backgroundColor: "rgba(0,0,0,0.5)"
5489
+ },
4298
5490
  alertBox: {
4299
5491
  backgroundColor: "white",
4300
5492
  borderRadius: 12,
@@ -4310,32 +5502,32 @@ var styles15 = StyleSheet16.create({
4310
5502
  });
4311
5503
 
4312
5504
  // src/overlay/alert/provider.tsx
4313
- import { jsx as jsx42, jsxs as jsxs18 } from "nativewind/jsx-runtime";
5505
+ import { jsx as jsx45, jsxs as jsxs19 } from "nativewind/jsx-runtime";
4314
5506
  function AlertProvider({ children }) {
4315
- const [alert, setAlert] = useState29(null);
4316
- const showAlert = useCallback21((options) => {
5507
+ const [alert, setAlert] = useState33(null);
5508
+ const showAlert = useCallback23((options) => {
4317
5509
  setAlert({ ...options, visible: true });
4318
5510
  }, []);
4319
- const confirm = useCallback21(
5511
+ const confirm = useCallback23(
4320
5512
  (options) => {
4321
5513
  showAlert({ ...options, showCancel: true });
4322
5514
  },
4323
5515
  [showAlert]
4324
5516
  );
4325
- const hide = useCallback21(() => {
5517
+ const hide = useCallback23(() => {
4326
5518
  setAlert(null);
4327
5519
  }, []);
4328
- const handleConfirm = useCallback21(() => {
5520
+ const handleConfirm = useCallback23(() => {
4329
5521
  alert?.onConfirm?.();
4330
5522
  hide();
4331
5523
  }, [alert, hide]);
4332
- const handleCancel = useCallback21(() => {
5524
+ const handleCancel = useCallback23(() => {
4333
5525
  alert?.onCancel?.();
4334
5526
  hide();
4335
5527
  }, [alert, hide]);
4336
- return /* @__PURE__ */ jsxs18(AlertContext.Provider, { value: { alert: showAlert, confirm }, children: [
5528
+ return /* @__PURE__ */ jsxs19(AlertContext.Provider, { value: { alert: showAlert, confirm }, children: [
4337
5529
  children,
4338
- /* @__PURE__ */ jsx42(
5530
+ /* @__PURE__ */ jsx45(
4339
5531
  AlertModal,
4340
5532
  {
4341
5533
  visible: alert?.visible ?? false,
@@ -4351,14 +5543,378 @@ function AlertProvider({ children }) {
4351
5543
  ] });
4352
5544
  }
4353
5545
 
5546
+ // src/overlay/logger/provider.tsx
5547
+ import { useCallback as useCallback24, useEffect as useEffect21, useMemo as useMemo12, useState as useState35 } from "react";
5548
+
5549
+ // src/overlay/logger/context.ts
5550
+ import { createContext as createContext5, useContext as useContext5 } from "react";
5551
+ var LoggerContext = createContext5(null);
5552
+ function useLoggerContext() {
5553
+ const ctx = useContext5(LoggerContext);
5554
+ if (!ctx) throw new Error("useLogger must be used within LoggerProvider");
5555
+ return ctx;
5556
+ }
5557
+
5558
+ // src/overlay/logger/component.tsx
5559
+ import { useMemo as useMemo11, useState as useState34 } from "react";
5560
+ import { jsx as jsx46, jsxs as jsxs20 } from "nativewind/jsx-runtime";
5561
+ var FILTERS = ["all", "error", "warn", "info", "debug"];
5562
+ var ALL_NAMESPACE = "all";
5563
+ var DEFAULT_SEARCH_PLACEHOLDER = "\u641C\u7D22\u65E5\u5FD7";
5564
+ function withAlpha(color, alpha = "20") {
5565
+ return color.startsWith("#") && color.length === 7 ? `${color}${alpha}` : color;
5566
+ }
5567
+ function matchesSearch(entry, keyword) {
5568
+ if (!keyword.trim()) return true;
5569
+ const normalized = keyword.trim().toLowerCase();
5570
+ const detail = stringifyLogData(entry.data).toLowerCase();
5571
+ return [entry.message, entry.namespace ?? "", detail].join(" ").toLowerCase().includes(normalized);
5572
+ }
5573
+ function LogOverlay({
5574
+ entries,
5575
+ onClear,
5576
+ defaultExpanded = false,
5577
+ exportEnabled = true,
5578
+ onExport
5579
+ }) {
5580
+ const colors = useThemeColors();
5581
+ const [expanded, setExpanded] = useState34(defaultExpanded);
5582
+ const [filter, setFilter] = useState34("all");
5583
+ const [namespaceFilter, setNamespaceFilter] = useState34(ALL_NAMESPACE);
5584
+ const [keyword, setKeyword] = useState34("");
5585
+ const namespaces = useMemo11(
5586
+ () => [
5587
+ ALL_NAMESPACE,
5588
+ ...Array.from(
5589
+ new Set(
5590
+ entries.map((entry) => entry.namespace).filter((value) => Boolean(value))
5591
+ )
5592
+ )
5593
+ ],
5594
+ [entries]
5595
+ );
5596
+ const filteredEntries = useMemo11(() => {
5597
+ return entries.filter((entry) => {
5598
+ const levelMatched = filter === "all" ? true : entry.level === filter;
5599
+ const namespaceMatched = namespaceFilter === ALL_NAMESPACE ? true : entry.namespace === namespaceFilter;
5600
+ const searchMatched = matchesSearch(entry, keyword);
5601
+ return levelMatched && namespaceMatched && searchMatched;
5602
+ });
5603
+ }, [entries, filter, keyword, namespaceFilter]);
5604
+ const levelStyles = {
5605
+ debug: { text: colors.muted, bg: colors.cardElevated },
5606
+ info: { text: colors.info, bg: withAlpha(colors.info) },
5607
+ warn: { text: colors.warning, bg: withAlpha(colors.warning) },
5608
+ error: { text: colors.error, bg: withAlpha(colors.error) }
5609
+ };
5610
+ const handleExport = () => {
5611
+ const payload = {
5612
+ entries: filteredEntries,
5613
+ serialized: serializeLogEntries(filteredEntries)
5614
+ };
5615
+ if (onExport) {
5616
+ onExport(payload);
5617
+ return;
5618
+ }
5619
+ console.info("[LoggerExport]", payload.serialized);
5620
+ };
5621
+ return /* @__PURE__ */ jsxs20(AppView, { pointerEvents: "box-none", style: { position: "absolute", right: 12, bottom: 24, left: 12, zIndex: 9998 }, children: [
5622
+ expanded && /* @__PURE__ */ jsxs20(
5623
+ AppView,
5624
+ {
5625
+ testID: "logger-overlay-panel",
5626
+ style: {
5627
+ maxHeight: 360,
5628
+ borderRadius: 16,
5629
+ backgroundColor: colors.card,
5630
+ borderWidth: 0.5,
5631
+ borderColor: colors.border,
5632
+ shadowColor: "#000000",
5633
+ shadowOpacity: 0.15,
5634
+ shadowRadius: 16,
5635
+ shadowOffset: { width: 0, height: 8 },
5636
+ elevation: 12,
5637
+ marginBottom: 12
5638
+ },
5639
+ children: [
5640
+ /* @__PURE__ */ jsxs20(AppView, { row: true, items: "center", justify: "between", className: "px-4 py-3", style: { borderBottomWidth: 0.5, borderBottomColor: colors.divider }, children: [
5641
+ /* @__PURE__ */ jsx46(AppText, { weight: "semibold", children: "\u5F00\u53D1\u65E5\u5FD7" }),
5642
+ /* @__PURE__ */ jsxs20(AppView, { row: true, gap: 2, children: [
5643
+ exportEnabled ? /* @__PURE__ */ jsx46(AppPressable, { testID: "logger-overlay-export", onPress: handleExport, className: "px-3 py-1 rounded-full", style: { backgroundColor: colors.cardElevated }, children: /* @__PURE__ */ jsx46(AppText, { size: "xs", tone: "muted", children: "\u5BFC\u51FA" }) }) : null,
5644
+ /* @__PURE__ */ jsx46(AppPressable, { onPress: onClear, className: "px-3 py-1 rounded-full", style: { backgroundColor: colors.cardElevated }, children: /* @__PURE__ */ jsx46(AppText, { size: "xs", tone: "muted", children: "\u6E05\u7A7A" }) }),
5645
+ /* @__PURE__ */ jsx46(AppPressable, { onPress: () => setExpanded(false), className: "px-3 py-1 rounded-full", style: { backgroundColor: colors.cardElevated }, children: /* @__PURE__ */ jsx46(AppText, { size: "xs", tone: "muted", children: "\u6536\u8D77" }) })
5646
+ ] })
5647
+ ] }),
5648
+ /* @__PURE__ */ jsx46(AppScrollView, { horizontal: true, showsHorizontalScrollIndicator: false, className: "px-3 py-2", contentContainerStyle: { gap: 8 }, children: FILTERS.map((item) => {
5649
+ const active = filter === item;
5650
+ return /* @__PURE__ */ jsx46(
5651
+ AppPressable,
5652
+ {
5653
+ onPress: () => setFilter(item),
5654
+ className: "px-3 py-1 rounded-full",
5655
+ style: { backgroundColor: active ? colors.primary : colors.cardElevated },
5656
+ children: /* @__PURE__ */ jsx46(AppText, { size: "xs", style: { color: active ? colors.textInverse : colors.textSecondary }, children: item.toUpperCase() })
5657
+ },
5658
+ item
5659
+ );
5660
+ }) }),
5661
+ /* @__PURE__ */ jsx46(AppScrollView, { horizontal: true, showsHorizontalScrollIndicator: false, className: "px-3 pb-2", contentContainerStyle: { gap: 8 }, children: namespaces.map((item) => {
5662
+ const active = namespaceFilter === item;
5663
+ const label = item === ALL_NAMESPACE ? "\u5168\u90E8\u6A21\u5757" : String(item);
5664
+ return /* @__PURE__ */ jsx46(
5665
+ AppPressable,
5666
+ {
5667
+ testID: `logger-namespace-${item}`,
5668
+ onPress: () => setNamespaceFilter(item),
5669
+ className: "px-3 py-1 rounded-full",
5670
+ style: { backgroundColor: active ? colors.info : colors.cardElevated },
5671
+ children: /* @__PURE__ */ jsx46(AppText, { size: "xs", style: { color: active ? colors.textInverse : colors.textSecondary }, children: label })
5672
+ },
5673
+ item
5674
+ );
5675
+ }) }),
5676
+ /* @__PURE__ */ jsx46(AppView, { className: "px-3 pb-2", children: /* @__PURE__ */ jsx46(
5677
+ AppInput,
5678
+ {
5679
+ placeholder: DEFAULT_SEARCH_PLACEHOLDER,
5680
+ value: keyword,
5681
+ onChangeText: setKeyword
5682
+ }
5683
+ ) }),
5684
+ /* @__PURE__ */ jsx46(AppScrollView, { className: "px-3 pb-3", showsVerticalScrollIndicator: false, children: /* @__PURE__ */ jsx46(AppView, { gap: 2, children: filteredEntries.length === 0 ? /* @__PURE__ */ jsx46(AppView, { className: "py-8 items-center", children: /* @__PURE__ */ jsx46(AppText, { tone: "muted", children: "\u6682\u65E0\u65E5\u5FD7" }) }) : filteredEntries.map((entry) => {
5685
+ const palette = levelStyles[entry.level];
5686
+ const detail = stringifyLogData(entry.data);
5687
+ return /* @__PURE__ */ jsxs20(
5688
+ AppView,
5689
+ {
5690
+ className: "px-3 py-3 rounded-xl",
5691
+ style: { backgroundColor: colors.cardElevated, borderWidth: 0.5, borderColor: colors.divider },
5692
+ children: [
5693
+ /* @__PURE__ */ jsxs20(AppView, { row: true, items: "center", justify: "between", children: [
5694
+ /* @__PURE__ */ jsxs20(AppView, { row: true, items: "center", gap: 2, children: [
5695
+ /* @__PURE__ */ jsx46(AppView, { className: "px-2 py-1 rounded-full", style: { backgroundColor: palette.bg }, children: /* @__PURE__ */ jsx46(AppText, { size: "xs", style: { color: palette.text }, children: entry.level.toUpperCase() }) }),
5696
+ entry.namespace ? /* @__PURE__ */ jsxs20(AppText, { size: "xs", tone: "muted", children: [
5697
+ "[",
5698
+ entry.namespace,
5699
+ "]"
5700
+ ] }) : null
5701
+ ] }),
5702
+ /* @__PURE__ */ jsx46(AppText, { size: "xs", tone: "muted", children: formatLogTime(entry.timestamp) })
5703
+ ] }),
5704
+ /* @__PURE__ */ jsx46(AppText, { className: "mt-2", size: "sm", children: entry.message }),
5705
+ detail ? /* @__PURE__ */ jsx46(AppView, { className: "mt-2 px-2 py-2 rounded-lg", style: { backgroundColor: colors.background }, children: /* @__PURE__ */ jsx46(AppText, { size: "xs", tone: "muted", children: detail }) }) : null
5706
+ ]
5707
+ },
5708
+ entry.id
5709
+ );
5710
+ }) }) })
5711
+ ]
5712
+ }
5713
+ ),
5714
+ /* @__PURE__ */ jsx46(AppView, { pointerEvents: "box-none", row: true, justify: "end", children: /* @__PURE__ */ jsx46(
5715
+ AppPressable,
5716
+ {
5717
+ testID: "logger-overlay-toggle",
5718
+ onPress: () => setExpanded((value) => !value),
5719
+ className: "px-4 py-3 rounded-full",
5720
+ style: { backgroundColor: colors.primary, shadowColor: "#000000", shadowOpacity: 0.18, shadowRadius: 12, shadowOffset: { width: 0, height: 4 }, elevation: 8 },
5721
+ children: /* @__PURE__ */ jsxs20(AppText, { weight: "semibold", style: { color: colors.textInverse }, children: [
5722
+ "Logs ",
5723
+ entries.length
5724
+ ] })
5725
+ }
5726
+ ) })
5727
+ ] });
5728
+ }
5729
+
5730
+ // src/overlay/logger/provider.tsx
5731
+ import { jsx as jsx47, jsxs as jsxs21 } from "nativewind/jsx-runtime";
5732
+ function LoggerProvider({
5733
+ children,
5734
+ enabled = false,
5735
+ level = "debug",
5736
+ maxEntries = 200,
5737
+ overlayEnabled = false,
5738
+ defaultExpanded = false,
5739
+ consoleEnabled = true,
5740
+ transports = [],
5741
+ exportEnabled = true,
5742
+ onExport
5743
+ }) {
5744
+ const [entries, setEntries] = useState35([]);
5745
+ const clear = useCallback24(() => {
5746
+ setEntries([]);
5747
+ }, []);
5748
+ const resolvedTransports = useMemo12(() => {
5749
+ const list = [];
5750
+ if (enabled && consoleEnabled) {
5751
+ list.push(createConsoleTransport());
5752
+ }
5753
+ if (transports.length > 0) {
5754
+ list.push(...transports);
5755
+ }
5756
+ return list;
5757
+ }, [consoleEnabled, enabled, transports]);
5758
+ const write = useCallback24(
5759
+ (nextLevel, message, data, namespace) => {
5760
+ if (!enabled || !shouldLog(level, nextLevel)) return;
5761
+ const entry = createLogEntry({ level: nextLevel, message, data, namespace, source: "logger" });
5762
+ setEntries((prev) => [entry, ...prev].slice(0, maxEntries));
5763
+ resolvedTransports.forEach((transport) => transport(entry));
5764
+ },
5765
+ [enabled, level, maxEntries, resolvedTransports]
5766
+ );
5767
+ const contextValue = useMemo12(
5768
+ () => ({
5769
+ entries,
5770
+ enabled,
5771
+ level,
5772
+ clear,
5773
+ log: write,
5774
+ debug: (message, data, namespace) => write("debug", message, data, namespace),
5775
+ info: (message, data, namespace) => write("info", message, data, namespace),
5776
+ warn: (message, data, namespace) => write("warn", message, data, namespace),
5777
+ error: (message, data, namespace) => write("error", message, data, namespace)
5778
+ }),
5779
+ [clear, enabled, entries, level, write]
5780
+ );
5781
+ useEffect21(() => {
5782
+ if (!enabled) {
5783
+ setGlobalLogger(null);
5784
+ return;
5785
+ }
5786
+ setGlobalLogger(contextValue);
5787
+ return () => {
5788
+ setGlobalLogger(null);
5789
+ };
5790
+ }, [contextValue, enabled]);
5791
+ return /* @__PURE__ */ jsxs21(LoggerContext.Provider, { value: contextValue, children: [
5792
+ children,
5793
+ enabled && overlayEnabled ? /* @__PURE__ */ jsx47(
5794
+ LogOverlay,
5795
+ {
5796
+ entries,
5797
+ onClear: clear,
5798
+ defaultExpanded,
5799
+ exportEnabled,
5800
+ onExport
5801
+ }
5802
+ ) : null
5803
+ ] });
5804
+ }
5805
+
5806
+ // src/overlay/error-boundary/component.tsx
5807
+ import React12 from "react";
5808
+ import { jsx as jsx48, jsxs as jsxs22 } from "nativewind/jsx-runtime";
5809
+ function areResetKeysChanged(prev = [], next = []) {
5810
+ if (prev.length !== next.length) return true;
5811
+ return prev.some((item, index) => !Object.is(item, next[index]));
5812
+ }
5813
+ function DefaultFallback({
5814
+ error,
5815
+ onReset,
5816
+ title = "\u9875\u9762\u53D1\u751F\u5F02\u5E38",
5817
+ description = "\u5DF2\u81EA\u52A8\u6355\u83B7\u6E32\u67D3\u9519\u8BEF\uFF0C\u4F60\u53EF\u4EE5\u91CD\u8BD5\u5E76\u7ED3\u5408\u5F00\u53D1\u65E5\u5FD7\u7EE7\u7EED\u6392\u67E5\u3002",
5818
+ resetText = "\u91CD\u8BD5",
5819
+ showDetails = isDevelopment()
5820
+ }) {
5821
+ return /* @__PURE__ */ jsxs22(AppView, { testID: "app-error-boundary", flex: true, center: true, className: "px-6 py-8", gap: 4, children: [
5822
+ /* @__PURE__ */ jsxs22(AppView, { className: "items-center", gap: 2, children: [
5823
+ /* @__PURE__ */ jsx48(AppText, { size: "xl", weight: "semibold", children: title }),
5824
+ /* @__PURE__ */ jsx48(AppText, { tone: "muted", style: { textAlign: "center" }, children: description })
5825
+ ] }),
5826
+ showDetails ? /* @__PURE__ */ jsx48(
5827
+ AppView,
5828
+ {
5829
+ className: "w-full px-4 py-3 rounded-xl",
5830
+ style: { maxWidth: 560, borderWidth: 0.5 },
5831
+ children: /* @__PURE__ */ jsx48(AppText, { testID: "app-error-boundary-detail", size: "sm", children: error.message || String(error) })
5832
+ }
5833
+ ) : null,
5834
+ /* @__PURE__ */ jsx48(
5835
+ AppPressable,
5836
+ {
5837
+ testID: "app-error-boundary-reset",
5838
+ onPress: onReset,
5839
+ className: "px-4 py-3 rounded-lg",
5840
+ style: { borderWidth: 0.5 },
5841
+ children: /* @__PURE__ */ jsx48(AppText, { weight: "semibold", children: resetText })
5842
+ }
5843
+ )
5844
+ ] });
5845
+ }
5846
+ var AppErrorBoundary = class extends React12.Component {
5847
+ constructor() {
5848
+ super(...arguments);
5849
+ this.state = {
5850
+ error: null
5851
+ };
5852
+ this.handleReset = () => {
5853
+ this.setState({ error: null });
5854
+ this.props.onReset?.();
5855
+ };
5856
+ }
5857
+ static getDerivedStateFromError(error) {
5858
+ return { error };
5859
+ }
5860
+ componentDidCatch(error, errorInfo) {
5861
+ this.context?.error(
5862
+ "React ErrorBoundary \u6355\u83B7\u6E32\u67D3\u5F02\u5E38",
5863
+ {
5864
+ name: error.name,
5865
+ message: error.message,
5866
+ stack: error.stack,
5867
+ componentStack: errorInfo.componentStack
5868
+ },
5869
+ "react"
5870
+ );
5871
+ this.props.onError?.(error, errorInfo);
5872
+ }
5873
+ componentDidUpdate(prevProps) {
5874
+ if (!this.state.error) return;
5875
+ if (areResetKeysChanged(prevProps.resetKeys, this.props.resetKeys)) {
5876
+ this.handleReset();
5877
+ }
5878
+ }
5879
+ render() {
5880
+ const {
5881
+ children,
5882
+ enabled = false,
5883
+ fallback,
5884
+ title,
5885
+ description,
5886
+ showDetails,
5887
+ resetText
5888
+ } = this.props;
5889
+ if (!enabled) return children;
5890
+ if (!this.state.error) return children;
5891
+ if (typeof fallback === "function") {
5892
+ return fallback({ error: this.state.error, reset: this.handleReset });
5893
+ }
5894
+ if (fallback) return fallback;
5895
+ return /* @__PURE__ */ jsx48(
5896
+ DefaultFallback,
5897
+ {
5898
+ error: this.state.error,
5899
+ onReset: this.handleReset,
5900
+ title,
5901
+ description,
5902
+ showDetails,
5903
+ resetText
5904
+ }
5905
+ );
5906
+ }
5907
+ };
5908
+ AppErrorBoundary.contextType = LoggerContext;
5909
+
4354
5910
  // src/overlay/provider.tsx
4355
- import { jsx as jsx43 } from "nativewind/jsx-runtime";
4356
- function OverlayProvider({ children }) {
4357
- return /* @__PURE__ */ jsx43(LoadingProvider, { children: /* @__PURE__ */ jsx43(ToastProvider, { children: /* @__PURE__ */ jsx43(AlertProvider, { children }) }) });
5911
+ import { jsx as jsx49 } from "nativewind/jsx-runtime";
5912
+ function OverlayProvider({ children, loggerProps, errorBoundaryProps }) {
5913
+ return /* @__PURE__ */ jsx49(LoggerProvider, { ...loggerProps, children: /* @__PURE__ */ jsx49(AppErrorBoundary, { ...errorBoundaryProps, children: /* @__PURE__ */ jsx49(LoadingProvider, { children: /* @__PURE__ */ jsx49(ToastProvider, { children: /* @__PURE__ */ jsx49(AlertProvider, { children }) }) }) }) });
4358
5914
  }
4359
5915
 
4360
5916
  // src/overlay/AppProvider.tsx
4361
- import { Fragment as Fragment4, jsx as jsx44, jsxs as jsxs19 } from "nativewind/jsx-runtime";
5917
+ import { Fragment as Fragment4, jsx as jsx50, jsxs as jsxs23 } from "nativewind/jsx-runtime";
4362
5918
  var defaultLightTheme = {
4363
5919
  colors: {
4364
5920
  primary: "#f38b32",
@@ -4384,6 +5940,8 @@ function AppProvider({
4384
5940
  enableNavigation = true,
4385
5941
  enableOverlay = true,
4386
5942
  enableTheme = true,
5943
+ enableLogger = isDevelopment(),
5944
+ enableErrorBoundary = isDevelopment(),
4387
5945
  enableStatusBar = true,
4388
5946
  enableSafeArea = true,
4389
5947
  lightTheme = defaultLightTheme,
@@ -4391,25 +5949,41 @@ function AppProvider({
4391
5949
  defaultDark = false,
4392
5950
  isDark,
4393
5951
  statusBarProps,
5952
+ loggerProps,
5953
+ errorBoundaryProps,
4394
5954
  ...navigationProps
4395
5955
  }) {
4396
5956
  let content = children;
4397
5957
  if (enableOverlay) {
4398
- content = /* @__PURE__ */ jsx44(OverlayProvider, { children: content });
5958
+ content = /* @__PURE__ */ jsx50(
5959
+ OverlayProvider,
5960
+ {
5961
+ loggerProps: enableLogger ? { enabled: true, overlayEnabled: true, ...loggerProps } : { enabled: false, overlayEnabled: false, ...loggerProps },
5962
+ errorBoundaryProps: enableErrorBoundary ? { enabled: true, ...errorBoundaryProps } : { enabled: false, ...errorBoundaryProps },
5963
+ children: content
5964
+ }
5965
+ );
5966
+ } else {
5967
+ if (enableErrorBoundary) {
5968
+ content = /* @__PURE__ */ jsx50(AppErrorBoundary, { enabled: true, ...errorBoundaryProps, children: content });
5969
+ }
5970
+ if (enableLogger) {
5971
+ content = /* @__PURE__ */ jsx50(LoggerProvider, { enabled: true, overlayEnabled: true, ...loggerProps, children: content });
5972
+ }
4399
5973
  }
4400
5974
  if (enableNavigation) {
4401
- content = /* @__PURE__ */ jsx44(NavigationProvider, { ...navigationProps, children: content });
5975
+ content = /* @__PURE__ */ jsx50(NavigationProvider, { ...navigationProps, children: content });
4402
5976
  }
4403
5977
  if (enableTheme) {
4404
- content = /* @__PURE__ */ jsx44(ThemeProvider, { light: lightTheme, dark: darkTheme, defaultDark, isDark, children: /* @__PURE__ */ jsxs19(Fragment4, { children: [
4405
- enableStatusBar && /* @__PURE__ */ jsx44(AppStatusBar, { testID: "status-bar", ...statusBarProps }),
5978
+ content = /* @__PURE__ */ jsx50(ThemeProvider, { light: lightTheme, dark: darkTheme, defaultDark, isDark, children: /* @__PURE__ */ jsxs23(Fragment4, { children: [
5979
+ enableStatusBar && /* @__PURE__ */ jsx50(AppStatusBar, { testID: "status-bar", ...statusBarProps }),
4406
5980
  content
4407
5981
  ] }) });
4408
5982
  }
4409
5983
  if (enableSafeArea) {
4410
- content = /* @__PURE__ */ jsx44(SafeAreaProvider, { children: content });
5984
+ content = /* @__PURE__ */ jsx50(SafeAreaProvider, { children: content });
4411
5985
  }
4412
- return /* @__PURE__ */ jsx44(Fragment4, { children: content });
5986
+ return /* @__PURE__ */ jsx50(Fragment4, { children: content });
4413
5987
  }
4414
5988
 
4415
5989
  // src/overlay/loading/hooks.ts
@@ -4427,8 +6001,29 @@ function useAlert() {
4427
6001
  return useAlertContext();
4428
6002
  }
4429
6003
 
6004
+ // src/overlay/logger/hooks.ts
6005
+ import { useMemo as useMemo13 } from "react";
6006
+ function useLogger(namespace) {
6007
+ const logger = useLoggerContext();
6008
+ return useMemo13(
6009
+ () => ({
6010
+ entries: logger.entries,
6011
+ enabled: logger.enabled,
6012
+ level: logger.level,
6013
+ clear: logger.clear,
6014
+ namespace,
6015
+ log: (level, message, data) => logger.log(level, message, data, namespace),
6016
+ debug: (message, data) => logger.debug(message, data, namespace),
6017
+ info: (message, data) => logger.info(message, data, namespace),
6018
+ warn: (message, data) => logger.warn(message, data, namespace),
6019
+ error: (message, data) => logger.error(message, data, namespace)
6020
+ }),
6021
+ [logger, namespace]
6022
+ );
6023
+ }
6024
+
4430
6025
  // src/navigation/components/AppHeader.tsx
4431
- import { Fragment as Fragment5, jsx as jsx45, jsxs as jsxs20 } from "nativewind/jsx-runtime";
6026
+ import { Fragment as Fragment5, jsx as jsx51, jsxs as jsxs24 } from "nativewind/jsx-runtime";
4432
6027
  function AppHeader({
4433
6028
  title,
4434
6029
  subtitle,
@@ -4442,9 +6037,9 @@ function AppHeader({
4442
6037
  const colors = useThemeColors();
4443
6038
  const insets = useSafeAreaInsets3();
4444
6039
  const backgroundColor = transparent ? "transparent" : colors.card;
4445
- return /* @__PURE__ */ jsxs20(Fragment5, { children: [
4446
- /* @__PURE__ */ jsx45(AppFocusedStatusBar, { translucent: true, backgroundColor: "transparent" }),
4447
- /* @__PURE__ */ jsx45(
6040
+ return /* @__PURE__ */ jsxs24(Fragment5, { children: [
6041
+ /* @__PURE__ */ jsx51(AppFocusedStatusBar, { translucent: true, backgroundColor: "transparent" }),
6042
+ /* @__PURE__ */ jsx51(
4448
6043
  AppView,
4449
6044
  {
4450
6045
  style: [
@@ -4454,39 +6049,39 @@ function AppHeader({
4454
6049
  },
4455
6050
  style
4456
6051
  ],
4457
- children: /* @__PURE__ */ jsxs20(AppView, { row: true, items: "center", px: 4, style: styles16.container, children: [
4458
- /* @__PURE__ */ jsx45(AppView, { style: [styles16.sideContainer, styles16.leftContainer], children: leftIcon && /* @__PURE__ */ jsx45(AppPressable, { onPress: onLeftPress, style: styles16.iconButton, children: /* @__PURE__ */ jsx45(Icon, { name: leftIcon, size: 24, color: colors.text }) }) }),
4459
- /* @__PURE__ */ jsxs20(AppView, { style: styles16.centerContainer, children: [
4460
- title && /* @__PURE__ */ jsx45(
6052
+ children: /* @__PURE__ */ jsxs24(AppView, { row: true, items: "center", px: 4, style: styles17.container, children: [
6053
+ /* @__PURE__ */ jsx51(AppView, { style: [styles17.sideContainer, styles17.leftContainer], children: leftIcon && /* @__PURE__ */ jsx51(AppPressable, { onPress: onLeftPress, style: styles17.iconButton, children: /* @__PURE__ */ jsx51(Icon, { name: leftIcon, size: 24, color: colors.text }) }) }),
6054
+ /* @__PURE__ */ jsxs24(AppView, { style: styles17.centerContainer, children: [
6055
+ title && /* @__PURE__ */ jsx51(
4461
6056
  AppText,
4462
6057
  {
4463
6058
  size: "lg",
4464
6059
  weight: "semibold",
4465
- style: [styles16.title, { color: colors.text }],
6060
+ style: [styles17.title, { color: colors.text }],
4466
6061
  numberOfLines: 1,
4467
6062
  children: title
4468
6063
  }
4469
6064
  ),
4470
- subtitle && /* @__PURE__ */ jsx45(
6065
+ subtitle && /* @__PURE__ */ jsx51(
4471
6066
  AppText,
4472
6067
  {
4473
6068
  size: "xs",
4474
- style: [styles16.subtitle, { color: colors.textMuted }],
6069
+ style: [styles17.subtitle, { color: colors.textMuted }],
4475
6070
  numberOfLines: 1,
4476
6071
  children: subtitle
4477
6072
  }
4478
6073
  )
4479
6074
  ] }),
4480
- /* @__PURE__ */ jsx45(AppView, { row: true, items: "center", style: [styles16.sideContainer, styles16.rightContainer], children: rightIcons.map((icon, index) => /* @__PURE__ */ jsx45(AppPressable, { onPress: icon.onPress, style: styles16.iconButton, children: /* @__PURE__ */ jsxs20(AppView, { children: [
4481
- /* @__PURE__ */ jsx45(Icon, { name: icon.icon, size: 24, color: colors.text }),
4482
- icon.badge ? /* @__PURE__ */ jsx45(AppView, { style: styles16.badge, children: /* @__PURE__ */ jsx45(AppText, { size: "xs", color: "white", style: styles16.badgeText, children: icon.badge > 99 ? "99+" : icon.badge }) }) : null
6075
+ /* @__PURE__ */ jsx51(AppView, { row: true, items: "center", style: [styles17.sideContainer, styles17.rightContainer], children: rightIcons.map((icon, index) => /* @__PURE__ */ jsx51(AppPressable, { onPress: icon.onPress, style: styles17.iconButton, children: /* @__PURE__ */ jsxs24(AppView, { children: [
6076
+ /* @__PURE__ */ jsx51(Icon, { name: icon.icon, size: 24, color: colors.text }),
6077
+ icon.badge ? /* @__PURE__ */ jsx51(AppView, { style: styles17.badge, children: /* @__PURE__ */ jsx51(AppText, { size: "xs", color: "white", style: styles17.badgeText, children: icon.badge > 99 ? "99+" : icon.badge }) }) : null
4483
6078
  ] }) }, index)) })
4484
6079
  ] })
4485
6080
  }
4486
6081
  )
4487
6082
  ] });
4488
6083
  }
4489
- var styles16 = StyleSheet17.create({
6084
+ var styles17 = StyleSheet18.create({
4490
6085
  container: {
4491
6086
  height: 44
4492
6087
  // iOS 标准导航栏高度
@@ -4537,9 +6132,9 @@ var styles16 = StyleSheet17.create({
4537
6132
  });
4538
6133
 
4539
6134
  // src/navigation/components/DrawerContent.tsx
4540
- import { View as View12, TouchableOpacity as TouchableOpacity8, StyleSheet as StyleSheet18 } from "react-native";
6135
+ import { View as View12, TouchableOpacity as TouchableOpacity9, StyleSheet as StyleSheet19 } from "react-native";
4541
6136
  import { DrawerContentScrollView } from "@react-navigation/drawer";
4542
- import { jsx as jsx46, jsxs as jsxs21 } from "nativewind/jsx-runtime";
6137
+ import { jsx as jsx52, jsxs as jsxs25 } from "nativewind/jsx-runtime";
4543
6138
  function DrawerContent({
4544
6139
  state,
4545
6140
  descriptors,
@@ -4568,20 +6163,20 @@ function DrawerContent({
4568
6163
  badge: options.tabBarBadge
4569
6164
  };
4570
6165
  });
4571
- return /* @__PURE__ */ jsxs21(DrawerContentScrollView, { style: [styles17.container, { backgroundColor }], children: [
4572
- header && /* @__PURE__ */ jsx46(View12, { style: [styles17.header, { borderBottomColor: dividerColor }], children: header }),
4573
- /* @__PURE__ */ jsx46(AppView, { className: "py-2", children: drawerItems.map((item) => {
6166
+ return /* @__PURE__ */ jsxs25(DrawerContentScrollView, { style: [styles18.container, { backgroundColor }], children: [
6167
+ header && /* @__PURE__ */ jsx52(View12, { style: [styles18.header, { borderBottomColor: dividerColor }], children: header }),
6168
+ /* @__PURE__ */ jsx52(AppView, { className: "py-2", children: drawerItems.map((item) => {
4574
6169
  const isFocused = state.routes[state.index].name === item.name;
4575
6170
  const onPress = () => {
4576
6171
  navigation.navigate(item.name);
4577
6172
  };
4578
- return /* @__PURE__ */ jsxs21(
4579
- TouchableOpacity8,
6173
+ return /* @__PURE__ */ jsxs25(
6174
+ TouchableOpacity9,
4580
6175
  {
4581
6176
  onPress,
4582
- style: [styles17.item, isFocused && { backgroundColor: activeBgColor }],
6177
+ style: [styles18.item, isFocused && { backgroundColor: activeBgColor }],
4583
6178
  children: [
4584
- item.icon && /* @__PURE__ */ jsx46(View12, { style: styles17.iconContainer, children: /* @__PURE__ */ jsx46(
6179
+ item.icon && /* @__PURE__ */ jsx52(View12, { style: styles18.iconContainer, children: /* @__PURE__ */ jsx52(
4585
6180
  Icon,
4586
6181
  {
4587
6182
  name: item.icon,
@@ -4589,28 +6184,28 @@ function DrawerContent({
4589
6184
  color: isFocused ? activeColor : inactiveColor
4590
6185
  }
4591
6186
  ) }),
4592
- /* @__PURE__ */ jsx46(
6187
+ /* @__PURE__ */ jsx52(
4593
6188
  AppText,
4594
6189
  {
4595
6190
  style: [
4596
- styles17.label,
6191
+ styles18.label,
4597
6192
  { color: isFocused ? activeColor : inactiveColor },
4598
- isFocused && styles17.activeLabel
6193
+ isFocused && styles18.activeLabel
4599
6194
  ],
4600
6195
  numberOfLines: 1,
4601
6196
  children: item.label
4602
6197
  }
4603
6198
  ),
4604
- item.badge != null && /* @__PURE__ */ jsx46(View12, { style: [styles17.badge, { backgroundColor: activeColor }], children: /* @__PURE__ */ jsx46(AppText, { style: styles17.badgeText, children: typeof item.badge === "number" && item.badge > 99 ? "99+" : item.badge }) })
6199
+ item.badge != null && /* @__PURE__ */ jsx52(View12, { style: [styles18.badge, { backgroundColor: activeColor }], children: /* @__PURE__ */ jsx52(AppText, { style: styles18.badgeText, children: typeof item.badge === "number" && item.badge > 99 ? "99+" : item.badge }) })
4605
6200
  ]
4606
6201
  },
4607
6202
  item.name
4608
6203
  );
4609
6204
  }) }),
4610
- footer && /* @__PURE__ */ jsx46(View12, { style: [styles17.footer, { borderTopColor: dividerColor }], children: footer })
6205
+ footer && /* @__PURE__ */ jsx52(View12, { style: [styles18.footer, { borderTopColor: dividerColor }], children: footer })
4611
6206
  ] });
4612
6207
  }
4613
- var styles17 = StyleSheet18.create({
6208
+ var styles18 = StyleSheet19.create({
4614
6209
  container: {
4615
6210
  flex: 1
4616
6211
  },
@@ -4660,7 +6255,7 @@ var styles17 = StyleSheet18.create({
4660
6255
  });
4661
6256
 
4662
6257
  // src/navigation/hooks/useNavigation.ts
4663
- import { useEffect as useEffect11 } from "react";
6258
+ import { useEffect as useEffect22 } from "react";
4664
6259
  import { useNavigation as useRNNavigation } from "@react-navigation/native";
4665
6260
  function useNavigation() {
4666
6261
  return useRNNavigation();
@@ -4676,7 +6271,7 @@ function useDrawerNavigation() {
4676
6271
  }
4677
6272
  function useBackHandler(handler) {
4678
6273
  const navigation = useRNNavigation();
4679
- useEffect11(() => {
6274
+ useEffect22(() => {
4680
6275
  const unsubscribe = navigation.addListener("beforeRemove", (e) => {
4681
6276
  if (!handler()) {
4682
6277
  e.preventDefault();
@@ -4714,6 +6309,7 @@ export {
4714
6309
  ActionIcons,
4715
6310
  Alert,
4716
6311
  AppButton,
6312
+ AppErrorBoundary,
4717
6313
  AppFocusedStatusBar,
4718
6314
  AppHeader,
4719
6315
  AppImage,
@@ -4741,13 +6337,18 @@ export {
4741
6337
  FormItem,
4742
6338
  GradientView,
4743
6339
  Icon,
6340
+ KeyboardDismissView,
6341
+ LOG_LEVEL_WEIGHT,
4744
6342
  Loading,
6343
+ LogOverlay,
6344
+ LoggerProvider,
4745
6345
  MemoryStorage,
4746
6346
  NavigationContainer2 as NavigationContainer,
4747
6347
  NavigationIcons,
4748
6348
  NavigationProvider,
4749
6349
  OverlayProvider,
4750
6350
  PageDrawer,
6351
+ Picker,
4751
6352
  Progress,
4752
6353
  Radio,
4753
6354
  RadioGroup,
@@ -4769,7 +6370,10 @@ export {
4769
6370
  clsx,
4770
6371
  cn,
4771
6372
  createAPI,
6373
+ createApiLoggerTransport,
6374
+ createConsoleTransport,
4772
6375
  createDrawerScreens,
6376
+ createLogEntry,
4773
6377
  createNavigationTheme,
4774
6378
  createStackScreens,
4775
6379
  createTabScreens,
@@ -4778,10 +6382,12 @@ export {
4778
6382
  enhanceError,
4779
6383
  formatCurrency,
4780
6384
  formatDate,
6385
+ formatLogTime,
4781
6386
  formatNumber,
4782
6387
  formatPercent,
4783
6388
  formatRelativeTime,
4784
6389
  generateColorPalette,
6390
+ getGlobalLogger,
4785
6391
  getThemeColors,
4786
6392
  getValidationErrors,
4787
6393
  hexToRgb,
@@ -4789,11 +6395,16 @@ export {
4789
6395
  isValidEmail,
4790
6396
  isValidPhone,
4791
6397
  mapHttpStatus,
6398
+ normalizeConsoleArgs,
4792
6399
  omit,
4793
6400
  pick,
4794
6401
  rgbToHex,
6402
+ serializeLogEntries,
6403
+ setGlobalLogger,
6404
+ shouldLog,
4795
6405
  slugify,
4796
6406
  storage,
6407
+ stringifyLogData,
4797
6408
  truncate,
4798
6409
  twMerge,
4799
6410
  useAlert,
@@ -4808,6 +6419,7 @@ export {
4808
6419
  useIsFocused2 as useIsFocused,
4809
6420
  useKeyboard,
4810
6421
  useLoading,
6422
+ useLogger,
4811
6423
  useMemoizedFn,
4812
6424
  useMutation,
4813
6425
  useNavigation,