@umituz/web-design-system 1.0.4 → 1.3.1

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 (41) hide show
  1. package/README.md +181 -0
  2. package/package.json +18 -5
  3. package/src/global.d.ts +20 -0
  4. package/src/index.ts +3 -0
  5. package/src/infrastructure/error/ErrorBoundary.tsx +258 -0
  6. package/src/infrastructure/error/ErrorDisplay.tsx +346 -0
  7. package/src/infrastructure/error/SuspenseWrapper.tsx +134 -0
  8. package/src/infrastructure/error/index.ts +5 -0
  9. package/src/infrastructure/performance/index.ts +6 -0
  10. package/src/infrastructure/performance/useLazyLoading.ts +342 -0
  11. package/src/infrastructure/performance/useMemoryOptimization.ts +293 -0
  12. package/src/infrastructure/performance/usePerformanceMonitor.ts +158 -0
  13. package/src/infrastructure/security/index.ts +10 -0
  14. package/src/infrastructure/security/security-config.ts +171 -0
  15. package/src/infrastructure/security/useFormValidation.ts +216 -0
  16. package/src/infrastructure/security/validation.ts +242 -0
  17. package/src/infrastructure/utils/cn.util.ts +7 -7
  18. package/src/infrastructure/utils/index.ts +1 -1
  19. package/src/presentation/atoms/Input.tsx +1 -1
  20. package/src/presentation/atoms/Slider.tsx +1 -1
  21. package/src/presentation/atoms/Text.tsx +1 -1
  22. package/src/presentation/atoms/Tooltip.tsx +2 -2
  23. package/src/presentation/hooks/index.ts +2 -1
  24. package/src/presentation/hooks/useClickOutside.ts +1 -1
  25. package/src/presentation/molecules/CheckboxGroup.tsx +1 -1
  26. package/src/presentation/molecules/FormField.tsx +2 -2
  27. package/src/presentation/molecules/InputGroup.tsx +1 -1
  28. package/src/presentation/molecules/RadioGroup.tsx +1 -1
  29. package/src/presentation/organisms/Alert.tsx +1 -1
  30. package/src/presentation/organisms/Card.tsx +1 -1
  31. package/src/presentation/organisms/Footer.tsx +99 -0
  32. package/src/presentation/organisms/Navbar.tsx +1 -1
  33. package/src/presentation/organisms/Table.tsx +1 -1
  34. package/src/presentation/organisms/index.ts +3 -0
  35. package/src/presentation/templates/Form.tsx +2 -2
  36. package/src/presentation/templates/List.tsx +1 -1
  37. package/src/presentation/templates/PageHeader.tsx +78 -0
  38. package/src/presentation/templates/PageLayout.tsx +38 -0
  39. package/src/presentation/templates/ProjectSkeleton.tsx +35 -0
  40. package/src/presentation/templates/Section.tsx +1 -1
  41. package/src/presentation/templates/index.ts +9 -0
