lynx-console 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/dist/assets/src/components/BottomSheet.css.ts.vanilla-D-1A77Ik.css +83 -0
  2. package/dist/assets/src/components/ConsolePanel.css.ts.vanilla-B3avfSlI.css +246 -0
  3. package/dist/assets/src/components/FloatingButton.css.ts.vanilla-rPj35oLW.css +55 -0
  4. package/dist/assets/src/components/NetworkPanel.css.ts.vanilla-DFMduT0T.css +247 -0
  5. package/dist/assets/src/components/PerformancePanel.css.ts.vanilla-D35LuXlW.css +216 -0
  6. package/dist/assets/src/components/Tabs.css.ts.vanilla-DD7L2oXt.css +50 -0
  7. package/dist/index.cjs +1058 -0
  8. package/dist/index.css +466 -0
  9. package/dist/index.css.map +1 -0
  10. package/dist/index.d.cts +17 -0
  11. package/dist/index.d.cts.map +1 -0
  12. package/dist/index.d.mts +17 -0
  13. package/dist/index.d.mts.map +1 -0
  14. package/dist/index.mjs +1059 -0
  15. package/dist/index.mjs.map +1 -0
  16. package/dist/setup.cjs +377 -0
  17. package/dist/setup.d.cts +15 -0
  18. package/dist/setup.d.cts.map +1 -0
  19. package/dist/setup.d.mts +15 -0
  20. package/dist/setup.d.mts.map +1 -0
  21. package/dist/setup.mjs +374 -0
  22. package/dist/setup.mjs.map +1 -0
  23. package/package.json +51 -0
  24. package/src/components/BottomSheet.css.ts +93 -0
  25. package/src/components/BottomSheet.tsx +142 -0
  26. package/src/components/ConsolePanel.css.ts +261 -0
  27. package/src/components/ConsolePanel.tsx +41 -0
  28. package/src/components/FloatingButton.css.ts +62 -0
  29. package/src/components/FloatingButton.tsx +37 -0
  30. package/src/components/LogPanel.tsx +241 -0
  31. package/src/components/NetworkDetailSection.tsx +42 -0
  32. package/src/components/NetworkPanel.css.ts +280 -0
  33. package/src/components/NetworkPanel.tsx +222 -0
  34. package/src/components/PerformancePanel.css.ts +224 -0
  35. package/src/components/PerformancePanel.tsx +209 -0
  36. package/src/components/Tabs.css.ts +66 -0
  37. package/src/components/Tabs.tsx +81 -0
  38. package/src/globals.d.ts +9 -0
  39. package/src/hooks/index.ts +3 -0
  40. package/src/hooks/useConsole.ts +35 -0
  41. package/src/hooks/useNetwork.ts +36 -0
  42. package/src/hooks/usePerformance.ts +39 -0
  43. package/src/index.tsx +110 -0
  44. package/src/setup/_setupMainThreadConsole.ts +80 -0
  45. package/src/setup/index.ts +4 -0
  46. package/src/setup/setupLogMonitor.ts +78 -0
  47. package/src/setup/setupMainThreadConsole.ts +34 -0
  48. package/src/setup/setupNetworkMonitor.ts +247 -0
  49. package/src/setup/setupPerformanceMonitor.ts +70 -0
  50. package/src/shared/ensureConsoleStructure.ts +20 -0
  51. package/src/styles/getDimensionValue.ts +7 -0
  52. package/src/styles/global.css.ts +10 -0
  53. package/src/styles/tokens.json +80 -0
  54. package/src/styles/typography.ts +25 -0
  55. package/src/styles/vars/color.ts +228 -0
  56. package/src/styles/vars/dimension.ts +79 -0
  57. package/src/styles/vars/index.css +463 -0
  58. package/src/styles/vars/index.ts +22 -0
  59. package/src/styles/vars/radius.ts +12 -0
  60. package/src/types.ts +96 -0
