mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-08-03 02:11:07 +05:30
style: run prettier formatter over src and tests
This commit is contained in:
@@ -65,7 +65,7 @@ const electronFsAdapter = {
|
|||||||
isDir: entry.isDirectory,
|
isDir: entry.isDirectory,
|
||||||
size: entry.size ?? 0,
|
size: entry.size ?? 0,
|
||||||
modified: entry.modified ?? 0,
|
modified: entry.modified ?? 0,
|
||||||
path: entry.path
|
path: entry.path,
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -105,7 +105,7 @@ const electronFsAdapter = {
|
|||||||
*/
|
*/
|
||||||
async move(source, dest) {
|
async move(source, dest) {
|
||||||
return await window.electronAPI.file.move(source, dest);
|
return await window.electronAPI.file.move(source, dest);
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = { electronFsAdapter };
|
module.exports = { electronFsAdapter };
|
||||||
|
|||||||
@@ -61,11 +61,15 @@ function showAnalyticsModal(tabManager) {
|
|||||||
<span class="analytics-label">Avg Sentence</span>
|
<span class="analytics-label">Avg Sentence</span>
|
||||||
<span class="analytics-value">${metrics.avgSentenceLength} words</span>
|
<span class="analytics-value">${metrics.avgSentenceLength} words</span>
|
||||||
</div>
|
</div>
|
||||||
${metrics.longestSentenceLength > 0 ? `
|
${
|
||||||
|
metrics.longestSentenceLength > 0
|
||||||
|
? `
|
||||||
<div class="analytics-row analytics-longest">
|
<div class="analytics-row analytics-longest">
|
||||||
<span class="analytics-label">Longest (${metrics.longestSentenceLength} words)</span>
|
<span class="analytics-label">Longest (${metrics.longestSentenceLength} words)</span>
|
||||||
<span class="analytics-value analytics-sentence-preview">${escapeHtml(metrics.longestSentence)}</span>
|
<span class="analytics-value analytics-sentence-preview">${escapeHtml(metrics.longestSentence)}</span>
|
||||||
</div>` : ''}
|
</div>`
|
||||||
|
: ''
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="analytics-section">
|
<div class="analytics-section">
|
||||||
@@ -74,13 +78,19 @@ function showAnalyticsModal(tabManager) {
|
|||||||
<span class="analytics-label">Unique</span>
|
<span class="analytics-label">Unique</span>
|
||||||
<span class="analytics-value">${metrics.uniqueWordCount} / ${metrics.wordCount}<small>${metrics.lexicalDiversity}%</small></span>
|
<span class="analytics-value">${metrics.uniqueWordCount} / ${metrics.wordCount}<small>${metrics.lexicalDiversity}%</small></span>
|
||||||
</div>
|
</div>
|
||||||
${metrics.topWords.length > 0 ? `
|
${
|
||||||
|
metrics.topWords.length > 0
|
||||||
|
? `
|
||||||
<div class="word-cloud">
|
<div class="word-cloud">
|
||||||
${metrics.topWords.map(w => {
|
${metrics.topWords
|
||||||
|
.map((w) => {
|
||||||
const scale = 13 + Math.round((w.count / maxCount) * 3);
|
const scale = 13 + Math.round((w.count / maxCount) * 3);
|
||||||
return `<span class="word-tag" style="font-size:${scale}px">${escapeHtml(w.word)}<small>${w.count}</small></span>`;
|
return `<span class="word-tag" style="font-size:${scale}px">${escapeHtml(w.word)}<small>${w.count}</small></span>`;
|
||||||
}).join('')}
|
})
|
||||||
</div>` : ''}
|
.join('')}
|
||||||
|
</div>`
|
||||||
|
: ''
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,14 +4,74 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const STOP_WORDS = new Set([
|
const STOP_WORDS = new Set([
|
||||||
'the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been',
|
'the',
|
||||||
'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would',
|
'a',
|
||||||
'could', 'should', 'to', 'of', 'in', 'for', 'on', 'with',
|
'an',
|
||||||
'at', 'by', 'from', 'as', 'and', 'or', 'but', 'if', 'it',
|
'is',
|
||||||
'its', 'this', 'that', 'these', 'those', 'i', 'me', 'my',
|
'are',
|
||||||
'we', 'our', 'you', 'your', 'he', 'him', 'his', 'she', 'her',
|
'was',
|
||||||
'they', 'them', 'their', 'not', 'no', 'so', 'than', 'too',
|
'were',
|
||||||
'very', 'also', 'just', 'about', 'up', 'out', 'what', 'which', 'who'
|
'be',
|
||||||
|
'been',
|
||||||
|
'have',
|
||||||
|
'has',
|
||||||
|
'had',
|
||||||
|
'do',
|
||||||
|
'does',
|
||||||
|
'did',
|
||||||
|
'will',
|
||||||
|
'would',
|
||||||
|
'could',
|
||||||
|
'should',
|
||||||
|
'to',
|
||||||
|
'of',
|
||||||
|
'in',
|
||||||
|
'for',
|
||||||
|
'on',
|
||||||
|
'with',
|
||||||
|
'at',
|
||||||
|
'by',
|
||||||
|
'from',
|
||||||
|
'as',
|
||||||
|
'and',
|
||||||
|
'or',
|
||||||
|
'but',
|
||||||
|
'if',
|
||||||
|
'it',
|
||||||
|
'its',
|
||||||
|
'this',
|
||||||
|
'that',
|
||||||
|
'these',
|
||||||
|
'those',
|
||||||
|
'i',
|
||||||
|
'me',
|
||||||
|
'my',
|
||||||
|
'we',
|
||||||
|
'our',
|
||||||
|
'you',
|
||||||
|
'your',
|
||||||
|
'he',
|
||||||
|
'him',
|
||||||
|
'his',
|
||||||
|
'she',
|
||||||
|
'her',
|
||||||
|
'they',
|
||||||
|
'them',
|
||||||
|
'their',
|
||||||
|
'not',
|
||||||
|
'no',
|
||||||
|
'so',
|
||||||
|
'than',
|
||||||
|
'too',
|
||||||
|
'very',
|
||||||
|
'also',
|
||||||
|
'just',
|
||||||
|
'about',
|
||||||
|
'up',
|
||||||
|
'out',
|
||||||
|
'what',
|
||||||
|
'which',
|
||||||
|
'who',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
function countSyllables(word) {
|
function countSyllables(word) {
|
||||||
@@ -48,17 +108,23 @@ function analyze(text) {
|
|||||||
avgSentenceLength: 0,
|
avgSentenceLength: 0,
|
||||||
longestSentence: '',
|
longestSentence: '',
|
||||||
longestSentenceLength: 0,
|
longestSentenceLength: 0,
|
||||||
topWords: []
|
topWords: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const words = extractWords(text);
|
const words = extractWords(text);
|
||||||
const wordCount = words.length;
|
const wordCount = words.length;
|
||||||
|
|
||||||
const sentences = text.split(/[.!?]+/).map(s => s.trim()).filter(Boolean);
|
const sentences = text
|
||||||
|
.split(/[.!?]+/)
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter(Boolean);
|
||||||
const sentenceCount = Math.max(sentences.length, 1);
|
const sentenceCount = Math.max(sentences.length, 1);
|
||||||
|
|
||||||
const paragraphs = text.split(/\n\s*\n/).map(p => p.trim()).filter(Boolean);
|
const paragraphs = text
|
||||||
|
.split(/\n\s*\n/)
|
||||||
|
.map((p) => p.trim())
|
||||||
|
.filter(Boolean);
|
||||||
const paragraphCount = Math.max(paragraphs.length, 1);
|
const paragraphCount = Math.max(paragraphs.length, 1);
|
||||||
|
|
||||||
let totalSyllables = 0;
|
let totalSyllables = 0;
|
||||||
@@ -66,16 +132,23 @@ function analyze(text) {
|
|||||||
totalSyllables += countSyllables(w);
|
totalSyllables += countSyllables(w);
|
||||||
}
|
}
|
||||||
|
|
||||||
const fleschEase = Math.round((206.835 - 1.015 * (wordCount / sentenceCount) - 84.6 * (totalSyllables / wordCount)) * 10) / 10;
|
const fleschEase =
|
||||||
const fleschGrade = Math.round((0.39 * (wordCount / sentenceCount) + 11.8 * (totalSyllables / wordCount) - 15.59) * 10) / 10;
|
Math.round(
|
||||||
|
(206.835 - 1.015 * (wordCount / sentenceCount) - 84.6 * (totalSyllables / wordCount)) * 10
|
||||||
|
) / 10;
|
||||||
|
const fleschGrade =
|
||||||
|
Math.round(
|
||||||
|
(0.39 * (wordCount / sentenceCount) + 11.8 * (totalSyllables / wordCount) - 15.59) * 10
|
||||||
|
) / 10;
|
||||||
const readabilityLabel = getReadabilityLabel(fleschEase);
|
const readabilityLabel = getReadabilityLabel(fleschEase);
|
||||||
|
|
||||||
const readingTime = Math.ceil(wordCount / 200);
|
const readingTime = Math.ceil(wordCount / 200);
|
||||||
const speakingTime = Math.ceil(wordCount / 130);
|
const speakingTime = Math.ceil(wordCount / 130);
|
||||||
|
|
||||||
const uniqueWords = new Set(words.map(w => w.toLowerCase()));
|
const uniqueWords = new Set(words.map((w) => w.toLowerCase()));
|
||||||
const uniqueWordCount = uniqueWords.size;
|
const uniqueWordCount = uniqueWords.size;
|
||||||
const lexicalDiversity = wordCount > 0 ? Math.round((uniqueWordCount / wordCount) * 1000) / 10 : 0;
|
const lexicalDiversity =
|
||||||
|
wordCount > 0 ? Math.round((uniqueWordCount / wordCount) * 1000) / 10 : 0;
|
||||||
|
|
||||||
const avgSentenceLength = Math.round((wordCount / sentenceCount) * 10) / 10;
|
const avgSentenceLength = Math.round((wordCount / sentenceCount) * 10) / 10;
|
||||||
|
|
||||||
@@ -120,7 +193,7 @@ function analyze(text) {
|
|||||||
avgSentenceLength,
|
avgSentenceLength,
|
||||||
longestSentence,
|
longestSentence,
|
||||||
longestSentenceLength,
|
longestSentenceLength,
|
||||||
topWords
|
topWords,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,38 +12,23 @@ const { EditorState } = require('@codemirror/state');
|
|||||||
const { markdown, markdownLanguage } = require('@codemirror/lang-markdown');
|
const { markdown, markdownLanguage } = require('@codemirror/lang-markdown');
|
||||||
// Language extensions loaded lazily on first use
|
// Language extensions loaded lazily on first use
|
||||||
let _javascript, _html, _css, _json, _python;
|
let _javascript, _html, _css, _json, _python;
|
||||||
const {
|
const { defaultKeymap, history, historyKeymap, indentWithTab } = require('@codemirror/commands');
|
||||||
defaultKeymap,
|
const { searchKeymap, highlightSelectionMatches } = require('@codemirror/search');
|
||||||
history,
|
const { autocompletion, completionKeymap } = require('@codemirror/autocomplete');
|
||||||
historyKeymap,
|
const { bracketMatching, foldGutter, indentOnInput } = require('@codemirror/language');
|
||||||
indentWithTab,
|
|
||||||
} = require('@codemirror/commands');
|
|
||||||
const {
|
|
||||||
searchKeymap,
|
|
||||||
highlightSelectionMatches,
|
|
||||||
} = require('@codemirror/search');
|
|
||||||
const {
|
|
||||||
autocompletion,
|
|
||||||
completionKeymap,
|
|
||||||
} = require('@codemirror/autocomplete');
|
|
||||||
const {
|
|
||||||
bracketMatching,
|
|
||||||
foldGutter,
|
|
||||||
indentOnInput,
|
|
||||||
} = require('@codemirror/language');
|
|
||||||
const { oneDark } = require('@codemirror/theme-one-dark');
|
const { oneDark } = require('@codemirror/theme-one-dark');
|
||||||
|
|
||||||
// Custom theme for JetBrains Mono font
|
// Custom theme for JetBrains Mono font
|
||||||
const jetBrainsMonoTheme = EditorView.theme({
|
const jetBrainsMonoTheme = EditorView.theme({
|
||||||
'&': {
|
'&': {
|
||||||
fontFamily: "'JetBrains Mono', 'Fira Code', 'SF Mono', Monaco, 'Courier New', monospace"
|
fontFamily: "'JetBrains Mono', 'Fira Code', 'SF Mono', Monaco, 'Courier New', monospace",
|
||||||
},
|
},
|
||||||
'.cm-content': {
|
'.cm-content': {
|
||||||
fontFamily: 'inherit'
|
fontFamily: 'inherit',
|
||||||
},
|
},
|
||||||
'.cm-scroller': {
|
'.cm-scroller': {
|
||||||
fontFamily: 'inherit'
|
fontFamily: 'inherit',
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -59,7 +44,14 @@ const jetBrainsMonoTheme = EditorView.theme({
|
|||||||
* @returns {EditorView} the created editor view
|
* @returns {EditorView} the created editor view
|
||||||
*/
|
*/
|
||||||
function createEditor(parentElement, options = {}) {
|
function createEditor(parentElement, options = {}) {
|
||||||
console.log('[createEditor] Called with parentElement:', parentElement?.id, 'dimensions:', parentElement?.clientWidth, 'x', parentElement?.clientHeight);
|
console.log(
|
||||||
|
'[createEditor] Called with parentElement:',
|
||||||
|
parentElement?.id,
|
||||||
|
'dimensions:',
|
||||||
|
parentElement?.clientWidth,
|
||||||
|
'x',
|
||||||
|
parentElement?.clientHeight
|
||||||
|
);
|
||||||
if (!parentElement) {
|
if (!parentElement) {
|
||||||
console.error('[createEditor] ERROR: parentElement is null or undefined!');
|
console.error('[createEditor] ERROR: parentElement is null or undefined!');
|
||||||
return null;
|
return null;
|
||||||
@@ -125,11 +117,26 @@ function createEditor(parentElement, options = {}) {
|
|||||||
*/
|
*/
|
||||||
function getLanguageExtension(lang) {
|
function getLanguageExtension(lang) {
|
||||||
const loaders = {
|
const loaders = {
|
||||||
javascript: () => { if (!_javascript) _javascript = require('@codemirror/lang-javascript').javascript; return _javascript(); },
|
javascript: () => {
|
||||||
html: () => { if (!_html) _html = require('@codemirror/lang-html').html; return _html(); },
|
if (!_javascript) _javascript = require('@codemirror/lang-javascript').javascript;
|
||||||
css: () => { if (!_css) _css = require('@codemirror/lang-css').css; return _css(); },
|
return _javascript();
|
||||||
json: () => { if (!_json) _json = require('@codemirror/lang-json').json; return _json(); },
|
},
|
||||||
python: () => { if (!_python) _python = require('@codemirror/lang-python').python; return _python(); },
|
html: () => {
|
||||||
|
if (!_html) _html = require('@codemirror/lang-html').html;
|
||||||
|
return _html();
|
||||||
|
},
|
||||||
|
css: () => {
|
||||||
|
if (!_css) _css = require('@codemirror/lang-css').css;
|
||||||
|
return _css();
|
||||||
|
},
|
||||||
|
json: () => {
|
||||||
|
if (!_json) _json = require('@codemirror/lang-json').json;
|
||||||
|
return _json();
|
||||||
|
},
|
||||||
|
python: () => {
|
||||||
|
if (!_python) _python = require('@codemirror/lang-python').python;
|
||||||
|
return _python();
|
||||||
|
},
|
||||||
markdown: () => markdown({ base: markdownLanguage }),
|
markdown: () => markdown({ base: markdownLanguage }),
|
||||||
};
|
};
|
||||||
loaders.js = loaders.javascript;
|
loaders.js = loaders.javascript;
|
||||||
|
|||||||
+58
-39
@@ -4,11 +4,11 @@ const { PDFDocument, rgb, degrees, StandardFonts } = require('pdf-lib');
|
|||||||
|
|
||||||
function parsePageRanges(rangeString, totalPages) {
|
function parsePageRanges(rangeString, totalPages) {
|
||||||
const pages = [];
|
const pages = [];
|
||||||
const ranges = rangeString.split(',').map(r => r.trim());
|
const ranges = rangeString.split(',').map((r) => r.trim());
|
||||||
|
|
||||||
for (const range of ranges) {
|
for (const range of ranges) {
|
||||||
if (range.includes('-')) {
|
if (range.includes('-')) {
|
||||||
const [start, end] = range.split('-').map(n => parseInt(n.trim()));
|
const [start, end] = range.split('-').map((n) => parseInt(n.trim()));
|
||||||
for (let i = start; i <= end && i <= totalPages; i++) {
|
for (let i = start; i <= end && i <= totalPages; i++) {
|
||||||
if (i > 0 && !pages.includes(i - 1)) {
|
if (i > 0 && !pages.includes(i - 1)) {
|
||||||
pages.push(i - 1);
|
pages.push(i - 1);
|
||||||
@@ -27,11 +27,13 @@ function parsePageRanges(rangeString, totalPages) {
|
|||||||
|
|
||||||
function hexToRgb(hex) {
|
function hexToRgb(hex) {
|
||||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||||
return result ? {
|
return result
|
||||||
|
? {
|
||||||
r: parseInt(result[1], 16) / 255,
|
r: parseInt(result[1], 16) / 255,
|
||||||
g: parseInt(result[2], 16) / 255,
|
g: parseInt(result[2], 16) / 255,
|
||||||
b: parseInt(result[3], 16) / 255
|
b: parseInt(result[3], 16) / 255,
|
||||||
} : { r: 0, g: 0, b: 0 };
|
}
|
||||||
|
: { r: 0, g: 0, b: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function pdfMerge(data) {
|
async function pdfMerge(data) {
|
||||||
@@ -42,7 +44,7 @@ async function pdfMerge(data) {
|
|||||||
const pdfBytes = fs.readFileSync(filePath);
|
const pdfBytes = fs.readFileSync(filePath);
|
||||||
const pdf = await PDFDocument.load(pdfBytes);
|
const pdf = await PDFDocument.load(pdfBytes);
|
||||||
const copiedPages = await mergedPdf.copyPages(pdf, pdf.getPageIndices());
|
const copiedPages = await mergedPdf.copyPages(pdf, pdf.getPageIndices());
|
||||||
copiedPages.forEach(page => mergedPdf.addPage(page));
|
copiedPages.forEach((page) => mergedPdf.addPage(page));
|
||||||
}
|
}
|
||||||
|
|
||||||
const pdfBytes = await mergedPdf.save();
|
const pdfBytes = await mergedPdf.save();
|
||||||
@@ -63,13 +65,13 @@ async function pdfSplit(data) {
|
|||||||
const splits = [];
|
const splits = [];
|
||||||
|
|
||||||
if (data.splitMode === 'pages') {
|
if (data.splitMode === 'pages') {
|
||||||
const ranges = data.pageRanges.split(',').map(r => r.trim());
|
const ranges = data.pageRanges.split(',').map((r) => r.trim());
|
||||||
for (let i = 0; i < ranges.length; i++) {
|
for (let i = 0; i < ranges.length; i++) {
|
||||||
const range = ranges[i];
|
const range = ranges[i];
|
||||||
const pages = [];
|
const pages = [];
|
||||||
|
|
||||||
if (range.includes('-')) {
|
if (range.includes('-')) {
|
||||||
const [start, end] = range.split('-').map(n => parseInt(n.trim()));
|
const [start, end] = range.split('-').map((n) => parseInt(n.trim()));
|
||||||
for (let p = start; p <= end && p <= totalPages; p++) {
|
for (let p = start; p <= end && p <= totalPages; p++) {
|
||||||
pages.push(p - 1);
|
pages.push(p - 1);
|
||||||
}
|
}
|
||||||
@@ -108,7 +110,7 @@ async function pdfSplit(data) {
|
|||||||
for (const split of splits) {
|
for (const split of splits) {
|
||||||
const newPdf = await PDFDocument.create();
|
const newPdf = await PDFDocument.create();
|
||||||
const copiedPages = await newPdf.copyPages(pdf, split.pages);
|
const copiedPages = await newPdf.copyPages(pdf, split.pages);
|
||||||
copiedPages.forEach(page => newPdf.addPage(page));
|
copiedPages.forEach((page) => newPdf.addPage(page));
|
||||||
|
|
||||||
const outputPath = path.join(data.outputFolder, `${baseName}_${split.name}.pdf`);
|
const outputPath = path.join(data.outputFolder, `${baseName}_${split.name}.pdf`);
|
||||||
const newPdfBytes = await newPdf.save();
|
const newPdfBytes = await newPdf.save();
|
||||||
@@ -129,18 +131,18 @@ async function pdfCompress(data) {
|
|||||||
const compressedPdfBytes = await pdf.save({
|
const compressedPdfBytes = await pdf.save({
|
||||||
useObjectStreams: true,
|
useObjectStreams: true,
|
||||||
addDefaultPage: false,
|
addDefaultPage: false,
|
||||||
objectsPerTick: 50
|
objectsPerTick: 50,
|
||||||
});
|
});
|
||||||
|
|
||||||
fs.writeFileSync(data.outputPath, compressedPdfBytes);
|
fs.writeFileSync(data.outputPath, compressedPdfBytes);
|
||||||
|
|
||||||
const originalSize = fs.statSync(data.inputPath).size;
|
const originalSize = fs.statSync(data.inputPath).size;
|
||||||
const compressedSize = fs.statSync(data.outputPath).size;
|
const compressedSize = fs.statSync(data.outputPath).size;
|
||||||
const savings = ((originalSize - compressedSize) / originalSize * 100).toFixed(1);
|
const savings = (((originalSize - compressedSize) / originalSize) * 100).toFixed(1);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: `PDF compressed. Size reduced by ${savings}% (${(originalSize / 1024).toFixed(1)}KB → ${(compressedSize / 1024).toFixed(1)}KB)`
|
message: `PDF compressed. Size reduced by ${savings}% (${(originalSize / 1024).toFixed(1)}KB → ${(compressedSize / 1024).toFixed(1)}KB)`,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
@@ -160,7 +162,7 @@ async function pdfRotate(data) {
|
|||||||
pagesToRotate = Array.from({ length: totalPages }, (_, i) => i);
|
pagesToRotate = Array.from({ length: totalPages }, (_, i) => i);
|
||||||
}
|
}
|
||||||
|
|
||||||
pagesToRotate.forEach(pageIndex => {
|
pagesToRotate.forEach((pageIndex) => {
|
||||||
const page = pdf.getPage(pageIndex);
|
const page = pdf.getPage(pageIndex);
|
||||||
page.setRotation(degrees(data.angle));
|
page.setRotation(degrees(data.angle));
|
||||||
});
|
});
|
||||||
@@ -170,7 +172,7 @@ async function pdfRotate(data) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: `Successfully rotated ${pagesToRotate.length} page(s) by ${data.angle}\u00B0`
|
message: `Successfully rotated ${pagesToRotate.length} page(s) by ${data.angle}\u00B0`,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
@@ -185,7 +187,9 @@ async function pdfDeletePages(data) {
|
|||||||
|
|
||||||
const pagesToDelete = parsePageRanges(data.pages, totalPages);
|
const pagesToDelete = parsePageRanges(data.pages, totalPages);
|
||||||
|
|
||||||
pagesToDelete.sort((a, b) => b - a).forEach(pageIndex => {
|
pagesToDelete
|
||||||
|
.sort((a, b) => b - a)
|
||||||
|
.forEach((pageIndex) => {
|
||||||
pdf.removePage(pageIndex);
|
pdf.removePage(pageIndex);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -194,7 +198,7 @@ async function pdfDeletePages(data) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: `Successfully deleted ${pagesToDelete.length} page(s). New PDF has ${totalPages - pagesToDelete.length} pages`
|
message: `Successfully deleted ${pagesToDelete.length} page(s). New PDF has ${totalPages - pagesToDelete.length} pages`,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
@@ -207,7 +211,7 @@ async function pdfReorder(data) {
|
|||||||
const pdf = await PDFDocument.load(pdfBytes);
|
const pdf = await PDFDocument.load(pdfBytes);
|
||||||
const totalPages = pdf.getPageCount();
|
const totalPages = pdf.getPageCount();
|
||||||
|
|
||||||
const newOrder = data.newOrder.split(',').map(n => parseInt(n.trim()) - 1);
|
const newOrder = data.newOrder.split(',').map((n) => parseInt(n.trim()) - 1);
|
||||||
|
|
||||||
if (newOrder.length !== totalPages) {
|
if (newOrder.length !== totalPages) {
|
||||||
return { success: false, error: `New order must include all ${totalPages} pages` };
|
return { success: false, error: `New order must include all ${totalPages} pages` };
|
||||||
@@ -215,7 +219,7 @@ async function pdfReorder(data) {
|
|||||||
|
|
||||||
const newPdf = await PDFDocument.create();
|
const newPdf = await PDFDocument.create();
|
||||||
const copiedPages = await newPdf.copyPages(pdf, newOrder);
|
const copiedPages = await newPdf.copyPages(pdf, newOrder);
|
||||||
copiedPages.forEach(page => newPdf.addPage(page));
|
copiedPages.forEach((page) => newPdf.addPage(page));
|
||||||
|
|
||||||
const reorderedPdfBytes = await newPdf.save();
|
const reorderedPdfBytes = await newPdf.save();
|
||||||
fs.writeFileSync(data.outputPath, reorderedPdfBytes);
|
fs.writeFileSync(data.outputPath, reorderedPdfBytes);
|
||||||
@@ -246,7 +250,9 @@ async function pdfWatermark(data) {
|
|||||||
const page = pdf.getPage(pageIndex);
|
const page = pdf.getPage(pageIndex);
|
||||||
const { width, height } = page.getSize();
|
const { width, height } = page.getSize();
|
||||||
|
|
||||||
let x, y, rotation = 0;
|
let x,
|
||||||
|
y,
|
||||||
|
rotation = 0;
|
||||||
|
|
||||||
switch (data.position) {
|
switch (data.position) {
|
||||||
case 'center':
|
case 'center':
|
||||||
@@ -294,7 +300,7 @@ async function pdfWatermark(data) {
|
|||||||
font,
|
font,
|
||||||
color: rgb(color.r, color.g, color.b),
|
color: rgb(color.r, color.g, color.b),
|
||||||
opacity: data.opacity,
|
opacity: data.opacity,
|
||||||
rotate: degrees(rotation)
|
rotate: degrees(rotation),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -303,7 +309,7 @@ async function pdfWatermark(data) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: `Successfully added watermark to ${pagesToWatermark.length} page(s)`
|
message: `Successfully added watermark to ${pagesToWatermark.length} page(s)`,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
@@ -325,8 +331,8 @@ async function pdfEncrypt(data) {
|
|||||||
annotating: data.permissions.annotating,
|
annotating: data.permissions.annotating,
|
||||||
fillingForms: data.permissions.fillingForms,
|
fillingForms: data.permissions.fillingForms,
|
||||||
contentAccessibility: data.permissions.contentAccessibility,
|
contentAccessibility: data.permissions.contentAccessibility,
|
||||||
documentAssembly: data.permissions.documentAssembly
|
documentAssembly: data.permissions.documentAssembly,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
fs.writeFileSync(data.outputPath, encryptedPdfBytes);
|
fs.writeFileSync(data.outputPath, encryptedPdfBytes);
|
||||||
@@ -336,7 +342,8 @@ async function pdfEncrypt(data) {
|
|||||||
if (error.message.includes('encrypt') || error.message.includes('password')) {
|
if (error.message.includes('encrypt') || error.message.includes('password')) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: 'PDF encryption requires pdf-lib with encryption support. This feature may not be available in the current version.'
|
error:
|
||||||
|
'PDF encryption requires pdf-lib with encryption support. This feature may not be available in the current version.',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
@@ -375,8 +382,8 @@ async function pdfSetPermissions(data) {
|
|||||||
annotating: data.permissions.annotating,
|
annotating: data.permissions.annotating,
|
||||||
fillingForms: data.permissions.fillingForms,
|
fillingForms: data.permissions.fillingForms,
|
||||||
contentAccessibility: data.permissions.contentAccessibility,
|
contentAccessibility: data.permissions.contentAccessibility,
|
||||||
documentAssembly: data.permissions.documentAssembly
|
documentAssembly: data.permissions.documentAssembly,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
fs.writeFileSync(data.outputPath, newPdfBytes);
|
fs.writeFileSync(data.outputPath, newPdfBytes);
|
||||||
@@ -386,7 +393,8 @@ async function pdfSetPermissions(data) {
|
|||||||
if (error.message.includes('encrypt') || error.message.includes('permission')) {
|
if (error.message.includes('encrypt') || error.message.includes('permission')) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: 'PDF permissions require pdf-lib with encryption support. This feature may not be available in the current version.'
|
error:
|
||||||
|
'PDF permissions require pdf-lib with encryption support. This feature may not be available in the current version.',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
@@ -395,17 +403,28 @@ async function pdfSetPermissions(data) {
|
|||||||
|
|
||||||
function executeOperation(operation, data) {
|
function executeOperation(operation, data) {
|
||||||
switch (operation) {
|
switch (operation) {
|
||||||
case 'merge': return pdfMerge(data);
|
case 'merge':
|
||||||
case 'split': return pdfSplit(data);
|
return pdfMerge(data);
|
||||||
case 'compress': return pdfCompress(data);
|
case 'split':
|
||||||
case 'rotate': return pdfRotate(data);
|
return pdfSplit(data);
|
||||||
case 'delete': return pdfDeletePages(data);
|
case 'compress':
|
||||||
case 'reorder': return pdfReorder(data);
|
return pdfCompress(data);
|
||||||
case 'watermark': return pdfWatermark(data);
|
case 'rotate':
|
||||||
case 'encrypt': return pdfEncrypt(data);
|
return pdfRotate(data);
|
||||||
case 'decrypt': return pdfDecrypt(data);
|
case 'delete':
|
||||||
case 'permissions': return pdfSetPermissions(data);
|
return pdfDeletePages(data);
|
||||||
default: return Promise.resolve({ success: false, error: `Unknown operation: ${operation}` });
|
case 'reorder':
|
||||||
|
return pdfReorder(data);
|
||||||
|
case 'watermark':
|
||||||
|
return pdfWatermark(data);
|
||||||
|
case 'encrypt':
|
||||||
|
return pdfEncrypt(data);
|
||||||
|
case 'decrypt':
|
||||||
|
return pdfDecrypt(data);
|
||||||
|
case 'permissions':
|
||||||
|
return pdfSetPermissions(data);
|
||||||
|
default:
|
||||||
|
return Promise.resolve({ success: false, error: `Unknown operation: ${operation}` });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -429,5 +448,5 @@ module.exports = {
|
|||||||
pdfDecrypt,
|
pdfDecrypt,
|
||||||
pdfSetPermissions,
|
pdfSetPermissions,
|
||||||
executeOperation,
|
executeOperation,
|
||||||
getPageCount
|
getPageCount,
|
||||||
};
|
};
|
||||||
|
|||||||
+12
-4
@@ -5,22 +5,30 @@ const GitOperations = require('../GitOperations');
|
|||||||
|
|
||||||
function register(currentFileRef) {
|
function register(currentFileRef) {
|
||||||
ipcMain.handle('git-status', async () => {
|
ipcMain.handle('git-status', async () => {
|
||||||
const dir = currentFileRef.current ? require('path').dirname(currentFileRef.current) : process.cwd();
|
const dir = currentFileRef.current
|
||||||
|
? require('path').dirname(currentFileRef.current)
|
||||||
|
: process.cwd();
|
||||||
return GitOperations.getStatus(dir);
|
return GitOperations.getStatus(dir);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('git-stage', async (_event, { files }) => {
|
ipcMain.handle('git-stage', async (_event, { files }) => {
|
||||||
const dir = currentFileRef.current ? require('path').dirname(currentFileRef.current) : process.cwd();
|
const dir = currentFileRef.current
|
||||||
|
? require('path').dirname(currentFileRef.current)
|
||||||
|
: process.cwd();
|
||||||
return GitOperations.stage(dir, files);
|
return GitOperations.stage(dir, files);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('git-commit', async (_event, { message }) => {
|
ipcMain.handle('git-commit', async (_event, { message }) => {
|
||||||
const dir = currentFileRef.current ? require('path').dirname(currentFileRef.current) : process.cwd();
|
const dir = currentFileRef.current
|
||||||
|
? require('path').dirname(currentFileRef.current)
|
||||||
|
: process.cwd();
|
||||||
return GitOperations.commit(dir, message);
|
return GitOperations.commit(dir, message);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('git-log', async () => {
|
ipcMain.handle('git-log', async () => {
|
||||||
const dir = currentFileRef.current ? require('path').dirname(currentFileRef.current) : process.cwd();
|
const dir = currentFileRef.current
|
||||||
|
? require('path').dirname(currentFileRef.current)
|
||||||
|
: process.cwd();
|
||||||
return GitOperations.log(dir);
|
return GitOperations.log(dir);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,13 @@ const path = require('path');
|
|||||||
const { register: registerGit } = require('./git');
|
const { register: registerGit } = require('./git');
|
||||||
const { register: registerBinary } = require('./binary');
|
const { register: registerBinary } = require('./binary');
|
||||||
|
|
||||||
function register({ validatePath, resolveWritablePath, isPathAccessible, currentFileRef, mainWindow }) {
|
function register({
|
||||||
|
validatePath,
|
||||||
|
resolveWritablePath,
|
||||||
|
isPathAccessible,
|
||||||
|
currentFileRef,
|
||||||
|
mainWindow,
|
||||||
|
}) {
|
||||||
// pick-folder
|
// pick-folder
|
||||||
ipcMain.handle('pick-folder', async () => {
|
ipcMain.handle('pick-folder', async () => {
|
||||||
const result = await dialog.showOpenDialog(mainWindow, {
|
const result = await dialog.showOpenDialog(mainWindow, {
|
||||||
|
|||||||
+392
-215
File diff suppressed because it is too large
Load Diff
@@ -10,7 +10,10 @@ function register({ crash, getMainWindow }) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('crash:delete', (_event, filename) => {
|
ipcMain.handle('crash:delete', (_event, filename) => {
|
||||||
if (typeof filename === 'string' && /^\d+(-\d+)?-(uncaughtException|unhandledRejection)\.json$/.test(filename)) {
|
if (
|
||||||
|
typeof filename === 'string' &&
|
||||||
|
/^\d+(-\d+)?-(uncaughtException|unhandledRejection)\.json$/.test(filename)
|
||||||
|
) {
|
||||||
crash.delete(filename);
|
crash.delete(filename);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const {
|
|||||||
convertItems,
|
convertItems,
|
||||||
pdfEditorItems,
|
pdfEditorItems,
|
||||||
toolsItems,
|
toolsItems,
|
||||||
helpItems
|
helpItems,
|
||||||
} = require('./items');
|
} = require('./items');
|
||||||
|
|
||||||
function buildMenu(mainWindow) {
|
function buildMenu(mainWindow) {
|
||||||
@@ -22,7 +22,7 @@ function buildMenu(mainWindow) {
|
|||||||
{ label: '&Convert', submenu: convertItems(mainWindow) },
|
{ label: '&Convert', submenu: convertItems(mainWindow) },
|
||||||
{ label: 'PDF Editor', submenu: pdfEditorItems(mainWindow) },
|
{ label: 'PDF Editor', submenu: pdfEditorItems(mainWindow) },
|
||||||
{ label: '&Tools', submenu: toolsItems(mainWindow) },
|
{ label: '&Tools', submenu: toolsItems(mainWindow) },
|
||||||
{ label: '&Help', submenu: helpItems(mainWindow) }
|
{ label: '&Help', submenu: helpItems(mainWindow) },
|
||||||
];
|
];
|
||||||
return Menu.buildFromTemplate(template);
|
return Menu.buildFromTemplate(template);
|
||||||
}
|
}
|
||||||
|
|||||||
+403
-132
@@ -11,14 +11,14 @@ function buildRecentFilesMenu(mainWindow) {
|
|||||||
const recentFilesPath = path.join(app.getPath('userData'), 'recent-files.json');
|
const recentFilesPath = path.join(app.getPath('userData'), 'recent-files.json');
|
||||||
if (!fs.existsSync(recentFilesPath)) return [{ label: 'No Recent Files', enabled: false }];
|
if (!fs.existsSync(recentFilesPath)) return [{ label: 'No Recent Files', enabled: false }];
|
||||||
const recentFiles = JSON.parse(fs.readFileSync(recentFilesPath, 'utf-8'));
|
const recentFiles = JSON.parse(fs.readFileSync(recentFilesPath, 'utf-8'));
|
||||||
const existing = recentFiles.filter(file => fs.existsSync(file));
|
const existing = recentFiles.filter((file) => fs.existsSync(file));
|
||||||
if (existing.length === 0) return [{ label: 'No Recent Files', enabled: false }];
|
if (existing.length === 0) return [{ label: 'No Recent Files', enabled: false }];
|
||||||
const items = existing.map(file => ({
|
const items = existing.map((file) => ({
|
||||||
label: path.basename(file),
|
label: path.basename(file),
|
||||||
click: () => {
|
click: () => {
|
||||||
const { openFileFromPath } = require('../index');
|
const { openFileFromPath } = require('../index');
|
||||||
openFileFromPath(file);
|
openFileFromPath(file);
|
||||||
}
|
},
|
||||||
}));
|
}));
|
||||||
items.push(
|
items.push(
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
@@ -28,7 +28,7 @@ function buildRecentFilesMenu(mainWindow) {
|
|||||||
if (mainWindow) {
|
if (mainWindow) {
|
||||||
mainWindow.webContents.send('clear-recent-files');
|
mainWindow.webContents.send('clear-recent-files');
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
return items;
|
return items;
|
||||||
@@ -42,7 +42,7 @@ function fileItems(mainWindow) {
|
|||||||
{
|
{
|
||||||
label: 'New',
|
label: 'New',
|
||||||
accelerator: 'CmdOrCtrl+N',
|
accelerator: 'CmdOrCtrl+N',
|
||||||
click: () => mainWindow.webContents.send('file-new')
|
click: () => mainWindow.webContents.send('file-new'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Open',
|
label: 'Open',
|
||||||
@@ -50,7 +50,7 @@ function fileItems(mainWindow) {
|
|||||||
click: () => {
|
click: () => {
|
||||||
const { openFile } = require('../index');
|
const { openFile } = require('../index');
|
||||||
openFile();
|
openFile();
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Open PDF',
|
label: 'Open PDF',
|
||||||
@@ -58,12 +58,12 @@ function fileItems(mainWindow) {
|
|||||||
click: () => {
|
click: () => {
|
||||||
const { openPdfFile } = require('../index');
|
const { openPdfFile } = require('../index');
|
||||||
openPdfFile();
|
openPdfFile();
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Save',
|
label: 'Save',
|
||||||
accelerator: 'CmdOrCtrl+S',
|
accelerator: 'CmdOrCtrl+S',
|
||||||
click: () => mainWindow.webContents.send('file-save')
|
click: () => mainWindow.webContents.send('file-save'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Save As',
|
label: 'Save As',
|
||||||
@@ -71,30 +71,60 @@ function fileItems(mainWindow) {
|
|||||||
click: () => {
|
click: () => {
|
||||||
const { saveAsFile } = require('../index');
|
const { saveAsFile } = require('../index');
|
||||||
saveAsFile();
|
saveAsFile();
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
// NOTE: Print Preview submenu removed — handled by React <PrintPreview> overlay
|
// NOTE: Print Preview submenu removed — handled by React <PrintPreview> overlay
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
{
|
{
|
||||||
label: 'Recent Files',
|
label: 'Recent Files',
|
||||||
submenu: buildRecentFilesMenu(mainWindow)
|
submenu: buildRecentFilesMenu(mainWindow),
|
||||||
},
|
},
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
{
|
{
|
||||||
label: 'New from Template',
|
label: 'New from Template',
|
||||||
submenu: [
|
submenu: [
|
||||||
{ label: 'Blog Post', click: () => mainWindow.webContents.send('load-template-menu', 'blog-post.md') },
|
{
|
||||||
{ label: 'Meeting Notes', click: () => mainWindow.webContents.send('load-template-menu', 'meeting-notes.md') },
|
label: 'Blog Post',
|
||||||
{ label: 'Technical Spec', click: () => mainWindow.webContents.send('load-template-menu', 'technical-spec.md') },
|
click: () => mainWindow.webContents.send('load-template-menu', 'blog-post.md'),
|
||||||
{ label: 'Changelog', click: () => mainWindow.webContents.send('load-template-menu', 'changelog.md') },
|
},
|
||||||
{ label: 'README', click: () => mainWindow.webContents.send('load-template-menu', 'readme.md') },
|
{
|
||||||
{ label: 'Project Plan', click: () => mainWindow.webContents.send('load-template-menu', 'project-plan.md') },
|
label: 'Meeting Notes',
|
||||||
{ label: 'API Documentation', click: () => mainWindow.webContents.send('load-template-menu', 'api-docs.md') },
|
click: () => mainWindow.webContents.send('load-template-menu', 'meeting-notes.md'),
|
||||||
{ label: 'Tutorial', click: () => mainWindow.webContents.send('load-template-menu', 'tutorial.md') },
|
},
|
||||||
{ label: 'Release Notes', click: () => mainWindow.webContents.send('load-template-menu', 'release-notes.md') },
|
{
|
||||||
{ label: 'Comparison', click: () => mainWindow.webContents.send('load-template-menu', 'comparison.md') }
|
label: 'Technical Spec',
|
||||||
]
|
click: () => mainWindow.webContents.send('load-template-menu', 'technical-spec.md'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Changelog',
|
||||||
|
click: () => mainWindow.webContents.send('load-template-menu', 'changelog.md'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'README',
|
||||||
|
click: () => mainWindow.webContents.send('load-template-menu', 'readme.md'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Project Plan',
|
||||||
|
click: () => mainWindow.webContents.send('load-template-menu', 'project-plan.md'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'API Documentation',
|
||||||
|
click: () => mainWindow.webContents.send('load-template-menu', 'api-docs.md'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Tutorial',
|
||||||
|
click: () => mainWindow.webContents.send('load-template-menu', 'tutorial.md'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Release Notes',
|
||||||
|
click: () => mainWindow.webContents.send('load-template-menu', 'release-notes.md'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Comparison',
|
||||||
|
click: () => mainWindow.webContents.send('load-template-menu', 'comparison.md'),
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
{
|
{
|
||||||
@@ -103,58 +133,137 @@ function fileItems(mainWindow) {
|
|||||||
click: () => {
|
click: () => {
|
||||||
const { importDocument } = require('../index');
|
const { importDocument } = require('../index');
|
||||||
importDocument();
|
importDocument();
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Export',
|
label: 'Export',
|
||||||
submenu: [
|
submenu: [
|
||||||
{
|
{
|
||||||
label: 'HTML', click: () => {
|
label: 'HTML',
|
||||||
|
click: () => {
|
||||||
const { exportFile } = require('../index');
|
const { exportFile } = require('../index');
|
||||||
exportFile('html');
|
exportFile('html');
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'PDF', click: () => {
|
label: 'PDF',
|
||||||
|
click: () => {
|
||||||
const { exportFile } = require('../index');
|
const { exportFile } = require('../index');
|
||||||
exportFile('pdf');
|
exportFile('pdf');
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'PDF (Enhanced)', click: () => {
|
label: 'PDF (Enhanced)',
|
||||||
|
click: () => {
|
||||||
const { exportPDFViaWordTemplate } = require('../index');
|
const { exportPDFViaWordTemplate } = require('../index');
|
||||||
exportPDFViaWordTemplate();
|
exportPDFViaWordTemplate();
|
||||||
}, accelerator: 'Ctrl+Shift+P'
|
},
|
||||||
|
accelerator: 'Ctrl+Shift+P',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'DOCX', click: () => {
|
label: 'DOCX',
|
||||||
|
click: () => {
|
||||||
const { exportFile } = require('../index');
|
const { exportFile } = require('../index');
|
||||||
exportFile('docx');
|
exportFile('docx');
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'DOCX (Enhanced)', click: () => {
|
label: 'DOCX (Enhanced)',
|
||||||
|
click: () => {
|
||||||
const { exportWordWithTemplate } = require('../index');
|
const { exportWordWithTemplate } = require('../index');
|
||||||
exportWordWithTemplate();
|
exportWordWithTemplate();
|
||||||
}, accelerator: 'Ctrl+Shift+W'
|
|
||||||
},
|
},
|
||||||
{ label: 'LaTeX', click: () => { const { exportFile } = require('../index'); exportFile('latex'); } },
|
accelerator: 'Ctrl+Shift+W',
|
||||||
{ label: 'RTF', click: () => { const { exportFile } = require('../index'); exportFile('rtf'); } },
|
},
|
||||||
{ label: 'ODT', click: () => { const { exportFile } = require('../index'); exportFile('odt'); } },
|
{
|
||||||
{ label: 'EPUB', click: () => { const { exportFile } = require('../index'); exportFile('epub'); } },
|
label: 'LaTeX',
|
||||||
|
click: () => {
|
||||||
|
const { exportFile } = require('../index');
|
||||||
|
exportFile('latex');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'RTF',
|
||||||
|
click: () => {
|
||||||
|
const { exportFile } = require('../index');
|
||||||
|
exportFile('rtf');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'ODT',
|
||||||
|
click: () => {
|
||||||
|
const { exportFile } = require('../index');
|
||||||
|
exportFile('odt');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'EPUB',
|
||||||
|
click: () => {
|
||||||
|
const { exportFile } = require('../index');
|
||||||
|
exportFile('epub');
|
||||||
|
},
|
||||||
|
},
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
{ label: 'PowerPoint (PPTX)', click: () => { const { exportFile } = require('../index'); exportFile('pptx'); } },
|
{
|
||||||
{ label: 'OpenDocument Presentation (ODP)', click: () => { const { exportFile } = require('../index'); exportFile('odp'); } },
|
label: 'PowerPoint (PPTX)',
|
||||||
|
click: () => {
|
||||||
|
const { exportFile } = require('../index');
|
||||||
|
exportFile('pptx');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'OpenDocument Presentation (ODP)',
|
||||||
|
click: () => {
|
||||||
|
const { exportFile } = require('../index');
|
||||||
|
exportFile('odp');
|
||||||
|
},
|
||||||
|
},
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
{ label: 'CSV (Tables)', click: () => { const { exportSpreadsheet } = require('../index'); exportSpreadsheet('csv'); } },
|
{
|
||||||
|
label: 'CSV (Tables)',
|
||||||
|
click: () => {
|
||||||
|
const { exportSpreadsheet } = require('../index');
|
||||||
|
exportSpreadsheet('csv');
|
||||||
|
},
|
||||||
|
},
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
{ label: 'JSON (.json)', click: () => { const { exportFile } = require('../index'); exportFile('json'); } },
|
{
|
||||||
{ label: 'YAML (.yaml)', click: () => { const { exportFile } = require('../index'); exportFile('yaml'); } },
|
label: 'JSON (.json)',
|
||||||
{ label: 'XML (.xml)', click: () => { const { exportFile } = require('../index'); exportFile('xml'); } },
|
click: () => {
|
||||||
|
const { exportFile } = require('../index');
|
||||||
|
exportFile('json');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'YAML (.yaml)',
|
||||||
|
click: () => {
|
||||||
|
const { exportFile } = require('../index');
|
||||||
|
exportFile('yaml');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'XML (.xml)',
|
||||||
|
click: () => {
|
||||||
|
const { exportFile } = require('../index');
|
||||||
|
exportFile('xml');
|
||||||
|
},
|
||||||
|
},
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
{ label: 'Confluence Wiki (.txt)', click: () => { const { exportFile } = require('../index'); exportFile('confluence'); } },
|
{
|
||||||
{ label: 'MOBI E-book (.mobi)', click: () => { const { exportFile } = require('../index'); exportFile('mobi'); } }
|
label: 'Confluence Wiki (.txt)',
|
||||||
]
|
click: () => {
|
||||||
|
const { exportFile } = require('../index');
|
||||||
|
exportFile('confluence');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'MOBI E-book (.mobi)',
|
||||||
|
click: () => {
|
||||||
|
const { exportFile } = require('../index');
|
||||||
|
exportFile('mobi');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
{
|
{
|
||||||
@@ -162,14 +271,14 @@ function fileItems(mainWindow) {
|
|||||||
click: () => {
|
click: () => {
|
||||||
const { selectWordTemplate } = require('../index');
|
const { selectWordTemplate } = require('../index');
|
||||||
selectWordTemplate();
|
selectWordTemplate();
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Template Settings...',
|
label: 'Template Settings...',
|
||||||
click: () => {
|
click: () => {
|
||||||
const { showTemplateSettings } = require('../index');
|
const { showTemplateSettings } = require('../index');
|
||||||
showTemplateSettings();
|
showTemplateSettings();
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Header & Footer Settings...',
|
label: 'Header & Footer Settings...',
|
||||||
@@ -177,14 +286,14 @@ function fileItems(mainWindow) {
|
|||||||
if (mainWindow) {
|
if (mainWindow) {
|
||||||
mainWindow.webContents.send('open-header-footer-dialog');
|
mainWindow.webContents.send('open-header-footer-dialog');
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
{
|
{
|
||||||
label: 'Quit',
|
label: 'Quit',
|
||||||
accelerator: process.platform === 'darwin' ? 'Cmd+Q' : 'Ctrl+Q',
|
accelerator: process.platform === 'darwin' ? 'Cmd+Q' : 'Ctrl+Q',
|
||||||
click: () => app.quit()
|
click: () => app.quit(),
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,12 +302,12 @@ function editItems(mainWindow) {
|
|||||||
{
|
{
|
||||||
label: 'Undo',
|
label: 'Undo',
|
||||||
accelerator: 'CmdOrCtrl+Z',
|
accelerator: 'CmdOrCtrl+Z',
|
||||||
click: () => mainWindow.webContents.send('undo')
|
click: () => mainWindow.webContents.send('undo'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Redo',
|
label: 'Redo',
|
||||||
accelerator: 'CmdOrCtrl+Shift+Z',
|
accelerator: 'CmdOrCtrl+Shift+Z',
|
||||||
click: () => mainWindow.webContents.send('redo')
|
click: () => mainWindow.webContents.send('redo'),
|
||||||
},
|
},
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
{ label: 'Cut', accelerator: 'CmdOrCtrl+X', role: 'cut' },
|
{ label: 'Cut', accelerator: 'CmdOrCtrl+X', role: 'cut' },
|
||||||
@@ -209,8 +318,8 @@ function editItems(mainWindow) {
|
|||||||
{
|
{
|
||||||
label: 'Find & Replace',
|
label: 'Find & Replace',
|
||||||
accelerator: 'CmdOrCtrl+F',
|
accelerator: 'CmdOrCtrl+F',
|
||||||
click: () => mainWindow.webContents.send('toggle-find')
|
click: () => mainWindow.webContents.send('toggle-find'),
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,60 +328,222 @@ function viewItems(mainWindow) {
|
|||||||
{
|
{
|
||||||
label: 'Toggle Preview',
|
label: 'Toggle Preview',
|
||||||
accelerator: 'CmdOrCtrl+Shift+V',
|
accelerator: 'CmdOrCtrl+Shift+V',
|
||||||
click: () => mainWindow.webContents.send('toggle-preview')
|
click: () => mainWindow.webContents.send('toggle-preview'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Writing Analytics',
|
label: 'Writing Analytics',
|
||||||
accelerator: 'CmdOrCtrl+Shift+A',
|
accelerator: 'CmdOrCtrl+Shift+A',
|
||||||
click: () => mainWindow.webContents.send('show-analytics-dialog')
|
click: () => mainWindow.webContents.send('show-analytics-dialog'),
|
||||||
},
|
},
|
||||||
// NOTE: Command Palette removed — handled by useCommandStore
|
// NOTE: Command Palette removed — handled by useCommandStore
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
{
|
{
|
||||||
label: 'Sidebar',
|
label: 'Sidebar',
|
||||||
submenu: [
|
submenu: [
|
||||||
{ label: 'Files', click: () => mainWindow.webContents.send('toggle-sidebar-panel', 'explorer') },
|
{
|
||||||
{ label: 'Outline', click: () => mainWindow.webContents.send('toggle-sidebar-panel', 'outline') },
|
label: 'Files',
|
||||||
{ label: 'Snippets', click: () => mainWindow.webContents.send('toggle-sidebar-panel', 'snippets') },
|
click: () => mainWindow.webContents.send('toggle-sidebar-panel', 'explorer'),
|
||||||
{ label: 'Templates', click: () => mainWindow.webContents.send('toggle-sidebar-panel', 'templates') },
|
},
|
||||||
{ label: 'Git', click: () => mainWindow.webContents.send('toggle-sidebar-panel', 'git') }
|
{
|
||||||
]
|
label: 'Outline',
|
||||||
|
click: () => mainWindow.webContents.send('toggle-sidebar-panel', 'outline'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Snippets',
|
||||||
|
click: () => mainWindow.webContents.send('toggle-sidebar-panel', 'snippets'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Templates',
|
||||||
|
click: () => mainWindow.webContents.send('toggle-sidebar-panel', 'templates'),
|
||||||
|
},
|
||||||
|
{ label: 'Git', click: () => mainWindow.webContents.send('toggle-sidebar-panel', 'git') },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Bottom Panel (REPL)',
|
label: 'Bottom Panel (REPL)',
|
||||||
click: () => mainWindow.webContents.send('toggle-bottom-panel')
|
click: () => mainWindow.webContents.send('toggle-bottom-panel'),
|
||||||
},
|
},
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
{
|
{
|
||||||
label: 'Theme',
|
label: 'Theme',
|
||||||
submenu: [
|
submenu: [
|
||||||
{ label: 'Atom One Light (Default)', click: () => { const { setTheme } = require('../index'); setTheme('atomonelight'); } },
|
{
|
||||||
{ label: 'GitHub Light', click: () => { const { setTheme } = require('../index'); setTheme('github'); } },
|
label: 'Atom One Light (Default)',
|
||||||
{ label: 'Light', click: () => { const { setTheme } = require('../index'); setTheme('light'); } },
|
click: () => {
|
||||||
{ label: 'Solarized Light', click: () => { const { setTheme } = require('../index'); setTheme('solarized'); } },
|
const { setTheme } = require('../index');
|
||||||
{ label: 'Gruvbox Light', click: () => { const { setTheme } = require('../index'); setTheme('gruvbox-light'); } },
|
setTheme('atomonelight');
|
||||||
{ label: 'Ayu Light', click: () => { const { setTheme } = require('../index'); setTheme('ayu-light'); } },
|
},
|
||||||
{ label: 'Sepia', click: () => { const { setTheme } = require('../index'); setTheme('sepia'); } },
|
},
|
||||||
{ label: 'Paper', click: () => { const { setTheme } = require('../index'); setTheme('paper'); } },
|
{
|
||||||
{ label: 'Rose Pine Dawn', click: () => { const { setTheme } = require('../index'); setTheme('rosepine-dawn'); } },
|
label: 'GitHub Light',
|
||||||
{ label: 'Concrete Light', click: () => { const { setTheme } = require('../index'); setTheme('concrete-light'); } },
|
click: () => {
|
||||||
|
const { setTheme } = require('../index');
|
||||||
|
setTheme('github');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Light',
|
||||||
|
click: () => {
|
||||||
|
const { setTheme } = require('../index');
|
||||||
|
setTheme('light');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Solarized Light',
|
||||||
|
click: () => {
|
||||||
|
const { setTheme } = require('../index');
|
||||||
|
setTheme('solarized');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Gruvbox Light',
|
||||||
|
click: () => {
|
||||||
|
const { setTheme } = require('../index');
|
||||||
|
setTheme('gruvbox-light');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Ayu Light',
|
||||||
|
click: () => {
|
||||||
|
const { setTheme } = require('../index');
|
||||||
|
setTheme('ayu-light');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Sepia',
|
||||||
|
click: () => {
|
||||||
|
const { setTheme } = require('../index');
|
||||||
|
setTheme('sepia');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Paper',
|
||||||
|
click: () => {
|
||||||
|
const { setTheme } = require('../index');
|
||||||
|
setTheme('paper');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Rose Pine Dawn',
|
||||||
|
click: () => {
|
||||||
|
const { setTheme } = require('../index');
|
||||||
|
setTheme('rosepine-dawn');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Concrete Light',
|
||||||
|
click: () => {
|
||||||
|
const { setTheme } = require('../index');
|
||||||
|
setTheme('concrete-light');
|
||||||
|
},
|
||||||
|
},
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
{ label: 'Dark', click: () => { const { setTheme } = require('../index'); setTheme('dark'); } },
|
{
|
||||||
{ label: 'One Dark', click: () => { const { setTheme } = require('../index'); setTheme('onedark'); } },
|
label: 'Dark',
|
||||||
{ label: 'Dracula', click: () => { const { setTheme } = require('../index'); setTheme('dracula'); } },
|
click: () => {
|
||||||
{ label: 'Nord', click: () => { const { setTheme } = require('../index'); setTheme('nord'); } },
|
const { setTheme } = require('../index');
|
||||||
{ label: 'Monokai', click: () => { const { setTheme } = require('../index'); setTheme('monokai'); } },
|
setTheme('dark');
|
||||||
{ label: 'Material', click: () => { const { setTheme } = require('../index'); setTheme('material'); } },
|
},
|
||||||
{ label: 'Gruvbox Dark', click: () => { const { setTheme } = require('../index'); setTheme('gruvbox-dark'); } },
|
},
|
||||||
{ label: 'Tokyo Night', click: () => { const { setTheme } = require('../index'); setTheme('tokyonight'); } },
|
{
|
||||||
{ label: 'Palenight', click: () => { const { setTheme } = require('../index'); setTheme('palenight'); } },
|
label: 'One Dark',
|
||||||
{ label: 'Ayu Dark', click: () => { const { setTheme } = require('../index'); setTheme('ayu-dark'); } },
|
click: () => {
|
||||||
{ label: 'Ayu Mirage', click: () => { const { setTheme } = require('../index'); setTheme('ayu-mirage'); } },
|
const { setTheme } = require('../index');
|
||||||
{ label: 'Oceanic Next', click: () => { const { setTheme } = require('../index'); setTheme('oceanic-next'); } },
|
setTheme('onedark');
|
||||||
{ label: 'Cobalt2', click: () => { const { setTheme } = require('../index'); setTheme('cobalt2'); } },
|
},
|
||||||
{ label: 'Concrete Dark', click: () => { const { setTheme } = require('../index'); setTheme('concrete-dark'); } },
|
},
|
||||||
{ label: 'Concrete Warm', click: () => { const { setTheme } = require('../index'); setTheme('concrete-warm'); } }
|
{
|
||||||
]
|
label: 'Dracula',
|
||||||
|
click: () => {
|
||||||
|
const { setTheme } = require('../index');
|
||||||
|
setTheme('dracula');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Nord',
|
||||||
|
click: () => {
|
||||||
|
const { setTheme } = require('../index');
|
||||||
|
setTheme('nord');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Monokai',
|
||||||
|
click: () => {
|
||||||
|
const { setTheme } = require('../index');
|
||||||
|
setTheme('monokai');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Material',
|
||||||
|
click: () => {
|
||||||
|
const { setTheme } = require('../index');
|
||||||
|
setTheme('material');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Gruvbox Dark',
|
||||||
|
click: () => {
|
||||||
|
const { setTheme } = require('../index');
|
||||||
|
setTheme('gruvbox-dark');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Tokyo Night',
|
||||||
|
click: () => {
|
||||||
|
const { setTheme } = require('../index');
|
||||||
|
setTheme('tokyonight');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Palenight',
|
||||||
|
click: () => {
|
||||||
|
const { setTheme } = require('../index');
|
||||||
|
setTheme('palenight');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Ayu Dark',
|
||||||
|
click: () => {
|
||||||
|
const { setTheme } = require('../index');
|
||||||
|
setTheme('ayu-dark');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Ayu Mirage',
|
||||||
|
click: () => {
|
||||||
|
const { setTheme } = require('../index');
|
||||||
|
setTheme('ayu-mirage');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Oceanic Next',
|
||||||
|
click: () => {
|
||||||
|
const { setTheme } = require('../index');
|
||||||
|
setTheme('oceanic-next');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Cobalt2',
|
||||||
|
click: () => {
|
||||||
|
const { setTheme } = require('../index');
|
||||||
|
setTheme('cobalt2');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Concrete Dark',
|
||||||
|
click: () => {
|
||||||
|
const { setTheme } = require('../index');
|
||||||
|
setTheme('concrete-dark');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Concrete Warm',
|
||||||
|
click: () => {
|
||||||
|
const { setTheme } = require('../index');
|
||||||
|
setTheme('concrete-warm');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
{
|
{
|
||||||
@@ -281,19 +552,19 @@ function viewItems(mainWindow) {
|
|||||||
{
|
{
|
||||||
label: 'Increase Font Size',
|
label: 'Increase Font Size',
|
||||||
accelerator: 'CmdOrCtrl+Shift+Plus',
|
accelerator: 'CmdOrCtrl+Shift+Plus',
|
||||||
click: () => mainWindow.webContents.send('adjust-font-size', 'increase')
|
click: () => mainWindow.webContents.send('adjust-font-size', 'increase'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Decrease Font Size',
|
label: 'Decrease Font Size',
|
||||||
accelerator: 'CmdOrCtrl+Shift+-',
|
accelerator: 'CmdOrCtrl+Shift+-',
|
||||||
click: () => mainWindow.webContents.send('adjust-font-size', 'decrease')
|
click: () => mainWindow.webContents.send('adjust-font-size', 'decrease'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Reset Font Size',
|
label: 'Reset Font Size',
|
||||||
accelerator: 'CmdOrCtrl+Shift+0',
|
accelerator: 'CmdOrCtrl+Shift+0',
|
||||||
click: () => mainWindow.webContents.send('adjust-font-size', 'reset')
|
click: () => mainWindow.webContents.send('adjust-font-size', 'reset'),
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
{
|
{
|
||||||
@@ -302,7 +573,7 @@ function viewItems(mainWindow) {
|
|||||||
checked: true,
|
checked: true,
|
||||||
click: (menuItem) => {
|
click: (menuItem) => {
|
||||||
mainWindow.webContents.session.setSpellCheckerEnabled(menuItem.checked);
|
mainWindow.webContents.session.setSpellCheckerEnabled(menuItem.checked);
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
{
|
{
|
||||||
@@ -310,13 +581,13 @@ function viewItems(mainWindow) {
|
|||||||
submenu: [
|
submenu: [
|
||||||
{
|
{
|
||||||
label: 'Load Custom Preview CSS...',
|
label: 'Load Custom Preview CSS...',
|
||||||
click: () => mainWindow.webContents.send('load-custom-css')
|
click: () => mainWindow.webContents.send('load-custom-css'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Clear Custom Preview CSS',
|
label: 'Clear Custom Preview CSS',
|
||||||
click: () => mainWindow.webContents.send('clear-custom-css')
|
click: () => mainWindow.webContents.send('clear-custom-css'),
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
{ label: 'Reload', accelerator: 'CmdOrCtrl+R', role: 'reload' },
|
{ label: 'Reload', accelerator: 'CmdOrCtrl+R', role: 'reload' },
|
||||||
@@ -324,7 +595,7 @@ function viewItems(mainWindow) {
|
|||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
{ label: 'Zoom In', accelerator: 'CmdOrCtrl+Plus', role: 'zoomIn' },
|
{ label: 'Zoom In', accelerator: 'CmdOrCtrl+Plus', role: 'zoomIn' },
|
||||||
{ label: 'Zoom Out', accelerator: 'CmdOrCtrl+-', role: 'zoomOut' },
|
{ label: 'Zoom Out', accelerator: 'CmdOrCtrl+-', role: 'zoomOut' },
|
||||||
{ label: 'Reset Zoom', accelerator: 'CmdOrCtrl+0', role: 'resetZoom' }
|
{ label: 'Reset Zoom', accelerator: 'CmdOrCtrl+0', role: 'resetZoom' },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -335,25 +606,25 @@ function batchItems(mainWindow) {
|
|||||||
click: () => {
|
click: () => {
|
||||||
const { showBatchConversionDialog } = require('../index');
|
const { showBatchConversionDialog } = require('../index');
|
||||||
showBatchConversionDialog();
|
showBatchConversionDialog();
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
{
|
{
|
||||||
label: 'Batch Image Conversion...',
|
label: 'Batch Image Conversion...',
|
||||||
click: () => mainWindow.webContents.send('show-batch-converter', 'image')
|
click: () => mainWindow.webContents.send('show-batch-converter', 'image'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Batch Audio Conversion...',
|
label: 'Batch Audio Conversion...',
|
||||||
click: () => mainWindow.webContents.send('show-batch-converter', 'audio')
|
click: () => mainWindow.webContents.send('show-batch-converter', 'audio'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Batch Video Conversion...',
|
label: 'Batch Video Conversion...',
|
||||||
click: () => mainWindow.webContents.send('show-batch-converter', 'video')
|
click: () => mainWindow.webContents.send('show-batch-converter', 'video'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Batch PDF Conversion...',
|
label: 'Batch PDF Conversion...',
|
||||||
click: () => mainWindow.webContents.send('show-batch-converter', 'pdf')
|
click: () => mainWindow.webContents.send('show-batch-converter', 'pdf'),
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -365,8 +636,8 @@ function convertItems(mainWindow) {
|
|||||||
click: () => {
|
click: () => {
|
||||||
const { showUniversalConverterDialog } = require('../index');
|
const { showUniversalConverterDialog } = require('../index');
|
||||||
showUniversalConverterDialog();
|
showUniversalConverterDialog();
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -378,7 +649,7 @@ function pdfEditorItems(mainWindow) {
|
|||||||
click: () => {
|
click: () => {
|
||||||
const { openPdfFile } = require('../index');
|
const { openPdfFile } = require('../index');
|
||||||
openPdfFile();
|
openPdfFile();
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
{
|
{
|
||||||
@@ -386,21 +657,21 @@ function pdfEditorItems(mainWindow) {
|
|||||||
click: () => {
|
click: () => {
|
||||||
const { showPDFEditorDialog } = require('../index');
|
const { showPDFEditorDialog } = require('../index');
|
||||||
showPDFEditorDialog('merge');
|
showPDFEditorDialog('merge');
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Split PDF...',
|
label: 'Split PDF...',
|
||||||
click: () => {
|
click: () => {
|
||||||
const { showPDFEditorDialog } = require('../index');
|
const { showPDFEditorDialog } = require('../index');
|
||||||
showPDFEditorDialog('split');
|
showPDFEditorDialog('split');
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Compress PDF...',
|
label: 'Compress PDF...',
|
||||||
click: () => {
|
click: () => {
|
||||||
const { showPDFEditorDialog } = require('../index');
|
const { showPDFEditorDialog } = require('../index');
|
||||||
showPDFEditorDialog('compress');
|
showPDFEditorDialog('compress');
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
{
|
{
|
||||||
@@ -408,21 +679,21 @@ function pdfEditorItems(mainWindow) {
|
|||||||
click: () => {
|
click: () => {
|
||||||
const { showPDFEditorDialog } = require('../index');
|
const { showPDFEditorDialog } = require('../index');
|
||||||
showPDFEditorDialog('rotate');
|
showPDFEditorDialog('rotate');
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Delete Pages...',
|
label: 'Delete Pages...',
|
||||||
click: () => {
|
click: () => {
|
||||||
const { showPDFEditorDialog } = require('../index');
|
const { showPDFEditorDialog } = require('../index');
|
||||||
showPDFEditorDialog('delete');
|
showPDFEditorDialog('delete');
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Reorder Pages...',
|
label: 'Reorder Pages...',
|
||||||
click: () => {
|
click: () => {
|
||||||
const { showPDFEditorDialog } = require('../index');
|
const { showPDFEditorDialog } = require('../index');
|
||||||
showPDFEditorDialog('reorder');
|
showPDFEditorDialog('reorder');
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
{
|
{
|
||||||
@@ -430,7 +701,7 @@ function pdfEditorItems(mainWindow) {
|
|||||||
click: () => {
|
click: () => {
|
||||||
const { showPDFEditorDialog } = require('../index');
|
const { showPDFEditorDialog } = require('../index');
|
||||||
showPDFEditorDialog('watermark');
|
showPDFEditorDialog('watermark');
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
{
|
{
|
||||||
@@ -441,23 +712,23 @@ function pdfEditorItems(mainWindow) {
|
|||||||
click: () => {
|
click: () => {
|
||||||
const { showPDFEditorDialog } = require('../index');
|
const { showPDFEditorDialog } = require('../index');
|
||||||
showPDFEditorDialog('encrypt');
|
showPDFEditorDialog('encrypt');
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Remove Password...',
|
label: 'Remove Password...',
|
||||||
click: () => {
|
click: () => {
|
||||||
const { showPDFEditorDialog } = require('../index');
|
const { showPDFEditorDialog } = require('../index');
|
||||||
showPDFEditorDialog('decrypt');
|
showPDFEditorDialog('decrypt');
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Set Permissions...',
|
label: 'Set Permissions...',
|
||||||
click: () => {
|
click: () => {
|
||||||
const { showPDFEditorDialog } = require('../index');
|
const { showPDFEditorDialog } = require('../index');
|
||||||
showPDFEditorDialog('permissions');
|
showPDFEditorDialog('permissions');
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
{
|
{
|
||||||
@@ -465,8 +736,8 @@ function pdfEditorItems(mainWindow) {
|
|||||||
click: () => {
|
click: () => {
|
||||||
const { showAboutDialog } = require('../index');
|
const { showAboutDialog } = require('../index');
|
||||||
showAboutDialog();
|
showAboutDialog();
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -478,8 +749,8 @@ function toolsItems(mainWindow) {
|
|||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
{
|
{
|
||||||
label: 'Document Compare',
|
label: 'Document Compare',
|
||||||
click: () => mainWindow.webContents.send('show-document-compare')
|
click: () => mainWindow.webContents.send('show-document-compare'),
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -490,7 +761,7 @@ function helpItems(mainWindow) {
|
|||||||
click: () => {
|
click: () => {
|
||||||
const { showAboutDialog } = require('../index');
|
const { showAboutDialog } = require('../index');
|
||||||
showAboutDialog();
|
showAboutDialog();
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
{
|
{
|
||||||
@@ -498,21 +769,21 @@ function helpItems(mainWindow) {
|
|||||||
click: () => {
|
click: () => {
|
||||||
const { showDependenciesDialog } = require('../index');
|
const { showDependenciesDialog } = require('../index');
|
||||||
showDependenciesDialog();
|
showDependenciesDialog();
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
{
|
{
|
||||||
label: 'Documentation',
|
label: 'Documentation',
|
||||||
click: () => shell.openExternal('https://github.com/amitwh/markdown-converter')
|
click: () => shell.openExternal('https://github.com/amitwh/markdown-converter'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Report Issue',
|
label: 'Report Issue',
|
||||||
click: () => shell.openExternal('https://github.com/amitwh/markdown-converter/issues')
|
click: () => shell.openExternal('https://github.com/amitwh/markdown-converter/issues'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Check for Updates',
|
label: 'Check for Updates',
|
||||||
click: () => shell.openExternal('https://github.com/amitwh/markdown-converter/releases')
|
click: () => shell.openExternal('https://github.com/amitwh/markdown-converter/releases'),
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -524,5 +795,5 @@ module.exports = {
|
|||||||
convertItems,
|
convertItems,
|
||||||
pdfEditorItems,
|
pdfEditorItems,
|
||||||
toolsItems,
|
toolsItems,
|
||||||
helpItems
|
helpItems,
|
||||||
};
|
};
|
||||||
+1
-1
@@ -25,7 +25,7 @@ const store = {
|
|||||||
} catch {}
|
} catch {}
|
||||||
settings[key] = value;
|
settings[key] = value;
|
||||||
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = store;
|
module.exports = store;
|
||||||
|
|||||||
@@ -29,13 +29,18 @@ class CrashWriter {
|
|||||||
const files = fs.readdirSync(this.dir).sort();
|
const files = fs.readdirSync(this.dir).sort();
|
||||||
while (files.length > MAX_DUMPS) {
|
while (files.length > MAX_DUMPS) {
|
||||||
const oldest = files.shift();
|
const oldest = files.shift();
|
||||||
try { fs.unlinkSync(path.join(this.dir, oldest)); } catch (_) { /* ignore */ }
|
try {
|
||||||
|
fs.unlinkSync(path.join(this.dir, oldest));
|
||||||
|
} catch (_) {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
list() {
|
list() {
|
||||||
if (!fs.existsSync(this.dir)) return [];
|
if (!fs.existsSync(this.dir)) return [];
|
||||||
return fs.readdirSync(this.dir)
|
return fs
|
||||||
|
.readdirSync(this.dir)
|
||||||
.filter((f) => f.endsWith('.json'))
|
.filter((f) => f.endsWith('.json'))
|
||||||
.sort()
|
.sort()
|
||||||
.reverse()
|
.reverse()
|
||||||
|
|||||||
@@ -2,16 +2,19 @@
|
|||||||
// Kept in sync manually; if the renderer transform changes, update this too.
|
// Kept in sync manually; if the renderer transform changes, update this too.
|
||||||
const { z } = require('zod');
|
const { z } = require('zod');
|
||||||
|
|
||||||
const v4SettingsSchema = z.object({
|
const v4SettingsSchema = z
|
||||||
|
.object({
|
||||||
theme: z.enum(['light', 'dark', 'auto']).default('auto'),
|
theme: z.enum(['light', 'dark', 'auto']).default('auto'),
|
||||||
customCss: z.string().optional().nullable(),
|
customCss: z.string().optional().nullable(),
|
||||||
recentFiles: z.array(z.string()).default([]),
|
recentFiles: z.array(z.string()).default([]),
|
||||||
editorFontSize: z.number().min(10).max(28).default(14),
|
editorFontSize: z.number().min(10).max(28).default(14),
|
||||||
keyBindings: z.record(z.string(), z.string()).optional(),
|
keyBindings: z.record(z.string(), z.string()).optional(),
|
||||||
snippets: z.array(z.unknown()).default([]),
|
snippets: z.array(z.unknown()).default([]),
|
||||||
}).passthrough();
|
})
|
||||||
|
.passthrough();
|
||||||
|
|
||||||
const v5SettingsSchema = z.object({
|
const v5SettingsSchema = z
|
||||||
|
.object({
|
||||||
fontSize: z.number().default(14),
|
fontSize: z.number().default(14),
|
||||||
tabSize: z.number().default(4),
|
tabSize: z.number().default(4),
|
||||||
lineNumbers: z.boolean().default(true),
|
lineNumbers: z.boolean().default(true),
|
||||||
@@ -37,7 +40,8 @@ const v5SettingsSchema = z.object({
|
|||||||
autoCheckUpdates: z.boolean().default(true),
|
autoCheckUpdates: z.boolean().default(true),
|
||||||
firstRun: z.boolean().default(true),
|
firstRun: z.boolean().default(true),
|
||||||
'migration.version': z.literal(5).optional(),
|
'migration.version': z.literal(5).optional(),
|
||||||
}).passthrough();
|
})
|
||||||
|
.passthrough();
|
||||||
|
|
||||||
const v5OnlyFields = ['updateChannel', 'autoCheckUpdates', 'firstRun'];
|
const v5OnlyFields = ['updateChannel', 'autoCheckUpdates', 'firstRun'];
|
||||||
const v5ThemeValues = ['light', 'dark', 'system'];
|
const v5ThemeValues = ['light', 'dark', 'system'];
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ class UpdaterService extends EventEmitter {
|
|||||||
au.on('update-downloaded', (info) => this._emit({ state: 'ready', version: info.version }));
|
au.on('update-downloaded', (info) => this._emit({ state: 'ready', version: info.version }));
|
||||||
au.on('update-not-available', () => this._emit({ state: 'idle' }));
|
au.on('update-not-available', () => this._emit({ state: 'idle' }));
|
||||||
au.on('error', (err) => {
|
au.on('error', (err) => {
|
||||||
const code = err && /ENOTFOUND|ETIMEDOUT|ECONNREFUSED/.test(err.message) ? 'NETWORK' : 'UNKNOWN';
|
const code =
|
||||||
|
err && /ENOTFOUND|ETIMEDOUT|ECONNREFUSED/.test(err.message) ? 'NETWORK' : 'UNKNOWN';
|
||||||
this._emit({ state: 'error', code });
|
this._emit({ state: 'error', code });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ function getAllowedDirectories() {
|
|||||||
app.getPath('desktop'),
|
app.getPath('desktop'),
|
||||||
app.getPath('downloads'),
|
app.getPath('downloads'),
|
||||||
app.getPath('home'),
|
app.getPath('home'),
|
||||||
process.cwd() // Current working directory
|
process.cwd(), // Current working directory
|
||||||
].filter(Boolean); // Remove any undefined paths
|
].filter(Boolean); // Remove any undefined paths
|
||||||
return dirs;
|
return dirs;
|
||||||
}
|
}
|
||||||
@@ -88,9 +88,13 @@ function resolveWritablePath(filePath) {
|
|||||||
function isPathAccessible(resolvedPath) {
|
function isPathAccessible(resolvedPath) {
|
||||||
// Block access to sensitive system directories
|
// Block access to sensitive system directories
|
||||||
const blockedPaths = [
|
const blockedPaths = [
|
||||||
'/etc/passwd', '/etc/shadow', '/root',
|
'/etc/passwd',
|
||||||
'C:\\Windows\\System32', 'C:\\Windows\\System',
|
'/etc/shadow',
|
||||||
'/System', '/private/etc'
|
'/root',
|
||||||
|
'C:\\Windows\\System32',
|
||||||
|
'C:\\Windows\\System',
|
||||||
|
'/System',
|
||||||
|
'/private/etc',
|
||||||
];
|
];
|
||||||
|
|
||||||
const normalizedPath = resolvedPath.toLowerCase();
|
const normalizedPath = resolvedPath.toLowerCase();
|
||||||
|
|||||||
@@ -25,9 +25,9 @@ function createMainWindow() {
|
|||||||
// on load and the renderer never gets the IPC bridge.
|
// on load and the renderer never gets the IPC bridge.
|
||||||
contextIsolation: true,
|
contextIsolation: true,
|
||||||
nodeIntegration: false,
|
nodeIntegration: false,
|
||||||
spellcheck: true
|
spellcheck: true,
|
||||||
},
|
},
|
||||||
icon: path.join(__dirname, '../../../assets/icon.png')
|
icon: path.join(__dirname, '../../../assets/icon.png'),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Dev (Vite): load the running dev server so .tsx is transformed on the fly.
|
// Dev (Vite): load the running dev server so .tsx is transformed on the fly.
|
||||||
@@ -54,7 +54,7 @@ function createMainWindow() {
|
|||||||
console.error(
|
console.error(
|
||||||
'[WINDOW] Renderer not found at',
|
'[WINDOW] Renderer not found at',
|
||||||
rendererIndex,
|
rendererIndex,
|
||||||
'— did you run `npm run build:renderer` before packaging?',
|
'— did you run `npm run build:renderer` before packaging?'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -84,18 +84,23 @@ function createMainWindow() {
|
|||||||
// Add spell check suggestions
|
// Add spell check suggestions
|
||||||
if (params.misspelledWord) {
|
if (params.misspelledWord) {
|
||||||
for (const suggestion of params.dictionarySuggestions) {
|
for (const suggestion of params.dictionarySuggestions) {
|
||||||
ctxMenu.append(new MenuItem({
|
ctxMenu.append(
|
||||||
|
new MenuItem({
|
||||||
label: suggestion,
|
label: suggestion,
|
||||||
click: () => win.webContents.replaceMisspelling(suggestion)
|
click: () => win.webContents.replaceMisspelling(suggestion),
|
||||||
}));
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (params.dictionarySuggestions.length > 0) {
|
if (params.dictionarySuggestions.length > 0) {
|
||||||
ctxMenu.append(new MenuItem({ type: 'separator' }));
|
ctxMenu.append(new MenuItem({ type: 'separator' }));
|
||||||
}
|
}
|
||||||
ctxMenu.append(new MenuItem({
|
ctxMenu.append(
|
||||||
|
new MenuItem({
|
||||||
label: 'Add to Dictionary',
|
label: 'Add to Dictionary',
|
||||||
click: () => win.webContents.session.addWordToSpellCheckerDictionary(params.misspelledWord)
|
click: () =>
|
||||||
}));
|
win.webContents.session.addWordToSpellCheckerDictionary(params.misspelledWord),
|
||||||
|
})
|
||||||
|
);
|
||||||
ctxMenu.append(new MenuItem({ type: 'separator' }));
|
ctxMenu.append(new MenuItem({ type: 'separator' }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ class WordTemplateExporter {
|
|||||||
b5: { width: 9979, height: 14170 },
|
b5: { width: 9979, height: 14170 },
|
||||||
letter: { width: 12240, height: 15840 },
|
letter: { width: 12240, height: 15840 },
|
||||||
legal: { width: 12240, height: 20160 },
|
legal: { width: 12240, height: 20160 },
|
||||||
tabloid: { width: 15840, height: 24480 }
|
tabloid: { width: 15840, height: 24480 },
|
||||||
};
|
};
|
||||||
|
|
||||||
let width, height;
|
let width, height;
|
||||||
@@ -373,7 +373,10 @@ class WordTemplateExporter {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// Split by | and trim
|
// Split by | and trim
|
||||||
const cells = line.split('|').filter(cell => cell.trim()).map(cell => cell.trim());
|
const cells = line
|
||||||
|
.split('|')
|
||||||
|
.filter((cell) => cell.trim())
|
||||||
|
.map((cell) => cell.trim());
|
||||||
if (cells.length > 0) {
|
if (cells.length > 0) {
|
||||||
rows.push(cells);
|
rows.push(cells);
|
||||||
}
|
}
|
||||||
@@ -382,7 +385,7 @@ class WordTemplateExporter {
|
|||||||
if (rows.length === 0) return '';
|
if (rows.length === 0) return '';
|
||||||
|
|
||||||
// Calculate number of columns
|
// Calculate number of columns
|
||||||
const numCols = Math.max(...rows.map(row => row.length));
|
const numCols = Math.max(...rows.map((row) => row.length));
|
||||||
|
|
||||||
// Calculate column width in twips (1440 twips = 1 inch)
|
// Calculate column width in twips (1440 twips = 1 inch)
|
||||||
// Assume standard page width of 9360 twips (6.5 inches usable)
|
// Assume standard page width of 9360 twips (6.5 inches usable)
|
||||||
@@ -444,7 +447,8 @@ class WordTemplateExporter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Cell borders
|
// Cell borders
|
||||||
tableXml += '<w:tcBorders>' +
|
tableXml +=
|
||||||
|
'<w:tcBorders>' +
|
||||||
'<w:top w:val="single" w:sz="8" w:space="0" w:color="F58220"/>' +
|
'<w:top w:val="single" w:sz="8" w:space="0" w:color="F58220"/>' +
|
||||||
'<w:left w:val="single" w:sz="8" w:space="0" w:color="F58220"/>' +
|
'<w:left w:val="single" w:sz="8" w:space="0" w:color="F58220"/>' +
|
||||||
'<w:bottom w:val="single" w:sz="8" w:space="0" w:color="F58220"/>' +
|
'<w:bottom w:val="single" w:sz="8" w:space="0" w:color="F58220"/>' +
|
||||||
@@ -452,7 +456,8 @@ class WordTemplateExporter {
|
|||||||
'</w:tcBorders>';
|
'</w:tcBorders>';
|
||||||
|
|
||||||
// Cell margins for proper padding
|
// Cell margins for proper padding
|
||||||
tableXml += '<w:tcMar>' +
|
tableXml +=
|
||||||
|
'<w:tcMar>' +
|
||||||
'<w:top w:w="80" w:type="dxa"/>' +
|
'<w:top w:w="80" w:type="dxa"/>' +
|
||||||
'<w:left w:w="120" w:type="dxa"/>' +
|
'<w:left w:w="120" w:type="dxa"/>' +
|
||||||
'<w:bottom w:w="80" w:type="dxa"/>' +
|
'<w:bottom w:w="80" w:type="dxa"/>' +
|
||||||
@@ -499,7 +504,7 @@ class WordTemplateExporter {
|
|||||||
// Don't treat markdown tables as ASCII art
|
// Don't treat markdown tables as ASCII art
|
||||||
if (line.trim().startsWith('|') && line.trim().endsWith('|')) {
|
if (line.trim().startsWith('|') && line.trim().endsWith('|')) {
|
||||||
// Check if it's a proper markdown table (has multiple cells)
|
// Check if it's a proper markdown table (has multiple cells)
|
||||||
const cells = line.split('|').filter(c => c.trim());
|
const cells = line.split('|').filter((c) => c.trim());
|
||||||
if (cells.length >= 2) {
|
if (cells.length >= 2) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -507,17 +512,72 @@ class WordTemplateExporter {
|
|||||||
|
|
||||||
// Common ASCII art characters (Unicode box drawing and symbols)
|
// Common ASCII art characters (Unicode box drawing and symbols)
|
||||||
const asciiArtChars = [
|
const asciiArtChars = [
|
||||||
'─', '│', '┌', '┐', '└', '┘', '├', '┤', '┬', '┴', '┼', // Box drawing
|
'─',
|
||||||
'═', '║', '╔', '╗', '╚', '╝', '╠', '╣', '╦', '╩', '╬', // Double box
|
'│',
|
||||||
'╭', '╮', '╯', '╰', // Rounded corners
|
'┌',
|
||||||
'▲', '▼', '◄', '►', '♦', '●', '○', '■', '□', '◆', '◇', // Shapes
|
'┐',
|
||||||
'↓', '→', '←', '↑', '↔', '↕', '⇒', '⇐', '⇓', '⇑', // Arrows
|
'└',
|
||||||
'┃', '━', '┏', '┓', '┗', '┛', '┣', '┫', '┳', '┻', '╋', // Heavy box
|
'┘',
|
||||||
'░', '▒', '▓', '█', // Shading
|
'├',
|
||||||
|
'┤',
|
||||||
|
'┬',
|
||||||
|
'┴',
|
||||||
|
'┼', // Box drawing
|
||||||
|
'═',
|
||||||
|
'║',
|
||||||
|
'╔',
|
||||||
|
'╗',
|
||||||
|
'╚',
|
||||||
|
'╝',
|
||||||
|
'╠',
|
||||||
|
'╣',
|
||||||
|
'╦',
|
||||||
|
'╩',
|
||||||
|
'╬', // Double box
|
||||||
|
'╭',
|
||||||
|
'╮',
|
||||||
|
'╯',
|
||||||
|
'╰', // Rounded corners
|
||||||
|
'▲',
|
||||||
|
'▼',
|
||||||
|
'◄',
|
||||||
|
'►',
|
||||||
|
'♦',
|
||||||
|
'●',
|
||||||
|
'○',
|
||||||
|
'■',
|
||||||
|
'□',
|
||||||
|
'◆',
|
||||||
|
'◇', // Shapes
|
||||||
|
'↓',
|
||||||
|
'→',
|
||||||
|
'←',
|
||||||
|
'↑',
|
||||||
|
'↔',
|
||||||
|
'↕',
|
||||||
|
'⇒',
|
||||||
|
'⇐',
|
||||||
|
'⇓',
|
||||||
|
'⇑', // Arrows
|
||||||
|
'┃',
|
||||||
|
'━',
|
||||||
|
'┏',
|
||||||
|
'┓',
|
||||||
|
'┗',
|
||||||
|
'┛',
|
||||||
|
'┣',
|
||||||
|
'┫',
|
||||||
|
'┳',
|
||||||
|
'┻',
|
||||||
|
'╋', // Heavy box
|
||||||
|
'░',
|
||||||
|
'▒',
|
||||||
|
'▓',
|
||||||
|
'█', // Shading
|
||||||
];
|
];
|
||||||
|
|
||||||
// Check for box drawing characters
|
// Check for box drawing characters
|
||||||
if (asciiArtChars.some(char => line.includes(char))) {
|
if (asciiArtChars.some((char) => line.includes(char))) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -537,7 +597,7 @@ class WordTemplateExporter {
|
|||||||
/^\s*\*[-=\*]+\*/, // *----*
|
/^\s*\*[-=\*]+\*/, // *----*
|
||||||
];
|
];
|
||||||
|
|
||||||
return asciiPatterns.some(pattern => pattern.test(line));
|
return asciiPatterns.some((pattern) => pattern.test(line));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -564,7 +624,7 @@ class WordTemplateExporter {
|
|||||||
|
|
||||||
// Check if line contains arrow characters and color them red
|
// Check if line contains arrow characters and color them red
|
||||||
const arrowChars = ['↓', '→', '←', '↑', '▼', '►', '◄', '▲'];
|
const arrowChars = ['↓', '→', '←', '↑', '▼', '►', '◄', '▲'];
|
||||||
const hasArrow = arrowChars.some(arrow => line.includes(arrow));
|
const hasArrow = arrowChars.some((arrow) => line.includes(arrow));
|
||||||
|
|
||||||
if (hasArrow) {
|
if (hasArrow) {
|
||||||
// Split line into parts and color arrows red
|
// Split line into parts and color arrows red
|
||||||
@@ -662,7 +722,7 @@ class WordTemplateExporter {
|
|||||||
{ regex: /\*\*\*(.+?)\*\*\*/g, bold: true, italic: true },
|
{ regex: /\*\*\*(.+?)\*\*\*/g, bold: true, italic: true },
|
||||||
{ regex: /\*\*(.+?)\*\*/g, bold: true },
|
{ regex: /\*\*(.+?)\*\*/g, bold: true },
|
||||||
{ regex: /\*(.+?)\*/g, italic: true },
|
{ regex: /\*(.+?)\*/g, italic: true },
|
||||||
{ regex: /`(.+?)`/g, code: true }
|
{ regex: /`(.+?)`/g, code: true },
|
||||||
];
|
];
|
||||||
|
|
||||||
// Simple approach: process text sequentially
|
// Simple approach: process text sequentially
|
||||||
@@ -693,7 +753,12 @@ class WordTemplateExporter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add formatted text
|
// Add formatted text
|
||||||
xml += this.createRunXml(match[1], matchedPattern.bold, matchedPattern.italic, matchedPattern.code);
|
xml += this.createRunXml(
|
||||||
|
match[1],
|
||||||
|
matchedPattern.bold,
|
||||||
|
matchedPattern.italic,
|
||||||
|
matchedPattern.code
|
||||||
|
);
|
||||||
|
|
||||||
remaining = remaining.substring(earliestPos + match[0].length);
|
remaining = remaining.substring(earliestPos + match[0].length);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -5,9 +5,7 @@
|
|||||||
"description": "Demonstrates the plugin system. Safe to delete.",
|
"description": "Demonstrates the plugin system. Safe to delete.",
|
||||||
"icon": "puzzle",
|
"icon": "puzzle",
|
||||||
"extensionPoints": {
|
"extensionPoints": {
|
||||||
"commands": [
|
"commands": [{ "id": "hello", "label": "Sample: Hello World", "shortcut": "" }]
|
||||||
{ "id": "hello", "label": "Sample: Hello World", "shortcut": "" }
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"settings": []
|
"settings": []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ class WritingStudioPlugin extends PluginAPI {
|
|||||||
this.context = context;
|
this.context = context;
|
||||||
|
|
||||||
this.sprintEngine = new SprintEngine({
|
this.sprintEngine = new SprintEngine({
|
||||||
onEvent: (name, data) => context.events.emit(name, data)
|
onEvent: (name, data) => context.events.emit(name, data),
|
||||||
});
|
});
|
||||||
this.goalTracker = new GoalTracker(context.settings);
|
this.goalTracker = new GoalTracker(context.settings);
|
||||||
this.snapshotManager = new SnapshotManager(context.settings);
|
this.snapshotManager = new SnapshotManager(context.settings);
|
||||||
@@ -17,14 +17,14 @@ class WritingStudioPlugin extends PluginAPI {
|
|||||||
readFile: (p) => context.ipc.invoke('read-file', p),
|
readFile: (p) => context.ipc.invoke('read-file', p),
|
||||||
writeFile: (p, c) => context.ipc.invoke('write-file', p, c),
|
writeFile: (p, c) => context.ipc.invoke('write-file', p, c),
|
||||||
fileExists: (p) => context.ipc.invoke('path-exists', p),
|
fileExists: (p) => context.ipc.invoke('path-exists', p),
|
||||||
listDir: (p) => context.ipc.invoke('list-directory', p)
|
listDir: (p) => context.ipc.invoke('list-directory', p),
|
||||||
});
|
});
|
||||||
|
|
||||||
this._engines = {
|
this._engines = {
|
||||||
sprint: this.sprintEngine,
|
sprint: this.sprintEngine,
|
||||||
goals: this.goalTracker,
|
goals: this.goalTracker,
|
||||||
snapshots: this.snapshotManager,
|
snapshots: this.snapshotManager,
|
||||||
projects: this.projectManager
|
projects: this.projectManager,
|
||||||
};
|
};
|
||||||
|
|
||||||
this._registerCommands(context);
|
this._registerCommands(context);
|
||||||
@@ -34,59 +34,89 @@ class WritingStudioPlugin extends PluginAPI {
|
|||||||
_registerCommands(context) {
|
_registerCommands(context) {
|
||||||
const { sprintEngine, snapshotManager, goalTracker } = this;
|
const { sprintEngine, snapshotManager, goalTracker } = this;
|
||||||
|
|
||||||
context.commands.register('start-sprint', 'Studio: Start Sprint', () => {
|
context.commands.register(
|
||||||
|
'start-sprint',
|
||||||
|
'Studio: Start Sprint',
|
||||||
|
() => {
|
||||||
const duration = context.settings.get('sprintDuration') || 25;
|
const duration = context.settings.get('sprintDuration') || 25;
|
||||||
const content = context.editor.getContent() || '';
|
const content = context.editor.getContent() || '';
|
||||||
const words = content.split(/\s+/).filter(Boolean).length;
|
const words = content.split(/\s+/).filter(Boolean).length;
|
||||||
sprintEngine.start(duration, words);
|
sprintEngine.start(duration, words);
|
||||||
}, 'Ctrl+Alt+S');
|
},
|
||||||
|
'Ctrl+Alt+S'
|
||||||
|
);
|
||||||
|
|
||||||
context.commands.register('stop-sprint', 'Studio: Stop Sprint', () => {
|
context.commands.register(
|
||||||
|
'stop-sprint',
|
||||||
|
'Studio: Stop Sprint',
|
||||||
|
() => {
|
||||||
if (!sprintEngine.isActive()) return;
|
if (!sprintEngine.isActive()) return;
|
||||||
const content = context.editor.getContent() || '';
|
const content = context.editor.getContent() || '';
|
||||||
const words = content.split(/\s+/).filter(Boolean).length;
|
const words = content.split(/\s+/).filter(Boolean).length;
|
||||||
const result = sprintEngine.stop(words);
|
const result = sprintEngine.stop(words);
|
||||||
goalTracker.addWords(result.wordDelta);
|
goalTracker.addWords(result.wordDelta);
|
||||||
context.events.emit('sprint:stopped', result);
|
context.events.emit('sprint:stopped', result);
|
||||||
}, 'Ctrl+Alt+Shift+S');
|
},
|
||||||
|
'Ctrl+Alt+Shift+S'
|
||||||
|
);
|
||||||
|
|
||||||
context.commands.register('take-snapshot', 'Studio: Take Snapshot', () => {
|
context.commands.register(
|
||||||
|
'take-snapshot',
|
||||||
|
'Studio: Take Snapshot',
|
||||||
|
() => {
|
||||||
const content = context.editor.getContent() || '';
|
const content = context.editor.getContent() || '';
|
||||||
snapshotManager.create(content, 'manual');
|
snapshotManager.create(content, 'manual');
|
||||||
context.events.emit('snapshot:created', {});
|
context.events.emit('snapshot:created', {});
|
||||||
}, 'Ctrl+Alt+N');
|
},
|
||||||
|
'Ctrl+Alt+N'
|
||||||
|
);
|
||||||
|
|
||||||
context.commands.register('restore-last-snapshot', 'Studio: Restore Last Snapshot', () => {
|
context.commands.register(
|
||||||
|
'restore-last-snapshot',
|
||||||
|
'Studio: Restore Last Snapshot',
|
||||||
|
() => {
|
||||||
const snaps = snapshotManager.list();
|
const snaps = snapshotManager.list();
|
||||||
if (snaps.length === 0) return;
|
if (snaps.length === 0) return;
|
||||||
const content = snapshotManager.restore(snaps[0].id);
|
const content = snapshotManager.restore(snaps[0].id);
|
||||||
context.editor.insertAtCursor(content);
|
context.editor.insertAtCursor(content);
|
||||||
}, 'Ctrl+Alt+Z');
|
},
|
||||||
|
'Ctrl+Alt+Z'
|
||||||
|
);
|
||||||
|
|
||||||
context.commands.register('new-project', 'Studio: New Project', () => {
|
context.commands.register('new-project', 'Studio: New Project', () => {
|
||||||
context.events.emit('studio:new-project', {});
|
context.events.emit('studio:new-project', {});
|
||||||
});
|
});
|
||||||
|
|
||||||
context.commands.register('compile-manuscript', 'Studio: Compile Manuscript', () => {
|
context.commands.register(
|
||||||
|
'compile-manuscript',
|
||||||
|
'Studio: Compile Manuscript',
|
||||||
|
() => {
|
||||||
context.events.emit('studio:compile', {});
|
context.events.emit('studio:compile', {});
|
||||||
}, 'Ctrl+Alt+E');
|
},
|
||||||
|
'Ctrl+Alt+E'
|
||||||
|
);
|
||||||
|
|
||||||
context.commands.register('proofread-document', 'Studio: Proofread Document', () => {
|
context.commands.register(
|
||||||
|
'proofread-document',
|
||||||
|
'Studio: Proofread Document',
|
||||||
|
() => {
|
||||||
if (context.events.hasHandler('ai:analyze')) {
|
if (context.events.hasHandler('ai:analyze')) {
|
||||||
const content = context.editor.getContent() || '';
|
const content = context.editor.getContent() || '';
|
||||||
context.events.emit('ai:analyze', { text: content, type: 'grammar' });
|
context.events.emit('ai:analyze', { text: content, type: 'grammar' });
|
||||||
}
|
}
|
||||||
}, 'Ctrl+Alt+G');
|
},
|
||||||
|
'Ctrl+Alt+G'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_registerStatusBar(context) {
|
_registerStatusBar(context) {
|
||||||
context.statusBar.registerIndicator('word-goal', {
|
context.statusBar.registerIndicator('word-goal', {
|
||||||
text: '0/1000',
|
text: '0/1000',
|
||||||
tooltip: 'Daily word goal progress'
|
tooltip: 'Daily word goal progress',
|
||||||
});
|
});
|
||||||
context.statusBar.registerIndicator('sprint-timer', {
|
context.statusBar.registerIndicator('sprint-timer', {
|
||||||
text: '',
|
text: '',
|
||||||
tooltip: 'Writing sprint timer'
|
tooltip: 'Writing sprint timer',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,17 +15,34 @@
|
|||||||
{ "id": "start-sprint", "label": "Studio: Start Sprint", "shortcut": "Ctrl+Alt+S" },
|
{ "id": "start-sprint", "label": "Studio: Start Sprint", "shortcut": "Ctrl+Alt+S" },
|
||||||
{ "id": "stop-sprint", "label": "Studio: Stop Sprint", "shortcut": "Ctrl+Alt+Shift+S" },
|
{ "id": "stop-sprint", "label": "Studio: Stop Sprint", "shortcut": "Ctrl+Alt+Shift+S" },
|
||||||
{ "id": "take-snapshot", "label": "Studio: Take Snapshot", "shortcut": "Ctrl+Alt+N" },
|
{ "id": "take-snapshot", "label": "Studio: Take Snapshot", "shortcut": "Ctrl+Alt+N" },
|
||||||
{ "id": "restore-last-snapshot", "label": "Studio: Restore Last Snapshot", "shortcut": "Ctrl+Alt+Z" },
|
{
|
||||||
|
"id": "restore-last-snapshot",
|
||||||
|
"label": "Studio: Restore Last Snapshot",
|
||||||
|
"shortcut": "Ctrl+Alt+Z"
|
||||||
|
},
|
||||||
{ "id": "new-project", "label": "Studio: New Project", "shortcut": "" },
|
{ "id": "new-project", "label": "Studio: New Project", "shortcut": "" },
|
||||||
{ "id": "compile-manuscript", "label": "Studio: Compile Manuscript", "shortcut": "Ctrl+Alt+E" },
|
{
|
||||||
{ "id": "proofread-document", "label": "Studio: Proofread Document", "shortcut": "Ctrl+Alt+G" }
|
"id": "compile-manuscript",
|
||||||
|
"label": "Studio: Compile Manuscript",
|
||||||
|
"shortcut": "Ctrl+Alt+E"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "proofread-document",
|
||||||
|
"label": "Studio: Proofread Document",
|
||||||
|
"shortcut": "Ctrl+Alt+G"
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"statusBar": { "indicators": ["sprint-timer", "word-goal"] }
|
"statusBar": { "indicators": ["sprint-timer", "word-goal"] }
|
||||||
},
|
},
|
||||||
"settings": [
|
"settings": [
|
||||||
{ "key": "dailyGoal", "type": "number", "default": 1000, "label": "Daily word goal" },
|
{ "key": "dailyGoal", "type": "number", "default": 1000, "label": "Daily word goal" },
|
||||||
{ "key": "sprintDuration", "type": "number", "default": 25, "label": "Sprint duration (min)" },
|
{ "key": "sprintDuration", "type": "number", "default": 25, "label": "Sprint duration (min)" },
|
||||||
{ "key": "autoSnapshotInterval", "type": "number", "default": 0, "label": "Auto-snapshot interval (min, 0=off)" },
|
{
|
||||||
|
"key": "autoSnapshotInterval",
|
||||||
|
"type": "number",
|
||||||
|
"default": 0,
|
||||||
|
"label": "Auto-snapshot interval (min, 0=off)"
|
||||||
|
},
|
||||||
{ "key": "maxSnapshots", "type": "number", "default": 50, "label": "Max snapshots to keep" }
|
{ "key": "maxSnapshots", "type": "number", "default": 50, "label": "Max snapshots to keep" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ function renderGoalsPanel(container, { engines, settings }) {
|
|||||||
const row = document.createElement('div');
|
const row = document.createElement('div');
|
||||||
row.className = 'ws-stat-row';
|
row.className = 'ws-stat-row';
|
||||||
const label = document.createElement('span');
|
const label = document.createElement('span');
|
||||||
label.textContent = progress.written.toLocaleString() + ' / ' + dailyGoal.toLocaleString() + ' words';
|
label.textContent =
|
||||||
|
progress.written.toLocaleString() + ' / ' + dailyGoal.toLocaleString() + ' words';
|
||||||
const pct = document.createElement('span');
|
const pct = document.createElement('span');
|
||||||
pct.className = 'ws-pct';
|
pct.className = 'ws-pct';
|
||||||
pct.textContent = progress.pct + '%';
|
pct.textContent = progress.pct + '%';
|
||||||
@@ -81,7 +82,7 @@ function renderGoalsPanel(container, { engines, settings }) {
|
|||||||
|
|
||||||
const chart = document.createElement('div');
|
const chart = document.createElement('div');
|
||||||
chart.className = 'ws-chart';
|
chart.className = 'ws-chart';
|
||||||
const maxWords = Math.max(...last30.map(d => d.words), 1);
|
const maxWords = Math.max(...last30.map((d) => d.words), 1);
|
||||||
for (const day of last30) {
|
for (const day of last30) {
|
||||||
const barEl = document.createElement('div');
|
const barEl = document.createElement('div');
|
||||||
const height = Math.max(2, (day.words / maxWords) * 60);
|
const height = Math.max(2, (day.words / maxWords) * 60);
|
||||||
|
|||||||
@@ -69,7 +69,8 @@ function renderManuscriptPanel(container, { engines, editor, settings }) {
|
|||||||
const row = document.createElement('div');
|
const row = document.createElement('div');
|
||||||
row.className = 'ws-stat-row';
|
row.className = 'ws-stat-row';
|
||||||
const label = document.createElement('span');
|
const label = document.createElement('span');
|
||||||
label.textContent = stats.totalWords.toLocaleString() + ' / ' + stats.targetWords.toLocaleString() + ' words';
|
label.textContent =
|
||||||
|
stats.totalWords.toLocaleString() + ' / ' + stats.targetWords.toLocaleString() + ' words';
|
||||||
const pct = document.createElement('span');
|
const pct = document.createElement('span');
|
||||||
pct.className = 'ws-pct';
|
pct.className = 'ws-pct';
|
||||||
pct.textContent = stats.pctComplete + '%';
|
pct.textContent = stats.pctComplete + '%';
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ function renderProofreadPanel(container, { events, editor }) {
|
|||||||
if (result && result.issues) {
|
if (result && result.issues) {
|
||||||
renderIssues(issuesList, result.issues);
|
renderIssues(issuesList, result.issues);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -80,7 +80,10 @@ function renderIssues(container, issues) {
|
|||||||
|
|
||||||
const actions = document.createElement('div');
|
const actions = document.createElement('div');
|
||||||
actions.className = 'ws-issue-actions';
|
actions.className = 'ws-issue-actions';
|
||||||
for (const [action, label] of [['accept', 'Accept'], ['dismiss', 'Dismiss']]) {
|
for (const [action, label] of [
|
||||||
|
['accept', 'Accept'],
|
||||||
|
['dismiss', 'Dismiss'],
|
||||||
|
]) {
|
||||||
const actionBtn = document.createElement('button');
|
const actionBtn = document.createElement('button');
|
||||||
actionBtn.className = 'ws-btn ws-btn-sm';
|
actionBtn.className = 'ws-btn ws-btn-sm';
|
||||||
actionBtn.textContent = label;
|
actionBtn.textContent = label;
|
||||||
|
|||||||
@@ -45,7 +45,11 @@ function renderSnapshotsPanel(container, { engines, editor }) {
|
|||||||
|
|
||||||
const actions = document.createElement('div');
|
const actions = document.createElement('div');
|
||||||
actions.className = 'ws-snapshot-actions';
|
actions.className = 'ws-snapshot-actions';
|
||||||
for (const [action, text, cls] of [['restore', 'Restore', ''], ['diff', 'Diff', ''], ['delete', 'Delete', 'ws-btn-danger']]) {
|
for (const [action, text, cls] of [
|
||||||
|
['restore', 'Restore', ''],
|
||||||
|
['diff', 'Diff', ''],
|
||||||
|
['delete', 'Delete', 'ws-btn-danger'],
|
||||||
|
]) {
|
||||||
const actionBtn = document.createElement('button');
|
const actionBtn = document.createElement('button');
|
||||||
actionBtn.className = 'ws-btn ws-btn-sm' + (cls ? ' ' + cls : '');
|
actionBtn.className = 'ws-btn ws-btn-sm' + (cls ? ' ' + cls : '');
|
||||||
actionBtn.textContent = text;
|
actionBtn.textContent = text;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ class ProjectManager {
|
|||||||
type: opts.type || 'manuscript',
|
type: opts.type || 'manuscript',
|
||||||
target: { words: opts.targetWords || 0, deadline: opts.deadline || null },
|
target: { words: opts.targetWords || 0, deadline: opts.deadline || null },
|
||||||
chapters: [],
|
chapters: [],
|
||||||
metadata: opts.metadata || {}
|
metadata: opts.metadata || {},
|
||||||
};
|
};
|
||||||
this.fs.writeFile(dir + '/.project.json', JSON.stringify(project, null, 2));
|
this.fs.writeFile(dir + '/.project.json', JSON.stringify(project, null, 2));
|
||||||
return project;
|
return project;
|
||||||
@@ -66,7 +66,7 @@ class ProjectManager {
|
|||||||
totalWords,
|
totalWords,
|
||||||
chapterCount: project.chapters.length,
|
chapterCount: project.chapters.length,
|
||||||
targetWords: target,
|
targetWords: target,
|
||||||
pctComplete: target > 0 ? Math.min(100, Math.round((totalWords / target) * 100)) : 0
|
pctComplete: target > 0 ? Math.min(100, Math.round((totalWords / target) * 100)) : 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class SnapshotManager {
|
|||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
content,
|
content,
|
||||||
wordCount: content.split(/\s+/).filter(Boolean).length,
|
wordCount: content.split(/\s+/).filter(Boolean).length,
|
||||||
label
|
label,
|
||||||
};
|
};
|
||||||
snaps.unshift(snap);
|
snaps.unshift(snap);
|
||||||
this._saveAll(snaps);
|
this._saveAll(snaps);
|
||||||
@@ -36,7 +36,7 @@ class SnapshotManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getById(id) {
|
getById(id) {
|
||||||
return this._getAll().find(s => s.id === id) || null;
|
return this._getAll().find((s) => s.id === id) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
restore(id) {
|
restore(id) {
|
||||||
@@ -46,7 +46,7 @@ class SnapshotManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
delete(id) {
|
delete(id) {
|
||||||
const snaps = this._getAll().filter(s => s.id !== id);
|
const snaps = this._getAll().filter((s) => s.id !== id);
|
||||||
this._saveAll(snaps);
|
this._saveAll(snaps);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,8 +59,12 @@ class SnapshotManager {
|
|||||||
const newSet = new Set(newLines);
|
const newSet = new Set(newLines);
|
||||||
let added = 0;
|
let added = 0;
|
||||||
let removed = 0;
|
let removed = 0;
|
||||||
for (const line of newLines) { if (!oldSet.has(line)) added++; }
|
for (const line of newLines) {
|
||||||
for (const line of oldLines) { if (!newSet.has(line)) removed++; }
|
if (!oldSet.has(line)) added++;
|
||||||
|
}
|
||||||
|
for (const line of oldLines) {
|
||||||
|
if (!newSet.has(line)) removed++;
|
||||||
|
}
|
||||||
return { added, removed };
|
return { added, removed };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,9 @@ class SprintEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
isActive() { return this._active; }
|
isActive() {
|
||||||
|
return this._active;
|
||||||
|
}
|
||||||
|
|
||||||
getRemaining() {
|
getRemaining() {
|
||||||
if (!this._active) return 0;
|
if (!this._active) return 0;
|
||||||
|
|||||||
@@ -12,10 +12,11 @@ class PluginContext {
|
|||||||
* @param {object} deps.exportHooks - { preHooks: [], postHooks: [] }
|
* @param {object} deps.exportHooks - { preHooks: [], postHooks: [] }
|
||||||
*/
|
*/
|
||||||
constructor(deps) {
|
constructor(deps) {
|
||||||
const { pluginId, sidebar, commands, statusBar, eventBus, settings, editor, ipc, exportHooks } = deps;
|
const { pluginId, sidebar, commands, statusBar, eventBus, settings, editor, ipc, exportHooks } =
|
||||||
|
deps;
|
||||||
|
|
||||||
this.sidebar = {
|
this.sidebar = {
|
||||||
registerPanel: (id, opts) => sidebar.registerPanel(`${pluginId}:${id}`, opts)
|
registerPanel: (id, opts) => sidebar.registerPanel(`${pluginId}:${id}`, opts),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.commands = {
|
this.commands = {
|
||||||
@@ -28,41 +29,45 @@ class PluginContext {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
commands.register(`${pluginId}:${id}`, label, safeHandler, shortcut);
|
commands.register(`${pluginId}:${id}`, label, safeHandler, shortcut);
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
this.statusBar = {
|
this.statusBar = {
|
||||||
registerIndicator: (id, opts) => statusBar.registerIndicator(`${pluginId}:${id}`, opts)
|
registerIndicator: (id, opts) => statusBar.registerIndicator(`${pluginId}:${id}`, opts),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.settings = {
|
this.settings = {
|
||||||
get: (key) => settings.get(`plugins.${pluginId}.${key}`),
|
get: (key) => settings.get(`plugins.${pluginId}.${key}`),
|
||||||
set: (key, value) => settings.set(`plugins.${pluginId}.${key}`, value),
|
set: (key, value) => settings.set(`plugins.${pluginId}.${key}`, value),
|
||||||
onChanged: (key, cb) => settings.onChanged(`plugins.${pluginId}.${key}`, cb)
|
onChanged: (key, cb) => settings.onChanged(`plugins.${pluginId}.${key}`, cb),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.editor = {
|
this.editor = {
|
||||||
getContent: () => editor.getContent(),
|
getContent: () => editor.getContent(),
|
||||||
getSelection: () => editor.getSelection(),
|
getSelection: () => editor.getSelection(),
|
||||||
insertAtCursor: (text) => editor.insertAtCursor(text),
|
insertAtCursor: (text) => editor.insertAtCursor(text),
|
||||||
onContentChanged: (cb) => editor.onContentChanged(cb)
|
onContentChanged: (cb) => editor.onContentChanged(cb),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.events = {
|
this.events = {
|
||||||
on: (event, handler) => eventBus.on(event, handler),
|
on: (event, handler) => eventBus.on(event, handler),
|
||||||
off: (event, handler) => eventBus.off(event, handler),
|
off: (event, handler) => eventBus.off(event, handler),
|
||||||
emit: (event, payload) => eventBus.emit(event, payload),
|
emit: (event, payload) => eventBus.emit(event, payload),
|
||||||
hasHandler: (event) => eventBus.hasHandler(event)
|
hasHandler: (event) => eventBus.hasHandler(event),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.ipc = {
|
this.ipc = {
|
||||||
invoke: (channel, ...args) => ipc.invoke(channel, ...args),
|
invoke: (channel, ...args) => ipc.invoke(channel, ...args),
|
||||||
on: (channel, handler) => ipc.on(channel, handler)
|
on: (channel, handler) => ipc.on(channel, handler),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.exports = {
|
this.exports = {
|
||||||
registerPreHook: (handler) => { if (exportHooks) exportHooks.preHooks.push(handler); },
|
registerPreHook: (handler) => {
|
||||||
registerPostHook: (handler) => { if (exportHooks) exportHooks.postHooks.push(handler); }
|
if (exportHooks) exportHooks.preHooks.push(handler);
|
||||||
|
},
|
||||||
|
registerPostHook: (handler) => {
|
||||||
|
if (exportHooks) exportHooks.postHooks.push(handler);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,10 @@ class PluginLoader {
|
|||||||
const loaded = require(indexPath);
|
const loaded = require(indexPath);
|
||||||
PluginClass = loaded.Plugin || loaded.default || null;
|
PluginClass = loaded.Plugin || loaded.default || null;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`[PluginLoader] Failed to load index.js for "${manifest.id}":`, err.message);
|
console.error(
|
||||||
|
`[PluginLoader] Failed to load index.js for "${manifest.id}":`,
|
||||||
|
err.message
|
||||||
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -46,7 +49,7 @@ class PluginLoader {
|
|||||||
description: manifest.description,
|
description: manifest.description,
|
||||||
manifest,
|
manifest,
|
||||||
PluginClass,
|
PluginClass,
|
||||||
dir: pluginDir
|
dir: pluginDir,
|
||||||
});
|
});
|
||||||
this.loadedIds.add(manifest.id);
|
this.loadedIds.add(manifest.id);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class PluginRegistry {
|
|||||||
settings: this.deps.settings,
|
settings: this.deps.settings,
|
||||||
editor: this.deps.editor,
|
editor: this.deps.editor,
|
||||||
ipc: this.deps.ipc,
|
ipc: this.deps.ipc,
|
||||||
exportHooks: this.exportHooks
|
exportHooks: this.exportHooks,
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
+22
-15
@@ -150,7 +150,7 @@ const ALLOWED_SEND_CHANNELS = [
|
|||||||
|
|
||||||
// Plugin settings
|
// Plugin settings
|
||||||
'plugin-settings:get',
|
'plugin-settings:get',
|
||||||
'plugin-settings:set'
|
'plugin-settings:set',
|
||||||
];
|
];
|
||||||
|
|
||||||
const ALLOWED_RECEIVE_CHANNELS = [
|
const ALLOWED_RECEIVE_CHANNELS = [
|
||||||
@@ -251,7 +251,7 @@ const ALLOWED_RECEIVE_CHANNELS = [
|
|||||||
// File dialog / directory listing
|
// File dialog / directory listing
|
||||||
'list-directory',
|
'list-directory',
|
||||||
'pick-folder',
|
'pick-folder',
|
||||||
'pick-file'
|
'pick-file',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -361,12 +361,12 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
move: (source, destination) => ipcRenderer.invoke('move-path', { source, destination }),
|
move: (source, destination) => ipcRenderer.invoke('move-path', { source, destination }),
|
||||||
list: (dirPath) => ipcRenderer.invoke('list-directory', dirPath),
|
list: (dirPath) => ipcRenderer.invoke('list-directory', dirPath),
|
||||||
pickFolder: () => ipcRenderer.invoke('pick-folder'),
|
pickFolder: () => ipcRenderer.invoke('pick-folder'),
|
||||||
pickFile: () => ipcRenderer.invoke('pick-file')
|
pickFile: () => ipcRenderer.invoke('pick-file'),
|
||||||
},
|
},
|
||||||
|
|
||||||
// Theme Operations
|
// Theme Operations
|
||||||
theme: {
|
theme: {
|
||||||
get: () => ipcRenderer.send('get-theme')
|
get: () => ipcRenderer.send('get-theme'),
|
||||||
},
|
},
|
||||||
|
|
||||||
// Print Operations
|
// Print Operations
|
||||||
@@ -378,7 +378,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
// Export Operations
|
// Export Operations
|
||||||
export: {
|
export: {
|
||||||
withOptions: (format, options) => ipcRenderer.send('export-with-options', { format, options }),
|
withOptions: (format, options) => ipcRenderer.send('export-with-options', { format, options }),
|
||||||
spreadsheet: (content, format) => ipcRenderer.send('export-spreadsheet', { content, format })
|
spreadsheet: (content, format) => ipcRenderer.send('export-spreadsheet', { content, format }),
|
||||||
},
|
},
|
||||||
|
|
||||||
// Batch Conversion
|
// Batch Conversion
|
||||||
@@ -386,7 +386,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
convert: (inputFolder, outputFolder, format, options) => {
|
convert: (inputFolder, outputFolder, format, options) => {
|
||||||
ipcRenderer.send('batch-convert', { inputFolder, outputFolder, format, options });
|
ipcRenderer.send('batch-convert', { inputFolder, outputFolder, format, options });
|
||||||
},
|
},
|
||||||
selectFolder: (type) => ipcRenderer.send('select-folder', type)
|
selectFolder: (type) => ipcRenderer.send('select-folder', type),
|
||||||
},
|
},
|
||||||
|
|
||||||
// Universal Converter
|
// Universal Converter
|
||||||
@@ -395,8 +395,14 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
ipcRenderer.send('universal-convert', { tool, fromFormat, toFormat, filePath });
|
ipcRenderer.send('universal-convert', { tool, fromFormat, toFormat, filePath });
|
||||||
},
|
},
|
||||||
convertBatch: (tool, fromFormat, toFormat, inputFolder, outputFolder) => {
|
convertBatch: (tool, fromFormat, toFormat, inputFolder, outputFolder) => {
|
||||||
ipcRenderer.send('universal-convert-batch', { tool, fromFormat, toFormat, inputFolder, outputFolder });
|
ipcRenderer.send('universal-convert-batch', {
|
||||||
}
|
tool,
|
||||||
|
fromFormat,
|
||||||
|
toFormat,
|
||||||
|
inputFolder,
|
||||||
|
outputFolder,
|
||||||
|
});
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// Header/Footer Operations
|
// Header/Footer Operations
|
||||||
@@ -404,22 +410,23 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
getSettings: () => ipcRenderer.send('get-header-footer-settings'),
|
getSettings: () => ipcRenderer.send('get-header-footer-settings'),
|
||||||
saveSettings: (settings) => ipcRenderer.send('save-header-footer-settings', settings),
|
saveSettings: (settings) => ipcRenderer.send('save-header-footer-settings', settings),
|
||||||
browseLogo: (position) => ipcRenderer.send('browse-header-footer-logo', position),
|
browseLogo: (position) => ipcRenderer.send('browse-header-footer-logo', position),
|
||||||
saveLogo: (position, filePath) => ipcRenderer.send('save-header-footer-logo', { position, filePath }),
|
saveLogo: (position, filePath) =>
|
||||||
clearLogo: (position) => ipcRenderer.send('clear-header-footer-logo', position)
|
ipcRenderer.send('save-header-footer-logo', { position, filePath }),
|
||||||
|
clearLogo: (position) => ipcRenderer.send('clear-header-footer-logo', position),
|
||||||
},
|
},
|
||||||
|
|
||||||
// Page Settings
|
// Page Settings
|
||||||
page: {
|
page: {
|
||||||
getSettings: () => ipcRenderer.send('get-page-settings'),
|
getSettings: () => ipcRenderer.send('get-page-settings'),
|
||||||
updateSettings: (settings) => ipcRenderer.send('update-page-settings', settings),
|
updateSettings: (settings) => ipcRenderer.send('update-page-settings', settings),
|
||||||
setCustomStartPage: (pageNumber) => ipcRenderer.send('set-custom-start-page', pageNumber)
|
setCustomStartPage: (pageNumber) => ipcRenderer.send('set-custom-start-page', pageNumber),
|
||||||
},
|
},
|
||||||
|
|
||||||
// PDF Operations
|
// PDF Operations
|
||||||
pdf: {
|
pdf: {
|
||||||
processOperation: (data) => ipcRenderer.send('process-pdf-operation', data),
|
processOperation: (data) => ipcRenderer.send('process-pdf-operation', data),
|
||||||
getPageCount: (filePath) => ipcRenderer.send('get-pdf-page-count', filePath),
|
getPageCount: (filePath) => ipcRenderer.send('get-pdf-page-count', filePath),
|
||||||
selectFolder: (inputId) => ipcRenderer.send('select-pdf-folder', inputId)
|
selectFolder: (inputId) => ipcRenderer.send('select-pdf-folder', inputId),
|
||||||
},
|
},
|
||||||
|
|
||||||
// Image Converter Operations
|
// Image Converter Operations
|
||||||
@@ -428,7 +435,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
batchConvert: (data) => ipcRenderer.send('image-batch-convert', data),
|
batchConvert: (data) => ipcRenderer.send('image-batch-convert', data),
|
||||||
resize: (data) => ipcRenderer.send('image-resize', data),
|
resize: (data) => ipcRenderer.send('image-resize', data),
|
||||||
compress: (data) => ipcRenderer.send('image-compress', data),
|
compress: (data) => ipcRenderer.send('image-compress', data),
|
||||||
rotate: (data) => ipcRenderer.send('image-rotate', data)
|
rotate: (data) => ipcRenderer.send('image-rotate', data),
|
||||||
},
|
},
|
||||||
|
|
||||||
// Audio Converter Operations
|
// Audio Converter Operations
|
||||||
@@ -437,7 +444,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
batchConvert: (data) => ipcRenderer.send('audio-batch-convert', data),
|
batchConvert: (data) => ipcRenderer.send('audio-batch-convert', data),
|
||||||
extract: (data) => ipcRenderer.send('audio-extract', data),
|
extract: (data) => ipcRenderer.send('audio-extract', data),
|
||||||
trim: (data) => ipcRenderer.send('audio-trim', data),
|
trim: (data) => ipcRenderer.send('audio-trim', data),
|
||||||
merge: (data) => ipcRenderer.send('audio-merge', data)
|
merge: (data) => ipcRenderer.send('audio-merge', data),
|
||||||
},
|
},
|
||||||
|
|
||||||
// Video Converter Operations
|
// Video Converter Operations
|
||||||
@@ -447,7 +454,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
compress: (data) => ipcRenderer.send('video-compress', data),
|
compress: (data) => ipcRenderer.send('video-compress', data),
|
||||||
trim: (data) => ipcRenderer.send('video-trim', data),
|
trim: (data) => ipcRenderer.send('video-trim', data),
|
||||||
extractFrames: (data) => ipcRenderer.send('video-frames', data),
|
extractFrames: (data) => ipcRenderer.send('video-frames', data),
|
||||||
toGif: (data) => ipcRenderer.send('video-gif', data)
|
toGif: (data) => ipcRenderer.send('video-gif', data),
|
||||||
},
|
},
|
||||||
|
|
||||||
getAppVersion: () => ipcRenderer.invoke('get-app-version'),
|
getAppVersion: () => ipcRenderer.invoke('get-app-version'),
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import { useSettingsStore } from '@/stores/settings-store';
|
|||||||
const TEMPLATES = {
|
const TEMPLATES = {
|
||||||
blank: '',
|
blank: '',
|
||||||
readme: '# Project\n\nDescription.\n\n## Usage\n\n```\nnpm install\n```\n',
|
readme: '# Project\n\nDescription.\n\n## Usage\n\n```\nnpm install\n```\n',
|
||||||
meeting: '# Meeting Notes — YYYY-MM-DD\n\n## Attendees\n\n- \n## Agenda\n\n1. \n\n## Action items\n\n- [ ] \n',
|
meeting:
|
||||||
|
'# Meeting Notes — YYYY-MM-DD\n\n## Attendees\n\n- \n## Agenda\n\n1. \n\n## Action items\n\n- [ ] \n',
|
||||||
blog: '# Title\n\n*Subtitle*\n\nLorem ipsum.\n\n---\n\n## Section 1\n',
|
blog: '# Title\n\n*Subtitle*\n\nLorem ipsum.\n\n---\n\n## Section 1\n',
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -23,7 +24,12 @@ export function FirstRunWizard() {
|
|||||||
const close = () => setFirstRun(false);
|
const close = () => setFirstRun(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-testid="first-run-wizard" role="dialog" aria-modal="true" className="fixed inset-0 z-50 bg-black/40 flex items-center justify-center">
|
<div
|
||||||
|
data-testid="first-run-wizard"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
className="fixed inset-0 z-50 bg-black/40 flex items-center justify-center"
|
||||||
|
>
|
||||||
<div className="bg-white dark:bg-neutral-900 rounded-lg p-6 w-[28rem] shadow-xl">
|
<div className="bg-white dark:bg-neutral-900 rounded-lg p-6 w-[28rem] shadow-xl">
|
||||||
{step === 0 && (
|
{step === 0 && (
|
||||||
<div>
|
<div>
|
||||||
@@ -31,7 +37,12 @@ export function FirstRunWizard() {
|
|||||||
<div className="flex gap-2 mb-4">
|
<div className="flex gap-2 mb-4">
|
||||||
{(['light', 'dark', 'system'] as const).map((t) => (
|
{(['light', 'dark', 'system'] as const).map((t) => (
|
||||||
<label key={t} className="flex items-center gap-1">
|
<label key={t} className="flex items-center gap-1">
|
||||||
<input type="radio" name="theme" checked={theme === t} onChange={() => setSetting('theme', t)} />
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="theme"
|
||||||
|
checked={theme === t}
|
||||||
|
onChange={() => setSetting('theme', t)}
|
||||||
|
/>
|
||||||
{t}
|
{t}
|
||||||
</label>
|
</label>
|
||||||
))}
|
))}
|
||||||
@@ -43,11 +54,21 @@ export function FirstRunWizard() {
|
|||||||
<h2 className="text-lg font-semibold mb-2">Update channel</h2>
|
<h2 className="text-lg font-semibold mb-2">Update channel</h2>
|
||||||
<div className="flex flex-col gap-2 mb-4">
|
<div className="flex flex-col gap-2 mb-4">
|
||||||
<label className="flex items-center gap-2">
|
<label className="flex items-center gap-2">
|
||||||
<input type="radio" name="channel" checked={updateChannel === 'github'} onChange={() => setSetting('updateChannel', 'github')} />
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="channel"
|
||||||
|
checked={updateChannel === 'github'}
|
||||||
|
onChange={() => setSetting('updateChannel', 'github')}
|
||||||
|
/>
|
||||||
GitHub Releases (public)
|
GitHub Releases (public)
|
||||||
</label>
|
</label>
|
||||||
<label className="flex items-center gap-2">
|
<label className="flex items-center gap-2">
|
||||||
<input type="radio" name="channel" checked={updateChannel === 'concreteinfo'} onChange={() => setSetting('updateChannel', 'concreteinfo')} />
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="channel"
|
||||||
|
checked={updateChannel === 'concreteinfo'}
|
||||||
|
onChange={() => setSetting('updateChannel', 'concreteinfo')}
|
||||||
|
/>
|
||||||
ConcreteInfo self-hosted
|
ConcreteInfo self-hosted
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -56,7 +77,11 @@ export function FirstRunWizard() {
|
|||||||
{step === 2 && (
|
{step === 2 && (
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg font-semibold mb-2">Starter template</h2>
|
<h2 className="text-lg font-semibold mb-2">Starter template</h2>
|
||||||
<select value={template} onChange={(e) => setTemplate(e.target.value as any)} className="border rounded px-2 py-1 w-full mb-4">
|
<select
|
||||||
|
value={template}
|
||||||
|
onChange={(e) => setTemplate(e.target.value as any)}
|
||||||
|
className="border rounded px-2 py-1 w-full mb-4"
|
||||||
|
>
|
||||||
<option value="blank">Blank</option>
|
<option value="blank">Blank</option>
|
||||||
<option value="readme">README</option>
|
<option value="readme">README</option>
|
||||||
<option value="meeting">Meeting notes</option>
|
<option value="meeting">Meeting notes</option>
|
||||||
@@ -65,13 +90,28 @@ export function FirstRunWizard() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex justify-between items-center mt-4">
|
<div className="flex justify-between items-center mt-4">
|
||||||
<button onClick={close} className="text-sm text-neutral-500">Skip</button>
|
<button onClick={close} className="text-sm text-neutral-500">
|
||||||
|
Skip
|
||||||
|
</button>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{step > 0 && <button onClick={() => setStep(step - 1)}>Back</button>}
|
{step > 0 && <button onClick={() => setStep(step - 1)}>Back</button>}
|
||||||
{step < 2 ? (
|
{step < 2 ? (
|
||||||
<button onClick={() => setStep(step + 1)} className="px-3 py-1 rounded bg-brand text-white">Next</button>
|
<button
|
||||||
|
onClick={() => setStep(step + 1)}
|
||||||
|
className="px-3 py-1 rounded bg-brand text-white"
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<button onClick={() => { useAppStore.getState().newBuffer(TEMPLATES[template]); close(); }} className="px-3 py-1 rounded bg-brand text-white">Done</button>
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
useAppStore.getState().newBuffer(TEMPLATES[template]);
|
||||||
|
close();
|
||||||
|
}}
|
||||||
|
className="px-3 py-1 rounded bg-brand text-white"
|
||||||
|
>
|
||||||
|
Done
|
||||||
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,9 +9,22 @@ export function UpdateBanner() {
|
|||||||
|
|
||||||
if (state === 'error') {
|
if (state === 'error') {
|
||||||
return (
|
return (
|
||||||
<div data-testid="update-banner" role="status" className="bg-amber-50 border-b border-amber-200 px-4 py-2 text-sm">
|
<div
|
||||||
|
data-testid="update-banner"
|
||||||
|
role="status"
|
||||||
|
className="bg-amber-50 border-b border-amber-200 px-4 py-2 text-sm"
|
||||||
|
>
|
||||||
Couldn't check for updates.{' '}
|
Couldn't check for updates.{' '}
|
||||||
<button onClick={async () => { try { await check(); } catch (e: any) { toast.error(e.message); } }} className="underline">
|
<button
|
||||||
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
await check();
|
||||||
|
} catch (e: any) {
|
||||||
|
toast.error(e.message);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="underline"
|
||||||
|
>
|
||||||
Try again
|
Try again
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -20,7 +33,10 @@ export function UpdateBanner() {
|
|||||||
|
|
||||||
if (state === 'available') {
|
if (state === 'available') {
|
||||||
return (
|
return (
|
||||||
<div data-testid="update-banner" className="bg-blue-50 border-b border-blue-200 px-4 py-2 text-sm">
|
<div
|
||||||
|
data-testid="update-banner"
|
||||||
|
className="bg-blue-50 border-b border-blue-200 px-4 py-2 text-sm"
|
||||||
|
>
|
||||||
A new version (v{version}) is available.{' '}
|
A new version (v{version}) is available.{' '}
|
||||||
<button onClick={() => useUpdaterStore.getState().check()} className="underline">
|
<button onClick={() => useUpdaterStore.getState().check()} className="underline">
|
||||||
Download now
|
Download now
|
||||||
@@ -31,7 +47,10 @@ export function UpdateBanner() {
|
|||||||
|
|
||||||
if (state === 'downloading') {
|
if (state === 'downloading') {
|
||||||
return (
|
return (
|
||||||
<div data-testid="update-banner" className="bg-blue-50 border-b border-blue-200 px-4 py-2 text-sm">
|
<div
|
||||||
|
data-testid="update-banner"
|
||||||
|
className="bg-blue-50 border-b border-blue-200 px-4 py-2 text-sm"
|
||||||
|
>
|
||||||
Downloading update… {Math.round(percent)}%
|
Downloading update… {Math.round(percent)}%
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -39,9 +58,19 @@ export function UpdateBanner() {
|
|||||||
|
|
||||||
if (state === 'ready') {
|
if (state === 'ready') {
|
||||||
return (
|
return (
|
||||||
<div data-testid="update-banner" className="bg-green-50 border-b border-green-200 px-4 py-2 text-sm flex items-center gap-3">
|
<div
|
||||||
|
data-testid="update-banner"
|
||||||
|
className="bg-green-50 border-b border-green-200 px-4 py-2 text-sm flex items-center gap-3"
|
||||||
|
>
|
||||||
<span>v{version} is ready.</span>
|
<span>v{version} is ready.</span>
|
||||||
<button onClick={() => ipc.app.openExternal(`https://github.com/amitwh/markdown-converter/releases/tag/v${version}`)} className="underline">
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
ipc.app.openExternal(
|
||||||
|
`https://github.com/amitwh/markdown-converter/releases/tag/v${version}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className="underline"
|
||||||
|
>
|
||||||
View release notes
|
View release notes
|
||||||
</button>
|
</button>
|
||||||
<button onClick={install} className="px-3 py-1 rounded bg-brand text-white">
|
<button onClick={install} className="px-3 py-1 rounded bg-brand text-white">
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { EditorState, Compartment } from '@codemirror/state';
|
import { EditorState, Compartment } from '@codemirror/state';
|
||||||
import { EditorView, keymap, lineNumbers, highlightActiveLine, drawSelection } from '@codemirror/view';
|
import {
|
||||||
|
EditorView,
|
||||||
|
keymap,
|
||||||
|
lineNumbers,
|
||||||
|
highlightActiveLine,
|
||||||
|
drawSelection,
|
||||||
|
} from '@codemirror/view';
|
||||||
import { defaultKeymap, history, historyKeymap, indentWithTab } from '@codemirror/commands';
|
import { defaultKeymap, history, historyKeymap, indentWithTab } from '@codemirror/commands';
|
||||||
import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
|
import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
|
||||||
import { searchKeymap, highlightSelectionMatches } from '@codemirror/search';
|
import { searchKeymap, highlightSelectionMatches } from '@codemirror/search';
|
||||||
@@ -46,8 +52,16 @@ export function CodeMirrorEditor({ bufferId, initialContent, onChange, onCursorC
|
|||||||
drawSelection(),
|
drawSelection(),
|
||||||
markdown({ base: markdownLanguage, codeLanguages: [] }),
|
markdown({ base: markdownLanguage, codeLanguages: [] }),
|
||||||
autocompletion(),
|
autocompletion(),
|
||||||
keymap.of([...defaultKeymap, ...historyKeymap, ...searchKeymap, ...completionKeymap, indentWithTab]),
|
keymap.of([
|
||||||
themeCompartment.current.of(resolvedTheme === 'dark' ? [oneDark] : [lightTheme, lightHighlight]),
|
...defaultKeymap,
|
||||||
|
...historyKeymap,
|
||||||
|
...searchKeymap,
|
||||||
|
...completionKeymap,
|
||||||
|
indentWithTab,
|
||||||
|
]),
|
||||||
|
themeCompartment.current.of(
|
||||||
|
resolvedTheme === 'dark' ? [oneDark] : [lightTheme, lightHighlight]
|
||||||
|
),
|
||||||
EditorView.lineWrapping,
|
EditorView.lineWrapping,
|
||||||
EditorView.updateListener.of((v) => {
|
EditorView.updateListener.of((v) => {
|
||||||
if (v.docChanged) {
|
if (v.docChanged) {
|
||||||
@@ -73,7 +87,7 @@ export function CodeMirrorEditor({ bufferId, initialContent, onChange, onCursorC
|
|||||||
const denom = el.scrollHeight - el.clientHeight;
|
const denom = el.scrollHeight - el.clientHeight;
|
||||||
setScrollRatio(denom > 0 ? el.scrollTop / denom : 0);
|
setScrollRatio(denom > 0 ? el.scrollTop / denom : 0);
|
||||||
setVisibleRatio(
|
setVisibleRatio(
|
||||||
el.clientHeight > 0 ? Math.min(1, el.clientHeight / el.scrollHeight) : 1,
|
el.clientHeight > 0 ? Math.min(1, el.clientHeight / el.scrollHeight) : 1
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
@@ -104,7 +118,9 @@ export function CodeMirrorEditor({ bufferId, initialContent, onChange, onCursorC
|
|||||||
return (
|
return (
|
||||||
<div className="relative h-full overflow-hidden">
|
<div className="relative h-full overflow-hidden">
|
||||||
<div ref={ref} className="h-full overflow-hidden" />
|
<div ref={ref} className="h-full overflow-hidden" />
|
||||||
{minimap && <Minimap content={initialContent} scrollRatio={scrollRatio} visibleRatio={visibleRatio} />}
|
{minimap && (
|
||||||
|
<Minimap content={initialContent} scrollRatio={scrollRatio} visibleRatio={visibleRatio} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,11 +22,7 @@ export function EditorPane() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full">
|
<div className="h-full">
|
||||||
<CodeMirrorEditor
|
<CodeMirrorEditor key={buf.id} bufferId={buf.id} initialContent={buf.content} />
|
||||||
key={buf.id}
|
|
||||||
bufferId={buf.id}
|
|
||||||
initialContent={buf.content}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -10,7 +10,10 @@ import { useAppStore } from '@/stores/app-store';
|
|||||||
import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from '@/components/ui/resizable';
|
import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from '@/components/ui/resizable';
|
||||||
import { useFileShortcuts } from '@/hooks/use-file-shortcuts';
|
import { useFileShortcuts } from '@/hooks/use-file-shortcuts';
|
||||||
import { useRestoreLastFolder } from '@/hooks/use-restore-last-folder';
|
import { useRestoreLastFolder } from '@/hooks/use-restore-last-folder';
|
||||||
import { useRegisterMenuCommands, useBridgeNativeMenu } from '@/lib/commands/register-menu-commands';
|
import {
|
||||||
|
useRegisterMenuCommands,
|
||||||
|
useBridgeNativeMenu,
|
||||||
|
} from '@/lib/commands/register-menu-commands';
|
||||||
import { useZenMode } from '@/hooks/use-zen-mode';
|
import { useZenMode } from '@/hooks/use-zen-mode';
|
||||||
|
|
||||||
export function AppShell() {
|
export function AppShell() {
|
||||||
@@ -27,7 +30,9 @@ export function AppShell() {
|
|||||||
<main className="h-screen w-screen overflow-hidden bg-background">
|
<main className="h-screen w-screen overflow-hidden bg-background">
|
||||||
<ResizablePanelGroup
|
<ResizablePanelGroup
|
||||||
direction="horizontal"
|
direction="horizontal"
|
||||||
onLayoutChange={(sizes) => setPaneSizes({ sidebar: 0, editor: sizes[0], preview: sizes[1] })}
|
onLayoutChange={(sizes) =>
|
||||||
|
setPaneSizes({ sidebar: 0, editor: sizes[0], preview: sizes[1] })
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<ResizablePanel defaultSize={previewVisible ? 50 : 100} minSize={20}>
|
<ResizablePanel defaultSize={previewVisible ? 50 : 100} minSize={20}>
|
||||||
<section className="h-full bg-background">
|
<section className="h-full bg-background">
|
||||||
@@ -58,7 +63,9 @@ export function AppShell() {
|
|||||||
<main className="flex-1 overflow-hidden">
|
<main className="flex-1 overflow-hidden">
|
||||||
<ResizablePanelGroup
|
<ResizablePanelGroup
|
||||||
direction="horizontal"
|
direction="horizontal"
|
||||||
onLayoutChange={(sizes) => setPaneSizes({ sidebar: sizes[0], editor: sizes[1], preview: sizes[2] })}
|
onLayoutChange={(sizes) =>
|
||||||
|
setPaneSizes({ sidebar: sizes[0], editor: sizes[1], preview: sizes[2] })
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{sidebarVisible && (
|
{sidebarVisible && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -27,7 +27,9 @@ export function Breadcrumb() {
|
|||||||
className="flex items-center gap-1 hover:text-foreground"
|
className="flex items-center gap-1 hover:text-foreground"
|
||||||
>
|
>
|
||||||
<span aria-hidden="true">›</span>
|
<span aria-hidden="true">›</span>
|
||||||
<span className="truncate">{'#'.repeat(h.level)} {h.text}</span>
|
<span className="truncate">
|
||||||
|
{'#'.repeat(h.level)} {h.text}
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -17,7 +17,9 @@ export function StatusBar() {
|
|||||||
<span>UTF-8</span>
|
<span>UTF-8</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<span>Ln {cursor.line}, Col {cursor.column}</span>
|
<span>
|
||||||
|
Ln {cursor.line}, Col {cursor.column}
|
||||||
|
</span>
|
||||||
<span>Markdown</span>
|
<span>Markdown</span>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
@@ -7,11 +7,7 @@ import {
|
|||||||
closestCenter,
|
closestCenter,
|
||||||
type DragEndEvent,
|
type DragEndEvent,
|
||||||
} from '@dnd-kit/core';
|
} from '@dnd-kit/core';
|
||||||
import {
|
import { SortableContext, horizontalListSortingStrategy, useSortable } from '@dnd-kit/sortable';
|
||||||
SortableContext,
|
|
||||||
horizontalListSortingStrategy,
|
|
||||||
useSortable,
|
|
||||||
} from '@dnd-kit/sortable';
|
|
||||||
import { CSS } from '@dnd-kit/utilities';
|
import { CSS } from '@dnd-kit/utilities';
|
||||||
import { useFileStore, type OpenTab } from '@/stores/file-store';
|
import { useFileStore, type OpenTab } from '@/stores/file-store';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { ipc } from '@/lib/ipc';
|
import { ipc } from '@/lib/ipc';
|
||||||
import { useAppStore } from '@/stores/app-store';
|
import { useAppStore } from '@/stores/app-store';
|
||||||
|
|||||||
@@ -18,7 +18,10 @@ export function AboutSettings() {
|
|||||||
<a
|
<a
|
||||||
href="https://github.com/amitwh/markdown-converter"
|
href="https://github.com/amitwh/markdown-converter"
|
||||||
className="text-brand hover:underline"
|
className="text-brand hover:underline"
|
||||||
onClick={(e) => { e.preventDefault(); ipc.app.openExternal('https://github.com/amitwh/markdown-converter'); }}
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
ipc.app.openExternal('https://github.com/amitwh/markdown-converter');
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
GitHub repository
|
GitHub repository
|
||||||
</a>
|
</a>
|
||||||
@@ -27,7 +30,10 @@ export function AboutSettings() {
|
|||||||
<a
|
<a
|
||||||
href="https://concreteinfo.co.in"
|
href="https://concreteinfo.co.in"
|
||||||
className="text-brand hover:underline"
|
className="text-brand hover:underline"
|
||||||
onClick={(e) => { e.preventDefault(); ipc.app.openExternal('https://concreteinfo.co.in'); }}
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
ipc.app.openExternal('https://concreteinfo.co.in');
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
ConcreteInfo
|
ConcreteInfo
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -1,8 +1,21 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
import { useAppStore } from '@/stores/app-store';
|
import { useAppStore } from '@/stores/app-store';
|
||||||
import { toast } from '@/lib/toast';
|
import { toast } from '@/lib/toast';
|
||||||
@@ -17,9 +30,15 @@ export function AsciiGeneratorDialog() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
figletText(text || ' ', font)
|
figletText(text || ' ', font)
|
||||||
.then((result) => { if (!cancelled) setOutput(result); })
|
.then((result) => {
|
||||||
.catch(() => { if (!cancelled) setOutput('(render error)'); });
|
if (!cancelled) setOutput(result);
|
||||||
return () => { cancelled = true; };
|
})
|
||||||
|
.catch(() => {
|
||||||
|
if (!cancelled) setOutput('(render error)');
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
}, [text, font]);
|
}, [text, font]);
|
||||||
|
|
||||||
const handleCopy = async () => {
|
const handleCopy = async () => {
|
||||||
@@ -52,23 +71,32 @@ export function AsciiGeneratorDialog() {
|
|||||||
<div>
|
<div>
|
||||||
<Label htmlFor="ascii-font">Font</Label>
|
<Label htmlFor="ascii-font">Font</Label>
|
||||||
<Select value={font} onValueChange={(v) => setFont(v as FigletFont)}>
|
<Select value={font} onValueChange={(v) => setFont(v as FigletFont)}>
|
||||||
<SelectTrigger id="ascii-font" aria-label="Font"><SelectValue /></SelectTrigger>
|
<SelectTrigger id="ascii-font" aria-label="Font">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{FIGLET_FONTS.map((f) => (
|
{FIGLET_FONTS.map((f) => (
|
||||||
<SelectItem key={f} value={f}>{f}</SelectItem>
|
<SelectItem key={f} value={f}>
|
||||||
|
{f}
|
||||||
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label>Output</Label>
|
<Label>Output</Label>
|
||||||
<pre className="overflow-auto rounded border border-border bg-card/30 p-3 text-xs" data-testid="ascii-output">
|
<pre
|
||||||
|
className="overflow-auto rounded border border-border bg-card/30 p-3 text-xs"
|
||||||
|
data-testid="ascii-output"
|
||||||
|
>
|
||||||
{output}
|
{output}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="ghost" onClick={closeModal}>Close</Button>
|
<Button variant="ghost" onClick={closeModal}>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
<Button onClick={handleCopy}>Copy</Button>
|
<Button onClick={handleCopy}>Copy</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@@ -1,10 +1,25 @@
|
|||||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { useAppStore, type ConfirmProps } from '@/stores/app-store';
|
import { useAppStore, type ConfirmProps } from '@/stores/app-store';
|
||||||
|
|
||||||
export function ConfirmDialog(props: ConfirmProps) {
|
export function ConfirmDialog(props: ConfirmProps) {
|
||||||
const closeModal = useAppStore((s) => s.closeModal);
|
const closeModal = useAppStore((s) => s.closeModal);
|
||||||
const { title, body, confirmLabel = 'Confirm', cancelLabel = 'Cancel', destructive, onConfirm, onCancel } = props;
|
const {
|
||||||
|
title,
|
||||||
|
body,
|
||||||
|
confirmLabel = 'Confirm',
|
||||||
|
cancelLabel = 'Cancel',
|
||||||
|
destructive,
|
||||||
|
onConfirm,
|
||||||
|
onCancel,
|
||||||
|
} = props;
|
||||||
|
|
||||||
const handleConfirm = async () => {
|
const handleConfirm = async () => {
|
||||||
await onConfirm();
|
await onConfirm();
|
||||||
|
|||||||
@@ -12,29 +12,52 @@ export function CrashReportModal({ onClose }: { onClose: () => void }) {
|
|||||||
const [dumps, setDumps] = useState<Dump[]>([]);
|
const [dumps, setDumps] = useState<Dump[]>([]);
|
||||||
|
|
||||||
const refresh = async () => setDumps(await ipc.crash.read());
|
const refresh = async () => setDumps(await ipc.crash.read());
|
||||||
useEffect(() => { refresh(); }, []);
|
useEffect(() => {
|
||||||
|
refresh();
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div role="dialog" aria-modal="true" data-testid="crash-report-modal" className="fixed inset-0 z-50 bg-black/40 flex items-center justify-center">
|
<div
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
data-testid="crash-report-modal"
|
||||||
|
className="fixed inset-0 z-50 bg-black/40 flex items-center justify-center"
|
||||||
|
>
|
||||||
<div className="bg-white dark:bg-neutral-900 rounded-lg p-6 w-[36rem] max-h-[80vh] overflow-y-auto shadow-xl">
|
<div className="bg-white dark:bg-neutral-900 rounded-lg p-6 w-[36rem] max-h-[80vh] overflow-y-auto shadow-xl">
|
||||||
<header className="flex items-center justify-between mb-4">
|
<header className="flex items-center justify-between mb-4">
|
||||||
<h2 className="text-lg font-semibold">Crash reports</h2>
|
<h2 className="text-lg font-semibold">Crash reports</h2>
|
||||||
<button onClick={onClose} aria-label="Close">×</button>
|
<button onClick={onClose} aria-label="Close">
|
||||||
|
×
|
||||||
|
</button>
|
||||||
</header>
|
</header>
|
||||||
<button onClick={() => ipc.crash.openDir()} className="mb-4 px-3 py-1 text-sm border rounded">
|
<button
|
||||||
|
onClick={() => ipc.crash.openDir()}
|
||||||
|
className="mb-4 px-3 py-1 text-sm border rounded"
|
||||||
|
>
|
||||||
Open dump folder
|
Open dump folder
|
||||||
</button>
|
</button>
|
||||||
{dumps.length === 0 ? (
|
{dumps.length === 0 ? (
|
||||||
<p data-testid="empty-state" className="text-neutral-500">No crashes recorded — nice work!</p>
|
<p data-testid="empty-state" className="text-neutral-500">
|
||||||
|
No crashes recorded — nice work!
|
||||||
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<ul className="space-y-2">
|
<ul className="space-y-2">
|
||||||
{dumps.map((d) => (
|
{dumps.map((d) => (
|
||||||
<li key={d.filename} className="border rounded p-2 text-sm flex justify-between items-start gap-2">
|
<li
|
||||||
|
key={d.filename}
|
||||||
|
className="border rounded p-2 text-sm flex justify-between items-start gap-2"
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<div className="font-mono text-xs text-neutral-500">{d.timestamp}</div>
|
<div className="font-mono text-xs text-neutral-500">{d.timestamp}</div>
|
||||||
<div>{d.message ?? '(no message)'}</div>
|
<div>{d.message ?? '(no message)'}</div>
|
||||||
</div>
|
</div>
|
||||||
<button onClick={async () => { await ipc.crash.delete(d.filename); refresh(); }} className="text-red-600 text-xs">
|
<button
|
||||||
|
onClick={async () => {
|
||||||
|
await ipc.crash.delete(d.filename);
|
||||||
|
refresh();
|
||||||
|
}}
|
||||||
|
className="text-red-600 text-xs"
|
||||||
|
>
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
import { useSettingsStore } from '@/stores/settings-store';
|
import { useSettingsStore } from '@/stores/settings-store';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Slider } from '@/components/ui/slider';
|
import { Slider } from '@/components/ui/slider';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select';
|
||||||
import { Switch } from '@/components/ui/switch';
|
import { Switch } from '@/components/ui/switch';
|
||||||
|
|
||||||
export function EditorSettings() {
|
export function EditorSettings() {
|
||||||
@@ -11,12 +17,24 @@ export function EditorSettings() {
|
|||||||
<div className="space-y-5 text-sm">
|
<div className="space-y-5 text-sm">
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="editor-font-size">Font size: {fontSize}px</Label>
|
<Label htmlFor="editor-font-size">Font size: {fontSize}px</Label>
|
||||||
<Slider id="editor-font-size" min={10} max={24} step={1} value={[fontSize]} onValueChange={([v]) => setSetting('fontSize', v)} />
|
<Slider
|
||||||
|
id="editor-font-size"
|
||||||
|
min={10}
|
||||||
|
max={24}
|
||||||
|
step={1}
|
||||||
|
value={[fontSize]}
|
||||||
|
onValueChange={([v]) => setSetting('fontSize', v)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="editor-tab-size">Tab size</Label>
|
<Label htmlFor="editor-tab-size">Tab size</Label>
|
||||||
<Select value={String(tabSize)} onValueChange={(v) => setSetting('tabSize', Number(v) as 2 | 4 | 8)}>
|
<Select
|
||||||
<SelectTrigger id="editor-tab-size" aria-label="Tab size"><SelectValue /></SelectTrigger>
|
value={String(tabSize)}
|
||||||
|
onValueChange={(v) => setSetting('tabSize', Number(v) as 2 | 4 | 8)}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="editor-tab-size" aria-label="Tab size">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="2">2 spaces</SelectItem>
|
<SelectItem value="2">2 spaces</SelectItem>
|
||||||
<SelectItem value="4">4 spaces</SelectItem>
|
<SelectItem value="4">4 spaces</SelectItem>
|
||||||
@@ -26,15 +44,27 @@ export function EditorSettings() {
|
|||||||
</div>
|
</div>
|
||||||
<label className="flex items-center justify-between">
|
<label className="flex items-center justify-between">
|
||||||
<span>Line numbers</span>
|
<span>Line numbers</span>
|
||||||
<Switch checked={lineNumbers} onCheckedChange={(c) => setSetting('lineNumbers', c)} aria-label="Line numbers" />
|
<Switch
|
||||||
|
checked={lineNumbers}
|
||||||
|
onCheckedChange={(c) => setSetting('lineNumbers', c)}
|
||||||
|
aria-label="Line numbers"
|
||||||
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label className="flex items-center justify-between">
|
<label className="flex items-center justify-between">
|
||||||
<span>Word wrap</span>
|
<span>Word wrap</span>
|
||||||
<Switch checked={wordWrap} onCheckedChange={(c) => setSetting('wordWrap', c)} aria-label="Word wrap" />
|
<Switch
|
||||||
|
checked={wordWrap}
|
||||||
|
onCheckedChange={(c) => setSetting('wordWrap', c)}
|
||||||
|
aria-label="Word wrap"
|
||||||
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label className="flex items-center justify-between">
|
<label className="flex items-center justify-between">
|
||||||
<span>Minimap</span>
|
<span>Minimap</span>
|
||||||
<Switch checked={minimap} onCheckedChange={(c) => setSetting('minimap', c)} aria-label="Minimap" />
|
<Switch
|
||||||
|
checked={minimap}
|
||||||
|
onCheckedChange={(c) => setSetting('minimap', c)}
|
||||||
|
aria-label="Minimap"
|
||||||
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,19 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select';
|
||||||
import { useAppStore } from '@/stores/app-store';
|
import { useAppStore } from '@/stores/app-store';
|
||||||
import { ipc } from '@/lib/ipc';
|
import { ipc } from '@/lib/ipc';
|
||||||
import { toast } from '@/lib/toast';
|
import { toast } from '@/lib/toast';
|
||||||
@@ -46,7 +58,9 @@ export function ExportBatchDialog({ sourcePaths }: { sourcePaths: string[] }) {
|
|||||||
<div>
|
<div>
|
||||||
<Label htmlFor="batch-format">Format</Label>
|
<Label htmlFor="batch-format">Format</Label>
|
||||||
<Select value={format} onValueChange={(v) => setFormat(v as any)}>
|
<Select value={format} onValueChange={(v) => setFormat(v as any)}>
|
||||||
<SelectTrigger id="batch-format" aria-label="Format"><SelectValue /></SelectTrigger>
|
<SelectTrigger id="batch-format" aria-label="Format">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="pdf">PDF</SelectItem>
|
<SelectItem value="pdf">PDF</SelectItem>
|
||||||
<SelectItem value="docx">DOCX</SelectItem>
|
<SelectItem value="docx">DOCX</SelectItem>
|
||||||
@@ -58,24 +72,40 @@ export function ExportBatchDialog({ sourcePaths }: { sourcePaths: string[] }) {
|
|||||||
<div>
|
<div>
|
||||||
<Label htmlFor="batch-concurrency">Concurrency</Label>
|
<Label htmlFor="batch-concurrency">Concurrency</Label>
|
||||||
<Select value={String(concurrency)} onValueChange={(v) => setConcurrency(Number(v))}>
|
<Select value={String(concurrency)} onValueChange={(v) => setConcurrency(Number(v))}>
|
||||||
<SelectTrigger id="batch-concurrency" aria-label="Concurrency"><SelectValue /></SelectTrigger>
|
<SelectTrigger id="batch-concurrency" aria-label="Concurrency">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{[1, 2, 4, 8, 16].map((n) => (
|
{[1, 2, 4, 8, 16].map((n) => (
|
||||||
<SelectItem key={n} value={String(n)}>{n}</SelectItem>
|
<SelectItem key={n} value={String(n)}>
|
||||||
|
{n}
|
||||||
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div className="max-h-40 overflow-auto rounded border border-border bg-card/20 p-2 text-xs">
|
<div className="max-h-40 overflow-auto rounded border border-border bg-card/20 p-2 text-xs">
|
||||||
{sourcePaths.map((p) => <div key={p} className="truncate">{p}</div>)}
|
{sourcePaths.map((p) => (
|
||||||
|
<div key={p} className="truncate">
|
||||||
|
{p}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
{error && (
|
{error && (
|
||||||
<div role="alert" className="rounded border border-destructive/40 bg-destructive/5 p-2 text-xs text-destructive">
|
<div
|
||||||
|
role="alert"
|
||||||
|
className="rounded border border-destructive/40 bg-destructive/5 p-2 text-xs text-destructive"
|
||||||
|
>
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<ExportDialogFooter onCancel={closeModal} onSubmit={handleSubmit} submitting={submitting} submitLabel="Export" />
|
<ExportDialogFooter
|
||||||
|
onCancel={closeModal}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
submitting={submitting}
|
||||||
|
submitLabel="Export"
|
||||||
|
/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,7 +9,13 @@ interface Props {
|
|||||||
submitDisabled?: boolean;
|
submitDisabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ExportDialogFooter({ onCancel, onSubmit, submitting, submitLabel, submitDisabled }: Props) {
|
export function ExportDialogFooter({
|
||||||
|
onCancel,
|
||||||
|
onSubmit,
|
||||||
|
submitting,
|
||||||
|
submitLabel,
|
||||||
|
submitDisabled,
|
||||||
|
}: Props) {
|
||||||
return (
|
return (
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="ghost" onClick={onCancel} disabled={submitting}>
|
<Button variant="ghost" onClick={onCancel} disabled={submitting}>
|
||||||
|
|||||||
@@ -1,8 +1,20 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
import { Checkbox } from '@/components/ui/checkbox';
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select';
|
||||||
import { useAppStore } from '@/stores/app-store';
|
import { useAppStore } from '@/stores/app-store';
|
||||||
import { useSettingsStore } from '@/stores/settings-store';
|
import { useSettingsStore } from '@/stores/settings-store';
|
||||||
import { useExportSource } from '@/hooks/use-export-source';
|
import { useExportSource } from '@/hooks/use-export-source';
|
||||||
@@ -21,12 +33,18 @@ export function ExportDocxDialog({ sourcePath }: { sourcePath: string }) {
|
|||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!source) { setError('No file open.'); return; }
|
if (!source) {
|
||||||
|
setError('No file open.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const blob = await generateDocx({ source: source.source, title: source.title });
|
const blob = await generateDocx({ source: source.source, title: source.title });
|
||||||
const saveResult = await ipc.app.showSaveDialog?.({ title: 'Save as Word document', defaultPath: source.path.replace(/\.md$/, '.docx') });
|
const saveResult = await ipc.app.showSaveDialog?.({
|
||||||
|
title: 'Save as Word document',
|
||||||
|
defaultPath: source.path.replace(/\.md$/, '.docx'),
|
||||||
|
});
|
||||||
if (!saveResult?.ok || !saveResult.data) {
|
if (!saveResult?.ok || !saveResult.data) {
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
return;
|
return;
|
||||||
@@ -59,7 +77,9 @@ export function ExportDocxDialog({ sourcePath }: { sourcePath: string }) {
|
|||||||
<div>
|
<div>
|
||||||
<Label htmlFor="docx-template">Template</Label>
|
<Label htmlFor="docx-template">Template</Label>
|
||||||
<Select value={template} onValueChange={(v) => setTemplate(v as any)}>
|
<Select value={template} onValueChange={(v) => setTemplate(v as any)}>
|
||||||
<SelectTrigger id="docx-template" aria-label="Template"><SelectValue /></SelectTrigger>
|
<SelectTrigger id="docx-template" aria-label="Template">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="standard">Standard</SelectItem>
|
<SelectItem value="standard">Standard</SelectItem>
|
||||||
<SelectItem value="minimal">Minimal</SelectItem>
|
<SelectItem value="minimal">Minimal</SelectItem>
|
||||||
@@ -67,21 +87,33 @@ export function ExportDocxDialog({ sourcePath }: { sourcePath: string }) {
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<p className="mt-1 text-xs text-muted-foreground">
|
<p className="mt-1 text-xs text-muted-foreground">
|
||||||
The renderer-side export produces the same document for all three
|
The renderer-side export produces the same document for all three template choices;
|
||||||
template choices; the option is preserved for future stylesheets.
|
the option is preserved for future stylesheets.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<label className="flex items-center gap-2">
|
<label className="flex items-center gap-2">
|
||||||
<Checkbox checked={ascii} onCheckedChange={(c) => setAscii(!!c)} aria-label="ASCII tables" />
|
<Checkbox
|
||||||
|
checked={ascii}
|
||||||
|
onCheckedChange={(c) => setAscii(!!c)}
|
||||||
|
aria-label="ASCII tables"
|
||||||
|
/>
|
||||||
Render tables as ASCII
|
Render tables as ASCII
|
||||||
</label>
|
</label>
|
||||||
{error && (
|
{error && (
|
||||||
<div role="alert" className="rounded border border-destructive/40 bg-destructive/5 p-2 text-xs text-destructive">
|
<div
|
||||||
|
role="alert"
|
||||||
|
className="rounded border border-destructive/40 bg-destructive/5 p-2 text-xs text-destructive"
|
||||||
|
>
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<ExportDialogFooter onCancel={closeModal} onSubmit={handleSubmit} submitting={submitting} submitLabel="Export" />
|
<ExportDialogFooter
|
||||||
|
onCancel={closeModal}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
submitting={submitting}
|
||||||
|
submitLabel="Export"
|
||||||
|
/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,8 +1,20 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
import { Checkbox } from '@/components/ui/checkbox';
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select';
|
||||||
import { useAppStore } from '@/stores/app-store';
|
import { useAppStore } from '@/stores/app-store';
|
||||||
import { useSettingsStore } from '@/stores/settings-store';
|
import { useSettingsStore } from '@/stores/settings-store';
|
||||||
import { useExportSource } from '@/hooks/use-export-source';
|
import { useExportSource } from '@/hooks/use-export-source';
|
||||||
@@ -22,7 +34,10 @@ export function ExportHtmlDialog({ sourcePath }: { sourcePath: string }) {
|
|||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!source) { setError('No file open.'); return; }
|
if (!source) {
|
||||||
|
setError('No file open.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
@@ -33,7 +48,10 @@ export function ExportHtmlDialog({ sourcePath }: { sourcePath: string }) {
|
|||||||
highlightStyle: highlight,
|
highlightStyle: highlight,
|
||||||
renderTablesAsAscii: ascii,
|
renderTablesAsAscii: ascii,
|
||||||
});
|
});
|
||||||
const saveResult = await ipc.app.showSaveDialog?.({ title: 'Save as HTML', defaultPath: source.path.replace(/\.md$/, '.html') });
|
const saveResult = await ipc.app.showSaveDialog?.({
|
||||||
|
title: 'Save as HTML',
|
||||||
|
defaultPath: source.path.replace(/\.md$/, '.html'),
|
||||||
|
});
|
||||||
if (!saveResult?.ok || !saveResult.data) {
|
if (!saveResult?.ok || !saveResult.data) {
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
return;
|
return;
|
||||||
@@ -64,13 +82,19 @@ export function ExportHtmlDialog({ sourcePath }: { sourcePath: string }) {
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="space-y-3 text-sm">
|
<div className="space-y-3 text-sm">
|
||||||
<label className="flex items-center gap-2">
|
<label className="flex items-center gap-2">
|
||||||
<Checkbox checked={standalone} onCheckedChange={(c) => setStandalone(!!c)} aria-label="Standalone" />
|
<Checkbox
|
||||||
|
checked={standalone}
|
||||||
|
onCheckedChange={(c) => setStandalone(!!c)}
|
||||||
|
aria-label="Standalone"
|
||||||
|
/>
|
||||||
Standalone document (with inline CSS)
|
Standalone document (with inline CSS)
|
||||||
</label>
|
</label>
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="html-highlight">Syntax highlight style</Label>
|
<Label htmlFor="html-highlight">Syntax highlight style</Label>
|
||||||
<Select value={highlight} onValueChange={(v) => setHighlight(v as any)}>
|
<Select value={highlight} onValueChange={(v) => setHighlight(v as any)}>
|
||||||
<SelectTrigger id="html-highlight" aria-label="Highlight style"><SelectValue /></SelectTrigger>
|
<SelectTrigger id="html-highlight" aria-label="Highlight style">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="github">GitHub</SelectItem>
|
<SelectItem value="github">GitHub</SelectItem>
|
||||||
<SelectItem value="monokai">Monokai</SelectItem>
|
<SelectItem value="monokai">Monokai</SelectItem>
|
||||||
@@ -80,16 +104,28 @@ export function ExportHtmlDialog({ sourcePath }: { sourcePath: string }) {
|
|||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<label className="flex items-center gap-2">
|
<label className="flex items-center gap-2">
|
||||||
<Checkbox checked={ascii} onCheckedChange={(c) => setAscii(!!c)} aria-label="ASCII tables" />
|
<Checkbox
|
||||||
|
checked={ascii}
|
||||||
|
onCheckedChange={(c) => setAscii(!!c)}
|
||||||
|
aria-label="ASCII tables"
|
||||||
|
/>
|
||||||
Render tables as ASCII
|
Render tables as ASCII
|
||||||
</label>
|
</label>
|
||||||
{error && (
|
{error && (
|
||||||
<div role="alert" className="rounded border border-destructive/40 bg-destructive/5 p-2 text-xs text-destructive">
|
<div
|
||||||
|
role="alert"
|
||||||
|
className="rounded border border-destructive/40 bg-destructive/5 p-2 text-xs text-destructive"
|
||||||
|
>
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<ExportDialogFooter onCancel={closeModal} onSubmit={handleSubmit} submitting={submitting} submitLabel="Export" />
|
<ExportDialogFooter
|
||||||
|
onCancel={closeModal}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
submitting={submitting}
|
||||||
|
submitLabel="Export"
|
||||||
|
/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,8 +1,20 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
import { Checkbox } from '@/components/ui/checkbox';
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select';
|
||||||
import { useAppStore } from '@/stores/app-store';
|
import { useAppStore } from '@/stores/app-store';
|
||||||
import { useSettingsStore } from '@/stores/settings-store';
|
import { useSettingsStore } from '@/stores/settings-store';
|
||||||
import { useExportSource } from '@/hooks/use-export-source';
|
import { useExportSource } from '@/hooks/use-export-source';
|
||||||
@@ -19,7 +31,8 @@ const MARGIN_MAP = {
|
|||||||
|
|
||||||
export function ExportPdfDialog({ sourcePath }: { sourcePath: string }) {
|
export function ExportPdfDialog({ sourcePath }: { sourcePath: string }) {
|
||||||
const closeModal = useAppStore((s) => s.closeModal);
|
const closeModal = useAppStore((s) => s.closeModal);
|
||||||
const { fontSize, pdfFormat, pdfMargins, pdfEmbedFonts, renderTablesAsAscii } = useSettingsStore();
|
const { fontSize, pdfFormat, pdfMargins, pdfEmbedFonts, renderTablesAsAscii } =
|
||||||
|
useSettingsStore();
|
||||||
const [format, setFormat] = useState<'letter' | 'a4' | 'legal'>(pdfFormat);
|
const [format, setFormat] = useState<'letter' | 'a4' | 'legal'>(pdfFormat);
|
||||||
const [margins, setMargins] = useState<'normal' | 'narrow' | 'wide'>(pdfMargins);
|
const [margins, setMargins] = useState<'normal' | 'narrow' | 'wide'>(pdfMargins);
|
||||||
const [embed, setEmbed] = useState(pdfEmbedFonts);
|
const [embed, setEmbed] = useState(pdfEmbedFonts);
|
||||||
@@ -45,8 +58,11 @@ export function ExportPdfDialog({ sourcePath }: { sourcePath: string }) {
|
|||||||
highlightStyle: 'github',
|
highlightStyle: 'github',
|
||||||
renderTablesAsAscii: ascii,
|
renderTablesAsAscii: ascii,
|
||||||
});
|
});
|
||||||
const fmt = format === 'a4' ? { width: '210mm', height: '297mm' }
|
const fmt =
|
||||||
: format === 'legal' ? { width: '8.5in', height: '14in' }
|
format === 'a4'
|
||||||
|
? { width: '210mm', height: '297mm' }
|
||||||
|
: format === 'legal'
|
||||||
|
? { width: '8.5in', height: '14in' }
|
||||||
: { width: '8.5in', height: '11in' };
|
: { width: '8.5in', height: '11in' };
|
||||||
const m = MARGIN_MAP[margins];
|
const m = MARGIN_MAP[margins];
|
||||||
const pageCss = `@page { size: ${fmt.width} ${fmt.height}; margin: ${m.top}mm ${m.right}mm ${m.bottom}mm ${m.left}mm; }`;
|
const pageCss = `@page { size: ${fmt.width} ${fmt.height}; margin: ${m.top}mm ${m.right}mm ${m.bottom}mm ${m.left}mm; }`;
|
||||||
@@ -80,7 +96,9 @@ export function ExportPdfDialog({ sourcePath }: { sourcePath: string }) {
|
|||||||
<div>
|
<div>
|
||||||
<Label htmlFor="pdf-format">Format</Label>
|
<Label htmlFor="pdf-format">Format</Label>
|
||||||
<Select value={format} onValueChange={(v) => setFormat(v as typeof format)}>
|
<Select value={format} onValueChange={(v) => setFormat(v as typeof format)}>
|
||||||
<SelectTrigger id="pdf-format" aria-label="Format"><SelectValue /></SelectTrigger>
|
<SelectTrigger id="pdf-format" aria-label="Format">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="letter">Letter</SelectItem>
|
<SelectItem value="letter">Letter</SelectItem>
|
||||||
<SelectItem value="a4">A4</SelectItem>
|
<SelectItem value="a4">A4</SelectItem>
|
||||||
@@ -91,7 +109,9 @@ export function ExportPdfDialog({ sourcePath }: { sourcePath: string }) {
|
|||||||
<div>
|
<div>
|
||||||
<Label htmlFor="pdf-margins">Margins</Label>
|
<Label htmlFor="pdf-margins">Margins</Label>
|
||||||
<Select value={margins} onValueChange={(v) => setMargins(v as typeof margins)}>
|
<Select value={margins} onValueChange={(v) => setMargins(v as typeof margins)}>
|
||||||
<SelectTrigger id="pdf-margins" aria-label="Margins"><SelectValue /></SelectTrigger>
|
<SelectTrigger id="pdf-margins" aria-label="Margins">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="narrow">Narrow</SelectItem>
|
<SelectItem value="narrow">Narrow</SelectItem>
|
||||||
<SelectItem value="normal">Normal</SelectItem>
|
<SelectItem value="normal">Normal</SelectItem>
|
||||||
@@ -100,20 +120,36 @@ export function ExportPdfDialog({ sourcePath }: { sourcePath: string }) {
|
|||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<label className="flex items-center gap-2">
|
<label className="flex items-center gap-2">
|
||||||
<Checkbox checked={embed} onCheckedChange={(c) => setEmbed(!!c)} aria-label="Embed fonts" />
|
<Checkbox
|
||||||
|
checked={embed}
|
||||||
|
onCheckedChange={(c) => setEmbed(!!c)}
|
||||||
|
aria-label="Embed fonts"
|
||||||
|
/>
|
||||||
Embed fonts
|
Embed fonts
|
||||||
</label>
|
</label>
|
||||||
<label className="flex items-center gap-2">
|
<label className="flex items-center gap-2">
|
||||||
<Checkbox checked={ascii} onCheckedChange={(c) => setAscii(!!c)} aria-label="ASCII tables" />
|
<Checkbox
|
||||||
|
checked={ascii}
|
||||||
|
onCheckedChange={(c) => setAscii(!!c)}
|
||||||
|
aria-label="ASCII tables"
|
||||||
|
/>
|
||||||
Render tables as ASCII
|
Render tables as ASCII
|
||||||
</label>
|
</label>
|
||||||
{error && (
|
{error && (
|
||||||
<div role="alert" className="rounded border border-destructive/40 bg-destructive/5 p-2 text-xs text-destructive">
|
<div
|
||||||
|
role="alert"
|
||||||
|
className="rounded border border-destructive/40 bg-destructive/5 p-2 text-xs text-destructive"
|
||||||
|
>
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<ExportDialogFooter onCancel={closeModal} onSubmit={handleSubmit} submitting={submitting} submitLabel="Export" />
|
<ExportDialogFooter
|
||||||
|
onCancel={closeModal}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
submitting={submitting}
|
||||||
|
submitLabel="Export"
|
||||||
|
/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,17 +1,33 @@
|
|||||||
import { useSettingsStore } from '@/stores/settings-store';
|
import { useSettingsStore } from '@/stores/settings-store';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select';
|
||||||
import { Switch } from '@/components/ui/switch';
|
import { Switch } from '@/components/ui/switch';
|
||||||
|
|
||||||
export function ExportSettings() {
|
export function ExportSettings() {
|
||||||
const { pdfFormat, pdfMargins, pdfEmbedFonts, docxTemplate, htmlHighlightStyle, renderTablesAsAscii, setSetting } = useSettingsStore();
|
const {
|
||||||
|
pdfFormat,
|
||||||
|
pdfMargins,
|
||||||
|
pdfEmbedFonts,
|
||||||
|
docxTemplate,
|
||||||
|
htmlHighlightStyle,
|
||||||
|
renderTablesAsAscii,
|
||||||
|
setSetting,
|
||||||
|
} = useSettingsStore();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-5 text-sm">
|
<div className="space-y-5 text-sm">
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="export-pdf-format">Default PDF format</Label>
|
<Label htmlFor="export-pdf-format">Default PDF format</Label>
|
||||||
<Select value={pdfFormat} onValueChange={(v) => setSetting('pdfFormat', v as any)}>
|
<Select value={pdfFormat} onValueChange={(v) => setSetting('pdfFormat', v as any)}>
|
||||||
<SelectTrigger id="export-pdf-format" aria-label="Default PDF format"><SelectValue /></SelectTrigger>
|
<SelectTrigger id="export-pdf-format" aria-label="Default PDF format">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="letter">Letter</SelectItem>
|
<SelectItem value="letter">Letter</SelectItem>
|
||||||
<SelectItem value="a4">A4</SelectItem>
|
<SelectItem value="a4">A4</SelectItem>
|
||||||
@@ -22,7 +38,9 @@ export function ExportSettings() {
|
|||||||
<div>
|
<div>
|
||||||
<Label htmlFor="export-pdf-margins">Default PDF margins</Label>
|
<Label htmlFor="export-pdf-margins">Default PDF margins</Label>
|
||||||
<Select value={pdfMargins} onValueChange={(v) => setSetting('pdfMargins', v as any)}>
|
<Select value={pdfMargins} onValueChange={(v) => setSetting('pdfMargins', v as any)}>
|
||||||
<SelectTrigger id="export-pdf-margins" aria-label="Default PDF margins"><SelectValue /></SelectTrigger>
|
<SelectTrigger id="export-pdf-margins" aria-label="Default PDF margins">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="narrow">Narrow</SelectItem>
|
<SelectItem value="narrow">Narrow</SelectItem>
|
||||||
<SelectItem value="normal">Normal</SelectItem>
|
<SelectItem value="normal">Normal</SelectItem>
|
||||||
@@ -32,12 +50,18 @@ export function ExportSettings() {
|
|||||||
</div>
|
</div>
|
||||||
<label className="flex items-center justify-between">
|
<label className="flex items-center justify-between">
|
||||||
<span>Embed fonts in PDFs</span>
|
<span>Embed fonts in PDFs</span>
|
||||||
<Switch checked={pdfEmbedFonts} onCheckedChange={(c) => setSetting('pdfEmbedFonts', c)} aria-label="Embed fonts" />
|
<Switch
|
||||||
|
checked={pdfEmbedFonts}
|
||||||
|
onCheckedChange={(c) => setSetting('pdfEmbedFonts', c)}
|
||||||
|
aria-label="Embed fonts"
|
||||||
|
/>
|
||||||
</label>
|
</label>
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="export-docx-template">Default DOCX template</Label>
|
<Label htmlFor="export-docx-template">Default DOCX template</Label>
|
||||||
<Select value={docxTemplate} onValueChange={(v) => setSetting('docxTemplate', v as any)}>
|
<Select value={docxTemplate} onValueChange={(v) => setSetting('docxTemplate', v as any)}>
|
||||||
<SelectTrigger id="export-docx-template" aria-label="Default DOCX template"><SelectValue /></SelectTrigger>
|
<SelectTrigger id="export-docx-template" aria-label="Default DOCX template">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="standard">Standard</SelectItem>
|
<SelectItem value="standard">Standard</SelectItem>
|
||||||
<SelectItem value="minimal">Minimal</SelectItem>
|
<SelectItem value="minimal">Minimal</SelectItem>
|
||||||
@@ -47,8 +71,13 @@ export function ExportSettings() {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="export-html-highlight">Default HTML highlight</Label>
|
<Label htmlFor="export-html-highlight">Default HTML highlight</Label>
|
||||||
<Select value={htmlHighlightStyle} onValueChange={(v) => setSetting('htmlHighlightStyle', v as any)}>
|
<Select
|
||||||
<SelectTrigger id="export-html-highlight" aria-label="Default HTML highlight"><SelectValue /></SelectTrigger>
|
value={htmlHighlightStyle}
|
||||||
|
onValueChange={(v) => setSetting('htmlHighlightStyle', v as any)}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="export-html-highlight" aria-label="Default HTML highlight">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="github">GitHub</SelectItem>
|
<SelectItem value="github">GitHub</SelectItem>
|
||||||
<SelectItem value="monokai">Monokai</SelectItem>
|
<SelectItem value="monokai">Monokai</SelectItem>
|
||||||
@@ -59,7 +88,11 @@ export function ExportSettings() {
|
|||||||
</div>
|
</div>
|
||||||
<label className="flex items-center justify-between">
|
<label className="flex items-center justify-between">
|
||||||
<span>Render tables as ASCII by default</span>
|
<span>Render tables as ASCII by default</span>
|
||||||
<Switch checked={renderTablesAsAscii} onCheckedChange={(c) => setSetting('renderTablesAsAscii', c)} aria-label="ASCII tables by default" />
|
<Switch
|
||||||
|
checked={renderTablesAsAscii}
|
||||||
|
onCheckedChange={(c) => setSetting('renderTablesAsAscii', c)}
|
||||||
|
aria-label="ASCII tables by default"
|
||||||
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Checkbox } from '@/components/ui/checkbox';
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
@@ -67,22 +74,35 @@ export function FindInFilesDialog() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<label className="flex items-center gap-2">
|
<label className="flex items-center gap-2">
|
||||||
<Checkbox checked={isRegex} onCheckedChange={(c) => setIsRegex(!!c)} aria-label="Regex" />
|
<Checkbox
|
||||||
|
checked={isRegex}
|
||||||
|
onCheckedChange={(c) => setIsRegex(!!c)}
|
||||||
|
aria-label="Regex"
|
||||||
|
/>
|
||||||
Regex
|
Regex
|
||||||
</label>
|
</label>
|
||||||
<label className="flex items-center gap-2">
|
<label className="flex items-center gap-2">
|
||||||
<Checkbox checked={caseSensitive} onCheckedChange={(c) => setCaseSensitive(!!c)} aria-label="Case sensitive" />
|
<Checkbox
|
||||||
|
checked={caseSensitive}
|
||||||
|
onCheckedChange={(c) => setCaseSensitive(!!c)}
|
||||||
|
aria-label="Case sensitive"
|
||||||
|
/>
|
||||||
Case sensitive
|
Case sensitive
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
{error && (
|
{error && (
|
||||||
<div role="alert" className="rounded border border-destructive/40 bg-destructive/5 p-2 text-xs text-destructive">
|
<div
|
||||||
|
role="alert"
|
||||||
|
className="rounded border border-destructive/40 bg-destructive/5 p-2 text-xs text-destructive"
|
||||||
|
>
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{results.length > 0 && (
|
{results.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<Label>{results.length} result{results.length === 1 ? '' : 's'}</Label>
|
<Label>
|
||||||
|
{results.length} result{results.length === 1 ? '' : 's'}
|
||||||
|
</Label>
|
||||||
<div className="max-h-64 overflow-auto rounded border border-border bg-card/20 text-xs">
|
<div className="max-h-64 overflow-auto rounded border border-border bg-card/20 text-xs">
|
||||||
{results.map((r, i) => (
|
{results.map((r, i) => (
|
||||||
<button
|
<button
|
||||||
@@ -91,7 +111,9 @@ export function FindInFilesDialog() {
|
|||||||
className="block w-full truncate border-b border-border/30 px-2 py-1 text-left hover:bg-card/50"
|
className="block w-full truncate border-b border-border/30 px-2 py-1 text-left hover:bg-card/50"
|
||||||
data-testid="find-result"
|
data-testid="find-result"
|
||||||
>
|
>
|
||||||
<span className="font-mono text-muted-foreground">{r.filePath}:{r.line}</span>
|
<span className="font-mono text-muted-foreground">
|
||||||
|
{r.filePath}:{r.line}
|
||||||
|
</span>
|
||||||
<span className="ml-2">{r.content}</span>
|
<span className="ml-2">{r.content}</span>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
@@ -100,7 +122,9 @@ export function FindInFilesDialog() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="ghost" onClick={closeModal} disabled={submitting}>Close</Button>
|
<Button variant="ghost" onClick={closeModal} disabled={submitting}>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
<Button onClick={handleSearch} disabled={submitting || !query}>
|
<Button onClick={handleSearch} disabled={submitting || !query}>
|
||||||
{submitting ? 'Searching…' : 'Search'}
|
{submitting ? 'Searching…' : 'Search'}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from '@/components/ui/sheet';
|
import {
|
||||||
|
Sheet,
|
||||||
|
SheetContent,
|
||||||
|
SheetDescription,
|
||||||
|
SheetHeader,
|
||||||
|
SheetTitle,
|
||||||
|
} from '@/components/ui/sheet';
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { useAppStore } from '@/stores/app-store';
|
import { useAppStore } from '@/stores/app-store';
|
||||||
@@ -31,16 +37,30 @@ export function SettingsSheet() {
|
|||||||
<TabsTrigger value="about">About</TabsTrigger>
|
<TabsTrigger value="about">About</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
<div className="mt-4 max-h-[70vh] overflow-y-auto pr-2">
|
<div className="mt-4 max-h-[70vh] overflow-y-auto pr-2">
|
||||||
<TabsContent value="editor"><EditorSettings /></TabsContent>
|
<TabsContent value="editor">
|
||||||
<TabsContent value="theme"><ThemeSettings /></TabsContent>
|
<EditorSettings />
|
||||||
<TabsContent value="export"><ExportSettings /></TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="plugins"><PluginsSettings /></TabsContent>
|
<TabsContent value="theme">
|
||||||
<TabsContent value="updates"><UpdatesSettings /></TabsContent>
|
<ThemeSettings />
|
||||||
<TabsContent value="about"><AboutSettings /></TabsContent>
|
</TabsContent>
|
||||||
|
<TabsContent value="export">
|
||||||
|
<ExportSettings />
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="plugins">
|
||||||
|
<PluginsSettings />
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="updates">
|
||||||
|
<UpdatesSettings />
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="about">
|
||||||
|
<AboutSettings />
|
||||||
|
</TabsContent>
|
||||||
</div>
|
</div>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
<div className="mt-4 flex justify-end border-t border-border pt-4">
|
<div className="mt-4 flex justify-end border-t border-border pt-4">
|
||||||
<Button variant="ghost" onClick={resetToDefaults}>Reset to defaults</Button>
|
<Button variant="ghost" onClick={resetToDefaults}>
|
||||||
|
Reset to defaults
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</SheetContent>
|
</SheetContent>
|
||||||
</Sheet>
|
</Sheet>
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Checkbox } from '@/components/ui/checkbox';
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
@@ -11,7 +18,13 @@ function pad(s: string, w: number) {
|
|||||||
return s.padEnd(w);
|
return s.padEnd(w);
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateTable(rows: number, cols: number, hasHeader: boolean, headers: string[], data: string[][]): string {
|
function generateTable(
|
||||||
|
rows: number,
|
||||||
|
cols: number,
|
||||||
|
hasHeader: boolean,
|
||||||
|
headers: string[],
|
||||||
|
data: string[][]
|
||||||
|
): string {
|
||||||
const width = cols;
|
const width = cols;
|
||||||
const colWidths: number[] = Array.from({ length: width }, (_, c) => {
|
const colWidths: number[] = Array.from({ length: width }, (_, c) => {
|
||||||
const all = [headers[c] ?? '', ...data.map((r) => r[c] ?? '')];
|
const all = [headers[c] ?? '', ...data.map((r) => r[c] ?? '')];
|
||||||
@@ -19,11 +32,21 @@ function generateTable(rows: number, cols: number, hasHeader: boolean, headers:
|
|||||||
});
|
});
|
||||||
const lines: string[] = [];
|
const lines: string[] = [];
|
||||||
if (hasHeader) {
|
if (hasHeader) {
|
||||||
lines.push('| ' + Array.from({ length: width }, (_, c) => pad(headers[c] ?? `Col ${c + 1}`, colWidths[c])).join(' | ') + ' |');
|
lines.push(
|
||||||
|
'| ' +
|
||||||
|
Array.from({ length: width }, (_, c) =>
|
||||||
|
pad(headers[c] ?? `Col ${c + 1}`, colWidths[c])
|
||||||
|
).join(' | ') +
|
||||||
|
' |'
|
||||||
|
);
|
||||||
lines.push('| ' + colWidths.map((w) => '-'.repeat(w)).join(' | ') + ' |');
|
lines.push('| ' + colWidths.map((w) => '-'.repeat(w)).join(' | ') + ' |');
|
||||||
}
|
}
|
||||||
for (const row of data) {
|
for (const row of data) {
|
||||||
lines.push('| ' + Array.from({ length: width }, (_, c) => pad(row[c] ?? '', colWidths[c])).join(' | ') + ' |');
|
lines.push(
|
||||||
|
'| ' +
|
||||||
|
Array.from({ length: width }, (_, c) => pad(row[c] ?? '', colWidths[c])).join(' | ') +
|
||||||
|
' |'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// Add empty rows if data has fewer
|
// Add empty rows if data has fewer
|
||||||
for (let i = data.length; i < rows; i++) {
|
for (let i = data.length; i < rows; i++) {
|
||||||
@@ -53,7 +76,13 @@ export function TableGeneratorDialog() {
|
|||||||
const updateHeadersCount = (newCols: number) => {
|
const updateHeadersCount = (newCols: number) => {
|
||||||
setCols(newCols);
|
setCols(newCols);
|
||||||
setHeaders((prev) => {
|
setHeaders((prev) => {
|
||||||
if (prev.length < newCols) return [...prev, ...Array(newCols - prev.length).fill('').map((_, i) => `Col ${prev.length + i + 1}`)];
|
if (prev.length < newCols)
|
||||||
|
return [
|
||||||
|
...prev,
|
||||||
|
...Array(newCols - prev.length)
|
||||||
|
.fill('')
|
||||||
|
.map((_, i) => `Col ${prev.length + i + 1}`),
|
||||||
|
];
|
||||||
return prev.slice(0, newCols);
|
return prev.slice(0, newCols);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -88,18 +117,27 @@ export function TableGeneratorDialog() {
|
|||||||
min={1}
|
min={1}
|
||||||
max={20}
|
max={20}
|
||||||
value={cols}
|
value={cols}
|
||||||
onChange={(e) => updateHeadersCount(Math.max(1, Math.min(20, Number(e.target.value) || 1)))}
|
onChange={(e) =>
|
||||||
|
updateHeadersCount(Math.max(1, Math.min(20, Number(e.target.value) || 1)))
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<label className="flex items-center gap-2">
|
<label className="flex items-center gap-2">
|
||||||
<Checkbox checked={hasHeader} onCheckedChange={(c) => setHasHeader(!!c)} aria-label="Header" />
|
<Checkbox
|
||||||
|
checked={hasHeader}
|
||||||
|
onCheckedChange={(c) => setHasHeader(!!c)}
|
||||||
|
aria-label="Header"
|
||||||
|
/>
|
||||||
Include header row
|
Include header row
|
||||||
</label>
|
</label>
|
||||||
{hasHeader && (
|
{hasHeader && (
|
||||||
<div>
|
<div>
|
||||||
<Label>Header names</Label>
|
<Label>Header names</Label>
|
||||||
<div className="grid gap-2" style={{ gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))` }}>
|
<div
|
||||||
|
className="grid gap-2"
|
||||||
|
style={{ gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))` }}
|
||||||
|
>
|
||||||
{Array.from({ length: cols }, (_, i) => (
|
{Array.from({ length: cols }, (_, i) => (
|
||||||
<Input
|
<Input
|
||||||
key={i}
|
key={i}
|
||||||
@@ -117,13 +155,18 @@ export function TableGeneratorDialog() {
|
|||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
<Label>Output</Label>
|
<Label>Output</Label>
|
||||||
<pre className="overflow-auto rounded border border-border bg-card/30 p-3 text-xs" data-testid="table-output">
|
<pre
|
||||||
|
className="overflow-auto rounded border border-border bg-card/30 p-3 text-xs"
|
||||||
|
data-testid="table-output"
|
||||||
|
>
|
||||||
{output}
|
{output}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="ghost" onClick={closeModal}>Close</Button>
|
<Button variant="ghost" onClick={closeModal}>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
<Button onClick={handleCopy}>Copy</Button>
|
<Button onClick={handleCopy}>Copy</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
import { useSettingsStore } from '@/stores/settings-store';
|
import { useSettingsStore } from '@/stores/settings-store';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select';
|
||||||
|
|
||||||
export function ThemeSettings() {
|
export function ThemeSettings() {
|
||||||
const { theme, accentColor, fontFamily, setSetting } = useSettingsStore();
|
const { theme, accentColor, fontFamily, setSetting } = useSettingsStore();
|
||||||
@@ -10,16 +16,30 @@ export function ThemeSettings() {
|
|||||||
<div className="space-y-5 text-sm">
|
<div className="space-y-5 text-sm">
|
||||||
<div>
|
<div>
|
||||||
<Label>Mode</Label>
|
<Label>Mode</Label>
|
||||||
<RadioGroup value={theme} onValueChange={(v) => setSetting('theme', v as 'light' | 'dark' | 'system')}>
|
<RadioGroup
|
||||||
<div className="flex items-center gap-2"><RadioGroupItem value="light" id="theme-light" /><Label htmlFor="theme-light">Light</Label></div>
|
value={theme}
|
||||||
<div className="flex items-center gap-2"><RadioGroupItem value="dark" id="theme-dark" /><Label htmlFor="theme-dark">Dark</Label></div>
|
onValueChange={(v) => setSetting('theme', v as 'light' | 'dark' | 'system')}
|
||||||
<div className="flex items-center gap-2"><RadioGroupItem value="system" id="theme-system" /><Label htmlFor="theme-system">System</Label></div>
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<RadioGroupItem value="light" id="theme-light" />
|
||||||
|
<Label htmlFor="theme-light">Light</Label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<RadioGroupItem value="dark" id="theme-dark" />
|
||||||
|
<Label htmlFor="theme-dark">Dark</Label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<RadioGroupItem value="system" id="theme-system" />
|
||||||
|
<Label htmlFor="theme-system">System</Label>
|
||||||
|
</div>
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="theme-accent">Accent color</Label>
|
<Label htmlFor="theme-accent">Accent color</Label>
|
||||||
<Select value={accentColor} onValueChange={(v) => setSetting('accentColor', v as any)}>
|
<Select value={accentColor} onValueChange={(v) => setSetting('accentColor', v as any)}>
|
||||||
<SelectTrigger id="theme-accent" aria-label="Accent color"><SelectValue /></SelectTrigger>
|
<SelectTrigger id="theme-accent" aria-label="Accent color">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="brand">Brand (orange)</SelectItem>
|
<SelectItem value="brand">Brand (orange)</SelectItem>
|
||||||
<SelectItem value="blue">Blue</SelectItem>
|
<SelectItem value="blue">Blue</SelectItem>
|
||||||
@@ -32,7 +52,9 @@ export function ThemeSettings() {
|
|||||||
<div>
|
<div>
|
||||||
<Label htmlFor="theme-font-family">Editor font</Label>
|
<Label htmlFor="theme-font-family">Editor font</Label>
|
||||||
<Select value={fontFamily} onValueChange={(v) => setSetting('fontFamily', v as any)}>
|
<Select value={fontFamily} onValueChange={(v) => setSetting('fontFamily', v as any)}>
|
||||||
<SelectTrigger id="theme-font-family" aria-label="Editor font"><SelectValue /></SelectTrigger>
|
<SelectTrigger id="theme-font-family" aria-label="Editor font">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="system">System (Plus Jakarta Sans)</SelectItem>
|
<SelectItem value="system">System (Plus Jakarta Sans)</SelectItem>
|
||||||
<SelectItem value="jetbrains">JetBrains Mono</SelectItem>
|
<SelectItem value="jetbrains">JetBrains Mono</SelectItem>
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Checkbox } from '@/components/ui/checkbox';
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
import { useAppStore } from '@/stores/app-store';
|
import { useAppStore } from '@/stores/app-store';
|
||||||
@@ -27,20 +34,30 @@ export function WelcomeDialog() {
|
|||||||
<div className="space-y-3 text-sm">
|
<div className="space-y-3 text-sm">
|
||||||
<div className="rounded-md border border-border bg-card/30 p-3">
|
<div className="rounded-md border border-border bg-card/30 p-3">
|
||||||
<h3 className="font-semibold">1. Open a folder</h3>
|
<h3 className="font-semibold">1. Open a folder</h3>
|
||||||
<p className="text-muted-foreground">File → Open Folder, or ⌘O. Your tree appears on the left.</p>
|
<p className="text-muted-foreground">
|
||||||
|
File → Open Folder, or ⌘O. Your tree appears on the left.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-md border border-border bg-card/30 p-3">
|
<div className="rounded-md border border-border bg-card/30 p-3">
|
||||||
<h3 className="font-semibold">2. Edit & preview</h3>
|
<h3 className="font-semibold">2. Edit & preview</h3>
|
||||||
<p className="text-muted-foreground">Type on the left, see the rendered preview on the right. Toggle with ⌘\.</p>
|
<p className="text-muted-foreground">
|
||||||
|
Type on the left, see the rendered preview on the right. Toggle with ⌘\.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-md border border-border bg-card/30 p-3">
|
<div className="rounded-md border border-border bg-card/30 p-3">
|
||||||
<h3 className="font-semibold">3. Export anywhere</h3>
|
<h3 className="font-semibold">3. Export anywhere</h3>
|
||||||
<p className="text-muted-foreground">File → Export to PDF / DOCX / HTML, or batch convert a folder.</p>
|
<p className="text-muted-foreground">
|
||||||
|
File → Export to PDF / DOCX / HTML, or batch convert a folder.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter className="flex-row items-center justify-between sm:justify-between">
|
<DialogFooter className="flex-row items-center justify-between sm:justify-between">
|
||||||
<label className="flex items-center gap-2 text-xs text-muted-foreground">
|
<label className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||||
<Checkbox checked={dontShow} onCheckedChange={(c) => setDontShow(!!c)} aria-label="Don't show again" />
|
<Checkbox
|
||||||
|
checked={dontShow}
|
||||||
|
onCheckedChange={(c) => setDontShow(!!c)}
|
||||||
|
aria-label="Don't show again"
|
||||||
|
/>
|
||||||
Don't show again
|
Don't show again
|
||||||
</label>
|
</label>
|
||||||
<Button onClick={handleClose}>Get started</Button>
|
<Button onClick={handleClose}>Get started</Button>
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
||||||
@@ -16,7 +23,9 @@ export function WordExportDialog({ sourcePath }: { sourcePath: string }) {
|
|||||||
const docxCustomTemplatePath = useSettingsStore((s) => s.docxCustomTemplatePath);
|
const docxCustomTemplatePath = useSettingsStore((s) => s.docxCustomTemplatePath);
|
||||||
const source = useExportSource();
|
const source = useExportSource();
|
||||||
|
|
||||||
const [templateMode, setTemplateMode] = useState<'standard' | 'custom'>(docxCustomTemplatePath ? 'custom' : 'standard');
|
const [templateMode, setTemplateMode] = useState<'standard' | 'custom'>(
|
||||||
|
docxCustomTemplatePath ? 'custom' : 'standard'
|
||||||
|
);
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
@@ -32,7 +41,10 @@ export function WordExportDialog({ sourcePath }: { sourcePath: string }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!source) { setError('No file open.'); return; }
|
if (!source) {
|
||||||
|
setError('No file open.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
@@ -41,7 +53,10 @@ export function WordExportDialog({ sourcePath }: { sourcePath: string }) {
|
|||||||
title: source.title,
|
title: source.title,
|
||||||
customTemplatePath: templateMode === 'custom' ? docxCustomTemplatePath : null,
|
customTemplatePath: templateMode === 'custom' ? docxCustomTemplatePath : null,
|
||||||
});
|
});
|
||||||
const saveResult = await ipc.app.showSaveDialog?.({ title: 'Save as Word document', defaultPath: source.path.replace(/\.md$/, '.docx') });
|
const saveResult = await ipc.app.showSaveDialog?.({
|
||||||
|
title: 'Save as Word document',
|
||||||
|
defaultPath: source.path.replace(/\.md$/, '.docx'),
|
||||||
|
});
|
||||||
if (!saveResult?.ok || !saveResult.data) {
|
if (!saveResult?.ok || !saveResult.data) {
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
return;
|
return;
|
||||||
@@ -73,7 +88,10 @@ export function WordExportDialog({ sourcePath }: { sourcePath: string }) {
|
|||||||
<div className="space-y-3 text-sm">
|
<div className="space-y-3 text-sm">
|
||||||
<div>
|
<div>
|
||||||
<Label>Template</Label>
|
<Label>Template</Label>
|
||||||
<RadioGroup value={templateMode} onValueChange={(v) => setTemplateMode(v as 'standard' | 'custom')}>
|
<RadioGroup
|
||||||
|
value={templateMode}
|
||||||
|
onValueChange={(v) => setTemplateMode(v as 'standard' | 'custom')}
|
||||||
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<RadioGroupItem value="standard" id="template-standard" />
|
<RadioGroupItem value="standard" id="template-standard" />
|
||||||
<Label htmlFor="template-standard">Standard (bundled)</Label>
|
<Label htmlFor="template-standard">Standard (bundled)</Label>
|
||||||
@@ -87,9 +105,13 @@ export function WordExportDialog({ sourcePath }: { sourcePath: string }) {
|
|||||||
{templateMode === 'custom' && (
|
{templateMode === 'custom' && (
|
||||||
<div className="rounded border border-border bg-card/20 p-2 text-xs">
|
<div className="rounded border border-border bg-card/20 p-2 text-xs">
|
||||||
{docxCustomTemplatePath ? (
|
{docxCustomTemplatePath ? (
|
||||||
<span>Template path: <code>{docxCustomTemplatePath}</code></span>
|
<span>
|
||||||
|
Template path: <code>{docxCustomTemplatePath}</code>
|
||||||
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-muted-foreground">No template selected. Click "Choose template..." to pick a .dotx file.</span>
|
<span className="text-muted-foreground">
|
||||||
|
No template selected. Click "Choose template..." to pick a .dotx file.
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
<Button variant="ghost" size="sm" onClick={handleChooseTemplate} className="ml-2">
|
<Button variant="ghost" size="sm" onClick={handleChooseTemplate} className="ml-2">
|
||||||
Choose template...
|
Choose template...
|
||||||
@@ -97,13 +119,18 @@ export function WordExportDialog({ sourcePath }: { sourcePath: string }) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{error && (
|
{error && (
|
||||||
<div role="alert" className="rounded border border-destructive/40 bg-destructive/5 p-2 text-xs text-destructive">
|
<div
|
||||||
|
role="alert"
|
||||||
|
className="rounded border border-destructive/40 bg-destructive/5 p-2 text-xs text-destructive"
|
||||||
|
>
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="ghost" onClick={closeModal} disabled={submitting}>Cancel</Button>
|
<Button variant="ghost" onClick={closeModal} disabled={submitting}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
<Button onClick={handleSubmit} disabled={submitting}>
|
<Button onClick={handleSubmit} disabled={submitting}>
|
||||||
{submitting ? 'Exporting…' : 'Export'}
|
{submitting ? 'Exporting…' : 'Export'}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { useAppStore } from '@/stores/app-store';
|
import { useAppStore } from '@/stores/app-store';
|
||||||
@@ -32,7 +38,8 @@ export function WritingAnalyticsDialog() {
|
|||||||
}, [isGoalReached, celebrated]);
|
}, [isGoalReached, celebrated]);
|
||||||
|
|
||||||
// Find max count for word cloud scaling
|
// Find max count for word cloud scaling
|
||||||
const maxWordCount = metrics.topWords.length > 0 ? Math.max(...metrics.topWords.map((w) => w.count)) : 1;
|
const maxWordCount =
|
||||||
|
metrics.topWords.length > 0 ? Math.max(...metrics.topWords.map((w) => w.count)) : 1;
|
||||||
|
|
||||||
// Helper to determine color coding for Flesch Readability Ease
|
// Helper to determine color coding for Flesch Readability Ease
|
||||||
const getReadabilityColor = (score: number) => {
|
const getReadabilityColor = (score: number) => {
|
||||||
@@ -50,7 +57,8 @@ export function WritingAnalyticsDialog() {
|
|||||||
<DialogTitle className="text-xl font-bold font-display">Writing Analytics</DialogTitle>
|
<DialogTitle className="text-xl font-bold font-display">Writing Analytics</DialogTitle>
|
||||||
</div>
|
</div>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
Real-time readability metrics, vocabulary analysis, and progress towards your daily word goal.
|
Real-time readability metrics, vocabulary analysis, and progress towards your daily word
|
||||||
|
goal.
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
@@ -75,9 +83,7 @@ export function WritingAnalyticsDialog() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<p className="text-xs text-muted-foreground">Grade Level</p>
|
<p className="text-xs text-muted-foreground">Grade Level</p>
|
||||||
<p className="text-2xl font-bold text-foreground">
|
<p className="text-2xl font-bold text-foreground">{metrics.fleschGrade}</p>
|
||||||
{metrics.fleschGrade}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-muted-foreground">US school grade</p>
|
<p className="text-xs text-muted-foreground">US school grade</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -225,12 +231,19 @@ export function WritingAnalyticsDialog() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-xs text-muted-foreground mb-4">
|
<div className="text-xs text-muted-foreground mb-4">
|
||||||
Unique words: <span className="font-semibold text-foreground">{metrics.uniqueWordCount}</span> /{' '}
|
Unique words:{' '}
|
||||||
{metrics.wordCount} ({metrics.wordCount > 0 ? Math.round((metrics.uniqueWordCount / metrics.wordCount) * 100) : 0}%)
|
<span className="font-semibold text-foreground">{metrics.uniqueWordCount}</span> /{' '}
|
||||||
|
{metrics.wordCount} (
|
||||||
|
{metrics.wordCount > 0
|
||||||
|
? Math.round((metrics.uniqueWordCount / metrics.wordCount) * 100)
|
||||||
|
: 0}
|
||||||
|
%)
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<p className="text-xs font-semibold text-muted-foreground mb-2">Word Frequency Cloud</p>
|
<p className="text-xs font-semibold text-muted-foreground mb-2">
|
||||||
|
Word Frequency Cloud
|
||||||
|
</p>
|
||||||
{metrics.topWords.length === 0 ? (
|
{metrics.topWords.length === 0 ? (
|
||||||
<div className="flex h-32 items-center justify-center rounded-lg border border-dashed border-border bg-card/10 text-xs text-muted-foreground">
|
<div className="flex h-32 items-center justify-center rounded-lg border border-dashed border-border bg-card/10 text-xs text-muted-foreground">
|
||||||
Add more words to see vocabulary statistics
|
Add more words to see vocabulary statistics
|
||||||
|
|||||||
@@ -39,7 +39,12 @@ export function MermaidLazy({ code }: Props) {
|
|||||||
};
|
};
|
||||||
}, [code]);
|
}, [code]);
|
||||||
|
|
||||||
if (error) return <div className="rounded border border-destructive/40 bg-destructive/5 p-2 text-xs text-destructive">{error}</div>;
|
if (error)
|
||||||
|
return (
|
||||||
|
<div className="rounded border border-destructive/40 bg-destructive/5 p-2 text-xs text-destructive">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
if (!svg) return <div className="text-xs text-muted-foreground">Loading diagram…</div>;
|
if (!svg) return <div className="text-xs text-muted-foreground">Loading diagram…</div>;
|
||||||
return <div data-testid="mermaid-output" dangerouslySetInnerHTML={{ __html: svg }} />;
|
return <div data-testid="mermaid-output" dangerouslySetInnerHTML={{ __html: svg }} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,13 @@ import { ChevronRight, Folder, FolderOpen, File, FileText } from 'lucide-react';
|
|||||||
import { useFileStore } from '@/stores/file-store';
|
import { useFileStore } from '@/stores/file-store';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
function FileTreeNode({ node, depth }: { node: import('@/stores/file-store').FileNode; depth: number }) {
|
function FileTreeNode({
|
||||||
|
node,
|
||||||
|
depth,
|
||||||
|
}: {
|
||||||
|
node: import('@/stores/file-store').FileNode;
|
||||||
|
depth: number;
|
||||||
|
}) {
|
||||||
const { expanded, activeTabId, loadChildren, toggleExpanded, openFile } = useFileStore();
|
const { expanded, activeTabId, loadChildren, toggleExpanded, openFile } = useFileStore();
|
||||||
const isExpanded = expanded.has(node.path);
|
const isExpanded = expanded.has(node.path);
|
||||||
const isActive = activeTabId === node.path;
|
const isActive = activeTabId === node.path;
|
||||||
|
|||||||
@@ -65,7 +65,9 @@ export function GitStatusPanel() {
|
|||||||
<div className="p-3 text-xs">
|
<div className="p-3 text-xs">
|
||||||
<div className="text-destructive">Error: {error}</div>
|
<div className="text-destructive">Error: {error}</div>
|
||||||
<p className="mt-1 text-muted-foreground">Not a git repository, or git not installed.</p>
|
<p className="mt-1 text-muted-foreground">Not a git repository, or git not installed.</p>
|
||||||
<Button size="sm" variant="ghost" onClick={load} className="mt-2">Retry</Button>
|
<Button size="sm" variant="ghost" onClick={load} className="mt-2">
|
||||||
|
Retry
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -77,7 +79,9 @@ export function GitStatusPanel() {
|
|||||||
return (
|
return (
|
||||||
<div className="p-2 text-xs">
|
<div className="p-2 text-xs">
|
||||||
<div className="flex items-center justify-between px-1 py-1">
|
<div className="flex items-center justify-between px-1 py-1">
|
||||||
<span className="font-semibold">{status.length} changed file{status.length === 1 ? '' : 's'}</span>
|
<span className="font-semibold">
|
||||||
|
{status.length} changed file{status.length === 1 ? '' : 's'}
|
||||||
|
</span>
|
||||||
<Button size="sm" variant="ghost" onClick={load} aria-label="Refresh">
|
<Button size="sm" variant="ghost" onClick={load} aria-label="Refresh">
|
||||||
<RefreshCw className="h-3 w-3" />
|
<RefreshCw className="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -35,7 +35,11 @@ export function Sidebar() {
|
|||||||
{!tree ? (
|
{!tree ? (
|
||||||
<div className="flex flex-col items-center gap-2 p-4 text-xs text-muted-foreground">
|
<div className="flex flex-col items-center gap-2 p-4 text-xs text-muted-foreground">
|
||||||
<span>No folder opened</span>
|
<span>No folder opened</span>
|
||||||
<Button size="sm" variant="outline" onClick={() => useFileStore.getState().openFolderDialog()}>
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => useFileStore.getState().openFolderDialog()}
|
||||||
|
>
|
||||||
<FolderOpen className="mr-1" /> Open Folder
|
<FolderOpen className="mr-1" /> Open Folder
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -114,11 +118,21 @@ export function Sidebar() {
|
|||||||
matching section into view. The hidden elements expose a hook for
|
matching section into view. The hidden elements expose a hook for
|
||||||
Playwright tests and the menu handler. */}
|
Playwright tests and the menu handler. */}
|
||||||
<div className="sr-only" aria-hidden="true">
|
<div className="sr-only" aria-hidden="true">
|
||||||
<button data-testid="sidebar-jump-explorer" onClick={() => scrollToSection('Files')}>jump-explorer</button>
|
<button data-testid="sidebar-jump-explorer" onClick={() => scrollToSection('Files')}>
|
||||||
<button data-testid="sidebar-jump-outline" onClick={() => scrollToSection('Outline')}>jump-outline</button>
|
jump-explorer
|
||||||
<button data-testid="sidebar-jump-snippets" onClick={() => scrollToSection('Snippets')}>jump-snippets</button>
|
</button>
|
||||||
<button data-testid="sidebar-jump-templates" onClick={() => scrollToSection('Templates')}>jump-templates</button>
|
<button data-testid="sidebar-jump-outline" onClick={() => scrollToSection('Outline')}>
|
||||||
<button data-testid="sidebar-jump-git" onClick={() => scrollToSection('Git')}>jump-git</button>
|
jump-outline
|
||||||
|
</button>
|
||||||
|
<button data-testid="sidebar-jump-snippets" onClick={() => scrollToSection('Snippets')}>
|
||||||
|
jump-snippets
|
||||||
|
</button>
|
||||||
|
<button data-testid="sidebar-jump-templates" onClick={() => scrollToSection('Templates')}>
|
||||||
|
jump-templates
|
||||||
|
</button>
|
||||||
|
<button data-testid="sidebar-jump-git" onClick={() => scrollToSection('Git')}>
|
||||||
|
jump-git
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,7 +5,11 @@ import { insertSnippet } from '@/lib/editor-commands';
|
|||||||
const TEMPLATES = [
|
const TEMPLATES = [
|
||||||
{ name: 'Blog Post', file: 'blog-post.md', description: 'Article with frontmatter' },
|
{ name: 'Blog Post', file: 'blog-post.md', description: 'Article with frontmatter' },
|
||||||
{ name: 'Meeting Notes', file: 'meeting-notes.md', description: 'Agenda, notes, action items' },
|
{ name: 'Meeting Notes', file: 'meeting-notes.md', description: 'Agenda, notes, action items' },
|
||||||
{ name: 'Technical Spec', file: 'technical-spec.md', description: 'Requirements and architecture' },
|
{
|
||||||
|
name: 'Technical Spec',
|
||||||
|
file: 'technical-spec.md',
|
||||||
|
description: 'Requirements and architecture',
|
||||||
|
},
|
||||||
{ name: 'Changelog', file: 'changelog.md', description: 'Keep a Changelog format' },
|
{ name: 'Changelog', file: 'changelog.md', description: 'Keep a Changelog format' },
|
||||||
{ name: 'README', file: 'readme.md', description: 'Project documentation' },
|
{ name: 'README', file: 'readme.md', description: 'Project documentation' },
|
||||||
{ name: 'Project Plan', file: 'project-plan.md', description: 'Goals, milestones, timeline' },
|
{ name: 'Project Plan', file: 'project-plan.md', description: 'Goals, milestones, timeline' },
|
||||||
@@ -50,7 +54,9 @@ export function Templates() {
|
|||||||
<span className="font-semibold text-foreground truncate">{t.name}</span>
|
<span className="font-semibold text-foreground truncate">{t.name}</span>
|
||||||
<Download className="h-3 w-3 opacity-0 group-hover:opacity-100 text-muted-foreground/80 hover:text-brand transition-opacity" />
|
<Download className="h-3 w-3 opacity-0 group-hover:opacity-100 text-muted-foreground/80 hover:text-brand transition-opacity" />
|
||||||
</div>
|
</div>
|
||||||
<p className="text-[10px] text-muted-foreground line-clamp-1 mt-0.5">{t.description}</p>
|
<p className="text-[10px] text-muted-foreground line-clamp-1 mt-0.5">
|
||||||
|
{t.description}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,57 +1,49 @@
|
|||||||
import * as React from "react"
|
import * as React from 'react';
|
||||||
import { Slot } from "@radix-ui/react-slot"
|
import { Slot } from '@radix-ui/react-slot';
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import { cva, type VariantProps } from 'class-variance-authority';
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
const buttonVariants = cva(
|
const buttonVariants = cva(
|
||||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default:
|
default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
|
||||||
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
|
||||||
destructive:
|
|
||||||
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
|
||||||
outline:
|
outline:
|
||||||
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
|
'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
|
||||||
secondary:
|
secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
|
||||||
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
link: 'text-primary underline-offset-4 hover:underline',
|
||||||
link: "text-primary underline-offset-4 hover:underline",
|
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
default: "h-9 px-4 py-2",
|
default: 'h-9 px-4 py-2',
|
||||||
sm: "h-8 rounded-md px-3 text-xs",
|
sm: 'h-8 rounded-md px-3 text-xs',
|
||||||
lg: "h-10 rounded-md px-8",
|
lg: 'h-10 rounded-md px-8',
|
||||||
icon: "h-9 w-9",
|
icon: 'h-9 w-9',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
variant: "default",
|
variant: 'default',
|
||||||
size: "default",
|
size: 'default',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
export interface ButtonProps
|
export interface ButtonProps
|
||||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
|
||||||
VariantProps<typeof buttonVariants> {
|
asChild?: boolean;
|
||||||
asChild?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||||
const Comp = asChild ? Slot : "button"
|
const Comp = asChild ? Slot : 'button';
|
||||||
return (
|
return (
|
||||||
<Comp
|
<Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
|
||||||
className={cn(buttonVariants({ variant, size, className }))}
|
);
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
Button.displayName = "Button"
|
Button.displayName = 'Button';
|
||||||
|
|
||||||
export { Button, buttonVariants }
|
export { Button, buttonVariants };
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import * as React from "react"
|
import * as React from 'react';
|
||||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
|
||||||
import { Check } from "lucide-react"
|
import { Check } from 'lucide-react';
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
const Checkbox = React.forwardRef<
|
const Checkbox = React.forwardRef<
|
||||||
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
||||||
@@ -10,18 +10,16 @@ const Checkbox = React.forwardRef<
|
|||||||
<CheckboxPrimitive.Root
|
<CheckboxPrimitive.Root
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
|
'peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<CheckboxPrimitive.Indicator
|
<CheckboxPrimitive.Indicator className={cn('flex items-center justify-center text-current')}>
|
||||||
className={cn("flex items-center justify-center text-current")}
|
|
||||||
>
|
|
||||||
<Check className="h-4 w-4" />
|
<Check className="h-4 w-4" />
|
||||||
</CheckboxPrimitive.Indicator>
|
</CheckboxPrimitive.Indicator>
|
||||||
</CheckboxPrimitive.Root>
|
</CheckboxPrimitive.Root>
|
||||||
))
|
));
|
||||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName
|
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
|
||||||
|
|
||||||
export { Checkbox }
|
export { Checkbox };
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
"use client"
|
'use client';
|
||||||
|
|
||||||
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
|
import * as CollapsiblePrimitive from '@radix-ui/react-collapsible';
|
||||||
|
|
||||||
const Collapsible = CollapsiblePrimitive.Root
|
const Collapsible = CollapsiblePrimitive.Root;
|
||||||
|
|
||||||
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
|
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
|
||||||
|
|
||||||
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
|
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;
|
||||||
|
|
||||||
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
|
export { Collapsible, CollapsibleTrigger, CollapsibleContent };
|
||||||
|
|||||||
@@ -1,34 +1,34 @@
|
|||||||
"use client"
|
'use client';
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from 'react';
|
||||||
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
|
import * as ContextMenuPrimitive from '@radix-ui/react-context-menu';
|
||||||
import { Check, ChevronRight, Circle } from "lucide-react"
|
import { Check, ChevronRight, Circle } from 'lucide-react';
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
const ContextMenu = ContextMenuPrimitive.Root
|
const ContextMenu = ContextMenuPrimitive.Root;
|
||||||
|
|
||||||
const ContextMenuTrigger = ContextMenuPrimitive.Trigger
|
const ContextMenuTrigger = ContextMenuPrimitive.Trigger;
|
||||||
|
|
||||||
const ContextMenuGroup = ContextMenuPrimitive.Group
|
const ContextMenuGroup = ContextMenuPrimitive.Group;
|
||||||
|
|
||||||
const ContextMenuPortal = ContextMenuPrimitive.Portal
|
const ContextMenuPortal = ContextMenuPrimitive.Portal;
|
||||||
|
|
||||||
const ContextMenuSub = ContextMenuPrimitive.Sub
|
const ContextMenuSub = ContextMenuPrimitive.Sub;
|
||||||
|
|
||||||
const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup
|
const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup;
|
||||||
|
|
||||||
const ContextMenuSubTrigger = React.forwardRef<
|
const ContextMenuSubTrigger = React.forwardRef<
|
||||||
React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
|
React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
|
||||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
|
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
|
||||||
inset?: boolean
|
inset?: boolean;
|
||||||
}
|
}
|
||||||
>(({ className, inset, children, ...props }, ref) => (
|
>(({ className, inset, children, ...props }, ref) => (
|
||||||
<ContextMenuPrimitive.SubTrigger
|
<ContextMenuPrimitive.SubTrigger
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
|
'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground',
|
||||||
inset && "pl-8",
|
inset && 'pl-8',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -36,8 +36,8 @@ const ContextMenuSubTrigger = React.forwardRef<
|
|||||||
{children}
|
{children}
|
||||||
<ChevronRight className="ml-auto h-4 w-4" />
|
<ChevronRight className="ml-auto h-4 w-4" />
|
||||||
</ContextMenuPrimitive.SubTrigger>
|
</ContextMenuPrimitive.SubTrigger>
|
||||||
))
|
));
|
||||||
ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName
|
ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName;
|
||||||
|
|
||||||
const ContextMenuSubContent = React.forwardRef<
|
const ContextMenuSubContent = React.forwardRef<
|
||||||
React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
|
React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
|
||||||
@@ -46,13 +46,13 @@ const ContextMenuSubContent = React.forwardRef<
|
|||||||
<ContextMenuPrimitive.SubContent
|
<ContextMenuPrimitive.SubContent
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-context-menu-content-transform-origin]",
|
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-context-menu-content-transform-origin]',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName
|
ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName;
|
||||||
|
|
||||||
const ContextMenuContent = React.forwardRef<
|
const ContextMenuContent = React.forwardRef<
|
||||||
React.ElementRef<typeof ContextMenuPrimitive.Content>,
|
React.ElementRef<typeof ContextMenuPrimitive.Content>,
|
||||||
@@ -62,32 +62,32 @@ const ContextMenuContent = React.forwardRef<
|
|||||||
<ContextMenuPrimitive.Content
|
<ContextMenuPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"z-50 max-h-[--radix-context-menu-content-available-height] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-context-menu-content-transform-origin]",
|
'z-50 max-h-[--radix-context-menu-content-available-height] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-context-menu-content-transform-origin]',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</ContextMenuPrimitive.Portal>
|
</ContextMenuPrimitive.Portal>
|
||||||
))
|
));
|
||||||
ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName
|
ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName;
|
||||||
|
|
||||||
const ContextMenuItem = React.forwardRef<
|
const ContextMenuItem = React.forwardRef<
|
||||||
React.ElementRef<typeof ContextMenuPrimitive.Item>,
|
React.ElementRef<typeof ContextMenuPrimitive.Item>,
|
||||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
|
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
|
||||||
inset?: boolean
|
inset?: boolean;
|
||||||
}
|
}
|
||||||
>(({ className, inset, ...props }, ref) => (
|
>(({ className, inset, ...props }, ref) => (
|
||||||
<ContextMenuPrimitive.Item
|
<ContextMenuPrimitive.Item
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||||
inset && "pl-8",
|
inset && 'pl-8',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName
|
ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName;
|
||||||
|
|
||||||
const ContextMenuCheckboxItem = React.forwardRef<
|
const ContextMenuCheckboxItem = React.forwardRef<
|
||||||
React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
|
React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
|
||||||
@@ -96,7 +96,7 @@ const ContextMenuCheckboxItem = React.forwardRef<
|
|||||||
<ContextMenuPrimitive.CheckboxItem
|
<ContextMenuPrimitive.CheckboxItem
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
checked={checked}
|
checked={checked}
|
||||||
@@ -109,9 +109,8 @@ const ContextMenuCheckboxItem = React.forwardRef<
|
|||||||
</span>
|
</span>
|
||||||
{children}
|
{children}
|
||||||
</ContextMenuPrimitive.CheckboxItem>
|
</ContextMenuPrimitive.CheckboxItem>
|
||||||
))
|
));
|
||||||
ContextMenuCheckboxItem.displayName =
|
ContextMenuCheckboxItem.displayName = ContextMenuPrimitive.CheckboxItem.displayName;
|
||||||
ContextMenuPrimitive.CheckboxItem.displayName
|
|
||||||
|
|
||||||
const ContextMenuRadioItem = React.forwardRef<
|
const ContextMenuRadioItem = React.forwardRef<
|
||||||
React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
|
React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
|
||||||
@@ -120,7 +119,7 @@ const ContextMenuRadioItem = React.forwardRef<
|
|||||||
<ContextMenuPrimitive.RadioItem
|
<ContextMenuPrimitive.RadioItem
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -132,26 +131,22 @@ const ContextMenuRadioItem = React.forwardRef<
|
|||||||
</span>
|
</span>
|
||||||
{children}
|
{children}
|
||||||
</ContextMenuPrimitive.RadioItem>
|
</ContextMenuPrimitive.RadioItem>
|
||||||
))
|
));
|
||||||
ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName
|
ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName;
|
||||||
|
|
||||||
const ContextMenuLabel = React.forwardRef<
|
const ContextMenuLabel = React.forwardRef<
|
||||||
React.ElementRef<typeof ContextMenuPrimitive.Label>,
|
React.ElementRef<typeof ContextMenuPrimitive.Label>,
|
||||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
|
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
|
||||||
inset?: boolean
|
inset?: boolean;
|
||||||
}
|
}
|
||||||
>(({ className, inset, ...props }, ref) => (
|
>(({ className, inset, ...props }, ref) => (
|
||||||
<ContextMenuPrimitive.Label
|
<ContextMenuPrimitive.Label
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn('px-2 py-1.5 text-sm font-semibold text-foreground', inset && 'pl-8', className)}
|
||||||
"px-2 py-1.5 text-sm font-semibold text-foreground",
|
|
||||||
inset && "pl-8",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName
|
ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName;
|
||||||
|
|
||||||
const ContextMenuSeparator = React.forwardRef<
|
const ContextMenuSeparator = React.forwardRef<
|
||||||
React.ElementRef<typeof ContextMenuPrimitive.Separator>,
|
React.ElementRef<typeof ContextMenuPrimitive.Separator>,
|
||||||
@@ -159,27 +154,21 @@ const ContextMenuSeparator = React.forwardRef<
|
|||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<ContextMenuPrimitive.Separator
|
<ContextMenuPrimitive.Separator
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn("-mx-1 my-1 h-px bg-border", className)}
|
className={cn('-mx-1 my-1 h-px bg-border', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName
|
ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName;
|
||||||
|
|
||||||
const ContextMenuShortcut = ({
|
const ContextMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn('ml-auto text-xs tracking-widest text-muted-foreground', className)}
|
||||||
"ml-auto text-xs tracking-widest text-muted-foreground",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
ContextMenuShortcut.displayName = "ContextMenuShortcut"
|
ContextMenuShortcut.displayName = 'ContextMenuShortcut';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
@@ -197,4 +186,4 @@ export {
|
|||||||
ContextMenuSubContent,
|
ContextMenuSubContent,
|
||||||
ContextMenuSubTrigger,
|
ContextMenuSubTrigger,
|
||||||
ContextMenuRadioGroup,
|
ContextMenuRadioGroup,
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,34 +1,26 @@
|
|||||||
"use client"
|
'use client';
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from 'react';
|
||||||
import { XIcon } from "lucide-react"
|
import { XIcon } from 'lucide-react';
|
||||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from '@/lib/utils';
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from '@/components/ui/button';
|
||||||
|
|
||||||
function Dialog({
|
function Dialog({ ...props }: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||||
...props
|
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
|
||||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogTrigger({
|
function DialogTrigger({ ...props }: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||||
...props
|
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
|
||||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogPortal({
|
function DialogPortal({ ...props }: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||||
...props
|
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
|
||||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogClose({
|
function DialogClose({ ...props }: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||||
...props
|
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
|
||||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogOverlay({
|
function DialogOverlay({
|
||||||
@@ -39,12 +31,12 @@ function DialogOverlay({
|
|||||||
<DialogPrimitive.Overlay
|
<DialogPrimitive.Overlay
|
||||||
data-slot="dialog-overlay"
|
data-slot="dialog-overlay"
|
||||||
className={cn(
|
className={cn(
|
||||||
"fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0",
|
'fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogContent({
|
function DialogContent({
|
||||||
@@ -53,7 +45,7 @@ function DialogContent({
|
|||||||
showCloseButton = true,
|
showCloseButton = true,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||||
showCloseButton?: boolean
|
showCloseButton?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<DialogPortal data-slot="dialog-portal">
|
<DialogPortal data-slot="dialog-portal">
|
||||||
@@ -61,7 +53,7 @@ function DialogContent({
|
|||||||
<DialogPrimitive.Content
|
<DialogPrimitive.Content
|
||||||
data-slot="dialog-content"
|
data-slot="dialog-content"
|
||||||
className={cn(
|
className={cn(
|
||||||
"fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border bg-background p-6 shadow-lg duration-200 outline-none data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 sm:max-w-lg",
|
'fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border bg-background p-6 shadow-lg duration-200 outline-none data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 sm:max-w-lg',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -78,17 +70,17 @@ function DialogContent({
|
|||||||
)}
|
)}
|
||||||
</DialogPrimitive.Content>
|
</DialogPrimitive.Content>
|
||||||
</DialogPortal>
|
</DialogPortal>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="dialog-header"
|
data-slot="dialog-header"
|
||||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
className={cn('flex flex-col gap-2 text-center sm:text-left', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogFooter({
|
function DialogFooter({
|
||||||
@@ -96,16 +88,13 @@ function DialogFooter({
|
|||||||
showCloseButton = false,
|
showCloseButton = false,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"div"> & {
|
}: React.ComponentProps<'div'> & {
|
||||||
showCloseButton?: boolean
|
showCloseButton?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="dialog-footer"
|
data-slot="dialog-footer"
|
||||||
className={cn(
|
className={cn('flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', className)}
|
||||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
@@ -115,20 +104,17 @@ function DialogFooter({
|
|||||||
</DialogPrimitive.Close>
|
</DialogPrimitive.Close>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogTitle({
|
function DialogTitle({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
|
||||||
return (
|
return (
|
||||||
<DialogPrimitive.Title
|
<DialogPrimitive.Title
|
||||||
data-slot="dialog-title"
|
data-slot="dialog-title"
|
||||||
className={cn("text-lg leading-none font-semibold", className)}
|
className={cn('text-lg leading-none font-semibold', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogDescription({
|
function DialogDescription({
|
||||||
@@ -138,10 +124,10 @@ function DialogDescription({
|
|||||||
return (
|
return (
|
||||||
<DialogPrimitive.Description
|
<DialogPrimitive.Description
|
||||||
data-slot="dialog-description"
|
data-slot="dialog-description"
|
||||||
className={cn("text-sm text-muted-foreground", className)}
|
className={cn('text-sm text-muted-foreground', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@@ -155,4 +141,4 @@ export {
|
|||||||
DialogPortal,
|
DialogPortal,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,75 +1,71 @@
|
|||||||
"use client"
|
'use client';
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from 'react';
|
||||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
import * as LabelPrimitive from '@radix-ui/react-label';
|
||||||
import { Slot } from "@radix-ui/react-slot"
|
import { Slot } from '@radix-ui/react-slot';
|
||||||
import {
|
import {
|
||||||
Controller,
|
Controller,
|
||||||
type ControllerProps,
|
type ControllerProps,
|
||||||
type FieldPath,
|
type FieldPath,
|
||||||
type FieldValues,
|
type FieldValues,
|
||||||
useFormContext,
|
useFormContext,
|
||||||
} from "react-hook-form"
|
} from 'react-hook-form';
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
const Form = React.forwardRef<
|
const Form = React.forwardRef<HTMLFormElement, React.ComponentProps<'form'>>(
|
||||||
HTMLFormElement,
|
({ className, ...props }, ref) => {
|
||||||
React.ComponentProps<"form">
|
const methods = useFormContext();
|
||||||
>(({ className, ...props }, ref) => {
|
|
||||||
const methods = useFormContext()
|
|
||||||
return (
|
return (
|
||||||
<form ref={ref} className={className} onSubmit={methods.handleSubmit(() => {})} {...props} />
|
<form ref={ref} className={className} onSubmit={methods.handleSubmit(() => {})} {...props} />
|
||||||
)
|
);
|
||||||
})
|
}
|
||||||
Form.displayName = "Form"
|
);
|
||||||
|
Form.displayName = 'Form';
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// FormFieldContext - provides field-level context
|
// FormFieldContext - provides field-level context
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
const FormFieldContext = React.createContext<
|
const FormFieldContext = React.createContext<ControllerProps<FieldValues, string>>({} as never);
|
||||||
ControllerProps<FieldValues, string>
|
|
||||||
>({} as never)
|
|
||||||
|
|
||||||
const FormField = <
|
const FormField = <
|
||||||
TFieldValues extends FieldValues = FieldValues,
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||||
>({
|
>({
|
||||||
...props
|
...props
|
||||||
}: ControllerProps<TFieldValues, TName>) => {
|
}: ControllerProps<TFieldValues, TName>) => {
|
||||||
const { name } = props
|
const { name } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormFieldContext.Provider value={props as ControllerProps<FieldValues, string>}>
|
<FormFieldContext.Provider value={props as ControllerProps<FieldValues, string>}>
|
||||||
<Controller {...props} />
|
<Controller {...props} />
|
||||||
</FormFieldContext.Provider>
|
</FormFieldContext.Provider>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// useFormField - must be used inside a FormField
|
// useFormField - must be used inside a FormField
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
function useFormField(): ControllerProps<FieldValues, string> {
|
function useFormField(): ControllerProps<FieldValues, string> {
|
||||||
const fieldContext = React.useContext(FormFieldContext)
|
const fieldContext = React.useContext(FormFieldContext);
|
||||||
if (!fieldContext) {
|
if (!fieldContext) {
|
||||||
throw new Error("useFormField must be used within a FormField")
|
throw new Error('useFormField must be used within a FormField');
|
||||||
}
|
}
|
||||||
return fieldContext
|
return fieldContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// FormItem - wraps a labeled form control
|
// FormItem - wraps a labeled form control
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
const FormItem = React.forwardRef<
|
const FormItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||||
HTMLDivElement,
|
({ className, ...props }, ref) => (
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
<div ref={ref} className={cn('space-y-2', className)} {...props} />
|
||||||
>(({ className, ...props }, ref) => (
|
)
|
||||||
<div ref={ref} className={cn("space-y-2", className)} {...props} />
|
);
|
||||||
))
|
FormItem.displayName = 'FormItem';
|
||||||
FormItem.displayName = "FormItem"
|
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// FormLabel
|
// FormLabel
|
||||||
@@ -82,13 +78,13 @@ const FormLabel = React.forwardRef<
|
|||||||
<LabelPrimitive.Root
|
<LabelPrimitive.Root
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
|
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
FormLabel.displayName = LabelPrimitive.Root.displayName
|
FormLabel.displayName = LabelPrimitive.Root.displayName;
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// FormControl - Slot bridge
|
// FormControl - Slot bridge
|
||||||
@@ -97,10 +93,8 @@ FormLabel.displayName = LabelPrimitive.Root.displayName
|
|||||||
const FormControl = React.forwardRef<
|
const FormControl = React.forwardRef<
|
||||||
React.ElementRef<typeof Slot>,
|
React.ElementRef<typeof Slot>,
|
||||||
React.ComponentPropsWithoutRef<typeof Slot>
|
React.ComponentPropsWithoutRef<typeof Slot>
|
||||||
>(({ ...props }, ref) => (
|
>(({ ...props }, ref) => <Slot ref={ref} {...props} />);
|
||||||
<Slot ref={ref} {...props} />
|
FormControl.displayName = 'FormControl';
|
||||||
))
|
|
||||||
FormControl.displayName = "FormControl"
|
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// FormDescription
|
// FormDescription
|
||||||
@@ -110,13 +104,9 @@ const FormDescription = React.forwardRef<
|
|||||||
HTMLParagraphElement,
|
HTMLParagraphElement,
|
||||||
React.HTMLAttributes<HTMLParagraphElement>
|
React.HTMLAttributes<HTMLParagraphElement>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<p
|
<p ref={ref} className={cn('text-sm text-muted-foreground', className)} {...props} />
|
||||||
ref={ref}
|
));
|
||||||
className={cn("text-sm text-muted-foreground", className)}
|
FormDescription.displayName = 'FormDescription';
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
FormDescription.displayName = "FormDescription"
|
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// FormMessage
|
// FormMessage
|
||||||
@@ -126,20 +116,16 @@ const FormMessage = React.forwardRef<
|
|||||||
HTMLParagraphElement,
|
HTMLParagraphElement,
|
||||||
React.HTMLAttributes<HTMLParagraphElement>
|
React.HTMLAttributes<HTMLParagraphElement>
|
||||||
>(({ className, children, ...props }, ref) => {
|
>(({ className, children, ...props }, ref) => {
|
||||||
const { error } = useFormField()
|
const { error } = useFormField();
|
||||||
const body = error ? String(error.message ?? "") : children
|
const body = error ? String(error.message ?? '') : children;
|
||||||
if (!body) return null
|
if (!body) return null;
|
||||||
return (
|
return (
|
||||||
<p
|
<p ref={ref} className={cn('text-sm font-medium text-destructive', className)} {...props}>
|
||||||
ref={ref}
|
|
||||||
className={cn("text-sm font-medium text-destructive", className)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{body}
|
{body}
|
||||||
</p>
|
</p>
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
FormMessage.displayName = "FormMessage"
|
FormMessage.displayName = 'FormMessage';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Form,
|
Form,
|
||||||
@@ -150,4 +136,4 @@ export {
|
|||||||
FormDescription,
|
FormDescription,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
useFormField,
|
useFormField,
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import * as React from "react"
|
import * as React from 'react';
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
export interface InputProps
|
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
|
||||||
|
|
||||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||||
({ className, type, ...props }, ref) => {
|
({ className, type, ...props }, ref) => {
|
||||||
@@ -11,15 +10,15 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|||||||
<input
|
<input
|
||||||
type={type}
|
type={type}
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
'h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
Input.displayName = "Input"
|
Input.displayName = 'Input';
|
||||||
|
|
||||||
export { Input }
|
export { Input };
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as React from "react"
|
import * as React from 'react';
|
||||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
import * as LabelPrimitive from '@radix-ui/react-label';
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
const Label = React.forwardRef<
|
const Label = React.forwardRef<
|
||||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||||
@@ -9,12 +9,12 @@ const Label = React.forwardRef<
|
|||||||
<LabelPrimitive.Root
|
<LabelPrimitive.Root
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
|
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
Label.displayName = LabelPrimitive.Root.displayName
|
Label.displayName = LabelPrimitive.Root.displayName;
|
||||||
|
|
||||||
export { Label }
|
export { Label };
|
||||||
|
|||||||
@@ -1,21 +1,15 @@
|
|||||||
import * as React from "react"
|
import * as React from 'react';
|
||||||
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
|
import * as RadioGroupPrimitive from '@radix-ui/react-radio-group';
|
||||||
import { Circle } from "lucide-react"
|
import { Circle } from 'lucide-react';
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
const RadioGroup = React.forwardRef<
|
const RadioGroup = React.forwardRef<
|
||||||
React.ElementRef<typeof RadioGroupPrimitive.Root>,
|
React.ElementRef<typeof RadioGroupPrimitive.Root>,
|
||||||
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
|
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
|
||||||
>(({ className, ...props }, ref) => {
|
>(({ className, ...props }, ref) => {
|
||||||
return (
|
return <RadioGroupPrimitive.Root className={cn('grid gap-2', className)} {...props} ref={ref} />;
|
||||||
<RadioGroupPrimitive.Root
|
});
|
||||||
className={cn("grid gap-2", className)}
|
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
|
||||||
{...props}
|
|
||||||
ref={ref}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
|
|
||||||
|
|
||||||
const RadioGroupItem = React.forwardRef<
|
const RadioGroupItem = React.forwardRef<
|
||||||
React.ElementRef<typeof RadioGroupPrimitive.Item>,
|
React.ElementRef<typeof RadioGroupPrimitive.Item>,
|
||||||
@@ -25,7 +19,7 @@ const RadioGroupItem = React.forwardRef<
|
|||||||
<RadioGroupPrimitive.Item
|
<RadioGroupPrimitive.Item
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
'aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -34,8 +28,8 @@ const RadioGroupItem = React.forwardRef<
|
|||||||
<Circle className="h-2.5 w-2.5 fill-current text-current" />
|
<Circle className="h-2.5 w-2.5 fill-current text-current" />
|
||||||
</RadioGroupPrimitive.Indicator>
|
</RadioGroupPrimitive.Indicator>
|
||||||
</RadioGroupPrimitive.Item>
|
</RadioGroupPrimitive.Item>
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
|
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;
|
||||||
|
|
||||||
export { RadioGroup, RadioGroupItem }
|
export { RadioGroup, RadioGroupItem };
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
"use client"
|
'use client';
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from 'react';
|
||||||
import { GripVertical } from "lucide-react"
|
import { GripVertical } from 'lucide-react';
|
||||||
import * as ResizablePrimitive from "react-resizable-panels"
|
import * as ResizablePrimitive from 'react-resizable-panels';
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
const ResizablePanelGroup = React.forwardRef<
|
const ResizablePanelGroup = React.forwardRef<
|
||||||
React.ElementRef<typeof ResizablePrimitive.Group>,
|
React.ElementRef<typeof ResizablePrimitive.Group>,
|
||||||
@@ -12,14 +12,11 @@ const ResizablePanelGroup = React.forwardRef<
|
|||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<ResizablePrimitive.Group
|
<ResizablePrimitive.Group
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn('flex h-full w-full data-[panel-group-direction=vertical]:flex-col', className)}
|
||||||
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
ResizablePanelGroup.displayName = "ResizablePanelGroup"
|
ResizablePanelGroup.displayName = 'ResizablePanelGroup';
|
||||||
|
|
||||||
const ResizablePanel = React.forwardRef<
|
const ResizablePanel = React.forwardRef<
|
||||||
React.ElementRef<typeof ResizablePrimitive.Panel>,
|
React.ElementRef<typeof ResizablePrimitive.Panel>,
|
||||||
@@ -27,25 +24,22 @@ const ResizablePanel = React.forwardRef<
|
|||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<ResizablePrimitive.Panel
|
<ResizablePrimitive.Panel
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn('h-full w-full overflow-hidden', className)}
|
||||||
"h-full w-full overflow-hidden",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
ResizablePanel.displayName = "ResizablePanel"
|
ResizablePanel.displayName = 'ResizablePanel';
|
||||||
|
|
||||||
const ResizableHandle = ({
|
const ResizableHandle = ({
|
||||||
withHandle,
|
withHandle,
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentPropsWithoutRef<typeof ResizablePrimitive.Separator> & {
|
}: React.ComponentPropsWithoutRef<typeof ResizablePrimitive.Separator> & {
|
||||||
withHandle?: boolean
|
withHandle?: boolean;
|
||||||
}) => (
|
}) => (
|
||||||
<ResizablePrimitive.Separator
|
<ResizablePrimitive.Separator
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90",
|
'relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -56,7 +50,7 @@ const ResizableHandle = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</ResizablePrimitive.Separator>
|
</ResizablePrimitive.Separator>
|
||||||
)
|
);
|
||||||
ResizableHandle.displayName = "ResizableHandle"
|
ResizableHandle.displayName = 'ResizableHandle';
|
||||||
|
|
||||||
export { ResizablePanelGroup, ResizablePanel, ResizableHandle }
|
export { ResizablePanelGroup, ResizablePanel, ResizableHandle };
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
"use client"
|
'use client';
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from 'react';
|
||||||
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
|
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
const ScrollArea = React.forwardRef<
|
const ScrollArea = React.forwardRef<
|
||||||
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
||||||
@@ -11,7 +11,7 @@ const ScrollArea = React.forwardRef<
|
|||||||
>(({ className, children, ...props }, ref) => (
|
>(({ className, children, ...props }, ref) => (
|
||||||
<ScrollAreaPrimitive.Root
|
<ScrollAreaPrimitive.Root
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn("relative overflow-hidden", className)}
|
className={cn('relative overflow-hidden', className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
|
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
|
||||||
@@ -20,28 +20,26 @@ const ScrollArea = React.forwardRef<
|
|||||||
<ScrollBar />
|
<ScrollBar />
|
||||||
<ScrollAreaPrimitive.Corner />
|
<ScrollAreaPrimitive.Corner />
|
||||||
</ScrollAreaPrimitive.Root>
|
</ScrollAreaPrimitive.Root>
|
||||||
))
|
));
|
||||||
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
|
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
|
||||||
|
|
||||||
const ScrollBar = React.forwardRef<
|
const ScrollBar = React.forwardRef<
|
||||||
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
||||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
|
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||||
>(({ className, orientation = "vertical", ...props }, ref) => (
|
>(({ className, orientation = 'vertical', ...props }, ref) => (
|
||||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||||
ref={ref}
|
ref={ref}
|
||||||
orientation={orientation}
|
orientation={orientation}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex touch-none select-none transition-colors",
|
'flex touch-none select-none transition-colors',
|
||||||
orientation === "vertical" &&
|
orientation === 'vertical' && 'h-full w-2.5 border-l border-l-transparent p-[1px]',
|
||||||
"h-full w-2.5 border-l border-l-transparent p-[1px]",
|
orientation === 'horizontal' && 'h-2.5 flex-col border-t border-t-transparent p-[1px]',
|
||||||
orientation === "horizontal" &&
|
|
||||||
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
|
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
|
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
|
||||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||||
))
|
));
|
||||||
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
|
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
|
||||||
export { ScrollArea, ScrollBar }
|
export { ScrollArea, ScrollBar };
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
"use client"
|
'use client';
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from 'react';
|
||||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
import * as SelectPrimitive from '@radix-ui/react-select';
|
||||||
import { Check, ChevronDown, ChevronUp } from "lucide-react"
|
import { Check, ChevronDown, ChevronUp } from 'lucide-react';
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
const Select = SelectPrimitive.Root
|
const Select = SelectPrimitive.Root;
|
||||||
|
|
||||||
const SelectGroup = SelectPrimitive.Group
|
const SelectGroup = SelectPrimitive.Group;
|
||||||
|
|
||||||
const SelectValue = SelectPrimitive.Value
|
const SelectValue = SelectPrimitive.Value;
|
||||||
|
|
||||||
const SelectTrigger = React.forwardRef<
|
const SelectTrigger = React.forwardRef<
|
||||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||||
@@ -18,7 +18,7 @@ const SelectTrigger = React.forwardRef<
|
|||||||
<SelectPrimitive.Trigger
|
<SelectPrimitive.Trigger
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -28,8 +28,8 @@ const SelectTrigger = React.forwardRef<
|
|||||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||||
</SelectPrimitive.Icon>
|
</SelectPrimitive.Icon>
|
||||||
</SelectPrimitive.Trigger>
|
</SelectPrimitive.Trigger>
|
||||||
))
|
));
|
||||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
|
||||||
|
|
||||||
const SelectScrollUpButton = React.forwardRef<
|
const SelectScrollUpButton = React.forwardRef<
|
||||||
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
||||||
@@ -37,16 +37,13 @@ const SelectScrollUpButton = React.forwardRef<
|
|||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<SelectPrimitive.ScrollUpButton
|
<SelectPrimitive.ScrollUpButton
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn('flex cursor-default items-center justify-center py-1', className)}
|
||||||
"flex cursor-default items-center justify-center py-1",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ChevronUp className="h-4 w-4" />
|
<ChevronUp className="h-4 w-4" />
|
||||||
</SelectPrimitive.ScrollUpButton>
|
</SelectPrimitive.ScrollUpButton>
|
||||||
))
|
));
|
||||||
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
|
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
|
||||||
|
|
||||||
const SelectScrollDownButton = React.forwardRef<
|
const SelectScrollDownButton = React.forwardRef<
|
||||||
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
||||||
@@ -54,29 +51,25 @@ const SelectScrollDownButton = React.forwardRef<
|
|||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<SelectPrimitive.ScrollDownButton
|
<SelectPrimitive.ScrollDownButton
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn('flex cursor-default items-center justify-center py-1', className)}
|
||||||
"flex cursor-default items-center justify-center py-1",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ChevronDown className="h-4 w-4" />
|
<ChevronDown className="h-4 w-4" />
|
||||||
</SelectPrimitive.ScrollDownButton>
|
</SelectPrimitive.ScrollDownButton>
|
||||||
))
|
));
|
||||||
SelectScrollDownButton.displayName =
|
SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName;
|
||||||
SelectPrimitive.ScrollDownButton.displayName
|
|
||||||
|
|
||||||
const SelectContent = React.forwardRef<
|
const SelectContent = React.forwardRef<
|
||||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||||
>(({ className, children, position = "popper", ...props }, ref) => (
|
>(({ className, children, position = 'popper', ...props }, ref) => (
|
||||||
<SelectPrimitive.Portal>
|
<SelectPrimitive.Portal>
|
||||||
<SelectPrimitive.Content
|
<SelectPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
'relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||||
position === "popper" &&
|
position === 'popper' &&
|
||||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
position={position}
|
position={position}
|
||||||
@@ -85,9 +78,9 @@ const SelectContent = React.forwardRef<
|
|||||||
<SelectScrollUpButton />
|
<SelectScrollUpButton />
|
||||||
<SelectPrimitive.Viewport
|
<SelectPrimitive.Viewport
|
||||||
className={cn(
|
className={cn(
|
||||||
"p-1",
|
'p-1',
|
||||||
position === "popper" &&
|
position === 'popper' &&
|
||||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
|
'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
@@ -95,8 +88,8 @@ const SelectContent = React.forwardRef<
|
|||||||
<SelectScrollDownButton />
|
<SelectScrollDownButton />
|
||||||
</SelectPrimitive.Content>
|
</SelectPrimitive.Content>
|
||||||
</SelectPrimitive.Portal>
|
</SelectPrimitive.Portal>
|
||||||
))
|
));
|
||||||
SelectContent.displayName = SelectPrimitive.Content.displayName
|
SelectContent.displayName = SelectPrimitive.Content.displayName;
|
||||||
|
|
||||||
const SelectLabel = React.forwardRef<
|
const SelectLabel = React.forwardRef<
|
||||||
React.ElementRef<typeof SelectPrimitive.Label>,
|
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||||
@@ -104,11 +97,11 @@ const SelectLabel = React.forwardRef<
|
|||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<SelectPrimitive.Label
|
<SelectPrimitive.Label
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
|
className={cn('py-1.5 pl-8 pr-2 text-sm font-semibold', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
SelectLabel.displayName = SelectPrimitive.Label.displayName;
|
||||||
|
|
||||||
const SelectItem = React.forwardRef<
|
const SelectItem = React.forwardRef<
|
||||||
React.ElementRef<typeof SelectPrimitive.Item>,
|
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||||
@@ -117,7 +110,7 @@ const SelectItem = React.forwardRef<
|
|||||||
<SelectPrimitive.Item
|
<SelectPrimitive.Item
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -130,8 +123,8 @@ const SelectItem = React.forwardRef<
|
|||||||
|
|
||||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||||
</SelectPrimitive.Item>
|
</SelectPrimitive.Item>
|
||||||
))
|
));
|
||||||
SelectItem.displayName = SelectPrimitive.Item.displayName
|
SelectItem.displayName = SelectPrimitive.Item.displayName;
|
||||||
|
|
||||||
const SelectSeparator = React.forwardRef<
|
const SelectSeparator = React.forwardRef<
|
||||||
React.ElementRef<typeof SelectPrimitive.Separator>,
|
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||||
@@ -139,11 +132,11 @@ const SelectSeparator = React.forwardRef<
|
|||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<SelectPrimitive.Separator
|
<SelectPrimitive.Separator
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
className={cn('-mx-1 my-1 h-px bg-muted', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Select,
|
Select,
|
||||||
@@ -156,4 +149,4 @@ export {
|
|||||||
SelectSeparator,
|
SelectSeparator,
|
||||||
SelectScrollUpButton,
|
SelectScrollUpButton,
|
||||||
SelectScrollDownButton,
|
SelectScrollDownButton,
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,25 +1,22 @@
|
|||||||
"use client"
|
'use client';
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from 'react';
|
||||||
import * as SheetPrimitive from "@radix-ui/react-dialog"
|
import * as SheetPrimitive from '@radix-ui/react-dialog';
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import { cva, type VariantProps } from 'class-variance-authority';
|
||||||
import { X } from "lucide-react"
|
import { X } from 'lucide-react';
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
const Sheet = SheetPrimitive.Root
|
const Sheet = SheetPrimitive.Root;
|
||||||
|
|
||||||
const SheetTrigger = SheetPrimitive.Trigger
|
const SheetTrigger = SheetPrimitive.Trigger;
|
||||||
|
|
||||||
const SheetClose = SheetPrimitive.Close
|
const SheetClose = SheetPrimitive.Close;
|
||||||
|
|
||||||
const SheetPortal = ({
|
const SheetPortal = ({ className, ...props }: SheetPrimitive.DialogPortalProps) => (
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: SheetPrimitive.DialogPortalProps) => (
|
|
||||||
<SheetPrimitive.Portal className={cn(className)} {...props} />
|
<SheetPrimitive.Portal className={cn(className)} {...props} />
|
||||||
)
|
);
|
||||||
SheetPortal.displayName = SheetPrimitive.Portal.displayName
|
SheetPortal.displayName = SheetPrimitive.Portal.displayName;
|
||||||
|
|
||||||
const SheetOverlay = React.forwardRef<
|
const SheetOverlay = React.forwardRef<
|
||||||
React.ElementRef<typeof SheetPrimitive.Overlay>,
|
React.ElementRef<typeof SheetPrimitive.Overlay>,
|
||||||
@@ -27,49 +24,46 @@ const SheetOverlay = React.forwardRef<
|
|||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<SheetPrimitive.Overlay
|
<SheetPrimitive.Overlay
|
||||||
className={cn(
|
className={cn(
|
||||||
"fixed inset-0 z-50 bg-background/80 backdrop-blur-sm data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0",
|
'fixed inset-0 z-50 bg-background/80 backdrop-blur-sm data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
|
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
|
||||||
|
|
||||||
const sheetVariants = cva(
|
const sheetVariants = cva(
|
||||||
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:animate-in data-[state=open]:duration-500",
|
'fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:animate-in data-[state=open]:duration-500',
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
side: {
|
side: {
|
||||||
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
|
top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',
|
||||||
bottom:
|
bottom:
|
||||||
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
|
'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',
|
||||||
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
|
left: 'inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm',
|
||||||
right:
|
right:
|
||||||
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm"
|
'inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
side: "right"
|
side: 'right',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
);
|
||||||
)
|
|
||||||
|
|
||||||
interface SheetContentProps
|
interface SheetContentProps
|
||||||
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
extends
|
||||||
|
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
||||||
VariantProps<typeof sheetVariants> {}
|
VariantProps<typeof sheetVariants> {}
|
||||||
|
|
||||||
const SheetContent = React.forwardRef<
|
const SheetContent = React.forwardRef<
|
||||||
React.ElementRef<typeof SheetPrimitive.Content>,
|
React.ElementRef<typeof SheetPrimitive.Content>,
|
||||||
SheetContentProps
|
SheetContentProps
|
||||||
>(({ side = "right", className, children, ...props }, ref) => (
|
>(({ side = 'right', className, children, ...props }, ref) => (
|
||||||
<SheetPortal>
|
<SheetPortal>
|
||||||
<SheetOverlay />
|
<SheetOverlay />
|
||||||
<SheetPrimitive.Content
|
<SheetPrimitive.Content ref={ref} className={cn(sheetVariants({ side }), className)} {...props}>
|
||||||
ref={ref}
|
|
||||||
className={cn(sheetVariants({ side }), className)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
<SheetPrimitive.Close className="absolute top-4 right-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:outline-none disabled:pointer-events-none data-[state=open]:bg-secondary">
|
<SheetPrimitive.Close className="absolute top-4 right-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:outline-none disabled:pointer-events-none data-[state=open]:bg-secondary">
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
@@ -77,36 +71,21 @@ const SheetContent = React.forwardRef<
|
|||||||
</SheetPrimitive.Close>
|
</SheetPrimitive.Close>
|
||||||
</SheetPrimitive.Content>
|
</SheetPrimitive.Content>
|
||||||
</SheetPortal>
|
</SheetPortal>
|
||||||
))
|
));
|
||||||
SheetContent.displayName = SheetPrimitive.Content.displayName
|
SheetContent.displayName = SheetPrimitive.Content.displayName;
|
||||||
|
|
||||||
const SheetHeader = ({
|
const SheetHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
className,
|
<div className={cn('flex flex-col space-y-2 text-center sm:text-left', className)} {...props} />
|
||||||
...props
|
);
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
SheetHeader.displayName = 'SheetHeader';
|
||||||
|
|
||||||
|
const SheetFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)}
|
||||||
"flex flex-col space-y-2 text-center sm:text-left",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
SheetHeader.displayName = "SheetHeader"
|
SheetFooter.displayName = 'SheetFooter';
|
||||||
|
|
||||||
const SheetFooter = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
SheetFooter.displayName = "SheetFooter"
|
|
||||||
|
|
||||||
const SheetTitle = React.forwardRef<
|
const SheetTitle = React.forwardRef<
|
||||||
React.ElementRef<typeof SheetPrimitive.Title>,
|
React.ElementRef<typeof SheetPrimitive.Title>,
|
||||||
@@ -114,11 +93,11 @@ const SheetTitle = React.forwardRef<
|
|||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<SheetPrimitive.Title
|
<SheetPrimitive.Title
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn("text-lg font-semibold text-foreground", className)}
|
className={cn('text-lg font-semibold text-foreground', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
SheetTitle.displayName = SheetPrimitive.Title.displayName
|
SheetTitle.displayName = SheetPrimitive.Title.displayName;
|
||||||
|
|
||||||
const SheetDescription = React.forwardRef<
|
const SheetDescription = React.forwardRef<
|
||||||
React.ElementRef<typeof SheetPrimitive.Description>,
|
React.ElementRef<typeof SheetPrimitive.Description>,
|
||||||
@@ -126,11 +105,11 @@ const SheetDescription = React.forwardRef<
|
|||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<SheetPrimitive.Description
|
<SheetPrimitive.Description
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn("text-sm text-muted-foreground", className)}
|
className={cn('text-sm text-muted-foreground', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
SheetDescription.displayName = SheetPrimitive.Description.displayName
|
SheetDescription.displayName = SheetPrimitive.Description.displayName;
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Sheet,
|
Sheet,
|
||||||
@@ -142,5 +121,5 @@ export {
|
|||||||
SheetHeader,
|
SheetHeader,
|
||||||
SheetFooter,
|
SheetFooter,
|
||||||
SheetTitle,
|
SheetTitle,
|
||||||
SheetDescription
|
SheetDescription,
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
"use client"
|
'use client';
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from 'react';
|
||||||
import * as SliderPrimitive from "@radix-ui/react-slider"
|
import * as SliderPrimitive from '@radix-ui/react-slider';
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
const Slider = React.forwardRef<
|
const Slider = React.forwardRef<
|
||||||
React.ElementRef<typeof SliderPrimitive.Root>,
|
React.ElementRef<typeof SliderPrimitive.Root>,
|
||||||
@@ -12,10 +12,7 @@ const Slider = React.forwardRef<
|
|||||||
return (
|
return (
|
||||||
<SliderPrimitive.Root
|
<SliderPrimitive.Root
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn('relative flex w-full touch-none select-none items-center', className)}
|
||||||
"relative flex w-full touch-none select-none items-center",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary">
|
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary">
|
||||||
@@ -27,7 +24,7 @@ const Slider = React.forwardRef<
|
|||||||
/>
|
/>
|
||||||
</SliderPrimitive.Root>
|
</SliderPrimitive.Root>
|
||||||
);
|
);
|
||||||
})
|
});
|
||||||
Slider.displayName = SliderPrimitive.Root.displayName
|
Slider.displayName = SliderPrimitive.Root.displayName;
|
||||||
|
|
||||||
export { Slider }
|
export { Slider };
|
||||||
|
|||||||
@@ -1,26 +1,24 @@
|
|||||||
"use client"
|
'use client';
|
||||||
|
|
||||||
import { useTheme } from "next-themes"
|
import { useTheme } from 'next-themes';
|
||||||
import { Toaster as Sonner } from "sonner"
|
import { Toaster as Sonner } from 'sonner';
|
||||||
|
|
||||||
type ToasterProps = React.ComponentProps<typeof Sonner>
|
type ToasterProps = React.ComponentProps<typeof Sonner>;
|
||||||
|
|
||||||
const Toaster = ({ ...props }: ToasterProps) => {
|
const Toaster = ({ ...props }: ToasterProps) => {
|
||||||
const { theme = "system" } = useTheme()
|
const { theme = 'system' } = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sonner
|
<Sonner
|
||||||
theme={theme as ToasterProps["theme"]}
|
theme={theme as ToasterProps['theme']}
|
||||||
className="toaster group"
|
className="toaster group"
|
||||||
toastOptions={{
|
toastOptions={{
|
||||||
classNames: {
|
classNames: {
|
||||||
toast:
|
toast:
|
||||||
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
|
'group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg',
|
||||||
description: "group-[.toast]:text-muted-foreground",
|
description: 'group-[.toast]:text-muted-foreground',
|
||||||
actionButton:
|
actionButton: 'group-[.toast]:bg-primary group-[.toast]:text-primary-foreground',
|
||||||
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
|
cancelButton: 'group-[.toast]:bg-muted group-[.toast]:text-muted-foreground',
|
||||||
cancelButton:
|
|
||||||
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
|
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
richColors
|
richColors
|
||||||
@@ -28,7 +26,7 @@ const Toaster = ({ ...props }: ToasterProps) => {
|
|||||||
position="bottom-right"
|
position="bottom-right"
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export { Toaster }
|
export { Toaster };
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as React from "react"
|
import * as React from 'react';
|
||||||
import * as SwitchPrimitive from "@radix-ui/react-switch"
|
import * as SwitchPrimitive from '@radix-ui/react-switch';
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
const Switch = React.forwardRef<
|
const Switch = React.forwardRef<
|
||||||
React.ElementRef<typeof SwitchPrimitive.Root>,
|
React.ElementRef<typeof SwitchPrimitive.Root>,
|
||||||
@@ -9,18 +9,18 @@ const Switch = React.forwardRef<
|
|||||||
<SwitchPrimitive.Root
|
<SwitchPrimitive.Root
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
'peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<SwitchPrimitive.Thumb
|
<SwitchPrimitive.Thumb
|
||||||
className={cn(
|
className={cn(
|
||||||
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
|
'pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0'
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</SwitchPrimitive.Root>
|
</SwitchPrimitive.Root>
|
||||||
))
|
));
|
||||||
Switch.displayName = SwitchPrimitive.Root.displayName
|
Switch.displayName = SwitchPrimitive.Root.displayName;
|
||||||
|
|
||||||
export { Switch }
|
export { Switch };
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
"use client"
|
'use client';
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from 'react';
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import { cva, type VariantProps } from 'class-variance-authority';
|
||||||
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
import * as TabsPrimitive from '@radix-ui/react-tabs';
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
function Tabs({
|
function Tabs({
|
||||||
className,
|
className,
|
||||||
orientation = "horizontal",
|
orientation = 'horizontal',
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
|
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
|
||||||
return (
|
return (
|
||||||
@@ -16,36 +16,32 @@ function Tabs({
|
|||||||
data-slot="tabs"
|
data-slot="tabs"
|
||||||
data-orientation={orientation}
|
data-orientation={orientation}
|
||||||
orientation={orientation}
|
orientation={orientation}
|
||||||
className={cn(
|
className={cn('group/tabs flex gap-2 data-[orientation=horizontal]:flex-col', className)}
|
||||||
"group/tabs flex gap-2 data-[orientation=horizontal]:flex-col",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const tabsListVariants = cva(
|
const tabsListVariants = cva(
|
||||||
"group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-[orientation=horizontal]/tabs:h-9 group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col data-[variant=line]:rounded-none",
|
'group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-[orientation=horizontal]/tabs:h-9 group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col data-[variant=line]:rounded-none',
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: "bg-muted",
|
default: 'bg-muted',
|
||||||
line: "gap-1 bg-transparent",
|
line: 'gap-1 bg-transparent',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
variant: "default",
|
variant: 'default',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
function TabsList({
|
function TabsList({
|
||||||
className,
|
className,
|
||||||
variant = "default",
|
variant = 'default',
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof TabsPrimitive.List> &
|
}: React.ComponentProps<typeof TabsPrimitive.List> & VariantProps<typeof tabsListVariants>) {
|
||||||
VariantProps<typeof tabsListVariants>) {
|
|
||||||
return (
|
return (
|
||||||
<TabsPrimitive.List
|
<TabsPrimitive.List
|
||||||
data-slot="tabs-list"
|
data-slot="tabs-list"
|
||||||
@@ -53,39 +49,33 @@ function TabsList({
|
|||||||
className={cn(tabsListVariants({ variant }), className)}
|
className={cn(tabsListVariants({ variant }), className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function TabsTrigger({
|
function TabsTrigger({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
|
|
||||||
return (
|
return (
|
||||||
<TabsPrimitive.Trigger
|
<TabsPrimitive.Trigger
|
||||||
data-slot="tabs-trigger"
|
data-slot="tabs-trigger"
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none dark:text-muted-foreground dark:hover:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
"relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none dark:text-muted-foreground dark:hover:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent",
|
'group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent',
|
||||||
"data-[state=active]:bg-background data-[state=active]:text-foreground dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 dark:data-[state=active]:text-foreground",
|
'data-[state=active]:bg-background data-[state=active]:text-foreground dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 dark:data-[state=active]:text-foreground',
|
||||||
"after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100",
|
'after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function TabsContent({
|
function TabsContent({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
|
||||||
return (
|
return (
|
||||||
<TabsPrimitive.Content
|
<TabsPrimitive.Content
|
||||||
data-slot="tabs-content"
|
data-slot="tabs-content"
|
||||||
className={cn("flex-1 outline-none", className)}
|
className={cn('flex-1 outline-none', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants }
|
export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants };
|
||||||
|
|||||||
@@ -1,24 +1,23 @@
|
|||||||
import * as React from "react"
|
import * as React from 'react';
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
export interface TextareaProps
|
export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
|
||||||
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
|
|
||||||
|
|
||||||
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||||
({ className, ...props }, ref) => {
|
({ className, ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<textarea
|
<textarea
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
'flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
Textarea.displayName = "Textarea"
|
Textarea.displayName = 'Textarea';
|
||||||
|
|
||||||
export { Textarea }
|
export { Textarea };
|
||||||
|
|||||||
@@ -59,8 +59,12 @@ export function useFileShortcuts(): void {
|
|||||||
const idx = activeTabId ? openTabs.findIndex((t) => t.id === activeTabId) : 0;
|
const idx = activeTabId ? openTabs.findIndex((t) => t.id === activeTabId) : 0;
|
||||||
const safeIdx = idx === -1 ? 0 : idx;
|
const safeIdx = idx === -1 ? 0 : idx;
|
||||||
const nextIdx = e.shiftKey
|
const nextIdx = e.shiftKey
|
||||||
? (safeIdx <= 0 ? openTabs.length - 1 : safeIdx - 1)
|
? safeIdx <= 0
|
||||||
: (safeIdx >= openTabs.length - 1 ? 0 : safeIdx + 1);
|
? openTabs.length - 1
|
||||||
|
: safeIdx - 1
|
||||||
|
: safeIdx >= openTabs.length - 1
|
||||||
|
? 0
|
||||||
|
: safeIdx + 1;
|
||||||
setActiveTab(openTabs[nextIdx].id);
|
setActiveTab(openTabs[nextIdx].id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,20 +9,26 @@ export function useScrollSync(opts: Options) {
|
|||||||
const FRAME_MS = 1000 / 60;
|
const FRAME_MS = 1000 / 60;
|
||||||
const lastTick = useRef(-FRAME_MS);
|
const lastTick = useRef(-FRAME_MS);
|
||||||
|
|
||||||
const handleEditorScroll = useCallback((evt: React.UIEvent<HTMLElement>) => {
|
const handleEditorScroll = useCallback(
|
||||||
|
(evt: React.UIEvent<HTMLElement>) => {
|
||||||
const target = evt.currentTarget;
|
const target = evt.currentTarget;
|
||||||
const ratio = target.scrollTop / Math.max(target.scrollHeight - target.clientHeight, 1);
|
const ratio = target.scrollTop / Math.max(target.scrollHeight - target.clientHeight, 1);
|
||||||
const now = performance.now();
|
const now = performance.now();
|
||||||
if (now - lastTick.current < FRAME_MS) return;
|
if (now - lastTick.current < FRAME_MS) return;
|
||||||
lastTick.current = now;
|
lastTick.current = now;
|
||||||
opts.onEditorScroll?.(ratio);
|
opts.onEditorScroll?.(ratio);
|
||||||
}, [opts]);
|
},
|
||||||
|
[opts]
|
||||||
|
);
|
||||||
|
|
||||||
const handlePreviewScroll = useCallback((evt: React.UIEvent<HTMLElement>) => {
|
const handlePreviewScroll = useCallback(
|
||||||
|
(evt: React.UIEvent<HTMLElement>) => {
|
||||||
const target = evt.currentTarget;
|
const target = evt.currentTarget;
|
||||||
const ratio = target.scrollTop / Math.max(target.scrollHeight - target.clientHeight, 1);
|
const ratio = target.scrollTop / Math.max(target.scrollHeight - target.clientHeight, 1);
|
||||||
opts.onPreviewScroll?.(ratio);
|
opts.onPreviewScroll?.(ratio);
|
||||||
}, [opts]);
|
},
|
||||||
|
[opts]
|
||||||
|
);
|
||||||
|
|
||||||
return { handleEditorScroll, handlePreviewScroll };
|
return { handleEditorScroll, handlePreviewScroll };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ export function useAutoUpdateCheck() {
|
|||||||
const check = useUpdaterStore((s) => s.check);
|
const check = useUpdaterStore((s) => s.check);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (auto) {
|
if (auto) {
|
||||||
const t = setTimeout(() => { check(); }, 5_000);
|
const t = setTimeout(() => {
|
||||||
|
check();
|
||||||
|
}, 5_000);
|
||||||
return () => clearTimeout(t);
|
return () => clearTimeout(t);
|
||||||
}
|
}
|
||||||
}, [auto, check]);
|
}, [auto, check]);
|
||||||
|
|||||||
+17
-11
@@ -1,16 +1,22 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8" />
|
||||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: blob:; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' https://fonts.googleapis.com https://fonts.gstatic.com;">
|
<meta
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
http-equiv="Content-Security-Policy"
|
||||||
|
content="default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: blob:; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' https://fonts.googleapis.com https://fonts.gstatic.com;"
|
||||||
|
/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>MarkdownConverter</title>
|
<title>MarkdownConverter</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=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
|
<link
|
||||||
</head>
|
href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600;700&display=swap"
|
||||||
<body class="font-sans antialiased">
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
</head>
|
||||||
|
<body class="font-sans antialiased">
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="./main.tsx"></script>
|
<script type="module" src="./main.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ export function toAsciiTable(rows: string[][]): string {
|
|||||||
const pad = rightAlign
|
const pad = rightAlign
|
||||||
? (s: string, w: number) => s.padStart(w)
|
? (s: string, w: number) => s.padStart(w)
|
||||||
: (s: string, w: number) => s.padEnd(w);
|
: (s: string, w: number) => s.padEnd(w);
|
||||||
const lines = rows.map((row) =>
|
const lines = rows.map(
|
||||||
|
(row) =>
|
||||||
'| ' +
|
'| ' +
|
||||||
Array.from({ length: numCols }, (_, c) => pad(row[c] ?? '', widths[c])).join(' | ') +
|
Array.from({ length: numCols }, (_, c) => pad(row[c] ?? '', widths[c])).join(' | ') +
|
||||||
' |'
|
' |'
|
||||||
@@ -35,9 +36,15 @@ const TABLE_RE = /^\|.+\|\n^\|[\s:|-]+\|\n((?:^\|.+\|\n?)+)/gm;
|
|||||||
export function applyAsciiTransform(source: string): string {
|
export function applyAsciiTransform(source: string): string {
|
||||||
return source.replace(TABLE_RE, (block) => {
|
return source.replace(TABLE_RE, (block) => {
|
||||||
const lines = block.trim().split('\n');
|
const lines = block.trim().split('\n');
|
||||||
const header = lines[0].slice(1, -1).split('|').map((s) => s.trim());
|
const header = lines[0]
|
||||||
|
.slice(1, -1)
|
||||||
|
.split('|')
|
||||||
|
.map((s) => s.trim());
|
||||||
const body = lines.slice(2).map((l) =>
|
const body = lines.slice(2).map((l) =>
|
||||||
l.slice(1, -1).split('|').map((s) => s.trim())
|
l
|
||||||
|
.slice(1, -1)
|
||||||
|
.split('|')
|
||||||
|
.map((s) => s.trim())
|
||||||
);
|
);
|
||||||
return '```\n' + toAsciiTable([header, ...body]) + '\n```';
|
return '```\n' + toAsciiTable([header, ...body]) + '\n```';
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ export function registerMenuCommands(): void {
|
|||||||
useAppStore.getState().toggleSidebar();
|
useAppStore.getState().toggleSidebar();
|
||||||
}
|
}
|
||||||
const target = document.querySelector(
|
const target = document.querySelector(
|
||||||
`[data-testid="sidebar-jump-${panel}"]`,
|
`[data-testid="sidebar-jump-${panel}"]`
|
||||||
) as HTMLButtonElement | null;
|
) as HTMLButtonElement | null;
|
||||||
target?.click();
|
target?.click();
|
||||||
},
|
},
|
||||||
@@ -184,7 +184,11 @@ export function registerMenuCommands(): void {
|
|||||||
const settings = useSettingsStore.getState();
|
const settings = useSettingsStore.getState();
|
||||||
const cur = settings.editorFontSize ?? 14;
|
const cur = settings.editorFontSize ?? 14;
|
||||||
const next =
|
const next =
|
||||||
direction === 'increase' ? Math.min(28, cur + 1) : direction === 'decrease' ? Math.max(10, cur - 1) : 14;
|
direction === 'increase'
|
||||||
|
? Math.min(28, cur + 1)
|
||||||
|
: direction === 'decrease'
|
||||||
|
? Math.max(10, cur - 1)
|
||||||
|
: 14;
|
||||||
settings.setSetting('editorFontSize', next);
|
settings.setSetting('editorFontSize', next);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -202,16 +206,24 @@ export function registerMenuCommands(): void {
|
|||||||
'template.load': (name?: string) => {
|
'template.load': (name?: string) => {
|
||||||
if (!name) return;
|
if (!name) return;
|
||||||
const templates: Record<string, string> = {
|
const templates: Record<string, string> = {
|
||||||
'blog-post.md': '# Blog Post\n\n_Author • Date_\n\n## Introduction\n\n## Body\n\n## Conclusion\n',
|
'blog-post.md':
|
||||||
'meeting-notes.md': '# Meeting Notes\n\n**Date:** \n**Attendees:** \n\n## Agenda\n\n## Discussion\n\n## Action items\n',
|
'# Blog Post\n\n_Author • Date_\n\n## Introduction\n\n## Body\n\n## Conclusion\n',
|
||||||
'technical-spec.md': '# Technical Specification\n\n## Overview\n\n## Goals\n\n## Design\n\n## Implementation\n\n## Testing\n',
|
'meeting-notes.md':
|
||||||
'changelog.md': '# Changelog\n\n## [Unreleased]\n\n### Added\n- \n\n### Changed\n- \n\n### Fixed\n- \n',
|
'# Meeting Notes\n\n**Date:** \n**Attendees:** \n\n## Agenda\n\n## Discussion\n\n## Action items\n',
|
||||||
|
'technical-spec.md':
|
||||||
|
'# Technical Specification\n\n## Overview\n\n## Goals\n\n## Design\n\n## Implementation\n\n## Testing\n',
|
||||||
|
'changelog.md':
|
||||||
|
'# Changelog\n\n## [Unreleased]\n\n### Added\n- \n\n### Changed\n- \n\n### Fixed\n- \n',
|
||||||
'readme.md': '# Project\n\n## Description\n\n## Installation\n\n## Usage\n\n## License\n',
|
'readme.md': '# Project\n\n## Description\n\n## Installation\n\n## Usage\n\n## License\n',
|
||||||
'project-plan.md': '# Project Plan\n\n## Goals\n\n## Milestones\n\n## Risks\n\n## Status\n',
|
'project-plan.md': '# Project Plan\n\n## Goals\n\n## Milestones\n\n## Risks\n\n## Status\n',
|
||||||
'api-docs.md': '# API Documentation\n\n## Authentication\n\n## Endpoints\n\n### `GET /resource`\n\n### `POST /resource`\n',
|
'api-docs.md':
|
||||||
'tutorial.md': '# Tutorial\n\n## Prerequisites\n\n## Step 1\n\n## Step 2\n\n## Conclusion\n',
|
'# API Documentation\n\n## Authentication\n\n## Endpoints\n\n### `GET /resource`\n\n### `POST /resource`\n',
|
||||||
'release-notes.md': '# Release Notes\n\n## New features\n\n## Improvements\n\n## Bug fixes\n',
|
'tutorial.md':
|
||||||
'comparison.md': '# Comparison\n\n| Option | A | B |\n|---|---|---|\n| Cost | | |\n| Speed | | |\n',
|
'# Tutorial\n\n## Prerequisites\n\n## Step 1\n\n## Step 2\n\n## Conclusion\n',
|
||||||
|
'release-notes.md':
|
||||||
|
'# Release Notes\n\n## New features\n\n## Improvements\n\n## Bug fixes\n',
|
||||||
|
'comparison.md':
|
||||||
|
'# Comparison\n\n| Option | A | B |\n|---|---|---|\n| Cost | | |\n| Speed | | |\n',
|
||||||
};
|
};
|
||||||
const snippet = templates[name];
|
const snippet = templates[name];
|
||||||
if (!snippet) {
|
if (!snippet) {
|
||||||
@@ -240,9 +252,7 @@ export function registerMenuCommands(): void {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
toast.info(
|
toast.info(`${type.charAt(0).toUpperCase() + type.slice(1)} batch conversion — coming soon!`);
|
||||||
`${type.charAt(0).toUpperCase() + type.slice(1)} batch conversion — coming soon!`,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Document compare — no modal yet, acknowledge with toast.
|
// Document compare — no modal yet, acknowledge with toast.
|
||||||
|
|||||||
@@ -16,10 +16,14 @@ export async function generateDocx(options: DocxOptions): Promise<Blob> {
|
|||||||
// Headings (# ## ###) get heading styles
|
// Headings (# ## ###) get heading styles
|
||||||
const lines = transformed.split('\n');
|
const lines = transformed.split('\n');
|
||||||
const children = lines.map((line) => {
|
const children = lines.map((line) => {
|
||||||
if (line.startsWith('### ')) return new Paragraph({ text: line.slice(4), heading: HeadingLevel.HEADING_3 });
|
if (line.startsWith('### '))
|
||||||
if (line.startsWith('## ')) return new Paragraph({ text: line.slice(3), heading: HeadingLevel.HEADING_2 });
|
return new Paragraph({ text: line.slice(4), heading: HeadingLevel.HEADING_3 });
|
||||||
if (line.startsWith('# ')) return new Paragraph({ text: line.slice(2), heading: HeadingLevel.HEADING_1 });
|
if (line.startsWith('## '))
|
||||||
if (line.startsWith('```')) return new Paragraph({ text: line, alignment: AlignmentType.CENTER });
|
return new Paragraph({ text: line.slice(3), heading: HeadingLevel.HEADING_2 });
|
||||||
|
if (line.startsWith('# '))
|
||||||
|
return new Paragraph({ text: line.slice(2), heading: HeadingLevel.HEADING_1 });
|
||||||
|
if (line.startsWith('```'))
|
||||||
|
return new Paragraph({ text: line, alignment: AlignmentType.CENTER });
|
||||||
return new Paragraph({ children: [new TextRun(line)] });
|
return new Paragraph({ children: [new TextRun(line)] });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -11,11 +11,7 @@
|
|||||||
*/
|
*/
|
||||||
import type { EditorView } from '@codemirror/view';
|
import type { EditorView } from '@codemirror/view';
|
||||||
import { EditorView as CMEditorView } from '@codemirror/view';
|
import { EditorView as CMEditorView } from '@codemirror/view';
|
||||||
import {
|
import { undo as cmUndo, redo as cmRedo, selectLine } from '@codemirror/commands';
|
||||||
undo as cmUndo,
|
|
||||||
redo as cmRedo,
|
|
||||||
selectLine,
|
|
||||||
} from '@codemirror/commands';
|
|
||||||
|
|
||||||
let activeView: EditorView | null = null;
|
let activeView: EditorView | null = null;
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,16 @@
|
|||||||
import figlet from 'figlet';
|
import figlet from 'figlet';
|
||||||
import type { Fonts } from 'figlet';
|
import type { Fonts } from 'figlet';
|
||||||
|
|
||||||
export const FIGLET_FONTS = ['Standard', 'Big', 'Small', 'Banner', 'Doom', 'Slant', 'Block'] as const;
|
export const FIGLET_FONTS = [
|
||||||
export type FigletFont = typeof FIGLET_FONTS[number];
|
'Standard',
|
||||||
|
'Big',
|
||||||
|
'Small',
|
||||||
|
'Banner',
|
||||||
|
'Doom',
|
||||||
|
'Slant',
|
||||||
|
'Block',
|
||||||
|
] as const;
|
||||||
|
export type FigletFont = (typeof FIGLET_FONTS)[number];
|
||||||
|
|
||||||
export function figletText(text: string, font: FigletFont): Promise<string> {
|
export function figletText(text: string, font: FigletFont): Promise<string> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
|||||||
@@ -123,5 +123,8 @@ ${body}
|
|||||||
}
|
}
|
||||||
|
|
||||||
function escapeHtml(s: string): string {
|
function escapeHtml(s: string): string {
|
||||||
return s.replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c] ?? c));
|
return s.replace(
|
||||||
|
/[&<>"']/g,
|
||||||
|
(c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c] ?? c
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+36
-24
@@ -25,7 +25,7 @@ function wrap<T>(fn: () => Promise<T>): Promise<IpcResult<T | ChannelMissing>> {
|
|||||||
(err: Error) => ({
|
(err: Error) => ({
|
||||||
ok: false as const,
|
ok: false as const,
|
||||||
error: { code: err.name || 'IPC_ERROR', message: err.message || String(err) },
|
error: { code: err.name || 'IPC_ERROR', message: err.message || String(err) },
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,8 +58,7 @@ function safeCall<T extends (...args: any[]) => Promise<any>>(
|
|||||||
|
|
||||||
export const ipc = {
|
export const ipc = {
|
||||||
file: {
|
file: {
|
||||||
open: (): Promise<IpcResult<FileResult | ChannelMissing>> =>
|
open: (): Promise<IpcResult<FileResult | ChannelMissing>> => safeCall('file', 'open'),
|
||||||
safeCall('file', 'open'),
|
|
||||||
read: (path: string): Promise<IpcResult<string | ChannelMissing>> =>
|
read: (path: string): Promise<IpcResult<string | ChannelMissing>> =>
|
||||||
safeCall('file', 'read', path),
|
safeCall('file', 'read', path),
|
||||||
write: (path: string, content: string): Promise<IpcResult<void | ChannelMissing>> =>
|
write: (path: string, content: string): Promise<IpcResult<void | ChannelMissing>> =>
|
||||||
@@ -76,12 +75,26 @@ export const ipc = {
|
|||||||
}
|
}
|
||||||
return window.electronAPI.file.onChange(cb);
|
return window.electronAPI.file.onChange(cb);
|
||||||
},
|
},
|
||||||
search: (args: { rootPath: string; query: string; isRegex: boolean; caseSensitive: boolean }): Promise<IpcResult<Array<{ filePath: string; line: number; content: string }> | ChannelMissing>> =>
|
search: (args: {
|
||||||
safeCall('file', 'search', args),
|
rootPath: string;
|
||||||
gitStatus: (args: { rootPath: string }): Promise<IpcResult<Array<{ filePath: string; status: 'modified' | 'added' | 'deleted' | 'untracked' }> | ChannelMissing>> =>
|
query: string;
|
||||||
safeCall('file', 'gitStatus', args),
|
isRegex: boolean;
|
||||||
writeBuffer: (args: { path: string; buffer: Uint8Array }): Promise<IpcResult<void | ChannelMissing>> =>
|
caseSensitive: boolean;
|
||||||
safeCall('file', 'writeBuffer', args),
|
}): Promise<
|
||||||
|
IpcResult<Array<{ filePath: string; line: number; content: string }> | ChannelMissing>
|
||||||
|
> => safeCall('file', 'search', args),
|
||||||
|
gitStatus: (args: {
|
||||||
|
rootPath: string;
|
||||||
|
}): Promise<
|
||||||
|
IpcResult<
|
||||||
|
| Array<{ filePath: string; status: 'modified' | 'added' | 'deleted' | 'untracked' }>
|
||||||
|
| ChannelMissing
|
||||||
|
>
|
||||||
|
> => safeCall('file', 'gitStatus', args),
|
||||||
|
writeBuffer: (args: {
|
||||||
|
path: string;
|
||||||
|
buffer: Uint8Array;
|
||||||
|
}): Promise<IpcResult<void | ChannelMissing>> => safeCall('file', 'writeBuffer', args),
|
||||||
setCurrent: (path: string | null): void => {
|
setCurrent: (path: string | null): void => {
|
||||||
if (typeof window !== 'undefined' && (window.electronAPI as any)?.file?.setCurrent) {
|
if (typeof window !== 'undefined' && (window.electronAPI as any)?.file?.setCurrent) {
|
||||||
(window.electronAPI as any).file.setCurrent(path);
|
(window.electronAPI as any).file.setCurrent(path);
|
||||||
@@ -101,17 +114,21 @@ export const ipc = {
|
|||||||
safeCall('export', 'docx', opts),
|
safeCall('export', 'docx', opts),
|
||||||
html: (opts: HtmlOptions): Promise<IpcResult<ExportResult | ChannelMissing>> =>
|
html: (opts: HtmlOptions): Promise<IpcResult<ExportResult | ChannelMissing>> =>
|
||||||
safeCall('export', 'html', opts),
|
safeCall('export', 'html', opts),
|
||||||
batch: (items: BatchItem[], opts: BatchOptions): Promise<IpcResult<BatchResult | ChannelMissing>> =>
|
batch: (
|
||||||
safeCall('export', 'batch', items, opts),
|
items: BatchItem[],
|
||||||
|
opts: BatchOptions
|
||||||
|
): Promise<IpcResult<BatchResult | ChannelMissing>> => safeCall('export', 'batch', items, opts),
|
||||||
},
|
},
|
||||||
app: {
|
app: {
|
||||||
getVersion: (): Promise<IpcResult<string | ChannelMissing>> =>
|
getVersion: (): Promise<IpcResult<string | ChannelMissing>> => safeCall('app', 'getVersion'),
|
||||||
safeCall('app', 'getVersion'),
|
|
||||||
openExternal: (url: string): Promise<IpcResult<void | ChannelMissing>> =>
|
openExternal: (url: string): Promise<IpcResult<void | ChannelMissing>> =>
|
||||||
safeCall('app', 'openExternal', url),
|
safeCall('app', 'openExternal', url),
|
||||||
showItemInFolder: (path: string): Promise<IpcResult<void | ChannelMissing>> =>
|
showItemInFolder: (path: string): Promise<IpcResult<void | ChannelMissing>> =>
|
||||||
safeCall('app', 'showItemInFolder', path),
|
safeCall('app', 'showItemInFolder', path),
|
||||||
showSaveDialog: (args?: { title?: string; defaultPath?: string }): Promise<IpcResult<string | null | ChannelMissing>> =>
|
showSaveDialog: (args?: {
|
||||||
|
title?: string;
|
||||||
|
defaultPath?: string;
|
||||||
|
}): Promise<IpcResult<string | null | ChannelMissing>> =>
|
||||||
safeCall('app', 'showSaveDialog', args),
|
safeCall('app', 'showSaveDialog', args),
|
||||||
},
|
},
|
||||||
menu: {
|
menu: {
|
||||||
@@ -127,12 +144,9 @@ export const ipc = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
updater: {
|
updater: {
|
||||||
check: (): Promise<IpcResult<void | ChannelMissing>> =>
|
check: (): Promise<IpcResult<void | ChannelMissing>> => safeCall('updater', 'check'),
|
||||||
safeCall('updater', 'check'),
|
install: (): Promise<IpcResult<void | ChannelMissing>> => safeCall('updater', 'install'),
|
||||||
install: (): Promise<IpcResult<void | ChannelMissing>> =>
|
getState: (): Promise<IpcResult<unknown | ChannelMissing>> => safeCall('updater', 'getState'),
|
||||||
safeCall('updater', 'install'),
|
|
||||||
getState: (): Promise<IpcResult<unknown | ChannelMissing>> =>
|
|
||||||
safeCall('updater', 'getState'),
|
|
||||||
onStatus: (cb: (payload: unknown) => void): (() => void) => {
|
onStatus: (cb: (payload: unknown) => void): (() => void) => {
|
||||||
if (typeof window === 'undefined' || !window.electronAPI?.updater?.onStatus) {
|
if (typeof window === 'undefined' || !window.electronAPI?.updater?.onStatus) {
|
||||||
return () => {};
|
return () => {};
|
||||||
@@ -141,10 +155,8 @@ export const ipc = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
crash: {
|
crash: {
|
||||||
read: (): Promise<IpcResult<unknown | ChannelMissing>> =>
|
read: (): Promise<IpcResult<unknown | ChannelMissing>> => safeCall('crash', 'read'),
|
||||||
safeCall('crash', 'read'),
|
openDir: (): Promise<IpcResult<void | ChannelMissing>> => safeCall('crash', 'openDir'),
|
||||||
openDir: (): Promise<IpcResult<void | ChannelMissing>> =>
|
|
||||||
safeCall('crash', 'openDir'),
|
|
||||||
delete: (filename: string): Promise<IpcResult<void | ChannelMissing>> =>
|
delete: (filename: string): Promise<IpcResult<void | ChannelMissing>> =>
|
||||||
safeCall('crash', 'delete', filename),
|
safeCall('crash', 'delete', filename),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -19,7 +19,16 @@ export function renderMarkdown(source: string): string {
|
|||||||
|
|
||||||
const rawHtml = marked.parse(withPlaceholders, { async: false }) as string;
|
const rawHtml = marked.parse(withPlaceholders, { async: false }) as string;
|
||||||
const clean = DOMPurify.sanitize(rawHtml, {
|
const clean = DOMPurify.sanitize(rawHtml, {
|
||||||
ALLOWED_ATTR: ['href', 'src', 'alt', 'title', 'class', 'data-mermaid-source', 'data-language', 'id'],
|
ALLOWED_ATTR: [
|
||||||
|
'href',
|
||||||
|
'src',
|
||||||
|
'alt',
|
||||||
|
'title',
|
||||||
|
'class',
|
||||||
|
'data-mermaid-source',
|
||||||
|
'data-language',
|
||||||
|
'id',
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
// The sanitized HTML still has placeholders; we leave the actual mermaid
|
// The sanitized HTML still has placeholders; we leave the actual mermaid
|
||||||
|
|||||||
@@ -25,7 +25,10 @@ function normalizeAlreadyV5(data: Record<string, unknown>): Record<string, unkno
|
|||||||
// zod schema on every launch. Always normalize theme against the v5 enum
|
// zod schema on every launch. Always normalize theme against the v5 enum
|
||||||
// before returning, so persisted files are always valid v5.
|
// before returning, so persisted files are always valid v5.
|
||||||
const out = { ...data };
|
const out = { ...data };
|
||||||
if (typeof out.theme !== 'string' || !v5ThemeValues.includes(out.theme as (typeof v5ThemeValues)[number])) {
|
if (
|
||||||
|
typeof out.theme !== 'string' ||
|
||||||
|
!v5ThemeValues.includes(out.theme as (typeof v5ThemeValues)[number])
|
||||||
|
) {
|
||||||
out.theme = 'system';
|
out.theme = 'system';
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
|
|||||||
@@ -1,17 +1,115 @@
|
|||||||
const STOP_WORDS = new Set([
|
const STOP_WORDS = new Set([
|
||||||
'the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been', 'being',
|
'the',
|
||||||
'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could',
|
'a',
|
||||||
'should', 'may', 'might', 'shall', 'can', 'to', 'of', 'in', 'for',
|
'an',
|
||||||
'on', 'with', 'at', 'by', 'from', 'as', 'into', 'through', 'during',
|
'is',
|
||||||
'before', 'after', 'above', 'below', 'between', 'out', 'off', 'over',
|
'are',
|
||||||
'under', 'again', 'further', 'then', 'once', 'here', 'there', 'when',
|
'was',
|
||||||
'where', 'why', 'how', 'all', 'each', 'every', 'both', 'few', 'more',
|
'were',
|
||||||
'most', 'other', 'some', 'such', 'no', 'nor', 'not', 'only', 'own',
|
'be',
|
||||||
'same', 'so', 'than', 'too', 'very', 'just', 'because', 'but', 'and',
|
'been',
|
||||||
'or', 'if', 'while', 'about', 'up', 'it', 'its', 'this', 'that',
|
'being',
|
||||||
'these', 'those', 'i', 'me', 'my', 'we', 'our', 'you', 'your', 'he',
|
'have',
|
||||||
'him', 'his', 'she', 'her', 'they', 'them', 'their', 'what', 'which',
|
'has',
|
||||||
'who', 'whom', 'also'
|
'had',
|
||||||
|
'do',
|
||||||
|
'does',
|
||||||
|
'did',
|
||||||
|
'will',
|
||||||
|
'would',
|
||||||
|
'could',
|
||||||
|
'should',
|
||||||
|
'may',
|
||||||
|
'might',
|
||||||
|
'shall',
|
||||||
|
'can',
|
||||||
|
'to',
|
||||||
|
'of',
|
||||||
|
'in',
|
||||||
|
'for',
|
||||||
|
'on',
|
||||||
|
'with',
|
||||||
|
'at',
|
||||||
|
'by',
|
||||||
|
'from',
|
||||||
|
'as',
|
||||||
|
'into',
|
||||||
|
'through',
|
||||||
|
'during',
|
||||||
|
'before',
|
||||||
|
'after',
|
||||||
|
'above',
|
||||||
|
'below',
|
||||||
|
'between',
|
||||||
|
'out',
|
||||||
|
'off',
|
||||||
|
'over',
|
||||||
|
'under',
|
||||||
|
'again',
|
||||||
|
'further',
|
||||||
|
'then',
|
||||||
|
'once',
|
||||||
|
'here',
|
||||||
|
'there',
|
||||||
|
'when',
|
||||||
|
'where',
|
||||||
|
'why',
|
||||||
|
'how',
|
||||||
|
'all',
|
||||||
|
'each',
|
||||||
|
'every',
|
||||||
|
'both',
|
||||||
|
'few',
|
||||||
|
'more',
|
||||||
|
'most',
|
||||||
|
'other',
|
||||||
|
'some',
|
||||||
|
'such',
|
||||||
|
'no',
|
||||||
|
'nor',
|
||||||
|
'not',
|
||||||
|
'only',
|
||||||
|
'own',
|
||||||
|
'same',
|
||||||
|
'so',
|
||||||
|
'than',
|
||||||
|
'too',
|
||||||
|
'very',
|
||||||
|
'just',
|
||||||
|
'because',
|
||||||
|
'but',
|
||||||
|
'and',
|
||||||
|
'or',
|
||||||
|
'if',
|
||||||
|
'while',
|
||||||
|
'about',
|
||||||
|
'up',
|
||||||
|
'it',
|
||||||
|
'its',
|
||||||
|
'this',
|
||||||
|
'that',
|
||||||
|
'these',
|
||||||
|
'those',
|
||||||
|
'i',
|
||||||
|
'me',
|
||||||
|
'my',
|
||||||
|
'we',
|
||||||
|
'our',
|
||||||
|
'you',
|
||||||
|
'your',
|
||||||
|
'he',
|
||||||
|
'him',
|
||||||
|
'his',
|
||||||
|
'she',
|
||||||
|
'her',
|
||||||
|
'they',
|
||||||
|
'them',
|
||||||
|
'their',
|
||||||
|
'what',
|
||||||
|
'which',
|
||||||
|
'who',
|
||||||
|
'whom',
|
||||||
|
'also',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
function countSyllables(word: string): number {
|
function countSyllables(word: string): number {
|
||||||
@@ -66,17 +164,23 @@ export function analyzeText(text: string): WritingMetrics {
|
|||||||
avgSentenceLength: 0,
|
avgSentenceLength: 0,
|
||||||
longestSentence: '',
|
longestSentence: '',
|
||||||
longestSentenceLength: 0,
|
longestSentenceLength: 0,
|
||||||
topWords: []
|
topWords: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const words = extractWords(text);
|
const words = extractWords(text);
|
||||||
const wordCount = words.length;
|
const wordCount = words.length;
|
||||||
|
|
||||||
const sentences = text.split(/[.!?]+/).map(s => s.trim()).filter(Boolean);
|
const sentences = text
|
||||||
|
.split(/[.!?]+/)
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter(Boolean);
|
||||||
const sentenceCount = Math.max(sentences.length, 1);
|
const sentenceCount = Math.max(sentences.length, 1);
|
||||||
|
|
||||||
const paragraphs = text.split(/\n\s*\n/).map(p => p.trim()).filter(Boolean);
|
const paragraphs = text
|
||||||
|
.split(/\n\s*\n/)
|
||||||
|
.map((p) => p.trim())
|
||||||
|
.filter(Boolean);
|
||||||
const paragraphCount = Math.max(paragraphs.length, 1);
|
const paragraphCount = Math.max(paragraphs.length, 1);
|
||||||
|
|
||||||
let totalSyllables = 0;
|
let totalSyllables = 0;
|
||||||
@@ -84,16 +188,27 @@ export function analyzeText(text: string): WritingMetrics {
|
|||||||
totalSyllables += countSyllables(w);
|
totalSyllables += countSyllables(w);
|
||||||
}
|
}
|
||||||
|
|
||||||
const fleschEase = wordCount > 0 ? Math.round((206.835 - 1.015 * (wordCount / sentenceCount) - 84.6 * (totalSyllables / wordCount)) * 10) / 10 : 0;
|
const fleschEase =
|
||||||
const fleschGrade = wordCount > 0 ? Math.round((0.39 * (wordCount / sentenceCount) + 11.8 * (totalSyllables / wordCount) - 15.59) * 10) / 10 : 0;
|
wordCount > 0
|
||||||
|
? Math.round(
|
||||||
|
(206.835 - 1.015 * (wordCount / sentenceCount) - 84.6 * (totalSyllables / wordCount)) * 10
|
||||||
|
) / 10
|
||||||
|
: 0;
|
||||||
|
const fleschGrade =
|
||||||
|
wordCount > 0
|
||||||
|
? Math.round(
|
||||||
|
(0.39 * (wordCount / sentenceCount) + 11.8 * (totalSyllables / wordCount) - 15.59) * 10
|
||||||
|
) / 10
|
||||||
|
: 0;
|
||||||
const readabilityLabel = getReadabilityLabel(fleschEase);
|
const readabilityLabel = getReadabilityLabel(fleschEase);
|
||||||
|
|
||||||
const readingTime = Math.ceil(wordCount / 200);
|
const readingTime = Math.ceil(wordCount / 200);
|
||||||
const speakingTime = Math.ceil(wordCount / 130);
|
const speakingTime = Math.ceil(wordCount / 130);
|
||||||
|
|
||||||
const uniqueWords = new Set(words.map(w => w.toLowerCase()));
|
const uniqueWords = new Set(words.map((w) => w.toLowerCase()));
|
||||||
const uniqueWordCount = uniqueWords.size;
|
const uniqueWordCount = uniqueWords.size;
|
||||||
const lexicalDiversity = wordCount > 0 ? Math.round((uniqueWordCount / wordCount) * 1000) / 10 : 0;
|
const lexicalDiversity =
|
||||||
|
wordCount > 0 ? Math.round((uniqueWordCount / wordCount) * 1000) / 10 : 0;
|
||||||
|
|
||||||
const avgSentenceLength = Math.round((wordCount / sentenceCount) * 10) / 10;
|
const avgSentenceLength = Math.round((wordCount / sentenceCount) * 10) / 10;
|
||||||
|
|
||||||
@@ -138,6 +253,6 @@ export function analyzeText(text: string): WritingMetrics {
|
|||||||
avgSentenceLength,
|
avgSentenceLength,
|
||||||
longestSentence,
|
longestSentence,
|
||||||
longestSentenceLength,
|
longestSentenceLength,
|
||||||
topWords
|
topWords,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,10 +30,8 @@ export const useCommandStore = create<CommandState>()(
|
|||||||
(set, get) => ({
|
(set, get) => ({
|
||||||
handlers: {},
|
handlers: {},
|
||||||
userBindings: {},
|
userBindings: {},
|
||||||
register: (id, handler) =>
|
register: (id, handler) => set((s) => ({ handlers: { ...s.handlers, [id]: handler } })),
|
||||||
set((s) => ({ handlers: { ...s.handlers, [id]: handler } })),
|
registerMany: (handlers) => set((s) => ({ handlers: { ...s.handlers, ...handlers } })),
|
||||||
registerMany: (handlers) =>
|
|
||||||
set((s) => ({ handlers: { ...s.handlers, ...handlers } })),
|
|
||||||
unregister: (id) =>
|
unregister: (id) =>
|
||||||
set((s) => {
|
set((s) => {
|
||||||
const next = { ...s.handlers };
|
const next = { ...s.handlers };
|
||||||
|
|||||||
@@ -177,7 +177,9 @@ export const useFileStore = create<FileState>()(
|
|||||||
openFileFromMain: async (filePath, content) => {
|
openFileFromMain: async (filePath, content) => {
|
||||||
const existing = useFileStore.getState().openTabs.find((t) => t.id === filePath);
|
const existing = useFileStore.getState().openTabs.find((t) => t.id === filePath);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
set((s) => { s.activeTabId = filePath; });
|
set((s) => {
|
||||||
|
s.activeTabId = filePath;
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,7 +197,7 @@ export const useFileStore = create<FileState>()(
|
|||||||
if (idx === -1) return;
|
if (idx === -1) return;
|
||||||
s.openTabs.splice(idx, 1);
|
s.openTabs.splice(idx, 1);
|
||||||
if (s.activeTabId === id) {
|
if (s.activeTabId === id) {
|
||||||
s.activeTabId = idx > 0 ? s.openTabs[idx - 1]?.id ?? null : null;
|
s.activeTabId = idx > 0 ? (s.openTabs[idx - 1]?.id ?? null) : null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -256,8 +258,8 @@ export const useFileStore = create<FileState>()(
|
|||||||
defaultPath: 'document.md',
|
defaultPath: 'document.md',
|
||||||
filters: [
|
filters: [
|
||||||
{ name: 'Markdown', extensions: ['md', 'markdown'] },
|
{ name: 'Markdown', extensions: ['md', 'markdown'] },
|
||||||
{ name: 'All Files', extensions: ['*'] }
|
{ name: 'All Files', extensions: ['*'] },
|
||||||
]
|
],
|
||||||
});
|
});
|
||||||
if (!dialogResult.ok || !dialogResult.data) {
|
if (!dialogResult.ok || !dialogResult.data) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export const useSettingsStore = create<SettingsState>()(
|
|||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
console.warn(
|
console.warn(
|
||||||
'[settings-store] invalid persisted state, replacing with defaults',
|
'[settings-store] invalid persisted state, replacing with defaults',
|
||||||
result.error.issues.map((i) => i.path.join('.') + ': ' + i.message).join('; '),
|
result.error.issues.map((i) => i.path.join('.') + ': ' + i.message).join('; ')
|
||||||
);
|
);
|
||||||
return { ...DEFAULTS } as Partial<SettingsState>;
|
return { ...DEFAULTS } as Partial<SettingsState>;
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+7
-2
@@ -22,7 +22,9 @@ export interface ElectronAPI {
|
|||||||
|
|
||||||
// Conversion status
|
// Conversion status
|
||||||
onConversionStatus: (callback: (message: string) => void) => () => void;
|
onConversionStatus: (callback: (message: string) => void) => () => void;
|
||||||
onConversionComplete: (callback: (data: { format: string; outputPath: string }) => void) => () => void;
|
onConversionComplete: (
|
||||||
|
callback: (data: { format: string; outputPath: string }) => void
|
||||||
|
) => () => void;
|
||||||
|
|
||||||
// Dialogs
|
// Dialogs
|
||||||
showExportDialog: (format: string) => void;
|
showExportDialog: (format: string) => void;
|
||||||
@@ -54,7 +56,10 @@ export interface ElectronAPI {
|
|||||||
|
|
||||||
// Images
|
// Images
|
||||||
selectFolder: () => Promise<string | null>;
|
selectFolder: () => Promise<string | null>;
|
||||||
savePastedImage: (data: { base64: string; ext: string }) => Promise<{ relativePath: string } | null>;
|
savePastedImage: (data: {
|
||||||
|
base64: string;
|
||||||
|
ext: string;
|
||||||
|
}) => Promise<{ relativePath: string } | null>;
|
||||||
|
|
||||||
// Templates
|
// Templates
|
||||||
loadTemplate: (file: string) => Promise<string>;
|
loadTemplate: (file: string) => Promise<string>;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user