@@ -0,0 +1,342 @@
1
+ import React, { useState, useEffect, useRef, useCallback } from 'react';
2
+
3
+ export const useIntersectionObserver = (
4
+ options: IntersectionObserverInit = {}
5
+ ) => {
6
+ const [isIntersecting, setIsIntersecting] = useState(false);
7
+ const [hasIntersected, setHasIntersected] = useState(false);
8
+ const targetRef = useRef<HTMLElement>(null);
9
+
10
+ const defaultOptions = React.useMemo(() => ({
11
+ threshold: 0.1,
12
+ rootMargin: '50px',
13
+ ...options,
14
+ }), [options]);
15
+
16
+ useEffect(() => {
17
+ const target = targetRef.current;
18
+ if (!target) return;
19
+
20
+ const observer = new IntersectionObserver(
21
+ ([entry]) => {
22
+ const isVisible = entry.isIntersecting;
23
+ setIsIntersecting(isVisible);
24
+
25
+ if (isVisible && !hasIntersected) {
26
+ setHasIntersected(true);
27
+ }
28
+ },
29
+ defaultOptions
30
+ );
31
+
32
+ observer.observe(target);
33
+
34
+ return () => {
35
+ observer.unobserve(target);
36
+ };
37
+ }, [defaultOptions, hasIntersected]);
38
+
39
+ return {
40
+ targetRef,
41
+ isIntersecting,
42
+ hasIntersected,
43
+ };
44
+ };
45
+
46
+ export const useLazyImage = (src: string, placeholder?: string) => {
47
+ const [imageSrc, setImageSrc] = useState(placeholder || '');
48
+ const [isLoaded, setIsLoaded] = useState(false);
49
+ const [isError, setIsError] = useState(false);
50
+ const { targetRef, hasIntersected } = useIntersectionObserver();
51
+
52
+ useEffect(() => {
53
+ if (!hasIntersected || !src) return;
54
+
55
+ const img = new Image();
56
+
57
+ img.onload = () => {
58
+ setImageSrc(src);
59
+ setIsLoaded(true);
60
+ setIsError(false);
61
+ };
62
+
63
+ img.onerror = () => {
64
+ setIsError(true);
65
+ setIsLoaded(false);
66
+ };
67
+
68
+ img.src = src;
69
+
70
+ return () => {
71
+ img.onload = null;
72
+ img.onerror = null;
73
+ };
74
+ }, [hasIntersected, src]);
75
+
76
+ return {
77
+ targetRef,
78
+ imageSrc,
79
+ isLoaded,
80
+ isError,
81
+ hasIntersected,
82
+ };
83
+ };
84
+
85
+ export const useVirtualList = <T>(
86
+ items: T[],
87
+ options: {
88
+ itemHeight: number;
89
+ containerHeight: number;
90
+ overscan?: number;
91
+ }
92
+ ) => {
93
+ const { itemHeight, containerHeight, overscan = 5 } = options;
94
+ const [scrollTop, setScrollTop] = useState(0);
95
+ const scrollElementRef = useRef<HTMLDivElement>(null);
96
+
97
+ const totalHeight = items.length * itemHeight;
98
+ const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - overscan);
99
+ const endIndex = Math.min(
100
+ items.length - 1,
101
+ Math.ceil((scrollTop + containerHeight) / itemHeight) + overscan
102
+ );
103
+
104
+ const visibleItems = items.slice(startIndex, endIndex + 1).map((item, index) => ({
105
+ item,
106
+ index: startIndex + index,
107
+ style: {
108
+ position: 'absolute' as const,
109
+ top: (startIndex + index) * itemHeight,
110
+ height: itemHeight,
111
+ width: '100%',
112
+ },
113
+ }));
114
+
115
+ const handleScroll = useCallback((event: React.UIEvent<HTMLDivElement>) => {
116
+ setScrollTop(event.currentTarget.scrollTop);
117
+ }, []);
118
+
119
+ return {
120
+ scrollElementRef,
121
+ visibleItems,
122
+ totalHeight,
123
+ handleScroll,
124
+ startIndex,
125
+ endIndex,
126
+ };
127
+ };
128
+
129
+ export const useResourcePreloader = () => {
130
+ const [preloadedResources, setPreloadedResources] = useState<Set<string>>(new Set());
131
+ const [loadingResources, setLoadingResources] = useState<Set<string>>(new Set());
132
+
133
+ const preloadImage = useCallback(async (src: string): Promise<void> => {
134
+ if (preloadedResources.has(src) || loadingResources.has(src)) {
135
+ return;
136
+ }
137
+
138
+ setLoadingResources(prev => new Set(prev).add(src));
139
+
140
+ return new Promise((resolve, reject) => {
141
+ const img = new Image();
142
+
143
+ img.onload = () => {
144
+ setPreloadedResources(prev => new Set(prev).add(src));
145
+ setLoadingResources(prev => {
146
+ const newSet = new Set(prev);
147
+ newSet.delete(src);
148
+ return newSet;
149
+ });
150
+ resolve();
151
+ };
152
+
153
+ img.onerror = () => {
154
+ setLoadingResources(prev => {
155
+ const newSet = new Set(prev);
156
+ newSet.delete(src);
157
+ return newSet;
158
+ });
159
+ reject(new Error(`Failed to preload image: ${src}`));
160
+ };
161
+
162
+ img.src = src;
163
+ });
164
+ }, [preloadedResources, loadingResources]);
165
+
166
+ return {
167
+ preloadImage,
168
+ preloadedResources: Array.from(preloadedResources),
169
+ loadingResources: Array.from(loadingResources),
170
+ isPreloaded: (src: string) => preloadedResources.has(src),
171
+ isLoading: (src: string) => loadingResources.has(src),
172
+ };
173
+ };
174
+
175
+ export const useLazyComponent = <TProps = Record<string, unknown>>(
176
+ importFunc: () => Promise<{ default: React.ComponentType<TProps> }>,
177
+ fallback?: React.ComponentType<TProps>
178
+ ) => {
179
+ const [Component, setComponent] = useState<React.ComponentType<TProps> | null>(null);
180
+ const [isLoading, setIsLoading] = useState(false);
181
+ const [error, setError] = useState<Error | null>(null);
182
+ const { targetRef, hasIntersected } = useIntersectionObserver();
183
+
184
+ const loadComponent = useCallback(async () => {
185
+ if (Component || isLoading) return;
186
+
187
+ setIsLoading(true);
188
+ setError(null);
189
+
190
+ try {
191
+ const module = await importFunc();
192
+ setComponent(() => module.default);
193
+ } catch (err) {
194
+ setError(err instanceof Error ? err : new Error('Failed to load component'));
195
+ } finally {
196
+ setIsLoading(false);
197
+ }
198
+ }, [importFunc, Component, isLoading]);
199
+
200
+ useEffect(() => {
201
+ if (hasIntersected) {
202
+ loadComponent();
203
+ }
204
+ }, [hasIntersected, loadComponent]);
205
+
206
+ const LazyComponent = useCallback((props: TProps) => {
207
+ if (error) {
208
+ return fallback ? React.createElement(fallback as React.ElementType, props) : null;
209
+ }
210
+
211
+ if (isLoading || !Component) {
212
+ return fallback ? React.createElement(fallback as React.ElementType, props) : null;
213
+ }
214
+
215
+ return React.createElement(Component as React.ElementType, props);
216
+ }, [Component, isLoading, error, fallback]);
217
+
218
+ return {
219
+ targetRef,
220
+ LazyComponent,
221
+ isLoading,
222
+ error,
223
+ hasIntersected,
224
+ loadComponent,
225
+ };
226
+ };
227
+
228
+ export const useProgressiveEnhancement = () => {
229
+ const [isOnline, setIsOnline] = useState(navigator.onLine);
230
+ const [connectionType, setConnectionType] = useState<'slow' | 'fast' | 'unknown'>('unknown');
231
+
232
+ const handleOnline = useCallback(() => {
233
+ setIsOnline(true);
234
+ }, []);
235
+
236
+ const handleOffline = useCallback(() => {
237
+ setIsOnline(false);
238
+ }, []);
239
+
240
+ useEffect(() => {
241
+ window.addEventListener('online', handleOnline);
242
+ window.addEventListener('offline', handleOffline);
243
+
244
+ interface NetworkInformation {
245
+ effectiveType?: string;
246
+ addEventListener?: (type: string, listener: () => void) => void;
247
+ removeEventListener?: (type: string, listener: () => void) => void;
248
+ }
249
+
250
+ function getConnection(): NetworkInformation | undefined {
251
+ const nav = navigator as unknown as {
252
+ connection?: unknown;
253
+ mozConnection?: unknown;
254
+ webkitConnection?: unknown;
255
+ };
256
+ const conn = nav.connection || nav.mozConnection || nav.webkitConnection;
257
+ if (
258
+ conn &&
259
+ (typeof conn === 'object') &&
260
+ ('effectiveType' in conn)
261
+ ) {
262
+ return conn as NetworkInformation;
263
+ }
264
+ return undefined;
265
+ }
266
+
267
+ const connection = getConnection();
268
+
269
+ if (connection) {
270
+ const updateConnectionType = () => {
271
+ const effectiveType = connection.effectiveType;
272
+ if (effectiveType === 'slow-2g' || effectiveType === '2g') {
273
+ setConnectionType('slow');
274
+ } else if (effectiveType === '3g' || effectiveType === '4g') {
275
+ setConnectionType('fast');
276
+ } else {
277
+ setConnectionType('unknown');
278
+ }
279
+ };
280
+
281
+ updateConnectionType();
282
+ connection.addEventListener?.('change', updateConnectionType);
283
+
284
+ return () => {
285
+ window.removeEventListener('online', handleOnline);
286
+ window.removeEventListener('offline', handleOffline);
287
+ connection.removeEventListener?.('change', updateConnectionType);
288
+ };
289
+ }
290
+
291
+ return () => {
292
+ window.removeEventListener('online', handleOnline);
293
+ window.removeEventListener('offline', handleOffline);
294
+ };
295
+ }, [handleOnline, handleOffline]);
296
+
297
+ const shouldLoadHeavyContent = useCallback(() => {
298
+ return isOnline && connectionType !== 'slow';
299
+ }, [isOnline, connectionType]);
300
+
301
+ const shouldPreloadContent = useCallback(() => {
302
+ return isOnline && connectionType === 'fast';
303
+ }, [isOnline, connectionType]);
304
+
305
+ return {
306
+ isOnline,
307
+ connectionType,
308
+ shouldLoadHeavyContent,
309
+ shouldPreloadContent,
310
+ };
311
+ };
312
+
313
+ export const useLazyLoading = (options: {
314
+ enableImageLazyLoading?: boolean;
315
+ enableComponentLazyLoading?: boolean;
316
+ enableVirtualization?: boolean;
317
+ enablePreloading?: boolean;
318
+ enableProgressiveEnhancement?: boolean;
319
+ } = {}) => {
320
+ const {
321
+ enableImageLazyLoading = true,
322
+ enableComponentLazyLoading = true,
323
+ enableVirtualization = true,
324
+ enablePreloading = true,
325
+ enableProgressiveEnhancement = true,
326
+ } = options;
327
+
328
+ const resourcePreloader = useResourcePreloader();
329
+ const progressiveEnhancement = useProgressiveEnhancement();
330
+
331
+ return {
332
+ useIntersectionObserver,
333
+ useLazyImage: enableImageLazyLoading ? useLazyImage : null,
334
+ useLazyComponent: enableComponentLazyLoading ? useLazyComponent : null,
335
+ useVirtualList: enableVirtualization ? useVirtualList : null,
336
+ preloadImage: enablePreloading ? resourcePreloader.preloadImage : undefined,
337
+ shouldLoadHeavyContent: enableProgressiveEnhancement ? progressiveEnhancement.shouldLoadHeavyContent : undefined,
338
+ shouldPreloadContent: enableProgressiveEnhancement ? progressiveEnhancement.shouldPreloadContent : undefined,
339
+ isOnline: enableProgressiveEnhancement ? progressiveEnhancement.isOnline : undefined,
340
+ connectionType: enableProgressiveEnhancement ? progressiveEnhancement.connectionType : undefined,
341
+ };
342
+ };
@@ -0,0 +1,293 @@
1
+ import { useEffect, useRef, useCallback, useState } from 'react';
2
+
3
+ export interface MemoryOptimizationConfig {
4
+ enableCleanupLogging?: boolean;
5
+ trackEventListeners?: boolean;
6
+ trackTimers?: boolean;
7
+ trackSubscriptions?: boolean;
8
+ }
9
+
10
+ export interface CleanupFunction {
11
+ (): void;
12
+ }
13
+
14
+ export const useMemoryOptimization = (config: MemoryOptimizationConfig = {}) => {
15
+ const {
16
+ enableCleanupLogging = import.meta.env.DEV,
17
+ trackEventListeners = true,
18
+ trackTimers = true,
19
+ trackSubscriptions = true
20
+ } = config;
21
+
22
+ const cleanupFunctions = useRef<CleanupFunction[]>([]);
23
+ const eventListeners = useRef<Array<{
24
+ element: EventTarget;
25
+ event: string;
26
+ handler: EventListener;
27
+ options?: AddEventListenerOptions;
28
+ }>>([]);
29
+ const timers = useRef<Array<{
30
+ id: number;
31
+ type: 'timeout' | 'interval';
32
+ }>>([]);
33
+ const subscriptions = useRef<Array<{
34
+ name: string;
35
+ unsubscribe: () => void;
36
+ }>>([]);
37
+
38
+ const addCleanup = useCallback((cleanup: CleanupFunction, name?: string) => {
39
+ cleanupFunctions.current.push(cleanup);
40
+
41
+ if (enableCleanupLogging && name) {
42
+ console.log(`[Memory] Added cleanup: ${name}`);
43
+ }
44
+
45
+ return () => {
46
+ const index = cleanupFunctions.current.indexOf(cleanup);
47
+ if (index > -1) {
48
+ cleanupFunctions.current.splice(index, 1);
49
+ }
50
+ };
51
+ }, [enableCleanupLogging]);
52
+
53
+ const addEventListener = useCallback((
54
+ element: EventTarget,
55
+ event: string,
56
+ handler: EventListener,
57
+ options?: AddEventListenerOptions
58
+ ) => {
59
+ if (!trackEventListeners) {
60
+ element.addEventListener(event, handler, options);
61
+ return () => element.removeEventListener(event, handler, options);
62
+ }
63
+
64
+ element.addEventListener(event, handler, options);
65
+
66
+ const listenerInfo = { element, event, handler, options };
67
+ eventListeners.current.push(listenerInfo);
68
+
69
+ if (enableCleanupLogging) {
70
+ console.log(`[Memory] Added event listener: ${event}`);
71
+ }
72
+
73
+ return () => {
74
+ element.removeEventListener(event, handler, options);
75
+ const index = eventListeners.current.indexOf(listenerInfo);
76
+ if (index > -1) {
77
+ eventListeners.current.splice(index, 1);
78
+ }
79
+ };
80
+ }, [trackEventListeners, enableCleanupLogging]);
81
+
82
+ const setTimeout = useCallback((
83
+ callback: () => void,
84
+ delay: number
85
+ ) => {
86
+ const id = window.setTimeout(callback, delay);
87
+
88
+ if (trackTimers) {
89
+ timers.current.push({ id, type: 'timeout' });
90
+
91
+ if (enableCleanupLogging) {
92
+ console.log(`[Memory] Added timeout: ${id}`);
93
+ }
94
+ }
95
+
96
+ return id;
97
+ }, [trackTimers, enableCleanupLogging]);
98
+
99
+ const setInterval = useCallback((
100
+ callback: () => void,
101
+ delay: number
102
+ ) => {
103
+ const id = window.setInterval(callback, delay);
104
+
105
+ if (trackTimers) {
106
+ timers.current.push({ id, type: 'interval' });
107
+
108
+ if (enableCleanupLogging) {
109
+ console.log(`[Memory] Added interval: ${id}`);
110
+ }
111
+ }
112
+
113
+ return id;
114
+ }, [trackTimers, enableCleanupLogging]);
115
+
116
+ const clearTimeout = useCallback((id: number) => {
117
+ window.clearTimeout(id);
118
+
119
+ if (trackTimers) {
120
+ const index = timers.current.findIndex(timer => timer.id === id);
121
+ if (index > -1) {
122
+ timers.current.splice(index, 1);
123
+
124
+ if (enableCleanupLogging) {
125
+ console.log(`[Memory] Cleared timeout: ${id}`);
126
+ }
127
+ }
128
+ }
129
+ }, [trackTimers, enableCleanupLogging]);
130
+
131
+ const clearInterval = useCallback((id: number) => {
132
+ window.clearInterval(id);
133
+
134
+ if (trackTimers) {
135
+ const index = timers.current.findIndex(timer => timer.id === id);
136
+ if (index > -1) {
137
+ timers.current.splice(index, 1);
138
+
139
+ if (enableCleanupLogging) {
140
+ console.log(`[Memory] Cleared interval: ${id}`);
141
+ }
142
+ }
143
+ }
144
+ }, [trackTimers, enableCleanupLogging]);
145
+
146
+ const addSubscription = useCallback((
147
+ name: string,
148
+ unsubscribe: () => void
149
+ ) => {
150
+ if (!trackSubscriptions) {
151
+ return unsubscribe;
152
+ }
153
+
154
+ const subscription = { name, unsubscribe };
155
+ subscriptions.current.push(subscription);
156
+
157
+ if (enableCleanupLogging) {
158
+ console.log(`[Memory] Added subscription: ${name}`);
159
+ }
160
+
161
+ return () => {
162
+ unsubscribe();
163
+ const index = subscriptions.current.indexOf(subscription);
164
+ if (index > -1) {
165
+ subscriptions.current.splice(index, 1);
166
+
167
+ if (enableCleanupLogging) {
168
+ console.log(`[Memory] Unsubscribed: ${name}`);
169
+ }
170
+ }
171
+ };
172
+ }, [trackSubscriptions, enableCleanupLogging]);
173
+
174
+ const getMemoryStats = useCallback(() => {
175
+ return {
176
+ cleanupFunctions: cleanupFunctions.current.length,
177
+ eventListeners: eventListeners.current.length,
178
+ timers: timers.current.length,
179
+ subscriptions: subscriptions.current.length,
180
+ totalTrackedItems:
181
+ cleanupFunctions.current.length +
182
+ eventListeners.current.length +
183
+ timers.current.length +
184
+ subscriptions.current.length
185
+ };
186
+ }, []);
187
+
188
+ const cleanup = useCallback(() => {
189
+ if (enableCleanupLogging) {
190
+ const stats = getMemoryStats();
191
+ console.log(`[Memory] Running cleanup with ${stats.totalTrackedItems} items`);
192
+ }
193
+
194
+ eventListeners.current.forEach(({ element, event, handler, options }) => {
195
+ element.removeEventListener(event, handler, options);
196
+ });
197
+ eventListeners.current = [];
198
+
199
+ timers.current.forEach(({ id, type }) => {
200
+ if (type === 'timeout') {
201
+ window.clearTimeout(id);
202
+ } else {
203
+ window.clearInterval(id);
204
+ }
205
+ });
206
+ timers.current = [];
207
+
208
+ subscriptions.current.forEach(({ unsubscribe }) => {
209
+ unsubscribe();
210
+ });
211
+ subscriptions.current = [];
212
+
213
+ cleanupFunctions.current.forEach(cleanup => {
214
+ try {
215
+ cleanup();
216
+ } catch (error) {
217
+ console.error('[Memory] Cleanup function failed:', error);
218
+ }
219
+ });
220
+ cleanupFunctions.current = [];
221
+
222
+ if (enableCleanupLogging) {
223
+ console.log('[Memory] Cleanup complete');
224
+ }
225
+ }, [enableCleanupLogging, getMemoryStats]);
226
+
227
+ useEffect(() => {
228
+ return () => {
229
+ if (enableCleanupLogging) {
230
+ const stats = getMemoryStats();
231
+ console.log(`[Memory] Unmounting with ${stats.totalTrackedItems} items to clean`);
232
+ }
233
+
234
+ cleanup();
235
+ };
236
+ }, [cleanup, enableCleanupLogging, getMemoryStats]);
237
+
238
+ return {
239
+ addCleanup,
240
+ addEventListener,
241
+ setTimeout,
242
+ setInterval,
243
+ clearTimeout,
244
+ clearInterval,
245
+ addSubscription,
246
+ getMemoryStats,
247
+ cleanup
248
+ };
249
+ };
250
+
251
+ export const useMemoryLeakDetector = (componentName?: string) => {
252
+ const mountTime = useRef<number>(Date.now());
253
+ const [renderCount, setRenderCount] = useState<number>(0);
254
+ const [lifespan, setLifespan] = useState<number>(0);
255
+
256
+ useEffect(() => {
257
+ setRenderCount(prev => {
258
+ const newCount = prev + 1;
259
+
260
+ if (newCount > 100 && newCount % 10 === 0) {
261
+ console.warn(
262
+ `[Memory Leak] ${componentName || 'Component'} has rendered ${newCount} times. ` +
263
+ 'This might indicate a memory leak or unnecessary re-renders.'
264
+ );
265
+ }
266
+
267
+ return newCount;
268
+ });
269
+ });
270
+
271
+ useEffect(() => {
272
+ const interval = setInterval(() => {
273
+ setLifespan(Date.now() - mountTime.current);
274
+ }, 1000);
275
+
276
+ return () => {
277
+ clearInterval(interval);
278
+ const finalLifespan = Date.now() - mountTime.current;
279
+
280
+ if (import.meta.env.DEV) {
281
+ console.log(
282
+ `[Memory] ${componentName || 'Component'} unmounted after ${finalLifespan}ms ` +
283
+ `with ${renderCount} renders`
284
+ );
285
+ }
286
+ };
287
+ }, [componentName, renderCount]);
288
+
289
+ return {
290
+ renderCount,
291
+ lifespan
292
+ };
293
+ };