mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-08-02 18:10:18 +05:30
refactor: delete 17 legacy files (renderer.js, 8 stylesheets, 5 scripts, 2 htmls, src/index.html orphan, deprecated src/main.js)
This commit is contained in:
+1
-2
@@ -19,8 +19,7 @@ module.exports = {
|
||||
// Coverage configuration
|
||||
collectCoverageFrom: [
|
||||
'src/**/*.js',
|
||||
'!src/main.js', // Main process needs electron-mock
|
||||
'!src/renderer.js', // Large renderer file with duplicate declarations
|
||||
'!src/main/**', // Main process needs electron-mock
|
||||
'!src/preload.js', // Electron preload requires contextBridge
|
||||
'!**/node_modules/**'
|
||||
],
|
||||
|
||||
@@ -1,601 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ASCII Art Generator - MarkdownConverter</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--ci-dark-gray: #464646;
|
||||
--ci-medium-gray: #9a9696;
|
||||
--ci-accent: #e5461f;
|
||||
--ci-light-gray: #e3e3e3;
|
||||
--ci-black: #0d0b09;
|
||||
--ci-white: #ffffff;
|
||||
--ci-bg: #f5f5f5;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', system-ui, sans-serif;
|
||||
background: var(--ci-bg);
|
||||
color: var(--ci-dark-gray);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: linear-gradient(135deg, var(--ci-dark-gray) 0%, var(--ci-black) 100%);
|
||||
padding: 16px 24px;
|
||||
border-bottom: 3px solid var(--ci-accent);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
color: var(--ci-white);
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 24px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.section {
|
||||
background: var(--ci-white);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 16px;
|
||||
color: var(--ci-dark-gray);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.mode-tabs {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.mode-tab {
|
||||
padding: 10px 20px;
|
||||
border: 2px solid var(--ci-light-gray);
|
||||
background: var(--ci-white);
|
||||
border-radius: 8px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.mode-tab:hover {
|
||||
border-color: var(--ci-accent);
|
||||
}
|
||||
|
||||
.mode-tab.active {
|
||||
background: var(--ci-accent);
|
||||
border-color: var(--ci-accent);
|
||||
color: var(--ci-white);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
color: var(--ci-dark-gray);
|
||||
}
|
||||
|
||||
.form-input, .form-select, .form-textarea {
|
||||
width: 100%;
|
||||
padding: 10px 14px;
|
||||
border: 2px solid var(--ci-light-gray);
|
||||
border-radius: 8px;
|
||||
font-size: 0.875rem;
|
||||
font-family: inherit;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.form-input:focus, .form-select:focus, .form-textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--ci-accent);
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
min-height: 80px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.preview-container {
|
||||
background: var(--ci-black);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
min-height: 200px;
|
||||
max-height: 350px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.preview-content {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.3;
|
||||
color: #00ff00;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.template-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.template-btn {
|
||||
padding: 8px 12px;
|
||||
border: 2px solid var(--ci-light-gray);
|
||||
background: var(--ci-white);
|
||||
border-radius: 6px;
|
||||
font-size: 0.75rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.template-btn:hover {
|
||||
border-color: var(--ci-accent);
|
||||
background: rgba(229, 70, 31, 0.05);
|
||||
}
|
||||
|
||||
.template-btn.active {
|
||||
border-color: var(--ci-accent);
|
||||
background: rgba(229, 70, 31, 0.1);
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
padding: 16px 24px;
|
||||
background: var(--ci-white);
|
||||
border-top: 1px solid var(--ci-light-gray);
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 24px;
|
||||
border-radius: 8px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--ci-accent);
|
||||
color: var(--ci-white);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #c93a18;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--ci-light-gray);
|
||||
color: var(--ci-dark-gray);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #d0d0d0;
|
||||
}
|
||||
|
||||
.mode-section {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mode-section.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.template-category {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.template-category-title {
|
||||
font-size: 0.8rem;
|
||||
color: var(--ci-medium-gray);
|
||||
margin-bottom: 8px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>ASCII Art Generator</h1>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="section">
|
||||
<div class="mode-tabs">
|
||||
<button class="mode-tab active" data-mode="text">Text Banner</button>
|
||||
<button class="mode-tab" data-mode="box">Box/Frame</button>
|
||||
<button class="mode-tab" data-mode="templates">Templates</button>
|
||||
</div>
|
||||
|
||||
<!-- Text Banner Mode -->
|
||||
<div id="text-mode" class="mode-section active">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Text to Convert</label>
|
||||
<input type="text" id="text-input" class="form-input" placeholder="Enter your text..." maxlength="30">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Style</label>
|
||||
<select id="font-style" class="form-select">
|
||||
<option value="standard">Standard</option>
|
||||
<option value="banner">Banner</option>
|
||||
<option value="block">Block</option>
|
||||
<option value="bubble">Bubble</option>
|
||||
<option value="digital">Digital</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Box/Frame Mode -->
|
||||
<div id="box-mode" class="mode-section">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Text Content</label>
|
||||
<textarea id="box-text" class="form-textarea" placeholder="Enter text for the box..."></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Box Style</label>
|
||||
<select id="box-style" class="form-select">
|
||||
<option value="single">Single Line</option>
|
||||
<option value="double">Double Line</option>
|
||||
<option value="rounded">Rounded</option>
|
||||
<option value="bold">Bold</option>
|
||||
<option value="ascii">ASCII (+|-)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Padding</label>
|
||||
<input type="number" id="box-padding" class="form-input" min="0" max="10" value="2" style="width: 100px;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Templates Mode -->
|
||||
<div id="templates-mode" class="mode-section">
|
||||
<div class="template-category">
|
||||
<div class="template-category-title">Arrows & Flow</div>
|
||||
<div class="template-grid">
|
||||
<button class="template-btn" data-template="arrow-right">Arrow Right</button>
|
||||
<button class="template-btn" data-template="arrow-down">Arrow Down</button>
|
||||
<button class="template-btn" data-template="decision">Decision</button>
|
||||
<button class="template-btn" data-template="process">Process Flow</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="template-category">
|
||||
<div class="template-category-title">Diagrams</div>
|
||||
<div class="template-grid">
|
||||
<button class="template-btn" data-template="flowchart">Flowchart</button>
|
||||
<button class="template-btn" data-template="sequence">Sequence</button>
|
||||
<button class="template-btn" data-template="network">Network</button>
|
||||
<button class="template-btn" data-template="hierarchy">Hierarchy</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="template-category">
|
||||
<div class="template-category-title">Boxes & Containers</div>
|
||||
<div class="template-grid">
|
||||
<button class="template-btn" data-template="header">Header</button>
|
||||
<button class="template-btn" data-template="note">Note Box</button>
|
||||
<button class="template-btn" data-template="warning">Warning</button>
|
||||
<button class="template-btn" data-template="info">Info Box</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="template-category">
|
||||
<div class="template-category-title">Decorative</div>
|
||||
<div class="template-grid">
|
||||
<button class="template-btn" data-template="divider">Divider</button>
|
||||
<button class="template-btn" data-template="separator">Separator</button>
|
||||
<button class="template-btn" data-template="banner">Banner</button>
|
||||
<button class="template-btn" data-template="checklist">Checklist</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-title">Preview</div>
|
||||
<div class="preview-container">
|
||||
<div id="preview" class="preview-content"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<button class="btn btn-secondary" id="btn-generate">Generate Preview</button>
|
||||
<button class="btn btn-primary" id="btn-insert">Insert to Editor</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ASCII Art Generator Logic
|
||||
const FONTS = {
|
||||
standard: {
|
||||
height: 5,
|
||||
chars: {
|
||||
'A': [' /\\ ', ' / \\ ', '/----\\', '| |', '| |'],
|
||||
'B': ['|----\\', '| |', '|----/', '| \\', '|----/'],
|
||||
'C': ['/----\\', '| ', '| ', '| ', '\\----/'],
|
||||
'D': ['|----\\', '| |', '| |', '| |', '|----/'],
|
||||
'E': ['|----', '| ', '|--- ', '| ', '|----'],
|
||||
'F': ['|----', '| ', '|--- ', '| ', '| '],
|
||||
'G': ['/----\\', '| ', '| |--\\', '| |', '\\----/'],
|
||||
'H': ['| |', '| |', '|----/', '| |', '| |'],
|
||||
'I': ['|---|', ' | ', ' | ', ' | ', '|---|'],
|
||||
'J': [' |', ' |', ' |', '| |', '\\---/'],
|
||||
'K': ['| /', '| / ', '|-- ', '| \\ ', '| \\'],
|
||||
'L': ['| ', '| ', '| ', '| ', '|----'],
|
||||
'M': ['|\\ /|', '| \\/ |', '| |', '| |', '| |'],
|
||||
'N': ['|\\ |', '| \\ |', '| \\ |', '| \\|', '| |'],
|
||||
'O': ['/----\\', '| |', '| |', '| |', '\\----/'],
|
||||
'P': ['|----\\', '| |', '|----/', '| ', '| '],
|
||||
'Q': ['/----\\', '| |', '| \\ |', '| \\|', '\\----\\'],
|
||||
'R': ['|----\\', '| |', '|----/', '| \\ ', '| \\ '],
|
||||
'S': ['/----\\', '| ', '\\----\\', ' |', '\\----/'],
|
||||
'T': ['-----', ' | ', ' | ', ' | ', ' | '],
|
||||
'U': ['| |', '| |', '| |', '| |', '\\----/'],
|
||||
'V': ['| |', '| |', ' \\ / ', ' \\/ ', ' '],
|
||||
'W': ['| |', '| |', '| |', '| /\\ |', '|/ \\|'],
|
||||
'X': ['\\ /', ' \\ / ', ' \\/ ', ' /\\ ', ' / \\ '],
|
||||
'Y': ['\\ /', ' \\ / ', ' | ', ' | ', ' | '],
|
||||
'Z': ['-----', ' / ', ' / ', ' / ', '-----'],
|
||||
' ': [' ', ' ', ' ', ' ', ' '],
|
||||
'0': ['/---\\', '| |', '| / |', '|/ |', '\\---/'],
|
||||
'1': [' /| ', ' / | ', ' | ', ' | ', ' ----'],
|
||||
'2': ['/---\\', ' |', ' ---/', '/ ', '-----'],
|
||||
'3': ['----\\', ' |', ' ---/', ' |', '----/'],
|
||||
'4': ['| |', '| |', '-----', ' |', ' |'],
|
||||
'5': ['-----', '| ', '----\\', ' |', '----/'],
|
||||
'6': ['/----', '| ', '|---\\', '| |', '\\---/'],
|
||||
'7': ['-----', ' / ', ' / ', ' / ', '/ '],
|
||||
'8': ['/---\\', '| |', ' --- ', '| |', '\\---/'],
|
||||
'9': ['/---\\', '| |', '\\----', ' |', '----/']
|
||||
}
|
||||
},
|
||||
banner: {
|
||||
height: 7,
|
||||
chars: {
|
||||
'A': [' ##### ', ' ## ##', '## ##', '#########', '## ##', '## ##', '## ##'],
|
||||
'B': ['######## ', '## ##', '## ##', '######## ', '## ##', '## ##', '######## '],
|
||||
'C': [' ###### ', '## ##', '## ', '## ', '## ', '## ##', ' ###### '],
|
||||
'D': ['######## ', '## ##', '## ##', '## ##', '## ##', '## ##', '######## '],
|
||||
'E': ['########', '## ', '## ', '###### ', '## ', '## ', '########'],
|
||||
'F': ['########', '## ', '## ', '###### ', '## ', '## ', '## '],
|
||||
'G': [' ###### ', '## ##', '## ', '## ####', '## ##', '## ##', ' ###### '],
|
||||
'H': ['## ##', '## ##', '## ##', '#########', '## ##', '## ##', '## ##'],
|
||||
'I': ['####', ' ## ', ' ## ', ' ## ', ' ## ', ' ## ', '####'],
|
||||
'J': [' ##', ' ##', ' ##', ' ##', '## ##', '## ##', ' ###### '],
|
||||
'K': ['## ##', '## ## ', '## ## ', '##### ', '## ## ', '## ## ', '## ##'],
|
||||
'L': ['## ', '## ', '## ', '## ', '## ', '## ', '########'],
|
||||
'M': ['## ##', '### ###', '#### ####', '## ### ##', '## ##', '## ##', '## ##'],
|
||||
'N': ['## ##', '### ##', '#### ##', '## ## ##', '## ####', '## ###', '## ##'],
|
||||
'O': [' ####### ', '## ##', '## ##', '## ##', '## ##', '## ##', ' ####### '],
|
||||
'P': ['######## ', '## ##', '## ##', '######## ', '## ', '## ', '## '],
|
||||
'Q': [' ####### ', '## ##', '## ##', '## ##', '## ## ##', '## ## ', ' ##### ##'],
|
||||
'R': ['######## ', '## ##', '## ##', '######## ', '## ## ', '## ## ', '## ##'],
|
||||
'S': [' ###### ', '## ##', '## ', ' ###### ', ' ##', '## ##', ' ###### '],
|
||||
'T': ['########', ' ## ', ' ## ', ' ## ', ' ## ', ' ## ', ' ## '],
|
||||
'U': ['## ##', '## ##', '## ##', '## ##', '## ##', '## ##', ' ####### '],
|
||||
'V': ['## ##', '## ##', '## ##', '## ##', ' ## ## ', ' ## ## ', ' ### '],
|
||||
'W': ['## ##', '## ## ##', '## ## ##', '## ## ##', '## ## ##', '## ## ##', ' ### ### '],
|
||||
'X': ['## ##', ' ## ## ', ' ## ## ', ' ### ', ' ## ## ', ' ## ## ', '## ##'],
|
||||
'Y': ['## ##', ' ## ## ', ' #### ', ' ## ', ' ## ', ' ## ', ' ## '],
|
||||
'Z': ['########', ' ## ', ' ## ', ' ## ', ' ## ', ' ## ', '########'],
|
||||
' ': [' ', ' ', ' ', ' ', ' ', ' ', ' '],
|
||||
'0': [' ###### ', '## ##', '## ##', '## ##', '## ##', '## ##', ' ###### '],
|
||||
'1': [' ## ', ' #### ', ' ## ', ' ## ', ' ## ', ' ## ', ' ###### '],
|
||||
'2': [' ###### ', '## ##', ' ## ', ' ## ', ' ## ', ' ## ', '########'],
|
||||
'3': [' ###### ', '## ##', ' ## ', ' #### ', ' ## ', '## ##', ' ###### '],
|
||||
'4': [' ## ', ' ### ', ' # ## ', ' # ## ', '########', ' ## ', ' ## '],
|
||||
'5': ['########', '## ', '####### ', ' ##', ' ##', '## ##', ' ###### '],
|
||||
'6': [' ###### ', '## ', '####### ', '## ##', '## ##', '## ##', ' ###### '],
|
||||
'7': ['########', '## ## ', ' ## ', ' ## ', ' ## ', ' ## ', ' ## '],
|
||||
'8': [' ###### ', '## ##', '## ##', ' ###### ', '## ##', '## ##', ' ###### '],
|
||||
'9': [' ###### ', '## ##', '## ##', ' #######', ' ##', '## ##', ' ###### ']
|
||||
}
|
||||
},
|
||||
block: {
|
||||
height: 6,
|
||||
chars: {
|
||||
'A': ['█████╗ ', '██╔══██╗', '███████║', '██╔══██║', '██║ ██║', '╚═╝ ╚═╝'],
|
||||
'B': ['██████╗ ', '██╔══██╗', '██████╔╝', '██╔══██╗', '██████╔╝', '╚═════╝ '],
|
||||
'C': ['█████╗ ', '██╔══██╗', '██║ ', '██║ ', '╚█████╔╝', ' ╚════╝ '],
|
||||
'D': ['██████╗ ', '██╔══██╗', '██║ ██║', '██║ ██║', '██████╔╝', '╚═════╝ '],
|
||||
'E': ['███████╗', '██╔════╝', '█████╗ ', '██╔══╝ ', '███████╗', '╚══════╝'],
|
||||
'F': ['███████╗', '██╔════╝', '█████╗ ', '██╔══╝ ', '██║ ', '╚═╝ '],
|
||||
'G': ['█████╗ ', '██╔══██╗', '██║ ███', '██║ ██', '╚█████╔╝', ' ╚════╝ '],
|
||||
'H': ['██╗ ██╗', '██║ ██║', '███████║', '██╔══██║', '██║ ██║', '╚═╝ ╚═╝'],
|
||||
'I': ['██╗', '██║', '██║', '██║', '██║', '╚═╝'],
|
||||
'J': [' ██╗', ' ██║', ' ██║', '██ ██║', '╚████╔╝', ' ╚═══╝ '],
|
||||
'K': ['██╗ ██╗', '██║ ██╔╝', '█████╔╝ ', '██╔═██╗ ', '██║ ██╗', '╚═╝ ╚═╝'],
|
||||
'L': ['██╗ ', '██║ ', '██║ ', '██║ ', '███████╗', '╚══════╝'],
|
||||
'M': ['███╗ ███╗', '████╗ ████║', '██╔████╔██║', '██║╚██╔╝██║', '██║ ╚═╝ ██║', '╚═╝ ╚═╝'],
|
||||
'N': ['███╗ ██╗', '████╗ ██║', '██╔██╗ ██║', '██║╚██╗██║', '██║ ╚████║', '╚═╝ ╚═══╝'],
|
||||
'O': ['█████╗ ', '██╔══██╗', '██║ ██║', '██║ ██║', '╚█████╔╝', ' ╚════╝ '],
|
||||
'P': ['██████╗ ', '██╔══██╗', '██████╔╝', '██╔═══╝ ', '██║ ', '╚═╝ '],
|
||||
'Q': ['█████╗ ', '██╔══██╗', '██║ ██║', '██║ ██║', '╚██████╗', ' ╚═══██╝'],
|
||||
'R': ['██████╗ ', '██╔══██╗', '██████╔╝', '██╔══██╗', '██║ ██║', '╚═╝ ╚═╝'],
|
||||
'S': ['█████╗ ', '██╔══╝ ', '█████╗ ', '╚══██║ ', '█████║ ', '╚════╝ '],
|
||||
'T': ['████████╗', '╚══██╔══╝', ' ██║ ', ' ██║ ', ' ██║ ', ' ╚═╝ '],
|
||||
'U': ['██╗ ██╗', '██║ ██║', '██║ ██║', '██║ ██║', '╚█████╔╝', ' ╚════╝ '],
|
||||
'V': ['██╗ ██╗', '██║ ██║', '██║ ██║', '╚██╗ ██╔╝', ' ╚████╔╝ ', ' ╚═══╝ '],
|
||||
'W': ['██╗ ██╗', '██║ ██║', '██║ █╗ ██║', '██║███╗██║', '╚███╔███╔╝', ' ╚══╝╚══╝ '],
|
||||
'X': ['██╗ ██╗', '╚██╗██╔╝', ' ╚███╔╝ ', ' ██╔██╗ ', '██╔╝ ██╗', '╚═╝ ╚═╝'],
|
||||
'Y': ['██╗ ██╗', '╚██╗ ██╔╝', ' ╚████╔╝ ', ' ╚██╔╝ ', ' ██║ ', ' ╚═╝ '],
|
||||
'Z': ['███████╗', '╚════██║', ' ███╔═╝', ' ██╔══╝ ', '███████╗', '╚══════╝'],
|
||||
' ': [' ', ' ', ' ', ' ', ' ', ' ']
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const TEMPLATES = {
|
||||
'arrow-right': ' ┌─────────────────────┐\n──▶│ Process or Action │──▶\n └─────────────────────┘',
|
||||
'arrow-down': ' │\n ▼\n┌───────────────┐\n│ Process │\n└───────────────┘\n │\n ▼',
|
||||
'decision': ' ╱╲\n ╱ ╲\n ╱ ? ╲\n ╱ ╲\n ╱────────╲\n ╱ ╲\n YES NO\n │ │\n ▼ ▼',
|
||||
'process': '┌─────┐ ┌─────┐ ┌─────┐\n│ 1 │──▶│ 2 │──▶│ 3 │\n└─────┘ └─────┘ └─────┘',
|
||||
'flowchart': '┌─────────────┐\n│ START │\n└──────┬──────┘\n │\n ▼\n┌─────────────┐\n│ Process A │\n└──────┬──────┘\n │\n ▼\n ╱────────╲\n ╱ Decision ╲\n ╲ ? ╱\n ╲────────╱\n │ │\n YES NO\n │ │\n ▼ ▼\n┌──────┐ ┌──────┐\n│ B │ │ C │\n└──────┘ └──────┘',
|
||||
'sequence': ' User System Database\n │ │ │\n │ Request │ │\n ├──────────►│ │\n │ │ Query │\n │ ├──────────►│\n │ │ │\n │ │ Result │\n │ │◄──────────┤\n │ Response │ │\n │◄──────────┤ │\n │ │ │',
|
||||
'network': ' ┌─────────┐\n │ Server │\n └────┬────┘\n │\n ┌─────────┼─────────┐\n │ │ │\n┌────┴────┐ ┌──┴──┐ ┌────┴────┐\n│ Client1 │ │ DB │ │ Client2 │\n└─────────┘ └─────┘ └─────────┘',
|
||||
'hierarchy': ' ┌─────────┐\n │ CEO │\n └────┬────┘\n ┌─────────┼─────────┐\n │ │ │\n ┌───┴───┐ ┌───┴───┐ ┌───┴───┐\n │ VP1 │ │ VP2 │ │ VP3 │\n └───┬───┘ └───┬───┘ └───┬───┘\n │ │ │\n ┌───┴───┐ ┌───┴───┐ ┌───┴───┐\n │ Team1 │ │ Team2 │ │ Team3 │\n └───────┘ └───────┘ └───────┘',
|
||||
'header': '╔════════════════════════════════════╗\n║ SECTION TITLE ║\n╚════════════════════════════════════╝',
|
||||
'note': '┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓\n┃ NOTE: ┃\n┃ This is an important note ┃\n┃ that requires attention! ┃\n┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛',
|
||||
'warning': '╔════════════════════════════════════╗\n║ ⚠️ WARNING ║\n║ ║\n║ Critical information here! ║\n╚════════════════════════════════════╝',
|
||||
'info': '╭────────────────────────────────────╮\n│ ℹ️ INFO │\n│ │\n│ Helpful information here. │\n╰────────────────────────────────────╯',
|
||||
'divider': '════════════════════════════════════════',
|
||||
'separator': '╭──────────────────────────────────────╮\n│ │\n╰──────────────────────────────────────╯',
|
||||
'banner': '★══════════════════════════════════════★\n║ YOUR TITLE HERE ║\n★══════════════════════════════════════★',
|
||||
'checklist': '☐ Task 1 - Not completed\n☑ Task 2 - Completed \n☐ Task 3 - Not completed\n☐ Task 4 - Not completed'
|
||||
};
|
||||
|
||||
const BOX_STYLES = {
|
||||
single: { tl: '┌', tr: '┐', bl: '└', br: '┘', h: '─', v: '│' },
|
||||
double: { tl: '╔', tr: '╗', bl: '╚', br: '╝', h: '═', v: '║' },
|
||||
rounded: { tl: '╭', tr: '╮', bl: '╰', br: '╯', h: '─', v: '│' },
|
||||
bold: { tl: '┏', tr: '┓', bl: '┗', br: '┛', h: '━', v: '┃' },
|
||||
ascii: { tl: '+', tr: '+', bl: '+', br: '+', h: '-', v: '|' }
|
||||
};
|
||||
|
||||
let currentMode = 'text';
|
||||
let currentTemplate = null;
|
||||
|
||||
// Mode switching
|
||||
document.querySelectorAll('.mode-tab').forEach(tab => {
|
||||
tab.addEventListener('click', () => {
|
||||
document.querySelectorAll('.mode-tab').forEach(t => t.classList.remove('active'));
|
||||
tab.classList.add('active');
|
||||
currentMode = tab.dataset.mode;
|
||||
|
||||
document.querySelectorAll('.mode-section').forEach(s => s.classList.remove('active'));
|
||||
document.getElementById(currentMode + '-mode').classList.add('active');
|
||||
|
||||
generatePreview();
|
||||
});
|
||||
});
|
||||
|
||||
// Template selection
|
||||
document.querySelectorAll('.template-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
document.querySelectorAll('.template-btn').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
currentTemplate = btn.dataset.template;
|
||||
generatePreview();
|
||||
});
|
||||
});
|
||||
|
||||
// Generate text banner
|
||||
function generateTextBanner(text, style) {
|
||||
const font = FONTS[style] || FONTS.standard;
|
||||
const lines = Array(font.height).fill('');
|
||||
|
||||
for (const char of text.toUpperCase()) {
|
||||
const charArt = font.chars[char] || font.chars[' '];
|
||||
if (charArt) {
|
||||
for (let i = 0; i < font.height; i++) {
|
||||
lines[i] += charArt[i] + ' ';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
// Generate box
|
||||
function generateBox(text, style, padding) {
|
||||
const box = BOX_STYLES[style] || BOX_STYLES.single;
|
||||
const lines = text.split('\n');
|
||||
const maxLen = Math.max(...lines.map(l => l.length)) + padding * 2;
|
||||
|
||||
let result = box.tl + box.h.repeat(maxLen + 2) + box.tr + '\n';
|
||||
|
||||
// Add padding lines at top
|
||||
for (let i = 0; i < Math.floor(padding / 2); i++) {
|
||||
result += box.v + ' '.repeat(maxLen + 2) + box.v + '\n';
|
||||
}
|
||||
|
||||
// Add text lines
|
||||
for (const line of lines) {
|
||||
const paddedLine = ' '.repeat(padding) + line.padEnd(maxLen - padding) + ' ';
|
||||
result += box.v + ' ' + paddedLine + box.v + '\n';
|
||||
}
|
||||
|
||||
// Add padding lines at bottom
|
||||
for (let i = 0; i < Math.floor(padding / 2); i++) {
|
||||
result += box.v + ' '.repeat(maxLen + 2) + box.v + '\n';
|
||||
}
|
||||
|
||||
result += box.bl + box.h.repeat(maxLen + 2) + box.br;
|
||||
return result;
|
||||
}
|
||||
|
||||
// Generate preview
|
||||
function generatePreview() {
|
||||
let result = '';
|
||||
|
||||
if (currentMode === 'text') {
|
||||
const text = document.getElementById('text-input').value || 'HELLO';
|
||||
const style = document.getElementById('font-style').value;
|
||||
result = generateTextBanner(text, style);
|
||||
} else if (currentMode === 'box') {
|
||||
const text = document.getElementById('box-text').value || 'Your text here';
|
||||
const style = document.getElementById('box-style').value;
|
||||
const padding = parseInt(document.getElementById('box-padding').value) || 2;
|
||||
result = generateBox(text, style, padding);
|
||||
} else if (currentMode === 'templates') {
|
||||
result = currentTemplate ? TEMPLATES[currentTemplate] : 'Select a template...';
|
||||
}
|
||||
|
||||
document.getElementById('preview').textContent = result;
|
||||
}
|
||||
|
||||
// Event listeners for live preview
|
||||
document.getElementById('text-input').addEventListener('input', generatePreview);
|
||||
document.getElementById('font-style').addEventListener('change', generatePreview);
|
||||
document.getElementById('box-text').addEventListener('input', generatePreview);
|
||||
document.getElementById('box-style').addEventListener('change', generatePreview);
|
||||
document.getElementById('box-padding').addEventListener('input', generatePreview);
|
||||
|
||||
document.getElementById('btn-generate').addEventListener('click', generatePreview);
|
||||
|
||||
document.getElementById('btn-insert').addEventListener('click', () => {
|
||||
const content = document.getElementById('preview').textContent;
|
||||
if (content && window.electronAPI) {
|
||||
// Wrap in code block for markdown
|
||||
const wrapped = '```\n' + content + '\n```';
|
||||
window.electronAPI.send('insert-generated-content', wrapped);
|
||||
window.close();
|
||||
}
|
||||
});
|
||||
|
||||
// Initial preview
|
||||
generatePreview();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,109 +0,0 @@
|
||||
class CommandPalette {
|
||||
constructor() {
|
||||
this.overlay = document.getElementById('command-palette-overlay');
|
||||
this.input = document.getElementById('command-palette-input');
|
||||
this.results = document.getElementById('command-palette-results');
|
||||
this.commands = [];
|
||||
this.selectedIndex = 0;
|
||||
this.filteredCommands = [];
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
register(label, shortcut, action) {
|
||||
this.commands.push({ label, shortcut, action });
|
||||
}
|
||||
|
||||
open() {
|
||||
this.overlay.classList.remove('hidden');
|
||||
this.input.value = '';
|
||||
this.input.focus();
|
||||
this.selectedIndex = 0;
|
||||
this.renderResults('');
|
||||
}
|
||||
|
||||
close() {
|
||||
this.overlay.classList.add('hidden');
|
||||
}
|
||||
|
||||
isOpen() {
|
||||
return !this.overlay.classList.contains('hidden');
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
this.input.addEventListener('input', () => {
|
||||
this.selectedIndex = 0;
|
||||
this.renderResults(this.input.value);
|
||||
});
|
||||
|
||||
this.overlay.addEventListener('click', (e) => {
|
||||
if (e.target === this.overlay) this.close();
|
||||
});
|
||||
|
||||
this.input.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
this.close();
|
||||
} else if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
this.executeSelected();
|
||||
} else if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
this.selectedIndex = Math.min(this.selectedIndex + 1, this.filteredCommands.length - 1);
|
||||
this.updateSelection();
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
this.selectedIndex = Math.max(this.selectedIndex - 1, 0);
|
||||
this.updateSelection();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
renderResults(query) {
|
||||
this.filteredCommands = query
|
||||
? this.commands.filter(cmd => cmd.label.toLowerCase().includes(query.toLowerCase()))
|
||||
: [...this.commands];
|
||||
|
||||
this.results.innerHTML = this.filteredCommands.map((cmd, i) => `
|
||||
<div class="command-item ${i === this.selectedIndex ? 'selected' : ''}" data-index="${i}">
|
||||
<span class="command-label">${this.highlightMatch(cmd.label, query)}</span>
|
||||
${cmd.shortcut ? `<span class="command-shortcut">${cmd.shortcut}</span>` : ''}
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
this.results.querySelectorAll('.command-item').forEach((el) => {
|
||||
el.addEventListener('click', () => {
|
||||
const idx = parseInt(el.dataset.index);
|
||||
this.filteredCommands[idx].action();
|
||||
this.close();
|
||||
});
|
||||
el.addEventListener('mouseenter', () => {
|
||||
this.selectedIndex = parseInt(el.dataset.index);
|
||||
this.updateSelection();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
highlightMatch(text, query) {
|
||||
if (!query) return text;
|
||||
const regex = new RegExp(`(${query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
|
||||
return text.replace(regex, '<strong>$1</strong>');
|
||||
}
|
||||
|
||||
updateSelection() {
|
||||
this.results.querySelectorAll('.command-item').forEach((el, i) => {
|
||||
el.classList.toggle('selected', i === this.selectedIndex);
|
||||
});
|
||||
// Scroll selected into view
|
||||
const selected = this.results.querySelector('.command-item.selected');
|
||||
if (selected) selected.scrollIntoView({ block: 'nearest' });
|
||||
}
|
||||
|
||||
executeSelected() {
|
||||
if (this.filteredCommands[this.selectedIndex]) {
|
||||
this.filteredCommands[this.selectedIndex].action();
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { CommandPalette };
|
||||
@@ -1,75 +0,0 @@
|
||||
/* Local Font Definitions for MarkdownConverter */
|
||||
|
||||
/* Inter Font Family */
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
src: url('../assets/fonts/Inter-Light.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('../assets/fonts/Inter-Regular.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url('../assets/fonts/Inter-Medium.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
src: url('../assets/fonts/Inter-SemiBold.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url('../assets/fonts/Inter-Bold.woff2') format('woff2');
|
||||
}
|
||||
|
||||
/* JetBrains Mono Font Family - For code, markdown editor, and ASCII art */
|
||||
@font-face {
|
||||
font-family: 'JetBrains Mono';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('../assets/fonts/JetBrainsMono-Regular.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'JetBrains Mono';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url('../assets/fonts/JetBrainsMono-Medium.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'JetBrains Mono';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
src: url('../assets/fonts/JetBrainsMono-SemiBold.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'JetBrains Mono';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url('../assets/fonts/JetBrainsMono-Bold.woff2') format('woff2');
|
||||
}
|
||||
-1667
File diff suppressed because it is too large
Load Diff
@@ -1,3 +0,0 @@
|
||||
// src/main.js — DEPRECATED, will be removed in Task 8
|
||||
// See src/main/index.js for the new modular entrypoint
|
||||
module.exports = require('./main/index.js');
|
||||
@@ -22,7 +22,7 @@ function createMainWindow() {
|
||||
icon: path.join(__dirname, '../../assets/icon.png')
|
||||
});
|
||||
|
||||
win.loadFile(path.join(__dirname, '../../index.html'));
|
||||
win.loadFile(path.join(__dirname, '../../renderer/index.html'));
|
||||
|
||||
// Show window only after content is ready — avoids blank flash
|
||||
win.once('ready-to-show', () => {
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
class PrintPreview {
|
||||
constructor() {
|
||||
this.overlay = document.getElementById('print-preview-overlay');
|
||||
this.modal = window.modals?.printPreviewModal;
|
||||
this._lastContent = '';
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
open(htmlContent) {
|
||||
this._lastContent = htmlContent;
|
||||
if (this.modal) {
|
||||
this.modal.open();
|
||||
} else {
|
||||
this.overlay.classList.remove('hidden');
|
||||
}
|
||||
this.updatePreview(htmlContent);
|
||||
this.updateScaleLabel();
|
||||
}
|
||||
|
||||
close() {
|
||||
if (this.modal) {
|
||||
this.modal.close();
|
||||
} else {
|
||||
this.overlay.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
document.getElementById('print-preview-close')?.addEventListener('click', () => this.close());
|
||||
document.getElementById('print-cancel')?.addEventListener('click', () => this.close());
|
||||
document.getElementById('print-execute')?.addEventListener('click', () => this.executePrint());
|
||||
|
||||
// Update preview on option changes
|
||||
['print-paper-size', 'print-orientation', 'print-margins'].forEach(id => {
|
||||
document.getElementById(id)?.addEventListener('change', () => this.refreshPreview());
|
||||
});
|
||||
|
||||
// Scale slider
|
||||
const scaleSlider = document.getElementById('print-scale');
|
||||
scaleSlider?.addEventListener('input', () => this.updateScaleLabel());
|
||||
|
||||
// Page range toggle
|
||||
document.getElementById('print-pages')?.addEventListener('change', (e) => {
|
||||
const rangeInput = document.getElementById('print-page-range');
|
||||
if (rangeInput) {
|
||||
rangeInput.classList.toggle('hidden', e.target.value !== 'custom');
|
||||
}
|
||||
});
|
||||
|
||||
// Note: Backdrop click and Escape key are now handled by ModalManager
|
||||
}
|
||||
|
||||
updateScaleLabel() {
|
||||
const scale = document.getElementById('print-scale')?.value || 100;
|
||||
const label = document.getElementById('print-scale-value');
|
||||
if (label) label.textContent = `${scale}%`;
|
||||
}
|
||||
|
||||
updatePreview(htmlContent) {
|
||||
const frame = document.getElementById('print-preview-frame');
|
||||
if (!frame) return;
|
||||
|
||||
this._lastContent = htmlContent;
|
||||
|
||||
const orientation = document.getElementById('print-orientation')?.value || 'portrait';
|
||||
const paperSize = document.getElementById('print-paper-size')?.value || 'A4';
|
||||
|
||||
// Get dimensions for paper size
|
||||
const sizes = {
|
||||
'A3': { width: '297mm', height: '420mm' },
|
||||
'A4': { width: '210mm', height: '297mm' },
|
||||
'A5': { width: '148mm', height: '210mm' },
|
||||
'Letter': { width: '8.5in', height: '11in' },
|
||||
'Legal': { width: '8.5in', height: '14in' },
|
||||
'Tabloid': { width: '11in', height: '17in' },
|
||||
};
|
||||
|
||||
const size = sizes[paperSize] || sizes['A4'];
|
||||
const width = orientation === 'landscape' ? size.height : size.width;
|
||||
const height = orientation === 'landscape' ? size.width : size.height;
|
||||
|
||||
const previewHtml = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
margin: 20px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
@page { size: ${width} ${height}; }
|
||||
pre { background: #f5f5f5; padding: 12px; border-radius: 6px; overflow-x: auto; }
|
||||
code { background: #f0f0f0; padding: 2px 6px; border-radius: 3px; font-size: 13px; }
|
||||
pre code { background: none; padding: 0; }
|
||||
table { border-collapse: collapse; width: 100%; }
|
||||
th, td { border: 1px solid #ddd; padding: 8px; }
|
||||
blockquote { border-left: 4px solid #ddd; margin-left: 0; padding-left: 16px; color: #666; }
|
||||
img { max-width: 100%; }
|
||||
h1, h2, h3 { margin-top: 1.5em; }
|
||||
</style>
|
||||
</head>
|
||||
<body>${htmlContent || ''}</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
frame.srcdoc = previewHtml;
|
||||
}
|
||||
|
||||
refreshPreview() {
|
||||
if (this._lastContent) {
|
||||
this.updatePreview(this._lastContent);
|
||||
}
|
||||
}
|
||||
|
||||
getOptions() {
|
||||
return {
|
||||
paperSize: document.getElementById('print-paper-size')?.value || 'A4',
|
||||
orientation: document.getElementById('print-orientation')?.value || 'portrait',
|
||||
margins: document.getElementById('print-margins')?.value || 'default',
|
||||
scale: parseInt(document.getElementById('print-scale')?.value || '100'),
|
||||
headers: document.getElementById('print-headers')?.checked ?? true,
|
||||
background: document.getElementById('print-background')?.checked ?? true,
|
||||
pages: document.getElementById('print-pages')?.value || 'all',
|
||||
pageRange: document.getElementById('print-page-range')?.value || '',
|
||||
};
|
||||
}
|
||||
|
||||
executePrint() {
|
||||
const options = this.getOptions();
|
||||
const { ipcRenderer } = require('electron');
|
||||
ipcRenderer.send('do-print-with-options', options);
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { PrintPreview };
|
||||
-5319
File diff suppressed because it is too large
Load Diff
@@ -1,969 +0,0 @@
|
||||
/**
|
||||
* ConcreteInfo Theme for MarkdownConverter
|
||||
* Based on logo palette: #464646, #9a9696, #e5461f, #e3e3e3, #0d0b09
|
||||
* Version: 4.1.0
|
||||
*/
|
||||
|
||||
/* ============================================
|
||||
CSS Variables - ConcreteInfo Theme
|
||||
============================================ */
|
||||
:root {
|
||||
/* Primary Colors from Logo Palette */
|
||||
--ci-dark-gray: #464646;
|
||||
--ci-medium-gray: #9a9696;
|
||||
--ci-accent: #e5461f;
|
||||
--ci-light-gray: #e3e3e3;
|
||||
--ci-black: #0d0b09;
|
||||
|
||||
/* Extended Palette */
|
||||
--ci-accent-hover: #c93a18;
|
||||
--ci-accent-light: rgba(229, 70, 31, 0.1);
|
||||
--ci-white: #ffffff;
|
||||
--ci-bg: #f5f5f5;
|
||||
--ci-border: #d0d0d0;
|
||||
|
||||
/* Semantic Colors */
|
||||
--ci-success: #28a745;
|
||||
--ci-warning: #ffc107;
|
||||
--ci-danger: #dc3545;
|
||||
--ci-info: #17a2b8;
|
||||
|
||||
/* Typography */
|
||||
--font-sans: 'Inter', 'Segoe UI', system-ui, -apple-system, sans-serif;
|
||||
--font-mono: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
|
||||
|
||||
/* Spacing */
|
||||
--spacing-xs: 4px;
|
||||
--spacing-sm: 8px;
|
||||
--spacing-md: 16px;
|
||||
--spacing-lg: 24px;
|
||||
--spacing-xl: 32px;
|
||||
|
||||
/* Border Radius */
|
||||
--radius-sm: 4px;
|
||||
--radius-md: 8px;
|
||||
--radius-lg: 12px;
|
||||
--radius-full: 9999px;
|
||||
|
||||
/* Shadows */
|
||||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
|
||||
--shadow-xl: 0 20px 25px rgba(0, 0, 0, 0.15);
|
||||
|
||||
/* Transitions */
|
||||
--transition-fast: 150ms ease;
|
||||
--transition-normal: 250ms ease;
|
||||
--transition-slow: 350ms ease;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
ConcreteInfo Theme Application
|
||||
============================================ */
|
||||
body.theme-concreteinfo {
|
||||
background: var(--ci-bg);
|
||||
color: var(--ci-dark-gray);
|
||||
}
|
||||
|
||||
body.theme-concreteinfo .toolbar {
|
||||
background: linear-gradient(135deg, var(--ci-dark-gray) 0%, var(--ci-black) 100%);
|
||||
border-bottom: 3px solid var(--ci-accent);
|
||||
}
|
||||
|
||||
body.theme-concreteinfo .toolbar button {
|
||||
color: var(--ci-white);
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
body.theme-concreteinfo .toolbar button:hover {
|
||||
background: var(--ci-accent);
|
||||
border-color: var(--ci-accent);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
body.theme-concreteinfo .tab-bar {
|
||||
background: var(--ci-dark-gray);
|
||||
border-bottom: 2px solid var(--ci-accent);
|
||||
}
|
||||
|
||||
body.theme-concreteinfo .tab {
|
||||
background: var(--ci-medium-gray);
|
||||
color: var(--ci-white);
|
||||
border: none;
|
||||
margin: 2px;
|
||||
border-radius: var(--radius-sm) var(--radius-sm) 0 0;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
body.theme-concreteinfo .tab:hover {
|
||||
background: var(--ci-accent-light);
|
||||
}
|
||||
|
||||
body.theme-concreteinfo .tab.active {
|
||||
background: var(--ci-accent);
|
||||
color: var(--ci-white);
|
||||
}
|
||||
|
||||
body.theme-concreteinfo .editor-textarea {
|
||||
background: var(--ci-white);
|
||||
color: var(--ci-black);
|
||||
border: 1px solid var(--ci-border);
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
body.theme-concreteinfo .preview-content {
|
||||
background: var(--ci-white);
|
||||
color: var(--ci-dark-gray);
|
||||
border-left: 3px solid var(--ci-accent);
|
||||
}
|
||||
|
||||
body.theme-concreteinfo .status-bar {
|
||||
background: var(--ci-dark-gray);
|
||||
color: var(--ci-light-gray);
|
||||
border-top: 2px solid var(--ci-accent);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Modern Modal Styles
|
||||
============================================ */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(13, 11, 9, 0.7);
|
||||
backdrop-filter: blur(4px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10000;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all var(--transition-normal);
|
||||
}
|
||||
|
||||
.modal-overlay.active {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: var(--ci-white);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-xl);
|
||||
max-width: 600px;
|
||||
width: 90%;
|
||||
max-height: 85vh;
|
||||
overflow: hidden;
|
||||
transform: scale(0.9) translateY(20px);
|
||||
transition: transform var(--transition-normal);
|
||||
}
|
||||
|
||||
.modal-overlay.active .modal-content {
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
background: linear-gradient(135deg, var(--ci-dark-gray) 0%, var(--ci-black) 100%);
|
||||
color: var(--ci-white);
|
||||
padding: var(--spacing-md) var(--spacing-lg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 3px solid var(--ci-accent);
|
||||
}
|
||||
|
||||
.modal-header h3 {
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--ci-white);
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: var(--radius-full);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.modal-close:hover {
|
||||
background: var(--ci-accent);
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: var(--spacing-lg);
|
||||
overflow-y: auto;
|
||||
max-height: 60vh;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
padding: var(--spacing-md) var(--spacing-lg);
|
||||
background: var(--ci-light-gray);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: var(--spacing-sm);
|
||||
border-top: 1px solid var(--ci-border);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Button Styles
|
||||
============================================ */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--spacing-xs);
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
font-family: var(--font-sans);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
border-radius: var(--radius-md);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--ci-accent);
|
||||
color: var(--ci-white);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--ci-accent-hover);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--ci-medium-gray);
|
||||
color: var(--ci-white);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--ci-dark-gray);
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
background: transparent;
|
||||
border: 2px solid var(--ci-accent);
|
||||
color: var(--ci-accent);
|
||||
}
|
||||
|
||||
.btn-outline:hover {
|
||||
background: var(--ci-accent);
|
||||
color: var(--ci-white);
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
background: transparent;
|
||||
color: var(--ci-dark-gray);
|
||||
}
|
||||
|
||||
.btn-ghost:hover {
|
||||
background: var(--ci-light-gray);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Form Controls
|
||||
============================================ */
|
||||
.form-group {
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
margin-bottom: var(--spacing-xs);
|
||||
font-weight: 500;
|
||||
color: var(--ci-dark-gray);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.form-input,
|
||||
.form-select {
|
||||
width: 100%;
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
font-family: var(--font-sans);
|
||||
font-size: 0.875rem;
|
||||
border: 2px solid var(--ci-border);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--ci-white);
|
||||
color: var(--ci-dark-gray);
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.form-input:focus,
|
||||
.form-select:focus {
|
||||
outline: none;
|
||||
border-color: var(--ci-accent);
|
||||
box-shadow: 0 0 0 3px var(--ci-accent-light);
|
||||
}
|
||||
|
||||
.form-input::placeholder {
|
||||
color: var(--ci-medium-gray);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
App Header with Logo
|
||||
============================================ */
|
||||
.app-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 16px;
|
||||
background: #ffffff;
|
||||
border-bottom: 2px solid #e1e4e8;
|
||||
min-height: 36px;
|
||||
}
|
||||
|
||||
.app-header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.app-header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.app-logo {
|
||||
height: 22px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.app-title {
|
||||
color: #24292e;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
.app-version {
|
||||
color: #6a737d;
|
||||
font-size: 0.7rem;
|
||||
font-family: 'SFMono-Regular', Consolas, monospace;
|
||||
}
|
||||
|
||||
/* Dark theme header adjustments */
|
||||
body.theme-dark .app-header,
|
||||
body.theme-dracula .app-header,
|
||||
body.theme-onedark .app-header,
|
||||
body.theme-tokyonight .app-header,
|
||||
body.theme-monokai .app-header,
|
||||
body.theme-cobalt2 .app-header,
|
||||
body.theme-gruvbox-dark .app-header,
|
||||
body.theme-ayu-dark .app-header,
|
||||
body.theme-ayu-mirage .app-header,
|
||||
body.theme-palenight .app-header,
|
||||
body.theme-oceanic-next .app-header,
|
||||
body.theme-nord .app-header,
|
||||
body.theme-concrete-dark .app-header {
|
||||
background: #1f2937;
|
||||
border-bottom-color: #374151;
|
||||
}
|
||||
|
||||
body.theme-dark .app-title,
|
||||
body.theme-dracula .app-title,
|
||||
body.theme-onedark .app-title,
|
||||
body.theme-tokyonight .app-title,
|
||||
body.theme-monokai .app-title,
|
||||
body.theme-cobalt2 .app-title,
|
||||
body.theme-gruvbox-dark .app-title,
|
||||
body.theme-ayu-dark .app-title,
|
||||
body.theme-ayu-mirage .app-title,
|
||||
body.theme-palenight .app-title,
|
||||
body.theme-oceanic-next .app-title,
|
||||
body.theme-nord .app-title,
|
||||
body.theme-concrete-dark .app-title {
|
||||
color: #f3f4f6;
|
||||
}
|
||||
|
||||
body.theme-dark .app-version,
|
||||
body.theme-dracula .app-version,
|
||||
body.theme-onedark .app-version,
|
||||
body.theme-tokyonight .app-version,
|
||||
body.theme-monokai .app-version,
|
||||
body.theme-cobalt2 .app-version,
|
||||
body.theme-gruvbox-dark .app-version,
|
||||
body.theme-ayu-dark .app-version,
|
||||
body.theme-ayu-mirage .app-version,
|
||||
body.theme-palenight .app-version,
|
||||
body.theme-oceanic-next .app-version,
|
||||
body.theme-nord .app-version,
|
||||
body.theme-concrete-dark .app-version {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
body.theme-dark .app-logo,
|
||||
body.theme-dracula .app-logo,
|
||||
body.theme-onedark .app-logo,
|
||||
body.theme-tokyonight .app-logo,
|
||||
body.theme-monokai .app-logo,
|
||||
body.theme-cobalt2 .app-logo,
|
||||
body.theme-gruvbox-dark .app-logo,
|
||||
body.theme-ayu-dark .app-logo,
|
||||
body.theme-ayu-mirage .app-logo,
|
||||
body.theme-palenight .app-logo,
|
||||
body.theme-oceanic-next .app-logo,
|
||||
body.theme-nord .app-logo,
|
||||
body.theme-concrete-dark .app-logo {
|
||||
filter: none;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Converter Cards
|
||||
============================================ */
|
||||
.converter-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: var(--spacing-md);
|
||||
padding: var(--spacing-md);
|
||||
}
|
||||
|
||||
.converter-card {
|
||||
background: var(--ci-white);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--spacing-lg);
|
||||
text-align: center;
|
||||
border: 2px solid var(--ci-border);
|
||||
transition: all var(--transition-fast);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.converter-card:hover {
|
||||
border-color: var(--ci-accent);
|
||||
transform: translateY(-4px);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.converter-card-icon {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.converter-card-title {
|
||||
font-weight: 600;
|
||||
color: var(--ci-dark-gray);
|
||||
margin-bottom: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.converter-card-desc {
|
||||
font-size: 0.875rem;
|
||||
color: var(--ci-medium-gray);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Progress Bar
|
||||
============================================ */
|
||||
.progress-bar {
|
||||
height: 8px;
|
||||
background: var(--ci-light-gray);
|
||||
border-radius: var(--radius-full);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--ci-accent) 0%, var(--ci-accent-hover) 100%);
|
||||
border-radius: var(--radius-full);
|
||||
transition: width var(--transition-normal);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
File Drop Zone
|
||||
============================================ */
|
||||
.drop-zone {
|
||||
border: 2px dashed var(--ci-border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--spacing-xl);
|
||||
text-align: center;
|
||||
transition: all var(--transition-fast);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.drop-zone:hover,
|
||||
.drop-zone.drag-over {
|
||||
border-color: var(--ci-accent);
|
||||
background: var(--ci-accent-light);
|
||||
}
|
||||
|
||||
.drop-zone-icon {
|
||||
font-size: 3rem;
|
||||
color: var(--ci-medium-gray);
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.drop-zone-text {
|
||||
color: var(--ci-dark-gray);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.drop-zone-hint {
|
||||
color: var(--ci-medium-gray);
|
||||
font-size: 0.875rem;
|
||||
margin-top: var(--spacing-xs);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Syntax Highlighting for Editor
|
||||
============================================ */
|
||||
.editor-syntax .md-heading {
|
||||
color: var(--ci-accent);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.editor-syntax .md-bold {
|
||||
color: var(--ci-dark-gray);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.editor-syntax .md-italic {
|
||||
color: var(--ci-dark-gray);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.editor-syntax .md-code {
|
||||
background: var(--ci-light-gray);
|
||||
color: var(--ci-accent);
|
||||
font-family: var(--font-mono);
|
||||
padding: 2px 4px;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.editor-syntax .md-link {
|
||||
color: var(--ci-info);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.editor-syntax .md-list {
|
||||
color: var(--ci-accent);
|
||||
}
|
||||
|
||||
.editor-syntax .md-blockquote {
|
||||
color: var(--ci-medium-gray);
|
||||
border-left: 3px solid var(--ci-accent);
|
||||
padding-left: var(--spacing-md);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
ASCII Art Generator Styles
|
||||
============================================ */
|
||||
.ascii-preview {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12px;
|
||||
line-height: 1.2;
|
||||
background: var(--ci-black);
|
||||
color: #00ff00;
|
||||
padding: var(--spacing-md);
|
||||
border-radius: var(--radius-md);
|
||||
overflow-x: auto;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.ascii-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-sm);
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Table Generator Styles
|
||||
============================================ */
|
||||
.table-generator-grid {
|
||||
display: grid;
|
||||
gap: 1px;
|
||||
background: var(--ci-border);
|
||||
border: 1px solid var(--ci-border);
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.table-generator-cell {
|
||||
background: var(--ci-white);
|
||||
padding: var(--spacing-sm);
|
||||
min-width: 80px;
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
.table-generator-cell input {
|
||||
width: 100%;
|
||||
border: none;
|
||||
background: transparent;
|
||||
text-align: center;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.table-generator-cell.header {
|
||||
background: var(--ci-light-gray);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
PDF Viewer/Editor Styles
|
||||
============================================ */
|
||||
.pdf-viewer {
|
||||
background: var(--ci-dark-gray);
|
||||
padding: var(--spacing-md);
|
||||
border-radius: var(--radius-md);
|
||||
min-height: 400px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.pdf-page {
|
||||
background: var(--ci-white);
|
||||
box-shadow: var(--shadow-lg);
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.pdf-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
padding: var(--spacing-sm);
|
||||
background: var(--ci-light-gray);
|
||||
border-radius: var(--radius-md);
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Dark Mode for ConcreteInfo Theme
|
||||
============================================ */
|
||||
body.theme-concreteinfo-dark {
|
||||
--ci-bg: #1a1a1a;
|
||||
--ci-white: #2a2a2a;
|
||||
--ci-light-gray: #3a3a3a;
|
||||
--ci-border: #4a4a4a;
|
||||
}
|
||||
|
||||
body.theme-concreteinfo-dark .editor-textarea,
|
||||
body.theme-concreteinfo-dark .preview-content {
|
||||
background: #2a2a2a;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
body.theme-concreteinfo-dark .modal-content {
|
||||
background: #2a2a2a;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
body.theme-concreteinfo-dark .modal-body {
|
||||
background: #2a2a2a;
|
||||
}
|
||||
|
||||
body.theme-concreteinfo-dark .modal-footer {
|
||||
background: #1a1a1a;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Animations
|
||||
============================================ */
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from { transform: translateY(20px); opacity: 0; }
|
||||
to { transform: translateY(0); opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { transform: scale(1); }
|
||||
50% { transform: scale(1.05); }
|
||||
}
|
||||
|
||||
.animate-fade-in {
|
||||
animation: fadeIn var(--transition-normal);
|
||||
}
|
||||
|
||||
.animate-slide-up {
|
||||
animation: slideUp var(--transition-normal);
|
||||
}
|
||||
|
||||
.animate-pulse {
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Responsive Design
|
||||
============================================ */
|
||||
@media (max-width: 768px) {
|
||||
.modal-content {
|
||||
width: 95%;
|
||||
max-height: 90vh;
|
||||
}
|
||||
|
||||
.converter-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.app-header {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Concrete Dark Theme
|
||||
============================================ */
|
||||
body.theme-concrete-dark {
|
||||
background: #1a1a1a;
|
||||
color: #e3e3e3;
|
||||
}
|
||||
|
||||
body.theme-concrete-dark .toolbar {
|
||||
background: linear-gradient(135deg, #0d0b09 0%, #1a1a1a 100%);
|
||||
border-bottom: 3px solid #e5461f;
|
||||
}
|
||||
|
||||
body.theme-concrete-dark .toolbar button {
|
||||
color: #e3e3e3;
|
||||
}
|
||||
|
||||
body.theme-concrete-dark .toolbar button:hover {
|
||||
background: #e5461f;
|
||||
}
|
||||
|
||||
body.theme-concrete-dark .tab-bar {
|
||||
background: #0d0b09;
|
||||
border-bottom: 2px solid #e5461f;
|
||||
}
|
||||
|
||||
body.theme-concrete-dark .tab {
|
||||
background: #2a2a2a;
|
||||
color: #e3e3e3;
|
||||
border-radius: 4px 4px 0 0;
|
||||
}
|
||||
|
||||
body.theme-concrete-dark .tab.active {
|
||||
background: #464646;
|
||||
border-bottom: 2px solid #e5461f;
|
||||
}
|
||||
|
||||
body.theme-concrete-dark .editor-textarea,
|
||||
body.theme-concrete-dark .editor-pane {
|
||||
background: #1a1a1a;
|
||||
color: #e3e3e3;
|
||||
}
|
||||
|
||||
body.theme-concrete-dark .preview-content {
|
||||
background: #1a1a1a;
|
||||
color: #e3e3e3;
|
||||
}
|
||||
|
||||
body.theme-concrete-dark .status-bar {
|
||||
background: #0d0b09;
|
||||
color: #9a9696;
|
||||
border-top: 1px solid #464646;
|
||||
}
|
||||
|
||||
body.theme-concrete-dark .export-dialog-content,
|
||||
body.theme-concrete-dark .batch-dialog-content {
|
||||
background: #2a2a2a;
|
||||
color: #e3e3e3;
|
||||
}
|
||||
|
||||
body.theme-concrete-dark .export-dialog-header,
|
||||
body.theme-concrete-dark .batch-dialog-header {
|
||||
background: #1a1a1a;
|
||||
border-bottom: 2px solid #e5461f;
|
||||
}
|
||||
|
||||
body.theme-concrete-dark .export-dialog-footer,
|
||||
body.theme-concrete-dark .batch-dialog-footer {
|
||||
background: #1a1a1a;
|
||||
}
|
||||
|
||||
body.theme-concrete-dark input,
|
||||
body.theme-concrete-dark select,
|
||||
body.theme-concrete-dark textarea {
|
||||
background: #1a1a1a;
|
||||
color: #e3e3e3;
|
||||
border-color: #464646;
|
||||
}
|
||||
|
||||
body.theme-concrete-dark input:focus,
|
||||
body.theme-concrete-dark select:focus,
|
||||
body.theme-concrete-dark textarea:focus {
|
||||
border-color: #e5461f;
|
||||
}
|
||||
|
||||
body.theme-concrete-dark .line-numbers {
|
||||
background: #0d0b09;
|
||||
color: #9a9696;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Concrete Light Theme
|
||||
============================================ */
|
||||
body.theme-concrete-light {
|
||||
background: #f5f5f5;
|
||||
color: #464646;
|
||||
}
|
||||
|
||||
body.theme-concrete-light .toolbar {
|
||||
background: #ffffff;
|
||||
border-bottom: 3px solid #e5461f;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
body.theme-concrete-light .toolbar button {
|
||||
color: #464646;
|
||||
}
|
||||
|
||||
body.theme-concrete-light .toolbar button:hover {
|
||||
background: #e5461f;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
body.theme-concrete-light .tab-bar {
|
||||
background: #ffffff;
|
||||
border-bottom: 2px solid #e3e3e3;
|
||||
}
|
||||
|
||||
body.theme-concrete-light .tab {
|
||||
background: #f5f5f5;
|
||||
color: #464646;
|
||||
border-radius: 4px 4px 0 0;
|
||||
}
|
||||
|
||||
body.theme-concrete-light .tab.active {
|
||||
background: #ffffff;
|
||||
border-bottom: 2px solid #e5461f;
|
||||
color: #e5461f;
|
||||
}
|
||||
|
||||
body.theme-concrete-light .editor-textarea,
|
||||
body.theme-concrete-light .editor-pane {
|
||||
background: #ffffff;
|
||||
color: #464646;
|
||||
}
|
||||
|
||||
body.theme-concrete-light .preview-content {
|
||||
background: #ffffff;
|
||||
color: #464646;
|
||||
}
|
||||
|
||||
body.theme-concrete-light .status-bar {
|
||||
background: #ffffff;
|
||||
color: #9a9696;
|
||||
border-top: 1px solid #e3e3e3;
|
||||
}
|
||||
|
||||
body.theme-concrete-light .export-dialog-content,
|
||||
body.theme-concrete-light .batch-dialog-content {
|
||||
background: #ffffff;
|
||||
color: #464646;
|
||||
}
|
||||
|
||||
body.theme-concrete-light .export-dialog-header,
|
||||
body.theme-concrete-light .batch-dialog-header {
|
||||
background: #f5f5f5;
|
||||
border-bottom: 2px solid #e5461f;
|
||||
}
|
||||
|
||||
body.theme-concrete-light .line-numbers {
|
||||
background: #f5f5f5;
|
||||
color: #9a9696;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Concrete Warm Theme
|
||||
============================================ */
|
||||
body.theme-concrete-warm {
|
||||
background: #faf8f5;
|
||||
color: #464646;
|
||||
}
|
||||
|
||||
body.theme-concrete-warm .toolbar {
|
||||
background: linear-gradient(135deg, #464646 0%, #5a5a5a 100%);
|
||||
border-bottom: 3px solid #e5461f;
|
||||
}
|
||||
|
||||
body.theme-concrete-warm .toolbar button {
|
||||
color: #faf8f5;
|
||||
}
|
||||
|
||||
body.theme-concrete-warm .toolbar button:hover {
|
||||
background: #e5461f;
|
||||
}
|
||||
|
||||
body.theme-concrete-warm .tab-bar {
|
||||
background: #464646;
|
||||
border-bottom: 2px solid #e5461f;
|
||||
}
|
||||
|
||||
body.theme-concrete-warm .tab {
|
||||
background: #6a6a6a;
|
||||
color: #faf8f5;
|
||||
border-radius: 4px 4px 0 0;
|
||||
}
|
||||
|
||||
body.theme-concrete-warm .tab.active {
|
||||
background: #faf8f5;
|
||||
color: #e5461f;
|
||||
border-bottom: 2px solid #e5461f;
|
||||
}
|
||||
|
||||
body.theme-concrete-warm .editor-textarea,
|
||||
body.theme-concrete-warm .editor-pane {
|
||||
background: #faf8f5;
|
||||
color: #464646;
|
||||
}
|
||||
|
||||
body.theme-concrete-warm .preview-content {
|
||||
background: #faf8f5;
|
||||
color: #464646;
|
||||
}
|
||||
|
||||
body.theme-concrete-warm .status-bar {
|
||||
background: #f0ebe4;
|
||||
color: #9a9696;
|
||||
border-top: 1px solid #e3e3e3;
|
||||
}
|
||||
|
||||
body.theme-concrete-warm .export-dialog-content,
|
||||
body.theme-concrete-warm .batch-dialog-content {
|
||||
background: #faf8f5;
|
||||
color: #464646;
|
||||
}
|
||||
|
||||
body.theme-concrete-warm .export-dialog-header,
|
||||
body.theme-concrete-warm .batch-dialog-header {
|
||||
background: #f0ebe4;
|
||||
border-bottom: 2px solid #e5461f;
|
||||
}
|
||||
|
||||
body.theme-concrete-warm h1,
|
||||
body.theme-concrete-warm h2,
|
||||
body.theme-concrete-warm h3,
|
||||
body.theme-concrete-warm h4,
|
||||
body.theme-concrete-warm h5,
|
||||
body.theme-concrete-warm h6 {
|
||||
color: #e5461f;
|
||||
}
|
||||
|
||||
body.theme-concrete-warm a {
|
||||
color: #e5461f;
|
||||
}
|
||||
|
||||
body.theme-concrete-warm .line-numbers {
|
||||
background: #f0ebe4;
|
||||
color: #9a9696;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,304 +0,0 @@
|
||||
/* Sidebar */
|
||||
.main-content {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
height: 100%;
|
||||
transition: width 0.2s ease;
|
||||
}
|
||||
|
||||
.sidebar.collapsed {
|
||||
width: 48px;
|
||||
}
|
||||
|
||||
.sidebar:not(.collapsed) {
|
||||
width: 328px; /* 48px icons + 280px panel */
|
||||
}
|
||||
|
||||
.sidebar-icons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 48px;
|
||||
background: var(--gray-100, #f3f4f6);
|
||||
border-right: 1px solid var(--gray-200, #e5e7eb);
|
||||
padding: 8px 0;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.sidebar-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
color: var(--gray-500, #6b7280);
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.sidebar-icon:hover {
|
||||
background: var(--gray-200, #e5e7eb);
|
||||
color: var(--gray-700, #374151);
|
||||
}
|
||||
|
||||
.sidebar-icon.active {
|
||||
background: var(--gray-200, #e5e7eb);
|
||||
color: var(--primary-dark, #5661b3);
|
||||
box-shadow: inset 3px 0 0 var(--primary-dark, #5661b3);
|
||||
}
|
||||
|
||||
.sidebar-icon:focus-visible {
|
||||
outline: 2px solid var(--primary-dark, #5661b3);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.sidebar-panel {
|
||||
width: 280px;
|
||||
background: var(--gray-50, #f9fafb);
|
||||
border-right: 1px solid var(--gray-200, #e5e7eb);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sidebar.collapsed .sidebar-panel {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidebar-panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--gray-200, #e5e7eb);
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--gray-600, #4b5563);
|
||||
}
|
||||
|
||||
.sidebar-panel-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
color: var(--gray-400, #9ca3af);
|
||||
padding: 0 4px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.sidebar-panel-close:hover {
|
||||
background: var(--gray-200, #e5e7eb);
|
||||
color: var(--gray-600, #4b5563);
|
||||
}
|
||||
|
||||
.sidebar-panel-close:focus-visible {
|
||||
outline: 2px solid var(--primary-dark, #5661b3);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.sidebar-panel-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
/* Dark theme support */
|
||||
body[class*="dark"] .sidebar-icons,
|
||||
body[class*="dark"] .sidebar-panel {
|
||||
background: #1e1e1e;
|
||||
border-color: #333;
|
||||
}
|
||||
|
||||
body[class*="dark"] .sidebar-icon {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
body[class*="dark"] .sidebar-icon:hover {
|
||||
background: #333;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
body[class*="dark"] .sidebar-icon.active {
|
||||
background: #333;
|
||||
color: #8b9aff;
|
||||
box-shadow: inset 3px 0 0 #8b9aff;
|
||||
}
|
||||
|
||||
body[class*="dark"] .sidebar-panel-header {
|
||||
border-color: #333;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
/* Panel list items (templates, etc.) */
|
||||
.panel-list-item {
|
||||
padding: 10px 12px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.panel-list-item:hover {
|
||||
background: var(--gray-200, #e5e7eb);
|
||||
}
|
||||
.panel-list-item-title {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--gray-800, #1f2937);
|
||||
}
|
||||
.panel-list-item-desc {
|
||||
font-size: 11px;
|
||||
color: var(--gray-500, #6b7280);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
body[class*="dark"] .panel-list-item:hover {
|
||||
background: #333;
|
||||
}
|
||||
body[class*="dark"] .panel-list-item-title {
|
||||
color: #ddd;
|
||||
}
|
||||
body[class*="dark"] .panel-list-item-desc {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
/* Explorer Panel */
|
||||
.explorer-toolbar {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.explorer-path {
|
||||
flex: 1;
|
||||
padding: 6px 8px;
|
||||
border: 1px solid var(--gray-300, #d1d5db);
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
background: white;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.explorer-browse-btn {
|
||||
border: 1px solid var(--gray-300, #d1d5db);
|
||||
border-radius: 6px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
.tree-item {
|
||||
padding: 3px 0;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
user-select: none;
|
||||
}
|
||||
.tree-item:hover {
|
||||
background: var(--gray-100, #f3f4f6);
|
||||
border-radius: 4px;
|
||||
}
|
||||
.tree-icon {
|
||||
margin-right: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.tree-children {
|
||||
padding-left: 16px;
|
||||
}
|
||||
.tree-folder.collapsed .tree-children {
|
||||
display: none;
|
||||
}
|
||||
.tree-name {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* Git Panel */
|
||||
.git-section { margin-bottom: 16px; }
|
||||
.git-section-title { font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; color: var(--gray-500); margin-bottom: 8px; }
|
||||
.git-file { display: flex; align-items: center; padding: 4px 6px; border-radius: 4px; font-size: 13px; gap: 6px; }
|
||||
.git-file:hover { background: var(--gray-100, #f3f4f6); }
|
||||
.git-file-status { font-weight: 700; font-family: monospace; width: 16px; text-align: center; }
|
||||
.git-file-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.git-stage-btn { border: none; background: var(--gray-200); border-radius: 4px; cursor: pointer; font-size: 14px; width: 22px; height: 22px; }
|
||||
.git-commit-input { width: 100%; padding: 8px; border: 1px solid var(--gray-300); border-radius: 6px; font-size: 13px; font-family: inherit; resize: vertical; box-sizing: border-box; }
|
||||
.git-commit-btn { width: 100%; margin-top: 8px; padding: 8px; background: var(--primary-dark, #5661b3); color: white; border: none; border-radius: 6px; font-size: 13px; cursor: pointer; }
|
||||
.git-commit-btn:hover { opacity: 0.9; }
|
||||
.git-log-entry { padding: 6px 0; border-bottom: 1px solid var(--gray-100); }
|
||||
.git-log-msg { font-size: 13px; }
|
||||
.git-log-meta { font-size: 11px; color: var(--gray-500); margin-top: 2px; }
|
||||
.git-info { font-size: 13px; color: var(--gray-500); padding: 8px 0; }
|
||||
.git-loading { font-size: 13px; color: var(--gray-400); }
|
||||
|
||||
/* Snippets Panel */
|
||||
.snippets-toolbar { display: flex; gap: 4px; margin-bottom: 8px; }
|
||||
.snippets-search { flex: 1; padding: 6px 10px; border: 1px solid var(--gray-300); border-radius: 6px; font-size: 13px; }
|
||||
.snippets-add-btn { width: 32px; border: 1px solid var(--gray-300); border-radius: 6px; background: white; font-size: 18px; cursor: pointer; }
|
||||
.snippet-item { padding: 8px; border: 1px solid var(--gray-200); border-radius: 6px; margin-bottom: 6px; }
|
||||
.snippet-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px; }
|
||||
.snippet-name { font-size: 13px; font-weight: 500; }
|
||||
.snippet-lang { font-size: 11px; background: var(--gray-100); padding: 2px 6px; border-radius: 4px; color: var(--gray-500); }
|
||||
.snippet-preview { font-size: 12px; background: var(--gray-50); padding: 6px; border-radius: 4px; margin: 4px 0; overflow: hidden; max-height: 60px; }
|
||||
.snippet-preview code { font-family: 'JetBrains Mono', monospace; }
|
||||
.snippet-actions { display: flex; gap: 4px; }
|
||||
.snippet-insert { font-size: 12px; padding: 4px 8px; border: 1px solid var(--gray-300); border-radius: 4px; background: white; cursor: pointer; }
|
||||
.snippet-delete { font-size: 14px; padding: 4px 8px; border: 1px solid var(--gray-300); border-radius: 4px; background: white; cursor: pointer; color: #ef4444; }
|
||||
|
||||
/* Dark theme for sidebar panels */
|
||||
body[class*="dark"] .explorer-path,
|
||||
body[class*="dark"] .explorer-browse-btn,
|
||||
body[class*="dark"] .snippets-search,
|
||||
body[class*="dark"] .snippets-add-btn,
|
||||
body[class*="dark"] .snippet-insert,
|
||||
body[class*="dark"] .snippet-delete {
|
||||
background: #2d2d2d;
|
||||
border-color: #444;
|
||||
color: #ccc;
|
||||
}
|
||||
body[class*="dark"] .git-commit-input {
|
||||
background: #2d2d2d;
|
||||
border-color: #444;
|
||||
color: #ccc;
|
||||
}
|
||||
body[class*="dark"] .snippet-item {
|
||||
border-color: #444;
|
||||
}
|
||||
body[class*="dark"] .snippet-preview {
|
||||
background: #2d2d2d;
|
||||
}
|
||||
body[class*="dark"] .tree-item:hover,
|
||||
body[class*="dark"] .git-file:hover {
|
||||
background: #333;
|
||||
}
|
||||
|
||||
/* Outline Panel */
|
||||
.outline-panel { display: flex; flex-direction: column; height: 100%; }
|
||||
.outline-list { flex: 1; overflow-y: auto; padding: 4px 0; }
|
||||
.outline-item { display: flex; align-items: center; justify-content: space-between; padding: 4px 12px; cursor: pointer; font-size: 13px; color: var(--text-secondary, #6b7280); transition: background 0.15s, color 0.15s; border-left: 2px solid transparent; }
|
||||
.outline-item:hover { background: var(--bg-tertiary, #f3f4f6); color: var(--text-primary, #1f2937); }
|
||||
.outline-item.active { color: var(--accent-blue, #3b82f6); background: rgba(59, 130, 246, 0.08); border-left-color: var(--accent-blue, #3b82f6); font-weight: 600; }
|
||||
.outline-level-1 { padding-left: 12px; }
|
||||
.outline-level-2 { padding-left: 24px; }
|
||||
.outline-level-3 { padding-left: 36px; }
|
||||
.outline-level-4 { padding-left: 48px; }
|
||||
.outline-level-5 { padding-left: 56px; }
|
||||
.outline-level-6 { padding-left: 64px; }
|
||||
.outline-text { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.outline-badge { font-size: 10px; opacity: 0.5; margin-left: 8px; flex-shrink: 0; }
|
||||
.outline-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 32px 16px; color: var(--text-muted, #9ca3af); text-align: center; }
|
||||
.outline-empty p { margin: 4px 0; }
|
||||
.outline-hint { font-family: monospace; font-size: 12px; opacity: 0.7; }
|
||||
.outline-footer { padding: 8px 12px; border-top: 1px solid var(--border-color, #e5e7eb); font-size: 11px; color: var(--text-muted, #9ca3af); }
|
||||
|
||||
/* Outline dark mode */
|
||||
body[class*="dark"] .outline-item { color: #9ca3af; }
|
||||
body[class*="dark"] .outline-item:hover { background: #374151; color: #e5e7eb; }
|
||||
body[class*="dark"] .outline-item.active { color: #8b9aff; background: rgba(139, 154, 255, 0.1); border-left-color: #8b9aff; }
|
||||
body[class*="dark"] .outline-empty { color: #6b7280; }
|
||||
body[class*="dark"] .outline-footer { border-color: #333; color: #6b7280; }
|
||||
@@ -1,24 +0,0 @@
|
||||
.welcome-container { padding: 40px; max-width: 900px; margin: 0 auto; }
|
||||
.welcome-hero { text-align: center; margin-bottom: 40px; }
|
||||
.welcome-title { font-size: 32px; font-weight: 700; color: var(--gray-800, #1f2937); }
|
||||
.welcome-version { font-size: 14px; color: var(--primary-dark, #5661b3); margin-top: 4px; font-weight: 500; }
|
||||
.welcome-subtitle { font-size: 16px; color: var(--gray-500, #6b7280); margin-top: 8px; }
|
||||
.welcome-grid { display: grid; gap: 32px; }
|
||||
.welcome-section h2 { font-size: 18px; margin-bottom: 16px; color: var(--gray-700, #374151); }
|
||||
.welcome-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; }
|
||||
.welcome-card { padding: 20px; border: 1px solid var(--gray-200, #e5e7eb); border-radius: 12px; cursor: pointer; text-align: center; transition: all 0.2s; }
|
||||
.welcome-card:hover { border-color: var(--primary-dark, #5661b3); box-shadow: 0 4px 12px rgba(0,0,0,0.08); transform: translateY(-2px); }
|
||||
.welcome-card-icon { font-size: 28px; margin-bottom: 8px; }
|
||||
.welcome-card h3 { font-size: 15px; margin-bottom: 4px; }
|
||||
.welcome-card p { font-size: 13px; color: var(--gray-500); }
|
||||
.welcome-card kbd { display: inline-block; margin-top: 8px; padding: 2px 8px; background: var(--gray-100); border: 1px solid var(--gray-300); border-radius: 4px; font-size: 12px; font-family: 'JetBrains Mono', monospace; }
|
||||
.welcome-features { list-style: none; padding: 0; }
|
||||
.welcome-features li { padding: 6px 0; font-size: 14px; border-bottom: 1px solid var(--gray-100, #f3f4f6); }
|
||||
.welcome-features strong { color: var(--primary-dark, #5661b3); }
|
||||
.welcome-recent-item { padding: 8px 12px; border-radius: 6px; cursor: pointer; margin-bottom: 4px; }
|
||||
.welcome-recent-item:hover { background: var(--gray-100); }
|
||||
.welcome-recent-name { font-size: 14px; font-weight: 500; display: block; }
|
||||
.welcome-recent-path { font-size: 12px; color: var(--gray-500); display: block; margin-top: 2px; overflow: hidden; text-overflow: ellipsis; }
|
||||
.welcome-muted { color: var(--gray-400); font-size: 14px; }
|
||||
.welcome-footer { margin-top: 32px; text-align: center; }
|
||||
.welcome-checkbox { font-size: 13px; color: var(--gray-500); cursor: pointer; }
|
||||
@@ -1,102 +0,0 @@
|
||||
/* Zen Mode — Distraction-free writing styles */
|
||||
|
||||
/* Hide chrome elements */
|
||||
body.zen-mode .app-header,
|
||||
body.zen-mode .tab-bar,
|
||||
body.zen-mode .toolbar,
|
||||
body.zen-mode .status-bar,
|
||||
body.zen-mode .sidebar,
|
||||
body.zen-mode .breadcrumb-bar {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Main content fills viewport */
|
||||
body.zen-mode .main-content {
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Editor container takes full width */
|
||||
body.zen-mode .editor-container {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
/* Hide preview panes */
|
||||
body.zen-mode .preview-container,
|
||||
body.zen-mode .preview-pane,
|
||||
body.zen-mode .pane-resizer {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Center and style editor content for comfortable reading width */
|
||||
body.zen-mode .cm-content {
|
||||
max-width: 700px;
|
||||
margin: 0 auto;
|
||||
font-size: 18px;
|
||||
line-height: 1.8;
|
||||
padding-top: 80px;
|
||||
}
|
||||
|
||||
body.zen-mode .cm-line {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
body.zen-mode .editor-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Floating HUD pill */
|
||||
.zen-hud {
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
padding: 10px 24px;
|
||||
border-radius: 24px;
|
||||
font-size: 13px;
|
||||
font-family: 'Plus Jakarta Sans', system-ui, sans-serif;
|
||||
z-index: 9999;
|
||||
pointer-events: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.zen-hud-stat {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.zen-hud-sep {
|
||||
opacity: 0.3;
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
/* Word goal progress bar */
|
||||
.zen-progress-container {
|
||||
width: 120px;
|
||||
height: 4px;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-radius: 2px;
|
||||
margin-top: 8px;
|
||||
overflow: hidden;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.zen-progress-bar {
|
||||
height: 100%;
|
||||
background: rgba(255, 178, 70, 0.85);
|
||||
border-radius: 2px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
/* Dark mode — HUD stays dark in both themes */
|
||||
body[class*="dark"].zen-mode .zen-hud {
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
-3723
File diff suppressed because it is too large
Load Diff
@@ -1,545 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Table Generator - MarkdownConverter</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--ci-dark-gray: #464646;
|
||||
--ci-medium-gray: #9a9696;
|
||||
--ci-accent: #e5461f;
|
||||
--ci-light-gray: #e3e3e3;
|
||||
--ci-black: #0d0b09;
|
||||
--ci-white: #ffffff;
|
||||
--ci-bg: #f5f5f5;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', system-ui, sans-serif;
|
||||
background: var(--ci-bg);
|
||||
color: var(--ci-dark-gray);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: linear-gradient(135deg, var(--ci-dark-gray) 0%, var(--ci-black) 100%);
|
||||
padding: 16px 24px;
|
||||
border-bottom: 3px solid var(--ci-accent);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
color: var(--ci-white);
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 24px;
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.section {
|
||||
background: var(--ci-white);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 16px;
|
||||
color: var(--ci-dark-gray);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.controls-row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.control-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.control-label {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
color: var(--ci-medium-gray);
|
||||
}
|
||||
|
||||
.form-input, .form-select {
|
||||
padding: 8px 12px;
|
||||
border: 2px solid var(--ci-light-gray);
|
||||
border-radius: 6px;
|
||||
font-size: 0.875rem;
|
||||
font-family: inherit;
|
||||
transition: border-color 0.2s;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.form-input:focus, .form-select:focus {
|
||||
outline: none;
|
||||
border-color: var(--ci-accent);
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.checkbox-label input {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
accent-color: var(--ci-accent);
|
||||
}
|
||||
|
||||
.table-container {
|
||||
overflow-x: auto;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.editable-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.editable-table th,
|
||||
.editable-table td {
|
||||
border: 1px solid var(--ci-light-gray);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.editable-table th {
|
||||
background: var(--ci-dark-gray);
|
||||
color: var(--ci-white);
|
||||
}
|
||||
|
||||
.editable-table input {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
border: none;
|
||||
font-family: inherit;
|
||||
font-size: 0.875rem;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.editable-table input:focus {
|
||||
outline: 2px solid var(--ci-accent);
|
||||
outline-offset: -2px;
|
||||
background: rgba(229, 70, 31, 0.05);
|
||||
}
|
||||
|
||||
.editable-table th input {
|
||||
color: var(--ci-white);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.editable-table th input::placeholder {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.preview-container {
|
||||
background: var(--ci-black);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
max-height: 250px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.preview-content {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
color: #00ff00;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 16px 24px;
|
||||
background: var(--ci-white);
|
||||
border-top: 1px solid var(--ci-light-gray);
|
||||
}
|
||||
|
||||
.footer-left {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.footer-right {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
border-radius: 8px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--ci-accent);
|
||||
color: var(--ci-white);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #c93a18;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--ci-light-gray);
|
||||
color: var(--ci-dark-gray);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #d0d0d0;
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
background: transparent;
|
||||
border: 2px solid var(--ci-accent);
|
||||
color: var(--ci-accent);
|
||||
}
|
||||
|
||||
.btn-outline:hover {
|
||||
background: var(--ci-accent);
|
||||
color: var(--ci-white);
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 6px 12px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.quick-templates {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.template-chip {
|
||||
padding: 6px 12px;
|
||||
background: var(--ci-light-gray);
|
||||
border-radius: 20px;
|
||||
font-size: 0.75rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.template-chip:hover {
|
||||
background: var(--ci-accent);
|
||||
color: var(--ci-white);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>Table Generator</h1>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="section">
|
||||
<div class="section-title">Table Settings</div>
|
||||
<div class="controls-row">
|
||||
<div class="control-group">
|
||||
<span class="control-label">Rows</span>
|
||||
<input type="number" id="rows" class="form-input" min="1" max="50" value="4" style="width: 80px;">
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<span class="control-label">Columns</span>
|
||||
<input type="number" id="cols" class="form-input" min="1" max="20" value="4" style="width: 80px;">
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<span class="control-label">Alignment</span>
|
||||
<select id="alignment" class="form-select">
|
||||
<option value="left">Left</option>
|
||||
<option value="center">Center</option>
|
||||
<option value="right">Right</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="control-group" style="justify-content: flex-end;">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="has-header" checked>
|
||||
Include Header Row
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="quick-templates">
|
||||
<button class="template-chip" data-rows="3" data-cols="2">2-Column (3 rows)</button>
|
||||
<button class="template-chip" data-rows="5" data-cols="3">3-Column (5 rows)</button>
|
||||
<button class="template-chip" data-rows="4" data-cols="4">4x4 Grid</button>
|
||||
<button class="template-chip" data-rows="10" data-cols="3">Data Table (10 rows)</button>
|
||||
<button class="template-chip" data-rows="2" data-cols="5">Wide Table</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-title">Edit Table Content</div>
|
||||
<div class="table-container">
|
||||
<table class="editable-table" id="editable-table">
|
||||
<!-- Generated dynamically -->
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-title">Markdown Preview</div>
|
||||
<div class="preview-container">
|
||||
<div id="preview" class="preview-content"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<div class="footer-left">
|
||||
<button class="btn btn-secondary btn-sm" id="btn-clear">Clear All</button>
|
||||
<button class="btn btn-outline btn-sm" id="btn-fill-sample">Fill Sample Data</button>
|
||||
</div>
|
||||
<div class="footer-right">
|
||||
<button class="btn btn-secondary" id="btn-copy">Copy Markdown</button>
|
||||
<button class="btn btn-primary" id="btn-insert">Insert to Editor</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentRows = 4;
|
||||
let currentCols = 4;
|
||||
let hasHeader = true;
|
||||
let alignment = 'left';
|
||||
|
||||
// Generate the editable table
|
||||
function generateEditableTable() {
|
||||
const table = document.getElementById('editable-table');
|
||||
hasHeader = document.getElementById('has-header').checked;
|
||||
alignment = document.getElementById('alignment').value;
|
||||
|
||||
let html = '';
|
||||
|
||||
for (let i = 0; i < currentRows; i++) {
|
||||
html += '<tr>';
|
||||
for (let j = 0; j < currentCols; j++) {
|
||||
const isHeader = hasHeader && i === 0;
|
||||
const tag = isHeader ? 'th' : 'td';
|
||||
const placeholder = isHeader ? `Header ${j + 1}` : `Cell ${i},${j + 1}`;
|
||||
html += `<${tag}><input type="text" data-row="${i}" data-col="${j}" placeholder="${placeholder}"></${tag}>`;
|
||||
}
|
||||
html += '</tr>';
|
||||
}
|
||||
|
||||
table.innerHTML = html;
|
||||
|
||||
// Add event listeners for live preview
|
||||
table.querySelectorAll('input').forEach(input => {
|
||||
input.addEventListener('input', updatePreview);
|
||||
});
|
||||
|
||||
updatePreview();
|
||||
}
|
||||
|
||||
// Get table data
|
||||
function getTableData() {
|
||||
const data = [];
|
||||
for (let i = 0; i < currentRows; i++) {
|
||||
const row = [];
|
||||
for (let j = 0; j < currentCols; j++) {
|
||||
const input = document.querySelector(`input[data-row="${i}"][data-col="${j}"]`);
|
||||
row.push(input ? input.value || '' : '');
|
||||
}
|
||||
data.push(row);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
// Generate markdown table
|
||||
function generateMarkdown(data) {
|
||||
if (data.length === 0 || data[0].length === 0) return '';
|
||||
|
||||
// Calculate column widths
|
||||
const colWidths = [];
|
||||
for (let j = 0; j < data[0].length; j++) {
|
||||
let maxWidth = 3; // Minimum width for separator
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
maxWidth = Math.max(maxWidth, (data[i][j] || '').length);
|
||||
}
|
||||
colWidths.push(maxWidth);
|
||||
}
|
||||
|
||||
// Generate alignment function
|
||||
const alignCell = (text, width) => {
|
||||
text = text || '';
|
||||
const padding = width - text.length;
|
||||
if (alignment === 'center') {
|
||||
const left = Math.floor(padding / 2);
|
||||
const right = padding - left;
|
||||
return ' '.repeat(left) + text + ' '.repeat(right);
|
||||
} else if (alignment === 'right') {
|
||||
return ' '.repeat(padding) + text;
|
||||
}
|
||||
return text + ' '.repeat(padding);
|
||||
};
|
||||
|
||||
// Generate separator based on alignment
|
||||
const getSeparator = (width) => {
|
||||
if (alignment === 'center') {
|
||||
return ':' + '-'.repeat(width - 2) + ':';
|
||||
} else if (alignment === 'right') {
|
||||
return '-'.repeat(width - 1) + ':';
|
||||
}
|
||||
return '-'.repeat(width);
|
||||
};
|
||||
|
||||
let markdown = '';
|
||||
const startRow = hasHeader ? 0 : -1;
|
||||
|
||||
for (let i = startRow; i < data.length; i++) {
|
||||
if (i === -1) {
|
||||
// Generate empty header for tables without header
|
||||
markdown += '| ' + colWidths.map(w => ' '.repeat(w)).join(' | ') + ' |\n';
|
||||
} else {
|
||||
const row = data[i];
|
||||
const cells = row.map((cell, j) => alignCell(cell, colWidths[j]));
|
||||
markdown += '| ' + cells.join(' | ') + ' |\n';
|
||||
}
|
||||
|
||||
// Add separator after first row
|
||||
if ((hasHeader && i === 0) || (!hasHeader && i === -1)) {
|
||||
const separators = colWidths.map(w => getSeparator(w));
|
||||
markdown += '| ' + separators.join(' | ') + ' |\n';
|
||||
}
|
||||
}
|
||||
|
||||
return markdown;
|
||||
}
|
||||
|
||||
// Update preview
|
||||
function updatePreview() {
|
||||
const data = getTableData();
|
||||
const markdown = generateMarkdown(data);
|
||||
document.getElementById('preview').textContent = markdown;
|
||||
}
|
||||
|
||||
// Event listeners
|
||||
document.getElementById('rows').addEventListener('change', (e) => {
|
||||
currentRows = parseInt(e.target.value) || 4;
|
||||
generateEditableTable();
|
||||
});
|
||||
|
||||
document.getElementById('cols').addEventListener('change', (e) => {
|
||||
currentCols = parseInt(e.target.value) || 4;
|
||||
generateEditableTable();
|
||||
});
|
||||
|
||||
document.getElementById('alignment').addEventListener('change', () => {
|
||||
updatePreview();
|
||||
});
|
||||
|
||||
document.getElementById('has-header').addEventListener('change', () => {
|
||||
generateEditableTable();
|
||||
});
|
||||
|
||||
// Quick templates
|
||||
document.querySelectorAll('.template-chip').forEach(chip => {
|
||||
chip.addEventListener('click', () => {
|
||||
currentRows = parseInt(chip.dataset.rows);
|
||||
currentCols = parseInt(chip.dataset.cols);
|
||||
document.getElementById('rows').value = currentRows;
|
||||
document.getElementById('cols').value = currentCols;
|
||||
generateEditableTable();
|
||||
});
|
||||
});
|
||||
|
||||
// Clear all
|
||||
document.getElementById('btn-clear').addEventListener('click', () => {
|
||||
document.querySelectorAll('#editable-table input').forEach(input => {
|
||||
input.value = '';
|
||||
});
|
||||
updatePreview();
|
||||
});
|
||||
|
||||
// Fill sample data
|
||||
document.getElementById('btn-fill-sample').addEventListener('click', () => {
|
||||
const sampleHeaders = ['Name', 'Description', 'Status', 'Priority', 'Due Date'];
|
||||
const sampleData = [
|
||||
['Task 1', 'First task description', 'In Progress', 'High', '2024-01-15'],
|
||||
['Task 2', 'Second task description', 'Completed', 'Medium', '2024-01-10'],
|
||||
['Task 3', 'Third task description', 'Pending', 'Low', '2024-01-20'],
|
||||
['Task 4', 'Fourth task description', 'In Review', 'High', '2024-01-18']
|
||||
];
|
||||
|
||||
for (let i = 0; i < currentRows; i++) {
|
||||
for (let j = 0; j < currentCols; j++) {
|
||||
const input = document.querySelector(`input[data-row="${i}"][data-col="${j}"]`);
|
||||
if (input) {
|
||||
if (hasHeader && i === 0) {
|
||||
input.value = sampleHeaders[j % sampleHeaders.length];
|
||||
} else {
|
||||
const dataRow = hasHeader ? i - 1 : i;
|
||||
if (dataRow < sampleData.length && j < sampleData[dataRow].length) {
|
||||
input.value = sampleData[dataRow][j];
|
||||
} else {
|
||||
input.value = `Data ${i}-${j + 1}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
updatePreview();
|
||||
});
|
||||
|
||||
// Copy markdown
|
||||
document.getElementById('btn-copy').addEventListener('click', () => {
|
||||
const markdown = document.getElementById('preview').textContent;
|
||||
navigator.clipboard.writeText(markdown).then(() => {
|
||||
const btn = document.getElementById('btn-copy');
|
||||
btn.textContent = 'Copied!';
|
||||
setTimeout(() => {
|
||||
btn.textContent = 'Copy Markdown';
|
||||
}, 2000);
|
||||
});
|
||||
});
|
||||
|
||||
// Insert to editor
|
||||
document.getElementById('btn-insert').addEventListener('click', () => {
|
||||
const content = document.getElementById('preview').textContent;
|
||||
if (content && window.electronAPI) {
|
||||
window.electronAPI.send('insert-generated-content', '\n' + content + '\n');
|
||||
window.close();
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize
|
||||
generateEditableTable();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,78 +0,0 @@
|
||||
function createWelcomeContent(recentFiles = [], appVersion = '') {
|
||||
const recentHtml = recentFiles.length
|
||||
? recentFiles.map(f => {
|
||||
const name = f.split(/[/\\]/).pop();
|
||||
return `<div class="welcome-recent-item" data-path="${f}"><span class="welcome-recent-name">${name}</span><span class="welcome-recent-path">${f}</span></div>`;
|
||||
}).join('')
|
||||
: '<p class="welcome-muted">No recent files</p>';
|
||||
|
||||
return `
|
||||
<div class="welcome-container">
|
||||
<div class="welcome-hero">
|
||||
<h1 class="welcome-title">MarkdownConverter</h1>
|
||||
<p class="welcome-version">${appVersion ? `Version ${appVersion}` : ''}</p>
|
||||
<p class="welcome-subtitle">Professional Markdown Editor & Universal Document Converter</p>
|
||||
</div>
|
||||
|
||||
<div class="welcome-grid">
|
||||
<div class="welcome-section">
|
||||
<h2>Quick Start</h2>
|
||||
<div class="welcome-cards">
|
||||
<div class="welcome-card" data-action="new-file">
|
||||
<div class="welcome-card-icon">+</div>
|
||||
<h3>New Document</h3>
|
||||
<p>Create a blank document</p>
|
||||
<kbd>Ctrl+N</kbd>
|
||||
</div>
|
||||
<div class="welcome-card" data-action="open-file">
|
||||
<div class="welcome-card-icon">📂</div>
|
||||
<h3>Open File</h3>
|
||||
<p>Open an existing file</p>
|
||||
<kbd>Ctrl+O</kbd>
|
||||
</div>
|
||||
<div class="welcome-card" data-action="open-template">
|
||||
<div class="welcome-card-icon">📋</div>
|
||||
<h3>From Template</h3>
|
||||
<p>Start from a template</p>
|
||||
</div>
|
||||
<div class="welcome-card" data-action="command-palette">
|
||||
<div class="welcome-card-icon">⌘</div>
|
||||
<h3>Command Palette</h3>
|
||||
<p>Search all actions</p>
|
||||
<kbd>Ctrl+Shift+P</kbd>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="welcome-section">
|
||||
<h2>What's New in v4.x</h2>
|
||||
<ul class="welcome-features">
|
||||
<li><strong>CodeMirror Editor</strong> — Syntax highlighting, code folding, multiple cursors</li>
|
||||
<li><strong>Sidebar Panels</strong> — File Explorer, Git, Snippets, Templates</li>
|
||||
<li><strong>Command Palette</strong> — Quick access to all actions</li>
|
||||
<li><strong>Code Execution</strong> — Run JS, Python, Bash from code blocks</li>
|
||||
<li><strong>Print Preview</strong> — Full print customization dialog</li>
|
||||
<li><strong>New Formats</strong> — Reveal.js, YAML, JSON, Confluence, and more</li>
|
||||
<li><strong>Spell Checking</strong> — System dictionary with suggestions</li>
|
||||
<li><strong>Markdown Extensions</strong> — Footnotes, admonitions, TOC</li>
|
||||
<li><strong>Image Handling</strong> — Paste and drag-drop images</li>
|
||||
<li><strong>PlantUML</strong> — Diagram rendering in preview</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="welcome-section">
|
||||
<h2>Recent Files</h2>
|
||||
<div class="welcome-recent">${recentHtml}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="welcome-footer">
|
||||
<label class="welcome-checkbox">
|
||||
<input type="checkbox" id="show-welcome-startup" checked>
|
||||
Show this tab on startup
|
||||
</label>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
module.exports = { createWelcomeContent };
|
||||
@@ -1,743 +0,0 @@
|
||||
/**
|
||||
* Word Template Exporter
|
||||
* Loads word_template.docx, preserves first 2 pages (cover + TOC),
|
||||
* and adds markdown content starting from page 3 using template styles
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const PizZip = require('pizzip');
|
||||
const Docx = require('docx4js').default;
|
||||
|
||||
class WordTemplateExporter {
|
||||
constructor(templatePath, startPage = 3, pageSettings = null) {
|
||||
this.templatePath = templatePath || path.join(__dirname, '../word_template.docx');
|
||||
this.startPage = startPage; // Which page to start inserting content
|
||||
this.pageSettings = pageSettings; // Page size and orientation settings
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert markdown to Word document using template
|
||||
*/
|
||||
async convert(markdownContent, outputPath) {
|
||||
try {
|
||||
// Load template
|
||||
const templateBuffer = fs.readFileSync(this.templatePath);
|
||||
const zip = new PizZip(templateBuffer);
|
||||
|
||||
// Extract document.xml
|
||||
let documentXml = zip.file('word/document.xml').asText();
|
||||
|
||||
// Set page size if settings provided
|
||||
if (this.pageSettings) {
|
||||
documentXml = this.setPageSize(documentXml);
|
||||
}
|
||||
|
||||
// Parse markdown and generate Word XML
|
||||
const newContentXml = this.markdownToWordXml(markdownContent);
|
||||
|
||||
// Insert new content after the specified start page
|
||||
const modifiedXml = this.insertContentAfterPage(documentXml, newContentXml, this.startPage);
|
||||
|
||||
// Update the zip with modified XML
|
||||
zip.file('word/document.xml', modifiedXml);
|
||||
|
||||
// Generate and save the new document
|
||||
const newDocBuffer = zip.generate({ type: 'nodebuffer' });
|
||||
fs.writeFileSync(outputPath, newDocBuffer);
|
||||
|
||||
return outputPath;
|
||||
} catch (error) {
|
||||
console.error('Error in Word export:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set page size in document XML
|
||||
*/
|
||||
setPageSize(documentXml) {
|
||||
// Import PAGE_SIZES from main process (need to pass as parameter)
|
||||
const PAGE_SIZES = {
|
||||
a4: { width: 11906, height: 16838 },
|
||||
a3: { width: 16838, height: 23811 },
|
||||
a5: { width: 8391, height: 11906 },
|
||||
b4: { width: 14170, height: 20015 },
|
||||
b5: { width: 9979, height: 14170 },
|
||||
letter: { width: 12240, height: 15840 },
|
||||
legal: { width: 12240, height: 20160 },
|
||||
tabloid: { width: 15840, height: 24480 }
|
||||
};
|
||||
|
||||
let width, height;
|
||||
const pageSize = PAGE_SIZES[this.pageSettings.size];
|
||||
|
||||
if (pageSize) {
|
||||
width = pageSize.width;
|
||||
height = pageSize.height;
|
||||
} else {
|
||||
// Default to A4
|
||||
width = 11906;
|
||||
height = 16838;
|
||||
}
|
||||
|
||||
// Swap dimensions for landscape
|
||||
if (this.pageSettings.orientation === 'landscape') {
|
||||
[width, height] = [height, width];
|
||||
}
|
||||
|
||||
// Update all <w:pgSz> elements in section properties
|
||||
const pgSzRegex = /<w:pgSz[^>]*\/>/g;
|
||||
let modifiedXml = documentXml.replace(pgSzRegex, () => {
|
||||
return `<w:pgSz w:w="${width}" w:h="${height}" w:orient="${this.pageSettings.orientation}"/>`;
|
||||
});
|
||||
|
||||
// If no pgSz found, add it to all sectPr elements
|
||||
if (!pgSzRegex.test(documentXml)) {
|
||||
const sectPrRegex = /<w:sectPr[^>]*>/g;
|
||||
modifiedXml = modifiedXml.replace(sectPrRegex, (match) => {
|
||||
return `${match}<w:pgSz w:w="${width}" w:h="${height}" w:orient="${this.pageSettings.orientation}"/>`;
|
||||
});
|
||||
}
|
||||
|
||||
return modifiedXml;
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert markdown content after the specified page in the document
|
||||
* @param {string} documentXml - The document XML
|
||||
* @param {string} newContentXml - The new content to insert
|
||||
* @param {number} afterPage - Insert content after this page number (1-based)
|
||||
*/
|
||||
insertContentAfterPage(documentXml, newContentXml, afterPage) {
|
||||
// Find section breaks that mark page boundaries
|
||||
// Look for the section properties tag that marks page breaks
|
||||
const sectionBreakRegex = /<w:sectPr[^>]*>[\s\S]*?<\/w:sectPr>/g;
|
||||
const matches = [...documentXml.matchAll(sectionBreakRegex)];
|
||||
|
||||
// Calculate which section break to insert after
|
||||
// Page 1 = before 1st section break
|
||||
// Page 2 = after 1st section break
|
||||
// Page 3 = after 2nd section break, etc.
|
||||
const sectionIndex = afterPage - 1;
|
||||
|
||||
if (matches.length >= sectionIndex && sectionIndex > 0) {
|
||||
// Insert after the specified section break
|
||||
const insertPoint = matches[sectionIndex - 1].index + matches[sectionIndex - 1][0].length;
|
||||
return documentXml.slice(0, insertPoint) + newContentXml + documentXml.slice(insertPoint);
|
||||
} else if (afterPage === 1 || matches.length === 0) {
|
||||
// Insert at the beginning (after <w:body>) or if no section breaks found
|
||||
const bodyStart = documentXml.indexOf('<w:body>') + 8;
|
||||
return documentXml.slice(0, bodyStart) + newContentXml + documentXml.slice(bodyStart);
|
||||
} else {
|
||||
// If not enough section breaks, insert before closing body tag
|
||||
return documentXml.replace('</w:body>', newContentXml + '</w:body>');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert markdown to Word XML format
|
||||
*/
|
||||
markdownToWordXml(markdown) {
|
||||
const lines = markdown.split('\n');
|
||||
let xml = '';
|
||||
let inCodeBlock = false;
|
||||
let codeLines = [];
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
|
||||
// Handle code blocks
|
||||
if (line.trim().startsWith('```')) {
|
||||
if (inCodeBlock) {
|
||||
// End code block
|
||||
xml += this.createCodeBlockXml(codeLines.join('\n'));
|
||||
codeLines = [];
|
||||
inCodeBlock = false;
|
||||
} else {
|
||||
inCodeBlock = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inCodeBlock) {
|
||||
codeLines.push(line);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Empty lines
|
||||
if (!line.trim()) {
|
||||
xml += '<w:p><w:pPr></w:pPr></w:p>';
|
||||
continue;
|
||||
}
|
||||
|
||||
// Tables - detect table lines
|
||||
if (line.includes('|') && line.trim().startsWith('|')) {
|
||||
const tableLines = [line];
|
||||
i++;
|
||||
// Collect all consecutive table lines
|
||||
while (i < lines.length && lines[i].includes('|')) {
|
||||
tableLines.push(lines[i]);
|
||||
i++;
|
||||
}
|
||||
i--; // Back up one line
|
||||
xml += this.createTableXml(tableLines);
|
||||
continue;
|
||||
}
|
||||
|
||||
// ASCII flowcharts/diagrams - detect box drawing characters
|
||||
if (this.isAsciiArt(line)) {
|
||||
const asciiLines = [line];
|
||||
i++;
|
||||
// Collect all consecutive ASCII art lines
|
||||
while (i < lines.length && (this.isAsciiArt(lines[i]) || !lines[i].trim())) {
|
||||
asciiLines.push(lines[i]);
|
||||
i++;
|
||||
if (i < lines.length && lines[i].trim() && !this.isAsciiArt(lines[i])) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
i--; // Back up one line
|
||||
xml += this.createAsciiArtXml(asciiLines.join('\n'));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Headings - strip markdown numbering
|
||||
if (line.trim().startsWith('#')) {
|
||||
const level = (line.match(/^#+/) || [''])[0].length;
|
||||
let text = line.replace(/^#+\s*/, '').trim();
|
||||
// Remove markdown numbering like "1.1 Title" -> "Title"
|
||||
text = text.replace(/^\d+(\.\d+)*\.?\s+/, '');
|
||||
xml += this.createHeadingXml(text, level);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Blockquotes
|
||||
if (line.trim().startsWith('>')) {
|
||||
const text = line.replace(/^>\s*/, '').trim();
|
||||
xml += this.createQuoteXml(text);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ordered lists - strip markdown numbering, use template numbering
|
||||
if (/^\s*\d+\.\s+/.test(line)) {
|
||||
const text = line.replace(/^\s*\d+\.\s+/, '');
|
||||
xml += this.createListItemXml(text, true);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Unordered lists
|
||||
if (/^\s*[-*+]\s+/.test(line)) {
|
||||
const text = line.replace(/^\s*[-*+]\s+/, '');
|
||||
xml += this.createListItemXml(text, false);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Horizontal rule
|
||||
if (/^[-*_]{3,}$/.test(line.trim())) {
|
||||
xml += this.createHorizontalRuleXml();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Normal paragraph
|
||||
xml += this.createParagraphXml(line);
|
||||
}
|
||||
|
||||
return xml;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create heading XML using template styles
|
||||
*/
|
||||
createHeadingXml(text, level) {
|
||||
const styleName = `Heading${level}`;
|
||||
const runs = this.parseInlineFormatting(text);
|
||||
|
||||
return `<w:p>
|
||||
<w:pPr>
|
||||
<w:pStyle w:val="${styleName}"/>
|
||||
</w:pPr>
|
||||
${runs}
|
||||
</w:p>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create paragraph XML with Normal style
|
||||
*/
|
||||
createParagraphXml(text) {
|
||||
const runs = this.parseInlineFormatting(text);
|
||||
|
||||
return `<w:p>
|
||||
<w:pPr>
|
||||
<w:pStyle w:val="Normal"/>
|
||||
</w:pPr>
|
||||
${runs}
|
||||
</w:p>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create quote XML
|
||||
*/
|
||||
createQuoteXml(text) {
|
||||
const runs = this.parseInlineFormatting(text);
|
||||
|
||||
return `<w:p>
|
||||
<w:pPr>
|
||||
<w:pStyle w:val="Quote"/>
|
||||
</w:pPr>
|
||||
${runs}
|
||||
</w:p>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create list item XML using template numbering
|
||||
*/
|
||||
createListItemXml(text, numbered) {
|
||||
const runs = this.parseInlineFormatting(text);
|
||||
const numId = numbered ? '1' : '2'; // Template numbering IDs
|
||||
|
||||
return `<w:p>
|
||||
<w:pPr>
|
||||
<w:pStyle w:val="${numbered ? 'ListNumber' : 'ListBullet'}"/>
|
||||
<w:numPr>
|
||||
<w:ilvl w:val="0"/>
|
||||
<w:numId w:val="${numId}"/>
|
||||
</w:numPr>
|
||||
</w:pPr>
|
||||
${runs}
|
||||
</w:p>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create code block XML - renders each line as separate paragraph
|
||||
* to preserve exact formatting like in preview (monospace, no wrapping)
|
||||
* Background shading applied at paragraph level to extend full width
|
||||
*/
|
||||
createCodeBlockXml(code) {
|
||||
const lines = code.split('\n');
|
||||
let xml = '';
|
||||
|
||||
lines.forEach((line, index) => {
|
||||
const escapedLine = this.escapeXml(line);
|
||||
|
||||
// Each line gets its own paragraph with exact spacing and no wrapping
|
||||
// Shading (shd) is at paragraph level so background extends full width
|
||||
xml += `<w:p>
|
||||
<w:pPr>
|
||||
<w:pStyle w:val="Normal"/>
|
||||
<w:shd w:val="clear" w:color="auto" w:fill="F5F5F5"/>
|
||||
<w:spacing w:before="${index === 0 ? '120' : '0'}" w:after="${index === lines.length - 1 ? '120' : '0'}" w:line="240" w:lineRule="exact"/>
|
||||
<w:ind w:left="284" w:right="284"/>
|
||||
<w:jc w:val="left"/>
|
||||
<w:keepLines/>
|
||||
<w:wordWrap w:val="0"/>
|
||||
</w:pPr>
|
||||
<w:r>
|
||||
<w:rPr>
|
||||
<w:rFonts w:ascii="Consolas" w:hAnsi="Consolas" w:cs="Consolas"/>
|
||||
<w:sz w:val="18"/>
|
||||
<w:szCs w:val="18"/>
|
||||
<w:noProof/>
|
||||
</w:rPr>
|
||||
<w:t xml:space="preserve">${escapedLine}</w:t>
|
||||
</w:r>
|
||||
</w:p>`;
|
||||
});
|
||||
|
||||
return xml;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create horizontal rule XML
|
||||
*/
|
||||
createHorizontalRuleXml() {
|
||||
return `<w:p>
|
||||
<w:pPr>
|
||||
<w:pBdr>
|
||||
<w:bottom w:val="single" w:sz="6" w:space="1" w:color="auto"/>
|
||||
</w:pBdr>
|
||||
</w:pPr>
|
||||
</w:p>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create table XML from markdown table lines with template styling
|
||||
* Uses full-width tables with equal column distribution matching template style
|
||||
*/
|
||||
createTableXml(tableLines) {
|
||||
// Parse table
|
||||
const rows = [];
|
||||
for (const line of tableLines) {
|
||||
// Skip separator lines (e.g., |---|---|)
|
||||
if (/^\s*\|[\s\-:]+\|\s*$/.test(line)) {
|
||||
continue;
|
||||
}
|
||||
// Split by | and trim
|
||||
const cells = line.split('|').filter(cell => cell.trim()).map(cell => cell.trim());
|
||||
if (cells.length > 0) {
|
||||
rows.push(cells);
|
||||
}
|
||||
}
|
||||
|
||||
if (rows.length === 0) return '';
|
||||
|
||||
// Calculate number of columns
|
||||
const numCols = Math.max(...rows.map(row => row.length));
|
||||
|
||||
// Calculate column width in twips (1440 twips = 1 inch)
|
||||
// Assume standard page width of 9360 twips (6.5 inches usable)
|
||||
const totalTableWidth = 9360;
|
||||
const colWidth = Math.floor(totalTableWidth / numCols);
|
||||
|
||||
// Build table XML
|
||||
let tableXml = '<w:tbl>';
|
||||
|
||||
// Table properties - use template table style with full width
|
||||
tableXml += `<w:tblPr>
|
||||
<w:tblStyle w:val="TableGrid"/>
|
||||
<w:tblW w:w="0" w:type="auto"/>
|
||||
<w:tblLayout w:type="fixed"/>
|
||||
<w:tblLook w:val="04A0" w:firstRow="1" w:lastRow="0" w:firstColumn="0" w:lastColumn="0" w:noHBand="0" w:noVBand="1"/>
|
||||
<w:tblBorders>
|
||||
<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:bottom w:val="single" w:sz="8" w:space="0" w:color="F58220"/>
|
||||
<w:right w:val="single" w:sz="8" w:space="0" w:color="F58220"/>
|
||||
<w:insideH w:val="single" w:sz="8" w:space="0" w:color="F58220"/>
|
||||
<w:insideV w:val="single" w:sz="8" w:space="0" w:color="F58220"/>
|
||||
</w:tblBorders>
|
||||
</w:tblPr>`;
|
||||
|
||||
// Table grid with explicit column widths
|
||||
tableXml += '<w:tblGrid>';
|
||||
for (let i = 0; i < numCols; i++) {
|
||||
tableXml += `<w:gridCol w:w="${colWidth}"/>`;
|
||||
}
|
||||
tableXml += '</w:tblGrid>';
|
||||
|
||||
// Table rows
|
||||
rows.forEach((rowCells, rowIndex) => {
|
||||
const isHeader = rowIndex === 0;
|
||||
|
||||
tableXml += '<w:tr>';
|
||||
|
||||
// Row properties for consistent height
|
||||
tableXml += '<w:trPr><w:trHeight w:val="0" w:hRule="atLeast"/></w:trPr>';
|
||||
|
||||
// Pad row to have same number of columns
|
||||
while (rowCells.length < numCols) {
|
||||
rowCells.push('');
|
||||
}
|
||||
|
||||
rowCells.forEach((cellText, colIndex) => {
|
||||
tableXml += '<w:tc>';
|
||||
tableXml += '<w:tcPr>';
|
||||
|
||||
// Cell width
|
||||
tableXml += `<w:tcW w:w="${colWidth}" w:type="dxa"/>`;
|
||||
|
||||
// Cell shading - orange for header, white for data rows
|
||||
if (isHeader) {
|
||||
tableXml += '<w:shd w:val="clear" w:color="auto" w:fill="F58220"/>';
|
||||
} else {
|
||||
tableXml += '<w:shd w:val="clear" w:color="auto" w:fill="FFFFFF"/>';
|
||||
}
|
||||
|
||||
// Cell borders
|
||||
tableXml += '<w:tcBorders>' +
|
||||
'<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:bottom w:val="single" w:sz="8" w:space="0" w:color="F58220"/>' +
|
||||
'<w:right w:val="single" w:sz="8" w:space="0" w:color="F58220"/>' +
|
||||
'</w:tcBorders>';
|
||||
|
||||
// Cell margins for proper padding
|
||||
tableXml += '<w:tcMar>' +
|
||||
'<w:top w:w="80" w:type="dxa"/>' +
|
||||
'<w:left w:w="120" w:type="dxa"/>' +
|
||||
'<w:bottom w:w="80" w:type="dxa"/>' +
|
||||
'<w:right w:w="120" w:type="dxa"/>' +
|
||||
'</w:tcMar>';
|
||||
|
||||
tableXml += '</w:tcPr>';
|
||||
|
||||
// Cell content
|
||||
tableXml += '<w:p>';
|
||||
tableXml += '<w:pPr>';
|
||||
tableXml += '<w:spacing w:before="0" w:after="0" w:line="240" w:lineRule="auto"/>';
|
||||
tableXml += '</w:pPr>';
|
||||
|
||||
const runs = this.parseInlineFormatting(cellText);
|
||||
|
||||
if (isHeader) {
|
||||
// Header: bold white text
|
||||
tableXml += runs.replace(/<w:rPr>/g, '<w:rPr><w:b/><w:color w:val="FFFFFF"/>');
|
||||
} else {
|
||||
// Data rows: normal black text
|
||||
tableXml += runs;
|
||||
}
|
||||
|
||||
tableXml += '</w:p>';
|
||||
tableXml += '</w:tc>';
|
||||
});
|
||||
|
||||
tableXml += '</w:tr>';
|
||||
});
|
||||
|
||||
tableXml += '</w:tbl>';
|
||||
|
||||
// Add spacing after table
|
||||
tableXml += '<w:p><w:pPr><w:spacing w:before="120" w:after="0"/></w:pPr></w:p>';
|
||||
|
||||
return tableXml;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect if line contains ASCII art/flowchart characters
|
||||
*/
|
||||
isAsciiArt(line) {
|
||||
// Don't treat markdown tables as ASCII art
|
||||
if (line.trim().startsWith('|') && line.trim().endsWith('|')) {
|
||||
// Check if it's a proper markdown table (has multiple cells)
|
||||
const cells = line.split('|').filter(c => c.trim());
|
||||
if (cells.length >= 2) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Common ASCII art characters (Unicode box drawing and symbols)
|
||||
const asciiArtChars = [
|
||||
'─', '│', '┌', '┐', '└', '┘', '├', '┤', '┬', '┴', '┼', // Box drawing
|
||||
'═', '║', '╔', '╗', '╚', '╝', '╠', '╣', '╦', '╩', '╬', // Double box
|
||||
'╭', '╮', '╯', '╰', // Rounded corners
|
||||
'▲', '▼', '◄', '►', '♦', '●', '○', '■', '□', '◆', '◇', // Shapes
|
||||
'↓', '→', '←', '↑', '↔', '↕', '⇒', '⇐', '⇓', '⇑', // Arrows
|
||||
'┃', '━', '┏', '┓', '┗', '┛', '┣', '┫', '┳', '┻', '╋', // Heavy box
|
||||
'░', '▒', '▓', '█', // Shading
|
||||
];
|
||||
|
||||
// Check for box drawing characters
|
||||
if (asciiArtChars.some(char => line.includes(char))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for ASCII box patterns with regular characters
|
||||
const asciiPatterns = [
|
||||
/^\s*\+[-=_+]+\+/, // +-----+ or +=====+
|
||||
/^\s*\|[-=_]{3,}\|/, // |-----|
|
||||
/^\s*[-=_]{5,}$/, // -----
|
||||
/^\s*\+[-]+\+[-]+\+/, // +---+---+
|
||||
/^\s*\([A-Z][a-z]+\)\s*\|\|/, // (Deformable) ||
|
||||
/^\s*\|\s+[A-Z\s]+[-:]\s+[A-Z]/, // | TILE ADHESIVE - TYPE
|
||||
/^\s*\|\s*\[[^\]]+\]/, // | [BIS Mark / CE Mark]
|
||||
/^\s*\|\s{2,}\w+.*\|\|/, // || text ||
|
||||
/^\s*\[[^\]]+\]\s*$/, // [Step in brackets]
|
||||
/^\s*<[-=]+>/, // <----> or <====>
|
||||
/^\s*\/[-_\\\/]+\//, // /----/
|
||||
/^\s*\*[-=\*]+\*/, // *----*
|
||||
];
|
||||
|
||||
return asciiPatterns.some(pattern => pattern.test(line));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create ASCII art XML with monospace font
|
||||
* Background shading applied at paragraph level to extend full width
|
||||
*/
|
||||
createAsciiArtXml(asciiContent) {
|
||||
// Split ASCII art into individual lines and create a paragraph for each
|
||||
const lines = asciiContent.split('\n');
|
||||
let xml = '';
|
||||
|
||||
lines.forEach((line, index) => {
|
||||
// Shading (shd) is at paragraph level so background extends full width
|
||||
xml += `<w:p>
|
||||
<w:pPr>
|
||||
<w:pStyle w:val="Normal"/>
|
||||
<w:shd w:val="clear" w:color="auto" w:fill="F5F5F5"/>
|
||||
<w:spacing w:before="${index === 0 ? '120' : '0'}" w:after="${index === lines.length - 1 ? '120' : '0'}" w:line="240" w:lineRule="exact"/>
|
||||
<w:ind w:left="284" w:right="284"/>
|
||||
<w:jc w:val="left"/>
|
||||
<w:keepLines/>
|
||||
<w:wordWrap w:val="0"/>
|
||||
</w:pPr>`;
|
||||
|
||||
// Check if line contains arrow characters and color them red
|
||||
const arrowChars = ['↓', '→', '←', '↑', '▼', '►', '◄', '▲'];
|
||||
const hasArrow = arrowChars.some(arrow => line.includes(arrow));
|
||||
|
||||
if (hasArrow) {
|
||||
// Split line into parts and color arrows red
|
||||
let remainingLine = line;
|
||||
|
||||
while (remainingLine.length > 0) {
|
||||
let foundArrow = false;
|
||||
let arrowIndex = -1;
|
||||
let foundArrowChar = '';
|
||||
|
||||
// Find the first arrow in the remaining text
|
||||
for (const arrow of arrowChars) {
|
||||
const idx = remainingLine.indexOf(arrow);
|
||||
if (idx !== -1 && (arrowIndex === -1 || idx < arrowIndex)) {
|
||||
arrowIndex = idx;
|
||||
foundArrowChar = arrow;
|
||||
foundArrow = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (foundArrow) {
|
||||
// Add text before arrow (if any)
|
||||
if (arrowIndex > 0) {
|
||||
const beforeArrow = this.escapeXml(remainingLine.substring(0, arrowIndex));
|
||||
xml += `<w:r>
|
||||
<w:rPr>
|
||||
<w:rFonts w:ascii="Consolas" w:hAnsi="Consolas" w:cs="Consolas"/>
|
||||
<w:sz w:val="16"/>
|
||||
<w:szCs w:val="16"/>
|
||||
<w:noProof/>
|
||||
</w:rPr>
|
||||
<w:t xml:space="preserve">${beforeArrow}</w:t>
|
||||
</w:r>`;
|
||||
}
|
||||
|
||||
// Add arrow in red
|
||||
const escapedArrow = this.escapeXml(foundArrowChar);
|
||||
xml += `<w:r>
|
||||
<w:rPr>
|
||||
<w:rFonts w:ascii="Consolas" w:hAnsi="Consolas" w:cs="Consolas"/>
|
||||
<w:sz w:val="16"/>
|
||||
<w:szCs w:val="16"/>
|
||||
<w:color w:val="FF0000"/>
|
||||
<w:noProof/>
|
||||
</w:rPr>
|
||||
<w:t xml:space="preserve">${escapedArrow}</w:t>
|
||||
</w:r>`;
|
||||
|
||||
// Continue with remaining text
|
||||
remainingLine = remainingLine.substring(arrowIndex + foundArrowChar.length);
|
||||
} else {
|
||||
// No more arrows, add remaining text
|
||||
const escapedRemaining = this.escapeXml(remainingLine);
|
||||
xml += `<w:r>
|
||||
<w:rPr>
|
||||
<w:rFonts w:ascii="Consolas" w:hAnsi="Consolas" w:cs="Consolas"/>
|
||||
<w:sz w:val="16"/>
|
||||
<w:szCs w:val="16"/>
|
||||
<w:noProof/>
|
||||
</w:rPr>
|
||||
<w:t xml:space="preserve">${escapedRemaining}</w:t>
|
||||
</w:r>`;
|
||||
remainingLine = '';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No arrows, just add the line normally
|
||||
const escapedLine = this.escapeXml(line);
|
||||
xml += `<w:r>
|
||||
<w:rPr>
|
||||
<w:rFonts w:ascii="Consolas" w:hAnsi="Consolas" w:cs="Consolas"/>
|
||||
<w:sz w:val="16"/>
|
||||
<w:szCs w:val="16"/>
|
||||
<w:noProof/>
|
||||
</w:rPr>
|
||||
<w:t xml:space="preserve">${escapedLine}</w:t>
|
||||
</w:r>`;
|
||||
}
|
||||
|
||||
xml += `</w:p>`;
|
||||
});
|
||||
|
||||
return xml;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse inline formatting (bold, italic, code)
|
||||
*/
|
||||
parseInlineFormatting(text) {
|
||||
let xml = '';
|
||||
const pos = 0;
|
||||
|
||||
// Patterns for inline formatting
|
||||
const patterns = [
|
||||
{ regex: /\*\*\*(.+?)\*\*\*/g, bold: true, italic: true },
|
||||
{ regex: /\*\*(.+?)\*\*/g, bold: true },
|
||||
{ regex: /\*(.+?)\*/g, italic: true },
|
||||
{ regex: /`(.+?)`/g, code: true }
|
||||
];
|
||||
|
||||
// Simple approach: process text sequentially
|
||||
let remaining = text;
|
||||
|
||||
while (remaining.length > 0) {
|
||||
let foundMatch = false;
|
||||
let earliestPos = remaining.length;
|
||||
let matchedPattern = null;
|
||||
let match = null;
|
||||
|
||||
// Find earliest match
|
||||
for (const pattern of patterns) {
|
||||
pattern.regex.lastIndex = 0;
|
||||
const m = pattern.regex.exec(remaining);
|
||||
if (m && m.index < earliestPos) {
|
||||
earliestPos = m.index;
|
||||
matchedPattern = pattern;
|
||||
match = m;
|
||||
foundMatch = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (foundMatch) {
|
||||
// Add text before match
|
||||
if (earliestPos > 0) {
|
||||
xml += this.createRunXml(remaining.substring(0, earliestPos));
|
||||
}
|
||||
|
||||
// Add formatted text
|
||||
xml += this.createRunXml(match[1], matchedPattern.bold, matchedPattern.italic, matchedPattern.code);
|
||||
|
||||
remaining = remaining.substring(earliestPos + match[0].length);
|
||||
} else {
|
||||
// No more matches, add remaining text
|
||||
xml += this.createRunXml(remaining);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return xml;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a run (text segment) XML
|
||||
*/
|
||||
createRunXml(text, bold = false, italic = false, code = false) {
|
||||
if (!text) return '';
|
||||
|
||||
const escapedText = this.escapeXml(text);
|
||||
let propsXml = '<w:rPr>';
|
||||
|
||||
if (bold) propsXml += '<w:b/>';
|
||||
if (italic) propsXml += '<w:i/>';
|
||||
if (code) {
|
||||
propsXml += '<w:rFonts w:ascii="Consolas" w:hAnsi="Consolas"/>';
|
||||
propsXml += '<w:sz w:val="20"/>';
|
||||
}
|
||||
|
||||
propsXml += '</w:rPr>';
|
||||
|
||||
return `<w:r>${propsXml}<w:t xml:space="preserve">${escapedText}</w:t></w:r>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape XML special characters
|
||||
*/
|
||||
escapeXml(text) {
|
||||
return text
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = WordTemplateExporter;
|
||||
-292
@@ -1,292 +0,0 @@
|
||||
/**
|
||||
* Zen Mode — distraction-free writing mode
|
||||
* Provides typewriter scrolling, line dimming, centered content, and a floating HUD.
|
||||
* Toggle with F11, exit with Escape.
|
||||
*/
|
||||
|
||||
const {
|
||||
EditorView,
|
||||
ViewPlugin,
|
||||
Decoration,
|
||||
DecorationSet,
|
||||
} = require('@codemirror/view');
|
||||
const { RangeSetBuilder } = require('@codemirror/state');
|
||||
|
||||
class ZenMode {
|
||||
/**
|
||||
* @param {import('./renderer').TabManager} tabManager
|
||||
*/
|
||||
constructor(tabManager) {
|
||||
this.tabManager = tabManager;
|
||||
this.active = false;
|
||||
this._hud = null;
|
||||
this._timerInterval = null;
|
||||
this._sessionStart = null;
|
||||
this._extensions = [];
|
||||
}
|
||||
|
||||
activate() {
|
||||
if (this.active) return;
|
||||
this.active = true;
|
||||
|
||||
document.body.classList.add('zen-mode');
|
||||
|
||||
// Collapse sidebar
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
if (sidebar) sidebar.classList.add('collapsed');
|
||||
|
||||
// Hide preview pane for the active tab
|
||||
const tab = this.tabManager.tabs.get(this.tabManager.activeTabId);
|
||||
if (tab) {
|
||||
const previewContainer = document.getElementById(`preview-pane-${tab.id}`);
|
||||
if (previewContainer) previewContainer.classList.add('hidden');
|
||||
const editorPane = document.getElementById(`editor-pane-${tab.id}`);
|
||||
if (editorPane) editorPane.classList.add('full-width');
|
||||
|
||||
// Add CM6 extensions to the active editor
|
||||
if (tab.editorView) {
|
||||
this._extensions = [
|
||||
typewriterScrollExtension,
|
||||
lineDimmingExtension,
|
||||
];
|
||||
// Reconfigure is not straightforward with CM6's immutable state.
|
||||
// Instead we dispatch effects or simply rebuild with new extensions.
|
||||
// Since we cannot hot-swap extensions on an existing EditorView,
|
||||
// we store a reference and the CSS + updateListener handle the rest.
|
||||
// The typewriter + dimming plugins are applied via a compartment approach
|
||||
// would be ideal, but for simplicity we use DOM + updateListener.
|
||||
this._applyTypewriterBehavior(tab.editorView);
|
||||
}
|
||||
}
|
||||
|
||||
// Create HUD
|
||||
this._createHUD();
|
||||
|
||||
// Start session timer
|
||||
this._sessionStart = Date.now();
|
||||
this._timerInterval = setInterval(() => this._updateHUD(), 1000);
|
||||
|
||||
// Focus editor
|
||||
if (tab?.editorView) tab.editorView.focus();
|
||||
}
|
||||
|
||||
deactivate() {
|
||||
if (!this.active) return;
|
||||
this.active = false;
|
||||
|
||||
document.body.classList.remove('zen-mode');
|
||||
|
||||
// Restore preview pane for the active tab
|
||||
const tab = this.tabManager.tabs.get(this.tabManager.activeTabId);
|
||||
if (tab) {
|
||||
const previewContainer = document.getElementById(`preview-pane-${tab.id}`);
|
||||
if (previewContainer) previewContainer.classList.remove('hidden');
|
||||
const editorPane = document.getElementById(`editor-pane-${tab.id}`);
|
||||
if (editorPane) editorPane.classList.remove('full-width');
|
||||
|
||||
// Remove typewriter listener
|
||||
if (tab.editorView && this._selectionListener) {
|
||||
window.removeEventListener('zen-typewriter', this._selectionListener);
|
||||
this._selectionListener = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove HUD
|
||||
if (this._hud && this._hud.parentNode) {
|
||||
this._hud.parentNode.removeChild(this._hud);
|
||||
this._hud = null;
|
||||
}
|
||||
|
||||
// Stop timer
|
||||
if (this._timerInterval) {
|
||||
clearInterval(this._timerInterval);
|
||||
this._timerInterval = null;
|
||||
}
|
||||
this._sessionStart = null;
|
||||
this._extensions = [];
|
||||
}
|
||||
|
||||
toggle() {
|
||||
if (this.active) {
|
||||
this.deactivate();
|
||||
} else {
|
||||
this.activate();
|
||||
}
|
||||
}
|
||||
|
||||
_applyTypewriterBehavior(view) {
|
||||
// We use a custom update listener that scrolls the cursor to center.
|
||||
// This is stored on the instance so we can clean it up.
|
||||
const scrollTimeout = null;
|
||||
|
||||
const scrollFn = () => {
|
||||
if (!this.active) return;
|
||||
const pos = view.state.selection.main.head;
|
||||
requestAnimationFrame(() => {
|
||||
view.dispatch({
|
||||
effects: EditorView.scrollIntoView(pos, { y: 'center' }),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Listen for selection/doc changes via a polling approach
|
||||
// since we cannot add extensions to an existing view.
|
||||
// Instead, use the DOM scrollIntoView after selection changes.
|
||||
const observer = new MutationObserver(() => {
|
||||
// No-op, we use interval below
|
||||
});
|
||||
|
||||
// Use interval to keep cursor centered during typing
|
||||
this._scrollInterval = setInterval(() => {
|
||||
if (!this.active) {
|
||||
clearInterval(this._scrollInterval);
|
||||
this._scrollInterval = null;
|
||||
return;
|
||||
}
|
||||
// Typewriter scroll: center the cursor line
|
||||
try {
|
||||
const pos = view.state.selection.main.head;
|
||||
const coords = view.coordsAtPos(pos);
|
||||
const scroller = view.scrollDOM;
|
||||
if (coords && scroller) {
|
||||
const scrollerRect = scroller.getBoundingClientRect();
|
||||
const cursorCenter = coords.top + (coords.bottom - coords.top) / 2;
|
||||
const viewCenter = scrollerRect.top + scrollerRect.height / 2;
|
||||
const diff = cursorCenter - viewCenter;
|
||||
if (Math.abs(diff) > 30) {
|
||||
scroller.scrollTop += diff;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore errors from destroyed views
|
||||
}
|
||||
}, 200);
|
||||
|
||||
// Initial scroll to center
|
||||
scrollFn();
|
||||
}
|
||||
|
||||
_createHUD() {
|
||||
const hud = document.createElement('div');
|
||||
hud.className = 'zen-hud';
|
||||
hud.innerHTML = `
|
||||
<span class="zen-hud-item" id="zen-words">0 words</span>
|
||||
<span class="zen-hud-sep">·</span>
|
||||
<span class="zen-hud-item" id="zen-reading-time">0 min read</span>
|
||||
<span class="zen-hud-sep">·</span>
|
||||
<span class="zen-hud-item" id="zen-timer">00:00</span>
|
||||
<div class="zen-progress-container" id="zen-progress-container">
|
||||
<div class="zen-progress-bar" id="zen-progress-bar"></div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(hud);
|
||||
this._hud = hud;
|
||||
this._updateHUD();
|
||||
}
|
||||
|
||||
_updateHUD() {
|
||||
if (!this.active || !this._hud) return;
|
||||
|
||||
const content = this.tabManager.getEditorContent();
|
||||
const words = content.trim() ? content.trim().split(/\s+/).filter(w => w.length > 0).length : 0;
|
||||
const readingTime = Math.max(1, Math.ceil(words / 200));
|
||||
|
||||
const wordsEl = document.getElementById('zen-words');
|
||||
const readingEl = document.getElementById('zen-reading-time');
|
||||
const timerEl = document.getElementById('zen-timer');
|
||||
|
||||
if (wordsEl) wordsEl.textContent = `${words.toLocaleString()} words`;
|
||||
if (readingEl) readingEl.textContent = `${readingTime} min read`;
|
||||
|
||||
// Session timer
|
||||
if (this._sessionStart) {
|
||||
const elapsed = Math.floor((Date.now() - this._sessionStart) / 1000);
|
||||
const minutes = String(Math.floor(elapsed / 60)).padStart(2, '0');
|
||||
const seconds = String(elapsed % 60).padStart(2, '0');
|
||||
if (timerEl) timerEl.textContent = `${minutes}:${seconds}`;
|
||||
}
|
||||
|
||||
// Word goal progress
|
||||
const goalStr = localStorage.getItem('zen-word-goal');
|
||||
const progressBar = document.getElementById('zen-progress-bar');
|
||||
const progressContainer = document.getElementById('zen-progress-container');
|
||||
if (progressBar && progressContainer) {
|
||||
if (goalStr) {
|
||||
const goal = parseInt(goalStr, 10);
|
||||
if (goal > 0) {
|
||||
const pct = Math.min(100, Math.round((words / goal) * 100));
|
||||
progressBar.style.width = `${pct}%`;
|
||||
progressContainer.style.display = 'block';
|
||||
} else {
|
||||
progressContainer.style.display = 'none';
|
||||
}
|
||||
} else {
|
||||
progressContainer.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Typewriter scroll ViewPlugin
|
||||
const typewriterScrollExtension = ViewPlugin.fromClass(class {
|
||||
constructor(view) {
|
||||
this.view = view;
|
||||
}
|
||||
|
||||
update(update) {
|
||||
if (update.selectionSet || update.docChanged) {
|
||||
const pos = update.state.selection.main.head;
|
||||
requestAnimationFrame(() => {
|
||||
if (this.view.dom && this.view.dom.isConnected) {
|
||||
this.view.dispatch({
|
||||
effects: EditorView.scrollIntoView(pos, { y: 'center' }),
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Line dimming ViewPlugin
|
||||
function getOpacity(distance) {
|
||||
if (distance === 0) return 1.0;
|
||||
if (distance === 1) return 0.7;
|
||||
if (distance === 2) return 0.6;
|
||||
if (distance === 3) return 0.45;
|
||||
return 0.3;
|
||||
}
|
||||
|
||||
const lineDimmingExtension = ViewPlugin.fromClass(class {
|
||||
constructor(view) {
|
||||
this.decorations = this.buildDecorations(view);
|
||||
}
|
||||
|
||||
update(update) {
|
||||
if (update.docChanged || update.selectionSet || update.viewportChanged) {
|
||||
this.decorations = this.buildDecorations(update.view);
|
||||
}
|
||||
}
|
||||
|
||||
buildDecorations(view) {
|
||||
const builder = new RangeSetBuilder();
|
||||
const pos = view.state.selection.main.head;
|
||||
const activeLine = view.state.doc.lineAt(pos).number;
|
||||
const doc = view.state.doc;
|
||||
|
||||
for (let lineNum = 1; lineNum <= doc.lines; lineNum++) {
|
||||
const line = doc.line(lineNum);
|
||||
const distance = Math.abs(lineNum - activeLine);
|
||||
const opacity = getOpacity(distance);
|
||||
const deco = Decoration.line({
|
||||
attributes: { style: `opacity:${opacity};transition:opacity 0.15s ease` },
|
||||
});
|
||||
builder.add(line.from, line.from, deco);
|
||||
}
|
||||
|
||||
return builder.finish();
|
||||
}
|
||||
}, {
|
||||
decorations: v => v.decorations,
|
||||
});
|
||||
|
||||
module.exports = { ZenMode };
|
||||
@@ -1,144 +0,0 @@
|
||||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
|
||||
/**
|
||||
* Tests for CommandPalette
|
||||
* Tests command registration, filtering, execution, and keyboard navigation
|
||||
*/
|
||||
|
||||
describe('CommandPalette', () => {
|
||||
let CommandPalette;
|
||||
|
||||
beforeEach(() => {
|
||||
document.body.innerHTML = `
|
||||
<div class="command-palette-overlay hidden" id="command-palette-overlay">
|
||||
<div class="command-palette">
|
||||
<input type="text" id="command-palette-input">
|
||||
<div id="command-palette-results"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
CommandPalette = require('../src/command-palette').CommandPalette;
|
||||
});
|
||||
|
||||
test('starts hidden', () => {
|
||||
const palette = new CommandPalette();
|
||||
expect(palette.isOpen()).toBe(false);
|
||||
});
|
||||
|
||||
test('opens and focuses input', () => {
|
||||
const palette = new CommandPalette();
|
||||
palette.open();
|
||||
expect(palette.isOpen()).toBe(true);
|
||||
});
|
||||
|
||||
test('closes', () => {
|
||||
const palette = new CommandPalette();
|
||||
palette.open();
|
||||
palette.close();
|
||||
expect(palette.isOpen()).toBe(false);
|
||||
});
|
||||
|
||||
test('registers and renders commands', () => {
|
||||
const palette = new CommandPalette();
|
||||
palette.register('Test Command', 'Ctrl+T', () => {});
|
||||
palette.register('Another Command', '', () => {});
|
||||
palette.open();
|
||||
const items = document.querySelectorAll('.command-item');
|
||||
expect(items.length).toBe(2);
|
||||
});
|
||||
|
||||
test('filters commands by search', () => {
|
||||
const palette = new CommandPalette();
|
||||
palette.register('Save File', 'Ctrl+S', () => {});
|
||||
palette.register('Open File', 'Ctrl+O', () => {});
|
||||
palette.register('Bold Text', 'Ctrl+B', () => {});
|
||||
palette.renderResults('file');
|
||||
const items = document.querySelectorAll('.command-item');
|
||||
expect(items.length).toBe(2);
|
||||
});
|
||||
|
||||
test('executes command', () => {
|
||||
const palette = new CommandPalette();
|
||||
const action = jest.fn();
|
||||
palette.register('Test', '', action);
|
||||
palette.open();
|
||||
palette.executeSelected();
|
||||
expect(action).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('highlights matching text', () => {
|
||||
const palette = new CommandPalette();
|
||||
const result = palette.highlightMatch('Save File', 'save');
|
||||
expect(result).toContain('<strong>');
|
||||
expect(result).toContain('Save');
|
||||
});
|
||||
|
||||
test('highlightMatch returns original text when no query', () => {
|
||||
const palette = new CommandPalette();
|
||||
const result = palette.highlightMatch('Save File', '');
|
||||
expect(result).toBe('Save File');
|
||||
});
|
||||
|
||||
test('filters case-insensitively', () => {
|
||||
const palette = new CommandPalette();
|
||||
palette.register('Save File', '', () => {});
|
||||
palette.register('Open File', '', () => {});
|
||||
palette.renderResults('SAVE');
|
||||
const items = document.querySelectorAll('.command-item');
|
||||
expect(items.length).toBe(1);
|
||||
});
|
||||
|
||||
test('shows all commands when query is empty', () => {
|
||||
const palette = new CommandPalette();
|
||||
palette.register('Cmd1', '', () => {});
|
||||
palette.register('Cmd2', '', () => {});
|
||||
palette.register('Cmd3', '', () => {});
|
||||
palette.renderResults('');
|
||||
const items = document.querySelectorAll('.command-item');
|
||||
expect(items.length).toBe(3);
|
||||
});
|
||||
|
||||
test('displays shortcut when provided', () => {
|
||||
const palette = new CommandPalette();
|
||||
palette.register('Save File', 'Ctrl+S', () => {});
|
||||
palette.open();
|
||||
const shortcut = document.querySelector('.command-shortcut');
|
||||
expect(shortcut).not.toBeNull();
|
||||
expect(shortcut.textContent).toBe('Ctrl+S');
|
||||
});
|
||||
|
||||
test('does not display shortcut when empty', () => {
|
||||
const palette = new CommandPalette();
|
||||
palette.register('Test Command', '', () => {});
|
||||
palette.open();
|
||||
const shortcut = document.querySelector('.command-shortcut');
|
||||
expect(shortcut).toBeNull();
|
||||
});
|
||||
|
||||
test('closes on overlay click', () => {
|
||||
const palette = new CommandPalette();
|
||||
palette.open();
|
||||
// Simulate click on the overlay itself
|
||||
const event = new Event('click', { bubbles: true });
|
||||
Object.defineProperty(event, 'target', { value: palette.overlay });
|
||||
palette.overlay.dispatchEvent(event);
|
||||
expect(palette.isOpen()).toBe(false);
|
||||
});
|
||||
|
||||
test('executeSelected does nothing when no commands', () => {
|
||||
const palette = new CommandPalette();
|
||||
palette.open();
|
||||
// Should not throw
|
||||
expect(() => palette.executeSelected()).not.toThrow();
|
||||
});
|
||||
|
||||
test('closes after executing command', () => {
|
||||
const palette = new CommandPalette();
|
||||
palette.register('Test', '', jest.fn());
|
||||
palette.open();
|
||||
palette.executeSelected();
|
||||
expect(palette.isOpen()).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -48,8 +48,6 @@ describe('Preload Security', () => {
|
||||
'process-pdf-operation',
|
||||
'get-pdf-page-count',
|
||||
'select-pdf-folder',
|
||||
'open-ascii-generator',
|
||||
'open-table-generator',
|
||||
'insert-generated-content',
|
||||
'save-pasted-image',
|
||||
'load-template',
|
||||
@@ -96,7 +94,6 @@ describe('Preload Security', () => {
|
||||
'show-export-dialog',
|
||||
'show-batch-dialog',
|
||||
'show-universal-converter-dialog',
|
||||
'show-table-generator',
|
||||
'show-pdf-editor-dialog',
|
||||
'show-image-converter',
|
||||
'show-audio-converter',
|
||||
@@ -119,13 +116,9 @@ describe('Preload Security', () => {
|
||||
'pdf-operation-complete',
|
||||
'pdf-operation-error',
|
||||
'pdf-operation-progress',
|
||||
'show-ascii-generator-window',
|
||||
'show-ascii-generator',
|
||||
'show-table-generator-window',
|
||||
'open-header-footer-dialog',
|
||||
'insert-content',
|
||||
'load-template-menu',
|
||||
'toggle-command-palette',
|
||||
'toggle-sidebar-panel',
|
||||
'toggle-bottom-panel'
|
||||
];
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
|
||||
/**
|
||||
* Tests for PrintPreview
|
||||
* Tests print dialog options, open/close, and preview rendering
|
||||
*/
|
||||
|
||||
describe('PrintPreview', () => {
|
||||
beforeEach(() => {
|
||||
// Mock electron require for executePrint
|
||||
jest.mock('electron', () => ({
|
||||
ipcRenderer: { send: jest.fn(), invoke: jest.fn() }
|
||||
}), { virtual: true });
|
||||
|
||||
document.body.innerHTML = `
|
||||
<div class="dialog-overlay hidden" id="print-preview-overlay">
|
||||
<button id="print-preview-close"></button>
|
||||
<button id="print-cancel"></button>
|
||||
<button id="print-execute"></button>
|
||||
<select id="print-paper-size"><option value="A4">A4</option><option value="Letter">Letter</option></select>
|
||||
<select id="print-orientation"><option value="portrait">Portrait</option><option value="landscape">Landscape</option></select>
|
||||
<select id="print-margins"><option value="default">Default</option></select>
|
||||
<input type="range" id="print-scale" value="100">
|
||||
<span id="print-scale-value">100%</span>
|
||||
<input type="checkbox" id="print-headers" checked>
|
||||
<input type="checkbox" id="print-background" checked>
|
||||
<select id="print-pages"><option value="all">All</option><option value="custom">Custom</option></select>
|
||||
<input type="text" id="print-page-range" class="hidden">
|
||||
<iframe id="print-preview-frame"></iframe>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
test('getOptions returns default values', () => {
|
||||
const { PrintPreview } = require('../src/print-preview');
|
||||
const preview = new PrintPreview();
|
||||
const options = preview.getOptions();
|
||||
expect(options.paperSize).toBe('A4');
|
||||
expect(options.orientation).toBe('portrait');
|
||||
expect(options.scale).toBe(100);
|
||||
expect(options.headers).toBe(true);
|
||||
expect(options.background).toBe(true);
|
||||
expect(options.pages).toBe('all');
|
||||
expect(options.margins).toBe('default');
|
||||
expect(options.pageRange).toBe('');
|
||||
});
|
||||
|
||||
test('opens and closes', () => {
|
||||
const { PrintPreview } = require('../src/print-preview');
|
||||
const preview = new PrintPreview();
|
||||
preview.open('<p>Test</p>');
|
||||
expect(document.getElementById('print-preview-overlay').classList.contains('hidden')).toBe(false);
|
||||
preview.close();
|
||||
expect(document.getElementById('print-preview-overlay').classList.contains('hidden')).toBe(true);
|
||||
});
|
||||
|
||||
test('updateScaleLabel reflects slider value', () => {
|
||||
const { PrintPreview } = require('../src/print-preview');
|
||||
const preview = new PrintPreview();
|
||||
document.getElementById('print-scale').value = '75';
|
||||
preview.updateScaleLabel();
|
||||
expect(document.getElementById('print-scale-value').textContent).toBe('75%');
|
||||
});
|
||||
|
||||
test('close button closes preview', () => {
|
||||
const { PrintPreview } = require('../src/print-preview');
|
||||
const preview = new PrintPreview();
|
||||
preview.open('<p>Test</p>');
|
||||
document.getElementById('print-preview-close').click();
|
||||
expect(document.getElementById('print-preview-overlay').classList.contains('hidden')).toBe(true);
|
||||
});
|
||||
|
||||
test('cancel button closes preview', () => {
|
||||
const { PrintPreview } = require('../src/print-preview');
|
||||
const preview = new PrintPreview();
|
||||
preview.open('<p>Test</p>');
|
||||
document.getElementById('print-cancel').click();
|
||||
expect(document.getElementById('print-preview-overlay').classList.contains('hidden')).toBe(true);
|
||||
});
|
||||
|
||||
test('getOptions reflects changed values', () => {
|
||||
const { PrintPreview } = require('../src/print-preview');
|
||||
const preview = new PrintPreview();
|
||||
|
||||
// Change paper size to Letter
|
||||
const paperSelect = document.getElementById('print-paper-size');
|
||||
paperSelect.value = 'Letter';
|
||||
|
||||
// Change scale
|
||||
document.getElementById('print-scale').value = '50';
|
||||
|
||||
// Uncheck headers
|
||||
document.getElementById('print-headers').checked = false;
|
||||
|
||||
const options = preview.getOptions();
|
||||
expect(options.paperSize).toBe('Letter');
|
||||
expect(options.scale).toBe(50);
|
||||
expect(options.headers).toBe(false);
|
||||
});
|
||||
|
||||
test('stores last content for refresh', () => {
|
||||
const { PrintPreview } = require('../src/print-preview');
|
||||
const preview = new PrintPreview();
|
||||
preview.open('<p>Hello World</p>');
|
||||
expect(preview._lastContent).toBe('<p>Hello World</p>');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user