Styles updated. Bend notes support added
This commit is contained in:
455
src/App.css
455
src/App.css
@@ -1,42 +1,433 @@
|
||||
#root {
|
||||
max-width: 1280px;
|
||||
/* Game Freak-inspired soft pastel color scheme */
|
||||
:root {
|
||||
--cream: #fffef5;
|
||||
--soft-purple: #5c4a72;
|
||||
--pastel-blue: #89b8d4;
|
||||
--pastel-pink: #f4c4d4;
|
||||
--pastel-yellow: #fff5ba;
|
||||
--soft-gray: #e8e4df;
|
||||
--text-primary: #3d5a6c;
|
||||
--text-secondary: #8b7e99;
|
||||
--accent-coral: #ff9aa2;
|
||||
--accent-mint: #b5ead7;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family:
|
||||
"Segoe UI",
|
||||
"Arial Rounded MT Bold",
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
sans-serif;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.gradient-bg {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--pastel-yellow) 0%,
|
||||
var(--cream) 25%,
|
||||
var(--pastel-pink) 50%,
|
||||
var(--pastel-blue) 100%
|
||||
);
|
||||
padding: 2rem 1rem;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 800;
|
||||
color: var(--soft-purple);
|
||||
text-align: center;
|
||||
margin-bottom: 0.5rem;
|
||||
text-shadow: 2px 2px 0px rgba(255, 255, 255, 0.5);
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
transition: filter 300ms;
|
||||
}
|
||||
.logo:hover {
|
||||
filter: drop-shadow(0 0 2em #646cffaa);
|
||||
}
|
||||
.logo.react:hover {
|
||||
filter: drop-shadow(0 0 2em #61dafbaa);
|
||||
}
|
||||
|
||||
@keyframes logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
a:nth-of-type(2) .logo {
|
||||
animation: logo-spin infinite 20s linear;
|
||||
}
|
||||
.subtitle {
|
||||
text-align: center;
|
||||
font-size: 1.1rem;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 2rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 24px;
|
||||
padding: 2rem;
|
||||
margin-bottom: 1.5rem;
|
||||
box-shadow:
|
||||
0 8px 32px rgba(92, 74, 114, 0.08),
|
||||
0 2px 8px rgba(92, 74, 114, 0.04);
|
||||
border: 2px solid rgba(255, 255, 255, 0.5);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.read-the-docs {
|
||||
color: #888;
|
||||
.card:hover {
|
||||
box-shadow:
|
||||
0 12px 48px rgba(92, 74, 114, 0.12),
|
||||
0 4px 12px rgba(92, 74, 114, 0.06);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.instructions-card {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(255, 245, 186, 0.3) 0%,
|
||||
rgba(255, 255, 255, 0.9) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.settings-card {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(181, 234, 215, 0.3) 0%,
|
||||
rgba(255, 255, 255, 0.9) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.preview-card {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(137, 184, 212, 0.2) 0%,
|
||||
rgba(255, 255, 255, 0.9) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.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 {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.instruction-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
border-radius: 16px;
|
||||
border-left: 4px solid var(--pastel-blue);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.instruction-item:hover {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border-left-color: var(--soft-purple);
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.instruction-number {
|
||||
min-width: 32px;
|
||||
height: 32px;
|
||||
background: linear-gradient(135deg, var(--pastel-blue), var(--pastel-pink));
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 700;
|
||||
font-size: 1rem;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 4px 12px rgba(137, 184, 212, 0.3);
|
||||
}
|
||||
|
||||
.instruction-content {
|
||||
flex: 1;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.instruction-content strong {
|
||||
color: var(--soft-purple);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.settings-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.settings-grid {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.setting-label {
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
font-size: 0.95rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.setting-hint {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 400;
|
||||
color: var(--text-secondary);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
padding: 0.875rem 1rem;
|
||||
border: 2px solid var(--soft-gray);
|
||||
border-radius: 12px;
|
||||
font-size: 1rem;
|
||||
font-family: inherit;
|
||||
background: white;
|
||||
color: var(--text-primary);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.input-field:focus {
|
||||
outline: none;
|
||||
border-color: var(--pastel-blue);
|
||||
box-shadow: 0 0 0 4px rgba(137, 184, 212, 0.1);
|
||||
}
|
||||
|
||||
.slider-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.slider {
|
||||
flex: 1;
|
||||
height: 8px;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
var(--pastel-pink) 0%,
|
||||
var(--pastel-blue) 100%
|
||||
);
|
||||
outline: none;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
.slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
background: white;
|
||||
border: 3px solid var(--soft-purple);
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 8px rgba(92, 74, 114, 0.2);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.slider::-webkit-slider-thumb:hover {
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 4px 12px rgba(92, 74, 114, 0.3);
|
||||
}
|
||||
|
||||
.slider::-moz-range-thumb {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
background: white;
|
||||
border: 3px solid var(--soft-purple);
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 8px rgba(92, 74, 114, 0.2);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.slider::-moz-range-thumb:hover {
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 4px 12px rgba(92, 74, 114, 0.3);
|
||||
}
|
||||
|
||||
.slider-value {
|
||||
min-width: 40px;
|
||||
padding: 0.5rem 1rem;
|
||||
background: var(--soft-purple);
|
||||
color: white;
|
||||
border-radius: 12px;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-top: 1.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.button {
|
||||
padding: 0.875rem 2rem;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-family: inherit;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.button-primary {
|
||||
background: linear-gradient(135deg, var(--soft-purple), #7d6b93);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.button-primary:hover {
|
||||
background: linear-gradient(135deg, #6e5a84, var(--soft-purple));
|
||||
}
|
||||
|
||||
.button-secondary {
|
||||
background: linear-gradient(135deg, var(--pastel-blue), #a5cee0);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.button-secondary:hover {
|
||||
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-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 2rem;
|
||||
background: var(--soft-gray);
|
||||
border-radius: 16px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.preview-container svg {
|
||||
border-radius: 12px;
|
||||
box-shadow:
|
||||
0 8px 32px rgba(92, 74, 114, 0.12),
|
||||
0 2px 8px rgba(92, 74, 114, 0.06);
|
||||
}
|
||||
|
||||
/* Mobile responsive */
|
||||
@media (max-width: 768px) {
|
||||
.title {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.button {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.preview-container {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -2,31 +2,32 @@ import React, { useState, useRef } from "react";
|
||||
|
||||
const DEMO_TABS = `
|
||||
4 6 -5 5 -4 4 3
|
||||
6 7 -6 6 -6 6 5
|
||||
4 4 4 5 5 5 5 5 -5 5 4
|
||||
6 7 -6 6 -6' 6 5
|
||||
4 4 4 5 5' 5 5 5 -5 5 4
|
||||
`;
|
||||
const MAX_LINES_PER_COLUMN = 10;
|
||||
|
||||
const HarmonicaTabGenerator = () => {
|
||||
const [input, setInput] = useState(DEMO_TABS);
|
||||
const [title, setTitle] = useState("My Harmonica Tab");
|
||||
const [maxLinesPerColumn, setMaxLinesPerColumn] = useState(10);
|
||||
const svgRef = useRef(null);
|
||||
|
||||
// Parse input and add + signs to positive numbers
|
||||
const parseInput = (text) => {
|
||||
const lines = text.split("\n");
|
||||
return lines.map((line) => {
|
||||
// Check if line contains tab notation (numbers with optional minus signs and spaces)
|
||||
// Check if line contains tab notation (numbers with optional minus signs, quotes for bends, and spaces)
|
||||
const isTabLine =
|
||||
/^[\s\d-]+$/.test(line.trim()) && line.trim().length > 0;
|
||||
/^[\s\d-'"]+$/.test(line.trim()) && line.trim().length > 0;
|
||||
|
||||
let processedContent = line;
|
||||
|
||||
// If it's a tab line, add + signs to positive numbers
|
||||
// If it's a tab line, add + signs to positive numbers (but preserve bend notation)
|
||||
if (isTabLine) {
|
||||
processedContent = line.replace(
|
||||
/(\s|^)(\d+)/g,
|
||||
(match, space, number) => {
|
||||
return space + "+" + number;
|
||||
/(\s|^)(\d+)(['"]?)/g,
|
||||
(match, space, number, bend) => {
|
||||
return space + "+" + number + bend;
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -38,10 +39,10 @@ const HarmonicaTabGenerator = () => {
|
||||
});
|
||||
};
|
||||
|
||||
// Calculate layout (columns based on max 10 lines per column)
|
||||
// Calculate layout (columns based on max lines per column)
|
||||
const calculateLayout = (parsedLines) => {
|
||||
const tabLines = parsedLines.filter((l) => l.isTab);
|
||||
const numColumns = Math.ceil(tabLines.length / MAX_LINES_PER_COLUMN);
|
||||
const numColumns = Math.ceil(tabLines.length / maxLinesPerColumn);
|
||||
|
||||
const columns = [];
|
||||
let currentIndex = 0;
|
||||
@@ -52,7 +53,7 @@ const HarmonicaTabGenerator = () => {
|
||||
|
||||
while (
|
||||
currentIndex < parsedLines.length &&
|
||||
tabCount < MAX_LINES_PER_COLUMN
|
||||
tabCount < maxLinesPerColumn
|
||||
) {
|
||||
const line = parsedLines[currentIndex];
|
||||
columnLines.push(line);
|
||||
@@ -76,6 +77,8 @@ const HarmonicaTabGenerator = () => {
|
||||
const lineHeight = 40;
|
||||
const fontSize = 28;
|
||||
const annotationFontSize = 12;
|
||||
const titleFontSize = 36;
|
||||
const titleHeight = 80;
|
||||
const columnWidth = 400;
|
||||
const padding = 40;
|
||||
const columnGap = 60;
|
||||
@@ -93,7 +96,8 @@ const HarmonicaTabGenerator = () => {
|
||||
columns.length * columnWidth +
|
||||
(columns.length - 1) * columnGap +
|
||||
padding * 2;
|
||||
const svgHeight = maxLinesInAnyColumn * lineHeight + padding * 2;
|
||||
const svgHeight =
|
||||
maxLinesInAnyColumn * lineHeight + padding * 2 + titleHeight;
|
||||
|
||||
// Export as PNG
|
||||
const exportAsPNG = () => {
|
||||
@@ -142,14 +146,98 @@ const HarmonicaTabGenerator = () => {
|
||||
return (
|
||||
<div className="gradient-bg">
|
||||
<div className="container">
|
||||
<h1 className="title">Harmonica Tabs Image Generator</h1>
|
||||
<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 */}
|
||||
<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
|
||||
</div>
|
||||
</li>
|
||||
<li className="instruction-item">
|
||||
<span className="instruction-number">2</span>
|
||||
<div className="instruction-content">
|
||||
<strong>Auto-formatting</strong> - Positive numbers get a + sign
|
||||
automatically. Use ' or " for bends (e.g., -3', 4")
|
||||
</div>
|
||||
</li>
|
||||
<li className="instruction-item">
|
||||
<span className="instruction-number">3</span>
|
||||
<div className="instruction-content">
|
||||
<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
|
||||
</div>
|
||||
</li>
|
||||
<li className="instruction-item">
|
||||
<span className="instruction-number">5</span>
|
||||
<div className="instruction-content">
|
||||
<strong>Download</strong> - Save as PNG or SVG when ready
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Settings Section */}
|
||||
<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>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
className="input-field"
|
||||
placeholder="Enter a title for your tabs..."
|
||||
/>
|
||||
</div>
|
||||
<div className="setting-item">
|
||||
<label className="setting-label">
|
||||
Rows Per Column
|
||||
<span className="setting-hint">
|
||||
How many lines before splitting into columns
|
||||
</span>
|
||||
</label>
|
||||
<div className="slider-container">
|
||||
<input
|
||||
type="range"
|
||||
min="5"
|
||||
max="20"
|
||||
value={maxLinesPerColumn}
|
||||
onChange={(e) =>
|
||||
setMaxLinesPerColumn(parseInt(e.target.value))
|
||||
}
|
||||
className="slider"
|
||||
/>
|
||||
<span className="slider-value">{maxLinesPerColumn}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Input Section */}
|
||||
<div className="card">
|
||||
<h2 className="section-title">Input Your Tabs</h2>
|
||||
<h2 className="section-title">✏️ Input Your Tabs</h2>
|
||||
<textarea
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
@@ -158,7 +246,9 @@ const HarmonicaTabGenerator = () => {
|
||||
|
||||
Example:
|
||||
4 6 -5 5 -4 4 3
|
||||
6 7 -6 6 -6 6 5"
|
||||
-3' 6 7 -6' 6 5
|
||||
Use ' or " for bends"
|
||||
rows={12}
|
||||
/>
|
||||
<div className="button-group">
|
||||
<button onClick={exportAsPNG} className="button button-primary">
|
||||
@@ -169,28 +259,45 @@ const HarmonicaTabGenerator = () => {
|
||||
</button>
|
||||
</div>
|
||||
<p className="tip">
|
||||
💡 Tip: Positive numbers will automatically get a + sign. Add text
|
||||
for annotations - they'll appear smaller.
|
||||
💡 <strong>Pro tip:</strong> Mix tab lines with text for
|
||||
annotations. Tab lines contain only numbers, spaces, and bend
|
||||
markers (' or ").
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Preview Section */}
|
||||
<div className="card">
|
||||
<h2 className="section-title">Preview</h2>
|
||||
<div className="card preview-card">
|
||||
<h2 className="section-title">👁️ Preview</h2>
|
||||
<div className="preview-container">
|
||||
<svg
|
||||
ref={svgRef}
|
||||
width={svgWidth}
|
||||
height={svgHeight}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
style={{ background: "#ffffff", display: "block" }}
|
||||
style={{ background: "#fffef5", display: "block" }}
|
||||
>
|
||||
{/* Background */}
|
||||
<rect width={svgWidth} height={svgHeight} fill="#ffffff" />
|
||||
{/* Background with soft cream color */}
|
||||
<rect width={svgWidth} height={svgHeight} fill="#fffef5" />
|
||||
|
||||
{/* Title - centered at top */}
|
||||
{title && (
|
||||
<text
|
||||
x={svgWidth / 2}
|
||||
y={padding + titleFontSize}
|
||||
fontFamily="'Segoe UI', 'Arial Rounded MT Bold', Arial, sans-serif"
|
||||
fontSize={titleFontSize}
|
||||
fontWeight="700"
|
||||
fill="#5c4a72"
|
||||
textAnchor="middle"
|
||||
letterSpacing="1"
|
||||
>
|
||||
{title}
|
||||
</text>
|
||||
)}
|
||||
|
||||
{/* Render columns */}
|
||||
{columns.map((column, colIndex) => {
|
||||
let yOffset = padding;
|
||||
let yOffset = padding + titleHeight;
|
||||
const xOffset = padding + colIndex * (columnWidth + columnGap);
|
||||
|
||||
return (
|
||||
@@ -199,7 +306,7 @@ const HarmonicaTabGenerator = () => {
|
||||
const currentY = yOffset;
|
||||
|
||||
if (line.isTab) {
|
||||
// Render tab line with monospace font
|
||||
// Render tab line with monospace font - softer color
|
||||
yOffset += lineHeight;
|
||||
return (
|
||||
<text
|
||||
@@ -209,14 +316,14 @@ const HarmonicaTabGenerator = () => {
|
||||
fontFamily="'Courier New', monospace"
|
||||
fontSize={fontSize}
|
||||
fontWeight="600"
|
||||
fill="#1f2937"
|
||||
fill="#3d5a6c"
|
||||
letterSpacing="2"
|
||||
>
|
||||
{line.content}
|
||||
</text>
|
||||
);
|
||||
} else if (line.content.trim()) {
|
||||
// Render annotation with smaller font
|
||||
// Render annotation with smaller font - soft gray
|
||||
yOffset += lineHeight * 0.6;
|
||||
return (
|
||||
<text
|
||||
@@ -226,7 +333,7 @@ const HarmonicaTabGenerator = () => {
|
||||
fontFamily="Arial, sans-serif"
|
||||
fontSize={annotationFontSize}
|
||||
fontStyle="italic"
|
||||
fill="#6b7280"
|
||||
fill="#8b7e99"
|
||||
>
|
||||
{line.content}
|
||||
</text>
|
||||
@@ -240,45 +347,6 @@ const HarmonicaTabGenerator = () => {
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Instructions */}
|
||||
<div className="card">
|
||||
<h2 className="section-title">How to Use</h2>
|
||||
<ul className="instructions">
|
||||
<li className="instruction-item">
|
||||
<span className="instruction-number">1.</span>
|
||||
<span>
|
||||
Enter your harmonica tabs in the input field (numbers with
|
||||
spaces)
|
||||
</span>
|
||||
</li>
|
||||
<li className="instruction-item">
|
||||
<span className="instruction-number">2.</span>
|
||||
<span>
|
||||
Positive numbers automatically get a + sign, negative numbers
|
||||
keep the - sign
|
||||
</span>
|
||||
</li>
|
||||
<li className="instruction-item">
|
||||
<span className="instruction-number">3.</span>
|
||||
<span>
|
||||
Add text annotations if needed - they'll appear smaller and
|
||||
italicized
|
||||
</span>
|
||||
</li>
|
||||
<li className="instruction-item">
|
||||
<span className="instruction-number">4.</span>
|
||||
<span>
|
||||
Preview updates automatically - tabs are split into columns (max
|
||||
10 lines per column)
|
||||
</span>
|
||||
</li>
|
||||
<li className="instruction-item">
|
||||
<span className="instruction-number">5.</span>
|
||||
<span>Download as PNG for sharing or SVG for editing</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user