@yargram/react 1.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 (105) hide show
  1. package/.storybook/main.ts +21 -0
  2. package/.storybook/preview.ts +21 -0
  3. package/dist/components/LogWindow/LogEntryRow.d.ts +8 -0
  4. package/dist/components/LogWindow/LogEntryRow.d.ts.map +1 -0
  5. package/dist/components/LogWindow/LogEntryRow.js +14 -0
  6. package/dist/components/LogWindow/LogWindow.d.ts +41 -0
  7. package/dist/components/LogWindow/LogWindow.d.ts.map +1 -0
  8. package/dist/components/LogWindow/LogWindow.js +144 -0
  9. package/dist/components/LogWindow/LogWindow.stories.d.ts +29 -0
  10. package/dist/components/LogWindow/LogWindow.stories.d.ts.map +1 -0
  11. package/dist/components/LogWindow/LogWindow.stories.js +183 -0
  12. package/dist/components/LogWindow/LogWindow.test.d.ts +2 -0
  13. package/dist/components/LogWindow/LogWindow.test.d.ts.map +1 -0
  14. package/dist/components/LogWindow/LogWindow.test.js +61 -0
  15. package/dist/components/LogWindow/LogWindowEscapeDemo.d.ts +12 -0
  16. package/dist/components/LogWindow/LogWindowEscapeDemo.d.ts.map +1 -0
  17. package/dist/components/LogWindow/LogWindowEscapeDemo.js +56 -0
  18. package/dist/components/LogWindow/NetworkEntryRow.d.ts +8 -0
  19. package/dist/components/LogWindow/NetworkEntryRow.d.ts.map +1 -0
  20. package/dist/components/LogWindow/NetworkEntryRow.js +32 -0
  21. package/dist/components/LogWindow/index.d.ts +7 -0
  22. package/dist/components/LogWindow/index.d.ts.map +1 -0
  23. package/dist/components/LogWindow/index.js +4 -0
  24. package/dist/components/LogWindow/types.d.ts +36 -0
  25. package/dist/components/LogWindow/types.d.ts.map +1 -0
  26. package/dist/components/LogWindow/types.js +1 -0
  27. package/dist/components/LoginWindow/LoginForm.d.ts +11 -0
  28. package/dist/components/LoginWindow/LoginForm.d.ts.map +1 -0
  29. package/dist/components/LoginWindow/LoginForm.js +28 -0
  30. package/dist/components/LoginWindow/LoginForm.test.d.ts +2 -0
  31. package/dist/components/LoginWindow/LoginForm.test.d.ts.map +1 -0
  32. package/dist/components/LoginWindow/LoginForm.test.js +34 -0
  33. package/dist/components/LoginWindow/LoginWindow.d.ts +15 -0
  34. package/dist/components/LoginWindow/LoginWindow.d.ts.map +1 -0
  35. package/dist/components/LoginWindow/LoginWindow.js +27 -0
  36. package/dist/components/LoginWindow/index.d.ts +3 -0
  37. package/dist/components/LoginWindow/index.d.ts.map +1 -0
  38. package/dist/components/LoginWindow/index.js +1 -0
  39. package/dist/contexts/ApiContext.d.ts +35 -0
  40. package/dist/contexts/ApiContext.d.ts.map +1 -0
  41. package/dist/contexts/ApiContext.js +82 -0
  42. package/dist/contexts/ApiContext.test.d.ts +2 -0
  43. package/dist/contexts/ApiContext.test.d.ts.map +1 -0
  44. package/dist/contexts/ApiContext.test.js +45 -0
  45. package/dist/contexts/PrinterContext.d.ts +12 -0
  46. package/dist/contexts/PrinterContext.d.ts.map +1 -0
  47. package/dist/contexts/PrinterContext.js +17 -0
  48. package/dist/contexts/PrinterContext.test.d.ts +2 -0
  49. package/dist/contexts/PrinterContext.test.d.ts.map +1 -0
  50. package/dist/contexts/PrinterContext.test.js +19 -0
  51. package/dist/contexts/YahmanContext.d.ts +69 -0
  52. package/dist/contexts/YahmanContext.d.ts.map +1 -0
  53. package/dist/contexts/YahmanContext.js +414 -0
  54. package/dist/contexts/YahmanContext.stories.d.ts +16 -0
  55. package/dist/contexts/YahmanContext.stories.d.ts.map +1 -0
  56. package/dist/contexts/YahmanContext.stories.js +64 -0
  57. package/dist/contexts/YargramContext.d.ts +69 -0
  58. package/dist/contexts/YargramContext.d.ts.map +1 -0
  59. package/dist/contexts/YargramContext.js +414 -0
  60. package/dist/contexts/YargramContext.stories.d.ts +16 -0
  61. package/dist/contexts/YargramContext.stories.d.ts.map +1 -0
  62. package/dist/contexts/YargramContext.stories.js +64 -0
  63. package/dist/contexts/YargramContext.test.d.ts +2 -0
  64. package/dist/contexts/YargramContext.test.d.ts.map +1 -0
  65. package/dist/contexts/YargramContext.test.js +54 -0
  66. package/dist/hooks/useLogWindowShortcut.d.ts +24 -0
  67. package/dist/hooks/useLogWindowShortcut.d.ts.map +1 -0
  68. package/dist/hooks/useLogWindowShortcut.js +61 -0
  69. package/dist/hooks/useLogWindowShortcut.test.d.ts +2 -0
  70. package/dist/hooks/useLogWindowShortcut.test.d.ts.map +1 -0
  71. package/dist/hooks/useLogWindowShortcut.test.js +93 -0
  72. package/dist/index.d.ts +6 -0
  73. package/dist/index.d.ts.map +1 -0
  74. package/dist/index.js +7 -0
  75. package/dist/test/setup.d.ts +2 -0
  76. package/dist/test/setup.d.ts.map +1 -0
  77. package/dist/test/setup.js +1 -0
  78. package/package.json +49 -0
  79. package/src/components/LogWindow/LogEntryRow.tsx +38 -0
  80. package/src/components/LogWindow/LogWindow.css +614 -0
  81. package/src/components/LogWindow/LogWindow.stories.tsx +206 -0
  82. package/src/components/LogWindow/LogWindow.test.tsx +68 -0
  83. package/src/components/LogWindow/LogWindow.tsx +379 -0
  84. package/src/components/LogWindow/LogWindowEscapeDemo.tsx +100 -0
  85. package/src/components/LogWindow/NetworkEntryRow.tsx +102 -0
  86. package/src/components/LogWindow/index.ts +13 -0
  87. package/src/components/LogWindow/types.ts +40 -0
  88. package/src/components/LoginWindow/LoginForm.test.tsx +38 -0
  89. package/src/components/LoginWindow/LoginForm.tsx +78 -0
  90. package/src/components/LoginWindow/LoginWindow.css +198 -0
  91. package/src/components/LoginWindow/LoginWindow.tsx +90 -0
  92. package/src/components/LoginWindow/index.ts +2 -0
  93. package/src/contexts/ApiContext.test.tsx +68 -0
  94. package/src/contexts/ApiContext.tsx +155 -0
  95. package/src/contexts/PrinterContext.test.tsx +37 -0
  96. package/src/contexts/PrinterContext.tsx +35 -0
  97. package/src/contexts/YargramContext.stories.tsx +148 -0
  98. package/src/contexts/YargramContext.test.tsx +105 -0
  99. package/src/contexts/YargramContext.tsx +676 -0
  100. package/src/hooks/useLogWindowShortcut.test.ts +111 -0
  101. package/src/hooks/useLogWindowShortcut.ts +96 -0
  102. package/src/index.ts +14 -0
  103. package/src/test/setup.ts +1 -0
  104. package/tsconfig.json +16 -0
  105. package/vitest.config.ts +18 -0
