Styles updated. Bend notes support added
This commit is contained in:
455
src/App.css
455
src/App.css
@@ -1,42 +1,433 @@
|
|||||||
#root {
|
/* Game Freak-inspired soft pastel color scheme */
|
||||||
max-width: 1280px;
|
: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;
|
margin: 0 auto;
|
||||||
padding: 2rem;
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: 800;
|
||||||
|
color: var(--soft-purple);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
text-shadow: 2px 2px 0px rgba(255, 255, 255, 0.5);
|
||||||
|
letter-spacing: -0.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
.subtitle {
|
||||||
height: 6em;
|
text-align: center;
|
||||||
padding: 1.5em;
|
font-size: 1.1rem;
|
||||||
will-change: filter;
|
color: var(--text-secondary);
|
||||||
transition: filter 300ms;
|
margin-bottom: 2rem;
|
||||||
}
|
font-weight: 500;
|
||||||
.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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.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 {
|
.card:hover {
|
||||||
color: #888;
|
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 = `
|
const DEMO_TABS = `
|
||||||
4 6 -5 5 -4 4 3
|
4 6 -5 5 -4 4 3
|
||||||
6 7 -6 6 -6 6 5
|
6 7 -6 6 -6' 6 5
|
||||||
4 4 4 5 5 5 5 5 -5 5 4
|
4 4 4 5 5' 5 5 5 -5 5 4
|
||||||
`;
|
`;
|
||||||
const MAX_LINES_PER_COLUMN = 10;
|
|
||||||
|
|
||||||
const HarmonicaTabGenerator = () => {
|
const HarmonicaTabGenerator = () => {
|
||||||
const [input, setInput] = useState(DEMO_TABS);
|
const [input, setInput] = useState(DEMO_TABS);
|
||||||
|
const [title, setTitle] = useState("My Harmonica Tab");
|
||||||
|
const [maxLinesPerColumn, setMaxLinesPerColumn] = useState(10);
|
||||||
const svgRef = useRef(null);
|
const svgRef = useRef(null);
|
||||||
|
|
||||||
// Parse input and add + signs to positive numbers
|
// Parse input and add + signs to positive numbers
|
||||||
const parseInput = (text) => {
|
const parseInput = (text) => {
|
||||||
const lines = text.split("\n");
|
const lines = text.split("\n");
|
||||||
return lines.map((line) => {
|
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 =
|
const isTabLine =
|
||||||
/^[\s\d-]+$/.test(line.trim()) && line.trim().length > 0;
|
/^[\s\d-'"]+$/.test(line.trim()) && line.trim().length > 0;
|
||||||
|
|
||||||
let processedContent = line;
|
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) {
|
if (isTabLine) {
|
||||||
processedContent = line.replace(
|
processedContent = line.replace(
|
||||||
/(\s|^)(\d+)/g,
|
/(\s|^)(\d+)(['"]?)/g,
|
||||||
(match, space, number) => {
|
(match, space, number, bend) => {
|
||||||
return space + "+" + number;
|
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 calculateLayout = (parsedLines) => {
|
||||||
const tabLines = parsedLines.filter((l) => l.isTab);
|
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 = [];
|
const columns = [];
|
||||||
let currentIndex = 0;
|
let currentIndex = 0;
|
||||||
@@ -52,7 +53,7 @@ const HarmonicaTabGenerator = () => {
|
|||||||
|
|
||||||
while (
|
while (
|
||||||
currentIndex < parsedLines.length &&
|
currentIndex < parsedLines.length &&
|
||||||
tabCount < MAX_LINES_PER_COLUMN
|
tabCount < maxLinesPerColumn
|
||||||
) {
|
) {
|
||||||
const line = parsedLines[currentIndex];
|
const line = parsedLines[currentIndex];
|
||||||
columnLines.push(line);
|
columnLines.push(line);
|
||||||
@@ -76,6 +77,8 @@ const HarmonicaTabGenerator = () => {
|
|||||||
const lineHeight = 40;
|
const lineHeight = 40;
|
||||||
const fontSize = 28;
|
const fontSize = 28;
|
||||||
const annotationFontSize = 12;
|
const annotationFontSize = 12;
|
||||||
|
const titleFontSize = 36;
|
||||||
|
const titleHeight = 80;
|
||||||
const columnWidth = 400;
|
const columnWidth = 400;
|
||||||
const padding = 40;
|
const padding = 40;
|
||||||
const columnGap = 60;
|
const columnGap = 60;
|
||||||
@@ -93,7 +96,8 @@ const HarmonicaTabGenerator = () => {
|
|||||||
columns.length * columnWidth +
|
columns.length * columnWidth +
|
||||||
(columns.length - 1) * columnGap +
|
(columns.length - 1) * columnGap +
|
||||||
padding * 2;
|
padding * 2;
|
||||||
const svgHeight = maxLinesInAnyColumn * lineHeight + padding * 2;
|
const svgHeight =
|
||||||
|
maxLinesInAnyColumn * lineHeight + padding * 2 + titleHeight;
|
||||||
|
|
||||||
// Export as PNG
|
// Export as PNG
|
||||||
const exportAsPNG = () => {
|
const exportAsPNG = () => {
|
||||||
@@ -142,14 +146,98 @@ const HarmonicaTabGenerator = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="gradient-bg">
|
<div className="gradient-bg">
|
||||||
<div className="container">
|
<div className="container">
|
||||||
<h1 className="title">Harmonica Tabs Image Generator</h1>
|
<h1 className="title">🎵 Harmonica Tabs Image Generator</h1>
|
||||||
<p className="subtitle">
|
<p className="subtitle">
|
||||||
Create beautiful, readable images of your harmonica tabs
|
Create beautiful, readable images of your harmonica tabs
|
||||||
</p>
|
</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 */}
|
{/* Input Section */}
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<h2 className="section-title">Input Your Tabs</h2>
|
<h2 className="section-title">✏️ Input Your Tabs</h2>
|
||||||
<textarea
|
<textarea
|
||||||
value={input}
|
value={input}
|
||||||
onChange={(e) => setInput(e.target.value)}
|
onChange={(e) => setInput(e.target.value)}
|
||||||
@@ -158,7 +246,9 @@ const HarmonicaTabGenerator = () => {
|
|||||||
|
|
||||||
Example:
|
Example:
|
||||||
4 6 -5 5 -4 4 3
|
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">
|
<div className="button-group">
|
||||||
<button onClick={exportAsPNG} className="button button-primary">
|
<button onClick={exportAsPNG} className="button button-primary">
|
||||||
@@ -169,28 +259,45 @@ const HarmonicaTabGenerator = () => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p className="tip">
|
<p className="tip">
|
||||||
💡 Tip: Positive numbers will automatically get a + sign. Add text
|
💡 <strong>Pro tip:</strong> Mix tab lines with text for
|
||||||
for annotations - they'll appear smaller.
|
annotations. Tab lines contain only numbers, spaces, and bend
|
||||||
|
markers (' or ").
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Preview Section */}
|
{/* Preview Section */}
|
||||||
<div className="card">
|
<div className="card preview-card">
|
||||||
<h2 className="section-title">Preview</h2>
|
<h2 className="section-title">👁️ Preview</h2>
|
||||||
<div className="preview-container">
|
<div className="preview-container">
|
||||||
<svg
|
<svg
|
||||||
ref={svgRef}
|
ref={svgRef}
|
||||||
width={svgWidth}
|
width={svgWidth}
|
||||||
height={svgHeight}
|
height={svgHeight}
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
style={{ background: "#ffffff", display: "block" }}
|
style={{ background: "#fffef5", display: "block" }}
|
||||||
>
|
>
|
||||||
{/* Background */}
|
{/* Background with soft cream color */}
|
||||||
<rect width={svgWidth} height={svgHeight} fill="#ffffff" />
|
<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 */}
|
{/* Render columns */}
|
||||||
{columns.map((column, colIndex) => {
|
{columns.map((column, colIndex) => {
|
||||||
let yOffset = padding;
|
let yOffset = padding + titleHeight;
|
||||||
const xOffset = padding + colIndex * (columnWidth + columnGap);
|
const xOffset = padding + colIndex * (columnWidth + columnGap);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -199,7 +306,7 @@ const HarmonicaTabGenerator = () => {
|
|||||||
const currentY = yOffset;
|
const currentY = yOffset;
|
||||||
|
|
||||||
if (line.isTab) {
|
if (line.isTab) {
|
||||||
// Render tab line with monospace font
|
// Render tab line with monospace font - softer color
|
||||||
yOffset += lineHeight;
|
yOffset += lineHeight;
|
||||||
return (
|
return (
|
||||||
<text
|
<text
|
||||||
@@ -209,14 +316,14 @@ const HarmonicaTabGenerator = () => {
|
|||||||
fontFamily="'Courier New', monospace"
|
fontFamily="'Courier New', monospace"
|
||||||
fontSize={fontSize}
|
fontSize={fontSize}
|
||||||
fontWeight="600"
|
fontWeight="600"
|
||||||
fill="#1f2937"
|
fill="#3d5a6c"
|
||||||
letterSpacing="2"
|
letterSpacing="2"
|
||||||
>
|
>
|
||||||
{line.content}
|
{line.content}
|
||||||
</text>
|
</text>
|
||||||
);
|
);
|
||||||
} else if (line.content.trim()) {
|
} else if (line.content.trim()) {
|
||||||
// Render annotation with smaller font
|
// Render annotation with smaller font - soft gray
|
||||||
yOffset += lineHeight * 0.6;
|
yOffset += lineHeight * 0.6;
|
||||||
return (
|
return (
|
||||||
<text
|
<text
|
||||||
@@ -226,7 +333,7 @@ const HarmonicaTabGenerator = () => {
|
|||||||
fontFamily="Arial, sans-serif"
|
fontFamily="Arial, sans-serif"
|
||||||
fontSize={annotationFontSize}
|
fontSize={annotationFontSize}
|
||||||
fontStyle="italic"
|
fontStyle="italic"
|
||||||
fill="#6b7280"
|
fill="#8b7e99"
|
||||||
>
|
>
|
||||||
{line.content}
|
{line.content}
|
||||||
</text>
|
</text>
|
||||||
@@ -240,45 +347,6 @@ const HarmonicaTabGenerator = () => {
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user