Improve pattern builder UX and animations
This commit is contained in:
parent
a1ee84967d
commit
0d6f4a9df3
849
assets/app.js
849
assets/app.js
@ -2,6 +2,282 @@
|
|||||||
let projects = JSON.parse(localStorage.getItem('crochetCounters')) || [];
|
let projects = JSON.parse(localStorage.getItem('crochetCounters')) || [];
|
||||||
let patterns = JSON.parse(localStorage.getItem('crochetPatterns')) || [];
|
let patterns = JSON.parse(localStorage.getItem('crochetPatterns')) || [];
|
||||||
if (!Array.isArray(patterns)) patterns = [];
|
if (!Array.isArray(patterns)) patterns = [];
|
||||||
|
let saveFlashTimer = null;
|
||||||
|
const crochetAbbrev = [
|
||||||
|
{ code: 'alt', desc: 'alternate' },
|
||||||
|
{ code: 'approx', desc: 'approximately' },
|
||||||
|
{ code: 'beg', desc: 'begin/beginning' },
|
||||||
|
{ code: 'bet', desc: 'between' },
|
||||||
|
{ code: 'bl/blo', desc: 'back loop/back loop only' },
|
||||||
|
{ code: 'bo', desc: 'bobble' },
|
||||||
|
{ code: 'bp', desc: 'back post' },
|
||||||
|
{ code: 'bpdc', desc: 'back post double crochet' },
|
||||||
|
{ code: 'bpdtr', desc: 'back post double treble crochet' },
|
||||||
|
{ code: 'bphdc', desc: 'back post half double crochet' },
|
||||||
|
{ code: 'bpsc', desc: 'back post single crochet' },
|
||||||
|
{ code: 'bptr', desc: 'back post treble crochet' },
|
||||||
|
{ code: 'cc', desc: 'contrasting color' },
|
||||||
|
{ code: 'ch', desc: 'chain stitch' },
|
||||||
|
{ code: 'ch-sp', desc: 'chain space' },
|
||||||
|
{ code: 'cl', desc: 'cluster' },
|
||||||
|
{ code: 'cont', desc: 'continue' },
|
||||||
|
{ code: 'dc', desc: 'double crochet' },
|
||||||
|
{ code: 'dc2tog', desc: 'double crochet 2 stitches together' },
|
||||||
|
{ code: 'dec', desc: 'decrease' },
|
||||||
|
{ code: 'dtr', desc: 'double treble crochet' },
|
||||||
|
{ code: 'edc', desc: 'extended double crochet' },
|
||||||
|
{ code: 'ehdc', desc: 'extended half double crochet' },
|
||||||
|
{ code: 'esc', desc: 'extended single crochet' },
|
||||||
|
{ code: 'etr', desc: 'extended treble crochet' },
|
||||||
|
{ code: 'fl/flo', desc: 'front loop/front loop only' },
|
||||||
|
{ code: 'foll', desc: 'following' },
|
||||||
|
{ code: 'fp', desc: 'front post' },
|
||||||
|
{ code: 'fpdc', desc: 'front post double crochet' },
|
||||||
|
{ code: 'fpdtr', desc: 'front post double treble crochet' },
|
||||||
|
{ code: 'fphdc', desc: 'front post half double crochet' },
|
||||||
|
{ code: 'fpsc', desc: 'front post single crochet' },
|
||||||
|
{ code: 'fptr', desc: 'front post treble crochet' },
|
||||||
|
{ code: 'hdc', desc: 'half double crochet' },
|
||||||
|
{ code: 'hdc2tog', desc: 'half double crochet 2 stitches together' },
|
||||||
|
{ code: 'inc', desc: 'increase' },
|
||||||
|
{ code: 'lp', desc: 'loop' },
|
||||||
|
{ code: 'm', desc: 'marker' },
|
||||||
|
{ code: 'mc', desc: 'main color' },
|
||||||
|
{ code: 'pat', desc: 'pattern' },
|
||||||
|
{ code: 'pc', desc: 'popcorn stitch' },
|
||||||
|
{ code: 'pm', desc: 'place marker' },
|
||||||
|
{ code: 'prev', desc: 'previous' },
|
||||||
|
{ code: 'ps/puff', desc: 'puff stitch' },
|
||||||
|
{ code: 'rem', desc: 'remaining' },
|
||||||
|
{ code: 'rep', desc: 'repeat' },
|
||||||
|
{ code: 'rnd', desc: 'round' },
|
||||||
|
{ code: 'rs', desc: 'right side' },
|
||||||
|
{ code: 'sc', desc: 'single crochet' },
|
||||||
|
{ code: 'sc2tog', desc: 'single crochet 2 stitches together' },
|
||||||
|
{ code: 'sh', desc: 'shell' },
|
||||||
|
{ code: 'sk', desc: 'skip' },
|
||||||
|
{ code: 'sl st', desc: 'slip stitch' },
|
||||||
|
{ code: 'sm/sl m', desc: 'slip marker' },
|
||||||
|
{ code: 'sp', desc: 'space' },
|
||||||
|
{ code: 'st', desc: 'stitch' },
|
||||||
|
{ code: 'tbl', desc: 'through back loop' },
|
||||||
|
{ code: 'tch/t-ch', desc: 'turning chain' },
|
||||||
|
{ code: 'tog', desc: 'together' },
|
||||||
|
{ code: 'tr', desc: 'treble crochet' },
|
||||||
|
{ code: 'tr2tog', desc: 'treble crochet 2 stitches together' },
|
||||||
|
{ code: 'trtr', desc: 'triple treble crochet' },
|
||||||
|
{ code: 'ws', desc: 'wrong side' },
|
||||||
|
{ code: 'yo', desc: 'yarn over' },
|
||||||
|
{ code: 'yoh', desc: 'yarn over hook' },
|
||||||
|
// Tunisian
|
||||||
|
{ code: 'etss', desc: 'extended Tunisian simple stitch' },
|
||||||
|
{ code: 'fwp', desc: 'forward pass' },
|
||||||
|
{ code: 'retp', desc: 'return pass' },
|
||||||
|
{ code: 'tdc', desc: 'Tunisian double crochet' },
|
||||||
|
{ code: 'tfs', desc: 'Tunisian full stitch' },
|
||||||
|
{ code: 'thdc', desc: 'Tunisian half double crochet' },
|
||||||
|
{ code: 'tks', desc: 'Tunisian knit stitch' },
|
||||||
|
{ code: 'tps', desc: 'Tunisian purl stitch' },
|
||||||
|
{ code: 'trs', desc: 'Tunisian reverse stitch' },
|
||||||
|
{ code: 'tsc', desc: 'Tunisian single crochet' },
|
||||||
|
{ code: 'tss', desc: 'Tunisian simple stitch' },
|
||||||
|
{ code: 'tslst', desc: 'Tunisian slip stitch' },
|
||||||
|
{ code: 'ttr', desc: 'Tunisian treble crochet' },
|
||||||
|
{ code: 'ttw', desc: 'Tunisian twisted' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const knitAbbrev = [
|
||||||
|
{ code: 'alt', desc: 'alternate' },
|
||||||
|
{ code: 'approx', desc: 'approximately' },
|
||||||
|
{ code: 'beg', desc: 'beginning' },
|
||||||
|
{ code: 'bet', desc: 'between' },
|
||||||
|
{ code: 'BO', desc: 'bind off' },
|
||||||
|
{ code: 'byo', desc: 'backward yarn over' },
|
||||||
|
{ code: 'CC', desc: 'contrasting color' },
|
||||||
|
{ code: 'cn', desc: 'cable needle' },
|
||||||
|
{ code: 'CO', desc: 'cast on' },
|
||||||
|
{ code: 'cont', desc: 'continue' },
|
||||||
|
{ code: 'dec', desc: 'decrease' },
|
||||||
|
{ code: 'dpn', desc: 'double-pointed needles' },
|
||||||
|
{ code: 'foll', desc: 'follow' },
|
||||||
|
{ code: 'inc', desc: 'increase' },
|
||||||
|
{ code: 'k', desc: 'knit' },
|
||||||
|
{ code: 'k1B', desc: 'knit stitch in row below' },
|
||||||
|
{ code: 'kfb', desc: 'knit 1 front and back (inc)' },
|
||||||
|
{ code: 'ksp', desc: 'knit 1, slip back, pass over (dec)' },
|
||||||
|
{ code: 'k2tog', desc: 'knit 2 together (dec)' },
|
||||||
|
{ code: 'kwise', desc: 'knitwise' },
|
||||||
|
{ code: 'LH', desc: 'left hand' },
|
||||||
|
{ code: 'lp', desc: 'loop' },
|
||||||
|
{ code: 'm', desc: 'marker' },
|
||||||
|
{ code: 'M1', desc: 'make one knitwise (inc)' },
|
||||||
|
{ code: 'M1R', desc: 'make one right (inc)' },
|
||||||
|
{ code: 'M1L', desc: 'make one left (inc)' },
|
||||||
|
{ code: 'M1p', desc: 'make one purlwise (inc)' },
|
||||||
|
{ code: 'M1rp', desc: 'make one right purlwise (inc)' },
|
||||||
|
{ code: 'M1lp', desc: 'make one left purlwise (inc)' },
|
||||||
|
{ code: 'MC', desc: 'main color' },
|
||||||
|
{ code: 'p', desc: 'purl' },
|
||||||
|
{ code: 'pat', desc: 'pattern' },
|
||||||
|
{ code: 'pfb', desc: 'purl front and back (inc)' },
|
||||||
|
{ code: 'pm', desc: 'place marker' },
|
||||||
|
{ code: 'p2tog', desc: 'purl 2 together (dec)' },
|
||||||
|
{ code: 'prev', desc: 'previous' },
|
||||||
|
{ code: 'psso', desc: 'pass slipped stitch over' },
|
||||||
|
{ code: 'p2sso', desc: 'pass 2 slipped stitches over' },
|
||||||
|
{ code: 'pwise', desc: 'purlwise' },
|
||||||
|
{ code: 'rem', desc: 'remaining' },
|
||||||
|
{ code: 'rep', desc: 'repeat' },
|
||||||
|
{ code: 'rev St st', desc: 'reverse stockinette stitch' },
|
||||||
|
{ code: 'RH', desc: 'right hand' },
|
||||||
|
{ code: 'rnd', desc: 'round' },
|
||||||
|
{ code: 'RS', desc: 'right side' },
|
||||||
|
{ code: 'SKP', desc: 'slip 1 k-wise, k1, pass slipped over (dec)' },
|
||||||
|
{ code: 'SK2P', desc: 'slip 1 k-wise, k2tog, pass slipped over (dec)' },
|
||||||
|
{ code: 'sl', desc: 'slip' },
|
||||||
|
{ code: 'sl1k', desc: 'slip 1 knitwise' },
|
||||||
|
{ code: 'sl1p', desc: 'slip 1 purlwise' },
|
||||||
|
{ code: 'sl st', desc: 'slip stitch' },
|
||||||
|
{ code: 'sm', desc: 'slip marker' },
|
||||||
|
{ code: 'ssk', desc: 'slip 2 k-wise, k through back loops (dec)' },
|
||||||
|
{ code: 'ssp', desc: 'slip 2 k-wise, return, p through back loops (dec)' },
|
||||||
|
{ code: 'sssk', desc: 'slip 3 k-wise, k through back loops (double dec)' },
|
||||||
|
{ code: 'sssp', desc: 'slip 3 k-wise, return, p through back loops (double dec)' },
|
||||||
|
{ code: 'S2KP2', desc: 'slip 2 as if to k2tog, k1, pass 2 over (double dec)' },
|
||||||
|
{ code: 'SSPP2', desc: 'slip 2 k-wise, return, p2tog tbl, p1, pass 2 over (double dec)' },
|
||||||
|
{ code: 'st', desc: 'stitch' },
|
||||||
|
{ code: 'St st', desc: 'stockinette stitch' },
|
||||||
|
{ code: 'tbl', desc: 'through back loop' },
|
||||||
|
{ code: 'tfl', desc: 'through front loop' },
|
||||||
|
{ code: 'tog', desc: 'together' },
|
||||||
|
{ code: 'WS', desc: 'wrong side' },
|
||||||
|
{ code: 'w&t', desc: 'wrap and turn' },
|
||||||
|
{ code: 'wyib', desc: 'with yarn in back' },
|
||||||
|
{ code: 'wyif', desc: 'with yarn in front' },
|
||||||
|
{ code: 'yb', desc: 'yarn back' },
|
||||||
|
{ code: 'yfwd/yf', desc: 'yarn forward' },
|
||||||
|
{ code: 'yo', desc: 'yarn over' },
|
||||||
|
{ code: 'yon', desc: 'yarn over needle' },
|
||||||
|
{ code: 'yrn', desc: 'yarn round needle' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const crochetBuckets = [
|
||||||
|
{ label: 'Core stitches', codes: ['ch', 'sc', 'hdc', 'dc', 'tr', 'sl st'] },
|
||||||
|
{ label: 'Shaping & repeats', codes: ['inc', 'dec', 'tog', 'rep', 'rem', 'rnd', 'rs', 'ws', 'tch/t-ch'] },
|
||||||
|
{ label: 'Placement & loops', codes: ['fl/flo', 'bl/blo', 'tbl', 'lp', 'sp', 'ch-sp', 'prev'] },
|
||||||
|
{ label: 'Post stitches', codes: ['fp', 'fpdc', 'fptr', 'fpsc', 'fphdc', 'fpdtr', 'bp', 'bpdc', 'bptr', 'bpsc', 'bphdc', 'bpdtr'] },
|
||||||
|
{ label: 'Extended & special', codes: ['pat', 'pc', 'ps/puff', 'cl', 'sh', 'dtr', 'edc', 'ehdc', 'esc', 'etr', 'tr2tog', 'trtr', 'yo', 'yoh'] },
|
||||||
|
{ label: 'Markers & color', codes: ['mc', 'cc', 'm', 'pm', 'sm/sl m'] },
|
||||||
|
{ label: 'Tunisian', codes: ['etss', 'fwp', 'retp', 'tdc', 'tfs', 'thdc', 'tks', 'tps', 'trs', 'tsc', 'tss', 'tslst', 'ttr', 'ttw'] },
|
||||||
|
{ label: 'General terms', codes: ['alt', 'approx', 'beg', 'bet', 'cont', 'foll', 'pat', 'prev'] }
|
||||||
|
];
|
||||||
|
|
||||||
|
const knitBuckets = [
|
||||||
|
{ label: 'Core stitches', codes: ['k', 'p', 'yo', 'sl st', 'St st', 'rev St st'] },
|
||||||
|
{ label: 'Increases', codes: ['inc', 'M1', 'M1R', 'M1L', 'M1p', 'M1rp', 'M1lp', 'kfb', 'pfb'] },
|
||||||
|
{ label: 'Decreases', codes: ['dec', 'k2tog', 'ssk', 'SKP', 'SK2P', 'ssp', 'sssk', 'sssp', 'S2KP2', 'SSPP2', 'p2tog', 'psso', 'p2sso', 'ksp', 'tog'] },
|
||||||
|
{ label: 'Slips & markers', codes: ['sl', 'sl1k', 'sl1p', 'm', 'pm', 'sm', 'cn', 'dpn'] },
|
||||||
|
{ label: 'Yarn moves', codes: ['wyib', 'wyif', 'yfwd/yf', 'yb', 'yon', 'yrn', 'w&t'] },
|
||||||
|
{ label: 'Cast on / bind off', codes: ['CO', 'BO'] },
|
||||||
|
{ label: 'Color & pattern', codes: ['MC', 'CC', 'pat'] },
|
||||||
|
{ label: 'Positioning', codes: ['k1B', 'tbl', 'tfl', 'lp', 'prev'] },
|
||||||
|
{ label: 'Rows & sides', codes: ['rep', 'rem', 'RS', 'WS', 'rnd', 'foll'] },
|
||||||
|
{ label: 'General terms', codes: ['alt', 'approx', 'beg', 'bet', 'cont'] }
|
||||||
|
];
|
||||||
|
|
||||||
|
const repeatTokens = ['*', 'rep from *', '[', ']', '(', ')', 'to end'];
|
||||||
|
const nonMergeTokens = new Set(['*', '[', ']', '(', ')', 'rep from *', 'to end']);
|
||||||
|
|
||||||
|
function getActiveAbbrevLibrary() {
|
||||||
|
return patternDraft.mode === 'knit' ? knitAbbrev : crochetAbbrev;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAbbrevByCode(code) {
|
||||||
|
return crochetAbbrev.find(a => a.code === code) || knitAbbrev.find(a => a.code === code);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildAbbrevGroups(library, buckets) {
|
||||||
|
const index = new Map(library.map(item => [item.code, item]));
|
||||||
|
const used = new Set();
|
||||||
|
const groups = [];
|
||||||
|
buckets.forEach(bucket => {
|
||||||
|
const items = [];
|
||||||
|
bucket.codes.forEach(code => {
|
||||||
|
const item = index.get(code);
|
||||||
|
if (item && !used.has(code)) {
|
||||||
|
items.push(item);
|
||||||
|
used.add(code);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (items.length) groups.push({ label: bucket.label, items });
|
||||||
|
});
|
||||||
|
const leftover = library.filter(item => !used.has(item.code));
|
||||||
|
if (leftover.length) groups.push({ label: 'Other', items: leftover });
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAbbrevGroups() {
|
||||||
|
const library = getActiveAbbrevLibrary();
|
||||||
|
const buckets = patternDraft.mode === 'knit' ? knitBuckets : crochetBuckets;
|
||||||
|
return buildAbbrevGroups(library, buckets);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCoreCodes() {
|
||||||
|
const buckets = patternDraft.mode === 'knit' ? knitBuckets : crochetBuckets;
|
||||||
|
const core = buckets.find(b => b.label === 'Core stitches');
|
||||||
|
return core ? core.codes : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPatternButtonCodes() {
|
||||||
|
const base = (patternDraft.abbrevSelection && patternDraft.abbrevSelection.length)
|
||||||
|
? patternDraft.abbrevSelection
|
||||||
|
: (patternDraft.mode === 'knit'
|
||||||
|
? ['k', 'p', 'yo', 'k2tog', 'ssk', 'sl st']
|
||||||
|
: ['ch', 'sc', 'hdc', 'dc', 'tr', 'inc', 'dec', 'sl st', 'sk', 'rep']);
|
||||||
|
const merged = [...base, ...repeatTokens];
|
||||||
|
return Array.from(new Set(merged));
|
||||||
|
}
|
||||||
|
function normalizePatternDraft(d = {}) {
|
||||||
|
const baseStep = { title: '', rows: [], rowDraft: '', note: '', image: '' };
|
||||||
|
const base = {
|
||||||
|
mode: 'crochet',
|
||||||
|
currentRow: 1,
|
||||||
|
line: '',
|
||||||
|
output: '',
|
||||||
|
meta: { title: '', designer: '' },
|
||||||
|
materials: '',
|
||||||
|
gauge: '',
|
||||||
|
gaugeSts: '',
|
||||||
|
gaugeRows: '',
|
||||||
|
gaugeHook: '',
|
||||||
|
size: '',
|
||||||
|
abbrev: '',
|
||||||
|
abbrevSelection: [],
|
||||||
|
stitches: '',
|
||||||
|
notes: '',
|
||||||
|
steps: []
|
||||||
|
};
|
||||||
|
const merged = { ...base, ...d };
|
||||||
|
merged.meta = { ...base.meta, ...(d.meta || {}) };
|
||||||
|
merged.abbrevSelection = Array.isArray(merged.abbrevSelection) ? merged.abbrevSelection : [];
|
||||||
|
merged.steps = Array.isArray(merged.steps) ? merged.steps.map(s => ({ ...baseStep, ...s })) : [];
|
||||||
|
merged.materials = merged.materials || '';
|
||||||
|
merged.gauge = merged.gauge || '';
|
||||||
|
merged.gaugeSts = merged.gaugeSts || '';
|
||||||
|
merged.gaugeRows = merged.gaugeRows || '';
|
||||||
|
merged.gaugeHook = merged.gaugeHook || '';
|
||||||
|
merged.size = merged.size || '';
|
||||||
|
merged.abbrev = merged.abbrev || '';
|
||||||
|
merged.stitches = merged.stitches || '';
|
||||||
|
merged.notes = merged.notes || '';
|
||||||
|
if (!merged.mode) merged.mode = 'crochet';
|
||||||
|
if (!merged.currentRow) merged.currentRow = 1;
|
||||||
|
if (merged.line === undefined) merged.line = '';
|
||||||
|
if (merged.output === undefined) merged.output = '';
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
|
|
||||||
|
let patternDraft = normalizePatternDraft(JSON.parse(localStorage.getItem('crochetPatternDraft')) || {});
|
||||||
// New Earthy/Woodland Palette extracted from image vibes
|
// New Earthy/Woodland Palette extracted from image vibes
|
||||||
const colors = [
|
const colors = [
|
||||||
'#a17d63', // Soft oak
|
'#a17d63', // Soft oak
|
||||||
@ -45,7 +321,570 @@ const saveOverlay = document.getElementById('saveOverlay');
|
|||||||
const saveList = document.getElementById('saveList');
|
const saveList = document.getElementById('saveList');
|
||||||
const patternPicker = document.getElementById('patternPicker');
|
const patternPicker = document.getElementById('patternPicker');
|
||||||
const patternSelect = document.getElementById('patternSelect');
|
const patternSelect = document.getElementById('patternSelect');
|
||||||
|
const patternOverlay = document.getElementById('patternOverlay');
|
||||||
|
const patternRowNumber = null;
|
||||||
|
const patternLine = null;
|
||||||
|
const patternOutput = document.getElementById('patternOutput');
|
||||||
|
const patternButtonsWrap = null;
|
||||||
if (patternPicker && patternSelect) populatePatternSelect();
|
if (patternPicker && patternSelect) populatePatternSelect();
|
||||||
|
if (patternOverlay) {
|
||||||
|
patternOverlay.addEventListener('click', (e) => {
|
||||||
|
if (e.target === patternOverlay) closePatternComposer();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Escape' && patternOverlay && patternOverlay.classList.contains('active')) {
|
||||||
|
closePatternComposer();
|
||||||
|
}
|
||||||
|
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter' && patternOverlay && patternOverlay.classList.contains('active')) {
|
||||||
|
e.preventDefault();
|
||||||
|
addStep();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function persistPatternDraft() {
|
||||||
|
localStorage.setItem('crochetPatternDraft', JSON.stringify(patternDraft));
|
||||||
|
flashSave();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPatternMode(mode) {
|
||||||
|
patternDraft.mode = mode;
|
||||||
|
persistPatternDraft();
|
||||||
|
if (!document.querySelector('.pattern-overlay')) return;
|
||||||
|
document.querySelectorAll('.pattern-mode').forEach(btn => {
|
||||||
|
btn.classList.toggle('is-active', btn.dataset.mode === mode);
|
||||||
|
});
|
||||||
|
renderAbbrevChecklist();
|
||||||
|
renderSteps();
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadDefaultAbbrev() {
|
||||||
|
patternDraft.abbrevSelection = getCoreCodes();
|
||||||
|
updateAbbrevFromSelection();
|
||||||
|
renderAbbrevChecklist();
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindPatternInputs() {
|
||||||
|
const titleEl = document.getElementById('patternTitle');
|
||||||
|
const designerEl = document.getElementById('patternDesigner');
|
||||||
|
const materialsEl = document.getElementById('patternMaterials');
|
||||||
|
const gaugeEl = document.getElementById('patternGauge');
|
||||||
|
const gaugeStsEl = document.getElementById('patternGaugeSts');
|
||||||
|
const gaugeRowsEl = document.getElementById('patternGaugeRows');
|
||||||
|
const gaugeHookEl = document.getElementById('patternGaugeHook');
|
||||||
|
const sizeEl = document.getElementById('patternSize');
|
||||||
|
const abbrevEl = document.getElementById('patternAbbrev');
|
||||||
|
const stitchesEl = document.getElementById('patternStitches');
|
||||||
|
const notesEl = document.getElementById('patternNotes');
|
||||||
|
const outputEl = document.getElementById('patternOutput');
|
||||||
|
if (titleEl) titleEl.addEventListener('input', e => { updateMetaField('title', e.target.value); });
|
||||||
|
if (designerEl) designerEl.addEventListener('input', e => { updateMetaField('designer', e.target.value); });
|
||||||
|
if (materialsEl) materialsEl.addEventListener('input', e => { updatePatternField('materials', e.target.value); });
|
||||||
|
if (gaugeEl) gaugeEl.addEventListener('input', e => { updatePatternField('gauge', e.target.value); });
|
||||||
|
if (gaugeStsEl) gaugeStsEl.addEventListener('input', e => { updatePatternField('gaugeSts', e.target.value); });
|
||||||
|
if (gaugeRowsEl) gaugeRowsEl.addEventListener('input', e => { updatePatternField('gaugeRows', e.target.value); });
|
||||||
|
if (gaugeHookEl) gaugeHookEl.addEventListener('input', e => { updatePatternField('gaugeHook', e.target.value); });
|
||||||
|
if (sizeEl) sizeEl.addEventListener('input', e => { updatePatternField('size', e.target.value); });
|
||||||
|
if (abbrevEl) abbrevEl.addEventListener('input', e => { updatePatternField('abbrev', e.target.value); });
|
||||||
|
if (stitchesEl) stitchesEl.addEventListener('input', e => { updatePatternField('stitches', e.target.value); });
|
||||||
|
if (notesEl) notesEl.addEventListener('input', e => { updatePatternField('notes', e.target.value); });
|
||||||
|
if (outputEl) outputEl.addEventListener('input', e => {
|
||||||
|
patternDraft.output = e.target.value;
|
||||||
|
const lines = patternDraft.output.split('\n').filter(l => l.trim() !== '');
|
||||||
|
patternDraft.currentRow = Math.max(1, lines.length + 1);
|
||||||
|
persistPatternDraft();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncPatternUI() {
|
||||||
|
setPatternMode(patternDraft.mode);
|
||||||
|
const titleEl = document.getElementById('patternTitle');
|
||||||
|
const designerEl = document.getElementById('patternDesigner');
|
||||||
|
const materialsEl = document.getElementById('patternMaterials');
|
||||||
|
const gaugeEl = document.getElementById('patternGauge');
|
||||||
|
const gaugeStsEl = document.getElementById('patternGaugeSts');
|
||||||
|
const gaugeRowsEl = document.getElementById('patternGaugeRows');
|
||||||
|
const gaugeHookEl = document.getElementById('patternGaugeHook');
|
||||||
|
const sizeEl = document.getElementById('patternSize');
|
||||||
|
const abbrevEl = document.getElementById('patternAbbrev');
|
||||||
|
const stitchesEl = document.getElementById('patternStitches');
|
||||||
|
const notesEl = document.getElementById('patternNotes');
|
||||||
|
if (titleEl) titleEl.value = patternDraft.meta.title;
|
||||||
|
if (designerEl) designerEl.value = patternDraft.meta.designer;
|
||||||
|
if (materialsEl) materialsEl.value = patternDraft.materials;
|
||||||
|
if (gaugeEl) gaugeEl.value = patternDraft.gauge;
|
||||||
|
if (gaugeStsEl) gaugeStsEl.value = patternDraft.gaugeSts;
|
||||||
|
if (gaugeRowsEl) gaugeRowsEl.value = patternDraft.gaugeRows;
|
||||||
|
if (gaugeHookEl) gaugeHookEl.value = patternDraft.gaugeHook;
|
||||||
|
if (sizeEl) sizeEl.value = patternDraft.size;
|
||||||
|
if (abbrevEl) abbrevEl.value = patternDraft.abbrev;
|
||||||
|
if (stitchesEl) stitchesEl.value = patternDraft.stitches;
|
||||||
|
if (notesEl) notesEl.value = patternDraft.notes;
|
||||||
|
renderSteps();
|
||||||
|
renderAbbrevChecklist();
|
||||||
|
}
|
||||||
|
|
||||||
|
function addPatternToken(tok) {
|
||||||
|
if (!patternLine) return;
|
||||||
|
patternDraft.line = patternDraft.line ? `${patternDraft.line} ${tok}` : tok;
|
||||||
|
patternLine.value = patternDraft.line;
|
||||||
|
persistPatternDraft();
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearPatternLine() {
|
||||||
|
if (!patternLine) return;
|
||||||
|
patternDraft.line = '';
|
||||||
|
patternLine.value = '';
|
||||||
|
persistPatternDraft();
|
||||||
|
}
|
||||||
|
|
||||||
|
function addPatternRow() {
|
||||||
|
if (!patternLine || !patternOutput) return;
|
||||||
|
const line = patternDraft.line.trim();
|
||||||
|
if (!line) return;
|
||||||
|
const rowLabel = patternDraft.mode === 'knit' ? 'Row' : 'Rnd';
|
||||||
|
const rowText = `${rowLabel} ${patternDraft.currentRow}: ${line}`;
|
||||||
|
patternDraft.output = patternDraft.output ? `${patternDraft.output}\n${rowText}` : rowText;
|
||||||
|
patternDraft.currentRow += 1;
|
||||||
|
patternDraft.line = '';
|
||||||
|
persistPatternDraft();
|
||||||
|
syncPatternUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
function removePatternRow() {
|
||||||
|
if (!patternOutput) return;
|
||||||
|
const lines = (patternDraft.output || '').split('\n').filter(l => l.trim() !== '');
|
||||||
|
if (lines.length === 0) return;
|
||||||
|
lines.pop();
|
||||||
|
patternDraft.output = lines.join('\n');
|
||||||
|
patternDraft.currentRow = Math.max(1, lines.length + 1);
|
||||||
|
persistPatternDraft();
|
||||||
|
syncPatternUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearPatternOutput() {
|
||||||
|
if (!patternOutput) return;
|
||||||
|
patternDraft.output = '';
|
||||||
|
patternDraft.currentRow = 1;
|
||||||
|
patternDraft.line = '';
|
||||||
|
patternDraft.meta = { title: '', designer: '' };
|
||||||
|
patternDraft.materials = '';
|
||||||
|
patternDraft.gauge = '';
|
||||||
|
patternDraft.gaugeSts = '';
|
||||||
|
patternDraft.gaugeRows = '';
|
||||||
|
patternDraft.gaugeHook = '';
|
||||||
|
patternDraft.size = '';
|
||||||
|
patternDraft.abbrev = '';
|
||||||
|
patternDraft.stitches = '';
|
||||||
|
patternDraft.notes = '';
|
||||||
|
patternDraft.steps = [];
|
||||||
|
if (patternOutput) patternOutput.value = '';
|
||||||
|
persistPatternDraft();
|
||||||
|
syncPatternUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportPatternJSON() {
|
||||||
|
const payload = { patternDraft };
|
||||||
|
const name = patternDraft.meta.title || 'pattern';
|
||||||
|
const filename = `${name.replace(/\s+/g,'_').toLowerCase()}_draft.json`;
|
||||||
|
const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = filename;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
function importPatternJSON() {
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'file';
|
||||||
|
input.accept = 'application/json';
|
||||||
|
input.onchange = async (e) => {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
try {
|
||||||
|
const text = await file.text();
|
||||||
|
const data = JSON.parse(text);
|
||||||
|
if (!data.patternDraft) throw new Error('Invalid file');
|
||||||
|
patternDraft = normalizePatternDraft(data.patternDraft);
|
||||||
|
persistPatternDraft();
|
||||||
|
syncPatternUI();
|
||||||
|
} catch (err) {
|
||||||
|
showAlert({ title: 'Import failed', text: err.message });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
input.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportPatternPDF() {
|
||||||
|
const w = window.open('', '_blank');
|
||||||
|
if (!w) return;
|
||||||
|
const styles = getComputedStyle(document.body);
|
||||||
|
const themeBg = styles.getPropertyValue('--bg') || '#f4f0e8';
|
||||||
|
const themeText = styles.getPropertyValue('--text') || '#2f2b28';
|
||||||
|
const accent = styles.getPropertyValue('--project-color') || '#7a8c6a';
|
||||||
|
const gaugeBlock = `${patternDraft.gaugeSts || ''}${patternDraft.gaugeRows ? ' • ' + patternDraft.gaugeRows : ''}${patternDraft.gaugeHook ? ' • ' + patternDraft.gaugeHook : ''}`;
|
||||||
|
const fontLink = '<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@600;700&family=Nunito:wght@400;600;700&display=swap" rel="stylesheet">';
|
||||||
|
const html = `
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>${patternDraft.meta.title || 'Pattern'} - PDF</title>
|
||||||
|
${fontLink}
|
||||||
|
<style>
|
||||||
|
@media print {
|
||||||
|
body { -webkit-print-color-adjust: exact; }
|
||||||
|
}
|
||||||
|
body { font-family: 'Nunito', 'Segoe UI', sans-serif; background: ${themeBg}; color: ${themeText}; padding: 20px; }
|
||||||
|
h1, h2, h3 { font-family: 'Playfair Display', Georgia, serif; margin-bottom: 6px; }
|
||||||
|
.section { margin-bottom: 16px; padding: 10px; border: 1px solid ${accent}30; border-radius: 10px; }
|
||||||
|
.section-title { font-size: 1.1rem; margin-bottom: 6px; border-bottom: 1px solid ${accent}50; padding-bottom: 4px; color: ${accent}; }
|
||||||
|
pre { white-space: pre-wrap; background: ${themeBg}; border: 1px solid ${themeText}20; padding: 10px; border-radius: 8px; }
|
||||||
|
hr { border: none; border-top: 1px solid ${themeText}20; margin: 10px 0; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>${patternDraft.meta.title || 'Pattern'}</h1>
|
||||||
|
<h3>${patternDraft.meta.designer || ''}</h3>
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title">Materials</div>
|
||||||
|
<pre>${patternDraft.materials || ''}</pre>
|
||||||
|
</div>
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title">Gauge / Size</div>
|
||||||
|
<pre>${gaugeBlock}\n${patternDraft.gauge || ''}\n${patternDraft.size || ''}</pre>
|
||||||
|
</div>
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title">Abbreviations</div>
|
||||||
|
<pre>${patternDraft.abbrev || ''}</pre>
|
||||||
|
</div>
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title">Stitch Guide</div>
|
||||||
|
<pre>${patternDraft.stitches || ''}</pre>
|
||||||
|
</div>
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title">Steps</div>
|
||||||
|
${patternDraft.steps.map((s, i) => {
|
||||||
|
const rows = (s.rows || []).map((r, idx) => `Row ${idx + 1}: ${r}`).join('<br>');
|
||||||
|
const note = s.note ? `<br>${s.note}` : '';
|
||||||
|
return `<div><strong>Step ${i + 1}${s.title ? ': ' + s.title : ''}</strong><br>${rows}${note}</div>`;
|
||||||
|
}).join('<hr>')}
|
||||||
|
</div>
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title">Rows</div>
|
||||||
|
<pre>${patternDraft.output || ''}</pre>
|
||||||
|
</div>
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title">Notes</div>
|
||||||
|
<pre>${patternDraft.notes || ''}</pre>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
w.document.write(html);
|
||||||
|
w.document.close();
|
||||||
|
w.focus();
|
||||||
|
w.print();
|
||||||
|
}
|
||||||
|
|
||||||
|
function showPatternTab(tab) {
|
||||||
|
document.querySelectorAll('.pattern-tab').forEach(btn => btn.classList.toggle('is-active', btn.dataset.tab === tab));
|
||||||
|
document.querySelectorAll('.pattern-section').forEach(sec => sec.classList.toggle('is-active', sec.dataset.section === tab));
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateMetaField(field, value) {
|
||||||
|
patternDraft.meta[field] = value;
|
||||||
|
persistPatternDraft();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePatternField(field, value) {
|
||||||
|
patternDraft[field] = value;
|
||||||
|
persistPatternDraft();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSteps() {
|
||||||
|
const container = document.getElementById('patternSteps');
|
||||||
|
if (!container) return;
|
||||||
|
container.innerHTML = '';
|
||||||
|
patternDraft.steps.forEach((step, idx) => {
|
||||||
|
const card = document.createElement('div');
|
||||||
|
card.className = 'pattern-step-card card-pop';
|
||||||
|
const rows = step.rows || [];
|
||||||
|
const rowList = rows.map((r, i) => `
|
||||||
|
<div class="row-item">
|
||||||
|
<label>Row ${i + 1}</label>
|
||||||
|
<input type="text" value="${r}" data-idx="${idx}" data-row="${i}">
|
||||||
|
<button type="button" onclick="removeStepRow(${idx}, ${i})">✕</button>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
card.innerHTML = `
|
||||||
|
<div class="step-number">Step ${idx + 1}</div>
|
||||||
|
<label class="field-label">Title (optional)</label>
|
||||||
|
<input type="text" value="${step.title || ''}" data-idx="${idx}" data-field="title">
|
||||||
|
<div class="pattern-buttons inline-buttons">
|
||||||
|
${getPatternButtonCodes().map(tok => `<button type="button" data-tok="${tok}" data-idx="${idx}">${tok}</button>`).join('')}
|
||||||
|
</div>
|
||||||
|
<div class="pattern-row-editor">
|
||||||
|
<textarea data-idx="${idx}" data-field="rowDraft" placeholder="Build a row with the buttons above or type manually...">${step.rowDraft || ''}</textarea>
|
||||||
|
<div class="pattern-row-actions">
|
||||||
|
<button class="secondary" onclick="clearStepRow(${idx})">Clear line</button>
|
||||||
|
<button class="primary" onclick="addStepRow(${idx})">Add row</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pattern-row-list">${rowList}</div>
|
||||||
|
<label class="field-label">Note</label>
|
||||||
|
<textarea data-idx="${idx}" data-field="note" placeholder="Notes or assembly tips">${step.note || ''}</textarea>
|
||||||
|
<label class="field-label">Image URL (optional)</label>
|
||||||
|
<input type="text" value="${step.image || ''}" data-idx="${idx}" data-field="image" placeholder="https://...">
|
||||||
|
<div class="pattern-step-actions">
|
||||||
|
<button onclick="moveStep(${idx}, -1)">↑</button>
|
||||||
|
<button onclick="moveStep(${idx}, 1)">↓</button>
|
||||||
|
<button onclick="deleteStep(${idx})">Delete</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
card.querySelectorAll('input, textarea').forEach(el => {
|
||||||
|
el.addEventListener('input', (e) => {
|
||||||
|
const i = Number(e.target.dataset.idx);
|
||||||
|
const field = e.target.dataset.field;
|
||||||
|
if (!patternDraft.steps[i]) return;
|
||||||
|
patternDraft.steps[i][field] = e.target.value;
|
||||||
|
persistPatternDraft();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
card.querySelectorAll('.pattern-buttons button').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
const tok = btn.dataset.tok;
|
||||||
|
addPatternTokenToStep(idx, tok);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
card.querySelectorAll('input[data-row]').forEach(inp => {
|
||||||
|
inp.addEventListener('input', (e) => {
|
||||||
|
const rowIdx = Number(e.target.dataset.row);
|
||||||
|
updateStepRow(idx, rowIdx, e.target.value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const rowTextarea = card.querySelector('textarea[data-field="rowDraft"]');
|
||||||
|
if (rowTextarea) {
|
||||||
|
rowTextarea.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey && !e.ctrlKey && !e.metaKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
addStepRow(idx);
|
||||||
|
} else if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
addStep();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
card.classList.remove('card-pop');
|
||||||
|
});
|
||||||
|
container.appendChild(card);
|
||||||
|
});
|
||||||
|
const addRow = document.createElement('div');
|
||||||
|
addRow.className = 'pattern-step-card add-step-row';
|
||||||
|
addRow.innerHTML = `
|
||||||
|
<button class="primary add-step-btn" onclick="addStep()">+ Add Step</button>
|
||||||
|
`;
|
||||||
|
container.appendChild(addRow);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addStep() {
|
||||||
|
patternDraft.steps.push({ title: '', rows: [], rowDraft: '', note: '', image: '' });
|
||||||
|
persistPatternDraft();
|
||||||
|
renderSteps();
|
||||||
|
}
|
||||||
|
|
||||||
|
function addStepRow(idx) {
|
||||||
|
const step = patternDraft.steps[idx];
|
||||||
|
if (!step) return;
|
||||||
|
const line = (step.rowDraft || '').trim();
|
||||||
|
if (!line) return;
|
||||||
|
step.rows.push(line);
|
||||||
|
step.rowDraft = '';
|
||||||
|
persistPatternDraft();
|
||||||
|
renderSteps();
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeStepLastRow(idx) {
|
||||||
|
// no-op: last-row removal handled per-row delete
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeStepRow(idx, rowIdx) {
|
||||||
|
const step = patternDraft.steps[idx];
|
||||||
|
if (!step || !step.rows || rowIdx < 0 || rowIdx >= step.rows.length) return;
|
||||||
|
step.rows.splice(rowIdx, 1);
|
||||||
|
persistPatternDraft();
|
||||||
|
renderSteps();
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearStepRow(idx) {
|
||||||
|
const step = patternDraft.steps[idx];
|
||||||
|
if (!step) return;
|
||||||
|
step.rowDraft = '';
|
||||||
|
persistPatternDraft();
|
||||||
|
renderSteps();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateStepRow(idx, rowIdx, value) {
|
||||||
|
const step = patternDraft.steps[idx];
|
||||||
|
if (!step || !step.rows || rowIdx < 0 || rowIdx >= step.rows.length) return;
|
||||||
|
step.rows[rowIdx] = value;
|
||||||
|
persistPatternDraft();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderAbbrevChecklist() {
|
||||||
|
const listEl = document.getElementById('patternAbbrevList');
|
||||||
|
if (!listEl) return;
|
||||||
|
listEl.innerHTML = '';
|
||||||
|
const selected = new Set(patternDraft.abbrevSelection);
|
||||||
|
getAbbrevGroups().forEach(bucket => {
|
||||||
|
const wrap = document.createElement('details');
|
||||||
|
wrap.className = 'abbrev-group';
|
||||||
|
wrap.open = true;
|
||||||
|
const summary = document.createElement('summary');
|
||||||
|
summary.textContent = `${bucket.label} (${bucket.items.length})`;
|
||||||
|
wrap.appendChild(summary);
|
||||||
|
const grid = document.createElement('div');
|
||||||
|
grid.className = 'abbrev-grid';
|
||||||
|
bucket.items.forEach(item => {
|
||||||
|
const pill = document.createElement('div');
|
||||||
|
pill.className = 'abbrev-pill';
|
||||||
|
if (selected.has(item.code)) pill.classList.add('is-selected');
|
||||||
|
pill.dataset.code = item.code;
|
||||||
|
pill.innerHTML = `
|
||||||
|
<span class="code">${item.code}</span>
|
||||||
|
<span class="desc">${item.desc}</span>
|
||||||
|
`;
|
||||||
|
pill.addEventListener('click', () => {
|
||||||
|
const code = pill.dataset.code;
|
||||||
|
if (pill.classList.contains('is-selected')) {
|
||||||
|
pill.classList.remove('is-selected');
|
||||||
|
patternDraft.abbrevSelection = patternDraft.abbrevSelection.filter(c => c !== code);
|
||||||
|
} else {
|
||||||
|
pill.classList.add('is-selected');
|
||||||
|
if (!patternDraft.abbrevSelection.includes(code)) patternDraft.abbrevSelection.push(code);
|
||||||
|
}
|
||||||
|
updateAbbrevFromSelection();
|
||||||
|
});
|
||||||
|
grid.appendChild(pill);
|
||||||
|
});
|
||||||
|
wrap.appendChild(grid);
|
||||||
|
listEl.appendChild(wrap);
|
||||||
|
});
|
||||||
|
renderSelectedAbbrev();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateAbbrevFromSelection() {
|
||||||
|
const lines = patternDraft.abbrevSelection.map(code => {
|
||||||
|
const found = getAbbrevByCode(code);
|
||||||
|
return found ? `${found.code} – ${found.desc}` : code;
|
||||||
|
});
|
||||||
|
patternDraft.abbrev = lines.join('\n');
|
||||||
|
persistPatternDraft();
|
||||||
|
const abbrevEl = document.getElementById('patternAbbrev');
|
||||||
|
if (abbrevEl) abbrevEl.value = patternDraft.abbrev;
|
||||||
|
renderSelectedAbbrev();
|
||||||
|
renderSteps();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSelectedAbbrev() {
|
||||||
|
renderPatternButtons();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPatternButtons() {
|
||||||
|
// Buttons are rendered per-step; no global buttons.
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteStep(idx) {
|
||||||
|
patternDraft.steps.splice(idx, 1);
|
||||||
|
persistPatternDraft();
|
||||||
|
renderSteps();
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveStep(idx, dir) {
|
||||||
|
const target = idx + dir;
|
||||||
|
if (target < 0 || target >= patternDraft.steps.length) return;
|
||||||
|
const tmp = patternDraft.steps[idx];
|
||||||
|
patternDraft.steps[idx] = patternDraft.steps[target];
|
||||||
|
patternDraft.steps[target] = tmp;
|
||||||
|
persistPatternDraft();
|
||||||
|
renderSteps();
|
||||||
|
}
|
||||||
|
|
||||||
|
function addPatternTokenToStep(idx, tok) {
|
||||||
|
const step = patternDraft.steps[idx];
|
||||||
|
if (!step) return;
|
||||||
|
// Append with comma/space
|
||||||
|
if (!step.rowDraft) {
|
||||||
|
step.rowDraft = tok;
|
||||||
|
} else {
|
||||||
|
// If last token matches, increment count
|
||||||
|
const parts = step.rowDraft.split(',').map(s => s.trim()).filter(Boolean);
|
||||||
|
if (parts.length && !nonMergeTokens.has(tok)) {
|
||||||
|
const last = parts[parts.length - 1];
|
||||||
|
const escapedTok = tok.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||||
|
const match = last.match(new RegExp(`^${escapedTok}(\\d+)$`, 'i'));
|
||||||
|
if (last.toLowerCase() === tok.toLowerCase()) {
|
||||||
|
parts[parts.length - 1] = `${tok}2`;
|
||||||
|
} else if (match) {
|
||||||
|
const n = parseInt(match[1], 10) + 1;
|
||||||
|
parts[parts.length - 1] = `${tok}${n}`;
|
||||||
|
} else {
|
||||||
|
parts.push(tok);
|
||||||
|
}
|
||||||
|
step.rowDraft = parts.join(', ');
|
||||||
|
} else {
|
||||||
|
parts.push(tok);
|
||||||
|
step.rowDraft = parts.join(', ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
persistPatternDraft();
|
||||||
|
renderSteps();
|
||||||
|
}
|
||||||
|
|
||||||
|
function addStepRow(idx) {
|
||||||
|
const step = patternDraft.steps[idx];
|
||||||
|
if (!step) return;
|
||||||
|
const line = (step.rowDraft || '').trim();
|
||||||
|
if (!line) return;
|
||||||
|
step.rows.push(line);
|
||||||
|
step.rowDraft = '';
|
||||||
|
persistPatternDraft();
|
||||||
|
renderSteps();
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeStepRow(idx) {
|
||||||
|
const step = patternDraft.steps[idx];
|
||||||
|
if (!step || !step.rows || step.rows.length === 0) return;
|
||||||
|
step.rows.pop();
|
||||||
|
persistPatternDraft();
|
||||||
|
renderSteps();
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearStepRow(idx) {
|
||||||
|
const step = patternDraft.steps[idx];
|
||||||
|
if (!step) return;
|
||||||
|
step.rowDraft = '';
|
||||||
|
persistPatternDraft();
|
||||||
|
renderSteps();
|
||||||
|
}
|
||||||
|
|
||||||
|
function openPatternComposer() {
|
||||||
|
if (!patternOverlay) return;
|
||||||
|
patternOverlay.classList.add('active');
|
||||||
|
syncPatternUI();
|
||||||
|
showPatternTab('steps');
|
||||||
|
}
|
||||||
|
|
||||||
|
function closePatternComposer() {
|
||||||
|
if (!patternOverlay) return;
|
||||||
|
patternOverlay.classList.remove('active');
|
||||||
|
}
|
||||||
|
syncPatternUI();
|
||||||
|
bindPatternInputs();
|
||||||
|
renderAbbrevChecklist();
|
||||||
let pendingSaveSelection = [];
|
let pendingSaveSelection = [];
|
||||||
let lastCountPulse = null;
|
let lastCountPulse = null;
|
||||||
let lastFinishedId = null;
|
let lastFinishedId = null;
|
||||||
@ -910,3 +1749,13 @@ function render() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render();
|
render();
|
||||||
|
function flashSave() {
|
||||||
|
const el = document.getElementById('patternSaveIndicator');
|
||||||
|
if (!el) return;
|
||||||
|
el.textContent = 'Saved';
|
||||||
|
el.style.opacity = '1';
|
||||||
|
if (saveFlashTimer) clearTimeout(saveFlashTimer);
|
||||||
|
saveFlashTimer = setTimeout(() => {
|
||||||
|
el.style.opacity = '0.6';
|
||||||
|
}, 1200);
|
||||||
|
}
|
||||||
|
|||||||
1762
assets/app.min.js
vendored
1762
assets/app.min.js
vendored
File diff suppressed because one or more lines are too long
193
assets/style.css
193
assets/style.css
@ -54,7 +54,7 @@ body.dark-mode {
|
|||||||
* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
|
* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: 'Quicksand', "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
font-family: 'Nunito', "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||||
background-color: var(--bg);
|
background-color: var(--bg);
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
@ -97,7 +97,7 @@ h1 {
|
|||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
color: var(--header-text);
|
color: var(--header-text);
|
||||||
text-shadow: 1px 1px 2px rgba(0,0,0,0.1);
|
text-shadow: 1px 1px 2px rgba(0,0,0,0.1);
|
||||||
font-family: 'Cormorant Garamond', Georgia, serif;
|
font-family: 'Playfair Display', Georgia, serif;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
@ -135,6 +135,189 @@ h1 {
|
|||||||
.header-btn.is-active { background: var(--header-text); color: var(--header-bg); }
|
.header-btn.is-active { background: var(--header-text); color: var(--header-bg); }
|
||||||
.hidden { display: none !important; }
|
.hidden { display: none !important; }
|
||||||
.hidden-input { display: none; }
|
.hidden-input { display: none; }
|
||||||
|
|
||||||
|
.fab-pattern { right: auto; left: 22px; background: var(--text); }
|
||||||
|
.fab-pattern:hover { transform: scale(1.07); }
|
||||||
|
|
||||||
|
/* Pattern Composer Overlay */
|
||||||
|
.pattern-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(44, 35, 25, 0.55);
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
display: none;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 215;
|
||||||
|
padding: 14px;
|
||||||
|
}
|
||||||
|
.pattern-overlay.active { display: flex; }
|
||||||
|
.pattern-sheet {
|
||||||
|
width: min(1280px, 96vw);
|
||||||
|
max-height: 92vh;
|
||||||
|
overflow: auto;
|
||||||
|
background: var(--card-bg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 16px;
|
||||||
|
box-shadow: 0 16px 40px rgba(0,0,0,0.25);
|
||||||
|
}
|
||||||
|
.pattern-sheet-header {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto auto auto;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.pattern-sheet-title h2 { margin: 0; font-size: 1.2rem; color: var(--text); }
|
||||||
|
.pattern-sheet-subtitle { margin: 2px 0 0; color: var(--text-muted); font-size: 0.9rem; }
|
||||||
|
.pattern-sheet-header h2 { margin: 0; font-size: 1.2rem; color: var(--text); }
|
||||||
|
.pattern-close {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.pattern-close:hover { color: var(--project-color); }
|
||||||
|
.pattern-modes { display: flex; gap: 8px; justify-content: flex-end; }
|
||||||
|
.pattern-mode { border: 1px solid var(--border); background: var(--input-bg); color: var(--text); padding: 6px 10px; border-radius: 10px; cursor: pointer; }
|
||||||
|
.pattern-mode.is-active { background: var(--project-color); color: var(--card-bg); border-color: var(--project-color); }
|
||||||
|
.pattern-save-indicator { color: var(--text-muted); font-size: 0.9rem; }
|
||||||
|
.pattern-body { display: grid; gap: 12px; }
|
||||||
|
.pattern-row-info { color: var(--text-muted); font-size: 0.95rem; }
|
||||||
|
.pattern-buttons { display: flex; flex-wrap: wrap; gap: 6px; }
|
||||||
|
.pattern-buttons button { padding: 6px 10px; border-radius: 10px; border: 1px solid var(--border); background: var(--input-bg); cursor: pointer; color: #fff; }
|
||||||
|
.pattern-buttons button:hover { border-color: var(--project-color); color: var(--project-color); }
|
||||||
|
.pattern-tabs { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 8px; }
|
||||||
|
.pattern-tab {
|
||||||
|
background: var(--input-bg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.pattern-tab.is-active { background: var(--project-color); color: var(--card-bg); border-color: var(--project-color); }
|
||||||
|
.pattern-section { display: none; border: 1px dashed var(--border); padding: 12px; border-radius: 12px; background: var(--card-bg); }
|
||||||
|
.pattern-section.is-active { display: block; }
|
||||||
|
.field-label { display: block; margin: 8px 0 4px; color: var(--text-muted); font-size: 0.9rem; }
|
||||||
|
.pattern-section input, .pattern-section textarea {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: var(--input-bg);
|
||||||
|
color: var(--text);
|
||||||
|
padding: 10px;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
.field-group-inline { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 8px; align-items: center; }
|
||||||
|
.pattern-row-editor textarea,
|
||||||
|
.pattern-output textarea {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 60px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: var(--input-bg);
|
||||||
|
color: #fff;
|
||||||
|
padding: 10px;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
.pattern-row-editor { display: grid; gap: 8px; }
|
||||||
|
.pattern-row-actions { display: flex; gap: 8px; justify-content: flex-end; flex-wrap: wrap; }
|
||||||
|
.pattern-row-actions .primary { background: var(--project-color); color: var(--card-bg); border: none; padding: 8px 12px; border-radius: 10px; cursor: pointer; }
|
||||||
|
.pattern-row-actions .secondary { background: var(--input-bg); color: var(--text); border: 1px solid var(--border); padding: 8px 12px; border-radius: 10px; cursor: pointer; }
|
||||||
|
.pattern-footer { justify-content: space-between; border-top: 1px solid var(--border); padding-top: 8px; }
|
||||||
|
.pattern-output { display: grid; gap: 6px; }
|
||||||
|
.pattern-steps-head { display: flex; align-items: center; justify-content: space-between; }
|
||||||
|
.pattern-step-card {
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
background: var(--card-bg);
|
||||||
|
transition: box-shadow 0.2s ease, transform 0.18s ease;
|
||||||
|
}
|
||||||
|
.pattern-step-card.card-pop { animation: cardPop 0.28s ease; }
|
||||||
|
.pattern-step-card:hover { box-shadow: 0 8px 18px rgba(0,0,0,0.08); transform: translateY(-1px); }
|
||||||
|
.pattern-row-list { display: grid; gap: 6px; }
|
||||||
|
.row-item { display: grid; grid-template-columns: auto 1fr auto; gap: 6px; align-items: center; }
|
||||||
|
.row-item label { color: var(--text-muted); font-size: 0.9rem; }
|
||||||
|
.row-item input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: var(--input-bg);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.row-item button { border: 1px solid var(--border); background: var(--btn-secondary-bg); color: var(--text); border-radius: 8px; padding: 4px 8px; cursor: pointer; }
|
||||||
|
.pattern-step-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 8px; flex-wrap: wrap; }
|
||||||
|
.add-step-row { display: flex; justify-content: center; padding: 10px; border: 1px dashed var(--border); background: var(--card-bg); }
|
||||||
|
.add-step-row .add-step-btn { padding: 10px 16px; border-radius: 12px; border: none; background: var(--project-color); color: var(--card-bg); cursor: pointer; font-weight: 700; }
|
||||||
|
.add-step-row .add-step-btn:hover { transform: translateY(-1px); box-shadow: 0 6px 14px rgba(0,0,0,0.12); }
|
||||||
|
.add-step-row .add-step-btn:active { transform: translateY(0); box-shadow: none; }
|
||||||
|
.step-number {
|
||||||
|
font-weight: 800;
|
||||||
|
color: var(--project-color);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
@keyframes cardPop {
|
||||||
|
0% { transform: translateY(6px) scale(0.97); opacity: 0; }
|
||||||
|
60% { transform: translateY(-2px) scale(1.01); opacity: 1; }
|
||||||
|
100% { transform: translateY(0) scale(1); opacity: 1; }
|
||||||
|
}
|
||||||
|
.abbrev-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
||||||
|
.abbrev-group { border: 1px solid var(--border); border-radius: 12px; margin: 6px 0; background: var(--card-bg); }
|
||||||
|
.abbrev-group[open] > summary { border-bottom: 1px solid var(--border); }
|
||||||
|
.abbrev-group summary {
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 8px 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text);
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
.abbrev-group summary::-webkit-details-marker { display: none; }
|
||||||
|
.abbrev-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px 10px 10px;
|
||||||
|
}
|
||||||
|
.abbrev-pill {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
background: var(--input-bg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.92rem;
|
||||||
|
transition: background 0.15s ease, border-color 0.15s ease, transform 0.1s ease;
|
||||||
|
}
|
||||||
|
.abbrev-pill:hover { transform: translateY(-1px); border-color: var(--project-color); }
|
||||||
|
.abbrev-pill.is-selected {
|
||||||
|
background: var(--project-color);
|
||||||
|
border-color: var(--project-color);
|
||||||
|
color: var(--card-bg);
|
||||||
|
}
|
||||||
|
.abbrev-pill .code { font-weight: 700; color: inherit; }
|
||||||
|
.abbrev-pill .desc { color: inherit; opacity: 0.85; font-size: 0.85rem; }
|
||||||
|
.selected-abbrev {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
margin: 6px 0 10px;
|
||||||
|
}
|
||||||
|
.selected-abbrev .pill {
|
||||||
|
background: var(--project-color);
|
||||||
|
color: var(--card-bg);
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
.color-overlay {
|
.color-overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
@ -156,7 +339,7 @@ h1 {
|
|||||||
box-shadow: 0 10px 30px rgba(0,0,0,0.25);
|
box-shadow: 0 10px 30px rgba(0,0,0,0.25);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
.color-title { margin: 0 0 12px; font-family: 'Cormorant Garamond', Georgia, serif; }
|
.color-title { margin: 0 0 12px; font-family: 'Playfair Display', Georgia, serif; }
|
||||||
.color-grid {
|
.color-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(60px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(60px, 1fr));
|
||||||
@ -277,7 +460,7 @@ h1 {
|
|||||||
.project-title {
|
.project-title {
|
||||||
font-size: 1.4rem; font-weight: 800; color: var(--project-color);
|
font-size: 1.4rem; font-weight: 800; color: var(--project-color);
|
||||||
text-transform: uppercase; letter-spacing: 1px;
|
text-transform: uppercase; letter-spacing: 1px;
|
||||||
font-family: 'Cormorant Garamond', Georgia, serif;
|
font-family: 'Playfair Display', Georgia, serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-toggle-project {
|
.btn-toggle-project {
|
||||||
@ -686,7 +869,7 @@ button:active { transform: scale(0.97); box-shadow: none; }
|
|||||||
}
|
}
|
||||||
|
|
||||||
.swal-title {
|
.swal-title {
|
||||||
font-family: 'Cormorant Garamond', Georgia, serif;
|
font-family: 'Playfair Display', Georgia, serif;
|
||||||
font-size: 1.4rem;
|
font-size: 1.4rem;
|
||||||
margin: 0 0 8px;
|
margin: 0 0 8px;
|
||||||
}
|
}
|
||||||
|
|||||||
900
assets/style.min.css
vendored
900
assets/style.min.css
vendored
File diff suppressed because one or more lines are too long
164
index.html
164
index.html
@ -6,7 +6,7 @@
|
|||||||
<title>Toadstool Cottage Counter</title>
|
<title>Toadstool Cottage Counter</title>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@500;700&family=Quicksand:wght@400;600;700&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@600;700&family=Nunito:wght@400;600;700&display=swap" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" crossorigin="anonymous" referrerpolicy="no-referrer">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" crossorigin="anonymous" referrerpolicy="no-referrer">
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="assets/icons/appicon-32x32.png">
|
<link rel="icon" type="image/png" sizes="32x32" href="assets/icons/appicon-32x32.png">
|
||||||
<link rel="icon" type="image/png" sizes="128x128" href="assets/icons/appicon-128x128.png">
|
<link rel="icon" type="image/png" sizes="128x128" href="assets/icons/appicon-128x128.png">
|
||||||
@ -35,6 +35,7 @@
|
|||||||
<div class="container" id="app"></div>
|
<div class="container" id="app"></div>
|
||||||
|
|
||||||
<button class="fab" onclick="openModal('addProject')">+</button>
|
<button class="fab" onclick="openModal('addProject')">+</button>
|
||||||
|
<button class="fab fab-pattern" onclick="openPatternComposer()" title="Open Pattern Composer"><i class="fa-solid fa-swatchbook"></i></button>
|
||||||
|
|
||||||
<div class="modal-overlay" id="modalOverlay">
|
<div class="modal-overlay" id="modalOverlay">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
@ -86,6 +87,167 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="pattern-overlay" id="patternOverlay">
|
||||||
|
<div class="pattern-sheet">
|
||||||
|
<div class="pattern-sheet-header">
|
||||||
|
<div class="pattern-sheet-title">
|
||||||
|
<h2>Pattern Composer</h2>
|
||||||
|
<p class="pattern-sheet-subtitle">Draft rows plus materials, gauge, and abbreviations.</p>
|
||||||
|
</div>
|
||||||
|
<div class="pattern-modes">
|
||||||
|
<button class="pattern-mode" data-mode="crochet" onclick="setPatternMode('crochet')">Crochet</button>
|
||||||
|
<button class="pattern-mode" data-mode="knit" onclick="setPatternMode('knit')">Knit</button>
|
||||||
|
</div>
|
||||||
|
<div class="pattern-save-indicator" id="patternSaveIndicator">Saved</div>
|
||||||
|
<button class="pattern-close" onclick="closePatternComposer()">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="pattern-body">
|
||||||
|
<div class="pattern-tabs">
|
||||||
|
<button class="pattern-tab" data-tab="info" onclick="showPatternTab('info')">Pattern Info</button>
|
||||||
|
<button class="pattern-tab" data-tab="steps" onclick="showPatternTab('steps')">Steps</button>
|
||||||
|
</div>
|
||||||
|
<div class="pattern-section" data-section="info">
|
||||||
|
<label class="field-label" for="patternTitle">Title</label>
|
||||||
|
<input id="patternTitle" type="text" placeholder="e.g., Baby Fox Plush">
|
||||||
|
<label class="field-label" for="patternDesigner">Designer / Credits</label>
|
||||||
|
<input id="patternDesigner" type="text" placeholder="Your name or shop">
|
||||||
|
<label class="field-label" for="patternMaterials">Materials (one per line)</label>
|
||||||
|
<textarea id="patternMaterials" placeholder="Yarn (Color A) – worsted Yarn (Color B) – accent Hook – 4.0 mm Safety eyes, stuffing, needle"></textarea>
|
||||||
|
<div class="field-group-inline">
|
||||||
|
<div>
|
||||||
|
<label class="field-label" for="patternGaugeSts">Stitches / 4in (10cm)</label>
|
||||||
|
<input id="patternGaugeSts" type="text" placeholder="e.g., 16 sc">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="field-label" for="patternGaugeRows">Rows / 4in (10cm)</label>
|
||||||
|
<input id="patternGaugeRows" type="text" placeholder="e.g., 18 rows">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<label class="field-label" for="patternGaugeHook">Hook / Needles</label>
|
||||||
|
<input id="patternGaugeHook" type="text" placeholder="e.g., 4.0 mm hook">
|
||||||
|
<label class="field-label" for="patternSize">Finished size</label>
|
||||||
|
<input id="patternSize" type="text" placeholder="Approx. 6 in / 15 cm tall">
|
||||||
|
<label class="field-label" for="patternGauge">Gauge (extra notes)</label>
|
||||||
|
<textarea id="patternGauge" placeholder="Magic ring start; or any extra gauge notes"></textarea>
|
||||||
|
<div class="abbrev-head">
|
||||||
|
<label class="field-label" for="patternAbbrev">Abbreviations</label>
|
||||||
|
<button class="secondary" onclick="loadDefaultAbbrev()">Load defaults</button>
|
||||||
|
</div>
|
||||||
|
<div id="patternAbbrevList" class="abbrev-grid"></div>
|
||||||
|
<textarea id="patternAbbrev" placeholder="sc – single crochet dc – double crochet inc – increase k – knit p – purl"></textarea>
|
||||||
|
<label class="field-label" for="patternStitches">Stitch guide / special stitches</label>
|
||||||
|
<textarea id="patternStitches" placeholder="Magic ring: ... Invisible decrease: ... Kfb: knit front and back ..."></textarea>
|
||||||
|
<label class="field-label" for="patternNotes">Notes / finishing</label>
|
||||||
|
<textarea id="patternNotes" placeholder="Assembly, finishing, safety warnings, credits..."></textarea>
|
||||||
|
<div class="pattern-row-actions pattern-footer">
|
||||||
|
<button onclick="clearPatternOutput()">Clear pattern</button>
|
||||||
|
<button class="secondary" onclick="exportPatternJSON()">Export JSON</button>
|
||||||
|
<button class="secondary" onclick="importPatternJSON()">Import JSON</button>
|
||||||
|
<button class="primary" onclick="exportPatternPDF()">Export PDF</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pattern-section" data-section="steps">
|
||||||
|
<div class="pattern-steps-head">
|
||||||
|
<h4>Steps</h4>
|
||||||
|
<button class="primary" onclick="addStep()">+ Step</button>
|
||||||
|
</div>
|
||||||
|
<div id="patternSteps"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pattern-overlay" id="patternOverlay">
|
||||||
|
<div class="pattern-sheet">
|
||||||
|
<div class="pattern-sheet-header">
|
||||||
|
<div class="pattern-sheet-title">
|
||||||
|
<h2>Pattern Composer</h2>
|
||||||
|
<p class="pattern-sheet-subtitle">Draft rows plus materials, gauge, and abbreviations.</p>
|
||||||
|
</div>
|
||||||
|
<div class="pattern-modes">
|
||||||
|
<button class="pattern-mode" data-mode="crochet" onclick="setPatternMode('crochet')">Crochet</button>
|
||||||
|
<button class="pattern-mode" data-mode="knit" onclick="setPatternMode('knit')">Knit</button>
|
||||||
|
</div>
|
||||||
|
<button class="pattern-close" onclick="closePatternComposer()">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="pattern-body">
|
||||||
|
<div class="pattern-tabs">
|
||||||
|
<button class="pattern-tab" data-tab="meta" onclick="showPatternTab('meta')">Cover</button>
|
||||||
|
<button class="pattern-tab" data-tab="materials" onclick="showPatternTab('materials')">Materials</button>
|
||||||
|
<button class="pattern-tab" data-tab="gauge" onclick="showPatternTab('gauge')">Gauge/Size</button>
|
||||||
|
<button class="pattern-tab" data-tab="abbrev" onclick="showPatternTab('abbrev')">Abbrev</button>
|
||||||
|
<button class="pattern-tab" data-tab="stitches" onclick="showPatternTab('stitches')">Stitches</button>
|
||||||
|
<button class="pattern-tab" data-tab="steps" onclick="showPatternTab('steps')">Steps</button>
|
||||||
|
<button class="pattern-tab" data-tab="notes" onclick="showPatternTab('notes')">Notes</button>
|
||||||
|
</div>
|
||||||
|
<div class="pattern-section" data-section="meta">
|
||||||
|
<label class="field-label" for="patternTitle">Title</label>
|
||||||
|
<input id="patternTitle" type="text" placeholder="e.g., Baby Fox Plush">
|
||||||
|
<label class="field-label" for="patternDesigner">Designer / Credits</label>
|
||||||
|
<input id="patternDesigner" type="text" placeholder="Your name or shop">
|
||||||
|
</div>
|
||||||
|
<div class="pattern-section" data-section="materials">
|
||||||
|
<label class="field-label" for="patternMaterials">Materials (one per line)</label>
|
||||||
|
<textarea id="patternMaterials" placeholder="Yarn (Color A) – worsted\nYarn (Color B) – accent\nHook – 4.0 mm\nSafety eyes, stuffing, needle"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="pattern-section" data-section="gauge">
|
||||||
|
<label class="field-label" for="patternGauge">Gauge</label>
|
||||||
|
<textarea id="patternGauge" placeholder="e.g., 16 sc x 18 rows = 4”/10 cm with 4.0 mm hook"></textarea>
|
||||||
|
<label class="field-label" for="patternSize">Finished size</label>
|
||||||
|
<input id="patternSize" type="text" placeholder="Approx. 6 in / 15 cm tall">
|
||||||
|
</div>
|
||||||
|
<div class="pattern-section" data-section="abbrev">
|
||||||
|
<label class="field-label" for="patternAbbrev">Abbreviations</label>
|
||||||
|
<textarea id="patternAbbrev" placeholder="sc – single crochet\ndc – double crochet\ninc – increase\nk – knit\np – purl"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="pattern-section" data-section="stitches">
|
||||||
|
<label class="field-label" for="patternStitches">Stitch guide / special stitches</label>
|
||||||
|
<textarea id="patternStitches" placeholder="Magic ring: ... Invisible decrease: ... Kfb: knit front and back ..."></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="pattern-section" data-section="steps">
|
||||||
|
<div class="pattern-row-info">Row <span id="patternRowNumber">1</span></div>
|
||||||
|
<div class="pattern-buttons">
|
||||||
|
<button onclick="addPatternToken('sc')">sc</button>
|
||||||
|
<button onclick="addPatternToken('hdc')">hdc</button>
|
||||||
|
<button onclick="addPatternToken('dc')">dc</button>
|
||||||
|
<button onclick="addPatternToken('ch')">ch</button>
|
||||||
|
<button onclick="addPatternToken('sl st')">sl st</button>
|
||||||
|
<button onclick="addPatternToken('inc')">inc</button>
|
||||||
|
<button onclick="addPatternToken('dec')">dec</button>
|
||||||
|
<button onclick="addPatternToken('k')">k</button>
|
||||||
|
<button onclick="addPatternToken('p')">p</button>
|
||||||
|
<button onclick="addPatternToken('yo')">yo</button>
|
||||||
|
<button onclick="addPatternToken('k2tog')">k2tog</button>
|
||||||
|
<button onclick="addPatternToken('[ ] x')">[ ] x</button>
|
||||||
|
</div>
|
||||||
|
<div class="pattern-row-editor">
|
||||||
|
<textarea id="patternLine" placeholder="Build a row with the buttons above or type manually..."></textarea>
|
||||||
|
<div class="pattern-row-actions">
|
||||||
|
<button onclick="clearPatternLine()">Clear line</button>
|
||||||
|
<button class="primary" onclick="addPatternRow()">Add row</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pattern-steps-head">
|
||||||
|
<h4>Steps</h4>
|
||||||
|
<button class="primary" onclick="addStep()">+ Step</button>
|
||||||
|
</div>
|
||||||
|
<div id="patternSteps"></div>
|
||||||
|
</div>
|
||||||
|
<div class="pattern-section" data-section="notes">
|
||||||
|
<label class="field-label" for="patternNotes">Notes / finishing</label>
|
||||||
|
<textarea id="patternNotes" placeholder="Assembly, finishing, safety warnings, credits..."></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="pattern-output">
|
||||||
|
<label for="patternOutput">Pattern draft (rows)</label>
|
||||||
|
<textarea id="patternOutput" placeholder="Rows will appear here as you add them..."></textarea>
|
||||||
|
<div class="pattern-row-actions">
|
||||||
|
<button onclick="clearPatternOutput()">Clear pattern</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script src="assets/app.js"></script>
|
<script src="assets/app.js"></script>
|
||||||
<footer class="footer-bg" aria-hidden="true"></footer>
|
<footer class="footer-bg" aria-hidden="true"></footer>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user