diff --git a/README.md b/README.md index 5c372df..c34627e 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ The central hub with 45+ algorithms across 5 categories: | **Barcode / QR Code Generator** | Create QR codes and 1D barcodes (EAN-13, EAN-8, Code 128, Code 39) with preview and download | | **Data Generator** | Generate mock data with templates using Faker library (UUID, ULID, Random String, Lorem Ipsum, User Profiles, API responses, SQL inserts, and more) | | **Code Formatter** | Format and minify JSON, XML, HTML, SQL, CSS, and JavaScript with advanced filtering support (jq for JSON, XPath for XML, CSS selectors for HTML) | +| **Color Converter** | Pick colors with eyedropper and generate code snippets for 11+ programming languages (CSS, Swift, .NET, Java, Android, Obj-C, Flutter, Unity, React Native, OpenGL, SVG) | | **RegExp Tester** | Test regular expressions with real-time matching | | **Unix Time Converter** | Convert between Unix timestamps and human-readable dates | | **String Utilities** | Sort/Dedupe lines, Case conversion (camelCase, snake_case, etc.), String Inspector | diff --git a/TOOL_STATUS.md b/TOOL_STATUS.md index 32df695..95c2764 100644 --- a/TOOL_STATUS.md +++ b/TOOL_STATUS.md @@ -21,6 +21,7 @@ This document tracks the refactoring and development status of each tool compone | BarcodeGenerator | 🟢 Done | Multi-standard barcode generator (QR, EAN-13, EAN-8, Code128, Code39). Features: configurable size, error correction levels for QR, client-side validation, download button. | Completed 2026-01-31 | | **DataGenerator** | 🟢 Done | Template-based mock data generator with Faker integration. Features: 10 built-in presets (UUID, ULID, Random String, Lorem Ipsum, User Profile, E-commerce Product, API Response, SQL Insert, Log Entries, Credit Card), batch generation (10-1000 records), multiple output formats (JSON, XML, CSV, YAML), comprehensive help documentation with 4 tabs (Quick Start, Syntax, Faker Reference, Examples). Backend: Go templates + gofakeit library with 80+ faker functions. Replaces: RandomStringGenerator, UuidGenerator, LoremIpsumGenerator | Completed 2026-01-31 | | **CodeFormatter** | 🟢 Done | Unified code formatting tool supporting JSON (with jq filters), XML (with XPath), HTML (with CSS selectors), SQL, CSS, and JavaScript. Features: format/minify modes, filter/query support for structured data, auto-format on input change, persistent state. Backend: Go with gojq library for jq support. Replaces: JsonFormatter, SqlFormatter | Completed 2026-01-31 | +| **ColorConverter** | 🟢 Done | Comprehensive color conversion tool with visual picker and eyedropper support. Features: 11 programming languages (CSS, Swift, .NET, Java, Android, Obj-C, Flutter, Unity, React Native, OpenGL, SVG), 5 color formats (HEX, RGB, HSL, HSV, CMYK), color history with 10 recent colors, random color generator, copy-to-clipboard for all code snippets. Uses Carbon Tabs for language selection. | Completed 2026-02-01 | | **CronJobParser** | 🟢 Done | Refactored to follow Carbon Design System. Features: Split-pane layout, 8 common examples in clickable tiles, real-time parsing, large centered output display, layout toggle. | Completed 2026-01-31 | | **RegExpTester** | 🟡 In Progress | Refactored with improved UI. Features: Flag toggle tags (g, i, m, s, u, y), split-pane layout, match count in output label, error display with styling, layout toggle. | Updated 2026-01-31 | | **TextDiffChecker** | 🟡 In Progress | Refactored with enhanced features. Features: Diff mode switcher (Lines/Words/Chars), auto-compare on input change, Clear button, improved diff view with color coding, layout toggle. | Updated 2026-01-31 | diff --git a/src/App.jsx b/src/App.jsx index b1b1a65..69d6174 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -16,6 +16,7 @@ import StringUtilities from './pages/StringUtilities'; import BarcodeGenerator from './pages/BarcodeGenerator'; import DataGenerator from './pages/DataGenerator'; import CodeFormatter from './pages/CodeFormatter'; +import ColorConverter from './pages/ColorConverter'; // Error boundary for catching React rendering errors class ErrorBoundary extends React.Component { @@ -105,6 +106,7 @@ function App() { case 'barcode': return ; case 'data-generator': return ; case 'code-formatter': return ; + case 'color-converter': return ; case 'regexp': return ; case 'cron': return ; case 'diff': return ; diff --git a/src/components/Sidebar.jsx b/src/components/Sidebar.jsx index fc148e6..d0318e0 100644 --- a/src/components/Sidebar.jsx +++ b/src/components/Sidebar.jsx @@ -22,6 +22,7 @@ export function Sidebar({ activeTool, setActiveTool, isVisible, toggleSidebar }) { id: 'barcode', name: 'Barcode / QR Code', icon: '▣' }, { id: 'data-generator', name: 'Data Generator', icon: '📊' }, { id: 'code-formatter', name: 'Code Formatter', icon: '📝' }, + { id: 'color-converter', name: 'Color Converter', icon: '🎨' }, { id: 'cron', name: 'Cron Job Parser', icon: '⏳' }, { id: 'regexp', name: 'RegExp Tester', icon: '.*' }, { id: 'diff', name: 'Text Diff Checker', icon: '⚖️' }, diff --git a/src/pages/ColorConverter.jsx b/src/pages/ColorConverter.jsx new file mode 100644 index 0000000..1191c43 --- /dev/null +++ b/src/pages/ColorConverter.jsx @@ -0,0 +1,882 @@ +import React, { useState, useEffect, useCallback, useReducer, useMemo, useRef } from 'react'; +import { Button, TextInput, Tile, Tabs, TabList, Tab, TabPanels, TabPanel } from '@carbon/react'; +import { Eyedropper, Copy, ColorPalette, TrashCan } from '@carbon/icons-react'; +import { ToolHeader, ToolControls, ToolLayoutToggle } from '../components/ToolUI'; +import useLayoutToggle from '../hooks/useLayoutToggle'; + +// Color utility functions +const hexToRgb = (hex) => { + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})?$/i.exec(hex) || + /^#?([a-f\d])([a-f\d])([a-f\d])$/i.exec(hex); + if (!result) return null; + + const r = parseInt(result[1].length === 1 ? result[1] + result[1] : result[1], 16); + const g = parseInt(result[2].length === 1 ? result[2] + result[2] : result[2], 16); + const b = parseInt(result[3].length === 1 ? result[3] + result[3] : result[3], 16); + const a = result[4] ? parseInt(result[4].length === 1 ? result[4] + result[4] : result[4], 16) / 255 : 1; + + return { r, g, b, a }; +}; + +const rgbToHex = (r, g, b, a = 1) => { + const toHex = (n) => Math.round(n).toString(16).padStart(2, '0'); + const hex = `#${toHex(r)}${toHex(g)}${toHex(b)}`; + if (a < 1) { + return `${hex}${toHex(a * 255)}`; + } + return hex; +}; + +const rgbToHsl = (r, g, b) => { + r /= 255; + g /= 255; + b /= 255; + + const max = Math.max(r, g, b); + const min = Math.min(r, g, b); + let h, s, l = (max + min) / 2; + + if (max === min) { + h = s = 0; + } else { + const d = max - min; + s = l > 0.5 ? d / (2 - max - min) : d / (max + min); + switch (max) { + case r: h = (g - b) / d + (g < b ? 6 : 0); break; + case g: h = (b - r) / d + 2; break; + case b: h = (r - g) / d + 4; break; + } + h /= 6; + } + + return { + h: Math.round(h * 360), + s: Math.round(s * 100), + l: Math.round(l * 100) + }; +}; + +const rgbToHsv = (r, g, b) => { + r /= 255; + g /= 255; + b /= 255; + + const max = Math.max(r, g, b); + const min = Math.min(r, g, b); + let h, s, v = max; + + const d = max - min; + s = max === 0 ? 0 : d / max; + + if (max === min) { + h = 0; + } else { + switch (max) { + case r: h = (g - b) / d + (g < b ? 6 : 0); break; + case g: h = (b - r) / d + 2; break; + case b: h = (r - g) / d + 4; break; + } + h /= 6; + } + + return { + h: Math.round(h * 360), + s: Math.round(s * 100), + v: Math.round(v * 100) + }; +}; + +const rgbToCmyk = (r, g, b) => { + r /= 255; + g /= 255; + b /= 255; + + const k = 1 - Math.max(r, g, b); + const c = k === 1 ? 0 : (1 - r - k) / (1 - k); + const m = k === 1 ? 0 : (1 - g - k) / (1 - k); + const y = k === 1 ? 0 : (1 - b - k) / (1 - k); + + return { + c: Math.round(c * 100), + m: Math.round(m * 100), + y: Math.round(y * 100), + k: Math.round(k * 100) + }; +}; + +// Generate code snippets for various languages +const generateCodeSnippets = (r, g, b, a, hsl, hsv) => { + const rf = (r / 255).toFixed(3); + const gf = (g / 255).toFixed(3); + const bf = (b / 255).toFixed(3); + const af = a.toFixed(2); + const hue = hsl.h; + const sat = hsv.s; + const bright = hsv.v; + + return { + css: [ + { name: 'RGB', code: `rgb(${r} ${g} ${b})` }, + { name: 'RGBA', code: `rgb(${r} ${g} ${b} / ${Math.round(a * 100)}%)` }, + { name: 'HSL', code: `hsl(${hue}deg ${hsl.s}% ${hsl.l}%)` }, + { name: 'HSLA', code: `hsl(${hue}deg ${hsl.s}% ${hsl.l}% / ${Math.round(a * 100)}%)` }, + { name: 'Hex', code: rgbToHex(r, g, b, a) }, + { name: 'CSS Variable', code: `--color-primary: ${rgbToHex(r, g, b, a)};` } + ], + swift: [ + { name: 'NSColor RGB', code: `NSColor( + calibratedRed: ${rf}, + green: ${gf}, + blue: ${bf}, + alpha: ${af} +)` }, + { name: 'NSColor HSB', code: `NSColor( + calibratedHue: ${hue / 360}, + saturation: ${(sat / 100).toFixed(3)}, + brightness: ${(bright / 100).toFixed(3)}, + alpha: ${af} +)` }, + { name: 'UIColor RGB', code: `UIColor( + red: ${rf}, + green: ${gf}, + blue: ${bf}, + alpha: ${af} +)` }, + { name: 'UIColor HSB', code: `UIColor( + hue: ${hue / 360}, + saturation: ${(sat / 100).toFixed(3)}, + brightness: ${(bright / 100).toFixed(3)}, + alpha: ${af} +)` } + ], + dotnet: [ + { name: 'FromRgb', code: `Color.FromRgb(${r}, ${g}, ${b})` }, + { name: 'FromArgb', code: `Color.FromArgb(${Math.round(a * 255)}, ${r}, ${g}, ${b})` }, + { name: 'FromHex', code: `Color.FromHex("${rgbToHex(r, g, b).replace('#', '')}")` } + ], + java: [ + { name: 'Color RGB', code: `new Color(${r}, ${g}, ${b})` }, + { name: 'Color RGBA', code: `new Color(${r}, ${g}, ${b}, ${Math.round(a * 255)})` }, + { name: 'Color HSB', code: `Color.getHSBColor(${hue / 360}f, ${(sat / 100).toFixed(3)}f, ${(bright / 100).toFixed(3)}f)` } + ], + android: [ + { name: 'Color.rgb', code: `Color.rgb(${r}, ${g}, ${b})` }, + { name: 'Color.argb', code: `Color.argb(${Math.round(a * 255)}, ${r}, ${g}, ${b})` }, + { name: 'Color.parseColor', code: `Color.parseColor("${rgbToHex(r, g, b)}")` }, + { name: 'Resource', code: `${rgbToHex(r, g, b)}` }, + { name: 'Resource with Alpha', code: `${rgbToHex(r, g, b, a)}` } + ], + opengl: [ + { name: 'glColor3f', code: `glColor3f(${rf}f, ${gf}f, ${bf}f)` }, + { name: 'glColor4f', code: `glColor4f(${rf}f, ${gf}f, ${bf}f, ${af}f)` }, + { name: 'glColor3ub', code: `glColor3ub(${r}, ${g}, ${b})` }, + { name: 'glColor4ub', code: `glColor4ub(${r}, ${g}, ${b}, ${Math.round(a * 255)})` } + ], + objc: [ + { name: 'UIColor RGB', code: `[UIColor colorWithRed:${rf} green:${gf} blue:${bf} alpha:${af}]` }, + { name: 'UIColor HSB', code: `[UIColor colorWithHue:${(hue / 360).toFixed(3)} saturation:${(sat / 100).toFixed(3)} brightness:${(bright / 100).toFixed(3)} alpha:${af}]` }, + { name: 'NSColor RGB', code: `[[NSColor colorWithCalibratedRed:${rf} green:${gf} blue:${bf} alpha:${af}]` } + ], + flutter: [ + { name: 'Color from RGB', code: `Color.fromRGBO(${r}, ${g}, ${b}, ${af})` }, + { name: 'Color from ARGB', code: `Color.fromARGB(${Math.round(a * 255)}, ${r}, ${g}, ${b})` }, + { name: 'Color hex', code: `Color(0xFF${rgbToHex(r, g, b).replace('#', '').toUpperCase()})` }, + { name: 'Color hex with alpha', code: `Color(0x${Math.round(a * 255).toString(16).padStart(2, '0').toUpperCase()}${rgbToHex(r, g, b).replace('#', '').toUpperCase()})` } + ], + unity: [ + { name: 'Color', code: `new Color(${rf}f, ${gf}f, ${bf}f, ${af}f)` }, + { name: 'Color32', code: `new Color32(${r}, ${g}, ${b}, ${Math.round(a * 255)})` }, + { name: 'Hex String', code: `ColorUtility.TryParseHtmlString("${rgbToHex(r, g, b)}", out Color color)` } + ], + reactnative: [ + { name: 'StyleSheet', code: `const styles = StyleSheet.create({ + container: { + backgroundColor: '${rgbToHex(r, g, b)}', + }, +});` }, + { name: 'Inline', code: `{ backgroundColor: '${rgbToHex(r, g, b)}' }` }, + { name: 'RGBA', code: `{ backgroundColor: 'rgba(${r}, ${g}, ${b}, ${af})' }` } + ], + svg: [ + { name: 'Fill', code: `fill="${rgbToHex(r, g, b)}"` }, + { name: 'Stroke', code: `stroke="${rgbToHex(r, g, b)}"` }, + { name: 'Fill with Opacity', code: `fill="${rgbToHex(r, g, b)}" fill-opacity="${af}"` } + ] + }; +}; + +// Initial state +const initialState = { + hex: '#3DD6F5', + rgb: { r: 61, g: 214, b: 245, a: 1 }, + hsl: { h: 191, s: 90, l: 60 }, + hsv: { h: 191, s: 75, v: 96 }, + cmyk: { c: 75, m: 13, y: 0, k: 4 }, + history: [], + selectedTab: 0 +}; + +// Reducer for state management +function colorReducer(state, action) { + switch (action.type) { + case 'SET_COLOR': + return { + ...state, + ...action.payload + }; + case 'ADD_TO_HISTORY': + const newEntry = action.payload; + if (state.history.some(h => h.hex === newEntry.hex)) { + return state; + } + return { + ...state, + history: [newEntry, ...state.history].slice(0, 10) + }; + case 'SET_SELECTED_TAB': + return { ...state, selectedTab: action.payload }; + case 'CLEAR_HISTORY': + return { ...state, history: [] }; + case 'LOAD_FROM_HISTORY': + return { ...state, ...action.payload }; + default: + return state; + } +} + +export default function ColorConverter() { + const [state, dispatch] = useReducer(colorReducer, initialState); + const [hexInput, setHexInput] = useState(initialState.hex); + const [isHexValid, setIsHexValid] = useState(true); + const [rgbInputs, setRgbInputs] = useState(initialState.rgb); + const [hslInputs, setHslInputs] = useState(initialState.hsl); + const [eyedropperSupported, setEyedropperSupported] = useState(false); + const [isPicking, setIsPicking] = useState(false); + const historyTimeoutRef = useRef(null); + + // Layout toggle support + const layout = useLayoutToggle({ + toolKey: 'color-converter-layout', + defaultDirection: 'horizontal', + showToggle: true, + persist: true + }); + + // Check for EyeDropper API support + useEffect(() => { + setEyedropperSupported('EyeDropper' in window); + }, []); + + // Cleanup timeout on unmount + useEffect(() => { + return () => { + if (historyTimeoutRef.current) { + clearTimeout(historyTimeoutRef.current); + } + }; + }, []); + + // Generate code snippets when color changes + const codeSnippets = useMemo(() => + generateCodeSnippets( + state.rgb.r, state.rgb.g, state.rgb.b, state.rgb.a, + state.hsl, state.hsv + ), + [state.rgb, state.hsl, state.hsv] + ); + + // Debounced history recording + const debouncedAddToHistory = useCallback((hex, rgb) => { + // Clear existing timeout + if (historyTimeoutRef.current) { + clearTimeout(historyTimeoutRef.current); + } + // Set new timeout to add to history after 100ms of no changes + historyTimeoutRef.current = setTimeout(() => { + dispatch({ + type: 'ADD_TO_HISTORY', + payload: { hex, rgb } + }); + }, 100); + }, []); + + // Update all color formats from RGB + const updateFromRgb = useCallback((r, g, b, a = 1) => { + const hex = rgbToHex(r, g, b, a); + const hsl = rgbToHsl(r, g, b); + const hsv = rgbToHsv(r, g, b); + const cmyk = rgbToCmyk(r, g, b); + + dispatch({ + type: 'SET_COLOR', + payload: { hex, rgb: { r, g, b, a }, hsl, hsv, cmyk } + }); + setHexInput(hex); + setRgbInputs({ r, g, b, a }); + setHslInputs(hsl); + + // Debounce history recording + debouncedAddToHistory(hex, { r, g, b, a }); + }, [debouncedAddToHistory]); + + // Handle hex input change + const handleHexChange = useCallback((value) => { + setHexInput(value); + const rgb = hexToRgb(value); + if (rgb) { + setIsHexValid(true); + updateFromRgb(rgb.r, rgb.g, rgb.b, rgb.a); + } else { + setIsHexValid(false); + } + }, [updateFromRgb]); + + // Handle hex input blur - validate and reset if invalid + const handleHexBlur = useCallback(() => { + const rgb = hexToRgb(hexInput); + if (!rgb) { + // Reset to current valid color + setHexInput(state.hex); + setIsHexValid(true); + } + }, [hexInput, state.hex]); + + // Handle RGB input changes + const handleRgbChange = useCallback((key, value) => { + const numValue = key === 'a' + ? parseFloat(value) || 0 // Preserve decimal for alpha + : parseInt(value, 10) || 0; + const newRgb = { ...rgbInputs, [key]: numValue }; + setRgbInputs(newRgb); + + if (key === 'a') { + updateFromRgb(newRgb.r, newRgb.g, newRgb.b, numValue); + } else { + updateFromRgb(newRgb.r, newRgb.g, newRgb.b, newRgb.a); + } + }, [rgbInputs, updateFromRgb]); + + // Handle HSL input changes + const handleHslChange = useCallback((key, value) => { + const numValue = parseInt(value, 10) || 0; + const newHsl = { ...hslInputs, [key]: numValue }; + setHslInputs(newHsl); + + // Convert HSL to RGB + const h = newHsl.h / 360; + const s = newHsl.s / 100; + const l = newHsl.l / 100; + + let r, g, b; + + if (s === 0) { + r = g = b = l; + } else { + const hue2rgb = (p, q, t) => { + if (t < 0) t += 1; + if (t > 1) t -= 1; + if (t < 1/6) return p + (q - p) * 6 * t; + if (t < 1/2) return q; + if (t < 2/3) return p + (q - p) * (2/3 - t) * 6; + return p; + }; + + const q = l < 0.5 ? l * (1 + s) : l + s - l * s; + const p = 2 * l - q; + r = hue2rgb(p, q, h + 1/3); + g = hue2rgb(p, q, h); + b = hue2rgb(p, q, h - 1/3); + } + + updateFromRgb(Math.round(r * 255), Math.round(g * 255), Math.round(b * 255), state.rgb.a); + }, [hslInputs, state.rgb.a, updateFromRgb]); + + // Handle native color picker change + const handleColorPickerChange = useCallback((e) => { + handleHexChange(e.target.value); + }, [handleHexChange]); + + // EyeDropper functionality + const openEyeDropper = useCallback(async () => { + if (!eyedropperSupported) return; + + setIsPicking(true); + try { + const eyeDropper = new window.EyeDropper(); + const result = await eyeDropper.open(); + if (result.sRGBHex) { + handleHexChange(result.sRGBHex); + } + } catch (e) { + // User cancelled or error + } finally { + setIsPicking(false); + } + }, [eyedropperSupported, handleHexChange]); + + // Copy to clipboard + const copyToClipboard = useCallback((text) => { + navigator.clipboard.writeText(text); + }, []); + + // Load color from history + const loadFromHistory = useCallback((item) => { + const rgb = hexToRgb(item.hex); + if (rgb) { + updateFromRgb(rgb.r, rgb.g, rgb.b, rgb.a); + } + }, [updateFromRgb]); + + // Generate random color + const generateRandomColor = useCallback(() => { + const r = Math.floor(Math.random() * 256); + const g = Math.floor(Math.random() * 256); + const b = Math.floor(Math.random() * 256); + updateFromRgb(r, g, b, 1); + }, [updateFromRgb]); + + const languageTabs = [ + { id: 'css', label: 'CSS' }, + { id: 'swift', label: 'Swift' }, + { id: 'dotnet', label: '.NET' }, + { id: 'java', label: 'Java' }, + { id: 'android', label: 'Android' }, + { id: 'objc', label: 'Obj-C' }, + { id: 'flutter', label: 'Flutter' }, + { id: 'unity', label: 'Unity' }, + { id: 'reactnative', label: 'React Native' }, + { id: 'opengl', label: 'OpenGL' }, + { id: 'svg', label: 'SVG' } + ]; + + return ( +
+ + + + {/* Color Preview & Picker */} +
+
+
+ +
+ +
+ + {eyedropperSupported && ( + + )} +
+ + {/* Input Controls */} +
+ {/* Hex Input */} +
+ + handleHexChange(e.target.value)} + onBlur={handleHexBlur} + placeholder="#3DD6F5" + style={{ fontFamily: "'IBM Plex Mono', monospace" }} + size="sm" + invalid={!isHexValid} + invalidText="Invalid hex color" + /> +
+ + {/* RGB Inputs */} +
+
+ + handleRgbChange('r', e.target.value)} + size="sm" + /> +
+
+ + handleRgbChange('g', e.target.value)} + size="sm" + /> +
+
+ + handleRgbChange('b', e.target.value)} + size="sm" + /> +
+
+ + handleRgbChange('a', e.target.value)} + size="sm" + /> +
+
+ + {/* HSL Inputs */} +
+
+ + handleHslChange('h', e.target.value)} + size="sm" + /> +
+
+ + handleHslChange('s', e.target.value)} + size="sm" + /> +
+
+ + handleHslChange('l', e.target.value)} + size="sm" + /> +
+
+
+ + {/* Layout Toggle */} +
+ +
+
+ + {/* Format Values */} +
+
+ RGB: + + {state.rgb.r}, {state.rgb.g}, {state.rgb.b} + +
+
+ HEX: + + {state.hex} + +
+
+ HSL: + + {state.hsl.h}°, {state.hsl.s}%, {state.hsl.l}% + +
+
+ HSV: + + {state.hsv.h}°, {state.hsv.s}%, {state.hsv.v}% + +
+
+ CMYK: + + {state.cmyk.c}%, {state.cmyk.m}%, {state.cmyk.y}%, {state.cmyk.k}% + +
+
+ + {/* Main Content Area */} +
+ {/* Color History */} + {state.history.length > 0 && ( +
+
+ History +
+
+ {state.history.map((item, idx) => ( +
loadFromHistory(item)} + style={{ + display: 'flex', + alignItems: 'center', + gap: '0.5rem', + padding: '0.5rem', + cursor: 'pointer', + borderRadius: '4px', + marginBottom: '0.25rem', + backgroundColor: 'var(--cds-layer-hover)' + }} + > +
+ + {item.hex} + +
+ ))} +
+
+ )} + + {/* Code Snippets */} +
+ dispatch({ type: 'SET_SELECTED_TAB', payload: selectedIndex })} + > + + {languageTabs.map(tab => ( + {tab.label} + ))} + + + {languageTabs.map(tab => ( + +
+ {(codeSnippets[tab.id] || []).map((snippet, idx) => ( + +
+
+
+ {snippet.name} +
+
+                                                            {snippet.code}
+                                                        
+
+
+
+ ))} +
+
+ ))} +
+
+
+
+
+ ); +}