Code cleanup
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
import React, { useState, useRef } from "react";
|
||||
import "../App.css";
|
||||
import "../styles.css";
|
||||
|
||||
// ============================================================================
|
||||
// CONSTANTS
|
||||
// ============================================================================
|
||||
|
||||
const DEMO_TABS = `
|
||||
4 6 -5 5 -4 4 3
|
||||
@@ -7,29 +11,44 @@ const DEMO_TABS = `
|
||||
4 4 4 5 5' 5 5 5 -5 5 4
|
||||
`;
|
||||
|
||||
const HarmonicaTabGenerator = () => {
|
||||
const [input, setInput] = useState(DEMO_TABS);
|
||||
const [title, setTitle] = useState("My Harmonica Tab");
|
||||
const [maxLinesPerColumn, setMaxLinesPerColumn] = useState(10);
|
||||
const svgRef = useRef(null);
|
||||
const SVG_CONFIG = {
|
||||
lineHeight: 40,
|
||||
fontSize: 28,
|
||||
annotationFontSize: 12,
|
||||
titleFontSize: 36,
|
||||
titleHeight: 80,
|
||||
padding: 40,
|
||||
columnGap: 80,
|
||||
charWidth: 17.3, // For tab lines (monospace)
|
||||
textCharWidth: 7, // For annotation lines
|
||||
minColumnWidth: 200,
|
||||
canvasScale: 2, // For PNG export quality
|
||||
backgroundColor: "#fffef5",
|
||||
};
|
||||
|
||||
// Parse input and add + signs to positive numbers
|
||||
// ============================================================================
|
||||
// UTILITY FUNCTIONS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Parse input text and identify tab lines vs annotation lines
|
||||
* Adds + signs to positive numbers in tab lines
|
||||
*/
|
||||
const parseInput = (text) => {
|
||||
const lines = text.split("\n");
|
||||
|
||||
return lines.map((line) => {
|
||||
// Check if line contains tab notation (numbers with optional minus signs, quotes/backticks for bends, and spaces)
|
||||
// Tab lines contain only numbers, spaces, minus signs, and bend markers
|
||||
const isTabLine =
|
||||
/^[\s\d\-'"`'']+$/.test(line.trim()) && line.trim().length > 0;
|
||||
|
||||
let processedContent = line;
|
||||
|
||||
// If it's a tab line, add + signs to positive numbers (but preserve bend notation)
|
||||
// Add + signs to positive numbers in tab lines
|
||||
if (isTabLine) {
|
||||
processedContent = line.replace(
|
||||
/(\s|^)(\d+)(['"`'']?)/g,
|
||||
(match, space, number, bend) => {
|
||||
return space + "+" + number + bend;
|
||||
},
|
||||
(match, space, number, bend) => space + "+" + number + bend,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -40,8 +59,10 @@ const HarmonicaTabGenerator = () => {
|
||||
});
|
||||
};
|
||||
|
||||
// Calculate layout (columns based on max lines per column)
|
||||
const calculateLayout = (parsedLines) => {
|
||||
/**
|
||||
* Calculate column layout based on max lines per column
|
||||
*/
|
||||
const calculateLayout = (parsedLines, maxLinesPerColumn) => {
|
||||
const tabLines = parsedLines.filter((l) => l.isTab);
|
||||
const numColumns = Math.ceil(tabLines.length / maxLinesPerColumn);
|
||||
|
||||
@@ -52,10 +73,7 @@ const HarmonicaTabGenerator = () => {
|
||||
const columnLines = [];
|
||||
let tabCount = 0;
|
||||
|
||||
while (
|
||||
currentIndex < parsedLines.length &&
|
||||
tabCount < maxLinesPerColumn
|
||||
) {
|
||||
while (currentIndex < parsedLines.length && tabCount < maxLinesPerColumn) {
|
||||
const line = parsedLines[currentIndex];
|
||||
columnLines.push(line);
|
||||
|
||||
@@ -71,60 +89,89 @@ const HarmonicaTabGenerator = () => {
|
||||
return columns;
|
||||
};
|
||||
|
||||
const parsedLines = parseInput(input);
|
||||
const columns = calculateLayout(parsedLines);
|
||||
|
||||
// Calculate the width of each column based on content
|
||||
/**
|
||||
* Calculate the width needed for a column based on its content
|
||||
*/
|
||||
const calculateColumnWidth = (column) => {
|
||||
let maxLength = 0;
|
||||
|
||||
column.forEach((line) => {
|
||||
if (line.content.trim()) {
|
||||
// Adjusted for letterSpacing of 0.5 instead of 2
|
||||
// Monospace at 28px is about 16.8px per char + 0.5px spacing
|
||||
const charWidth = line.isTab ? 17.3 : 7;
|
||||
const charWidth = line.isTab
|
||||
? SVG_CONFIG.charWidth
|
||||
: SVG_CONFIG.textCharWidth;
|
||||
const length = line.content.length * charWidth;
|
||||
maxLength = Math.max(maxLength, length);
|
||||
}
|
||||
});
|
||||
return Math.max(maxLength, 200); // Minimum 200px per column
|
||||
|
||||
return Math.max(maxLength, SVG_CONFIG.minColumnWidth);
|
||||
};
|
||||
|
||||
const columnWidths = columns.map(calculateColumnWidth);
|
||||
/**
|
||||
* Calculate SVG dimensions based on content
|
||||
*/
|
||||
const calculateSVGDimensions = (columns, columnWidths) => {
|
||||
const totalContentWidth = columnWidths.reduce((sum, width) => sum + width, 0);
|
||||
|
||||
// SVG dimensions and styling
|
||||
const lineHeight = 40;
|
||||
const fontSize = 28;
|
||||
const annotationFontSize = 12;
|
||||
const titleFontSize = 36;
|
||||
const titleHeight = 80;
|
||||
const padding = 40;
|
||||
const columnGap = 80;
|
||||
|
||||
const maxLinesInAnyColumn = Math.max(
|
||||
...columns.map((col) => {
|
||||
return col.reduce((sum, line) => {
|
||||
return sum + (line.isTab ? 1 : 0.5);
|
||||
}, 0);
|
||||
return col.reduce((sum, line) => sum + (line.isTab ? 1 : 0.5), 0);
|
||||
}),
|
||||
1,
|
||||
);
|
||||
|
||||
const svgWidth =
|
||||
totalContentWidth + (columns.length - 1) * columnGap + padding * 2;
|
||||
const svgHeight =
|
||||
maxLinesInAnyColumn * lineHeight + padding * 2 + titleHeight;
|
||||
totalContentWidth +
|
||||
(columns.length - 1) * SVG_CONFIG.columnGap +
|
||||
SVG_CONFIG.padding * 2;
|
||||
|
||||
// Export as PNG
|
||||
const svgHeight =
|
||||
maxLinesInAnyColumn * SVG_CONFIG.lineHeight +
|
||||
SVG_CONFIG.padding * 2 +
|
||||
SVG_CONFIG.titleHeight;
|
||||
|
||||
return { svgWidth, svgHeight };
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// MAIN COMPONENT
|
||||
// ============================================================================
|
||||
|
||||
const HarmonicaTabGenerator = () => {
|
||||
// State
|
||||
const [input, setInput] = useState(DEMO_TABS);
|
||||
const [title, setTitle] = useState("My Harmonica Tab");
|
||||
const [maxLinesPerColumn, setMaxLinesPerColumn] = useState(10);
|
||||
|
||||
// Ref for SVG element
|
||||
const svgRef = useRef(null);
|
||||
|
||||
// ============================================================================
|
||||
// COMPUTED VALUES
|
||||
// ============================================================================
|
||||
|
||||
const parsedLines = parseInput(input);
|
||||
const columns = calculateLayout(parsedLines, maxLinesPerColumn);
|
||||
const columnWidths = columns.map(calculateColumnWidth);
|
||||
const { svgWidth, svgHeight } = calculateSVGDimensions(columns, columnWidths);
|
||||
|
||||
// ============================================================================
|
||||
// EVENT HANDLERS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Export the SVG as a PNG image
|
||||
*/
|
||||
const exportAsPNG = () => {
|
||||
const svgElement = svgRef.current;
|
||||
const svgData = new XMLSerializer().serializeToString(svgElement);
|
||||
const canvas = document.createElement("canvas");
|
||||
const ctx = canvas.getContext("2d");
|
||||
|
||||
// Set canvas size (2x for better quality)
|
||||
canvas.width = svgWidth * 2;
|
||||
canvas.height = svgHeight * 2;
|
||||
// Set canvas size with 2x scaling for better quality
|
||||
canvas.width = svgWidth * SVG_CONFIG.canvasScale;
|
||||
canvas.height = svgHeight * SVG_CONFIG.canvasScale;
|
||||
|
||||
const img = new Image();
|
||||
const svgBlob = new Blob([svgData], {
|
||||
@@ -133,7 +180,7 @@ const HarmonicaTabGenerator = () => {
|
||||
const url = URL.createObjectURL(svgBlob);
|
||||
|
||||
img.onload = () => {
|
||||
ctx.scale(2, 2);
|
||||
ctx.scale(SVG_CONFIG.canvasScale, SVG_CONFIG.canvasScale);
|
||||
ctx.drawImage(img, 0, 0);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
@@ -148,7 +195,9 @@ const HarmonicaTabGenerator = () => {
|
||||
img.src = url;
|
||||
};
|
||||
|
||||
// Export as SVG
|
||||
/**
|
||||
* Export the SVG as an SVG file
|
||||
*/
|
||||
const exportAsSVG = () => {
|
||||
const svgElement = svgRef.current;
|
||||
const svgData = new XMLSerializer().serializeToString(svgElement);
|
||||
@@ -159,23 +208,67 @@ const HarmonicaTabGenerator = () => {
|
||||
link.click();
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// RENDER
|
||||
// ============================================================================
|
||||
|
||||
return (
|
||||
<div className="gradient-bg">
|
||||
<div className="container">
|
||||
{/* Header */}
|
||||
<h1 className="title">🎵 Harmonica Tabs Image Generator</h1>
|
||||
<p className="subtitle">
|
||||
Create beautiful, readable images of your harmonica tabs
|
||||
</p>
|
||||
|
||||
{/* Instructions Section - MOVED TO TOP */}
|
||||
{/* Instructions */}
|
||||
<InstructionsSection />
|
||||
|
||||
{/* Settings */}
|
||||
<SettingsSection
|
||||
title={title}
|
||||
setTitle={setTitle}
|
||||
maxLinesPerColumn={maxLinesPerColumn}
|
||||
setMaxLinesPerColumn={setMaxLinesPerColumn}
|
||||
/>
|
||||
|
||||
{/* Input */}
|
||||
<InputSection
|
||||
input={input}
|
||||
setInput={setInput}
|
||||
exportAsPNG={exportAsPNG}
|
||||
exportAsSVG={exportAsSVG}
|
||||
/>
|
||||
|
||||
{/* Preview */}
|
||||
<PreviewSection
|
||||
svgRef={svgRef}
|
||||
svgWidth={svgWidth}
|
||||
svgHeight={svgHeight}
|
||||
title={title}
|
||||
columns={columns}
|
||||
columnWidths={columnWidths}
|
||||
exportAsPNG={exportAsPNG}
|
||||
exportAsSVG={exportAsSVG}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// SUB-COMPONENTS
|
||||
// ============================================================================
|
||||
|
||||
const InstructionsSection = () => (
|
||||
<div className="card instructions-card">
|
||||
<h2 className="section-title">📋 How to Use</h2>
|
||||
<ul className="instructions">
|
||||
<li className="instruction-item">
|
||||
<span className="instruction-number">1</span>
|
||||
<div className="instruction-content">
|
||||
<strong>Enter your tabs</strong> - Type your harmonica tab
|
||||
numbers with spaces
|
||||
<strong>Enter your tabs</strong> - Type your harmonica tab numbers
|
||||
with spaces
|
||||
</div>
|
||||
</li>
|
||||
<li className="instruction-item">
|
||||
@@ -188,15 +281,14 @@ const HarmonicaTabGenerator = () => {
|
||||
<li className="instruction-item">
|
||||
<span className="instruction-number">3</span>
|
||||
<div className="instruction-content">
|
||||
<strong>Add annotations</strong> - Plain text appears smaller
|
||||
and italicized
|
||||
<strong>Add annotations</strong> - Plain text appears smaller and
|
||||
italicized
|
||||
</div>
|
||||
</li>
|
||||
<li className="instruction-item">
|
||||
<span className="instruction-number">4</span>
|
||||
<div className="instruction-content">
|
||||
<strong>Customize layout</strong> - Set your title and rows per
|
||||
column
|
||||
<strong>Customize layout</strong> - Set your title and rows per column
|
||||
</div>
|
||||
</li>
|
||||
<li className="instruction-item">
|
||||
@@ -207,17 +299,21 @@ const HarmonicaTabGenerator = () => {
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
|
||||
{/* Settings Section */}
|
||||
const SettingsSection = ({
|
||||
title,
|
||||
setTitle,
|
||||
maxLinesPerColumn,
|
||||
setMaxLinesPerColumn,
|
||||
}) => (
|
||||
<div className="card settings-card">
|
||||
<h2 className="section-title">⚙️ Settings</h2>
|
||||
<div className="settings-grid">
|
||||
<div className="setting-item">
|
||||
<label className="setting-label">
|
||||
Tab Title
|
||||
<span className="setting-hint">
|
||||
Appears at the top of your image
|
||||
</span>
|
||||
<span className="setting-hint">Appears at the top of your image</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -240,9 +336,7 @@ const HarmonicaTabGenerator = () => {
|
||||
min="5"
|
||||
max="20"
|
||||
value={maxLinesPerColumn}
|
||||
onChange={(e) =>
|
||||
setMaxLinesPerColumn(parseInt(e.target.value))
|
||||
}
|
||||
onChange={(e) => setMaxLinesPerColumn(parseInt(e.target.value))}
|
||||
className="slider"
|
||||
/>
|
||||
<span className="slider-value">{maxLinesPerColumn}</span>
|
||||
@@ -250,8 +344,9 @@ const HarmonicaTabGenerator = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
{/* Input Section */}
|
||||
const InputSection = ({ input, setInput, exportAsPNG, exportAsSVG }) => (
|
||||
<div className="card">
|
||||
<h2 className="section-title">✏️ Input Your Tabs</h2>
|
||||
<textarea
|
||||
@@ -276,13 +371,22 @@ Add quotes after numbers for bends"
|
||||
</button>
|
||||
</div>
|
||||
<p className="tip">
|
||||
💡 <strong>Pro tip:</strong> Mix tab lines with text for
|
||||
annotations. Tab lines contain only numbers, spaces, and bend
|
||||
markers (' " ` ').
|
||||
💡 <strong>Pro tip:</strong> Mix tab lines with text for annotations. Tab
|
||||
lines contain only numbers, spaces, and bend markers (' " ` ').
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
{/* Preview Section */}
|
||||
const PreviewSection = ({
|
||||
svgRef,
|
||||
svgWidth,
|
||||
svgHeight,
|
||||
title,
|
||||
columns,
|
||||
columnWidths,
|
||||
exportAsPNG,
|
||||
exportAsSVG,
|
||||
}) => (
|
||||
<div className="card preview-card">
|
||||
<h2 className="section-title">👁️ Preview</h2>
|
||||
<div className="preview-container">
|
||||
@@ -291,18 +395,22 @@ Add quotes after numbers for bends"
|
||||
width={svgWidth}
|
||||
height={svgHeight}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
style={{ background: "#fffef5", display: "block" }}
|
||||
style={{ background: SVG_CONFIG.backgroundColor, display: "block" }}
|
||||
>
|
||||
{/* Background with soft cream color */}
|
||||
<rect width={svgWidth} height={svgHeight} fill="#fffef5" />
|
||||
{/* Background */}
|
||||
<rect
|
||||
width={svgWidth}
|
||||
height={svgHeight}
|
||||
fill={SVG_CONFIG.backgroundColor}
|
||||
/>
|
||||
|
||||
{/* Title - centered at top */}
|
||||
{/* Title */}
|
||||
{title && (
|
||||
<text
|
||||
x={svgWidth / 2}
|
||||
y={padding + titleFontSize}
|
||||
y={SVG_CONFIG.padding + SVG_CONFIG.titleFontSize}
|
||||
fontFamily="'Segoe UI', 'Arial Rounded MT Bold', Arial, sans-serif"
|
||||
fontSize={titleFontSize}
|
||||
fontSize={SVG_CONFIG.titleFontSize}
|
||||
fontWeight="700"
|
||||
fill="#5c4a72"
|
||||
textAnchor="middle"
|
||||
@@ -312,13 +420,14 @@ Add quotes after numbers for bends"
|
||||
</text>
|
||||
)}
|
||||
|
||||
{/* Render columns */}
|
||||
{/* Columns */}
|
||||
{columns.map((column, colIndex) => {
|
||||
let yOffset = padding + titleHeight;
|
||||
// Calculate xOffset based on cumulative widths of previous columns
|
||||
let xOffset = padding;
|
||||
let yOffset = SVG_CONFIG.padding + SVG_CONFIG.titleHeight;
|
||||
|
||||
// Calculate x position based on previous columns
|
||||
let xOffset = SVG_CONFIG.padding;
|
||||
for (let i = 0; i < colIndex; i++) {
|
||||
xOffset += columnWidths[i] + columnGap;
|
||||
xOffset += columnWidths[i] + SVG_CONFIG.columnGap;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -327,15 +436,15 @@ Add quotes after numbers for bends"
|
||||
const currentY = yOffset;
|
||||
|
||||
if (line.isTab) {
|
||||
// Render tab line with monospace font - softer color
|
||||
yOffset += lineHeight;
|
||||
// Tab line - monospace, larger font
|
||||
yOffset += SVG_CONFIG.lineHeight;
|
||||
return (
|
||||
<text
|
||||
key={lineIndex}
|
||||
x={xOffset}
|
||||
y={currentY}
|
||||
fontFamily="'Courier New', monospace"
|
||||
fontSize={fontSize}
|
||||
fontSize={SVG_CONFIG.fontSize}
|
||||
fontWeight="600"
|
||||
fill="#3d5a6c"
|
||||
letterSpacing="0.5"
|
||||
@@ -344,15 +453,15 @@ Add quotes after numbers for bends"
|
||||
</text>
|
||||
);
|
||||
} else if (line.content.trim()) {
|
||||
// Render annotation with smaller font - soft gray
|
||||
yOffset += lineHeight * 0.6;
|
||||
// Annotation line - smaller, italic
|
||||
yOffset += SVG_CONFIG.lineHeight * 0.6;
|
||||
return (
|
||||
<text
|
||||
key={lineIndex}
|
||||
x={xOffset}
|
||||
y={currentY}
|
||||
fontFamily="Arial, sans-serif"
|
||||
fontSize={annotationFontSize}
|
||||
fontSize={SVG_CONFIG.annotationFontSize}
|
||||
fontStyle="italic"
|
||||
fill="#8b7e99"
|
||||
>
|
||||
@@ -376,9 +485,6 @@ Add quotes after numbers for bends"
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HarmonicaTabGenerator;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
/* Game Freak-inspired soft pastel color scheme */
|
||||
/* ============================================================================
|
||||
CSS VARIABLES - Color Palette
|
||||
============================================================================ */
|
||||
:root {
|
||||
--cream: #fffef5;
|
||||
--soft-purple: #5c4a72;
|
||||
@@ -12,6 +14,9 @@
|
||||
--accent-mint: #b5ead7;
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
GLOBAL STYLES
|
||||
============================================================================ */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@@ -29,6 +34,9 @@ body {
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
LAYOUT
|
||||
============================================================================ */
|
||||
.gradient-bg {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(
|
||||
@@ -46,6 +54,9 @@ body {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
TYPOGRAPHY
|
||||
============================================================================ */
|
||||
.title {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 800;
|
||||
@@ -64,6 +75,19 @@ body {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--soft-purple);
|
||||
margin-bottom: 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
CARDS
|
||||
============================================================================ */
|
||||
.card {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(10px);
|
||||
@@ -84,6 +108,7 @@ body {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* Card Variants */
|
||||
.instructions-card {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
@@ -109,16 +134,9 @@ body {
|
||||
);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--soft-purple);
|
||||
margin-bottom: 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
INSTRUCTIONS
|
||||
============================================================================ */
|
||||
.instructions {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
@@ -169,6 +187,9 @@ body {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
SETTINGS
|
||||
============================================================================ */
|
||||
.settings-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
@@ -205,6 +226,9 @@ body {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
FORM INPUTS
|
||||
============================================================================ */
|
||||
.input-field {
|
||||
padding: 0.875rem 1.125rem;
|
||||
border: 2px solid var(--soft-gray);
|
||||
@@ -223,6 +247,29 @@ body {
|
||||
box-shadow: 0 0 0 4px rgba(137, 184, 212, 0.1);
|
||||
}
|
||||
|
||||
.textarea {
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
border: 2px solid var(--soft-gray);
|
||||
border-radius: 16px;
|
||||
font-family: "Courier New", monospace;
|
||||
font-size: 1rem;
|
||||
line-height: 1.8;
|
||||
resize: vertical;
|
||||
background: white;
|
||||
color: var(--text-primary);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--pastel-blue);
|
||||
box-shadow: 0 0 0 4px rgba(137, 184, 212, 0.1);
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
SLIDER
|
||||
============================================================================ */
|
||||
.slider-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -293,26 +340,9 @@ body {
|
||||
box-shadow: 0 2px 8px rgba(92, 74, 114, 0.2);
|
||||
}
|
||||
|
||||
.textarea {
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
border: 2px solid var(--soft-gray);
|
||||
border-radius: 16px;
|
||||
font-family: "Courier New", monospace;
|
||||
font-size: 1rem;
|
||||
line-height: 1.8;
|
||||
resize: vertical;
|
||||
background: white;
|
||||
color: var(--text-primary);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--pastel-blue);
|
||||
box-shadow: 0 0 0 4px rgba(137, 184, 212, 0.1);
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
BUTTONS
|
||||
============================================================================ */
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
@@ -362,25 +392,9 @@ body {
|
||||
background: linear-gradient(135deg, #99c4dc, var(--pastel-blue));
|
||||
}
|
||||
|
||||
.tip {
|
||||
margin-top: 1rem;
|
||||
padding: 1rem;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(255, 154, 162, 0.1) 0%,
|
||||
rgba(181, 234, 215, 0.1) 100%
|
||||
);
|
||||
border-radius: 12px;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.95rem;
|
||||
border-left: 4px solid var(--accent-coral);
|
||||
}
|
||||
|
||||
.tip strong {
|
||||
color: var(--soft-purple);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
PREVIEW
|
||||
============================================================================ */
|
||||
.preview-container {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
@@ -402,7 +416,54 @@ body {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Mobile responsive */
|
||||
/* ============================================================================
|
||||
TIP
|
||||
============================================================================ */
|
||||
.tip {
|
||||
margin-top: 1rem;
|
||||
padding: 1rem;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(255, 154, 162, 0.1) 0%,
|
||||
rgba(181, 234, 215, 0.1) 100%
|
||||
);
|
||||
border-radius: 12px;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.95rem;
|
||||
border-left: 4px solid var(--accent-coral);
|
||||
}
|
||||
|
||||
.tip strong {
|
||||
color: var(--soft-purple);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
SCROLLBAR
|
||||
============================================================================ */
|
||||
::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--soft-gray);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--pastel-blue);
|
||||
border-radius: 8px;
|
||||
border: 2px solid var(--soft-gray);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--soft-purple);
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
RESPONSIVE
|
||||
============================================================================ */
|
||||
@media (max-width: 768px) {
|
||||
.title {
|
||||
font-size: 2rem;
|
||||
@@ -425,24 +486,3 @@ body {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Scrollbar styling */
|
||||
::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--soft-gray);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--pastel-blue);
|
||||
border-radius: 8px;
|
||||
border: 2px solid var(--soft-gray);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--soft-purple);
|
||||
}
|
||||
Reference in New Issue
Block a user