@@ -0,0 +1,21 @@
1
+ import type { StorybookConfig } from '@storybook/react-vite';
2
+
3
+ const config: StorybookConfig = {
4
+ stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
5
+ addons: ['@storybook/addon-essentials'],
6
+ framework: {
7
+ name: '@storybook/react-vite',
8
+ options: {},
9
+ },
10
+ async viteFinal(config) {
11
+ return {
12
+ ...config,
13
+ resolve: {
14
+ ...config.resolve,
15
+ dedupe: ['react', 'react-dom', ...(config.resolve?.dedupe ?? [])],
16
+ },
17
+ };
18
+ },
19
+ };
20
+
21
+ export default config;
@@ -0,0 +1,21 @@
1
+ import type { Preview } from '@storybook/react';
2
+
3
+ const preview: Preview = {
4
+ parameters: {
5
+ controls: {
6
+ matchers: {
7
+ color: /(background|color)$/i,
8
+ date: /Date$/i,
9
+ },
10
+ },
11
+ backgrounds: {
12
+ default: 'dark',
13
+ values: [
14
+ { name: 'dark', value: '#1a1a1a' },
15
+ { name: 'light', value: '#f5f5f5' },
16
+ ],
17
+ },
18
+ },
19
+ };
20
+
21
+ export default preview;
@@ -0,0 +1,8 @@
1
+ import type { LogEntry as LogEntryType } from './types';
2
+ import './LogWindow.css';
3
+ type LogEntryRowProps = {
4
+ entry: LogEntryType;
5
+ };
6
+ export declare function LogEntryRow({ entry }: LogEntryRowProps): import("react/jsx-runtime").JSX.Element;
7
+ export {};
8
+ //# sourceMappingURL=LogEntryRow.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LogEntryRow.d.ts","sourceRoot":"","sources":["../../../src/components/LogWindow/LogEntryRow.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,IAAI,YAAY,EAAY,MAAM,SAAS,CAAC;AAClE,OAAO,iBAAiB,CAAC;AAEzB,KAAK,gBAAgB,GAAG;IACtB,KAAK,EAAE,YAAY,CAAC;CACrB,CAAC;AASF,wBAAgB,WAAW,CAAC,EAAE,KAAK,EAAE,EAAE,gBAAgB,2CAqBtD"}
@@ -0,0 +1,14 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Info, AlertTriangle, CircleAlert } from 'lucide-react';
3
+ import './LogWindow.css';
4
+ const levelIcons = {
5
+ info: Info,
6
+ warn: AlertTriangle,
7
+ error: CircleAlert,
8
+ };
9
+ export function LogEntryRow({ entry }) {
10
+ const levelClass = `logWindowEntry${entry.level.charAt(0).toUpperCase() + entry.level.slice(1)}`;
11
+ const iconClass = `logWindowEntryIcon${entry.level.charAt(0).toUpperCase() + entry.level.slice(1)}`;
12
+ const IconComponent = levelIcons[entry.level];
13
+ return (_jsxs("div", { className: `logWindowEntry ${levelClass}`, children: [_jsx("span", { className: `logWindowEntryIcon ${iconClass}`, children: _jsx(IconComponent, { size: 12 }) }), _jsx("span", { className: "logWindowEntryMessage", children: entry.message }), _jsx("span", { className: "logWindowEntrySource", children: entry.source })] }));
14
+ }
@@ -0,0 +1,41 @@
1
+ import type { LogEntry, LogWindowTab, NetworkEntry } from './types';
2
+ import '../LoginWindow/LoginWindow.css';
3
+ import './LogWindow.css';
4
+ export type LogWindowProps = {
5
+ /** 表示するログエントリの配列 */
6
+ entries?: LogEntry[];
7
+ /** Networks タブで表示するネットワークエントリ(REST / GraphQL) */
8
+ networkEntries?: NetworkEntry[];
9
+ /** アクティブなタブ(未指定時は 'logs') */
10
+ defaultTab?: LogWindowTab;
11
+ /** タブ切り替え時のコールバック */
12
+ onTabChange?: (tab: LogWindowTab) => void;
13
+ /** 高さ(CSS 値)。未指定時は max-height: 320px */
14
+ height?: string | number;
15
+ className?: string;
16
+ /** true のときヘッダーをドラッグしてウィンドウを移動できる */
17
+ draggable?: boolean;
18
+ /** draggable 時の初期位置(未指定時は { x: 100, y: 100 }) */
19
+ defaultPosition?: {
20
+ x: number;
21
+ y: number;
22
+ };
23
+ /** true のとき表示時に Windows 風のスケール+フェードインアニメーションを行う */
24
+ animateOnOpen?: boolean;
25
+ /** 右上の赤ボタンで閉じる際に呼ばれる。指定時は赤ボタンが閉じるボタンになる */
26
+ onClose?: () => void;
27
+ /** 指定時はヘッダーにログアウトボタンを表示(認証連携用) */
28
+ onLogout?: () => void;
29
+ /** true のときボディにパスワード(ログイン)画面を表示(production/staging 用) */
30
+ showLogin?: boolean;
31
+ /** ログイン画面のタイトル */
32
+ loginTitle?: string;
33
+ /** ログイン送信 */
34
+ onLogin?: (password: string) => Promise<void>;
35
+ /** ログイン失敗メッセージ */
36
+ loginError?: string;
37
+ /** ログインエラーをクリア */
38
+ onClearLoginError?: () => void;
39
+ };
40
+ export declare function LogWindow({ entries, networkEntries, defaultTab, onTabChange, height, className, draggable, defaultPosition, animateOnOpen, onClose, onLogout, showLogin, loginTitle, onLogin, loginError, onClearLoginError, }: LogWindowProps): import("react/jsx-runtime").JSX.Element;
41
+ //# sourceMappingURL=LogWindow.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LogWindow.d.ts","sourceRoot":"","sources":["../../../src/components/LogWindow/LogWindow.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAIpE,OAAO,gCAAgC,CAAC;AACxC,OAAO,iBAAiB,CAAC;AAgBzB,MAAM,MAAM,cAAc,GAAG;IAC3B,oBAAoB;IACpB,OAAO,CAAC,EAAE,QAAQ,EAAE,CAAC;IACrB,iDAAiD;IACjD,cAAc,CAAC,EAAE,YAAY,EAAE,CAAC;IAChC,6BAA6B;IAC7B,UAAU,CAAC,EAAE,YAAY,CAAC;IAC1B,qBAAqB;IACrB,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,YAAY,KAAK,IAAI,CAAC;IAC1C,wCAAwC;IACxC,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,qCAAqC;IACrC,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,iDAAiD;IACjD,eAAe,CAAC,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC3C,mDAAmD;IACnD,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,2CAA2C;IAC3C,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,kCAAkC;IAClC,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,yDAAyD;IACzD,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,kBAAkB;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa;IACb,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9C,kBAAkB;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,kBAAkB;IAClB,iBAAiB,CAAC,EAAE,MAAM,IAAI,CAAC;CAChC,CAAC;AAIF,wBAAgB,SAAS,CAAC,EACxB,OAAY,EACZ,cAAmB,EACnB,UAAmB,EACnB,WAAW,EACX,MAAM,EACN,SAAc,EACd,SAAiB,EACjB,eAAe,EACf,aAAqB,EACrB,OAAO,EACP,QAAQ,EACR,SAAiB,EACjB,UAAoB,EACpB,OAAO,EACP,UAAU,EACV,iBAAiB,GAClB,EAAE,cAAc,2CA8ShB"}
@@ -0,0 +1,144 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useRef, useCallback, useEffect } from 'react';
3
+ import { Terminal, Wifi, X, Download, FileJson, FileText, LogOut } from 'lucide-react';
4
+ import { LogEntryRow } from './LogEntryRow';
5
+ import { NetworkEntryRow } from './NetworkEntryRow';
6
+ import { LoginForm } from '../LoginWindow/LoginForm';
7
+ import '../LoginWindow/LoginWindow.css';
8
+ import './LogWindow.css';
9
+ function escapeCsvCell(value) {
10
+ if (/[",\n\r]/.test(value))
11
+ return `"${value.replace(/"/g, '""')}"`;
12
+ return value;
13
+ }
14
+ function downloadBlob(blob, filename) {
15
+ const url = URL.createObjectURL(blob);
16
+ const a = document.createElement('a');
17
+ a.href = url;
18
+ a.download = filename;
19
+ a.click();
20
+ URL.revokeObjectURL(url);
21
+ }
22
+ const CLOSE_ANIMATION_MS = 200;
23
+ export function LogWindow({ entries = [], networkEntries = [], defaultTab = 'logs', onTabChange, height, className = '', draggable = false, defaultPosition, animateOnOpen = false, onClose, onLogout, showLogin = false, loginTitle = 'Login', onLogin, loginError, onClearLoginError, }) {
24
+ const [activeTab, setActiveTab] = useState(defaultTab);
25
+ const [isClosing, setIsClosing] = useState(false);
26
+ const [unreadLogsCount, setUnreadLogsCount] = useState(0);
27
+ const [unreadNetworksCount, setUnreadNetworksCount] = useState(0);
28
+ const [exportDialogOpen, setExportDialogOpen] = useState(false);
29
+ const closeTimeoutRef = useRef(null);
30
+ const prevEntriesLengthRef = useRef(entries.length);
31
+ const prevNetworkEntriesLengthRef = useRef(networkEntries.length);
32
+ const [position, setPosition] = useState(() => draggable ? defaultPosition ?? { x: 100, y: 100 } : { x: 0, y: 0 });
33
+ const dragStartRef = useRef(null);
34
+ useEffect(() => {
35
+ const prevLogs = prevEntriesLengthRef.current;
36
+ const prevNetworks = prevNetworkEntriesLengthRef.current;
37
+ if (entries.length > prevLogs && activeTab === 'networks') {
38
+ setUnreadLogsCount((c) => c + (entries.length - prevLogs));
39
+ }
40
+ prevEntriesLengthRef.current = entries.length;
41
+ if (networkEntries.length > prevNetworks && activeTab === 'logs') {
42
+ setUnreadNetworksCount((c) => c + (networkEntries.length - prevNetworks));
43
+ }
44
+ prevNetworkEntriesLengthRef.current = networkEntries.length;
45
+ }, [entries.length, networkEntries.length, activeTab]);
46
+ const handleTab = (tab) => {
47
+ setActiveTab(tab);
48
+ if (tab === 'logs')
49
+ setUnreadLogsCount(0);
50
+ if (tab === 'networks')
51
+ setUnreadNetworksCount(0);
52
+ onTabChange?.(tab);
53
+ };
54
+ const handleHeaderMouseDown = useCallback((e) => {
55
+ if (!draggable || e.button !== 0)
56
+ return;
57
+ e.preventDefault();
58
+ dragStartRef.current = {
59
+ x: position.x,
60
+ y: position.y,
61
+ clientX: e.clientX,
62
+ clientY: e.clientY,
63
+ };
64
+ }, [draggable, position]);
65
+ useEffect(() => {
66
+ if (!draggable)
67
+ return;
68
+ const handleMouseMove = (e) => {
69
+ if (dragStartRef.current == null)
70
+ return;
71
+ setPosition({
72
+ x: dragStartRef.current.x + (e.clientX - dragStartRef.current.clientX),
73
+ y: dragStartRef.current.y + (e.clientY - dragStartRef.current.clientY),
74
+ });
75
+ };
76
+ const handleMouseUp = () => {
77
+ dragStartRef.current = null;
78
+ };
79
+ document.addEventListener('mousemove', handleMouseMove);
80
+ document.addEventListener('mouseup', handleMouseUp);
81
+ return () => {
82
+ document.removeEventListener('mousemove', handleMouseMove);
83
+ document.removeEventListener('mouseup', handleMouseUp);
84
+ };
85
+ }, [draggable]);
86
+ useEffect(() => {
87
+ return () => {
88
+ if (closeTimeoutRef.current)
89
+ clearTimeout(closeTimeoutRef.current);
90
+ };
91
+ }, []);
92
+ const handleCloseClick = useCallback(() => {
93
+ if (!onClose || isClosing)
94
+ return;
95
+ setIsClosing(true);
96
+ closeTimeoutRef.current = setTimeout(() => {
97
+ closeTimeoutRef.current = null;
98
+ onClose();
99
+ }, CLOSE_ANIMATION_MS);
100
+ }, [onClose, isClosing]);
101
+ const handleExportCsv = useCallback(() => {
102
+ const header = 'level,message,source\n';
103
+ const rows = entries.map((e) => [e.level, escapeCsvCell(e.message), escapeCsvCell(e.source)].join(','));
104
+ const csv = header + rows.join('\n');
105
+ const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' });
106
+ downloadBlob(blob, `logs-${Date.now()}.csv`);
107
+ setExportDialogOpen(false);
108
+ }, [entries]);
109
+ const handleExportJson = useCallback(() => {
110
+ const data = { logs: entries, networks: networkEntries };
111
+ const json = JSON.stringify(data, null, 2);
112
+ const blob = new Blob([json], { type: 'application/json;charset=utf-8' });
113
+ downloadBlob(blob, `logs-${Date.now()}.json`);
114
+ setExportDialogOpen(false);
115
+ }, [entries, networkEntries]);
116
+ const bodyStyle = height != null ? { maxHeight: typeof height === 'number' ? `${height}px` : height } : undefined;
117
+ const rootStyle = draggable
118
+ ? {
119
+ position: 'fixed',
120
+ left: position.x,
121
+ top: position.y,
122
+ zIndex: 10000,
123
+ }
124
+ : undefined;
125
+ const rootClassName = [
126
+ 'logWindow',
127
+ animateOnOpen && !isClosing ? 'logWindowOpenAnimation' : '',
128
+ isClosing ? 'logWindowCloseAnimation' : '',
129
+ className,
130
+ ]
131
+ .filter(Boolean)
132
+ .join(' ')
133
+ .trim();
134
+ const innerClassName = [
135
+ 'logWindowInner',
136
+ showLogin && loginError ? 'logWindowLoginError' : '',
137
+ ]
138
+ .filter(Boolean)
139
+ .join(' ')
140
+ .trim();
141
+ return (_jsx("div", { className: rootClassName.trim(), ...(rootStyle != null ? { style: rootStyle } : {}), children: _jsxs("div", { className: innerClassName, children: [_jsxs("header", { className: `logWindowHeader ${draggable ? 'logWindowHeaderDraggable' : ''} ${showLogin ? 'logWindowHeaderLoginOnly' : ''}`, onMouseDown: handleHeaderMouseDown, role: draggable ? 'button' : undefined, tabIndex: draggable ? 0 : undefined, "aria-label": draggable ? 'Move window' : undefined, children: [!showLogin && (_jsxs("div", { className: "logWindowTabs", children: [_jsxs("button", { type: "button", className: `logWindowTab ${activeTab === 'logs' ? 'logWindowTabActive' : ''}`, onClick: () => handleTab('logs'), onMouseDown: (e) => draggable && e.stopPropagation(), "aria-pressed": activeTab === 'logs', children: [_jsx(Terminal, { className: "logWindowTabIcon logWindowTabIconTerminal", size: 14, "aria-hidden": true }), "Logs", activeTab !== 'logs' && unreadLogsCount > 0 && (_jsx("span", { className: "logWindowTabBadge", "aria-label": `New logs: ${unreadLogsCount}`, children: unreadLogsCount > 99 ? '99+' : unreadLogsCount }))] }), _jsxs("button", { type: "button", className: `logWindowTab ${activeTab === 'networks' ? 'logWindowTabActive' : ''}`, onClick: () => handleTab('networks'), onMouseDown: (e) => draggable && e.stopPropagation(), "aria-pressed": activeTab === 'networks', children: [_jsx(Wifi, { className: "logWindowTabIcon logWindowTabIconWifi", size: 14, "aria-hidden": true }), "Networks", activeTab !== 'networks' && unreadNetworksCount > 0 && (_jsx("span", { className: "logWindowTabBadge", "aria-label": `New networks: ${unreadNetworksCount}`, children: unreadNetworksCount > 99 ? '99+' : unreadNetworksCount }))] })] })), showLogin && _jsx("span", { className: "logWindowHeaderLoginTitle", children: loginTitle }), onLogout && !showLogin && (_jsxs("button", { type: "button", className: "logWindowLogoutButton", onClick: onLogout, "aria-label": "Log out", title: "Log out", children: [_jsx(LogOut, { size: 14, "aria-hidden": true }), _jsx("span", { children: "Log out" })] })), onClose ? (_jsx("button", { type: "button", className: "logWindowCloseButton", onClick: handleCloseClick, disabled: isClosing, "aria-label": "close", title: "close", children: _jsx(X, { size: 16, fill: "currentColor", "aria-hidden": true }) })) : (_jsx("span", { className: "logWindowIndicator", title: "Recording / Active", children: _jsx(X, { size: 16, fill: "currentColor", "aria-hidden": true }) }))] }), _jsx("div", { className: "logWindowBody", style: bodyStyle, children: showLogin && onLogin ? (_jsx(LoginForm, { title: loginTitle, onLogin: onLogin, errorMessage: loginError, onClearError: onClearLoginError })) : (_jsxs("div", { className: "logWindowBodySlider", style: {
142
+ transform: activeTab === 'logs' ? 'translateX(0)' : 'translateX(-50%)',
143
+ }, children: [_jsx("div", { className: "logWindowBodyPanel", children: entries.map((entry) => (_jsx(LogEntryRow, { entry: entry }, entry.id))) }), _jsx("div", { className: "logWindowBodyPanel", children: networkEntries.length > 0 ? (networkEntries.map((entry) => (_jsx(NetworkEntryRow, { entry: entry }, entry.id)))) : (_jsx("div", { className: "logWindowEntry logWindowEntryInfo", style: { color: 'var(--logw-text-muted)' }, children: "Network requests will appear here." })) })] })) }), !showLogin && (_jsx("footer", { className: "logWindowFooter", children: _jsxs("button", { type: "button", className: "logWindowExportButton", onClick: () => setExportDialogOpen(true), "aria-haspopup": "dialog", "aria-expanded": exportDialogOpen, children: [_jsx(Download, { size: 14, "aria-hidden": true }), "Export"] }) })), exportDialogOpen && (_jsx("div", { className: "logWindowExportOverlay", role: "dialog", "aria-modal": "true", "aria-labelledby": "logWindowExportDialogTitle", children: _jsxs("div", { className: "logWindowExportDialog", children: [_jsx("h3", { id: "logWindowExportDialogTitle", className: "logWindowExportDialogTitle", children: "Export format" }), _jsx("p", { className: "logWindowExportDialogDescription", children: "CSV \u3067\u30A8\u30AF\u30B9\u30DD\u30FC\u30C8\u3057\u307E\u3059\u304B\u3001JSON \u3067\u30A8\u30AF\u30B9\u30DD\u30FC\u30C8\u3057\u307E\u3059\u304B\uFF1F" }), _jsxs("div", { className: "logWindowExportDialogActions", children: [_jsxs("button", { type: "button", className: "logWindowExportFormatButton", onClick: handleExportCsv, children: [_jsx(FileText, { size: 18, "aria-hidden": true }), "CSV"] }), _jsxs("button", { type: "button", className: "logWindowExportFormatButton", onClick: handleExportJson, children: [_jsx(FileJson, { size: 18, "aria-hidden": true }), "JSON"] })] }), _jsx("button", { type: "button", className: "logWindowExportDialogCancel", onClick: () => setExportDialogOpen(false), children: "Cancel" })] }) }))] }) }));
144
+ }
@@ -0,0 +1,29 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { LogWindow } from './LogWindow';
3
+ declare const meta: Meta<typeof LogWindow>;
4
+ export default meta;
5
+ type Story = StoryObj<typeof LogWindow>;
6
+ export declare const Default: Story;
7
+ export declare const InfoOnly: Story;
8
+ export declare const ManyEntries: Story;
9
+ export declare const Empty: Story;
10
+ export declare const NetworksTab: Story;
11
+ /** Networks タブで REST API リクエストを表示 */
12
+ export declare const NetworkREST: Story;
13
+ /** Networks タブで GraphQL リクエストを表示 */
14
+ export declare const NetworkGraphQL: Story;
15
+ /** REST と GraphQL の両方のネットワークエントリを表示 */
16
+ export declare const NetworkMixed: Story;
17
+ /**
18
+ * ヘッダーをドラッグしてウィンドウを自由に移動できます。
19
+ * タブ(Logs / Networks)をクリックしてもドラッグは開始されません。
20
+ */
21
+ export declare const Draggable: Story;
22
+ /**
23
+ * Escape キーを 5 回押すとログウィンドウがオーバーレイで開きます。
24
+ * テスト手順: ストーリーを開き、Escape を 5 回連続で押す(約 1.5 秒以内に次のキーを押す)。
25
+ * ログウィンドウ表示中に Escape を 1 回押すか、背景をクリックすると閉じます。
26
+ * 開いたウィンドウはヘッダーをドラッグして移動できます。
27
+ */
28
+ export declare const EscapeToOpen: Story;
29
+ //# sourceMappingURL=LogWindow.stories.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LogWindow.stories.d.ts","sourceRoot":"","sources":["../../../src/components/LogWindow/LogWindow.stories.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AA2FxC,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,OAAO,SAAS,CAahC,CAAC;AAEF,eAAe,IAAI,CAAC;AAEpB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,SAAS,CAAC,CAAC;AAExC,eAAO,MAAM,OAAO,EAAE,KAKrB,CAAC;AAEF,eAAO,MAAM,QAAQ,EAAE,KAOtB,CAAC;AAEF,eAAO,MAAM,WAAW,EAAE,KAUzB,CAAC;AAEF,eAAO,MAAM,KAAK,EAAE,KAInB,CAAC;AAEF,eAAO,MAAM,WAAW,EAAE,KAKzB,CAAC;AAEF,qCAAqC;AACrC,eAAO,MAAM,WAAW,EAAE,KAKzB,CAAC;AAEF,oCAAoC;AACpC,eAAO,MAAM,cAAc,EAAE,KAK5B,CAAC;AAEF,uCAAuC;AACvC,eAAO,MAAM,YAAY,EAAE,KAQ1B,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,SAAS,EAAE,KAOvB,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,YAAY,EAAE,KAO1B,CAAC"}
@@ -0,0 +1,183 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { LogWindow } from './LogWindow';
3
+ import { LogWindowEscapeDemo } from './LogWindowEscapeDemo';
4
+ const sampleEntries = [
5
+ {
6
+ id: '1',
7
+ level: 'info',
8
+ message: 'Hello!!',
9
+ source: 'ArticleContainer.tsx:32',
10
+ },
11
+ {
12
+ id: '2',
13
+ level: 'warn',
14
+ message: 'Hello!!',
15
+ source: 'ArticleContainer.tsx:32',
16
+ },
17
+ {
18
+ id: '3',
19
+ level: 'error',
20
+ message: 'Hello!!',
21
+ source: 'ArticleContainer.tsx:32',
22
+ },
23
+ ];
24
+ const sampleNetworkRest = [
25
+ {
26
+ id: 'r1',
27
+ type: 'rest',
28
+ method: 'GET',
29
+ url: '/api/users',
30
+ status: 200,
31
+ statusText: 'OK',
32
+ request: 'GET /api/users HTTP/1.1\nAccept: application/json',
33
+ response: '{\n "data": [\n { "id": "1", "name": "Alice" },\n { "id": "2", "name": "Bob" }\n ]\n}',
34
+ },
35
+ {
36
+ id: 'r2',
37
+ type: 'rest',
38
+ method: 'POST',
39
+ url: '/api/auth/login',
40
+ status: 201,
41
+ statusText: 'Created',
42
+ request: 'POST /api/auth/login HTTP/1.1\nContent-Type: application/json\n\n{"email":"user@example.com","password":"***"}',
43
+ response: '{\n "token": "eyJhbGciOiJIUzI1NiIs...",\n "user": { "id": "1", "email": "user@example.com" }\n}',
44
+ },
45
+ {
46
+ id: 'r3',
47
+ type: 'rest',
48
+ method: 'GET',
49
+ url: '/api/articles/123',
50
+ status: 404,
51
+ statusText: 'Not Found',
52
+ request: 'GET /api/articles/123 HTTP/1.1',
53
+ response: '{\n "error": "Not Found",\n "message": "Article 123 does not exist"\n}',
54
+ },
55
+ ];
56
+ const sampleNetworkGraphql = [
57
+ {
58
+ id: 'g1',
59
+ type: 'graphql',
60
+ operationName: 'GetUser',
61
+ url: '/graphql',
62
+ status: 200,
63
+ statusText: 'OK',
64
+ request: 'query GetUser($id: ID!) {\n user(id: $id) {\n id\n name\n email\n }\n}\n\nVariables: { "id": "1" }',
65
+ response: '{\n "data": {\n "user": {\n "id": "1",\n "name": "Alice",\n "email": "alice@example.com"\n }\n }\n}',
66
+ },
67
+ {
68
+ id: 'g2',
69
+ type: 'graphql',
70
+ operationName: 'ListArticles',
71
+ url: '/graphql',
72
+ status: 200,
73
+ statusText: 'OK',
74
+ request: 'query ListArticles {\n articles {\n id\n title\n publishedAt\n }\n}',
75
+ response: '{\n "data": {\n "articles": [\n { "id": "1", "title": "Hello", "publishedAt": "2024-01-01" }\n ]\n }\n}',
76
+ },
77
+ {
78
+ id: 'g3',
79
+ type: 'graphql',
80
+ operationName: 'CreatePost',
81
+ url: '/graphql',
82
+ status: 400,
83
+ statusText: 'Bad Request',
84
+ request: 'mutation CreatePost($input: CreatePostInput!) {\n createPost(input: $input) { id }\n}',
85
+ response: '{\n "errors": [\n { "message": "Validation error: title is required" }\n ]\n}',
86
+ },
87
+ ];
88
+ const meta = {
89
+ title: 'Components/LogWindow',
90
+ component: LogWindow,
91
+ parameters: {
92
+ layout: 'centered',
93
+ },
94
+ tags: ['autodocs'],
95
+ argTypes: {
96
+ defaultTab: {
97
+ control: 'radio',
98
+ options: ['logs', 'networks'],
99
+ },
100
+ },
101
+ };
102
+ export default meta;
103
+ export const Default = {
104
+ args: {
105
+ entries: sampleEntries,
106
+ defaultTab: 'logs',
107
+ },
108
+ };
109
+ export const InfoOnly = {
110
+ args: {
111
+ entries: [
112
+ { id: '1', level: 'info', message: 'Application started', source: 'App.tsx:12' },
113
+ { id: '2', level: 'info', message: 'User logged in', source: 'Auth.tsx:45' },
114
+ ],
115
+ },
116
+ };
117
+ export const ManyEntries = {
118
+ args: {
119
+ entries: [
120
+ ...sampleEntries,
121
+ { id: '4', level: 'info', message: 'Fetching data...', source: 'useApi.ts:18' },
122
+ { id: '5', level: 'warn', message: 'Deprecated API used', source: 'legacy.ts:3' },
123
+ { id: '6', level: 'error', message: 'Network request failed', source: 'api.ts:92' },
124
+ ],
125
+ height: 280,
126
+ },
127
+ };
128
+ export const Empty = {
129
+ args: {
130
+ entries: [],
131
+ },
132
+ };
133
+ export const NetworksTab = {
134
+ args: {
135
+ entries: sampleEntries,
136
+ defaultTab: 'networks',
137
+ },
138
+ };
139
+ /** Networks タブで REST API リクエストを表示 */
140
+ export const NetworkREST = {
141
+ args: {
142
+ networkEntries: sampleNetworkRest,
143
+ defaultTab: 'networks',
144
+ },
145
+ };
146
+ /** Networks タブで GraphQL リクエストを表示 */
147
+ export const NetworkGraphQL = {
148
+ args: {
149
+ networkEntries: sampleNetworkGraphql,
150
+ defaultTab: 'networks',
151
+ },
152
+ };
153
+ /** REST と GraphQL の両方のネットワークエントリを表示 */
154
+ export const NetworkMixed = {
155
+ args: {
156
+ networkEntries: [
157
+ ...sampleNetworkRest,
158
+ ...sampleNetworkGraphql,
159
+ ],
160
+ defaultTab: 'networks',
161
+ },
162
+ };
163
+ /**
164
+ * ヘッダーをドラッグしてウィンドウを自由に移動できます。
165
+ * タブ(Logs / Networks)をクリックしてもドラッグは開始されません。
166
+ */
167
+ export const Draggable = {
168
+ args: {
169
+ entries: sampleEntries,
170
+ networkEntries: [...sampleNetworkRest, ...sampleNetworkGraphql],
171
+ draggable: true,
172
+ defaultPosition: { x: 80, y: 80 },
173
+ },
174
+ };
175
+ /**
176
+ * Escape キーを 5 回押すとログウィンドウがオーバーレイで開きます。
177
+ * テスト手順: ストーリーを開き、Escape を 5 回連続で押す(約 1.5 秒以内に次のキーを押す)。
178
+ * ログウィンドウ表示中に Escape を 1 回押すか、背景をクリックすると閉じます。
179
+ * 開いたウィンドウはヘッダーをドラッグして移動できます。
180
+ */
181
+ export const EscapeToOpen = {
182
+ render: () => (_jsx(LogWindowEscapeDemo, { entries: sampleEntries, networkEntries: [...sampleNetworkRest, ...sampleNetworkGraphql] })),
183
+ };
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=LogWindow.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LogWindow.test.d.ts","sourceRoot":"","sources":["../../../src/components/LogWindow/LogWindow.test.tsx"],"names":[],"mappings":""}
@@ -0,0 +1,61 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { describe, it, expect, vi } from 'vitest';
3
+ import { render, screen } from '@testing-library/react';
4
+ import userEvent from '@testing-library/user-event';
5
+ import { LogWindow } from './LogWindow';
6
+ describe('LogWindow', () => {
7
+ it('renders with empty entries by default', () => {
8
+ render(_jsx(LogWindow, {}));
9
+ expect(screen.getByRole('button', { name: /logs/i })).toBeInTheDocument();
10
+ expect(screen.getByRole('button', { name: /networks/i })).toBeInTheDocument();
11
+ });
12
+ it('renders log entries', () => {
13
+ const entries = [
14
+ { id: '1', level: 'info', message: 'Test message', source: 'app' },
15
+ { id: '2', level: 'warn', message: 'Warning', source: 'app' },
16
+ ];
17
+ render(_jsx(LogWindow, { entries: entries }));
18
+ expect(screen.getByText('Test message')).toBeInTheDocument();
19
+ expect(screen.getByText('Warning')).toBeInTheDocument();
20
+ });
21
+ it('renders network entries in Networks tab', async () => {
22
+ const user = userEvent.setup();
23
+ const networkEntries = [
24
+ {
25
+ id: 'n1',
26
+ type: 'rest',
27
+ method: 'GET',
28
+ url: 'https://api.example.com/posts',
29
+ status: 200,
30
+ statusText: 'OK',
31
+ request: 'GET /posts',
32
+ response: '[]',
33
+ },
34
+ ];
35
+ render(_jsx(LogWindow, { networkEntries: networkEntries }));
36
+ const networksTab = screen.getByRole('button', { name: /networks/i });
37
+ await user.click(networksTab);
38
+ expect(screen.getByText(/GET.*posts/i)).toBeInTheDocument();
39
+ });
40
+ it('shows login form when showLogin is true and onLogin provided', () => {
41
+ const onLogin = vi.fn().mockResolvedValue(undefined);
42
+ render(_jsx(LogWindow, { showLogin: true, loginTitle: "Sign in", onLogin: onLogin }));
43
+ expect(screen.getByPlaceholderText('••••••••')).toBeInTheDocument();
44
+ expect(screen.getByRole('button', { name: /log in/i })).toBeInTheDocument();
45
+ expect(screen.getAllByText('Sign in').length).toBeGreaterThan(0);
46
+ });
47
+ it('when showLogin, does not show Logs/Networks tabs', () => {
48
+ render(_jsx(LogWindow, { showLogin: true, onLogin: async () => { } }));
49
+ expect(screen.queryByRole('button', { name: /^logs$/i })).not.toBeInTheDocument();
50
+ expect(screen.queryByRole('button', { name: /^networks$/i })).not.toBeInTheDocument();
51
+ });
52
+ it('calls onClose when close button is clicked', async () => {
53
+ const user = userEvent.setup();
54
+ const onClose = vi.fn();
55
+ render(_jsx(LogWindow, { onClose: onClose }));
56
+ const closeBtn = screen.getByRole('button', { name: /close/i });
57
+ await user.click(closeBtn);
58
+ await new Promise((r) => setTimeout(r, 250));
59
+ expect(onClose).toHaveBeenCalled();
60
+ });
61
+ });
@@ -0,0 +1,12 @@
1
+ import type { LogEntry, NetworkEntry } from './types';
2
+ import './LogWindow.css';
3
+ export type LogWindowEscapeDemoProps = {
4
+ entries?: LogEntry[];
5
+ networkEntries?: NetworkEntry[];
6
+ };
7
+ /**
8
+ * Escape を 5 回押すとログウィンドウを表示するデモ。
9
+ * Storybook でテスト用に利用。
10
+ */
11
+ export declare function LogWindowEscapeDemo({ entries, networkEntries, }: LogWindowEscapeDemoProps): import("react/jsx-runtime").JSX.Element;
12
+ //# sourceMappingURL=LogWindowEscapeDemo.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LogWindowEscapeDemo.d.ts","sourceRoot":"","sources":["../../../src/components/LogWindow/LogWindowEscapeDemo.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACtD,OAAO,iBAAiB,CAAC;AAyCzB,MAAM,MAAM,wBAAwB,GAAG;IACrC,OAAO,CAAC,EAAE,QAAQ,EAAE,CAAC;IACrB,cAAc,CAAC,EAAE,YAAY,EAAE,CAAC;CACjC,CAAC;AAEF;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,OAAY,EACZ,cAAmB,GACpB,EAAE,wBAAwB,2CAyC1B"}
@@ -0,0 +1,56 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { createPortal } from 'react-dom';
3
+ import { LogWindow } from './LogWindow';
4
+ import { useLogWindowShortcut } from '../../hooks/useLogWindowShortcut';
5
+ import './LogWindow.css';
6
+ const demoStyles = {
7
+ position: 'relative',
8
+ minHeight: 300,
9
+ padding: 24,
10
+ background: 'var(--logw-bg, #1e1e1e)',
11
+ borderRadius: 8,
12
+ color: 'var(--logw-text, #d4d4d4)',
13
+ fontFamily: 'ui-monospace, monospace',
14
+ };
15
+ const overlayStyles = {
16
+ position: 'fixed',
17
+ inset: 0,
18
+ background: 'transparent',
19
+ display: 'flex',
20
+ alignItems: 'center',
21
+ justifyContent: 'center',
22
+ zIndex: 9999,
23
+ padding: 24,
24
+ boxSizing: 'border-box',
25
+ };
26
+ const instructionStyles = {
27
+ marginBottom: 16,
28
+ fontSize: 14,
29
+ color: 'var(--logw-text-muted, #858585)',
30
+ };
31
+ const countStyles = {
32
+ fontSize: 12,
33
+ marginTop: 8,
34
+ padding: '6px 12px',
35
+ background: 'rgba(255,255,255,0.08)',
36
+ borderRadius: 4,
37
+ display: 'inline-block',
38
+ };
39
+ const ESCAPE_COUNT = 5;
40
+ /**
41
+ * Escape を 5 回押すとログウィンドウを表示するデモ。
42
+ * Storybook でテスト用に利用。
43
+ */
44
+ export function LogWindowEscapeDemo({ entries = [], networkEntries = [], }) {
45
+ const { isOpen, close, escapeCount: count } = useLogWindowShortcut({
46
+ escapeCount: ESCAPE_COUNT,
47
+ resetAfterMs: 1500,
48
+ closeOnEscape: true,
49
+ });
50
+ return (_jsxs("div", { style: demoStyles, children: [_jsx("div", { style: instructionStyles, children: _jsxs("strong", { children: ["Escape \u30AD\u30FC\u3092 ", ESCAPE_COUNT, " \u56DE\u62BC\u3059\u3068\u30ED\u30B0\u30A6\u30A3\u30F3\u30C9\u30A6\u304C\u958B\u304D\u307E\u3059\u3002"] }) }), _jsxs("div", { style: countStyles, children: ["Escape: ", count, " / ", ESCAPE_COUNT] }), isOpen &&
51
+ typeof document !== 'undefined' &&
52
+ createPortal(_jsx("div", { style: overlayStyles, role: "presentation", children: _jsx("div", { onClick: (e) => e.stopPropagation(), children: _jsx(LogWindow, { entries: entries, networkEntries: networkEntries, draggable: true, animateOnOpen: true, onClose: close, defaultPosition: {
53
+ x: typeof window !== 'undefined' ? Math.max(0, (window.innerWidth - 696) / 2) : 100,
54
+ y: typeof window !== 'undefined' ? Math.max(0, (window.innerHeight - 466) / 2) : 100,
55
+ } }) }) }), document.body)] }));
56
+ }
@@ -0,0 +1,8 @@
1
+ import type { NetworkEntry } from './types';
2
+ import './LogWindow.css';
3
+ type NetworkEntryRowProps = {
4
+ entry: NetworkEntry;
5
+ };
6
+ export declare function NetworkEntryRow({ entry }: NetworkEntryRowProps): import("react/jsx-runtime").JSX.Element;
7
+ export {};
8
+ //# sourceMappingURL=NetworkEntryRow.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"NetworkEntryRow.d.ts","sourceRoot":"","sources":["../../../src/components/LogWindow/NetworkEntryRow.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAC5C,OAAO,iBAAiB,CAAC;AAEzB,KAAK,oBAAoB,GAAG;IAC1B,KAAK,EAAE,YAAY,CAAC;CACrB,CAAC;AASF,wBAAgB,eAAe,CAAC,EAAE,KAAK,EAAE,EAAE,oBAAoB,2CAqF9D"}