@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,414 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { createContext, useContext, useState, useCallback, useMemo, useId, useRef, useEffect, } from 'react';
3
+ import { createPortal } from 'react-dom';
4
+ import { createPrinter } from '@yahman/core';
5
+ import { ApolloClient, ApolloProvider, InMemoryCache, } from '@apollo/client';
6
+ import { getOperationAST, print } from 'graphql';
7
+ import { ApiProvider, ApiContext } from './ApiContext';
8
+ import { PrinterProvider } from './PrinterContext';
9
+ import { useLogWindowShortcut } from '../hooks/useLogWindowShortcut';
10
+ import { LogWindow } from '../components/LogWindow/LogWindow';
11
+ import '../components/LogWindow/LogWindow.css';
12
+ const AUTH_STORAGE_KEY = 'yahman_auth_token';
13
+ const DEFAULT_TOKEN_TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7日
14
+ function isProductionOrStaging() {
15
+ if (typeof process === 'undefined')
16
+ return false;
17
+ const env = process.env?.NODE_ENV;
18
+ return env === 'production' || env === 'staging';
19
+ }
20
+ function isStorybook() {
21
+ if (typeof window === 'undefined')
22
+ return false;
23
+ const w = window;
24
+ return w.IS_STORYBOOK === true || Boolean(w.__STORYBOOK_CLIENT_API__);
25
+ }
26
+ function storybookSimulateProduction(prop) {
27
+ if (typeof window === 'undefined')
28
+ return false;
29
+ const w = window;
30
+ return prop === true || w.__YAHMAN_STORYBOOK_SIMULATE_PRODUCTION__ === true;
31
+ }
32
+ function getCurrentDomain() {
33
+ if (typeof window === 'undefined')
34
+ return '';
35
+ return window.location.hostname || '';
36
+ }
37
+ function loadPersistedAuth() {
38
+ if (typeof localStorage === 'undefined')
39
+ return false;
40
+ try {
41
+ const raw = localStorage.getItem(AUTH_STORAGE_KEY);
42
+ if (!raw)
43
+ return false;
44
+ const data = JSON.parse(raw);
45
+ if (data &&
46
+ typeof data === 'object' &&
47
+ 'token' in data &&
48
+ 'domain' in data &&
49
+ 'expiresAt' in data &&
50
+ typeof data.domain === 'string' &&
51
+ typeof data.expiresAt === 'number') {
52
+ const { domain, expiresAt } = data;
53
+ const now = Date.now();
54
+ return domain === getCurrentDomain() && expiresAt > now;
55
+ }
56
+ return false;
57
+ }
58
+ catch {
59
+ return false;
60
+ }
61
+ }
62
+ function persistAuth(value, ttlMs = DEFAULT_TOKEN_TTL_MS) {
63
+ if (typeof localStorage === 'undefined')
64
+ return;
65
+ try {
66
+ if (value) {
67
+ const token = typeof crypto !== 'undefined' && crypto.randomUUID
68
+ ? crypto.randomUUID()
69
+ : `t-${Date.now()}-${Math.random().toString(36).slice(2)}`;
70
+ const domain = getCurrentDomain();
71
+ const expiresAt = Date.now() + ttlMs;
72
+ const data = { token, domain, expiresAt };
73
+ localStorage.setItem(AUTH_STORAGE_KEY, JSON.stringify(data));
74
+ }
75
+ else {
76
+ localStorage.removeItem(AUTH_STORAGE_KEY);
77
+ }
78
+ }
79
+ catch {
80
+ // ignore
81
+ }
82
+ }
83
+ /** デフォルトパスワード "12345678" の SHA-256 (hex) */
84
+ const DEFAULT_PASSWORD_HASH = 'ef797c8118f02dfb649607dd5d3f8c7623048c9c063d532cc95c5ed7a898a64f';
85
+ /**
86
+ * 環境変数からログイン用パスワードハッシュを取得。未設定時はデフォルト(12345678 の SHA-256)。
87
+ * パスワードを変更する場合は YAHMAN_LOGIN_PASSWORD_HASH または VITE_YAHMAN_LOGIN_PASSWORD_HASH に、
88
+ * 希望するパスワードの SHA-256 ハッシュ(hex)を設定する。
89
+ */
90
+ function getAcceptedPasswordHash() {
91
+ const fromProcess = typeof process !== 'undefined' && process.env?.YAHMAN_LOGIN_PASSWORD_HASH
92
+ ? String(process.env.YAHMAN_LOGIN_PASSWORD_HASH).trim()
93
+ : '';
94
+ const meta = typeof import.meta !== 'undefined' ? import.meta : null;
95
+ const viteHash = meta?.env?.VITE_YAHMAN_LOGIN_PASSWORD_HASH;
96
+ const fromMeta = viteHash ? String(viteHash).trim() : '';
97
+ const fromEnv = fromProcess || fromMeta;
98
+ return fromEnv || DEFAULT_PASSWORD_HASH;
99
+ }
100
+ /** 文字列を SHA-256 でハッシュし hex 文字列で返す */
101
+ async function sha256Hex(str) {
102
+ const buf = new TextEncoder().encode(str);
103
+ const hash = typeof crypto !== 'undefined' && crypto.subtle
104
+ ? await crypto.subtle.digest('SHA-256', buf)
105
+ : new Uint8Array(0);
106
+ return Array.from(new Uint8Array(hash))
107
+ .map((b) => b.toString(16).padStart(2, '0'))
108
+ .join('');
109
+ }
110
+ function resolveBody(body) {
111
+ if (body == null)
112
+ return undefined;
113
+ if (typeof body === 'string' ||
114
+ body instanceof ArrayBuffer ||
115
+ ArrayBuffer.isView(body) ||
116
+ body instanceof FormData ||
117
+ body instanceof URLSearchParams) {
118
+ return body;
119
+ }
120
+ return JSON.stringify(body);
121
+ }
122
+ const YahmanContext = createContext(null);
123
+ /** 認証有効時の子要素ラッパー(ログアウトは LogWindow 内のボタンから行う) */
124
+ function AuthEscapeToLogin({ children }) {
125
+ return _jsx(_Fragment, { children: children });
126
+ }
127
+ /** 認証時はログウィンドウをポータル表示。未認証時は LogWindow 内にパスワード画面(production/staging のみ) */
128
+ function LogWindowGate({ instanceId, defaultPosition, loginTitle, isAuthenticated, login, logout, loginError, clearLoginError, logEntries, networkEntries, isLogWindowOpen, closeLogWindow, }) {
129
+ if (!isLogWindowOpen || typeof document === 'undefined') {
130
+ return null;
131
+ }
132
+ return createPortal(_jsx("div", { onClick: (e) => e.stopPropagation(), children: _jsx(LogWindow, { entries: logEntries, networkEntries: networkEntries, draggable: true, animateOnOpen: true, onClose: closeLogWindow, onLogout: isAuthenticated ? logout : undefined, defaultPosition: defaultPosition, showLogin: !isAuthenticated, loginTitle: loginTitle, onLogin: !isAuthenticated ? login : undefined, loginError: loginError, onClearLoginError: clearLoginError }, instanceId) }), document.body);
133
+ }
134
+ function generateId() {
135
+ return typeof crypto !== 'undefined' && crypto.randomUUID
136
+ ? crypto.randomUUID()
137
+ : `id-${Math.random().toString(36).slice(2)}`;
138
+ }
139
+ export function YahmanProvider({ children, api, printer = {}, logWindow, auth, }) {
140
+ const [logEntries, setLogEntries] = useState([]);
141
+ const [networkEntries, setNetworkEntries] = useState([]);
142
+ const instanceId = useId();
143
+ const simulateProduction = auth && typeof auth === 'object' && isStorybook() && storybookSimulateProduction(auth.storybookSimulateProduction);
144
+ const requiresAuth = auth && (auth === true || (typeof auth === 'object' && (auth.productionOnly !== false)))
145
+ ? isProductionOrStaging() || !!simulateProduction
146
+ : false;
147
+ const [isAuthenticated, setIsAuthenticated] = useState(() => auth && requiresAuth ? loadPersistedAuth() : false);
148
+ const [loginError, setLoginError] = useState(undefined);
149
+ useEffect(() => {
150
+ if (!requiresAuth)
151
+ return;
152
+ setIsAuthenticated(loadPersistedAuth());
153
+ }, [requiresAuth]);
154
+ const onLoginProp = auth && typeof auth === 'object' ? auth.onLogin : undefined;
155
+ const authPasswordHash = auth && typeof auth === 'object' && auth.passwordHash?.trim()
156
+ ? auth.passwordHash.trim()
157
+ : '';
158
+ const tokenTtlMs = auth && typeof auth === 'object' && auth.tokenTtlMs != null
159
+ ? auth.tokenTtlMs
160
+ : DEFAULT_TOKEN_TTL_MS;
161
+ const login = useCallback(async (password) => {
162
+ if (onLoginProp) {
163
+ await onLoginProp(password);
164
+ setLoginError(undefined);
165
+ setIsAuthenticated(true);
166
+ persistAuth(true, tokenTtlMs);
167
+ }
168
+ else {
169
+ if (!password)
170
+ throw new Error('Password is required.');
171
+ const inputHash = await sha256Hex(password);
172
+ const accepted = authPasswordHash || getAcceptedPasswordHash();
173
+ if (inputHash.toLowerCase() !== accepted.toLowerCase()) {
174
+ setLoginError('Invalid password.');
175
+ }
176
+ else {
177
+ setLoginError(undefined);
178
+ setIsAuthenticated(true);
179
+ persistAuth(true, tokenTtlMs);
180
+ }
181
+ }
182
+ }, [onLoginProp, authPasswordHash, tokenTtlMs]);
183
+ const logout = useCallback(() => {
184
+ setIsAuthenticated(false);
185
+ persistAuth(false);
186
+ }, []);
187
+ const handleLogin = useCallback(async (password) => {
188
+ setLoginError(undefined);
189
+ try {
190
+ await login(password);
191
+ }
192
+ catch (err) {
193
+ const message = err instanceof Error ? err.message : 'Login failed.';
194
+ setLoginError(message);
195
+ throw err;
196
+ }
197
+ }, [login]);
198
+ const clearLoginError = useCallback(() => setLoginError(undefined), []);
199
+ const addLogEntry = useCallback((entry) => {
200
+ const id = 'id' in entry && entry.id ? entry.id : generateId();
201
+ setLogEntries((prev) => [...prev, { ...entry, id }]);
202
+ }, []);
203
+ const addLogEntryRef = useRef(addLogEntry);
204
+ useEffect(() => {
205
+ addLogEntryRef.current = addLogEntry;
206
+ }, [addLogEntry]);
207
+ const addNetworkEntry = useCallback((entry) => {
208
+ const id = 'id' in entry && entry.id ? entry.id : generateId();
209
+ setNetworkEntries((prev) => [...prev, { ...entry, id }]);
210
+ }, []);
211
+ const env = printer.env ?? 'local';
212
+ const wrappedPrinter = useMemo(() => {
213
+ const base = createPrinter(env);
214
+ return {
215
+ info: (msg) => {
216
+ base.info(msg);
217
+ addLogEntryRef.current({ level: 'info', message: msg, source: 'app' });
218
+ },
219
+ warn: (msg) => {
220
+ base.warn(msg);
221
+ addLogEntryRef.current({ level: 'warn', message: msg, source: 'app' });
222
+ },
223
+ error: (msg) => {
224
+ base.error(msg);
225
+ addLogEntryRef.current({ level: 'error', message: msg, source: 'app' });
226
+ },
227
+ };
228
+ }, [env]);
229
+ const restBaseUrl = api.provider === 'rest'
230
+ ? (api.baseUrl ?? (typeof process !== 'undefined' ? process.env?.ENDPOINT_URL : '') ?? '')
231
+ : '';
232
+ const makeRestRequest = useCallback((method, path, body, options) => {
233
+ const url = restBaseUrl + path;
234
+ const isJson = body != null &&
235
+ typeof body === 'object' &&
236
+ !(body instanceof FormData) &&
237
+ !(body instanceof URLSearchParams);
238
+ const init = {
239
+ ...options,
240
+ method,
241
+ body: resolveBody(body),
242
+ headers: isJson ? { 'Content-Type': 'application/json', ...options?.headers } : options?.headers,
243
+ };
244
+ const requestStr = method === 'GET' || method === 'DELETE'
245
+ ? `${method} ${path}`
246
+ : body != null
247
+ ? typeof body === 'object' && !(body instanceof FormData) && !(body instanceof URLSearchParams)
248
+ ? JSON.stringify(body)
249
+ : String(body)
250
+ : `${method} ${path}`;
251
+ const addEntry = (status, statusText, response) => {
252
+ addNetworkEntry({
253
+ type: 'rest',
254
+ method,
255
+ url,
256
+ status,
257
+ statusText,
258
+ request: requestStr,
259
+ response,
260
+ });
261
+ };
262
+ return fetch(url, init)
263
+ .then(async (res) => {
264
+ const text = await res.clone().text().catch(() => '(read failed)');
265
+ addEntry(res.status, res.statusText, text);
266
+ return res;
267
+ })
268
+ .catch((err) => {
269
+ addEntry(0, 'Error', String(err?.message ?? err));
270
+ throw err;
271
+ });
272
+ }, [restBaseUrl, addNetworkEntry]);
273
+ const wrappedRestApi = useMemo(() => ({
274
+ provider: 'rest',
275
+ get: (path, options) => makeRestRequest('GET', path, undefined, options),
276
+ post: (path, body, options) => makeRestRequest('POST', path, body, options),
277
+ put: (path, body, options) => makeRestRequest('PUT', path, body, options),
278
+ delete: (path, options) => makeRestRequest('DELETE', path, undefined, options),
279
+ }), [makeRestRequest]);
280
+ const graphqlUri = api.provider === 'graphql'
281
+ ? (api.uri ?? (typeof process !== 'undefined' ? process.env?.GRAPHQL_URI : '') ?? '')
282
+ : '';
283
+ const graphqlClient = useMemo(() => {
284
+ if (api.provider !== 'graphql')
285
+ return null;
286
+ const clientOpt = api.client;
287
+ if (clientOpt)
288
+ return clientOpt;
289
+ return new ApolloClient({
290
+ uri: graphqlUri || '/graphql',
291
+ cache: new InMemoryCache(),
292
+ });
293
+ }, [api, graphqlUri]);
294
+ const wrappedGraphqlApi = useMemo(() => {
295
+ if (!graphqlClient || api.provider !== 'graphql')
296
+ return null;
297
+ const url = graphqlUri || '/graphql';
298
+ return {
299
+ provider: 'graphql',
300
+ ransack: (options) => {
301
+ const requestStr = JSON.stringify({
302
+ query: print(options.query),
303
+ variables: options.variables,
304
+ });
305
+ const op = getOperationAST(options.query);
306
+ const operationName = op?.name?.value ?? 'Query';
307
+ return graphqlClient
308
+ .query(options)
309
+ .then((result) => {
310
+ addNetworkEntry({
311
+ type: 'graphql',
312
+ operationName,
313
+ url,
314
+ status: 200,
315
+ statusText: 'OK',
316
+ request: requestStr,
317
+ response: JSON.stringify(result.data ?? result),
318
+ });
319
+ return result;
320
+ })
321
+ .catch((err) => {
322
+ addNetworkEntry({
323
+ type: 'graphql',
324
+ operationName,
325
+ url,
326
+ status: 0,
327
+ statusText: 'Error',
328
+ request: requestStr,
329
+ response: String(err?.message ?? err),
330
+ });
331
+ throw err;
332
+ });
333
+ },
334
+ handing: (options) => {
335
+ const requestStr = JSON.stringify({
336
+ mutation: print(options.mutation),
337
+ variables: options.variables,
338
+ });
339
+ const op = getOperationAST(options.mutation);
340
+ const operationName = op?.name?.value ?? 'Mutation';
341
+ return graphqlClient
342
+ .mutate(options)
343
+ .then((result) => {
344
+ addNetworkEntry({
345
+ type: 'graphql',
346
+ operationName,
347
+ url,
348
+ status: 200,
349
+ statusText: 'OK',
350
+ request: requestStr,
351
+ response: JSON.stringify(result.data ?? result),
352
+ });
353
+ return result;
354
+ })
355
+ .catch((err) => {
356
+ addNetworkEntry({
357
+ type: 'graphql',
358
+ operationName,
359
+ url,
360
+ status: 0,
361
+ statusText: 'Error',
362
+ request: requestStr,
363
+ response: String(err?.message ?? err),
364
+ });
365
+ throw err;
366
+ });
367
+ },
368
+ };
369
+ }, [graphqlClient, api.provider, graphqlUri, addNetworkEntry]);
370
+ const logWindowShortcut = useLogWindowShortcut(logWindow
371
+ ? {
372
+ escapeCount: 5,
373
+ resetAfterMs: 1500,
374
+ closeOnEscape: true,
375
+ }
376
+ : { escapeCount: 5, resetAfterMs: 1500 });
377
+ const { isOpen: isLogWindowOpen, close: closeLogWindow, open: openLogWindow } = logWindowShortcut;
378
+ const defaultLogWindowPosition = useMemo(() => ({
379
+ x: typeof window !== 'undefined' ? Math.max(0, (window.innerWidth - 696) / 2) : 100,
380
+ y: typeof window !== 'undefined' ? Math.max(0, (window.innerHeight - 466) / 2) : 100,
381
+ }), []);
382
+ const yahmanValue = useMemo(() => ({
383
+ addLogEntry,
384
+ addNetworkEntry,
385
+ openLogWindow,
386
+ closeLogWindow,
387
+ logEntries,
388
+ networkEntries,
389
+ isLogWindowOpen,
390
+ }), [
391
+ addLogEntry,
392
+ addNetworkEntry,
393
+ openLogWindow,
394
+ closeLogWindow,
395
+ logEntries,
396
+ networkEntries,
397
+ isLogWindowOpen,
398
+ ]);
399
+ const apiElement = api.provider === 'rest' ? (_jsx(ApiContext.Provider, { value: wrappedRestApi, children: children })) : wrappedGraphqlApi && graphqlClient ? (_jsx(ApolloProvider, { client: graphqlClient, children: _jsx(ApiContext.Provider, { value: wrappedGraphqlApi, children: children }) })) : (_jsx(ApiProvider, { provider: "graphql", uri: api.uri, client: api.client, children: children }));
400
+ /** 認証なしのときのみここでログウィンドウを表示。認証ありのときは LogWindowGate で表示 */
401
+ const logWindowElement = !auth &&
402
+ isLogWindowOpen &&
403
+ typeof document !== 'undefined' &&
404
+ createPortal(_jsx("div", { onClick: (e) => e.stopPropagation(), children: _jsx(LogWindow, { entries: logEntries, networkEntries: networkEntries, draggable: true, animateOnOpen: true, onClose: closeLogWindow, defaultPosition: defaultLogWindowPosition }, instanceId) }), document.body);
405
+ const content = (_jsx(PrinterProvider, { env: env, printer: wrappedPrinter, children: apiElement }));
406
+ return (_jsxs(YahmanContext.Provider, { value: yahmanValue, children: [auth ? (_jsxs(_Fragment, { children: [_jsx(AuthEscapeToLogin, { children: content }), _jsx(LogWindowGate, { instanceId: instanceId, defaultPosition: defaultLogWindowPosition, loginTitle: typeof auth === 'object' ? auth.loginTitle : undefined, isAuthenticated: isAuthenticated, login: handleLogin, logout: logout, loginError: loginError, clearLoginError: clearLoginError, logEntries: logEntries, networkEntries: networkEntries, isLogWindowOpen: isLogWindowOpen, closeLogWindow: closeLogWindow })] })) : (content), logWindowElement] }));
407
+ }
408
+ export function useYahman() {
409
+ const ctx = useContext(YahmanContext);
410
+ if (!ctx) {
411
+ throw new Error('useYahman must be used within YahmanProvider');
412
+ }
413
+ return ctx;
414
+ }
@@ -0,0 +1,16 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { YahmanProvider } from './YahmanContext';
3
+ declare const meta: Meta<typeof YahmanProvider>;
4
+ export default meta;
5
+ type Story = StoryObj<typeof YahmanProvider>;
6
+ /** Api (REST) + Printer + LogWindow(Escape 5回で表示)。REST は JSONPlaceholder /posts を使用 */
7
+ export declare const Default: Story;
8
+ /**
9
+ * 本番時のみ認証(auth: true)。
10
+ * storybookSimulateProduction: true で Storybook 内だけ本番扱いし、ログイン画面を表示。
11
+ * 本番ビルド時は NODE_ENV=production で同様にログイン要求。
12
+ */
13
+ export declare const WithAuthProductionOnly: Story;
14
+ /** GraphQL: useApi().ransack (QUERY) / .handing (MUTATION) → Network */
15
+ export declare const GraphQL: Story;
16
+ //# sourceMappingURL=YahmanContext.stories.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"YahmanContext.stories.d.ts","sourceRoot":"","sources":["../../src/contexts/YahmanContext.stories.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAEvD,OAAO,EAAE,cAAc,EAAa,MAAM,iBAAiB,CAAC;AA4F5D,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,OAAO,cAAc,CAKrC,CAAC;AAEF,eAAe,IAAI,CAAC;AAEpB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,cAAc,CAAC,CAAC;AAE7C,uFAAuF;AACvF,eAAO,MAAM,OAAO,EAAE,KAUrB,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,EAAE,KAWpC,CAAC;AAEF,wEAAwE;AACxE,eAAO,MAAM,OAAO,EAAE,KAUrB,CAAC"}
@@ -0,0 +1,64 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { gql } from '@apollo/client';
3
+ import { YahmanProvider, useYahman } from './YahmanContext';
4
+ import { usePrinter } from './PrinterContext';
5
+ import { useApi } from './ApiContext';
6
+ function DemoContent() {
7
+ const { openLogWindow } = useYahman();
8
+ const printer = usePrinter();
9
+ const api = useApi();
10
+ const addInfo = () => printer.info('Info from usePrinter');
11
+ const addWarn = () => printer.warn('Warn from usePrinter');
12
+ const addError = () => printer.error('Error from usePrinter');
13
+ if (api.provider === 'rest') {
14
+ return (_jsxs("div", { style: { padding: 24, fontFamily: 'sans-serif' }, children: [_jsx("h3", { children: "YahmanProvider \u30C7\u30E2 (REST)" }), _jsx("p", { children: "Escape \u30AD\u30FC\u3092 5 \u56DE\u62BC\u3059\u3068\u30ED\u30B0\u30A6\u30A3\u30F3\u30C9\u30A6\u304C\u958B\u304D\u307E\u3059\u3002" }), _jsx("p", { style: { fontSize: 12, color: '#666', marginTop: 8 }, children: "usePrinter().info / warn / error \u2192 Log \u30BF\u30D6\u3002useApi().get / post / put / delete \u2192 Network \u30BF\u30D6\u306B\u53CD\u6620\u3002" }), _jsxs("div", { style: { display: 'flex', gap: 8, flexWrap: 'wrap', marginTop: 16 }, children: [_jsx("button", { type: "button", onClick: addInfo, children: "Log: Info" }), _jsx("button", { type: "button", onClick: addWarn, children: "Log: Warn" }), _jsx("button", { type: "button", onClick: addError, children: "Log: Error" }), _jsx("button", { type: "button", onClick: () => api.get('/posts').then((res) => printer.info(`Response: ${res.status} ${res.statusText}`)).catch((err) => printer.error(`Error: ${err.message}`)), children: "Network: GET /posts" }), _jsx("button", { type: "button", onClick: () => api
15
+ .post('/posts', { title: 'Storybook post', body: 'Body from demo', userId: 1 })
16
+ .then((res) => printer.info(`Response: ${res.status} ${res.statusText}`))
17
+ .catch((err) => printer.error(`Error: ${err.message}`)), children: "Network: POST /posts" }), _jsx("button", { type: "button", onClick: () => api
18
+ .put('/posts/1', { id: 1, title: 'Updated title', body: 'Updated body', userId: 1 })
19
+ .then((res) => printer.info(`Response: ${res.status} ${res.statusText}`))
20
+ .catch((err) => printer.error(`Error: ${err.message}`)), children: "Network: PUT /posts/1" }), _jsx("button", { type: "button", onClick: () => api.delete('/posts/1').then((res) => printer.info(`Response: ${res.status} ${res.statusText}`)).catch(() => { }), children: "Network: DELETE /posts/1" }), _jsx("button", { type: "button", onClick: openLogWindow, children: "\u30ED\u30B0\u30A6\u30A3\u30F3\u30C9\u30A6\u3092\u958B\u304F" })] })] }));
21
+ }
22
+ const runQuery = () => {
23
+ api
24
+ .ransack({
25
+ query: gql `
26
+ query GetUser { user(id: "1") { id name } }
27
+ `,
28
+ })
29
+ .catch((err) => printer.error(`Error: ${err.message}`));
30
+ };
31
+ const runMutation = () => {
32
+ api
33
+ .handing({
34
+ mutation: gql `
35
+ mutation UpdateUser { updateUser(id: "1", name: "x") { id name } }
36
+ `,
37
+ })
38
+ .catch((err) => printer.error(`Error: ${err.message}`));
39
+ };
40
+ return (_jsxs("div", { style: { padding: 24, fontFamily: 'sans-serif' }, children: [_jsx("h3", { children: "YahmanProvider \u30C7\u30E2 (GraphQL)" }), _jsx("p", { children: "useApi().ransack (QUERY) / .handing (MUTATION) \u3067 Network \u30BF\u30D6\u306B\u53CD\u6620\u3002" }), _jsxs("div", { style: { display: 'flex', gap: 8, flexWrap: 'wrap', marginTop: 16 }, children: [_jsx("button", { type: "button", onClick: addInfo, children: "Log: Info" }), _jsx("button", { type: "button", onClick: runQuery, children: "Network: ransack (QUERY)" }), _jsx("button", { type: "button", onClick: runMutation, children: "Network: handing (MUTATION)" }), _jsx("button", { type: "button", onClick: openLogWindow, children: "\u30ED\u30B0\u30A6\u30A3\u30F3\u30C9\u30A6\u3092\u958B\u304F" })] })] }));
41
+ }
42
+ const meta = {
43
+ title: 'Contexts/YahmanProvider',
44
+ component: YahmanProvider,
45
+ parameters: { layout: 'centered' },
46
+ tags: ['autodocs'],
47
+ };
48
+ export default meta;
49
+ /** Api (REST) + Printer + LogWindow(Escape 5回で表示)。REST は JSONPlaceholder /posts を使用 */
50
+ export const Default = {
51
+ render: () => (_jsx(YahmanProvider, { api: { provider: 'rest', baseUrl: 'https://jsonplaceholder.typicode.com' }, printer: { env: 'local' }, logWindow: {}, children: _jsx(DemoContent, {}) })),
52
+ };
53
+ /**
54
+ * 本番時のみ認証(auth: true)。
55
+ * storybookSimulateProduction: true で Storybook 内だけ本番扱いし、ログイン画面を表示。
56
+ * 本番ビルド時は NODE_ENV=production で同様にログイン要求。
57
+ */
58
+ export const WithAuthProductionOnly = {
59
+ render: () => (_jsx(YahmanProvider, { api: { provider: 'rest', baseUrl: 'https://jsonplaceholder.typicode.com' }, printer: { env: 'local' }, logWindow: {}, auth: { storybookSimulateProduction: true }, children: _jsx(DemoContent, {}) })),
60
+ };
61
+ /** GraphQL: useApi().ransack (QUERY) / .handing (MUTATION) → Network */
62
+ export const GraphQL = {
63
+ render: () => (_jsx(YahmanProvider, { api: { provider: 'graphql', uri: 'https://api.example.com/graphql' }, printer: { env: 'local' }, logWindow: {}, children: _jsx(DemoContent, {}) })),
64
+ };
@@ -0,0 +1,69 @@
1
+ import React from 'react';
2
+ import { ApolloClient } from '@apollo/client';
3
+ import type { LogEntry, NetworkEntry } from '../components/LogWindow/types';
4
+ import type { Env } from '@yargram/core';
5
+ import type { NormalizedCacheObject } from '@apollo/client';
6
+ import '../components/LogWindow/LogWindow.css';
7
+ /** Api 設定(ApiProvider の props から children を除いたもの) */
8
+ type YargramApiConfig = {
9
+ provider: 'rest';
10
+ baseUrl?: string;
11
+ } | {
12
+ provider: 'graphql';
13
+ uri?: string;
14
+ client?: ApolloClient<NormalizedCacheObject>;
15
+ };
16
+ /** Printer 設定 */
17
+ type YargramPrinterConfig = {
18
+ env?: Env;
19
+ };
20
+ /** LogWindow 設定(Escape 5 回で表示) */
21
+ type YargramLogWindowConfig = Record<string, never>;
22
+ /** 認証設定。本番のみログインを要求する場合は true、カスタム時はオブジェクト */
23
+ type YargramAuthConfig = true | {
24
+ /** 本番時のみ認証(デフォルト true) */
25
+ productionOnly?: boolean;
26
+ /** Storybook 時のみ本番として扱う(ログイン要求する) */
27
+ storybookSimulateProduction?: boolean;
28
+ /** カスタム認証(指定時は onLogin で検証。未指定時は passwordHash または env でハッシュ比較) */
29
+ onLogin?: (password: string) => Promise<void>;
30
+ /** ログインウィンドウのタイトル */
31
+ loginTitle?: string;
32
+ /**
33
+ * ログイン用パスワードの SHA-256 ハッシュ(hex)。指定時は env より優先。
34
+ * 未指定時は YAHMAN_LOGIN_PASSWORD_HASH / VITE_YAHMAN_LOGIN_PASSWORD_HASH、それも無ければデフォルト(12345678 のハッシュ)。
35
+ */
36
+ passwordHash?: string;
37
+ /**
38
+ * ログイン後のトークン有効期限(ミリ秒)。未指定時は 7 日。
39
+ */
40
+ tokenTtlMs?: number;
41
+ };
42
+ type YargramContextValue = {
43
+ addLogEntry: (entry: Omit<LogEntry, 'id'> | LogEntry) => void;
44
+ addNetworkEntry: (entry: Omit<NetworkEntry, 'id'> | NetworkEntry) => void;
45
+ openLogWindow: () => void;
46
+ closeLogWindow: () => void;
47
+ logEntries: LogEntry[];
48
+ networkEntries: NetworkEntry[];
49
+ isLogWindowOpen: boolean;
50
+ };
51
+ export type YargramProviderProps = {
52
+ children: React.ReactNode;
53
+ /** Api 設定(REST または GraphQL) */
54
+ api: YargramApiConfig;
55
+ /** Printer 設定 */
56
+ printer?: YargramPrinterConfig;
57
+ /** LogWindow を Escape で出せるようにする設定。省略時は LogWindow 機能なし */
58
+ logWindow?: YargramLogWindowConfig;
59
+ /**
60
+ * 認証を有効にする。true のとき本番(NODE_ENV=production/staging)のみログインウィンドウを表示。
61
+ * オブジェクトで productionOnly / onLogin / loginTitle / passwordHash を指定可能。
62
+ * パスワードハッシュは auth.passwordHash → env(YAHMAN_LOGIN_PASSWORD_HASH 等)→ デフォルト(12345678 の SHA-256)の順で解決される。
63
+ */
64
+ auth?: YargramAuthConfig;
65
+ };
66
+ export declare function YargramProvider({ children, api, printer, logWindow, auth, }: YargramProviderProps): import("react/jsx-runtime").JSX.Element;
67
+ export declare function useYargram(): YargramContextValue;
68
+ export {};
69
+ //# sourceMappingURL=YargramContext.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"YargramContext.d.ts","sourceRoot":"","sources":["../../src/contexts/YargramContext.tsx"],"names":[],"mappings":"AAAA,OAAO,KASN,MAAM,OAAO,CAAC;AAGf,OAAO,EACL,YAAY,EAGb,MAAM,gBAAgB,CAAC;AAOxB,OAAO,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAC5E,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,KAAK,EAEV,qBAAqB,EAItB,MAAM,gBAAgB,CAAC;AACxB,OAAO,uCAAuC,CAAC;AAiI/C,qDAAqD;AACrD,KAAK,gBAAgB,GACjB;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GACtC;IACE,QAAQ,EAAE,SAAS,CAAC;IACpB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,YAAY,CAAC,qBAAqB,CAAC,CAAC;CAC9C,CAAC;AAEN,iBAAiB;AACjB,KAAK,oBAAoB,GAAG;IAC1B,GAAG,CAAC,EAAE,GAAG,CAAC;CACX,CAAC;AAEF,kCAAkC;AAClC,KAAK,sBAAsB,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;AAEpD,8CAA8C;AAC9C,KAAK,iBAAiB,GAClB,IAAI,GACJ;IACE,0BAA0B;IAC1B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,qCAAqC;IACrC,2BAA2B,CAAC,EAAE,OAAO,CAAC;IACtC,kEAAkE;IAClE,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9C,qBAAqB;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEN,KAAK,mBAAmB,GAAG;IACzB,WAAW,EAAE,CAAC,KAAK,EAAE,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,GAAG,QAAQ,KAAK,IAAI,CAAC;IAC9D,eAAe,EAAE,CAAC,KAAK,EAAE,IAAI,CAAC,YAAY,EAAE,IAAI,CAAC,GAAG,YAAY,KAAK,IAAI,CAAC;IAC1E,aAAa,EAAE,MAAM,IAAI,CAAC;IAC1B,cAAc,EAAE,MAAM,IAAI,CAAC;IAC3B,UAAU,EAAE,QAAQ,EAAE,CAAC;IACvB,cAAc,EAAE,YAAY,EAAE,CAAC;IAC/B,eAAe,EAAE,OAAO,CAAC;CAC1B,CAAC;AAIF,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,+BAA+B;IAC/B,GAAG,EAAE,gBAAgB,CAAC;IACtB,iBAAiB;IACjB,OAAO,CAAC,EAAE,oBAAoB,CAAC;IAC/B,yDAAyD;IACzD,SAAS,CAAC,EAAE,sBAAsB,CAAC;IACnC;;;;OAIG;IACH,IAAI,CAAC,EAAE,iBAAiB,CAAC;CAC1B,CAAC;AAkEF,wBAAgB,eAAe,CAAC,EAC9B,QAAQ,EACR,GAAG,EACH,OAAY,EACZ,SAAS,EACT,IAAI,GACL,EAAE,oBAAoB,2CAgXtB;AAED,wBAAgB,UAAU,IAAI,mBAAmB,CAMhD"}