@@ -0,0 +1,66 @@
1
+ import { style } from "@vanilla-extract/css";
2
+ import { recipe } from "@vanilla-extract/recipes";
3
+ import { typography } from "../styles/typography";
4
+ import { vars } from "../styles/vars";
5
+
6
+ export const tabs = style({
7
+ flex: 1,
8
+ display: "flex",
9
+ flexDirection: "column",
10
+ });
11
+
12
+ export const tabHeader = style({
13
+ display: "flex",
14
+ boxShadow: `inset 0 -1px 0 0 ${vars.$color.stroke.neutralSubtle}`,
15
+ });
16
+
17
+ export const tabTriggerButton = style({
18
+ flex: 1,
19
+ color: vars.$color.fg.neutralSubtle,
20
+ display: "flex",
21
+ justifyContent: "center",
22
+ alignItems: "center",
23
+ padding: "12px 0 10px 0",
24
+ position: "relative",
25
+ });
26
+
27
+ export const tabTriggerButtonText = recipe({
28
+ base: {
29
+ ...typography("t5", "bold"),
30
+ color: vars.$color.fg.neutralSubtle,
31
+ },
32
+ variants: {
33
+ active: {
34
+ true: {
35
+ color: vars.$color.fg.neutral,
36
+ },
37
+ },
38
+ },
39
+ });
40
+
41
+ export const tabTriggerIndicator = style({
42
+ position: "absolute",
43
+ bottom: 0,
44
+ left: 0,
45
+ padding: "0 16px",
46
+ width: "100%",
47
+ transition: "200ms",
48
+ transitionTimingFunction: "cubic-bezier(.35, 0, .35, 1)",
49
+ });
50
+
51
+ export const tabTriggerIndicatorLine = style({
52
+ backgroundColor: vars.$color.fg.neutral,
53
+ width: "100%",
54
+ height: "2px",
55
+ });
56
+
57
+ export const tabContents = style({
58
+ flex: 1,
59
+ width: "100%",
60
+ });
61
+
62
+ export const tabContent = style({
63
+ width: "100%",
64
+ display: "flex",
65
+ flexDirection: "column",
66
+ });
@@ -0,0 +1,81 @@
1
+ import { type ReactNode, useRef, useState } from "@lynx-js/react";
2
+ import type { ListSnapEvent, NodesRef } from "@lynx-js/types";
3
+ import * as css from "./Tabs.css";
4
+
5
+ type TabsProps = {
6
+ items: Array<{
7
+ key: string;
8
+ label: string;
9
+ renderContent: () => ReactNode;
10
+ }>;
11
+ };
12
+
13
+ export default function Tabs(props: TabsProps) {
14
+ const tabContentsRef = useRef<NodesRef>(null);
15
+ const [activeIndex, setActiveIndex] = useState(0);
16
+
17
+ return (
18
+ <view className={css.tabs}>
19
+ <view className={css.tabHeader}>
20
+ {props.items.map((item, i) => (
21
+ <view
22
+ key={item.key}
23
+ className={css.tabTriggerButton}
24
+ bindtap={() => {
25
+ setActiveIndex(i);
26
+
27
+ tabContentsRef.current
28
+ ?.invoke({
29
+ method: "scrollToPosition",
30
+ params: {
31
+ position: i,
32
+ smooth: true,
33
+ },
34
+ })
35
+ .exec();
36
+ }}
37
+ >
38
+ <text
39
+ className={css.tabTriggerButtonText({
40
+ active: i === activeIndex,
41
+ })}
42
+ >
43
+ {item.label}
44
+ </text>
45
+ {i === 0 && (
46
+ <view
47
+ className={css.tabTriggerIndicator}
48
+ style={{ transform: `translateX(${activeIndex * 100}%)` }}
49
+ >
50
+ <view className={css.tabTriggerIndicatorLine} />
51
+ </view>
52
+ )}
53
+ </view>
54
+ ))}
55
+ </view>
56
+
57
+ <list
58
+ ref={tabContentsRef}
59
+ className={css.tabContents}
60
+ scroll-orientation="horizontal"
61
+ item-snap={{ factor: 0, offset: 0 }}
62
+ bindsnap={(e: ListSnapEvent) => {
63
+ setActiveIndex(e.detail.position);
64
+ }}
65
+ bounces={false}
66
+ preload-buffer-count={props.items.length}
67
+ >
68
+ {props.items.map((item) => (
69
+ <list-item
70
+ key={item.key}
71
+ item-key={item.key}
72
+ recyclable={false}
73
+ className={css.tabContent}
74
+ >
75
+ {item.renderContent()}
76
+ </list-item>
77
+ ))}
78
+ </list>
79
+ </view>
80
+ );
81
+ }
@@ -0,0 +1,9 @@
1
+ declare module "*.css" {
2
+ const content: string;
3
+ export default content;
4
+ }
5
+
6
+ declare module "*.css.ts" {
7
+ const content: Record<string, string>;
8
+ export default content;
9
+ }
@@ -0,0 +1,3 @@
1
+ export { useConsole } from "./useConsole";
2
+ export { useNetwork } from "./useNetwork";
3
+ export { usePerformance } from "./usePerformance";
@@ -0,0 +1,35 @@
1
+ import { useEffect, useState } from "@lynx-js/react";
2
+ import type { LogEntry } from "../types";
3
+
4
+ export const useConsole = () => {
5
+ const [logs, setLogs] = useState<LogEntry[]>([]);
6
+
7
+ useEffect(() => {
8
+ if (typeof globalThis.__LYNX_CONSOLE__?.state?.logs === "undefined") {
9
+ console.warn("[LynxConsole] Log monitoring not initialized");
10
+ return;
11
+ }
12
+
13
+ const state = globalThis.__LYNX_CONSOLE__.state;
14
+
15
+ setLogs([...(state.logs ?? [])]);
16
+
17
+ const updateLogs = (_entry: LogEntry) => {
18
+ setLogs([...(state.logs ?? [])]);
19
+ };
20
+
21
+ const unsubscribe = state.logSubscribe?.(updateLogs);
22
+
23
+ return unsubscribe;
24
+ }, []);
25
+
26
+ const clearLogs = () => {
27
+ if (typeof globalThis.__LYNX_CONSOLE__?.state?.logs !== "undefined") {
28
+ const state = globalThis.__LYNX_CONSOLE__.state;
29
+ state.logs = [];
30
+ setLogs([]);
31
+ }
32
+ };
33
+
34
+ return { logs, clearLogs };
35
+ };
@@ -0,0 +1,36 @@
1
+ import { useEffect, useState } from "@lynx-js/react";
2
+ import type { NetworkEntry } from "../types";
3
+
4
+ export const useNetwork = () => {
5
+ const [networks, setNetworks] = useState<NetworkEntry[]>([]);
6
+
7
+ useEffect(() => {
8
+ if (typeof globalThis.__LYNX_CONSOLE__?.state?.networks === "undefined") {
9
+ console.warn("[LynxConsole] Network monitoring not initialized");
10
+ return;
11
+ }
12
+
13
+ const state = globalThis.__LYNX_CONSOLE__.state;
14
+
15
+ setNetworks([...(state.networks ?? [])]);
16
+
17
+ const updateNetworks = (_entry: NetworkEntry) => {
18
+ setNetworks([...(state.networks ?? [])]);
19
+ };
20
+
21
+ const unsubscribe = state.subscribeNetwork?.(updateNetworks);
22
+
23
+ return unsubscribe;
24
+ }, []);
25
+
26
+ const clearNetworks = () => {
27
+ if (typeof globalThis.__LYNX_CONSOLE__?.state?.networks !== "undefined") {
28
+ const state = globalThis.__LYNX_CONSOLE__.state;
29
+ state.networks = [];
30
+ state.networksMap?.clear();
31
+ setNetworks([]);
32
+ }
33
+ };
34
+
35
+ return { networks, clearNetworks };
36
+ };
@@ -0,0 +1,39 @@
1
+ import { useEffect, useState } from "@lynx-js/react";
2
+ import type { PerformanceEntryData } from "../types";
3
+
4
+ export const usePerformance = () => {
5
+ const [performances, setPerformances] = useState<PerformanceEntryData[]>([]);
6
+
7
+ useEffect(() => {
8
+ if (
9
+ typeof globalThis.__LYNX_CONSOLE__?.state?.performances === "undefined"
10
+ ) {
11
+ console.warn("[LynxConsole] Performance monitoring not initialized");
12
+ return;
13
+ }
14
+
15
+ const state = globalThis.__LYNX_CONSOLE__.state;
16
+
17
+ setPerformances([...(state.performances ?? [])]);
18
+
19
+ const updatePerformances = (_entry: PerformanceEntryData) => {
20
+ setPerformances([...(state.performances ?? [])]);
21
+ };
22
+
23
+ const unsubscribe = state.subscribePerformance?.(updatePerformances);
24
+
25
+ return unsubscribe;
26
+ }, []);
27
+
28
+ const clearPerformances = () => {
29
+ if (
30
+ typeof globalThis.__LYNX_CONSOLE__?.state?.performances !== "undefined"
31
+ ) {
32
+ const state = globalThis.__LYNX_CONSOLE__.state;
33
+ state.performances = [];
34
+ setPerformances([]);
35
+ }
36
+ };
37
+
38
+ return { performances, clearPerformances };
39
+ };
package/src/index.tsx ADDED
@@ -0,0 +1,110 @@
1
+ import "./styles/vars/index.css";
2
+ import {
3
+ type ForwardedRef,
4
+ forwardRef,
5
+ useImperativeHandle,
6
+ useMemo,
7
+ useState,
8
+ } from "@lynx-js/react";
9
+ import BottomSheet from "./components/BottomSheet.jsx";
10
+ import { ConsolePanel } from "./components/ConsolePanel.jsx";
11
+ import * as floatingButtonCss from "./components/FloatingButton.css";
12
+ import { FloatingButton } from "./components/FloatingButton.jsx";
13
+ import { usePerformance } from "./hooks/usePerformance";
14
+
15
+ export interface LynxConsoleHandle {
16
+ open: () => void;
17
+ close: () => void;
18
+ isOpen: () => boolean;
19
+ }
20
+
21
+ export interface LynxConsoleProps {
22
+ theme?: "light" | "dark";
23
+ safeAreaInsetBottom?: string;
24
+ }
25
+
26
+ interface FcpMetric {
27
+ name: string;
28
+ duration: number;
29
+ }
30
+
31
+ interface MetricFcpEntry {
32
+ totalFcp?: FcpMetric;
33
+ lynxFcp?: FcpMetric;
34
+ fcp?: FcpMetric;
35
+ }
36
+
37
+ const LynxConsole = forwardRef<LynxConsoleHandle, LynxConsoleProps>(
38
+ (
39
+ { theme = "light", safeAreaInsetBottom = "50px" },
40
+ ref: ForwardedRef<LynxConsoleHandle>,
41
+ ) => {
42
+ const [isOpen, setIsOpen] = useState(false);
43
+ const [shouldClose, setShouldClose] = useState(false);
44
+ const { performances } = usePerformance();
45
+
46
+ const latestFcp = useMemo(() => {
47
+ for (let i = performances.length - 1; i >= 0; i--) {
48
+ const perf = performances[i];
49
+ if (perf && perf.entryType === "metric" && perf.name === "fcp") {
50
+ const metricEntry = perf.rawEntry as MetricFcpEntry | undefined;
51
+ // totalFcp를 먼저 시도하고, 없으면 lynxFcp 반환
52
+ if (metricEntry?.totalFcp?.duration !== undefined) {
53
+ return metricEntry.totalFcp;
54
+ }
55
+ if (metricEntry?.lynxFcp?.duration !== undefined) {
56
+ return metricEntry.lynxFcp;
57
+ }
58
+ }
59
+ }
60
+ return undefined;
61
+ }, [performances]);
62
+
63
+ useImperativeHandle(ref, () => ({
64
+ open: () => {
65
+ setIsOpen(true);
66
+ setShouldClose(false);
67
+ },
68
+ close: () => {
69
+ setShouldClose(true);
70
+ },
71
+ isOpen: () => isOpen,
72
+ }));
73
+
74
+ const handleOpenBottomSheet = () => {
75
+ setIsOpen(true);
76
+ setShouldClose(false);
77
+ };
78
+
79
+ const handleCloseBottomSheet = () => {
80
+ setIsOpen(false);
81
+ setShouldClose(false);
82
+ };
83
+
84
+ const themeClass = `data-seed-color-mode__${theme}-only`;
85
+
86
+ return (
87
+ <view className={themeClass}>
88
+ <FloatingButton bindtap={handleOpenBottomSheet} isVisible={!isOpen}>
89
+ <text className={floatingButtonCss.title}>LynxConsole</text>
90
+ <text className={floatingButtonCss.subtitle}>
91
+ {`${latestFcp?.name ?? "FCP"}: ${latestFcp?.duration ? latestFcp.duration.toFixed(2) : "--"}ms`}
92
+ </text>
93
+ </FloatingButton>
94
+ {isOpen && (
95
+ <BottomSheet
96
+ isOpen={isOpen}
97
+ shouldClose={shouldClose}
98
+ onClose={handleCloseBottomSheet}
99
+ title="Lynx Console"
100
+ safeAreaInsetBottom={safeAreaInsetBottom}
101
+ >
102
+ <ConsolePanel />
103
+ </BottomSheet>
104
+ )}
105
+ </view>
106
+ );
107
+ },
108
+ );
109
+
110
+ export default LynxConsole;
@@ -0,0 +1,80 @@
1
+ import { runOnBackground } from "@lynx-js/react";
2
+ import type { LogEntry, LogLevel } from "../types";
3
+
4
+ const _setupMainThreadConsole = (): void => {
5
+ "main thread";
6
+
7
+ //IMPORTANT: do not use external functions in main thread
8
+ if (!globalThis.__LYNX_CONSOLE__) globalThis.__LYNX_CONSOLE__ = {};
9
+ const lynxConsole = globalThis.__LYNX_CONSOLE__;
10
+
11
+ if (lynxConsole.mainThreadInitialized) {
12
+ console.warn("[LynxConsole] Main thread console already initialized");
13
+ return;
14
+ }
15
+
16
+ const LOG_METHODS: LogLevel[] = ["log", "warn", "error", "info"];
17
+ const LOG_ID_PREFIX = "main-thread";
18
+
19
+ const serializeArgs = (args: unknown[]): unknown[] => {
20
+ return args.map((arg) => {
21
+ try {
22
+ JSON.stringify(arg);
23
+ return arg;
24
+ } catch {
25
+ return String(arg);
26
+ }
27
+ });
28
+ };
29
+
30
+ const generateLogId = (): string => {
31
+ return `${LOG_ID_PREFIX}-${Date.now()}-${Math.random()}`;
32
+ };
33
+
34
+ // Main Thread에서 Background Thread로 로그 전송하는 함수
35
+ const sendLogToBackground = runOnBackground((entry: LogEntry): void => {
36
+ const state = globalThis.__LYNX_CONSOLE__?.state;
37
+ if (!state) return;
38
+
39
+ state.logs?.push(entry);
40
+ state.logListeners?.forEach((listener) => {
41
+ listener(entry);
42
+ });
43
+ });
44
+
45
+ const originalConsole = globalThis.console;
46
+
47
+ const originalMethods = {
48
+ log: originalConsole.log.bind(originalConsole),
49
+ warn: originalConsole.warn.bind(originalConsole),
50
+ error: originalConsole.error.bind(originalConsole),
51
+ info: originalConsole.info.bind(originalConsole),
52
+ };
53
+
54
+ // Main Thread console 오버라이드
55
+ LOG_METHODS.forEach((method) => {
56
+ const original = originalMethods[method];
57
+ originalConsole[method] = ((...args: unknown[]) => {
58
+ // 원본 console 호출
59
+ original(...args);
60
+
61
+ const serializedArgs = serializeArgs(args);
62
+ const timestamp = Date.now();
63
+ const id = generateLogId();
64
+
65
+ sendLogToBackground({
66
+ id,
67
+ level: method,
68
+ message: "",
69
+ timestamp,
70
+ args: serializedArgs,
71
+ });
72
+ }).bind(originalConsole);
73
+ });
74
+
75
+ lynxConsole.mainThreadInitialized = true;
76
+
77
+ originalConsole.log("[LynxConsole] ✅ Main thread console initialized");
78
+ };
79
+
80
+ export default _setupMainThreadConsole;
@@ -0,0 +1,4 @@
1
+ export { initLogMonitor } from "./setupLogMonitor";
2
+ export { initMainThreadConsole } from "./setupMainThreadConsole";
3
+ export { initNetworkMonitor } from "./setupNetworkMonitor";
4
+ export { initPerformanceMonitor } from "./setupPerformanceMonitor";
@@ -0,0 +1,78 @@
1
+ import { ensureConsoleStructure } from "../shared/ensureConsoleStructure";
2
+ import type { LogEntry, LogLevel } from "../types";
3
+
4
+ type LogListener = (entry: LogEntry) => void;
5
+
6
+ const LOG_METHODS: LogLevel[] = ["log", "warn", "error", "info"];
7
+ const LOG_ID_PREFIX = "background-thread";
8
+
9
+ const generateLogId = (): string => {
10
+ return `${LOG_ID_PREFIX}-${Date.now()}-${Math.random()}`;
11
+ };
12
+
13
+ const createLogEntry = (method: LogLevel, args: unknown[]): LogEntry => {
14
+ return {
15
+ id: generateLogId(),
16
+ level: method,
17
+ message: "",
18
+ timestamp: Date.now(),
19
+ args,
20
+ };
21
+ };
22
+
23
+ const addLogEntry = (entry: LogEntry): void => {
24
+ const state = globalThis.__LYNX_CONSOLE__?.state;
25
+ if (!state?.logs || !state?.logListeners) {
26
+ console.error(
27
+ "[LynxConsole] Cannot add log entry: Log monitor not initialized. Call initLogMonitor() first.",
28
+ );
29
+ return;
30
+ }
31
+
32
+ state.logs.push(entry);
33
+ state.logListeners.forEach((listener) => {
34
+ listener(entry);
35
+ });
36
+ };
37
+
38
+ // Background Thread: Log monitoring 초기화
39
+ export const initLogMonitor = () => {
40
+ "background only";
41
+
42
+ const { lynxConsole, state } = ensureConsoleStructure();
43
+
44
+ if (lynxConsole.originalConsole) {
45
+ console.warn("[LynxConsole] Log monitor already initialized");
46
+ return;
47
+ }
48
+
49
+ const originalConsole = globalThis.console;
50
+ lynxConsole.originalConsole = {
51
+ log: originalConsole.log.bind(originalConsole),
52
+ warn: originalConsole.warn.bind(originalConsole),
53
+ error: originalConsole.error.bind(originalConsole),
54
+ info: originalConsole.info.bind(originalConsole),
55
+ };
56
+
57
+ state.logs = [];
58
+ state.logListeners = new Set();
59
+ state.logSubscribe = (listener: LogListener) => {
60
+ state.logListeners?.add(listener);
61
+ return () => {
62
+ state.logListeners?.delete(listener);
63
+ };
64
+ };
65
+
66
+ // Background Thread console 오버라이드
67
+ LOG_METHODS.forEach((method) => {
68
+ globalThis.console[method] = ((...args: unknown[]) => {
69
+ lynxConsole.originalConsole?.[method](...args);
70
+ const entry = createLogEntry(method, args);
71
+ addLogEntry(entry);
72
+ }).bind(globalThis.console);
73
+ });
74
+
75
+ lynxConsole.originalConsole?.log(
76
+ "[LynxConsole] ✅ Log monitoring initialized",
77
+ );
78
+ };
@@ -0,0 +1,34 @@
1
+ // both thread;
2
+ import "./_setupMainThreadConsole";
3
+
4
+ import { runOnMainThread } from "@lynx-js/react";
5
+ import { ensureConsoleStructure } from "../shared/ensureConsoleStructure";
6
+ import _setupMainThreadConsole from "./_setupMainThreadConsole";
7
+
8
+ // Main Thread: Console 초기화
9
+
10
+ export const initMainThreadConsole = async (): Promise<void> => {
11
+ "background only";
12
+
13
+ const { lynxConsole, state } = ensureConsoleStructure();
14
+
15
+ if (lynxConsole.mainThreadInitialized) {
16
+ console.error("[LynxConsole] Main thread console already initialized");
17
+ return;
18
+ }
19
+
20
+ if (!state.logs) {
21
+ console.error("[LynxConsole] Background thread console not initialized");
22
+ return;
23
+ }
24
+
25
+ try {
26
+ const setupOnMainThread = runOnMainThread(_setupMainThreadConsole);
27
+ await setupOnMainThread();
28
+ } catch (error) {
29
+ console.error(
30
+ "[LynxConsole] Failed to initialize main thread console:",
31
+ error,
32
+ );
33
+ }
34
+ };