@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,100 @@
1
+ import React from 'react';
2
+ import { createPortal } from 'react-dom';
3
+ import { LogWindow } from './LogWindow';
4
+ import { useLogWindowShortcut } from '../../hooks/useLogWindowShortcut';
5
+ import type { LogEntry, NetworkEntry } from './types';
6
+ import './LogWindow.css';
7
+
8
+ const demoStyles: React.CSSProperties = {
9
+ position: 'relative',
10
+ minHeight: 300,
11
+ padding: 24,
12
+ background: 'var(--logw-bg, #1e1e1e)',
13
+ borderRadius: 8,
14
+ color: 'var(--logw-text, #d4d4d4)',
15
+ fontFamily: 'ui-monospace, monospace',
16
+ };
17
+
18
+ const overlayStyles: React.CSSProperties = {
19
+ position: 'fixed',
20
+ inset: 0,
21
+ background: 'transparent',
22
+ display: 'flex',
23
+ alignItems: 'center',
24
+ justifyContent: 'center',
25
+ zIndex: 9999,
26
+ padding: 24,
27
+ boxSizing: 'border-box',
28
+ };
29
+
30
+ const instructionStyles: React.CSSProperties = {
31
+ marginBottom: 16,
32
+ fontSize: 14,
33
+ color: 'var(--logw-text-muted, #858585)',
34
+ };
35
+
36
+ const countStyles: React.CSSProperties = {
37
+ fontSize: 12,
38
+ marginTop: 8,
39
+ padding: '6px 12px',
40
+ background: 'rgba(255,255,255,0.08)',
41
+ borderRadius: 4,
42
+ display: 'inline-block',
43
+ };
44
+
45
+ const ESCAPE_COUNT = 5;
46
+
47
+ export type LogWindowEscapeDemoProps = {
48
+ entries?: LogEntry[];
49
+ networkEntries?: NetworkEntry[];
50
+ };
51
+
52
+ /**
53
+ * Escape を 5 回押すとログウィンドウを表示するデモ。
54
+ * Storybook でテスト用に利用。
55
+ */
56
+ export function LogWindowEscapeDemo({
57
+ entries = [],
58
+ networkEntries = [],
59
+ }: LogWindowEscapeDemoProps) {
60
+ const { isOpen, close, escapeCount: count } = useLogWindowShortcut({
61
+ escapeCount: ESCAPE_COUNT,
62
+ resetAfterMs: 1500,
63
+ closeOnEscape: true,
64
+ });
65
+
66
+ return (
67
+ <div style={demoStyles}>
68
+ <div style={instructionStyles}>
69
+ <strong>Escape キーを {ESCAPE_COUNT} 回押すとログウィンドウが開きます。</strong>
70
+ </div>
71
+ <div style={countStyles}>
72
+ Escape: {count} / {ESCAPE_COUNT}
73
+ </div>
74
+
75
+ {isOpen &&
76
+ typeof document !== 'undefined' &&
77
+ createPortal(
78
+ <div
79
+ style={overlayStyles}
80
+ role="presentation"
81
+ >
82
+ <div onClick={(e) => e.stopPropagation()}>
83
+ <LogWindow
84
+ entries={entries}
85
+ networkEntries={networkEntries}
86
+ draggable
87
+ animateOnOpen
88
+ onClose={close}
89
+ defaultPosition={{
90
+ x: typeof window !== 'undefined' ? Math.max(0, (window.innerWidth - 696) / 2) : 100,
91
+ y: typeof window !== 'undefined' ? Math.max(0, (window.innerHeight - 466) / 2) : 100,
92
+ }}
93
+ />
94
+ </div>
95
+ </div>,
96
+ document.body
97
+ )}
98
+ </div>
99
+ );
100
+ }
@@ -0,0 +1,102 @@
1
+ import React, { useState } from 'react';
2
+ import { Globe, Database, ChevronRight, ChevronDown } from 'lucide-react';
3
+ import type { NetworkEntry } from './types';
4
+ import './LogWindow.css';
5
+
6
+ type NetworkEntryRowProps = {
7
+ entry: NetworkEntry;
8
+ };
9
+
10
+ function getStatusColor(status?: number): string {
11
+ if (status == null) return 'var(--logw-text-muted)';
12
+ if (status >= 200 && status < 300) return '#4ec9b0';
13
+ if (status >= 400) return '#f14c4c';
14
+ return 'var(--logw-text-muted)';
15
+ }
16
+
17
+ export function NetworkEntryRow({ entry }: NetworkEntryRowProps) {
18
+ const [expanded, setExpanded] = useState(false);
19
+ const statusColor = getStatusColor(entry.status);
20
+ const statusText = entry.statusText ?? (entry.status != null ? String(entry.status) : '—');
21
+ const hasDetails = Boolean(entry.request ?? entry.response);
22
+
23
+ const handleToggle = () => {
24
+ if (hasDetails) setExpanded((e) => !e);
25
+ };
26
+
27
+ const header = (
28
+ <div
29
+ className={`logWindowNetworkEntry ${hasDetails ? 'logWindowNetworkEntryClickable' : ''}`}
30
+ onClick={hasDetails ? handleToggle : undefined}
31
+ onKeyDown={
32
+ hasDetails
33
+ ? (e) => {
34
+ if (e.key === 'Enter' || e.key === ' ') {
35
+ e.preventDefault();
36
+ handleToggle();
37
+ }
38
+ }
39
+ : undefined
40
+ }
41
+ role={hasDetails ? 'button' : undefined}
42
+ tabIndex={hasDetails ? 0 : undefined}
43
+ aria-expanded={hasDetails ? expanded : undefined}
44
+ >
45
+ <span className="logWindowNetworkEntryChevron">
46
+ {hasDetails ? (
47
+ expanded ? (
48
+ <ChevronDown size={14} aria-hidden />
49
+ ) : (
50
+ <ChevronRight size={14} aria-hidden />
51
+ )
52
+ ) : null}
53
+ </span>
54
+ <span
55
+ className={`logWindowNetworkEntryIcon ${entry.type === 'graphql' ? 'logWindowNetworkEntryIconGraphql' : ''}`}
56
+ title={entry.type === 'rest' ? 'REST' : 'GraphQL'}
57
+ >
58
+ {entry.type === 'rest' ? <Globe size={12} /> : <Database size={12} />}
59
+ </span>
60
+ {entry.type === 'rest' ? (
61
+ <>
62
+ <span className="logWindowNetworkEntryMethod">{entry.method}</span>
63
+ <span className="logWindowNetworkEntryUrl">{entry.url}</span>
64
+ </>
65
+ ) : (
66
+ <span className="logWindowNetworkEntryOperation">
67
+ {entry.operationName ?? '(anonymous)'}
68
+ </span>
69
+ )}
70
+ <span className="logWindowNetworkEntryStatus" style={{ color: statusColor }}>
71
+ {statusText}
72
+ </span>
73
+ </div>
74
+ );
75
+
76
+ return (
77
+ <div className="logWindowNetworkEntryAccordion">
78
+ {header}
79
+ {hasDetails && (entry.request != null || entry.response != null) && (
80
+ <div
81
+ className={`logWindowNetworkEntryDetailsWrapper ${expanded ? 'logWindowNetworkEntryDetailsWrapperExpanded' : ''}`}
82
+ aria-hidden={!expanded}
83
+ >
84
+ <div className="logWindowNetworkEntryDetails">
85
+ {entry.request != null && (
86
+ <div className="logWindowNetworkEntryDetailSection">
87
+ <div className="logWindowNetworkEntryDetailLabel">Request</div>
88
+ <pre className="logWindowNetworkEntryDetailContent">{entry.request}</pre>
89
+ </div>
90
+ )}
91
+ {entry.response != null && (
92
+ <div className="logWindowNetworkEntryDetailSection">
93
+ <div className="logWindowNetworkEntryDetailLabel">Response</div>
94
+ <pre className="logWindowNetworkEntryDetailContent">{entry.response}</pre>
95
+ </div>
96
+ )}
97
+ </div>
98
+ </div>
99
+ )}
100
+ </div>
101
+ );
102
+ }
@@ -0,0 +1,13 @@
1
+ export { LogWindow } from './LogWindow';
2
+ export { LogWindowEscapeDemo } from './LogWindowEscapeDemo';
3
+ export type { LogWindowProps } from './LogWindow';
4
+ export { LogEntryRow } from './LogEntryRow';
5
+ export { NetworkEntryRow } from './NetworkEntryRow';
6
+ export type {
7
+ LogEntry,
8
+ LogLevel,
9
+ LogWindowTab,
10
+ NetworkEntry,
11
+ NetworkEntryRest,
12
+ NetworkEntryGraphql,
13
+ } from './types';
@@ -0,0 +1,40 @@
1
+ export type LogLevel = 'info' | 'warn' | 'error';
2
+
3
+ export type LogEntry = {
4
+ id: string;
5
+ level: LogLevel;
6
+ message: string;
7
+ source: string;
8
+ };
9
+
10
+ export type LogWindowTab = 'logs' | 'networks';
11
+
12
+ /** REST API のネットワークエントリ */
13
+ export type NetworkEntryRest = {
14
+ id: string;
15
+ type: 'rest';
16
+ method: string;
17
+ url: string;
18
+ status?: number;
19
+ statusText?: string;
20
+ /** アコーディオン展開時の Request 表示用 */
21
+ request?: string;
22
+ /** アコーディオン展開時の Response 表示用 */
23
+ response?: string;
24
+ };
25
+
26
+ /** GraphQL のネットワークエントリ */
27
+ export type NetworkEntryGraphql = {
28
+ id: string;
29
+ type: 'graphql';
30
+ operationName?: string;
31
+ url: string;
32
+ status?: number;
33
+ statusText?: string;
34
+ /** アコーディオン展開時の Request 表示用 */
35
+ request?: string;
36
+ /** アコーディオン展開時の Response 表示用 */
37
+ response?: string;
38
+ };
39
+
40
+ export type NetworkEntry = NetworkEntryRest | NetworkEntryGraphql;
@@ -0,0 +1,38 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { render, screen } from '@testing-library/react';
3
+ import userEvent from '@testing-library/user-event';
4
+ import { LoginForm } from './LoginForm';
5
+
6
+ describe('LoginForm', () => {
7
+ it('renders password input and submit button', () => {
8
+ const onLogin = vi.fn();
9
+ render(<LoginForm onLogin={onLogin} />);
10
+ expect(screen.getByPlaceholderText('••••••••')).toBeInTheDocument();
11
+ expect(screen.getByRole('button', { name: /log in/i })).toBeInTheDocument();
12
+ });
13
+
14
+ it('submit button is disabled when password is empty', () => {
15
+ const onLogin = vi.fn();
16
+ render(<LoginForm onLogin={onLogin} />);
17
+ expect(screen.getByRole('button', { name: /log in/i })).toBeDisabled();
18
+ });
19
+
20
+ it('calls onLogin with password on submit', async () => {
21
+ const user = userEvent.setup();
22
+ const onLogin = vi.fn().mockResolvedValue(undefined);
23
+ render(<LoginForm onLogin={onLogin} />);
24
+ await user.type(screen.getByPlaceholderText('••••••••'), 'secret');
25
+ await user.click(screen.getByRole('button', { name: /log in/i }));
26
+ expect(onLogin).toHaveBeenCalledWith('secret');
27
+ });
28
+
29
+ it('shows custom title when provided', () => {
30
+ render(<LoginForm onLogin={vi.fn()} title="Sign in" />);
31
+ expect(screen.getByText('Sign in')).toBeInTheDocument();
32
+ });
33
+
34
+ it('shows custom submit label when provided', () => {
35
+ render(<LoginForm onLogin={vi.fn()} submitLabel="Submit" />);
36
+ expect(screen.getByRole('button', { name: /submit/i })).toBeInTheDocument();
37
+ });
38
+ });
@@ -0,0 +1,78 @@
1
+ import React, { useState, useCallback } from 'react';
2
+ import { Lock, LogIn } from 'lucide-react';
3
+ import './LoginWindow.css';
4
+
5
+ export type LoginFormProps = {
6
+ onLogin: (password: string) => Promise<void>;
7
+ title?: string;
8
+ submitLabel?: string;
9
+ errorMessage?: string;
10
+ onClearError?: () => void;
11
+ };
12
+
13
+ /** ログイン用フォーム(LogWindow 内などに埋め込む用。オーバーレイなし) */
14
+ export function LoginForm({
15
+ onLogin,
16
+ title = 'Login',
17
+ submitLabel = 'Log in',
18
+ errorMessage,
19
+ onClearError,
20
+ }: LoginFormProps) {
21
+ const [password, setPassword] = useState('');
22
+ const [isSubmitting, setIsSubmitting] = useState(false);
23
+
24
+ const handleSubmit = useCallback(
25
+ (e: React.FormEvent) => {
26
+ e.preventDefault();
27
+ onClearError?.();
28
+ if (!password) return;
29
+ setIsSubmitting(true);
30
+ onLogin(password)
31
+ .then(() => {
32
+ setPassword('');
33
+ })
34
+ .catch(() => {})
35
+ .finally(() => {
36
+ setIsSubmitting(false);
37
+ });
38
+ },
39
+ [onLogin, password, onClearError]
40
+ );
41
+
42
+ return (
43
+ <div className="loginFormWrap">
44
+ {title && (
45
+ <div className="loginFormHeader">
46
+ <Lock size={18} className="loginFormHeaderIcon" aria-hidden />
47
+ <h3 className="loginFormTitle">{title}</h3>
48
+ </div>
49
+ )}
50
+ <form onSubmit={handleSubmit} className="loginWindowForm">
51
+ <label className="loginWindowLabel">
52
+ <Lock size={16} className="loginWindowLabelIcon" aria-hidden />
53
+ <span>Password</span>
54
+ <input
55
+ type="password"
56
+ className="loginWindowInput"
57
+ value={password}
58
+ onChange={(e) => {
59
+ setPassword(e.target.value);
60
+ onClearError?.();
61
+ }}
62
+ autoComplete="current-password"
63
+ placeholder="••••••••"
64
+ disabled={isSubmitting}
65
+ />
66
+ </label>
67
+ <button
68
+ type="submit"
69
+ className="loginWindowSubmit"
70
+ disabled={isSubmitting || !password}
71
+ >
72
+ <LogIn size={16} aria-hidden />
73
+ {isSubmitting ? '...' : submitLabel}
74
+ </button>
75
+ </form>
76
+ </div>
77
+ );
78
+ }
@@ -0,0 +1,198 @@
1
+ /* ログインウィンドウオーバーレイ(全画面・フェード) */
2
+ .loginWindowOverlay {
3
+ position: fixed;
4
+ inset: 0;
5
+ display: flex;
6
+ align-items: center;
7
+ justify-content: center;
8
+ background: rgba(0, 0, 0, 0.7);
9
+ z-index: 99999;
10
+ animation: loginWindowOverlayFadeIn 0.25s ease-out forwards;
11
+ }
12
+
13
+ @keyframes loginWindowOverlayFadeIn {
14
+ from {
15
+ opacity: 0;
16
+ }
17
+ to {
18
+ opacity: 1;
19
+ }
20
+ }
21
+
22
+ .loginWindow {
23
+ --loginw-bg: linear-gradient(135deg, #1e1e1e 0%, #252526 100%);
24
+ --loginw-header-bg: #252526;
25
+ --loginw-border: #3c3c3c;
26
+ --loginw-text: #d4d4d4;
27
+ --loginw-text-muted: #858585;
28
+ --loginw-input-bg: #2d2d2d;
29
+ --loginw-focus: #3794ff;
30
+ --loginw-error-bg: rgba(241, 76, 76, 0.15);
31
+ --loginw-error-text: #f14c4c;
32
+ width: 100%;
33
+ max-width: 380px;
34
+ background: var(--loginw-bg);
35
+ border: 1px solid var(--loginw-border);
36
+ border-radius: 10px;
37
+ box-shadow: 0 8px 40px rgba(0, 0, 0, 0.5);
38
+ animation: loginWindowFadeIn 0.25s ease-out forwards;
39
+ }
40
+
41
+ @keyframes loginWindowFadeIn {
42
+ from {
43
+ opacity: 0;
44
+ transform: scale(0.96);
45
+ }
46
+ to {
47
+ opacity: 1;
48
+ transform: scale(1);
49
+ }
50
+ }
51
+
52
+ .loginWindowHeader {
53
+ display: flex;
54
+ align-items: center;
55
+ gap: 10px;
56
+ padding: 20px 24px;
57
+ background: var(--loginw-header-bg);
58
+ border-bottom: 1px solid var(--loginw-border);
59
+ border-radius: 10px 10px 0 0;
60
+ }
61
+
62
+ .loginWindowHeaderIcon {
63
+ color: var(--loginw-focus);
64
+ flex-shrink: 0;
65
+ }
66
+
67
+ .loginWindowTitle {
68
+ margin: 0;
69
+ font-size: 18px;
70
+ font-weight: 600;
71
+ color: var(--loginw-text);
72
+ font-family: ui-sans-serif, system-ui, sans-serif;
73
+ }
74
+
75
+ .loginWindowForm {
76
+ padding: 24px;
77
+ display: flex;
78
+ flex-direction: column;
79
+ gap: 18px;
80
+ }
81
+
82
+ .loginWindowError {
83
+ padding: 10px 12px;
84
+ font-size: 12px;
85
+ color: var(--loginw-error-text);
86
+ background: var(--loginw-error-bg);
87
+ border-radius: 6px;
88
+ border: 1px solid rgba(241, 76, 76, 0.3);
89
+ }
90
+
91
+ .loginWindowLabel {
92
+ display: flex;
93
+ flex-direction: column;
94
+ gap: 6px;
95
+ font-size: 12px;
96
+ color: #fff;
97
+ font-family: ui-sans-serif, system-ui, sans-serif;
98
+ }
99
+
100
+ .loginWindowLabel span {
101
+ display: flex;
102
+ align-items: center;
103
+ gap: 6px;
104
+ }
105
+
106
+ .loginWindowLabelIcon {
107
+ color: var(--loginw-text-muted);
108
+ flex-shrink: 0;
109
+ }
110
+
111
+ .loginWindowInput {
112
+ width: 100%;
113
+ padding: 10px 12px;
114
+ font-size: 14px;
115
+ font-family: inherit;
116
+ color: var(--loginw-text);
117
+ background: var(--loginw-input-bg);
118
+ border: 1px solid #ffffff8d;
119
+ border-radius: 6px;
120
+ box-sizing: border-box;
121
+ transition: border-color 0.15s, box-shadow 0.15s;
122
+ }
123
+
124
+ .loginWindowInput::placeholder {
125
+ color: rgb(110, 110, 110);
126
+ }
127
+
128
+ .loginWindowInput:hover {
129
+ border-color: #505050;
130
+ }
131
+
132
+ .loginWindowInput:focus {
133
+ outline: none;
134
+ border-color: var(--loginw-focus);
135
+ box-shadow: 0 0 0 2px rgba(55, 148, 255, 0.25);
136
+ }
137
+
138
+ .loginWindowInput:disabled {
139
+ opacity: 0.7;
140
+ cursor: not-allowed;
141
+ }
142
+
143
+ .loginWindowSubmit {
144
+ display: inline-flex;
145
+ align-items: center;
146
+ justify-content: center;
147
+ gap: 8px;
148
+ margin-top: 8px;
149
+ padding: 12px 16px;
150
+ font-size: 14px;
151
+ font-weight: 500;
152
+ font-family: inherit;
153
+ color: #fff;
154
+ background: var(--loginw-focus);
155
+ border: none;
156
+ border-radius: 6px;
157
+ cursor: pointer;
158
+ transition: background 0.15s, opacity 0.15s;
159
+ }
160
+
161
+ .loginWindowSubmit:hover:not(:disabled) {
162
+ background: #2a7fd4;
163
+ }
164
+
165
+ .loginWindowSubmit:disabled {
166
+ opacity: 0.6;
167
+ cursor: not-allowed;
168
+ }
169
+
170
+ /* LogWindow 内埋め込み用(LoginForm) */
171
+ .loginFormWrap {
172
+ padding: 16px;
173
+ min-height: 200px;
174
+ display: flex;
175
+ flex-direction: column;
176
+ }
177
+
178
+ .loginFormHeader {
179
+ display: flex;
180
+ align-items: center;
181
+ gap: 8px;
182
+ margin-bottom: 16px;
183
+ padding-bottom: 12px;
184
+ border-bottom: 1px solid var(--loginw-border, #3c3c3c);
185
+ }
186
+
187
+ .loginFormHeaderIcon {
188
+ color: var(--loginw-focus, #3794ff);
189
+ flex-shrink: 0;
190
+ }
191
+
192
+ .loginFormTitle {
193
+ margin: 0;
194
+ font-size: 14px;
195
+ font-weight: 600;
196
+ color: var(--loginw-text, #d4d4d4);
197
+ font-family: ui-sans-serif, system-ui, sans-serif;
198
+ }
@@ -0,0 +1,90 @@
1
+ import React, { useState, useCallback } from 'react';
2
+ import { Lock, LogIn } from 'lucide-react';
3
+ import './LoginWindow.css';
4
+
5
+ export type LoginWindowProps = {
6
+ /** ログイン送信。成功で resolve、失敗で reject(メッセージは errorMessage で表示) */
7
+ onLogin: (password: string) => Promise<void>;
8
+ /** ウィンドウタイトル */
9
+ title?: string;
10
+ /** 送信ボタンラベル */
11
+ submitLabel?: string;
12
+ /** エラー表示用(onLogin が reject したとき) */
13
+ errorMessage?: string;
14
+ /** エラーをクリアする(入力変更時などに親から渡す) */
15
+ onClearError?: () => void;
16
+ };
17
+
18
+ export function LoginWindow({
19
+ onLogin,
20
+ title = 'Login',
21
+ submitLabel = 'Log in',
22
+ errorMessage,
23
+ onClearError,
24
+ }: LoginWindowProps) {
25
+ const [password, setPassword] = useState('');
26
+ const [isSubmitting, setIsSubmitting] = useState(false);
27
+
28
+ const handleSubmit = useCallback(
29
+ (e: React.FormEvent) => {
30
+ e.preventDefault();
31
+ onClearError?.();
32
+ if (!password) return;
33
+ setIsSubmitting(true);
34
+ onLogin(password)
35
+ .then(() => {
36
+ setPassword('');
37
+ })
38
+ .catch(() => {})
39
+ .finally(() => {
40
+ setIsSubmitting(false);
41
+ });
42
+ },
43
+ [onLogin, password, onClearError]
44
+ );
45
+
46
+ return (
47
+ <div className="loginWindowOverlay" role="dialog" aria-modal="true" aria-labelledby="loginWindowTitle">
48
+ <div className="loginWindow">
49
+ <div className="loginWindowHeader">
50
+ <Lock size={20} className="loginWindowHeaderIcon" aria-hidden />
51
+ <h2 id="loginWindowTitle" className="loginWindowTitle">
52
+ {title}
53
+ </h2>
54
+ </div>
55
+ <form onSubmit={handleSubmit} className="loginWindowForm">
56
+ {errorMessage && (
57
+ <div className="loginWindowError" role="alert">
58
+ {errorMessage}
59
+ </div>
60
+ )}
61
+ <label className="loginWindowLabel">
62
+ <Lock size={16} className="loginWindowLabelIcon" aria-hidden />
63
+ <span>Password</span>
64
+ <input
65
+ type="password"
66
+ className="loginWindowInput"
67
+ value={password}
68
+ onChange={(e) => {
69
+ setPassword(e.target.value);
70
+ onClearError?.();
71
+ }}
72
+ autoComplete="current-password"
73
+ placeholder="••••••••"
74
+ disabled={isSubmitting}
75
+ autoFocus
76
+ />
77
+ </label>
78
+ <button
79
+ type="submit"
80
+ className="loginWindowSubmit"
81
+ disabled={isSubmitting || !password}
82
+ >
83
+ <LogIn size={16} aria-hidden />
84
+ {isSubmitting ? '...' : submitLabel}
85
+ </button>
86
+ </form>
87
+ </div>
88
+ </div>
89
+ );
90
+ }
@@ -0,0 +1,2 @@
1
+ export { LoginWindow } from './LoginWindow';
2
+ export type { LoginWindowProps } from './LoginWindow';